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