@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
@@ -3,9 +3,35 @@
3
3
  var bsv = require('../../')
4
4
  var Hash = bsv.crypto.Hash
5
5
  var ECDSA = bsv.crypto.ECDSA
6
+ var Signature = bsv.crypto.Signature
6
7
  var PrivateKey = bsv.PrivateKey
7
8
  var $ = bsv.util.preconditions
8
9
 
10
+ /**
11
+ * Resolve the AUTHORITATIVE signer public key for a token: the issuer's key,
12
+ * bound to identity (never a key named inside the token's own proof, which a
13
+ * forger controls). did:smartledger embeds the pubkey in the DID itself. Callers
14
+ * may override with options.issuerPublicKey. Returns null when unresolvable —
15
+ * callers MUST fail closed on null.
16
+ */
17
+ function _resolveIssuerPublicKey (token, options) {
18
+ options = options || {}
19
+ if (options.issuerPublicKey) {
20
+ return options.issuerPublicKey instanceof bsv.PublicKey
21
+ ? options.issuerPublicKey
22
+ : bsv.PublicKey.fromString(options.issuerPublicKey)
23
+ }
24
+ var did = token && token.issuer
25
+ if (typeof did !== 'string') return null
26
+ try {
27
+ var identifier = did.split('#')[0].split(':')[2] // did:method:<identifier>[:...]
28
+ if (!identifier) return null
29
+ return bsv.PublicKey.fromString(identifier)
30
+ } catch (e) {
31
+ return null
32
+ }
33
+ }
34
+
9
35
  /**
10
36
  * Legal Token Protocol - Obligation Creation Primitives
11
37
  *
@@ -216,7 +242,7 @@ var ObligationToken = {
216
242
  this._validateTokenStructure(token, errors)
217
243
 
218
244
  // Signature verification
219
- this._verifyTokenSignature(token, errors)
245
+ this._verifyTokenSignature(token, errors, options)
220
246
 
221
247
  // Temporal validation
222
248
  this._validateTokenTemporal(token, errors, warnings)
@@ -807,26 +833,48 @@ var ObligationToken = {
807
833
  * Verify token signature
808
834
  * @private
809
835
  */
