@arcnow/mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -0
- package/README.md +624 -0
- package/dist/client-options.d.ts +16 -0
- package/dist/client-options.d.ts.map +1 -0
- package/dist/client-options.js +22 -0
- package/dist/client-options.js.map +1 -0
- package/dist/config.d.ts +223 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +516 -0
- package/dist/config.js.map +1 -0
- package/dist/format.d.ts +79 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +164 -0
- package/dist/format.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +50 -0
- package/dist/index.js.map +1 -0
- package/dist/sdk-port.d.ts +227 -0
- package/dist/sdk-port.d.ts.map +1 -0
- package/dist/sdk-port.js +144 -0
- package/dist/sdk-port.js.map +1 -0
- package/dist/server.d.ts +25 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +103 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/errors.d.ts +44 -0
- package/dist/tools/errors.d.ts.map +1 -0
- package/dist/tools/errors.js +231 -0
- package/dist/tools/errors.js.map +1 -0
- package/dist/tools/index.d.ts +35 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +59 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/launch-params.d.ts +27 -0
- package/dist/tools/launch-params.d.ts.map +1 -0
- package/dist/tools/launch-params.js +29 -0
- package/dist/tools/launch-params.js.map +1 -0
- package/dist/tools/pool.d.ts +84 -0
- package/dist/tools/pool.d.ts.map +1 -0
- package/dist/tools/pool.js +249 -0
- package/dist/tools/pool.js.map +1 -0
- package/dist/tools/quote.d.ts +53 -0
- package/dist/tools/quote.d.ts.map +1 -0
- package/dist/tools/quote.js +85 -0
- package/dist/tools/quote.js.map +1 -0
- package/dist/tools/read.d.ts +26 -0
- package/dist/tools/read.d.ts.map +1 -0
- package/dist/tools/read.js +920 -0
- package/dist/tools/read.js.map +1 -0
- package/dist/tools/resolve.d.ts +45 -0
- package/dist/tools/resolve.d.ts.map +1 -0
- package/dist/tools/resolve.js +75 -0
- package/dist/tools/resolve.js.map +1 -0
- package/dist/tools/schema.d.ts +98 -0
- package/dist/tools/schema.d.ts.map +1 -0
- package/dist/tools/schema.js +143 -0
- package/dist/tools/schema.js.map +1 -0
- package/dist/tools/spend.d.ts +45 -0
- package/dist/tools/spend.d.ts.map +1 -0
- package/dist/tools/spend.js +99 -0
- package/dist/tools/spend.js.map +1 -0
- package/dist/tools/venue.d.ts +67 -0
- package/dist/tools/venue.d.ts.map +1 -0
- package/dist/tools/venue.js +110 -0
- package/dist/tools/venue.js.map +1 -0
- package/dist/tools/write.d.ts +56 -0
- package/dist/tools/write.d.ts.map +1 -0
- package/dist/tools/write.js +950 -0
- package/dist/tools/write.js.map +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1,920 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The read-only tools: everything that cannot cost anybody anything.
|
|
3
|
+
*
|
|
4
|
+
* These are always published, on every server, in every mode, and they are the
|
|
5
|
+
* part worth getting excellent. An assistant that can answer "what is this
|
|
6
|
+
* token, what would 50 USDC buy me, and how close is it to graduating" without
|
|
7
|
+
* being able to sign anything is useful to far more people than one that can
|
|
8
|
+
* trade, and it is useful with no key anywhere near it.
|
|
9
|
+
*
|
|
10
|
+
* A quote answers for wherever the token trades now: its bonding curve before
|
|
11
|
+
* graduation, its Uniswap v4 pool after migration (see `./pool.ts`), and
|
|
12
|
+
* nowhere in between — which is said, not glossed.
|
|
13
|
+
*
|
|
14
|
+
* @module
|
|
15
|
+
*/
|
|
16
|
+
import { Bps, isArcNowError, QuoteAmount, TRADE_FEE_BPS, Tokens, } from "@arcnow/sdk";
|
|
17
|
+
import { addr, curveParamsLine, effectivePrice, money, note, price, priceMove, progress, qty, quoteLine, report, section, share, venueOf, ZERO_ADDRESS, } from "../format.js";
|
|
18
|
+
import { renderError } from "./errors.js";
|
|
19
|
+
import { addressArg, boolArg, decimalArg, defineTool, intArg, metadataUriArg, textArg, z } from "./schema.js";
|
|
20
|
+
import { launchParams } from "./launch-params.js";
|
|
21
|
+
import { buyPaymentLine, lpFee, quotePoolBuy, quotePoolSell } from "./pool.js";
|
|
22
|
+
import { chooseQuote, parseQuoteAmount } from "./quote.js";
|
|
23
|
+
import { capText } from "./spend.js";
|
|
24
|
+
import { configuredRouter, resolveMarket, strandedText } from "./venue.js";
|
|
25
|
+
const TRADE_FEE = Bps.of(TRADE_FEE_BPS);
|
|
26
|
+
/** The tolerances a quote is shown at, so a floor is a choice and not a guess. */
|
|
27
|
+
const TOLERANCES = [50n, 100n, 300n, 1000n];
|
|
28
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
29
|
+
const network = defineTool({
|
|
30
|
+
name: "arcnow_network",
|
|
31
|
+
title: "Network and server mode",
|
|
32
|
+
access: "read",
|
|
33
|
+
description: "Which chain this server is pointed at, which arcnow.io contracts are deployed there, "
|
|
34
|
+
+ "which graduation venues exist, whether a Uniswap v4 router is configured for trading "
|
|
35
|
+
+ "graduated tokens, and — the part that decides what else you can do — whether this "
|
|
36
|
+
+ "server is READ-ONLY or has writes enabled.\n\n"
|
|
37
|
+
+ "Call this first in any session that might trade. It tells you the spend ceilings the "
|
|
38
|
+
+ "operator set — one per quote token, and a quote with none is refused — the address that "
|
|
39
|
+
+ "would sign, and whether the write tools exist at all. It lists the network's quote tokens: "
|
|
40
|
+
+ "a token is priced for life in native USDC (the gas currency, 18 decimals) or an ERC-20 "
|
|
41
|
+
+ "such as EURC (6 decimals), and every amount is in that quote. The 6-decimal USDC ERC-20 "
|
|
42
|
+
+ "predeploy is a separate view of native USDC that pays for nothing. Takes no arguments and "
|
|
43
|
+
+ "touches no chain state; arcnow_quote_tokens asks the registry which quotes a launch accepts.",
|
|
44
|
+
input: {},
|
|
45
|
+
run(_args, ctx) {
|
|
46
|
+
const { config } = ctx.port;
|
|
47
|
+
const c = config.contracts;
|
|
48
|
+
const router = configuredRouter(config);
|
|
49
|
+
const deployed = [
|
|
50
|
+
["launchpad", addr(c.launchpad)],
|
|
51
|
+
["tokenFactory", addr(c.tokenFactory)],
|
|
52
|
+
["curveFactory", addr(c.curveFactory)],
|
|
53
|
+
["migratorRegistry", addr(c.migratorRegistry)],
|
|
54
|
+
["platformRegistry", addr(c.platformRegistry)],
|
|
55
|
+
["arcnowPlatform", addr(c.arcnowPlatform)],
|
|
56
|
+
];
|
|
57
|
+
const optional = [
|
|
58
|
+
["escrowMigrator", c.escrowMigrator],
|
|
59
|
+
["v2Migrator", c.v2Migrator],
|
|
60
|
+
["v3Migrator", c.v3Migrator],
|
|
61
|
+
["v4Migrator", c.v4Migrator],
|
|
62
|
+
["feeHook", c.feeHook],
|
|
63
|
+
["v4Router", router],
|
|
64
|
+
].map(([name, address]) => [
|
|
65
|
+
name,
|
|
66
|
+
address === undefined ? "not deployed on this chain" : addr(address),
|
|
67
|
+
]);
|
|
68
|
+
const venues = Object.entries(config.venues)
|
|
69
|
+
.filter(([, present]) => present)
|
|
70
|
+
.map(([name]) => name);
|
|
71
|
+
const mode = ctx.config.mode === "write"
|
|
72
|
+
? `WRITES ENABLED — signing as ${addr(ctx.port.signerAddress ?? ZERO_ADDRESS)}`
|
|
73
|
+
: "READ-ONLY — no tool on this server can sign, spend or launch anything";
|
|
74
|
+
return Promise.resolve({
|
|
75
|
+
text: report(section("arcnow.io MCP server", [
|
|
76
|
+
["mode", mode],
|
|
77
|
+
...(ctx.config.mode === "write"
|
|
78
|
+
? [["spend ceilings", "per quote token, per write call, set by the operator and "
|
|
79
|
+
+ "enforced before any transaction is built — below, with the quote tokens"]]
|
|
80
|
+
: [["to enable writes", "the operator restarts this server with --allow-writes and "
|
|
81
|
+
+ "a key in ARCNOW_PRIVATE_KEY. A key is never a tool argument and asking for "
|
|
82
|
+
+ "one will not help."]]),
|
|
83
|
+
["network", ctx.config.networkFile === undefined
|
|
84
|
+
? config.name
|
|
85
|
+
: `${config.name} — from the operator's ARCNOW_MCP_NETWORK_FILE, not an SDK preset`],
|
|
86
|
+
["chain id", String(config.chainId)],
|
|
87
|
+
["endpoint", ctx.config.rpcUrl],
|
|
88
|
+
["contracts commit", config.contractsCommit ?? "not recorded"],
|
|
89
|
+
["first block", config.deployedAtBlock === undefined
|
|
90
|
+
? "not recorded"
|
|
91
|
+
: String(config.deployedAtBlock)],
|
|
92
|
+
]), section("money on this chain", [
|
|
93
|
+
["native", "USDC, 18 decimals. This is msg.value and what gas is paid in, and one of "
|
|
94
|
+
+ "the quote tokens a token can be priced in."],
|
|
95
|
+
["ERC-20 view", config.usdcErc20 === undefined
|
|
96
|
+
? "none recorded"
|
|
97
|
+
: `${addr(config.usdcErc20)}, 6 decimals — the SAME asset through an ERC-20 `
|
|
98
|
+
+ "interface. It does not pay for gas and no arcnow.io contract touches it. "
|
|
99
|
+
+ "The two raw integers differ by 1e12."],
|
|
100
|
+
["trade fee", `${TRADE_FEE.bps} bps — a flat 1% on every buy and sell, in the token's `
|
|
101
|
+
+ "quote, not a platform's to change. What a platform configures is how that 1% is divided."],
|
|
102
|
+
]), section("quote tokens — what a token can be priced in, for life", [
|
|
103
|
+
...config.quoteTokens.map((token) => [
|
|
104
|
+
token.symbol,
|
|
105
|
+
`${quoteLine(token)}${ctx.config.mode === "write" ? `. Spend cap: ${capText(ctx, token)}` : ""}`,
|
|
106
|
+
]),
|
|
107
|
+
["caveat", "metadata from this network's configuration, not the allowlist: "
|
|
108
|
+
+ "arcnow_quote_tokens asks the quote registry which of them a launch accepts now. A "
|
|
109
|
+
+ "token's own quote is on arcnow_token."],
|
|
110
|
+
]), section("contracts (required)", deployed), section("contracts (present only where the chain supports them)", optional), section("graduation venues", [
|
|
111
|
+
["available", venues.length === 0 ? "none" : venues.join(", ")],
|
|
112
|
+
["caveat", "a token graduates to the migrator ITS OWN CURVE snapshotted at launch, "
|
|
113
|
+
+ "which is immutable and need not be the one this list would suggest. Ask the "
|
|
114
|
+
+ "curve with arcnow_token, never this list, for a particular token."],
|
|
115
|
+
]), section("trading after graduation", router === undefined
|
|
116
|
+
? [
|
|
117
|
+
["v4 router", "NONE configured for this network"],
|
|
118
|
+
["what that means", "graduated tokens cannot be quoted or traded through this "
|
|
119
|
+
+ "server, however healthy their pools are. Bonding curves are unaffected."],
|
|
120
|
+
]
|
|
121
|
+
: [
|
|
122
|
+
["v4 router", `${addr(router)} — arcnow.io's UniswapV4Router04, the only route `
|
|
123
|
+
+ "into a graduated token's pool"],
|
|
124
|
+
["pool manager", config.v4?.poolManager === undefined
|
|
125
|
+
? "not recorded"
|
|
126
|
+
: `${addr(config.v4.poolManager)} — where arcnow.io's pools hold their `
|
|
127
|
+
+ "liquidity. A v4 pool has no address of its own."],
|
|
128
|
+
["what that means", "graduated tokens trade: once a token has migrated, "
|
|
129
|
+
+ "arcnow_quote_buy, arcnow_quote_sell, arcnow_buy and arcnow_sell route to its "
|
|
130
|
+
+ "pool on their own. Whether this router reaches a particular token's pool is "
|
|
131
|
+
+ "checked per token by arcnow_token. A pool sell needs an ERC-20 approval to "
|
|
132
|
+
+ "this router first."],
|
|
133
|
+
])),
|
|
134
|
+
});
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
138
|
+
const listTokens = defineTool({
|
|
139
|
+
name: "arcnow_list_tokens",
|
|
140
|
+
title: "Recent token launches",
|
|
141
|
+
access: "read",
|
|
142
|
+
description: "Recent arcnow.io launches, newest first, read from the launchpad's `Launched` log.\n\n"
|
|
143
|
+
+ "There is no index and no backend behind this: it walks the chain backwards from the "
|
|
144
|
+
+ "tip in bounded chunks, so it sees a WINDOW of recent history and the result says "
|
|
145
|
+
+ "exactly which blocks it covered. If it stopped on its budget before finding what you "
|
|
146
|
+
+ "asked for, that is reported and is not the same as 'there are no more' — do not tell "
|
|
147
|
+
+ "a user a token does not exist on the strength of this tool.\n\n"
|
|
148
|
+
+ "Filter by `creator` to find what one address launched. Set `includeState` to false "
|
|
149
|
+
+ "when you only need the addresses; each token's curve state is an extra round trip.",
|
|
150
|
+
input: {
|
|
151
|
+
limit: intArg(1, 25, 10, "How many launches to return. Each one with includeState costs a round trip."),
|
|
152
|
+
creator: addressArg("Only launches by this creator address.").optional(),
|
|
153
|
+
includeState: boolArg(true, "Also read each curve's live state: price, reserve, progress to graduation, whether "
|
|
154
|
+
+ "it graduated. Off is faster and tells you only what the launch log recorded."),
|
|
155
|
+
},
|
|
156
|
+
async run(args, ctx) {
|
|
157
|
+
const scan = await ctx.port.listLaunches({
|
|
158
|
+
limit: args.limit,
|
|
159
|
+
...(args.creator === undefined ? {} : { creator: args.creator }),
|
|
160
|
+
});
|
|
161
|
+
if (scan.launches.length === 0) {
|
|
162
|
+
return {
|
|
163
|
+
text: report(`No launches found in blocks ${scan.scannedFromBlock}–${scan.scannedToBlock}.`, note(scan.reachedDeployment
|
|
164
|
+
? "That scan reached the block this deployment starts at, so for the filter you "
|
|
165
|
+
+ "gave, there really are none."
|
|
166
|
+
: "That scan stopped on its block budget with history still unread, so this is "
|
|
167
|
+
+ "NOT evidence that none exist — only that none are recent. Say so rather "
|
|
168
|
+
+ "than reporting an absence.")),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const blocks = [];
|
|
172
|
+
for (const launch of scan.launches) {
|
|
173
|
+
const rows = [
|
|
174
|
+
["token", addr(launch.token)],
|
|
175
|
+
["curve", addr(launch.curve)],
|
|
176
|
+
["creator", addr(launch.creator)],
|
|
177
|
+
["platform", addr(launch.platform)],
|
|
178
|
+
["graduates to", `${addr(launch.migrator)} — ${venueOf(launch.migrator, ctx.port.config)}`],
|
|
179
|
+
["quote", quoteLine(launch.quoteToken)],
|
|
180
|
+
["at launch", `${money(launch.initialBuy)} initial buy, ${money(launch.launchFee)} `
|
|
181
|
+
+ `launch fee, ${qty(launch.tokensOut)} tokens to the creator`],
|
|
182
|
+
["block", `${launch.blockNumber} (tx ${launch.txHash})`],
|
|
183
|
+
];
|
|
184
|
+
if (args.includeState) {
|
|
185
|
+
try {
|
|
186
|
+
const [state, symbol, name] = await Promise.all([
|
|
187
|
+
ctx.port.curve(launch.curve).state(),
|
|
188
|
+
ctx.port.token(launch.token).symbol(),
|
|
189
|
+
ctx.port.token(launch.token).name(),
|
|
190
|
+
]);
|
|
191
|
+
rows.unshift(["name", `${name} (${symbol})`]);
|
|
192
|
+
rows.push(["price now", price(state.spotPrice)], ["raised", `${money(state.realReserve)} of ${money(state.target)}`], ["progress", progress(state.progressBps)], ["status", statusLine(state.graduated, state.migrated)]);
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
rows.push(["state", `could not be read: ${describe(error)}`]);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
blocks.push(section(`launch at block ${launch.blockNumber}`, rows));
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
text: report(`${scan.launches.length} launch(es), newest first.`, ...blocks, section("what this scan covered", [
|
|
202
|
+
["blocks", `${scan.scannedFromBlock}–${scan.scannedToBlock} (tip is ${scan.tipBlock})`],
|
|
203
|
+
["complete", scan.reachedDeployment
|
|
204
|
+
? "yes — the scan reached the first block of this deployment"
|
|
205
|
+
: scan.stoppedOnBudget
|
|
206
|
+
? "NO — it stopped on its block budget with history unread. There may be more."
|
|
207
|
+
: "the limit was reached before the budget was; there is more history below this."],
|
|
208
|
+
])),
|
|
209
|
+
};
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
213
|
+
const tokenTool = defineTool({
|
|
214
|
+
name: "arcnow_token",
|
|
215
|
+
title: "Inspect a token and where it trades",
|
|
216
|
+
access: "read",
|
|
217
|
+
description: "Everything about one arcnow.io token: its metadata, its bonding curve's live state, "
|
|
218
|
+
+ "the price, how far it is from graduation, whether it has graduated — and WHERE IT "
|
|
219
|
+
+ "TRADES NOW: its bonding curve, its Uniswap v4 pool, or nowhere.\n\n"
|
|
220
|
+
+ "For a token in its pool this reports the router this server would trade through, "
|
|
221
|
+
+ "whether that router can actually reach the pool, the PoolManager the liquidity is in "
|
|
222
|
+
+ "(a v4 pool has no address of its own), and the pool's own LP fee, which is charged on "
|
|
223
|
+
+ "top of arcnow.io's 1%.\n\n"
|
|
224
|
+
+ "It names the token's QUOTE TOKEN — native USDC or an ERC-20 such as EURC, fixed at "
|
|
225
|
+
+ "launch — with its address and decimals: every amount here, and every amount the trade "
|
|
226
|
+
+ "tools take for this token, is in that quote.\n\n"
|
|
227
|
+
+ "Pass either the token address or its curve address; this works out which it got. "
|
|
228
|
+
+ "Pass `holder` to include that address's balance and any refund the curve is holding "
|
|
229
|
+
+ "for it.\n\n"
|
|
230
|
+
+ "Two things this tool reports that are easy to misread. The spot price is the marginal "
|
|
231
|
+
+ "price of the next infinitesimal token, NOT the price a trade of any size fills at — "
|
|
232
|
+
+ "use arcnow_quote_buy for that. And 'graduated' and 'migrated' are different states: a "
|
|
233
|
+
+ "curve can have graduated (trading on it is over, permanently) while its pool was never "
|
|
234
|
+
+ "created, which leaves the token tradeable nowhere until somebody runs migrate().",
|
|
235
|
+
input: {
|
|
236
|
+
address: addressArg("The token address, or its curve address. Either works."),
|
|
237
|
+
holder: addressArg("Optional: also report this address's token balance and any of the token's quote the "
|
|
238
|
+
+ "curve is holding for it after a failed payout.").optional(),
|
|
239
|
+
},
|
|
240
|
+
async run(args, ctx) {
|
|
241
|
+
const market = await resolveMarket(ctx, args.address);
|
|
242
|
+
const { curve, state, viaToken } = market;
|
|
243
|
+
const token = ctx.port.token(state.token);
|
|
244
|
+
const [name, symbol, metadataUri, totalSupply, canonical] = await Promise.all([
|
|
245
|
+
token.name(), token.symbol(), token.metadataUri(), token.totalSupply(),
|
|
246
|
+
token.canonicalRouter(),
|
|
247
|
+
]);
|
|
248
|
+
const venueBlock = await whereItTrades(ctx, market);
|
|
249
|
+
const graduationBlock = state.graduated
|
|
250
|
+
? section("graduation", [
|
|
251
|
+
["trading on the curve", "OVER, permanently. Every further buy, sell and quote on "
|
|
252
|
+
+ "this curve reverts with CurveGraduated. There is no way to reopen it."],
|
|
253
|
+
["graduated to", `${addr(state.migrator)} — ${venueOf(state.migrator, ctx.port.config)}`],
|
|
254
|
+
["pool", state.migrated
|
|
255
|
+
? "created — see where it trades, above"
|
|
256
|
+
: "NOT CREATED YET. The curve graduated and the pool was never recorded, which "
|
|
257
|
+
+ "happens when the graduating buy's instant migration ran out of gas and was "
|
|
258
|
+
+ "caught. migrate() is permissionless and anyone can finish it; on this server "
|
|
259
|
+
+ "that is arcnow_migrate, and it costs only gas."],
|
|
260
|
+
["canonical router", canonical.toLowerCase() === ZERO_ADDRESS
|
|
261
|
+
? "none (the zero address) — no holder has a standing allowance to anybody, which "
|
|
262
|
+
+ "is why a pool sell needs its own approval to the v4 router"
|
|
263
|
+
: `${addr(canonical)}, auto-approved for every holder after migration`],
|
|
264
|
+
])
|
|
265
|
+
: section("progress to graduation", [
|
|
266
|
+
["raised", `${money(state.realReserve)} of ${money(state.target)}`],
|
|
267
|
+
["progress", progress(state.progressBps)],
|
|
268
|
+
["remaining", money(state.target.subSaturating(state.realReserve))],
|
|
269
|
+
["will graduate to", `${addr(state.migrator)} — ${venueOf(state.migrator, ctx.port.config)}`],
|
|
270
|
+
["how", "the buy that fills the curve migrates it in ITS OWN transaction, is capped "
|
|
271
|
+
+ `at the remaining inventory, and does not take the unspent ${state.quoteToken.symbol}.`],
|
|
272
|
+
]);
|
|
273
|
+
const holderBlock = args.holder === undefined
|
|
274
|
+
? undefined
|
|
275
|
+
: await (async () => {
|
|
276
|
+
const holder = args.holder;
|
|
277
|
+
const [balance, pending] = await Promise.all([
|
|
278
|
+
token.balanceOf(holder),
|
|
279
|
+
curve.pendingWithdrawal(holder).catch(() => QuoteAmount.zero(state.quoteToken)),
|
|
280
|
+
]);
|
|
281
|
+
return section(`holder ${addr(holder)}`, [
|
|
282
|
+
["balance", qty(balance, symbol)],
|
|
283
|
+
["curve owes", pending.isZero()
|
|
284
|
+
? "nothing"
|
|
285
|
+
: `${money(pending)} — a payout or refund whose transfer failed and was credited `
|
|
286
|
+
+ "instead. It is claimable by whoever owns the address."],
|
|
287
|
+
]);
|
|
288
|
+
})();
|
|
289
|
+
return {
|
|
290
|
+
text: report(`${name} (${symbol})`, section("addresses", [
|
|
291
|
+
["token", addr(state.token)],
|
|
292
|
+
["curve", addr(curve.address)],
|
|
293
|
+
["creator", `${addr(state.creator)} — a transferable seat that earns a share of the `
|
|
294
|
+
+ "fee. It carries no power over the token."],
|
|
295
|
+
["metadata", metadataUri === "" ? "(none set)" : metadataUri],
|
|
296
|
+
["resolved from", viaToken ? "the token address you gave" : "the curve address you gave"],
|
|
297
|
+
]), section("curve", [
|
|
298
|
+
["status", statusLine(state.graduated, state.migrated)],
|
|
299
|
+
["quote token", `${quoteLine(state.quoteToken)} — fixed at launch; every amount below `
|
|
300
|
+
+ "is in it"],
|
|
301
|
+
["curve parameters", curveParamsLine(state.params, state.quoteToken)],
|
|
302
|
+
["spot price", `${price(state.spotPrice)} — the marginal price of the next `
|
|
303
|
+
+ "infinitesimal token, not what an order of any size fills at"],
|
|
304
|
+
["tokens sold", qty(state.tokensSold, symbol)],
|
|
305
|
+
["tokens left on the curve", qty(state.tokensRemaining, symbol)],
|
|
306
|
+
["total supply", qty(totalSupply, symbol)],
|
|
307
|
+
["real reserve", money(state.realReserve)],
|
|
308
|
+
["virtual reserve", `${money(state.virtualReserve)} — part of the curve's shape, `
|
|
309
|
+
+ "not money anybody holds"],
|
|
310
|
+
]), venueBlock, graduationBlock, holderBlock),
|
|
311
|
+
};
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
/** The "where it trades" block of arcnow_token. */
|
|
315
|
+
async function whereItTrades(ctx, market) {
|
|
316
|
+
const router = configuredRouter(ctx.port.config);
|
|
317
|
+
const routerLine = router === undefined
|
|
318
|
+
? "none — no v4 router is configured for this network, so this server cannot quote or "
|
|
319
|
+
+ "trade any graduated token"
|
|
320
|
+
: `${addr(router)} — arcnow.io's UniswapV4Router04 (contracts.v4Router)`;
|
|
321
|
+
if (market.venue === "curve") {
|
|
322
|
+
return section("where it trades", [
|
|
323
|
+
["venue", "its bonding curve. The quote and trade tools trade it there; once it "
|
|
324
|
+
+ "graduates and migrates, the same tools route to its Uniswap v4 pool."],
|
|
325
|
+
["router, after that", routerLine],
|
|
326
|
+
]);
|
|
327
|
+
}
|
|
328
|
+
if (market.venue === "stranded") {
|
|
329
|
+
return section("where it trades", [
|
|
330
|
+
["venue", "NONE — graduated and never migrated. Not tradeable anywhere until somebody "
|
|
331
|
+
+ "calls migrate(), which is permissionless; arcnow_migrate does it for the cost of gas."],
|
|
332
|
+
["router, after that", routerLine],
|
|
333
|
+
]);
|
|
334
|
+
}
|
|
335
|
+
const pool = market.trade.pool;
|
|
336
|
+
const [reachable, manager, id, key, quote, quoteIsCurrency0] = await Promise.all([
|
|
337
|
+
settle(pool.isReachable()),
|
|
338
|
+
settle(pool.poolManager()),
|
|
339
|
+
settle(pool.poolId()),
|
|
340
|
+
settle(pool.key()),
|
|
341
|
+
settle(pool.quoteToken()),
|
|
342
|
+
settle(pool.quoteIsCurrency0()),
|
|
343
|
+
]);
|
|
344
|
+
// The SDK checks the hook's VERSION() before it reads the accrual, and refuses
|
|
345
|
+
// any hook but arcnow/arc-now-fee-hook@3.x.x as UnknownHookVersion.
|
|
346
|
+
const accrued = await settle(pool.accruedHookFee());
|
|
347
|
+
return section("where it trades", [
|
|
348
|
+
["venue", "its Uniswap v4 pool — graduated and migrated. The quote and trade tools route "
|
|
349
|
+
+ "there on their own, and every pool quote says it is one."],
|
|
350
|
+
["router", routerLine],
|
|
351
|
+
["reachable", reachable instanceof Error
|
|
352
|
+
? `could not be checked: ${reachable.message}`
|
|
353
|
+
: reachable
|
|
354
|
+
? "yes — the router's own PoolManager is the one this pool is in"
|
|
355
|
+
: router === undefined
|
|
356
|
+
? "no — there is no router to reach it through"
|
|
357
|
+
: "no — the router serves a different PoolManager, so it cannot see this pool, and "
|
|
358
|
+
+ "never will: both are immutable"],
|
|
359
|
+
["pool manager", manager instanceof Error
|
|
360
|
+
? `could not be read: ${manager.message}`
|
|
361
|
+
: `${addr(manager)} — the Uniswap v4 PoolManager holding the liquidity. A v4 pool has no `
|
|
362
|
+
+ "address of its own: it is a pool id inside this manager, and token.migratedPool() "
|
|
363
|
+
+ "reports this same address for every token that migrated into it."],
|
|
364
|
+
["pool id", id instanceof Error ? `could not be read: ${id.message}` : id],
|
|
365
|
+
["quote currency", quote instanceof Error
|
|
366
|
+
? `could not be read: ${quote.message}`
|
|
367
|
+
: quoteIsCurrency0 instanceof Error
|
|
368
|
+
? `${quote.symbol}; which currency of the key it is could not be read: ${quoteIsCurrency0.message}`
|
|
369
|
+
: `${quote.symbol} — currency${quoteIsCurrency0 ? 0 : 1} of the pool key, with the token as `
|
|
370
|
+
+ `currency${quoteIsCurrency0 ? 1 : 0}, as the SDK reads the key. The key orders its `
|
|
371
|
+
+ "currencies by address, so an ERC-20 quote can be either; every amount here is already "
|
|
372
|
+
+ `in ${quote.symbol}`],
|
|
373
|
+
["pool fee", key instanceof Error
|
|
374
|
+
? `could not be read: ${key.message}`
|
|
375
|
+
: `${lpFee(key.fee)} — Uniswap's LP fee, on top of arcnow.io's 1%, which the fee hook `
|
|
376
|
+
+ `at ${addr(key.hooks)} takes in ${market.state.quoteToken.symbol}`],
|
|
377
|
+
["fee hook", "accrues arcnow.io's 1% as a PoolManager claim and pays it out to the fee "
|
|
378
|
+
+ "recipients at the start of a later swap in this pool, or when anyone calls distributeFees"],
|
|
379
|
+
["hook fees accrued", accrued instanceof Error
|
|
380
|
+
? isArcNowError(accrued) && accrued.code === "UnknownHookVersion"
|
|
381
|
+
? `refused: UnknownHookVersion — ${accrued.message}`
|
|
382
|
+
: `could not be read: ${accrued.message}`
|
|
383
|
+
: accrued.isZero()
|
|
384
|
+
? "none — nothing charged and not yet paid out"
|
|
385
|
+
: `${money(accrued)} — charged by earlier swaps and not yet paid out. The next swap in `
|
|
386
|
+
+ "this pool, or anyone's distributeFees, pays it to the fee recipients; it is never "
|
|
387
|
+
+ "part of a trader's fill"],
|
|
388
|
+
["selling", "needs an ERC-20 approval to the router first, a separate transaction. "
|
|
389
|
+
+ "arcnow_sell grants one only when asked, for exactly the amount sold."],
|
|
390
|
+
]);
|
|
391
|
+
}
|
|
392
|
+
async function settle(promise) {
|
|
393
|
+
try {
|
|
394
|
+
return await promise;
|
|
395
|
+
}
|
|
396
|
+
catch (error) {
|
|
397
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
401
|
+
const quoteBuy = defineTool({
|
|
402
|
+
name: "arcnow_quote_buy",
|
|
403
|
+
title: "Quote a buy",
|
|
404
|
+
access: "read",
|
|
405
|
+
description: "What a given amount of the token's quote would buy right now, wherever the token trades: "
|
|
406
|
+
+ "tokens out, "
|
|
407
|
+
+ "every fee and who receives it, the average price the order would fill at, and the "
|
|
408
|
+
+ "minimum-out floor at several slippage tolerances.\n\n"
|
|
409
|
+
+ "Costs nothing and signs nothing. Use it before every buy, and use it INSTEAD of a spot "
|
|
410
|
+
+ "price whenever someone asks what an amount would get them — an order moves the price "
|
|
411
|
+
+ "across its own size, so the spot price is only ever its first infinitesimal slice.\n\n"
|
|
412
|
+
+ "ON A BONDING CURVE: if the buy would fill the curve, this says so, reports the refund, "
|
|
413
|
+
+ "and explains the gas-limit trap that decides whether the token's market is created in "
|
|
414
|
+
+ "that same transaction or never. Read that part before quoting a graduating buy to "
|
|
415
|
+
+ "anyone.\n\n"
|
|
416
|
+
+ "IN A UNISWAP V4 POOL (the token graduated and migrated): the quote says it is a pool "
|
|
417
|
+
+ "quote, priced by simulating the real swap through arcnow.io's router. It shows "
|
|
418
|
+
+ "arcnow.io's 1% and the pool's own LP fee as two separate charges, the average fill "
|
|
419
|
+
+ "price against the pool's spot price, and the price impact. referrer and developer do "
|
|
420
|
+
+ "not exist on a pool swap and are refused.\n\n"
|
|
421
|
+
+ "A token that graduated but never migrated trades nowhere; this says so and names "
|
|
422
|
+
+ "arcnow_migrate. A quote is a snapshot of one block. Anyone else's trade changes it.\n\n"
|
|
423
|
+
+ "THE AMOUNT IS IN THE TOKEN'S QUOTE: native USDC, or the ERC-20 (such as EURC) it was "
|
|
424
|
+
+ "launched in — arcnow_token names it. Every figure in the answer is labelled with that "
|
|
425
|
+
+ "quote's symbol. An ERC-20 quote is pulled with an exact approval, which the answer says.",
|
|
426
|
+
input: {
|
|
427
|
+
address: addressArg("The curve address, or the token address. Either works."),
|
|
428
|
+
quoteIn: decimalArg("How much of the token's quote to spend, in whole units as a person writes it — \"25\", "
|
|
429
|
+
+ "\"1.5\" — in the quote the token is priced in: native USDC (18 decimals) or an ERC-20 such "
|
|
430
|
+
+ "as EURC (6). It is read exactly in that quote's decimals; more decimal places than the "
|
|
431
|
+
+ "quote has is refused, never rounded. A string, because a JSON number cannot hold 18 "
|
|
432
|
+
+ "decimals."),
|
|
433
|
+
referrer: addressArg("CURVE ONLY: the address credited with the referral share of the fee. With no referrer "
|
|
434
|
+
+ "that share goes to the platform instead. Refused for a token in its pool, whose swap "
|
|
435
|
+
+ "has no referrer.").optional(),
|
|
436
|
+
developer: addressArg("CURVE ONLY: the address credited with the developer share of the fee. With none, it "
|
|
437
|
+
+ "goes to the platform. Refused for a token in its pool.").optional(),
|
|
438
|
+
},
|
|
439
|
+
async run(args, ctx) {
|
|
440
|
+
const market = await resolveMarket(ctx, args.address);
|
|
441
|
+
const { curve, state } = market;
|
|
442
|
+
const symbol = await ctx.port.token(state.token).symbol();
|
|
443
|
+
if (market.venue === "stranded") {
|
|
444
|
+
return { isError: true, text: strandedText("arcnow_quote_buy") };
|
|
445
|
+
}
|
|
446
|
+
if (market.venue === "pool")
|
|
447
|
+
return quotePoolBuy(args, ctx, market, symbol);
|
|
448
|
+
const quoteIn = parseQuoteAmount(state.quoteToken, args.quoteIn, "quoteIn");
|
|
449
|
+
const quote = await curve.quoteBuy(quoteIn);
|
|
450
|
+
const split = await curve.previewFeeSplit(quote.fee, args.referrer, args.developer);
|
|
451
|
+
const avg = effectivePrice(quote.quoteSpent, quote.tokensOut);
|
|
452
|
+
return {
|
|
453
|
+
text: report(`Buy quote — ${money(quoteIn)} into ${symbol} (curve ${addr(curve.address)})`, section("what you pay and get", [
|
|
454
|
+
["venue", "its bonding curve"],
|
|
455
|
+
["you send", buyPaymentLine(quoteIn, `the curve ${addr(curve.address)}`)],
|
|
456
|
+
["trade fee", `${money(quote.fee)} — a flat 1% of the input, taken before anything `
|
|
457
|
+
+ "reaches the reserve"],
|
|
458
|
+
["reaches the curve", money(quote.quoteSpent)],
|
|
459
|
+
["tokens out", qty(quote.tokensOut, symbol)],
|
|
460
|
+
["refund", quote.refund.isZero()
|
|
461
|
+
? "none"
|
|
462
|
+
: `${money(quote.refund)} — this buy fills the curve and is capped at the `
|
|
463
|
+
+ (state.quoteToken.isNative
|
|
464
|
+
? "remaining inventory, so the rest comes back"
|
|
465
|
+
: "remaining inventory, so the rest is never pulled")],
|
|
466
|
+
["average fill price", avg === undefined ? "n/a" : price(avg)],
|
|
467
|
+
]), section("the curve after this buy", [
|
|
468
|
+
["spot price", `${price(state.spotPrice)} → ${price(quote.newPrice)} `
|
|
469
|
+
+ `(${priceMove(state.spotPrice, quote.newPrice)})`],
|
|
470
|
+
["real reserve", `${money(state.realReserve)} → ${money(quote.newReserve)} `
|
|
471
|
+
+ `of ${money(state.target)}`],
|
|
472
|
+
["tokens sold", `${qty(state.tokensSold)} → ${qty(quote.newTokensSold)}`],
|
|
473
|
+
["graduates", quote.graduates ? "YES — this is the buy that ends trading" : "no"],
|
|
474
|
+
]), feeSplitSection(quote.fee, split, args.referrer, args.developer), section("minimum tokens out, by tolerance", TOLERANCES.map((bps) => {
|
|
475
|
+
const floor = Bps.of(10000n - bps).applyToTokens(quote.tokensOut);
|
|
476
|
+
return [`${bps} bps (${Number(bps) / 100}%)`, qty(floor, symbol)];
|
|
477
|
+
})), quote.graduates ? note(GRADUATING_BUY_WARNING) : undefined, note("This quote is one block old the moment it is returned. Any other trade against "
|
|
478
|
+
+ "this curve moves it, which is what the minimum-out floor is for.")),
|
|
479
|
+
};
|
|
480
|
+
},
|
|
481
|
+
});
|
|
482
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
483
|
+
const quoteSell = defineTool({
|
|
484
|
+
name: "arcnow_quote_sell",
|
|
485
|
+
title: "Quote a sell",
|
|
486
|
+
access: "read",
|
|
487
|
+
description: "What a given quantity of tokens would fetch right now, wherever the token trades: "
|
|
488
|
+
+ "gross proceeds, every fee and who receives it, net proceeds in the token's quote (native "
|
|
489
|
+
+ "USDC or an ERC-20 such as EURC), and the minimum-out floor at several tolerances.\n\n"
|
|
490
|
+
+ "Costs nothing and signs nothing.\n\n"
|
|
491
|
+
+ "ON A BONDING CURVE: selling needs NO approval, ever. The curve pulls the tokens "
|
|
492
|
+
+ "through a privileged path that reads no allowance at all, so a holder who has "
|
|
493
|
+
+ "approved nobody can always sell. If you find yourself suggesting an approve step "
|
|
494
|
+
+ "before a curve sell, you are fixing the wrong problem.\n\n"
|
|
495
|
+
+ "IN A UNISWAP V4 POOL (the token graduated and migrated) it is the other way round, and "
|
|
496
|
+
+ "worth telling a user before they sell: the router pulls the tokens with transferFrom, "
|
|
497
|
+
+ "so a pool sell needs an ERC-20 APPROVAL to the router first — a separate transaction. "
|
|
498
|
+
+ "This quote says it is a pool quote, shows arcnow.io's 1% and the pool's own LP fee "
|
|
499
|
+
+ "separately, the average fill against the pool's spot price, the price impact, and "
|
|
500
|
+
+ "how much `holder` has approved the router for already. A pool sell is priced as a "
|
|
501
|
+
+ "real holder, so `holder` must actually hold the tokens; on a writing server it "
|
|
502
|
+
+ "defaults to the signing address. referrer and developer are refused on a pool.\n\n"
|
|
503
|
+
+ "A token that graduated but never migrated trades nowhere; this says so and names "
|
|
504
|
+
+ "arcnow_migrate.",
|
|
505
|
+
input: {
|
|
506
|
+
address: addressArg("The curve address, or the token address. Either works."),
|
|
507
|
+
tokensIn: decimalArg("How many whole tokens to sell, as a string — \"1000\", \"12.5\". Tokens are 18-decimal."),
|
|
508
|
+
holder: addressArg("POOL: the address whose tokens would be sold. A pool sell is priced by simulating the "
|
|
509
|
+
+ "real swap as that address, so it must actually hold the tokens. Defaults to the "
|
|
510
|
+
+ "signing address on a writing server; required on a read-only one. Not needed on a "
|
|
511
|
+
+ "bonding curve, where a sell is priced for nobody in particular.").optional(),
|
|
512
|
+
referrer: addressArg("CURVE ONLY: credited with the referral share of the fee. Refused for a token in its "
|
|
513
|
+
+ "pool.").optional(),
|
|
514
|
+
developer: addressArg("CURVE ONLY: credited with the developer share of the fee. Refused for a token in its "
|
|
515
|
+
+ "pool.").optional(),
|
|
516
|
+
},
|
|
517
|
+
async run(args, ctx) {
|
|
518
|
+
const market = await resolveMarket(ctx, args.address);
|
|
519
|
+
const { curve, state } = market;
|
|
520
|
+
const symbol = await ctx.port.token(state.token).symbol();
|
|
521
|
+
if (market.venue === "stranded") {
|
|
522
|
+
return { isError: true, text: strandedText("arcnow_quote_sell") };
|
|
523
|
+
}
|
|
524
|
+
if (market.venue === "pool")
|
|
525
|
+
return quotePoolSell(args, ctx, market, symbol);
|
|
526
|
+
const tokensIn = Tokens.parse(args.tokensIn);
|
|
527
|
+
const quote = await curve.quoteSell(tokensIn);
|
|
528
|
+
const split = await curve.previewFeeSplit(quote.fee, args.referrer, args.developer);
|
|
529
|
+
const avg = effectivePrice(quote.gross, tokensIn);
|
|
530
|
+
return {
|
|
531
|
+
text: report(`Sell quote — ${qty(tokensIn, symbol)} into curve ${addr(curve.address)}`, section("what you give and get", [
|
|
532
|
+
["venue", "its bonding curve"],
|
|
533
|
+
["you send", `${qty(tokensIn, symbol)} — no approval needed, and none will be asked for`],
|
|
534
|
+
["gross", money(quote.gross)],
|
|
535
|
+
["trade fee", `${money(quote.fee)} — the same flat 1%, taken from the proceeds`],
|
|
536
|
+
["you receive", money(quote.quoteOut.floorToRepresentable())],
|
|
537
|
+
["average fill price", avg === undefined ? "n/a" : price(avg)],
|
|
538
|
+
]), section("the curve after this sell", [
|
|
539
|
+
["spot price", `${price(state.spotPrice)} → ${price(quote.newPrice)} `
|
|
540
|
+
+ `(${priceMove(state.spotPrice, quote.newPrice)})`],
|
|
541
|
+
["real reserve", `${money(state.realReserve)} → ${money(quote.newReserve)}`],
|
|
542
|
+
["tokens sold", `${qty(state.tokensSold)} → ${qty(quote.newTokensSold)}`],
|
|
543
|
+
]), feeSplitSection(quote.fee, split, args.referrer, args.developer), section(`minimum ${state.quoteToken.symbol} out, by tolerance`, TOLERANCES.map((bps) => {
|
|
544
|
+
const floor = Bps.of(10000n - bps).applyToQuote(quote.quoteOut);
|
|
545
|
+
return [`${bps} bps (${Number(bps) / 100}%)`, money(floor)];
|
|
546
|
+
})), args.holder === undefined
|
|
547
|
+
? undefined
|
|
548
|
+
: note("holder was not needed for this quote: a bonding-curve sell is priced for "
|
|
549
|
+
+ "nobody in particular, and needs no approval from anybody."), note("A sell moves the price down across your own order the same way a buy moves it up. "
|
|
550
|
+
+ "This quote is a snapshot of one block.")),
|
|
551
|
+
};
|
|
552
|
+
},
|
|
553
|
+
});
|
|
554
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
555
|
+
const quoteLaunch = defineTool({
|
|
556
|
+
name: "arcnow_quote_launch",
|
|
557
|
+
title: "Quote a launch",
|
|
558
|
+
access: "read",
|
|
559
|
+
description: "What launching a token would cost, before anything is spent: the flat launch fee, the "
|
|
560
|
+
+ "initial buy, the 1% trade fee the initial buy itself pays, the tokens the creator "
|
|
561
|
+
+ "would receive, and — given a creator address — the exact addresses the token and its "
|
|
562
|
+
+ "curve would land at.\n\n"
|
|
563
|
+
+ "Costs nothing and signs nothing. Always call this before arcnow_launch and show the "
|
|
564
|
+
+ "user the total, because a launch is irreversible: the name, symbol, supply, curve "
|
|
565
|
+
+ "shape and graduation venue are fixed at that transaction and can never be changed "
|
|
566
|
+
+ "afterwards, by anyone, including whoever launched it.\n\n"
|
|
567
|
+
+ "THE QUOTE. A token is priced for life in the quote it launches in: `quote` names it by "
|
|
568
|
+
+ "symbol or address — native USDC by default, or an ERC-20 such as EURC that the quote "
|
|
569
|
+
+ "registry accepts (arcnow_quote_tokens). The launch fee and the initial buy are both in that "
|
|
570
|
+
+ "quote, and so is every trade in the token afterwards. Native USDC is paid as msg.value; an "
|
|
571
|
+
+ "ERC-20 is pulled by the launchpad after an exact approval.\n\n"
|
|
572
|
+
+ "The total is EXACTLY what a launch requires, not a minimum — for native USDC the launchpad "
|
|
573
|
+
+ "reverts on an overpayment as readily as an underpayment, because it has no refund path.",
|
|
574
|
+
input: {
|
|
575
|
+
name: textArg(64, "The token's name. Permanent — there is no setter for it, on any contract, ever."),
|
|
576
|
+
symbol: textArg(16, "The token's ticker. Permanent, and not unique: two tokens may share a symbol, so an "
|
|
577
|
+
+ "address is the only identifier that means anything."),
|
|
578
|
+
metadataUri: metadataUriArg(),
|
|
579
|
+
initialBuy: decimalArg("How much of the launch's quote the creator spends buying their own token in the launch "
|
|
580
|
+
+ "transaction, in whole units, with no more decimals than the quote has. It is an "
|
|
581
|
+
+ "ORDINARY buy: it runs through the curve and pays the 1% trade fee on top of the flat "
|
|
582
|
+
+ "launch fee. There is no fee-free entry into a curve. \"0\" is allowed."),
|
|
583
|
+
quote: quoteArg(),
|
|
584
|
+
creator: addressArg("Optional: the address that would launch. Given one, this also predicts the token and "
|
|
585
|
+
+ "curve addresses — valid only for that address's CURRENT launch nonce and exactly "
|
|
586
|
+
+ "these parameters.").optional(),
|
|
587
|
+
platform: addressArg("Optional: the PlatformConfig to launch under. Defaults to arcnow.io's own, which is "
|
|
588
|
+
+ "what almost every launch uses.").optional(),
|
|
589
|
+
migrator: addressArg("Optional: where this token graduates to, snapshotted into the curve at launch and "
|
|
590
|
+
+ "immutable thereafter. Defaults to the platform's default migrator.").optional(),
|
|
591
|
+
},
|
|
592
|
+
async run(args, ctx) {
|
|
593
|
+
const choice = chooseQuote(ctx.port.config, args.quote);
|
|
594
|
+
// A quote the network does not list is metadata the SDK can read, once and
|
|
595
|
+
// cached — but no spend cap can name it, so arcnow_launch will refuse it.
|
|
596
|
+
const quoteToken = choice.listed
|
|
597
|
+
? choice.token
|
|
598
|
+
: await ctx.port.quoteTokenInfo(choice.address);
|
|
599
|
+
const params = launchParams(args, quoteToken);
|
|
600
|
+
const quote = await ctx.port.launchpad.quoteLaunch(params);
|
|
601
|
+
const predicted = args.creator === undefined
|
|
602
|
+
? undefined
|
|
603
|
+
: await ctx.port.launchpad.predictAddresses(args.creator, params);
|
|
604
|
+
const platform = (args.platform ?? ctx.port.config.contracts.arcnowPlatform);
|
|
605
|
+
const [settings, template] = await Promise.all([
|
|
606
|
+
ctx.port.platforms.settings(platform).catch(() => undefined),
|
|
607
|
+
ctx.port.platforms.curveParametersFor(platform, quoteToken).catch(() => undefined),
|
|
608
|
+
]);
|
|
609
|
+
const migrator = (args.migrator ?? settings?.defaultMigrator);
|
|
610
|
+
const launchpad = addr(ctx.port.launchpad.address);
|
|
611
|
+
return {
|
|
612
|
+
text: report(`Launch quote — ${args.name} (${args.symbol}), in ${quoteToken.symbol}`, section("what it would cost", [
|
|
613
|
+
["total, exactly", quoteToken.isNative
|
|
614
|
+
? `${money(quote.totalCost)} — this is the exact value the launch transaction must `
|
|
615
|
+
+ "carry. Overpaying reverts; there is no refund path."
|
|
616
|
+
: `${money(quote.totalCost)} — exactly what the launchpad pulls, no more`],
|
|
617
|
+
[" launch fee", `${money(quote.launchFee)} — flat, to the launchpad, as the quote registry `
|
|
618
|
+
+ `sets it for ${quoteToken.symbol} now`],
|
|
619
|
+
[" initial buy", money(quote.initialBuy)],
|
|
620
|
+
[" of which fee", `${money(quote.tradeFee)} — the initial buy's own 1%`],
|
|
621
|
+
["paid as", quoteToken.isNative
|
|
622
|
+
? `msg.value: exactly ${money(quote.nativeValue)}`
|
|
623
|
+
: `an ERC-20 pull by the launchpad — the transaction carries no value. An exact approve `
|
|
624
|
+
+ `of ${money(quote.totalCost)} to the launchpad ${launchpad} is sent first when the `
|
|
625
|
+
+ "allowance falls short: a separate transaction, never for more"],
|
|
626
|
+
["quote", quoteLine(quoteToken)],
|
|
627
|
+
["tokens received", qty(quote.tokensOut, args.symbol)],
|
|
628
|
+
["plus gas", "paid in native USDC, the gas currency, whatever the quote"],
|
|
629
|
+
["graduates at launch", quote.graduates
|
|
630
|
+
? "YES — the initial buy fills the curve, so the token graduates and migrates in "
|
|
631
|
+
+ "the launch transaction itself. The SDK sends it with an explicit 8,000,000 gas "
|
|
632
|
+
+ "limit, because an estimated limit starves the migration silently."
|
|
633
|
+
: "no"],
|
|
634
|
+
]), section("what would be fixed forever", [
|
|
635
|
+
["name / symbol", `${args.name} / ${args.symbol}`],
|
|
636
|
+
["metadata", args.metadataUri],
|
|
637
|
+
["platform", `${addr(platform)}${args.platform === undefined ? " (the default)" : ""}`],
|
|
638
|
+
["graduation venue", migrator === undefined
|
|
639
|
+
? "the platform's default"
|
|
640
|
+
: `${addr(migrator)} — ${venueOf(migrator, ctx.port.config)}`],
|
|
641
|
+
["curve shape", template === undefined
|
|
642
|
+
? `the platform's ${quoteToken.symbol} template, which could not be read`
|
|
643
|
+
: `${qty(template.totalSupply)} total supply, `
|
|
644
|
+
+ `${qty(template.curveSupply)} on the curve, `
|
|
645
|
+
+ `graduating at ${money(template.target)}, `
|
|
646
|
+
+ `y0 ${qty(template.y0)} (the virtual token reserve)`],
|
|
647
|
+
]), predicted === undefined
|
|
648
|
+
? undefined
|
|
649
|
+
: section("where it would land", [
|
|
650
|
+
["token", addr(predicted.token)],
|
|
651
|
+
["curve", addr(predicted.curve)],
|
|
652
|
+
["valid for", `${addr(args.creator)} at its current launch nonce, with `
|
|
653
|
+
+ "exactly these parameters. Any other launch by that address first, and these "
|
|
654
|
+
+ "move."],
|
|
655
|
+
]), choice.listed
|
|
656
|
+
? undefined
|
|
657
|
+
: note(`${quoteToken.symbol} at ${addr(quoteToken.address)} is not one of this network's `
|
|
658
|
+
+ "quote tokens, so this server's operator can set no spend cap for it and "
|
|
659
|
+
+ "arcnow_launch will refuse to launch in it. Whether the chain would accept it is "
|
|
660
|
+
+ "arcnow_quote_tokens' answer."), note("Nothing has been spent and nothing has been signed. A launch is irreversible and "
|
|
661
|
+
+ "none of the parameters above can be changed afterwards — not by the creator, not "
|
|
662
|
+
+ "by the platform, not by anyone. Show this total to the person whose money it is "
|
|
663
|
+
+ "before calling arcnow_launch.")),
|
|
664
|
+
};
|
|
665
|
+
},
|
|
666
|
+
});
|
|
667
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
668
|
+
/** The launch tools' `quote` argument. */
|
|
669
|
+
export function quoteArg() {
|
|
670
|
+
return z.string().min(1).max(64).optional().describe("Optional: the quote token to launch in, by symbol (\"USDC\", \"EURC\") or by address. "
|
|
671
|
+
+ "Defaults to native USDC. The token is priced in it FOR LIFE: its launch fee, its initial "
|
|
672
|
+
+ "buy, and every trade in it afterwards. A symbol is looked up in this network's own quote "
|
|
673
|
+
+ "tokens only. arcnow_quote_tokens lists them with whether the quote registry accepts each "
|
|
674
|
+
+ "and this server's spend cap for it; a quote with no cap cannot be launched in here.");
|
|
675
|
+
}
|
|
676
|
+
const platform = defineTool({
|
|
677
|
+
name: "arcnow_platform",
|
|
678
|
+
title: "Inspect a platform's fees and defaults",
|
|
679
|
+
access: "read",
|
|
680
|
+
description: "One platform's configuration: who administers it, who receives its cut, how the 1% "
|
|
681
|
+
+ "trade fee is divided between creator, referrer, developer, the platform and the "
|
|
682
|
+
+ "protocol, which migrator it launches tokens into by default, and the curve template "
|
|
683
|
+
+ "it stamps onto every token it launches.\n\n"
|
|
684
|
+
+ "With no address, reports arcnow.io's own platform — the default a launch uses.\n\n"
|
|
685
|
+
+ "The single easiest thing to misread here is what a share means. Every share is basis "
|
|
686
|
+
+ "points OF THE FEE, never of the trade. A creator share of 3000 bps is 30% of the fee "
|
|
687
|
+
+ "and 0.30% of the trade. This tool prints both, every time. The platform's own share "
|
|
688
|
+
+ "is never configured: it is the residual, whatever is left after the protocol, "
|
|
689
|
+
+ "creator, referrer and developer shares.",
|
|
690
|
+
input: {
|
|
691
|
+
address: addressArg("Optional: the PlatformConfig address. Defaults to arcnow.io's own platform.").optional(),
|
|
692
|
+
},
|
|
693
|
+
async run(args, ctx) {
|
|
694
|
+
const address = (args.address ?? ctx.port.config.contracts.arcnowPlatform);
|
|
695
|
+
const isPlatform = await ctx.port.platforms.isPlatform(address);
|
|
696
|
+
if (!isPlatform) {
|
|
697
|
+
return {
|
|
698
|
+
isError: true,
|
|
699
|
+
text: `${addr(address)} is not a platform this registry certifies. The registry `
|
|
700
|
+
+ "deploys every PlatformConfig itself, precisely so that membership certifies code "
|
|
701
|
+
+ "rather than a claim — so an address that answers 'no' here is not a platform, "
|
|
702
|
+
+ "whatever it says about itself.",
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
const [settings, fees, protocol, template] = await Promise.all([
|
|
706
|
+
ctx.port.platforms.settings(address),
|
|
707
|
+
ctx.port.platforms.feeConfigFor(address),
|
|
708
|
+
ctx.port.platforms.protocolSummary(),
|
|
709
|
+
ctx.port.platforms.curveParametersFor(address),
|
|
710
|
+
]);
|
|
711
|
+
const isDefault = address.toLowerCase()
|
|
712
|
+
=== ctx.port.config.contracts.arcnowPlatform.toLowerCase();
|
|
713
|
+
return {
|
|
714
|
+
text: report(`Platform ${addr(address)}${isDefault ? " — arcnow.io's own, the launch default" : ""}`, section("who runs it", [
|
|
715
|
+
["admin", addr(settings.admin)],
|
|
716
|
+
["fee recipient", addr(settings.feeRecipient)],
|
|
717
|
+
["certified", "yes — this registry deployed this PlatformConfig itself"],
|
|
718
|
+
]), section("how the 1% trade fee is divided", [
|
|
719
|
+
["creator", share(settings.creatorShareBps, TRADE_FEE)],
|
|
720
|
+
["referrer", share(settings.refShareBps, TRADE_FEE)],
|
|
721
|
+
["developer", share(settings.devShareBps, TRADE_FEE)],
|
|
722
|
+
["protocol", share(fees.protocolShareBps, TRADE_FEE)],
|
|
723
|
+
["platform", `${share(settings.platformShareBps, TRADE_FEE)} — the RESIDUAL. Not an `
|
|
724
|
+
+ "input anywhere in the contracts; it is 10000 minus the four above, computed on "
|
|
725
|
+
+ "demand."],
|
|
726
|
+
["", "A share whose address is zero at swap time — no referrer, no developer — is "
|
|
727
|
+
+ "paid to the platform instead, as is the rounding dust."],
|
|
728
|
+
]), section("defaults it stamps on a launch", [
|
|
729
|
+
["migrator", `${addr(settings.defaultMigrator)} — `
|
|
730
|
+
+ `${venueOf(settings.defaultMigrator, ctx.port.config)}. Snapshotted into each `
|
|
731
|
+
+ "curve at launch and immutable from then on."],
|
|
732
|
+
["platform version", settings.version],
|
|
733
|
+
["template", `${template.quoteToken.symbol}'s — a platform serves one template per quote `
|
|
734
|
+
+ "token it enables; arcnow_quote_launch with `quote` shows another quote's"],
|
|
735
|
+
["total supply", qty(template.totalSupply)],
|
|
736
|
+
["on the curve", qty(template.curveSupply)],
|
|
737
|
+
["held back for the pool", qty(template.totalSupply.sub(template.curveSupply))],
|
|
738
|
+
["graduation target", money(template.target)],
|
|
739
|
+
["initial price", price(template.initialPrice)],
|
|
740
|
+
["y0 (virtual token reserve)", `${qty(template.y0)} — y0Wad ${template.y0.wad}`],
|
|
741
|
+
]), section("the protocol, above every platform", [
|
|
742
|
+
["share", share(protocol.protocolShareBps, TRADE_FEE)],
|
|
743
|
+
["recipient", addr(protocol.protocolRecipient)],
|
|
744
|
+
["launch fee", `${money(protocol.launchFee)} — flat, per launch in native USDC, on top of `
|
|
745
|
+
+ "any initial buy. Each quote token has its own: arcnow_quote_tokens"],
|
|
746
|
+
])),
|
|
747
|
+
};
|
|
748
|
+
},
|
|
749
|
+
});
|
|
750
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
751
|
+
const listPlatforms = defineTool({
|
|
752
|
+
name: "arcnow_list_platforms",
|
|
753
|
+
title: "List registered platforms",
|
|
754
|
+
access: "read",
|
|
755
|
+
description: "Every platform the registry has deployed, with its fee split and default graduation "
|
|
756
|
+
+ "venue. Unlike arcnow_list_tokens this is a real enumeration held in contract storage, "
|
|
757
|
+
+ "so it is complete — there is no scan window and nothing is missed.\n\n"
|
|
758
|
+
+ "Use it to answer 'what can I launch under' or 'who else builds on this'. Launching "
|
|
759
|
+
+ "does not require your own platform; arcnow.io's is the default and is one entry here "
|
|
760
|
+
+ "among the others, not a privileged one.",
|
|
761
|
+
input: {
|
|
762
|
+
limit: intArg(1, 50, 20, "How many platforms to return."),
|
|
763
|
+
offset: intArg(0, 1_000_000, 0, "How many to skip, for paging through a long registry."),
|
|
764
|
+
},
|
|
765
|
+
async run(args, ctx) {
|
|
766
|
+
const count = await ctx.port.platforms.platformCount();
|
|
767
|
+
const start = BigInt(args.offset);
|
|
768
|
+
if (start >= count) {
|
|
769
|
+
return {
|
|
770
|
+
text: `This registry has ${count} platform(s); offset ${args.offset} is past the end.`,
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
const end = start + BigInt(args.limit) > count ? count : start + BigInt(args.limit);
|
|
774
|
+
const blocks = [];
|
|
775
|
+
for (let i = start; i < end; i += 1n) {
|
|
776
|
+
const address = await ctx.port.platforms.platformAt(i);
|
|
777
|
+
const isDefault = address.toLowerCase()
|
|
778
|
+
=== ctx.port.config.contracts.arcnowPlatform.toLowerCase();
|
|
779
|
+
try {
|
|
780
|
+
const settings = await ctx.port.platforms.settings(address);
|
|
781
|
+
blocks.push(section(`#${i} ${addr(address)}${isDefault ? " (arcnow.io's own)" : ""}`, [
|
|
782
|
+
["fee recipient", addr(settings.feeRecipient)],
|
|
783
|
+
["creator / ref / dev / platform", `${settings.creatorShareBps.bps} / `
|
|
784
|
+
+ `${settings.refShareBps.bps} / ${settings.devShareBps.bps} / `
|
|
785
|
+
+ `${settings.platformShareBps.bps} bps of the fee`],
|
|
786
|
+
["default venue", `${addr(settings.defaultMigrator)} — `
|
|
787
|
+
+ venueOf(settings.defaultMigrator, ctx.port.config)],
|
|
788
|
+
["graduation target", "per quote token — arcnow_platform shows it"],
|
|
789
|
+
]));
|
|
790
|
+
}
|
|
791
|
+
catch (error) {
|
|
792
|
+
blocks.push(section(`#${i} ${addr(address)}`, [
|
|
793
|
+
["settings", `could not be read: ${describe(error)}`],
|
|
794
|
+
]));
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
return {
|
|
798
|
+
text: report(`${count} platform(s) registered; showing ${start}–${end - 1n}.`, ...blocks),
|
|
799
|
+
};
|
|
800
|
+
},
|
|
801
|
+
});
|
|
802
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
803
|
+
const quoteTokens = defineTool({
|
|
804
|
+
name: "arcnow_quote_tokens",
|
|
805
|
+
title: "Quote tokens a launch may use",
|
|
806
|
+
access: "read",
|
|
807
|
+
description: "The quote tokens a token can be launched in — what its bonding curve, and later its pool, "
|
|
808
|
+
+ "is priced in for life: native USDC (the gas currency, 18 decimals, paid as msg.value) and "
|
|
809
|
+
+ "the ERC-20s arcnow.io's quote registry allowlists, such as EURC (6 decimals, pulled with "
|
|
810
|
+
+ "an exact ERC-20 approval). For each: symbol, name, decimals, address, whether it is "
|
|
811
|
+
+ "native, its flat launch fee in its own units, whether the registry accepts it for new "
|
|
812
|
+
+ "launches now, and this server's spend cap for it.\n\n"
|
|
813
|
+
+ "SPEND CAPS ARE PER QUOTE, AND FAIL CLOSED. The operator caps native USDC with "
|
|
814
|
+
+ "ARCNOW_MCP_MAX_SPEND_USDC (default 100) and every other quote with "
|
|
815
|
+
+ "ARCNOW_MCP_MAX_SPEND_<SYMBOL>, in that quote's own units (ARCNOW_MCP_MAX_SPEND_EURC=50). "
|
|
816
|
+
+ "A quote with no cap is REFUSED for every launch and buy, and a quote this network does not "
|
|
817
|
+
+ "list can have no cap at all. Tell the user which quotes they can actually spend here "
|
|
818
|
+
+ "before offering a launch or a buy in one.\n\n"
|
|
819
|
+
+ "Reads the registry in at most three eth_calls. If the chain has no quote registry to ask "
|
|
820
|
+
+ "— the 2.x contracts Arc testnet ran until the multi-quote reset have none — this says so, "
|
|
821
|
+
+ "shows the error, "
|
|
822
|
+
+ "and lists this network's own quote-token metadata instead, with NOTHING known to be "
|
|
823
|
+
+ "accepted. Takes no arguments and spends nothing.",
|
|
824
|
+
input: {},
|
|
825
|
+
async run(_args, ctx) {
|
|
826
|
+
let entries;
|
|
827
|
+
try {
|
|
828
|
+
entries = await ctx.port.quoteRegistry.list();
|
|
829
|
+
}
|
|
830
|
+
catch (error) {
|
|
831
|
+
return {
|
|
832
|
+
text: report("The quote registry could not be read, so which quote tokens a launch accepts is NOT "
|
|
833
|
+
+ "known. Below is this network's own quote-token metadata: what a token could be "
|
|
834
|
+
+ "priced in, not what the launchpad takes.", renderError("arcnow_quote_tokens", error), note("Do not tell a user a quote is accepted on the strength of this list. A launch in "
|
|
835
|
+
+ "a quote the registry does not accept reverts QuoteTokenNotSupported; the 2.x "
|
|
836
|
+
+ "contracts Arc testnet ran until the multi-quote reset have no registry at all and "
|
|
837
|
+
+ "accept only native USDC."), ...ctx.port.config.quoteTokens.map((token) => section(`${token.symbol} — ${token.name}`, [
|
|
838
|
+
...quoteTokenRows(token),
|
|
839
|
+
["launch fee", "unknown — the registry could not be read"],
|
|
840
|
+
["accepted", "unknown — the registry could not be read"],
|
|
841
|
+
["spend cap", capText(ctx, token)],
|
|
842
|
+
]))),
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
return {
|
|
846
|
+
text: report(`${entries.length} quote token(s) registered with this launchpad's quote registry.`, ...entries.map(({ token, launchFee, active }) => section(`${token.symbol} — ${token.name}`, [
|
|
847
|
+
...quoteTokenRows(token),
|
|
848
|
+
["launch fee", `${money(launchFee)} — flat, per launch in ${token.symbol}`],
|
|
849
|
+
["active", active
|
|
850
|
+
? "yes — a launch may use it"
|
|
851
|
+
: "NO — deregistered: no new launch may use it. Curves already priced in it keep "
|
|
852
|
+
+ "trading"],
|
|
853
|
+
["spend cap", capText(ctx, token)],
|
|
854
|
+
])), ctx.config.mode === "write"
|
|
855
|
+
? undefined
|
|
856
|
+
: note("This server is read-only, so it spends nothing in any quote; the caps above are "
|
|
857
|
+
+ "what it would enforce with writes enabled.")),
|
|
858
|
+
};
|
|
859
|
+
},
|
|
860
|
+
});
|
|
861
|
+
function quoteTokenRows(token) {
|
|
862
|
+
return [
|
|
863
|
+
["address", addr(token.address)],
|
|
864
|
+
["decimals", String(token.decimals)],
|
|
865
|
+
["kind", token.isNative
|
|
866
|
+
? "native — the gas currency, paid as msg.value, needing no approval"
|
|
867
|
+
: "ERC-20 — pulled with an exact ERC-20 approval of each spend, never unlimited"],
|
|
868
|
+
];
|
|
869
|
+
}
|
|
870
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
871
|
+
// helpers shared with the write tools
|
|
872
|
+
export const GRADUATING_BUY_WARNING = "THIS BUY GRADUATES THE CURVE, AND THE GAS LIMIT DECIDES WHETHER A MARKET EXISTS. "
|
|
873
|
+
+ "The graduating buy migrates the curve in its own transaction, under a bounded gas "
|
|
874
|
+
+ "budget whose failure the curve CATCHES rather than reverting. So eth_estimateGas — "
|
|
875
|
+
+ "which searches for the lowest limit at which the transaction still succeeds — converges "
|
|
876
|
+
+ "on exactly the limit at which the migration is starved. The buy fills, the curve "
|
|
877
|
+
+ "graduates, the refund is correct, and the pool is simply never created. There is no "
|
|
878
|
+
+ "revert and no error anywhere. The curve budgets 6,000,000 for the migrator and keeps "
|
|
879
|
+
+ "100,000 back, so a graduating buy needs comfortably more than 6.1M; this server sends "
|
|
880
|
+
+ "8,000,000 on a buy it can see will graduate. If a pool ends up missing anyway, "
|
|
881
|
+
+ "migrate() is permissionless and arcnow_migrate finishes it for the cost of gas.";
|
|
882
|
+
export function statusLine(graduated, migrated) {
|
|
883
|
+
if (!graduated)
|
|
884
|
+
return "trading on the curve";
|
|
885
|
+
if (migrated) {
|
|
886
|
+
return "graduated and migrated — trades in its Uniswap v4 pool, and the quote and trade "
|
|
887
|
+
+ "tools route there";
|
|
888
|
+
}
|
|
889
|
+
return "GRADUATED but NOT MIGRATED — trading is over and the pool was never created, so it "
|
|
890
|
+
+ "trades nowhere. migrate() is permissionless; anyone can finish it.";
|
|
891
|
+
}
|
|
892
|
+
export function feeSplitSection(fee, split, referrer, developer) {
|
|
893
|
+
const rows = [
|
|
894
|
+
["creator", `${money(split.creatorAmount)} → ${addr(split.creator)}`],
|
|
895
|
+
["platform", `${money(split.platformAmount)} → ${addr(split.platform)}`],
|
|
896
|
+
["referrer", referrer === undefined
|
|
897
|
+
? `${money(split.refAmount)} → no referrer given, so this goes to the platform`
|
|
898
|
+
: `${money(split.refAmount)} → ${addr(split.ref)}`],
|
|
899
|
+
["developer", developer === undefined
|
|
900
|
+
? `${money(split.devAmount)} → no developer given, so this goes to the platform`
|
|
901
|
+
: `${money(split.devAmount)} → ${addr(split.dev)}`],
|
|
902
|
+
["protocol", `${money(split.protocolAmount)} → ${addr(split.protocol)}`],
|
|
903
|
+
];
|
|
904
|
+
return section(`fee split — where the ${money(fee)} goes`, rows);
|
|
905
|
+
}
|
|
906
|
+
function describe(error) {
|
|
907
|
+
return error instanceof Error ? error.message : String(error);
|
|
908
|
+
}
|
|
909
|
+
export const READ_TOOLS = [
|
|
910
|
+
network,
|
|
911
|
+
quoteTokens,
|
|
912
|
+
listTokens,
|
|
913
|
+
tokenTool,
|
|
914
|
+
quoteBuy,
|
|
915
|
+
quoteSell,
|
|
916
|
+
quoteLaunch,
|
|
917
|
+
platform,
|
|
918
|
+
listPlatforms,
|
|
919
|
+
];
|
|
920
|
+
//# sourceMappingURL=read.js.map
|