@agentwonderland/mcp 0.1.45 → 0.1.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/core/__tests__/payments.test.js +55 -1
  2. package/dist/core/__tests__/principal.test.js +18 -0
  3. package/dist/core/config.d.ts +16 -0
  4. package/dist/core/config.js +37 -0
  5. package/dist/core/link-cli.d.ts +27 -0
  6. package/dist/core/link-cli.js +259 -0
  7. package/dist/core/mpp-client.d.ts +13 -1
  8. package/dist/core/mpp-client.js +45 -2
  9. package/dist/core/payments.js +135 -10
  10. package/dist/core/principal.js +2 -2
  11. package/dist/core/version.d.ts +1 -1
  12. package/dist/core/version.js +1 -1
  13. package/dist/index.js +6 -4
  14. package/dist/tools/__tests__/search.test.d.ts +1 -0
  15. package/dist/tools/__tests__/search.test.js +66 -0
  16. package/dist/tools/__tests__/wallet.test.js +153 -0
  17. package/dist/tools/passes.js +1 -1
  18. package/dist/tools/run.js +1 -1
  19. package/dist/tools/search.js +33 -0
  20. package/dist/tools/solve.js +1 -1
  21. package/dist/tools/wallet.js +195 -7
  22. package/package.json +1 -1
  23. package/src/core/__tests__/payments.test.ts +78 -1
  24. package/src/core/__tests__/principal.test.ts +23 -0
  25. package/src/core/config.ts +56 -0
  26. package/src/core/link-cli.ts +300 -0
  27. package/src/core/mpp-client.ts +69 -2
  28. package/src/core/payments.ts +153 -11
  29. package/src/core/principal.ts +2 -2
  30. package/src/core/version.ts +1 -1
  31. package/src/index.ts +6 -4
  32. package/src/tools/__tests__/search.test.ts +78 -0
  33. package/src/tools/__tests__/wallet.test.ts +190 -0
  34. package/src/tools/passes.ts +1 -1
  35. package/src/tools/run.ts +1 -1
  36. package/src/tools/search.ts +40 -0
  37. package/src/tools/solve.ts +1 -1
  38. package/src/tools/wallet.ts +229 -6
