@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,950 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The write tools: everything that can cost somebody something.
|
|
3
|
+
*
|
|
4
|
+
* # What guards these, in order
|
|
5
|
+
*
|
|
6
|
+
* 1. **The operator's opt-in.** These are not published at all unless the
|
|
7
|
+
* process was started with `--allow-writes` and a key in the environment.
|
|
8
|
+
* A model cannot turn them on, and asking for a key will not help it: see
|
|
9
|
+
* `config.ts`.
|
|
10
|
+
* 2. **The operator's ceilings.** One per quote token — `ARCNOW_MCP_MAX_SPEND_USDC`
|
|
11
|
+
* for native USDC, `ARCNOW_MCP_MAX_SPEND_<SYMBOL>` for each other quote, in its
|
|
12
|
+
* own units — bound every single call, whatever an argument says. A quote with
|
|
13
|
+
* no cap is refused outright. See `./spend.ts`.
|
|
14
|
+
* 3. **The caller's stated ceiling.** Every spending tool requires
|
|
15
|
+
* `maxTotalCost`, in the token's quote: the most the caller believes this call will cost. A
|
|
16
|
+
* fresh quote is taken inside the call and compared against it, so a quote
|
|
17
|
+
* that went stale between being shown to a person and being acted on is
|
|
18
|
+
* caught, and so is a model that was talked into a bigger number than the
|
|
19
|
+
* conversation agreed to.
|
|
20
|
+
* 4. **Nothing that does not apply.** A buy or sell goes wherever the token
|
|
21
|
+
* trades — its curve or its pool, as `client.trade(token)` decides — and a
|
|
22
|
+
* parameter the venue has no notion of is refused before anything is
|
|
23
|
+
* quoted or sent, never silently dropped. See `./venue.ts`.
|
|
24
|
+
* 5. **The report.** Every one of these states the exact cost and the exact,
|
|
25
|
+
* permanent effect before it sends anything, and says afterwards what
|
|
26
|
+
* actually happened — including the two things about graduation that are
|
|
27
|
+
* invisible from a receipt.
|
|
28
|
+
*
|
|
29
|
+
* # The one write here that grants rights rather than spending money
|
|
30
|
+
*
|
|
31
|
+
* A sell in a Uniswap v4 pool needs an ERC-20 approval to the router first: a
|
|
32
|
+
* separate transaction giving the router the right to move the seller's
|
|
33
|
+
* tokens. `arcnow_sell` never sends one unless the call says
|
|
34
|
+
* `approveRouter: true`; never for more than the amount being sold; never when
|
|
35
|
+
* the existing allowance already covers the sale; and always reports it —
|
|
36
|
+
* token, owner, spender, amount, transaction, the allowance left afterwards —
|
|
37
|
+
* including when the sell that followed it then failed.
|
|
38
|
+
*
|
|
39
|
+
* None of that makes an assistant trustworthy with money. It makes the blast
|
|
40
|
+
* radius a number somebody chose.
|
|
41
|
+
*
|
|
42
|
+
* @module
|
|
43
|
+
*/
|
|
44
|
+
import { Bps, CurveTemplate, Deadline, GRADUATION_GAS_FLOOR, GRADUATION_GAS_LIMIT, minQuoteOutFromQuote, minTokensOutFromQuote, platformShareBps, Tokens, TRADE_FEE_BPS, validateNewPlatform, } from "@arcnow/sdk";
|
|
45
|
+
import { addr, effectivePrice, money, note, price, progress, qty, quoteLine, report, section, share, venueOf, ZERO_ADDRESS, } from "../format.js";
|
|
46
|
+
import { addressArg, decimalArg, defineTool, intArg, maxCostArg, metadataUriArg, slippageArg, textArg, z, } from "./schema.js";
|
|
47
|
+
import { renderError, renderPoolError } from "./errors.js";
|
|
48
|
+
import { launchParams } from "./launch-params.js";
|
|
49
|
+
import { lpFee } from "./pool.js";
|
|
50
|
+
import { chooseQuote, parseQuoteAmount } from "./quote.js";
|
|
51
|
+
import { GRADUATING_BUY_WARNING, quoteArg } from "./read.js";
|
|
52
|
+
import { refuseIfOverBudget, refuseWithoutCap } from "./spend.js";
|
|
53
|
+
import { resolveCurve } from "./resolve.js";
|
|
54
|
+
import { payee, refuseCurveApproval, refuseCurveOnly, refusePoolOnlyRecipient, resolveMarket, routerName, strandedText, venueMovedText, } from "./venue.js";
|
|
55
|
+
const TRADE_FEE = Bps.of(TRADE_FEE_BPS);
|
|
56
|
+
// GRADUATION_GAS_LIMIT (8,000,000) and GRADUATION_GAS_FLOOR (6,200,000) are the
|
|
57
|
+
// SDK's own since sdk#2, which gives a graduating launch the same explicit
|
|
58
|
+
// limit. One copy of the number that decides whether a market is created.
|
|
59
|
+
function deadlineArg() {
|
|
60
|
+
return intArg(1, 60, 5, "Minutes until the transaction may no longer be executed. Five is the sensible default. "
|
|
61
|
+
+ "A transaction with no deadline can be held back and executed at a much later price, "
|
|
62
|
+
+ "on a curve or in a pool alike.");
|
|
63
|
+
}
|
|
64
|
+
/** arcnow.io's 1% as a pool trade's receipt records it, including a trade too small to charge. */
|
|
65
|
+
function poolFeeTaken(fee) {
|
|
66
|
+
if (fee.isZero()) {
|
|
67
|
+
return "none — this trade was too small for arcnow.io's fee hook to charge: its 1% floors "
|
|
68
|
+
+ "to zero, and the receipt carries no fee log";
|
|
69
|
+
}
|
|
70
|
+
return `${money(fee)} — the 1% arcnow.io's fee hook took, from its own HookFeeTaken log in the `
|
|
71
|
+
+ "receipt; accrued by the hook as a PoolManager claim, and paid out to the fee recipients "
|
|
72
|
+
+ "at the start of a later swap";
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Fees from EARLIER trades that this swap paid out: the hook's `FeesDistributed`
|
|
76
|
+
* logs for the pool. Never part of the trader's fill.
|
|
77
|
+
*/
|
|
78
|
+
function earlierFeesPaidOut(distributed) {
|
|
79
|
+
if (distributed.isZero())
|
|
80
|
+
return "none — nothing accrued by earlier trades was waiting";
|
|
81
|
+
return `${money(distributed)} — fees earlier trades accrued, paid to the fee recipients by the `
|
|
82
|
+
+ "fee hook at the start of this swap. It moved the hook's claim, not your money, and is not "
|
|
83
|
+
+ "part of your fill";
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The ERC-20 approve an ERC-20 spend needed: sent (with its transaction) or not.
|
|
87
|
+
* `undefined` for native USDC, which is msg.value and has no approval at all.
|
|
88
|
+
*/
|
|
89
|
+
function quoteApprovalSection(amount, spender, hash) {
|
|
90
|
+
if (amount.token.isNative)
|
|
91
|
+
return undefined;
|
|
92
|
+
const { symbol } = amount.token;
|
|
93
|
+
return section(`${symbol} approval`, hash === undefined
|
|
94
|
+
? [["approve", `none sent — the existing ${symbol} allowance to ${spender} already covered `
|
|
95
|
+
+ `${money(amount)}, so no approval transaction was needed`]]
|
|
96
|
+
: [
|
|
97
|
+
["approve", `SENT first, as its own transaction: exactly ${money(amount)} to ${spender} — `
|
|
98
|
+
+ "the spend itself, never unlimited, and used up by it"],
|
|
99
|
+
["transaction", hash],
|
|
100
|
+
]);
|
|
101
|
+
}
|
|
102
|
+
/** The "gas limit sent" row of a curve buy: what went out, and who chose it. */
|
|
103
|
+
function gasLimitSentLine(erc20, graduates, caller, sent) {
|
|
104
|
+
if (sent === undefined) {
|
|
105
|
+
return erc20
|
|
106
|
+
? "estimated by the node, with the SDK's headroom for the ERC-20 fee-share transfers added "
|
|
107
|
+
+ "on top — max(20%, 150,000) more (this buy did not graduate the curve)"
|
|
108
|
+
: "estimated by the node (this buy did not graduate the curve)";
|
|
109
|
+
}
|
|
110
|
+
if (erc20 && !graduates) {
|
|
111
|
+
return `at least ${sent}, raised by the SDK to the node's estimate plus max(20%, 150,000) if `
|
|
112
|
+
+ "that is higher; a higher limit is kept — unused gas is not charged";
|
|
113
|
+
}
|
|
114
|
+
if (erc20 && caller !== undefined && caller < sent) {
|
|
115
|
+
return `${sent}, explicitly — the ${caller} given was raised to GRADUATION_GAS_LIMIT so the `
|
|
116
|
+
+ "migration can run; unused gas is not charged";
|
|
117
|
+
}
|
|
118
|
+
return `${sent}, explicitly — unused gas is not charged`;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* The "gas limit sent" row of a pool swap. A native pool swap goes out at the
|
|
122
|
+
* node's estimate. An ERC-20 one gets the SDK's pool headroom, max(20%, 400,000):
|
|
123
|
+
* more than a curve trade's, because the estimate can miss the fee hook paying out
|
|
124
|
+
* an accrual that a front-running swap created, and each ERC-20 share it pushes
|
|
125
|
+
* needs 111,587 gas left before it (contracts security review L-1).
|
|
126
|
+
*/
|
|
127
|
+
function poolSwapGasLine(native, side) {
|
|
128
|
+
return native
|
|
129
|
+
? `estimated by the node — a pool ${side} cannot graduate anything, so an estimate is safe here`
|
|
130
|
+
: "estimated by the node, plus the SDK's headroom for a pool swap — max(20%, 400,000) more, "
|
|
131
|
+
+ "because the fee hook may pay out earlier fees in ERC-20 shares during the swap, each "
|
|
132
|
+
+ "needing gas an estimate can miss";
|
|
133
|
+
}
|
|
134
|
+
/** How a spend left the wallet, in a result: msg.value, or an ERC-20 pull by `puller`. */
|
|
135
|
+
function spentLine(amount, puller) {
|
|
136
|
+
return amount.token.isNative
|
|
137
|
+
? `${money(amount)} — as msg.value`
|
|
138
|
+
: `${money(amount)} — pulled by ${puller}; the transaction carried no value`;
|
|
139
|
+
}
|
|
140
|
+
/** The SDK sent the trade to the other venue: the token migrated between quote and send. */
|
|
141
|
+
function sentElsewhere(tool, venue, hash) {
|
|
142
|
+
return report(`${tool}: the SDK sent this trade to the token's ${venue}, not the venue it was quoted `
|
|
143
|
+
+ `on — the token migrated between the quote and the send. Transaction ${hash}.`, note("The minimum-out floor from the quote still bound the fill. Read the result back with "
|
|
144
|
+
+ "arcnow_token and the holder's balance rather than trusting any figure from here."));
|
|
145
|
+
}
|
|
146
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
147
|
+
const launch = defineTool({
|
|
148
|
+
name: "arcnow_launch",
|
|
149
|
+
title: "Launch a new token (IRREVERSIBLE)",
|
|
150
|
+
access: "write",
|
|
151
|
+
destructive: true,
|
|
152
|
+
description: "Launch a new token with its bonding curve. THIS SPENDS REAL MONEY AND CANNOT BE UNDONE.\n\n"
|
|
153
|
+
+ "What becomes permanent the moment this transaction mines, and can never be changed by "
|
|
154
|
+
+ "anyone — not the creator, not the platform, not arcnow.io: the name, the symbol, the "
|
|
155
|
+
+ "metadata URI, the total supply, the curve's shape and graduation target, and the venue "
|
|
156
|
+
+ "the token will graduate to. There is no setter for any of them. There is no admin key "
|
|
157
|
+
+ "that can fix a typo in the symbol. A token launched with the wrong name is wrong "
|
|
158
|
+
+ "forever and the only remedy is to launch another one and spend the fee again.\n\n"
|
|
159
|
+
+ "THE QUOTE is permanent too: `quote` (native USDC by default, or an ERC-20 such as EURC, by "
|
|
160
|
+
+ "symbol or address) prices the token for life. What it costs: a flat launch fee plus "
|
|
161
|
+
+ "whatever initial buy you specify, both in that quote, and the initial buy pays the "
|
|
162
|
+
+ "ordinary 1% trade fee on top — there is no fee-free entry into a curve. Native USDC is "
|
|
163
|
+
+ "sent as the exact msg.value; an ERC-20 is pulled by the launchpad, after an approve of "
|
|
164
|
+
+ "exactly the total, sent only when the allowance falls short and reported.\n\n"
|
|
165
|
+
+ "SPEND CAPS ARE PER QUOTE: the total is checked against the operator's cap for that quote "
|
|
166
|
+
+ "(ARCNOW_MCP_MAX_SPEND_USDC, or ARCNOW_MCP_MAX_SPEND_<SYMBOL>), and a quote with no cap — "
|
|
167
|
+
+ "or one this network does not list — is refused with nothing sent.\n\n"
|
|
168
|
+
+ "Call arcnow_quote_launch first, show the total and the permanent parameters to the "
|
|
169
|
+
+ "person whose money this is, and get their agreement to the specific name, symbol and "
|
|
170
|
+
+ "quote before calling this. Set maxTotalCost from the figure they agreed to.",
|
|
171
|
+
input: {
|
|
172
|
+
name: textArg(64, "The token's name. PERMANENT. Check the spelling with the user."),
|
|
173
|
+
symbol: textArg(16, "The token's ticker. PERMANENT. Check the spelling with the user."),
|
|
174
|
+
metadataUri: metadataUriArg(),
|
|
175
|
+
initialBuy: decimalArg("How much of the launch's quote to spend buying your own token in the same transaction, "
|
|
176
|
+
+ "as a string in whole units, with no more decimals than the quote has. \"0\" is allowed "
|
|
177
|
+
+ "and means launching without taking a position."),
|
|
178
|
+
quote: quoteArg(),
|
|
179
|
+
slippageBps: slippageArg(),
|
|
180
|
+
maxTotalCost: maxCostArg("the whole launch — launch fee plus initial buy"),
|
|
181
|
+
acknowledgeIrreversible: z.literal(true).describe("Must be true. By setting it you assert that the user has been shown the total cost "
|
|
182
|
+
+ "and the permanent parameters — name, symbol, supply, curve, graduation venue — and "
|
|
183
|
+
+ "has agreed to those exact values. This is the one action on this server that cannot "
|
|
184
|
+
+ "be undone or adjusted afterwards in any way."),
|
|
185
|
+
platform: addressArg("Optional: the PlatformConfig to launch under. Defaults to arcnow.io's own.").optional(),
|
|
186
|
+
migrator: addressArg("Optional: the graduation venue, snapshotted into the curve and immutable thereafter. "
|
|
187
|
+
+ "Defaults to the platform's default, which is what almost every launch uses.").optional(),
|
|
188
|
+
},
|
|
189
|
+
async run(args, ctx) {
|
|
190
|
+
const what = `launching ${args.symbol}`;
|
|
191
|
+
// The quote and its cap first, before anything is read: a quote with no cap
|
|
192
|
+
// is refused whatever it would cost, and one the network does not list is
|
|
193
|
+
// refused without asking the chain anything about it.
|
|
194
|
+
const choice = chooseQuote(ctx.port.config, args.quote);
|
|
195
|
+
if (!choice.listed) {
|
|
196
|
+
return { isError: true, text: refuseWithoutCap(ctx, { address: choice.address }, what) ?? "" };
|
|
197
|
+
}
|
|
198
|
+
const token = choice.token;
|
|
199
|
+
const uncapped = refuseWithoutCap(ctx, token, what);
|
|
200
|
+
if (uncapped !== undefined)
|
|
201
|
+
return { isError: true, text: uncapped };
|
|
202
|
+
const statedMax = parseQuoteAmount(token, args.maxTotalCost, "maxTotalCost");
|
|
203
|
+
const quoteParams = launchParams(args, token);
|
|
204
|
+
const quote = await ctx.port.launchpad.quoteLaunch(quoteParams);
|
|
205
|
+
const refusal = refuseIfOverBudget(quote.totalCost, statedMax, ctx, what);
|
|
206
|
+
if (refusal !== undefined)
|
|
207
|
+
return { isError: true, text: refusal };
|
|
208
|
+
const minTokensOut = minTokensOutFromQuote(quote, Bps.of(BigInt(args.slippageBps)));
|
|
209
|
+
const launchpad = ctx.port.launchpad.address;
|
|
210
|
+
let result;
|
|
211
|
+
try {
|
|
212
|
+
result = await ctx.port.launchpad.launch(launchParams(args, token, minTokensOut));
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
if (token.isNative || ctx.port.signerAddress === undefined)
|
|
216
|
+
throw error;
|
|
217
|
+
return { isError: true, text: await launchFailedAfterApprove(ctx, token, launchpad, error) };
|
|
218
|
+
}
|
|
219
|
+
const state = await ctx.port.curve(result.curve).state().catch(() => undefined);
|
|
220
|
+
return {
|
|
221
|
+
text: report(`Launched ${args.name} (${args.symbol}).`, section("what now exists, permanently", [
|
|
222
|
+
["token", addr(result.token)],
|
|
223
|
+
["curve", addr(result.curve)],
|
|
224
|
+
["creator", addr(ctx.port.signerAddress ?? ZERO_ADDRESS)],
|
|
225
|
+
["name / symbol", `${args.name} / ${args.symbol} — no setter exists for either`],
|
|
226
|
+
["graduation venue", state === undefined
|
|
227
|
+
? "(read it back with arcnow_token)"
|
|
228
|
+
: `${addr(state.migrator)} — ${venueOf(state.migrator, ctx.port.config)}, `
|
|
229
|
+
+ "snapshotted at launch and immutable"],
|
|
230
|
+
]), section("what it cost", [
|
|
231
|
+
["total", `${money(quote.totalCost)} — exactly, ${token.isNative
|
|
232
|
+
? "sent as msg.value, as the launchpad requires"
|
|
233
|
+
: "pulled by the launchpad; the launch transaction carried no value"}`],
|
|
234
|
+
[" launch fee", money(quote.launchFee)],
|
|
235
|
+
[" of which fee", `${money(quote.tradeFee)} on the initial buy`],
|
|
236
|
+
["quote", `${quoteLine(token)} — this token is priced in it for life`],
|
|
237
|
+
["tokens received", qty(result.tokensOut, args.symbol)],
|
|
238
|
+
["floor you set", `${qty(minTokensOut, args.symbol)} (${args.slippageBps} bps)`],
|
|
239
|
+
["transaction", result.txHash],
|
|
240
|
+
]), quoteApprovalSection(quote.totalCost, `the launchpad ${addr(launchpad)}`, result.approvalTxHash), result.graduated
|
|
241
|
+
? section("graduation", [
|
|
242
|
+
["graduated at launch", "YES — the initial buy filled the curve, so trading on it "
|
|
243
|
+
+ "is already over. The SDK sent the launch with an explicit gas limit for the "
|
|
244
|
+
+ "migration."],
|
|
245
|
+
["migrated here", result.migratedInThisTransaction
|
|
246
|
+
? "YES — the pool was created in the launch transaction; the token trades in it, "
|
|
247
|
+
+ "and the quote and trade tools route there"
|
|
248
|
+
: "NO — the pool was not created in this transaction. The token has no market "
|
|
249
|
+
+ "until somebody migrates it; migrate() is permissionless, and arcnow_migrate "
|
|
250
|
+
+ "does it for the cost of gas."],
|
|
251
|
+
["instant migration", result.instantMigrationFailed
|
|
252
|
+
? "FAILED and was caught — the curve logged InstantMigrationFailed"
|
|
253
|
+
: "did not fail"],
|
|
254
|
+
])
|
|
255
|
+
: undefined, state === undefined
|
|
256
|
+
? undefined
|
|
257
|
+
: section("the curve now", [
|
|
258
|
+
["price", price(state.spotPrice)],
|
|
259
|
+
["raised", `${money(state.realReserve)} of ${money(state.target)}`],
|
|
260
|
+
["progress", progress(state.progressBps)],
|
|
261
|
+
]), note("Anyone can now buy this token. Nothing about it can be changed. If the name or "
|
|
262
|
+
+ "symbol is wrong, say so plainly rather than looking for a way to edit it: there "
|
|
263
|
+
+ "is none, and the only remedy is another launch and another fee.")),
|
|
264
|
+
};
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
/**
|
|
268
|
+
* An ERC-20 launch that failed after the SDK may already have approved the
|
|
269
|
+
* launchpad. The SDK's error does not carry the approve's hash, so the standing
|
|
270
|
+
* allowance is read back — one read, on this failure path only — and disclosed.
|
|
271
|
+
*/
|
|
272
|
+
async function launchFailedAfterApprove(ctx, token, launchpad, error) {
|
|
273
|
+
const owner = ctx.port.signerAddress;
|
|
274
|
+
const state = await ctx.port.quoteSpendState(token, owner, launchpad).catch(() => undefined);
|
|
275
|
+
const allowance = state?.allowance;
|
|
276
|
+
return report(`arcnow_launch: the launch in ${token.symbol} did not go through.`, renderError("arcnow_launch", error), note(allowance === undefined
|
|
277
|
+
? `An ERC-20 launch approves the launchpad for the total before it launches, and the ${token.symbol} `
|
|
278
|
+
+ `allowance to the launchpad could not be read back afterwards. Check it before trying `
|
|
279
|
+
+ "again: an approve that went through still stands."
|
|
280
|
+
: `An ERC-20 launch approves the launchpad for the total before it launches. The `
|
|
281
|
+
+ `${token.symbol} allowance to the launchpad now stands at ${money(allowance)}: if an approve `
|
|
282
|
+
+ "went through before the launch failed, it still stands, and the launchpad may pull up to "
|
|
283
|
+
+ "that much until a launch uses it or another approve replaces it. Nothing else was sent. "
|
|
284
|
+
+ "Tell the user. Calling arcnow_launch again uses this allowance and does not approve twice."));
|
|
285
|
+
}
|
|
286
|
+
const buy = defineTool({
|
|
287
|
+
name: "arcnow_buy",
|
|
288
|
+
title: "Buy a token (spends USDC)",
|
|
289
|
+
access: "write",
|
|
290
|
+
description: "Buy a token wherever it currently trades — its bonding curve before graduation, its "
|
|
291
|
+
+ "Uniswap v4 pool after migration — through the SDK's client.trade(token), which decides "
|
|
292
|
+
+ "the venue. SPENDS REAL MONEY, in the token's own QUOTE — native USDC, or the ERC-20 such as "
|
|
293
|
+
+ "EURC it launched in (arcnow_token names it); quoteIn and maxTotalCost are amounts of that "
|
|
294
|
+
+ "quote.\n\n"
|
|
295
|
+
+ "A fresh quote is taken inside this call, on whichever venue the token is on, and "
|
|
296
|
+
+ "checked against maxTotalCost and the operator's cap for that quote before anything is "
|
|
297
|
+
+ "signed; the minimum-tokens-out floor is computed from that quote and your slippage "
|
|
298
|
+
+ "tolerance. SPEND CAPS ARE PER QUOTE: ARCNOW_MCP_MAX_SPEND_USDC for native USDC, "
|
|
299
|
+
+ "ARCNOW_MCP_MAX_SPEND_<SYMBOL> for each other quote, and a quote with no cap is refused.\n\n"
|
|
300
|
+
+ "NATIVE USDC is the transaction's value and needs no approval. AN ERC-20 QUOTE is pulled by "
|
|
301
|
+
+ "the curve or the router: an approve of EXACTLY quoteIn is sent first, only when the "
|
|
302
|
+
+ "allowance falls short, and the result reports it — or says none was needed.\n\n"
|
|
303
|
+
+ "ON A CURVE: if the quote shows this buy fills the curve, this tool sends an explicit "
|
|
304
|
+
+ "8,000,000 gas limit rather than letting the node estimate one. That is not tuning. An "
|
|
305
|
+
+ "estimate finds the lowest limit at which the transaction still SUCCEEDS, and a "
|
|
306
|
+
+ "graduating buy succeeds even when the migration it triggers runs out of gas and is "
|
|
307
|
+
+ "caught — so an estimated limit silently guarantees the token graduates with no market. "
|
|
308
|
+
+ "The result says whether the pool was actually created in this transaction, which is a "
|
|
309
|
+
+ "different question from whether the curve graduated.\n\n"
|
|
310
|
+
+ "IN A POOL (the token graduated and migrated): the buy goes through arcnow.io's v4 "
|
|
311
|
+
+ "router. arcnow.io's 1% is taken in the quote by its fee hook and the pool charges its own "
|
|
312
|
+
+ "LP fee on top; the result reports both. gasLimit, referrer and developer are "
|
|
313
|
+
+ "curve-only and are REFUSED here rather than ignored — a pool swap has no referrer or "
|
|
314
|
+
+ "developer and cannot graduate anything. recipient is pool-only: it sends the tokens "
|
|
315
|
+
+ "to another address, and is refused on a curve.\n\n"
|
|
316
|
+
+ "A token that graduated but never migrated has no market at all; this refuses and names "
|
|
317
|
+
+ "arcnow_migrate. A buy is not irreversible the way a launch is — the tokens can be sold "
|
|
318
|
+
+ "back, at whatever the price is then.",
|
|
319
|
+
input: {
|
|
320
|
+
address: addressArg("The curve address, or the token address. Either works."),
|
|
321
|
+
quoteIn: decimalArg("How much of the token's quote to spend, as a string in whole units — native USDC (18 "
|
|
322
|
+
+ "decimals) or the ERC-20 the token is priced in, such as EURC (6). Read exactly in that "
|
|
323
|
+
+ "quote's decimals: more decimal places than it has is refused, never rounded."),
|
|
324
|
+
slippageBps: slippageArg(),
|
|
325
|
+
maxTotalCost: maxCostArg("this buy"),
|
|
326
|
+
deadlineMinutes: deadlineArg(),
|
|
327
|
+
referrer: addressArg("CURVE ONLY: credited with the referral share of the fee. Refused for a token in its "
|
|
328
|
+
+ "pool: a pool swap has no referrer, and that share goes to the platform.").optional(),
|
|
329
|
+
developer: addressArg("CURVE ONLY: credited with the developer share of the fee. Refused for a token in its "
|
|
330
|
+
+ "pool, for the same reason.").optional(),
|
|
331
|
+
gasLimit: z.number().int().min(100_000).max(30_000_000).optional().describe("CURVE ONLY. Optional override for the gas limit. Leave it out: this server already "
|
|
332
|
+
+ "sends 8,000,000 on a curve buy it can see will graduate and lets the node estimate "
|
|
333
|
+
+ "otherwise. FOR AN ERC-20 QUOTE a limit set here is raised to a safe minimum, never "
|
|
334
|
+
+ "refused: the node's estimate plus max(20%, 150,000) for the fee-share transfers, or "
|
|
335
|
+
+ "8,000,000 when the buy graduates the curve. A higher limit is kept. For native USDC it "
|
|
336
|
+
+ "is sent as given, and a value below 6,200,000 on a graduating buy is REFUSED, because the "
|
|
337
|
+
+ "migration would be starved and the failure would be silent. Refused for a token in "
|
|
338
|
+
+ "its pool, whose buy cannot graduate anything."),
|
|
339
|
+
recipient: addressArg("POOL ONLY: who receives the tokens. Defaults to the signing address. Refused for a "
|
|
340
|
+
+ "token still on its bonding curve, which always pays the sender. If it is not the "
|
|
341
|
+
+ "user's own address, check it with them: tokens sent to a wrong address are "
|
|
342
|
+
+ "gone.").optional(),
|
|
343
|
+
},
|
|
344
|
+
async run(args, ctx) {
|
|
345
|
+
const market = await resolveMarket(ctx, args.address);
|
|
346
|
+
if (market.venue === "stranded")
|
|
347
|
+
return { isError: true, text: strandedText("arcnow_buy") };
|
|
348
|
+
if (market.venue === "pool") {
|
|
349
|
+
const refused = ["gasLimit", "referrer", "developer"]
|
|
350
|
+
.filter((field) => args[field] !== undefined);
|
|
351
|
+
if (refused.length > 0) {
|
|
352
|
+
return { isError: true, text: refuseCurveOnly("arcnow_buy", refused, "sent") };
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
else if (args.recipient !== undefined) {
|
|
356
|
+
return { isError: true, text: refusePoolOnlyRecipient("arcnow_buy") };
|
|
357
|
+
}
|
|
358
|
+
const symbol = await ctx.port.token(market.state.token).symbol();
|
|
359
|
+
// The quote comes with the curve's state, read already: no extra call.
|
|
360
|
+
const quoteToken = market.state.quoteToken;
|
|
361
|
+
const uncapped = refuseWithoutCap(ctx, quoteToken, `buying ${symbol}`);
|
|
362
|
+
if (uncapped !== undefined)
|
|
363
|
+
return { isError: true, text: uncapped };
|
|
364
|
+
const quoteIn = parseQuoteAmount(quoteToken, args.quoteIn, "quoteIn");
|
|
365
|
+
const statedMax = parseQuoteAmount(quoteToken, args.maxTotalCost, "maxTotalCost");
|
|
366
|
+
const refusal = refuseIfOverBudget(quoteIn, statedMax, ctx, `buying ${symbol}`);
|
|
367
|
+
if (refusal !== undefined)
|
|
368
|
+
return { isError: true, text: refusal };
|
|
369
|
+
return market.venue === "pool"
|
|
370
|
+
? buyInPool(args, ctx, market, symbol, quoteIn, statedMax)
|
|
371
|
+
: buyOnCurve(args, ctx, market, symbol, quoteIn);
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
async function buyOnCurve(args, ctx, market, symbol, quoteIn) {
|
|
375
|
+
const { state } = market;
|
|
376
|
+
const quote = await market.trade.quoteBuy(quoteIn);
|
|
377
|
+
if (quote.venue !== "curve")
|
|
378
|
+
return { isError: true, text: venueMovedText("arcnow_buy") };
|
|
379
|
+
const erc20 = !quoteIn.token.isNative;
|
|
380
|
+
const callerGasLimit = args.gasLimit === undefined ? undefined : BigInt(args.gasLimit);
|
|
381
|
+
// A graduating buy needs the migration's budget. For an ERC-20 quote a caller's
|
|
382
|
+
// limit is only ever raised — to GRADUATION_GAS_LIMIT here, and on a buy that does
|
|
383
|
+
// not graduate, by the SDK, to the estimate plus headroom — never refused or lowered.
|
|
384
|
+
// Native USDC is unchanged: the caller's limit as given, refused below the floor.
|
|
385
|
+
const gasLimit = quote.graduates
|
|
386
|
+
? erc20
|
|
387
|
+
? (callerGasLimit !== undefined && callerGasLimit > GRADUATION_GAS_LIMIT
|
|
388
|
+
? callerGasLimit
|
|
389
|
+
: GRADUATION_GAS_LIMIT)
|
|
390
|
+
: (callerGasLimit ?? GRADUATION_GAS_LIMIT)
|
|
391
|
+
: callerGasLimit;
|
|
392
|
+
if (!erc20 && quote.graduates && gasLimit !== undefined && gasLimit < GRADUATION_GAS_FLOOR) {
|
|
393
|
+
return {
|
|
394
|
+
isError: true,
|
|
395
|
+
text: report(`Refused: this buy would graduate the curve and the gas limit you gave `
|
|
396
|
+
+ `(${gasLimit}) is below the ${GRADUATION_GAS_FLOOR} needed for the migration to `
|
|
397
|
+
+ "run. Nothing was sent.", note(GRADUATING_BUY_WARNING)),
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
const minTokensOut = minTokensOutFromQuote(quote, Bps.of(BigInt(args.slippageBps)));
|
|
401
|
+
const result = await market.trade.buy({
|
|
402
|
+
quoteIn,
|
|
403
|
+
minTokensOut,
|
|
404
|
+
deadline: Deadline.inMinutes(args.deadlineMinutes),
|
|
405
|
+
...(args.referrer === undefined ? {} : { referrer: args.referrer }),
|
|
406
|
+
...(args.developer === undefined ? {} : { developer: args.developer }),
|
|
407
|
+
...(gasLimit === undefined ? {} : { gasLimit }),
|
|
408
|
+
});
|
|
409
|
+
if (result.venue !== "curve") {
|
|
410
|
+
return { isError: true, text: sentElsewhere("arcnow_buy", "pool", result.hash) };
|
|
411
|
+
}
|
|
412
|
+
const avg = effectivePrice(result.quoteSpent, result.tokensOut);
|
|
413
|
+
return {
|
|
414
|
+
text: report(`Bought ${qty(result.tokensOut, symbol)}.`, section("what happened", [
|
|
415
|
+
["venue", "its bonding curve"],
|
|
416
|
+
["you sent", spentLine(quoteIn, `the curve ${addr(market.curve.address)}`)],
|
|
417
|
+
["fee", `${money(result.fee)} — the flat 1%`],
|
|
418
|
+
["reached the curve", money(result.quoteSpent)],
|
|
419
|
+
["refunded", result.refund.isZero()
|
|
420
|
+
? "nothing"
|
|
421
|
+
: `${money(result.refund)} — the buy was capped at the remaining inventory${quoteIn.token.isNative ? "" : "; an ERC-20 refund is simply never pulled"}`],
|
|
422
|
+
["tokens out", qty(result.tokensOut, symbol)],
|
|
423
|
+
["floor you set", `${qty(minTokensOut, symbol)} (${args.slippageBps} bps)`],
|
|
424
|
+
["average fill price", avg === undefined ? "n/a" : price(avg)],
|
|
425
|
+
["new spot price", price(result.newPrice)],
|
|
426
|
+
["gas limit sent", gasLimitSentLine(erc20, quote.graduates, callerGasLimit, gasLimit)],
|
|
427
|
+
["transaction", result.txHash],
|
|
428
|
+
]), quoteApprovalSection(quoteIn, `the curve ${addr(market.curve.address)}`, result.approvalTxHash), result.graduated
|
|
429
|
+
? section("graduation", [
|
|
430
|
+
["curve", "GRADUATED. Trading on it is over, permanently."],
|
|
431
|
+
["migrated here", result.migratedInThisTransaction
|
|
432
|
+
? "YES — the pool was created in this same transaction; the token now trades in "
|
|
433
|
+
+ "it, and the quote and trade tools route there"
|
|
434
|
+
: "NO — the pool was not created in this transaction"],
|
|
435
|
+
["instant migration", result.instantMigrationFailed
|
|
436
|
+
? "FAILED and was caught. The curve logged InstantMigrationFailed. The token "
|
|
437
|
+
+ "has graduated and has no market until somebody migrates it — migrate() is "
|
|
438
|
+
+ "permissionless, and arcnow_migrate does it for the cost of gas. Do that "
|
|
439
|
+
+ "now, or tell the user it needs doing."
|
|
440
|
+
: "succeeded"],
|
|
441
|
+
])
|
|
442
|
+
: section("progress", [
|
|
443
|
+
["raised", `${money(state.realReserve)} → ${money(quote.newReserve)} of `
|
|
444
|
+
+ money(state.target)],
|
|
445
|
+
["graduates at", money(state.target)],
|
|
446
|
+
])),
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
async function buyInPool(args, ctx, market, symbol, quoteIn, statedMax) {
|
|
450
|
+
let quote;
|
|
451
|
+
try {
|
|
452
|
+
quote = await market.trade.quoteBuy(quoteIn);
|
|
453
|
+
}
|
|
454
|
+
catch (error) {
|
|
455
|
+
return { isError: true, text: renderPoolError("arcnow_buy", "buy", error) };
|
|
456
|
+
}
|
|
457
|
+
if (quote.venue !== "pool")
|
|
458
|
+
return { isError: true, text: venueMovedText("arcnow_buy") };
|
|
459
|
+
// The router settles exactly the amount sent, so this is the same number; it
|
|
460
|
+
// is checked again against the quote anyway, so a quote that disagreed with
|
|
461
|
+
// the order could not slip past either ceiling.
|
|
462
|
+
const cost = quote.quoteIn.gt(quoteIn) ? quote.quoteIn : quoteIn;
|
|
463
|
+
const refusal = refuseIfOverBudget(cost, statedMax, ctx, `buying ${symbol}`);
|
|
464
|
+
if (refusal !== undefined)
|
|
465
|
+
return { isError: true, text: refusal };
|
|
466
|
+
const minTokensOut = minTokensOutFromQuote(quote, Bps.of(BigInt(args.slippageBps)));
|
|
467
|
+
const signer = ctx.port.signerAddress;
|
|
468
|
+
const recipient = (args.recipient ?? signer);
|
|
469
|
+
let result;
|
|
470
|
+
try {
|
|
471
|
+
result = await market.trade.buy({
|
|
472
|
+
quoteIn,
|
|
473
|
+
minTokensOut,
|
|
474
|
+
deadline: Deadline.inMinutes(args.deadlineMinutes),
|
|
475
|
+
...(args.recipient === undefined ? {} : { recipient: args.recipient }),
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
catch (error) {
|
|
479
|
+
return { isError: true, text: renderPoolError("arcnow_buy", "buy", error) };
|
|
480
|
+
}
|
|
481
|
+
if (result.venue !== "pool") {
|
|
482
|
+
return { isError: true, text: sentElsewhere("arcnow_buy", "curve", result.txHash) };
|
|
483
|
+
}
|
|
484
|
+
const key = await market.trade.pool.key().catch(() => undefined);
|
|
485
|
+
const avg = effectivePrice(result.quote, result.tokens);
|
|
486
|
+
const router = routerName(ctx.port.config);
|
|
487
|
+
return {
|
|
488
|
+
text: report(`Bought ${qty(result.tokens, symbol)} in its Uniswap v4 pool.`, section("what happened", [
|
|
489
|
+
["venue", `Uniswap v4 pool, through arcnow.io's router ${router}`],
|
|
490
|
+
["you sent", `${money(result.quote)} — read from the PoolManager's Swap log in this `
|
|
491
|
+
+ `transaction's receipt, arcnow.io's fee included${quoteIn.token.isNative
|
|
492
|
+
? "; as msg.value"
|
|
493
|
+
: "; pulled by the router, with no value sent"}`],
|
|
494
|
+
["arcnow.io fee", poolFeeTaken(result.feeQuote)],
|
|
495
|
+
["earlier fees paid out", earlierFeesPaidOut(result.feesDistributed)],
|
|
496
|
+
["pool fee", key === undefined
|
|
497
|
+
? "(the pool key could not be read back)"
|
|
498
|
+
: `${lpFee(key.fee)} — Uniswap's LP fee, inside the price`],
|
|
499
|
+
["tokens out", `${qty(result.tokens, symbol)} — from the token's Transfer log in the `
|
|
500
|
+
+ "receipt"],
|
|
501
|
+
["tokens to", recipient === undefined ? "the signing address" : payee(recipient, signer)],
|
|
502
|
+
["floor you set", `${qty(minTokensOut, symbol)} (${args.slippageBps} bps)`],
|
|
503
|
+
["average fill price", avg === undefined ? "n/a" : price(avg)],
|
|
504
|
+
["gas limit sent", poolSwapGasLine(quoteIn.token.isNative, "buy")],
|
|
505
|
+
["transaction", result.hash],
|
|
506
|
+
]), quoteApprovalSection(quoteIn, `arcnow.io's router ${router}`, result.approvalTxHash)),
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
const sell = defineTool({
|
|
510
|
+
name: "arcnow_sell",
|
|
511
|
+
title: "Sell a token",
|
|
512
|
+
access: "write",
|
|
513
|
+
description: "Sell a token wherever it currently trades, through the SDK's client.trade(token). "
|
|
514
|
+
+ "Spends nothing but gas; it receives the token's quote — native USDC, or the ERC-20 such as "
|
|
515
|
+
+ "EURC it is priced in.\n\n"
|
|
516
|
+
+ "A fresh quote is taken inside this call and the minimum-out floor, in that quote, is "
|
|
517
|
+
+ "computed from it and your slippage tolerance.\n\n"
|
|
518
|
+
+ "ON A CURVE there is no approval step, and this server will never emit one. The curve "
|
|
519
|
+
+ "pulls the tokens through a privileged path that reads no allowance at all: a holder "
|
|
520
|
+
+ "who has approved nobody can always sell, and an allowance granted to the curve is not "
|
|
521
|
+
+ "spent by selling. If a curve sell fails with InsufficientTokenBalance, the wallet does "
|
|
522
|
+
+ "not hold the tokens — an approval will not change that.\n\n"
|
|
523
|
+
+ "IN A POOL (the token graduated and migrated) it is different, and this is the one "
|
|
524
|
+
+ "place this server can GRANT SPENDING RIGHTS. The router pulls the tokens with "
|
|
525
|
+
+ "transferFrom, so it must be approved first, in a separate ERC-20 approval transaction. "
|
|
526
|
+
+ "This tool never sends one silently. Without approveRouter: true it checks the "
|
|
527
|
+
+ "allowance and, if it is short, refuses, sends nothing, and says exactly what the "
|
|
528
|
+
+ "approval would be. With approveRouter: true it approves EXACTLY the amount being sold "
|
|
529
|
+
+ "— never an unlimited allowance — and only if the existing allowance does not already "
|
|
530
|
+
+ "cover the sale; the result reports the approval, its transaction and the allowance "
|
|
531
|
+
+ "left, including when the sell that followed it failed. Tell the user about the "
|
|
532
|
+
+ "approval before setting approveRouter. referrer and developer are curve-only and "
|
|
533
|
+
+ "refused in a pool; recipient is pool-only and refused on a curve.\n\n"
|
|
534
|
+
+ "A token that graduated but never migrated cannot be sold anywhere; this refuses and "
|
|
535
|
+
+ "names arcnow_migrate.",
|
|
536
|
+
input: {
|
|
537
|
+
address: addressArg("The curve address, or the token address. Either works."),
|
|
538
|
+
tokensIn: decimalArg("How many whole tokens to sell, as a string."),
|
|
539
|
+
slippageBps: slippageArg(),
|
|
540
|
+
deadlineMinutes: deadlineArg(),
|
|
541
|
+
referrer: addressArg("CURVE ONLY: credited with the referral share of the fee. Refused for a token in its "
|
|
542
|
+
+ "pool.").optional(),
|
|
543
|
+
developer: addressArg("CURVE ONLY: credited with the developer share of the fee. Refused for a token in its "
|
|
544
|
+
+ "pool.").optional(),
|
|
545
|
+
recipient: addressArg("POOL ONLY: who receives the proceeds, in the token's quote. Defaults to the signing "
|
|
546
|
+
+ "address. Refused for a token still on its bonding curve, which always pays the seller. If "
|
|
547
|
+
+ "it is not the user's own address, check it with them: money sent to a wrong address is "
|
|
548
|
+
+ "gone.").optional(),
|
|
549
|
+
approveRouter: z.boolean().optional().describe("POOL ONLY. Set true to let this call send the ERC-20 approval a pool sell needs, if "
|
|
550
|
+
+ "the router's existing allowance does not already cover the sale. The approval is a "
|
|
551
|
+
+ "separate transaction granting arcnow.io's router the right to move EXACTLY the "
|
|
552
|
+
+ "amount being sold — never more — and it is reported in the result. Omit it and a "
|
|
553
|
+
+ "sell that needs an approval is refused with nothing sent, saying what the approval "
|
|
554
|
+
+ "would be. Refused on a bonding curve, whose sells need no approval."),
|
|
555
|
+
},
|
|
556
|
+
async run(args, ctx) {
|
|
557
|
+
const market = await resolveMarket(ctx, args.address);
|
|
558
|
+
if (market.venue === "stranded")
|
|
559
|
+
return { isError: true, text: strandedText("arcnow_sell") };
|
|
560
|
+
if (market.venue === "pool") {
|
|
561
|
+
const refused = ["referrer", "developer"]
|
|
562
|
+
.filter((field) => args[field] !== undefined);
|
|
563
|
+
if (refused.length > 0) {
|
|
564
|
+
return { isError: true, text: refuseCurveOnly("arcnow_sell", refused, "sent") };
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
if (args.recipient !== undefined) {
|
|
569
|
+
return { isError: true, text: refusePoolOnlyRecipient("arcnow_sell") };
|
|
570
|
+
}
|
|
571
|
+
if (args.approveRouter === true)
|
|
572
|
+
return { isError: true, text: refuseCurveApproval() };
|
|
573
|
+
}
|
|
574
|
+
const symbol = await ctx.port.token(market.state.token).symbol();
|
|
575
|
+
const tokensIn = Tokens.parse(args.tokensIn);
|
|
576
|
+
return market.venue === "pool"
|
|
577
|
+
? sellInPool(args, ctx, market, symbol, tokensIn)
|
|
578
|
+
: sellOnCurve(args, market, symbol, tokensIn);
|
|
579
|
+
},
|
|
580
|
+
});
|
|
581
|
+
async function sellOnCurve(args, market, symbol, tokensIn) {
|
|
582
|
+
const quote = await market.trade.quoteSell(tokensIn);
|
|
583
|
+
if (quote.venue !== "curve")
|
|
584
|
+
return { isError: true, text: venueMovedText("arcnow_sell") };
|
|
585
|
+
const minQuoteOut = minQuoteOutFromQuote(quote, Bps.of(BigInt(args.slippageBps)));
|
|
586
|
+
const result = await market.trade.sell({
|
|
587
|
+
tokensIn,
|
|
588
|
+
minQuoteOut,
|
|
589
|
+
deadline: Deadline.inMinutes(args.deadlineMinutes),
|
|
590
|
+
...(args.referrer === undefined ? {} : { referrer: args.referrer }),
|
|
591
|
+
...(args.developer === undefined ? {} : { developer: args.developer }),
|
|
592
|
+
});
|
|
593
|
+
if (result.venue !== "curve") {
|
|
594
|
+
return { isError: true, text: sentElsewhere("arcnow_sell", "pool", result.hash) };
|
|
595
|
+
}
|
|
596
|
+
return {
|
|
597
|
+
text: report(`Sold ${qty(tokensIn, symbol)} for ${money(paidOut(result.quoteOut))}.`, section("what happened", [
|
|
598
|
+
["venue", "its bonding curve"],
|
|
599
|
+
["tokens in", qty(tokensIn, symbol)],
|
|
600
|
+
["fee", money(result.fee)],
|
|
601
|
+
["you received", result.quoteOut.token.isNative
|
|
602
|
+
? money(result.quoteOut)
|
|
603
|
+
: `${money(paidOut(result.quoteOut))} — ${result.quoteOut.token.symbol} pays whole raw units, `
|
|
604
|
+
+ `so the ${money(result.quoteOut)} the curve accounted is paid down to that`],
|
|
605
|
+
["floor you set", `${money(minQuoteOut)} (${args.slippageBps} bps)`],
|
|
606
|
+
["new spot price", price(result.newPrice)],
|
|
607
|
+
["transaction", result.txHash],
|
|
608
|
+
]), note("No approval was needed and none was granted; the curve's pull path reads no "
|
|
609
|
+
+ "allowance. Any allowance that existed before this sell is untouched.")),
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
function approvalSection(approval, at) {
|
|
613
|
+
return section("approval granted — a separate transaction, sent before the sell", [
|
|
614
|
+
["token", `${addr(at.token)} (${at.symbol})`],
|
|
615
|
+
["owner", addr(at.owner)],
|
|
616
|
+
["spender", `${at.router} — arcnow.io's v4 router`],
|
|
617
|
+
["amount", `${qty(approval.amount, at.symbol)} — exactly the amount sold, not unlimited`],
|
|
618
|
+
...(approval.previous.isZero()
|
|
619
|
+
? []
|
|
620
|
+
: [["replaced", `a previous allowance of ${qty(approval.previous, at.symbol)}, which did `
|
|
621
|
+
+ "not cover this sale"]]),
|
|
622
|
+
["transaction", approval.hash],
|
|
623
|
+
["allowance now", at.left === undefined ? "(could not be read back)" : qty(at.left, at.symbol)],
|
|
624
|
+
]);
|
|
625
|
+
}
|
|
626
|
+
async function sellInPool(args, ctx, market, symbol, tokensIn) {
|
|
627
|
+
const signer = ctx.port.signerAddress;
|
|
628
|
+
if (signer === undefined) {
|
|
629
|
+
return { isError: true, text: "This server has no signer, so there is nobody to sell for." };
|
|
630
|
+
}
|
|
631
|
+
const pool = market.trade.pool;
|
|
632
|
+
const router = routerName(ctx.port.config);
|
|
633
|
+
const token = market.state.token;
|
|
634
|
+
let quote;
|
|
635
|
+
try {
|
|
636
|
+
quote = await market.trade.quoteSell(tokensIn, { from: signer });
|
|
637
|
+
}
|
|
638
|
+
catch (error) {
|
|
639
|
+
return { isError: true, text: renderPoolError("arcnow_sell", "sell", error) };
|
|
640
|
+
}
|
|
641
|
+
if (quote.venue !== "pool")
|
|
642
|
+
return { isError: true, text: venueMovedText("arcnow_sell") };
|
|
643
|
+
// The pool pays whole raw units of its quote, and the SDK sends the floor
|
|
644
|
+
// rounded UP to one; rounded here too, so the floor reported is the one sent.
|
|
645
|
+
const minQuoteOut = minQuoteOutFromQuote(quote, Bps.of(BigInt(args.slippageBps)))
|
|
646
|
+
.ceilToRepresentable();
|
|
647
|
+
const before = await pool.routerAllowance(signer);
|
|
648
|
+
let approval;
|
|
649
|
+
if (before.lt(tokensIn)) {
|
|
650
|
+
if (args.approveRouter !== true) {
|
|
651
|
+
return {
|
|
652
|
+
isError: true,
|
|
653
|
+
text: report(`Refused: selling ${qty(tokensIn, symbol)} in its Uniswap v4 pool needs an ERC-20 `
|
|
654
|
+
+ `approval to arcnow.io's router first, and ${addr(signer)} has approved it for `
|
|
655
|
+
+ `${qty(before, symbol)}. Nothing was sent.`, section("the approval this sell would need", [
|
|
656
|
+
["token", `${addr(token)} (${symbol})`],
|
|
657
|
+
["owner", addr(signer)],
|
|
658
|
+
["spender", `${router} — arcnow.io's v4 router`],
|
|
659
|
+
["amount", `${qty(tokensIn, symbol)} — exactly the amount to be sold, never unlimited`],
|
|
660
|
+
["what it is", "a SEPARATE transaction, paid for in gas, granting the router the "
|
|
661
|
+
+ "right to move that many of these tokens. The sell then uses it up."],
|
|
662
|
+
]), note("To go ahead, tell the person whose tokens these are that selling in the pool "
|
|
663
|
+
+ "takes this approval, and call arcnow_sell again with approveRouter: true. It is "
|
|
664
|
+
+ "then sent only if it is still needed, for exactly the amount sold, and reported "
|
|
665
|
+
+ "in the result. A bonding-curve sell never needed this; a pool sell always does, "
|
|
666
|
+
+ "because the router has no privileged path to the tokens.")),
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
const hash = await pool.approveRouter(tokensIn);
|
|
670
|
+
approval = { hash, amount: tokensIn, previous: before };
|
|
671
|
+
}
|
|
672
|
+
let result;
|
|
673
|
+
try {
|
|
674
|
+
result = await market.trade.sell({
|
|
675
|
+
tokensIn,
|
|
676
|
+
minQuoteOut,
|
|
677
|
+
deadline: Deadline.inMinutes(args.deadlineMinutes),
|
|
678
|
+
...(args.recipient === undefined ? {} : { recipient: args.recipient }),
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
catch (error) {
|
|
682
|
+
if (approval === undefined) {
|
|
683
|
+
return { isError: true, text: renderPoolError("arcnow_sell", "sell", error) };
|
|
684
|
+
}
|
|
685
|
+
const left = await pool.routerAllowance(signer).catch(() => undefined);
|
|
686
|
+
return {
|
|
687
|
+
isError: true,
|
|
688
|
+
text: report("arcnow_sell: the router approval went through, and the sell did not.", approvalSection(approval, { token, symbol, router, owner: signer, left }), note(`That approval still stands: arcnow.io's router may move up to `
|
|
689
|
+
+ `${qty(left ?? approval.amount, symbol)} of ${addr(signer)}'s ${symbol} until a sell `
|
|
690
|
+
+ "uses it or another approval replaces it. Nothing else was sent. Tell the user. "
|
|
691
|
+
+ "Calling arcnow_sell again uses this allowance and does not approve a second time."), renderPoolError("arcnow_sell", "sell", error)),
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
if (result.venue !== "pool") {
|
|
695
|
+
return { isError: true, text: sentElsewhere("arcnow_sell", "curve", result.txHash) };
|
|
696
|
+
}
|
|
697
|
+
const left = await pool.routerAllowance(signer).catch(() => undefined);
|
|
698
|
+
const recipient = (args.recipient ?? signer);
|
|
699
|
+
const key = await pool.key().catch(() => undefined);
|
|
700
|
+
return {
|
|
701
|
+
text: report(`Sold ${qty(result.tokens, symbol)} in its Uniswap v4 pool for ${money(result.quote)}.`, approval === undefined
|
|
702
|
+
? section("approval", [
|
|
703
|
+
["approval", `No approval was sent: the router's existing allowance of `
|
|
704
|
+
+ `${qty(before, symbol)} already covered this sale.`],
|
|
705
|
+
["allowance now", left === undefined
|
|
706
|
+
? "(could not be read back)"
|
|
707
|
+
: `${qty(left, symbol)}${left.isZero()
|
|
708
|
+
? ""
|
|
709
|
+
: " — a standing allowance remains: the router can move that many of these "
|
|
710
|
+
+ "tokens until a sell uses it or another approval replaces it"}`],
|
|
711
|
+
])
|
|
712
|
+
: approvalSection(approval, { token, symbol, router, owner: signer, left }), section("what happened", [
|
|
713
|
+
["venue", `Uniswap v4 pool, through arcnow.io's router ${router}`],
|
|
714
|
+
["tokens in", `${qty(result.tokens, symbol)} — from the token's Transfer log in the `
|
|
715
|
+
+ "receipt"],
|
|
716
|
+
["pool fee", key === undefined
|
|
717
|
+
? "(the pool key could not be read back)"
|
|
718
|
+
: `${lpFee(key.fee)} — Uniswap's LP fee, taken from the tokens sold`],
|
|
719
|
+
["arcnow.io fee", poolFeeTaken(result.feeQuote)],
|
|
720
|
+
["earlier fees paid out", earlierFeesPaidOut(result.feesDistributed)],
|
|
721
|
+
["you received", `${money(result.quote)} — read from the PoolManager's Swap log in this `
|
|
722
|
+
+ "transaction's receipt, net of both fees"],
|
|
723
|
+
[`${result.quote.token.symbol} to`, payee(recipient, signer)],
|
|
724
|
+
["floor you set", `${money(minQuoteOut)} (${args.slippageBps} bps)`],
|
|
725
|
+
["gas limit sent", poolSwapGasLine(result.quote.token.isNative, "sell")],
|
|
726
|
+
["transaction", result.hash],
|
|
727
|
+
])),
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
731
|
+
const migrate = defineTool({
|
|
732
|
+
name: "arcnow_migrate",
|
|
733
|
+
title: "Finish a stranded graduation",
|
|
734
|
+
access: "write",
|
|
735
|
+
description: "Create the pool for a curve that graduated without one. Costs only gas and spends "
|
|
736
|
+
+ "nothing; it is the rescue path, not a normal step.\n\n"
|
|
737
|
+
+ "When a graduating buy is sent with an estimated gas limit, the migration it triggers "
|
|
738
|
+
+ "runs out of gas and the curve CATCHES that failure rather than reverting. The buy "
|
|
739
|
+
+ "succeeds, the curve graduates, the refund is correct, and the pool is never created — "
|
|
740
|
+
+ "with no revert and no error anywhere. Until it is, the token trades NOWHERE. "
|
|
741
|
+
+ "migrate() is deliberately permissionless and unbounded in gas so that anybody, not "
|
|
742
|
+
+ "just the buyer, can finish the job. That is what this does; afterwards the token trades "
|
|
743
|
+
+ "in its Uniswap v4 pool and the quote and trade tools route there.\n\n"
|
|
744
|
+
+ "If the curve already migrated, this reports that and sends nothing. If it has not "
|
|
745
|
+
+ "graduated yet, this reports that and sends nothing: migration is not something you can "
|
|
746
|
+
+ "bring forward.",
|
|
747
|
+
input: {
|
|
748
|
+
address: addressArg("The curve address, or the token address. Either works."),
|
|
749
|
+
},
|
|
750
|
+
async run(args, ctx) {
|
|
751
|
+
const { curve, state } = await resolveCurve(ctx.port, args.address);
|
|
752
|
+
if (!state.graduated) {
|
|
753
|
+
return {
|
|
754
|
+
isError: true,
|
|
755
|
+
text: report("Nothing to do: this curve has not graduated, so there is nothing to migrate.", section("where it is", [
|
|
756
|
+
["raised", `${money(state.realReserve)} of ${money(state.target)}`],
|
|
757
|
+
["progress", progress(state.progressBps)],
|
|
758
|
+
]), note("Migration happens when the curve collects its target, in the buy that fills "
|
|
759
|
+
+ "it. It cannot be triggered early.")),
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
if (state.migrated) {
|
|
763
|
+
const manager = await ctx.port.token(state.token).migratedPool().catch(() => undefined);
|
|
764
|
+
return {
|
|
765
|
+
text: report("Nothing to do: this curve has already migrated, and nothing was sent.", section("where it went", [
|
|
766
|
+
["venue", `${addr(state.migrator)} — ${venueOf(state.migrator, ctx.port.config)}`],
|
|
767
|
+
["pool manager", manager === undefined
|
|
768
|
+
? "(could not be read)"
|
|
769
|
+
: `${addr(manager)} — a v4 pool has no address of its own; this is the manager `
|
|
770
|
+
+ "it lives in"],
|
|
771
|
+
])),
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
const result = await curve.migrate();
|
|
775
|
+
return {
|
|
776
|
+
text: report("Migrated. The token now has a market: its Uniswap v4 pool.", section("what moved", [
|
|
777
|
+
[`${result.quote.token.symbol} into the pool`, money(result.quote)],
|
|
778
|
+
["tokens into the pool", qty(result.tokens)],
|
|
779
|
+
["recorded pool", result.pool === undefined
|
|
780
|
+
? "(not reported in the receipt)"
|
|
781
|
+
: `${addr(result.pool)} — for a v4 pool this is the PoolManager, not a per-token `
|
|
782
|
+
+ "address"],
|
|
783
|
+
["venue", `${addr(state.migrator)} — ${venueOf(state.migrator, ctx.port.config)}`],
|
|
784
|
+
["transaction", result.txHash],
|
|
785
|
+
]), note("This cost gas and nothing else. Anyone could have called it; there is no reward "
|
|
786
|
+
+ "for doing so, which is why a stranded curve can sit unmigrated until somebody "
|
|
787
|
+
+ "notices. arcnow_quote_buy and arcnow_buy now route to the pool.")),
|
|
788
|
+
};
|
|
789
|
+
},
|
|
790
|
+
});
|
|
791
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
792
|
+
const withdraw = defineTool({
|
|
793
|
+
name: "arcnow_withdraw_refund",
|
|
794
|
+
title: "Claim what a curve is holding for you",
|
|
795
|
+
access: "write",
|
|
796
|
+
description: "Claim the quote token (native USDC, or the ERC-20 such as EURC the curve is priced in) that "
|
|
797
|
+
+ "a curve credited to an address because a direct transfer to it failed — "
|
|
798
|
+
+ "a refund on a graduating buy, or a fee payout. Costs only gas and receives money; it "
|
|
799
|
+
+ "cannot spend anything.\n\n"
|
|
800
|
+
+ "This is the only write tool here that recovers funds rather than committing them. It "
|
|
801
|
+
+ "claims for the address this server signs with, and sends the proceeds wherever you "
|
|
802
|
+
+ "say. If there is nothing owed, it reports that and sends nothing.",
|
|
803
|
+
input: {
|
|
804
|
+
address: addressArg("The curve address, or the token address. Either works."),
|
|
805
|
+
to: addressArg("Where to send the claimed money. Defaults to the signing address. Check this with the "
|
|
806
|
+
+ "user if it is not their own — money sent to a wrong address is gone.").optional(),
|
|
807
|
+
},
|
|
808
|
+
async run(args, ctx) {
|
|
809
|
+
const signer = ctx.port.signerAddress;
|
|
810
|
+
if (signer === undefined) {
|
|
811
|
+
return { isError: true, text: "This server has no signer, so there is no account to claim for." };
|
|
812
|
+
}
|
|
813
|
+
const { curve } = await resolveCurve(ctx.port, args.address);
|
|
814
|
+
const owed = await curve.pendingWithdrawal(signer);
|
|
815
|
+
if (owed.isZero()) {
|
|
816
|
+
return {
|
|
817
|
+
text: `This curve owes ${addr(signer)} nothing, and nothing was sent. A balance here `
|
|
818
|
+
+ "only appears when a transfer to the address failed and the curve credited it "
|
|
819
|
+
+ "instead.",
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
const to = (args.to ?? signer);
|
|
823
|
+
const result = await curve.withdraw(to);
|
|
824
|
+
return {
|
|
825
|
+
text: report(`Claimed ${money(result.amount)}.`, section("where it went", [
|
|
826
|
+
["amount", money(result.amount)],
|
|
827
|
+
["to", `${addr(to)}${to.toLowerCase() === signer.toLowerCase()
|
|
828
|
+
? " (the signing address)"
|
|
829
|
+
: " — NOT the signing address"}`],
|
|
830
|
+
["transaction", result.txHash],
|
|
831
|
+
])),
|
|
832
|
+
};
|
|
833
|
+
},
|
|
834
|
+
});
|
|
835
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
836
|
+
const registerPlatform = defineTool({
|
|
837
|
+
name: "arcnow_register_platform",
|
|
838
|
+
title: "Register a new platform (protocol admin only)",
|
|
839
|
+
access: "write",
|
|
840
|
+
destructive: true,
|
|
841
|
+
description: "Register a new platform: a fee split, a curve template and a default graduation venue. "
|
|
842
|
+
+ "Costs only gas, and deploys a new PlatformConfig contract that will exist forever.\n\n"
|
|
843
|
+
+ "READ THIS BEFORE OFFERING IT. Registration is callable ONLY by the registry's "
|
|
844
|
+
+ "protocolAdmin; every other caller gets NotProtocolAdmin. The registry deploys each "
|
|
845
|
+
+ "PlatformConfig itself, so that membership certifies code rather than a claim, and it "
|
|
846
|
+
+ "will not deploy one on behalf of an arbitrary caller. If someone just wants to launch "
|
|
847
|
+
+ "a token, they do NOT need a platform — they launch under arcnow.io's, which is the "
|
|
848
|
+
+ "default.\n\n"
|
|
849
|
+
+ "The shares you give are basis points OF THE FEE, not of the trade: 3000 means 30% of "
|
|
850
|
+
+ "the 1% fee, which is 0.30% of a trade. The platform's own share is not an input and "
|
|
851
|
+
+ "cannot be one — it is the residual, 10000 minus the protocol's maximum share and the "
|
|
852
|
+
+ "three you set, and this tool tells you what you are actually choosing before it sends. "
|
|
853
|
+
+ "Creator, referrer and developer may claim at most 7500 between them.\n\n"
|
|
854
|
+
+ "The curve template is arcnow.io's shipped constant-product one, whose constants were "
|
|
855
|
+
+ "solved to 80 "
|
|
856
|
+
+ "digits and placed rather than rounded. This tool does not accept a custom template: a "
|
|
857
|
+
+ "recomputed Y0 or R0 lands a few wei out, the contracts' own validator refuses it, and "
|
|
858
|
+
+ "the failure reads like a bug in the contracts rather than in the arithmetic.",
|
|
859
|
+
input: {
|
|
860
|
+
admin: addressArg("The address that will administer the new platform."),
|
|
861
|
+
feeRecipient: addressArg("Where the platform's residual share of every fee is paid."),
|
|
862
|
+
creatorShareBps: intArg(0, 7500, 3000, "The creator's share, in basis points OF THE FEE. arcnow.io's own platform uses 3000 "
|
|
863
|
+
+ "— 30% of the fee, 0.30% of a trade."),
|
|
864
|
+
refShareBps: intArg(0, 7500, 1000, "The referrer's share, in basis points of the fee. Paid to the platform when a trade "
|
|
865
|
+
+ "names no referrer."),
|
|
866
|
+
devShareBps: intArg(0, 7500, 1000, "The developer's share, in basis points of the fee. Paid to the platform when a trade "
|
|
867
|
+
+ "names no developer."),
|
|
868
|
+
defaultMigrator: addressArg("The graduation venue every token launched under this platform gets by default. It is "
|
|
869
|
+
+ "snapshotted into each curve at launch and immutable from then on. Use a migrator "
|
|
870
|
+
+ "this deployment actually has — arcnow_network lists them."),
|
|
871
|
+
},
|
|
872
|
+
async run(args, ctx) {
|
|
873
|
+
const newPlatform = {
|
|
874
|
+
admin: args.admin,
|
|
875
|
+
feeRecipient: args.feeRecipient,
|
|
876
|
+
creatorShareBps: Bps.of(BigInt(args.creatorShareBps)),
|
|
877
|
+
refShareBps: Bps.of(BigInt(args.refShareBps)),
|
|
878
|
+
devShareBps: Bps.of(BigInt(args.devShareBps)),
|
|
879
|
+
defaultMigrator: args.defaultMigrator,
|
|
880
|
+
curve: CurveTemplate.arcnowDefaults(),
|
|
881
|
+
};
|
|
882
|
+
// The SDK checks the 7500 allowance client-side and names the residual you
|
|
883
|
+
// are actually choosing. Letting it reject here costs nothing and produces
|
|
884
|
+
// a better sentence than a revert would.
|
|
885
|
+
const { platformShare } = validateNewPlatform(newPlatform);
|
|
886
|
+
const residual = platformShareBps(newPlatform.creatorShareBps, newPlatform.refShareBps, newPlatform.devShareBps);
|
|
887
|
+
const result = await ctx.port.platforms.registerPlatform(newPlatform);
|
|
888
|
+
return {
|
|
889
|
+
text: report(`Registered a new platform at ${addr(result.platform)}.`, section("the split it will apply", [
|
|
890
|
+
["creator", share(newPlatform.creatorShareBps, TRADE_FEE)],
|
|
891
|
+
["referrer", share(newPlatform.refShareBps, TRADE_FEE)],
|
|
892
|
+
["developer", share(newPlatform.devShareBps, TRADE_FEE)],
|
|
893
|
+
["platform (residual)", share(residual, TRADE_FEE)],
|
|
894
|
+
["as registered", `${result.platformShareBps.bps} bps (the registry's own figure; `
|
|
895
|
+
+ `this SDK computed ${platformShare.bps} before sending)`],
|
|
896
|
+
]), section("its defaults", [
|
|
897
|
+
["admin", addr(newPlatform.admin)],
|
|
898
|
+
["fee recipient", addr(newPlatform.feeRecipient)],
|
|
899
|
+
["graduation venue", `${addr(newPlatform.defaultMigrator)} — `
|
|
900
|
+
+ venueOf(newPlatform.defaultMigrator, ctx.port.config)],
|
|
901
|
+
["curve template", "arcnow.io's shipped template, CurveTemplate.arcnowDefaults(): "
|
|
902
|
+
+ `${qty(newPlatform.curve.totalSupply)} supply, ${qty(newPlatform.curve.curveSupply)} on `
|
|
903
|
+
+ `the curve, graduating at ${money(newPlatform.curve.target)}`],
|
|
904
|
+
["transaction", result.txHash],
|
|
905
|
+
]), note("This PlatformConfig now exists permanently. Tokens launched under it carry its "
|
|
906
|
+
+ "split and its venue for their whole lives.")),
|
|
907
|
+
};
|
|
908
|
+
},
|
|
909
|
+
});
|
|
910
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
911
|
+
/**
|
|
912
|
+
* What a curve sell of an ERC-20 quote actually pays: the contract pays
|
|
913
|
+
* `out - out % scale`, whole raw units, and the dust stays in the curve.
|
|
914
|
+
*/
|
|
915
|
+
function paidOut(amount) {
|
|
916
|
+
return amount.floorToRepresentable();
|
|
917
|
+
}
|
|
918
|
+
export const WRITE_TOOLS = [
|
|
919
|
+
launch,
|
|
920
|
+
buy,
|
|
921
|
+
sell,
|
|
922
|
+
migrate,
|
|
923
|
+
withdraw,
|
|
924
|
+
registerPlatform,
|
|
925
|
+
];
|
|
926
|
+
/**
|
|
927
|
+
* What a write tool says when the server is read-only.
|
|
928
|
+
*
|
|
929
|
+
* Deliberately a refusal by name rather than "unknown tool". A model that gets
|
|
930
|
+
* "unknown tool" concludes the server is broken or that it guessed the name
|
|
931
|
+
* wrong, and tries variations; a model that gets this stops, and can tell the
|
|
932
|
+
* user the one true thing — that this is a configuration the operator controls
|
|
933
|
+
* and the conversation cannot.
|
|
934
|
+
*/
|
|
935
|
+
export function writeRefusal(name) {
|
|
936
|
+
return {
|
|
937
|
+
isError: true,
|
|
938
|
+
text: report(`${name} is a write tool and this server is running READ-ONLY. Nothing was sent, `
|
|
939
|
+
+ "nothing was signed, and no key is loaded.", note("This is not something to work around and not something you can be granted mid-"
|
|
940
|
+
+ "conversation. Writes are enabled by whoever starts the server, by restarting it "
|
|
941
|
+
+ "with --allow-writes and a signing key in the ARCNOW_PRIVATE_KEY environment "
|
|
942
|
+
+ "variable. A private key is never an argument to any tool here: a tool call is "
|
|
943
|
+
+ "written into a transcript, and a key that has been through a transcript has been "
|
|
944
|
+
+ "published. Do not ask the user to paste one, and if they offer, tell them to put "
|
|
945
|
+
+ "it in the server's environment instead."), "Everything read-only still works: arcnow_network, arcnow_quote_tokens, arcnow_list_tokens, arcnow_token, "
|
|
946
|
+
+ "arcnow_quote_buy, arcnow_quote_sell, arcnow_quote_launch, arcnow_platform and "
|
|
947
|
+
+ "arcnow_list_platforms. A quote is often the whole answer."),
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
//# sourceMappingURL=write.js.map
|