@kaiachain/web3js-ext
Version:
web3.js extension for kaiachain blockchain
217 lines (189 loc) • 8.27 kB
text/typescript
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 };
}
}