@@ -126,7 +126,7 @@ export function registerSolveTools(server) {
126
126
  .trim()
127
127
  .min(1)
128
128
  .optional()
129
- .describe("Payment method — wallet ID, chain name (tempo, base, etc.), or 'card'. Auto-detected if omitted."),
129
+ .describe("Payment method — wallet ID, chain name (tempo, base, solana), 'link', or 'card'. Auto-detected if omitted."),
130
130
  confirmed: z
131
131
  .boolean()
132
132
  .optional()
@@ -1,24 +1,66 @@
1
1
  import { z } from "zod";
2
- import { getWallets, getCardConfig, setCardConfig, addWallet, getPendingCardSetupToken, getSpendPolicy, setSpendPolicy, } from "../core/config.js";
2
+ import { getWallets, getCardConfig, getLinkConfig, getPendingLinkSetup, setPendingLinkSetup, setLinkConfig, setCardConfig, addWallet, getPendingCardSetupToken, getSpendPolicy, setDefaultPaymentMethod, setSpendPolicy, } from "../core/config.js";
3
3
  import { getWalletAddress, isCardPaymentEnabled } from "../core/payments.js";
4
4
  import { fetchUsdcBalance } from "../core/balances.js";
5
5
  import { getSettings } from "../core/settings.js";
6
6
  import { getOrCreatePendingCardSetup, formatCardSetupBlocks, getCardCapabilities, pollCardSetup, } from "../core/card-setup.js";
7
+ import { getLinkCliAuthStatus, listLinkPaymentMethods, openLinkPaymentMethodAdd, startLinkCliLogin, } from "../core/link-cli.js";
7
8
  import { isOwsAvailable, createOwsWallet, importKeyToOws, listOwsWallets, listOwsWalletsByChain, installOws, platformSupportsOws, } from "../core/ows-adapter.js";
8
9
  import { ensureConsumerPrincipal, getConsumerPrincipal } from "../core/principal.js";
9
10
  import { MCP_PACKAGE_VERSION } from "../core/version.js";
10
11
  function text(t) {
11
12
  return { content: [{ type: "text", text: t }] };
12
13
  }
14
+ function formatLinkApprovalPrompt(pending) {
15
+ return [
16
+ "Approve Agent Wonderland in Link:",
17
+ "",
18
+ pending.verificationUrl,
19
+ "",
20
+ `Verification phrase: ${pending.phrase}`,
21
+ "",
22
+ "After approving, run wallet_setup({ action: \"add-link\" }) again and I will finish connecting the payment method.",
23
+ ].join("\n");
24
+ }
25
+ function isFreshLinkSetup(pending) {
26
+ const createdAt = Date.parse(pending.createdAt);
27
+ if (!Number.isFinite(createdAt))
28
+ return false;
29
+ return Date.now() - createdAt < 10 * 60 * 1000;
30
+ }
31
+ function formatPaymentSetupMenu() {
32
+ return [
33
+ "Set up a payment method:",
34
+ "",
35
+ "1. Link card/bank (recommended)",
36
+ " wallet_setup({ action: \"add-link\" })",
37
+ "",
38
+ "2. Tempo USDC",
39
+ " wallet_setup({ action: \"create\", chain: \"tempo\" })",
40
+ "",
41
+ "3. Base USDC",
42
+ " wallet_setup({ action: \"create\", chain: \"base\" })",
43
+ "",
44
+ "4. Solana USDC",
45
+ " wallet_setup({ action: \"create\", chain: \"solana\" })",
46
+ "",
47
+ "5. Import an existing wallet",
48
+ " wallet_setup({ action: \"import\", chain: \"base\", key: \"<private key>\" })",
49
+ ].join("\n");
50
+ }
51
+ function setDefaultCryptoPaymentMethod(chain) {
52
+ setDefaultPaymentMethod(chain === "solana" ? "solana" : chain === "base" ? "base" : "tempo");
53
+ }
13
54
  export function registerWalletTools(server) {
14
55
  // ── wallet_status (extracted from check_wallet) ─────────────────
15
56
  server.tool("wallet_status", "Check payment readiness. Shows all configured wallets, their chains, addresses, and card status.", {}, async () => {
16
57
  const wallets = getWallets();
17
58
  const card = getCardConfig();
59
+ const link = getLinkConfig();
18
60
  const pendingCardSetupToken = getPendingCardSetupToken();
19
61
  const consumerPrincipal = await getConsumerPrincipal();
20
- if (wallets.length === 0 && !card && !pendingCardSetupToken) {
21
- return text("No payment methods configured.\nUse wallet_setup to create or import a wallet.");
62
+ if (wallets.length === 0 && !card && !link && !pendingCardSetupToken) {
63
+ return text(`No payment methods configured.\n\n${formatPaymentSetupMenu()}`);
22
64
  }
23
65
  const settings = await getSettings();
24
66
  const networkLabel = settings ? ` (${settings.network})` : "";
@@ -61,6 +103,14 @@ export function registerWalletTools(server) {
61
103
  lines.push(` Card MPP: unknown — ${capabilities.message ?? "Could not determine card payment readiness."}`);
62
104
  }
63
105
  }
106
+ if (link) {
107
+ const auth = await getLinkCliAuthStatus();
108
+ const label = link.label ? ` (${link.label})` : "";
109
+ lines.push(` Link${label}: ${link.paymentMethodId}`);
110
+ lines.push(auth.authenticated
111
+ ? " Link CLI: authenticated"
112
+ : " Link CLI: not authenticated — run npx @stripe/link-cli auth login");
113
+ }
64
114
  if (pendingCardSetupToken) {
65
115
  lines.push(" Card setup: pending confirmation");
66
116
  }
@@ -77,10 +127,10 @@ export function registerWalletTools(server) {
77
127
  return text(lines.join("\n"));
78
128
  });
79
129
  // ── wallet_setup ────────────────────────────────────────────────
80
- server.tool("wallet_setup", "Set up or manage a payment wallet. 'create' makes a new crypto wallet (encrypted via OWS if available, otherwise plaintext — run 'enable-ows' to upgrade). 'import' takes an existing private key. 'enable-ows' installs the Open Wallet Standard native module for encrypted at-rest storage. Tempo/Base share one EVM key; Solana uses a separate ed25519 key. NEVER delete or rotate keys programmatically; direct users to edit ~/.agentwonderland/config.json or ~/.ows/ manually.", {
130
+ server.tool("wallet_setup", "Set up or manage an Agent Wonderland payment method. Use 'start' for a guided setup menu. Link card/bank is recommended for most users. 'create' makes a new crypto wallet (encrypted via OWS if available, otherwise plaintext — run 'enable-ows' to upgrade). 'import' takes an existing private key. 'enable-ows' installs the Open Wallet Standard native module for encrypted at-rest storage. Tempo/Base share one EVM key; Solana uses a separate ed25519 key. NEVER delete or rotate keys programmatically; direct users to edit ~/.agentwonderland/config.json or ~/.ows/ manually.", {
81
131
  action: z
82
- .enum(["create", "import", "add-card", "remove-card", "enable-ows"])
83
- .describe("'create' a wallet, 'import' an existing key, 'enable-ows' to install encrypted key storage, 'add-card'/'remove-card' for credit card (may be disabled depending on Stripe SPT availability)"),
132
+ .enum(["start", "create", "import", "add-card", "remove-card", "add-link", "remove-link", "enable-ows"])
133
+ .describe("'start' shows the guided payment setup menu, 'add-link' connects Link card/bank, 'create' makes a crypto wallet, 'import' imports an existing key, 'enable-ows' installs encrypted key storage"),
84
134
  name: z
85
135
  .string()
86
136
  .optional()
@@ -91,7 +141,138 @@ export function registerWalletTools(server) {
91
141
  .describe("Private key hex string (required for 'import', ignored for 'create')"),
92
142
  chain: z.enum(["tempo", "base", "solana"]).optional()
93
143
  .describe("Primary chain (default: tempo). Tempo/Base use a shared EVM wallet; Solana uses a separate OWS wallet."),
94
- }, async ({ action, name, key, chain }) => {
144
+ link_payment_method_id: z.string().optional()
145
+ .describe("Link payment method ID from `npx @stripe/link-cli payment-methods list`; used with action 'add-link'."),
146
+ link_payment_method: z.string().optional()
147
+ .describe("Friendly Link payment method selector, such as a card/bank name or last4; used with action 'add-link'."),
148
+ }, async ({ action, name, key, chain, link_payment_method_id, link_payment_method }) => {
149
+ if (action === "start") {
150
+ return text(formatPaymentSetupMenu());
151
+ }
152
+ // ── Link setup flow ──────────────────────────────────────
153
+ if (action === "add-link") {
154
+ let auth = await getLinkCliAuthStatus();
155
+ if (!auth.authenticated) {
156
+ const pending = getPendingLinkSetup();
157
+ if (pending && isFreshLinkSetup(pending)) {
158
+ return text(formatLinkApprovalPrompt(pending));
159
+ }
160
+ if (pending) {
161
+ setPendingLinkSetup(null);
162
+ }
163
+ try {
164
+ const login = await startLinkCliLogin();
165
+ setPendingLinkSetup({
166
+ verificationUrl: login.verificationUrl,
167
+ phrase: login.phrase,
168
+ createdAt: new Date().toISOString(),
169
+ });
170
+ auth = await getLinkCliAuthStatus();
171
+ if (!auth.authenticated) {
172
+ return text(formatLinkApprovalPrompt({
173
+ verificationUrl: login.verificationUrl,
174
+ phrase: login.phrase,
175
+ }));
176
+ }
177
+ }
178
+ catch (err) {
179
+ return text([
180
+ "Link authentication could not start from the MCP session.",
181
+ `Reason: ${err instanceof Error ? err.message : String(err)}`,
182
+ "",
183
+ "Run wallet_setup({ action: \"add-link\" }) again to retry.",
184
+ ].join("\n"));
185
+ }
186
+ }
187
+ setPendingLinkSetup(null);
188
+ const explicitLinkSelector = link_payment_method_id ?? link_payment_method;
189
+ if (explicitLinkSelector) {
190
+ const methods = await listLinkPaymentMethods();
191
+ const normalizedSelector = explicitLinkSelector.toLowerCase().trim();
192
+ const exact = methods.find((method) => method.id === explicitLinkSelector);
193
+ const matches = exact
194
+ ? [exact]
195
+ : methods.filter((method) => method.searchText?.includes(normalizedSelector));
196
+ if (matches.length === 0 && !link_payment_method_id) {
197
+ return text([
198
+ `No Link payment method matched "${explicitLinkSelector}".`,
199
+ "",
200
+ "Available methods:",
201
+ ...methods.map((method) => ` ${method.label ?? method.id}${method.label ? ` (${method.id})` : ""}`),
202
+ ].join("\n"));
203
+ }
204
+ if (matches.length > 1) {
205
+ return text([
206
+ `Multiple Link payment methods matched "${explicitLinkSelector}". Choose one:`,
207
+ ...matches.map((method) => ` ${method.label ?? method.id}${method.label ? ` (${method.id})` : ""}`),
208
+ "",
209
+ "Then run:",
210
+ " wallet_setup({ action: \"add-link\", link_payment_method: \"<name or last4>\" })",
211
+ ].join("\n"));
212
+ }
213
+ const selected = matches[0] ?? { id: link_payment_method_id, label: name };
214
+ setLinkConfig({
215
+ paymentMethodId: selected.id,
216
+ label: name ?? selected.label,
217
+ });
218
+ const principal = await ensureConsumerPrincipal();
219
+ return text([
220
+ "Link payment method connected.",
221
+ ` Payment method: ${selected.label ?? selected.id}${selected.label ? ` (${selected.id})` : ""}`,
222
+ ` Consumer principal: ${principal}`,
223
+ "",
224
+ "Use pay_with: \"link\" for agent runs.",
225
+ ].join("\n"));
226
+ }
227
+ let methods = await listLinkPaymentMethods();
228
+ if (methods.length === 0) {
229
+ try {
230
+ await openLinkPaymentMethodAdd();
231
+ methods = await listLinkPaymentMethods();
232
+ }
233
+ catch (err) {
234
+ return text([
235
+ "No Link payment methods are available yet.",
236
+ `Reason: ${err instanceof Error ? err.message : String(err)}`,
237
+ "",
238
+ "Run wallet_setup({ action: \"add-link\" }) again after adding a payment method in Link.",
239
+ ].join("\n"));
240
+ }
241
+ if (methods.length === 0) {
242
+ return text("Link payment-method setup was started. Run wallet_setup({ action: \"add-link\" }) again after adding a payment method in Link.");
243
+ }
244
+ }
245
+ if (methods.length === 1) {
246
+ const method = methods[0];
247
+ setLinkConfig({
248
+ paymentMethodId: method.id,
249
+ label: method.label,
250
+ });
251
+ const principal = await ensureConsumerPrincipal();
252
+ return text([
253
+ "Link payment method connected.",
254
+ ` Payment method: ${method.id}${method.label ? ` (${method.label})` : ""}`,
255
+ ` Consumer principal: ${principal}`,
256
+ "",
257
+ "Use pay_with: \"link\" for agent runs.",
258
+ ].join("\n"));
259
+ }
260
+ return text([
261
+ "Multiple Link payment methods found. Choose one:",
262
+ ...methods.map((method) => ` ${method.label ?? method.id}${method.label ? ` (${method.id})` : ""}`),
263
+ "",
264
+ "Then run:",
265
+ " wallet_setup({ action: \"add-link\", link_payment_method: \"<name or last4>\" })",
266
+ ].join("\n"));
267
+ }
268
+ if (action === "remove-link") {
269
+ const existing = getLinkConfig();
270
+ if (!existing) {
271
+ return text("No Link payment method is currently connected.");
272
+ }
273
+ setLinkConfig(null);
274
+ return text(`Removed Link payment method ${existing.paymentMethodId}.`);
275
+ }
95
276
  // ── Card setup flow ──────────────────────────────────────
96
277
  if (action === "add-card") {
97
278
  if (!isCardPaymentEnabled()) {
@@ -206,6 +387,7 @@ export function registerWalletTools(server) {
206
387
  chainStatus = "Already linked to Agent Wonderland.";
207
388
  }
208
389
  }
390
+ setDefaultCryptoPaymentMethod(defaultCh);
209
391
  const principal = await ensureConsumerPrincipal();
210
392
  return text([
211
393
  "Found existing OWS wallet:",
@@ -239,6 +421,7 @@ export function registerWalletTools(server) {
239
421
  defaultChain: "solana",
240
422
  label: walletName,
241
423
  });
424
+ setDefaultCryptoPaymentMethod("solana");
242
425
  const principal = await ensureConsumerPrincipal();
243
426
  const owsNudge = platformSupportsOws()
244
427
  ? "\n\n⚠ Key stored in plaintext at ~/.agentwonderland/config.json. Run wallet_setup({ action: \"enable-ows\" }) to install encrypted at-rest storage — takes ~30s."
@@ -266,6 +449,7 @@ export function registerWalletTools(server) {
266
449
  defaultChain: defaultCh === "solana" ? "tempo" : defaultCh,
267
450
  label: walletName,
268
451
  });
452
+ setDefaultCryptoPaymentMethod(defaultCh);
269
453
  const principal = await ensureConsumerPrincipal();
270
454
  const owsNudge = platformSupportsOws()
271
455
  ? "\n\n⚠ Key stored in plaintext at ~/.agentwonderland/config.json. Run wallet_setup({ action: \"enable-ows\" }) to install encrypted at-rest storage — takes ~30s."
@@ -293,6 +477,7 @@ export function registerWalletTools(server) {
293
477
  defaultChain: defaultCh,
294
478
  label: walletName,
295
479
  });
480
+ setDefaultCryptoPaymentMethod(defaultCh);
296
481
  const principal = await ensureConsumerPrincipal();
297
482
  return text([
298
483
  `Wallet created [encrypted]:`,
@@ -321,6 +506,7 @@ export function registerWalletTools(server) {
321
506
  defaultChain: defaultCh,
322
507
  label: walletName,
323
508
  });
509
+ setDefaultCryptoPaymentMethod(defaultCh);
324
510
  const principal = await ensureConsumerPrincipal();
325
511
  return text([
326
512
  `Key imported to OWS [encrypted]:`,
@@ -349,6 +535,7 @@ export function registerWalletTools(server) {
349
535
  defaultChain: "solana",
350
536
  label: walletName,
351
537
  });
538
+ setDefaultCryptoPaymentMethod("solana");
352
539
  const principal = await ensureConsumerPrincipal();
353
540
  const owsNudge = platformSupportsOws()
354
541
  ? "\n\n⚠ Key stored in plaintext at ~/.agentwonderland/config.json. Run wallet_setup({ action: \"enable-ows\" }) to install encrypted at-rest storage — takes ~30s."
@@ -382,6 +569,7 @@ export function registerWalletTools(server) {
382
569
  defaultChain: defaultCh,
383
570
  label: walletName,
384
571
  });
572
+ setDefaultCryptoPaymentMethod(defaultCh);
385
573
  const principal = await ensureConsumerPrincipal();
386
574
  const owsNudge = platformSupportsOws()
387
575
  ? "\n\n⚠ Key stored in plaintext at ~/.agentwonderland/config.json. Run wallet_setup({ action: \"enable-ows\" }) to install encrypted at-rest storage — takes ~30s."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentwonderland/mcp",
3
- "version": "0.1.45",
3
+ "version": "0.1.47",
4
4
  "type": "module",
5
5
  "description": "MCP server for the Agent Wonderland AI agent marketplace",
6
6
  "bin": {
@@ -8,21 +8,25 @@ let currentCard = {
8
8
  last4: "1111",
9
9
  brand: "visa",
10
10
  };
11
+ let currentLink: { paymentMethodId: string; label?: string } | null = null;
11
12
 
12
13
  let currentDefaultWallet: any;
13
14
  let currentWallets: any[] = [];
14
15
  let currentResolvedMethod: { wallet: any; chain: string } | null = null;
16
+ let currentDefaultPaymentMethod = "card";
15
17
 
16
18
  const createdFetches = [vi.fn(), vi.fn(), vi.fn(), vi.fn()];
17
19
  const mockMppxCreate = vi.fn();
18
20
  const mockStripe = vi.fn((opts: unknown) => opts);
19
21
  const mockTempoChargeClient = vi.fn((..._args: unknown[]) => "tempo_method");
20
22
  const mockBaseChargeClient = vi.fn((..._args: unknown[]) => "base_method");
23
+ const mockCreateLinkSharedPaymentToken = vi.fn(async (_config: unknown) => "spt_test");
21
24
 
22
25
  vi.mock("../config.js", () => ({
23
26
  getApiUrl: () => "http://api.test",
24
27
  getCardConfig: () => currentCard,
25
- getConfig: () => ({ defaultPaymentMethod: "card" }),
28
+ getLinkConfig: () => currentLink,
29
+ getConfig: () => ({ defaultPaymentMethod: currentDefaultPaymentMethod }),
26
30
  getDefaultWallet: () => currentDefaultWallet,
27
31
  getWallets: () => currentWallets,
28
32
  resolveWalletAndChain: () => currentResolvedMethod,
@@ -43,10 +47,15 @@ vi.mock("../base-charge.js", () => ({
43
47
  baseChargeClient: (config: unknown) => mockBaseChargeClient(config),
44
48
  }));
45
49
 
50
+ vi.mock("../link-cli.js", () => ({
51
+ createLinkSharedPaymentToken: (config: unknown) => mockCreateLinkSharedPaymentToken(config),
52
+ }));
53
+
46
54
  describe("payment method initialization", () => {
47
55
  beforeEach(() => {
48
56
  vi.clearAllMocks();
49
57
  vi.resetModules();
58
+ delete process.env.AGENTWONDERLAND_LINK_APPROVAL_LIMIT_CENTS;
50
59
  currentCard = {
51
60
  consumerToken: "consumer_one",
52
61
  paymentMethodId: "pm_one",
@@ -56,6 +65,8 @@ describe("payment method initialization", () => {
56
65
  currentDefaultWallet = undefined;
57
66
  currentWallets = [];
58
67
  currentResolvedMethod = null;
68
+ currentLink = null;
69
+ currentDefaultPaymentMethod = "card";
59
70
  mockMppxCreate
60
71
  .mockReturnValueOnce({ fetch: createdFetches[0] })
61
72
  .mockReturnValueOnce({ fetch: createdFetches[1] })
@@ -116,4 +127,70 @@ describe("payment method initialization", () => {
116
127
  expect.objectContaining({ methods: ["tempo_method"], polyfill: false }),
117
128
  );
118
129
  });
130
+
131
+ it("initializes Stripe SPT method when Link is requested", async () => {
132
+ currentLink = {
133
+ paymentMethodId: "csmrpd_link_123",
134
+ label: "Visa ****4242",
135
+ };
136
+ currentDefaultPaymentMethod = "link";
137
+
138
+ const { getPaymentFetch } = await import("../payments.js");
139
+ await getPaymentFetch("link");
140
+
141
+ expect(mockStripe).toHaveBeenCalledWith(
142
+ expect.objectContaining({ paymentMethod: "csmrpd_link_123" }),
143
+ );
144
+ expect(mockMppxCreate).toHaveBeenCalledWith(
145
+ expect.objectContaining({
146
+ methods: [expect.objectContaining({ paymentMethod: "csmrpd_link_123" })],
147
+ polyfill: false,
148
+ }),
149
+ );
150
+
151
+ const stripeConfig = mockStripe.mock.calls[0]?.[0] as {
152
+ createToken: (params: {
153
+ amount: string;
154
+ currency: string;
155
+ expiresAt: number;
156
+ metadata?: Record<string, string>;
157
+ networkId: string;
158
+ }) => Promise<string>;
159
+ };
160
+ await stripeConfig.createToken({
161
+ amount: "25",
162
+ currency: "usd",
163
+ expiresAt: 1778290000,
164
+ networkId: "profile_test",
165
+ });
166
+
167
+ expect(mockCreateLinkSharedPaymentToken).toHaveBeenCalledWith(
168
+ expect.objectContaining({
169
+ amount: "10000",
170
+ currency: "usd",
171
+ networkId: "profile_test",
172
+ paymentMethodId: "csmrpd_link_123",
173
+ }),
174
+ );
175
+ const linkTokenRequest = mockCreateLinkSharedPaymentToken.mock.calls[0]?.[0] as { context: string } | undefined;
176
+ expect(linkTokenRequest?.context).toContain("up to USD 100.00");
177
+ expect(linkTokenRequest?.context).toContain("quoted at USD 0.25");
178
+ });
179
+
180
+ it("advertises Link as Stripe SPT compatibility", async () => {
181
+ currentLink = {
182
+ paymentMethodId: "csmrpd_link_123",
183
+ };
184
+ currentDefaultPaymentMethod = "link";
185
+
186
+ const { getAcceptedPaymentMethods, getCompatiblePaymentMethods } = await import("../payments.js");
187
+
188
+ expect(getAcceptedPaymentMethods()).toEqual(["stripe_card", "card"]);
189
+ expect(getCompatiblePaymentMethods({
190
+ payment: { accepted_payments: ["stripe_card"] },
191
+ } as any)).toEqual(["link"]);
192
+ expect(getCompatiblePaymentMethods({
193
+ payment: { accepted_payments: ["card"] },
194
+ } as any)).toEqual(["link"]);
195
+ });
119
196
  });
@@ -114,6 +114,29 @@ describe("consumer principal helpers", () => {
114
114
  );
115
115
  });
116
116
 
117
+ it("uses the default consumer principal for Link payments", async () => {
118
+ state.wallets = [
119
+ { id: "aw-main", keyType: "ows", owsWalletId: "ows-main", chains: ["tempo", "base"], defaultChain: "base" },
120
+ ];
121
+ state.addresses = {
122
+ "aw-main": "0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
123
+ base: "0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
124
+ };
125
+
126
+ const { getConsumerPrincipalForMethod } = await import("../principal.js");
127
+ const principal = await getConsumerPrincipalForMethod("link");
128
+
129
+ expect(principal).toBe("did:pkh:eip155:8453:0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
130
+ });
131
+
132
+ it("creates a fallback identity wallet for Link payments when no wallet exists", async () => {
133
+ const { ensureConsumerPrincipalForMethod } = await import("../principal.js");
134
+ const principal = await ensureConsumerPrincipalForMethod("link");
135
+
136
+ expect(principal).toMatch(/^did:pkh:eip155:8453:0x[a-f0-9]{40}$/);
137
+ expect(state.addedWallets).toHaveLength(1);
138
+ });
139
+
117
140
  it("returns the Base rebate principal when an EVM wallet is available", async () => {
118
141
  state.wallets = [
119
142
  { id: "aw-main", keyType: "ows", owsWalletId: "ows-main", chains: ["tempo", "base", "solana"], defaultChain: "tempo" },
@@ -21,6 +21,17 @@ export interface CardConfig {
21
21
  brand: string;
22
22
  }
23
23
 
24
+ export interface LinkConfig {
25
+ paymentMethodId: string;
26
+ label?: string;
27
+ }
28
+
29
+ export interface PendingLinkSetup {
30
+ verificationUrl: string;
31
+ phrase: string;
32
+ createdAt: string;
33
+ }
34
+
24
35
  export interface SpendPolicy {
25
36
  maxPerTxUsd?: number;
26
37
  maxPerDayUsd?: number;
@@ -42,6 +53,8 @@ export interface Config {
42
53
  defaultWallet: string | null;
43
54
  defaultPaymentMethod?: string;
44
55
  card: CardConfig | null;
56
+ link: LinkConfig | null;
57
+ pendingLinkSetup?: PendingLinkSetup | null;
45
58
  pendingCardSetupToken?: string | null;
46
59
  favorites: string[];
47
60
  /** Require user confirmation before spending. Default: true. Set false for headless/automated use. */
@@ -98,6 +111,8 @@ interface LegacyConfig {
98
111
  wallets?: WalletEntry[];
99
112
  defaultWallet?: string | null;
100
113
  card?: CardConfig | null;
114
+ link?: LinkConfig | null;
115
+ pendingLinkSetup?: PendingLinkSetup | null;
101
116
  pendingCardSetupToken?: string | null;
102
117
  spendPolicies?: Record<string, SpendPolicy>;
103
118
  spendLedger?: SpendLedgerEntry[];
@@ -119,6 +134,8 @@ function migrateIfNeeded(raw: LegacyConfig): Config {
119
134
  defaultWallet: raw.defaultWallet ?? null,
120
135
  defaultPaymentMethod: raw.defaultPaymentMethod ?? undefined,
121
136
  card: raw.card ?? null,
137
+ link: raw.link ?? null,
138
+ pendingLinkSetup: raw.link ? null : (raw.pendingLinkSetup ?? null),
122
139
  pendingCardSetupToken: (r.pendingCardSetupToken as string | null | undefined) ?? null,
123
140
  favorites: r.favorites as string[] ?? [],
124
141
  confirmBeforeSpend: r.confirmBeforeSpend !== false,
@@ -195,6 +212,8 @@ function migrateIfNeeded(raw: LegacyConfig): Config {
195
212
  defaultWallet,
196
213
  defaultPaymentMethod: raw.defaultPaymentMethod ?? undefined,
197
214
  card,
215
+ link: raw.link ?? null,
216
+ pendingLinkSetup: raw.link ? null : (raw.pendingLinkSetup ?? null),
198
217
  pendingCardSetupToken: null,
199
218
  favorites: [],
200
219
  confirmBeforeSpend: true,
@@ -222,6 +241,8 @@ export function getConfig(): Config {
222
241
  wallets: [],
223
242
  defaultWallet: null,
224
243
  card: null,
244
+ link: null,
245
+ pendingLinkSetup: null,
225
246
  pendingCardSetupToken: null,
226
247
  favorites: [],
227
248
  confirmBeforeSpend: true,
@@ -286,6 +307,10 @@ export function setSpendPolicy(method: string, policy: SpendPolicy): void {
286
307
  saveConfig({ spendPolicies: policies });
287
308
  }
288
309
 
310
+ export function setDefaultPaymentMethod(defaultPaymentMethod: string | undefined): void {
311
+ saveConfig({ defaultPaymentMethod });
312
+ }
313
+
289
314
  export function getSpendLedger(): SpendLedgerEntry[] {
290
315
  return getConfig().spendLedger ?? [];
291
316
  }
@@ -399,6 +424,14 @@ export function getCardConfig(): CardConfig | null {
399
424
  return getConfig().card;
400
425
  }
401
426
 
427
+ export function getLinkConfig(): LinkConfig | null {
428
+ return getConfig().link;
429
+ }
430
+
431
+ export function getPendingLinkSetup(): PendingLinkSetup | null {
432
+ return getConfig().pendingLinkSetup ?? null;
433
+ }
434
+
402
435
  /**
403
436
  * Save card configuration after setup.
404
437
  */
@@ -421,6 +454,29 @@ export function setCardConfig(card: CardConfig | null): void {
421
454
  }
422
455
  }
423
456
 
457
+ export function setLinkConfig(link: LinkConfig | null): void {
458
+ const current = getConfig();
459
+ if (link) {
460
+ saveConfig({
461
+ link,
462
+ defaultPaymentMethod: "link",
463
+ pendingLinkSetup: null,
464
+ });
465
+ } else {
466
+ saveConfig({
467
+ link,
468
+ pendingLinkSetup: null,
469
+ defaultPaymentMethod: current.defaultPaymentMethod === "link"
470
+ ? undefined
471
+ : current.defaultPaymentMethod,
472
+ });
473
+ }
474
+ }
475
+
476
+ export function setPendingLinkSetup(pendingLinkSetup: PendingLinkSetup | null): void {
477
+ saveConfig({ pendingLinkSetup });
478
+ }
479
+
424
480
  export function getPendingCardSetupToken(): string | null {
425
481
  return getConfig().pendingCardSetupToken ?? null;
426
482
  }