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