@vallum/sdk 0.0.1-prerelease.0 → 0.1.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.
package/README.md CHANGED
@@ -4,10 +4,10 @@ TypeScript client scaffold for applications integrating with an Vallum policy ga
4
4
 
5
5
  ## Install
6
6
 
7
- For the npm prerelease, install:
7
+ For the npm release, install:
8
8
 
9
9
  ```sh
10
- npm install @vallum/sdk@next
10
+ npm install @vallum/sdk
11
11
  ```
12
12
 
13
13
  See the full package selection and configuration guide in the repository:
@@ -0,0 +1,92 @@
1
+ import { type EscrowReceipt, type EscrowSettlementRail, type EscrowSettlementReleaseMode, type ReceiptAmount } from "@vallum/receipts";
2
+ export type EscrowSettlementErrorCode = "IDEMPOTENCY_REPLAYED" | "ESCROW_BINDING_MISMATCH" | "ESCROW_SETTLEMENT_NOT_OPEN" | "ESCROW_STORE_CONFLICT";
3
+ export declare class EscrowSettlementError extends Error {
4
+ readonly code: EscrowSettlementErrorCode;
5
+ constructor(code: EscrowSettlementErrorCode, message: string);
6
+ }
7
+ export interface IotaEscrowOpenExecutionRequest {
8
+ readonly receipt: EscrowReceipt;
9
+ readonly settlementRail: EscrowSettlementRail;
10
+ readonly releaseMode: EscrowSettlementReleaseMode;
11
+ readonly invocationId: string;
12
+ readonly actionId: string;
13
+ readonly actionContractId: string;
14
+ readonly actionContractVersion: string;
15
+ readonly providerPayoutRef: string;
16
+ readonly platformFeeRef: string;
17
+ readonly refundDestinationRef: string;
18
+ readonly providerNetAmount: ReceiptAmount;
19
+ readonly platformFeeAmount: ReceiptAmount;
20
+ }
21
+ export interface IotaEscrowReleaseExecutionRequest {
22
+ readonly receipt: EscrowReceipt;
23
+ readonly verifierId: string;
24
+ readonly escrowId: string;
25
+ readonly invocationId: string;
26
+ readonly releaseProofHash: string;
27
+ readonly providerExecutionReceiptHash: string;
28
+ readonly evidenceAttestationHash: string;
29
+ readonly settlementReceiptHash: string;
30
+ readonly buyerFacingReceiptHash: string;
31
+ }
32
+ export interface IotaEscrowRefundExecutionRequest {
33
+ readonly receipt: EscrowReceipt;
34
+ readonly escrowId: string;
35
+ readonly invocationId: string;
36
+ readonly reason: string;
37
+ readonly settlementReceiptHash: string;
38
+ readonly buyerFacingReceiptHash: string;
39
+ }
40
+ export interface IotaEscrowOpenExecutionResult {
41
+ readonly escrowId: string;
42
+ readonly transactionDigest: string;
43
+ }
44
+ export interface IotaEscrowSettlementExecutionResult {
45
+ readonly transactionDigest: string;
46
+ }
47
+ export interface IotaEscrowSettlementExecutor {
48
+ readonly open: (request: IotaEscrowOpenExecutionRequest) => Promise<IotaEscrowOpenExecutionResult>;
49
+ readonly release: (request: IotaEscrowReleaseExecutionRequest) => Promise<IotaEscrowSettlementExecutionResult>;
50
+ readonly refund: (request: IotaEscrowRefundExecutionRequest) => Promise<IotaEscrowSettlementExecutionResult>;
51
+ }
52
+ export interface EscrowSettlementStoreRecord {
53
+ readonly idempotencyKey: string;
54
+ readonly receiptId: string;
55
+ readonly agentId: string;
56
+ readonly ownerId: string;
57
+ readonly providerId: string;
58
+ readonly verifierId: string;
59
+ readonly escrowId: string;
60
+ readonly invocationId: string;
61
+ readonly status: "open" | "released" | "refunded";
62
+ }
63
+ export interface EscrowSettlementStore {
64
+ readonly getByIdempotencyKey: (idempotencyKey: string) => Promise<EscrowSettlementStoreRecord | undefined>;
65
+ readonly getByEscrowId: (escrowId: string) => Promise<EscrowSettlementStoreRecord | undefined>;
66
+ /**
67
+ * Must reject conflicting idempotency-key or escrow-id bindings. A live executor
68
+ * should back this with a durable conditional write before funds can move.
69
+ */
70
+ readonly put: (record: EscrowSettlementStoreRecord) => Promise<void>;
71
+ }
72
+ export interface IotaEscrowSettlementClientOptions {
73
+ readonly executor: IotaEscrowSettlementExecutor;
74
+ readonly store?: EscrowSettlementStore;
75
+ readonly now?: () => Date;
76
+ }
77
+ export type IotaEscrowOpenInput = IotaEscrowOpenExecutionRequest;
78
+ export type IotaEscrowReleaseInput = IotaEscrowReleaseExecutionRequest;
79
+ export type IotaEscrowRefundInput = IotaEscrowRefundExecutionRequest;
80
+ export interface IotaEscrowSettlementClientResult {
81
+ readonly receipt: EscrowReceipt;
82
+ }
83
+ export interface IotaEscrowOpenResult extends IotaEscrowSettlementClientResult {
84
+ readonly escrowId: string;
85
+ readonly transactionDigest: string;
86
+ }
87
+ export declare function createInMemoryEscrowSettlementStore(): EscrowSettlementStore;
88
+ export declare function createIotaEscrowSettlementClient(options: IotaEscrowSettlementClientOptions): {
89
+ open(input: IotaEscrowOpenInput): Promise<IotaEscrowOpenResult>;
90
+ release(input: IotaEscrowReleaseInput): Promise<IotaEscrowSettlementClientResult>;
91
+ refund(input: IotaEscrowRefundInput): Promise<IotaEscrowSettlementClientResult>;
92
+ };
@@ -0,0 +1,225 @@
1
+ import { recordEscrowSettlementOpen, recordEscrowSettlementRefund, recordEscrowSettlementRelease, } from "@vallum/receipts";
2
+ export class EscrowSettlementError extends Error {
3
+ code;
4
+ constructor(code, message) {
5
+ super(message);
6
+ this.code = code;
7
+ this.name = "EscrowSettlementError";
8
+ }
9
+ }
10
+ export function createInMemoryEscrowSettlementStore() {
11
+ const byIdempotencyKey = new Map();
12
+ const byEscrowId = new Map();
13
+ return {
14
+ async getByIdempotencyKey(idempotencyKey) {
15
+ return byIdempotencyKey.get(idempotencyKey);
16
+ },
17
+ async getByEscrowId(escrowId) {
18
+ return byEscrowId.get(escrowId);
19
+ },
20
+ async put(record) {
21
+ const existingByIdempotencyKey = byIdempotencyKey.get(record.idempotencyKey);
22
+ if (existingByIdempotencyKey && !hasSameEscrowStoreBinding(existingByIdempotencyKey, record)) {
23
+ throw new EscrowSettlementError("ESCROW_STORE_CONFLICT", "Escrow idempotency key is already bound to another settlement.");
24
+ }
25
+ const existingByEscrowId = byEscrowId.get(record.escrowId);
26
+ if (existingByEscrowId && !hasSameEscrowStoreBinding(existingByEscrowId, record)) {
27
+ throw new EscrowSettlementError("ESCROW_STORE_CONFLICT", "Escrow id is already bound to another receipt.");
28
+ }
29
+ byIdempotencyKey.set(record.idempotencyKey, record);
30
+ byEscrowId.set(record.escrowId, record);
31
+ },
32
+ };
33
+ }
34
+ export function createIotaEscrowSettlementClient(options) {
35
+ const store = options.store ?? createInMemoryEscrowSettlementStore();
36
+ const now = () => options.now?.() ?? new Date();
37
+ const openingIdempotencyKeys = new Set();
38
+ return {
39
+ async open(input) {
40
+ const idempotencyKey = input.receipt.idempotencyKey;
41
+ if (openingIdempotencyKeys.has(idempotencyKey)) {
42
+ throw new EscrowSettlementError("IDEMPOTENCY_REPLAYED", "Escrow idempotency key has already been used.");
43
+ }
44
+ openingIdempotencyKeys.add(idempotencyKey);
45
+ try {
46
+ const existing = await store.getByIdempotencyKey(idempotencyKey);
47
+ if (existing) {
48
+ throw new EscrowSettlementError("IDEMPOTENCY_REPLAYED", "Escrow idempotency key has already been used.");
49
+ }
50
+ preflightEscrowSettlementOpen(input);
51
+ const opened = await options.executor.open(input);
52
+ const receipt = recordEscrowSettlementOpen(input.receipt, {
53
+ at: now(),
54
+ settlementRail: input.settlementRail,
55
+ escrowId: opened.escrowId,
56
+ releaseMode: input.releaseMode,
57
+ invocationId: input.invocationId,
58
+ actionId: input.actionId,
59
+ actionContractId: input.actionContractId,
60
+ actionContractVersion: input.actionContractVersion,
61
+ providerPayoutRef: input.providerPayoutRef,
62
+ platformFeeRef: input.platformFeeRef,
63
+ refundDestinationRef: input.refundDestinationRef,
64
+ providerNetAmount: input.providerNetAmount,
65
+ platformFeeAmount: input.platformFeeAmount,
66
+ transactionDigest: opened.transactionDigest,
67
+ });
68
+ await store.put(toEscrowSettlementStoreRecord({
69
+ receipt: input.receipt,
70
+ escrowId: opened.escrowId,
71
+ invocationId: input.invocationId,
72
+ status: "open",
73
+ }));
74
+ return {
75
+ receipt,
76
+ escrowId: opened.escrowId,
77
+ transactionDigest: opened.transactionDigest,
78
+ };
79
+ }
80
+ finally {
81
+ openingIdempotencyKeys.delete(idempotencyKey);
82
+ }
83
+ },
84
+ async release(input) {
85
+ const record = await requireOpenStoreRecord(store, input.escrowId, input.invocationId);
86
+ requireStoreReceiptBinding(record, input.receipt);
87
+ requireReceiptBinding(input.receipt, input.escrowId, input.invocationId);
88
+ preflightEscrowSettlementRelease(input);
89
+ const released = await options.executor.release(input);
90
+ const receipt = recordEscrowSettlementRelease(input.receipt, {
91
+ at: now(),
92
+ verifierId: input.verifierId,
93
+ escrowId: input.escrowId,
94
+ invocationId: input.invocationId,
95
+ releaseProofHash: input.releaseProofHash,
96
+ providerExecutionReceiptHash: input.providerExecutionReceiptHash,
97
+ evidenceAttestationHash: input.evidenceAttestationHash,
98
+ settlementReceiptHash: input.settlementReceiptHash,
99
+ buyerFacingReceiptHash: input.buyerFacingReceiptHash,
100
+ transactionDigest: released.transactionDigest,
101
+ });
102
+ await store.put(toEscrowSettlementStoreRecord({
103
+ receipt: input.receipt,
104
+ escrowId: input.escrowId,
105
+ invocationId: input.invocationId,
106
+ status: "released",
107
+ }));
108
+ return { receipt };
109
+ },
110
+ async refund(input) {
111
+ const record = await requireOpenStoreRecord(store, input.escrowId, input.invocationId);
112
+ requireStoreReceiptBinding(record, input.receipt);
113
+ requireReceiptBinding(input.receipt, input.escrowId, input.invocationId);
114
+ preflightEscrowSettlementRefund(input);
115
+ const refunded = await options.executor.refund(input);
116
+ const receipt = recordEscrowSettlementRefund(input.receipt, {
117
+ at: now(),
118
+ escrowId: input.escrowId,
119
+ invocationId: input.invocationId,
120
+ reason: input.reason,
121
+ settlementReceiptHash: input.settlementReceiptHash,
122
+ buyerFacingReceiptHash: input.buyerFacingReceiptHash,
123
+ transactionDigest: refunded.transactionDigest,
124
+ });
125
+ await store.put(toEscrowSettlementStoreRecord({
126
+ receipt: input.receipt,
127
+ escrowId: input.escrowId,
128
+ invocationId: input.invocationId,
129
+ status: "refunded",
130
+ }));
131
+ return { receipt };
132
+ },
133
+ };
134
+ }
135
+ function hasSameEscrowStoreBinding(left, right) {
136
+ return left.idempotencyKey === right.idempotencyKey
137
+ && left.receiptId === right.receiptId
138
+ && left.agentId === right.agentId
139
+ && left.ownerId === right.ownerId
140
+ && left.providerId === right.providerId
141
+ && left.verifierId === right.verifierId
142
+ && left.escrowId === right.escrowId
143
+ && left.invocationId === right.invocationId;
144
+ }
145
+ function toEscrowSettlementStoreRecord(input) {
146
+ return {
147
+ idempotencyKey: input.receipt.idempotencyKey,
148
+ receiptId: input.receipt.receiptId,
149
+ agentId: input.receipt.agentId,
150
+ ownerId: input.receipt.ownerId,
151
+ providerId: input.receipt.escrow.providerId,
152
+ verifierId: input.receipt.escrow.verifierId,
153
+ escrowId: input.escrowId,
154
+ invocationId: input.invocationId,
155
+ status: input.status,
156
+ };
157
+ }
158
+ function preflightEscrowSettlementOpen(input) {
159
+ recordEscrowSettlementOpen(input.receipt, {
160
+ at: new Date(0),
161
+ settlementRail: input.settlementRail,
162
+ escrowId: "preflight-escrow",
163
+ releaseMode: input.releaseMode,
164
+ invocationId: input.invocationId,
165
+ actionId: input.actionId,
166
+ actionContractId: input.actionContractId,
167
+ actionContractVersion: input.actionContractVersion,
168
+ providerPayoutRef: input.providerPayoutRef,
169
+ platformFeeRef: input.platformFeeRef,
170
+ refundDestinationRef: input.refundDestinationRef,
171
+ providerNetAmount: input.providerNetAmount,
172
+ platformFeeAmount: input.platformFeeAmount,
173
+ transactionDigest: "preflight-digest",
174
+ });
175
+ }
176
+ function preflightEscrowSettlementRelease(input) {
177
+ recordEscrowSettlementRelease(input.receipt, {
178
+ at: new Date(0),
179
+ verifierId: input.verifierId,
180
+ escrowId: input.escrowId,
181
+ invocationId: input.invocationId,
182
+ releaseProofHash: input.releaseProofHash,
183
+ providerExecutionReceiptHash: input.providerExecutionReceiptHash,
184
+ evidenceAttestationHash: input.evidenceAttestationHash,
185
+ settlementReceiptHash: input.settlementReceiptHash,
186
+ buyerFacingReceiptHash: input.buyerFacingReceiptHash,
187
+ transactionDigest: "preflight-digest",
188
+ });
189
+ }
190
+ function preflightEscrowSettlementRefund(input) {
191
+ recordEscrowSettlementRefund(input.receipt, {
192
+ at: new Date(0),
193
+ escrowId: input.escrowId,
194
+ invocationId: input.invocationId,
195
+ reason: input.reason,
196
+ settlementReceiptHash: input.settlementReceiptHash,
197
+ buyerFacingReceiptHash: input.buyerFacingReceiptHash,
198
+ transactionDigest: "preflight-digest",
199
+ });
200
+ }
201
+ async function requireOpenStoreRecord(store, escrowId, invocationId) {
202
+ const record = await store.getByEscrowId(escrowId);
203
+ if (!record || record.status !== "open") {
204
+ throw new EscrowSettlementError("ESCROW_SETTLEMENT_NOT_OPEN", "Escrow settlement is not open.");
205
+ }
206
+ if (record.invocationId !== invocationId) {
207
+ throw new EscrowSettlementError("ESCROW_BINDING_MISMATCH", "Escrow invocation binding does not match.");
208
+ }
209
+ return record;
210
+ }
211
+ function requireStoreReceiptBinding(record, receipt) {
212
+ if (record.idempotencyKey !== receipt.idempotencyKey ||
213
+ record.receiptId !== receipt.receiptId ||
214
+ record.agentId !== receipt.agentId ||
215
+ record.ownerId !== receipt.ownerId ||
216
+ record.providerId !== receipt.escrow.providerId ||
217
+ record.verifierId !== receipt.escrow.verifierId) {
218
+ throw new EscrowSettlementError("ESCROW_BINDING_MISMATCH", "Escrow receipt binding does not match the stored settlement.");
219
+ }
220
+ }
221
+ function requireReceiptBinding(receipt, escrowId, invocationId) {
222
+ if (receipt.escrowSettlement?.escrowId !== escrowId || receipt.escrowSettlement.invocationId !== invocationId) {
223
+ throw new EscrowSettlementError("ESCROW_BINDING_MISMATCH", "Escrow proof binding does not match the receipt.");
224
+ }
225
+ }
@@ -0,0 +1,83 @@
1
+ import type { IotaClient } from "@iota/iota-sdk/client";
2
+ import { Transaction, type TransactionObjectInput } from "@iota/iota-sdk/transactions";
3
+ import type { ExecuteSponsoredTransactionRequest, ExecuteSponsoredTransactionResponse, ReserveGasRequest, ReserveGasResponse } from "../types.js";
4
+ import type { IotaEscrowOpenExecutionRequest, IotaEscrowRefundExecutionRequest, IotaEscrowReleaseExecutionRequest, IotaEscrowSettlementExecutor } from "./iotaEscrowSettlement.js";
5
+ export type LiveEscrowSettlementExecutorErrorCode = "ESCROW_EXECUTOR_CONFIG_INVALID" | "ESCROW_EXECUTOR_RESERVE_RESPONSE_INVALID" | "ESCROW_EXECUTOR_EXECUTE_RESPONSE_INVALID" | "ESCROW_EXECUTOR_ESCROW_ID_MISSING";
6
+ export declare class LiveEscrowSettlementExecutorError extends Error {
7
+ readonly code: LiveEscrowSettlementExecutorErrorCode;
8
+ constructor(code: LiveEscrowSettlementExecutorErrorCode, message: string);
9
+ }
10
+ export interface IotaEscrowSettlementSigner {
11
+ readonly address: string;
12
+ readonly signTransaction: (bytes: Uint8Array) => Promise<{
13
+ readonly signature: string;
14
+ }> | {
15
+ readonly signature: string;
16
+ };
17
+ }
18
+ export interface IotaEscrowSettlementGateway {
19
+ readonly reserveGas: (request: ReserveGasRequest) => Promise<ReserveGasResponse>;
20
+ readonly executeSponsoredTransaction: (request: ExecuteSponsoredTransactionRequest) => Promise<ExecuteSponsoredTransactionResponse>;
21
+ }
22
+ export interface IotaEscrowSettlementMoveContract {
23
+ readonly packageId: string;
24
+ readonly moduleName?: string;
25
+ readonly openFunction?: string;
26
+ readonly releaseFunction?: string;
27
+ readonly refundFunction?: string;
28
+ readonly escrowTypeName?: string;
29
+ /**
30
+ * Shared escrows can be released or refunded by the verifier/owner signer.
31
+ * Transfer-to-owner is available for deployments that wrap release/refund in
32
+ * a different access pattern.
33
+ */
34
+ readonly publishEscrowObject?: "share" | "transfer-to-owner" | "none";
35
+ }
36
+ export interface IotaEscrowSettlementParticipants {
37
+ readonly ownerAddress: string;
38
+ readonly providerAddress: string;
39
+ readonly verifierAddress: string;
40
+ }
41
+ export type IotaEscrowSettlementParticipantResolver = (request: IotaEscrowOpenExecutionRequest) => IotaEscrowSettlementParticipants | Promise<IotaEscrowSettlementParticipants>;
42
+ export type IotaEscrowSettlementAmountResolver = (request: IotaEscrowOpenExecutionRequest) => bigint | number | string | Promise<bigint | number | string>;
43
+ export type IotaEscrowSettlementObjectResolver = (request: IotaEscrowReleaseExecutionRequest | IotaEscrowRefundExecutionRequest) => TransactionObjectInput | Promise<TransactionObjectInput>;
44
+ export interface IotaEscrowSettlementExecutionContext {
45
+ readonly operation: "open" | "release" | "refund";
46
+ readonly reservation: ReserveGasResponse;
47
+ readonly execution: ExecuteSponsoredTransactionResponse;
48
+ }
49
+ export type IotaEscrowSettlementEscrowIdExtractor = (context: IotaEscrowSettlementExecutionContext & {
50
+ readonly request: IotaEscrowOpenExecutionRequest;
51
+ }) => string | undefined;
52
+ export interface IotaEscrowSettlementPolicyTarget {
53
+ readonly packageId?: string;
54
+ readonly functionName?: string;
55
+ }
56
+ export type IotaEscrowSettlementPolicyTargetResolver<Request> = (request: Request) => IotaEscrowSettlementPolicyTarget | Promise<IotaEscrowSettlementPolicyTarget>;
57
+ export interface CreateSponsoredIotaEscrowSettlementExecutorOptions {
58
+ readonly gateway: IotaEscrowSettlementGateway;
59
+ readonly contract: IotaEscrowSettlementMoveContract;
60
+ readonly signer: IotaEscrowSettlementSigner;
61
+ readonly iotaClient?: IotaClient;
62
+ readonly gasBudget: number;
63
+ readonly reserveDurationSecs?: number;
64
+ readonly resolveParticipants: IotaEscrowSettlementParticipantResolver;
65
+ readonly amountToBaseUnits: IotaEscrowSettlementAmountResolver;
66
+ readonly resolveEscrowObject?: IotaEscrowSettlementObjectResolver;
67
+ readonly extractEscrowId?: IotaEscrowSettlementEscrowIdExtractor;
68
+ readonly policyTargetForOpen?: IotaEscrowSettlementPolicyTargetResolver<IotaEscrowOpenExecutionRequest>;
69
+ readonly policyTargetForRelease?: IotaEscrowSettlementPolicyTargetResolver<IotaEscrowReleaseExecutionRequest>;
70
+ readonly policyTargetForRefund?: IotaEscrowSettlementPolicyTargetResolver<IotaEscrowRefundExecutionRequest>;
71
+ /**
72
+ * Unit-test escape hatch for deterministic transaction bytes.
73
+ *
74
+ * Live executors should pass `iotaClient` and let the IOTA SDK build bytes
75
+ * from the configured Move call. This hook can bypass that boundary, so it
76
+ * also requires `allowUnsafeCustomTransactionBuilder: true`.
77
+ */
78
+ readonly unsafeBuildTransactionBytesForTesting?: (tx: Transaction, context: {
79
+ readonly operation: "open" | "release" | "refund";
80
+ }) => Promise<Uint8Array> | Uint8Array;
81
+ readonly allowUnsafeCustomTransactionBuilder?: boolean;
82
+ }
83
+ export declare function createSponsoredIotaEscrowSettlementExecutor(options: CreateSponsoredIotaEscrowSettlementExecutorOptions): IotaEscrowSettlementExecutor;
@@ -0,0 +1,287 @@
1
+ import { toBase64 } from "@iota/bcs";
2
+ import { Transaction } from "@iota/iota-sdk/transactions";
3
+ export class LiveEscrowSettlementExecutorError extends Error {
4
+ code;
5
+ constructor(code, message) {
6
+ super(message);
7
+ this.code = code;
8
+ this.name = "LiveEscrowSettlementExecutorError";
9
+ }
10
+ }
11
+ const DEFAULT_MODULE = "escrow";
12
+ const DEFAULT_OPEN_FUNCTION = "create";
13
+ const DEFAULT_RELEASE_FUNCTION = "release";
14
+ const DEFAULT_REFUND_FUNCTION = "refund";
15
+ const DEFAULT_ESCROW_TYPE = "Escrow";
16
+ const MAX_U64 = (1n << 64n) - 1n;
17
+ export function createSponsoredIotaEscrowSettlementExecutor(options) {
18
+ validateExecutorOptions(options);
19
+ return {
20
+ async open(request) {
21
+ const participants = await options.resolveParticipants(request);
22
+ requireParticipants(participants);
23
+ const amount = normalizeU64BaseUnits(await options.amountToBaseUnits(request));
24
+ const tx = new Transaction();
25
+ const contract = normalizeContract(options.contract);
26
+ const createdEscrow = tx.moveCall({
27
+ target: moveTarget(contract.packageId, contract.moduleName, contract.openFunction),
28
+ arguments: [
29
+ tx.pure.address(participants.ownerAddress),
30
+ tx.pure.address(participants.providerAddress),
31
+ tx.pure.address(participants.verifierAddress),
32
+ tx.pure.u64(amount),
33
+ tx.pure.vector("u8", utf8Bytes(request.providerNetAmount.asset)),
34
+ tx.pure.vector("u8", utf8Bytes(request.receipt.idempotencyKey)),
35
+ tx.pure.vector("u8", utf8Bytes(request.receipt.receiptId)),
36
+ ],
37
+ });
38
+ if (contract.publishEscrowObject === "share") {
39
+ tx.moveCall({
40
+ target: "0x2::transfer::share_object",
41
+ typeArguments: [escrowType(contract)],
42
+ arguments: [createdEscrow],
43
+ });
44
+ }
45
+ else if (contract.publishEscrowObject === "transfer-to-owner") {
46
+ tx.transferObjects([createdEscrow], tx.pure.address(participants.ownerAddress));
47
+ }
48
+ const policyTarget = await resolvePolicyTarget(options.policyTargetForOpen, request, contract.packageId, contract.openFunction);
49
+ const executed = await executeSponsoredTransaction(options, tx, {
50
+ operation: "open",
51
+ policyTarget,
52
+ });
53
+ const escrowId = options.extractEscrowId ? options.extractEscrowId({
54
+ operation: "open",
55
+ request,
56
+ reservation: executed.reservation,
57
+ execution: executed.execution,
58
+ }) : defaultEscrowIdExtractor({
59
+ operation: "open",
60
+ reservation: executed.reservation,
61
+ execution: executed.execution,
62
+ }, escrowType(contract));
63
+ if (!escrowId) {
64
+ throw new LiveEscrowSettlementExecutorError("ESCROW_EXECUTOR_ESCROW_ID_MISSING", "Live escrow open executed but no escrow object id was available in the bounded response.");
65
+ }
66
+ return { escrowId, transactionDigest: executed.digest };
67
+ },
68
+ async release(request) {
69
+ const contract = normalizeContract(options.contract);
70
+ const escrowObject = await resolveEscrowObject(options, request);
71
+ const tx = new Transaction();
72
+ tx.moveCall({
73
+ target: moveTarget(contract.packageId, contract.moduleName, contract.releaseFunction),
74
+ arguments: [
75
+ tx.object(escrowObject),
76
+ tx.pure.vector("u8", utf8Bytes(request.releaseProofHash)),
77
+ ],
78
+ });
79
+ const policyTarget = await resolvePolicyTarget(options.policyTargetForRelease, request, contract.packageId, contract.releaseFunction);
80
+ const executed = await executeSponsoredTransaction(options, tx, {
81
+ operation: "release",
82
+ policyTarget,
83
+ });
84
+ return { transactionDigest: executed.digest };
85
+ },
86
+ async refund(request) {
87
+ const contract = normalizeContract(options.contract);
88
+ const escrowObject = await resolveEscrowObject(options, request);
89
+ const tx = new Transaction();
90
+ tx.moveCall({
91
+ target: moveTarget(contract.packageId, contract.moduleName, contract.refundFunction),
92
+ arguments: [
93
+ tx.object(escrowObject),
94
+ tx.pure.vector("u8", utf8Bytes(request.reason)),
95
+ ],
96
+ });
97
+ const policyTarget = await resolvePolicyTarget(options.policyTargetForRefund, request, contract.packageId, contract.refundFunction);
98
+ const executed = await executeSponsoredTransaction(options, tx, {
99
+ operation: "refund",
100
+ policyTarget,
101
+ });
102
+ return { transactionDigest: executed.digest };
103
+ },
104
+ };
105
+ }
106
+ function validateExecutorOptions(options) {
107
+ if (!options.gateway) {
108
+ throw new LiveEscrowSettlementExecutorError("ESCROW_EXECUTOR_CONFIG_INVALID", "A sponsorship gateway client is required.");
109
+ }
110
+ if (!options.contract?.packageId) {
111
+ throw new LiveEscrowSettlementExecutorError("ESCROW_EXECUTOR_CONFIG_INVALID", "An escrow Move package id is required.");
112
+ }
113
+ if (!Number.isSafeInteger(options.gasBudget) || options.gasBudget <= 0) {
114
+ throw new LiveEscrowSettlementExecutorError("ESCROW_EXECUTOR_CONFIG_INVALID", "A positive safe gas budget is required.");
115
+ }
116
+ if (!options.signer?.address || !options.signer.signTransaction) {
117
+ throw new LiveEscrowSettlementExecutorError("ESCROW_EXECUTOR_CONFIG_INVALID", "A settlement signer address and signTransaction method are required.");
118
+ }
119
+ if (options.unsafeBuildTransactionBytesForTesting && options.allowUnsafeCustomTransactionBuilder !== true) {
120
+ throw new LiveEscrowSettlementExecutorError("ESCROW_EXECUTOR_CONFIG_INVALID", "Unsafe custom transaction builder requires allowUnsafeCustomTransactionBuilder: true.");
121
+ }
122
+ if (!options.iotaClient && !options.unsafeBuildTransactionBytesForTesting) {
123
+ throw new LiveEscrowSettlementExecutorError("ESCROW_EXECUTOR_CONFIG_INVALID", "An IOTA client is required for live transaction building.");
124
+ }
125
+ }
126
+ function normalizeContract(contract) {
127
+ return {
128
+ packageId: contract.packageId,
129
+ moduleName: contract.moduleName ?? DEFAULT_MODULE,
130
+ openFunction: contract.openFunction ?? DEFAULT_OPEN_FUNCTION,
131
+ releaseFunction: contract.releaseFunction ?? DEFAULT_RELEASE_FUNCTION,
132
+ refundFunction: contract.refundFunction ?? DEFAULT_REFUND_FUNCTION,
133
+ escrowTypeName: contract.escrowTypeName ?? DEFAULT_ESCROW_TYPE,
134
+ publishEscrowObject: contract.publishEscrowObject ?? "share",
135
+ };
136
+ }
137
+ function requireParticipants(participants) {
138
+ if (!participants.ownerAddress || !participants.providerAddress || !participants.verifierAddress) {
139
+ throw new LiveEscrowSettlementExecutorError("ESCROW_EXECUTOR_CONFIG_INVALID", "Escrow participant resolver must return owner, provider, and verifier addresses.");
140
+ }
141
+ }
142
+ async function executeSponsoredTransaction(options, tx, input) {
143
+ const reservation = await options.gateway.reserveGas({
144
+ gasBudget: options.gasBudget,
145
+ reserveDurationSecs: options.reserveDurationSecs,
146
+ walletAddress: options.signer.address,
147
+ packageId: input.policyTarget.packageId,
148
+ functionName: input.policyTarget.functionName,
149
+ });
150
+ const gasCoin = firstGasCoin(reservation);
151
+ if (!reservation.sponsorAddress || !gasCoin) {
152
+ throw new LiveEscrowSettlementExecutorError("ESCROW_EXECUTOR_RESERVE_RESPONSE_INVALID", "Reserve response did not include sponsor gas details required for sponsored execution.");
153
+ }
154
+ tx.setSender(options.signer.address);
155
+ tx.setGasOwner(reservation.sponsorAddress);
156
+ tx.setGasBudget(options.gasBudget);
157
+ tx.setGasPayment([gasCoin]);
158
+ const transactionBytes = await buildTransactionBytes(options, tx, input.operation);
159
+ const { signature } = await options.signer.signTransaction(transactionBytes);
160
+ const execution = await options.gateway.executeSponsoredTransaction({
161
+ reservationId: reservation.reservationId,
162
+ agentRailTransactionId: reservation.agentRailTransactionId,
163
+ transactionBytes: toBase64(transactionBytes),
164
+ userSignature: signature,
165
+ });
166
+ if (!execution.digest) {
167
+ throw new LiveEscrowSettlementExecutorError("ESCROW_EXECUTOR_EXECUTE_RESPONSE_INVALID", "Vallum execute response did not include a transaction digest.");
168
+ }
169
+ return { reservation, execution, digest: execution.digest };
170
+ }
171
+ async function buildTransactionBytes(options, tx, operation) {
172
+ if (options.unsafeBuildTransactionBytesForTesting) {
173
+ return options.unsafeBuildTransactionBytesForTesting(tx, { operation });
174
+ }
175
+ if (!options.iotaClient) {
176
+ throw new LiveEscrowSettlementExecutorError("ESCROW_EXECUTOR_CONFIG_INVALID", "An IOTA client is required for live transaction building.");
177
+ }
178
+ return tx.build({ client: options.iotaClient });
179
+ }
180
+ function normalizeU64BaseUnits(value) {
181
+ if (typeof value === "bigint") {
182
+ requireU64Range(value);
183
+ return value;
184
+ }
185
+ if (typeof value === "number") {
186
+ if (!Number.isSafeInteger(value)) {
187
+ throw invalidAmountError();
188
+ }
189
+ requireU64Range(BigInt(value));
190
+ return value;
191
+ }
192
+ if (!/^(0|[1-9]\d*)$/.test(value)) {
193
+ throw invalidAmountError();
194
+ }
195
+ requireU64Range(BigInt(value));
196
+ return value;
197
+ }
198
+ function requireU64Range(value) {
199
+ if (value < 0n || value > MAX_U64) {
200
+ throw invalidAmountError();
201
+ }
202
+ }
203
+ function invalidAmountError() {
204
+ return new LiveEscrowSettlementExecutorError("ESCROW_EXECUTOR_CONFIG_INVALID", "Escrow amount resolver must return a non-negative u64-safe integer base-unit amount.");
205
+ }
206
+ function firstGasCoin(reservation) {
207
+ const coin = reservation.gasCoins?.[0];
208
+ if (!isRecord(coin))
209
+ return undefined;
210
+ const objectId = stringField(coin, "objectId");
211
+ const digest = stringField(coin, "digest");
212
+ const version = coin["version"];
213
+ if (!objectId || !digest || (typeof version !== "string" && typeof version !== "number"))
214
+ return undefined;
215
+ return { objectId, version, digest };
216
+ }
217
+ function defaultEscrowIdExtractor(context, expectedObjectType) {
218
+ const raw = context.execution.raw;
219
+ const record = isRecord(raw) ? raw : {};
220
+ const explicit = stringField(record, "escrowId") ?? stringField(record, "escrow_id");
221
+ if (explicit)
222
+ return explicit;
223
+ const objectChanges = Array.isArray(record["objectChanges"]) ? record["objectChanges"] : [];
224
+ let firstCreatedObjectId;
225
+ let sawTypedCreatedObject = false;
226
+ for (const change of objectChanges) {
227
+ if (!isRecord(change) || change["type"] !== "created")
228
+ continue;
229
+ const objectId = stringField(change, "objectId") ?? stringField(change, "object_id");
230
+ if (!objectId)
231
+ continue;
232
+ if (!firstCreatedObjectId)
233
+ firstCreatedObjectId = objectId;
234
+ const objectType = stringField(change, "objectType") ?? stringField(change, "object_type");
235
+ if (objectType)
236
+ sawTypedCreatedObject = true;
237
+ if (objectType === expectedObjectType)
238
+ return objectId;
239
+ }
240
+ if (!sawTypedCreatedObject && firstCreatedObjectId)
241
+ return firstCreatedObjectId;
242
+ const effects = isRecord(record["effects"]) ? record["effects"] : {};
243
+ const created = Array.isArray(effects["created"]) ? effects["created"] : [];
244
+ for (const createdObject of created) {
245
+ const objectId = objectIdFromCreatedObject(createdObject);
246
+ if (objectId)
247
+ return objectId;
248
+ }
249
+ return undefined;
250
+ }
251
+ function objectIdFromCreatedObject(value) {
252
+ if (!isRecord(value))
253
+ return undefined;
254
+ const direct = stringField(value, "objectId") ?? stringField(value, "object_id");
255
+ if (direct)
256
+ return direct;
257
+ const reference = isRecord(value["reference"]) ? value["reference"] : undefined;
258
+ if (reference)
259
+ return stringField(reference, "objectId") ?? stringField(reference, "object_id");
260
+ return undefined;
261
+ }
262
+ async function resolveEscrowObject(options, request) {
263
+ return options.resolveEscrowObject ? options.resolveEscrowObject(request) : request.escrowId;
264
+ }
265
+ async function resolvePolicyTarget(resolver, request, packageId, functionName) {
266
+ const target = resolver ? await resolver(request) : {};
267
+ return {
268
+ packageId: target.packageId ?? packageId,
269
+ functionName: target.functionName ?? functionName,
270
+ };
271
+ }
272
+ function moveTarget(packageId, moduleName, functionName) {
273
+ return `${packageId}::${moduleName}::${functionName}`;
274
+ }
275
+ function escrowType(contract) {
276
+ return `${contract.packageId}::${contract.moduleName}::${contract.escrowTypeName}`;
277
+ }
278
+ function utf8Bytes(value) {
279
+ return Array.from(new TextEncoder().encode(value));
280
+ }
281
+ function isRecord(value) {
282
+ return typeof value === "object" && value !== null;
283
+ }
284
+ function stringField(record, key) {
285
+ const value = record[key];
286
+ return typeof value === "string" && value.length > 0 ? value : undefined;
287
+ }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  export * from "./IotaAgent.js";
2
2
  export * from "./client.js";
