@dcasegr/amm-client 0.0.0-stage → 0.38.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 +107 -2
- package/dist/client.d.ts +534 -0
- package/dist/client.js +1319 -0
- package/dist/errors.d.ts +17 -0
- package/dist/errors.js +75 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +53 -0
- package/dist/lib/config.d.ts +42 -0
- package/dist/lib/config.js +64 -0
- package/dist/lib/keys.d.ts +42 -0
- package/dist/lib/keys.js +48 -0
- package/dist/lib/overlay.d.ts +190 -0
- package/dist/lib/overlay.js +302 -0
- package/dist/lib/skein.d.ts +82 -0
- package/dist/lib/skein.js +142 -0
- package/dist/lib/tokenId.d.ts +72 -0
- package/dist/lib/tokenId.js +121 -0
- package/dist/lp/amounts.d.ts +12 -0
- package/dist/lp/amounts.js +38 -0
- package/dist/lp/close.d.ts +90 -0
- package/dist/lp/close.js +140 -0
- package/dist/lp/images.d.ts +11 -0
- package/dist/lp/images.js +45 -0
- package/dist/lp/inventory.d.ts +68 -0
- package/dist/lp/inventory.js +84 -0
- package/dist/lp/poolDeploy.d.ts +220 -0
- package/dist/lp/poolDeploy.js +291 -0
- package/dist/lp/poolRows.d.ts +13 -0
- package/dist/lp/poolRows.js +19 -0
- package/dist/lp/positions.d.ts +81 -0
- package/dist/lp/positions.js +144 -0
- package/dist/lp/rebalance.d.ts +101 -0
- package/dist/lp/rebalance.js +168 -0
- package/dist/lp/wallet.d.ts +12 -0
- package/dist/lp/wallet.js +19 -0
- package/dist/market/activeSwap.d.ts +158 -0
- package/dist/market/activeSwap.js +320 -0
- package/dist/market/plan.d.ts +85 -0
- package/dist/market/plan.js +149 -0
- package/dist/market/prices.d.ts +13 -0
- package/dist/market/prices.js +14 -0
- package/dist/market/relay.d.ts +84 -0
- package/dist/market/relay.js +201 -0
- package/dist/market/swapAction.d.ts +306 -0
- package/dist/market/swapAction.js +508 -0
- package/dist/market/swapFlow.d.ts +58 -0
- package/dist/market/swapFlow.js +86 -0
- package/dist/market/view.d.ts +58 -0
- package/dist/market/view.js +63 -0
- package/dist/pool/artifact.d.ts +23 -0
- package/dist/pool/artifact.js +38 -0
- package/dist/pool/index.d.ts +8 -0
- package/dist/pool/index.js +8 -0
- package/dist/pool/oneColor.d.ts +19 -0
- package/dist/pool/oneColor.js +39 -0
- package/dist/pool/pool.artifact.json +5069 -0
- package/dist/pool/runar-ir-schema.d.ts +21 -0
- package/dist/pool/runar-ir-schema.js +18 -0
- package/dist/pool/template.d.ts +295 -0
- package/dist/pool/template.js +715 -0
- package/dist/vendor/runar/LICENSE +21 -0
- package/dist/vendor/runar/NOTICE +15 -0
- package/dist/vendor/runar/runar-ir-schema/src/abi-type-encoding.d.ts +42 -0
- package/dist/vendor/runar/runar-ir-schema/src/abi-type-encoding.js +53 -0
- package/dist/vendor/runar/runar-ir-schema/src/anf-ir.d.ts +221 -0
- package/dist/vendor/runar/runar-ir-schema/src/anf-ir.js +33 -0
- package/dist/vendor/runar/runar-ir-schema/src/artifact.d.ts +288 -0
- package/dist/vendor/runar/runar-ir-schema/src/artifact.js +8 -0
- package/dist/vendor/runar/runar-ir-schema/src/canonical-json.d.ts +43 -0
- package/dist/vendor/runar/runar-ir-schema/src/canonical-json.js +204 -0
- package/dist/vendor/runar/runar-ir-schema/src/input-limits.d.ts +89 -0
- package/dist/vendor/runar/runar-ir-schema/src/input-limits.js +100 -0
- package/dist/vendor/runar/runar-ir-schema/src/stack-ir.d.ts +118 -0
- package/dist/vendor/runar/runar-ir-schema/src/stack-ir.js +8 -0
- package/dist/vendor/runar/runar-ir-schema/src/state-layout.d.ts +56 -0
- package/dist/vendor/runar/runar-ir-schema/src/state-layout.js +145 -0
- package/dist/vendor/runar/runar-ir-schema/src/unknown-anf-kind-error.d.ts +18 -0
- package/dist/vendor/runar/runar-ir-schema/src/unknown-anf-kind-error.js +25 -0
- package/dist/vendor/runar/runar-sdk/src/anf-interpreter.d.ts +160 -0
- package/dist/vendor/runar/runar-sdk/src/anf-interpreter.js +1149 -0
- package/dist/vendor/runar/runar-sdk/src/calling.d.ts +98 -0
- package/dist/vendor/runar/runar-sdk/src/calling.js +209 -0
- package/dist/vendor/runar/runar-sdk/src/contract.d.ts +443 -0
- package/dist/vendor/runar/runar-sdk/src/contract.js +2516 -0
- package/dist/vendor/runar/runar-sdk/src/deployment.d.ts +36 -0
- package/dist/vendor/runar/runar-sdk/src/deployment.js +119 -0
- package/dist/vendor/runar/runar-sdk/src/errors.d.ts +46 -0
- package/dist/vendor/runar/runar-sdk/src/errors.js +59 -0
- package/dist/vendor/runar/runar-sdk/src/oppushtx.d.ts +23 -0
- package/dist/vendor/runar/runar-sdk/src/oppushtx.js +95 -0
- package/dist/vendor/runar/runar-sdk/src/ordinals/envelope.d.ts +31 -0
- package/dist/vendor/runar/runar-sdk/src/ordinals/envelope.js +272 -0
- package/dist/vendor/runar/runar-sdk/src/ordinals/types.d.ts +21 -0
- package/dist/vendor/runar/runar-sdk/src/ordinals/types.js +4 -0
- package/dist/vendor/runar/runar-sdk/src/providers/provider.d.ts +65 -0
- package/dist/vendor/runar/runar-sdk/src/providers/provider.js +59 -0
- package/dist/vendor/runar/runar-sdk/src/providers/wallet-provider.d.ts +94 -0
- package/dist/vendor/runar/runar-sdk/src/providers/wallet-provider.js +297 -0
- package/dist/vendor/runar/runar-sdk/src/script-utils.d.ts +54 -0
- package/dist/vendor/runar/runar-sdk/src/script-utils.js +252 -0
- package/dist/vendor/runar/runar-sdk/src/signers/signer.d.ts +17 -0
- package/dist/vendor/runar/runar-sdk/src/signers/signer.js +4 -0
- package/dist/vendor/runar/runar-sdk/src/spend-safety.d.ts +31 -0
- package/dist/vendor/runar/runar-sdk/src/spend-safety.js +35 -0
- package/dist/vendor/runar/runar-sdk/src/state.d.ts +54 -0
- package/dist/vendor/runar/runar-sdk/src/state.js +584 -0
- package/dist/vendor/runar/runar-sdk/src/types.d.ts +227 -0
- package/dist/vendor/runar/runar-sdk/src/types.js +4 -0
- package/dist/vendor/runar/runar-sdk/src/unsound-primitives.d.ts +30 -0
- package/dist/vendor/runar/runar-sdk/src/unsound-primitives.js +42 -0
- package/dist/vendor/runar/runar-sdk.d.ts +9 -0
- package/dist/vendor/runar/runar-sdk.js +8 -0
- package/dist/wallet/action.d.ts +28 -0
- package/dist/wallet/action.js +42 -0
- package/dist/wallet/brc29.d.ts +83 -0
- package/dist/wallet/brc29.js +80 -0
- package/dist/wallet/color.d.ts +110 -0
- package/dist/wallet/color.js +134 -0
- package/dist/wallet/swapPayouts.d.ts +35 -0
- package/dist/wallet/swapPayouts.js +146 -0
- package/package.json +39 -4
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// runar-sdk/providers/wallet-provider.ts — BRC-100 wallet provider
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
//
|
|
5
|
+
// Provider implementation that uses a BRC-100 wallet for UTXO management,
|
|
6
|
+
// GorillaPool ARC for broadcast (EF format), and an optional overlay
|
|
7
|
+
// service for tx indexing.
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
import { txToTransactionData, warnNonFatal } from './provider.js';
|
|
10
|
+
import { buildP2PKHScript } from '../script-utils.js';
|
|
11
|
+
import { Transaction, } from '@bsv/sdk';
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Options
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
/**
|
|
16
|
+
* The default ARC broadcaster. This is a MAINNET endpoint — see R-179 for why
|
|
17
|
+
* that matters at construction time.
|
|
18
|
+
*/
|
|
19
|
+
const MAINNET_ARC_URL = 'https://arc.gorillapool.io';
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// WalletProvider
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
export class WalletProvider {
|
|
24
|
+
wallet;
|
|
25
|
+
signer;
|
|
26
|
+
basket;
|
|
27
|
+
fundingTag;
|
|
28
|
+
arcUrl;
|
|
29
|
+
overlayUrl;
|
|
30
|
+
overlayTopics;
|
|
31
|
+
_network;
|
|
32
|
+
_feeRate;
|
|
33
|
+
broadcaster;
|
|
34
|
+
txCache = new Map();
|
|
35
|
+
constructor(options) {
|
|
36
|
+
this.wallet = options.wallet;
|
|
37
|
+
this.signer = options.signer;
|
|
38
|
+
this.basket = options.basket;
|
|
39
|
+
this.fundingTag = options.fundingTag ?? 'funding';
|
|
40
|
+
this._network = options.network ?? 'mainnet';
|
|
41
|
+
// R-179: arcUrl and network used to be defaulted independently, so a
|
|
42
|
+
// provider configured for testnet reported getNetwork() === 'testnet' and
|
|
43
|
+
// broadcast every transaction to the MAINNET ARC, with nothing to say so.
|
|
44
|
+
// There is no canonical testnet ARC endpoint in this repo to default to,
|
|
45
|
+
// so a testnet provider has to name its own rather than inherit one that
|
|
46
|
+
// points at real money. The sibling GorillaPoolProvider derives its base
|
|
47
|
+
// URL from the network for the same reason.
|
|
48
|
+
if (options.arcUrl) {
|
|
49
|
+
this.arcUrl = options.arcUrl;
|
|
50
|
+
}
|
|
51
|
+
else if (this._network === 'mainnet') {
|
|
52
|
+
this.arcUrl = MAINNET_ARC_URL;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
throw new Error(`WalletProvider: no default ARC endpoint for network '${this._network}' — ` +
|
|
56
|
+
`${MAINNET_ARC_URL} is a MAINNET broadcaster. Pass arcUrl explicitly.`);
|
|
57
|
+
}
|
|
58
|
+
this.overlayUrl = options.overlayUrl;
|
|
59
|
+
this.overlayTopics = options.overlayTopics;
|
|
60
|
+
this._feeRate = options.feeRate ?? 100;
|
|
61
|
+
this.broadcaster = options.broadcaster;
|
|
62
|
+
}
|
|
63
|
+
// -------------------------------------------------------------------------
|
|
64
|
+
// Typed accessors (used by RunarContract.deployWithWallet; also lets
|
|
65
|
+
// consumers reach the wallet without reaching into internals)
|
|
66
|
+
// -------------------------------------------------------------------------
|
|
67
|
+
/** The BRC-100 wallet client this provider wraps. */
|
|
68
|
+
get walletClient() {
|
|
69
|
+
return this.wallet;
|
|
70
|
+
}
|
|
71
|
+
/** The wallet basket used for UTXO management. */
|
|
72
|
+
get basketName() {
|
|
73
|
+
return this.basket;
|
|
74
|
+
}
|
|
75
|
+
// -------------------------------------------------------------------------
|
|
76
|
+
// Transaction cache
|
|
77
|
+
// -------------------------------------------------------------------------
|
|
78
|
+
/** Cache a raw tx hex by its txid (for EF parent lookups). */
|
|
79
|
+
cacheTx(txid, rawHex) {
|
|
80
|
+
this.txCache.set(txid, rawHex);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Fetch raw tx hex: local cache → overlay → throw.
|
|
84
|
+
*
|
|
85
|
+
* Protected so subclasses can supply parents from another source (their
|
|
86
|
+
* own index, a node RPC, …); the EF assembly in `broadcastTx` dispatches
|
|
87
|
+
* through the override.
|
|
88
|
+
*/
|
|
89
|
+
async fetchRawTx(txid) {
|
|
90
|
+
const cached = this.txCache.get(txid);
|
|
91
|
+
if (cached)
|
|
92
|
+
return cached;
|
|
93
|
+
if (this.overlayUrl) {
|
|
94
|
+
const resp = await fetch(`${this.overlayUrl}/api/tx/${txid}/hex`);
|
|
95
|
+
if (resp.ok) {
|
|
96
|
+
const hex = (await resp.text()).trim();
|
|
97
|
+
this.txCache.set(txid, hex);
|
|
98
|
+
return hex;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
throw new Error(`WalletProvider: could not fetch parent tx ${txid} (not in cache${this.overlayUrl ? ', overlay returned error' : ''})`);
|
|
102
|
+
}
|
|
103
|
+
// -------------------------------------------------------------------------
|
|
104
|
+
// Broadcast
|
|
105
|
+
// -------------------------------------------------------------------------
|
|
106
|
+
/**
|
|
107
|
+
* Broadcast a transaction via ARC in EF format.
|
|
108
|
+
*
|
|
109
|
+
* Protected so subclasses can reroute broadcast (e.g. through an overlay
|
|
110
|
+
* that gates admission) while `broadcast()` and `ensureFunding()` keep
|
|
111
|
+
* dispatching through the override.
|
|
112
|
+
*/
|
|
113
|
+
async broadcastTx(tx) {
|
|
114
|
+
// Attach source transactions for EF format
|
|
115
|
+
for (const input of tx.inputs) {
|
|
116
|
+
if (input.sourceTransaction)
|
|
117
|
+
continue;
|
|
118
|
+
const parentTxid = input.sourceTXID;
|
|
119
|
+
if (!parentTxid)
|
|
120
|
+
continue;
|
|
121
|
+
const parentHex = await this.fetchRawTx(parentTxid);
|
|
122
|
+
input.sourceTransaction = Transaction.fromHex(parentHex);
|
|
123
|
+
}
|
|
124
|
+
// Issue #107: when a broadcaster is injected, delegate to it so the SDK and
|
|
125
|
+
// the calling layer share ONE broadcaster instance/config (keeps parent +
|
|
126
|
+
// child at the same ARC deployment → avoids SEEN_IN_ORPHAN_MEMPOOL). The
|
|
127
|
+
// structured BroadcastFailure is flattened into a thrown Error to preserve
|
|
128
|
+
// the existing `broadcast()` contract.
|
|
129
|
+
if (this.broadcaster) {
|
|
130
|
+
const result = await this.broadcaster.broadcast(tx);
|
|
131
|
+
if (result.status === 'error') {
|
|
132
|
+
throw new Error(`WalletProvider: injected broadcaster failed (${result.code}): ${result.description}`);
|
|
133
|
+
}
|
|
134
|
+
const txid = result.txid;
|
|
135
|
+
this.txCache.set(txid, tx.toHex());
|
|
136
|
+
if (this.overlayUrl && this.overlayTopics && this.overlayTopics.length > 0) {
|
|
137
|
+
this.submitToOverlay(tx).catch((e) => warnNonFatal('overlay submission', e));
|
|
138
|
+
}
|
|
139
|
+
return txid;
|
|
140
|
+
}
|
|
141
|
+
const efBytes = tx.toEFUint8Array();
|
|
142
|
+
const resp = await fetch(`${this.arcUrl}/v1/tx`, {
|
|
143
|
+
method: 'POST',
|
|
144
|
+
headers: { 'Content-Type': 'application/octet-stream' },
|
|
145
|
+
body: efBytes.buffer,
|
|
146
|
+
});
|
|
147
|
+
if (!resp.ok) {
|
|
148
|
+
const body = await resp.text();
|
|
149
|
+
throw new Error(`WalletProvider: ARC broadcast failed (${resp.status}): ${body}`);
|
|
150
|
+
}
|
|
151
|
+
const result = (await resp.json());
|
|
152
|
+
const txid = result.txid;
|
|
153
|
+
// Cache for future EF lookups
|
|
154
|
+
this.txCache.set(txid, tx.toHex());
|
|
155
|
+
// Fire-and-forget: submit to overlay for indexing
|
|
156
|
+
if (this.overlayUrl && this.overlayTopics && this.overlayTopics.length > 0) {
|
|
157
|
+
this.submitToOverlay(tx).catch((e) => warnNonFatal('overlay submission', e));
|
|
158
|
+
}
|
|
159
|
+
return txid;
|
|
160
|
+
}
|
|
161
|
+
/** Submit a transaction to the overlay for indexing (non-fatal).
|
|
162
|
+
* Protected so subclasses can adapt the submit request to their overlay. */
|
|
163
|
+
async submitToOverlay(tx) {
|
|
164
|
+
if (!this.overlayUrl || !this.overlayTopics)
|
|
165
|
+
return;
|
|
166
|
+
const beef = tx.toBEEF();
|
|
167
|
+
await fetch(`${this.overlayUrl}/submit`, {
|
|
168
|
+
method: 'POST',
|
|
169
|
+
headers: {
|
|
170
|
+
'Content-Type': 'application/json',
|
|
171
|
+
'X-Topics': JSON.stringify(this.overlayTopics),
|
|
172
|
+
},
|
|
173
|
+
body: JSON.stringify({
|
|
174
|
+
beef: Array.from(beef),
|
|
175
|
+
topics: this.overlayTopics,
|
|
176
|
+
}),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
// -------------------------------------------------------------------------
|
|
180
|
+
// Provider interface
|
|
181
|
+
// -------------------------------------------------------------------------
|
|
182
|
+
/**
|
|
183
|
+
* Get UTXOs from the wallet's basket.
|
|
184
|
+
* Returns only spendable P2PKH UTXOs locked to the signer's derived key.
|
|
185
|
+
*/
|
|
186
|
+
async getUtxos(_address) {
|
|
187
|
+
const result = await this.wallet.listOutputs({
|
|
188
|
+
basket: this.basket,
|
|
189
|
+
tags: [this.fundingTag],
|
|
190
|
+
tagQueryMode: 'all',
|
|
191
|
+
include: 'locking scripts',
|
|
192
|
+
limit: 100,
|
|
193
|
+
seekPermission: false,
|
|
194
|
+
});
|
|
195
|
+
const derivedPubKey = await this.signer.getPublicKey();
|
|
196
|
+
const expectedScript = buildP2PKHScript(derivedPubKey);
|
|
197
|
+
const utxos = [];
|
|
198
|
+
for (const out of result.outputs) {
|
|
199
|
+
if (!out.spendable || !out.lockingScript)
|
|
200
|
+
continue;
|
|
201
|
+
if (out.lockingScript !== expectedScript)
|
|
202
|
+
continue;
|
|
203
|
+
const [txid, voutStr] = out.outpoint.split('.');
|
|
204
|
+
utxos.push({
|
|
205
|
+
txid: txid,
|
|
206
|
+
outputIndex: Number(voutStr),
|
|
207
|
+
satoshis: out.satoshis,
|
|
208
|
+
script: out.lockingScript,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
return utxos;
|
|
212
|
+
}
|
|
213
|
+
async getTransaction(txid) {
|
|
214
|
+
// R-151: this used to fall back to
|
|
215
|
+
// { txid, version: 1, inputs: [], outputs: [], locktime: 0 }
|
|
216
|
+
// on a cache miss OR a parse failure, so a caller could not tell "this
|
|
217
|
+
// transaction has no outputs" from "I could not find this transaction".
|
|
218
|
+
// The same shape was already fixed once in this SDK — txToTransactionData
|
|
219
|
+
// in providers/provider.ts is that remediation — and never applied here.
|
|
220
|
+
const cached = this.txCache.get(txid);
|
|
221
|
+
if (cached === undefined) {
|
|
222
|
+
throw new Error(`WalletProvider.getTransaction: transaction ${txid} is not in the provider's ` +
|
|
223
|
+
`cache. A wallet provider only knows transactions it has broadcast or been ` +
|
|
224
|
+
`given via cacheTx().`);
|
|
225
|
+
}
|
|
226
|
+
let tx;
|
|
227
|
+
try {
|
|
228
|
+
tx = Transaction.fromHex(cached);
|
|
229
|
+
}
|
|
230
|
+
catch (e) {
|
|
231
|
+
throw new Error(`WalletProvider.getTransaction: cached hex for ${txid} did not parse as a ` +
|
|
232
|
+
`transaction: ${e instanceof Error ? e.message : String(e)}`);
|
|
233
|
+
}
|
|
234
|
+
return txToTransactionData(txid, tx);
|
|
235
|
+
}
|
|
236
|
+
async broadcast(tx) {
|
|
237
|
+
return this.broadcastTx(tx);
|
|
238
|
+
}
|
|
239
|
+
async getContractUtxo(_scriptHash) {
|
|
240
|
+
// Contract UTXOs typically come from overlay services or app logic,
|
|
241
|
+
// not from the wallet provider.
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
getNetwork() {
|
|
245
|
+
return this._network;
|
|
246
|
+
}
|
|
247
|
+
async getRawTransaction(txid) {
|
|
248
|
+
return this.fetchRawTx(txid);
|
|
249
|
+
}
|
|
250
|
+
async getFeeRate() {
|
|
251
|
+
return this._feeRate;
|
|
252
|
+
}
|
|
253
|
+
// -------------------------------------------------------------------------
|
|
254
|
+
// Funding
|
|
255
|
+
// -------------------------------------------------------------------------
|
|
256
|
+
/**
|
|
257
|
+
* Ensure there are enough P2PKH funding UTXOs in the wallet basket.
|
|
258
|
+
* Creates a new funding UTXO via the wallet if the balance is insufficient.
|
|
259
|
+
*
|
|
260
|
+
* @param minSatoshis - Minimum total satoshis required.
|
|
261
|
+
*/
|
|
262
|
+
async ensureFunding(minSatoshis) {
|
|
263
|
+
const address = await this.signer.getAddress();
|
|
264
|
+
const utxos = await this.getUtxos(address);
|
|
265
|
+
const totalAvailable = utxos.reduce((sum, u) => sum + u.satoshis, 0);
|
|
266
|
+
if (totalAvailable >= minSatoshis)
|
|
267
|
+
return;
|
|
268
|
+
const derivedPubKey = await this.signer.getPublicKey();
|
|
269
|
+
const lockingScript = buildP2PKHScript(derivedPubKey);
|
|
270
|
+
const fundAmount = minSatoshis - totalAvailable;
|
|
271
|
+
const result = await this.wallet.createAction({
|
|
272
|
+
description: 'Runar contract funding',
|
|
273
|
+
outputs: [{
|
|
274
|
+
lockingScript,
|
|
275
|
+
satoshis: fundAmount,
|
|
276
|
+
outputDescription: 'Funding UTXO',
|
|
277
|
+
basket: this.basket,
|
|
278
|
+
tags: [this.fundingTag],
|
|
279
|
+
}],
|
|
280
|
+
});
|
|
281
|
+
// Cache the funding tx so child txs can build EF
|
|
282
|
+
if (result.tx) {
|
|
283
|
+
try {
|
|
284
|
+
const tx = Transaction.fromAtomicBEEF(result.tx);
|
|
285
|
+
const rawHex = tx.toHex();
|
|
286
|
+
const txid = result.txid || '';
|
|
287
|
+
if (txid)
|
|
288
|
+
this.txCache.set(txid, rawHex);
|
|
289
|
+
// Broadcast to ARC (may already be known — non-fatal)
|
|
290
|
+
await this.broadcastTx(Transaction.fromHex(rawHex)).catch((e) => warnNonFatal('funding-tx broadcast', e));
|
|
291
|
+
}
|
|
292
|
+
catch (e) {
|
|
293
|
+
warnNonFatal('funding-tx parse', e);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { RunarArtifact } from '../../../../pool/runar-ir-schema.js';
|
|
2
|
+
/**
|
|
3
|
+
* Hash a 66- or 130-char hex secp256k1 public key into its 40-char hex
|
|
4
|
+
* hash160 (RIPEMD160(SHA256(pubkey))). Used as the `changePKH` argument
|
|
5
|
+
* to contract methods and as the input to `buildP2PKHScript`.
|
|
6
|
+
*/
|
|
7
|
+
export declare function pubkeyToPKH(pubkeyHex: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Build a standard P2PKH locking script hex from an address, pubkey hash,
|
|
10
|
+
* or public key.
|
|
11
|
+
*
|
|
12
|
+
* OP_DUP OP_HASH160 OP_PUSH20 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG
|
|
13
|
+
* 76 a9 14 <20 bytes> 88 ac
|
|
14
|
+
*
|
|
15
|
+
* Accepted input formats:
|
|
16
|
+
* - 40-char hex: treated as raw 20-byte pubkey hash (hash160)
|
|
17
|
+
* - 66-char hex: compressed public key (auto-hashed via hash160)
|
|
18
|
+
* - 130-char hex: uncompressed public key (auto-hashed via hash160)
|
|
19
|
+
* - Other: decoded as Base58Check P2PKH address (mainnet version 0x00, or
|
|
20
|
+
* testnet/regtest version 0x6f), payload required to be exactly 20 bytes
|
|
21
|
+
*
|
|
22
|
+
* Round-three audit M-2: the Base58Check branch used to concatenate whatever
|
|
23
|
+
* payload came back, with no length and no version check — @bsv/sdk verifies
|
|
24
|
+
* the checksum, and nothing else. Three measured consequences:
|
|
25
|
+
*
|
|
26
|
+
* - a WIF private key (version 0x80, 33-byte payload) produced a 38-byte
|
|
27
|
+
* script whose trailing push runs off the end: unparseable, and any change
|
|
28
|
+
* sent there is permanently unspendable;
|
|
29
|
+
* - an xpub (version 0x0488B21E, 78-byte payload) produced an 82-byte script
|
|
30
|
+
* with no error raised at all;
|
|
31
|
+
* - a P2SH address (version 0x05 / 0xc4) produced a syntactically PERFECT
|
|
32
|
+
* 25-byte script that is a burn — the payload is a script hash, and only
|
|
33
|
+
* the version byte says so.
|
|
34
|
+
*
|
|
35
|
+
* All three are reachable from `DeployOptions.changeAddress` /
|
|
36
|
+
* `CallOptions.changeAddress`, which are public caller-supplied fields. Six
|
|
37
|
+
* peer tiers already gate this; Java's `Base58Check.decodeP2PKH` checks both
|
|
38
|
+
* length and version, and is the shape followed here.
|
|
39
|
+
*/
|
|
40
|
+
export declare function buildP2PKHScript(addressOrPubKey: string): string;
|
|
41
|
+
export declare function decodeScriptNumber(dataHex: string): bigint;
|
|
42
|
+
/**
|
|
43
|
+
* Extract constructor argument values from a compiled on-chain script.
|
|
44
|
+
*
|
|
45
|
+
* Uses `artifact.constructorSlots` to locate each constructor arg at its
|
|
46
|
+
* byte offset, reads the push data, and deserializes according to the
|
|
47
|
+
* ABI param type.
|
|
48
|
+
*/
|
|
49
|
+
export declare function extractConstructorArgs(artifact: RunarArtifact, scriptHex: string): Record<string, unknown>;
|
|
50
|
+
/**
|
|
51
|
+
* Determine whether a given on-chain script was produced from the given
|
|
52
|
+
* contract artifact (regardless of what constructor args were used).
|
|
53
|
+
*/
|
|
54
|
+
export declare function matchesArtifact(artifact: RunarArtifact, scriptHex: string): boolean;
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// runar-sdk/script-utils.ts — Script utilities
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
import { Hash, Utils } from '@bsv/sdk';
|
|
5
|
+
import { abiValueEncoding } from '../../../../pool/runar-ir-schema.js';
|
|
6
|
+
import { findLastOpReturn } from './state.js';
|
|
7
|
+
/**
|
|
8
|
+
* Hash a 66- or 130-char hex secp256k1 public key into its 40-char hex
|
|
9
|
+
* hash160 (RIPEMD160(SHA256(pubkey))). Used as the `changePKH` argument
|
|
10
|
+
* to contract methods and as the input to `buildP2PKHScript`.
|
|
11
|
+
*/
|
|
12
|
+
export function pubkeyToPKH(pubkeyHex) {
|
|
13
|
+
return Utils.toHex(Hash.hash160(Utils.toArray(pubkeyHex, 'hex')));
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Build a standard P2PKH locking script hex from an address, pubkey hash,
|
|
17
|
+
* or public key.
|
|
18
|
+
*
|
|
19
|
+
* OP_DUP OP_HASH160 OP_PUSH20 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG
|
|
20
|
+
* 76 a9 14 <20 bytes> 88 ac
|
|
21
|
+
*
|
|
22
|
+
* Accepted input formats:
|
|
23
|
+
* - 40-char hex: treated as raw 20-byte pubkey hash (hash160)
|
|
24
|
+
* - 66-char hex: compressed public key (auto-hashed via hash160)
|
|
25
|
+
* - 130-char hex: uncompressed public key (auto-hashed via hash160)
|
|
26
|
+
* - Other: decoded as Base58Check P2PKH address (mainnet version 0x00, or
|
|
27
|
+
* testnet/regtest version 0x6f), payload required to be exactly 20 bytes
|
|
28
|
+
*
|
|
29
|
+
* Round-three audit M-2: the Base58Check branch used to concatenate whatever
|
|
30
|
+
* payload came back, with no length and no version check — @bsv/sdk verifies
|
|
31
|
+
* the checksum, and nothing else. Three measured consequences:
|
|
32
|
+
*
|
|
33
|
+
* - a WIF private key (version 0x80, 33-byte payload) produced a 38-byte
|
|
34
|
+
* script whose trailing push runs off the end: unparseable, and any change
|
|
35
|
+
* sent there is permanently unspendable;
|
|
36
|
+
* - an xpub (version 0x0488B21E, 78-byte payload) produced an 82-byte script
|
|
37
|
+
* with no error raised at all;
|
|
38
|
+
* - a P2SH address (version 0x05 / 0xc4) produced a syntactically PERFECT
|
|
39
|
+
* 25-byte script that is a burn — the payload is a script hash, and only
|
|
40
|
+
* the version byte says so.
|
|
41
|
+
*
|
|
42
|
+
* All three are reachable from `DeployOptions.changeAddress` /
|
|
43
|
+
* `CallOptions.changeAddress`, which are public caller-supplied fields. Six
|
|
44
|
+
* peer tiers already gate this; Java's `Base58Check.decodeP2PKH` checks both
|
|
45
|
+
* length and version, and is the shape followed here.
|
|
46
|
+
*/
|
|
47
|
+
export function buildP2PKHScript(addressOrPubKey) {
|
|
48
|
+
let pubKeyHash;
|
|
49
|
+
if (/^[0-9a-fA-F]{40}$/.test(addressOrPubKey)) {
|
|
50
|
+
// Already a raw 20-byte pubkey hash in hex
|
|
51
|
+
pubKeyHash = addressOrPubKey;
|
|
52
|
+
}
|
|
53
|
+
else if (/^[0-9a-fA-F]{66}$/.test(addressOrPubKey) || /^[0-9a-fA-F]{130}$/.test(addressOrPubKey)) {
|
|
54
|
+
// Compressed (33 bytes) or uncompressed (65 bytes) public key — hash it
|
|
55
|
+
pubKeyHash = pubkeyToPKH(addressOrPubKey);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
// Decode Base58Check address to extract the 20-byte pubkey hash. The
|
|
59
|
+
// checksum is verified by `fromBase58Check` itself (it throws
|
|
60
|
+
// `Invalid checksum`); length and version are checked here (M-2).
|
|
61
|
+
const decoded = Utils.fromBase58Check(addressOrPubKey);
|
|
62
|
+
const dataHex = typeof decoded.data === 'string'
|
|
63
|
+
? decoded.data
|
|
64
|
+
: Utils.toHex(decoded.data);
|
|
65
|
+
const prefixHex = typeof decoded.prefix === 'string'
|
|
66
|
+
? decoded.prefix
|
|
67
|
+
: Utils.toHex(decoded.prefix);
|
|
68
|
+
if (dataHex.length !== 40) {
|
|
69
|
+
throw new Error(`buildP2PKHScript: '${addressOrPubKey}' is not a P2PKH address — its Base58Check ` +
|
|
70
|
+
`payload is ${dataHex.length / 2} bytes, expected 20. A WIF private key, an ` +
|
|
71
|
+
'xpub/xprv, or any other Base58Check string is not an address; pass a P2PKH ' +
|
|
72
|
+
'address, a 40-char hash160, or a 66-/130-char public key.');
|
|
73
|
+
}
|
|
74
|
+
if (prefixHex !== '00' && prefixHex !== '6f') {
|
|
75
|
+
throw new Error(`buildP2PKHScript: '${addressOrPubKey}' is not a P2PKH address — Base58Check ` +
|
|
76
|
+
`version byte 0x${prefixHex}, expected 0x00 (mainnet) or 0x6f (testnet/regtest). ` +
|
|
77
|
+
'A P2SH address (0x05 / 0xc4) carries a SCRIPT hash: wrapping it in a P2PKH ' +
|
|
78
|
+
'script produces a well-formed script that burns the output.');
|
|
79
|
+
}
|
|
80
|
+
pubKeyHash = dataHex;
|
|
81
|
+
}
|
|
82
|
+
return '76a914' + pubKeyHash + '88ac';
|
|
83
|
+
}
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Constructor arg extraction
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
function readScriptElement(hex, offset) {
|
|
88
|
+
const opcode = parseInt(hex.slice(offset, offset + 2), 16);
|
|
89
|
+
if (opcode === 0x00)
|
|
90
|
+
return { dataHex: '', totalHexChars: 2, opcode };
|
|
91
|
+
if (opcode >= 0x01 && opcode <= 0x4b) {
|
|
92
|
+
const dataLen = opcode * 2;
|
|
93
|
+
return { dataHex: hex.slice(offset + 2, offset + 2 + dataLen), totalHexChars: 2 + dataLen, opcode };
|
|
94
|
+
}
|
|
95
|
+
if (opcode === 0x4c) {
|
|
96
|
+
const len = parseInt(hex.slice(offset + 2, offset + 4), 16);
|
|
97
|
+
const dataLen = len * 2;
|
|
98
|
+
return { dataHex: hex.slice(offset + 4, offset + 4 + dataLen), totalHexChars: 4 + dataLen, opcode };
|
|
99
|
+
}
|
|
100
|
+
if (opcode === 0x4d) {
|
|
101
|
+
const lo = parseInt(hex.slice(offset + 2, offset + 4), 16);
|
|
102
|
+
const hi = parseInt(hex.slice(offset + 4, offset + 6), 16);
|
|
103
|
+
const len = lo | (hi << 8);
|
|
104
|
+
const dataLen = len * 2;
|
|
105
|
+
return { dataHex: hex.slice(offset + 6, offset + 6 + dataLen), totalHexChars: 6 + dataLen, opcode };
|
|
106
|
+
}
|
|
107
|
+
if (opcode === 0x4e) {
|
|
108
|
+
const b0 = parseInt(hex.slice(offset + 2, offset + 4), 16);
|
|
109
|
+
const b1 = parseInt(hex.slice(offset + 4, offset + 6), 16);
|
|
110
|
+
const b2 = parseInt(hex.slice(offset + 6, offset + 8), 16);
|
|
111
|
+
const b3 = parseInt(hex.slice(offset + 8, offset + 10), 16);
|
|
112
|
+
const len = b0 | (b1 << 8) | (b2 << 16) | (b3 << 24);
|
|
113
|
+
const dataLen = len * 2;
|
|
114
|
+
return { dataHex: hex.slice(offset + 10, offset + 10 + dataLen), totalHexChars: 10 + dataLen, opcode };
|
|
115
|
+
}
|
|
116
|
+
return { dataHex: '', totalHexChars: 2, opcode };
|
|
117
|
+
}
|
|
118
|
+
export function decodeScriptNumber(dataHex) {
|
|
119
|
+
if (dataHex.length === 0)
|
|
120
|
+
return 0n;
|
|
121
|
+
const bytes = [];
|
|
122
|
+
for (let i = 0; i < dataHex.length; i += 2)
|
|
123
|
+
bytes.push(parseInt(dataHex.slice(i, i + 2), 16));
|
|
124
|
+
const negative = (bytes[bytes.length - 1] & 0x80) !== 0;
|
|
125
|
+
bytes[bytes.length - 1] &= 0x7f;
|
|
126
|
+
let result = 0n;
|
|
127
|
+
for (let i = bytes.length - 1; i >= 0; i--)
|
|
128
|
+
result = (result << 8n) | BigInt(bytes[i]);
|
|
129
|
+
if (result === 0n)
|
|
130
|
+
return 0n;
|
|
131
|
+
return negative ? -result : result;
|
|
132
|
+
}
|
|
133
|
+
function interpretScriptElement(opcode, dataHex, type) {
|
|
134
|
+
// Table-driven, NOT a hand-maintained `case` list: every spelling the
|
|
135
|
+
// compiler can emit must land in the right branch, and the two that did not
|
|
136
|
+
// (`RabinSig`/`RabinPubKey`, and the canonical `boolean`) each turned a
|
|
137
|
+
// value into a hex string here. `abiValueEncoding` is the same table the
|
|
138
|
+
// compiler stamps `ConstructorSlot.valueEncoding` from, so a new alias can
|
|
139
|
+
// only ever be added in one place.
|
|
140
|
+
switch (abiValueEncoding(type)) {
|
|
141
|
+
case 'scriptnum': {
|
|
142
|
+
if (opcode === 0x00)
|
|
143
|
+
return 0n;
|
|
144
|
+
if (opcode >= 0x51 && opcode <= 0x60)
|
|
145
|
+
return BigInt(opcode - 0x50);
|
|
146
|
+
if (opcode === 0x4f)
|
|
147
|
+
return -1n;
|
|
148
|
+
return decodeScriptNumber(dataHex);
|
|
149
|
+
}
|
|
150
|
+
case 'bool': {
|
|
151
|
+
if (opcode === 0x00)
|
|
152
|
+
return false;
|
|
153
|
+
if (opcode === 0x51)
|
|
154
|
+
return true;
|
|
155
|
+
return dataHex !== '00';
|
|
156
|
+
}
|
|
157
|
+
default: {
|
|
158
|
+
// S1: a ByteString (or other non-numeric) ctor arg whose 1-byte value
|
|
159
|
+
// was MINIMALDATA-encoded as OP_1..OP_16 / OP_1NEGATE carries no
|
|
160
|
+
// separate data bytes in the script — `readScriptElement` reports an
|
|
161
|
+
// empty `dataHex` for these opcodes (they aren't direct pushes or
|
|
162
|
+
// OP_PUSHDATA*). The opcode itself IS the value; reconstruct it
|
|
163
|
+
// instead of forwarding the (empty) dataHex. OP_0 correctly falls
|
|
164
|
+
// through to `dataHex` (the empty string), matching OP_0's true
|
|
165
|
+
// semantics (pushes `[]`, not a 1-byte `0x00`).
|
|
166
|
+
if (opcode >= 0x51 && opcode <= 0x60)
|
|
167
|
+
return (opcode - 0x50).toString(16).padStart(2, '0');
|
|
168
|
+
if (opcode === 0x4f)
|
|
169
|
+
return '81';
|
|
170
|
+
return dataHex;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Extract constructor argument values from a compiled on-chain script.
|
|
176
|
+
*
|
|
177
|
+
* Uses `artifact.constructorSlots` to locate each constructor arg at its
|
|
178
|
+
* byte offset, reads the push data, and deserializes according to the
|
|
179
|
+
* ABI param type.
|
|
180
|
+
*/
|
|
181
|
+
export function extractConstructorArgs(artifact, scriptHex) {
|
|
182
|
+
if (!artifact.constructorSlots || artifact.constructorSlots.length === 0)
|
|
183
|
+
return {};
|
|
184
|
+
let codeHex = scriptHex;
|
|
185
|
+
if (artifact.stateFields && artifact.stateFields.length > 0) {
|
|
186
|
+
const opReturnPos = findLastOpReturn(scriptHex);
|
|
187
|
+
if (opReturnPos !== -1)
|
|
188
|
+
codeHex = scriptHex.slice(0, opReturnPos);
|
|
189
|
+
}
|
|
190
|
+
// Walk EVERY slot in byte order. A constructor param referenced more than
|
|
191
|
+
// once in the contract body emits one slot per reference, and each
|
|
192
|
+
// occurrence's encoded width contributes to the cumulative offset shift —
|
|
193
|
+
// deduplicating before the walk drops those widths and mis-aligns every
|
|
194
|
+
// later slot on artifacts with repeated references. The VALUE is taken
|
|
195
|
+
// from the first occurrence per param.
|
|
196
|
+
const slots = [...artifact.constructorSlots].sort((a, b) => a.byteOffset - b.byteOffset);
|
|
197
|
+
const result = {};
|
|
198
|
+
const assigned = new Set();
|
|
199
|
+
let cumulativeShift = 0;
|
|
200
|
+
for (const slot of slots) {
|
|
201
|
+
const adjustedHexOffset = (slot.byteOffset + cumulativeShift) * 2;
|
|
202
|
+
const elem = readScriptElement(codeHex, adjustedHexOffset);
|
|
203
|
+
// Template placeholders are exactly 1 byte, so the shift contributed by
|
|
204
|
+
// each occurrence is its encoded width minus that byte.
|
|
205
|
+
cumulativeShift += elem.totalHexChars / 2 - 1;
|
|
206
|
+
if (assigned.has(slot.paramIndex))
|
|
207
|
+
continue;
|
|
208
|
+
assigned.add(slot.paramIndex);
|
|
209
|
+
const param = artifact.abi.constructor.params[slot.paramIndex];
|
|
210
|
+
if (!param)
|
|
211
|
+
continue;
|
|
212
|
+
result[param.name] = interpretScriptElement(elem.opcode, elem.dataHex, param.type);
|
|
213
|
+
}
|
|
214
|
+
return result;
|
|
215
|
+
}
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
// Script matching
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
/**
|
|
220
|
+
* Determine whether a given on-chain script was produced from the given
|
|
221
|
+
* contract artifact (regardless of what constructor args were used).
|
|
222
|
+
*/
|
|
223
|
+
export function matchesArtifact(artifact, scriptHex) {
|
|
224
|
+
let codeHex = scriptHex;
|
|
225
|
+
if (artifact.stateFields && artifact.stateFields.length > 0) {
|
|
226
|
+
const opReturnPos = findLastOpReturn(scriptHex);
|
|
227
|
+
if (opReturnPos !== -1)
|
|
228
|
+
codeHex = scriptHex.slice(0, opReturnPos);
|
|
229
|
+
}
|
|
230
|
+
const template = artifact.script;
|
|
231
|
+
if (!artifact.constructorSlots || artifact.constructorSlots.length === 0)
|
|
232
|
+
return codeHex === template;
|
|
233
|
+
const seenOffsets = new Set();
|
|
234
|
+
const slots = [...artifact.constructorSlots]
|
|
235
|
+
.sort((a, b) => a.byteOffset - b.byteOffset)
|
|
236
|
+
.filter((slot) => { if (seenOffsets.has(slot.byteOffset))
|
|
237
|
+
return false; seenOffsets.add(slot.byteOffset); return true; });
|
|
238
|
+
let templatePos = 0;
|
|
239
|
+
let codePos = 0;
|
|
240
|
+
for (const slot of slots) {
|
|
241
|
+
const slotHexOffset = slot.byteOffset * 2;
|
|
242
|
+
const templateSegment = template.slice(templatePos, slotHexOffset);
|
|
243
|
+
const codeSegment = codeHex.slice(codePos, codePos + templateSegment.length);
|
|
244
|
+
if (templateSegment !== codeSegment)
|
|
245
|
+
return false;
|
|
246
|
+
templatePos = slotHexOffset + 2;
|
|
247
|
+
const elemOffset = codePos + templateSegment.length;
|
|
248
|
+
const elem = readScriptElement(codeHex, elemOffset);
|
|
249
|
+
codePos = elemOffset + elem.totalHexChars;
|
|
250
|
+
}
|
|
251
|
+
return template.slice(templatePos) === codeHex.slice(codePos);
|
|
252
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface Signer {
|
|
2
|
+
/** Get the hex-encoded compressed public key (33 bytes = 66 hex chars). */
|
|
3
|
+
getPublicKey(): Promise<string>;
|
|
4
|
+
/** Get the BSV address (Base58Check-encoded). */
|
|
5
|
+
getAddress(): Promise<string>;
|
|
6
|
+
/**
|
|
7
|
+
* Sign a transaction input.
|
|
8
|
+
*
|
|
9
|
+
* @param txHex - The full raw transaction hex being signed.
|
|
10
|
+
* @param inputIndex - Index of the input being signed.
|
|
11
|
+
* @param subscript - The locking script of the UTXO being spent (hex).
|
|
12
|
+
* @param satoshis - The satoshi value of the UTXO being spent.
|
|
13
|
+
* @param sigHashType - Sighash flags (defaults to ALL | FORKID = 0x41).
|
|
14
|
+
* @returns The DER-encoded signature with sighash byte appended, hex-encoded.
|
|
15
|
+
*/
|
|
16
|
+
sign(txHex: string, inputIndex: number, subscript: string, satoshis: number, sigHashType?: number): Promise<string>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NEW-005: @bsv/sdk's `Spend` destroys the scripts it is given.
|
|
3
|
+
*
|
|
4
|
+
* `Spend` pushes a script chunk's `data` array onto its stack BY REFERENCE, and
|
|
5
|
+
* at least one opcode mutates its operand in place — `OP_NUM2BIN` does
|
|
6
|
+
* `rawnum[rawnum.length - 1] &= 0x7f` (see `Spend.js`) to strip the sign bit
|
|
7
|
+
* before zero-padding. A stateful continuation serialises every mutable state
|
|
8
|
+
* field with OP_NUM2BIN, so evaluating a call that writes a NEGATIVE
|
|
9
|
+
* unlocking-script push into state permanently clears the sign bit of that push
|
|
10
|
+
* inside the caller's own `Script` object: a `-34` push becomes `+34`.
|
|
11
|
+
*
|
|
12
|
+
* The corruption is invisible to serialisation. `Script.fromBinary` seeds
|
|
13
|
+
* `rawBytesCache`/`hexCache` from the original bytes, and mutating
|
|
14
|
+
* `chunks[i].data` does not invalidate them, so `toHex()` keeps returning the
|
|
15
|
+
* true bytes and nothing wrong is ever broadcast. The damage is confined to the
|
|
16
|
+
* NEXT in-memory evaluation of the same object, which sees the wrong value and
|
|
17
|
+
* rejects a transaction the network accepts — a FALSE REJECTION.
|
|
18
|
+
*
|
|
19
|
+
* Every `new Spend(...)` must therefore be handed scripts it is free to
|
|
20
|
+
* destroy. `fromHex` re-parses into fresh chunks with fresh `data` arrays.
|
|
21
|
+
*
|
|
22
|
+
* Only the two scripts `Spend` EXECUTES need detaching. The `outputs` array is
|
|
23
|
+
* read solely by `TransactionSignature.formatBytes`, which serialises each
|
|
24
|
+
* output's locking script and never pushes it, so passing `tx.outputs` by
|
|
25
|
+
* reference is harmless (and cheaper than deep-copying every output script).
|
|
26
|
+
*/
|
|
27
|
+
import { LockingScript, UnlockingScript, type Script } from '@bsv/sdk';
|
|
28
|
+
/** A private copy of `script` that `Spend` may corrupt without consequence. */
|
|
29
|
+
export declare function detachLockingScript(script: Script): LockingScript;
|
|
30
|
+
/** A private copy of `script` that `Spend` may corrupt without consequence. */
|
|
31
|
+
export declare function detachUnlockingScript(script: Script): UnlockingScript;
|