UNPKG

@kaiachain/web3js-ext

Version:
217 lines (189 loc) 8.27 kB
import { RLP } from "@ethereumjs/rlp"; import { KlaytnTxFactory, TxType, isFeePayerSigTxType, KlaytnTx as CoreKlaytnTx } from "@kaiachain/js-ext-core"; import { keccak256 } from "ethereum-cryptography/keccak.js"; import * as ethereumCryptography from "ethereum-cryptography/secp256k1.js"; import { Transaction as LegacyTransaction, TxOptions, ECDSASignature } from "web3-eth-accounts"; import { bytesToHex, hexToBytes, toHex, toNumber, numberToHex, toBigInt } from "web3-utils"; import { KlaytnTxData } from "../types.js"; export const secp256k1 = ethereumCryptography.secp256k1 ?? ethereumCryptography; // Mimics the LegacyTransaction. // See web3-eth-accounts/src/tx/legacyTransaction.ts // and web3-eth-accounts/src/tx/baseTransaction.ts export class KlaytnTypedTransaction extends LegacyTransaction { // Override 'type' field (actually a getter) because LegacyTransaction.type is always 0. private readonly _klaytnType: number; public override get type(): number { return this._klaytnType; } // Additional fields not in LegacyTransaction public readonly from?: string; public readonly chainId?: bigint; public readonly key?: any; public readonly feePayer?: string; public readonly feePayer_v?: bigint; public readonly feePayer_r?: bigint; public readonly feePayer_s?: bigint; public readonly txSignatures?: any; public readonly feePayerSignatures?: any; public readonly feeRatio?: number; // The CoreKlaytnTx object to be used to calculate RLP encoding. private readonly coreKlaytnTx: CoreKlaytnTx; // This constructor creates a frozen read-only transaction object out of TxData. // Any modifications to the fields (e.g. adding the signature) should involve // the construction of a new object, rather than modifying the fields directly. public constructor(txData: KlaytnTxData, txOptions: TxOptions = {}) { // Allow adding new fields inside constructor. const savedFreeze = txOptions.freeze; txOptions.freeze = false; // Construct LegacyTransaction and parse TxData fields super(txData, txOptions); // Parse KlaytnTxData fields if (!txData.type) { // Should not reach here because KlaytnTx is selected via explicit type field. throw new Error("Missing 'type' field"); } this._klaytnType = toNumber(txData.type) as number; this.from = txData.from; this.chainId = txData.chainId; this.key = txData.key; this.feePayer = txData.feePayer; this.feePayer_v = txData.feePayer_v; this.feePayer_r = txData.feePayer_r; this.feePayer_s = txData.feePayer_s; this.txSignatures = txData.txSignatures; this.feePayerSignatures = txData.feePayerSignatures; this.feeRatio = txData.feeRatio; // Build the inner CoreKlaytnTx object. // Convert to type understood by CoreKlaytnTx. const klaytnTxObject = { type: toHex(this.type || 0), nonce: toHex(this.nonce), gasPrice: toHex(this.gasPrice), gasLimit: toHex(this.gasLimit), to: this.to ? bytesToHex(this.to.toString()) : undefined, value: toHex(this.value), from: this.from, data: bytesToHex(this.data), input: bytesToHex(this.data), chainId: this.chainId ? toHex(this.chainId) : undefined, humanReadable: false, codeFormat: 0x00, key: this.key, feePayer: this.feePayer, txSignatures: this.txSignatures, feePayerSignatures: this.feePayerSignatures, feeRatio: this.feeRatio, }; if (txData.type == TxType.SmartContractDeploy || txData.type == TxType.FeeDelegatedSmartContractDeploy || txData.type == TxType.FeeDelegatedSmartContractDeployWithRatio) { klaytnTxObject.to = "0x0000000000000000000000000000000000000000"; } this.coreKlaytnTx = KlaytnTxFactory.fromObject(klaytnTxObject); if (this.v && this.r && this.s) { this.coreKlaytnTx.addSenderSig({ v: Number(this.v), r: numberToHex(this.r), s: numberToHex(this.s), }); } if (this.feePayer_v && this.feePayer_r && this.feePayer_s) { this.coreKlaytnTx.addFeePayerSig({ v: Number(this.feePayer_v), r: numberToHex(this.feePayer_r), s: numberToHex(this.feePayer_s), }); } // Resume the freeze behavior at the end of LegacyTransaction.constructor(). this.txOptions.freeze = savedFreeze; if (this.txOptions.freeze ?? true) { Object.freeze(this); } } // Return sender signing message. i.e. SigRLP public getMessageToSign(hashMessage: false): Uint8Array[]; public getMessageToSign(hashMessage?: true): Uint8Array; public getMessageToSign(hashMessage = true) { const rlp = hexToBytes(this.coreKlaytnTx.sigRLP()); if (hashMessage) { return keccak256(rlp); // Hashed Uint8Array } else { return RLP.decode(rlp); // RLP-decoded Uint8Array[] } } // Return feePayer signing message. i.e. sigFeePayerRLP public getMessageToSignAsFeePayer(hashMessage: false): Uint8Array[]; public getMessageToSignAsFeePayer(hashMessage?: true): Uint8Array; public getMessageToSignAsFeePayer(hashMessage = true) { const rlp = hexToBytes(this.coreKlaytnTx.sigFeePayerRLP()); if (hashMessage) { return keccak256(rlp); // Hashed Uint8Array } else { return RLP.decode(rlp); // RLP-decoded Uint8Array[] } } // Return a new KlaytnTx object with the (v,r,s) signature added. public sign(privateKey: Uint8Array): KlaytnTypedTransaction { if (privateKey.length !== 32) { const msg = this._errorMsg("Private key must be 32 bytes in length."); throw new Error(msg); } if (!this.chainId) { // shouldn't reach here because chainId is required in every Klaytn TxType. // The chainId should have been supplied by user or filled at prepareTransaction(). throw new Error("Missing 'chainId' field"); } const msgHash = this.getMessageToSign(true); const { r, s, v } = this._eip155sign(msgHash, privateKey, this.chainId); return new KlaytnTypedTransaction({ ...this, type: this.type, // The '...this' expression does not include this.type because 'type()' a getter. v: v, r: toBigInt(bytesToHex(r)), s: toBigInt(bytesToHex(s)), }, this.txOptions); } // Analogous to sign(), but uses *AsFeePayer methods. // See web3-eth-accounts/src/tx/baseTransaction.ts:sign() public signAsFeePayer(privateKey: Uint8Array): KlaytnTypedTransaction { if (privateKey.length !== 32) { const msg = this._errorMsg("Private key must be 32 bytes in length."); throw new Error(msg); } if (!this.chainId) { // shouldn't reach here because chainId is required in every Klaytn TxType. // The chainId should have been supplied by user or filled at prepareTransaction(). throw new Error("Missing 'chainId' field"); } const msgHash = this.getMessageToSignAsFeePayer(true); const { v, r, s } = this._eip155sign(msgHash, privateKey, this.chainId); return new KlaytnTypedTransaction({ ...this, type: this.type, // The '...this' expression does not include this.type because 'type()' is a getter. feePayer_v: v, feePayer_r: toBigInt(bytesToHex(r)), feePayer_s: toBigInt(bytesToHex(s)), }, this.txOptions); } // Return the sender-signed raw transaction. i.e. SenderTxHashRLP or TxHashRLP public serialize(): Uint8Array { if (isFeePayerSigTxType(this.type)) { return hexToBytes(this.coreKlaytnTx.senderTxHashRLP()); } return hexToBytes(this.coreKlaytnTx.txHashRLP()); } // Return the feePayer-signed raw transaction. i.e. TxHashRLP public serializeAsFeePayer(): Uint8Array { return hexToBytes(this.coreKlaytnTx.txHashRLP()); } // Recreating BaseTransaction._ecsign because that's private. private _eip155sign(msgHash: Uint8Array, privateKey: Uint8Array, chainId: bigint): ECDSASignature { const signature = secp256k1.sign(msgHash, privateKey); const signatureBytes = signature.toCompactRawBytes(); const r = signatureBytes.subarray(0, 32); const s = signatureBytes.subarray(32, 64); const recoveryParam = signature.recovery; const v = BigInt(recoveryParam + 35) + chainId * BigInt(2); return { v, r, s }; } }