Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions src/__tests__/trading.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
signLegacyTransaction,
signSolanaTransaction,
signEvmTransaction,
signEip1559EvmTransaction,
buildApprovalTransaction,
stripLeadingZeros,
buildTradingCommands,
Expand Down Expand Up @@ -482,6 +483,91 @@ describe('signEvmTransaction (API response format)', () => {
});
});

// ============= EIP-1559 (Type 2) Transaction Signing =============

describe('signEip1559EvmTransaction', () => {
it('should produce valid signed tx hex with 0x02 type prefix', () => {
const wallet = generateEvmWallet();
const txData = {
nonce: 0,
maxFeePerGas: '30000000000',
maxPriorityFeePerGas: '1000000000',
gas: '21000',
to: '0x' + 'ab'.repeat(20),
value: '0',
data: '0x',
chainId: 8453,
};
const signedHex = signEip1559EvmTransaction(txData, wallet.privateKey);
expect(signedHex).toMatch(/^0x02[0-9a-f]+$/);
});

it('should produce deterministic signatures (RFC 6979)', () => {
const wallet = generateEvmWallet();
const txData = {
nonce: 0,
maxFeePerGas: '30000000000',
maxPriorityFeePerGas: '1000000000',
gas: '21000',
to: '0x' + 'ab'.repeat(20),
value: '0',
data: '0x',
chainId: 1,
};
const sig1 = signEip1559EvmTransaction(txData, wallet.privateKey);
const sig2 = signEip1559EvmTransaction(txData, wallet.privateKey);
expect(sig1).toBe(sig2);
});

it('should produce different signed tx for different nonces', () => {
const wallet = generateEvmWallet();
const base = {
maxFeePerGas: '30000000000',
maxPriorityFeePerGas: '1000000000',
gas: '21000',
to: '0x' + 'ab'.repeat(20),
value: '0',
data: '0x',
chainId: 8453,
};
const sig0 = signEip1559EvmTransaction({ ...base, nonce: 0 }, wallet.privateKey);
const sig1 = signEip1559EvmTransaction({ ...base, nonce: 1 }, wallet.privateKey);
expect(sig0).not.toBe(sig1);
});
});

describe('signEvmTransaction (EIP-1559 dispatch)', () => {
it('should sign as type 2 when maxFeePerGas is present', () => {
const wallet = generateEvmWallet();
const txData = {
to: '0x' + 'ab'.repeat(20),
data: '0x',
value: '0',
gas: '21000',
maxFeePerGas: '30000000000',
maxPriorityFeePerGas: '1000000000',
};
const signedHex = signEvmTransaction(txData, wallet.privateKey, 'base', 0);
// Type 2 transactions start with 0x02
expect(signedHex).toMatch(/^0x02/);
});

it('should sign as legacy type 0 when only gasPrice is present', () => {
const wallet = generateEvmWallet();
const txData = {
to: '0x' + 'ab'.repeat(20),
data: '0x',
value: '0',
gas: '21000',
gasPrice: '1000000000',
};
const signedHex = signEvmTransaction(txData, wallet.privateKey, 'base', 0);
// Legacy transactions have RLP list prefix (>= 0xc0), NOT 0x02
const firstByte = parseInt(signedHex.slice(2, 4), 16);
expect(firstByte).toBeGreaterThanOrEqual(0xc0);
});
});

// ============= ERC-20 Approval Transaction =============

