@bitgo-beta/babylonlabs-io-btc-staking-ts 0.4.0-bitgo.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +168 -0
- package/README.md +1 -0
- package/dist/index.cjs +2521 -0
- package/dist/index.d.cts +937 -0
- package/dist/index.js +2455 -0
- package/package.json +49 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2455 @@
|
|
|
1
|
+
// src/staking/stakingScript.ts
|
|
2
|
+
import { opcodes, script } from "bitcoinjs-lib";
|
|
3
|
+
|
|
4
|
+
// src/constants/keys.ts
|
|
5
|
+
var NO_COORD_PK_BYTE_LENGTH = 32;
|
|
6
|
+
|
|
7
|
+
// src/staking/stakingScript.ts
|
|
8
|
+
var MAGIC_BYTES_LEN = 4;
|
|
9
|
+
var StakingScriptData = class {
|
|
10
|
+
constructor(stakerKey, finalityProviderKeys, covenantKeys, covenantThreshold, stakingTimelock, unbondingTimelock) {
|
|
11
|
+
if (!stakerKey || !finalityProviderKeys || !covenantKeys || !covenantThreshold || !stakingTimelock || !unbondingTimelock) {
|
|
12
|
+
throw new Error("Missing required input values");
|
|
13
|
+
}
|
|
14
|
+
this.stakerKey = stakerKey;
|
|
15
|
+
this.finalityProviderKeys = finalityProviderKeys;
|
|
16
|
+
this.covenantKeys = covenantKeys;
|
|
17
|
+
this.covenantThreshold = covenantThreshold;
|
|
18
|
+
this.stakingTimeLock = stakingTimelock;
|
|
19
|
+
this.unbondingTimeLock = unbondingTimelock;
|
|
20
|
+
if (!this.validate()) {
|
|
21
|
+
throw new Error("Invalid script data provided");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Validates the staking script.
|
|
26
|
+
* @returns {boolean} Returns true if the staking script is valid, otherwise false.
|
|
27
|
+
*/
|
|
28
|
+
validate() {
|
|
29
|
+
if (this.stakerKey.length != NO_COORD_PK_BYTE_LENGTH) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
if (this.finalityProviderKeys.some(
|
|
33
|
+
(finalityProviderKey) => finalityProviderKey.length != NO_COORD_PK_BYTE_LENGTH
|
|
34
|
+
)) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
if (this.covenantKeys.some((covenantKey) => covenantKey.length != NO_COORD_PK_BYTE_LENGTH)) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
const allPks = [
|
|
41
|
+
this.stakerKey,
|
|
42
|
+
...this.finalityProviderKeys,
|
|
43
|
+
...this.covenantKeys
|
|
44
|
+
];
|
|
45
|
+
const allPksSet = new Set(allPks);
|
|
46
|
+
if (allPks.length !== allPksSet.size) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
if (this.covenantThreshold == 0 || this.covenantThreshold > this.covenantKeys.length) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
if (this.stakingTimeLock == 0 || this.stakingTimeLock > 65535) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
if (this.unbondingTimeLock == 0 || this.unbondingTimeLock > 65535) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
// The staking script allows for multiple finality provider public keys
|
|
61
|
+
// to support (re)stake to multiple finality providers
|
|
62
|
+
// Covenant members are going to have multiple keys
|
|
63
|
+
/**
|
|
64
|
+
* Builds a timelock script.
|
|
65
|
+
* @param timelock - The timelock value to encode in the script.
|
|
66
|
+
* @returns {Buffer} containing the compiled timelock script.
|
|
67
|
+
*/
|
|
68
|
+
buildTimelockScript(timelock) {
|
|
69
|
+
return script.compile([
|
|
70
|
+
this.stakerKey,
|
|
71
|
+
opcodes.OP_CHECKSIGVERIFY,
|
|
72
|
+
script.number.encode(timelock),
|
|
73
|
+
opcodes.OP_CHECKSEQUENCEVERIFY
|
|
74
|
+
]);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Builds the staking timelock script.
|
|
78
|
+
* Only holder of private key for given pubKey can spend after relative lock time
|
|
79
|
+
* Creates the timelock script in the form:
|
|
80
|
+
* <stakerPubKey>
|
|
81
|
+
* OP_CHECKSIGVERIFY
|
|
82
|
+
* <stakingTimeBlocks>
|
|
83
|
+
* OP_CHECKSEQUENCEVERIFY
|
|
84
|
+
* @returns {Buffer} The staking timelock script.
|
|
85
|
+
*/
|
|
86
|
+
buildStakingTimelockScript() {
|
|
87
|
+
return this.buildTimelockScript(this.stakingTimeLock);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Builds the unbonding timelock script.
|
|
91
|
+
* Creates the unbonding timelock script in the form:
|
|
92
|
+
* <stakerPubKey>
|
|
93
|
+
* OP_CHECKSIGVERIFY
|
|
94
|
+
* <unbondingTimeBlocks>
|
|
95
|
+
* OP_CHECKSEQUENCEVERIFY
|
|
96
|
+
* @returns {Buffer} The unbonding timelock script.
|
|
97
|
+
*/
|
|
98
|
+
buildUnbondingTimelockScript() {
|
|
99
|
+
return this.buildTimelockScript(this.unbondingTimeLock);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Builds the unbonding script in the form:
|
|
103
|
+
* buildSingleKeyScript(stakerPk, true) ||
|
|
104
|
+
* buildMultiKeyScript(covenantPks, covenantThreshold, false)
|
|
105
|
+
* || means combining the scripts
|
|
106
|
+
* @returns {Buffer} The unbonding script.
|
|
107
|
+
*/
|
|
108
|
+
buildUnbondingScript() {
|
|
109
|
+
return Buffer.concat([
|
|
110
|
+
this.buildSingleKeyScript(this.stakerKey, true),
|
|
111
|
+
this.buildMultiKeyScript(
|
|
112
|
+
this.covenantKeys,
|
|
113
|
+
this.covenantThreshold,
|
|
114
|
+
false
|
|
115
|
+
)
|
|
116
|
+
]);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Builds the slashing script for staking in the form:
|
|
120
|
+
* buildSingleKeyScript(stakerPk, true) ||
|
|
121
|
+
* buildMultiKeyScript(finalityProviderPKs, 1, true) ||
|
|
122
|
+
* buildMultiKeyScript(covenantPks, covenantThreshold, false)
|
|
123
|
+
* || means combining the scripts
|
|
124
|
+
* The slashing script is a combination of single-key and multi-key scripts.
|
|
125
|
+
* The single-key script is used for staker key verification.
|
|
126
|
+
* The multi-key script is used for finality provider key verification and covenant key verification.
|
|
127
|
+
* @returns {Buffer} The slashing script as a Buffer.
|
|
128
|
+
*/
|
|
129
|
+
buildSlashingScript() {
|
|
130
|
+
return Buffer.concat([
|
|
131
|
+
this.buildSingleKeyScript(this.stakerKey, true),
|
|
132
|
+
this.buildMultiKeyScript(
|
|
133
|
+
this.finalityProviderKeys,
|
|
134
|
+
// The threshold is always 1 as we only need one
|
|
135
|
+
// finalityProvider signature to perform slashing
|
|
136
|
+
// (only one finalityProvider performs an offence)
|
|
137
|
+
1,
|
|
138
|
+
// OP_VERIFY/OP_CHECKSIGVERIFY is added at the end
|
|
139
|
+
true
|
|
140
|
+
),
|
|
141
|
+
this.buildMultiKeyScript(
|
|
142
|
+
this.covenantKeys,
|
|
143
|
+
this.covenantThreshold,
|
|
144
|
+
// No need to add verify since covenants are at the end of the script
|
|
145
|
+
false
|
|
146
|
+
)
|
|
147
|
+
]);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Builds the staking scripts.
|
|
151
|
+
* @returns {StakingScripts} The staking scripts.
|
|
152
|
+
*/
|
|
153
|
+
buildScripts() {
|
|
154
|
+
return {
|
|
155
|
+
timelockScript: this.buildStakingTimelockScript(),
|
|
156
|
+
unbondingScript: this.buildUnbondingScript(),
|
|
157
|
+
slashingScript: this.buildSlashingScript(),
|
|
158
|
+
unbondingTimelockScript: this.buildUnbondingTimelockScript()
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
// buildSingleKeyScript and buildMultiKeyScript allow us to reuse functionality
|
|
162
|
+
// for creating Bitcoin scripts for the unbonding script and the slashing script
|
|
163
|
+
/**
|
|
164
|
+
* Builds a single key script in the form:
|
|
165
|
+
* buildSingleKeyScript creates a single key script
|
|
166
|
+
* <pk> OP_CHECKSIGVERIFY (if withVerify is true)
|
|
167
|
+
* <pk> OP_CHECKSIG (if withVerify is false)
|
|
168
|
+
* @param pk - The public key buffer.
|
|
169
|
+
* @param withVerify - A boolean indicating whether to include the OP_CHECKSIGVERIFY opcode.
|
|
170
|
+
* @returns The compiled script buffer.
|
|
171
|
+
*/
|
|
172
|
+
buildSingleKeyScript(pk, withVerify) {
|
|
173
|
+
if (pk.length != NO_COORD_PK_BYTE_LENGTH) {
|
|
174
|
+
throw new Error("Invalid key length");
|
|
175
|
+
}
|
|
176
|
+
return script.compile([
|
|
177
|
+
pk,
|
|
178
|
+
withVerify ? opcodes.OP_CHECKSIGVERIFY : opcodes.OP_CHECKSIG
|
|
179
|
+
]);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Builds a multi-key script in the form:
|
|
183
|
+
* <pk1> OP_CHEKCSIG <pk2> OP_CHECKSIGADD <pk3> OP_CHECKSIGADD ... <pkN> OP_CHECKSIGADD <threshold> OP_NUMEQUAL
|
|
184
|
+
* <withVerify -> OP_NUMEQUALVERIFY>
|
|
185
|
+
* It validates whether provided keys are unique and the threshold is not greater than number of keys
|
|
186
|
+
* If there is only one key provided it will return single key sig script
|
|
187
|
+
* @param pks - An array of public keys.
|
|
188
|
+
* @param threshold - The required number of valid signers.
|
|
189
|
+
* @param withVerify - A boolean indicating whether to include the OP_VERIFY opcode.
|
|
190
|
+
* @returns The compiled multi-key script as a Buffer.
|
|
191
|
+
* @throws {Error} If no keys are provided, if the required number of valid signers is greater than the number of provided keys, or if duplicate keys are provided.
|
|
192
|
+
*/
|
|
193
|
+
buildMultiKeyScript(pks, threshold, withVerify) {
|
|
194
|
+
if (!pks || pks.length === 0) {
|
|
195
|
+
throw new Error("No keys provided");
|
|
196
|
+
}
|
|
197
|
+
if (pks.some((pk) => pk.length != NO_COORD_PK_BYTE_LENGTH)) {
|
|
198
|
+
throw new Error("Invalid key length");
|
|
199
|
+
}
|
|
200
|
+
if (threshold > pks.length) {
|
|
201
|
+
throw new Error(
|
|
202
|
+
"Required number of valid signers is greater than number of provided keys"
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
if (pks.length === 1) {
|
|
206
|
+
return this.buildSingleKeyScript(pks[0], withVerify);
|
|
207
|
+
}
|
|
208
|
+
const sortedPks = [...pks].sort(Buffer.compare);
|
|
209
|
+
for (let i = 0; i < sortedPks.length - 1; ++i) {
|
|
210
|
+
if (sortedPks[i].equals(sortedPks[i + 1])) {
|
|
211
|
+
throw new Error("Duplicate keys provided");
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const scriptElements = [sortedPks[0], opcodes.OP_CHECKSIG];
|
|
215
|
+
for (let i = 1; i < sortedPks.length; i++) {
|
|
216
|
+
scriptElements.push(sortedPks[i]);
|
|
217
|
+
scriptElements.push(opcodes.OP_CHECKSIGADD);
|
|
218
|
+
}
|
|
219
|
+
scriptElements.push(script.number.encode(threshold));
|
|
220
|
+
if (withVerify) {
|
|
221
|
+
scriptElements.push(opcodes.OP_NUMEQUALVERIFY);
|
|
222
|
+
} else {
|
|
223
|
+
scriptElements.push(opcodes.OP_NUMEQUAL);
|
|
224
|
+
}
|
|
225
|
+
return script.compile(scriptElements);
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// src/error/index.ts
|
|
230
|
+
var StakingError = class _StakingError extends Error {
|
|
231
|
+
constructor(code, message) {
|
|
232
|
+
super(message);
|
|
233
|
+
this.code = code;
|
|
234
|
+
}
|
|
235
|
+
// Static method to safely handle unknown errors
|
|
236
|
+
static fromUnknown(error, code, fallbackMsg) {
|
|
237
|
+
if (error instanceof _StakingError) {
|
|
238
|
+
return error;
|
|
239
|
+
}
|
|
240
|
+
if (error instanceof Error) {
|
|
241
|
+
return new _StakingError(code, error.message);
|
|
242
|
+
}
|
|
243
|
+
return new _StakingError(code, fallbackMsg);
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// src/staking/transactions.ts
|
|
248
|
+
import { Psbt, Transaction as Transaction2, payments as payments3, script as script2, address as address3 } from "bitcoinjs-lib";
|
|
249
|
+
|
|
250
|
+
// src/constants/dustSat.ts
|
|
251
|
+
var BTC_DUST_SAT = 546;
|
|
252
|
+
|
|
253
|
+
// src/constants/internalPubkey.ts
|
|
254
|
+
var key = "0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0";
|
|
255
|
+
var internalPubkey = Buffer.from(key, "hex").subarray(1, 33);
|
|
256
|
+
|
|
257
|
+
// src/utils/btc.ts
|
|
258
|
+
import * as ecc from "@bitcoin-js/tiny-secp256k1-asmjs";
|
|
259
|
+
import { initEccLib, address, networks } from "bitcoinjs-lib";
|
|
260
|
+
var initBTCCurve = () => {
|
|
261
|
+
initEccLib(ecc);
|
|
262
|
+
};
|
|
263
|
+
var isValidBitcoinAddress = (btcAddress, network) => {
|
|
264
|
+
try {
|
|
265
|
+
return !!address.toOutputScript(btcAddress, network);
|
|
266
|
+
} catch (error) {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
var isTaproot = (taprootAddress, network) => {
|
|
271
|
+
try {
|
|
272
|
+
const decoded = address.fromBech32(taprootAddress);
|
|
273
|
+
if (decoded.version !== 1) {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
switch (network) {
|
|
277
|
+
case networks.bitcoin:
|
|
278
|
+
return taprootAddress.startsWith("bc1p");
|
|
279
|
+
case networks.testnet:
|
|
280
|
+
return taprootAddress.startsWith("tb1p") || taprootAddress.startsWith("sb1p");
|
|
281
|
+
default:
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
} catch (error) {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
var isValidNoCoordPublicKey = (pkWithNoCoord) => {
|
|
289
|
+
try {
|
|
290
|
+
const keyBuffer = Buffer.from(pkWithNoCoord, "hex");
|
|
291
|
+
return validateNoCoordPublicKeyBuffer(keyBuffer);
|
|
292
|
+
} catch (error) {
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
var getPublicKeyNoCoord = (pkHex) => {
|
|
297
|
+
const publicKey = Buffer.from(pkHex, "hex");
|
|
298
|
+
const publicKeyNoCoordBuffer = publicKey.length === NO_COORD_PK_BYTE_LENGTH ? publicKey : publicKey.subarray(1, 33);
|
|
299
|
+
if (!validateNoCoordPublicKeyBuffer(publicKeyNoCoordBuffer)) {
|
|
300
|
+
throw new Error("Invalid public key without coordinate");
|
|
301
|
+
}
|
|
302
|
+
return publicKeyNoCoordBuffer.toString("hex");
|
|
303
|
+
};
|
|
304
|
+
var validateNoCoordPublicKeyBuffer = (pkBuffer) => {
|
|
305
|
+
if (pkBuffer.length !== NO_COORD_PK_BYTE_LENGTH) {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
const compressedKeyEven = Buffer.concat([Buffer.from([2]), pkBuffer]);
|
|
309
|
+
const compressedKeyOdd = Buffer.concat([Buffer.from([3]), pkBuffer]);
|
|
310
|
+
return ecc.isPoint(compressedKeyEven) || ecc.isPoint(compressedKeyOdd);
|
|
311
|
+
};
|
|
312
|
+
var transactionIdToHash = (txId) => {
|
|
313
|
+
if (txId === "") {
|
|
314
|
+
throw new Error("Transaction id cannot be empty");
|
|
315
|
+
}
|
|
316
|
+
return Buffer.from(txId, "hex").reverse();
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
// src/utils/fee/index.ts
|
|
320
|
+
import { script as bitcoinScript2 } from "bitcoinjs-lib";
|
|
321
|
+
|
|
322
|
+
// src/constants/fee.ts
|
|
323
|
+
var DEFAULT_INPUT_SIZE = 180;
|
|
324
|
+
var P2WPKH_INPUT_SIZE = 68;
|
|
325
|
+
var P2TR_INPUT_SIZE = 58;
|
|
326
|
+
var TX_BUFFER_SIZE_OVERHEAD = 11;
|
|
327
|
+
var LOW_RATE_ESTIMATION_ACCURACY_BUFFER = 30;
|
|
328
|
+
var MAX_NON_LEGACY_OUTPUT_SIZE = 43;
|
|
329
|
+
var WITHDRAW_TX_BUFFER_SIZE = 17;
|
|
330
|
+
var WALLET_RELAY_FEE_RATE_THRESHOLD = 2;
|
|
331
|
+
var OP_RETURN_OUTPUT_VALUE_SIZE = 8;
|
|
332
|
+
var OP_RETURN_VALUE_SERIALIZE_SIZE = 1;
|
|
333
|
+
|
|
334
|
+
// src/utils/fee/utils.ts
|
|
335
|
+
import { script as bitcoinScript, opcodes as opcodes2, payments } from "bitcoinjs-lib";
|
|
336
|
+
var isOP_RETURN = (script4) => {
|
|
337
|
+
const decompiled = bitcoinScript.decompile(script4);
|
|
338
|
+
return !!decompiled && decompiled[0] === opcodes2.OP_RETURN;
|
|
339
|
+
};
|
|
340
|
+
var getInputSizeByScript = (script4) => {
|
|
341
|
+
try {
|
|
342
|
+
const { address: p2wpkhAddress } = payments.p2wpkh({
|
|
343
|
+
output: script4
|
|
344
|
+
});
|
|
345
|
+
if (p2wpkhAddress) {
|
|
346
|
+
return P2WPKH_INPUT_SIZE;
|
|
347
|
+
}
|
|
348
|
+
} catch (error) {
|
|
349
|
+
}
|
|
350
|
+
try {
|
|
351
|
+
const { address: p2trAddress } = payments.p2tr({
|
|
352
|
+
output: script4
|
|
353
|
+
});
|
|
354
|
+
if (p2trAddress) {
|
|
355
|
+
return P2TR_INPUT_SIZE;
|
|
356
|
+
}
|
|
357
|
+
} catch (error) {
|
|
358
|
+
}
|
|
359
|
+
return DEFAULT_INPUT_SIZE;
|
|
360
|
+
};
|
|
361
|
+
var getEstimatedChangeOutputSize = () => {
|
|
362
|
+
return MAX_NON_LEGACY_OUTPUT_SIZE;
|
|
363
|
+
};
|
|
364
|
+
var inputValueSum = (inputUTXOs) => {
|
|
365
|
+
return inputUTXOs.reduce((acc, utxo) => acc + utxo.value, 0);
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
// src/utils/fee/index.ts
|
|
369
|
+
var getStakingTxInputUTXOsAndFees = (availableUTXOs, stakingAmount, feeRate, outputs) => {
|
|
370
|
+
if (availableUTXOs.length === 0) {
|
|
371
|
+
throw new Error("Insufficient funds");
|
|
372
|
+
}
|
|
373
|
+
const validUTXOs = availableUTXOs.filter((utxo) => {
|
|
374
|
+
const script4 = Buffer.from(utxo.scriptPubKey, "hex");
|
|
375
|
+
return !!bitcoinScript2.decompile(script4);
|
|
376
|
+
});
|
|
377
|
+
if (validUTXOs.length === 0) {
|
|
378
|
+
throw new Error("Insufficient funds: no valid UTXOs available for staking");
|
|
379
|
+
}
|
|
380
|
+
const sortedUTXOs = validUTXOs.sort((a, b) => b.value - a.value);
|
|
381
|
+
const selectedUTXOs = [];
|
|
382
|
+
let accumulatedValue = 0;
|
|
383
|
+
let estimatedFee = 0;
|
|
384
|
+
for (const utxo of sortedUTXOs) {
|
|
385
|
+
selectedUTXOs.push(utxo);
|
|
386
|
+
accumulatedValue += utxo.value;
|
|
387
|
+
const estimatedSize = getEstimatedSize(selectedUTXOs, outputs);
|
|
388
|
+
estimatedFee = estimatedSize * feeRate + rateBasedTxBufferFee(feeRate);
|
|
389
|
+
if (accumulatedValue - (stakingAmount + estimatedFee) > BTC_DUST_SAT) {
|
|
390
|
+
estimatedFee += getEstimatedChangeOutputSize() * feeRate;
|
|
391
|
+
}
|
|
392
|
+
if (accumulatedValue >= stakingAmount + estimatedFee) {
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if (accumulatedValue < stakingAmount + estimatedFee) {
|
|
397
|
+
throw new Error(
|
|
398
|
+
"Insufficient funds: unable to gather enough UTXOs to cover the staking amount and fees"
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
return {
|
|
402
|
+
selectedUTXOs,
|
|
403
|
+
fee: estimatedFee
|
|
404
|
+
};
|
|
405
|
+
};
|
|
406
|
+
var getWithdrawTxFee = (feeRate) => {
|
|
407
|
+
const inputSize = P2TR_INPUT_SIZE;
|
|
408
|
+
const outputSize = getEstimatedChangeOutputSize();
|
|
409
|
+
return feeRate * (inputSize + outputSize + TX_BUFFER_SIZE_OVERHEAD + WITHDRAW_TX_BUFFER_SIZE) + rateBasedTxBufferFee(feeRate);
|
|
410
|
+
};
|
|
411
|
+
var getEstimatedSize = (inputUtxos, outputs) => {
|
|
412
|
+
const inputSize = inputUtxos.reduce((acc, u) => {
|
|
413
|
+
const script4 = Buffer.from(u.scriptPubKey, "hex");
|
|
414
|
+
const decompiledScript = bitcoinScript2.decompile(script4);
|
|
415
|
+
if (!decompiledScript) {
|
|
416
|
+
return acc;
|
|
417
|
+
}
|
|
418
|
+
return acc + getInputSizeByScript(script4);
|
|
419
|
+
}, 0);
|
|
420
|
+
const outputSize = outputs.reduce((acc, output) => {
|
|
421
|
+
if (isOP_RETURN(output.scriptPubKey)) {
|
|
422
|
+
return acc + output.scriptPubKey.length + OP_RETURN_OUTPUT_VALUE_SIZE + OP_RETURN_VALUE_SERIALIZE_SIZE;
|
|
423
|
+
}
|
|
424
|
+
return acc + MAX_NON_LEGACY_OUTPUT_SIZE;
|
|
425
|
+
}, 0);
|
|
426
|
+
return inputSize + outputSize + TX_BUFFER_SIZE_OVERHEAD;
|
|
427
|
+
};
|
|
428
|
+
var rateBasedTxBufferFee = (feeRate) => {
|
|
429
|
+
return feeRate <= WALLET_RELAY_FEE_RATE_THRESHOLD ? LOW_RATE_ESTIMATION_ACCURACY_BUFFER : 0;
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
// src/utils/staking/index.ts
|
|
433
|
+
import { address as address2, payments as payments2 } from "bitcoinjs-lib";
|
|
434
|
+
|
|
435
|
+
// src/constants/unbonding.ts
|
|
436
|
+
var MIN_UNBONDING_OUTPUT_VALUE = 1e3;
|
|
437
|
+
|
|
438
|
+
// src/utils/staking/index.ts
|
|
439
|
+
var buildStakingTransactionOutputs = (scripts, network, amount) => {
|
|
440
|
+
const stakingOutputInfo = deriveStakingOutputInfo(scripts, network);
|
|
441
|
+
const transactionOutputs = [
|
|
442
|
+
{
|
|
443
|
+
scriptPubKey: stakingOutputInfo.scriptPubKey,
|
|
444
|
+
value: amount
|
|
445
|
+
}
|
|
446
|
+
];
|
|
447
|
+
if (scripts.dataEmbedScript) {
|
|
448
|
+
transactionOutputs.push({
|
|
449
|
+
scriptPubKey: scripts.dataEmbedScript,
|
|
450
|
+
value: 0
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
return transactionOutputs;
|
|
454
|
+
};
|
|
455
|
+
var deriveStakingOutputInfo = (scripts, network) => {
|
|
456
|
+
const scriptTree = [
|
|
457
|
+
{
|
|
458
|
+
output: scripts.slashingScript
|
|
459
|
+
},
|
|
460
|
+
[{ output: scripts.unbondingScript }, { output: scripts.timelockScript }]
|
|
461
|
+
];
|
|
462
|
+
const stakingOutput = payments2.p2tr({
|
|
463
|
+
internalPubkey,
|
|
464
|
+
scriptTree,
|
|
465
|
+
network
|
|
466
|
+
});
|
|
467
|
+
if (!stakingOutput.address) {
|
|
468
|
+
throw new StakingError(
|
|
469
|
+
"INVALID_OUTPUT" /* INVALID_OUTPUT */,
|
|
470
|
+
"Failed to build staking output"
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
outputAddress: stakingOutput.address,
|
|
475
|
+
scriptPubKey: address2.toOutputScript(stakingOutput.address, network)
|
|
476
|
+
};
|
|
477
|
+
};
|
|
478
|
+
var deriveUnbondingOutputInfo = (scripts, network) => {
|
|
479
|
+
const outputScriptTree = [
|
|
480
|
+
{
|
|
481
|
+
output: scripts.slashingScript
|
|
482
|
+
},
|
|
483
|
+
{ output: scripts.unbondingTimelockScript }
|
|
484
|
+
];
|
|
485
|
+
const unbondingOutput = payments2.p2tr({
|
|
486
|
+
internalPubkey,
|
|
487
|
+
scriptTree: outputScriptTree,
|
|
488
|
+
network
|
|
489
|
+
});
|
|
490
|
+
if (!unbondingOutput.address) {
|
|
491
|
+
throw new StakingError(
|
|
492
|
+
"INVALID_OUTPUT" /* INVALID_OUTPUT */,
|
|
493
|
+
"Failed to build unbonding output"
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
return {
|
|
497
|
+
outputAddress: unbondingOutput.address,
|
|
498
|
+
scriptPubKey: address2.toOutputScript(unbondingOutput.address, network)
|
|
499
|
+
};
|
|
500
|
+
};
|
|
501
|
+
var deriveSlashingOutput = (scripts, network) => {
|
|
502
|
+
const slashingOutput = payments2.p2tr({
|
|
503
|
+
internalPubkey,
|
|
504
|
+
scriptTree: { output: scripts.unbondingTimelockScript },
|
|
505
|
+
network
|
|
506
|
+
});
|
|
507
|
+
const slashingOutputAddress = slashingOutput.address;
|
|
508
|
+
if (!slashingOutputAddress) {
|
|
509
|
+
throw new StakingError(
|
|
510
|
+
"INVALID_OUTPUT" /* INVALID_OUTPUT */,
|
|
511
|
+
"Failed to build slashing output address"
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
return {
|
|
515
|
+
outputAddress: slashingOutputAddress,
|
|
516
|
+
scriptPubKey: address2.toOutputScript(slashingOutputAddress, network)
|
|
517
|
+
};
|
|
518
|
+
};
|
|
519
|
+
var findMatchingTxOutputIndex = (tx, outputAddress, network) => {
|
|
520
|
+
const index = tx.outs.findIndex((output) => {
|
|
521
|
+
return address2.fromOutputScript(output.script, network) === outputAddress;
|
|
522
|
+
});
|
|
523
|
+
if (index === -1) {
|
|
524
|
+
throw new StakingError(
|
|
525
|
+
"INVALID_OUTPUT" /* INVALID_OUTPUT */,
|
|
526
|
+
`Matching output not found for address: ${outputAddress}`
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
return index;
|
|
530
|
+
};
|
|
531
|
+
var validateStakingTxInputData = (stakingAmountSat, timelock, params, inputUTXOs, feeRate) => {
|
|
532
|
+
if (stakingAmountSat < params.minStakingAmountSat || stakingAmountSat > params.maxStakingAmountSat) {
|
|
533
|
+
throw new StakingError(
|
|
534
|
+
"INVALID_INPUT" /* INVALID_INPUT */,
|
|
535
|
+
"Invalid staking amount"
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
if (timelock < params.minStakingTimeBlocks || timelock > params.maxStakingTimeBlocks) {
|
|
539
|
+
throw new StakingError(
|
|
540
|
+
"INVALID_INPUT" /* INVALID_INPUT */,
|
|
541
|
+
"Invalid timelock"
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
if (inputUTXOs.length == 0) {
|
|
545
|
+
throw new StakingError(
|
|
546
|
+
"INVALID_INPUT" /* INVALID_INPUT */,
|
|
547
|
+
"No input UTXOs provided"
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
if (feeRate <= 0) {
|
|
551
|
+
throw new StakingError(
|
|
552
|
+
"INVALID_INPUT" /* INVALID_INPUT */,
|
|
553
|
+
"Invalid fee rate"
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
var validateParams = (params) => {
|
|
558
|
+
if (params.covenantNoCoordPks.length == 0) {
|
|
559
|
+
throw new StakingError(
|
|
560
|
+
"INVALID_PARAMS" /* INVALID_PARAMS */,
|
|
561
|
+
"Could not find any covenant public keys"
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
if (params.covenantNoCoordPks.length < params.covenantQuorum) {
|
|
565
|
+
throw new StakingError(
|
|
566
|
+
"INVALID_PARAMS" /* INVALID_PARAMS */,
|
|
567
|
+
"Covenant public keys must be greater than or equal to the quorum"
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
params.covenantNoCoordPks.forEach((pk) => {
|
|
571
|
+
if (!isValidNoCoordPublicKey(pk)) {
|
|
572
|
+
throw new StakingError(
|
|
573
|
+
"INVALID_PARAMS" /* INVALID_PARAMS */,
|
|
574
|
+
"Covenant public key should contains no coordinate"
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
if (params.unbondingTime <= 0) {
|
|
579
|
+
throw new StakingError(
|
|
580
|
+
"INVALID_PARAMS" /* INVALID_PARAMS */,
|
|
581
|
+
"Unbonding time must be greater than 0"
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
if (params.unbondingFeeSat <= 0) {
|
|
585
|
+
throw new StakingError(
|
|
586
|
+
"INVALID_PARAMS" /* INVALID_PARAMS */,
|
|
587
|
+
"Unbonding fee must be greater than 0"
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
if (params.maxStakingAmountSat < params.minStakingAmountSat) {
|
|
591
|
+
throw new StakingError(
|
|
592
|
+
"INVALID_PARAMS" /* INVALID_PARAMS */,
|
|
593
|
+
"Max staking amount must be greater or equal to min staking amount"
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
if (params.minStakingAmountSat < params.unbondingFeeSat + MIN_UNBONDING_OUTPUT_VALUE) {
|
|
597
|
+
throw new StakingError(
|
|
598
|
+
"INVALID_PARAMS" /* INVALID_PARAMS */,
|
|
599
|
+
`Min staking amount must be greater than unbonding fee plus ${MIN_UNBONDING_OUTPUT_VALUE}`
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
if (params.maxStakingTimeBlocks < params.minStakingTimeBlocks) {
|
|
603
|
+
throw new StakingError(
|
|
604
|
+
"INVALID_PARAMS" /* INVALID_PARAMS */,
|
|
605
|
+
"Max staking time must be greater or equal to min staking time"
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
if (params.minStakingTimeBlocks <= 0) {
|
|
609
|
+
throw new StakingError(
|
|
610
|
+
"INVALID_PARAMS" /* INVALID_PARAMS */,
|
|
611
|
+
"Min staking time must be greater than 0"
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
if (params.covenantQuorum <= 0) {
|
|
615
|
+
throw new StakingError(
|
|
616
|
+
"INVALID_PARAMS" /* INVALID_PARAMS */,
|
|
617
|
+
"Covenant quorum must be greater than 0"
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
if (params.slashing) {
|
|
621
|
+
if (params.slashing.slashingRate <= 0) {
|
|
622
|
+
throw new StakingError(
|
|
623
|
+
"INVALID_PARAMS" /* INVALID_PARAMS */,
|
|
624
|
+
"Slashing rate must be greater than 0"
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
if (params.slashing.slashingRate > 1) {
|
|
628
|
+
throw new StakingError(
|
|
629
|
+
"INVALID_PARAMS" /* INVALID_PARAMS */,
|
|
630
|
+
"Slashing rate must be less or equal to 1"
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
if (params.slashing.slashingPkScriptHex.length == 0) {
|
|
634
|
+
throw new StakingError(
|
|
635
|
+
"INVALID_PARAMS" /* INVALID_PARAMS */,
|
|
636
|
+
"Slashing public key script is missing"
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
if (params.slashing.minSlashingTxFeeSat <= 0) {
|
|
640
|
+
throw new StakingError(
|
|
641
|
+
"INVALID_PARAMS" /* INVALID_PARAMS */,
|
|
642
|
+
"Minimum slashing transaction fee must be greater than 0"
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
var validateStakingTimelock = (stakingTimelock, params) => {
|
|
648
|
+
if (stakingTimelock < params.minStakingTimeBlocks || stakingTimelock > params.maxStakingTimeBlocks) {
|
|
649
|
+
throw new StakingError(
|
|
650
|
+
"INVALID_INPUT" /* INVALID_INPUT */,
|
|
651
|
+
"Staking transaction timelock is out of range"
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
var toBuffers = (inputs) => {
|
|
656
|
+
try {
|
|
657
|
+
return inputs.map(
|
|
658
|
+
(i) => Buffer.from(i, "hex")
|
|
659
|
+
);
|
|
660
|
+
} catch (error) {
|
|
661
|
+
throw StakingError.fromUnknown(
|
|
662
|
+
error,
|
|
663
|
+
"INVALID_INPUT" /* INVALID_INPUT */,
|
|
664
|
+
"Cannot convert values to buffers"
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
// src/constants/psbt.ts
|
|
670
|
+
var NON_RBF_SEQUENCE = 4294967295;
|
|
671
|
+
var TRANSACTION_VERSION = 2;
|
|
672
|
+
|
|
673
|
+
// src/constants/transaction.ts
|
|
674
|
+
var REDEEM_VERSION = 192;
|
|
675
|
+
|
|
676
|
+
// src/staking/transactions.ts
|
|
677
|
+
var BTC_LOCKTIME_HEIGHT_TIME_CUTOFF = 5e8;
|
|
678
|
+
function stakingTransaction(scripts, amount, changeAddress, inputUTXOs, network, feeRate, lockHeight) {
|
|
679
|
+
if (amount <= 0 || feeRate <= 0) {
|
|
680
|
+
throw new Error("Amount and fee rate must be bigger than 0");
|
|
681
|
+
}
|
|
682
|
+
if (!isValidBitcoinAddress(changeAddress, network)) {
|
|
683
|
+
throw new Error("Invalid change address");
|
|
684
|
+
}
|
|
685
|
+
const stakingOutputs = buildStakingTransactionOutputs(scripts, network, amount);
|
|
686
|
+
const { selectedUTXOs, fee } = getStakingTxInputUTXOsAndFees(
|
|
687
|
+
inputUTXOs,
|
|
688
|
+
amount,
|
|
689
|
+
feeRate,
|
|
690
|
+
stakingOutputs
|
|
691
|
+
);
|
|
692
|
+
const tx = new Transaction2();
|
|
693
|
+
tx.version = TRANSACTION_VERSION;
|
|
694
|
+
for (let i = 0; i < selectedUTXOs.length; ++i) {
|
|
695
|
+
const input = selectedUTXOs[i];
|
|
696
|
+
tx.addInput(
|
|
697
|
+
transactionIdToHash(input.txid),
|
|
698
|
+
input.vout,
|
|
699
|
+
NON_RBF_SEQUENCE
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
stakingOutputs.forEach((o) => {
|
|
703
|
+
tx.addOutput(o.scriptPubKey, o.value);
|
|
704
|
+
});
|
|
705
|
+
const inputsSum = inputValueSum(selectedUTXOs);
|
|
706
|
+
if (inputsSum - (amount + fee) > BTC_DUST_SAT) {
|
|
707
|
+
tx.addOutput(
|
|
708
|
+
address3.toOutputScript(changeAddress, network),
|
|
709
|
+
inputsSum - (amount + fee)
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
if (lockHeight) {
|
|
713
|
+
if (lockHeight >= BTC_LOCKTIME_HEIGHT_TIME_CUTOFF) {
|
|
714
|
+
throw new Error("Invalid lock height");
|
|
715
|
+
}
|
|
716
|
+
tx.locktime = lockHeight;
|
|
717
|
+
}
|
|
718
|
+
return {
|
|
719
|
+
transaction: tx,
|
|
720
|
+
fee
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
function withdrawEarlyUnbondedTransaction(scripts, unbondingTx, withdrawalAddress, network, feeRate) {
|
|
724
|
+
const scriptTree = [
|
|
725
|
+
{
|
|
726
|
+
output: scripts.slashingScript
|
|
727
|
+
},
|
|
728
|
+
{ output: scripts.unbondingTimelockScript }
|
|
729
|
+
];
|
|
730
|
+
return withdrawalTransaction(
|
|
731
|
+
{
|
|
732
|
+
timelockScript: scripts.unbondingTimelockScript
|
|
733
|
+
},
|
|
734
|
+
scriptTree,
|
|
735
|
+
unbondingTx,
|
|
736
|
+
withdrawalAddress,
|
|
737
|
+
network,
|
|
738
|
+
feeRate,
|
|
739
|
+
0
|
|
740
|
+
// unbonding always has a single output
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
function withdrawTimelockUnbondedTransaction(scripts, tx, withdrawalAddress, network, feeRate, outputIndex = 0) {
|
|
744
|
+
const scriptTree = [
|
|
745
|
+
{
|
|
746
|
+
output: scripts.slashingScript
|
|
747
|
+
},
|
|
748
|
+
[{ output: scripts.unbondingScript }, { output: scripts.timelockScript }]
|
|
749
|
+
];
|
|
750
|
+
return withdrawalTransaction(
|
|
751
|
+
scripts,
|
|
752
|
+
scriptTree,
|
|
753
|
+
tx,
|
|
754
|
+
withdrawalAddress,
|
|
755
|
+
network,
|
|
756
|
+
feeRate,
|
|
757
|
+
outputIndex
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
function withdrawSlashingTransaction(scripts, slashingTx, withdrawalAddress, network, feeRate, outputIndex) {
|
|
761
|
+
const scriptTree = { output: scripts.unbondingTimelockScript };
|
|
762
|
+
return withdrawalTransaction(
|
|
763
|
+
{
|
|
764
|
+
timelockScript: scripts.unbondingTimelockScript
|
|
765
|
+
},
|
|
766
|
+
scriptTree,
|
|
767
|
+
slashingTx,
|
|
768
|
+
withdrawalAddress,
|
|
769
|
+
network,
|
|
770
|
+
feeRate,
|
|
771
|
+
outputIndex
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
function withdrawalTransaction(scripts, scriptTree, tx, withdrawalAddress, network, feeRate, outputIndex = 0) {
|
|
775
|
+
if (feeRate <= 0) {
|
|
776
|
+
throw new Error("Withdrawal feeRate must be bigger than 0");
|
|
777
|
+
}
|
|
778
|
+
if (outputIndex < 0) {
|
|
779
|
+
throw new Error("Output index must be bigger or equal to 0");
|
|
780
|
+
}
|
|
781
|
+
const timePosition = 2;
|
|
782
|
+
const decompiled = script2.decompile(scripts.timelockScript);
|
|
783
|
+
if (!decompiled) {
|
|
784
|
+
throw new Error("Timelock script is not valid");
|
|
785
|
+
}
|
|
786
|
+
let timelock = 0;
|
|
787
|
+
if (typeof decompiled[timePosition] !== "number") {
|
|
788
|
+
const timeBuffer = decompiled[timePosition];
|
|
789
|
+
timelock = script2.number.decode(timeBuffer);
|
|
790
|
+
} else {
|
|
791
|
+
const wrap = decompiled[timePosition] % 16;
|
|
792
|
+
timelock = wrap === 0 ? 16 : wrap;
|
|
793
|
+
}
|
|
794
|
+
const redeem = {
|
|
795
|
+
output: scripts.timelockScript,
|
|
796
|
+
redeemVersion: REDEEM_VERSION
|
|
797
|
+
};
|
|
798
|
+
const p2tr = payments3.p2tr({
|
|
799
|
+
internalPubkey,
|
|
800
|
+
scriptTree,
|
|
801
|
+
redeem,
|
|
802
|
+
network
|
|
803
|
+
});
|
|
804
|
+
const tapLeafScript = {
|
|
805
|
+
leafVersion: redeem.redeemVersion,
|
|
806
|
+
script: redeem.output,
|
|
807
|
+
controlBlock: p2tr.witness[p2tr.witness.length - 1]
|
|
808
|
+
};
|
|
809
|
+
const psbt = new Psbt({ network });
|
|
810
|
+
psbt.setVersion(TRANSACTION_VERSION);
|
|
811
|
+
psbt.addInput({
|
|
812
|
+
hash: tx.getHash(),
|
|
813
|
+
index: outputIndex,
|
|
814
|
+
tapInternalKey: internalPubkey,
|
|
815
|
+
witnessUtxo: {
|
|
816
|
+
value: tx.outs[outputIndex].value,
|
|
817
|
+
script: tx.outs[outputIndex].script
|
|
818
|
+
},
|
|
819
|
+
tapLeafScript: [tapLeafScript],
|
|
820
|
+
sequence: timelock
|
|
821
|
+
});
|
|
822
|
+
const estimatedFee = getWithdrawTxFee(feeRate);
|
|
823
|
+
const outputValue = tx.outs[outputIndex].value - estimatedFee;
|
|
824
|
+
if (outputValue < 0) {
|
|
825
|
+
throw new Error(
|
|
826
|
+
"Not enough funds to cover the fee for withdrawal transaction"
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
if (outputValue < BTC_DUST_SAT) {
|
|
830
|
+
throw new Error("Output value is less than dust limit");
|
|
831
|
+
}
|
|
832
|
+
psbt.addOutput({
|
|
833
|
+
address: withdrawalAddress,
|
|
834
|
+
value: outputValue
|
|
835
|
+
});
|
|
836
|
+
psbt.setLocktime(0);
|
|
837
|
+
return {
|
|
838
|
+
psbt,
|
|
839
|
+
fee: estimatedFee
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
function slashTimelockUnbondedTransaction(scripts, stakingTransaction2, slashingPkScriptHex, slashingRate, minimumFee, network, outputIndex = 0) {
|
|
843
|
+
const slashingScriptTree = [
|
|
844
|
+
{
|
|
845
|
+
output: scripts.slashingScript
|
|
846
|
+
},
|
|
847
|
+
[{ output: scripts.unbondingScript }, { output: scripts.timelockScript }]
|
|
848
|
+
];
|
|
849
|
+
return slashingTransaction(
|
|
850
|
+
{
|
|
851
|
+
unbondingTimelockScript: scripts.unbondingTimelockScript,
|
|
852
|
+
slashingScript: scripts.slashingScript
|
|
853
|
+
},
|
|
854
|
+
slashingScriptTree,
|
|
855
|
+
stakingTransaction2,
|
|
856
|
+
slashingPkScriptHex,
|
|
857
|
+
slashingRate,
|
|
858
|
+
minimumFee,
|
|
859
|
+
network,
|
|
860
|
+
outputIndex
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
function slashEarlyUnbondedTransaction(scripts, unbondingTx, slashingPkScriptHex, slashingRate, minimumSlashingFee, network) {
|
|
864
|
+
const unbondingScriptTree = [
|
|
865
|
+
{
|
|
866
|
+
output: scripts.slashingScript
|
|
867
|
+
},
|
|
868
|
+
{
|
|
869
|
+
output: scripts.unbondingTimelockScript
|
|
870
|
+
}
|
|
871
|
+
];
|
|
872
|
+
return slashingTransaction(
|
|
873
|
+
{
|
|
874
|
+
unbondingTimelockScript: scripts.unbondingTimelockScript,
|
|
875
|
+
slashingScript: scripts.slashingScript
|
|
876
|
+
},
|
|
877
|
+
unbondingScriptTree,
|
|
878
|
+
unbondingTx,
|
|
879
|
+
slashingPkScriptHex,
|
|
880
|
+
slashingRate,
|
|
881
|
+
minimumSlashingFee,
|
|
882
|
+
network,
|
|
883
|
+
0
|
|
884
|
+
// unbonding always has a single output
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
function slashingTransaction(scripts, scriptTree, transaction, slashingPkScriptHex, slashingRate, minimumFee, network, outputIndex = 0) {
|
|
888
|
+
if (slashingRate <= 0 || slashingRate >= 1) {
|
|
889
|
+
throw new Error("Slashing rate must be between 0 and 1");
|
|
890
|
+
}
|
|
891
|
+
slashingRate = parseFloat(slashingRate.toFixed(2));
|
|
892
|
+
if (minimumFee <= 0 || !Number.isInteger(minimumFee)) {
|
|
893
|
+
throw new Error("Minimum fee must be a positve integer");
|
|
894
|
+
}
|
|
895
|
+
if (outputIndex < 0 || !Number.isInteger(outputIndex)) {
|
|
896
|
+
throw new Error("Output index must be an integer bigger or equal to 0");
|
|
897
|
+
}
|
|
898
|
+
if (!transaction.outs[outputIndex]) {
|
|
899
|
+
throw new Error("Output index is out of range");
|
|
900
|
+
}
|
|
901
|
+
const redeem = {
|
|
902
|
+
output: scripts.slashingScript,
|
|
903
|
+
redeemVersion: REDEEM_VERSION
|
|
904
|
+
};
|
|
905
|
+
const p2tr = payments3.p2tr({
|
|
906
|
+
internalPubkey,
|
|
907
|
+
scriptTree,
|
|
908
|
+
redeem,
|
|
909
|
+
network
|
|
910
|
+
});
|
|
911
|
+
const tapLeafScript = {
|
|
912
|
+
leafVersion: redeem.redeemVersion,
|
|
913
|
+
script: redeem.output,
|
|
914
|
+
controlBlock: p2tr.witness[p2tr.witness.length - 1]
|
|
915
|
+
};
|
|
916
|
+
const stakingAmount = transaction.outs[outputIndex].value;
|
|
917
|
+
const slashingAmount = Math.floor(stakingAmount * slashingRate);
|
|
918
|
+
if (slashingAmount <= BTC_DUST_SAT) {
|
|
919
|
+
throw new Error("Slashing amount is less than dust limit");
|
|
920
|
+
}
|
|
921
|
+
const userFunds = stakingAmount - slashingAmount - minimumFee;
|
|
922
|
+
if (userFunds <= BTC_DUST_SAT) {
|
|
923
|
+
throw new Error("User funds are less than dust limit");
|
|
924
|
+
}
|
|
925
|
+
const psbt = new Psbt({ network });
|
|
926
|
+
psbt.setVersion(TRANSACTION_VERSION);
|
|
927
|
+
psbt.addInput({
|
|
928
|
+
hash: transaction.getHash(),
|
|
929
|
+
index: outputIndex,
|
|
930
|
+
tapInternalKey: internalPubkey,
|
|
931
|
+
witnessUtxo: {
|
|
932
|
+
value: stakingAmount,
|
|
933
|
+
script: transaction.outs[outputIndex].script
|
|
934
|
+
},
|
|
935
|
+
tapLeafScript: [tapLeafScript],
|
|
936
|
+
// not RBF-able
|
|
937
|
+
sequence: NON_RBF_SEQUENCE
|
|
938
|
+
});
|
|
939
|
+
psbt.addOutput({
|
|
940
|
+
script: Buffer.from(slashingPkScriptHex, "hex"),
|
|
941
|
+
value: slashingAmount
|
|
942
|
+
});
|
|
943
|
+
const changeOutput = payments3.p2tr({
|
|
944
|
+
internalPubkey,
|
|
945
|
+
scriptTree: { output: scripts.unbondingTimelockScript },
|
|
946
|
+
network
|
|
947
|
+
});
|
|
948
|
+
psbt.addOutput({
|
|
949
|
+
address: changeOutput.address,
|
|
950
|
+
value: userFunds
|
|
951
|
+
});
|
|
952
|
+
psbt.setLocktime(0);
|
|
953
|
+
return { psbt };
|
|
954
|
+
}
|
|
955
|
+
function unbondingTransaction(scripts, stakingTx, unbondingFee, network, outputIndex = 0) {
|
|
956
|
+
if (unbondingFee <= 0) {
|
|
957
|
+
throw new Error("Unbonding fee must be bigger than 0");
|
|
958
|
+
}
|
|
959
|
+
if (outputIndex < 0) {
|
|
960
|
+
throw new Error("Output index must be bigger or equal to 0");
|
|
961
|
+
}
|
|
962
|
+
const tx = new Transaction2();
|
|
963
|
+
tx.version = TRANSACTION_VERSION;
|
|
964
|
+
tx.addInput(
|
|
965
|
+
stakingTx.getHash(),
|
|
966
|
+
outputIndex,
|
|
967
|
+
NON_RBF_SEQUENCE
|
|
968
|
+
// not RBF-able
|
|
969
|
+
);
|
|
970
|
+
const unbondingOutputInfo = deriveUnbondingOutputInfo(scripts, network);
|
|
971
|
+
const outputValue = stakingTx.outs[outputIndex].value - unbondingFee;
|
|
972
|
+
if (outputValue < BTC_DUST_SAT) {
|
|
973
|
+
throw new Error("Output value is less than dust limit for unbonding transaction");
|
|
974
|
+
}
|
|
975
|
+
if (!unbondingOutputInfo.outputAddress) {
|
|
976
|
+
throw new Error("Unbonding output address is not defined");
|
|
977
|
+
}
|
|
978
|
+
tx.addOutput(
|
|
979
|
+
unbondingOutputInfo.scriptPubKey,
|
|
980
|
+
outputValue
|
|
981
|
+
);
|
|
982
|
+
tx.locktime = 0;
|
|
983
|
+
return {
|
|
984
|
+
transaction: tx,
|
|
985
|
+
fee: unbondingFee
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
var createCovenantWitness = (originalWitness, paramsCovenants, covenantSigs, covenantQuorum) => {
|
|
989
|
+
if (covenantSigs.length < covenantQuorum) {
|
|
990
|
+
throw new Error(
|
|
991
|
+
`Not enough covenant signatures. Required: ${covenantQuorum}, got: ${covenantSigs.length}`
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
for (const sig of covenantSigs) {
|
|
995
|
+
const btcPkHexBuf = Buffer.from(sig.btcPkHex, "hex");
|
|
996
|
+
if (!paramsCovenants.some((covenant) => covenant.equals(btcPkHexBuf))) {
|
|
997
|
+
throw new Error(
|
|
998
|
+
`Covenant signature public key ${sig.btcPkHex} not found in params covenants`
|
|
999
|
+
);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
const covenantSigsBuffers = covenantSigs.slice(0, covenantQuorum).map((sig) => ({
|
|
1003
|
+
btcPkHex: Buffer.from(sig.btcPkHex, "hex"),
|
|
1004
|
+
sigHex: Buffer.from(sig.sigHex, "hex")
|
|
1005
|
+
}));
|
|
1006
|
+
const paramsCovenantsSorted = [...paramsCovenants].sort(Buffer.compare).reverse();
|
|
1007
|
+
const composedCovenantSigs = paramsCovenantsSorted.map((covenant) => {
|
|
1008
|
+
const covenantSig = covenantSigsBuffers.find(
|
|
1009
|
+
(sig) => sig.btcPkHex.compare(covenant) === 0
|
|
1010
|
+
);
|
|
1011
|
+
return covenantSig?.sigHex || Buffer.alloc(0);
|
|
1012
|
+
});
|
|
1013
|
+
return [...composedCovenantSigs, ...originalWitness];
|
|
1014
|
+
};
|
|
1015
|
+
|
|
1016
|
+
// src/staking/psbt.ts
|
|
1017
|
+
import { Psbt as Psbt2, payments as payments5 } from "bitcoinjs-lib";
|
|
1018
|
+
|
|
1019
|
+
// src/utils/utxo/findInputUTXO.ts
|
|
1020
|
+
var findInputUTXO = (inputUTXOs, input) => {
|
|
1021
|
+
const inputUTXO = inputUTXOs.find(
|
|
1022
|
+
(u) => transactionIdToHash(u.txid).toString("hex") === input.hash.toString("hex") && u.vout === input.index
|
|
1023
|
+
);
|
|
1024
|
+
if (!inputUTXO) {
|
|
1025
|
+
throw new Error(
|
|
1026
|
+
`Input UTXO not found for txid: ${Buffer.from(input.hash).reverse().toString("hex")} and vout: ${input.index}`
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
return inputUTXO;
|
|
1030
|
+
};
|
|
1031
|
+
|
|
1032
|
+
// src/utils/utxo/getScriptType.ts
|
|
1033
|
+
import { payments as payments4 } from "bitcoinjs-lib";
|
|
1034
|
+
var BitcoinScriptType = /* @__PURE__ */ ((BitcoinScriptType2) => {
|
|
1035
|
+
BitcoinScriptType2["P2PKH"] = "pubkeyhash";
|
|
1036
|
+
BitcoinScriptType2["P2SH"] = "scripthash";
|
|
1037
|
+
BitcoinScriptType2["P2WPKH"] = "witnesspubkeyhash";
|
|
1038
|
+
BitcoinScriptType2["P2WSH"] = "witnessscripthash";
|
|
1039
|
+
BitcoinScriptType2["P2TR"] = "taproot";
|
|
1040
|
+
return BitcoinScriptType2;
|
|
1041
|
+
})(BitcoinScriptType || {});
|
|
1042
|
+
var getScriptType = (script4) => {
|
|
1043
|
+
try {
|
|
1044
|
+
payments4.p2pkh({ output: script4 });
|
|
1045
|
+
return "pubkeyhash" /* P2PKH */;
|
|
1046
|
+
} catch {
|
|
1047
|
+
}
|
|
1048
|
+
try {
|
|
1049
|
+
payments4.p2sh({ output: script4 });
|
|
1050
|
+
return "scripthash" /* P2SH */;
|
|
1051
|
+
} catch {
|
|
1052
|
+
}
|
|
1053
|
+
try {
|
|
1054
|
+
payments4.p2wpkh({ output: script4 });
|
|
1055
|
+
return "witnesspubkeyhash" /* P2WPKH */;
|
|
1056
|
+
} catch {
|
|
1057
|
+
}
|
|
1058
|
+
try {
|
|
1059
|
+
payments4.p2wsh({ output: script4 });
|
|
1060
|
+
return "witnessscripthash" /* P2WSH */;
|
|
1061
|
+
} catch {
|
|
1062
|
+
}
|
|
1063
|
+
try {
|
|
1064
|
+
payments4.p2tr({ output: script4 });
|
|
1065
|
+
return "taproot" /* P2TR */;
|
|
1066
|
+
} catch {
|
|
1067
|
+
}
|
|
1068
|
+
throw new Error("Unknown script type");
|
|
1069
|
+
};
|
|
1070
|
+
|
|
1071
|
+
// src/utils/utxo/getPsbtInputFields.ts
|
|
1072
|
+
var getPsbtInputFields = (utxo, publicKeyNoCoord) => {
|
|
1073
|
+
const scriptPubKey = Buffer.from(utxo.scriptPubKey, "hex");
|
|
1074
|
+
const type = getScriptType(scriptPubKey);
|
|
1075
|
+
switch (type) {
|
|
1076
|
+
case "pubkeyhash" /* P2PKH */: {
|
|
1077
|
+
if (!utxo.rawTxHex) {
|
|
1078
|
+
throw new Error("Missing rawTxHex for legacy P2PKH input");
|
|
1079
|
+
}
|
|
1080
|
+
return { nonWitnessUtxo: Buffer.from(utxo.rawTxHex, "hex") };
|
|
1081
|
+
}
|
|
1082
|
+
case "scripthash" /* P2SH */: {
|
|
1083
|
+
if (!utxo.rawTxHex) {
|
|
1084
|
+
throw new Error("Missing rawTxHex for P2SH input");
|
|
1085
|
+
}
|
|
1086
|
+
if (!utxo.redeemScript) {
|
|
1087
|
+
throw new Error("Missing redeemScript for P2SH input");
|
|
1088
|
+
}
|
|
1089
|
+
return {
|
|
1090
|
+
nonWitnessUtxo: Buffer.from(utxo.rawTxHex, "hex"),
|
|
1091
|
+
redeemScript: Buffer.from(utxo.redeemScript, "hex")
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
case "witnesspubkeyhash" /* P2WPKH */: {
|
|
1095
|
+
return {
|
|
1096
|
+
witnessUtxo: {
|
|
1097
|
+
script: scriptPubKey,
|
|
1098
|
+
value: utxo.value
|
|
1099
|
+
}
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
case "witnessscripthash" /* P2WSH */: {
|
|
1103
|
+
if (!utxo.witnessScript) {
|
|
1104
|
+
throw new Error("Missing witnessScript for P2WSH input");
|
|
1105
|
+
}
|
|
1106
|
+
return {
|
|
1107
|
+
witnessUtxo: {
|
|
1108
|
+
script: scriptPubKey,
|
|
1109
|
+
value: utxo.value
|
|
1110
|
+
},
|
|
1111
|
+
witnessScript: Buffer.from(utxo.witnessScript, "hex")
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
case "taproot" /* P2TR */: {
|
|
1115
|
+
return {
|
|
1116
|
+
witnessUtxo: {
|
|
1117
|
+
script: scriptPubKey,
|
|
1118
|
+
value: utxo.value
|
|
1119
|
+
},
|
|
1120
|
+
// this is needed only if the wallet is in taproot mode
|
|
1121
|
+
...publicKeyNoCoord && { tapInternalKey: publicKeyNoCoord }
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
default:
|
|
1125
|
+
throw new Error(`Unsupported script type: ${type}`);
|
|
1126
|
+
}
|
|
1127
|
+
};
|
|
1128
|
+
|
|
1129
|
+
// src/staking/psbt.ts
|
|
1130
|
+
var stakingPsbt = (stakingTx, network, inputUTXOs, publicKeyNoCoord) => {
|
|
1131
|
+
if (publicKeyNoCoord && publicKeyNoCoord.length !== NO_COORD_PK_BYTE_LENGTH) {
|
|
1132
|
+
throw new Error("Invalid public key");
|
|
1133
|
+
}
|
|
1134
|
+
const psbt = new Psbt2({ network });
|
|
1135
|
+
if (stakingTx.version !== void 0)
|
|
1136
|
+
psbt.setVersion(stakingTx.version);
|
|
1137
|
+
if (stakingTx.locktime !== void 0)
|
|
1138
|
+
psbt.setLocktime(stakingTx.locktime);
|
|
1139
|
+
stakingTx.ins.forEach((input) => {
|
|
1140
|
+
const inputUTXO = findInputUTXO(inputUTXOs, input);
|
|
1141
|
+
const psbtInputData = getPsbtInputFields(inputUTXO, publicKeyNoCoord);
|
|
1142
|
+
psbt.addInput({
|
|
1143
|
+
hash: input.hash,
|
|
1144
|
+
index: input.index,
|
|
1145
|
+
sequence: input.sequence,
|
|
1146
|
+
...psbtInputData
|
|
1147
|
+
});
|
|
1148
|
+
});
|
|
1149
|
+
stakingTx.outs.forEach((o) => {
|
|
1150
|
+
psbt.addOutput({ script: o.script, value: o.value });
|
|
1151
|
+
});
|
|
1152
|
+
return psbt;
|
|
1153
|
+
};
|
|
1154
|
+
var unbondingPsbt = (scripts, unbondingTx, stakingTx, network) => {
|
|
1155
|
+
if (unbondingTx.outs.length !== 1) {
|
|
1156
|
+
throw new Error("Unbonding transaction must have exactly one output");
|
|
1157
|
+
}
|
|
1158
|
+
if (unbondingTx.ins.length !== 1) {
|
|
1159
|
+
throw new Error("Unbonding transaction must have exactly one input");
|
|
1160
|
+
}
|
|
1161
|
+
validateUnbondingOutput(scripts, unbondingTx, network);
|
|
1162
|
+
const psbt = new Psbt2({ network });
|
|
1163
|
+
if (unbondingTx.version !== void 0) {
|
|
1164
|
+
psbt.setVersion(unbondingTx.version);
|
|
1165
|
+
}
|
|
1166
|
+
if (unbondingTx.locktime !== void 0) {
|
|
1167
|
+
psbt.setLocktime(unbondingTx.locktime);
|
|
1168
|
+
}
|
|
1169
|
+
const input = unbondingTx.ins[0];
|
|
1170
|
+
const outputIndex = input.index;
|
|
1171
|
+
const inputScriptTree = [
|
|
1172
|
+
{ output: scripts.slashingScript },
|
|
1173
|
+
[{ output: scripts.unbondingScript }, { output: scripts.timelockScript }]
|
|
1174
|
+
];
|
|
1175
|
+
const inputRedeem = {
|
|
1176
|
+
output: scripts.unbondingScript,
|
|
1177
|
+
redeemVersion: REDEEM_VERSION
|
|
1178
|
+
};
|
|
1179
|
+
const p2tr = payments5.p2tr({
|
|
1180
|
+
internalPubkey,
|
|
1181
|
+
scriptTree: inputScriptTree,
|
|
1182
|
+
redeem: inputRedeem,
|
|
1183
|
+
network
|
|
1184
|
+
});
|
|
1185
|
+
const inputTapLeafScript = {
|
|
1186
|
+
leafVersion: inputRedeem.redeemVersion,
|
|
1187
|
+
script: inputRedeem.output,
|
|
1188
|
+
controlBlock: p2tr.witness[p2tr.witness.length - 1]
|
|
1189
|
+
};
|
|
1190
|
+
psbt.addInput({
|
|
1191
|
+
hash: input.hash,
|
|
1192
|
+
index: input.index,
|
|
1193
|
+
sequence: input.sequence,
|
|
1194
|
+
tapInternalKey: internalPubkey,
|
|
1195
|
+
witnessUtxo: {
|
|
1196
|
+
value: stakingTx.outs[outputIndex].value,
|
|
1197
|
+
script: stakingTx.outs[outputIndex].script
|
|
1198
|
+
},
|
|
1199
|
+
tapLeafScript: [inputTapLeafScript]
|
|
1200
|
+
});
|
|
1201
|
+
psbt.addOutput({
|
|
1202
|
+
script: unbondingTx.outs[0].script,
|
|
1203
|
+
value: unbondingTx.outs[0].value
|
|
1204
|
+
});
|
|
1205
|
+
return psbt;
|
|
1206
|
+
};
|
|
1207
|
+
var validateUnbondingOutput = (scripts, unbondingTx, network) => {
|
|
1208
|
+
const unbondingOutputInfo = deriveUnbondingOutputInfo(scripts, network);
|
|
1209
|
+
if (unbondingOutputInfo.scriptPubKey.toString("hex") !== unbondingTx.outs[0].script.toString("hex")) {
|
|
1210
|
+
throw new Error(
|
|
1211
|
+
"Unbonding output script does not match the expected script while building psbt"
|
|
1212
|
+
);
|
|
1213
|
+
}
|
|
1214
|
+
};
|
|
1215
|
+
|
|
1216
|
+
// src/staking/index.ts
|
|
1217
|
+
var Staking = class {
|
|
1218
|
+
constructor(network, stakerInfo, params, finalityProviderPkNoCoordHex, stakingTimelock) {
|
|
1219
|
+
if (!isValidBitcoinAddress(stakerInfo.address, network)) {
|
|
1220
|
+
throw new StakingError(
|
|
1221
|
+
"INVALID_INPUT" /* INVALID_INPUT */,
|
|
1222
|
+
"Invalid staker bitcoin address"
|
|
1223
|
+
);
|
|
1224
|
+
}
|
|
1225
|
+
if (!isValidNoCoordPublicKey(stakerInfo.publicKeyNoCoordHex)) {
|
|
1226
|
+
throw new StakingError(
|
|
1227
|
+
"INVALID_INPUT" /* INVALID_INPUT */,
|
|
1228
|
+
"Invalid staker public key"
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
if (!isValidNoCoordPublicKey(finalityProviderPkNoCoordHex)) {
|
|
1232
|
+
throw new StakingError(
|
|
1233
|
+
"INVALID_INPUT" /* INVALID_INPUT */,
|
|
1234
|
+
"Invalid finality provider public key"
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
validateParams(params);
|
|
1238
|
+
validateStakingTimelock(stakingTimelock, params);
|
|
1239
|
+
this.network = network;
|
|
1240
|
+
this.stakerInfo = stakerInfo;
|
|
1241
|
+
this.params = params;
|
|
1242
|
+
this.finalityProviderPkNoCoordHex = finalityProviderPkNoCoordHex;
|
|
1243
|
+
this.stakingTimelock = stakingTimelock;
|
|
1244
|
+
}
|
|
1245
|
+
/**
|
|
1246
|
+
* buildScripts builds the staking scripts for the staking transaction.
|
|
1247
|
+
* Note: different staking types may have different scripts.
|
|
1248
|
+
* e.g the observable staking script has a data embed script.
|
|
1249
|
+
*
|
|
1250
|
+
* @returns {StakingScripts} - The staking scripts.
|
|
1251
|
+
*/
|
|
1252
|
+
buildScripts() {
|
|
1253
|
+
const { covenantQuorum, covenantNoCoordPks, unbondingTime } = this.params;
|
|
1254
|
+
let stakingScriptData;
|
|
1255
|
+
try {
|
|
1256
|
+
stakingScriptData = new StakingScriptData(
|
|
1257
|
+
Buffer.from(this.stakerInfo.publicKeyNoCoordHex, "hex"),
|
|
1258
|
+
[Buffer.from(this.finalityProviderPkNoCoordHex, "hex")],
|
|
1259
|
+
toBuffers(covenantNoCoordPks),
|
|
1260
|
+
covenantQuorum,
|
|
1261
|
+
this.stakingTimelock,
|
|
1262
|
+
unbondingTime
|
|
1263
|
+
);
|
|
1264
|
+
} catch (error) {
|
|
1265
|
+
throw StakingError.fromUnknown(
|
|
1266
|
+
error,
|
|
1267
|
+
"SCRIPT_FAILURE" /* SCRIPT_FAILURE */,
|
|
1268
|
+
"Cannot build staking script data"
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
1271
|
+
let scripts;
|
|
1272
|
+
try {
|
|
1273
|
+
scripts = stakingScriptData.buildScripts();
|
|
1274
|
+
} catch (error) {
|
|
1275
|
+
throw StakingError.fromUnknown(
|
|
1276
|
+
error,
|
|
1277
|
+
"SCRIPT_FAILURE" /* SCRIPT_FAILURE */,
|
|
1278
|
+
"Cannot build staking scripts"
|
|
1279
|
+
);
|
|
1280
|
+
}
|
|
1281
|
+
return scripts;
|
|
1282
|
+
}
|
|
1283
|
+
/**
|
|
1284
|
+
* Create a staking transaction for staking.
|
|
1285
|
+
*
|
|
1286
|
+
* @param {number} stakingAmountSat - The amount to stake in satoshis.
|
|
1287
|
+
* @param {UTXO[]} inputUTXOs - The UTXOs to use as inputs for the staking
|
|
1288
|
+
* transaction.
|
|
1289
|
+
* @param {number} feeRate - The fee rate for the transaction in satoshis per byte.
|
|
1290
|
+
* @returns {TransactionResult} - An object containing the unsigned
|
|
1291
|
+
* transaction, and fee
|
|
1292
|
+
* @throws {StakingError} - If the transaction cannot be built
|
|
1293
|
+
*/
|
|
1294
|
+
createStakingTransaction(stakingAmountSat, inputUTXOs, feeRate) {
|
|
1295
|
+
validateStakingTxInputData(
|
|
1296
|
+
stakingAmountSat,
|
|
1297
|
+
this.stakingTimelock,
|
|
1298
|
+
this.params,
|
|
1299
|
+
inputUTXOs,
|
|
1300
|
+
feeRate
|
|
1301
|
+
);
|
|
1302
|
+
const scripts = this.buildScripts();
|
|
1303
|
+
try {
|
|
1304
|
+
const { transaction, fee } = stakingTransaction(
|
|
1305
|
+
scripts,
|
|
1306
|
+
stakingAmountSat,
|
|
1307
|
+
this.stakerInfo.address,
|
|
1308
|
+
inputUTXOs,
|
|
1309
|
+
this.network,
|
|
1310
|
+
feeRate
|
|
1311
|
+
);
|
|
1312
|
+
return {
|
|
1313
|
+
transaction,
|
|
1314
|
+
fee
|
|
1315
|
+
};
|
|
1316
|
+
} catch (error) {
|
|
1317
|
+
throw StakingError.fromUnknown(
|
|
1318
|
+
error,
|
|
1319
|
+
"BUILD_TRANSACTION_FAILURE" /* BUILD_TRANSACTION_FAILURE */,
|
|
1320
|
+
"Cannot build unsigned staking transaction"
|
|
1321
|
+
);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
/**
|
|
1325
|
+
* Create a staking psbt based on the existing staking transaction.
|
|
1326
|
+
*
|
|
1327
|
+
* @param {Transaction} stakingTx - The staking transaction.
|
|
1328
|
+
* @param {UTXO[]} inputUTXOs - The UTXOs to use as inputs for the staking
|
|
1329
|
+
* transaction. The UTXOs that were used to create the staking transaction should
|
|
1330
|
+
* be included in this array.
|
|
1331
|
+
* @returns {Psbt} - The psbt.
|
|
1332
|
+
*/
|
|
1333
|
+
toStakingPsbt(stakingTx, inputUTXOs) {
|
|
1334
|
+
const scripts = this.buildScripts();
|
|
1335
|
+
const stakingOutputInfo = deriveStakingOutputInfo(scripts, this.network);
|
|
1336
|
+
findMatchingTxOutputIndex(
|
|
1337
|
+
stakingTx,
|
|
1338
|
+
stakingOutputInfo.outputAddress,
|
|
1339
|
+
this.network
|
|
1340
|
+
);
|
|
1341
|
+
return stakingPsbt(
|
|
1342
|
+
stakingTx,
|
|
1343
|
+
this.network,
|
|
1344
|
+
inputUTXOs,
|
|
1345
|
+
isTaproot(
|
|
1346
|
+
this.stakerInfo.address,
|
|
1347
|
+
this.network
|
|
1348
|
+
) ? Buffer.from(this.stakerInfo.publicKeyNoCoordHex, "hex") : void 0
|
|
1349
|
+
);
|
|
1350
|
+
}
|
|
1351
|
+
/**
|
|
1352
|
+
* Create an unbonding transaction for staking.
|
|
1353
|
+
*
|
|
1354
|
+
* @param {Transaction} stakingTx - The staking transaction to unbond.
|
|
1355
|
+
* @returns {TransactionResult} - An object containing the unsigned
|
|
1356
|
+
* transaction, and fee
|
|
1357
|
+
* @throws {StakingError} - If the transaction cannot be built
|
|
1358
|
+
*/
|
|
1359
|
+
createUnbondingTransaction(stakingTx) {
|
|
1360
|
+
const scripts = this.buildScripts();
|
|
1361
|
+
const { outputAddress } = deriveStakingOutputInfo(scripts, this.network);
|
|
1362
|
+
const stakingOutputIndex = findMatchingTxOutputIndex(
|
|
1363
|
+
stakingTx,
|
|
1364
|
+
outputAddress,
|
|
1365
|
+
this.network
|
|
1366
|
+
);
|
|
1367
|
+
try {
|
|
1368
|
+
const { transaction } = unbondingTransaction(
|
|
1369
|
+
scripts,
|
|
1370
|
+
stakingTx,
|
|
1371
|
+
this.params.unbondingFeeSat,
|
|
1372
|
+
this.network,
|
|
1373
|
+
stakingOutputIndex
|
|
1374
|
+
);
|
|
1375
|
+
return {
|
|
1376
|
+
transaction,
|
|
1377
|
+
fee: this.params.unbondingFeeSat
|
|
1378
|
+
};
|
|
1379
|
+
} catch (error) {
|
|
1380
|
+
throw StakingError.fromUnknown(
|
|
1381
|
+
error,
|
|
1382
|
+
"BUILD_TRANSACTION_FAILURE" /* BUILD_TRANSACTION_FAILURE */,
|
|
1383
|
+
"Cannot build the unbonding transaction"
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
/**
|
|
1388
|
+
* Create an unbonding psbt based on the existing unbonding transaction and
|
|
1389
|
+
* staking transaction.
|
|
1390
|
+
*
|
|
1391
|
+
* @param {Transaction} unbondingTx - The unbonding transaction.
|
|
1392
|
+
* @param {Transaction} stakingTx - The staking transaction.
|
|
1393
|
+
*
|
|
1394
|
+
* @returns {Psbt} - The psbt.
|
|
1395
|
+
*/
|
|
1396
|
+
toUnbondingPsbt(unbondingTx, stakingTx) {
|
|
1397
|
+
return unbondingPsbt(
|
|
1398
|
+
this.buildScripts(),
|
|
1399
|
+
unbondingTx,
|
|
1400
|
+
stakingTx,
|
|
1401
|
+
this.network
|
|
1402
|
+
);
|
|
1403
|
+
}
|
|
1404
|
+
/**
|
|
1405
|
+
* Creates a withdrawal transaction that spends from an unbonding or slashing
|
|
1406
|
+
* transaction. The timelock on the input transaction must have expired before
|
|
1407
|
+
* this withdrawal can be valid.
|
|
1408
|
+
*
|
|
1409
|
+
* @param {Transaction} earlyUnbondedTx - The unbonding or slashing
|
|
1410
|
+
* transaction to withdraw from
|
|
1411
|
+
* @param {number} feeRate - Fee rate in satoshis per byte for the withdrawal
|
|
1412
|
+
* transaction
|
|
1413
|
+
* @returns {PsbtResult} - Contains the unsigned PSBT and fee amount
|
|
1414
|
+
* @throws {StakingError} - If the input transaction is invalid or withdrawal
|
|
1415
|
+
* transaction cannot be built
|
|
1416
|
+
*/
|
|
1417
|
+
createWithdrawEarlyUnbondedTransaction(earlyUnbondedTx, feeRate) {
|
|
1418
|
+
const scripts = this.buildScripts();
|
|
1419
|
+
try {
|
|
1420
|
+
return withdrawEarlyUnbondedTransaction(
|
|
1421
|
+
scripts,
|
|
1422
|
+
earlyUnbondedTx,
|
|
1423
|
+
this.stakerInfo.address,
|
|
1424
|
+
this.network,
|
|
1425
|
+
feeRate
|
|
1426
|
+
);
|
|
1427
|
+
} catch (error) {
|
|
1428
|
+
throw StakingError.fromUnknown(
|
|
1429
|
+
error,
|
|
1430
|
+
"BUILD_TRANSACTION_FAILURE" /* BUILD_TRANSACTION_FAILURE */,
|
|
1431
|
+
"Cannot build unsigned withdraw early unbonded transaction"
|
|
1432
|
+
);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
/**
|
|
1436
|
+
* Create a withdrawal psbt that spends a naturally expired staking
|
|
1437
|
+
* transaction.
|
|
1438
|
+
*
|
|
1439
|
+
* @param {Transaction} stakingTx - The staking transaction to withdraw from.
|
|
1440
|
+
* @param {number} feeRate - The fee rate for the transaction in satoshis per byte.
|
|
1441
|
+
* @returns {PsbtResult} - An object containing the unsigned psbt and fee
|
|
1442
|
+
* @throws {StakingError} - If the delegation is invalid or the transaction cannot be built
|
|
1443
|
+
*/
|
|
1444
|
+
createWithdrawStakingExpiredPsbt(stakingTx, feeRate) {
|
|
1445
|
+
const scripts = this.buildScripts();
|
|
1446
|
+
const { outputAddress } = deriveStakingOutputInfo(scripts, this.network);
|
|
1447
|
+
const stakingOutputIndex = findMatchingTxOutputIndex(
|
|
1448
|
+
stakingTx,
|
|
1449
|
+
outputAddress,
|
|
1450
|
+
this.network
|
|
1451
|
+
);
|
|
1452
|
+
try {
|
|
1453
|
+
return withdrawTimelockUnbondedTransaction(
|
|
1454
|
+
scripts,
|
|
1455
|
+
stakingTx,
|
|
1456
|
+
this.stakerInfo.address,
|
|
1457
|
+
this.network,
|
|
1458
|
+
feeRate,
|
|
1459
|
+
stakingOutputIndex
|
|
1460
|
+
);
|
|
1461
|
+
} catch (error) {
|
|
1462
|
+
throw StakingError.fromUnknown(
|
|
1463
|
+
error,
|
|
1464
|
+
"BUILD_TRANSACTION_FAILURE" /* BUILD_TRANSACTION_FAILURE */,
|
|
1465
|
+
"Cannot build unsigned timelock unbonded transaction"
|
|
1466
|
+
);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
/**
|
|
1470
|
+
* Create a slashing psbt spending from the staking output.
|
|
1471
|
+
*
|
|
1472
|
+
* @param {Transaction} stakingTx - The staking transaction to slash.
|
|
1473
|
+
* @returns {PsbtResult} - An object containing the unsigned psbt and fee
|
|
1474
|
+
* @throws {StakingError} - If the delegation is invalid or the transaction cannot be built
|
|
1475
|
+
*/
|
|
1476
|
+
createStakingOutputSlashingPsbt(stakingTx) {
|
|
1477
|
+
if (!this.params.slashing) {
|
|
1478
|
+
throw new StakingError(
|
|
1479
|
+
"INVALID_PARAMS" /* INVALID_PARAMS */,
|
|
1480
|
+
"Slashing parameters are missing"
|
|
1481
|
+
);
|
|
1482
|
+
}
|
|
1483
|
+
const scripts = this.buildScripts();
|
|
1484
|
+
try {
|
|
1485
|
+
const { psbt } = slashTimelockUnbondedTransaction(
|
|
1486
|
+
scripts,
|
|
1487
|
+
stakingTx,
|
|
1488
|
+
this.params.slashing.slashingPkScriptHex,
|
|
1489
|
+
this.params.slashing.slashingRate,
|
|
1490
|
+
this.params.slashing.minSlashingTxFeeSat,
|
|
1491
|
+
this.network
|
|
1492
|
+
);
|
|
1493
|
+
return {
|
|
1494
|
+
psbt,
|
|
1495
|
+
fee: this.params.slashing.minSlashingTxFeeSat
|
|
1496
|
+
};
|
|
1497
|
+
} catch (error) {
|
|
1498
|
+
throw StakingError.fromUnknown(
|
|
1499
|
+
error,
|
|
1500
|
+
"BUILD_TRANSACTION_FAILURE" /* BUILD_TRANSACTION_FAILURE */,
|
|
1501
|
+
"Cannot build the slash timelock unbonded transaction"
|
|
1502
|
+
);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
/**
|
|
1506
|
+
* Create a slashing psbt for an unbonding output.
|
|
1507
|
+
*
|
|
1508
|
+
* @param {Transaction} unbondingTx - The unbonding transaction to slash.
|
|
1509
|
+
* @returns {PsbtResult} - An object containing the unsigned psbt and fee
|
|
1510
|
+
* @throws {StakingError} - If the delegation is invalid or the transaction cannot be built
|
|
1511
|
+
*/
|
|
1512
|
+
createUnbondingOutputSlashingPsbt(unbondingTx) {
|
|
1513
|
+
if (!this.params.slashing) {
|
|
1514
|
+
throw new StakingError(
|
|
1515
|
+
"INVALID_PARAMS" /* INVALID_PARAMS */,
|
|
1516
|
+
"Slashing parameters are missing"
|
|
1517
|
+
);
|
|
1518
|
+
}
|
|
1519
|
+
const scripts = this.buildScripts();
|
|
1520
|
+
try {
|
|
1521
|
+
const { psbt } = slashEarlyUnbondedTransaction(
|
|
1522
|
+
scripts,
|
|
1523
|
+
unbondingTx,
|
|
1524
|
+
this.params.slashing.slashingPkScriptHex,
|
|
1525
|
+
this.params.slashing.slashingRate,
|
|
1526
|
+
this.params.slashing.minSlashingTxFeeSat,
|
|
1527
|
+
this.network
|
|
1528
|
+
);
|
|
1529
|
+
return {
|
|
1530
|
+
psbt,
|
|
1531
|
+
fee: this.params.slashing.minSlashingTxFeeSat
|
|
1532
|
+
};
|
|
1533
|
+
} catch (error) {
|
|
1534
|
+
throw StakingError.fromUnknown(
|
|
1535
|
+
error,
|
|
1536
|
+
"BUILD_TRANSACTION_FAILURE" /* BUILD_TRANSACTION_FAILURE */,
|
|
1537
|
+
"Cannot build the slash early unbonded transaction"
|
|
1538
|
+
);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
/**
|
|
1542
|
+
* Create a withdraw slashing psbt that spends a slashing transaction from the
|
|
1543
|
+
* staking output.
|
|
1544
|
+
*
|
|
1545
|
+
* @param {Transaction} slashingTx - The slashing transaction.
|
|
1546
|
+
* @param {number} feeRate - The fee rate for the transaction in satoshis per byte.
|
|
1547
|
+
* @returns {PsbtResult} - An object containing the unsigned psbt and fee
|
|
1548
|
+
* @throws {StakingError} - If the delegation is invalid or the transaction cannot be built
|
|
1549
|
+
*/
|
|
1550
|
+
createWithdrawSlashingPsbt(slashingTx, feeRate) {
|
|
1551
|
+
const scripts = this.buildScripts();
|
|
1552
|
+
const slashingOutputInfo = deriveSlashingOutput(scripts, this.network);
|
|
1553
|
+
const slashingOutputIndex = findMatchingTxOutputIndex(
|
|
1554
|
+
slashingTx,
|
|
1555
|
+
slashingOutputInfo.outputAddress,
|
|
1556
|
+
this.network
|
|
1557
|
+
);
|
|
1558
|
+
try {
|
|
1559
|
+
return withdrawSlashingTransaction(
|
|
1560
|
+
scripts,
|
|
1561
|
+
slashingTx,
|
|
1562
|
+
this.stakerInfo.address,
|
|
1563
|
+
this.network,
|
|
1564
|
+
feeRate,
|
|
1565
|
+
slashingOutputIndex
|
|
1566
|
+
);
|
|
1567
|
+
} catch (error) {
|
|
1568
|
+
throw StakingError.fromUnknown(
|
|
1569
|
+
error,
|
|
1570
|
+
"BUILD_TRANSACTION_FAILURE" /* BUILD_TRANSACTION_FAILURE */,
|
|
1571
|
+
"Cannot build withdraw slashing transaction"
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
};
|
|
1576
|
+
|
|
1577
|
+
// src/staking/observable/observableStakingScript.ts
|
|
1578
|
+
import { opcodes as opcodes3, script as script3 } from "bitcoinjs-lib";
|
|
1579
|
+
var ObservableStakingScriptData = class extends StakingScriptData {
|
|
1580
|
+
constructor(stakerKey, finalityProviderKeys, covenantKeys, covenantThreshold, stakingTimelock, unbondingTimelock, magicBytes) {
|
|
1581
|
+
super(
|
|
1582
|
+
stakerKey,
|
|
1583
|
+
finalityProviderKeys,
|
|
1584
|
+
covenantKeys,
|
|
1585
|
+
covenantThreshold,
|
|
1586
|
+
stakingTimelock,
|
|
1587
|
+
unbondingTimelock
|
|
1588
|
+
);
|
|
1589
|
+
if (!magicBytes) {
|
|
1590
|
+
throw new Error("Missing required input values");
|
|
1591
|
+
}
|
|
1592
|
+
if (magicBytes.length != MAGIC_BYTES_LEN) {
|
|
1593
|
+
throw new Error("Invalid script data provided");
|
|
1594
|
+
}
|
|
1595
|
+
this.magicBytes = magicBytes;
|
|
1596
|
+
}
|
|
1597
|
+
/**
|
|
1598
|
+
* Builds a data embed script for staking in the form:
|
|
1599
|
+
* OP_RETURN || <serializedStakingData>
|
|
1600
|
+
* where serializedStakingData is the concatenation of:
|
|
1601
|
+
* MagicBytes || Version || StakerPublicKey || FinalityProviderPublicKey || StakingTimeLock
|
|
1602
|
+
* Note: Only a single finality provider key is supported for now in phase 1
|
|
1603
|
+
* @throws {Error} If the number of finality provider keys is not equal to 1.
|
|
1604
|
+
* @returns {Buffer} The compiled data embed script.
|
|
1605
|
+
*/
|
|
1606
|
+
buildDataEmbedScript() {
|
|
1607
|
+
if (this.finalityProviderKeys.length != 1) {
|
|
1608
|
+
throw new Error("Only a single finality provider key is supported");
|
|
1609
|
+
}
|
|
1610
|
+
const version = Buffer.alloc(1);
|
|
1611
|
+
version.writeUInt8(0);
|
|
1612
|
+
const stakingTimeLock = Buffer.alloc(2);
|
|
1613
|
+
stakingTimeLock.writeUInt16BE(this.stakingTimeLock);
|
|
1614
|
+
const serializedStakingData = Buffer.concat([
|
|
1615
|
+
this.magicBytes,
|
|
1616
|
+
version,
|
|
1617
|
+
this.stakerKey,
|
|
1618
|
+
this.finalityProviderKeys[0],
|
|
1619
|
+
stakingTimeLock
|
|
1620
|
+
]);
|
|
1621
|
+
return script3.compile([opcodes3.OP_RETURN, serializedStakingData]);
|
|
1622
|
+
}
|
|
1623
|
+
/**
|
|
1624
|
+
* Builds the staking scripts.
|
|
1625
|
+
* @returns {ObservableStakingScripts} The staking scripts that can be used to stake.
|
|
1626
|
+
* contains the timelockScript, unbondingScript, slashingScript,
|
|
1627
|
+
* unbondingTimelockScript, and dataEmbedScript.
|
|
1628
|
+
* @throws {Error} If script data is invalid.
|
|
1629
|
+
*/
|
|
1630
|
+
buildScripts() {
|
|
1631
|
+
const scripts = super.buildScripts();
|
|
1632
|
+
return {
|
|
1633
|
+
...scripts,
|
|
1634
|
+
dataEmbedScript: this.buildDataEmbedScript()
|
|
1635
|
+
};
|
|
1636
|
+
}
|
|
1637
|
+
};
|
|
1638
|
+
|
|
1639
|
+
// src/staking/observable/index.ts
|
|
1640
|
+
var ObservableStaking = class extends Staking {
|
|
1641
|
+
constructor(network, stakerInfo, params, finalityProviderPkNoCoordHex, stakingTimelock) {
|
|
1642
|
+
super(
|
|
1643
|
+
network,
|
|
1644
|
+
stakerInfo,
|
|
1645
|
+
params,
|
|
1646
|
+
finalityProviderPkNoCoordHex,
|
|
1647
|
+
stakingTimelock
|
|
1648
|
+
);
|
|
1649
|
+
if (!params.tag) {
|
|
1650
|
+
throw new StakingError(
|
|
1651
|
+
"INVALID_INPUT" /* INVALID_INPUT */,
|
|
1652
|
+
"Observable staking parameters must include tag"
|
|
1653
|
+
);
|
|
1654
|
+
}
|
|
1655
|
+
if (!params.btcActivationHeight) {
|
|
1656
|
+
throw new StakingError(
|
|
1657
|
+
"INVALID_INPUT" /* INVALID_INPUT */,
|
|
1658
|
+
"Observable staking parameters must include a positive activation height"
|
|
1659
|
+
);
|
|
1660
|
+
}
|
|
1661
|
+
this.params = params;
|
|
1662
|
+
}
|
|
1663
|
+
/**
|
|
1664
|
+
* Build the staking scripts for observable staking.
|
|
1665
|
+
* This method overwrites the base method to include the OP_RETURN tag based
|
|
1666
|
+
* on the tag provided in the parameters.
|
|
1667
|
+
*
|
|
1668
|
+
* @returns {ObservableStakingScripts} - The staking scripts for observable staking.
|
|
1669
|
+
* @throws {StakingError} - If the scripts cannot be built.
|
|
1670
|
+
*/
|
|
1671
|
+
buildScripts() {
|
|
1672
|
+
const { covenantQuorum, covenantNoCoordPks, unbondingTime, tag } = this.params;
|
|
1673
|
+
let stakingScriptData;
|
|
1674
|
+
try {
|
|
1675
|
+
stakingScriptData = new ObservableStakingScriptData(
|
|
1676
|
+
Buffer.from(this.stakerInfo.publicKeyNoCoordHex, "hex"),
|
|
1677
|
+
[Buffer.from(this.finalityProviderPkNoCoordHex, "hex")],
|
|
1678
|
+
toBuffers(covenantNoCoordPks),
|
|
1679
|
+
covenantQuorum,
|
|
1680
|
+
this.stakingTimelock,
|
|
1681
|
+
unbondingTime,
|
|
1682
|
+
Buffer.from(tag, "hex")
|
|
1683
|
+
);
|
|
1684
|
+
} catch (error) {
|
|
1685
|
+
throw StakingError.fromUnknown(
|
|
1686
|
+
error,
|
|
1687
|
+
"SCRIPT_FAILURE" /* SCRIPT_FAILURE */,
|
|
1688
|
+
"Cannot build staking script data"
|
|
1689
|
+
);
|
|
1690
|
+
}
|
|
1691
|
+
let scripts;
|
|
1692
|
+
try {
|
|
1693
|
+
scripts = stakingScriptData.buildScripts();
|
|
1694
|
+
} catch (error) {
|
|
1695
|
+
throw StakingError.fromUnknown(
|
|
1696
|
+
error,
|
|
1697
|
+
"SCRIPT_FAILURE" /* SCRIPT_FAILURE */,
|
|
1698
|
+
"Cannot build staking scripts"
|
|
1699
|
+
);
|
|
1700
|
+
}
|
|
1701
|
+
return scripts;
|
|
1702
|
+
}
|
|
1703
|
+
/**
|
|
1704
|
+
* Create a staking transaction for observable staking.
|
|
1705
|
+
* This overwrites the method from the Staking class with the addtion
|
|
1706
|
+
* of the
|
|
1707
|
+
* 1. OP_RETURN tag in the staking scripts
|
|
1708
|
+
* 2. lockHeight parameter
|
|
1709
|
+
*
|
|
1710
|
+
* @param {number} stakingAmountSat - The amount to stake in satoshis.
|
|
1711
|
+
* @param {UTXO[]} inputUTXOs - The UTXOs to use as inputs for the staking
|
|
1712
|
+
* transaction.
|
|
1713
|
+
* @param {number} feeRate - The fee rate for the transaction in satoshis per byte.
|
|
1714
|
+
* @returns {TransactionResult} - An object containing the unsigned transaction,
|
|
1715
|
+
* and fee
|
|
1716
|
+
*/
|
|
1717
|
+
createStakingTransaction(stakingAmountSat, inputUTXOs, feeRate) {
|
|
1718
|
+
validateStakingTxInputData(
|
|
1719
|
+
stakingAmountSat,
|
|
1720
|
+
this.stakingTimelock,
|
|
1721
|
+
this.params,
|
|
1722
|
+
inputUTXOs,
|
|
1723
|
+
feeRate
|
|
1724
|
+
);
|
|
1725
|
+
const scripts = this.buildScripts();
|
|
1726
|
+
try {
|
|
1727
|
+
const { transaction, fee } = stakingTransaction(
|
|
1728
|
+
scripts,
|
|
1729
|
+
stakingAmountSat,
|
|
1730
|
+
this.stakerInfo.address,
|
|
1731
|
+
inputUTXOs,
|
|
1732
|
+
this.network,
|
|
1733
|
+
feeRate,
|
|
1734
|
+
// `lockHeight` is exclusive of the provided value.
|
|
1735
|
+
// For example, if a Bitcoin height of X is provided,
|
|
1736
|
+
// the transaction will be included starting from height X+1.
|
|
1737
|
+
// https://learnmeabitcoin.com/technical/transaction/locktime/
|
|
1738
|
+
this.params.btcActivationHeight - 1
|
|
1739
|
+
);
|
|
1740
|
+
return {
|
|
1741
|
+
transaction,
|
|
1742
|
+
fee
|
|
1743
|
+
};
|
|
1744
|
+
} catch (error) {
|
|
1745
|
+
throw StakingError.fromUnknown(
|
|
1746
|
+
error,
|
|
1747
|
+
"BUILD_TRANSACTION_FAILURE" /* BUILD_TRANSACTION_FAILURE */,
|
|
1748
|
+
"Cannot build unsigned staking transaction"
|
|
1749
|
+
);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
/**
|
|
1753
|
+
* Create a staking psbt for observable staking.
|
|
1754
|
+
*
|
|
1755
|
+
* @param {Transaction} stakingTx - The staking transaction.
|
|
1756
|
+
* @param {UTXO[]} inputUTXOs - The UTXOs to use as inputs for the staking
|
|
1757
|
+
* transaction.
|
|
1758
|
+
* @returns {Psbt} - The psbt.
|
|
1759
|
+
*/
|
|
1760
|
+
toStakingPsbt(stakingTx, inputUTXOs) {
|
|
1761
|
+
return stakingPsbt(
|
|
1762
|
+
stakingTx,
|
|
1763
|
+
this.network,
|
|
1764
|
+
inputUTXOs,
|
|
1765
|
+
isTaproot(
|
|
1766
|
+
this.stakerInfo.address,
|
|
1767
|
+
this.network
|
|
1768
|
+
) ? Buffer.from(this.stakerInfo.publicKeyNoCoordHex, "hex") : void 0
|
|
1769
|
+
);
|
|
1770
|
+
}
|
|
1771
|
+
};
|
|
1772
|
+
|
|
1773
|
+
// src/utils/staking/param.ts
|
|
1774
|
+
var getBabylonParamByBtcHeight = (height, babylonParamsVersions) => {
|
|
1775
|
+
const sortedParams = [...babylonParamsVersions].sort(
|
|
1776
|
+
(a, b) => b.btcActivationHeight - a.btcActivationHeight
|
|
1777
|
+
);
|
|
1778
|
+
const params = sortedParams.find(
|
|
1779
|
+
(p) => height >= p.btcActivationHeight
|
|
1780
|
+
);
|
|
1781
|
+
if (!params)
|
|
1782
|
+
throw new Error(`Babylon params not found for height ${height}`);
|
|
1783
|
+
return params;
|
|
1784
|
+
};
|
|
1785
|
+
var getBabylonParamByVersion = (version, babylonParams) => {
|
|
1786
|
+
const params = babylonParams.find((p) => p.version === version);
|
|
1787
|
+
if (!params)
|
|
1788
|
+
throw new Error(`Babylon params not found for version ${version}`);
|
|
1789
|
+
return params;
|
|
1790
|
+
};
|
|
1791
|
+
|
|
1792
|
+
// src/staking/manager.ts
|
|
1793
|
+
import { Psbt as Psbt3 } from "bitcoinjs-lib";
|
|
1794
|
+
import { fromBech32 as fromBech322 } from "@cosmjs/encoding";
|
|
1795
|
+
import {
|
|
1796
|
+
btccheckpoint,
|
|
1797
|
+
btcstaking,
|
|
1798
|
+
btcstakingtx
|
|
1799
|
+
} from "@babylonlabs-io/babylon-proto-ts";
|
|
1800
|
+
import {
|
|
1801
|
+
BTCSigType
|
|
1802
|
+
} from "@babylonlabs-io/babylon-proto-ts/dist/generated/babylon/btcstaking/v1/pop";
|
|
1803
|
+
|
|
1804
|
+
// src/constants/registry.ts
|
|
1805
|
+
var BABYLON_REGISTRY_TYPE_URLS = {
|
|
1806
|
+
MsgCreateBTCDelegation: "/babylon.btcstaking.v1.MsgCreateBTCDelegation"
|
|
1807
|
+
};
|
|
1808
|
+
|
|
1809
|
+
// src/utils/index.ts
|
|
1810
|
+
var reverseBuffer = (buffer) => {
|
|
1811
|
+
const clonedBuffer = new Uint8Array(buffer);
|
|
1812
|
+
if (clonedBuffer.length < 1)
|
|
1813
|
+
return clonedBuffer;
|
|
1814
|
+
for (let i = 0, j = clonedBuffer.length - 1; i < clonedBuffer.length / 2; i++, j--) {
|
|
1815
|
+
let tmp = clonedBuffer[i];
|
|
1816
|
+
clonedBuffer[i] = clonedBuffer[j];
|
|
1817
|
+
clonedBuffer[j] = tmp;
|
|
1818
|
+
}
|
|
1819
|
+
return clonedBuffer;
|
|
1820
|
+
};
|
|
1821
|
+
var uint8ArrayToHex = (uint8Array) => {
|
|
1822
|
+
return Array.from(uint8Array).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1823
|
+
};
|
|
1824
|
+
|
|
1825
|
+
// src/utils/babylon.ts
|
|
1826
|
+
import { fromBech32 } from "@cosmjs/encoding";
|
|
1827
|
+
var isValidBabylonAddress = (address4) => {
|
|
1828
|
+
try {
|
|
1829
|
+
const { prefix } = fromBech32(address4);
|
|
1830
|
+
return prefix === "bbn";
|
|
1831
|
+
} catch (error) {
|
|
1832
|
+
return false;
|
|
1833
|
+
}
|
|
1834
|
+
};
|
|
1835
|
+
|
|
1836
|
+
// src/staking/manager.ts
|
|
1837
|
+
var SigningStep = /* @__PURE__ */ ((SigningStep2) => {
|
|
1838
|
+
SigningStep2["STAKING_SLASHING"] = "staking-slashing";
|
|
1839
|
+
SigningStep2["UNBONDING_SLASHING"] = "unbonding-slashing";
|
|
1840
|
+
SigningStep2["PROOF_OF_POSSESSION"] = "proof-of-possession";
|
|
1841
|
+
SigningStep2["CREATE_BTC_DELEGATION_MSG"] = "create-btc-delegation-msg";
|
|
1842
|
+
SigningStep2["STAKING"] = "staking";
|
|
1843
|
+
SigningStep2["UNBONDING"] = "unbonding";
|
|
1844
|
+
SigningStep2["WITHDRAW_STAKING_EXPIRED"] = "withdraw-staking-expired";
|
|
1845
|
+
SigningStep2["WITHDRAW_EARLY_UNBONDED"] = "withdraw-early-unbonded";
|
|
1846
|
+
SigningStep2["WITHDRAW_SLASHING"] = "withdraw-slashing";
|
|
1847
|
+
return SigningStep2;
|
|
1848
|
+
})(SigningStep || {});
|
|
1849
|
+
var BabylonBtcStakingManager = class {
|
|
1850
|
+
constructor(network, stakingParams, btcProvider, babylonProvider) {
|
|
1851
|
+
this.network = network;
|
|
1852
|
+
this.btcProvider = btcProvider;
|
|
1853
|
+
this.babylonProvider = babylonProvider;
|
|
1854
|
+
if (stakingParams.length === 0) {
|
|
1855
|
+
throw new Error("No staking parameters provided");
|
|
1856
|
+
}
|
|
1857
|
+
this.stakingParams = stakingParams;
|
|
1858
|
+
}
|
|
1859
|
+
/**
|
|
1860
|
+
* Creates a signed Pre-Staking Registration transaction that is ready to be
|
|
1861
|
+
* sent to the Babylon chain.
|
|
1862
|
+
* @param stakerBtcInfo - The staker BTC info which includes the BTC address
|
|
1863
|
+
* and the no-coord public key in hex format.
|
|
1864
|
+
* @param stakingInput - The staking inputs.
|
|
1865
|
+
* @param babylonBtcTipHeight - The Babylon BTC tip height.
|
|
1866
|
+
* @param inputUTXOs - The UTXOs that will be used to pay for the staking
|
|
1867
|
+
* transaction.
|
|
1868
|
+
* @param feeRate - The fee rate in satoshis per byte.
|
|
1869
|
+
* @param babylonAddress - The Babylon bech32 encoded address of the staker.
|
|
1870
|
+
* @returns The signed babylon pre-staking registration transaction in base64
|
|
1871
|
+
* format.
|
|
1872
|
+
*/
|
|
1873
|
+
async preStakeRegistrationBabylonTransaction(stakerBtcInfo, stakingInput, babylonBtcTipHeight, inputUTXOs, feeRate, babylonAddress) {
|
|
1874
|
+
if (babylonBtcTipHeight === 0) {
|
|
1875
|
+
throw new Error("Babylon BTC tip height cannot be 0");
|
|
1876
|
+
}
|
|
1877
|
+
if (inputUTXOs.length === 0) {
|
|
1878
|
+
throw new Error("No input UTXOs provided");
|
|
1879
|
+
}
|
|
1880
|
+
if (!isValidBabylonAddress(babylonAddress)) {
|
|
1881
|
+
throw new Error("Invalid Babylon address");
|
|
1882
|
+
}
|
|
1883
|
+
const params = getBabylonParamByBtcHeight(
|
|
1884
|
+
babylonBtcTipHeight,
|
|
1885
|
+
this.stakingParams
|
|
1886
|
+
);
|
|
1887
|
+
const staking = new Staking(
|
|
1888
|
+
this.network,
|
|
1889
|
+
stakerBtcInfo,
|
|
1890
|
+
params,
|
|
1891
|
+
stakingInput.finalityProviderPkNoCoordHex,
|
|
1892
|
+
stakingInput.stakingTimelock
|
|
1893
|
+
);
|
|
1894
|
+
const { transaction } = staking.createStakingTransaction(
|
|
1895
|
+
stakingInput.stakingAmountSat,
|
|
1896
|
+
inputUTXOs,
|
|
1897
|
+
feeRate
|
|
1898
|
+
);
|
|
1899
|
+
const msg = await this.createBtcDelegationMsg(
|
|
1900
|
+
staking,
|
|
1901
|
+
stakingInput,
|
|
1902
|
+
transaction,
|
|
1903
|
+
babylonAddress,
|
|
1904
|
+
stakerBtcInfo,
|
|
1905
|
+
params
|
|
1906
|
+
);
|
|
1907
|
+
return {
|
|
1908
|
+
signedBabylonTx: await this.babylonProvider.signTransaction(
|
|
1909
|
+
"create-btc-delegation-msg" /* CREATE_BTC_DELEGATION_MSG */,
|
|
1910
|
+
msg
|
|
1911
|
+
),
|
|
1912
|
+
stakingTx: transaction
|
|
1913
|
+
};
|
|
1914
|
+
}
|
|
1915
|
+
/**
|
|
1916
|
+
* Creates a signed post-staking registration transaction that is ready to be
|
|
1917
|
+
* sent to the Babylon chain. This is used when a staking transaction is
|
|
1918
|
+
* already created and included in a BTC block and we want to register it on
|
|
1919
|
+
* the Babylon chain.
|
|
1920
|
+
* @param stakerBtcInfo - The staker BTC info which includes the BTC address
|
|
1921
|
+
* and the no-coord public key in hex format.
|
|
1922
|
+
* @param stakingTx - The staking transaction.
|
|
1923
|
+
* @param stakingTxHeight - The BTC height in which the staking transaction
|
|
1924
|
+
* is included.
|
|
1925
|
+
* @param stakingInput - The staking inputs.
|
|
1926
|
+
* @param inclusionProof - The inclusion proof of the staking transaction.
|
|
1927
|
+
* @param babylonAddress - The Babylon bech32 encoded address of the staker.
|
|
1928
|
+
* @returns The signed babylon transaction in base64 format.
|
|
1929
|
+
*/
|
|
1930
|
+
async postStakeRegistrationBabylonTransaction(stakerBtcInfo, stakingTx, stakingTxHeight, stakingInput, inclusionProof, babylonAddress) {
|
|
1931
|
+
const params = getBabylonParamByBtcHeight(stakingTxHeight, this.stakingParams);
|
|
1932
|
+
if (!isValidBabylonAddress(babylonAddress)) {
|
|
1933
|
+
throw new Error("Invalid Babylon address");
|
|
1934
|
+
}
|
|
1935
|
+
const stakingInstance = new Staking(
|
|
1936
|
+
this.network,
|
|
1937
|
+
stakerBtcInfo,
|
|
1938
|
+
params,
|
|
1939
|
+
stakingInput.finalityProviderPkNoCoordHex,
|
|
1940
|
+
stakingInput.stakingTimelock
|
|
1941
|
+
);
|
|
1942
|
+
const scripts = stakingInstance.buildScripts();
|
|
1943
|
+
const stakingOutputInfo = deriveStakingOutputInfo(scripts, this.network);
|
|
1944
|
+
findMatchingTxOutputIndex(
|
|
1945
|
+
stakingTx,
|
|
1946
|
+
stakingOutputInfo.outputAddress,
|
|
1947
|
+
this.network
|
|
1948
|
+
);
|
|
1949
|
+
const delegationMsg = await this.createBtcDelegationMsg(
|
|
1950
|
+
stakingInstance,
|
|
1951
|
+
stakingInput,
|
|
1952
|
+
stakingTx,
|
|
1953
|
+
babylonAddress,
|
|
1954
|
+
stakerBtcInfo,
|
|
1955
|
+
params,
|
|
1956
|
+
this.getInclusionProof(inclusionProof)
|
|
1957
|
+
);
|
|
1958
|
+
return {
|
|
1959
|
+
signedBabylonTx: await this.babylonProvider.signTransaction(
|
|
1960
|
+
"create-btc-delegation-msg" /* CREATE_BTC_DELEGATION_MSG */,
|
|
1961
|
+
delegationMsg
|
|
1962
|
+
)
|
|
1963
|
+
};
|
|
1964
|
+
}
|
|
1965
|
+
/**
|
|
1966
|
+
* Estimates the BTC fee required for staking.
|
|
1967
|
+
* @param stakerBtcInfo - The staker BTC info which includes the BTC address
|
|
1968
|
+
* and the no-coord public key in hex format.
|
|
1969
|
+
* @param babylonBtcTipHeight - The BTC tip height recorded on the Babylon
|
|
1970
|
+
* chain.
|
|
1971
|
+
* @param stakingInput - The staking inputs.
|
|
1972
|
+
* @param inputUTXOs - The UTXOs that will be used to pay for the staking
|
|
1973
|
+
* transaction.
|
|
1974
|
+
* @param feeRate - The fee rate in satoshis per byte.
|
|
1975
|
+
* @returns The estimated BTC fee in satoshis.
|
|
1976
|
+
*/
|
|
1977
|
+
estimateBtcStakingFee(stakerBtcInfo, babylonBtcTipHeight, stakingInput, inputUTXOs, feeRate) {
|
|
1978
|
+
if (babylonBtcTipHeight === 0) {
|
|
1979
|
+
throw new Error("Babylon BTC tip height cannot be 0");
|
|
1980
|
+
}
|
|
1981
|
+
const params = getBabylonParamByBtcHeight(
|
|
1982
|
+
babylonBtcTipHeight,
|
|
1983
|
+
this.stakingParams
|
|
1984
|
+
);
|
|
1985
|
+
const staking = new Staking(
|
|
1986
|
+
this.network,
|
|
1987
|
+
stakerBtcInfo,
|
|
1988
|
+
params,
|
|
1989
|
+
stakingInput.finalityProviderPkNoCoordHex,
|
|
1990
|
+
stakingInput.stakingTimelock
|
|
1991
|
+
);
|
|
1992
|
+
const { fee: stakingFee } = staking.createStakingTransaction(
|
|
1993
|
+
stakingInput.stakingAmountSat,
|
|
1994
|
+
inputUTXOs,
|
|
1995
|
+
feeRate
|
|
1996
|
+
);
|
|
1997
|
+
return stakingFee;
|
|
1998
|
+
}
|
|
1999
|
+
/**
|
|
2000
|
+
* Creates a signed staking transaction that is ready to be sent to the BTC
|
|
2001
|
+
* network.
|
|
2002
|
+
* @param stakerBtcInfo - The staker BTC info which includes the BTC address
|
|
2003
|
+
* and the no-coord public key in hex format.
|
|
2004
|
+
* @param stakingInput - The staking inputs.
|
|
2005
|
+
* @param unsignedStakingTx - The unsigned staking transaction.
|
|
2006
|
+
* @param inputUTXOs - The UTXOs that will be used to pay for the staking
|
|
2007
|
+
* transaction.
|
|
2008
|
+
* @param stakingParamsVersion - The params version that was used to create the
|
|
2009
|
+
* delegation in Babylon chain
|
|
2010
|
+
* @returns The signed staking transaction.
|
|
2011
|
+
*/
|
|
2012
|
+
async createSignedBtcStakingTransaction(stakerBtcInfo, stakingInput, unsignedStakingTx, inputUTXOs, stakingParamsVersion) {
|
|
2013
|
+
const params = getBabylonParamByVersion(stakingParamsVersion, this.stakingParams);
|
|
2014
|
+
if (inputUTXOs.length === 0) {
|
|
2015
|
+
throw new Error("No input UTXOs provided");
|
|
2016
|
+
}
|
|
2017
|
+
const staking = new Staking(
|
|
2018
|
+
this.network,
|
|
2019
|
+
stakerBtcInfo,
|
|
2020
|
+
params,
|
|
2021
|
+
stakingInput.finalityProviderPkNoCoordHex,
|
|
2022
|
+
stakingInput.stakingTimelock
|
|
2023
|
+
);
|
|
2024
|
+
const stakingPsbt2 = staking.toStakingPsbt(
|
|
2025
|
+
unsignedStakingTx,
|
|
2026
|
+
inputUTXOs
|
|
2027
|
+
);
|
|
2028
|
+
const signedStakingPsbtHex = await this.btcProvider.signPsbt(
|
|
2029
|
+
"staking" /* STAKING */,
|
|
2030
|
+
stakingPsbt2.toHex()
|
|
2031
|
+
);
|
|
2032
|
+
return Psbt3.fromHex(signedStakingPsbtHex).extractTransaction();
|
|
2033
|
+
}
|
|
2034
|
+
/**
|
|
2035
|
+
* Creates a partial signed unbonding transaction that is only signed by the
|
|
2036
|
+
* staker. In order to complete the unbonding transaction, the covenant
|
|
2037
|
+
* unbonding signatures need to be added to the transaction before sending it
|
|
2038
|
+
* to the BTC network.
|
|
2039
|
+
* NOTE: This method should only be used for Babylon phase-1 unbonding.
|
|
2040
|
+
* @param stakerBtcInfo - The staker BTC info which includes the BTC address
|
|
2041
|
+
* and the no-coord public key in hex format.
|
|
2042
|
+
* @param stakingInput - The staking inputs.
|
|
2043
|
+
* @param stakingParamsVersion - The params version that was used to create the
|
|
2044
|
+
* delegation in Babylon chain
|
|
2045
|
+
* @param stakingTx - The staking transaction.
|
|
2046
|
+
* @returns The partial signed unbonding transaction and its fee.
|
|
2047
|
+
*/
|
|
2048
|
+
async createPartialSignedBtcUnbondingTransaction(stakerBtcInfo, stakingInput, stakingParamsVersion, stakingTx) {
|
|
2049
|
+
const params = getBabylonParamByVersion(
|
|
2050
|
+
stakingParamsVersion,
|
|
2051
|
+
this.stakingParams
|
|
2052
|
+
);
|
|
2053
|
+
const staking = new Staking(
|
|
2054
|
+
this.network,
|
|
2055
|
+
stakerBtcInfo,
|
|
2056
|
+
params,
|
|
2057
|
+
stakingInput.finalityProviderPkNoCoordHex,
|
|
2058
|
+
stakingInput.stakingTimelock
|
|
2059
|
+
);
|
|
2060
|
+
const {
|
|
2061
|
+
transaction: unbondingTx,
|
|
2062
|
+
fee
|
|
2063
|
+
} = staking.createUnbondingTransaction(stakingTx);
|
|
2064
|
+
const psbt = staking.toUnbondingPsbt(unbondingTx, stakingTx);
|
|
2065
|
+
const signedUnbondingPsbtHex = await this.btcProvider.signPsbt(
|
|
2066
|
+
"unbonding" /* UNBONDING */,
|
|
2067
|
+
psbt.toHex()
|
|
2068
|
+
);
|
|
2069
|
+
const signedUnbondingTx = Psbt3.fromHex(
|
|
2070
|
+
signedUnbondingPsbtHex
|
|
2071
|
+
).extractTransaction();
|
|
2072
|
+
return {
|
|
2073
|
+
transaction: signedUnbondingTx,
|
|
2074
|
+
fee
|
|
2075
|
+
};
|
|
2076
|
+
}
|
|
2077
|
+
/**
|
|
2078
|
+
* Creates a signed unbonding transaction that is ready to be sent to the BTC
|
|
2079
|
+
* network.
|
|
2080
|
+
* @param stakerBtcInfo - The staker BTC info which includes the BTC address
|
|
2081
|
+
* and the no-coord public key in hex format.
|
|
2082
|
+
* @param stakingInput - The staking inputs.
|
|
2083
|
+
* @param stakingParamsVersion - The params version that was used to create the
|
|
2084
|
+
* delegation in Babylon chain
|
|
2085
|
+
* @param stakingTx - The staking transaction.
|
|
2086
|
+
* @param unsignedUnbondingTx - The unsigned unbonding transaction.
|
|
2087
|
+
* @param covenantUnbondingSignatures - The covenant unbonding signatures.
|
|
2088
|
+
* It can be retrieved from the Babylon chain or API.
|
|
2089
|
+
* @returns The signed unbonding transaction and its fee.
|
|
2090
|
+
*/
|
|
2091
|
+
async createSignedBtcUnbondingTransaction(stakerBtcInfo, stakingInput, stakingParamsVersion, stakingTx, unsignedUnbondingTx, covenantUnbondingSignatures) {
|
|
2092
|
+
const params = getBabylonParamByVersion(
|
|
2093
|
+
stakingParamsVersion,
|
|
2094
|
+
this.stakingParams
|
|
2095
|
+
);
|
|
2096
|
+
const {
|
|
2097
|
+
transaction: signedUnbondingTx,
|
|
2098
|
+
fee
|
|
2099
|
+
} = await this.createPartialSignedBtcUnbondingTransaction(
|
|
2100
|
+
stakerBtcInfo,
|
|
2101
|
+
stakingInput,
|
|
2102
|
+
stakingParamsVersion,
|
|
2103
|
+
stakingTx
|
|
2104
|
+
);
|
|
2105
|
+
if (signedUnbondingTx.getId() !== unsignedUnbondingTx.getId()) {
|
|
2106
|
+
throw new Error(
|
|
2107
|
+
"Unbonding transaction hash does not match the computed hash"
|
|
2108
|
+
);
|
|
2109
|
+
}
|
|
2110
|
+
const covenantBuffers = params.covenantNoCoordPks.map(
|
|
2111
|
+
(covenant) => Buffer.from(covenant, "hex")
|
|
2112
|
+
);
|
|
2113
|
+
const witness = createCovenantWitness(
|
|
2114
|
+
// Since unbonding transactions always have a single input and output,
|
|
2115
|
+
// we expect exactly one signature in TaprootScriptSpendSig when the
|
|
2116
|
+
// signing is successful
|
|
2117
|
+
signedUnbondingTx.ins[0].witness,
|
|
2118
|
+
covenantBuffers,
|
|
2119
|
+
covenantUnbondingSignatures,
|
|
2120
|
+
params.covenantQuorum
|
|
2121
|
+
);
|
|
2122
|
+
signedUnbondingTx.ins[0].witness = witness;
|
|
2123
|
+
return {
|
|
2124
|
+
transaction: signedUnbondingTx,
|
|
2125
|
+
fee
|
|
2126
|
+
};
|
|
2127
|
+
}
|
|
2128
|
+
/**
|
|
2129
|
+
* Creates a signed withdrawal transaction on the unbodning output expiry path
|
|
2130
|
+
* that is ready to be sent to the BTC network.
|
|
2131
|
+
* @param stakingInput - The staking inputs.
|
|
2132
|
+
* @param stakingParamsVersion - The params version that was used to create the
|
|
2133
|
+
* delegation in Babylon chain
|
|
2134
|
+
* @param earlyUnbondingTx - The early unbonding transaction.
|
|
2135
|
+
* @param feeRate - The fee rate in satoshis per byte.
|
|
2136
|
+
* @returns The signed withdrawal transaction and its fee.
|
|
2137
|
+
*/
|
|
2138
|
+
async createSignedBtcWithdrawEarlyUnbondedTransaction(stakerBtcInfo, stakingInput, stakingParamsVersion, earlyUnbondingTx, feeRate) {
|
|
2139
|
+
const params = getBabylonParamByVersion(
|
|
2140
|
+
stakingParamsVersion,
|
|
2141
|
+
this.stakingParams
|
|
2142
|
+
);
|
|
2143
|
+
const staking = new Staking(
|
|
2144
|
+
this.network,
|
|
2145
|
+
stakerBtcInfo,
|
|
2146
|
+
params,
|
|
2147
|
+
stakingInput.finalityProviderPkNoCoordHex,
|
|
2148
|
+
stakingInput.stakingTimelock
|
|
2149
|
+
);
|
|
2150
|
+
const { psbt: unbondingPsbt2, fee } = staking.createWithdrawEarlyUnbondedTransaction(
|
|
2151
|
+
earlyUnbondingTx,
|
|
2152
|
+
feeRate
|
|
2153
|
+
);
|
|
2154
|
+
const signedWithdrawalPsbtHex = await this.btcProvider.signPsbt(
|
|
2155
|
+
"withdraw-early-unbonded" /* WITHDRAW_EARLY_UNBONDED */,
|
|
2156
|
+
unbondingPsbt2.toHex()
|
|
2157
|
+
);
|
|
2158
|
+
return {
|
|
2159
|
+
transaction: Psbt3.fromHex(signedWithdrawalPsbtHex).extractTransaction(),
|
|
2160
|
+
fee
|
|
2161
|
+
};
|
|
2162
|
+
}
|
|
2163
|
+
/**
|
|
2164
|
+
* Creates a signed withdrawal transaction on the staking output expiry path
|
|
2165
|
+
* that is ready to be sent to the BTC network.
|
|
2166
|
+
* @param stakerBtcInfo - The staker BTC info which includes the BTC address
|
|
2167
|
+
* and the no-coord public key in hex format.
|
|
2168
|
+
* @param stakingInput - The staking inputs.
|
|
2169
|
+
* @param stakingParamsVersion - The params version that was used to create the
|
|
2170
|
+
* delegation in Babylon chain
|
|
2171
|
+
* @param stakingTx - The staking transaction.
|
|
2172
|
+
* @param feeRate - The fee rate in satoshis per byte.
|
|
2173
|
+
* @returns The signed withdrawal transaction and its fee.
|
|
2174
|
+
*/
|
|
2175
|
+
async createSignedBtcWithdrawStakingExpiredTransaction(stakerBtcInfo, stakingInput, stakingParamsVersion, stakingTx, feeRate) {
|
|
2176
|
+
const params = getBabylonParamByVersion(
|
|
2177
|
+
stakingParamsVersion,
|
|
2178
|
+
this.stakingParams
|
|
2179
|
+
);
|
|
2180
|
+
const staking = new Staking(
|
|
2181
|
+
this.network,
|
|
2182
|
+
stakerBtcInfo,
|
|
2183
|
+
params,
|
|
2184
|
+
stakingInput.finalityProviderPkNoCoordHex,
|
|
2185
|
+
stakingInput.stakingTimelock
|
|
2186
|
+
);
|
|
2187
|
+
const { psbt, fee } = staking.createWithdrawStakingExpiredPsbt(
|
|
2188
|
+
stakingTx,
|
|
2189
|
+
feeRate
|
|
2190
|
+
);
|
|
2191
|
+
const signedWithdrawalPsbtHex = await this.btcProvider.signPsbt(
|
|
2192
|
+
"withdraw-staking-expired" /* WITHDRAW_STAKING_EXPIRED */,
|
|
2193
|
+
psbt.toHex()
|
|
2194
|
+
);
|
|
2195
|
+
return {
|
|
2196
|
+
transaction: Psbt3.fromHex(signedWithdrawalPsbtHex).extractTransaction(),
|
|
2197
|
+
fee
|
|
2198
|
+
};
|
|
2199
|
+
}
|
|
2200
|
+
/**
|
|
2201
|
+
* Creates a signed withdrawal transaction for the expired slashing output that
|
|
2202
|
+
* is ready to be sent to the BTC network.
|
|
2203
|
+
* @param stakerBtcInfo - The staker BTC info which includes the BTC address
|
|
2204
|
+
* and the no-coord public key in hex format.
|
|
2205
|
+
* @param stakingInput - The staking inputs.
|
|
2206
|
+
* @param stakingParamsVersion - The params version that was used to create the
|
|
2207
|
+
* delegation in Babylon chain
|
|
2208
|
+
* @param slashingTx - The slashing transaction.
|
|
2209
|
+
* @param feeRate - The fee rate in satoshis per byte.
|
|
2210
|
+
* @returns The signed withdrawal transaction and its fee.
|
|
2211
|
+
*/
|
|
2212
|
+
async createSignedBtcWithdrawSlashingTransaction(stakerBtcInfo, stakingInput, stakingParamsVersion, slashingTx, feeRate) {
|
|
2213
|
+
const params = getBabylonParamByVersion(
|
|
2214
|
+
stakingParamsVersion,
|
|
2215
|
+
this.stakingParams
|
|
2216
|
+
);
|
|
2217
|
+
const staking = new Staking(
|
|
2218
|
+
this.network,
|
|
2219
|
+
stakerBtcInfo,
|
|
2220
|
+
params,
|
|
2221
|
+
stakingInput.finalityProviderPkNoCoordHex,
|
|
2222
|
+
stakingInput.stakingTimelock
|
|
2223
|
+
);
|
|
2224
|
+
const { psbt, fee } = staking.createWithdrawSlashingPsbt(
|
|
2225
|
+
slashingTx,
|
|
2226
|
+
feeRate
|
|
2227
|
+
);
|
|
2228
|
+
const signedSlashingPsbtHex = await this.btcProvider.signPsbt(
|
|
2229
|
+
"withdraw-slashing" /* WITHDRAW_SLASHING */,
|
|
2230
|
+
psbt.toHex()
|
|
2231
|
+
);
|
|
2232
|
+
return {
|
|
2233
|
+
transaction: Psbt3.fromHex(signedSlashingPsbtHex).extractTransaction(),
|
|
2234
|
+
fee
|
|
2235
|
+
};
|
|
2236
|
+
}
|
|
2237
|
+
/**
|
|
2238
|
+
* Creates a proof of possession for the staker based on ECDSA signature.
|
|
2239
|
+
* @param bech32Address - The staker's bech32 address.
|
|
2240
|
+
* @returns The proof of possession.
|
|
2241
|
+
*/
|
|
2242
|
+
async createProofOfPossession(bech32Address) {
|
|
2243
|
+
if (!this.btcProvider.signMessage) {
|
|
2244
|
+
throw new Error("Sign message function not found");
|
|
2245
|
+
}
|
|
2246
|
+
const bech32AddressHex = uint8ArrayToHex(fromBech322(bech32Address).data);
|
|
2247
|
+
const signedBabylonAddress = await this.btcProvider.signMessage(
|
|
2248
|
+
"proof-of-possession" /* PROOF_OF_POSSESSION */,
|
|
2249
|
+
bech32AddressHex,
|
|
2250
|
+
"ecdsa"
|
|
2251
|
+
);
|
|
2252
|
+
const ecdsaSig = Uint8Array.from(Buffer.from(signedBabylonAddress, "base64"));
|
|
2253
|
+
return {
|
|
2254
|
+
btcSigType: BTCSigType.ECDSA,
|
|
2255
|
+
btcSig: ecdsaSig
|
|
2256
|
+
};
|
|
2257
|
+
}
|
|
2258
|
+
/**
|
|
2259
|
+
* Creates the unbonding, slashing, and unbonding slashing transactions and
|
|
2260
|
+
* PSBTs.
|
|
2261
|
+
* @param stakingInstance - The staking instance.
|
|
2262
|
+
* @param stakingTx - The staking transaction.
|
|
2263
|
+
* @returns The unbonding, slashing, and unbonding slashing transactions and
|
|
2264
|
+
* PSBTs.
|
|
2265
|
+
*/
|
|
2266
|
+
async createDelegationTransactionsAndPsbts(stakingInstance, stakingTx) {
|
|
2267
|
+
const { transaction: unbondingTx } = stakingInstance.createUnbondingTransaction(stakingTx);
|
|
2268
|
+
const { psbt: slashingPsbt } = stakingInstance.createStakingOutputSlashingPsbt(stakingTx);
|
|
2269
|
+
const { psbt: unbondingSlashingPsbt } = stakingInstance.createUnbondingOutputSlashingPsbt(unbondingTx);
|
|
2270
|
+
return {
|
|
2271
|
+
unbondingTx,
|
|
2272
|
+
slashingPsbt,
|
|
2273
|
+
unbondingSlashingPsbt
|
|
2274
|
+
};
|
|
2275
|
+
}
|
|
2276
|
+
/**
|
|
2277
|
+
* Creates a protobuf message for the BTC delegation.
|
|
2278
|
+
* @param stakingInstance - The staking instance.
|
|
2279
|
+
* @param stakingInput - The staking inputs.
|
|
2280
|
+
* @param stakingTx - The staking transaction.
|
|
2281
|
+
* @param bech32Address - The staker's babylon chain bech32 address
|
|
2282
|
+
* @param stakerBtcInfo - The staker's BTC information such as address and
|
|
2283
|
+
* public key
|
|
2284
|
+
* @param params - The staking parameters.
|
|
2285
|
+
* @param inclusionProof - The inclusion proof of the staking transaction.
|
|
2286
|
+
* @returns The protobuf message.
|
|
2287
|
+
*/
|
|
2288
|
+
async createBtcDelegationMsg(stakingInstance, stakingInput, stakingTx, bech32Address, stakerBtcInfo, params, inclusionProof) {
|
|
2289
|
+
const {
|
|
2290
|
+
unbondingTx,
|
|
2291
|
+
slashingPsbt,
|
|
2292
|
+
unbondingSlashingPsbt
|
|
2293
|
+
} = await this.createDelegationTransactionsAndPsbts(
|
|
2294
|
+
stakingInstance,
|
|
2295
|
+
stakingTx
|
|
2296
|
+
);
|
|
2297
|
+
const signedSlashingPsbtHex = await this.btcProvider.signPsbt(
|
|
2298
|
+
"staking-slashing" /* STAKING_SLASHING */,
|
|
2299
|
+
slashingPsbt.toHex()
|
|
2300
|
+
);
|
|
2301
|
+
const signedSlashingTx = Psbt3.fromHex(
|
|
2302
|
+
signedSlashingPsbtHex
|
|
2303
|
+
).extractTransaction();
|
|
2304
|
+
const slashingSig = extractFirstSchnorrSignatureFromTransaction(
|
|
2305
|
+
signedSlashingTx
|
|
2306
|
+
);
|
|
2307
|
+
if (!slashingSig) {
|
|
2308
|
+
throw new Error("No signature found in the staking output slashing PSBT");
|
|
2309
|
+
}
|
|
2310
|
+
const signedUnbondingSlashingPsbtHex = await this.btcProvider.signPsbt(
|
|
2311
|
+
"unbonding-slashing" /* UNBONDING_SLASHING */,
|
|
2312
|
+
unbondingSlashingPsbt.toHex()
|
|
2313
|
+
);
|
|
2314
|
+
const signedUnbondingSlashingTx = Psbt3.fromHex(
|
|
2315
|
+
signedUnbondingSlashingPsbtHex
|
|
2316
|
+
).extractTransaction();
|
|
2317
|
+
const unbondingSignatures = extractFirstSchnorrSignatureFromTransaction(
|
|
2318
|
+
signedUnbondingSlashingTx
|
|
2319
|
+
);
|
|
2320
|
+
if (!unbondingSignatures) {
|
|
2321
|
+
throw new Error("No signature found in the unbonding output slashing PSBT");
|
|
2322
|
+
}
|
|
2323
|
+
const proofOfPossession = await this.createProofOfPossession(bech32Address);
|
|
2324
|
+
const msg = btcstakingtx.MsgCreateBTCDelegation.fromPartial({
|
|
2325
|
+
stakerAddr: bech32Address,
|
|
2326
|
+
pop: proofOfPossession,
|
|
2327
|
+
btcPk: Uint8Array.from(
|
|
2328
|
+
Buffer.from(stakerBtcInfo.publicKeyNoCoordHex, "hex")
|
|
2329
|
+
),
|
|
2330
|
+
fpBtcPkList: [
|
|
2331
|
+
Uint8Array.from(
|
|
2332
|
+
Buffer.from(stakingInput.finalityProviderPkNoCoordHex, "hex")
|
|
2333
|
+
)
|
|
2334
|
+
],
|
|
2335
|
+
stakingTime: stakingInput.stakingTimelock,
|
|
2336
|
+
stakingValue: stakingInput.stakingAmountSat,
|
|
2337
|
+
stakingTx: Uint8Array.from(stakingTx.toBuffer()),
|
|
2338
|
+
slashingTx: Uint8Array.from(
|
|
2339
|
+
Buffer.from(clearTxSignatures(signedSlashingTx).toHex(), "hex")
|
|
2340
|
+
),
|
|
2341
|
+
delegatorSlashingSig: Uint8Array.from(slashingSig),
|
|
2342
|
+
unbondingTime: params.unbondingTime,
|
|
2343
|
+
unbondingTx: Uint8Array.from(unbondingTx.toBuffer()),
|
|
2344
|
+
unbondingValue: stakingInput.stakingAmountSat - params.unbondingFeeSat,
|
|
2345
|
+
unbondingSlashingTx: Uint8Array.from(
|
|
2346
|
+
Buffer.from(
|
|
2347
|
+
clearTxSignatures(signedUnbondingSlashingTx).toHex(),
|
|
2348
|
+
"hex"
|
|
2349
|
+
)
|
|
2350
|
+
),
|
|
2351
|
+
delegatorUnbondingSlashingSig: Uint8Array.from(unbondingSignatures),
|
|
2352
|
+
stakingTxInclusionProof: inclusionProof
|
|
2353
|
+
});
|
|
2354
|
+
return {
|
|
2355
|
+
typeUrl: BABYLON_REGISTRY_TYPE_URLS.MsgCreateBTCDelegation,
|
|
2356
|
+
value: msg
|
|
2357
|
+
};
|
|
2358
|
+
}
|
|
2359
|
+
/**
|
|
2360
|
+
* Gets the inclusion proof for the staking transaction.
|
|
2361
|
+
* See the type `InclusionProof` for more information
|
|
2362
|
+
* @param inclusionProof - The inclusion proof.
|
|
2363
|
+
* @returns The inclusion proof.
|
|
2364
|
+
*/
|
|
2365
|
+
getInclusionProof(inclusionProof) {
|
|
2366
|
+
const {
|
|
2367
|
+
pos,
|
|
2368
|
+
merkle,
|
|
2369
|
+
blockHashHex
|
|
2370
|
+
} = inclusionProof;
|
|
2371
|
+
const proofHex = deriveMerkleProof(merkle);
|
|
2372
|
+
const hash = reverseBuffer(Uint8Array.from(Buffer.from(blockHashHex, "hex")));
|
|
2373
|
+
const inclusionProofKey = btccheckpoint.TransactionKey.fromPartial({
|
|
2374
|
+
index: pos,
|
|
2375
|
+
hash
|
|
2376
|
+
});
|
|
2377
|
+
return btcstaking.InclusionProof.fromPartial({
|
|
2378
|
+
key: inclusionProofKey,
|
|
2379
|
+
proof: Uint8Array.from(Buffer.from(proofHex, "hex"))
|
|
2380
|
+
});
|
|
2381
|
+
}
|
|
2382
|
+
};
|
|
2383
|
+
var extractFirstSchnorrSignatureFromTransaction = (singedTransaction) => {
|
|
2384
|
+
for (const input of singedTransaction.ins) {
|
|
2385
|
+
if (input.witness && input.witness.length > 0) {
|
|
2386
|
+
const schnorrSignature = input.witness[0];
|
|
2387
|
+
if (schnorrSignature.length === 64) {
|
|
2388
|
+
return schnorrSignature;
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
return void 0;
|
|
2393
|
+
};
|
|
2394
|
+
var clearTxSignatures = (tx) => {
|
|
2395
|
+
tx.ins.forEach((input) => {
|
|
2396
|
+
input.script = Buffer.alloc(0);
|
|
2397
|
+
input.witness = [];
|
|
2398
|
+
});
|
|
2399
|
+
return tx;
|
|
2400
|
+
};
|
|
2401
|
+
var deriveMerkleProof = (merkle) => {
|
|
2402
|
+
const proofHex = merkle.reduce((acc, m) => {
|
|
2403
|
+
return acc + Buffer.from(m, "hex").reverse().toString("hex");
|
|
2404
|
+
}, "");
|
|
2405
|
+
return proofHex;
|
|
2406
|
+
};
|
|
2407
|
+
var getUnbondingTxStakerSignature = (unbondingTx) => {
|
|
2408
|
+
try {
|
|
2409
|
+
return unbondingTx.ins[0].witness[0].toString("hex");
|
|
2410
|
+
} catch (error) {
|
|
2411
|
+
throw StakingError.fromUnknown(
|
|
2412
|
+
error,
|
|
2413
|
+
"INVALID_INPUT" /* INVALID_INPUT */,
|
|
2414
|
+
"Failed to get staker signature"
|
|
2415
|
+
);
|
|
2416
|
+
}
|
|
2417
|
+
};
|
|
2418
|
+
export {
|
|
2419
|
+
BabylonBtcStakingManager,
|
|
2420
|
+
BitcoinScriptType,
|
|
2421
|
+
ObservableStaking,
|
|
2422
|
+
ObservableStakingScriptData,
|
|
2423
|
+
SigningStep,
|
|
2424
|
+
Staking,
|
|
2425
|
+
StakingScriptData,
|
|
2426
|
+
buildStakingTransactionOutputs,
|
|
2427
|
+
createCovenantWitness,
|
|
2428
|
+
deriveSlashingOutput,
|
|
2429
|
+
deriveStakingOutputInfo,
|
|
2430
|
+
deriveUnbondingOutputInfo,
|
|
2431
|
+
findInputUTXO,
|
|
2432
|
+
findMatchingTxOutputIndex,
|
|
2433
|
+
getBabylonParamByBtcHeight,
|
|
2434
|
+
getBabylonParamByVersion,
|
|
2435
|
+
getPsbtInputFields,
|
|
2436
|
+
getPublicKeyNoCoord,
|
|
2437
|
+
getScriptType,
|
|
2438
|
+
getUnbondingTxStakerSignature,
|
|
2439
|
+
initBTCCurve,
|
|
2440
|
+
isTaproot,
|
|
2441
|
+
isValidBitcoinAddress,
|
|
2442
|
+
isValidNoCoordPublicKey,
|
|
2443
|
+
slashEarlyUnbondedTransaction,
|
|
2444
|
+
slashTimelockUnbondedTransaction,
|
|
2445
|
+
stakingTransaction,
|
|
2446
|
+
toBuffers,
|
|
2447
|
+
transactionIdToHash,
|
|
2448
|
+
unbondingTransaction,
|
|
2449
|
+
validateParams,
|
|
2450
|
+
validateStakingTimelock,
|
|
2451
|
+
validateStakingTxInputData,
|
|
2452
|
+
withdrawEarlyUnbondedTransaction,
|
|
2453
|
+
withdrawSlashingTransaction,
|
|
2454
|
+
withdrawTimelockUnbondedTransaction
|
|
2455
|
+
};
|