@smartledger/bsv 5.5.3 → 6.0.0

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 +115 -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 +283 -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 +183 -0
  30. package/lib/ordinals/index.js +49 -0
  31. package/lib/ordinals/inscription.js +159 -0
  32. package/lib/ordinals/ordlock.js +535 -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 +116 -0
  52. package/test/ordinals/inscription.js +71 -0
  53. package/test/ordinals/ordlock.js +482 -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,159 @@
1
+ 'use strict'
2
+ /**
3
+ * 1Sat Ordinals inscriptions.
4
+ *
5
+ * An inscription is content (a content-type + a body) carried on a 1-satoshi output
6
+ * behind an inert data envelope, appended to a normal locking script (P2PKH by
7
+ * default). The envelope is never executed, so spending is controlled entirely by the
8
+ * base lock while the inscription rides on the satoshi:
9
+ *
10
+ * <lockingScript>
11
+ * OP_FALSE OP_IF
12
+ * "ord"
13
+ * OP_1 <contentType>
14
+ * OP_0 <content>
15
+ * OP_ENDIF
16
+ *
17
+ * This matches the widely-used 1Sat Ordinals format (js-1sat-ord).
18
+ */
19
+ var bsv = require('../..')
20
+
21
+ var ORD = Buffer.from('ord', 'utf8')
22
+
23
+ function scriptClass () { return bsv.Script }
24
+ function op () { return bsv.Opcode }
25
+
26
+ /** Resolve a base locking script from a Script, an Address, or an address string. */
27
+ function resolveLock (params) {
28
+ var Script = scriptClass()
29
+ if (params.lock) {
30
+ if (params.lock instanceof Script) return params.lock
31
+ if (Buffer.isBuffer(params.lock)) return Script.fromBuffer(params.lock)
32
+ if (typeof params.lock === 'string') return Script.fromHex(params.lock)
33
+ throw new Error('lock must be a Script, Buffer, or hex string')
34
+ }
35
+ if (params.address) {
36
+ var addr = (params.address instanceof bsv.Address)
37
+ ? params.address
38
+ : bsv.Address.fromString(String(params.address))
39
+ return Script.buildPublicKeyHashOut(addr)
40
+ }
41
+ throw new Error('buildInscription requires an address or a lock script')
42
+ }
43
+
44
+ function toBuf (v, enc) {
45
+ if (Buffer.isBuffer(v)) return v
46
+ if (v == null) return Buffer.alloc(0)
47
+ return Buffer.from(String(v), enc || 'utf8')
48
+ }
49
+
50
+ /**
51
+ * Build a 1Sat Ordinals inscription locking script: a base lock followed by the
52
+ * inert `OP_FALSE OP_IF ... OP_ENDIF` envelope carrying the content.
53
+ *
54
+ * @param {object} params
55
+ * @param {string|Buffer} params.contentType e.g. 'text/plain', 'image/png'
56
+ * @param {string|Buffer} params.content the inscription body
57
+ * @param {Script|Buffer|string} [params.lock] base locking script (overrides address)
58
+ * @param {Address|string} [params.address] P2PKH owner (used if no `lock`)
59
+ * @returns {Script} the full inscription locking script
60
+ */
61
+ function buildInscription (params) {
62
+ params = params || {}
63
+ var Opcode = op()
64
+ var lock = resolveLock(params)
65
+ var contentType = toBuf(params.contentType || 'text/plain')
66
+ var content = toBuf(params.content)
67
+
68
+ // Clone the base lock so we never mutate the caller's Script.
69
+ var s = scriptClass().fromBuffer(lock.toBuffer())
70
+ s.add(Opcode.OP_FALSE)
71
+ s.add(Opcode.OP_IF)
72
+ s.add(ORD)
73
+ s.add(Opcode.OP_1)
74
+ s.add(contentType)
75
+ s.add(Opcode.OP_0)
76
+ s.add(content)
77
+ s.add(Opcode.OP_ENDIF)
78
+ return s
79
+ }
80
+
81
+ function chunkIsOp (chunk, opcodenum) {
82
+ return chunk && chunk.opcodenum === opcodenum && (chunk.buf == null)
83
+ }
84
+
85
+ /**
86
+ * Parse a 1Sat Ordinals inscription out of a locking script.
87
+ * @param {Script|Buffer|string} script
88
+ * @returns {null|{ contentType: string, content: Buffer, contentText: string, lock: Script }}
89
+ * null if the script carries no inscription envelope.
90
+ */
91
+ function parseInscription (script) {
92
+ var Script = scriptClass()
93
+ var Opcode = op()
94
+ var s = (script instanceof Script) ? script
95
+ : Buffer.isBuffer(script) ? Script.fromBuffer(script)
96
+ : Script.fromHex(script)
97
+ var chunks = s.chunks
98
+
99
+ // Find `OP_FALSE OP_IF "ord"`.
100
+ var start = -1
101
+ for (var i = 0; i + 2 < chunks.length; i++) {
102
+ if (chunkIsOp(chunks[i], Opcode.OP_FALSE) &&
103
+ chunkIsOp(chunks[i + 1], Opcode.OP_IF) &&
104
+ chunks[i + 2].buf && chunks[i + 2].buf.equals(ORD)) {
105
+ start = i
106
+ break
107
+ }
108
+ }
109
+ if (start === -1) return null
110
+
111
+ // Everything before the envelope is the base lock.
112
+ var lock = new Script()
113
+ for (var k = 0; k < start; k++) lock.chunks.push(chunks[k])
114
+
115
+ // Walk fields after "ord" until OP_ENDIF: OP_1 => content-type, OP_0 => body.
116
+ var contentType = Buffer.alloc(0)
117
+ var content = Buffer.alloc(0)
118
+ for (var j = start + 3; j < chunks.length; j++) {
119
+ var c = chunks[j]
120
+ if (chunkIsOp(c, Opcode.OP_ENDIF)) break
121
+ if (chunkIsOp(c, Opcode.OP_1) && chunks[j + 1] && chunks[j + 1].buf) {
122
+ contentType = chunks[j + 1].buf; j++
123
+ } else if (chunkIsOp(c, Opcode.OP_0) && chunks[j + 1] && chunks[j + 1].buf) {
124
+ content = chunks[j + 1].buf; j++
125
+ }
126
+ }
127
+
128
+ return {
129
+ contentType: contentType.toString('utf8'),
130
+ content: content,
131
+ contentText: content.toString('utf8'),
132
+ lock: lock
133
+ }
134
+ }
135
+
136
+ /** True if the script carries an inscription envelope. */
137
+ function isInscription (script) {
138
+ try { return parseInscription(script) !== null } catch (e) { return false }
139
+ }
140
+
141
+ /**
142
+ * Build the 1-satoshi Transaction.Output carrying an inscription.
143
+ * @returns {Transaction.Output}
144
+ */
145
+ function createInscriptionOutput (params) {
146
+ params = params || {}
147
+ var satoshis = params.satoshis != null ? params.satoshis : 1
148
+ return new bsv.Transaction.Output({
149
+ script: buildInscription(params),
150
+ satoshis: satoshis
151
+ })
152
+ }
153
+
154
+ module.exports = {
155
+ buildInscription: buildInscription,
156
+ parseInscription: parseInscription,
157
+ isInscription: isInscription,
158
+ createInscriptionOutput: createInscriptionOutput
159
+ }
@@ -0,0 +1,535 @@
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
+ var BN = bsv.crypto.BN
49
+
50
+ // OrdLock commits to the FULL output set (so payments can be pinned) but leaves the
51
+ // input set open for the buyer's funding — SIGHASH_ALL|ANYONECANPAY|FORKID (0xc1).
52
+ var ORDLOCK_SIGHASH = P.SIGHASH_ALL_ANYONECANPAY_FORKID
53
+
54
+ /** Push a possibly-empty buffer MINIMALDATA-cleanly (empty => OP_0, i.e. empty vector). */
55
+ function pushData (s, buf) { return (buf && buf.length) ? s.add(Buffer.from(buf)) : s.add(Opcode.OP_0) }
56
+
57
+ /** Serialize a Transaction.Output to its on-wire bytes (8-byte value || varint || script). */
58
+ function serializeOutput (out) { return out.toBufferWriter().toBuffer() }
59
+
60
+ /** Coerce a Script, Buffer, or hex string to a Script. */
61
+ function coerceScript (v) {
62
+ if (v instanceof Script) return v
63
+ if (Buffer.isBuffer(v)) return Script.fromBuffer(v)
64
+ return Script.fromHex(v)
65
+ }
66
+
67
+ /** Coerce an Address, PublicKey, PrivateKey, or address string to an Address. */
68
+ function toAddress (v) {
69
+ if (v instanceof bsv.Address) return v
70
+ if (v && typeof v.toAddress === 'function') return v.toAddress() // PublicKey / PrivateKey
71
+ return bsv.Address.fromString(String(v))
72
+ }
73
+
74
+ /** Resolve a 20-byte HASH160 owner commitment from an Address/PublicKey/key or 20-byte Buffer. */
75
+ function resolvePubKeyHash (owner) {
76
+ if (Buffer.isBuffer(owner)) {
77
+ if (owner.length !== 20) throw new Error('seller pubkeyhash buffer must be 20 bytes (HASH160)')
78
+ return owner
79
+ }
80
+ if (owner instanceof bsv.PublicKey) return bsv.crypto.Hash.sha256ripemd160(owner.toBuffer())
81
+ return toAddress(owner).hashBuffer
82
+ }
83
+
84
+ /** Build the seller's payment Output (P2PKH) from an address/pubkey/key and a price in sats. */
85
+ function payOutputFor (payTo, price) {
86
+ if (payTo instanceof bsv.Transaction.Output) return payTo
87
+ return new bsv.Transaction.Output({
88
+ script: Script.buildPublicKeyHashOut(toAddress(payTo)), satoshis: price
89
+ })
90
+ }
91
+
92
+ /** Coerce one payment spec to a Transaction.Output. */
93
+ function outputFromSpec (spec) {
94
+ if (spec instanceof bsv.Transaction.Output) return spec
95
+ if (Buffer.isBuffer(spec)) return bsv.Transaction.Output.fromBufferReader(new BufferReader(spec))
96
+ var to = spec.payTo != null ? spec.payTo : spec.address
97
+ var amt = spec.satoshis != null ? spec.satoshis : spec.price
98
+ if (to == null || amt == null) throw new Error('payment spec needs {address|payTo, satoshis|price}')
99
+ return payOutputFor(to, amt)
100
+ }
101
+
102
+ /**
103
+ * Resolve the required payment output(s) for a listing, in the order they must be
104
+ * recreated. Accepts (in priority order): `payOutputs` (array of Output/spec/Buffer),
105
+ * a single `payOutput`, or `price`+`payTo` (defaulting the recipient to `seller`) with
106
+ * optional `royalties` appended.
107
+ */
108
+ function resolvePayOutputs (params) {
109
+ if (params.payOutputs) {
110
+ if (!params.payOutputs.length) throw new Error('payOutputs must be a non-empty array')
111
+ return params.payOutputs.map(outputFromSpec)
112
+ }
113
+ if (params.payOutput != null) return [outputFromSpec(params.payOutput)]
114
+ if (params.price != null) {
115
+ var arr = [payOutputFor(params.payTo != null ? params.payTo : params.seller, params.price)]
116
+ if (params.royalties) params.royalties.forEach(function (r) { arr.push(outputFromSpec(r)) })
117
+ return arr
118
+ }
119
+ throw new Error('buildOrdLock requires a price, a payOutput, or payOutputs')
120
+ }
121
+
122
+ /** The pinned payment bytes = concatenation of every required output's serialization. */
123
+ function payBlobFrom (outputs) { return Buffer.concat(outputs.map(serializeOutput)) }
124
+
125
+ /**
126
+ * Build an OrdLock listing (locking) script.
127
+ *
128
+ * @param {object} params
129
+ * @param {Address|PublicKey|string|Buffer} params.seller who may CANCEL the listing
130
+ * (an address, public key, or 20-byte HASH160). Also the default payment recipient.
131
+ * @param {number} [params.price] asking price in satoshis (simple single-payment form).
132
+ * @param {Address|PublicKey|string|Transaction.Output} [params.payTo] payment recipient
133
+ * for `price` (default: `seller`).
134
+ * @param {Array} [params.royalties] extra payment specs appended after the `price` output
135
+ * (each: Transaction.Output, {address,satoshis}, or a serialized-output Buffer).
136
+ * @param {Array} [params.payOutputs] pin an explicit ordered list of payment outputs,
137
+ * overriding price/payTo/royalties (seller + royalty + marketplace-fee, etc.).
138
+ * @param {Transaction.Output|Buffer} [params.payOutput] pin a single explicit output.
139
+ * @param {object} [params.inscription] if given ({contentType, content}), an inscription
140
+ * envelope is appended so a fresh inscribe+list share one output.
141
+ * @returns {Script} the OrdLock locking script.
142
+ */
143
+ function buildOrdLock (params) {
144
+ params = params || {}
145
+ if (params.seller == null) throw new Error('buildOrdLock requires a seller')
146
+ var sellerPKH = resolvePubKeyHash(params.seller)
147
+ var payBlob = payBlobFrom(resolvePayOutputs(params))
148
+
149
+ var s = new Script()
150
+ s.add(Opcode.OP_IF)
151
+ // CANCEL: seller signs with their key; standard P2PKH gate.
152
+ s.add(Opcode.OP_DUP).add(Opcode.OP_HASH160)
153
+ s.add(Buffer.from(sellerPKH)).add(Opcode.OP_EQUALVERIFY).add(Opcode.OP_CHECKSIG)
154
+ s.add(Opcode.OP_ELSE)
155
+ // PURCHASE: stack (after OP_IF pops the false marker) = [.., trailing, buyerOuts, preimage]
156
+ s.add(Opcode.OP_DUP)
157
+ P.pushTxCore(s, { sighashType: ORDLOCK_SIGHASH }) // authenticate the preimage
158
+ s.add(Opcode.OP_VERIFY)
159
+ P.assertSighashType(s, ORDLOCK_SIGHASH) // pin the flag (defense-in-depth); leaves preimage
160
+ P.extractHashOutputs(s) // preimage -> committed hashOutputs
161
+ s.add(Opcode.OP_TOALTSTACK) // park committedHO; main: [trailing, buyerOuts]
162
+ s.add(Buffer.from(payBlob)).add(Opcode.OP_CAT) // buyerOuts || payBlob
163
+ s.add(Opcode.OP_SWAP).add(Opcode.OP_CAT) // (buyerOuts || payBlob) || trailing
164
+ s.add(Opcode.OP_HASH256)
165
+ s.add(Opcode.OP_FROMALTSTACK).add(Opcode.OP_EQUAL) // == committed hashOutputs ?
166
+ s.add(Opcode.OP_ENDIF)
167
+
168
+ if (params.inscription) {
169
+ var env = inscription.buildInscription({
170
+ lock: new Script(), // envelope only; the covenant above is the real lock
171
+ contentType: params.inscription.contentType,
172
+ content: params.inscription.content
173
+ })
174
+ env.chunks.forEach(function (c) { s.chunks.push(c) })
175
+ }
176
+ return s
177
+ }
178
+
179
+ function chunkIsOp (chunk, opcodenum) {
180
+ return chunk && chunk.opcodenum === opcodenum && (chunk.buf == null)
181
+ }
182
+
183
+ /** Split a pinned payBlob back into its constituent Transaction.Outputs. */
184
+ function splitPayBlob (blob) {
185
+ var br = new BufferReader(blob)
186
+ var outs = []
187
+ while (!br.eof()) outs.push(bsv.Transaction.Output.fromBufferReader(br))
188
+ return outs
189
+ }
190
+
191
+ /**
192
+ * Parse an OrdLock listing script into its economic terms. Returns null if `script`
193
+ * is not a recognizable OrdLock (so it doubles as a detector).
194
+ *
195
+ * @param {Script|Buffer|string} script
196
+ * @returns {null|{
197
+ * seller: { pubKeyHash: Buffer, address: string },
198
+ * payOutputs: Array<{ satoshis: number, script: Script, address: string|null }>,
199
+ * payBlob: Buffer,
200
+ * totalPrice: number,
201
+ * inscription: null|{ contentType: string, content: Buffer, contentText: string }
202
+ * }}
203
+ */
204
+ function parseOrdLock (script) {
205
+ try {
206
+ var s = coerceScript(script)
207
+ var chunks = s.chunks
208
+ // Locate the top-level OP_IF … OP_ELSE … OP_ENDIF of the covenant.
209
+ var depth = 0
210
+ var ifIdx = -1
211
+ var elseIdx = -1
212
+ var endIdx = -1
213
+ for (var i = 0; i < chunks.length; i++) {
214
+ 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 } }
215
+ }
216
+ if (ifIdx === -1 || elseIdx === -1 || endIdx === -1) return null
217
+
218
+ // sellerPKH: the sole 20-byte push in the CANCEL branch (after OP_HASH160).
219
+ var sellerPKH = null
220
+ for (var c = ifIdx + 1; c < elseIdx; c++) {
221
+ if (chunks[c].buf && chunks[c].buf.length === 20) { sellerPKH = chunks[c].buf; break }
222
+ }
223
+ if (!sellerPKH) return null
224
+
225
+ // payBlob: the constant pushed after the LAST OP_TOALTSTACK in the PURCHASE branch
226
+ // (an earlier OP_TOALTSTACK inside pushTxCore parks the pubkey; ours parks the
227
+ // committed hashOutputs and is immediately followed by the payBlob push, then
228
+ // OP_CAT OP_SWAP OP_CAT OP_HASH256).
229
+ var payBlob = null
230
+ for (var d = elseIdx + 1; d < endIdx; d++) {
231
+ if (chunkIsOp(chunks[d], Opcode.OP_TOALTSTACK) &&
232
+ chunks[d + 1] && chunks[d + 1].buf &&
233
+ chunkIsOp(chunks[d + 2], Opcode.OP_CAT) &&
234
+ chunkIsOp(chunks[d + 3], Opcode.OP_SWAP)) {
235
+ payBlob = chunks[d + 1].buf // keep scanning is unnecessary — this tail is unique
236
+ }
237
+ }
238
+ if (!payBlob) return null
239
+
240
+ var payOutputs = splitPayBlob(payBlob).map(function (o) {
241
+ var addr = null
242
+ try { if (o.script.isPublicKeyHashOut()) addr = o.script.toAddress().toString() } catch (e) {}
243
+ return { satoshis: o.satoshis, script: o.script, address: addr }
244
+ })
245
+ var totalPrice = payOutputs.reduce(function (a, o) { return a + o.satoshis }, 0)
246
+
247
+ var insc = null
248
+ var parsedInsc = inscription.parseInscription(s)
249
+ if (parsedInsc) {
250
+ insc = { contentType: parsedInsc.contentType, content: parsedInsc.content, contentText: parsedInsc.contentText }
251
+ }
252
+
253
+ return {
254
+ seller: { pubKeyHash: sellerPKH, address: bsv.Address.fromPublicKeyHash(sellerPKH).toString() },
255
+ payOutputs: payOutputs,
256
+ payBlob: payBlob,
257
+ totalPrice: totalPrice,
258
+ inscription: insc
259
+ }
260
+ } catch (e) {
261
+ return null
262
+ }
263
+ }
264
+
265
+ /** True if `script` is a recognizable OrdLock listing. */
266
+ function isOrdLock (script) { return parseOrdLock(script) !== null }
267
+
268
+ /**
269
+ * Build the 1-sat Transaction.Output that lists an ordinal for sale under an OrdLock.
270
+ * @returns {Transaction.Output}
271
+ */
272
+ function listInscriptionOutput (params) {
273
+ params = params || {}
274
+ var satoshis = params.satoshis != null ? params.satoshis : 1
275
+ return new bsv.Transaction.Output({ script: buildOrdLock(params), satoshis: satoshis })
276
+ }
277
+
278
+ /**
279
+ * Build the unlocking script that PURCHASES a listed ordinal.
280
+ *
281
+ * The caller must first build `spend` with the ordinal's OrdLock input at
282
+ * `inputIndex`, and its OUTPUTS arranged as:
283
+ * [ ...buyerOutputs, <pinned payment output(s)>, ...trailingOutputs ]
284
+ * where the pinned block (starting at `payoutIndex`, `payoutCount` outputs long) is the
285
+ * required payment(s) recreated EXACTLY as the listing pinned them. `payoutCount` is
286
+ * auto-derived from the listing script when omitted. Everything before the block
287
+ * (typically the 1-sat ordinal sent to the buyer) and everything after it (change, data)
288
+ * is unconstrained by the covenant.
289
+ *
290
+ * IMPORTANT ORDERING: this grinds the spend's nLockTime to a valid OP_PUSH_TX
291
+ * signature, which changes the sighash of EVERY input. Call `purchase()` BEFORE
292
+ * signing any funding inputs the buyer adds.
293
+ *
294
+ * @param {object} params
295
+ * @param {Transaction} params.spend the spend tx (outputs already set).
296
+ * @param {Script} params.lockingScript the OrdLock script being spent.
297
+ * @param {number} [params.satoshis=1] satoshis on the listing UTXO.
298
+ * @param {number} [params.inputIndex=0] the OrdLock input's index.
299
+ * @param {number} [params.payoutIndex=1] index of the first pinned payment output.
300
+ * @param {number} [params.payoutCount] number of pinned outputs (auto-derived if omitted).
301
+ * @param {boolean} [params.validate=true] assert the pinned block matches the listing.
302
+ * @param {object} [params.grind] options forwarded to PushTx.grind.
303
+ * @returns {Script} the unlocking script (also assigned onto the input).
304
+ */
305
+ function purchase (params) {
306
+ params = params || {}
307
+ var spend = params.spend
308
+ var lockingScript = params.lockingScript
309
+ if (!spend || !lockingScript) throw new Error('purchase requires { spend, lockingScript }')
310
+ var inputIndex = params.inputIndex || 0
311
+ var payoutIndex = params.payoutIndex != null ? params.payoutIndex : 1
312
+ var satoshis = params.satoshis != null ? params.satoshis : 1
313
+ var outs = spend.outputs || []
314
+
315
+ var parsed = parseOrdLock(lockingScript)
316
+ var payoutCount = params.payoutCount != null ? params.payoutCount
317
+ : (parsed ? parsed.payOutputs.length : 1)
318
+ if (payoutIndex + payoutCount > outs.length) {
319
+ throw new Error('purchase: spend has fewer outputs than the pinned block requires ' +
320
+ '(need ' + (payoutIndex + payoutCount) + ', have ' + outs.length + ')')
321
+ }
322
+
323
+ // Fail-fast: the pinned block must recreate the listing's required payment(s) exactly.
324
+ if (params.validate !== false && parsed) {
325
+ var pinned = Buffer.concat(outs.slice(payoutIndex, payoutIndex + payoutCount).map(serializeOutput))
326
+ if (!pinned.equals(parsed.payBlob)) {
327
+ throw new Error('purchase: outputs [' + payoutIndex + '..' + (payoutIndex + payoutCount - 1) +
328
+ '] do not match the listing payment — the seller (or a royalty/fee payee) would not be paid')
329
+ }
330
+ }
331
+
332
+ var buyerOuts = Buffer.concat(outs.slice(0, payoutIndex).map(serializeOutput))
333
+ var trailing = Buffer.concat(outs.slice(payoutIndex + payoutCount).map(serializeOutput))
334
+
335
+ var grindOpts = Object.assign({ sighashType: ORDLOCK_SIGHASH }, params.grind || {})
336
+ var g = P.grind(spend, inputIndex, lockingScript, satoshis, grindOpts)
337
+
338
+ var us = new Script()
339
+ pushData(us, trailing)
340
+ pushData(us, buyerOuts)
341
+ us.add(Buffer.from(g.preimage))
342
+ us.add(Opcode.OP_FALSE) // select the OP_ELSE (purchase) branch
343
+ spend.inputs[inputIndex].setScript(us)
344
+ return us
345
+ }
346
+
347
+ /**
348
+ * Build the unlocking script that CANCELS a listing (seller reclaims the ordinal).
349
+ * The seller signs the spend over the OrdLock locking script with their own key.
350
+ *
351
+ * @param {object} params
352
+ * @param {PrivateKey} params.privateKey the seller's key (must hash to sellerPKH).
353
+ * @param {Transaction} params.spend the reclaim tx (outputs already set).
354
+ * @param {Script} params.lockingScript the OrdLock script being spent.
355
+ * @param {number} [params.satoshis=1] satoshis on the listing UTXO.
356
+ * @param {number} [params.inputIndex=0] the OrdLock input's index.
357
+ * @param {number} [params.sighashType] signature flag (default SIGHASH_ALL|FORKID).
358
+ * @returns {Script} the unlocking script (also assigned onto the input).
359
+ */
360
+ function cancel (params) {
361
+ params = params || {}
362
+ var privateKey = params.privateKey
363
+ var spend = params.spend
364
+ var lockingScript = params.lockingScript
365
+ if (!privateKey || !spend || !lockingScript) {
366
+ throw new Error('cancel requires { privateKey, spend, lockingScript }')
367
+ }
368
+ var inputIndex = params.inputIndex || 0
369
+ var satoshis = params.satoshis != null ? params.satoshis : 1
370
+ var sighashType = params.sighashType != null ? params.sighashType : H.SIGHASH
371
+
372
+ var sig = H.signInput(spend, privateKey, inputIndex, lockingScript, satoshis, sighashType)
373
+ var us = new Script()
374
+ us.add(Buffer.from(sig))
375
+ us.add(privateKey.toPublicKey().toBuffer())
376
+ us.add(Opcode.OP_TRUE) // select the OP_IF (cancel) branch
377
+ spend.inputs[inputIndex].setScript(us)
378
+ return us
379
+ }
380
+
381
+ /** Add an input spending `outpoint` under `script` worth `satoshis`, script-sig empty. */
382
+ function addSpendInput (tx, outpoint, script, satoshis) {
383
+ tx.addInput(new bsv.Transaction.Input({
384
+ prevTxId: outpoint.txid || outpoint.prevTxId,
385
+ outputIndex: outpoint.outputIndex != null ? outpoint.outputIndex : outpoint.vout,
386
+ script: Script.empty()
387
+ }), coerceScript(script), satoshis)
388
+ }
389
+
390
+ /** Sign input `idx` as P2PKH with `coin.privateKey` over the finalized tx (ALL|FORKID). */
391
+ function signP2PKHInput (tx, idx, coin) {
392
+ var sc = coerceScript(coin.script)
393
+ var sig = H.signInput(tx, coin.privateKey, idx, sc, coin.satoshis, P.SIGHASH_ALL_FORKID)
394
+ var us = new Script()
395
+ us.add(Buffer.from(sig))
396
+ us.add(coin.privateKey.toPublicKey().toBuffer())
397
+ tx.inputs[idx].setScript(us)
398
+ }
399
+
400
+ /**
401
+ * Assemble a COMPLETE, signed LISTING transaction: move a 1-sat ordinal held under a
402
+ * P2PKH (its `privateKey`) into an OrdLock listing output, paying the fee from separate
403
+ * funding coins. The ordinal's satoshi (input 0's first sat) maps to output 0 (the
404
+ * listing) under ordinal FIFO, so the inscribed sat is preserved into the listing.
405
+ *
406
+ * Accepts all the OrdLock term params of `buildOrdLock` (seller, price, royalties,
407
+ * payOutputs, inscription) plus:
408
+ * @param {object} params.ordinal the ordinal UTXO: { txid|prevTxId, outputIndex|vout,
409
+ * script (P2PKH), satoshis=1, privateKey }.
410
+ * @param {Array} [params.funding] P2PKH fee coins: { ...outpoint, script, satoshis, privateKey }.
411
+ * @param {number} [params.fee=500] miner fee (paid from funding).
412
+ * @param {Address|PublicKey|string} [params.changeAddress] change recipient
413
+ * (default: the ordinal owner's address).
414
+ * @returns {{ tx: Transaction, listingScript: Script, listingOutpoint: {txid,outputIndex} }}
415
+ */
416
+ function buildListingTx (params) {
417
+ params = params || {}
418
+ var ordinal = params.ordinal
419
+ if (!ordinal || !ordinal.script || !ordinal.privateKey) {
420
+ throw new Error('buildListingTx requires ordinal { script, privateKey, txid, outputIndex }')
421
+ }
422
+ var ordSats = ordinal.satoshis != null ? ordinal.satoshis : 1
423
+ var lock = buildOrdLock(params)
424
+
425
+ var funding = params.funding || []
426
+ var fee = params.fee != null ? params.fee : 500
427
+ var fundingTotal = funding.reduce(function (a, f) { return a + f.satoshis }, 0)
428
+ var change = fundingTotal - fee
429
+ if (change < 0) {
430
+ throw new Error('buildListingTx: insufficient funding — need ' + fee + ' sat fee, funded ' + fundingTotal)
431
+ }
432
+
433
+ var tx = new bsv.Transaction()
434
+ addSpendInput(tx, ordinal, ordinal.script, ordSats) // input 0 = the ordinal (P2PKH)
435
+ funding.forEach(function (f) { addSpendInput(tx, f, f.script, f.satoshis) })
436
+
437
+ tx.addOutput(new bsv.Transaction.Output({ script: lock, satoshis: ordSats })) // output 0 = listing
438
+ if (change > 0) {
439
+ tx.addOutput(payOutputFor(params.changeAddress || ordinal.privateKey.toAddress(), change))
440
+ }
441
+
442
+ signP2PKHInput(tx, 0, ordinal)
443
+ funding.forEach(function (f, i) { signP2PKHInput(tx, 1 + i, f) })
444
+
445
+ return { tx: tx, listingScript: lock, listingOutpoint: { txid: tx.hash, outputIndex: 0 } }
446
+ }
447
+
448
+ /**
449
+ * Assemble a COMPLETE, signed purchase transaction from a listing UTXO and the buyer's
450
+ * P2PKH funding coins. The required payment(s) are read straight off the listing script
451
+ * (or overridden via `payOutputs`), so the buyer needs only the listing outpoint.
452
+ *
453
+ * Output layout: [ ordinal -> buyer, <pinned payment output(s)>, change -> buyer ].
454
+ * The ordinal's satoshi (input 0's first sat) maps to output 0 under ordinal FIFO, so it
455
+ * lands with the buyer; every pinned payee is paid; the fee comes from the funding coins.
456
+ *
457
+ * @param {object} params
458
+ * @param {object} params.listing { txid|prevTxId, outputIndex|vout, script, satoshis=1 }.
459
+ * @param {Address|PublicKey|string} params.ordinalDestination where the ordinal goes.
460
+ * @param {Array} params.funding buyer coins: { txid|prevTxId, outputIndex|vout, script,
461
+ * satoshis, privateKey } (P2PKH).
462
+ * @param {Array} [params.payOutputs] override the pinned payment(s) (must still match the
463
+ * listing byte-for-byte, else the covenant rejects).
464
+ * @param {number} [params.fee=500] miner fee in satoshis (paid from funding).
465
+ * @param {Address|PublicKey|string} [params.changeAddress] change recipient
466
+ * (default: ordinalDestination). Change omitted if zero.
467
+ * @param {object} [params.grind] options forwarded to PushTx.grind.
468
+ * @returns {Transaction} the fully-built, signed purchase transaction.
469
+ */
470
+ function buildPurchaseTx (params) {
471
+ params = params || {}
472
+ var listing = params.listing
473
+ if (!listing || !listing.script) throw new Error('buildPurchaseTx requires listing.script')
474
+ if (!params.ordinalDestination) throw new Error('buildPurchaseTx requires ordinalDestination')
475
+ if (!params.funding || !params.funding.length) throw new Error('buildPurchaseTx requires funding coins')
476
+
477
+ var lockingScript = coerceScript(listing.script)
478
+ var parsed = parseOrdLock(lockingScript)
479
+ if (!parsed) throw new Error('buildPurchaseTx: listing.script is not a parseable OrdLock')
480
+ var ordSats = listing.satoshis != null ? listing.satoshis : 1
481
+
482
+ var payOuts = params.payOutputs
483
+ ? params.payOutputs.map(outputFromSpec)
484
+ : parsed.payOutputs.map(function (p) {
485
+ return new bsv.Transaction.Output({ script: p.script, satoshis: p.satoshis })
486
+ })
487
+ var paidTotal = payOuts.reduce(function (a, o) { return a + o.satoshis }, 0)
488
+
489
+ var fee = params.fee != null ? params.fee : 500
490
+ var fundingTotal = params.funding.reduce(function (a, f) { return a + f.satoshis }, 0)
491
+ var change = fundingTotal - paidTotal - fee
492
+ if (change < 0) {
493
+ throw new Error('buildPurchaseTx: insufficient funding — need ' + (paidTotal + fee) +
494
+ ' sat (payments ' + paidTotal + ' + fee ' + fee + '), funded ' + fundingTotal)
495
+ }
496
+
497
+ var tx = new bsv.Transaction()
498
+ addSpendInput(tx, listing, lockingScript, ordSats) // input 0 = the listing (covenant)
499
+ params.funding.forEach(function (f) { addSpendInput(tx, f, f.script, f.satoshis) })
500
+
501
+ tx.addOutput(payOutputFor(params.ordinalDestination, ordSats)) // output 0 = ordinal -> buyer
502
+ payOuts.forEach(function (o) { tx.addOutput(o) }) // pinned payment(s)
503
+ if (change > 0) {
504
+ tx.addOutput(payOutputFor(params.changeAddress || params.ordinalDestination, change))
505
+ }
506
+
507
+ // Grind + covenant unlock on input 0 (fixes nLockTime for ALL inputs).
508
+ purchase({
509
+ spend: tx,
510
+ lockingScript: lockingScript,
511
+ satoshis: ordSats,
512
+ inputIndex: 0,
513
+ payoutIndex: 1,
514
+ payoutCount: payOuts.length,
515
+ grind: params.grind
516
+ })
517
+
518
+ // Now sign the P2PKH funding inputs over the finalized transaction.
519
+ params.funding.forEach(function (f, i) { signP2PKHInput(tx, 1 + i, f) })
520
+
521
+ return tx
522
+ }
523
+
524
+ module.exports = {
525
+ ORDLOCK_SIGHASH: ORDLOCK_SIGHASH,
526
+ buildOrdLock: buildOrdLock,
527
+ parseOrdLock: parseOrdLock,
528
+ isOrdLock: isOrdLock,
529
+ listInscriptionOutput: listInscriptionOutput,
530
+ payOutputFor: payOutputFor,
531
+ purchase: purchase,
532
+ cancel: cancel,
533
+ buildListingTx: buildListingTx,
534
+ buildPurchaseTx: buildPurchaseTx
535
+ }