@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 +16 -0
- package/dist/client/SimplicityClient.d.ts +3 -0
- package/dist/client/SimplicityClient.js +3 -0
- package/dist/x402/index.d.ts +70 -3
- package/dist/x402/index.js +241 -20
- package/package.json +3 -1
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
|
},
|
package/dist/x402/index.d.ts
CHANGED
|
@@ -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(
|
|
125
|
-
export {};
|
|
192
|
+
export declare function buildLiquidPsetSummaryHash(_decoded: DecodePsbtResult, requirements: LiquidX402PaymentRequirements | Record<string, unknown>): string;
|
package/dist/x402/index.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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(
|
|
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
|
-
|
|
271
|
-
|
|
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
|
-
|
|
280
|
-
}
|
|
281
|
-
function
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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:
|
|
452
|
+
asset: assetId,
|
|
306
453
|
extra: {
|
|
307
454
|
paymentRequestId: String(extra.paymentRequestId ?? raw.paymentRequestId ?? ""),
|
|
308
455
|
asset: asset.key,
|
|
309
|
-
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
|
-
|
|
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.
|
|
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
|
},
|