Skip to content

Commit

Permalink
Merge pull request #30 from near-examples/update-api
Browse files Browse the repository at this point in the history
wip: adding v1.multichain
  • Loading branch information
gagdiez authored Jul 30, 2024
2 parents 5cb169f + 373fa00 commit c2d0af3
Show file tree
Hide file tree
Showing 8 changed files with 109 additions and 85 deletions.
15 changes: 9 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,29 @@
"@ethereumjs/common": "^4.3.0",
"@ethereumjs/tx": "^5.3.0",
"@ethereumjs/util": "^9.0.3",
"@near-wallet-selector/core": "^8.9.5",
"@near-wallet-selector/here-wallet": "^8.9.5",
"@near-wallet-selector/modal-ui": "^8.9.5",
"@near-wallet-selector/my-near-wallet": "^8.9.5",
"@near-wallet-selector/core": "^8.9.10",
"@near-wallet-selector/here-wallet": "^8.9.10",
"@near-wallet-selector/meteor-wallet": "^8.9.10",
"@near-wallet-selector/modal-ui": "^8.9.10",
"@near-wallet-selector/my-near-wallet": "^8.9.10",
"@vitejs/plugin-react": "^4.2.1",
"axios": "^1.6.8",
"bitcoinjs-lib": "^6.1.5",
"bn.js": "^5.2.1",
"bs58check": "^3.0.1",
"elliptic": "^6.5.5",
"ethers": "^6.11.1",
"hash.js": "^1.1.7",
"js-sha3": "^0.9.3",
"keccak": "^3.0.4",
"near-api-js": "^3.0.4",
"prop-types": "^15.8.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"rxjs": "^7.8.1",
"viem": "^2.18.4",
"vite-plugin-node-polyfills": "^0.21.0",
"web3": "^4.6.0",
"@vitejs/plugin-react": "^4.2.1"
"web3": "^4.6.0"
},
"overrides": {
"near-api-js": "^3.0.4"
Expand Down
12 changes: 8 additions & 4 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ import { EthereumView } from "./components/Ethereum/Ethereum";
import { BitcoinView } from "./components/Bitcoin";

// CONSTANTS
const MPC_CONTRACT = 'v2.multichain-mpc.testnet';
const MPC_CONTRACT = 'v1.signer-prod.testnet';

// NEAR WALLET
const wallet = new Wallet({ network: 'testnet', createAccessKeyFor: MPC_CONTRACT });
const wallet = new Wallet({ network: 'testnet' });

// parse transactionHashes from URL
const txHash = new URLSearchParams(window.location.search).get('transactionHashes');
const transactions = txHash ? txHash.split(',') : [];

function App() {
const [signedAccountId, setSignedAccountId] = useState('');
Expand Down Expand Up @@ -43,8 +47,8 @@ function App() {
</select>
</div>

{chain === 'eth' && <EthereumView props={{ setStatus, MPC_CONTRACT }} />}
{chain === 'btc' && <BitcoinView props={{ setStatus, MPC_CONTRACT }} />}
{chain === 'eth' && <EthereumView props={{ setStatus, MPC_CONTRACT, transactions }} />}
{chain === 'btc' && <BitcoinView props={{ setStatus, MPC_CONTRACT, transactions }} />}
</div>
}

Expand Down
68 changes: 49 additions & 19 deletions src/components/Ethereum/Ethereum.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,37 +11,60 @@ import { FunctionCallForm } from "./FunctionCall";
const Sepolia = 11155111;
const Eth = new Ethereum('https://rpc2.sepolia.org', Sepolia);

export function EthereumView({ props: { setStatus, MPC_CONTRACT } }) {
export function EthereumView({ props: { setStatus, MPC_CONTRACT, transactions } }) {
const { wallet, signedAccountId } = useContext(NearContext);

const [loading, setLoading] = useState(false);
const [step, setStep] = useState("request");
const [step, setStep] = useState(transactions ? 'relay' : "request");
const [signedTransaction, setSignedTransaction] = useState(null);

const [senderLabel, setSenderLabel] = useState("")
const [senderAddress, setSenderAddress] = useState("")
const [action, setAction] = useState("transfer")
const [derivation, setDerivation] = useState("ethereum-1");
const derivationPath = useDebounce(derivation, 1000);
const [derivation, setDerivation] = useState(sessionStorage.getItem('derivation') || "ethereum-1");
const derivationPath = useDebounce(derivation, 1200);

const [reloaded, setReloaded] = useState(transactions.length? true : false);

const childRef = useRef();

useEffect(() => {
setSenderAddress('Waiting for you to stop typing...')
// special case for web wallet that reload the whole page
if (reloaded && senderAddress) signTransaction()

async function signTransaction() {
const { big_r, s, recovery_id } = await wallet.getTransactionResult(transactions[0]);
console.log({ big_r, s, recovery_id });
const signedTransaction = await Eth.reconstructSignatureFromLocalSession(big_r, s, recovery_id, senderAddress);
setSignedTransaction(signedTransaction);
setStatus(`✅ Signed payload ready to be relayed to the Ethereum network`);
setStep('relay');

setReloaded(false);
removeUrlParams();
}

}, [senderAddress]);

useEffect(() => {
setSenderLabel('Waiting for you to stop typing...')
setStatus('Querying Ethereum address and Balance...');
setSenderAddress(null)
setStep('request');
}, [derivation]);

useEffect(() => {
setEthAddress()

console.log(derivationPath)
async function setEthAddress() {
setStatus('Querying your address and balance');
setSenderAddress(`Deriving address from path ${derivationPath}...`);

const { address } = await Eth.deriveAddress(signedAccountId, derivationPath);
setSenderAddress(address);
setSenderLabel(address);

const balance = await Eth.getBalance(address);
setStatus(`Your Ethereum address is: ${address}, balance: ${balance} ETH`);
if (!reloaded) setStatus(`Your Ethereum address is: ${address}, balance: ${balance} ETH`);
}
}, [signedAccountId, derivationPath, setStatus]);
}, [derivationPath]);

async function chainSignature() {
setStatus('🏗️ Creating transaction');
Expand All @@ -51,7 +74,9 @@ export function EthereumView({ props: { setStatus, MPC_CONTRACT } }) {

setStatus(`🕒 Asking ${MPC_CONTRACT} to sign the transaction, this might take a while`);
try {
const signedTransaction = await Eth.requestSignatureToMPC(wallet, MPC_CONTRACT, derivationPath, payload, transaction, senderAddress);
const { big_r, s, recovery_id } = await Eth.requestSignatureToMPC(wallet, MPC_CONTRACT, derivationPath, payload, transaction, senderAddress);
const signedTransaction = await Eth.reconstructSignature(big_r, s, recovery_id, transaction, senderAddress);

setSignedTransaction(signedTransaction);
setStatus(`✅ Signed payload ready to be relayed to the Ethereum network`);
setStep('relay');
Expand All @@ -61,12 +86,10 @@ export function EthereumView({ props: { setStatus, MPC_CONTRACT } }) {
}
}



async function relayTransaction() {
setLoading(true);
setStatus('🔗 Relaying transaction to the Ethereum network... this might take a while');

try {
const txHash = await Eth.relayTransaction(signedTransaction);
setStatus(
Expand Down Expand Up @@ -95,7 +118,7 @@ export function EthereumView({ props: { setStatus, MPC_CONTRACT } }) {
<label className="col-sm-2 col-form-label col-form-label-sm">Path:</label>
<div className="col-sm-10">
<input type="text" className="form-control form-control-sm" value={derivation} onChange={(e) => setDerivation(e.target.value)} disabled={loading} />
<div className="form-text" id="eth-sender"> {senderAddress} </div>
<div className="form-text" id="eth-sender"> {senderLabel} </div>
</div>
</div>
<div className="input-group input-group-sm my-2 mb-4">
Expand All @@ -107,9 +130,9 @@ export function EthereumView({ props: { setStatus, MPC_CONTRACT } }) {
</div>

{
action === 'transfer'
? <TransferForm ref={childRef} props={{ Eth, senderAddress, loading }} />
: <FunctionCallForm ref={childRef} props={{ Eth, senderAddress, loading }} />
action === 'transfer'
? <TransferForm ref={childRef} props={{ Eth, senderAddress, loading }} />
: <FunctionCallForm ref={childRef} props={{ Eth, senderAddress, loading }} />
}

<div className="text-center">
Expand All @@ -118,11 +141,18 @@ export function EthereumView({ props: { setStatus, MPC_CONTRACT } }) {
</div>
</>
)

function removeUrlParams () {
const url = new URL(window.location.href);
url.searchParams.delete('transactionHashes');
window.history.replaceState({}, document.title, url);
}
}

EthereumView.propTypes = {
props: PropTypes.shape({
setStatus: PropTypes.func.isRequired,
MPC_CONTRACT: PropTypes.string.isRequired,
transactions: PropTypes.arrayOf(PropTypes.string).isRequired
}).isRequired
};
2 changes: 1 addition & 1 deletion src/components/Ethereum/Transfer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { forwardRef } from "react";
import { useImperativeHandle } from "react";

export const TransferForm = forwardRef(({ props: { Eth, senderAddress, loading } }, ref) => {
const [receiver, setReceiver] = useState("0x427F9620Be0fe8Db2d840E2b6145D1CF2975bcaD");
const [receiver, setReceiver] = useState("0xe0f3B7e68151E9306727104973752A415c2bcbEb");
const [amount, setAmount] = useState(0.005);

useImperativeHandle(ref, () => ({
Expand Down
13 changes: 6 additions & 7 deletions src/hooks/debounce.jsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { useEffect, useState, useRef } from "react";
import { useEffect, useState } from "react";

export function useDebounce(value, delay = 500){
export function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
const timerRef = useRef();

useEffect(() => {
timerRef.current = setTimeout(() => setDebouncedValue(value), delay);
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);

return () => {
clearTimeout(timerRef.current);
};
return () => clearTimeout(handler);
}, [value, delay]);

return debouncedValue;
Expand Down
37 changes: 24 additions & 13 deletions src/services/ethereum.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,34 +62,45 @@ export class Ethereum {
chain: this.chain_id,
};

// Return the message hash
// Create a transaction
const transaction = FeeMarketEIP1559Transaction.fromTxData(transactionData, { common });
const payload = transaction.getHashedMessageToSign();

// Store in sessionStorage for later
sessionStorage.setItem('transaction', transaction.serialize());

return { transaction, payload };
}

async requestSignatureToMPC(wallet, contractId, path, ethPayload, transaction, sender) {
async requestSignatureToMPC(wallet, contractId, path, ethPayload) {
// Ask the MPC to sign the payload
const payload = Array.from(ethPayload.reverse());
const [big_r, big_s] = await wallet.callMethod({ contractId, method: 'sign', args: { payload, path, key_version: 0 }, gas: '250000000000000' });
sessionStorage.setItem('derivation', path);

// reconstruct the signature
const r = Buffer.from(big_r.substring(2), 'hex');
const s = Buffer.from(big_s, 'hex');
const payload = Array.from(ethPayload);
const { big_r, s, recovery_id } = await wallet.callMethod({ contractId, method: 'sign', args: { request: { payload, path, key_version: 0 } }, gas: '250000000000000', deposit: '1' });
return { big_r, s, recovery_id };
}

const candidates = [0n, 1n].map((v) => transaction.addSignature(v, r, s));
const signature = candidates.find((c) => c.getSenderAddress().toString().toLowerCase() === sender.toLowerCase());
async reconstructSignature(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 v = recovery_id;

if (!signature) {
throw new Error("Signature is not valid");
}
const signature = transaction.addSignature(v, r, s);

if (signature.getValidationErrors().length > 0) throw new Error("Transaction validation errors");
if (!signature.verifySignature()) throw new Error("Signature is not valid");

return signature;
}

async reconstructSignatureFromLocalSession(big_r, s, recovery_id, sender) {
const serialized = Uint8Array.from(JSON.parse(`[${sessionStorage.getItem('transaction')}]`));
const transaction = FeeMarketEIP1559Transaction.fromSerializedTx(serialized);
console.log("transaction", transaction)
return this.reconstructSignature(big_r, s, recovery_id, transaction, sender);
}

// This code can be used to actually relay the transaction to the Ethereum network
async relayTransaction(signedTransaction) {
const serializedTx = bytesToHex(signedTransaction.serialize());
Expand Down
44 changes: 10 additions & 34 deletions src/services/kdf.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { base_decode } from 'near-api-js/lib/utils/serialize';
import { ec as EC } from 'elliptic';
import BN from 'bn.js';
import keccak from 'keccak';
import hash from 'hash.js';
import { keccak256 } from "viem";import hash from 'hash.js';
import bs58check from 'bs58check';
import { sha3_256 } from 'js-sha3'

const rootPublicKey = 'secp256k1:4NfTiv3UsGahebgTaHyD9vF8KYKMBnfd6kh94mK6xv8fGBiJB8TBtFMP5WWXz6B89Ac1fbpzPwAvoyQebemHFwx3';

Expand All @@ -12,34 +11,15 @@ export function najPublicKeyStrToUncompressedHexPoint() {
return res;
}

async function sha256Hash(str) {
const encoder = new TextEncoder();
const data = encoder.encode(str);

const hashBuffer = await crypto.subtle.digest('SHA-256', data);

const hashArray = [...new Uint8Array(hashBuffer)];
return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
}

function sha256StringToScalarLittleEndian(hashString) {
const littleEndianString = hashString.match(/../g).reverse().join('');

const scalar = new BN(littleEndianString, 16);

return scalar;
}

export async function deriveChildPublicKey(
parentUncompressedPublicKeyHex,
signerId,
path = ''
) {
const ec = new EC('secp256k1');
let scalar = await sha256Hash(
const ec = new EC("secp256k1");
const scalarHex = sha3_256(
`near-mpc-recovery v0.1.0 epsilon derivation:${signerId},${path}`
);
scalar = sha256StringToScalarLittleEndian(scalar);

const x = parentUncompressedPublicKeyHex.substring(2, 66);
const y = parentUncompressedPublicKeyHex.substring(66);
Expand All @@ -48,24 +28,20 @@ export async function deriveChildPublicKey(
const oldPublicKeyPoint = ec.curve.point(x, y);

// Multiply the scalar by the generator point G
const scalarTimesG = ec.g.mul(scalar);
const scalarTimesG = ec.g.mul(scalarHex);

// Add the result to the old public key point
const newPublicKeyPoint = oldPublicKeyPoint.add(scalarTimesG);

return '04' + (
newPublicKeyPoint.getX().toString('hex').padStart(64, '0') +
newPublicKeyPoint.getY().toString('hex').padStart(64, '0')
);
const newX = newPublicKeyPoint.getX().toString("hex").padStart(64, "0");
const newY = newPublicKeyPoint.getY().toString("hex").padStart(64, "0");
return "04" + newX + newY;
}

export function uncompressedHexPointToEvmAddress(uncompressedHexPoint) {
const address = keccak('keccak256')
.update(Buffer.from(uncompressedHexPoint.substring(2), 'hex'))
.digest('hex');
const addressHash = keccak256(`0x${uncompressedHexPoint.slice(2)}`);

// Ethereum address is last 20 bytes of hash (40 characters), prefixed with 0x
return '0x' + address.substring(address.length - 40)
return ("0x" + addressHash.substring(addressHash.length - 40));
}

export async function uncompressedHexPointToBtcAddress(publicKeyHex, network) {
Expand Down
3 changes: 2 additions & 1 deletion src/services/near-wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { setupModal } from '@near-wallet-selector/modal-ui';
import { setupWalletSelector } from '@near-wallet-selector/core';
import { setupHereWallet } from '@near-wallet-selector/here-wallet';
import { setupMyNearWallet } from '@near-wallet-selector/my-near-wallet';
import { setupMeteorWallet } from '@near-wallet-selector/meteor-wallet';

const THIRTY_TGAS = '30000000000000';
const NO_DEPOSIT = '0';
Expand Down Expand Up @@ -35,7 +36,7 @@ export class Wallet {
startUp = async (accountChangeHook) => {
this.selector = setupWalletSelector({
network: {networkId: this.networkId, nodeUrl: 'https://rpc.testnet.pagoda.co'},
modules: [setupMyNearWallet(), setupHereWallet()]
modules: [setupMyNearWallet(), setupHereWallet(), setupMeteorWallet()]
});

const walletSelector = await this.selector;
Expand Down

0 comments on commit c2d0af3

Please sign in to comment.