Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Brings service functions to sign Ethereum contract transactions #23

Merged
merged 2 commits into from
Jul 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { NearContext } from './context';
import { useEffect, useState } from "react";
import Navbar from "./components/Navbar"
import { Wallet } from "./services/near-wallet";
import { EthereumView } from "./components/Ethereum";
import { EthereumView } from "./components/Ethereum/Ethereum";
import { BitcoinView } from "./components/Bitcoin";

// CONSTANTS
Expand Down
2 changes: 1 addition & 1 deletion src/components/Bitcoin.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export function BitcoinView({ props: { setStatus, MPC_CONTRACT } }) {
const balance = await BTC.getBalance(address);
setStatus(`Your Bitcoin address is: ${address}, balance: ${balance} satoshi`);
}
}, [signedAccountId, derivationPath]);
}, [signedAccountId, derivationPath, setStatus]);

async function chainSignature() {
setStatus('🏗️ Creating transaction');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,29 @@
import { useState, useEffect, useContext } from "react";
import { NearContext } from "../context";
import { NearContext } from "../../context";

import { Ethereum } from "../services/ethereum";
import { useDebounce } from "../hooks/debounce";
import { Ethereum } from "../../services/ethereum";
import { useDebounce } from "../../hooks/debounce";
import PropTypes from 'prop-types';
import { useRef } from "react";
import { TransferForm } from "./Transfer";
import { FunctionCallForm } from "./FunctionCall";

const Sepolia = 11155111;
const Eth = new Ethereum('https://rpc2.sepolia.org', Sepolia);

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

const [receiver, setReceiver] = useState("0xe0f3B7e68151E9306727104973752A415c2bcbEb");
const [amount, setAmount] = useState(0.01);
const [loading, setLoading] = useState(false);
const [step, setStep] = useState("request");
const [signedTransaction, setSignedTransaction] = useState(null);
const [senderAddress, setSenderAddress] = useState("")

const [action, setAction] = useState("transfer")
const [derivation, setDerivation] = useState("ethereum-1");
const derivationPath = useDebounce(derivation, 1000);

const childRef = useRef();

useEffect(() => {
setSenderAddress('Waiting for you to stop typing...')
}, [derivation]);
Expand All @@ -38,11 +41,13 @@ export function EthereumView({ props: { setStatus, MPC_CONTRACT } }) {
const balance = await Eth.getBalance(address);
setStatus(`Your Ethereum address is: ${address}, balance: ${balance} ETH`);
}
}, [signedAccountId, derivationPath]);
}, [signedAccountId, derivationPath, setStatus]);

async function chainSignature() {
setStatus('🏗️ Creating transaction');
const { transaction, payload } = await Eth.createPayload(senderAddress, receiver, amount);

const { transaction, payload } = await childRef.current.createPayload();
// const { transaction, payload } = await Eth.createPayload(senderAddress, receiver, amount, undefined);

setStatus(`🕒 Asking ${MPC_CONTRACT} to sign the transaction, this might take a while`);
try {
Expand All @@ -56,16 +61,20 @@ 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(<>
<a href={`https://sepolia.etherscan.io/tx/${txHash}`} target="_blank"> ✅ Successful </a>
</>
setStatus(
<>
<a href={`https://sepolia.etherscan.io/tx/${txHash}`} target="_blank"> ✅ Successful </a>
</>
);
childRef.current.afterRelay();
} catch (e) {
setStatus(`❌ Error: ${e.message}`);
}
Expand All @@ -89,20 +98,20 @@ export function EthereumView({ props: { setStatus, MPC_CONTRACT } }) {
<div className="form-text" id="eth-sender"> {senderAddress} </div>
</div>
</div>
<div className="row mb-3">
<label className="col-sm-2 col-form-label col-form-label-sm">To:</label>
<div className="col-sm-10">
<input type="text" className="form-control form-control-sm" value={receiver} onChange={(e) => setReceiver(e.target.value)} disabled={loading} />
</div>
</div>
<div className="row mb-3">
<label className="col-sm-2 col-form-label col-form-label-sm">Amount:</label>
<div className="col-sm-10">
<input type="number" className="form-control form-control-sm" value={amount} onChange={(e) => setAmount(e.target.value)} step="0.01" disabled={loading} />
<div className="form-text"> Ethereum units </div>
</div>
<div className="input-group input-group-sm my-2 mb-4">
<span className="text-primary input-group-text" id="chain">Action</span>
<select className="form-select" aria-describedby="chain" onChange={e => setAction(e.target.value)} >
<option value="transfer"> Ξ Transfer </option>
<option value="function-call"> Ξ Call Counter </option>
</select>
</div>

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

<div className="text-center">
{step === 'request' && <button className="btn btn-primary text-center" onClick={UIChainSignature} disabled={loading}> Request Signature </button>}
{step === 'relay' && <button className="btn btn-success text-center" onClick={relayTransaction} disabled={loading}> Relay Transaction </button>}
Expand Down
120 changes: 120 additions & 0 deletions src/components/Ethereum/FunctionCall.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
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 createPayload() {
const data = Eth.createTransactionData(contract, abi, 'set', [number]);
const { transaction, payload } = await Eth.createPayload(senderAddress, contract, 0, data);
return { transaction, payload };
},

async afterRelay() {
getNumber();
}
}));

