@bsv/templates 1.0.0 → 1.2.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 (43) hide show
  1. package/README.md +1 -0
  2. package/dist/cjs/mod.js +5 -26
  3. package/dist/cjs/mod.js.map +1 -1
  4. package/dist/cjs/package.json +7 -6
  5. package/dist/cjs/src/Metanet.js +14 -14
  6. package/dist/cjs/src/Metanet.js.map +1 -1
  7. package/dist/cjs/src/MultiPushDrop.js +279 -0
  8. package/dist/cjs/src/MultiPushDrop.js.map +1 -0
  9. package/dist/cjs/src/MultiSigPubkeyHash.js +173 -0
  10. package/dist/cjs/src/MultiSigPubkeyHash.js.map +1 -0
  11. package/dist/cjs/src/OpReturn.js +15 -14
  12. package/dist/cjs/src/OpReturn.js.map +1 -1
  13. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  14. package/dist/esm/mod.js +2 -2
  15. package/dist/esm/mod.js.map +1 -1
  16. package/dist/esm/src/Metanet.js +14 -14
  17. package/dist/esm/src/Metanet.js.map +1 -1
  18. package/dist/esm/src/MultiPushDrop.js +275 -0
  19. package/dist/esm/src/MultiPushDrop.js.map +1 -0
  20. package/dist/esm/src/MultiSigPubkeyHash.js +170 -0
  21. package/dist/esm/src/MultiSigPubkeyHash.js.map +1 -0
  22. package/dist/esm/src/OpReturn.js +14 -14
  23. package/dist/esm/src/OpReturn.js.map +1 -1
  24. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  25. package/dist/types/mod.d.ts +2 -2
  26. package/dist/types/mod.d.ts.map +1 -1
  27. package/dist/types/src/Metanet.d.ts +13 -13
  28. package/dist/types/src/Metanet.d.ts.map +1 -1
  29. package/dist/types/src/MultiPushDrop.d.ts +66 -0
  30. package/dist/types/src/MultiPushDrop.d.ts.map +1 -0
  31. package/dist/types/src/MultiSigPubkeyHash.d.ts +24 -0
  32. package/dist/types/src/MultiSigPubkeyHash.d.ts.map +1 -0
  33. package/dist/types/src/OpReturn.d.ts +13 -13
  34. package/dist/types/src/OpReturn.d.ts.map +1 -1
  35. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  36. package/mod.ts +2 -2
  37. package/package.json +8 -7
  38. package/src/MultiPushDrop.ts +317 -0
  39. package/src/MultiSigPubkeyHash.ts +214 -0
  40. package/src/OpReturn.ts +33 -33
  41. package/src/Metanet.ts +0 -49
  42. package/src/__tests/Metanet.test.ts +0 -21
  43. package/src/__tests/OpReturn.test.ts +0 -11
