@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.
Files changed (121) hide show
  1. package/README.md +107 -2
  2. package/dist/client.d.ts +534 -0
  3. package/dist/client.js +1319 -0
  4. package/dist/errors.d.ts +17 -0
  5. package/dist/errors.js +75 -0
  6. package/dist/index.d.ts +51 -0
  7. package/dist/index.js +53 -0
  8. package/dist/lib/config.d.ts +42 -0
  9. package/dist/lib/config.js +64 -0
  10. package/dist/lib/keys.d.ts +42 -0
  11. package/dist/lib/keys.js +48 -0
  12. package/dist/lib/overlay.d.ts +190 -0
  13. package/dist/lib/overlay.js +302 -0
  14. package/dist/lib/skein.d.ts +82 -0
  15. package/dist/lib/skein.js +142 -0
  16. package/dist/lib/tokenId.d.ts +72 -0
  17. package/dist/lib/tokenId.js +121 -0
  18. package/dist/lp/amounts.d.ts +12 -0
  19. package/dist/lp/amounts.js +38 -0
  20. package/dist/lp/close.d.ts +90 -0
  21. package/dist/lp/close.js +140 -0
  22. package/dist/lp/images.d.ts +11 -0
  23. package/dist/lp/images.js +45 -0
  24. package/dist/lp/inventory.d.ts +68 -0
  25. package/dist/lp/inventory.js +84 -0
  26. package/dist/lp/poolDeploy.d.ts +220 -0
  27. package/dist/lp/poolDeploy.js +291 -0
  28. package/dist/lp/poolRows.d.ts +13 -0
  29. package/dist/lp/poolRows.js +19 -0
  30. package/dist/lp/positions.d.ts +81 -0
  31. package/dist/lp/positions.js +144 -0
  32. package/dist/lp/rebalance.d.ts +101 -0
  33. package/dist/lp/rebalance.js +168 -0
  34. package/dist/lp/wallet.d.ts +12 -0
  35. package/dist/lp/wallet.js +19 -0
  36. package/dist/market/activeSwap.d.ts +158 -0
  37. package/dist/market/activeSwap.js +320 -0
  38. package/dist/market/plan.d.ts +85 -0
  39. package/dist/market/plan.js +149 -0
  40. package/dist/market/prices.d.ts +13 -0
  41. package/dist/market/prices.js +14 -0
  42. package/dist/market/relay.d.ts +84 -0
  43. package/dist/market/relay.js +201 -0
  44. package/dist/market/swapAction.d.ts +306 -0
  45. package/dist/market/swapAction.js +508 -0
  46. package/dist/market/swapFlow.d.ts +58 -0
  47. package/dist/market/swapFlow.js +86 -0
  48. package/dist/market/view.d.ts +58 -0
  49. package/dist/market/view.js +63 -0
  50. package/dist/pool/artifact.d.ts +23 -0
  51. package/dist/pool/artifact.js +38 -0
  52. package/dist/pool/index.d.ts +8 -0
  53. package/dist/pool/index.js +8 -0
  54. package/dist/pool/oneColor.d.ts +19 -0
  55. package/dist/pool/oneColor.js +39 -0
  56. package/dist/pool/pool.artifact.json +5069 -0
  57. package/dist/pool/runar-ir-schema.d.ts +21 -0
  58. package/dist/pool/runar-ir-schema.js +18 -0
  59. package/dist/pool/template.d.ts +295 -0
  60. package/dist/pool/template.js +715 -0
  61. package/dist/vendor/runar/LICENSE +21 -0
  62. package/dist/vendor/runar/NOTICE +15 -0
  63. package/dist/vendor/runar/runar-ir-schema/src/abi-type-encoding.d.ts +42 -0
  64. package/dist/vendor/runar/runar-ir-schema/src/abi-type-encoding.js +53 -0
  65. package/dist/vendor/runar/runar-ir-schema/src/anf-ir.d.ts +221 -0
  66. package/dist/vendor/runar/runar-ir-schema/src/anf-ir.js +33 -0
  67. package/dist/vendor/runar/runar-ir-schema/src/artifact.d.ts +288 -0
  68. package/dist/vendor/runar/runar-ir-schema/src/artifact.js +8 -0
  69. package/dist/vendor/runar/runar-ir-schema/src/canonical-json.d.ts +43 -0
  70. package/dist/vendor/runar/runar-ir-schema/src/canonical-json.js +204 -0
  71. package/dist/vendor/runar/runar-ir-schema/src/input-limits.d.ts +89 -0
  72. package/dist/vendor/runar/runar-ir-schema/src/input-limits.js +100 -0
  73. package/dist/vendor/runar/runar-ir-schema/src/stack-ir.d.ts +118 -0
  74. package/dist/vendor/runar/runar-ir-schema/src/stack-ir.js +8 -0
  75. package/dist/vendor/runar/runar-ir-schema/src/state-layout.d.ts +56 -0
  76. package/dist/vendor/runar/runar-ir-schema/src/state-layout.js +145 -0
  77. package/dist/vendor/runar/runar-ir-schema/src/unknown-anf-kind-error.d.ts +18 -0
  78. package/dist/vendor/runar/runar-ir-schema/src/unknown-anf-kind-error.js +25 -0
  79. package/dist/vendor/runar/runar-sdk/src/anf-interpreter.d.ts +160 -0
  80. package/dist/vendor/runar/runar-sdk/src/anf-interpreter.js +1149 -0
  81. package/dist/vendor/runar/runar-sdk/src/calling.d.ts +98 -0
  82. package/dist/vendor/runar/runar-sdk/src/calling.js +209 -0
  83. package/dist/vendor/runar/runar-sdk/src/contract.d.ts +443 -0
  84. package/dist/vendor/runar/runar-sdk/src/contract.js +2516 -0
  85. package/dist/vendor/runar/runar-sdk/src/deployment.d.ts +36 -0
  86. package/dist/vendor/runar/runar-sdk/src/deployment.js +119 -0
  87. package/dist/vendor/runar/runar-sdk/src/errors.d.ts +46 -0
  88. package/dist/vendor/runar/runar-sdk/src/errors.js +59 -0
  89. package/dist/vendor/runar/runar-sdk/src/oppushtx.d.ts +23 -0
  90. package/dist/vendor/runar/runar-sdk/src/oppushtx.js +95 -0
  91. package/dist/vendor/runar/runar-sdk/src/ordinals/envelope.d.ts +31 -0
  92. package/dist/vendor/runar/runar-sdk/src/ordinals/envelope.js +272 -0
  93. package/dist/vendor/runar/runar-sdk/src/ordinals/types.d.ts +21 -0
  94. package/dist/vendor/runar/runar-sdk/src/ordinals/types.js +4 -0
  95. package/dist/vendor/runar/runar-sdk/src/providers/provider.d.ts +65 -0
  96. package/dist/vendor/runar/runar-sdk/src/providers/provider.js +59 -0
  97. package/dist/vendor/runar/runar-sdk/src/providers/wallet-provider.d.ts +94 -0
  98. package/dist/vendor/runar/runar-sdk/src/providers/wallet-provider.js +297 -0
  99. package/dist/vendor/runar/runar-sdk/src/script-utils.d.ts +54 -0
  100. package/dist/vendor/runar/runar-sdk/src/script-utils.js +252 -0
  101. package/dist/vendor/runar/runar-sdk/src/signers/signer.d.ts +17 -0
  102. package/dist/vendor/runar/runar-sdk/src/signers/signer.js +4 -0
  103. package/dist/vendor/runar/runar-sdk/src/spend-safety.d.ts +31 -0
  104. package/dist/vendor/runar/runar-sdk/src/spend-safety.js +35 -0
  105. package/dist/vendor/runar/runar-sdk/src/state.d.ts +54 -0
  106. package/dist/vendor/runar/runar-sdk/src/state.js +584 -0
  107. package/dist/vendor/runar/runar-sdk/src/types.d.ts +227 -0
  108. package/dist/vendor/runar/runar-sdk/src/types.js +4 -0
  109. package/dist/vendor/runar/runar-sdk/src/unsound-primitives.d.ts +30 -0
  110. package/dist/vendor/runar/runar-sdk/src/unsound-primitives.js +42 -0
  111. package/dist/vendor/runar/runar-sdk.d.ts +9 -0
  112. package/dist/vendor/runar/runar-sdk.js +8 -0
  113. package/dist/wallet/action.d.ts +28 -0
  114. package/dist/wallet/action.js +42 -0
  115. package/dist/wallet/brc29.d.ts +83 -0
  116. package/dist/wallet/brc29.js +80 -0
  117. package/dist/wallet/color.d.ts +110 -0
  118. package/dist/wallet/color.js +134 -0
  119. package/dist/wallet/swapPayouts.d.ts +35 -0
  120. package/dist/wallet/swapPayouts.js +146 -0
  121. 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,4 @@
1
+ // ---------------------------------------------------------------------------
2
+ // runar-sdk/signers/signer.ts — Signer interface
3
+ // ---------------------------------------------------------------------------
4
+ export {};
@@ -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;