@remitmd/sdk 0.1.0

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.
Files changed (132) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +250 -0
  3. package/dist/a2a.d.ts +137 -0
  4. package/dist/a2a.d.ts.map +1 -0
  5. package/dist/a2a.js +121 -0
  6. package/dist/a2a.js.map +1 -0
  7. package/dist/client.d.ts +41 -0
  8. package/dist/client.d.ts.map +1 -0
  9. package/dist/client.js +81 -0
  10. package/dist/client.js.map +1 -0
  11. package/dist/errors.d.ts +108 -0
  12. package/dist/errors.d.ts.map +1 -0
  13. package/dist/errors.js +218 -0
  14. package/dist/errors.js.map +1 -0
  15. package/dist/http.d.ts +23 -0
  16. package/dist/http.d.ts.map +1 -0
  17. package/dist/http.js +150 -0
  18. package/dist/http.js.map +1 -0
  19. package/dist/index.d.ts +18 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +21 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/integrations/vercel-ai.d.ts +44 -0
  24. package/dist/integrations/vercel-ai.d.ts.map +1 -0
  25. package/dist/integrations/vercel-ai.js +175 -0
  26. package/dist/integrations/vercel-ai.js.map +1 -0
  27. package/dist/models/bounty.d.ts +22 -0
  28. package/dist/models/bounty.d.ts.map +1 -0
  29. package/dist/models/bounty.js +2 -0
  30. package/dist/models/bounty.js.map +1 -0
  31. package/dist/models/common.d.ts +78 -0
  32. package/dist/models/common.d.ts.map +1 -0
  33. package/dist/models/common.js +3 -0
  34. package/dist/models/common.js.map +1 -0
  35. package/dist/models/deposit.d.ts +13 -0
  36. package/dist/models/deposit.d.ts.map +1 -0
  37. package/dist/models/deposit.js +2 -0
  38. package/dist/models/deposit.js.map +1 -0
  39. package/dist/models/escrow.d.ts +16 -0
  40. package/dist/models/escrow.d.ts.map +1 -0
  41. package/dist/models/escrow.js +2 -0
  42. package/dist/models/escrow.js.map +1 -0
  43. package/dist/models/index.d.ts +9 -0
  44. package/dist/models/index.d.ts.map +1 -0
  45. package/dist/models/index.js +9 -0
  46. package/dist/models/index.js.map +1 -0
  47. package/dist/models/invoice.d.ts +30 -0
  48. package/dist/models/invoice.d.ts.map +1 -0
  49. package/dist/models/invoice.js +2 -0
  50. package/dist/models/invoice.js.map +1 -0
  51. package/dist/models/reputation.d.ts +7 -0
  52. package/dist/models/reputation.d.ts.map +1 -0
  53. package/dist/models/reputation.js +2 -0
  54. package/dist/models/reputation.js.map +1 -0
  55. package/dist/models/stream.d.ts +15 -0
  56. package/dist/models/stream.d.ts.map +1 -0
  57. package/dist/models/stream.js +2 -0
  58. package/dist/models/stream.js.map +1 -0
  59. package/dist/models/tab.d.ts +21 -0
  60. package/dist/models/tab.d.ts.map +1 -0
  61. package/dist/models/tab.js +2 -0
  62. package/dist/models/tab.js.map +1 -0
  63. package/dist/provider.d.ts +135 -0
  64. package/dist/provider.d.ts.map +1 -0
  65. package/dist/provider.js +218 -0
  66. package/dist/provider.js.map +1 -0
  67. package/dist/signer.d.ts +31 -0
  68. package/dist/signer.d.ts.map +1 -0
  69. package/dist/signer.js +35 -0
  70. package/dist/signer.js.map +1 -0
  71. package/dist/testing/local.d.ts +31 -0
  72. package/dist/testing/local.d.ts.map +1 -0
  73. package/dist/testing/local.js +100 -0
  74. package/dist/testing/local.js.map +1 -0
  75. package/dist/testing/mock.d.ts +95 -0
  76. package/dist/testing/mock.d.ts.map +1 -0
  77. package/dist/testing/mock.js +407 -0
  78. package/dist/testing/mock.js.map +1 -0
  79. package/dist/wallet.d.ts +162 -0
  80. package/dist/wallet.d.ts.map +1 -0
  81. package/dist/wallet.js +365 -0
  82. package/dist/wallet.js.map +1 -0
  83. package/dist/x402.d.ts +78 -0
  84. package/dist/x402.d.ts.map +1 -0
  85. package/dist/x402.js +151 -0
  86. package/dist/x402.js.map +1 -0
  87. package/eslint.config.js +27 -0
  88. package/package.json +39 -0
  89. package/src/a2a.ts +241 -0
  90. package/src/client.ts +104 -0
  91. package/src/errors.ts +261 -0
  92. package/src/http.ts +190 -0
  93. package/src/index.ts +94 -0
  94. package/src/integrations/vercel-ai.ts +213 -0
  95. package/src/models/bounty.ts +23 -0
  96. package/src/models/common.ts +106 -0
  97. package/src/models/deposit.ts +13 -0
  98. package/src/models/escrow.ts +16 -0
  99. package/src/models/index.ts +8 -0
  100. package/src/models/invoice.ts +32 -0
  101. package/src/models/reputation.ts +7 -0
  102. package/src/models/stream.ts +15 -0
  103. package/src/models/tab.ts +22 -0
  104. package/src/provider.ts +281 -0
  105. package/src/signer.ts +70 -0
  106. package/src/testing/local.ts +118 -0
  107. package/src/testing/mock.ts +507 -0
  108. package/src/wallet.ts +546 -0
  109. package/src/x402.ts +202 -0
  110. package/tests/acceptance/bounty.test.ts +82 -0
  111. package/tests/acceptance/deposit.test.ts +70 -0
  112. package/tests/acceptance/direct.test.ts +53 -0
  113. package/tests/acceptance/escrow.test.ts +67 -0
  114. package/tests/acceptance/setup.ts +113 -0
  115. package/tests/acceptance/stream.test.ts +98 -0
  116. package/tests/acceptance/tab.test.ts +108 -0
  117. package/tests/acceptance/x402.test.ts +140 -0
  118. package/tests/compliance/auth.ts +69 -0
  119. package/tests/compliance/escrows.ts +96 -0
  120. package/tests/compliance/helpers.ts +90 -0
  121. package/tests/compliance/payments.ts +69 -0
  122. package/tests/compliance/tabs.ts +52 -0
  123. package/tests/test_a2a.ts +151 -0
  124. package/tests/test_errors.ts +80 -0
  125. package/tests/test_golden_vectors.ts +162 -0
  126. package/tests/test_integrations.ts +115 -0
  127. package/tests/test_mock.ts +217 -0
  128. package/tests/test_permit.ts +216 -0
  129. package/tests/test_provider.ts +304 -0
  130. package/tests/test_wallet.ts +108 -0
  131. package/tests/test_x402.ts +302 -0
  132. package/tsconfig.json +19 -0
