Packages

Higher-level Elixir SDK for Polymarket: Gamma market discovery, Data API, CTF/pUSD helpers, and convenience facades. Depends on polymarket_clob for the low-level CLOB surface.

Current section

Files

Jump to
polymarket_sdk scripts generate_tx_fixtures.mjs
Raw

scripts/generate_tx_fixtures.mjs

// Phase 11b transaction-serialization fixture generator.
//
// Produces deterministic RLP and EIP-155 legacy (type-0) transaction
// vectors. ethers.js v6 is the trusted reference encoder; the output
// JSON is checked into `test/fixtures/tx.json` and the Elixir test
// compares its hand-rolled output byte-for-byte.
//
// Setup (first time only) — installs ethers.js locally inside scripts/:
// (cd scripts && npm install)
//
// Generate fixtures (run from the polymarket package root):
// node scripts/generate_tx_fixtures.mjs > test/fixtures/tx.json
//
// The fixture file is the authoritative record once committed; do not
// regenerate without reviewing the diff.
import { ethers } from 'ethers';
// ── Helpers ──
// RLP-encode either a list or a hex string. ethers.js exposes
// `encodeRlp` directly so we can vector-check our hand-rolled RLP.
function rlpHex(value) {
return ethers.encodeRlp(value);
}
function buildTx(input) {
const tx = ethers.Transaction.from({
type: 0,
chainId: input.chainId,
nonce: input.nonce,
gasPrice: input.gasPrice,
gasLimit: input.gasLimit,
to: input.to, // null for contract creation
value: input.value,
data: input.data,
});
return tx;
}
function signingHashFor(tx) {
return tx.unsignedHash; // keccak256(tx.unsignedSerialized)
}
function signTx(tx, privateKey) {
const sigKey = new ethers.SigningKey(privateKey);
const sig = sigKey.sign(tx.unsignedHash);
tx.signature = sig;
return tx;
}
// ── Test inputs ──
const PK = '0x1111111111111111111111111111111111111111111111111111111111111111';
// Address derived from the test key by ethers (constant — captured here
// for documentation; the test doesn't compare against it).
const ADDR_FROM_PK = ethers.computeAddress(PK);
const TO_ADDR = '0x' + '11'.repeat(20);
const PUSD = '0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB';
const SOME_BYTECODE =
'0x6080604052348015600f57600080fd5b50600080fdfe' +
'a2646970667358221220c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1' +
'c1c1c1c1c1c1c1c164736f6c63430008140033';
// Pre-built ERC-20 approve calldata (matches Phase 11a fixture
// `approve_pusd_to_exchange_max`, which we know is correct).
const ERC20_APPROVE_CALLDATA = new ethers.Interface([
'function approve(address spender, uint256 amount)',
]).encodeFunctionData('approve', [
'0xE111180000d2663C0091e4f400237545B87B996B',
ethers.MaxUint256,
]);
const txInputs = [
{
name: 'polygon_simple_transfer',
description: 'Polygon legacy transfer with empty data and 1 MATIC value',
chainId: 137,
nonce: 0,
gasPrice: 30_000_000_000n,
gasLimit: 21_000n,
to: TO_ADDR,
value: 1_000_000_000_000_000_000n,
data: '0x',
},
{
name: 'polygon_erc20_approve',
description: 'Polygon contract call carrying ERC-20 approve calldata',
chainId: 137,
nonce: 5,
gasPrice: 50_000_000_000n,
gasLimit: 200_000n,
to: PUSD,
value: 0n,
data: ERC20_APPROVE_CALLDATA,
},
{
name: 'polygon_contract_creation',
description: 'Polygon contract creation (to == null)',
chainId: 137,
nonce: 0,
gasPrice: 30_000_000_000n,
gasLimit: 1_000_000n,
to: null,
value: 0n,
data: SOME_BYTECODE,
},
{
name: 'polygon_high_byte_nonce',
description: 'Polygon transfer with nonce that requires multi-byte RLP encoding',
chainId: 137,
nonce: 257,
gasPrice: 30_000_000_000n,
gasLimit: 21_000n,
to: TO_ADDR,
value: 0n,
data: '0x',
},
{
name: 'mainnet_simple_transfer',
description: 'Ethereum mainnet (chainId 1) — sanity check for the EIP-155 v formula',
chainId: 1,
nonce: 0,
gasPrice: 30_000_000_000n,
gasLimit: 21_000n,
to: TO_ADDR,
value: 1n,
data: '0x',
},
];
// ── Build and serialize ──
const fixtures = txInputs.map((input) => {
const unsigned = buildTx(input);
const signed = signTx(buildTx(input), PK);
return {
name: input.name,
description: input.description,
inputs: {
chain_id: input.chainId,
nonce: input.nonce.toString(),
gas_price: input.gasPrice.toString(),
gas_limit: input.gasLimit.toString(),
to: input.to, // null preserved
value: input.value.toString(),
data: input.data,
private_key: PK,
signer_address: ADDR_FROM_PK,
},
unsigned: {
rlp: unsigned.unsignedSerialized,
hash: unsigned.unsignedHash,
},
signature: {
// ethers stores the legacy "recovery" v (27 or 28). The EIP-155
// on-the-wire v is `chain_id * 2 + 35 + yParity` and lives inside
// `signed.rlp` — the byte-for-byte comparison validates it.
v_recovery: signed.signature.v.toString(),
y_parity: signed.signature.yParity,
v_eip155: (BigInt(input.chainId) * 2n + 35n + BigInt(signed.signature.yParity)).toString(),
r: signed.signature.r,
s: signed.signature.s,
},
signed: {
rlp: signed.serialized,
hash: signed.hash,
},
};
});
// ── Pure-RLP primitive vectors ──
//
// Cross-validate the RLP layer separately. These are the spec examples
// from https://ethereum.org/en/developers/docs/data-structures-and-encoding/rlp/
// plus a few more edge cases.
const rlpPrimitives = [
// Single byte < 0x80 → encoded as itself.
{ description: 'empty string', input: { kind: 'bytes', hex: '0x' }, rlp: rlpHex('0x') },
{ description: 'single zero byte', input: { kind: 'bytes', hex: '0x00' }, rlp: rlpHex('0x00') },
{
description: 'single byte 0x7f (max single-byte form)',
input: { kind: 'bytes', hex: '0x7f' },
rlp: rlpHex('0x7f'),
},
{
description: 'single byte 0x80 (boundary, requires prefix)',
input: { kind: 'bytes', hex: '0x80' },
rlp: rlpHex('0x80'),
},
{
description: '"dog" — short string',
input: { kind: 'bytes', hex: '0x' + Buffer.from('dog').toString('hex') },
rlp: rlpHex('0x' + Buffer.from('dog').toString('hex')),
},
{
description: '55-byte string (boundary for short-string form)',
input: { kind: 'bytes', hex: '0x' + 'aa'.repeat(55) },
rlp: rlpHex('0x' + 'aa'.repeat(55)),
},
{
description: '56-byte string (boundary into long-string form)',
input: { kind: 'bytes', hex: '0x' + 'aa'.repeat(56) },
rlp: rlpHex('0x' + 'aa'.repeat(56)),
},
{
description: '1024-byte string (long-string form requiring 2-byte length)',
input: { kind: 'bytes', hex: '0x' + 'aa'.repeat(1024) },
rlp: rlpHex('0x' + 'aa'.repeat(1024)),
},
// Lists
{
description: 'empty list',
input: { kind: 'list', items: [] },
rlp: rlpHex([]),
},
{
description: '["dog","cat"] — short list',
input: {
kind: 'list',
items: [
'0x' + Buffer.from('dog').toString('hex'),
'0x' + Buffer.from('cat').toString('hex'),
],
},
rlp: rlpHex([
'0x' + Buffer.from('dog').toString('hex'),
'0x' + Buffer.from('cat').toString('hex'),
]),
},
{
description: 'list of 11 short strings (boundary into long-list form)',
input: {
kind: 'list',
items: Array.from({ length: 11 }, (_, i) => '0x' + (i + 1).toString(16).padStart(2, '0').repeat(8)),
},
rlp: rlpHex(
Array.from({ length: 11 }, (_, i) => '0x' + (i + 1).toString(16).padStart(2, '0').repeat(8)),
),
},
];
// ── Output ──
const out = {
generated_by: 'scripts/generate_tx_fixtures.mjs',
encoder: `ethers.js ${ethers.version}`,
rlp_primitives: rlpPrimitives,
legacy_transactions: fixtures,
};
process.stdout.write(JSON.stringify(out, null, 2) + '\n');