@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,552 @@
1
+ 'use strict'
2
+
3
+ /* global describe, it, before, after */
4
+
5
+ // OrdLock marketplace covenant — list a 1Sat ordinal for sale behind a trustless
6
+ // "pay the seller or cancel" covenant. Every locking/unlocking pair below is driven
7
+ // through the consensus interpreter (the "if it emits Script, it's interpreter-
8
+ // verified" operating principle), including adversarial spends that MUST be rejected.
9
+
10
+ require('chai').should()
11
+ var bsv = require('../..')
12
+ var Ord = bsv.Ordinals
13
+ var SC = bsv.SmartContract
14
+ var Script = bsv.Script
15
+ var PrivateKey = bsv.PrivateKey
16
+ var Transaction = bsv.Transaction
17
+
18
+ var help = SC.CovenantHelpers
19
+ var verify = help.verify
20
+ var p2pkhOutput = help.p2pkhOutput
21
+
22
+ var ORD_SATS = 1
23
+ var PRICE = 100000
24
+
25
+ describe('Ordinals OrdLock marketplace', function () {
26
+ this.timeout(20000)
27
+ var I = bsv.Script.Interpreter
28
+ var saved
29
+
30
+ before(function () {
31
+ saved = { el: I.MAX_SCRIPT_ELEMENT_SIZE, num: I.MAXIMUM_ELEMENT_SIZE, ops: I.MAX_OPS_PER_SCRIPT }
32
+ SC.enableGenesis()
33
+ })
34
+ after(function () {
35
+ I.MAX_SCRIPT_ELEMENT_SIZE = saved.el
36
+ I.MAXIMUM_ELEMENT_SIZE = saved.num
37
+ I.MAX_OPS_PER_SCRIPT = saved.ops
38
+ })
39
+
40
+ var seller = PrivateKey.fromRandom()
41
+ var buyer = PrivateKey.fromRandom()
42
+
43
+ // A funding tx that pays the listing UTXO, plus a spend consuming it at input 0.
44
+ function fundListing (lock) {
45
+ return help.fundAndSpend(lock, ORD_SATS, {}).spend
46
+ }
47
+
48
+ // Build the buyer's outputs: [ ordinal->buyer (index 0), payment->seller (index 1) ].
49
+ function buyerOutputs (payTo, price) {
50
+ return [
51
+ p2pkhOutput(buyer, ORD_SATS), // the ordinal goes to the buyer at output 0
52
+ Ord.payOutputFor(payTo || seller, price == null ? PRICE : price) // seller payment at 1
53
+ ]
54
+ }
55
+
56
+ it('verifies a purchase that pays the seller', function () {
57
+ var lock = Ord.buildOrdLock({ seller: seller.toAddress(), price: PRICE })
58
+ var spend = fundListing(lock)
59
+ buyerOutputs().forEach(function (o) { spend.addOutput(o) })
60
+ Ord.purchaseOrdLock({ spend: spend, lockingScript: lock, satoshis: ORD_SATS })
61
+ verify(spend.inputs[0].script, lock, { tx: spend, satoshis: ORD_SATS }).ok.should.equal(true)
62
+ })
63
+
64
+ it('stays valid when the buyer adds a funding input and a change output', function () {
65
+ var lock = Ord.buildOrdLock({ seller: seller.toAddress(), price: PRICE })
66
+ var spend = fundListing(lock)
67
+ // outputs: [ordinal->buyer, payment->seller, change->buyer]
68
+ spend.addOutput(p2pkhOutput(buyer, ORD_SATS))
69
+ spend.addOutput(Ord.payOutputFor(seller, PRICE))
70
+ spend.addOutput(p2pkhOutput(buyer, 40000)) // buyer change, trailing
71
+
72
+ // Grind + build the unlock BEFORE adding the (ANYONECANPAY) funding input.
73
+ Ord.purchaseOrdLock({ spend: spend, lockingScript: lock, satoshis: ORD_SATS })
74
+ spend.addInput(new Transaction.Input({
75
+ prevTxId: '22'.repeat(32), outputIndex: 0, script: Script.empty()
76
+ }), p2pkhOutput(buyer, PRICE + 50000).script, PRICE + 50000)
77
+
78
+ verify(spend.inputs[0].script, lock, { tx: spend, satoshis: ORD_SATS }).ok.should.equal(true)
79
+ })
80
+
81
+ it('rejects a purchase that underpays the seller', function () {
82
+ var lock = Ord.buildOrdLock({ seller: seller.toAddress(), price: PRICE })
83
+ var spend = fundListing(lock)
84
+ spend.addOutput(p2pkhOutput(buyer, ORD_SATS))
85
+ spend.addOutput(Ord.payOutputFor(seller, PRICE))
86
+ Ord.purchaseOrdLock({ spend: spend, lockingScript: lock, satoshis: ORD_SATS })
87
+
88
+ // Tamper: shave the seller's payment after grinding → committed outputs change.
89
+ spend.outputs[1] = Ord.payOutputFor(seller, PRICE - 1)
90
+ verify(spend.inputs[0].script, lock, { tx: spend, satoshis: ORD_SATS }).ok.should.equal(false)
91
+ })
92
+
93
+ it('rejects a purchase that redirects the payment to someone else', function () {
94
+ var lock = Ord.buildOrdLock({ seller: seller.toAddress(), price: PRICE })
95
+ var spend = fundListing(lock)
96
+ spend.addOutput(p2pkhOutput(buyer, ORD_SATS))
97
+ spend.addOutput(Ord.payOutputFor(seller, PRICE))
98
+ Ord.purchaseOrdLock({ spend: spend, lockingScript: lock, satoshis: ORD_SATS })
99
+
100
+ spend.outputs[1] = Ord.payOutputFor(PrivateKey.fromRandom(), PRICE) // thief's address
101
+ verify(spend.inputs[0].script, lock, { tx: spend, satoshis: ORD_SATS }).ok.should.equal(false)
102
+ })
103
+
104
+ it('pays a distinct payTo recipient while the seller keeps cancel rights', function () {
105
+ var payee = PrivateKey.fromRandom()
106
+ var lock = Ord.buildOrdLock({ seller: seller.toAddress(), payTo: payee.toAddress(), price: PRICE })
107
+ var spend = fundListing(lock)
108
+ spend.addOutput(p2pkhOutput(buyer, ORD_SATS))
109
+ spend.addOutput(Ord.payOutputFor(payee, PRICE))
110
+ Ord.purchaseOrdLock({ spend: spend, lockingScript: lock, satoshis: ORD_SATS })
111
+ verify(spend.inputs[0].script, lock, { tx: spend, satoshis: ORD_SATS }).ok.should.equal(true)
112
+ })
113
+
114
+ it('purchase() fail-fasts (throws) when the payout output is wrong', function () {
115
+ var lock = Ord.buildOrdLock({ seller: seller.toAddress(), price: PRICE })
116
+ var spend = fundListing(lock)
117
+ spend.addOutput(p2pkhOutput(buyer, ORD_SATS))
118
+ spend.addOutput(Ord.payOutputFor(seller, PRICE - 1)) // underpay
119
+ var pinned = Ord.payOutputFor(seller, PRICE)
120
+ ;(function () {
121
+ Ord.purchaseOrdLock({ spend: spend, lockingScript: lock, satoshis: ORD_SATS, payOutput: pinned })
122
+ }).should.throw(/would not be paid/)
123
+ })
124
+
125
+ it('lets the seller CANCEL the listing with their key', function () {
126
+ var lock = Ord.buildOrdLock({ seller: seller.toAddress(), price: PRICE })
127
+ var spend = fundListing(lock)
128
+ spend.addOutput(p2pkhOutput(seller, ORD_SATS)) // seller reclaims the ordinal
129
+ Ord.cancelOrdLock({ privateKey: seller, spend: spend, lockingScript: lock, satoshis: ORD_SATS })
130
+ verify(spend.inputs[0].script, lock, { tx: spend, satoshis: ORD_SATS }).ok.should.equal(true)
131
+ })
132
+
133
+ it('rejects a CANCEL signed by the wrong key', function () {
134
+ var lock = Ord.buildOrdLock({ seller: seller.toAddress(), price: PRICE })
135
+ var spend = fundListing(lock)
136
+ spend.addOutput(p2pkhOutput(buyer, ORD_SATS))
137
+ Ord.cancelOrdLock({ privateKey: buyer, spend: spend, lockingScript: lock, satoshis: ORD_SATS })
138
+ verify(spend.inputs[0].script, lock, { tx: spend, satoshis: ORD_SATS }).ok.should.equal(false)
139
+ })
140
+
141
+ it('listInscriptionOutput is a 1-sat output whose script cancels + purchases', function () {
142
+ var out = Ord.listInscriptionOutput({ seller: seller.toAddress(), price: PRICE })
143
+ out.satoshis.should.equal(1)
144
+ // Purchase path round-trips through the interpreter.
145
+ var lock = out.script
146
+ var spend = fundListing(lock)
147
+ spend.addOutput(p2pkhOutput(buyer, ORD_SATS))
148
+ spend.addOutput(Ord.payOutputFor(seller, PRICE))
149
+ Ord.purchaseOrdLock({ spend: spend, lockingScript: lock, satoshis: ORD_SATS })
150
+ verify(spend.inputs[0].script, lock, { tx: spend, satoshis: ORD_SATS }).ok.should.equal(true)
151
+ })
152
+
153
+ it('can inscribe + list in one output; the envelope is inert and parseable', function () {
154
+ var lock = Ord.buildOrdLock({
155
+ seller: seller.toAddress(),
156
+ price: PRICE,
157
+ inscription: { contentType: 'text/plain', content: 'for sale' }
158
+ })
159
+ // The inscription rides along and is recoverable.
160
+ Ord.isInscription(lock).should.equal(true)
161
+ Ord.parseInscription(lock).contentText.should.equal('for sale')
162
+ // ...and the covenant still enforces payment.
163
+ var spend = fundListing(lock)
164
+ spend.addOutput(p2pkhOutput(buyer, ORD_SATS))
165
+ spend.addOutput(Ord.payOutputFor(seller, PRICE))
166
+ Ord.purchaseOrdLock({ spend: spend, lockingScript: lock, satoshis: ORD_SATS })
167
+ verify(spend.inputs[0].script, lock, { tx: spend, satoshis: ORD_SATS }).ok.should.equal(true)
168
+ })
169
+ })
170
+
171
+ describe('Ordinals OrdLock — multi-output payments (royalties + marketplace fee)', function () {
172
+ this.timeout(20000)
173
+ var I = bsv.Script.Interpreter
174
+ var saved
175
+ before(function () {
176
+ saved = { el: I.MAX_SCRIPT_ELEMENT_SIZE, num: I.MAXIMUM_ELEMENT_SIZE, ops: I.MAX_OPS_PER_SCRIPT }
177
+ SC.enableGenesis()
178
+ })
179
+ after(function () {
180
+ I.MAX_SCRIPT_ELEMENT_SIZE = saved.el
181
+ I.MAXIMUM_ELEMENT_SIZE = saved.num
182
+ I.MAX_OPS_PER_SCRIPT = saved.ops
183
+ })
184
+
185
+ var seller = PrivateKey.fromRandom()
186
+ var royalty = PrivateKey.fromRandom()
187
+ var market = PrivateKey.fromRandom()
188
+ var buyer = PrivateKey.fromRandom()
189
+ var ROY = 5000
190
+ var FEE = 2000
191
+
192
+ function multiLock () {
193
+ return Ord.buildOrdLock({
194
+ seller: seller.toAddress(),
195
+ price: PRICE,
196
+ royalties: [ { address: royalty.toAddress(), satoshis: ROY }, { address: market.toAddress(), satoshis: FEE } ]
197
+ })
198
+ }
199
+
200
+ // The three pinned outputs, in order, that a purchase must recreate.
201
+ function pinnedOuts () {
202
+ return [ Ord.payOutputFor(seller, PRICE), Ord.payOutputFor(royalty, ROY), Ord.payOutputFor(market, FEE) ]
203
+ }
204
+
205
+ it('pays seller + royalty + marketplace fee atomically (auto-derived payoutCount)', function () {
206
+ var lock = multiLock()
207
+ var spend = help.fundAndSpend(lock, ORD_SATS, {}).spend
208
+ spend.addOutput(p2pkhOutput(buyer, ORD_SATS)) // ordinal -> buyer
209
+ pinnedOuts().forEach(function (o) { spend.addOutput(o) }) // the 3 pinned payments
210
+ spend.addOutput(p2pkhOutput(buyer, 10000)) // buyer change (trailing)
211
+ // No payoutCount passed — purchaseOrdLock reads it from the listing script.
212
+ Ord.purchaseOrdLock({ spend: spend, lockingScript: lock, satoshis: ORD_SATS })
213
+ verify(spend.inputs[0].script, lock, { tx: spend, satoshis: ORD_SATS }).ok.should.equal(true)
214
+ })
215
+
216
+ it('rejects a purchase that skips the royalty payment', function () {
217
+ var lock = multiLock()
218
+ var spend = help.fundAndSpend(lock, ORD_SATS, {}).spend
219
+ spend.addOutput(p2pkhOutput(buyer, ORD_SATS))
220
+ pinnedOuts().forEach(function (o) { spend.addOutput(o) })
221
+ Ord.purchaseOrdLock({ spend: spend, lockingScript: lock, satoshis: ORD_SATS, validate: false })
222
+ // Zero out the royalty (output index 2) after signing → covenant must reject.
223
+ spend.outputs[2] = Ord.payOutputFor(royalty, 1)
224
+ verify(spend.inputs[0].script, lock, { tx: spend, satoshis: ORD_SATS }).ok.should.equal(false)
225
+ })
226
+
227
+ it('rejects a purchase that redirects the marketplace fee', function () {
228
+ var lock = multiLock()
229
+ var spend = help.fundAndSpend(lock, ORD_SATS, {}).spend
230
+ spend.addOutput(p2pkhOutput(buyer, ORD_SATS))
231
+ pinnedOuts().forEach(function (o) { spend.addOutput(o) })
232
+ Ord.purchaseOrdLock({ spend: spend, lockingScript: lock, satoshis: ORD_SATS, validate: false })
233
+ spend.outputs[3] = Ord.payOutputFor(PrivateKey.fromRandom(), FEE) // thief keeps the fee
234
+ verify(spend.inputs[0].script, lock, { tx: spend, satoshis: ORD_SATS }).ok.should.equal(false)
235
+ })
236
+
237
+ it('purchaseOrdLock fail-fasts if a pinned payment is missing before broadcast', function () {
238
+ var lock = multiLock()
239
+ var spend = help.fundAndSpend(lock, ORD_SATS, {}).spend
240
+ spend.addOutput(p2pkhOutput(buyer, ORD_SATS))
241
+ spend.addOutput(Ord.payOutputFor(seller, PRICE))
242
+ spend.addOutput(Ord.payOutputFor(royalty, ROY))
243
+ spend.addOutput(Ord.payOutputFor(market, 1)) // underpays the fee
244
+ ;(function () {
245
+ Ord.purchaseOrdLock({ spend: spend, lockingScript: lock, satoshis: ORD_SATS })
246
+ }).should.throw(/would not be paid/)
247
+ })
248
+ })
249
+
250
+ describe('Ordinals OrdLock — parseOrdLock (self-describing listings)', function () {
251
+ var seller = PrivateKey.fromRandom()
252
+ var royalty = PrivateKey.fromRandom()
253
+
254
+ it('recovers seller, payments, total price, and inscription', function () {
255
+ var lock = Ord.buildOrdLock({
256
+ seller: seller.toAddress(),
257
+ price: PRICE,
258
+ royalties: [ { address: royalty.toAddress(), satoshis: 7500 } ],
259
+ inscription: { contentType: 'image/png', content: 'PNGDATA' }
260
+ })
261
+ var p = Ord.parseOrdLock(lock)
262
+ p.should.be.an('object')
263
+ p.seller.address.should.equal(seller.toAddress().toString())
264
+ p.payOutputs.length.should.equal(2)
265
+ p.payOutputs[0].satoshis.should.equal(PRICE)
266
+ p.payOutputs[0].address.should.equal(seller.toAddress().toString())
267
+ p.payOutputs[1].satoshis.should.equal(7500)
268
+ p.payOutputs[1].address.should.equal(royalty.toAddress().toString())
269
+ p.totalPrice.should.equal(PRICE + 7500)
270
+ p.inscription.contentType.should.equal('image/png')
271
+ p.inscription.contentText.should.equal('PNGDATA')
272
+ })
273
+
274
+ it('isOrdLock distinguishes a listing from a plain P2PKH and an inscription', function () {
275
+ var lock = Ord.buildOrdLock({ seller: seller.toAddress(), price: PRICE })
276
+ Ord.isOrdLock(lock).should.equal(true)
277
+ Ord.isOrdLock(lock.toHex()).should.equal(true) // accepts hex too
278
+ Ord.isOrdLock(bsv.Script.buildPublicKeyHashOut(seller.toAddress())).should.equal(false)
279
+ Ord.isOrdLock(Ord.buildInscription({ address: seller.toAddress(), content: 'x' })).should.equal(false)
280
+ })
281
+
282
+ it('parseOrdLock returns null for a non-listing script', function () {
283
+ var isNull = Ord.parseOrdLock(bsv.Script.buildPublicKeyHashOut(seller.toAddress())) === null
284
+ isNull.should.equal(true)
285
+ })
286
+ })
287
+
288
+ describe('Ordinals OrdLock — buildPurchaseTx (end-to-end)', function () {
289
+ this.timeout(20000)
290
+ var I = bsv.Script.Interpreter
291
+ var saved
292
+ before(function () {
293
+ saved = { el: I.MAX_SCRIPT_ELEMENT_SIZE, num: I.MAXIMUM_ELEMENT_SIZE, ops: I.MAX_OPS_PER_SCRIPT }
294
+ SC.enableGenesis()
295
+ })
296
+ after(function () {
297
+ I.MAX_SCRIPT_ELEMENT_SIZE = saved.el
298
+ I.MAXIMUM_ELEMENT_SIZE = saved.num
299
+ I.MAX_OPS_PER_SCRIPT = saved.ops
300
+ })
301
+
302
+ var seller = PrivateKey.fromRandom()
303
+ var royalty = PrivateKey.fromRandom()
304
+ var buyer = PrivateKey.fromRandom()
305
+ var fundKey = PrivateKey.fromRandom()
306
+ var fundScript = bsv.Script.buildPublicKeyHashOut(fundKey.toAddress())
307
+
308
+ function listing () {
309
+ var lock = Ord.buildOrdLock({
310
+ seller: seller.toAddress(), price: PRICE,
311
+ royalties: [ { address: royalty.toAddress(), satoshis: 5000 } ]
312
+ })
313
+ return { txid: 'a1'.repeat(32), outputIndex: 0, script: lock, satoshis: ORD_SATS }
314
+ }
315
+
316
+ function fundingCoins (amounts) {
317
+ return amounts.map(function (sats, i) {
318
+ return { txid: 'f' + i + '0'.repeat(62), outputIndex: i, script: fundScript, satoshis: sats, privateKey: fundKey }
319
+ })
320
+ }
321
+
322
+ // Verify every input of a built tx through the consensus interpreter.
323
+ function verifyAllInputs (tx, l, funding) {
324
+ var r0 = verify(tx.inputs[0].script, l.script, { tx: tx, satoshis: l.satoshis, inputIndex: 0 })
325
+ r0.ok.should.equal(true)
326
+ funding.forEach(function (f, i) {
327
+ var r = verify(tx.inputs[i + 1].script, fundScript, { tx: tx, satoshis: f.satoshis, inputIndex: i + 1 })
328
+ r.ok.should.equal(true)
329
+ })
330
+ }
331
+
332
+ it('assembles a fully-signed purchase from just the listing UTXO and buyer coins', function () {
333
+ var l = listing()
334
+ var funding = fundingCoins([80000, 60000])
335
+ var tx = Ord.buildPurchaseTx({
336
+ listing: l, ordinalDestination: buyer.toAddress(), funding: funding, fee: 500
337
+ })
338
+ // Layout: [ordinal->buyer, seller PRICE, royalty 5000, change].
339
+ tx.inputs.length.should.equal(3)
340
+ tx.outputs.length.should.equal(4)
341
+ tx.outputs[0].satoshis.should.equal(ORD_SATS)
342
+ tx.outputs[1].satoshis.should.equal(PRICE)
343
+ tx.outputs[2].satoshis.should.equal(5000)
344
+ // Value conservation: in 1+80000+60000 = 140001; out 1+100000+5000+change; fee 500.
345
+ tx.outputs[3].satoshis.should.equal(140001 - ORD_SATS - PRICE - 5000 - 500)
346
+ verifyAllInputs(tx, l, funding)
347
+ })
348
+
349
+ it('reads the required payments straight off the listing (buyer supplies no terms)', function () {
350
+ var l = listing()
351
+ var funding = fundingCoins([200000])
352
+ var tx = Ord.buildPurchaseTx({ listing: l, ordinalDestination: buyer.toAddress(), funding: funding, fee: 250 })
353
+ // The pinned payments came from parseOrdLock, and the covenant still validates.
354
+ verifyAllInputs(tx, l, funding)
355
+ tx.outputs[1].satoshis.should.equal(PRICE) // seller
356
+ tx.outputs[2].satoshis.should.equal(5000) // royalty
357
+ })
358
+
359
+ it('omits the change output when funding is exact', function () {
360
+ var l = listing()
361
+ var funding = fundingCoins([PRICE + 5000 + 500]) // exactly payments + fee
362
+ var tx = Ord.buildPurchaseTx({ listing: l, ordinalDestination: buyer.toAddress(), funding: funding, fee: 500 })
363
+ tx.outputs.length.should.equal(3) // ordinal + 2 payments, no change
364
+ verifyAllInputs(tx, l, funding)
365
+ })
366
+
367
+ it('throws on insufficient funding', function () {
368
+ var l = listing()
369
+ var funding = fundingCoins([1000]) // nowhere near PRICE + royalty + fee
370
+ ;(function () {
371
+ Ord.buildPurchaseTx({ listing: l, ordinalDestination: buyer.toAddress(), funding: funding, fee: 500 })
372
+ }).should.throw(/insufficient funding/)
373
+ })
374
+
375
+ it('the covenant still binds — tampering any output after build breaks input 0', function () {
376
+ var l = listing()
377
+ var funding = fundingCoins([200000])
378
+ var tx = Ord.buildPurchaseTx({ listing: l, ordinalDestination: buyer.toAddress(), funding: funding, fee: 500 })
379
+ tx.outputs[1] = Ord.payOutputFor(PrivateKey.fromRandom(), PRICE) // steal the seller payment
380
+ verify(tx.inputs[0].script, l.script, { tx: tx, satoshis: l.satoshis, inputIndex: 0 }).ok.should.equal(false)
381
+ })
382
+ })
383
+
384
+ describe('Ordinals OrdLock — full lifecycle (list -> buy / cancel)', function () {
385
+ this.timeout(20000)
386
+ var I = bsv.Script.Interpreter
387
+ var saved
388
+ before(function () {
389
+ saved = { el: I.MAX_SCRIPT_ELEMENT_SIZE, num: I.MAXIMUM_ELEMENT_SIZE, ops: I.MAX_OPS_PER_SCRIPT }
390
+ SC.enableGenesis()
391
+ })
392
+ after(function () {
393
+ I.MAX_SCRIPT_ELEMENT_SIZE = saved.el
394
+ I.MAXIMUM_ELEMENT_SIZE = saved.num
395
+ I.MAX_OPS_PER_SCRIPT = saved.ops
396
+ })
397
+
398
+ var owner = PrivateKey.fromRandom() // current ordinal owner / seller
399
+ var royalty = PrivateKey.fromRandom()
400
+ var buyer = PrivateKey.fromRandom()
401
+ var fundKey = PrivateKey.fromRandom()
402
+ var fundScript = bsv.Script.buildPublicKeyHashOut(fundKey.toAddress())
403
+ var ownerScript = bsv.Script.buildPublicKeyHashOut(owner.toAddress())
404
+
405
+ // The ordinal UTXO currently held by the owner under P2PKH.
406
+ function ordinalUtxo () {
407
+ return { txid: '0e'.repeat(32), outputIndex: 0, script: ownerScript, satoshis: ORD_SATS, privateKey: owner }
408
+ }
409
+ function fundCoin (sats, i) {
410
+ return { txid: 'f' + (i || 0) + '0'.repeat(62), outputIndex: 0, script: fundScript, satoshis: sats, privateKey: fundKey }
411
+ }
412
+
413
+ it('buildListingTx signs the ordinal + fee and preserves the sat into the listing', function () {
414
+ var funding = [fundCoin(20000)]
415
+ var out = Ord.buildListingTx({
416
+ ordinal: ordinalUtxo(), seller: owner.toAddress(), price: PRICE,
417
+ royalties: [{ address: royalty.toAddress(), satoshis: 4000 }],
418
+ funding: funding, fee: 500
419
+ })
420
+ // Output 0 is the listing (ordinal's sat), and it's a parseable OrdLock.
421
+ out.tx.outputs[0].satoshis.should.equal(ORD_SATS)
422
+ out.listingOutpoint.outputIndex.should.equal(0)
423
+ Ord.isOrdLock(out.tx.outputs[0].script).should.equal(true)
424
+ Ord.parseOrdLock(out.listingScript).totalPrice.should.equal(PRICE + 4000)
425
+ // Change = funding - fee.
426
+ out.tx.outputs[1].satoshis.should.equal(20000 - 500)
427
+ // Both inputs (ordinal P2PKH + funding P2PKH) verify.
428
+ verify(out.tx.inputs[0].script, ownerScript, { tx: out.tx, satoshis: ORD_SATS, inputIndex: 0 }).ok.should.equal(true)
429
+ verify(out.tx.inputs[1].script, fundScript, { tx: out.tx, satoshis: 20000, inputIndex: 1 }).ok.should.equal(true)
430
+ })
431
+
432
+ it('a listing created by buildListingTx can then be PURCHASED end-to-end', function () {
433
+ // 1) Seller lists.
434
+ var listed = Ord.buildListingTx({
435
+ ordinal: ordinalUtxo(), seller: owner.toAddress(), price: PRICE,
436
+ royalties: [{ address: royalty.toAddress(), satoshis: 4000 }],
437
+ funding: [fundCoin(20000)], fee: 500
438
+ })
439
+ // 2) Buyer purchases the freshly-created listing UTXO (terms read off its script).
440
+ var listing = {
441
+ txid: listed.listingOutpoint.txid,
442
+ outputIndex: listed.listingOutpoint.outputIndex,
443
+ script: listed.listingScript,
444
+ satoshis: ORD_SATS
445
+ }
446
+ var buyFunding = [fundCoin(200000, 1)]
447
+ var buyTx = Ord.buildPurchaseTx({
448
+ listing: listing, ordinalDestination: buyer.toAddress(), funding: buyFunding, fee: 500
449
+ })
450
+ // Ordinal -> buyer, seller + royalty paid, change back to buyer.
451
+ buyTx.outputs[0].satoshis.should.equal(ORD_SATS)
452
+ buyTx.outputs[1].satoshis.should.equal(PRICE)
453
+ buyTx.outputs[2].satoshis.should.equal(4000)
454
+ // Covenant input + funding input both verify through the interpreter.
455
+ verify(buyTx.inputs[0].script, listing.script, { tx: buyTx, satoshis: ORD_SATS, inputIndex: 0 }).ok.should.equal(true)
456
+ verify(buyTx.inputs[1].script, fundScript, { tx: buyTx, satoshis: 200000, inputIndex: 1 }).ok.should.equal(true)
457
+ })
458
+
459
+ it('a listing created by buildListingTx can instead be CANCELLED by the seller', function () {
460
+ var listed = Ord.buildListingTx({
461
+ ordinal: ordinalUtxo(), seller: owner.toAddress(), price: PRICE,
462
+ funding: [fundCoin(20000)], fee: 500
463
+ })
464
+ // Seller reclaims the ordinal from the listing UTXO.
465
+ var reclaim = new Transaction()
466
+ reclaim.addInput(new Transaction.Input({
467
+ prevTxId: listed.listingOutpoint.txid, outputIndex: 0, script: Script.empty()
468
+ }), listed.listingScript, ORD_SATS)
469
+ reclaim.addOutput(p2pkhOutput(owner, ORD_SATS))
470
+ Ord.cancelOrdLock({ privateKey: owner, spend: reclaim, lockingScript: listed.listingScript, satoshis: ORD_SATS })
471
+ verify(reclaim.inputs[0].script, listed.listingScript, { tx: reclaim, satoshis: ORD_SATS }).ok.should.equal(true)
472
+ })
473
+
474
+ it('buildListingTx throws when funding cannot cover the fee', function () {
475
+ ;(function () {
476
+ Ord.buildListingTx({
477
+ ordinal: ordinalUtxo(), seller: owner.toAddress(), price: PRICE,
478
+ funding: [fundCoin(300)], fee: 500
479
+ })
480
+ }).should.throw(/insufficient funding/)
481
+ })
482
+ })
483
+
484
+ describe('Ordinals OrdLock — code-review regressions', function () {
485
+ this.timeout(20000)
486
+ var I = bsv.Script.Interpreter
487
+ var saved
488
+ before(function () {
489
+ saved = { el: I.MAX_SCRIPT_ELEMENT_SIZE, num: I.MAXIMUM_ELEMENT_SIZE, ops: I.MAX_OPS_PER_SCRIPT }
490
+ SC.enableGenesis()
491
+ })
492
+ after(function () {
493
+ I.MAX_SCRIPT_ELEMENT_SIZE = saved.el
494
+ I.MAXIMUM_ELEMENT_SIZE = saved.num
495
+ I.MAX_OPS_PER_SCRIPT = saved.ops
496
+ })
497
+
498
+ var owner = PrivateKey.fromRandom()
499
+ var fundKey = PrivateKey.fromRandom()
500
+ var ownerScript = bsv.Script.buildPublicKeyHashOut(owner.toAddress())
501
+ var fundScript = bsv.Script.buildPublicKeyHashOut(fundKey.toAddress())
502
+
503
+ // Finding #1: buildListingTx must sign the ordinal input with the resolved value even
504
+ // when the caller omits ordinal.satoshis (documented to default to 1).
505
+ it('buildListingTx signs a valid ordinal input when ordinal.satoshis is omitted', function () {
506
+ var out = Ord.buildListingTx({
507
+ ordinal: { txid: '0e'.repeat(32), outputIndex: 0, script: ownerScript, privateKey: owner }, // no satoshis
508
+ seller: owner.toAddress(),
509
+ price: PRICE,
510
+ funding: [{ txid: 'f0'.repeat(32), outputIndex: 0, script: fundScript, satoshis: 20000, privateKey: fundKey }],
511
+ fee: 500
512
+ })
513
+ verify(out.tx.inputs[0].script, ownerScript, { tx: out.tx, satoshis: 1, inputIndex: 0 }).ok.should.equal(true)
514
+ })
515
+
516
+ // Finding #2: parsed addresses honor a network option instead of always being livenet.
517
+ it('parseOrdLock formats addresses for the requested network', function () {
518
+ var lock = Ord.buildOrdLock({ seller: owner.toAddress(), price: PRICE })
519
+ var live = Ord.parseOrdLock(lock).seller.address
520
+ var test = Ord.parseOrdLock(lock, { network: 'testnet' }).seller.address
521
+ live.should.not.equal(test)
522
+ bsv.Address.fromString(test).network.name.should.equal('testnet')
523
+ bsv.Address.fromString(live).network.name.should.equal('livenet')
524
+ })
525
+
526
+ // Finding #3: a funding coin missing satoshis fails fast (was a silent wrong-amount signature).
527
+ it('buildPurchaseTx throws when a funding coin has no satoshis', function () {
528
+ var lock = Ord.buildOrdLock({ seller: owner.toAddress(), price: PRICE })
529
+ ;(function () {
530
+ Ord.buildPurchaseTx({
531
+ listing: { txid: 'a1'.repeat(32), outputIndex: 0, script: lock, satoshis: 1 },
532
+ ordinalDestination: fundKey.toAddress(),
533
+ funding: [{ txid: 'b1'.repeat(32), outputIndex: 0, script: fundScript, privateKey: fundKey }], // no satoshis
534
+ fee: 500
535
+ })
536
+ }).should.throw(/satoshis/)
537
+ })
538
+
539
+ // Finding #5: payOutputFor never silently ignores price when handed a full Output.
540
+ it('payOutputFor rejects a Transaction.Output (price would be ignored)', function () {
541
+ var o = new bsv.Transaction.Output({ script: ownerScript, satoshis: 5 })
542
+ ;(function () { Ord.payOutputFor(o, PRICE) }).should.throw()
543
+ })
544
+
545
+ it('buildOrdLock still accepts a pre-built Output as payTo (uses its own value)', function () {
546
+ var lock = Ord.buildOrdLock({
547
+ seller: owner.toAddress(), price: 1,
548
+ payTo: new bsv.Transaction.Output({ script: ownerScript, satoshis: 777 })
549
+ })
550
+ Ord.parseOrdLock(lock).totalPrice.should.equal(777)
551
+ })
552
+ })
@@ -0,0 +1,61 @@
1
+ 'use strict'
2
+
3
+ /* global describe, it */
4
+
5
+ // Regression for the inverted OP_SPLIT/OP_DROP idiom in CovenantBuilder.extractField.
6
+ // After OP_SPLIT the RIGHT part is on top, so keeping it needs OP_NIP; the code used
7
+ // OP_DROP (which keeps the LEFT), so RIGHT-strategy fields (value, nSequence,
8
+ // hashOutputs, nLocktime, sighashType) extracted the WRONG bytes — a covenant that
9
+ // either fails to constrain the spend or is unspendable. These tests drive the real
10
+ // builder through the interpreter and assert the extracted bytes are correct.
11
+
12
+ require('chai').should()
13
+ var bsv = require('../..')
14
+ var Interpreter = bsv.Script.Interpreter
15
+ var Script = bsv.Script
16
+ var Opcode = bsv.Opcode
17
+ var Transaction = bsv.Transaction
18
+ var CovenantBuilder = require('../../lib/smart_contract/covenant_builder').CovenantBuilder
19
+
20
+ // Synthetic preimage: 120 bytes of filler + a 52-byte BIP-143 right zone whose
21
+ // fields are individually recognizable.
22
+ var FILLER = Buffer.alloc(120, 0x11)
23
+ var VALUE = Buffer.alloc(8, 0xaa)
24
+ var NSEQ = Buffer.alloc(4, 0xbb)
25
+ var HASHOUT = Buffer.alloc(32, 0xcc)
26
+ var NLOCK = Buffer.alloc(4, 0xdd)
27
+ var SIGHASH = Buffer.alloc(4, 0xee)
28
+ var PREIMAGE = Buffer.concat([FILLER, VALUE, NSEQ, HASHOUT, NLOCK, SIGHASH])
29
+
30
+ // Run `<PREIMAGE> <extractField(name) ops> <expected> OP_EQUAL` through the
31
+ // interpreter (fromASM avoids the separately-tracked _asmToHex issue).
32
+ function extractYields (fieldName, expected) {
33
+ var b = new CovenantBuilder()
34
+ b.extractField(fieldName)
35
+ var extractScript = Script.fromASM(b.operations.join(' '))
36
+ var lock = new Script().add(PREIMAGE)
37
+ extractScript.chunks.forEach(function (c) { lock.chunks.push(c) })
38
+ lock.add(expected).add(Opcode.OP_EQUAL)
39
+ var interp = new Interpreter()
40
+ return interp.verify(new Script(), lock, new Transaction(), 0,
41
+ Interpreter.SCRIPT_ENABLE_MAGNETIC_OPCODES | Interpreter.SCRIPT_ENABLE_MONOLITH_OPCODES)
42
+ }
43
+
44
+ describe('CovenantBuilder.extractField (OP_SPLIT/OP_NIP regression)', function () {
45
+ it('extracts RIGHT field "value" (offsetFromEnd 52, len 8)', function () {
46
+ extractYields('value', VALUE).should.equal(true)
47
+ })
48
+ it('extracts RIGHT field "hashOutputs" (offsetFromEnd 40, len 32)', function () {
49
+ extractYields('hashOutputs', HASHOUT).should.equal(true)
50
+ })
51
+ it('extracts RIGHT field "nSequence" (offsetFromEnd 44, len 4)', function () {
52
+ extractYields('nSequence', NSEQ).should.equal(true)
53
+ })
54
+ it('extracts RIGHT field "sighashType" (offsetFromEnd 4, len 4)', function () {
55
+ extractYields('sighashType', SIGHASH).should.equal(true)
56
+ })
57
+ it('extracts LEFT field "hashPrevouts" (offset 4, len 32)', function () {
58
+ // First 4 bytes are filler(nVersion); bytes 4..36 are filler too here.
59
+ extractYields('hashPrevouts', PREIMAGE.slice(4, 36)).should.equal(true)
60
+ })
61
+ })
@@ -0,0 +1,44 @@
1
+ 'use strict'
2
+
3
+ /* global describe, it */
4
+
5
+ // Phase 1 consolidation guard: the two NON-ENFORCING covenant builders
6
+ // (SmartContract.Covenant and SmartContract.Builder) reduce to plain P2PK and
7
+ // bind nothing about the spend. They must fail loud rather than silently hand back
8
+ // a fake covenant — callers are redirected to the enforcing OP_PUSH_TX API
9
+ // (policy() / PushTx / Token). Explicit { allowNonEnforcing: true } opts in.
10
+
11
+ require('chai').should()
12
+ var bsv = require('../..')
13
+ var SC = bsv.SmartContract
14
+ var PrivateKey = bsv.PrivateKey
15
+
16
+ describe('Non-enforcing covenant builders fail loud (Phase 1)', function () {
17
+ var key = PrivateKey.fromRandom()
18
+
19
+ it('SmartContract.Covenant.createFromP2PKH throws by default', function () {
20
+ ;(function () {
21
+ new SC.Covenant(key).createFromP2PKH({ satoshis: 1000, script: '00' })
22
+ }).should.throw(/NON-ENFORCING|allowNonEnforcing/)
23
+ })
24
+
25
+ it('SmartContract.Builder.buildLockingScript throws by default but works with allowNonEnforcing', function () {
26
+ ;(function () {
27
+ new SC.Builder(key).buildLockingScript({})
28
+ }).should.throw(/NON-ENFORCING|allowNonEnforcing/)
29
+
30
+ var script = new SC.Builder(key, { allowNonEnforcing: true }).buildLockingScript({})
31
+ script.toBuffer().length.should.be.greaterThan(0)
32
+ })
33
+
34
+ it('SmartContract.Builder.createCovenant throws by default', function () {
35
+ ;(function () {
36
+ new SC.Builder(key).createCovenant({ satoshis: 1000, script: '00' })
37
+ }).should.throw(/NON-ENFORCING|allowNonEnforcing/)
38
+ })
39
+
40
+ it('the enforcing path (ordinalLock / valueCovenant) still produces scripts', function () {
41
+ SC.ordinalLock(bsv.SmartContract.Token.ownerId(key)).toBuffer().length.should.be.greaterThan(0)
42
+ SC.valueCovenant(Buffer.alloc(32)).toBuffer().length.should.be.greaterThan(0)
43
+ })
44
+ })