@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
package/lib/ltp/obligation.js
CHANGED
|
@@ -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
|
-
//
|
|
828
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
782
|
-
//
|
|
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
|
+
}
|