@hazbase/simplicity 0.4.1 → 0.4.3

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
@@ -20,20 +20,25 @@ With the current public SDK you can build and test:
20
20
  - bond redemption / settlement / close-out flows
21
21
  - LP fund capital call / distribution / close-out flows
22
22
  - receivable repayment-first funding / partial repayment / closing flows
23
+ - RWA delivery-versus-payment flows over Liquid PSETs and Liquid x402
24
+ - atomic Liquid DvP proposals where service delivery and buyer payment settle in one PSET
23
25
  - evidence, trust summary, lineage, and finality exports
24
26
 
25
27
  ## Public Architecture
26
28
 
27
- The public SDK is organized into five layers:
29
+ The public SDK is organized into domain clients and lower-level payment helpers:
28
30
  - `sdk.outputBinding`: shared output-binding support, evaluation, and fallback behavior
29
31
  - `sdk.policies`: generic constrained transfer and recursive policy engine
30
32
  - `sdk.bonds`: private bond / credit settlement business layer
31
33
  - `sdk.funds`: LP fund settlement business layer
32
34
  - `sdk.receivables`: repayment-first receivable business layer
35
+ - `sdk.rwaDvp`: RWA purchase terms, payment requirements, claim descriptors, and evidence
36
+ - `sdk.payments.x402`: Liquid x402 assets, PSET payment helpers, verification, and settlement
33
37
 
34
38
  A useful mental model is:
35
39
  - `sdk.outputBinding` + `sdk.policies` provide the shared settlement kernel
36
- - `sdk.bonds`, `sdk.funds`, and `sdk.receivables` build domain flows on top of that kernel
40
+ - `sdk.bonds`, `sdk.funds`, `sdk.receivables`, and `sdk.rwaDvp` build domain flows on top of that kernel
41
+ - `sdk.payments.x402` and the root-level `x402` exports provide the Liquid PSET payment layer used by those flows
37
42
 
38
43
  ## Quickstart
39
44
 
@@ -158,12 +163,121 @@ Use `sdk.rwaDvp` when a purchase flow needs a Liquid PSET payment request,
158
163
  delivery/refund claim descriptors, and an evidence bundle that ties the Liquid
159
164
  payment and delivery to an external lock or allocation record.
160
165
 
166
+ The EVM-side lock reference can describe the source asset being held for
167
+ settlement. Set `evmLock.tokenStandard` to `"ERC3475"`, `"ERC20"`, `"ERC721"`,
168
+ or `"ERC1155"` and include the token address / id fields used by your lock
169
+ manager. Existing callers that omit `tokenStandard` are treated as `"ERC3475"`
170
+ for backwards compatibility.
171
+
161
172
  For standard Liquid assets, `payment.asset` can be `"lbtc"` or `"usdt"`. If a
162
173
  testnet issuer or deployment uses a different USDt asset id, set
163
174
  `payment.asset: "usdt"` and pass the explicit `payment.assetId`; the generated
164
175
  payment requirements will preserve that asset id instead of replacing it with
165
176
  the SDK's default registry id.
166
177
 