describe('buildApprovalTransaction', () => {
Expand Down
71 changes: 68 additions & 3 deletions src/trading.js
Original file line number Diff line number Diff line change
Expand Up @@ -271,16 +271,29 @@ export function signSolanaTransaction(transactionBase64, privateKeyHex) {
* @returns {string} 0x-prefixed signed transaction hex
*/
// ⚠️ SECURITY: EVM transaction signing - requires thorough review before production use
// TODO: Always signs as legacy (type 0) transactions. Do we need EIP-1559 (type 2) support?
export function signEvmTransaction(txData, privateKeyHex, chain, nonce) {
const chainConfig = CHAIN_MAP[chain];
if (!chainConfig || chainConfig.type !== 'evm') {
throw new Error(`Unsupported EVM chain: ${chain}`);
}

// Use EIP-1559 (type 2) when the quote provides maxFeePerGas
if (txData.maxFeePerGas) {
return signEip1559EvmTransaction({
nonce,
maxFeePerGas: txData.maxFeePerGas,
maxPriorityFeePerGas: txData.maxPriorityFeePerGas || '0',
gas: txData.gas || txData.gasLimit || '210000',
to: txData.to,
value: txData.value || '0',
data: txData.data || '0x',
chainId: chainConfig.chainId,
}, privateKeyHex);
}

const tx = {
nonce,
gasPrice: toHex(txData.gasPrice || txData.maxFeePerGas || '1'),
gasPrice: toHex(txData.gasPrice || '1'),
gasLimit: toHex(txData.gas || txData.gasLimit || '210000'),
to: txData.to,
value: toHex(txData.value || '0'),
Expand Down Expand Up @@ -482,6 +495,52 @@ export function buildApprovalTransaction(tokenAddress, spenderAddress, privateKe
return signLegacyTransaction(tx, privateKeyHex);
}

// ============= EIP-1559 (Type 2) EVM Transaction Signing =============

/**
* Sign an EIP-1559 (type 2) EVM transaction.
*
* @param {object} txData - { nonce, maxFeePerGas, maxPriorityFeePerGas, gas, to, value, data, chainId }
* @param {string} privateKeyHex - 32-byte private key as hex
* @returns {string} 0x-prefixed signed transaction hex
*/
export function signEip1559EvmTransaction(txData, privateKeyHex) {
const chainId = BigInt(txData.chainId);
const nonce = BigInt(txData.nonce || 0);
const maxPriorityFeePerGas = BigInt(txData.maxPriorityFeePerGas || 0);
const maxFeePerGas = BigInt(txData.maxFeePerGas || 0);
const gasLimit = BigInt(txData.gas || txData.gasLimit || 210000);
const value = BigInt(txData.value || 0);

const bigIntToHex = (n) => n === 0n ? '0x' : '0x' + n.toString(16);

const txFields = [
bigIntToHex(chainId),
bigIntToHex(nonce),
bigIntToHex(maxPriorityFeePerGas),
bigIntToHex(maxFeePerGas),
bigIntToHex(gasLimit),
txData.to,
bigIntToHex(value),
txData.data || '0x',
[], // accessList
];

const unsigned = rlpEncode(txFields);
const txHash = keccak256(Buffer.concat([Buffer.from([0x02]), unsigned]));
const sig = signSecp256k1(txHash, Buffer.from(privateKeyHex, 'hex'));

const signed = rlpEncode([
...txFields,
bigIntToHex(BigInt(sig.v)),
'0x' + sig.r.toString('hex'),
'0x' + sig.s.toString('hex'),
]);

const rawTx = Buffer.concat([Buffer.from([0x02]), signed]);
return '0x' + rawTx.toString('hex');
}

// ============= Legacy (Type 0) EVM Transaction Signing =============
// ⚠️ SECURITY: Legacy EVM transaction signing - requires thorough review before production use

Expand Down Expand Up @@ -992,8 +1051,14 @@ EXAMPLES:
errorOutput(` Nonce: ${nonce}`);

errorOutput(' Signing EVM transaction...');
// EIP-1559 fields are top-level on the quote, not inside .transaction
const signTxData = { ...currentQuote.transaction };
if (currentQuote.maxFeePerGas) {
signTxData.maxFeePerGas = currentQuote.maxFeePerGas;
signTxData.maxPriorityFeePerGas = currentQuote.maxPriorityFeePerGas;
}
signedTransaction = signEvmTransaction(
currentQuote.transaction,
signTxData,
exported.evm.privateKey,
chain,
nonce
Expand Down