From eec75d5af1abba67d42d94fc67264bd919309813 Mon Sep 17 00:00:00 2001 From: Abraham Sewill Date: Wed, 20 May 2026 18:27:19 -0500 Subject: [PATCH 1/2] Fix estimate_fee against coinset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit coinset's get_fee_estimate accepts only `cost` + `target_times` and rejects `spend_type`/`spend_count`. estimate_fee forwarded those, so a bare call (or one with `spend_type`) errored with "missing field `cost`" / "unknown field `spend_type`" — only an explicit `cost` ever worked. Resolve to a `cost` and send only `cost` + `target_times`: the explicit `cost` if given, else the `spend_type`'s approximate cost (rough multiples of one standard spend) or a standard spend, scaled by `spend_count`. The spend_type / spend_count hints stay and are mapped to a cost client-side (coinset's own error says "use cost instead"). Param/tool descriptions are unchanged. Also fixes the unit test, which had mocked get_fee_estimate and asserted the broken no-cost payload — so it stayed green while the real call failed. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/tools/mempool/estimate-fee.ts | 35 ++++++++++++++++--- tests/tools.mempool.test.ts | 58 +++++++++++++++++++++++++------ 2 files changed, 78 insertions(+), 15 deletions(-) diff --git a/src/tools/mempool/estimate-fee.ts b/src/tools/mempool/estimate-fee.ts index 057f003..c0728eb 100644 --- a/src/tools/mempool/estimate-fee.ts +++ b/src/tools/mempool/estimate-fee.ts @@ -19,6 +19,24 @@ const SPEND_TYPES = [ 'create_new_did_wallet', ] as const; +// Approximate CLVM cost of one standard XCH spend. +const DEFAULT_SPEND_COST = 11_000_000; + +// coinset's get_fee_estimate accepts only `cost` + `target_times` (it rejects `spend_type`), +// so a spend_type hint is translated into an approximate cost here. These are rough multiples +// of one standard spend, meant only to bias the estimate — pass an explicit `cost` for precision. +const SPEND_TYPE_COST: Record<(typeof SPEND_TYPES)[number], number> = { + send_xch_transaction: DEFAULT_SPEND_COST, + cat_spend: DEFAULT_SPEND_COST * 3, + take_offer: DEFAULT_SPEND_COST * 4, + cancel_offer: DEFAULT_SPEND_COST * 2, + nft_set_nft_did: DEFAULT_SPEND_COST * 5, + nft_transfer_nft: DEFAULT_SPEND_COST * 5, + create_new_pool_wallet: DEFAULT_SPEND_COST * 5, + pw_absorb_rewards: DEFAULT_SPEND_COST * 4, + create_new_did_wallet: DEFAULT_SPEND_COST * 4, +}; + export function register(server: McpServer): void { server.tool( 'estimate_fee', @@ -51,11 +69,18 @@ export function register(server: McpServer): void { async ({ target_times, cost, spend_type, spend_count, network }) => { try { const agent = getAgent(network as Network); - const payload: Parameters[1] = { target_times }; - if (cost !== undefined) payload.cost = cost; - if (spend_type !== undefined) payload.spend_type = spend_type; - if (spend_count !== undefined) payload.spend_count = spend_count; - const res = await get_fee_estimate(agent, payload); + // coinset's get_fee_estimate accepts only `cost` + `target_times`. Resolve to a cost: + // an explicit `cost`, else the spend_type's approximate cost (or a standard spend) + // scaled by spend_count. This avoids the "missing field cost" / "unknown field + // spend_type" errors coinset returns for the raw payload. + let effectiveCost: number; + if (cost !== undefined) { + effectiveCost = cost; + } else { + const perSpend = spend_type ? SPEND_TYPE_COST[spend_type] : DEFAULT_SPEND_COST; + effectiveCost = perSpend * (spend_count ?? 1); + } + const res = await get_fee_estimate(agent, { target_times, cost: effectiveCost }); const estimates_mojo = (res.estimates ?? []).map((e) => toBigInt(e).toString()); const estimates_xch = estimates_mojo.map((m) => mojoToXch(m)); diff --git a/tests/tools.mempool.test.ts b/tests/tools.mempool.test.ts index 66ce6a7..ded9eb4 100644 --- a/tests/tools.mempool.test.ts +++ b/tests/tools.mempool.test.ts @@ -162,7 +162,7 @@ describe('mempool tools (mocked RPC)', () => { }); describe('estimate_fee', () => { - it('passes default target_times [60,300,900] when none supplied', async () => { + it('defaults target_times and a cost when none supplied', async () => { mocks.get_fee_estimate.mockResolvedValue({ estimates: [0, 1000, 5000], target_times: [60, 300, 900], @@ -181,12 +181,15 @@ describe('mempool tools (mocked RPC)', () => { expect(body.target_times).toEqual([60, 300, 900]); expect(body.estimates_mojo).toEqual(['0', '1000', '5000']); expect(body.estimates_xch[0]).toBe('0'); + // coinset requires `cost`; with no args the tool defaults one (~ a standard XCH + // send) so a bare call doesn't error with "missing field cost". expect(mocks.get_fee_estimate).toHaveBeenCalledWith(expect.anything(), { target_times: [60, 300, 900], + cost: 11_000_000, }); }); - it('forwards spend_type, spend_count and cost when supplied', async () => { + it('uses an explicit cost as-is (no spend_type reaches coinset)', async () => { mocks.get_fee_estimate.mockResolvedValue({ estimates: [0], target_times: [60], @@ -198,18 +201,32 @@ describe('mempool tools (mocked RPC)', () => { }); await client.callTool({ name: 'estimate_fee', - arguments: { - target_times: [60], - cost: 1_000_000, - spend_type: 'take_offer', - spend_count: 2, - }, + arguments: { target_times: [60], cost: 1_000_000 }, }); + // Only `cost` + `target_times` reach the RPC: coinset rejects spend_type/spend_count. expect(mocks.get_fee_estimate).toHaveBeenCalledWith(expect.anything(), { target_times: [60], cost: 1_000_000, - spend_type: 'take_offer', - spend_count: 2, + }); + }); + + it('scales the default cost by spend_count when no explicit cost is given', async () => { + mocks.get_fee_estimate.mockResolvedValue({ + estimates: [0], + target_times: [60], + current_fee_rate: 0, + mempool_size: 0, + mempool_fees: 0, + full_node_synced: true, + peak_height: 1, + }); + await client.callTool({ + name: 'estimate_fee', + arguments: { target_times: [60], spend_count: 2 }, + }); + expect(mocks.get_fee_estimate).toHaveBeenCalledWith(expect.anything(), { + target_times: [60], + cost: 22_000_000, }); }); @@ -229,6 +246,27 @@ describe('mempool tools (mocked RPC)', () => { expect(res.isError).toBe(true); }); + it('maps spend_type to an approximate cost', async () => { + mocks.get_fee_estimate.mockResolvedValue({ + estimates: [0], + target_times: [60], + current_fee_rate: 0, + mempool_size: 0, + mempool_fees: 0, + full_node_synced: true, + peak_height: 1, + }); + // cat_spend ~ 3x a standard spend (11_000_000); only the resolved cost reaches coinset. + await client.callTool({ + name: 'estimate_fee', + arguments: { target_times: [60], spend_type: 'cat_spend' }, + }); + expect(mocks.get_fee_estimate).toHaveBeenCalledWith(expect.anything(), { + target_times: [60], + cost: 33_000_000, + }); + }); + it('rejects an unknown spend_type', async () => { const res = (await client.callTool({ name: 'estimate_fee', From 3e91f3cb07a2d628757810005ce438aa2b95594c Mon Sep 17 00:00:00 2001 From: Freddie Coleman Date: Thu, 21 May 2026 12:13:03 +0800 Subject: [PATCH 2/2] Bump take_offer's approximate cost from 4x to 9x a standard spend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit chia.net's Jan-2024 fees post puts offer-take cost at 80-150M. The previous 44M (4x) under-shoots by ~2-3x, and because coinset ranks by cost-per-fee, an under-estimated cost yields an under-estimated fee for this spend_type — the user's transaction would sit longer than the target_times they asked for. 9x (99M) sits in the middle of the published range and matches the table's "multiples of one standard spend" style. Co-Authored-By: Claude Opus 4.7 --- src/tools/mempool/estimate-fee.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/tools/mempool/estimate-fee.ts b/src/tools/mempool/estimate-fee.ts index c0728eb..4e07ad0 100644 --- a/src/tools/mempool/estimate-fee.ts +++ b/src/tools/mempool/estimate-fee.ts @@ -28,7 +28,9 @@ const DEFAULT_SPEND_COST = 11_000_000; const SPEND_TYPE_COST: Record<(typeof SPEND_TYPES)[number], number> = { send_xch_transaction: DEFAULT_SPEND_COST, cat_spend: DEFAULT_SPEND_COST * 3, - take_offer: DEFAULT_SPEND_COST * 4, + // chia.net's Jan-2024 fees post puts offer-take cost at 80-150M; *4 (44M) + // under-shoots by ~2-3x, which would translate directly to an underestimated fee. + take_offer: DEFAULT_SPEND_COST * 9, cancel_offer: DEFAULT_SPEND_COST * 2, nft_set_nft_did: DEFAULT_SPEND_COST * 5, nft_transfer_nft: DEFAULT_SPEND_COST * 5,