@agentwonderland/mcp 0.1.23 → 0.1.25
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__/amount-utils.test.d.ts +1 -0
- package/dist/core/__tests__/amount-utils.test.js +11 -0
- package/dist/core/__tests__/card-setup.test.d.ts +1 -0
- package/dist/core/__tests__/card-setup.test.js +99 -0
- package/dist/core/__tests__/formatters.test.d.ts +1 -0
- package/dist/core/__tests__/formatters.test.js +15 -0
- package/dist/core/__tests__/passes.test.d.ts +1 -0
- package/dist/core/__tests__/passes.test.js +82 -0
- package/dist/core/__tests__/payments.test.d.ts +1 -0
- package/dist/core/__tests__/payments.test.js +101 -0
- package/dist/core/__tests__/principal.test.d.ts +1 -0
- package/dist/core/__tests__/principal.test.js +67 -0
- package/dist/core/__tests__/spend-policy.test.d.ts +1 -0
- package/dist/core/__tests__/spend-policy.test.js +40 -0
- package/dist/core/amount-utils.d.ts +1 -0
- package/dist/core/amount-utils.js +4 -0
- package/dist/core/api-client.d.ts +9 -4
- package/dist/core/api-client.js +52 -22
- package/dist/core/base-charge.js +3 -2
- package/dist/core/card-setup.d.ts +20 -13
- package/dist/core/card-setup.js +85 -29
- package/dist/core/config.d.ts +22 -0
- package/dist/core/config.js +46 -2
- package/dist/core/formatters.d.ts +4 -3
- package/dist/core/formatters.js +10 -8
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +2 -0
- package/dist/core/ows-adapter.d.ts +10 -2
- package/dist/core/ows-adapter.js +54 -10
- package/dist/core/passes.d.ts +40 -0
- package/dist/core/passes.js +32 -0
- package/dist/core/payments.d.ts +8 -0
- package/dist/core/payments.js +111 -17
- package/dist/core/principal.d.ts +2 -0
- package/dist/core/principal.js +109 -0
- package/dist/core/solana-charge.d.ts +9 -0
- package/dist/core/solana-charge.js +96 -0
- package/dist/core/spend-policy.d.ts +12 -0
- package/dist/core/spend-policy.js +53 -0
- package/dist/core/types.d.ts +11 -2
- package/dist/index.js +11 -3
- package/dist/prompts/index.js +4 -2
- package/dist/resources/agents.js +1 -1
- package/dist/resources/wallet.js +8 -1
- package/dist/tools/__tests__/_payment-confirmation.test.d.ts +1 -0
- package/dist/tools/__tests__/_payment-confirmation.test.js +30 -0
- package/dist/tools/_payment-confirmation.d.ts +6 -0
- package/dist/tools/_payment-confirmation.js +28 -0
- package/dist/tools/agent-info.js +16 -2
- package/dist/tools/favorites.js +1 -1
- 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/passes.d.ts +2 -0
- package/dist/tools/passes.js +157 -0
- package/dist/tools/run.js +127 -53
- package/dist/tools/solve.js +115 -51
- package/dist/tools/wallet.js +110 -59
- package/package.json +3 -1
- package/src/core/__tests__/amount-utils.test.ts +13 -0
- package/src/core/__tests__/card-setup.test.ts +118 -0
- package/src/core/__tests__/formatters.test.ts +17 -0
- package/src/core/__tests__/passes.test.ts +94 -0
- package/src/core/__tests__/payments.test.ts +122 -0
- package/src/core/__tests__/principal.test.ts +87 -0
- package/src/core/__tests__/spend-policy.test.ts +58 -0
- package/src/core/amount-utils.ts +5 -0
- package/src/core/api-client.ts +70 -23
- package/src/core/base-charge.ts +3 -2
- package/src/core/card-setup.ts +109 -34
- package/src/core/config.ts +74 -3
- package/src/core/formatters.ts +13 -9
- package/src/core/index.ts +2 -0
- package/src/core/ows-adapter.ts +74 -8
- package/src/core/passes.ts +74 -0
- package/src/core/payments.ts +130 -17
- package/src/core/principal.ts +128 -0
- package/src/core/solana-charge.ts +150 -0
- package/src/core/spend-policy.ts +69 -0
- package/src/core/types.ts +11 -2
- package/src/index.ts +11 -3
- package/src/prompts/index.ts +4 -2
- package/src/resources/agents.ts +1 -1
- package/src/resources/wallet.ts +8 -1
- package/src/tools/__tests__/_payment-confirmation.test.ts +45 -0
- package/src/tools/_payment-confirmation.ts +52 -0
- package/src/tools/agent-info.ts +25 -2
- package/src/tools/favorites.ts +1 -4
- package/src/tools/index.ts +1 -0
- package/src/tools/observability.ts +43 -0
- package/src/tools/passes.ts +228 -0
- package/src/tools/run.ts +174 -57
- package/src/tools/solve.ts +147 -59
- package/src/tools/wallet.ts +132 -62
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const mockApiGet = vi.fn();
|
|
4
|
+
const mockApiPost = vi.fn();
|
|
5
|
+
|
|
6
|
+
class MockApiError extends Error {
|
|
7
|
+
constructor(public status: number, message: string) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "ApiError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let pendingToken: string | null = null;
|
|
14
|
+
const savedCards: unknown[] = [];
|
|
15
|
+
|
|
16
|
+
vi.mock("../api-client.js", () => ({
|
|
17
|
+
ApiError: MockApiError,
|
|
18
|
+
apiGet: (...args: unknown[]) => mockApiGet(...args),
|
|
19
|
+
apiPost: (...args: unknown[]) => mockApiPost(...args),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
vi.mock("../config.js", () => ({
|
|
23
|
+
getApiUrl: () => "http://api.test",
|
|
24
|
+
getPendingCardSetupToken: () => pendingToken,
|
|
25
|
+
setCardConfig: (card: unknown) => {
|
|
26
|
+
savedCards.push(card);
|
|
27
|
+
},
|
|
28
|
+
setPendingCardSetupToken: (token: string | null) => {
|
|
29
|
+
pendingToken = token;
|
|
30
|
+
},
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
describe("card setup helpers", () => {
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
vi.clearAllMocks();
|
|
36
|
+
pendingToken = null;
|
|
37
|
+
savedCards.length = 0;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("reuses the existing pending setup token", async () => {
|
|
41
|
+
pendingToken = "tok_existing";
|
|
42
|
+
const { getOrCreatePendingCardSetup } = await import("../card-setup.js");
|
|
43
|
+
|
|
44
|
+
const result = await getOrCreatePendingCardSetup();
|
|
45
|
+
|
|
46
|
+
expect(mockApiPost).not.toHaveBeenCalled();
|
|
47
|
+
expect(result).toMatchObject({
|
|
48
|
+
token: "tok_existing",
|
|
49
|
+
url: "http://api.test/card/handoff/tok_existing",
|
|
50
|
+
isNew: false,
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("stores a newly created setup token for later resume", async () => {
|
|
55
|
+
mockApiPost.mockResolvedValueOnce({ token: "tok_new" });
|
|
56
|
+
const { getOrCreatePendingCardSetup } = await import("../card-setup.js");
|
|
57
|
+
|
|
58
|
+
const result = await getOrCreatePendingCardSetup();
|
|
59
|
+
|
|
60
|
+
expect(mockApiPost).toHaveBeenCalledWith("/card/setup", {});
|
|
61
|
+
expect(pendingToken).toBe("tok_new");
|
|
62
|
+
expect(result).toMatchObject({
|
|
63
|
+
token: "tok_new",
|
|
64
|
+
url: "http://api.test/card/handoff/tok_new",
|
|
65
|
+
isNew: true,
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("persists the card and clears pending setup on successful completion", async () => {
|
|
70
|
+
pendingToken = "tok_ready";
|
|
71
|
+
mockApiGet.mockResolvedValueOnce({
|
|
72
|
+
status: "complete",
|
|
73
|
+
card_last4: "4242",
|
|
74
|
+
card_brand: "visa",
|
|
75
|
+
consumer_token: "consumer_123",
|
|
76
|
+
payment_method_id: "pm_123",
|
|
77
|
+
});
|
|
78
|
+
const { pollCardSetup } = await import("../card-setup.js");
|
|
79
|
+
|
|
80
|
+
const result = await pollCardSetup("tok_ready", 50);
|
|
81
|
+
|
|
82
|
+
expect(result).toEqual({
|
|
83
|
+
last4: "4242",
|
|
84
|
+
brand: "visa",
|
|
85
|
+
consumerToken: "consumer_123",
|
|
86
|
+
});
|
|
87
|
+
expect(savedCards).toEqual([{
|
|
88
|
+
consumerToken: "consumer_123",
|
|
89
|
+
paymentMethodId: "pm_123",
|
|
90
|
+
last4: "4242",
|
|
91
|
+
brand: "visa",
|
|
92
|
+
}]);
|
|
93
|
+
expect(pendingToken).toBeNull();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("clears expired pending setup tokens", async () => {
|
|
97
|
+
pendingToken = "tok_expired";
|
|
98
|
+
mockApiGet.mockRejectedValueOnce(new MockApiError(404, "Setup not found"));
|
|
99
|
+
const { pollCardSetup } = await import("../card-setup.js");
|
|
100
|
+
|
|
101
|
+
const result = await pollCardSetup("tok_expired", 50);
|
|
102
|
+
|
|
103
|
+
expect(result).toBeNull();
|
|
104
|
+
expect(pendingToken).toBeNull();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("formats card setup as a single visible message with the setup page URL", async () => {
|
|
108
|
+
const { formatCardSetupBlocks } = await import("../card-setup.js");
|
|
109
|
+
|
|
110
|
+
const blocks = formatCardSetupBlocks("http://api.test/card/handoff/tok");
|
|
111
|
+
|
|
112
|
+
expect(blocks).toHaveLength(1);
|
|
113
|
+
expect(blocks[0]).toContain("Open this setup page to connect your card:");
|
|
114
|
+
expect(blocks[0]).toContain("http://api.test/card/handoff/tok");
|
|
115
|
+
expect(blocks[0]).toContain("securely save the card");
|
|
116
|
+
expect(blocks[0]).not.toContain("QR code above");
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { formatRunResult } from "../formatters.js";
|
|
3
|
+
|
|
4
|
+
describe("formatRunResult", () => {
|
|
5
|
+
it("does not show a paid line for credit-pack-backed runs with zero cost", () => {
|
|
6
|
+
const output = formatRunResult({
|
|
7
|
+
agent_name: "Credit Pack Agent",
|
|
8
|
+
status: "success",
|
|
9
|
+
cost: 0,
|
|
10
|
+
consumption_mode: "credit_pack",
|
|
11
|
+
credit_pack_id: "pack-123",
|
|
12
|
+
}, { paymentMethod: "card" });
|
|
13
|
+
|
|
14
|
+
expect(output).toContain("Covered by credit pack (pack-123)");
|
|
15
|
+
expect(output).not.toContain("Paid:");
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
formatCreditPack,
|
|
4
|
+
formatCreditPackOffer,
|
|
5
|
+
getActiveCreditPack,
|
|
6
|
+
getCreditPackProgram,
|
|
7
|
+
} from "../passes.js";
|
|
8
|
+
import type { AgentRecord } from "../types.js";
|
|
9
|
+
|
|
10
|
+
const baseAgent: AgentRecord = {
|
|
11
|
+
id: "agent-1",
|
|
12
|
+
name: "Test Agent",
|
|
13
|
+
payment: {
|
|
14
|
+
credit_packs: {
|
|
15
|
+
unit_type: "run",
|
|
16
|
+
packs: [
|
|
17
|
+
{ key: "starter", name: "Starter Pack", included_units: 5, price_usd: "1.00" },
|
|
18
|
+
{ key: "growth", name: "Growth Pack", included_units: 20, price_usd: "3.00" },
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
describe("getCreditPackProgram", () => {
|
|
25
|
+
it("returns the configured credit-pack program from agent metadata", () => {
|
|
26
|
+
expect(getCreditPackProgram(baseAgent)).toEqual({
|
|
27
|
+
unit_type: "run",
|
|
28
|
+
packs: [
|
|
29
|
+
{ key: "starter", name: "Starter Pack", included_units: 5, price_usd: "1.00" },
|
|
30
|
+
{ key: "growth", name: "Growth Pack", included_units: 20, price_usd: "3.00" },
|
|
31
|
+
],
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("formatCreditPackOffer", () => {
|
|
37
|
+
it("shows units and per-unit price", () => {
|
|
38
|
+
expect(formatCreditPackOffer({
|
|
39
|
+
pack_id: "starter",
|
|
40
|
+
label: "Starter Pack",
|
|
41
|
+
included_units: 5,
|
|
42
|
+
price_usd: "1.00",
|
|
43
|
+
effective_price_per_unit_usd: "0.200000",
|
|
44
|
+
})).toContain("Starter Pack");
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("formatCreditPack", () => {
|
|
49
|
+
it("includes the pack status when it is no longer active", () => {
|
|
50
|
+
expect(formatCreditPack({
|
|
51
|
+
id: "pack-1",
|
|
52
|
+
status: "depleted",
|
|
53
|
+
unit_type: "run",
|
|
54
|
+
included_units: 5,
|
|
55
|
+
remaining_units: 0,
|
|
56
|
+
price_usd: "1.00",
|
|
57
|
+
purchased_at: "2026-05-04T15:08:28.700Z",
|
|
58
|
+
pack: { key: "starter", name: "Starter Pack" },
|
|
59
|
+
})).toContain("depleted");
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("getActiveCreditPack", () => {
|
|
64
|
+
it("returns the oldest active balance with remaining units", () => {
|
|
65
|
+
const pack = getActiveCreditPack({
|
|
66
|
+
consumer_principal: "did:pkh:eip155:8453:0x1111111111111111111111111111111111111111",
|
|
67
|
+
offers: [],
|
|
68
|
+
balances: [
|
|
69
|
+
{
|
|
70
|
+
id: "newer",
|
|
71
|
+
status: "active",
|
|
72
|
+
unit_type: "run",
|
|
73
|
+
included_units: 20,
|
|
74
|
+
remaining_units: 20,
|
|
75
|
+
price_usd: "3.00",
|
|
76
|
+
purchased_at: "2026-04-10T00:00:00Z",
|
|
77
|
+
pack: { key: "growth", name: "Growth Pack" },
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: "older",
|
|
81
|
+
status: "active",
|
|
82
|
+
unit_type: "run",
|
|
83
|
+
included_units: 5,
|
|
84
|
+
remaining_units: 2,
|
|
85
|
+
price_usd: "1.00",
|
|
86
|
+
purchased_at: "2026-04-01T00:00:00Z",
|
|
87
|
+
pack: { key: "starter", name: "Starter Pack" },
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(pack?.id).toBe("older");
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const TEST_KEY = "1111111111111111111111111111111111111111111111111111111111111111";
|
|
4
|
+
|
|
5
|
+
let currentCard = {
|
|
6
|
+
consumerToken: "consumer_one",
|
|
7
|
+
paymentMethodId: "pm_one",
|
|
8
|
+
last4: "1111",
|
|
9
|
+
brand: "visa",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
let currentDefaultWallet: any;
|
|
13
|
+
let currentWallets: any[] = [];
|
|
14
|
+
let currentResolvedMethod: { wallet: any; chain: string } | null = null;
|
|
15
|
+
|
|
16
|
+
const createdFetches = [vi.fn(), vi.fn(), vi.fn(), vi.fn()];
|
|
17
|
+
const mockMppxCreate = vi.fn();
|
|
18
|
+
const mockStripe = vi.fn((opts: unknown) => opts);
|
|
19
|
+
const mockTempo = vi.fn((..._args: unknown[]) => "tempo_method");
|
|
20
|
+
const mockBaseChargeClient = vi.fn((..._args: unknown[]) => "base_method");
|
|
21
|
+
|
|
22
|
+
vi.mock("../config.js", () => ({
|
|
23
|
+
getApiUrl: () => "http://api.test",
|
|
24
|
+
getCardConfig: () => currentCard,
|
|
25
|
+
getConfig: () => ({ defaultPaymentMethod: "card" }),
|
|
26
|
+
getDefaultWallet: () => currentDefaultWallet,
|
|
27
|
+
getWallets: () => currentWallets,
|
|
28
|
+
resolveWalletAndChain: () => currentResolvedMethod,
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
vi.mock("mppx/client", () => ({
|
|
32
|
+
Mppx: {
|
|
33
|
+
create: (config: unknown) => mockMppxCreate(config),
|
|
34
|
+
},
|
|
35
|
+
stripe: (config: unknown) => mockStripe(config),
|
|
36
|
+
tempo: (config: unknown) => mockTempo(config),
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
vi.mock("../base-charge.js", () => ({
|
|
40
|
+
baseChargeClient: (config: unknown) => mockBaseChargeClient(config),
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
describe("payment method initialization", () => {
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
vi.clearAllMocks();
|
|
46
|
+
vi.resetModules();
|
|
47
|
+
currentCard = {
|
|
48
|
+
consumerToken: "consumer_one",
|
|
49
|
+
paymentMethodId: "pm_one",
|
|
50
|
+
last4: "1111",
|
|
51
|
+
brand: "visa",
|
|
52
|
+
};
|
|
53
|
+
currentDefaultWallet = undefined;
|
|
54
|
+
currentWallets = [];
|
|
55
|
+
currentResolvedMethod = null;
|
|
56
|
+
mockMppxCreate
|
|
57
|
+
.mockReturnValueOnce({ fetch: createdFetches[0] })
|
|
58
|
+
.mockReturnValueOnce({ fetch: createdFetches[1] })
|
|
59
|
+
.mockReturnValueOnce({ fetch: createdFetches[2] })
|
|
60
|
+
.mockReturnValueOnce({ fetch: createdFetches[3] });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("rebuilds the cached card fetch when the card config changes", async () => {
|
|
64
|
+
const { getPaymentFetch } = await import("../payments.js");
|
|
65
|
+
|
|
66
|
+
const firstFetch = await getPaymentFetch("card");
|
|
67
|
+
currentCard = {
|
|
68
|
+
consumerToken: "consumer_two",
|
|
69
|
+
paymentMethodId: "pm_two",
|
|
70
|
+
last4: "2222",
|
|
71
|
+
brand: "mastercard",
|
|
72
|
+
};
|
|
73
|
+
const secondFetch = await getPaymentFetch("card");
|
|
74
|
+
|
|
75
|
+
expect(firstFetch).not.toBe(secondFetch);
|
|
76
|
+
expect(mockMppxCreate).toHaveBeenCalledTimes(2);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("initializes only the Base method when base is requested", async () => {
|
|
80
|
+
const wallet = {
|
|
81
|
+
id: "aw-main",
|
|
82
|
+
keyType: "raw",
|
|
83
|
+
key: TEST_KEY,
|
|
84
|
+
chains: ["tempo", "base"],
|
|
85
|
+
defaultChain: "base",
|
|
86
|
+
};
|
|
87
|
+
currentDefaultWallet = wallet;
|
|
88
|
+
currentWallets = [wallet];
|
|
89
|
+
currentResolvedMethod = { wallet, chain: "base" };
|
|
90
|
+
|
|
91
|
+
const { getPaymentFetch } = await import("../payments.js");
|
|
92
|
+
await getPaymentFetch("base");
|
|
93
|
+
|
|
94
|
+
expect(mockBaseChargeClient).toHaveBeenCalledTimes(1);
|
|
95
|
+
expect(mockTempo).not.toHaveBeenCalled();
|
|
96
|
+
expect(mockMppxCreate).toHaveBeenCalledWith(
|
|
97
|
+
expect.objectContaining({ methods: ["base_method"] }),
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("initializes only the Tempo method when tempo is requested", async () => {
|
|
102
|
+
const wallet = {
|
|
103
|
+
id: "aw-main",
|
|
104
|
+
keyType: "raw",
|
|
105
|
+
key: TEST_KEY,
|
|
106
|
+
chains: ["tempo", "base"],
|
|
107
|
+
defaultChain: "tempo",
|
|
108
|
+
};
|
|
109
|
+
currentDefaultWallet = wallet;
|
|
110
|
+
currentWallets = [wallet];
|
|
111
|
+
currentResolvedMethod = { wallet, chain: "tempo" };
|
|
112
|
+
|
|
113
|
+
const { getPaymentFetch } = await import("../payments.js");
|
|
114
|
+
await getPaymentFetch("tempo");
|
|
115
|
+
|
|
116
|
+
expect(mockTempo).toHaveBeenCalledTimes(1);
|
|
117
|
+
expect(mockBaseChargeClient).not.toHaveBeenCalled();
|
|
118
|
+
expect(mockMppxCreate).toHaveBeenCalledWith(
|
|
119
|
+
expect.objectContaining({ methods: ["tempo_method"] }),
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const state = vi.hoisted(() => ({
|
|
4
|
+
wallets: [] as Array<{
|
|
5
|
+
id: string;
|
|
6
|
+
keyType: "evm" | "ows";
|
|
7
|
+
key?: string;
|
|
8
|
+
owsWalletId?: string;
|
|
9
|
+
chains: string[];
|
|
10
|
+
defaultChain?: string;
|
|
11
|
+
label?: string;
|
|
12
|
+
}>,
|
|
13
|
+
addresses: {} as Record<string, string | null>,
|
|
14
|
+
addedWallets: [] as Array<Record<string, unknown>>,
|
|
15
|
+
owsAvailable: false,
|
|
16
|
+
owsWallets: [] as Array<{ id: string; name: string; address: string }>,
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
vi.mock("../config.js", () => ({
|
|
20
|
+
addWallet: (wallet: Record<string, unknown>) => {
|
|
21
|
+
state.addedWallets.push(wallet);
|
|
22
|
+
state.wallets.push(wallet as any);
|
|
23
|
+
},
|
|
24
|
+
getDefaultWallet: () => state.wallets[0],
|
|
25
|
+
getWallets: () => state.wallets,
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
vi.mock("../payments.js", () => ({
|
|
29
|
+
getWalletAddress: async (walletId: string) => {
|
|
30
|
+
if (walletId in state.addresses) {
|
|
31
|
+
return state.addresses[walletId] ?? null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const wallet = state.wallets.find((entry) => entry.id === walletId);
|
|
35
|
+
if (wallet?.key) {
|
|
36
|
+
const { privateKeyToAccount } = await import("viem/accounts");
|
|
37
|
+
return privateKeyToAccount(wallet.key as `0x${string}`).address;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return null;
|
|
41
|
+
},
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
vi.mock("../ows-adapter.js", () => ({
|
|
45
|
+
createOwsWallet: async (name: string) => ({
|
|
46
|
+
walletId: `ows-${name}`,
|
|
47
|
+
address: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
48
|
+
}),
|
|
49
|
+
isOwsAvailable: async () => state.owsAvailable,
|
|
50
|
+
listOwsWallets: async () => state.owsWallets,
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
describe("consumer principal helpers", () => {
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
vi.resetModules();
|
|
56
|
+
state.wallets = [];
|
|
57
|
+
state.addresses = {};
|
|
58
|
+
state.addedWallets = [];
|
|
59
|
+
state.owsAvailable = false;
|
|
60
|
+
state.owsWallets = [];
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("prefers an EVM-backed principal when both EVM and Solana wallets exist", async () => {
|
|
64
|
+
state.wallets = [
|
|
65
|
+
{ id: "sol-wallet", keyType: "ows", owsWalletId: "ows-sol", chains: ["solana"], defaultChain: "solana" },
|
|
66
|
+
{ id: "evm-wallet", keyType: "evm", key: "0x1234", chains: ["tempo", "base"], defaultChain: "base" },
|
|
67
|
+
];
|
|
68
|
+
state.addresses = {
|
|
69
|
+
"sol-wallet": "So1ana11111111111111111111111111111111111111",
|
|
70
|
+
"evm-wallet": "0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const { getConsumerPrincipal } = await import("../principal.js");
|
|
74
|
+
const principal = await getConsumerPrincipal();
|
|
75
|
+
|
|
76
|
+
expect(principal).toBe("did:pkh:eip155:8453:0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("creates a fallback EVM identity wallet when none exists", async () => {
|
|
80
|
+
const { ensureConsumerPrincipal } = await import("../principal.js");
|
|
81
|
+
const principal = await ensureConsumerPrincipal();
|
|
82
|
+
|
|
83
|
+
expect(principal).toMatch(/^did:pkh:eip155:8453:0x[a-f0-9]{40}$/);
|
|
84
|
+
expect(state.addedWallets).toHaveLength(1);
|
|
85
|
+
expect(state.addedWallets[0]?.chains).toEqual(["tempo", "base"]);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const state = {
|
|
4
|
+
policies: {} as Record<string, {
|
|
5
|
+
maxPerTxUsd?: number;
|
|
6
|
+
maxPerDayUsd?: number;
|
|
7
|
+
requireConfirmationAboveUsd?: number;
|
|
8
|
+
}>,
|
|
9
|
+
ledger: [] as Array<{ method: string; amountUsd: number; day: string; timestamp: string }>,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
vi.mock("../config.js", () => ({
|
|
13
|
+
getSpendPolicy: (method: string) => state.policies[method] ?? null,
|
|
14
|
+
getSpendLedger: () => state.ledger,
|
|
15
|
+
saveSpendLedger: (entries: Array<{ method: string; amountUsd: number; day: string; timestamp: string }>) => {
|
|
16
|
+
state.ledger = entries;
|
|
17
|
+
},
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
canSpend,
|
|
22
|
+
getDailySpend,
|
|
23
|
+
recordSpend,
|
|
24
|
+
requiresPolicyConfirmation,
|
|
25
|
+
} from "../spend-policy.js";
|
|
26
|
+
|
|
27
|
+
describe("spend-policy", () => {
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
state.policies = {};
|
|
30
|
+
state.ledger = [];
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("allows spend when no policy is set", () => {
|
|
34
|
+
expect(canSpend({ method: "wallet-1", amountUsd: 5 })).toEqual({ ok: true });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("blocks spends above max_per_tx", () => {
|
|
38
|
+
state.policies["wallet-1"] = { maxPerTxUsd: 2 };
|
|
39
|
+
const result = canSpend({ method: "wallet-1", amountUsd: 3 });
|
|
40
|
+
expect(result.ok).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("tracks daily spend totals per method", () => {
|
|
44
|
+
const now = new Date("2026-04-08T12:00:00.000Z");
|
|
45
|
+
recordSpend("wallet-1", 1.25, now);
|
|
46
|
+
recordSpend("wallet-1", 0.75, now);
|
|
47
|
+
recordSpend("wallet-2", 10, now);
|
|
48
|
+
|
|
49
|
+
expect(getDailySpend("wallet-1", now)).toBeCloseTo(2);
|
|
50
|
+
expect(getDailySpend("wallet-2", now)).toBeCloseTo(10);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("requires explicit confirmation above configured threshold", () => {
|
|
54
|
+
state.policies["wallet-1"] = { requireConfirmationAboveUsd: 2 };
|
|
55
|
+
expect(requiresPolicyConfirmation("wallet-1", 1.99)).toBe(false);
|
|
56
|
+
expect(requiresPolicyConfirmation("wallet-1", 2)).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
});
|
package/src/core/api-client.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getApiUrl, getApiKey } from "./config.js";
|
|
2
2
|
import { getPaymentFetch } from "./payments.js";
|
|
3
|
+
import { ensureConsumerPrincipal, getConsumerPrincipal } from "./principal.js";
|
|
3
4
|
|
|
4
5
|
// ── Error class ────────────────────────────────────────────────────
|
|
5
6
|
|
|
@@ -16,7 +17,12 @@ export class ApiError extends Error {
|
|
|
16
17
|
|
|
17
18
|
// ── Internal helpers ───────────────────────────────────────────────
|
|
18
19
|
|
|
19
|
-
|
|
20
|
+
interface RequestOptions {
|
|
21
|
+
ensureConsumerPrincipal?: boolean;
|
|
22
|
+
extraHeaders?: Record<string, string>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function buildHeaders(options?: RequestOptions): Promise<Record<string, string>> {
|
|
20
26
|
const headers: Record<string, string> = {
|
|
21
27
|
"Content-Type": "application/json",
|
|
22
28
|
Accept: "application/json",
|
|
@@ -27,6 +33,17 @@ function buildHeaders(): Record<string, string> {
|
|
|
27
33
|
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
28
34
|
}
|
|
29
35
|
|
|
36
|
+
const principal = options?.ensureConsumerPrincipal
|
|
37
|
+
? await ensureConsumerPrincipal()
|
|
38
|
+
: await getConsumerPrincipal();
|
|
39
|
+
if (principal) {
|
|
40
|
+
headers["X-AW-Consumer-Principal"] = principal;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (options?.extraHeaders) {
|
|
44
|
+
Object.assign(headers, options.extraHeaders);
|
|
45
|
+
}
|
|
46
|
+
|
|
30
47
|
return headers;
|
|
31
48
|
}
|
|
32
49
|
|
|
@@ -55,25 +72,56 @@ async function handleResponse<T>(response: Response): Promise<T> {
|
|
|
55
72
|
return body as T;
|
|
56
73
|
}
|
|
57
74
|
|
|
75
|
+
function attachResponseMetadata<T>(result: T, response: Response): T {
|
|
76
|
+
if (!result || typeof result !== "object") {
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const record = result as Record<string, unknown>;
|
|
81
|
+
const headerToken = response.headers.get("x-feedback-token");
|
|
82
|
+
if (headerToken && !("feedback_token" in record)) {
|
|
83
|
+
record.feedback_token = headerToken;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const consumptionMode = response.headers.get("x-aw-consumption-mode");
|
|
87
|
+
if (consumptionMode) {
|
|
88
|
+
record.consumption_mode = consumptionMode;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const creditPackId = response.headers.get("x-aw-credit-pack-id");
|
|
92
|
+
if (creditPackId) {
|
|
93
|
+
record.credit_pack_id = creditPackId;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const principal = response.headers.get("x-aw-consumer-principal");
|
|
97
|
+
if (principal) {
|
|
98
|
+
record.consumer_principal = principal;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
|
|
58
104
|
// ── Public API ─────────────────────────────────────────────────────
|
|
59
105
|
|
|
60
|
-
export async function apiGet<T>(path: string): Promise<T> {
|
|
106
|
+
export async function apiGet<T>(path: string, options?: RequestOptions): Promise<T> {
|
|
61
107
|
const url = `${getApiUrl()}${path}`;
|
|
62
108
|
const response = await fetch(url, {
|
|
63
109
|
method: "GET",
|
|
64
|
-
headers: buildHeaders(),
|
|
110
|
+
headers: await buildHeaders(options),
|
|
65
111
|
});
|
|
66
|
-
|
|
112
|
+
const result = await handleResponse<T>(response);
|
|
113
|
+
return attachResponseMetadata(result, response);
|
|
67
114
|
}
|
|
68
115
|
|
|
69
|
-
export async function apiPost<T>(path: string, body: unknown): Promise<T> {
|
|
116
|
+
export async function apiPost<T>(path: string, body: unknown, options?: RequestOptions): Promise<T> {
|
|
70
117
|
const url = `${getApiUrl()}${path}`;
|
|
71
118
|
const response = await fetch(url, {
|
|
72
119
|
method: "POST",
|
|
73
|
-
headers: buildHeaders(),
|
|
120
|
+
headers: await buildHeaders(options),
|
|
74
121
|
body: JSON.stringify(body),
|
|
75
122
|
});
|
|
76
|
-
|
|
123
|
+
const result = await handleResponse<T>(response);
|
|
124
|
+
return attachResponseMetadata(result, response);
|
|
77
125
|
}
|
|
78
126
|
|
|
79
127
|
/**
|
|
@@ -81,34 +129,33 @@ export async function apiPost<T>(path: string, body: unknown): Promise<T> {
|
|
|
81
129
|
* auto-handle 402 → sign → retry for paid endpoints.
|
|
82
130
|
* Pass `payWith` to specify a method, or omit for auto-detection.
|
|
83
131
|
*/
|
|
84
|
-
export async function apiPostWithPayment<T>(
|
|
132
|
+
export async function apiPostWithPayment<T>(
|
|
133
|
+
path: string,
|
|
134
|
+
body: unknown,
|
|
135
|
+
payWith?: string,
|
|
136
|
+
options?: RequestOptions,
|
|
137
|
+
): Promise<T> {
|
|
85
138
|
const url = `${getApiUrl()}${path}`;
|
|
86
139
|
const paymentFetch = await getPaymentFetch(payWith);
|
|
87
140
|
const response = await paymentFetch(url, {
|
|
88
141
|
method: "POST",
|
|
89
|
-
headers: buildHeaders(
|
|
142
|
+
headers: await buildHeaders({
|
|
143
|
+
ensureConsumerPrincipal: true,
|
|
144
|
+
...options,
|
|
145
|
+
}),
|
|
90
146
|
body: JSON.stringify(body),
|
|
91
147
|
});
|
|
92
148
|
const result = await handleResponse<T>(response);
|
|
93
|
-
|
|
94
|
-
// mppx may strip extra fields from the response body during receipt processing.
|
|
95
|
-
// The gateway also sends feedback_token as a header to survive this.
|
|
96
|
-
if (result && typeof result === "object" && !("feedback_token" in result)) {
|
|
97
|
-
const headerToken = response.headers.get("x-feedback-token");
|
|
98
|
-
if (headerToken) {
|
|
99
|
-
(result as Record<string, unknown>).feedback_token = headerToken;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return result;
|
|
149
|
+
return attachResponseMetadata(result, response);
|
|
104
150
|
}
|
|
105
151
|
|
|
106
|
-
export async function apiPut<T>(path: string, body: unknown): Promise<T> {
|
|
152
|
+
export async function apiPut<T>(path: string, body: unknown, options?: RequestOptions): Promise<T> {
|
|
107
153
|
const url = `${getApiUrl()}${path}`;
|
|
108
154
|
const response = await fetch(url, {
|
|
109
155
|
method: "PUT",
|
|
110
|
-
headers: buildHeaders(),
|
|
156
|
+
headers: await buildHeaders(options),
|
|
111
157
|
body: JSON.stringify(body),
|
|
112
158
|
});
|
|
113
|
-
|
|
159
|
+
const result = await handleResponse<T>(response);
|
|
160
|
+
return attachResponseMetadata(result, response);
|
|
114
161
|
}
|
package/src/core/base-charge.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { Method, Credential, z } from "mppx";
|
|
8
8
|
import type { LocalAccount } from "viem/accounts";
|
|
9
|
+
import { toAtomicAmount } from "./amount-utils.js";
|
|
9
10
|
|
|
10
11
|
// Base USDC (Circle native)
|
|
11
12
|
const BASE_USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" as const;
|
|
@@ -63,12 +64,12 @@ export function baseChargeClient(config: BaseChargeClientConfig) {
|
|
|
63
64
|
return Method.toClient(baseChargeMethod as any, {
|
|
64
65
|
async createCredential({ challenge }: any) {
|
|
65
66
|
const { request } = challenge;
|
|
66
|
-
const amount =
|
|
67
|
+
const amount = toAtomicAmount(request.amount, request.decimals ?? 6);
|
|
67
68
|
const recipient = request.recipient as `0x${string}`;
|
|
68
69
|
const currency = (request.currency ?? BASE_USDC) as `0x${string}`;
|
|
69
70
|
|
|
70
71
|
// Dynamic imports to keep the module lightweight
|
|
71
|
-
const { createWalletClient, createPublicClient, http
|
|
72
|
+
const { createWalletClient, createPublicClient, http } = await import("viem");
|
|
72
73
|
const { base } = await import("viem/chains");
|
|
73
74
|
|
|
74
75
|
const rpcUrl = config.rpcUrl ?? "https://mainnet.base.org";
|