crumb-alpha-core 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 (54) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/.turbo/turbo-test.log +34 -0
  3. package/dist/balance.d.ts +5 -0
  4. package/dist/balance.d.ts.map +1 -0
  5. package/dist/balance.js +16 -0
  6. package/dist/balance.js.map +1 -0
  7. package/dist/errors.d.ts +7 -0
  8. package/dist/errors.d.ts.map +1 -0
  9. package/dist/errors.js +11 -0
  10. package/dist/errors.js.map +1 -0
  11. package/dist/gateway.d.ts +6 -0
  12. package/dist/gateway.d.ts.map +1 -0
  13. package/dist/gateway.js +6 -0
  14. package/dist/gateway.js.map +1 -0
  15. package/dist/index.d.ts +11 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +15 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/payment402.d.ts +2 -0
  20. package/dist/payment402.d.ts.map +1 -0
  21. package/dist/payment402.js +15 -0
  22. package/dist/payment402.js.map +1 -0
  23. package/dist/settlement.d.ts +11 -0
  24. package/dist/settlement.d.ts.map +1 -0
  25. package/dist/settlement.js +46 -0
  26. package/dist/settlement.js.map +1 -0
  27. package/dist/signing.d.ts +2 -0
  28. package/dist/signing.d.ts.map +1 -0
  29. package/dist/signing.js +4 -0
  30. package/dist/signing.js.map +1 -0
  31. package/dist/types.d.ts +43 -0
  32. package/dist/types.d.ts.map +1 -0
  33. package/dist/types.js +2 -0
  34. package/dist/types.js.map +1 -0
  35. package/dist/wallet.d.ts +5 -0
  36. package/dist/wallet.d.ts.map +1 -0
  37. package/dist/wallet.js +21 -0
  38. package/dist/wallet.js.map +1 -0
  39. package/package.json +27 -0
  40. package/src/balance.ts +26 -0
  41. package/src/errors.ts +20 -0
  42. package/src/gateway.ts +10 -0
  43. package/src/index.ts +38 -0
  44. package/src/payment402.ts +18 -0
  45. package/src/settlement.ts +68 -0
  46. package/src/signing.ts +4 -0
  47. package/src/types.ts +48 -0
  48. package/src/wallet.ts +24 -0
  49. package/test/balance.test.ts +49 -0
  50. package/test/errors.test.ts +58 -0
  51. package/test/payment402.test.ts +43 -0
  52. package/test/settlement.test.ts +108 -0
  53. package/test/wallet.test.ts +58 -0
  54. package/tsconfig.json +8 -0
