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