@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
|
@@ -5,6 +5,7 @@ const state = vi.hoisted(() => ({
|
|
|
5
5
|
card: null,
|
|
6
6
|
link: null,
|
|
7
7
|
pendingLinkSetup: null,
|
|
8
|
+
linkCooldown: null,
|
|
8
9
|
pendingCardSetupToken: null,
|
|
9
10
|
spendPolicies: {},
|
|
10
11
|
defaultPaymentMethod: undefined,
|
|
@@ -53,6 +54,7 @@ vi.mock("../../core/config.js", () => ({
|
|
|
53
54
|
},
|
|
54
55
|
getCardConfig: () => state.card,
|
|
55
56
|
getLinkConfig: () => state.link,
|
|
57
|
+
getLinkCooldown: () => state.linkCooldown,
|
|
56
58
|
getPendingLinkSetup: () => state.pendingLinkSetup,
|
|
57
59
|
getPendingCardSetupToken: () => state.pendingCardSetupToken,
|
|
58
60
|
getSpendPolicy: (walletId) => state.spendPolicies[walletId] ?? null,
|
|
@@ -122,6 +124,7 @@ vi.mock("../../core/ows-adapter.js", () => ({
|
|
|
122
124
|
}));
|
|
123
125
|
vi.mock("../../core/principal.js", () => ({
|
|
124
126
|
ensureConsumerPrincipal: async () => state.consumerPrincipal,
|
|
127
|
+
ensureConsumerPrincipalForMethod: async () => state.consumerPrincipal,
|
|
125
128
|
getConsumerPrincipal: async () => state.consumerPrincipal,
|
|
126
129
|
}));
|
|
127
130
|
function resetState() {
|
|
@@ -130,6 +133,7 @@ function resetState() {
|
|
|
130
133
|
state.card = null;
|
|
131
134
|
state.link = null;
|
|
132
135
|
state.pendingLinkSetup = null;
|
|
136
|
+
state.linkCooldown = null;
|
|
133
137
|
state.pendingCardSetupToken = null;
|
|
134
138
|
state.spendPolicies = {};
|
|
135
139
|
state.defaultPaymentMethod = undefined;
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export { registerWalletTools } from "./wallet.js";
|
|
|
8
8
|
export { registerFavoriteTools } from "./favorites.js";
|
|
9
9
|
export { registerTipTools } from "./tip.js";
|
|
10
10
|
export { registerPassTools } from "./passes.js";
|
|
11
|
+
export { registerRebateTools } from "./rebates.js";
|
|
11
12
|
export { registerUploadTools } from "./upload.js";
|
|
12
13
|
export { registerProbeTools } from "./probe.js";
|
|
13
14
|
export { registerProviderTools } from "./providers.js";
|
package/dist/tools/index.js
CHANGED
|
@@ -8,6 +8,7 @@ export { registerWalletTools } from "./wallet.js";
|
|
|
8
8
|
export { registerFavoriteTools } from "./favorites.js";
|
|
9
9
|
export { registerTipTools } from "./tip.js";
|
|
10
10
|
export { registerPassTools } from "./passes.js";
|
|
11
|
+
export { registerRebateTools } from "./rebates.js";
|
|
11
12
|
export { registerUploadTools } from "./upload.js";
|
|
12
13
|
export { registerProbeTools } from "./probe.js";
|
|
13
14
|
export { registerProviderTools } from "./providers.js";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { apiPost } from "../core/api-client.js";
|
|
2
|
+
function text(t) {
|
|
3
|
+
return { content: [{ type: "text", text: t }] };
|
|
4
|
+
}
|
|
5
|
+
export function registerObservabilityTools(server) {
|
|
6
|
+
server.tool("open_observability_dashboard", "Generate a secure one-click sign-in URL for the Agent Wonderland web observability dashboard. The dashboard shows your agent runs, spend, rebates, and recent activity.", {}, async () => {
|
|
7
|
+
const result = await apiPost("/observability/link", {}, { ensureConsumerPrincipal: true });
|
|
8
|
+
const lines = [
|
|
9
|
+
"Your secure observability link is ready:",
|
|
10
|
+
result.url,
|
|
11
|
+
"",
|
|
12
|
+
`Expires: ${result.expires_at}`,
|
|
13
|
+
];
|
|
14
|
+
if (result.consumer_principal) {
|
|
15
|
+
lines.push(`Consumer principal: ${result.consumer_principal}`);
|
|
16
|
+
}
|
|
17
|
+
lines.push("", "Open the link in your browser to view usage metrics, spend, rebates, and recent runs.");
|
|
18
|
+
return text(lines.join("\n"));
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { apiGet } from "../core/api-client.js";
|
|
2
|
+
function text(t) {
|
|
3
|
+
return { content: [{ type: "text", text: t }] };
|
|
4
|
+
}
|
|
5
|
+
function money(value) {
|
|
6
|
+
const numeric = Number(value);
|
|
7
|
+
return Number.isFinite(numeric) ? `$${numeric.toFixed(4)}` : `$${value}`;
|
|
8
|
+
}
|
|
9
|
+
function sourceLabel(value) {
|
|
10
|
+
return value.replace(/_/g, " ");
|
|
11
|
+
}
|
|
12
|
+
export function registerRebateTools(server) {
|
|
13
|
+
server.tool("rebate_status", "Show your Agent Wonderland consumer rebate balance, payout threshold, blocked rebates, and recent payout transactions.", {}, async () => {
|
|
14
|
+
const status = await apiGet("/rebates/status", { ensureConsumerPrincipal: true });
|
|
15
|
+
const threshold = Number(status.auto_payout_threshold_usd);
|
|
16
|
+
const pending = Number(status.totals.pending_usd);
|
|
17
|
+
const remaining = Math.max(0, threshold - pending);
|
|
18
|
+
const lines = [
|
|
19
|
+
"Rebate status",
|
|
20
|
+
`Consumer principal: ${status.consumer_principal}`,
|
|
21
|
+
`Base payout address: ${status.payout_address ?? "not configured"}`,
|
|
22
|
+
"",
|
|
23
|
+
`Lifetime earned: ${money(status.totals.lifetime_earned_usd)} across ${status.totals.total_count} rebate(s)`,
|
|
24
|
+
`Paid out: ${money(status.totals.paid_usd)} across ${status.totals.paid_count} rebate(s)`,
|
|
25
|
+
`Pending: ${money(status.totals.pending_usd)} across ${status.totals.pending_count} rebate(s)`,
|
|
26
|
+
`Blocked: ${money(status.totals.blocked_usd)} across ${status.totals.blocked_count} rebate(s)`,
|
|
27
|
+
];
|
|
28
|
+
if (Number.isFinite(threshold) && threshold > 0) {
|
|
29
|
+
lines.push(pending >= threshold
|
|
30
|
+
? `Auto payout: eligible for the next processor run (threshold ${money(threshold)})`
|
|
31
|
+
: `Auto payout: ${money(remaining)} until the ${money(threshold)} threshold`);
|
|
32
|
+
}
|
|
33
|
+
if (status.by_source.length > 0) {
|
|
34
|
+
lines.push("", "By source:");
|
|
35
|
+
for (const row of status.by_source) {
|
|
36
|
+
lines.push(` ${sourceLabel(row.source_type)} / ${row.status}: ${money(row.rebate_usd)} (${row.count})`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (status.recent_payouts.length > 0) {
|
|
40
|
+
lines.push("", "Recent payouts:");
|
|
41
|
+
for (const payout of status.recent_payouts.slice(0, 5)) {
|
|
42
|
+
const tx = payout.tx_hash ? ` tx=${payout.tx_hash}` : "";
|
|
43
|
+
lines.push(` ${payout.status}: ${money(payout.amount_usd)} (${payout.earning_count})${tx}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (!status.payout_address) {
|
|
47
|
+
lines.push("", "No Base payout address is configured, so new rebates may be blocked.");
|
|
48
|
+
}
|
|
49
|
+
return text(lines.join("\n"));
|
|
50
|
+
});
|
|
51
|
+
}
|
package/dist/tools/wallet.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { getWallets, getCardConfig, getLinkConfig, getPendingLinkSetup, setPendingLinkSetup, setLinkConfig, setCardConfig, addWallet, getPendingCardSetupToken, getSpendPolicy, setDefaultPaymentMethod, setSpendPolicy, } from "../core/config.js";
|
|
2
|
+
import { getWallets, getCardConfig, getLinkConfig, getLinkCooldown, 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
7
|
import { getLinkCliAuthStatus, listLinkPaymentMethods, openLinkPaymentMethodAdd, startLinkCliLogin, } from "../core/link-cli.js";
|
|
8
8
|
import { isOwsAvailable, createOwsWallet, importKeyToOws, listOwsWallets, listOwsWalletsByChain, installOws, platformSupportsOws, } from "../core/ows-adapter.js";
|
|
9
|
-
import { ensureConsumerPrincipal, getConsumerPrincipal } from "../core/principal.js";
|
|
9
|
+
import { ensureConsumerPrincipal, ensureConsumerPrincipalForMethod, getConsumerPrincipal } from "../core/principal.js";
|
|
10
10
|
import { MCP_PACKAGE_VERSION } from "../core/version.js";
|
|
11
11
|
function text(t) {
|
|
12
12
|
return { content: [{ type: "text", text: t }] };
|
|
@@ -110,6 +110,12 @@ export function registerWalletTools(server) {
|
|
|
110
110
|
lines.push(auth.authenticated
|
|
111
111
|
? " Link CLI: authenticated"
|
|
112
112
|
: " Link CLI: not authenticated — run npx @stripe/link-cli auth login");
|
|
113
|
+
const cooldown = getLinkCooldown();
|
|
114
|
+
const blockedUntil = cooldown ? Date.parse(cooldown.blockedUntil) : NaN;
|
|
115
|
+
if (cooldown && Number.isFinite(blockedUntil) && blockedUntil > Date.now()) {
|
|
116
|
+
lines.push(` Link status: temporarily blocked by Stripe projected-spend cap until ${cooldown.blockedUntil}`);
|
|
117
|
+
lines.push(" Link note: reauthing or switching cards in the same Link account will not clear this cap.");
|
|
118
|
+
}
|
|
113
119
|
}
|
|
114
120
|
if (pendingCardSetupToken) {
|
|
115
121
|
lines.push(" Card setup: pending confirmation");
|
|
@@ -215,7 +221,7 @@ export function registerWalletTools(server) {
|
|
|
215
221
|
paymentMethodId: selected.id,
|
|
216
222
|
label: name ?? selected.label,
|
|
217
223
|
});
|
|
218
|
-
const principal = await
|
|
224
|
+
const principal = await ensureConsumerPrincipalForMethod("link");
|
|
219
225
|
return text([
|
|
220
226
|
"Link payment method connected.",
|
|
221
227
|
` Payment method: ${selected.label ?? selected.id}${selected.label ? ` (${selected.id})` : ""}`,
|
|
@@ -248,7 +254,7 @@ export function registerWalletTools(server) {
|
|
|
248
254
|
paymentMethodId: method.id,
|
|
249
255
|
label: method.label,
|
|
250
256
|
});
|
|
251
|
-
const principal = await
|
|
257
|
+
const principal = await ensureConsumerPrincipalForMethod("link");
|
|
252
258
|
return text([
|
|
253
259
|
"Link payment method connected.",
|
|
254
260
|
` Payment method: ${method.id}${method.label ? ` (${method.label})` : ""}`,
|
|
@@ -287,7 +293,7 @@ export function registerWalletTools(server) {
|
|
|
287
293
|
if (pendingToken) {
|
|
288
294
|
const result = await pollCardSetup(pendingToken, 250);
|
|
289
295
|
if (result) {
|
|
290
|
-
const principal = await
|
|
296
|
+
const principal = await ensureConsumerPrincipalForMethod("card");
|
|
291
297
|
return text(`Connected! ${result.brand} ****${result.last4} is ready for payments.\n\n` +
|
|
292
298
|
`Consumer principal: ${principal}`);
|
|
293
299
|
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { MCP_PACKAGE_VERSION } from "../version.js";
|
|
2
3
|
|
|
3
4
|
const {
|
|
4
5
|
mockGetApiUrl,
|
|
5
6
|
mockGetApiKey,
|
|
6
7
|
mockGetPaymentFetch,
|
|
7
8
|
mockEnsureConsumerPrincipalForMethod,
|
|
9
|
+
mockEnsureBaseRebatePrincipal,
|
|
8
10
|
mockGetConsumerPrincipalForMethod,
|
|
9
11
|
mockGetBaseRebatePrincipal,
|
|
10
12
|
mockPaymentFetch,
|
|
@@ -13,6 +15,7 @@ const {
|
|
|
13
15
|
mockGetApiKey: vi.fn(),
|
|
14
16
|
mockGetPaymentFetch: vi.fn(),
|
|
15
17
|
mockEnsureConsumerPrincipalForMethod: vi.fn(),
|
|
18
|
+
mockEnsureBaseRebatePrincipal: vi.fn(),
|
|
16
19
|
mockGetConsumerPrincipalForMethod: vi.fn(),
|
|
17
20
|
mockGetBaseRebatePrincipal: vi.fn(),
|
|
18
21
|
mockPaymentFetch: vi.fn(),
|
|
@@ -29,6 +32,7 @@ vi.mock("../payments.js", () => ({
|
|
|
29
32
|
|
|
30
33
|
vi.mock("../principal.js", () => ({
|
|
31
34
|
ensureConsumerPrincipal: vi.fn(),
|
|
35
|
+
ensureBaseRebatePrincipal: (...args: unknown[]) => mockEnsureBaseRebatePrincipal(...args),
|
|
32
36
|
ensureConsumerPrincipalForMethod: (...args: unknown[]) => mockEnsureConsumerPrincipalForMethod(...args),
|
|
33
37
|
getConsumerPrincipal: vi.fn(),
|
|
34
38
|
getConsumerPrincipalForMethod: (...args: unknown[]) => mockGetConsumerPrincipalForMethod(...args),
|
|
@@ -50,6 +54,9 @@ describe("api-client headers", () => {
|
|
|
50
54
|
mockGetBaseRebatePrincipal.mockResolvedValue(
|
|
51
55
|
"did:pkh:eip155:8453:0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
|
52
56
|
);
|
|
57
|
+
mockEnsureBaseRebatePrincipal.mockResolvedValue(
|
|
58
|
+
"did:pkh:eip155:8453:0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
|
59
|
+
);
|
|
53
60
|
mockPaymentFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), {
|
|
54
61
|
status: 200,
|
|
55
62
|
headers: { "content-type": "application/json" },
|
|
@@ -72,7 +79,7 @@ describe("api-client headers", () => {
|
|
|
72
79
|
"X-AW-Rebate-Principal":
|
|
73
80
|
"did:pkh:eip155:8453:0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
|
74
81
|
"X-AW-Surface": "mcp",
|
|
75
|
-
"X-AW-MCP-Version":
|
|
82
|
+
"X-AW-MCP-Version": MCP_PACKAGE_VERSION,
|
|
76
83
|
"X-AW-MCP-Tool": "run_agent",
|
|
77
84
|
"X-AW-MCP-Action": "execute",
|
|
78
85
|
}),
|
|
@@ -26,6 +26,7 @@ vi.mock("../config.js", () => ({
|
|
|
26
26
|
getApiUrl: () => "http://api.test",
|
|
27
27
|
getCardConfig: () => currentCard,
|
|
28
28
|
getLinkConfig: () => currentLink,
|
|
29
|
+
getLinkCooldown: () => null,
|
|
29
30
|
getConfig: () => ({ defaultPaymentMethod: currentDefaultPaymentMethod }),
|
|
30
31
|
getDefaultWallet: () => currentDefaultWallet,
|
|
31
32
|
getWallets: () => currentWallets,
|
|
@@ -166,17 +167,50 @@ describe("payment method initialization", () => {
|
|
|
166
167
|
|
|
167
168
|
expect(mockCreateLinkSharedPaymentToken).toHaveBeenCalledWith(
|
|
168
169
|
expect.objectContaining({
|
|
169
|
-
amount: "
|
|
170
|
+
amount: "25",
|
|
170
171
|
currency: "usd",
|
|
171
172
|
networkId: "profile_test",
|
|
172
173
|
paymentMethodId: "csmrpd_link_123",
|
|
173
174
|
}),
|
|
174
175
|
);
|
|
175
176
|
const linkTokenRequest = mockCreateLinkSharedPaymentToken.mock.calls[0]?.[0] as { context: string } | undefined;
|
|
176
|
-
expect(linkTokenRequest?.context).toContain("up to USD
|
|
177
|
+
expect(linkTokenRequest?.context).toContain("up to USD 0.25");
|
|
177
178
|
expect(linkTokenRequest?.context).toContain("quoted at USD 0.25");
|
|
178
179
|
});
|
|
179
180
|
|
|
181
|
+
it("uses an explicit Link approval override when configured", async () => {
|
|
182
|
+
process.env.AGENTWONDERLAND_LINK_APPROVAL_LIMIT_CENTS = "2000";
|
|
183
|
+
currentLink = {
|
|
184
|
+
paymentMethodId: "csmrpd_link_123",
|
|
185
|
+
label: "Visa ****4242",
|
|
186
|
+
};
|
|
187
|
+
currentDefaultPaymentMethod = "link";
|
|
188
|
+
|
|
189
|
+
const { getPaymentFetch } = await import("../payments.js");
|
|
190
|
+
await getPaymentFetch("link");
|
|
191
|
+
|
|
192
|
+
const stripeConfig = mockStripe.mock.calls[0]?.[0] as {
|
|
193
|
+
createToken: (params: {
|
|
194
|
+
amount: string;
|
|
195
|
+
currency: string;
|
|
196
|
+
expiresAt: number;
|
|
197
|
+
networkId: string;
|
|
198
|
+
}) => Promise<string>;
|
|
199
|
+
};
|
|
200
|
+
await stripeConfig.createToken({
|
|
201
|
+
amount: "25",
|
|
202
|
+
currency: "usd",
|
|
203
|
+
expiresAt: 1778290000,
|
|
204
|
+
networkId: "profile_test",
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
expect(mockCreateLinkSharedPaymentToken).toHaveBeenCalledWith(
|
|
208
|
+
expect.objectContaining({
|
|
209
|
+
amount: "2000",
|
|
210
|
+
}),
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
|
|
180
214
|
it("allows a low env override while never approving below the quoted amount", async () => {
|
|
181
215
|
process.env.AGENTWONDERLAND_LINK_APPROVAL_LIMIT_CENTS = "10";
|
|
182
216
|
currentLink = {
|
|
@@ -137,6 +137,22 @@ describe("consumer principal helpers", () => {
|
|
|
137
137
|
expect(state.addedWallets).toHaveLength(1);
|
|
138
138
|
});
|
|
139
139
|
|
|
140
|
+
it("creates an EVM identity for Link payments when only a Solana wallet exists", async () => {
|
|
141
|
+
state.wallets = [
|
|
142
|
+
{ id: "sol-wallet", keyType: "ows", owsWalletId: "ows-sol", chains: ["solana"], defaultChain: "solana" },
|
|
143
|
+
];
|
|
144
|
+
state.addresses = {
|
|
145
|
+
"sol-wallet": "42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF",
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const { ensureConsumerPrincipalForMethod } = await import("../principal.js");
|
|
149
|
+
const principal = await ensureConsumerPrincipalForMethod("link");
|
|
150
|
+
|
|
151
|
+
expect(principal).toMatch(/^did:pkh:eip155:8453:0x[a-f0-9]{40}$/);
|
|
152
|
+
expect(state.addedWallets).toHaveLength(1);
|
|
153
|
+
expect(state.addedWallets[0]?.chains).toEqual(["tempo", "base"]);
|
|
154
|
+
});
|
|
155
|
+
|
|
140
156
|
it("returns the Base rebate principal when an EVM wallet is available", async () => {
|
|
141
157
|
state.wallets = [
|
|
142
158
|
{ id: "aw-main", keyType: "ows", owsWalletId: "ows-main", chains: ["tempo", "base", "solana"], defaultChain: "tempo" },
|
|
@@ -152,4 +168,19 @@ describe("consumer principal helpers", () => {
|
|
|
152
168
|
|
|
153
169
|
expect(principal).toBe("did:pkh:eip155:8453:0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
|
|
154
170
|
});
|
|
171
|
+
|
|
172
|
+
it("ensures a Base rebate principal even when the default wallet is Solana-only", async () => {
|
|
173
|
+
state.wallets = [
|
|
174
|
+
{ id: "sol-wallet", keyType: "ows", owsWalletId: "ows-sol", chains: ["solana"], defaultChain: "solana" },
|
|
175
|
+
];
|
|
176
|
+
state.addresses = {
|
|
177
|
+
"sol-wallet": "42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF",
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const { ensureBaseRebatePrincipal } = await import("../principal.js");
|
|
181
|
+
const principal = await ensureBaseRebatePrincipal();
|
|
182
|
+
|
|
183
|
+
expect(principal).toMatch(/^did:pkh:eip155:8453:0x[a-f0-9]{40}$/);
|
|
184
|
+
expect(state.addedWallets).toHaveLength(1);
|
|
185
|
+
});
|
|
155
186
|
});
|
package/src/core/api-client.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { getApiUrl, getApiKey } from "./config.js";
|
|
|
2
2
|
import { getPaymentFetch } from "./payments.js";
|
|
3
3
|
import {
|
|
4
4
|
getBaseRebatePrincipal,
|
|
5
|
-
|
|
5
|
+
ensureBaseRebatePrincipal,
|
|
6
6
|
ensureConsumerPrincipalForMethod,
|
|
7
7
|
getConsumerPrincipal,
|
|
8
8
|
getConsumerPrincipalForMethod,
|
|
@@ -70,7 +70,9 @@ async function buildHeaders(path: string, method: string, options?: RequestOptio
|
|
|
70
70
|
headers["X-AW-Consumer-Principal"] = principal;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
const rebatePrincipal =
|
|
73
|
+
const rebatePrincipal = options?.ensureConsumerPrincipal
|
|
74
|
+
? await ensureBaseRebatePrincipal()
|
|
75
|
+
: await getBaseRebatePrincipal();
|
|
74
76
|
if (rebatePrincipal) {
|
|
75
77
|
headers["X-AW-Rebate-Principal"] = rebatePrincipal;
|
|
76
78
|
}
|
package/src/core/config.ts
CHANGED
|
@@ -32,6 +32,24 @@ export interface PendingLinkSetup {
|
|
|
32
32
|
createdAt: string;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
export interface PendingLinkSpendRequest {
|
|
36
|
+
id: string;
|
|
37
|
+
approvalUrl?: string;
|
|
38
|
+
amount: string;
|
|
39
|
+
currency: string;
|
|
40
|
+
context: string;
|
|
41
|
+
expiresAt: number;
|
|
42
|
+
networkId: string;
|
|
43
|
+
paymentMethodId: string;
|
|
44
|
+
createdAt: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface LinkCooldown {
|
|
48
|
+
reason: string;
|
|
49
|
+
createdAt: string;
|
|
50
|
+
blockedUntil: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
35
53
|
export interface SpendPolicy {
|
|
36
54
|
maxPerTxUsd?: number;
|
|
37
55
|
maxPerDayUsd?: number;
|
|
@@ -55,6 +73,8 @@ export interface Config {
|
|
|
55
73
|
card: CardConfig | null;
|
|
56
74
|
link: LinkConfig | null;
|
|
57
75
|
pendingLinkSetup?: PendingLinkSetup | null;
|
|
76
|
+
pendingLinkSpendRequest?: PendingLinkSpendRequest | null;
|
|
77
|
+
linkCooldown?: LinkCooldown | null;
|
|
58
78
|
pendingCardSetupToken?: string | null;
|
|
59
79
|
favorites: string[];
|
|
60
80
|
/** Require user confirmation before spending. Default: true. Set false for headless/automated use. */
|
|
@@ -113,6 +133,8 @@ interface LegacyConfig {
|
|
|
113
133
|
card?: CardConfig | null;
|
|
114
134
|
link?: LinkConfig | null;
|
|
115
135
|
pendingLinkSetup?: PendingLinkSetup | null;
|
|
136
|
+
pendingLinkSpendRequest?: PendingLinkSpendRequest | null;
|
|
137
|
+
linkCooldown?: LinkCooldown | null;
|
|
116
138
|
pendingCardSetupToken?: string | null;
|
|
117
139
|
spendPolicies?: Record<string, SpendPolicy>;
|
|
118
140
|
spendLedger?: SpendLedgerEntry[];
|
|
@@ -136,6 +158,8 @@ function migrateIfNeeded(raw: LegacyConfig): Config {
|
|
|
136
158
|
card: raw.card ?? null,
|
|
137
159
|
link: raw.link ?? null,
|
|
138
160
|
pendingLinkSetup: raw.link ? null : (raw.pendingLinkSetup ?? null),
|
|
161
|
+
pendingLinkSpendRequest: raw.pendingLinkSpendRequest ?? null,
|
|
162
|
+
linkCooldown: raw.linkCooldown ?? null,
|
|
139
163
|
pendingCardSetupToken: (r.pendingCardSetupToken as string | null | undefined) ?? null,
|
|
140
164
|
favorites: r.favorites as string[] ?? [],
|
|
141
165
|
confirmBeforeSpend: r.confirmBeforeSpend !== false,
|
|
@@ -214,6 +238,8 @@ function migrateIfNeeded(raw: LegacyConfig): Config {
|
|
|
214
238
|
card,
|
|
215
239
|
link: raw.link ?? null,
|
|
216
240
|
pendingLinkSetup: raw.link ? null : (raw.pendingLinkSetup ?? null),
|
|
241
|
+
pendingLinkSpendRequest: raw.pendingLinkSpendRequest ?? null,
|
|
242
|
+
linkCooldown: raw.linkCooldown ?? null,
|
|
217
243
|
pendingCardSetupToken: null,
|
|
218
244
|
favorites: [],
|
|
219
245
|
confirmBeforeSpend: true,
|
|
@@ -243,6 +269,8 @@ export function getConfig(): Config {
|
|
|
243
269
|
card: null,
|
|
244
270
|
link: null,
|
|
245
271
|
pendingLinkSetup: null,
|
|
272
|
+
pendingLinkSpendRequest: null,
|
|
273
|
+
linkCooldown: null,
|
|
246
274
|
pendingCardSetupToken: null,
|
|
247
275
|
favorites: [],
|
|
248
276
|
confirmBeforeSpend: true,
|
|
@@ -432,6 +460,22 @@ export function getPendingLinkSetup(): PendingLinkSetup | null {
|
|
|
432
460
|
return getConfig().pendingLinkSetup ?? null;
|
|
433
461
|
}
|
|
434
462
|
|
|
463
|
+
export function getPendingLinkSpendRequest(): PendingLinkSpendRequest | null {
|
|
464
|
+
return getConfig().pendingLinkSpendRequest ?? null;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export function setPendingLinkSpendRequest(pendingLinkSpendRequest: PendingLinkSpendRequest | null): void {
|
|
468
|
+
saveConfig({ pendingLinkSpendRequest });
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export function getLinkCooldown(): LinkCooldown | null {
|
|
472
|
+
return getConfig().linkCooldown ?? null;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export function setLinkCooldown(linkCooldown: LinkCooldown | null): void {
|
|
476
|
+
saveConfig({ linkCooldown });
|
|
477
|
+
}
|
|
478
|
+
|
|
435
479
|
/**
|
|
436
480
|
* Save card configuration after setup.
|
|
437
481
|
*/
|
|
@@ -461,11 +505,13 @@ export function setLinkConfig(link: LinkConfig | null): void {
|
|
|
461
505
|
link,
|
|
462
506
|
defaultPaymentMethod: "link",
|
|
463
507
|
pendingLinkSetup: null,
|
|
508
|
+
pendingLinkSpendRequest: null,
|
|
464
509
|
});
|
|
465
510
|
} else {
|
|
466
511
|
saveConfig({
|
|
467
512
|
link,
|
|
468
513
|
pendingLinkSetup: null,
|
|
514
|
+
pendingLinkSpendRequest: null,
|
|
469
515
|
defaultPaymentMethod: current.defaultPaymentMethod === "link"
|
|
470
516
|
? undefined
|
|
471
517
|
: current.defaultPaymentMethod,
|
package/src/core/link-cli.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
|
+
import {
|
|
4
|
+
getPendingLinkSpendRequest,
|
|
5
|
+
setLinkCooldown,
|
|
6
|
+
setPendingLinkSpendRequest,
|
|
7
|
+
type PendingLinkSpendRequest,
|
|
8
|
+
} from "./config.js";
|
|
3
9
|
|
|
4
10
|
const execFileAsync = promisify(execFile);
|
|
5
11
|
const LINK_CLI_PACKAGE = "@stripe/link-cli";
|
|
@@ -116,6 +122,40 @@ function extractSpendRequestApproval(output: unknown): { id: string; approvalUrl
|
|
|
116
122
|
return null;
|
|
117
123
|
}
|
|
118
124
|
|
|
125
|
+
function isMatchingPendingSpendRequest(
|
|
126
|
+
pending: PendingLinkSpendRequest | null,
|
|
127
|
+
params: {
|
|
128
|
+
amount: string;
|
|
129
|
+
currency: string;
|
|
130
|
+
context: string;
|
|
131
|
+
expiresAt: number;
|
|
132
|
+
networkId: string;
|
|
133
|
+
paymentMethodId: string;
|
|
134
|
+
},
|
|
135
|
+
): pending is PendingLinkSpendRequest {
|
|
136
|
+
if (!pending) return false;
|
|
137
|
+
if (pending.amount !== params.amount) return false;
|
|
138
|
+
if (pending.currency.toLowerCase() !== params.currency.toLowerCase()) return false;
|
|
139
|
+
if (pending.context !== params.context) return false;
|
|
140
|
+
if (pending.networkId !== params.networkId) return false;
|
|
141
|
+
if (pending.paymentMethodId !== params.paymentMethodId) return false;
|
|
142
|
+
if (pending.expiresAt <= Math.floor(Date.now() / 1000)) return false;
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function isProjectedSpendCapError(message: string): boolean {
|
|
147
|
+
return /projected daily spend|projected spend|exceeds limit/i.test(message);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function recordLinkCooldown(reason: string): void {
|
|
151
|
+
const now = new Date();
|
|
152
|
+
setLinkCooldown({
|
|
153
|
+
reason,
|
|
154
|
+
createdAt: now.toISOString(),
|
|
155
|
+
blockedUntil: new Date(now.getTime() + 24 * 60 * 60 * 1000).toISOString(),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
119
159
|
function normalizePaymentMethods(output: unknown): LinkCliPaymentMethod[] {
|
|
120
160
|
const values = Array.isArray(output)
|
|
121
161
|
? output
|
|
@@ -215,6 +255,22 @@ export async function createLinkSharedPaymentToken(params: {
|
|
|
215
255
|
networkId: string;
|
|
216
256
|
paymentMethodId: string;
|
|
217
257
|
}): Promise<string> {
|
|
258
|
+
const existing = getPendingLinkSpendRequest();
|
|
259
|
+
if (isMatchingPendingSpendRequest(existing, params)) {
|
|
260
|
+
try {
|
|
261
|
+
console.error(`Resuming pending Link approval: ${existing.id}`);
|
|
262
|
+
const spt = await retrieveSharedPaymentToken(existing.id, existing.approvalUrl);
|
|
263
|
+
setPendingLinkSpendRequest(null);
|
|
264
|
+
return spt;
|
|
265
|
+
} catch (err) {
|
|
266
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
267
|
+
if (!/denied|expired|not found|without a shared payment token|POLLING_TIMEOUT/i.test(message)) {
|
|
268
|
+
throw err;
|
|
269
|
+
}
|
|
270
|
+
setPendingLinkSpendRequest(null);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
218
274
|
const args = [
|
|
219
275
|
"spend-request",
|
|
220
276
|
"create",
|
|
@@ -242,6 +298,18 @@ export async function createLinkSharedPaymentToken(params: {
|
|
|
242
298
|
output = await runLinkCli(args);
|
|
243
299
|
} catch (err) {
|
|
244
300
|
const message = err instanceof Error ? err.message : String(err);
|
|
301
|
+
if (isProjectedSpendCapError(message)) {
|
|
302
|
+
recordLinkCooldown(message);
|
|
303
|
+
throw new Error(
|
|
304
|
+
[
|
|
305
|
+
"Link is temporarily blocked by Stripe's projected-spend cap.",
|
|
306
|
+
"Reauthing Link or switching cards in the same Link account will not fix this.",
|
|
307
|
+
"Use USDC for now, wait for the rolling Link window to clear, or ask Stripe to raise/clear the merchant projected-spend cap.",
|
|
308
|
+
"",
|
|
309
|
+
message,
|
|
310
|
+
].join("\n"),
|
|
311
|
+
);
|
|
312
|
+
}
|
|
245
313
|
if (/invalid network_id|could not retrieve merchant information/i.test(message)) {
|
|
246
314
|
throw new Error(
|
|
247
315
|
[
|
|
@@ -256,45 +324,63 @@ export async function createLinkSharedPaymentToken(params: {
|
|
|
256
324
|
}
|
|
257
325
|
const spt = extractSharedPaymentToken(output);
|
|
258
326
|
if (spt) {
|
|
327
|
+
setPendingLinkSpendRequest(null);
|
|
259
328
|
return spt;
|
|
260
329
|
}
|
|
261
330
|
|
|
262
331
|
const approval = extractSpendRequestApproval(output);
|
|
263
332
|
if (approval?.id && approval.status === "pending_approval") {
|
|
333
|
+
setPendingLinkSpendRequest({
|
|
334
|
+
id: approval.id,
|
|
335
|
+
approvalUrl: approval.approvalUrl,
|
|
336
|
+
amount: params.amount,
|
|
337
|
+
currency: params.currency,
|
|
338
|
+
context: params.context,
|
|
339
|
+
expiresAt: params.expiresAt,
|
|
340
|
+
networkId: params.networkId,
|
|
341
|
+
paymentMethodId: params.paymentMethodId,
|
|
342
|
+
createdAt: new Date().toISOString(),
|
|
343
|
+
});
|
|
264
344
|
if (approval.approvalUrl) {
|
|
265
345
|
console.error(`Link approval required: ${approval.approvalUrl}`);
|
|
266
346
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
approval.id,
|
|
271
|
-
"--interval",
|
|
272
|
-
"2",
|
|
273
|
-
"--max-attempts",
|
|
274
|
-
"150",
|
|
275
|
-
]);
|
|
276
|
-
let retrievedSpt = extractSharedPaymentToken(retrieved);
|
|
277
|
-
for (let attempt = 0; !retrievedSpt && attempt < 30; attempt += 1) {
|
|
278
|
-
await sleep(2_000);
|
|
279
|
-
retrieved = await runLinkCli([
|
|
280
|
-
"spend-request",
|
|
281
|
-
"retrieve",
|
|
282
|
-
approval.id,
|
|
283
|
-
]);
|
|
284
|
-
retrievedSpt = extractSharedPaymentToken(retrieved);
|
|
285
|
-
}
|
|
286
|
-
if (retrievedSpt) {
|
|
287
|
-
return retrievedSpt;
|
|
288
|
-
}
|
|
289
|
-
throw new Error(
|
|
290
|
-
[
|
|
291
|
-
"Link spend request finished without a shared payment token.",
|
|
292
|
-
approval.approvalUrl ? `Approval URL: ${approval.approvalUrl}` : undefined,
|
|
293
|
-
].filter(Boolean).join("\n"),
|
|
294
|
-
);
|
|
347
|
+
const retrievedSpt = await retrieveSharedPaymentToken(approval.id, approval.approvalUrl);
|
|
348
|
+
setPendingLinkSpendRequest(null);
|
|
349
|
+
return retrievedSpt;
|
|
295
350
|
}
|
|
296
351
|
|
|
297
352
|
{
|
|
298
353
|
throw new Error("Link spend request completed without a shared payment token in the CLI response.");
|
|
299
354
|
}
|
|
300
355
|
}
|
|
356
|
+
|
|
357
|
+
async function retrieveSharedPaymentToken(spendRequestId: string, approvalUrl?: string): Promise<string> {
|
|
358
|
+
let retrieved = await runLinkCli([
|
|
359
|
+
"spend-request",
|
|
360
|
+
"retrieve",
|
|
361
|
+
spendRequestId,
|
|
362
|
+
"--interval",
|
|
363
|
+
"2",
|
|
364
|
+
"--max-attempts",
|
|
365
|
+
"150",
|
|
366
|
+
]);
|
|
367
|
+
let retrievedSpt = extractSharedPaymentToken(retrieved);
|
|
368
|
+
for (let attempt = 0; !retrievedSpt && attempt < 30; attempt += 1) {
|
|
369
|
+
await sleep(2_000);
|
|
370
|
+
retrieved = await runLinkCli([
|
|
371
|
+
"spend-request",
|
|
372
|
+
"retrieve",
|
|
373
|
+
spendRequestId,
|
|
374
|
+
]);
|
|
375
|
+
retrievedSpt = extractSharedPaymentToken(retrieved);
|
|
376
|
+
}
|
|
377
|
+
if (retrievedSpt) {
|
|
378
|
+
return retrievedSpt;
|
|
379
|
+
}
|
|
380
|
+
throw new Error(
|
|
381
|
+
[
|
|
382
|
+
"Link spend request finished without a shared payment token.",
|
|
383
|
+
approvalUrl ? `Approval URL: ${approvalUrl}` : undefined,
|
|
384
|
+
].filter(Boolean).join("\n"),
|
|
385
|
+
);
|
|
386
|
+
}
|