@hazbase/simplicity 0.3.0 → 0.3.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
@@ -193,6 +193,22 @@ RECEIVABLE_OUTPUT_BINDING_MODE=script-bound npm run e2e:receivable-testnet
193
193
  RECEIVABLE_OUTPUT_BINDING_MODE=descriptor-bound npm run e2e:receivable-testnet
194
194
  ```
195
195
 
196
+ Liquid x402 buyer smoke test:
197
+
198
+ ```bash
199
+ npm install --no-save lwk_node@^0.17.1
200
+ npm run e2e:x402-lwk-address -- --generate
201
+ npm run e2e:x402-lwk-buyer -- https://share.example/v/slug --dry-run
202
+ LIQUID_X402_E2E_MNEMONIC="..." npm run e2e:x402-lwk-buyer -- https://share.example/v/slug --no-submit
203
+ LIQUID_X402_E2E_MNEMONIC="..." npm run e2e:x402-lwk-buyer -- https://share.example/v/slug --output unlocked.bin
204
+ ```
205
+
206
+ The address script derives a confidential Liquid receive address for funding a
207
+ test buyer wallet. The buyer script then uses LWK locally to scan, build, sign,
208
+ and finalize a Liquid PSET, then sends the resulting `X-PAYMENT` header back to
209
+ the protected URL. `--dry-run` only inspects the 402 requirements, while
210
+ `--no-submit` signs a payment without broadcasting through the seller backend.
211
+
196
212
  These commands are useful both as reproducible checks and as examples of the SDK's current validated surface.
197
213
 
198
214
  ## Where To Go Next
@@ -22,6 +22,9 @@ export declare class SimplicityClient {
22
22
  encodePayment: typeof liquidX402.encodeLiquidXPayment;
23
23
  decodePayment: typeof liquidX402.decodeLiquidXPayment;
24
24
  preparePsetPayment: (input: liquidX402.LiquidX402PreparePsetPaymentInput) => Promise<liquidX402.LiquidX402PreparePsetPaymentResult>;
25
+ buildPaymentFromPset: typeof liquidX402.buildLiquidX402PaymentFromPset;
26
+ prepareLwkWasmPayment: typeof liquidX402.prepareLiquidX402LwkWasmPayment;
27
+ deriveLwkWasmAddress: typeof liquidX402.deriveLiquidX402LwkWasmAddress;
25
28
  verify: (input: liquidX402.LiquidX402VerifyPaymentInput) => Promise<liquidX402.LiquidX402VerifyPaymentResult>;
26
29
  settle: (input: liquidX402.LiquidX402SettlePaymentInput) => Promise<liquidX402.LiquidX402SettlePaymentResult>;
27
30
  };
@@ -71,6 +71,9 @@ class SimplicityClient {
71
71
  encodePayment: liquidX402.encodeLiquidXPayment,
72
72
  decodePayment: liquidX402.decodeLiquidXPayment,
73
73
  preparePsetPayment: async (input) => liquidX402.prepareLiquidX402PsetPayment(this.rpc, input),
74
+ buildPaymentFromPset: liquidX402.buildLiquidX402PaymentFromPset,
75
+ prepareLwkWasmPayment: liquidX402.prepareLiquidX402LwkWasmPayment,
76
+ deriveLwkWasmAddress: liquidX402.deriveLiquidX402LwkWasmAddress,
74
77
  verify: async (input) => liquidX402.verifyLiquidX402Payment(this.rpc, input),
75
78
  settle: async (input) => liquidX402.settleLiquidX402Payment(this.rpc, input),
76
79
  },
@@ -75,6 +75,49 @@ export interface LiquidX402PreparePsetPaymentResult {
75
75
  psetBase64: string;
76
76
  summaryHash: string;
77
77
  }
78
+ export interface LiquidX402BuildPaymentFromPsetInput {
79
+ requirements: LiquidX402PaymentRequirements | Record<string, unknown>;
80
+ psetBase64: string;
81
+ decoded?: DecodePsbtResult;
82
+ payer?: string;
83
+ summaryHash?: string;
84
+ }
85
+ export interface LiquidX402PrepareLwkWasmPaymentInput {
86
+ requirements: LiquidX402PaymentRequirements | Record<string, unknown>;
87
+ mnemonic: string;
88
+ payer?: string;
89
+ esploraUrl?: string;
90
+ waterfalls?: boolean;
91
+ concurrency?: number;
92
+ utxoOnly?: boolean;
93
+ scan?: boolean;
94
+ scanToIndex?: number;
95
+ feeRate?: number;
96
+ finalize?: boolean;
97
+ lwk?: LiquidX402LwkWasmModule;
98
+ importLwk?: () => Promise<LiquidX402LwkWasmModule>;
99
+ }
100
+ export type LiquidX402PrepareLwkWasmPaymentResult = LiquidX402PreparePsetPaymentResult & {
101
+ descriptor: string;
102
+ dwid?: string;
103
+ };
104
+ export interface LiquidX402DeriveLwkWasmAddressInput {
105
+ mnemonic: string;
106
+ network?: LiquidX402Network;
107
+ index?: number;
108
+ lwk?: LiquidX402LwkWasmModule;
109
+ importLwk?: () => Promise<LiquidX402LwkWasmModule>;
110
+ }
111
+ export interface LiquidX402DeriveLwkWasmAddressResult {
112
+ network: LiquidX402Network;
113
+ address: string;
114
+ unconfidentialAddress?: string;
115
+ index: number;
116
+ isBlinded?: boolean;
117
+ descriptor: string;
118
+ dwid?: string;
119
+ policyAsset?: string;
120
+ }
78
121
  export interface LiquidX402VerifyPaymentInput {
79
122
  requirements: LiquidX402PaymentRequirements | Record<string, unknown>;
80
123
  paymentPayload: LiquidX402PaymentPayload | Record<string, unknown>;
@@ -100,7 +143,7 @@ export interface LiquidX402SettlePaymentResult {
100
143
  rawTxHex?: string;
101
144
  summaryHash?: string;
102
145
  }
103
- type DecodePsbtResult = {
146
+ export type DecodePsbtResult = {
104
147
  fees?: Record<string, number>;
105
148
  outputs?: Array<{
106
149
  amount?: number;
@@ -109,8 +152,30 @@ type DecodePsbtResult = {
109
152
  address?: string;
110
153
  hex?: string;
111
154
  };
155
+ scriptPubKey?: {
156
+ address?: string;
157
+ hex?: string;
158
+ };
112
159
  }>;
113
160
  };
161
+ export type LiquidX402LwkWasmModule = {
162
+ Address: {
163
+ new (value: string): any;
164
+ parse?: (value: string, network: any) => any;
165
+ };
166
+ AssetId: {
167
+ new (value: string): any;
168
+ fromString?: (value: string) => any;
169
+ };
170
+ EsploraClient: new (network: any, url: string, waterfalls: boolean, concurrency: number, utxoOnly: boolean) => any;
171
+ Mnemonic: new (value: string) => any;
172
+ Network: {
173
+ mainnet: () => any;
174
+ testnet: () => any;
175
+ };
176
+ Signer: new (mnemonic: any, network: any) => any;
177
+ Wollet: new (network: any, descriptor: any) => any;
178
+ };
114
179
  export declare const LIQUID_X402_ASSETS: Record<LiquidX402Network, Record<LiquidX402AssetKey, LiquidX402Asset>>;
115
180
  export declare function listLiquidX402Assets(network?: LiquidX402Network): LiquidX402Asset[];
116
181
  export declare function resolveLiquidX402Asset(asset: LiquidX402AssetKey | string, network?: LiquidX402Network): LiquidX402Asset;
@@ -118,8 +183,10 @@ export declare function buildLiquidX402Requirements(input: LiquidX402Requirement
118
183
  export declare function encodeLiquidXPayment(payload: LiquidX402PaymentPayload): string;
119
184
  export declare function decodeLiquidXPayment(raw: string): LiquidX402PaymentPayload | null;
120
185
  export declare function prepareLiquidX402PsetPayment(rpc: ElementsRpcClient, input: LiquidX402PreparePsetPaymentInput): Promise<LiquidX402PreparePsetPaymentResult>;
186
+ export declare function buildLiquidX402PaymentFromPset(input: LiquidX402BuildPaymentFromPsetInput): LiquidX402PreparePsetPaymentResult;
187
+ export declare function prepareLiquidX402LwkWasmPayment(input: LiquidX402PrepareLwkWasmPaymentInput): Promise<LiquidX402PrepareLwkWasmPaymentResult>;
188
+ export declare function deriveLiquidX402LwkWasmAddress(input: LiquidX402DeriveLwkWasmAddressInput): Promise<LiquidX402DeriveLwkWasmAddressResult>;
121
189
  export declare function verifyLiquidX402Payment(rpc: ElementsRpcClient | null, input: LiquidX402VerifyPaymentInput): Promise<LiquidX402VerifyPaymentResult>;
122
190
  export declare function settleLiquidX402Payment(rpc: ElementsRpcClient, input: LiquidX402SettlePaymentInput): Promise<LiquidX402SettlePaymentResult>;
123
191
  export declare function verifyLiquidX402PayloadFields(requirements: LiquidX402PaymentRequirements | Record<string, unknown>, payload: LiquidX402PaymentPayload | Record<string, unknown>, now?: Date): LiquidX402VerifyPaymentResult;
124
- export declare function buildLiquidPsetSummaryHash(decoded: DecodePsbtResult, requirements: LiquidX402PaymentRequirements | Record<string, unknown>): string;
125
- export {};
192
+ export declare function buildLiquidPsetSummaryHash(_decoded: DecodePsbtResult, requirements: LiquidX402PaymentRequirements | Record<string, unknown>): string;
@@ -7,6 +7,9 @@ exports.buildLiquidX402Requirements = buildLiquidX402Requirements;
7
7
  exports.encodeLiquidXPayment = encodeLiquidXPayment;
8
8
  exports.decodeLiquidXPayment = decodeLiquidXPayment;
9
9
  exports.prepareLiquidX402PsetPayment = prepareLiquidX402PsetPayment;
10
+ exports.buildLiquidX402PaymentFromPset = buildLiquidX402PaymentFromPset;
11
+ exports.prepareLiquidX402LwkWasmPayment = prepareLiquidX402LwkWasmPayment;
12
+ exports.deriveLiquidX402LwkWasmAddress = deriveLiquidX402LwkWasmAddress;
10
13
  exports.verifyLiquidX402Payment = verifyLiquidX402Payment;
11
14
  exports.settleLiquidX402Payment = settleLiquidX402Payment;
12
15
  exports.verifyLiquidX402PayloadFields = verifyLiquidX402PayloadFields;
@@ -18,13 +21,15 @@ exports.LIQUID_X402_DEFAULT_NETWORK = "liquidtestnet";
18
21
  exports.LIQUID_X402_DEFAULT_TIMEOUT_SECONDS = 60;
19
22
  const LIQUID_TESTNET_USDT_ASSET_ID = "b612eb46313a2cd6ebabd8b7a8eed5696e29898b87a43bff41c94f51acef9d73";
20
23
  const LIQUID_MAINNET_USDT_ASSET_ID = "ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2";
24
+ const LIQUID_TESTNET_LBTC_ASSET_ID = "144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49";
25
+ const LIQUID_MAINNET_LBTC_ASSET_ID = "6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d";
21
26
  exports.LIQUID_X402_ASSETS = {
22
27
  liquidtestnet: {
23
28
  lbtc: {
24
29
  key: "lbtc",
25
30
  label: "Liquid Bitcoin",
26
31
  network: "liquidtestnet",
27
- assetId: "bitcoin",
32
+ assetId: LIQUID_TESTNET_LBTC_ASSET_ID,
28
33
  decimals: 8,
29
34
  isNative: true,
30
35
  },
@@ -42,7 +47,7 @@ exports.LIQUID_X402_ASSETS = {
42
47
  key: "lbtc",
43
48
  label: "Liquid Bitcoin",
44
49
  network: "liquidv1",
45
- assetId: "bitcoin",
50
+ assetId: LIQUID_MAINNET_LBTC_ASSET_ID,
46
51
  decimals: 8,
47
52
  isNative: true,
48
53
  },
@@ -161,6 +166,103 @@ async function prepareLiquidX402PsetPayment(rpc, input) {
161
166
  summaryHash,
162
167
  };
163
168
  }
169
+ function buildLiquidX402PaymentFromPset(input) {
170
+ const requirements = coerceRequirements(input.requirements);
171
+ const psetBase64 = String(input.psetBase64 ?? "").trim();
172
+ if (!psetBase64)
173
+ throw new Error("psetBase64 is required");
174
+ const summaryHash = input.summaryHash ?? buildLiquidPsetSummaryHash(input.decoded ?? {}, requirements);
175
+ const paymentPayload = {
176
+ scheme: exports.LIQUID_X402_SCHEME,
177
+ network: requirements.network,
178
+ paymentRequestId: requirements.extra.paymentRequestId,
179
+ asset: requirements.extra.asset,
180
+ assetId: requirements.asset,
181
+ amountAtomic: requirements.maxAmountRequired,
182
+ payTo: requirements.payTo,
183
+ psetBase64,
184
+ summaryHash,
185
+ ...(input.payer ? { payer: input.payer } : {}),
186
+ expiresAt: requirements.extra.expiresAt,
187
+ };
188
+ return {
189
+ paymentPayload,
190
+ xPayment: encodeLiquidXPayment(paymentPayload),
191
+ psetBase64,
192
+ summaryHash,
193
+ };
194
+ }
195
+ async function prepareLiquidX402LwkWasmPayment(input) {
196
+ const requirements = coerceRequirements(input.requirements);
197
+ ensureLwkWasmNodeTimerCompat();
198
+ const lwk = input.lwk ?? await loadLwkWasm(input.importLwk);
199
+ const network = requirements.network === "liquidv1" ? lwk.Network.mainnet() : lwk.Network.testnet();
200
+ const mnemonic = new lwk.Mnemonic(input.mnemonic);
201
+ const signer = new lwk.Signer(mnemonic, network);
202
+ const descriptor = signer.wpkhSlip77Descriptor();
203
+ const wollet = new lwk.Wollet(network, descriptor);
204
+ const client = input.esploraUrl
205
+ ? new lwk.EsploraClient(network, input.esploraUrl, input.waterfalls ?? false, input.concurrency ?? 4, input.utxoOnly ?? false)
206
+ : network.defaultEsploraClient();
207
+ if (input.scan !== false) {
208
+ const update = input.scanToIndex !== undefined
209
+ ? await client.fullScanToIndex(wollet, input.scanToIndex)
210
+ : await client.fullScan(wollet);
211
+ if (update)
212
+ wollet.applyUpdate(update);
213
+ }
214
+ let builder = network.txBuilder();
215
+ if (input.feeRate !== undefined)
216
+ builder = builder.feeRate(input.feeRate);
217
+ const recipient = parseLwkAddress(lwk, requirements.payTo, network);
218
+ const amount = BigInt(requirements.maxAmountRequired);
219
+ if (requirements.extra.asset === "lbtc") {
220
+ builder = builder.addLbtcRecipient(recipient, amount);
221
+ }
222
+ else if (typeof recipient.isBlinded === "function" && recipient.isBlinded() === false && typeof builder.addExplicitRecipient === "function") {
223
+ builder = builder.addExplicitRecipient(recipient, amount, parseLwkAssetId(lwk, requirements.asset));
224
+ }
225
+ else {
226
+ builder = builder.addRecipient(recipient, amount, parseLwkAssetId(lwk, requirements.asset));
227
+ }
228
+ const unsigned = builder.finish(wollet);
229
+ const signed = signer.sign(unsigned);
230
+ const pset = input.finalize === false ? signed : wollet.finalize(signed);
231
+ const decoded = decodeLwkPsetForX402(pset, wollet);
232
+ const payment = buildLiquidX402PaymentFromPset({
233
+ requirements,
234
+ psetBase64: pset.toString(),
235
+ decoded,
236
+ payer: input.payer,
237
+ });
238
+ return {
239
+ ...payment,
240
+ descriptor: descriptor.toString(),
241
+ ...(typeof wollet.dwid === "function" ? { dwid: wollet.dwid() } : {}),
242
+ };
243
+ }
244
+ async function deriveLiquidX402LwkWasmAddress(input) {
245
+ const networkKey = normalizeNetwork(input.network ?? exports.LIQUID_X402_DEFAULT_NETWORK);
246
+ ensureLwkWasmNodeTimerCompat();
247
+ const lwk = input.lwk ?? await loadLwkWasm(input.importLwk);
248
+ const network = networkKey === "liquidv1" ? lwk.Network.mainnet() : lwk.Network.testnet();
249
+ const mnemonic = new lwk.Mnemonic(input.mnemonic);
250
+ const signer = new lwk.Signer(mnemonic, network);
251
+ const descriptor = signer.wpkhSlip77Descriptor();
252
+ const wollet = new lwk.Wollet(network, descriptor);
253
+ const addressResult = wollet.address(input.index ?? null);
254
+ const address = addressResult.address();
255
+ return {
256
+ network: networkKey,
257
+ address: address.toString(),
258
+ ...(typeof address.toUnconfidential === "function" ? { unconfidentialAddress: address.toUnconfidential().toString() } : {}),
259
+ index: typeof addressResult.index === "function" ? addressResult.index() : input.index ?? 0,
260
+ ...(typeof address.isBlinded === "function" ? { isBlinded: address.isBlinded() } : {}),
261
+ descriptor: descriptor.toString(),
262
+ ...(typeof wollet.dwid === "function" ? { dwid: wollet.dwid() } : {}),
263
+ ...(typeof network.policyAsset === "function" ? { policyAsset: network.policyAsset().toString() } : {}),
264
+ };
265
+ }
164
266
  async function verifyLiquidX402Payment(rpc, input) {
165
267
  const requirements = coerceRequirements(input.requirements);
166
268
  const payload = coercePaymentPayload(input.paymentPayload);
@@ -171,12 +273,13 @@ async function verifyLiquidX402Payment(rpc, input) {
171
273
  return basic;
172
274
  }
173
275
  try {
276
+ const expectedTarget = await resolveExpectedLiquidOutputTarget(rpc, requirements);
174
277
  const decoded = await rpc.call("decodepsbt", [payload.psetBase64]);
175
278
  const actualSummaryHash = buildLiquidPsetSummaryHash(decoded, requirements);
176
279
  if (actualSummaryHash !== payload.summaryHash) {
177
280
  return { ...basic, isValid: false, invalidReason: "pset_summary_mismatch" };
178
281
  }
179
- if (!decodedContainsExpectedOutput(decoded, requirements)) {
282
+ if (!decodedContainsExpectedOutput(decoded, requirements, expectedTarget)) {
180
283
  return { ...basic, isValid: false, invalidReason: "pset_expected_output_missing" };
181
284
  }
182
285
  if (!decodedFeeWithinCap(decoded, requirements)) {
@@ -257,7 +360,7 @@ function verifyLiquidX402PayloadFields(requirements, payload, now = new Date())
257
360
  }
258
361
  return { ...base, isValid: true };
259
362
  }
260
- function buildLiquidPsetSummaryHash(decoded, requirements) {
363
+ function buildLiquidPsetSummaryHash(_decoded, requirements) {
261
364
  const req = coerceRequirements(requirements);
262
365
  return sha256Hex(stableStringify({
263
366
  scheme: exports.LIQUID_X402_SCHEME,
@@ -267,24 +370,66 @@ function buildLiquidPsetSummaryHash(decoded, requirements) {
267
370
  assetId: req.asset,
268
371
  amountAtomic: req.maxAmountRequired,
269
372
  payTo: req.payTo,
270
- outputs: normalizeDecodedOutputs(decoded),
271
- fees: decoded.fees ?? {},
373
+ expectedOutput: {
374
+ amountAtomic: req.maxAmountRequired,
375
+ assetId: req.asset,
376
+ payTo: req.payTo,
377
+ },
378
+ feeAsset: req.extra.feeAsset,
379
+ feeAssetId: req.extra.feeAssetId ?? exports.LIQUID_X402_ASSETS[req.network].lbtc.assetId,
380
+ maxFeeSat: req.extra.maxFeeSat ?? null,
272
381
  }));
273
382
  }
274
- function decodedContainsExpectedOutput(decoded, requirements) {
383
+ function decodedContainsExpectedOutput(decoded, requirements, expectedTarget = buildFallbackExpectedLiquidOutputTarget(requirements)) {
275
384
  const expectedAmount = atomicToDecimalNumber(requirements.maxAmountRequired, requirements.extra.decimals);
276
385
  return (decoded.outputs ?? []).some((output) => (String(output.asset ?? "").toLowerCase() === requirements.asset.toLowerCase() &&
277
386
  Math.round(Number(output.amount ?? 0) * 10 ** requirements.extra.decimals) ===
278
387
  Math.round(expectedAmount * 10 ** requirements.extra.decimals) &&
279
- String(output.script?.address ?? "") === requirements.payTo));
280
- }
281
- function normalizeDecodedOutputs(decoded) {
282
- return (decoded.outputs ?? []).map((output) => ({
283
- amount: output.amount ?? null,
284
- asset: output.asset ?? null,
285
- address: output.script?.address ?? null,
286
- scriptPubKey: output.script?.hex ?? null,
287
- }));
388
+ decodedOutputMatchesLiquidTarget(output, expectedTarget)));
389
+ }
390
+ async function resolveExpectedLiquidOutputTarget(rpc, requirements) {
391
+ const target = buildFallbackExpectedLiquidOutputTarget(requirements);
392
+ try {
393
+ const info = await rpc.call("validateaddress", [requirements.payTo]);
394
+ if (info?.isvalid !== false) {
395
+ addNormalizedAddress(target.addresses, info.address);
396
+ addNormalizedAddress(target.addresses, info.unconfidential);
397
+ addNormalizedAddress(target.addresses, info.confidential);
398
+ addNormalizedHex(target.scriptHexes, info.scriptPubKey);
399
+ }
400
+ }
401
+ catch {
402
+ // Older or mocked RPC clients may not expose validateaddress. Exact payTo
403
+ // matching remains safe for unconfidential outputs in that case.
404
+ }
405
+ return target;
406
+ }
407
+ function buildFallbackExpectedLiquidOutputTarget(requirements) {
408
+ const target = { addresses: new Set(), scriptHexes: new Set() };
409
+ addNormalizedAddress(target.addresses, requirements.payTo);
410
+ return target;
411
+ }
412
+ function decodedOutputMatchesLiquidTarget(output, expectedTarget) {
413
+ const address = output.script?.address ?? output.scriptPubKey?.address;
414
+ const scriptHex = output.script?.hex ?? output.scriptPubKey?.hex;
415
+ return (expectedTarget.addresses.has(normalizeAddressForComparison(address)) ||
416
+ expectedTarget.scriptHexes.has(normalizeHexForComparison(scriptHex)));
417
+ }
418
+ function addNormalizedAddress(target, value) {
419
+ const normalized = normalizeAddressForComparison(value);
420
+ if (normalized)
421
+ target.add(normalized);
422
+ }
423
+ function addNormalizedHex(target, value) {
424
+ const normalized = normalizeHexForComparison(value);
425
+ if (normalized)
426
+ target.add(normalized);
427
+ }
428
+ function normalizeAddressForComparison(value) {
429
+ return String(value ?? "").trim().toLowerCase();
430
+ }
431
+ function normalizeHexForComparison(value) {
432
+ return String(value ?? "").trim().toLowerCase().replace(/^0x/u, "");
288
433
  }
289
434
  function coerceRequirements(value) {
290
435
  if (!value || typeof value !== "object" || Array.isArray(value))
@@ -293,6 +438,8 @@ function coerceRequirements(value) {
293
438
  const extra = raw.extra && typeof raw.extra === "object" && !Array.isArray(raw.extra) ? raw.extra : {};
294
439
  const network = normalizeNetwork(raw.network);
295
440
  const asset = resolveLiquidX402Asset(extra.asset ?? raw.asset, network);
441
+ const rawAssetId = String(raw.asset ?? extra.assetId ?? asset.assetId);
442
+ const assetId = isLiquidAssetAlias(rawAssetId) ? asset.assetId : rawAssetId;
296
443
  return {
297
444
  scheme: exports.LIQUID_X402_SCHEME,
298
445
  network,
@@ -302,11 +449,11 @@ function coerceRequirements(value) {
302
449
  mimeType: String(raw.mimeType ?? ""),
303
450
  payTo: String(raw.payTo ?? ""),
304
451
  maxTimeoutSeconds: normalizeTimeout(raw.maxTimeoutSeconds),
305
- asset: String(raw.asset ?? extra.assetId ?? asset.assetId),
452
+ asset: assetId,
306
453
  extra: {
307
454
  paymentRequestId: String(extra.paymentRequestId ?? raw.paymentRequestId ?? ""),
308
455
  asset: asset.key,
309
- assetId: String(extra.assetId ?? raw.asset ?? asset.assetId),
456
+ assetId,
310
457
  decimals: Number(extra.decimals ?? asset.decimals),
311
458
  expiresAt: String(extra.expiresAt ?? ""),
312
459
  feeAsset: "lbtc",
@@ -326,6 +473,66 @@ function decodedFeeWithinCap(decoded, requirements) {
326
473
  }, 0n);
327
474
  return feeSat <= maxFeeSat;
328
475
  }
476
+ async function loadLwkWasm(importer) {
477
+ ensureLwkWasmNodeTimerCompat();
478
+ if (importer)
479
+ return importer();
480
+ const dynamicImport = new Function("specifier", "return import(specifier)");
481
+ const failures = [];
482
+ for (const specifier of ["lwk_wasm", "lwk_node"]) {
483
+ try {
484
+ const imported = await dynamicImport(specifier);
485
+ return imported.default ?? imported;
486
+ }
487
+ catch (error) {
488
+ failures.push(`${specifier}: ${error instanceof Error ? error.message : String(error)}`);
489
+ }
490
+ }
491
+ throw new Error("lwk_wasm or lwk_node is required for lightweight Liquid x402 payments. " +
492
+ "Install one of them in the consuming app and bundle its WASM artifact when needed. " +
493
+ `Import failed: ${failures.join("; ")}`);
494
+ }
495
+ function ensureLwkWasmNodeTimerCompat() {
496
+ const scope = globalThis;
497
+ if (typeof scope.window === "undefined" && typeof scope.setTimeout === "function") {
498
+ scope.window = scope;
499
+ }
500
+ if (typeof scope.Window === "undefined" && typeof scope.window !== "undefined") {
501
+ scope.Window = Object;
502
+ }
503
+ }
504
+ function parseLwkAddress(lwk, address, network) {
505
+ if (typeof lwk.Address.parse === "function") {
506
+ try {
507
+ return lwk.Address.parse(address, network);
508
+ }
509
+ catch (error) {
510
+ const message = error instanceof Error ? error.message : String(error);
511
+ if (!message.includes("non-blinded"))
512
+ throw error;
513
+ }
514
+ }
515
+ return new lwk.Address(address);
516
+ }
517
+ function parseLwkAssetId(lwk, assetId) {
518
+ return typeof lwk.AssetId.fromString === "function" ? lwk.AssetId.fromString(assetId) : new lwk.AssetId(assetId);
519
+ }
520
+ function decodeLwkPsetForX402(pset, wollet) {
521
+ const details = wollet.psetDetails(pset);
522
+ const balance = details.balance();
523
+ const recipients = typeof balance.recipients === "function" ? balance.recipients() : [];
524
+ const fee = typeof balance.fee === "function" ? balance.fee() : 0n;
525
+ return {
526
+ fees: { bitcoin: atomicToDecimalNumber(String(fee), 8) },
527
+ outputs: recipients.map((recipient) => ({
528
+ amount: recipient.value() === undefined ? undefined : atomicToDecimalNumber(String(recipient.value()), 8),
529
+ asset: recipient.asset()?.toString(),
530
+ script: {
531
+ address: recipient.address()?.toString(),
532
+ },
533
+ })),
534
+ };
535
+ }
329
536
  function coercePaymentPayload(value) {
330
537
  if (!value || typeof value !== "object" || Array.isArray(value))
331
538
  throw new Error("payment payload must be an object");
@@ -352,12 +559,26 @@ function normalizeNetwork(input) {
352
559
  }
353
560
  function normalizeAssetKey(input) {
354
561
  const value = String(input ?? "").trim().toLowerCase();
355
- if (value === "lbtc" || value === "bitcoin")
562
+ if (value === "lbtc" || value === "bitcoin" || value === LIQUID_TESTNET_LBTC_ASSET_ID || value === LIQUID_MAINNET_LBTC_ASSET_ID) {
356
563
  return "lbtc";
357
- if (value === "usdt" || value === "usdt-liquid" || value === "tether")
564
+ }
565
+ if (value === "usdt" ||
566
+ value === "usdt-liquid" ||
567
+ value === "tether" ||
568
+ value === LIQUID_TESTNET_USDT_ASSET_ID ||
569
+ value === LIQUID_MAINNET_USDT_ASSET_ID) {
358
570
  return "usdt";
571
+ }
359
572
  throw new Error(`unsupported Liquid x402 asset: ${String(input)}`);
360
573
  }
574
+ function isLiquidAssetAlias(input) {
575
+ const value = String(input ?? "").trim().toLowerCase();
576
+ return value === "lbtc" ||
577
+ value === "bitcoin" ||
578
+ value === "usdt" ||
579
+ value === "usdt-liquid" ||
580
+ value === "tether";
581
+ }
361
582
  function normalizeAmountAtomic(input) {
362
583
  const raw = typeof input === "bigint" ? input.toString() : String(input ?? "").trim();
363
584
  if (!/^\d+$/u.test(raw))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hazbase/simplicity",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "An SDK for Simplicity on Liquid",
5
5
  "author": "IndieSquare Inc <info@hazbase.com>",
6
6
  "keywords": [
@@ -50,6 +50,8 @@
50
50
  "e2e:receivable-local": "npm run build && node scripts/e2e-receivable-local.mjs",
51
51
  "e2e:receivable-testnet": "npm run build && node scripts/e2e-receivable-testnet.mjs",
52
52
  "e2e:receivable-consumer": "npm run build && node scripts/e2e-receivable-consumer.mjs",
53
+ "e2e:x402-lwk-address": "npm run build && node scripts/e2e-x402-lwk-address.mjs",
54
+ "e2e:x402-lwk-buyer": "npm run build && node scripts/e2e-x402-lwk-buyer.mjs",
53
55
  "e2e:simplicity-relayer": "npm run build && node scripts/e2e-simplicity-relayer.mjs",
54
56
  "prepublishOnly": "npm run build"
55
57
  },