@smartledger/bsv 7.2.0 → 7.4.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.
- package/CHANGELOG.md +136 -0
- package/README.md +38 -38
- package/bsv-gdaf.min.js +44 -44
- package/bsv-ltp.min.js +44 -44
- package/bsv-smartcontract.min.js +1 -1
- package/bsv.bundle.js +44 -44
- package/bsv.d.ts +19 -4
- package/bsv.min.js +44 -44
- 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/lib/ordinals/README.md +48 -5
- package/lib/ordinals/bsv20.js +215 -15
- package/lib/ordinals/ordlock.js +27 -1
- package/package.json +1 -1
- package/test/ordinals/bsv20.js +213 -2
- package/test/ordinals/ordlock.js +71 -0
- package/version.js +1 -1
package/lib/ordinals/bsv20.js
CHANGED
|
@@ -21,19 +21,41 @@ var inscription = require('./inscription')
|
|
|
21
21
|
var CONTENT_TYPE = 'application/bsv-20'
|
|
22
22
|
var MAX_DEC = 18
|
|
23
23
|
|
|
24
|
+
// "Amounts in BSV-21: strings representing uint64" — amt/max/lim are bounded by 2^64-1.
|
|
25
|
+
// A larger value is emitted happily by JSON but rejected by indexers, burning the tokens.
|
|
26
|
+
var MAX_UINT64 = '18446744073709551615'
|
|
27
|
+
|
|
24
28
|
/** A non-negative integer string (no sign, no decimal point). */
|
|
25
29
|
function isIntString (v) { return typeof v === 'string' && /^\d+$/.test(v) }
|
|
26
30
|
|
|
31
|
+
/** True if the CANONICAL decimal string `s` is greater than 2^64-1. */
|
|
32
|
+
function exceedsUint64 (s) {
|
|
33
|
+
return s.length > MAX_UINT64.length || (s.length === MAX_UINT64.length && s > MAX_UINT64)
|
|
34
|
+
}
|
|
35
|
+
|
|
27
36
|
/** Normalize an amount-like field to a CANONICAL non-negative integer string (no leading zeros). */
|
|
28
37
|
function normInt (v, name) {
|
|
38
|
+
var s
|
|
29
39
|
if (typeof v === 'number') {
|
|
30
40
|
if (!Number.isInteger(v) || v < 0) throw new Error(name + ' must be a non-negative integer')
|
|
31
|
-
|
|
41
|
+
// Beyond 2^53 a JS number is already an approximation, so String(v) would silently
|
|
42
|
+
// emit a *different* amount than the caller believes they passed.
|
|
43
|
+
if (v > Number.MAX_SAFE_INTEGER) {
|
|
44
|
+
throw new Error(name + ' exceeds Number.MAX_SAFE_INTEGER and would lose precision — ' +
|
|
45
|
+
'pass it as a string')
|
|
46
|
+
}
|
|
47
|
+
s = String(v)
|
|
48
|
+
} else if (isIntString(v)) {
|
|
49
|
+
// Strip leading zeros ("007" -> "7", "000" -> "0") so the emitted payload is canonical
|
|
50
|
+
// and not rejected by indexers that expect canonical decimal integers.
|
|
51
|
+
s = v.replace(/^0+(?=\d)/, '')
|
|
52
|
+
} else {
|
|
53
|
+
throw new Error(name + ' must be a non-negative integer (string or number)')
|
|
32
54
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
55
|
+
if (exceedsUint64(s)) {
|
|
56
|
+
throw new Error(name + ' exceeds the uint64 maximum (' + MAX_UINT64 + ')')
|
|
57
|
+
}
|
|
58
|
+
return s
|
|
37
59
|
}
|
|
38
60
|
|
|
39
61
|
/** Normalize a strictly-positive amount (mint/transfer/supply must move > 0). */
|
|
@@ -57,14 +79,39 @@ function normDec (dec) {
|
|
|
57
79
|
return String(d)
|
|
58
80
|
}
|
|
59
81
|
|
|
82
|
+
var OUTPOINT_RE = /^[0-9a-fA-F]{64}_\d+$/
|
|
83
|
+
|
|
60
84
|
/** Validate a BSV-21 token id: `<64-hex-txid>_<vout>`. */
|
|
61
85
|
function assertId (id) {
|
|
62
|
-
if (typeof id !== 'string' ||
|
|
86
|
+
if (typeof id !== 'string' || !OUTPOINT_RE.test(id)) {
|
|
63
87
|
throw new Error('id must be "<txid>_<vout>" (64-hex txid, underscore, output index)')
|
|
64
88
|
}
|
|
65
89
|
return id
|
|
66
90
|
}
|
|
67
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Validate a free-text field (`sym`). Rejects non-strings rather than running them through
|
|
94
|
+
* `String()`, which wrote the literal text `[object Object]` into a permanent payload.
|
|
95
|
+
*/
|
|
96
|
+
function assertText (v, name) {
|
|
97
|
+
if (typeof v !== 'string') {
|
|
98
|
+
var t = v === null ? 'null' : Array.isArray(v) ? 'an array' : (typeof v === 'object' ? 'an ' : 'a ') + typeof v
|
|
99
|
+
throw new Error(name + ' must be a string, got ' + t)
|
|
100
|
+
}
|
|
101
|
+
if (!v.length) throw new Error(name + ' must not be empty')
|
|
102
|
+
return v
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Validate `icon`, which the spec defines as an outpoint reference (`<txid>_<vout>`). */
|
|
106
|
+
function assertIcon (v) {
|
|
107
|
+
assertText(v, 'icon')
|
|
108
|
+
if (!OUTPOINT_RE.test(v)) {
|
|
109
|
+
throw new Error('icon must be an outpoint reference "<txid>_<vout>" (64-hex txid, ' +
|
|
110
|
+
'underscore, output index)')
|
|
111
|
+
}
|
|
112
|
+
return v
|
|
113
|
+
}
|
|
114
|
+
|
|
68
115
|
/** Wrap a BSV-20 JSON payload in an inscription locking script (P2PKH owner by default). */
|
|
69
116
|
function buildBsv20 (payload, params) {
|
|
70
117
|
params = params || {}
|
|
@@ -84,7 +131,9 @@ function buildBsv20 (payload, params) {
|
|
|
84
131
|
function buildDeploy (params) {
|
|
85
132
|
params = params || {}
|
|
86
133
|
var p = { p: 'bsv-20', op: 'deploy', tick: assertTick(params.tick), max: normPositive(params.max, 'max') }
|
|
87
|
-
|
|
134
|
+
// `lim` is "0 or omitted = unlimited" per the spec, so 0 is a meaningful value here and
|
|
135
|
+
// must not be rejected the way a zero `max`/`amt` is.
|
|
136
|
+
if (params.lim != null) p.lim = normInt(params.lim, 'lim')
|
|
88
137
|
if (params.dec != null) p.dec = normDec(params.dec)
|
|
89
138
|
return buildBsv20(p, params)
|
|
90
139
|
}
|
|
@@ -94,9 +143,38 @@ function buildDeploy (params) {
|
|
|
94
143
|
* @param {object} params { tick, amt, address|lock, contentType? }
|
|
95
144
|
* @returns {Script}
|
|
96
145
|
*/
|
|
146
|
+
/**
|
|
147
|
+
* Name the token an operation acts on: `tick` (v1) or `id` (BSV-21), never both.
|
|
148
|
+
* Passing both used to silently drop the `tick`, which is the wrong token, not a preference.
|
|
149
|
+
*/
|
|
150
|
+
function assignToken (p, params, op) {
|
|
151
|
+
var hasTick = params.tick != null
|
|
152
|
+
var hasId = params.id != null
|
|
153
|
+
if (hasTick && hasId) {
|
|
154
|
+
throw new Error(op + ' names a token by `tick` (v1) or `id` (BSV-21), not both')
|
|
155
|
+
}
|
|
156
|
+
if (!hasTick && !hasId) {
|
|
157
|
+
throw new Error(op + ' requires `tick` (v1) or `id` (BSV-21)')
|
|
158
|
+
}
|
|
159
|
+
if (hasId) p.id = assertId(params.id)
|
|
160
|
+
else p.tick = assertTick(params.tick)
|
|
161
|
+
return p
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Reject `amt` on the operations the spec says must not carry one. */
|
|
165
|
+
function assertNoAmt (params, op) {
|
|
166
|
+
if (params.amt != null) {
|
|
167
|
+
throw new Error(op + ' must not carry an amt — the specification forbids it, and a ' +
|
|
168
|
+
'payload that does is discarded')
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
97
172
|
function buildMint (params) {
|
|
98
173
|
params = params || {}
|
|
99
|
-
|
|
174
|
+
// `{p, op, tick|id, amt}` — the token first, then the amount.
|
|
175
|
+
var p = { p: 'bsv-20', op: 'mint' }
|
|
176
|
+
assignToken(p, params, 'mint')
|
|
177
|
+
p.amt = normPositive(params.amt, 'amt')
|
|
100
178
|
return buildBsv20(p, params)
|
|
101
179
|
}
|
|
102
180
|
|
|
@@ -108,8 +186,63 @@ function buildMint (params) {
|
|
|
108
186
|
function buildTransfer (params) {
|
|
109
187
|
params = params || {}
|
|
110
188
|
var p = { p: 'bsv-20', op: 'transfer', amt: normPositive(params.amt, 'amt') }
|
|
111
|
-
|
|
112
|
-
|
|
189
|
+
assignToken(p, params, 'transfer')
|
|
190
|
+
return buildBsv20(p, params)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Burn an amount of a BSV-21 token: `{p, op:'burn', id, amt}`.
|
|
195
|
+
*
|
|
196
|
+
* BSV-21 (id-based) only — the specification defines `burn` under BSV-21, so there is no
|
|
197
|
+
* ticker form. To retire v1 supply, transfer it to an unspendable output instead.
|
|
198
|
+
*
|
|
199
|
+
* @param {object} params { id, amt, address|lock, contentType? }
|
|
200
|
+
* @returns {Script}
|
|
201
|
+
*/
|
|
202
|
+
function buildBurn (params) {
|
|
203
|
+
params = params || {}
|
|
204
|
+
if (params.tick != null) {
|
|
205
|
+
throw new Error('burn is a BSV-21 operation and names its token by `id` (<txid>_<vout>), ' +
|
|
206
|
+
'not `tick`')
|
|
207
|
+
}
|
|
208
|
+
var p = { p: 'bsv-20', op: 'burn', id: assertId(params.id), amt: normPositive(params.amt, 'amt') }
|
|
209
|
+
return buildBsv20(p, params)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Deploy a BSV-21 token under an AUTHORITY rather than a fixed supply:
|
|
214
|
+
* `{p, op:'deploy+auth', sym?, dec?, icon?}`.
|
|
215
|
+
*
|
|
216
|
+
* Unlike `deploy+mint`, no supply is created here — minting happens later via `buildMint`
|
|
217
|
+
* against this deploy's outpoint, and requires an auth input. The spec is explicit that
|
|
218
|
+
* `amt` must NOT be present.
|
|
219
|
+
*
|
|
220
|
+
* @param {object} params { sym?, dec?, icon?, address|lock, contentType? }
|
|
221
|
+
* @returns {Script}
|
|
222
|
+
*/
|
|
223
|
+
function buildDeployAuth (params) {
|
|
224
|
+
params = params || {}
|
|
225
|
+
assertNoAmt(params, 'deploy+auth')
|
|
226
|
+
var p = { p: 'bsv-20', op: 'deploy+auth' }
|
|
227
|
+
if (params.dec != null) p.dec = normDec(params.dec)
|
|
228
|
+
if (params.sym != null) p.sym = assertText(params.sym, 'sym')
|
|
229
|
+
if (params.icon != null) p.icon = assertIcon(params.icon)
|
|
230
|
+
return buildBsv20(p, params)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Mint authority for a BSV-21 token: `{p, op:'auth', id}`.
|
|
235
|
+
*
|
|
236
|
+
* The output this locks carries the right to mint the token, and "can be split, combined,
|
|
237
|
+
* or transferred to delegate minting authority". It carries no amount — `amt` is forbidden.
|
|
238
|
+
*
|
|
239
|
+
* @param {object} params { id, address|lock, contentType? }
|
|
240
|
+
* @returns {Script}
|
|
241
|
+
*/
|
|
242
|
+
function buildAuth (params) {
|
|
243
|
+
params = params || {}
|
|
244
|
+
assertNoAmt(params, 'auth')
|
|
245
|
+
var p = { p: 'bsv-20', op: 'auth', id: assertId(params.id) }
|
|
113
246
|
return buildBsv20(p, params)
|
|
114
247
|
}
|
|
115
248
|
|
|
@@ -122,8 +255,8 @@ function buildDeployMint (params) {
|
|
|
122
255
|
params = params || {}
|
|
123
256
|
var p = { p: 'bsv-20', op: 'deploy+mint', amt: normPositive(params.amt, 'amt') }
|
|
124
257
|
if (params.dec != null) p.dec = normDec(params.dec)
|
|
125
|
-
if (params.sym != null) p.sym =
|
|
126
|
-
if (params.icon != null) p.icon =
|
|
258
|
+
if (params.sym != null) p.sym = assertText(params.sym, 'sym')
|
|
259
|
+
if (params.icon != null) p.icon = assertIcon(params.icon)
|
|
127
260
|
return buildBsv20(p, params)
|
|
128
261
|
}
|
|
129
262
|
|
|
@@ -137,6 +270,9 @@ function createDeployOutput (params) { return outputFor(buildDeploy(params), par
|
|
|
137
270
|
function createMintOutput (params) { return outputFor(buildMint(params), params && params.satoshis) }
|
|
138
271
|
function createTransferOutput (params) { return outputFor(buildTransfer(params), params && params.satoshis) }
|
|
139
272
|
function createDeployMintOutput (params) { return outputFor(buildDeployMint(params), params && params.satoshis) }
|
|
273
|
+
function createBurnOutput (params) { return outputFor(buildBurn(params), params && params.satoshis) }
|
|
274
|
+
function createDeployAuthOutput (params) { return outputFor(buildDeployAuth(params), params && params.satoshis) }
|
|
275
|
+
function createAuthOutput (params) { return outputFor(buildAuth(params), params && params.satoshis) }
|
|
140
276
|
|
|
141
277
|
/** Extract the JSON body from a locking script, a JSON string, a Buffer, or an object. */
|
|
142
278
|
function bodyOf (input) {
|
|
@@ -149,10 +285,69 @@ function bodyOf (input) {
|
|
|
149
285
|
return insc ? insc.contentText : null
|
|
150
286
|
}
|
|
151
287
|
|
|
288
|
+
/**
|
|
289
|
+
* Required-field rules per operation, from the BSV-20 / BSV-21 specification.
|
|
290
|
+
*
|
|
291
|
+
* `tickOrId` means the operation must carry exactly one of `tick` (v1) or `id` (v2/BSV-21).
|
|
292
|
+
* `noAmt` marks the operations the spec says must NOT carry an `amt` at all.
|
|
293
|
+
*/
|
|
294
|
+
var OP_RULES = {
|
|
295
|
+
deploy: { need: ['tick', 'max'] },
|
|
296
|
+
mint: { need: ['amt'], tickOrId: true },
|
|
297
|
+
transfer: { need: ['amt'], tickOrId: true },
|
|
298
|
+
'deploy+mint': { need: ['amt'] },
|
|
299
|
+
'deploy+auth': { need: [], noAmt: true },
|
|
300
|
+
auth: { need: ['id'], noAmt: true },
|
|
301
|
+
burn: { need: ['id', 'amt'] }
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Is `v` a uint64 amount as it may appear ON CHAIN? Leading zeros are tolerated here even
|
|
306
|
+
* though the builder emits canonical values: this reads other people's payloads, and a
|
|
307
|
+
* non-canonical amount is still an amount.
|
|
308
|
+
*/
|
|
309
|
+
function isParsedAmount (v) {
|
|
310
|
+
var s = typeof v === 'number' ? (Number.isInteger(v) && v >= 0 ? String(v) : null) : (isIntString(v) ? v : null)
|
|
311
|
+
if (s == null) return false
|
|
312
|
+
return !exceedsUint64(s.replace(/^0+(?=\d)/, ''))
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Validate a parsed payload against the spec's per-operation field rules. */
|
|
316
|
+
function isValidPayload (obj) {
|
|
317
|
+
if (!obj || obj.p !== 'bsv-20' || typeof obj.op !== 'string') return false
|
|
318
|
+
var rule = OP_RULES[obj.op]
|
|
319
|
+
if (!rule) return false // unknown operation: we cannot vouch for it
|
|
320
|
+
for (var i = 0; i < rule.need.length; i++) {
|
|
321
|
+
if (obj[rule.need[i]] == null) return false
|
|
322
|
+
}
|
|
323
|
+
if (rule.tickOrId && (obj.tick == null) === (obj.id == null)) return false
|
|
324
|
+
if (rule.noAmt && obj.amt != null) return false
|
|
325
|
+
if (obj.tick != null) {
|
|
326
|
+
if (typeof obj.tick !== 'string' || !obj.tick.length || Buffer.byteLength(obj.tick, 'utf8') > 4) return false
|
|
327
|
+
}
|
|
328
|
+
if (obj.id != null && (typeof obj.id !== 'string' || !OUTPOINT_RE.test(obj.id))) return false
|
|
329
|
+
var amounts = ['amt', 'max', 'lim']
|
|
330
|
+
for (var j = 0; j < amounts.length; j++) {
|
|
331
|
+
if (obj[amounts[j]] != null && !isParsedAmount(obj[amounts[j]])) return false
|
|
332
|
+
}
|
|
333
|
+
if (obj.dec != null) {
|
|
334
|
+
var d = typeof obj.dec === 'string' ? Number(obj.dec) : obj.dec
|
|
335
|
+
if (!Number.isInteger(d) || d < 0 || d > MAX_DEC) return false
|
|
336
|
+
}
|
|
337
|
+
return true
|
|
338
|
+
}
|
|
339
|
+
|
|
152
340
|
/**
|
|
153
341
|
* Parse a BSV-20 payload from a locking script (Script/Buffer/hex), a JSON string, or an
|
|
154
342
|
* already-parsed object. Returns the payload object (with `p:'bsv-20'`) or null if the
|
|
155
343
|
* input carries no valid BSV-20 inscription.
|
|
344
|
+
*
|
|
345
|
+
* "Valid" is enforced, not assumed: the operation must be one the spec defines and must
|
|
346
|
+
* carry the fields that operation requires, with `tick`/`id`/amount/`dec` well-formed.
|
|
347
|
+
* A payload this returns is one an indexer will act on; previously any JSON object with
|
|
348
|
+
* `p: 'bsv-20'` and a string `op` was returned, including `{p:'bsv-20', op:'transfer'}`
|
|
349
|
+
* with no amount and no token — which an indexer discards.
|
|
350
|
+
*
|
|
156
351
|
* @returns {null|{ p:'bsv-20', op:string, tick?:string, id?:string, amt?:string, max?:string, lim?:string, dec?:string, sym?:string, icon?:string }}
|
|
157
352
|
*/
|
|
158
353
|
function parseBsv20 (input) {
|
|
@@ -160,14 +355,13 @@ function parseBsv20 (input) {
|
|
|
160
355
|
var body = bodyOf(input)
|
|
161
356
|
if (body == null) return null
|
|
162
357
|
var obj = (typeof body === 'object') ? body : JSON.parse(body)
|
|
163
|
-
|
|
164
|
-
return obj
|
|
358
|
+
return isValidPayload(obj) ? obj : null
|
|
165
359
|
} catch (e) {
|
|
166
360
|
return null
|
|
167
361
|
}
|
|
168
362
|
}
|
|
169
363
|
|
|
170
|
-
/** True if the input carries a valid BSV-20 inscription. */
|
|
364
|
+
/** True if the input carries a valid BSV-20 inscription (see parseBsv20 for what that means). */
|
|
171
365
|
function isBsv20 (input) { return parseBsv20(input) !== null }
|
|
172
366
|
|
|
173
367
|
module.exports = {
|
|
@@ -176,10 +370,16 @@ module.exports = {
|
|
|
176
370
|
buildMint: buildMint,
|
|
177
371
|
buildTransfer: buildTransfer,
|
|
178
372
|
buildDeployMint: buildDeployMint,
|
|
373
|
+
buildBurn: buildBurn,
|
|
374
|
+
buildDeployAuth: buildDeployAuth,
|
|
375
|
+
buildAuth: buildAuth,
|
|
179
376
|
createDeployOutput: createDeployOutput,
|
|
180
377
|
createMintOutput: createMintOutput,
|
|
181
378
|
createTransferOutput: createTransferOutput,
|
|
182
379
|
createDeployMintOutput: createDeployMintOutput,
|
|
380
|
+
createBurnOutput: createBurnOutput,
|
|
381
|
+
createDeployAuthOutput: createDeployAuthOutput,
|
|
382
|
+
createAuthOutput: createAuthOutput,
|
|
183
383
|
parseBsv20: parseBsv20,
|
|
184
384
|
isBsv20: isBsv20
|
|
185
385
|
}
|
package/lib/ordinals/ordlock.js
CHANGED
|
@@ -205,7 +205,19 @@ function splitPayBlob (blob) {
|
|
|
205
205
|
|
|
206
206
|
/**
|
|
207
207
|
* Parse an OrdLock listing script into its economic terms. Returns null if `script`
|
|
208
|
-
* is not
|
|
208
|
+
* is not an OrdLock (so it doubles as a detector).
|
|
209
|
+
*
|
|
210
|
+
* The terms are recovered from the script's shape and then **verified by reconstruction**:
|
|
211
|
+
* the listing is rebuilt from the recovered seller / payment outputs / inscription and must
|
|
212
|
+
* match the input byte-for-byte. A script that merely wears the same arrangement of opcodes
|
|
213
|
+
* — without the OP_PUSH_TX covenant that actually binds the payment into `hashOutputs` —
|
|
214
|
+
* is rejected, so a non-null result means the purchase branch genuinely enforces payment.
|
|
215
|
+
*
|
|
216
|
+
* Note this is *this library's* OrdLock. It is semantically the widely-deployed
|
|
217
|
+
* ordinal-lock pattern (`hash256(destOutput ‖ payOutput ‖ trailingOutputs) == hashOutputs`
|
|
218
|
+
* under SIGHASH_ALL|ANYONECANPAY), generalized to multiple payment outputs, but it is built
|
|
219
|
+
* on our OP_PUSH_TX core rather than compiled from the sCrypt contract — so the bytes differ
|
|
220
|
+
* and listings are not interchangeable with that template.
|
|
209
221
|
*
|
|
210
222
|
* @param {Script|Buffer|string} script
|
|
211
223
|
* @param {object} [opts] { network } network for the returned address strings
|
|
@@ -269,6 +281,20 @@ function parseOrdLock (script, opts) {
|
|
|
269
281
|
insc = { contentType: parsedInsc.contentType, content: parsedInsc.content, contentText: parsedInsc.contentText }
|
|
270
282
|
}
|
|
271
283
|
|
|
284
|
+
// Everything above recovers terms from the script's SHAPE. Shape alone proves nothing:
|
|
285
|
+
// a script carrying this arrangement of opcodes but no OP_PUSH_TX covenant enforces no
|
|
286
|
+
// payment at all, and reporting a seller and a price for it invents a listing that does
|
|
287
|
+
// not exist. So rebuild the listing from the recovered terms and require it to match
|
|
288
|
+
// byte-for-byte — the only way to know the purchase branch really binds the payment.
|
|
289
|
+
var rebuilt = buildOrdLock({
|
|
290
|
+
seller: sellerPKH,
|
|
291
|
+
payOutputs: splitPayBlob(payBlob),
|
|
292
|
+
inscription: parsedInsc
|
|
293
|
+
? { contentType: parsedInsc.contentType, content: parsedInsc.content }
|
|
294
|
+
: undefined
|
|
295
|
+
})
|
|
296
|
+
if (rebuilt.toHex() !== s.toHex()) return null
|
|
297
|
+
|
|
272
298
|
return {
|
|
273
299
|
seller: { pubKeyHash: sellerPKH, address: bsv.Address.fromPublicKeyHash(sellerPKH, network).toString() },
|
|
274
300
|
payOutputs: payOutputs,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smartledger/bsv",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.4.0",
|
|
4
4
|
"description": "🚀 Complete Bitcoin SV development framework with legally-recognizable DID:web + W3C VC-JWT toolkit, Legal Token Protocol (LTP), Global Digital Attestation Framework (GDAF), StatusList2021 revocation, and 16 flexible loading options. Standards-based credentials with ES256/ES256K support, on-chain BSV anchoring, and comprehensive Bitcoin SV API. Perfect for legal tokens, verifiable credentials, DeFi, smart contracts, and secure Bitcoin applications.",
|
|
5
5
|
"author": "SmartLedger Technology <hello@smartledger.technology> (https://smartledger.technology)",
|
|
6
6
|
"homepage": "https://github.com/codenlighten/smartledger-bsv#readme",
|
package/test/ordinals/bsv20.js
CHANGED
|
@@ -56,7 +56,9 @@ describe('Ordinals BSV-20 fungible tokens', function () {
|
|
|
56
56
|
})
|
|
57
57
|
|
|
58
58
|
it('preserves integer amounts larger than 2^53 exactly (string, never a JS number)', function () {
|
|
59
|
-
|
|
59
|
+
// uint64 max: far beyond 2^53, and the largest amount the spec allows. The value this
|
|
60
|
+
// used to test (26 digits) exceeded uint64 and would have been burned by an indexer.
|
|
61
|
+
var huge = '18446744073709551615'
|
|
60
62
|
var p = B.parseBsv20(B.buildMint({ address: address, tick: 'BIG', amt: huge }))
|
|
61
63
|
p.amt.should.equal(huge)
|
|
62
64
|
})
|
|
@@ -72,7 +74,7 @@ describe('Ordinals BSV-20 fungible tokens', function () {
|
|
|
72
74
|
B.parseBsv20(B.buildMint({ address: address, tick: 'ORDI', amt: '007' })).amt.should.equal('7')
|
|
73
75
|
B.parseBsv20(B.buildDeploy({ address: address, tick: 'ORDI', max: '000100' })).max.should.equal('100')
|
|
74
76
|
// canonicalization does not corrupt a huge value
|
|
75
|
-
var huge = '
|
|
77
|
+
var huge = '18446744073709551615'
|
|
76
78
|
B.parseBsv20(B.buildMint({ address: address, tick: 'ORDI', amt: '0' + huge })).amt.should.equal(huge)
|
|
77
79
|
})
|
|
78
80
|
|
|
@@ -123,4 +125,213 @@ describe('Ordinals BSV-20 fungible tokens', function () {
|
|
|
123
125
|
;(function () { B.buildMint({ tick: 'ORDI', amt: '1' }) }).should.throw(/address or a lock/)
|
|
124
126
|
})
|
|
125
127
|
})
|
|
128
|
+
|
|
129
|
+
// Checked against the BSV-20 / BSV-21 specification at docs.1satordinals.com.
|
|
130
|
+
describe('specification conformance', function () {
|
|
131
|
+
var OUTPOINT = '3b31'.repeat(16) + '_0'
|
|
132
|
+
var UINT64_MAX = '18446744073709551615'
|
|
133
|
+
|
|
134
|
+
it('accepts lim: 0, which the spec defines as unlimited', function () {
|
|
135
|
+
// "lim | No | String | Per-mint limit; 0 or omitted = unlimited"
|
|
136
|
+
B.parseBsv20(B.buildDeploy({ address: address, tick: 'ORDI', max: '21000000', lim: 0 }))
|
|
137
|
+
.lim.should.equal('0')
|
|
138
|
+
B.parseBsv20(B.buildDeploy({ address: address, tick: 'ORDI', max: '21000000', lim: '0' }))
|
|
139
|
+
.lim.should.equal('0')
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('accepts an amount of exactly uint64 max but rejects one above it', function () {
|
|
143
|
+
// "Amounts in BSV-21: strings representing uint64" — larger values are emitted
|
|
144
|
+
// happily as JSON and then discarded by indexers, burning the tokens.
|
|
145
|
+
B.parseBsv20(B.buildDeploy({ address: address, tick: 'ORDI', max: UINT64_MAX }))
|
|
146
|
+
.max.should.equal(UINT64_MAX)
|
|
147
|
+
;(function () {
|
|
148
|
+
B.buildDeploy({ address: address, tick: 'ORDI', max: '18446744073709551616' })
|
|
149
|
+
}).should.throw(/exceeds the uint64 maximum/)
|
|
150
|
+
;(function () {
|
|
151
|
+
B.buildMint({ address: address, tick: 'ORDI', amt: '9'.repeat(26) })
|
|
152
|
+
}).should.throw(/exceeds the uint64 maximum/)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('rejects a numeric amount past 2^53, which is already imprecise', function () {
|
|
156
|
+
(function () {
|
|
157
|
+
B.buildMint({ address: address, tick: 'ORDI', amt: 1e17 })
|
|
158
|
+
}).should.throw(/MAX_SAFE_INTEGER/)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('rejects non-string sym / icon instead of stringifying them', function () {
|
|
162
|
+
(function () {
|
|
163
|
+
B.buildDeployMint({ address: address, amt: '100', sym: {} })
|
|
164
|
+
}).should.throw(/sym must be a string, got an object/)
|
|
165
|
+
;(function () {
|
|
166
|
+
B.buildDeployMint({ address: address, amt: '100', icon: 42 })
|
|
167
|
+
}).should.throw(/icon must be a string/)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('requires icon to be an outpoint reference, as the spec defines it', function () {
|
|
171
|
+
(function () {
|
|
172
|
+
B.buildDeployMint({ address: address, amt: '100', icon: 'https://example.com/i.png' })
|
|
173
|
+
}).should.throw(/outpoint reference/)
|
|
174
|
+
B.parseBsv20(B.buildDeployMint({ address: address, amt: '100', icon: OUTPOINT }))
|
|
175
|
+
.icon.should.equal(OUTPOINT)
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
// parseBsv20/isBsv20 are documented to report VALIDITY. They previously returned any
|
|
179
|
+
// JSON object carrying p:'bsv-20' and a string op, including payloads no indexer acts on.
|
|
180
|
+
describe('parseBsv20 enforces the validity it reports', function () {
|
|
181
|
+
function rejects (label, payload) {
|
|
182
|
+
it('rejects ' + label, function () {
|
|
183
|
+
var json = JSON.stringify(payload)
|
|
184
|
+
B.isBsv20(json).should.equal(false)
|
|
185
|
+
;(B.parseBsv20(json) === null).should.equal(true)
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
rejects('a transfer with no amount and no token', { p: 'bsv-20', op: 'transfer' })
|
|
189
|
+
rejects('a mint with no amount', { p: 'bsv-20', op: 'mint', tick: 'ORDI' })
|
|
190
|
+
rejects('a deploy with no max', { p: 'bsv-20', op: 'deploy', tick: 'ORDI' })
|
|
191
|
+
rejects('an operation the spec does not define', { p: 'bsv-20', op: 'not-an-op' })
|
|
192
|
+
rejects('a transfer naming both tick and id', { p: 'bsv-20', op: 'transfer', tick: 'A', id: OUTPOINT, amt: '1' })
|
|
193
|
+
rejects('an auth carrying amt, which the spec forbids', { p: 'bsv-20', op: 'auth', id: OUTPOINT, amt: '1' })
|
|
194
|
+
rejects('a deploy+auth carrying amt, which the spec forbids', { p: 'bsv-20', op: 'deploy+auth', amt: '1' })
|
|
195
|
+
rejects('a ticker longer than 4 bytes', { p: 'bsv-20', op: 'mint', tick: 'TOOLONG', amt: '1' })
|
|
196
|
+
rejects('a malformed token id', { p: 'bsv-20', op: 'transfer', id: 'nope', amt: '1' })
|
|
197
|
+
rejects('an amount above uint64', { p: 'bsv-20', op: 'mint', tick: 'A', amt: '9'.repeat(26) })
|
|
198
|
+
rejects('dec out of range', { p: 'bsv-20', op: 'deploy', tick: 'A', max: '1', dec: '19' })
|
|
199
|
+
|
|
200
|
+
it('accepts the spec operations this library does not yet emit', function () {
|
|
201
|
+
// Reading the chain is not the same as writing it: burn/auth/deploy+auth are valid
|
|
202
|
+
// BSV-21 and must be recognised even though there is no builder for them yet.
|
|
203
|
+
B.isBsv20(JSON.stringify({ p: 'bsv-20', op: 'burn', id: OUTPOINT, amt: '5' })).should.equal(true)
|
|
204
|
+
B.isBsv20(JSON.stringify({ p: 'bsv-20', op: 'auth', id: OUTPOINT })).should.equal(true)
|
|
205
|
+
B.isBsv20(JSON.stringify({ p: 'bsv-20', op: 'deploy+auth', sym: 'STABLE' })).should.equal(true)
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('tolerates a non-canonical on-chain amount when reading', function () {
|
|
209
|
+
// Our builder emits canonical amounts, but other people's payloads are not ours
|
|
210
|
+
// to reject over leading zeros.
|
|
211
|
+
B.isBsv20(JSON.stringify({ p: 'bsv-20', op: 'mint', tick: 'A', amt: '007' })).should.equal(true)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('still round-trips everything this library builds', function () {
|
|
215
|
+
B.isBsv20(B.buildDeploy({ address: address, tick: 'ORDI', max: '21000000' })).should.equal(true)
|
|
216
|
+
B.isBsv20(B.buildMint({ address: address, tick: 'ORDI', amt: '1' })).should.equal(true)
|
|
217
|
+
B.isBsv20(B.buildTransfer({ address: address, id: OUTPOINT, amt: '1' })).should.equal(true)
|
|
218
|
+
B.isBsv20(B.buildDeployMint({ address: address, amt: '1000' })).should.equal(true)
|
|
219
|
+
B.isBsv20(B.buildBurn({ address: address, id: OUTPOINT, amt: '1' })).should.equal(true)
|
|
220
|
+
B.isBsv20(B.buildAuth({ address: address, id: OUTPOINT })).should.equal(true)
|
|
221
|
+
B.isBsv20(B.buildDeployAuth({ address: address, sym: 'STABLE' })).should.equal(true)
|
|
222
|
+
})
|
|
223
|
+
})
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
// BSV-21 authority model: deploy+auth creates no supply, an auth output carries the right
|
|
227
|
+
// to mint, and mint names its token by `id`. Previously only the fixed-supply deploy+mint
|
|
228
|
+
// path could be built, so these operations could be read but never written.
|
|
229
|
+
describe('BSV-21 authority operations', function () {
|
|
230
|
+
var OUTPOINT = '3b31'.repeat(16) + '_0'
|
|
231
|
+
|
|
232
|
+
it('mints by id, which requires an auth input on chain', function () {
|
|
233
|
+
var p = B.parseBsv20(B.buildMint({ address: address, id: OUTPOINT, amt: '1000000' }))
|
|
234
|
+
p.op.should.equal('mint')
|
|
235
|
+
p.id.should.equal(OUTPOINT)
|
|
236
|
+
p.amt.should.equal('1000000')
|
|
237
|
+
;(p.tick === undefined).should.equal(true)
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
it('leaves the v1 ticker mint byte-for-byte unchanged', function () {
|
|
241
|
+
var p = B.parseBsv20(B.buildMint({ address: address, tick: 'ORDI', amt: '1000' }))
|
|
242
|
+
JSON.stringify(p).should.equal('{"p":"bsv-20","op":"mint","tick":"ORDI","amt":"1000"}')
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('refuses to guess which token an operation means', function () {
|
|
246
|
+
// Naming both used to silently drop the tick — a different token, not a preference.
|
|
247
|
+
(function () {
|
|
248
|
+
B.buildMint({ address: address, tick: 'ORDI', id: OUTPOINT, amt: '1' })
|
|
249
|
+
}).should.throw(/not both/)
|
|
250
|
+
;(function () {
|
|
251
|
+
B.buildTransfer({ address: address, tick: 'ORDI', id: OUTPOINT, amt: '1' })
|
|
252
|
+
}).should.throw(/not both/)
|
|
253
|
+
;(function () {
|
|
254
|
+
B.buildMint({ address: address, amt: '1' })
|
|
255
|
+
}).should.throw(/requires `tick` \(v1\) or `id`/)
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('builds a burn', function () {
|
|
259
|
+
var p = B.parseBsv20(B.buildBurn({ address: address, id: OUTPOINT, amt: '5' }))
|
|
260
|
+
p.op.should.equal('burn')
|
|
261
|
+
p.id.should.equal(OUTPOINT)
|
|
262
|
+
p.amt.should.equal('5')
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('rejects a ticker burn, which the spec does not define', function () {
|
|
266
|
+
(function () {
|
|
267
|
+
B.buildBurn({ address: address, tick: 'ORDI', amt: '5' })
|
|
268
|
+
}).should.throw(/BSV-21 operation/)
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
it('builds deploy+auth with no supply', function () {
|
|
272
|
+
var p = B.parseBsv20(B.buildDeployAuth({ address: address, sym: 'STABLE', dec: 6 }))
|
|
273
|
+
p.op.should.equal('deploy+auth')
|
|
274
|
+
p.sym.should.equal('STABLE')
|
|
275
|
+
p.dec.should.equal('6')
|
|
276
|
+
;(p.amt === undefined).should.equal(true)
|
|
277
|
+
// Every field is optional.
|
|
278
|
+
B.parseBsv20(B.buildDeployAuth({ address: address })).op.should.equal('deploy+auth')
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
it('builds an auth output carrying no amount', function () {
|
|
282
|
+
var p = B.parseBsv20(B.buildAuth({ address: address, id: OUTPOINT }))
|
|
283
|
+
p.op.should.equal('auth')
|
|
284
|
+
p.id.should.equal(OUTPOINT)
|
|
285
|
+
;(p.amt === undefined).should.equal(true)
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it('refuses to attach an amt where the spec forbids one', function () {
|
|
289
|
+
(function () {
|
|
290
|
+
B.buildAuth({ address: address, id: OUTPOINT, amt: '1' })
|
|
291
|
+
}).should.throw(/must not carry an amt/)
|
|
292
|
+
;(function () {
|
|
293
|
+
B.buildDeployAuth({ address: address, sym: 'X', amt: '1' })
|
|
294
|
+
}).should.throw(/must not carry an amt/)
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
it('applies the same amount rules as the other operations', function () {
|
|
298
|
+
(function () {
|
|
299
|
+
B.buildBurn({ address: address, id: OUTPOINT, amt: '0' })
|
|
300
|
+
}).should.throw(/greater than zero/)
|
|
301
|
+
;(function () {
|
|
302
|
+
B.buildBurn({ address: address, id: OUTPOINT, amt: '9'.repeat(26) })
|
|
303
|
+
}).should.throw(/exceeds the uint64 maximum/)
|
|
304
|
+
;(function () {
|
|
305
|
+
B.buildMint({ address: address, id: 'notanid', amt: '1' })
|
|
306
|
+
}).should.throw(/<txid>/)
|
|
307
|
+
;(function () {
|
|
308
|
+
B.buildDeployAuth({ address: address, sym: {} })
|
|
309
|
+
}).should.throw(/sym must be a string/)
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
it('builds 1-sat outputs for each new operation', function () {
|
|
313
|
+
var outs = [
|
|
314
|
+
B.createBurnOutput({ address: address, id: OUTPOINT, amt: '5' }),
|
|
315
|
+
B.createAuthOutput({ address: address, id: OUTPOINT }),
|
|
316
|
+
B.createDeployAuthOutput({ address: address, sym: 'STABLE' })
|
|
317
|
+
]
|
|
318
|
+
outs.forEach(function (o) {
|
|
319
|
+
o.satoshis.should.equal(1)
|
|
320
|
+
B.isBsv20(o.script).should.equal(true)
|
|
321
|
+
})
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
it('round-trips a full authority lifecycle', function () {
|
|
325
|
+
// deploy+auth -> auth output delegating mint rights -> mint by id -> burn.
|
|
326
|
+
var deploy = B.parseBsv20(B.buildDeployAuth({ address: address, sym: 'GOLD', dec: 8 }))
|
|
327
|
+
var auth = B.parseBsv20(B.buildAuth({ address: address, id: OUTPOINT }))
|
|
328
|
+
var mint = B.parseBsv20(B.buildMint({ address: address, id: OUTPOINT, amt: '1000' }))
|
|
329
|
+
var burn = B.parseBsv20(B.buildBurn({ address: address, id: OUTPOINT, amt: '400' }))
|
|
330
|
+
deploy.op.should.equal('deploy+auth')
|
|
331
|
+
auth.op.should.equal('auth')
|
|
332
|
+
mint.amt.should.equal('1000')
|
|
333
|
+
burn.amt.should.equal('400')
|
|
334
|
+
;[deploy, auth, mint, burn].forEach(function (p) { p.p.should.equal('bsv-20') })
|
|
335
|
+
})
|
|
336
|
+
})
|
|
126
337
|
})
|