crumb-alpha-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.
- package/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-test.log +32 -0
- package/dist/client.d.ts +49 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +122 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/express.d.ts +30 -0
- package/dist/middleware/express.d.ts.map +1 -0
- package/dist/middleware/express.js +43 -0
- package/dist/middleware/express.js.map +1 -0
- package/dist/middleware/fastify.d.ts +9 -0
- package/dist/middleware/fastify.d.ts.map +1 -0
- package/dist/middleware/fastify.js +44 -0
- package/dist/middleware/fastify.js.map +1 -0
- package/dist/middleware/fetch.d.ts +7 -0
- package/dist/middleware/fetch.d.ts.map +1 -0
- package/dist/middleware/fetch.js +49 -0
- package/dist/middleware/fetch.js.map +1 -0
- package/dist/provider.d.ts +33 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +46 -0
- package/dist/provider.js.map +1 -0
- package/package.json +43 -0
- package/src/client.ts +161 -0
- package/src/index.ts +29 -0
- package/src/middleware/express.ts +45 -0
- package/src/provider.ts +50 -0
- package/test/client.test.ts +243 -0
- package/test/middleware.test.ts +63 -0
- package/test/provider.test.ts +89 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
> @crumb/sdk@0.1.0 test /Users/taylorferran/Desktop/dev/crumb-sdk/packages/sdk
|
|
4
|
+
> vitest run
|
|
5
|
+
|
|
6
|
+
[?25l
|
|
7
|
+
[1m[46m RUN [49m[22m [36mv4.0.18 [39m[90m/Users/taylorferran/Desktop/dev/crumb-sdk/packages/sdk[39m
|
|
8
|
+
|
|
9
|
+
[?2026h
|
|
10
|
+
[1m[33m ❯ [39m[22mtest/provider.test.ts[2m [queued][22m
|
|
11
|
+
|
|
12
|
+
[2m Test Files [22m[1m[32m0 passed[39m[22m[90m (3)[39m
|
|
13
|
+
[2m Tests [22m[1m[32m0 passed[39m[22m[90m (0)[39m
|
|
14
|
+
[2m Start at [22m11:08:54
|
|
15
|
+
[2m Duration [22m101ms
|
|
16
|
+
[?2026l[?2026h[K[1A[K[1A[K[1A[K[1A[K[1A[K[1A[K[1A[K [32m✓[39m test/middleware.test.ts [2m([22m[2m4 tests[22m[2m)[22m[32m 2[2mms[22m[39m
|
|
17
|
+
[32m✓[39m test/provider.test.ts [2m([22m[2m6 tests[22m[2m)[22m[32m 3[2mms[22m[39m
|
|
18
|
+
|
|
19
|
+
[1m[33m ❯ [39m[22mtest/client.test.ts[2m 0/15[22m
|
|
20
|
+
|
|
21
|
+
[2m Test Files [22m[1m[32m2 passed[39m[22m[90m (3)[39m
|
|
22
|
+
[2m Tests [22m[1m[32m10 passed[39m[22m[90m (25)[39m
|
|
23
|
+
[2m Start at [22m11:08:54
|
|
24
|
+
[2m Duration [22m302ms
|
|
25
|
+
[?2026l[K[1A[K[1A[K[1A[K[1A[K[1A[K[1A[K[1A[K [32m✓[39m test/client.test.ts [2m([22m[2m15 tests[22m[2m)[22m[32m 25[2mms[22m[39m
|
|
26
|
+
|
|
27
|
+
[2m Test Files [22m [1m[32m3 passed[39m[22m[90m (3)[39m
|
|
28
|
+
[2m Tests [22m [1m[32m25 passed[39m[22m[90m (25)[39m
|
|
29
|
+
[2m Start at [22m 11:08:54
|
|
30
|
+
[2m Duration [22m 400ms[2m (transform 129ms, setup 0ms, import 354ms, tests 31ms, environment 0ms)[22m
|
|
31
|
+
|
|
32
|
+
[?25h
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { GatewayClient } from 'crumb-alpha-core';
|
|
2
|
+
import type { FetchOptions, FetchResult, SupportedChainName, Balances } from 'crumb-alpha-core';
|
|
3
|
+
export declare class Crumb {
|
|
4
|
+
private wallet;
|
|
5
|
+
private gateway;
|
|
6
|
+
private chain;
|
|
7
|
+
private lowBalanceHook?;
|
|
8
|
+
private lowBalanceThreshold;
|
|
9
|
+
constructor(config: {
|
|
10
|
+
privateKey: string;
|
|
11
|
+
chain?: SupportedChainName;
|
|
12
|
+
rpcUrl?: string;
|
|
13
|
+
lowBalanceThreshold?: number;
|
|
14
|
+
onLowBalance?: (balance: string) => void;
|
|
15
|
+
});
|
|
16
|
+
get address(): string;
|
|
17
|
+
/**
|
|
18
|
+
* Pay for an x402-protected resource. Handles the full 402 flow automatically.
|
|
19
|
+
*/
|
|
20
|
+
fetch<T = unknown>(url: string, options?: FetchOptions): Promise<FetchResult<T>>;
|
|
21
|
+
/**
|
|
22
|
+
* Deposit USDC into Gateway for gasless payments.
|
|
23
|
+
*/
|
|
24
|
+
deposit(amount: string): Promise<import("@circle-fin/x402-batching/client").DepositResult>;
|
|
25
|
+
/**
|
|
26
|
+
* Withdraw USDC from Gateway.
|
|
27
|
+
*/
|
|
28
|
+
withdraw(amount: string, options?: {
|
|
29
|
+
chain?: SupportedChainName;
|
|
30
|
+
recipient?: string;
|
|
31
|
+
}): Promise<import("@circle-fin/x402-batching/client").WithdrawResult>;
|
|
32
|
+
/**
|
|
33
|
+
* Get all balances (wallet + gateway).
|
|
34
|
+
*/
|
|
35
|
+
balances(): Promise<Balances>;
|
|
36
|
+
/**
|
|
37
|
+
* Get gateway available balance as a formatted string.
|
|
38
|
+
*/
|
|
39
|
+
balance(): Promise<string>;
|
|
40
|
+
/**
|
|
41
|
+
* Check if a URL supports Gateway batching.
|
|
42
|
+
*/
|
|
43
|
+
supports(url: string): Promise<import("@circle-fin/x402-batching/client").SupportsResult>;
|
|
44
|
+
onLowBalance(threshold: number, fn: (balance: string) => void): void;
|
|
45
|
+
/** Access the underlying GatewayClient for advanced operations. */
|
|
46
|
+
get client(): GatewayClient;
|
|
47
|
+
private checkLowBalance;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EAKd,MAAM,kBAAkB,CAAA;AACzB,OAAO,KAAK,EAEV,YAAY,EACZ,WAAW,EACX,kBAAkB,EAClB,QAAQ,EACT,MAAM,kBAAkB,CAAA;AAEzB,qBAAa,KAAK;IAChB,OAAO,CAAC,MAAM,CAAa;IAC3B,OAAO,CAAC,OAAO,CAAe;IAC9B,OAAO,CAAC,KAAK,CAAoB;IACjC,OAAO,CAAC,cAAc,CAAC,CAA2B;IAClD,OAAO,CAAC,mBAAmB,CAAc;gBAE7B,MAAM,EAAE;QAClB,UAAU,EAAE,MAAM,CAAA;QAClB,KAAK,CAAC,EAAE,kBAAkB,CAAA;QAC1B,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,mBAAmB,CAAC,EAAE,MAAM,CAAA;QAC5B,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;KACzC;IAmBD,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED;;OAEG;IACG,KAAK,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;IA8B1F;;OAEG;IACG,OAAO,CAAC,MAAM,EAAE,MAAM;IAI5B;;OAEG;IACG,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QACvC,KAAK,CAAC,EAAE,kBAAkB,CAAA;QAC1B,SAAS,CAAC,EAAE,MAAM,CAAA;KACnB;IAOD;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,QAAQ,CAAC;IAWnC;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC;IAWhC;;OAEG;IACG,QAAQ,CAAC,GAAG,EAAE,MAAM;IAI1B,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI;IAK7D,mEAAmE;IACnE,IAAI,MAAM,IAAI,aAAa,CAE1B;YAEa,eAAe;CAW9B"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { GatewayClient, checkPriceCeiling, deriveAddress, } from 'crumb-alpha-core';
|
|
2
|
+
export class Crumb {
|
|
3
|
+
wallet;
|
|
4
|
+
gateway;
|
|
5
|
+
chain;
|
|
6
|
+
lowBalanceHook;
|
|
7
|
+
lowBalanceThreshold = 1.0;
|
|
8
|
+
constructor(config) {
|
|
9
|
+
this.chain = config.chain ?? 'arcTestnet';
|
|
10
|
+
this.wallet = {
|
|
11
|
+
address: deriveAddress(config.privateKey),
|
|
12
|
+
privateKey: config.privateKey,
|
|
13
|
+
};
|
|
14
|
+
this.gateway = new GatewayClient({
|
|
15
|
+
chain: this.chain,
|
|
16
|
+
privateKey: config.privateKey,
|
|
17
|
+
rpcUrl: config.rpcUrl,
|
|
18
|
+
});
|
|
19
|
+
if (config.lowBalanceThreshold !== undefined) {
|
|
20
|
+
this.lowBalanceThreshold = config.lowBalanceThreshold;
|
|
21
|
+
}
|
|
22
|
+
if (config.onLowBalance) {
|
|
23
|
+
this.lowBalanceHook = config.onLowBalance;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
get address() {
|
|
27
|
+
return this.wallet.address;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Pay for an x402-protected resource. Handles the full 402 flow automatically.
|
|
31
|
+
*/
|
|
32
|
+
async fetch(url, options = {}) {
|
|
33
|
+
if (options.maxPayment !== undefined) {
|
|
34
|
+
// Pre-check: probe the URL to see the price before paying
|
|
35
|
+
const supported = await this.gateway.supports(url);
|
|
36
|
+
if (supported.supported && supported.requirements) {
|
|
37
|
+
const amount = supported.requirements?.amount;
|
|
38
|
+
if (amount) {
|
|
39
|
+
checkPriceCeiling(amount, options.maxPayment);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const result = await this.gateway.pay(url, {
|
|
44
|
+
method: options.method,
|
|
45
|
+
body: options.body,
|
|
46
|
+
headers: options.headers,
|
|
47
|
+
});
|
|
48
|
+
// Check low balance after payment
|
|
49
|
+
this.checkLowBalance();
|
|
50
|
+
return {
|
|
51
|
+
data: result.data,
|
|
52
|
+
amount: result.amount,
|
|
53
|
+
formattedAmount: result.formattedAmount,
|
|
54
|
+
txHash: result.transaction,
|
|
55
|
+
status: result.status,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Deposit USDC into Gateway for gasless payments.
|
|
60
|
+
*/
|
|
61
|
+
async deposit(amount) {
|
|
62
|
+
return this.gateway.deposit(amount);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Withdraw USDC from Gateway.
|
|
66
|
+
*/
|
|
67
|
+
async withdraw(amount, options) {
|
|
68
|
+
return this.gateway.withdraw(amount, {
|
|
69
|
+
chain: options?.chain,
|
|
70
|
+
recipient: options?.recipient,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Get all balances (wallet + gateway).
|
|
75
|
+
*/
|
|
76
|
+
async balances() {
|
|
77
|
+
const result = await this.gateway.getBalances();
|
|
78
|
+
const available = result.gateway.formattedAvailable;
|
|
79
|
+
if (parseFloat(available) < this.lowBalanceThreshold) {
|
|
80
|
+
this.lowBalanceHook?.(available);
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Get gateway available balance as a formatted string.
|
|
86
|
+
*/
|
|
87
|
+
async balance() {
|
|
88
|
+
const result = await this.gateway.getBalances();
|
|
89
|
+
const available = result.gateway.formattedAvailable;
|
|
90
|
+
if (parseFloat(available) < this.lowBalanceThreshold) {
|
|
91
|
+
this.lowBalanceHook?.(available);
|
|
92
|
+
}
|
|
93
|
+
return available;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Check if a URL supports Gateway batching.
|
|
97
|
+
*/
|
|
98
|
+
async supports(url) {
|
|
99
|
+
return this.gateway.supports(url);
|
|
100
|
+
}
|
|
101
|
+
onLowBalance(threshold, fn) {
|
|
102
|
+
this.lowBalanceThreshold = threshold;
|
|
103
|
+
this.lowBalanceHook = fn;
|
|
104
|
+
}
|
|
105
|
+
/** Access the underlying GatewayClient for advanced operations. */
|
|
106
|
+
get client() {
|
|
107
|
+
return this.gateway;
|
|
108
|
+
}
|
|
109
|
+
async checkLowBalance() {
|
|
110
|
+
try {
|
|
111
|
+
const result = await this.gateway.getBalances();
|
|
112
|
+
const available = parseFloat(result.gateway.formattedAvailable);
|
|
113
|
+
if (available < this.lowBalanceThreshold) {
|
|
114
|
+
this.lowBalanceHook?.(result.gateway.formattedAvailable);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// Don't fail the payment if balance check fails
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { Crumb } from './client.js';
|
|
2
|
+
export { createProvider } from './provider.js';
|
|
3
|
+
export type { GatewayMiddleware, PaymentRequest } from './provider.js';
|
|
4
|
+
export { crumbMiddleware } from './middleware/express.js';
|
|
5
|
+
export type { CrumbWallet, SettlementResult, FetchOptions, FetchResult, MiddlewareConfig, SupportedChainName, Balances, PayResult, } from 'crumb-alpha-core';
|
|
6
|
+
export { CrumbError, GatewayClient, CHAIN_CONFIGS, GATEWAY_DOMAINS, } from 'crumb-alpha-core';
|
|
7
|
+
export type { CrumbErrorCode } from 'crumb-alpha-core';
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAA;AAGnC,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAC9C,YAAY,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAGtE,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AAGzD,YAAY,EACV,WAAW,EACX,gBAAgB,EAChB,YAAY,EACZ,WAAW,EACX,gBAAgB,EAChB,kBAAkB,EAClB,QAAQ,EACR,SAAS,GACV,MAAM,kBAAkB,CAAA;AAEzB,OAAO,EACL,UAAU,EACV,aAAa,EACb,aAAa,EACb,eAAe,GAChB,MAAM,kBAAkB,CAAA;AACzB,YAAY,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Agent-side
|
|
2
|
+
export { Crumb } from './client.js';
|
|
3
|
+
// Provider-side
|
|
4
|
+
export { createProvider } from './provider.js';
|
|
5
|
+
// Express middleware (convenience re-export)
|
|
6
|
+
export { crumbMiddleware } from './middleware/express.js';
|
|
7
|
+
export { CrumbError, GatewayClient, CHAIN_CONFIGS, GATEWAY_DOMAINS, } from 'crumb-alpha-core';
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,aAAa;AACb,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAA;AAEnC,gBAAgB;AAChB,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAG9C,6CAA6C;AAC7C,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AAczD,OAAO,EACL,UAAU,EACV,aAAa,EACb,aAAa,EACb,eAAe,GAChB,MAAM,kBAAkB,CAAA"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { MiddlewareConfig } from 'crumb-alpha-core';
|
|
2
|
+
/**
|
|
3
|
+
* Express middleware that gates routes behind x402 payments via Circle Gateway.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```typescript
|
|
7
|
+
* import { crumbMiddleware } from 'crumb-alpha-sdk/middleware/express'
|
|
8
|
+
*
|
|
9
|
+
* const gateway = crumbMiddleware({ sellerAddress: '0x...' })
|
|
10
|
+
*
|
|
11
|
+
* app.get('/resource', gateway.require('$0.01'), (req, res) => {
|
|
12
|
+
* console.log('Payer:', req.payment?.payer)
|
|
13
|
+
* res.json({ data: 'paid content' })
|
|
14
|
+
* })
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export declare function crumbMiddleware(config: MiddlewareConfig): {
|
|
18
|
+
require: (price: string) => (req: import("@circle-fin/x402-batching/server").PaymentRequest, res: import("@circle-fin/x402-batching/server").PaymentResponse, next: (err?: unknown) => void) => void | Promise<void>;
|
|
19
|
+
verify: (payment: unknown) => Promise<{
|
|
20
|
+
valid: boolean;
|
|
21
|
+
payer?: string;
|
|
22
|
+
error?: string;
|
|
23
|
+
}>;
|
|
24
|
+
settle: (payment: unknown) => Promise<{
|
|
25
|
+
success: boolean;
|
|
26
|
+
transaction?: string;
|
|
27
|
+
error?: string;
|
|
28
|
+
}>;
|
|
29
|
+
};
|
|
30
|
+
//# sourceMappingURL=express.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"express.d.ts","sourceRoot":"","sources":["../../src/middleware/express.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAExD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,gBAAgB;qBASnC,MAAM;;;aAkBspO,CAAC;aAAuB,CAAC;;;;mBAA6J,CAAC;aAAuB,CAAC;;EAD/3O"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { createGatewayMiddleware } from '@circle-fin/x402-batching/server';
|
|
2
|
+
/**
|
|
3
|
+
* Express middleware that gates routes behind x402 payments via Circle Gateway.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```typescript
|
|
7
|
+
* import { crumbMiddleware } from 'crumb-alpha-sdk/middleware/express'
|
|
8
|
+
*
|
|
9
|
+
* const gateway = crumbMiddleware({ sellerAddress: '0x...' })
|
|
10
|
+
*
|
|
11
|
+
* app.get('/resource', gateway.require('$0.01'), (req, res) => {
|
|
12
|
+
* console.log('Payer:', req.payment?.payer)
|
|
13
|
+
* res.json({ data: 'paid content' })
|
|
14
|
+
* })
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export function crumbMiddleware(config) {
|
|
18
|
+
const gateway = createGatewayMiddleware({
|
|
19
|
+
sellerAddress: config.sellerAddress,
|
|
20
|
+
networks: config.networks,
|
|
21
|
+
facilitatorUrl: config.facilitatorUrl,
|
|
22
|
+
description: config.description,
|
|
23
|
+
});
|
|
24
|
+
return {
|
|
25
|
+
require: (price) => {
|
|
26
|
+
const mw = gateway.require(price);
|
|
27
|
+
if (config.onPayment) {
|
|
28
|
+
return async (req, res, next) => {
|
|
29
|
+
await mw(req, res, () => {
|
|
30
|
+
if (req.payment) {
|
|
31
|
+
config.onPayment(req.payment);
|
|
32
|
+
}
|
|
33
|
+
next();
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return mw;
|
|
38
|
+
},
|
|
39
|
+
verify: gateway.verify,
|
|
40
|
+
settle: gateway.settle,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=express.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"express.js","sourceRoot":"","sources":["../../src/middleware/express.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,kCAAkC,CAAA;AAG1E;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,eAAe,CAAC,MAAwB;IACtD,MAAM,OAAO,GAAG,uBAAuB,CAAC;QACtC,aAAa,EAAE,MAAM,CAAC,aAAa;QACnC,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,cAAc,EAAE,MAAM,CAAC,cAAc;QACrC,WAAW,EAAE,MAAM,CAAC,WAAW;KAChC,CAAC,CAAA;IAEF,OAAO;QACL,OAAO,EAAE,CAAC,KAAa,EAAE,EAAE;YACzB,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;YACjC,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;gBACrB,OAAO,KAAK,EAAE,GAAQ,EAAE,GAAQ,EAAE,IAAS,EAAE,EAAE;oBAC7C,MAAM,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE;wBACtB,IAAI,GAAG,CAAC,OAAO,EAAE,CAAC;4BAChB,MAAM,CAAC,SAAU,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;wBAChC,CAAC;wBACD,IAAI,EAAE,CAAA;oBACR,CAAC,CAAC,CAAA;gBACJ,CAAC,CAAA;YACH,CAAC;YACD,OAAO,EAAE,CAAA;QACX,CAAC;QACD,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,MAAM,EAAE,OAAO,CAAC,MAAM;KACvB,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { MiddlewareConfig } from 'crumb-alpha-core';
|
|
2
|
+
import type { FastifyPluginAsync } from 'fastify';
|
|
3
|
+
declare module 'fastify' {
|
|
4
|
+
interface FastifyRequest {
|
|
5
|
+
crumbPayment?: import('crumb-alpha-core').SettlementResult;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export declare const crumbPlugin: FastifyPluginAsync<MiddlewareConfig>;
|
|
9
|
+
//# sourceMappingURL=fastify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fastify.d.ts","sourceRoot":"","sources":["../../src/middleware/fastify.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAKnD,OAAO,KAAK,EAAE,kBAAkB,EAAgC,MAAM,SAAS,CAAA;AAE/E,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,cAAc;QACtB,YAAY,CAAC,EAAE,OAAO,aAAa,EAAE,gBAAgB,CAAA;KACtD;CACF;AAcD,eAAO,MAAM,WAAW,EAAE,kBAAkB,CAAC,gBAAgB,CAyC5D,CAAA"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { verifyAndSettle, CrumbError, BASE_SEPOLIA_NETWORK, BASE_SEPOLIA_USDC, } from '@crumb/core';
|
|
2
|
+
import { decodePaymentSignatureHeader, encodePaymentRequiredHeader, } from '@x402/core/http';
|
|
3
|
+
function buildRequirement(config) {
|
|
4
|
+
return {
|
|
5
|
+
scheme: 'exact',
|
|
6
|
+
network: config.network ?? BASE_SEPOLIA_NETWORK,
|
|
7
|
+
amount: config.price,
|
|
8
|
+
asset: config.asset ?? BASE_SEPOLIA_USDC,
|
|
9
|
+
payTo: config.address,
|
|
10
|
+
maxTimeoutSeconds: config.maxTimeoutSeconds ?? 300,
|
|
11
|
+
extra: {},
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export const crumbPlugin = async (fastify, config) => {
|
|
15
|
+
const requirement = buildRequirement(config);
|
|
16
|
+
const paymentRequired = {
|
|
17
|
+
x402Version: 2,
|
|
18
|
+
accepts: [requirement],
|
|
19
|
+
resource: {},
|
|
20
|
+
};
|
|
21
|
+
fastify.addHook('preHandler', async (request, reply) => {
|
|
22
|
+
const paymentHeader = request.headers['x-payment'];
|
|
23
|
+
if (!paymentHeader) {
|
|
24
|
+
reply.header('X-Payment-Required', encodePaymentRequiredHeader(paymentRequired));
|
|
25
|
+
return reply.status(402).send(paymentRequired);
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const paymentPayload = decodePaymentSignatureHeader(paymentHeader);
|
|
29
|
+
const result = await verifyAndSettle(paymentPayload, requirement, config.facilitatorUrl);
|
|
30
|
+
request.crumbPayment = result;
|
|
31
|
+
config.onPayment?.(result);
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
if (err instanceof CrumbError) {
|
|
35
|
+
return reply.status(402).send({
|
|
36
|
+
error: err.code,
|
|
37
|
+
detail: err.detail,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
throw err;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
//# sourceMappingURL=fastify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fastify.js","sourceRoot":"","sources":["../../src/middleware/fastify.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EACf,UAAU,EACV,oBAAoB,EACpB,iBAAiB,GAClB,MAAM,aAAa,CAAA;AAEpB,OAAO,EACL,4BAA4B,EAC5B,2BAA2B,GAC5B,MAAM,iBAAiB,CAAA;AASxB,SAAS,gBAAgB,CAAC,MAAwB;IAChD,OAAO;QACL,MAAM,EAAE,OAAO;QACf,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,oBAAoB;QAC/C,MAAM,EAAE,MAAM,CAAC,KAAK;QACpB,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,iBAAiB;QACxC,KAAK,EAAE,MAAM,CAAC,OAAO;QACrB,iBAAiB,EAAE,MAAM,CAAC,iBAAiB,IAAI,GAAG;QAClD,KAAK,EAAE,EAAE;KACV,CAAA;AACH,CAAC;AAED,MAAM,CAAC,MAAM,WAAW,GAAyC,KAAK,EACpE,OAAO,EACP,MAAM,EACN,EAAE;IACF,MAAM,WAAW,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAA;IAC5C,MAAM,eAAe,GAAG;QACtB,WAAW,EAAE,CAAC;QACd,OAAO,EAAE,CAAC,WAAW,CAAC;QACtB,QAAQ,EAAE,EAAE;KACb,CAAA;IAED,OAAO,CAAC,OAAO,CAAC,YAAY,EAAE,KAAK,EAAE,OAAuB,EAAE,KAAmB,EAAE,EAAE;QACnF,MAAM,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC,WAAW,CAAuB,CAAA;QAExE,IAAI,CAAC,aAAa,EAAE,CAAC;YACnB,KAAK,CAAC,MAAM,CACV,oBAAoB,EACpB,2BAA2B,CAAC,eAAsB,CAAC,CACpD,CAAA;YACD,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,CAAA;QAChD,CAAC;QAED,IAAI,CAAC;YACH,MAAM,cAAc,GAAG,4BAA4B,CAAC,aAAa,CAAC,CAAA;YAClE,MAAM,MAAM,GAAG,MAAM,eAAe,CAClC,cAAc,EACd,WAAkB,EAClB,MAAM,CAAC,cAAc,CACtB,CAAA;YACD,OAAO,CAAC,YAAY,GAAG,MAAM,CAAA;YAC7B,MAAM,CAAC,SAAS,EAAE,CAAC,MAAM,CAAC,CAAA;QAC5B,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,GAAG,YAAY,UAAU,EAAE,CAAC;gBAC9B,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;oBAC5B,KAAK,EAAE,GAAG,CAAC,IAAI;oBACf,MAAM,EAAE,GAAG,CAAC,MAAM;iBACnB,CAAC,CAAA;YACJ,CAAC;YACD,MAAM,GAAG,CAAA;QACX,CAAC;IACH,CAAC,CAAC,CAAA;AACJ,CAAC,CAAA"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { MiddlewareConfig, SettlementResult } from 'crumb-alpha-core';
|
|
2
|
+
export interface CrumbFetchHandlerResult {
|
|
3
|
+
paid: boolean;
|
|
4
|
+
payment?: SettlementResult;
|
|
5
|
+
}
|
|
6
|
+
export declare function createFetchHandler(config: MiddlewareConfig): (request: Request) => Promise<Response | CrumbFetchHandlerResult>;
|
|
7
|
+
//# sourceMappingURL=fetch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../src/middleware/fetch.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAMrE,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,OAAO,CAAA;IACb,OAAO,CAAC,EAAE,gBAAgB,CAAA;CAC3B;AAcD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,gBAAgB,IAQ3C,SAAS,OAAO,KAAG,OAAO,CAAC,QAAQ,GAAG,uBAAuB,CAAC,CAsC7E"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { verifyAndSettle, CrumbError, BASE_SEPOLIA_NETWORK, BASE_SEPOLIA_USDC, } from '@crumb/core';
|
|
2
|
+
import { decodePaymentSignatureHeader, encodePaymentRequiredHeader, } from '@x402/core/http';
|
|
3
|
+
function buildRequirement(config) {
|
|
4
|
+
return {
|
|
5
|
+
scheme: 'exact',
|
|
6
|
+
network: config.network ?? BASE_SEPOLIA_NETWORK,
|
|
7
|
+
amount: config.price,
|
|
8
|
+
asset: config.asset ?? BASE_SEPOLIA_USDC,
|
|
9
|
+
payTo: config.address,
|
|
10
|
+
maxTimeoutSeconds: config.maxTimeoutSeconds ?? 300,
|
|
11
|
+
extra: {},
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export function createFetchHandler(config) {
|
|
15
|
+
const requirement = buildRequirement(config);
|
|
16
|
+
const paymentRequired = {
|
|
17
|
+
x402Version: 2,
|
|
18
|
+
accepts: [requirement],
|
|
19
|
+
resource: {},
|
|
20
|
+
};
|
|
21
|
+
return async (request) => {
|
|
22
|
+
const paymentHeader = request.headers.get('x-payment');
|
|
23
|
+
if (!paymentHeader) {
|
|
24
|
+
return new Response(JSON.stringify(paymentRequired), {
|
|
25
|
+
status: 402,
|
|
26
|
+
headers: {
|
|
27
|
+
'Content-Type': 'application/json',
|
|
28
|
+
'X-Payment-Required': encodePaymentRequiredHeader(paymentRequired),
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const paymentPayload = decodePaymentSignatureHeader(paymentHeader);
|
|
34
|
+
const result = await verifyAndSettle(paymentPayload, requirement, config.facilitatorUrl);
|
|
35
|
+
config.onPayment?.(result);
|
|
36
|
+
return { paid: true, payment: result };
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
if (err instanceof CrumbError) {
|
|
40
|
+
return new Response(JSON.stringify({ error: err.code, detail: err.detail }), {
|
|
41
|
+
status: 402,
|
|
42
|
+
headers: { 'Content-Type': 'application/json' },
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
throw err;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=fetch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch.js","sourceRoot":"","sources":["../../src/middleware/fetch.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EACf,UAAU,EACV,oBAAoB,EACpB,iBAAiB,GAClB,MAAM,aAAa,CAAA;AAEpB,OAAO,EACL,4BAA4B,EAC5B,2BAA2B,GAC5B,MAAM,iBAAiB,CAAA;AAOxB,SAAS,gBAAgB,CAAC,MAAwB;IAChD,OAAO;QACL,MAAM,EAAE,OAAO;QACf,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,oBAAoB;QAC/C,MAAM,EAAE,MAAM,CAAC,KAAK;QACpB,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,iBAAiB;QACxC,KAAK,EAAE,MAAM,CAAC,OAAO;QACrB,iBAAiB,EAAE,MAAM,CAAC,iBAAiB,IAAI,GAAG;QAClD,KAAK,EAAE,EAAE;KACV,CAAA;AACH,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,MAAwB;IACzD,MAAM,WAAW,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAA;IAC5C,MAAM,eAAe,GAAG;QACtB,WAAW,EAAE,CAAC;QACd,OAAO,EAAE,CAAC,WAAW,CAAC;QACtB,QAAQ,EAAE,EAAE;KACb,CAAA;IAED,OAAO,KAAK,EAAE,OAAgB,EAA+C,EAAE;QAC7E,MAAM,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,CAAA;QAEtD,IAAI,CAAC,aAAa,EAAE,CAAC;YACnB,OAAO,IAAI,QAAQ,CACjB,IAAI,CAAC,SAAS,CAAC,eAAe,CAAC,EAC/B;gBACE,MAAM,EAAE,GAAG;gBACX,OAAO,EAAE;oBACP,cAAc,EAAE,kBAAkB;oBAClC,oBAAoB,EAAE,2BAA2B,CAAC,eAAsB,CAAC;iBAC1E;aACF,CACF,CAAA;QACH,CAAC;QAED,IAAI,CAAC;YACH,MAAM,cAAc,GAAG,4BAA4B,CAAC,aAAa,CAAC,CAAA;YAClE,MAAM,MAAM,GAAG,MAAM,eAAe,CAClC,cAAc,EACd,WAAkB,EAClB,MAAM,CAAC,cAAc,CACtB,CAAA;YACD,MAAM,CAAC,SAAS,EAAE,CAAC,MAAM,CAAC,CAAA;YAC1B,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,CAAA;QACxC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,GAAG,YAAY,UAAU,EAAE,CAAC;gBAC9B,OAAO,IAAI,QAAQ,CACjB,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,EACvD;oBACE,MAAM,EAAE,GAAG;oBACX,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;iBAChD,CACF,CAAA;YACH,CAAC;YACD,MAAM,GAAG,CAAA;QACX,CAAC;IACH,CAAC,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { MiddlewareConfig } from 'crumb-alpha-core';
|
|
2
|
+
export type { GatewayMiddleware, PaymentRequest } from '@circle-fin/x402-batching/server';
|
|
3
|
+
/**
|
|
4
|
+
* Create a CrumbProvider — wraps Circle's createGatewayMiddleware.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* const gateway = createProvider({
|
|
9
|
+
* sellerAddress: '0x...',
|
|
10
|
+
* })
|
|
11
|
+
*
|
|
12
|
+
* app.get('/resource', gateway.require('$0.01'), (req, res) => {
|
|
13
|
+
* res.json({ data: 'paid content' })
|
|
14
|
+
* })
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export declare function createProvider(config: MiddlewareConfig): {
|
|
18
|
+
/** Require payment — returns Express middleware */
|
|
19
|
+
require: (price: string) => (req: import("@circle-fin/x402-batching/server").PaymentRequest, res: import("@circle-fin/x402-batching/server").PaymentResponse, next: (err?: unknown) => void) => void | Promise<void>;
|
|
20
|
+
/** Verify a payment without settling */
|
|
21
|
+
verify: (payment: unknown) => Promise<{
|
|
22
|
+
valid: boolean;
|
|
23
|
+
payer?: string;
|
|
24
|
+
error?: string;
|
|
25
|
+
}>;
|
|
26
|
+
/** Settle a verified payment */
|
|
27
|
+
settle: (payment: unknown) => Promise<{
|
|
28
|
+
success: boolean;
|
|
29
|
+
transaction?: string;
|
|
30
|
+
error?: string;
|
|
31
|
+
}>;
|
|
32
|
+
};
|
|
33
|
+
//# sourceMappingURL=provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAExD,YAAY,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAA;AAEzF;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,gBAAgB;IASnD,mDAAmD;qBAClC,MAAM;IAevB,wCAAwC;;;aAM++N,CAAC;aAAuB,CAAC;;IAJhjO,gCAAgC;;;mBAI6qO,CAAC;aAAuB,CAAC;;EADzuO"}
|
package/dist/provider.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createGatewayMiddleware } from '@circle-fin/x402-batching/server';
|
|
2
|
+
/**
|
|
3
|
+
* Create a CrumbProvider — wraps Circle's createGatewayMiddleware.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```typescript
|
|
7
|
+
* const gateway = createProvider({
|
|
8
|
+
* sellerAddress: '0x...',
|
|
9
|
+
* })
|
|
10
|
+
*
|
|
11
|
+
* app.get('/resource', gateway.require('$0.01'), (req, res) => {
|
|
12
|
+
* res.json({ data: 'paid content' })
|
|
13
|
+
* })
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export function createProvider(config) {
|
|
17
|
+
const middleware = createGatewayMiddleware({
|
|
18
|
+
sellerAddress: config.sellerAddress,
|
|
19
|
+
networks: config.networks,
|
|
20
|
+
facilitatorUrl: config.facilitatorUrl,
|
|
21
|
+
description: config.description,
|
|
22
|
+
});
|
|
23
|
+
return {
|
|
24
|
+
/** Require payment — returns Express middleware */
|
|
25
|
+
require: (price) => {
|
|
26
|
+
const mw = middleware.require(price);
|
|
27
|
+
// Wrap to call onPayment hook
|
|
28
|
+
if (config.onPayment) {
|
|
29
|
+
return async (req, res, next) => {
|
|
30
|
+
await mw(req, res, () => {
|
|
31
|
+
if (req.payment) {
|
|
32
|
+
config.onPayment(req.payment);
|
|
33
|
+
}
|
|
34
|
+
next();
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return mw;
|
|
39
|
+
},
|
|
40
|
+
/** Verify a payment without settling */
|
|
41
|
+
verify: middleware.verify,
|
|
42
|
+
/** Settle a verified payment */
|
|
43
|
+
settle: middleware.settle,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.js","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,kCAAkC,CAAA;AAK1E;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,cAAc,CAAC,MAAwB;IACrD,MAAM,UAAU,GAAG,uBAAuB,CAAC;QACzC,aAAa,EAAE,MAAM,CAAC,aAAa;QACnC,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,cAAc,EAAE,MAAM,CAAC,cAAc;QACrC,WAAW,EAAE,MAAM,CAAC,WAAW;KAChC,CAAC,CAAA;IAEF,OAAO;QACL,mDAAmD;QACnD,OAAO,EAAE,CAAC,KAAa,EAAE,EAAE;YACzB,MAAM,EAAE,GAAG,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;YACpC,8BAA8B;YAC9B,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;gBACrB,OAAO,KAAK,EAAE,GAAQ,EAAE,GAAQ,EAAE,IAAS,EAAE,EAAE;oBAC7C,MAAM,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE;wBACtB,IAAI,GAAG,CAAC,OAAO,EAAE,CAAC;4BAChB,MAAM,CAAC,SAAU,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;wBAChC,CAAC;wBACD,IAAI,EAAE,CAAA;oBACR,CAAC,CAAC,CAAA;gBACJ,CAAC,CAAA;YACH,CAAC;YACD,OAAO,EAAE,CAAA;QACX,CAAC;QACD,wCAAwC;QACxC,MAAM,EAAE,UAAU,CAAC,MAAM;QACzB,gCAAgC;QAChC,MAAM,EAAE,UAAU,CAAC,MAAM;KAC1B,CAAA;AACH,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "crumb-alpha-sdk",
|
|
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
|
+
"./middleware/express": {
|
|
14
|
+
"types": "./dist/middleware/express.d.ts",
|
|
15
|
+
"import": "./dist/middleware/express.js",
|
|
16
|
+
"default": "./dist/middleware/express.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@circle-fin/x402-batching": "^2.0.4",
|
|
24
|
+
"crumb-alpha-core": "0.1.0"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"express": ">=4.0.0"
|
|
28
|
+
},
|
|
29
|
+
"peerDependenciesMeta": {
|
|
30
|
+
"express": {
|
|
31
|
+
"optional": true
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/express": "^5.0.0"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsc",
|
|
39
|
+
"dev": "tsc --watch",
|
|
40
|
+
"clean": "rm -rf dist",
|
|
41
|
+
"test": "vitest run"
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import {
|
|
2
|
+
GatewayClient,
|
|
3
|
+
getBalances,
|
|
4
|
+
checkPriceCeiling,
|
|
5
|
+
deriveAddress,
|
|
6
|
+
CrumbError,
|
|
7
|
+
} from 'crumb-alpha-core'
|
|
8
|
+
import type {
|
|
9
|
+
CrumbWallet,
|
|
10
|
+
FetchOptions,
|
|
11
|
+
FetchResult,
|
|
12
|
+
SupportedChainName,
|
|
13
|
+
Balances,
|
|
14
|
+
} from 'crumb-alpha-core'
|
|
15
|
+
|
|
16
|
+
export class Crumb {
|
|
17
|
+
private wallet: CrumbWallet
|
|
18
|
+
private gateway: GatewayClient
|
|
19
|
+
private chain: SupportedChainName
|
|
20
|
+
private lowBalanceHook?: (balance: string) => void
|
|
21
|
+
private lowBalanceThreshold: number = 1.0
|
|
22
|
+
|
|
23
|
+
constructor(config: {
|
|
24
|
+
privateKey: string
|
|
25
|
+
chain?: SupportedChainName
|
|
26
|
+
rpcUrl?: string
|
|
27
|
+
lowBalanceThreshold?: number
|
|
28
|
+
onLowBalance?: (balance: string) => void
|
|
29
|
+
}) {
|
|
30
|
+
this.chain = config.chain ?? 'arcTestnet'
|
|
31
|
+
this.wallet = {
|
|
32
|
+
address: deriveAddress(config.privateKey),
|
|
33
|
+
privateKey: config.privateKey,
|
|
34
|
+
}
|
|
35
|
+
this.gateway = new GatewayClient({
|
|
36
|
+
chain: this.chain,
|
|
37
|
+
privateKey: config.privateKey as `0x${string}`,
|
|
38
|
+
rpcUrl: config.rpcUrl,
|
|
39
|
+
})
|
|
40
|
+
if (config.lowBalanceThreshold !== undefined) {
|
|
41
|
+
this.lowBalanceThreshold = config.lowBalanceThreshold
|
|
42
|
+
}
|
|
43
|
+
if (config.onLowBalance) {
|
|
44
|
+
this.lowBalanceHook = config.onLowBalance
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get address(): string {
|
|
49
|
+
return this.wallet.address
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Pay for an x402-protected resource. Handles the full 402 flow automatically.
|
|
54
|
+
*/
|
|
55
|
+
async fetch<T = unknown>(url: string, options: FetchOptions = {}): Promise<FetchResult<T>> {
|
|
56
|
+
if (options.maxPayment !== undefined) {
|
|
57
|
+
// Pre-check: probe the URL to see the price before paying
|
|
58
|
+
const supported = await this.gateway.supports(url)
|
|
59
|
+
if (supported.supported && supported.requirements) {
|
|
60
|
+
const amount = (supported.requirements as any)?.amount
|
|
61
|
+
if (amount) {
|
|
62
|
+
checkPriceCeiling(amount, options.maxPayment)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const result = await this.gateway.pay<T>(url, {
|
|
68
|
+
method: options.method,
|
|
69
|
+
body: options.body,
|
|
70
|
+
headers: options.headers,
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
// Check low balance after payment
|
|
74
|
+
this.checkLowBalance()
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
data: result.data,
|
|
78
|
+
amount: result.amount,
|
|
79
|
+
formattedAmount: result.formattedAmount,
|
|
80
|
+
txHash: result.transaction,
|
|
81
|
+
status: result.status,
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Deposit USDC into Gateway for gasless payments.
|
|
87
|
+
*/
|
|
88
|
+
async deposit(amount: string) {
|
|
89
|
+
return this.gateway.deposit(amount)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Withdraw USDC from Gateway.
|
|
94
|
+
*/
|
|
95
|
+
async withdraw(amount: string, options?: {
|
|
96
|
+
chain?: SupportedChainName
|
|
97
|
+
recipient?: string
|
|
98
|
+
}) {
|
|
99
|
+
return this.gateway.withdraw(amount, {
|
|
100
|
+
chain: options?.chain,
|
|
101
|
+
recipient: options?.recipient as `0x${string}`,
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get all balances (wallet + gateway).
|
|
107
|
+
*/
|
|
108
|
+
async balances(): Promise<Balances> {
|
|
109
|
+
const result = await this.gateway.getBalances()
|
|
110
|
+
|
|
111
|
+
const available = result.gateway.formattedAvailable
|
|
112
|
+
if (parseFloat(available) < this.lowBalanceThreshold) {
|
|
113
|
+
this.lowBalanceHook?.(available)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return result
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get gateway available balance as a formatted string.
|
|
121
|
+
*/
|
|
122
|
+
async balance(): Promise<string> {
|
|
123
|
+
const result = await this.gateway.getBalances()
|
|
124
|
+
const available = result.gateway.formattedAvailable
|
|
125
|
+
|
|
126
|
+
if (parseFloat(available) < this.lowBalanceThreshold) {
|
|
127
|
+
this.lowBalanceHook?.(available)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return available
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Check if a URL supports Gateway batching.
|
|
135
|
+
*/
|
|
136
|
+
async supports(url: string) {
|
|
137
|
+
return this.gateway.supports(url)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
onLowBalance(threshold: number, fn: (balance: string) => void) {
|
|
141
|
+
this.lowBalanceThreshold = threshold
|
|
142
|
+
this.lowBalanceHook = fn
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Access the underlying GatewayClient for advanced operations. */
|
|
146
|
+
get client(): GatewayClient {
|
|
147
|
+
return this.gateway
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private async checkLowBalance() {
|
|
151
|
+
try {
|
|
152
|
+
const result = await this.gateway.getBalances()
|
|
153
|
+
const available = parseFloat(result.gateway.formattedAvailable)
|
|
154
|
+
if (available < this.lowBalanceThreshold) {
|
|
155
|
+
this.lowBalanceHook?.(result.gateway.formattedAvailable)
|
|
156
|
+
}
|
|
157
|
+
} catch {
|
|
158
|
+
// Don't fail the payment if balance check fails
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Agent-side
|
|
2
|
+
export { Crumb } from './client.js'
|
|
3
|
+
|
|
4
|
+
// Provider-side
|
|
5
|
+
export { createProvider } from './provider.js'
|
|
6
|
+
export type { GatewayMiddleware, PaymentRequest } from './provider.js'
|
|
7
|
+
|
|
8
|
+
// Express middleware (convenience re-export)
|
|
9
|
+
export { crumbMiddleware } from './middleware/express.js'
|
|
10
|
+
|
|
11
|
+
// Re-export core types for convenience
|
|
12
|
+
export type {
|
|
13
|
+
CrumbWallet,
|
|
14
|
+
SettlementResult,
|
|
15
|
+
FetchOptions,
|
|
16
|
+
FetchResult,
|
|
17
|
+
MiddlewareConfig,
|
|
18
|
+
SupportedChainName,
|
|
19
|
+
Balances,
|
|
20
|
+
PayResult,
|
|
21
|
+
} from 'crumb-alpha-core'
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
CrumbError,
|
|
25
|
+
GatewayClient,
|
|
26
|
+
CHAIN_CONFIGS,
|
|
27
|
+
GATEWAY_DOMAINS,
|
|
28
|
+
} from 'crumb-alpha-core'
|
|
29
|
+
export type { CrumbErrorCode } from 'crumb-alpha-core'
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createGatewayMiddleware } from '@circle-fin/x402-batching/server'
|
|
2
|
+
import type { MiddlewareConfig } from 'crumb-alpha-core'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Express middleware that gates routes behind x402 payments via Circle Gateway.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { crumbMiddleware } from 'crumb-alpha-sdk/middleware/express'
|
|
10
|
+
*
|
|
11
|
+
* const gateway = crumbMiddleware({ sellerAddress: '0x...' })
|
|
12
|
+
*
|
|
13
|
+
* app.get('/resource', gateway.require('$0.01'), (req, res) => {
|
|
14
|
+
* console.log('Payer:', req.payment?.payer)
|
|
15
|
+
* res.json({ data: 'paid content' })
|
|
16
|
+
* })
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export function crumbMiddleware(config: MiddlewareConfig) {
|
|
20
|
+
const gateway = createGatewayMiddleware({
|
|
21
|
+
sellerAddress: config.sellerAddress,
|
|
22
|
+
networks: config.networks,
|
|
23
|
+
facilitatorUrl: config.facilitatorUrl,
|
|
24
|
+
description: config.description,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
require: (price: string) => {
|
|
29
|
+
const mw = gateway.require(price)
|
|
30
|
+
if (config.onPayment) {
|
|
31
|
+
return async (req: any, res: any, next: any) => {
|
|
32
|
+
await mw(req, res, () => {
|
|
33
|
+
if (req.payment) {
|
|
34
|
+
config.onPayment!(req.payment)
|
|
35
|
+
}
|
|
36
|
+
next()
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return mw
|
|
41
|
+
},
|
|
42
|
+
verify: gateway.verify,
|
|
43
|
+
settle: gateway.settle,
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/provider.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createGatewayMiddleware } from '@circle-fin/x402-batching/server'
|
|
2
|
+
import type { MiddlewareConfig } from 'crumb-alpha-core'
|
|
3
|
+
|
|
4
|
+
export type { GatewayMiddleware, PaymentRequest } from '@circle-fin/x402-batching/server'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Create a CrumbProvider — wraps Circle's createGatewayMiddleware.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* const gateway = createProvider({
|
|
12
|
+
* sellerAddress: '0x...',
|
|
13
|
+
* })
|
|
14
|
+
*
|
|
15
|
+
* app.get('/resource', gateway.require('$0.01'), (req, res) => {
|
|
16
|
+
* res.json({ data: 'paid content' })
|
|
17
|
+
* })
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export function createProvider(config: MiddlewareConfig) {
|
|
21
|
+
const middleware = createGatewayMiddleware({
|
|
22
|
+
sellerAddress: config.sellerAddress,
|
|
23
|
+
networks: config.networks,
|
|
24
|
+
facilitatorUrl: config.facilitatorUrl,
|
|
25
|
+
description: config.description,
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
/** Require payment — returns Express middleware */
|
|
30
|
+
require: (price: string) => {
|
|
31
|
+
const mw = middleware.require(price)
|
|
32
|
+
// Wrap to call onPayment hook
|
|
33
|
+
if (config.onPayment) {
|
|
34
|
+
return async (req: any, res: any, next: any) => {
|
|
35
|
+
await mw(req, res, () => {
|
|
36
|
+
if (req.payment) {
|
|
37
|
+
config.onPayment!(req.payment)
|
|
38
|
+
}
|
|
39
|
+
next()
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return mw
|
|
44
|
+
},
|
|
45
|
+
/** Verify a payment without settling */
|
|
46
|
+
verify: middleware.verify,
|
|
47
|
+
/** Settle a verified payment */
|
|
48
|
+
settle: middleware.settle,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
+
|
|
3
|
+
const mockPay = vi.fn()
|
|
4
|
+
const mockDeposit = vi.fn()
|
|
5
|
+
const mockWithdraw = vi.fn()
|
|
6
|
+
const mockGetBalances = vi.fn()
|
|
7
|
+
const mockSupports = vi.fn()
|
|
8
|
+
|
|
9
|
+
class MockGatewayClient {
|
|
10
|
+
pay = mockPay
|
|
11
|
+
deposit = mockDeposit
|
|
12
|
+
withdraw = mockWithdraw
|
|
13
|
+
getBalances = mockGetBalances
|
|
14
|
+
supports = mockSupports
|
|
15
|
+
constructor(_config: any) {}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
vi.mock('@circle-fin/x402-batching/client', () => ({
|
|
19
|
+
GatewayClient: MockGatewayClient,
|
|
20
|
+
}))
|
|
21
|
+
|
|
22
|
+
vi.mock('crumb-alpha-core', async () => {
|
|
23
|
+
const actual = await vi.importActual<any>('crumb-alpha-core')
|
|
24
|
+
return {
|
|
25
|
+
...actual,
|
|
26
|
+
GatewayClient: MockGatewayClient,
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const { Crumb } = await import('../src/client.js')
|
|
31
|
+
|
|
32
|
+
const TEST_PRIVATE_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'
|
|
33
|
+
|
|
34
|
+
describe('Crumb', () => {
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
vi.clearAllMocks()
|
|
37
|
+
mockGetBalances.mockResolvedValue({
|
|
38
|
+
wallet: { formatted: '100.00' },
|
|
39
|
+
gateway: { formattedAvailable: '50.00', available: 50000000n },
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
describe('constructor', () => {
|
|
44
|
+
it('derives address from private key', () => {
|
|
45
|
+
const crumb = new Crumb({ privateKey: TEST_PRIVATE_KEY })
|
|
46
|
+
expect(crumb.address).toBe('0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('defaults chain to arcTestnet', () => {
|
|
50
|
+
const crumb = new Crumb({ privateKey: TEST_PRIVATE_KEY })
|
|
51
|
+
expect(crumb).toBeDefined()
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('exposes the underlying GatewayClient', () => {
|
|
55
|
+
const crumb = new Crumb({ privateKey: TEST_PRIVATE_KEY })
|
|
56
|
+
expect(crumb.client).toBeDefined()
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe('fetch', () => {
|
|
61
|
+
it('calls gateway.pay with url and options', async () => {
|
|
62
|
+
mockPay.mockResolvedValue({
|
|
63
|
+
data: { results: [] },
|
|
64
|
+
amount: 1000n,
|
|
65
|
+
formattedAmount: '0.001',
|
|
66
|
+
transaction: '0xtx123',
|
|
67
|
+
status: 200,
|
|
68
|
+
})
|
|
69
|
+
mockSupports.mockResolvedValue({ supported: false })
|
|
70
|
+
|
|
71
|
+
const crumb = new Crumb({ privateKey: TEST_PRIVATE_KEY })
|
|
72
|
+
const result = await crumb.fetch('http://localhost:3001/search', {
|
|
73
|
+
method: 'POST',
|
|
74
|
+
body: { query: 'test' },
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
expect(mockPay).toHaveBeenCalledWith('http://localhost:3001/search', {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
body: { query: 'test' },
|
|
80
|
+
headers: undefined,
|
|
81
|
+
})
|
|
82
|
+
expect(result.data).toEqual({ results: [] })
|
|
83
|
+
expect(result.formattedAmount).toBe('0.001')
|
|
84
|
+
expect(result.txHash).toBe('0xtx123')
|
|
85
|
+
expect(result.status).toBe(200)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('checks price ceiling when maxPayment is set', async () => {
|
|
89
|
+
mockSupports.mockResolvedValue({
|
|
90
|
+
supported: true,
|
|
91
|
+
requirements: { amount: '$0.05' },
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const crumb = new Crumb({ privateKey: TEST_PRIVATE_KEY })
|
|
95
|
+
|
|
96
|
+
await expect(
|
|
97
|
+
crumb.fetch('http://localhost:3001/search', { maxPayment: 0.01 }),
|
|
98
|
+
).rejects.toThrow()
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('proceeds when price is within maxPayment', async () => {
|
|
102
|
+
mockSupports.mockResolvedValue({
|
|
103
|
+
supported: true,
|
|
104
|
+
requirements: { amount: '$0.001' },
|
|
105
|
+
})
|
|
106
|
+
mockPay.mockResolvedValue({
|
|
107
|
+
data: {},
|
|
108
|
+
amount: 1000n,
|
|
109
|
+
formattedAmount: '0.001',
|
|
110
|
+
transaction: '0xtx',
|
|
111
|
+
status: 200,
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
const crumb = new Crumb({ privateKey: TEST_PRIVATE_KEY })
|
|
115
|
+
const result = await crumb.fetch('http://localhost:3001/search', { maxPayment: 0.01 })
|
|
116
|
+
expect(result.formattedAmount).toBe('0.001')
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
describe('deposit', () => {
|
|
121
|
+
it('delegates to gateway.deposit', async () => {
|
|
122
|
+
mockDeposit.mockResolvedValue({ success: true })
|
|
123
|
+
|
|
124
|
+
const crumb = new Crumb({ privateKey: TEST_PRIVATE_KEY })
|
|
125
|
+
await crumb.deposit('10.0')
|
|
126
|
+
|
|
127
|
+
expect(mockDeposit).toHaveBeenCalledWith('10.0')
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
describe('withdraw', () => {
|
|
132
|
+
it('delegates to gateway.withdraw', async () => {
|
|
133
|
+
mockWithdraw.mockResolvedValue({ success: true })
|
|
134
|
+
|
|
135
|
+
const crumb = new Crumb({ privateKey: TEST_PRIVATE_KEY })
|
|
136
|
+
await crumb.withdraw('5.0')
|
|
137
|
+
|
|
138
|
+
expect(mockWithdraw).toHaveBeenCalledWith('5.0', {
|
|
139
|
+
chain: undefined,
|
|
140
|
+
recipient: undefined,
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('passes chain and recipient options', async () => {
|
|
145
|
+
mockWithdraw.mockResolvedValue({ success: true })
|
|
146
|
+
|
|
147
|
+
const crumb = new Crumb({ privateKey: TEST_PRIVATE_KEY })
|
|
148
|
+
await crumb.withdraw('5.0', { chain: 'arcTestnet', recipient: '0xrecipient' })
|
|
149
|
+
|
|
150
|
+
expect(mockWithdraw).toHaveBeenCalledWith('5.0', {
|
|
151
|
+
chain: 'arcTestnet',
|
|
152
|
+
recipient: '0xrecipient',
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
describe('balance / balances', () => {
|
|
158
|
+
it('returns formatted gateway balance', async () => {
|
|
159
|
+
mockGetBalances.mockResolvedValue({
|
|
160
|
+
wallet: { formatted: '100.00' },
|
|
161
|
+
gateway: { formattedAvailable: '42.50', available: 42500000n },
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
const crumb = new Crumb({ privateKey: TEST_PRIVATE_KEY })
|
|
165
|
+
const bal = await crumb.balance()
|
|
166
|
+
expect(bal).toBe('42.50')
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('returns full balances object', async () => {
|
|
170
|
+
const mockResult = {
|
|
171
|
+
wallet: { formatted: '100.00' },
|
|
172
|
+
gateway: { formattedAvailable: '42.50', available: 42500000n },
|
|
173
|
+
}
|
|
174
|
+
mockGetBalances.mockResolvedValue(mockResult)
|
|
175
|
+
|
|
176
|
+
const crumb = new Crumb({ privateKey: TEST_PRIVATE_KEY })
|
|
177
|
+
const result = await crumb.balances()
|
|
178
|
+
expect(result).toEqual(mockResult)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('triggers low balance hook when below threshold', async () => {
|
|
182
|
+
mockGetBalances.mockResolvedValue({
|
|
183
|
+
wallet: { formatted: '0.50' },
|
|
184
|
+
gateway: { formattedAvailable: '0.25', available: 250000n },
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
const hook = vi.fn()
|
|
188
|
+
const crumb = new Crumb({
|
|
189
|
+
privateKey: TEST_PRIVATE_KEY,
|
|
190
|
+
lowBalanceThreshold: 1.0,
|
|
191
|
+
onLowBalance: hook,
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
await crumb.balance()
|
|
195
|
+
expect(hook).toHaveBeenCalledWith('0.25')
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('does not trigger hook when above threshold', async () => {
|
|
199
|
+
mockGetBalances.mockResolvedValue({
|
|
200
|
+
wallet: { formatted: '100.00' },
|
|
201
|
+
gateway: { formattedAvailable: '50.00', available: 50000000n },
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
const hook = vi.fn()
|
|
205
|
+
const crumb = new Crumb({
|
|
206
|
+
privateKey: TEST_PRIVATE_KEY,
|
|
207
|
+
lowBalanceThreshold: 1.0,
|
|
208
|
+
onLowBalance: hook,
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
await crumb.balance()
|
|
212
|
+
expect(hook).not.toHaveBeenCalled()
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
describe('supports', () => {
|
|
217
|
+
it('delegates to gateway.supports', async () => {
|
|
218
|
+
mockSupports.mockResolvedValue({ supported: true, requirements: {} })
|
|
219
|
+
|
|
220
|
+
const crumb = new Crumb({ privateKey: TEST_PRIVATE_KEY })
|
|
221
|
+
const result = await crumb.supports('http://example.com/api')
|
|
222
|
+
|
|
223
|
+
expect(mockSupports).toHaveBeenCalledWith('http://example.com/api')
|
|
224
|
+
expect(result.supported).toBe(true)
|
|
225
|
+
})
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
describe('onLowBalance', () => {
|
|
229
|
+
it('updates threshold and hook', async () => {
|
|
230
|
+
mockGetBalances.mockResolvedValue({
|
|
231
|
+
wallet: { formatted: '5.00' },
|
|
232
|
+
gateway: { formattedAvailable: '3.00', available: 3000000n },
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
const hook = vi.fn()
|
|
236
|
+
const crumb = new Crumb({ privateKey: TEST_PRIVATE_KEY })
|
|
237
|
+
crumb.onLowBalance(5.0, hook)
|
|
238
|
+
|
|
239
|
+
await crumb.balance()
|
|
240
|
+
expect(hook).toHaveBeenCalledWith('3.00')
|
|
241
|
+
})
|
|
242
|
+
})
|
|
243
|
+
})
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
const mockRequire = vi.fn().mockReturnValue(vi.fn())
|
|
4
|
+
const mockVerify = vi.fn()
|
|
5
|
+
const mockSettle = vi.fn()
|
|
6
|
+
|
|
7
|
+
vi.mock('@circle-fin/x402-batching/server', () => ({
|
|
8
|
+
createGatewayMiddleware: vi.fn().mockImplementation(() => ({
|
|
9
|
+
require: mockRequire,
|
|
10
|
+
verify: mockVerify,
|
|
11
|
+
settle: mockSettle,
|
|
12
|
+
})),
|
|
13
|
+
}))
|
|
14
|
+
|
|
15
|
+
const { crumbMiddleware } = await import('../src/middleware/express.js')
|
|
16
|
+
|
|
17
|
+
describe('crumbMiddleware', () => {
|
|
18
|
+
it('returns an object with require, verify, settle', () => {
|
|
19
|
+
const gateway = crumbMiddleware({ sellerAddress: '0xseller' })
|
|
20
|
+
expect(gateway).toHaveProperty('require')
|
|
21
|
+
expect(gateway).toHaveProperty('verify')
|
|
22
|
+
expect(gateway).toHaveProperty('settle')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('require returns middleware', () => {
|
|
26
|
+
const gateway = crumbMiddleware({ sellerAddress: '0xseller' })
|
|
27
|
+
const mw = gateway.require('$0.01')
|
|
28
|
+
expect(mockRequire).toHaveBeenCalledWith('$0.01')
|
|
29
|
+
expect(mw).toBeDefined()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('wraps middleware with onPayment hook', async () => {
|
|
33
|
+
const onPayment = vi.fn()
|
|
34
|
+
const innerMw = vi.fn().mockImplementation((_req: any, _res: any, next: () => void) => {
|
|
35
|
+
next()
|
|
36
|
+
})
|
|
37
|
+
mockRequire.mockReturnValue(innerMw)
|
|
38
|
+
|
|
39
|
+
const gateway = crumbMiddleware({
|
|
40
|
+
sellerAddress: '0xseller',
|
|
41
|
+
onPayment,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const mw = gateway.require('$0.01')
|
|
45
|
+
const req = { payment: { verified: true, payer: '0xpayer', amount: '0.01', network: 'arcTestnet' } }
|
|
46
|
+
const res = {}
|
|
47
|
+
const next = vi.fn()
|
|
48
|
+
|
|
49
|
+
await mw(req, res, next)
|
|
50
|
+
|
|
51
|
+
expect(onPayment).toHaveBeenCalledWith(req.payment)
|
|
52
|
+
expect(next).toHaveBeenCalled()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('returns raw middleware when no onPayment', () => {
|
|
56
|
+
const innerMw = vi.fn()
|
|
57
|
+
mockRequire.mockReturnValue(innerMw)
|
|
58
|
+
|
|
59
|
+
const gateway = crumbMiddleware({ sellerAddress: '0xseller' })
|
|
60
|
+
const mw = gateway.require('$0.01')
|
|
61
|
+
expect(mw).toBe(innerMw)
|
|
62
|
+
})
|
|
63
|
+
})
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
const mockRequire = vi.fn().mockReturnValue(vi.fn())
|
|
4
|
+
const mockVerify = vi.fn()
|
|
5
|
+
const mockSettle = vi.fn()
|
|
6
|
+
|
|
7
|
+
vi.mock('@circle-fin/x402-batching/server', () => ({
|
|
8
|
+
createGatewayMiddleware: vi.fn().mockImplementation(() => ({
|
|
9
|
+
require: mockRequire,
|
|
10
|
+
verify: mockVerify,
|
|
11
|
+
settle: mockSettle,
|
|
12
|
+
})),
|
|
13
|
+
}))
|
|
14
|
+
|
|
15
|
+
const { createProvider } = await import('../src/provider.js')
|
|
16
|
+
|
|
17
|
+
describe('createProvider', () => {
|
|
18
|
+
it('returns an object with require, verify, settle', () => {
|
|
19
|
+
const provider = createProvider({ sellerAddress: '0xseller' })
|
|
20
|
+
expect(provider).toHaveProperty('require')
|
|
21
|
+
expect(provider).toHaveProperty('verify')
|
|
22
|
+
expect(provider).toHaveProperty('settle')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('passes config to createGatewayMiddleware', async () => {
|
|
26
|
+
const { createGatewayMiddleware } = await import('@circle-fin/x402-batching/server')
|
|
27
|
+
|
|
28
|
+
createProvider({
|
|
29
|
+
sellerAddress: '0xseller',
|
|
30
|
+
description: 'Test provider',
|
|
31
|
+
networks: 'arcTestnet',
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
expect(createGatewayMiddleware).toHaveBeenCalledWith({
|
|
35
|
+
sellerAddress: '0xseller',
|
|
36
|
+
description: 'Test provider',
|
|
37
|
+
networks: 'arcTestnet',
|
|
38
|
+
facilitatorUrl: undefined,
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('require returns middleware from gateway', () => {
|
|
43
|
+
const provider = createProvider({ sellerAddress: '0xseller' })
|
|
44
|
+
const mw = provider.require('$0.01')
|
|
45
|
+
expect(mockRequire).toHaveBeenCalledWith('$0.01')
|
|
46
|
+
expect(mw).toBeDefined()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('wraps middleware to call onPayment hook', async () => {
|
|
50
|
+
const onPayment = vi.fn()
|
|
51
|
+
const innerMw = vi.fn().mockImplementation((_req: any, _res: any, next: () => void) => {
|
|
52
|
+
next()
|
|
53
|
+
})
|
|
54
|
+
mockRequire.mockReturnValue(innerMw)
|
|
55
|
+
|
|
56
|
+
const provider = createProvider({
|
|
57
|
+
sellerAddress: '0xseller',
|
|
58
|
+
onPayment,
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const mw = provider.require('$0.01')
|
|
62
|
+
|
|
63
|
+
const req = { payment: { verified: true, payer: '0xpayer', amount: '0.01', network: 'arcTestnet' } }
|
|
64
|
+
const res = {}
|
|
65
|
+
const next = vi.fn()
|
|
66
|
+
|
|
67
|
+
await mw(req, res, next)
|
|
68
|
+
|
|
69
|
+
expect(onPayment).toHaveBeenCalledWith(req.payment)
|
|
70
|
+
expect(next).toHaveBeenCalled()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('does not wrap middleware when no onPayment hook', () => {
|
|
74
|
+
const innerMw = vi.fn()
|
|
75
|
+
mockRequire.mockReturnValue(innerMw)
|
|
76
|
+
|
|
77
|
+
const provider = createProvider({ sellerAddress: '0xseller' })
|
|
78
|
+
const mw = provider.require('$0.01')
|
|
79
|
+
|
|
80
|
+
// Without onPayment, it should return the raw middleware
|
|
81
|
+
expect(mw).toBe(innerMw)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('exposes verify and settle from gateway', () => {
|
|
85
|
+
const provider = createProvider({ sellerAddress: '0xseller' })
|
|
86
|
+
expect(provider.verify).toBe(mockVerify)
|
|
87
|
+
expect(provider.settle).toBe(mockSettle)
|
|
88
|
+
})
|
|
89
|
+
})
|