@leashmarket/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/README.md +157 -0
- package/dist/cli.d.ts +44 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +314 -0
- package/dist/cli.js.map +1 -0
- package/dist/config-write.d.ts +38 -0
- package/dist/config-write.d.ts.map +1 -0
- package/dist/config-write.js +80 -0
- package/dist/config-write.js.map +1 -0
- package/dist/config.d.ts +114 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +164 -0
- package/dist/config.js.map +1 -0
- package/dist/host-stdio.d.ts +27 -0
- package/dist/host-stdio.d.ts.map +1 -0
- package/dist/host-stdio.js +1001 -0
- package/dist/host-stdio.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/mint-local.d.ts +128 -0
- package/dist/mint-local.d.ts.map +1 -0
- package/dist/mint-local.js +267 -0
- package/dist/mint-local.js.map +1 -0
- package/dist/sandbox-api.d.ts +56 -0
- package/dist/sandbox-api.d.ts.map +1 -0
- package/dist/sandbox-api.js +38 -0
- package/dist/sandbox-api.js.map +1 -0
- package/dist/server.d.ts +131 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +502 -0
- package/dist/server.js.map +1 -0
- package/dist/signer.d.ts +54 -0
- package/dist/signer.d.ts.map +1 -0
- package/dist/signer.js +102 -0
- package/dist/signer.js.map +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1,1001 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `LeashHost` implementation for the standalone STDIO MCP / CLI.
|
|
3
|
+
*
|
|
4
|
+
* Differs from the chat product in two important ways:
|
|
5
|
+
*
|
|
6
|
+
* 1. **Settlement happens in-process.** `pay` and `withdraw` actually
|
|
7
|
+
* sign + submit on Solana using the local executive keypair —
|
|
8
|
+
* no UI in the loop. The result blob carries a real `tx_signature`,
|
|
9
|
+
* not a "review-the-card" artifact. This is the killer demo path.
|
|
10
|
+
*
|
|
11
|
+
* 2. **No platform DB / Privy session.** Off-chain calls to the
|
|
12
|
+
* Leash API authenticate via a legacy `LEASH_API_KEY` bearer
|
|
13
|
+
* token until the X-Leash-Sig auth path lands in batch 4.
|
|
14
|
+
*
|
|
15
|
+
* The four host methods all `try/catch` aggressively and return a
|
|
16
|
+
* structured `{ status: 'ok' | 'error' }` blob — never throw — so
|
|
17
|
+
* the LLM never sees a tool exception, only a recoverable JSON
|
|
18
|
+
* response with a `message` it can surface.
|
|
19
|
+
*/
|
|
20
|
+
import { LEASH_EXPLORER_DEFAULT, TOKEN_2022_PROGRAM_ADDRESS, TOKEN_2022_PROGRAM_ID, deriveAgentTreasuryAddress, deriveAgentTreasuryAta, leashReceiptUrl, listSplBalances, parseLeashHeaders, tokenProgramForMint, } from '@leashmarket/core';
|
|
21
|
+
import { fetchDiscover, fetchPaySkillsProvider, fetchReputation, isLikelyBase58Address, jsonResult, lookupTokenBySymbolSafe, noAgentResult, probePaymentLink, } from '@leashmarket/mcp-core';
|
|
22
|
+
import { SPL_TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID as UMI_TOKEN_2022_PROGRAM_ID, getSpendDelegation, revokeSpendDelegation, setSpendDelegation, withdrawTreasury, withdrawTreasurySol, } from '@leashmarket/registry-utils';
|
|
23
|
+
import { createBuyer } from '@leashmarket/buyer-kit';
|
|
24
|
+
import { loadSigner } from './signer.js';
|
|
25
|
+
const LAMPORTS_PER_SOL = 1000000000n;
|
|
26
|
+
/**
|
|
27
|
+
* Default spend rules baked into every standalone-MCP buyer-kit
|
|
28
|
+
* call. Conservative — the user can raise these later by setting
|
|
29
|
+
* env vars (LEASH_PER_CALL_USDC, LEASH_PER_DAY_USDC) or editing
|
|
30
|
+
* `~/.config/leash/agent.json`. The on-chain SPL `Approve`
|
|
31
|
+
* delegation is the real ceiling; these values are belt + braces.
|
|
32
|
+
*/
|
|
33
|
+
function defaultRules() {
|
|
34
|
+
const perCall = process.env.LEASH_PER_CALL_USDC?.trim() || '1';
|
|
35
|
+
const perDay = process.env.LEASH_PER_DAY_USDC?.trim() || '10';
|
|
36
|
+
return {
|
|
37
|
+
v: '0.1',
|
|
38
|
+
budget: { perCall, daily: perDay, currency: 'USDC' },
|
|
39
|
+
hosts: {},
|
|
40
|
+
triggers: [],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function tokenNetwork(network) {
|
|
44
|
+
return network === 'solana-mainnet' ? 'mainnet' : 'devnet';
|
|
45
|
+
}
|
|
46
|
+
/** Network slug -> buyer-kit + receipt cluster slug. */
|
|
47
|
+
function buyerKitNetwork(network) {
|
|
48
|
+
return network;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Build the standalone `LeashHost`. Captures the `LeashAgentConfig`
|
|
52
|
+
* + `LeashSigner` once so per-call methods can stay tight.
|
|
53
|
+
*/
|
|
54
|
+
export function createStdioHost(config) {
|
|
55
|
+
const signer = loadSigner(config.executiveSecretBase58);
|
|
56
|
+
return new StdioHost(config, signer);
|
|
57
|
+
}
|
|
58
|
+
class StdioHost {
|
|
59
|
+
agentMint;
|
|
60
|
+
ownerWallet;
|
|
61
|
+
network;
|
|
62
|
+
rpcUrl;
|
|
63
|
+
apiBaseUrl;
|
|
64
|
+
explorerBaseUrl;
|
|
65
|
+
config;
|
|
66
|
+
signer;
|
|
67
|
+
constructor(config, signer) {
|
|
68
|
+
this.config = config;
|
|
69
|
+
this.signer = signer;
|
|
70
|
+
this.agentMint = config.agentMint;
|
|
71
|
+
this.ownerWallet = signer.pubkey;
|
|
72
|
+
this.network = config.network;
|
|
73
|
+
this.rpcUrl = config.rpcUrl;
|
|
74
|
+
this.apiBaseUrl = config.apiBaseUrl;
|
|
75
|
+
this.explorerBaseUrl = config.explorerBaseUrl ?? LEASH_EXPLORER_DEFAULT;
|
|
76
|
+
}
|
|
77
|
+
async checkTreasuryBalance(args) {
|
|
78
|
+
if (!this.agentMint)
|
|
79
|
+
return noAgentResult('treasury_balance');
|
|
80
|
+
try {
|
|
81
|
+
const treasury = await deriveAgentTreasuryAddress(this.agentMint);
|
|
82
|
+
const result = await listSplBalances({
|
|
83
|
+
owner: String(treasury),
|
|
84
|
+
rpcUrl: this.rpcUrl,
|
|
85
|
+
network: tokenNetwork(this.network),
|
|
86
|
+
pinKnownStables: true,
|
|
87
|
+
});
|
|
88
|
+
const filtered = args.symbol
|
|
89
|
+
? result.tokens.filter((t) => (t.symbol ?? '').toLowerCase() === args.symbol.toLowerCase())
|
|
90
|
+
: result.tokens;
|
|
91
|
+
return jsonResult({
|
|
92
|
+
kind: 'treasury_balance',
|
|
93
|
+
status: 'ok',
|
|
94
|
+
treasury: String(treasury),
|
|
95
|
+
network: this.network,
|
|
96
|
+
sol: result.sol,
|
|
97
|
+
tokens: filtered.map((t) => ({
|
|
98
|
+
symbol: t.symbol,
|
|
99
|
+
name: t.name,
|
|
100
|
+
ui: t.ui,
|
|
101
|
+
amount: t.amount,
|
|
102
|
+
decimals: t.decimals,
|
|
103
|
+
mint: t.mint,
|
|
104
|
+
program: t.program,
|
|
105
|
+
})),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
catch (e) {
|
|
109
|
+
return jsonResult({
|
|
110
|
+
kind: 'treasury_balance',
|
|
111
|
+
status: 'error',
|
|
112
|
+
message: e instanceof Error ? e.message : 'unknown',
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async pay(args) {
|
|
117
|
+
if (!this.agentMint)
|
|
118
|
+
return noAgentResult('payment_receipt');
|
|
119
|
+
try {
|
|
120
|
+
// Probe the seller's paywall first so we can pick the right
|
|
121
|
+
// SPL mint for the buyer-kit's `sourceTokenAccount` (the
|
|
122
|
+
// treasury's ATA for the demanded asset). The probe is cheap
|
|
123
|
+
// (one HTTP GET) and lets us surface a clean error if the URL
|
|
124
|
+
// isn't actually an x402 link.
|
|
125
|
+
const preview = await probePaymentLink(args.url);
|
|
126
|
+
const tokenProgramKind = tokenProgramForMint(preview.asset);
|
|
127
|
+
const sourceAta = await deriveAgentTreasuryAta({
|
|
128
|
+
asset: this.agentMint,
|
|
129
|
+
mint: preview.asset,
|
|
130
|
+
...(tokenProgramKind === 'spl-token-2022'
|
|
131
|
+
? { tokenProgram: TOKEN_2022_PROGRAM_ADDRESS }
|
|
132
|
+
: {}),
|
|
133
|
+
});
|
|
134
|
+
const kitSigner = await this.signer.getKitSigner();
|
|
135
|
+
const buyer = createBuyer({
|
|
136
|
+
agent: this.agentMint,
|
|
137
|
+
signer: kitSigner,
|
|
138
|
+
networks: [buyerKitNetwork(this.network)],
|
|
139
|
+
rpcUrl: this.rpcUrl,
|
|
140
|
+
sourceTokenAccount: String(sourceAta.ata),
|
|
141
|
+
rules: defaultRules(),
|
|
142
|
+
});
|
|
143
|
+
const { response, receipt, failureReason } = await buyer.fetch(args.url, {
|
|
144
|
+
method: 'GET',
|
|
145
|
+
});
|
|
146
|
+
const bodyText = await response
|
|
147
|
+
.clone()
|
|
148
|
+
.text()
|
|
149
|
+
.catch(() => '');
|
|
150
|
+
if (receipt.tx_sig && response.ok) {
|
|
151
|
+
// Prefer the seller-stamped `X-Leash-*` headers over the
|
|
152
|
+
// buyer-kit's locally-computed receipt. The buyer-side hash is
|
|
153
|
+
// computed against the buyer's view of the request (its own
|
|
154
|
+
// `nonce` / `ts`) so it diverges from the canonical seller-side
|
|
155
|
+
// earn receipt that `apps/api`'s paywall publishes — and the
|
|
156
|
+
// explorer only indexes the seller-side hash. Falling back to
|
|
157
|
+
// the local hash keeps legacy paywalls (no header stamping)
|
|
158
|
+
// working. Same precedence as the chat product applies in
|
|
159
|
+
// `apps/agents/components/chat/pay-request-artifact.tsx`.
|
|
160
|
+
const stamped = parseLeashHeaders(response);
|
|
161
|
+
const txSignature = stamped.txSig ?? receipt.tx_sig;
|
|
162
|
+
const receiptHash = stamped.receiptHash ?? receipt.receipt_hash ?? null;
|
|
163
|
+
return jsonResult({
|
|
164
|
+
kind: 'payment_receipt',
|
|
165
|
+
status: 'ok',
|
|
166
|
+
url: args.url,
|
|
167
|
+
agent_mint: this.agentMint,
|
|
168
|
+
network: this.network,
|
|
169
|
+
paid_amount_atomic: receipt.price?.amount ?? null,
|
|
170
|
+
currency: receipt.price?.currency ?? null,
|
|
171
|
+
tx_signature: txSignature,
|
|
172
|
+
response_status: response.status,
|
|
173
|
+
response_body: bodyText.slice(0, 4000),
|
|
174
|
+
receipt_hash: receiptHash,
|
|
175
|
+
receipt_url: leashReceiptUrl(receiptHash, { baseUrl: this.explorerBaseUrl }),
|
|
176
|
+
explorer_url: explorerTxUrl(txSignature, this.network),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return jsonResult({
|
|
180
|
+
kind: 'payment_receipt',
|
|
181
|
+
status: 'error',
|
|
182
|
+
url: args.url,
|
|
183
|
+
agent_mint: this.agentMint,
|
|
184
|
+
message: failureReason ?? `seller returned HTTP ${response.status}`,
|
|
185
|
+
response_status: response.status,
|
|
186
|
+
response_body: bodyText.slice(0, 1000),
|
|
187
|
+
quoted_amount_atomic: receipt.price?.amount ?? null,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
return jsonResult({
|
|
192
|
+
kind: 'payment_receipt',
|
|
193
|
+
status: 'error',
|
|
194
|
+
url: args.url,
|
|
195
|
+
message: err instanceof Error ? err.message : 'unknown error',
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
async withdraw(args) {
|
|
200
|
+
if (!this.agentMint)
|
|
201
|
+
return noAgentResult('withdraw_receipt');
|
|
202
|
+
if (!isLikelyBase58Address(args.destination)) {
|
|
203
|
+
return jsonResult({
|
|
204
|
+
kind: 'withdraw_receipt',
|
|
205
|
+
status: 'error',
|
|
206
|
+
message: 'Destination does not look like a Solana wallet address (base58, 32–44 chars).',
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
const umi = this.signer.getUmi(this.rpcUrl);
|
|
211
|
+
if (args.token === 'SOL') {
|
|
212
|
+
const lamports = BigInt(Math.floor(args.amount * Number(LAMPORTS_PER_SOL)));
|
|
213
|
+
if (lamports <= 0n) {
|
|
214
|
+
return jsonResult({
|
|
215
|
+
kind: 'withdraw_receipt',
|
|
216
|
+
status: 'error',
|
|
217
|
+
message: 'Amount rounds to zero lamports — request a larger amount.',
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
const result = await withdrawTreasurySol(umi, {
|
|
221
|
+
agentAsset: this.agentMint,
|
|
222
|
+
destination: args.destination,
|
|
223
|
+
lamports,
|
|
224
|
+
});
|
|
225
|
+
return jsonResult({
|
|
226
|
+
kind: 'withdraw_receipt',
|
|
227
|
+
status: 'ok',
|
|
228
|
+
agent_mint: this.agentMint,
|
|
229
|
+
token: 'SOL',
|
|
230
|
+
decimals: 9,
|
|
231
|
+
amount: String(args.amount),
|
|
232
|
+
amount_atomic: lamports.toString(),
|
|
233
|
+
destination: args.destination,
|
|
234
|
+
treasury: result.treasury,
|
|
235
|
+
tx_signature: result.signature,
|
|
236
|
+
network: this.network,
|
|
237
|
+
explorer_url: explorerTxUrl(result.signature, this.network),
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
const meta = lookupTokenBySymbolSafe(args.token, tokenNetwork(this.network));
|
|
241
|
+
if (!meta) {
|
|
242
|
+
return jsonResult({
|
|
243
|
+
kind: 'withdraw_receipt',
|
|
244
|
+
status: 'error',
|
|
245
|
+
message: `Token ${args.token} is not catalogued on ${this.network}. Try USDC, USDG, or USDT.`,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
const atomic = BigInt(Math.floor(args.amount * 10 ** meta.decimals));
|
|
249
|
+
if (atomic <= 0n) {
|
|
250
|
+
return jsonResult({
|
|
251
|
+
kind: 'withdraw_receipt',
|
|
252
|
+
status: 'error',
|
|
253
|
+
message: 'Amount rounds to zero atomic units — request a larger amount.',
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
const result = await withdrawTreasury(umi, {
|
|
257
|
+
agentAsset: this.agentMint,
|
|
258
|
+
mint: meta.mint,
|
|
259
|
+
destination: args.destination,
|
|
260
|
+
amount: atomic,
|
|
261
|
+
decimals: meta.decimals,
|
|
262
|
+
...(meta.program === 'spl-token-2022' ? { tokenProgram: UMI_TOKEN_2022_PROGRAM_ID } : {}),
|
|
263
|
+
});
|
|
264
|
+
return jsonResult({
|
|
265
|
+
kind: 'withdraw_receipt',
|
|
266
|
+
status: 'ok',
|
|
267
|
+
agent_mint: this.agentMint,
|
|
268
|
+
token: meta.symbol,
|
|
269
|
+
mint: meta.mint,
|
|
270
|
+
token_program: meta.program === 'spl-token-2022' ? TOKEN_2022_PROGRAM_ID : null,
|
|
271
|
+
decimals: meta.decimals,
|
|
272
|
+
amount: String(args.amount),
|
|
273
|
+
amount_atomic: atomic.toString(),
|
|
274
|
+
destination: args.destination,
|
|
275
|
+
treasury: result.treasury,
|
|
276
|
+
tx_signature: result.signature,
|
|
277
|
+
network: this.network,
|
|
278
|
+
explorer_url: explorerTxUrl(result.signature, this.network),
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
catch (err) {
|
|
282
|
+
return jsonResult({
|
|
283
|
+
kind: 'withdraw_receipt',
|
|
284
|
+
status: 'error',
|
|
285
|
+
message: err instanceof Error ? err.message : 'unknown error',
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
async createPaymentLink(args) {
|
|
290
|
+
if (!this.agentMint)
|
|
291
|
+
return noAgentResult('payment_link');
|
|
292
|
+
// Until X-Leash-Sig auth ships in batch 4, the standalone MCP
|
|
293
|
+
// requires a legacy API key. If the user hasn't set one, return a
|
|
294
|
+
// clean error the LLM can surface verbatim.
|
|
295
|
+
if (!this.config.apiKey) {
|
|
296
|
+
return jsonResult({
|
|
297
|
+
kind: 'payment_link',
|
|
298
|
+
status: 'error',
|
|
299
|
+
message: 'Creating payment links from the standalone MCP currently requires LEASH_API_KEY in the environment. (X-Leash-Sig auth ships in the next release.)',
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
try {
|
|
303
|
+
const body = {
|
|
304
|
+
label: args.label,
|
|
305
|
+
description: args.description,
|
|
306
|
+
owner_agent: this.agentMint,
|
|
307
|
+
method: 'GET',
|
|
308
|
+
price: `${args.amount} ${args.currency}`,
|
|
309
|
+
currency: args.currency,
|
|
310
|
+
response: {
|
|
311
|
+
status: 200,
|
|
312
|
+
mimeType: 'application/json',
|
|
313
|
+
body: { ok: true, label: args.label },
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
const res = await fetch(`${this.apiBaseUrl}/v1/payment-links`, {
|
|
317
|
+
method: 'POST',
|
|
318
|
+
headers: {
|
|
319
|
+
authorization: `Bearer ${this.config.apiKey}`,
|
|
320
|
+
'content-type': 'application/json',
|
|
321
|
+
},
|
|
322
|
+
body: JSON.stringify(body),
|
|
323
|
+
});
|
|
324
|
+
const text = await res.text();
|
|
325
|
+
if (!res.ok) {
|
|
326
|
+
return jsonResult({
|
|
327
|
+
kind: 'payment_link',
|
|
328
|
+
status: 'error',
|
|
329
|
+
message: `Leash API ${res.status}: ${text.slice(0, 300)}`,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
const json = JSON.parse(text);
|
|
333
|
+
return jsonResult({
|
|
334
|
+
kind: 'payment_link',
|
|
335
|
+
status: 'ok',
|
|
336
|
+
id: json.id,
|
|
337
|
+
url: json.share_url,
|
|
338
|
+
price: `${args.amount} ${args.currency}`,
|
|
339
|
+
currency: args.currency,
|
|
340
|
+
label: args.label,
|
|
341
|
+
network: json.network,
|
|
342
|
+
owner_agent: json.owner_agent,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
catch (err) {
|
|
346
|
+
return jsonResult({
|
|
347
|
+
kind: 'payment_link',
|
|
348
|
+
status: 'error',
|
|
349
|
+
message: err instanceof Error ? err.message : 'unknown error',
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
async registerAgent(_args) {
|
|
354
|
+
return jsonResult({
|
|
355
|
+
kind: 'register_agent',
|
|
356
|
+
status: 'already_registered',
|
|
357
|
+
agent_mint: this.agentMint,
|
|
358
|
+
executive_pubkey: this.ownerWallet,
|
|
359
|
+
network: this.network,
|
|
360
|
+
message: `Agent ${this.agentMint} is already registered on this host. Use \`leash_get_identity\` to inspect it, or rotate to a fresh agent by deleting \`~/.config/leash/agent.json\` and re-running.`,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
async getIdentity(_args) {
|
|
364
|
+
if (!this.agentMint)
|
|
365
|
+
return noAgentResult('identity');
|
|
366
|
+
try {
|
|
367
|
+
const treasury = await deriveAgentTreasuryAddress(this.agentMint);
|
|
368
|
+
return jsonResult({
|
|
369
|
+
kind: 'identity',
|
|
370
|
+
status: 'ok',
|
|
371
|
+
agent_mint: this.agentMint,
|
|
372
|
+
treasury_address: String(treasury),
|
|
373
|
+
executive_pubkey: this.ownerWallet,
|
|
374
|
+
network: this.network,
|
|
375
|
+
api_base_url: this.apiBaseUrl,
|
|
376
|
+
rpc_url: this.rpcUrl,
|
|
377
|
+
explorer_url: explorerAccountUrl(this.agentMint, this.network),
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
catch (err) {
|
|
381
|
+
return jsonResult({
|
|
382
|
+
kind: 'identity',
|
|
383
|
+
status: 'error',
|
|
384
|
+
message: err instanceof Error ? err.message : 'unknown error',
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
async receipts(args) {
|
|
389
|
+
if (!this.agentMint)
|
|
390
|
+
return noAgentResult('receipts');
|
|
391
|
+
if (!this.config.apiKey) {
|
|
392
|
+
return jsonResult({
|
|
393
|
+
kind: 'receipts',
|
|
394
|
+
status: 'error',
|
|
395
|
+
message: 'Listing receipts from the standalone MCP currently requires LEASH_API_KEY in the environment. (X-Leash-Sig auth ships in the next release alongside discovery + reputation.)',
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
try {
|
|
399
|
+
const url = new URL(`${this.apiBaseUrl}/v1/receipts/${this.agentMint}`);
|
|
400
|
+
if (args.limit)
|
|
401
|
+
url.searchParams.set('limit', String(args.limit));
|
|
402
|
+
if (args.direction === 'outgoing')
|
|
403
|
+
url.searchParams.set('kind', 'spend');
|
|
404
|
+
else if (args.direction === 'incoming')
|
|
405
|
+
url.searchParams.set('kind', 'earn');
|
|
406
|
+
const res = await fetch(url, {
|
|
407
|
+
headers: { authorization: `Bearer ${this.config.apiKey}` },
|
|
408
|
+
});
|
|
409
|
+
const text = await res.text();
|
|
410
|
+
if (!res.ok) {
|
|
411
|
+
return jsonResult({
|
|
412
|
+
kind: 'receipts',
|
|
413
|
+
status: 'error',
|
|
414
|
+
message: `Leash API ${res.status}: ${text.slice(0, 300)}`,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
const json = JSON.parse(text);
|
|
418
|
+
return jsonResult({
|
|
419
|
+
kind: 'receipts',
|
|
420
|
+
status: 'ok',
|
|
421
|
+
agent_mint: this.agentMint,
|
|
422
|
+
network: this.network,
|
|
423
|
+
count: json.items.length,
|
|
424
|
+
next_cursor: json.next_cursor,
|
|
425
|
+
items: json.items.map((r) => ({
|
|
426
|
+
receipt_hash: r.receipt_hash,
|
|
427
|
+
direction: r.kind === 'spend' ? 'outgoing' : 'incoming',
|
|
428
|
+
decision: r.decision,
|
|
429
|
+
tx_signature: r.tx_sig,
|
|
430
|
+
url: r.raw?.request?.url ?? null,
|
|
431
|
+
amount: r.raw?.price?.amount ?? null,
|
|
432
|
+
currency: r.raw?.price?.currency ?? null,
|
|
433
|
+
timestamp: r.ingested_at,
|
|
434
|
+
explorer_url: r.tx_sig ? explorerTxUrl(r.tx_sig, this.network) : null,
|
|
435
|
+
})),
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
catch (err) {
|
|
439
|
+
return jsonResult({
|
|
440
|
+
kind: 'receipts',
|
|
441
|
+
status: 'error',
|
|
442
|
+
message: err instanceof Error ? err.message : 'unknown error',
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
async discover(args) {
|
|
447
|
+
return fetchDiscover({
|
|
448
|
+
apiBaseUrl: this.apiBaseUrl,
|
|
449
|
+
network: this.network,
|
|
450
|
+
query: args,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
async reputation(args) {
|
|
454
|
+
return fetchReputation({
|
|
455
|
+
apiBaseUrl: this.apiBaseUrl,
|
|
456
|
+
network: this.network,
|
|
457
|
+
query: args,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
async paySkillsProvider(args) {
|
|
461
|
+
return fetchPaySkillsProvider({
|
|
462
|
+
apiBaseUrl: this.apiBaseUrl,
|
|
463
|
+
network: this.network,
|
|
464
|
+
query: args,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Owner-driven update of the SPL `Approve` delegation that lets the
|
|
469
|
+
* executive spend the requested stable from the agent treasury PDA.
|
|
470
|
+
* Mode + amount semantics:
|
|
471
|
+
* - `unlimited` (default) → `u64::MAX` (the protocol default).
|
|
472
|
+
* - `revoke` → drop the delegation entirely.
|
|
473
|
+
* - `amount` + `amount: N` → cap at `N * 10**decimals`.
|
|
474
|
+
*/
|
|
475
|
+
async setSpendLimit(args) {
|
|
476
|
+
if (!this.agentMint)
|
|
477
|
+
return noAgentResult('spend_limit');
|
|
478
|
+
const symbol = (args.symbol ?? 'USDC');
|
|
479
|
+
const meta = lookupTokenBySymbolSafe(symbol, tokenNetwork(this.network));
|
|
480
|
+
if (!meta) {
|
|
481
|
+
return jsonResult({
|
|
482
|
+
kind: 'spend_limit',
|
|
483
|
+
status: 'error',
|
|
484
|
+
message: `${symbol} is not configured for ${this.network}.`,
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
const mode = args.mode ?? 'unlimited';
|
|
488
|
+
if (mode === 'amount' && (args.amount === undefined || !(args.amount > 0))) {
|
|
489
|
+
return jsonResult({
|
|
490
|
+
kind: 'spend_limit',
|
|
491
|
+
status: 'error',
|
|
492
|
+
message: '`mode: "amount"` requires `amount` (a positive decimal number).',
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
try {
|
|
496
|
+
const umi = this.signer.getUmi(this.rpcUrl);
|
|
497
|
+
const tokenProgram = meta.program === 'spl-token-2022' ? UMI_TOKEN_2022_PROGRAM_ID : SPL_TOKEN_PROGRAM_ID;
|
|
498
|
+
if (mode === 'revoke') {
|
|
499
|
+
const result = await revokeSpendDelegation(umi, {
|
|
500
|
+
agentAsset: this.agentMint,
|
|
501
|
+
mint: meta.mint,
|
|
502
|
+
tokenProgram,
|
|
503
|
+
});
|
|
504
|
+
return jsonResult({
|
|
505
|
+
kind: 'spend_limit',
|
|
506
|
+
status: 'ok',
|
|
507
|
+
mode: 'revoke',
|
|
508
|
+
symbol,
|
|
509
|
+
mint: meta.mint,
|
|
510
|
+
treasury: result.treasury,
|
|
511
|
+
source_token_account: result.sourceTokenAccount,
|
|
512
|
+
delegated_amount_atomic: '0',
|
|
513
|
+
delegated_amount: '0',
|
|
514
|
+
tx_signature: result.signature,
|
|
515
|
+
network: this.network,
|
|
516
|
+
explorer_url: explorerTxUrl(result.signature, this.network),
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
const cap = mode === 'unlimited'
|
|
520
|
+
? 2n ** 64n - 1n
|
|
521
|
+
: decimalToAtomic(args.amount, meta.decimals);
|
|
522
|
+
if (cap <= 0n) {
|
|
523
|
+
return jsonResult({
|
|
524
|
+
kind: 'spend_limit',
|
|
525
|
+
status: 'error',
|
|
526
|
+
message: 'Resolved cap is zero — pass a larger `amount` or use `mode: "unlimited"`.',
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
const result = await setSpendDelegation(umi, {
|
|
530
|
+
agentAsset: this.agentMint,
|
|
531
|
+
mint: meta.mint,
|
|
532
|
+
executive: this.ownerWallet ?? '',
|
|
533
|
+
amount: cap,
|
|
534
|
+
tokenProgram,
|
|
535
|
+
});
|
|
536
|
+
return jsonResult({
|
|
537
|
+
kind: 'spend_limit',
|
|
538
|
+
status: 'ok',
|
|
539
|
+
mode,
|
|
540
|
+
symbol,
|
|
541
|
+
mint: meta.mint,
|
|
542
|
+
delegate: result.delegate,
|
|
543
|
+
treasury: result.treasury,
|
|
544
|
+
source_token_account: result.sourceTokenAccount,
|
|
545
|
+
delegated_amount_atomic: result.delegatedAmount.toString(),
|
|
546
|
+
delegated_amount: mode === 'unlimited' ? 'unlimited' : atomicToDecimal(cap, meta.decimals),
|
|
547
|
+
tx_signature: result.signature,
|
|
548
|
+
network: this.network,
|
|
549
|
+
explorer_url: explorerTxUrl(result.signature, this.network),
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
catch (err) {
|
|
553
|
+
return jsonResult({
|
|
554
|
+
kind: 'spend_limit',
|
|
555
|
+
status: 'error',
|
|
556
|
+
message: err instanceof Error ? err.message : 'unknown error',
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Look up a single ReceiptV1 by its `receipt_hash` via the Leash
|
|
562
|
+
* API's by-hash endpoint. Returns the canonical seller-side blob
|
|
563
|
+
* (the same JSON the explorer renders) plus a few convenience
|
|
564
|
+
* fields the LLM can quote inline.
|
|
565
|
+
*/
|
|
566
|
+
async getReceipt(args) {
|
|
567
|
+
if (!this.config.apiKey) {
|
|
568
|
+
return jsonResult({
|
|
569
|
+
kind: 'receipt',
|
|
570
|
+
status: 'error',
|
|
571
|
+
message: 'Looking up receipts by hash currently requires LEASH_API_KEY in the environment. (X-Leash-Sig auth ships in the next release alongside discovery + reputation.)',
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
const hash = (args.receipt_hash ?? '').trim();
|
|
575
|
+
if (!hash) {
|
|
576
|
+
return jsonResult({
|
|
577
|
+
kind: 'receipt',
|
|
578
|
+
status: 'error',
|
|
579
|
+
message: '`receipt_hash` is required (the 64-hex-char value the explorer renders at /receipt/{hash}).',
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
try {
|
|
583
|
+
const url = `${this.apiBaseUrl}/v1/receipts/by-hash/${encodeURIComponent(hash)}`;
|
|
584
|
+
const res = await fetch(url, {
|
|
585
|
+
headers: { authorization: `Bearer ${this.config.apiKey}` },
|
|
586
|
+
});
|
|
587
|
+
const text = await res.text();
|
|
588
|
+
if (res.status === 404) {
|
|
589
|
+
return jsonResult({
|
|
590
|
+
kind: 'receipt',
|
|
591
|
+
status: 'not_found',
|
|
592
|
+
receipt_hash: hash,
|
|
593
|
+
network: this.network,
|
|
594
|
+
message: `No receipt with hash ${hash} on ${this.network} (cross-network reads are impossible by design \u2014 if this hash came from the sibling cluster, switch LEASH_NETWORK and retry).`,
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
if (!res.ok) {
|
|
598
|
+
return jsonResult({
|
|
599
|
+
kind: 'receipt',
|
|
600
|
+
status: 'error',
|
|
601
|
+
message: `Leash API ${res.status}: ${text.slice(0, 300)}`,
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
const row = JSON.parse(text);
|
|
605
|
+
return jsonResult({
|
|
606
|
+
kind: 'receipt',
|
|
607
|
+
status: 'ok',
|
|
608
|
+
receipt_hash: row.receipt_hash,
|
|
609
|
+
agent: row.agent,
|
|
610
|
+
direction: row.kind === 'spend' ? 'outgoing' : 'incoming',
|
|
611
|
+
decision: row.decision,
|
|
612
|
+
network: row.network,
|
|
613
|
+
tx_signature: row.tx_sig,
|
|
614
|
+
ingested_at: row.ingested_at,
|
|
615
|
+
explorer_url: leashReceiptUrl(row.receipt_hash, { baseUrl: this.explorerBaseUrl }),
|
|
616
|
+
tx_explorer_url: row.tx_sig ? explorerTxUrl(row.tx_sig, row.network) : null,
|
|
617
|
+
receipt: row.raw,
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
catch (err) {
|
|
621
|
+
return jsonResult({
|
|
622
|
+
kind: 'receipt',
|
|
623
|
+
status: 'error',
|
|
624
|
+
message: err instanceof Error ? err.message : 'unknown error',
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Paginate `/v1/receipts/{agent}` and trim to the rolling
|
|
630
|
+
* `now - days` window, returning the receipts plus running totals.
|
|
631
|
+
* USD totals sum stables (USDC/USDG/USDT) at 1:1.
|
|
632
|
+
*/
|
|
633
|
+
async transactionHistory(args) {
|
|
634
|
+
if (!this.agentMint)
|
|
635
|
+
return noAgentResult('transaction_history');
|
|
636
|
+
if (!this.config.apiKey) {
|
|
637
|
+
return jsonResult({
|
|
638
|
+
kind: 'transaction_history',
|
|
639
|
+
status: 'error',
|
|
640
|
+
message: 'Listing receipts from the standalone MCP currently requires LEASH_API_KEY in the environment.',
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
const days = clampInt(args.days ?? 7, 1, 90);
|
|
644
|
+
const limit = clampInt(args.limit ?? 200, 1, 1000);
|
|
645
|
+
const direction = args.direction ?? 'both';
|
|
646
|
+
const cutoffMs = Date.now() - days * 86_400_000;
|
|
647
|
+
try {
|
|
648
|
+
const rows = await fetchReceiptWindow({
|
|
649
|
+
apiBaseUrl: this.apiBaseUrl,
|
|
650
|
+
apiKey: this.config.apiKey,
|
|
651
|
+
agent: this.agentMint,
|
|
652
|
+
direction,
|
|
653
|
+
limit,
|
|
654
|
+
cutoffMs,
|
|
655
|
+
});
|
|
656
|
+
const totals = aggregateReceipts(rows.items);
|
|
657
|
+
return jsonResult({
|
|
658
|
+
kind: 'transaction_history',
|
|
659
|
+
status: 'ok',
|
|
660
|
+
agent_mint: this.agentMint,
|
|
661
|
+
network: this.network,
|
|
662
|
+
range: {
|
|
663
|
+
from: new Date(cutoffMs).toISOString(),
|
|
664
|
+
to: new Date().toISOString(),
|
|
665
|
+
days,
|
|
666
|
+
},
|
|
667
|
+
direction,
|
|
668
|
+
count: rows.items.length,
|
|
669
|
+
truncated: rows.truncated,
|
|
670
|
+
total_sent_usd: totals.totalSentUsd,
|
|
671
|
+
total_received_usd: totals.totalReceivedUsd,
|
|
672
|
+
net_usd: totals.netUsd,
|
|
673
|
+
sent_count: totals.sentCount,
|
|
674
|
+
received_count: totals.receivedCount,
|
|
675
|
+
non_usd_count: totals.nonUsdCount,
|
|
676
|
+
items: rows.items.map((r) => ({
|
|
677
|
+
receipt_hash: r.receipt_hash,
|
|
678
|
+
direction: r.kind === 'spend' ? 'outgoing' : 'incoming',
|
|
679
|
+
decision: r.decision,
|
|
680
|
+
tx_signature: r.tx_sig,
|
|
681
|
+
url: r.raw?.request?.url ?? null,
|
|
682
|
+
method: r.raw?.request?.method ?? null,
|
|
683
|
+
amount: r.raw?.price?.amount ?? null,
|
|
684
|
+
currency: r.raw?.price?.currency ?? null,
|
|
685
|
+
timestamp: r.ingested_at,
|
|
686
|
+
explorer_url: leashReceiptUrl(r.receipt_hash, { baseUrl: this.explorerBaseUrl }),
|
|
687
|
+
tx_explorer_url: r.tx_sig ? explorerTxUrl(r.tx_sig, this.network) : null,
|
|
688
|
+
})),
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
catch (err) {
|
|
692
|
+
return jsonResult({
|
|
693
|
+
kind: 'transaction_history',
|
|
694
|
+
status: 'error',
|
|
695
|
+
message: err instanceof Error ? err.message : 'unknown error',
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Bin the same receipts as `transactionHistory` by UTC ingest date
|
|
701
|
+
* and return per-day buckets plus grand totals. Days with zero
|
|
702
|
+
* activity are filled with zeros so the timeline is continuous.
|
|
703
|
+
*/
|
|
704
|
+
async dailyTransactions(args) {
|
|
705
|
+
if (!this.agentMint)
|
|
706
|
+
return noAgentResult('daily_transactions');
|
|
707
|
+
if (!this.config.apiKey) {
|
|
708
|
+
return jsonResult({
|
|
709
|
+
kind: 'daily_transactions',
|
|
710
|
+
status: 'error',
|
|
711
|
+
message: 'Daily aggregates from the standalone MCP currently require LEASH_API_KEY in the environment.',
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
const days = clampInt(args.days ?? 7, 1, 90);
|
|
715
|
+
const cutoffMs = Date.now() - days * 86_400_000;
|
|
716
|
+
try {
|
|
717
|
+
const rows = await fetchReceiptWindow({
|
|
718
|
+
apiBaseUrl: this.apiBaseUrl,
|
|
719
|
+
apiKey: this.config.apiKey,
|
|
720
|
+
agent: this.agentMint,
|
|
721
|
+
direction: 'both',
|
|
722
|
+
limit: 1000,
|
|
723
|
+
cutoffMs,
|
|
724
|
+
});
|
|
725
|
+
const buckets = bucketReceiptsByDay(rows.items, days);
|
|
726
|
+
const totals = aggregateReceipts(rows.items);
|
|
727
|
+
return jsonResult({
|
|
728
|
+
kind: 'daily_transactions',
|
|
729
|
+
status: 'ok',
|
|
730
|
+
agent_mint: this.agentMint,
|
|
731
|
+
network: this.network,
|
|
732
|
+
range: {
|
|
733
|
+
from: new Date(cutoffMs).toISOString(),
|
|
734
|
+
to: new Date().toISOString(),
|
|
735
|
+
days,
|
|
736
|
+
},
|
|
737
|
+
daily: buckets,
|
|
738
|
+
totals: {
|
|
739
|
+
sent_count: totals.sentCount,
|
|
740
|
+
sent_usd: totals.totalSentUsd,
|
|
741
|
+
received_count: totals.receivedCount,
|
|
742
|
+
received_usd: totals.totalReceivedUsd,
|
|
743
|
+
net_usd: totals.netUsd,
|
|
744
|
+
non_usd_count: totals.nonUsdCount,
|
|
745
|
+
},
|
|
746
|
+
truncated: rows.truncated,
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
catch (err) {
|
|
750
|
+
return jsonResult({
|
|
751
|
+
kind: 'daily_transactions',
|
|
752
|
+
status: 'error',
|
|
753
|
+
message: err instanceof Error ? err.message : 'unknown error',
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Read the current SPL delegation + treasury balance for `symbol`.
|
|
759
|
+
* Pure RPC — no signing.
|
|
760
|
+
*/
|
|
761
|
+
async getSpendLimit(args) {
|
|
762
|
+
if (!this.agentMint)
|
|
763
|
+
return noAgentResult('spend_limit');
|
|
764
|
+
const symbol = (args.symbol ?? 'USDC');
|
|
765
|
+
const meta = lookupTokenBySymbolSafe(symbol, tokenNetwork(this.network));
|
|
766
|
+
if (!meta) {
|
|
767
|
+
return jsonResult({
|
|
768
|
+
kind: 'spend_limit',
|
|
769
|
+
status: 'error',
|
|
770
|
+
message: `${symbol} is not configured for ${this.network}.`,
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
try {
|
|
774
|
+
const umi = this.signer.getUmi(this.rpcUrl);
|
|
775
|
+
const tokenProgram = meta.program === 'spl-token-2022' ? UMI_TOKEN_2022_PROGRAM_ID : SPL_TOKEN_PROGRAM_ID;
|
|
776
|
+
const status = await getSpendDelegation(umi, {
|
|
777
|
+
agentAsset: this.agentMint,
|
|
778
|
+
mint: meta.mint,
|
|
779
|
+
tokenProgram,
|
|
780
|
+
});
|
|
781
|
+
const isUnlimited = status.delegatedAmount === 2n ** 64n - 1n;
|
|
782
|
+
return jsonResult({
|
|
783
|
+
kind: 'spend_limit',
|
|
784
|
+
status: 'ok',
|
|
785
|
+
symbol,
|
|
786
|
+
mint: meta.mint,
|
|
787
|
+
treasury: status.treasury,
|
|
788
|
+
source_token_account: status.sourceTokenAccount,
|
|
789
|
+
source_exists: status.sourceExists,
|
|
790
|
+
delegate: status.delegate,
|
|
791
|
+
executive_pubkey: this.ownerWallet,
|
|
792
|
+
delegate_matches_executive: status.delegate === this.ownerWallet,
|
|
793
|
+
delegated_amount_atomic: status.delegatedAmount.toString(),
|
|
794
|
+
delegated_amount: isUnlimited
|
|
795
|
+
? 'unlimited'
|
|
796
|
+
: atomicToDecimal(status.delegatedAmount, meta.decimals),
|
|
797
|
+
balance_atomic: status.balance.toString(),
|
|
798
|
+
balance: atomicToDecimal(status.balance, meta.decimals),
|
|
799
|
+
decimals: meta.decimals,
|
|
800
|
+
network: this.network,
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
catch (err) {
|
|
804
|
+
return jsonResult({
|
|
805
|
+
kind: 'spend_limit',
|
|
806
|
+
status: 'error',
|
|
807
|
+
message: err instanceof Error ? err.message : 'unknown error',
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* Convert a human decimal (e.g. `100`, `1.5`) to atomic units using
|
|
814
|
+
* the mint's decimals. Floors to avoid silently rounding up.
|
|
815
|
+
*/
|
|
816
|
+
function decimalToAtomic(amount, decimals) {
|
|
817
|
+
if (!Number.isFinite(amount) || amount <= 0)
|
|
818
|
+
return 0n;
|
|
819
|
+
// Multiply via string to avoid float precision drift on small
|
|
820
|
+
// amounts (e.g. 0.000001 USDC).
|
|
821
|
+
const [whole, frac = ''] = amount.toString().split('.');
|
|
822
|
+
const fracPadded = (frac + '0'.repeat(decimals)).slice(0, decimals);
|
|
823
|
+
const combined = `${whole}${fracPadded}`.replace(/^0+(?=\d)/, '');
|
|
824
|
+
try {
|
|
825
|
+
return BigInt(combined);
|
|
826
|
+
}
|
|
827
|
+
catch {
|
|
828
|
+
return 0n;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
/** Inverse of {@link decimalToAtomic}. Trims trailing zeros. */
|
|
832
|
+
function atomicToDecimal(amount, decimals) {
|
|
833
|
+
if (decimals === 0)
|
|
834
|
+
return amount.toString();
|
|
835
|
+
const s = amount.toString().padStart(decimals + 1, '0');
|
|
836
|
+
const whole = s.slice(0, s.length - decimals);
|
|
837
|
+
const frac = s.slice(s.length - decimals).replace(/0+$/, '');
|
|
838
|
+
return frac.length > 0 ? `${whole}.${frac}` : whole;
|
|
839
|
+
}
|
|
840
|
+
function explorerAccountUrl(pubkey, network) {
|
|
841
|
+
const cluster = network === 'solana-mainnet' ? '' : '?cluster=devnet';
|
|
842
|
+
return `https://solscan.io/account/${pubkey}${cluster}`;
|
|
843
|
+
}
|
|
844
|
+
function explorerTxUrl(signature, network) {
|
|
845
|
+
const cluster = network === 'solana-mainnet' ? '' : '?cluster=devnet';
|
|
846
|
+
return `https://solscan.io/tx/${signature}${cluster}`;
|
|
847
|
+
}
|
|
848
|
+
/** Clamp an integer into `[min, max]`, falling back to `min` on NaN. */
|
|
849
|
+
function clampInt(n, min, max) {
|
|
850
|
+
if (!Number.isFinite(n))
|
|
851
|
+
return min;
|
|
852
|
+
return Math.max(min, Math.min(max, Math.floor(n)));
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Walk the paginated `/v1/receipts/{agent}` feed newest-first until
|
|
856
|
+
* we either hit `cutoffMs` (i.e. the user's window boundary), exhaust
|
|
857
|
+
* the feed, or hit `limit` rows. Returns the in-window subset plus a
|
|
858
|
+
* `truncated` flag the caller can surface when more receipts exist
|
|
859
|
+
* but would have blown the cap.
|
|
860
|
+
*/
|
|
861
|
+
async function fetchReceiptWindow(args) {
|
|
862
|
+
const items = [];
|
|
863
|
+
let cursor = null;
|
|
864
|
+
let truncated = false;
|
|
865
|
+
// Cap the underlying paginations to prevent runaway loops on
|
|
866
|
+
// extremely active agents \u2014 200 rows/page * 10 pages = 2000 rows
|
|
867
|
+
// is well above any sane day-window response.
|
|
868
|
+
const maxPages = 10;
|
|
869
|
+
for (let page = 0; page < maxPages; page++) {
|
|
870
|
+
const url = new URL(`${args.apiBaseUrl}/v1/receipts/${args.agent}`);
|
|
871
|
+
url.searchParams.set('limit', '200');
|
|
872
|
+
if (args.direction === 'outgoing')
|
|
873
|
+
url.searchParams.set('kind', 'spend');
|
|
874
|
+
else if (args.direction === 'incoming')
|
|
875
|
+
url.searchParams.set('kind', 'earn');
|
|
876
|
+
if (cursor)
|
|
877
|
+
url.searchParams.set('cursor', cursor);
|
|
878
|
+
const res = await fetch(url, { headers: { authorization: `Bearer ${args.apiKey}` } });
|
|
879
|
+
const text = await res.text();
|
|
880
|
+
if (!res.ok) {
|
|
881
|
+
throw new Error(`Leash API ${res.status}: ${text.slice(0, 300)}`);
|
|
882
|
+
}
|
|
883
|
+
const json = JSON.parse(text);
|
|
884
|
+
let stop = false;
|
|
885
|
+
for (const r of json.items) {
|
|
886
|
+
const ingestedMs = Date.parse(r.ingested_at);
|
|
887
|
+
if (Number.isFinite(ingestedMs) && ingestedMs < args.cutoffMs) {
|
|
888
|
+
stop = true;
|
|
889
|
+
break;
|
|
890
|
+
}
|
|
891
|
+
items.push(r);
|
|
892
|
+
if (items.length >= args.limit) {
|
|
893
|
+
truncated = true;
|
|
894
|
+
stop = true;
|
|
895
|
+
break;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
if (stop || !json.next_cursor)
|
|
899
|
+
break;
|
|
900
|
+
cursor = json.next_cursor;
|
|
901
|
+
}
|
|
902
|
+
return { items, truncated };
|
|
903
|
+
}
|
|
904
|
+
/** USD-symbol whitelist used by the per-day + transaction-history aggregators. */
|
|
905
|
+
const USD_STABLES = new Set(['USDC', 'USDG', 'USDT']);
|
|
906
|
+
/** Sum sent/received decimals across a list of receipts. */
|
|
907
|
+
function aggregateReceipts(items) {
|
|
908
|
+
let sentCount = 0;
|
|
909
|
+
let receivedCount = 0;
|
|
910
|
+
let nonUsdCount = 0;
|
|
911
|
+
// Use a string-based decimal sum to avoid float drift on long
|
|
912
|
+
// running totals. We accumulate in a Decimal128-style helper.
|
|
913
|
+
let sentSum = 0;
|
|
914
|
+
let receivedSum = 0;
|
|
915
|
+
for (const r of items) {
|
|
916
|
+
const amt = parseFloat(r.raw?.price?.amount ?? '');
|
|
917
|
+
const cur = (r.raw?.price?.currency ?? '').toUpperCase();
|
|
918
|
+
if (r.kind === 'spend')
|
|
919
|
+
sentCount++;
|
|
920
|
+
else if (r.kind === 'earn')
|
|
921
|
+
receivedCount++;
|
|
922
|
+
if (!Number.isFinite(amt) || !cur)
|
|
923
|
+
continue;
|
|
924
|
+
if (!USD_STABLES.has(cur)) {
|
|
925
|
+
nonUsdCount++;
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
928
|
+
if (r.kind === 'spend')
|
|
929
|
+
sentSum += amt;
|
|
930
|
+
else if (r.kind === 'earn')
|
|
931
|
+
receivedSum += amt;
|
|
932
|
+
}
|
|
933
|
+
const round = (n) => Math.round(n * 1_000_000) / 1_000_000;
|
|
934
|
+
return {
|
|
935
|
+
sentCount,
|
|
936
|
+
receivedCount,
|
|
937
|
+
nonUsdCount,
|
|
938
|
+
totalSentUsd: round(sentSum).toString(),
|
|
939
|
+
totalReceivedUsd: round(receivedSum).toString(),
|
|
940
|
+
netUsd: round(receivedSum - sentSum).toString(),
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Bucket receipts into per-day rows (UTC `YYYY-MM-DD`). Days with no
|
|
945
|
+
* activity are emitted with zeros so the timeline is continuous.
|
|
946
|
+
* Sorted newest-first.
|
|
947
|
+
*/
|
|
948
|
+
function bucketReceiptsByDay(items, days) {
|
|
949
|
+
const map = new Map();
|
|
950
|
+
// Seed with empty buckets for every day in the window so the LLM
|
|
951
|
+
// sees a continuous range even when the agent had no activity.
|
|
952
|
+
const today = utcDay(new Date());
|
|
953
|
+
for (let i = 0; i < days; i++) {
|
|
954
|
+
const d = new Date(today.getTime() - i * 86_400_000);
|
|
955
|
+
map.set(formatUtcDate(d), { sentCount: 0, sentSum: 0, receivedCount: 0, receivedSum: 0 });
|
|
956
|
+
}
|
|
957
|
+
for (const r of items) {
|
|
958
|
+
const ingested = new Date(r.ingested_at);
|
|
959
|
+
if (Number.isNaN(ingested.getTime()))
|
|
960
|
+
continue;
|
|
961
|
+
const key = formatUtcDate(ingested);
|
|
962
|
+
let bucket = map.get(key);
|
|
963
|
+
if (!bucket) {
|
|
964
|
+
bucket = { sentCount: 0, sentSum: 0, receivedCount: 0, receivedSum: 0 };
|
|
965
|
+
map.set(key, bucket);
|
|
966
|
+
}
|
|
967
|
+
if (r.kind === 'spend')
|
|
968
|
+
bucket.sentCount++;
|
|
969
|
+
else if (r.kind === 'earn')
|
|
970
|
+
bucket.receivedCount++;
|
|
971
|
+
const amt = parseFloat(r.raw?.price?.amount ?? '');
|
|
972
|
+
const cur = (r.raw?.price?.currency ?? '').toUpperCase();
|
|
973
|
+
if (!Number.isFinite(amt) || !USD_STABLES.has(cur))
|
|
974
|
+
continue;
|
|
975
|
+
if (r.kind === 'spend')
|
|
976
|
+
bucket.sentSum += amt;
|
|
977
|
+
else if (r.kind === 'earn')
|
|
978
|
+
bucket.receivedSum += amt;
|
|
979
|
+
}
|
|
980
|
+
const round = (n) => Math.round(n * 1_000_000) / 1_000_000;
|
|
981
|
+
return [...map.entries()]
|
|
982
|
+
.sort((a, b) => (a[0] < b[0] ? 1 : -1))
|
|
983
|
+
.map(([date, b]) => ({
|
|
984
|
+
date,
|
|
985
|
+
sent_count: b.sentCount,
|
|
986
|
+
sent_usd: round(b.sentSum).toString(),
|
|
987
|
+
received_count: b.receivedCount,
|
|
988
|
+
received_usd: round(b.receivedSum).toString(),
|
|
989
|
+
net_usd: round(b.receivedSum - b.sentSum).toString(),
|
|
990
|
+
}));
|
|
991
|
+
}
|
|
992
|
+
function utcDay(d) {
|
|
993
|
+
return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
|
|
994
|
+
}
|
|
995
|
+
function formatUtcDate(d) {
|
|
996
|
+
const y = d.getUTCFullYear();
|
|
997
|
+
const m = String(d.getUTCMonth() + 1).padStart(2, '0');
|
|
998
|
+
const day = String(d.getUTCDate()).padStart(2, '0');
|
|
999
|
+
return `${y}-${m}-${day}`;
|
|
1000
|
+
}
|
|
1001
|
+
//# sourceMappingURL=host-stdio.js.map
|