178
+ Typical entrypoints:
179
+ - `sdk.rwaDvp.definePurchase(...)`
180
+ - `sdk.rwaDvp.buildPaymentRequirements(...)`
181
+ - `sdk.rwaDvp.verifyPaymentPset(...)`
182
+ - `sdk.rwaDvp.compileEscrowContract(...)`
183
+ - `sdk.rwaDvp.prepareDeliveryClaim(...)`
184
+ - `sdk.rwaDvp.inspectDeliveryClaim(...)`
185
+ - `sdk.rwaDvp.executeDeliveryClaim(...)`
186
+ - `sdk.rwaDvp.prepareRefundClaim(...)`
187
+ - `sdk.rwaDvp.inspectRefundClaim(...)`
188
+ - `sdk.rwaDvp.executeRefundClaim(...)`
189
+ - `sdk.rwaDvp.exportEvidence(...)`
190
+
191
+ For a delivery claim, the operator spends the funded escrow output and sends
192
+ the payment asset to the treasury while delivering the RWA asset to the buyer.
193
+ For a refund claim, the operator spends the same escrow output after the
194
+ configured timeout and returns the payment asset to the buyer. The inspect
195
+ methods build and validate the candidate spend without broadcasting it; the
196
+ execute methods finalize the Simplicity input, optionally test mempool
197
+ acceptance, and broadcast when `broadcast: true`.
198
+
199
+ The claim execution helpers assume the Elements wallet can provide any extra
200
+ inputs needed for RWA delivery and L-BTC fees. You can also pass explicit
201
+ `extraInputs` when the service wants deterministic coin selection. Script-bound
202
+ delivery/refund output checks are the practical default; descriptor-bound checks
203
+ should be used only when the caller can provide the exact output data required
204
+ by the descriptor.
205
+
206
+ #### Atomic Liquid DvP PSET helpers
207
+
208
+ For flows that should avoid a separate "buyer pays first, service delivers
209
+ later" step, the SDK also exposes lower-level atomic DvP helpers from the root
210
+ package:
211
+
212
+ - `buildLiquidAtomicDvpRequirements(...)`
213
+ - `prepareLiquidAtomicDvpLwkWasmMakerProposal(...)`
214
+ - `prepareLiquidAtomicDvpLwkWasmTakerPayment(...)`
215
+ - `buildLiquidAtomicDvpPaymentFromPset(...)`
216
+ - `verifyLiquidAtomicDvpPayment(...)`
217
+ - `encodeLiquidAtomicDvpPayment(...)`
218
+ - `decodeLiquidAtomicDvpPayment(...)`
219
+
220
+ These helpers model a Liquidex-style exchange:
221
+ - the service/maker selects an exact RWA delivery UTXO and creates a proposal
222
+ - the buyer/taker adds the required payment output and signs the combined PSET
223
+ - the resulting `X-PAYMENT` payload commits to both the payment output and the
224
+ delivery output through a summary hash
225
+
226
+ The LWK convenience helpers dynamically load `lwk_node` or `lwk_wasm` from the
227
+ consuming application. Install one of them in the application that prepares or
228
+ takes proposals:
229
+
230
+ ```bash
231
+ npm install lwk_node
232
+ ```
233
+
234
+ Typical server-side shape:
235
+
236
+ ```ts
237
+ import {
238
+ buildLiquidAtomicDvpRequirements,
239
+ prepareLiquidAtomicDvpLwkWasmMakerProposal,
240
+ verifyLiquidAtomicDvpPayment,
241
+ } from "@hazbase/simplicity";
242
+
243
+ const requirements = buildLiquidAtomicDvpRequirements({
244
+ network: "liquidtestnet",
245
+ resource: "/v1/orders/<order-id>/liquid-atomic-pset",
246
+ paymentToTreasury: {
247
+ assetId: "<lbtc-or-usdt-asset-id>",
248
+ amountAtomic: "10000",
249
+ recipient: "<treasury-confidential-address>",
250
+ },
251
+ rwaToBuyer: {
252
+ assetId: "<rwa-liquid-asset-id>",
253
+ amountAtomic: "10",
254
+ recipient: "<buyer-confidential-address>",
255
+ },
256
+ expiresAt: new Date(Date.now() + 15 * 60_000),
257
+ });
258
+
259
+ const maker = await prepareLiquidAtomicDvpLwkWasmMakerProposal({
260
+ requirements,
261
+ mnemonic: process.env.SERVICE_LIQUID_MNEMONIC!,
262
+ descriptor: process.env.SERVICE_LIQUID_DESCRIPTOR!,
263
+ electrumUrl: process.env.LIQUID_ELECTRUM_URL!,
264
+ });
265
+
266
+ // Store maker.proposalPsetBase64 with the order/payment requirements.
267
+ // When the buyer submits X-PAYMENT, verify it before broadcasting.
268
+ const verified = verifyLiquidAtomicDvpPayment({
269
+ requirements,
270
+ paymentPayload: buyerPaymentPayload,
271
+ });
272
+ if (!verified.ok) throw new Error(verified.reason);
273
+ ```
274
+
275
+ The SDK-level payload check is intentionally lightweight: it verifies the
276
+ scheme, network, resource, expiry, PSET value, and summary hash. Production
277
+ settlement services should still decode the final PSET with their Liquid node or
278
+ wallet stack, verify the concrete payment and delivery outputs, and only then
279
+ broadcast.
280
+
167
281
  ## CLI and Confidence Commands
168
282
 
169
283
  ### Validation Surface
