@agentwonderland/mcp 0.1.25 → 0.1.27
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/dist/core/__tests__/api-client.test.d.ts +1 -0
- package/dist/core/__tests__/api-client.test.js +51 -0
- package/dist/core/__tests__/formatters.test.js +10 -0
- package/dist/core/__tests__/passes-api.test.d.ts +1 -0
- package/dist/core/__tests__/passes-api.test.js +27 -0
- package/dist/core/__tests__/payments.test.js +10 -6
- package/dist/core/__tests__/principal.test.js +41 -4
- package/dist/core/__tests__/solana-charge.test.d.ts +1 -0
- package/dist/core/__tests__/solana-charge.test.js +50 -0
- package/dist/core/api-client.d.ts +1 -0
- package/dist/core/api-client.js +8 -3
- package/dist/core/balances.d.ts +1 -0
- package/dist/core/balances.js +56 -0
- package/dist/core/base-charge.js +13 -6
- package/dist/core/formatters.d.ts +3 -2
- package/dist/core/formatters.js +7 -1
- package/dist/core/passes.d.ts +1 -1
- package/dist/core/passes.js +5 -2
- package/dist/core/payments.d.ts +1 -0
- package/dist/core/payments.js +20 -7
- package/dist/core/principal.d.ts +3 -0
- package/dist/core/principal.js +29 -1
- package/dist/core/settings.d.ts +20 -0
- package/dist/core/settings.js +19 -0
- package/dist/core/solana-charge.d.ts +5 -0
- package/dist/core/solana-charge.js +29 -7
- package/dist/core/tempo-charge.d.ts +7 -0
- package/dist/core/tempo-charge.js +84 -0
- package/dist/index.js +5 -7
- package/dist/prompts/index.js +1 -1
- package/dist/tools/__tests__/jobs.test.d.ts +1 -0
- package/dist/tools/__tests__/jobs.test.js +71 -0
- package/dist/tools/__tests__/run.test.d.ts +1 -0
- package/dist/tools/__tests__/run.test.js +149 -0
- package/dist/tools/__tests__/solve.test.d.ts +1 -0
- package/dist/tools/__tests__/solve.test.js +158 -0
- package/dist/tools/__tests__/wallet.test.d.ts +1 -0
- package/dist/tools/__tests__/wallet.test.js +230 -0
- package/dist/tools/_payment-confirmation.js +1 -1
- package/dist/tools/agent-info.js +14 -28
- package/dist/tools/jobs.js +82 -16
- package/dist/tools/passes.js +30 -14
- package/dist/tools/run.js +35 -20
- package/dist/tools/search.js +9 -8
- package/dist/tools/solve.js +45 -25
- package/dist/tools/wallet.js +35 -15
- package/package.json +2 -2
- package/src/core/__tests__/api-client.test.ts +78 -0
- package/src/core/__tests__/formatters.test.ts +12 -0
- package/src/core/__tests__/passes-api.test.ts +33 -0
- package/src/core/__tests__/payments.test.ts +17 -6
- package/src/core/__tests__/principal.test.ts +49 -4
- package/src/core/__tests__/solana-charge.test.ts +59 -0
- package/src/core/api-client.ts +16 -3
- package/src/core/balances.ts +63 -0
- package/src/core/base-charge.ts +13 -6
- package/src/core/formatters.ts +10 -3
- package/src/core/passes.ts +5 -2
- package/src/core/payments.ts +22 -7
- package/src/core/principal.ts +42 -1
- package/src/core/settings.ts +36 -0
- package/src/core/solana-charge.ts +43 -9
- package/src/core/tempo-charge.ts +104 -0
- package/src/index.ts +5 -7
- package/src/prompts/index.ts +1 -1
- package/src/tools/__tests__/jobs.test.ts +89 -0
- package/src/tools/__tests__/run.test.ts +176 -0
- package/src/tools/__tests__/solve.test.ts +186 -0
- package/src/tools/__tests__/wallet.test.ts +289 -0
- package/src/tools/_payment-confirmation.ts +1 -1
- package/src/tools/agent-info.ts +15 -38
- package/src/tools/jobs.ts +79 -17
- package/src/tools/passes.ts +30 -14
- package/src/tools/run.ts +38 -20
- package/src/tools/search.ts +10 -9
- package/src/tools/solve.ts +48 -25
- package/src/tools/wallet.ts +33 -17
- package/src/tools/observability.ts +0 -43
package/dist/tools/solve.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { apiGet, apiPost, apiPostWithPayment } from "../core/api-client.js";
|
|
3
3
|
import { getOrCreatePendingCardSetup, formatCardSetupBlocks } from "../core/card-setup.js";
|
|
4
|
-
import { getCompatiblePaymentMethods, hasWalletConfigured, getConfiguredMethods, getAcceptedPaymentMethods, getWalletAddress, normalizePaymentMethod, toRegistryPaymentMethod, } from "../core/payments.js";
|
|
4
|
+
import { getCompatiblePaymentMethods, hasWalletConfigured, getConfiguredMethods, getAcceptedPaymentMethods, getWalletAddress, normalizePaymentMethod, toRegistryPaymentMethod, isCardPaymentEnabled, } from "../core/payments.js";
|
|
5
5
|
import { requiresSpendConfirmation, getDefaultTipAmount } from "../core/config.js";
|
|
6
6
|
import { agentList, formatRunResult } from "../core/formatters.js";
|
|
7
7
|
import { canSpend, recordSpend, requiresPolicyConfirmation } from "../core/spend-policy.js";
|
|
@@ -11,9 +11,9 @@ import { formatPaymentLabel, formatSolveConfirmationCommand, makeSolvePendingKey
|
|
|
11
11
|
import { getActiveCreditPack, getCreditPackInventory, getCreditPackProgram } from "../core/passes.js";
|
|
12
12
|
const POLL_INTERVAL_MS = 3000;
|
|
13
13
|
const POLL_MAX_MS = 120000;
|
|
14
|
-
async function pollSolveJob(jobId) {
|
|
14
|
+
async function pollSolveJob(jobId, paymentMethod) {
|
|
15
15
|
const deadline = Date.now() + POLL_MAX_MS;
|
|
16
|
-
const walletAddress = await getWalletAddress();
|
|
16
|
+
const walletAddress = await getWalletAddress(paymentMethod);
|
|
17
17
|
const walletParam = walletAddress ? `?wallet=${walletAddress}` : "";
|
|
18
18
|
while (Date.now() < deadline) {
|
|
19
19
|
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
@@ -106,29 +106,41 @@ export function registerSolveTools(server) {
|
|
|
106
106
|
.string()
|
|
107
107
|
.trim()
|
|
108
108
|
.min(1)
|
|
109
|
-
.
|
|
109
|
+
.optional()
|
|
110
|
+
.describe("Payment method — wallet ID, chain name (tempo, base, etc.), or 'card'. Auto-detected if omitted."),
|
|
110
111
|
confirmed: z
|
|
111
112
|
.boolean()
|
|
112
113
|
.optional()
|
|
113
114
|
.describe("Set to true to confirm spending after seeing the price quote."),
|
|
114
115
|
}, async ({ intent, input, budget, pay_with, confirmed }) => {
|
|
115
116
|
if (!hasWalletConfigured()) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
if (isCardPaymentEnabled()) {
|
|
118
|
+
try {
|
|
119
|
+
const { url } = await getOrCreatePendingCardSetup();
|
|
120
|
+
return multiText(...formatCardSetupBlocks(url));
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// Fall through to the setup message below.
|
|
124
|
+
}
|
|
119
125
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
126
|
+
const setupLines = [
|
|
127
|
+
"No payment method configured.",
|
|
128
|
+
"",
|
|
129
|
+
"Supported rails: Tempo USDC, Base USDC, Solana USDC.",
|
|
130
|
+
"Run wallet_setup({ action: \"create\" }) to create a crypto wallet,",
|
|
131
|
+
"or wallet_setup({ action: \"import\" }) with an existing key.",
|
|
132
|
+
];
|
|
133
|
+
if (isCardPaymentEnabled()) {
|
|
134
|
+
setupLines.push("", "Or wallet_setup({ action: \"add-card\" }) to connect a credit card.");
|
|
124
135
|
}
|
|
136
|
+
return text(setupLines.join("\n"));
|
|
125
137
|
}
|
|
126
138
|
const pendingKey = makeSolvePendingKey(intent, input, budget);
|
|
127
139
|
const pending = pendingSolves.get(pendingKey);
|
|
128
140
|
const configuredMethods = getConfiguredMethods();
|
|
129
|
-
const requestedMethod = pay_with;
|
|
130
|
-
const normalizedRequestedMethod = normalizePaymentMethod(requestedMethod);
|
|
131
|
-
if (!normalizedRequestedMethod) {
|
|
141
|
+
const requestedMethod = pay_with ?? pending?.method;
|
|
142
|
+
const normalizedRequestedMethod = requestedMethod ? normalizePaymentMethod(requestedMethod) : null;
|
|
143
|
+
if (requestedMethod && !normalizedRequestedMethod) {
|
|
132
144
|
return text(`Payment method "${requestedMethod}" is not configured.\n\n` +
|
|
133
145
|
"Use wallet_status to review your current payment methods.");
|
|
134
146
|
}
|
|
@@ -156,13 +168,16 @@ export function registerSolveTools(server) {
|
|
|
156
168
|
const cost = result.cost;
|
|
157
169
|
const usedCreditPack = result.consumption_mode === "credit_pack";
|
|
158
170
|
const tipMsg = result.feedback_token ? await autoTip(jobId, agentId, agentName, result.feedback_token) : "";
|
|
159
|
-
|
|
171
|
+
const header = agentName ? [`Matched agent: ${agentName}`, ""].join("\n") : "";
|
|
172
|
+
return multiText(header + formatRunResult(result), feedbackAsk(jobId, agentId, usedCreditPack ? undefined : cost, tipMsg));
|
|
160
173
|
}
|
|
161
174
|
catch (err) {
|
|
162
|
-
const
|
|
163
|
-
"status" in err
|
|
164
|
-
err.status
|
|
165
|
-
|
|
175
|
+
const status = err instanceof Error &&
|
|
176
|
+
"status" in err
|
|
177
|
+
? err.status
|
|
178
|
+
: undefined;
|
|
179
|
+
const isRecoverableDirectSolveError = status === 401 || status === 402;
|
|
180
|
+
if (!isRecoverableDirectSolveError || !hasWalletConfigured())
|
|
166
181
|
throw err;
|
|
167
182
|
}
|
|
168
183
|
const params = new URLSearchParams({ q: intent, limit: "5" });
|
|
@@ -182,15 +197,19 @@ export function registerSolveTools(server) {
|
|
|
182
197
|
return price <= budget;
|
|
183
198
|
});
|
|
184
199
|
const selected = affordable[0] ?? agents[0];
|
|
185
|
-
const activeCreditPack = await getCreditPackInventory(selected.id).then(getActiveCreditPack);
|
|
200
|
+
const activeCreditPack = await getCreditPackInventory(selected.id, requestedMethod).then(getActiveCreditPack);
|
|
186
201
|
const compatibleMethods = getCompatiblePaymentMethods(selected, configuredMethods);
|
|
187
|
-
if (!compatibleMethods.includes(normalizedRequestedMethod)) {
|
|
202
|
+
if (normalizedRequestedMethod && !compatibleMethods.includes(normalizedRequestedMethod)) {
|
|
188
203
|
return text(`The best matching agent cannot be paid with "${requestedMethod}".\n\n` +
|
|
189
204
|
`Available payment methods for ${selected.name}: ${compatibleMethods.join(", ") || "none"}.\n` +
|
|
190
205
|
"Choose another payment method or refine your search.");
|
|
191
206
|
}
|
|
192
|
-
|
|
193
|
-
|
|
207
|
+
if (!activeCreditPack && compatibleMethods.length === 0) {
|
|
208
|
+
return text(`No compatible payment methods are configured for ${selected.name}.\n\n` +
|
|
209
|
+
"Use wallet_status to review your current payment methods.");
|
|
210
|
+
}
|
|
211
|
+
const method = resolveConfirmationMethod(requestedMethod, pending?.method, compatibleMethods);
|
|
212
|
+
const spendCheckMethod = method ?? normalizedRequestedMethod ?? compatibleMethods[0];
|
|
194
213
|
const selectedPrice = parseFloat(selected.pricePerRunUsd ?? "0.01");
|
|
195
214
|
const estimatedCost = selectedPrice;
|
|
196
215
|
const needsPolicyConfirmation = !activeCreditPack && requiresPolicyConfirmation(spendCheckMethod, estimatedCost);
|
|
@@ -247,8 +266,9 @@ export function registerSolveTools(server) {
|
|
|
247
266
|
catch (err) {
|
|
248
267
|
const apiErr = err;
|
|
249
268
|
if (apiErr?.status === 402) {
|
|
269
|
+
const reason = apiErr.message ? `Payment failed: ${apiErr.message}` : "Payment failed — wallet may not have enough funds or the selected method was rejected.";
|
|
250
270
|
return text([
|
|
251
|
-
|
|
271
|
+
reason,
|
|
252
272
|
"",
|
|
253
273
|
"Check your balance and try again.",
|
|
254
274
|
"Use wallet_status to check your current payment methods.",
|
|
@@ -268,7 +288,7 @@ export function registerSolveTools(server) {
|
|
|
268
288
|
storeFeedbackToken(jobId, result.feedback_token, agentId);
|
|
269
289
|
}
|
|
270
290
|
if (status === "processing") {
|
|
271
|
-
const pollResult = await pollSolveJob(jobId);
|
|
291
|
+
const pollResult = await pollSolveJob(jobId, method);
|
|
272
292
|
if (pollResult.status === "completed") {
|
|
273
293
|
const asyncFormatted = formatRunResult({
|
|
274
294
|
...result,
|
package/dist/tools/wallet.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { getWallets, getCardConfig, setCardConfig, addWallet, getPendingCardSetupToken, getSpendPolicy, setSpendPolicy, } from "../core/config.js";
|
|
3
|
-
import { getWalletAddress } from "../core/payments.js";
|
|
3
|
+
import { getWalletAddress, isCardPaymentEnabled } from "../core/payments.js";
|
|
4
|
+
import { fetchUsdcBalance } from "../core/balances.js";
|
|
5
|
+
import { getSettings } from "../core/settings.js";
|
|
4
6
|
import { getOrCreatePendingCardSetup, formatCardSetupBlocks, getCardCapabilities, pollCardSetup, } from "../core/card-setup.js";
|
|
5
7
|
import { isOwsAvailable, createOwsWallet, importKeyToOws, listOwsWallets, listOwsWalletsByChain, } from "../core/ows-adapter.js";
|
|
6
8
|
import { ensureConsumerPrincipal, getConsumerPrincipal } from "../core/principal.js";
|
|
@@ -17,18 +19,33 @@ export function registerWalletTools(server) {
|
|
|
17
19
|
if (wallets.length === 0 && !card && !pendingCardSetupToken) {
|
|
18
20
|
return text("No payment methods configured.\nUse wallet_setup to create or import a wallet.");
|
|
19
21
|
}
|
|
20
|
-
const
|
|
22
|
+
const settings = await getSettings();
|
|
23
|
+
const networkLabel = settings ? ` (${settings.network})` : "";
|
|
24
|
+
const lines = [`Payment methods${networkLabel}:`];
|
|
21
25
|
for (const w of wallets) {
|
|
22
26
|
const label = w.label ? ` (${w.label})` : "";
|
|
23
27
|
const storage = w.keyType === "ows" ? " [encrypted]" : " [plaintext]";
|
|
24
|
-
const
|
|
28
|
+
const chainLines = await Promise.all(w.chains.map(async (chainName) => {
|
|
25
29
|
const addr = await getWalletAddress(chainName);
|
|
26
|
-
|
|
30
|
+
if (!addr)
|
|
31
|
+
return `${chainName}: unknown`;
|
|
32
|
+
let balanceStr = "";
|
|
33
|
+
if (chainName === "tempo" || chainName === "base" || chainName === "solana") {
|
|
34
|
+
const balance = await fetchUsdcBalance(chainName, addr);
|
|
35
|
+
if (balance !== null) {
|
|
36
|
+
const num = Number(balance);
|
|
37
|
+
balanceStr = ` ${Number.isFinite(num) ? num.toFixed(4).replace(/\.?0+$/, "") : balance} USDC`;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return `${chainName}: ${addr}${balanceStr}`;
|
|
27
41
|
}));
|
|
28
|
-
lines.push(` ${w.id}${label}${storage}
|
|
42
|
+
lines.push(` ${w.id}${label}${storage}:`);
|
|
43
|
+
for (const line of chainLines)
|
|
44
|
+
lines.push(` ${line}`);
|
|
29
45
|
}
|
|
30
|
-
if (card) {
|
|
31
|
-
|
|
46
|
+
if (card && isCardPaymentEnabled()) {
|
|
47
|
+
const stripeMode = settings?.stripe.mode === "test" ? " [Stripe test mode]" : "";
|
|
48
|
+
lines.push(` Card: ${card.brand} ****${card.last4}${stripeMode}`);
|
|
32
49
|
const capabilities = await getCardCapabilities();
|
|
33
50
|
if (capabilities.spt_status === "enabled") {
|
|
34
51
|
lines.push(" Card MPP: ready");
|
|
@@ -48,11 +65,11 @@ export function registerWalletTools(server) {
|
|
|
48
65
|
}
|
|
49
66
|
return text(lines.join("\n"));
|
|
50
67
|
});
|
|
51
|
-
// ── wallet_setup
|
|
52
|
-
server.tool("wallet_setup", "Set up or manage payment
|
|
68
|
+
// ── wallet_setup ────────────────────────────────────────────────
|
|
69
|
+
server.tool("wallet_setup", "Set up or manage a payment wallet. 'create' makes a new encrypted crypto wallet (OWS); 'import' takes an existing private key. Tempo/Base share one EVM key — a single wallet covers both. Solana uses a separate ed25519 key. For crypto wallet deletion or key rotation, direct users to edit ~/.agentwonderland/config.json or ~/.ows/ manually; never handle key material programmatically.", {
|
|
53
70
|
action: z
|
|
54
71
|
.enum(["create", "import", "add-card", "remove-card"])
|
|
55
|
-
.describe("'
|
|
72
|
+
.describe("'create' a crypto wallet (recommended), 'import' an existing key, 'add-card'/'remove-card' for credit card (card-backed MPP availability depends on Stripe SPT)"),
|
|
56
73
|
name: z
|
|
57
74
|
.string()
|
|
58
75
|
.optional()
|
|
@@ -62,10 +79,13 @@ export function registerWalletTools(server) {
|
|
|
62
79
|
.optional()
|
|
63
80
|
.describe("Private key hex string (required for 'import', ignored for 'create')"),
|
|
64
81
|
chain: z.enum(["tempo", "base", "solana"]).optional()
|
|
65
|
-
.describe("Primary chain (default: tempo).
|
|
82
|
+
.describe("Primary chain (default: tempo). Tempo/Base use a shared EVM wallet; Solana uses a separate OWS wallet."),
|
|
66
83
|
}, async ({ action, name, key, chain }) => {
|
|
67
84
|
// ── Card setup flow ──────────────────────────────────────
|
|
68
85
|
if (action === "add-card") {
|
|
86
|
+
if (!isCardPaymentEnabled()) {
|
|
87
|
+
return text("Card payments are temporarily unavailable. Use wallet_setup({ action: \"create\" }) for a crypto wallet instead.");
|
|
88
|
+
}
|
|
69
89
|
const existing = getCardConfig();
|
|
70
90
|
if (existing) {
|
|
71
91
|
return text(`Card already connected: ${existing.brand} ****${existing.last4}\n\nTo replace it, remove the current card first.`);
|
|
@@ -257,8 +277,8 @@ export function registerWalletTools(server) {
|
|
|
257
277
|
}
|
|
258
278
|
});
|
|
259
279
|
// ── wallet_set_policy (NEW) ─────────────────────────────────────
|
|
260
|
-
server.tool("wallet_set_policy", "Set spending limits on a wallet to control agent costs. Limits reset daily.", {
|
|
261
|
-
wallet_id: z.string().describe("Wallet ID to set policy on"),
|
|
280
|
+
server.tool("wallet_set_policy", "Set client-side spending limits on a wallet to control agent costs in this MCP client. Limits reset daily.", {
|
|
281
|
+
wallet_id: z.string().describe("Wallet ID to set local policy on"),
|
|
262
282
|
max_per_tx: z
|
|
263
283
|
.number()
|
|
264
284
|
.positive()
|
|
@@ -306,10 +326,10 @@ export function registerWalletTools(server) {
|
|
|
306
326
|
policies.push(`Require confirmation above: $${nextPolicy.requireConfirmationAboveUsd.toFixed(2)}`);
|
|
307
327
|
}
|
|
308
328
|
return text([
|
|
309
|
-
`
|
|
329
|
+
`Local spending policy set for wallet "${wallet_id}":`,
|
|
310
330
|
...policies.map((p) => ` ${p}`),
|
|
311
331
|
"",
|
|
312
|
-
"Policy
|
|
332
|
+
"Policy is stored in this MCP client's local config and enforced on future transactions from this wallet on this client.",
|
|
313
333
|
].join("\n"));
|
|
314
334
|
});
|
|
315
335
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentwonderland/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.27",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "MCP server for the Agent Wonderland AI agent marketplace",
|
|
6
6
|
"bin": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
26
26
|
"@solana/spl-token": "^0.4.14",
|
|
27
27
|
"@solana/web3.js": "^1.98.4",
|
|
28
|
-
"mppx": "^0.
|
|
28
|
+
"mppx": "^0.5.10",
|
|
29
29
|
"qrcode": "^1.5.4",
|
|
30
30
|
"viem": "^2.47.6",
|
|
31
31
|
"zod": "^3.24.0"
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
mockGetApiUrl,
|
|
5
|
+
mockGetApiKey,
|
|
6
|
+
mockGetPaymentFetch,
|
|
7
|
+
mockEnsureConsumerPrincipalForMethod,
|
|
8
|
+
mockGetConsumerPrincipalForMethod,
|
|
9
|
+
mockGetBaseRebatePrincipal,
|
|
10
|
+
mockPaymentFetch,
|
|
11
|
+
} = vi.hoisted(() => ({
|
|
12
|
+
mockGetApiUrl: vi.fn(),
|
|
13
|
+
mockGetApiKey: vi.fn(),
|
|
14
|
+
mockGetPaymentFetch: vi.fn(),
|
|
15
|
+
mockEnsureConsumerPrincipalForMethod: vi.fn(),
|
|
16
|
+
mockGetConsumerPrincipalForMethod: vi.fn(),
|
|
17
|
+
mockGetBaseRebatePrincipal: vi.fn(),
|
|
18
|
+
mockPaymentFetch: vi.fn(),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
vi.mock("../config.js", () => ({
|
|
22
|
+
getApiUrl: () => mockGetApiUrl(),
|
|
23
|
+
getApiKey: () => mockGetApiKey(),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
vi.mock("../payments.js", () => ({
|
|
27
|
+
getPaymentFetch: (...args: unknown[]) => mockGetPaymentFetch(...args),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
vi.mock("../principal.js", () => ({
|
|
31
|
+
ensureConsumerPrincipal: vi.fn(),
|
|
32
|
+
ensureConsumerPrincipalForMethod: (...args: unknown[]) => mockEnsureConsumerPrincipalForMethod(...args),
|
|
33
|
+
getConsumerPrincipal: vi.fn(),
|
|
34
|
+
getConsumerPrincipalForMethod: (...args: unknown[]) => mockGetConsumerPrincipalForMethod(...args),
|
|
35
|
+
getBaseRebatePrincipal: (...args: unknown[]) => mockGetBaseRebatePrincipal(...args),
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
describe("api-client headers", () => {
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
vi.clearAllMocks();
|
|
41
|
+
mockGetApiUrl.mockReturnValue("https://api.agentwonderland.test");
|
|
42
|
+
mockGetApiKey.mockReturnValue(null);
|
|
43
|
+
mockGetPaymentFetch.mockResolvedValue(mockPaymentFetch);
|
|
44
|
+
mockEnsureConsumerPrincipalForMethod.mockResolvedValue(
|
|
45
|
+
"did:pkh:solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF",
|
|
46
|
+
);
|
|
47
|
+
mockGetConsumerPrincipalForMethod.mockResolvedValue(
|
|
48
|
+
"did:pkh:eip155:8453:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
49
|
+
);
|
|
50
|
+
mockGetBaseRebatePrincipal.mockResolvedValue(
|
|
51
|
+
"did:pkh:eip155:8453:0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
|
52
|
+
);
|
|
53
|
+
mockPaymentFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), {
|
|
54
|
+
status: 200,
|
|
55
|
+
headers: { "content-type": "application/json" },
|
|
56
|
+
}));
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("includes the Base rebate principal alongside the method-specific consumer principal", async () => {
|
|
60
|
+
const { apiPostWithPayment } = await import("../api-client.js");
|
|
61
|
+
|
|
62
|
+
await apiPostWithPayment("/agents/agent-1/run", { input: { text: "hello" } }, "solana");
|
|
63
|
+
|
|
64
|
+
expect(mockPaymentFetch).toHaveBeenCalledWith(
|
|
65
|
+
"https://api.agentwonderland.test/agents/agent-1/run",
|
|
66
|
+
expect.objectContaining({
|
|
67
|
+
headers: expect.objectContaining({
|
|
68
|
+
"Content-Type": "application/json",
|
|
69
|
+
Accept: "application/json",
|
|
70
|
+
"X-AW-Consumer-Principal":
|
|
71
|
+
"did:pkh:solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF",
|
|
72
|
+
"X-AW-Rebate-Principal":
|
|
73
|
+
"did:pkh:eip155:8453:0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
|
74
|
+
}),
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -14,4 +14,16 @@ describe("formatRunResult", () => {
|
|
|
14
14
|
expect(output).toContain("Covered by credit pack (pack-123)");
|
|
15
15
|
expect(output).not.toContain("Paid:");
|
|
16
16
|
});
|
|
17
|
+
|
|
18
|
+
it("formats string-valued settled amounts from job lookups", () => {
|
|
19
|
+
const output = formatRunResult({
|
|
20
|
+
agent_name: "Async Analyzer",
|
|
21
|
+
status: "completed",
|
|
22
|
+
settled_amount: "0.100000",
|
|
23
|
+
job_id: "job-123",
|
|
24
|
+
}, { paymentMethod: "card" });
|
|
25
|
+
|
|
26
|
+
expect(output).toContain("Paid: $0.10 via card");
|
|
27
|
+
expect(output).toContain("Job ID: job-123");
|
|
28
|
+
});
|
|
17
29
|
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const state = vi.hoisted(() => ({
|
|
4
|
+
apiGet: vi.fn(),
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
vi.mock("../api-client.js", () => ({
|
|
8
|
+
apiGet: (...args: unknown[]) => state.apiGet(...args),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
describe("getCreditPackInventory", () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
vi.resetModules();
|
|
14
|
+
state.apiGet.mockReset();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("requests inventory for the selected payment method principal", async () => {
|
|
18
|
+
state.apiGet.mockResolvedValueOnce({
|
|
19
|
+
consumer_principal: "did:pkh:solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF",
|
|
20
|
+
offers: [],
|
|
21
|
+
balances: [],
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const { getCreditPackInventory } = await import("../passes.js");
|
|
25
|
+
const result = await getCreditPackInventory("agent-1", "solana");
|
|
26
|
+
|
|
27
|
+
expect(result?.consumer_principal).toContain("did:pkh:solana:");
|
|
28
|
+
expect(state.apiGet).toHaveBeenCalledWith("/agents/agent-1/credit-packs", {
|
|
29
|
+
ensureConsumerPrincipal: true,
|
|
30
|
+
principalMethod: "solana",
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -16,7 +16,7 @@ let currentResolvedMethod: { wallet: any; chain: string } | null = null;
|
|
|
16
16
|
const createdFetches = [vi.fn(), vi.fn(), vi.fn(), vi.fn()];
|
|
17
17
|
const mockMppxCreate = vi.fn();
|
|
18
18
|
const mockStripe = vi.fn((opts: unknown) => opts);
|
|
19
|
-
const
|
|
19
|
+
const mockTempoChargeClient = vi.fn((..._args: unknown[]) => "tempo_method");
|
|
20
20
|
const mockBaseChargeClient = vi.fn((..._args: unknown[]) => "base_method");
|
|
21
21
|
|
|
22
22
|
vi.mock("../config.js", () => ({
|
|
@@ -33,7 +33,10 @@ vi.mock("mppx/client", () => ({
|
|
|
33
33
|
create: (config: unknown) => mockMppxCreate(config),
|
|
34
34
|
},
|
|
35
35
|
stripe: (config: unknown) => mockStripe(config),
|
|
36
|
-
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
vi.mock("../tempo-charge.js", () => ({
|
|
39
|
+
tempoChargeClient: (config: unknown) => mockTempoChargeClient(config),
|
|
37
40
|
}));
|
|
38
41
|
|
|
39
42
|
vi.mock("../base-charge.js", () => ({
|
|
@@ -74,6 +77,14 @@ describe("payment method initialization", () => {
|
|
|
74
77
|
|
|
75
78
|
expect(firstFetch).not.toBe(secondFetch);
|
|
76
79
|
expect(mockMppxCreate).toHaveBeenCalledTimes(2);
|
|
80
|
+
expect(mockMppxCreate).toHaveBeenNthCalledWith(
|
|
81
|
+
1,
|
|
82
|
+
expect.objectContaining({ polyfill: false }),
|
|
83
|
+
);
|
|
84
|
+
expect(mockMppxCreate).toHaveBeenNthCalledWith(
|
|
85
|
+
2,
|
|
86
|
+
expect.objectContaining({ polyfill: false }),
|
|
87
|
+
);
|
|
77
88
|
});
|
|
78
89
|
|
|
79
90
|
it("initializes only the Base method when base is requested", async () => {
|
|
@@ -92,9 +103,9 @@ describe("payment method initialization", () => {
|
|
|
92
103
|
await getPaymentFetch("base");
|
|
93
104
|
|
|
94
105
|
expect(mockBaseChargeClient).toHaveBeenCalledTimes(1);
|
|
95
|
-
expect(
|
|
106
|
+
expect(mockTempoChargeClient).not.toHaveBeenCalled();
|
|
96
107
|
expect(mockMppxCreate).toHaveBeenCalledWith(
|
|
97
|
-
expect.objectContaining({ methods: ["base_method"] }),
|
|
108
|
+
expect.objectContaining({ methods: ["base_method"], polyfill: false }),
|
|
98
109
|
);
|
|
99
110
|
});
|
|
100
111
|
|
|
@@ -113,10 +124,10 @@ describe("payment method initialization", () => {
|
|
|
113
124
|
const { getPaymentFetch } = await import("../payments.js");
|
|
114
125
|
await getPaymentFetch("tempo");
|
|
115
126
|
|
|
116
|
-
expect(
|
|
127
|
+
expect(mockTempoChargeClient).toHaveBeenCalledTimes(1);
|
|
117
128
|
expect(mockBaseChargeClient).not.toHaveBeenCalled();
|
|
118
129
|
expect(mockMppxCreate).toHaveBeenCalledWith(
|
|
119
|
-
expect.objectContaining({ methods: ["tempo_method"] }),
|
|
130
|
+
expect.objectContaining({ methods: ["tempo_method"], polyfill: false }),
|
|
120
131
|
);
|
|
121
132
|
});
|
|
122
133
|
});
|
|
@@ -23,15 +23,26 @@ vi.mock("../config.js", () => ({
|
|
|
23
23
|
},
|
|
24
24
|
getDefaultWallet: () => state.wallets[0],
|
|
25
25
|
getWallets: () => state.wallets,
|
|
26
|
+
resolveWalletAndChain: (method: string) => {
|
|
27
|
+
for (const wallet of state.wallets) {
|
|
28
|
+
if (wallet.id === method) {
|
|
29
|
+
return { wallet, chain: wallet.defaultChain ?? wallet.chains[0] };
|
|
30
|
+
}
|
|
31
|
+
if (wallet.chains.includes(method)) {
|
|
32
|
+
return { wallet, chain: method };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
},
|
|
26
37
|
}));
|
|
27
38
|
|
|
28
39
|
vi.mock("../payments.js", () => ({
|
|
29
|
-
getWalletAddress: async (
|
|
30
|
-
if (
|
|
31
|
-
return state.addresses[
|
|
40
|
+
getWalletAddress: async (method?: string) => {
|
|
41
|
+
if (method && method in state.addresses) {
|
|
42
|
+
return state.addresses[method] ?? null;
|
|
32
43
|
}
|
|
33
44
|
|
|
34
|
-
const wallet = state.wallets.find((entry) => entry.id ===
|
|
45
|
+
const wallet = state.wallets.find((entry) => entry.id === method);
|
|
35
46
|
if (wallet?.key) {
|
|
36
47
|
const { privateKeyToAccount } = await import("viem/accounts");
|
|
37
48
|
return privateKeyToAccount(wallet.key as `0x${string}`).address;
|
|
@@ -84,4 +95,38 @@ describe("consumer principal helpers", () => {
|
|
|
84
95
|
expect(state.addedWallets).toHaveLength(1);
|
|
85
96
|
expect(state.addedWallets[0]?.chains).toEqual(["tempo", "base"]);
|
|
86
97
|
});
|
|
98
|
+
|
|
99
|
+
it("derives a Solana principal when the selected payment method is solana", async () => {
|
|
100
|
+
state.wallets = [
|
|
101
|
+
{ id: "aw-main", keyType: "ows", owsWalletId: "ows-main", chains: ["tempo", "base", "solana"], defaultChain: "tempo" },
|
|
102
|
+
];
|
|
103
|
+
state.addresses = {
|
|
104
|
+
solana: "42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF",
|
|
105
|
+
tempo: "0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
|
|
106
|
+
base: "0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const { getConsumerPrincipalForMethod } = await import("../principal.js");
|
|
110
|
+
const principal = await getConsumerPrincipalForMethod("solana");
|
|
111
|
+
|
|
112
|
+
expect(principal).toBe(
|
|
113
|
+
"did:pkh:solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF",
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("returns the Base rebate principal when an EVM wallet is available", async () => {
|
|
118
|
+
state.wallets = [
|
|
119
|
+
{ id: "aw-main", keyType: "ows", owsWalletId: "ows-main", chains: ["tempo", "base", "solana"], defaultChain: "tempo" },
|
|
120
|
+
];
|
|
121
|
+
state.addresses = {
|
|
122
|
+
solana: "42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF",
|
|
123
|
+
tempo: "0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
|
|
124
|
+
base: "0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const { getBaseRebatePrincipal } = await import("../principal.js");
|
|
128
|
+
const principal = await getBaseRebatePrincipal();
|
|
129
|
+
|
|
130
|
+
expect(principal).toBe("did:pkh:eip155:8453:0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
|
|
131
|
+
});
|
|
87
132
|
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { PublicKey } from "@solana/web3.js";
|
|
2
|
+
import { TOKEN_PROGRAM_ID, getAssociatedTokenAddressSync } from "@solana/spl-token";
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { SOLANA_USDC_MINT } from "../solana-charge.js";
|
|
5
|
+
|
|
6
|
+
describe("resolveRecipientTokenAccount", () => {
|
|
7
|
+
it("uses the recipient directly when Stripe gives us a token account", async () => {
|
|
8
|
+
const { resolveRecipientTokenAccount } = await import("../solana-charge.js");
|
|
9
|
+
const connection = {
|
|
10
|
+
getParsedAccountInfo: vi.fn().mockResolvedValue({
|
|
11
|
+
value: { owner: TOKEN_PROGRAM_ID },
|
|
12
|
+
}),
|
|
13
|
+
} as any;
|
|
14
|
+
const mint = new PublicKey(SOLANA_USDC_MINT);
|
|
15
|
+
const recipient = new PublicKey("42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF");
|
|
16
|
+
|
|
17
|
+
const resolved = await resolveRecipientTokenAccount(connection, mint, recipient);
|
|
18
|
+
|
|
19
|
+
expect(resolved.tokenAccount.toBase58()).toBe(recipient.toBase58());
|
|
20
|
+
expect(resolved.needsCreateAssociatedAccount).toBe(false);
|
|
21
|
+
expect(connection.getParsedAccountInfo).toHaveBeenCalledTimes(1);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("reuses the associated token account when it already exists", async () => {
|
|
25
|
+
const { resolveRecipientTokenAccount } = await import("../solana-charge.js");
|
|
26
|
+
const connection = {
|
|
27
|
+
getParsedAccountInfo: vi.fn()
|
|
28
|
+
.mockResolvedValueOnce({ value: null })
|
|
29
|
+
.mockResolvedValueOnce({ value: { owner: TOKEN_PROGRAM_ID } }),
|
|
30
|
+
} as any;
|
|
31
|
+
const mint = new PublicKey(SOLANA_USDC_MINT);
|
|
32
|
+
const recipient = new PublicKey("G7kW6ZPMAK9v11AaVkK9FHed2Gd4WYwvCbanXUmK9WtS");
|
|
33
|
+
const expectedAta = getAssociatedTokenAddressSync(mint, recipient, false, TOKEN_PROGRAM_ID);
|
|
34
|
+
|
|
35
|
+
const resolved = await resolveRecipientTokenAccount(connection, mint, recipient);
|
|
36
|
+
|
|
37
|
+
expect(resolved.tokenAccount.toBase58()).toBe(expectedAta.toBase58());
|
|
38
|
+
expect(resolved.needsCreateAssociatedAccount).toBe(false);
|
|
39
|
+
expect(connection.getParsedAccountInfo).toHaveBeenCalledTimes(2);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("marks the associated token account for creation when neither account exists", async () => {
|
|
43
|
+
const { resolveRecipientTokenAccount } = await import("../solana-charge.js");
|
|
44
|
+
const connection = {
|
|
45
|
+
getParsedAccountInfo: vi.fn()
|
|
46
|
+
.mockResolvedValueOnce({ value: null })
|
|
47
|
+
.mockResolvedValueOnce({ value: null }),
|
|
48
|
+
} as any;
|
|
49
|
+
const mint = new PublicKey(SOLANA_USDC_MINT);
|
|
50
|
+
const recipient = new PublicKey("G7kW6ZPMAK9v11AaVkK9FHed2Gd4WYwvCbanXUmK9WtS");
|
|
51
|
+
const expectedAta = getAssociatedTokenAddressSync(mint, recipient, false, TOKEN_PROGRAM_ID);
|
|
52
|
+
|
|
53
|
+
const resolved = await resolveRecipientTokenAccount(connection, mint, recipient);
|
|
54
|
+
|
|
55
|
+
expect(resolved.tokenAccount.toBase58()).toBe(expectedAta.toBase58());
|
|
56
|
+
expect(resolved.needsCreateAssociatedAccount).toBe(true);
|
|
57
|
+
expect(connection.getParsedAccountInfo).toHaveBeenCalledTimes(2);
|
|
58
|
+
});
|
|
59
|
+
});
|
package/src/core/api-client.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { getApiUrl, getApiKey } from "./config.js";
|
|
2
2
|
import { getPaymentFetch } from "./payments.js";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
getBaseRebatePrincipal,
|
|
5
|
+
ensureConsumerPrincipal,
|
|
6
|
+
ensureConsumerPrincipalForMethod,
|
|
7
|
+
getConsumerPrincipal,
|
|
8
|
+
getConsumerPrincipalForMethod,
|
|
9
|
+
} from "./principal.js";
|
|
4
10
|
|
|
5
11
|
// ── Error class ────────────────────────────────────────────────────
|
|
6
12
|
|
|
@@ -19,6 +25,7 @@ export class ApiError extends Error {
|
|
|
19
25
|
|
|
20
26
|
interface RequestOptions {
|
|
21
27
|
ensureConsumerPrincipal?: boolean;
|
|
28
|
+
principalMethod?: string;
|
|
22
29
|
extraHeaders?: Record<string, string>;
|
|
23
30
|
}
|
|
24
31
|
|
|
@@ -34,12 +41,17 @@ async function buildHeaders(options?: RequestOptions): Promise<Record<string, st
|
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
const principal = options?.ensureConsumerPrincipal
|
|
37
|
-
? await
|
|
38
|
-
: await
|
|
44
|
+
? await ensureConsumerPrincipalForMethod(options?.principalMethod)
|
|
45
|
+
: await getConsumerPrincipalForMethod(options?.principalMethod);
|
|
39
46
|
if (principal) {
|
|
40
47
|
headers["X-AW-Consumer-Principal"] = principal;
|
|
41
48
|
}
|
|
42
49
|
|
|
50
|
+
const rebatePrincipal = await getBaseRebatePrincipal();
|
|
51
|
+
if (rebatePrincipal) {
|
|
52
|
+
headers["X-AW-Rebate-Principal"] = rebatePrincipal;
|
|
53
|
+
}
|
|
54
|
+
|
|
43
55
|
if (options?.extraHeaders) {
|
|
44
56
|
Object.assign(headers, options.extraHeaders);
|
|
45
57
|
}
|
|
@@ -141,6 +153,7 @@ export async function apiPostWithPayment<T>(
|
|
|
141
153
|
method: "POST",
|
|
142
154
|
headers: await buildHeaders({
|
|
143
155
|
ensureConsumerPrincipal: true,
|
|
156
|
+
principalMethod: payWith,
|
|
144
157
|
...options,
|
|
145
158
|
}),
|
|
146
159
|
body: JSON.stringify(body),
|