@agentwonderland/mcp 0.1.50 → 0.1.52
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.js +6 -2
- package/dist/core/__tests__/payments.test.js +23 -2
- package/dist/core/__tests__/principal.test.js +25 -0
- package/dist/core/api-client.js +4 -2
- package/dist/core/config.d.ts +22 -0
- package/dist/core/config.js +20 -0
- package/dist/core/link-cli.js +97 -25
- package/dist/core/payments.js +20 -6
- package/dist/core/principal.d.ts +1 -0
- package/dist/core/principal.js +49 -1
- package/dist/core/version.d.ts +1 -1
- package/dist/core/version.js +1 -1
- package/dist/index.js +3 -0
- package/dist/tools/__tests__/rebates.test.d.ts +1 -0
- package/dist/tools/__tests__/rebates.test.js +72 -0
- package/dist/tools/__tests__/wallet.test.js +4 -0
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.js +1 -0
- package/dist/tools/observability.d.ts +2 -0
- package/dist/tools/observability.js +20 -0
- package/dist/tools/rebates.d.ts +2 -0
- package/dist/tools/rebates.js +51 -0
- package/dist/tools/wallet.js +11 -5
- package/package.json +1 -1
- package/src/core/__tests__/api-client.test.ts +8 -1
- package/src/core/__tests__/payments.test.ts +36 -2
- package/src/core/__tests__/principal.test.ts +31 -0
- package/src/core/api-client.ts +4 -2
- package/src/core/config.ts +46 -0
- package/src/core/link-cli.ts +114 -28
- package/src/core/payments.ts +21 -6
- package/src/core/principal.ts +54 -1
- package/src/core/version.ts +1 -1
- package/src/index.ts +3 -0
- package/src/tools/__tests__/rebates.test.ts +91 -0
- package/src/tools/__tests__/wallet.test.ts +10 -0
- package/src/tools/index.ts +1 -0
- package/src/tools/rebates.ts +102 -0
- package/src/tools/wallet.ts +11 -4
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
|
|
2
|
+
import { MCP_PACKAGE_VERSION } from "../version.js";
|
|
3
|
+
const { mockGetApiUrl, mockGetApiKey, mockGetPaymentFetch, mockEnsureConsumerPrincipalForMethod, mockEnsureBaseRebatePrincipal, mockGetConsumerPrincipalForMethod, mockGetBaseRebatePrincipal, mockPaymentFetch, } = vi.hoisted(() => ({
|
|
3
4
|
mockGetApiUrl: vi.fn(),
|
|
4
5
|
mockGetApiKey: vi.fn(),
|
|
5
6
|
mockGetPaymentFetch: vi.fn(),
|
|
6
7
|
mockEnsureConsumerPrincipalForMethod: vi.fn(),
|
|
8
|
+
mockEnsureBaseRebatePrincipal: vi.fn(),
|
|
7
9
|
mockGetConsumerPrincipalForMethod: vi.fn(),
|
|
8
10
|
mockGetBaseRebatePrincipal: vi.fn(),
|
|
9
11
|
mockPaymentFetch: vi.fn(),
|
|
@@ -17,6 +19,7 @@ vi.mock("../payments.js", () => ({
|
|
|
17
19
|
}));
|
|
18
20
|
vi.mock("../principal.js", () => ({
|
|
19
21
|
ensureConsumerPrincipal: vi.fn(),
|
|
22
|
+
ensureBaseRebatePrincipal: (...args) => mockEnsureBaseRebatePrincipal(...args),
|
|
20
23
|
ensureConsumerPrincipalForMethod: (...args) => mockEnsureConsumerPrincipalForMethod(...args),
|
|
21
24
|
getConsumerPrincipal: vi.fn(),
|
|
22
25
|
getConsumerPrincipalForMethod: (...args) => mockGetConsumerPrincipalForMethod(...args),
|
|
@@ -31,6 +34,7 @@ describe("api-client headers", () => {
|
|
|
31
34
|
mockEnsureConsumerPrincipalForMethod.mockResolvedValue("did:pkh:solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF");
|
|
32
35
|
mockGetConsumerPrincipalForMethod.mockResolvedValue("did:pkh:eip155:8453:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
|
|
33
36
|
mockGetBaseRebatePrincipal.mockResolvedValue("did:pkh:eip155:8453:0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
|
|
37
|
+
mockEnsureBaseRebatePrincipal.mockResolvedValue("did:pkh:eip155:8453:0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
|
|
34
38
|
mockPaymentFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), {
|
|
35
39
|
status: 200,
|
|
36
40
|
headers: { "content-type": "application/json" },
|
|
@@ -46,7 +50,7 @@ describe("api-client headers", () => {
|
|
|
46
50
|
"X-AW-Consumer-Principal": "did:pkh:solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF",
|
|
47
51
|
"X-AW-Rebate-Principal": "did:pkh:eip155:8453:0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
|
48
52
|
"X-AW-Surface": "mcp",
|
|
49
|
-
"X-AW-MCP-Version":
|
|
53
|
+
"X-AW-MCP-Version": MCP_PACKAGE_VERSION,
|
|
50
54
|
"X-AW-MCP-Tool": "run_agent",
|
|
51
55
|
"X-AW-MCP-Action": "execute",
|
|
52
56
|
}),
|
|
@@ -21,6 +21,7 @@ vi.mock("../config.js", () => ({
|
|
|
21
21
|
getApiUrl: () => "http://api.test",
|
|
22
22
|
getCardConfig: () => currentCard,
|
|
23
23
|
getLinkConfig: () => currentLink,
|
|
24
|
+
getLinkCooldown: () => null,
|
|
24
25
|
getConfig: () => ({ defaultPaymentMethod: currentDefaultPaymentMethod }),
|
|
25
26
|
getDefaultWallet: () => currentDefaultWallet,
|
|
26
27
|
getWallets: () => currentWallets,
|
|
@@ -124,15 +125,35 @@ describe("payment method initialization", () => {
|
|
|
124
125
|
networkId: "profile_test",
|
|
125
126
|
});
|
|
126
127
|
expect(mockCreateLinkSharedPaymentToken).toHaveBeenCalledWith(expect.objectContaining({
|
|
127
|
-
amount: "
|
|
128
|
+
amount: "25",
|
|
128
129
|
currency: "usd",
|
|
129
130
|
networkId: "profile_test",
|
|
130
131
|
paymentMethodId: "csmrpd_link_123",
|
|
131
132
|
}));
|
|
132
133
|
const linkTokenRequest = mockCreateLinkSharedPaymentToken.mock.calls[0]?.[0];
|
|
133
|
-
expect(linkTokenRequest?.context).toContain("up to USD
|
|
134
|
+
expect(linkTokenRequest?.context).toContain("up to USD 0.25");
|
|
134
135
|
expect(linkTokenRequest?.context).toContain("quoted at USD 0.25");
|
|
135
136
|
});
|
|
137
|
+
it("uses an explicit Link approval override when configured", async () => {
|
|
138
|
+
process.env.AGENTWONDERLAND_LINK_APPROVAL_LIMIT_CENTS = "2000";
|
|
139
|
+
currentLink = {
|
|
140
|
+
paymentMethodId: "csmrpd_link_123",
|
|
141
|
+
label: "Visa ****4242",
|
|
142
|
+
};
|
|
143
|
+
currentDefaultPaymentMethod = "link";
|
|
144
|
+
const { getPaymentFetch } = await import("../payments.js");
|
|
145
|
+
await getPaymentFetch("link");
|
|
146
|
+
const stripeConfig = mockStripe.mock.calls[0]?.[0];
|
|
147
|
+
await stripeConfig.createToken({
|
|
148
|
+
amount: "25",
|
|
149
|
+
currency: "usd",
|
|
150
|
+
expiresAt: 1778290000,
|
|
151
|
+
networkId: "profile_test",
|
|
152
|
+
});
|
|
153
|
+
expect(mockCreateLinkSharedPaymentToken).toHaveBeenCalledWith(expect.objectContaining({
|
|
154
|
+
amount: "2000",
|
|
155
|
+
}));
|
|
156
|
+
});
|
|
136
157
|
it("allows a low env override while never approving below the quoted amount", async () => {
|
|
137
158
|
process.env.AGENTWONDERLAND_LINK_APPROVAL_LIMIT_CENTS = "10";
|
|
138
159
|
currentLink = {
|
|
@@ -106,6 +106,19 @@ describe("consumer principal helpers", () => {
|
|
|
106
106
|
expect(principal).toMatch(/^did:pkh:eip155:8453:0x[a-f0-9]{40}$/);
|
|
107
107
|
expect(state.addedWallets).toHaveLength(1);
|
|
108
108
|
});
|
|
109
|
+
it("creates an EVM identity for Link payments when only a Solana wallet exists", async () => {
|
|
110
|
+
state.wallets = [
|
|
111
|
+
{ id: "sol-wallet", keyType: "ows", owsWalletId: "ows-sol", chains: ["solana"], defaultChain: "solana" },
|
|
112
|
+
];
|
|
113
|
+
state.addresses = {
|
|
114
|
+
"sol-wallet": "42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF",
|
|
115
|
+
};
|
|
116
|
+
const { ensureConsumerPrincipalForMethod } = await import("../principal.js");
|
|
117
|
+
const principal = await ensureConsumerPrincipalForMethod("link");
|
|
118
|
+
expect(principal).toMatch(/^did:pkh:eip155:8453:0x[a-f0-9]{40}$/);
|
|
119
|
+
expect(state.addedWallets).toHaveLength(1);
|
|
120
|
+
expect(state.addedWallets[0]?.chains).toEqual(["tempo", "base"]);
|
|
121
|
+
});
|
|
109
122
|
it("returns the Base rebate principal when an EVM wallet is available", async () => {
|
|
110
123
|
state.wallets = [
|
|
111
124
|
{ id: "aw-main", keyType: "ows", owsWalletId: "ows-main", chains: ["tempo", "base", "solana"], defaultChain: "tempo" },
|
|
@@ -119,4 +132,16 @@ describe("consumer principal helpers", () => {
|
|
|
119
132
|
const principal = await getBaseRebatePrincipal();
|
|
120
133
|
expect(principal).toBe("did:pkh:eip155:8453:0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
|
|
121
134
|
});
|
|
135
|
+
it("ensures a Base rebate principal even when the default wallet is Solana-only", async () => {
|
|
136
|
+
state.wallets = [
|
|
137
|
+
{ id: "sol-wallet", keyType: "ows", owsWalletId: "ows-sol", chains: ["solana"], defaultChain: "solana" },
|
|
138
|
+
];
|
|
139
|
+
state.addresses = {
|
|
140
|
+
"sol-wallet": "42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF",
|
|
141
|
+
};
|
|
142
|
+
const { ensureBaseRebatePrincipal } = await import("../principal.js");
|
|
143
|
+
const principal = await ensureBaseRebatePrincipal();
|
|
144
|
+
expect(principal).toMatch(/^did:pkh:eip155:8453:0x[a-f0-9]{40}$/);
|
|
145
|
+
expect(state.addedWallets).toHaveLength(1);
|
|
146
|
+
});
|
|
122
147
|
});
|
package/dist/core/api-client.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getApiUrl, getApiKey } from "./config.js";
|
|
2
2
|
import { getPaymentFetch } from "./payments.js";
|
|
3
|
-
import { getBaseRebatePrincipal, ensureConsumerPrincipalForMethod, getConsumerPrincipalForMethod, } from "./principal.js";
|
|
3
|
+
import { getBaseRebatePrincipal, ensureBaseRebatePrincipal, ensureConsumerPrincipalForMethod, getConsumerPrincipalForMethod, } from "./principal.js";
|
|
4
4
|
import { MCP_PACKAGE_VERSION } from "./version.js";
|
|
5
5
|
// ── Error class ────────────────────────────────────────────────────
|
|
6
6
|
export class ApiError extends Error {
|
|
@@ -49,7 +49,9 @@ async function buildHeaders(path, method, options) {
|
|
|
49
49
|
if (principal) {
|
|
50
50
|
headers["X-AW-Consumer-Principal"] = principal;
|
|
51
51
|
}
|
|
52
|
-
const rebatePrincipal =
|
|
52
|
+
const rebatePrincipal = options?.ensureConsumerPrincipal
|
|
53
|
+
? await ensureBaseRebatePrincipal()
|
|
54
|
+
: await getBaseRebatePrincipal();
|
|
53
55
|
if (rebatePrincipal) {
|
|
54
56
|
headers["X-AW-Rebate-Principal"] = rebatePrincipal;
|
|
55
57
|
}
|
package/dist/core/config.d.ts
CHANGED
|
@@ -22,6 +22,22 @@ export interface PendingLinkSetup {
|
|
|
22
22
|
phrase: string;
|
|
23
23
|
createdAt: string;
|
|
24
24
|
}
|
|
25
|
+
export interface PendingLinkSpendRequest {
|
|
26
|
+
id: string;
|
|
27
|
+
approvalUrl?: string;
|
|
28
|
+
amount: string;
|
|
29
|
+
currency: string;
|
|
30
|
+
context: string;
|
|
31
|
+
expiresAt: number;
|
|
32
|
+
networkId: string;
|
|
33
|
+
paymentMethodId: string;
|
|
34
|
+
createdAt: string;
|
|
35
|
+
}
|
|
36
|
+
export interface LinkCooldown {
|
|
37
|
+
reason: string;
|
|
38
|
+
createdAt: string;
|
|
39
|
+
blockedUntil: string;
|
|
40
|
+
}
|
|
25
41
|
export interface SpendPolicy {
|
|
26
42
|
maxPerTxUsd?: number;
|
|
27
43
|
maxPerDayUsd?: number;
|
|
@@ -43,6 +59,8 @@ export interface Config {
|
|
|
43
59
|
card: CardConfig | null;
|
|
44
60
|
link: LinkConfig | null;
|
|
45
61
|
pendingLinkSetup?: PendingLinkSetup | null;
|
|
62
|
+
pendingLinkSpendRequest?: PendingLinkSpendRequest | null;
|
|
63
|
+
linkCooldown?: LinkCooldown | null;
|
|
46
64
|
pendingCardSetupToken?: string | null;
|
|
47
65
|
favorites: string[];
|
|
48
66
|
/** Require user confirmation before spending. Default: true. Set false for headless/automated use. */
|
|
@@ -103,6 +121,10 @@ export declare function removeWallet(id: string): void;
|
|
|
103
121
|
export declare function getCardConfig(): CardConfig | null;
|
|
104
122
|
export declare function getLinkConfig(): LinkConfig | null;
|
|
105
123
|
export declare function getPendingLinkSetup(): PendingLinkSetup | null;
|
|
124
|
+
export declare function getPendingLinkSpendRequest(): PendingLinkSpendRequest | null;
|
|
125
|
+
export declare function setPendingLinkSpendRequest(pendingLinkSpendRequest: PendingLinkSpendRequest | null): void;
|
|
126
|
+
export declare function getLinkCooldown(): LinkCooldown | null;
|
|
127
|
+
export declare function setLinkCooldown(linkCooldown: LinkCooldown | null): void;
|
|
106
128
|
/**
|
|
107
129
|
* Save card configuration after setup.
|
|
108
130
|
*/
|
package/dist/core/config.js
CHANGED
|
@@ -38,6 +38,8 @@ function migrateIfNeeded(raw) {
|
|
|
38
38
|
card: raw.card ?? null,
|
|
39
39
|
link: raw.link ?? null,
|
|
40
40
|
pendingLinkSetup: raw.link ? null : (raw.pendingLinkSetup ?? null),
|
|
41
|
+
pendingLinkSpendRequest: raw.pendingLinkSpendRequest ?? null,
|
|
42
|
+
linkCooldown: raw.linkCooldown ?? null,
|
|
41
43
|
pendingCardSetupToken: r.pendingCardSetupToken ?? null,
|
|
42
44
|
favorites: r.favorites ?? [],
|
|
43
45
|
confirmBeforeSpend: r.confirmBeforeSpend !== false,
|
|
@@ -113,6 +115,8 @@ function migrateIfNeeded(raw) {
|
|
|
113
115
|
card,
|
|
114
116
|
link: raw.link ?? null,
|
|
115
117
|
pendingLinkSetup: raw.link ? null : (raw.pendingLinkSetup ?? null),
|
|
118
|
+
pendingLinkSpendRequest: raw.pendingLinkSpendRequest ?? null,
|
|
119
|
+
linkCooldown: raw.linkCooldown ?? null,
|
|
116
120
|
pendingCardSetupToken: null,
|
|
117
121
|
favorites: [],
|
|
118
122
|
confirmBeforeSpend: true,
|
|
@@ -138,6 +142,8 @@ export function getConfig() {
|
|
|
138
142
|
card: null,
|
|
139
143
|
link: null,
|
|
140
144
|
pendingLinkSetup: null,
|
|
145
|
+
pendingLinkSpendRequest: null,
|
|
146
|
+
linkCooldown: null,
|
|
141
147
|
pendingCardSetupToken: null,
|
|
142
148
|
favorites: [],
|
|
143
149
|
confirmBeforeSpend: true,
|
|
@@ -301,6 +307,18 @@ export function getLinkConfig() {
|
|
|
301
307
|
export function getPendingLinkSetup() {
|
|
302
308
|
return getConfig().pendingLinkSetup ?? null;
|
|
303
309
|
}
|
|
310
|
+
export function getPendingLinkSpendRequest() {
|
|
311
|
+
return getConfig().pendingLinkSpendRequest ?? null;
|
|
312
|
+
}
|
|
313
|
+
export function setPendingLinkSpendRequest(pendingLinkSpendRequest) {
|
|
314
|
+
saveConfig({ pendingLinkSpendRequest });
|
|
315
|
+
}
|
|
316
|
+
export function getLinkCooldown() {
|
|
317
|
+
return getConfig().linkCooldown ?? null;
|
|
318
|
+
}
|
|
319
|
+
export function setLinkCooldown(linkCooldown) {
|
|
320
|
+
saveConfig({ linkCooldown });
|
|
321
|
+
}
|
|
304
322
|
/**
|
|
305
323
|
* Save card configuration after setup.
|
|
306
324
|
*/
|
|
@@ -330,12 +348,14 @@ export function setLinkConfig(link) {
|
|
|
330
348
|
link,
|
|
331
349
|
defaultPaymentMethod: "link",
|
|
332
350
|
pendingLinkSetup: null,
|
|
351
|
+
pendingLinkSpendRequest: null,
|
|
333
352
|
});
|
|
334
353
|
}
|
|
335
354
|
else {
|
|
336
355
|
saveConfig({
|
|
337
356
|
link,
|
|
338
357
|
pendingLinkSetup: null,
|
|
358
|
+
pendingLinkSpendRequest: null,
|
|
339
359
|
defaultPaymentMethod: current.defaultPaymentMethod === "link"
|
|
340
360
|
? undefined
|
|
341
361
|
: current.defaultPaymentMethod,
|
package/dist/core/link-cli.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
|
+
import { getPendingLinkSpendRequest, setLinkCooldown, setPendingLinkSpendRequest, } from "./config.js";
|
|
3
4
|
const execFileAsync = promisify(execFile);
|
|
4
5
|
const LINK_CLI_PACKAGE = "@stripe/link-cli";
|
|
5
6
|
const LINK_CLI_TIMEOUT_MS = 10 * 60 * 1000;
|
|
@@ -92,6 +93,34 @@ function extractSpendRequestApproval(output) {
|
|
|
92
93
|
}
|
|
93
94
|
return null;
|
|
94
95
|
}
|
|
96
|
+
function isMatchingPendingSpendRequest(pending, params) {
|
|
97
|
+
if (!pending)
|
|
98
|
+
return false;
|
|
99
|
+
if (pending.amount !== params.amount)
|
|
100
|
+
return false;
|
|
101
|
+
if (pending.currency.toLowerCase() !== params.currency.toLowerCase())
|
|
102
|
+
return false;
|
|
103
|
+
if (pending.context !== params.context)
|
|
104
|
+
return false;
|
|
105
|
+
if (pending.networkId !== params.networkId)
|
|
106
|
+
return false;
|
|
107
|
+
if (pending.paymentMethodId !== params.paymentMethodId)
|
|
108
|
+
return false;
|
|
109
|
+
if (pending.expiresAt <= Math.floor(Date.now() / 1000))
|
|
110
|
+
return false;
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
function isProjectedSpendCapError(message) {
|
|
114
|
+
return /projected daily spend|projected spend|exceeds limit/i.test(message);
|
|
115
|
+
}
|
|
116
|
+
function recordLinkCooldown(reason) {
|
|
117
|
+
const now = new Date();
|
|
118
|
+
setLinkCooldown({
|
|
119
|
+
reason,
|
|
120
|
+
createdAt: now.toISOString(),
|
|
121
|
+
blockedUntil: new Date(now.getTime() + 24 * 60 * 60 * 1000).toISOString(),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
95
124
|
function normalizePaymentMethods(output) {
|
|
96
125
|
const values = Array.isArray(output)
|
|
97
126
|
? output
|
|
@@ -181,6 +210,22 @@ export async function listLinkPaymentMethods() {
|
|
|
181
210
|
return normalizePaymentMethods(output);
|
|
182
211
|
}
|
|
183
212
|
export async function createLinkSharedPaymentToken(params) {
|
|
213
|
+
const existing = getPendingLinkSpendRequest();
|
|
214
|
+
if (isMatchingPendingSpendRequest(existing, params)) {
|
|
215
|
+
try {
|
|
216
|
+
console.error(`Resuming pending Link approval: ${existing.id}`);
|
|
217
|
+
const spt = await retrieveSharedPaymentToken(existing.id, existing.approvalUrl);
|
|
218
|
+
setPendingLinkSpendRequest(null);
|
|
219
|
+
return spt;
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
223
|
+
if (!/denied|expired|not found|without a shared payment token|POLLING_TIMEOUT/i.test(message)) {
|
|
224
|
+
throw err;
|
|
225
|
+
}
|
|
226
|
+
setPendingLinkSpendRequest(null);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
184
229
|
const args = [
|
|
185
230
|
"spend-request",
|
|
186
231
|
"create",
|
|
@@ -207,6 +252,16 @@ export async function createLinkSharedPaymentToken(params) {
|
|
|
207
252
|
}
|
|
208
253
|
catch (err) {
|
|
209
254
|
const message = err instanceof Error ? err.message : String(err);
|
|
255
|
+
if (isProjectedSpendCapError(message)) {
|
|
256
|
+
recordLinkCooldown(message);
|
|
257
|
+
throw new Error([
|
|
258
|
+
"Link is temporarily blocked by Stripe's projected-spend cap.",
|
|
259
|
+
"Reauthing Link or switching cards in the same Link account will not fix this.",
|
|
260
|
+
"Use USDC for now, wait for the rolling Link window to clear, or ask Stripe to raise/clear the merchant projected-spend cap.",
|
|
261
|
+
"",
|
|
262
|
+
message,
|
|
263
|
+
].join("\n"));
|
|
264
|
+
}
|
|
210
265
|
if (/invalid network_id|could not retrieve merchant information/i.test(message)) {
|
|
211
266
|
throw new Error([
|
|
212
267
|
message,
|
|
@@ -219,41 +274,58 @@ export async function createLinkSharedPaymentToken(params) {
|
|
|
219
274
|
}
|
|
220
275
|
const spt = extractSharedPaymentToken(output);
|
|
221
276
|
if (spt) {
|
|
277
|
+
setPendingLinkSpendRequest(null);
|
|
222
278
|
return spt;
|
|
223
279
|
}
|
|
224
280
|
const approval = extractSpendRequestApproval(output);
|
|
225
281
|
if (approval?.id && approval.status === "pending_approval") {
|
|
282
|
+
setPendingLinkSpendRequest({
|
|
283
|
+
id: approval.id,
|
|
284
|
+
approvalUrl: approval.approvalUrl,
|
|
285
|
+
amount: params.amount,
|
|
286
|
+
currency: params.currency,
|
|
287
|
+
context: params.context,
|
|
288
|
+
expiresAt: params.expiresAt,
|
|
289
|
+
networkId: params.networkId,
|
|
290
|
+
paymentMethodId: params.paymentMethodId,
|
|
291
|
+
createdAt: new Date().toISOString(),
|
|
292
|
+
});
|
|
226
293
|
if (approval.approvalUrl) {
|
|
227
294
|
console.error(`Link approval required: ${approval.approvalUrl}`);
|
|
228
295
|
}
|
|
229
|
-
|
|
296
|
+
const retrievedSpt = await retrieveSharedPaymentToken(approval.id, approval.approvalUrl);
|
|
297
|
+
setPendingLinkSpendRequest(null);
|
|
298
|
+
return retrievedSpt;
|
|
299
|
+
}
|
|
300
|
+
{
|
|
301
|
+
throw new Error("Link spend request completed without a shared payment token in the CLI response.");
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
async function retrieveSharedPaymentToken(spendRequestId, approvalUrl) {
|
|
305
|
+
let retrieved = await runLinkCli([
|
|
306
|
+
"spend-request",
|
|
307
|
+
"retrieve",
|
|
308
|
+
spendRequestId,
|
|
309
|
+
"--interval",
|
|
310
|
+
"2",
|
|
311
|
+
"--max-attempts",
|
|
312
|
+
"150",
|
|
313
|
+
]);
|
|
314
|
+
let retrievedSpt = extractSharedPaymentToken(retrieved);
|
|
315
|
+
for (let attempt = 0; !retrievedSpt && attempt < 30; attempt += 1) {
|
|
316
|
+
await sleep(2_000);
|
|
317
|
+
retrieved = await runLinkCli([
|
|
230
318
|
"spend-request",
|
|
231
319
|
"retrieve",
|
|
232
|
-
|
|
233
|
-
"--interval",
|
|
234
|
-
"2",
|
|
235
|
-
"--max-attempts",
|
|
236
|
-
"150",
|
|
320
|
+
spendRequestId,
|
|
237
321
|
]);
|
|
238
|
-
|
|
239
|
-
for (let attempt = 0; !retrievedSpt && attempt < 30; attempt += 1) {
|
|
240
|
-
await sleep(2_000);
|
|
241
|
-
retrieved = await runLinkCli([
|
|
242
|
-
"spend-request",
|
|
243
|
-
"retrieve",
|
|
244
|
-
approval.id,
|
|
245
|
-
]);
|
|
246
|
-
retrievedSpt = extractSharedPaymentToken(retrieved);
|
|
247
|
-
}
|
|
248
|
-
if (retrievedSpt) {
|
|
249
|
-
return retrievedSpt;
|
|
250
|
-
}
|
|
251
|
-
throw new Error([
|
|
252
|
-
"Link spend request finished without a shared payment token.",
|
|
253
|
-
approval.approvalUrl ? `Approval URL: ${approval.approvalUrl}` : undefined,
|
|
254
|
-
].filter(Boolean).join("\n"));
|
|
322
|
+
retrievedSpt = extractSharedPaymentToken(retrieved);
|
|
255
323
|
}
|
|
256
|
-
{
|
|
257
|
-
|
|
324
|
+
if (retrievedSpt) {
|
|
325
|
+
return retrievedSpt;
|
|
258
326
|
}
|
|
327
|
+
throw new Error([
|
|
328
|
+
"Link spend request finished without a shared payment token.",
|
|
329
|
+
approvalUrl ? `Approval URL: ${approvalUrl}` : undefined,
|
|
330
|
+
].filter(Boolean).join("\n"));
|
|
259
331
|
}
|
package/dist/core/payments.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Users can configure multiple wallets with different chains and select
|
|
10
10
|
* which to use per-request via `--pay-with <wallet-id|chain|card>`.
|
|
11
11
|
*/
|
|
12
|
-
import { getConfig, getWallets, getDefaultWallet, getCardConfig, getLinkConfig, resolveWalletAndChain, getApiUrl, } from "./config.js";
|
|
12
|
+
import { getConfig, getWallets, getDefaultWallet, getCardConfig, getLinkConfig, getLinkCooldown, resolveWalletAndChain, getApiUrl, } from "./config.js";
|
|
13
13
|
// Feature flag: disable card payment for launch. Flip to true once Stripe
|
|
14
14
|
// approves the live SPT issuer. Config/card state is still persisted so this
|
|
15
15
|
// can be re-enabled without reconfiguring wallets.
|
|
@@ -26,7 +26,6 @@ const REGISTRY_METHOD_MAP = {
|
|
|
26
26
|
card: "stripe_card",
|
|
27
27
|
link: "stripe_card",
|
|
28
28
|
};
|
|
29
|
-
const DEFAULT_LINK_APPROVAL_LIMIT_CENTS = 2_000;
|
|
30
29
|
const ACCEPTED_PAYMENT_ALIASES = {
|
|
31
30
|
tempo: ["tempo_usdc", "tempo"],
|
|
32
31
|
base: ["base_usdc", "base"],
|
|
@@ -164,10 +163,24 @@ function formatMinorCurrencyAmount(currency, amount) {
|
|
|
164
163
|
function getLinkApprovalLimitAmount(actualAmount) {
|
|
165
164
|
const actualAmountCents = Number(actualAmount);
|
|
166
165
|
const configuredLimit = Number(process.env.AGENTWONDERLAND_LINK_APPROVAL_LIMIT_CENTS);
|
|
167
|
-
const
|
|
168
|
-
? Math.floor(configuredLimit)
|
|
169
|
-
:
|
|
170
|
-
return String(
|
|
166
|
+
const approvalLimit = Number.isFinite(configuredLimit) && configuredLimit > 0
|
|
167
|
+
? Math.max(actualAmountCents, Math.floor(configuredLimit))
|
|
168
|
+
: actualAmountCents;
|
|
169
|
+
return String(approvalLimit);
|
|
170
|
+
}
|
|
171
|
+
function assertLinkNotCoolingDown() {
|
|
172
|
+
const cooldown = getLinkCooldown();
|
|
173
|
+
if (!cooldown)
|
|
174
|
+
return;
|
|
175
|
+
const blockedUntil = Date.parse(cooldown.blockedUntil);
|
|
176
|
+
if (!Number.isFinite(blockedUntil) || blockedUntil <= Date.now())
|
|
177
|
+
return;
|
|
178
|
+
throw new Error([
|
|
179
|
+
"Link is temporarily blocked by Stripe's projected-spend cap.",
|
|
180
|
+
"Reauthing Link or switching cards in the same Link account will not fix this.",
|
|
181
|
+
`Try again after ${cooldown.blockedUntil}, use USDC, or ask Stripe to raise/clear the merchant projected-spend cap.`,
|
|
182
|
+
`Last Link error: ${cooldown.reason}`,
|
|
183
|
+
].join("\n"));
|
|
171
184
|
}
|
|
172
185
|
function buildLinkApprovalContext(params) {
|
|
173
186
|
const amountText = formatMinorCurrencyAmount(params.currency, params.amount);
|
|
@@ -188,6 +201,7 @@ async function initLink() {
|
|
|
188
201
|
methods: [stripe({
|
|
189
202
|
paymentMethod: linkConfig.paymentMethodId,
|
|
190
203
|
createToken: async (params) => {
|
|
204
|
+
assertLinkNotCoolingDown();
|
|
191
205
|
const approvalAmount = getLinkApprovalLimitAmount(params.amount);
|
|
192
206
|
console.error(`Requesting Link approval up to ${formatMinorCurrencyAmount(params.currency, approvalAmount)} ` +
|
|
193
207
|
`for this ${formatMinorCurrencyAmount(params.currency, params.amount)} Agent Wonderland run.`);
|
package/dist/core/principal.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export declare function getConsumerPrincipal(): Promise<string | null>;
|
|
2
2
|
export declare function getBaseRebatePrincipal(): Promise<string | null>;
|
|
3
|
+
export declare function ensureBaseRebatePrincipal(): Promise<string>;
|
|
3
4
|
export declare function getConsumerPrincipalForMethod(method?: string): Promise<string | null>;
|
|
4
5
|
export declare function ensureConsumerPrincipal(): Promise<string>;
|
|
5
6
|
export declare function ensureConsumerPrincipalForMethod(method?: string): Promise<string>;
|
package/dist/core/principal.js
CHANGED
|
@@ -59,6 +59,40 @@ async function createFallbackIdentityWallet() {
|
|
|
59
59
|
addWallet(entry);
|
|
60
60
|
return entry;
|
|
61
61
|
}
|
|
62
|
+
async function ensureEvmIdentityWallet() {
|
|
63
|
+
const existing = preferredWallets().find((wallet) => walletSupportsEvm(wallet));
|
|
64
|
+
if (existing)
|
|
65
|
+
return existing;
|
|
66
|
+
if (await isOwsAvailable()) {
|
|
67
|
+
const existingOwsWallets = await listOwsWallets();
|
|
68
|
+
const linked = existingOwsWallets[0];
|
|
69
|
+
if (linked) {
|
|
70
|
+
const entry = {
|
|
71
|
+
id: linked.name,
|
|
72
|
+
keyType: "ows",
|
|
73
|
+
owsWalletId: linked.id,
|
|
74
|
+
chains: ["tempo", "base"],
|
|
75
|
+
defaultChain: "base",
|
|
76
|
+
label: linked.name,
|
|
77
|
+
};
|
|
78
|
+
addWallet(entry);
|
|
79
|
+
return entry;
|
|
80
|
+
}
|
|
81
|
+
const walletName = `aw-identity-${Date.now()}`;
|
|
82
|
+
const created = await createOwsWallet(walletName, "evm");
|
|
83
|
+
const entry = {
|
|
84
|
+
id: walletName,
|
|
85
|
+
keyType: "ows",
|
|
86
|
+
owsWalletId: created.walletId,
|
|
87
|
+
chains: ["tempo", "base"],
|
|
88
|
+
defaultChain: "base",
|
|
89
|
+
label: AUTO_IDENTITY_LABEL,
|
|
90
|
+
};
|
|
91
|
+
addWallet(entry);
|
|
92
|
+
return entry;
|
|
93
|
+
}
|
|
94
|
+
return createFallbackIdentityWallet();
|
|
95
|
+
}
|
|
62
96
|
async function ensureIdentityWallet() {
|
|
63
97
|
const wallets = preferredWallets();
|
|
64
98
|
const existing = wallets.find((wallet) => walletSupportsEvm(wallet) || walletSupportsSolana(wallet));
|
|
@@ -105,9 +139,20 @@ export async function getConsumerPrincipal() {
|
|
|
105
139
|
export async function getBaseRebatePrincipal() {
|
|
106
140
|
return (await principalForChain("base")) ?? principalForChain("tempo");
|
|
107
141
|
}
|
|
142
|
+
export async function ensureBaseRebatePrincipal() {
|
|
143
|
+
const existing = await getBaseRebatePrincipal();
|
|
144
|
+
if (existing)
|
|
145
|
+
return existing;
|
|
146
|
+
const wallet = await ensureEvmIdentityWallet();
|
|
147
|
+
const principal = await walletPrincipal(wallet);
|
|
148
|
+
if (!principal || !principal.startsWith(`did:pkh:eip155:${BASE_CHAIN_ID}:`)) {
|
|
149
|
+
throw new Error("Could not derive a Base rebate principal from the configured identity wallet.");
|
|
150
|
+
}
|
|
151
|
+
return principal;
|
|
152
|
+
}
|
|
108
153
|
export async function getConsumerPrincipalForMethod(method) {
|
|
109
154
|
if (!method || method === "card" || method === "link") {
|
|
110
|
-
return
|
|
155
|
+
return getBaseRebatePrincipal();
|
|
111
156
|
}
|
|
112
157
|
const resolved = resolveWalletAndChain(method);
|
|
113
158
|
if (!resolved)
|
|
@@ -129,6 +174,9 @@ export async function ensureConsumerPrincipalForMethod(method) {
|
|
|
129
174
|
const existing = await getConsumerPrincipalForMethod(method);
|
|
130
175
|
if (existing)
|
|
131
176
|
return existing;
|
|
177
|
+
if (!method || method === "card" || method === "link") {
|
|
178
|
+
return ensureBaseRebatePrincipal();
|
|
179
|
+
}
|
|
132
180
|
if (method && method !== "card" && method !== "link") {
|
|
133
181
|
throw new Error(`Could not derive a consumer principal for payment method "${method}". ` +
|
|
134
182
|
"Check wallet_status and confirm that chain is configured for the active wallet.");
|
package/dist/core/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const MCP_PACKAGE_VERSION = "0.1.
|
|
1
|
+
export declare const MCP_PACKAGE_VERSION = "0.1.51";
|
package/dist/core/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const MCP_PACKAGE_VERSION = "0.1.
|
|
1
|
+
export const MCP_PACKAGE_VERSION = "0.1.51";
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import { registerWalletTools } from "./tools/wallet.js";
|
|
|
12
12
|
import { registerFavoriteTools } from "./tools/favorites.js";
|
|
13
13
|
import { registerTipTools } from "./tools/tip.js";
|
|
14
14
|
import { registerPassTools } from "./tools/passes.js";
|
|
15
|
+
import { registerRebateTools } from "./tools/rebates.js";
|
|
15
16
|
import { registerUploadTools } from "./tools/upload.js";
|
|
16
17
|
import { registerProbeTools } from "./tools/probe.js";
|
|
17
18
|
import { registerProviderTools } from "./tools/providers.js";
|
|
@@ -64,6 +65,7 @@ export async function startMcpServer() {
|
|
|
64
65
|
"",
|
|
65
66
|
"WALLET HYGIENE:",
|
|
66
67
|
"- wallet_status shows per-chain USDC balance and the active network (mainnet vs testnet).",
|
|
68
|
+
"- rebate_status shows accrued, pending, paid, and blocked consumer rebates plus recent payout transactions.",
|
|
67
69
|
"- To set up payments: wallet_setup({ action: \"start\" }). Link card/bank is recommended for most users.",
|
|
68
70
|
"- To create or import a crypto wallet directly: wallet_setup({ action: \"create\" }) or { action: \"import\", key }.",
|
|
69
71
|
"- NEVER delete or rotate keys programmatically. Direct users to edit ~/.agentwonderland/config.json or ~/.ows/ manually.",
|
|
@@ -80,6 +82,7 @@ export async function startMcpServer() {
|
|
|
80
82
|
registerFavoriteTools(server);
|
|
81
83
|
registerTipTools(server);
|
|
82
84
|
registerPassTools(server);
|
|
85
|
+
registerRebateTools(server);
|
|
83
86
|
registerUploadTools(server);
|
|
84
87
|
registerProbeTools(server);
|
|
85
88
|
registerProviderTools(server);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
const { mockApiGet } = vi.hoisted(() => ({
|
|
3
|
+
mockApiGet: vi.fn(),
|
|
4
|
+
}));
|
|
5
|
+
vi.mock("../../core/api-client.js", () => ({
|
|
6
|
+
apiGet: (...args) => mockApiGet(...args),
|
|
7
|
+
}));
|
|
8
|
+
async function getRebateStatusTool() {
|
|
9
|
+
const tools = new Map();
|
|
10
|
+
const server = {
|
|
11
|
+
tool: (name, _description, _schema, handler) => {
|
|
12
|
+
tools.set(name, handler);
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
const { registerRebateTools } = await import("../rebates.js");
|
|
16
|
+
registerRebateTools(server);
|
|
17
|
+
const tool = tools.get("rebate_status");
|
|
18
|
+
if (!tool)
|
|
19
|
+
throw new Error("rebate_status tool was not registered");
|
|
20
|
+
return tool;
|
|
21
|
+
}
|
|
22
|
+
function flattenText(result) {
|
|
23
|
+
return result.content.map((item) => item.text).join("\n");
|
|
24
|
+
}
|
|
25
|
+
describe("rebate_status tool", () => {
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
vi.resetModules();
|
|
28
|
+
mockApiGet.mockReset();
|
|
29
|
+
});
|
|
30
|
+
it("formats rebate balances and recent payouts", async () => {
|
|
31
|
+
mockApiGet.mockResolvedValueOnce({
|
|
32
|
+
consumer_principal: "did:pkh:eip155:8453:0xabc",
|
|
33
|
+
principal_type: "evm",
|
|
34
|
+
principal_value: "0xabc",
|
|
35
|
+
payout_address: "0xabc",
|
|
36
|
+
payout_chain: "base",
|
|
37
|
+
auto_payout_threshold_usd: "5.000000",
|
|
38
|
+
totals: {
|
|
39
|
+
lifetime_earned_usd: "2.500000",
|
|
40
|
+
pending_usd: "1.250000",
|
|
41
|
+
paid_usd: "1.000000",
|
|
42
|
+
blocked_usd: "0.250000",
|
|
43
|
+
total_count: 5,
|
|
44
|
+
pending_count: 2,
|
|
45
|
+
paid_count: 2,
|
|
46
|
+
blocked_count: 1,
|
|
47
|
+
},
|
|
48
|
+
by_source: [
|
|
49
|
+
{ source_type: "job_execution", status: "earned", count: 2, rebate_usd: "1.250000" },
|
|
50
|
+
],
|
|
51
|
+
recent_payouts: [
|
|
52
|
+
{
|
|
53
|
+
id: "payout-1",
|
|
54
|
+
status: "completed",
|
|
55
|
+
amount_usd: "1.000000",
|
|
56
|
+
earning_count: 2,
|
|
57
|
+
tx_hash: "0xhash",
|
|
58
|
+
processed_at: "2026-05-09T12:00:00.000Z",
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
});
|
|
62
|
+
const tool = await getRebateStatusTool();
|
|
63
|
+
const result = await tool({});
|
|
64
|
+
const text = flattenText(result);
|
|
65
|
+
expect(mockApiGet).toHaveBeenCalledWith("/rebates/status", { ensureConsumerPrincipal: true });
|
|
66
|
+
expect(text).toContain("Lifetime earned: $2.5000 across 5 rebate(s)");
|
|
67
|
+
expect(text).toContain("Pending: $1.2500 across 2 rebate(s)");
|
|
68
|
+
expect(text).toContain("Auto payout: $3.7500 until the $5.0000 threshold");
|
|
69
|
+
expect(text).toContain("job execution / earned: $1.2500 (2)");
|
|
70
|
+
expect(text).toContain("completed: $1.0000 (2) tx=0xhash");
|
|
71
|
+
});
|
|
72
|
+
});
|