@temporalabs/treasury 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.
Files changed (55) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +10 -0
  3. package/README.md +153 -0
  4. package/THIRD_PARTY_NOTICES.md +366 -0
  5. package/dist/abi/erc4626.d.ts +243 -0
  6. package/dist/abi/erc4626.js +146 -0
  7. package/dist/abi/erc4626.js.map +1 -0
  8. package/dist/build.d.ts +88 -0
  9. package/dist/build.js +124 -0
  10. package/dist/build.js.map +1 -0
  11. package/dist/client.d.ts +429 -0
  12. package/dist/client.js +248 -0
  13. package/dist/client.js.map +1 -0
  14. package/dist/config/earn.d.ts +47 -0
  15. package/dist/config/earn.js +46 -0
  16. package/dist/config/earn.js.map +1 -0
  17. package/dist/disclosures.d.ts +14 -0
  18. package/dist/disclosures.js +28 -0
  19. package/dist/disclosures.js.map +1 -0
  20. package/dist/index.d.ts +14 -0
  21. package/dist/index.js +10 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/links.d.ts +21 -0
  24. package/dist/links.js +32 -0
  25. package/dist/links.js.map +1 -0
  26. package/dist/mcp/server.d.ts +33 -0
  27. package/dist/mcp/server.js +346 -0
  28. package/dist/mcp/server.js.map +1 -0
  29. package/dist/mcp-server.mjs +56281 -0
  30. package/dist/position.d.ts +142 -0
  31. package/dist/position.js +354 -0
  32. package/dist/position.js.map +1 -0
  33. package/dist/preflight.d.ts +62 -0
  34. package/dist/preflight.js +165 -0
  35. package/dist/preflight.js.map +1 -0
  36. package/dist/quote.d.ts +55 -0
  37. package/dist/quote.js +83 -0
  38. package/dist/quote.js.map +1 -0
  39. package/dist/redact.d.ts +44 -0
  40. package/dist/redact.js +92 -0
  41. package/dist/redact.js.map +1 -0
  42. package/dist/registry-schema.d.ts +127 -0
  43. package/dist/registry-schema.js +124 -0
  44. package/dist/registry-schema.js.map +1 -0
  45. package/dist/registry.d.ts +26 -0
  46. package/dist/registry.js +62 -0
  47. package/dist/registry.js.map +1 -0
  48. package/dist/units.d.ts +8 -0
  49. package/dist/units.js +24 -0
  50. package/dist/units.js.map +1 -0
  51. package/dist/version.d.ts +1 -0
  52. package/dist/version.js +14 -0
  53. package/dist/version.js.map +1 -0
  54. package/package.json +58 -0
  55. package/registry/vaults.json +68 -0
