@bitgo-beta/babylonlabs-io-btc-staking-ts 0.4.0-beta.73 → 0.4.0-beta.730
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 -1
- package/dist/index.cjs +2219 -1477
- package/dist/index.cjs.map +7 -0
- package/dist/index.d.cts +777 -490
- package/dist/index.d.ts +1235 -0
- package/dist/index.js +2196 -1452
- package/dist/index.js.map +7 -0
- package/package.json +9 -6
package/dist/index.js
CHANGED
|
@@ -1,231 +1,3 @@
|
|
|
1
|
-
// src/staking/stakingScript.ts
|
|
2
|
-
import { opcodes, script } from "bitcoinjs-lib";
|
|
3
|
-
|
|
4
|
-
// src/constants/keys.ts
|
|
5
|
-
var NO_COORD_PK_BYTE_LENGTH = 32;
|
|
6
|
-
|
|
7
|
-
// src/staking/stakingScript.ts
|
|
8
|
-
var MAGIC_BYTES_LEN = 4;
|
|
9
|
-
var StakingScriptData = class {
|
|
10
|
-
constructor(stakerKey, finalityProviderKeys, covenantKeys, covenantThreshold, stakingTimelock, unbondingTimelock) {
|
|
11
|
-
if (!stakerKey || !finalityProviderKeys || !covenantKeys || !covenantThreshold || !stakingTimelock || !unbondingTimelock) {
|
|
12
|
-
throw new Error("Missing required input values");
|
|
13
|
-
}
|
|
14
|
-
this.stakerKey = stakerKey;
|
|
15
|
-
this.finalityProviderKeys = finalityProviderKeys;
|
|
16
|
-
this.covenantKeys = covenantKeys;
|
|
17
|
-
this.covenantThreshold = covenantThreshold;
|
|
18
|
-
this.stakingTimeLock = stakingTimelock;
|
|
19
|
-
this.unbondingTimeLock = unbondingTimelock;
|
|
20
|
-
if (!this.validate()) {
|
|
21
|
-
throw new Error("Invalid script data provided");
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Validates the staking script.
|
|
26
|
-
* @returns {boolean} Returns true if the staking script is valid, otherwise false.
|
|
27
|
-
*/
|
|
28
|
-
validate() {
|
|
29
|
-
if (this.stakerKey.length != NO_COORD_PK_BYTE_LENGTH) {
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
if (this.finalityProviderKeys.some(
|
|
33
|
-
(finalityProviderKey) => finalityProviderKey.length != NO_COORD_PK_BYTE_LENGTH
|
|
34
|
-
)) {
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
37
|
-
if (this.covenantKeys.some((covenantKey) => covenantKey.length != NO_COORD_PK_BYTE_LENGTH)) {
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
|
-
const allPks = [
|
|
41
|
-
this.stakerKey,
|
|
42
|
-
...this.finalityProviderKeys,
|
|
43
|
-
...this.covenantKeys
|
|
44
|
-
];
|
|
45
|
-
const allPksSet = new Set(allPks);
|
|
46
|
-
if (allPks.length !== allPksSet.size) {
|
|
47
|
-
return false;
|
|
48
|
-
}
|
|
49
|
-
if (this.covenantThreshold == 0 || this.covenantThreshold > this.covenantKeys.length) {
|
|
50
|
-
return false;
|
|
51
|
-
}
|
|
52
|
-
if (this.stakingTimeLock == 0 || this.stakingTimeLock > 65535) {
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
55
|
-
if (this.unbondingTimeLock == 0 || this.unbondingTimeLock > 65535) {
|
|
56
|
-
return false;
|
|
57
|
-
}
|
|
58
|
-
return true;
|
|
59
|
-
}
|
|
60
|
-
// The staking script allows for multiple finality provider public keys
|
|
61
|
-
// to support (re)stake to multiple finality providers
|
|
62
|
-
// Covenant members are going to have multiple keys
|
|
63
|
-
/**
|
|
64
|
-
* Builds a timelock script.
|
|
65
|
-
* @param timelock - The timelock value to encode in the script.
|
|
66
|
-
* @returns {Buffer} containing the compiled timelock script.
|
|
67
|
-
*/
|
|
68
|
-
buildTimelockScript(timelock) {
|
|
69
|
-
return script.compile([
|
|
70
|
-
this.stakerKey,
|
|
71
|
-
opcodes.OP_CHECKSIGVERIFY,
|
|
72
|
-
script.number.encode(timelock),
|
|
73
|
-
opcodes.OP_CHECKSEQUENCEVERIFY
|
|
74
|
-
]);
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Builds the staking timelock script.
|
|
78
|
-
* Only holder of private key for given pubKey can spend after relative lock time
|
|
79
|
-
* Creates the timelock script in the form:
|
|
80
|
-
* <stakerPubKey>
|
|
81
|
-
* OP_CHECKSIGVERIFY
|
|
82
|
-
* <stakingTimeBlocks>
|
|
83
|
-
* OP_CHECKSEQUENCEVERIFY
|
|
84
|
-
* @returns {Buffer} The staking timelock script.
|
|
85
|
-
*/
|
|
86
|
-
buildStakingTimelockScript() {
|
|
87
|
-
return this.buildTimelockScript(this.stakingTimeLock);
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Builds the unbonding timelock script.
|
|
91
|
-
* Creates the unbonding timelock script in the form:
|
|
92
|
-
* <stakerPubKey>
|
|
93
|
-
* OP_CHECKSIGVERIFY
|
|
94
|
-
* <unbondingTimeBlocks>
|
|
95
|
-
* OP_CHECKSEQUENCEVERIFY
|
|
96
|
-
* @returns {Buffer} The unbonding timelock script.
|
|
97
|
-
*/
|
|
98
|
-
buildUnbondingTimelockScript() {
|
|
99
|
-
return this.buildTimelockScript(this.unbondingTimeLock);
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Builds the unbonding script in the form:
|
|
103
|
-
* buildSingleKeyScript(stakerPk, true) ||
|
|
104
|
-
* buildMultiKeyScript(covenantPks, covenantThreshold, false)
|
|
105
|
-
* || means combining the scripts
|
|
106
|
-
* @returns {Buffer} The unbonding script.
|
|
107
|
-
*/
|
|
108
|
-
buildUnbondingScript() {
|
|
109
|
-
return Buffer.concat([
|
|
110
|
-
this.buildSingleKeyScript(this.stakerKey, true),
|
|
111
|
-
this.buildMultiKeyScript(
|
|
112
|
-
this.covenantKeys,
|
|
113
|
-
this.covenantThreshold,
|
|
114
|
-
false
|
|
115
|
-
)
|
|
116
|
-
]);
|
|
117
|
-
}
|
|
118
|
-
/**
|
|
119
|
-
* Builds the slashing script for staking in the form:
|
|
120
|
-
* buildSingleKeyScript(stakerPk, true) ||
|
|
121
|
-
* buildMultiKeyScript(finalityProviderPKs, 1, true) ||
|
|
122
|
-
* buildMultiKeyScript(covenantPks, covenantThreshold, false)
|
|
123
|
-
* || means combining the scripts
|
|
124
|
-
* The slashing script is a combination of single-key and multi-key scripts.
|
|
125
|
-
* The single-key script is used for staker key verification.
|
|
126
|
-
* The multi-key script is used for finality provider key verification and covenant key verification.
|
|
127
|
-
* @returns {Buffer} The slashing script as a Buffer.
|
|
128
|
-
*/
|
|
129
|
-
buildSlashingScript() {
|
|
130
|
-
return Buffer.concat([
|
|
131
|
-
this.buildSingleKeyScript(this.stakerKey, true),
|
|
132
|
-
this.buildMultiKeyScript(
|
|
133
|
-
this.finalityProviderKeys,
|
|
134
|
-
// The threshold is always 1 as we only need one
|
|
135
|
-
// finalityProvider signature to perform slashing
|
|
136
|
-
// (only one finalityProvider performs an offence)
|
|
137
|
-
1,
|
|
138
|
-
// OP_VERIFY/OP_CHECKSIGVERIFY is added at the end
|
|
139
|
-
true
|
|
140
|
-
),
|
|
141
|
-
this.buildMultiKeyScript(
|
|
142
|
-
this.covenantKeys,
|
|
143
|
-
this.covenantThreshold,
|
|
144
|
-
// No need to add verify since covenants are at the end of the script
|
|
145
|
-
false
|
|
146
|
-
)
|
|
147
|
-
]);
|
|
148
|
-
}
|
|
149
|
-
/**
|
|
150
|
-
* Builds the staking scripts.
|
|
151
|
-
* @returns {StakingScripts} The staking scripts.
|
|
152
|
-
*/
|
|
153
|
-
buildScripts() {
|
|
154
|
-
return {
|
|
155
|
-
timelockScript: this.buildStakingTimelockScript(),
|
|
156
|
-
unbondingScript: this.buildUnbondingScript(),
|
|
157
|
-
slashingScript: this.buildSlashingScript(),
|
|
158
|
-
unbondingTimelockScript: this.buildUnbondingTimelockScript()
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
// buildSingleKeyScript and buildMultiKeyScript allow us to reuse functionality
|
|
162
|
-
// for creating Bitcoin scripts for the unbonding script and the slashing script
|
|
163
|
-
/**
|
|
164
|
-
* Builds a single key script in the form:
|
|
165
|
-
* buildSingleKeyScript creates a single key script
|
|
166
|
-
* <pk> OP_CHECKSIGVERIFY (if withVerify is true)
|
|
167
|
-
* <pk> OP_CHECKSIG (if withVerify is false)
|
|
168
|
-
* @param pk - The public key buffer.
|
|
169
|
-
* @param withVerify - A boolean indicating whether to include the OP_CHECKSIGVERIFY opcode.
|
|
170
|
-
* @returns The compiled script buffer.
|
|
171
|
-
*/
|
|
172
|
-
buildSingleKeyScript(pk, withVerify) {
|
|
173
|
-
if (pk.length != NO_COORD_PK_BYTE_LENGTH) {
|
|
174
|
-
throw new Error("Invalid key length");
|
|
175
|
-
}
|
|
176
|
-
return script.compile([
|
|
177
|
-
pk,
|
|
178
|
-
withVerify ? opcodes.OP_CHECKSIGVERIFY : opcodes.OP_CHECKSIG
|
|
179
|
-
]);
|
|
180
|
-
}
|
|
181
|
-
/**
|
|
182
|
-
* Builds a multi-key script in the form:
|
|
183
|
-
* <pk1> OP_CHEKCSIG <pk2> OP_CHECKSIGADD <pk3> OP_CHECKSIGADD ... <pkN> OP_CHECKSIGADD <threshold> OP_NUMEQUAL
|
|
184
|
-
* <withVerify -> OP_NUMEQUALVERIFY>
|
|
185
|
-
* It validates whether provided keys are unique and the threshold is not greater than number of keys
|
|
186
|
-
* If there is only one key provided it will return single key sig script
|
|
187
|
-
* @param pks - An array of public keys.
|
|
188
|
-
* @param threshold - The required number of valid signers.
|
|
189
|
-
* @param withVerify - A boolean indicating whether to include the OP_VERIFY opcode.
|
|
190
|
-
* @returns The compiled multi-key script as a Buffer.
|
|
191
|
-
* @throws {Error} If no keys are provided, if the required number of valid signers is greater than the number of provided keys, or if duplicate keys are provided.
|
|
192
|
-
*/
|
|
193
|
-
buildMultiKeyScript(pks, threshold, withVerify) {
|
|
194
|
-
if (!pks || pks.length === 0) {
|
|
195
|
-
throw new Error("No keys provided");
|
|
196
|
-
}
|
|
197
|
-
if (pks.some((pk) => pk.length != NO_COORD_PK_BYTE_LENGTH)) {
|
|
198
|
-
throw new Error("Invalid key length");
|
|
199
|
-
}
|
|
200
|
-
if (threshold > pks.length) {
|
|
201
|
-
throw new Error(
|
|
202
|
-
"Required number of valid signers is greater than number of provided keys"
|
|
203
|
-
);
|
|
204
|
-
}
|
|
205
|
-
if (pks.length === 1) {
|
|
206
|
-
return this.buildSingleKeyScript(pks[0], withVerify);
|
|
207
|
-
}
|
|
208
|
-
const sortedPks = [...pks].sort(Buffer.compare);
|
|
209
|
-
for (let i = 0; i < sortedPks.length - 1; ++i) {
|
|
210
|
-
if (sortedPks[i].equals(sortedPks[i + 1])) {
|
|
211
|
-
throw new Error("Duplicate keys provided");
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
const scriptElements = [sortedPks[0], opcodes.OP_CHECKSIG];
|
|
215
|
-
for (let i = 1; i < sortedPks.length; i++) {
|
|
216
|
-
scriptElements.push(sortedPks[i]);
|
|
217
|
-
scriptElements.push(opcodes.OP_CHECKSIGADD);
|
|
218
|
-
}
|
|
219
|
-
scriptElements.push(script.number.encode(threshold));
|
|
220
|
-
if (withVerify) {
|
|
221
|
-
scriptElements.push(opcodes.OP_NUMEQUALVERIFY);
|
|
222
|
-
} else {
|
|
223
|
-
scriptElements.push(opcodes.OP_NUMEQUAL);
|
|
224
|
-
}
|
|
225
|
-
return script.compile(scriptElements);
|
|
226
|
-
}
|
|
227
|
-
};
|
|
228
|
-
|
|
229
1
|
// src/error/index.ts
|
|
230
2
|
var StakingError = class _StakingError extends Error {
|
|
231
3
|
constructor(code, message) {
|
|
@@ -244,19 +16,14 @@ var StakingError = class _StakingError extends Error {
|
|
|
244
16
|
}
|
|
245
17
|
};
|
|
246
18
|
|
|
247
|
-
// src/
|
|
248
|
-
import
|
|
249
|
-
|
|
250
|
-
// src/constants/dustSat.ts
|
|
251
|
-
var BTC_DUST_SAT = 546;
|
|
19
|
+
// src/utils/btc.ts
|
|
20
|
+
import * as ecc from "@bitcoin-js/tiny-secp256k1-asmjs";
|
|
21
|
+
import { address, initEccLib, networks } from "bitcoinjs-lib";
|
|
252
22
|
|
|
253
|
-
// src/constants/
|
|
254
|
-
var
|
|
255
|
-
var internalPubkey = Buffer.from(key, "hex").subarray(1, 33);
|
|
23
|
+
// src/constants/keys.ts
|
|
24
|
+
var NO_COORD_PK_BYTE_LENGTH = 32;
|
|
256
25
|
|
|
257
26
|
// src/utils/btc.ts
|
|
258
|
-
import * as ecc from "@bitcoin-js/tiny-secp256k1-asmjs";
|
|
259
|
-
import { initEccLib, address, networks } from "bitcoinjs-lib";
|
|
260
27
|
var initBTCCurve = () => {
|
|
261
28
|
initEccLib(ecc);
|
|
262
29
|
};
|
|
@@ -273,14 +40,28 @@ var isTaproot = (taprootAddress, network) => {
|
|
|
273
40
|
if (decoded.version !== 1) {
|
|
274
41
|
return false;
|
|
275
42
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
43
|
+
if (network.bech32 === networks.bitcoin.bech32) {
|
|
44
|
+
return taprootAddress.startsWith("bc1p");
|
|
45
|
+
} else if (network.bech32 === networks.testnet.bech32) {
|
|
46
|
+
return taprootAddress.startsWith("tb1p") || taprootAddress.startsWith("sb1p");
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
var isNativeSegwit = (segwitAddress, network) => {
|
|
54
|
+
try {
|
|
55
|
+
const decoded = address.fromBech32(segwitAddress);
|
|
56
|
+
if (decoded.version !== 0) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
if (network.bech32 === networks.bitcoin.bech32) {
|
|
60
|
+
return segwitAddress.startsWith("bc1q");
|
|
61
|
+
} else if (network.bech32 === networks.testnet.bech32) {
|
|
62
|
+
return segwitAddress.startsWith("tb1q");
|
|
283
63
|
}
|
|
64
|
+
return false;
|
|
284
65
|
} catch (error) {
|
|
285
66
|
return false;
|
|
286
67
|
}
|
|
@@ -316,124 +97,12 @@ var transactionIdToHash = (txId) => {
|
|
|
316
97
|
return Buffer.from(txId, "hex").reverse();
|
|
317
98
|
};
|
|
318
99
|
|
|
319
|
-
// src/utils/
|
|
320
|
-
import {
|
|
100
|
+
// src/utils/staking/index.ts
|
|
101
|
+
import { address as address2, payments } from "bitcoinjs-lib";
|
|
321
102
|
|
|
322
|
-
// src/constants/
|
|
323
|
-
var
|
|
324
|
-
var
|
|
325
|
-
var P2TR_INPUT_SIZE = 58;
|
|
326
|
-
var TX_BUFFER_SIZE_OVERHEAD = 11;
|
|
327
|
-
var LOW_RATE_ESTIMATION_ACCURACY_BUFFER = 30;
|
|
328
|
-
var MAX_NON_LEGACY_OUTPUT_SIZE = 43;
|
|
329
|
-
var WITHDRAW_TX_BUFFER_SIZE = 17;
|
|
330
|
-
var WALLET_RELAY_FEE_RATE_THRESHOLD = 2;
|
|
331
|
-
var OP_RETURN_OUTPUT_VALUE_SIZE = 8;
|
|
332
|
-
var OP_RETURN_VALUE_SERIALIZE_SIZE = 1;
|
|
333
|
-
|
|
334
|
-
// src/utils/fee/utils.ts
|
|
335
|
-
import { script as bitcoinScript, opcodes as opcodes2, payments } from "bitcoinjs-lib";
|
|
336
|
-
var isOP_RETURN = (script4) => {
|
|
337
|
-
const decompiled = bitcoinScript.decompile(script4);
|
|
338
|
-
return !!decompiled && decompiled[0] === opcodes2.OP_RETURN;
|
|
339
|
-
};
|
|
340
|
-
var getInputSizeByScript = (script4) => {
|
|
341
|
-
try {
|
|
342
|
-
const { address: p2wpkhAddress } = payments.p2wpkh({
|
|
343
|
-
output: script4
|
|
344
|
-
});
|
|
345
|
-
if (p2wpkhAddress) {
|
|
346
|
-
return P2WPKH_INPUT_SIZE;
|
|
347
|
-
}
|
|
348
|
-
} catch (error) {
|
|
349
|
-
}
|
|
350
|
-
try {
|
|
351
|
-
const { address: p2trAddress } = payments.p2tr({
|
|
352
|
-
output: script4
|
|
353
|
-
});
|
|
354
|
-
if (p2trAddress) {
|
|
355
|
-
return P2TR_INPUT_SIZE;
|
|
356
|
-
}
|
|
357
|
-
} catch (error) {
|
|
358
|
-
}
|
|
359
|
-
return DEFAULT_INPUT_SIZE;
|
|
360
|
-
};
|
|
361
|
-
var getEstimatedChangeOutputSize = () => {
|
|
362
|
-
return MAX_NON_LEGACY_OUTPUT_SIZE;
|
|
363
|
-
};
|
|
364
|
-
var inputValueSum = (inputUTXOs) => {
|
|
365
|
-
return inputUTXOs.reduce((acc, utxo) => acc + utxo.value, 0);
|
|
366
|
-
};
|
|
367
|
-
|
|
368
|
-
// src/utils/fee/index.ts
|
|
369
|
-
var getStakingTxInputUTXOsAndFees = (availableUTXOs, stakingAmount, feeRate, outputs) => {
|
|
370
|
-
if (availableUTXOs.length === 0) {
|
|
371
|
-
throw new Error("Insufficient funds");
|
|
372
|
-
}
|
|
373
|
-
const validUTXOs = availableUTXOs.filter((utxo) => {
|
|
374
|
-
const script4 = Buffer.from(utxo.scriptPubKey, "hex");
|
|
375
|
-
return !!bitcoinScript2.decompile(script4);
|
|
376
|
-
});
|
|
377
|
-
if (validUTXOs.length === 0) {
|
|
378
|
-
throw new Error("Insufficient funds: no valid UTXOs available for staking");
|
|
379
|
-
}
|
|
380
|
-
const sortedUTXOs = validUTXOs.sort((a, b) => b.value - a.value);
|
|
381
|
-
const selectedUTXOs = [];
|
|
382
|
-
let accumulatedValue = 0;
|
|
383
|
-
let estimatedFee = 0;
|
|
384
|
-
for (const utxo of sortedUTXOs) {
|
|
385
|
-
selectedUTXOs.push(utxo);
|
|
386
|
-
accumulatedValue += utxo.value;
|
|
387
|
-
const estimatedSize = getEstimatedSize(selectedUTXOs, outputs);
|
|
388
|
-
estimatedFee = estimatedSize * feeRate + rateBasedTxBufferFee(feeRate);
|
|
389
|
-
if (accumulatedValue - (stakingAmount + estimatedFee) > BTC_DUST_SAT) {
|
|
390
|
-
estimatedFee += getEstimatedChangeOutputSize() * feeRate;
|
|
391
|
-
}
|
|
392
|
-
if (accumulatedValue >= stakingAmount + estimatedFee) {
|
|
393
|
-
break;
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
if (accumulatedValue < stakingAmount + estimatedFee) {
|
|
397
|
-
throw new Error(
|
|
398
|
-
"Insufficient funds: unable to gather enough UTXOs to cover the staking amount and fees"
|
|
399
|
-
);
|
|
400
|
-
}
|
|
401
|
-
return {
|
|
402
|
-
selectedUTXOs,
|
|
403
|
-
fee: estimatedFee
|
|
404
|
-
};
|
|
405
|
-
};
|
|
406
|
-
var getWithdrawTxFee = (feeRate) => {
|
|
407
|
-
const inputSize = P2TR_INPUT_SIZE;
|
|
408
|
-
const outputSize = getEstimatedChangeOutputSize();
|
|
409
|
-
return feeRate * (inputSize + outputSize + TX_BUFFER_SIZE_OVERHEAD + WITHDRAW_TX_BUFFER_SIZE) + rateBasedTxBufferFee(feeRate);
|
|
410
|
-
};
|
|
411
|
-
var getEstimatedSize = (inputUtxos, outputs) => {
|
|
412
|
-
const inputSize = inputUtxos.reduce((acc, u) => {
|
|
413
|
-
const script4 = Buffer.from(u.scriptPubKey, "hex");
|
|
414
|
-
const decompiledScript = bitcoinScript2.decompile(script4);
|
|
415
|
-
if (!decompiledScript) {
|
|
416
|
-
return acc;
|
|
417
|
-
}
|
|
418
|
-
return acc + getInputSizeByScript(script4);
|
|
419
|
-
}, 0);
|
|
420
|
-
const outputSize = outputs.reduce((acc, output) => {
|
|
421
|
-
if (isOP_RETURN(output.scriptPubKey)) {
|
|
422
|
-
return acc + output.scriptPubKey.length + OP_RETURN_OUTPUT_VALUE_SIZE + OP_RETURN_VALUE_SERIALIZE_SIZE;
|
|
423
|
-
}
|
|
424
|
-
return acc + MAX_NON_LEGACY_OUTPUT_SIZE;
|
|
425
|
-
}, 0);
|
|
426
|
-
return inputSize + outputSize + TX_BUFFER_SIZE_OVERHEAD;
|
|
427
|
-
};
|
|
428
|
-
var rateBasedTxBufferFee = (feeRate) => {
|
|
429
|
-
return feeRate <= WALLET_RELAY_FEE_RATE_THRESHOLD ? LOW_RATE_ESTIMATION_ACCURACY_BUFFER : 0;
|
|
430
|
-
};
|
|
431
|
-
|
|
432
|
-
// src/utils/staking/index.ts
|
|
433
|
-
import { address as address2, payments as payments2 } from "bitcoinjs-lib";
|
|
434
|
-
|
|
435
|
-
// src/constants/unbonding.ts
|
|
436
|
-
var MIN_UNBONDING_OUTPUT_VALUE = 1e3;
|
|
103
|
+
// src/constants/internalPubkey.ts
|
|
104
|
+
var key = "0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0";
|
|
105
|
+
var internalPubkey = Buffer.from(key, "hex").subarray(1, 33);
|
|
437
106
|
|
|
438
107
|
// src/utils/staking/index.ts
|
|
439
108
|
var buildStakingTransactionOutputs = (scripts, network, amount) => {
|
|
@@ -459,7 +128,7 @@ var deriveStakingOutputInfo = (scripts, network) => {
|
|
|
459
128
|
},
|
|
460
129
|
[{ output: scripts.unbondingScript }, { output: scripts.timelockScript }]
|
|
461
130
|
];
|
|
462
|
-
const stakingOutput =
|
|
131
|
+
const stakingOutput = payments.p2tr({
|
|
463
132
|
internalPubkey,
|
|
464
133
|
scriptTree,
|
|
465
134
|
network
|
|
@@ -482,7 +151,7 @@ var deriveUnbondingOutputInfo = (scripts, network) => {
|
|
|
482
151
|
},
|
|
483
152
|
{ output: scripts.unbondingTimelockScript }
|
|
484
153
|
];
|
|
485
|
-
const unbondingOutput =
|
|
154
|
+
const unbondingOutput = payments.p2tr({
|
|
486
155
|
internalPubkey,
|
|
487
156
|
scriptTree: outputScriptTree,
|
|
488
157
|
network
|
|
@@ -499,7 +168,7 @@ var deriveUnbondingOutputInfo = (scripts, network) => {
|
|
|
499
168
|
};
|
|
500
169
|
};
|
|
501
170
|
var deriveSlashingOutput = (scripts, network) => {
|
|
502
|
-
const slashingOutput =
|
|
171
|
+
const slashingOutput = payments.p2tr({
|
|
503
172
|
internalPubkey,
|
|
504
173
|
scriptTree: { output: scripts.unbondingTimelockScript },
|
|
505
174
|
network
|
|
@@ -518,7 +187,11 @@ var deriveSlashingOutput = (scripts, network) => {
|
|
|
518
187
|
};
|
|
519
188
|
var findMatchingTxOutputIndex = (tx, outputAddress, network) => {
|
|
520
189
|
const index = tx.outs.findIndex((output) => {
|
|
521
|
-
|
|
190
|
+
try {
|
|
191
|
+
return address2.fromOutputScript(output.script, network) === outputAddress;
|
|
192
|
+
} catch (error) {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
522
195
|
});
|
|
523
196
|
if (index === -1) {
|
|
524
197
|
throw new StakingError(
|
|
@@ -528,694 +201,1302 @@ var findMatchingTxOutputIndex = (tx, outputAddress, network) => {
|
|
|
528
201
|
}
|
|
529
202
|
return index;
|
|
530
203
|
};
|
|
531
|
-
var
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
}
|
|
538
|
-
if (timelock < params.minStakingTimeBlocks || timelock > params.maxStakingTimeBlocks) {
|
|
539
|
-
throw new StakingError(
|
|
540
|
-
"INVALID_INPUT" /* INVALID_INPUT */,
|
|
541
|
-
"Invalid timelock"
|
|
542
|
-
);
|
|
543
|
-
}
|
|
544
|
-
if (inputUTXOs.length == 0) {
|
|
545
|
-
throw new StakingError(
|
|
546
|
-
"INVALID_INPUT" /* INVALID_INPUT */,
|
|
547
|
-
"No input UTXOs provided"
|
|
548
|
-
);
|
|
549
|
-
}
|
|
550
|
-
if (feeRate <= 0) {
|
|
551
|
-
throw new StakingError(
|
|
204
|
+
var toBuffers = (inputs) => {
|
|
205
|
+
try {
|
|
206
|
+
return inputs.map((i) => Buffer.from(i, "hex"));
|
|
207
|
+
} catch (error) {
|
|
208
|
+
throw StakingError.fromUnknown(
|
|
209
|
+
error,
|
|
552
210
|
"INVALID_INPUT" /* INVALID_INPUT */,
|
|
553
|
-
"
|
|
211
|
+
"Cannot convert values to buffers"
|
|
554
212
|
);
|
|
555
213
|
}
|
|
556
214
|
};
|
|
557
|
-
var
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
"Could not find any covenant public keys"
|
|
562
|
-
);
|
|
563
|
-
}
|
|
564
|
-
if (params.covenantNoCoordPks.length < params.covenantQuorum) {
|
|
565
|
-
throw new StakingError(
|
|
566
|
-
"INVALID_PARAMS" /* INVALID_PARAMS */,
|
|
567
|
-
"Covenant public keys must be greater than or equal to the quorum"
|
|
568
|
-
);
|
|
569
|
-
}
|
|
570
|
-
params.covenantNoCoordPks.forEach((pk) => {
|
|
571
|
-
if (!isValidNoCoordPublicKey(pk)) {
|
|
572
|
-
throw new StakingError(
|
|
573
|
-
"INVALID_PARAMS" /* INVALID_PARAMS */,
|
|
574
|
-
"Covenant public key should contains no coordinate"
|
|
575
|
-
);
|
|
576
|
-
}
|
|
215
|
+
var clearTxSignatures = (tx) => {
|
|
216
|
+
tx.ins.forEach((input) => {
|
|
217
|
+
input.script = Buffer.alloc(0);
|
|
218
|
+
input.witness = [];
|
|
577
219
|
});
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
);
|
|
220
|
+
return tx;
|
|
221
|
+
};
|
|
222
|
+
var deriveMerkleProof = (merkle) => {
|
|
223
|
+
const proofHex = merkle.reduce((acc, m) => {
|
|
224
|
+
return acc + Buffer.from(m, "hex").reverse().toString("hex");
|
|
225
|
+
}, "");
|
|
226
|
+
return proofHex;
|
|
227
|
+
};
|
|
228
|
+
var extractFirstSchnorrSignatureFromTransaction = (singedTransaction) => {
|
|
229
|
+
for (const input of singedTransaction.ins) {
|
|
230
|
+
if (input.witness && input.witness.length > 0) {
|
|
231
|
+
const schnorrSignature = input.witness[0];
|
|
232
|
+
if (schnorrSignature.length === 64) {
|
|
233
|
+
return schnorrSignature;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
583
236
|
}
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
237
|
+
return void 0;
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
// src/staking/psbt.ts
|
|
241
|
+
import { Psbt, payments as payments3 } from "bitcoinjs-lib";
|
|
242
|
+
|
|
243
|
+
// src/constants/transaction.ts
|
|
244
|
+
var REDEEM_VERSION = 192;
|
|
245
|
+
|
|
246
|
+
// src/utils/utxo/findInputUTXO.ts
|
|
247
|
+
var findInputUTXO = (inputUTXOs, input) => {
|
|
248
|
+
const inputUTXO = inputUTXOs.find(
|
|
249
|
+
(u) => transactionIdToHash(u.txid).toString("hex") === input.hash.toString("hex") && u.vout === input.index
|
|
250
|
+
);
|
|
251
|
+
if (!inputUTXO) {
|
|
252
|
+
throw new Error(
|
|
253
|
+
`Input UTXO not found for txid: ${Buffer.from(input.hash).reverse().toString("hex")} and vout: ${input.index}`
|
|
588
254
|
);
|
|
589
255
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
256
|
+
return inputUTXO;
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
// src/utils/utxo/getScriptType.ts
|
|
260
|
+
import { payments as payments2 } from "bitcoinjs-lib";
|
|
261
|
+
var BitcoinScriptType = /* @__PURE__ */ ((BitcoinScriptType2) => {
|
|
262
|
+
BitcoinScriptType2["P2PKH"] = "pubkeyhash";
|
|
263
|
+
BitcoinScriptType2["P2SH"] = "scripthash";
|
|
264
|
+
BitcoinScriptType2["P2WPKH"] = "witnesspubkeyhash";
|
|
265
|
+
BitcoinScriptType2["P2WSH"] = "witnessscripthash";
|
|
266
|
+
BitcoinScriptType2["P2TR"] = "taproot";
|
|
267
|
+
return BitcoinScriptType2;
|
|
268
|
+
})(BitcoinScriptType || {});
|
|
269
|
+
var getScriptType = (script4) => {
|
|
270
|
+
try {
|
|
271
|
+
payments2.p2pkh({ output: script4 });
|
|
272
|
+
return "pubkeyhash" /* P2PKH */;
|
|
273
|
+
} catch {
|
|
595
274
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
);
|
|
275
|
+
try {
|
|
276
|
+
payments2.p2sh({ output: script4 });
|
|
277
|
+
return "scripthash" /* P2SH */;
|
|
278
|
+
} catch {
|
|
601
279
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
);
|
|
280
|
+
try {
|
|
281
|
+
payments2.p2wpkh({ output: script4 });
|
|
282
|
+
return "witnesspubkeyhash" /* P2WPKH */;
|
|
283
|
+
} catch {
|
|
607
284
|
}
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
);
|
|
285
|
+
try {
|
|
286
|
+
payments2.p2wsh({ output: script4 });
|
|
287
|
+
return "witnessscripthash" /* P2WSH */;
|
|
288
|
+
} catch {
|
|
613
289
|
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
);
|
|
290
|
+
try {
|
|
291
|
+
payments2.p2tr({ output: script4 });
|
|
292
|
+
return "taproot" /* P2TR */;
|
|
293
|
+
} catch {
|
|
619
294
|
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
295
|
+
throw new Error("Unknown script type");
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
// src/utils/utxo/getPsbtInputFields.ts
|
|
299
|
+
var getPsbtInputFields = (utxo, publicKeyNoCoord) => {
|
|
300
|
+
const scriptPubKey = Buffer.from(utxo.scriptPubKey, "hex");
|
|
301
|
+
const type = getScriptType(scriptPubKey);
|
|
302
|
+
switch (type) {
|
|
303
|
+
case "pubkeyhash" /* P2PKH */: {
|
|
304
|
+
if (!utxo.rawTxHex) {
|
|
305
|
+
throw new Error("Missing rawTxHex for legacy P2PKH input");
|
|
306
|
+
}
|
|
307
|
+
return { nonWitnessUtxo: Buffer.from(utxo.rawTxHex, "hex") };
|
|
626
308
|
}
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
"
|
|
630
|
-
|
|
631
|
-
)
|
|
309
|
+
case "scripthash" /* P2SH */: {
|
|
310
|
+
if (!utxo.rawTxHex) {
|
|
311
|
+
throw new Error("Missing rawTxHex for P2SH input");
|
|
312
|
+
}
|
|
313
|
+
if (!utxo.redeemScript) {
|
|
314
|
+
throw new Error("Missing redeemScript for P2SH input");
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
nonWitnessUtxo: Buffer.from(utxo.rawTxHex, "hex"),
|
|
318
|
+
redeemScript: Buffer.from(utxo.redeemScript, "hex")
|
|
319
|
+
};
|
|
632
320
|
}
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
321
|
+
case "witnesspubkeyhash" /* P2WPKH */: {
|
|
322
|
+
return {
|
|
323
|
+
witnessUtxo: {
|
|
324
|
+
script: scriptPubKey,
|
|
325
|
+
value: utxo.value
|
|
326
|
+
}
|
|
327
|
+
};
|
|
638
328
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
"
|
|
642
|
-
|
|
643
|
-
|
|
329
|
+
case "witnessscripthash" /* P2WSH */: {
|
|
330
|
+
if (!utxo.witnessScript) {
|
|
331
|
+
throw new Error("Missing witnessScript for P2WSH input");
|
|
332
|
+
}
|
|
333
|
+
return {
|
|
334
|
+
witnessUtxo: {
|
|
335
|
+
script: scriptPubKey,
|
|
336
|
+
value: utxo.value
|
|
337
|
+
},
|
|
338
|
+
witnessScript: Buffer.from(utxo.witnessScript, "hex")
|
|
339
|
+
};
|
|
644
340
|
}
|
|
341
|
+
case "taproot" /* P2TR */: {
|
|
342
|
+
return {
|
|
343
|
+
witnessUtxo: {
|
|
344
|
+
script: scriptPubKey,
|
|
345
|
+
value: utxo.value
|
|
346
|
+
},
|
|
347
|
+
// this is needed only if the wallet is in taproot mode
|
|
348
|
+
...publicKeyNoCoord && { tapInternalKey: publicKeyNoCoord }
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
default:
|
|
352
|
+
throw new Error(`Unsupported script type: ${type}`);
|
|
645
353
|
}
|
|
646
354
|
};
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
);
|
|
653
|
-
}
|
|
654
|
-
};
|
|
655
|
-
var toBuffers = (inputs) => {
|
|
656
|
-
try {
|
|
657
|
-
return inputs.map(
|
|
658
|
-
(i) => Buffer.from(i, "hex")
|
|
659
|
-
);
|
|
660
|
-
} catch (error) {
|
|
661
|
-
throw StakingError.fromUnknown(
|
|
662
|
-
error,
|
|
663
|
-
"INVALID_INPUT" /* INVALID_INPUT */,
|
|
664
|
-
"Cannot convert values to buffers"
|
|
665
|
-
);
|
|
355
|
+
|
|
356
|
+
// src/staking/psbt.ts
|
|
357
|
+
var stakingPsbt = (stakingTx, network, inputUTXOs, publicKeyNoCoord) => {
|
|
358
|
+
if (publicKeyNoCoord && publicKeyNoCoord.length !== NO_COORD_PK_BYTE_LENGTH) {
|
|
359
|
+
throw new Error("Invalid public key");
|
|
666
360
|
}
|
|
361
|
+
const psbt = new Psbt({ network });
|
|
362
|
+
if (stakingTx.version !== void 0) psbt.setVersion(stakingTx.version);
|
|
363
|
+
if (stakingTx.locktime !== void 0) psbt.setLocktime(stakingTx.locktime);
|
|
364
|
+
stakingTx.ins.forEach((input) => {
|
|
365
|
+
const inputUTXO = findInputUTXO(inputUTXOs, input);
|
|
366
|
+
const psbtInputData = getPsbtInputFields(inputUTXO, publicKeyNoCoord);
|
|
367
|
+
psbt.addInput({
|
|
368
|
+
hash: input.hash,
|
|
369
|
+
index: input.index,
|
|
370
|
+
sequence: input.sequence,
|
|
371
|
+
...psbtInputData
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
stakingTx.outs.forEach((o) => {
|
|
375
|
+
psbt.addOutput({ script: o.script, value: o.value });
|
|
376
|
+
});
|
|
377
|
+
return psbt;
|
|
667
378
|
};
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
var REDEEM_VERSION = 192;
|
|
675
|
-
|
|
676
|
-
// src/staking/transactions.ts
|
|
677
|
-
var BTC_LOCKTIME_HEIGHT_TIME_CUTOFF = 5e8;
|
|
678
|
-
function stakingTransaction(scripts, amount, changeAddress, inputUTXOs, network, feeRate, lockHeight) {
|
|
679
|
-
if (amount <= 0 || feeRate <= 0) {
|
|
680
|
-
throw new Error("Amount and fee rate must be bigger than 0");
|
|
379
|
+
var stakingExpansionPsbt = (network, stakingTx, previousStakingTxInfo, inputUTXOs, previousScripts, publicKeyNoCoord) => {
|
|
380
|
+
const psbt = new Psbt({ network });
|
|
381
|
+
if (stakingTx.version !== void 0) psbt.setVersion(stakingTx.version);
|
|
382
|
+
if (stakingTx.locktime !== void 0) psbt.setLocktime(stakingTx.locktime);
|
|
383
|
+
if (publicKeyNoCoord && publicKeyNoCoord.length !== NO_COORD_PK_BYTE_LENGTH) {
|
|
384
|
+
throw new Error("Invalid public key");
|
|
681
385
|
}
|
|
682
|
-
|
|
683
|
-
|
|
386
|
+
const previousStakingOutput = previousStakingTxInfo.stakingTx.outs[previousStakingTxInfo.outputIndex];
|
|
387
|
+
if (!previousStakingOutput) {
|
|
388
|
+
throw new Error("Previous staking output not found");
|
|
684
389
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
amount,
|
|
689
|
-
feeRate,
|
|
690
|
-
stakingOutputs
|
|
691
|
-
);
|
|
692
|
-
const tx = new Transaction2();
|
|
693
|
-
tx.version = TRANSACTION_VERSION;
|
|
694
|
-
for (let i = 0; i < selectedUTXOs.length; ++i) {
|
|
695
|
-
const input = selectedUTXOs[i];
|
|
696
|
-
tx.addInput(
|
|
697
|
-
transactionIdToHash(input.txid),
|
|
698
|
-
input.vout,
|
|
699
|
-
NON_RBF_SEQUENCE
|
|
700
|
-
);
|
|
390
|
+
;
|
|
391
|
+
if (getScriptType(previousStakingOutput.script) !== "taproot" /* P2TR */) {
|
|
392
|
+
throw new Error("Previous staking output script type is not P2TR");
|
|
701
393
|
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
const inputsSum = inputValueSum(selectedUTXOs);
|
|
706
|
-
if (inputsSum - (amount + fee) > BTC_DUST_SAT) {
|
|
707
|
-
tx.addOutput(
|
|
708
|
-
address3.toOutputScript(changeAddress, network),
|
|
709
|
-
inputsSum - (amount + fee)
|
|
394
|
+
if (stakingTx.ins.length !== 2) {
|
|
395
|
+
throw new Error(
|
|
396
|
+
"Staking expansion transaction must have exactly 2 inputs"
|
|
710
397
|
);
|
|
711
398
|
}
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
399
|
+
const txInputs = stakingTx.ins;
|
|
400
|
+
if (Buffer.from(txInputs[0].hash).reverse().toString("hex") !== previousStakingTxInfo.stakingTx.getId()) {
|
|
401
|
+
throw new Error("Previous staking input hash does not match");
|
|
402
|
+
} else if (txInputs[0].index !== previousStakingTxInfo.outputIndex) {
|
|
403
|
+
throw new Error("Previous staking input index does not match");
|
|
717
404
|
}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
};
|
|
722
|
-
}
|
|
723
|
-
function withdrawEarlyUnbondedTransaction(scripts, unbondingTx, withdrawalAddress, network, feeRate) {
|
|
724
|
-
const scriptTree = [
|
|
725
|
-
{
|
|
726
|
-
output: scripts.slashingScript
|
|
727
|
-
},
|
|
728
|
-
{ output: scripts.unbondingTimelockScript }
|
|
729
|
-
];
|
|
730
|
-
return withdrawalTransaction(
|
|
731
|
-
{
|
|
732
|
-
timelockScript: scripts.unbondingTimelockScript
|
|
733
|
-
},
|
|
734
|
-
scriptTree,
|
|
735
|
-
unbondingTx,
|
|
736
|
-
withdrawalAddress,
|
|
737
|
-
network,
|
|
738
|
-
feeRate,
|
|
739
|
-
0
|
|
740
|
-
// unbonding always has a single output
|
|
741
|
-
);
|
|
742
|
-
}
|
|
743
|
-
function withdrawTimelockUnbondedTransaction(scripts, tx, withdrawalAddress, network, feeRate, outputIndex = 0) {
|
|
744
|
-
const scriptTree = [
|
|
745
|
-
{
|
|
746
|
-
output: scripts.slashingScript
|
|
747
|
-
},
|
|
748
|
-
[{ output: scripts.unbondingScript }, { output: scripts.timelockScript }]
|
|
405
|
+
const inputScriptTree = [
|
|
406
|
+
{ output: previousScripts.slashingScript },
|
|
407
|
+
[{ output: previousScripts.unbondingScript }, { output: previousScripts.timelockScript }]
|
|
749
408
|
];
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
scriptTree,
|
|
753
|
-
tx,
|
|
754
|
-
withdrawalAddress,
|
|
755
|
-
network,
|
|
756
|
-
feeRate,
|
|
757
|
-
outputIndex
|
|
758
|
-
);
|
|
759
|
-
}
|
|
760
|
-
function withdrawSlashingTransaction(scripts, slashingTx, withdrawalAddress, network, feeRate, outputIndex) {
|
|
761
|
-
const scriptTree = { output: scripts.unbondingTimelockScript };
|
|
762
|
-
return withdrawalTransaction(
|
|
763
|
-
{
|
|
764
|
-
timelockScript: scripts.unbondingTimelockScript
|
|
765
|
-
},
|
|
766
|
-
scriptTree,
|
|
767
|
-
slashingTx,
|
|
768
|
-
withdrawalAddress,
|
|
769
|
-
network,
|
|
770
|
-
feeRate,
|
|
771
|
-
outputIndex
|
|
772
|
-
);
|
|
773
|
-
}
|
|
774
|
-
function withdrawalTransaction(scripts, scriptTree, tx, withdrawalAddress, network, feeRate, outputIndex = 0) {
|
|
775
|
-
if (feeRate <= 0) {
|
|
776
|
-
throw new Error("Withdrawal feeRate must be bigger than 0");
|
|
777
|
-
}
|
|
778
|
-
if (outputIndex < 0) {
|
|
779
|
-
throw new Error("Output index must be bigger or equal to 0");
|
|
780
|
-
}
|
|
781
|
-
const timePosition = 2;
|
|
782
|
-
const decompiled = script2.decompile(scripts.timelockScript);
|
|
783
|
-
if (!decompiled) {
|
|
784
|
-
throw new Error("Timelock script is not valid");
|
|
785
|
-
}
|
|
786
|
-
let timelock = 0;
|
|
787
|
-
if (typeof decompiled[timePosition] !== "number") {
|
|
788
|
-
const timeBuffer = decompiled[timePosition];
|
|
789
|
-
timelock = script2.number.decode(timeBuffer);
|
|
790
|
-
} else {
|
|
791
|
-
const wrap = decompiled[timePosition] % 16;
|
|
792
|
-
timelock = wrap === 0 ? 16 : wrap;
|
|
793
|
-
}
|
|
794
|
-
const redeem = {
|
|
795
|
-
output: scripts.timelockScript,
|
|
409
|
+
const inputRedeem = {
|
|
410
|
+
output: previousScripts.unbondingScript,
|
|
796
411
|
redeemVersion: REDEEM_VERSION
|
|
797
412
|
};
|
|
798
413
|
const p2tr = payments3.p2tr({
|
|
799
414
|
internalPubkey,
|
|
800
|
-
scriptTree,
|
|
801
|
-
redeem,
|
|
415
|
+
scriptTree: inputScriptTree,
|
|
416
|
+
redeem: inputRedeem,
|
|
802
417
|
network
|
|
803
418
|
});
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
419
|
+
if (!p2tr.witness || p2tr.witness.length === 0) {
|
|
420
|
+
throw new Error(
|
|
421
|
+
"Failed to create P2TR witness for expansion transaction input"
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
const inputTapLeafScript = {
|
|
425
|
+
leafVersion: inputRedeem.redeemVersion,
|
|
426
|
+
script: inputRedeem.output,
|
|
807
427
|
controlBlock: p2tr.witness[p2tr.witness.length - 1]
|
|
808
428
|
};
|
|
809
|
-
const psbt = new Psbt({ network });
|
|
810
|
-
psbt.setVersion(TRANSACTION_VERSION);
|
|
811
429
|
psbt.addInput({
|
|
812
|
-
hash:
|
|
813
|
-
index:
|
|
814
|
-
|
|
430
|
+
hash: txInputs[0].hash,
|
|
431
|
+
index: txInputs[0].index,
|
|
432
|
+
sequence: txInputs[0].sequence,
|
|
815
433
|
witnessUtxo: {
|
|
816
|
-
|
|
817
|
-
|
|
434
|
+
script: previousStakingOutput.script,
|
|
435
|
+
value: previousStakingOutput.value
|
|
818
436
|
},
|
|
819
|
-
|
|
820
|
-
|
|
437
|
+
tapInternalKey: internalPubkey,
|
|
438
|
+
tapLeafScript: [inputTapLeafScript]
|
|
821
439
|
});
|
|
822
|
-
const
|
|
823
|
-
const
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
if (outputValue < BTC_DUST_SAT) {
|
|
830
|
-
throw new Error("Output value is less than dust limit");
|
|
831
|
-
}
|
|
832
|
-
psbt.addOutput({
|
|
833
|
-
address: withdrawalAddress,
|
|
834
|
-
value: outputValue
|
|
440
|
+
const inputUTXO = findInputUTXO(inputUTXOs, txInputs[1]);
|
|
441
|
+
const psbtInputData = getPsbtInputFields(inputUTXO, publicKeyNoCoord);
|
|
442
|
+
psbt.addInput({
|
|
443
|
+
hash: txInputs[1].hash,
|
|
444
|
+
index: txInputs[1].index,
|
|
445
|
+
sequence: txInputs[1].sequence,
|
|
446
|
+
...psbtInputData
|
|
835
447
|
});
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
{
|
|
845
|
-
output: scripts.slashingScript
|
|
846
|
-
},
|
|
847
|
-
[{ output: scripts.unbondingScript }, { output: scripts.timelockScript }]
|
|
848
|
-
];
|
|
849
|
-
return slashingTransaction(
|
|
850
|
-
{
|
|
851
|
-
unbondingTimelockScript: scripts.unbondingTimelockScript,
|
|
852
|
-
slashingScript: scripts.slashingScript
|
|
853
|
-
},
|
|
854
|
-
slashingScriptTree,
|
|
855
|
-
stakingTransaction2,
|
|
856
|
-
slashingPkScriptHex,
|
|
857
|
-
slashingRate,
|
|
858
|
-
minimumFee,
|
|
859
|
-
network,
|
|
860
|
-
outputIndex
|
|
861
|
-
);
|
|
862
|
-
}
|
|
863
|
-
function slashEarlyUnbondedTransaction(scripts, unbondingTx, slashingPkScriptHex, slashingRate, minimumSlashingFee, network) {
|
|
864
|
-
const unbondingScriptTree = [
|
|
865
|
-
{
|
|
866
|
-
output: scripts.slashingScript
|
|
867
|
-
},
|
|
868
|
-
{
|
|
869
|
-
output: scripts.unbondingTimelockScript
|
|
870
|
-
}
|
|
871
|
-
];
|
|
872
|
-
return slashingTransaction(
|
|
873
|
-
{
|
|
874
|
-
unbondingTimelockScript: scripts.unbondingTimelockScript,
|
|
875
|
-
slashingScript: scripts.slashingScript
|
|
876
|
-
},
|
|
877
|
-
unbondingScriptTree,
|
|
878
|
-
unbondingTx,
|
|
879
|
-
slashingPkScriptHex,
|
|
880
|
-
slashingRate,
|
|
881
|
-
minimumSlashingFee,
|
|
882
|
-
network,
|
|
883
|
-
0
|
|
884
|
-
// unbonding always has a single output
|
|
885
|
-
);
|
|
886
|
-
}
|
|
887
|
-
function slashingTransaction(scripts, scriptTree, transaction, slashingPkScriptHex, slashingRate, minimumFee, network, outputIndex = 0) {
|
|
888
|
-
if (slashingRate <= 0 || slashingRate >= 1) {
|
|
889
|
-
throw new Error("Slashing rate must be between 0 and 1");
|
|
448
|
+
stakingTx.outs.forEach((o) => {
|
|
449
|
+
psbt.addOutput({ script: o.script, value: o.value });
|
|
450
|
+
});
|
|
451
|
+
return psbt;
|
|
452
|
+
};
|
|
453
|
+
var unbondingPsbt = (scripts, unbondingTx, stakingTx, network) => {
|
|
454
|
+
if (unbondingTx.outs.length !== 1) {
|
|
455
|
+
throw new Error("Unbonding transaction must have exactly one output");
|
|
890
456
|
}
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
throw new Error("Minimum fee must be a positve integer");
|
|
457
|
+
if (unbondingTx.ins.length !== 1) {
|
|
458
|
+
throw new Error("Unbonding transaction must have exactly one input");
|
|
894
459
|
}
|
|
895
|
-
|
|
896
|
-
|
|
460
|
+
validateUnbondingOutput(scripts, unbondingTx, network);
|
|
461
|
+
const psbt = new Psbt({ network });
|
|
462
|
+
if (unbondingTx.version !== void 0) {
|
|
463
|
+
psbt.setVersion(unbondingTx.version);
|
|
897
464
|
}
|
|
898
|
-
if (
|
|
899
|
-
|
|
465
|
+
if (unbondingTx.locktime !== void 0) {
|
|
466
|
+
psbt.setLocktime(unbondingTx.locktime);
|
|
900
467
|
}
|
|
901
|
-
const
|
|
902
|
-
|
|
468
|
+
const input = unbondingTx.ins[0];
|
|
469
|
+
const outputIndex = input.index;
|
|
470
|
+
const inputScriptTree = [
|
|
471
|
+
{ output: scripts.slashingScript },
|
|
472
|
+
[{ output: scripts.unbondingScript }, { output: scripts.timelockScript }]
|
|
473
|
+
];
|
|
474
|
+
const inputRedeem = {
|
|
475
|
+
output: scripts.unbondingScript,
|
|
903
476
|
redeemVersion: REDEEM_VERSION
|
|
904
477
|
};
|
|
905
478
|
const p2tr = payments3.p2tr({
|
|
906
479
|
internalPubkey,
|
|
907
|
-
scriptTree,
|
|
908
|
-
redeem,
|
|
480
|
+
scriptTree: inputScriptTree,
|
|
481
|
+
redeem: inputRedeem,
|
|
909
482
|
network
|
|
910
483
|
});
|
|
911
|
-
const
|
|
912
|
-
leafVersion:
|
|
913
|
-
script:
|
|
484
|
+
const inputTapLeafScript = {
|
|
485
|
+
leafVersion: inputRedeem.redeemVersion,
|
|
486
|
+
script: inputRedeem.output,
|
|
914
487
|
controlBlock: p2tr.witness[p2tr.witness.length - 1]
|
|
915
488
|
};
|
|
916
|
-
const stakingAmount = transaction.outs[outputIndex].value;
|
|
917
|
-
const slashingAmount = Math.floor(stakingAmount * slashingRate);
|
|
918
|
-
if (slashingAmount <= BTC_DUST_SAT) {
|
|
919
|
-
throw new Error("Slashing amount is less than dust limit");
|
|
920
|
-
}
|
|
921
|
-
const userFunds = stakingAmount - slashingAmount - minimumFee;
|
|
922
|
-
if (userFunds <= BTC_DUST_SAT) {
|
|
923
|
-
throw new Error("User funds are less than dust limit");
|
|
924
|
-
}
|
|
925
|
-
const psbt = new Psbt({ network });
|
|
926
|
-
psbt.setVersion(TRANSACTION_VERSION);
|
|
927
489
|
psbt.addInput({
|
|
928
|
-
hash:
|
|
929
|
-
index:
|
|
490
|
+
hash: input.hash,
|
|
491
|
+
index: input.index,
|
|
492
|
+
sequence: input.sequence,
|
|
930
493
|
tapInternalKey: internalPubkey,
|
|
931
494
|
witnessUtxo: {
|
|
932
|
-
value:
|
|
933
|
-
script:
|
|
495
|
+
value: stakingTx.outs[outputIndex].value,
|
|
496
|
+
script: stakingTx.outs[outputIndex].script
|
|
934
497
|
},
|
|
935
|
-
tapLeafScript: [
|
|
936
|
-
// not RBF-able
|
|
937
|
-
sequence: NON_RBF_SEQUENCE
|
|
938
|
-
});
|
|
939
|
-
psbt.addOutput({
|
|
940
|
-
script: Buffer.from(slashingPkScriptHex, "hex"),
|
|
941
|
-
value: slashingAmount
|
|
942
|
-
});
|
|
943
|
-
const changeOutput = payments3.p2tr({
|
|
944
|
-
internalPubkey,
|
|
945
|
-
scriptTree: { output: scripts.unbondingTimelockScript },
|
|
946
|
-
network
|
|
498
|
+
tapLeafScript: [inputTapLeafScript]
|
|
947
499
|
});
|
|
948
500
|
psbt.addOutput({
|
|
949
|
-
|
|
950
|
-
value:
|
|
951
|
-
});
|
|
952
|
-
psbt.setLocktime(0);
|
|
953
|
-
return { psbt };
|
|
954
|
-
}
|
|
955
|
-
function unbondingTransaction(scripts, stakingTx, unbondingFee, network, outputIndex = 0) {
|
|
956
|
-
if (unbondingFee <= 0) {
|
|
957
|
-
throw new Error("Unbonding fee must be bigger than 0");
|
|
958
|
-
}
|
|
959
|
-
if (outputIndex < 0) {
|
|
960
|
-
throw new Error("Output index must be bigger or equal to 0");
|
|
961
|
-
}
|
|
962
|
-
const tx = new Transaction2();
|
|
963
|
-
tx.version = TRANSACTION_VERSION;
|
|
964
|
-
tx.addInput(
|
|
965
|
-
stakingTx.getHash(),
|
|
966
|
-
outputIndex,
|
|
967
|
-
NON_RBF_SEQUENCE
|
|
968
|
-
// not RBF-able
|
|
969
|
-
);
|
|
970
|
-
const unbondingOutputInfo = deriveUnbondingOutputInfo(scripts, network);
|
|
971
|
-
const outputValue = stakingTx.outs[outputIndex].value - unbondingFee;
|
|
972
|
-
if (outputValue < BTC_DUST_SAT) {
|
|
973
|
-
throw new Error("Output value is less than dust limit for unbonding transaction");
|
|
974
|
-
}
|
|
975
|
-
if (!unbondingOutputInfo.outputAddress) {
|
|
976
|
-
throw new Error("Unbonding output address is not defined");
|
|
977
|
-
}
|
|
978
|
-
tx.addOutput(
|
|
979
|
-
unbondingOutputInfo.scriptPubKey,
|
|
980
|
-
outputValue
|
|
981
|
-
);
|
|
982
|
-
tx.locktime = 0;
|
|
983
|
-
return {
|
|
984
|
-
transaction: tx,
|
|
985
|
-
fee: unbondingFee
|
|
986
|
-
};
|
|
987
|
-
}
|
|
988
|
-
var createCovenantWitness = (originalWitness, paramsCovenants, covenantSigs, covenantQuorum) => {
|
|
989
|
-
if (covenantSigs.length < covenantQuorum) {
|
|
990
|
-
throw new Error(
|
|
991
|
-
`Not enough covenant signatures. Required: ${covenantQuorum}, got: ${covenantSigs.length}`
|
|
992
|
-
);
|
|
993
|
-
}
|
|
994
|
-
for (const sig of covenantSigs) {
|
|
995
|
-
const btcPkHexBuf = Buffer.from(sig.btcPkHex, "hex");
|
|
996
|
-
if (!paramsCovenants.some((covenant) => covenant.equals(btcPkHexBuf))) {
|
|
997
|
-
throw new Error(
|
|
998
|
-
`Covenant signature public key ${sig.btcPkHex} not found in params covenants`
|
|
999
|
-
);
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
const covenantSigsBuffers = covenantSigs.slice(0, covenantQuorum).map((sig) => ({
|
|
1003
|
-
btcPkHex: Buffer.from(sig.btcPkHex, "hex"),
|
|
1004
|
-
sigHex: Buffer.from(sig.sigHex, "hex")
|
|
1005
|
-
}));
|
|
1006
|
-
const paramsCovenantsSorted = [...paramsCovenants].sort(Buffer.compare).reverse();
|
|
1007
|
-
const composedCovenantSigs = paramsCovenantsSorted.map((covenant) => {
|
|
1008
|
-
const covenantSig = covenantSigsBuffers.find(
|
|
1009
|
-
(sig) => sig.btcPkHex.compare(covenant) === 0
|
|
1010
|
-
);
|
|
1011
|
-
return covenantSig?.sigHex || Buffer.alloc(0);
|
|
501
|
+
script: unbondingTx.outs[0].script,
|
|
502
|
+
value: unbondingTx.outs[0].value
|
|
1012
503
|
});
|
|
1013
|
-
return
|
|
504
|
+
return psbt;
|
|
1014
505
|
};
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
// src/utils/utxo/findInputUTXO.ts
|
|
1020
|
-
var findInputUTXO = (inputUTXOs, input) => {
|
|
1021
|
-
const inputUTXO = inputUTXOs.find(
|
|
1022
|
-
(u) => transactionIdToHash(u.txid).toString("hex") === input.hash.toString("hex") && u.vout === input.index
|
|
1023
|
-
);
|
|
1024
|
-
if (!inputUTXO) {
|
|
506
|
+
var validateUnbondingOutput = (scripts, unbondingTx, network) => {
|
|
507
|
+
const unbondingOutputInfo = deriveUnbondingOutputInfo(scripts, network);
|
|
508
|
+
if (unbondingOutputInfo.scriptPubKey.toString("hex") !== unbondingTx.outs[0].script.toString("hex")) {
|
|
1025
509
|
throw new Error(
|
|
1026
|
-
|
|
510
|
+
"Unbonding output script does not match the expected script while building psbt"
|
|
1027
511
|
);
|
|
1028
512
|
}
|
|
1029
|
-
return inputUTXO;
|
|
1030
513
|
};
|
|
1031
514
|
|
|
1032
|
-
// src/
|
|
1033
|
-
import {
|
|
1034
|
-
var
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
payments4.p2sh({ output: script4 });
|
|
1050
|
-
return "scripthash" /* P2SH */;
|
|
1051
|
-
} catch {
|
|
515
|
+
// src/staking/stakingScript.ts
|
|
516
|
+
import { opcodes, script } from "bitcoinjs-lib";
|
|
517
|
+
var MAGIC_BYTES_LEN = 4;
|
|
518
|
+
var StakingScriptData = class {
|
|
519
|
+
constructor(stakerKey, finalityProviderKeys, covenantKeys, covenantThreshold, stakingTimelock, unbondingTimelock) {
|
|
520
|
+
if (!stakerKey || !finalityProviderKeys || !covenantKeys || !covenantThreshold || !stakingTimelock || !unbondingTimelock) {
|
|
521
|
+
throw new Error("Missing required input values");
|
|
522
|
+
}
|
|
523
|
+
this.stakerKey = stakerKey;
|
|
524
|
+
this.finalityProviderKeys = finalityProviderKeys;
|
|
525
|
+
this.covenantKeys = covenantKeys;
|
|
526
|
+
this.covenantThreshold = covenantThreshold;
|
|
527
|
+
this.stakingTimeLock = stakingTimelock;
|
|
528
|
+
this.unbondingTimeLock = unbondingTimelock;
|
|
529
|
+
if (!this.validate()) {
|
|
530
|
+
throw new Error("Invalid script data provided");
|
|
531
|
+
}
|
|
1052
532
|
}
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
533
|
+
/**
|
|
534
|
+
* Validates the staking script.
|
|
535
|
+
* @returns {boolean} Returns true if the staking script is valid, otherwise false.
|
|
536
|
+
*/
|
|
537
|
+
validate() {
|
|
538
|
+
if (this.stakerKey.length != NO_COORD_PK_BYTE_LENGTH) {
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
if (this.finalityProviderKeys.some(
|
|
542
|
+
(finalityProviderKey) => finalityProviderKey.length != NO_COORD_PK_BYTE_LENGTH
|
|
543
|
+
)) {
|
|
544
|
+
return false;
|
|
545
|
+
}
|
|
546
|
+
if (this.covenantKeys.some((covenantKey) => covenantKey.length != NO_COORD_PK_BYTE_LENGTH)) {
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
const allPks = [
|
|
550
|
+
this.stakerKey,
|
|
551
|
+
...this.finalityProviderKeys,
|
|
552
|
+
...this.covenantKeys
|
|
553
|
+
];
|
|
554
|
+
const allPksSet = new Set(allPks);
|
|
555
|
+
if (allPks.length !== allPksSet.size) {
|
|
556
|
+
return false;
|
|
557
|
+
}
|
|
558
|
+
if (this.covenantThreshold <= 0 || this.covenantThreshold > this.covenantKeys.length) {
|
|
559
|
+
return false;
|
|
560
|
+
}
|
|
561
|
+
if (this.stakingTimeLock <= 0 || this.stakingTimeLock > 65535) {
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
if (this.unbondingTimeLock <= 0 || this.unbondingTimeLock > 65535) {
|
|
565
|
+
return false;
|
|
566
|
+
}
|
|
567
|
+
return true;
|
|
1057
568
|
}
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
569
|
+
// The staking script allows for multiple finality provider public keys
|
|
570
|
+
// to support (re)stake to multiple finality providers
|
|
571
|
+
// Covenant members are going to have multiple keys
|
|
572
|
+
/**
|
|
573
|
+
* Builds a timelock script.
|
|
574
|
+
* @param timelock - The timelock value to encode in the script.
|
|
575
|
+
* @returns {Buffer} containing the compiled timelock script.
|
|
576
|
+
*/
|
|
577
|
+
buildTimelockScript(timelock) {
|
|
578
|
+
return script.compile([
|
|
579
|
+
this.stakerKey,
|
|
580
|
+
opcodes.OP_CHECKSIGVERIFY,
|
|
581
|
+
script.number.encode(timelock),
|
|
582
|
+
opcodes.OP_CHECKSEQUENCEVERIFY
|
|
583
|
+
]);
|
|
1062
584
|
}
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
585
|
+
/**
|
|
586
|
+
* Builds the staking timelock script.
|
|
587
|
+
* Only holder of private key for given pubKey can spend after relative lock time
|
|
588
|
+
* Creates the timelock script in the form:
|
|
589
|
+
* <stakerPubKey>
|
|
590
|
+
* OP_CHECKSIGVERIFY
|
|
591
|
+
* <stakingTimeBlocks>
|
|
592
|
+
* OP_CHECKSEQUENCEVERIFY
|
|
593
|
+
* @returns {Buffer} The staking timelock script.
|
|
594
|
+
*/
|
|
595
|
+
buildStakingTimelockScript() {
|
|
596
|
+
return this.buildTimelockScript(this.stakingTimeLock);
|
|
1067
597
|
}
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
598
|
+
/**
|
|
599
|
+
* Builds the unbonding timelock script.
|
|
600
|
+
* Creates the unbonding timelock script in the form:
|
|
601
|
+
* <stakerPubKey>
|
|
602
|
+
* OP_CHECKSIGVERIFY
|
|
603
|
+
* <unbondingTimeBlocks>
|
|
604
|
+
* OP_CHECKSEQUENCEVERIFY
|
|
605
|
+
* @returns {Buffer} The unbonding timelock script.
|
|
606
|
+
*/
|
|
607
|
+
buildUnbondingTimelockScript() {
|
|
608
|
+
return this.buildTimelockScript(this.unbondingTimeLock);
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Builds the unbonding script in the form:
|
|
612
|
+
* buildSingleKeyScript(stakerPk, true) ||
|
|
613
|
+
* buildMultiKeyScript(covenantPks, covenantThreshold, false)
|
|
614
|
+
* || means combining the scripts
|
|
615
|
+
* @returns {Buffer} The unbonding script.
|
|
616
|
+
*/
|
|
617
|
+
buildUnbondingScript() {
|
|
618
|
+
return Buffer.concat([
|
|
619
|
+
this.buildSingleKeyScript(this.stakerKey, true),
|
|
620
|
+
this.buildMultiKeyScript(
|
|
621
|
+
this.covenantKeys,
|
|
622
|
+
this.covenantThreshold,
|
|
623
|
+
false
|
|
624
|
+
)
|
|
625
|
+
]);
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Builds the slashing script for staking in the form:
|
|
629
|
+
* buildSingleKeyScript(stakerPk, true) ||
|
|
630
|
+
* buildMultiKeyScript(finalityProviderPKs, 1, true) ||
|
|
631
|
+
* buildMultiKeyScript(covenantPks, covenantThreshold, false)
|
|
632
|
+
* || means combining the scripts
|
|
633
|
+
* The slashing script is a combination of single-key and multi-key scripts.
|
|
634
|
+
* The single-key script is used for staker key verification.
|
|
635
|
+
* The multi-key script is used for finality provider key verification and covenant key verification.
|
|
636
|
+
* @returns {Buffer} The slashing script as a Buffer.
|
|
637
|
+
*/
|
|
638
|
+
buildSlashingScript() {
|
|
639
|
+
return Buffer.concat([
|
|
640
|
+
this.buildSingleKeyScript(this.stakerKey, true),
|
|
641
|
+
this.buildMultiKeyScript(
|
|
642
|
+
this.finalityProviderKeys,
|
|
643
|
+
// The threshold is always 1 as we only need one
|
|
644
|
+
// finalityProvider signature to perform slashing
|
|
645
|
+
// (only one finalityProvider performs an offence)
|
|
646
|
+
1,
|
|
647
|
+
// OP_VERIFY/OP_CHECKSIGVERIFY is added at the end
|
|
648
|
+
true
|
|
649
|
+
),
|
|
650
|
+
this.buildMultiKeyScript(
|
|
651
|
+
this.covenantKeys,
|
|
652
|
+
this.covenantThreshold,
|
|
653
|
+
// No need to add verify since covenants are at the end of the script
|
|
654
|
+
false
|
|
655
|
+
)
|
|
656
|
+
]);
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Builds the staking scripts.
|
|
660
|
+
* @returns {StakingScripts} The staking scripts.
|
|
661
|
+
*/
|
|
662
|
+
buildScripts() {
|
|
663
|
+
return {
|
|
664
|
+
timelockScript: this.buildStakingTimelockScript(),
|
|
665
|
+
unbondingScript: this.buildUnbondingScript(),
|
|
666
|
+
slashingScript: this.buildSlashingScript(),
|
|
667
|
+
unbondingTimelockScript: this.buildUnbondingTimelockScript()
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
// buildSingleKeyScript and buildMultiKeyScript allow us to reuse functionality
|
|
671
|
+
// for creating Bitcoin scripts for the unbonding script and the slashing script
|
|
672
|
+
/**
|
|
673
|
+
* Builds a single key script in the form:
|
|
674
|
+
* buildSingleKeyScript creates a single key script
|
|
675
|
+
* <pk> OP_CHECKSIGVERIFY (if withVerify is true)
|
|
676
|
+
* <pk> OP_CHECKSIG (if withVerify is false)
|
|
677
|
+
* @param pk - The public key buffer.
|
|
678
|
+
* @param withVerify - A boolean indicating whether to include the OP_CHECKSIGVERIFY opcode.
|
|
679
|
+
* @returns The compiled script buffer.
|
|
680
|
+
*/
|
|
681
|
+
buildSingleKeyScript(pk, withVerify) {
|
|
682
|
+
if (pk.length != NO_COORD_PK_BYTE_LENGTH) {
|
|
683
|
+
throw new Error("Invalid key length");
|
|
1081
684
|
}
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
685
|
+
return script.compile([
|
|
686
|
+
pk,
|
|
687
|
+
withVerify ? opcodes.OP_CHECKSIGVERIFY : opcodes.OP_CHECKSIG
|
|
688
|
+
]);
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Builds a multi-key script in the form:
|
|
692
|
+
* <pk1> OP_CHEKCSIG <pk2> OP_CHECKSIGADD <pk3> OP_CHECKSIGADD ... <pkN> OP_CHECKSIGADD <threshold> OP_NUMEQUAL
|
|
693
|
+
* <withVerify -> OP_NUMEQUALVERIFY>
|
|
694
|
+
* It validates whether provided keys are unique and the threshold is not greater than number of keys
|
|
695
|
+
* If there is only one key provided it will return single key sig script
|
|
696
|
+
* @param pks - An array of public keys.
|
|
697
|
+
* @param threshold - The required number of valid signers.
|
|
698
|
+
* @param withVerify - A boolean indicating whether to include the OP_VERIFY opcode.
|
|
699
|
+
* @returns The compiled multi-key script as a Buffer.
|
|
700
|
+
* @throws {Error} If no keys are provided, if the required number of valid signers is greater than the number of provided keys, or if duplicate keys are provided.
|
|
701
|
+
*/
|
|
702
|
+
buildMultiKeyScript(pks, threshold, withVerify) {
|
|
703
|
+
if (!pks || pks.length === 0) {
|
|
704
|
+
throw new Error("No keys provided");
|
|
1093
705
|
}
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
witnessUtxo: {
|
|
1097
|
-
script: scriptPubKey,
|
|
1098
|
-
value: utxo.value
|
|
1099
|
-
}
|
|
1100
|
-
};
|
|
706
|
+
if (pks.some((pk) => pk.length != NO_COORD_PK_BYTE_LENGTH)) {
|
|
707
|
+
throw new Error("Invalid key length");
|
|
1101
708
|
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
709
|
+
if (threshold > pks.length) {
|
|
710
|
+
throw new Error(
|
|
711
|
+
"Required number of valid signers is greater than number of provided keys"
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
if (pks.length === 1) {
|
|
715
|
+
return this.buildSingleKeyScript(pks[0], withVerify);
|
|
716
|
+
}
|
|
717
|
+
const sortedPks = [...pks].sort(Buffer.compare);
|
|
718
|
+
for (let i = 0; i < sortedPks.length - 1; ++i) {
|
|
719
|
+
if (sortedPks[i].equals(sortedPks[i + 1])) {
|
|
720
|
+
throw new Error("Duplicate keys provided");
|
|
1105
721
|
}
|
|
1106
|
-
return {
|
|
1107
|
-
witnessUtxo: {
|
|
1108
|
-
script: scriptPubKey,
|
|
1109
|
-
value: utxo.value
|
|
1110
|
-
},
|
|
1111
|
-
witnessScript: Buffer.from(utxo.witnessScript, "hex")
|
|
1112
|
-
};
|
|
1113
722
|
}
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
value: utxo.value
|
|
1119
|
-
},
|
|
1120
|
-
// this is needed only if the wallet is in taproot mode
|
|
1121
|
-
...publicKeyNoCoord && { tapInternalKey: publicKeyNoCoord }
|
|
1122
|
-
};
|
|
723
|
+
const scriptElements = [sortedPks[0], opcodes.OP_CHECKSIG];
|
|
724
|
+
for (let i = 1; i < sortedPks.length; i++) {
|
|
725
|
+
scriptElements.push(sortedPks[i]);
|
|
726
|
+
scriptElements.push(opcodes.OP_CHECKSIGADD);
|
|
1123
727
|
}
|
|
1124
|
-
|
|
1125
|
-
|
|
728
|
+
scriptElements.push(script.number.encode(threshold));
|
|
729
|
+
if (withVerify) {
|
|
730
|
+
scriptElements.push(opcodes.OP_NUMEQUALVERIFY);
|
|
731
|
+
} else {
|
|
732
|
+
scriptElements.push(opcodes.OP_NUMEQUAL);
|
|
733
|
+
}
|
|
734
|
+
return script.compile(scriptElements);
|
|
1126
735
|
}
|
|
1127
736
|
};
|
|
1128
737
|
|
|
1129
|
-
// src/staking/
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
738
|
+
// src/staking/transactions.ts
|
|
739
|
+
import {
|
|
740
|
+
Psbt as Psbt2,
|
|
741
|
+
Transaction as Transaction3,
|
|
742
|
+
payments as payments5,
|
|
743
|
+
script as script2,
|
|
744
|
+
address as address3,
|
|
745
|
+
opcodes as opcodes3
|
|
746
|
+
} from "bitcoinjs-lib";
|
|
747
|
+
|
|
748
|
+
// src/constants/dustSat.ts
|
|
749
|
+
var BTC_DUST_SAT = 546;
|
|
750
|
+
|
|
751
|
+
// src/utils/fee/index.ts
|
|
752
|
+
import { script as bitcoinScript2 } from "bitcoinjs-lib";
|
|
753
|
+
|
|
754
|
+
// src/constants/fee.ts
|
|
755
|
+
var DEFAULT_INPUT_SIZE = 180;
|
|
756
|
+
var P2WPKH_INPUT_SIZE = 68;
|
|
757
|
+
var P2TR_INPUT_SIZE = 58;
|
|
758
|
+
var P2TR_STAKING_EXPANSION_INPUT_SIZE = 268;
|
|
759
|
+
var TX_BUFFER_SIZE_OVERHEAD = 11;
|
|
760
|
+
var LOW_RATE_ESTIMATION_ACCURACY_BUFFER = 30;
|
|
761
|
+
var MAX_NON_LEGACY_OUTPUT_SIZE = 43;
|
|
762
|
+
var WITHDRAW_TX_BUFFER_SIZE = 17;
|
|
763
|
+
var WALLET_RELAY_FEE_RATE_THRESHOLD = 2;
|
|
764
|
+
var OP_RETURN_OUTPUT_VALUE_SIZE = 8;
|
|
765
|
+
var OP_RETURN_VALUE_SERIALIZE_SIZE = 1;
|
|
766
|
+
|
|
767
|
+
// src/utils/fee/utils.ts
|
|
768
|
+
import { script as bitcoinScript, opcodes as opcodes2, payments as payments4 } from "bitcoinjs-lib";
|
|
769
|
+
var isOP_RETURN = (script4) => {
|
|
770
|
+
const decompiled = bitcoinScript.decompile(script4);
|
|
771
|
+
return !!decompiled && decompiled[0] === opcodes2.OP_RETURN;
|
|
772
|
+
};
|
|
773
|
+
var getInputSizeByScript = (script4) => {
|
|
774
|
+
try {
|
|
775
|
+
const { address: p2wpkhAddress } = payments4.p2wpkh({
|
|
776
|
+
output: script4
|
|
1147
777
|
});
|
|
778
|
+
if (p2wpkhAddress) {
|
|
779
|
+
return P2WPKH_INPUT_SIZE;
|
|
780
|
+
}
|
|
781
|
+
} catch (error) {
|
|
782
|
+
}
|
|
783
|
+
try {
|
|
784
|
+
const { address: p2trAddress } = payments4.p2tr({
|
|
785
|
+
output: script4
|
|
786
|
+
});
|
|
787
|
+
if (p2trAddress) {
|
|
788
|
+
return P2TR_INPUT_SIZE;
|
|
789
|
+
}
|
|
790
|
+
} catch (error) {
|
|
791
|
+
}
|
|
792
|
+
return DEFAULT_INPUT_SIZE;
|
|
793
|
+
};
|
|
794
|
+
var getEstimatedChangeOutputSize = () => {
|
|
795
|
+
return MAX_NON_LEGACY_OUTPUT_SIZE;
|
|
796
|
+
};
|
|
797
|
+
var inputValueSum = (inputUTXOs) => {
|
|
798
|
+
return inputUTXOs.reduce((acc, utxo) => acc + utxo.value, 0);
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
// src/utils/fee/index.ts
|
|
802
|
+
var getStakingTxInputUTXOsAndFees = (availableUTXOs, stakingAmount, feeRate, outputs) => {
|
|
803
|
+
if (availableUTXOs.length === 0) {
|
|
804
|
+
throw new Error("Insufficient funds");
|
|
805
|
+
}
|
|
806
|
+
const validUTXOs = availableUTXOs.filter((utxo) => {
|
|
807
|
+
const script4 = Buffer.from(utxo.scriptPubKey, "hex");
|
|
808
|
+
return !!bitcoinScript2.decompile(script4);
|
|
1148
809
|
});
|
|
1149
|
-
|
|
1150
|
-
|
|
810
|
+
if (validUTXOs.length === 0) {
|
|
811
|
+
throw new Error("Insufficient funds: no valid UTXOs available for staking");
|
|
812
|
+
}
|
|
813
|
+
const sortedUTXOs = validUTXOs.sort((a, b) => b.value - a.value);
|
|
814
|
+
const selectedUTXOs = [];
|
|
815
|
+
let accumulatedValue = 0;
|
|
816
|
+
let estimatedFee = 0;
|
|
817
|
+
for (const utxo of sortedUTXOs) {
|
|
818
|
+
selectedUTXOs.push(utxo);
|
|
819
|
+
accumulatedValue += utxo.value;
|
|
820
|
+
const estimatedSize = getEstimatedSize(selectedUTXOs, outputs);
|
|
821
|
+
estimatedFee = estimatedSize * feeRate + rateBasedTxBufferFee(feeRate);
|
|
822
|
+
if (accumulatedValue - (stakingAmount + estimatedFee) > BTC_DUST_SAT) {
|
|
823
|
+
estimatedFee += getEstimatedChangeOutputSize() * feeRate;
|
|
824
|
+
}
|
|
825
|
+
if (accumulatedValue >= stakingAmount + estimatedFee) {
|
|
826
|
+
break;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
if (accumulatedValue < stakingAmount + estimatedFee) {
|
|
830
|
+
throw new Error(
|
|
831
|
+
"Insufficient funds: unable to gather enough UTXOs to cover the staking amount and fees"
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
return {
|
|
835
|
+
selectedUTXOs,
|
|
836
|
+
fee: estimatedFee
|
|
837
|
+
};
|
|
838
|
+
};
|
|
839
|
+
var getStakingExpansionTxFundingUTXOAndFees = (availableUTXOs, feeRate, outputs) => {
|
|
840
|
+
if (availableUTXOs.length === 0) {
|
|
841
|
+
throw new Error("Insufficient funds");
|
|
842
|
+
}
|
|
843
|
+
const validUTXOs = availableUTXOs.filter((utxo) => {
|
|
844
|
+
const script4 = Buffer.from(utxo.scriptPubKey, "hex");
|
|
845
|
+
const decompiledScript = bitcoinScript2.decompile(script4);
|
|
846
|
+
return decompiledScript && decompiledScript.length > 0;
|
|
1151
847
|
});
|
|
1152
|
-
|
|
848
|
+
if (validUTXOs.length === 0) {
|
|
849
|
+
throw new Error("Insufficient funds: no valid UTXOs available for staking");
|
|
850
|
+
}
|
|
851
|
+
const sortedUTXOs = validUTXOs.sort((a, b) => a.value - b.value);
|
|
852
|
+
for (const utxo of sortedUTXOs) {
|
|
853
|
+
const estimatedSize = getEstimatedSize(
|
|
854
|
+
[utxo],
|
|
855
|
+
outputs
|
|
856
|
+
) + P2TR_STAKING_EXPANSION_INPUT_SIZE;
|
|
857
|
+
let estimatedFee = estimatedSize * feeRate + rateBasedTxBufferFee(feeRate);
|
|
858
|
+
if (utxo.value >= estimatedFee) {
|
|
859
|
+
if (utxo.value - estimatedFee > BTC_DUST_SAT) {
|
|
860
|
+
estimatedFee += getEstimatedChangeOutputSize() * feeRate;
|
|
861
|
+
}
|
|
862
|
+
if (utxo.value >= estimatedFee) {
|
|
863
|
+
return {
|
|
864
|
+
selectedUTXO: utxo,
|
|
865
|
+
fee: estimatedFee
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
throw new Error(
|
|
871
|
+
"Insufficient funds: unable to find a UTXO to cover the fees for the staking expansion transaction."
|
|
872
|
+
);
|
|
1153
873
|
};
|
|
1154
|
-
var
|
|
1155
|
-
|
|
1156
|
-
|
|
874
|
+
var getWithdrawTxFee = (feeRate) => {
|
|
875
|
+
const inputSize = P2TR_INPUT_SIZE;
|
|
876
|
+
const outputSize = getEstimatedChangeOutputSize();
|
|
877
|
+
return feeRate * (inputSize + outputSize + TX_BUFFER_SIZE_OVERHEAD + WITHDRAW_TX_BUFFER_SIZE) + rateBasedTxBufferFee(feeRate);
|
|
878
|
+
};
|
|
879
|
+
var getEstimatedSize = (inputUtxos, outputs) => {
|
|
880
|
+
const inputSize = inputUtxos.reduce((acc, u) => {
|
|
881
|
+
const script4 = Buffer.from(u.scriptPubKey, "hex");
|
|
882
|
+
const decompiledScript = bitcoinScript2.decompile(script4);
|
|
883
|
+
if (!decompiledScript) {
|
|
884
|
+
return acc;
|
|
885
|
+
}
|
|
886
|
+
return acc + getInputSizeByScript(script4);
|
|
887
|
+
}, 0);
|
|
888
|
+
const outputSize = outputs.reduce((acc, output) => {
|
|
889
|
+
if (isOP_RETURN(output.scriptPubKey)) {
|
|
890
|
+
return acc + output.scriptPubKey.length + OP_RETURN_OUTPUT_VALUE_SIZE + OP_RETURN_VALUE_SERIALIZE_SIZE;
|
|
891
|
+
}
|
|
892
|
+
return acc + MAX_NON_LEGACY_OUTPUT_SIZE;
|
|
893
|
+
}, 0);
|
|
894
|
+
return inputSize + outputSize + TX_BUFFER_SIZE_OVERHEAD;
|
|
895
|
+
};
|
|
896
|
+
var rateBasedTxBufferFee = (feeRate) => {
|
|
897
|
+
return feeRate <= WALLET_RELAY_FEE_RATE_THRESHOLD ? LOW_RATE_ESTIMATION_ACCURACY_BUFFER : 0;
|
|
898
|
+
};
|
|
899
|
+
|
|
900
|
+
// src/constants/psbt.ts
|
|
901
|
+
var NON_RBF_SEQUENCE = 4294967295;
|
|
902
|
+
var TRANSACTION_VERSION = 2;
|
|
903
|
+
|
|
904
|
+
// src/staking/transactions.ts
|
|
905
|
+
var BTC_LOCKTIME_HEIGHT_TIME_CUTOFF = 5e8;
|
|
906
|
+
var BTC_SLASHING_FRACTION_DIGITS = 4;
|
|
907
|
+
function stakingTransaction(scripts, amount, changeAddress, inputUTXOs, network, feeRate, lockHeight) {
|
|
908
|
+
if (amount <= 0 || feeRate <= 0) {
|
|
909
|
+
throw new Error("Amount and fee rate must be bigger than 0");
|
|
1157
910
|
}
|
|
1158
|
-
if (
|
|
1159
|
-
throw new Error("
|
|
911
|
+
if (!isValidBitcoinAddress(changeAddress, network)) {
|
|
912
|
+
throw new Error("Invalid change address");
|
|
1160
913
|
}
|
|
1161
|
-
|
|
914
|
+
const stakingOutputs = buildStakingTransactionOutputs(scripts, network, amount);
|
|
915
|
+
const { selectedUTXOs, fee } = getStakingTxInputUTXOsAndFees(
|
|
916
|
+
inputUTXOs,
|
|
917
|
+
amount,
|
|
918
|
+
feeRate,
|
|
919
|
+
stakingOutputs
|
|
920
|
+
);
|
|
921
|
+
const tx = new Transaction3();
|
|
922
|
+
tx.version = TRANSACTION_VERSION;
|
|
923
|
+
for (let i = 0; i < selectedUTXOs.length; ++i) {
|
|
924
|
+
const input = selectedUTXOs[i];
|
|
925
|
+
tx.addInput(
|
|
926
|
+
transactionIdToHash(input.txid),
|
|
927
|
+
input.vout,
|
|
928
|
+
NON_RBF_SEQUENCE
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
stakingOutputs.forEach((o) => {
|
|
932
|
+
tx.addOutput(o.scriptPubKey, o.value);
|
|
933
|
+
});
|
|
934
|
+
const inputsSum = inputValueSum(selectedUTXOs);
|
|
935
|
+
if (inputsSum - (amount + fee) > BTC_DUST_SAT) {
|
|
936
|
+
tx.addOutput(
|
|
937
|
+
address3.toOutputScript(changeAddress, network),
|
|
938
|
+
inputsSum - (amount + fee)
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
if (lockHeight) {
|
|
942
|
+
if (lockHeight >= BTC_LOCKTIME_HEIGHT_TIME_CUTOFF) {
|
|
943
|
+
throw new Error("Invalid lock height");
|
|
944
|
+
}
|
|
945
|
+
tx.locktime = lockHeight;
|
|
946
|
+
}
|
|
947
|
+
return {
|
|
948
|
+
transaction: tx,
|
|
949
|
+
fee
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
function stakingExpansionTransaction(network, scripts, amount, changeAddress, feeRate, inputUTXOs, previousStakingTxInfo) {
|
|
953
|
+
if (amount <= 0 || feeRate <= 0) {
|
|
954
|
+
throw new Error("Amount and fee rate must be bigger than 0");
|
|
955
|
+
} else if (!isValidBitcoinAddress(changeAddress, network)) {
|
|
956
|
+
throw new Error("Invalid BTC change address");
|
|
957
|
+
}
|
|
958
|
+
const previousStakingOutputInfo = deriveStakingOutputInfo(
|
|
959
|
+
previousStakingTxInfo.scripts,
|
|
960
|
+
network
|
|
961
|
+
);
|
|
962
|
+
const previousStakingOutputIndex = findMatchingTxOutputIndex(
|
|
963
|
+
previousStakingTxInfo.stakingTx,
|
|
964
|
+
previousStakingOutputInfo.outputAddress,
|
|
965
|
+
network
|
|
966
|
+
);
|
|
967
|
+
const previousStakingAmount = previousStakingTxInfo.stakingTx.outs[previousStakingOutputIndex].value;
|
|
968
|
+
if (amount !== previousStakingAmount) {
|
|
969
|
+
throw new Error(
|
|
970
|
+
"Expansion staking transaction amount must be equal to the previous staking amount. Increase of the staking amount is not supported yet."
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
const stakingOutputs = buildStakingTransactionOutputs(
|
|
974
|
+
scripts,
|
|
975
|
+
network,
|
|
976
|
+
amount
|
|
977
|
+
);
|
|
978
|
+
const { selectedUTXO, fee } = getStakingExpansionTxFundingUTXOAndFees(
|
|
979
|
+
inputUTXOs,
|
|
980
|
+
feeRate,
|
|
981
|
+
stakingOutputs
|
|
982
|
+
);
|
|
983
|
+
const tx = new Transaction3();
|
|
984
|
+
tx.version = TRANSACTION_VERSION;
|
|
985
|
+
tx.addInput(
|
|
986
|
+
previousStakingTxInfo.stakingTx.getHash(),
|
|
987
|
+
previousStakingOutputIndex,
|
|
988
|
+
NON_RBF_SEQUENCE
|
|
989
|
+
);
|
|
990
|
+
tx.addInput(
|
|
991
|
+
transactionIdToHash(selectedUTXO.txid),
|
|
992
|
+
selectedUTXO.vout,
|
|
993
|
+
NON_RBF_SEQUENCE
|
|
994
|
+
);
|
|
995
|
+
stakingOutputs.forEach((o) => {
|
|
996
|
+
tx.addOutput(o.scriptPubKey, o.value);
|
|
997
|
+
});
|
|
998
|
+
if (selectedUTXO.value - fee > BTC_DUST_SAT) {
|
|
999
|
+
tx.addOutput(
|
|
1000
|
+
address3.toOutputScript(changeAddress, network),
|
|
1001
|
+
selectedUTXO.value - fee
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
return {
|
|
1005
|
+
transaction: tx,
|
|
1006
|
+
fee,
|
|
1007
|
+
fundingUTXO: selectedUTXO
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
function withdrawEarlyUnbondedTransaction(scripts, unbondingTx, withdrawalAddress, network, feeRate) {
|
|
1011
|
+
const scriptTree = [
|
|
1012
|
+
{
|
|
1013
|
+
output: scripts.slashingScript
|
|
1014
|
+
},
|
|
1015
|
+
{ output: scripts.unbondingTimelockScript }
|
|
1016
|
+
];
|
|
1017
|
+
return withdrawalTransaction(
|
|
1018
|
+
{
|
|
1019
|
+
timelockScript: scripts.unbondingTimelockScript
|
|
1020
|
+
},
|
|
1021
|
+
scriptTree,
|
|
1022
|
+
unbondingTx,
|
|
1023
|
+
withdrawalAddress,
|
|
1024
|
+
network,
|
|
1025
|
+
feeRate,
|
|
1026
|
+
0
|
|
1027
|
+
// unbonding always has a single output
|
|
1028
|
+
);
|
|
1029
|
+
}
|
|
1030
|
+
function withdrawTimelockUnbondedTransaction(scripts, tx, withdrawalAddress, network, feeRate, outputIndex = 0) {
|
|
1031
|
+
const scriptTree = [
|
|
1032
|
+
{
|
|
1033
|
+
output: scripts.slashingScript
|
|
1034
|
+
},
|
|
1035
|
+
[{ output: scripts.unbondingScript }, { output: scripts.timelockScript }]
|
|
1036
|
+
];
|
|
1037
|
+
return withdrawalTransaction(
|
|
1038
|
+
scripts,
|
|
1039
|
+
scriptTree,
|
|
1040
|
+
tx,
|
|
1041
|
+
withdrawalAddress,
|
|
1042
|
+
network,
|
|
1043
|
+
feeRate,
|
|
1044
|
+
outputIndex
|
|
1045
|
+
);
|
|
1046
|
+
}
|
|
1047
|
+
function withdrawSlashingTransaction(scripts, slashingTx, withdrawalAddress, network, feeRate, outputIndex) {
|
|
1048
|
+
const scriptTree = { output: scripts.unbondingTimelockScript };
|
|
1049
|
+
return withdrawalTransaction(
|
|
1050
|
+
{
|
|
1051
|
+
timelockScript: scripts.unbondingTimelockScript
|
|
1052
|
+
},
|
|
1053
|
+
scriptTree,
|
|
1054
|
+
slashingTx,
|
|
1055
|
+
withdrawalAddress,
|
|
1056
|
+
network,
|
|
1057
|
+
feeRate,
|
|
1058
|
+
outputIndex
|
|
1059
|
+
);
|
|
1060
|
+
}
|
|
1061
|
+
function withdrawalTransaction(scripts, scriptTree, tx, withdrawalAddress, network, feeRate, outputIndex = 0) {
|
|
1062
|
+
if (feeRate <= 0) {
|
|
1063
|
+
throw new Error("Withdrawal feeRate must be bigger than 0");
|
|
1064
|
+
}
|
|
1065
|
+
if (outputIndex < 0) {
|
|
1066
|
+
throw new Error("Output index must be bigger or equal to 0");
|
|
1067
|
+
}
|
|
1068
|
+
const timePosition = 2;
|
|
1069
|
+
const decompiled = script2.decompile(scripts.timelockScript);
|
|
1070
|
+
if (!decompiled) {
|
|
1071
|
+
throw new Error("Timelock script is not valid");
|
|
1072
|
+
}
|
|
1073
|
+
let timelock = 0;
|
|
1074
|
+
if (typeof decompiled[timePosition] !== "number") {
|
|
1075
|
+
const timeBuffer = decompiled[timePosition];
|
|
1076
|
+
timelock = script2.number.decode(timeBuffer);
|
|
1077
|
+
} else {
|
|
1078
|
+
const wrap = decompiled[timePosition] % 16;
|
|
1079
|
+
timelock = wrap === 0 ? 16 : wrap;
|
|
1080
|
+
}
|
|
1081
|
+
const redeem = {
|
|
1082
|
+
output: scripts.timelockScript,
|
|
1083
|
+
redeemVersion: REDEEM_VERSION
|
|
1084
|
+
};
|
|
1085
|
+
const p2tr = payments5.p2tr({
|
|
1086
|
+
internalPubkey,
|
|
1087
|
+
scriptTree,
|
|
1088
|
+
redeem,
|
|
1089
|
+
network
|
|
1090
|
+
});
|
|
1091
|
+
const tapLeafScript = {
|
|
1092
|
+
leafVersion: redeem.redeemVersion,
|
|
1093
|
+
script: redeem.output,
|
|
1094
|
+
controlBlock: p2tr.witness[p2tr.witness.length - 1]
|
|
1095
|
+
};
|
|
1162
1096
|
const psbt = new Psbt2({ network });
|
|
1163
|
-
|
|
1164
|
-
|
|
1097
|
+
psbt.setVersion(TRANSACTION_VERSION);
|
|
1098
|
+
psbt.addInput({
|
|
1099
|
+
hash: tx.getHash(),
|
|
1100
|
+
index: outputIndex,
|
|
1101
|
+
tapInternalKey: internalPubkey,
|
|
1102
|
+
witnessUtxo: {
|
|
1103
|
+
value: tx.outs[outputIndex].value,
|
|
1104
|
+
script: tx.outs[outputIndex].script
|
|
1105
|
+
},
|
|
1106
|
+
tapLeafScript: [tapLeafScript],
|
|
1107
|
+
sequence: timelock
|
|
1108
|
+
});
|
|
1109
|
+
const estimatedFee = getWithdrawTxFee(feeRate);
|
|
1110
|
+
const outputValue = tx.outs[outputIndex].value - estimatedFee;
|
|
1111
|
+
if (outputValue < 0) {
|
|
1112
|
+
throw new Error(
|
|
1113
|
+
"Not enough funds to cover the fee for withdrawal transaction"
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
if (outputValue < BTC_DUST_SAT) {
|
|
1117
|
+
throw new Error("Output value is less than dust limit");
|
|
1118
|
+
}
|
|
1119
|
+
psbt.addOutput({
|
|
1120
|
+
address: withdrawalAddress,
|
|
1121
|
+
value: outputValue
|
|
1122
|
+
});
|
|
1123
|
+
psbt.setLocktime(0);
|
|
1124
|
+
return {
|
|
1125
|
+
psbt,
|
|
1126
|
+
fee: estimatedFee
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
function slashTimelockUnbondedTransaction(scripts, stakingTransaction2, slashingPkScriptHex, slashingRate, minimumFee, network, outputIndex = 0) {
|
|
1130
|
+
const slashingScriptTree = [
|
|
1131
|
+
{
|
|
1132
|
+
output: scripts.slashingScript
|
|
1133
|
+
},
|
|
1134
|
+
[{ output: scripts.unbondingScript }, { output: scripts.timelockScript }]
|
|
1135
|
+
];
|
|
1136
|
+
return slashingTransaction(
|
|
1137
|
+
{
|
|
1138
|
+
unbondingTimelockScript: scripts.unbondingTimelockScript,
|
|
1139
|
+
slashingScript: scripts.slashingScript
|
|
1140
|
+
},
|
|
1141
|
+
slashingScriptTree,
|
|
1142
|
+
stakingTransaction2,
|
|
1143
|
+
slashingPkScriptHex,
|
|
1144
|
+
slashingRate,
|
|
1145
|
+
minimumFee,
|
|
1146
|
+
network,
|
|
1147
|
+
outputIndex
|
|
1148
|
+
);
|
|
1149
|
+
}
|
|
1150
|
+
function slashEarlyUnbondedTransaction(scripts, unbondingTx, slashingPkScriptHex, slashingRate, minimumSlashingFee, network) {
|
|
1151
|
+
const unbondingScriptTree = [
|
|
1152
|
+
{
|
|
1153
|
+
output: scripts.slashingScript
|
|
1154
|
+
},
|
|
1155
|
+
{
|
|
1156
|
+
output: scripts.unbondingTimelockScript
|
|
1157
|
+
}
|
|
1158
|
+
];
|
|
1159
|
+
return slashingTransaction(
|
|
1160
|
+
{
|
|
1161
|
+
unbondingTimelockScript: scripts.unbondingTimelockScript,
|
|
1162
|
+
slashingScript: scripts.slashingScript
|
|
1163
|
+
},
|
|
1164
|
+
unbondingScriptTree,
|
|
1165
|
+
unbondingTx,
|
|
1166
|
+
slashingPkScriptHex,
|
|
1167
|
+
slashingRate,
|
|
1168
|
+
minimumSlashingFee,
|
|
1169
|
+
network,
|
|
1170
|
+
0
|
|
1171
|
+
// unbonding always has a single output
|
|
1172
|
+
);
|
|
1173
|
+
}
|
|
1174
|
+
function slashingTransaction(scripts, scriptTree, transaction, slashingPkScriptHex, slashingRate, minimumFee, network, outputIndex = 0) {
|
|
1175
|
+
if (slashingRate <= 0 || slashingRate >= 1) {
|
|
1176
|
+
throw new Error("Slashing rate must be between 0 and 1");
|
|
1177
|
+
}
|
|
1178
|
+
slashingRate = parseFloat(slashingRate.toFixed(BTC_SLASHING_FRACTION_DIGITS));
|
|
1179
|
+
if (minimumFee <= 0 || !Number.isInteger(minimumFee)) {
|
|
1180
|
+
throw new Error("Minimum fee must be a positve integer");
|
|
1181
|
+
}
|
|
1182
|
+
if (outputIndex < 0 || !Number.isInteger(outputIndex)) {
|
|
1183
|
+
throw new Error("Output index must be an integer bigger or equal to 0");
|
|
1165
1184
|
}
|
|
1166
|
-
if (
|
|
1167
|
-
|
|
1185
|
+
if (!transaction.outs[outputIndex]) {
|
|
1186
|
+
throw new Error("Output index is out of range");
|
|
1168
1187
|
}
|
|
1169
|
-
const
|
|
1170
|
-
|
|
1171
|
-
const inputScriptTree = [
|
|
1172
|
-
{ output: scripts.slashingScript },
|
|
1173
|
-
[{ output: scripts.unbondingScript }, { output: scripts.timelockScript }]
|
|
1174
|
-
];
|
|
1175
|
-
const inputRedeem = {
|
|
1176
|
-
output: scripts.unbondingScript,
|
|
1188
|
+
const redeem = {
|
|
1189
|
+
output: scripts.slashingScript,
|
|
1177
1190
|
redeemVersion: REDEEM_VERSION
|
|
1178
1191
|
};
|
|
1179
1192
|
const p2tr = payments5.p2tr({
|
|
1180
1193
|
internalPubkey,
|
|
1181
|
-
scriptTree
|
|
1182
|
-
redeem
|
|
1194
|
+
scriptTree,
|
|
1195
|
+
redeem,
|
|
1183
1196
|
network
|
|
1184
1197
|
});
|
|
1185
|
-
const
|
|
1186
|
-
leafVersion:
|
|
1187
|
-
script:
|
|
1198
|
+
const tapLeafScript = {
|
|
1199
|
+
leafVersion: redeem.redeemVersion,
|
|
1200
|
+
script: redeem.output,
|
|
1188
1201
|
controlBlock: p2tr.witness[p2tr.witness.length - 1]
|
|
1189
1202
|
};
|
|
1203
|
+
const stakingAmount = transaction.outs[outputIndex].value;
|
|
1204
|
+
const slashingAmount = Math.round(stakingAmount * slashingRate);
|
|
1205
|
+
const slashingOutput = Buffer.from(slashingPkScriptHex, "hex");
|
|
1206
|
+
if (opcodes3.OP_RETURN != slashingOutput[0]) {
|
|
1207
|
+
if (slashingAmount <= BTC_DUST_SAT) {
|
|
1208
|
+
throw new Error("Slashing amount is less than dust limit");
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
const userFunds = stakingAmount - slashingAmount - minimumFee;
|
|
1212
|
+
if (userFunds <= BTC_DUST_SAT) {
|
|
1213
|
+
throw new Error("User funds are less than dust limit");
|
|
1214
|
+
}
|
|
1215
|
+
const psbt = new Psbt2({ network });
|
|
1216
|
+
psbt.setVersion(TRANSACTION_VERSION);
|
|
1190
1217
|
psbt.addInput({
|
|
1191
|
-
hash:
|
|
1192
|
-
index:
|
|
1193
|
-
sequence: input.sequence,
|
|
1218
|
+
hash: transaction.getHash(),
|
|
1219
|
+
index: outputIndex,
|
|
1194
1220
|
tapInternalKey: internalPubkey,
|
|
1195
1221
|
witnessUtxo: {
|
|
1196
|
-
value:
|
|
1197
|
-
script:
|
|
1222
|
+
value: stakingAmount,
|
|
1223
|
+
script: transaction.outs[outputIndex].script
|
|
1198
1224
|
},
|
|
1199
|
-
tapLeafScript: [
|
|
1225
|
+
tapLeafScript: [tapLeafScript],
|
|
1226
|
+
// not RBF-able
|
|
1227
|
+
sequence: NON_RBF_SEQUENCE
|
|
1200
1228
|
});
|
|
1201
1229
|
psbt.addOutput({
|
|
1202
|
-
script:
|
|
1203
|
-
value:
|
|
1230
|
+
script: slashingOutput,
|
|
1231
|
+
value: slashingAmount
|
|
1204
1232
|
});
|
|
1205
|
-
|
|
1233
|
+
const changeOutput = payments5.p2tr({
|
|
1234
|
+
internalPubkey,
|
|
1235
|
+
scriptTree: { output: scripts.unbondingTimelockScript },
|
|
1236
|
+
network
|
|
1237
|
+
});
|
|
1238
|
+
psbt.addOutput({
|
|
1239
|
+
address: changeOutput.address,
|
|
1240
|
+
value: userFunds
|
|
1241
|
+
});
|
|
1242
|
+
psbt.setLocktime(0);
|
|
1243
|
+
return { psbt };
|
|
1244
|
+
}
|
|
1245
|
+
function unbondingTransaction(scripts, stakingTx, unbondingFee, network, outputIndex = 0) {
|
|
1246
|
+
if (unbondingFee <= 0) {
|
|
1247
|
+
throw new Error("Unbonding fee must be bigger than 0");
|
|
1248
|
+
}
|
|
1249
|
+
if (outputIndex < 0) {
|
|
1250
|
+
throw new Error("Output index must be bigger or equal to 0");
|
|
1251
|
+
}
|
|
1252
|
+
const tx = new Transaction3();
|
|
1253
|
+
tx.version = TRANSACTION_VERSION;
|
|
1254
|
+
tx.addInput(
|
|
1255
|
+
stakingTx.getHash(),
|
|
1256
|
+
outputIndex,
|
|
1257
|
+
NON_RBF_SEQUENCE
|
|
1258
|
+
// not RBF-able
|
|
1259
|
+
);
|
|
1260
|
+
const unbondingOutputInfo = deriveUnbondingOutputInfo(scripts, network);
|
|
1261
|
+
const outputValue = stakingTx.outs[outputIndex].value - unbondingFee;
|
|
1262
|
+
if (outputValue < BTC_DUST_SAT) {
|
|
1263
|
+
throw new Error("Output value is less than dust limit for unbonding transaction");
|
|
1264
|
+
}
|
|
1265
|
+
if (!unbondingOutputInfo.outputAddress) {
|
|
1266
|
+
throw new Error("Unbonding output address is not defined");
|
|
1267
|
+
}
|
|
1268
|
+
tx.addOutput(
|
|
1269
|
+
unbondingOutputInfo.scriptPubKey,
|
|
1270
|
+
outputValue
|
|
1271
|
+
);
|
|
1272
|
+
tx.locktime = 0;
|
|
1273
|
+
return {
|
|
1274
|
+
transaction: tx,
|
|
1275
|
+
fee: unbondingFee
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
var createCovenantWitness = (originalWitness, paramsCovenants, covenantSigs, covenantQuorum) => {
|
|
1279
|
+
if (covenantSigs.length < covenantQuorum) {
|
|
1280
|
+
throw new Error(
|
|
1281
|
+
`Not enough covenant signatures. Required: ${covenantQuorum}, got: ${covenantSigs.length}`
|
|
1282
|
+
);
|
|
1283
|
+
}
|
|
1284
|
+
const filteredCovenantSigs = covenantSigs.filter((sig) => {
|
|
1285
|
+
const btcPkHexBuf = Buffer.from(sig.btcPkHex, "hex");
|
|
1286
|
+
return paramsCovenants.some((covenant) => covenant.equals(btcPkHexBuf));
|
|
1287
|
+
});
|
|
1288
|
+
if (filteredCovenantSigs.length < covenantQuorum) {
|
|
1289
|
+
throw new Error(
|
|
1290
|
+
`Not enough valid covenant signatures. Required: ${covenantQuorum}, got: ${filteredCovenantSigs.length}`
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
const covenantSigsBuffers = covenantSigs.slice(0, covenantQuorum).map((sig) => ({
|
|
1294
|
+
btcPkHex: Buffer.from(sig.btcPkHex, "hex"),
|
|
1295
|
+
sigHex: Buffer.from(sig.sigHex, "hex")
|
|
1296
|
+
}));
|
|
1297
|
+
const paramsCovenantsSorted = [...paramsCovenants].sort(Buffer.compare).reverse();
|
|
1298
|
+
const composedCovenantSigs = paramsCovenantsSorted.map((covenant) => {
|
|
1299
|
+
const covenantSig = covenantSigsBuffers.find(
|
|
1300
|
+
(sig) => sig.btcPkHex.compare(covenant) === 0
|
|
1301
|
+
);
|
|
1302
|
+
return covenantSig?.sigHex || Buffer.alloc(0);
|
|
1303
|
+
});
|
|
1304
|
+
return [...composedCovenantSigs, ...originalWitness];
|
|
1305
|
+
};
|
|
1306
|
+
|
|
1307
|
+
// src/constants/unbonding.ts
|
|
1308
|
+
var MIN_UNBONDING_OUTPUT_VALUE = 1e3;
|
|
1309
|
+
|
|
1310
|
+
// src/utils/babylon.ts
|
|
1311
|
+
import { fromBech32 } from "@cosmjs/encoding";
|
|
1312
|
+
var isValidBabylonAddress = (address4) => {
|
|
1313
|
+
try {
|
|
1314
|
+
const { prefix } = fromBech32(address4);
|
|
1315
|
+
return prefix === "bbn";
|
|
1316
|
+
} catch (error) {
|
|
1317
|
+
return false;
|
|
1318
|
+
}
|
|
1319
|
+
};
|
|
1320
|
+
|
|
1321
|
+
// src/utils/staking/validation.ts
|
|
1322
|
+
var validateStakingExpansionInputs = ({
|
|
1323
|
+
babylonBtcTipHeight,
|
|
1324
|
+
inputUTXOs,
|
|
1325
|
+
stakingInput,
|
|
1326
|
+
previousStakingInput,
|
|
1327
|
+
babylonAddress
|
|
1328
|
+
}) => {
|
|
1329
|
+
if (babylonBtcTipHeight === 0) {
|
|
1330
|
+
throw new StakingError(
|
|
1331
|
+
"INVALID_INPUT" /* INVALID_INPUT */,
|
|
1332
|
+
"Babylon BTC tip height cannot be 0"
|
|
1333
|
+
);
|
|
1334
|
+
}
|
|
1335
|
+
if (!inputUTXOs || inputUTXOs.length === 0) {
|
|
1336
|
+
throw new StakingError(
|
|
1337
|
+
"INVALID_INPUT" /* INVALID_INPUT */,
|
|
1338
|
+
"No input UTXOs provided"
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
if (babylonAddress && !isValidBabylonAddress(babylonAddress)) {
|
|
1342
|
+
throw new StakingError(
|
|
1343
|
+
"INVALID_INPUT" /* INVALID_INPUT */,
|
|
1344
|
+
"Invalid Babylon address"
|
|
1345
|
+
);
|
|
1346
|
+
}
|
|
1347
|
+
if (stakingInput.stakingAmountSat !== previousStakingInput.stakingAmountSat) {
|
|
1348
|
+
throw new StakingError(
|
|
1349
|
+
"INVALID_INPUT" /* INVALID_INPUT */,
|
|
1350
|
+
"Staking expansion amount must equal the previous staking amount"
|
|
1351
|
+
);
|
|
1352
|
+
}
|
|
1353
|
+
const currentFPs = stakingInput.finalityProviderPksNoCoordHex;
|
|
1354
|
+
const previousFPs = previousStakingInput.finalityProviderPksNoCoordHex;
|
|
1355
|
+
const missingPreviousFPs = previousFPs.filter((prevFp) => !currentFPs.includes(prevFp));
|
|
1356
|
+
if (missingPreviousFPs.length > 0) {
|
|
1357
|
+
throw new StakingError(
|
|
1358
|
+
"INVALID_INPUT" /* INVALID_INPUT */,
|
|
1359
|
+
`Invalid staking expansion: all finality providers from the previous
|
|
1360
|
+
staking must be included. Missing: ${missingPreviousFPs.join(", ")}`
|
|
1361
|
+
);
|
|
1362
|
+
}
|
|
1363
|
+
};
|
|
1364
|
+
var validateStakingTxInputData = (stakingAmountSat, timelock, params, inputUTXOs, feeRate) => {
|
|
1365
|
+
if (stakingAmountSat < params.minStakingAmountSat || stakingAmountSat > params.maxStakingAmountSat) {
|
|
1366
|
+
throw new StakingError(
|
|
1367
|
+
"INVALID_INPUT" /* INVALID_INPUT */,
|
|
1368
|
+
"Invalid staking amount"
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1371
|
+
if (timelock < params.minStakingTimeBlocks || timelock > params.maxStakingTimeBlocks) {
|
|
1372
|
+
throw new StakingError("INVALID_INPUT" /* INVALID_INPUT */, "Invalid timelock");
|
|
1373
|
+
}
|
|
1374
|
+
if (inputUTXOs.length == 0) {
|
|
1375
|
+
throw new StakingError(
|
|
1376
|
+
"INVALID_INPUT" /* INVALID_INPUT */,
|
|
1377
|
+
"No input UTXOs provided"
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1380
|
+
if (feeRate <= 0) {
|
|
1381
|
+
throw new StakingError("INVALID_INPUT" /* INVALID_INPUT */, "Invalid fee rate");
|
|
1382
|
+
}
|
|
1383
|
+
};
|
|
1384
|
+
var validateParams = (params) => {
|
|
1385
|
+
if (params.covenantNoCoordPks.length == 0) {
|
|
1386
|
+
throw new StakingError(
|
|
1387
|
+
"INVALID_PARAMS" /* INVALID_PARAMS */,
|
|
1388
|
+
"Could not find any covenant public keys"
|
|
1389
|
+
);
|
|
1390
|
+
}
|
|
1391
|
+
if (params.covenantNoCoordPks.length < params.covenantQuorum) {
|
|
1392
|
+
throw new StakingError(
|
|
1393
|
+
"INVALID_PARAMS" /* INVALID_PARAMS */,
|
|
1394
|
+
"Covenant public keys must be greater than or equal to the quorum"
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1397
|
+
params.covenantNoCoordPks.forEach((pk) => {
|
|
1398
|
+
if (!isValidNoCoordPublicKey(pk)) {
|
|
1399
|
+
throw new StakingError(
|
|
1400
|
+
"INVALID_PARAMS" /* INVALID_PARAMS */,
|
|
1401
|
+
"Covenant public key should contains no coordinate"
|
|
1402
|
+
);
|
|
1403
|
+
}
|
|
1404
|
+
});
|
|
1405
|
+
if (params.unbondingTime <= 0) {
|
|
1406
|
+
throw new StakingError(
|
|
1407
|
+
"INVALID_PARAMS" /* INVALID_PARAMS */,
|
|
1408
|
+
"Unbonding time must be greater than 0"
|
|
1409
|
+
);
|
|
1410
|
+
}
|
|
1411
|
+
if (params.unbondingFeeSat <= 0) {
|
|
1412
|
+
throw new StakingError(
|
|
1413
|
+
"INVALID_PARAMS" /* INVALID_PARAMS */,
|
|
1414
|
+
"Unbonding fee must be greater than 0"
|
|
1415
|
+
);
|
|
1416
|
+
}
|
|
1417
|
+
if (params.maxStakingAmountSat < params.minStakingAmountSat) {
|
|
1418
|
+
throw new StakingError(
|
|
1419
|
+
"INVALID_PARAMS" /* INVALID_PARAMS */,
|
|
1420
|
+
"Max staking amount must be greater or equal to min staking amount"
|
|
1421
|
+
);
|
|
1422
|
+
}
|
|
1423
|
+
if (params.minStakingAmountSat < params.unbondingFeeSat + MIN_UNBONDING_OUTPUT_VALUE) {
|
|
1424
|
+
throw new StakingError(
|
|
1425
|
+
"INVALID_PARAMS" /* INVALID_PARAMS */,
|
|
1426
|
+
`Min staking amount must be greater than unbonding fee plus ${MIN_UNBONDING_OUTPUT_VALUE}`
|
|
1427
|
+
);
|
|
1428
|
+
}
|
|
1429
|
+
if (params.maxStakingTimeBlocks < params.minStakingTimeBlocks) {
|
|
1430
|
+
throw new StakingError(
|
|
1431
|
+
"INVALID_PARAMS" /* INVALID_PARAMS */,
|
|
1432
|
+
"Max staking time must be greater or equal to min staking time"
|
|
1433
|
+
);
|
|
1434
|
+
}
|
|
1435
|
+
if (params.minStakingTimeBlocks <= 0) {
|
|
1436
|
+
throw new StakingError(
|
|
1437
|
+
"INVALID_PARAMS" /* INVALID_PARAMS */,
|
|
1438
|
+
"Min staking time must be greater than 0"
|
|
1439
|
+
);
|
|
1440
|
+
}
|
|
1441
|
+
if (params.covenantQuorum <= 0) {
|
|
1442
|
+
throw new StakingError(
|
|
1443
|
+
"INVALID_PARAMS" /* INVALID_PARAMS */,
|
|
1444
|
+
"Covenant quorum must be greater than 0"
|
|
1445
|
+
);
|
|
1446
|
+
}
|
|
1447
|
+
if (params.slashing) {
|
|
1448
|
+
if (params.slashing.slashingRate <= 0) {
|
|
1449
|
+
throw new StakingError(
|
|
1450
|
+
"INVALID_PARAMS" /* INVALID_PARAMS */,
|
|
1451
|
+
"Slashing rate must be greater than 0"
|
|
1452
|
+
);
|
|
1453
|
+
}
|
|
1454
|
+
if (params.slashing.slashingRate > 1) {
|
|
1455
|
+
throw new StakingError(
|
|
1456
|
+
"INVALID_PARAMS" /* INVALID_PARAMS */,
|
|
1457
|
+
"Slashing rate must be less or equal to 1"
|
|
1458
|
+
);
|
|
1459
|
+
}
|
|
1460
|
+
if (params.slashing.slashingPkScriptHex.length == 0) {
|
|
1461
|
+
throw new StakingError(
|
|
1462
|
+
"INVALID_PARAMS" /* INVALID_PARAMS */,
|
|
1463
|
+
"Slashing public key script is missing"
|
|
1464
|
+
);
|
|
1465
|
+
}
|
|
1466
|
+
if (params.slashing.minSlashingTxFeeSat <= 0) {
|
|
1467
|
+
throw new StakingError(
|
|
1468
|
+
"INVALID_PARAMS" /* INVALID_PARAMS */,
|
|
1469
|
+
"Minimum slashing transaction fee must be greater than 0"
|
|
1470
|
+
);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1206
1473
|
};
|
|
1207
|
-
var
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
"
|
|
1474
|
+
var validateStakingTimelock = (stakingTimelock, params) => {
|
|
1475
|
+
if (stakingTimelock < params.minStakingTimeBlocks || stakingTimelock > params.maxStakingTimeBlocks) {
|
|
1476
|
+
throw new StakingError(
|
|
1477
|
+
"INVALID_INPUT" /* INVALID_INPUT */,
|
|
1478
|
+
"Staking transaction timelock is out of range"
|
|
1479
|
+
);
|
|
1480
|
+
}
|
|
1481
|
+
};
|
|
1482
|
+
var validateStakingExpansionCovenantQuorum = (paramsForPreviousStakingTx, paramsForCurrentStakingTx) => {
|
|
1483
|
+
const previousCovenantMembers = paramsForPreviousStakingTx.covenantNoCoordPks;
|
|
1484
|
+
const currentCovenantMembers = paramsForCurrentStakingTx.covenantNoCoordPks;
|
|
1485
|
+
const requiredQuorum = paramsForPreviousStakingTx.covenantQuorum;
|
|
1486
|
+
const activePreviousMembers = previousCovenantMembers.filter(
|
|
1487
|
+
(prevMember) => currentCovenantMembers.includes(prevMember)
|
|
1488
|
+
).length;
|
|
1489
|
+
if (activePreviousMembers < requiredQuorum) {
|
|
1490
|
+
throw new StakingError(
|
|
1491
|
+
"INVALID_INPUT" /* INVALID_INPUT */,
|
|
1492
|
+
`Staking expansion failed: insufficient covenant quorum. Required: ${requiredQuorum}, Available: ${activePreviousMembers}. Too many covenant members have rotated out.`
|
|
1212
1493
|
);
|
|
1213
1494
|
}
|
|
1214
1495
|
};
|
|
1215
1496
|
|
|
1216
1497
|
// src/staking/index.ts
|
|
1217
|
-
var Staking = class {
|
|
1218
|
-
constructor(network, stakerInfo, params,
|
|
1498
|
+
var Staking = class _Staking {
|
|
1499
|
+
constructor(network, stakerInfo, params, finalityProviderPksNoCoordHex, stakingTimelock) {
|
|
1219
1500
|
if (!isValidBitcoinAddress(stakerInfo.address, network)) {
|
|
1220
1501
|
throw new StakingError(
|
|
1221
1502
|
"INVALID_INPUT" /* INVALID_INPUT */,
|
|
@@ -1228,10 +1509,10 @@ var Staking = class {
|
|
|
1228
1509
|
"Invalid staker public key"
|
|
1229
1510
|
);
|
|
1230
1511
|
}
|
|
1231
|
-
if (!isValidNoCoordPublicKey
|
|
1512
|
+
if (finalityProviderPksNoCoordHex.length === 0 || !finalityProviderPksNoCoordHex.every(isValidNoCoordPublicKey)) {
|
|
1232
1513
|
throw new StakingError(
|
|
1233
1514
|
"INVALID_INPUT" /* INVALID_INPUT */,
|
|
1234
|
-
"Invalid finality
|
|
1515
|
+
"Invalid finality providers public keys"
|
|
1235
1516
|
);
|
|
1236
1517
|
}
|
|
1237
1518
|
validateParams(params);
|
|
@@ -1239,14 +1520,14 @@ var Staking = class {
|
|
|
1239
1520
|
this.network = network;
|
|
1240
1521
|
this.stakerInfo = stakerInfo;
|
|
1241
1522
|
this.params = params;
|
|
1242
|
-
this.
|
|
1523
|
+
this.finalityProviderPksNoCoordHex = finalityProviderPksNoCoordHex;
|
|
1243
1524
|
this.stakingTimelock = stakingTimelock;
|
|
1244
1525
|
}
|
|
1245
1526
|
/**
|
|
1246
1527
|
* buildScripts builds the staking scripts for the staking transaction.
|
|
1247
1528
|
* Note: different staking types may have different scripts.
|
|
1248
1529
|
* e.g the observable staking script has a data embed script.
|
|
1249
|
-
*
|
|
1530
|
+
*
|
|
1250
1531
|
* @returns {StakingScripts} - The staking scripts.
|
|
1251
1532
|
*/
|
|
1252
1533
|
buildScripts() {
|
|
@@ -1255,7 +1536,7 @@ var Staking = class {
|
|
|
1255
1536
|
try {
|
|
1256
1537
|
stakingScriptData = new StakingScriptData(
|
|
1257
1538
|
Buffer.from(this.stakerInfo.publicKeyNoCoordHex, "hex"),
|
|
1258
|
-
|
|
1539
|
+
this.finalityProviderPksNoCoordHex.map((pk) => Buffer.from(pk, "hex")),
|
|
1259
1540
|
toBuffers(covenantNoCoordPks),
|
|
1260
1541
|
covenantQuorum,
|
|
1261
1542
|
this.stakingTimelock,
|
|
@@ -1282,9 +1563,9 @@ var Staking = class {
|
|
|
1282
1563
|
}
|
|
1283
1564
|
/**
|
|
1284
1565
|
* Create a staking transaction for staking.
|
|
1285
|
-
*
|
|
1566
|
+
*
|
|
1286
1567
|
* @param {number} stakingAmountSat - The amount to stake in satoshis.
|
|
1287
|
-
* @param {UTXO[]} inputUTXOs - The UTXOs to use as inputs for the staking
|
|
1568
|
+
* @param {UTXO[]} inputUTXOs - The UTXOs to use as inputs for the staking
|
|
1288
1569
|
* transaction.
|
|
1289
1570
|
* @param {number} feeRate - The fee rate for the transaction in satoshis per byte.
|
|
1290
1571
|
* @returns {TransactionResult} - An object containing the unsigned
|
|
@@ -1322,10 +1603,85 @@ var Staking = class {
|
|
|
1322
1603
|
}
|
|
1323
1604
|
}
|
|
1324
1605
|
/**
|
|
1325
|
-
*
|
|
1606
|
+
* Creates a staking expansion transaction that extends an existing BTC stake
|
|
1607
|
+
* to new finality providers or renews the timelock.
|
|
1326
1608
|
*
|
|
1609
|
+
* This method implements RFC 037 BTC Stake Expansion,
|
|
1610
|
+
* allowing existing active BTC staking transactions
|
|
1611
|
+
* to extend their delegation to new finality providers without going through
|
|
1612
|
+
* the full unbonding process.
|
|
1613
|
+
*
|
|
1614
|
+
* The expansion transaction:
|
|
1615
|
+
* 1. Spends the previous staking transaction output as the first input
|
|
1616
|
+
* 2. Uses funding UTXO as additional input to cover transaction fees or
|
|
1617
|
+
* to increase the staking amount
|
|
1618
|
+
* 3. Creates a new staking output with expanded finality provider coverage or
|
|
1619
|
+
* renews the timelock
|
|
1620
|
+
* 4. Has an output returning the remaining funds as change (if any) to the
|
|
1621
|
+
* staker BTC address
|
|
1622
|
+
*
|
|
1623
|
+
* @param {number} stakingAmountSat - The total staking amount in satoshis
|
|
1624
|
+
* (The amount had to be equal to the previous staking amount for now, this
|
|
1625
|
+
* lib does not yet support increasing the staking amount at this stage)
|
|
1626
|
+
* @param {UTXO[]} inputUTXOs - Available UTXOs to use for funding the
|
|
1627
|
+
* expansion transaction fees. Only one will be selected for the expansion
|
|
1628
|
+
* @param {number} feeRate - Fee rate in satoshis per byte for the
|
|
1629
|
+
* expansion transaction
|
|
1630
|
+
* @param {StakingParams} paramsForPreviousStakingTx - Staking parameters
|
|
1631
|
+
* used in the previous staking transaction
|
|
1632
|
+
* @param {Object} previousStakingTxInfo - Necessary information to spend the
|
|
1633
|
+
* previous staking transaction.
|
|
1634
|
+
* @returns {TransactionResult & { fundingUTXO: UTXO }} - An object containing
|
|
1635
|
+
* the unsigned expansion transaction and calculated fee, and the funding UTXO
|
|
1636
|
+
* @throws {StakingError} - If the transaction cannot be built or validation
|
|
1637
|
+
* fails
|
|
1638
|
+
*/
|
|
1639
|
+
createStakingExpansionTransaction(stakingAmountSat, inputUTXOs, feeRate, paramsForPreviousStakingTx, previousStakingTxInfo) {
|
|
1640
|
+
validateStakingTxInputData(
|
|
1641
|
+
stakingAmountSat,
|
|
1642
|
+
this.stakingTimelock,
|
|
1643
|
+
this.params,
|
|
1644
|
+
inputUTXOs,
|
|
1645
|
+
feeRate
|
|
1646
|
+
);
|
|
1647
|
+
validateStakingExpansionCovenantQuorum(
|
|
1648
|
+
paramsForPreviousStakingTx,
|
|
1649
|
+
this.params
|
|
1650
|
+
);
|
|
1651
|
+
const previousStaking = new _Staking(
|
|
1652
|
+
this.network,
|
|
1653
|
+
this.stakerInfo,
|
|
1654
|
+
paramsForPreviousStakingTx,
|
|
1655
|
+
previousStakingTxInfo.stakingInput.finalityProviderPksNoCoordHex,
|
|
1656
|
+
previousStakingTxInfo.stakingInput.stakingTimelock
|
|
1657
|
+
);
|
|
1658
|
+
const {
|
|
1659
|
+
transaction: stakingExpansionTx,
|
|
1660
|
+
fee: stakingExpansionTxFee,
|
|
1661
|
+
fundingUTXO
|
|
1662
|
+
} = stakingExpansionTransaction(
|
|
1663
|
+
this.network,
|
|
1664
|
+
this.buildScripts(),
|
|
1665
|
+
stakingAmountSat,
|
|
1666
|
+
this.stakerInfo.address,
|
|
1667
|
+
feeRate,
|
|
1668
|
+
inputUTXOs,
|
|
1669
|
+
{
|
|
1670
|
+
stakingTx: previousStakingTxInfo.stakingTx,
|
|
1671
|
+
scripts: previousStaking.buildScripts()
|
|
1672
|
+
}
|
|
1673
|
+
);
|
|
1674
|
+
return {
|
|
1675
|
+
transaction: stakingExpansionTx,
|
|
1676
|
+
fee: stakingExpansionTxFee,
|
|
1677
|
+
fundingUTXO
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
/**
|
|
1681
|
+
* Create a staking psbt based on the existing staking transaction.
|
|
1682
|
+
*
|
|
1327
1683
|
* @param {Transaction} stakingTx - The staking transaction.
|
|
1328
|
-
* @param {UTXO[]} inputUTXOs - The UTXOs to use as inputs for the staking
|
|
1684
|
+
* @param {UTXO[]} inputUTXOs - The UTXOs to use as inputs for the staking
|
|
1329
1685
|
* transaction. The UTXOs that were used to create the staking transaction should
|
|
1330
1686
|
* be included in this array.
|
|
1331
1687
|
* @returns {Psbt} - The psbt.
|
|
@@ -1342,15 +1698,54 @@ var Staking = class {
|
|
|
1342
1698
|
stakingTx,
|
|
1343
1699
|
this.network,
|
|
1344
1700
|
inputUTXOs,
|
|
1345
|
-
isTaproot(
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1701
|
+
isTaproot(this.stakerInfo.address, this.network) ? Buffer.from(this.stakerInfo.publicKeyNoCoordHex, "hex") : void 0
|
|
1702
|
+
);
|
|
1703
|
+
}
|
|
1704
|
+
/**
|
|
1705
|
+
* Convert a staking expansion transaction to a PSBT.
|
|
1706
|
+
*
|
|
1707
|
+
* @param {Transaction} stakingExpansionTx - The staking expansion
|
|
1708
|
+
* transaction to convert
|
|
1709
|
+
* @param {UTXO[]} inputUTXOs - Available UTXOs for the
|
|
1710
|
+
* funding input (second input)
|
|
1711
|
+
* @param {StakingParams} paramsForPreviousStakingTx - Staking parameters
|
|
1712
|
+
* used for the previous staking transaction
|
|
1713
|
+
* @param {Object} previousStakingTxInfo - Information about the previous
|
|
1714
|
+
* staking transaction
|
|
1715
|
+
* @returns {Psbt} The PSBT for the staking expansion transaction
|
|
1716
|
+
* @throws {Error} If the previous staking output cannot be found or
|
|
1717
|
+
* validation fails
|
|
1718
|
+
*/
|
|
1719
|
+
toStakingExpansionPsbt(stakingExpansionTx, inputUTXOs, paramsForPreviousStakingTx, previousStakingTxInfo) {
|
|
1720
|
+
const previousStaking = new _Staking(
|
|
1721
|
+
this.network,
|
|
1722
|
+
this.stakerInfo,
|
|
1723
|
+
paramsForPreviousStakingTx,
|
|
1724
|
+
previousStakingTxInfo.stakingInput.finalityProviderPksNoCoordHex,
|
|
1725
|
+
previousStakingTxInfo.stakingInput.stakingTimelock
|
|
1726
|
+
);
|
|
1727
|
+
const previousScripts = previousStaking.buildScripts();
|
|
1728
|
+
const { outputAddress } = deriveStakingOutputInfo(previousScripts, this.network);
|
|
1729
|
+
const previousStakingOutputIndex = findMatchingTxOutputIndex(
|
|
1730
|
+
previousStakingTxInfo.stakingTx,
|
|
1731
|
+
outputAddress,
|
|
1732
|
+
this.network
|
|
1733
|
+
);
|
|
1734
|
+
return stakingExpansionPsbt(
|
|
1735
|
+
this.network,
|
|
1736
|
+
stakingExpansionTx,
|
|
1737
|
+
{
|
|
1738
|
+
stakingTx: previousStakingTxInfo.stakingTx,
|
|
1739
|
+
outputIndex: previousStakingOutputIndex
|
|
1740
|
+
},
|
|
1741
|
+
inputUTXOs,
|
|
1742
|
+
previousScripts,
|
|
1743
|
+
isTaproot(this.stakerInfo.address, this.network) ? Buffer.from(this.stakerInfo.publicKeyNoCoordHex, "hex") : void 0
|
|
1349
1744
|
);
|
|
1350
1745
|
}
|
|
1351
1746
|
/**
|
|
1352
1747
|
* Create an unbonding transaction for staking.
|
|
1353
|
-
*
|
|
1748
|
+
*
|
|
1354
1749
|
* @param {Transaction} stakingTx - The staking transaction to unbond.
|
|
1355
1750
|
* @returns {TransactionResult} - An object containing the unsigned
|
|
1356
1751
|
* transaction, and fee
|
|
@@ -1387,10 +1782,10 @@ var Staking = class {
|
|
|
1387
1782
|
/**
|
|
1388
1783
|
* Create an unbonding psbt based on the existing unbonding transaction and
|
|
1389
1784
|
* staking transaction.
|
|
1390
|
-
*
|
|
1785
|
+
*
|
|
1391
1786
|
* @param {Transaction} unbondingTx - The unbonding transaction.
|
|
1392
1787
|
* @param {Transaction} stakingTx - The staking transaction.
|
|
1393
|
-
*
|
|
1788
|
+
*
|
|
1394
1789
|
* @returns {Psbt} - The psbt.
|
|
1395
1790
|
*/
|
|
1396
1791
|
toUnbondingPsbt(unbondingTx, stakingTx) {
|
|
@@ -1405,7 +1800,7 @@ var Staking = class {
|
|
|
1405
1800
|
* Creates a withdrawal transaction that spends from an unbonding or slashing
|
|
1406
1801
|
* transaction. The timelock on the input transaction must have expired before
|
|
1407
1802
|
* this withdrawal can be valid.
|
|
1408
|
-
*
|
|
1803
|
+
*
|
|
1409
1804
|
* @param {Transaction} earlyUnbondedTx - The unbonding or slashing
|
|
1410
1805
|
* transaction to withdraw from
|
|
1411
1806
|
* @param {number} feeRate - Fee rate in satoshis per byte for the withdrawal
|
|
@@ -1433,9 +1828,9 @@ var Staking = class {
|
|
|
1433
1828
|
}
|
|
1434
1829
|
}
|
|
1435
1830
|
/**
|
|
1436
|
-
* Create a withdrawal psbt that spends a naturally expired staking
|
|
1831
|
+
* Create a withdrawal psbt that spends a naturally expired staking
|
|
1437
1832
|
* transaction.
|
|
1438
|
-
*
|
|
1833
|
+
*
|
|
1439
1834
|
* @param {Transaction} stakingTx - The staking transaction to withdraw from.
|
|
1440
1835
|
* @param {number} feeRate - The fee rate for the transaction in satoshis per byte.
|
|
1441
1836
|
* @returns {PsbtResult} - An object containing the unsigned psbt and fee
|
|
@@ -1468,7 +1863,7 @@ var Staking = class {
|
|
|
1468
1863
|
}
|
|
1469
1864
|
/**
|
|
1470
1865
|
* Create a slashing psbt spending from the staking output.
|
|
1471
|
-
*
|
|
1866
|
+
*
|
|
1472
1867
|
* @param {Transaction} stakingTx - The staking transaction to slash.
|
|
1473
1868
|
* @returns {PsbtResult} - An object containing the unsigned psbt and fee
|
|
1474
1869
|
* @throws {StakingError} - If the delegation is invalid or the transaction cannot be built
|
|
@@ -1481,6 +1876,12 @@ var Staking = class {
|
|
|
1481
1876
|
);
|
|
1482
1877
|
}
|
|
1483
1878
|
const scripts = this.buildScripts();
|
|
1879
|
+
const { outputAddress } = deriveStakingOutputInfo(scripts, this.network);
|
|
1880
|
+
const stakingOutputIndex = findMatchingTxOutputIndex(
|
|
1881
|
+
stakingTx,
|
|
1882
|
+
outputAddress,
|
|
1883
|
+
this.network
|
|
1884
|
+
);
|
|
1484
1885
|
try {
|
|
1485
1886
|
const { psbt } = slashTimelockUnbondedTransaction(
|
|
1486
1887
|
scripts,
|
|
@@ -1488,7 +1889,8 @@ var Staking = class {
|
|
|
1488
1889
|
this.params.slashing.slashingPkScriptHex,
|
|
1489
1890
|
this.params.slashing.slashingRate,
|
|
1490
1891
|
this.params.slashing.minSlashingTxFeeSat,
|
|
1491
|
-
this.network
|
|
1892
|
+
this.network,
|
|
1893
|
+
stakingOutputIndex
|
|
1492
1894
|
);
|
|
1493
1895
|
return {
|
|
1494
1896
|
psbt,
|
|
@@ -1504,7 +1906,7 @@ var Staking = class {
|
|
|
1504
1906
|
}
|
|
1505
1907
|
/**
|
|
1506
1908
|
* Create a slashing psbt for an unbonding output.
|
|
1507
|
-
*
|
|
1909
|
+
*
|
|
1508
1910
|
* @param {Transaction} unbondingTx - The unbonding transaction to slash.
|
|
1509
1911
|
* @returns {PsbtResult} - An object containing the unsigned psbt and fee
|
|
1510
1912
|
* @throws {StakingError} - If the delegation is invalid or the transaction cannot be built
|
|
@@ -1541,7 +1943,7 @@ var Staking = class {
|
|
|
1541
1943
|
/**
|
|
1542
1944
|
* Create a withdraw slashing psbt that spends a slashing transaction from the
|
|
1543
1945
|
* staking output.
|
|
1544
|
-
*
|
|
1946
|
+
*
|
|
1545
1947
|
* @param {Transaction} slashingTx - The slashing transaction.
|
|
1546
1948
|
* @param {number} feeRate - The fee rate for the transaction in satoshis per byte.
|
|
1547
1949
|
* @returns {PsbtResult} - An object containing the unsigned psbt and fee
|
|
@@ -1561,267 +1963,37 @@ var Staking = class {
|
|
|
1561
1963
|
slashingTx,
|
|
1562
1964
|
this.stakerInfo.address,
|
|
1563
1965
|
this.network,
|
|
1564
|
-
feeRate,
|
|
1565
|
-
slashingOutputIndex
|
|
1566
|
-
);
|
|
1567
|
-
} catch (error) {
|
|
1568
|
-
throw StakingError.fromUnknown(
|
|
1569
|
-
error,
|
|
1570
|
-
"BUILD_TRANSACTION_FAILURE" /* BUILD_TRANSACTION_FAILURE */,
|
|
1571
|
-
"Cannot build withdraw slashing transaction"
|
|
1572
|
-
);
|
|
1573
|
-
}
|
|
1574
|
-
}
|
|
1575
|
-
};
|
|
1576
|
-
|
|
1577
|
-
// src/staking/observable/observableStakingScript.ts
|
|
1578
|
-
import { opcodes as opcodes3, script as script3 } from "bitcoinjs-lib";
|
|
1579
|
-
var ObservableStakingScriptData = class extends StakingScriptData {
|
|
1580
|
-
constructor(stakerKey, finalityProviderKeys, covenantKeys, covenantThreshold, stakingTimelock, unbondingTimelock, magicBytes) {
|
|
1581
|
-
super(
|
|
1582
|
-
stakerKey,
|
|
1583
|
-
finalityProviderKeys,
|
|
1584
|
-
covenantKeys,
|
|
1585
|
-
covenantThreshold,
|
|
1586
|
-
stakingTimelock,
|
|
1587
|
-
unbondingTimelock
|
|
1588
|
-
);
|
|
1589
|
-
if (!magicBytes) {
|
|
1590
|
-
throw new Error("Missing required input values");
|
|
1591
|
-
}
|
|
1592
|
-
if (magicBytes.length != MAGIC_BYTES_LEN) {
|
|
1593
|
-
throw new Error("Invalid script data provided");
|
|
1594
|
-
}
|
|
1595
|
-
this.magicBytes = magicBytes;
|
|
1596
|
-
}
|
|
1597
|
-
/**
|
|
1598
|
-
* Builds a data embed script for staking in the form:
|
|
1599
|
-
* OP_RETURN || <serializedStakingData>
|
|
1600
|
-
* where serializedStakingData is the concatenation of:
|
|
1601
|
-
* MagicBytes || Version || StakerPublicKey || FinalityProviderPublicKey || StakingTimeLock
|
|
1602
|
-
* Note: Only a single finality provider key is supported for now in phase 1
|
|
1603
|
-
* @throws {Error} If the number of finality provider keys is not equal to 1.
|
|
1604
|
-
* @returns {Buffer} The compiled data embed script.
|
|
1605
|
-
*/
|
|
1606
|
-
buildDataEmbedScript() {
|
|
1607
|
-
if (this.finalityProviderKeys.length != 1) {
|
|
1608
|
-
throw new Error("Only a single finality provider key is supported");
|
|
1609
|
-
}
|
|
1610
|
-
const version = Buffer.alloc(1);
|
|
1611
|
-
version.writeUInt8(0);
|
|
1612
|
-
const stakingTimeLock = Buffer.alloc(2);
|
|
1613
|
-
stakingTimeLock.writeUInt16BE(this.stakingTimeLock);
|
|
1614
|
-
const serializedStakingData = Buffer.concat([
|
|
1615
|
-
this.magicBytes,
|
|
1616
|
-
version,
|
|
1617
|
-
this.stakerKey,
|
|
1618
|
-
this.finalityProviderKeys[0],
|
|
1619
|
-
stakingTimeLock
|
|
1620
|
-
]);
|
|
1621
|
-
return script3.compile([opcodes3.OP_RETURN, serializedStakingData]);
|
|
1622
|
-
}
|
|
1623
|
-
/**
|
|
1624
|
-
* Builds the staking scripts.
|
|
1625
|
-
* @returns {ObservableStakingScripts} The staking scripts that can be used to stake.
|
|
1626
|
-
* contains the timelockScript, unbondingScript, slashingScript,
|
|
1627
|
-
* unbondingTimelockScript, and dataEmbedScript.
|
|
1628
|
-
* @throws {Error} If script data is invalid.
|
|
1629
|
-
*/
|
|
1630
|
-
buildScripts() {
|
|
1631
|
-
const scripts = super.buildScripts();
|
|
1632
|
-
return {
|
|
1633
|
-
...scripts,
|
|
1634
|
-
dataEmbedScript: this.buildDataEmbedScript()
|
|
1635
|
-
};
|
|
1636
|
-
}
|
|
1637
|
-
};
|
|
1638
|
-
|
|
1639
|
-
// src/staking/observable/index.ts
|
|
1640
|
-
var ObservableStaking = class extends Staking {
|
|
1641
|
-
constructor(network, stakerInfo, params, finalityProviderPkNoCoordHex, stakingTimelock) {
|
|
1642
|
-
super(
|
|
1643
|
-
network,
|
|
1644
|
-
stakerInfo,
|
|
1645
|
-
params,
|
|
1646
|
-
finalityProviderPkNoCoordHex,
|
|
1647
|
-
stakingTimelock
|
|
1648
|
-
);
|
|
1649
|
-
if (!params.tag) {
|
|
1650
|
-
throw new StakingError(
|
|
1651
|
-
"INVALID_INPUT" /* INVALID_INPUT */,
|
|
1652
|
-
"Observable staking parameters must include tag"
|
|
1653
|
-
);
|
|
1654
|
-
}
|
|
1655
|
-
if (!params.btcActivationHeight) {
|
|
1656
|
-
throw new StakingError(
|
|
1657
|
-
"INVALID_INPUT" /* INVALID_INPUT */,
|
|
1658
|
-
"Observable staking parameters must include a positive activation height"
|
|
1659
|
-
);
|
|
1660
|
-
}
|
|
1661
|
-
this.params = params;
|
|
1662
|
-
}
|
|
1663
|
-
/**
|
|
1664
|
-
* Build the staking scripts for observable staking.
|
|
1665
|
-
* This method overwrites the base method to include the OP_RETURN tag based
|
|
1666
|
-
* on the tag provided in the parameters.
|
|
1667
|
-
*
|
|
1668
|
-
* @returns {ObservableStakingScripts} - The staking scripts for observable staking.
|
|
1669
|
-
* @throws {StakingError} - If the scripts cannot be built.
|
|
1670
|
-
*/
|
|
1671
|
-
buildScripts() {
|
|
1672
|
-
const { covenantQuorum, covenantNoCoordPks, unbondingTime, tag } = this.params;
|
|
1673
|
-
let stakingScriptData;
|
|
1674
|
-
try {
|
|
1675
|
-
stakingScriptData = new ObservableStakingScriptData(
|
|
1676
|
-
Buffer.from(this.stakerInfo.publicKeyNoCoordHex, "hex"),
|
|
1677
|
-
[Buffer.from(this.finalityProviderPkNoCoordHex, "hex")],
|
|
1678
|
-
toBuffers(covenantNoCoordPks),
|
|
1679
|
-
covenantQuorum,
|
|
1680
|
-
this.stakingTimelock,
|
|
1681
|
-
unbondingTime,
|
|
1682
|
-
Buffer.from(tag, "hex")
|
|
1683
|
-
);
|
|
1684
|
-
} catch (error) {
|
|
1685
|
-
throw StakingError.fromUnknown(
|
|
1686
|
-
error,
|
|
1687
|
-
"SCRIPT_FAILURE" /* SCRIPT_FAILURE */,
|
|
1688
|
-
"Cannot build staking script data"
|
|
1689
|
-
);
|
|
1690
|
-
}
|
|
1691
|
-
let scripts;
|
|
1692
|
-
try {
|
|
1693
|
-
scripts = stakingScriptData.buildScripts();
|
|
1694
|
-
} catch (error) {
|
|
1695
|
-
throw StakingError.fromUnknown(
|
|
1696
|
-
error,
|
|
1697
|
-
"SCRIPT_FAILURE" /* SCRIPT_FAILURE */,
|
|
1698
|
-
"Cannot build staking scripts"
|
|
1699
|
-
);
|
|
1700
|
-
}
|
|
1701
|
-
return scripts;
|
|
1702
|
-
}
|
|
1703
|
-
/**
|
|
1704
|
-
* Create a staking transaction for observable staking.
|
|
1705
|
-
* This overwrites the method from the Staking class with the addtion
|
|
1706
|
-
* of the
|
|
1707
|
-
* 1. OP_RETURN tag in the staking scripts
|
|
1708
|
-
* 2. lockHeight parameter
|
|
1709
|
-
*
|
|
1710
|
-
* @param {number} stakingAmountSat - The amount to stake in satoshis.
|
|
1711
|
-
* @param {UTXO[]} inputUTXOs - The UTXOs to use as inputs for the staking
|
|
1712
|
-
* transaction.
|
|
1713
|
-
* @param {number} feeRate - The fee rate for the transaction in satoshis per byte.
|
|
1714
|
-
* @returns {TransactionResult} - An object containing the unsigned transaction,
|
|
1715
|
-
* and fee
|
|
1716
|
-
*/
|
|
1717
|
-
createStakingTransaction(stakingAmountSat, inputUTXOs, feeRate) {
|
|
1718
|
-
validateStakingTxInputData(
|
|
1719
|
-
stakingAmountSat,
|
|
1720
|
-
this.stakingTimelock,
|
|
1721
|
-
this.params,
|
|
1722
|
-
inputUTXOs,
|
|
1723
|
-
feeRate
|
|
1724
|
-
);
|
|
1725
|
-
const scripts = this.buildScripts();
|
|
1726
|
-
try {
|
|
1727
|
-
const { transaction, fee } = stakingTransaction(
|
|
1728
|
-
scripts,
|
|
1729
|
-
stakingAmountSat,
|
|
1730
|
-
this.stakerInfo.address,
|
|
1731
|
-
inputUTXOs,
|
|
1732
|
-
this.network,
|
|
1733
|
-
feeRate,
|
|
1734
|
-
// `lockHeight` is exclusive of the provided value.
|
|
1735
|
-
// For example, if a Bitcoin height of X is provided,
|
|
1736
|
-
// the transaction will be included starting from height X+1.
|
|
1737
|
-
// https://learnmeabitcoin.com/technical/transaction/locktime/
|
|
1738
|
-
this.params.btcActivationHeight - 1
|
|
1739
|
-
);
|
|
1740
|
-
return {
|
|
1741
|
-
transaction,
|
|
1742
|
-
fee
|
|
1743
|
-
};
|
|
1966
|
+
feeRate,
|
|
1967
|
+
slashingOutputIndex
|
|
1968
|
+
);
|
|
1744
1969
|
} catch (error) {
|
|
1745
1970
|
throw StakingError.fromUnknown(
|
|
1746
1971
|
error,
|
|
1747
1972
|
"BUILD_TRANSACTION_FAILURE" /* BUILD_TRANSACTION_FAILURE */,
|
|
1748
|
-
"Cannot build
|
|
1973
|
+
"Cannot build withdraw slashing transaction"
|
|
1749
1974
|
);
|
|
1750
1975
|
}
|
|
1751
1976
|
}
|
|
1752
|
-
/**
|
|
1753
|
-
* Create a staking psbt for observable staking.
|
|
1754
|
-
*
|
|
1755
|
-
* @param {Transaction} stakingTx - The staking transaction.
|
|
1756
|
-
* @param {UTXO[]} inputUTXOs - The UTXOs to use as inputs for the staking
|
|
1757
|
-
* transaction.
|
|
1758
|
-
* @returns {Psbt} - The psbt.
|
|
1759
|
-
*/
|
|
1760
|
-
toStakingPsbt(stakingTx, inputUTXOs) {
|
|
1761
|
-
return stakingPsbt(
|
|
1762
|
-
stakingTx,
|
|
1763
|
-
this.network,
|
|
1764
|
-
inputUTXOs,
|
|
1765
|
-
isTaproot(
|
|
1766
|
-
this.stakerInfo.address,
|
|
1767
|
-
this.network
|
|
1768
|
-
) ? Buffer.from(this.stakerInfo.publicKeyNoCoordHex, "hex") : void 0
|
|
1769
|
-
);
|
|
1770
|
-
}
|
|
1771
|
-
};
|
|
1772
|
-
|
|
1773
|
-
// src/utils/babylon.ts
|
|
1774
|
-
import { fromBech32 } from "@cosmjs/encoding";
|
|
1775
|
-
var isValidBabylonAddress = (address4) => {
|
|
1776
|
-
try {
|
|
1777
|
-
const { prefix } = fromBech32(address4);
|
|
1778
|
-
return prefix === "bbn";
|
|
1779
|
-
} catch (error) {
|
|
1780
|
-
return false;
|
|
1781
|
-
}
|
|
1782
|
-
};
|
|
1783
|
-
|
|
1784
|
-
// src/utils/staking/param.ts
|
|
1785
|
-
var getBabylonParamByBtcHeight = (height, babylonParamsVersions) => {
|
|
1786
|
-
const sortedParams = [...babylonParamsVersions].sort(
|
|
1787
|
-
(a, b) => b.btcActivationHeight - a.btcActivationHeight
|
|
1788
|
-
);
|
|
1789
|
-
const params = sortedParams.find(
|
|
1790
|
-
(p) => height >= p.btcActivationHeight
|
|
1791
|
-
);
|
|
1792
|
-
if (!params)
|
|
1793
|
-
throw new Error(`Babylon params not found for height ${height}`);
|
|
1794
|
-
return params;
|
|
1795
|
-
};
|
|
1796
|
-
var getBabylonParamByVersion = (version, babylonParams) => {
|
|
1797
|
-
const params = babylonParams.find((p) => p.version === version);
|
|
1798
|
-
if (!params)
|
|
1799
|
-
throw new Error(`Babylon params not found for version ${version}`);
|
|
1800
|
-
return params;
|
|
1801
1977
|
};
|
|
1802
1978
|
|
|
1803
1979
|
// src/staking/manager.ts
|
|
1804
|
-
import {
|
|
1805
|
-
import { fromBech32 as fromBech322 } from "@cosmjs/encoding";
|
|
1806
|
-
import {
|
|
1807
|
-
btccheckpoint,
|
|
1808
|
-
btcstaking,
|
|
1809
|
-
btcstakingtx
|
|
1810
|
-
} from "@babylonlabs-io/babylon-proto-ts";
|
|
1980
|
+
import { btccheckpoint, btcstaking, btcstakingtx } from "@babylonlabs-io/babylon-proto-ts";
|
|
1811
1981
|
import {
|
|
1982
|
+
BIP322Sig,
|
|
1812
1983
|
BTCSigType
|
|
1813
1984
|
} from "@babylonlabs-io/babylon-proto-ts/dist/generated/babylon/btcstaking/v1/pop";
|
|
1985
|
+
import { Psbt as Psbt3, Transaction as Transaction4 } from "bitcoinjs-lib";
|
|
1814
1986
|
|
|
1815
1987
|
// src/constants/registry.ts
|
|
1816
1988
|
var BABYLON_REGISTRY_TYPE_URLS = {
|
|
1817
|
-
MsgCreateBTCDelegation: "/babylon.btcstaking.v1.MsgCreateBTCDelegation"
|
|
1989
|
+
MsgCreateBTCDelegation: "/babylon.btcstaking.v1.MsgCreateBTCDelegation",
|
|
1990
|
+
MsgBtcStakeExpand: "/babylon.btcstaking.v1.MsgBtcStakeExpand"
|
|
1818
1991
|
};
|
|
1819
1992
|
|
|
1820
1993
|
// src/utils/index.ts
|
|
1821
1994
|
var reverseBuffer = (buffer) => {
|
|
1822
1995
|
const clonedBuffer = new Uint8Array(buffer);
|
|
1823
|
-
if (clonedBuffer.length < 1)
|
|
1824
|
-
return clonedBuffer;
|
|
1996
|
+
if (clonedBuffer.length < 1) return clonedBuffer;
|
|
1825
1997
|
for (let i = 0, j = clonedBuffer.length - 1; i < clonedBuffer.length / 2; i++, j--) {
|
|
1826
1998
|
let tmp = clonedBuffer[i];
|
|
1827
1999
|
clonedBuffer[i] = clonedBuffer[j];
|
|
@@ -1829,32 +2001,57 @@ var reverseBuffer = (buffer) => {
|
|
|
1829
2001
|
}
|
|
1830
2002
|
return clonedBuffer;
|
|
1831
2003
|
};
|
|
1832
|
-
|
|
1833
|
-
|
|
2004
|
+
|
|
2005
|
+
// src/utils/pop.ts
|
|
2006
|
+
import { sha256 } from "bitcoinjs-lib/src/crypto";
|
|
2007
|
+
|
|
2008
|
+
// src/constants/staking.ts
|
|
2009
|
+
var STAKING_MODULE_ADDRESS = "bbn13837feaxn8t0zvwcjwhw7lhpgdcx4s36eqteah";
|
|
2010
|
+
|
|
2011
|
+
// src/utils/pop.ts
|
|
2012
|
+
function createStakerPopContext(chainId, popContextVersion = 0) {
|
|
2013
|
+
const contextString = `btcstaking/${popContextVersion}/staker_pop/${chainId}/${STAKING_MODULE_ADDRESS}`;
|
|
2014
|
+
return sha256(Buffer.from(contextString, "utf8")).toString("hex");
|
|
2015
|
+
}
|
|
2016
|
+
function buildPopMessage(bech32Address, currentHeight, chainId, upgradeConfig) {
|
|
2017
|
+
if (chainId !== void 0 && upgradeConfig?.upgradeHeight !== void 0 && upgradeConfig.version !== void 0 && currentHeight !== void 0 && currentHeight >= upgradeConfig.upgradeHeight) {
|
|
2018
|
+
const contextHash = createStakerPopContext(chainId, upgradeConfig.version);
|
|
2019
|
+
return contextHash + bech32Address;
|
|
2020
|
+
}
|
|
2021
|
+
return bech32Address;
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
// src/utils/staking/param.ts
|
|
2025
|
+
var getBabylonParamByBtcHeight = (height, babylonParamsVersions) => {
|
|
2026
|
+
const sortedParams = [...babylonParamsVersions].sort(
|
|
2027
|
+
(a, b) => b.btcActivationHeight - a.btcActivationHeight
|
|
2028
|
+
);
|
|
2029
|
+
const params = sortedParams.find(
|
|
2030
|
+
(p) => height >= p.btcActivationHeight
|
|
2031
|
+
);
|
|
2032
|
+
if (!params) throw new Error(`Babylon params not found for height ${height}`);
|
|
2033
|
+
return params;
|
|
2034
|
+
};
|
|
2035
|
+
var getBabylonParamByVersion = (version, babylonParams) => {
|
|
2036
|
+
const params = babylonParams.find((p) => p.version === version);
|
|
2037
|
+
if (!params) throw new Error(`Babylon params not found for version ${version}`);
|
|
2038
|
+
return params;
|
|
1834
2039
|
};
|
|
1835
2040
|
|
|
1836
2041
|
// src/staking/manager.ts
|
|
1837
|
-
var SigningStep = /* @__PURE__ */ ((SigningStep2) => {
|
|
1838
|
-
SigningStep2["STAKING_SLASHING"] = "staking-slashing";
|
|
1839
|
-
SigningStep2["UNBONDING_SLASHING"] = "unbonding-slashing";
|
|
1840
|
-
SigningStep2["PROOF_OF_POSSESSION"] = "proof-of-possession";
|
|
1841
|
-
SigningStep2["CREATE_BTC_DELEGATION_MSG"] = "create-btc-delegation-msg";
|
|
1842
|
-
SigningStep2["STAKING"] = "staking";
|
|
1843
|
-
SigningStep2["UNBONDING"] = "unbonding";
|
|
1844
|
-
SigningStep2["WITHDRAW_STAKING_EXPIRED"] = "withdraw-staking-expired";
|
|
1845
|
-
SigningStep2["WITHDRAW_EARLY_UNBONDED"] = "withdraw-early-unbonded";
|
|
1846
|
-
SigningStep2["WITHDRAW_SLASHING"] = "withdraw-slashing";
|
|
1847
|
-
return SigningStep2;
|
|
1848
|
-
})(SigningStep || {});
|
|
1849
2042
|
var BabylonBtcStakingManager = class {
|
|
1850
|
-
constructor(network, stakingParams, btcProvider, babylonProvider) {
|
|
2043
|
+
constructor(network, stakingParams, btcProvider, babylonProvider, ee, upgradeConfig) {
|
|
1851
2044
|
this.network = network;
|
|
2045
|
+
this.stakingParams = stakingParams;
|
|
1852
2046
|
this.btcProvider = btcProvider;
|
|
1853
2047
|
this.babylonProvider = babylonProvider;
|
|
2048
|
+
this.ee = ee;
|
|
2049
|
+
this.network = network;
|
|
1854
2050
|
if (stakingParams.length === 0) {
|
|
1855
2051
|
throw new Error("No staking parameters provided");
|
|
1856
2052
|
}
|
|
1857
2053
|
this.stakingParams = stakingParams;
|
|
2054
|
+
this.upgradeConfig = upgradeConfig;
|
|
1858
2055
|
}
|
|
1859
2056
|
/**
|
|
1860
2057
|
* Creates a signed Pre-Staking Registration transaction that is ready to be
|
|
@@ -1865,9 +2062,11 @@ var BabylonBtcStakingManager = class {
|
|
|
1865
2062
|
* @param babylonBtcTipHeight - The Babylon BTC tip height.
|
|
1866
2063
|
* @param inputUTXOs - The UTXOs that will be used to pay for the staking
|
|
1867
2064
|
* transaction.
|
|
1868
|
-
* @param feeRate - The fee rate in satoshis per byte.
|
|
2065
|
+
* @param feeRate - The fee rate in satoshis per byte. Typical value for the
|
|
2066
|
+
* fee rate is above 1. If the fee rate is too low, the transaction will not
|
|
2067
|
+
* be included in a block.
|
|
1869
2068
|
* @param babylonAddress - The Babylon bech32 encoded address of the staker.
|
|
1870
|
-
* @returns The signed babylon pre-staking registration transaction in base64
|
|
2069
|
+
* @returns The signed babylon pre-staking registration transaction in base64
|
|
1871
2070
|
* format.
|
|
1872
2071
|
*/
|
|
1873
2072
|
async preStakeRegistrationBabylonTransaction(stakerBtcInfo, stakingInput, babylonBtcTipHeight, inputUTXOs, feeRate, babylonAddress) {
|
|
@@ -1880,23 +2079,17 @@ var BabylonBtcStakingManager = class {
|
|
|
1880
2079
|
if (!isValidBabylonAddress(babylonAddress)) {
|
|
1881
2080
|
throw new Error("Invalid Babylon address");
|
|
1882
2081
|
}
|
|
1883
|
-
const params = getBabylonParamByBtcHeight(
|
|
1884
|
-
babylonBtcTipHeight,
|
|
1885
|
-
this.stakingParams
|
|
1886
|
-
);
|
|
2082
|
+
const params = getBabylonParamByBtcHeight(babylonBtcTipHeight, this.stakingParams);
|
|
1887
2083
|
const staking = new Staking(
|
|
1888
2084
|
this.network,
|
|
1889
2085
|
stakerBtcInfo,
|
|
1890
2086
|
params,
|
|
1891
|
-
stakingInput.
|
|
2087
|
+
stakingInput.finalityProviderPksNoCoordHex,
|
|
1892
2088
|
stakingInput.stakingTimelock
|
|
1893
2089
|
);
|
|
1894
|
-
const { transaction } = staking.createStakingTransaction(
|
|
1895
|
-
stakingInput.stakingAmountSat,
|
|
1896
|
-
inputUTXOs,
|
|
1897
|
-
feeRate
|
|
1898
|
-
);
|
|
2090
|
+
const { transaction } = staking.createStakingTransaction(stakingInput.stakingAmountSat, inputUTXOs, feeRate);
|
|
1899
2091
|
const msg = await this.createBtcDelegationMsg(
|
|
2092
|
+
"delegation:create",
|
|
1900
2093
|
staking,
|
|
1901
2094
|
stakingInput,
|
|
1902
2095
|
transaction,
|
|
@@ -1904,26 +2097,131 @@ var BabylonBtcStakingManager = class {
|
|
|
1904
2097
|
stakerBtcInfo,
|
|
1905
2098
|
params
|
|
1906
2099
|
);
|
|
2100
|
+
this.ee?.emit("delegation:create", {
|
|
2101
|
+
type: "create-btc-delegation-msg"
|
|
2102
|
+
});
|
|
1907
2103
|
return {
|
|
1908
|
-
signedBabylonTx: await this.babylonProvider.signTransaction(
|
|
1909
|
-
"create-btc-delegation-msg" /* CREATE_BTC_DELEGATION_MSG */,
|
|
1910
|
-
msg
|
|
1911
|
-
),
|
|
2104
|
+
signedBabylonTx: await this.babylonProvider.signTransaction(msg),
|
|
1912
2105
|
stakingTx: transaction
|
|
1913
2106
|
};
|
|
1914
2107
|
}
|
|
1915
2108
|
/**
|
|
1916
|
-
*
|
|
1917
|
-
*
|
|
1918
|
-
|
|
2109
|
+
* Create a signed staking expansion transaction that is ready to be sent to
|
|
2110
|
+
* the Babylon chain.
|
|
2111
|
+
*/
|
|
2112
|
+
async stakingExpansionRegistrationBabylonTransaction(stakerBtcInfo, stakingInput, babylonBtcTipHeight, inputUTXOs, feeRate, babylonAddress, previousStakingTxInfo) {
|
|
2113
|
+
validateStakingExpansionInputs({
|
|
2114
|
+
babylonBtcTipHeight,
|
|
2115
|
+
inputUTXOs,
|
|
2116
|
+
stakingInput,
|
|
2117
|
+
previousStakingInput: previousStakingTxInfo.stakingInput,
|
|
2118
|
+
babylonAddress
|
|
2119
|
+
});
|
|
2120
|
+
const params = getBabylonParamByBtcHeight(babylonBtcTipHeight, this.stakingParams);
|
|
2121
|
+
const paramsForPreviousStakingTx = getBabylonParamByVersion(previousStakingTxInfo.paramVersion, this.stakingParams);
|
|
2122
|
+
const stakingInstance = new Staking(
|
|
2123
|
+
this.network,
|
|
2124
|
+
stakerBtcInfo,
|
|
2125
|
+
params,
|
|
2126
|
+
stakingInput.finalityProviderPksNoCoordHex,
|
|
2127
|
+
stakingInput.stakingTimelock
|
|
2128
|
+
);
|
|
2129
|
+
const { transaction: stakingExpansionTx, fundingUTXO } = stakingInstance.createStakingExpansionTransaction(
|
|
2130
|
+
stakingInput.stakingAmountSat,
|
|
2131
|
+
inputUTXOs,
|
|
2132
|
+
feeRate,
|
|
2133
|
+
paramsForPreviousStakingTx,
|
|
2134
|
+
previousStakingTxInfo
|
|
2135
|
+
);
|
|
2136
|
+
let fundingTx;
|
|
2137
|
+
try {
|
|
2138
|
+
fundingTx = await this.btcProvider.getTransactionHex(fundingUTXO.txid);
|
|
2139
|
+
} catch (error) {
|
|
2140
|
+
throw StakingError.fromUnknown(
|
|
2141
|
+
error,
|
|
2142
|
+
"INVALID_INPUT" /* INVALID_INPUT */,
|
|
2143
|
+
"Failed to retrieve funding transaction hex"
|
|
2144
|
+
);
|
|
2145
|
+
}
|
|
2146
|
+
const msg = await this.createBtcDelegationMsg(
|
|
2147
|
+
"delegation:expand",
|
|
2148
|
+
stakingInstance,
|
|
2149
|
+
stakingInput,
|
|
2150
|
+
stakingExpansionTx,
|
|
2151
|
+
babylonAddress,
|
|
2152
|
+
stakerBtcInfo,
|
|
2153
|
+
params,
|
|
2154
|
+
{
|
|
2155
|
+
delegationExpansionInfo: {
|
|
2156
|
+
previousStakingTx: previousStakingTxInfo.stakingTx,
|
|
2157
|
+
fundingTx: Transaction4.fromHex(fundingTx)
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
);
|
|
2161
|
+
this.ee?.emit("delegation:expand", {
|
|
2162
|
+
type: "create-btc-delegation-msg"
|
|
2163
|
+
});
|
|
2164
|
+
return {
|
|
2165
|
+
signedBabylonTx: await this.babylonProvider.signTransaction(msg),
|
|
2166
|
+
stakingTx: stakingExpansionTx
|
|
2167
|
+
};
|
|
2168
|
+
}
|
|
2169
|
+
/**
|
|
2170
|
+
* Estimates the transaction fee for a BTC staking expansion transaction.
|
|
2171
|
+
*
|
|
2172
|
+
* @param {StakerInfo} stakerBtcInfo - The staker's Bitcoin information
|
|
2173
|
+
* including address and public key
|
|
2174
|
+
* @param {number} babylonBtcTipHeight - The current Babylon BTC tip height
|
|
2175
|
+
* used to determine staking parameters
|
|
2176
|
+
* @param {StakingInputs} stakingInput - The new staking input parameters for
|
|
2177
|
+
* the expansion
|
|
2178
|
+
* @param {UTXO[]} inputUTXOs - Available UTXOs that can be used for funding
|
|
2179
|
+
* the expansion transaction
|
|
2180
|
+
* @param {number} feeRate - Fee rate in satoshis per byte for the expansion
|
|
2181
|
+
* transaction
|
|
2182
|
+
* @param {Object} previousStakingTxInfo - Information about the previous
|
|
2183
|
+
* staking transaction being expanded
|
|
2184
|
+
* @returns {number} - The estimated transaction fee in satoshis
|
|
2185
|
+
* @throws {Error} - If validation fails or the fee cannot be calculated
|
|
2186
|
+
*/
|
|
2187
|
+
estimateBtcStakingExpansionFee(stakerBtcInfo, babylonBtcTipHeight, stakingInput, inputUTXOs, feeRate, previousStakingTxInfo) {
|
|
2188
|
+
validateStakingExpansionInputs({
|
|
2189
|
+
babylonBtcTipHeight,
|
|
2190
|
+
inputUTXOs,
|
|
2191
|
+
stakingInput,
|
|
2192
|
+
previousStakingInput: previousStakingTxInfo.stakingInput
|
|
2193
|
+
});
|
|
2194
|
+
const params = getBabylonParamByBtcHeight(babylonBtcTipHeight, this.stakingParams);
|
|
2195
|
+
const paramsForPreviousStakingTx = getBabylonParamByVersion(previousStakingTxInfo.paramVersion, this.stakingParams);
|
|
2196
|
+
const stakingInstance = new Staking(
|
|
2197
|
+
this.network,
|
|
2198
|
+
stakerBtcInfo,
|
|
2199
|
+
params,
|
|
2200
|
+
stakingInput.finalityProviderPksNoCoordHex,
|
|
2201
|
+
stakingInput.stakingTimelock
|
|
2202
|
+
);
|
|
2203
|
+
const { fee } = stakingInstance.createStakingExpansionTransaction(
|
|
2204
|
+
stakingInput.stakingAmountSat,
|
|
2205
|
+
inputUTXOs,
|
|
2206
|
+
feeRate,
|
|
2207
|
+
paramsForPreviousStakingTx,
|
|
2208
|
+
previousStakingTxInfo
|
|
2209
|
+
);
|
|
2210
|
+
return fee;
|
|
2211
|
+
}
|
|
2212
|
+
/**
|
|
2213
|
+
* Creates a signed post-staking registration transaction that is ready to be
|
|
2214
|
+
* sent to the Babylon chain. This is used when a staking transaction is
|
|
2215
|
+
* already created and included in a BTC block and we want to register it on
|
|
1919
2216
|
* the Babylon chain.
|
|
1920
2217
|
* @param stakerBtcInfo - The staker BTC info which includes the BTC address
|
|
1921
2218
|
* and the no-coord public key in hex format.
|
|
1922
2219
|
* @param stakingTx - The staking transaction.
|
|
1923
|
-
* @param stakingTxHeight - The BTC height in which the staking transaction
|
|
2220
|
+
* @param stakingTxHeight - The BTC height in which the staking transaction
|
|
1924
2221
|
* is included.
|
|
1925
2222
|
* @param stakingInput - The staking inputs.
|
|
1926
|
-
* @param inclusionProof -
|
|
2223
|
+
* @param inclusionProof - Merkle Proof of Inclusion: Verifies transaction
|
|
2224
|
+
* inclusion in a Bitcoin block that is k-deep.
|
|
1927
2225
|
* @param babylonAddress - The Babylon bech32 encoded address of the staker.
|
|
1928
2226
|
* @returns The signed babylon transaction in base64 format.
|
|
1929
2227
|
*/
|
|
@@ -1936,30 +2234,29 @@ var BabylonBtcStakingManager = class {
|
|
|
1936
2234
|
this.network,
|
|
1937
2235
|
stakerBtcInfo,
|
|
1938
2236
|
params,
|
|
1939
|
-
stakingInput.
|
|
2237
|
+
stakingInput.finalityProviderPksNoCoordHex,
|
|
1940
2238
|
stakingInput.stakingTimelock
|
|
1941
2239
|
);
|
|
1942
2240
|
const scripts = stakingInstance.buildScripts();
|
|
1943
2241
|
const stakingOutputInfo = deriveStakingOutputInfo(scripts, this.network);
|
|
1944
|
-
findMatchingTxOutputIndex(
|
|
1945
|
-
stakingTx,
|
|
1946
|
-
stakingOutputInfo.outputAddress,
|
|
1947
|
-
this.network
|
|
1948
|
-
);
|
|
2242
|
+
findMatchingTxOutputIndex(stakingTx, stakingOutputInfo.outputAddress, this.network);
|
|
1949
2243
|
const delegationMsg = await this.createBtcDelegationMsg(
|
|
2244
|
+
"delegation:register",
|
|
1950
2245
|
stakingInstance,
|
|
1951
2246
|
stakingInput,
|
|
1952
2247
|
stakingTx,
|
|
1953
2248
|
babylonAddress,
|
|
1954
2249
|
stakerBtcInfo,
|
|
1955
2250
|
params,
|
|
1956
|
-
|
|
2251
|
+
{
|
|
2252
|
+
inclusionProof: this.getInclusionProof(inclusionProof)
|
|
2253
|
+
}
|
|
1957
2254
|
);
|
|
2255
|
+
this.ee?.emit("delegation:register", {
|
|
2256
|
+
type: "create-btc-delegation-msg"
|
|
2257
|
+
});
|
|
1958
2258
|
return {
|
|
1959
|
-
signedBabylonTx: await this.babylonProvider.signTransaction(
|
|
1960
|
-
"create-btc-delegation-msg" /* CREATE_BTC_DELEGATION_MSG */,
|
|
1961
|
-
delegationMsg
|
|
1962
|
-
)
|
|
2259
|
+
signedBabylonTx: await this.babylonProvider.signTransaction(delegationMsg)
|
|
1963
2260
|
};
|
|
1964
2261
|
}
|
|
1965
2262
|
/**
|
|
@@ -1971,33 +2268,28 @@ var BabylonBtcStakingManager = class {
|
|
|
1971
2268
|
* @param stakingInput - The staking inputs.
|
|
1972
2269
|
* @param inputUTXOs - The UTXOs that will be used to pay for the staking
|
|
1973
2270
|
* transaction.
|
|
1974
|
-
* @param feeRate - The fee rate in satoshis per byte.
|
|
2271
|
+
* @param feeRate - The fee rate in satoshis per byte. Typical value for the
|
|
2272
|
+
* fee rate is above 1. If the fee rate is too low, the transaction will not
|
|
2273
|
+
* be included in a block.
|
|
1975
2274
|
* @returns The estimated BTC fee in satoshis.
|
|
1976
2275
|
*/
|
|
1977
2276
|
estimateBtcStakingFee(stakerBtcInfo, babylonBtcTipHeight, stakingInput, inputUTXOs, feeRate) {
|
|
1978
2277
|
if (babylonBtcTipHeight === 0) {
|
|
1979
2278
|
throw new Error("Babylon BTC tip height cannot be 0");
|
|
1980
2279
|
}
|
|
1981
|
-
const params = getBabylonParamByBtcHeight(
|
|
1982
|
-
babylonBtcTipHeight,
|
|
1983
|
-
this.stakingParams
|
|
1984
|
-
);
|
|
2280
|
+
const params = getBabylonParamByBtcHeight(babylonBtcTipHeight, this.stakingParams);
|
|
1985
2281
|
const staking = new Staking(
|
|
1986
2282
|
this.network,
|
|
1987
2283
|
stakerBtcInfo,
|
|
1988
2284
|
params,
|
|
1989
|
-
stakingInput.
|
|
2285
|
+
stakingInput.finalityProviderPksNoCoordHex,
|
|
1990
2286
|
stakingInput.stakingTimelock
|
|
1991
2287
|
);
|
|
1992
|
-
const { fee: stakingFee } = staking.createStakingTransaction(
|
|
1993
|
-
stakingInput.stakingAmountSat,
|
|
1994
|
-
inputUTXOs,
|
|
1995
|
-
feeRate
|
|
1996
|
-
);
|
|
2288
|
+
const { fee: stakingFee } = staking.createStakingTransaction(stakingInput.stakingAmountSat, inputUTXOs, feeRate);
|
|
1997
2289
|
return stakingFee;
|
|
1998
2290
|
}
|
|
1999
2291
|
/**
|
|
2000
|
-
* Creates a signed staking transaction that is ready to be sent to the BTC
|
|
2292
|
+
* Creates a signed staking transaction that is ready to be sent to the BTC
|
|
2001
2293
|
* network.
|
|
2002
2294
|
* @param stakerBtcInfo - The staker BTC info which includes the BTC address
|
|
2003
2295
|
* and the no-coord public key in hex format.
|
|
@@ -2018,18 +2310,129 @@ var BabylonBtcStakingManager = class {
|
|
|
2018
2310
|
this.network,
|
|
2019
2311
|
stakerBtcInfo,
|
|
2020
2312
|
params,
|
|
2021
|
-
stakingInput.
|
|
2313
|
+
stakingInput.finalityProviderPksNoCoordHex,
|
|
2022
2314
|
stakingInput.stakingTimelock
|
|
2023
2315
|
);
|
|
2024
|
-
const stakingPsbt2 = staking.toStakingPsbt(
|
|
2025
|
-
|
|
2026
|
-
|
|
2316
|
+
const stakingPsbt2 = staking.toStakingPsbt(unsignedStakingTx, inputUTXOs);
|
|
2317
|
+
const contracts = [
|
|
2318
|
+
{
|
|
2319
|
+
id: "babylon:staking" /* STAKING */,
|
|
2320
|
+
params: {
|
|
2321
|
+
stakerPk: stakerBtcInfo.publicKeyNoCoordHex,
|
|
2322
|
+
finalityProviders: stakingInput.finalityProviderPksNoCoordHex,
|
|
2323
|
+
covenantPks: params.covenantNoCoordPks,
|
|
2324
|
+
covenantThreshold: params.covenantQuorum,
|
|
2325
|
+
minUnbondingTime: params.unbondingTime,
|
|
2326
|
+
stakingDuration: stakingInput.stakingTimelock
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
];
|
|
2330
|
+
this.ee?.emit("delegation:stake", {
|
|
2331
|
+
stakerPk: stakerBtcInfo.publicKeyNoCoordHex,
|
|
2332
|
+
finalityProviders: stakingInput.finalityProviderPksNoCoordHex,
|
|
2333
|
+
covenantPks: params.covenantNoCoordPks,
|
|
2334
|
+
covenantThreshold: params.covenantQuorum,
|
|
2335
|
+
unbondingTimeBlocks: params.unbondingTime,
|
|
2336
|
+
stakingDuration: stakingInput.stakingTimelock,
|
|
2337
|
+
type: "staking"
|
|
2338
|
+
});
|
|
2339
|
+
const signedStakingPsbtHex = await this.btcProvider.signPsbt(stakingPsbt2.toHex(), {
|
|
2340
|
+
contracts,
|
|
2341
|
+
action: {
|
|
2342
|
+
name: "sign-btc-staking-transaction" /* SIGN_BTC_STAKING_TRANSACTION */
|
|
2343
|
+
}
|
|
2344
|
+
});
|
|
2345
|
+
return Psbt3.fromHex(signedStakingPsbtHex).extractTransaction();
|
|
2346
|
+
}
|
|
2347
|
+
/**
|
|
2348
|
+
* Creates a signed staking expansion transaction that is ready to be sent to
|
|
2349
|
+
* the BTC network.
|
|
2350
|
+
*
|
|
2351
|
+
* @param {StakerInfo} stakerBtcInfo - The staker's BTC information including
|
|
2352
|
+
* address and public key
|
|
2353
|
+
* @param {StakingInputs} stakingInput - The staking inputs for the expansion
|
|
2354
|
+
* @param {Transaction} unsignedStakingExpansionTx - The unsigned staking
|
|
2355
|
+
* expansion transaction
|
|
2356
|
+
* @param {UTXO[]} inputUTXOs - Available UTXOs for the funding input
|
|
2357
|
+
* @param {number} stakingParamsVersion - The version of staking parameters
|
|
2358
|
+
* that was used when registering the staking expansion delegation.
|
|
2359
|
+
* @param {Object} previousStakingTxInfo - Information about the previous
|
|
2360
|
+
* staking transaction
|
|
2361
|
+
* @param {Array} covenantStakingExpansionSignatures - Covenant committee
|
|
2362
|
+
* signatures for the expansion
|
|
2363
|
+
* @returns {Promise<Transaction>} The fully signed staking expansion
|
|
2364
|
+
* transaction
|
|
2365
|
+
* @throws {Error} If signing fails, validation fails, or required data is
|
|
2366
|
+
* missing
|
|
2367
|
+
*/
|
|
2368
|
+
async createSignedBtcStakingExpansionTransaction(stakerBtcInfo, stakingInput, unsignedStakingExpansionTx, inputUTXOs, stakingParamsVersion, previousStakingTxInfo, covenantStakingExpansionSignatures) {
|
|
2369
|
+
validateStakingExpansionInputs({
|
|
2370
|
+
inputUTXOs,
|
|
2371
|
+
stakingInput,
|
|
2372
|
+
previousStakingInput: previousStakingTxInfo.stakingInput
|
|
2373
|
+
});
|
|
2374
|
+
const params = getBabylonParamByVersion(stakingParamsVersion, this.stakingParams);
|
|
2375
|
+
if (inputUTXOs.length === 0) {
|
|
2376
|
+
throw new Error("No input UTXOs provided");
|
|
2377
|
+
}
|
|
2378
|
+
const staking = new Staking(
|
|
2379
|
+
this.network,
|
|
2380
|
+
stakerBtcInfo,
|
|
2381
|
+
params,
|
|
2382
|
+
stakingInput.finalityProviderPksNoCoordHex,
|
|
2383
|
+
stakingInput.stakingTimelock
|
|
2027
2384
|
);
|
|
2028
|
-
const
|
|
2029
|
-
|
|
2030
|
-
|
|
2385
|
+
const previousParams = getBabylonParamByVersion(previousStakingTxInfo.paramVersion, this.stakingParams);
|
|
2386
|
+
const stakingExpansionPsbt2 = staking.toStakingExpansionPsbt(
|
|
2387
|
+
unsignedStakingExpansionTx,
|
|
2388
|
+
inputUTXOs,
|
|
2389
|
+
previousParams,
|
|
2390
|
+
previousStakingTxInfo
|
|
2391
|
+
);
|
|
2392
|
+
const contracts = [
|
|
2393
|
+
{
|
|
2394
|
+
id: "babylon:staking" /* STAKING */,
|
|
2395
|
+
params: {
|
|
2396
|
+
stakerPk: stakerBtcInfo.publicKeyNoCoordHex,
|
|
2397
|
+
finalityProviders: stakingInput.finalityProviderPksNoCoordHex,
|
|
2398
|
+
covenantPks: params.covenantNoCoordPks,
|
|
2399
|
+
covenantThreshold: params.covenantQuorum,
|
|
2400
|
+
minUnbondingTime: params.unbondingTime,
|
|
2401
|
+
stakingDuration: stakingInput.stakingTimelock
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
];
|
|
2405
|
+
this.ee?.emit("delegation:stake", {
|
|
2406
|
+
stakerPk: stakerBtcInfo.publicKeyNoCoordHex,
|
|
2407
|
+
finalityProviders: stakingInput.finalityProviderPksNoCoordHex,
|
|
2408
|
+
covenantPks: params.covenantNoCoordPks,
|
|
2409
|
+
covenantThreshold: params.covenantQuorum,
|
|
2410
|
+
unbondingTimeBlocks: params.unbondingTime,
|
|
2411
|
+
stakingDuration: stakingInput.stakingTimelock,
|
|
2412
|
+
type: "staking"
|
|
2413
|
+
});
|
|
2414
|
+
const signedStakingPsbtHex = await this.btcProvider.signPsbt(stakingExpansionPsbt2.toHex(), {
|
|
2415
|
+
contracts,
|
|
2416
|
+
action: {
|
|
2417
|
+
name: "sign-btc-staking-transaction" /* SIGN_BTC_STAKING_TRANSACTION */
|
|
2418
|
+
}
|
|
2419
|
+
});
|
|
2420
|
+
const signedStakingExpansionTx = Psbt3.fromHex(signedStakingPsbtHex).extractTransaction();
|
|
2421
|
+
if (signedStakingExpansionTx.getId() !== unsignedStakingExpansionTx.getId()) {
|
|
2422
|
+
throw new Error("Staking expansion transaction hash does not match the computed hash");
|
|
2423
|
+
}
|
|
2424
|
+
const covenantBuffers = previousParams.covenantNoCoordPks.map((covenant) => Buffer.from(covenant, "hex"));
|
|
2425
|
+
const witness = createCovenantWitness(
|
|
2426
|
+
// The first input of the staking expansion transaction is the previous
|
|
2427
|
+
// staking output. We will attach the covenant signatures to this input
|
|
2428
|
+
// to unbond the previousstaking output.
|
|
2429
|
+
signedStakingExpansionTx.ins[0].witness,
|
|
2430
|
+
covenantBuffers,
|
|
2431
|
+
covenantStakingExpansionSignatures,
|
|
2432
|
+
previousParams.covenantQuorum
|
|
2031
2433
|
);
|
|
2032
|
-
|
|
2434
|
+
signedStakingExpansionTx.ins[0].witness = witness;
|
|
2435
|
+
return signedStakingExpansionTx;
|
|
2033
2436
|
}
|
|
2034
2437
|
/**
|
|
2035
2438
|
* Creates a partial signed unbonding transaction that is only signed by the
|
|
@@ -2046,36 +2449,64 @@ var BabylonBtcStakingManager = class {
|
|
|
2046
2449
|
* @returns The partial signed unbonding transaction and its fee.
|
|
2047
2450
|
*/
|
|
2048
2451
|
async createPartialSignedBtcUnbondingTransaction(stakerBtcInfo, stakingInput, stakingParamsVersion, stakingTx) {
|
|
2049
|
-
const params = getBabylonParamByVersion(
|
|
2050
|
-
stakingParamsVersion,
|
|
2051
|
-
this.stakingParams
|
|
2052
|
-
);
|
|
2452
|
+
const params = getBabylonParamByVersion(stakingParamsVersion, this.stakingParams);
|
|
2053
2453
|
const staking = new Staking(
|
|
2054
2454
|
this.network,
|
|
2055
2455
|
stakerBtcInfo,
|
|
2056
2456
|
params,
|
|
2057
|
-
stakingInput.
|
|
2457
|
+
stakingInput.finalityProviderPksNoCoordHex,
|
|
2058
2458
|
stakingInput.stakingTimelock
|
|
2059
2459
|
);
|
|
2060
|
-
const {
|
|
2061
|
-
transaction: unbondingTx,
|
|
2062
|
-
fee
|
|
2063
|
-
} = staking.createUnbondingTransaction(stakingTx);
|
|
2460
|
+
const { transaction: unbondingTx, fee } = staking.createUnbondingTransaction(stakingTx);
|
|
2064
2461
|
const psbt = staking.toUnbondingPsbt(unbondingTx, stakingTx);
|
|
2065
|
-
const
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2462
|
+
const contracts = [
|
|
2463
|
+
{
|
|
2464
|
+
id: "babylon:staking" /* STAKING */,
|
|
2465
|
+
params: {
|
|
2466
|
+
stakerPk: stakerBtcInfo.publicKeyNoCoordHex,
|
|
2467
|
+
finalityProviders: stakingInput.finalityProviderPksNoCoordHex,
|
|
2468
|
+
covenantPks: params.covenantNoCoordPks,
|
|
2469
|
+
covenantThreshold: params.covenantQuorum,
|
|
2470
|
+
minUnbondingTime: params.unbondingTime,
|
|
2471
|
+
stakingDuration: stakingInput.stakingTimelock
|
|
2472
|
+
}
|
|
2473
|
+
},
|
|
2474
|
+
{
|
|
2475
|
+
id: "babylon:unbonding" /* UNBONDING */,
|
|
2476
|
+
params: {
|
|
2477
|
+
stakerPk: stakerBtcInfo.publicKeyNoCoordHex,
|
|
2478
|
+
finalityProviders: stakingInput.finalityProviderPksNoCoordHex,
|
|
2479
|
+
covenantPks: params.covenantNoCoordPks,
|
|
2480
|
+
covenantThreshold: params.covenantQuorum,
|
|
2481
|
+
unbondingTimeBlocks: params.unbondingTime,
|
|
2482
|
+
unbondingFeeSat: params.unbondingFeeSat
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
];
|
|
2486
|
+
this.ee?.emit("delegation:unbond", {
|
|
2487
|
+
stakerPk: stakerBtcInfo.publicKeyNoCoordHex,
|
|
2488
|
+
finalityProviders: stakingInput.finalityProviderPksNoCoordHex,
|
|
2489
|
+
covenantPks: params.covenantNoCoordPks,
|
|
2490
|
+
covenantThreshold: params.covenantQuorum,
|
|
2491
|
+
stakingDuration: stakingInput.stakingTimelock,
|
|
2492
|
+
unbondingTimeBlocks: params.unbondingTime,
|
|
2493
|
+
unbondingFeeSat: params.unbondingFeeSat,
|
|
2494
|
+
type: "unbonding"
|
|
2495
|
+
});
|
|
2496
|
+
const signedUnbondingPsbtHex = await this.btcProvider.signPsbt(psbt.toHex(), {
|
|
2497
|
+
contracts,
|
|
2498
|
+
action: {
|
|
2499
|
+
name: "sign-btc-unbonding-transaction" /* SIGN_BTC_UNBONDING_TRANSACTION */
|
|
2500
|
+
}
|
|
2501
|
+
});
|
|
2502
|
+
const signedUnbondingTx = Psbt3.fromHex(signedUnbondingPsbtHex).extractTransaction();
|
|
2072
2503
|
return {
|
|
2073
2504
|
transaction: signedUnbondingTx,
|
|
2074
2505
|
fee
|
|
2075
2506
|
};
|
|
2076
2507
|
}
|
|
2077
2508
|
/**
|
|
2078
|
-
* Creates a signed unbonding transaction that is ready to be sent to the BTC
|
|
2509
|
+
* Creates a signed unbonding transaction that is ready to be sent to the BTC
|
|
2079
2510
|
* network.
|
|
2080
2511
|
* @param stakerBtcInfo - The staker BTC info which includes the BTC address
|
|
2081
2512
|
* and the no-coord public key in hex format.
|
|
@@ -2089,27 +2520,17 @@ var BabylonBtcStakingManager = class {
|
|
|
2089
2520
|
* @returns The signed unbonding transaction and its fee.
|
|
2090
2521
|
*/
|
|
2091
2522
|
async createSignedBtcUnbondingTransaction(stakerBtcInfo, stakingInput, stakingParamsVersion, stakingTx, unsignedUnbondingTx, covenantUnbondingSignatures) {
|
|
2092
|
-
const params = getBabylonParamByVersion(
|
|
2093
|
-
|
|
2094
|
-
this.stakingParams
|
|
2095
|
-
);
|
|
2096
|
-
const {
|
|
2097
|
-
transaction: signedUnbondingTx,
|
|
2098
|
-
fee
|
|
2099
|
-
} = await this.createPartialSignedBtcUnbondingTransaction(
|
|
2523
|
+
const params = getBabylonParamByVersion(stakingParamsVersion, this.stakingParams);
|
|
2524
|
+
const { transaction: signedUnbondingTx, fee } = await this.createPartialSignedBtcUnbondingTransaction(
|
|
2100
2525
|
stakerBtcInfo,
|
|
2101
2526
|
stakingInput,
|
|
2102
2527
|
stakingParamsVersion,
|
|
2103
2528
|
stakingTx
|
|
2104
2529
|
);
|
|
2105
2530
|
if (signedUnbondingTx.getId() !== unsignedUnbondingTx.getId()) {
|
|
2106
|
-
throw new Error(
|
|
2107
|
-
"Unbonding transaction hash does not match the computed hash"
|
|
2108
|
-
);
|
|
2531
|
+
throw new Error("Unbonding transaction hash does not match the computed hash");
|
|
2109
2532
|
}
|
|
2110
|
-
const covenantBuffers = params.covenantNoCoordPks.map(
|
|
2111
|
-
(covenant) => Buffer.from(covenant, "hex")
|
|
2112
|
-
);
|
|
2533
|
+
const covenantBuffers = params.covenantNoCoordPks.map((covenant) => Buffer.from(covenant, "hex"));
|
|
2113
2534
|
const witness = createCovenantWitness(
|
|
2114
2535
|
// Since unbonding transactions always have a single input and output,
|
|
2115
2536
|
// we expect exactly one signature in TaprootScriptSpendSig when the
|
|
@@ -2126,42 +2547,54 @@ var BabylonBtcStakingManager = class {
|
|
|
2126
2547
|
};
|
|
2127
2548
|
}
|
|
2128
2549
|
/**
|
|
2129
|
-
* Creates a signed withdrawal transaction on the unbodning output expiry path
|
|
2550
|
+
* Creates a signed withdrawal transaction on the unbodning output expiry path
|
|
2130
2551
|
* that is ready to be sent to the BTC network.
|
|
2131
2552
|
* @param stakingInput - The staking inputs.
|
|
2132
2553
|
* @param stakingParamsVersion - The params version that was used to create the
|
|
2133
2554
|
* delegation in Babylon chain
|
|
2134
2555
|
* @param earlyUnbondingTx - The early unbonding transaction.
|
|
2135
|
-
* @param feeRate - The fee rate in satoshis per byte.
|
|
2556
|
+
* @param feeRate - The fee rate in satoshis per byte. Typical value for the
|
|
2557
|
+
* fee rate is above 1. If the fee rate is too low, the transaction will not
|
|
2558
|
+
* be included in a block.
|
|
2136
2559
|
* @returns The signed withdrawal transaction and its fee.
|
|
2137
2560
|
*/
|
|
2138
2561
|
async createSignedBtcWithdrawEarlyUnbondedTransaction(stakerBtcInfo, stakingInput, stakingParamsVersion, earlyUnbondingTx, feeRate) {
|
|
2139
|
-
const params = getBabylonParamByVersion(
|
|
2140
|
-
stakingParamsVersion,
|
|
2141
|
-
this.stakingParams
|
|
2142
|
-
);
|
|
2562
|
+
const params = getBabylonParamByVersion(stakingParamsVersion, this.stakingParams);
|
|
2143
2563
|
const staking = new Staking(
|
|
2144
2564
|
this.network,
|
|
2145
2565
|
stakerBtcInfo,
|
|
2146
2566
|
params,
|
|
2147
|
-
stakingInput.
|
|
2567
|
+
stakingInput.finalityProviderPksNoCoordHex,
|
|
2148
2568
|
stakingInput.stakingTimelock
|
|
2149
2569
|
);
|
|
2150
|
-
const { psbt: unbondingPsbt2, fee } = staking.createWithdrawEarlyUnbondedTransaction(
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2570
|
+
const { psbt: unbondingPsbt2, fee } = staking.createWithdrawEarlyUnbondedTransaction(earlyUnbondingTx, feeRate);
|
|
2571
|
+
const contracts = [
|
|
2572
|
+
{
|
|
2573
|
+
id: "babylon:withdraw" /* WITHDRAW */,
|
|
2574
|
+
params: {
|
|
2575
|
+
stakerPk: stakerBtcInfo.publicKeyNoCoordHex,
|
|
2576
|
+
timelockBlocks: params.unbondingTime
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
];
|
|
2580
|
+
this.ee?.emit("delegation:withdraw", {
|
|
2581
|
+
stakerPk: stakerBtcInfo.publicKeyNoCoordHex,
|
|
2582
|
+
timelockBlocks: params.unbondingTime,
|
|
2583
|
+
type: "early-unbonded"
|
|
2584
|
+
});
|
|
2585
|
+
const signedWithdrawalPsbtHex = await this.btcProvider.signPsbt(unbondingPsbt2.toHex(), {
|
|
2586
|
+
contracts,
|
|
2587
|
+
action: {
|
|
2588
|
+
name: "sign-btc-withdraw-transaction" /* SIGN_BTC_WITHDRAW_TRANSACTION */
|
|
2589
|
+
}
|
|
2590
|
+
});
|
|
2158
2591
|
return {
|
|
2159
2592
|
transaction: Psbt3.fromHex(signedWithdrawalPsbtHex).extractTransaction(),
|
|
2160
2593
|
fee
|
|
2161
2594
|
};
|
|
2162
2595
|
}
|
|
2163
2596
|
/**
|
|
2164
|
-
* Creates a signed withdrawal transaction on the staking output expiry path
|
|
2597
|
+
* Creates a signed withdrawal transaction on the staking output expiry path
|
|
2165
2598
|
* that is ready to be sent to the BTC network.
|
|
2166
2599
|
* @param stakerBtcInfo - The staker BTC info which includes the BTC address
|
|
2167
2600
|
* and the no-coord public key in hex format.
|
|
@@ -2169,36 +2602,48 @@ var BabylonBtcStakingManager = class {
|
|
|
2169
2602
|
* @param stakingParamsVersion - The params version that was used to create the
|
|
2170
2603
|
* delegation in Babylon chain
|
|
2171
2604
|
* @param stakingTx - The staking transaction.
|
|
2172
|
-
* @param feeRate - The fee rate in satoshis per byte.
|
|
2605
|
+
* @param feeRate - The fee rate in satoshis per byte. Typical value for the
|
|
2606
|
+
* fee rate is above 1. If the fee rate is too low, the transaction will not
|
|
2607
|
+
* be included in a block.
|
|
2173
2608
|
* @returns The signed withdrawal transaction and its fee.
|
|
2174
2609
|
*/
|
|
2175
2610
|
async createSignedBtcWithdrawStakingExpiredTransaction(stakerBtcInfo, stakingInput, stakingParamsVersion, stakingTx, feeRate) {
|
|
2176
|
-
const params = getBabylonParamByVersion(
|
|
2177
|
-
stakingParamsVersion,
|
|
2178
|
-
this.stakingParams
|
|
2179
|
-
);
|
|
2611
|
+
const params = getBabylonParamByVersion(stakingParamsVersion, this.stakingParams);
|
|
2180
2612
|
const staking = new Staking(
|
|
2181
2613
|
this.network,
|
|
2182
2614
|
stakerBtcInfo,
|
|
2183
2615
|
params,
|
|
2184
|
-
stakingInput.
|
|
2616
|
+
stakingInput.finalityProviderPksNoCoordHex,
|
|
2185
2617
|
stakingInput.stakingTimelock
|
|
2186
2618
|
);
|
|
2187
|
-
const { psbt, fee } = staking.createWithdrawStakingExpiredPsbt(
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2619
|
+
const { psbt, fee } = staking.createWithdrawStakingExpiredPsbt(stakingTx, feeRate);
|
|
2620
|
+
const contracts = [
|
|
2621
|
+
{
|
|
2622
|
+
id: "babylon:withdraw" /* WITHDRAW */,
|
|
2623
|
+
params: {
|
|
2624
|
+
stakerPk: stakerBtcInfo.publicKeyNoCoordHex,
|
|
2625
|
+
timelockBlocks: stakingInput.stakingTimelock
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
];
|
|
2629
|
+
this.ee?.emit("delegation:withdraw", {
|
|
2630
|
+
stakerPk: stakerBtcInfo.publicKeyNoCoordHex,
|
|
2631
|
+
timelockBlocks: stakingInput.stakingTimelock,
|
|
2632
|
+
type: "staking-expired"
|
|
2633
|
+
});
|
|
2634
|
+
const signedWithdrawalPsbtHex = await this.btcProvider.signPsbt(psbt.toHex(), {
|
|
2635
|
+
contracts,
|
|
2636
|
+
action: {
|
|
2637
|
+
name: "sign-btc-withdraw-transaction" /* SIGN_BTC_WITHDRAW_TRANSACTION */
|
|
2638
|
+
}
|
|
2639
|
+
});
|
|
2195
2640
|
return {
|
|
2196
2641
|
transaction: Psbt3.fromHex(signedWithdrawalPsbtHex).extractTransaction(),
|
|
2197
2642
|
fee
|
|
2198
2643
|
};
|
|
2199
2644
|
}
|
|
2200
2645
|
/**
|
|
2201
|
-
* Creates a signed withdrawal transaction for the expired slashing output that
|
|
2646
|
+
* Creates a signed withdrawal transaction for the expired slashing output that
|
|
2202
2647
|
* is ready to be sent to the BTC network.
|
|
2203
2648
|
* @param stakerBtcInfo - The staker BTC info which includes the BTC address
|
|
2204
2649
|
* and the no-coord public key in hex format.
|
|
@@ -2206,31 +2651,43 @@ var BabylonBtcStakingManager = class {
|
|
|
2206
2651
|
* @param stakingParamsVersion - The params version that was used to create the
|
|
2207
2652
|
* delegation in Babylon chain
|
|
2208
2653
|
* @param slashingTx - The slashing transaction.
|
|
2209
|
-
* @param feeRate - The fee rate in satoshis per byte.
|
|
2654
|
+
* @param feeRate - The fee rate in satoshis per byte. Typical value for the
|
|
2655
|
+
* fee rate is above 1. If the fee rate is too low, the transaction will not
|
|
2656
|
+
* be included in a block.
|
|
2210
2657
|
* @returns The signed withdrawal transaction and its fee.
|
|
2211
2658
|
*/
|
|
2212
2659
|
async createSignedBtcWithdrawSlashingTransaction(stakerBtcInfo, stakingInput, stakingParamsVersion, slashingTx, feeRate) {
|
|
2213
|
-
const params = getBabylonParamByVersion(
|
|
2214
|
-
stakingParamsVersion,
|
|
2215
|
-
this.stakingParams
|
|
2216
|
-
);
|
|
2660
|
+
const params = getBabylonParamByVersion(stakingParamsVersion, this.stakingParams);
|
|
2217
2661
|
const staking = new Staking(
|
|
2218
2662
|
this.network,
|
|
2219
2663
|
stakerBtcInfo,
|
|
2220
2664
|
params,
|
|
2221
|
-
stakingInput.
|
|
2665
|
+
stakingInput.finalityProviderPksNoCoordHex,
|
|
2222
2666
|
stakingInput.stakingTimelock
|
|
2223
2667
|
);
|
|
2224
|
-
const { psbt, fee } = staking.createWithdrawSlashingPsbt(
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2668
|
+
const { psbt, fee } = staking.createWithdrawSlashingPsbt(slashingTx, feeRate);
|
|
2669
|
+
const contracts = [
|
|
2670
|
+
{
|
|
2671
|
+
id: "babylon:withdraw" /* WITHDRAW */,
|
|
2672
|
+
params: {
|
|
2673
|
+
stakerPk: stakerBtcInfo.publicKeyNoCoordHex,
|
|
2674
|
+
timelockBlocks: params.unbondingTime
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
];
|
|
2678
|
+
this.ee?.emit("delegation:withdraw", {
|
|
2679
|
+
stakerPk: stakerBtcInfo.publicKeyNoCoordHex,
|
|
2680
|
+
timelockBlocks: params.unbondingTime,
|
|
2681
|
+
type: "slashing"
|
|
2682
|
+
});
|
|
2683
|
+
const signedWithrawSlashingPsbtHex = await this.btcProvider.signPsbt(psbt.toHex(), {
|
|
2684
|
+
contracts,
|
|
2685
|
+
action: {
|
|
2686
|
+
name: "sign-btc-withdraw-transaction" /* SIGN_BTC_WITHDRAW_TRANSACTION */
|
|
2687
|
+
}
|
|
2688
|
+
});
|
|
2232
2689
|
return {
|
|
2233
|
-
transaction: Psbt3.fromHex(
|
|
2690
|
+
transaction: Psbt3.fromHex(signedWithrawSlashingPsbtHex).extractTransaction(),
|
|
2234
2691
|
fee
|
|
2235
2692
|
};
|
|
2236
2693
|
}
|
|
@@ -2239,28 +2696,54 @@ var BabylonBtcStakingManager = class {
|
|
|
2239
2696
|
* @param bech32Address - The staker's bech32 address.
|
|
2240
2697
|
* @returns The proof of possession.
|
|
2241
2698
|
*/
|
|
2242
|
-
async createProofOfPossession(bech32Address) {
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
const
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2699
|
+
async createProofOfPossession(channel, bech32Address, stakerBtcAddress) {
|
|
2700
|
+
let sigType = BTCSigType.ECDSA;
|
|
2701
|
+
if (isTaproot(stakerBtcAddress, this.network) || isNativeSegwit(stakerBtcAddress, this.network)) {
|
|
2702
|
+
sigType = BTCSigType.BIP322;
|
|
2703
|
+
}
|
|
2704
|
+
const [chainId, babyTipHeight] = await Promise.all([
|
|
2705
|
+
this.babylonProvider.getChainId?.(),
|
|
2706
|
+
this.babylonProvider.getCurrentHeight?.()
|
|
2707
|
+
]);
|
|
2708
|
+
const upgradeConfig = this.upgradeConfig?.pop;
|
|
2709
|
+
const messageToSign = buildPopMessage(
|
|
2710
|
+
bech32Address,
|
|
2711
|
+
babyTipHeight,
|
|
2712
|
+
chainId,
|
|
2713
|
+
upgradeConfig && {
|
|
2714
|
+
upgradeHeight: upgradeConfig.upgradeHeight,
|
|
2715
|
+
version: upgradeConfig.version
|
|
2716
|
+
}
|
|
2251
2717
|
);
|
|
2252
|
-
|
|
2718
|
+
this.ee?.emit(channel, {
|
|
2719
|
+
messageToSign,
|
|
2720
|
+
type: "proof-of-possession"
|
|
2721
|
+
});
|
|
2722
|
+
const signedBabylonAddress = await this.btcProvider.signMessage(
|
|
2723
|
+
messageToSign,
|
|
2724
|
+
sigType === BTCSigType.BIP322 ? "bip322-simple" : "ecdsa"
|
|
2725
|
+
);
|
|
2726
|
+
let btcSig;
|
|
2727
|
+
if (sigType === BTCSigType.BIP322) {
|
|
2728
|
+
const bip322Sig = BIP322Sig.fromPartial({
|
|
2729
|
+
address: stakerBtcAddress,
|
|
2730
|
+
sig: Buffer.from(signedBabylonAddress, "base64")
|
|
2731
|
+
});
|
|
2732
|
+
btcSig = BIP322Sig.encode(bip322Sig).finish();
|
|
2733
|
+
} else {
|
|
2734
|
+
btcSig = Buffer.from(signedBabylonAddress, "base64");
|
|
2735
|
+
}
|
|
2253
2736
|
return {
|
|
2254
|
-
btcSigType:
|
|
2255
|
-
btcSig
|
|
2737
|
+
btcSigType: sigType,
|
|
2738
|
+
btcSig
|
|
2256
2739
|
};
|
|
2257
2740
|
}
|
|
2258
2741
|
/**
|
|
2259
|
-
* Creates the unbonding, slashing, and unbonding slashing transactions and
|
|
2742
|
+
* Creates the unbonding, slashing, and unbonding slashing transactions and
|
|
2260
2743
|
* PSBTs.
|
|
2261
2744
|
* @param stakingInstance - The staking instance.
|
|
2262
2745
|
* @param stakingTx - The staking transaction.
|
|
2263
|
-
* @returns The unbonding, slashing, and unbonding slashing transactions and
|
|
2746
|
+
* @returns The unbonding, slashing, and unbonding slashing transactions and
|
|
2264
2747
|
* PSBTs.
|
|
2265
2748
|
*/
|
|
2266
2749
|
async createDelegationTransactionsAndPsbts(stakingInstance, stakingTx) {
|
|
@@ -2275,81 +2758,164 @@ var BabylonBtcStakingManager = class {
|
|
|
2275
2758
|
}
|
|
2276
2759
|
/**
|
|
2277
2760
|
* Creates a protobuf message for the BTC delegation.
|
|
2761
|
+
* @param channel - The event channel to emit the message on.
|
|
2278
2762
|
* @param stakingInstance - The staking instance.
|
|
2279
2763
|
* @param stakingInput - The staking inputs.
|
|
2280
2764
|
* @param stakingTx - The staking transaction.
|
|
2281
2765
|
* @param bech32Address - The staker's babylon chain bech32 address
|
|
2282
|
-
* @param stakerBtcInfo - The staker's BTC information such as address and
|
|
2766
|
+
* @param stakerBtcInfo - The staker's BTC information such as address and
|
|
2283
2767
|
* public key
|
|
2284
2768
|
* @param params - The staking parameters.
|
|
2285
|
-
* @param
|
|
2769
|
+
* @param options - The options for the BTC delegation.
|
|
2770
|
+
* @param options.inclusionProof - The inclusion proof of the staking
|
|
2771
|
+
* transaction.
|
|
2772
|
+
* @param options.delegationExpansionInfo - The information for the BTC
|
|
2773
|
+
* delegation expansion.
|
|
2286
2774
|
* @returns The protobuf message.
|
|
2287
2775
|
*/
|
|
2288
|
-
async createBtcDelegationMsg(stakingInstance, stakingInput, stakingTx, bech32Address, stakerBtcInfo, params,
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2776
|
+
async createBtcDelegationMsg(channel, stakingInstance, stakingInput, stakingTx, bech32Address, stakerBtcInfo, params, options) {
|
|
2777
|
+
if (!params.slashing) {
|
|
2778
|
+
throw new StakingError(
|
|
2779
|
+
"INVALID_PARAMS" /* INVALID_PARAMS */,
|
|
2780
|
+
"Slashing parameters are required for creating delegation message"
|
|
2781
|
+
);
|
|
2782
|
+
}
|
|
2783
|
+
const { unbondingTx, slashingPsbt, unbondingSlashingPsbt } = await this.createDelegationTransactionsAndPsbts(
|
|
2294
2784
|
stakingInstance,
|
|
2295
2785
|
stakingTx
|
|
2296
2786
|
);
|
|
2297
|
-
const
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2787
|
+
const slashingContracts = [
|
|
2788
|
+
{
|
|
2789
|
+
id: "babylon:staking" /* STAKING */,
|
|
2790
|
+
params: {
|
|
2791
|
+
stakerPk: stakerBtcInfo.publicKeyNoCoordHex,
|
|
2792
|
+
finalityProviders: stakingInput.finalityProviderPksNoCoordHex,
|
|
2793
|
+
covenantPks: params.covenantNoCoordPks,
|
|
2794
|
+
covenantThreshold: params.covenantQuorum,
|
|
2795
|
+
minUnbondingTime: params.unbondingTime,
|
|
2796
|
+
stakingDuration: stakingInput.stakingTimelock
|
|
2797
|
+
}
|
|
2798
|
+
},
|
|
2799
|
+
{
|
|
2800
|
+
id: "babylon:slashing" /* SLASHING */,
|
|
2801
|
+
params: {
|
|
2802
|
+
stakerPk: stakerBtcInfo.publicKeyNoCoordHex,
|
|
2803
|
+
unbondingTimeBlocks: params.unbondingTime,
|
|
2804
|
+
slashingFeeSat: params.slashing.minSlashingTxFeeSat
|
|
2805
|
+
}
|
|
2806
|
+
},
|
|
2807
|
+
{
|
|
2808
|
+
id: "babylon:slashing-burn" /* SLASHING_BURN */,
|
|
2809
|
+
params: {
|
|
2810
|
+
stakerPk: stakerBtcInfo.publicKeyNoCoordHex,
|
|
2811
|
+
slashingPkScriptHex: params.slashing.slashingPkScriptHex
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
];
|
|
2815
|
+
this.ee?.emit(channel, {
|
|
2816
|
+
stakerPk: stakerBtcInfo.publicKeyNoCoordHex,
|
|
2817
|
+
finalityProviders: stakingInput.finalityProviderPksNoCoordHex,
|
|
2818
|
+
covenantPks: params.covenantNoCoordPks,
|
|
2819
|
+
covenantThreshold: params.covenantQuorum,
|
|
2820
|
+
unbondingTimeBlocks: params.unbondingTime,
|
|
2821
|
+
stakingDuration: stakingInput.stakingTimelock,
|
|
2822
|
+
slashingFeeSat: params.slashing.minSlashingTxFeeSat,
|
|
2823
|
+
slashingPkScriptHex: params.slashing.slashingPkScriptHex,
|
|
2824
|
+
type: "staking-slashing"
|
|
2825
|
+
});
|
|
2826
|
+
const signedSlashingPsbtHex = await this.btcProvider.signPsbt(slashingPsbt.toHex(), {
|
|
2827
|
+
contracts: slashingContracts,
|
|
2828
|
+
action: {
|
|
2829
|
+
name: "sign-btc-slashing-transaction" /* SIGN_BTC_SLASHING_TRANSACTION */
|
|
2830
|
+
}
|
|
2831
|
+
});
|
|
2832
|
+
const signedSlashingTx = Psbt3.fromHex(signedSlashingPsbtHex).extractTransaction();
|
|
2833
|
+
const slashingSig = extractFirstSchnorrSignatureFromTransaction(signedSlashingTx);
|
|
2307
2834
|
if (!slashingSig) {
|
|
2308
2835
|
throw new Error("No signature found in the staking output slashing PSBT");
|
|
2309
2836
|
}
|
|
2310
|
-
const
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2837
|
+
const unbondingSlashingContracts = [
|
|
2838
|
+
{
|
|
2839
|
+
id: "babylon:unbonding" /* UNBONDING */,
|
|
2840
|
+
params: {
|
|
2841
|
+
stakerPk: stakerBtcInfo.publicKeyNoCoordHex,
|
|
2842
|
+
finalityProviders: stakingInput.finalityProviderPksNoCoordHex,
|
|
2843
|
+
covenantPks: params.covenantNoCoordPks,
|
|
2844
|
+
covenantThreshold: params.covenantQuorum,
|
|
2845
|
+
unbondingTimeBlocks: params.unbondingTime,
|
|
2846
|
+
unbondingFeeSat: params.unbondingFeeSat
|
|
2847
|
+
}
|
|
2848
|
+
},
|
|
2849
|
+
{
|
|
2850
|
+
id: "babylon:slashing" /* SLASHING */,
|
|
2851
|
+
params: {
|
|
2852
|
+
stakerPk: stakerBtcInfo.publicKeyNoCoordHex,
|
|
2853
|
+
unbondingTimeBlocks: params.unbondingTime,
|
|
2854
|
+
slashingFeeSat: params.slashing.minSlashingTxFeeSat
|
|
2855
|
+
}
|
|
2856
|
+
},
|
|
2857
|
+
{
|
|
2858
|
+
id: "babylon:slashing-burn" /* SLASHING_BURN */,
|
|
2859
|
+
params: {
|
|
2860
|
+
stakerPk: stakerBtcInfo.publicKeyNoCoordHex,
|
|
2861
|
+
slashingPkScriptHex: params.slashing.slashingPkScriptHex
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
];
|
|
2865
|
+
this.ee?.emit(channel, {
|
|
2866
|
+
stakerPk: stakerBtcInfo.publicKeyNoCoordHex,
|
|
2867
|
+
finalityProviders: stakingInput.finalityProviderPksNoCoordHex,
|
|
2868
|
+
covenantPks: params.covenantNoCoordPks,
|
|
2869
|
+
covenantThreshold: params.covenantQuorum,
|
|
2870
|
+
unbondingTimeBlocks: params.unbondingTime,
|
|
2871
|
+
unbondingFeeSat: params.unbondingFeeSat,
|
|
2872
|
+
slashingFeeSat: params.slashing.minSlashingTxFeeSat,
|
|
2873
|
+
slashingPkScriptHex: params.slashing.slashingPkScriptHex,
|
|
2874
|
+
type: "unbonding-slashing"
|
|
2875
|
+
});
|
|
2876
|
+
const signedUnbondingSlashingPsbtHex = await this.btcProvider.signPsbt(unbondingSlashingPsbt.toHex(), {
|
|
2877
|
+
contracts: unbondingSlashingContracts,
|
|
2878
|
+
action: {
|
|
2879
|
+
name: "sign-btc-unbonding-slashing-transaction" /* SIGN_BTC_UNBONDING_SLASHING_TRANSACTION */
|
|
2880
|
+
}
|
|
2881
|
+
});
|
|
2882
|
+
const signedUnbondingSlashingTx = Psbt3.fromHex(signedUnbondingSlashingPsbtHex).extractTransaction();
|
|
2883
|
+
const unbondingSignatures = extractFirstSchnorrSignatureFromTransaction(signedUnbondingSlashingTx);
|
|
2320
2884
|
if (!unbondingSignatures) {
|
|
2321
2885
|
throw new Error("No signature found in the unbonding output slashing PSBT");
|
|
2322
2886
|
}
|
|
2323
|
-
const proofOfPossession = await this.createProofOfPossession(bech32Address);
|
|
2324
|
-
const
|
|
2887
|
+
const proofOfPossession = await this.createProofOfPossession(channel, bech32Address, stakerBtcInfo.address);
|
|
2888
|
+
const commonMsg = {
|
|
2325
2889
|
stakerAddr: bech32Address,
|
|
2326
2890
|
pop: proofOfPossession,
|
|
2327
|
-
btcPk: Uint8Array.from(
|
|
2328
|
-
|
|
2329
|
-
),
|
|
2330
|
-
fpBtcPkList: [
|
|
2331
|
-
Uint8Array.from(
|
|
2332
|
-
Buffer.from(stakingInput.finalityProviderPkNoCoordHex, "hex")
|
|
2333
|
-
)
|
|
2334
|
-
],
|
|
2891
|
+
btcPk: Uint8Array.from(Buffer.from(stakerBtcInfo.publicKeyNoCoordHex, "hex")),
|
|
2892
|
+
fpBtcPkList: stakingInput.finalityProviderPksNoCoordHex.map((pk) => Uint8Array.from(Buffer.from(pk, "hex"))),
|
|
2335
2893
|
stakingTime: stakingInput.stakingTimelock,
|
|
2336
2894
|
stakingValue: stakingInput.stakingAmountSat,
|
|
2337
2895
|
stakingTx: Uint8Array.from(stakingTx.toBuffer()),
|
|
2338
|
-
slashingTx: Uint8Array.from(
|
|
2339
|
-
Buffer.from(clearTxSignatures(signedSlashingTx).toHex(), "hex")
|
|
2340
|
-
),
|
|
2896
|
+
slashingTx: Uint8Array.from(Buffer.from(clearTxSignatures(signedSlashingTx).toHex(), "hex")),
|
|
2341
2897
|
delegatorSlashingSig: Uint8Array.from(slashingSig),
|
|
2342
2898
|
unbondingTime: params.unbondingTime,
|
|
2343
2899
|
unbondingTx: Uint8Array.from(unbondingTx.toBuffer()),
|
|
2344
2900
|
unbondingValue: stakingInput.stakingAmountSat - params.unbondingFeeSat,
|
|
2345
|
-
unbondingSlashingTx: Uint8Array.from(
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2901
|
+
unbondingSlashingTx: Uint8Array.from(Buffer.from(clearTxSignatures(signedUnbondingSlashingTx).toHex(), "hex")),
|
|
2902
|
+
delegatorUnbondingSlashingSig: Uint8Array.from(unbondingSignatures)
|
|
2903
|
+
};
|
|
2904
|
+
if (options?.delegationExpansionInfo) {
|
|
2905
|
+
const fundingTx = Uint8Array.from(options.delegationExpansionInfo.fundingTx.toBuffer());
|
|
2906
|
+
const msg2 = btcstakingtx.MsgBtcStakeExpand.fromPartial({
|
|
2907
|
+
...commonMsg,
|
|
2908
|
+
previousStakingTxHash: options.delegationExpansionInfo.previousStakingTx.getId(),
|
|
2909
|
+
fundingTx
|
|
2910
|
+
});
|
|
2911
|
+
return {
|
|
2912
|
+
typeUrl: BABYLON_REGISTRY_TYPE_URLS.MsgBtcStakeExpand,
|
|
2913
|
+
value: msg2
|
|
2914
|
+
};
|
|
2915
|
+
}
|
|
2916
|
+
const msg = btcstakingtx.MsgCreateBTCDelegation.fromPartial({
|
|
2917
|
+
...commonMsg,
|
|
2918
|
+
stakingTxInclusionProof: options?.inclusionProof
|
|
2353
2919
|
});
|
|
2354
2920
|
return {
|
|
2355
2921
|
typeUrl: BABYLON_REGISTRY_TYPE_URLS.MsgCreateBTCDelegation,
|
|
@@ -2363,11 +2929,7 @@ var BabylonBtcStakingManager = class {
|
|
|
2363
2929
|
* @returns The inclusion proof.
|
|
2364
2930
|
*/
|
|
2365
2931
|
getInclusionProof(inclusionProof) {
|
|
2366
|
-
const {
|
|
2367
|
-
pos,
|
|
2368
|
-
merkle,
|
|
2369
|
-
blockHashHex
|
|
2370
|
-
} = inclusionProof;
|
|
2932
|
+
const { pos, merkle, blockHashHex } = inclusionProof;
|
|
2371
2933
|
const proofHex = deriveMerkleProof(merkle);
|
|
2372
2934
|
const hash = reverseBuffer(Uint8Array.from(Buffer.from(blockHashHex, "hex")));
|
|
2373
2935
|
const inclusionProofKey = btccheckpoint.TransactionKey.fromPartial({
|
|
@@ -2380,54 +2942,235 @@ var BabylonBtcStakingManager = class {
|
|
|
2380
2942
|
});
|
|
2381
2943
|
}
|
|
2382
2944
|
};
|
|
2383
|
-
var extractFirstSchnorrSignatureFromTransaction = (singedTransaction) => {
|
|
2384
|
-
for (const input of singedTransaction.ins) {
|
|
2385
|
-
if (input.witness && input.witness.length > 0) {
|
|
2386
|
-
const schnorrSignature = input.witness[0];
|
|
2387
|
-
if (schnorrSignature.length === 64) {
|
|
2388
|
-
return schnorrSignature;
|
|
2389
|
-
}
|
|
2390
|
-
}
|
|
2391
|
-
}
|
|
2392
|
-
return void 0;
|
|
2393
|
-
};
|
|
2394
|
-
var clearTxSignatures = (tx) => {
|
|
2395
|
-
tx.ins.forEach((input) => {
|
|
2396
|
-
input.script = Buffer.alloc(0);
|
|
2397
|
-
input.witness = [];
|
|
2398
|
-
});
|
|
2399
|
-
return tx;
|
|
2400
|
-
};
|
|
2401
|
-
var deriveMerkleProof = (merkle) => {
|
|
2402
|
-
const proofHex = merkle.reduce((acc, m) => {
|
|
2403
|
-
return acc + Buffer.from(m, "hex").reverse().toString("hex");
|
|
2404
|
-
}, "");
|
|
2405
|
-
return proofHex;
|
|
2406
|
-
};
|
|
2407
2945
|
var getUnbondingTxStakerSignature = (unbondingTx) => {
|
|
2408
2946
|
try {
|
|
2409
2947
|
return unbondingTx.ins[0].witness[0].toString("hex");
|
|
2410
2948
|
} catch (error) {
|
|
2411
|
-
throw StakingError.fromUnknown(
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2949
|
+
throw StakingError.fromUnknown(error, "INVALID_INPUT" /* INVALID_INPUT */, "Failed to get staker signature");
|
|
2950
|
+
}
|
|
2951
|
+
};
|
|
2952
|
+
|
|
2953
|
+
// src/staking/observable/observableStakingScript.ts
|
|
2954
|
+
import { opcodes as opcodes4, script as script3 } from "bitcoinjs-lib";
|
|
2955
|
+
var ObservableStakingScriptData = class extends StakingScriptData {
|
|
2956
|
+
constructor(stakerKey, finalityProviderKeys, covenantKeys, covenantThreshold, stakingTimelock, unbondingTimelock, magicBytes) {
|
|
2957
|
+
super(
|
|
2958
|
+
stakerKey,
|
|
2959
|
+
finalityProviderKeys,
|
|
2960
|
+
covenantKeys,
|
|
2961
|
+
covenantThreshold,
|
|
2962
|
+
stakingTimelock,
|
|
2963
|
+
unbondingTimelock
|
|
2964
|
+
);
|
|
2965
|
+
if (!magicBytes) {
|
|
2966
|
+
throw new Error("Missing required input values");
|
|
2967
|
+
}
|
|
2968
|
+
if (magicBytes.length != MAGIC_BYTES_LEN) {
|
|
2969
|
+
throw new Error("Invalid script data provided");
|
|
2970
|
+
}
|
|
2971
|
+
this.magicBytes = magicBytes;
|
|
2972
|
+
}
|
|
2973
|
+
/**
|
|
2974
|
+
* Builds a data embed script for staking in the form:
|
|
2975
|
+
* OP_RETURN || <serializedStakingData>
|
|
2976
|
+
* where serializedStakingData is the concatenation of:
|
|
2977
|
+
* MagicBytes || Version || StakerPublicKey || FinalityProviderPublicKey || StakingTimeLock
|
|
2978
|
+
* Note: Only a single finality provider key is supported for now in phase 1
|
|
2979
|
+
* @throws {Error} If the number of finality provider keys is not equal to 1.
|
|
2980
|
+
* @returns {Buffer} The compiled data embed script.
|
|
2981
|
+
*/
|
|
2982
|
+
buildDataEmbedScript() {
|
|
2983
|
+
if (this.finalityProviderKeys.length != 1) {
|
|
2984
|
+
throw new Error("Only a single finality provider key is supported");
|
|
2985
|
+
}
|
|
2986
|
+
const version = Buffer.alloc(1);
|
|
2987
|
+
version.writeUInt8(0);
|
|
2988
|
+
const stakingTimeLock = Buffer.alloc(2);
|
|
2989
|
+
stakingTimeLock.writeUInt16BE(this.stakingTimeLock);
|
|
2990
|
+
const serializedStakingData = Buffer.concat([
|
|
2991
|
+
this.magicBytes,
|
|
2992
|
+
version,
|
|
2993
|
+
this.stakerKey,
|
|
2994
|
+
this.finalityProviderKeys[0],
|
|
2995
|
+
stakingTimeLock
|
|
2996
|
+
]);
|
|
2997
|
+
return script3.compile([opcodes4.OP_RETURN, serializedStakingData]);
|
|
2998
|
+
}
|
|
2999
|
+
/**
|
|
3000
|
+
* Builds the staking scripts.
|
|
3001
|
+
* @returns {ObservableStakingScripts} The staking scripts that can be used to stake.
|
|
3002
|
+
* contains the timelockScript, unbondingScript, slashingScript,
|
|
3003
|
+
* unbondingTimelockScript, and dataEmbedScript.
|
|
3004
|
+
* @throws {Error} If script data is invalid.
|
|
3005
|
+
*/
|
|
3006
|
+
buildScripts() {
|
|
3007
|
+
const scripts = super.buildScripts();
|
|
3008
|
+
return {
|
|
3009
|
+
...scripts,
|
|
3010
|
+
dataEmbedScript: this.buildDataEmbedScript()
|
|
3011
|
+
};
|
|
3012
|
+
}
|
|
3013
|
+
};
|
|
3014
|
+
|
|
3015
|
+
// src/staking/observable/index.ts
|
|
3016
|
+
var ObservableStaking = class extends Staking {
|
|
3017
|
+
constructor(network, stakerInfo, params, finalityProviderPksNoCoordHex, stakingTimelock) {
|
|
3018
|
+
super(
|
|
3019
|
+
network,
|
|
3020
|
+
stakerInfo,
|
|
3021
|
+
params,
|
|
3022
|
+
finalityProviderPksNoCoordHex,
|
|
3023
|
+
stakingTimelock
|
|
3024
|
+
);
|
|
3025
|
+
if (!params.tag) {
|
|
3026
|
+
throw new StakingError(
|
|
3027
|
+
"INVALID_INPUT" /* INVALID_INPUT */,
|
|
3028
|
+
"Observable staking parameters must include tag"
|
|
3029
|
+
);
|
|
3030
|
+
}
|
|
3031
|
+
if (!params.btcActivationHeight) {
|
|
3032
|
+
throw new StakingError(
|
|
3033
|
+
"INVALID_INPUT" /* INVALID_INPUT */,
|
|
3034
|
+
"Observable staking parameters must include a positive activation height"
|
|
3035
|
+
);
|
|
3036
|
+
}
|
|
3037
|
+
if (finalityProviderPksNoCoordHex.length !== 1) {
|
|
3038
|
+
throw new StakingError(
|
|
3039
|
+
"INVALID_INPUT" /* INVALID_INPUT */,
|
|
3040
|
+
"Observable staking requires exactly one finality provider public key"
|
|
3041
|
+
);
|
|
3042
|
+
}
|
|
3043
|
+
this.params = params;
|
|
3044
|
+
}
|
|
3045
|
+
/**
|
|
3046
|
+
* Build the staking scripts for observable staking.
|
|
3047
|
+
* This method overwrites the base method to include the OP_RETURN tag based
|
|
3048
|
+
* on the tag provided in the parameters.
|
|
3049
|
+
*
|
|
3050
|
+
* @returns {ObservableStakingScripts} - The staking scripts for observable staking.
|
|
3051
|
+
* @throws {StakingError} - If the scripts cannot be built.
|
|
3052
|
+
*/
|
|
3053
|
+
buildScripts() {
|
|
3054
|
+
const { covenantQuorum, covenantNoCoordPks, unbondingTime, tag } = this.params;
|
|
3055
|
+
let stakingScriptData;
|
|
3056
|
+
try {
|
|
3057
|
+
stakingScriptData = new ObservableStakingScriptData(
|
|
3058
|
+
Buffer.from(this.stakerInfo.publicKeyNoCoordHex, "hex"),
|
|
3059
|
+
this.finalityProviderPksNoCoordHex.map((pk) => Buffer.from(pk, "hex")),
|
|
3060
|
+
toBuffers(covenantNoCoordPks),
|
|
3061
|
+
covenantQuorum,
|
|
3062
|
+
this.stakingTimelock,
|
|
3063
|
+
unbondingTime,
|
|
3064
|
+
Buffer.from(tag, "hex")
|
|
3065
|
+
);
|
|
3066
|
+
} catch (error) {
|
|
3067
|
+
throw StakingError.fromUnknown(
|
|
3068
|
+
error,
|
|
3069
|
+
"SCRIPT_FAILURE" /* SCRIPT_FAILURE */,
|
|
3070
|
+
"Cannot build staking script data"
|
|
3071
|
+
);
|
|
3072
|
+
}
|
|
3073
|
+
let scripts;
|
|
3074
|
+
try {
|
|
3075
|
+
scripts = stakingScriptData.buildScripts();
|
|
3076
|
+
} catch (error) {
|
|
3077
|
+
throw StakingError.fromUnknown(
|
|
3078
|
+
error,
|
|
3079
|
+
"SCRIPT_FAILURE" /* SCRIPT_FAILURE */,
|
|
3080
|
+
"Cannot build staking scripts"
|
|
3081
|
+
);
|
|
3082
|
+
}
|
|
3083
|
+
return scripts;
|
|
3084
|
+
}
|
|
3085
|
+
/**
|
|
3086
|
+
* Create a staking transaction for observable staking.
|
|
3087
|
+
* This overwrites the method from the Staking class with the addtion
|
|
3088
|
+
* of the
|
|
3089
|
+
* 1. OP_RETURN tag in the staking scripts
|
|
3090
|
+
* 2. lockHeight parameter
|
|
3091
|
+
*
|
|
3092
|
+
* @param {number} stakingAmountSat - The amount to stake in satoshis.
|
|
3093
|
+
* @param {UTXO[]} inputUTXOs - The UTXOs to use as inputs for the staking
|
|
3094
|
+
* transaction.
|
|
3095
|
+
* @param {number} feeRate - The fee rate for the transaction in satoshis per byte.
|
|
3096
|
+
* @returns {TransactionResult} - An object containing the unsigned transaction,
|
|
3097
|
+
* and fee
|
|
3098
|
+
*/
|
|
3099
|
+
createStakingTransaction(stakingAmountSat, inputUTXOs, feeRate) {
|
|
3100
|
+
validateStakingTxInputData(
|
|
3101
|
+
stakingAmountSat,
|
|
3102
|
+
this.stakingTimelock,
|
|
3103
|
+
this.params,
|
|
3104
|
+
inputUTXOs,
|
|
3105
|
+
feeRate
|
|
3106
|
+
);
|
|
3107
|
+
const scripts = this.buildScripts();
|
|
3108
|
+
try {
|
|
3109
|
+
const { transaction, fee } = stakingTransaction(
|
|
3110
|
+
scripts,
|
|
3111
|
+
stakingAmountSat,
|
|
3112
|
+
this.stakerInfo.address,
|
|
3113
|
+
inputUTXOs,
|
|
3114
|
+
this.network,
|
|
3115
|
+
feeRate,
|
|
3116
|
+
// `lockHeight` is exclusive of the provided value.
|
|
3117
|
+
// For example, if a Bitcoin height of X is provided,
|
|
3118
|
+
// the transaction will be included starting from height X+1.
|
|
3119
|
+
// https://learnmeabitcoin.com/technical/transaction/locktime/
|
|
3120
|
+
this.params.btcActivationHeight - 1
|
|
3121
|
+
);
|
|
3122
|
+
return {
|
|
3123
|
+
transaction,
|
|
3124
|
+
fee
|
|
3125
|
+
};
|
|
3126
|
+
} catch (error) {
|
|
3127
|
+
throw StakingError.fromUnknown(
|
|
3128
|
+
error,
|
|
3129
|
+
"BUILD_TRANSACTION_FAILURE" /* BUILD_TRANSACTION_FAILURE */,
|
|
3130
|
+
"Cannot build unsigned staking transaction"
|
|
3131
|
+
);
|
|
3132
|
+
}
|
|
3133
|
+
}
|
|
3134
|
+
/**
|
|
3135
|
+
* Create a staking psbt for observable staking.
|
|
3136
|
+
*
|
|
3137
|
+
* @param {Transaction} stakingTx - The staking transaction.
|
|
3138
|
+
* @param {UTXO[]} inputUTXOs - The UTXOs to use as inputs for the staking
|
|
3139
|
+
* transaction.
|
|
3140
|
+
* @returns {Psbt} - The psbt.
|
|
3141
|
+
*/
|
|
3142
|
+
toStakingPsbt(stakingTx, inputUTXOs) {
|
|
3143
|
+
return stakingPsbt(
|
|
3144
|
+
stakingTx,
|
|
3145
|
+
this.network,
|
|
3146
|
+
inputUTXOs,
|
|
3147
|
+
isTaproot(
|
|
3148
|
+
this.stakerInfo.address,
|
|
3149
|
+
this.network
|
|
3150
|
+
) ? Buffer.from(this.stakerInfo.publicKeyNoCoordHex, "hex") : void 0
|
|
2415
3151
|
);
|
|
2416
3152
|
}
|
|
2417
3153
|
};
|
|
3154
|
+
|
|
3155
|
+
// src/types/params.ts
|
|
3156
|
+
function hasSlashing(params) {
|
|
3157
|
+
return params.slashing !== void 0;
|
|
3158
|
+
}
|
|
2418
3159
|
export {
|
|
2419
3160
|
BabylonBtcStakingManager,
|
|
2420
3161
|
BitcoinScriptType,
|
|
2421
3162
|
ObservableStaking,
|
|
2422
3163
|
ObservableStakingScriptData,
|
|
2423
|
-
SigningStep,
|
|
2424
3164
|
Staking,
|
|
2425
3165
|
StakingScriptData,
|
|
2426
3166
|
buildStakingTransactionOutputs,
|
|
3167
|
+
clearTxSignatures,
|
|
2427
3168
|
createCovenantWitness,
|
|
3169
|
+
deriveMerkleProof,
|
|
2428
3170
|
deriveSlashingOutput,
|
|
2429
3171
|
deriveStakingOutputInfo,
|
|
2430
3172
|
deriveUnbondingOutputInfo,
|
|
3173
|
+
extractFirstSchnorrSignatureFromTransaction,
|
|
2431
3174
|
findInputUTXO,
|
|
2432
3175
|
findMatchingTxOutputIndex,
|
|
2433
3176
|
getBabylonParamByBtcHeight,
|
|
@@ -2436,21 +3179,22 @@ export {
|
|
|
2436
3179
|
getPublicKeyNoCoord,
|
|
2437
3180
|
getScriptType,
|
|
2438
3181
|
getUnbondingTxStakerSignature,
|
|
3182
|
+
hasSlashing,
|
|
2439
3183
|
initBTCCurve,
|
|
3184
|
+
isNativeSegwit,
|
|
2440
3185
|
isTaproot,
|
|
2441
3186
|
isValidBabylonAddress,
|
|
2442
3187
|
isValidBitcoinAddress,
|
|
2443
3188
|
isValidNoCoordPublicKey,
|
|
2444
3189
|
slashEarlyUnbondedTransaction,
|
|
2445
3190
|
slashTimelockUnbondedTransaction,
|
|
3191
|
+
stakingExpansionTransaction,
|
|
2446
3192
|
stakingTransaction,
|
|
2447
3193
|
toBuffers,
|
|
2448
3194
|
transactionIdToHash,
|
|
2449
3195
|
unbondingTransaction,
|
|
2450
|
-
validateParams,
|
|
2451
|
-
validateStakingTimelock,
|
|
2452
|
-
validateStakingTxInputData,
|
|
2453
3196
|
withdrawEarlyUnbondedTransaction,
|
|
2454
3197
|
withdrawSlashingTransaction,
|
|
2455
3198
|
withdrawTimelockUnbondedTransaction
|
|
2456
3199
|
};
|
|
3200
|
+
//# sourceMappingURL=index.js.map
|