diff --git a/packages/builder/test/build.test.ts b/packages/builder/test/build.test.ts index 82be9d3..63d2dab 100644 --- a/packages/builder/test/build.test.ts +++ b/packages/builder/test/build.test.ts @@ -133,6 +133,24 @@ describe('buildSendApex', () => { await assert.rejects(() => buildSendApex(lucid, { recipientAddress: FOREIGN_ADDRESS, amountApex: 0 }), /positive/); await assert.rejects(() => buildSendApex(lucid, { recipientAddress: FOREIGN_ADDRESS, amountApex: -5 }), /positive/); }); + + test('pins exact lovelace at the call site for a fractional amount float math gets wrong', async () => { + // 1.005 * 1_000_000 === 1004999.9999999999 in IEEE-754 (node -e + // "console.log(Math.floor(1.005*1e6))" -> 1004999, one lovelace short of + // the correct 1005000; see packages/shared/test/tx.test.ts for the full + // empirical proof). apexToLovelace itself is already proven exact there - + // this test instead pins that buildSendApex's call site actually invokes + // it, so a future edit reverting to Math.floor(amountApex * 1_000_000) + // fails HERE, not just in an untouched helper's own unit tests. + const r = await buildSendApex(lucid, { recipientAddress: FOREIGN_ADDRESS, amountApex: 1.005 }); + const outs = decodeOutputs(r.txCbor); + const toRecipient = outs.filter((o) => o.address === FOREIGN_ADDRESS); + assert.equal(toRecipient.length, 1); + assert.equal( + toRecipient[0].lovelace, 1_005_000n, + 'lost a lovelace - buildSendApex must call apexToLovelace, not Math.floor(amountApex * 1_000_000)', + ); + }); }); describe('buildSendTokens', () => { @@ -181,4 +199,37 @@ describe('buildSendTokens', () => { /positive/, ); }); + + test('pins exact lovelace at the call site on the apexAmount path for a fractional amount float math gets wrong', async () => { + // Same defect class as buildSendApex's pin above, but 1.005 AP3X + // (1,005,000 lovelace) turned out to be BELOW this output's actual + // min-UTxO once it carries a native asset - Lucid silently bumped it to + // 1,043,020 during .complete(), which would have made this test measure + // ledger dust-floor behavior instead of the float-conversion bug it + // exists to pin (confirmed empirically: that bump reproduces regardless + // of amountApex's exactness, so it is not this defect). Reused the + // endorse test's already-verified defect value instead - comfortably + // above min-UTxO here too, since the existing 2,000,000n default (see + // 'moves the token...' above) is already sufficient for this same + // single-token output shape: + // 8.03 * 1_000_000 === 8029999.999999999 in IEEE-754 (node -e + // "console.log(Math.floor(8.03*1e6))" -> 8029999, one lovelace short of + // the correct 8030000). This is the apexAmount branch's OWN call site + // (build.ts's buildSendTokens, not buildSendApex), so it needs its own + // pin; a shared helper test cannot prove either caller wires it up. + const r = await buildSendTokens(lucid, { + recipientAddress: FOREIGN_ADDRESS, + policyId: FIXTURE_TOKEN_UNIT.slice(0, 56), + assetName: FIXTURE_TOKEN_UNIT.slice(56), + amount: '50', + apexAmount: 8.03, + }); + const outs = decodeOutputs(r.txCbor); + const toRecipient = outs.find((o) => o.address === FOREIGN_ADDRESS); + assert.ok(toRecipient, 'no output to recipient'); + assert.equal( + toRecipient.lovelace, 8_030_000n, + 'lost a lovelace - buildSendTokens apexAmount path must call apexToLovelace, not Math.floor(apexAmount * 1_000_000)', + ); + }); }); diff --git a/packages/builder/test/gov-build.test.ts b/packages/builder/test/gov-build.test.ts index a2e1194..be5e3eb 100644 --- a/packages/builder/test/gov-build.test.ts +++ b/packages/builder/test/gov-build.test.ts @@ -326,6 +326,31 @@ describe('buildEndorse (offline, FixtureProvider)', () => { assert.equal(endorseOut!.amount().coin(), 7_000_000n, 'exactly-minimum stake (5 AP3X) must still build: 5 + 2 AP3X'); }); + test('pins exact lovelace at the call site for a fractional stake float math gets wrong', async () => { + // 10.005 and 5.005 (the "obvious" values at each family's minimum) both + // happen to be bit-exact under Math.floor(x * 1e6) on this Node/V8 build + // (verified: node -e "console.log(Math.floor(10.005*1e6), Math.floor(5.005*1e6))" + // -> 10005000 5005000, both correct - neither reproduces the defect). + // Scanned upward from the 5 AP3X endorse minimum + // for a real loser instead: 8.03 * 1_000_000 === 8029999.999999999 in + // IEEE-754 (node -e "console.log(Math.floor(8.03*1e6))" -> 8029999, one + // lovelace short of the correct 8030000). This pins buildEndorse's OWN + // call site - apexToLovelace itself is already proven exact in + // packages/shared/test/tx.test.ts; this test instead catches a future + // revert of THIS call site back to Math.floor(stakeApex * 1_000_000). + const r = await buildEndorse(lucid, { + agentDid: AGENT_DID, proposalTxHash: PROPOSAL_TX, proposalOutputIndex: 3, stakeApex: 8.03, + }); + assert.equal(r.stakeLovelace, '8030000'); + const tx = decodeTx(r.txCbor); + const endorseOut = findOutputTo(tx, r.scriptAddress); + assert.ok(endorseOut, 'no output to the endorsement address'); + assert.equal( + endorseOut!.amount().coin(), 10_030_000n, + 'lost a lovelace - buildEndorse must call apexToLovelace, not Math.floor(stakeApex * 1_000_000): stake (8.03 AP3X = 8,030,000 lovelace) + 2 AP3X rider (2,000,000) = 10,030,000', + ); + }); + test('rejects a stake below the minimum', async () => { await assert.rejects( buildEndorse(lucid, {