@bsv/templates 1.1.0 → 1.2.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/README.md +1 -0
- package/dist/cjs/mod.js +4 -40
- package/dist/cjs/mod.js.map +1 -1
- package/dist/cjs/package.json +6 -5
- package/dist/cjs/src/MultiPushDrop.js +2 -1
- package/dist/cjs/src/MultiPushDrop.js.map +1 -1
- package/dist/cjs/src/MultiSigPubkeyHash.js +173 -0
- package/dist/cjs/src/MultiSigPubkeyHash.js.map +1 -0
- package/dist/cjs/src/OpReturn.js +2 -1
- package/dist/cjs/src/OpReturn.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/mod.js +2 -3
- package/dist/esm/mod.js.map +1 -1
- package/dist/esm/src/MultiPushDrop.js +1 -1
- package/dist/esm/src/MultiPushDrop.js.map +1 -1
- package/dist/esm/src/MultiSigPubkeyHash.js +170 -0
- package/dist/esm/src/MultiSigPubkeyHash.js.map +1 -0
- package/dist/esm/src/OpReturn.js +1 -1
- package/dist/esm/src/OpReturn.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/mod.d.ts +2 -3
- package/dist/types/mod.d.ts.map +1 -1
- package/dist/types/src/MultiPushDrop.d.ts +1 -1
- package/dist/types/src/MultiPushDrop.d.ts.map +1 -1
- package/dist/types/src/MultiSigPubkeyHash.d.ts +24 -0
- package/dist/types/src/MultiSigPubkeyHash.d.ts.map +1 -0
- package/dist/types/src/OpReturn.d.ts +1 -1
- package/dist/types/src/OpReturn.d.ts.map +1 -1
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/mod.ts +3 -3
- package/package.json +6 -5
- package/src/MultiPushDrop.ts +1 -1
- package/src/MultiSigPubkeyHash.ts +214 -0
- package/src/OpReturn.ts +1 -1
- package/src/Metanet.ts +0 -49
- package/src/__tests/Metanet.test.ts +0 -21
- package/src/__tests/MultiPushDrop.test.ts +0 -256
- package/src/__tests/OpReturn.test.ts +0 -11
|
@@ -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,7 +5,7 @@ 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
|
|
8
|
+
export class OpReturn implements ScriptTemplate {
|
|
9
9
|
/**
|
|
10
10
|
* Creates an OpReturn script
|
|
11
11
|
*
|
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: Array<{ 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
|
-
})
|
|
@@ -1,256 +0,0 @@
|
|
|
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 +0,0 @@
|
|
|
1
|
-
import OpReturn from '../OpReturn'
|
|
2
|
-
|
|
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
|
-
})
|
|
11
|
-
})
|