@bsv/templates 1.0.0 → 1.1.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.
Files changed (36) hide show
  1. package/dist/cjs/mod.js +22 -7
  2. package/dist/cjs/mod.js.map +1 -1
  3. package/dist/cjs/package.json +2 -2
  4. package/dist/cjs/src/Metanet.js +14 -14
  5. package/dist/cjs/src/Metanet.js.map +1 -1
  6. package/dist/cjs/src/MultiPushDrop.js +278 -0
  7. package/dist/cjs/src/MultiPushDrop.js.map +1 -0
  8. package/dist/cjs/src/OpReturn.js +13 -13
  9. package/dist/cjs/src/OpReturn.js.map +1 -1
  10. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  11. package/dist/esm/mod.js +1 -0
  12. package/dist/esm/mod.js.map +1 -1
  13. package/dist/esm/src/Metanet.js +14 -14
  14. package/dist/esm/src/Metanet.js.map +1 -1
  15. package/dist/esm/src/MultiPushDrop.js +275 -0
  16. package/dist/esm/src/MultiPushDrop.js.map +1 -0
  17. package/dist/esm/src/OpReturn.js +13 -13
  18. package/dist/esm/src/OpReturn.js.map +1 -1
  19. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  20. package/dist/types/mod.d.ts +1 -0
  21. package/dist/types/mod.d.ts.map +1 -1
  22. package/dist/types/src/Metanet.d.ts +13 -13
  23. package/dist/types/src/Metanet.d.ts.map +1 -1
  24. package/dist/types/src/MultiPushDrop.d.ts +66 -0
  25. package/dist/types/src/MultiPushDrop.d.ts.map +1 -0
  26. package/dist/types/src/OpReturn.d.ts +12 -12
  27. package/dist/types/src/OpReturn.d.ts.map +1 -1
  28. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  29. package/mod.ts +1 -0
  30. package/package.json +4 -4
  31. package/src/Metanet.ts +37 -37
  32. package/src/MultiPushDrop.ts +317 -0
  33. package/src/OpReturn.ts +32 -32
  34. package/src/__tests/Metanet.test.ts +15 -15
  35. package/src/__tests/MultiPushDrop.test.ts +256 -0
  36. package/src/__tests/OpReturn.test.ts +7 -7
