@agentwonderland/mcp 0.1.24 → 0.1.26
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__/api-client.test.d.ts +1 -0
- package/dist/core/__tests__/api-client.test.js +51 -0
- package/dist/core/__tests__/formatters.test.js +10 -0
- package/dist/core/__tests__/passes-api.test.d.ts +1 -0
- package/dist/core/__tests__/passes-api.test.js +27 -0
- package/dist/core/__tests__/payments.test.js +59 -6
- package/dist/core/__tests__/principal.test.js +41 -4
- package/dist/core/__tests__/solana-charge.test.d.ts +1 -0
- package/dist/core/__tests__/solana-charge.test.js +50 -0
- package/dist/core/__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 +1 -0
- package/dist/core/api-client.js +8 -3
- package/dist/core/balances.d.ts +1 -0
- package/dist/core/balances.js +56 -0
- package/dist/core/base-charge.js +16 -8
- package/dist/core/config.d.ts +19 -0
- package/dist/core/config.js +22 -0
- package/dist/core/formatters.d.ts +5 -5
- package/dist/core/formatters.js +12 -8
- package/dist/core/passes.d.ts +1 -1
- package/dist/core/passes.js +5 -2
- package/dist/core/payments.d.ts +1 -0
- package/dist/core/payments.js +32 -9
- package/dist/core/principal.d.ts +3 -0
- package/dist/core/principal.js +29 -1
- package/dist/core/settings.d.ts +20 -0
- package/dist/core/settings.js +19 -0
- package/dist/core/solana-charge.d.ts +5 -0
- package/dist/core/solana-charge.js +31 -8
- package/dist/core/spend-policy.d.ts +12 -0
- package/dist/core/spend-policy.js +53 -0
- package/dist/core/tempo-charge.d.ts +7 -0
- package/dist/core/tempo-charge.js +84 -0
- package/dist/core/types.d.ts +1 -2
- package/dist/index.js +9 -5
- package/dist/prompts/index.js +4 -2
- package/dist/resources/agents.js +1 -1
- package/dist/tools/__tests__/jobs.test.d.ts +1 -0
- package/dist/tools/__tests__/jobs.test.js +71 -0
- package/dist/tools/__tests__/run.test.d.ts +1 -0
- package/dist/tools/__tests__/run.test.js +149 -0
- package/dist/tools/__tests__/solve.test.d.ts +1 -0
- package/dist/tools/__tests__/solve.test.js +158 -0
- package/dist/tools/__tests__/wallet.test.d.ts +1 -0
- package/dist/tools/__tests__/wallet.test.js +230 -0
- package/dist/tools/_payment-confirmation.js +1 -1
- package/dist/tools/agent-info.js +2 -2
- package/dist/tools/favorites.js +1 -1
- package/dist/tools/jobs.js +8 -1
- package/dist/tools/observability.d.ts +2 -0
- package/dist/tools/observability.js +20 -0
- package/dist/tools/passes.js +11 -6
- package/dist/tools/run.js +45 -29
- package/dist/tools/solve.js +53 -40
- package/dist/tools/wallet.js +58 -22
- package/package.json +2 -2
- package/src/core/__tests__/amount-utils.test.ts +13 -0
- package/src/core/__tests__/api-client.test.ts +78 -0
- package/src/core/__tests__/formatters.test.ts +12 -0
- package/src/core/__tests__/passes-api.test.ts +33 -0
- package/src/core/__tests__/payments.test.ts +79 -6
- package/src/core/__tests__/principal.test.ts +49 -4
- package/src/core/__tests__/solana-charge.test.ts +59 -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 +16 -3
- package/src/core/balances.ts +63 -0
- package/src/core/base-charge.ts +16 -8
- package/src/core/config.ts +45 -0
- package/src/core/formatters.ts +16 -11
- package/src/core/passes.ts +5 -2
- package/src/core/payments.ts +37 -9
- package/src/core/principal.ts +42 -1
- package/src/core/settings.ts +36 -0
- package/src/core/solana-charge.ts +45 -10
- package/src/core/spend-policy.ts +69 -0
- package/src/core/tempo-charge.ts +104 -0
- package/src/core/types.ts +1 -2
- package/src/index.ts +9 -5
- package/src/prompts/index.ts +4 -2
- package/src/resources/agents.ts +1 -1
- package/src/tools/__tests__/jobs.test.ts +89 -0
- package/src/tools/__tests__/run.test.ts +176 -0
- package/src/tools/__tests__/solve.test.ts +186 -0
- package/src/tools/__tests__/wallet.test.ts +289 -0
- package/src/tools/_payment-confirmation.ts +1 -1
- package/src/tools/agent-info.ts +2 -2
- package/src/tools/favorites.ts +1 -4
- package/src/tools/jobs.ts +10 -1
- package/src/tools/observability.ts +43 -0
- package/src/tools/passes.ts +12 -12
- package/src/tools/run.ts +50 -41
- package/src/tools/solve.ts +58 -52
- package/src/tools/wallet.ts +60 -24
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const state = vi.hoisted(() => ({
|
|
4
|
+
apiGet: vi.fn(),
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
vi.mock("../api-client.js", () => ({
|
|
8
|
+
apiGet: (...args: unknown[]) => state.apiGet(...args),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
describe("getCreditPackInventory", () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
vi.resetModules();
|
|
14
|
+
state.apiGet.mockReset();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("requests inventory for the selected payment method principal", async () => {
|
|
18
|
+
state.apiGet.mockResolvedValueOnce({
|
|
19
|
+
consumer_principal: "did:pkh:solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF",
|
|
20
|
+
offers: [],
|
|
21
|
+
balances: [],
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const { getCreditPackInventory } = await import("../passes.js");
|
|
25
|
+
const result = await getCreditPackInventory("agent-1", "solana");
|
|
26
|
+
|
|
27
|
+
expect(result?.consumer_principal).toContain("did:pkh:solana:");
|
|
28
|
+
expect(state.apiGet).toHaveBeenCalledWith("/agents/agent-1/credit-packs", {
|
|
29
|
+
ensureConsumerPrincipal: true,
|
|
30
|
+
principalMethod: "solana",
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
|
|
3
|
+
const TEST_KEY = "1111111111111111111111111111111111111111111111111111111111111111";
|
|
4
|
+
|
|
3
5
|
let currentCard = {
|
|
4
6
|
consumerToken: "consumer_one",
|
|
5
7
|
paymentMethodId: "pm_one",
|
|
@@ -7,17 +9,23 @@ let currentCard = {
|
|
|
7
9
|
brand: "visa",
|
|
8
10
|
};
|
|
9
11
|
|
|
10
|
-
|
|
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()];
|
|
11
17
|
const mockMppxCreate = vi.fn();
|
|
12
18
|
const mockStripe = vi.fn((opts: unknown) => opts);
|
|
19
|
+
const mockTempoChargeClient = vi.fn((..._args: unknown[]) => "tempo_method");
|
|
20
|
+
const mockBaseChargeClient = vi.fn((..._args: unknown[]) => "base_method");
|
|
13
21
|
|
|
14
22
|
vi.mock("../config.js", () => ({
|
|
15
23
|
getApiUrl: () => "http://api.test",
|
|
16
24
|
getCardConfig: () => currentCard,
|
|
17
25
|
getConfig: () => ({ defaultPaymentMethod: "card" }),
|
|
18
|
-
getDefaultWallet: () =>
|
|
19
|
-
getWallets: () =>
|
|
20
|
-
resolveWalletAndChain: () =>
|
|
26
|
+
getDefaultWallet: () => currentDefaultWallet,
|
|
27
|
+
getWallets: () => currentWallets,
|
|
28
|
+
resolveWalletAndChain: () => currentResolvedMethod,
|
|
21
29
|
}));
|
|
22
30
|
|
|
23
31
|
vi.mock("mppx/client", () => ({
|
|
@@ -27,19 +35,32 @@ vi.mock("mppx/client", () => ({
|
|
|
27
35
|
stripe: (config: unknown) => mockStripe(config),
|
|
28
36
|
}));
|
|
29
37
|
|
|
30
|
-
|
|
38
|
+
vi.mock("../tempo-charge.js", () => ({
|
|
39
|
+
tempoChargeClient: (config: unknown) => mockTempoChargeClient(config),
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
vi.mock("../base-charge.js", () => ({
|
|
43
|
+
baseChargeClient: (config: unknown) => mockBaseChargeClient(config),
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
describe("payment method initialization", () => {
|
|
31
47
|
beforeEach(() => {
|
|
32
48
|
vi.clearAllMocks();
|
|
49
|
+
vi.resetModules();
|
|
33
50
|
currentCard = {
|
|
34
51
|
consumerToken: "consumer_one",
|
|
35
52
|
paymentMethodId: "pm_one",
|
|
36
53
|
last4: "1111",
|
|
37
54
|
brand: "visa",
|
|
38
55
|
};
|
|
56
|
+
currentDefaultWallet = undefined;
|
|
57
|
+
currentWallets = [];
|
|
58
|
+
currentResolvedMethod = null;
|
|
39
59
|
mockMppxCreate
|
|
40
60
|
.mockReturnValueOnce({ fetch: createdFetches[0] })
|
|
41
61
|
.mockReturnValueOnce({ fetch: createdFetches[1] })
|
|
42
|
-
.mockReturnValueOnce({ fetch: createdFetches[2] })
|
|
62
|
+
.mockReturnValueOnce({ fetch: createdFetches[2] })
|
|
63
|
+
.mockReturnValueOnce({ fetch: createdFetches[3] });
|
|
43
64
|
});
|
|
44
65
|
|
|
45
66
|
it("rebuilds the cached card fetch when the card config changes", async () => {
|
|
@@ -56,5 +77,57 @@ describe("card payment fetch cache", () => {
|
|
|
56
77
|
|
|
57
78
|
expect(firstFetch).not.toBe(secondFetch);
|
|
58
79
|
expect(mockMppxCreate).toHaveBeenCalledTimes(2);
|
|
80
|
+
expect(mockMppxCreate).toHaveBeenNthCalledWith(
|
|
81
|
+
1,
|
|
82
|
+
expect.objectContaining({ polyfill: false }),
|
|
83
|
+
);
|
|
84
|
+
expect(mockMppxCreate).toHaveBeenNthCalledWith(
|
|
85
|
+
2,
|
|
86
|
+
expect.objectContaining({ polyfill: false }),
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("initializes only the Base method when base is requested", async () => {
|
|
91
|
+
const wallet = {
|
|
92
|
+
id: "aw-main",
|
|
93
|
+
keyType: "raw",
|
|
94
|
+
key: TEST_KEY,
|
|
95
|
+
chains: ["tempo", "base"],
|
|
96
|
+
defaultChain: "base",
|
|
97
|
+
};
|
|
98
|
+
currentDefaultWallet = wallet;
|
|
99
|
+
currentWallets = [wallet];
|
|
100
|
+
currentResolvedMethod = { wallet, chain: "base" };
|
|
101
|
+
|
|
102
|
+
const { getPaymentFetch } = await import("../payments.js");
|
|
103
|
+
await getPaymentFetch("base");
|
|
104
|
+
|
|
105
|
+
expect(mockBaseChargeClient).toHaveBeenCalledTimes(1);
|
|
106
|
+
expect(mockTempoChargeClient).not.toHaveBeenCalled();
|
|
107
|
+
expect(mockMppxCreate).toHaveBeenCalledWith(
|
|
108
|
+
expect.objectContaining({ methods: ["base_method"], polyfill: false }),
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("initializes only the Tempo method when tempo is requested", async () => {
|
|
113
|
+
const wallet = {
|
|
114
|
+
id: "aw-main",
|
|
115
|
+
keyType: "raw",
|
|
116
|
+
key: TEST_KEY,
|
|
117
|
+
chains: ["tempo", "base"],
|
|
118
|
+
defaultChain: "tempo",
|
|
119
|
+
};
|
|
120
|
+
currentDefaultWallet = wallet;
|
|
121
|
+
currentWallets = [wallet];
|
|
122
|
+
currentResolvedMethod = { wallet, chain: "tempo" };
|
|
123
|
+
|
|
124
|
+
const { getPaymentFetch } = await import("../payments.js");
|
|
125
|
+
await getPaymentFetch("tempo");
|
|
126
|
+
|
|
127
|
+
expect(mockTempoChargeClient).toHaveBeenCalledTimes(1);
|
|
128
|
+
expect(mockBaseChargeClient).not.toHaveBeenCalled();
|
|
129
|
+
expect(mockMppxCreate).toHaveBeenCalledWith(
|
|
130
|
+
expect.objectContaining({ methods: ["tempo_method"], polyfill: false }),
|
|
131
|
+
);
|
|
59
132
|
});
|
|
60
133
|
});
|
|
@@ -23,15 +23,26 @@ vi.mock("../config.js", () => ({
|
|
|
23
23
|
},
|
|
24
24
|
getDefaultWallet: () => state.wallets[0],
|
|
25
25
|
getWallets: () => state.wallets,
|
|
26
|
+
resolveWalletAndChain: (method: string) => {
|
|
27
|
+
for (const wallet of state.wallets) {
|
|
28
|
+
if (wallet.id === method) {
|
|
29
|
+
return { wallet, chain: wallet.defaultChain ?? wallet.chains[0] };
|
|
30
|
+
}
|
|
31
|
+
if (wallet.chains.includes(method)) {
|
|
32
|
+
return { wallet, chain: method };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
},
|
|
26
37
|
}));
|
|
27
38
|
|
|
28
39
|
vi.mock("../payments.js", () => ({
|
|
29
|
-
getWalletAddress: async (
|
|
30
|
-
if (
|
|
31
|
-
return state.addresses[
|
|
40
|
+
getWalletAddress: async (method?: string) => {
|
|
41
|
+
if (method && method in state.addresses) {
|
|
42
|
+
return state.addresses[method] ?? null;
|
|
32
43
|
}
|
|
33
44
|
|
|
34
|
-
const wallet = state.wallets.find((entry) => entry.id ===
|
|
45
|
+
const wallet = state.wallets.find((entry) => entry.id === method);
|
|
35
46
|
if (wallet?.key) {
|
|
36
47
|
const { privateKeyToAccount } = await import("viem/accounts");
|
|
37
48
|
return privateKeyToAccount(wallet.key as `0x${string}`).address;
|
|
@@ -84,4 +95,38 @@ describe("consumer principal helpers", () => {
|
|
|
84
95
|
expect(state.addedWallets).toHaveLength(1);
|
|
85
96
|
expect(state.addedWallets[0]?.chains).toEqual(["tempo", "base"]);
|
|
86
97
|
});
|
|
98
|
+
|
|
99
|
+
it("derives a Solana principal when the selected payment method is solana", async () => {
|
|
100
|
+
state.wallets = [
|
|
101
|
+
{ id: "aw-main", keyType: "ows", owsWalletId: "ows-main", chains: ["tempo", "base", "solana"], defaultChain: "tempo" },
|
|
102
|
+
];
|
|
103
|
+
state.addresses = {
|
|
104
|
+
solana: "42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF",
|
|
105
|
+
tempo: "0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
|
|
106
|
+
base: "0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const { getConsumerPrincipalForMethod } = await import("../principal.js");
|
|
110
|
+
const principal = await getConsumerPrincipalForMethod("solana");
|
|
111
|
+
|
|
112
|
+
expect(principal).toBe(
|
|
113
|
+
"did:pkh:solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF",
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("returns the Base rebate principal when an EVM wallet is available", async () => {
|
|
118
|
+
state.wallets = [
|
|
119
|
+
{ id: "aw-main", keyType: "ows", owsWalletId: "ows-main", chains: ["tempo", "base", "solana"], defaultChain: "tempo" },
|
|
120
|
+
];
|
|
121
|
+
state.addresses = {
|
|
122
|
+
solana: "42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF",
|
|
123
|
+
tempo: "0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
|
|
124
|
+
base: "0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const { getBaseRebatePrincipal } = await import("../principal.js");
|
|
128
|
+
const principal = await getBaseRebatePrincipal();
|
|
129
|
+
|
|
130
|
+
expect(principal).toBe("did:pkh:eip155:8453:0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
|
|
131
|
+
});
|
|
87
132
|
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { PublicKey } from "@solana/web3.js";
|
|
2
|
+
import { TOKEN_PROGRAM_ID, getAssociatedTokenAddressSync } from "@solana/spl-token";
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { SOLANA_USDC_MINT } from "../solana-charge.js";
|
|
5
|
+
|
|
6
|
+
describe("resolveRecipientTokenAccount", () => {
|
|
7
|
+
it("uses the recipient directly when Stripe gives us a token account", async () => {
|
|
8
|
+
const { resolveRecipientTokenAccount } = await import("../solana-charge.js");
|
|
9
|
+
const connection = {
|
|
10
|
+
getParsedAccountInfo: vi.fn().mockResolvedValue({
|
|
11
|
+
value: { owner: TOKEN_PROGRAM_ID },
|
|
12
|
+
}),
|
|
13
|
+
} as any;
|
|
14
|
+
const mint = new PublicKey(SOLANA_USDC_MINT);
|
|
15
|
+
const recipient = new PublicKey("42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF");
|
|
16
|
+
|
|
17
|
+
const resolved = await resolveRecipientTokenAccount(connection, mint, recipient);
|
|
18
|
+
|
|
19
|
+
expect(resolved.tokenAccount.toBase58()).toBe(recipient.toBase58());
|
|
20
|
+
expect(resolved.needsCreateAssociatedAccount).toBe(false);
|
|
21
|
+
expect(connection.getParsedAccountInfo).toHaveBeenCalledTimes(1);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("reuses the associated token account when it already exists", async () => {
|
|
25
|
+
const { resolveRecipientTokenAccount } = await import("../solana-charge.js");
|
|
26
|
+
const connection = {
|
|
27
|
+
getParsedAccountInfo: vi.fn()
|
|
28
|
+
.mockResolvedValueOnce({ value: null })
|
|
29
|
+
.mockResolvedValueOnce({ value: { owner: TOKEN_PROGRAM_ID } }),
|
|
30
|
+
} as any;
|
|
31
|
+
const mint = new PublicKey(SOLANA_USDC_MINT);
|
|
32
|
+
const recipient = new PublicKey("G7kW6ZPMAK9v11AaVkK9FHed2Gd4WYwvCbanXUmK9WtS");
|
|
33
|
+
const expectedAta = getAssociatedTokenAddressSync(mint, recipient, false, TOKEN_PROGRAM_ID);
|
|
34
|
+
|
|
35
|
+
const resolved = await resolveRecipientTokenAccount(connection, mint, recipient);
|
|
36
|
+
|
|
37
|
+
expect(resolved.tokenAccount.toBase58()).toBe(expectedAta.toBase58());
|
|
38
|
+
expect(resolved.needsCreateAssociatedAccount).toBe(false);
|
|
39
|
+
expect(connection.getParsedAccountInfo).toHaveBeenCalledTimes(2);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("marks the associated token account for creation when neither account exists", async () => {
|
|
43
|
+
const { resolveRecipientTokenAccount } = await import("../solana-charge.js");
|
|
44
|
+
const connection = {
|
|
45
|
+
getParsedAccountInfo: vi.fn()
|
|
46
|
+
.mockResolvedValueOnce({ value: null })
|
|
47
|
+
.mockResolvedValueOnce({ value: null }),
|
|
48
|
+
} as any;
|
|
49
|
+
const mint = new PublicKey(SOLANA_USDC_MINT);
|
|
50
|
+
const recipient = new PublicKey("G7kW6ZPMAK9v11AaVkK9FHed2Gd4WYwvCbanXUmK9WtS");
|
|
51
|
+
const expectedAta = getAssociatedTokenAddressSync(mint, recipient, false, TOKEN_PROGRAM_ID);
|
|
52
|
+
|
|
53
|
+
const resolved = await resolveRecipientTokenAccount(connection, mint, recipient);
|
|
54
|
+
|
|
55
|
+
expect(resolved.tokenAccount.toBase58()).toBe(expectedAta.toBase58());
|
|
56
|
+
expect(resolved.needsCreateAssociatedAccount).toBe(true);
|
|
57
|
+
expect(connection.getParsedAccountInfo).toHaveBeenCalledTimes(2);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -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,6 +1,12 @@
|
|
|
1
1
|
import { getApiUrl, getApiKey } from "./config.js";
|
|
2
2
|
import { getPaymentFetch } from "./payments.js";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
getBaseRebatePrincipal,
|
|
5
|
+
ensureConsumerPrincipal,
|
|
6
|
+
ensureConsumerPrincipalForMethod,
|
|
7
|
+
getConsumerPrincipal,
|
|
8
|
+
getConsumerPrincipalForMethod,
|
|
9
|
+
} from "./principal.js";
|
|
4
10
|
|
|
5
11
|
// ── Error class ────────────────────────────────────────────────────
|
|
6
12
|
|
|
@@ -19,6 +25,7 @@ export class ApiError extends Error {
|
|
|
19
25
|
|
|
20
26
|
interface RequestOptions {
|
|
21
27
|
ensureConsumerPrincipal?: boolean;
|
|
28
|
+
principalMethod?: string;
|
|
22
29
|
extraHeaders?: Record<string, string>;
|
|
23
30
|
}
|
|
24
31
|
|
|
@@ -34,12 +41,17 @@ async function buildHeaders(options?: RequestOptions): Promise<Record<string, st
|
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
const principal = options?.ensureConsumerPrincipal
|
|
37
|
-
? await
|
|
38
|
-
: await
|
|
44
|
+
? await ensureConsumerPrincipalForMethod(options?.principalMethod)
|
|
45
|
+
: await getConsumerPrincipalForMethod(options?.principalMethod);
|
|
39
46
|
if (principal) {
|
|
40
47
|
headers["X-AW-Consumer-Principal"] = principal;
|
|
41
48
|
}
|
|
42
49
|
|
|
50
|
+
const rebatePrincipal = await getBaseRebatePrincipal();
|
|
51
|
+
if (rebatePrincipal) {
|
|
52
|
+
headers["X-AW-Rebate-Principal"] = rebatePrincipal;
|
|
53
|
+
}
|
|
54
|
+
|
|
43
55
|
if (options?.extraHeaders) {
|
|
44
56
|
Object.assign(headers, options.extraHeaders);
|
|
45
57
|
}
|
|
@@ -141,6 +153,7 @@ export async function apiPostWithPayment<T>(
|
|
|
141
153
|
method: "POST",
|
|
142
154
|
headers: await buildHeaders({
|
|
143
155
|
ensureConsumerPrincipal: true,
|
|
156
|
+
principalMethod: payWith,
|
|
144
157
|
...options,
|
|
145
158
|
}),
|
|
146
159
|
body: JSON.stringify(body),
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { getSettings, type ChainSettings } from "./settings.js";
|
|
2
|
+
|
|
3
|
+
const ERC20_BALANCE_ABI = [
|
|
4
|
+
{
|
|
5
|
+
type: "function",
|
|
6
|
+
name: "balanceOf",
|
|
7
|
+
stateMutability: "view",
|
|
8
|
+
inputs: [{ name: "account", type: "address" }],
|
|
9
|
+
outputs: [{ name: "", type: "uint256" }],
|
|
10
|
+
},
|
|
11
|
+
] as const;
|
|
12
|
+
|
|
13
|
+
async function fetchEvmUsdcBalance(
|
|
14
|
+
chain: ChainSettings,
|
|
15
|
+
address: string,
|
|
16
|
+
): Promise<string | null> {
|
|
17
|
+
try {
|
|
18
|
+
const { createPublicClient, http, formatUnits } = await import("viem");
|
|
19
|
+
const client = createPublicClient({ transport: http(chain.rpcUrl) });
|
|
20
|
+
const raw = await client.readContract({
|
|
21
|
+
address: chain.usdc as `0x${string}`,
|
|
22
|
+
abi: ERC20_BALANCE_ABI,
|
|
23
|
+
functionName: "balanceOf",
|
|
24
|
+
args: [address as `0x${string}`],
|
|
25
|
+
});
|
|
26
|
+
return formatUnits(raw as bigint, 6);
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function fetchSolanaUsdcBalance(
|
|
33
|
+
chain: ChainSettings,
|
|
34
|
+
address: string,
|
|
35
|
+
): Promise<string | null> {
|
|
36
|
+
try {
|
|
37
|
+
const { Connection, PublicKey } = await import("@solana/web3.js");
|
|
38
|
+
const connection = new Connection(chain.rpcUrl, "confirmed");
|
|
39
|
+
const owner = new PublicKey(address);
|
|
40
|
+
const mint = new PublicKey(chain.usdc);
|
|
41
|
+
const accounts = await connection.getParsedTokenAccountsByOwner(owner, { mint });
|
|
42
|
+
let total = 0;
|
|
43
|
+
for (const { account } of accounts.value) {
|
|
44
|
+
const amount = (account.data as any)?.parsed?.info?.tokenAmount?.uiAmount;
|
|
45
|
+
if (typeof amount === "number") total += amount;
|
|
46
|
+
}
|
|
47
|
+
return total.toString();
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function fetchUsdcBalance(
|
|
54
|
+
chain: "tempo" | "base" | "solana",
|
|
55
|
+
address: string,
|
|
56
|
+
): Promise<string | null> {
|
|
57
|
+
const settings = await getSettings();
|
|
58
|
+
if (!settings) return null;
|
|
59
|
+
const chainConfig = settings.chains[chain];
|
|
60
|
+
if (!chainConfig) return null;
|
|
61
|
+
if (chain === "solana") return fetchSolanaUsdcBalance(chainConfig, address);
|
|
62
|
+
return fetchEvmUsdcBalance(chainConfig, address);
|
|
63
|
+
}
|
package/src/core/base-charge.ts
CHANGED
|
@@ -6,10 +6,13 @@
|
|
|
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;
|
|
13
|
+
const BASE_SEPOLIA_USDC = "0x036CbD53842c5426634e7929541eC2318f3dCF7e" as const;
|
|
12
14
|
const BASE_CHAIN_ID = 8453;
|
|
15
|
+
const BASE_SEPOLIA_CHAIN_ID = 84532;
|
|
13
16
|
|
|
14
17
|
// Standard ERC-20 transfer function ABI
|
|
15
18
|
const ERC20_ABI = [
|
|
@@ -63,24 +66,29 @@ export function baseChargeClient(config: BaseChargeClientConfig) {
|
|
|
63
66
|
return Method.toClient(baseChargeMethod as any, {
|
|
64
67
|
async createCredential({ challenge }: any) {
|
|
65
68
|
const { request } = challenge;
|
|
66
|
-
const amount =
|
|
69
|
+
const amount = toAtomicAmount(request.amount, request.decimals ?? 6);
|
|
67
70
|
const recipient = request.recipient as `0x${string}`;
|
|
68
|
-
const
|
|
71
|
+
const chainId = request.chainId ?? BASE_CHAIN_ID;
|
|
72
|
+
const currency = (request.currency
|
|
73
|
+
?? (chainId === BASE_SEPOLIA_CHAIN_ID ? BASE_SEPOLIA_USDC : BASE_USDC)) as `0x${string}`;
|
|
69
74
|
|
|
70
75
|
// Dynamic imports to keep the module lightweight
|
|
71
|
-
const { createWalletClient, createPublicClient, http
|
|
72
|
-
const { base } = await import("viem/chains");
|
|
76
|
+
const { createWalletClient, createPublicClient, http } = await import("viem");
|
|
77
|
+
const { base, baseSepolia } = await import("viem/chains");
|
|
73
78
|
|
|
74
|
-
const
|
|
79
|
+
const chain = chainId === BASE_SEPOLIA_CHAIN_ID ? baseSepolia : base;
|
|
80
|
+
const rpcUrl = config.rpcUrl ?? (chainId === BASE_SEPOLIA_CHAIN_ID
|
|
81
|
+
? "https://sepolia.base.org"
|
|
82
|
+
: "https://mainnet.base.org");
|
|
75
83
|
|
|
76
84
|
const walletClient = createWalletClient({
|
|
77
85
|
account: config.account,
|
|
78
|
-
chain
|
|
86
|
+
chain,
|
|
79
87
|
transport: http(rpcUrl),
|
|
80
88
|
});
|
|
81
89
|
|
|
82
90
|
const publicClient = createPublicClient({
|
|
83
|
-
chain
|
|
91
|
+
chain,
|
|
84
92
|
transport: http(rpcUrl),
|
|
85
93
|
});
|
|
86
94
|
|
|
@@ -98,7 +106,7 @@ export function baseChargeClient(config: BaseChargeClientConfig) {
|
|
|
98
106
|
return Credential.serialize({
|
|
99
107
|
challenge,
|
|
100
108
|
payload: { hash, type: "hash" as const },
|
|
101
|
-
source: `did:pkh:eip155:${
|
|
109
|
+
source: `did:pkh:eip155:${chainId}:${config.account.address}`,
|
|
102
110
|
});
|
|
103
111
|
},
|
|
104
112
|
});
|
package/src/core/config.ts
CHANGED
|
@@ -21,6 +21,19 @@ export interface CardConfig {
|
|
|
21
21
|
brand: string;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
export interface SpendPolicy {
|
|
25
|
+
maxPerTxUsd?: number;
|
|
26
|
+
maxPerDayUsd?: number;
|
|
27
|
+
requireConfirmationAboveUsd?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SpendLedgerEntry {
|
|
31
|
+
method: string;
|
|
32
|
+
amountUsd: number;
|
|
33
|
+
day: string;
|
|
34
|
+
timestamp: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
24
37
|
export interface Config {
|
|
25
38
|
apiUrl: string;
|
|
26
39
|
apiKey: string | null;
|
|
@@ -35,6 +48,10 @@ export interface Config {
|
|
|
35
48
|
confirmBeforeSpend: boolean;
|
|
36
49
|
/** Auto-tip amount in USD for successful runs. Default: 0 (no auto-tip). */
|
|
37
50
|
defaultTipAmount: number;
|
|
51
|
+
/** Optional per-method spend policies enforced client-side by MCP tools. */
|
|
52
|
+
spendPolicies?: Record<string, SpendPolicy>;
|
|
53
|
+
/** Daily spend ledger used to enforce max-per-day limits. */
|
|
54
|
+
spendLedger?: SpendLedgerEntry[];
|
|
38
55
|
}
|
|
39
56
|
|
|
40
57
|
/** All supported chain identifiers. */
|
|
@@ -82,6 +99,8 @@ interface LegacyConfig {
|
|
|
82
99
|
defaultWallet?: string | null;
|
|
83
100
|
card?: CardConfig | null;
|
|
84
101
|
pendingCardSetupToken?: string | null;
|
|
102
|
+
spendPolicies?: Record<string, SpendPolicy>;
|
|
103
|
+
spendLedger?: SpendLedgerEntry[];
|
|
85
104
|
}
|
|
86
105
|
|
|
87
106
|
/**
|
|
@@ -104,6 +123,8 @@ function migrateIfNeeded(raw: LegacyConfig): Config {
|
|
|
104
123
|
favorites: r.favorites as string[] ?? [],
|
|
105
124
|
confirmBeforeSpend: r.confirmBeforeSpend !== false,
|
|
106
125
|
defaultTipAmount: typeof r.defaultTipAmount === "number" ? r.defaultTipAmount : 0,
|
|
126
|
+
spendPolicies: r.spendPolicies as Record<string, SpendPolicy> | undefined,
|
|
127
|
+
spendLedger: r.spendLedger as SpendLedgerEntry[] | undefined,
|
|
107
128
|
};
|
|
108
129
|
}
|
|
109
130
|
|
|
@@ -178,6 +199,8 @@ function migrateIfNeeded(raw: LegacyConfig): Config {
|
|
|
178
199
|
favorites: [],
|
|
179
200
|
confirmBeforeSpend: true,
|
|
180
201
|
defaultTipAmount: 0,
|
|
202
|
+
spendPolicies: {},
|
|
203
|
+
spendLedger: [],
|
|
181
204
|
};
|
|
182
205
|
|
|
183
206
|
// Write migrated config (only if there was something to migrate)
|
|
@@ -203,6 +226,8 @@ export function getConfig(): Config {
|
|
|
203
226
|
favorites: [],
|
|
204
227
|
confirmBeforeSpend: true,
|
|
205
228
|
defaultTipAmount: 0,
|
|
229
|
+
spendPolicies: {},
|
|
230
|
+
spendLedger: [],
|
|
206
231
|
};
|
|
207
232
|
|
|
208
233
|
if (!existsSync(CONFIG_FILE)) {
|
|
@@ -249,6 +274,26 @@ export function getDefaultTipAmount(): number {
|
|
|
249
274
|
return getConfig().defaultTipAmount;
|
|
250
275
|
}
|
|
251
276
|
|
|
277
|
+
export function getSpendPolicy(method: string): SpendPolicy | null {
|
|
278
|
+
const policies = getConfig().spendPolicies ?? {};
|
|
279
|
+
return policies[method] ?? null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function setSpendPolicy(method: string, policy: SpendPolicy): void {
|
|
283
|
+
const config = getConfig();
|
|
284
|
+
const policies = config.spendPolicies ?? {};
|
|
285
|
+
policies[method] = policy;
|
|
286
|
+
saveConfig({ spendPolicies: policies });
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function getSpendLedger(): SpendLedgerEntry[] {
|
|
290
|
+
return getConfig().spendLedger ?? [];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function saveSpendLedger(entries: SpendLedgerEntry[]): void {
|
|
294
|
+
saveConfig({ spendLedger: entries });
|
|
295
|
+
}
|
|
296
|
+
|
|
252
297
|
// ── Wallet helpers ─────────────────────────────────────────────────
|
|
253
298
|
|
|
254
299
|
/**
|