@agentwonderland/mcp 0.1.25 → 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__/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 +10 -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/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 +13 -6
- package/dist/core/formatters.d.ts +3 -2
- package/dist/core/formatters.js +7 -1
- 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 +20 -7
- 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 +29 -7
- package/dist/core/tempo-charge.d.ts +7 -0
- package/dist/core/tempo-charge.js +84 -0
- package/dist/index.js +5 -4
- 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/jobs.js +8 -1
- package/dist/tools/passes.js +11 -6
- package/dist/tools/run.js +16 -12
- package/dist/tools/solve.js +22 -15
- package/dist/tools/wallet.js +32 -12
- package/package.json +2 -2
- 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 +17 -6
- package/src/core/__tests__/principal.test.ts +49 -4
- package/src/core/__tests__/solana-charge.test.ts +59 -0
- package/src/core/api-client.ts +16 -3
- package/src/core/balances.ts +63 -0
- package/src/core/base-charge.ts +13 -6
- package/src/core/formatters.ts +10 -3
- package/src/core/passes.ts +5 -2
- package/src/core/payments.ts +22 -7
- package/src/core/principal.ts +42 -1
- package/src/core/settings.ts +36 -0
- package/src/core/solana-charge.ts +43 -9
- package/src/core/tempo-charge.ts +104 -0
- package/src/index.ts +5 -4
- 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/jobs.ts +10 -1
- package/src/tools/passes.ts +11 -5
- package/src/tools/run.ts +19 -11
- package/src/tools/solve.ts +25 -14
- package/src/tools/wallet.ts +30 -14
|
@@ -14,4 +14,16 @@ describe("formatRunResult", () => {
|
|
|
14
14
|
expect(output).toContain("Covered by credit pack (pack-123)");
|
|
15
15
|
expect(output).not.toContain("Paid:");
|
|
16
16
|
});
|
|
17
|
+
|
|
18
|
+
it("formats string-valued settled amounts from job lookups", () => {
|
|
19
|
+
const output = formatRunResult({
|
|
20
|
+
agent_name: "Async Analyzer",
|
|
21
|
+
status: "completed",
|
|
22
|
+
settled_amount: "0.100000",
|
|
23
|
+
job_id: "job-123",
|
|
24
|
+
}, { paymentMethod: "card" });
|
|
25
|
+
|
|
26
|
+
expect(output).toContain("Paid: $0.10 via card");
|
|
27
|
+
expect(output).toContain("Job ID: job-123");
|
|
28
|
+
});
|
|
17
29
|
});
|
|
@@ -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
|
+
});
|
|
@@ -16,7 +16,7 @@ let currentResolvedMethod: { wallet: any; chain: string } | null = null;
|
|
|
16
16
|
const createdFetches = [vi.fn(), vi.fn(), vi.fn(), vi.fn()];
|
|
17
17
|
const mockMppxCreate = vi.fn();
|
|
18
18
|
const mockStripe = vi.fn((opts: unknown) => opts);
|
|
19
|
-
const
|
|
19
|
+
const mockTempoChargeClient = vi.fn((..._args: unknown[]) => "tempo_method");
|
|
20
20
|
const mockBaseChargeClient = vi.fn((..._args: unknown[]) => "base_method");
|
|
21
21
|
|
|
22
22
|
vi.mock("../config.js", () => ({
|
|
@@ -33,7 +33,10 @@ vi.mock("mppx/client", () => ({
|
|
|
33
33
|
create: (config: unknown) => mockMppxCreate(config),
|
|
34
34
|
},
|
|
35
35
|
stripe: (config: unknown) => mockStripe(config),
|
|
36
|
-
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
vi.mock("../tempo-charge.js", () => ({
|
|
39
|
+
tempoChargeClient: (config: unknown) => mockTempoChargeClient(config),
|
|
37
40
|
}));
|
|
38
41
|
|
|
39
42
|
vi.mock("../base-charge.js", () => ({
|
|
@@ -74,6 +77,14 @@ describe("payment method initialization", () => {
|
|
|
74
77
|
|
|
75
78
|
expect(firstFetch).not.toBe(secondFetch);
|
|
76
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
|
+
);
|
|
77
88
|
});
|
|
78
89
|
|
|
79
90
|
it("initializes only the Base method when base is requested", async () => {
|
|
@@ -92,9 +103,9 @@ describe("payment method initialization", () => {
|
|
|
92
103
|
await getPaymentFetch("base");
|
|
93
104
|
|
|
94
105
|
expect(mockBaseChargeClient).toHaveBeenCalledTimes(1);
|
|
95
|
-
expect(
|
|
106
|
+
expect(mockTempoChargeClient).not.toHaveBeenCalled();
|
|
96
107
|
expect(mockMppxCreate).toHaveBeenCalledWith(
|
|
97
|
-
expect.objectContaining({ methods: ["base_method"] }),
|
|
108
|
+
expect.objectContaining({ methods: ["base_method"], polyfill: false }),
|
|
98
109
|
);
|
|
99
110
|
});
|
|
100
111
|
|
|
@@ -113,10 +124,10 @@ describe("payment method initialization", () => {
|
|
|
113
124
|
const { getPaymentFetch } = await import("../payments.js");
|
|
114
125
|
await getPaymentFetch("tempo");
|
|
115
126
|
|
|
116
|
-
expect(
|
|
127
|
+
expect(mockTempoChargeClient).toHaveBeenCalledTimes(1);
|
|
117
128
|
expect(mockBaseChargeClient).not.toHaveBeenCalled();
|
|
118
129
|
expect(mockMppxCreate).toHaveBeenCalledWith(
|
|
119
|
-
expect.objectContaining({ methods: ["tempo_method"] }),
|
|
130
|
+
expect.objectContaining({ methods: ["tempo_method"], polyfill: false }),
|
|
120
131
|
);
|
|
121
132
|
});
|
|
122
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
|
+
});
|
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
|
@@ -10,7 +10,9 @@ import { toAtomicAmount } from "./amount-utils.js";
|
|
|
10
10
|
|
|
11
11
|
// Base USDC (Circle native)
|
|
12
12
|
const BASE_USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" as const;
|
|
13
|
+
const BASE_SEPOLIA_USDC = "0x036CbD53842c5426634e7929541eC2318f3dCF7e" as const;
|
|
13
14
|
const BASE_CHAIN_ID = 8453;
|
|
15
|
+
const BASE_SEPOLIA_CHAIN_ID = 84532;
|
|
14
16
|
|
|
15
17
|
// Standard ERC-20 transfer function ABI
|
|
16
18
|
const ERC20_ABI = [
|
|
@@ -66,22 +68,27 @@ export function baseChargeClient(config: BaseChargeClientConfig) {
|
|
|
66
68
|
const { request } = challenge;
|
|
67
69
|
const amount = toAtomicAmount(request.amount, request.decimals ?? 6);
|
|
68
70
|
const recipient = request.recipient as `0x${string}`;
|
|
69
|
-
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}`;
|
|
70
74
|
|
|
71
75
|
// Dynamic imports to keep the module lightweight
|
|
72
76
|
const { createWalletClient, createPublicClient, http } = await import("viem");
|
|
73
|
-
const { base } = await import("viem/chains");
|
|
77
|
+
const { base, baseSepolia } = await import("viem/chains");
|
|
74
78
|
|
|
75
|
-
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");
|
|
76
83
|
|
|
77
84
|
const walletClient = createWalletClient({
|
|
78
85
|
account: config.account,
|
|
79
|
-
chain
|
|
86
|
+
chain,
|
|
80
87
|
transport: http(rpcUrl),
|
|
81
88
|
});
|
|
82
89
|
|
|
83
90
|
const publicClient = createPublicClient({
|
|
84
|
-
chain
|
|
91
|
+
chain,
|
|
85
92
|
transport: http(rpcUrl),
|
|
86
93
|
});
|
|
87
94
|
|
|
@@ -99,7 +106,7 @@ export function baseChargeClient(config: BaseChargeClientConfig) {
|
|
|
99
106
|
return Credential.serialize({
|
|
100
107
|
challenge,
|
|
101
108
|
payload: { hash, type: "hash" as const },
|
|
102
|
-
source: `did:pkh:eip155:${
|
|
109
|
+
source: `did:pkh:eip155:${chainId}:${config.account.address}`,
|
|
103
110
|
});
|
|
104
111
|
},
|
|
105
112
|
});
|
package/src/core/formatters.ts
CHANGED
|
@@ -112,14 +112,21 @@ interface RunResultLike {
|
|
|
112
112
|
error_code?: string;
|
|
113
113
|
latency_ms?: number;
|
|
114
114
|
execution_latency_ms?: number;
|
|
115
|
-
cost?: number;
|
|
116
|
-
|
|
115
|
+
cost?: number | string;
|
|
116
|
+
settled_amount?: number | string;
|
|
117
|
+
estimated_cost?: number | string;
|
|
117
118
|
input_tokens?: number;
|
|
118
119
|
tags?: string[];
|
|
119
120
|
consumption_mode?: string;
|
|
120
121
|
credit_pack_id?: string;
|
|
121
122
|
}
|
|
122
123
|
|
|
124
|
+
function toDisplayCost(value: number | string | null | undefined): number | null {
|
|
125
|
+
if (value == null) return null;
|
|
126
|
+
const numeric = typeof value === "number" ? value : Number.parseFloat(value);
|
|
127
|
+
return Number.isFinite(numeric) ? numeric : null;
|
|
128
|
+
}
|
|
129
|
+
|
|
123
130
|
export function formatRunResult(result: RunResultLike, opts?: { paymentMethod?: string }): string {
|
|
124
131
|
const lines: string[] = [];
|
|
125
132
|
|
|
@@ -146,7 +153,7 @@ export function formatRunResult(result: RunResultLike, opts?: { paymentMethod?:
|
|
|
146
153
|
}
|
|
147
154
|
|
|
148
155
|
// Summary line
|
|
149
|
-
const cost = result.cost ?? result.estimated_cost;
|
|
156
|
+
const cost = toDisplayCost(result.cost ?? result.settled_amount ?? result.estimated_cost);
|
|
150
157
|
const usedCreditPack = result.consumption_mode === "credit_pack";
|
|
151
158
|
const status = result.status === "success" || result.status === "completed" ? "✓" : "✗";
|
|
152
159
|
const agent = result.agent_name ?? result.agent_id ?? "";
|
package/src/core/passes.ts
CHANGED
|
@@ -45,9 +45,12 @@ export function getCreditPackProgram(agent: AgentRecord): CreditPackProgramInfo
|
|
|
45
45
|
return creditPacks ?? null;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
export async function getCreditPackInventory(agentId: string): Promise<CreditPackInventory | null> {
|
|
48
|
+
export async function getCreditPackInventory(agentId: string, method?: string): Promise<CreditPackInventory | null> {
|
|
49
49
|
try {
|
|
50
|
-
return await apiGet<CreditPackInventory>(`/agents/${agentId}/credit-packs`, {
|
|
50
|
+
return await apiGet<CreditPackInventory>(`/agents/${agentId}/credit-packs`, {
|
|
51
|
+
ensureConsumerPrincipal: true,
|
|
52
|
+
principalMethod: method,
|
|
53
|
+
});
|
|
51
54
|
} catch {
|
|
52
55
|
return null;
|
|
53
56
|
}
|
package/src/core/payments.ts
CHANGED
|
@@ -21,6 +21,15 @@ import {
|
|
|
21
21
|
} from "./config.js";
|
|
22
22
|
import type { AgentRecord } from "./types.js";
|
|
23
23
|
|
|
24
|
+
// Feature flag: disable card payment for launch. Flip to true once Stripe
|
|
25
|
+
// approves the live SPT issuer. Config/card state is still persisted so this
|
|
26
|
+
// can be re-enabled without reconfiguring wallets.
|
|
27
|
+
const ENABLE_CARD_PAYMENT = false;
|
|
28
|
+
|
|
29
|
+
export function isCardPaymentEnabled(): boolean {
|
|
30
|
+
return ENABLE_CARD_PAYMENT;
|
|
31
|
+
}
|
|
32
|
+
|
|
24
33
|
// Cache per wallet+chain combo to avoid re-initializing
|
|
25
34
|
const fetchCache = new Map<string, typeof fetch>();
|
|
26
35
|
const REGISTRY_METHOD_MAP: Record<string, string> = {
|
|
@@ -68,7 +77,7 @@ async function initEvmMppForChain(
|
|
|
68
77
|
chain: "tempo" | "base",
|
|
69
78
|
): Promise<typeof fetch | null> {
|
|
70
79
|
try {
|
|
71
|
-
const { Mppx
|
|
80
|
+
const { Mppx } = await import("mppx/client");
|
|
72
81
|
let account;
|
|
73
82
|
|
|
74
83
|
if (wallet.keyType === "ows" && wallet.owsWalletId) {
|
|
@@ -83,13 +92,14 @@ async function initEvmMppForChain(
|
|
|
83
92
|
|
|
84
93
|
const methods = [];
|
|
85
94
|
if (chain === "tempo") {
|
|
86
|
-
|
|
95
|
+
const { tempoChargeClient } = await import("./tempo-charge.js");
|
|
96
|
+
methods.push(tempoChargeClient({ account }));
|
|
87
97
|
} else {
|
|
88
98
|
const { baseChargeClient } = await import("./base-charge.js");
|
|
89
99
|
methods.push(baseChargeClient({ account }));
|
|
90
100
|
}
|
|
91
101
|
|
|
92
|
-
const mppx = Mppx.create({ methods: methods as any });
|
|
102
|
+
const mppx = Mppx.create({ methods: methods as any, polyfill: false });
|
|
93
103
|
return mppx.fetch.bind(mppx) as typeof fetch;
|
|
94
104
|
} catch {
|
|
95
105
|
return null;
|
|
@@ -106,6 +116,7 @@ async function initSolanaMpp(wallet: WalletEntry): Promise<typeof fetch | null>
|
|
|
106
116
|
const { solanaChargeClient } = await import("./solana-charge.js");
|
|
107
117
|
const mppx = Mppx.create({
|
|
108
118
|
methods: [solanaChargeClient({ wallet })] as any,
|
|
119
|
+
polyfill: false,
|
|
109
120
|
});
|
|
110
121
|
return mppx.fetch.bind(mppx) as typeof fetch;
|
|
111
122
|
} catch {
|
|
@@ -146,6 +157,7 @@ async function initCard(): Promise<typeof fetch | null> {
|
|
|
146
157
|
return spt;
|
|
147
158
|
},
|
|
148
159
|
})] as any,
|
|
160
|
+
polyfill: false,
|
|
149
161
|
});
|
|
150
162
|
return mppx.fetch.bind(mppx) as typeof fetch;
|
|
151
163
|
} catch {
|
|
@@ -177,6 +189,9 @@ async function initForChain(wallet: WalletEntry, chain: string): Promise<typeof
|
|
|
177
189
|
export async function getPaymentFetch(method?: string): Promise<typeof fetch> {
|
|
178
190
|
// Card payment
|
|
179
191
|
if (method === "card") {
|
|
192
|
+
if (!ENABLE_CARD_PAYMENT) {
|
|
193
|
+
throw new Error('Card payments are temporarily unavailable. Use a crypto wallet (tempo, base, or solana).');
|
|
194
|
+
}
|
|
180
195
|
const ck = cardCacheKey();
|
|
181
196
|
clearStaleCardCache(ck ?? undefined);
|
|
182
197
|
if (!ck) {
|
|
@@ -286,14 +301,14 @@ export function getConfiguredMethods(): string[] {
|
|
|
286
301
|
}
|
|
287
302
|
}
|
|
288
303
|
|
|
289
|
-
// Add card if configured
|
|
290
|
-
if (getCardConfig()) {
|
|
304
|
+
// Add card if configured (gated for launch)
|
|
305
|
+
if (ENABLE_CARD_PAYMENT && getCardConfig()) {
|
|
291
306
|
methods.push("card");
|
|
292
307
|
}
|
|
293
308
|
|
|
294
|
-
// Respect defaultPaymentMethod — move it to front of list
|
|
309
|
+
// Respect defaultPaymentMethod — move it to front of list (ignore card when disabled)
|
|
295
310
|
const defaultMethod = getConfig().defaultPaymentMethod;
|
|
296
|
-
if (defaultMethod) {
|
|
311
|
+
if (defaultMethod && (defaultMethod !== "card" || ENABLE_CARD_PAYMENT)) {
|
|
297
312
|
const idx = methods.indexOf(defaultMethod);
|
|
298
313
|
if (idx > 0) {
|
|
299
314
|
methods.splice(idx, 1);
|
package/src/core/principal.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
addWallet,
|
|
3
|
+
getDefaultWallet,
|
|
4
|
+
getWallets,
|
|
5
|
+
resolveWalletAndChain,
|
|
6
|
+
type WalletEntry,
|
|
7
|
+
} from "./config.js";
|
|
2
8
|
import { getWalletAddress } from "./payments.js";
|
|
3
9
|
import {
|
|
4
10
|
createOwsWallet,
|
|
@@ -39,6 +45,12 @@ async function walletPrincipal(wallet: WalletEntry): Promise<string | null> {
|
|
|
39
45
|
return null;
|
|
40
46
|
}
|
|
41
47
|
|
|
48
|
+
async function principalForChain(chain: string): Promise<string | null> {
|
|
49
|
+
const address = await getWalletAddress(chain);
|
|
50
|
+
if (!address) return null;
|
|
51
|
+
return principalFromAddress(address, chain === "solana" ? "solana" : "evm");
|
|
52
|
+
}
|
|
53
|
+
|
|
42
54
|
function preferredWallets(): WalletEntry[] {
|
|
43
55
|
const wallets = getWallets();
|
|
44
56
|
const defaultWallet = getDefaultWallet();
|
|
@@ -115,6 +127,21 @@ export async function getConsumerPrincipal(): Promise<string | null> {
|
|
|
115
127
|
return null;
|
|
116
128
|
}
|
|
117
129
|
|
|
130
|
+
export async function getBaseRebatePrincipal(): Promise<string | null> {
|
|
131
|
+
return (await principalForChain("base")) ?? principalForChain("tempo");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function getConsumerPrincipalForMethod(method?: string): Promise<string | null> {
|
|
135
|
+
if (!method || method === "card") {
|
|
136
|
+
return getConsumerPrincipal();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const resolved = resolveWalletAndChain(method);
|
|
140
|
+
if (!resolved) return null;
|
|
141
|
+
|
|
142
|
+
return principalForChain(resolved.chain);
|
|
143
|
+
}
|
|
144
|
+
|
|
118
145
|
export async function ensureConsumerPrincipal(): Promise<string> {
|
|
119
146
|
const existing = await getConsumerPrincipal();
|
|
120
147
|
if (existing) return existing;
|
|
@@ -126,3 +153,17 @@ export async function ensureConsumerPrincipal(): Promise<string> {
|
|
|
126
153
|
}
|
|
127
154
|
return principal;
|
|
128
155
|
}
|
|
156
|
+
|
|
157
|
+
export async function ensureConsumerPrincipalForMethod(method?: string): Promise<string> {
|
|
158
|
+
const existing = await getConsumerPrincipalForMethod(method);
|
|
159
|
+
if (existing) return existing;
|
|
160
|
+
|
|
161
|
+
if (method && method !== "card") {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`Could not derive a consumer principal for payment method "${method}". ` +
|
|
164
|
+
"Check wallet_status and confirm that chain is configured for the active wallet.",
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return ensureConsumerPrincipal();
|
|
169
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { getApiUrl } from "./config.js";
|
|
2
|
+
|
|
3
|
+
export interface ChainSettings {
|
|
4
|
+
chainId: number | string;
|
|
5
|
+
usdc: string;
|
|
6
|
+
rpcUrl: string;
|
|
7
|
+
explorer?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface PublicSettings {
|
|
11
|
+
network: "testnet" | "mainnet";
|
|
12
|
+
stripe: { mode: "test" | "live" };
|
|
13
|
+
chains: {
|
|
14
|
+
tempo: ChainSettings;
|
|
15
|
+
base: ChainSettings;
|
|
16
|
+
solana: ChainSettings & { cluster: "mainnet-beta" | "devnet" };
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let cache: { data: PublicSettings; expiresAt: number } | null = null;
|
|
21
|
+
const TTL_MS = 5 * 60 * 1000;
|
|
22
|
+
|
|
23
|
+
export async function getSettings(): Promise<PublicSettings | null> {
|
|
24
|
+
const now = Date.now();
|
|
25
|
+
if (cache && cache.expiresAt > now) return cache.data;
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const res = await fetch(`${getApiUrl()}/settings`);
|
|
29
|
+
if (!res.ok) return cache?.data ?? null;
|
|
30
|
+
const data = await res.json() as PublicSettings;
|
|
31
|
+
cache = { data, expiresAt: now + TTL_MS };
|
|
32
|
+
return data;
|
|
33
|
+
} catch {
|
|
34
|
+
return cache?.data ?? null;
|
|
35
|
+
}
|
|
36
|
+
}
|