@obbyx/sdk 0.1.0 → 0.2.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/README.md +14 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/seller/consumed-proofs.d.ts +6 -0
- package/dist/seller/consumed-proofs.d.ts.map +1 -1
- package/dist/seller/consumed-proofs.js +20 -0
- package/dist/seller/consumed-proofs.js.map +1 -1
- package/dist/seller/cors.d.ts +33 -0
- package/dist/seller/cors.d.ts.map +1 -0
- package/dist/seller/cors.js +77 -0
- package/dist/seller/cors.js.map +1 -0
- package/dist/seller/defaults.d.ts +7 -0
- package/dist/seller/defaults.d.ts.map +1 -0
- package/dist/seller/defaults.js +7 -0
- package/dist/seller/defaults.js.map +1 -0
- package/dist/seller/paywall.d.ts +14 -0
- package/dist/seller/paywall.d.ts.map +1 -1
- package/dist/seller/paywall.js +42 -7
- package/dist/seller/paywall.js.map +1 -1
- package/dist/seller/x402-server.d.ts +50 -8
- package/dist/seller/x402-server.d.ts.map +1 -1
- package/dist/seller/x402-server.js +86 -24
- package/dist/seller/x402-server.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +10 -1
- package/src/seller/consumed-proofs.ts +22 -0
- package/src/seller/cors.ts +106 -0
- package/src/seller/defaults.ts +6 -0
- package/src/seller/paywall.ts +57 -8
- package/src/seller/x402-server.ts +130 -31
package/src/seller/paywall.ts
CHANGED
|
@@ -43,6 +43,17 @@ import {
|
|
|
43
43
|
operationFingerprint,
|
|
44
44
|
} from './quote.js';
|
|
45
45
|
import type { ConsumedProofsStore, StoredResponse } from './consumed-proofs.js';
|
|
46
|
+
import {
|
|
47
|
+
type PaywallCorsOptions,
|
|
48
|
+
allowOrigin,
|
|
49
|
+
exposePaymentHeaders,
|
|
50
|
+
preflightHandler,
|
|
51
|
+
} from './cors.js';
|
|
52
|
+
import {
|
|
53
|
+
DEFAULT_MAX_TIMEOUT_SECONDS,
|
|
54
|
+
DEFAULT_QUOTE_TTL_SECONDS,
|
|
55
|
+
DEFAULT_SERVING_LEASE_SECONDS,
|
|
56
|
+
} from './defaults.js';
|
|
46
57
|
|
|
47
58
|
/**
|
|
48
59
|
* Express paywall for x402 `exact` payments on Obyte (seller side).
|
|
@@ -117,11 +128,15 @@ export interface ObytePaywallOptions {
|
|
|
117
128
|
readonly paymentIdentifier?: { readonly required: boolean };
|
|
118
129
|
/** Enables batch-settlement (AA payment channels) offers; requires a running seller watcher. */
|
|
119
130
|
readonly batch?: { readonly minChannelTimeoutSeconds: number };
|
|
131
|
+
/**
|
|
132
|
+
* CORS for browser clients on another origin (x402 HTTP transport, "Browser
|
|
133
|
+
* clients"). Also mount {@link ObytePaywall.preflight} before the routes.
|
|
134
|
+
* Without it, same-origin pages and non-browser clients work as before.
|
|
135
|
+
*/
|
|
136
|
+
readonly cors?: PaywallCorsOptions;
|
|
120
137
|
readonly now?: () => number;
|
|
121
138
|
}
|
|
122
139
|
|
|
123
|
-
const EXPOSE = 'PAYMENT-REQUIRED, PAYMENT-RESPONSE';
|
|
124
|
-
|
|
125
140
|
export class ObytePaywall {
|
|
126
141
|
private readonly log: Logger;
|
|
127
142
|
private readonly now: () => number;
|
|
@@ -140,6 +155,15 @@ export class ObytePaywall {
|
|
|
140
155
|
this.now = options.now ?? (() => Math.floor(Date.now() / 1000));
|
|
141
156
|
}
|
|
142
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Answers CORS preflight requests (`app.use(paywall.preflight())`); a no-op
|
|
160
|
+
* without the `cors` option. Preflights never require payment.
|
|
161
|
+
*/
|
|
162
|
+
preflight(): RequestHandler {
|
|
163
|
+
const cors = this.options.cors;
|
|
164
|
+
return cors === undefined ? (_req, _res, next) => next() : preflightHandler(cors);
|
|
165
|
+
}
|
|
166
|
+
|
|
143
167
|
/** Wraps a handler so it only runs after a settled payment. */
|
|
144
168
|
paid(route: PaidRouteOptions, handler: PaidHandler): RequestHandler {
|
|
145
169
|
const asset = route.price.asset ?? 'base';
|
|
@@ -160,7 +184,8 @@ export class ObytePaywall {
|
|
|
160
184
|
price: { asset: string; amount: string },
|
|
161
185
|
handler: PaidHandler,
|
|
162
186
|
): Promise<void> {
|
|
163
|
-
res
|
|
187
|
+
exposePaymentHeaders(res);
|
|
188
|
+
if (this.options.cors !== undefined) allowOrigin(req, res, this.options.cors);
|
|
164
189
|
res.setHeader('Cache-Control', 'private, no-store');
|
|
165
190
|
const header = req.get('payment-signature');
|
|
166
191
|
if (header === undefined) return this.paymentRequired(req, res, route, price);
|
|
@@ -221,12 +246,13 @@ export class ObytePaywall {
|
|
|
221
246
|
// A proof left in `serving` by a crashed process is recovered after the lease.
|
|
222
247
|
if (
|
|
223
248
|
record.state === 'serving' &&
|
|
224
|
-
store.reclaimStale(key, this.options.servingLeaseSeconds ??
|
|
249
|
+
store.reclaimStale(key, this.options.servingLeaseSeconds ?? DEFAULT_SERVING_LEASE_SECONDS)
|
|
225
250
|
) {
|
|
226
251
|
this.log.warn('recovered a payment stuck in serving (lease expired)');
|
|
227
252
|
record = store.get(key) ?? record;
|
|
228
253
|
}
|
|
229
254
|
if (!bindingsEqual(record.binding, binding)) {
|
|
255
|
+
res.setHeader('PAYMENT-RESPONSE', this.refusal('invalid_payment_reused'));
|
|
230
256
|
res.status(402).json({ error: 'invalid_payment_reused' });
|
|
231
257
|
return;
|
|
232
258
|
}
|
|
@@ -283,8 +309,12 @@ export class ObytePaywall {
|
|
|
283
309
|
const settled = await this.settleWithWait(payload, accepted);
|
|
284
310
|
const paymentResponse = encodePaymentResponseHeader(settled);
|
|
285
311
|
if (settled.success) {
|
|
286
|
-
store.transition(key, 'settling', 'paid_not_served', { paymentResponse });
|
|
287
312
|
this.log.info({ transaction: settled.transaction }, 'payment settled');
|
|
313
|
+
if (store.markSettled(key, binding, paymentResponse) === 'served') {
|
|
314
|
+
// A concurrent request with the same payment already served it.
|
|
315
|
+
const served = store.get(key);
|
|
316
|
+
return this.send(res, served?.response ?? null, served?.paymentResponse ?? null);
|
|
317
|
+
}
|
|
288
318
|
return this.serve(req, res, key, paymentResponse, handler);
|
|
289
319
|
}
|
|
290
320
|
if (settled.errorReason === 'settlement_pending') {
|
|
@@ -299,7 +329,9 @@ export class ObytePaywall {
|
|
|
299
329
|
}
|
|
300
330
|
if (settled.transaction === '') {
|
|
301
331
|
// Rejected before reaching the network: the proof was not consumed.
|
|
302
|
-
|
|
332
|
+
// Only the request that created the row removes it: a concurrent
|
|
333
|
+
// request with the same payment may be settling it right now.
|
|
334
|
+
if (created) store.forget(key, 'settling');
|
|
303
335
|
} else {
|
|
304
336
|
store.transition(key, 'settling', 'failed', { paymentResponse });
|
|
305
337
|
}
|
|
@@ -447,7 +479,14 @@ export class ObytePaywall {
|
|
|
447
479
|
price: { asset: string; amount: string },
|
|
448
480
|
error?: string,
|
|
449
481
|
): void {
|
|
450
|
-
|
|
482
|
+
// A refused payment that was never settled: say so explicitly (success:
|
|
483
|
+
// false, empty transaction) so the client knows it was NOT used and can
|
|
484
|
+
// release its budget and its channel voucher. A settle failure already
|
|
485
|
+
// set its own PAYMENT-RESPONSE.
|
|
486
|
+
if (error !== undefined && !res.hasHeader('PAYMENT-RESPONSE')) {
|
|
487
|
+
res.setHeader('PAYMENT-RESPONSE', this.refusal(error));
|
|
488
|
+
}
|
|
489
|
+
const quote = newQuote(this.now(), this.options.quoteTtlSeconds ?? DEFAULT_QUOTE_TTL_SECONDS);
|
|
451
490
|
const facts = this.requestFacts(req, route);
|
|
452
491
|
const terms = this.terms(price);
|
|
453
492
|
const accepts: PaymentRequirements[] = this.schemesFor(route).map((scheme) => ({
|
|
@@ -456,7 +495,7 @@ export class ObytePaywall {
|
|
|
456
495
|
asset: price.asset,
|
|
457
496
|
amount: price.amount,
|
|
458
497
|
payTo: this.options.payTo,
|
|
459
|
-
maxTimeoutSeconds: this.options.maxTimeoutSeconds ??
|
|
498
|
+
maxTimeoutSeconds: this.options.maxTimeoutSeconds ?? DEFAULT_MAX_TIMEOUT_SECONDS,
|
|
460
499
|
extra: {
|
|
461
500
|
binding: offerBinding(this.options, scheme, quote, facts, terms),
|
|
462
501
|
...quote,
|
|
@@ -493,6 +532,16 @@ export class ObytePaywall {
|
|
|
493
532
|
res.status(402).json(body);
|
|
494
533
|
}
|
|
495
534
|
|
|
535
|
+
/** PAYMENT-RESPONSE of a payment refused before settlement (not used, nothing charged). */
|
|
536
|
+
private refusal(errorReason: string): string {
|
|
537
|
+
return encodePaymentResponseHeader({
|
|
538
|
+
success: false,
|
|
539
|
+
errorReason,
|
|
540
|
+
transaction: '',
|
|
541
|
+
network: this.options.network as PaymentRequirements['network'],
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
496
545
|
private requestFacts(req: Request, route: PaidRouteOptions): RequestFacts {
|
|
497
546
|
return {
|
|
498
547
|
method: req.method,
|
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
SchemeServerHooks,
|
|
10
10
|
} from '@x402/core/types';
|
|
11
11
|
import {
|
|
12
|
+
CHANNEL_BASE_AA,
|
|
12
13
|
ObbyxError,
|
|
13
14
|
fromDisplay,
|
|
14
15
|
getNetwork,
|
|
@@ -17,13 +18,24 @@ import {
|
|
|
17
18
|
parseAmount,
|
|
18
19
|
} from '@obbyx/obyte';
|
|
19
20
|
import { type Logger, moduleLogger } from '@obbyx/obyte/logger';
|
|
20
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
BATCH_SCHEME,
|
|
23
|
+
EXACT_SCHEME,
|
|
24
|
+
batchExtraSchema,
|
|
25
|
+
bindingsEqual,
|
|
26
|
+
canonical,
|
|
27
|
+
exactExtraSchema,
|
|
28
|
+
sha256Hex,
|
|
29
|
+
} from '@obbyx/x402-obyte';
|
|
21
30
|
import type { ConsumedProofsStore } from './consumed-proofs.js';
|
|
31
|
+
import { DEFAULT_QUOTE_TTL_SECONDS, DEFAULT_SERVING_LEASE_SECONDS } from './defaults.js';
|
|
22
32
|
import { type Quote, bodyHashOf, newQuote, offerBinding } from './quote.js';
|
|
23
33
|
|
|
24
34
|
/**
|
|
25
|
-
* Obyte
|
|
26
|
-
* `@x402/express`, `@x402/hono`, …):
|
|
35
|
+
* Obyte schemes for the OFFICIAL x402 resource server (`@x402/core/server`,
|
|
36
|
+
* `@x402/express`, `@x402/hono`, …): SchemeNetworkServer implementations for
|
|
37
|
+
* `exact` ({@link ObyteExactServer}) and `batch-settlement`
|
|
38
|
+
* ({@link ObyteBatchServer}), sharing {@link ObyteSchemeServer}.
|
|
27
39
|
*
|
|
28
40
|
* - Binding (I1/I2): `enrichPaymentRequiredResponse` adds a per-request HMAC
|
|
29
41
|
* binding + quote to every offer. On a paid request it recomputes the HMAC
|
|
@@ -37,7 +49,9 @@ import { type Quote, bodyHashOf, newQuote, offerBinding } from './quote.js';
|
|
|
37
49
|
* Unlike `@obbyx/sdk`'s ObytePaywall, a repeated request gets a 402 instead
|
|
38
50
|
* of the stored response (the official middleware cannot replay responses).
|
|
39
51
|
*/
|
|
40
|
-
export interface
|
|
52
|
+
export interface ObyteSchemeServerOptions {
|
|
53
|
+
/** How long a request may hold a payment in `serving` before a retry takes over (default 300 s). */
|
|
54
|
+
readonly servingLeaseSeconds?: number;
|
|
41
55
|
readonly sellerId: string;
|
|
42
56
|
/** HMAC key for bindings (≥ 32 bytes). */
|
|
43
57
|
readonly secret: string | Uint8Array;
|
|
@@ -69,8 +83,32 @@ function requestOf(transportContext: unknown): RequestLike | undefined {
|
|
|
69
83
|
};
|
|
70
84
|
}
|
|
71
85
|
|
|
72
|
-
|
|
73
|
-
|
|
86
|
+
/** @deprecated Use {@link ObyteSchemeServerOptions}. */
|
|
87
|
+
export type ObyteExactServerOptions = ObyteSchemeServerOptions;
|
|
88
|
+
|
|
89
|
+
/** Options of {@link ObyteBatchServer}. */
|
|
90
|
+
export interface ObyteBatchServerOptions extends ObyteSchemeServerOptions {
|
|
91
|
+
/** Minimum channel dispute window the seller accepts (seconds); must match the facilitator's. */
|
|
92
|
+
readonly minChannelTimeoutSeconds: number;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Per-scheme parts of an Obyte resource-server scheme. */
|
|
96
|
+
export interface ObyteSchemeSpec {
|
|
97
|
+
readonly scheme: typeof EXACT_SCHEME | typeof BATCH_SCHEME;
|
|
98
|
+
/** `extra` fields the scheme adds to every offer besides binding and quote. */
|
|
99
|
+
readonly staticExtra: Record<string, unknown>;
|
|
100
|
+
/** Parses the quote fields of the accepted offer. */
|
|
101
|
+
readonly acceptedExtra: (extra: unknown) => {
|
|
102
|
+
readonly binding: string;
|
|
103
|
+
readonly quoteId: string;
|
|
104
|
+
readonly validUntil: number;
|
|
105
|
+
};
|
|
106
|
+
/** Proof id and key kind of a payment payload (undefined when malformed). */
|
|
107
|
+
readonly proofOf: (payload: Record<string, unknown>) => { kind: string; id: string } | undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export abstract class ObyteSchemeServer implements SchemeNetworkServer {
|
|
111
|
+
readonly scheme: string;
|
|
74
112
|
readonly defaultAssetTransferMethod = 'default';
|
|
75
113
|
readonly paymentFlows = {
|
|
76
114
|
default: { supported: ['upfront'] as const, default: 'upfront' as const },
|
|
@@ -80,24 +118,25 @@ export class ObyteExactServer implements SchemeNetworkServer {
|
|
|
80
118
|
private readonly log: Logger;
|
|
81
119
|
private readonly now: () => number;
|
|
82
120
|
|
|
83
|
-
constructor(
|
|
84
|
-
|
|
121
|
+
protected constructor(
|
|
122
|
+
private readonly options: ObyteSchemeServerOptions,
|
|
123
|
+
private readonly spec: ObyteSchemeSpec,
|
|
124
|
+
) {
|
|
125
|
+
this.scheme = spec.scheme;
|
|
126
|
+
this.log = moduleLogger(options.logger, 'sdk.x402-server', { scheme: spec.scheme });
|
|
85
127
|
this.now = options.now ?? (() => Math.floor(Date.now() / 1000));
|
|
86
128
|
this.schemeHooks = {
|
|
87
129
|
onBeforeVerify: async (ctx) => {
|
|
88
|
-
const
|
|
130
|
+
const key = this.keyOf(ctx);
|
|
89
131
|
const binding = (ctx.requirements.extra as { binding?: unknown }).binding;
|
|
90
|
-
if (
|
|
132
|
+
if (key === undefined || typeof binding !== 'string') {
|
|
91
133
|
return { abort: true, reason: 'invalid_payload' };
|
|
92
134
|
}
|
|
93
|
-
const { record } = this.options.proofs.begin(
|
|
94
|
-
this.key(ctx.requirements.network, unit),
|
|
95
|
-
binding,
|
|
96
|
-
);
|
|
135
|
+
const { record } = this.options.proofs.begin(key, binding);
|
|
97
136
|
if (!bindingsEqual(record.binding, binding))
|
|
98
137
|
return { abort: true, reason: 'invalid_payment_reused' };
|
|
99
138
|
if (record.state === 'served') {
|
|
100
|
-
this.log.warn({ proofId:
|
|
139
|
+
this.log.warn({ proofId: key }, 'payment already served a request; refused');
|
|
101
140
|
return { abort: true, reason: 'payment_already_used' };
|
|
102
141
|
}
|
|
103
142
|
return undefined;
|
|
@@ -109,7 +148,16 @@ export class ObyteExactServer implements SchemeNetworkServer {
|
|
|
109
148
|
onBeforeSettle: async (ctx) => {
|
|
110
149
|
const key = this.keyOf(ctx);
|
|
111
150
|
if (key === undefined) return { abort: true, reason: 'invalid_payload' };
|
|
112
|
-
|
|
151
|
+
const proofs = this.options.proofs;
|
|
152
|
+
// A claim left behind by a crashed process is taken over after the lease.
|
|
153
|
+
proofs.reclaimStale(key, this.options.servingLeaseSeconds ?? DEFAULT_SERVING_LEASE_SECONDS);
|
|
154
|
+
if (
|
|
155
|
+
proofs.transition(key, 'settling', 'serving') ||
|
|
156
|
+
// settled earlier, but the handler failed: serve it now (settle is idempotent)
|
|
157
|
+
proofs.transition(key, 'paid_not_served', 'serving')
|
|
158
|
+
) {
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
113
161
|
const state = this.options.proofs.get(key)?.state;
|
|
114
162
|
this.log.warn({ state }, 'payment already in use by another request; refused');
|
|
115
163
|
return {
|
|
@@ -125,6 +173,21 @@ export class ObyteExactServer implements SchemeNetworkServer {
|
|
|
125
173
|
if (ctx.result.success) this.options.proofs.transition(key, 'serving', 'served');
|
|
126
174
|
else this.options.proofs.transition(key, 'serving', 'settling');
|
|
127
175
|
},
|
|
176
|
+
// The handler failed or threw after the payment was settled (upfront):
|
|
177
|
+
// the payment stays usable for the SAME operation instead of "already used".
|
|
178
|
+
onVerifiedPaymentCanceled: async (ctx) => {
|
|
179
|
+
const key = this.keyOf(ctx);
|
|
180
|
+
if (key === undefined) return;
|
|
181
|
+
if (ctx.settledPhases.includes('before-handler')) {
|
|
182
|
+
this.options.proofs.transition(key, 'served', 'paid_not_served');
|
|
183
|
+
} else {
|
|
184
|
+
this.options.proofs.transition(key, 'serving', 'settling');
|
|
185
|
+
}
|
|
186
|
+
this.log.warn(
|
|
187
|
+
{ reason: ctx.reason },
|
|
188
|
+
'paid request not served; the same payment can retry',
|
|
189
|
+
);
|
|
190
|
+
},
|
|
128
191
|
onSettleFailure: async (ctx) => {
|
|
129
192
|
const key = this.keyOf(ctx);
|
|
130
193
|
if (key !== undefined) this.options.proofs.transition(key, 'serving', 'settling');
|
|
@@ -156,7 +219,7 @@ export class ObyteExactServer implements SchemeNetworkServer {
|
|
|
156
219
|
return asset === 'base' ? 9 : undefined;
|
|
157
220
|
}
|
|
158
221
|
|
|
159
|
-
/** Nothing to add from /supported
|
|
222
|
+
/** Nothing to add from /supported; the per-request fields come from the enrich hook. */
|
|
160
223
|
async enhancePaymentRequirements(
|
|
161
224
|
requirements: PaymentRequirements,
|
|
162
225
|
): Promise<PaymentRequirements> {
|
|
@@ -169,20 +232,21 @@ export class ObyteExactServer implements SchemeNetworkServer {
|
|
|
169
232
|
): Promise<PaymentRequirements[]> => {
|
|
170
233
|
const request = requestOf(ctx.transportContext);
|
|
171
234
|
if (request === undefined)
|
|
172
|
-
throw new ObbyxError('no_request',
|
|
235
|
+
throw new ObbyxError('no_request', `Obyte ${this.scheme} needs the HTTP request context`);
|
|
173
236
|
const accepted = this.acceptedQuote(request);
|
|
174
237
|
return ctx.requirements.map((req) => {
|
|
175
|
-
if (req.scheme !==
|
|
238
|
+
if (req.scheme !== this.spec.scheme) return req;
|
|
176
239
|
const quote =
|
|
177
240
|
accepted !== undefined &&
|
|
178
241
|
bindingsEqual(this.bindingFor(req, request, accepted), accepted.binding)
|
|
179
242
|
? accepted
|
|
180
243
|
: undefined;
|
|
181
|
-
|
|
182
|
-
|
|
244
|
+
const extra = { ...req.extra, ...this.spec.staticExtra };
|
|
245
|
+
if (quote !== undefined) return { ...req, extra: { ...extra, ...quote } };
|
|
246
|
+
const fresh = newQuote(this.now(), this.options.quoteTtlSeconds ?? DEFAULT_QUOTE_TTL_SECONDS);
|
|
183
247
|
return {
|
|
184
248
|
...req,
|
|
185
|
-
extra: { ...
|
|
249
|
+
extra: { ...extra, binding: this.bindingFor(req, request, fresh), ...fresh },
|
|
186
250
|
};
|
|
187
251
|
});
|
|
188
252
|
};
|
|
@@ -190,10 +254,10 @@ export class ObyteExactServer implements SchemeNetworkServer {
|
|
|
190
254
|
private acceptedQuote(request: RequestLike) {
|
|
191
255
|
if (request.paymentHeader === undefined) return undefined;
|
|
192
256
|
try {
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
);
|
|
196
|
-
return extra.
|
|
257
|
+
const accepted = decodePaymentSignatureHeader(request.paymentHeader).accepted;
|
|
258
|
+
if (accepted?.scheme !== this.spec.scheme) return undefined;
|
|
259
|
+
const extra = this.spec.acceptedExtra(accepted.extra);
|
|
260
|
+
return { quoteId: extra.quoteId, validUntil: extra.validUntil, binding: extra.binding };
|
|
197
261
|
} catch {
|
|
198
262
|
return undefined;
|
|
199
263
|
}
|
|
@@ -202,7 +266,7 @@ export class ObyteExactServer implements SchemeNetworkServer {
|
|
|
202
266
|
private bindingFor(req: PaymentRequirements, request: RequestLike, quote: Quote): string {
|
|
203
267
|
return offerBinding(
|
|
204
268
|
this.options,
|
|
205
|
-
|
|
269
|
+
this.spec.scheme,
|
|
206
270
|
quote,
|
|
207
271
|
{ method: request.method, url: request.url, bodyHash: bodyHashOf(request.body) },
|
|
208
272
|
{ network: req.network, asset: req.asset, amount: req.amount, payTo: req.payTo },
|
|
@@ -214,11 +278,46 @@ export class ObyteExactServer implements SchemeNetworkServer {
|
|
|
214
278
|
paymentPayload: { payload: Record<string, unknown> };
|
|
215
279
|
requirements: { network: string };
|
|
216
280
|
}): string | undefined {
|
|
217
|
-
const
|
|
218
|
-
return
|
|
281
|
+
const proof = this.spec.proofOf(ctx.paymentPayload.payload);
|
|
282
|
+
return proof === undefined
|
|
283
|
+
? undefined
|
|
284
|
+
: networkKey(getNetwork(ctx.requirements.network), proof.kind, proof.id);
|
|
219
285
|
}
|
|
286
|
+
}
|
|
220
287
|
|
|
221
|
-
|
|
222
|
-
|
|
288
|
+
/** `exact`: the proof is the unit (key `<network>:unit:<hash>`, as ObytePaywall). */
|
|
289
|
+
export class ObyteExactServer extends ObyteSchemeServer {
|
|
290
|
+
constructor(options: ObyteSchemeServerOptions) {
|
|
291
|
+
super(options, {
|
|
292
|
+
scheme: EXACT_SCHEME,
|
|
293
|
+
staticExtra: {},
|
|
294
|
+
acceptedExtra: (extra) => exactExtraSchema.parse(extra),
|
|
295
|
+
proofOf: (payload) => {
|
|
296
|
+
const unit = (payload as { unit?: { unit?: unknown } }).unit?.unit;
|
|
297
|
+
return typeof unit === 'string' ? { kind: 'unit', id: unit } : undefined;
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* `batch-settlement` (AA payment channels): the proof is the voucher (key
|
|
305
|
+
* `<network>:voucher:<sha256 of the canonical signed package>`, as
|
|
306
|
+
* ObytePaywall). Offers carry the channel base AA and the minimum dispute
|
|
307
|
+
* window. Needs a facilitator with batch-settlement and the seller's watcher.
|
|
308
|
+
*/
|
|
309
|
+
export class ObyteBatchServer extends ObyteSchemeServer {
|
|
310
|
+
constructor(options: ObyteBatchServerOptions) {
|
|
311
|
+
super(options, {
|
|
312
|
+
scheme: BATCH_SCHEME,
|
|
313
|
+
staticExtra: { baseAa: CHANNEL_BASE_AA, minTimeout: options.minChannelTimeoutSeconds },
|
|
314
|
+
acceptedExtra: (extra) => batchExtraSchema.parse(extra),
|
|
315
|
+
proofOf: (payload) => {
|
|
316
|
+
const pkg = payload['signedPackage'];
|
|
317
|
+
return pkg !== null && typeof pkg === 'object'
|
|
318
|
+
? { kind: 'voucher', id: sha256Hex(canonical(pkg)) }
|
|
319
|
+
: undefined;
|
|
320
|
+
},
|
|
321
|
+
});
|
|
223
322
|
}
|
|
224
323
|
}
|