@@ -0,0 +1,256 @@
1
+ import MultiPushDrop from '../MultiPushDrop'
2
+ import { OP, WalletInterface, WalletCounterparty, PubKeyHex, SecurityLevel, Transaction, CompletedProtoWallet, PrivateKey, PublicKey, Utils, Script, Spend, LockingScript, UnlockingScript } from '@bsv/sdk'
3
+
4
+ // Helper function like createDecodeRedeem from PushDrop tests
5
+ const testLockUnlockDecode = async (
6
+ creatorMultiPushDrop: MultiPushDrop,
7
+ creatorWallet: WalletInterface,
8
+ fields: number[][],
9
+ protocolID: [SecurityLevel, string],
10
+ keyID: string,
11
+ ownerPrivateKeys: PrivateKey[],
12
+ signOutputs: 'all' | 'none' | 'single' = 'all',
13
+ anyoneCanPay: boolean = false
14
+ ): Promise<void> => {
15
+ // --- Lock ---
16
+ const counterparties = ownerPrivateKeys.map(x => x.toPublicKey().toString())
17
+ const lockingScript = await creatorMultiPushDrop.lock(
18
+ fields,
19
+ protocolID,
20
+ keyID,
21
+ counterparties
22
+ )
23
+ expect(lockingScript).toBeInstanceOf(LockingScript)
24
+
25
+ // --- Decode ---
26
+ const decoded = MultiPushDrop.decode(lockingScript)
27
+ expect(decoded.fields).toEqual(fields)
28
+ expect(decoded.lockingPublicKeys.length).toEqual(ownerPrivateKeys.length)
29
+
30
+ // Verify decoded keys match derived keys
31
+ const derivedKeys: PubKeyHex[] = []
32
+ for (const c of counterparties) {
33
+ const { publicKey } = await creatorWallet.getPublicKey({
34
+ protocolID, keyID, counterparty: c
35
+ })
36
+ derivedKeys.push(publicKey)
37
+ }
38
+ expect(decoded.lockingPublicKeys).toEqual(derivedKeys)
39
+
40
+ // --- Unlock (for each counterparty) ---
41
+ const satoshis = 1000 // Use a non-dust amount
42
+
43
+ const sourceTx = new Transaction(
44
+ 1, [], [{ lockingScript, satoshis }], 0
45
+ )
46
+ const sourceOutputIndex = 0
47
+ const { publicKey: creatorIdentityKey } = await creatorWallet.getPublicKey({ identityKey: true })
48
+
49
+ for (let i = 0; i < ownerPrivateKeys.length; i++) {
50
+ const ownerWallet = new CompletedProtoWallet(ownerPrivateKeys[i])
51
+ const ownerMultiPushDrop = new MultiPushDrop(ownerWallet)
52
+ console.log(`Testing unlock with counterparty index ${i}`)
53
+
54
+ const unlockingTemplate = ownerMultiPushDrop.unlock(
55
+ protocolID,
56
+ keyID,
57
+ creatorIdentityKey,
58
+ signOutputs,
59
+ anyoneCanPay
60
+ )
61
+
62
+ // Create a dummy spending transaction
63
+ const spendTx = new Transaction(
64
+ 1,
65
+ [{
66
+ sourceTransaction: sourceTx, // Link for signing context
67
+ sourceOutputIndex,
68
+ // unlockingScript will be added by sign method
69
+ sequence: 0xffffffff
70
+ }],
71
+ [{ // Dummy output
72
+ lockingScript: Script.fromASM('OP_RETURN'),
73
+ satoshis: satoshis - 500 // Account for potential fees
74
+ }],
75
+ 0
76
+ )
77
+
78
+ // Sign to get the unlocking script
79
+ const unlockingScript = await unlockingTemplate.sign(spendTx, 0)
80
+ expect(unlockingScript).toBeInstanceOf(UnlockingScript)
81
+ expect(unlockingScript.chunks.length).toBe(2) // Signature + Index
82
+ // Verify index chunk
83
+ const indexChunk = unlockingScript.chunks[1]
84
+ let decodedIndex: number
85
+ if (indexChunk.op === OP.OP_0) decodedIndex = 0
86
+ else if (indexChunk.op >= OP.OP_1 && indexChunk.op <= OP.OP_16) decodedIndex = indexChunk.op - OP.OP_1 + 1
87
+ else if (indexChunk.data?.length === 1) decodedIndex = indexChunk.data[0]
88
+ else throw new Error('Cannot decode index')
89
+ expect(decodedIndex).toEqual(ownerPrivateKeys.length - 1 - i) // It should be nKeys - 1 - the loop count
90
+
91
+ const estimatedLength = await unlockingTemplate.estimateLength(null as unknown as Transaction, 0)
92
+ // Check if length is reasonable (e.g., 74 +/- a few bytes)
93
+ expect(estimatedLength).toBeGreaterThanOrEqual(72)
94
+ expect(estimatedLength).toBeLessThanOrEqual(80)
95
+
96
+ // --- Verify Spend ---
97
+ const spend = new Spend({
98
+ sourceTXID: sourceTx.id('hex'),
99
+ sourceOutputIndex,
100
+ sourceSatoshis: satoshis,
101
+ lockingScript, // From lock step
102
+ transactionVersion: spendTx.version,
103
+ otherInputs: [], // No other inputs in this simple case
104
+ inputIndex: 0,
105
+ unlockingScript, // From sign step
106
+ outputs: spendTx.outputs,
107
+ inputSequence: spendTx.inputs[0].sequence ?? 0xffffffff,
108
+ lockTime: spendTx.lockTime
109
+ })
110
+ const valid = spend.validate()
111
+ expect(valid).toBe(true)
112
+ }
113
+ }
114
+
115
+ describe('MultiPushDrop', () => {
116
+ let selfKey: PrivateKey
117
+ let wallet: WalletInterface
118
+ let multiPushDrop: MultiPushDrop
119
+ let counterparty1Key: PrivateKey
120
+ let counterparty2Key: PrivateKey
121
+ const protocolID: [SecurityLevel, string] = [0, 'tests']
122
+ const keyID = 'test-key-123'
123
+
124
+ beforeEach(() => {
125
+ selfKey = PrivateKey.fromRandom()
126
+ counterparty1Key = PrivateKey.fromRandom()
127
+ counterparty2Key = PrivateKey.fromRandom()
128
+
129
+ // Use CompletedProtoWallet or mock as needed
130
+ wallet = new CompletedProtoWallet(selfKey)
131
+ multiPushDrop = new MultiPushDrop(wallet)
132
+ })
133
+
134
+ it('should lock, decode, and unlock with a single key (self)', async () => {
135
+ await testLockUnlockDecode(
136
+ multiPushDrop,
137
+ wallet,
138
+ [[1, 2, 3]],
139
+ protocolID,
140
+ keyID,
141
+ [selfKey]
142
+ )
143
+ })
144
+
145
+ it('should lock, decode, and unlock with a single key (external)', async () => {
146
+ await testLockUnlockDecode(
147
+ multiPushDrop,
148
+ wallet,
149
+ [[0xaa, 0xbb]],
150
+ protocolID,
151
+ keyID,
152
+ [counterparty1Key]
153
+ )
154
+ })
155
+
156
+ it('should lock, decode, and unlock with two keys (self, external)', async () => {
157
+ await testLockUnlockDecode(
158
+ multiPushDrop,
159
+ wallet,
160
+ [Utils.toArray('hello', 'utf8')],
161
+ protocolID,
162
+ keyID,
163
+ [counterparty1Key, selfKey]
164
+ )
165
+ })
166
+
167
+ it('should lock, decode, and unlock with three keys (self, external1, external2)', async () => {
168
+ await testLockUnlockDecode(
169
+ multiPushDrop,
170
+ wallet,
171
+ [[1], [1], [0xff]],
172
+ protocolID,
173
+ keyID,
174
+ [selfKey, counterparty1Key, counterparty2Key]
175
+ )
176
+ })
177
+
178
+ it('should lock, decode, and unlock with 10 keys', async () => {
179
+ const keys: PrivateKey[] = []
180
+ while (keys.length < 10) {
181
+ keys.push(PrivateKey.fromRandom())
182
+ }
183
+ await testLockUnlockDecode(
184
+ multiPushDrop,
185
+ wallet,
186
+ [[1], [1], [0xff]],
187
+ protocolID,
188
+ keyID,
189
+ keys
190
+ )
191
+ })
192
+
193
+ it('should handle empty fields', async () => {
194
+ await testLockUnlockDecode(
195
+ multiPushDrop,
196
+ wallet,
197
+ [],
198
+ protocolID,
199
+ keyID,
200
+ [selfKey, counterparty1Key]
201
+ )
202
+ })
203
+
204
+ it('should handle large fields', async () => {
205
+ await testLockUnlockDecode(
206
+ multiPushDrop,
207
+ wallet,
208
+ [new Array(100).fill(0xaa), new Array(80).fill(0xbb), new Array(70000).fill(0xbb)],
209
+ protocolID,
210
+ keyID,
211
+ [selfKey, counterparty1Key]
212
+ )
213
+ })
214
+
215
+ it('should handle different signOutputs modes (anyonecanpay=false)', async () => {
216
+ const counterparties = [selfKey, counterparty1Key]
217
+ const fields = [[1]]
218
+ await testLockUnlockDecode(multiPushDrop, wallet, fields, protocolID, keyID, counterparties, 'all', false)
219
+ await testLockUnlockDecode(multiPushDrop, wallet, fields, protocolID, keyID, counterparties, 'none', false)
220
+ await testLockUnlockDecode(multiPushDrop, wallet, fields, protocolID, keyID, counterparties, 'single', false)
221
+ })
222
+
223
+ it('should handle different signOutputs modes (anyonecanpay=true)', async () => {
224
+ const counterparties = [selfKey, counterparty1Key]
225
+ const fields = [[2]]
226
+ await testLockUnlockDecode(multiPushDrop, wallet, fields, protocolID, keyID, counterparties, 'all', true)
227
+ await testLockUnlockDecode(multiPushDrop, wallet, fields, protocolID, keyID, counterparties, 'none', true)
228
+ await testLockUnlockDecode(multiPushDrop, wallet, fields, protocolID, keyID, counterparties, 'single', true)
229
+ })
230
+
231
+ it('lock should fail with empty counterparties array', async () => {
232
+ await expect(multiPushDrop.lock(
233
+ [[1]],
234
+ protocolID,
235
+ keyID,
236
+ []
237
+ )).rejects.toThrow('MultiPushDrop requires at least one counterparty.')
238
+ })
239
+
240
+ it('unlock should fail if unlocker key is not in the list', async () => {
241
+ const { publicKey: creatorIdentityKey } = await wallet.getPublicKey({ identityKey: true })
242
+ const lockingScript = await multiPushDrop.lock([[1]], protocolID, keyID, ['self'])
243
+ const sourceTx = new Transaction(1, [], [{ lockingScript, satoshis: 1000 }], 0)
244
+ const spendTx = new Transaction(1, [{ sourceTransaction: sourceTx, sourceOutputIndex: 0 }], [], 0)
245
+
246
+ const unknownKey = PrivateKey.fromRandom()
247
+ const walletWithUnknown = new CompletedProtoWallet(unknownKey)
248
+ const mpdAsUnknown = new MultiPushDrop(walletWithUnknown)
249
+ const unlockingTemplate = mpdAsUnknown.unlock(
250
+ protocolID,
251
+ keyID,
252
+ creatorIdentityKey
253
+ )
254
+ await expect(unlockingTemplate.sign(spendTx, 0)).rejects.toThrow(/Unlocker key derived .* not found/)
255
+ })
256
+ })
@@ -1,11 +1,11 @@
1
1
  import OpReturn from '../OpReturn'
2
2
 
3
3
  describe('OpReturn script', () => {
4
- it('locks OpReturn data', () => {
5
- expect(new OpReturn().lock('1234').toASM()).toEqual('OP_0 OP_RETURN 31323334')
6
- expect(new OpReturn().lock(['1234', '5678']).toASM()).toEqual('OP_0 OP_RETURN 31323334 35363738')
7
- })
8
- it('does not support unlocking', () => {
9
- expect(() => new OpReturn().unlock()).toThrow()
10
- })
4
+ it('locks OpReturn data', () => {
5
+ expect(new OpReturn().lock('1234').toASM()).toEqual('OP_0 OP_RETURN 31323334')
6
+ expect(new OpReturn().lock(['1234', '5678']).toASM()).toEqual('OP_0 OP_RETURN 31323334 35363738')
7
+ })
8
+ it('does not support unlocking', () => {
9
+ expect(() => new OpReturn().unlock()).toThrow()
10
+ })
11
11
  })