crypto-wallet-core
Version:
A multi-currency support library for address derivation, private key creation, and transaction creation
170 lines (153 loc) • 5.25 kB
text/typescript
import { ethers } from 'ethers';
import Web3 from 'web3';
import { AbiItem } from 'web3-utils';
import { Constants } from '../../constants';
import {
EVM_CHAIN_DEFAULT_TESTNET as defaultTestnet,
EVM_CHAIN_NETWORK_TO_CHAIN_ID as chainIds
} from '../../constants/chains';
import { Key } from '../../derivation';
import { MULTISENDAbi } from '../erc20/abi';
const utils = require('web3-utils');
const { toBN } = Web3.utils;
export class ETHTxProvider {
chain: string;
constructor(chain = 'ETH') {
this.chain = chain;
}
create(params: {
recipients: Array<{ address: string; amount: string }>;
nonce: number;
gasPrice?: number;
data: string;
gasLimit: number;
network: string;
chainId?: number;
contractAddress?: string;
maxGasFee?: number;
priorityGasFee?: number;
txType?: number;
}) {
const { recipients, nonce, gasPrice, gasLimit, network, contractAddress, maxGasFee, priorityGasFee, txType } = params;
let { data } = params;
let to;
let amount;
if (recipients.length > 1) {
if (!contractAddress) {
throw new Error('Multiple recipients requires use of multi-send contract, please specify contractAddress');
}
const addresses = [];
const amounts = [];
amount = toBN(0);
for (let recipient of recipients) {
addresses.push(recipient.address);
amounts.push(toBN(this._valueToString(recipient.amount)));
amount = amount.add(toBN(this._valueToString(recipient.amount)));
}
const multisendContract = this.getMultiSendContract(contractAddress);
data = data || multisendContract.methods.sendEth(addresses, amounts).encodeABI();
to = contractAddress;
} else {
to = recipients[0].address;
amount = toBN(this._valueToString(recipients[0].amount));
}
let { chainId } = params;
chainId = chainId || this.getChainId(network);
let txData: any = {
nonce: utils.toHex(nonce),
gasLimit: utils.toHex(gasLimit),
to,
data,
value: utils.toHex(amount),
chainId
};
if (maxGasFee && (txType == null || txType >= 2)) {
txData.maxFeePerGas = utils.toHex(maxGasFee);
txData.maxPriorityFeePerGas = utils.toHex(priorityGasFee || this.getPriorityFeeMinimum(chainId));
txData.type = 2;
} else {
txData.gasPrice = utils.toHex(gasPrice);
txData.type = txType || 0;
}
return ethers.Transaction.from(txData).unsignedSerialized;
}
_valueToString(value) {
const type = typeof value;
if (type === 'number') {
return (value).toLocaleString('fullwide', { useGrouping: false });
} else if (type === 'bigint') {
return value.toString()
} else if (type === 'string') {
return value;
} else {
throw new Error(`Unexpected type of: ${type}`);
}
}
getMultiSendContract(tokenContractAddress: string) {
const web3 = new Web3();
return new web3.eth.Contract(MULTISENDAbi as AbiItem[], tokenContractAddress);
}
getPriorityFeeMinimum(chainId: number) {
const chain = Constants.EVM_CHAIN_ID_TO_CHAIN[chainId];
return Constants.FEE_MINIMUMS[chain]?.priority || 0;
}
getChainId(network: string) {
if (network === 'testnet') {
network = defaultTestnet[this.chain];
}
return chainIds[`${this.chain}_${network}`] || chainIds[`${this.chain}_mainnet`];
}
getSignatureObject(params: { tx: string; key: Key }) {
const { tx, key } = params;
// To comply with new ethers
let k = key.privKey;
if (k.substring(0, 2) != '0x') {
k = '0x' + k;
}
const signingKey = new ethers.SigningKey(k);
return signingKey.sign(ethers.keccak256(tx));
}
getSignature(params: { tx: string; key: Key }) {
const signatureHex = this.getSignatureObject(params).serialized;
return signatureHex;
}
getHash(params: { tx: string }) {
const { tx } = params;
// tx must be signed for hash to exist
return ethers.Transaction.from(tx).hash;
}
applySignature(params: { tx: string; signature: any }) {
let { tx, signature } = params;
const parsedTx = ethers.Transaction.from(tx);
const { gasPrice, maxFeePerGas, maxPriorityFeePerGas } = parsedTx;
// backwards compatibility
if (maxFeePerGas) {
parsedTx.maxFeePerGas = maxFeePerGas;
parsedTx.maxPriorityFeePerGas = maxPriorityFeePerGas;
parsedTx.type = 2;
} else if (!gasPrice) {
throw new Error('either gasPrice or maxFeePerGas is required');
}
// Verify the signature
let valid = false;
let signedTx;
try {
parsedTx.signature = ethers.Signature.from(signature);
signedTx = ethers.Transaction.from(parsedTx);
if (signedTx.hash) {
const recoveredAddress = ethers.recoverAddress(ethers.keccak256(tx), signature);
const expectedAddress = parsedTx.from;
valid = recoveredAddress === expectedAddress
}
} catch {}
if (!valid) {
throw new Error('invalid signature');
}
return signedTx.serialized;
}
sign(params: { tx: string; key: Key }) {
const { tx, key } = params;
const signature = this.getSignatureObject({ tx, key });
return this.applySignature({ tx, signature });
}
}