mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-25 22:21:16 +00:00
189 lines
8.9 KiB
JavaScript
189 lines
8.9 KiB
JavaScript
// ---------------------------------------------------------------------------
|
|
// AMM UI test — add a CUSTOM (unlisted) token by id through the Liquidity view.
|
|
//
|
|
// Token D is created on-chain by the setup script but deliberately LEFT OUT of
|
|
// the token config, so it never appears in the selector on its own. This test
|
|
// pastes D's definition id into a liquidity token slot and verifies the app
|
|
// RESOLVES it (reads its on-chain definition), SELECTS it, and PERSISTS it to the
|
|
// custom-token store — the addCustomToken path. No pool / submit is involved, so
|
|
// it needs neither an open wallet nor a seeded pool.
|
|
//
|
|
// NOTE: the add path is holding-agnostic — a token resolves from its public
|
|
// definition whether or not the wallet holds it (balance shows "0" when not held).
|
|
// D happens to be held by the test wallet only because minting requires the holding
|
|
// account to sign; a genuinely un-owned token adds via exactly the same path.
|
|
//
|
|
// Prereqs in the running app (see apps/amm/tests/README.md):
|
|
// * launched against the isolated test wallet + TOKENS_CONFIG the setup writes
|
|
// (TKA, TKB, TKC — NOT token D) and CUSTOM_TOKEN_CONFIG pointing at a
|
|
// writable path (defaults below, matching the README launch line)
|
|
// * a reachable local sequencer (to read D's definition)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
import { resolve } from "node:path";
|
|
import { rm } from "node:fs/promises";
|
|
import { execFileSync } from "node:child_process";
|
|
|
|
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 isolated test wallet home (set by the setup script) — used to resolve token
|
|
// D's deterministic definition id via the wallet CLI, the same way the setup does.
|
|
const WALLET_HOME =
|
|
process.env.LEE_WALLET_HOME_DIR ||
|
|
new URL("./testnet/.wallet", import.meta.url).pathname;
|
|
|
|
// Where the app persists custom tokens. Defaults to the isolated test store; set
|
|
// CUSTOM_TOKEN_CONFIG to override. IMPORTANT: launch the app with the SAME path
|
|
// (CUSTOM_TOKEN_CONFIG) so the test's clean-slate clears the store the app actually
|
|
// uses — otherwise the app writes to its default per-user store and the test starts
|
|
// from a stale slate. Only used to pre-clear; persistence is verified through the app.
|
|
const CUSTOM_TOKEN_CONFIG =
|
|
process.env.CUSTOM_TOKEN_CONFIG ||
|
|
new URL("./testnet/custom-tokens.json", import.meta.url).pathname;
|
|
|
|
// --- small helpers (mirror create-pool.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 evaluate(app, id, expression) {
|
|
await app.inspector.send("evaluate", { expression, objectId: id });
|
|
}
|
|
|
|
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 import("node:fs/promises").then(({ writeFile }) =>
|
|
writeFile(path, Buffer.from(shot.image, "base64")));
|
|
console.log(` screenshot -> ${path}`);
|
|
}
|
|
}
|
|
|
|
// Resolve token D's definition id from the test wallet (deterministic under the
|
|
// test mnemonic), the same account label the setup script mints it to.
|
|
function resolveTokenD() {
|
|
const out = execFileSync("wallet", ["account", "id", "--account-id", "token-d-def"], {
|
|
encoding: "utf8",
|
|
env: { ...process.env, LEE_WALLET_HOME_DIR: WALLET_HOME, NSSA_WALLET_HOME_DIR: WALLET_HOME },
|
|
});
|
|
const id = (out.match(/[1-9A-HJ-NP-Za-km-z]{32,44}/) || [])[0];
|
|
if (!id)
|
|
throw new Error(`could not resolve token-d-def id from wallet (home=${WALLET_HOME}) — run the setup script`);
|
|
return id;
|
|
}
|
|
|
|
// Trigger a token-list reload and wait for it to complete. resolveTokens() re-reads the
|
|
// persisted custom-token store from disk (in C++), so a token that survives a reload was
|
|
// genuinely persisted — no need to know the app's store path.
|
|
async function reloadTokens(app, pageId) {
|
|
await evaluate(app, pageId, "refreshTokens()");
|
|
await app.waitFor(
|
|
async () => { if ((await prop(app, pageId, "tokensLoading")) === true) throw new Error("reloading"); },
|
|
{ timeout: 10000, interval: 200, description: "token reload to finish" },
|
|
);
|
|
}
|
|
|
|
async function selectableIds(app, formId) {
|
|
// Read the CSV string form (var arrays don't reliably serialize over the inspector).
|
|
const csv = await prop(app, formId, "selectableTokenIdsCsv");
|
|
return typeof csv === "string" && csv.length > 0 ? csv.split(",") : [];
|
|
}
|
|
|
|
// --- the test ---------------------------------------------------------------
|
|
|
|
test("amm liquidity: add a custom (unlisted) token by id", async (app) => {
|
|
const tokenD = resolveTokenD();
|
|
console.log(` custom token D = ${tokenD}`);
|
|
|
|
// 1. Switch to the Liquidity tab and wait for the form + page to render.
|
|
await app.waitFor(
|
|
async () => { await app.expectTexts(["Trade", "Pool"]); },
|
|
{ timeout: 20000, interval: 500, description: "nav bar to load" },
|
|
);
|
|
// "Liquidity" is now "Pool > Create pool" (tab 2, sub 1); drive the navbar directly.
|
|
const navBarId = await idByObjectName(app, "navBar");
|
|
await evaluate(app, navBarId, "select(2, 1)");
|
|
await app.waitFor(
|
|
async () => { await idByObjectName(app, "newPositionForm"); },
|
|
{ timeout: 10000, interval: 300, description: "liquidity form to render" },
|
|
);
|
|
const formId = await idByObjectName(app, "newPositionForm");
|
|
const pageId = await idByObjectName(app, "liquidityPage");
|
|
|
|
// 2. Clean slate: clear the isolated custom-token store the app uses and reload, so D is
|
|
// genuinely absent and pasting it must go through addCustomToken (not a direct select).
|
|
// If D is still listed after this, the app isn't using this store — launch it with
|
|
// CUSTOM_TOKEN_CONFIG pointing here (see README).
|
|
await rm(CUSTOM_TOKEN_CONFIG, { force: true });
|
|
await reloadTokens(app, pageId);
|
|
if ((await selectableIds(app, formId)).includes(tokenD))
|
|
throw new Error(
|
|
`token D is still listed after clearing ${CUSTOM_TOKEN_CONFIG} — launch the app with ` +
|
|
"CUSTOM_TOKEN_CONFIG set to this same path so the test controls the store (see README).",
|
|
);
|
|
|
|
// 3. Paste D's id into token slot A — the same entry point the token input's
|
|
// onTokenEntered uses. It's unlisted, so resolveToken routes through the app's
|
|
// addCustomToken: resolve the on-chain definition, persist, select.
|
|
await evaluate(app, formId, `resolveToken("A", "${tokenD}")`);
|
|
|
|
// 4. Wait for the resolution to complete: D selected on side A, no resolution error.
|
|
try {
|
|
await app.waitFor(
|
|
async () => {
|
|
const err = await prop(app, formId, "tokenResolutionError");
|
|
if (err) throw new Error(`resolution failed: ${err}`);
|
|
const selected = await prop(app, formId, "selectedTokenAId");
|
|
if (selected !== tokenD) throw new Error(`D not selected yet (selectedTokenAId=${selected})`);
|
|
},
|
|
{ timeout: 20000, interval: 500, description: "token D resolved + selected" },
|
|
);
|
|
} catch (e) {
|
|
await saveShot(app, "custom-token-not-resolved");
|
|
const err = await prop(app, formId, "tokenResolutionError");
|
|
throw new Error(`${e.message}. tokenResolutionError=${err}`);
|
|
}
|
|
|
|
// 5. The persistence proof: reload the list (re-reads the store from disk) and confirm D
|
|
// SURVIVES. D isn't in the token config, so if it's still selectable after a reload it
|
|
// can only have come from the persisted custom-token store — i.e. addCustomToken wrote
|
|
// it. If persistence silently failed, D would vanish here.
|
|
await reloadTokens(app, pageId);
|
|
if (!(await selectableIds(app, formId)).includes(tokenD)) {
|
|
await saveShot(app, "custom-token-not-persisted");
|
|
throw new Error(
|
|
"token D disappeared after a reload — it resolved + selected but was NOT persisted to the " +
|
|
"custom-token store. Launch the app with CUSTOM_TOKEN_CONFIG set to a writable path (see README).",
|
|
);
|
|
}
|
|
|
|
console.log(" token D added as a custom token ✓ (survives a token-list reload)");
|
|
await saveShot(app, "custom-token-added");
|
|
|
|
// Leave no side effects: clear the persisted custom token. The running app keeps it in
|
|
// memory until restart, but the next run's clean-slate step re-reads this cleared store.
|
|
await rm(CUSTOM_TOKEN_CONFIG, { force: true });
|
|
});
|
|
|
|
run();
|
|
|
|
// How to run: see apps/amm/tests/README.md — same flow as create-pool.mjs, plus
|
|
// launch the UI with CUSTOM_TOKEN_CONFIG set (the setup creates token D on-chain
|
|
// but leaves it out of the token config).
|