Skip to content

Commit

Permalink
feat: add new Ledger signer class and support EIP-1559 txs (#94)
Browse files Browse the repository at this point in the history
  • Loading branch information
milapsheth authored Oct 26, 2023
1 parent c835241 commit 3f42aeb
Show file tree
Hide file tree
Showing 7 changed files with 501 additions and 1,050 deletions.
5 changes: 3 additions & 2 deletions axelar-chains-config/info/mainnet.json
Original file line number Diff line number Diff line change
Expand Up @@ -892,9 +892,10 @@
"gasOptions": {
"gasLimit": 500000000
},
"eip1559": true,
"staticGasOptions": {
"gasLimit": 3000000,
"gasPrice": 1000000000
"gasLimit": 100000000,
"maxFeePerGas": 1000000000
}
},
"optimism": {
Expand Down
9 changes: 3 additions & 6 deletions axelar-chains-config/info/testnet.json
Original file line number Diff line number Diff line change
Expand Up @@ -1338,7 +1338,8 @@
"gasOptions": {
"gasLimit": 300000000
},
"confirmations": 3
"confirmations": 3,
"eip1559": true
},
"linea": {
"name": "Linea",
Expand Down Expand Up @@ -1646,11 +1647,7 @@
"gasOptions": {
"gasLimit": 8000000
},
"skipRevertTests": true,
"staticGasOptions": {
"gasLimit": 3000000,
"gasPrice": 1000000000
}
"skipRevertTests": true
},
"scroll": {
"name": "Scroll",
Expand Down
107 changes: 107 additions & 0 deletions evm/LedgerSigner.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
'use strict';

const { ethers } = require('hardhat');
const {
BigNumber,
Signer,
VoidSigner,
utils: { serializeTransaction },
} = ethers;
const TransportNodeHid = require('@ledgerhq/hw-transport-node-hid').default;
const Eth = require('@ledgerhq/hw-app-eth').default;

const { printInfo } = require('./utils');

class LedgerSigner extends Signer {
constructor(provider, path = "m/44'/60'/0'/0/0") {
super();
this.path = path;
this.provider = provider;
}

async connect(provider = null) {
if (provider) {
this.provider = provider;
}

this.transport = await TransportNodeHid.open();
this.eth = new Eth(this.transport);
}

async getAddress() {
if (!this.eth) await this.connect();
const result = await this.eth.getAddress(this.path);
return result.address;
}

async signMessage(message) {
if (!this.eth) await this.connect();

if (typeof message === 'string') {
message = ethers.utils.toUtf8Bytes(message);
}

const messageHex = ethers.utils.hexlify(message).substring(2);

const sig = await this.eth.signPersonalMessage(this.path, messageHex);

return ethers.utils.joinSignature(await this._fixSignature(sig, 2, 1));
}

async signTransaction(tx) {
if (!this.eth) await this.connect();

delete tx.from;

tx = await ethers.utils.resolveProperties(tx);

console.log('Unsigned tx', tx);

const rawTx = serializeTransaction(tx).substring(2);

const sig = await this._fixSignature(await this.eth.signTransaction(this.path, rawTx, null), tx.type, tx.chainId);

const signedTx = serializeTransaction(tx, sig);

printInfo('Signed Tx', signedTx);

return signedTx;
}

async populateTransaction(tx) {
if (!this.eth) await this.connect();

return new VoidSigner(await this.getAddress(), this.provider).populateTransaction(tx);
}

async _fixSignature(signature, type, chainId) {
let v = BigNumber.from('0x' + signature.v).toNumber();

if (type === 2) {
// EIP-1559 transaction. Nothing to do.
// v is already returned as 0 or 1 by Ledger for Type 2 txs
} else {
// Undefined or Legacy Type 0 transaction. Ledger computes EIP-155 sig.v computation incorrectly in this case
// v in {0,1} + 2 * chainId + 35
// Ledger gives this value mod 256
// So from that, compute whether v is 0 or 1 and then add to 2 * chainId + 35 without doing a mod
v = 2 * chainId + 35 + ((v + 256 * 100000000000 - (2 * chainId + 35)) % 256);
}

return {
r: '0x' + signature.r,
s: '0x' + signature.s,
v,
};
}

disconnect() {
if (this.transport) {
this.transport.close();
}
}
}

module.exports = {
LedgerSigner,
};
65 changes: 17 additions & 48 deletions evm/sign-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@ const fs = require('fs');
const { ethers } = require('hardhat');
const {
Wallet,
BigNumber,
utils: { isAddress, serializeTransaction },
utils: { isAddress },
} = ethers;

const path = require('path');
const { LedgerSigner } = require('@ethersproject/hardware-wallets');
const { LedgerSigner } = require('./LedgerSigner');

const { printError, printInfo, printObj, isValidPrivateKey, isNumber, isValidNumber } = require('./utils');

Expand All @@ -28,7 +27,7 @@ const getWallet = async (privateKey, provider, options = {}) => {
}

if (privateKey === 'ledger') {
wallet = getLedgerWallet(provider, options?.ledgerPath);
wallet = await getLedgerWallet(provider, options?.ledgerPath);
} else {
if (!isValidPrivateKey(privateKey)) {
throw new Error('Private key is missing/ not provided correctly');
Expand All @@ -41,10 +40,10 @@ const getWallet = async (privateKey, provider, options = {}) => {
};

// function to create a ledgerSigner type wallet object
const getLedgerWallet = (provider, path) => {
const type = 'hid';
const getLedgerWallet = async (provider, path) => {
path = path || "m/44'/60'/0'/0/0";
return new LedgerSigner(provider, type, path);

return new LedgerSigner(provider, path);
};

/**
Expand All @@ -68,12 +67,6 @@ const signTransaction = async (wallet, chain, tx, options = {}) => {
}

if (!options.offline) {
// force legacy tx type for ledger signer
if (wallet instanceof LedgerSigner) {
tx.type = 0;
tx.gasPrice = tx.gasPrice || (await wallet.provider.getGasPrice());
}

tx = await wallet.populateTransaction(tx);
} else {
const address = options.signerAddress || (await wallet.getAddress());
Expand Down Expand Up @@ -106,23 +99,23 @@ const signTransaction = async (wallet, chain, tx, options = {}) => {
throw new Error('Gas limit is missing/not provided for the tx in function arguments');
}

if (!tx.gasPrice && !(isNumber(tx.maxFeePerGas) && isNumber(tx.maxPriorityFeePerGas))) {
if (
!tx.gasPrice &&
!(isValidNumber(tx.maxFeePerGas) && (tx.maxPriorityFeePerGas === undefined || isNumber(tx.maxPriorityFeePerGas)))
) {
throw new Error('Gas price (legacy or eip-1559) is missing/not provided for the tx in function arguments');
}

if (tx.maxFeePerGas !== undefined) {
tx.type = 2;
} else {
tx.type = 0;
}

printInfo('Transaction being signed', JSON.stringify(tx, null, 2));
}

let signedTx;

if (wallet instanceof LedgerSigner) {
// Ledger doesn't like .from to be set
delete tx.from;

signedTx = await ledgerSign(wallet, chain, tx);
} else {
signedTx = await wallet.signTransaction(tx);
}
const signedTx = await wallet.signTransaction(tx);

if (!options.offline) {
await sendTransaction(signedTx, wallet.provider, chain.confirmations);
Expand All @@ -131,30 +124,6 @@ const signTransaction = async (wallet, chain, tx, options = {}) => {
return { baseTx: tx, signedTx };
};

const ledgerSign = async (wallet, chain, baseTx) => {
printInfo('Waiting for user to approve transaction through ledger wallet');

const unsignedTx = serializeTransaction(baseTx).substring(2);
const sig = await wallet._retry((eth) => eth.signTransaction("m/44'/60'/0'/0/0", unsignedTx));

// EIP-155 sig.v computation
// v in {0,1} + 2 * chainId + 35
// Ledger gives this value mod 256
// So from that, compute whether v is 0 or 1 and then add to 2 * chainId + 35 without doing a mod
var v = BigNumber.from('0x' + sig.v).toNumber();
v = 2 * chain.chainId + 35 + ((v + 256 * 100000000000 - (2 * chain.chainId + 35)) % 256);

const signedTx = serializeTransaction(baseTx, {
v,
r: '0x' + sig.r,
s: '0x' + sig.s,
});

printInfo('Signed Tx from ledger with signedTxHash as', signedTx);

return signedTx;
};

const sendTransaction = async (tx, provider, confirmations = undefined) => {
const response = await provider.sendTransaction(tx);
const receipt = await response.wait(confirmations);
Expand Down
44 changes: 38 additions & 6 deletions evm/update-static-gas-options.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const { ethers } = require('hardhat');
const {
getDefaultProvider,
utils: { parseUnits },
BigNumber,
} = ethers;

const { printInfo, mainProcessor, prompt } = require('./utils');
Expand Down Expand Up @@ -36,27 +37,58 @@ const minGasPrices = {
},
};

const minGasLimits = {
mainnet: {
filecoin: 3e8,
arbitrum: 20e8,
},
testnet: {
filecoin: 3e8,
arbitrum: 20e8,
},
};

async function getBaseFee(provider) {
const block = await provider.getBlock('latest');
return block.baseFeePerGas;
}

async function processCommand(_, chain, options) {
const { env, rpc, yes } = options;
const provider = rpc ? getDefaultProvider(rpc) : getDefaultProvider(chain.rpc);
const provider = getDefaultProvider(rpc || chain.rpc);

if (prompt(`Proceed with the static gasOption update on ${chalk.green(chain.name)}`, yes)) {
return;
}

const gasPriceWei = await provider.getGasPrice();
let gasPriceWei = await provider.getGasPrice();

if (chain.eip1559) {
const baseFee = await getBaseFee(provider);
const maxPriorityFeePerGas = await provider.send('eth_maxPriorityFeePerGas', []);
gasPriceWei = BigNumber.from(baseFee).add(BigNumber.from(maxPriorityFeePerGas));
}

printInfo(`${chain.name} gas price`, `${gasPriceWei / 1e9} gwei`);

const gasPrice = parseUnits(gasPriceWei.toString(), 'wei') * gasPriceMultiplier;
let gasPrice = parseUnits(gasPriceWei.toString(), 'wei') * gasPriceMultiplier;

const minGasLimit = (minGasLimits[env] || {})[chain.name.toLowerCase()] || defaultGasLimit;

if (!(chain.staticGasOptions && chain.staticGasOptions.gasLimit !== undefined)) {
chain.staticGasOptions = { gasLimit: defaultGasLimit };
chain.staticGasOptions = { gasLimit: minGasLimit };
}

const minGasPrice = ((minGasPrices[env] || {})[chain.name.toLowerCase()] || 0) * 1e9;
chain.staticGasOptions.gasPrice = gasPrice < minGasPrice ? minGasPrice : gasPrice;
gasPrice = gasPrice < minGasPrice ? minGasPrice : gasPrice;

if (chain.eip1559) {
chain.staticGasOptions.maxFeePerGas = gasPrice;
} else {
chain.staticGasOptions.gasPrice = gasPrice;
}

printInfo(`${chain.name} static gas price set to`, `${chain.staticGasOptions.gasPrice / 1e9} gwei`);
printInfo(`${chain.name} static gas price set to`, `${gasPrice / 1e9} gwei`);

printInfo(`staticGasOptions updated succesfully and stored in config file`);
}
Expand Down
Loading

0 comments on commit 3f42aeb

Please sign in to comment.