SPB Git

Repository Public

tron.test.ts 20.9 KB 300 lines typescript

1import { describe, expect, it } from 'vitest';2import fc from 'fast-check';3import { TronWeb, utils as tronUtils } from 'tronweb';4import { secp256k1 } from '@noble/curves/secp256k1.js';5import { sha256 } from '@noble/hashes/sha2.js';6import {7  buildTronTx, canonicalBytes, DEFAULT_POLICY, getAsset, intentHash, NETWORKS, normalizeAddress, pluginFor, toHex, tronAddressFromPublicKey,8  tronPlugin, tronWire, type Intent, type KeyHandle,9} from '../src/index.ts';1011// Public BIP-39 test vector mnemonic — holds no funds of ours, never used outside tests.12const MNEMONIC = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';13const net = NETWORKS.tron;14const USDT = 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t';15// official SDK: derivation m/44'/195'/0'/0/0, address, offline transaction building (no network: unroutable host, never contacted)16const sdkAccount = TronWeb.fromMnemonic(MNEMONIC, "m/44'/195'/0'/0/0");17const tw = new TronWeb({ fullHost: 'http://127.0.0.1:9' });18const priv = Uint8Array.from(Buffer.from(sdkAccount.privateKey.slice(2), 'hex'));19const pub = secp256k1.getPublicKey(priv, true);20const ME = 'TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH';21const DEST = 'TKHuVq1oKVruCGLvqVexFs6dawKv6fQgFs';2223/** same shape as the signer's key handle (services/isolated-signer/src/keys.ts) */24const key: KeyHandle = { curve: 'secp256k1', publicKey: pub, signDigest: (digest) => {25  const sig = secp256k1.sign(digest, priv, { prehash: false, lowS: true, format: 'recovered' });26  const compact = sig.slice(1);27  return { compact, recovery: sig[0]!, der: secp256k1.Signature.fromBytes(compact, 'compact').toBytes('der') };28} };2930const TS = 1_794_354_657_628, EXP = 1_794_354_717_000;31const now = Math.floor(TS / 1000);32const base = { v: 1 as const, proposalId: '6f1c1f0e-8f5f-4b1e-9c56-0d6b5f0f1a11', keyId: 'spw-0123456789abcdef', createdAt: now, expiresAt: now + 120, policyVersion: 3,33  network: 'tron', chainRef: net.chainRef, from: ME, to: DEST };34const X = { ref_block_bytes: '2660', ref_block_hash: 'afad6ef83bbf9b23', expiration: String(EXP), timestamp: String(TS) };35const trx: Intent = { ...base, asset: 'native', symbol: 'TRX', decimals: 6, amount: '1000000', maxFee: '1100000', x: X };36const usdt: Intent = { ...base, asset: USDT, symbol: 'USDT', decimals: 6, amount: '1000000', maxFee: '31000000', x: { ...X, fee_limit: '30000000' } };37const hex = (b: Uint8Array) => Buffer.from(b).toString('hex');3839const header = (x: Record<string, string>) => ({ ref_block_bytes: x.ref_block_bytes!, ref_block_hash: x.ref_block_hash!, expiration: Number(x.expiration), timestamp: Number(x.timestamp) });40/** the same transfer built by tronweb, offline */41async function sdkTx(i: Intent) {42  if (i.asset === 'native') return tw.transactionBuilder.sendTrx(i.to, Number(i.amount), i.from, { blockHeader: header(i.x!) });43  const r = await tw.transactionBuilder.triggerSmartContract(i.asset, 'transfer(address,uint256)',44    { feeLimit: Number(i.x!.fee_limit), callValue: 0, txLocal: true, blockHeader: header(i.x!) } as never,45    [{ type: 'address', value: i.to }, { type: 'uint256', value: i.amount }], i.from);46  return r.transaction;47}4849describe('TRON address', () => {50  it('derives the test-mnemonic address exactly like the official SDK (m/44\'/195\'/0\'/0/0)', () => {51    expect(sdkAccount.address).toBe(ME);52    expect(tronPlugin.path(net)).toBe("m/44'/195'/0'/0/0");53    expect(tronPlugin.address(net, pub)).toBe(ME);54    expect(tronAddressFromPublicKey(secp256k1.getPublicKey(priv, false))).toBe(ME);55    expect(pluginFor(net)).toBe(tronPlugin);56  });57  it('matches the SDK for random keys (property)', () => {58    fc.assert(fc.property(fc.uint8Array({ minLength: 32, maxLength: 32 }), (k) => {59      if (!secp256k1.utils.isValidSecretKey(k)) return true;60      return tronPlugin.address(net, secp256k1.getPublicKey(k, true)) === TronWeb.address.fromPrivateKey(hex(k));61    }), { numRuns: 40 });62  });63  it('normalizes only canonical base58check mainnet addresses', () => {64    expect(normalizeAddress('tron', ` ${DEST} `)).toBe(DEST);65    expect(normalizeAddress('tron', USDT)).toBe(USDT);66    for (const bad of [67      '4166426c7ac3d98b29191063833345b6bc540d7278', // hex form68      'TKHuVq1oKVruCGLvqVexFs6dawKv6fQgFt', // checksum69      'tkhuvq1okvrucglvqvexfs6dawkv6fqgfs', // case70      'T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb', // zero address71      '0x9858EfFD232B4033E47d90003D41EC34EcaEda94', '1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2', 'TKHuVq1oKVruCGLvqVexFs6dawKv6fQgF', '',72    ]) expect(() => normalizeAddress('tron', bad), bad).toThrow();73    // a valid base58check string with another version byte (0xa0 = TRON testnet prefix) is refused74    const other = TronWeb.address.fromHex('a0' + '66426c7ac3d98b29191063833345b6bc540d7278');75    expect(() => normalizeAddress('tron', other)).toThrow();76  });77});7879describe('TRON registry', () => {80  it('lists TRX and exactly two tokens, USDT then USDD, with a policy cap and a fee ceiling', () => {81    expect(net.family).toBe('tron');82    expect(net.coinType).toBe(195);83    expect(net.chainRef).toBe('00000000000000001ebf88508a03865c71d452e25f4d51194196a1d22b6653dc');84    expect(net.rpc.length).toBe(2);85    expect(net.tokens.map((t) => [t.id, t.symbol, t.decimals])).toEqual([[USDT, 'USDT', 6], ['TXDk8mbtRbXeYuMNS83CfKPaYYT8XWv9Hz', 'USDD', 18]]);86    expect(getAsset('tron', USDT).symbol).toBe('USDT');87    expect(() => getAsset('tron', 'TEkxiTehnzSmSe2XqrBj4w32RUN966rdz8')).toThrow(); // USDC on TRON: not allowlisted88    expect(DEFAULT_POLICY.perTx['tron:TRX']).toBe('150000000');89    expect(DEFAULT_POLICY.perTx['tron:USDT']).toBe('50000000');90    expect(DEFAULT_POLICY.maxFee.tron).toBe('40000000');91  });92  it('hashes a TRON intent (x section) and refuses a foreign chain section', () => {93    expect(toHex(intentHash(usdt))).toMatch(/^[0-9a-f]{64}$/);94    expect(toHex(intentHash(usdt))).not.toBe(toHex(intentHash({ ...usdt, x: { ...usdt.x!, fee_limit: '30000001' } })));95    const { x: _x, ...noX } = usdt;96    expect(() => canonicalBytes(noX as Intent)).toThrow();97  });98});99100describe('TRON transaction encoding vs the official SDK (tronweb, offline)', () => {101  it('TRX transfer: raw_data and txid are byte-identical', async () => {102    const mine = buildTronTx(trx, net), sdk = await sdkTx(trx);103    expect(hex(mine.rawData)).toBe(sdk.raw_data_hex);104    expect(hex(mine.txid)).toBe(sdk.txID);105    expect(mine.kind).toBe('TransferContract');106    expect(tronPlugin.build(trx, net).signDocs.map(hex)).toEqual([sdk.txID]);107  });108  it('USDT transfer: raw_data and txid are byte-identical', async () => {109    const mine = buildTronTx(usdt, net), sdk = await sdkTx(usdt);110    expect(hex(mine.rawData)).toBe(sdk.raw_data_hex);111    expect(hex(mine.txid)).toBe(sdk.txID);112    expect(mine.feeLimit).toBe(30_000_000n);113    expect(mine.bandwidthBytes).toBe(345); // what mainnet charges for this shape (net_usage of real USDT transfers)114  });115  it('the SDK decodes our bytes back to the same fields (and nothing else: no memo, no permission id, no call value)', () => {116    const d = tronUtils.deserializeTx.deserializeTransaction('TriggerSmartContract', hex(buildTronTx(usdt, net).rawData));117    expect({ ...d, contract: undefined, ref_block_hash: d.ref_block_hash.toLowerCase() }).toEqual({ contract: undefined, data: '', fee_limit: 30_000_000, ref_block_bytes: '2660',118      ref_block_hash: 'afad6ef83bbf9b23', expiration: EXP, timestamp: TS, scripts: '', auths: [] });119    expect(d.contract.length).toBe(1);120    expect(d.contract[0].type).toBe('TriggerSmartContract');121    expect(d.contract[0].Permission_id).toBe(0);122    const v = d.contract[0].parameter.value;123    expect(TronWeb.address.fromHex(v.owner_address)).toBe(ME);124    expect(TronWeb.address.fromHex(v.contract_address)).toBe(USDT);125    expect(v.data.toLowerCase()).toBe('a9059cbb' + '00000000000000000000000066426c7ac3d98b29191063833345b6bc540d7278' + '00000000000000000000000000000000000000000000000000000000000f4240');126    expect(v.call_value ?? 0).toBe(0);127    expect(v.call_token_value ?? 0).toBe(0);128    const n = tronUtils.deserializeTx.deserializeTransaction('TransferContract', hex(buildTronTx(trx, net).rawData));129    expect(n.contract.length).toBe(1);130    expect(n.fee_limit).toBe(0);131    const w = n.contract[0].parameter.value;132    expect([TronWeb.address.fromHex(w.owner_address), TronWeb.address.fromHex(w.to_address), w.amount]).toEqual([ME, DEST, 1_000_000]);133  });134  it('byte-identical for arbitrary parameters (property)', async () => {135    const hexN = (n: number) => fc.uint8Array({ minLength: n, maxLength: n }).map(hex);136    const dest = fc.uint8Array({ minLength: 32, maxLength: 32 }).filter((k) => secp256k1.utils.isValidSecretKey(k)).map((k) => tronAddressFromPublicKey(secp256k1.getPublicKey(k, true)));137    await fc.assert(fc.asyncProperty(fc.boolean(), fc.bigInt({ min: 1n, max: (1n << 53n) - 1n }), dest, hexN(2), hexN(8),138      fc.integer({ min: 1, max: 86_399_999 }), fc.integer({ min: -600_000, max: 600_000 }), fc.integer({ min: 1, max: 2_000_000_000 }),139      async (token, amount, to, rbb, rbh, life, skew, feeLimit) => {140        const timestamp = now * 1000 + skew, expiration = Math.min(timestamp + life, (now + 120) * 1000 + 600_000);141        fc.pre(expiration > timestamp && to !== ME);142        const x = { ref_block_bytes: rbb, ref_block_hash: rbh, expiration: String(expiration), timestamp: String(timestamp), ...(token ? { fee_limit: String(feeLimit) } : {}) };143        const i: Intent = { ...(token ? usdt : trx), to, amount: amount.toString(), maxFee: '20000000000', x };144        const mine = buildTronTx(i, net), sdk = await sdkTx(i);145        return hex(mine.rawData) === sdk.raw_data_hex && hex(mine.txid) === sdk.txID;146      }), { numRuns: 60 });147  });148  it('token amounts above 2^53 are encoded exactly', async () => {149    const i: Intent = { ...usdt, amount: '123456789012345678901234567890' };150    const sdk = await sdkTx(i);151    expect(hex(buildTronTx(i, net).rawData)).toBe(sdk.raw_data_hex);152  });153});154155describe('TRON encoding vs java-tron (recorded mainnet data)', () => {156  // Transaction 9dc1c48f…d1b3, block 87880468 (irreversible), read from two public full nodes on 2026-11-10.157  it('reproduces a real mainnet TRX transfer: raw_data_hex, txID and the bandwidth it was charged', () => {158    const ts = 1_794_315_198_458, c = Math.floor(ts / 1000);159    const real: Intent = { ...base, createdAt: c, expiresAt: c + 120, from: 'TEYtK8nj6LXrWtEvbGoiAYaV6Rzqs7hFpd', to: 'TV6MuMXfmLbBqPZvBHdwFsDnQeVfnmiuSi',160      asset: 'native', symbol: 'TRX', decimals: 6, amount: '7', maxFee: '1000000',161      x: { ref_block_bytes: 'f301', ref_block_hash: '8d1e8e380c40a4c6', expiration: '1794315258000', timestamp: String(ts) } };162    const t = buildTronTx(real, net);163    expect(hex(t.rawData)).toBe('0a02f30122088d1e8e380c40a4c640909198ac9c345a65080112610a2d747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e5472616e73666572436f6e747261637412300a1541323f0babf73d763030d62d11dcb3e3e72c1398a9121541d1c4bb7b2f39aba5707711719b2236b5b605af2e180770fabf94ac9c34');164    expect(hex(t.txid)).toBe('9dc1c48fc17d0bb3ff8942095b24d3a2d4f147e0debc779d0372126d4d00d1b3');165    expect(t.bandwidthBytes).toBe(265); // receipt.net_usage of that transaction166    expect(t.worstFee).toBe(265_000n);167  });168  // Answer of /wallet/createtransaction (tron-rpc.publicnode.com, 2026-11-10) for these parameters; scripts/smoke-tron.mts repeats169  // the comparison live for both shapes.170  it('reproduces what a full node builds for the same transfer', () => {171    const from = 'TV6MuMXfmLbBqPZvBHdwFsDnQeVfnmiuSi';172    const a = buildTronTx({ ...trx, from, to: DEST }, net);173    expect(hex(a.rawData)).toBe('0a0226602208afad6ef83bbf9b2340c8c280bf9c345a67080112630a2d747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e5472616e73666572436f6e747261637412320a1541d1c4bb7b2f39aba5707711719b2236b5b605af2e12154166426c7ac3d98b29191063833345b6bc540d727818c0843d70dcf2fcbe9c34');174    expect(hex(a.txid)).toBe('720a71525a4aa8ac8fcf8e00c1f5e7dcf6aeafdb7ce732ed7ddb4ad12f5bd157');175  });176});177178describe('TRON signing', () => {179  it.each([['TRX', trx], ['USDT', usdt]] as const)('%s: signature equals the SDK\'s, verifies, and the wire bytes equal the SDK protobuf', async (_n, i) => {180    const out = tronPlugin.sign(i, net, key);181    const sdk = await sdkTx(i);182    const signed = await tw.trx.sign(sdk, sdkAccount.privateKey.slice(2));183    expect(out.txid).toBe(sdk.txID);184    const raw = Uint8Array.from(Buffer.from(out.raw, 'hex'));185    const sig = raw.subarray(raw.length - 65);186    expect(hex(sig)).toBe(signed.signature[0]!.toLowerCase()); // RFC 6979 + low-S: deterministic, so identical to tronweb's187    // noble: verifies against the public key, and the recovery byte recovers the same key188    const digest = sha256(buildTronTx(i, net).rawData);189    expect(secp256k1.verify(sig.subarray(0, 64), digest, pub, { prehash: false })).toBe(true);190    const rec = secp256k1.Signature.fromBytes(sig.subarray(0, 64), 'compact').addRecoveryBit(sig[64]! - 27).recoverPublicKey(digest).toBytes(true);191    expect(hex(rec)).toBe(hex(pub));192    // broadcast payload = protobuf Transaction { raw_data, signature }, as serialized by the SDK's own protobuf classes193    const pb = tronUtils.transaction.txJsonToPb(sdk);194    pb.addSignature(Uint8Array.from(Buffer.from(signed.signature[0]!, 'hex')));195    expect(out.raw).toBe(hex(pb.serializeBinary()));196    expect(out.raw).toBe(hex(tronWire(buildTronTx(i, net).rawData, sig)));197  });198  it('refuses to sign for an address the key does not control', () => {199    const other = secp256k1.getPublicKey(sha256(new TextEncoder().encode('another test key')), true);200    expect(() => tronPlugin.sign(trx, net, { ...key, publicKey: other })).toThrow(/not controlled/);201    expect(() => tronPlugin.sign(trx, net, { curve: 'ed25519', publicKey: pub, signMessage: () => new Uint8Array(64) })).toThrow();202    // a handle that returns a signature made with another key is caught by the self-check203    const rogue = sha256(new TextEncoder().encode('rogue'));204    const bad: KeyHandle = { ...key, signDigest: (d) => { const s = secp256k1.sign(d, rogue, { prehash: false, lowS: true, format: 'recovered' }); return { compact: s.slice(1), recovery: s[0]!, der: new Uint8Array() }; } };205    expect(() => tronPlugin.sign(trx, net, bad)).toThrow(/self-check/);206  });207});208209describe('TRON builder fails closed', () => {210  const bad = (name: string, i: Intent, re?: RegExp) => it(name, () => { if (re) expect(() => tronPlugin.build(i, net)).toThrow(re); else expect(() => tronPlugin.build(i, net)).toThrow(); });211  const withX = (i: Intent, patch: Record<string, string | undefined>): Intent => {212    const x: Record<string, string> = { ...i.x! };213    for (const [k, v] of Object.entries(patch)) { if (v === undefined) delete x[k]; else x[k] = v; }214    return { ...i, x };215  };216  it('accepts the two reference intents', () => { expect(() => tronPlugin.build(trx, net)).not.toThrow(); expect(() => tronPlugin.build(usdt, net)).not.toThrow(); });217  bad('wrong genesis', { ...trx, chainRef: NETWORKS.bitcoin.chainRef }, /chain identity/);218  bad('another network id', { ...trx, network: 'solana' }, /not a TRON intent/);219  it('another network definition', () => expect(() => tronPlugin.build(trx, NETWORKS.solana)).toThrow(/not a TRON intent/));220  bad('hex-form source', { ...trx, from: '41c8599111f29c1e1e061265b4af93ea1f274ad78a' });221  bad('destination with surrounding space', { ...trx, to: ` ${DEST}` });222  bad('destination with a bad checksum', { ...trx, to: 'TKHuVq1oKVruCGLvqVexFs6dawKv6fQgFt' });223  bad('EVM destination', { ...trx, to: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8' });224  bad('zero-address destination', { ...trx, to: 'T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb' }, /zero address/);225  bad('destination equal to source', { ...trx, to: ME }, /source equals destination/);226  bad('TRX sent to the token contract', { ...trx, to: USDT }, /token contract/);227  bad('USDT sent to the token contract', { ...usdt, to: USDT }, /token contract/);228  bad('token that is not allowlisted', { ...usdt, asset: 'TEkxiTehnzSmSe2XqrBj4w32RUN966rdz8', symbol: 'USDC' }, /not allowlisted/);229  bad('EVM token address as asset', { ...usdt, asset: '0xdAC17F958D2ee523a2206206994597C13D831ec7' }, /not allowlisted/);230  bad('wrong symbol', { ...usdt, symbol: 'USDC' }, /metadata/);231  bad('wrong decimals', { ...trx, decimals: 18 }, /metadata/);232  bad('zero amount', { ...trx, amount: '0' }, /positive/);233  bad('zero token amount', { ...usdt, amount: '0' }, /positive/);234  bad('native amount beyond int64', { ...trx, amount: (1n << 63n).toString() }, /out of range/);235  bad('token amount beyond uint256', { ...usdt, amount: (1n << 256n).toString() });236  bad('native fee above maxFee (bandwidth burn)', { ...trx, maxFee: '266999' }, /fee exceeds/);237  bad('fee_limit above maxFee', { ...usdt, maxFee: '30000000' }, /fee exceeds/);238  bad('fee_limit + bandwidth above maxFee', { ...usdt, maxFee: '30344999' }, /fee exceeds/);239  it('fee exactly at maxFee is accepted', () => {240    expect(() => tronPlugin.build({ ...trx, maxFee: '267000' }, net)).not.toThrow();241    expect(() => tronPlugin.build({ ...usdt, maxFee: '30345000' }, net)).not.toThrow();242  });243  bad('fee_limit of zero', withX(usdt, { fee_limit: '0' }), /fee_limit/);244  bad('fee_limit above the protocol maximum', { ...withX(usdt, { fee_limit: '15000000001' }), maxFee: '99000000000' }, /fee_limit/);245  bad('fee_limit on a native transfer', withX(trx, { fee_limit: '1' }), /exactly/);246  bad('missing fee_limit on a token transfer', withX(usdt, { fee_limit: undefined }), /exactly/);247  bad('unknown x key', withX(trx, { data: '00' }), /exactly/);248  bad('permission id smuggled in x', withX(usdt, { permission_id: '2' }), /exactly/);249  bad('call_value smuggled in x', withX(usdt, { call_value: '1' }), /exactly/);250  for (const k of ['ref_block_bytes', 'ref_block_hash', 'expiration', 'timestamp']) bad(`missing x.${k}`, withX(trx, { [k]: undefined }), /exactly/);251  bad('no x section', (() => { const { x: _x, ...r } = trx; return r as Intent; })(), /missing TRON parameters/);252  bad('foreign evm section', { ...trx, evm: { nonce: 0, gasLimit: '21000', maxFeePerGas: '1', maxPriorityFeePerGas: '1' } }, /foreign/);253  bad('memo', { ...trx, memo: 'hello' }, /memo/);254  bad('ref_block_bytes wrong length', withX(trx, { ref_block_bytes: '266000' }));255  bad('ref_block_bytes uppercase', withX(trx, { ref_block_bytes: '26AB' }));256  bad('ref_block_hash wrong length', withX(trx, { ref_block_hash: 'afad6ef83bbf9b' }));257  bad('expiration not decimal', withX(trx, { expiration: '0x1' }));258  bad('expiration with leading zero', withX(trx, { expiration: '0' + EXP }));259  bad('expiration before timestamp', withX(trx, { expiration: String(TS) }), /after timestamp/);260  bad('expiration a day away', withX(trx, { expiration: String(TS + 86_400_000) }));261  bad('expiration outliving the approval window', withX(trx, { expiration: String((now + 120) * 1000 + 600_001) }), /approval window/);262  bad('timestamp far from the approval window', withX(trx, { timestamp: String(TS - 700_000) }), /approval window/);263  bad('negative timestamp', withX(trx, { timestamp: '-1' }));264});265266describe('TRON: every intent field that reaches the chain changes the signed bytes or throws', () => {267  const sig = (i: Intent): string | null => { try { return tronPlugin.sign(i, net, key).raw; } catch { return null; } };268  const OTHER = 'TV6MuMXfmLbBqPZvBHdwFsDnQeVfnmiuSi';269  const mutations: [string, (i: Intent) => Intent][] = [270    ['amount', (i) => ({ ...i, amount: '1000001' })],271    ['to', (i) => ({ ...i, to: OTHER })],272    ['from', (i) => ({ ...i, from: OTHER })],273    ['asset', (i) => (i.asset === 'native' ? { ...usdt } : { ...trx })],274    ['network', (i) => ({ ...i, network: 'solana' })],275    ['chainRef', (i) => ({ ...i, chainRef: i.chainRef.replace(/.$/, 'd') })],276    ['symbol', (i) => ({ ...i, symbol: 'XXX' })],277    ['decimals', (i) => ({ ...i, decimals: 8 })],278    ['x.ref_block_bytes', (i) => ({ ...i, x: { ...i.x!, ref_block_bytes: '2661' } })],279    ['x.ref_block_hash', (i) => ({ ...i, x: { ...i.x!, ref_block_hash: 'afad6ef83bbf9b24' } })],280    ['x.expiration', (i) => ({ ...i, x: { ...i.x!, expiration: String(EXP + 1) } })],281    ['x.timestamp', (i) => ({ ...i, x: { ...i.x!, timestamp: String(TS + 1) } })],282    ['x.fee_limit', (i) => ({ ...i, x: { ...i.x!, fee_limit: '29999999' } })],283    ['maxFee below the fee', (i) => ({ ...i, maxFee: '1' })],284    ['memo', (i) => ({ ...i, memo: '1' })],285  ];286  for (const [label, ref] of [['TRX', trx], ['USDT', usdt]] as const)287    for (const [name, f] of mutations) it(`${label}: ${name}`, () => {288      const a = sig(ref), b = sig(f(ref));289      expect(a).not.toBeNull();290      expect(b === null || b !== a).toBe(true);291    });292  it('same intent, same bytes (deterministic)', () => { expect(sig(usdt)).toBe(sig({ ...usdt, x: { ...usdt.x! } })); });293  it('fields outside the transaction (proposal id, approval window, larger fee ceiling) change the approved hash, not the bytes', () => {294    for (const j of [{ ...usdt, proposalId: '6f1c1f0e-8f5f-4b1e-9c56-0d6b5f0f1a12' }, { ...usdt, expiresAt: usdt.expiresAt + 1 }, { ...usdt, maxFee: '31000001' }, { ...usdt, policyVersion: 4 }]) {295      expect(sig(j)).toBe(sig(usdt));296      expect(toHex(intentHash(j))).not.toBe(toHex(intentHash(usdt)));297    }298  });299});300
Files main
Menu

Theme