@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
package/README.md
ADDED
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
> **This repository is a published mirror.** Every release of the arcnow.io MCP
|
|
2
|
+
> server lands here as one commit, tagged `vX.Y.Z`, with the `npm pack` tarball
|
|
3
|
+
> under [Releases](https://github.com/arcnow-io/arcnow-io-mcp/releases). Issues and pull requests are welcome
|
|
4
|
+
> here. The maintainers' tooling (the pin gate against the SDK's history, the
|
|
5
|
+
> fork proof against the contracts) is not part of the mirror, so `scripts/`
|
|
6
|
+
> referred to below is absent; `@arcnow/sdk` is a path dependency, so clone
|
|
7
|
+
> [arcnow-io/arcnow-io-sdk](https://github.com/arcnow-io/arcnow-io-sdk) as a
|
|
8
|
+
> sibling directory named `sdk` and build `sdk/typescript` first.
|
|
9
|
+
> Site: [www.arcnow.io](https://www.arcnow.io) - docs:
|
|
10
|
+
> [docs.arcnow.io](https://docs.arcnow.io).
|
|
11
|
+
|
|
12
|
+
# `@arcnow/mcp`
|
|
13
|
+
|
|
14
|
+
An [MCP](https://modelcontextprotocol.io) server that lets an AI assistant use
|
|
15
|
+
[arcnow.io](https://arcnow.io) on [Arc](https://docs.arc.io/): read the bonding
|
|
16
|
+
curves, quote a trade with its full fee breakdown — on a token's curve before it
|
|
17
|
+
graduates, or in its Uniswap v4 pool after — see how close a token is to
|
|
18
|
+
graduating, and, **only if the operator explicitly turns it on**, launch, buy,
|
|
19
|
+
sell and rescue a stranded migration.
|
|
20
|
+
|
|
21
|
+
Every chain interaction goes through
|
|
22
|
+
[`@arcnow/sdk`](https://github.com/arcnow-io/arcnow-io-sdk), pinned to a commit in
|
|
23
|
+
[`pins.json`](pins.json). Nothing here encodes a call or computes a curve.
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
node dist/index.js # read-only. The default, and the useful part.
|
|
27
|
+
node dist/index.js --allow-writes # can spend, if ARCNOW_PRIVATE_KEY is in the environment.
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## The two rules this server is built around
|
|
33
|
+
|
|
34
|
+
### 1. A private key is never a tool argument
|
|
35
|
+
|
|
36
|
+
No tool here takes a key, a mnemonic or a credential of any kind. The signing
|
|
37
|
+
key is read once, at startup, from `ARCNOW_PRIVATE_KEY` or a file named by
|
|
38
|
+
`ARCNOW_PRIVATE_KEY_FILE`, and from nowhere else.
|
|
39
|
+
|
|
40
|
+
This is not a claim about environment variables being safer in general. It is
|
|
41
|
+
about where a tool argument *goes*. An argument is written by a model into a
|
|
42
|
+
transcript: to the model provider, into the client's logs, into the context of
|
|
43
|
+
every later turn, and into the bug report somebody pastes into an issue. A key
|
|
44
|
+
that has been through a tool call has been published, and there is no
|
|
45
|
+
un-publishing it.
|
|
46
|
+
|
|
47
|
+
So the schemas declare no such field, and — because declaring nothing is not
|
|
48
|
+
enough — **every schema is strict**. An argument this server did not ask for is
|
|
49
|
+
a refusal, not a silently dropped key, and if the field was named like a
|
|
50
|
+
credential the refusal says so and says to rotate it. `test/unit/no-key-arguments.test.ts`
|
|
51
|
+
walks every published schema and asserts both halves.
|
|
52
|
+
|
|
53
|
+
### 2. Read-only is the default, and writing is the operator's decision
|
|
54
|
+
|
|
55
|
+
With an empty environment this server can read everything and spend nothing. The
|
|
56
|
+
write tools are not published at all: a model that cannot see `arcnow_launch`
|
|
57
|
+
does not offer to launch anything.
|
|
58
|
+
|
|
59
|
+
Writing requires `--allow-writes` (or `ARCNOW_MCP_ALLOW_WRITES=1`) **and** a key,
|
|
60
|
+
from whoever starts the process — a person editing a config file, not a model
|
|
61
|
+
mid-conversation. The mode is printed on stderr at startup and reported by
|
|
62
|
+
`arcnow_network`, so nobody has to guess which one they are talking to.
|
|
63
|
+
|
|
64
|
+
Two edge cases, both deliberate:
|
|
65
|
+
|
|
66
|
+
- **A key with no opt-in starts read-only** and warns that the key is present
|
|
67
|
+
and unused. A key is not consent to spend it.
|
|
68
|
+
- **An opt-in with no key refuses to start.** The operator asked for something
|
|
69
|
+
this process cannot do, and a server that quietly downgrades produces its
|
|
70
|
+
failure later, in the middle of somebody's trade.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## The tools, and why each one exists
|
|
75
|
+
|
|
76
|
+
Nine read tools, always published. Six write tools, published only on opt-in.
|
|
77
|
+
|
|
78
|
+
### Read
|
|
79
|
+
|
|
80
|
+
| tool | what it answers |
|
|
81
|
+
| --- | --- |
|
|
82
|
+
| `arcnow_network` | Which chain, which contracts, which graduation venues, whether a v4 router is configured for graduated tokens, **and which mode this server is in**. The first call of any session that might trade. |
|
|
83
|
+
| `arcnow_quote_tokens` | The quote tokens a launch may use — native USDC and the ERC-20s the quote registry allowlists, such as EURC — each with its symbol, name, decimals, address, whether it is native, its launch fee in its own units, whether the registry accepts it now, and **this server's spend cap for it**. At most three `eth_call`s. On a chain with no quote registry (the 2.x contracts Arc testnet ran until the multi-quote reset), it says so, shows the error, and lists the network's own quote-token metadata with nothing known to be accepted. |
|
|
84
|
+
| `arcnow_list_tokens` | Recent launches, newest first, from the launchpad's `Launched` log, back to the block it was deployed in. Reports the block window it actually covered — see [the SDK gap](#what-made-a-clean-surface-awkward). |
|
|
85
|
+
| `arcnow_token` | One token in full: metadata, **its quote token** (address, symbol, decimals, native or ERC-20), its curve's parameters (`r0Wad`, `y0Wad`), curve state, price, progress, and **where it trades now** — its curve, its v4 pool (with the router, whether it reaches the pool, the PoolManager, the pool's LP fee and what the fee hook holds accrued), or nowhere. Takes a token *or* a curve address. |
|
|
86
|
+
| `arcnow_quote_buy` | What an amount of the token's quote (`quoteIn`) would buy, wherever the token trades. On a curve: tokens out, the 1% fee and exactly who receives it, the average fill price, minimum-out floors at four tolerances. In a pool: a quote that says it is a pool quote, with arcnow.io's 1% and the pool's own LP fee apart, the average fill against the pool's spot price, and the price impact. |
|
|
87
|
+
| `arcnow_quote_sell` | The same for a sell — plus, on a curve, that selling needs no approval, ever, and in a pool, that it does, and how much the holder has approved already. |
|
|
88
|
+
| `arcnow_quote_launch` | What a launch in a given `quote` (native USDC by default, or an ERC-20 by symbol or address) would cost before anything is spent, split into the flat fee and the initial buy's own trade fee, with the predicted token and curve addresses. |
|
|
89
|
+
| `arcnow_platform` | A platform's fee split, its default migrator and its curve template — every share printed both as bps of the fee and as a percentage of a trade. |
|
|
90
|
+
| `arcnow_list_platforms` | The registry's actual enumeration. Complete, unlike the token list. |
|
|
91
|
+
|
|
92
|
+
The read tools are the valuable part. An assistant that can answer *what is this
|
|
93
|
+
token, what would 50 EURC buy me, how close is it to graduating* with no key
|
|
94
|
+
anywhere near it is useful to far more people than one that can trade.
|
|
95
|
+
|
|
96
|
+
Three things the read tools go out of their way to say, because each is a
|
|
97
|
+
mistake that costs money:
|
|
98
|
+
|
|
99
|
+
- **A spot price is not a fill price.** The curve integrates price across an
|
|
100
|
+
order, so a buyer pays a rising price over their own trade. Every quote
|
|
101
|
+
reports an *average fill price* next to the spot price, and the tool
|
|
102
|
+
descriptions tell the model to quote rather than multiply.
|
|
103
|
+
- **A share of the fee is not a share of the trade.** 3000 bps of the fee is
|
|
104
|
+
0.30% of a trade. Both are printed, every time.
|
|
105
|
+
- **Graduated and migrated are different states.** A curve can have stopped
|
|
106
|
+
trading permanently while its pool was never created, and then the token
|
|
107
|
+
trades *nowhere*. That is reported as its own thing, with the rescue named.
|
|
108
|
+
- **A pool quote is not a curve quote.** It says so, and it names two charges,
|
|
109
|
+
not one: arcnow.io's 1%, taken in the pool's quote by the fee hook, and the pool's own
|
|
110
|
+
Uniswap LP fee on top.
|
|
111
|
+
|
|
112
|
+
### Write
|
|
113
|
+
|
|
114
|
+
| tool | what it does | irreversible? |
|
|
115
|
+
| --- | --- | --- |
|
|
116
|
+
| `arcnow_launch` | Launches a token and its curve, priced for life in the `quote` it names. For an ERC-20 quote the SDK approves the launchpad for exactly the total first, and the result reports that approve. | **Yes, in every respect.** |
|
|
117
|
+
| `arcnow_buy` | Buys wherever the token trades — its curve, or its v4 pool once migrated. Spends `quoteIn`, in the token's quote; an ERC-20 quote is approved for exactly that first, and the approve is reported. | No — the tokens can be sold back, at a price. |
|
|
118
|
+
| `arcnow_sell` | Sells wherever the token trades. Receives the token's quote. In a pool, grants the router an approval **only** when told to, for exactly the amount sold. | No |
|
|
119
|
+
| `arcnow_migrate` | Creates the pool for a curve that graduated without one. | No — it only ever adds a market. |
|
|
120
|
+
| `arcnow_withdraw_refund` | Claims the quote a curve credited after a failed transfer. | No — it recovers funds. |
|
|
121
|
+
| `arcnow_register_platform` | Deploys a new `PlatformConfig`. Protocol admin only. | The config is permanent. |
|
|
122
|
+
|
|
123
|
+
`arcnow_launch`'s description says, in as many words, that the name, symbol,
|
|
124
|
+
supply, curve shape and graduation venue are fixed at that transaction and can
|
|
125
|
+
never be changed by anyone, that there is no admin key that can fix a typo in a
|
|
126
|
+
symbol, and that the only remedy is another launch and another fee. It also
|
|
127
|
+
requires `acknowledgeIrreversible: true`, which is not security — a model can
|
|
128
|
+
set a boolean — but does make the assertion explicit in the transcript.
|
|
129
|
+
|
|
130
|
+
### How a write is gated, in order
|
|
131
|
+
|
|
132
|
+
1. **The operator's opt-in.** Not published, and refused by name if called
|
|
133
|
+
anyway. (A cached tool list is why the refusal exists as well as the
|
|
134
|
+
omission; "unknown tool" reads as "you guessed wrong" and invites a model to
|
|
135
|
+
try variations.)
|
|
136
|
+
2. **The operator's ceilings, one per quote token.** See
|
|
137
|
+
[spend caps](#spend-caps-per-quote-token-fail-closed). Not arguments, and they
|
|
138
|
+
cannot be raised from a tool call.
|
|
139
|
+
3. **The caller's stated ceiling.** Every spending tool requires
|
|
140
|
+
`maxTotalCost`, in the token's quote: the most the caller believes this call will cost. A fresh
|
|
141
|
+
quote is taken *inside* the call and compared against it. That catches a
|
|
142
|
+
quote that went stale between being shown to a person and being acted on, a
|
|
143
|
+
curve that moved, and a model talked into a bigger number than the
|
|
144
|
+
conversation agreed to — none of which one session-wide ceiling catches.
|
|
145
|
+
4. **No default slippage.** `slippageBps` is required on every trading tool.
|
|
146
|
+
Zero means "fill me at any price", which on a public mempool is a donation,
|
|
147
|
+
so it has to be typed rather than omitted.
|
|
148
|
+
5. **Nothing that does not apply.** A curve trade pays the sender and has no
|
|
149
|
+
`recipient`; a pool swap has no `referrer` or `developer` and cannot graduate
|
|
150
|
+
anything, so `gasLimit` guards nothing there. Each is **refused** on the
|
|
151
|
+
venue it does not exist on, before anything is quoted or sent — never
|
|
152
|
+
silently dropped. The SDK refuses the same things; this server says so first.
|
|
153
|
+
6. **The report.** Each tool states the exact cost and the exact effect, and
|
|
154
|
+
says afterwards what actually happened.
|
|
155
|
+
|
|
156
|
+
None of that makes an assistant trustworthy with money. It makes the blast
|
|
157
|
+
radius a number somebody chose.
|
|
158
|
+
|
|
159
|
+
### The gas trap, which this server handles for you
|
|
160
|
+
|
|
161
|
+
A graduating buy migrates the curve **in its own transaction**, under a bounded
|
|
162
|
+
gas budget whose failure the curve *catches* rather than reverting. So
|
|
163
|
+
`eth_estimateGas` — which searches for the lowest limit at which the transaction
|
|
164
|
+
still succeeds, and a graduating buy succeeds either way — converges on exactly
|
|
165
|
+
the limit at which the migration is starved. The buy fills, the curve graduates,
|
|
166
|
+
the refund is correct, and the pool is simply never created. **There is no revert
|
|
167
|
+
and no error anywhere.**
|
|
168
|
+
|
|
169
|
+
`arcnow_buy` therefore takes a fresh quote, and when that quote says the buy
|
|
170
|
+
graduates it sends an explicit **8,000,000** gas limit rather than letting the
|
|
171
|
+
node estimate one. (The curve budgets 6,000,000 for the migrator and keeps
|
|
172
|
+
100,000 back; a limit is a ceiling, not a charge.) A `gasLimit` below 6,200,000
|
|
173
|
+
on a graduating buy is refused outright. The result reports
|
|
174
|
+
`migratedInThisTransaction`, which is a different question from whether the
|
|
175
|
+
curve graduated — and when the migration was starved anyway, it names
|
|
176
|
+
`arcnow_migrate` as the permissionless rescue.
|
|
177
|
+
|
|
178
|
+
### Trading a graduated token
|
|
179
|
+
|
|
180
|
+
A token's market moves once, irreversibly: from its bonding curve to a Uniswap
|
|
181
|
+
v4 pool, when the curve graduates and migrates. The quote and trade tools go
|
|
182
|
+
through the SDK's `client.trade(token)`, which decides the venue, so the same
|
|
183
|
+
four tools work across that line. Three states, told apart out loud:
|
|
184
|
+
|
|
185
|
+
| state | quote | buy / sell |
|
|
186
|
+
| --- | --- | --- |
|
|
187
|
+
| on its curve | a curve quote, as before | through the curve; `recipient` refused |
|
|
188
|
+
| graduated **and** migrated | a pool quote, labelled as one | through arcnow.io's router; `gasLimit`, `referrer`, `developer` refused |
|
|
189
|
+
| graduated, **not** migrated | refused: tradeable nowhere until `migrate()` | refused, naming `arcnow_migrate` |
|
|
190
|
+
|
|
191
|
+
**What a pool quote contains.** The SDK prices a pool trade by simulating the
|
|
192
|
+
real swap through the router, so the figures are the real fill with two charges
|
|
193
|
+
inside them. The quote takes them back out and names each: arcnow.io's 1%, which
|
|
194
|
+
the fee hook takes in the pool's quote, and the pool's own LP fee — the pool key's `fee`, in
|
|
195
|
+
hundredths of a bip — which Uniswap charges on top. It shows the average fill
|
|
196
|
+
price, the pool's spot price, and the price impact between them. The SDK has no
|
|
197
|
+
reader for a pool's price, so the spot price is the SDK's own quote of a
|
|
198
|
+
probe buy with both fees taken out — 0.000001 of an 18-decimal quote, or 10,000 raw
|
|
199
|
+
units of a smaller one (0.01 EURC), so the fees' raw-unit rounding stays below one
|
|
200
|
+
part in ten thousand — and the report says so.
|
|
201
|
+
|
|
202
|
+
**The pool's quote is the SDK's.** A v4 key orders its currencies by address:
|
|
203
|
+
native USDC is always `currency0`, an ERC-20 such as EURC can be either.
|
|
204
|
+
Nothing here reads `currency0` as the quote; `arcnow_token` says which currency
|
|
205
|
+
of the key the SDK found the quote to be.
|
|
206
|
+
|
|
207
|
+
**The sell approval.** A curve sell needs no approval; a pool sell does, because
|
|
208
|
+
the router pulls the tokens with `transferFrom`. That approval is a separate
|
|
209
|
+
transaction granting spending rights, and it is the one thing this server does
|
|
210
|
+
that grants rights rather than spending money. So `arcnow_sell`:
|
|
211
|
+
|
|
212
|
+
- **never sends it unless the call says `approveRouter: true`.** Without it, a
|
|
213
|
+
sell whose allowance is short is refused, nothing is sent, and the refusal
|
|
214
|
+
says exactly what the approval would be: token, owner, spender, amount;
|
|
215
|
+
- **approves exactly the amount being sold**, never an unlimited allowance, and
|
|
216
|
+
only if the existing allowance does not already cover the sale;
|
|
217
|
+
- **reports it** — token, owner, spender, amount, transaction, and the allowance
|
|
218
|
+
left afterwards — including when the sell that followed it then failed, in
|
|
219
|
+
which case it says the approval still stands.
|
|
220
|
+
|
|
221
|
+
`approveRouter` on a curve sell is refused: nothing would use it.
|
|
222
|
+
|
|
223
|
+
**A revert inside the pool.** Uniswap v4 wraps a revert from inside a hook, and
|
|
224
|
+
a failed transfer out of the PoolManager, in `WrappedError` (`0x90bfb865`). The
|
|
225
|
+
SDK unwraps it: a known inner error keeps its own code with the wrapper layers on
|
|
226
|
+
`details.wrappedBy`, and an empty one is `WrappedRevert`, with each layer named
|
|
227
|
+
— `NativeTransferFailed` or `ERC20TransferFailed` for a transfer. This
|
|
228
|
+
server names the failed transfer from the decoded layers, points at
|
|
229
|
+
`arcnow_quote_buy` for a smaller amount, and shows the SDK's reason and the layers
|
|
230
|
+
once. It adds no cause of its own.
|
|
231
|
+
|
|
232
|
+
**How a failure is said.** A code the SDK raises itself (`SDK_ERROR_CODES`) is a
|
|
233
|
+
*refusal* made before any transaction — `AddressIsNotACurve`,
|
|
234
|
+
`UnknownCurveVersion`, `UnknownHookVersion` and the rest. A
|
|
235
|
+
decoded contract error or revert *failed on-chain*. `RpcFailure` *could not
|
|
236
|
+
reach the chain*: the public endpoint rate-limits, and that says nothing about
|
|
237
|
+
the token.
|
|
238
|
+
|
|
239
|
+
### One curve
|
|
240
|
+
|
|
241
|
+
arcnow.io has one bonding curve — the constant-product curve,
|
|
242
|
+
`arcnow/bonding-curve@3.x.x`, priced in a quote token, with parameters `r0Wad`
|
|
243
|
+
(the virtual quote reserve at launch, in WAD) and `y0Wad` (the virtual token
|
|
244
|
+
reserve at launch) — launched through one contract stack, and a fee hook,
|
|
245
|
+
`arcnow/arc-now-fee-hook@3.x.x`, that
|
|
246
|
+
accrues each fee as a PoolManager claim and pays it out at the start of a later
|
|
247
|
+
swap.
|
|
248
|
+
|
|
249
|
+
- **Nothing a tool prints names a curve kind or a stack.** There is one of
|
|
250
|
+
each. `arcnow_token` prints the curve's own parameters, and a pool trade
|
|
251
|
+
reports the fees it paid out from earlier trades (`FeesDistributed`) apart
|
|
252
|
+
from the trader's fill.
|
|
253
|
+
- **Any other version is refused by name, and never priced.** A curve or a
|
|
254
|
+
platform of another version — the 2.x contracts Arc testnet ran until the
|
|
255
|
+
multi-quote reset and the retired linear curve, `@1.x.x`, included —
|
|
256
|
+
is the SDK's `UnknownCurveVersion`, naming the version, before any quote or
|
|
257
|
+
send. A fee hook of another version is `UnknownHookVersion`, and its accrual
|
|
258
|
+
is not read.
|
|
259
|
+
- **An address that is not a curve is refused by name.** The SDK's
|
|
260
|
+
`AddressIsNotACurve` for an arcnow.io token's address is followed to its curve;
|
|
261
|
+
for anything else — an ordinary ERC-20, an account, nothing at all — it is the
|
|
262
|
+
refusal, saying the address is not an arcnow.io token either.
|
|
263
|
+
- **`arcnow_list_tokens` reads the one launchpad**, back to the block its stack
|
|
264
|
+
was deployed in (61,911,405 on Arc testnet).
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## Configuration
|
|
269
|
+
|
|
270
|
+
Everything is an environment variable, because an MCP client's config file is
|
|
271
|
+
where an operator's decisions belong. See
|
|
272
|
+
[`examples/`](examples) for a read-only and a writes-enabled client config.
|
|
273
|
+
|
|
274
|
+
| variable | default | what it does |
|
|
275
|
+
| --- | --- | --- |
|
|
276
|
+
| `ARCNOW_MCP_NETWORK` | `arc-testnet` | The network preset. `arc-mainnet` is **refused at startup, by name** — arcnow.io is not deployed to one, and this server will not invent an address. |
|
|
277
|
+
| `ARCNOW_MCP_NETWORK_FILE` | unset | A path to a JSON document in the SDK's `CustomNetwork` shape, for a deployment no preset names — a local anvil stack, or a stack deployed onto a fork. **Mutually exclusive with a preset name.** See [below](#a-network-the-sdk-has-no-preset-for). |
|
|
278
|
+
| `ARCNOW_RPC_URL` | the preset's endpoint | Override the endpoint. Redacted of credentials before it is ever printed. |
|
|
279
|
+
| `ARCNOW_MCP_ALLOW_WRITES` | unset | `1` enables the write tools. `--allow-writes` does the same. |
|
|
280
|
+
| `ARCNOW_PRIVATE_KEY` | unset | The signing key. Read once, at startup. Never printed, never serialised, never a tool argument. |
|
|
281
|
+
| `ARCNOW_PRIVATE_KEY_FILE` | unset | The same, from a file — so the key is not in the client's config file either. Setting both is an error rather than a guess. |
|
|
282
|
+
| `ARCNOW_MCP_MAX_SPEND_USDC` | `100` | The ceiling on any single write call spending **native USDC**, in whole USDC. |
|
|
283
|
+
| `ARCNOW_MCP_MAX_SPEND_<SYMBOL>` | **unset: refused** | The ceiling on any single write call spending that quote token, in its own units: `ARCNOW_MCP_MAX_SPEND_EURC=50`. No default. |
|
|
284
|
+
| `ARCNOW_MCP_LOG_CHUNK_BLOCKS` | `10000` | Block range per `eth_getLogs` request in the launch scan. |
|
|
285
|
+
| `ARCNOW_MCP_LOG_MAX_CHUNKS` | `20` | How many such requests one `arcnow_list_tokens` call may make. |
|
|
286
|
+
|
|
287
|
+
There is no secret anywhere in this repository, and nothing here writes one to
|
|
288
|
+
disk. `ARCNOW_PRIVATE_KEY_FILE` is the recommended shape: a client config that
|
|
289
|
+
names a path holds no key even when it is committed or screenshotted.
|
|
290
|
+
|
|
291
|
+
### Spend caps: per quote token, fail-closed
|
|
292
|
+
|
|
293
|
+
A token is priced in one quote for life, and a cap in USDC cannot bound a spend
|
|
294
|
+
in EURC. So every quote token has its own cap, in its own units:
|
|
295
|
+
|
|
296
|
+
- `ARCNOW_MCP_MAX_SPEND_USDC` caps native USDC, and defaults to `100`.
|
|
297
|
+
- `ARCNOW_MCP_MAX_SPEND_<SYMBOL>` caps each other quote — `ARCNOW_MCP_MAX_SPEND_EURC=50`
|
|
298
|
+
is 50 EURC. The symbol is upper-cased; a character that is not a letter or a
|
|
299
|
+
digit is written `_`.
|
|
300
|
+
|
|
301
|
+
It fails closed, four ways:
|
|
302
|
+
|
|
303
|
+
- **A quote with no cap is refused** for every spending write — `arcnow_launch`
|
|
304
|
+
and `arcnow_buy` — with a refusal that names the variable to set. Non-native
|
|
305
|
+
quotes have no default. **A cap can never be got round by switching quote.**
|
|
306
|
+
- **A symbol resolves only against the network's own `quoteTokens`** — the SDK's
|
|
307
|
+
`networks.json`, or `ARCNOW_MCP_NETWORK_FILE` — and a cap is matched to a
|
|
308
|
+
token by address. A token's on-chain `symbol()` is never consulted, so a token
|
|
309
|
+
calling itself EURC does not get EURC's cap, and a quote the network does not
|
|
310
|
+
list can have no cap at all: it is always refused.
|
|
311
|
+
- **A `ARCNOW_MCP_MAX_SPEND_…` variable that names no quote token of the network
|
|
312
|
+
refuses to start** the server, which catches a typo such as `…_EURO`.
|
|
313
|
+
- **Two quote tokens of the network sharing a symbol refuses to start** it: one
|
|
314
|
+
variable cannot mean two caps.
|
|
315
|
+
|
|
316
|
+
What counts: a launch spends its `totalCost` — the launch fee plus the initial
|
|
317
|
+
buy, in the launch's quote; a buy spends its `quoteIn`. For an ERC-20 quote the
|
|
318
|
+
SDK approves exactly that spend, so the same check covers the approve. The
|
|
319
|
+
caller's own `maxTotalCost` is read in the same quote. The startup banner,
|
|
320
|
+
`arcnow_network` and `arcnow_quote_tokens` show every cap, and
|
|
321
|
+
`describe()` reports `maxSpendPerCallUsdc` and `maxSpendPerCall`
|
|
322
|
+
(`{ "USDC": "100", "EURC": "50" }`, or `null` where none is set).
|
|
323
|
+
|
|
324
|
+
### A network the SDK has no preset for
|
|
325
|
+
|
|
326
|
+
`ARCNOW_MCP_NETWORK_FILE=/path/to/network.json` points the server at a
|
|
327
|
+
deployment by its addresses — a local anvil stack, or the 3.x stack this
|
|
328
|
+
repository's fork proof deploys onto a fork of Arc testnet:
|
|
329
|
+
|
|
330
|
+
```json
|
|
331
|
+
{
|
|
332
|
+
"name": "arc-testnet-fork",
|
|
333
|
+
"rpcUrl": "http://127.0.0.1:8545",
|
|
334
|
+
"chainId": 5042002,
|
|
335
|
+
"contracts": { "launchpad": "0x…", "tokenFactory": "0x…", "curveFactory": "0x…",
|
|
336
|
+
"migratorRegistry": "0x…", "platformRegistry": "0x…",
|
|
337
|
+
"arcnowPlatform": "0x…", "quoteRegistry": "0x…",
|
|
338
|
+
"v4Migrator": "0x…", "feeHook": "0x…", "v4Router": "0x…" },
|
|
339
|
+
"v4": { "poolManager": "0x…" },
|
|
340
|
+
"quoteTokens": [
|
|
341
|
+
{ "address": "0x0000000000000000000000000000000000000000", "symbol": "USDC", "name": "USD Coin", "decimals": 18, "isNative": true },
|
|
342
|
+
{ "address": "0x89b50855aa3be2f677cd6303cec089b5f319d72a", "symbol": "EURC", "name": "EURC", "decimals": 6, "isNative": false }
|
|
343
|
+
],
|
|
344
|
+
"quoteAllowanceSlots": { "0x89b50855aa3be2f677cd6303cec089b5f319d72a": 10 }
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
It is validated by the SDK's own `resolveNetwork`: a missing required contract,
|
|
349
|
+
an invalid quote token or unreadable JSON stops the server at startup.
|
|
350
|
+
`quoteAllowanceSlots` may be numbers or decimal strings. `ARCNOW_RPC_URL` still
|
|
351
|
+
overrides the endpoint. Setting it together with `ARCNOW_MCP_NETWORK` or
|
|
352
|
+
`--network=` is an error, not a guess.
|
|
353
|
+
|
|
354
|
+
### Amounts are decimal strings
|
|
355
|
+
|
|
356
|
+
`"25"`, `"1.5"` — never JSON numbers. A JSON number is an IEEE double, and a
|
|
357
|
+
double cannot hold `0.000193050193050194` (the shipped curve's last price) or
|
|
358
|
+
any 18-decimal quantity above about nine million.
|
|
359
|
+
|
|
360
|
+
**Every amount is in the token's own quote, parsed exactly in that quote's
|
|
361
|
+
decimals.** `quoteIn`, `initialBuy` and `maxTotalCost` are read with the SDK's
|
|
362
|
+
`QuoteAmount.parse` in the decimals of the quote they are in — 18 for native
|
|
363
|
+
USDC, 6 for EURC — so `"1.0000001"` of EURC is refused, naming the decimals,
|
|
364
|
+
rather than rounded to an amount nobody typed. Nothing this server prints is a
|
|
365
|
+
bare number: money is always `123.75 USDC` or `1.5 EURC`, in the symbol of the
|
|
366
|
+
quote it is actually in.
|
|
367
|
+
|
|
368
|
+
**On Arc, native USDC is the gas currency, at 18 decimals.** The USDC ERC-20
|
|
369
|
+
predeploy is the same asset reporting 6; it pays for nothing and is not a quote
|
|
370
|
+
token. Gas is always native USDC, whatever a token's quote.
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
## The SDK, and how the pin works
|
|
375
|
+
|
|
376
|
+
`@arcnow/sdk` **is not published to npm.** `package.json` depends on it by
|
|
377
|
+
path — `file:../sdk/typescript` — so a checkout of
|
|
378
|
+
[`arcnow-io/sdk`](https://github.com/arcnow-io/arcnow-io-sdk) has to sit beside this one:
|
|
379
|
+
|
|
380
|
+
```
|
|
381
|
+
project/
|
|
382
|
+
sdk/ git clone https://github.com/arcnow-io/arcnow-io-sdk.git sdk
|
|
383
|
+
mcp/ this repository
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
npm **links** that directory into `node_modules`, which means "which SDK am I
|
|
387
|
+
running" is answered by whatever somebody happens to have checked out — a
|
|
388
|
+
branch, a rebase, an edit made five minutes ago. None of that fails loudly. The
|
|
389
|
+
server still compiles, the tool descriptions still promise what they promised,
|
|
390
|
+
and the call that reaches the chain is encoded by code this repository has never
|
|
391
|
+
seen.
|
|
392
|
+
|
|
393
|
+
So the SDK is pinned the way `arcnow-io/sdk` pins `arcnow-io/contracts`, and
|
|
394
|
+
`scripts/check-pins.sh` (run straight after the install by `scripts/preflight.sh`)
|
|
395
|
+
enforces five things:
|
|
396
|
+
|
|
397
|
+
0. `sdk.commit` is a **full 40-hex commit**, never a ref that resolves to
|
|
398
|
+
whatever it names today.
|
|
399
|
+
1. `package.json` still depends on the SDK **by path**. A `file:` specifier that
|
|
400
|
+
became a version range would resolve through the public registry, under a
|
|
401
|
+
name nobody in this project has claimed.
|
|
402
|
+
2. `node_modules/@arcnow/sdk` really is that sibling directory — linked, not
|
|
403
|
+
copied — carries the pinned version, and its `dist/` is **exactly what its
|
|
404
|
+
sources compile to**. The gate compiles the SDK again, with the SDK's own
|
|
405
|
+
TypeScript, into a temporary directory and compares every emitted file. This
|
|
406
|
+
server imports `dist/`, not `src/`, so a `dist/` left over from an older
|
|
407
|
+
checkout runs old code under new hashes — which is the state the sibling
|
|
408
|
+
checkout was found in when the pin moved off `ac52eaa`, with no `dist/pool.js`
|
|
409
|
+
at all, and the old gate passed it.
|
|
410
|
+
3. The sibling's **working tree** hashes to `surface_sha256`, with **no surface
|
|
411
|
+
file unpinned**. The linked directory *is* the working tree, so an
|
|
412
|
+
uncommitted edit — or an uncommitted new module — is code this server runs
|
|
413
|
+
under no git ref at all.
|
|
414
|
+
4. The sibling repository has `sdk.commit`, **it is on `sdk.branch`**, and it
|
|
415
|
+
carries exactly the pinned files with the pinned bytes — the only check that
|
|
416
|
+
can see that the recorded hashes belong to the commit named. It also
|
|
417
|
+
**reports, without failing**, when the SDK has moved past the pin and whether
|
|
418
|
+
the surface changed on the way. A pin is supposed to lag.
|
|
419
|
+
|
|
420
|
+
Together: `dist/` is the compilation of the working tree, the working tree is the
|
|
421
|
+
pinned bytes, and the pinned bytes are the commit on the branch.
|
|
422
|
+
|
|
423
|
+
**What is pinned is a rule, not a list:** `networks.json`,
|
|
424
|
+
`typescript/package.json`, and every file under `typescript/src` except
|
|
425
|
+
`typescript/src/errors/`. A module the SDK adds is surface the moment it exists.
|
|
426
|
+
The generated ABIs are inside the rule because they encode every call, and the
|
|
427
|
+
generated `networks.json` because it is the copy the SDK compiles its
|
|
428
|
+
**addresses** from. `errors/` is outside it: message prose behind a surface
|
|
429
|
+
`index.ts` already pins, where pinning would make every improved error message
|
|
430
|
+
a false alarm. `test/unit/check-pins.test.ts` builds real repositories and
|
|
431
|
+
breaks each link on purpose — a stale build, an unpinned module, an uncommitted
|
|
432
|
+
edit, a ref for a commit, a commit off the branch.
|
|
433
|
+
|
|
434
|
+
Moving the pin is a commit of its own: check the SDK out at the commit and build
|
|
435
|
+
it, run `scripts/check-pins.sh --record` — which records `sdk.commit` and every
|
|
436
|
+
surface hash from `HEAD`, and refuses an edit no commit carries — update
|
|
437
|
+
`sdk.why`, run preflight, and say what changed in the SDK surface and what it
|
|
438
|
+
meant for the tools. A tool description that still promises what an older SDK
|
|
439
|
+
did is a model quoting a wrong price.
|
|
440
|
+
|
|
441
|
+
Point the gate at a checkout somewhere else with `ARCNOW_SDK_DIR`.
|
|
442
|
+
|
|
443
|
+
---
|
|
444
|
+
|
|
445
|
+
## Running the gates
|
|
446
|
+
|
|
447
|
+
```bash
|
|
448
|
+
./scripts/preflight.sh # install, pins, lint, typecheck, build, tests, fork proof
|
|
449
|
+
./scripts/preflight.sh --no-install
|
|
450
|
+
./scripts/preflight.sh --no-chain # everything but the fork proof
|
|
451
|
+
./scripts/check-pins.sh # just the pin
|
|
452
|
+
npm test # just the unit suite — no chain, no container, no key
|
|
453
|
+
npm run test:fork # just the fork proof — needs Docker
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
`.github/workflows/ci.yml` is `workflow_dispatch:` only, the same decision every
|
|
457
|
+
repository in this org has made. **`scripts/preflight.sh` is the gate**; the
|
|
458
|
+
workflow is a transcription of it, kept because a clean-checkout run is the one
|
|
459
|
+
thing a local run cannot prove.
|
|
460
|
+
|
|
461
|
+
The fork proof starts one container, an anvil fork of Arc testnet from the
|
|
462
|
+
Foundry image `pins.json` pins, labelled `io.arcnow.mcp.test`, and deploys
|
|
463
|
+
arcnow.io's 3.x multi-quote contracts onto it with the pinned SDK's
|
|
464
|
+
`scripts/fork-deploy-stack.sh`. Arc testnet now runs a 3.x stack of its own,
|
|
465
|
+
deployed on 2026-09-15, but the fork is taken at a pinned, already-cached block
|
|
466
|
+
from before that, where the live contracts are the 2.x ones the SDK refuses — and
|
|
467
|
+
a proof that deploys its own stack does not depend on what happens to be live
|
|
468
|
+
anyway. That needs `ARCNOW_CONTRACTS_DIR`, a checkout
|
|
469
|
+
of arcnow-io/contracts at the commit the SDK's `pins.json` names, and forge at
|
|
470
|
+
the release it names. Its harness —
|
|
471
|
+
copied from `arcnow-io/sdk`'s, not imported, because that repository's tests are
|
|
472
|
+
not part of the pinned surface — removes it on every exit path, sweeps only its
|
|
473
|
+
own label, and touches nothing else on a shared daemon.
|
|
474
|
+
|
|
475
|
+
### What the tests prove
|
|
476
|
+
|
|
477
|
+
- **A key is never an argument.** Every published schema is walked for
|
|
478
|
+
credential-shaped field names and for descriptions that ask for one; every
|
|
479
|
+
schema is asserted strict; and passing `privateKey` or `mnemonic` anyway is
|
|
480
|
+
asserted to be refused, named, answered with "rotate it", and to leave the
|
|
481
|
+
port untouched.
|
|
482
|
+
- **A write refuses without the opt-in.** Every write tool, called on a
|
|
483
|
+
read-only server, is asserted to error, to name `--allow-writes` and
|
|
484
|
+
`ARCNOW_PRIVATE_KEY`, to tell the model not to ask a user for a key — and,
|
|
485
|
+
the assertion that carries the weight, **to have sent nothing**.
|
|
486
|
+
- **Both spend ceilings stop a transaction rather than annotate one**, on a
|
|
487
|
+
curve and in a pool alike, **per quote token**: a USDC-only configuration
|
|
488
|
+
refuses a EURC buy and launch; an EURC cap allows up to it and refuses a raw
|
|
489
|
+
unit above; a typo'd cap variable refuses to start; a USDC cap does not apply
|
|
490
|
+
to EURC or the reverse; a quote the network does not list is refused.
|
|
491
|
+
- **Every amount is labelled with its own quote** — native, a 6-decimal and an
|
|
492
|
+
18-decimal ERC-20 — a pool's quote is the SDK's in either currency order, an
|
|
493
|
+
input with more decimals than its quote is refused, and an ERC-20 spend's
|
|
494
|
+
approve is reported whether it was sent or not.
|
|
495
|
+
- **The gas trap is handled**, for a buy and for a launch whose initial buy
|
|
496
|
+
graduates the curve.
|
|
497
|
+
- **The venues are told apart.** A migrated token is quoted and traded in its
|
|
498
|
+
pool through `client.trade`; a stranded one is refused everywhere with
|
|
499
|
+
`arcnow_migrate` named; a curve-only parameter in a pool and a pool-only one
|
|
500
|
+
on a curve are refused with nothing sent.
|
|
501
|
+
- **The sell approval is gated and disclosed.** Never sent without
|
|
502
|
+
`approveRouter: true`, never for more than the amount sold, not sent when the
|
|
503
|
+
allowance already covers the sale, reported in full, and still reported when
|
|
504
|
+
the sell after it fails.
|
|
505
|
+
- **The reports say true things.** Fees broken out, a pool's two charges apart,
|
|
506
|
+
an absent referrer's share named as going to the platform, the average fill
|
|
507
|
+
price distinguished from the spot price, graduated distinguished from
|
|
508
|
+
migrated, an undecodable pool revert explained cautiously rather than bare.
|
|
509
|
+
- **The pin gate fails when it should.** See above.
|
|
510
|
+
- **The wire works.** A real MCP client against a real MCP server over an
|
|
511
|
+
in-memory transport, and — in the fork proof — over stdio against the built
|
|
512
|
+
server.
|
|
513
|
+
- **Against the 3.x contracts deployed onto a fork of Arc testnet**
|
|
514
|
+
(`test/fork/mcp.fork.test.ts`), the built server — started with
|
|
515
|
+
`ARCNOW_MCP_NETWORK_FILE` naming the deployed stack — driven by a real MCP
|
|
516
|
+
client lists the quote tokens from the deployed registry in at most three
|
|
517
|
+
`eth_call`s with each one's cap, launches a native USDC
|
|
518
|
+
token on the launchpad, reads it
|
|
519
|
+
back with its quote token, `r0Wad` and `y0Wad`, buys it on its curve,
|
|
520
|
+
watches an impersonated Arc account graduate it, then quotes, buys and sells it
|
|
521
|
+
in its Uniswap v4 pool through the live router — and sells a second token back
|
|
522
|
+
to its curve — and launches a token in EURC, with its exact approve and exactly
|
|
523
|
+
the total pulled, buys it in EURC, and has a buy above the 50 EURC cap refused
|
|
524
|
+
with nothing sent. Every figure is checked against the chain with viem, not through
|
|
525
|
+
the server: every quote equals its fill, to the wei; exactly the amount and
|
|
526
|
+
the gas left the signer; the sell's approval is one `Approval` log to the
|
|
527
|
+
router for exactly the amount sold, used up by the sale; the recipient is paid
|
|
528
|
+
exactly what the report says; a swap's fee payout matches the hook's
|
|
529
|
+
`FeesDistributed` log; the refusals leave the signer's nonce where it was; and
|
|
530
|
+
the USDC ERC-20 predeploy comes back as `AddressIsNotACurve`. The server's RPC
|
|
531
|
+
goes through a counting proxy, and the per-call request counts are printed.
|
|
532
|
+
- **One curve, and nothing else priced.** Against the fake, whose version
|
|
533
|
+
checks are the SDK's own `assertCurveVersion` and `assertPlatformVersion`: no
|
|
534
|
+
report names a curve kind or a stack, an `@1` curve, platform or fee hook is
|
|
535
|
+
refused by name with nothing quoted or sent, and the launch scan reads the one
|
|
536
|
+
launchpad from its deployment block.
|
|
537
|
+
|
|
538
|
+
### What they do not prove
|
|
539
|
+
|
|
540
|
+
- **Not Arc's own execution semantics.** A fork re-executes locally with
|
|
541
|
+
anvil's EVM and disagrees with Arc about blocklisted transfers, EIP-1153, the
|
|
542
|
+
EIP-7708 system emitter and burn-to-zero without saying so.
|
|
543
|
+
- **Not a EURC pool against a chain.** The fork proof launches a EURC token,
|
|
544
|
+
buys it on its curve with the exact approve, and holds the 50 EURC cap; a
|
|
545
|
+
EURC token's pool is unit-tested against the fake.
|
|
546
|
+
- **Not every path against a chain.** The fork proof covers a launch, curve
|
|
547
|
+
trades, graduation and pool trades. A
|
|
548
|
+
migration through `arcnow_migrate`, a graduating buy through `arcnow_buy` (the
|
|
549
|
+
proof graduates its token through the SDK, so that nothing credits the
|
|
550
|
+
server's signer), a token that graduated but never migrated — there is none on
|
|
551
|
+
Arc testnet to fork — and a curve, platform or hook whose version the SDK
|
|
552
|
+
refuses are
|
|
553
|
+
unit-tested here against the fake, and proved against a chain, where they are
|
|
554
|
+
at all, by the SDK's own fork suite. If you change how a tool calls the SDK,
|
|
555
|
+
run the SDK's preflight too.
|
|
556
|
+
- **Not that the tool descriptions are true.** Those are prose, read by a model
|
|
557
|
+
deciding whether to spend somebody's money. When the pin moves, that is a
|
|
558
|
+
person's job.
|
|
559
|
+
- **Not that a model behaves.** Every guard in this server is a bound on
|
|
560
|
+
damage, not a guarantee of judgement.
|
|
561
|
+
|
|
562
|
+
---
|
|
563
|
+
|
|
564
|
+
## What made a clean surface awkward
|
|
565
|
+
|
|
566
|
+
Five things, all worth fixing upstream rather than here — and one of them now is.
|
|
567
|
+
|
|
568
|
+
**The SDK cannot enumerate tokens.** There is no `recentLaunches()` anywhere in
|
|
569
|
+
`@arcnow/sdk`, and its `networks.json` explains why a preset cannot carry one: a
|
|
570
|
+
token and its curve come from the `Launched` log, one pair per launch, and there
|
|
571
|
+
are as many as there have been launches. But "show me the recent tokens" is the
|
|
572
|
+
first thing anybody asks an assistant, so `arcnow_list_tokens` reads that log
|
|
573
|
+
directly — with the SDK's own pinned ABI, through the SDK's own configured
|
|
574
|
+
client, at the SDK's own launchpad address, adding nothing but the scan. It is
|
|
575
|
+
the only place in this repository that talks to the chain outside an SDK method,
|
|
576
|
+
and it is in `src/sdk-port.ts` with a comment saying so. A
|
|
577
|
+
`launchpad.recentLaunches()` belongs in the SDK, where the forked-chain suite
|
|
578
|
+
could test it against a real chain; here it is tested against a fake.
|
|
579
|
+
|
|
580
|
+
Because the scan is a bounded walk backwards from the tip rather than an index,
|
|
581
|
+
`arcnow_list_tokens` reports the block window it covered and says plainly when
|
|
582
|
+
it stopped on its budget with history unread. An assistant must not conclude a
|
|
583
|
+
token does not exist from a tool that only ever saw a window.
|
|
584
|
+
|
|
585
|
+
**The SDK's handles are classes with private fields.** `Curve`, `Token`,
|
|
586
|
+
`Launchpad` and `PlatformRegistry` are nominally typed, so nothing can be
|
|
587
|
+
assigned to them — a test cannot construct a stand-in, and there is no seam to
|
|
588
|
+
substitute one. That is why `src/sdk-port.ts` exists: a set of interfaces the
|
|
589
|
+
SDK's handles already satisfy, wrapping nothing and computing nothing, so that a
|
|
590
|
+
fake can drive every tool through the states that matter and are hardest to
|
|
591
|
+
reach on a real chain (a buy that graduates, a curve that graduated and never
|
|
592
|
+
migrated, a cost that moved between the quote and the order). If the SDK exposed
|
|
593
|
+
interfaces alongside its classes, that file would be a re-export.
|
|
594
|
+
|
|
595
|
+
**The SDK has no reader for a pool's price.** A pool quote is the real fill, but
|
|
596
|
+
"how far is that from the pool's price" needs the pool's price, and there is no
|
|
597
|
+
`slot0`/`sqrtPriceX96` read anywhere in `@arcnow/sdk`. Reading the PoolManager's
|
|
598
|
+
storage here would be the second, unpinned copy of chain code this server exists
|
|
599
|
+
not to have. So the spot price is the SDK's own quote of a tiny probe buy with
|
|
600
|
+
both fees taken out — accurate to far below the printed digits, one extra
|
|
601
|
+
`eth_call`, and labelled as what it is. A `pool.spotPrice()` belongs in the SDK.
|
|
602
|
+
The pool's LP fee is likewise not in a quote; it is read from `pool.key()`.
|
|
603
|
+
|
|
604
|
+
**Fixed upstream: a revert inside the pool.** A revert from inside the fee hook,
|
|
605
|
+
or a failed transfer out of the PoolManager, arrives wrapped in Uniswap v4's
|
|
606
|
+
`WrappedError`. It used to surface as a bare `UnknownRevert`. Since sdk#7 the SDK
|
|
607
|
+
unwraps it and names the failed transfer (`NativeTransferFailed` /
|
|
608
|
+
`ERC20TransferFailed`), so this server only shows what the SDK decoded.
|
|
609
|
+
|
|
610
|
+
**A pool sell quote needs a real holder.** The SDK overrides the router
|
|
611
|
+
allowance for a sell simulation but deliberately not the balance, so
|
|
612
|
+
`arcnow_quote_sell` on a pool needs `holder` on a read-only server. That is the
|
|
613
|
+
right call, and it is why that argument exists.
|
|
614
|
+
|
|
615
|
+
Nothing else got in the way. In particular, the SDK's four amount types, its
|
|
616
|
+
refusal to default a slippage floor, its explicit `gasLimit`, its
|
|
617
|
+
`migratedInThisTransaction` flag and its by-selector error decoding are each the
|
|
618
|
+
reason a corresponding class of mistake is not reachable from here.
|
|
619
|
+
|
|
620
|
+
---
|
|
621
|
+
|
|
622
|
+
## Licence
|
|
623
|
+
|
|
624
|
+
GPL-3.0-or-later, the same licence as `@arcnow/sdk`, which this server builds on. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The options the SDK client is built from, out of a {@link ServerConfig}.
|
|
3
|
+
*
|
|
4
|
+
* Its own module, rather than three lines in `index.ts`, so the one thing that
|
|
5
|
+
* decides which deployment the server talks to — the preset or the network
|
|
6
|
+
* file, the endpoint override, the signer — is testable without starting a
|
|
7
|
+
* process.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
import type { ArcNowClientOptions, NetworkConfig } from "@arcnow/sdk";
|
|
12
|
+
import type { ServerConfig } from "./config.js";
|
|
13
|
+
export declare function clientOptionsFor(config: ServerConfig): ArcNowClientOptions & {
|
|
14
|
+
readonly network: NetworkConfig;
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=client-options.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client-options.d.ts","sourceRoot":"","sources":["../src/client-options.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEtE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,YAAY,GACnB,mBAAmB,GAAG;IAAE,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAA;CAAE,CAU3D"}
|