@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
@@ -0,0 +1,108 @@
1
+ /**
2
+ * SDK acceptance: Tab lifecycle via wallet.openTab(), chargeTab(), closeTab().
3
+ * Verifies SDK permit signing + tab charge signing + full lifecycle balances.
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: Tab 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("openTab → chargeTab → closeTab with correct balances", async () => {
29
+ const limit = 10.0;
30
+ const chargeAmount = 2.0;
31
+ const chargeUnits = BigInt(Math.round(chargeAmount * 1e6));
32
+ const fee = chargeAmount * 0.01; // 1% = $0.02
33
+ const providerReceives = chargeAmount - fee; // $1.98
34
+
35
+ const agentBefore = await getUsdcBalance(agent.address);
36
+ const providerBefore = await getUsdcBalance(provider.address);
37
+ const feeBefore = await getFeeWalletBalance();
38
+
39
+ // Step 1: Open tab (agent, with permit for Tab contract)
40
+ const contracts = await agent.getContracts();
41
+ const permit = await agent.signPermit(contracts.tab, limit + 1);
42
+
43
+ const tab = await agent.openTab({
44
+ to: provider.address,
45
+ limit,
46
+ perUnit: 0.1,
47
+ permit,
48
+ });
49
+
50
+ assert.ok(tab.id, "tab should have an id");
51
+
52
+ // Wait for on-chain lock (agent USDC moves to Tab contract)
53
+ await waitForBalanceChange(agent.address, agentBefore);
54
+
55
+ // Step 2: Provider charges $2 (off-chain with TabCharge EIP-712 sig)
56
+ const callCount = 1;
57
+ const chargeSig = await provider.signTabCharge(
58
+ contracts.tab,
59
+ tab.id,
60
+ chargeUnits,
61
+ callCount,
62
+ );
63
+
64
+ const charge = await provider.chargeTab(tab.id, {
65
+ amount: chargeAmount,
66
+ cumulative: chargeAmount,
67
+ callCount,
68
+ providerSig: chargeSig,
69
+ });
70
+
71
+ const chargeTabId = charge.tabId ?? (charge as unknown as Record<string, string>).tab_id;
72
+ assert.equal(chargeTabId, tab.id, "charge should reference the tab");
73
+
74
+ // Step 3: Close tab (agent, with provider's close signature on final state)
75
+ const closeSig = await provider.signTabCharge(
76
+ contracts.tab,
77
+ tab.id,
78
+ chargeUnits,
79
+ callCount,
80
+ );
81
+
82
+ const closed = await agent.closeTab(tab.id, {
83
+ finalAmount: chargeAmount,
84
+ providerSig: closeSig,
85
+ });
86
+
87
+ const closedStatus = closed.status ?? (closed as unknown as Record<string, string>).status;
88
+ assert.equal(closedStatus, "closed", "tab should be closed");
89
+
90
+ const closedTxHash =
91
+ closed.txHash ??
92
+ (closed as unknown as Record<string, string>).closedTxHash ??
93
+ (closed as unknown as Record<string, string>).closed_tx_hash;
94
+ assert.ok(closedTxHash?.startsWith("0x"), `close should return tx hash, got: ${closedTxHash}`);
95
+
96
+ // Verify balances
97
+ const providerAfter = await waitForBalanceChange(provider.address, providerBefore);
98
+ const feeAfter = await getFeeWalletBalance();
99
+ const agentAfter = await getUsdcBalance(agent.address);
100
+
101
+ // Agent: locked $10, refunded $8, net change = -$2
102
+ assertBalanceChange("agent", agentBefore, agentAfter, -chargeAmount);
103
+ // Provider: received $2 minus 1% fee = $1.98
104
+ assertBalanceChange("provider", providerBefore, providerAfter, providerReceives);
105
+ // Fee wallet: received 1% of $2 = $0.02
106
+ assertBalanceChange("fee wallet", feeBefore, feeAfter, fee);
107
+ });
108
+ });
@@ -0,0 +1,140 @@
1
+ /**
2
+ * SDK acceptance: x402 auto-payment via wallet.x402Fetch().
3
+ *
4
+ * Spins up a local test server that returns 402 with a PAYMENT-REQUIRED header.
5
+ * The SDK's x402Fetch() auto-signs EIP-3009 and retries with PAYMENT-SIGNATURE.
6
+ * We verify the payment signature is structurally valid and the retry succeeds.
7
+ *
8
+ * On-chain settlement is tested separately in the API acceptance tests (C2).
9
+ * This test focuses on the SDK client-side flow: 402 detection → EIP-3009 signing → retry.
10
+ */
11
+
12
+ import { describe, it, before, after } from "node:test";
13
+ import assert from "node:assert/strict";
14
+ import { createServer, type Server } from "node:http";
15
+ import type { Wallet } from "../../src/wallet.js";
16
+ import { createWallet, fundWallet, API_URL } from "./setup.js";
17
+
18
+ describe("SDK: x402 Auto-Payment", { timeout: 120_000 }, () => {
19
+ let agent: Wallet;
20
+ let server: Server;
21
+ let serverUrl: string;
22
+
23
+ /** Fetch contracts to get USDC + Router addresses for the paywall header. */
24
+ async function getContractAddrs(): Promise<{ usdc: string; router: string }> {
25
+ const res = await fetch(`${API_URL}/contracts`);
26
+ const data = (await res.json()) as { usdc: string; router: string };
27
+ return data;
28
+ }
29
+
30
+ before(async () => {
31
+ agent = await createWallet();
32
+ await fundWallet(agent, 100);
33
+
34
+ const contracts = await getContractAddrs();
35
+
36
+ // Create a local x402 paywall server
37
+ server = createServer((req, res) => {
38
+ const paymentSig = req.headers["payment-signature"];
39
+
40
+ if (!paymentSig) {
41
+ // First request: return 402 with payment requirements
42
+ const paymentRequired = {
43
+ scheme: "exact",
44
+ network: "eip155:84532",
45
+ amount: "100000", // $0.10 USDC (within default maxAutoPayUsdc)
46
+ asset: contracts.usdc,
47
+ payTo: contracts.router,
48
+ maxTimeoutSeconds: 60,
49
+ resource: "/test-resource",
50
+ description: "x402 acceptance test",
51
+ mimeType: "text/plain",
52
+ };
53
+ const encoded = Buffer.from(JSON.stringify(paymentRequired)).toString("base64");
54
+ res.writeHead(402, {
55
+ "Content-Type": "text/plain",
56
+ "payment-required": encoded,
57
+ });
58
+ res.end("Payment Required");
59
+ return;
60
+ }
61
+
62
+ // Second request: has PAYMENT-SIGNATURE — validate structure then return 200
63
+ try {
64
+ const decoded = JSON.parse(
65
+ Buffer.from(paymentSig as string, "base64").toString("utf8"),
66
+ ) as {
67
+ scheme: string;
68
+ network: string;
69
+ x402Version: number;
70
+ payload: {
71
+ signature: string;
72
+ authorization: {
73
+ from: string;
74
+ to: string;
75
+ value: string;
76
+ validAfter: string;
77
+ validBefore: string;
78
+ nonce: string;
79
+ };
80
+ };
81
+ };
82
+
83
+ // Validate payment structure
84
+ if (decoded.scheme !== "exact") throw new Error("wrong scheme");
85
+ if (decoded.network !== "eip155:84532") throw new Error("wrong network");
86
+ if (!decoded.payload.signature.startsWith("0x")) throw new Error("bad signature");
87
+ if (decoded.payload.authorization.from.toLowerCase() !== agent.address.toLowerCase()) {
88
+ throw new Error("wrong payer");
89
+ }
90
+ if (decoded.payload.authorization.value !== "100000") throw new Error("wrong amount");
91
+
92
+ res.writeHead(200, { "Content-Type": "text/plain" });
93
+ res.end("paid content");
94
+ } catch (e) {
95
+ res.writeHead(400);
96
+ res.end(`Invalid payment: ${(e as Error).message}`);
97
+ }
98
+ });
99
+
100
+ await new Promise<void>((resolve) => {
101
+ server.listen(0, "127.0.0.1", () => {
102
+ const addr = server.address() as { port: number };
103
+ serverUrl = `http://127.0.0.1:${addr.port}`;
104
+ resolve();
105
+ });
106
+ });
107
+ });
108
+
109
+ after(() => {
110
+ server?.close();
111
+ });
112
+
113
+ it("x402Fetch auto-pays 402 and returns 200 with content", async () => {
114
+ const { response, lastPayment } = await agent.x402Fetch(`${serverUrl}/test-resource`);
115
+
116
+ assert.equal(response.status, 200, "should get 200 after auto-payment");
117
+ const body = await response.text();
118
+ assert.equal(body, "paid content", "should receive paid content");
119
+
120
+ // Verify lastPayment metadata (V2 fields)
121
+ assert.ok(lastPayment, "lastPayment should be set");
122
+ assert.equal(lastPayment.scheme, "exact");
123
+ assert.equal(lastPayment.amount, "100000");
124
+ assert.equal(lastPayment.resource, "/test-resource");
125
+ assert.equal(lastPayment.description, "x402 acceptance test");
126
+ assert.equal(lastPayment.mimeType, "text/plain");
127
+ });
128
+
129
+ it("x402Fetch rejects payment above maxAutoPayUsdc", async () => {
130
+ // The default limit is $0.10, and the paywall asks for $0.10 — right at the edge.
131
+ // Test with a lower limit ($0.01) to verify rejection.
132
+ await assert.rejects(
133
+ () => agent.x402Fetch(`${serverUrl}/test-resource`, 0.01),
134
+ (err: Error) => {
135
+ assert.ok(err.message.includes("exceeds auto-pay limit"), `wrong error: ${err.message}`);
136
+ return true;
137
+ },
138
+ );
139
+ });
140
+ });
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Compliance: EIP-712 authentication against a real server.
3
+ *
4
+ * Proves the TypeScript SDK can authenticate — 200 responses, not 401s.
5
+ * Requires the compliance server to be running (docker-compose.compliance.yml).
6
+ */
7
+
8
+ import { describe, it, before } from "node:test";
9
+ import assert from "node:assert/strict";
10
+
11
+ import {
12
+ SERVER_URL,
13
+ ROUTER_ADDRESS,
14
+ serverIsReachable,
15
+ makeWallet,
16
+ } from "./helpers.js";
17
+
18
+ let skip = false;
19
+
20
+ before(async () => {
21
+ skip = !(await serverIsReachable());
22
+ if (skip) {
23
+ console.warn(
24
+ `[compliance] Server not reachable at ${SERVER_URL}. Skipping auth tests.`,
25
+ );
26
+ }
27
+ });
28
+
29
+ describe("TypeScript compliance: authentication", () => {
30
+ it("authenticated GET /status returns 200", async (t) => {
31
+ if (skip) return t.skip("server not available");
32
+
33
+ const wallet = await makeWallet();
34
+ // status() uses #auth.get — if EIP-712 is wrong this returns 401 not 200.
35
+ // Server returns { wallet, tier, monthly_volume, fee_rate_bps, ... }.
36
+ const status = await wallet.status();
37
+ // Server uses "wallet" key (not "address"), cast to access it.
38
+ const raw = status as unknown as Record<string, unknown>;
39
+ assert.equal(
40
+ (raw["wallet"] as string).toLowerCase(),
41
+ wallet.address.toLowerCase(),
42
+ "Server must return our wallet address",
43
+ );
44
+ assert.ok(typeof status.tier === "string" && status.tier.length > 0, "tier must be set");
45
+ });
46
+
47
+ it("unauthenticated POST /payments/direct returns 401", async (t) => {
48
+ if (skip) return t.skip("server not available");
49
+
50
+ const resp = await fetch(`${SERVER_URL}/api/v0/payments/direct`, {
51
+ method: "POST",
52
+ headers: { "Content-Type": "application/json" },
53
+ body: JSON.stringify({
54
+ to: ROUTER_ADDRESS,
55
+ amount: 1.0,
56
+ }),
57
+ });
58
+ assert.equal(resp.status, 401, `Expected 401, got ${resp.status}`);
59
+ });
60
+
61
+ it("mint credits testnet funds", async (t) => {
62
+ if (skip) return t.skip("server not available");
63
+
64
+ const wallet = await makeWallet();
65
+ const tx = await wallet.mint(100);
66
+ assert.ok(tx.tx_hash, "tx_hash must be set after mint");
67
+ });
68
+
69
+ });
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Compliance: escrow lifecycle against a real server.
3
+ *
4
+ * Verifies: create escrow (pay) → funded state → cancel → cancelled state.
5
+ * Requires the compliance server to be running (docker-compose.compliance.yml).
6
+ */
7
+
8
+ import { describe, it, before } from "node:test";
9
+ import assert from "node:assert/strict";
10
+
11
+ import { SERVER_URL, serverIsReachable, makeFundedPair } from "./helpers.js";
12
+ import type { Invoice } from "../../src/models/invoice.js";
13
+
14
+ let skip = false;
15
+
16
+ before(async () => {
17
+ skip = !(await serverIsReachable());
18
+ if (skip) {
19
+ console.warn(
20
+ `[compliance] Server not reachable at ${SERVER_URL}. Skipping escrow tests.`,
21
+ );
22
+ }
23
+ });
24
+
25
+ describe("TypeScript compliance: escrow lifecycle", () => {
26
+ it("pay (escrow create) returns tx with invoiceId", async (t) => {
27
+ if (skip) return t.skip("server not available");
28
+
29
+ const { payer, payeeAddress } = await makeFundedPair();
30
+
31
+ const tx = await payer.pay({
32
+ id: "",
33
+ from: payer.address,
34
+ to: payeeAddress,
35
+ amount: 10.0,
36
+ chain: "base-sepolia",
37
+ status: "pending",
38
+ paymentType: "escrow",
39
+ createdAt: 0,
40
+ memo: "compliance escrow test",
41
+ } as Invoice);
42
+
43
+ assert.ok(tx.invoiceId, "invoiceId must be set after pay");
44
+ assert.ok(tx.txHash, "txHash must be set after pay");
45
+ });
46
+
47
+ it("getEscrow returns funded escrow immediately after pay", async (t) => {
48
+ if (skip) return t.skip("server not available");
49
+
50
+ const { payer, payeeAddress } = await makeFundedPair();
51
+
52
+ const tx = await payer.pay({
53
+ id: "",
54
+ from: payer.address,
55
+ to: payeeAddress,
56
+ amount: 10.0,
57
+ chain: "base-sepolia",
58
+ status: "pending",
59
+ paymentType: "escrow",
60
+ createdAt: 0,
61
+ memo: "compliance escrow funded check",
62
+ } as Invoice);
63
+
64
+ const escrow = await payer.getEscrow(tx.invoiceId!);
65
+ assert.equal(escrow.invoiceId, tx.invoiceId);
66
+ assert.equal(escrow.status, "funded");
67
+ assert.ok(
68
+ Math.abs(escrow.amount - 10.0) < 0.01,
69
+ `Expected ~10 USDC, got ${escrow.amount}`,
70
+ );
71
+ });
72
+
73
+ it("cancelEscrow transitions escrow to cancelled", async (t) => {
74
+ if (skip) return t.skip("server not available");
75
+
76
+ const { payer, payeeAddress } = await makeFundedPair();
77
+
78
+ const tx = await payer.pay({
79
+ id: "",
80
+ from: payer.address,
81
+ to: payeeAddress,
82
+ amount: 10.0,
83
+ chain: "base-sepolia",
84
+ status: "pending",
85
+ paymentType: "escrow",
86
+ createdAt: 0,
87
+ memo: "compliance escrow cancel test",
88
+ } as Invoice);
89
+
90
+ const cancelTx = await payer.cancelEscrow(tx.invoiceId!);
91
+ assert.ok(cancelTx.txHash, "cancelTx must have txHash");
92
+
93
+ const escrow = await payer.getEscrow(tx.invoiceId!);
94
+ assert.equal(escrow.status, "cancelled");
95
+ });
96
+ });
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Compliance test helpers: register operators, create funded Wallet instances.
3
+ *
4
+ * Environment variables (set by CI; defaults match docker-compose.compliance.yml):
5
+ * REMIT_TEST_SERVER_URL Server base URL (default: http://localhost:3000)
6
+ * REMIT_ROUTER_ADDRESS Router contract address for EIP-712 domain
7
+ * REMIT_CHAIN_ID Chain ID for EIP-712 domain (default: 84532)
8
+ */
9
+
10
+ import { Wallet } from "../../src/wallet.js";
11
+
12
+ export const SERVER_URL =
13
+ process.env["REMIT_TEST_SERVER_URL"] ?? "http://localhost:3000";
14
+ export const ROUTER_ADDRESS =
15
+ process.env["REMIT_ROUTER_ADDRESS"] ??
16
+ "0x70997970C51812dc3A010C7d01b50e0d17dc79C8";
17
+ export const CHAIN_ID = Number(process.env["REMIT_CHAIN_ID"] ?? "84532");
18
+
19
+ /** Returns true if the compliance server is reachable and healthy. */
20
+ export async function serverIsReachable(): Promise<boolean> {
21
+ try {
22
+ const resp = await fetch(`${SERVER_URL}/health`, { signal: AbortSignal.timeout(3000) });
23
+ return resp.ok;
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+
29
+ /** Register a new operator and return (privateKey, walletAddress). */
30
+ export async function registerAndGetWallet(): Promise<{
31
+ privateKey: string;
32
+ walletAddress: string;
33
+ }> {
34
+ const email = `compliance.ts.${Date.now()}@test.remitmd.local`;
35
+ const password = "ComplianceTestPass1!";
36
+
37
+ const regResp = await fetch(`${SERVER_URL}/api/v0/auth/register`, {
38
+ method: "POST",
39
+ headers: { "Content-Type": "application/json" },
40
+ body: JSON.stringify({ email, password }),
41
+ });
42
+ if (!regResp.ok) {
43
+ throw new Error(`register failed: ${regResp.status} ${await regResp.text()}`);
44
+ }
45
+ const reg = (await regResp.json()) as { token: string; wallet_address: string };
46
+
47
+ const keyResp = await fetch(`${SERVER_URL}/api/v0/auth/agent-key`, {
48
+ headers: { Authorization: `Bearer ${reg.token}` },
49
+ });
50
+ if (!keyResp.ok) {
51
+ throw new Error(`agent-key failed: ${keyResp.status} ${await keyResp.text()}`);
52
+ }
53
+ const keyData = (await keyResp.json()) as { private_key: string };
54
+
55
+ return { privateKey: keyData.private_key, walletAddress: reg.wallet_address };
56
+ }
57
+
58
+ /** Create a Wallet backed by a freshly registered operator. */
59
+ export async function makeWallet(): Promise<Wallet> {
60
+ const { privateKey } = await registerAndGetWallet();
61
+ return new Wallet({
62
+ privateKey,
63
+ chain: "base-sepolia",
64
+ apiUrl: `${SERVER_URL}/api/v0`,
65
+ routerAddress: ROUTER_ADDRESS,
66
+ });
67
+ }
68
+
69
+ /** Create two wallets (payer + payee) and fund the payer via mint. */
70
+ export async function makeFundedPair(): Promise<{
71
+ payer: Wallet;
72
+ payee: Wallet;
73
+ payeeAddress: string;
74
+ }> {
75
+ const { privateKey: pkA } = await registerAndGetWallet();
76
+ const { privateKey: pkB, walletAddress: addrB } = await registerAndGetWallet();
77
+
78
+ const walletOpts = {
79
+ chain: "base-sepolia" as const,
80
+ apiUrl: `${SERVER_URL}/api/v0`,
81
+ routerAddress: ROUTER_ADDRESS,
82
+ };
83
+
84
+ const payer = new Wallet({ privateKey: pkA, ...walletOpts });
85
+ const payee = new Wallet({ privateKey: pkB, ...walletOpts });
86
+
87
+ await payer.mint(100);
88
+
89
+ return { payer, payee, payeeAddress: addrB };
90
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Compliance: pay_direct against a real server.
3
+ *
4
+ * Verifies end-to-end direct payment: mint → payDirect → invoice exists.
5
+ * Requires the compliance server to be running (docker-compose.compliance.yml).
6
+ */
7
+
8
+ import { describe, it, before } from "node:test";
9
+ import assert from "node:assert/strict";
10
+
11
+ import { SERVER_URL, serverIsReachable, makeFundedPair } from "./helpers.js";
12
+ import { RemitError } from "../../src/errors.js";
13
+
14
+ let skip = false;
15
+
16
+ before(async () => {
17
+ skip = !(await serverIsReachable());
18
+ if (skip) {
19
+ console.warn(
20
+ `[compliance] Server not reachable at ${SERVER_URL}. Skipping payment tests.`,
21
+ );
22
+ }
23
+ });
24
+
25
+ describe("TypeScript compliance: payDirect", () => {
26
+ it("happy path: payDirect returns tx with txHash and invoiceId", async (t) => {
27
+ if (skip) return t.skip("server not available");
28
+
29
+ const { payer, payeeAddress } = await makeFundedPair();
30
+ const tx = await payer.payDirect(payeeAddress, 5.0, "compliance test");
31
+
32
+ assert.ok(tx.txHash, "txHash must be set");
33
+ assert.ok(tx.invoiceId, "invoiceId must be set");
34
+ });
35
+
36
+ it("below minimum amount returns 400/422 error", async (t) => {
37
+ if (skip) return t.skip("server not available");
38
+
39
+ const { payer, payeeAddress } = await makeFundedPair();
40
+ await assert.rejects(
41
+ () => payer.payDirect(payeeAddress, 0.001, "too small"),
42
+ (err: unknown) => {
43
+ assert.ok(err instanceof RemitError, "must throw RemitError");
44
+ assert.ok(
45
+ err.httpStatus === 422 || err.httpStatus === 400,
46
+ `Expected 400/422, got ${err.httpStatus}`,
47
+ );
48
+ return true;
49
+ },
50
+ );
51
+ });
52
+
53
+ it("self-payment returns 400/422 error", async (t) => {
54
+ if (skip) return t.skip("server not available");
55
+
56
+ const { payer } = await makeFundedPair();
57
+ await assert.rejects(
58
+ () => payer.payDirect(payer.address, 1.0, "self pay"),
59
+ (err: unknown) => {
60
+ assert.ok(err instanceof RemitError, "must throw RemitError");
61
+ assert.ok(
62
+ err.httpStatus === 422 || err.httpStatus === 400,
63
+ `Expected 400/422, got ${err.httpStatus}`,
64
+ );
65
+ return true;
66
+ },
67
+ );
68
+ });
69
+ });
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Compliance: tab lifecycle against a real server.
3
+ *
4
+ * Verifies: openTab → tab in open state → closeTab → tab no longer open.
5
+ * Requires the compliance server to be running (docker-compose.compliance.yml).
6
+ */
7
+
8
+ import { describe, it, before } from "node:test";
9
+ import assert from "node:assert/strict";
10
+
11
+ import { SERVER_URL, serverIsReachable, makeFundedPair } from "./helpers.js";
12
+
13
+ let skip = false;
14
+
15
+ before(async () => {
16
+ skip = !(await serverIsReachable());
17
+ if (skip) {
18
+ console.warn(
19
+ `[compliance] Server not reachable at ${SERVER_URL}. Skipping tab tests.`,
20
+ );
21
+ }
22
+ });
23
+
24
+ describe("TypeScript compliance: tab lifecycle", () => {
25
+ it("openTab returns tab in open state", async (t) => {
26
+ if (skip) return t.skip("server not available");
27
+
28
+ const { payer, payeeAddress } = await makeFundedPair();
29
+ const tab = await payer.openTab({ to: payeeAddress, limit: 20.0, perUnit: 0.10 });
30
+
31
+ assert.ok(tab.id, "tab must have an id");
32
+ assert.equal(tab.status, "open");
33
+ // Server returns 'limit_amount' which after camelCase transform becomes 'limitAmount'.
34
+ // The TypeScript Tab interface uses 'limit' (a naming mismatch tracked separately).
35
+ // We verify the tab was created in open state, which is the key compliance assertion.
36
+ const raw = tab as unknown as Record<string, unknown>;
37
+ assert.ok(raw["limitAmount"] !== undefined || raw["limit"] !== undefined, "limit field must be present");
38
+ });
39
+
40
+ it("closeTab returns tx and tab is no longer open", async (t) => {
41
+ if (skip) return t.skip("server not available");
42
+
43
+ const { payer, payeeAddress } = await makeFundedPair();
44
+ const tab = await payer.openTab({ to: payeeAddress, limit: 50.0, perUnit: 1.0 });
45
+
46
+ const closeTx = await payer.closeTab(tab.id);
47
+ assert.ok(closeTx.txHash, "closeTx must have txHash");
48
+
49
+ const closedTab = await payer.getTab(tab.id);
50
+ assert.notEqual(closedTab.status, "open", "tab must not be open after close");
51
+ });
52
+ });