@obbyx/sdk 0.1.1 → 0.2.1

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.
@@ -43,6 +43,12 @@ 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';
46
52
  import {
47
53
  DEFAULT_MAX_TIMEOUT_SECONDS,
48
54
  DEFAULT_QUOTE_TTL_SECONDS,
@@ -68,6 +74,32 @@ export interface PaidResult {
68
74
 
69
75
  export type PaidHandler = (req: Request) => Promise<PaidResult> | PaidResult;
70
76
 
77
+ /** A route's price, resolved: asset and atomic amount. */
78
+ type Price = { readonly asset: string; readonly amount: string };
79
+
80
+ /** One call of a paid route. */
81
+ interface PaidCall {
82
+ readonly req: Request;
83
+ readonly res: Response;
84
+ readonly route: PaidRouteOptions;
85
+ readonly price: Price;
86
+ readonly handler: PaidHandler;
87
+ }
88
+
89
+ /** A paid call whose payment matches this offer and was bound to this request. */
90
+ interface Payment extends PaidCall {
91
+ readonly payload: PaymentPayload;
92
+ readonly accepted: PaymentRequirements;
93
+ /** Unit hash (exact) or voucher hash (batch). */
94
+ readonly proofId: string;
95
+ /** Proof key in the consumed-proofs store (network-scoped). */
96
+ readonly key: string;
97
+ readonly binding: string;
98
+ readonly validUntil: number;
99
+ readonly facts: RequestFacts;
100
+ readonly terms: PaymentTerms;
101
+ }
102
+
71
103
  export interface RoutePrice {
72
104
  /** Atomic units as a decimal string. */
73
105
  readonly amount: string;
@@ -122,11 +154,15 @@ export interface ObytePaywallOptions {
122
154
  readonly paymentIdentifier?: { readonly required: boolean };
123
155
  /** Enables batch-settlement (AA payment channels) offers; requires a running seller watcher. */
124
156
  readonly batch?: { readonly minChannelTimeoutSeconds: number };
157
+ /**
158
+ * CORS for browser clients on another origin (x402 HTTP transport, "Browser
159
+ * clients"). Also mount {@link ObytePaywall.preflight} before the routes.
160
+ * Without it, same-origin pages and non-browser clients work as before.
161
+ */
162
+ readonly cors?: PaywallCorsOptions;
125
163
  readonly now?: () => number;
126
164
  }
127
165
 
128
- const EXPOSE = 'PAYMENT-REQUIRED, PAYMENT-RESPONSE';
129
-
130
166
  export class ObytePaywall {
131
167
  private readonly log: Logger;
132
168
  private readonly now: () => number;
@@ -145,41 +181,66 @@ export class ObytePaywall {
145
181
  this.now = options.now ?? (() => Math.floor(Date.now() / 1000));
146
182
  }
147
183
 
184
+ /**
185
+ * Answers CORS preflight requests (`app.use(paywall.preflight())`); a no-op
186
+ * without the `cors` option. Preflights never require payment.
187
+ */
188
+ preflight(): RequestHandler {
189
+ const cors = this.options.cors;
190
+ return cors === undefined ? (_req, _res, next) => next() : preflightHandler(cors);
191
+ }
192
+
148
193
  /** Wraps a handler so it only runs after a settled payment. */
149
194
  paid(route: PaidRouteOptions, handler: PaidHandler): RequestHandler {
150
195
  const asset = route.price.asset ?? 'base';
151
196
  if (!isAssetId(asset)) throw new Error(`Invalid asset ${asset}`);
152
- const amount = parseAmount(route.price.amount).toString();
197
+ const price = { asset, amount: parseAmount(route.price.amount).toString() };
153
198
  return (req: Request, res: Response, next: NextFunction) => {
154
199
  const requestId = String(req.get('x-request-id') ?? cryptoId());
155
200
  withLogContext({ requestId, network: this.options.network }, () =>
156
- this.handle(req, res, route, { asset, amount }, handler).catch(next),
201
+ this.handle({ req, res, route, price, handler }).catch(next),
157
202
  );
158
203
  };
159
204
  }
160
205
 
161
- private async handle(
162
- req: Request,
163
- res: Response,
164
- route: PaidRouteOptions,
165
- price: { asset: string; amount: string },
166
- handler: PaidHandler,
167
- ): Promise<void> {
168
- res.setHeader('Access-Control-Expose-Headers', EXPOSE);
206
+ private async handle(call: PaidCall): Promise<void> {
207
+ const { req, res } = call;
208
+ exposePaymentHeaders(res);
209
+ if (this.options.cors !== undefined) allowOrigin(req, res, this.options.cors);
169
210
  res.setHeader('Cache-Control', 'private, no-store');
170
211
  const header = req.get('payment-signature');
171
- if (header === undefined) return this.paymentRequired(req, res, route, price);
212
+ if (header === undefined) return this.paymentRequired(call);
213
+
214
+ const parsed = this.parsePayment(call, header);
215
+ if (parsed.kind === 'bad_header') {
216
+ res.status(400).json({ error: 'invalid_payment_header' });
217
+ return;
218
+ }
219
+ if (parsed.kind === 'refused') return this.paymentRequired(call, parsed.error);
220
+ const payment = parsed.payment;
221
+ await withLogContext({ binding: payment.binding, proofId: payment.proofId }, () =>
222
+ this.process(payment),
223
+ );
224
+ }
172
225
 
226
+ /**
227
+ * Decodes the payment and checks it against THIS offer: our scheme, network,
228
+ * price and payee, and a binding we issued for this very request.
229
+ */
230
+ private parsePayment(
231
+ call: PaidCall,
232
+ header: string,
233
+ ):
234
+ { kind: 'ok'; payment: Payment } | { kind: 'bad_header' } | { kind: 'refused'; error: string } {
173
235
  let payload: PaymentPayload;
174
236
  try {
175
237
  payload = decodePaymentSignatureHeader(header);
176
238
  } catch {
177
- res.status(400).json({ error: 'invalid_payment_header' });
178
- return;
239
+ return { kind: 'bad_header' };
179
240
  }
241
+ const { route, price } = call;
180
242
  const accepted = payload.accepted;
181
243
  const scheme = accepted?.scheme as PaywallScheme | undefined;
182
- const offered = this.schemesFor(route);
183
244
  const extra =
184
245
  scheme === EXACT_SCHEME
185
246
  ? exactExtraSchema.safeParse(accepted?.extra)
@@ -190,152 +251,169 @@ export class ObytePaywall {
190
251
  if (
191
252
  payload.x402Version !== 2 ||
192
253
  scheme === undefined ||
193
- !offered.includes(scheme) ||
254
+ !this.schemesFor(route).includes(scheme) ||
194
255
  accepted?.network !== this.options.network ||
195
256
  accepted.asset !== price.asset ||
196
257
  accepted.amount !== price.amount ||
197
258
  accepted.payTo !== this.options.payTo ||
198
- extra === undefined ||
199
- !extra.success ||
259
+ extra?.success !== true ||
200
260
  proofId === undefined ||
201
261
  (scheme === BATCH_SCHEME &&
202
262
  (extra.data as { minTimeout?: number }).minTimeout !==
203
263
  this.options.batch?.minChannelTimeoutSeconds)
204
264
  ) {
205
- return this.paymentRequired(req, res, route, price, 'invalid_payment_requirements');
265
+ return { kind: 'refused', error: 'invalid_payment_requirements' };
206
266
  }
207
- const facts = this.requestFacts(req, route);
267
+ const facts = this.requestFacts(call.req, route);
208
268
  const terms = this.terms(price);
209
- const expected = offerBinding(this.options, scheme, extra.data, facts, terms);
210
- if (!bindingsEqual(expected, extra.data.binding)) {
269
+ if (
270
+ !bindingsEqual(
271
+ offerBinding(this.options, scheme, extra.data, facts, terms),
272
+ extra.data.binding,
273
+ )
274
+ ) {
211
275
  this.log.warn({ proofId }, 'payment is bound to another operation or a forged quote');
212
- return this.paymentRequired(req, res, route, price, 'invalid_binding');
276
+ return { kind: 'refused', error: 'invalid_binding' };
213
277
  }
214
- const binding = extra.data.binding;
215
- const key = networkKey(
216
- getNetwork(this.options.network),
217
- scheme === EXACT_SCHEME ? 'unit' : 'voucher',
218
- proofId,
219
- );
278
+ return {
279
+ kind: 'ok',
280
+ payment: {
281
+ ...call,
282
+ payload,
283
+ accepted,
284
+ proofId,
285
+ binding: extra.data.binding,
286
+ validUntil: extra.data.validUntil,
287
+ facts,
288
+ terms,
289
+ key: networkKey(
290
+ getNetwork(this.options.network),
291
+ scheme === EXACT_SCHEME ? 'unit' : 'voucher',
292
+ proofId,
293
+ ),
294
+ },
295
+ };
296
+ }
220
297
 
221
- await withLogContext({ binding, proofId }, async () => {
222
- const store = this.options.store;
223
- const begun = store.begin(key, binding);
224
- const { created } = begun;
225
- let { record } = begun;
226
- // A proof left in `serving` by a crashed process is recovered after the lease.
227
- if (
228
- record.state === 'serving' &&
229
- store.reclaimStale(key, this.options.servingLeaseSeconds ?? DEFAULT_SERVING_LEASE_SECONDS)
230
- ) {
231
- this.log.warn('recovered a payment stuck in serving (lease expired)');
232
- record = store.get(key) ?? record;
233
- }
234
- if (!bindingsEqual(record.binding, binding)) {
235
- res.setHeader('PAYMENT-RESPONSE', this.refusal('invalid_payment_reused'));
236
- res.status(402).json({ error: 'invalid_payment_reused' });
298
+ /** Registers the proof, then continues from its state: one payment, one operation. */
299
+ private async process(payment: Payment): Promise<void> {
300
+ const { res, key, binding } = payment;
301
+ const store = this.options.store;
302
+ const begun = store.begin(key, binding);
303
+ const { created } = begun;
304
+ let { record } = begun;
305
+ // A proof left in `serving` by a crashed process is recovered after the lease.
306
+ if (
307
+ record.state === 'serving' &&
308
+ store.reclaimStale(key, this.options.servingLeaseSeconds ?? DEFAULT_SERVING_LEASE_SECONDS)
309
+ ) {
310
+ this.log.warn('recovered a payment stuck in serving (lease expired)');
311
+ record = store.get(key) ?? record;
312
+ }
313
+ if (!bindingsEqual(record.binding, binding)) {
314
+ res.setHeader('PAYMENT-RESPONSE', this.refusal('invalid_payment_reused'));
315
+ res.status(402).json({ error: 'invalid_payment_reused' });
316
+ return;
317
+ }
318
+ if (this.paymentIdentifierAnswered(payment, created)) return;
319
+ if (created && payment.validUntil < this.now()) {
320
+ store.forget(key, 'settling');
321
+ return this.paymentRequired(payment, 'quote_expired');
322
+ }
323
+ switch (record.state) {
324
+ case 'served':
325
+ this.log.info('replaying stored response for a repeated operation');
326
+ return this.send(res, record.response, record.paymentResponse);
327
+ case 'serving':
328
+ if (record.paymentResponse !== null)
329
+ res.setHeader('PAYMENT-RESPONSE', record.paymentResponse);
330
+ res.status(409).json({ error: 'payment_in_progress' });
237
331
  return;
238
- }
239
- // x402 payment-identifier: one client id = one operation. Decided AFTER
240
- // the proof row exists, so a concurrent request always sees this proof
241
- // as in progress (never as "absent") and cannot take the id over.
242
- if (this.options.paymentIdentifier !== undefined) {
243
- const decision = decidePaymentId(store, {
244
- paymentId: paymentIdOf(payload),
245
- required: this.options.paymentIdentifier.required,
246
- scopeId: (id) => `${this.options.sellerId}:${id}`,
247
- fingerprint: operationFingerprint(facts, terms),
248
- proofKey: key,
249
- });
250
- if (decision.kind !== 'proceed' && created) store.forget(key, 'settling');
251
- if (decision.kind === 'reject') {
252
- res.status(decision.status).json({ error: decision.error });
253
- return;
254
- }
255
- if (decision.kind === 'replay') {
256
- const original = store.get(decision.proofKey);
257
- this.log.info('payment-identifier repeat: stored response, new payment NOT settled');
258
- return this.send(res, original?.response ?? null, original?.paymentResponse ?? null);
259
- }
260
- }
261
- if (created && extra.data.validUntil < this.now()) {
262
- store.forget(key, 'settling');
263
- return this.paymentRequired(req, res, route, price, 'quote_expired');
264
- }
265
- switch (record.state) {
266
- case 'served':
267
- this.log.info('replaying stored response for a repeated operation');
268
- return this.send(res, record.response, record.paymentResponse);
269
- case 'serving':
270
- if (record.paymentResponse !== null)
271
- res.setHeader('PAYMENT-RESPONSE', record.paymentResponse);
272
- res.status(409).json({ error: 'payment_in_progress' });
273
- return;
274
- case 'failed':
275
- res.status(402).json({ error: 'payment_failed' });
276
- return;
277
- case 'paid_not_served':
278
- return this.serve(req, res, key, record.paymentResponse, handler);
279
- case 'awaiting_settle':
280
- return this.settleServed(req, res, route, price, key, payload, accepted);
281
- case 'settling':
282
- break;
283
- }
332
+ case 'failed':
333
+ res.status(402).json({ error: 'payment_failed' });
334
+ return;
335
+ case 'paid_not_served':
336
+ return this.serve(payment, record.paymentResponse);
337
+ case 'awaiting_settle':
338
+ return this.settleServed(payment);
339
+ case 'settling':
340
+ return payment.route.flow === 'authorization'
341
+ ? this.authorize(payment)
342
+ : this.settleUpfront(payment, created);
343
+ }
344
+ }
284
345
 
285
- if (route.flow === 'authorization') {
286
- return this.authorize(req, res, route, price, key, payload, accepted, handler);
287
- }
346
+ /**
347
+ * x402 payment-identifier: one client id = one operation. Decided AFTER the
348
+ * proof row exists, so a concurrent request always sees this proof as in
349
+ * progress (never as "absent") and cannot take the id over. True when the
350
+ * response was already sent (refused, or the stored response replayed).
351
+ */
352
+ private paymentIdentifierAnswered(payment: Payment, created: boolean): boolean {
353
+ const declared = this.options.paymentIdentifier;
354
+ if (declared === undefined) return false;
355
+ const store = this.options.store;
356
+ const decision = decidePaymentId(store, {
357
+ paymentId: paymentIdOf(payment.payload),
358
+ required: declared.required,
359
+ scopeId: (id) => `${this.options.sellerId}:${id}`,
360
+ fingerprint: operationFingerprint(payment.facts, payment.terms),
361
+ proofKey: payment.key,
362
+ });
363
+ if (decision.kind === 'proceed') return false;
364
+ if (created) store.forget(payment.key, 'settling');
365
+ if (decision.kind === 'reject') {
366
+ payment.res.status(decision.status).json({ error: decision.error });
367
+ return true;
368
+ }
369
+ const original = store.get(decision.proofKey);
370
+ this.log.info('payment-identifier repeat: stored response, new payment NOT settled');
371
+ this.send(payment.res, original?.response ?? null, original?.paymentResponse ?? null);
372
+ return true;
373
+ }
288
374
 
289
- const settled = await this.settleWithWait(payload, accepted);
290
- const paymentResponse = encodePaymentResponseHeader(settled);
291
- if (settled.success) {
292
- this.log.info({ transaction: settled.transaction }, 'payment settled');
293
- if (store.markSettled(key, binding, paymentResponse) === 'served') {
294
- // A concurrent request with the same payment already served it.
295
- const served = store.get(key);
296
- return this.send(res, served?.response ?? null, served?.paymentResponse ?? null);
297
- }
298
- return this.serve(req, res, key, paymentResponse, handler);
299
- }
300
- if (settled.errorReason === 'settlement_pending') {
301
- this.log.info(
302
- { transaction: settled.transaction },
303
- 'settlement pending; client should retry with the same payment',
304
- );
305
- res.setHeader('PAYMENT-RESPONSE', paymentResponse);
306
- res.setHeader('Retry-After', '5');
307
- res.status(402).json({ error: 'settlement_pending', transaction: settled.transaction });
308
- return;
309
- }
310
- if (settled.transaction === '') {
311
- // Rejected before reaching the network: the proof was not consumed.
312
- // Only the request that created the row removes it: a concurrent
313
- // request with the same payment may be settling it right now.
314
- if (created) store.forget(key, 'settling');
315
- } else {
316
- store.transition(key, 'settling', 'failed', { paymentResponse });
375
+ /** upfront flow: settle to finality, then run the handler once. */
376
+ private async settleUpfront(payment: Payment, created: boolean): Promise<void> {
377
+ const { res, key } = payment;
378
+ const store = this.options.store;
379
+ const settled = await this.settleWithWait(payment.payload, payment.accepted);
380
+ const paymentResponse = encodePaymentResponseHeader(settled);
381
+ if (settled.success) {
382
+ this.log.info({ transaction: settled.transaction }, 'payment settled');
383
+ if (store.markSettled(key, payment.binding, paymentResponse) === 'served') {
384
+ // A concurrent request with the same payment already served it.
385
+ const served = store.get(key);
386
+ return this.send(res, served?.response ?? null, served?.paymentResponse ?? null);
317
387
  }
318
- this.log.warn({ reason: settled.errorReason }, 'payment failed');
319
- res.setHeader('PAYMENT-RESPONSE', paymentResponse);
320
- return this.paymentRequired(req, res, route, price, settled.errorReason ?? 'payment_failed');
321
- });
388
+ return this.serve(payment, paymentResponse);
389
+ }
390
+ res.setHeader('PAYMENT-RESPONSE', paymentResponse);
391
+ if (settled.errorReason === 'settlement_pending') {
392
+ this.log.info(
393
+ { transaction: settled.transaction },
394
+ 'settlement pending; client should retry with the same payment',
395
+ );
396
+ return this.answerPending(res, settled.transaction);
397
+ }
398
+ if (settled.transaction === '') {
399
+ // Rejected before reaching the network: the proof was not consumed.
400
+ // Only the request that created the row removes it: a concurrent
401
+ // request with the same payment may be settling it right now.
402
+ if (created) store.forget(key, 'settling');
403
+ } else {
404
+ store.transition(key, 'settling', 'failed', { paymentResponse });
405
+ }
406
+ this.log.warn({ reason: settled.errorReason }, 'payment failed');
407
+ return this.paymentRequired(payment, settled.errorReason ?? 'payment_failed');
322
408
  }
323
409
 
324
410
  /** authorization flow: verify → handler (response stored) → settle → send. */
325
- private async authorize(
326
- req: Request,
327
- res: Response,
328
- route: PaidRouteOptions,
329
- price: { asset: string; amount: string },
330
- key: string,
331
- payload: PaymentPayload,
332
- accepted: PaymentRequirements,
333
- handler: PaidHandler,
334
- ): Promise<void> {
411
+ private async authorize(payment: Payment): Promise<void> {
412
+ const { res, key } = payment;
335
413
  const store = this.options.store;
336
414
  let verified;
337
415
  try {
338
- verified = await this.options.facilitator.verify(payload, accepted);
416
+ verified = await this.options.facilitator.verify(payment.payload, payment.accepted);
339
417
  } catch (err) {
340
418
  this.log.warn({ err }, 'facilitator unreachable during verify');
341
419
  store.forget(key, 'settling');
@@ -344,13 +422,7 @@ export class ObytePaywall {
344
422
  }
345
423
  if (!verified.isValid) {
346
424
  store.forget(key, 'settling');
347
- return this.paymentRequired(
348
- req,
349
- res,
350
- route,
351
- price,
352
- verified.invalidReason ?? 'invalid_payment',
353
- );
425
+ return this.paymentRequired(payment, verified.invalidReason ?? 'invalid_payment');
354
426
  }
355
427
  if (!store.transition(key, 'settling', 'serving')) {
356
428
  res.status(409).json({ error: 'payment_in_progress' });
@@ -358,7 +430,7 @@ export class ObytePaywall {
358
430
  }
359
431
  let result: PaidResult;
360
432
  try {
361
- result = await handler(req);
433
+ result = await payment.handler(payment.req);
362
434
  } catch (err) {
363
435
  store.transition(key, 'serving', 'settling'); // nothing settled; the same payment may retry
364
436
  this.log.error({ err }, 'paid handler failed before settlement');
@@ -366,20 +438,14 @@ export class ObytePaywall {
366
438
  return;
367
439
  }
368
440
  store.transition(key, 'serving', 'awaiting_settle', { response: toStored(result) });
369
- return this.settleServed(req, res, route, price, key, payload, accepted);
441
+ return this.settleServed(payment);
370
442
  }
371
443
 
372
- private async settleServed(
373
- req: Request,
374
- res: Response,
375
- route: PaidRouteOptions,
376
- price: { asset: string; amount: string },
377
- key: string,
378
- payload: PaymentPayload,
379
- accepted: PaymentRequirements,
380
- ): Promise<void> {
444
+ /** authorization flow, after the handler: settle, then send the stored response. */
445
+ private async settleServed(payment: Payment): Promise<void> {
446
+ const { res, key } = payment;
381
447
  const store = this.options.store;
382
- const settled = await this.settleWithWait(payload, accepted);
448
+ const settled = await this.settleWithWait(payment.payload, payment.accepted);
383
449
  const paymentResponse = encodePaymentResponseHeader(settled);
384
450
  if (settled.success) {
385
451
  store.transition(key, 'awaiting_settle', 'served', { paymentResponse });
@@ -392,25 +458,25 @@ export class ObytePaywall {
392
458
  }
393
459
  res.setHeader('PAYMENT-RESPONSE', paymentResponse);
394
460
  if (settled.errorReason === 'settlement_pending') {
395
- res.setHeader('Retry-After', '5');
396
- res.status(402).json({ error: 'settlement_pending', transaction: settled.transaction });
397
- return;
461
+ return this.answerPending(res, settled.transaction);
398
462
  }
399
463
  store.transition(key, 'awaiting_settle', 'failed', { paymentResponse });
400
464
  this.log.warn(
401
465
  { reason: settled.errorReason },
402
466
  'payment failed AFTER the handler ran (authorization flow); response withheld',
403
467
  );
404
- return this.paymentRequired(req, res, route, price, settled.errorReason ?? 'payment_failed');
468
+ return this.paymentRequired(payment, settled.errorReason ?? 'payment_failed');
405
469
  }
406
470
 
407
- private async serve(
408
- req: Request,
409
- res: Response,
410
- key: string,
411
- paymentResponse: string | null,
412
- handler: PaidHandler,
413
- ): Promise<void> {
471
+ /** Not final yet: the client retries with the same payment. */
472
+ private answerPending(res: Response, transaction: string): void {
473
+ res.setHeader('Retry-After', '5');
474
+ res.status(402).json({ error: 'settlement_pending', transaction });
475
+ }
476
+
477
+ /** Runs the handler once for a settled payment and stores its response. */
478
+ private async serve(payment: Payment, paymentResponse: string | null): Promise<void> {
479
+ const { res, key } = payment;
414
480
  const store = this.options.store;
415
481
  if (!store.transition(key, 'paid_not_served', 'serving')) {
416
482
  res.status(409).json({ error: 'payment_in_progress' });
@@ -418,7 +484,7 @@ export class ObytePaywall {
418
484
  }
419
485
  let result: PaidResult;
420
486
  try {
421
- result = await handler(req);
487
+ result = await payment.handler(payment.req);
422
488
  } catch (err) {
423
489
  store.transition(key, 'serving', 'paid_not_served');
424
490
  this.log.error(
@@ -452,13 +518,7 @@ export class ObytePaywall {
452
518
  res.status(response.status).end(Buffer.from(response.body));
453
519
  }
454
520
 
455
- private paymentRequired(
456
- req: Request,
457
- res: Response,
458
- route: PaidRouteOptions,
459
- price: { asset: string; amount: string },
460
- error?: string,
461
- ): void {
521
+ private paymentRequired({ req, res, route, price }: PaidCall, error?: string): void {
462
522
  // A refused payment that was never settled: say so explicitly (success:
463
523
  // false, empty transaction) so the client knows it was NOT used and can
464
524
  // release its budget and its channel voucher. A settle failure already
@@ -530,7 +590,7 @@ export class ObytePaywall {
530
590
  };
531
591
  }
532
592
 
533
- private terms(price: { asset: string; amount: string }): PaymentTerms {
593
+ private terms(price: Price): PaymentTerms {
534
594
  return { network: this.options.network, payTo: this.options.payTo, ...price };
535
595
  }
536
596