From 19c54e3b3e24742ba1f94926d645eb7fada454ee Mon Sep 17 00:00:00 2001 From: r4bbit <445106+0x-r4bbit@users.noreply.github.com> Date: Fri, 7 Aug 2026 15:37:06 +0200 Subject: [PATCH] test(apps/amm): add create-pool UI e2e test + Token C setup - setup-amm-testnet.sh: mint a third token (TKC), appended to ACCOUNT_LABELS so the deterministic a/b/lp ids don't shift and left unseeded (only A/B is created) for the test to create the A/C pair. Split wallet restore from account registration (ensure_accounts always runs) so adding a token needs no re-restore. - create-pool.mjs: drives the Liquidity view to create the A/C pool, then verifies it on-chain via the swap card's resolvePool. evaluate() fallbacks for the submit/confirm buttons (synthetic clicks aren't reliable on QtQuick Buttons). - Adds test-hook objectNames (newPositionSubmitButton, liquidityConfirmDialog). --- .../components/liquidity/NewPositionForm.qml | 1 + apps/amm/qml/pages/LiquidityPage.qml | 1 + apps/amm/tests/README.md | 25 +- apps/amm/tests/create-pool.mjs | 273 ++++++++++++++++++ apps/amm/tests/testnet/setup-amm-testnet.sh | 73 ++++- 5 files changed, 348 insertions(+), 25 deletions(-) create mode 100644 apps/amm/tests/create-pool.mjs diff --git a/apps/amm/qml/components/liquidity/NewPositionForm.qml b/apps/amm/qml/components/liquidity/NewPositionForm.qml index 624f309..a8b15d3 100644 --- a/apps/amm/qml/components/liquidity/NewPositionForm.qml +++ b/apps/amm/qml/components/liquidity/NewPositionForm.qml @@ -597,6 +597,7 @@ AmmActionCard { } AmmPrimaryButton { + objectName: "newPositionSubmitButton" Layout.fillWidth: true Layout.minimumHeight: 56 theme: root.theme diff --git a/apps/amm/qml/pages/LiquidityPage.qml b/apps/amm/qml/pages/LiquidityPage.qml index 9a3cc1b..49ccc17 100644 --- a/apps/amm/qml/pages/LiquidityPage.qml +++ b/apps/amm/qml/pages/LiquidityPage.qml @@ -262,6 +262,7 @@ Item { TransactionConfirmationDialog { id: confirmationDialog + objectName: "liquidityConfirmDialog" title: qsTr("Confirm new position") confirmText: qsTr("Submit") busy: newPositionFlow.submitting diff --git a/apps/amm/tests/README.md b/apps/amm/tests/README.md index b316c6b..5ea2079 100644 --- a/apps/amm/tests/README.md +++ b/apps/amm/tests/README.md @@ -1,10 +1,14 @@ # AMM UI tests -UI-driven tests for the AMM app. `swap.mjs` drives the running app through the -QML inspector (framework from -[`logos-co/logos-qt-mcp`](https://github.com/logos-co/logos-qt-mcp)): it selects -two tokens, enters an amount, submits a swap, and verifies the pool reserves -changed **on-chain**. +UI-driven tests for the AMM app, driving the running app through the QML +inspector (framework from +[`logos-co/logos-qt-mcp`](https://github.com/logos-co/logos-qt-mcp)): + +- `swap.mjs` selects two tokens, enters an amount, submits a swap, and verifies + the **A/B** pool reserves changed **on-chain**. +- `create-pool.mjs` selects the **A/C** pair (which the setup script leaves + unseeded — only A/B is created), submits a pool creation, and verifies the A/C + pool now exists **on-chain**. ## Isolation @@ -40,8 +44,9 @@ LEE_WALLET_HOME_DIR=$(pwd)/apps/amm/tests/testnet/.wallet \ TOKENS_CONFIG=$(pwd)/apps/amm/tests/testnet/amm-tokens.json \ nix run .#amm-ui -# 3. Terminal 2 — drive the swap test; watch it click through the live UI. -node apps/amm/tests/swap.mjs +# 3. Terminal 2 — drive a test; watch it click through the live UI. +node apps/amm/tests/swap.mjs # swap against the seeded A/B pool +node apps/amm/tests/create-pool.mjs # create the (unseeded) A/C pool ``` Headless CI variant (no window, launches the app itself, pass/fail only): @@ -71,6 +76,8 @@ nix build .#integration-test -L ## Files -- `swap.mjs` — the end-to-end swap UI test. -- `testnet/setup-amm-testnet.sh` — isolated testnet + wallet bootstrap. +- `swap.mjs` — the end-to-end swap UI test (A/B pool). +- `create-pool.mjs` — the end-to-end create-pool UI test (creates the A/C pool). +- `testnet/setup-amm-testnet.sh` — isolated testnet + wallet bootstrap (TKA/TKB/TKC, + seeds the A/B pool only). - `qml/`, `cpp/` — the module's own QML/C++ unit tests. diff --git a/apps/amm/tests/create-pool.mjs b/apps/amm/tests/create-pool.mjs new file mode 100644 index 0000000..2d8c5af --- /dev/null +++ b/apps/amm/tests/create-pool.mjs @@ -0,0 +1,273 @@ +// --------------------------------------------------------------------------- +// AMM UI test — create a NEW pool (A/C) through the Liquidity view. +// +// Drives the running AMM UI through the QML inspector (logos-qt-mcp), the same +// way swap.mjs does. It selects the A/C pair (which the setup script leaves +// UNSEEDED — only A/B is created), lets the form auto-fill the minimum opening +// deposit, submits the create, and verifies the pool now exists ON-CHAIN. +// +// Prereqs in the running app (see apps/amm/tests/README.md): +// * launched against the isolated test wallet + TOKENS_CONFIG that +// testnet/setup-amm-testnet.sh writes (TKA, TKB, TKC) +// * an open wallet + reachable local sequencer +// * the A/C pool must NOT exist yet (setup only seeds A/B) +// --------------------------------------------------------------------------- + +import { resolve } from "node:path"; +import { readFile, writeFile } from "node:fs/promises"; + +const fwRoot = + process.env.LOGOS_QT_MCP || + new URL("../result-mcp", import.meta.url).pathname; +const { test, run } = await import(resolve(fwRoot, "test-framework/framework.mjs")); + +// The token config the app was launched with (same file the setup script writes). +const TOKENS_CONFIG = + process.env.TOKENS_CONFIG || + new URL("./testnet/amm-tokens.json", import.meta.url).pathname; + +// --- small helpers (mirrors swap.mjs) -------------------------------------- + +const ignore = async (fn) => { try { return await fn(); } catch { /* best effort */ } }; + +async function idByObjectName(app, name) { + const res = await app.findByProperty("objectName", name); + if (res.error || !res.matches || res.matches.length === 0) + throw new Error(`no object with objectName="${name}" (is the app on the Liquidity tab?)`); + return res.matches[0].id; +} + +async function prop(app, id, name) { + const props = (await app.getProperties(id)).properties || []; + const p = props.find((x) => x.name === name); + return p ? p.value : undefined; +} + +async function setProp(app, id, property, value) { + await app.inspector.send("setProperty", { objectId: id, property, value }); +} + +async function evaluate(app, id, expression) { + await app.inspector.send("evaluate", { expression, objectId: id }); +} + +// The NewPositionForm's create/pool state — explains WHY the CTA isn't ready. +async function formState(app, formId) { + const props = (await app.getProperties(formId)).properties || []; + const get = (n) => { const p = props.find((x) => x.name === n); return p ? p.value : undefined; }; + return { + poolStatus: get("poolStatus"), + missingPool: get("missingPool"), + canConfirm: get("canConfirm"), + amountA: get("amountA"), + amountB: get("amountB"), + submitError: get("submitError"), + transactionId: get("transactionId"), + }; +} + +async function saveShot(app, name) { + const shot = await ignore(() => app.screenshot()); + if (shot && shot.image) { + const path = new URL(`./${name}.png`, import.meta.url).pathname; + await writeFile(path, Buffer.from(shot.image, "base64")); + console.log(` screenshot -> ${path}`); + } +} + +// --- Swap-card token picker helpers (shared shape with swap.mjs), used only to +// read the A/C pool's on-chain state for the final assertion. ------------ + +async function pickerOpen(app) { + const id = await idByObjectName(app, "tokenSelectorModal"); + return (await prop(app, id, "visible")) === true; +} + +async function openPicker(app, buttonObjectName) { + const btnId = await idByObjectName(app, buttonObjectName); + await app.inspector.send("click", { objectId: btnId }); + await app.waitFor( + async () => { if (!(await pickerOpen(app))) throw new Error("picker not open"); }, + { timeout: 5000, interval: 200, description: `open ${buttonObjectName}` }, + ); +} + +async function pickToken(app, index) { + const res = await app.findByProperty("objectName", "tokenListItem"); + const items = (res && res.matches) || []; + if (items.length <= index) + throw new Error(`token #${index + 1} not found — only ${items.length} in the list`); + await app.inspector.send("click", { objectId: items[index].id }); + await app.waitFor( + async () => { if (await pickerOpen(app)) throw new Error("picker still open"); }, + { timeout: 5000, interval: 200, description: `select token #${index + 1}` }, + ); +} + +// --- the test --------------------------------------------------------------- + +test("amm liquidity: create the A/C pool", async (app) => { + // Resolve the A and C token-definition ids from the launched token config. + const tokens = JSON.parse(await readFile(TOKENS_CONFIG, "utf8")); + const bySymbol = (s) => { + const t = tokens.find((x) => (x.symbol || "").toUpperCase() === s); + if (!t) throw new Error(`token ${s} not in ${TOKENS_CONFIG} — run the setup script`); + return t.definitionId; + }; + const tokenA = bySymbol("TKA"); + const tokenC = bySymbol("TKC"); + console.log(` create pool A(${tokenA.slice(0, 6)}…) / C(${tokenC.slice(0, 6)}…)`); + + // 1. Switch to the Liquidity tab and wait for the form to render. + await app.waitFor( + async () => { await app.expectTexts(["Trade", "Liquidity"]); }, + { timeout: 20000, interval: 500, description: "nav bar to load" }, + ); + await ignore(() => app.click("Liquidity")); + // waitFor resolves when the condition stops throwing — it does NOT return the + // callback's value, so fetch the id with a direct call afterwards. + await app.waitFor( + async () => { await idByObjectName(app, "newPositionForm"); }, + { timeout: 10000, interval: 300, description: "liquidity form to render" }, + ); + const formId = await idByObjectName(app, "newPositionForm"); + + // 2. Select the A/C pair through selectToken() — the same entry point the token picker + // uses — so the form runs its normal selection logic and resetPairDraft(), clearing any + // persisted amounts/price/minimums from a reused app session and firing a fresh quote. + // Setting selectedToken*Id directly would bypass that reset and could assert against + // stale draft state. Both tokens are already in the config, so no async token resolution + // is needed. The first missing-pool quote returns the minimum opening deposit, which + // applyQuoteSideEffects auto-fills — so canConfirm becomes ready. + await evaluate(app, formId, `selectToken("A", "${tokenA}")`); + await evaluate(app, formId, `selectToken("B", "${tokenC}")`); + + // Sanity: the selection must stick — the ids have to be present in the token + // config the app was launched with, or tokenById can't resolve them. + await app.waitFor( + async () => { + const a = await prop(app, formId, "selectedTokenAId"); + const b = await prop(app, formId, "selectedTokenBId"); + if (!a || !b) throw new Error(`pair not selected (A=${a} B=${b})`); + }, + { timeout: 5000, interval: 300, description: "A/C pair selected" }, + ); + await evaluate(app, formId, "requestQuote(true)"); + + // 3. Wait for a submittable create quote (missing pool + funded minimum deposit). + try { + await app.waitFor( + async () => { + const s = await formState(app, formId); + if (s.poolStatus === "active_pool") + throw new Error("A/C pool already exists — reset the testnet (only A/B should be seeded)"); + if (!s.canConfirm) throw new Error("create CTA not ready yet"); + }, + { timeout: 20000, interval: 500, description: "create CTA ready" }, + ); + } catch (e) { + await saveShot(app, "create-pool-cta-not-ready"); + throw new Error(`${e.message}. Form state: ${JSON.stringify(await formState(app, formId))}`); + } + await saveShot(app, "create-pool-filled"); + console.log(` minimum deposit: A=${(await formState(app, formId)).amountA} C=${(await formState(app, formId)).amountB}`); + + // 4. Submit -> confirmation dialog -> confirm. + const dialogId = await idByObjectName(app, "liquidityConfirmDialog"); + const submitId = await idByObjectName(app, "newPositionSubmitButton"); + await app.inspector.send("click", { objectId: submitId }); + + // QtQuick Controls Buttons don't reliably take the inspector's synthetic click, + // so if the dialog didn't open, emit the form's confirmationRequested signal + // directly (exactly what the button's onClicked does). + try { + await app.waitFor( + async () => { if ((await prop(app, dialogId, "visible")) !== true) throw new Error("not open"); }, + { timeout: 4000, interval: 300, description: "confirm dialog open" }, + ); + } catch { + console.log(" submit click didn't take — emitting confirmationRequested via evaluate"); + await ignore(() => evaluate(app, formId, "confirmationRequested(submissionSnapshot())")); + await app.waitFor( + async () => { if ((await prop(app, dialogId, "visible")) !== true) throw new Error("dialog not open"); }, + { timeout: 8000, interval: 300, description: "confirm dialog open (after evaluate)" }, + ); + } + + const confirmId = await idByObjectName(app, "transactionConfirmButton"); + await app.inspector.send("click", { objectId: confirmId }); + // QtQuick Buttons don't always take the synthetic click; fall back to invoking + // the dialog's confirm() slot directly (fires onConfirmed -> flow.confirm()). + try { + await app.waitFor( + async () => { if ((await prop(app, dialogId, "visible")) === true) throw new Error("still open"); }, + { timeout: 3000, interval: 300, description: "confirm click registered" }, + ); + } catch { + console.log(" confirm button click didn't take — invoking confirm() via evaluate"); + await ignore(() => evaluate(app, dialogId, "confirm()")); + } + + // 5. Wait for the create to submit. It's fully async — createAccountPublic (mint + // the LP holding) then createPool then the tx submit — so transactionId lands + // a few seconds after confirm(), not immediately. + try { + await app.waitFor( + async () => { + const s = await formState(app, formId); + if (!s.transactionId) throw new Error("create not submitted yet"); + }, + { timeout: 30000, interval: 1000, description: "create to submit (transactionId set)" }, + ); + } catch { + await saveShot(app, "create-pool-result"); + throw new Error(`create did not submit. Form state: ${JSON.stringify(await formState(app, formId))}`); + } + const final = await formState(app, formId); + console.log(` create submitted: tx ${final.transactionId}`); + + // 6. Verify ON-CHAIN via the Swap card's resolvePool — a plain hex pool read + // against the sequencer (createPool has no tx poll / poolStatus, so we don't + // lean on the liquidity form's legacy quote). Selecting A/C on the Trade tab + // must now report the pool as existing with reserves. + await ignore(() => app.click("Trade")); + await app.waitFor( + async () => { await app.expectTexts(["Sell", "Buy"]); }, + { timeout: 10000, interval: 500, description: "swap card to load" }, + ); + await openPicker(app, "swapSellTokenButton"); + await pickToken(app, 0); // TKA + await openPicker(app, "swapBuyTokenButton"); + await pickToken(app, 2); // TKC + const swapId = await idByObjectName(app, "swapCard"); + + try { + await app.waitFor( + async () => { + // Force a fresh read each poll — the create block may not be applied yet. + await ignore(() => evaluate(app, swapId, "doResolvePool()")); + await new Promise((r) => setTimeout(r, 800)); + if ((await prop(app, swapId, "poolExists")) !== true) + throw new Error("A/C pool not found yet"); + }, + { timeout: 40000, interval: 1500, description: "A/C pool to exist on-chain" }, + ); + } catch { + await saveShot(app, "create-pool-result"); + throw new Error( + `A/C pool not found on-chain after the create (tx ${final.transactionId}).\n` + + ` swap card: poolExists=${await prop(app, swapId, "poolExists")} ` + + `reserveA=${await prop(app, swapId, "poolReserveA")} ` + + `reserveB=${await prop(app, swapId, "poolReserveB")}`, + ); + } + const rA = await prop(app, swapId, "poolReserveA"); + const rB = await prop(app, swapId, "poolReserveB"); + console.log(` A/C pool exists on-chain ✓ reserves A=${rA} B=${rB} (tx ${final.transactionId})`); + await saveShot(app, "create-pool-result"); +}); + +run(); + +// How to run (from scratch, interactive + CI): see the "Running the UI tests" +// section in apps/amm/tests/README.md — same flow as swap.mjs. diff --git a/apps/amm/tests/testnet/setup-amm-testnet.sh b/apps/amm/tests/testnet/setup-amm-testnet.sh index 65155a5..a1e23fa 100755 --- a/apps/amm/tests/testnet/setup-amm-testnet.sh +++ b/apps/amm/tests/testnet/setup-amm-testnet.sh @@ -2,11 +2,12 @@ # # setup-amm-testnet.sh # -------------------- -# Deploy the token/amm/twap programs, mint two fungible tokens, initialize the -# AMM, and create a pool — from scratch — against whatever sequencer your -# `wallet` / `spel` config points at. This is the prerequisite state that the -# AMM UI swap test (apps/amm/tests/swap.mjs) exercises; run it once to stand up -# a swappable pool, then launch the UI / run the test. +# Deploy the token/amm/twap programs, mint three fungible tokens, initialize the +# AMM, and create the A/B pool — from scratch — against whatever sequencer your +# `wallet` / `spel` config points at. This is the prerequisite state the AMM UI +# tests exercise: swap.mjs swaps against the seeded A/B pool, and create-pool.mjs +# creates the (deliberately unseeded) A/C pool. Run it once, then launch the UI / +# run the tests. # # DETERMINISTIC TEST WALLET: by default the script bootstraps an ISOLATED wallet # (git-ignored, under this folder) by restoring it from a fixed BIP-39 mnemonic @@ -63,7 +64,10 @@ TEST_SEQUENCER_ADDR="${TEST_SEQUENCER_ADDR:-}" # Deterministic accounts, created in THIS fixed order after a fresh restore so # their ids are reproducible. Resolved to ids at runtime via `wallet account id`. -ACCOUNT_LABELS=(token-a-def token-a-holding token-b-def token-b-holding lp-holding) +# token-c-* are APPENDED (not inserted) so the pre-existing a/b/lp ids don't shift. +# Token C has no seeded pool — the create-pool UI test (apps/amm/tests/create-pool.mjs) +# creates the A/C pool itself, minting its own LP holding via the app. +ACCOUNT_LABELS=(token-a-def token-a-holding token-b-def token-b-holding lp-holding token-c-def token-c-holding) ############################################################################### # CONFIG — non-account parameters (edit freely) @@ -81,6 +85,7 @@ AMM_IDL="artifacts/amm-idl.json" # --- Token metadata --- TOKEN_A_NAME="TOKEN A"; TOKEN_A_SYMBOL="TKA"; TOKEN_A_SUPPLY="1000000000000000000000"; TOKEN_A_DECIMALS=18 TOKEN_B_NAME="TOKEN B"; TOKEN_B_SYMBOL="TKB"; TOKEN_B_SUPPLY="1000000000000000000000"; TOKEN_B_DECIMALS=18 +TOKEN_C_NAME="TOKEN C"; TOKEN_C_SYMBOL="TKC"; TOKEN_C_SUPPLY="1000000000000000000000"; TOKEN_C_DECIMALS=18 # --- Pool inputs --- CLOCK_ACCOUNT="4BdcjoXkq786TMWcBGGHqcxeLYMZmn17rL4eM9ZyRWNU" # canonical LEZ system clock @@ -172,10 +177,11 @@ acct_id() { printf '%s' "$out" | grep -oE '[1-9A-HJ-NP-Za-km-z]{32,44}' | head -n1 } -# Restore the isolated test wallet from the fixed mnemonic and register the -# deterministic accounts. `restore-keys` REWRITES storage (safe here — it's a -# throwaway test home). Idempotent: skips accounts that already resolve. -bootstrap_test_wallet() { +# Restore the isolated test wallet from the fixed mnemonic. `restore-keys` +# REWRITES storage (safe here — it's a throwaway test home). Only the key +# material is restored here; account registration is a separate idempotent step +# (ensure_accounts) so adding a label doesn't require a full re-restore. +restore_test_wallet() { sec "Bootstrap deterministic test wallet" kv "wallet home" "$TEST_WALLET_HOME" mkdir -p "$TEST_WALLET_HOME" @@ -198,14 +204,24 @@ bootstrap_test_wallet() { printf '%s\n%s\n' "$TEST_MNEMONIC" "$TEST_WALLET_PASSWORD" \ | wallet restore-keys --depth "$TEST_WALLET_DEPTH" \ || die "wallet restore-keys failed" +} +# Register the deterministic accounts, in ACCOUNT_LABELS order. Idempotent — +# skips accounts that already resolve, so newly-appended labels (e.g. token-c-*) +# are created on an existing wallet WITHOUT re-restoring keys. Their ids stay +# deterministic because they're appended after the pre-existing accounts. +ensure_accounts() { + sec "Ensure deterministic test accounts" local label for label in "${ACCOUNT_LABELS[@]}"; do if acct_id "$label" >/dev/null 2>&1; then kv "exists" "$label" else log "${DIM}\$ wallet account new public --label $label${RST}" - wallet account new public --label "$label" || die "failed to create account: $label" + # Feed the password in case the wallet prompts to unlock before writing. + printf '%s\n' "$TEST_WALLET_PASSWORD" \ + | wallet account new public --label "$label" \ + || die "failed to create account: $label (try FORCE_BOOTSTRAP=1 to re-restore)" fi done } @@ -223,10 +239,13 @@ kv "repo root" "$REPO_ROOT" kv "token bin" "$TOKEN_BIN"; kv "amm bin" "$AMM_BIN"; kv "twap bin" "$TWAP_BIN" if [ ! -d "$TEST_WALLET_HOME" ] || [ "${FORCE_BOOTSTRAP:-0}" = "1" ]; then - bootstrap_test_wallet + restore_test_wallet else - kv "test wallet" "reusing $TEST_WALLET_HOME (FORCE_BOOTSTRAP=1 to re-restore)" + kv "test wallet" "reusing $TEST_WALLET_HOME (FORCE_BOOTSTRAP=1 to re-restore keys)" fi +# Always register accounts — creates any newly-added labels (e.g. token-c-*) on +# an existing wallet without a full key re-restore. +ensure_accounts ############################################################################### # 1. Resolve the deterministic test accounts @@ -237,12 +256,14 @@ TOKEN_A_HOLDING="$(acct_id token-a-holding)" || die "token-a-holding not registe TOKEN_B_DEF="$(acct_id token-b-def)" || die "token-b-def not registered" TOKEN_B_HOLDING="$(acct_id token-b-holding)" || die "token-b-holding not registered" USER_HOLDING_LP="$(acct_id lp-holding)" || die "lp-holding not registered" -for v in TOKEN_A_DEF TOKEN_A_HOLDING TOKEN_B_DEF TOKEN_B_HOLDING USER_HOLDING_LP; do +TOKEN_C_DEF="$(acct_id token-c-def)" || die "token-c-def not registered" +TOKEN_C_HOLDING="$(acct_id token-c-holding)" || die "token-c-holding not registered" +for v in TOKEN_A_DEF TOKEN_A_HOLDING TOKEN_B_DEF TOKEN_B_HOLDING USER_HOLDING_LP TOKEN_C_DEF TOKEN_C_HOLDING; do [ -n "${!v}" ] || die "failed to resolve account id for $v" done # Derived roles (the input holding signs; mint authority == holding; authority is the A holding). -TOKEN_A_MINT_AUTH="$TOKEN_A_HOLDING"; TOKEN_B_MINT_AUTH="$TOKEN_B_HOLDING" +TOKEN_A_MINT_AUTH="$TOKEN_A_HOLDING"; TOKEN_B_MINT_AUTH="$TOKEN_B_HOLDING"; TOKEN_C_MINT_AUTH="$TOKEN_C_HOLDING" AMM_AUTHORITY="$TOKEN_A_HOLDING" USER_HOLDING_A="$TOKEN_A_HOLDING"; USER_HOLDING_B="$TOKEN_B_HOLDING" @@ -251,6 +272,8 @@ kv "token-a-holding" "$TOKEN_A_HOLDING" kv "token-b-def" "$TOKEN_B_DEF" kv "token-b-holding" "$TOKEN_B_HOLDING" kv "lp-holding" "$USER_HOLDING_LP" +kv "token-c-def" "$TOKEN_C_DEF" +kv "token-c-holding" "$TOKEN_C_HOLDING" ############################################################################### # 2. Deploy programs @@ -284,6 +307,14 @@ run_tx strict "create fungible definition: $TOKEN_B_NAME" -- \ --holding-target-account "$TOKEN_B_HOLDING" \ --mint-authority "$TOKEN_B_MINT_AUTH" +# Token C has no seeded pool — the create-pool UI test creates the A/C pool. +run_tx strict "create fungible definition: $TOKEN_C_NAME" -- \ + spel --idl "$TOKEN_IDL" --program "$TOKEN_BIN" -- new-fungible-definition \ + --name "$TOKEN_C_NAME" --total-supply "$TOKEN_C_SUPPLY" \ + --definition-target-account "$TOKEN_C_DEF" \ + --holding-target-account "$TOKEN_C_HOLDING" \ + --mint-authority "$TOKEN_C_MINT_AUTH" + ############################################################################### # 5. Verify token definitions & holdings ############################################################################### @@ -291,6 +322,8 @@ inspect "$TOKEN_IDL" "$TOKEN_A_DEF" "TokenDefinition" inspect "$TOKEN_IDL" "$TOKEN_A_HOLDING" "TokenHolding" inspect "$TOKEN_IDL" "$TOKEN_B_DEF" "TokenDefinition" inspect "$TOKEN_IDL" "$TOKEN_B_HOLDING" "TokenHolding" +inspect "$TOKEN_IDL" "$TOKEN_C_DEF" "TokenDefinition" +inspect "$TOKEN_IDL" "$TOKEN_C_HOLDING" "TokenHolding" ############################################################################### # 6. Derive AMM PDAs from the program ids + token pair @@ -378,6 +411,13 @@ cat > "$TOKENS_CONFIG_OUT" <