@@ -38,10 +38,15 @@ export declare class SimplicityClient {
38
38
  definePurchase: (input: Parameters<typeof rwaDvp.definePurchase>[1]) => ReturnType<typeof rwaDvp.definePurchase>;
39
39
  buildPaymentRequirements: (input: Parameters<typeof rwaDvp.buildPaymentRequirements>[1]) => ReturnType<typeof rwaDvp.buildPaymentRequirements>;
40
40
  verifyPaymentPset: (input: Parameters<typeof rwaDvp.verifyPaymentPset>[1]) => ReturnType<typeof rwaDvp.verifyPaymentPset>;
41
+ compileEscrowContract: (input: Parameters<typeof rwaDvp.compileEscrowContract>[1]) => ReturnType<typeof rwaDvp.compileEscrowContract>;
41
42
  prepareDeliveryClaim: (input: Parameters<typeof rwaDvp.prepareDeliveryClaim>[1]) => ReturnType<typeof rwaDvp.prepareDeliveryClaim>;
42
43
  verifyDeliveryClaim: (input: Parameters<typeof rwaDvp.verifyDeliveryClaim>[1]) => ReturnType<typeof rwaDvp.verifyDeliveryClaim>;
44
+ inspectDeliveryClaim: (input: Parameters<typeof rwaDvp.inspectDeliveryClaim>[1]) => ReturnType<typeof rwaDvp.inspectDeliveryClaim>;
45
+ executeDeliveryClaim: (input: Parameters<typeof rwaDvp.executeDeliveryClaim>[1]) => ReturnType<typeof rwaDvp.executeDeliveryClaim>;
43
46
  prepareRefundClaim: (input: Parameters<typeof rwaDvp.prepareRefundClaim>[1]) => ReturnType<typeof rwaDvp.prepareRefundClaim>;
44
47
  verifyRefundClaim: (input: Parameters<typeof rwaDvp.verifyRefundClaim>[1]) => ReturnType<typeof rwaDvp.verifyRefundClaim>;
48
+ inspectRefundClaim: (input: Parameters<typeof rwaDvp.inspectRefundClaim>[1]) => ReturnType<typeof rwaDvp.inspectRefundClaim>;
49
+ executeRefundClaim: (input: Parameters<typeof rwaDvp.executeRefundClaim>[1]) => ReturnType<typeof rwaDvp.executeRefundClaim>;
45
50
  exportEvidence: (input: Parameters<typeof rwaDvp.exportEvidence>[1]) => ReturnType<typeof rwaDvp.exportEvidence>;
46
51
  };