810
- _verifyTokenSignature: function(token, errors) {
836
+ _verifyTokenSignature: function(token, errors, options) {
811
837
  try {
812
838
  if (!token.proof || !token.proof.jws) {
813
839
  errors.push('Missing signature proof')
814
840
  return
815
841
  }
816
-
842
+
817
843
  // Recreate canonical hash
818
844
  var canonicalToken = this._canonicalizeToken(token)
819
845
  var expectedHash = Hash.sha256(Buffer.from(canonicalToken))
820
-
846
+
821
847
  // Compare with stored hash if available
822
848
  if (token.tokenHash && token.tokenHash !== expectedHash.toString('hex')) {
823
849
  errors.push('Token hash mismatch')
824
850
  return
825
851
  }
826
-
827
- // TODO: Verify JWS signature with issuer's public key
828
- // This would require DID resolution to get the public key
829
-
852
+
853
+ // Verify the JWS signature against the issuer's key — the real security
854
+ // check. Fail CLOSED: unresolvable key or bad signature => error, never a
855
+ // silent pass.
856
+ var pubKey = _resolveIssuerPublicKey(token, options)
857
+ if (!pubKey) {
858
+ errors.push('Signature not verified: issuer public key could not be ' +
859
+ 'resolved from token.issuer; pass options.issuerPublicKey')
860
+ return
861
+ }
862
+
863
+ var parts = token.proof.jws.split('.')
864
+ if (parts.length !== 3 || !parts[2]) {
865
+ errors.push('Malformed JWS')
866
+ return
867
+ }
868
+ var signature = Signature.fromDER(Buffer.from(parts[2], 'base64url'))
869
+
870
+ var ecdsa = new ECDSA()
871
+ ecdsa.hashbuf = expectedHash
872
+ ecdsa.sig = signature
873
+ ecdsa.pubkey = pubKey
874
+ // .verify() returns the instance (truthy); the boolean is on .verified.
875
+ if (ecdsa.verify().verified !== true) {
876
+ errors.push('Signature verification failed')
877
+ }
830
878
  } catch (error) {
831
879
  errors.push('Signature verification failed: ' + error.message)
832
880
  }
package/lib/ltp/proof.js CHANGED
@@ -131,8 +131,10 @@ var LTPProof = {
131
131
  ecdsa.sig = signature
132
132
  ecdsa.pubkey = bsv.PublicKey.fromString(publicKey)
133
133
 
134
- var isValid = ecdsa.verify()
135
-
134
+ // ECDSA.prototype.verify() returns the instance (always truthy) and stashes
135
+ // the real result on .verified — read the boolean, never the instance.
136
+ var isValid = ecdsa.verify().verified === true
137
+
136
138
  return {
137
139
  success: true,
138
140
  verification: {
package/lib/ltp/right.js CHANGED
@@ -8,6 +8,33 @@ var PrivateKey = bsv.PrivateKey
8
8
  var $ = bsv.util.preconditions
9
9
  var _ = require('../util/_')
10
10
 
11
+ /**
12
+ * Resolve the AUTHORITATIVE signer public key for a token: the issuer's key.
13
+ *
14
+ * The key is bound to the issuer's identity, never taken from a key named inside
15
+ * the token's own proof (which a forger controls). did:smartledger embeds the
16
+ * public key in the DID itself (did:<method>:<pubkeyHex>[:...]), so the issuer DID
17
+ * IS the key. Callers may override with options.issuerPublicKey. Returns null when
18
+ * no key can be resolved — callers MUST fail closed on null.
19
+ */
20
+ function _resolveIssuerPublicKey (token, options) {
21
+ options = options || {}
22
+ if (options.issuerPublicKey) {
23
+ return options.issuerPublicKey instanceof bsv.PublicKey
24
+ ? options.issuerPublicKey
25
+ : bsv.PublicKey.fromString(options.issuerPublicKey)
26
+ }
27
+ var did = token && token.issuer
28
+ if (typeof did !== 'string') return null
29
+ try {
30
+ var identifier = did.split('#')[0].split(':')[2] // did:method:<identifier>[:...]
31
+ if (!identifier) return null
32
+ return bsv.PublicKey.fromString(identifier)
33
+ } catch (e) {
34
+ return null
35
+ }
36
+ }
37
+
11
38
  /**
12
39
  * Legal Token Protocol - Right Creation Primitives
13
40
  *
@@ -199,11 +226,11 @@ var RightToken = {
199
226
  this._validateTokenStructure(token, errors)
200
227
 
201
228
  // Signature verification
202
- this._verifyTokenSignature(token, errors)
203
-
229
+ this._verifyTokenSignature(token, errors, options)
230
+
204
231
  // Temporal validation
205
232
  this._validateTokenTemporal(token, errors, warnings)
206
-
233
+
207
234
  // Type validation
208
235
  this._validateRightType(token, errors)
209
236
 
@@ -299,7 +326,32 @@ var RightToken = {
299
326
  error: 'Token is not transferable'
300
327
  }
301
328
  }
302
-
329
+
330
+ // AUTHORIZATION: bind the signing key to the CURRENT owner. Verifying the
331
+ // token above only proves the ISSUER signed it — NOT that the caller holds
332
+ // it. Without this, prepareRightTokenTransfer(victimToken, attackerDID,
333
+ // attackerKey) would mint a "valid" transfer of a token the attacker does
334
+ // not own. The current owner is token.credentialSubject.id, whose DID embeds
335
+ // the owner's public key (did:key / did:smartledger).
336
+ var ownerDid = token.credentialSubject && token.credentialSubject.id
337
+ var ownerPubKey = _resolveIssuerPublicKey(
338
+ { issuer: ownerDid },
339
+ { issuerPublicKey: options.currentOwnerPublicKey }
340
+ )
341
+ if (!ownerPubKey) {
342
+ return {
343
+ success: false,
344
+ error: 'Cannot resolve current owner public key from token.credentialSubject.id; ' +
345
+ 'pass options.currentOwnerPublicKey'
346
+ }
347
+ }
348
+ if (currentOwnerKey.toPublicKey().toString() !== ownerPubKey.toString()) {
349
+ return {
350
+ success: false,
351
+ error: 'Unauthorized transfer: signing key does not match the current owner'
352
+ }
353
+ }
354
+
303
355
  // Create transfer record
304
356
  var transfer = {
305
357
  '@context': [
@@ -761,26 +813,49 @@ var RightToken = {
761
813
  * Verify token signature
762
814
  * @private
763
815
  */
764
- _verifyTokenSignature: function(token, errors) {
816
+ _verifyTokenSignature: function(token, errors, options) {
765
817
  try {
766
818
  if (!token.proof || !token.proof.jws) {
767
819
  errors.push('Missing signature proof')
768
820
  return
769
821
  }
770
-
822
+
771
823
  // Recreate canonical hash
772
824
  var canonicalToken = this._canonicalizeToken(token)
773
825
  var expectedHash = Hash.sha256(Buffer.from(canonicalToken))
774
-
826
+
775
827
  // Compare with stored hash if available
776
828
  if (token.tokenHash && token.tokenHash !== expectedHash.toString('hex')) {
777
829
  errors.push('Token hash mismatch')
778
830
  return
779
831
  }
780
-
781
- // TODO: Verify JWS signature with issuer's public key
782
- // This would require DID resolution to get the public key
783
-
832
+
833
+ // Verify the JWS signature against the issuer's key. This is the actual
834
+ // security check — the hash comparison above only guards integrity of a
835
+ // caller-supplied field. Fail CLOSED: an unresolvable key or a bad signature
836
+ // must produce an error, never a silent pass.
837
+ var pubKey = _resolveIssuerPublicKey(token, options)
838
+ if (!pubKey) {
839
+ errors.push('Signature not verified: issuer public key could not be ' +
840
+ 'resolved from token.issuer; pass options.issuerPublicKey')
841
+ return
842
+ }
843
+
844
+ var parts = token.proof.jws.split('.')
845
+ if (parts.length !== 3 || !parts[2]) {
846
+ errors.push('Malformed JWS')
847
+ return
848
+ }
849
+ var signature = Signature.fromDER(Buffer.from(parts[2], 'base64url'))
850
+
851
+ var ecdsa = new ECDSA()
852
+ ecdsa.hashbuf = expectedHash
853
+ ecdsa.sig = signature
854
+ ecdsa.pubkey = pubKey
855
+ // .verify() returns the instance (truthy); the boolean is on .verified.
856
+ if (ecdsa.verify().verified !== true) {
857
+ errors.push('Signature verification failed')
858
+ }
784
859
  } catch (error) {
785
860
  errors.push('Signature verification failed: ' + error.message)
786
861
  }
@@ -0,0 +1,170 @@
1
+ # `bsv.Ordinals` — 1Sat Ordinals inscriptions + trustless marketplace
2
+
3
+ Build and parse [1Sat Ordinals](https://docs.1satordinals.com/) inscriptions, and list,
4
+ buy, or cancel them behind a trustless **OrdLock** "pay the seller or cancel" covenant —
5
+ built on the audited configurable-SIGHASH `OP_PUSH_TX` core (`bsv.SmartContract.PushTx`).
6
+
7
+ Every locking/unlocking script this module emits is exercised through the consensus
8
+ interpreter in `test/ordinals/`, including adversarial spends asserted to be **rejected**.
9
+
10
+ > Covenant scripts need post-Genesis limits. Call `bsv.SmartContract.enableGenesis()`
11
+ > once before building/verifying OrdLock scripts.
12
+
13
+ ## Inscriptions
14
+
15
+ An inscription is content (a content-type + a body) carried on a 1-satoshi output behind
16
+ an inert `OP_FALSE OP_IF "ord" … OP_ENDIF` envelope, appended to a normal locking script
17
+ (P2PKH by default). The envelope never executes, so spending is controlled entirely by the
18
+ base lock while the inscription rides on the satoshi.
19
+
20
+ ```js
21
+ var bsv = require('@smartledger/bsv')
22
+ var Ord = bsv.Ordinals
23
+
24
+ // Build an inscription locking script (P2PKH owner) and the 1-sat output.
25
+ var script = Ord.buildInscription({
26
+ address: ownerAddress,
27
+ contentType: 'text/plain',
28
+ content: 'hello ordinals'
29
+ })
30
+ var output = Ord.createInscriptionOutput({ address: ownerAddress, contentType: 'image/png', content: pngBuffer })
31
+
32
+ // Parse it back.
33
+ var insc = Ord.parseInscription(script) // { contentType, content, contentText, lock }
34
+ Ord.isInscription(script) // true
35
+
36
+ // Batch many inscriptions into one transaction.
37
+ var outs = Ord.batchInscriptionOutputs([
38
+ { address: a, contentType: 'text/plain', content: 'a' },
39
+ { address: b, contentType: 'text/plain', content: 'b' }
40
+ ])
41
+ ```
42
+
43
+ ## Marketplace: the OrdLock covenant
44
+
45
+ A listing locks the 1-sat ordinal behind a script with two spend paths:
46
+
47
+ - **PURCHASE** — anyone may take the ordinal, but only by recreating the required payment
48
+ output(s) byte-for-byte. Under `SIGHASH_ALL|ANYONECANPAY` the buyer supplies the
49
+ surrounding outputs and adds funding inputs / change freely; the one thing they cannot do
50
+ is take the ordinal without paying.
51
+ - **CANCEL** — the seller reclaims the listing any time with an ECDSA signature over their key.
52
+
53
+ Payments can be **multi-output** — a seller payment plus royalty and marketplace-fee
54
+ outputs — so a purchase atomically pays every party or fails.
55
+
56
+ ### List (sell side)
57
+
58
+ ```js
59
+ bsv.SmartContract.enableGenesis()
60
+
61
+ // Move a P2PKH ordinal into a listing, fee paid from a separate funding coin.
62
+ var listed = Ord.buildListingTx({
63
+ ordinal: { txid: ordTxid, outputIndex: 0, script: ownerP2PKH, satoshis: 1, privateKey: ownerKey },
64
+ seller: ownerAddress,
65
+ price: 100000, // paid to the seller
66
+ royalties: [{ address: creatorAddress, satoshis: 5000 }], // + optional royalty / fee outputs
67
+ funding: [{ txid: feeTxid, outputIndex: 0, script: feeP2PKH, satoshis: 20000, privateKey: feeKey }],
68
+ fee: 500
69
+ })
70
+ // listed.tx -> the fully-signed listing transaction (broadcast it)
71
+ // listed.listingScript -> the OrdLock locking script
72
+ // listed.listingOutpoint = { txid, outputIndex } -> where the listing now lives
73
+ ```
74
+
75
+ Prefer the raw pieces? `Ord.buildOrdLock({ seller, price, royalties })` returns just the
76
+ locking script and `Ord.listInscriptionOutput({...})` the 1-sat output.
77
+
78
+ ### Read a listing (indexer / wallet / UI)
79
+
80
+ Listings are self-describing — recover their terms straight off-chain:
81
+
82
+ ```js
83
+ Ord.isOrdLock(script) // true for an OrdLock listing
84
+ var terms = Ord.parseOrdLock(script)
85
+ // {
86
+ // seller: { pubKeyHash, address },
87
+ // payOutputs: [ { satoshis, script, address }, ... ], // seller, royalty, fee, ...
88
+ // totalPrice: 105000,
89
+ // inscription: { contentType, content, contentText } | null
90
+ // }
91
+ ```
92
+
93
+ ### Buy (purchase side)
94
+
95
+ ```js
96
+ // The buyer needs only the listing UTXO — the required payments are read off its script.
97
+ var buyTx = Ord.buildPurchaseTx({
98
+ listing: { txid: listed.listingOutpoint.txid, outputIndex: 0, script: listed.listingScript, satoshis: 1 },
99
+ ordinalDestination: buyerAddress,
100
+ funding: [{ txid: coinTxid, outputIndex: 0, script: buyerP2PKH, satoshis: 200000, privateKey: buyerKey }],
101
+ fee: 500
102
+ })
103
+ // Output layout: [ ordinal -> buyer, <pinned payments>, change -> buyer ].
104
+ // buyTx is fully signed (covenant input + funding inputs). Broadcast it.
105
+ ```
106
+
107
+ ### Cancel
108
+
109
+ ```js
110
+ var reclaim = new bsv.Transaction()
111
+ reclaim.addInput(new bsv.Transaction.Input({
112
+ prevTxId: listed.listingOutpoint.txid, outputIndex: 0, script: bsv.Script.empty()
113
+ }), listed.listingScript, 1)
114
+ reclaim.addOutput(new bsv.Transaction.Output({ script: sellerP2PKH, satoshis: 1 }))
115
+ Ord.cancelOrdLock({ privateKey: ownerKey, spend: reclaim, lockingScript: listed.listingScript, satoshis: 1 })
116
+ ```
117
+
118
+ ## Lower-level unlock builders
119
+
120
+ `Ord.purchaseOrdLock({ spend, lockingScript })` and `Ord.cancelOrdLock({ privateKey, spend, lockingScript })`
121
+ build (and assign) the unlocking script when you are assembling the spend transaction
122
+ yourself. `purchaseOrdLock` grinds the OP_PUSH_TX signature into `nLockTime`, so call it
123
+ **before** signing any funding inputs the buyer adds.
124
+
125
+ ## BSV-20 / BSV-21 fungible tokens
126
+
127
+ Fungible tokens are a JSON payload (`application/bsv-20`) inside an inscription on a 1-sat
128
+ output. Two eras: **v1** is ticker-based (`tick`, 1–4 bytes); **v2 / BSV-21** is id-based
129
+ (`deploy+mint`, then transfer by `id` = the deploy `<txid>_<vout>`). Amounts are **integer
130
+ strings** — they routinely exceed 2^53, so they are never coerced to JS numbers.
131
+
132
+ ```js
133
+ var B = bsv.Ordinals.BSV20
134
+
135
+ // v1: deploy a ticker, mint, transfer.
136
+ B.buildDeploy({ address: owner, tick: 'ORDI', max: '21000000', lim: '1000', dec: 18 })
137
+ B.buildMint({ address: owner, tick: 'ORDI', amt: '1000' })
138
+ B.buildTransfer({ address: newOwner, tick: 'ORDI', amt: '250' })
139
+
140
+ // v2 / BSV-21: deploy+mint a supply, transfer by id.
141
+ B.buildDeployMint({ address: owner, amt: '1000000', dec: 8, sym: 'XYZ' })
142
+ B.buildTransfer({ address: newOwner, id: deployTxid + '_0', amt: '5' })
143
+
144
+ // 1-sat outputs and parsing.
145
+ var out = B.createMintOutput({ address: owner, tick: 'ORDI', amt: '1' })
146
+ B.isBsv20(out.script) // true
147
+ B.parseBsv20(script) // { p:'bsv-20', op:'mint', tick:'ORDI', amt:'1000' } | null
148
+ B.parseBsv20('{"p":"bsv-20","op":"mint","tick":"ORDI","amt":"1"}') // also accepts JSON / objects
149
+ ```
150
+
151
+ Builders validate their inputs (ticker ≤ 4 bytes, non-negative integer amounts, `dec` 0–18,
152
+ well-formed BSV-21 `id`) and throw on bad data. Balance tracking is an indexer concern —
153
+ this module builds and reads the on-chain payloads.
154
+
155
+ ## API
156
+
157
+ | Function | Purpose |
158
+ | --- | --- |
159
+ | `buildInscription`, `createInscriptionOutput`, `batchInscriptionOutputs` | build inscription scripts / outputs |
160
+ | `parseInscription`, `isInscription` | read an inscription back |
161
+ | `buildOrdLock`, `listInscriptionOutput` | build an OrdLock listing script / 1-sat output |
162
+ | `parseOrdLock`, `isOrdLock` | recover a listing's seller, payments, price, inscription |
163
+ | `buildListingTx` | assemble a signed listing tx (P2PKH ordinal → OrdLock) |
164
+ | `buildPurchaseTx` | assemble a signed purchase tx from a listing UTXO + buyer coins |
165
+ | `purchaseOrdLock`, `cancelOrdLock` | build unlock scripts for a spend you assemble |
166
+ | `payOutputFor` | build a P2PKH payment output |
167
+ | `ORDLOCK_SIGHASH` | `SIGHASH_ALL\|ANYONECANPAY\|FORKID` (0xc1) |
168
+ | `BSV20.buildDeploy` / `buildMint` / `buildTransfer` / `buildDeployMint` | build BSV-20/21 token inscriptions |
169
+ | `BSV20.create*Output` | 1-sat token outputs |
170
+ | `BSV20.parseBsv20`, `BSV20.isBsv20` | read a token payload back |
@@ -0,0 +1,185 @@
1
+ 'use strict'
2
+ /**
3
+ * BSV-20 / BSV-21 fungible-token inscriptions.
4
+ *
5
+ * A BSV-20 token is a JSON payload (content-type `application/bsv-20`) carried inside an
6
+ * ordinary 1Sat Ordinals inscription envelope on a 1-satoshi output. The token amount is
7
+ * per-UTXO; an indexer tracks balances by replaying deploy / mint / transfer operations.
8
+ * This module builds and parses those payloads — the on-chain plumbing is just an
9
+ * inscription (see ./inscription), so a token output is a normal P2PKH-owned 1-sat output.
10
+ *
11
+ * Two protocol eras are supported:
12
+ * v1 (ticker-based) deploy / mint / transfer keyed by a 1–4 byte `tick`.
13
+ * v2 / BSV-21 (id-based) `deploy+mint` a supply in one step; transfer by `id`
14
+ * (the deploy outpoint, `<txid>_<vout>`).
15
+ *
16
+ * Amounts (`amt`, `max`, `lim`) are INTEGER STRINGS — they routinely exceed 2^53, so they
17
+ * are never coerced to JS numbers. `dec` (decimals, 0–18) scales display only.
18
+ */
19
+ var inscription = require('./inscription')
20
+
21
+ var CONTENT_TYPE = 'application/bsv-20'
22
+ var MAX_DEC = 18
23
+
24
+ /** A non-negative integer string (no sign, no decimal point). */
25
+ function isIntString (v) { return typeof v === 'string' && /^\d+$/.test(v) }
26
+
27
+ /** Normalize an amount-like field to a CANONICAL non-negative integer string (no leading zeros). */
28
+ function normInt (v, name) {
29
+ if (typeof v === 'number') {
30
+ if (!Number.isInteger(v) || v < 0) throw new Error(name + ' must be a non-negative integer')
31
+ return String(v)
32
+ }
33
+ // Strip leading zeros ("007" -> "7", "000" -> "0") so the emitted payload is canonical
34
+ // and not rejected by indexers that expect canonical decimal integers.
35
+ if (isIntString(v)) return v.replace(/^0+(?=\d)/, '')
36
+ throw new Error(name + ' must be a non-negative integer (string or number)')
37
+ }
38
+
39
+ /** Normalize a strictly-positive amount (mint/transfer/supply must move > 0). */
40
+ function normPositive (v, name) {
41
+ var s = normInt(v, name)
42
+ if (!/[1-9]/.test(s)) throw new Error(name + ' must be greater than zero')
43
+ return s
44
+ }
45
+
46
+ /** Validate a v1 ticker: 1–4 UTF-8 bytes. */
47
+ function assertTick (tick) {
48
+ if (typeof tick !== 'string' || !tick.length) throw new Error('tick is required')
49
+ if (Buffer.byteLength(tick, 'utf8') > 4) throw new Error('tick must be 1–4 UTF-8 bytes')
50
+ return tick
51
+ }
52
+
53
+ /** Normalize `dec` (decimals) to a string in 0…18. */
54
+ function normDec (dec) {
55
+ var d = typeof dec === 'string' ? Number(dec) : dec
56
+ if (!Number.isInteger(d) || d < 0 || d > MAX_DEC) throw new Error('dec must be an integer 0…' + MAX_DEC)
57
+ return String(d)
58
+ }
59
+
60
+ /** Validate a BSV-21 token id: `<64-hex-txid>_<vout>`. */
61
+ function assertId (id) {
62
+ if (typeof id !== 'string' || !/^[0-9a-fA-F]{64}_\d+$/.test(id)) {
63
+ throw new Error('id must be "<txid>_<vout>" (64-hex txid, underscore, output index)')
64
+ }
65
+ return id
66
+ }
67
+
68
+ /** Wrap a BSV-20 JSON payload in an inscription locking script (P2PKH owner by default). */
69
+ function buildBsv20 (payload, params) {
70
+ params = params || {}
71
+ return inscription.buildInscription({
72
+ lock: params.lock,
73
+ address: params.address,
74
+ contentType: params.contentType || CONTENT_TYPE,
75
+ content: JSON.stringify(payload)
76
+ })
77
+ }
78
+
79
+ /**
80
+ * Deploy a v1 (ticker) token: fix its ticker, max supply, per-mint limit, and decimals.
81
+ * @param {object} params { tick, max, lim?, dec?, address|lock, contentType? }
82
+ * @returns {Script}
83
+ */
84
+ function buildDeploy (params) {
85
+ params = params || {}
86
+ var p = { p: 'bsv-20', op: 'deploy', tick: assertTick(params.tick), max: normPositive(params.max, 'max') }
87
+ if (params.lim != null) p.lim = normPositive(params.lim, 'lim')
88
+ if (params.dec != null) p.dec = normDec(params.dec)
89
+ return buildBsv20(p, params)
90
+ }
91
+
92
+ /**
93
+ * Mint an amount of a v1 (ticker) token.
94
+ * @param {object} params { tick, amt, address|lock, contentType? }
95
+ * @returns {Script}
96
+ */
97
+ function buildMint (params) {
98
+ params = params || {}
99
+ var p = { p: 'bsv-20', op: 'mint', tick: assertTick(params.tick), amt: normPositive(params.amt, 'amt') }
100
+ return buildBsv20(p, params)
101
+ }
102
+
103
+ /**
104
+ * Transfer an amount of a token. Provide `tick` (v1) OR `id` (v2 / BSV-21).
105
+ * @param {object} params { tick?|id?, amt, address|lock, contentType? }
106
+ * @returns {Script}
107
+ */
108
+ function buildTransfer (params) {
109
+ params = params || {}
110
+ var p = { p: 'bsv-20', op: 'transfer', amt: normPositive(params.amt, 'amt') }
111
+ if (params.id != null) p.id = assertId(params.id)
112
+ else p.tick = assertTick(params.tick)
113
+ return buildBsv20(p, params)
114
+ }
115
+
116
+ /**
117
+ * Deploy + mint a BSV-21 (id-based) token supply in one operation.
118
+ * @param {object} params { amt, dec?, sym?, icon?, address|lock, contentType? }
119
+ * @returns {Script}
120
+ */
121
+ function buildDeployMint (params) {
122
+ params = params || {}
123
+ var p = { p: 'bsv-20', op: 'deploy+mint', amt: normPositive(params.amt, 'amt') }
124
+ if (params.dec != null) p.dec = normDec(params.dec)
125
+ if (params.sym != null) p.sym = String(params.sym)
126
+ if (params.icon != null) p.icon = String(params.icon)
127
+ return buildBsv20(p, params)
128
+ }
129
+
130
+ function outputFor (script, satoshis) {
131
+ var bsv = require('../..')
132
+ return new bsv.Transaction.Output({ script: script, satoshis: satoshis != null ? satoshis : 1 })
133
+ }
134
+
135
+ /** 1-sat Output helpers mirroring the builders. */
136
+ function createDeployOutput (params) { return outputFor(buildDeploy(params), params && params.satoshis) }
137
+ function createMintOutput (params) { return outputFor(buildMint(params), params && params.satoshis) }
138
+ function createTransferOutput (params) { return outputFor(buildTransfer(params), params && params.satoshis) }
139
+ function createDeployMintOutput (params) { return outputFor(buildDeployMint(params), params && params.satoshis) }
140
+
141
+ /** Extract the JSON body from a locking script, a JSON string, a Buffer, or an object. */
142
+ function bodyOf (input) {
143
+ var bsv = require('../..')
144
+ if (input && typeof input === 'object' && !Buffer.isBuffer(input) && !(input instanceof bsv.Script)) {
145
+ return input // already a parsed object
146
+ }
147
+ if (typeof input === 'string' && input.trim()[0] === '{') return input // raw JSON string
148
+ var insc = inscription.parseInscription(input) // Script / Buffer / hex script
149
+ return insc ? insc.contentText : null
150
+ }
151
+
152
+ /**
153
+ * Parse a BSV-20 payload from a locking script (Script/Buffer/hex), a JSON string, or an
154
+ * already-parsed object. Returns the payload object (with `p:'bsv-20'`) or null if the
155
+ * input carries no valid BSV-20 inscription.
156
+ * @returns {null|{ p:'bsv-20', op:string, tick?:string, id?:string, amt?:string, max?:string, lim?:string, dec?:string, sym?:string, icon?:string }}
157
+ */
158
+ function parseBsv20 (input) {
159
+ try {
160
+ var body = bodyOf(input)
161
+ if (body == null) return null
162
+ var obj = (typeof body === 'object') ? body : JSON.parse(body)
163
+ if (!obj || obj.p !== 'bsv-20' || typeof obj.op !== 'string') return null
164
+ return obj
165
+ } catch (e) {
166
+ return null
167
+ }
168
+ }
169
+
170
+ /** True if the input carries a valid BSV-20 inscription. */
171
+ function isBsv20 (input) { return parseBsv20(input) !== null }
172
+
173
+ module.exports = {
174
+ CONTENT_TYPE: CONTENT_TYPE,
175
+ buildDeploy: buildDeploy,
176
+ buildMint: buildMint,
177
+ buildTransfer: buildTransfer,
178
+ buildDeployMint: buildDeployMint,
179
+ createDeployOutput: createDeployOutput,
180
+ createMintOutput: createMintOutput,
181
+ createTransferOutput: createTransferOutput,
182
+ createDeployMintOutput: createDeployMintOutput,
183
+ parseBsv20: parseBsv20,
184
+ isBsv20: isBsv20
185
+ }
@@ -0,0 +1,49 @@
1
+ 'use strict'
2
+ /**
3
+ * Ordinals — 1Sat Ordinals inscriptions for BSV.
4
+ *
5
+ * Build/parse inscription scripts, create the 1-sat inscription output, batch many
6
+ * inscriptions into one transaction, and (see ./ordlock) list an ordinal for sale
7
+ * behind an OP_PUSH_TX "pay the seller or cancel" covenant.
8
+ */
9
+ var inscription = require('./inscription')
10
+ var ordlock = require('./ordlock')
11
+ var bsv20 = require('./bsv20')
12
+
13
+ /**
14
+ * Build the 1-sat output(s) for one or more inscriptions.
15
+ * @param {Array<object>} items each: { contentType, content, address|lock, satoshis? }
16
+ * @returns {Transaction.Output[]}
17
+ */
18
+ function batchInscriptionOutputs (items) {
19
+ if (!Array.isArray(items) || !items.length) {
20
+ throw new Error('batchInscriptionOutputs requires a non-empty array of items')
21
+ }
22
+ return items.map(function (it) { return inscription.createInscriptionOutput(it) })
23
+ }
24
+
25
+ module.exports = {
26
+ buildInscription: inscription.buildInscription,
27
+ parseInscription: inscription.parseInscription,
28
+ isInscription: inscription.isInscription,
29
+ createInscriptionOutput: inscription.createInscriptionOutput,
30
+ batchInscriptionOutputs: batchInscriptionOutputs,
31
+
32
+ // Marketplace: list an ordinal for sale behind a "pay the seller or cancel" covenant.
33
+ // Payments may be multi-output (seller + royalties + marketplace fee), and a listing is
34
+ // self-describing — parseOrdLock recovers its terms and buildPurchaseTx assembles a
35
+ // complete signed purchase from just the listing UTXO and the buyer's coins.
36
+ ORDLOCK_SIGHASH: ordlock.ORDLOCK_SIGHASH,
37
+ buildOrdLock: ordlock.buildOrdLock,
38
+ parseOrdLock: ordlock.parseOrdLock,
39
+ isOrdLock: ordlock.isOrdLock,
40
+ listInscriptionOutput: ordlock.listInscriptionOutput,
41
+ payOutputFor: ordlock.payOutputFor,
42
+ purchaseOrdLock: ordlock.purchase,
43
+ cancelOrdLock: ordlock.cancel,
44
+ buildListingTx: ordlock.buildListingTx,
45
+ buildPurchaseTx: ordlock.buildPurchaseTx,
46
+
47
+ // BSV-20 / BSV-21 fungible-token inscriptions (deploy / mint / transfer + parse).
48
+ BSV20: bsv20
49
+ }