@smartledger/bsv 5.5.3 → 6.0.1

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 (60) hide show
  1. package/CHANGELOG.md +142 -0
  2. package/README.md +38 -38
  3. package/bin/cli.js +35 -5
  4. package/bsv-covenant.min.js +6 -6
  5. package/bsv-gdaf.min.js +2 -2
  6. package/bsv-ltp.min.js +2 -2
  7. package/bsv-shamir.min.js +1 -1
  8. package/bsv-smartcontract.min.js +6 -6
  9. package/bsv-statuslist.min.js +5 -5
  10. package/bsv-vcjwt.min.js +1 -1
  11. package/bsv.bundle.js +2 -2
  12. package/bsv.d.ts +289 -10
  13. package/bsv.min.js +2 -2
  14. package/docs/MODULE_REFERENCE_COMPLETE.md +27 -27
  15. package/docs/advanced/UTXO_MANAGER_GUIDE.md +1 -1
  16. package/docs/getting-started/INSTALLATION.md +23 -23
  17. package/docs/getting-started/QUICK_START.md +7 -7
  18. package/docs/migration/FROM_BSV_1_5_6.md +5 -5
  19. package/index.js +4 -0
  20. package/lib/crypto/ecdsa.js +13 -0
  21. package/lib/crypto/shamir.js +16 -4
  22. package/lib/gdaf/did-resolver.js +3 -1
  23. package/lib/gdaf/smartledger-anchor.js +154 -12
  24. package/lib/ltp/anchor.js +16 -3
  25. package/lib/ltp/obligation.js +56 -8
  26. package/lib/ltp/proof.js +4 -2
  27. package/lib/ltp/right.js +86 -11
  28. package/lib/ordinals/README.md +170 -0
  29. package/lib/ordinals/bsv20.js +185 -0
  30. package/lib/ordinals/index.js +49 -0
  31. package/lib/ordinals/inscription.js +159 -0
  32. package/lib/ordinals/ordlock.js +560 -0
  33. package/lib/smart_contract/builder.js +27 -3
  34. package/lib/smart_contract/covenant.js +37 -10
  35. package/lib/smart_contract/covenant_builder.js +7 -5
  36. package/lib/smart_contract/index.js +28 -15
  37. package/lib/smart_contract/preimage.js +7 -7
  38. package/lib/smart_contract/pushtx.js +47 -7
  39. package/lib/smart_contract/token.js +69 -1
  40. package/lib/spv/headerchain.js +77 -0
  41. package/lib/spv/index.js +16 -0
  42. package/lib/spv/merkleproof.js +108 -0
  43. package/lib/statuslist/index.js +65 -24
  44. package/lib/vcjwt/index.js +6 -0
  45. package/package.json +1 -1
  46. package/test/credentials-test.js +22 -19
  47. package/test/crypto/shamir.js +9 -1
  48. package/test/crypto/shamir_rngcap.js +29 -0
  49. package/test/gdaf/anchor_spv.js +163 -0
  50. package/test/ltp/verify_failclosed.js +134 -0
  51. package/test/ordinals/bsv20.js +126 -0
  52. package/test/ordinals/inscription.js +71 -0
  53. package/test/ordinals/ordlock.js +552 -0
  54. package/test/smart_contract/extract_field.js +61 -0
  55. package/test/smart_contract/nonenforcing_guard.js +44 -0
  56. package/test/smart_contract/ordinal_transfer.js +83 -0
  57. package/test/smart_contract/sighash_marketplace.js +110 -0
  58. package/test/spv/headerchain.js +86 -0
  59. package/test/spv/merkleproof.js +133 -0
  60. package/test/statuslist/failclosed.js +78 -0
