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

Remove ethereumjs dependency #21

Merged
merged 10 commits into from
Apr 15, 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
File renamed without changes.
14 changes: 11 additions & 3 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: Node.js CI

on:
push:
branches: [ main ]
branches: [main]
pull_request:
branches: [ main ]
branches: [main]

jobs:
types:
Expand All @@ -15,7 +15,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
node-version: "20"

- name: Install & Build
run: yarn && yarn build
Expand All @@ -24,3 +24,11 @@ jobs:
run: |
yarn lint
yarn test
env:
NODE_URL: https://rpc2.sepolia.org
SCAN_URL: https://sepolia.etherscan.io
GAS_STATION_URL: https://sepolia.beaconcha.in/api/v1/execution/gasnow

NEAR_MULTICHAIN_CONTRACT: multichain-testnet-2.testnet
NEAR_ACCOUNT_ID: ${{secrets.NEAR_ACCOUNT_ID}}
NEAR_ACCOUNT_PRIVATE_KEY: ${{secrets.NEAR_PK}}
2 changes: 1 addition & 1 deletion examples/send-eth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const run = async (): Promise<void> => {
const evm = await setupNearEthAdapter();
await evm.signAndSendTransaction({
to: "0xdeADBeeF0000000000000000000000000b00B1e5",
// THIS IS ONE WAY!
// THIS IS ONE WEI!
value: 1n,
});
};
Expand Down
4 changes: 2 additions & 2 deletions examples/weth/wrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import { setupNearEthAdapter } from "../setup";
const run = async (): Promise<void> => {
const neareth = await setupNearEthAdapter();
const sepoliaWETH = "0xfff9976782d46cc05630d1f6ebab18b2324d6b14";
const ethAmount = 0.01;
const ethAmount = parseEther("0.01");
const deposit = "0xd0e30db0";

await neareth.signAndSendTransaction({
to: sepoliaWETH,
value: parseEther(ethAmount.toString()),
value: ethAmount,
data: deposit,
});
};
Expand Down
5 changes: 1 addition & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"scripts": {
"build": "rm -rf ./dist && tsc",
"lint": "eslint . --ext .ts,.tsx",
"test": "jest",
"test": "jest --testTimeout 30000",
"fmt": "prettier --write '{src,examples,tests}/**/*.{js,jsx,ts,tsx}'"
},
"devDependencies": {
Expand All @@ -30,9 +30,6 @@
"typescript": "^5.4.2"
},
"dependencies": {
"@ethereumjs/common": "^4.3.0",
"@ethereumjs/tx": "^5.3.0",
"@ethereumjs/util": "^9.0.3",
"@near-js/accounts": "^1.0.4",
"@near-js/crypto": "^1.2.1",
"@near-js/keystores": "^0.0.9",
Expand Down
81 changes: 33 additions & 48 deletions src/chains/ethereum.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { FeeMarketEIP1559Transaction } from "@ethereumjs/tx";
import { bytesToHex } from "@ethereumjs/util";
import {
Address,
Hex,
PublicClient,
createPublicClient,
http,
Hash,
serializeTransaction,
} from "viem";
import {
BaseTx,
Expand All @@ -15,12 +14,13 @@ import {
TxPayload,
TransactionWithSignature,
} from "../types";
import { queryGasPrice } from "../utils/gasPrice";
import { MultichainContract } from "../mpcContract";
import BN from "bn.js";
import { queryGasPrice } from "../utils/gasPrice";
import { buildTxPayload, addSignature } from "../utils/transaction";

export class NearEthAdapter {
private ethClient: PublicClient;
ethClient: PublicClient;
private scanUrl: string;
private gasStationUrl: string;

Expand Down Expand Up @@ -109,7 +109,7 @@ export class NearEthAdapter {
txData: BaseTx,
nearGas?: BN
): Promise<{
transaction: FeeMarketEIP1559Transaction;
transaction: Hex;
requestPayload: NearContractFunctionPayload;
}> {
console.log("Creating Payload for sender:", this.sender);
Expand Down Expand Up @@ -140,25 +140,21 @@ export class NearEthAdapter {
* and payload bytes in preparation to be relayed to Near MPC contract.
*
* @param {BaseTx} tx - Minimal transaction data to be signed by Near MPC and executed on EVM.
* @param {number?} nonce - Optional transaction nonce.
* @returns Transaction and its bytes (the payload to be signed on Near).
*/
async createTxPayload(tx: BaseTx, nonce?: number): Promise<TxPayload> {
const transaction = await this.buildTransaction(tx, nonce);
console.log("Built (unsigned) Transaction", transaction.toJSON());
const payload = Array.from(
new Uint8Array(transaction.getHashedMessageToSign().slice().reverse())
);
const signArgs = { payload, path: this.derivationPath, key_version: 0 };
console.log("Built (unsigned) Transaction", transaction);
const signArgs = {
payload: buildTxPayload(transaction),
path: this.derivationPath,
key_version: 0,
};
return { transaction, signArgs };
}

async buildTransaction(
tx: BaseTx,
nonce?: number
): Promise<FeeMarketEIP1559Transaction> {
const { maxFeePerGas, maxPriorityFeePerGas } = await queryGasPrice(
this.gasStationUrl
);
async buildTransaction(tx: BaseTx, nonce?: number): Promise<Hex> {
const transactionData = {
nonce:
nonce ||
Expand All @@ -167,52 +163,41 @@ export class NearEthAdapter {
})),
account: this.sender,
to: tx.to,
value: tx.value || 0n,
data: tx.data || "0x",
value: tx.value ?? 0n,
data: tx.data ?? "0x",
};
const estimatedGas = await this.ethClient.estimateGas(transactionData);
const [estimatedGas, { maxFeePerGas, maxPriorityFeePerGas }, chainId] =
await Promise.all([
this.ethClient.estimateGas(transactionData),
queryGasPrice(this.gasStationUrl),
this.ethClient.getChainId(),
]);
const transactionDataWithGasLimit = {
...transactionData,
gasLimit: BigInt(estimatedGas.toString()),
gas: BigInt(estimatedGas.toString()),
maxFeePerGas,
maxPriorityFeePerGas,
chainId: await this.ethClient.getChainId(),
chainId,
};
return FeeMarketEIP1559Transaction.fromTxData(transactionDataWithGasLimit);
console.log("Gas Estimation:", estimatedGas);
console.log("Transaction Request", transactionDataWithGasLimit);
return serializeTransaction(transactionDataWithGasLimit);
}

reconstructSignature(
tx: TransactionWithSignature
): FeeMarketEIP1559Transaction {
const { transaction, signature: sig } = tx;
const r = Buffer.from(sig.big_r.substring(2), "hex");
const s = Buffer.from(sig.big_s, "hex");

const candidates = [0n, 1n].map((v) => transaction.addSignature(v, r, s));
const signature = candidates.find(
(c) =>
c.getSenderAddress().toString().toLowerCase() ===
this.sender.toLowerCase()
);

if (!signature) {
throw new Error("Signature is not valid");
}

return signature;
reconstructSignature(tx: TransactionWithSignature): Hex {
return addSignature(tx, this.sender);
}

/**
* Relays signed transaction to Etherem mempool for execution.
* @param signedTx - Signed Ethereum transaction.
* Relays signed transaction to Ethereum mem-pool for execution.
* @param serializedTransaction - Signed Ethereum transaction.
* @returns Transaction Hash of relayed transaction.
*/
async relaySignedTransaction(
signedTx: FeeMarketEIP1559Transaction
private async relaySignedTransaction(
serializedTransaction: Hex
): Promise<Hash> {
const serializedTx = bytesToHex(signedTx.serialize()) as Hex;
const txHash = await this.ethClient.sendRawTransaction({
serializedTransaction: serializedTx,
serializedTransaction,
});
console.log(`Transaction Confirmed: ${this.scanUrl}/tx/${txHash}`);
return txHash;
Expand Down
11 changes: 5 additions & 6 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import { FeeMarketEIP1559Transaction } from "@ethereumjs/tx";
import { Address, Hex } from "viem";
import { MultichainContract } from "./mpcContract";
import { FunctionCallAction } from "@near-wallet-selector/core";
import BN from "bn.js";
import { Hex } from "viem";

export interface BaseTx {
/// Recipient of the transaction
to: Address;
to: `0x${string}`;
/// ETH value of transaction
value?: bigint;
/// Call Data of the transaction
data?: Hex;
data?: `0x${string}`;
}

export interface NearEthAdapterParams {
Expand Down Expand Up @@ -66,7 +65,7 @@ export interface SignArgs {

export interface TxPayload {
/// Deserialized Ethereum Transaction.
transaction: FeeMarketEIP1559Transaction;
transaction: Hex;
/// Arguments required by Near MPC Contract signature request.
signArgs: SignArgs;
}
Expand Down Expand Up @@ -94,7 +93,7 @@ export interface MPCSignature {
*/
export interface TransactionWithSignature {
/// Unsigned Ethereum transaction data.
transaction: FeeMarketEIP1559Transaction;
transaction: Hex;
/// Representation of the transaction's signature.
signature: MPCSignature;
}
70 changes: 70 additions & 0 deletions src/utils/transaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import {
Address,
Hex,
hexToBytes,
hexToNumber,
keccak256,
parseTransaction,
serializeTransaction,
signatureToHex,
} from "viem";
import { TransactionWithSignature } from "../types";
import { secp256k1 } from "@noble/curves/secp256k1";

import { publicKeyToAddress } from "viem/utils";

export function buildTxPayload(unsignedTxHash: `0x${string}`): number[] {
// Compute the Transaction Message Hash.
const messageHash = keccak256(unsignedTxHash);
return Array.from(hexToBytes(messageHash).slice().reverse());
}

export function addSignature(
{ transaction, signature: { big_r, big_s } }: TransactionWithSignature,
sender: Address
): Hex {
const txData = parseTransaction(transaction);
const r = `0x${big_r.substring(2)}` as Hex;
const s = `0x${big_s}` as Hex;

const candidates = [27n, 28n].map((v) => {
return {
v,
r,
s,
...txData,
};
});

const signedTx = candidates.find((tx) => {
const signature = signatureToHex({
r: tx.r!,
s: tx.s!,
v: tx.v!,
});
const pk = publicKeyToAddress(
recoverPublicKey(keccak256(transaction), signature)
);
return pk.toLowerCase() === sender.toLowerCase();
});
if (!signedTx) {
throw new Error("Signature is not valid");
}
return serializeTransaction(signedTx);
}

// This method is mostly pasted from viem since they use an unnecessary async import.
// import { secp256k1 } from "@noble/curves/secp256k1";
// TODO - fix their async import!
export function recoverPublicKey(hash: Hex, signature: Hex): Hex {
// Derive v = recoveryId + 27 from end of the signature (27 is added when signing the message)
// The recoveryId represents the y-coordinate on the secp256k1 elliptic curve and can have a value [0, 1].
let v = hexToNumber(`0x${signature.slice(130)}`);
if (v === 0 || v === 1) v += 27;

const publicKey = secp256k1.Signature.fromCompact(signature.substring(2, 130))
.addRecoveryBit(v - 27)
.recoverPublicKey(hash.substring(2))
.toHex(false);
return `0x${publicKey}`;
}
32 changes: 32 additions & 0 deletions tests/e2e.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { setupNearEthAdapter } from "../examples/setup";
import { NearEthAdapter } from "../src";
import { getBalance } from "viem/actions";

describe("End To End", () => {
let evm: NearEthAdapter;
const to = "0xdeADBeeF0000000000000000000000000b00B1e5";
const ONE_WEI = 1n;

beforeAll(async () => {
evm = await setupNearEthAdapter();
});

afterAll(async () => {
clearTimeout();
});

it("Runs the Send ETH Tx", async () => {
await expect(
evm.signAndSendTransaction({ to, value: ONE_WEI })
).resolves.not.toThrow();
});

it("Fails Invalid Send ETH Tx", async () => {
const senderBalance = await getBalance(evm.ethClient, {
address: evm.ethPublicKey(),
});
await expect(
evm.signAndSendTransaction({ to, value: senderBalance + ONE_WEI })
).rejects.toThrow();
});
bh2smith marked this conversation as resolved.
Show resolved Hide resolved
});
27 changes: 0 additions & 27 deletions tests/ethereum.test.ts

This file was deleted.

Loading