@vallum/sdk 0.1.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.
@@ -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
@@ -2,6 +2,7 @@ export * from "./IotaAgent.js";
2
2
  export * from "./client.js";
3
3
  export * from "./contracts/dataLicense.js";
4
4
  export * from "./contracts/iotaEscrowSettlement.js";
5
+ export * from "./contracts/iotaEscrowSettlementExecutor.js";
5
6
  export * from "./contracts/openEscrow.js";
6
7
  export * from "./contracts/payPerCall.js";
7
8
  export * from "./contracts/reputationReceipt.js";
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ export * from "./IotaAgent.js";
2
2
  export * from "./client.js";
3
3
  export * from "./contracts/dataLicense.js";
4
4
  export * from "./contracts/iotaEscrowSettlement.js";
5
+ export * from "./contracts/iotaEscrowSettlementExecutor.js";
5
6
  export * from "./contracts/openEscrow.js";
6
7
  export * from "./contracts/payPerCall.js";
7
8
  export * from "./contracts/reputationReceipt.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vallum/sdk",
3
- "version": "0.1.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.1.0",
16
- "@vallum/registry": "0.1.0",
17
- "@vallum/receipts": "0.1.0",
18
- "@vallum/shared-types": "0.1.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.1.0"
23
+ "@vallum/policy-gateway": "0.1.1"
22
24
  },
23
25
  "description": "TypeScript SDK scaffold for Vallum sponsorship gateways.",
24
26
  "files": [