package/src/x402.ts ADDED
@@ -0,0 +1,202 @@
1
+ /**
2
+ * x402 client middleware for auto-paying HTTP 402 Payment Required responses.
3
+ *
4
+ * x402 is an open payment standard where resource servers return HTTP 402 with
5
+ * a `PAYMENT-REQUIRED` header describing the cost. This module provides a
6
+ * `fetch` wrapper that intercepts those responses, signs an EIP-3009
7
+ * authorization, and retries the request with a `PAYMENT-SIGNATURE` header.
8
+ *
9
+ * Usage:
10
+ * ```typescript
11
+ * import { PrivateKeySigner } from "@remitmd/sdk";
12
+ * import { X402Client } from "@remitmd/sdk/x402";
13
+ *
14
+ * const signer = new PrivateKeySigner("0x...");
15
+ * const client = new X402Client({
16
+ * signer,
17
+ * address: signer.getAddress(),
18
+ * maxAutoPayUsdc: 0.10,
19
+ * });
20
+ *
21
+ * const response = await client.fetch("https://api.provider.com/v1/data");
22
+ * ```
23
+ */
24
+
25
+ import { randomBytes } from "node:crypto";
26
+ import type { Signer } from "./signer.js";
27
+
28
+ /** EIP-712 type definitions for USDC's transferWithAuthorization (EIP-3009). */
29
+ const EIP3009_TYPES = {
30
+ TransferWithAuthorization: [
31
+ { name: "from", type: "address" },
32
+ { name: "to", type: "address" },
33
+ { name: "value", type: "uint256" },
34
+ { name: "validAfter", type: "uint256" },
35
+ { name: "validBefore", type: "uint256" },
36
+ { name: "nonce", type: "bytes32" },
37
+ ],
38
+ } as const;
39
+
40
+ /** Raised when an x402 payment amount exceeds the configured auto-pay limit. */
41
+ export class AllowanceExceededError extends Error {
42
+ readonly amountUsdc: number;
43
+ readonly limitUsdc: number;
44
+
45
+ constructor(amountUsdc: number, limitUsdc: number) {
46
+ super(
47
+ `x402 payment ${amountUsdc.toFixed(6)} USDC exceeds auto-pay limit ${limitUsdc.toFixed(6)} USDC`,
48
+ );
49
+ this.name = "AllowanceExceededError";
50
+ this.amountUsdc = amountUsdc;
51
+ this.limitUsdc = limitUsdc;
52
+ }
53
+ }
54
+
55
+ /** Configuration for {@link X402Client}. */
56
+ export interface X402ClientOptions {
57
+ /** Signer used for EIP-3009 authorization signatures. */
58
+ signer: Signer;
59
+ /** Checksummed payer address — must match the signer's public key. */
60
+ address: string;
61
+ /** Maximum USDC amount to auto-pay per request (default: 0.10). */
62
+ maxAutoPayUsdc?: number;
63
+ }
64
+
65
+ /** Shape of the base64-decoded PAYMENT-REQUIRED header (V2). */
66
+ export interface PaymentRequired {
67
+ scheme: string;
68
+ network: string;
69
+ amount: string;
70
+ asset: string;
71
+ payTo: string;
72
+ maxTimeoutSeconds?: number;
73
+ // V2 optional fields — informational metadata about the resource being paid for.
74
+ /** URL or path of the resource being protected (e.g. "/api/v0/data"). */
75
+ resource?: string;
76
+ /** Human-readable description of what the payment is for. */
77
+ description?: string;
78
+ /** MIME type of the resource (e.g. "application/json"). */
79
+ mimeType?: string;
80
+ }
81
+
82
+ /**
83
+ * `fetch` wrapper that auto-handles HTTP 402 Payment Required responses.
84
+ *
85
+ * On receiving a 402, the client:
86
+ * 1. Decodes the `PAYMENT-REQUIRED` header (base64 JSON)
87
+ * 2. Checks the amount is within `maxAutoPayUsdc`
88
+ * 3. Builds and signs an EIP-3009 `transferWithAuthorization`
89
+ * 4. Base64-encodes the `PAYMENT-SIGNATURE` header
90
+ * 5. Retries the original request with payment attached
91
+ *
92
+ * V2: The decoded `PAYMENT-REQUIRED` may include `resource`, `description`,
93
+ * and `mimeType` fields. Access the last payment via `lastPayment`.
94
+ */
95
+ export class X402Client {
96
+ readonly #signer: Signer;
97
+ readonly #address: string;
98
+ readonly #maxAutoPayUsdc: number;
99
+ /** The last PAYMENT-REQUIRED decoded before payment. Useful for logging/display. */
100
+ lastPayment: PaymentRequired | null = null;
101
+
102
+ constructor({ signer, address, maxAutoPayUsdc = 0.1 }: X402ClientOptions) {
103
+ this.#signer = signer;
104
+ this.#address = address;
105
+ this.#maxAutoPayUsdc = maxAutoPayUsdc;
106
+ }
107
+
108
+ /** Make a fetch request, auto-paying any 402 responses within the configured limit. */
109
+ async fetch(url: string, init?: RequestInit): Promise<Response> {
110
+ const response = await globalThis.fetch(url, init);
111
+ if (response.status === 402) {
112
+ return this.#handle402(url, response, init);
113
+ }
114
+ return response;
115
+ }
116
+
117
+ async #handle402(url: string, response: Response, init?: RequestInit): Promise<Response> {
118
+ // 1. Decode PAYMENT-REQUIRED header (header names are case-insensitive per HTTP spec).
119
+ const raw = response.headers.get("payment-required");
120
+ if (!raw) {
121
+ throw new Error("402 response missing PAYMENT-REQUIRED header");
122
+ }
123
+ const required = JSON.parse(Buffer.from(raw, "base64").toString("utf8")) as PaymentRequired;
124
+
125
+ // 2. Only the "exact" scheme is supported in V5.
126
+ if (required.scheme !== "exact") {
127
+ throw new Error(`Unsupported x402 scheme: ${required.scheme}`);
128
+ }
129
+
130
+ // Store for caller inspection (V2 fields: resource, description, mimeType).
131
+ this.lastPayment = required;
132
+
133
+ // 3. Check auto-pay limit.
134
+ const amountBaseUnits = BigInt(required.amount);
135
+ const amountUsdc = Number(amountBaseUnits) / 1_000_000;
136
+ if (amountUsdc > this.#maxAutoPayUsdc) {
137
+ throw new AllowanceExceededError(amountUsdc, this.#maxAutoPayUsdc);
138
+ }
139
+
140
+ // 4. Parse chainId from CAIP-2 network string (e.g. "eip155:84532" → 84532).
141
+ const chainId = parseInt(required.network.split(":")[1]!, 10);
142
+
143
+ // 5. Build EIP-3009 authorization fields.
144
+ const nowSecs = Math.floor(Date.now() / 1000);
145
+ const validBefore = nowSecs + (required.maxTimeoutSeconds ?? 60);
146
+ const nonce = `0x${randomBytes(32).toString("hex")}` as `0x${string}`;
147
+
148
+ const domain = {
149
+ name: "USD Coin",
150
+ version: "2",
151
+ chainId,
152
+ verifyingContract: required.asset as `0x${string}`,
153
+ };
154
+
155
+ // viem requires uint256 values as bigint.
156
+ const message = {
157
+ from: this.#address as `0x${string}`,
158
+ to: required.payTo as `0x${string}`,
159
+ value: amountBaseUnits,
160
+ validAfter: BigInt(0),
161
+ validBefore: BigInt(validBefore),
162
+ nonce,
163
+ };
164
+
165
+ // 6. Sign EIP-712 typed data.
166
+ const signature = await this.#signer.signTypedData(
167
+ domain,
168
+ // Cast: EIP3009_TYPES satisfies TypedDataTypes but has readonly arrays.
169
+ EIP3009_TYPES as unknown as Record<string, Array<{ name: string; type: string }>>,
170
+ message as unknown as Record<string, unknown>,
171
+ );
172
+
173
+ // 7. Build PAYMENT-SIGNATURE JSON payload.
174
+ const paymentPayload = {
175
+ scheme: required.scheme,
176
+ network: required.network,
177
+ x402Version: 1,
178
+ payload: {
179
+ signature,
180
+ authorization: {
181
+ from: this.#address,
182
+ to: required.payTo,
183
+ value: required.amount, // string (base units)
184
+ validAfter: "0",
185
+ validBefore: String(validBefore),
186
+ nonce,
187
+ },
188
+ },
189
+ };
190
+ const paymentHeader = Buffer.from(JSON.stringify(paymentPayload)).toString("base64");
191
+
192
+ // 8. Retry with PAYMENT-SIGNATURE header.
193
+ const newHeaders = new Headers(init?.headers);
194
+ newHeaders.set("PAYMENT-SIGNATURE", paymentHeader);
195
+ return globalThis.fetch(url, { ...init, headers: newHeaders });
196
+ }
197
+
198
+ /** Prevent address leakage via structured cloning / serialisation. */
199
+ toJSON(): Record<string, string> {
200
+ return { address: this.#address };
201
+ }
202
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * SDK acceptance: Bounty lifecycle via wallet.postBounty(), submitBounty(), awardBounty().
3
+ * Verifies SDK permit signing + full bounty lifecycle with balance assertions.
4
+ */
5
+
6
+ import { describe, it, before } from "node:test";
7
+ import assert from "node:assert/strict";
8
+ import type { Wallet } from "../../src/wallet.js";
9
+ import {
10
+ createWallet,
11
+ fundWallet,
12
+ getUsdcBalance,
13
+ getFeeWalletBalance,
14
+ assertBalanceChange,
15
+ waitForBalanceChange,
16
+ } from "./setup.js";
17
+
18
+ describe("SDK: Bounty Lifecycle", { timeout: 180_000 }, () => {
19
+ let poster: Wallet;
20
+ let provider: Wallet;
21
+
22
+ before(async () => {
23
+ poster = await createWallet();
24
+ provider = await createWallet();
25
+ await fundWallet(poster, 100);
26
+ });
27
+
28
+ it("postBounty → submitBounty → awardBounty with correct balances", async () => {
29
+ const amount = 5.0;
30
+ const fee = amount * 0.01; // 1% = $0.05
31
+ const providerReceives = amount - fee; // $4.95
32
+
33
+ const posterBefore = await getUsdcBalance(poster.address);
34
+ const providerBefore = await getUsdcBalance(provider.address);
35
+ const feeBefore = await getFeeWalletBalance();
36
+
37
+ // Step 1: Post bounty with permit for Bounty contract
38
+ const contracts = await poster.getContracts();
39
+ const permit = await poster.signPermit(contracts.bounty, amount + 1);
40
+ const deadline = Math.floor(Date.now() / 1000) + 3600;
41
+
42
+ const bounty = await poster.postBounty({
43
+ amount,
44
+ task: "sdk-bounty-acceptance-test",
45
+ deadline,
46
+ permit,
47
+ });
48
+
49
+ assert.ok(bounty.id, "bounty should have an id");
50
+
51
+ // Wait for on-chain bounty creation (poster USDC locked in Bounty contract)
52
+ await waitForBalanceChange(poster.address, posterBefore);
53
+
54
+ // Step 2: Provider submits evidence
55
+ const evidenceHash = `0x${"ab".repeat(32)}`;
56
+ const submission = await provider.submitBounty(bounty.id, evidenceHash);
57
+ const submissionId =
58
+ (submission as unknown as Record<string, number>).id ??
59
+ (submission as unknown as Record<string, number>).submissionId;
60
+
61
+ // Wait for submission tx
62
+ await new Promise((r) => setTimeout(r, 5000));
63
+
64
+ // Step 3: Poster awards to the submission
65
+ const awarded = await poster.awardBounty(bounty.id, submissionId);
66
+ const awardedStatus =
67
+ awarded.status ?? (awarded as unknown as Record<string, string>).status;
68
+ assert.equal(awardedStatus, "awarded", "bounty should be awarded");
69
+
70
+ // Verify balances
71
+ const providerAfter = await waitForBalanceChange(provider.address, providerBefore);
72
+ const feeAfter = await getFeeWalletBalance();
73
+ const posterAfter = await getUsdcBalance(poster.address);
74
+
75
+ // Poster: lost $5 (bounty amount)
76
+ assertBalanceChange("poster", posterBefore, posterAfter, -amount);
77
+ // Provider: received $5 minus 1% fee = $4.95
78
+ assertBalanceChange("provider", providerBefore, providerAfter, providerReceives);
79
+ // Fee wallet: received 1% of $5 = $0.05
80
+ assertBalanceChange("fee wallet", feeBefore, feeAfter, fee);
81
+ });
82
+ });
@@ -0,0 +1,70 @@
1
+ /**
2
+ * SDK acceptance: Deposit lifecycle via wallet.placeDeposit(), returnDeposit().
3
+ * Verifies SDK permit signing + deposit lock/return with full refund (no fee).
4
+ */
5
+
6
+ import { describe, it, before } from "node:test";
7
+ import assert from "node:assert/strict";
8
+ import type { Wallet } from "../../src/wallet.js";
9
+ import {
10
+ createWallet,
11
+ fundWallet,
12
+ getUsdcBalance,
13
+ getFeeWalletBalance,
14
+ assertBalanceChange,
15
+ waitForBalanceChange,
16
+ } from "./setup.js";
17
+
18
+ describe("SDK: Deposit Lifecycle", { timeout: 180_000 }, () => {
19
+ let agent: Wallet;
20
+ let provider: Wallet;
21
+
22
+ before(async () => {
23
+ agent = await createWallet();
24
+ provider = await createWallet();
25
+ await fundWallet(agent, 100);
26
+ });
27
+
28
+ it("placeDeposit → returnDeposit with full refund (no fee)", async () => {
29
+ const amount = 5.0;
30
+
31
+ const agentBefore = await getUsdcBalance(agent.address);
32
+ const providerBefore = await getUsdcBalance(provider.address);
33
+ const feeBefore = await getFeeWalletBalance();
34
+
35
+ // Step 1: Place deposit with permit for Deposit contract
36
+ const contracts = await agent.getContracts();
37
+ const permit = await agent.signPermit(contracts.deposit, amount + 1);
38
+
39
+ const deposit = await agent.placeDeposit({
40
+ to: provider.address,
41
+ amount,
42
+ expires: 3600, // 1 hour
43
+ permit,
44
+ });
45
+
46
+ assert.ok(deposit.id, "deposit should have an id");
47
+
48
+ // Wait for on-chain deposit lock
49
+ const agentMid = await waitForBalanceChange(agent.address, agentBefore);
50
+ assertBalanceChange("agent locked", agentBefore, agentMid, -amount);
51
+
52
+ // Step 2: Provider returns the deposit
53
+ const returned = await provider.returnDeposit(deposit.id);
54
+ const returnedStatus =
55
+ returned.status ?? (returned as unknown as Record<string, string>).status;
56
+ assert.equal(returnedStatus, "returned", "deposit should be returned");
57
+
58
+ // Wait for return settlement (agent gets full refund)
59
+ const agentAfter = await waitForBalanceChange(agent.address, agentMid);
60
+ const providerAfter = await getUsdcBalance(provider.address);
61
+ const feeAfter = await getFeeWalletBalance();
62
+
63
+ // Agent: full refund — net change ≈ $0
64
+ assertBalanceChange("agent net", agentBefore, agentAfter, 0);
65
+ // Provider: unchanged
66
+ assertBalanceChange("provider", providerBefore, providerAfter, 0);
67
+ // Fee wallet: unchanged (deposits have no fee)
68
+ assertBalanceChange("fee wallet", feeBefore, feeAfter, 0);
69
+ });
70
+ });
@@ -0,0 +1,53 @@
1
+ /**
2
+ * SDK acceptance: Direct payment via wallet.payDirect().
3
+ * Verifies SDK permit signing + payment works end-to-end.
4
+ */
5
+
6
+ import { describe, it, before } from "node:test";
7
+ import assert from "node:assert/strict";
8
+ import type { Wallet } from "../../src/wallet.js";
9
+ import {
10
+ createWallet,
11
+ fundWallet,
12
+ getUsdcBalance,
13
+ getFeeWalletBalance,
14
+ assertBalanceChange,
15
+ waitForBalanceChange,
16
+ } from "./setup.js";
17
+
18
+ describe("SDK: Direct Payment", { timeout: 120_000 }, () => {
19
+ let agent: Wallet;
20
+ let provider: Wallet;
21
+
22
+ before(async () => {
23
+ agent = await createWallet();
24
+ provider = await createWallet();
25
+ await fundWallet(agent, 100);
26
+ });
27
+
28
+ it("payDirect with signPermit — correct balances", async () => {
29
+ const amount = 1.0;
30
+ const fee = 0.01;
31
+ const providerReceives = amount - fee;
32
+
33
+ const agentBefore = await getUsdcBalance(agent.address);
34
+ const providerBefore = await getUsdcBalance(provider.address);
35
+ const feeBefore = await getFeeWalletBalance();
36
+
37
+ // SDK: get contracts, sign permit, pay
38
+ const contracts = await agent.getContracts();
39
+ const permit = await agent.signPermit(contracts.router, 2.0);
40
+ const tx = await agent.payDirect(provider.address, amount, "sdk-acceptance", { permit });
41
+
42
+ const txHash = tx.txHash ?? (tx as unknown as Record<string, string>).tx_hash;
43
+ assert.ok(txHash?.startsWith("0x"), `should return tx hash, got: ${txHash}`);
44
+
45
+ const agentAfter = await waitForBalanceChange(agent.address, agentBefore);
46
+ const providerAfter = await getUsdcBalance(provider.address);
47
+ const feeAfter = await getFeeWalletBalance();
48
+
49
+ assertBalanceChange("agent", agentBefore, agentAfter, -amount);
50
+ assertBalanceChange("provider", providerBefore, providerAfter, providerReceives);
51
+ assertBalanceChange("fee wallet", feeBefore, feeAfter, fee);
52
+ });
53
+ });
@@ -0,0 +1,67 @@
1
+ /**
2
+ * SDK acceptance: Escrow lifecycle via wallet.pay(), claimStart(), releaseEscrow().
3
+ */
4
+
5
+ import { describe, it, before } from "node:test";
6
+ import assert from "node:assert/strict";
7
+ import type { Wallet } from "../../src/wallet.js";
8
+ import {
9
+ createWallet,
10
+ fundWallet,
11
+ getUsdcBalance,
12
+ getFeeWalletBalance,
13
+ assertBalanceChange,
14
+ waitForBalanceChange,
15
+ } from "./setup.js";
16
+
17
+ describe("SDK: Escrow Lifecycle", { timeout: 180_000 }, () => {
18
+ let agent: Wallet;
19
+ let provider: Wallet;
20
+
21
+ before(async () => {
22
+ agent = await createWallet();
23
+ provider = await createWallet();
24
+ await fundWallet(agent, 100);
25
+ });
26
+
27
+ it("pay → claimStart → release with correct balances", async () => {
28
+ const amount = 5.0;
29
+ const fee = amount * 0.01;
30
+ const providerReceives = amount - fee;
31
+
32
+ const agentBefore = await getUsdcBalance(agent.address);
33
+ const providerBefore = await getUsdcBalance(provider.address);
34
+ const feeBefore = await getFeeWalletBalance();
35
+
36
+ // Sign permit for Escrow contract
37
+ const contracts = await agent.getContracts();
38
+ const permit = await agent.signPermit(contracts.escrow, amount + 1);
39
+
40
+ // Fund escrow
41
+ const escrow = await agent.pay(
42
+ { to: provider.address, amount, memo: "sdk-escrow-test" },
43
+ { permit },
44
+ );
45
+ const escrowId = escrow.invoiceId ?? (escrow as unknown as Record<string, string>).invoice_id;
46
+ assert.ok(escrowId, "escrow should have id");
47
+
48
+ // Wait for lock
49
+ await waitForBalanceChange(agent.address, agentBefore);
50
+
51
+ // Provider claims
52
+ await provider.claimStart(escrowId);
53
+ await new Promise((r) => setTimeout(r, 5000));
54
+
55
+ // Agent releases
56
+ await agent.releaseEscrow(escrowId);
57
+
58
+ // Verify balances
59
+ const providerAfter = await waitForBalanceChange(provider.address, providerBefore);
60
+ const feeAfter = await getFeeWalletBalance();
61
+ const agentAfter = await getUsdcBalance(agent.address);
62
+
63
+ assertBalanceChange("agent", agentBefore, agentAfter, -amount);
64
+ assertBalanceChange("provider", providerBefore, providerAfter, providerReceives);
65
+ assertBalanceChange("fee wallet", feeBefore, feeAfter, fee);
66
+ });
67
+ });
@@ -0,0 +1,113 @@
1
+ /**
2
+ * SDK acceptance test harness.
3
+ *
4
+ * Uses SDK Wallet to interact with the live Base Sepolia API.
5
+ * No raw HTTP — everything goes through SDK methods.
6
+ */
7
+
8
+ import { Wallet } from "../../src/wallet.js";
9
+ import { generatePrivateKey } from "viem/accounts";
10
+
11
+ // ─── Config ──────────────────────────────────────────────────────────────────
12
+
13
+ export const API_URL = process.env["ACCEPTANCE_API_URL"] ?? "https://remit.md/api/v0";
14
+ export const RPC_URL = process.env["ACCEPTANCE_RPC_URL"] ?? "https://sepolia.base.org";
15
+ export const FEE_WALLET = "0xd3f721BDF92a2bB5Dd8d2FE2AFC03aFE5629B420";
16
+
17
+ // ─── Router address (fetched once from /contracts) ──────────────────────────
18
+
19
+ let _routerAddress: string | null = null;
20
+
21
+ async function getRouterAddress(): Promise<string> {
22
+ if (_routerAddress) return _routerAddress;
23
+ const res = await fetch(`${API_URL}/contracts`);
24
+ if (!res.ok) throw new Error(`GET /contracts failed: ${res.status}`);
25
+ const data = (await res.json()) as { router: string };
26
+ _routerAddress = data.router;
27
+ return _routerAddress;
28
+ }
29
+
30
+ // ─── Wallet creation ────────────────────────────────────────────────────────
31
+
32
+ export async function createWallet(): Promise<Wallet> {
33
+ const key = generatePrivateKey();
34
+ const routerAddress = await getRouterAddress();
35
+ return new Wallet({
36
+ privateKey: key,
37
+ chain: "base-sepolia",
38
+ apiUrl: API_URL,
39
+ rpcUrl: RPC_URL,
40
+ routerAddress,
41
+ });
42
+ }
43
+
44
+ // ─── Funding ────────────────────────────────────────────────────────────────
45
+
46
+ /** Mint testnet USDC and wait for on-chain confirmation. */
47
+ export async function fundWallet(wallet: Wallet, amount = 100): Promise<void> {
48
+ await wallet.mint(amount);
49
+ // Wait for balance via RPC (doesn't require auth)
50
+ await waitForBalanceChange(wallet.address, 0);
51
+ }
52
+
53
+ // ─── On-chain balance via RPC ───────────────────────────────────────────────
54
+
55
+ /** Read USDC balance via RPC eth_call to balanceOf(address). Returns USD. */
56
+ export async function getUsdcBalance(address: string): Promise<number> {
57
+ const paddedAddr = address.toLowerCase().replace("0x", "").padStart(64, "0");
58
+ const data = `0x70a08231${paddedAddr}`;
59
+ const usdcAddress = "0x142aD61B8d2edD6b3807D9266866D97C35Ee0317";
60
+
61
+ const res = await fetch(RPC_URL, {
62
+ method: "POST",
63
+ headers: { "Content-Type": "application/json" },
64
+ body: JSON.stringify({
65
+ jsonrpc: "2.0",
66
+ id: 1,
67
+ method: "eth_call",
68
+ params: [{ to: usdcAddress, data }, "latest"],
69
+ }),
70
+ });
71
+ const json = (await res.json()) as { result?: string; error?: { message: string } };
72
+ if (json.error) throw new Error(`RPC balanceOf error: ${json.error.message}`);
73
+ return Number(BigInt(json.result ?? "0x0")) / 1e6;
74
+ }
75
+
76
+ export async function getFeeWalletBalance(): Promise<number> {
77
+ return getUsdcBalance(FEE_WALLET);
78
+ }
79
+
80
+ /** Wait for a balance change (polls every 2s, up to maxWait). */
81
+ export async function waitForBalanceChange(
82
+ address: string,
83
+ beforeBalance: number,
84
+ maxWaitMs = 30000,
85
+ ): Promise<number> {
86
+ const start = Date.now();
87
+ while (Date.now() - start < maxWaitMs) {
88
+ const current = await getUsdcBalance(address);
89
+ if (Math.abs(current - beforeBalance) > 0.0001) return current;
90
+ await new Promise((r) => setTimeout(r, 2000));
91
+ }
92
+ return getUsdcBalance(address);
93
+ }
94
+
95
+ /** Assert a balance changed by expected delta within tolerance. */
96
+ export function assertBalanceChange(
97
+ label: string,
98
+ before: number,
99
+ after: number,
100
+ expectedDelta: number,
101
+ toleranceBps = 10,
102
+ ): void {
103
+ const actualDelta = after - before;
104
+ const tolerance = Math.abs(expectedDelta) * (toleranceBps / 10000);
105
+ const diff = Math.abs(actualDelta - expectedDelta);
106
+
107
+ if (diff > tolerance) {
108
+ throw new Error(
109
+ `${label}: expected delta ${expectedDelta}, got ${actualDelta} ` +
110
+ `(before=${before}, after=${after}, tolerance=${tolerance})`,
111
+ );
112
+ }
113
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * SDK acceptance: Stream lifecycle via wallet.openStream(), closeStream().
3
+ * Verifies SDK permit signing + stream accrual + close with balance bounds.
4
+ *
5
+ * Stream accrual is time-dependent (block timestamps). We use generous bounds
6
+ * and conservation-of-funds checks rather than exact delta assertions.
7
+ */
8
+
9
+ import { describe, it, before } from "node:test";
10
+ import assert from "node:assert/strict";
11
+ import type { Wallet } from "../../src/wallet.js";
12
+ import {
13
+ createWallet,
14
+ fundWallet,
15
+ getUsdcBalance,
16
+ getFeeWalletBalance,
17
+ waitForBalanceChange,
18
+ } from "./setup.js";
19
+
20
+ describe("SDK: Stream Lifecycle", { timeout: 180_000 }, () => {
21
+ let agent: Wallet;
22
+ let provider: Wallet;
23
+
24
+ before(async () => {
25
+ agent = await createWallet();
26
+ provider = await createWallet();
27
+ await fundWallet(agent, 100);
28
+ });
29
+
30
+ it("openStream → wait → closeStream with correct balance bounds", async () => {
31
+ const ratePerSecond = 0.1; // $0.10/s
32
+ const maxTotal = 5.0;
33
+
34
+ const agentBefore = await getUsdcBalance(agent.address);
35
+ const providerBefore = await getUsdcBalance(provider.address);
36
+ const feeBefore = await getFeeWalletBalance();
37
+
38
+ // Step 1: Open stream with permit for Stream contract
39
+ const contracts = await agent.getContracts();
40
+ const permit = await agent.signPermit(contracts.stream, maxTotal + 1);
41
+
42
+ const stream = await agent.openStream({
43
+ to: provider.address,
44
+ rate: ratePerSecond,
45
+ maxTotal,
46
+ permit,
47
+ });
48
+
49
+ assert.ok(stream.id, "stream should have an id");
50
+
51
+ // Wait for on-chain creation (agent locks maxTotal in Stream contract)
52
+ await waitForBalanceChange(agent.address, agentBefore);
53
+
54
+ // Step 2: Wait for accrual (~5 seconds real time)
55
+ await new Promise((r) => setTimeout(r, 5000));
56
+
57
+ // Step 3: Close stream (payer only, no body)
58
+ const closed = await agent.closeStream(stream.id);
59
+ const closedStatus = closed.status ?? (closed as unknown as Record<string, string>).status;
60
+ assert.equal(closedStatus, "closed", "stream should be closed");
61
+
62
+ // Wait for settlement (provider balance should increase)
63
+ const providerAfter = await waitForBalanceChange(provider.address, providerBefore);
64
+ const feeAfter = await getFeeWalletBalance();
65
+ const agentAfter = await getUsdcBalance(agent.address);
66
+
67
+ // Calculate actual changes
68
+ const agentLoss = agentBefore - agentAfter;
69
+ const providerGain = providerAfter - providerBefore;
70
+ const feeGain = feeAfter - feeBefore;
71
+
72
+ // Agent should have lost money (stream accrued), but <= maxTotal
73
+ assert.ok(
74
+ agentLoss > 0.05,
75
+ `agent should have lost money from streaming, got loss=${agentLoss}`,
76
+ );
77
+ assert.ok(
78
+ agentLoss <= maxTotal + 0.01,
79
+ `agent loss should not exceed maxTotal ($${maxTotal}), got loss=${agentLoss}`,
80
+ );
81
+
82
+ // Provider should have received payout (accrued minus 1% fee)
83
+ assert.ok(
84
+ providerGain > 0.04,
85
+ `provider should have received payout, got gain=${providerGain}`,
86
+ );
87
+
88
+ // Fee wallet should not decrease
89
+ assert.ok(feeGain >= 0, `fee wallet should not decrease, got change=${feeGain}`);
90
+
91
+ // Conservation of funds: agent loss ≈ provider gain + fee
92
+ const conservationDiff = Math.abs(agentLoss - (providerGain + feeGain));
93
+ assert.ok(
94
+ conservationDiff < 0.01,
95
+ `conservation violated: agent lost ${agentLoss}, provider+fee gained ${providerGain + feeGain}, diff=${conservationDiff}`,
96
+ );
97
+ });
98
+ });