UNPKG

@kaiachain/ethers-ext

Version:
438 lines (437 loc) 21.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.JsonRpcSigner = exports.Wallet = void 0; const abstract_signer_1 = require("@ethersproject/abstract-signer"); const address_1 = require("@ethersproject/address"); const bytes_1 = require("@ethersproject/bytes"); const hash_1 = require("@ethersproject/hash"); const keccak256_1 = require("@ethersproject/keccak256"); const logger_1 = require("@ethersproject/logger"); const providers_1 = require("@ethersproject/providers"); const strings_1 = require("@ethersproject/strings"); const wallet_1 = require("@ethersproject/wallet"); const lodash_1 = __importDefault(require("lodash")); const js_ext_core_1 = require("@kaiachain/js-ext-core"); const keystore_js_1 = require("./keystore.js"); const txutil_js_1 = require("./txutil.js"); const logger = new logger_1.Logger("@kaiachain/ethers-ext"); class Wallet extends wallet_1.Wallet { // Override Wallet factories accepting keystores to support both v3 and v4 (KIP-3) formats static async fromEncryptedJson(json, password, progress) { const { address, privateKey } = await (0, keystore_js_1.decryptKeystoreList)(json, password, progress); return new Wallet(address, privateKey); } static fromEncryptedJsonSync(json, password) { const { address, privateKey } = (0, keystore_js_1.decryptKeystoreListSync)(json, password); return new Wallet(address, privateKey); } // New Wallet[] factories accepting keystores supporting v4 (KIP-3) format static async fromEncryptedJsonList(json, password, progress) { const { address, privateKeyList } = await (0, keystore_js_1.decryptKeystoreList)(json, password, progress); return lodash_1.default.map(privateKeyList, (privateKey) => new Wallet(address, privateKey)); } static fromEncryptedJsonListSync(json, password) { const { address, privateKeyList } = (0, keystore_js_1.decryptKeystoreListSync)(json, password); return lodash_1.default.map(privateKeyList, (privateKey) => new Wallet(address, privateKey)); } // new KlaytnWallet(privateKey, provider?) or // new KlaytnWallet(address, privateKey, provider?) constructor(addressOrPrivateKey, privateKeyOrProvider, provider) { if (js_ext_core_1.HexStr.isHex(addressOrPrivateKey, 20)) { // First argument is an address. new KlaytnWallet(address, privateKey, provider?) const _address = js_ext_core_1.HexStr.from(addressOrPrivateKey); const _privateKey = privateKeyOrProvider; const _provider = provider; super(_privateKey, _provider); this.klaytnAddr = _address; } else { // First argument is a private key. new KlaytnWallet(privateKey, provider?) const _privateKey = addressOrPrivateKey; const _provider = privateKeyOrProvider; super(_privateKey, _provider); } } // If the Wallet is created as a decoupled account, and `legacy` is false, returns the decoupled address. // Otherwise, returns the address derived from the private key. getAddress(legacy) { if (legacy || !this.klaytnAddr) { return super.getAddress(); } else { return Promise.resolve(this.klaytnAddr); } } // @deprecated in favor of getAddress(true) getEtherAddress() { return super.getAddress(); } // @deprecated in favor of parseTransaction decodeTxFromRLP(rlp) { return (0, js_ext_core_1.parseTransaction)(rlp); } async isDecoupled() { if (!this.klaytnAddr) { return false; } else { return (await this.getAddress(false)) == (await this.getAddress(true)); } } // Fill 'from' if not set. Check 'from' against the private key or decoupled address. checkTransaction(transaction) { const tx = lodash_1.default.clone(transaction); const useLegacyFrom = !(0, js_ext_core_1.isKlaytnTxType)((0, js_ext_core_1.parseTxType)(tx.type)); const expectedFrom = this.getAddress(useLegacyFrom); (0, txutil_js_1.populateFromSync)(tx, expectedFrom); return tx; } async populateTransaction(transaction) { return this._populateTransaction(transaction, false); } // If asFeePayer is true, skip the 'from' address check. async _populateTransaction(transaction, asFeePayer) { const tx = await (0, txutil_js_1.getTransactionRequest)(transaction); // Not a Klaytn TxType; fallback to ethers.Signer.populateTransaction() if (!(0, js_ext_core_1.isKlaytnTxType)((0, js_ext_core_1.parseTxType)(tx.type))) { return super.populateTransaction(tx); } // If the current Wallet acts as feePayer, then tx.from is unrelated to this.getAddress(). // Skip the check, and does not fill up here. If tx.from was empty, then an error is generated // at signTransaction(), not here. if (!asFeePayer) { await (0, txutil_js_1.populateFrom)(tx, await this.getAddress()); } await (0, txutil_js_1.populateTo)(tx, this.provider); await (0, txutil_js_1.populateNonce)(tx, this.provider, await this.getAddress()); await (0, txutil_js_1.populateGasLimit)(tx, this.provider); await (0, txutil_js_1.populateGasPrice)(tx, this.provider); await (0, txutil_js_1.populateChainId)(tx, this.provider); return tx; } // Sign as a sender // tx.sigs += Sign(tx.sigRLP(), wallet.privateKey) // return tx.txHashRLP() or tx.senderTxHashRLP(); async signTransaction(transaction) { const tx = await (0, txutil_js_1.getTransactionRequest)(transaction); // Not a Klaytn TxType; fallback to ethers.Wallet.signTransaction() if (!(0, js_ext_core_1.isKlaytnTxType)((0, js_ext_core_1.parseTxType)(tx.type))) { return super.signTransaction(tx); } // Because RLP-encoded tx may not contain chainId, fill up here. await (0, txutil_js_1.populateChainId)(tx, this.provider); const chainId = tx.chainId; // chainId should have been determined in populateChainId. const klaytnTx = js_ext_core_1.KlaytnTxFactory.fromObject(tx); const sigHash = (0, keccak256_1.keccak256)(klaytnTx.sigRLP()); const sig = (0, txutil_js_1.eip155sign)(this._signingKey(), sigHash, chainId); klaytnTx.addSenderSig(sig); if ((0, js_ext_core_1.isFeePayerSigTxType)(klaytnTx.type)) { return klaytnTx.senderTxHashRLP(); } else { return klaytnTx.txHashRLP(); } } // Sign as a fee payer // tx.feepayerSigs += Sign(tx.sigFeePayerRLP(), wallet.privateKey) // return tx.txHashRLP(); async signTransactionAsFeePayer(transactionOrRLP) { const tx = await (0, txutil_js_1.getTransactionRequest)(transactionOrRLP); // Not a Klaytn FeePayerSig TxType; not supported if (!(0, js_ext_core_1.isFeePayerSigTxType)((0, js_ext_core_1.parseTxType)(tx.type))) { throw new Error(`signTransactionAsFeePayer not supported for tx type ${tx.type}`); } // Because RLP-encoded tx may contain dummy fee payer fields, fix here. await (0, txutil_js_1.populateFeePayerAndSignatures)(tx, await this.getAddress()); // Because RLP-encoded tx may not contain chainId, fill up here. await (0, txutil_js_1.populateChainId)(tx, this.provider); const chainId = tx.chainId; // chainId should have been determined in populateChainId. const klaytnTx = js_ext_core_1.KlaytnTxFactory.fromObject(tx); const sigFeePayerHash = (0, keccak256_1.keccak256)(klaytnTx.sigFeePayerRLP()); const sig = (0, txutil_js_1.eip155sign)(this._signingKey(), sigFeePayerHash, chainId); klaytnTx.addFeePayerSig(sig); return klaytnTx.txHashRLP(); } async sendTransaction(transaction) { const tx = await (0, txutil_js_1.getTransactionRequest)(transaction); if (!(0, js_ext_core_1.isKlaytnTxType)((0, js_ext_core_1.parseTxType)(tx.type))) { return super.sendTransaction(tx); } const populatedTx = await this._populateTransaction(tx, false); const signedTx = await this.signTransaction(populatedTx); return await this._sendKlaytnRawTransaction(signedTx); } async sendTransactionAsFeePayer(transactionOrRLP) { const tx = await (0, txutil_js_1.getTransactionRequest)(transactionOrRLP); // Not a Klaytn FeePayerSig TxType; not supported if (!(0, js_ext_core_1.isFeePayerSigTxType)((0, js_ext_core_1.parseTxType)(tx.type))) { throw new Error(`sendTransactionAsFeePayer not supported for tx type ${tx.type}`); } const populatedTx = await this._populateTransaction(tx, true); const signedTx = await this.signTransactionAsFeePayer(populatedTx); return await this._sendKlaytnRawTransaction(signedTx); } async _sendKlaytnRawTransaction(signedTx) { if (!(this.provider instanceof providers_1.JsonRpcProvider)) { throw new Error("Provider is not JsonRpcProvider: cannot send klay_sendRawTransaction"); } else { const txhash = await this.provider.send("klay_sendRawTransaction", [signedTx]); return await (0, txutil_js_1.pollTransactionInPool)(txhash, this.provider); } } } exports.Wallet = Wallet; // EthersJsonRpcSigner cannot be subclassed because of the constructorGuard. // Instead, we re-create the class by copying the implementation. class JsonRpcSigner extends abstract_signer_1.Signer { // Equivalent to EthersJsonRpcSigner.constructor, but without constructorGuard. // @ethersproject/providers/src.ts/json-rpc-provider.ts:JsonRpcSigner.constructor constructor(provider, addressOrIndex) { super(); this.provider = provider; if (addressOrIndex == null) { addressOrIndex = 0; } if (typeof (addressOrIndex) === "string") { this._address = (0, address_1.getAddress)(addressOrIndex); this._index = null; } else if (typeof (addressOrIndex) === "number") { this._address = null; this._index = addressOrIndex; } else { throw new Error(`invalid address or index '${addressOrIndex}'`); } } isKaikas() { if (this.provider instanceof providers_1.Web3Provider) { // The EIP-1193 provider, usually injected as window.ethereum or window.klaytn. const injectedProvider = this.provider.provider; return injectedProvider.isKaikas === true; } return false; } // @ethersproject/providers/src.ts/json-rpc-provider.ts:JsonRpcSigner.getAddress async getAddress() { if (this._address) { return Promise.resolve(this._address); } return this.provider.send("eth_accounts", []).then((accounts) => { if (accounts.length <= this._index) { logger.throwError("unknown account #" + this._index, logger_1.Logger.errors.UNSUPPORTED_OPERATION, { operation: "getAddress" }); } return this.provider.formatter.address(accounts[this._index]); }); } // @ethersproject/providers/src.ts/json-rpc-provider.ts:JsonRpcSigner.connect connect(_provider) { return logger.throwError("cannot alter JSON-RPC Signer connection", logger_1.Logger.errors.UNSUPPORTED_OPERATION, { operation: "connect" }); } // @ethersproject/providers/src.ts/json-rpc-provider.ts:JsonRpcSigner.connectUnchecked connectUnchecked() { return new UncheckedJsonRpcSigner(this.provider, this._address || this._index); } // If underlying EIP-1193 provider is Kaikas, return the KIP-97 signed message in which // the message is prefixed with "\x19Klaytn Signed Message:\n" + len(message) before signing. // https://kips.kaia.io/KIPs/kip-97 // // Otherwise, return the ERC-191 signed message in which the message is prefixed with // "\x19Ethereum Signed Message:\n" + len(message) before signing. // https://eips.ethereum.org/EIPS/eip-191 // // @ethersproject/providers/src.ts/json-rpc-provider.ts:JsonRpcSigner.signMessage async signMessage(message) { const data = ((typeof (message) === "string") ? (0, strings_1.toUtf8Bytes)(message) : message); const address = await this.getAddress(); try { if (this.isKaikas()) { // KIP-97 states that the prefixed message signature should be accessible // through the personal_sign method, but Kaikas provides it as eth_sign. return await this.provider.send("eth_sign", [address.toLowerCase(), (0, bytes_1.hexlify)(data)]); } else { // Otherwise, use the standard personal_sign for ERC-191. return await this.provider.send("personal_sign", [(0, bytes_1.hexlify)(data), address.toLowerCase()]); } } catch (error) { catchUserRejectedSigning(error, "signMessage", address, message); throw error; } } // Return the signature of the message without prefix via the eth_sign method. // This operation is deprecated in favor of signMessage (ERC-191 or KIP-97). // // Some providers may support this operation for backward compatibility, or reject it. // In Kaikas, eth_sign is reserved for KIP-97 purpose, so the legacy sign message is decisively not supported. // - If the provider is Kaikas, always throw an error. // - If the provider is not Kaikas, try the eth_sign method. // - If the provider rejects the operation, throw an error. // - If the provider accepts the operation, return the signature. // https://docs.metamask.io/wallet/concepts/signing-methods/#eth_sign // https://support.metamask.io/hc/en-us/articles/14764161421467-What-is-eth-sign-and-why-is-it-a-risk- async _legacySignMessage(message) { if (this.isKaikas()) { logger.throwError("Kaikas does not support the prefix-less legacy sign message", logger_1.Logger.errors.UNSUPPORTED_OPERATION, { operation: "_legacySignMessage" }); } // @ethersproject/providers/src.ts/json-rpc-provider.ts:JsonRpcSigner._legacySignMessage const data = ((typeof (message) === "string") ? (0, strings_1.toUtf8Bytes)(message) : message); const address = await this.getAddress(); try { return await this.provider.send("eth_sign", [address.toLowerCase(), (0, bytes_1.hexlify)(data)]); } catch (error) { catchUserRejectedSigning(error, "_legacySignMessage", address, message); throw error; } } // Return the signature of the structured data according to EIP-712 via the eth_signTypedData_v4 method. // https://eips.ethereum.org/EIPS/eip-712 // https://docs.metamask.io/wallet/how-to/sign-data/#use-eth_signtypeddata_v4 async _signTypedData(domain, types, value) { if (this.isKaikas()) { logger.throwError("Kaikas does not support the EIP-712 typed structured data signing", logger_1.Logger.errors.UNSUPPORTED_OPERATION, { operation: "_signTypedData" }); } // @ethersproject/providers/src.ts/json-rpc-provider.ts:JsonRpcSigner._signTypedData // Populate any ENS names (in-place) const populated = await hash_1._TypedDataEncoder.resolveNames(domain, types, value, (name) => { return this.provider.resolveName(name); }); const address = await this.getAddress(); try { return await this.provider.send("eth_signTypedData_v4", [ address.toLowerCase(), JSON.stringify(hash_1._TypedDataEncoder.getPayload(populated.domain, types, populated.value)) ]); } catch (error) { catchUserRejectedSigning(error, "_signTypedData", address, { domain: populated.domain, types, value: populated.value }); throw error; } } checkTransaction(transaction) { const tx = lodash_1.default.clone(transaction); const expectedFrom = this.getAddress(); (0, txutil_js_1.populateFromSync)(tx, expectedFrom); return tx; } async populateTransaction(transaction) { const tx = await (0, txutil_js_1.getTransactionRequest)(transaction); // Not a Klaytn TxType; fallback to ethers.Signer.populateTransaction() if (!(0, js_ext_core_1.isKlaytnTxType)((0, js_ext_core_1.parseTxType)(tx.type))) { return super.populateTransaction(tx); } await (0, txutil_js_1.populateFrom)(tx, await this.getAddress()); await (0, txutil_js_1.populateTo)(tx, this.provider); await (0, txutil_js_1.populateNonce)(tx, this.provider, await this.getAddress()); await (0, txutil_js_1.populateGasLimit)(tx, this.provider); await (0, txutil_js_1.populateGasPrice)(tx, this.provider); await (0, txutil_js_1.populateChainId)(tx, this.provider); return tx; } // Return the signed transaction as a string but do not send it. async signTransaction(transaction) { if (!this.isKaikas()) { return logger.throwError("signing transactions is only supported in Kaikas", logger_1.Logger.errors.UNSUPPORTED_OPERATION, { operation: "signTransaction" }); } const tx = await (0, txutil_js_1.getTransactionRequest)(transaction); await (0, txutil_js_1.populateFrom)(tx, await this.getAddress()); await (0, txutil_js_1.populateGasLimit)(tx, this.provider); await (0, txutil_js_1.populateTo)(tx, this.provider); const rpcTx = (0, js_ext_core_1.getRpcTxObject)(tx); if (this.isKaikas()) { rpcTx.type = (0, js_ext_core_1.getKaikasTxType)(rpcTx.type); } try { // Kaikas returns the web3-style signed transaction object. const signedTx = await this.provider.send("klay_signTransaction", [rpcTx]); return Promise.resolve(signedTx.rawTransaction); } catch (error) { catchUserRejectedTransaction(error, "signTransaction", transaction); throw error; } } async sendTransaction(transaction) { const txhash = await this.sendUncheckedTransaction(transaction); return (0, txutil_js_1.pollTransactionInPool)(txhash, this.provider); } async sendUncheckedTransaction(transaction) { const tx = await (0, txutil_js_1.getTransactionRequest)(transaction); await (0, txutil_js_1.populateFrom)(tx, await this.getAddress()); await (0, txutil_js_1.populateGasLimit)(tx, this.provider); await (0, txutil_js_1.populateTo)(tx, this.provider); const rpcTx = (0, js_ext_core_1.getRpcTxObject)(tx); if (this.isKaikas()) { rpcTx.type = (0, js_ext_core_1.getKaikasTxType)(rpcTx.type); } try { if (this.isKaikas()) { return await this.provider.send("klay_sendTransaction", [rpcTx]); } else { return await this.provider.send("eth_sendTransaction", [rpcTx]); } } catch (error) { catchUserRejectedTransaction(error, "sendTransaction", tx); throw error; } } async unlock(password) { const address = await this.getAddress(); return this.provider.send("personal_unlockAccount", [address.toLowerCase(), password, null]); } } exports.JsonRpcSigner = JsonRpcSigner; // Variant of JsonRpcSigner where it does not wait for the transaction to be in the txpool. // @ethersproject/providers/src.ts/json-rpc-provider.ts:UncheckedJsonRpcSigner class UncheckedJsonRpcSigner extends JsonRpcSigner { async sendTransaction(transaction) { const txhash = await this.sendUncheckedTransaction(transaction); return Promise.resolve({ hash: txhash, nonce: null, gasLimit: null, gasPrice: null, data: null, value: null, chainId: null, confirmations: 0, from: null, wait: (confirmations) => { return this.provider.waitForTransaction(txhash, confirmations); } }); // forcefully cast to TransactionResponse } } function catchUserRejectedSigning(error, action, from, messageData) { if (typeof (error.message) === "string" && error.message.match(/user denied/i)) { logger.throwError("user rejected signing", logger_1.Logger.errors.ACTION_REJECTED, { action, from, messageData, }); } } function catchUserRejectedTransaction(error, action, transaction) { if (typeof (error.message) === "string" && error.message.match(/user denied/i)) { logger.throwError("user rejected transaction", logger_1.Logger.errors.ACTION_REJECTED, { action, transaction, }); } }