@@ -0,0 +1,4 @@
1
+
2
+ > crumb-alpha-core@0.1.0 build /Users/taylorferran/Desktop/dev/crumb-sdk/packages/core
3
+ > tsc
4
+
@@ -0,0 +1,34 @@
1
+
2
+ 
3
+ > @crumb/core@0.1.0 test /Users/taylorferran/Desktop/dev/crumb-sdk/packages/core
4
+ > vitest run
5
+
6
+ [?25l
7
+  RUN  v4.0.18 /Users/taylorferran/Desktop/dev/crumb-sdk/packages/core
8
+
9
+ [?2026h
10
+  ❯ test/payment402.test.ts [queued]
11
+
12
+  Test Files 0 passed (5)
13
+  Tests 0 passed (0)
14
+  Start at 11:08:53
15
+  Duration 101ms
16
+ [?2026l[?2026h ✓ test/errors.test.ts (6 tests) 2ms
17
+ ✓ test/balance.test.ts (3 tests) 2ms
18
+ ✓ test/payment402.test.ts (7 tests) 3ms
19
+ ✓ test/settlement.test.ts (6 tests) 4ms
20
+
21
+  ❯ test/wallet.test.ts 1/8
22
+
23
+  Test Files 4 passed (5)
24
+  Tests 23 passed (30)
25
+  Start at 11:08:53
26
+  Duration 202ms
27
+ [?2026l ✓ test/wallet.test.ts (8 tests) 24ms
28
+
29
+  Test Files  5 passed (5)
30
+  Tests  30 passed (30)
31
+  Start at  11:08:53
32
+  Duration  237ms (transform 170ms, setup 0ms, import 258ms, tests 35ms, environment 0ms)
33
+
34
+ [?25h
@@ -0,0 +1,5 @@
1
+ import type { SupportedChainName, Balances } from '@circle-fin/x402-batching/client';
2
+ import type { CrumbWallet } from './types.js';
3
+ export declare function getBalances(wallet: CrumbWallet, chain?: SupportedChainName): Promise<Balances>;
4
+ export declare function getBalancesForAddress(wallet: CrumbWallet, address: string, chain?: SupportedChainName): Promise<Balances>;
5
+ //# sourceMappingURL=balance.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"balance.d.ts","sourceRoot":"","sources":["../src/balance.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,QAAQ,EAAE,MAAM,kCAAkC,CAAA;AACpF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAE7C,wBAAsB,WAAW,CAC/B,MAAM,EAAE,WAAW,EACnB,KAAK,GAAE,kBAAiC,GACvC,OAAO,CAAC,QAAQ,CAAC,CAMnB;AAED,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,WAAW,EACnB,OAAO,EAAE,MAAM,EACf,KAAK,GAAE,kBAAiC,GACvC,OAAO,CAAC,QAAQ,CAAC,CAMnB"}
@@ -0,0 +1,16 @@
1
+ import { GatewayClient } from '@circle-fin/x402-batching/client';
2
+ export async function getBalances(wallet, chain = 'arcTestnet') {
3
+ const client = new GatewayClient({
4
+ chain,
5
+ privateKey: wallet.privateKey,
6
+ });
7
+ return client.getBalances();
8
+ }
9
+ export async function getBalancesForAddress(wallet, address, chain = 'arcTestnet') {
10
+ const client = new GatewayClient({
11
+ chain,
12
+ privateKey: wallet.privateKey,
13
+ });
14
+ return client.getBalances(address);
15
+ }
16
+ //# sourceMappingURL=balance.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"balance.js","sourceRoot":"","sources":["../src/balance.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAA;AAIhE,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,MAAmB,EACnB,QAA4B,YAAY;IAExC,MAAM,MAAM,GAAG,IAAI,aAAa,CAAC;QAC/B,KAAK;QACL,UAAU,EAAE,MAAM,CAAC,UAA2B;KAC/C,CAAC,CAAA;IACF,OAAO,MAAM,CAAC,WAAW,EAAE,CAAA;AAC7B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,MAAmB,EACnB,OAAe,EACf,QAA4B,YAAY;IAExC,MAAM,MAAM,GAAG,IAAI,aAAa,CAAC;QAC/B,KAAK;QACL,UAAU,EAAE,MAAM,CAAC,UAA2B;KAC/C,CAAC,CAAA;IACF,OAAO,MAAM,CAAC,WAAW,CAAC,OAAwB,CAAC,CAAA;AACrD,CAAC"}
@@ -0,0 +1,7 @@
1
+ export type CrumbErrorCode = 'PRICE_EXCEEDED' | 'INSUFFICIENT_BALANCE' | 'VERIFY_FAILED' | 'SETTLE_FAILED' | 'INVALID_402' | 'NOT_402' | 'PAYMENT_REJECTED' | 'WALLET_NOT_FOUND' | 'NETWORK_ERROR';
2
+ export declare class CrumbError extends Error {
3
+ code: CrumbErrorCode;
4
+ detail?: Record<string, any> | undefined;
5
+ constructor(code: CrumbErrorCode, detail?: Record<string, any> | undefined);
6
+ }
7
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,cAAc,GACtB,gBAAgB,GAChB,sBAAsB,GACtB,eAAe,GACf,eAAe,GACf,aAAa,GACb,SAAS,GACT,kBAAkB,GAClB,kBAAkB,GAClB,eAAe,CAAA;AAEnB,qBAAa,UAAW,SAAQ,KAAK;IAE1B,IAAI,EAAE,cAAc;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;gBAD5B,IAAI,EAAE,cAAc,EACpB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,YAAA;CAKtC"}
package/dist/errors.js ADDED
@@ -0,0 +1,11 @@
1
+ export class CrumbError extends Error {
2
+ code;
3
+ detail;
4
+ constructor(code, detail) {
5
+ super(`[${code}] ${JSON.stringify(detail ?? {})}`);
6
+ this.code = code;
7
+ this.detail = detail;
8
+ this.name = 'CrumbError';
9
+ }
10
+ }
11
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAWA,MAAM,OAAO,UAAW,SAAQ,KAAK;IAE1B;IACA;IAFT,YACS,IAAoB,EACpB,MAA4B;QAEnC,KAAK,CAAC,IAAI,IAAI,KAAK,IAAI,CAAC,SAAS,CAAC,MAAM,IAAI,EAAE,CAAC,EAAE,CAAC,CAAA;QAH3C,SAAI,GAAJ,IAAI,CAAgB;QACpB,WAAM,GAAN,MAAM,CAAsB;QAGnC,IAAI,CAAC,IAAI,GAAG,YAAY,CAAA;IAC1B,CAAC;CACF"}
@@ -0,0 +1,6 @@
1
+ import { GatewayClient } from '@circle-fin/x402-batching/client';
2
+ import type { SupportedChainName, GatewayClientConfig } from '@circle-fin/x402-batching/client';
3
+ export type { SupportedChainName, GatewayClientConfig };
4
+ export { GatewayClient };
5
+ export { CHAIN_CONFIGS, GATEWAY_DOMAINS } from '@circle-fin/x402-batching/client';
6
+ //# sourceMappingURL=gateway.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../src/gateway.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAA;AAChE,OAAO,KAAK,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAA;AAE/F,YAAY,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,CAAA;AAGvD,OAAO,EAAE,aAAa,EAAE,CAAA;AAGxB,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,kCAAkC,CAAA"}
@@ -0,0 +1,6 @@
1
+ import { GatewayClient } from '@circle-fin/x402-batching/client';
2
+ // Re-export GatewayClient so SDK and CLI can use it
3
+ export { GatewayClient };
4
+ // Re-export chain constants
5
+ export { CHAIN_CONFIGS, GATEWAY_DOMAINS } from '@circle-fin/x402-batching/client';
6
+ //# sourceMappingURL=gateway.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gateway.js","sourceRoot":"","sources":["../src/gateway.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAA;AAKhE,oDAAoD;AACpD,OAAO,EAAE,aAAa,EAAE,CAAA;AAExB,4BAA4B;AAC5B,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,kCAAkC,CAAA"}
@@ -0,0 +1,11 @@
1
+ export type { CrumbWallet, SettlementResult, FetchOptions, FetchResult, MiddlewareConfig, SupportedChainName, Balances, PayResult, } from './types.js';
2
+ export { CrumbError } from './errors.js';
3
+ export type { CrumbErrorCode } from './errors.js';
4
+ export { GatewayClient, CHAIN_CONFIGS, GATEWAY_DOMAINS, } from './gateway.js';
5
+ export type { GatewayClientConfig } from './gateway.js';
6
+ export { registerBatchScheme, BatchEvmScheme } from './signing.js';
7
+ export { verifyPayment, settlePayment, verifyAndSettle, BatchFacilitatorClient } from './settlement.js';
8
+ export { getBalances, getBalancesForAddress } from './balance.js';
9
+ export { checkPriceCeiling } from './payment402.js';
10
+ export { createWallet, walletFromPrivateKey, deriveAddress } from './wallet.js';
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EACV,WAAW,EACX,gBAAgB,EAChB,YAAY,EACZ,WAAW,EACX,gBAAgB,EAChB,kBAAkB,EAClB,QAAQ,EACR,SAAS,GACV,MAAM,YAAY,CAAA;AAGnB,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAGjD,OAAO,EACL,aAAa,EACb,aAAa,EACb,eAAe,GAChB,MAAM,cAAc,CAAA;AACrB,YAAY,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA;AAGvD,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAGlE,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,eAAe,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAA;AAGvG,OAAO,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AAGjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AAGnD,OAAO,EAAE,YAAY,EAAE,oBAAoB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,15 @@
1
+ // Errors
2
+ export { CrumbError } from './errors.js';
3
+ // Gateway Client
4
+ export { GatewayClient, CHAIN_CONFIGS, GATEWAY_DOMAINS, } from './gateway.js';
5
+ // Signing (advanced)
6
+ export { registerBatchScheme, BatchEvmScheme } from './signing.js';
7
+ // Settlement / Facilitator
8
+ export { verifyPayment, settlePayment, verifyAndSettle, BatchFacilitatorClient } from './settlement.js';
9
+ // Balance
10
+ export { getBalances, getBalancesForAddress } from './balance.js';
11
+ // 402 Payment parsing
12
+ export { checkPriceCeiling } from './payment402.js';
13
+ // Wallet
14
+ export { createWallet, walletFromPrivateKey, deriveAddress } from './wallet.js';
15
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAYA,SAAS;AACT,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAGxC,iBAAiB;AACjB,OAAO,EACL,aAAa,EACb,aAAa,EACb,eAAe,GAChB,MAAM,cAAc,CAAA;AAGrB,qBAAqB;AACrB,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAElE,2BAA2B;AAC3B,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,eAAe,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAA;AAEvG,UAAU;AACV,OAAO,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AAEjE,sBAAsB;AACtB,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AAEnD,SAAS;AACT,OAAO,EAAE,YAAY,EAAE,oBAAoB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA"}
@@ -0,0 +1,2 @@
1
+ export declare function checkPriceCeiling(amount: string, maxPayment: number): void;
2
+ //# sourceMappingURL=payment402.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"payment402.d.ts","sourceRoot":"","sources":["../src/payment402.ts"],"names":[],"mappings":"AAEA,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,GACjB,IAAI,CAYN"}
@@ -0,0 +1,15 @@
1
+ import { CrumbError } from './errors.js';
2
+ export function checkPriceCeiling(amount, maxPayment) {
3
+ // Strip $ prefix if present
4
+ const clean = amount.startsWith('$') ? amount.slice(1) : amount;
5
+ const quoted = parseFloat(clean);
6
+ if (isNaN(quoted))
7
+ return;
8
+ if (quoted > maxPayment) {
9
+ throw new CrumbError('PRICE_EXCEEDED', {
10
+ quoted,
11
+ ceiling: maxPayment,
12
+ });
13
+ }
14
+ }
15
+ //# sourceMappingURL=payment402.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"payment402.js","sourceRoot":"","sources":["../src/payment402.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAExC,MAAM,UAAU,iBAAiB,CAC/B,MAAc,EACd,UAAkB;IAElB,4BAA4B;IAC5B,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAA;IAC/D,MAAM,MAAM,GAAG,UAAU,CAAC,KAAK,CAAC,CAAA;IAChC,IAAI,KAAK,CAAC,MAAM,CAAC;QAAE,OAAM;IAEzB,IAAI,MAAM,GAAG,UAAU,EAAE,CAAC;QACxB,MAAM,IAAI,UAAU,CAAC,gBAAgB,EAAE;YACrC,MAAM;YACN,OAAO,EAAE,UAAU;SACpB,CAAC,CAAA;IACJ,CAAC;AACH,CAAC"}
@@ -0,0 +1,11 @@
1
+ import { BatchFacilitatorClient } from '@circle-fin/x402-batching/server';
2
+ import type { SettlementResult } from './types.js';
3
+ export declare function verifyPayment(payload: any, requirements: any, facilitatorUrl?: string): Promise<{
4
+ isValid: boolean;
5
+ invalidReason?: string;
6
+ payer?: string;
7
+ }>;
8
+ export declare function settlePayment(payload: any, requirements: any, facilitatorUrl?: string): Promise<SettlementResult>;
9
+ export declare function verifyAndSettle(payload: any, requirements: any, facilitatorUrl?: string): Promise<SettlementResult>;
10
+ export { BatchFacilitatorClient };
11
+ //# sourceMappingURL=settlement.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settlement.d.ts","sourceRoot":"","sources":["../src/settlement.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAA;AACzE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAelD,wBAAsB,aAAa,CACjC,OAAO,EAAE,GAAG,EACZ,YAAY,EAAE,GAAG,EACjB,cAAc,CAAC,EAAE,MAAM,GACtB,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAWvE;AAED,wBAAsB,aAAa,CACjC,OAAO,EAAE,GAAG,EACZ,YAAY,EAAE,GAAG,EACjB,cAAc,CAAC,EAAE,MAAM,GACtB,OAAO,CAAC,gBAAgB,CAAC,CAmB3B;AAED,wBAAsB,eAAe,CACnC,OAAO,EAAE,GAAG,EACZ,YAAY,EAAE,GAAG,EACjB,cAAc,CAAC,EAAE,MAAM,GACtB,OAAO,CAAC,gBAAgB,CAAC,CAG3B;AAED,OAAO,EAAE,sBAAsB,EAAE,CAAA"}
@@ -0,0 +1,46 @@
1
+ import { BatchFacilitatorClient } from '@circle-fin/x402-batching/server';
2
+ import { CrumbError } from './errors.js';
3
+ let defaultFacilitator = null;
4
+ function getFacilitator(facilitatorUrl) {
5
+ if (facilitatorUrl) {
6
+ return new BatchFacilitatorClient({ url: facilitatorUrl });
7
+ }
8
+ if (!defaultFacilitator) {
9
+ defaultFacilitator = new BatchFacilitatorClient();
10
+ }
11
+ return defaultFacilitator;
12
+ }
13
+ export async function verifyPayment(payload, requirements, facilitatorUrl) {
14
+ const facilitator = getFacilitator(facilitatorUrl);
15
+ const result = await facilitator.verify(payload, requirements);
16
+ if (!result.isValid) {
17
+ throw new CrumbError('VERIFY_FAILED', {
18
+ reason: result.invalidReason,
19
+ });
20
+ }
21
+ return result;
22
+ }
23
+ export async function settlePayment(payload, requirements, facilitatorUrl) {
24
+ const facilitator = getFacilitator(facilitatorUrl);
25
+ const result = await facilitator.settle(payload, requirements);
26
+ if (!result.success) {
27
+ throw new CrumbError('SETTLE_FAILED', {
28
+ errorReason: result.errorReason,
29
+ });
30
+ }
31
+ return {
32
+ success: true,
33
+ txHash: result.transaction,
34
+ amount: requirements.amount ?? '0',
35
+ formattedAmount: requirements.amount ?? '0',
36
+ network: result.network,
37
+ payer: result.payer,
38
+ timestamp: new Date().toISOString(),
39
+ };
40
+ }
41
+ export async function verifyAndSettle(payload, requirements, facilitatorUrl) {
42
+ await verifyPayment(payload, requirements, facilitatorUrl);
43
+ return settlePayment(payload, requirements, facilitatorUrl);
44
+ }
45
+ export { BatchFacilitatorClient };
46
+ //# sourceMappingURL=settlement.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settlement.js","sourceRoot":"","sources":["../src/settlement.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAA;AAEzE,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAExC,IAAI,kBAAkB,GAAkC,IAAI,CAAA;AAE5D,SAAS,cAAc,CAAC,cAAuB;IAC7C,IAAI,cAAc,EAAE,CAAC;QACnB,OAAO,IAAI,sBAAsB,CAAC,EAAE,GAAG,EAAE,cAAc,EAAE,CAAC,CAAA;IAC5D,CAAC;IACD,IAAI,CAAC,kBAAkB,EAAE,CAAC;QACxB,kBAAkB,GAAG,IAAI,sBAAsB,EAAE,CAAA;IACnD,CAAC;IACD,OAAO,kBAAkB,CAAA;AAC3B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,OAAY,EACZ,YAAiB,EACjB,cAAuB;IAEvB,MAAM,WAAW,GAAG,cAAc,CAAC,cAAc,CAAC,CAAA;IAClD,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,MAAM,CAAC,OAAO,EAAE,YAAY,CAAC,CAAA;IAE9D,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,IAAI,UAAU,CAAC,eAAe,EAAE;YACpC,MAAM,EAAE,MAAM,CAAC,aAAa;SAC7B,CAAC,CAAA;IACJ,CAAC;IAED,OAAO,MAAM,CAAA;AACf,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,OAAY,EACZ,YAAiB,EACjB,cAAuB;IAEvB,MAAM,WAAW,GAAG,cAAc,CAAC,cAAc,CAAC,CAAA;IAClD,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,MAAM,CAAC,OAAO,EAAE,YAAY,CAAC,CAAA;IAE9D,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,IAAI,UAAU,CAAC,eAAe,EAAE;YACpC,WAAW,EAAE,MAAM,CAAC,WAAW;SAChC,CAAC,CAAA;IACJ,CAAC;IAED,OAAO;QACL,OAAO,EAAE,IAAI;QACb,MAAM,EAAE,MAAM,CAAC,WAAW;QAC1B,MAAM,EAAE,YAAY,CAAC,MAAM,IAAI,GAAG;QAClC,eAAe,EAAE,YAAY,CAAC,MAAM,IAAI,GAAG;QAC3C,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;KACpC,CAAA;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,OAAY,EACZ,YAAiB,EACjB,cAAuB;IAEvB,MAAM,aAAa,CAAC,OAAO,EAAE,YAAY,EAAE,cAAc,CAAC,CAAA;IAC1D,OAAO,aAAa,CAAC,OAAO,EAAE,YAAY,EAAE,cAAc,CAAC,CAAA;AAC7D,CAAC;AAED,OAAO,EAAE,sBAAsB,EAAE,CAAA"}
@@ -0,0 +1,2 @@
1
+ export { registerBatchScheme, BatchEvmScheme } from '@circle-fin/x402-batching/client';
2
+ //# sourceMappingURL=signing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signing.d.ts","sourceRoot":"","sources":["../src/signing.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAA"}
@@ -0,0 +1,4 @@
1
+ // Signing is handled internally by GatewayClient via BatchEvmScheme.
2
+ // This module re-exports the registration helper for advanced use cases.
3
+ export { registerBatchScheme, BatchEvmScheme } from '@circle-fin/x402-batching/client';
4
+ //# sourceMappingURL=signing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signing.js","sourceRoot":"","sources":["../src/signing.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,yEAAyE;AAEzE,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAA"}
@@ -0,0 +1,43 @@
1
+ import type { SupportedChainName, Balances, PayResult } from '@circle-fin/x402-batching/client';
2
+ export type { SupportedChainName, Balances, PayResult };
3
+ export interface CrumbWallet {
4
+ address: string;
5
+ privateKey: string;
6
+ }
7
+ export interface SettlementResult {
8
+ success: boolean;
9
+ txHash: string;
10
+ amount: string;
11
+ formattedAmount: string;
12
+ network: string;
13
+ payer?: string;
14
+ timestamp: string;
15
+ }
16
+ export interface FetchOptions {
17
+ method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
18
+ body?: unknown;
19
+ headers?: Record<string, string>;
20
+ maxPayment?: number;
21
+ metadata?: Record<string, string>;
22
+ }
23
+ export interface FetchResult<T = unknown> {
24
+ data: T;
25
+ amount: bigint;
26
+ formattedAmount: string;
27
+ txHash: string;
28
+ status: number;
29
+ }
30
+ export interface MiddlewareConfig {
31
+ sellerAddress: string;
32
+ networks?: string | string[];
33
+ facilitatorUrl?: string;
34
+ description?: string;
35
+ onPayment?: (payment: {
36
+ verified: boolean;
37
+ payer: string;
38
+ amount: string;
39
+ network: string;
40
+ transaction?: string;
41
+ }) => void;
42
+ }
43
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAA;AAE/F,YAAY,EAAE,kBAAkB,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAA;AAEvD,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,eAAe,EAAE,MAAM,CAAA;IACvB,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,CAAA;IAC1C,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAClC;AAED,MAAM,WAAW,WAAW,CAAC,CAAC,GAAG,OAAO;IACtC,IAAI,EAAE,CAAC,CAAA;IACP,MAAM,EAAE,MAAM,CAAA;IACd,eAAe,EAAE,MAAM,CAAA;IACvB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,gBAAgB;IAC/B,aAAa,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IAC5B,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE;QACpB,QAAQ,EAAE,OAAO,CAAA;QACjB,KAAK,EAAE,MAAM,CAAA;QACb,MAAM,EAAE,MAAM,CAAA;QACd,OAAO,EAAE,MAAM,CAAA;QACf,WAAW,CAAC,EAAE,MAAM,CAAA;KACrB,KAAK,IAAI,CAAA;CACX"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,5 @@
1
+ import type { CrumbWallet } from './types.js';
2
+ export declare function createWallet(): CrumbWallet;
3
+ export declare function walletFromPrivateKey(privateKey: string): CrumbWallet;
4
+ export declare function deriveAddress(privateKey: string): string;
5
+ //# sourceMappingURL=wallet.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wallet.d.ts","sourceRoot":"","sources":["../src/wallet.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAE7C,wBAAgB,YAAY,IAAI,WAAW,CAO1C;AAED,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,WAAW,CAMpE;AAED,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAGxD"}
package/dist/wallet.js ADDED
@@ -0,0 +1,21 @@
1
+ import { privateKeyToAccount, generatePrivateKey } from 'viem/accounts';
2
+ export function createWallet() {
3
+ const privateKey = generatePrivateKey();
4
+ const account = privateKeyToAccount(privateKey);
5
+ return {
6
+ address: account.address,
7
+ privateKey,
8
+ };
9
+ }
10
+ export function walletFromPrivateKey(privateKey) {
11
+ const account = privateKeyToAccount(privateKey);
12
+ return {
13
+ address: account.address,
14
+ privateKey,
15
+ };
16
+ }
17
+ export function deriveAddress(privateKey) {
18
+ const account = privateKeyToAccount(privateKey);
19
+ return account.address;
20
+ }
21
+ //# sourceMappingURL=wallet.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wallet.js","sourceRoot":"","sources":["../src/wallet.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAA;AAGvE,MAAM,UAAU,YAAY;IAC1B,MAAM,UAAU,GAAG,kBAAkB,EAAE,CAAA;IACvC,MAAM,OAAO,GAAG,mBAAmB,CAAC,UAAU,CAAC,CAAA;IAC/C,OAAO;QACL,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,UAAU;KACX,CAAA;AACH,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,UAAkB;IACrD,MAAM,OAAO,GAAG,mBAAmB,CAAC,UAA2B,CAAC,CAAA;IAChE,OAAO;QACL,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,UAAU;KACX,CAAA;AACH,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,UAAkB;IAC9C,MAAM,OAAO,GAAG,mBAAmB,CAAC,UAA2B,CAAC,CAAA;IAChE,OAAO,OAAO,CAAC,OAAO,CAAA;AACxB,CAAC"}
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "crumb-alpha-core",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "dependencies": {
18
+ "@circle-fin/x402-batching": "^2.0.4",
19
+ "viem": "^2.23.0"
20
+ },
21
+ "scripts": {
22
+ "build": "tsc",
23
+ "dev": "tsc --watch",
24
+ "clean": "rm -rf dist",
25
+ "test": "vitest run"
26
+ }
27
+ }
package/src/balance.ts ADDED
@@ -0,0 +1,26 @@
1
+ import { GatewayClient } from '@circle-fin/x402-batching/client'
2
+ import type { SupportedChainName, Balances } from '@circle-fin/x402-batching/client'
3
+ import type { CrumbWallet } from './types.js'
4
+
5
+ export async function getBalances(
6
+ wallet: CrumbWallet,
7
+ chain: SupportedChainName = 'arcTestnet',
8
+ ): Promise<Balances> {
9
+ const client = new GatewayClient({
10
+ chain,
11
+ privateKey: wallet.privateKey as `0x${string}`,
12
+ })
13
+ return client.getBalances()
14
+ }
15
+
16
+ export async function getBalancesForAddress(
17
+ wallet: CrumbWallet,
18
+ address: string,
19
+ chain: SupportedChainName = 'arcTestnet',
20
+ ): Promise<Balances> {
21
+ const client = new GatewayClient({
22
+ chain,
23
+ privateKey: wallet.privateKey as `0x${string}`,
24
+ })
25
+ return client.getBalances(address as `0x${string}`)
26
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,20 @@
1
+ export type CrumbErrorCode =
2
+ | 'PRICE_EXCEEDED' // quoted price > maxPayment ceiling
3
+ | 'INSUFFICIENT_BALANCE' // not enough funds
4
+ | 'VERIFY_FAILED' // facilitator.verify() rejected the payload
5
+ | 'SETTLE_FAILED' // facilitator.settle() returned failure
6
+ | 'INVALID_402' // provider returned malformed 402 payload
7
+ | 'NOT_402' // expected 402, got something else
8
+ | 'PAYMENT_REJECTED' // provider rejected payment header on retry
9
+ | 'WALLET_NOT_FOUND' // no wallet configured
10
+ | 'NETWORK_ERROR' // connectivity issue
11
+
12
+ export class CrumbError extends Error {
13
+ constructor(
14
+ public code: CrumbErrorCode,
15
+ public detail?: Record<string, any>,
16
+ ) {
17
+ super(`[${code}] ${JSON.stringify(detail ?? {})}`)
18
+ this.name = 'CrumbError'
19
+ }
20
+ }
package/src/gateway.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { GatewayClient } from '@circle-fin/x402-batching/client'
2
+ import type { SupportedChainName, GatewayClientConfig } from '@circle-fin/x402-batching/client'
3
+
4
+ export type { SupportedChainName, GatewayClientConfig }
5
+
6
+ // Re-export GatewayClient so SDK and CLI can use it
7
+ export { GatewayClient }
8
+
9
+ // Re-export chain constants
10
+ export { CHAIN_CONFIGS, GATEWAY_DOMAINS } from '@circle-fin/x402-batching/client'
package/src/index.ts ADDED
@@ -0,0 +1,38 @@
1
+ // Types
2
+ export type {
3
+ CrumbWallet,
4
+ SettlementResult,
5
+ FetchOptions,
6
+ FetchResult,
7
+ MiddlewareConfig,
8
+ SupportedChainName,
9
+ Balances,
10
+ PayResult,
11
+ } from './types.js'
12
+
13
+ // Errors
14
+ export { CrumbError } from './errors.js'
15
+ export type { CrumbErrorCode } from './errors.js'
16
+
17
+ // Gateway Client
18
+ export {
19
+ GatewayClient,
20
+ CHAIN_CONFIGS,
21
+ GATEWAY_DOMAINS,
22
+ } from './gateway.js'
23
+ export type { GatewayClientConfig } from './gateway.js'
24
+
25
+ // Signing (advanced)
26
+ export { registerBatchScheme, BatchEvmScheme } from './signing.js'
27
+
28
+ // Settlement / Facilitator
29
+ export { verifyPayment, settlePayment, verifyAndSettle, BatchFacilitatorClient } from './settlement.js'
30
+
31
+ // Balance
32
+ export { getBalances, getBalancesForAddress } from './balance.js'
33
+
34
+ // 402 Payment parsing
35
+ export { checkPriceCeiling } from './payment402.js'
36
+
37
+ // Wallet
38
+ export { createWallet, walletFromPrivateKey, deriveAddress } from './wallet.js'
@@ -0,0 +1,18 @@
1
+ import { CrumbError } from './errors.js'
2
+
3
+ export function checkPriceCeiling(
4
+ amount: string,
5
+ maxPayment: number,
6
+ ): void {
7
+ // Strip $ prefix if present
8
+ const clean = amount.startsWith('$') ? amount.slice(1) : amount
9
+ const quoted = parseFloat(clean)
10
+ if (isNaN(quoted)) return
11
+
12
+ if (quoted > maxPayment) {
13
+ throw new CrumbError('PRICE_EXCEEDED', {
14
+ quoted,
15
+ ceiling: maxPayment,
16
+ })
17
+ }
18
+ }
@@ -0,0 +1,68 @@
1
+ import { BatchFacilitatorClient } from '@circle-fin/x402-batching/server'
2
+ import type { SettlementResult } from './types.js'
3
+ import { CrumbError } from './errors.js'
4
+
5
+ let defaultFacilitator: BatchFacilitatorClient | null = null
6
+
7
+ function getFacilitator(facilitatorUrl?: string): BatchFacilitatorClient {
8
+ if (facilitatorUrl) {
9
+ return new BatchFacilitatorClient({ url: facilitatorUrl })
10
+ }
11
+ if (!defaultFacilitator) {
12
+ defaultFacilitator = new BatchFacilitatorClient()
13
+ }
14
+ return defaultFacilitator
15
+ }
16
+
17
+ export async function verifyPayment(
18
+ payload: any,
19
+ requirements: any,
20
+ facilitatorUrl?: string,
21
+ ): Promise<{ isValid: boolean; invalidReason?: string; payer?: string }> {
22
+ const facilitator = getFacilitator(facilitatorUrl)
23
+ const result = await facilitator.verify(payload, requirements)
24
+
25
+ if (!result.isValid) {
26
+ throw new CrumbError('VERIFY_FAILED', {
27
+ reason: result.invalidReason,
28
+ })
29
+ }
30
+
31
+ return result
32
+ }
33
+
34
+ export async function settlePayment(
35
+ payload: any,
36
+ requirements: any,
37
+ facilitatorUrl?: string,
38
+ ): Promise<SettlementResult> {
39
+ const facilitator = getFacilitator(facilitatorUrl)
40
+ const result = await facilitator.settle(payload, requirements)
41
+
42
+ if (!result.success) {
43
+ throw new CrumbError('SETTLE_FAILED', {
44
+ errorReason: result.errorReason,
45
+ })
46
+ }
47
+
48
+ return {
49
+ success: true,
50
+ txHash: result.transaction,
51
+ amount: requirements.amount ?? '0',
52
+ formattedAmount: requirements.amount ?? '0',
53
+ network: result.network,
54
+ payer: result.payer,
55
+ timestamp: new Date().toISOString(),
56
+ }
57
+ }
58
+
59
+ export async function verifyAndSettle(
60
+ payload: any,
61
+ requirements: any,
62
+ facilitatorUrl?: string,
63
+ ): Promise<SettlementResult> {
64
+ await verifyPayment(payload, requirements, facilitatorUrl)
65
+ return settlePayment(payload, requirements, facilitatorUrl)
66
+ }
67
+
68
+ export { BatchFacilitatorClient }
package/src/signing.ts ADDED
@@ -0,0 +1,4 @@
1
+ // Signing is handled internally by GatewayClient via BatchEvmScheme.
2
+ // This module re-exports the registration helper for advanced use cases.
3
+
4
+ export { registerBatchScheme, BatchEvmScheme } from '@circle-fin/x402-batching/client'
package/src/types.ts ADDED
@@ -0,0 +1,48 @@
1
+ import type { SupportedChainName, Balances, PayResult } from '@circle-fin/x402-batching/client'
2
+
3
+ export type { SupportedChainName, Balances, PayResult }
4
+
5
+ export interface CrumbWallet {
6
+ address: string
7
+ privateKey: string
8
+ }
9
+
10
+ export interface SettlementResult {
11
+ success: boolean
12
+ txHash: string
13
+ amount: string
14
+ formattedAmount: string
15
+ network: string
16
+ payer?: string
17
+ timestamp: string
18
+ }
19
+
20
+ export interface FetchOptions {
21
+ method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
22
+ body?: unknown
23
+ headers?: Record<string, string>
24
+ maxPayment?: number // USDC ceiling — throws if quoted price exceeds this
25
+ metadata?: Record<string, string>
26
+ }
27
+
28
+ export interface FetchResult<T = unknown> {
29
+ data: T
30
+ amount: bigint
31
+ formattedAmount: string
32
+ txHash: string
33
+ status: number
34
+ }
35
+
36
+ export interface MiddlewareConfig {
37
+ sellerAddress: string
38
+ networks?: string | string[]
39
+ facilitatorUrl?: string
40
+ description?: string
41
+ onPayment?: (payment: {
42
+ verified: boolean
43
+ payer: string
44
+ amount: string
45
+ network: string
46
+ transaction?: string
47
+ }) => void
48
+ }
package/src/wallet.ts ADDED
@@ -0,0 +1,24 @@
1
+ import { privateKeyToAccount, generatePrivateKey } from 'viem/accounts'
2
+ import type { CrumbWallet } from './types.js'
3
+
4
+ export function createWallet(): CrumbWallet {
5
+ const privateKey = generatePrivateKey()
6
+ const account = privateKeyToAccount(privateKey)
7
+ return {
8
+ address: account.address,
9
+ privateKey,
10
+ }
11
+ }
12
+
13
+ export function walletFromPrivateKey(privateKey: string): CrumbWallet {
14
+ const account = privateKeyToAccount(privateKey as `0x${string}`)
15
+ return {
16
+ address: account.address,
17
+ privateKey,
18
+ }
19
+ }
20
+
21
+ export function deriveAddress(privateKey: string): string {
22
+ const account = privateKeyToAccount(privateKey as `0x${string}`)
23
+ return account.address
24
+ }
@@ -0,0 +1,49 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+
3
+ const mockGetBalances = vi.fn()
4
+
5
+ class MockGatewayClient {
6
+ getBalances = mockGetBalances
7
+ constructor(public config: any) {}
8
+ }
9
+
10
+ vi.mock('@circle-fin/x402-batching/client', () => ({
11
+ GatewayClient: MockGatewayClient,
12
+ }))
13
+
14
+ const { getBalances, getBalancesForAddress } = await import('../src/balance.js')
15
+
16
+ describe('getBalances', () => {
17
+ it('creates a GatewayClient and calls getBalances', async () => {
18
+ const mockResult = {
19
+ wallet: { formatted: '10.00' },
20
+ gateway: { formattedAvailable: '5.00' },
21
+ }
22
+ mockGetBalances.mockResolvedValue(mockResult)
23
+
24
+ const wallet = { address: '0xabc', privateKey: '0x1234' }
25
+ const result = await getBalances(wallet)
26
+ expect(result).toEqual(mockResult)
27
+ expect(mockGetBalances).toHaveBeenCalled()
28
+ })
29
+
30
+ it('uses baseSepolia as default chain', async () => {
31
+ mockGetBalances.mockResolvedValue({})
32
+
33
+ const wallet = { address: '0xabc', privateKey: '0x1234' }
34
+ await getBalances(wallet)
35
+
36
+ expect(mockGetBalances).toHaveBeenCalled()
37
+ })
38
+ })
39
+
40
+ describe('getBalancesForAddress', () => {
41
+ it('passes address to getBalances call', async () => {
42
+ mockGetBalances.mockResolvedValue({})
43
+
44
+ const wallet = { address: '0xabc', privateKey: '0x1234' }
45
+ await getBalancesForAddress(wallet, '0xother')
46
+
47
+ expect(mockGetBalances).toHaveBeenCalledWith('0xother')
48
+ })
49
+ })
@@ -0,0 +1,58 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { CrumbError } from '../src/errors.js'
3
+
4
+ describe('CrumbError', () => {
5
+ it('creates an error with code and detail', () => {
6
+ const err = new CrumbError('PRICE_EXCEEDED', { quoted: 0.5, ceiling: 0.01 })
7
+ expect(err).toBeInstanceOf(Error)
8
+ expect(err).toBeInstanceOf(CrumbError)
9
+ expect(err.code).toBe('PRICE_EXCEEDED')
10
+ expect(err.detail).toEqual({ quoted: 0.5, ceiling: 0.01 })
11
+ expect(err.name).toBe('CrumbError')
12
+ })
13
+
14
+ it('includes code in message', () => {
15
+ const err = new CrumbError('INSUFFICIENT_BALANCE')
16
+ expect(err.message).toContain('INSUFFICIENT_BALANCE')
17
+ })
18
+
19
+ it('includes detail in message as JSON', () => {
20
+ const err = new CrumbError('VERIFY_FAILED', { reason: 'expired' })
21
+ expect(err.message).toContain('expired')
22
+ })
23
+
24
+ it('works without detail', () => {
25
+ const err = new CrumbError('NETWORK_ERROR')
26
+ expect(err.code).toBe('NETWORK_ERROR')
27
+ expect(err.detail).toBeUndefined()
28
+ expect(err.message).toContain('NETWORK_ERROR')
29
+ })
30
+
31
+ it('can be caught as Error', () => {
32
+ try {
33
+ throw new CrumbError('WALLET_NOT_FOUND')
34
+ } catch (e) {
35
+ expect(e).toBeInstanceOf(Error)
36
+ expect((e as CrumbError).code).toBe('WALLET_NOT_FOUND')
37
+ }
38
+ })
39
+
40
+ it('preserves all error code types', () => {
41
+ const codes = [
42
+ 'PRICE_EXCEEDED',
43
+ 'INSUFFICIENT_BALANCE',
44
+ 'VERIFY_FAILED',
45
+ 'SETTLE_FAILED',
46
+ 'INVALID_402',
47
+ 'NOT_402',
48
+ 'PAYMENT_REJECTED',
49
+ 'WALLET_NOT_FOUND',
50
+ 'NETWORK_ERROR',
51
+ ] as const
52
+
53
+ for (const code of codes) {
54
+ const err = new CrumbError(code)
55
+ expect(err.code).toBe(code)
56
+ }
57
+ })
58
+ })
@@ -0,0 +1,43 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { checkPriceCeiling } from '../src/payment402.js'
3
+ import { CrumbError } from '../src/errors.js'
4
+
5
+ describe('checkPriceCeiling', () => {
6
+ it('does not throw when price is under ceiling', () => {
7
+ expect(() => checkPriceCeiling('0.001', 0.01)).not.toThrow()
8
+ })
9
+
10
+ it('does not throw when price equals ceiling', () => {
11
+ expect(() => checkPriceCeiling('0.01', 0.01)).not.toThrow()
12
+ })
13
+
14
+ it('throws PRICE_EXCEEDED when price exceeds ceiling', () => {
15
+ expect(() => checkPriceCeiling('0.05', 0.01)).toThrow(CrumbError)
16
+ try {
17
+ checkPriceCeiling('0.05', 0.01)
18
+ } catch (e) {
19
+ expect((e as CrumbError).code).toBe('PRICE_EXCEEDED')
20
+ expect((e as CrumbError).detail).toEqual({ quoted: 0.05, ceiling: 0.01 })
21
+ }
22
+ })
23
+
24
+ it('handles $ prefix in amount', () => {
25
+ expect(() => checkPriceCeiling('$0.001', 0.01)).not.toThrow()
26
+ expect(() => checkPriceCeiling('$0.05', 0.01)).toThrow(CrumbError)
27
+ })
28
+
29
+ it('does not throw for non-numeric amounts', () => {
30
+ expect(() => checkPriceCeiling('free', 0.01)).not.toThrow()
31
+ expect(() => checkPriceCeiling('', 0.01)).not.toThrow()
32
+ })
33
+
34
+ it('handles large amounts correctly', () => {
35
+ expect(() => checkPriceCeiling('100.00', 50)).toThrow(CrumbError)
36
+ expect(() => checkPriceCeiling('50.00', 100)).not.toThrow()
37
+ })
38
+
39
+ it('handles very small amounts', () => {
40
+ expect(() => checkPriceCeiling('0.0001', 0.001)).not.toThrow()
41
+ expect(() => checkPriceCeiling('0.001', 0.0001)).toThrow(CrumbError)
42
+ })
43
+ })
@@ -0,0 +1,108 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
+ import { CrumbError } from '../src/errors.js'
3
+
4
+ const mockVerify = vi.fn()
5
+ const mockSettle = vi.fn()
6
+
7
+ class MockBatchFacilitatorClient {
8
+ verify = mockVerify
9
+ settle = mockSettle
10
+ constructor(_config?: any) {}
11
+ }
12
+
13
+ vi.mock('@circle-fin/x402-batching/server', () => ({
14
+ BatchFacilitatorClient: MockBatchFacilitatorClient,
15
+ }))
16
+
17
+ // Import after mocking
18
+ const { verifyPayment, settlePayment, verifyAndSettle } = await import('../src/settlement.js')
19
+
20
+ describe('verifyPayment', () => {
21
+ beforeEach(() => {
22
+ vi.clearAllMocks()
23
+ })
24
+
25
+ it('returns result when verification succeeds', async () => {
26
+ mockVerify.mockResolvedValue({ isValid: true, payer: '0xabc' })
27
+
28
+ const result = await verifyPayment({ payment: 'data' }, { amount: '0.01' })
29
+ expect(result.isValid).toBe(true)
30
+ expect(result.payer).toBe('0xabc')
31
+ expect(mockVerify).toHaveBeenCalledWith({ payment: 'data' }, { amount: '0.01' })
32
+ })
33
+
34
+ it('throws VERIFY_FAILED when verification fails', async () => {
35
+ mockVerify.mockResolvedValue({ isValid: false, invalidReason: 'expired signature' })
36
+
37
+ await expect(verifyPayment({}, {})).rejects.toThrow(CrumbError)
38
+ try {
39
+ await verifyPayment({}, {})
40
+ } catch (e) {
41
+ expect((e as CrumbError).code).toBe('VERIFY_FAILED')
42
+ expect((e as CrumbError).detail?.reason).toBe('expired signature')
43
+ }
44
+ })
45
+ })
46
+
47
+ describe('settlePayment', () => {
48
+ beforeEach(() => {
49
+ vi.clearAllMocks()
50
+ })
51
+
52
+ it('returns SettlementResult on success', async () => {
53
+ mockSettle.mockResolvedValue({
54
+ success: true,
55
+ transaction: '0xtxhash',
56
+ network: 'arcTestnet',
57
+ payer: '0xpayer',
58
+ })
59
+
60
+ const result = await settlePayment({}, { amount: '0.001' })
61
+ expect(result.success).toBe(true)
62
+ expect(result.txHash).toBe('0xtxhash')
63
+ expect(result.network).toBe('arcTestnet')
64
+ expect(result.payer).toBe('0xpayer')
65
+ expect(result.amount).toBe('0.001')
66
+ expect(result.timestamp).toBeDefined()
67
+ })
68
+
69
+ it('throws SETTLE_FAILED when settlement fails', async () => {
70
+ mockSettle.mockResolvedValue({ success: false, errorReason: 'insufficient funds' })
71
+
72
+ await expect(settlePayment({}, {})).rejects.toThrow(CrumbError)
73
+ try {
74
+ await settlePayment({}, {})
75
+ } catch (e) {
76
+ expect((e as CrumbError).code).toBe('SETTLE_FAILED')
77
+ expect((e as CrumbError).detail?.errorReason).toBe('insufficient funds')
78
+ }
79
+ })
80
+ })
81
+
82
+ describe('verifyAndSettle', () => {
83
+ beforeEach(() => {
84
+ vi.clearAllMocks()
85
+ })
86
+
87
+ it('calls verify then settle', async () => {
88
+ mockVerify.mockResolvedValue({ isValid: true, payer: '0xabc' })
89
+ mockSettle.mockResolvedValue({
90
+ success: true,
91
+ transaction: '0xtx',
92
+ network: 'arcTestnet',
93
+ payer: '0xabc',
94
+ })
95
+
96
+ const result = await verifyAndSettle({}, { amount: '0.01' })
97
+ expect(mockVerify).toHaveBeenCalled()
98
+ expect(mockSettle).toHaveBeenCalled()
99
+ expect(result.success).toBe(true)
100
+ })
101
+
102
+ it('does not settle if verify fails', async () => {
103
+ mockVerify.mockResolvedValue({ isValid: false, invalidReason: 'bad sig' })
104
+
105
+ await expect(verifyAndSettle({}, {})).rejects.toThrow(CrumbError)
106
+ expect(mockSettle).not.toHaveBeenCalled()
107
+ })
108
+ })
@@ -0,0 +1,58 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { createWallet, walletFromPrivateKey, deriveAddress } from '../src/wallet.js'
3
+
4
+ describe('createWallet', () => {
5
+ it('returns an object with address and privateKey', () => {
6
+ const wallet = createWallet()
7
+ expect(wallet).toHaveProperty('address')
8
+ expect(wallet).toHaveProperty('privateKey')
9
+ })
10
+
11
+ it('generates a valid hex private key', () => {
12
+ const wallet = createWallet()
13
+ expect(wallet.privateKey).toMatch(/^0x[0-9a-f]{64}$/i)
14
+ })
15
+
16
+ it('generates a valid checksummed address', () => {
17
+ const wallet = createWallet()
18
+ expect(wallet.address).toMatch(/^0x[0-9a-fA-F]{40}$/)
19
+ })
20
+
21
+ it('generates unique wallets each time', () => {
22
+ const a = createWallet()
23
+ const b = createWallet()
24
+ expect(a.privateKey).not.toBe(b.privateKey)
25
+ expect(a.address).not.toBe(b.address)
26
+ })
27
+ })
28
+
29
+ describe('walletFromPrivateKey', () => {
30
+ it('derives the correct address from a known private key', () => {
31
+ const pk = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'
32
+ const wallet = walletFromPrivateKey(pk)
33
+ expect(wallet.privateKey).toBe(pk)
34
+ // This is the well-known Hardhat account #0 address
35
+ expect(wallet.address).toBe('0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266')
36
+ })
37
+
38
+ it('returns consistent results for the same key', () => {
39
+ const pk = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'
40
+ const a = walletFromPrivateKey(pk)
41
+ const b = walletFromPrivateKey(pk)
42
+ expect(a.address).toBe(b.address)
43
+ })
44
+ })
45
+
46
+ describe('deriveAddress', () => {
47
+ it('derives address from a private key', () => {
48
+ const pk = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'
49
+ const address = deriveAddress(pk)
50
+ expect(address).toBe('0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266')
51
+ })
52
+
53
+ it('matches createWallet output', () => {
54
+ const wallet = createWallet()
55
+ const address = deriveAddress(wallet.privateKey)
56
+ expect(address).toBe(wallet.address)
57
+ })
58
+ })
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src"]
8
+ }