mirror of
https://github.com/logos-blockchain/lez-programs.git
synced 2026-08-25 14:11:09 +00:00
Consolidate wallet account decoding and portfolio handling around the shared Rust IDL decoder. Simplify AMM wallet integration, remove obsolete caches and network plumbing, and cover account selection and live flows.
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).
|