diff --git a/TODO.md b/TODO.md index 3d8835c..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 -- [ ] `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, 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/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..3a7d5aa 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,48 @@ 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. "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; + + 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 ppForBuild = { ...pp }; + for (const k of ["cost_models", "costModels", "cost_mdls", "costMdls"]) delete ppForBuild[k]; + + 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, 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); + 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); + }); });