Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
36 changes: 28 additions & 8 deletions wrappers/js/test/intents.e2e.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(); });
Expand All @@ -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();
});

Expand All @@ -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);
});
});
57 changes: 57 additions & 0 deletions wrappers/js/test/quicktx.integration.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
});
});
Loading