@hazbase/simplicity 0.0.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.
Files changed (43) hide show
  1. package/LICENSE +182 -0
  2. package/README.md +778 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +628 -0
  5. package/dist/client/ContractFactory.d.ts +13 -0
  6. package/dist/client/ContractFactory.js +50 -0
  7. package/dist/client/DeployedContract.d.ts +12 -0
  8. package/dist/client/DeployedContract.js +43 -0
  9. package/dist/client/SimplicityClient.d.ts +21 -0
  10. package/dist/client/SimplicityClient.js +65 -0
  11. package/dist/core/artifact.d.ts +6 -0
  12. package/dist/core/artifact.js +100 -0
  13. package/dist/core/compiler.d.ts +3 -0
  14. package/dist/core/compiler.js +117 -0
  15. package/dist/core/errors.d.ts +33 -0
  16. package/dist/core/errors.js +70 -0
  17. package/dist/core/executor.d.ts +5 -0
  18. package/dist/core/executor.js +664 -0
  19. package/dist/core/presets.d.ts +16 -0
  20. package/dist/core/presets.js +251 -0
  21. package/dist/core/rpc.d.ts +7 -0
  22. package/dist/core/rpc.js +37 -0
  23. package/dist/core/summary.d.ts +6 -0
  24. package/dist/core/summary.js +27 -0
  25. package/dist/core/templating.d.ts +2 -0
  26. package/dist/core/templating.js +17 -0
  27. package/dist/core/toolchain.d.ts +14 -0
  28. package/dist/core/toolchain.js +82 -0
  29. package/dist/core/types.d.ts +258 -0
  30. package/dist/core/types.js +2 -0
  31. package/dist/gasless/RelayerClient.d.ts +13 -0
  32. package/dist/gasless/RelayerClient.js +76 -0
  33. package/dist/gasless/types.d.ts +144 -0
  34. package/dist/gasless/types.js +2 -0
  35. package/dist/index.d.ts +8 -0
  36. package/dist/index.js +35 -0
  37. package/dist/presets/htlc.simf.tmpl +35 -0
  38. package/dist/presets/manifest.d.ts +1 -0
  39. package/dist/presets/manifest.js +5 -0
  40. package/dist/presets/p2pk.simf.tmpl +4 -0
  41. package/dist/presets/p2pkLockHeight.simf.tmpl +5 -0
  42. package/dist/presets/transferWithTimeout.simf.tmpl +26 -0
  43. package/package.json +45 -0
