@celo/connect
Version:
Light Toolkit for connecting with the Celo network
439 lines • 20.8 kB
JavaScript
"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