@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 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { toAtomicAmount } from "../amount-utils.js";
|
|
3
|
+
describe("toAtomicAmount", () => {
|
|
4
|
+
it("converts decimal USDC challenge amounts into atomic units", () => {
|
|
5
|
+
expect(toAtomicAmount("0.020000")).toBe(20000n);
|
|
6
|
+
expect(toAtomicAmount("1.000000")).toBe(1000000n);
|
|
7
|
+
});
|
|
8
|
+
it("supports different decimal precisions", () => {
|
|
9
|
+
expect(toAtomicAmount("0.50", 2)).toBe(50n);
|
|
10
|
+
});
|
|
11
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
const { mockGetApiUrl, mockGetApiKey, mockGetPaymentFetch, mockEnsureConsumerPrincipalForMethod, mockGetConsumerPrincipalForMethod, mockGetBaseRebatePrincipal, mockPaymentFetch, } = vi.hoisted(() => ({
|
|
3
|
+
mockGetApiUrl: vi.fn(),
|
|
4
|
+
mockGetApiKey: vi.fn(),
|
|
5
|
+
mockGetPaymentFetch: vi.fn(),
|
|
6
|
+
mockEnsureConsumerPrincipalForMethod: vi.fn(),
|
|
7
|
+
mockGetConsumerPrincipalForMethod: vi.fn(),
|
|
8
|
+
mockGetBaseRebatePrincipal: vi.fn(),
|
|
9
|
+
mockPaymentFetch: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
vi.mock("../config.js", () => ({
|
|
12
|
+
getApiUrl: () => mockGetApiUrl(),
|
|
13
|
+
getApiKey: () => mockGetApiKey(),
|
|
14
|
+
}));
|
|
15
|
+
vi.mock("../payments.js", () => ({
|
|
16
|
+
getPaymentFetch: (...args) => mockGetPaymentFetch(...args),
|
|
17
|
+
}));
|
|
18
|
+
vi.mock("../principal.js", () => ({
|
|
19
|
+
ensureConsumerPrincipal: vi.fn(),
|
|
20
|
+
ensureConsumerPrincipalForMethod: (...args) => mockEnsureConsumerPrincipalForMethod(...args),
|
|
21
|
+
getConsumerPrincipal: vi.fn(),
|
|
22
|
+
getConsumerPrincipalForMethod: (...args) => mockGetConsumerPrincipalForMethod(...args),
|
|
23
|
+
getBaseRebatePrincipal: (...args) => mockGetBaseRebatePrincipal(...args),
|
|
24
|
+
}));
|
|
25
|
+
describe("api-client headers", () => {
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
vi.clearAllMocks();
|
|
28
|
+
mockGetApiUrl.mockReturnValue("https://api.agentwonderland.test");
|
|
29
|
+
mockGetApiKey.mockReturnValue(null);
|
|
30
|
+
mockGetPaymentFetch.mockResolvedValue(mockPaymentFetch);
|
|
31
|
+
mockEnsureConsumerPrincipalForMethod.mockResolvedValue("did:pkh:solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF");
|
|
32
|
+
mockGetConsumerPrincipalForMethod.mockResolvedValue("did:pkh:eip155:8453:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
|
|
33
|
+
mockGetBaseRebatePrincipal.mockResolvedValue("did:pkh:eip155:8453:0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
|
|
34
|
+
mockPaymentFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), {
|
|
35
|
+
status: 200,
|
|
36
|
+
headers: { "content-type": "application/json" },
|
|
37
|
+
}));
|
|
38
|
+
});
|
|
39
|
+
it("includes the Base rebate principal alongside the method-specific consumer principal", async () => {
|
|
40
|
+
const { apiPostWithPayment } = await import("../api-client.js");
|
|
41
|
+
await apiPostWithPayment("/agents/agent-1/run", { input: { text: "hello" } }, "solana");
|
|
42
|
+
expect(mockPaymentFetch).toHaveBeenCalledWith("https://api.agentwonderland.test/agents/agent-1/run", expect.objectContaining({
|
|
43
|
+
headers: expect.objectContaining({
|
|
44
|
+
"Content-Type": "application/json",
|
|
45
|
+
Accept: "application/json",
|
|
46
|
+
"X-AW-Consumer-Principal": "did:pkh:solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF",
|
|
47
|
+
"X-AW-Rebate-Principal": "did:pkh:eip155:8453:0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
|
48
|
+
}),
|
|
49
|
+
}));
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -12,4 +12,14 @@ describe("formatRunResult", () => {
|
|
|
12
12
|
expect(output).toContain("Covered by credit pack (pack-123)");
|
|
13
13
|
expect(output).not.toContain("Paid:");
|
|
14
14
|
});
|
|
15
|
+
it("formats string-valued settled amounts from job lookups", () => {
|
|
16
|
+
const output = formatRunResult({
|
|
17
|
+
agent_name: "Async Analyzer",
|
|
18
|
+
status: "completed",
|
|
19
|
+
settled_amount: "0.100000",
|
|
20
|
+
job_id: "job-123",
|
|
21
|
+
}, { paymentMethod: "card" });
|
|
22
|
+
expect(output).toContain("Paid: $0.10 via card");
|
|
23
|
+
expect(output).toContain("Job ID: job-123");
|
|
24
|
+
});
|
|
15
25
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
const state = vi.hoisted(() => ({
|
|
3
|
+
apiGet: vi.fn(),
|
|
4
|
+
}));
|
|
5
|
+
vi.mock("../api-client.js", () => ({
|
|
6
|
+
apiGet: (...args) => state.apiGet(...args),
|
|
7
|
+
}));
|
|
8
|
+
describe("getCreditPackInventory", () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
vi.resetModules();
|
|
11
|
+
state.apiGet.mockReset();
|
|
12
|
+
});
|
|
13
|
+
it("requests inventory for the selected payment method principal", async () => {
|
|
14
|
+
state.apiGet.mockResolvedValueOnce({
|
|
15
|
+
consumer_principal: "did:pkh:solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF",
|
|
16
|
+
offers: [],
|
|
17
|
+
balances: [],
|
|
18
|
+
});
|
|
19
|
+
const { getCreditPackInventory } = await import("../passes.js");
|
|
20
|
+
const result = await getCreditPackInventory("agent-1", "solana");
|
|
21
|
+
expect(result?.consumer_principal).toContain("did:pkh:solana:");
|
|
22
|
+
expect(state.apiGet).toHaveBeenCalledWith("/agents/agent-1/credit-packs", {
|
|
23
|
+
ensureConsumerPrincipal: true,
|
|
24
|
+
principalMethod: "solana",
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
const TEST_KEY = "1111111111111111111111111111111111111111111111111111111111111111";
|
|
2
3
|
let currentCard = {
|
|
3
4
|
consumerToken: "consumer_one",
|
|
4
5
|
paymentMethodId: "pm_one",
|
|
5
6
|
last4: "1111",
|
|
6
7
|
brand: "visa",
|
|
7
8
|
};
|
|
8
|
-
|
|
9
|
+
let currentDefaultWallet;
|
|
10
|
+
let currentWallets = [];
|
|
11
|
+
let currentResolvedMethod = null;
|
|
12
|
+
const createdFetches = [vi.fn(), vi.fn(), vi.fn(), vi.fn()];
|
|
9
13
|
const mockMppxCreate = vi.fn();
|
|
10
14
|
const mockStripe = vi.fn((opts) => opts);
|
|
15
|
+
const mockTempoChargeClient = vi.fn((..._args) => "tempo_method");
|
|
16
|
+
const mockBaseChargeClient = vi.fn((..._args) => "base_method");
|
|
11
17
|
vi.mock("../config.js", () => ({
|
|
12
18
|
getApiUrl: () => "http://api.test",
|
|
13
19
|
getCardConfig: () => currentCard,
|
|
14
20
|
getConfig: () => ({ defaultPaymentMethod: "card" }),
|
|
15
|
-
getDefaultWallet: () =>
|
|
16
|
-
getWallets: () =>
|
|
17
|
-
resolveWalletAndChain: () =>
|
|
21
|
+
getDefaultWallet: () => currentDefaultWallet,
|
|
22
|
+
getWallets: () => currentWallets,
|
|
23
|
+
resolveWalletAndChain: () => currentResolvedMethod,
|
|
18
24
|
}));
|
|
19
25
|
vi.mock("mppx/client", () => ({
|
|
20
26
|
Mppx: {
|
|
@@ -22,19 +28,30 @@ vi.mock("mppx/client", () => ({
|
|
|
22
28
|
},
|
|
23
29
|
stripe: (config) => mockStripe(config),
|
|
24
30
|
}));
|
|
25
|
-
|
|
31
|
+
vi.mock("../tempo-charge.js", () => ({
|
|
32
|
+
tempoChargeClient: (config) => mockTempoChargeClient(config),
|
|
33
|
+
}));
|
|
34
|
+
vi.mock("../base-charge.js", () => ({
|
|
35
|
+
baseChargeClient: (config) => mockBaseChargeClient(config),
|
|
36
|
+
}));
|
|
37
|
+
describe("payment method initialization", () => {
|
|
26
38
|
beforeEach(() => {
|
|
27
39
|
vi.clearAllMocks();
|
|
40
|
+
vi.resetModules();
|
|
28
41
|
currentCard = {
|
|
29
42
|
consumerToken: "consumer_one",
|
|
30
43
|
paymentMethodId: "pm_one",
|
|
31
44
|
last4: "1111",
|
|
32
45
|
brand: "visa",
|
|
33
46
|
};
|
|
47
|
+
currentDefaultWallet = undefined;
|
|
48
|
+
currentWallets = [];
|
|
49
|
+
currentResolvedMethod = null;
|
|
34
50
|
mockMppxCreate
|
|
35
51
|
.mockReturnValueOnce({ fetch: createdFetches[0] })
|
|
36
52
|
.mockReturnValueOnce({ fetch: createdFetches[1] })
|
|
37
|
-
.mockReturnValueOnce({ fetch: createdFetches[2] })
|
|
53
|
+
.mockReturnValueOnce({ fetch: createdFetches[2] })
|
|
54
|
+
.mockReturnValueOnce({ fetch: createdFetches[3] });
|
|
38
55
|
});
|
|
39
56
|
it("rebuilds the cached card fetch when the card config changes", async () => {
|
|
40
57
|
const { getPaymentFetch } = await import("../payments.js");
|
|
@@ -48,5 +65,41 @@ describe("card payment fetch cache", () => {
|
|
|
48
65
|
const secondFetch = await getPaymentFetch("card");
|
|
49
66
|
expect(firstFetch).not.toBe(secondFetch);
|
|
50
67
|
expect(mockMppxCreate).toHaveBeenCalledTimes(2);
|
|
68
|
+
expect(mockMppxCreate).toHaveBeenNthCalledWith(1, expect.objectContaining({ polyfill: false }));
|
|
69
|
+
expect(mockMppxCreate).toHaveBeenNthCalledWith(2, expect.objectContaining({ polyfill: false }));
|
|
70
|
+
});
|
|
71
|
+
it("initializes only the Base method when base is requested", async () => {
|
|
72
|
+
const wallet = {
|
|
73
|
+
id: "aw-main",
|
|
74
|
+
keyType: "raw",
|
|
75
|
+
key: TEST_KEY,
|
|
76
|
+
chains: ["tempo", "base"],
|
|
77
|
+
defaultChain: "base",
|
|
78
|
+
};
|
|
79
|
+
currentDefaultWallet = wallet;
|
|
80
|
+
currentWallets = [wallet];
|
|
81
|
+
currentResolvedMethod = { wallet, chain: "base" };
|
|
82
|
+
const { getPaymentFetch } = await import("../payments.js");
|
|
83
|
+
await getPaymentFetch("base");
|
|
84
|
+
expect(mockBaseChargeClient).toHaveBeenCalledTimes(1);
|
|
85
|
+
expect(mockTempoChargeClient).not.toHaveBeenCalled();
|
|
86
|
+
expect(mockMppxCreate).toHaveBeenCalledWith(expect.objectContaining({ methods: ["base_method"], polyfill: false }));
|
|
87
|
+
});
|
|
88
|
+
it("initializes only the Tempo method when tempo is requested", async () => {
|
|
89
|
+
const wallet = {
|
|
90
|
+
id: "aw-main",
|
|
91
|
+
keyType: "raw",
|
|
92
|
+
key: TEST_KEY,
|
|
93
|
+
chains: ["tempo", "base"],
|
|
94
|
+
defaultChain: "tempo",
|
|
95
|
+
};
|
|
96
|
+
currentDefaultWallet = wallet;
|
|
97
|
+
currentWallets = [wallet];
|
|
98
|
+
currentResolvedMethod = { wallet, chain: "tempo" };
|
|
99
|
+
const { getPaymentFetch } = await import("../payments.js");
|
|
100
|
+
await getPaymentFetch("tempo");
|
|
101
|
+
expect(mockTempoChargeClient).toHaveBeenCalledTimes(1);
|
|
102
|
+
expect(mockBaseChargeClient).not.toHaveBeenCalled();
|
|
103
|
+
expect(mockMppxCreate).toHaveBeenCalledWith(expect.objectContaining({ methods: ["tempo_method"], polyfill: false }));
|
|
51
104
|
});
|
|
52
105
|
});
|
|
@@ -13,13 +13,24 @@ vi.mock("../config.js", () => ({
|
|
|
13
13
|
},
|
|
14
14
|
getDefaultWallet: () => state.wallets[0],
|
|
15
15
|
getWallets: () => state.wallets,
|
|
16
|
+
resolveWalletAndChain: (method) => {
|
|
17
|
+
for (const wallet of state.wallets) {
|
|
18
|
+
if (wallet.id === method) {
|
|
19
|
+
return { wallet, chain: wallet.defaultChain ?? wallet.chains[0] };
|
|
20
|
+
}
|
|
21
|
+
if (wallet.chains.includes(method)) {
|
|
22
|
+
return { wallet, chain: method };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
},
|
|
16
27
|
}));
|
|
17
28
|
vi.mock("../payments.js", () => ({
|
|
18
|
-
getWalletAddress: async (
|
|
19
|
-
if (
|
|
20
|
-
return state.addresses[
|
|
29
|
+
getWalletAddress: async (method) => {
|
|
30
|
+
if (method && method in state.addresses) {
|
|
31
|
+
return state.addresses[method] ?? null;
|
|
21
32
|
}
|
|
22
|
-
const wallet = state.wallets.find((entry) => entry.id ===
|
|
33
|
+
const wallet = state.wallets.find((entry) => entry.id === method);
|
|
23
34
|
if (wallet?.key) {
|
|
24
35
|
const { privateKeyToAccount } = await import("viem/accounts");
|
|
25
36
|
return privateKeyToAccount(wallet.key).address;
|
|
@@ -64,4 +75,30 @@ describe("consumer principal helpers", () => {
|
|
|
64
75
|
expect(state.addedWallets).toHaveLength(1);
|
|
65
76
|
expect(state.addedWallets[0]?.chains).toEqual(["tempo", "base"]);
|
|
66
77
|
});
|
|
78
|
+
it("derives a Solana principal when the selected payment method is solana", async () => {
|
|
79
|
+
state.wallets = [
|
|
80
|
+
{ id: "aw-main", keyType: "ows", owsWalletId: "ows-main", chains: ["tempo", "base", "solana"], defaultChain: "tempo" },
|
|
81
|
+
];
|
|
82
|
+
state.addresses = {
|
|
83
|
+
solana: "42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF",
|
|
84
|
+
tempo: "0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
|
|
85
|
+
base: "0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
|
|
86
|
+
};
|
|
87
|
+
const { getConsumerPrincipalForMethod } = await import("../principal.js");
|
|
88
|
+
const principal = await getConsumerPrincipalForMethod("solana");
|
|
89
|
+
expect(principal).toBe("did:pkh:solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF");
|
|
90
|
+
});
|
|
91
|
+
it("returns the Base rebate principal when an EVM wallet is available", async () => {
|
|
92
|
+
state.wallets = [
|
|
93
|
+
{ id: "aw-main", keyType: "ows", owsWalletId: "ows-main", chains: ["tempo", "base", "solana"], defaultChain: "tempo" },
|
|
94
|
+
];
|
|
95
|
+
state.addresses = {
|
|
96
|
+
solana: "42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF",
|
|
97
|
+
tempo: "0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
|
|
98
|
+
base: "0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
|
|
99
|
+
};
|
|
100
|
+
const { getBaseRebatePrincipal } = await import("../principal.js");
|
|
101
|
+
const principal = await getBaseRebatePrincipal();
|
|
102
|
+
expect(principal).toBe("did:pkh:eip155:8453:0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
|
|
103
|
+
});
|
|
67
104
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
describe("resolveRecipientTokenAccount", () => {
|
|
6
|
+
it("uses the recipient directly when Stripe gives us a token account", async () => {
|
|
7
|
+
const { resolveRecipientTokenAccount } = await import("../solana-charge.js");
|
|
8
|
+
const connection = {
|
|
9
|
+
getParsedAccountInfo: vi.fn().mockResolvedValue({
|
|
10
|
+
value: { owner: TOKEN_PROGRAM_ID },
|
|
11
|
+
}),
|
|
12
|
+
};
|
|
13
|
+
const mint = new PublicKey(SOLANA_USDC_MINT);
|
|
14
|
+
const recipient = new PublicKey("42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF");
|
|
15
|
+
const resolved = await resolveRecipientTokenAccount(connection, mint, recipient);
|
|
16
|
+
expect(resolved.tokenAccount.toBase58()).toBe(recipient.toBase58());
|
|
17
|
+
expect(resolved.needsCreateAssociatedAccount).toBe(false);
|
|
18
|
+
expect(connection.getParsedAccountInfo).toHaveBeenCalledTimes(1);
|
|
19
|
+
});
|
|
20
|
+
it("reuses the associated token account when it already exists", async () => {
|
|
21
|
+
const { resolveRecipientTokenAccount } = await import("../solana-charge.js");
|
|
22
|
+
const connection = {
|
|
23
|
+
getParsedAccountInfo: vi.fn()
|
|
24
|
+
.mockResolvedValueOnce({ value: null })
|
|
25
|
+
.mockResolvedValueOnce({ value: { owner: TOKEN_PROGRAM_ID } }),
|
|
26
|
+
};
|
|
27
|
+
const mint = new PublicKey(SOLANA_USDC_MINT);
|
|
28
|
+
const recipient = new PublicKey("G7kW6ZPMAK9v11AaVkK9FHed2Gd4WYwvCbanXUmK9WtS");
|
|
29
|
+
const expectedAta = getAssociatedTokenAddressSync(mint, recipient, false, TOKEN_PROGRAM_ID);
|
|
30
|
+
const resolved = await resolveRecipientTokenAccount(connection, mint, recipient);
|
|
31
|
+
expect(resolved.tokenAccount.toBase58()).toBe(expectedAta.toBase58());
|
|
32
|
+
expect(resolved.needsCreateAssociatedAccount).toBe(false);
|
|
33
|
+
expect(connection.getParsedAccountInfo).toHaveBeenCalledTimes(2);
|
|
34
|
+
});
|
|
35
|
+
it("marks the associated token account for creation when neither account exists", async () => {
|
|
36
|
+
const { resolveRecipientTokenAccount } = await import("../solana-charge.js");
|
|
37
|
+
const connection = {
|
|
38
|
+
getParsedAccountInfo: vi.fn()
|
|
39
|
+
.mockResolvedValueOnce({ value: null })
|
|
40
|
+
.mockResolvedValueOnce({ value: null }),
|
|
41
|
+
};
|
|
42
|
+
const mint = new PublicKey(SOLANA_USDC_MINT);
|
|
43
|
+
const recipient = new PublicKey("G7kW6ZPMAK9v11AaVkK9FHed2Gd4WYwvCbanXUmK9WtS");
|
|
44
|
+
const expectedAta = getAssociatedTokenAddressSync(mint, recipient, false, TOKEN_PROGRAM_ID);
|
|
45
|
+
const resolved = await resolveRecipientTokenAccount(connection, mint, recipient);
|
|
46
|
+
expect(resolved.tokenAccount.toBase58()).toBe(expectedAta.toBase58());
|
|
47
|
+
expect(resolved.needsCreateAssociatedAccount).toBe(true);
|
|
48
|
+
expect(connection.getParsedAccountInfo).toHaveBeenCalledTimes(2);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
const state = {
|
|
3
|
+
policies: {},
|
|
4
|
+
ledger: [],
|
|
5
|
+
};
|
|
6
|
+
vi.mock("../config.js", () => ({
|
|
7
|
+
getSpendPolicy: (method) => state.policies[method] ?? null,
|
|
8
|
+
getSpendLedger: () => state.ledger,
|
|
9
|
+
saveSpendLedger: (entries) => {
|
|
10
|
+
state.ledger = entries;
|
|
11
|
+
},
|
|
12
|
+
}));
|
|
13
|
+
import { canSpend, getDailySpend, recordSpend, requiresPolicyConfirmation, } from "../spend-policy.js";
|
|
14
|
+
describe("spend-policy", () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
state.policies = {};
|
|
17
|
+
state.ledger = [];
|
|
18
|
+
});
|
|
19
|
+
it("allows spend when no policy is set", () => {
|
|
20
|
+
expect(canSpend({ method: "wallet-1", amountUsd: 5 })).toEqual({ ok: true });
|
|
21
|
+
});
|
|
22
|
+
it("blocks spends above max_per_tx", () => {
|
|
23
|
+
state.policies["wallet-1"] = { maxPerTxUsd: 2 };
|
|
24
|
+
const result = canSpend({ method: "wallet-1", amountUsd: 3 });
|
|
25
|
+
expect(result.ok).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
it("tracks daily spend totals per method", () => {
|
|
28
|
+
const now = new Date("2026-04-08T12:00:00.000Z");
|
|
29
|
+
recordSpend("wallet-1", 1.25, now);
|
|
30
|
+
recordSpend("wallet-1", 0.75, now);
|
|
31
|
+
recordSpend("wallet-2", 10, now);
|
|
32
|
+
expect(getDailySpend("wallet-1", now)).toBeCloseTo(2);
|
|
33
|
+
expect(getDailySpend("wallet-2", now)).toBeCloseTo(10);
|
|
34
|
+
});
|
|
35
|
+
it("requires explicit confirmation above configured threshold", () => {
|
|
36
|
+
state.policies["wallet-1"] = { requireConfirmationAboveUsd: 2 };
|
|
37
|
+
expect(requiresPolicyConfirmation("wallet-1", 1.99)).toBe(false);
|
|
38
|
+
expect(requiresPolicyConfirmation("wallet-1", 2)).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function toAtomicAmount(amount: string, decimals?: number): bigint;
|
|
@@ -5,6 +5,7 @@ export declare class ApiError extends Error {
|
|
|
5
5
|
}
|
|
6
6
|
interface RequestOptions {
|
|
7
7
|
ensureConsumerPrincipal?: boolean;
|
|
8
|
+
principalMethod?: string;
|
|
8
9
|
extraHeaders?: Record<string, string>;
|
|
9
10
|
}
|
|
10
11
|
export declare function apiGet<T>(path: string, options?: RequestOptions): Promise<T>;
|
package/dist/core/api-client.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getApiUrl, getApiKey } from "./config.js";
|
|
2
2
|
import { getPaymentFetch } from "./payments.js";
|
|
3
|
-
import {
|
|
3
|
+
import { getBaseRebatePrincipal, ensureConsumerPrincipalForMethod, getConsumerPrincipalForMethod, } from "./principal.js";
|
|
4
4
|
// ── Error class ────────────────────────────────────────────────────
|
|
5
5
|
export class ApiError extends Error {
|
|
6
6
|
status;
|
|
@@ -22,11 +22,15 @@ async function buildHeaders(options) {
|
|
|
22
22
|
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
23
23
|
}
|
|
24
24
|
const principal = options?.ensureConsumerPrincipal
|
|
25
|
-
? await
|
|
26
|
-
: await
|
|
25
|
+
? await ensureConsumerPrincipalForMethod(options?.principalMethod)
|
|
26
|
+
: await getConsumerPrincipalForMethod(options?.principalMethod);
|
|
27
27
|
if (principal) {
|
|
28
28
|
headers["X-AW-Consumer-Principal"] = principal;
|
|
29
29
|
}
|
|
30
|
+
const rebatePrincipal = await getBaseRebatePrincipal();
|
|
31
|
+
if (rebatePrincipal) {
|
|
32
|
+
headers["X-AW-Rebate-Principal"] = rebatePrincipal;
|
|
33
|
+
}
|
|
30
34
|
if (options?.extraHeaders) {
|
|
31
35
|
Object.assign(headers, options.extraHeaders);
|
|
32
36
|
}
|
|
@@ -109,6 +113,7 @@ export async function apiPostWithPayment(path, body, payWith, options) {
|
|
|
109
113
|
method: "POST",
|
|
110
114
|
headers: await buildHeaders({
|
|
111
115
|
ensureConsumerPrincipal: true,
|
|
116
|
+
principalMethod: payWith,
|
|
112
117
|
...options,
|
|
113
118
|
}),
|
|
114
119
|
body: JSON.stringify(body),
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function fetchUsdcBalance(chain: "tempo" | "base" | "solana", address: string): Promise<string | null>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { getSettings } from "./settings.js";
|
|
2
|
+
const ERC20_BALANCE_ABI = [
|
|
3
|
+
{
|
|
4
|
+
type: "function",
|
|
5
|
+
name: "balanceOf",
|
|
6
|
+
stateMutability: "view",
|
|
7
|
+
inputs: [{ name: "account", type: "address" }],
|
|
8
|
+
outputs: [{ name: "", type: "uint256" }],
|
|
9
|
+
},
|
|
10
|
+
];
|
|
11
|
+
async function fetchEvmUsdcBalance(chain, address) {
|
|
12
|
+
try {
|
|
13
|
+
const { createPublicClient, http, formatUnits } = await import("viem");
|
|
14
|
+
const client = createPublicClient({ transport: http(chain.rpcUrl) });
|
|
15
|
+
const raw = await client.readContract({
|
|
16
|
+
address: chain.usdc,
|
|
17
|
+
abi: ERC20_BALANCE_ABI,
|
|
18
|
+
functionName: "balanceOf",
|
|
19
|
+
args: [address],
|
|
20
|
+
});
|
|
21
|
+
return formatUnits(raw, 6);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function fetchSolanaUsdcBalance(chain, address) {
|
|
28
|
+
try {
|
|
29
|
+
const { Connection, PublicKey } = await import("@solana/web3.js");
|
|
30
|
+
const connection = new Connection(chain.rpcUrl, "confirmed");
|
|
31
|
+
const owner = new PublicKey(address);
|
|
32
|
+
const mint = new PublicKey(chain.usdc);
|
|
33
|
+
const accounts = await connection.getParsedTokenAccountsByOwner(owner, { mint });
|
|
34
|
+
let total = 0;
|
|
35
|
+
for (const { account } of accounts.value) {
|
|
36
|
+
const amount = account.data?.parsed?.info?.tokenAmount?.uiAmount;
|
|
37
|
+
if (typeof amount === "number")
|
|
38
|
+
total += amount;
|
|
39
|
+
}
|
|
40
|
+
return total.toString();
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export async function fetchUsdcBalance(chain, address) {
|
|
47
|
+
const settings = await getSettings();
|
|
48
|
+
if (!settings)
|
|
49
|
+
return null;
|
|
50
|
+
const chainConfig = settings.chains[chain];
|
|
51
|
+
if (!chainConfig)
|
|
52
|
+
return null;
|
|
53
|
+
if (chain === "solana")
|
|
54
|
+
return fetchSolanaUsdcBalance(chainConfig, address);
|
|
55
|
+
return fetchEvmUsdcBalance(chainConfig, address);
|
|
56
|
+
}
|
package/dist/core/base-charge.js
CHANGED
|
@@ -5,9 +5,12 @@
|
|
|
5
5
|
* the tx hash as a credential. Plugs into mppx's compose/dispatch system.
|
|
6
6
|
*/
|
|
7
7
|
import { Method, Credential, z } from "mppx";
|
|
8
|
+
import { toAtomicAmount } from "./amount-utils.js";
|
|
8
9
|
// Base USDC (Circle native)
|
|
9
10
|
const BASE_USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
11
|
+
const BASE_SEPOLIA_USDC = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
|
|
10
12
|
const BASE_CHAIN_ID = 8453;
|
|
13
|
+
const BASE_SEPOLIA_CHAIN_ID = 84532;
|
|
11
14
|
// Standard ERC-20 transfer function ABI
|
|
12
15
|
const ERC20_ABI = [
|
|
13
16
|
{
|
|
@@ -50,20 +53,25 @@ export function baseChargeClient(config) {
|
|
|
50
53
|
return Method.toClient(baseChargeMethod, {
|
|
51
54
|
async createCredential({ challenge }) {
|
|
52
55
|
const { request } = challenge;
|
|
53
|
-
const amount =
|
|
56
|
+
const amount = toAtomicAmount(request.amount, request.decimals ?? 6);
|
|
54
57
|
const recipient = request.recipient;
|
|
55
|
-
const
|
|
58
|
+
const chainId = request.chainId ?? BASE_CHAIN_ID;
|
|
59
|
+
const currency = (request.currency
|
|
60
|
+
?? (chainId === BASE_SEPOLIA_CHAIN_ID ? BASE_SEPOLIA_USDC : BASE_USDC));
|
|
56
61
|
// Dynamic imports to keep the module lightweight
|
|
57
|
-
const { createWalletClient, createPublicClient, http
|
|
58
|
-
const { base } = await import("viem/chains");
|
|
59
|
-
const
|
|
62
|
+
const { createWalletClient, createPublicClient, http } = await import("viem");
|
|
63
|
+
const { base, baseSepolia } = await import("viem/chains");
|
|
64
|
+
const chain = chainId === BASE_SEPOLIA_CHAIN_ID ? baseSepolia : base;
|
|
65
|
+
const rpcUrl = config.rpcUrl ?? (chainId === BASE_SEPOLIA_CHAIN_ID
|
|
66
|
+
? "https://sepolia.base.org"
|
|
67
|
+
: "https://mainnet.base.org");
|
|
60
68
|
const walletClient = createWalletClient({
|
|
61
69
|
account: config.account,
|
|
62
|
-
chain
|
|
70
|
+
chain,
|
|
63
71
|
transport: http(rpcUrl),
|
|
64
72
|
});
|
|
65
73
|
const publicClient = createPublicClient({
|
|
66
|
-
chain
|
|
74
|
+
chain,
|
|
67
75
|
transport: http(rpcUrl),
|
|
68
76
|
});
|
|
69
77
|
// Send ERC-20 transfer
|
|
@@ -78,7 +86,7 @@ export function baseChargeClient(config) {
|
|
|
78
86
|
return Credential.serialize({
|
|
79
87
|
challenge,
|
|
80
88
|
payload: { hash, type: "hash" },
|
|
81
|
-
source: `did:pkh:eip155:${
|
|
89
|
+
source: `did:pkh:eip155:${chainId}:${config.account.address}`,
|
|
82
90
|
});
|
|
83
91
|
},
|
|
84
92
|
});
|
package/dist/core/config.d.ts
CHANGED
|
@@ -13,6 +13,17 @@ export interface CardConfig {
|
|
|
13
13
|
last4: string;
|
|
14
14
|
brand: string;
|
|
15
15
|
}
|
|
16
|
+
export interface SpendPolicy {
|
|
17
|
+
maxPerTxUsd?: number;
|
|
18
|
+
maxPerDayUsd?: number;
|
|
19
|
+
requireConfirmationAboveUsd?: number;
|
|
20
|
+
}
|
|
21
|
+
export interface SpendLedgerEntry {
|
|
22
|
+
method: string;
|
|
23
|
+
amountUsd: number;
|
|
24
|
+
day: string;
|
|
25
|
+
timestamp: string;
|
|
26
|
+
}
|
|
16
27
|
export interface Config {
|
|
17
28
|
apiUrl: string;
|
|
18
29
|
apiKey: string | null;
|
|
@@ -27,6 +38,10 @@ export interface Config {
|
|
|
27
38
|
confirmBeforeSpend: boolean;
|
|
28
39
|
/** Auto-tip amount in USD for successful runs. Default: 0 (no auto-tip). */
|
|
29
40
|
defaultTipAmount: number;
|
|
41
|
+
/** Optional per-method spend policies enforced client-side by MCP tools. */
|
|
42
|
+
spendPolicies?: Record<string, SpendPolicy>;
|
|
43
|
+
/** Daily spend ledger used to enforce max-per-day limits. */
|
|
44
|
+
spendLedger?: SpendLedgerEntry[];
|
|
30
45
|
}
|
|
31
46
|
/** All supported chain identifiers. */
|
|
32
47
|
export declare const SUPPORTED_CHAINS: readonly ["tempo", "base", "solana"];
|
|
@@ -43,6 +58,10 @@ export declare function getApiKey(): string | null;
|
|
|
43
58
|
export declare function isAuthenticated(): boolean;
|
|
44
59
|
export declare function requiresSpendConfirmation(): boolean;
|
|
45
60
|
export declare function getDefaultTipAmount(): number;
|
|
61
|
+
export declare function getSpendPolicy(method: string): SpendPolicy | null;
|
|
62
|
+
export declare function setSpendPolicy(method: string, policy: SpendPolicy): void;
|
|
63
|
+
export declare function getSpendLedger(): SpendLedgerEntry[];
|
|
64
|
+
export declare function saveSpendLedger(entries: SpendLedgerEntry[]): void;
|
|
46
65
|
/**
|
|
47
66
|
* Get all wallets from config + env var synthetic wallets.
|
|
48
67
|
*/
|
package/dist/core/config.js
CHANGED
|
@@ -40,6 +40,8 @@ function migrateIfNeeded(raw) {
|
|
|
40
40
|
favorites: r.favorites ?? [],
|
|
41
41
|
confirmBeforeSpend: r.confirmBeforeSpend !== false,
|
|
42
42
|
defaultTipAmount: typeof r.defaultTipAmount === "number" ? r.defaultTipAmount : 0,
|
|
43
|
+
spendPolicies: r.spendPolicies,
|
|
44
|
+
spendLedger: r.spendLedger,
|
|
43
45
|
};
|
|
44
46
|
}
|
|
45
47
|
// Build wallets from legacy flat fields
|
|
@@ -111,6 +113,8 @@ function migrateIfNeeded(raw) {
|
|
|
111
113
|
favorites: [],
|
|
112
114
|
confirmBeforeSpend: true,
|
|
113
115
|
defaultTipAmount: 0,
|
|
116
|
+
spendPolicies: {},
|
|
117
|
+
spendLedger: [],
|
|
114
118
|
};
|
|
115
119
|
// Write migrated config (only if there was something to migrate)
|
|
116
120
|
if (raw.tempoPrivateKey || raw.evmPrivateKey || raw.stripeConsumerToken) {
|
|
@@ -132,6 +136,8 @@ export function getConfig() {
|
|
|
132
136
|
favorites: [],
|
|
133
137
|
confirmBeforeSpend: true,
|
|
134
138
|
defaultTipAmount: 0,
|
|
139
|
+
spendPolicies: {},
|
|
140
|
+
spendLedger: [],
|
|
135
141
|
};
|
|
136
142
|
if (!existsSync(CONFIG_FILE)) {
|
|
137
143
|
return defaults;
|
|
@@ -172,6 +178,22 @@ export function requiresSpendConfirmation() {
|
|
|
172
178
|
export function getDefaultTipAmount() {
|
|
173
179
|
return getConfig().defaultTipAmount;
|
|
174
180
|
}
|
|
181
|
+
export function getSpendPolicy(method) {
|
|
182
|
+
const policies = getConfig().spendPolicies ?? {};
|
|
183
|
+
return policies[method] ?? null;
|
|
184
|
+
}
|
|
185
|
+
export function setSpendPolicy(method, policy) {
|
|
186
|
+
const config = getConfig();
|
|
187
|
+
const policies = config.spendPolicies ?? {};
|
|
188
|
+
policies[method] = policy;
|
|
189
|
+
saveConfig({ spendPolicies: policies });
|
|
190
|
+
}
|
|
191
|
+
export function getSpendLedger() {
|
|
192
|
+
return getConfig().spendLedger ?? [];
|
|
193
|
+
}
|
|
194
|
+
export function saveSpendLedger(entries) {
|
|
195
|
+
saveConfig({ spendLedger: entries });
|
|
196
|
+
}
|
|
175
197
|
// ── Wallet helpers ─────────────────────────────────────────────────
|
|
176
198
|
/**
|
|
177
199
|
* Get all wallets from config + env var synthetic wallets.
|