@vallum/receipts 0.0.0-prerelease → 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/dist/index.d.ts +64 -0
- package/dist/index.js +170 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -24,6 +24,33 @@ export interface EscrowReceipt {
|
|
|
24
24
|
readonly events: readonly ReceiptEvent[];
|
|
25
25
|
readonly externalPayment?: LinkedReceiptState;
|
|
26
26
|
readonly iotaReceipt?: LinkedReceiptState;
|
|
27
|
+
readonly escrowSettlement?: EscrowSettlementState;
|
|
28
|
+
}
|
|
29
|
+
export type EscrowSettlementRail = "local" | "iota-testnet" | "iota-mainnet";
|
|
30
|
+
export type EscrowSettlementReleaseMode = "proof" | "acceptance" | "review";
|
|
31
|
+
export interface EscrowSettlementState {
|
|
32
|
+
readonly status: "open" | "released" | "refunded";
|
|
33
|
+
readonly settlementRail: EscrowSettlementRail;
|
|
34
|
+
readonly escrowId: string;
|
|
35
|
+
readonly releaseMode: EscrowSettlementReleaseMode;
|
|
36
|
+
readonly invocationId: string;
|
|
37
|
+
readonly actionId: string;
|
|
38
|
+
readonly actionContractId: string;
|
|
39
|
+
readonly actionContractVersion: string;
|
|
40
|
+
readonly providerPayoutRef: string;
|
|
41
|
+
readonly platformFeeRef: string;
|
|
42
|
+
readonly refundDestinationRef: string;
|
|
43
|
+
readonly providerNetAmount: ReceiptAmount;
|
|
44
|
+
readonly platformFeeAmount: ReceiptAmount;
|
|
45
|
+
readonly openedTransactionDigest: string;
|
|
46
|
+
readonly providerExecutionReceiptHash?: string;
|
|
47
|
+
readonly evidenceAttestationHash?: string;
|
|
48
|
+
readonly settlementReceiptHash?: string;
|
|
49
|
+
readonly buyerFacingReceiptHash?: string;
|
|
50
|
+
readonly settlementTransactionDigest?: string;
|
|
51
|
+
readonly releaseProofHash?: string;
|
|
52
|
+
readonly refundReason?: string;
|
|
53
|
+
readonly platformFeePaid?: boolean;
|
|
27
54
|
}
|
|
28
55
|
export interface PayPerCallReceipt {
|
|
29
56
|
readonly receiptId: string;
|
|
@@ -228,6 +255,40 @@ export interface CreateSubscriptionReceiptInput {
|
|
|
228
255
|
export interface TransitionOptions {
|
|
229
256
|
readonly at: Date;
|
|
230
257
|
}
|
|
258
|
+
export interface RecordEscrowSettlementOpenOptions extends TransitionOptions {
|
|
259
|
+
readonly settlementRail: EscrowSettlementRail;
|
|
260
|
+
readonly escrowId: string;
|
|
261
|
+
readonly releaseMode: EscrowSettlementReleaseMode;
|
|
262
|
+
readonly invocationId: string;
|
|
263
|
+
readonly actionId: string;
|
|
264
|
+
readonly actionContractId: string;
|
|
265
|
+
readonly actionContractVersion: string;
|
|
266
|
+
readonly providerPayoutRef: string;
|
|
267
|
+
readonly platformFeeRef: string;
|
|
268
|
+
readonly refundDestinationRef: string;
|
|
269
|
+
readonly providerNetAmount: ReceiptAmount;
|
|
270
|
+
readonly platformFeeAmount: ReceiptAmount;
|
|
271
|
+
readonly transactionDigest: string;
|
|
272
|
+
}
|
|
273
|
+
export interface RecordEscrowSettlementReleaseOptions extends TransitionOptions {
|
|
274
|
+
readonly verifierId: string;
|
|
275
|
+
readonly escrowId: string;
|
|
276
|
+
readonly invocationId: string;
|
|
277
|
+
readonly releaseProofHash: string;
|
|
278
|
+
readonly providerExecutionReceiptHash: string;
|
|
279
|
+
readonly evidenceAttestationHash: string;
|
|
280
|
+
readonly settlementReceiptHash: string;
|
|
281
|
+
readonly buyerFacingReceiptHash: string;
|
|
282
|
+
readonly transactionDigest: string;
|
|
283
|
+
}
|
|
284
|
+
export interface RecordEscrowSettlementRefundOptions extends TransitionOptions {
|
|
285
|
+
readonly escrowId: string;
|
|
286
|
+
readonly invocationId: string;
|
|
287
|
+
readonly reason: string;
|
|
288
|
+
readonly settlementReceiptHash: string;
|
|
289
|
+
readonly buyerFacingReceiptHash: string;
|
|
290
|
+
readonly transactionDigest: string;
|
|
291
|
+
}
|
|
231
292
|
export declare class ReceiptTransitionError extends Error {
|
|
232
293
|
readonly code: "INVALID_TRANSITION" | "UNAUTHORIZED_VERIFIER";
|
|
233
294
|
constructor(code: "INVALID_TRANSITION" | "UNAUTHORIZED_VERIFIER", message: string);
|
|
@@ -363,6 +424,9 @@ export declare function refundEscrow(receipt: EscrowReceipt, options: Transition
|
|
|
363
424
|
export declare function expireEscrow(receipt: EscrowReceipt, options: TransitionOptions & {
|
|
364
425
|
readonly reason: string;
|
|
365
426
|
}): EscrowReceipt;
|
|
427
|
+
export declare function recordEscrowSettlementOpen(receipt: EscrowReceipt, options: RecordEscrowSettlementOpenOptions): EscrowReceipt;
|
|
428
|
+
export declare function recordEscrowSettlementRelease(receipt: EscrowReceipt, options: RecordEscrowSettlementReleaseOptions): EscrowReceipt;
|
|
429
|
+
export declare function recordEscrowSettlementRefund(receipt: EscrowReceipt, options: RecordEscrowSettlementRefundOptions): EscrowReceipt;
|
|
366
430
|
export declare function linkExternalPaymentState(receipt: EscrowReceipt, state: LinkedReceiptState): EscrowReceipt;
|
|
367
431
|
export declare function linkIotaReceiptState(receipt: EscrowReceipt, state: LinkedReceiptState): EscrowReceipt;
|
|
368
432
|
export * from "./x402Receipt.js";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export * from "./ap2Receipt.js";
|
|
2
|
+
const escrowSettlementRails = ["local", "iota-testnet", "iota-mainnet"];
|
|
3
|
+
const escrowSettlementReleaseModes = ["proof", "acceptance", "review"];
|
|
2
4
|
export class ReceiptTransitionError extends Error {
|
|
3
5
|
code;
|
|
4
6
|
constructor(code, message) {
|
|
@@ -457,6 +459,97 @@ export function expireEscrow(receipt, options) {
|
|
|
457
459
|
refundReason: options.reason,
|
|
458
460
|
}, options.reason);
|
|
459
461
|
}
|
|
462
|
+
export function recordEscrowSettlementOpen(receipt, options) {
|
|
463
|
+
requireReceiptStatus(receipt, ["sponsored"], "record escrow settlement open");
|
|
464
|
+
if (receipt.escrowSettlement) {
|
|
465
|
+
throw new ReceiptTransitionError("INVALID_TRANSITION", "Escrow settlement is already recorded.");
|
|
466
|
+
}
|
|
467
|
+
requireEscrowSettlementRail(options.settlementRail);
|
|
468
|
+
requireEscrowSettlementReleaseMode(options.releaseMode);
|
|
469
|
+
requireNonEmpty(options.escrowId, "escrowId");
|
|
470
|
+
requireNonEmpty(options.invocationId, "invocationId");
|
|
471
|
+
requireNonEmpty(options.actionId, "actionId");
|
|
472
|
+
requireNonEmpty(options.actionContractId, "actionContractId");
|
|
473
|
+
requireNonEmpty(options.actionContractVersion, "actionContractVersion");
|
|
474
|
+
requireNonEmpty(options.transactionDigest, "transactionDigest");
|
|
475
|
+
requireSafeSettlementReference(options.providerPayoutRef, "providerPayoutRef");
|
|
476
|
+
requireSafeSettlementReference(options.platformFeeRef, "platformFeeRef");
|
|
477
|
+
requireSafeSettlementReference(options.refundDestinationRef, "refundDestinationRef");
|
|
478
|
+
requireFeeSplit(receipt.amount, options.providerNetAmount, options.platformFeeAmount);
|
|
479
|
+
return withReceiptEvent(receipt, "submitted", options.at, {
|
|
480
|
+
status: "submitted",
|
|
481
|
+
transactionDigest: options.transactionDigest,
|
|
482
|
+
escrowSettlement: {
|
|
483
|
+
status: "open",
|
|
484
|
+
settlementRail: options.settlementRail,
|
|
485
|
+
escrowId: options.escrowId,
|
|
486
|
+
releaseMode: options.releaseMode,
|
|
487
|
+
invocationId: options.invocationId,
|
|
488
|
+
actionId: options.actionId,
|
|
489
|
+
actionContractId: options.actionContractId,
|
|
490
|
+
actionContractVersion: options.actionContractVersion,
|
|
491
|
+
providerPayoutRef: options.providerPayoutRef,
|
|
492
|
+
platformFeeRef: options.platformFeeRef,
|
|
493
|
+
refundDestinationRef: options.refundDestinationRef,
|
|
494
|
+
providerNetAmount: options.providerNetAmount,
|
|
495
|
+
platformFeeAmount: options.platformFeeAmount,
|
|
496
|
+
openedTransactionDigest: options.transactionDigest,
|
|
497
|
+
},
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
export function recordEscrowSettlementRelease(receipt, options) {
|
|
501
|
+
const settlement = requireOpenEscrowSettlement(receipt);
|
|
502
|
+
requireSettlementBinding(settlement, options.escrowId, options.invocationId);
|
|
503
|
+
requireSafeHashReference(options.releaseProofHash, "releaseProofHash");
|
|
504
|
+
requireSafeHashReference(options.providerExecutionReceiptHash, "providerExecutionReceiptHash");
|
|
505
|
+
requireSafeHashReference(options.evidenceAttestationHash, "evidenceAttestationHash");
|
|
506
|
+
requireSafeHashReference(options.settlementReceiptHash, "settlementReceiptHash");
|
|
507
|
+
requireSafeHashReference(options.buyerFacingReceiptHash, "buyerFacingReceiptHash");
|
|
508
|
+
requireNonEmpty(options.transactionDigest, "transactionDigest");
|
|
509
|
+
const released = releaseEscrow(receipt, {
|
|
510
|
+
at: options.at,
|
|
511
|
+
verifierId: options.verifierId,
|
|
512
|
+
releaseProofHash: options.releaseProofHash,
|
|
513
|
+
});
|
|
514
|
+
return {
|
|
515
|
+
...released,
|
|
516
|
+
escrowSettlement: {
|
|
517
|
+
...settlement,
|
|
518
|
+
status: "released",
|
|
519
|
+
releaseProofHash: options.releaseProofHash,
|
|
520
|
+
providerExecutionReceiptHash: options.providerExecutionReceiptHash,
|
|
521
|
+
evidenceAttestationHash: options.evidenceAttestationHash,
|
|
522
|
+
settlementReceiptHash: options.settlementReceiptHash,
|
|
523
|
+
buyerFacingReceiptHash: options.buyerFacingReceiptHash,
|
|
524
|
+
settlementTransactionDigest: options.transactionDigest,
|
|
525
|
+
platformFeePaid: true,
|
|
526
|
+
},
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
export function recordEscrowSettlementRefund(receipt, options) {
|
|
530
|
+
const settlement = requireOpenEscrowSettlement(receipt);
|
|
531
|
+
requireSettlementBinding(settlement, options.escrowId, options.invocationId);
|
|
532
|
+
requireNonEmpty(options.reason, "reason");
|
|
533
|
+
requireSafeHashReference(options.settlementReceiptHash, "settlementReceiptHash");
|
|
534
|
+
requireSafeHashReference(options.buyerFacingReceiptHash, "buyerFacingReceiptHash");
|
|
535
|
+
requireNonEmpty(options.transactionDigest, "transactionDigest");
|
|
536
|
+
const refunded = refundEscrow(receipt, {
|
|
537
|
+
at: options.at,
|
|
538
|
+
reason: options.reason,
|
|
539
|
+
});
|
|
540
|
+
return {
|
|
541
|
+
...refunded,
|
|
542
|
+
escrowSettlement: {
|
|
543
|
+
...settlement,
|
|
544
|
+
status: "refunded",
|
|
545
|
+
settlementReceiptHash: options.settlementReceiptHash,
|
|
546
|
+
buyerFacingReceiptHash: options.buyerFacingReceiptHash,
|
|
547
|
+
settlementTransactionDigest: options.transactionDigest,
|
|
548
|
+
refundReason: options.reason,
|
|
549
|
+
platformFeePaid: false,
|
|
550
|
+
},
|
|
551
|
+
};
|
|
552
|
+
}
|
|
460
553
|
export function linkExternalPaymentState(receipt, state) {
|
|
461
554
|
return {
|
|
462
555
|
...receipt,
|
|
@@ -480,13 +573,35 @@ function requireEscrowOpen(receipt, action) {
|
|
|
480
573
|
throw new ReceiptTransitionError("INVALID_TRANSITION", `Cannot ${action} escrow from ${receipt.escrow.status}.`);
|
|
481
574
|
}
|
|
482
575
|
}
|
|
576
|
+
function requireOpenEscrowSettlement(receipt) {
|
|
577
|
+
const settlement = receipt.escrowSettlement;
|
|
578
|
+
if (!settlement || settlement.status !== "open") {
|
|
579
|
+
throw new ReceiptTransitionError("INVALID_TRANSITION", "Escrow settlement must be open.");
|
|
580
|
+
}
|
|
581
|
+
return settlement;
|
|
582
|
+
}
|
|
583
|
+
function requireSettlementBinding(settlement, escrowId, invocationId) {
|
|
584
|
+
if (settlement.escrowId !== escrowId || settlement.invocationId !== invocationId) {
|
|
585
|
+
throw new ReceiptInputError("FIELD_REQUIRED", "Escrow settlement binding does not match the receipt.");
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
function requireEscrowSettlementRail(value) {
|
|
589
|
+
if (!escrowSettlementRails.includes(value)) {
|
|
590
|
+
throw new ReceiptInputError("FIELD_REQUIRED", "settlementRail must be a supported escrow settlement rail.");
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
function requireEscrowSettlementReleaseMode(value) {
|
|
594
|
+
if (!escrowSettlementReleaseModes.includes(value)) {
|
|
595
|
+
throw new ReceiptInputError("FIELD_REQUIRED", "releaseMode must be a supported escrow settlement release mode.");
|
|
596
|
+
}
|
|
597
|
+
}
|
|
483
598
|
function requireReceiptStatus(receipt, allowed, action) {
|
|
484
599
|
if (!allowed.includes(receipt.status)) {
|
|
485
600
|
throw new ReceiptTransitionError("INVALID_TRANSITION", `Cannot ${action} receipt from ${receipt.status}.`);
|
|
486
601
|
}
|
|
487
602
|
}
|
|
488
603
|
function requireNonEmpty(value, field) {
|
|
489
|
-
if (value.trim() === "") {
|
|
604
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
490
605
|
throw new ReceiptInputError("FIELD_REQUIRED", `${field} is required.`);
|
|
491
606
|
}
|
|
492
607
|
}
|
|
@@ -497,6 +612,60 @@ function requireSafeHashReference(value, field) {
|
|
|
497
612
|
throw new ReceiptInputError("FIELD_REQUIRED", `${field} must be a safe sha256 reference.`);
|
|
498
613
|
}
|
|
499
614
|
}
|
|
615
|
+
function requireSafeSettlementReference(value, field) {
|
|
616
|
+
requireNonEmpty(value, field);
|
|
617
|
+
if (value.length > 160 ||
|
|
618
|
+
!/^[A-Za-z0-9._:-]+$/.test(value) ||
|
|
619
|
+
/^0x[a-f0-9]{16,}$/i.test(value) ||
|
|
620
|
+
/^(iota|sui)1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{20,}$/i.test(value) ||
|
|
621
|
+
/(private prompt|review payload|bearer|access-token|signer_ref|payment credential|privateKey|mnemonic|seed|raw transaction|user signature)/i.test(value)) {
|
|
622
|
+
throw new ReceiptInputError("FIELD_REQUIRED", `${field} must be an opaque public reference, not raw settlement material.`);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
function requireFeeSplit(gross, providerNet, platformFee) {
|
|
626
|
+
requireReceiptAmount(gross, "amount");
|
|
627
|
+
requireReceiptAmount(providerNet, "providerNetAmount");
|
|
628
|
+
requireReceiptAmount(platformFee, "platformFeeAmount");
|
|
629
|
+
if (gross.asset !== providerNet.asset || gross.asset !== platformFee.asset) {
|
|
630
|
+
throw new ReceiptInputError("FIELD_REQUIRED", "Escrow fee split assets must match the gross amount asset.");
|
|
631
|
+
}
|
|
632
|
+
const grossAmount = parseDecimalAmount(gross.amount, "amount");
|
|
633
|
+
const providerNetAmount = parseDecimalAmount(providerNet.amount, "providerNetAmount");
|
|
634
|
+
const platformFeeAmount = parseDecimalAmount(platformFee.amount, "platformFeeAmount");
|
|
635
|
+
const scale = Math.max(grossAmount.scale, providerNetAmount.scale, platformFeeAmount.scale);
|
|
636
|
+
const grossUnits = scaleDecimal(grossAmount, scale);
|
|
637
|
+
const providerUnits = scaleDecimal(providerNetAmount, scale);
|
|
638
|
+
const platformUnits = scaleDecimal(platformFeeAmount, scale);
|
|
639
|
+
if (providerUnits + platformUnits !== grossUnits) {
|
|
640
|
+
throw new ReceiptInputError("FIELD_REQUIRED", "Escrow fee split must equal the gross receipt amount.");
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
function requireReceiptAmount(value, field) {
|
|
644
|
+
if (!value || typeof value !== "object") {
|
|
645
|
+
throw new ReceiptInputError("FIELD_REQUIRED", `${field} is required.`);
|
|
646
|
+
}
|
|
647
|
+
requireNonEmpty(value.amount, `${field}.amount`);
|
|
648
|
+
requireNonEmpty(value.asset, `${field}.asset`);
|
|
649
|
+
}
|
|
650
|
+
function parseDecimalAmount(value, field) {
|
|
651
|
+
if (value.length > 80) {
|
|
652
|
+
throw new ReceiptInputError("FIELD_REQUIRED", `${field} is too large.`);
|
|
653
|
+
}
|
|
654
|
+
if (!/^(0|[1-9][0-9]*)(\.[0-9]+)?$/.test(value)) {
|
|
655
|
+
throw new ReceiptInputError("FIELD_REQUIRED", `${field} must be a non-negative decimal amount.`);
|
|
656
|
+
}
|
|
657
|
+
const [whole, fraction = ""] = value.split(".");
|
|
658
|
+
if (fraction.length > 18) {
|
|
659
|
+
throw new ReceiptInputError("FIELD_REQUIRED", `${field} has too many decimal places.`);
|
|
660
|
+
}
|
|
661
|
+
return {
|
|
662
|
+
units: BigInt(`${whole}${fraction}`),
|
|
663
|
+
scale: fraction.length,
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
function scaleDecimal(value, targetScale) {
|
|
667
|
+
return value.units * (10n ** BigInt(targetScale - value.scale));
|
|
668
|
+
}
|
|
500
669
|
function requireMatchingField(left, right, leftField, rightField) {
|
|
501
670
|
if (left !== right) {
|
|
502
671
|
throw new ReceiptInputError("FIELD_REQUIRED", `${leftField} must match ${rightField}.`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vallum/receipts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -27,6 +27,6 @@
|
|
|
27
27
|
},
|
|
28
28
|
"publishConfig": {
|
|
29
29
|
"access": "public",
|
|
30
|
-
"tag": "
|
|
30
|
+
"tag": "latest"
|
|
31
31
|
}
|
|
32
32
|
}
|