@hazbase/simplicity 0.4.1 → 0.4.2

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
@@ -164,6 +164,34 @@ testnet issuer or deployment uses a different USDt asset id, set
164
164
  payment requirements will preserve that asset id instead of replacing it with
165
165
  the SDK's default registry id.
166
166
 
167
+ Typical entrypoints:
168
+ - `sdk.rwaDvp.definePurchase(...)`
169
+ - `sdk.rwaDvp.buildPaymentRequirements(...)`
170
+ - `sdk.rwaDvp.verifyPaymentPset(...)`
171
+ - `sdk.rwaDvp.compileEscrowContract(...)`
172
+ - `sdk.rwaDvp.prepareDeliveryClaim(...)`
173
+ - `sdk.rwaDvp.inspectDeliveryClaim(...)`
174
+ - `sdk.rwaDvp.executeDeliveryClaim(...)`
175
+ - `sdk.rwaDvp.prepareRefundClaim(...)`
176
+ - `sdk.rwaDvp.inspectRefundClaim(...)`
177
+ - `sdk.rwaDvp.executeRefundClaim(...)`
178
+ - `sdk.rwaDvp.exportEvidence(...)`
179
+
180
+ For a delivery claim, the operator spends the funded escrow output and sends
181
+ the payment asset to the treasury while delivering the RWA asset to the buyer.
182
+ For a refund claim, the operator spends the same escrow output after the
183
+ configured timeout and returns the payment asset to the buyer. The inspect
184
+ methods build and validate the candidate spend without broadcasting it; the
185
+ execute methods finalize the Simplicity input, optionally test mempool
186
+ acceptance, and broadcast when `broadcast: true`.
187
+
188
+ The claim execution helpers assume the Elements wallet can provide any extra
189
+ inputs needed for RWA delivery and L-BTC fees. You can also pass explicit
190
+ `extraInputs` when the service wants deterministic coin selection. Script-bound
191
+ delivery/refund output checks are the practical default; descriptor-bound checks
192
+ should be used only when the caller can provide the exact output data required
193
+ by the descriptor.
194
+
167
195
  ## CLI and Confidence Commands
168
196
 
169
197
  ### 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>;
@@ -5,8 +5,13 @@ exports.runSimcCompile = runSimcCompile;
5
5
  exports.runSimcWithWitness = runSimcWithWitness;
6
6
  exports.runHalInfo = runHalInfo;
7
7
  exports.runHalUpdateInput = runHalUpdateInput;
8
+ exports.runHalCreatePset = runHalCreatePset;
9
+ exports.runSimplicityCreatePset = runSimplicityCreatePset;
10
+ exports.runSimplicityUpdateInput = runSimplicityUpdateInput;
11
+ exports.runSimplicityBlindPset = runSimplicityBlindPset;
8
12
  exports.runHalSighash = runHalSighash;
9
13
  exports.runHalFinalize = runHalFinalize;
14
+ exports.runSimplicityFinalize = runSimplicityFinalize;
10
15
  exports.runHalExtract = runHalExtract;
11
16
  const node_child_process_1 = require("node:child_process");
12
17
  const node_util_1 = require("node:util");
@@ -21,7 +26,13 @@ async function runCommand(command, args, options = {}) {
21
26
  return { stdout: stdout.trim(), stderr: stderr.trim() };
22
27
  }
23
28
  catch (error) {
24
- throw new errors_1.ToolchainError(`Command failed: ${command} ${args.join(" ")}`, error);
29
+ const cause = error;
30
+ const details = [
31
+ cause.message ? `message=${cause.message}` : "",
32
+ cause.stderr ? `stderr=${cause.stderr.trim()}` : "",
33
+ cause.stdout ? `stdout=${cause.stdout.trim()}` : "",
34
+ ].filter(Boolean);
35
+ throw new errors_1.ToolchainError(`Command failed: ${command} ${args.slice(0, 3).join(" ")}${details.length ? ` | ${details.join(" | ")}` : ""}`, error);
25
36
  }
26
37
  }
27
38
  async function runSimcCompile(simcPath, simfPath) {
@@ -41,6 +52,7 @@ async function runHalUpdateInput(halPath, pset, inputIndex, utxoSpec, cmr, inter
41
52
  "simplicity",
42
53
  "pset",
43
54
  "update-input",
55
+ "--liquid",
44
56
  pset,
45
57
  String(inputIndex),
46
58
  "-i",
@@ -52,10 +64,73 @@ async function runHalUpdateInput(halPath, pset, inputIndex, utxoSpec, cmr, inter
52
64
  ]);
53
65
  return JSON.parse(result.stdout);
54
66
  }
