diff --git a/src/App.jsx b/src/App.jsx index 2d73f48..94af4f6 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -2,7 +2,8 @@ import { useEffect, useState } from 'react'; import { NearContext } from './context'; import { Wallet } from './services/near-wallet'; import Navbar from './components/Navbar'; -import { EthereumView } from './components/Ethereum/Ethereum'; +import { EthereumView } from './components/EVM/Ethereum'; +import { BaseView } from './components/EVM/Base'; import { BitcoinView } from './components/Bitcoin'; import { MPC_CONTRACT } from './services/kdf/mpc'; @@ -68,6 +69,7 @@ function App() { onChange={(e) => setChain(e.target.value)} > + @@ -75,6 +77,9 @@ function App() { {chain === 'eth' && ( )} + {chain === 'base' && ( + + )} {chain === 'btc' && ( )} diff --git a/src/components/EVM/Base.jsx b/src/components/EVM/Base.jsx new file mode 100644 index 0000000..598858b --- /dev/null +++ b/src/components/EVM/Base.jsx @@ -0,0 +1,325 @@ +import { useState, useEffect, useContext, useRef } from "react"; +import PropTypes from "prop-types"; + +import { NearContext } from "../../context"; +import { useDebounce } from "../../hooks/debounce"; +import { getTransactionHashes } from "../../services/utils"; +import { TransferForm } from "./Transfer"; +import { FunctionCallForm } from "./FunctionCall"; +import { EthereumVM } from "../../services/evm"; + +const Evm = new EthereumVM("https://base-sepolia.drpc.org"); + +const contractAddress = "0xCd3b988b216790C598d9AB85Eee189e446CE526D"; + +const transactions = getTransactionHashes(); + +export function BaseView({ props: { setStatus, MPC_CONTRACT } }) { + const { wallet, signedAccountId } = useContext(NearContext); + + const [loading, setLoading] = useState(false); + const [step, setStep] = useState(transactions ? "relay" : "request"); + const [signedTransaction, setSignedTransaction] = useState(null); + const [senderLabel, setSenderLabel] = useState(""); + const [senderAddress, setSenderAddress] = useState(""); + const [balance, setBalance] = useState(""); // Add balance state + const [action, setAction] = useState("transfer"); + const [derivation, setDerivation] = useState( + sessionStorage.getItem("derivation") || "ethereum-1" + ); + const [reloaded, setReloaded] = useState(transactions.length); + + const [gasPriceInGwei, setGasPriceInGwei] = useState(""); + const [txCost, setTxCost] = useState(""); + + const derivationPath = useDebounce(derivation, 1200); + const childRef = useRef(); + + useEffect(() => { + async function fetchEthereumGasPrice() { + try { + // Fetch gas price in Wei + const gasPriceInWei = await Evm.web3.eth.getGasPrice(); + + // Convert gas price from Wei to Gwei + const gasPriceInGwei = Evm.web3.utils.fromWei(gasPriceInWei, "gwei"); + + // Gas limit for a standard ETH transfer + const gasLimit = 21000; + + // Calculate transaction cost in ETH (gwei * gasLimit) / 1e9 + const txCost = (gasPriceInGwei * gasLimit) / 1000000000; + + // Format both gas price and transaction cost to 7 decimal places + const formattedGasPriceInGwei = parseFloat(gasPriceInGwei).toFixed(7); + const formattedTxCost = parseFloat(txCost).toFixed(7); + + console.log( + `Current Sepolia Gas Price: ${formattedGasPriceInGwei} Gwei` + ); + console.log(`Estimated Transaction Cost: ${formattedTxCost} ETH`); + + setTxCost(formattedTxCost); + setGasPriceInGwei(formattedGasPriceInGwei); + } catch (error) { + console.error("Error fetching gas price:", error); + } + } + + fetchEthereumGasPrice(); + }, []); + + // Handle signing transaction when the page is reloaded and senderAddress is set + useEffect(() => { + if (reloaded && senderAddress) { + signTransaction(); + } + + async function signTransaction() { + const { big_r, s, recovery_id } = await wallet.getTransactionResult( + transactions[0] + ); + const signedTransaction = await Evm.reconstructSignedTXFromLocalSession( + big_r, + s, + recovery_id, + senderAddress + ); + + setSignedTransaction(signedTransaction); + setStatus( + "✅ Signed payload ready to be relayed to the Base network" + ); + setStep("relay"); + + setReloaded(false); + removeUrlParams(); + } + }, [senderAddress, reloaded, wallet, setStatus]); + + // Handle changes to derivation path and query Base address and balance + useEffect(() => { + resetAddressState(); + fetchEthereumAddress(); + }, [derivationPath, signedAccountId]); + + const resetAddressState = () => { + setSenderLabel("Waiting for you to stop typing..."); + setSenderAddress(null); + setStatus(""); + setBalance(""); // Reset balance when derivation path changes + setStep("request"); + }; + + const fetchEthereumAddress = async () => { + const { address } = await Evm.deriveAddress( + signedAccountId, + derivationPath + ); + setSenderAddress(address); + setSenderLabel(address); + + if (!reloaded) { + const balance = await Evm.getBalance(address); + setBalance(balance); // Update balance state + } + }; + + async function chainSignature() { + setStatus("🏗️ Creating transaction"); + + const { transaction } = await childRef.current.createTransaction(); + + setStatus( + `🕒 Asking ${MPC_CONTRACT} to sign the transaction, this might take a while` + ); + try { + // to reconstruct on reload + sessionStorage.setItem("derivation", derivationPath); + + const { big_r, s, recovery_id } = await Evm.requestSignatureToMPC({ + wallet, + path: derivationPath, + transaction, + }); + const signedTransaction = await Evm.reconstructSignedTransaction( + big_r, + s, + recovery_id, + transaction + ); + + setSignedTransaction(signedTransaction); + setStatus( + `✅ Signed payload ready to be relayed to the Base network` + ); + setStep("relay"); + } catch (e) { + setStatus(`❌ Error: ${e.message}`); + setLoading(false); + } + } + + async function relayTransaction() { + setLoading(true); + setStatus( + "🔗 Relaying transaction to the Base network... this might take a while" + ); + + try { + const txHash = await Evm.broadcastTX(signedTransaction); + setStatus( + <> + + {" "} + ✅ Successful{" "} + + + ); + childRef.current.afterRelay(); + } catch (e) { + setStatus(`❌ Error: ${e.message}`); + } + + setStep("request"); + setLoading(false); + } + + const UIChainSignature = async () => { + setLoading(true); + await chainSignature(); + setLoading(false); + }; + + function removeUrlParams() { + const url = new URL(window.location.href); + url.searchParams.delete("transactionHashes"); + window.history.replaceState({}, document.title, url); + } + + return ( + <> + {/* Form Inputs */} +
+ +
+
+ +
+ + PATH + + setDerivation(e.target.value)} + disabled={loading} + /> +
+ + {/* ADDRESS & BALANCE */} +
+
+ +
+
+ {senderLabel} +
+
+
+
+ +
+
+ {balance ? ( + `${balance} ETH` + ) : ( + Fetching balance... + )} +
+
+
+
+ +
+ + ACTION + + +
+ + {action === "transfer" ? ( + + ) : ( + + )} + +
+
+ + + + + + + + + + + + + + + + + + +
+ Sepolia Gas Prices +
PriceUnit
{gasPriceInGwei}GWEI
{txCost}ETH
+
+
+ + {/* Execute Buttons */} +
+ {step === "request" && ( + + )} + {step === "relay" && ( + + )} +
+ + ); +} + +BaseView.propTypes = { + props: PropTypes.shape({ + setStatus: PropTypes.func.isRequired, + MPC_CONTRACT: PropTypes.string.isRequired, + }).isRequired, +}; diff --git a/src/components/Ethereum/Ethereum.jsx b/src/components/EVM/Ethereum.jsx similarity index 91% rename from src/components/Ethereum/Ethereum.jsx rename to src/components/EVM/Ethereum.jsx index 1c13828..e6b61f3 100644 --- a/src/components/Ethereum/Ethereum.jsx +++ b/src/components/EVM/Ethereum.jsx @@ -2,14 +2,15 @@ import { useState, useEffect, useContext, useRef } from "react"; import PropTypes from "prop-types"; import { NearContext } from "../../context"; -import { Ethereum } from "../../services/ethereum"; import { useDebounce } from "../../hooks/debounce"; import { getTransactionHashes } from "../../services/utils"; import { TransferForm } from "./Transfer"; import { FunctionCallForm } from "./FunctionCall"; +import { EthereumVM } from "../../services/evm"; -const Sepolia = 11155111; -const Eth = new Ethereum("https://sepolia.drpc.org", Sepolia); +const Evm = new EthereumVM("https://sepolia.drpc.org"); + +const contractAddress = "0xe2a01146FFfC8432497ae49A7a6cBa5B9Abd71A3"; const transactions = getTransactionHashes(); @@ -38,10 +39,10 @@ export function EthereumView({ props: { setStatus, MPC_CONTRACT } }) { async function fetchEthereumGasPrice() { try { // Fetch gas price in Wei - const gasPriceInWei = await Eth.web3.eth.getGasPrice(); + const gasPriceInWei = await Evm.web3.eth.getGasPrice(); // Convert gas price from Wei to Gwei - const gasPriceInGwei = Eth.web3.utils.fromWei(gasPriceInWei, "gwei"); + const gasPriceInGwei = Evm.web3.utils.fromWei(gasPriceInWei, "gwei"); // Gas limit for a standard ETH transfer const gasLimit = 21000; @@ -78,12 +79,13 @@ export function EthereumView({ props: { setStatus, MPC_CONTRACT } }) { const { big_r, s, recovery_id } = await wallet.getTransactionResult( transactions[0] ); - const signedTransaction = await Eth.reconstructSignedTXFromLocalSession( + const signedTransaction = await Evm.reconstructSignedTXFromLocalSession( big_r, s, recovery_id, senderAddress ); + setSignedTransaction(signedTransaction); setStatus( "✅ Signed payload ready to be relayed to the Ethereum network" @@ -110,7 +112,7 @@ export function EthereumView({ props: { setStatus, MPC_CONTRACT } }) { }; const fetchEthereumAddress = async () => { - const { address } = await Eth.deriveAddress( + const { address } = await Evm.deriveAddress( signedAccountId, derivationPath ); @@ -118,11 +120,11 @@ export function EthereumView({ props: { setStatus, MPC_CONTRACT } }) { setSenderLabel(address); if (!reloaded) { - const balance = await Eth.getBalance(address); + const balance = await Evm.getBalance(address); setBalance(balance); // Update balance state } }; - + async function chainSignature() { setStatus("🏗️ Creating transaction"); @@ -135,12 +137,12 @@ export function EthereumView({ props: { setStatus, MPC_CONTRACT } }) { // to reconstruct on reload sessionStorage.setItem("derivation", derivationPath); - const { big_r, s, recovery_id } = await Eth.requestSignatureToMPC({ + const { big_r, s, recovery_id } = await Evm.requestSignatureToMPC({ wallet, path: derivationPath, transaction, }); - const signedTransaction = await Eth.reconstructSignedTransaction( + const signedTransaction = await Evm.reconstructSignedTransaction( big_r, s, recovery_id, @@ -165,7 +167,7 @@ export function EthereumView({ props: { setStatus, MPC_CONTRACT } }) { ); try { - const txHash = await Eth.broadcastTX(signedTransaction); + const txHash = await Evm.broadcastTX(signedTransaction); setStatus( <> @@ -256,11 +258,11 @@ export function EthereumView({ props: { setStatus, MPC_CONTRACT } }) { {action === "transfer" ? ( - + ) : ( )} diff --git a/src/components/EVM/FunctionCall.jsx b/src/components/EVM/FunctionCall.jsx new file mode 100644 index 0000000..7b30a3c --- /dev/null +++ b/src/components/EVM/FunctionCall.jsx @@ -0,0 +1,139 @@ +import { useState, useEffect } from "react"; + +import PropTypes from "prop-types"; +import { forwardRef } from "react"; +import { useImperativeHandle } from "react"; + +const abi = [ + { + inputs: [ + { + internalType: "uint256", + name: "_num", + type: "uint256", + }, + ], + name: "set", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "get", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "num", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, +]; + +export const FunctionCallForm = forwardRef( + ({ props: { Evm, contractAddress, senderAddress, loading } }, ref) => { + const [number, setNumber] = useState(1000); + const [currentNumber, setCurrentNumber] = useState(""); + + async function getNumber() { + const result = await Evm.getContractViewFunction( + contractAddress, + abi, + "get" + ); + setCurrentNumber(String(result)); + } + + useEffect(() => { + getNumber(); + }, []); + + useImperativeHandle(ref, () => ({ + async createTransaction() { + const data = Evm.createTransactionData(contractAddress, abi, "set", [ + number, + ]); + const { transaction } = await Evm.createTransaction({ + sender: senderAddress, + receiver: contractAddress, + amount: 0, + data, + }); + return { transaction }; + }, + + async afterRelay() { + getNumber(); + }, + })); + + return ( + <> +
+ +
+ +
Contract address
+
+
+
+ +
+ setNumber(e.target.value)} + step="1" + disabled={loading} + /> +
+ {" "} + The number to save, current value: {currentNumber} {" "} +
+
+
+ + ); + } +); + +FunctionCallForm.propTypes = { + props: PropTypes.shape({ + senderAddress: PropTypes.string.isRequired, + contractAddress: PropTypes.string.isRequired, + loading: PropTypes.bool.isRequired, + Evm: PropTypes.shape({ + createTransaction: PropTypes.func.isRequired, + createTransactionData: PropTypes.func.isRequired, + getContractViewFunction: PropTypes.func.isRequired, + }).isRequired, + }).isRequired, +}; + +FunctionCallForm.displayName = "FunctionCallForm"; diff --git a/src/components/Ethereum/Transfer.jsx b/src/components/EVM/Transfer.jsx similarity index 70% rename from src/components/Ethereum/Transfer.jsx rename to src/components/EVM/Transfer.jsx index 2edfaff..c56c984 100644 --- a/src/components/Ethereum/Transfer.jsx +++ b/src/components/EVM/Transfer.jsx @@ -4,24 +4,16 @@ import PropTypes from "prop-types"; import { forwardRef } from "react"; import { useImperativeHandle } from "react"; -export const TransferForm = forwardRef( - ({ props: { Eth, senderAddress, loading } }, ref) => { - const [receiver, setReceiver] = useState( - "0x427F9620Be0fe8Db2d840E2b6145D1CF2975bcaD" - ); - const [amount, setAmount] = useState(0.005); +export const TransferForm = forwardRef(({ props: { Evm, senderAddress, loading } }, ref) => { + const [receiver, setReceiver] = useState("0xb8A6a4eb89b27703E90ED18fDa1101c7aa02930D"); + const [amount, setAmount] = useState(0.005); - useImperativeHandle(ref, () => ({ - async createTransaction() { - return await Eth.createTransaction({ - sender: senderAddress, - receiver: receiver, - amount: amount, - data: undefined, - }); - }, - async afterRelay() {}, - })); + useImperativeHandle(ref, () => ({ + async createTransaction() { + return await Evm.createTransaction({ sender: senderAddress, receiver, amount }); + }, + async afterRelay() { } + })); return ( <> @@ -70,10 +62,10 @@ TransferForm.propTypes = { props: PropTypes.shape({ senderAddress: PropTypes.string.isRequired, loading: PropTypes.bool.isRequired, - Eth: PropTypes.shape({ - createTransaction: PropTypes.func.isRequired, - }).isRequired, - }).isRequired, + Evm: PropTypes.shape({ + createTransaction: PropTypes.func.isRequired + }).isRequired + }).isRequired }; TransferForm.displayName = "TransferForm"; diff --git a/src/components/Ethereum/FunctionCall.jsx b/src/components/Ethereum/FunctionCall.jsx deleted file mode 100644 index 385ed5a..0000000 --- a/src/components/Ethereum/FunctionCall.jsx +++ /dev/null @@ -1,118 +0,0 @@ -import { useState, useEffect } from 'react'; - -import PropTypes from 'prop-types'; -import { forwardRef } from 'react'; -import { useImperativeHandle } from 'react'; - -const abi = [ - { - inputs: [ - { - internalType: 'uint256', - name: '_num', - type: 'uint256', - }, - ], - name: 'set', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [], - name: 'get', - outputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'num', - outputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - stateMutability: 'view', - type: 'function', - }, -]; - -const contract = '0xe2a01146FFfC8432497ae49A7a6cBa5B9Abd71A3'; - -export const FunctionCallForm = forwardRef(({ props: { Eth, senderAddress, loading } }, ref) => { - const [number, setNumber] = useState(1000); - const [currentNumber, setCurrentNumber] = useState(''); - - async function getNumber() { - const result = await Eth.getContractViewFunction(contract, abi, 'get'); - setCurrentNumber(String(result)); - } - - useEffect(() => { getNumber() }, []); - - useImperativeHandle(ref, () => ({ - async createTransaction() { - const data = Eth.createTransactionData(contract, abi, 'set', [number]); - const { transaction } = await Eth.createTransaction({ sender: senderAddress, receiver: contract, amount: 0, data }); - return { transaction }; - }, - - async afterRelay() { getNumber(); } - })); - - return ( - <> -
- -
- -
Contract address
-
-
-
- -
- setNumber(e.target.value)} - step="1" - disabled={loading} - /> -
The number to save, current value: {currentNumber}
-
-
- - ); -}); - -FunctionCallForm.propTypes = { - props: PropTypes.shape({ - senderAddress: PropTypes.string.isRequired, - loading: PropTypes.bool.isRequired, - Eth: PropTypes.shape({ - createTransaction: PropTypes.func.isRequired, - createTransactionData: PropTypes.func.isRequired, - getContractViewFunction: PropTypes.func.isRequired, - }).isRequired, - }).isRequired -}; - -FunctionCallForm.displayName = 'EthereumContractView'; \ No newline at end of file diff --git a/src/services/ethereum.js b/src/services/evm.js similarity index 62% rename from src/services/ethereum.js rename to src/services/evm.js index 7125074..d94cacd 100644 --- a/src/services/ethereum.js +++ b/src/services/evm.js @@ -1,22 +1,23 @@ -import { Web3 } from 'web3'; -import { bytesToHex } from '@ethereumjs/util'; -import { FeeMarketEIP1559Transaction } from '@ethereumjs/tx'; -import { generateEthAddress } from './kdf/eth'; -import { Common } from '@ethereumjs/common' +import { Web3 } from "web3"; +import { bytesToHex } from "@ethereumjs/util"; +import { FeeMarketEIP1559Transaction } from "@ethereumjs/tx"; +import { generateEthAddress } from "./kdf/eth"; import { Contract, JsonRpcProvider } from "ethers"; import { MPC_CONTRACT } from "./kdf/mpc"; -export class Ethereum { - constructor(chain_rpc, chain_id) { - this.web3 = new Web3(chain_rpc); +export class EthereumVM { + constructor(chain_rpc) { + this.web3 = new Web3(new Web3.providers.HttpProvider(chain_rpc)); window.web3 = this.web3; this.provider = new JsonRpcProvider(chain_rpc); - this.chain_id = chain_id; this.queryGasPrice(); } async deriveAddress(accountId, derivation_path) { - const { address, publicKey } = await generateEthAddress({ accountId, derivation_path }); + const { address, publicKey } = await generateEthAddress({ + accountId, + derivation_path, + }); return { address, publicKey }; } @@ -45,12 +46,12 @@ export class Ethereum { } async createTransaction({ sender, receiver, amount, data = undefined }) { - const common = new Common({ chain: this.chain_id }); - // Get the nonce & gas price const nonce = await this.web3.eth.getTransactionCount(sender); const { maxFeePerGas, maxPriorityFeePerGas } = await this.queryGasPrice(); + const { chainId } = await this.provider.getNetwork(); + // Construct transaction const transactionData = { nonce, @@ -59,50 +60,72 @@ export class Ethereum { maxPriorityFeePerGas, to: receiver, data: data, - value: BigInt(this.web3.utils.toWei(amount, 'ether')), - chain: this.chain_id, + value: BigInt(this.web3.utils.toWei(amount, "ether")), + chainId: chainId, }; // Create a transaction - const transaction = FeeMarketEIP1559Transaction.fromTxData(transactionData, { common }); + const transaction = FeeMarketEIP1559Transaction.fromTxData( + transactionData, + {} + ); // Store in sessionStorage for later - sessionStorage.setItem('transaction', transaction.serialize()); + sessionStorage.setItem("transaction", transaction.serialize()); return { transaction }; } - async requestSignatureToMPC({ wallet, path, transaction, attachedDeposit = 1 }) { + async requestSignatureToMPC({ + wallet, + path, + transaction, + attachedDeposit = 1, + }) { const payload = Array.from(transaction.getHashedMessageToSign()); const { big_r, s, recovery_id } = await wallet.callMethod({ contractId: MPC_CONTRACT, - method: 'sign', + method: "sign", args: { request: { payload, path, key_version: 0 } }, - gas: '250000000000000', // 250 Tgas + gas: "250000000000000", // 250 Tgas deposit: attachedDeposit, }); - return { big_r, s, recovery_id }; + return { + big_r, + s, + recovery_id, + }; } async reconstructSignedTransaction(big_r, S, recovery_id, transaction) { // reconstruct the signature - const r = Buffer.from(big_r.affine_point.substring(2), 'hex'); - const s = Buffer.from(S.scalar, 'hex'); + const r = Buffer.from(big_r.affine_point.substring(2), "hex"); + const s = Buffer.from(S.scalar, "hex"); const v = recovery_id; const signedTx = transaction.addSignature(v, r, s); - if (signedTx.getValidationErrors().length > 0) throw new Error("Transaction validation errors"); + if (signedTx.getValidationErrors().length > 0) + throw new Error("Transaction validation errors"); if (!signedTx.verifySignature()) throw new Error("Signature is not valid"); return signedTx; } async reconstructSignedTXFromLocalSession(big_r, s, recovery_id, sender) { - const serialized = Uint8Array.from(JSON.parse(`[${sessionStorage.getItem('transaction')}]`)); - const transaction = FeeMarketEIP1559Transaction.fromSerializedTx(serialized); - return this.reconstructSignedTransaction(big_r, s, recovery_id, transaction, sender); + const serialized = Uint8Array.from( + JSON.parse(`[${sessionStorage.getItem("transaction")}]`) + ); + const transaction = + FeeMarketEIP1559Transaction.fromSerializedTx(serialized); + return this.reconstructSignedTransaction( + big_r, + s, + recovery_id, + transaction, + sender + ); } // This code can be used to actually relay the transaction to the Ethereum network @@ -110,7 +133,9 @@ export class Ethereum { const serializedTx = bytesToHex(signedTransaction.serialize()); const relayed = this.web3.eth.sendSignedTransaction(serializedTx); let txHash; - await relayed.on('transactionHash', (hash) => { txHash = hash }); + await relayed.on("transactionHash", (hash) => { + txHash = hash; + }); return txHash; } }