chaingate
Version:
Multi-chain cryptocurrency SDK for TypeScript — unified API for Bitcoin, Ethereum, Litecoin, Dogecoin, Bitcoin Cash, Polygon, Arbitrum, and any EVM-compatible chain. Create wallets, query balances, send transactions, and manage tokens and NFTs across UTXO
151 lines (150 loc) • 5.84 kB
JavaScript
"use strict";
/**
* EIP-1559 (type-2) transaction serialization and signing.
*
* Produces the signed raw transaction hex ready for broadcast.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.signEip1559Transaction = signEip1559Transaction;
exports.signLegacyTransaction = signLegacyTransaction;
const secp256k1_js_1 = require("@noble/curves/secp256k1.js");
const sha3_js_1 = require("@noble/hashes/sha3.js");
const rlp_1 = require("./rlp");
const encoding_1 = require("./encoding");
/** Converts a bigint to minimal big-endian bytes (no leading zeros). */
function bigintToBytes(n) {
if (n === 0n)
return new Uint8Array(0);
let hex = n.toString(16);
if (hex.length % 2 !== 0)
hex = '0' + hex;
return (0, encoding_1.hexToBytes)(hex);
}
/**
* Builds the RLP-encoded EIP-1559 transaction payload (unsigned) for signing.
*
* Format: 0x02 || RLP([chainId, nonce, maxPriorityFeePerGas, maxFeePerGas,
* gasLimit, to, value, data, accessList])
*/
function encodeUnsigned(tx) {
const fields = [
bigintToBytes(tx.chainId),
bigintToBytes(tx.nonce),
bigintToBytes(tx.maxPriorityFeePerGas),
bigintToBytes(tx.maxFeePerGas),
bigintToBytes(tx.gasLimit),
(0, encoding_1.hexToBytes)(tx.to), // 20 bytes
bigintToBytes(tx.value),
tx.data === '0x' || tx.data === '' ? new Uint8Array(0) : (0, encoding_1.hexToBytes)(tx.data),
[], // accessList — empty for simple transfers
];
const rlp = (0, rlp_1.rlpEncode)(fields);
// Prepend the EIP-2718 type byte (0x02).
const envelope = new Uint8Array(1 + rlp.length);
envelope[0] = 0x02;
envelope.set(rlp, 1);
return envelope;
}
/**
* Signs an EIP-1559 transaction and returns the raw signed transaction hex
* ready for broadcast (with `0x` prefix).
*
* @param tx - Transaction parameters.
* @param privateKey - 32-byte private key.
* @returns Signed raw transaction as a hex string with `0x` prefix.
*/
function signEip1559Transaction(tx, privateKey) {
const unsigned = encodeUnsigned(tx);
const hash = (0, sha3_js_1.keccak_256)(unsigned);
// Sign with secp256k1. Use 'recovered' format to get [recovery, r, s].
// prehash: false because we already hashed the payload ourselves.
const sigBytes = secp256k1_js_1.secp256k1.sign(hash, privateKey, {
prehash: false,
lowS: true,
format: 'recovered',
});
// 'recovered' format: 1 byte recovery || 32 bytes r || 32 bytes s = 65 bytes
const recovery = sigBytes[0]; // 0 or 1
const r = sigBytes.subarray(1, 33);
const s = sigBytes.subarray(33, 65);
// Strip leading zeros from r and s for RLP encoding.
const rTrimmed = trimLeadingZeros(r);
const sTrimmed = trimLeadingZeros(s);
// Signed payload: 0x02 || RLP([chainId, nonce, maxPriorityFeePerGas, maxFeePerGas,
// gasLimit, to, value, data, accessList, v, r, s])
const fields = [
bigintToBytes(tx.chainId),
bigintToBytes(tx.nonce),
bigintToBytes(tx.maxPriorityFeePerGas),
bigintToBytes(tx.maxFeePerGas),
bigintToBytes(tx.gasLimit),
(0, encoding_1.hexToBytes)(tx.to),
bigintToBytes(tx.value),
tx.data === '0x' || tx.data === '' ? new Uint8Array(0) : (0, encoding_1.hexToBytes)(tx.data),
[], // accessList
bigintToBytes(BigInt(recovery)),
rTrimmed,
sTrimmed,
];
const rlp = (0, rlp_1.rlpEncode)(fields);
const signed = new Uint8Array(1 + rlp.length);
signed[0] = 0x02;
signed.set(rlp, 1);
return '0x' + (0, encoding_1.bytesToHex)(signed);
}
/**
* Signs a legacy (type-0) transaction and returns the raw signed transaction hex
* ready for broadcast (with `0x` prefix).
*
* Uses EIP-155 replay protection.
*
* @param tx - Transaction parameters.
* @param privateKey - 32-byte private key.
* @returns Signed raw transaction as a hex string with `0x` prefix.
*/
function signLegacyTransaction(tx, privateKey) {
// EIP-155 unsigned payload: RLP([nonce, gasPrice, gasLimit, to, value, data, chainId, 0, 0])
const unsignedFields = [
bigintToBytes(tx.nonce),
bigintToBytes(tx.gasPrice),
bigintToBytes(tx.gasLimit),
(0, encoding_1.hexToBytes)(tx.to),
bigintToBytes(tx.value),
tx.data === '0x' || tx.data === '' ? new Uint8Array(0) : (0, encoding_1.hexToBytes)(tx.data),
bigintToBytes(tx.chainId),
new Uint8Array(0), // 0 for EIP-155
new Uint8Array(0), // 0 for EIP-155
];
const rlpUnsigned = (0, rlp_1.rlpEncode)(unsignedFields);
const hash = (0, sha3_js_1.keccak_256)(rlpUnsigned);
const sigBytes = secp256k1_js_1.secp256k1.sign(hash, privateKey, {
prehash: false,
lowS: true,
format: 'recovered',
});
const recovery = sigBytes[0]; // 0 or 1
const r = sigBytes.subarray(1, 33);
const s = sigBytes.subarray(33, 65);
// EIP-155: v = chainId * 2 + 35 + recovery
const v = tx.chainId * 2n + 35n + BigInt(recovery);
const signedFields = [
bigintToBytes(tx.nonce),
bigintToBytes(tx.gasPrice),
bigintToBytes(tx.gasLimit),
(0, encoding_1.hexToBytes)(tx.to),
bigintToBytes(tx.value),
tx.data === '0x' || tx.data === '' ? new Uint8Array(0) : (0, encoding_1.hexToBytes)(tx.data),
bigintToBytes(v),
trimLeadingZeros(r),
trimLeadingZeros(s),
];
const rlpSigned = (0, rlp_1.rlpEncode)(signedFields);
return '0x' + (0, encoding_1.bytesToHex)(rlpSigned);
}
/** Removes leading zero bytes from a byte array. */
function trimLeadingZeros(bytes) {
let i = 0;
while (i < bytes.length - 1 && bytes[i] === 0)
i++;
return i === 0 ? bytes : bytes.subarray(i);
}