@@ -0,0 +1,560 @@
1
+ 'use strict'
2
+ /**
3
+ * OrdLock — a trustless "list an ordinal for sale" covenant for 1Sat Ordinals.
4
+ *
5
+ * A seller locks the 1-sat ordinal UTXO behind a script with two spend paths:
6
+ *
7
+ * PURCHASE anyone may take the ordinal, but ONLY by paying the seller. The buyer
8
+ * builds the spend, the covenant authenticates its BIP-143 preimage with
9
+ * OP_PUSH_TX and requires that the transaction's outputs are exactly
10
+ * `buyerOutputs || payBlob || trailingOutputs` — where `payBlob` (the
11
+ * required payment output(s)) is hard-baked into the locking script. The
12
+ * buyer is free to choose where the ordinal goes and to add funding inputs
13
+ * / change (SIGHASH_ALL|ANYONECANPAY): the ONE thing they cannot do is take
14
+ * the ordinal without recreating the required payment(s) byte-for-byte.
15
+ *
16
+ * CANCEL the seller reclaims the listing at any time with an ordinary ECDSA
17
+ * signature over their own public key (a P2PKH gate inside the covenant).
18
+ *
19
+ * `payBlob` may pin MORE THAN ONE output — the concatenation of a seller payment plus
20
+ * any royalty / marketplace-fee outputs — so a purchase atomically pays every party or
21
+ * fails. This is the widely-deployed 1Sat Ordinals OrdinalLock pattern, generalized and
22
+ * built on the audited configurable-SIGHASH OP_PUSH_TX core (SmartContract.PushTx).
23
+ * Every script this module emits is exercised through the consensus interpreter.
24
+ *
25
+ * The listing is self-describing: `parseOrdLock(script)` recovers the seller, the pinned
26
+ * payment output(s), the total price, and any inline inscription — so an indexer, wallet,
27
+ * or marketplace UI can read a listing straight off-chain, and `buildPurchaseTx` can
28
+ * assemble a complete, signed purchase knowing only the listing UTXO and the buyer's coins.
29
+ *
30
+ * Layout (locking script):
31
+ * OP_IF
32
+ * <sellerPKH> ... // CANCEL: OP_DUP OP_HASH160 <pkh> OP_EQUALVERIFY OP_CHECKSIG
33
+ * OP_ELSE
34
+ * ...OP_PUSH_TX core... // PURCHASE: authenticate preimage, bind outputs to payBlob
35
+ * OP_ENDIF
36
+ * [OP_FALSE OP_IF "ord" … OP_ENDIF] // optional inert inscription envelope
37
+ *
38
+ * Requires post-Genesis limits: call SmartContract.enableGenesis() before verifying.
39
+ */
40
+ var bsv = require('../..')
41
+ var P = require('../smart_contract/pushtx')
42
+ var H = require('../smart_contract/covenant_helpers')
43
+ var inscription = require('./inscription')
44
+
45
+ var Script = bsv.Script
46
+ var Opcode = bsv.Opcode
47
+ var BufferReader = bsv.encoding.BufferReader
48
+
49
+ // OrdLock commits to the FULL output set (so payments can be pinned) but leaves the
50
+ // input set open for the buyer's funding — SIGHASH_ALL|ANYONECANPAY|FORKID (0xc1).
51
+ var ORDLOCK_SIGHASH = P.SIGHASH_ALL_ANYONECANPAY_FORKID
52
+
53
+ /** Push a possibly-empty buffer MINIMALDATA-cleanly (empty => OP_0, i.e. empty vector). */
54
+ function pushData (s, buf) { return (buf && buf.length) ? s.add(Buffer.from(buf)) : s.add(Opcode.OP_0) }
55
+
56
+ /** Serialize a Transaction.Output to its on-wire bytes (8-byte value || varint || script). */
57
+ function serializeOutput (out) { return out.toBufferWriter().toBuffer() }
58
+
59
+ /** Assert a value is a non-negative integer satoshi amount; return it. */
60
+ function assertSats (v, label) {
61
+ if (typeof v !== 'number' || !isFinite(v) || v < 0 || Math.floor(v) !== v) {
62
+ throw new Error(label + ' must be a non-negative integer number of satoshis (got ' + v + ')')
63
+ }
64
+ return v
65
+ }
66
+
67
+ /** Coerce a Script, Buffer, or hex string to a Script. */
68
+ function coerceScript (v) {
69
+ if (v instanceof Script) return v
70
+ if (Buffer.isBuffer(v)) return Script.fromBuffer(v)
71
+ return Script.fromHex(v)
72
+ }
73
+
74
+ /** Coerce an Address, PublicKey, PrivateKey, or address string to an Address. */
75
+ function toAddress (v) {
76
+ if (v instanceof bsv.Address) return v
77
+ if (v && typeof v.toAddress === 'function') return v.toAddress() // PublicKey / PrivateKey
78
+ return bsv.Address.fromString(String(v))
79
+ }
80
+
81
+ /** Resolve a 20-byte HASH160 owner commitment from an Address/PublicKey/key or 20-byte Buffer. */
82
+ function resolvePubKeyHash (owner) {
83
+ if (Buffer.isBuffer(owner)) {
84
+ if (owner.length !== 20) throw new Error('seller pubkeyhash buffer must be 20 bytes (HASH160)')
85
+ return owner
86
+ }
87
+ if (owner instanceof bsv.PublicKey) return bsv.crypto.Hash.sha256ripemd160(owner.toBuffer())
88
+ return toAddress(owner).hashBuffer
89
+ }
90
+
91
+ /**
92
+ * Build the seller's payment Output (P2PKH) from an address/pubkey/key and a price in sats.
93
+ * A pre-built Transaction.Output is NOT accepted here — use it directly (or pass it as a
94
+ * `payOutputs`/`payTo` spec) — so that `price` is never silently ignored.
95
+ */
96
+ function payOutputFor (payTo, price) {
97
+ assertSats(price, 'payment price')
98
+ return new bsv.Transaction.Output({
99
+ script: Script.buildPublicKeyHashOut(toAddress(payTo)), satoshis: price
100
+ })
101
+ }
102
+
103
+ /** Coerce one payment spec to a Transaction.Output. */
104
+ function outputFromSpec (spec) {
105
+ if (spec instanceof bsv.Transaction.Output) return spec
106
+ if (Buffer.isBuffer(spec)) return bsv.Transaction.Output.fromBufferReader(new BufferReader(spec))
107
+ var to = spec.payTo != null ? spec.payTo : spec.address
108
+ var amt = spec.satoshis != null ? spec.satoshis : spec.price
109
+ if (to == null || amt == null) throw new Error('payment spec needs {address|payTo, satoshis|price}')
110
+ return payOutputFor(to, amt)
111
+ }
112
+
113
+ /**
114
+ * Resolve the required payment output(s) for a listing, in the order they must be
115
+ * recreated. Accepts (in priority order): `payOutputs` (array of Output/spec/Buffer),
116
+ * a single `payOutput`, or `price`+`payTo` (defaulting the recipient to `seller`) with
117
+ * optional `royalties` appended.
118
+ */
119
+ function resolvePayOutputs (params) {
120
+ if (params.payOutputs) {
121
+ if (!params.payOutputs.length) throw new Error('payOutputs must be a non-empty array')
122
+ return params.payOutputs.map(outputFromSpec)
123
+ }
124
+ if (params.payOutput != null) return [outputFromSpec(params.payOutput)]
125
+ if (params.price != null) {
126
+ var payTo = params.payTo != null ? params.payTo : params.seller
127
+ // A pre-built Output as payTo is used verbatim (its own value); otherwise build P2PKH(price).
128
+ var first = (payTo instanceof bsv.Transaction.Output) ? payTo : payOutputFor(payTo, params.price)
129
+ var arr = [first]
130
+ if (params.royalties) params.royalties.forEach(function (r) { arr.push(outputFromSpec(r)) })
131
+ return arr
132
+ }
133
+ throw new Error('buildOrdLock requires a price, a payOutput, or payOutputs')
134
+ }
135
+
136
+ /** The pinned payment bytes = concatenation of every required output's serialization. */
137
+ function payBlobFrom (outputs) { return Buffer.concat(outputs.map(serializeOutput)) }
138
+
139
+ /**
140
+ * Build an OrdLock listing (locking) script.
141
+ *
142
+ * @param {object} params
143
+ * @param {Address|PublicKey|string|Buffer} params.seller who may CANCEL the listing
144
+ * (an address, public key, or 20-byte HASH160). Also the default payment recipient.
145
+ * @param {number} [params.price] asking price in satoshis (simple single-payment form).
146
+ * @param {Address|PublicKey|string|Transaction.Output} [params.payTo] payment recipient
147
+ * for `price` (default: `seller`).
148
+ * @param {Array} [params.royalties] extra payment specs appended after the `price` output
149
+ * (each: Transaction.Output, {address,satoshis}, or a serialized-output Buffer).
150
+ * @param {Array} [params.payOutputs] pin an explicit ordered list of payment outputs,
151
+ * overriding price/payTo/royalties (seller + royalty + marketplace-fee, etc.).
152
+ * @param {Transaction.Output|Buffer} [params.payOutput] pin a single explicit output.
153
+ * @param {object} [params.inscription] if given ({contentType, content}), an inscription
154
+ * envelope is appended so a fresh inscribe+list share one output.
155
+ * @returns {Script} the OrdLock locking script.
156
+ */
157
+ function buildOrdLock (params) {
158
+ params = params || {}
159
+ if (params.seller == null) throw new Error('buildOrdLock requires a seller')
160
+ var sellerPKH = resolvePubKeyHash(params.seller)
161
+ var payBlob = payBlobFrom(resolvePayOutputs(params))
162
+
163
+ var s = new Script()
164
+ s.add(Opcode.OP_IF)
165
+ // CANCEL: seller signs with their key; standard P2PKH gate.
166
+ s.add(Opcode.OP_DUP).add(Opcode.OP_HASH160)
167
+ s.add(Buffer.from(sellerPKH)).add(Opcode.OP_EQUALVERIFY).add(Opcode.OP_CHECKSIG)
168
+ s.add(Opcode.OP_ELSE)
169
+ // PURCHASE: stack (after OP_IF pops the false marker) = [.., trailing, buyerOuts, preimage]
170
+ s.add(Opcode.OP_DUP)
171
+ P.pushTxCore(s, { sighashType: ORDLOCK_SIGHASH }) // authenticate the preimage
172
+ s.add(Opcode.OP_VERIFY)
173
+ P.assertSighashType(s, ORDLOCK_SIGHASH) // pin the flag (defense-in-depth); leaves preimage
174
+ P.extractHashOutputs(s) // preimage -> committed hashOutputs
175
+ s.add(Opcode.OP_TOALTSTACK) // park committedHO; main: [trailing, buyerOuts]
176
+ s.add(Buffer.from(payBlob)).add(Opcode.OP_CAT) // buyerOuts || payBlob
177
+ s.add(Opcode.OP_SWAP).add(Opcode.OP_CAT) // (buyerOuts || payBlob) || trailing
178
+ s.add(Opcode.OP_HASH256)
179
+ s.add(Opcode.OP_FROMALTSTACK).add(Opcode.OP_EQUAL) // == committed hashOutputs ?
180
+ s.add(Opcode.OP_ENDIF)
181
+
182
+ if (params.inscription) {
183
+ var env = inscription.buildInscription({
184
+ lock: new Script(), // envelope only; the covenant above is the real lock
185
+ contentType: params.inscription.contentType,
186
+ content: params.inscription.content
187
+ })
188
+ env.chunks.forEach(function (c) { s.chunks.push(c) })
189
+ }
190
+ return s
191
+ }
192
+
193
+ function chunkIsOp (chunk, opcodenum) {
194
+ return chunk && chunk.opcodenum === opcodenum && (chunk.buf == null)
195
+ }
196
+
197
+ /** Split a pinned payBlob back into its constituent Transaction.Outputs. */
198
+ function splitPayBlob (blob) {
199
+ var br = new BufferReader(blob)
200
+ var outs = []
201
+ while (!br.eof()) outs.push(bsv.Transaction.Output.fromBufferReader(br))
202
+ return outs
203
+ }
204
+
205
+ /**
206
+ * Parse an OrdLock listing script into its economic terms. Returns null if `script`
207
+ * is not a recognizable OrdLock (so it doubles as a detector).
208
+ *
209
+ * @param {Script|Buffer|string} script
210
+ * @param {object} [opts] { network } network for the returned address strings
211
+ * ('livenet' | 'testnet' | a bsv.Networks.Network); defaults to livenet. The
212
+ * pubKeyHash / output scripts are network-agnostic and always exact.
213
+ * @returns {null|{
214
+ * seller: { pubKeyHash: Buffer, address: string },
215
+ * payOutputs: Array<{ satoshis: number, script: Script, address: string|null }>,
216
+ * payBlob: Buffer,
217
+ * totalPrice: number,
218
+ * inscription: null|{ contentType: string, content: Buffer, contentText: string }
219
+ * }}
220
+ */
221
+ function parseOrdLock (script, opts) {
222
+ var network = opts && opts.network
223
+ try {
224
+ var s = coerceScript(script)
225
+ var chunks = s.chunks
226
+ // Locate the top-level OP_IF … OP_ELSE … OP_ENDIF of the covenant.
227
+ var depth = 0
228
+ var ifIdx = -1
229
+ var elseIdx = -1
230
+ var endIdx = -1
231
+ for (var i = 0; i < chunks.length; i++) {
232
+ if (chunkIsOp(chunks[i], Opcode.OP_IF)) { if (depth === 0) ifIdx = i; depth++ } else if (chunkIsOp(chunks[i], Opcode.OP_ELSE) && depth === 1) { elseIdx = i } else if (chunkIsOp(chunks[i], Opcode.OP_ENDIF)) { depth--; if (depth === 0) { endIdx = i; break } }
233
+ }
234
+ if (ifIdx === -1 || elseIdx === -1 || endIdx === -1) return null
235
+
236
+ // sellerPKH: the sole 20-byte push in the CANCEL branch (after OP_HASH160).
237
+ var sellerPKH = null
238
+ for (var c = ifIdx + 1; c < elseIdx; c++) {
239
+ if (chunks[c].buf && chunks[c].buf.length === 20) { sellerPKH = chunks[c].buf; break }
240
+ }
241
+ if (!sellerPKH) return null
242
+
243
+ // payBlob: the constant pushed after the LAST OP_TOALTSTACK in the PURCHASE branch
244
+ // (an earlier OP_TOALTSTACK inside pushTxCore parks the pubkey; ours parks the
245
+ // committed hashOutputs and is immediately followed by the payBlob push, then
246
+ // OP_CAT OP_SWAP OP_CAT OP_HASH256).
247
+ var payBlob = null
248
+ for (var d = elseIdx + 1; d < endIdx; d++) {
249
+ if (chunkIsOp(chunks[d], Opcode.OP_TOALTSTACK) &&
250
+ chunks[d + 1] && chunks[d + 1].buf &&
251
+ chunkIsOp(chunks[d + 2], Opcode.OP_CAT) &&
252
+ chunkIsOp(chunks[d + 3], Opcode.OP_SWAP)) {
253
+ payBlob = chunks[d + 1].buf // keep scanning is unnecessary — this tail is unique
254
+ }
255
+ }
256
+ if (!payBlob) return null
257
+
258
+ var payOutputs = splitPayBlob(payBlob).map(function (o) {
259
+ var addr = null
260
+ try { if (o.script.isPublicKeyHashOut()) addr = o.script.toAddress(network).toString() } catch (e) {}
261
+ return { satoshis: o.satoshis, script: o.script, address: addr }
262
+ })
263
+ var totalPrice = payOutputs.reduce(function (a, o) { return a + o.satoshis }, 0)
264
+
265
+ var insc = null
266
+ var parsedInsc = inscription.parseInscription(s)
267
+ if (parsedInsc) {
268
+ insc = { contentType: parsedInsc.contentType, content: parsedInsc.content, contentText: parsedInsc.contentText }
269
+ }
270
+
271
+ return {
272
+ seller: { pubKeyHash: sellerPKH, address: bsv.Address.fromPublicKeyHash(sellerPKH, network).toString() },
273
+ payOutputs: payOutputs,
274
+ payBlob: payBlob,
275
+ totalPrice: totalPrice,
276
+ inscription: insc
277
+ }
278
+ } catch (e) {
279
+ return null
280
+ }
281
+ }
282
+
283
+ /** True if `script` is a recognizable OrdLock listing. */
284
+ function isOrdLock (script, opts) { return parseOrdLock(script, opts) !== null }
285
+
286
+ /**
287
+ * Build the 1-sat Transaction.Output that lists an ordinal for sale under an OrdLock.
288
+ * @returns {Transaction.Output}
289
+ */
290
+ function listInscriptionOutput (params) {
291
+ params = params || {}
292
+ var satoshis = params.satoshis != null ? params.satoshis : 1
293
+ return new bsv.Transaction.Output({ script: buildOrdLock(params), satoshis: satoshis })
294
+ }
295
+
296
+ /**
297
+ * Build the unlocking script that PURCHASES a listed ordinal.
298
+ *
299
+ * The caller must first build `spend` with the ordinal's OrdLock input at
300
+ * `inputIndex`, and its OUTPUTS arranged as:
301
+ * [ ...buyerOutputs, <pinned payment output(s)>, ...trailingOutputs ]
302
+ * where the pinned block (starting at `payoutIndex`, `payoutCount` outputs long) is the
303
+ * required payment(s) recreated EXACTLY as the listing pinned them. `payoutCount` is
304
+ * auto-derived from the listing script when omitted. Everything before the block
305
+ * (typically the 1-sat ordinal sent to the buyer) and everything after it (change, data)
306
+ * is unconstrained by the covenant.
307
+ *
308
+ * IMPORTANT ORDERING: this grinds the spend's nLockTime to a valid OP_PUSH_TX
309
+ * signature, which changes the sighash of EVERY input. Call `purchase()` BEFORE
310
+ * signing any funding inputs the buyer adds.
311
+ *
312
+ * @param {object} params
313
+ * @param {Transaction} params.spend the spend tx (outputs already set).
314
+ * @param {Script} params.lockingScript the OrdLock script being spent.
315
+ * @param {number} [params.satoshis=1] satoshis on the listing UTXO.
316
+ * @param {number} [params.inputIndex=0] the OrdLock input's index.
317
+ * @param {number} [params.payoutIndex=1] index of the first pinned payment output.
318
+ * @param {number} [params.payoutCount] number of pinned outputs (auto-derived if omitted).
319
+ * @param {boolean} [params.validate=true] assert the pinned block matches the listing.
320
+ * @param {object} [params.grind] options forwarded to PushTx.grind.
321
+ * @returns {Script} the unlocking script (also assigned onto the input).
322
+ */
323
+ function purchase (params) {
324
+ params = params || {}
325
+ var spend = params.spend
326
+ var lockingScript = params.lockingScript
327
+ if (!spend || !lockingScript) throw new Error('purchase requires { spend, lockingScript }')
328
+ var inputIndex = params.inputIndex || 0
329
+ var payoutIndex = params.payoutIndex != null ? params.payoutIndex : 1
330
+ var satoshis = params.satoshis != null ? params.satoshis : 1
331
+ var outs = spend.outputs || []
332
+
333
+ // Reuse a caller-supplied parse of the SAME lockingScript when given (avoids re-parsing).
334
+ var parsed = params.parsed || parseOrdLock(lockingScript)
335
+ var payoutCount = params.payoutCount != null ? params.payoutCount
336
+ : (parsed ? parsed.payOutputs.length : 1)
337
+ if (payoutIndex + payoutCount > outs.length) {
338
+ throw new Error('purchase: spend has fewer outputs than the pinned block requires ' +
339
+ '(need ' + (payoutIndex + payoutCount) + ', have ' + outs.length + ')')
340
+ }
341
+
342
+ // Fail-fast: the pinned block must recreate the listing's required payment(s) exactly.
343
+ if (params.validate !== false && parsed) {
344
+ var pinned = Buffer.concat(outs.slice(payoutIndex, payoutIndex + payoutCount).map(serializeOutput))
345
+ if (!pinned.equals(parsed.payBlob)) {
346
+ throw new Error('purchase: outputs [' + payoutIndex + '..' + (payoutIndex + payoutCount - 1) +
347
+ '] do not match the listing payment — the seller (or a royalty/fee payee) would not be paid')
348
+ }
349
+ }
350
+
351
+ var buyerOuts = Buffer.concat(outs.slice(0, payoutIndex).map(serializeOutput))
352
+ var trailing = Buffer.concat(outs.slice(payoutIndex + payoutCount).map(serializeOutput))
353
+
354
+ var grindOpts = Object.assign({ sighashType: ORDLOCK_SIGHASH }, params.grind || {})
355
+ var g = P.grind(spend, inputIndex, lockingScript, satoshis, grindOpts)
356
+
357
+ var us = new Script()
358
+ pushData(us, trailing)
359
+ pushData(us, buyerOuts)
360
+ us.add(Buffer.from(g.preimage))
361
+ us.add(Opcode.OP_FALSE) // select the OP_ELSE (purchase) branch
362
+ spend.inputs[inputIndex].setScript(us)
363
+ return us
364
+ }
365
+
366
+ /**
367
+ * Build the unlocking script that CANCELS a listing (seller reclaims the ordinal).
368
+ * The seller signs the spend over the OrdLock locking script with their own key.
369
+ *
370
+ * @param {object} params
371
+ * @param {PrivateKey} params.privateKey the seller's key (must hash to sellerPKH).
372
+ * @param {Transaction} params.spend the reclaim tx (outputs already set).
373
+ * @param {Script} params.lockingScript the OrdLock script being spent.
374
+ * @param {number} [params.satoshis=1] satoshis on the listing UTXO.
375
+ * @param {number} [params.inputIndex=0] the OrdLock input's index.
376
+ * @param {number} [params.sighashType] signature flag (default SIGHASH_ALL|FORKID).
377
+ * @returns {Script} the unlocking script (also assigned onto the input).
378
+ */
379
+ function cancel (params) {
380
+ params = params || {}
381
+ var privateKey = params.privateKey
382
+ var spend = params.spend
383
+ var lockingScript = params.lockingScript
384
+ if (!privateKey || !spend || !lockingScript) {
385
+ throw new Error('cancel requires { privateKey, spend, lockingScript }')
386
+ }
387
+ var inputIndex = params.inputIndex || 0
388
+ var satoshis = params.satoshis != null ? params.satoshis : 1
389
+ var sighashType = params.sighashType != null ? params.sighashType : H.SIGHASH
390
+
391
+ var sig = H.signInput(spend, privateKey, inputIndex, lockingScript, satoshis, sighashType)
392
+ var us = new Script()
393
+ us.add(Buffer.from(sig))
394
+ us.add(privateKey.toPublicKey().toBuffer())
395
+ us.add(Opcode.OP_TRUE) // select the OP_IF (cancel) branch
396
+ spend.inputs[inputIndex].setScript(us)
397
+ return us
398
+ }
399
+
400
+ /** Add an input spending `outpoint` under `script` worth `satoshis`, script-sig empty. */
401
+ function addSpendInput (tx, outpoint, script, satoshis) {
402
+ tx.addInput(new bsv.Transaction.Input({
403
+ prevTxId: outpoint.txid || outpoint.prevTxId,
404
+ outputIndex: outpoint.outputIndex != null ? outpoint.outputIndex : outpoint.vout,
405
+ script: Script.empty()
406
+ }), coerceScript(script), satoshis)
407
+ }
408
+
409
+ /** Sign input `idx` as P2PKH with `coin.privateKey` over the finalized tx (ALL|FORKID). */
410
+ function signP2PKHInput (tx, idx, coin) {
411
+ // The satoshi amount is part of the BIP-143 preimage — a missing/NaN value silently
412
+ // produces a signature over the wrong amount that fails on-chain, so require it explicitly.
413
+ assertSats(coin.satoshis, 'input ' + idx + ' satoshis')
414
+ var sc = coerceScript(coin.script)
415
+ var sig = H.signInput(tx, coin.privateKey, idx, sc, coin.satoshis, P.SIGHASH_ALL_FORKID)
416
+ var us = new Script()
417
+ us.add(Buffer.from(sig))
418
+ us.add(coin.privateKey.toPublicKey().toBuffer())
419
+ tx.inputs[idx].setScript(us)
420
+ }
421
+
422
+ /**
423
+ * Assemble a COMPLETE, signed LISTING transaction: move a 1-sat ordinal held under a
424
+ * P2PKH (its `privateKey`) into an OrdLock listing output, paying the fee from separate
425
+ * funding coins. The ordinal's satoshi (input 0's first sat) maps to output 0 (the
426
+ * listing) under ordinal FIFO, so the inscribed sat is preserved into the listing.
427
+ *
428
+ * Accepts all the OrdLock term params of `buildOrdLock` (seller, price, royalties,
429
+ * payOutputs, inscription) plus:
430
+ * @param {object} params.ordinal the ordinal UTXO: { txid|prevTxId, outputIndex|vout,
431
+ * script (P2PKH), satoshis=1, privateKey }.
432
+ * @param {Array} [params.funding] P2PKH fee coins: { ...outpoint, script, satoshis, privateKey }.
433
+ * @param {number} [params.fee=500] miner fee (paid from funding).
434
+ * @param {Address|PublicKey|string} [params.changeAddress] change recipient
435
+ * (default: the ordinal owner's address).
436
+ * @returns {{ tx: Transaction, listingScript: Script, listingOutpoint: {txid,outputIndex} }}
437
+ */
438
+ function buildListingTx (params) {
439
+ params = params || {}
440
+ var ordinal = params.ordinal
441
+ if (!ordinal || !ordinal.script || !ordinal.privateKey) {
442
+ throw new Error('buildListingTx requires ordinal { script, privateKey, txid, outputIndex }')
443
+ }
444
+ var ordSats = assertSats(ordinal.satoshis != null ? ordinal.satoshis : 1, 'ordinal satoshis')
445
+ var lock = buildOrdLock(params)
446
+
447
+ var funding = params.funding || []
448
+ var fee = params.fee != null ? params.fee : 500
449
+ var fundingTotal = funding.reduce(function (a, f) { return a + assertSats(f.satoshis, 'funding satoshis') }, 0)
450
+ var change = fundingTotal - fee
451
+ if (change < 0) {
452
+ throw new Error('buildListingTx: insufficient funding — need ' + fee + ' sat fee, funded ' + fundingTotal)
453
+ }
454
+
455
+ var tx = new bsv.Transaction()
456
+ addSpendInput(tx, ordinal, ordinal.script, ordSats) // input 0 = the ordinal (P2PKH)
457
+ funding.forEach(function (f) { addSpendInput(tx, f, f.script, f.satoshis) })
458
+
459
+ tx.addOutput(new bsv.Transaction.Output({ script: lock, satoshis: ordSats })) // output 0 = listing
460
+ if (change > 0) {
461
+ tx.addOutput(payOutputFor(params.changeAddress || ordinal.privateKey.toAddress(), change))
462
+ }
463
+
464
+ // Sign the ordinal input with the RESOLVED value (ordSats), not the raw (maybe-omitted)
465
+ // ordinal.satoshis — else the signature commits to the wrong amount and fails on-chain.
466
+ signP2PKHInput(tx, 0, { script: ordinal.script, privateKey: ordinal.privateKey, satoshis: ordSats })
467
+ funding.forEach(function (f, i) { signP2PKHInput(tx, 1 + i, f) })
468
+
469
+ return { tx: tx, listingScript: lock, listingOutpoint: { txid: tx.hash, outputIndex: 0 } }
470
+ }
471
+
472
+ /**
473
+ * Assemble a COMPLETE, signed purchase transaction from a listing UTXO and the buyer's
474
+ * P2PKH funding coins. The required payment(s) are read straight off the listing script
475
+ * (or overridden via `payOutputs`), so the buyer needs only the listing outpoint.
476
+ *
477
+ * Output layout: [ ordinal -> buyer, <pinned payment output(s)>, change -> buyer ].
478
+ * The ordinal's satoshi (input 0's first sat) maps to output 0 under ordinal FIFO, so it
479
+ * lands with the buyer; every pinned payee is paid; the fee comes from the funding coins.
480
+ *
481
+ * @param {object} params
482
+ * @param {object} params.listing { txid|prevTxId, outputIndex|vout, script, satoshis=1 }.
483
+ * @param {Address|PublicKey|string} params.ordinalDestination where the ordinal goes.
484
+ * @param {Array} params.funding buyer coins: { txid|prevTxId, outputIndex|vout, script,
485
+ * satoshis, privateKey } (P2PKH).
486
+ * @param {Array} [params.payOutputs] override the pinned payment(s) (must still match the
487
+ * listing byte-for-byte, else the covenant rejects).
488
+ * @param {number} [params.fee=500] miner fee in satoshis (paid from funding).
489
+ * @param {Address|PublicKey|string} [params.changeAddress] change recipient
490
+ * (default: ordinalDestination). Change omitted if zero.
491
+ * @param {object} [params.grind] options forwarded to PushTx.grind.
492
+ * @returns {Transaction} the fully-built, signed purchase transaction.
493
+ */
494
+ function buildPurchaseTx (params) {
495
+ params = params || {}
496
+ var listing = params.listing
497
+ if (!listing || !listing.script) throw new Error('buildPurchaseTx requires listing.script')
498
+ if (!params.ordinalDestination) throw new Error('buildPurchaseTx requires ordinalDestination')
499
+ if (!params.funding || !params.funding.length) throw new Error('buildPurchaseTx requires funding coins')
500
+
501
+ var lockingScript = coerceScript(listing.script)
502
+ var parsed = parseOrdLock(lockingScript)
503
+ if (!parsed) throw new Error('buildPurchaseTx: listing.script is not a parseable OrdLock')
504
+ var ordSats = assertSats(listing.satoshis != null ? listing.satoshis : 1, 'listing satoshis')
505
+
506
+ var payOuts = params.payOutputs
507
+ ? params.payOutputs.map(outputFromSpec)
508
+ : parsed.payOutputs.map(function (p) {
509
+ return new bsv.Transaction.Output({ script: p.script, satoshis: p.satoshis })
510
+ })
511
+ var paidTotal = payOuts.reduce(function (a, o) { return a + o.satoshis }, 0)
512
+
513
+ var fee = params.fee != null ? params.fee : 500
514
+ var fundingTotal = params.funding.reduce(function (a, f) { return a + assertSats(f.satoshis, 'funding satoshis') }, 0)
515
+ var change = fundingTotal - paidTotal - fee
516
+ if (change < 0) {
517
+ throw new Error('buildPurchaseTx: insufficient funding — need ' + (paidTotal + fee) +
518
+ ' sat (payments ' + paidTotal + ' + fee ' + fee + '), funded ' + fundingTotal)
519
+ }
520
+
521
+ var tx = new bsv.Transaction()
522
+ addSpendInput(tx, listing, lockingScript, ordSats) // input 0 = the listing (covenant)
523
+ params.funding.forEach(function (f) { addSpendInput(tx, f, f.script, f.satoshis) })
524
+
525
+ tx.addOutput(payOutputFor(params.ordinalDestination, ordSats)) // output 0 = ordinal -> buyer
526
+ payOuts.forEach(function (o) { tx.addOutput(o) }) // pinned payment(s)
527
+ if (change > 0) {
528
+ tx.addOutput(payOutputFor(params.changeAddress || params.ordinalDestination, change))
529
+ }
530
+
531
+ // Grind + covenant unlock on input 0 (fixes nLockTime for ALL inputs).
532
+ purchase({
533
+ spend: tx,
534
+ lockingScript: lockingScript,
535
+ parsed: parsed, // reuse the parse above — don't re-parse inside purchase()
536
+ satoshis: ordSats,
537
+ inputIndex: 0,
538
+ payoutIndex: 1,
539
+ payoutCount: payOuts.length,
540
+ grind: params.grind
541
+ })
542
+
543
+ // Now sign the P2PKH funding inputs over the finalized transaction.
544
+ params.funding.forEach(function (f, i) { signP2PKHInput(tx, 1 + i, f) })
545
+
546
+ return tx
547
+ }
548
+
549
+ module.exports = {
550
+ ORDLOCK_SIGHASH: ORDLOCK_SIGHASH,
551
+ buildOrdLock: buildOrdLock,
552
+ parseOrdLock: parseOrdLock,
553
+ isOrdLock: isOrdLock,
554
+ listInscriptionOutput: listInscriptionOutput,
555
+ payOutputFor: payOutputFor,
556
+ purchase: purchase,
557
+ cancel: cancel,
558
+ buildListingTx: buildListingTx,
559
+ buildPurchaseTx: buildPurchaseTx
560
+ }
@@ -16,8 +16,16 @@
16
16
  var bsv = require('../..')
