UNPKG

@faast/ethereum-payments

Version:

Library to assist in processing ethereum payments, such as deriving addresses and sweeping funds

949 lines (939 loc) 78.5 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('bignumber.js'), require('ethereumjs-tx'), require('lodash'), require('@faast/payments-common'), require('@faast/ts-common'), require('request-promise-native'), require('io-ts'), require('web3'), require('ethereumjs-util'), require('bip32'), require('crypto'), require('elliptic'), require('promise-retry'), require('ethereum-input-data-decoder'), require('web3-eth-contract')) : typeof define === 'function' && define.amd ? define(['exports', 'bignumber.js', 'ethereumjs-tx', 'lodash', '@faast/payments-common', '@faast/ts-common', 'request-promise-native', 'io-ts', 'web3', 'ethereumjs-util', 'bip32', 'crypto', 'elliptic', 'promise-retry', 'ethereum-input-data-decoder', 'web3-eth-contract'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.faastEthereumPayments = {}, global.BigNumber, global.ethereumjsTx, global.lodash, global.paymentsCommon, global.tsCommon, global.request, global.t, global.Web3$1, global.ethereumjsUtil, global.bip32, global.crypto, global.elliptic, global.promiseRetry, global.InputDataDecoder, global.Contract)); }(this, (function (exports, BigNumber, ethereumjsTx, lodash, paymentsCommon, tsCommon, request, t, Web3$1, ethereumjsUtil, bip32, crypto, elliptic, promiseRetry, InputDataDecoder, Contract) { 'use strict'; function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var BigNumber__default = /*#__PURE__*/_interopDefaultLegacy(BigNumber); var request__default = /*#__PURE__*/_interopDefaultLegacy(request); var Web3__default = /*#__PURE__*/_interopDefaultLegacy(Web3$1); var crypto__default = /*#__PURE__*/_interopDefaultLegacy(crypto); var promiseRetry__default = /*#__PURE__*/_interopDefaultLegacy(promiseRetry); var InputDataDecoder__default = /*#__PURE__*/_interopDefaultLegacy(InputDataDecoder); var Contract__default = /*#__PURE__*/_interopDefaultLegacy(Contract); (function (EthereumAddressFormat) { EthereumAddressFormat["Lowercase"] = "lowercase"; EthereumAddressFormat["Checksum"] = "checksum"; })(exports.EthereumAddressFormat || (exports.EthereumAddressFormat = {})); const EthereumAddressFormatT = tsCommon.enumCodec(exports.EthereumAddressFormat, 'EthereumAddressFormat'); const keys = t.type({ pub: t.string, prv: t.string, }); const xkeys = t.type({ xprv: t.string, xpub: t.string, }); const OptionalString = tsCommon.optional(t.string); const OptionalNumber = tsCommon.optional(t.number); const EthereumSignatory = t.type({ address: t.string, keys, xkeys, }, 'EthereumSignatory'); const EthereumPaymentsUtilsConfig = tsCommon.extendCodec(paymentsCommon.BaseConfig, {}, { fullNode: OptionalString, parityNode: OptionalString, blockbookNode: OptionalString, gasStation: OptionalString, symbol: OptionalString, name: OptionalString, decimals: t.number, providerOptions: t.any, web3: t.any, }, 'EthereumPaymentsUtilsConfig'); const BaseEthereumPaymentsConfig = tsCommon.extendCodec(EthereumPaymentsUtilsConfig, {}, { depositKeyIndex: OptionalNumber, }, 'BaseEthereumPaymentsConfig'); const HdEthereumPaymentsConfig = tsCommon.extendCodec(BaseEthereumPaymentsConfig, { hdKey: t.string, }, 'HdEthereumPaymentsConfig'); const KeyPairEthereumPaymentsConfig = tsCommon.extendCodec(BaseEthereumPaymentsConfig, { keyPairs: paymentsCommon.KeyPairsConfigParam, }, 'KeyPairEthereumPaymentsConfig'); const BaseErc20PaymentsConfig = tsCommon.extendCodec(BaseEthereumPaymentsConfig, { tokenAddress: t.string, }, { masterAddress: t.string, }, 'BaseErc20PaymentsConfig'); const HdErc20PaymentsConfig = tsCommon.extendCodec(BaseErc20PaymentsConfig, { hdKey: t.string, }, 'HdErc20PaymentsConfig'); const KeyPairErc20PaymentsConfig = tsCommon.extendCodec(BaseErc20PaymentsConfig, { keyPairs: paymentsCommon.KeyPairsConfigParam, }, 'KeyPairErc20PaymentsConfig'); const Erc20PaymentsConfig = t.union([HdErc20PaymentsConfig, KeyPairErc20PaymentsConfig], 'Erc20PaymentsConfig'); const EthereumPaymentsConfig = t.union([ HdEthereumPaymentsConfig, KeyPairEthereumPaymentsConfig, HdErc20PaymentsConfig, KeyPairErc20PaymentsConfig, ], 'EthereumPaymentsConfig'); const EthereumTransactionOptions = tsCommon.extendCodec(paymentsCommon.CreateTransactionOptions, {}, { data: t.string, gas: tsCommon.Numeric, proxyAddress: t.string, }, 'EthereumTransactionOptions'); const EthereumUnsignedTransaction = tsCommon.extendCodec(paymentsCommon.BaseUnsignedTransaction, { amount: t.string, fee: t.string, }, 'EthereumUnsignedTransaction'); const EthereumSignedTransaction = tsCommon.extendCodec(paymentsCommon.BaseSignedTransaction, { data: t.type({ hex: t.string }), }, {}, 'EthereumSignedTransaction'); const EthereumTransactionInfo = tsCommon.extendCodec(paymentsCommon.BaseTransactionInfo, {}, {}, 'EthereumTransactionInfo'); const EthereumBroadcastResult = tsCommon.extendCodec(paymentsCommon.BaseBroadcastResult, {}, 'EthereumBroadcastResult'); const EthereumResolvedFeeOption = tsCommon.extendCodec(paymentsCommon.ResolvedFeeOption, { gasPrice: t.string, }, 'EthereumResolvedFeeOption'); const EthereumFeeOption = tsCommon.extendCodec(paymentsCommon.FeeOption, {}, { isSweep: t.boolean, }, 'EthereumFeeOption'); const EthereumFeeOptionCustom = tsCommon.extendCodec(paymentsCommon.FeeOptionCustom, {}, { isSweep: t.boolean, }, 'EthereumFeeOption'); const BnRounding = t.union([ t.literal(1), t.literal(2), t.literal(3), t.literal(4), t.literal(5), t.literal(6), t.literal(7), t.literal(8), ]); const BaseDenominationOptions = tsCommon.extendCodec(t.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 = paymentsCommon.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 = { [paymentsCommon.FeeLevel.Low]: 'safeLow', [paymentsCommon.FeeLevel.Medium]: 'average', [paymentsCommon.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 = exports.EthereumAddressFormat.Lowercase; const web3 = new Web3__default['default'](); const ec = new elliptic.ec('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(bip32.fromBase58(xkey)); } throw new Error('Not extended key'); } getAddress(index) { const derived = this.deriveByIndex(index); let address = ethereumjsUtil.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(bip32.fromSeed(crypto__default['default'].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__default['default']((retry, attempt) => { return fn().catch(async (e) => { if (tsCommon.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 tsCommon.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.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.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 request.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.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(() => request.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.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.BigNumber.ROUND_DOWN).toFixed(); } async getWeb3GasPrice() { try { const wei = new BigNumber.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.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 tsCommon.DelegateLogger(config.logger, PACKAGE_NAME); this.networkType = config.network || paymentsCommon.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 (tsCommon.isNull(this.server)) { this.web3 = new Web3__default['default'](); } else if (this.server.startsWith('http')) { provider = new Web3__default['default'].providers.HttpProvider(this.server, config.providerOptions); this.web3 = new Web3__default['default'](provider); } else if (this.server.startsWith('ws')) { provider = new Web3__default['default'].providers.WebsocketProvider(this.server, config.providerOptions); this.web3 = new Web3__default['default'](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 = paymentsCommon.createUnitConverters(this.coinDecimals); this.toMainDenominationBigNumber = unitConverters.toMainDenominationBigNumber; this.toBaseDenominationBigNumber = unitConverters.toBaseDenominationBigNumber; this.toMainDenomination = unitConverters.toMainDenominationString; this.toBaseDenomination = unitConverters.toBaseDenominationString; const ethUnitConverters = paymentsCommon.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 === exports.EthereumAddressFormat.Lowercase) { return this.web3.utils.isAddress(address) && address === address.toLowerCase(); } else if (format === exports.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 = tsCommon.assertType(EthereumAddressFormatT, (_a = options === null || options === void 0 ? void 0 : options.format) !== null && _a !== void 0 ? _a : DEFAULT_ADDRESS_FORMAT, 'format'); if (format === exports.EthereumAddressFormat.Lowercase) { return address.toLowerCase(); } else { return this.web3.utils.toChecksumAddress(address); } } isValidExtraId(extraId) { return false; } isValidPayport(payport) { return paymentsCommon.Payport.is(payport) && !this._getPayportValidationMessage(payport); } validatePayport(payport) { const message = this._getPayportValidationMessage(payport); if (message) { throw new Error(message); } } getPayportValidationMessage(payport) { try { payport = tsCommon.assertType(paymentsCommon.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: paymentsCommon.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__default['default'](tx.gasPrice)).multipliedBy(tx.gas)), sequenceNumber: tx.nonce, weight: tx.gas, isExecuted: false, isConfirmed: false, confirmations: 0, confirmationId: null, confirmationTimestamp: null, currentBlockNumber: currentBlockNumber, status: paymentsCommon.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 = paymentsCommon.TransactionStatus.Pending; if (isConfirmed) { status = paymentsCommon.TransactionStatus.Confirmed; if (txInfo.hasOwnProperty('status') && (txInfo.status === false || txInfo.status.toString() === 'false')) { status = paymentsCommon.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__default['default'](tx.gasPrice)).multipliedBy(txInfo.gasUsed)), sequenceNumber: tx.nonce, weight: txInfo.gasUsed, isExecuted: status !== paymentsCommon.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.BigNumber(amountOfGas).dp() > 0) { throw new Error(`Amount of gas must be a whole number ${amountOfGas}`); } return tsCommon.isType(paymentsCommon.FeeOptionCustom, feeOption) ? this.resolveCustomFeeOption(feeOption, amountOfGas) : this.resolveLeveledFeeOption(feeOption.feeLevel, amountOfGas); } resolveCustomFeeOption(feeOption, amountOfGas) { const { feeRate, feeRateType } = feeOption; let gasPrice; if (feeRateType === paymentsCommon.FeeRateType.BasePerWeight) { gasPrice = new BigNumber.BigNumber(feeRate); } else { const feeRateBase = feeRateType === paymentsCommon.FeeRateType.Main ? this.toBaseDenominationBigNumberEth(feeRate) : new BigNumber.BigNumber(feeRate); gasPrice = feeRateBase.dividedBy(amountOfGas); } gasPrice = gasPrice.dp(0, BigNumber.BigNumber.ROUND_DOWN); const feeBase = gasPrice.multipliedBy(amountOfGas); const feeMain = this.toMainDenominationBigNumberEth(feeBase); return { targetFeeRate: feeOption.feeRate, targetFeeLevel: paymentsCommon.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.BigNumber(await this.gasStation.getGasPrice(feeLevel)); const feeBase = gasPrice.multipliedBy(amountOfGas).toFixed(); return { targetFeeRate: gasPrice.toFixed(), targetFeeLevel: feeLevel, targetFeeRateType: paymentsCommon.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 = lodash.cloneDeep(unsignedTx.data); const extraParam = this.networkType === paymentsCommon.NetworkType.Testnet ? { chain: 'ropsten' } : undefined; const tx = new ethereumjsTx.Transaction(unsignedRaw, extraParam); const key = Buffer.from(fromPrivateKey.slice(2), 'hex'); tx.sign(key); const result = { ...unsignedTx, id: `0x${tx.hash().toString('hex')}`, status: paymentsCommon.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 !== paymentsCommon.TransactionStatus.Signed) { throw new Error(`Tx ${tx.id} has not status ${paymentsCommon.TransactionStatus.Signed}`); } try { if (this.config.blockbookNode) { const url = `${this.config.blockbookNode}/api/sendtx/${tx.data.hex}`; request__default['default'] .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 (tsCommon.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 (tsCommon.isMatchingError(e, ['nonce too low'])) { throw new paymentsCommon.PaymentsError(paymentsCommon.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.BigNumber(options.gas).dp(0, BigNumber.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.BigNumber(feeBase); const maxFeePercent = new BigNumber.BigNumber((_a = options.maxFeePercent) !== null && _a !== void 0 ? _a : paymentsCommon.DEFAULT_MAX_FEE_PERCENT); const balanceWei = this.toBaseDenominationBigNumberEth(balanceEth); let amountWei = new BigNumber.BigNumber(0); if (balanceWei.eq(0)) { throw new paymentsCommon.PaymentsError(paymentsCommon.PaymentsErrorCode.TxInsufficientBalance, `${fromPayport.address} No balance available (${balanceEth})`); } if (sweepFlag) { amountWei = balanceWei.minus(feeWei); if (balanceWei.isLessThan(feeWei)) { throw new paymentsCommon.PaymentsError(paymentsCommon.PaymentsErrorCode.TxFeeTooHigh, `${fromPayport.address} Insufficient balance (${balanceEth}) to pay sweep fee of ${feeMain}`); } if (feeWei.gt(maxFeePercent.times(balanceWei))) { throw new paymentsCommon.PaymentsError(paymentsCommon.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 paymentsCommon.PaymentsError(paymentsCommon.PaymentsErrorCode.TxInsufficientBalance, `${fromPayport.address} Insufficient balance (${balanceEth}) to send ${amountEth} including fee of ${feeOption.feeMain}`); } if (feeWei.gt(maxFeePercent.times(amountWei))) { throw new paymentsCommon.PaymentsError(paymentsCommon.PaymentsErrorCode.TxFeeTooHigh, `${fromPayport.address} Sweep fee (${feeMain}) exceeds max fee percent (${maxFeePercent}%) of send amount (${amountEth})`); } } else { if (balanceWei.isLessThan(feeWei)) { throw new paymentsCommon.PaymentsError(paymentsCommon.PaymentsErrorCode.TxFeeTooHigh, `${fromPayport.address} Insufficient balance (${balanceEth}) to pay contract deploy fee of ${feeOption.feeMain}`); } } const result = { id: null, status: paymentsCommon.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.BigNumber(feeOption.gasPrice)).toString(16)}`, nonce: `0x${(new BigNumber.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 { ...lodash.omit(this.getFullConfig(), PUBLIC_CONFIG_OMIT_FIELDS), depositKeyIndex: this.depositKeyIndex, hdKey: this.getXpub(), }; } getAc