Skip to content

Commit

Permalink
Merge pull request #23 from denbite/add_transaction_data
Browse files Browse the repository at this point in the history
Brings service functions to sign Ethereum contract transactions
  • Loading branch information
gagdiez authored Jul 12, 2024
2 parents f4efb9e + 1c07d9a commit 2f9d8c5
Show file tree
Hide file tree
Showing 8 changed files with 223 additions and 29 deletions.
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

0 comments on commit 2f9d8c5

Please sign in to comment.