@@ -0,0 +1,664 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.inspectContractCall = inspectContractCall;
7
+ exports.executeContractCall = executeContractCall;
8
+ exports.findContractUtxos = findContractUtxos;
9
+ exports.executeGaslessContractCall = executeGaslessContractCall;
10
+ const promises_1 = require("node:fs/promises");
11
+ const node_os_1 = require("node:os");
12
+ const node_path_1 = __importDefault(require("node:path"));
13
+ const errors_1 = require("./errors");
14
+ const summary_1 = require("./summary");
15
+ const templating_1 = require("./templating");
16
+ const presets_1 = require("./presets");
17
+ const toolchain_1 = require("./toolchain");
18
+ const DEFAULT_FEE_SAT = 100;
19
+ const DEFAULT_SEQUENCE = 4294967293;
20
+ const DUST_MARGIN_SAT = 600;
21
+ function btcStringToSatNumber(btcStr) {
22
+ const x = Number(btcStr);
23
+ if (!Number.isFinite(x))
24
+ throw new errors_1.ValidationError(`Invalid BTC amount: ${btcStr}`);
25
+ return Math.round(x * 1e8);
26
+ }
27
+ function satToBtcNumber(sat) {
28
+ return Number((sat / 1e8).toFixed(8));
29
+ }
30
+ function satToBtcStringFromNumber(sat) {
31
+ const value = BigInt(sat);
32
+ const whole = value / 100000000n;
33
+ const frac = value % 100000000n;
34
+ return `${whole}.${frac.toString().padStart(8, "0")}`;
35
+ }
36
+ function getArtifactLocktime(artifact) {
37
+ const legacyMinHeight = artifact.legacy?.params?.minHeight;
38
+ if (typeof legacyMinHeight === "number" && Number.isFinite(legacyMinHeight) && legacyMinHeight >= 0) {
39
+ return Math.trunc(legacyMinHeight);
40
+ }
41
+ const templateVars = artifact.source.templateVars ?? {};
42
+ const candidate = templateVars.MIN_HEIGHT ?? templateVars.TIMEOUT_HEIGHT;
43
+ if (typeof candidate === "number" && Number.isFinite(candidate) && candidate >= 0) {
44
+ return Math.trunc(candidate);
45
+ }
46
+ if (typeof candidate === "string" && candidate.trim().length > 0) {
47
+ const parsed = Number(candidate);
48
+ if (Number.isFinite(parsed) && parsed >= 0) {
49
+ return Math.trunc(parsed);
50
+ }
51
+ }
52
+ return 0;
53
+ }
54
+ function parseSimcWitness(output) {
55
+ const lines = output
56
+ .split("\n")
57
+ .map((line) => line.trim())
58
+ .filter(Boolean);
59
+ for (const line of lines) {
60
+ if (line.startsWith("Witness:")) {
61
+ const rest = line.slice("Witness:".length).trim();
62
+ if (rest)
63
+ return rest;
64
+ }
65
+ }
66
+ const nextLineIndex = lines.findIndex((line) => line === "Witness:");
67
+ if (nextLineIndex >= 0 && lines[nextLineIndex + 1]) {
68
+ return lines[nextLineIndex + 1];
69
+ }
70
+ throw new errors_1.ExecutionError("Could not parse Witness from simc output", { output });
71
+ }
72
+ function normalizeRawTx(maybeJsonOrHex) {
73
+ const value = String(maybeJsonOrHex).trim();
74
+ if (value.startsWith("{") || value.startsWith("[")) {
75
+ const parsed = JSON.parse(value);
76
+ const hex = parsed.hex ?? parsed.rawtx ?? parsed.rawTx ?? parsed.rawTransactionHex;
77
+ if (hex)
78
+ return hex.trim();
79
+ }
80
+ if (value.startsWith('"') && value.endsWith('"')) {
81
+ const parsed = JSON.parse(value);
82
+ return parsed.trim();
83
+ }
84
+ return value;
85
+ }
86
+ function chooseUtxo(utxos, minSat, policy) {
87
+ const sortCandidates = (entries) => entries.sort((left, right) => {
88
+ if (policy === "largest")
89
+ return right.sat - left.sat;
90
+ if (policy === "newest")
91
+ return (right.height ?? 0) - (left.height ?? 0);
92
+ return left.sat - right.sat;
93
+ });
94
+ const confirmed = sortCandidates(utxos.filter((utxo) => utxo.confirmed).filter((utxo) => utxo.sat >= minSat));
95
+ if (confirmed[0])
96
+ return confirmed[0];
97
+ const unconfirmed = sortCandidates(utxos.filter((utxo) => !utxo.confirmed).filter((utxo) => utxo.sat >= minSat));
98
+ return unconfirmed[0] ?? null;
99
+ }
100
+ async function findContractUtxosInMempool(elementsCliPath, contractAddress) {
101
+ const mempool = await (0, toolchain_1.runCommand)(elementsCliPath, ["getrawmempool"]);
102
+ const txids = JSON.parse(mempool.stdout);
103
+ const matches = [];
104
+ for (const txid of txids) {
105
+ const tx = await (0, toolchain_1.runCommand)(elementsCliPath, ["getrawtransaction", txid, "true"]);
106
+ const parsed = JSON.parse(tx.stdout);
107
+ for (const output of parsed.vout ?? []) {
108
+ if (output.scriptPubKey?.address !== contractAddress)
109
+ continue;
110
+ matches.push({
111
+ txid,
112
+ vout: output.n,
113
+ scriptPubKey: output.scriptPubKey?.hex ?? "",
114
+ asset: output.asset ?? "",
115
+ sat: btcStringToSatNumber(String(output.value ?? 0)),
116
+ confirmed: false,
117
+ });
118
+ }
119
+ }
120
+ return matches;
121
+ }
122
+ async function scanUtxosByAddress(elementsCliPath, contractAddress) {
123
+ const pattern = `["addr(${contractAddress})"]`;
124
+ const result = await (0, toolchain_1.runCommand)(elementsCliPath, ["scantxoutset", "start", pattern]);
125
+ const parsed = JSON.parse(result.stdout);
126
+ if (!parsed.success || !Array.isArray(parsed.unspents)) {
127
+ throw new errors_1.ExecutionError("scantxoutset failed", parsed);
128
+ }
129
+ const confirmed = parsed.unspents.map((utxo) => ({
130
+ txid: utxo.txid,
131
+ vout: utxo.vout,
132
+ scriptPubKey: utxo.scriptPubKey,
133
+ asset: utxo.asset,
134
+ sat: btcStringToSatNumber(String(utxo.amount)),
135
+ height: utxo.height,
136
+ confirmed: true,
137
+ }));
138
+ const mempool = confirmed.length === 0 ? await findContractUtxosInMempool(elementsCliPath, contractAddress) : [];
139
+ return [...confirmed, ...mempool];
140
+ }
141
+ async function decodePsbt(elementsCliPath, psetBase64, wallet) {
142
+ const result = await (0, toolchain_1.runCommand)(elementsCliPath, [`-rpcwallet=${wallet}`, "decodepsbt", psetBase64]);
143
+ return JSON.parse(result.stdout);
144
+ }
145
+ async function getAddressInfo(elementsCliPath, wallet, address) {
146
+ const result = await (0, toolchain_1.runCommand)(elementsCliPath, [`-rpcwallet=${wallet}`, "getaddressinfo", address]);
147
+ const parsed = JSON.parse(result.stdout);
148
+ if (!parsed.scriptPubKey) {
149
+ throw new errors_1.ExecutionError(`Could not get scriptPubKey from address: ${address}`);
150
+ }
151
+ return { scriptPubKey: parsed.scriptPubKey, unconfidential: parsed.unconfidential };
152
+ }
153
+ async function allocateWalletAddress(elementsCliPath, wallet) {
154
+ const result = await (0, toolchain_1.runCommand)(elementsCliPath, [`-rpcwallet=${wallet}`, "getnewaddress"]);
155
+ const address = result.stdout.trim();
156
+ const info = await getAddressInfo(elementsCliPath, wallet, address);
157
+ return {
158
+ address: info.unconfidential ?? address,
159
+ scriptPubKey: info.scriptPubKey,
160
+ };
161
+ }
162
+ async function listWalletUtxos(elementsCliPath, wallet) {
163
+ const result = await (0, toolchain_1.runCommand)(elementsCliPath, [`-rpcwallet=${wallet}`, "listunspent", "0", "9999999", "[]", "true"]);
164
+ const parsed = JSON.parse(result.stdout);
165
+ return parsed.map((entry) => ({
166
+ txid: entry.txid,
167
+ vout: entry.vout,
168
+ amountSat: btcStringToSatNumber(String(entry.amount)),
169
+ asset: entry.asset,
170
+ isExplicit: !entry.amountblinder || /^0+$/.test(entry.amountblinder),
171
+ }));
172
+ }
173
+ function chooseSponsorUtxo(utxos, feeSat) {
174
+ const candidates = utxos
175
+ .filter((entry) => entry.amountSat >= feeSat)
176
+ .sort((left, right) => {
177
+ if (left.isExplicit !== right.isExplicit) {
178
+ return left.isExplicit ? -1 : 1;
179
+ }
180
+ return left.amountSat - right.amountSat;
181
+ });
182
+ return candidates[0] ?? null;
183
+ }
184
+ function buildPsetSummary(decoded, meta) {
185
+ const inputs = Array.isArray(decoded.inputs)
186
+ ? decoded.inputs.map((entry) => ({
187
+ txid: entry.previous_txid ?? null,
188
+ vout: entry.previous_vout ?? null,
189
+ sequence: entry.sequence ?? null,
190
+ }))
191
+ : [];
192
+ const outputs = Array.isArray(decoded.outputs)
193
+ ? decoded.outputs.map((entry, index) => ({
194
+ n: index,
195
+ value: entry.amount ?? null,
196
+ asset: entry.asset ?? null,
197
+ address: entry.script?.address ?? null,
198
+ scriptPubKeyHex: entry.script?.hex ?? null,
199
+ isFee: entry.script?.type === "fee" || entry.script?.is_fee === true || entry.script?.fee === true,
200
+ }))
201
+ : [];
202
+ return {
203
+ network: meta.network,
204
+ purpose: meta.purpose,
205
+ bondDefinitionId: meta.bondDefinitionId ?? null,
206
+ periodId: meta.periodId ?? null,
207
+ contract: {
208
+ address: meta.contractAddress,
209
+ cmr: meta.cmr,
210
+ internalKey: meta.internalKey,
211
+ program: meta.program,
212
+ minHeight: meta.minHeight,
213
+ },
214
+ expectedLiquidReceiver: meta.expectedLiquidReceiver ?? null,
215
+ inputs,
216
+ outputs,
217
+ fee: decoded.fees ?? decoded.fee ?? null,
218
+ };
219
+ }
220
+ async function getScriptPubKeyHexFromAddress(elementsCliPath, address) {
221
+ const result = await (0, toolchain_1.runCommand)(elementsCliPath, ["getaddressinfo", address]);
222
+ const parsed = JSON.parse(result.stdout);
223
+ if (!parsed.scriptPubKey) {
224
+ throw new errors_1.ExecutionError(`Could not get scriptPubKey from address: ${address}`);
225
+ }
226
+ return parsed.scriptPubKey.toLowerCase();
227
+ }
228
+ async function assertSummaryPolicy(elementsCliPath, summary, expectedLiquidReceiver) {
229
+ if (!expectedLiquidReceiver)
230
+ return;
231
+ const expectedSpk = await getScriptPubKeyHexFromAddress(elementsCliPath, expectedLiquidReceiver);
232
+ const normalOutputs = summary.outputs.filter((output) => output.isFee !== true);
233
+ const found = normalOutputs.some((output) => (output.scriptPubKeyHex ?? "").toLowerCase() === expectedSpk);
234
+ if (!found) {
235
+ throw new errors_1.ExecutionError("Expected receiver not found in non-fee outputs", {
236
+ expectedLiquidReceiver,
237
+ expectedSpk,
238
+ outputs: normalOutputs,
239
+ });
240
+ }
241
+ }
242
+ async function buildExecutionState(config, artifact, input) {
243
+ const elementsCliPath = config.toolchain.elementsCliPath ?? "eltc";
244
+ await (0, toolchain_1.runCommand)(elementsCliPath, [`-rpcwallet=${input.wallet}`, "getwalletinfo"]);
245
+ const feeSat = input.feeSat ?? config.defaults?.feeSat ?? DEFAULT_FEE_SAT;
246
+ const minSat = feeSat + DUST_MARGIN_SAT;
247
+ const utxoPolicy = input.utxoPolicy ?? config.defaults?.utxoPolicy ?? "smallest_over";
248
+ const recipientInfo = await getAddressInfo(elementsCliPath, input.wallet, input.toAddress);
249
+ const recipientAddress = recipientInfo.unconfidential ?? input.toAddress;
250
+ const utxos = await scanUtxosByAddress(elementsCliPath, artifact.compiled.contractAddress);
251
+ const contractUtxo = chooseUtxo(utxos, minSat, utxoPolicy);
252
+ if (!contractUtxo) {
253
+ throw new errors_1.UtxoNotFoundError(`No contract UTXO found for address=${artifact.compiled.contractAddress} satisfying minSat=${minSat}`);
254
+ }
255
+ const sendSat = input.sendAmount
256
+ ? Math.round(input.sendAmount * 1e8)
257
+ : contractUtxo.sat - feeSat;
258
+ if (!Number.isFinite(sendSat) || sendSat <= 0) {
259
+ throw new errors_1.ExecutionError("Invalid send amount after fee calculation", {
260
+ utxoSat: contractUtxo.sat,
261
+ feeSat,
262
+ sendSat,
263
+ });
264
+ }
265
+ const inputsJson = JSON.stringify([
266
+ {
267
+ txid: contractUtxo.txid,
268
+ vout: contractUtxo.vout,
269
+ sequence: input.sequence ?? DEFAULT_SEQUENCE,
270
+ },
271
+ ]);
272
+ const outputsJson = JSON.stringify([{ [recipientAddress]: satToBtcNumber(sendSat) }, { fee: satToBtcNumber(feeSat) }]);
273
+ const pset1 = await (0, toolchain_1.runCommand)(elementsCliPath, [
274
+ "createpsbt",
275
+ inputsJson,
276
+ outputsJson,
277
+ String(getArtifactLocktime(artifact)),
278
+ "true",
279
+ ]);
280
+ const psetUpdated = await (0, toolchain_1.runCommand)(elementsCliPath, ["utxoupdatepsbt", pset1.stdout]);
281
+ const utxoSpec = `${contractUtxo.scriptPubKey}:${contractUtxo.asset}:${satToBtcStringFromNumber(contractUtxo.sat)}`;
282
+ const updateJson = (await (0, toolchain_1.runHalUpdateInput)(config.toolchain.halSimplicityPath, psetUpdated.stdout, 0, utxoSpec, artifact.compiled.cmr, artifact.compiled.internalKey));
283
+ const pset2 = updateJson.pset;
284
+ if (!pset2) {
285
+ throw new errors_1.ExecutionError("update-input did not return a pset", updateJson);
286
+ }
287
+ const decoded = await decodePsbt(elementsCliPath, pset2, input.wallet);
288
+ const summary = buildPsetSummary(decoded, {
289
+ network: artifact.network,
290
+ purpose: input.purpose ?? "sdk_execute",
291
+ bondDefinitionId: input.bondDefinitionId,
292
+ periodId: input.periodId,
293
+ expectedLiquidReceiver: input.expectedLiquidReceiver ?? recipientAddress,
294
+ contractAddress: artifact.compiled.contractAddress,
295
+ cmr: artifact.compiled.cmr,
296
+ internalKey: artifact.compiled.internalKey,
297
+ program: artifact.compiled.program,
298
+ minHeight: getArtifactLocktime(artifact) || undefined,
299
+ });
300
+ const { canonicalJson: summaryCanonicalJson, hash: summaryHash } = (0, summary_1.summarize)(summary);
301
+ return {
302
+ pset2,
303
+ decoded,
304
+ summary,
305
+ summaryHash,
306
+ summaryCanonicalJson,
307
+ contractUtxo,
308
+ sendSat,
309
+ };
310
+ }
311
+ async function inspectContractCall(config, artifact, input) {
312
+ const state = await buildExecutionState(config, artifact, input);
313
+ return {
314
+ mode: "inspect",
315
+ summary: state.summary,
316
+ summaryHash: state.summaryHash,
317
+ summaryCanonicalJson: state.summaryCanonicalJson,
318
+ psetBase64: state.pset2,
319
+ contractUtxo: state.contractUtxo,
320
+ warnings: [],
321
+ };
322
+ }
323
+ async function executeContractCall(config, artifact, input) {
324
+ const elementsCliPath = config.toolchain.elementsCliPath ?? "eltc";
325
+ const state = await buildExecutionState(config, artifact, input);
326
+ await assertSummaryPolicy(elementsCliPath, state.summary, input.expectedLiquidReceiver);
327
+ const finalizedPset = await signContractInput(config, artifact, state.pset2, input.signer.privkeyHex, input.witness);
328
+ const finalized = { pset: finalizedPset };
329
+ if (!finalized.pset) {
330
+ throw new errors_1.ExecutionError("finalize did not return a pset", finalized);
331
+ }
332
+ const rawTxHex = normalizeRawTx(await (0, toolchain_1.runHalExtract)(config.toolchain.halSimplicityPath, finalized.pset));
333
+ let txId;
334
+ if (input.broadcast) {
335
+ const mempool = await (0, toolchain_1.runCommand)(elementsCliPath, ["testmempoolaccept", `["${rawTxHex}"]`]);
336
+ const mempoolParsed = JSON.parse(mempool.stdout);
337
+ if (!Array.isArray(mempoolParsed) || mempoolParsed[0]?.allowed !== true) {
338
+ throw new errors_1.ExecutionError("testmempoolaccept rejected transaction", mempoolParsed);
339
+ }
340
+ const sendTx = await (0, toolchain_1.runCommand)(elementsCliPath, ["sendrawtransaction", rawTxHex]);
341
+ txId = sendTx.stdout.trim();
342
+ }
343
+ return {
344
+ mode: "execute",
345
+ summary: state.summary,
346
+ summaryHash: state.summaryHash,
347
+ summaryCanonicalJson: state.summaryCanonicalJson,
348
+ psetBase64: finalized.pset,
349
+ rawTxHex,
350
+ txId,
351
+ broadcasted: Boolean(input.broadcast),
352
+ contractUtxo: state.contractUtxo,
353
+ };
354
+ }
355
+ async function findContractUtxos(config, artifact) {
356
+ const elementsCliPath = config.toolchain.elementsCliPath ?? "eltc";
357
+ return scanUtxosByAddress(elementsCliPath, artifact.compiled.contractAddress);
358
+ }
359
+ async function executeGaslessContractCall(config, artifact, input) {
360
+ if (input.relayer) {
361
+ return executeRelayedGaslessContractCall(config, artifact, input, input.relayer);
362
+ }
363
+ const elementsCliPath = config.toolchain.elementsCliPath ?? "eltc";
364
+ const wallet = input.wallet ?? config.rpc.wallet ?? "simplicity-test";
365
+ if (!input.sponsorWallet) {
366
+ throw new errors_1.ValidationError("sponsorWallet is required when relayer is not provided");
367
+ }
368
+ await (0, toolchain_1.runCommand)(elementsCliPath, [`-rpcwallet=${wallet}`, "getwalletinfo"]);
369
+ await (0, toolchain_1.runCommand)(elementsCliPath, [`-rpcwallet=${input.sponsorWallet}`, "getwalletinfo"]);
370
+ const contractUtxos = await scanUtxosByAddress(elementsCliPath, artifact.compiled.contractAddress);
371
+ const contractUtxo = chooseUtxo(contractUtxos, input.sendAmount ? Math.round(input.sendAmount * 1e8) : 1, input.utxoPolicy ?? config.defaults?.utxoPolicy ?? "smallest_over");
372
+ if (!contractUtxo) {
373
+ throw new errors_1.UtxoNotFoundError(`No contract UTXO found for address=${artifact.compiled.contractAddress}`);
374
+ }
375
+ const feeSat = input.feeSat ?? config.defaults?.feeSat ?? DEFAULT_FEE_SAT;
376
+ const sendSat = input.sendAmount ? Math.round(input.sendAmount * 1e8) : contractUtxo.sat;
377
+ if (sendSat <= 0 || sendSat > contractUtxo.sat) {
378
+ throw new errors_1.ValidationError("sendAmount must be positive and no greater than the contract UTXO amount");
379
+ }
380
+ let contractChangeAddress;
381
+ let contractChangeScriptPubKey;
382
+ const contractChangeSat = contractUtxo.sat - sendSat;
383
+ if (contractChangeSat > 0) {
384
+ if (!input.contractChangeAddress) {
385
+ throw new errors_1.ValidationError("contractChangeAddress is required when sendAmount is smaller than the contract UTXO");
386
+ }
387
+ const changeInfo = await getAddressInfo(elementsCliPath, wallet, input.contractChangeAddress);
388
+ contractChangeAddress = changeInfo.unconfidential ?? input.contractChangeAddress;
389
+ contractChangeScriptPubKey = changeInfo.scriptPubKey;
390
+ }
391
+ const sponsorUtxos = await listWalletUtxos(elementsCliPath, input.sponsorWallet);
392
+ const sponsorInput = chooseSponsorUtxo(sponsorUtxos, feeSat);
393
+ if (!sponsorInput) {
394
+ throw new errors_1.UtxoNotFoundError(`No sponsor UTXO found in wallet=${input.sponsorWallet} for feeSat=${feeSat}`);
395
+ }
396
+ const sponsorChangeSat = sponsorInput.amountSat - feeSat;
397
+ let sponsorChangeAddress;
398
+ let sponsorChangeScriptPubKey;
399
+ if (sponsorChangeSat > 0) {
400
+ if (input.sponsorChangeAddress) {
401
+ const info = await getAddressInfo(elementsCliPath, input.sponsorWallet, input.sponsorChangeAddress);
402
+ sponsorChangeAddress = info.unconfidential ?? input.sponsorChangeAddress;
403
+ sponsorChangeScriptPubKey = info.scriptPubKey;
404
+ }
405
+ else {
406
+ const allocated = await allocateWalletAddress(elementsCliPath, input.sponsorWallet);
407
+ sponsorChangeAddress = allocated.address;
408
+ sponsorChangeScriptPubKey = allocated.scriptPubKey;
409
+ }
410
+ }
411
+ const recipientInfo = await getAddressInfo(elementsCliPath, wallet, input.toAddress);
412
+ const recipientAddress = recipientInfo.unconfidential ?? input.toAddress;
413
+ const outputs = [{ [recipientAddress]: satToBtcNumber(sendSat) }];
414
+ if (contractChangeSat > 0 && contractChangeAddress) {
415
+ outputs.push({ [contractChangeAddress]: satToBtcNumber(contractChangeSat) });
416
+ }
417
+ if (sponsorChangeSat > 0 && sponsorChangeAddress) {
418
+ outputs.push({ [sponsorChangeAddress]: satToBtcNumber(sponsorChangeSat) });
419
+ }
420
+ outputs.push({ fee: satToBtcNumber(feeSat) });
421
+ const inputsJson = JSON.stringify([
422
+ { txid: contractUtxo.txid, vout: contractUtxo.vout, sequence: DEFAULT_SEQUENCE },
423
+ { txid: sponsorInput.txid, vout: sponsorInput.vout, sequence: DEFAULT_SEQUENCE },
424
+ ]);
425
+ const outputsJson = JSON.stringify(outputs);
426
+ const pset1 = await (0, toolchain_1.runCommand)(elementsCliPath, [
427
+ "createpsbt",
428
+ inputsJson,
429
+ outputsJson,
430
+ String(getArtifactLocktime(artifact)),
431
+ "true",
432
+ ]);
433
+ const psetUpdated = await (0, toolchain_1.runCommand)(elementsCliPath, ["utxoupdatepsbt", pset1.stdout]);
434
+ const contractSpec = `${contractUtxo.scriptPubKey}:${contractUtxo.asset}:${satToBtcStringFromNumber(contractUtxo.sat)}`;
435
+ const contractUpdated = (await (0, toolchain_1.runHalUpdateInput)(config.toolchain.halSimplicityPath, psetUpdated.stdout, 0, contractSpec, artifact.compiled.cmr, artifact.compiled.internalKey));
436
+ if (!contractUpdated.pset) {
437
+ throw new errors_1.ExecutionError("update-input did not return a pset", contractUpdated);
438
+ }
439
+ const decoded = await decodePsbt(elementsCliPath, contractUpdated.pset, input.sponsorWallet);
440
+ const summary = buildPsetSummary(decoded, {
441
+ network: artifact.network,
442
+ purpose: "sdk_gasless_execute",
443
+ contractAddress: artifact.compiled.contractAddress,
444
+ cmr: artifact.compiled.cmr,
445
+ internalKey: artifact.compiled.internalKey,
446
+ program: artifact.compiled.program,
447
+ minHeight: getArtifactLocktime(artifact) || undefined,
448
+ expectedLiquidReceiver: recipientAddress,
449
+ });
450
+ const { canonicalJson: summaryCanonicalJson, hash: summaryHash } = (0, summary_1.summarize)(summary);
451
+ const contractSignedPset = await signContractInput(config, artifact, contractUpdated.pset, input.signer.privkeyHex, input.witness);
452
+ const sponsorSigned = await (0, toolchain_1.runCommand)(elementsCliPath, [
453
+ `-rpcwallet=${input.sponsorWallet}`,
454
+ "walletprocesspsbt",
455
+ contractSignedPset,
456
+ "true",
457
+ "ALL",
458
+ "true",
459
+ ]);
460
+ const sponsorSignedParsed = JSON.parse(sponsorSigned.stdout);
461
+ if (!sponsorSignedParsed.psbt) {
462
+ throw new errors_1.ExecutionError("walletprocesspsbt did not return a sponsor-signed pset", sponsorSignedParsed);
463
+ }
464
+ const finalized = await (0, toolchain_1.runCommand)(elementsCliPath, ["finalizepsbt", sponsorSignedParsed.psbt, "true"]);
465
+ const finalizedParsed = JSON.parse(finalized.stdout);
466
+ if (!finalizedParsed.complete || !finalizedParsed.hex) {
467
+ throw new errors_1.ExecutionError("PSET was not complete after sponsor signing", finalizedParsed);
468
+ }
469
+ let txId;
470
+ if (input.broadcast) {
471
+ const mempool = await (0, toolchain_1.runCommand)(elementsCliPath, ["testmempoolaccept", `[\"${finalizedParsed.hex}\"]`]);
472
+ const mempoolParsed = JSON.parse(mempool.stdout);
473
+ if (!Array.isArray(mempoolParsed) || mempoolParsed[0]?.allowed !== true) {
474
+ throw new errors_1.ExecutionError("testmempoolaccept rejected transaction", mempoolParsed);
475
+ }
476
+ const sent = await (0, toolchain_1.runCommand)(elementsCliPath, ["sendrawtransaction", finalizedParsed.hex]);
477
+ txId = sent.stdout.trim();
478
+ }
479
+ return {
480
+ mode: "gasless-execute",
481
+ summary,
482
+ summaryHash,
483
+ summaryCanonicalJson,
484
+ psetBase64: sponsorSignedParsed.psbt,
485
+ rawTxHex: finalizedParsed.hex,
486
+ txId,
487
+ broadcasted: Boolean(input.broadcast),
488
+ contractUtxo,
489
+ sponsorInput: {
490
+ txid: sponsorInput.txid,
491
+ vout: sponsorInput.vout,
492
+ amountSat: sponsorInput.amountSat,
493
+ },
494
+ };
495
+ }
496
+ async function signContractInput(config, artifact, psetBase64, privkeyHex, witnessConfig) {
497
+ assertArtifactExecutionSupport(artifact, "direct");
498
+ const signatures = await buildSignatureMap(config, artifact, psetBase64, privkeyHex, witnessConfig);
499
+ const workDir = await (0, promises_1.mkdtemp)(node_path_1.default.join((0, node_os_1.tmpdir)(), "simplicity-sdk-gasless-"));
500
+ const witnessPath = node_path_1.default.join(workDir, "witness.json");
501
+ await (0, promises_1.writeFile)(witnessPath, JSON.stringify(buildWitnessJson(artifact, signatures, witnessConfig), null, 2), "utf8");
502
+ const simfSourcePath = artifact.legacy?.simfTemplatePath ?? artifact.source.simfPath;
503
+ if (!simfSourcePath) {
504
+ throw new errors_1.ExecutionError("Artifact does not include a source simf path for witness generation");
505
+ }
506
+ const simfTemplate = await (0, promises_1.readFile)(simfSourcePath, "utf8");
507
+ const simfRendered = (0, templating_1.renderTemplate)(simfTemplate, artifact.source.templateVars ?? {});
508
+ const simfRenderedPath = node_path_1.default.join(workDir, "program.simf");
509
+ await (0, promises_1.writeFile)(simfRenderedPath, simfRendered, "utf8");
510
+ const witnessOutput = await (0, toolchain_1.runSimcWithWitness)(config.toolchain.simcPath, simfRenderedPath, witnessPath);
511
+ const witness = parseSimcWitness(witnessOutput);
512
+ const contractFinalized = (await (0, toolchain_1.runHalFinalize)(config.toolchain.halSimplicityPath, psetBase64, 0, artifact.compiled.program, witness));
513
+ if (!contractFinalized.pset) {
514
+ throw new errors_1.ExecutionError("finalize did not return a pset", contractFinalized);
515
+ }
516
+ return contractFinalized.pset;
517
+ }
518
+ async function buildSignatureMap(config, artifact, psetBase64, privkeyHex, witnessConfig) {
519
+ const primary = (await (0, toolchain_1.runHalSighash)(config.toolchain.halSimplicityPath, psetBase64, 0, artifact.compiled.cmr, privkeyHex));
520
+ if (!primary.signature) {
521
+ throw new errors_1.ExecutionError("sighash did not return a signature", primary);
522
+ }
523
+ const signatures = {
524
+ SIGNATURE: primary.signature,
525
+ };
526
+ for (const [name, signer] of Object.entries(witnessConfig?.signers ?? {})) {
527
+ if (signer.type !== "schnorrPrivkeyHex") {
528
+ throw new errors_1.UnsupportedFeatureError(`Unsupported witness signer type for '${name}'`);
529
+ }
530
+ const result = (await (0, toolchain_1.runHalSighash)(config.toolchain.halSimplicityPath, psetBase64, 0, artifact.compiled.cmr, signer.privkeyHex));
531
+ if (!result.signature) {
532
+ throw new errors_1.ExecutionError(`sighash did not return a signature for witness signer '${name}'`, result);
533
+ }
534
+ signatures[`SIGNATURE:${name}`] = result.signature;
535
+ }
536
+ return signatures;
537
+ }
538
+ function replaceSignaturePlaceholders(value, signatures) {
539
+ return value.replace(/\$\{SIGNATURE(?::([A-Z0-9_]+))?\}/g, (_match, name) => {
540
+ const key = name ? `SIGNATURE:${name}` : "SIGNATURE";
541
+ const signature = signatures[key];
542
+ if (!signature) {
543
+ throw new errors_1.PresetExecutionError(`Missing signature placeholder binding for '${key}'`);
544
+ }
545
+ return `0x${signature}`;
546
+ });
547
+ }
548
+ function buildWitnessJson(artifact, signatures, witnessConfig) {
549
+ if (witnessConfig?.source) {
550
+ throw new errors_1.UnsupportedFeatureError("witness.source is not supported in v0.1.0; use witness.values because simc currently expects JSON witness input");
551
+ }
552
+ const assignments = new Map();
553
+ for (const [name, assignment] of Object.entries(witnessConfig?.values ?? {})) {
554
+ assignments.set(name, {
555
+ type: assignment.type,
556
+ value: replaceSignaturePlaceholders(assignment.value, signatures),
557
+ });
558
+ }
559
+ const presetId = artifact.source.preset;
560
+ const requiredWitnessFields = presetId
561
+ ? (0, presets_1.getPresetOrThrow)(presetId).executionProfile.requiredWitnessFields
562
+ : ["SIGNER_SIGNATURE"];
563
+ const preset = presetId ? (0, presets_1.getPresetOrThrow)(presetId) : null;
564
+ if (preset) {
565
+ (0, presets_1.validateWitnessConfig)(preset, witnessConfig);
566
+ }
567
+ if (requiredWitnessFields.length === 1 &&
568
+ requiredWitnessFields[0] === "SIGNER_SIGNATURE" &&
569
+ !assignments.has("SIGNER_SIGNATURE")) {
570
+ assignments.set("SIGNER_SIGNATURE", { type: "Signature", value: `0x${signatures.SIGNATURE}` });
571
+ }
572
+ for (const field of requiredWitnessFields) {
573
+ if (!assignments.has(field)) {
574
+ throw new errors_1.PresetExecutionError(`Missing witness value for required field '${field}'`, {
575
+ preset: presetId,
576
+ requiredWitnessFields,
577
+ });
578
+ }
579
+ }
580
+ const witnessJson = {};
581
+ for (const [name, assignment] of Array.from(assignments.entries()).sort(([a], [b]) => a.localeCompare(b))) {
582
+ witnessJson[name] = assignment;
583
+ }
584
+ return witnessJson;
585
+ }
586
+ function assertArtifactExecutionSupport(artifact, mode) {
587
+ if (artifact.source.mode !== "preset" || !artifact.source.preset)
588
+ return;
589
+ const preset = (0, presets_1.getPresetOrThrow)(artifact.source.preset);
590
+ const supported = mode === "direct"
591
+ ? preset.executionProfile.supportsDirectExecute
592
+ : preset.executionProfile.supportsRelayerExecute;
593
+ if (!supported) {
594
+ throw new errors_1.PresetExecutionError(`Preset '${preset.id}' does not support ${mode === "direct" ? "direct execute" : "relayer-backed execute"} in v0.1.0`, {
595
+ preset: preset.id,
596
+ requiredWitnessFields: preset.executionProfile.requiredWitnessFields,
597
+ });
598
+ }
599
+ }
600
+ async function executeRelayedGaslessContractCall(config, artifact, input, relayer) {
601
+ assertArtifactExecutionSupport(artifact, "relayer");
602
+ if (!input.fromLabel) {
603
+ throw new errors_1.ValidationError("fromLabel is required when relayer is provided");
604
+ }
605
+ const request = await relayer.requestSimplicityExecution({
606
+ fromLabel: input.fromLabel,
607
+ artifact: {
608
+ compiled: artifact.compiled,
609
+ source: artifact.source,
610
+ legacy: { params: { minHeight: getArtifactLocktime(artifact) || undefined } },
611
+ network: artifact.network,
612
+ },
613
+ toAddress: input.toAddress,
614
+ sendAmount: input.sendAmount,
615
+ feeSat: input.feeSat,
616
+ });
617
+ const signedPsetBase64 = await signContractInput(config, artifact, request.psetBase64, input.signer.privkeyHex, input.witness);
618
+ const submit = await relayer.submitSimplicityExecution({
619
+ requestId: request.requestId,
620
+ signedPsetBase64,
621
+ });
622
+ return {
623
+ mode: "gasless-execute",
624
+ summary: {
625
+ network: artifact.network,
626
+ purpose: "sdk_gasless_execute_relayer",
627
+ bondDefinitionId: null,
628
+ periodId: null,
629
+ contract: {
630
+ address: request.detailedSummary.contract.contractAddress,
631
+ cmr: request.detailedSummary.contract.cmr,
632
+ internalKey: request.detailedSummary.contract.internalKey,
633
+ program: request.detailedSummary.contract.program,
634
+ minHeight: getArtifactLocktime(artifact) || undefined,
635
+ },
636
+ expectedLiquidReceiver: request.detailedSummary.expectedReceiver,
637
+ inputs: request.detailedSummary.inputs,
638
+ outputs: request.detailedSummary.outputs.map((output) => ({
639
+ n: output.n,
640
+ value: output.amount,
641
+ asset: output.asset,
642
+ address: output.address,
643
+ scriptPubKeyHex: output.scriptPubKeyHex,
644
+ isFee: output.isFee,
645
+ })),
646
+ fee: request.detailedSummary.fee,
647
+ },
648
+ summaryHash: submit.summaryHash,
649
+ summaryCanonicalJson: request.summaryCanonicalJson,
650
+ psetBase64: signedPsetBase64,
651
+ rawTxHex: submit.rawTxHex,
652
+ txId: submit.txId,
653
+ broadcasted: true,
654
+ contractUtxo: {
655
+ txid: request.summary.contractInput.txid,
656
+ vout: request.summary.contractInput.vout,
657
+ scriptPubKey: "",
658
+ asset: "",
659
+ sat: request.summary.contractInput.amountSat,
660
+ confirmed: true,
661
+ },
662
+ sponsorInput: request.summary.sponsorInput,
663
+ };
664
+ }
@@ -0,0 +1,16 @@
1
+ import { PresetManifestEntry } from "./types";
2
+ export declare const PRESET_MANIFEST: Record<string, PresetManifestEntry>;
3
+ export declare function listPresets(): PresetManifestEntry[];
4
+ export declare function getPresetOrThrow(preset: string): PresetManifestEntry;
5
+ export declare function validatePresetParams(preset: PresetManifestEntry, params: Record<string, string | number>): Record<string, string | number>;
6
+ export declare function describePreset(preset: PresetManifestEntry): Record<string, unknown>;
7
+ export declare function validateWitnessConfig(preset: PresetManifestEntry, witness: {
8
+ values?: Record<string, {
9
+ type: string;
10
+ value: string;
11
+ }>;
12
+ signers?: Record<string, {
13
+ type: "schnorrPrivkeyHex";
14
+ privkeyHex: string;
15
+ }>;
16
+ } | undefined): void;