return (
<>
<div className="row mb-3">
<label className="col-sm-2 col-form-label col-form-label-sm">Counter:</label>
<div className="col-sm-10">
<input
type="text"
className="form-control form-control-sm"
value={contract}
disabled
/>
<div className="form-text">Contract address</div>
</div>
</div>
<div className="row mb-3">
<label className="col-sm-2 col-form-label col-form-label-sm">
Number:
</label>
<div className="col-sm-10">
<input
type="number"
className="form-control form-control-sm"
value={number}
onChange={(e) => setNumber(e.target.value)}
step="1"
disabled={loading}
/>
<div className="form-text"> The number to save, current value: <b> {currentNumber} </b> </div>
</div>
</div>
</>
);
});

FunctionCallForm.propTypes = {
props: PropTypes.shape({
senderAddress: PropTypes.string.isRequired,
loading: PropTypes.bool.isRequired,
Eth: PropTypes.shape({
createPayload: PropTypes.func.isRequired,
createTransactionData: PropTypes.func.isRequired,
getContractViewFunction: PropTypes.func.isRequired,
}).isRequired,
}).isRequired
};

FunctionCallForm.displayName = 'EthereumContractView';
48 changes: 48 additions & 0 deletions src/components/Ethereum/Transfer.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useState } from "react";

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);

useImperativeHandle(ref, () => ({
async createPayload() {
const { transaction, payload } = await Eth.createPayload(senderAddress, receiver, amount, undefined);
return { transaction, payload };
},
async afterRelay() { }
}));

return (
<>
<div className="row mb-3">
<label className="col-sm-2 col-form-label col-form-label-sm">To:</label>
<div className="col-sm-10">
<input type="text" className="form-control form-control-sm" value={receiver} onChange={(e) => setReceiver(e.target.value)} disabled={loading} />
</div>
</div>
<div className="row mb-3">
<label className="col-sm-2 col-form-label col-form-label-sm">Amount:</label>
<div className="col-sm-10">
<input type="number" className="form-control form-control-sm" value={amount} onChange={(e) => setAmount(e.target.value)} step="0.01" disabled={loading} />
<div className="form-text"> Ethereum units </div>
</div>
</div>
</>
)
});

TransferForm.propTypes = {
props: PropTypes.shape({
senderAddress: PropTypes.string.isRequired,
loading: PropTypes.bool.isRequired,
Eth: PropTypes.shape({
createPayload: PropTypes.func.isRequired
}).isRequired
}).isRequired
};

TransferForm.displayName = 'TransferForm';
2 changes: 2 additions & 0 deletions src/services/bitcoin.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ export class Bitcoin {
address: sender,
value: change,
});
} else {
alert(`Not enough funds to cover the transaction and fee. Missing ${-change} satoshis`);
}

return { psbt, utxos };
Expand Down
19 changes: 17 additions & 2 deletions src/services/ethereum.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import { bytesToHex } from '@ethereumjs/util';
import { FeeMarketEIP1559Transaction } from '@ethereumjs/tx';
import { deriveChildPublicKey, najPublicKeyStrToUncompressedHexPoint, uncompressedHexPointToEvmAddress } from '../services/kdf';
import { Common } from '@ethereumjs/common'
import { Contract, JsonRpcProvider } from "ethers";

export class Ethereum {
constructor(chain_rpc, chain_id) {
this.web3 = new Web3(chain_rpc);
this.provider = new JsonRpcProvider(chain_rpc);
this.chain_id = chain_id;
this.queryGasPrice();
}
Expand All @@ -29,7 +31,19 @@ export class Ethereum {
return Number(balance * 100n / ONE_ETH) / 100;
}

async createPayload(sender, receiver, amount) {
async getContractViewFunction(receiver, abi, methodName, args = []) {
const contract = new Contract(receiver, abi, this.provider);

return await contract[methodName](...args);
}

createTransactionData(receiver, abi, methodName, args = []) {
const contract = new Contract(receiver, abi);

return contract.interface.encodeFunctionData(methodName, args);
}

async createPayload(sender, receiver, amount, data) {
const common = new Common({ chain: this.chain_id });

// Get the nonce & gas price
Expand All @@ -39,10 +53,11 @@ export class Ethereum {
// Construct transaction
const transactionData = {
nonce,
gasLimit: 21000,
gasLimit: 50_000,
maxFeePerGas,
maxPriorityFeePerGas,
to: receiver,
data: data,
value: BigInt(this.web3.utils.toWei(amount, "ether")),
chain: this.chain_id,
};
Expand Down
2 changes: 1 addition & 1 deletion src/services/near-wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export class Wallet {
*/
startUp = async (accountChangeHook) => {
this.selector = setupWalletSelector({
network: this.networkId,
network: {networkId: this.networkId, nodeUrl: 'https://rpc.testnet.pagoda.co'},
modules: [setupMyNearWallet(), setupHereWallet()]
});

Expand Down