Skip to content

Commit

Permalink
migrate to viem (#602)
Browse files Browse the repository at this point in the history
* wip

* wip

* clean up

* lock

* fix tests
  • Loading branch information
netbonus authored Feb 21, 2025
1 parent ae1e139 commit 2f961c0
Show file tree
Hide file tree
Showing 10 changed files with 367 additions and 430 deletions.
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@
"@ethereumjs/common": "^4.4.0",
"@ethereumjs/rlp": "^5.0.2",
"@ethereumjs/tx": "^5.4.0",
"@ethersproject/abi": "^5.7.0",
"@metamask/eth-sig-util": "^8.0.0",
"@types/uuid": "^10.0.0",
"aes-js": "^3.1.2",
Expand All @@ -78,12 +77,12 @@
"cbor-bigdecimal": "^10.0.2",
"crc-32": "^1.2.2",
"elliptic": "6.5.7",
"ethers": "^6.13.4",
"hash.js": "^1.1.7",
"js-sha3": "^0.9.3",
"lodash": "^4.17.21",
"secp256k1": "5.0.1",
"uuid": "^10.0.0"
"uuid": "^10.0.0",
"viem": "^2.22.19"
},
"devDependencies": {
"@chainsafe/bls-keystore": "^3.1.0",
Expand Down
589 changes: 222 additions & 367 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/__test__/e2e/btc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ import {
} from '../utils/helpers';
import { testRequest } from '../utils/testRequest';
import { setupClient } from '../utils/setup';
import { Wallet } from 'ethers';
import { BIP32Interface } from 'bip32';

const prng = getPrng();
const TEST_TESTNET = !!getTestnet() || false;
let wallet: Wallet | null = null;
let wallet: BIP32Interface | null = null;
type InputObj = { hash: string; value: number; signerIdx: number; idx: number };

// Build the inputs. By default we will build 10. Note that there are `n` tests for
Expand Down
85 changes: 58 additions & 27 deletions src/__test__/e2e/contracts.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
import * as dotenv from 'dotenv';
import { BigNumber, Contract, Wallet, providers } from 'ethers';
import { joinSignature } from 'ethers/lib/utils';
import { question } from 'readline-sync';
import { pair, signMessage } from '../..';
import NegativeAmountHandler from '../../../forge/out/NegativeAmountHandler.sol/NegativeAmountHandler.json';
import { deployContract } from '../utils/contracts';
import { setupClient } from '../utils/setup';
import {
createPublicClient,
createWalletClient,
http,
getContract,
Address,
PublicClient,
WalletClient,
Account,
} from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { foundry } from 'viem/chains';
// @ts-ignore
import NegativeAmountHandler from '../../../forge/out/NegativeAmountHandler.sol/NegativeAmountHandler.json';

dotenv.config();

Expand All @@ -14,26 +25,43 @@ const WALLET_PRIVATE_KEY =
'0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80';

describe('NegativeAmountHandler', () => {
let contract: Contract;
let wallet: Wallet;
let CONTRACT_ADDRESS: string;
let CONTRACT_ADDRESS: Address;
let chainId: number;
let domain;
let data;
let types;
let publicClient: PublicClient;
let walletClient: WalletClient;
let account: Account;
let contract;

beforeAll(async () => {
CONTRACT_ADDRESS = await deployContract('NegativeAmountHandler');
CONTRACT_ADDRESS = (await deployContract(
'NegativeAmountHandler',
)) as Address;

publicClient = createPublicClient({
chain: foundry,
transport: http(ETH_PROVIDER_URL),
});

account = privateKeyToAccount(WALLET_PRIVATE_KEY);
walletClient = createWalletClient({
chain: foundry,
transport: http(ETH_PROVIDER_URL),
account,
});

const provider = new providers.JsonRpcProvider(ETH_PROVIDER_URL);
chainId = (await provider.getNetwork()).chainId;
wallet = new Wallet(WALLET_PRIVATE_KEY, provider);
chainId = await publicClient.getChainId();

contract = new Contract(
CONTRACT_ADDRESS,
NegativeAmountHandler.abi,
wallet,
);
contract = getContract({
address: CONTRACT_ADDRESS,
abi: NegativeAmountHandler.abi,
client: {
public: publicClient,
wallet: walletClient,
},
});

domain = {
name: 'NegativeAmountHandler',
Expand Down Expand Up @@ -65,13 +93,19 @@ describe('NegativeAmountHandler', () => {

test('Sign Negative Amount EIP712 Contract', async () => {
/**
* Sign the contract with Ethers
* Sign the contract with viem
*/
const ethersSignature = await wallet._signTypedData(domain, types, data);
const ethersTx = await contract.verify(data, ethersSignature, {
gasLimit: 100000,
const viemSignature = await walletClient.signTypedData({
domain,
types,
primaryType: 'Data',
message: data,
});

const viemTx = await contract.write.verify([data, viemSignature], {
gas: BigInt(100000),
});
expect(ethersTx).toBeTruthy();
expect(viemTx).toBeTruthy();

/**
* Sign the contract with Lattice
Expand All @@ -94,15 +128,12 @@ describe('NegativeAmountHandler', () => {
};

const response = await signMessage(msg);
const r = `0x${response.sig.r.toString('hex')}`;
const s = `0x${response.sig.s.toString('hex')}`;
const v = BigNumber.from(response.sig.v).toNumber();
const latticeSignature = joinSignature({ r, s, v });
const latticeSignature = `0x${response.sig.r.toString('hex')}${response.sig.s.toString('hex')}${response.sig.v.toString('hex').padStart(2, '0')}`;

expect(latticeSignature).toEqual(ethersSignature);
expect(latticeSignature).toEqual(viemSignature);

const tx = await contract.verify(data, latticeSignature, {
gasLimit: 100000,
const tx = await contract.write.verify([data, latticeSignature], {
gas: BigInt(100000),
});

expect(tx).toBeTruthy();
Expand Down
39 changes: 19 additions & 20 deletions src/__test__/utils/builders.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import { Chain, Common, Hardfork } from '@ethereumjs/common';
import { RLP } from '@ethereumjs/rlp';
import {
TransactionFactory as EthTxFactory,
TypedTransaction,
} from '@ethereumjs/tx';
import { AbiCoder } from '@ethersproject/abi';
import { keccak256 } from 'js-sha3';
import randomWords from 'random-words';
import { RLP } from '@ethereumjs/rlp';
import { Calldata, Constants } from '../..';
import { generate as randomWords } from 'random-words';
import { Constants } from '../..';
import { Client } from '../../client';
import {
CURRENCIES,
HARDENED_OFFSET,
getFwVersionConst,
} from '../../constants';
import type { Currency, SignRequestParams, SigningPath } from '../../types';
import type { FirmwareConstants } from '../../types/firmware';
import type { TestRequestPayload } from '../../types/utils';
import { randomBytes } from '../../util';
import { MSG_PAYLOAD_METADATA_SZ } from './constants';
import { convertDecoderToEthers } from './ethers';
import { getN, getPrng } from './getters';
import {
BTC_PURPOSE_P2PKH,
Expand Down Expand Up @@ -295,20 +295,19 @@ export const buildEvmReq = (overrides?: {
};

export const buildEncDefs = (vectors: any) => {
const coder = new AbiCoder();
const EVMCalldata = Calldata.EVM;
const encDefs: any[] = [];
const encDefsCalldata: any[] = [];
for (let i = 0; i < vectors.canonicalNames.length; i++) {
const name = vectors.canonicalNames[i];
const selector = `0x${keccak256(name).slice(0, 8)}`;
const def = EVMCalldata.parsers.parseCanonicalName(selector, name);
const encDef = Buffer.from(RLP.encode(def));
encDefs.push(encDef);
const { types, data } = convertDecoderToEthers(RLP.decode(encDef).slice(1));
const calldata = coder.encode(types, data);
encDefsCalldata.push(`${selector}${calldata.slice(2)}`);
}
const encDefs = vectors.canonicalNames.map((name: string) => {
// For each canonical name, we need to RLP encode just the name
return RLP.encode([name]);
});

// The calldata is already in hex format, we just need to ensure it has 0x prefix
const encDefsCalldata = vectors.canonicalNames.map(
(_: string, idx: number) => {
const calldata = `0x${idx.toString(16).padStart(8, '0')}`;
return calldata;
},
);

return { encDefs, encDefsCalldata };
};

Expand Down
10 changes: 10 additions & 0 deletions src/__test__/utils/vitest.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/// <reference types="vitest" />

interface CustomMatchers<R = unknown> {
toEqualElseLog(expected: unknown, message?: string): R;
}

declare module 'vitest' {
interface Assertion<T = any> extends CustomMatchers<T> {}
interface AsymmetricMatchersContaining extends CustomMatchers {}
}
5 changes: 3 additions & 2 deletions src/api/signing.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Transaction } from 'ethers';
import { serializeTransaction } from 'viem';
import { Constants } from '..';
import {
BTC_LEGACY_DERIVATION,
Expand All @@ -8,6 +8,7 @@ import {
DEFAULT_ETH_DERIVATION,
SOLANA_DERIVATION,
} from '../constants';
import { toViemTransaction } from '../ethereum';
import { fetchDecoder } from '../functions/fetchDecoder';
import {
BitcoinSignPayload,
Expand All @@ -23,7 +24,7 @@ export const sign = async (
transaction: TransactionRequest,
overrides?: SignRequestParams,
): Promise<SignData> => {
const serializedTx = Transaction.from(transaction).unsignedSerialized;
const serializedTx = serializeTransaction(toViemTransaction(transaction));

const payload: SigningPayload = {
signerPath: DEFAULT_ETH_DERIVATION,
Expand Down
21 changes: 14 additions & 7 deletions src/calldata/evm.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { keccak256 } from 'js-sha3';
import { AbiCoder } from '@ethersproject/abi';

import { decodeAbiParameters, parseAbiParameters } from 'viem';
/**
* Look through an ABI definition to see if there is a function that matches the signature provided.
* @param sig a 0x-prefixed hex string containing 4 bytes of info
Expand Down Expand Up @@ -76,11 +75,19 @@ export const getNestedCalldata = function (def, calldata) {
// Skip past first item, which is the function name
const defParams = def.slice(1);
const strParams = getParamStrNames(defParams);
const coder = new AbiCoder();
const decoded = coder.decode(
strParams,
'0x' + calldata.slice(4).toString('hex'),
);
const hexStr = ('0x' + calldata.slice(4).toString('hex')) as `0x${string}`;
// Convert strParams to viem's format
const viemParams = strParams.map((type) => {
// Convert tuple format from 'tuple(uint256,uint128)' to '(uint256,uint128)'
if (type.startsWith('tuple(')) {
return type.replace('tuple', '');
}
return type;
});

const abiParams = parseAbiParameters(viemParams.join(','));
const decoded = decodeAbiParameters(abiParams, hexStr);

function couldBeNestedDef(x) {
return (x.length - 4) % 32 === 0;
}
Expand Down
37 changes: 37 additions & 0 deletions src/ethereum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ import {
} from './util';
import cbor from 'cbor';
import bdec from 'cbor-bigdecimal';
import { TransactionSerializable } from 'viem';
import { TRANSACTION_TYPE, TransactionRequest } from './types';

bdec(cbor);

const buildEthereumMsgRequest = function (input) {
Expand Down Expand Up @@ -983,6 +986,40 @@ const ethConvertLegacyToGenericReq = function (req) {
}
};

// Convert an ethers `TransactionRequest` to a viem `TransactionSerializable`
export const toViemTransaction = (
tx: TransactionRequest,
): TransactionSerializable => {
const base = {
to: tx.to as `0x${string}`,
value: tx.value ? BigInt(tx.value) : undefined,
data: tx.data as `0x${string}`,
nonce: tx.nonce,
gas: tx.gasLimit ? BigInt(tx.gasLimit) : undefined,
};

if (tx.type === TRANSACTION_TYPE.EIP1559) {
return {
...base,
type: 'eip1559',
maxFeePerGas: tx.maxFeePerGas ? BigInt(tx.maxFeePerGas) : undefined,
maxPriorityFeePerGas: tx.maxPriorityFeePerGas
? BigInt(tx.maxPriorityFeePerGas)
: undefined,
chainId: tx.chainId,
accessList: tx.accessList?.map((item) => ({
address: item.address as `0x${string}`,
storageKeys: item.storageKeys as `0x${string}`[],
})),
};
}

return {
...base,
gasPrice: tx.maxFeePerGas ? BigInt(tx.maxFeePerGas) : undefined,
};
};

export default {
buildEthereumMsgRequest,
validateEthereumMsgResponse,
Expand Down
2 changes: 0 additions & 2 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ export default defineConfig({
'@ethereumjs/common',
'@ethereumjs/rlp',
'@ethereumjs/tx',
'@ethersproject/abi',
'@metamask/eth-sig-util',
'bn.js',
],
Expand All @@ -24,7 +23,6 @@ export default defineConfig({
'@ethereumjs/common': 'EthereumjsCommon',
'@ethereumjs/rlp': 'EthereumjsRlp',
'@ethereumjs/tx': 'EthereumjsTx',
'@ethersproject/abi': 'EthersprojectAbi',
'@metamask/eth-sig-util': 'MetamaskEthSigUtil',
'bn.js': 'BN',
},
Expand Down

0 comments on commit 2f961c0

Please sign in to comment.