package/dist/client.js ADDED
@@ -0,0 +1,248 @@
1
+ import { createPublicClient, http } from "viem";
2
+ import { base } from "viem/chains";
3
+ export const chains = { 8453: base };
4
+ export function isSupportedChainId(id) {
5
+ return id in chains;
6
+ }
7
+ /** The public endpoint used when nothing is configured, and the fallback for event scans. */
8
+ export const PUBLIC_RPC = { 8453: "https://mainnet.base.org" };
9
+ /**
10
+ * A `fetch` that waits out HTTP 429 instead of failing the tool. Measured 2026-09-14: a free-tier
11
+ * keyed Base RPC answered an `earn_quote` read with 429 and the tool failed outright, because viem's
12
+ * own retries (3, from 150 ms) are spent in under a second. Only a 429 is retried here, up to 5 times,
13
+ * honouring `Retry-After` when present and otherwise backing off from 500 ms, with the sleeps capped at
14
+ * `RATE_LIMIT_WAIT_MS` in total whatever the headers say.
15
+ *
16
+ * The transport's deadline (`RPC_TIMEOUT_MS`, viem's own 10 s) wraps this whole loop and is enforced by
17
+ * ABORT: viem only reports its `TimeoutError` when the fetch throws an abort error, and resolves a value
18
+ * returned after the abort as-is. So the sleep races the abort signal, and on abort the LAST 429 is
19
+ * handed back — the caller then sees the provider's 429, never "The request took too long to respond".
20
+ * Measured 2026-09-17 before this: a keyed provider at its monthly cap answered 429 in 77 ms, the loop
21
+ * slept through the deadline, and a whole live tier read as "the RPC is slow" when the key was out of
22
+ * quota. The deadline itself is NOT raised: a host that accepts and never answers must still fail at
23
+ * 10 s (measured in review: 25.5 s made `earn_status` 2.5× slower on exactly that case).
24
+ *
25
+ * A 429 whose body carries the one measured exhausted-plan sentence is returned at once — it resets
26
+ * next period, not in seconds. ONLY that sentence: a bare /capacity/ also matched Alchemy's per-second
27
+ * throttle body ("exceeded its compute units per second capacity") and a "temporarily at capacity"
28
+ * message, and would have disabled the wait on the exact case it exists for (measured in review).
29
+ * An unrecognised body is waited out and then reported plainly — the safe direction.
30
+ * An unreachable host is deliberately NOT stretched out: that is a fault to report, not a limit to
31
+ * wait for, and `earn_status` must answer quickly when the RPC is down.
32
+ */
33
+ export const RATE_LIMIT_WAIT_MS = 15_500;
34
+ /** The transport's whole-call deadline — viem's default, set explicitly because the wrapper's design depends on it. */
35
+ export const RPC_TIMEOUT_MS = 10_000;
36
+ /** Measured 2026-09-17 on a keyed provider at its cap: `Monthly capacity limit exceeded. … billing …`. */
37
+ const QUOTA_EXHAUSTED = /\b(monthly|daily) capacity limit exceeded\b/i;
38
+ export function makeRateLimitedFetch(base = fetch, sleep = (ms) => new Promise((r) => setTimeout(r, ms).unref?.())) {
39
+ return async (input, init) => {
40
+ const signal = init?.signal ?? null;
41
+ const aborted = new Promise((r) => {
42
+ if (!signal)
43
+ return;
44
+ if (signal.aborted)
45
+ r();
46
+ else
47
+ signal.addEventListener("abort", () => r(), { once: true });
48
+ });
49
+ let waited = 0;
50
+ for (let attempt = 0;; attempt++) {
51
+ const res = await base(input, init);
52
+ if (res.status !== 429 || attempt >= 5 || waited >= RATE_LIMIT_WAIT_MS)
53
+ return res;
54
+ // Read a COPY of the body: the caller (viem) still needs the original to report the provider's message.
55
+ if (QUOTA_EXHAUSTED.test(await res.clone().text().catch(() => "")))
56
+ return res;
57
+ const after = Number(res.headers.get("retry-after"));
58
+ const want = Number.isFinite(after) && after > 0 ? Math.min(after, 10) * 1000 : 500 * 2 ** attempt;
59
+ const ms = Math.min(want, RATE_LIMIT_WAIT_MS - waited);
60
+ waited += ms;
61
+ await Promise.race([sleep(ms), aborted]);
62
+ if (signal?.aborted)
63
+ return res; // the deadline passed while waiting: report the 429, not a timeout
64
+ }
65
+ };
66
+ }
67
+ export function makePublicClient(chainId, rpcUrl) {
68
+ // `retryCount: 0` because THIS wrapper owns the retry policy. viem's own retry sits OUTSIDE the
69
+ // transport's fetchFn and also retries 429, so the two multiply — measured in review: one
70
+ // persistently rate-limited request made 24 underlying fetch calls, not 6, and took ~62 s instead of
71
+ // the ~15 s this file claimed.
72
+ // `timeout` is viem's default, named so the wrapper's abort-race above is pinned to it (see above).
73
+ //
74
+ // 🔴 `ccipRead: false` closes the one path by which this client could contact a host the operator
75
+ // never configured. viem enables CCIP-read by default: a contract that reverts with
76
+ // `OffchainLookup` hands back a URL and viem fetches it — an arbitrary host, chosen by the
77
+ // contract, outside the RPC. Treasury reads only registry vaults and needs no off-chain
78
+ // resolution, so the capability is pure exposure here, and "the RPC you configure and nothing
79
+ // else" is only true with it off. A test pins it.
80
+ return createPublicClient({ chain: chains[chainId], ccipRead: false, transport: http(rpcUrl, { fetchFn: makeRateLimitedFetch(), retryCount: 0, timeout: RPC_TIMEOUT_MS }) });
81
+ }
82
+ /**
83
+ * An env value counts as an RPC URL only if it parses as one with an http(s) scheme. Anything else —
84
+ * empty, whitespace, a non-URL, and in particular an UNEXPANDED placeholder like `${TREASURY_RPC_BASE}` —
85
+ * is treated as unset. Measured 2026-09-12: Claude Code forwards the plugin's `.mcp.json` value
86
+ * `${TREASURY_RPC_BASE}` verbatim when the host variable is unset, and with the old `if (v)` test every
87
+ * RPC-touching tool died on "Failed to parse URL from ${TREASURY_RPC_BASE}". `.mcp.json` now also uses
88
+ * `${VAR:-}`, but this guard is what makes the server independent of any host's expansion rules.
89
+ */
90
+ export function rpcUrlFromEnvValue(v) {
91
+ if (!v)
92
+ return undefined;
93
+ const t = v.trim();
94
+ if (t.length === 0 || t.includes("${"))
95
+ return undefined;
96
+ try {
97
+ const u = new URL(t);
98
+ return u.protocol === "http:" || u.protocol === "https:" ? t : undefined;
99
+ }
100
+ catch {
101
+ return undefined;
102
+ }
103
+ }
104
+ /**
105
+ * Resolves the RPC URL for a chain from the environment. ⚠️ A keyed provider URL IS a secret: it must
106
+ * never appear in tool output — every error crossing the tool boundary goes through `describeError`
107
+ * (`src/redact.ts`), which strips endpoints. Fallback names after `TREASURY_RPC_BASE`:
108
+ * `BASE_RPC_URL`; the plugin's `.mcp.json` passes only the `TREASURY_*` pair through, so under the
109
+ * packaged path the fallback is unreachable.
110
+ * `TREASURY_LOGS_RPC_BASE`, if set, is preferred for log scans (providers cap eth_getLogs ranges very
111
+ * differently: Alchemy free 10 blocks, Base public 2,000) — see `position.ts`.
112
+ */
113
+ export function rpcUrlFromEnv(chainId) {
114
+ const candidates = chainId === 8453 ? ["TREASURY_RPC_BASE", "BASE_RPC_URL"] : [];
115
+ for (const k of candidates) {
116
+ const v = rpcUrlFromEnvValue(process.env[k]);
117
+ if (v)
118
+ return v;
119
+ }
120
+ return PUBLIC_RPC[chainId];
121
+ }
122
+ /**
123
+ * The endpoint an event scan falls back to when the configured logs RPC cannot cover the range — and
124
+ * the operator's opt-out from it. `TREASURY_LOGS_FALLBACK` unset ⇒ Base's public endpoint; an http(s)
125
+ * URL ⇒ that endpoint; ANY other value — `off`, `none`, `disabled`, a typo — ⇒ no fallback at all, and
126
+ * the scan degrades to a cut-short window as it did before the fallback existed. It exists because the fallback
127
+ * otherwise sends a query to a third party the operator never named, which some
128
+ * deployments cannot accept — and it fails CLOSED for the same reason.
129
+ */
130
+ export function logsFallbackUrlFromEnv(chainId, logsUrl) {
131
+ const raw = (process.env["TREASURY_LOGS_FALLBACK"] ?? "").trim();
132
+ // Unset, empty, or an unexpanded `${VAR}` placeholder: the default fallback.
133
+ // ⚠️ The placeholder is THE ONE DELIBERATE EXCEPTION to fail-closed below.
134
+ // An MCP host forwards `${TREASURY_LOGS_FALLBACK}` verbatim when the host variable is unset, so a
135
+ // placeholder means the operator never set it — and unset must behave exactly as absent, or every
136
+ // plugin install without the variable silently loses its fallback. The function cannot tell "the
137
+ // host did not expand it" from "someone typed it"; the second reading is treated as implausible on
138
+ // purpose, and this comment is the record of that choice.
139
+ if (raw.length === 0 || raw.includes("${"))
140
+ return sameOrUndefined(PUBLIC_RPC[chainId], logsUrl);
141
+ // 🔴 An operator who set this variable at all was constraining where this process talks. Reading an
142
+ // unrecognised value as "use the default third party" is the one outcome they cannot have meant
143
+ // (measured in review: `disabled` silently kept the public fallback until this failed closed).
144
+ const url = rpcUrlFromEnvValue(raw);
145
+ return url === undefined ? undefined : sameOrUndefined(url, logsUrl);
146
+ }
147
+ /** A fallback that is the same endpoint as the primary is not a fallback. */
148
+ function sameOrUndefined(url, logsUrl) {
149
+ return url === logsUrl ? undefined : url;
150
+ }
151
+ /** RPC to use for eth_getLogs scans: a dedicated one if configured, else the general one. */
152
+ export function logsRpcUrlFromEnv(chainId) {
153
+ return rpcUrlFromEnvValue(process.env["TREASURY_LOGS_RPC_BASE"]) ?? rpcUrlFromEnv(chainId);
154
+ }
155
+ /**
156
+ * Which environment variable supplied the RPC URL — the NAME only, never the value. A keyed provider
157
+ * URL is a secret, so `earn_status` reports provenance and nothing else. `configured: false` means
158
+ * no variable resolved and the public endpoint is in use (rate-limited, never a failure).
159
+ */
160
+ export function rpcSourceForEnv(chainId) {
161
+ const candidates = chainId === 8453 ? ["TREASURY_RPC_BASE", "BASE_RPC_URL"] : [];
162
+ for (const k of candidates) {
163
+ if (rpcUrlFromEnvValue(process.env[k]))
164
+ return { source: k, configured: true };
165
+ }
166
+ return { source: "public default", configured: false };
167
+ }
168
+ /**
169
+ * Every RPC secret VALUE this process could be holding — the resolved URLs plus the parts of them
170
+ * that carry the credential (path segments, query values, and a subdomain-keyed provider's first
171
+ * label). Fed to `registerSecretSource` so the redactor masks the literal wherever it appears,
172
+ * including inside a provider's own error body, where no shape rule can reach it.
173
+ *
174
+ * ⚠️ Returns VALUES, and is used only to mask them. It must never be logged or returned by a tool.
175
+ */
176
+ export function resolvedRpcSecrets() {
177
+ const out = new Set();
178
+ // 🔴 BOTH FORMS. A credential sits in the URL percent-ENCODED; a provider echoes the DECODED one.
179
+ // Measured by review: for `sk-Live_a+b/c=d9f8e7` the URL carries
180
+ // `sk-Live_a%2Bb%2Fc%3Dd9f8e7`, only that was registered, and the plain key leaked. The earlier
181
+ // test missed it because `SECRETKEY123` percent-encodes to ITSELF — the transformation was the
182
+ // identity function, so the test could not tell the two forms apart.
183
+ const add = (v) => {
184
+ if (!v || v.length < 8)
185
+ return;
186
+ out.add(v);
187
+ try {
188
+ const decoded = decodeURIComponent(v);
189
+ if (decoded !== v && decoded.length >= 8)
190
+ out.add(decoded);
191
+ }
192
+ catch {
193
+ /* a malformed percent-sequence is not a second form */
194
+ }
195
+ };
196
+ const urls = new Set();
197
+ // Every variable that can name a URL the package will CALL — the logs fallback included: it builds a
198
+ // real client, and a provider's error body can echo its key exactly as the primary's (measured in
199
+ // review; dormant until describeError began reporting the body).
200
+ for (const k of ["TREASURY_RPC_BASE", "TREASURY_LOGS_RPC_BASE", "BASE_RPC_URL", "TREASURY_LOGS_FALLBACK"]) {
201
+ const v = rpcUrlFromEnvValue(process.env[k]);
202
+ if (v)
203
+ urls.add(v);
204
+ }
205
+ for (const u of urls) {
206
+ add(u);
207
+ try {
208
+ const p = new URL(u);
209
+ for (const seg of p.pathname.split("/"))
210
+ add(seg); // raw: the usual place a key sits
211
+ // ⚠️ Query values need BOTH readings. `searchParams` decodes `+` as a SPACE (form-encoding),
212
+ // so for `?apikey=sk+Live_abcdefgh` it yields `sk Live_abcdefgh` while the provider echoes
213
+ // the literal `sk+Live_abcdefgh` — the same defect as the path form, on the other branch
214
+ // (review). Register the raw token as well as what searchParams parsed.
215
+ for (const [, val] of p.searchParams)
216
+ add(val);
217
+ for (const pair of p.search.replace(/^\?/, "").split("&")) {
218
+ const eq = pair.indexOf("=");
219
+ if (eq >= 0)
220
+ add(pair.slice(eq + 1));
221
+ }
222
+ add(p.hostname.split(".")[0]); // QuickNode-style subdomain keys
223
+ }
224
+ catch {
225
+ /* rpcUrlFromEnvValue already parsed it; unreachable */
226
+ }
227
+ }
228
+ return [...out];
229
+ }
230
+ /**
231
+ * The one sentence a caller needs when the public endpoint is what broke them.
232
+ *
233
+ * 🔴 Measured 2026-09-12 through the installed plugin with no configuration: the deposit pre-flight
234
+ * returns UNRESOLVED 3/3 on `mainnet.base.org` ("RPC Request failed") and NEEDS_APPROVAL 3/3 on a
235
+ * keyed URL. So zero-config can browse vaults and read terms but CANNOT complete a deposit — and the
236
+ * message it fails with reads as "this product is broken" rather than "you need a key". Returns
237
+ * undefined when a keyed RPC resolved, so a real chain fault is never mislabelled as a setup problem.
238
+ */
239
+ export function publicRpcHint(chainId) {
240
+ if (rpcSourceForEnv(chainId).configured)
241
+ return undefined;
242
+ return ("No RPC was configured, so this used the public endpoint (mainnet.base.org), which rate-limits " +
243
+ "after a handful of calls — that is the most likely cause of the failure above, not the vault. " +
244
+ "Set TREASURY_RPC_BASE to a keyed Base RPC URL (Alchemy, Infura, QuickNode or your own node) and " +
245
+ "retry. Reads like the vault list and the disclosures work without one; quotes, pre-flight and " +
246
+ "position history generally do not.");
247
+ }
248
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,IAAI,EAAyC,MAAM,MAAM,CAAC;AACvF,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAEnC,MAAM,CAAC,MAAM,MAAM,GAAG,EAAE,IAAI,EAAE,IAAI,EAAW,CAAC;AAG9C,MAAM,UAAU,kBAAkB,CAAC,EAAU;IAC3C,OAAO,EAAE,IAAI,MAAM,CAAC;AACtB,CAAC;AASD,6FAA6F;AAC7F,MAAM,CAAC,MAAM,UAAU,GAAqC,EAAE,IAAI,EAAE,0BAA0B,EAAE,CAAC;AAEjG;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,MAAM,CAAC;AACzC,uHAAuH;AACvH,MAAM,CAAC,MAAM,cAAc,GAAG,MAAM,CAAC;AACrC,0GAA0G;AAC1G,MAAM,eAAe,GAAG,8CAA8C,CAAC;AAEvE,MAAM,UAAU,oBAAoB,CAClC,OAAqB,KAAK,EAC1B,QAAuC,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC;IAE9F,OAAO,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;QAC3B,MAAM,MAAM,GAAG,IAAI,EAAE,MAAM,IAAI,IAAI,CAAC;QACpC,MAAM,OAAO,GAAG,IAAI,OAAO,CAAO,CAAC,CAAC,EAAE,EAAE;YACtC,IAAI,CAAC,MAAM;gBAAE,OAAO;YACpB,IAAI,MAAM,CAAC,OAAO;gBAAE,CAAC,EAAE,CAAC;;gBACnB,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QACnE,CAAC,CAAC,CAAC;QACH,IAAI,MAAM,GAAG,CAAC,CAAC;QACf,KAAK,IAAI,OAAO,GAAG,CAAC,GAAI,OAAO,EAAE,EAAE,CAAC;YAClC,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;YACpC,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,IAAI,OAAO,IAAI,CAAC,IAAI,MAAM,IAAI,kBAAkB;gBAAE,OAAO,GAAG,CAAC;YACnF,wGAAwG;YACxG,IAAI,eAAe,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;gBAAE,OAAO,GAAG,CAAC;YAC/E,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC;YACrD,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,IAAI,OAAO,CAAC;YACnG,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,kBAAkB,GAAG,MAAM,CAAC,CAAC;YACvD,MAAM,IAAI,EAAE,CAAC;YACb,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;YACzC,IAAI,MAAM,EAAE,OAAO;gBAAE,OAAO,GAAG,CAAC,CAAC,mEAAmE;QACtG,CAAC;IACH,CAAC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,OAAyB,EAAE,MAAc;IACxE,gGAAgG;IAChG,0FAA0F;IAC1F,qGAAqG;IACrG,+BAA+B;IAC/B,oGAAoG;IACpG,EAAE;IACF,kGAAkG;IAClG,oFAAoF;IACpF,2FAA2F;IAC3F,wFAAwF;IACxF,8FAA8F;IAC9F,kDAAkD;IAClD,OAAO,kBAAkB,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,CAAC,MAAM,EAAE,EAAE,OAAO,EAAE,oBAAoB,EAAE,EAAE,UAAU,EAAE,CAAC,EAAE,OAAO,EAAE,cAAc,EAAE,CAAC,EAAE,CAAC,CAAC;AAC/K,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,kBAAkB,CAAC,CAAqB;IACtD,IAAI,CAAC,CAAC;QAAE,OAAO,SAAS,CAAC;IACzB,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IACnB,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,SAAS,CAAC;IACzD,IAAI,CAAC;QACH,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC;QACrB,OAAO,CAAC,CAAC,QAAQ,KAAK,OAAO,IAAI,CAAC,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAC3E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,aAAa,CAAC,OAAyB;IACrD,MAAM,UAAU,GAAG,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,mBAAmB,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACjF,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;QAC3B,MAAM,CAAC,GAAG,kBAAkB,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QAC7C,IAAI,CAAC;YAAE,OAAO,CAAC,CAAC;IAClB,CAAC;IACD,OAAO,UAAU,CAAC,OAAO,CAAC,CAAC;AAC7B,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,sBAAsB,CAAC,OAAyB,EAAE,OAAe;IAC/E,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,wBAAwB,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACjE,6EAA6E;IAC7E,2EAA2E;IAC3E,kGAAkG;IAClG,kGAAkG;IAClG,iGAAiG;IACjG,mGAAmG;IACnG,0DAA0D;IAC1D,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,eAAe,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,CAAC;IACjG,oGAAoG;IACpG,gGAAgG;IAChG,+FAA+F;IAC/F,MAAM,GAAG,GAAG,kBAAkB,CAAC,GAAG,CAAC,CAAC;IACpC,OAAO,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,eAAe,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;AACvE,CAAC;AAED,6EAA6E;AAC7E,SAAS,eAAe,CAAC,GAAW,EAAE,OAAe;IACnD,OAAO,GAAG,KAAK,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC;AAC3C,CAAC;AAED,6FAA6F;AAC7F,MAAM,UAAU,iBAAiB,CAAC,OAAyB;IACzD,OAAO,kBAAkB,CAAC,OAAO,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAC,IAAI,aAAa,CAAC,OAAO,CAAC,CAAC;AAC7F,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,OAAyB;IACvD,MAAM,UAAU,GAAG,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,mBAAmB,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACjF,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;QAC3B,IAAI,kBAAkB,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YAAE,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC;IACjF,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,gBAAgB,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC;AACzD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,kBAAkB;IAChC,MAAM,GAAG,GAAG,IAAI,GAAG,EAAU,CAAC;IAC9B,kGAAkG;IAClG,iEAAiE;IACjE,gGAAgG;IAChG,+FAA+F;IAC/F,qEAAqE;IACrE,MAAM,GAAG,GAAG,CAAC,CAAqB,EAAE,EAAE;QACpC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO;QAC/B,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACX,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,kBAAkB,CAAC,CAAC,CAAC,CAAC;YACtC,IAAI,OAAO,KAAK,CAAC,IAAI,OAAO,CAAC,MAAM,IAAI,CAAC;gBAAE,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAC7D,CAAC;QAAC,MAAM,CAAC;YACP,uDAAuD;QACzD,CAAC;IACH,CAAC,CAAC;IAEF,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,qGAAqG;IACrG,kGAAkG;IAClG,iEAAiE;IACjE,KAAK,MAAM,CAAC,IAAI,CAAC,mBAAmB,EAAE,wBAAwB,EAAE,cAAc,EAAE,wBAAwB,CAAC,EAAE,CAAC;QAC1G,MAAM,CAAC,GAAG,kBAAkB,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QAC7C,IAAI,CAAC;YAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IACrB,CAAC;IACD,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;QACrB,GAAG,CAAC,CAAC,CAAC,CAAC;QACP,IAAI,CAAC;YACH,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC;YACrB,KAAK,MAAM,GAAG,IAAI,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC;gBAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,kCAAkC;YACrF,6FAA6F;YAC7F,2FAA2F;YAC3F,yFAAyF;YACzF,wEAAwE;YACxE,KAAK,MAAM,CAAC,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC,YAAY;gBAAE,GAAG,CAAC,GAAG,CAAC,CAAC;YAC/C,KAAK,MAAM,IAAI,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;gBAC1D,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;gBAC7B,IAAI,EAAE,IAAI,CAAC;oBAAE,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;YACvC,CAAC;YACD,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,iCAAiC;QAClE,CAAC;QAAC,MAAM,CAAC;YACP,uDAAuD;QACzD,CAAC;IACH,CAAC;IACD,OAAO,CAAC,GAAG,GAAG,CAAC,CAAC;AAClB,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,aAAa,CAAC,OAAyB;IACrD,IAAI,eAAe,CAAC,OAAO,CAAC,CAAC,UAAU;QAAE,OAAO,SAAS,CAAC;IAC1D,OAAO,CACL,gGAAgG;QAChG,gGAAgG;QAChG,kGAAkG;QAClG,gGAAgG;QAChG,oCAAoC,CACrC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The earn wiring — THE place to point the skill at a vault. Shipped code names no vault ticker
3
+ * as a literal; `scripts/roundtrip.ts` and the tiers read the default and the round-trip target
4
+ * from here (the fork and live tiers also name the gated sibling directly, for the access pair).
5
+ *
6
+ * Two knobs, deliberately separate, because they can legitimately differ:
7
+ *
8
+ * `defaultVault` what the tools use when the caller names none. The registry row must also be
9
+ * `isDefault` — `defaultVault()` refuses a config/registry disagreement — and the
10
+ * schema allows a WHITELIST_GATED default, because the pre-flight re-measures
11
+ * access live on every call and refuses a non-member before anything is built.
12
+ * `roundTripVault` what the deposit → redeem tiers exercise. The same vault today.
13
+ *
14
+ * Addresses live in the registry (`registry/vaults.json`, schema-validated, block-stamped) — a
15
+ * ticker here resolves through it.
16
+ *
17
+ * 🔴 NO DEPOSITOR ADDRESS IS CONFIGURED HERE, and that is a rule rather than an omission. A gated
18
+ * vault's whitelist is the fund's to hold, and its members are readable from the chain: the fork
19
+ * tier discovers one from the vault's own AccessManager (`tests/access.ts`) and impersonates it.
20
+ * Writing a member's address into this file would publish who the fund has onboarded, and would go
21
+ * stale the moment the whitelist changes. An address is never a key either way (CONTRIBUTING.md, the first rule).
22
+ */
23
+ /** A vault's ERC-20 ticker — resolved via `getVault`, so a typo fails at load, not at a call site. */
24
+ export type VaultSymbol = string;
25
+ export declare const EARN: {
26
+ /**
27
+ * Tempora Labs Cash Plus USDC (Test 2), Base — a Morpho Vault V2, the Tempora vault this client
28
+ * offers by default. Deposits are OPEN to any account (measured by a simulated stranger deposit;
29
+ * `earn_vaults` reports `defaultAccess: "open"`). The whitelist-gated sibling, Cash Plus USDC
30
+ * (Test 2A), stays listed as `tlCashPlusUSDC2A` and is refused per account by the pre-flight.
31
+ */
32
+ readonly defaultVault: "tlCashPlusUSDC2";
33
+ /** The round-trip target. The same vault as the default; open, so the fork tier deposits from the whale directly. */
34
+ readonly roundTripVault: "tlCashPlusUSDC2";
35
+ /** USDC, as a decimal string — the amount `scripts/roundtrip.ts` prepares by default. Never a float. */
36
+ readonly roundTripAmountUsdc: "0.05";
37
+ /** Fixtures the fork and live tiers share. */
38
+ readonly fixtures: {
39
+ /** An address that has never touched any vault — the negative control for access checks. */
40
+ readonly stranger: "0x000000000000000000000000000000000000dEaD";
41
+ /** Morpho Blue on Base — held ~2.1e14 USDC base units when probed 2026-09-11. Impersonated on the fork, never keyed. */
42
+ readonly usdcWhale: "0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb";
43
+ /** USDC the fork round trip deposits, and what it seeds a discovered depositor with beforehand. */
44
+ readonly forkDepositUsdc: "100";
45
+ readonly forkSeedUsdc: "1000";
46
+ };
47
+ };
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The earn wiring — THE place to point the skill at a vault. Shipped code names no vault ticker
3
+ * as a literal; `scripts/roundtrip.ts` and the tiers read the default and the round-trip target
4
+ * from here (the fork and live tiers also name the gated sibling directly, for the access pair).
5
+ *
6
+ * Two knobs, deliberately separate, because they can legitimately differ:
7
+ *
8
+ * `defaultVault` what the tools use when the caller names none. The registry row must also be
9
+ * `isDefault` — `defaultVault()` refuses a config/registry disagreement — and the
10
+ * schema allows a WHITELIST_GATED default, because the pre-flight re-measures
11
+ * access live on every call and refuses a non-member before anything is built.
12
+ * `roundTripVault` what the deposit → redeem tiers exercise. The same vault today.
13
+ *
14
+ * Addresses live in the registry (`registry/vaults.json`, schema-validated, block-stamped) — a
15
+ * ticker here resolves through it.
16
+ *
17
+ * 🔴 NO DEPOSITOR ADDRESS IS CONFIGURED HERE, and that is a rule rather than an omission. A gated
18
+ * vault's whitelist is the fund's to hold, and its members are readable from the chain: the fork
19
+ * tier discovers one from the vault's own AccessManager (`tests/access.ts`) and impersonates it.
20
+ * Writing a member's address into this file would publish who the fund has onboarded, and would go
21
+ * stale the moment the whitelist changes. An address is never a key either way (CONTRIBUTING.md, the first rule).
22
+ */
23
+ export const EARN = {
24
+ /**
25
+ * Tempora Labs Cash Plus USDC (Test 2), Base — a Morpho Vault V2, the Tempora vault this client
26
+ * offers by default. Deposits are OPEN to any account (measured by a simulated stranger deposit;
27
+ * `earn_vaults` reports `defaultAccess: "open"`). The whitelist-gated sibling, Cash Plus USDC
28
+ * (Test 2A), stays listed as `tlCashPlusUSDC2A` and is refused per account by the pre-flight.
29
+ */
30
+ defaultVault: "tlCashPlusUSDC2",
31
+ /** The round-trip target. The same vault as the default; open, so the fork tier deposits from the whale directly. */
32
+ roundTripVault: "tlCashPlusUSDC2",
33
+ /** USDC, as a decimal string — the amount `scripts/roundtrip.ts` prepares by default. Never a float. */
34
+ roundTripAmountUsdc: "0.05",
35
+ /** Fixtures the fork and live tiers share. */
36
+ fixtures: {
37
+ /** An address that has never touched any vault — the negative control for access checks. */
38
+ stranger: "0x000000000000000000000000000000000000dEaD",
39
+ /** Morpho Blue on Base — held ~2.1e14 USDC base units when probed 2026-09-11. Impersonated on the fork, never keyed. */
40
+ usdcWhale: "0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb",
41
+ /** USDC the fork round trip deposits, and what it seeds a discovered depositor with beforehand. */
42
+ forkDepositUsdc: "100",
43
+ forkSeedUsdc: "1000",
44
+ },
45
+ };
46
+ //# sourceMappingURL=earn.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"earn.js","sourceRoot":"","sources":["../../src/config/earn.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAKH,MAAM,CAAC,MAAM,IAAI,GAAG;IAClB;;;;;OAKG;IACH,YAAY,EAAE,iBAAuC;IAErD,qHAAqH;IACrH,cAAc,EAAE,iBAAuC;IAEvD,wGAAwG;IACxG,mBAAmB,EAAE,MAAM;IAE3B,8CAA8C;IAC9C,QAAQ,EAAE;QACR,4FAA4F;QAC5F,QAAQ,EAAE,4CAAqD;QAC/D,wHAAwH;QACxH,SAAS,EAAE,4CAAqD;QAChE,mGAAmG;QACnG,eAAe,EAAE,KAAK;QACtB,YAAY,EAAE,MAAM;KACrB;CACO,CAAC"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The disclosures a distribution surface presents before a depositor's first deposit.
3
+ * An agent skill IS a distribution surface, so `earn_terms` returns these verbatim and the
4
+ * skill instructs the agent to show them and get an explicit acknowledgement before building a
5
+ * first deposit. They are written for the vaults this registry actually offers — Tempora test
6
+ * vaults on ERC-4626 chassis on Base — and say what is true of those, not of a fund that has not
7
+ * launched. Change them when the offering changes, and nowhere else.
8
+ */
9
+ export declare const DISCLOSURES: {
10
+ readonly source: "Agent Treasury — pre-deposit disclosures, 2026-09-15";
11
+ readonly presentBefore: "the depositor's first deposit, on every distribution surface";
12
+ readonly items: readonly ["EVERY VAULT THIS CLIENT OFFERS TODAY IS A TEST VAULT — unproven, and named as such on-chain. They exist to exercise the product, not to hold savings. Deposit only an amount you are fully prepared to lose entirely, and do not move significant funds into one.", "This is a smart-contract vault, not a bank deposit. No deposit insurance of any kind applies.", "The share token's value is a function of the vault's underlying holdings and is not guaranteed. It can go down.", "The vault holds positions in third-party protocols, each of which carries smart-contract, custody, and mechanism risk that Tempora does not control.", "Any yield figure is a measurement over a past window, not a promise. Historical performance is not indicative of future results.", "A withdrawal is served from the vault's liquid balance and then by unwinding its positions. In stressed conditions part of a position may not be withdrawable immediately; earn_balance reports what is exitable now, simulated at the current block.", "Deposits into a whitelist-gated vault are accepted only from accounts the fund has admitted. Withdrawals are public: an account that holds shares can always leave.", "The vault's current holdings, weights and fees are on-chain and publicly readable at the vault's address.", "The default vault is a Tempora-curated destination. Tempora can set fees on it as curator (a management fee and a performance fee, both readable on-chain; check them before depositing). This client offers it because it is Tempora's, not because it is the best-yielding vault available."];
13
+ readonly clientNotes: readonly ["This client prepares unsigned calls only. It cannot sign, send, or move funds; the operator's own signer does that, and the operator is responsible for what it signs.", "Geography: the operator is responsible for the depositor's eligibility in its own jurisdiction. This client has no geographic signal and makes no representation about eligibility."];
14
+ };
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The disclosures a distribution surface presents before a depositor's first deposit.
3
+ * An agent skill IS a distribution surface, so `earn_terms` returns these verbatim and the
4
+ * skill instructs the agent to show them and get an explicit acknowledgement before building a
5
+ * first deposit. They are written for the vaults this registry actually offers — Tempora test
6
+ * vaults on ERC-4626 chassis on Base — and say what is true of those, not of a fund that has not
7
+ * launched. Change them when the offering changes, and nowhere else.
8
+ */
9
+ export const DISCLOSURES = {
10
+ source: "Agent Treasury — pre-deposit disclosures, 2026-09-15",
11
+ presentBefore: "the depositor's first deposit, on every distribution surface",
12
+ items: [
13
+ "EVERY VAULT THIS CLIENT OFFERS TODAY IS A TEST VAULT — unproven, and named as such on-chain. They exist to exercise the product, not to hold savings. Deposit only an amount you are fully prepared to lose entirely, and do not move significant funds into one.",
14
+ "This is a smart-contract vault, not a bank deposit. No deposit insurance of any kind applies.",
15
+ "The share token's value is a function of the vault's underlying holdings and is not guaranteed. It can go down.",
16
+ "The vault holds positions in third-party protocols, each of which carries smart-contract, custody, and mechanism risk that Tempora does not control.",
17
+ "Any yield figure is a measurement over a past window, not a promise. Historical performance is not indicative of future results.",
18
+ "A withdrawal is served from the vault's liquid balance and then by unwinding its positions. In stressed conditions part of a position may not be withdrawable immediately; earn_balance reports what is exitable now, simulated at the current block.",
19
+ "Deposits into a whitelist-gated vault are accepted only from accounts the fund has admitted. Withdrawals are public: an account that holds shares can always leave.",
20
+ "The vault's current holdings, weights and fees are on-chain and publicly readable at the vault's address.",
21
+ "The default vault is a Tempora-curated destination. Tempora can set fees on it as curator (a management fee and a performance fee, both readable on-chain; check them before depositing). This client offers it because it is Tempora's, not because it is the best-yielding vault available.",
22
+ ],
23
+ clientNotes: [
24
+ "This client prepares unsigned calls only. It cannot sign, send, or move funds; the operator's own signer does that, and the operator is responsible for what it signs.",
25
+ "Geography: the operator is responsible for the depositor's eligibility in its own jurisdiction. This client has no geographic signal and makes no representation about eligibility.",
26
+ ],
27
+ };
28
+ //# sourceMappingURL=disclosures.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"disclosures.js","sourceRoot":"","sources":["../src/disclosures.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,MAAM,EAAE,sDAAsD;IAC9D,aAAa,EAAE,8DAA8D;IAC7E,KAAK,EAAE;QACL,mQAAmQ;QACnQ,+FAA+F;QAC/F,iHAAiH;QACjH,sJAAsJ;QACtJ,kIAAkI;QAClI,uPAAuP;QACvP,qKAAqK;QACrK,2GAA2G;QAC3G,+RAA+R;KAChS;IACD,WAAW,EAAE;QACX,wKAAwK;QACxK,qLAAqL;KACtL;CACO,CAAC"}
@@ -0,0 +1,14 @@
1
+ export { loadRegistry, listVaults, getVault, depositableVaults, defaultVault, resolveVault } from "./registry.js";
2
+ export { makePublicClient, rpcUrlFromEnv, logsRpcUrlFromEnv, isSupportedChainId, chains } from "./client.js";
3
+ export type { ReadClient } from "./client.js";
4
+ export { preflightDeposit, classifyRevert, extractRevert } from "./preflight.js";
5
+ export type { PreflightResult, PreflightStatus, RevertObservation } from "./preflight.js";
6
+ export { getPosition } from "./position.js";
7
+ export type { Position } from "./position.js";
8
+ export { quoteDeposit, quoteWithdraw } from "./quote.js";
9
+ export type { DepositQuote, WithdrawQuote } from "./quote.js";
10
+ export { buildDeposit, buildWithdraw } from "./build.js";
11
+ export type { UnsignedCall } from "./build.js";
12
+ export { parseAmount, formatAmount } from "./units.js";
13
+ export { DISCLOSURES } from "./disclosures.js";
14
+ export { UNKNOWN_AFTER_SCAN_FAILURE } from "./position.js";
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ export { loadRegistry, listVaults, getVault, depositableVaults, defaultVault, resolveVault } from "./registry.js";
2
+ export { makePublicClient, rpcUrlFromEnv, logsRpcUrlFromEnv, isSupportedChainId, chains } from "./client.js";
3
+ export { preflightDeposit, classifyRevert, extractRevert } from "./preflight.js";
4
+ export { getPosition } from "./position.js";
5
+ export { quoteDeposit, quoteWithdraw } from "./quote.js";
6
+ export { buildDeposit, buildWithdraw } from "./build.js";
7
+ export { parseAmount, formatAmount } from "./units.js";
8
+ export { DISCLOSURES } from "./disclosures.js";
9
+ export { UNKNOWN_AFTER_SCAN_FAILURE } from "./position.js";
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,QAAQ,EAAE,iBAAiB,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAClH,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE7G,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEjF,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE5C,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEzD,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEzD,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,0BAA0B,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Public, human-checkable links for a vault — the verification path that costs the operator
3
+ * nothing to follow and this client nothing to serve.
4
+ *
5
+ * These are derived from `chainId`, `chassis` and `address`, never stored in the registry: a stored
6
+ * URL is another field that can drift from the address it claims to describe, and the whole point
7
+ * of a verification link is that it resolves to the same contract the tools are about to build a
8
+ * call for. Derive it and the two cannot disagree.
9
+ *
10
+ * No RPC is involved. An operator who has not configured an endpoint can still open every one of
11
+ * these, which is why they are the answer to "how do I check this is real" rather than a live
12
+ * `symbol()` call the client would have to make on their behalf.
13
+ */
14
+ import type { VaultEntry } from "./registry-schema.js";
15
+ export interface VaultLinks {
16
+ /** The contract on a block explorer: source, holdings, every transaction. Always present. */
17
+ explorer: string;
18
+ /** The protocol's own UI for this vault, when the chassis has a known one. */
19
+ app?: string;
20
+ }
21
+ export declare function linksFor(vault: Pick<VaultEntry, "chainId" | "address" | "chassis">): VaultLinks;
package/dist/links.js ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Block explorers, by chain.
3
+ *
4
+ * 🔴 THIS MAP MAY NOT BE MISSED A CHAIN. An unknown `chainId` interpolates `undefined` into the
5
+ * URL — `undefined0x040f…` — which is a well-formed string, so nothing throws and the caller
6
+ * receives a link that goes nowhere. Today that is unreachable, but only because the registry
7
+ * schema pins `chainId: z.literal(8453)`: a wrong value is a compile-time error wherever a
8
+ * `VaultEntry` is built, and `loadRegistry()` rejects the whole file on any parse failure.
9
+ *
10
+ * So the safety here is the LITERAL, not this map. **Widening the schema to a second chain
11
+ * without adding its row below reintroduces the broken link**, silently. Add the row in the same
12
+ * change, or make the lookup fail loudly instead.
13
+ */
14
+ const EXPLORER = {
15
+ 8453: "https://basescan.org/address/",
16
+ };
17
+ /**
18
+ * The protocol's own front end, where one exists and the URL shape is known. A chassis with no
19
+ * entry gets no link rather than a guessed one — a wrong app URL sends an operator to someone
20
+ * else's vault, which is worse than sending them nowhere.
21
+ */
22
+ const APP = {
23
+ "morpho-v2": (chainId, address) => (chainId === 8453 ? `https://app.morpho.org/base/vault/${address}` : undefined),
24
+ };
25
+ export function linksFor(vault) {
26
+ const links = { explorer: `${EXPLORER[vault.chainId]}${vault.address}` };
27
+ const app = APP[vault.chassis]?.(vault.chainId, vault.address);
28
+ if (app)
29
+ links.app = app;
30
+ return links;
31
+ }
32
+ //# sourceMappingURL=links.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"links.js","sourceRoot":"","sources":["../src/links.ts"],"names":[],"mappings":"AAeA;;;;;;;;;;;;GAYG;AACH,MAAM,QAAQ,GAA2B;IACvC,IAAI,EAAE,+BAA+B;CACtC,CAAC;AAEF;;;;GAIG;AACH,MAAM,GAAG,GAA6E;IACpF,WAAW,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,qCAAqC,OAAO,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;CACnH,CAAC;AASF,MAAM,UAAU,QAAQ,CAAC,KAA0D;IACjF,MAAM,KAAK,GAAe,EAAE,QAAQ,EAAE,GAAG,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC;IACrF,MAAM,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;IAC/D,IAAI,GAAG;QAAE,KAAK,CAAC,GAAG,GAAG,GAAG,CAAC;IACzB,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Agent Treasury — MCP server (earn skill).
3
+ *
4
+ * Read tools, quotes, and UNSIGNED call builders only. There is no `sign` and no `send` tool, and
5
+ * there will not be one here: the skill supplies intent, the consumer supplies the signer. Anything
6
+ * that holds a key lives outside this process and receives the calls this server emits.
7
+ *
8
+ * Eight tools, all `earn_`-prefixed so later skills can sit beside them
9
+ * in one namespace:
10
+ *
11
+ * earn_vaults earn_terms earn_quote (direction) earn_status (no args = health)
12
+ * earn_balance earn_claim earn_prepare_deposit earn_prepare_withdraw
13
+ *
14
+ * Two contracts worth stating here because they are easy to erode:
15
+ *
16
+ * 1. `earn_prepare_*` return an ENVELOPE, `{ requires_signature, status: "unsigned", calls }`,
17
+ * not a bare array. This cannot stop a model reporting "deposited" — nothing at this boundary
18
+ * can — but it makes an unsigned build structurally distinguishable from a completed one, so a
19
+ * consumer or a test can assert on it. Wrapped HERE only: `buildDeposit`/`buildWithdraw` still
20
+ * return arrays, so a direct library consumer (see docs/runbooks/sign_and_send.md) gets the
21
+ * naked calls exactly as before.
22
+ * 2. The address argument is `account` everywhere. It was `depositor`, `owner` and `principal` in
23
+ * three different tools. `receiver` stays distinct because it genuinely is: `account` owns the
24
+ * shares, `receiver` is where the money lands.
25
+ */
26
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
27
+ /**
28
+ * What a signer must do for these calls to land exactly once. Each rule is a failure measured on a
29
+ * real Base round trip, not a precaution (2026-09-13, two runs through these tools).
30
+ * The server cannot enforce any of them — it never signs — so it states them with every build.
31
+ */
32
+ export declare const SIGNER_RULES: readonly ["Before signing, check every call's destination against the addresses the operator gave: `to`, and the receiver/owner named in `description`. Do not sign a call whose destination you did not confirm.", "Set the nonce explicitly from eth_getTransactionCount(account, \"pending\") immediately before each send. A load-balanced RPC can hand a signing library a stale nonce, and the send is then rejected as \"nonce too low\" (measured 2026-09-14, mainnet.base.org).", "Set the gas limit to the call's gasAdvice (estimate × 1.5).", "Wait for each receipt before sending the next call, and poll for it on a provider that serves receipts: a public endpoint may refuse eth_getTransactionReceipt (measured 2026-09-14, publicnode). A failed receipt poll does not mean the transaction failed.", "If a send or receipt poll fails for any reason other than an on-chain revert, read the account's nonce on a DIFFERENT provider before re-sending. A nonce that moved means the transaction was accepted; re-sending it would repeat the deposit or withdrawal."];
33
+ export declare function buildServer(): McpServer;