From d41140132199a2c49c9e0e731de031c97ad9b8f3 Mon Sep 17 00:00:00 2001 From: Mateusz Czeladka Date: Fri, 26 Jun 2026 14:55:54 +0200 Subject: [PATCH 1/5] test(js): cover Plutus/script paths (lock fixture + Plutus mint round-trip) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The §3 TODO item predates the TxPlan refactor, which deleted the fluent ScriptTxBuilder/collectFromScript/mintPlutusAssets/readFrom API it referenced; script/Plutus paths are now TxPlan YAML fixtures. intents.e2e.test.js already covered the top-level intents + Plutus mint/spend; this closes the remaining gaps: - Build the previously-untested plutus/plutus_lock.yaml (pay-to-script with a datum hash; no exec units). - Deepen the Plutus mint/spend assertions to non-empty CBOR + 64-char hash + positive fee (shared assertBuilt helper, mirroring Python's _assert_built), keeping the exec-units-required negative checks. - Assert plutus.dataHash('182a') reproduces the lock fixture's datum hash, tying the primitive to the on-fixture value. - Add a DevKit integration round-trip for a Plutus mint: build with exec units -> sign (payment) -> submit -> assert the minted asset landed on-chain, mirroring Go's TestIntegrationPlutusMint (skip-gated like the others). Update the TODO item to reflect the refactor and mark it done. Co-Authored-By: Claude Opus 4.8 (1M context) --- TODO.md | 2 +- wrappers/js/test/intents.e2e.test.js | 36 ++++++++++++++---- wrappers/js/test/quicktx.integration.test.js | 40 ++++++++++++++++++++ 3 files changed, 69 insertions(+), 9 deletions(-) diff --git a/TODO.md b/TODO.md index 3d8835c..dddae4b 100644 --- a/TODO.md +++ b/TODO.md @@ -116,7 +116,7 @@ untouched and the helpers are optional and swappable. This is the sibling of §2 ## 3. Testing -- [ ] `P1` Add JS integration tests for the script/Plutus paths — these are implemented in `wrappers/js/src/index.js` but have **zero** test coverage: `ScriptTxBuilder` validators + redeemers, `collectFromScript`, `mintPlutusAssets`, `readFrom` (reference inputs), and compose-with-`ScriptTx`. Python's `tests/` are the reference for what to assert. +- [x] `P1` ~~Add JS integration tests for the script/Plutus paths.~~ **Done (and the item's premise was superseded by the TxPlan refactor):** the old fluent `ScriptTxBuilder` / `collectFromScript` / `mintPlutusAssets` / `readFrom` API was deleted — script/Plutus paths are now TxPlan YAML fixtures. Offline (`test/intents.e2e.test.js`): all 20 top-level intents (incl. `reference_input`, `compose`, `native_script`) plus the three `plutus/` fixtures — **mint**, **spend**, and **lock** — each asserting non-empty CBOR + 64-char hash + positive fee, that mint/spend **require** caller-supplied exec units (build throws without them), and that `plutus.dataHash` reproduces the lock fixture's datum hash. Integration (`test/quicktx.integration.test.js`, DevKit): a Plutus-mint **build → sign → submit → assert the minted asset landed on-chain** round-trip, mirroring the Go suite. - [ ] `P1` Raise Go and Rust test breadth toward Python's (~100 cases vs ~61); port Python's per-module unit tests. - [ ] `P1` Add a cross-wrapper parity test matrix asserting every `@CEntryPoint` is exercised in every language. - [ ] `P2` Run the Yaci DevKit integration tests in CI (containerized DevKit) instead of skip-if-not-running. diff --git a/wrappers/js/test/intents.e2e.test.js b/wrappers/js/test/intents.e2e.test.js index 02fe77c..4ccdf5d 100644 --- a/wrappers/js/test/intents.e2e.test.js +++ b/wrappers/js/test/intents.e2e.test.js @@ -40,6 +40,14 @@ function utxos() { ]; } +// A successfully built tx: non-empty CBOR, a 64-char hash, and a positive fee. Mirrors the Python +// reference's _assert_built so the wrappers assert the same shape. +function assertBuilt(result) { + expect(result.tx_cbor.length).toBeGreaterThan(0); + expect(result.tx_hash.length).toBe(64); + expect(Number(result.fee)).toBeGreaterThan(0); +} + describe("QuickTx intents E2E", () => { let bridge; beforeAll(() => { bridge = new CclBridge(); }); @@ -50,19 +58,18 @@ describe("QuickTx intents E2E", () => { for (const f of fixtures) { it(`builds ${f.replace(".yaml", "")}`, () => { const yaml = readFileSync(join(FIXTURES, f), "utf8"); - const result = bridge.quicktx.build(yaml, utxos(), PROTOCOL_PARAMS); - expect(result.tx_cbor.length).toBeGreaterThan(0); - expect(result.tx_hash.length).toBe(64); - expect(Number(result.fee)).toBeGreaterThan(0); + assertBuilt(bridge.quicktx.build(yaml, utxos(), PROTOCOL_PARAMS)); }); } + // --- Plutus / script paths (the plutus/ sub-directory, not covered by the top-level loop) --- + it("builds a Plutus mint with execution units", () => { const yaml = readFileSync(join(FIXTURES, "plutus", "script_minting.yaml"), "utf8"); const u = [{ tx_hash: "a".repeat(64), output_index: 0, address: SENDER, amount: [{ unit: "lovelace", quantity: "2000000000" }] }]; - const result = bridge.quicktx.build(yaml, u, PROTOCOL_PARAMS, EXEC_UNITS); - expect(result.tx_hash.length).toBe(64); + assertBuilt(bridge.quicktx.build(yaml, u, PROTOCOL_PARAMS, EXEC_UNITS)); + // The Plutus path requires caller-supplied exec units; without them the build is rejected. expect(() => bridge.quicktx.build(yaml, u, PROTOCOL_PARAMS)).toThrow(); }); @@ -74,8 +81,21 @@ describe("QuickTx intents E2E", () => { { tx_hash: "a".repeat(64), output_index: 0, address: SENDER, amount: [{ unit: "lovelace", quantity: "2000000000" }] }, ]; - const result = bridge.quicktx.build(yaml, u, PROTOCOL_PARAMS, EXEC_UNITS); - expect(result.tx_hash.length).toBe(64); + assertBuilt(bridge.quicktx.build(yaml, u, PROTOCOL_PARAMS, EXEC_UNITS)); expect(() => bridge.quicktx.build(yaml, u, PROTOCOL_PARAMS)).toThrow(); }); + + it("builds a Plutus lock (pay to a script address with a datum hash)", () => { + // Locking funds at a script address is a plain payment carrying a datum hash — no script runs, + // so no exec units are required (unlike the spend that later unlocks it). + const yaml = readFileSync(join(FIXTURES, "plutus", "plutus_lock.yaml"), "utf8"); + assertBuilt(bridge.quicktx.build(yaml, utxos(), PROTOCOL_PARAMS)); + }); + + it("derives the datum hash the lock fixture commits to", () => { + // plutus_lock.yaml locks under datum_hash 9e1199… — the Plutus data hash of the integer 42 + // (CBOR 182a). Assert the bridge computes the exact value the fixture embeds, tying the + // plutus.dataHash primitive to the on-fixture datum. + expect(bridge.plutus.dataHash("182a")).toBe(SCRIPT_DATUM_HASH); + }); }); diff --git a/wrappers/js/test/quicktx.integration.test.js b/wrappers/js/test/quicktx.integration.test.js index a112f82..74de00c 100644 --- a/wrappers/js/test/quicktx.integration.test.js +++ b/wrappers/js/test/quicktx.integration.test.js @@ -11,9 +11,22 @@ import { describe, it, expect, beforeAll, afterAll, setDefaultTimeout } from "bun:test"; import { CclBridge, TESTNET } from "../src/index.js"; import { DevKitHelper } from "./devkit-helper.js"; +import { readFileSync } from "fs"; +import { join, dirname } from "path"; +import { fileURLToPath } from "url"; setDefaultTimeout(60_000); +const FIXTURES = join(dirname(fileURLToPath(import.meta.url)), "../../../test-fixtures/quicktx-intents"); + +// The fixed test account the quicktx-intents fixtures are derived from (account 0/0). A Plutus +// fixture bakes this address in as the fee payer, so submitting it means funding and signing with +// this exact account rather than a freshly-created one. +const INTENT_MNEMONIC = "test walk nut penalty hip pave soap entry language right filter choice"; +const INTENT_SENDER = "addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp"; +// The enterprise address the mint fixtures pay the freshly minted asset to. +const MINT_RECEIVER = "addr_test1vz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzerspjrlsz"; + function paymentYaml(from, to, quantity) { return ` version: 1.0 @@ -137,4 +150,31 @@ transaction: const yaml = paymentYaml(sender.base_address, receiver.base_address, "100000000"); expect(() => bridge.quicktx.build(yaml, utxos, pp)).toThrow(); }); + + // Plutus round-trip: build the script_minting fixture with caller-supplied exec units, sign with + // the fee payer's payment key, submit, and assert the minted asset actually landed on-chain. + // "Submit accepted" alone doesn't prove the script ran and minted — the receiver holding a + // non-lovelace asset does. Mirrors the Go TestIntegrationPlutusMint. + it("should build, sign, and submit a Plutus mint", async () => { + if (skip) return; + + await devkit.topup(INTENT_SENDER, 6000); + await devkit.waitForBlock(3000); + + const utxos = await devkit.getUtxos(INTENT_SENDER); + const pp = await devkit.getProtocolParams(); + const yaml = readFileSync(join(FIXTURES, "plutus", "script_minting.yaml"), "utf8"); + + const result = bridge.quicktx.build(yaml, utxos, pp, [{ mem: 2000000, steps: 500000000 }]); + expect(result.tx_hash.length).toBe(64); + + const signedTx = bridge.account.signTxWithKeys(INTENT_MNEMONIC, TESTNET, 0, 0, result.tx_cbor, ["payment"]); + const txHash = await devkit.submitTx(signedTx); + expect(txHash).toBeTruthy(); + + await devkit.waitForBlock(3000); + const receiverUtxos = await devkit.getUtxos(MINT_RECEIVER); + const hasMintedAsset = receiverUtxos.some((u) => u.amount.some((a) => a.unit !== "lovelace")); + expect(hasMintedAsset).toBe(true); + }); }); From c21c6c84a9fc53c817210f3e9e61f2ee16a303a2 Mon Sep 17 00:00:00 2001 From: Mateusz Czeladka Date: Fri, 26 Jun 2026 15:17:42 +0200 Subject: [PATCH 2/5] test(js): isolate the Plutus-mint integration test on a fresh devnet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Plutus mint round-trip failed in CI: it ran 4th in the describe, on a devnet already mutated by the three payment tests, and never reset — unlike the Go suite's buildSignSubmit, which resets immediately before building. The submit was rejected but the helper returns the response body on any status, so the truthy txHash check passed and only the later 'asset landed' assert failed, masking the cause. Reset + wait before funding the fixed fixture account (mirroring Go), and assert the submit returns a real 64-char tx hash so a rejected Plutus validation surfaces at submit rather than as a missing asset. Co-Authored-By: Claude Opus 4.8 (1M context) --- wrappers/js/test/quicktx.integration.test.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/wrappers/js/test/quicktx.integration.test.js b/wrappers/js/test/quicktx.integration.test.js index 74de00c..747fd91 100644 --- a/wrappers/js/test/quicktx.integration.test.js +++ b/wrappers/js/test/quicktx.integration.test.js @@ -158,6 +158,11 @@ transaction: it("should build, sign, and submit a Plutus mint", async () => { if (skip) return; + // Mirror the Go suite's buildSignSubmit: reset to an isolated devnet first, then fund the + // fixture's fee payer and build from its real UTXOs. (The fixture bakes in the fixed account, + // so it must run against clean state rather than the devnet the earlier payment tests mutated.) + await devkit.reset(); + await devkit.waitForBlock(3000); await devkit.topup(INTENT_SENDER, 6000); await devkit.waitForBlock(3000); @@ -169,8 +174,10 @@ transaction: expect(result.tx_hash.length).toBe(64); const signedTx = bridge.account.signTxWithKeys(INTENT_MNEMONIC, TESTNET, 0, 0, result.tx_cbor, ["payment"]); - const txHash = await devkit.submitTx(signedTx); - expect(txHash).toBeTruthy(); + // A successful submit returns the 64-char tx hash; a rejected one returns an error body. Assert + // the hash so a failed Plutus validation surfaces here, not as a missing asset further down. + const submitResult = await devkit.submitTx(signedTx); + expect(submitResult).toMatch(/^[0-9a-f]{64}$/); await devkit.waitForBlock(3000); const receiverUtxos = await devkit.getUtxos(MINT_RECEIVER); From 03e5139b107244ed3e43bcc0aa3d12ca6a949348 Mon Sep 17 00:00:00 2001 From: Mateusz Czeladka Date: Fri, 26 Jun 2026 15:44:33 +0200 Subject: [PATCH 3/5] test(js): drop the DevKit Plutus-mint submit; keep offline coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The DevKit Plutus-mint round-trip is rejected by the node with PPViewHashesDontMatch: the cost models marshalled from /epochs/parameters don't reproduce the node's script-integrity hash, so the built tx's script-data hash mismatches. The Go suite submits the same fixture successfully, so this is a JS-side protocol-params/cost-model serialization subtlety — and node-level Plutus acceptance is the Go suite's job by design anyway (only Go submits the intent set to DevKit; the other wrappers have build E2E parity). Revert quicktx.integration.test.js to develop and keep the build-level additions (plutus_lock, deepened mint/spend assertions, datum-hash invariant). Record the JS Plutus-submit round-trip as a P2 follow-up needing a live DevKit to debug. Co-Authored-By: Claude Opus 4.8 (1M context) --- TODO.md | 2 +- wrappers/js/test/quicktx.integration.test.js | 47 -------------------- 2 files changed, 1 insertion(+), 48 deletions(-) diff --git a/TODO.md b/TODO.md index dddae4b..795999a 100644 --- a/TODO.md +++ b/TODO.md @@ -116,7 +116,7 @@ untouched and the helpers are optional and swappable. This is the sibling of §2 ## 3. Testing -- [x] `P1` ~~Add JS integration tests for the script/Plutus paths.~~ **Done (and the item's premise was superseded by the TxPlan refactor):** the old fluent `ScriptTxBuilder` / `collectFromScript` / `mintPlutusAssets` / `readFrom` API was deleted — script/Plutus paths are now TxPlan YAML fixtures. Offline (`test/intents.e2e.test.js`): all 20 top-level intents (incl. `reference_input`, `compose`, `native_script`) plus the three `plutus/` fixtures — **mint**, **spend**, and **lock** — each asserting non-empty CBOR + 64-char hash + positive fee, that mint/spend **require** caller-supplied exec units (build throws without them), and that `plutus.dataHash` reproduces the lock fixture's datum hash. Integration (`test/quicktx.integration.test.js`, DevKit): a Plutus-mint **build → sign → submit → assert the minted asset landed on-chain** round-trip, mirroring the Go suite. +- [x] `P1` ~~Add JS integration tests for the script/Plutus paths.~~ **Done (and the item's premise was superseded by the TxPlan refactor):** the old fluent `ScriptTxBuilder` / `collectFromScript` / `mintPlutusAssets` / `readFrom` API was deleted — script/Plutus paths are now TxPlan YAML fixtures, covered at the build level in `test/intents.e2e.test.js`: all 20 top-level intents (incl. `reference_input`, `compose`, `native_script`) plus the three `plutus/` fixtures — **mint**, **spend**, and **lock** — each asserting non-empty CBOR + 64-char hash + positive fee, that mint/spend **require** caller-supplied exec units (build throws without them), and that `plutus.dataHash` reproduces the lock fixture's datum hash. Node-level Plutus *submit* acceptance stays with the **Go** suite per the cross-wrapper-submit decision (only Go submits the intent set to DevKit; the others have build E2E parity). _Follow-up `P2`: a JS Plutus-mint DevKit round-trip is blocked on a cost-model script-data-hash subtlety — the node rejects the submit with `PPViewHashesDontMatch` because the cost models marshalled from `/epochs/parameters` don't reproduce the node's script-integrity hash; needs investigation against a live DevKit (likely a protocol-params/cost-model serialization difference vs the Go path)._ - [ ] `P1` Raise Go and Rust test breadth toward Python's (~100 cases vs ~61); port Python's per-module unit tests. - [ ] `P1` Add a cross-wrapper parity test matrix asserting every `@CEntryPoint` is exercised in every language. - [ ] `P2` Run the Yaci DevKit integration tests in CI (containerized DevKit) instead of skip-if-not-running. diff --git a/wrappers/js/test/quicktx.integration.test.js b/wrappers/js/test/quicktx.integration.test.js index 747fd91..a112f82 100644 --- a/wrappers/js/test/quicktx.integration.test.js +++ b/wrappers/js/test/quicktx.integration.test.js @@ -11,22 +11,9 @@ import { describe, it, expect, beforeAll, afterAll, setDefaultTimeout } from "bun:test"; import { CclBridge, TESTNET } from "../src/index.js"; import { DevKitHelper } from "./devkit-helper.js"; -import { readFileSync } from "fs"; -import { join, dirname } from "path"; -import { fileURLToPath } from "url"; setDefaultTimeout(60_000); -const FIXTURES = join(dirname(fileURLToPath(import.meta.url)), "../../../test-fixtures/quicktx-intents"); - -// The fixed test account the quicktx-intents fixtures are derived from (account 0/0). A Plutus -// fixture bakes this address in as the fee payer, so submitting it means funding and signing with -// this exact account rather than a freshly-created one. -const INTENT_MNEMONIC = "test walk nut penalty hip pave soap entry language right filter choice"; -const INTENT_SENDER = "addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp"; -// The enterprise address the mint fixtures pay the freshly minted asset to. -const MINT_RECEIVER = "addr_test1vz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzerspjrlsz"; - function paymentYaml(from, to, quantity) { return ` version: 1.0 @@ -150,38 +137,4 @@ transaction: const yaml = paymentYaml(sender.base_address, receiver.base_address, "100000000"); expect(() => bridge.quicktx.build(yaml, utxos, pp)).toThrow(); }); - - // Plutus round-trip: build the script_minting fixture with caller-supplied exec units, sign with - // the fee payer's payment key, submit, and assert the minted asset actually landed on-chain. - // "Submit accepted" alone doesn't prove the script ran and minted — the receiver holding a - // non-lovelace asset does. Mirrors the Go TestIntegrationPlutusMint. - it("should build, sign, and submit a Plutus mint", async () => { - if (skip) return; - - // Mirror the Go suite's buildSignSubmit: reset to an isolated devnet first, then fund the - // fixture's fee payer and build from its real UTXOs. (The fixture bakes in the fixed account, - // so it must run against clean state rather than the devnet the earlier payment tests mutated.) - await devkit.reset(); - await devkit.waitForBlock(3000); - await devkit.topup(INTENT_SENDER, 6000); - await devkit.waitForBlock(3000); - - const utxos = await devkit.getUtxos(INTENT_SENDER); - const pp = await devkit.getProtocolParams(); - const yaml = readFileSync(join(FIXTURES, "plutus", "script_minting.yaml"), "utf8"); - - const result = bridge.quicktx.build(yaml, utxos, pp, [{ mem: 2000000, steps: 500000000 }]); - expect(result.tx_hash.length).toBe(64); - - const signedTx = bridge.account.signTxWithKeys(INTENT_MNEMONIC, TESTNET, 0, 0, result.tx_cbor, ["payment"]); - // A successful submit returns the 64-char tx hash; a rejected one returns an error body. Assert - // the hash so a failed Plutus validation surfaces here, not as a missing asset further down. - const submitResult = await devkit.submitTx(signedTx); - expect(submitResult).toMatch(/^[0-9a-f]{64}$/); - - await devkit.waitForBlock(3000); - const receiverUtxos = await devkit.getUtxos(MINT_RECEIVER); - const hasMintedAsset = receiverUtxos.some((u) => u.amount.some((a) => a.unit !== "lovelace")); - expect(hasMintedAsset).toBe(true); - }); }); From 0da6ba17c9ca11f65d06a7fd2c33c4eb05d2a77e Mon Sep 17 00:00:00 2001 From: Mateusz Czeladka Date: Fri, 26 Jun 2026 15:58:40 +0200 Subject: [PATCH 4/5] test(js): instrument Plutus-mint submit + try cost-model fallback Diagnostic CI cycle for the PPViewHashesDontMatch rejection: log the cost-model shape DevKit's /epochs/parameters returns, and try dropping the fetched cost models so the native lib uses its built-in standard Conway cost models (which should match the devnet node). The CI log will confirm whether the fallback is accepted; if not, the logged cost-model shape drives the real fix. Co-Authored-By: Claude Opus 4.8 (1M context) --- wrappers/js/test/quicktx.integration.test.js | 59 ++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/wrappers/js/test/quicktx.integration.test.js b/wrappers/js/test/quicktx.integration.test.js index a112f82..5ca8522 100644 --- a/wrappers/js/test/quicktx.integration.test.js +++ b/wrappers/js/test/quicktx.integration.test.js @@ -11,9 +11,22 @@ import { describe, it, expect, beforeAll, afterAll, setDefaultTimeout } from "bun:test"; import { CclBridge, TESTNET } from "../src/index.js"; import { DevKitHelper } from "./devkit-helper.js"; +import { readFileSync } from "fs"; +import { join, dirname } from "path"; +import { fileURLToPath } from "url"; setDefaultTimeout(60_000); +const FIXTURES = join(dirname(fileURLToPath(import.meta.url)), "../../../test-fixtures/quicktx-intents"); + +// The fixed test account the quicktx-intents fixtures are derived from (account 0/0). A Plutus +// fixture bakes this address in as the fee payer, so submitting it means funding and signing with +// this exact account rather than a freshly-created one. +const INTENT_MNEMONIC = "test walk nut penalty hip pave soap entry language right filter choice"; +const INTENT_SENDER = "addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp"; +// The enterprise address the mint fixtures pay the freshly minted asset to. +const MINT_RECEIVER = "addr_test1vz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzerspjrlsz"; + function paymentYaml(from, to, quantity) { return ` version: 1.0 @@ -137,4 +150,50 @@ transaction: const yaml = paymentYaml(sender.base_address, receiver.base_address, "100000000"); expect(() => bridge.quicktx.build(yaml, utxos, pp)).toThrow(); }); + + // Plutus round-trip: build the script_minting fixture with caller-supplied exec units, sign with + // the fee payer's payment key, submit, and assert the minted asset landed on-chain. The Go suite + // does this successfully; the JS path was rejected with PPViewHashesDontMatch (the script-data / + // cost-model hash in the built tx didn't match the node). This run instruments the cost-model + // shape and tries dropping the fetched cost models so the lib uses its built-in standard Conway + // ones (which should match the devnet node). + it("should build, sign, and submit a Plutus mint", async () => { + if (skip) return; + + await devkit.reset(); + await devkit.waitForBlock(3000); + await devkit.topup(INTENT_SENDER, 6000); + await devkit.waitForBlock(3000); + + const utxos = await devkit.getUtxos(INTENT_SENDER); + const pp = await devkit.getProtocolParams(); + const yaml = readFileSync(join(FIXTURES, "plutus", "script_minting.yaml"), "utf8"); + const execUnits = [{ mem: 2000000, steps: 500000000 }]; + + // DIAGNOSTIC: what cost-model shape does /epochs/parameters return, and under which key? + console.log("DIAG pp keys:", Object.keys(pp).sort().join(",")); + for (const k of ["cost_models", "costModels", "cost_mdls", "costMdls", "plutus_v2", "plutusV2"]) { + if (pp[k] !== undefined) console.log(`DIAG ${k} =`, JSON.stringify(pp[k]).slice(0, 1800)); + } + + // CANDIDATE FIX: drop the fetched cost models so the native lib falls back to its built-in + // (standard Conway) cost models, sidestepping any params round-trip that corrupts the script + // integrity hash. If the devnet uses standard cost models, the node will accept this. + const ppNoCm = { ...pp }; + for (const k of ["cost_models", "costModels", "cost_mdls", "costMdls"]) delete ppNoCm[k]; + + const built = bridge.quicktx.build(yaml, utxos, ppNoCm, execUnits); + expect(built.tx_hash.length).toBe(64); + + const signedTx = bridge.account.signTxWithKeys(INTENT_MNEMONIC, TESTNET, 0, 0, built.tx_cbor, ["payment"]); + const submitResult = await devkit.submitTx(signedTx); + console.log("DIAG no-cost-models submit:", String(submitResult).slice(0, 320)); + // A successful submit returns the 64-char tx hash; a rejection returns an error body. + expect(submitResult).toMatch(/^[0-9a-f]{64}$/); + + await devkit.waitForBlock(3000); + const receiverUtxos = await devkit.getUtxos(MINT_RECEIVER); + const hasMintedAsset = receiverUtxos.some((u) => u.amount.some((a) => a.unit !== "lovelace")); + expect(hasMintedAsset).toBe(true); + }); }); From 4055aa90383719ae88ef2fe70058053c994cd2c6 Mon Sep 17 00:00:00 2001 From: Mateusz Czeladka Date: Fri, 26 Jun 2026 16:17:00 +0200 Subject: [PATCH 5/5] test(js): finalize Plutus-mint DevKit round-trip; remove diagnostics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The diagnostic run confirmed the root cause: DevKit's /epochs/parameters returns cost models as a map keyed by zero-padded indices, and JS's JSON parse reorders the non-padded integer-like keys ahead of the padded ones, scrambling cost-model order and producing PPViewHashesDontMatch on submit. Build without the fetched cost models so the lib uses its built-in standard Conway set (which the devnet runs); the submit is accepted and the minted asset lands on-chain. Document the underlying JS cost-model key-ordering bug as a P1 follow-up (it will bite the §2c provider helpers, which fetch and pass these params). Co-Authored-By: Claude Opus 4.8 (1M context) --- TODO.md | 3 +- wrappers/js/test/quicktx.integration.test.js | 42 ++++++++++---------- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/TODO.md b/TODO.md index 795999a..efebb95 100644 --- a/TODO.md +++ b/TODO.md @@ -116,7 +116,8 @@ untouched and the helpers are optional and swappable. This is the sibling of §2 ## 3. Testing -- [x] `P1` ~~Add JS integration tests for the script/Plutus paths.~~ **Done (and the item's premise was superseded by the TxPlan refactor):** the old fluent `ScriptTxBuilder` / `collectFromScript` / `mintPlutusAssets` / `readFrom` API was deleted — script/Plutus paths are now TxPlan YAML fixtures, covered at the build level in `test/intents.e2e.test.js`: all 20 top-level intents (incl. `reference_input`, `compose`, `native_script`) plus the three `plutus/` fixtures — **mint**, **spend**, and **lock** — each asserting non-empty CBOR + 64-char hash + positive fee, that mint/spend **require** caller-supplied exec units (build throws without them), and that `plutus.dataHash` reproduces the lock fixture's datum hash. Node-level Plutus *submit* acceptance stays with the **Go** suite per the cross-wrapper-submit decision (only Go submits the intent set to DevKit; the others have build E2E parity). _Follow-up `P2`: a JS Plutus-mint DevKit round-trip is blocked on a cost-model script-data-hash subtlety — the node rejects the submit with `PPViewHashesDontMatch` because the cost models marshalled from `/epochs/parameters` don't reproduce the node's script-integrity hash; needs investigation against a live DevKit (likely a protocol-params/cost-model serialization difference vs the Go path)._ +- [x] `P1` ~~Add JS integration tests for the script/Plutus paths.~~ **Done (and the item's premise was superseded by the TxPlan refactor):** the old fluent `ScriptTxBuilder` / `collectFromScript` / `mintPlutusAssets` / `readFrom` API was deleted — script/Plutus paths are now TxPlan YAML fixtures, covered at the build level in `test/intents.e2e.test.js`: all 20 top-level intents (incl. `reference_input`, `compose`, `native_script`) plus the three `plutus/` fixtures — **mint**, **spend**, and **lock** — each asserting non-empty CBOR + 64-char hash + positive fee, that mint/spend **require** caller-supplied exec units (build throws without them), and that `plutus.dataHash` reproduces the lock fixture's datum hash. Node-level (DevKit): a Plutus-mint **build → sign → submit → assert the minted asset landed on-chain** round-trip in `test/quicktx.integration.test.js`, mirroring Go's `TestIntegrationPlutusMint`. +- [ ] `P1` **Fix JS cost-model key ordering for Plutus builds.** Surfaced while adding the JS Plutus-mint submit test: passing the cost models fetched from a Blockfrost-style provider (`/epochs/parameters` returns them as a map keyed by zero-padded indices `"000".."165"`) into a Plutus `build()` yields a tx the node rejects with `PPViewHashesDontMatch` — JS's JSON parse reorders the non-padded integer-like keys (`"100".."165"`) ahead of the padded ones, scrambling the cost-model order vs the ledger's canonical order and corrupting the script-integrity hash. (Go's `json.Marshal` sorts keys lexicographically, which for zero-padded keys equals numeric order, so the Go path is unaffected.) The integration test works around it by dropping the fetched cost models so the lib uses its built-in standard set; the real fix is to preserve canonical cost-model order in the JS wrapper before handing params to the native lib. Likely affects the §2c provider helpers, which will fetch and pass these params. - [ ] `P1` Raise Go and Rust test breadth toward Python's (~100 cases vs ~61); port Python's per-module unit tests. - [ ] `P1` Add a cross-wrapper parity test matrix asserting every `@CEntryPoint` is exercised in every language. - [ ] `P2` Run the Yaci DevKit integration tests in CI (containerized DevKit) instead of skip-if-not-running. diff --git a/wrappers/js/test/quicktx.integration.test.js b/wrappers/js/test/quicktx.integration.test.js index 5ca8522..3a7d5aa 100644 --- a/wrappers/js/test/quicktx.integration.test.js +++ b/wrappers/js/test/quicktx.integration.test.js @@ -152,11 +152,19 @@ transaction: }); // Plutus round-trip: build the script_minting fixture with caller-supplied exec units, sign with - // the fee payer's payment key, submit, and assert the minted asset landed on-chain. The Go suite - // does this successfully; the JS path was rejected with PPViewHashesDontMatch (the script-data / - // cost-model hash in the built tx didn't match the node). This run instruments the cost-model - // shape and tries dropping the fetched cost models so the lib uses its built-in standard Conway - // ones (which should match the devnet node). + // the fee payer's payment key, submit, and assert the minted asset landed on-chain. "Submit + // accepted" alone doesn't prove the script ran and minted — the receiver holding a non-lovelace + // asset does. Mirrors the Go TestIntegrationPlutusMint. + // + // We build WITHOUT the devnet's fetched cost models, so the native lib uses its built-in standard + // Conway cost models (which the devnet runs). Passing DevKit's fetched cost models instead is + // rejected with PPViewHashesDontMatch: /epochs/parameters returns them as a map keyed by + // zero-padded indices ("000".."165"), and JS's JSON parse reorders the non-padded integer-like + // keys ("100".."165") ahead of the padded ones, scrambling the cost-model order vs the ledger's + // canonical order and corrupting the script-integrity hash. Go's lexicographic map marshalling + // preserves the order, which is why its equivalent test passes with the fetched params. Threading + // fetched cost models through to Plutus builds needs an order-preserving fix in the wrapper — + // tracked as a follow-up in TODO.md §3. it("should build, sign, and submit a Plutus mint", async () => { if (skip) return; @@ -168,27 +176,17 @@ transaction: const utxos = await devkit.getUtxos(INTENT_SENDER); const pp = await devkit.getProtocolParams(); const yaml = readFileSync(join(FIXTURES, "plutus", "script_minting.yaml"), "utf8"); - const execUnits = [{ mem: 2000000, steps: 500000000 }]; - // DIAGNOSTIC: what cost-model shape does /epochs/parameters return, and under which key? - console.log("DIAG pp keys:", Object.keys(pp).sort().join(",")); - for (const k of ["cost_models", "costModels", "cost_mdls", "costMdls", "plutus_v2", "plutusV2"]) { - if (pp[k] !== undefined) console.log(`DIAG ${k} =`, JSON.stringify(pp[k]).slice(0, 1800)); - } - - // CANDIDATE FIX: drop the fetched cost models so the native lib falls back to its built-in - // (standard Conway) cost models, sidestepping any params round-trip that corrupts the script - // integrity hash. If the devnet uses standard cost models, the node will accept this. - const ppNoCm = { ...pp }; - for (const k of ["cost_models", "costModels", "cost_mdls", "costMdls"]) delete ppNoCm[k]; + const ppForBuild = { ...pp }; + for (const k of ["cost_models", "costModels", "cost_mdls", "costMdls"]) delete ppForBuild[k]; - const built = bridge.quicktx.build(yaml, utxos, ppNoCm, execUnits); - expect(built.tx_hash.length).toBe(64); + const result = bridge.quicktx.build(yaml, utxos, ppForBuild, [{ mem: 2000000, steps: 500000000 }]); + expect(result.tx_hash.length).toBe(64); - const signedTx = bridge.account.signTxWithKeys(INTENT_MNEMONIC, TESTNET, 0, 0, built.tx_cbor, ["payment"]); + const signedTx = bridge.account.signTxWithKeys(INTENT_MNEMONIC, TESTNET, 0, 0, result.tx_cbor, ["payment"]); + // A successful submit returns the 64-char tx hash; a rejection returns an error body. Assert the + // hash so a failed Plutus validation surfaces here, not as a missing asset further down. const submitResult = await devkit.submitTx(signedTx); - console.log("DIAG no-cost-models submit:", String(submitResult).slice(0, 320)); - // A successful submit returns the 64-char tx hash; a rejection returns an error body. expect(submitResult).toMatch(/^[0-9a-f]{64}$/); await devkit.waitForBlock(3000);