@faast/ethereum-payments
Version:
Library to assist in processing ethereum payments, such as deriving addresses and sweeping funds
1,072 lines (1,060 loc) • 70.7 kB
JavaScript
import BigNumber$1, { BigNumber } from 'bignumber.js';
import { Transaction } from 'ethereumjs-tx';
import { cloneDeep, omit } from 'lodash';
import { BaseConfig, KeyPairsConfigParam, CreateTransactionOptions, BaseUnsignedTransaction, BaseSignedTransaction, BaseTransactionInfo, BaseBroadcastResult, ResolvedFeeOption, FeeOption, FeeOptionCustom, FeeLevel, NetworkType, createUnitConverters, Payport, FeeRateType, TransactionStatus, PaymentsError, PaymentsErrorCode, DEFAULT_MAX_FEE_PERCENT, PaymentsFactory } from '@faast/payments-common';
import { enumCodec, optional, extendCodec, Numeric, isMatchingError, DelegateLogger, isNull, assertType, isType } from '@faast/ts-common';
import request, { post, get } from 'request-promise-native';
import { type, string, number, any, union, boolean, literal, object } from 'io-ts';
import Web3$1 from 'web3';
import { pubToAddress } from 'ethereumjs-util';
import { fromSeed, fromBase58 } from 'bip32';
import crypto from 'crypto';
import { ec as ec$1 } from 'elliptic';
import promiseRetry from 'promise-retry';
import InputDataDecoder from 'ethereum-input-data-decoder';
import Contract from 'web3-eth-contract';
var EthereumAddressFormat;
(function (EthereumAddressFormat) {
EthereumAddressFormat["Lowercase"] = "lowercase";
EthereumAddressFormat["Checksum"] = "checksum";
})(EthereumAddressFormat || (EthereumAddressFormat = {}));
const EthereumAddressFormatT = enumCodec(EthereumAddressFormat, 'EthereumAddressFormat');
const keys = type({
pub: string,
prv: string,
});
const xkeys = type({
xprv: string,
xpub: string,
});
const OptionalString = optional(string);
const OptionalNumber = optional(number);
const EthereumSignatory = type({
address: string,
keys,
xkeys,
}, 'EthereumSignatory');
const EthereumPaymentsUtilsConfig = extendCodec(BaseConfig, {}, {
fullNode: OptionalString,
parityNode: OptionalString,
blockbookNode: OptionalString,
gasStation: OptionalString,
symbol: OptionalString,
name: OptionalString,
decimals: number,
providerOptions: any,
web3: any,
}, 'EthereumPaymentsUtilsConfig');
const BaseEthereumPaymentsConfig = extendCodec(EthereumPaymentsUtilsConfig, {}, {
depositKeyIndex: OptionalNumber,
}, 'BaseEthereumPaymentsConfig');
const HdEthereumPaymentsConfig = extendCodec(BaseEthereumPaymentsConfig, {
hdKey: string,
}, 'HdEthereumPaymentsConfig');
const KeyPairEthereumPaymentsConfig = extendCodec(BaseEthereumPaymentsConfig, {
keyPairs: KeyPairsConfigParam,
}, 'KeyPairEthereumPaymentsConfig');
const BaseErc20PaymentsConfig = extendCodec(BaseEthereumPaymentsConfig, {
tokenAddress: string,
}, {
masterAddress: string,
}, 'BaseErc20PaymentsConfig');
const HdErc20PaymentsConfig = extendCodec(BaseErc20PaymentsConfig, {
hdKey: string,
}, 'HdErc20PaymentsConfig');
const KeyPairErc20PaymentsConfig = extendCodec(BaseErc20PaymentsConfig, {
keyPairs: KeyPairsConfigParam,
}, 'KeyPairErc20PaymentsConfig');
const Erc20PaymentsConfig = union([HdErc20PaymentsConfig, KeyPairErc20PaymentsConfig], 'Erc20PaymentsConfig');
const EthereumPaymentsConfig = union([
HdEthereumPaymentsConfig, KeyPairEthereumPaymentsConfig, HdErc20PaymentsConfig, KeyPairErc20PaymentsConfig,
], 'EthereumPaymentsConfig');
const EthereumTransactionOptions = extendCodec(CreateTransactionOptions, {}, {
data: string,
gas: Numeric,
proxyAddress: string,
}, 'EthereumTransactionOptions');
const EthereumUnsignedTransaction = extendCodec(BaseUnsignedTransaction, {
amount: string,
fee: string,
}, 'EthereumUnsignedTransaction');
const EthereumSignedTransaction = extendCodec(BaseSignedTransaction, {
data: type({
hex: string
}),
}, {}, 'EthereumSignedTransaction');
const EthereumTransactionInfo = extendCodec(BaseTransactionInfo, {}, {}, 'EthereumTransactionInfo');
const EthereumBroadcastResult = extendCodec(BaseBroadcastResult, {}, 'EthereumBroadcastResult');
const EthereumResolvedFeeOption = extendCodec(ResolvedFeeOption, {
gasPrice: string,
}, 'EthereumResolvedFeeOption');
const EthereumFeeOption = extendCodec(FeeOption, {}, {
isSweep: boolean,
}, 'EthereumFeeOption');
const EthereumFeeOptionCustom = extendCodec(FeeOptionCustom, {}, {
isSweep: boolean,
}, 'EthereumFeeOption');
const BnRounding = union([
literal(1),
literal(2),
literal(3),
literal(4),
literal(5),
literal(6),
literal(7),
literal(8),
]);
const BaseDenominationOptions = extendCodec(object, {}, {
rounding: BnRounding
}, 'BaseDenominationOptions');
const PACKAGE_NAME = 'ethereum-payments';
const ETH_SYMBOL = 'ETH';
const ETH_NAME = 'Ethereum';
const ETH_DECIMAL_PLACES = 18;
const DEFAULT_FULL_NODE = process.env.ETH_FULL_NODE_URL;
const DEFAULT_SOLIDITY_NODE = process.env.ETH_SOLIDITY_NODE_URL;
const DEFAULT_EVENT_SERVER = process.env.ETH_EVENT_SERVER_URL;
const DEFAULT_FEE_LEVEL = FeeLevel.Medium;
const MIN_CONFIRMATIONS = 0;
const DEFAULT_GAS_PRICE_IN_WEI = '50000000000';
const GAS_STATION_URL = 'https://ethgasstation.info';
const ETHEREUM_TRANSFER_COST = 50000;
const CONTRACT_DEPLOY_COST = 300000;
const TOKEN_SWEEP_COST = 300000;
const TOKEN_TRANSFER_COST = 300000;
const GAS_ESTIMATE_MULTIPLIER = 1.5;
const MIN_SWEEPABLE_WEI = String(21000 * 10e9);
const GAS_STATION_FEE_SPEED = {
[FeeLevel.Low]: 'safeLow',
[FeeLevel.Medium]: 'average',
[FeeLevel.High]: 'fast',
};
const MAXIMUM_GAS = {
'ETHEREUM_TRANSFER': ETHEREUM_TRANSFER_COST,
'CONTRACT_DEPLOY': CONTRACT_DEPLOY_COST,
'TOKEN_SWEEP': TOKEN_SWEEP_COST,
'TOKEN_TRANSFER': TOKEN_TRANSFER_COST,
};
const TOKEN_WALLET_DATA_LEGACY = '0x608060405234801561001057600080fd5b5061032a806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063b8dc491b1461002d575b005b61008f6004803603604081101561004357600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803573ffffffffffffffffffffffffffffffffffffffff1690602001909291905050506100a5565b6040518082815260200191505060405180910390f35b600073<address of owner>73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16146100f357600080fd5b73<address of owner>73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161461013f57600080fd5b600083905060008173ffffffffffffffffffffffffffffffffffffffff166370a08231306040518263ffffffff1660e01b8152600401808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060206040518083038186803b1580156101c357600080fd5b505afa1580156101d7573d6000803e3d6000fd5b505050506040513d60208110156101ed57600080fd5b810190808051906020019092919050505090508173ffffffffffffffffffffffffffffffffffffffff1663a9059cbb85836040518363ffffffff1660e01b8152600401808373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200182815260200192505050600060405180830381600087803b15801561028757600080fd5b505af115801561029b573d6000803e3d6000fd5b505050508373ffffffffffffffffffffffffffffffffffffffff167f69ca02dd4edd7bf0a4abb9ed3b7af3f14778db5d61921c7dc7cd545266326de2826040518082815260200191505060405180910390a250509291505056fea265627a7a72315820744abee8c455537f572ad5bce76da343e87aad96f4459adbea41be853699b3bb64736f6c63430005110032';
const TOKEN_WALLET_DATA = '0x6080604052600080546001600160a01b031916905534801561002057600080fd5b50600080546001600160a01b0319163317905561059e806100426000396000f3fe608060405234801561001057600080fd5b50600436106100565760003560e01c8062f55d9d1461005b5780632c8f55de146100835780638a6d2cc8146100bf578063b416663e146100f8578063beabacc814610175575b600080fd5b6100816004803603602081101561007157600080fd5b50356001600160a01b03166101ab565b005b6100816004803603608081101561009957600080fd5b508035906001600160a01b036020820135811691604081013590911690606001356101ff565b6100dc600480360360208110156100d557600080fd5b50356103b2565b604080516001600160a01b039092168252519081900360200190f35b610100610419565b6040805160208082528351818301528351919283929083019185019080838360005b8381101561013a578181015183820152602001610122565b50505050905090810190601f1680156101675780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b6100816004803603606081101561018b57600080fd5b506001600160a01b03813581169160208101359091169060400135610470565b6000546001600160a01b0316156101f35760405162461bcd60e51b81526004018080602001828103825260238152602001806105476023913960400191505060405180910390fd5b806001600160a01b0316ff5b6000546001600160a01b031615610265576000546001600160a01b03163314610265576040805162461bcd60e51b815260206004820152601360248201527239b2b73232b91034b9903737ba1037bbb732b960691b604482015290519081900360640190fd5b606061026f610419565b90506000858251602084016000f590506001600160a01b0381166102da576040805162461bcd60e51b815260206004820152601960248201527f437265617465323a204661696c6564206f6e206465706c6f7900000000000000604482015290519081900360640190fd5b604080516317d5759960e31b81526001600160a01b0387811660048301528681166024830152604482018690529151839283169163beabacc891606480830192600092919082900301818387803b15801561033457600080fd5b505af1158015610348573d6000803e3d6000fd5b50506040805162f55d9d60e01b815233600482015290516001600160a01b038516935062f55d9d9250602480830192600092919082900301818387803b15801561039157600080fd5b505af11580156103a5573d6000803e3d6000fd5b5050505050505050505050565b600060ff816103bf610419565b80516020918201206040805160f89590951b6001600160f81b031916858401523060601b60218601526035850196909652605580850191909152855180850390910181526075909301909452508051920191909120919050565b60408051733d602d80600a3d3981f3363d3d373d3d3d363d7360601b60208201523060601b60348201526e5af43d82803e903d91602b57fd5bf360881b604882015281516037818303018152605790910190915290565b6000546001600160a01b0316156104d6576000546001600160a01b031633146104d6576040805162461bcd60e51b815260206004820152601360248201527239b2b73232b91034b9903737ba1037bbb732b960691b604482015290519081900360640190fd5b6040805163a9059cbb60e01b81526001600160a01b038481166004830152602482018490529151859283169163a9059cbb91604480830192600092919082900301818387803b15801561052857600080fd5b505af115801561053c573d6000803e3d6000fd5b505050505050505056fe6d617374657220636f6e74726163742063616e6e6f742062652064657374726f796564a265627a7a72315820c25d8d6c752e7c31557effc6f9c1c9a9e0e5e870ce370c111d5246f44635baa464736f6c63430005110032';
const TOKEN_WALLET_ABI = JSON.parse('[{"inputs":[],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"constant":true,"inputs":[{"internalType":"uint256","name":"salt","type":"uint256"}],"name":"computeAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"addresspayable","name":"ethDestination","type":"address"}],"name":"destroy","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getProxyBytecode","outputs":[{"internalType":"bytes","name":"","type":"bytes"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"salt","type":"uint256"},{"internalType":"address","name":"erc20contract","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"proxyTransfer","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"erc20contract","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transfer","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"}]');
const TOKEN_WALLET_ABI_LEGACY = JSON.parse('[{"inputs":[],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"_to","type":"address"},{"indexed":false,"internalType":"uint256","name":"_value","type":"uint256"}],"name":"Transfer","type":"event"},{"payable":false,"stateMutability":"nonpayable","type":"fallback"},{"constant":false,"inputs":[{"internalType":"address","name":"erc20TokenSale","type":"address"},{"internalType":"address","name":"to","type":"address"}],"name":"sweep","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"}]');
const TOKEN_PROXY_DATA = '0x3d602d80600a3d3981f3363d3d373d3d3d363d73<address to proxy>5af43d82803e903d91602b57fd5bf3';
const TOKEN_METHODS_ABI = JSON.parse('[{"constant":true,"inputs":[{"name":"_owner","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"inputs":[],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"},{"indexed":false,"name":"data","type":"bytes"}],"name":"Transfer","type":"event"}]');
const DEPOSIT_KEY_INDEX = 0;
const PUBLIC_CONFIG_OMIT_FIELDS = [
'logger', 'fullNode', 'parityNode', 'gasStation', 'keyPairs', 'hdKey', 'providerOptions', 'web3'
];
const DEFAULT_ADDRESS_FORMAT = EthereumAddressFormat.Lowercase;
const web3 = new Web3$1();
const ec = new ec$1('secp256k1');
class EthereumBIP44 {
constructor(hdKey) {
this.parts = [
'm',
"44'",
"60'",
"0'",
'0'
];
this.key = hdKey;
}
static fromExtKey(xkey) {
if (['xprv', 'xpub'].includes(xkey.substring(0, 4))) {
return new EthereumBIP44(fromBase58(xkey));
}
throw new Error('Not extended key');
}
getAddress(index) {
const derived = this.deriveByIndex(index);
let address = pubToAddress(derived.publicKey, true);
return web3.utils.toChecksumAddress(`0x${address.toString('hex')}`).toLowerCase();
}
getPrivateKey(index) {
const derived = this.deriveByIndex(index);
if (!derived.privateKey) {
return '';
}
return `0x${derived.privateKey.toString('hex')}`;
}
getPublicKey(index) {
return this.deriveByIndex(index).publicKey.toString('hex');
}
getXPrivateKey(index) {
const key = this.deriveByIndex(index).toBase58();
return key.substring(0, 4) === 'xpub' ? '' : key;
}
getXPublicKey(index) {
return this.deriveByIndex(index).neutered().toBase58();
}
deriveByIndex(index) {
if (typeof index === 'undefined') {
return this.key;
}
const path = this.parts.slice(this.key.depth);
const keyPath = path.length > 0 ? path.join('/') + '/' : '';
return this.key.derivePath(`${keyPath}${index.toString()}`);
}
}
function deriveSignatory(xkey, index) {
const wallet = xkey ?
EthereumBIP44.fromExtKey(xkey) :
EthereumBIP44.fromExtKey(fromSeed(crypto.randomBytes(32)).toBase58());
return {
address: wallet.getAddress(index),
keys: {
prv: wallet.getPrivateKey(index) || '',
pub: wallet.getPublicKey(index),
},
xkeys: {
xprv: wallet.getXPrivateKey(index) || '',
xpub: wallet.getXPublicKey(index),
}
};
}
function isValidXkey(key) {
try {
EthereumBIP44.fromExtKey(key);
return true;
}
catch (e) {
return false;
}
}
const RETRYABLE_ERRORS = [
'request failed or timed out',
];
const MAX_RETRIES = 2;
function retryIfDisconnected(fn, logger) {
return promiseRetry((retry, attempt) => {
return fn().catch(async (e) => {
if (isMatchingError(e, RETRYABLE_ERRORS)) {
logger.log(`Retryable error during ethereum-payments call, retrying ${MAX_RETRIES - attempt} more times`, e.toString());
retry(e);
}
throw e;
});
}, {
retries: MAX_RETRIES,
});
}
class NetworkData {
constructor(eth, gasStationUrl = GAS_STATION_URL, parityUrl, logger) {
this.eth = eth;
this.gasStationUrl = gasStationUrl;
this.parityUrl = parityUrl;
this.logger = new DelegateLogger(logger, 'NetworkData');
}
async getNetworkData(txType, speed, from, to, data) {
const pricePerGasUnit = await this.getGasPrice(speed);
const nonce = await this.getNonce(from);
const amountOfGas = await this.estimateGas({ from, to, data }, txType);
return {
pricePerGasUnit,
amountOfGas,
nonce,
};
}
async getNonce(address) {
const web3Nonce = await this.getWeb3Nonce(address) || '0';
const parityNonce = await this.getParityNonce(address) || '0';
const nonce = BigNumber.maximum(web3Nonce, parityNonce);
return nonce.toNumber() ? nonce.toString() : '0';
}
async getGasPrice(speed) {
let gasPrice = await this.getGasStationGasPrice(speed);
if (gasPrice)
return gasPrice;
gasPrice = await this.getWeb3GasPrice();
if (gasPrice)
return gasPrice;
return DEFAULT_GAS_PRICE_IN_WEI;
}
async estimateGas(txObject, txType) {
try {
let gas = await this._retryDced(() => this.eth.estimateGas({ ...txObject }));
if (gas > 21000) {
gas = gas * GAS_ESTIMATE_MULTIPLIER;
}
const maxGas = MAXIMUM_GAS[txType];
if (gas > maxGas) {
gas = maxGas;
}
const result = Math.ceil(gas);
this.logger.debug(`Estimated gas limit of ${result} for ${txType}`);
return result;
}
catch (e) {
this.logger.warn(`Failed to estimate gas for ${txType} -- ${e}`);
return MAXIMUM_GAS[txType];
}
}
async getWeb3Nonce(address) {
try {
const nonce = await this._retryDced(() => this.eth.getTransactionCount(address, 'pending'));
return (new BigNumber(nonce)).toString();
}
catch (e) {
return '';
}
}
async getParityNonce(address) {
const data = {
method: 'parity_nextNonce',
params: [address],
id: 1,
jsonrpc: '2.0'
};
const options = {
url: this.parityUrl || '',
json: data
};
let body;
try {
body = await post(options);
}
catch (e) {
this.logger.warn('Failed to retrieve nonce from parity - ', e.toString());
return '';
}
if (!body || !body.result) {
this.logger.warn('Bad result or missing fields in parity nextNonce response', body);
return '';
}
return (new BigNumber(body.result, 16)).toString();
}
async getGasStationGasPrice(level) {
const hasKey = /\?api-key=/.test(this.gasStationUrl || '');
const options = {
url: hasKey ? `${this.gasStationUrl}` : `${this.gasStationUrl}/json/ethgasAPI.json`,
json: true,
timeout: 5000
};
let body;
try {
body = await this._retryDced(() => get(options));
}
catch (e) {
this.logger.warn('Failed to retrieve gas price from ethgasstation - ', e.toString());
return '';
}
const speed = GAS_STATION_FEE_SPEED[level];
if (!(body && body.blockNum && body[speed])) {
this.logger.warn('Bad result or missing fields in ethgasstation response', body);
return '';
}
const price10xGwei = body[speed];
const gwei = new BigNumber(price10xGwei).dividedBy(10);
this.logger.log(`Retrieved gas price of ${gwei} Gwei from ethgasstation using speed ${speed}`);
return gwei.multipliedBy(1e9).dp(0, BigNumber.ROUND_DOWN).toFixed();
}
async getWeb3GasPrice() {
try {
const wei = new BigNumber(await this._retryDced(() => this.eth.getGasPrice()));
this.logger.log(`Retrieved gas price of ${wei.div(1e9)} Gwei from web3`);
return wei.dp(0, BigNumber.ROUND_DOWN).toFixed();
}
catch (e) {
this.logger.warn('Failed to retrieve gas price from web3 - ', e.toString());
return '';
}
}
async _retryDced(fn) {
return retryIfDisconnected(fn, this.logger);
}
}
class EthereumPaymentsUtils {
constructor(config) {
var _a, _b, _c;
this.logger = new DelegateLogger(config.logger, PACKAGE_NAME);
this.networkType = config.network || NetworkType.Mainnet;
this.coinName = (_a = config.name) !== null && _a !== void 0 ? _a : ETH_NAME;
this.coinSymbol = (_b = config.symbol) !== null && _b !== void 0 ? _b : ETH_SYMBOL;
this.coinDecimals = (_c = config.decimals) !== null && _c !== void 0 ? _c : ETH_DECIMAL_PLACES;
this.server = config.fullNode || null;
let provider;
if (config.web3) {
this.web3 = config.web3;
}
else if (isNull(this.server)) {
this.web3 = new Web3$1();
}
else if (this.server.startsWith('http')) {
provider = new Web3$1.providers.HttpProvider(this.server, config.providerOptions);
this.web3 = new Web3$1(provider);
}
else if (this.server.startsWith('ws')) {
provider = new Web3$1.providers.WebsocketProvider(this.server, config.providerOptions);
this.web3 = new Web3$1(provider);
}
else {
throw new Error(`Invalid ethereum payments fullNode, must start with http or ws: ${this.server}`);
}
if (provider && process.env.NODE_DEBUG && process.env.NODE_DEBUG.includes('ethereum-payments')) {
const send = provider.send;
provider.send = (payload, cb) => {
this.logger.debug(`web3 provider request ${this.server}`, payload);
send.call(provider, payload, (error, result) => {
if (error) {
this.logger.debug(`web3 provider response error ${this.server}`, error);
}
else {
this.logger.debug(`web3 provider response result ${this.server}`, result);
}
cb(error, result);
});
};
}
this.eth = this.web3.eth;
this.gasStation = new NetworkData(this.eth, config.gasStation, config.parityNode, this.logger);
const unitConverters = createUnitConverters(this.coinDecimals);
this.toMainDenominationBigNumber = unitConverters.toMainDenominationBigNumber;
this.toBaseDenominationBigNumber = unitConverters.toBaseDenominationBigNumber;
this.toMainDenomination = unitConverters.toMainDenominationString;
this.toBaseDenomination = unitConverters.toBaseDenominationString;
const ethUnitConverters = createUnitConverters(ETH_DECIMAL_PLACES);
this.toMainDenominationBigNumberEth = ethUnitConverters.toMainDenominationBigNumber;
this.toBaseDenominationBigNumberEth = ethUnitConverters.toBaseDenominationBigNumber;
this.toMainDenominationEth = ethUnitConverters.toMainDenominationString;
this.toBaseDenominationEth = ethUnitConverters.toBaseDenominationString;
}
async init() { }
async destroy() { }
isValidAddress(address, options = {}) {
const { format } = options;
if (format === EthereumAddressFormat.Lowercase) {
return this.web3.utils.isAddress(address) &&
address === address.toLowerCase();
}
else if (format === EthereumAddressFormat.Checksum) {
return this.web3.utils.checkAddressChecksum(address);
}
return this.web3.utils.isAddress(address);
}
standardizeAddress(address, options) {
var _a;
if (!this.web3.utils.isAddress(address)) {
return null;
}
const format = assertType(EthereumAddressFormatT, (_a = options === null || options === void 0 ? void 0 : options.format) !== null && _a !== void 0 ? _a : DEFAULT_ADDRESS_FORMAT, 'format');
if (format === EthereumAddressFormat.Lowercase) {
return address.toLowerCase();
}
else {
return this.web3.utils.toChecksumAddress(address);
}
}
isValidExtraId(extraId) {
return false;
}
isValidPayport(payport) {
return Payport.is(payport) && !this._getPayportValidationMessage(payport);
}
validatePayport(payport) {
const message = this._getPayportValidationMessage(payport);
if (message) {
throw new Error(message);
}
}
getPayportValidationMessage(payport) {
try {
payport = assertType(Payport, payport, 'payport');
}
catch (e) {
return e.message;
}
return this._getPayportValidationMessage(payport);
}
isValidXprv(xprv) {
return isValidXkey(xprv) && xprv.substring(0, 4) === 'xprv';
}
isValidXpub(xpub) {
return isValidXkey(xpub) && xpub.substring(0, 4) === 'xpub';
}
isValidPrivateKey(prv) {
try {
return Boolean(this.web3.eth.accounts.privateKeyToAccount(prv));
}
catch (e) {
return false;
}
}
privateKeyToAddress(prv) {
let key;
if (prv.substring(0, 2) === '0x') {
key = prv;
}
else {
key = `0x${prv}`;
}
return this.web3.eth.accounts.privateKeyToAccount(key).address.toLowerCase();
}
_getPayportValidationMessage(payport) {
try {
const { address } = payport;
if (!(this.isValidAddress(address))) {
return 'Invalid payport address';
}
}
catch (e) {
return 'Invalid payport address';
}
return undefined;
}
async getFeeRateRecommendation(level) {
const gasPrice = await this.gasStation.getGasPrice(level);
return {
feeRate: gasPrice,
feeRateType: FeeRateType.BasePerWeight,
};
}
async _retryDced(fn) {
return retryIfDisconnected(fn, this.logger);
}
async getCurrentBlockNumber() {
return this._retryDced(() => this.eth.getBlockNumber());
}
isAddressBalanceSweepable(balanceEth) {
return this.toBaseDenominationBigNumberEth(balanceEth).gt(MIN_SWEEPABLE_WEI);
}
async getAddressBalance(address) {
const balance = await this._retryDced(() => this.eth.getBalance(address));
const confirmedBalance = this.toMainDenomination(balance).toString();
const sweepable = this.isAddressBalanceSweepable(confirmedBalance);
return {
confirmedBalance,
unconfirmedBalance: '0',
spendableBalance: confirmedBalance,
sweepable,
requiresActivation: false,
};
}
async getAddressNextSequenceNumber(address) {
return this.gasStation.getNonce(address);
}
async getAddressUtxos() {
return [];
}
async getTransactionInfo(txid) {
const minConfirmations = MIN_CONFIRMATIONS;
const tx = await this._retryDced(() => this.eth.getTransaction(txid));
if (!tx) {
throw new Error(`Transaction ${txid} not found`);
}
const currentBlockNumber = await this.getCurrentBlockNumber();
let txInfo = await this._retryDced(() => this.eth.getTransactionReceipt(txid));
tx.from = tx.from ? tx.from.toLowerCase() : '';
tx.to = tx.to ? tx.to.toLowerCase() : '';
if (!txInfo) {
txInfo = {
transactionHash: tx.hash,
from: tx.from,
to: tx.to,
status: true,
blockNumber: 0,
cumulativeGasUsed: 0,
gasUsed: 0,
transactionIndex: 0,
blockHash: '',
logs: [],
logsBloom: ''
};
return {
id: txid,
amount: this.toMainDenomination(tx.value),
toAddress: tx.to ? tx.to.toLowerCase() : null,
fromAddress: tx.from ? tx.from.toLowerCase() : null,
toExtraId: null,
fromIndex: null,
toIndex: null,
fee: this.toMainDenomination((new BigNumber$1(tx.gasPrice)).multipliedBy(tx.gas)),
sequenceNumber: tx.nonce,
weight: tx.gas,
isExecuted: false,
isConfirmed: false,
confirmations: 0,
confirmationId: null,
confirmationTimestamp: null,
currentBlockNumber: currentBlockNumber,
status: TransactionStatus.Pending,
data: {
...tx,
...txInfo,
currentBlock: currentBlockNumber
},
};
}
let isConfirmed = false;
let confirmationTimestamp = null;
let confirmations = 0;
if (tx.blockNumber) {
confirmations = currentBlockNumber - tx.blockNumber;
if (confirmations > minConfirmations) {
isConfirmed = true;
const txBlock = await this._retryDced(() => this.eth.getBlock(tx.blockNumber));
confirmationTimestamp = new Date(Number(txBlock.timestamp) * 1000);
}
}
let status = TransactionStatus.Pending;
if (isConfirmed) {
status = TransactionStatus.Confirmed;
if (txInfo.hasOwnProperty('status') && (txInfo.status === false || txInfo.status.toString() === 'false')) {
status = TransactionStatus.Failed;
}
}
txInfo.from = tx.from;
txInfo.to = tx.to;
return {
id: txid,
amount: this.toMainDenomination(tx.value),
toAddress: tx.to ? tx.to.toLowerCase() : null,
fromAddress: tx.from ? tx.from.toLowerCase() : null,
toExtraId: null,
fromIndex: null,
toIndex: null,
fee: this.toMainDenomination((new BigNumber$1(tx.gasPrice)).multipliedBy(txInfo.gasUsed)),
sequenceNumber: tx.nonce,
weight: txInfo.gasUsed,
isExecuted: status !== TransactionStatus.Failed,
isConfirmed,
confirmations,
confirmationId: tx.blockHash,
confirmationTimestamp,
status,
currentBlockNumber: currentBlockNumber,
data: {
...tx,
...txInfo,
currentBlock: currentBlockNumber
},
};
}
}
class BaseEthereumPayments extends EthereumPaymentsUtils {
constructor(config) {
super(config);
this.config = config;
this.depositKeyIndex = (typeof config.depositKeyIndex === 'undefined') ? DEPOSIT_KEY_INDEX : config.depositKeyIndex;
}
getFullConfig() {
return this.config;
}
async resolvePayport(payport) {
if (typeof payport === 'number') {
return this.getPayport(payport);
}
else if (typeof payport === 'string') {
if (!this.isValidAddress(payport)) {
throw new Error(`Invalid Ethereum address: ${payport}`);
}
return { address: payport.toLowerCase() };
}
if (!this.isValidPayport(payport)) {
throw new Error(`Invalid Ethereum payport: ${JSON.stringify(payport)}`);
}
else {
if (!this.isValidAddress(payport.address)) {
throw new Error(`Invalid Ethereum payport: ${JSON.stringify(payport)}`);
}
}
return { ...payport, address: payport.address.toLowerCase() };
}
async resolveFromTo(from, to) {
const fromPayport = await this.getPayport(from);
const toPayport = await this.resolvePayport(to);
return {
fromAddress: fromPayport.address,
fromIndex: from,
fromExtraId: fromPayport.extraId,
fromPayport,
toAddress: toPayport.address,
toIndex: typeof to === 'number' ? to : null,
toExtraId: toPayport.extraId,
toPayport,
};
}
async resolveFeeOption(feeOption, amountOfGas = ETHEREUM_TRANSFER_COST) {
if (new BigNumber(amountOfGas).dp() > 0) {
throw new Error(`Amount of gas must be a whole number ${amountOfGas}`);
}
return isType(FeeOptionCustom, feeOption)
? this.resolveCustomFeeOption(feeOption, amountOfGas)
: this.resolveLeveledFeeOption(feeOption.feeLevel, amountOfGas);
}
resolveCustomFeeOption(feeOption, amountOfGas) {
const { feeRate, feeRateType } = feeOption;
let gasPrice;
if (feeRateType === FeeRateType.BasePerWeight) {
gasPrice = new BigNumber(feeRate);
}
else {
const feeRateBase = feeRateType === FeeRateType.Main
? this.toBaseDenominationBigNumberEth(feeRate)
: new BigNumber(feeRate);
gasPrice = feeRateBase.dividedBy(amountOfGas);
}
gasPrice = gasPrice.dp(0, BigNumber.ROUND_DOWN);
const feeBase = gasPrice.multipliedBy(amountOfGas);
const feeMain = this.toMainDenominationBigNumberEth(feeBase);
return {
targetFeeRate: feeOption.feeRate,
targetFeeLevel: FeeLevel.Custom,
targetFeeRateType: feeOption.feeRateType,
feeBase: feeBase.toFixed(),
feeMain: feeMain.toFixed(),
gasPrice: gasPrice.toFixed(),
};
}
async resolveLeveledFeeOption(feeLevel = DEFAULT_FEE_LEVEL, amountOfGas) {
const gasPrice = new BigNumber(await this.gasStation.getGasPrice(feeLevel));
const feeBase = gasPrice.multipliedBy(amountOfGas).toFixed();
return {
targetFeeRate: gasPrice.toFixed(),
targetFeeLevel: feeLevel,
targetFeeRateType: FeeRateType.BasePerWeight,
feeBase,
feeMain: this.toMainDenominationEth(feeBase),
gasPrice: gasPrice.toFixed(),
};
}
requiresBalanceMonitor() {
return false;
}
async getAvailableUtxos() {
return [];
}
async getUtxos() {
return [];
}
usesSequenceNumber() {
return true;
}
usesUtxos() {
return false;
}
async getBalance(resolveablePayport) {
const payport = await this.resolvePayport(resolveablePayport);
return this.getAddressBalance(payport.address);
}
async isSweepableBalance(balance) {
return this.isAddressBalanceSweepable(balance);
}
async getNextSequenceNumber(payport) {
const resolvedPayport = await this.resolvePayport(payport);
return this.getAddressNextSequenceNumber(resolvedPayport.address);
}
async createTransaction(from, to, amountEth, options = {}) {
this.logger.debug('createTransaction', from, to, amountEth);
return this.createTransactionObject(from, to, amountEth, options);
}
async createServiceTransaction(from = this.depositKeyIndex, options = {}) {
this.logger.debug('createDepositTransaction', from);
return this.createTransactionObject(from, undefined, '', options);
}
async createSweepTransaction(from, to, options = {}) {
this.logger.debug('createSweepTransaction', from, to);
return this.createTransactionObject(from, to, 'max', options);
}
async createMultiOutputTransaction(from, to, options = {}) {
return null;
}
async signTransaction(unsignedTx) {
const fromPrivateKey = await this.getPrivateKey(unsignedTx.fromIndex);
const payport = await this.getPayport(unsignedTx.fromIndex);
const unsignedRaw = cloneDeep(unsignedTx.data);
const extraParam = this.networkType === NetworkType.Testnet ? { chain: 'ropsten' } : undefined;
const tx = new Transaction(unsignedRaw, extraParam);
const key = Buffer.from(fromPrivateKey.slice(2), 'hex');
tx.sign(key);
const result = {
...unsignedTx,
id: `0x${tx.hash().toString('hex')}`,
status: TransactionStatus.Signed,
data: {
hex: `0x${tx.serialize().toString('hex')}`
}
};
this.logger.debug('signTransaction result', result);
return result;
}
sendTransactionWithoutConfirmation(txHex) {
return this._retryDced(() => new Promise((resolve, reject) => this.eth.sendSignedTransaction(txHex)
.on('transactionHash', resolve)
.on('error', reject)));
}
async broadcastTransaction(tx) {
if (tx.status !== TransactionStatus.Signed) {
throw new Error(`Tx ${tx.id} has not status ${TransactionStatus.Signed}`);
}
try {
if (this.config.blockbookNode) {
const url = `${this.config.blockbookNode}/api/sendtx/${tx.data.hex}`;
request
.get(url, { json: true })
.then((res) => this.logger.log(`Successful secondary broadcast to blockbook ethereum ${res.result}`))
.catch((e) => this.logger.log(`Failed secondary broadcast to blockbook ethereum ${tx.id}: ${url} - ${e}`));
}
const txId = await this.sendTransactionWithoutConfirmation(tx.data.hex);
return {
id: txId,
};
}
catch (e) {
if (isMatchingError(e, ['already known'])) {
this.logger.log(`Ethereum broadcast tx already known ${tx.id}`);
return {
id: tx.id
};
}
this.logger.warn(`Ethereum broadcast tx unsuccessful ${tx.id}: ${e.message}`);
if (isMatchingError(e, ['nonce too low'])) {
throw new PaymentsError(PaymentsErrorCode.TxSequenceCollision, e.message);
}
throw new Error(`Ethereum broadcast tx unsuccessful: ${tx.id} ${e.message}`);
}
}
async gasOptionOrEstimate(options, txObject, txType) {
if (options.gas) {
return new BigNumber(options.gas).dp(0, BigNumber.ROUND_UP).toNumber();
}
return this.gasStation.estimateGas(txObject, txType);
}
async createTransactionObject(from, to, amountEth, options = {}) {
var _a;
const serviceFlag = (amountEth === '' && typeof to === 'undefined');
const sweepFlag = amountEth === 'max';
const txType = serviceFlag ? 'CONTRACT_DEPLOY' : 'ETHEREUM_TRANSFER';
const fromPayport = await this.getPayport(from);
const toPayport = serviceFlag ? { address: '' } : await this.resolvePayport(to);
const toIndex = typeof to === 'number' ? to : null;
const txConfig = { from: fromPayport.address };
if (serviceFlag) {
if (options.data) {
txConfig.data = options.data;
}
else if (options.proxyAddress) {
txConfig.data = TOKEN_PROXY_DATA
.replace(/<address to proxy>/g, options.proxyAddress.replace('0x', '').toLowerCase());
}
else {
txConfig.data = TOKEN_WALLET_DATA;
}
}
if (toPayport.address) {
txConfig.to = toPayport.address;
}
const amountOfGas = await this.gasOptionOrEstimate(options, txConfig, txType);
const feeOption = await this.resolveFeeOption(options, amountOfGas);
const { confirmedBalance: balanceEth } = await this.getBalance(fromPayport);
const nonce = options.sequenceNumber || await this.getNextSequenceNumber(fromPayport.address);
const { feeMain, feeBase } = feeOption;
const feeWei = new BigNumber(feeBase);
const maxFeePercent = new BigNumber((_a = options.maxFeePercent) !== null && _a !== void 0 ? _a : DEFAULT_MAX_FEE_PERCENT);
const balanceWei = this.toBaseDenominationBigNumberEth(balanceEth);
let amountWei = new BigNumber(0);
if (balanceWei.eq(0)) {
throw new PaymentsError(PaymentsErrorCode.TxInsufficientBalance, `${fromPayport.address} No balance available (${balanceEth})`);
}
if (sweepFlag) {
amountWei = balanceWei.minus(feeWei);
if (balanceWei.isLessThan(feeWei)) {
throw new PaymentsError(PaymentsErrorCode.TxFeeTooHigh, `${fromPayport.address} Insufficient balance (${balanceEth}) to pay sweep fee of ${feeMain}`);
}
if (feeWei.gt(maxFeePercent.times(balanceWei))) {
throw new PaymentsError(PaymentsErrorCode.TxFeeTooHigh, `${fromPayport.address} Sweep fee (${feeMain}) exceeds max fee percent (${maxFeePercent}%) of address balance (${balanceEth})`);
}
}
else if (!sweepFlag && !serviceFlag) {
amountWei = this.toBaseDenominationBigNumberEth(amountEth);
if (amountWei.plus(feeWei).isGreaterThan(balanceWei)) {
throw new PaymentsError(PaymentsErrorCode.TxInsufficientBalance, `${fromPayport.address} Insufficient balance (${balanceEth}) to send ${amountEth} including fee of ${feeOption.feeMain}`);
}
if (feeWei.gt(maxFeePercent.times(amountWei))) {
throw new PaymentsError(PaymentsErrorCode.TxFeeTooHigh, `${fromPayport.address} Sweep fee (${feeMain}) exceeds max fee percent (${maxFeePercent}%) of send amount (${amountEth})`);
}
}
else {
if (balanceWei.isLessThan(feeWei)) {
throw new PaymentsError(PaymentsErrorCode.TxFeeTooHigh, `${fromPayport.address} Insufficient balance (${balanceEth}) to pay contract deploy fee of ${feeOption.feeMain}`);
}
}
const result = {
id: null,
status: TransactionStatus.Unsigned,
fromAddress: fromPayport.address,
fromIndex: from,
toAddress: serviceFlag ? '' : toPayport.address,
toIndex,
toExtraId: null,
amount: serviceFlag ? '' : this.toMainDenomination(amountWei),
fee: feeOption.feeMain,
targetFeeLevel: feeOption.targetFeeLevel,
targetFeeRate: feeOption.targetFeeRate,
targetFeeRateType: feeOption.targetFeeRateType,
weight: amountOfGas,
sequenceNumber: nonce.toString(),
data: {
...txConfig,
value: `0x${amountWei.toString(16)}`,
gas: `0x${amountOfGas.toString(16)}`,
gasPrice: `0x${(new BigNumber(feeOption.gasPrice)).toString(16)}`,
nonce: `0x${(new BigNumber(nonce)).toString(16)}`,
},
};
this.logger.debug('createTransactionObject result', result);
return result;
}
}
class HdEthereumPayments extends BaseEthereumPayments {
constructor(config) {
super(config);
try {
this.xprv = '';
this.xpub = '';
if (this.isValidXpub(config.hdKey)) {
this.xpub = config.hdKey;
}
else if (this.isValidXprv(config.hdKey)) {
this.xprv = config.hdKey;
this.xpub = deriveSignatory(config.hdKey, 0).xkeys.xpub;
}
else {
throw new Error(config.hdKey);
}
}
catch (e) {
throw new Error(`Account must be a valid xprv or xpub: ${e.message}`);
}
}
static generateNewKeys() {
return deriveSignatory();
}
getXpub() {
return this.xpub;
}
getPublicConfig() {
return {
...omit(this.getFullConfig(), PUBLIC_CONFIG_OMIT_FIELDS),
depositKeyIndex: this.depositKeyIndex,
hdKey: this.getXpub(),
};
}
getAccountId(index) {
return this.getXpub();
}
getAccountIds() {
return [this.getXpub()];
}
async getPayport(index) {
const { address } = deriveSignatory(this.getXpub(), index);
if (!this.isValidAddress(address)) {
throw new Error(`Cannot get address ${index} - validation failed for derived address`);
}
return { address };
}
async getPrivateKey(index) {
if (!this.xprv) {
throw new Error(`Cannot get private key ${index} - HdEthereumPayments was created with an xpub`);
}
return deriveSignatory(deriveSignatory(this.xprv, 0).xkeys.xprv, index).keys.prv;
}
}
class KeyPairEthereumPayments extends BaseEthereumPayments {
constructor(config) {
super(config);
this.addresses = {};
this.privateKeys = {};
this.addressIndices = {};
Object.entries(config.keyPairs).forEach(([key, value]) => {
if (typeof value === 'undefined' || value === null) {
return;
}
const i = Number.parseInt(key);
let address;
let pkey = null;
if (this.web3.utils.isAddress(value)) {
address = value.toLowerCase();
}
else if (this.isValidPrivateKey(value)) {
address = this.privateKeyToAddress(value);
}
else if (this.isValidXprv(value)) {
const signatory = deriveSignatory(value);
address = signatory.address;
pkey = signatory.keys.prv;
}
else {
throw new Error(`KeyPairEthereumPaymentsConfig.keyPairs[${i}] is not a valid private key or address`);
}
if (typeof this.addressIndices[address] === 'number') {
return;
}
this.addresses[i] = address;
this.privateKeys[i] = pkey;
this.addressIndices[address] = i;
});
}
getPublicConfig() {
return {
...omit(this.getFullConfig(), PUBLIC_CONFIG_OMIT_FIELDS),
keyPairs: this.addresses,
};
}
getAccountId(index) {
const accountId = this.addresses[index] || '';
if (!accountId) {
throw new Error(`No KeyPairEthereumPayments account configured at index ${index}`);
}
return accountId;
}
getAccountIds() {
return Object.keys(this.addressIndices);
}
async getPayport(index) {
const address = this.addresses[index] || '';
if (!this.isValidAddress(address)) {
throw new Error(`Cannot get address ${index} - keyPair[${index}] is undefined or invalid address`);
}
return { address };
}
async getPrivateKey(index) {
const privateKey = this.privateKeys[index] || '';
if (!this.isValidPrivateKey(privateKey)) {
throw new Error(`Cannot get private key ${index} - keyPair[${index}] is undefined`);
}
return privateKey;
}
}
class EthereumConnectionManager {
constructor() {
this.connections = {};
}
getConnection(connected) {
return connected.web3;
}
getConnectionUrl(config) {
return config.fullNode || null;
}
setConnection(config, web3) {
return config.web3 = web3;
}
}
const Web3 = require('web3');
const web3$1 = new Web3({});
function deriveAddress(creatorAddress, salt, hashed = false) {
const address = creatorAddress.replace(/0x/, '').toLowerCase();
const proxy = web3$1.utils.sha3(TOKEN_PROXY_DATA.replace(/<address to proxy>/g, address))
.replace(/0x/, '').toLowerCase();
let saltHash = (hashed ? salt : web3$1.utils.sha3(`0x${salt}`)).replace(/0x/, '').toLowerCase();
while (saltHash.length < 64) {
saltHash = `0${saltHash}`;
}
return `0x${web3$1.utils.sha3(`0xff${address}${saltHash}${proxy}`).slice(-40)}`.toLowerCase();
}
const ERC20_TRANSFER = '0xa9059cbb';
const ERC20_SWEEP_LEGACY = '0xb8dc491b';
const ERC20_SWEEP = '0x2c8f55de';
const ERC20_SWEEP_CONTRACT_DEPLOY = '0x60806040';
const ERC20_SWEEP_CONTRACT_DEPLOY_LEGACY = '0x60606040';
const ERC20_PROXY = '0x3d602d806';
const L