@@ -0,0 +1,317 @@
1
+ import {
2
+ ScriptTemplate,
3
+ LockingScript,
4
+ UnlockingScript,
5
+ OP,
6
+ ScriptTemplateUnlock,
7
+ Utils,
8
+ Hash,
9
+ TransactionSignature,
10
+ Signature,
11
+ WalletInterface, SecurityLevel, WalletCounterparty,
12
+ Transaction,
13
+ PubKeyHex
14
+ } from '@bsv/sdk'
15
+
16
+ // Helper to ensure a value is not null or undefined
17
+ function verifyTruthy<T> (v: T | undefined | null, err?: string): T {
18
+ if (v === null || v === undefined) throw new Error(err || 'Value must not be null or undefined')
19
+ return v
20
+ }
21
+
22
+ // Helper to create minimally encoded script chunks (same as in PushDrop)
23
+ const createMinimallyEncodedScriptChunk = (
24
+ data: number[]
25
+ ): { op: number, data?: number[] } => {
26
+ if (data.length === 0) return { op: 0 } // OP_0
27
+ if (data.length === 1 && data[0] === 0) return { op: 0 } // OP_0
28
+ if (data.length === 1 && data[0] > 0 && data[0] <= 16) return { op: 0x50 + data[0] } // OP_1 to OP_16
29
+ if (data.length === 1 && data[0] === 0x81) return { op: 0x4f } // OP_1NEGATE
30
+ if (data.length <= 75) return { op: data.length, data }
31
+ if (data.length <= 255) return { op: 0x4c, data } // OP_PUSHDATA1
32
+ if (data.length <= 65535) return { op: 0x4d, data } // OP_PUSHDATA2
33
+ return { op: 0x4e, data } // OP_PUSHDATA4
34
+ }
35
+
36
+ /**
37
+ * Represents the decoded structure of a MultiPushDrop locking script.
38
+ */
39
+ export interface MultiPushDropDecoded {
40
+ lockingPublicKeys: PubKeyHex[]
41
+ fields: number[][]
42
+ }
43
+
44
+ /**
45
+ * MultiPushDrop Script Template
46
+ *
47
+ * This template creates locking scripts that allow spending by any one of multiple
48
+ * specified public keys (1-of-N). It also pushes arbitrary data fields onto the stack,
49
+ * which are dropped after the signature check.
50
+ *
51
+ * When using this among adversarial or non-trusted groups, the MASSIVE caveat is that
52
+ * there is no constraint enforcing that any group members are kept in the loop. Any group
53
+ * member can trivially destroy the token. For more practical non-trusted arrangements,
54
+ * techniques like OP_PUSH_TX should be used instead.
55
+ *
56
+ * There's also a known bug in this implementation where it won't work with over around 120
57
+ * keys but involving more than a few people than just a few into a FULLY TRUST BASED exchange
58
+ * is never a good idea. Use a more robust, application-specific mechanism.
59
+ */
60
+ export class MultiPushDrop implements ScriptTemplate {
61
+ wallet: WalletInterface
62
+ originator?: string
63
+
64
+ /**
65
+ * Decodes a MultiPushDrop locking script back into its data fields and the list of locking public keys.
66
+ * @param script The MultiPushDrop locking script to decode.
67
+ * @returns {MultiPushDropDecoded} An object containing the locking public keys and data fields.
68
+ * @throws {Error} If the script structure is not a valid MultiPushDrop script.
69
+ */
70
+ static decode (script: LockingScript): MultiPushDropDecoded {
71
+ const chunks = script.chunks
72
+ let cursor = 0
73
+
74
+ // Decode keys until they stop being 33 bytes long
75
+ const lockingPublicKeys: PubKeyHex[] = []
76
+ while (chunks[cursor].data?.length === 33) {
77
+ const keyChunk = verifyTruthy(chunks[cursor], `Missing public key chunk ${cursor}`)
78
+ const keyData = verifyTruthy(keyChunk.data, `Public key chunk ${cursor} has no data`)
79
+ lockingPublicKeys.push(Utils.toHex(keyData))
80
+ cursor++
81
+ }
82
+
83
+ // Skip the nPublicKeys chunk and opcodes.
84
+ // This amounts to 8 items to skip.
85
+ cursor += 8
86
+
87
+ // Decode Data Fields
88
+ const fields: number[][] = []
89
+ for (let i = cursor; i < chunks.length; i++) {
90
+ const nextOpcode = chunks[i + 1]?.op
91
+ const chunkData = chunks[i].data ?? [] // Use OP code for OP_0-OP_16 etc. if data is null
92
+
93
+ let currentField: number[] = []
94
+ if (chunkData.length > 0) {
95
+ currentField = chunkData
96
+ } else if (chunks[i].op >= OP.OP_1 && chunks[i].op <= OP.OP_16) {
97
+ currentField = [chunks[i].op - OP.OP_1 + 1]
98
+ } else if (chunks[i].op === OP.OP_0) {
99
+ currentField = [] // Represent OP_0 as empty array
100
+ } else if (chunks[i].op === OP.OP_1NEGATE) {
101
+ currentField = [0x81]
102
+ } else if (chunks[i].op === OP.OP_DROP || chunks[i].op === OP.OP_2DROP) {
103
+ // Stop before the drops
104
+ break
105
+ } else {
106
+ // Assume it's a data push even if data is empty for some reason
107
+ currentField = chunkData
108
+ }
109
+ fields.push(currentField)
110
+ // If the next opcode is a DROP, we've found the last field
111
+ if (nextOpcode === OP.OP_DROP || nextOpcode === OP.OP_2DROP) {
112
+ break
113
+ }
114
+ }
115
+
116
+ return {
117
+ lockingPublicKeys,
118
+ fields
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Constructs a new instance of the MultiPushDrop class.
124
+ *
125
+ * @param {WalletInterface} wallet - The wallet interface used for deriving keys and signing.
126
+ * @param {string} [originator] - The originator domain for wallet requests.
127
+ */
128
+ constructor (wallet: WalletInterface, originator?: string) {
129
+ this.wallet = wallet
130
+ this.originator = originator
131
+ }
132
+
133
+ /**
134
+ * Creates a MultiPushDrop locking script.
135
+ *
136
+ * @param {number[][]} fields - The arbitrary data fields to include in the script.
137
+ * @param {[SecurityLevel, string]} protocolID - The protocol ID used for key derivation.
138
+ * @param {string} keyID - The key ID used for key derivation.
139
+ * @param {WalletCounterparty[]} counterparties - An array of counterparties ('self' or PubKeyHex) whose derived keys can unlock the script. Must contain at least one.
140
+ * @returns {Promise<LockingScript>} The generated MultiPushDrop locking script.
141
+ * @throws {Error} If counterparties array is empty.
142
+ */
143
+ async lock (
144
+ fields: number[][],
145
+ protocolID: [SecurityLevel, string],
146
+ keyID: string,
147
+ counterparties: WalletCounterparty[]
148
+ ): Promise<LockingScript> {
149
+ if (!Array.isArray(counterparties) || counterparties.length === 0) {
150
+ throw new Error('MultiPushDrop requires at least one counterparty.')
151
+ }
152
+
153
+ const publicKeys: string[] = []
154
+ for (const counterparty of counterparties) {
155
+ const { publicKey } = await this.wallet.getPublicKey({
156
+ protocolID,
157
+ keyID,
158
+ counterparty
159
+ }, this.originator)
160
+ publicKeys.push(publicKey)
161
+ }
162
+
163
+ const nPublicKeys = publicKeys.length
164
+ const lockPart: Array<{ op: number, data?: number[] }> = []
165
+
166
+ // Push Public Keys
167
+ for (const publicKeyHex of publicKeys) {
168
+ lockPart.push({
169
+ op: publicKeyHex.length / 2, // Length of compressed pubkey is 33 bytes (66 hex)
170
+ data: Utils.toArray(publicKeyHex, 'hex')
171
+ })
172
+ }
173
+
174
+ // Pick the value on the stack that's right before the locking script.
175
+ // This should be the index of the key to use in the unlock.
176
+ lockPart.push(createMinimallyEncodedScriptChunk([nPublicKeys]))
177
+ lockPart.push({ op: OP.OP_PICK })
178
+
179
+ // Now we use the index to get the actual key.
180
+ lockPart.push({ op: OP.OP_PICK })
181
+
182
+ // We pull the signature from the bottom of the stack, no matter the number of keys.
183
+ lockPart.push({ op: OP.OP_DEPTH })
184
+ lockPart.push({ op: OP.OP_1SUB })
185
+ lockPart.push({ op: OP.OP_PICK })
186
+
187
+ // We swap the signature and public key so they're in the correct order, then CHECKSIGVERIFY
188
+ lockPart.push({ op: OP.OP_SWAP })
189
+ lockPart.push({ op: OP.OP_CHECKSIGVERIFY })
190
+
191
+ // Construct PushDrop Part for fields
192
+ const pushDropPart: Array<{ op: number, data?: number[] }> = []
193
+ for (const field of fields) {
194
+ pushDropPart.push(createMinimallyEncodedScriptChunk(field))
195
+ }
196
+
197
+ // Add Drop Opcodes
198
+ // We need to drop N keys, the number N itself, and M fields after verification succeeds.
199
+ // We also copied the signature itself so we need to drop that.
200
+ // Then we push a single true.
201
+ let itemsToDrop = fields.length + nPublicKeys + 2
202
+ while (itemsToDrop > 1) {
203
+ pushDropPart.push({ op: OP.OP_2DROP })
204
+ itemsToDrop -= 2
205
+ }
206
+ if (itemsToDrop === 1) {
207
+ pushDropPart.push({ op: OP.OP_DROP })
208
+ }
209
+
210
+ // Combine parts and return
211
+ return new LockingScript([
212
+ ...lockPart,
213
+ ...pushDropPart,
214
+ { op: OP.OP_TRUE }
215
+ ])
216
+ }
217
+
218
+ /**
219
+ * Creates an unlocking script template for spending a MultiPushDrop output.
220
+ *
221
+ * @param {[SecurityLevel, string]} protocolID - The protocol ID used for key derivation.
222
+ * @param {string} keyID - The key ID used for key derivation.
223
+ * @param {WalletCounterparty} creator - The identity key of the person who made the locking script. Could come from one of the fields or be passed off chain.
224
+ * @param {'all' | 'none' | 'single'} [signOutputs='all'] - Specifies which transaction outputs to sign.
225
+ * @param {boolean} [anyoneCanPay=false] - Specifies if the SIGHASH_ANYONECANPAY flag should be used.
226
+ * @returns {ScriptTemplateUnlock} An object containing `sign` and `estimateLength` functions.
227
+ * @throws {Error} If we are not found in the list of keys, or if required signing info (sourceTXID, satoshis, lockingScript) is missing.
228
+ */
229
+ unlock (
230
+ protocolID: [SecurityLevel, string],
231
+ keyID: string,
232
+ creator: WalletCounterparty,
233
+ signOutputs: 'all' | 'none' | 'single' = 'all',
234
+ anyoneCanPay = false
235
+ ): ScriptTemplateUnlock {
236
+ return {
237
+ sign: async (
238
+ tx: Transaction,
239
+ inputIndex: number
240
+ ): Promise<UnlockingScript> => {
241
+ // Prepare for signing
242
+ let signatureScope = TransactionSignature.SIGHASH_FORKID
243
+ if (signOutputs === 'all') signatureScope |= TransactionSignature.SIGHASH_ALL
244
+ else if (signOutputs === 'none') signatureScope |= TransactionSignature.SIGHASH_NONE
245
+ else if (signOutputs === 'single') signatureScope |= TransactionSignature.SIGHASH_SINGLE
246
+ if (anyoneCanPay) signatureScope |= TransactionSignature.SIGHASH_ANYONECANPAY
247
+ const input = tx.inputs[inputIndex]
248
+ const currentSourceTXID = input.sourceTXID ?? input.sourceTransaction?.id('hex')
249
+ const currentSourceSatoshis = input.sourceTransaction?.outputs[input.sourceOutputIndex].satoshis
250
+ const currentLockingScript = input.sourceTransaction?.outputs[input.sourceOutputIndex]?.lockingScript
251
+ if (typeof currentSourceTXID !== 'string') throw new Error('Input sourceTXID or sourceTransaction required for signing.')
252
+ if (currentSourceSatoshis === undefined) throw new Error('Input sourceSatoshis or sourceTransaction required for signing.')
253
+ if (currentLockingScript == null) throw new Error('Input lockingScript or sourceTransaction required for signing.')
254
+ const otherInputs = tx.inputs.filter((_, index) => index !== inputIndex)
255
+ const decoded = MultiPushDrop.decode(currentLockingScript)
256
+
257
+ // Find the index of the unlocker's public key
258
+ let unlockerIndex = -1
259
+ const { publicKey: unlockerPubKeyHex } = await this.wallet.getPublicKey({
260
+ protocolID,
261
+ keyID,
262
+ counterparty: creator,
263
+ forSelf: true
264
+ }, this.originator)
265
+ for (let i = 0; i < decoded.lockingPublicKeys.length; i++) {
266
+ if (decoded.lockingPublicKeys[i] === unlockerPubKeyHex) {
267
+ unlockerIndex = i
268
+ break
269
+ }
270
+ }
271
+ if (unlockerIndex === -1) {
272
+ throw new Error(`Unlocker key derived for counterparty (creator) "${creator}" not found in the list of locking keys.`)
273
+ }
274
+ unlockerIndex = decoded.lockingPublicKeys.length - 1 - unlockerIndex
275
+
276
+ // Calculate Preimage
277
+ const preimage = TransactionSignature.format({
278
+ sourceTXID: currentSourceTXID,
279
+ sourceOutputIndex: verifyTruthy(input.sourceOutputIndex),
280
+ sourceSatoshis: currentSourceSatoshis,
281
+ transactionVersion: tx.version,
282
+ otherInputs,
283
+ inputIndex,
284
+ outputs: tx.outputs,
285
+ inputSequence: input.sequence ?? 0xffffffff,
286
+ subscript: currentLockingScript,
287
+ lockTime: tx.lockTime,
288
+ scope: signatureScope
289
+ })
290
+
291
+ // Create Signature
292
+ const preimageHash = Hash.hash256(preimage)
293
+ const { signature: bareSignature } = await this.wallet.createSignature({
294
+ hashToDirectlySign: preimageHash,
295
+ protocolID,
296
+ keyID,
297
+ counterparty: creator
298
+ }, this.originator)
299
+ const signature = Signature.fromDER([...bareSignature])
300
+ const txSignature = new TransactionSignature(signature.r, signature.s, signatureScope)
301
+ const sigForScript = txSignature.toChecksigFormat()
302
+
303
+ // Create Unlocking Script Chunks: <Signature> <Index>
304
+ const unlockingChunks: Array<{ op: number, data?: number[] }> = []
305
+ unlockingChunks.push({ op: sigForScript.length, data: sigForScript })
306
+ unlockingChunks.push(createMinimallyEncodedScriptChunk([unlockerIndex]))
307
+ return new UnlockingScript(unlockingChunks)
308
+ },
309
+ // Estimate length: Signature (~71-73 bytes) + Index push (1 byte for 0-15, potentially more)
310
+ estimateLength: async (): Promise<number> => {
311
+ // A conservative estimate, usually 73 + 1 = 74
312
+ // Could potentially be larger if index > 15, but that's rare.
313
+ return 74
314
+ }
315
+ }
316
+ }
317
+ }
@@ -0,0 +1,214 @@
1
+ import { ScriptTemplate, LockingScript, UnlockingScript, OP, Hash, PublicKey, TransactionSignature, Signature, Utils, WalletInterface, Transaction, ScriptChunk } from "@bsv/sdk"
2
+
3
+ export type MultiSigInstructions = {
4
+ keyID: string
5
+ counterparty: string
6
+ pubkeys: string[]
7
+ }
8
+
9
+ function concatPubkeys(pubkeys: PublicKey[]): number[] {
10
+ return pubkeys.map((p) => p.toDER() as number[]).reduce((a, b) => a.concat(b), [])
11
+ }
12
+
13
+ export class MultiSigPubkeyHash implements ScriptTemplate {
14
+
15
+ static address(pubkeys: PublicKey[], threshold: number): string {
16
+ const concat = concatPubkeys(pubkeys)
17
+ const hash = Hash.hash160(concat)
18
+ const writer = new Utils.Writer()
19
+ writer.write(hash)
20
+ writer.writeVarIntNum(threshold)
21
+ writer.writeVarIntNum(pubkeys.length)
22
+ const data = writer.toArray()
23
+ return Utils.toBase58Check(data, [0x98])
24
+ }
25
+
26
+ static async addressBRC29(wallet: WalletInterface, counterparties: string[], keyID: string, threshold: number): Promise<{ pubkeys: string[], address: string }> {
27
+ const pubkeys = await Promise.all(counterparties.map(async (counterparty) => {
28
+ const { publicKey } = await wallet.getPublicKey({
29
+ protocolID: [1, "multi sig brc29"],
30
+ keyID,
31
+ counterparty
32
+ })
33
+ return PublicKey.fromString(publicKey)
34
+ }))
35
+ return { pubkeys: pubkeys.map(p => p.toString()), address: this.address(pubkeys, threshold) }
36
+ }
37
+
38
+ static thresholdAndTotalFromAddress(address: string): { hash: number[], threshold: number, total: number } {
39
+ const h = Utils.fromBase58Check(address)
40
+ if (h.prefix[0] !== 0x98) {
41
+ throw new Error('only P2MSH is supported, set your prefix byte to 0x98')
42
+ }
43
+ const reader = new Utils.Reader(h.data as number[])
44
+ const hash = reader.read(20)
45
+ const threshold = reader.readVarIntNum()
46
+ const total = reader.readVarIntNum()
47
+ return { hash, threshold, total }
48
+ }
49
+
50
+ lock(
51
+ address?: string,
52
+ pubkeys?: PublicKey[],
53
+ threshold: number = 1,
54
+ ): LockingScript {
55
+ let hash: number[]
56
+ let total: number
57
+ if (address) {
58
+ if (typeof address !== 'string') throw new Error('address must be a string')
59
+ const result = MultiSigPubkeyHash.thresholdAndTotalFromAddress(address)
60
+ hash = result.hash
61
+ total = result.total
62
+ threshold = result.threshold
63
+ } else {
64
+ if (!pubkeys || pubkeys.length < 2 || pubkeys.length < threshold) throw new Error(`at least ${threshold} pubkeys are required, or use an address`)
65
+ const concat = concatPubkeys(pubkeys)
66
+ hash = Hash.hash160(concat)
67
+ total = pubkeys.length
68
+ }
69
+
70
+ const script = new LockingScript();
71
+ for (let i = 0; i < total - 1; i++) {
72
+ script.writeOpCode(OP.OP_CAT)
73
+ }
74
+ script
75
+ .writeOpCode(OP.OP_DUP)
76
+ .writeOpCode(OP.OP_HASH160)
77
+ .writeBin(hash)
78
+ .writeOpCode(OP.OP_EQUALVERIFY)
79
+ .writeNumber(threshold)
80
+ .writeOpCode(OP.OP_SWAP);
81
+ for (let i = 0; i < total - 1; i++) {
82
+ script
83
+ .writeNumber(33)
84
+ .writeOpCode(OP.OP_SPLIT)
85
+ }
86
+ script.writeNumber(total)
87
+ script.writeOpCode(OP.OP_CHECKMULTISIG);
88
+
89
+ return script
90
+ }
91
+
92
+ unlock(
93
+ wallet: WalletInterface,
94
+ customInstructions: MultiSigInstructions,
95
+ workingUnlockingScript?: UnlockingScript,
96
+ signOutputs: "all" | "none" | "single" = "all",
97
+ anyoneCanPay = false,
98
+ sourceSatoshis?: number,
99
+ lockingScript?: LockingScript
100
+ ): {
101
+ sign: (tx: Transaction, inputIndex: number) => Promise<UnlockingScript>;
102
+ estimateLength: (tx: Transaction, inputIndex: number) => Promise<number>;
103
+ } {
104
+ return {
105
+ sign: async (tx: Transaction, inputIndex: number) => {
106
+ if (!workingUnlockingScript) {
107
+ workingUnlockingScript = new UnlockingScript()
108
+ workingUnlockingScript.writeOpCode(OP.OP_0)
109
+ customInstructions.pubkeys.forEach((pubkey) => {
110
+ workingUnlockingScript!.writeBin(PublicKey.fromString(pubkey).toDER() as number[])
111
+ })
112
+ }
113
+ let signatureScope = TransactionSignature.SIGHASH_FORKID;
114
+ if (signOutputs === "all") {
115
+ signatureScope |= TransactionSignature.SIGHASH_ALL;
116
+ }
117
+ if (signOutputs === "none") {
118
+ signatureScope |= TransactionSignature.SIGHASH_NONE;
119
+ }
120
+ if (signOutputs === "single") {
121
+ signatureScope |= TransactionSignature.SIGHASH_SINGLE;
122
+ }
123
+ if (anyoneCanPay) {
124
+ signatureScope |= TransactionSignature.SIGHASH_ANYONECANPAY;
125
+ }
126
+ const input = tx.inputs[inputIndex];
127
+
128
+ const otherInputs = tx.inputs.filter(
129
+ (_, index) => index !== inputIndex
130
+ )
131
+
132
+ const sourceTXID = input.sourceTXID
133
+ ? input.sourceTXID
134
+ : input.sourceTransaction?.id("hex");
135
+ if (!sourceTXID) {
136
+ throw new Error(
137
+ "The input sourceTXID or sourceTransaction is required for transaction signing."
138
+ );
139
+ }
140
+ sourceSatoshis ||=
141
+ input.sourceTransaction?.outputs[input.sourceOutputIndex].satoshis;
142
+ if (!sourceSatoshis) {
143
+ throw new Error(
144
+ "The sourceSatoshis or input sourceTransaction is required for transaction signing."
145
+ );
146
+ }
147
+ lockingScript ||=
148
+ input.sourceTransaction?.outputs[input.sourceOutputIndex]
149
+ .lockingScript;
150
+ if (!lockingScript) {
151
+ throw new Error(
152
+ "The lockingScript or input sourceTransaction is required for transaction signing."
153
+ );
154
+ }
155
+
156
+ const preimage = TransactionSignature.format({
157
+ sourceTXID,
158
+ sourceOutputIndex: input.sourceOutputIndex,
159
+ sourceSatoshis,
160
+ transactionVersion: tx.version,
161
+ otherInputs,
162
+ inputIndex,
163
+ outputs: tx.outputs,
164
+ inputSequence: input.sequence || 0xffffffff,
165
+ subscript: lockingScript,
166
+ lockTime: tx.lockTime,
167
+ scope: signatureScope,
168
+ })
169
+
170
+ const hashToDirectlySign = Hash.hash256(preimage)
171
+
172
+ const { signature } = await wallet.createSignature({
173
+ hashToDirectlySign,
174
+ protocolID: [1, "multi sig brc29"],
175
+ counterparty: customInstructions.counterparty,
176
+ keyID: customInstructions.keyID,
177
+ })
178
+
179
+ const s = Signature.fromDER(signature)
180
+ const sig = new TransactionSignature(s.r, s.s, signatureScope)
181
+ const sigForScript = sig.toChecksigFormat()
182
+
183
+ workingUnlockingScript.writeBin(sigForScript)
184
+ const chunkforSig = workingUnlockingScript.chunks.pop() as ScriptChunk
185
+ // add it to the array at position 1, pushing the other content to the right
186
+ workingUnlockingScript.chunks.splice(1, 0, chunkforSig)
187
+ return workingUnlockingScript
188
+ },
189
+
190
+ estimateLength: (tx: Transaction, inputIndex: number) => {
191
+ let numberOfPubkeys = 2
192
+ let numberOfSignatures = 1
193
+ const staticLength = 28
194
+ const input = tx.inputs[inputIndex];
195
+ const lockingScript = input.sourceTransaction?.outputs[input.sourceOutputIndex].lockingScript;
196
+ if (!lockingScript) {
197
+ return Promise.resolve(1000) // guess
198
+ }
199
+ let chunks = lockingScript.chunks.length - 8 // remove static chunks
200
+ const numPubKeys = Math.floor(chunks / 3)
201
+ const thresholdPos = 5 + (numPubKeys - 1)
202
+ const n = lockingScript?.chunks[thresholdPos] as { op: number, data: number[] }
203
+ if (!n.data) {
204
+ numberOfSignatures = 1 + (n.op as number) - OP.OP_1
205
+ } else {
206
+ const reader = new Utils.Reader(n.data)
207
+ const threshold = reader.readInt64LEBn()
208
+ numberOfSignatures = threshold.toNumber()
209
+ }
210
+ return Promise.resolve(staticLength + (numberOfPubkeys * 34) + (numberOfSignatures * 73))
211
+ }
212
+ }
213
+ }
214
+ }
package/src/OpReturn.ts CHANGED
@@ -5,53 +5,53 @@ import { OP, Script, ScriptTemplate, LockingScript, UnlockingScript, Transaction
5
5
  *
6
6
  * This class provides methods to create OpReturn scripts from data. Only lock script is available.
7
7
  */
8
- export default class OpReturn implements ScriptTemplate {
9
- /**
8
+ export class OpReturn implements ScriptTemplate {
9
+ /**
10
10
  * Creates an OpReturn script
11
11
  *
12
12
  * @param {string | string[] | number[]} data The data or array of data to push after OP_RETURN.
13
13
  * @param {('hex' | 'utf8' | 'base64')} enc The data encoding type, defaults to utf8.
14
14
  * @returns {LockingScript} - An OpReturn locking script.
15
15
  */
16
- lock(data: string | string[] | number[], enc?: 'hex' | 'utf8' | 'base64'): LockingScript {
17
- const script: { op: number, data?: number[] }[] = [
18
- { op: OP.OP_FALSE },
19
- { op: OP.OP_RETURN }
20
- ]
16
+ lock (data: string | string[] | number[], enc?: 'hex' | 'utf8' | 'base64'): LockingScript {
17
+ const script: Array<{ op: number, data?: number[] }> = [
18
+ { op: OP.OP_FALSE },
19
+ { op: OP.OP_RETURN }
20
+ ]
21
21
 
22
- if (typeof data === 'string') {
23
- data = [data]
24
- }
25
-
26
- if (data.length && typeof data[0] === 'number') {
27
- script.push({ op: data.length, data: data as number[] })
28
- } else {
29
- for (const entry of data.filter(Boolean)) {
30
- const arr = Utils.toArray(entry, enc)
31
- script.push({ op: arr.length, data: arr })
32
- }
33
- }
22
+ if (typeof data === 'string') {
23
+ data = [data]
24
+ }
34
25
 
35
- return new LockingScript(script)
26
+ if ((data.length > 0) && typeof data[0] === 'number') {
27
+ script.push({ op: data.length, data: data as number[] })
28
+ } else {
29
+ for (const entry of data.filter(Boolean)) {
30
+ const arr = Utils.toArray(entry, enc)
31
+ script.push({ op: arr.length, data: arr })
32
+ }
36
33
  }
37
34
 
38
- /**
35
+ return new LockingScript(script)
36
+ }
37
+
38
+ /**
39
39
  * Unlock method is not available for OpReturn scripts, throws exception.
40
40
  */
41
- unlock(): {
42
- sign: (tx: Transaction, inputIndex: number) => Promise<UnlockingScript>
43
- estimateLength: () => Promise<number>
44
- } {
45
- throw new Error('Unlock is not supported for OpReturn scripts')
46
- }
41
+ unlock (): {
42
+ sign: (tx: Transaction, inputIndex: number) => Promise<UnlockingScript>
43
+ estimateLength: () => Promise<number>
44
+ } {
45
+ throw new Error('Unlock is not supported for OpReturn scripts')
46
+ }
47
47
 
48
- /**
48
+ /**
49
49
  * Decodes an OpReturn script data to utf8
50
50
  * @param script The opreturn script
51
51
  * @returns An array of UTF8 encoded strings
52
52
  */
53
- static decode(script: Script): string[] {
54
- const tokens = script.toASM().split(' ').slice(2)
55
- return tokens.map(token => Utils.toUTF8(Utils.toArray(token, 'hex')))
56
- }
57
- }
53
+ static decode (script: Script): string[] {
54
+ const tokens = script.toASM().split(' ').slice(2)
55
+ return tokens.map(token => Utils.toUTF8(Utils.toArray(token, 'hex')))
56
+ }
57
+ }
package/src/Metanet.ts DELETED
@@ -1,49 +0,0 @@
1
- import { OP, PublicKey, ScriptTemplate, LockingScript, UnlockingScript, Transaction, Utils } from '@bsv/sdk'
2
-
3
- /**
4
- * Metanet class implementing ScriptTemplate.
5
- *
6
- * This class provides methods to create Metanet outputs from data. Only lock script is available.
7
- */
8
- export default class Metanet implements ScriptTemplate {
9
- /**
10
- * Creates a Metanet output script
11
- *
12
- * @param {PublicKey} pubkey the public key responsible for the metanet node
13
- * @param {string} parentTXID the TXID of the parent metanet transaction or null for root node
14
- * @param {string[]} data the output data, an array of metadata ending in data payload
15
- * @returns {LockingScript} - A Metanet locking script.
16
- *
17
- * @example
18
- * // creates a root metanet output with 'subprotocol' and 'filename' metadata followed by data
19
- * lock(pubkey, null, txid, ['subprotocol', 'filename', data ])
20
- */
21
- lock(pubkey: PublicKey, parentTXID: string | null, data: string[] | string = []): LockingScript {
22
- const script: { op: number, data?: number[] }[] = [
23
- { op: OP.OP_FALSE },
24
- { op: OP.OP_RETURN }
25
- ]
26
-
27
- const fields = [
28
- 'meta',
29
- pubkey.toString(),
30
- parentTXID || 'null'
31
- ].concat(data)
32
-
33
- for (const field of fields.filter(Boolean)) {
34
- script.push({ op: field.length, data: Utils.toArray(field) })
35
- }
36
-
37
- return new LockingScript(script)
38
- }
39
-
40
- /**
41
- * Unlock method is not available for Metanet scripts, throws exception.
42
- */
43
- unlock(): {
44
- sign: (tx: Transaction, inputIndex: number) => Promise<UnlockingScript>
45
- estimateLength: () => Promise<number>
46
- } {
47
- throw new Error('Unlock is not supported for Metanet scripts')
48
- }
49
- }
@@ -1,21 +0,0 @@
1
- import Metanet from '../Metanet'
2
- import { PrivateKey, Utils } from '@bsv/sdk'
3
-
4
- describe('Metanet template', () => {
5
- it('creates metanet output', () => {
6
- const priv = PrivateKey.fromRandom()
7
- const script = new Metanet().lock(priv.toPublicKey(), null, ['subprotocol', 'data'])
8
- const tokens = script.toASM().split(' ')
9
- expect(tokens[0]).toEqual('OP_0')
10
- expect(tokens[1]).toEqual('OP_RETURN')
11
- expect(Utils.toUTF8(Utils.toArray(tokens[2], 'hex'))).toEqual('meta')
12
- expect(Utils.toUTF8(Utils.toArray(tokens[3], 'hex'))).toEqual(priv.toPublicKey().toString())
13
- expect(Utils.toUTF8(Utils.toArray(tokens[4], 'hex'))).toEqual('null')
14
- expect(Utils.toUTF8(Utils.toArray(tokens[5], 'hex'))).toEqual('subprotocol')
15
- expect(Utils.toUTF8(Utils.toArray(tokens[6], 'hex'))).toEqual('data')
16
- })
17
-
18
- it('fails to create metanet input', () => {
19
- expect(() => new Metanet().unlock()).toThrow()
20
- })
21
- })