mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-25 06:01:11 +00:00
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).
This commit is contained in:
@@ -597,6 +597,7 @@ AmmActionCard {
|
||||
}
|
||||
|
||||
AmmPrimaryButton {
|
||||
objectName: "newPositionSubmitButton"
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: 56
|
||||
theme: root.theme
|
||||
|
||||
@@ -262,6 +262,7 @@ Item {
|
||||
TransactionConfirmationDialog {
|
||||
id: confirmationDialog
|
||||
|
||||
objectName: "liquidityConfirmDialog"
|
||||
title: qsTr("Confirm new position")
|
||||
confirmText: qsTr("Submit")
|
||||
busy: newPositionFlow.submitting
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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" <<JSON
|
||||
"definitionId": "$TOKEN_B_DEF",
|
||||
"holding": "$TOKEN_B_HOLDING",
|
||||
"decimals": $TOKEN_B_DECIMALS
|
||||
},
|
||||
{
|
||||
"symbol": "$TOKEN_C_SYMBOL",
|
||||
"name": "$TOKEN_C_NAME",
|
||||
"definitionId": "$TOKEN_C_DEF",
|
||||
"holding": "$TOKEN_C_HOLDING",
|
||||
"decimals": $TOKEN_C_DECIMALS
|
||||
}
|
||||
]
|
||||
JSON
|
||||
@@ -394,4 +434,5 @@ log " ${DIM}LEE_WALLET_HOME_DIR=$TEST_WALLET_HOME \\${RST}"
|
||||
log " ${DIM} AMM_PROGRAM_BIN=$REPO_ROOT/$AMM_BIN \\${RST}"
|
||||
log " ${DIM} TOKENS_CONFIG=$REPO_ROOT/$TOKENS_CONFIG_OUT \\${RST}"
|
||||
log " ${DIM} nix run .#amm-ui${RST}"
|
||||
log "Then in another terminal: ${DIM}node apps/amm/tests/swap.mjs${RST}"
|
||||
log "Then in another terminal: ${DIM}node apps/amm/tests/swap.mjs${RST} (swap A/B)"
|
||||
log " or: ${DIM}node apps/amm/tests/create-pool.mjs${RST} (create A/C pool)"
|
||||
|
||||
Reference in New Issue
Block a user