Skip to content

Commit 0022e74

Browse files
authored
Merge pull request #52 from near-examples/add_base_chain
Add Base network to the chain selector
2 parents aafcfe2 + 7a4c2a0 commit 0022e74

File tree

7 files changed

+551
-181
lines changed

7 files changed

+551
-181
lines changed

src/App.jsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { useEffect, useState } from 'react';
22
import { NearContext } from './context';
33
import { Wallet } from './services/near-wallet';
44
import Navbar from './components/Navbar';
5-
import { EthereumView } from './components/Ethereum/Ethereum';
5+
import { EthereumView } from './components/EVM/Ethereum';
6+
import { BaseView } from './components/EVM/Base';
67
import { BitcoinView } from './components/Bitcoin';
78
import { MPC_CONTRACT } from './services/kdf/mpc';
89

@@ -68,13 +69,17 @@ function App() {
6869
onChange={(e) => setChain(e.target.value)}
6970
>
7071
<option value='eth'> Ξ Ethereum </option>
72+
<option value='base'> Ξ Base </option>
7173
<option value='btc'> ₿ BTC </option>
7274
</select>
7375
</div>
7476

7577
{chain === 'eth' && (
7678
<EthereumView props={{ setStatus, MPC_CONTRACT }} />
7779
)}
80+
{chain === 'base' && (
81+
<BaseView props={{ setStatus, MPC_CONTRACT }} />
82+
)}
7883
{chain === 'btc' && (
7984
<BitcoinView props={{ setStatus, MPC_CONTRACT }} />
8085
)}

src/components/EVM/Base.jsx

Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
import { useState, useEffect, useContext, useRef } from "react";
2+
import PropTypes from "prop-types";
3+
4+
import { NearContext } from "../../context";
5+
import { useDebounce } from "../../hooks/debounce";
6+
import { getTransactionHashes } from "../../services/utils";
7+
import { TransferForm } from "./Transfer";
8+
import { FunctionCallForm } from "./FunctionCall";
9+
import { EthereumVM } from "../../services/evm";
10+
11+
const Evm = new EthereumVM("https://base-sepolia.drpc.org");
12+
13+
const contractAddress = "0xCd3b988b216790C598d9AB85Eee189e446CE526D";
14+
15+
const transactions = getTransactionHashes();
16+
17+
export function BaseView({ props: { setStatus, MPC_CONTRACT } }) {
18+
const { wallet, signedAccountId } = useContext(NearContext);
19+
20+
const [loading, setLoading] = useState(false);
21+
const [step, setStep] = useState(transactions ? "relay" : "request");
22+
const [signedTransaction, setSignedTransaction] = useState(null);
23+
const [senderLabel, setSenderLabel] = useState("");
24+
const [senderAddress, setSenderAddress] = useState("");
25+
const [balance, setBalance] = useState(""); // Add balance state
26+
const [action, setAction] = useState("transfer");
27+
const [derivation, setDerivation] = useState(
28+
sessionStorage.getItem("derivation") || "ethereum-1"
29+
);
30+
const [reloaded, setReloaded] = useState(transactions.length);
31+
32+
const [gasPriceInGwei, setGasPriceInGwei] = useState("");
33+
const [txCost, setTxCost] = useState("");
34+
35+
const derivationPath = useDebounce(derivation, 1200);
36+
const childRef = useRef();
37+
38+
useEffect(() => {
39+
async function fetchEthereumGasPrice() {
40+
try {
41+
// Fetch gas price in Wei
42+
const gasPriceInWei = await Evm.web3.eth.getGasPrice();
43+
44+
// Convert gas price from Wei to Gwei
45+
const gasPriceInGwei = Evm.web3.utils.fromWei(gasPriceInWei, "gwei");
46+
47+
// Gas limit for a standard ETH transfer
48+
const gasLimit = 21000;
49+
50+
// Calculate transaction cost in ETH (gwei * gasLimit) / 1e9
51+
const txCost = (gasPriceInGwei * gasLimit) / 1000000000;
52+
53+
// Format both gas price and transaction cost to 7 decimal places
54+
const formattedGasPriceInGwei = parseFloat(gasPriceInGwei).toFixed(7);
55+
const formattedTxCost = parseFloat(txCost).toFixed(7);
56+
57+
console.log(
58+
`Current Sepolia Gas Price: ${formattedGasPriceInGwei} Gwei`
59+
);
60+
console.log(`Estimated Transaction Cost: ${formattedTxCost} ETH`);
61+
62+
setTxCost(formattedTxCost);
63+
setGasPriceInGwei(formattedGasPriceInGwei);
64+
} catch (error) {
65+
console.error("Error fetching gas price:", error);
66+
}
67+
}
68+
69+
fetchEthereumGasPrice();
70+
}, []);
71+
72+
// Handle signing transaction when the page is reloaded and senderAddress is set
73+
useEffect(() => {
74+
if (reloaded && senderAddress) {
75+
signTransaction();
76+
}
77+
78+
async function signTransaction() {
79+
const { big_r, s, recovery_id } = await wallet.getTransactionResult(
80+
transactions[0]
81+
);
82+
const signedTransaction = await Evm.reconstructSignedTXFromLocalSession(
83+
big_r,
84+
s,
85+
recovery_id,
86+
senderAddress
87+
);
88+
89+
setSignedTransaction(signedTransaction);
90+
setStatus(
91+
"✅ Signed payload ready to be relayed to the Base network"
92+
);
93+
setStep("relay");
94+
95+
setReloaded(false);
96+
removeUrlParams();
97+
}
98+
}, [senderAddress, reloaded, wallet, setStatus]);
99+
100+
// Handle changes to derivation path and query Base address and balance
101+
useEffect(() => {
102+
resetAddressState();
103+
fetchEthereumAddress();
104+
}, [derivationPath, signedAccountId]);
105+
106+
const resetAddressState = () => {
107+
setSenderLabel("Waiting for you to stop typing...");
108+
setSenderAddress(null);
109+
setStatus("");
110+
setBalance(""); // Reset balance when derivation path changes
111+
setStep("request");
112+
};
113+
114+
const fetchEthereumAddress = async () => {
115+
const { address } = await Evm.deriveAddress(
116+
signedAccountId,
117+
derivationPath
118+
);
119+
setSenderAddress(address);
120+
setSenderLabel(address);
121+
122+
if (!reloaded) {
123+
const balance = await Evm.getBalance(address);
124+
setBalance(balance); // Update balance state
125+
}
126+
};
127+
128+
async function chainSignature() {
129+
setStatus("🏗️ Creating transaction");
130+
131+
const { transaction } = await childRef.current.createTransaction();
132+
133+
setStatus(
134+
`🕒 Asking ${MPC_CONTRACT} to sign the transaction, this might take a while`
135+
);
136+
try {
137+
// to reconstruct on reload
138+
sessionStorage.setItem("derivation", derivationPath);
139+
140+
const { big_r, s, recovery_id } = await Evm.requestSignatureToMPC({
141+
wallet,
142+
path: derivationPath,
143+
transaction,
144+
});
145+
const signedTransaction = await Evm.reconstructSignedTransaction(
146+
big_r,
147+
s,
148+
recovery_id,
149+
transaction
150+
);
151+
152+
setSignedTransaction(signedTransaction);
153+
setStatus(
154+
`✅ Signed payload ready to be relayed to the Base network`
155+
);
156+
setStep("relay");
157+
} catch (e) {
158+
setStatus(`❌ Error: ${e.message}`);
159+
setLoading(false);
160+
}
161+
}
162+
163+
async function relayTransaction() {
164+
setLoading(true);
165+
setStatus(
166+
"🔗 Relaying transaction to the Base network... this might take a while"
167+
);
168+
169+
try {
170+
const txHash = await Evm.broadcastTX(signedTransaction);
171+
setStatus(
172+
<>
173+
<a href={`https://base-sepolia.blockscout.com/tx/${txHash}`} target="_blank">
174+
{" "}
175+
✅ Successful{" "}
176+
</a>
177+
</>
178+
);
179+
childRef.current.afterRelay();
180+
} catch (e) {
181+
setStatus(`❌ Error: ${e.message}`);
182+
}
183+
184+
setStep("request");
185+
setLoading(false);
186+
}
187+
188+
const UIChainSignature = async () => {
189+
setLoading(true);
190+
await chainSignature();
191+
setLoading(false);
192+
};
193+
194+
function removeUrlParams() {
195+
const url = new URL(window.location.href);
196+
url.searchParams.delete("transactionHashes");
197+
window.history.replaceState({}, document.title, url);
198+
}
199+
200+
return (
201+
<>
202+
{/* Form Inputs */}
203+
<div className="row mb-0">
204+
<label className="col-sm-2 col-form-label"></label>
205+
<div className="col-sm-10"></div>
206+
</div>
207+
208+
<div className="input-group input-group-sm my-2 mb-2">
209+
<span className="input-group-text bg-primary text-white" id="chain">
210+
PATH
211+
</span>
212+
<input
213+
type="text"
214+
className="form-control form-control-sm"
215+
value={derivation}
216+
onChange={(e) => setDerivation(e.target.value)}
217+
disabled={loading}
218+
/>
219+
</div>
220+
221+
{/* ADDRESS & BALANCE */}
222+
<div className="card">
223+
<div className="row mb-0">
224+
<label className="col-sm-2 col-form-label text-end">Address:</label>
225+
<div className="col-sm-10 fs-5">
226+
<div className="form-text" id="eth-sender">
227+
{senderLabel}
228+
</div>
229+
</div>
230+
</div>
231+
<div className="row mb-0">
232+
<label className="col-sm-2 col-form-label text-end">Balance:</label>
233+
<div className="col-sm-10 fs-5">
234+
<div className="form-text text-muted ">
235+
{balance ? (
236+
`${balance} ETH`
237+
) : (
238+
<span className="text-warning">Fetching balance...</span>
239+
)}
240+
</div>
241+
</div>
242+
</div>
243+
</div>
244+
245+
<div className="input-group input-group-sm my-2 mb-4">
246+
<span className="input-group-text bg-info text-white" id="chain">
247+
ACTION
248+
</span>
249+
<select
250+
className="form-select"
251+
aria-describedby="chain"
252+
onChange={(e) => setAction(e.target.value)}
253+
disabled={loading}
254+
>
255+
<option value="transfer">Ξ Transfer</option>
256+
<option value="function-call">Ξ Call Counter</option>
257+
</select>
258+
</div>
259+
260+
{action === "transfer" ? (
261+
<TransferForm ref={childRef} props={{ Evm, senderAddress, loading }} />
262+
) : (
263+
<FunctionCallForm
264+
ref={childRef}
265+
props={{ Evm, contractAddress, senderAddress, loading }}
266+
/>
267+
)}
268+
269+
<div className="text-center mt-4 d-flex justify-content-center">
270+
<div className="table-responsive " style={{ maxWidth: "400px" }}>
271+
<table className="table table-hover text-center w-auto">
272+
<caption className="caption-top text-center">
273+
Sepolia Gas Prices
274+
</caption>
275+
<thead>
276+
<tr className="table-light">
277+
<th scope="col">Price</th>
278+
<th scope="col">Unit</th>
279+
</tr>
280+
</thead>
281+
<tbody>
282+
<tr>
283+
<td>{gasPriceInGwei}</td>
284+
<td>GWEI</td>
285+
</tr>
286+
<tr>
287+
<td>{txCost}</td>
288+
<td>ETH</td>
289+
</tr>
290+
</tbody>
291+
</table>
292+
</div>
293+
</div>
294+
295+
{/* Execute Buttons */}
296+
<div className="d-grid gap-2">
297+
{step === "request" && (
298+
<button
299+
className="btn btn-outline-success text-center btn-lg"
300+
onClick={UIChainSignature}
301+
disabled={loading}
302+
>
303+
Request Signature
304+
</button>
305+
)}
306+
{step === "relay" && (
307+
<button
308+
className="btn btn-success text-center"
309+
onClick={relayTransaction}
310+
disabled={loading}
311+
>
312+
Relay Transaction
313+
</button>
314+
)}
315+
</div>
316+
</>
317+
);
318+
}
319+
320+
BaseView.propTypes = {
321+
props: PropTypes.shape({
322+
setStatus: PropTypes.func.isRequired,
323+
MPC_CONTRACT: PropTypes.string.isRequired,
324+
}).isRequired,
325+
};

0 commit comments

Comments
 (0)