UNPKG

@celo/connect

Version:

Light Toolkit for connecting with the Celo network

439 lines 20.8 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.isPresent = exports.Connection = void 0; const address_1 = require("@celo/utils/lib/address"); const sign_typed_data_utils_1 = require("@celo/utils/lib/sign-typed-data-utils"); const signatureUtils_1 = require("@celo/utils/lib/signatureUtils"); const util_1 = require("@ethereumjs/util"); const debug_1 = __importDefault(require("debug")); const web3_1 = __importDefault(require("web3")); const celo_provider_1 = require("./celo-provider"); const abi_utils_1 = require("./utils/abi-utils"); const formatter_1 = require("./utils/formatter"); const provider_utils_1 = require("./utils/provider-utils"); const rpc_caller_1 = require("./utils/rpc-caller"); const tx_params_normalizer_1 = require("./utils/tx-params-normalizer"); const tx_result_1 = require("./utils/tx-result"); const debugGasEstimation = (0, debug_1.default)('connection:gas-estimation'); /** * Connection is a Class for connecting to Celo, sending Transactions, etc * @param web3 an instance of web3 * @param wallet a child class of {@link WalletBase} * @param handleRevert sets handleRevert on the web3.eth instance passed in */ class Connection { constructor(web3, wallet, handleRevert = true) { var _a; this.web3 = web3; this.wallet = wallet; this.keccak256 = (value) => { return this.web3.utils.keccak256(value); }; this.hexToAscii = (hex) => { return this.web3.utils.hexToAscii(hex); }; /** * Send a transaction to celo-blockchain. * * Similar to `web3.eth.sendTransaction()` but with following differences: * - applies connections tx's defaults * - estimatesGas before sending * - returns a `TransactionResult` instead of `PromiEvent` */ this.sendTransaction = (tx) => __awaiter(this, void 0, void 0, function* () { tx = this.fillTxDefaults(tx); let gas = tx.gas; if (gas == null) { gas = yield this.estimateGasWithInflationFactor(tx); } return (0, tx_result_1.toTxResult)(this.web3.eth.sendTransaction(Object.assign(Object.assign({}, tx), { gas }))); }); this.sendTransactionObject = (txObj, tx) => __awaiter(this, void 0, void 0, function* () { tx = this.fillTxDefaults(tx); let gas = tx.gas; if (gas == null) { const gasEstimator = (_tx) => txObj.estimateGas(Object.assign({}, _tx)); const getCallTx = (_tx) => { // @ts-ignore missing _parent property from TransactionObject type. return Object.assign(Object.assign({}, _tx), { data: txObj.encodeABI(), to: txObj._parent._address }); }; const caller = (_tx) => this.web3.eth.call(getCallTx(_tx)); gas = yield this.estimateGasWithInflationFactor(tx, gasEstimator, caller); } return (0, tx_result_1.toTxResult)(txObj.send(Object.assign(Object.assign({}, tx), { gas }))); }); /* * @param signer - The address of account signing this data * @param typedData - Structured data to be signed * @param version - Optionally provide a version which will be appended to the method. E.G. (4) becomes 'eth_signTypedData_v4' * @remarks Some providers like Metamask treat eth_signTypedData differently from versioned method eth_signTypedData_v4 * @see [Metamask info in signing Typed Data](https://docs.metamask.io/guide/signing-data.html) */ this.signTypedData = (signer, typedData, version = 4) => __awaiter(this, void 0, void 0, function* () { // stringify data for v3 & v4 based on https://github.com/MetaMask/metamask-extension/blob/c72199a1a6e4151c40c22f79d0f3b6ed7a2d59a7/app/scripts/lib/typed-message-manager.js#L185 const shouldStringify = version === 3 || version === 4; // Uses the Provider and not the RpcCaller, because this method should be intercepted // by the CeloProvider if there is a local wallet that could sign it. The RpcCaller // would just forward it to the node const signature = yield new Promise((resolve, reject) => { const method = version ? `eth_signTypedData_v${version}` : 'eth_signTypedData'; this.web3.currentProvider.send({ id: (0, rpc_caller_1.getRandomId)(), jsonrpc: '2.0', method, params: [ (0, formatter_1.inputAddressFormatter)(signer), shouldStringify ? JSON.stringify(typedData) : typedData, ], }, (error, resp) => { if (error) { reject(error); } else if (resp) { resolve(resp.result); } else { reject(new Error('empty-response')); } }); }); const messageHash = (0, util_1.bufferToHex)((0, sign_typed_data_utils_1.generateTypedDataHash)(typedData)); return (0, signatureUtils_1.parseSignatureWithoutPrefix)(messageHash, signature, signer); }); this.sign = (dataToSign, address) => __awaiter(this, void 0, void 0, function* () { // Uses the Provider and not the RpcCaller, because this method should be intercepted // by the CeloProvider if there is a local wallet that could sign it. The RpcCaller // would just forward it to the node const signature = yield new Promise((resolve, reject) => { ; this.web3.currentProvider.send({ id: (0, rpc_caller_1.getRandomId)(), jsonrpc: '2.0', method: 'eth_sign', params: [(0, formatter_1.inputAddressFormatter)(address.toString()), (0, formatter_1.inputSignFormatter)(dataToSign)], }, (error, resp) => { if (error) { reject(error); } else if (resp) { resolve(resp.result); } else { reject(new Error('empty-response')); } }); }); return signature; }); this.sendSignedTransaction = (signedTransactionData) => __awaiter(this, void 0, void 0, function* () { return (0, tx_result_1.toTxResult)(this.web3.eth.sendSignedTransaction(signedTransactionData)); }); // if neither gas price nor feeMarket fields are present set them. this.setFeeMarketGas = (tx) => __awaiter(this, void 0, void 0, function* () { if (isEmpty(tx.maxPriorityFeePerGas)) { tx.maxPriorityFeePerGas = yield this.getMaxPriorityFeePerGas(tx.feeCurrency); } if (isEmpty(tx.maxFeePerGas)) { const baseFee = isEmpty(tx.feeCurrency) ? yield this.getBlock('latest').then((block) => block.baseFeePerGas) : yield this.gasPrice(tx.feeCurrency); const withBuffer = addBufferToBaseFee(BigInt(baseFee)); const maxFeePerGas = withBuffer + BigInt((0, address_1.ensureLeading0x)(tx.maxPriorityFeePerGas.toString(16))); tx.maxFeePerGas = (0, address_1.ensureLeading0x)(maxFeePerGas.toString(16)); } return Object.assign(Object.assign({}, tx), { gasPrice: undefined }); }); this.estimateGas = (tx, gasEstimator = this.web3.eth.estimateGas, caller = this.web3.eth.call) => __awaiter(this, void 0, void 0, function* () { try { const gas = yield gasEstimator(Object.assign({}, tx)); debugGasEstimation('estimatedGas: %s', gas.toString()); return gas; } catch (e) { const called = yield caller({ data: tx.data, to: tx.to, from: tx.from }); let revertReason = 'Could not decode transaction failure reason'; if (called.startsWith('0x08c379a')) { revertReason = (0, abi_utils_1.decodeStringParameter)(this.getAbiCoder(), called.substring(10)); } debugGasEstimation('Recover transaction failure reason', { called, data: tx.data, to: tx.to, from: tx.from, error: e, revertReason, }); return Promise.reject(`Gas estimation failed: ${revertReason} or ${e}`); } }); this.estimateGasWithInflationFactor = (tx, gasEstimator, caller) => __awaiter(this, void 0, void 0, function* () { try { const gas = Math.round((yield this.estimateGas(tx, gasEstimator, caller)) * this.config.gasInflationFactor); debugGasEstimation('estimatedGasWithInflationFactor: %s', gas); return gas; } catch (e) { throw new Error(e); } }); // An instance of Connection will only change chain id if provider is changed. this.chainId = () => __awaiter(this, void 0, void 0, function* () { if (this._chainID) { return this._chainID; } // Reference: https://eth.wiki/json-rpc/API#net_version const response = yield this.rpcCaller.call('net_version', []); const chainID = parseInt(response.result.toString(), 10); this._chainID = chainID; return chainID; }); this.getTransactionCount = (address) => __awaiter(this, void 0, void 0, function* () { // Reference: https://eth.wiki/json-rpc/API#eth_gettransactioncount const response = yield this.rpcCaller.call('eth_getTransactionCount', [address, 'pending']); return (0, formatter_1.hexToNumber)(response.result); }); this.nonce = (address) => __awaiter(this, void 0, void 0, function* () { return this.getTransactionCount(address); }); this.coinbase = () => __awaiter(this, void 0, void 0, function* () { // Reference: https://eth.wiki/json-rpc/API#eth_coinbase const response = yield this.rpcCaller.call('eth_coinbase', []); return response.result.toString(); }); this.gasPrice = (feeCurrency) => __awaiter(this, void 0, void 0, function* () { // Required otherwise is not backward compatible const parameter = feeCurrency ? [feeCurrency] : []; // Reference: https://eth.wiki/json-rpc/API#eth_gasprice const response = yield this.rpcCaller.call('eth_gasPrice', parameter); const gasPriceInHex = response.result.toString(); return gasPriceInHex; }); this.getMaxPriorityFeePerGas = (feeCurrency) => __awaiter(this, void 0, void 0, function* () { const parameter = feeCurrency ? [feeCurrency] : []; return this.rpcCaller.call('eth_maxPriorityFeePerGas', parameter).then((rpcResponse) => { return rpcResponse.result; }); }); this.getBlockNumber = () => __awaiter(this, void 0, void 0, function* () { const response = yield this.rpcCaller.call('eth_blockNumber', []); return (0, formatter_1.hexToNumber)(response.result); }); this.isBlockNumberHash = (blockNumber) => blockNumber instanceof String && blockNumber.indexOf('0x') === 0; this.getBlock = (blockHashOrBlockNumber, fullTxObjects = true) => __awaiter(this, void 0, void 0, function* () { const endpoint = this.isBlockNumberHash(blockHashOrBlockNumber) ? 'eth_getBlockByHash' // Reference: https://eth.wiki/json-rpc/API#eth_getBlockByHash : 'eth_getBlockByNumber'; // Reference: https://eth.wiki/json-rpc/API#eth_getBlockByNumber const response = yield this.rpcCaller.call(endpoint, [ (0, formatter_1.inputBlockNumberFormatter)(blockHashOrBlockNumber), fullTxObjects, ]); return (0, formatter_1.outputBlockFormatter)(response.result); }); this.getBlockHeader = (blockHashOrBlockNumber) => __awaiter(this, void 0, void 0, function* () { const endpoint = this.isBlockNumberHash(blockHashOrBlockNumber) ? 'eth_getHeaderByHash' : 'eth_getHeaderByNumber'; const response = yield this.rpcCaller.call(endpoint, [ (0, formatter_1.inputBlockNumberFormatter)(blockHashOrBlockNumber), ]); return (0, formatter_1.outputBlockHeaderFormatter)(response.result); }); this.getBalance = (address, defaultBlock) => __awaiter(this, void 0, void 0, function* () { // Reference: https://eth.wiki/json-rpc/API#eth_getBalance const response = yield this.rpcCaller.call('eth_getBalance', [ (0, formatter_1.inputAddressFormatter)(address), (0, formatter_1.inputDefaultBlockNumberFormatter)(defaultBlock), ]); return (0, formatter_1.outputBigNumberFormatter)(response.result); }); this.getTransaction = (transactionHash) => __awaiter(this, void 0, void 0, function* () { // Reference: https://eth.wiki/json-rpc/API#eth_getTransactionByHash const response = yield this.rpcCaller.call('eth_getTransactionByHash', [ (0, address_1.ensureLeading0x)(transactionHash), ]); return (0, formatter_1.outputCeloTxFormatter)(response.result); }); this.getTransactionReceipt = (txhash) => __awaiter(this, void 0, void 0, function* () { // Reference: https://eth.wiki/json-rpc/API#eth_getTransactionReceipt const response = yield this.rpcCaller.call('eth_getTransactionReceipt', [ (0, address_1.ensureLeading0x)(txhash), ]); if (response.result === null) { return null; } return (0, formatter_1.outputCeloTxReceiptFormatter)(response.result); }); web3.eth.handleRevert = handleRevert; this.config = { gasInflationFactor: 1.3, }; const existingProvider = web3.currentProvider; this.setProvider(existingProvider); // TODO: Add this line with the wallets separation completed // this.wallet = _wallet ?? new LocalWallet() this.config.from = (_a = web3.eth.defaultAccount) !== null && _a !== void 0 ? _a : undefined; this.paramsPopulator = new tx_params_normalizer_1.TxParamsNormalizer(this); } setProvider(provider) { if (!provider) { throw new Error('Must have a valid Provider'); } this._chainID = undefined; try { if (!(provider instanceof celo_provider_1.CeloProvider)) { this.rpcCaller = new rpc_caller_1.HttpRpcCaller(provider); provider = new celo_provider_1.CeloProvider(provider, this); } this.web3.setProvider(provider); return true; } catch (error) { console.error(`could not attach provider`, error); return false; } } /** * Set default account for generated transactions (eg. tx.from ) */ set defaultAccount(address) { this.config.from = address; this.web3.eth.defaultAccount = address ? address : null; } /** * Default account for generated transactions (eg. tx.from) */ get defaultAccount() { return this.config.from; } set defaultGasInflationFactor(factor) { this.config.gasInflationFactor = factor; } get defaultGasInflationFactor() { return this.config.gasInflationFactor; } /** * Set the ERC20 address for the token to use to pay for transaction fees. * The ERC20 address SHOULD be whitelisted for gas, but this is not checked or enforced. * * Set to `null` to use CELO * * @param address ERC20 address */ set defaultFeeCurrency(address) { this.config.feeCurrency = address; } get defaultFeeCurrency() { return this.config.feeCurrency; } isLocalAccount(address) { return this.wallet != null && this.wallet.hasAccount(address); } addAccount(privateKey) { if (this.wallet) { if ((0, provider_utils_1.hasProperty)(this.wallet, 'addAccount')) { this.wallet.addAccount(privateKey); } else { throw new Error("The wallet used, can't add accounts"); } } else { throw new Error('No wallet set'); } } removeAccount(address) { if (this.wallet) { if ((0, provider_utils_1.hasProperty)(this.wallet, 'removeAccount')) { this.wallet.removeAccount(address); } else { throw new Error("The wallet used, can't remove accounts"); } } else { throw new Error('No wallet set'); } } getNodeAccounts() { var _a; return __awaiter(this, void 0, void 0, function* () { const nodeAccountsResp = yield this.rpcCaller.call('eth_accounts', []); return this.toChecksumAddresses((_a = nodeAccountsResp.result) !== null && _a !== void 0 ? _a : []); }); } getLocalAccounts() { return this.wallet ? this.toChecksumAddresses(this.wallet.getAccounts()) : []; } getAccounts() { return __awaiter(this, void 0, void 0, function* () { return (yield this.getNodeAccounts()).concat(this.getLocalAccounts()); }); } toChecksumAddresses(addresses) { return addresses.map((value) => (0, address_1.toChecksumAddress)(value)); } isListening() { return this.web3.eth.net.isListening(); } isSyncing() { return new Promise((resolve, reject) => { this.web3.eth .isSyncing() .then((response) => { // isSyncing returns a syncProgress object when it's still syncing if (typeof response === 'boolean') { resolve(response); } else { resolve(true); } }) .catch(reject); }); } getAbiCoder() { return this.web3.eth.abi; } fillTxDefaults(tx) { const defaultTx = { from: this.config.from, feeCurrency: this.config.feeCurrency, }; return Object.assign(Object.assign({}, defaultTx), tx); } stop() { (0, celo_provider_1.assertIsCeloProvider)(this.web3.currentProvider); this.web3.currentProvider.stop(); } } exports.Connection = Connection; const addBufferToBaseFee = (gasPrice) => (gasPrice * BigInt(120)) / BigInt(100); function isEmpty(value) { return (value === 0 || value === undefined || value === null || value === '0' || value === BigInt(0) || (typeof value === 'string' && (value.toLowerCase() === '0x' || value.toLowerCase() === '0x0')) || web3_1.default.utils.toBN(value.toString(10)).eq(web3_1.default.utils.toBN(0))); } function isPresent(value) { return !isEmpty(value); } exports.isPresent = isPresent; //# sourceMappingURL=connection.js.map