@bsv/templates 1.6.1 → 1.7.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/dist/cjs/mod.js +2 -1
- package/dist/cjs/mod.js.map +1 -1
- package/dist/cjs/package.json +2 -2
- package/dist/cjs/src/MandalaAdmin.js +69 -37
- package/dist/cjs/src/MandalaAdmin.js.map +1 -1
- package/dist/cjs/src/MandalaToken.js +8 -12
- package/dist/cjs/src/MandalaToken.js.map +1 -1
- package/dist/cjs/src/mandala-encoding.js +1 -2
- package/dist/cjs/src/mandala-encoding.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/mod.js +1 -1
- package/dist/esm/mod.js.map +1 -1
- package/dist/esm/src/MandalaAdmin.js +68 -37
- package/dist/esm/src/MandalaAdmin.js.map +1 -1
- package/dist/esm/src/MandalaToken.js +9 -12
- package/dist/esm/src/MandalaToken.js.map +1 -1
- package/dist/esm/src/mandala-encoding.js +0 -1
- package/dist/esm/src/mandala-encoding.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/mod.d.ts +2 -2
- package/dist/types/mod.d.ts.map +1 -1
- package/dist/types/src/MandalaAdmin.d.ts +22 -10
- package/dist/types/src/MandalaAdmin.d.ts.map +1 -1
- package/dist/types/src/MandalaToken.d.ts.map +1 -1
- package/dist/types/src/mandala-encoding.d.ts +0 -1
- package/dist/types/src/mandala-encoding.d.ts.map +1 -1
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/mod.ts +5 -2
- package/package.json +2 -2
- package/src/MandalaAdmin.ts +84 -46
- package/src/MandalaToken.ts +9 -11
- package/src/__tests/MandalaAdmin.derive.test.ts +7 -9
- package/src/__tests/MandalaAdmin.script.test.ts +44 -13
- package/src/__tests/MandalaAdmin.spend.test.ts +80 -0
- package/src/__tests/MandalaToken.test.ts +6 -9
- package/src/__tests/mandala-encoding.test.ts +1 -5
- package/src/mandala-encoding.ts +0 -2
package/src/MandalaToken.ts
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
} from '@bsv/sdk'
|
|
6
6
|
import {
|
|
7
7
|
createMinimallyEncodedScriptChunk, encodeScriptNum, decodeScriptNumChunk,
|
|
8
|
-
encodeAssetId, decodeAssetId
|
|
8
|
+
encodeAssetId, decodeAssetId
|
|
9
9
|
} from './mandala-encoding.js'
|
|
10
10
|
import { buildSighashPreimage } from './mandala-signing.js'
|
|
11
11
|
|
|
@@ -47,12 +47,12 @@ export class MandalaToken implements ScriptTemplate {
|
|
|
47
47
|
if (pubKeyHash.length !== 20) throw new Error('pubKeyHash must be 20 bytes')
|
|
48
48
|
if (!Number.isInteger(amount) || amount < 1) throw new Error('amount must be a positive integer')
|
|
49
49
|
const assetIdBytes = encodeAssetId(assetId)
|
|
50
|
+
// assetId + amount are pushed then dropped by a single OP_2DROP; the tail is
|
|
51
|
+
// a standard P2PKH. No identifier prefix — outputs are classified off-chain.
|
|
50
52
|
return new LockingScript([
|
|
51
|
-
createMinimallyEncodedScriptChunk([MARKER]),
|
|
52
53
|
createMinimallyEncodedScriptChunk(assetIdBytes),
|
|
53
54
|
createMinimallyEncodedScriptChunk(encodeScriptNum(amount)),
|
|
54
55
|
{ op: OP.OP_2DROP },
|
|
55
|
-
{ op: OP.OP_DROP },
|
|
56
56
|
{ op: OP.OP_DUP },
|
|
57
57
|
{ op: OP.OP_HASH160 },
|
|
58
58
|
{ op: pubKeyHash.length, data: pubKeyHash },
|
|
@@ -85,17 +85,15 @@ export class MandalaToken implements ScriptTemplate {
|
|
|
85
85
|
|
|
86
86
|
static decode (script: LockingScript): MandalaTokenDecoded {
|
|
87
87
|
const c = script.chunks
|
|
88
|
-
if (c.length !==
|
|
89
|
-
|
|
90
|
-
if (c[
|
|
91
|
-
if (c[3].op !== OP.OP_2DROP || c[4].op !== OP.OP_DROP) throw new Error('not a MandalaToken script: bad drops')
|
|
92
|
-
if (c[5].op !== OP.OP_DUP || c[6].op !== OP.OP_HASH160 || c[8].op !== OP.OP_EQUALVERIFY || c[9].op !== OP.OP_CHECKSIG) {
|
|
88
|
+
if (c.length !== 8) throw new Error('not a MandalaToken script: wrong chunk count')
|
|
89
|
+
if (c[2].op !== OP.OP_2DROP) throw new Error('not a MandalaToken script: bad drops')
|
|
90
|
+
if (c[3].op !== OP.OP_DUP || c[4].op !== OP.OP_HASH160 || c[6].op !== OP.OP_EQUALVERIFY || c[7].op !== OP.OP_CHECKSIG) {
|
|
93
91
|
throw new Error('not a MandalaToken script: bad P2PKH tail')
|
|
94
92
|
}
|
|
95
|
-
const assetId = decodeAssetId(vt(c[
|
|
96
|
-
const amount = decodeScriptNumChunk(c[
|
|
93
|
+
const assetId = decodeAssetId(vt(c[0].data))
|
|
94
|
+
const amount = decodeScriptNumChunk(c[1])
|
|
97
95
|
if (!Number.isInteger(amount) || amount < 1) throw new Error('not a MandalaToken script: bad amount')
|
|
98
|
-
const pubKeyHash = vt(c[
|
|
96
|
+
const pubKeyHash = vt(c[5].data)
|
|
99
97
|
if (pubKeyHash.length !== 20) throw new Error('not a MandalaToken script: bad pubKeyHash')
|
|
100
98
|
return { assetId, amount, pubKeyHash }
|
|
101
99
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MandalaAdmin } from '../MandalaAdmin.js'
|
|
1
|
+
import { MandalaAdmin, ADMIN_PROTOCOL } from '../MandalaAdmin.js'
|
|
2
2
|
|
|
3
3
|
describe('MandalaAdmin canonicalize/commitment', () => {
|
|
4
4
|
it('is insensitive to key ordering', () => {
|
|
@@ -18,17 +18,15 @@ describe('MandalaAdmin canonicalize/commitment', () => {
|
|
|
18
18
|
expect(c).toBe(MandalaAdmin.commitment({ assetId: 'x.0', kind: 'register' } as any))
|
|
19
19
|
})
|
|
20
20
|
|
|
21
|
-
it('
|
|
21
|
+
it('locks the output to keyID = commitment(data) with counterparty self by default', async () => {
|
|
22
22
|
const calls: any[] = []
|
|
23
23
|
const wallet: any = {
|
|
24
24
|
getPublicKey: async (args: any) => { calls.push(args); return { publicKey: '02' + 'a'.repeat(64) } }
|
|
25
25
|
}
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
expect(
|
|
30
|
-
expect(
|
|
31
|
-
expect(calls[0].counterparty).toBe('anyone')
|
|
32
|
-
expect(calls[0].keyID).toBe(keyID)
|
|
26
|
+
const data = { kind: 'issue', assetId: 'x.0', amount: 10 } as const
|
|
27
|
+
await MandalaAdmin.lock({ wallet, data })
|
|
28
|
+
expect(calls[0].protocolID).toEqual(ADMIN_PROTOCOL)
|
|
29
|
+
expect(calls[0].keyID).toBe(MandalaAdmin.commitment(data))
|
|
30
|
+
expect(calls[0].counterparty).toBe('self')
|
|
33
31
|
})
|
|
34
32
|
})
|
|
@@ -1,26 +1,57 @@
|
|
|
1
1
|
import { MandalaAdmin } from '../MandalaAdmin.js'
|
|
2
|
-
import { PrivateKey, OP } from '@bsv/sdk'
|
|
2
|
+
import { ProtoWallet, PrivateKey, OP, Utils } from '@bsv/sdk'
|
|
3
3
|
|
|
4
4
|
describe('MandalaAdmin lock/decode', () => {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
const wallet = new ProtoWallet(PrivateKey.fromRandom())
|
|
6
|
+
const data = { kind: 'register', assetId: `${'a'.repeat(64)}.0` } as const
|
|
7
|
+
|
|
8
|
+
it('builds a standard P2PKH script (OP_DUP OP_HASH160 <20> OP_EQUALVERIFY OP_CHECKSIG)', async () => {
|
|
9
|
+
const script = await MandalaAdmin.lock({ wallet: wallet as any, data })
|
|
9
10
|
const ops = script.chunks.map(c => c.op)
|
|
10
|
-
expect(
|
|
11
|
-
expect(
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
expect(ops).toEqual([OP.OP_DUP, OP.OP_HASH160, 20, OP.OP_EQUALVERIFY, OP.OP_CHECKSIG])
|
|
12
|
+
expect(script.chunks[2].data?.length).toBe(20)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('decode returns the pubKeyHash', async () => {
|
|
16
|
+
const script = await MandalaAdmin.lock({ wallet: wallet as any, data })
|
|
17
|
+
const decoded = MandalaAdmin.decode(script)
|
|
18
|
+
expect(decoded.pubKeyHash).toEqual(script.chunks[2].data)
|
|
14
19
|
})
|
|
15
20
|
|
|
16
21
|
it('decode throws on non-admin scripts', () => {
|
|
17
22
|
expect(() => MandalaAdmin.decode({ chunks: [{ op: 0x00 }] } as any)).toThrow()
|
|
18
23
|
})
|
|
19
24
|
|
|
20
|
-
it('decode
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
script.
|
|
25
|
+
it('decode throws when the hash push is not 20 bytes', async () => {
|
|
26
|
+
const script = await MandalaAdmin.lock({ wallet: wallet as any, data })
|
|
27
|
+
script.chunks[2] = { op: 19, data: new Array(19).fill(1) }
|
|
28
|
+
expect(() => MandalaAdmin.decode(script)).toThrow()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('embeds publicData as <push JSON> OP_DROP before the P2PKH', async () => {
|
|
32
|
+
const script = await MandalaAdmin.lock({ wallet: wallet as any, data, publicData: { label: 'Gold' } })
|
|
33
|
+
const ops = script.chunks.map(c => c.op)
|
|
34
|
+
expect(ops.slice(1)).toEqual([OP.OP_DROP, OP.OP_DUP, OP.OP_HASH160, 20, OP.OP_EQUALVERIFY, OP.OP_CHECKSIG])
|
|
35
|
+
expect(JSON.parse(Utils.toUTF8(script.chunks[0].data as number[]))).toEqual({ label: 'Gold' })
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('decode round-trips publicData and pubKeyHash (7-chunk)', async () => {
|
|
39
|
+
const script = await MandalaAdmin.lock({ wallet: wallet as any, data, publicData: { label: 'Gold', ticker: 'GLD' } })
|
|
40
|
+
const decoded = MandalaAdmin.decode(script)
|
|
41
|
+
expect(decoded.pubKeyHash).toEqual(script.chunks[4].data)
|
|
42
|
+
expect(decoded.publicData).toEqual({ label: 'Gold', ticker: 'GLD' })
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('decode returns no publicData for a plain 5-chunk admin script', async () => {
|
|
46
|
+
const script = await MandalaAdmin.lock({ wallet: wallet as any, data })
|
|
47
|
+
const decoded = MandalaAdmin.decode(script)
|
|
48
|
+
expect(decoded.pubKeyHash).toEqual(script.chunks[2].data)
|
|
49
|
+
expect(decoded.publicData).toBeUndefined()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('decode rejects a 7-chunk script whose second op is not OP_DROP', async () => {
|
|
53
|
+
const script = await MandalaAdmin.lock({ wallet: wallet as any, data, publicData: { label: 'X' } })
|
|
54
|
+
script.chunks[1] = { op: OP.OP_DUP }
|
|
24
55
|
expect(() => MandalaAdmin.decode(script)).toThrow()
|
|
25
56
|
})
|
|
26
57
|
})
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { MandalaAdmin } from '../MandalaAdmin.js'
|
|
2
|
+
import { ProtoWallet, PrivateKey, Transaction, Spend, LockingScript, OP } from '@bsv/sdk'
|
|
3
|
+
|
|
4
|
+
// End-to-end script test: a P2PKH admin-auth output locked by MandalaAdmin.lock
|
|
5
|
+
// must be spendable by MandalaAdmin.unlock. The lock hashes the counterparty-child
|
|
6
|
+
// public key (forSelf:false); unlock signs with the matching private key and pushes
|
|
7
|
+
// its forSelf:true public key. By BRC-42 symmetry these are the same point, so
|
|
8
|
+
// OP_HASH160 / OP_CHECKSIG pass — for self-spend and for transfer to a new admin.
|
|
9
|
+
describe('MandalaAdmin spend round-trip (interpreter)', () => {
|
|
10
|
+
const data = { kind: 'issue', assetId: `${'a'.repeat(64)}.0`, amount: 5 } as const
|
|
11
|
+
|
|
12
|
+
const buildSpend = (lock: LockingScript, unlock: any, spendTx: Transaction, srcTx: Transaction): Spend =>
|
|
13
|
+
new Spend({
|
|
14
|
+
sourceTXID: srcTx.id('hex'),
|
|
15
|
+
sourceOutputIndex: 0,
|
|
16
|
+
sourceSatoshis: 1,
|
|
17
|
+
lockingScript: lock,
|
|
18
|
+
transactionVersion: spendTx.version,
|
|
19
|
+
otherInputs: [],
|
|
20
|
+
inputIndex: 0,
|
|
21
|
+
unlockingScript: unlock,
|
|
22
|
+
outputs: spendTx.outputs,
|
|
23
|
+
inputSequence: 0xffffffff,
|
|
24
|
+
lockTime: spendTx.lockTime
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('CHECKSIG verifies a self-locked, self-spent admin auth output', async () => {
|
|
28
|
+
const wallet = new ProtoWallet(PrivateKey.fromRandom())
|
|
29
|
+
const lock = await MandalaAdmin.lock({ wallet: wallet as any, data })
|
|
30
|
+
|
|
31
|
+
const srcTx = new Transaction()
|
|
32
|
+
srcTx.addOutput({ lockingScript: lock, satoshis: 1 })
|
|
33
|
+
const spendTx = new Transaction()
|
|
34
|
+
spendTx.addInput({ sourceTransaction: srcTx, sourceOutputIndex: 0, sequence: 0xffffffff })
|
|
35
|
+
spendTx.addOutput({ lockingScript: new LockingScript([{ op: OP.OP_TRUE }]), satoshis: 1 })
|
|
36
|
+
|
|
37
|
+
spendTx.inputs[0].unlockingScriptTemplate = MandalaAdmin.unlock({ wallet: wallet as any, data })
|
|
38
|
+
await spendTx.sign()
|
|
39
|
+
|
|
40
|
+
expect(buildSpend(lock, spendTx.inputs[0].unlockingScript!, spendTx, srcTx).validate()).toBe(true)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('CHECKSIG verifies a publicData admin output (prefix is dropped)', async () => {
|
|
44
|
+
const wallet = new ProtoWallet(PrivateKey.fromRandom())
|
|
45
|
+
const lock = await MandalaAdmin.lock({ wallet: wallet as any, data, publicData: { label: 'Gold' } })
|
|
46
|
+
|
|
47
|
+
const srcTx = new Transaction()
|
|
48
|
+
srcTx.addOutput({ lockingScript: lock, satoshis: 1 })
|
|
49
|
+
const spendTx = new Transaction()
|
|
50
|
+
spendTx.addInput({ sourceTransaction: srcTx, sourceOutputIndex: 0, sequence: 0xffffffff })
|
|
51
|
+
spendTx.addOutput({ lockingScript: new LockingScript([{ op: OP.OP_TRUE }]), satoshis: 1 })
|
|
52
|
+
|
|
53
|
+
spendTx.inputs[0].unlockingScriptTemplate = MandalaAdmin.unlock({ wallet: wallet as any, data })
|
|
54
|
+
await spendTx.sign()
|
|
55
|
+
|
|
56
|
+
expect(buildSpend(lock, spendTx.inputs[0].unlockingScript!, spendTx, srcTx).validate()).toBe(true)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('transfers admin: granter locks to a new admin who spends it', async () => {
|
|
60
|
+
const granter = new ProtoWallet(PrivateKey.fromRandom())
|
|
61
|
+
const newAdmin = new ProtoWallet(PrivateKey.fromRandom())
|
|
62
|
+
const { publicKey: granterId } = await granter.getPublicKey({ identityKey: true })
|
|
63
|
+
const { publicKey: newAdminId } = await newAdmin.getPublicKey({ identityKey: true })
|
|
64
|
+
|
|
65
|
+
// Granter locks the auth output to the new admin (counterparty = new admin id).
|
|
66
|
+
const lock = await MandalaAdmin.lock({ wallet: granter as any, data, counterparty: newAdminId })
|
|
67
|
+
|
|
68
|
+
const srcTx = new Transaction()
|
|
69
|
+
srcTx.addOutput({ lockingScript: lock, satoshis: 1 })
|
|
70
|
+
const spendTx = new Transaction()
|
|
71
|
+
spendTx.addInput({ sourceTransaction: srcTx, sourceOutputIndex: 0, sequence: 0xffffffff })
|
|
72
|
+
spendTx.addOutput({ lockingScript: new LockingScript([{ op: OP.OP_TRUE }]), satoshis: 1 })
|
|
73
|
+
|
|
74
|
+
// New admin spends, deriving against the granter's identity.
|
|
75
|
+
spendTx.inputs[0].unlockingScriptTemplate = MandalaAdmin.unlock({ wallet: newAdmin as any, data, counterparty: granterId })
|
|
76
|
+
await spendTx.sign()
|
|
77
|
+
|
|
78
|
+
expect(buildSpend(lock, spendTx.inputs[0].unlockingScript!, spendTx, srcTx).validate()).toBe(true)
|
|
79
|
+
})
|
|
80
|
+
})
|
|
@@ -43,17 +43,14 @@ describe('MandalaToken lock/decode', () => {
|
|
|
43
43
|
const assetId = `${'a'.repeat(64)}.0`
|
|
44
44
|
const pkh = new Array(20).fill(1)
|
|
45
45
|
const script = new MandalaToken().lock(assetId, 5, pkh)
|
|
46
|
-
// Replace the amount push (chunk index
|
|
47
|
-
script.chunks[
|
|
46
|
+
// Replace the amount push (chunk index 1) with an empty (OP_0) push.
|
|
47
|
+
script.chunks[1] = { op: 0 }
|
|
48
48
|
expect(() => MandalaToken.decode(script)).toThrow()
|
|
49
49
|
})
|
|
50
50
|
|
|
51
|
-
it('
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
// Re-encode the marker push (chunk 0) as PUSHDATA1 of the same byte.
|
|
56
|
-
script.chunks[0] = { op: 0x4c, data: [0x21] }
|
|
57
|
-
expect(() => MandalaToken.decode(script)).toThrow()
|
|
51
|
+
it('has no identifier prefix (8 chunks, leads with the assetId push)', () => {
|
|
52
|
+
const script = new MandalaToken().lock(assetId, 1, pubKeyHash)
|
|
53
|
+
expect(script.chunks).toHaveLength(8)
|
|
54
|
+
expect(script.chunks[0].data?.length).toBe(36) // assetId bytes, not a marker
|
|
58
55
|
})
|
|
59
56
|
})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createMinimallyEncodedScriptChunk, encodeScriptNum, decodeScriptNum,
|
|
3
|
-
decodeScriptNumChunk, encodeAssetId, decodeAssetId
|
|
3
|
+
decodeScriptNumChunk, encodeAssetId, decodeAssetId
|
|
4
4
|
} from '../mandala-encoding'
|
|
5
5
|
|
|
6
6
|
describe('mandala-encoding', () => {
|
|
@@ -35,8 +35,4 @@ describe('mandala-encoding', () => {
|
|
|
35
35
|
const assetId = `${'a'.repeat(64)}.4294967295`
|
|
36
36
|
expect(decodeAssetId(encodeAssetId(assetId))).toBe(assetId)
|
|
37
37
|
})
|
|
38
|
-
|
|
39
|
-
it('exposes the ! marker', () => {
|
|
40
|
-
expect(MARKER).toBe(0x21)
|
|
41
|
-
})
|
|
42
38
|
})
|