3
3
  export * from "./contracts/dataLicense.js";
4
+ export * from "./contracts/iotaEscrowSettlement.js";
5
+ export * from "./contracts/iotaEscrowSettlementExecutor.js";
4
6
  export * from "./contracts/openEscrow.js";
5
7
  export * from "./contracts/payPerCall.js";
6
8
  export * from "./contracts/reputationReceipt.js";
package/dist/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  export * from "./IotaAgent.js";
2
2
  export * from "./client.js";
3
3
  export * from "./contracts/dataLicense.js";
4
+ export * from "./contracts/iotaEscrowSettlement.js";
5
+ export * from "./contracts/iotaEscrowSettlementExecutor.js";
4
6
  export * from "./contracts/openEscrow.js";
5
7
  export * from "./contracts/payPerCall.js";
6
8
  export * from "./contracts/reputationReceipt.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vallum/sdk",
3
- "version": "0.0.1-prerelease.0",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -12,13 +12,15 @@
12
12
  },
13
13
  "license": "Apache-2.0",
14
14
  "dependencies": {
15
- "@vallum/manifest": "0.0.1-prerelease.0",
16
- "@vallum/registry": "0.0.1-prerelease.0",
17
- "@vallum/receipts": "0.0.1-prerelease.0",
18
- "@vallum/shared-types": "0.0.1-prerelease.0"
15
+ "@iota/bcs": "1.6.0",
16
+ "@iota/iota-sdk": "1.14.0",
17
+ "@vallum/manifest": "0.1.1",
18
+ "@vallum/registry": "0.1.1",
19
+ "@vallum/receipts": "0.1.1",
20
+ "@vallum/shared-types": "0.1.1"
19
21
  },
20
22
  "devDependencies": {
21
- "@vallum/policy-gateway": "0.0.1-prerelease.0"
23
+ "@vallum/policy-gateway": "0.1.1"
22
24
  },
23
25
  "description": "TypeScript SDK scaffold for Vallum sponsorship gateways.",
24
26
  "files": [
@@ -36,6 +38,6 @@
36
38
  },
37
39
  "publishConfig": {
38
40
  "access": "public",
39
- "tag": "next"
41
+ "tag": "latest"
40
42
  }
41
43
  }