@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.
- package/CHANGELOG.md +142 -0
- package/README.md +38 -38
- package/bin/cli.js +35 -5
- package/bsv-covenant.min.js +6 -6
- package/bsv-gdaf.min.js +2 -2
- package/bsv-ltp.min.js +2 -2
- package/bsv-shamir.min.js +1 -1
- package/bsv-smartcontract.min.js +6 -6
- package/bsv-statuslist.min.js +5 -5
- package/bsv-vcjwt.min.js +1 -1
- package/bsv.bundle.js +2 -2
- package/bsv.d.ts +289 -10
- package/bsv.min.js +2 -2
- package/docs/MODULE_REFERENCE_COMPLETE.md +27 -27
- package/docs/advanced/UTXO_MANAGER_GUIDE.md +1 -1
- package/docs/getting-started/INSTALLATION.md +23 -23
- package/docs/getting-started/QUICK_START.md +7 -7
- package/docs/migration/FROM_BSV_1_5_6.md +5 -5
- package/index.js +4 -0
- package/lib/crypto/ecdsa.js +13 -0
- package/lib/crypto/shamir.js +16 -4
- package/lib/gdaf/did-resolver.js +3 -1
- package/lib/gdaf/smartledger-anchor.js +154 -12
- package/lib/ltp/anchor.js +16 -3
- package/lib/ltp/obligation.js +56 -8
- package/lib/ltp/proof.js +4 -2
- package/lib/ltp/right.js +86 -11
- package/lib/ordinals/README.md +170 -0
- package/lib/ordinals/bsv20.js +185 -0
- package/lib/ordinals/index.js +49 -0
- package/lib/ordinals/inscription.js +159 -0
- package/lib/ordinals/ordlock.js +560 -0
- package/lib/smart_contract/builder.js +27 -3
- package/lib/smart_contract/covenant.js +37 -10
- package/lib/smart_contract/covenant_builder.js +7 -5
- package/lib/smart_contract/index.js +28 -15
- package/lib/smart_contract/preimage.js +7 -7
- package/lib/smart_contract/pushtx.js +47 -7
- package/lib/smart_contract/token.js +69 -1
- package/lib/spv/headerchain.js +77 -0
- package/lib/spv/index.js +16 -0
- package/lib/spv/merkleproof.js +108 -0
- package/lib/statuslist/index.js +65 -24
- package/lib/vcjwt/index.js +6 -0
- package/package.json +1 -1
- package/test/credentials-test.js +22 -19
- package/test/crypto/shamir.js +9 -1
- package/test/crypto/shamir_rngcap.js +29 -0
- package/test/gdaf/anchor_spv.js +163 -0
- package/test/ltp/verify_failclosed.js +134 -0
- package/test/ordinals/bsv20.js +126 -0
- package/test/ordinals/inscription.js +71 -0
- package/test/ordinals/ordlock.js +552 -0
- package/test/smart_contract/extract_field.js +61 -0
- package/test/smart_contract/nonenforcing_guard.js +44 -0
- package/test/smart_contract/ordinal_transfer.js +83 -0
- package/test/smart_contract/sighash_marketplace.js +110 -0
- package/test/spv/headerchain.js +86 -0
- package/test/spv/merkleproof.js +133 -0
- 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
|
-
*
|
|
20
|
-
*
|
|
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
|