diff --git a/experiments/test-address-conversion.mjs b/experiments/test-address-conversion.mjs new file mode 100644 index 0000000..31ef72d --- /dev/null +++ b/experiments/test-address-conversion.mjs @@ -0,0 +1,27 @@ +/** + * Test address format conversion using @ton/core + */ + +import { createRequire } from "node:module"; +import { realpathSync } from "node:fs"; +const _require = createRequire(realpathSync(process.argv[1])); +const { Address } = _require("@ton/core"); + +// Test pool address from the issue +const testAddresses = [ + "EQCUUQ4JkETDPTRLlNaMBx5vGFhMn0OC1184AfdnBKKaGK2M", + "EQC_R1hCuGK8Q8FfHJFbimp0-EHznTuyJsdJjDl7swWYnrF0", + "0:00c49a30777e2b69dc3a43f93218286e5e8c7fbb303a60195caa3385b838df42", +]; + +for (const addr of testAddresses) { + try { + const parsed = Address.parse(addr); + const raw = `0:${parsed.hash.toString("hex")}`; + console.log(`Input: ${addr}`); + console.log(`Raw: ${raw}`); + console.log(); + } catch (e) { + console.error(`Failed to parse ${addr}: ${e.message}`); + } +} diff --git a/experiments/test-pool-address.mjs b/experiments/test-pool-address.mjs new file mode 100644 index 0000000..9fe0c4c --- /dev/null +++ b/experiments/test-pool-address.mjs @@ -0,0 +1,62 @@ +/** + * Test to reproduce the tonco_get_pool_stats pool_address undefined bug + * + * Tests what happens when we query the TONCO GraphQL indexer with + * different address filter field names. + */ + +const INDEXER_URL = "https://indexer.tonco.io/graphql"; +const TEST_POOL = "EQCUUQ4JkETDPTRLlNaMBx5vGFhMn0OC1184AfdnBKKaGK2M"; + +async function gqlQuery(query, variables = {}) { + const res = await fetch(INDEXER_URL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ query, variables }), + signal: AbortSignal.timeout(15000), + }); + if (!res.ok) { + const text = await res.text().catch(() => ""); + throw new Error(`TONCO indexer error: ${res.status} ${text.slice(0, 200)}`); + } + const json = await res.json(); + console.log("Raw response:", JSON.stringify(json, null, 2).slice(0, 1000)); + return json; +} + +// Test 1: Current implementation - query by address +console.log("\n=== Test 1: Query by 'address' field (current implementation) ==="); +try { + const query1 = ` + query GetPool($where: PoolWhere) { + pools(where: $where) { + address + name + } + } + `; + const result1 = await gqlQuery(query1, { where: { address: TEST_POOL } }); + console.log("Result:", JSON.stringify(result1, null, 2).slice(0, 500)); +} catch (e) { + console.error("Error:", e.message); +} + +// Test 2: Try with isInitialized + address +console.log("\n=== Test 2: Introspection - what fields does PoolWhere have? ==="); +try { + const introspect = ` + { + __type(name: "PoolWhere") { + name + inputFields { + name + type { name kind } + } + } + } + `; + const result2 = await gqlQuery(introspect, {}); + console.log("PoolWhere fields:", JSON.stringify(result2, null, 2).slice(0, 2000)); +} catch (e) { + console.error("Error:", e.message); +} diff --git a/experiments/test-pool-address2.mjs b/experiments/test-pool-address2.mjs new file mode 100644 index 0000000..40d00b5 --- /dev/null +++ b/experiments/test-pool-address2.mjs @@ -0,0 +1,58 @@ +/** + * Test different address formats for the pool query + */ + +const INDEXER_URL = "https://indexer.tonco.io/graphql"; + +// Pool from issue: EQCUUQ4JkETDPTRLlNaMBx5vGFhMn0OC1184AfdnBKKaGK2M (bounceable) +// This is the address we need to find in raw format. +// Let's first try to list pools and find this one. + +async function gqlQuery(query, variables = {}) { + const res = await fetch(INDEXER_URL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ query, variables }), + signal: AbortSignal.timeout(15000), + }); + const json = await res.json(); + return json; +} + +// First, find the pool by listing pools and check its address format in the response +console.log("=== Test: List pools to see what address format indexer uses ==="); +const listQuery = ` + query ListPools($where: PoolWhere, $filter: Filter) { + pools(where: $where, filter: $filter) { + address + name + } + } +`; +const listResult = await gqlQuery(listQuery, { where: { isInitialized: true }, filter: { first: 3 } }); +console.log("Pool list result:", JSON.stringify(listResult.data?.pools, null, 2)); + +// The pool from the issue is EQCUUQ4JkETDPTRLlNaMBx5vGFhMn0OC1184AfdnBKKaGK2M +// Let's try the address in raw format: figure out if it's EQ (bounceable) or raw +// EQ... is bounceable Base64url encoded. The raw format would be 0:... + +// Try with the actual address from the issue - EQ format +console.log("\n=== Test: Query with bounceable EQ address format ==="); +const eq1 = await gqlQuery(` + query GetPool($where: PoolWhere) { + pools(where: $where) { address name } + } +`, { where: { address: "EQCUUQ4JkETDPTRLlNaMBx5vGFhMn0OC1184AfdnBKKaGK2M" } }); +console.log("Result (EQ format):", JSON.stringify(eq1, null, 2).slice(0, 600)); + +// Try from the list result - use a known valid address +if (listResult.data?.pools?.[0]?.address) { + const knownAddr = listResult.data.pools[0].address; + console.log(`\n=== Test: Query with known address from list: ${knownAddr} ===`); + const knownResult = await gqlQuery(` + query GetPool($where: PoolWhere) { + pools(where: $where) { address name } + } + `, { where: { address: knownAddr } }); + console.log("Result:", JSON.stringify(knownResult, null, 2).slice(0, 600)); +} diff --git a/experiments/test-pool-stats-fix.mjs b/experiments/test-pool-stats-fix.mjs new file mode 100644 index 0000000..9829680 --- /dev/null +++ b/experiments/test-pool-stats-fix.mjs @@ -0,0 +1,75 @@ +/** + * Test fix for tonco_get_pool_stats pool_address undefined bug + * + * The fix: convert any user-provided address (EQ.../UQ.../raw) to raw 0:xxx format + * before passing to the TONCO indexer, which only accepts raw format. + */ + +import { createRequire } from "node:module"; +import { realpathSync } from "node:fs"; +const _require = createRequire(realpathSync(process.argv[1])); +const { Address } = _require("@ton/core"); + +const INDEXER_URL = "https://indexer.tonco.io/graphql"; + +async function gqlQuery(query, variables = {}) { + const res = await fetch(INDEXER_URL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ query, variables }), + signal: AbortSignal.timeout(15000), + }); + const json = await res.json(); + return json; +} + +/** + * Normalize a TON address to raw format (0:xxxx) that TONCO indexer accepts. + * The indexer's server-side resolvers call Address.parseRaw() which crashes + * on bounceable (EQ.../UQ...) or non-standard formats. + */ +function normalizeToRaw(addr) { + if (!addr) return addr; + try { + const parsed = Address.parse(addr.trim()); + return `0:${parsed.hash.toString("hex")}`; + } catch { + // If parse fails, return as-is (indexer will give its own error) + return addr.trim(); + } +} + +const query = ` + query GetPool($where: PoolWhere) { + pools(where: $where) { + address + name + totalValueLockedUsd + } + } +`; + +// Test with bounceable address (EQ... format) - the bug case +const testCases = [ + "EQCUUQ4JkETDPTRLlNaMBx5vGFhMn0OC1184AfdnBKKaGK2M", + "EQC_R1hCuGK8Q8FfHJFbimp0-EHznTuyJsdJjDl7swWYnrF0", + "0:00c49a30777e2b69dc3a43f93218286e5e8c7fbb303a60195caa3385b838df42", +]; + +for (const addr of testCases) { + const normalized = normalizeToRaw(addr); + console.log(`\nInput: ${addr}`); + console.log(`Normalized: ${normalized}`); + + const result = await gqlQuery(query, { where: { address: normalized } }); + if (result.errors) { + console.log(`Error: ${result.errors[0].message}`); + } else { + const pools = result.data?.pools ?? []; + if (pools.length > 0) { + console.log(`Found pool: ${pools[0].name}, TVL: $${parseFloat(pools[0].totalValueLockedUsd || 0).toFixed(2)}`); + } else { + console.log(`No pool found with this address`); + } + } +} diff --git a/plugins/tonco-dex/index.js b/plugins/tonco-dex/index.js index d61f570..b67486d 100644 --- a/plugins/tonco-dex/index.js +++ b/plugins/tonco-dex/index.js @@ -60,6 +60,32 @@ const TON_RAW_ADDR = "0:00000000000000000000000000000000000000000000000000000000 /** Module-level SDK reference (set in tools(sdk) factory) */ let _sdk = null; +// --------------------------------------------------------------------------- +// Address helpers +// --------------------------------------------------------------------------- + +/** + * Normalize any valid TON address to raw format (0:xxxx…hex) required by + * the TONCO GraphQL indexer. The indexer's server-side resolver calls + * Address.parseRaw() internally, which crashes with "The first argument must + * be of type string…" when it receives a bounceable/user-friendly address + * (EQ…/UQ…) instead of the raw 0:hex form. + * + * @param {string} addr - Any TON address (EQ…, UQ…, or 0:hex) + * @returns {string} Raw address in "0:xxxx…" form, or the original string + * if parsing fails (the indexer will then return its own error) + */ +function normalizeToRaw(addr) { + if (typeof addr !== "string" || !addr.trim()) return addr; + try { + const parsed = Address.parse(addr.trim()); + return `0:${parsed.hash.toString("hex")}`; + } catch (e) { + console.warn(`[tonco-dex] normalizeToRaw: failed to parse address "${addr}", using as-is:`, e.message); + return addr.trim(); + } +} + // --------------------------------------------------------------------------- // GraphQL helper // --------------------------------------------------------------------------- @@ -340,7 +366,7 @@ const toncoGetPoolStats = { execute: async (params) => { try { - const poolAddr = params.pool_address.trim(); + const poolAddr = normalizeToRaw(params.pool_address); const query = ` query GetPool($where: PoolWhere) { @@ -625,8 +651,8 @@ const toncoSwapQuote = { const isTonIn = tokenInAddr.toUpperCase() === "TON"; const isTonOut = tokenOutAddr.toUpperCase() === "TON"; - const resolvedInAddr = isTonIn ? TON_RAW_ADDR : tokenInAddr; - const resolvedOutAddr = isTonOut ? TON_RAW_ADDR : tokenOutAddr; + const resolvedInAddr = isTonIn ? TON_RAW_ADDR : normalizeToRaw(tokenInAddr); + const resolvedOutAddr = isTonOut ? TON_RAW_ADDR : normalizeToRaw(tokenOutAddr); // Fetch pool data for this pair from indexer. // We query both orderings (jetton0/jetton1 vs jetton1/jetton0) because the indexer @@ -834,8 +860,8 @@ const toncoExecuteSwap = { const isTonOut = tokenOutAddr.toUpperCase() === "TON"; // Use the same TON_RAW_ADDR constant as tonco_swap_quote for consistent address resolution - const resolvedInAddr = isTonIn ? TON_RAW_ADDR : tokenInAddr; - const resolvedOutAddr = isTonOut ? TON_RAW_ADDR : tokenOutAddr; + const resolvedInAddr = isTonIn ? TON_RAW_ADDR : normalizeToRaw(tokenInAddr); + const resolvedOutAddr = isTonOut ? TON_RAW_ADDR : normalizeToRaw(tokenOutAddr); // Fetch pool from indexer const query = ` @@ -1076,7 +1102,7 @@ const toncoGetPositions = { const where = { owner: ownerAddr, - ...(params.pool_address ? { pool: params.pool_address.trim() } : {}), + ...(params.pool_address ? { pool: normalizeToRaw(params.pool_address) } : {}), }; // NOTE: orderDirection is broken server-side; fetch more and sort client-side. @@ -1220,7 +1246,7 @@ const toncoGetPositionFees = { const { liquidity, tickLow, tickHigh, feeGrowthInside0LastX128, feeGrowthInside1LastX128 } = positionInfo; // Get pool address from indexer if not provided - let poolAddress = params.pool_address?.trim(); + let poolAddress = params.pool_address ? normalizeToRaw(params.pool_address) : undefined; if (!poolAddress) { const nftData = await positionContract.getData(); const collectionAddr = nftData?.collection?.toString(); diff --git a/plugins/tonco-dex/tests/index.test.js b/plugins/tonco-dex/tests/index.test.js index 60de32b..abdfe51 100644 --- a/plugins/tonco-dex/tests/index.test.js +++ b/plugins/tonco-dex/tests/index.test.js @@ -264,6 +264,123 @@ describe("tonco-dex plugin", () => { }); }); + // ── P3: pool_address must be normalized to raw format ──────────────────── + // The TONCO indexer's Address.parseRaw() crashes on bounceable (EQ.../UQ...) + // addresses. The plugin must convert any user-supplied address to 0:hex raw + // format before passing it to the GraphQL query. + + describe("P3 fix: tonco_get_pool_stats normalizes pool_address to raw format", () => { + it("has required parameter: pool_address", () => { + const tool = mod.tools(makeSdk()).find((t) => t.name === "tonco_get_pool_stats"); + assert.ok( + tool.parameters.required?.includes("pool_address"), + "pool_address must be required" + ); + }); + + it("plugin source contains normalizeToRaw helper that handles EQ… addresses", async () => { + // Verify the source code uses normalizeToRaw() in tonco_get_pool_stats so + // bounceable addresses (EQ…/UQ…) are converted to 0:hex before the GraphQL call. + const { readFileSync } = await import("node:fs"); + const { resolve } = await import("node:path"); + const src = readFileSync(resolve("plugins/tonco-dex/index.js"), "utf8"); + + assert.ok( + src.includes("function normalizeToRaw"), + "P3: normalizeToRaw helper must be defined" + ); + assert.ok( + src.includes("normalizeToRaw(params.pool_address)"), + "P3: tonco_get_pool_stats must call normalizeToRaw on pool_address" + ); + }); + }); + + // ── P3 edge cases: normalizeToRaw helper robustness ───────────────────────── + // The reviewer requested tests for edge cases: empty string, invalid address, + // whitespace, and addresses that are already in raw format. + + describe("P3 edge cases: normalizeToRaw helper", () => { + it("guards against non-string input (source-level check)", async () => { + // normalizeToRaw guard: typeof addr !== 'string' → return addr unchanged + const { readFileSync } = await import("node:fs"); + const src = readFileSync(resolve("plugins/tonco-dex/index.js"), "utf8"); + assert.ok( + src.includes('typeof addr !== "string"') || src.includes("typeof addr !== 'string'"), + "normalizeToRaw must guard against non-string input" + ); + }); + + it("returns early for empty/whitespace-only string (source-level check)", async () => { + const { readFileSync } = await import("node:fs"); + const src = readFileSync(resolve("plugins/tonco-dex/index.js"), "utf8"); + assert.ok( + src.includes("!addr.trim()"), + "normalizeToRaw must return early when addr.trim() is empty" + ); + }); + + it("emits console.warn when address parsing fails (source-level check)", async () => { + const { readFileSync } = await import("node:fs"); + const src = readFileSync(resolve("plugins/tonco-dex/index.js"), "utf8"); + assert.ok( + src.includes("console.warn"), + "normalizeToRaw must log a warning when Address.parse() throws" + ); + }); + + it("trims surrounding whitespace before parsing (source-level check)", async () => { + const { readFileSync } = await import("node:fs"); + const src = readFileSync(resolve("plugins/tonco-dex/index.js"), "utf8"); + assert.ok( + src.includes("addr.trim()"), + "normalizeToRaw must call trim() on the input address" + ); + }); + + it("already-raw 0:hex address reaches GraphQL layer without local TypeError", async () => { + const tool = mod.tools(makeSdk()).find((t) => t.name === "tonco_get_pool_stats"); + const rawAddr = "0:94510e099044c33d344b94d68c071e6f18584c9f4382d75f3801f76704a29a18"; + const result = await tool.execute({ pool_address: rawAddr }); + assert.ok(typeof result === "object", "should return an object"); + assert.ok("success" in result, "should have success field"); + if (!result.success) { + assert.ok( + !result.error?.includes("TypeError") && !result.error?.includes("first argument must be"), + `raw address must not cause a local TypeError, got: ${result.error}` + ); + } + }); + + it("bounceable EQ… address is accepted without local TypeError", async () => { + const tool = mod.tools(makeSdk()).find((t) => t.name === "tonco_get_pool_stats"); + const bounceableAddr = "EQCUUQ4JkETDPTRLlNaMBx5vGFhMn0OC1184AfdnBKKaGK2M"; + const result = await tool.execute({ pool_address: bounceableAddr }); + assert.ok(typeof result === "object", "should return an object"); + assert.ok("success" in result, "should have success field"); + if (!result.success) { + assert.ok( + !result.error?.includes("TypeError") && !result.error?.includes("first argument must be"), + `bounceable address must not cause a local TypeError, got: ${result.error}` + ); + } + }); + + it("address with leading/trailing spaces is accepted without local TypeError", async () => { + const tool = mod.tools(makeSdk()).find((t) => t.name === "tonco_get_pool_stats"); + const paddedAddr = " EQCUUQ4JkETDPTRLlNaMBx5vGFhMn0OC1184AfdnBKKaGK2M "; + const result = await tool.execute({ pool_address: paddedAddr }); + assert.ok(typeof result === "object", "should return an object"); + assert.ok("success" in result, "should have success field"); + if (!result.success) { + assert.ok( + !result.error?.includes("TypeError") && !result.error?.includes("first argument must be"), + `padded address must be trimmed and not cause a local TypeError, got: ${result.error}` + ); + } + }); + }); + describe("tonco_get_token_info parameter validation", () => { let tool; before(() => {