@hazbase/simplicity 0.4.1 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -2
- package/dist/client/SimplicityClient.d.ts +5 -0
- package/dist/client/SimplicityClient.js +5 -0
- package/dist/core/executor.d.ts +3 -1
- package/dist/core/executor.js +307 -8
- package/dist/core/rpc.js +3 -1
- package/dist/core/toolchain.d.ts +24 -0
- package/dist/core/toolchain.js +106 -2
- package/dist/core/types.d.ts +44 -0
- package/dist/docs/definitions/rwa-dvp-escrow.simf +128 -0
- package/dist/domain/rwaDvp.d.ts +76 -1
- package/dist/domain/rwaDvp.js +427 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +7 -2
- package/dist/x402/atomicDvp.d.ts +197 -0
- package/dist/x402/atomicDvp.js +482 -0
- package/dist/x402/index.d.ts +1 -0
- package/dist/x402/index.js +15 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,20 +20,25 @@ With the current public SDK you can build and test:
|
|
|
20
20
|
- bond redemption / settlement / close-out flows
|
|
21
21
|
- LP fund capital call / distribution / close-out flows
|
|
22
22
|
- receivable repayment-first funding / partial repayment / closing flows
|
|
23
|
+
- RWA delivery-versus-payment flows over Liquid PSETs and Liquid x402
|
|
24
|
+
- atomic Liquid DvP proposals where service delivery and buyer payment settle in one PSET
|
|
23
25
|
- evidence, trust summary, lineage, and finality exports
|
|
24
26
|
|
|
25
27
|
## Public Architecture
|
|
26
28
|
|
|
27
|
-
The public SDK is organized into
|
|
29
|
+
The public SDK is organized into domain clients and lower-level payment helpers:
|
|
28
30
|
- `sdk.outputBinding`: shared output-binding support, evaluation, and fallback behavior
|
|
29
31
|
- `sdk.policies`: generic constrained transfer and recursive policy engine
|
|
30
32
|
- `sdk.bonds`: private bond / credit settlement business layer
|
|
31
33
|
- `sdk.funds`: LP fund settlement business layer
|
|
32
34
|
- `sdk.receivables`: repayment-first receivable business layer
|
|
35
|
+
- `sdk.rwaDvp`: RWA purchase terms, payment requirements, claim descriptors, and evidence
|
|
36
|
+
- `sdk.payments.x402`: Liquid x402 assets, PSET payment helpers, verification, and settlement
|
|
33
37
|
|
|
34
38
|
A useful mental model is:
|
|
35
39
|
- `sdk.outputBinding` + `sdk.policies` provide the shared settlement kernel
|
|
36
|
-
- `sdk.bonds`, `sdk.funds`, and `sdk.
|
|
40
|
+
- `sdk.bonds`, `sdk.funds`, `sdk.receivables`, and `sdk.rwaDvp` build domain flows on top of that kernel
|
|
41
|
+
- `sdk.payments.x402` and the root-level `x402` exports provide the Liquid PSET payment layer used by those flows
|
|
37
42
|
|
|
38
43
|
## Quickstart
|
|
39
44
|
|
|
@@ -158,12 +163,121 @@ Use `sdk.rwaDvp` when a purchase flow needs a Liquid PSET payment request,
|
|
|
158
163
|
delivery/refund claim descriptors, and an evidence bundle that ties the Liquid
|
|
159
164
|
payment and delivery to an external lock or allocation record.
|
|
160
165
|
|
|
166
|
+
The EVM-side lock reference can describe the source asset being held for
|
|
167
|
+
settlement. Set `evmLock.tokenStandard` to `"ERC3475"`, `"ERC20"`, `"ERC721"`,
|
|
168
|
+
or `"ERC1155"` and include the token address / id fields used by your lock
|
|
169
|
+
manager. Existing callers that omit `tokenStandard` are treated as `"ERC3475"`
|
|
170
|
+
for backwards compatibility.
|
|
171
|
+
|
|
161
172
|
For standard Liquid assets, `payment.asset` can be `"lbtc"` or `"usdt"`. If a
|
|
162
173
|
testnet issuer or deployment uses a different USDt asset id, set
|
|
163
174
|
`payment.asset: "usdt"` and pass the explicit `payment.assetId`; the generated
|
|
164
175
|
payment requirements will preserve that asset id instead of replacing it with
|
|
165
176
|
the SDK's default registry id.
|
|
166
177
|
|
|
178
|
+
Typical entrypoints:
|
|
179
|
+
- `sdk.rwaDvp.definePurchase(...)`
|
|
180
|
+
- `sdk.rwaDvp.buildPaymentRequirements(...)`
|
|
181
|
+
- `sdk.rwaDvp.verifyPaymentPset(...)`
|
|
182
|
+
- `sdk.rwaDvp.compileEscrowContract(...)`
|
|
183
|
+
- `sdk.rwaDvp.prepareDeliveryClaim(...)`
|
|
184
|
+
- `sdk.rwaDvp.inspectDeliveryClaim(...)`
|
|
185
|
+
- `sdk.rwaDvp.executeDeliveryClaim(...)`
|
|
186
|
+
- `sdk.rwaDvp.prepareRefundClaim(...)`
|
|
187
|
+
- `sdk.rwaDvp.inspectRefundClaim(...)`
|
|
188
|
+
- `sdk.rwaDvp.executeRefundClaim(...)`
|
|
189
|
+
- `sdk.rwaDvp.exportEvidence(...)`
|
|
190
|
+
|
|
191
|
+
For a delivery claim, the operator spends the funded escrow output and sends
|
|
192
|
+
the payment asset to the treasury while delivering the RWA asset to the buyer.
|
|
193
|
+
For a refund claim, the operator spends the same escrow output after the
|
|
194
|
+
configured timeout and returns the payment asset to the buyer. The inspect
|
|
195
|
+
methods build and validate the candidate spend without broadcasting it; the
|
|
196
|
+
execute methods finalize the Simplicity input, optionally test mempool
|
|
197
|
+
acceptance, and broadcast when `broadcast: true`.
|
|
198
|
+
|
|
199
|
+
The claim execution helpers assume the Elements wallet can provide any extra
|
|
200
|
+
inputs needed for RWA delivery and L-BTC fees. You can also pass explicit
|
|
201
|
+
`extraInputs` when the service wants deterministic coin selection. Script-bound
|
|
202
|
+
delivery/refund output checks are the practical default; descriptor-bound checks
|
|
203
|
+
should be used only when the caller can provide the exact output data required
|
|
204
|
+
by the descriptor.
|
|
205
|
+
|
|
206
|
+
#### Atomic Liquid DvP PSET helpers
|
|
207
|
+
|
|
208
|
+
For flows that should avoid a separate "buyer pays first, service delivers
|
|
209
|
+
later" step, the SDK also exposes lower-level atomic DvP helpers from the root
|
|
210
|
+
package:
|
|
211
|
+
|
|
212
|
+
- `buildLiquidAtomicDvpRequirements(...)`
|
|
213
|
+
- `prepareLiquidAtomicDvpLwkWasmMakerProposal(...)`
|
|
214
|
+
- `prepareLiquidAtomicDvpLwkWasmTakerPayment(...)`
|
|
215
|
+
- `buildLiquidAtomicDvpPaymentFromPset(...)`
|
|
216
|
+
- `verifyLiquidAtomicDvpPayment(...)`
|
|
217
|
+
- `encodeLiquidAtomicDvpPayment(...)`
|
|
218
|
+
- `decodeLiquidAtomicDvpPayment(...)`
|
|
219
|
+
|
|
220
|
+
These helpers model a Liquidex-style exchange:
|
|
221
|
+
- the service/maker selects an exact RWA delivery UTXO and creates a proposal
|
|
222
|
+
- the buyer/taker adds the required payment output and signs the combined PSET
|
|
223
|
+
- the resulting `X-PAYMENT` payload commits to both the payment output and the
|
|
224
|
+
delivery output through a summary hash
|
|
225
|
+
|
|
226
|
+
The LWK convenience helpers dynamically load `lwk_node` or `lwk_wasm` from the
|
|
227
|
+
consuming application. Install one of them in the application that prepares or
|
|
228
|
+
takes proposals:
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
npm install lwk_node
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Typical server-side shape:
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
import {
|
|
238
|
+
buildLiquidAtomicDvpRequirements,
|
|
239
|
+
prepareLiquidAtomicDvpLwkWasmMakerProposal,
|
|
240
|
+
verifyLiquidAtomicDvpPayment,
|
|
241
|
+
} from "@hazbase/simplicity";
|
|
242
|
+
|
|
243
|
+
const requirements = buildLiquidAtomicDvpRequirements({
|
|
244
|
+
network: "liquidtestnet",
|
|
245
|
+
resource: "/v1/orders/<order-id>/liquid-atomic-pset",
|
|
246
|
+
paymentToTreasury: {
|
|
247
|
+
assetId: "<lbtc-or-usdt-asset-id>",
|
|
248
|
+
amountAtomic: "10000",
|
|
249
|
+
recipient: "<treasury-confidential-address>",
|
|
250
|
+
},
|
|
251
|
+
rwaToBuyer: {
|
|
252
|
+
assetId: "<rwa-liquid-asset-id>",
|
|
253
|
+
amountAtomic: "10",
|
|
254
|
+
recipient: "<buyer-confidential-address>",
|
|
255
|
+
},
|
|
256
|
+
expiresAt: new Date(Date.now() + 15 * 60_000),
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const maker = await prepareLiquidAtomicDvpLwkWasmMakerProposal({
|
|
260
|
+
requirements,
|
|
261
|
+
mnemonic: process.env.SERVICE_LIQUID_MNEMONIC!,
|
|
262
|
+
descriptor: process.env.SERVICE_LIQUID_DESCRIPTOR!,
|
|
263
|
+
electrumUrl: process.env.LIQUID_ELECTRUM_URL!,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Store maker.proposalPsetBase64 with the order/payment requirements.
|
|
267
|
+
// When the buyer submits X-PAYMENT, verify it before broadcasting.
|
|
268
|
+
const verified = verifyLiquidAtomicDvpPayment({
|
|
269
|
+
requirements,
|
|
270
|
+
paymentPayload: buyerPaymentPayload,
|
|
271
|
+
});
|
|
272
|
+
if (!verified.ok) throw new Error(verified.reason);
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
The SDK-level payload check is intentionally lightweight: it verifies the
|
|
276
|
+
scheme, network, resource, expiry, PSET value, and summary hash. Production
|
|
277
|
+
settlement services should still decode the final PSET with their Liquid node or
|
|
278
|
+
wallet stack, verify the concrete payment and delivery outputs, and only then
|
|
279
|
+
broadcast.
|
|
280
|
+
|
|
167
281
|
## CLI and Confidence Commands
|
|
168
282
|
|
|
169
283
|
### Validation Surface
|
|
@@ -38,10 +38,15 @@ export declare class SimplicityClient {
|
|
|
38
38
|
definePurchase: (input: Parameters<typeof rwaDvp.definePurchase>[1]) => ReturnType<typeof rwaDvp.definePurchase>;
|
|
39
39
|
buildPaymentRequirements: (input: Parameters<typeof rwaDvp.buildPaymentRequirements>[1]) => ReturnType<typeof rwaDvp.buildPaymentRequirements>;
|
|
40
40
|
verifyPaymentPset: (input: Parameters<typeof rwaDvp.verifyPaymentPset>[1]) => ReturnType<typeof rwaDvp.verifyPaymentPset>;
|
|
41
|
+
compileEscrowContract: (input: Parameters<typeof rwaDvp.compileEscrowContract>[1]) => ReturnType<typeof rwaDvp.compileEscrowContract>;
|
|
41
42
|
prepareDeliveryClaim: (input: Parameters<typeof rwaDvp.prepareDeliveryClaim>[1]) => ReturnType<typeof rwaDvp.prepareDeliveryClaim>;
|
|
42
43
|
verifyDeliveryClaim: (input: Parameters<typeof rwaDvp.verifyDeliveryClaim>[1]) => ReturnType<typeof rwaDvp.verifyDeliveryClaim>;
|
|
44
|
+
inspectDeliveryClaim: (input: Parameters<typeof rwaDvp.inspectDeliveryClaim>[1]) => ReturnType<typeof rwaDvp.inspectDeliveryClaim>;
|
|
45
|
+
executeDeliveryClaim: (input: Parameters<typeof rwaDvp.executeDeliveryClaim>[1]) => ReturnType<typeof rwaDvp.executeDeliveryClaim>;
|
|
43
46
|
prepareRefundClaim: (input: Parameters<typeof rwaDvp.prepareRefundClaim>[1]) => ReturnType<typeof rwaDvp.prepareRefundClaim>;
|
|
44
47
|
verifyRefundClaim: (input: Parameters<typeof rwaDvp.verifyRefundClaim>[1]) => ReturnType<typeof rwaDvp.verifyRefundClaim>;
|
|
48
|
+
inspectRefundClaim: (input: Parameters<typeof rwaDvp.inspectRefundClaim>[1]) => ReturnType<typeof rwaDvp.inspectRefundClaim>;
|
|
49
|
+
executeRefundClaim: (input: Parameters<typeof rwaDvp.executeRefundClaim>[1]) => ReturnType<typeof rwaDvp.executeRefundClaim>;
|
|
45
50
|
exportEvidence: (input: Parameters<typeof rwaDvp.exportEvidence>[1]) => ReturnType<typeof rwaDvp.exportEvidence>;
|
|
46
51
|
};
|
|
47
52
|
readonly policies: {
|
|
@@ -88,10 +88,15 @@ class SimplicityClient {
|
|
|
88
88
|
definePurchase: (input) => rwaDvp.definePurchase(this, input),
|
|
89
89
|
buildPaymentRequirements: (input) => rwaDvp.buildPaymentRequirements(this, input),
|
|
90
90
|
verifyPaymentPset: (input) => rwaDvp.verifyPaymentPset(this, input),
|
|
91
|
+
compileEscrowContract: (input) => rwaDvp.compileEscrowContract(this, input),
|
|
91
92
|
prepareDeliveryClaim: (input) => rwaDvp.prepareDeliveryClaim(this, input),
|
|
92
93
|
verifyDeliveryClaim: (input) => rwaDvp.verifyDeliveryClaim(this, input),
|
|
94
|
+
inspectDeliveryClaim: (input) => rwaDvp.inspectDeliveryClaim(this, input),
|
|
95
|
+
executeDeliveryClaim: (input) => rwaDvp.executeDeliveryClaim(this, input),
|
|
93
96
|
prepareRefundClaim: (input) => rwaDvp.prepareRefundClaim(this, input),
|
|
94
97
|
verifyRefundClaim: (input) => rwaDvp.verifyRefundClaim(this, input),
|
|
98
|
+
inspectRefundClaim: (input) => rwaDvp.inspectRefundClaim(this, input),
|
|
99
|
+
executeRefundClaim: (input) => rwaDvp.executeRefundClaim(this, input),
|
|
95
100
|
exportEvidence: (input) => rwaDvp.exportEvidence(this, input),
|
|
96
101
|
};
|
|
97
102
|
this.policies = {
|
package/dist/core/executor.d.ts
CHANGED
|
@@ -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>;
|
package/dist/core/executor.js
CHANGED
|
@@ -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
|
|
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:
|
|
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
|
|
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
|
|
524
|
-
const contractFinalized = (await (0, toolchain_1.
|
|
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(
|
|
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
|
-
|
|
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
|
});
|
package/dist/core/toolchain.d.ts
CHANGED
|
@@ -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>;
|