17
17
 
18
18
  /**
19
- * Builder Class - Advanced covenant construction
20
- * @param {PrivateKey} privateKey - Private key for covenant operations
19
+ * @deprecated NON-ENFORCING. `buildLockingScript` / `createCovenant` emit
20
+ * `OP_DUP OP_HASH256 <build-time constant> OP_EQUALVERIFY OP_DROP <pubkey>
21
+ * OP_CHECKSIG` — the hash target is a constant known at build time, so the check
22
+ * binds nothing about the SPENDING transaction and the script reduces to plain
23
+ * P2PK (same flaw as SmartContract.Covenant). Use the enforcing OP_PUSH_TX API
24
+ * instead: `SmartContract.policy()` (declarative), or `SmartContract.PushTx` /
25
+ * `SmartContract.Token` / `SmartContract.valueCovenant`. Construct with
26
+ * `{ allowNonEnforcing: true }` only if you knowingly want the demo behavior.
27
+ *
28
+ * @param {PrivateKey} privateKey - Private key for covenant operations
21
29
  * @param {Object} options - Configuration options
22
30
  */
23
31
  function Builder(privateKey, options) {
@@ -28,6 +36,7 @@ function Builder(privateKey, options) {
28
36
  this.privateKey = privateKey
29
37
  this.publicKey = privateKey ? privateKey.publicKey : null
30
38
  this.options = options || {}
39
+ this.allowNonEnforcing = this.options.allowNonEnforcing === true
31
40
 
32
41
  // Builder state
33
42
  this.conditions = []
@@ -35,10 +44,23 @@ function Builder(privateKey, options) {
35
44
  this.scriptTemplate = null
36
45
  this.validationRules = []
37
46
 
38
- this.sighashType = this.options.sighashType ||
47
+ this.sighashType = this.options.sighashType ||
39
48
  (bsv.crypto.Signature.SIGHASH_ALL | bsv.crypto.Signature.SIGHASH_FORKID)
40
49
  }
41
50
 
51
+ // The scripts this class builds are non-enforcing (see the class @deprecated note).
52
+ // Refuse to hand one back as if it were a real covenant unless explicitly acknowledged.
53
+ Builder.prototype._assertEnforcingAllowed = function () {
54
+ if (!this.allowNonEnforcing) {
55
+ throw new Error(
56
+ 'SmartContract.Builder produces a NON-ENFORCING script (constant hash-lock → ' +
57
+ 'plain P2PK; it does not constrain the spend). Use SmartContract.policy() ' +
58
+ '(declarative OP_PUSH_TX), or SmartContract.PushTx / Token / valueCovenant for ' +
59
+ 'real covenants. To use this demo anyway, construct with { allowNonEnforcing: true }.'
60
+ )
61
+ }
62
+ }
63
+
42
64
  /**
43
65
  * Add preimage field validation
44
66
  * @param {string} field - Preimage field name
@@ -102,6 +124,7 @@ Builder.prototype.useTemplate = function(template, params) {
102
124
  * @returns {Script} Constructed locking script
103
125
  */
104
126
  Builder.prototype.buildLockingScript = function(preimageData) {
127
+ this._assertEnforcingAllowed()
105
128
  var script = new bsv.Script()
106
129
 
107
130
  if (this.scriptTemplate) {
@@ -162,6 +185,7 @@ Builder.prototype.buildUnlockingScript = function(spendingTx, inputIndex, covena
162
185
  * @returns {Object} Created covenant with metadata
163
186
  */
164
187
  Builder.prototype.createCovenant = function(utxo, buildOptions) {
188
+ this._assertEnforcingAllowed()
165
189
  buildOptions = buildOptions || {}
166
190
 
167
191
  // Create covenant creation transaction