@kaiachain/ethers-ext
Version:
ethers.js extension for kaia blockchain
438 lines (437 loc) • 21.5 kB
JavaScript
"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,
});
}
}