67
+ async function runHalCreatePset(halPath, inputs, outputs) {
68
+ const result = await runCommand(halPath, [
69
+ "simplicity",
70
+ "pset",
71
+ "create",
72
+ "--liquid",
73
+ JSON.stringify(inputs),
74
+ JSON.stringify(outputs),
75
+ ]);
76
+ return JSON.parse(result.stdout);
77
+ }
78
+ async function runSimplicityCreatePset(halPath, inputs, outputs) {
79
+ const helperPath = process.env.HAZBASE_SIMPLICITY_CREATE_PATH ?? "hazbase-simplicity-create";
80
+ try {
81
+ const helper = await runCommand(helperPath, [
82
+ "--liquid",
83
+ JSON.stringify(inputs),
84
+ JSON.stringify(outputs),
85
+ ]);
86
+ return JSON.parse(helper.stdout);
87
+ }
88
+ catch (error) {
89
+ const message = error instanceof Error ? error.message : String(error);
90
+ if (!message.includes("ENOENT")) {
91
+ return {
92
+ error: message,
93
+ };
94
+ }
95
+ }
96
+ return runHalCreatePset(halPath, inputs, outputs);
97
+ }
98
+ async function runSimplicityUpdateInput(halPath, pset, inputIndex, inputUtxo, cmr, internalKey) {
99
+ const helperPath = process.env.HAZBASE_SIMPLICITY_UPDATE_INPUT_PATH ?? "hazbase-simplicity-update-input";
100
+ try {
101
+ const helper = await runCommand(helperPath, [
102
+ "--liquid",
103
+ pset,
104
+ String(inputIndex),
105
+ internalKey,
106
+ cmr,
107
+ ]);
108
+ return JSON.parse(helper.stdout);
109
+ }
110
+ catch (error) {
111
+ const message = error instanceof Error ? error.message : String(error);
112
+ if (!message.includes("ENOENT")) {
113
+ return {
114
+ error: message,
115
+ };
116
+ }
117
+ }
118
+ return runHalUpdateInput(halPath, pset, inputIndex, inputUtxo, cmr, internalKey);
119
+ }
120
+ async function runSimplicityBlindPset(pset, inputSecrets) {
121
+ const helperPath = process.env.HAZBASE_SIMPLICITY_BLIND_PSET_PATH ?? "hazbase-simplicity-blind-pset";
122
+ const helper = await runCommand(helperPath, [
123
+ "--liquid",
124
+ pset,
125
+ JSON.stringify(inputSecrets),
126
+ ]);
127
+ return JSON.parse(helper.stdout);
128
+ }
55
129
  async function runHalSighash(halPath, pset, inputIndex, cmr, privkeyHex) {
56
130
  const result = await runCommand(halPath, [
57
131
  "simplicity",
58
132
  "sighash",
133
+ "--liquid",
59
134
  pset,
60
135
  String(inputIndex),
61
136
  cmr,
@@ -69,6 +144,7 @@ async function runHalFinalize(halPath, pset, inputIndex, program, witness) {
69
144
  "simplicity",
70
145
  "pset",
71
146
  "finalize",
147
+ "--liquid",
72
148
  pset,
73
149
  String(inputIndex),
74
150
  program,
@@ -76,7 +152,35 @@ async function runHalFinalize(halPath, pset, inputIndex, program, witness) {
76
152
  ]);
77
153
  return JSON.parse(result.stdout);
78
154
  }
155
+ async function runSimplicityFinalize(halPath, pset, inputIndex, program, witness, options = {}) {
156
+ const helperPath = process.env.HAZBASE_SIMPLICITY_FINALIZE_PATH ?? "hazbase-simplicity-finalize";
157
+ if (options.redeemProgram) {
158
+ try {
159
+ const helper = await runCommand(helperPath, [
160
+ "--liquid",
161
+ pset,
162
+ String(inputIndex),
163
+ options.redeemProgram,
164
+ witness,
165
+ ]);
166
+ return JSON.parse(helper.stdout);
167
+ }
168
+ catch (error) {
169
+ const message = error instanceof Error ? error.message : String(error);
170
+ if (!message.includes("ENOENT")) {
171
+ const halFallback = await runHalFinalize(halPath, pset, inputIndex, program, witness);
172
+ if (halFallback.pset)
173
+ return halFallback;
174
+ return {
175
+ ...halFallback,
176
+ helperError: message,
177
+ };
178
+ }
179
+ }
180
+ }
181
+ return runHalFinalize(halPath, pset, inputIndex, program, witness);
182
+ }
79
183
  async function runHalExtract(halPath, pset) {
80
- const result = await runCommand(halPath, ["simplicity", "pset", "extract", pset]);
184
+ const result = await runCommand(halPath, ["simplicity", "pset", "extract", "--liquid", pset]);
81
185
  return result.stdout;
82
186
  }