47
52
  readonly policies: {
@@ -88,10 +88,15 @@ class SimplicityClient {
88
88
  definePurchase: (input) => rwaDvp.definePurchase(this, input),
89
89
  buildPaymentRequirements: (input) => rwaDvp.buildPaymentRequirements(this, input),
90
90
  verifyPaymentPset: (input) => rwaDvp.verifyPaymentPset(this, input),
91
+ compileEscrowContract: (input) => rwaDvp.compileEscrowContract(this, input),
91
92
  prepareDeliveryClaim: (input) => rwaDvp.prepareDeliveryClaim(this, input),
92
93
  verifyDeliveryClaim: (input) => rwaDvp.verifyDeliveryClaim(this, input),
94
+ inspectDeliveryClaim: (input) => rwaDvp.inspectDeliveryClaim(this, input),
95
+ executeDeliveryClaim: (input) => rwaDvp.executeDeliveryClaim(this, input),
93
96
  prepareRefundClaim: (input) => rwaDvp.prepareRefundClaim(this, input),
94
97
  verifyRefundClaim: (input) => rwaDvp.verifyRefundClaim(this, input),
98
+ inspectRefundClaim: (input) => rwaDvp.inspectRefundClaim(this, input),
99
+ executeRefundClaim: (input) => rwaDvp.executeRefundClaim(this, input),
95
100
  exportEvidence: (input) => rwaDvp.exportEvidence(this, input),
96
101
  };
97
102
  this.policies = {
@@ -1,5 +1,7 @@
1
- import { ContractUtxo, ExecuteCallInput, ExecuteResult, GaslessExecuteInput, GaslessExecuteResult, InspectCallInput, InspectResult, SimplicityArtifact, SimplicityClientConfig } from "./types";
1
+ import { ContractUtxo, ExecuteCallInput, ExecuteResult, GaslessExecuteInput, GaslessExecuteResult, InspectCallInput, InspectResult, MultiAssetContractCallInput, MultiAssetExecuteResult, MultiAssetInspectResult, SimplicityArtifact, SimplicityClientConfig } from "./types";
2
2
  export declare function inspectContractCall(config: SimplicityClientConfig, artifact: SimplicityArtifact, input: InspectCallInput): Promise<InspectResult>;
3
3
  export declare function executeContractCall(config: SimplicityClientConfig, artifact: SimplicityArtifact, input: ExecuteCallInput): Promise<ExecuteResult>;
4
4
  export declare function findContractUtxos(config: SimplicityClientConfig, artifact: SimplicityArtifact): Promise<ContractUtxo[]>;
5
+ export declare function inspectMultiAssetContractCall(config: SimplicityClientConfig, artifact: SimplicityArtifact, input: MultiAssetContractCallInput): Promise<MultiAssetInspectResult>;
6
+ export declare function executeMultiAssetContractCall(config: SimplicityClientConfig, artifact: SimplicityArtifact, input: MultiAssetContractCallInput): Promise<MultiAssetExecuteResult>;
5
7
  export declare function executeGaslessContractCall(config: SimplicityClientConfig, artifact: SimplicityArtifact, input: GaslessExecuteInput): Promise<GaslessExecuteResult>;
@@ -6,6 +6,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.inspectContractCall = inspectContractCall;
7
7
  exports.executeContractCall = executeContractCall;
8
8
  exports.findContractUtxos = findContractUtxos;
9
+ exports.inspectMultiAssetContractCall = inspectMultiAssetContractCall;
10
+ exports.executeMultiAssetContractCall = executeMultiAssetContractCall;
9
11
  exports.executeGaslessContractCall = executeGaslessContractCall;
10
12
  const promises_1 = require("node:fs/promises");
11
13
  const node_os_1 = require("node:os");
@@ -25,6 +27,11 @@ function btcStringToSatNumber(btcStr) {
25
27
  throw new errors_1.ValidationError(`Invalid BTC amount: ${btcStr}`);
26
28
  return Math.round(x * 1e8);
27
29
  }
30
+ function optionalBtcStringToSatNumber(value) {
31
+ if (value === undefined || value === null || value === "")
32
+ return undefined;
33
+ return btcStringToSatNumber(String(value));
34
+ }
28
35
  function satToBtcNumber(sat) {
29
36
  return Number((sat / 1e8).toFixed(8));
30
37
  }
@@ -34,6 +41,24 @@ function satToBtcStringFromNumber(sat) {
34
41
  const frac = value % 100000000n;
35
42
  return `${whole}.${frac.toString().padStart(8, "0")}`;
36
43
  }
44
+ function normalizeAssetId(asset) {
45
+ return asset.trim().toLowerCase();
46
+ }
47
+ function addAssetAmount(target, asset, amountSat) {
48
+ if (!Number.isSafeInteger(amountSat) || amountSat < 0) {
49
+ throw new errors_1.ValidationError("amountSat must be a non-negative safe integer");
50
+ }
51
+ const key = normalizeAssetId(asset);
52
+ target.set(key, (target.get(key) ?? 0) + amountSat);
53
+ }
54
+ async function resolveLbtcAssetId(config) {
55
+ const sidechain = await callRpc(config, "getsidechaininfo", []);
56
+ const asset = sidechain.pegged_asset;
57
+ if (asset && /^[0-9a-f]{64}$/i.test(asset)) {
58
+ return asset.toLowerCase();
59
+ }
60
+ return "bitcoin";
61
+ }
37
62
  function getArtifactLocktime(artifact) {
38
63
  const legacyMinHeight = artifact.legacy?.params?.minHeight;
39
64
  if (typeof legacyMinHeight === "number" && Number.isFinite(legacyMinHeight) && legacyMinHeight >= 0) {
@@ -59,21 +84,49 @@ function getEffectiveLocktime(artifact, input) {
59
84
  }
60
85
  return getArtifactLocktime(artifact);
61
86
  }
62
- function parseSimcWitness(output) {
87
+ function parseSimcWitnessOutput(output) {
63
88
  const lines = output
64
89
  .split("\n")
65
90
  .map((line) => line.trim())
66
91
  .filter(Boolean);
92
+ let program;
93
+ for (const line of lines) {
94
+ if (line.startsWith("{")) {
95
+ try {
96
+ const parsed = JSON.parse(line);
97
+ if (typeof parsed.witness === "string" && parsed.witness.trim()) {
98
+ return {
99
+ ...(typeof parsed.program === "string" && parsed.program.trim() ? { program: parsed.program.trim() } : {}),
100
+ witness: parsed.witness.trim(),
101
+ };
102
+ }
103
+ }
104
+ catch {
105
+ // Continue with the human-readable simc output format.
106
+ }
107
+ }
108
+ }
109
+ for (const line of lines) {
110
+ if (line.startsWith("Program:")) {
111
+ const rest = line.slice("Program:".length).trim();
112
+ if (rest)
113
+ program = rest;
114
+ }
115
+ }
116
+ const programLineIndex = lines.findIndex((line) => line === "Program:");
117
+ if (!program && programLineIndex >= 0 && lines[programLineIndex + 1]) {
118
+ program = lines[programLineIndex + 1];
119
+ }
67
120
  for (const line of lines) {
68
121
  if (line.startsWith("Witness:")) {
69
122
  const rest = line.slice("Witness:".length).trim();
70
123
  if (rest)
71
- return rest;
124
+ return { ...(program ? { program } : {}), witness: rest };
72
125
  }
73
126
  }
74
127
  const nextLineIndex = lines.findIndex((line) => line === "Witness:");
75
128
  if (nextLineIndex >= 0 && lines[nextLineIndex + 1]) {
76
- return lines[nextLineIndex + 1];
129
+ return { ...(program ? { program } : {}), witness: lines[nextLineIndex + 1] };
77
130
  }
78
131
  throw new errors_1.ExecutionError("Could not parse Witness from simc output", { output });
79
132
  }
@@ -139,7 +192,7 @@ async function scanUtxosByAddress(config, contractAddress) {
139
192
  vout: utxo.vout,
140
193
  scriptPubKey: utxo.scriptPubKey,
141
194
  asset: utxo.asset,
142
- sat: btcStringToSatNumber(String(utxo.amount)),
195
+ sat: optionalBtcStringToSatNumber(utxo.amount) ?? 0,
143
196
  height: utxo.height,
144
197
  confirmed: true,
145
198
  }));
@@ -160,7 +213,7 @@ async function allocateWalletAddress(config, wallet) {
160
213
  const address = await callRpc(config, "getnewaddress", [], wallet);
161
214
  const info = await getAddressInfo(config, wallet, address);
162
215
  return {
163
- address: info.unconfidential ?? address,
216
+ address,
164
217
  scriptPubKey: info.scriptPubKey,
165
218
  };
166
219
  }
@@ -375,6 +428,252 @@ async function executeContractCall(config, artifact, input) {
375
428
  async function findContractUtxos(config, artifact) {
376
429
  return scanUtxosByAddress(config, artifact.compiled.contractAddress);
377
430
  }
431
+ function findRequestedContractUtxo(utxos, input) {
432
+ if (!input)
433
+ return null;
434
+ const requested = utxos.find((utxo) => (utxo.txid === input.txid
435
+ && (input.vout === undefined || utxo.vout === input.vout))) ?? null;
436
+ if (!requested)
437
+ return null;
438
+ return {
439
+ ...requested,
440
+ asset: input.asset ?? requested.asset,
441
+ sat: input.amountSat ?? requested.sat,
442
+ };
443
+ }
444
+ function buildMultiAssetBlindingSecrets(input) {
445
+ const secrets = [];
446
+ if (input.contractInput?.rawTxHex && input.contractInput.blindingPrivateKey) {
447
+ secrets.push({
448
+ index: 0,
449
+ rawTxHex: input.contractInput.rawTxHex,
450
+ vout: input.contractInput.vout ?? input.contractUtxo.vout,
451
+ blindingPrivateKey: input.contractInput.blindingPrivateKey,
452
+ });
453
+ }
454
+ input.extraInputs.forEach((extra, index) => {
455
+ if (extra.amountBlinder && extra.assetBlinder) {
456
+ secrets.push({
457
+ index: index + 1,
458
+ asset: extra.asset,
459
+ amountSat: extra.amountSat,
460
+ amountBlinder: extra.amountBlinder,
461
+ assetBlinder: extra.assetBlinder,
462
+ });
463
+ }
464
+ });
465
+ return secrets;
466
+ }
467
+ async function buildMultiAssetExecutionState(config, artifact, input) {
468
+ await callRpc(config, "getwalletinfo", [], input.wallet);
469
+ if (input.outputs.length === 0) {
470
+ throw new errors_1.ValidationError("outputs must contain at least one recipient");
471
+ }
472
+ const contractUtxos = await scanUtxosByAddress(config, artifact.compiled.contractAddress);
473
+ const feeSat = input.feeSat ?? config.defaults?.feeSat ?? DEFAULT_FEE_SAT;
474
+ const requested = findRequestedContractUtxo(contractUtxos, input.contractInput);
475
+ const contractUtxo = requested ?? chooseUtxo(contractUtxos, 1, config.defaults?.utxoPolicy ?? "smallest_over");
476
+ if (!contractUtxo) {
477
+ throw new errors_1.UtxoNotFoundError(`No contract UTXO found for address=${artifact.compiled.contractAddress}`);
478
+ }
479
+ const extraInputs = input.extraInputs ?? [];
480
+ const inputTotals = new Map();
481
+ addAssetAmount(inputTotals, contractUtxo.asset, contractUtxo.sat);
482
+ for (const extra of extraInputs) {
483
+ addAssetAmount(inputTotals, extra.asset, extra.amountSat);
484
+ }
485
+ const lbtcAssetId = await resolveLbtcAssetId(config);
486
+ const outputTotals = new Map();
487
+ for (const output of input.outputs) {
488
+ addAssetAmount(outputTotals, output.asset, output.amountSat);
489
+ }
490
+ addAssetAmount(outputTotals, lbtcAssetId, feeSat);
491
+ const changeOutputs = [];
492
+ let changeAddress;
493
+ if (input.changeAddress) {
494
+ await getAddressInfo(config, input.wallet, input.changeAddress);
495
+ changeAddress = input.changeAddress;
496
+ }
497
+ else {
498
+ changeAddress = (await allocateWalletAddress(config, input.wallet)).address;
499
+ }
500
+ const assetKeys = Array.from(new Set([...inputTotals.keys(), ...outputTotals.keys()])).sort();
501
+ for (const asset of assetKeys) {
502
+ const available = inputTotals.get(asset) ?? 0;
503
+ const required = outputTotals.get(asset) ?? 0;
504
+ if (available < required) {
505
+ throw new errors_1.ValidationError("Insufficient inputs for multi-asset contract call", {
506
+ asset,
507
+ available,
508
+ required,
509
+ });
510
+ }
511
+ const changeSat = available - required;
512
+ if (changeSat > 0) {
513
+ changeOutputs.push({
514
+ address: changeAddress,
515
+ asset,
516
+ amountSat: changeSat,
517
+ });
518
+ }
519
+ }
520
+ const inputsJson = [
521
+ {
522
+ txid: contractUtxo.txid,
523
+ vout: contractUtxo.vout,
524
+ sequence: input.locktimeHeight ? DEFAULT_SEQUENCE : DEFAULT_SEQUENCE,
525
+ },
526
+ ...extraInputs.map((extra) => ({
527
+ txid: extra.txid,
528
+ vout: extra.vout,
529
+ sequence: extra.sequence ?? DEFAULT_SEQUENCE,
530
+ })),
531
+ ];
532
+ const inputAssets = [contractUtxo.asset, ...extraInputs.map((extra) => extra.asset)];
533
+ const blinderIndexFor = (asset) => {
534
+ const normalized = normalizeAssetId(asset);
535
+ const extraIndex = extraInputs.findIndex((input) => normalizeAssetId(input.asset) === normalized);
536
+ if (extraIndex >= 0)
537
+ return extraIndex + 1;
538
+ const index = inputAssets.findIndex((inputAsset) => normalizeAssetId(inputAsset) === normalized);
539
+ return index >= 0 ? index : undefined;
540
+ };
541
+ const psetOutputs = await Promise.all([...input.outputs, ...changeOutputs].map(async (output) => {
542
+ await getAddressInfo(config, input.wallet, output.address);
543
+ return output;
544
+ }));
545
+ const outputsJson = [
546
+ ...psetOutputs.map((output) => ({
547
+ [output.address]: satToBtcNumber(output.amountSat),
548
+ asset: output.asset,
549
+ })),
550
+ { fee: satToBtcNumber(feeSat) },
551
+ ];
552
+ const halOutputsJson = [
553
+ ...psetOutputs.map((output) => ({
554
+ address: output.address,
555
+ asset: output.asset,
556
+ amount: satToBtcNumber(output.amountSat),
557
+ blinderIndex: blinderIndexFor(output.asset),
558
+ })),
559
+ {
560
+ address: "fee",
561
+ asset: lbtcAssetId,
562
+ amount: satToBtcNumber(feeSat),
563
+ },
564
+ ];
565
+ const locktime = getEffectiveLocktime(artifact, input);
566
+ const createResult = locktime
567
+ ? {
568
+ pset: await callRpc(config, "createpsbt", [inputsJson, outputsJson, locktime, true], input.wallet),
569
+ }
570
+ : (await (0, toolchain_1.runSimplicityCreatePset)(config.toolchain.halSimplicityPath, inputsJson, halOutputsJson));
571
+ const pset1 = createResult.pset;
572
+ if (!pset1) {
573
+ throw new errors_1.ExecutionError("pset create did not return a pset", createResult);
574
+ }
575
+ const contractSpec = `${contractUtxo.scriptPubKey}:${contractUtxo.asset}:${satToBtcStringFromNumber(contractUtxo.sat)}`;
576
+ const psetWithUtxos = await callRpc(config, "utxoupdatepsbt", [pset1], input.wallet);
577
+ const contractUpdated = (await (0, toolchain_1.runSimplicityUpdateInput)(config.toolchain.halSimplicityPath, psetWithUtxos, 0, contractSpec, artifact.compiled.cmr, artifact.compiled.internalKey));
578
+ if (!contractUpdated.pset) {
579
+ throw new errors_1.ExecutionError("update-input did not return a pset", contractUpdated);
580
+ }
581
+ const psetUpdated = await callRpc(config, "utxoupdatepsbt", [contractUpdated.pset], input.wallet);
582
+ const decoded = await decodePsbt(config, psetUpdated, input.wallet);
583
+ const summary = buildPsetSummary(decoded, {
584
+ network: artifact.network,
585
+ purpose: input.purpose ?? "sdk_multi_asset_contract_call",
586
+ bondDefinitionId: null,
587
+ definitionType: artifact.definition?.definitionType,
588
+ definitionId: artifact.definition?.definitionId,
589
+ definitionHash: artifact.definition?.hash,
590
+ definitionTrustMode: artifact.definition?.trustMode,
591
+ definitionAnchorMode: artifact.definition?.anchorMode,
592
+ stateType: artifact.state?.stateType,
593
+ stateId: artifact.state?.stateId,
594
+ stateHash: artifact.state?.hash,
595
+ stateTrustMode: artifact.state?.trustMode,
596
+ stateAnchorMode: artifact.state?.anchorMode,
597
+ expectedLiquidReceiver: input.outputs[0]?.address,
598
+ contractAddress: artifact.compiled.contractAddress,
599
+ cmr: artifact.compiled.cmr,
600
+ internalKey: artifact.compiled.internalKey,
601
+ program: artifact.compiled.program,
602
+ minHeight: locktime || undefined,
603
+ });
604
+ const { canonicalJson: summaryCanonicalJson, hash: summaryHash } = (0, summary_1.summarize)(summary);
605
+ return {
606
+ psetBase64: psetUpdated,
607
+ summary,
608
+ summaryHash,
609
+ summaryCanonicalJson,
610
+ contractUtxo,
611
+ extraInputs,
612
+ requestedOutputs: psetOutputs,
613
+ changeOutputs,
614
+ };
615
+ }
616
+ async function inspectMultiAssetContractCall(config, artifact, input) {
617
+ const state = await buildMultiAssetExecutionState(config, artifact, input);
618
+ return {
619
+ mode: "inspect",
620
+ summary: state.summary,
621
+ summaryHash: state.summaryHash,
622
+ summaryCanonicalJson: state.summaryCanonicalJson,
623
+ psetBase64: state.psetBase64,
624
+ contractUtxo: state.contractUtxo,
625
+ warnings: [],
626
+ extraInputs: state.extraInputs,
627
+ requestedOutputs: state.requestedOutputs,
628
+ changeOutputs: state.changeOutputs,
629
+ };
630
+ }
631
+ async function executeMultiAssetContractCall(config, artifact, input) {
632
+ const state = await buildMultiAssetExecutionState(config, artifact, input);
633
+ const blindingSecrets = buildMultiAssetBlindingSecrets({
634
+ contractInput: input.contractInput,
635
+ contractUtxo: state.contractUtxo,
636
+ extraInputs: state.extraInputs,
637
+ });
638
+ const blindedPset = blindingSecrets.length > 0
639
+ ? (await (0, toolchain_1.runSimplicityBlindPset)(state.psetBase64, blindingSecrets))
640
+ : { pset: state.psetBase64 };
641
+ if (!blindedPset.pset) {
642
+ throw new errors_1.ExecutionError(`blind PSET did not return a pset${blindedPset.error ? `: ${blindedPset.error}` : ""}`, blindedPset);
643
+ }
644
+ const contractSignedPset = await signContractInput(config, artifact, blindedPset.pset, input.signer.privkeyHex, input.witness);
645
+ const walletSignedParsed = await callRpc(config, "walletprocesspsbt", [contractSignedPset, true, "ALL", true], input.wallet);
646
+ if (!walletSignedParsed.psbt) {
647
+ throw new errors_1.ExecutionError("walletprocesspsbt did not return a signed pset", walletSignedParsed);
648
+ }
649
+ const finalizedParsed = await callRpc(config, "finalizepsbt", [walletSignedParsed.psbt, true]);
650
+ if (!finalizedParsed.complete || !finalizedParsed.hex) {
651
+ throw new errors_1.ExecutionError("PSET was not complete after wallet signing", finalizedParsed);
652
+ }
653
+ const rawTxHex = normalizeRawTx(finalizedParsed.hex);
654
+ let txId;
655
+ if (input.broadcast) {
656
+ const mempoolParsed = await callRpc(config, "testmempoolaccept", [[rawTxHex]], input.wallet);
657
+ if (!Array.isArray(mempoolParsed) || mempoolParsed[0]?.allowed !== true) {
658
+ throw new errors_1.ExecutionError("testmempoolaccept rejected transaction", mempoolParsed);
659
+ }
660
+ txId = await callRpc(config, "sendrawtransaction", [rawTxHex], input.wallet);
661
+ }
662
+ return {
663
+ mode: "execute",
664
+ summary: state.summary,
665
+ summaryHash: state.summaryHash,
666
+ summaryCanonicalJson: state.summaryCanonicalJson,
667
+ psetBase64: walletSignedParsed.psbt,
668
+ rawTxHex,
669
+ txId,
670
+ broadcasted: Boolean(input.broadcast),
671
+ contractUtxo: state.contractUtxo,
672
+ extraInputs: state.extraInputs,
673
+ requestedOutputs: state.requestedOutputs,
674
+ changeOutputs: state.changeOutputs,
675
+ };
676
+ }
378
677
  async function executeGaslessContractCall(config, artifact, input) {
379
678
  if (input.relayer) {
380
679
  return executeRelayedGaslessContractCall(config, artifact, input, input.relayer);
@@ -520,10 +819,10 @@ async function signContractInput(config, artifact, psetBase64, privkeyHex, witne
520
819
  const simfRenderedPath = node_path_1.default.join(workDir, "program.simf");
521
820
  await (0, promises_1.writeFile)(simfRenderedPath, simfRendered, "utf8");
522
821
  const witnessOutput = await (0, toolchain_1.runSimcWithWitness)(config.toolchain.simcPath, simfRenderedPath, witnessPath);
523
- const witness = parseSimcWitness(witnessOutput);
524
- const contractFinalized = (await (0, toolchain_1.runHalFinalize)(config.toolchain.halSimplicityPath, psetBase64, 0, artifact.compiled.program, witness));
822
+ const simc = parseSimcWitnessOutput(witnessOutput);
823
+ const contractFinalized = (await (0, toolchain_1.runSimplicityFinalize)(config.toolchain.halSimplicityPath, psetBase64, 0, artifact.compiled.program, simc.witness, { ...(simc.program ? { redeemProgram: simc.program } : {}) }));
525
824
  if (!contractFinalized.pset) {
526
- throw new errors_1.ExecutionError("finalize did not return a pset", contractFinalized);
825
+ throw new errors_1.ExecutionError(`finalize did not return a pset${contractFinalized.error ? `: ${contractFinalized.error}` : ""}`, contractFinalized);
527
826
  }
528
827
  return contractFinalized.pset;
529
828
  }
package/dist/core/rpc.js CHANGED
@@ -26,7 +26,9 @@ class ElementsRpcClient {
26
26
  });
27
27
  const payload = (await response.json());
28
28
  if (!response.ok || payload.error) {
29
- throw new errors_1.ExecutionError(`RPC ${method} failed`, {
29
+ const rpcMessage = payload.error?.message ? `: ${payload.error.message}` : "";
30
+ const rpcCode = payload.error?.code === undefined ? "" : ` (code ${payload.error.code})`;
31
+ throw new errors_1.ExecutionError(`RPC ${method} failed${rpcCode}${rpcMessage}`, {
30
32
  status: response.status,
31
33
  error: payload.error,
32
34
  });
@@ -9,6 +9,30 @@ export declare function runSimcCompile(simcPath: string, simfPath: string): Prom
9
9
  export declare function runSimcWithWitness(simcPath: string, simfPath: string, witnessPath: string): Promise<string>;
10
10
  export declare function runHalInfo(halPath: string, program: string): Promise<unknown>;
11
11
  export declare function runHalUpdateInput(halPath: string, pset: string, inputIndex: number, utxoSpec: string, cmr: string, internalKey: string): Promise<unknown>;
12
+ export declare function runHalCreatePset(halPath: string, inputs: Array<{
13
+ txid: string;
14
+ vout: number;
15
+ sequence?: number;
16
+ }>, outputs: Array<{
17
+ address: string;
18
+ asset: string;
19
+ amount: number;
20
+ }>): Promise<unknown>;
21
+ export declare function runSimplicityCreatePset(halPath: string, inputs: Array<{
22
+ txid: string;
23
+ vout: number;
24
+ sequence?: number;
25
+ }>, outputs: Array<{
26
+ address: string;
27
+ asset: string;
28
+ amount: number;
29
+ blinderIndex?: number;
30
+ }>): Promise<unknown>;
31
+ export declare function runSimplicityUpdateInput(halPath: string, pset: string, inputIndex: number, inputUtxo: string, cmr: string, internalKey: string): Promise<unknown>;
32
+ export declare function runSimplicityBlindPset(pset: string, inputSecrets: Array<Record<string, unknown>>): Promise<unknown>;
12
33
  export declare function runHalSighash(halPath: string, pset: string, inputIndex: number, cmr: string, privkeyHex: string): Promise<unknown>;
13
34
  export declare function runHalFinalize(halPath: string, pset: string, inputIndex: number, program: string, witness: string): Promise<unknown>;
35
+ export declare function runSimplicityFinalize(halPath: string, pset: string, inputIndex: number, program: string, witness: string, options?: {
36
+ redeemProgram?: string;
37
+ }): Promise<unknown>;
14
38
  export declare function runHalExtract(halPath: string, pset: string): Promise<string>;