@atomicfinance/bitcoin-ddk-provider 4.1.0
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/.turbo/turbo-build.log +5 -0
- package/.turbo/turbo-lint$colon$fix.log +42 -0
- package/.turbo/turbo-lint.log +14 -0
- package/CHANGELOG.md +13 -0
- package/LICENSE +674 -0
- package/README.md +142 -0
- package/dist/BitcoinDdkProvider.d.ts +297 -0
- package/dist/BitcoinDdkProvider.js +2557 -0
- package/dist/BitcoinDdkProvider.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/utils/Utils.d.ts +30 -0
- package/dist/utils/Utils.js +82 -0
- package/dist/utils/Utils.js.map +1 -0
- package/lib/BitcoinDdkProvider.ts +4294 -0
- package/lib/index.ts +2 -0
- package/lib/utils/Utils.ts +118 -0
- package/package.json +35 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,2557 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
const provider_1 = __importDefault(require("@atomicfinance/provider"));
|
|
40
|
+
const types_1 = require("@atomicfinance/types");
|
|
41
|
+
const utils_1 = require("@atomicfinance/utils");
|
|
42
|
+
const bitcoin_1 = require("@node-dlc/bitcoin");
|
|
43
|
+
const bufio_1 = require("@node-dlc/bufio");
|
|
44
|
+
const core_1 = require("@node-dlc/core");
|
|
45
|
+
const core_2 = require("@node-dlc/core");
|
|
46
|
+
const crypto_1 = require("@node-dlc/crypto");
|
|
47
|
+
const messaging_1 = require("@node-dlc/messaging");
|
|
48
|
+
const assert_1 = __importDefault(require("assert"));
|
|
49
|
+
const bignumber_js_1 = __importDefault(require("bignumber.js"));
|
|
50
|
+
const bitcoin_networks_1 = require("bitcoin-networks");
|
|
51
|
+
const bitcoinjs_lib_1 = require("bitcoinjs-lib");
|
|
52
|
+
const crypto_2 = __importDefault(require("crypto"));
|
|
53
|
+
const ecpair_1 = require("ecpair");
|
|
54
|
+
const ecc = __importStar(require("tiny-secp256k1"));
|
|
55
|
+
const Utils_1 = require("./utils/Utils");
|
|
56
|
+
const ECPair = (0, ecpair_1.ECPairFactory)(ecc);
|
|
57
|
+
class BitcoinDdkProvider extends provider_1.default {
|
|
58
|
+
constructor(network, ddkLib) {
|
|
59
|
+
super();
|
|
60
|
+
this._network = network;
|
|
61
|
+
this._ddk = ddkLib;
|
|
62
|
+
}
|
|
63
|
+
async DdkLoaded() {
|
|
64
|
+
while (!this._ddk) {
|
|
65
|
+
await (0, utils_1.sleep)(10);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Find private key for DLC funding pubkey by deriving wallet addresses
|
|
70
|
+
*/
|
|
71
|
+
async findDlcFundingPrivateKey(localFundPubkey, remoteFundPubkey) {
|
|
72
|
+
const targetPubkeys = [localFundPubkey, remoteFundPubkey];
|
|
73
|
+
// First check existing wallet addresses
|
|
74
|
+
const addresses = await this.getMethod('getAddresses')();
|
|
75
|
+
for (const addressInfo of addresses) {
|
|
76
|
+
if (addressInfo.derivationPath) {
|
|
77
|
+
try {
|
|
78
|
+
const keyPair = await this.getMethod('keyPair')(addressInfo.derivationPath);
|
|
79
|
+
const pubkey = Buffer.from(keyPair.publicKey);
|
|
80
|
+
const pubkeyHex = pubkey.toString('hex');
|
|
81
|
+
if (targetPubkeys.includes(pubkeyHex)) {
|
|
82
|
+
return Buffer.from(keyPair.privateKey).toString('hex');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// If not found in existing addresses, do comprehensive search
|
|
91
|
+
// For DLC splicing, funding pubkeys can be at much higher derivation paths
|
|
92
|
+
console.log('Searching extensively for DLC funding private key...');
|
|
93
|
+
for (const isChange of [false, true]) {
|
|
94
|
+
for (let i = 0; i < 1000; i++) {
|
|
95
|
+
// Search up to 1000 addresses for DLC keys
|
|
96
|
+
try {
|
|
97
|
+
const address = await this.client.wallet.getAddresses(i, 1, isChange);
|
|
98
|
+
if (address && address.length > 0) {
|
|
99
|
+
const addressInfo = address[0];
|
|
100
|
+
if (addressInfo.derivationPath) {
|
|
101
|
+
const keyPair = await this.getMethod('keyPair')(addressInfo.derivationPath);
|
|
102
|
+
const pubkey = Buffer.from(keyPair.publicKey);
|
|
103
|
+
const pubkeyHex = pubkey.toString('hex');
|
|
104
|
+
if (targetPubkeys.includes(pubkeyHex)) {
|
|
105
|
+
console.log(`Found DLC funding key at derivation path: ${addressInfo.derivationPath}`);
|
|
106
|
+
return Buffer.from(keyPair.privateKey).toString('hex');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
throw new Error(`Could not find private key for DLC funding pubkeys: local=${localFundPubkey}, remote=${remoteFundPubkey}`);
|
|
117
|
+
}
|
|
118
|
+
async GetPrivKeysForInputs(inputs) {
|
|
119
|
+
const privKeys = [];
|
|
120
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
121
|
+
const input = inputs[i];
|
|
122
|
+
if (input.isDlcInput()) {
|
|
123
|
+
// Handle DLC input - use the dedicated method to find the funding private key
|
|
124
|
+
const dlcInput = input.dlcInput;
|
|
125
|
+
const foundPrivKey = await this.findDlcFundingPrivateKey(dlcInput.localFundPubkey, dlcInput.remoteFundPubkey);
|
|
126
|
+
privKeys.push(foundPrivKey);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
// Handle regular input
|
|
130
|
+
let derivationPath = input.derivationPath;
|
|
131
|
+
if (!derivationPath) {
|
|
132
|
+
try {
|
|
133
|
+
derivationPath = (await this.getMethod('getWalletAddress')(input.address)).derivationPath;
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
throw new Error(`Unable to find address ${input.address} in wallet. ` +
|
|
137
|
+
`This may happen when using derivation paths outside the normal range. ` +
|
|
138
|
+
`Error: ${error.message}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const keyPair = await this.getMethod('keyPair')(derivationPath);
|
|
142
|
+
const privKey = Buffer.from(keyPair.__D).toString('hex');
|
|
143
|
+
privKeys.push(privKey);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return privKeys;
|
|
147
|
+
}
|
|
148
|
+
async GetCfdNetwork() {
|
|
149
|
+
const network = await this.getConnectedNetwork();
|
|
150
|
+
switch (network.name) {
|
|
151
|
+
case 'bitcoin_testnet':
|
|
152
|
+
return 'testnet';
|
|
153
|
+
case 'bitcoin_regtest':
|
|
154
|
+
return 'regtest';
|
|
155
|
+
default:
|
|
156
|
+
return 'bitcoin';
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Get inputs for amount with explicit supplementation control
|
|
161
|
+
*/
|
|
162
|
+
async GetInputsForAmountWithMode(amounts, feeRatePerVb, fixedInputs = [], supplementation = types_1.InputSupplementationMode.Required) {
|
|
163
|
+
if (amounts.length === 0)
|
|
164
|
+
return [];
|
|
165
|
+
// For "none" mode, use exactly the provided inputs
|
|
166
|
+
if (supplementation === types_1.InputSupplementationMode.None) {
|
|
167
|
+
return fixedInputs;
|
|
168
|
+
}
|
|
169
|
+
// For "required" and "optional" modes, attempt supplementation
|
|
170
|
+
const fixedUtxos = fixedInputs.map((input) => input.toUtxo());
|
|
171
|
+
try {
|
|
172
|
+
const inputsForAmount = await this.getMethod('getInputsForDualFunding')(amounts, feeRatePerVb, fixedUtxos);
|
|
173
|
+
// Convert UTXO objects to Input class instances
|
|
174
|
+
return inputsForAmount.inputs.map((utxo) => types_1.Input.fromUTXO(utxo));
|
|
175
|
+
}
|
|
176
|
+
catch (e) {
|
|
177
|
+
const errorMessage = e instanceof Error ? e.message : 'Unknown error';
|
|
178
|
+
if (supplementation === types_1.InputSupplementationMode.Required) {
|
|
179
|
+
throw Error(`Not enough balance GetInputsForAmountWithMode. Error: ${errorMessage}`);
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
// Optional mode: fallback to provided inputs
|
|
183
|
+
return fixedInputs;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async GetInputsForAmount(amounts, feeRatePerVb, fixedInputs = []) {
|
|
188
|
+
if (amounts.length === 0)
|
|
189
|
+
return [];
|
|
190
|
+
const fixedUtxos = fixedInputs.map((input) => input.toUtxo());
|
|
191
|
+
let inputs;
|
|
192
|
+
try {
|
|
193
|
+
const inputsForAmount = await this.getMethod('getInputsForDualFunding')(amounts, feeRatePerVb, fixedUtxos);
|
|
194
|
+
// Convert UTXO objects to Input class instances
|
|
195
|
+
inputs = inputsForAmount.inputs.map((utxo) => types_1.Input.fromUTXO(utxo));
|
|
196
|
+
}
|
|
197
|
+
catch (e) {
|
|
198
|
+
const errorMessage = e instanceof Error ? e.message : 'Unknown error';
|
|
199
|
+
if (fixedInputs.length === 0) {
|
|
200
|
+
throw Error(`Not enough balance getInputsForAmount. Error: ${errorMessage}`);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
inputs = fixedInputs;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return inputs;
|
|
207
|
+
}
|
|
208
|
+
async Initialize(collateral, feeRatePerVb, fixedInputs, inputSupplementationMode = types_1.InputSupplementationMode.Required) {
|
|
209
|
+
const network = await this.getConnectedNetwork();
|
|
210
|
+
const payoutAddress = await this.client.wallet.getUnusedAddress(false);
|
|
211
|
+
const payoutSPK = bitcoinjs_lib_1.address.toOutputScript(payoutAddress.address, network);
|
|
212
|
+
const changeAddress = await this.client.wallet.getUnusedAddress(true);
|
|
213
|
+
const changeSPK = bitcoinjs_lib_1.address.toOutputScript(changeAddress.address, network);
|
|
214
|
+
const fundingAddress = await this.client.wallet.getUnusedAddress(false);
|
|
215
|
+
const fundingPubKey = Buffer.from(fundingAddress.publicKey, 'hex');
|
|
216
|
+
if (fundingAddress.address === payoutAddress.address)
|
|
217
|
+
throw Error('Address reuse');
|
|
218
|
+
const inputs = await this.GetInputsForAmountWithMode([collateral], feeRatePerVb, fixedInputs, inputSupplementationMode);
|
|
219
|
+
const fundingInputs = await Promise.all(inputs.map(async (input) => {
|
|
220
|
+
return this.inputToFundingInput(input);
|
|
221
|
+
}));
|
|
222
|
+
const payoutSerialId = (0, Utils_1.generateSerialId)();
|
|
223
|
+
const changeSerialId = (0, Utils_1.generateSerialId)();
|
|
224
|
+
return {
|
|
225
|
+
fundingPubKey,
|
|
226
|
+
payoutSPK,
|
|
227
|
+
payoutSerialId,
|
|
228
|
+
fundingInputs,
|
|
229
|
+
changeSPK,
|
|
230
|
+
changeSerialId,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* TODO: Add GetPayoutFromOutcomes
|
|
235
|
+
*
|
|
236
|
+
* private GetPayoutsFromOutcomes(
|
|
237
|
+
* contractDescriptor: ContractDescriptorV0,
|
|
238
|
+
* totalCollateral: bigint,
|
|
239
|
+
* ): PayoutRequest[] {}
|
|
240
|
+
*/
|
|
241
|
+
GetPayoutsFromPayoutFunction(dlcOffer, contractDescriptor, oracleInfo, totalCollateral) {
|
|
242
|
+
const payoutFunction = contractDescriptor.payoutFunction;
|
|
243
|
+
if (payoutFunction.payoutFunctionPieces.length === 0)
|
|
244
|
+
throw Error('PayoutFunction must have at least once PayoutCurvePiece');
|
|
245
|
+
if (payoutFunction.payoutFunctionPieces.length > 1)
|
|
246
|
+
throw Error('More than one PayoutCurvePiece not supported');
|
|
247
|
+
const payoutCurvePiece = payoutFunction.payoutFunctionPieces[0]
|
|
248
|
+
.payoutCurvePiece;
|
|
249
|
+
if (payoutCurvePiece.payoutCurvePieceType !== messaging_1.PayoutCurvePieceType.Hyperbola)
|
|
250
|
+
throw Error('Must be HyperbolaPayoutCurvePiece');
|
|
251
|
+
if (!payoutCurvePiece.b.eq(messaging_1.F64.ZERO) || !payoutCurvePiece.c.eq(messaging_1.F64.ZERO))
|
|
252
|
+
throw Error('b and c HyperbolaPayoutCurvePiece values must be 0');
|
|
253
|
+
// Cast to SingleOracleInfo to access announcement property
|
|
254
|
+
const singleOracleInfo = oracleInfo;
|
|
255
|
+
const eventDescriptor = singleOracleInfo.announcement.oracleEvent
|
|
256
|
+
.eventDescriptor;
|
|
257
|
+
if (eventDescriptor.type !== messaging_1.MessageType.DigitDecompositionEventDescriptor)
|
|
258
|
+
throw Error('Only DigitDecomposition Oracle Events supported');
|
|
259
|
+
const roundingIntervals = contractDescriptor.roundingIntervals;
|
|
260
|
+
const cetPayouts = core_2.HyperbolaPayoutCurve.computePayouts(payoutFunction, totalCollateral, roundingIntervals);
|
|
261
|
+
const payoutGroups = [];
|
|
262
|
+
cetPayouts.forEach((p) => {
|
|
263
|
+
payoutGroups.push({
|
|
264
|
+
payout: p.payout,
|
|
265
|
+
groups: (0, core_2.groupByIgnoringDigits)(p.indexFrom, p.indexTo, eventDescriptor.base, contractDescriptor.numDigits),
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
const rValuesMessagesList = this.GenerateMessages(singleOracleInfo);
|
|
269
|
+
const { payouts, messagesList } = (0, Utils_1.outputsToPayouts)(payoutGroups, rValuesMessagesList, dlcOffer.offerCollateral, dlcOffer.contractInfo.totalCollateral - dlcOffer.offerCollateral, true);
|
|
270
|
+
return { payouts, payoutGroups, messagesList };
|
|
271
|
+
}
|
|
272
|
+
GetPayoutsFromPolynomialPayoutFunction(dlcOffer, contractDescriptor, oracleInfo, totalCollateral) {
|
|
273
|
+
const payoutFunction = contractDescriptor.payoutFunction;
|
|
274
|
+
if (payoutFunction.payoutFunctionPieces.length === 0)
|
|
275
|
+
throw Error('PayoutFunction must have at least once PayoutCurvePiece');
|
|
276
|
+
for (const piece of payoutFunction.payoutFunctionPieces) {
|
|
277
|
+
if (piece.payoutCurvePiece.type !== messaging_1.MessageType.PolynomialPayoutCurvePiece)
|
|
278
|
+
throw Error('Must be PolynomialPayoutCurvePiece');
|
|
279
|
+
}
|
|
280
|
+
const eventDescriptor = oracleInfo.announcement.oracleEvent
|
|
281
|
+
.eventDescriptor;
|
|
282
|
+
if (eventDescriptor.type !== messaging_1.MessageType.DigitDecompositionEventDescriptor)
|
|
283
|
+
throw Error('Only DigitDecomposition Oracle Events supported');
|
|
284
|
+
const roundingIntervals = contractDescriptor.roundingIntervals;
|
|
285
|
+
const cetPayouts = core_2.PolynomialPayoutCurve.computePayouts(payoutFunction, totalCollateral, roundingIntervals);
|
|
286
|
+
const payoutGroups = [];
|
|
287
|
+
cetPayouts.forEach((p) => {
|
|
288
|
+
payoutGroups.push({
|
|
289
|
+
payout: p.payout,
|
|
290
|
+
groups: (0, core_2.groupByIgnoringDigits)(p.indexFrom, p.indexTo, eventDescriptor.base, contractDescriptor.numDigits),
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
const rValuesMessagesList = this.GenerateMessages(oracleInfo);
|
|
294
|
+
const { payouts, messagesList } = (0, Utils_1.outputsToPayouts)(payoutGroups, rValuesMessagesList, dlcOffer.offerCollateral, dlcOffer.contractInfo.totalCollateral - dlcOffer.offerCollateral, true);
|
|
295
|
+
return { payouts, payoutGroups, messagesList };
|
|
296
|
+
}
|
|
297
|
+
GetPayouts(dlcOffer) {
|
|
298
|
+
const contractInfo = dlcOffer.contractInfo;
|
|
299
|
+
const totalCollateral = contractInfo.totalCollateral;
|
|
300
|
+
const contractOraclePairs = this.GetContractOraclePairs(contractInfo);
|
|
301
|
+
const payoutResponses = contractOraclePairs.map(({ contractDescriptor, oracleInfo }) => this.GetPayoutsFromContractDescriptor(dlcOffer, contractDescriptor, oracleInfo, totalCollateral));
|
|
302
|
+
return payoutResponses;
|
|
303
|
+
}
|
|
304
|
+
FlattenPayouts(payoutResponses) {
|
|
305
|
+
return payoutResponses.reduce((acc, { payouts, payoutGroups, messagesList }) => {
|
|
306
|
+
return {
|
|
307
|
+
payouts: acc.payouts.concat(payouts),
|
|
308
|
+
payoutGroups: acc.payoutGroups.concat(payoutGroups),
|
|
309
|
+
messagesList: acc.messagesList.concat(messagesList),
|
|
310
|
+
};
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
GetIndicesFromPayouts(payoutResponses) {
|
|
314
|
+
return payoutResponses.reduce((prev, acc) => {
|
|
315
|
+
return prev.concat({
|
|
316
|
+
startingMessagesIndex: prev[prev.length - 1].startingMessagesIndex +
|
|
317
|
+
acc.messagesList.length,
|
|
318
|
+
startingPayoutGroupsIndex: prev[prev.length - 1].startingPayoutGroupsIndex +
|
|
319
|
+
acc.payoutGroups.length,
|
|
320
|
+
});
|
|
321
|
+
}, [{ startingMessagesIndex: 0, startingPayoutGroupsIndex: 0 }]);
|
|
322
|
+
}
|
|
323
|
+
GetPayoutsFromEnumeratedDescriptor(dlcOffer, contractDescriptor, oracleInfo, totalCollateral) {
|
|
324
|
+
const payoutGroups = [];
|
|
325
|
+
const rValuesMessagesList = this.GenerateMessages(oracleInfo);
|
|
326
|
+
// For enumerated descriptors, each outcome creates one payout
|
|
327
|
+
// Each outcome maps to one index in the oracle's possible outcomes
|
|
328
|
+
contractDescriptor.outcomes.forEach((outcome, index) => {
|
|
329
|
+
payoutGroups.push({
|
|
330
|
+
payout: outcome.localPayout,
|
|
331
|
+
groups: [[index]], // Simple index-based grouping for enum outcomes
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
const { payouts, messagesList } = (0, Utils_1.outputsToPayouts)(payoutGroups, rValuesMessagesList, dlcOffer.offerCollateral, totalCollateral - dlcOffer.offerCollateral, true);
|
|
335
|
+
return { payouts, payoutGroups, messagesList };
|
|
336
|
+
}
|
|
337
|
+
GetPayoutsFromContractDescriptor(dlcOffer, contractDescriptor, oracleInfo, totalCollateral) {
|
|
338
|
+
switch (contractDescriptor.contractDescriptorType) {
|
|
339
|
+
case messaging_1.ContractDescriptorType.Enumerated: {
|
|
340
|
+
return this.GetPayoutsFromEnumeratedDescriptor(dlcOffer, contractDescriptor, oracleInfo, totalCollateral);
|
|
341
|
+
}
|
|
342
|
+
case messaging_1.ContractDescriptorType.NumericOutcome: {
|
|
343
|
+
const numericalDescriptor = contractDescriptor;
|
|
344
|
+
const payoutFunction = numericalDescriptor.payoutFunction;
|
|
345
|
+
// TODO: add a better check for this
|
|
346
|
+
const payoutCurvePiece = payoutFunction.payoutFunctionPieces[0].payoutCurvePiece;
|
|
347
|
+
switch (payoutCurvePiece.payoutCurvePieceType) {
|
|
348
|
+
case messaging_1.PayoutCurvePieceType.Hyperbola:
|
|
349
|
+
return this.GetPayoutsFromPayoutFunction(dlcOffer, numericalDescriptor, oracleInfo, totalCollateral);
|
|
350
|
+
case messaging_1.PayoutCurvePieceType.Polynomial:
|
|
351
|
+
return this.GetPayoutsFromPolynomialPayoutFunction(dlcOffer, numericalDescriptor, oracleInfo, totalCollateral);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Converts a @node-dlc/bitcoin Tx to a DDK Transaction
|
|
358
|
+
* @param tx The @node-dlc/bitcoin transaction
|
|
359
|
+
* @returns DDK Transaction object
|
|
360
|
+
*/
|
|
361
|
+
convertTxToDdkTransaction(tx) {
|
|
362
|
+
return {
|
|
363
|
+
version: tx.version,
|
|
364
|
+
lockTime: tx.locktime.value,
|
|
365
|
+
inputs: tx.inputs.map((input) => ({
|
|
366
|
+
txid: input.outpoint.txid.toString(),
|
|
367
|
+
vout: input.outpoint.outputIndex,
|
|
368
|
+
scriptSig: Buffer.from(''),
|
|
369
|
+
sequence: input.sequence.value,
|
|
370
|
+
witness: input.witness.map((witness) => witness.serialize()),
|
|
371
|
+
})),
|
|
372
|
+
outputs: tx.outputs.map((output) => ({
|
|
373
|
+
value: BigInt(output.value.sats),
|
|
374
|
+
scriptPubkey: output.scriptPubKey.serialize(),
|
|
375
|
+
})),
|
|
376
|
+
rawBytes: tx.serialize(),
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
async createDlcTxs(dlcOffer, dlcAccept) {
|
|
380
|
+
const localFundPubkey = dlcOffer.fundingPubkey.toString('hex');
|
|
381
|
+
const remoteFundPubkey = dlcAccept.fundingPubkey.toString('hex');
|
|
382
|
+
const localFinalScriptPubkey = dlcOffer.payoutSpk.toString('hex');
|
|
383
|
+
const remoteFinalScriptPubkey = dlcAccept.payoutSpk.toString('hex');
|
|
384
|
+
const localChangeScriptPubkey = dlcOffer.changeSpk.toString('hex');
|
|
385
|
+
const remoteChangeScriptPubkey = dlcAccept.changeSpk.toString('hex');
|
|
386
|
+
// Separate regular inputs from DLC inputs (only from offeror side)
|
|
387
|
+
const localRegularInputs = [];
|
|
388
|
+
const localDlcInputs = [];
|
|
389
|
+
for (const fundingInput of dlcOffer.fundingInputs) {
|
|
390
|
+
if (fundingInput.dlcInput) {
|
|
391
|
+
// This is a DLC input for splicing
|
|
392
|
+
// The pubkeys should be from the original DLC to correctly spend its funding output
|
|
393
|
+
localDlcInputs.push({
|
|
394
|
+
fundTx: this.convertTxToDdkTransaction(fundingInput.prevTx),
|
|
395
|
+
fundVout: fundingInput.prevTxVout,
|
|
396
|
+
localFundPubkey: fundingInput.dlcInput.localFundPubkey,
|
|
397
|
+
remoteFundPubkey: fundingInput.dlcInput.remoteFundPubkey,
|
|
398
|
+
fundAmount: fundingInput.prevTx.outputs[fundingInput.prevTxVout].value.sats,
|
|
399
|
+
maxWitnessLen: fundingInput.maxWitnessLen,
|
|
400
|
+
inputSerialId: fundingInput.inputSerialId,
|
|
401
|
+
contractId: fundingInput.dlcInput.contractId,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
// Regular input
|
|
406
|
+
const input = await this.fundingInputToInput(fundingInput, false);
|
|
407
|
+
localRegularInputs.push(input.toUtxo());
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
// Process remote inputs (no DLC inputs from acceptor side)
|
|
411
|
+
const remoteInputs = await Promise.all(dlcAccept.fundingInputs.map(async (fundingInput) => {
|
|
412
|
+
const input = await this.fundingInputToInput(fundingInput, false);
|
|
413
|
+
return input.toUtxo();
|
|
414
|
+
}));
|
|
415
|
+
// Calculate input amounts
|
|
416
|
+
const localInputAmount = localRegularInputs.reduce((prev, cur) => prev + cur.amount.GetSatoshiAmount(), 0);
|
|
417
|
+
const remoteInputAmount = remoteInputs.reduce((prev, cur) => prev + cur.amount.GetSatoshiAmount(), 0);
|
|
418
|
+
let payouts = [];
|
|
419
|
+
let messagesList = [];
|
|
420
|
+
if (dlcOffer.contractInfo.type === messaging_1.MessageType.SingleContractInfo &&
|
|
421
|
+
dlcOffer.contractInfo.contractDescriptor.type ===
|
|
422
|
+
messaging_1.ContractDescriptorType.Enumerated) {
|
|
423
|
+
for (const outcome of dlcOffer.contractInfo
|
|
424
|
+
.contractDescriptor.outcomes) {
|
|
425
|
+
payouts.push({
|
|
426
|
+
local: outcome.localPayout,
|
|
427
|
+
remote: dlcOffer.offerCollateral +
|
|
428
|
+
dlcAccept.acceptCollateral -
|
|
429
|
+
outcome.localPayout,
|
|
430
|
+
});
|
|
431
|
+
messagesList.push({ messages: [outcome.outcome] });
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
const payoutResponses = this.GetPayouts(dlcOffer);
|
|
436
|
+
const { payouts: tempPayouts, messagesList: tempMessagesList } = this.FlattenPayouts(payoutResponses);
|
|
437
|
+
payouts = tempPayouts;
|
|
438
|
+
messagesList = tempMessagesList;
|
|
439
|
+
}
|
|
440
|
+
const outcomes = payouts.map((payout) => ({
|
|
441
|
+
offer: BigInt(payout.local),
|
|
442
|
+
accept: BigInt(payout.remote),
|
|
443
|
+
}));
|
|
444
|
+
const localParams = {
|
|
445
|
+
fundPubkey: Buffer.from(localFundPubkey, 'hex'),
|
|
446
|
+
changeScriptPubkey: Buffer.from(localChangeScriptPubkey, 'hex'),
|
|
447
|
+
changeSerialId: BigInt(dlcOffer.changeSerialId),
|
|
448
|
+
payoutScriptPubkey: Buffer.from(localFinalScriptPubkey, 'hex'),
|
|
449
|
+
payoutSerialId: BigInt(dlcOffer.payoutSerialId),
|
|
450
|
+
inputs: localRegularInputs.map((input) => input.toTxInputInfo()),
|
|
451
|
+
inputAmount: BigInt(localInputAmount),
|
|
452
|
+
collateral: BigInt(dlcOffer.offerCollateral),
|
|
453
|
+
dlcInputs: localDlcInputs,
|
|
454
|
+
};
|
|
455
|
+
const remoteParams = {
|
|
456
|
+
fundPubkey: Buffer.from(remoteFundPubkey, 'hex'),
|
|
457
|
+
changeScriptPubkey: Buffer.from(remoteChangeScriptPubkey, 'hex'),
|
|
458
|
+
changeSerialId: BigInt(dlcAccept.changeSerialId),
|
|
459
|
+
payoutScriptPubkey: Buffer.from(remoteFinalScriptPubkey, 'hex'),
|
|
460
|
+
payoutSerialId: BigInt(dlcAccept.payoutSerialId),
|
|
461
|
+
inputs: remoteInputs.map((input) => input.toTxInputInfo()),
|
|
462
|
+
inputAmount: BigInt(remoteInputAmount),
|
|
463
|
+
collateral: BigInt(dlcAccept.acceptCollateral),
|
|
464
|
+
dlcInputs: [],
|
|
465
|
+
};
|
|
466
|
+
// Determine whether to use regular or spliced DLC transactions
|
|
467
|
+
const hasDlcInputs = localDlcInputs.length > 0;
|
|
468
|
+
let dlcTxs;
|
|
469
|
+
if (hasDlcInputs) {
|
|
470
|
+
// Use spliced DLC transactions when DLC inputs are present
|
|
471
|
+
dlcTxs = await this._ddk.createSplicedDlcTransactions(outcomes, localParams, remoteParams, dlcOffer.refundLocktime, BigInt(dlcOffer.feeRatePerVb), 0, dlcOffer.cetLocktime, dlcOffer.fundOutputSerialId);
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
// Use regular DLC transactions when no DLC inputs
|
|
475
|
+
dlcTxs = this._ddk.createDlcTransactions(outcomes, localParams, remoteParams, dlcOffer.refundLocktime, BigInt(dlcOffer.feeRatePerVb), 0, dlcOffer.cetLocktime, BigInt(dlcOffer.fundOutputSerialId));
|
|
476
|
+
}
|
|
477
|
+
const dlcTransactions = new messaging_1.DlcTransactions();
|
|
478
|
+
dlcTransactions.fundTx = bitcoin_1.Tx.decode(bufio_1.StreamReader.fromBuffer(dlcTxs.fund.rawBytes));
|
|
479
|
+
// Build serial IDs based on actual outputs in the transaction
|
|
480
|
+
const actualOutputs = dlcTransactions.fundTx.outputs;
|
|
481
|
+
const serialIds = [];
|
|
482
|
+
// Always include the funding output serial ID
|
|
483
|
+
serialIds.push(BigInt(dlcOffer.fundOutputSerialId));
|
|
484
|
+
// Only include change serial IDs if there are actually change outputs
|
|
485
|
+
// For exact amount DLCs with no change, there will be only 1 output (the funding output)
|
|
486
|
+
if (actualOutputs.length > 1) {
|
|
487
|
+
// Multiple outputs means there are change outputs
|
|
488
|
+
if (dlcOffer.offerCollateral > 0n) {
|
|
489
|
+
serialIds.push(BigInt(dlcOffer.changeSerialId));
|
|
490
|
+
}
|
|
491
|
+
if (dlcAccept.acceptCollateral > 0n) {
|
|
492
|
+
serialIds.push(BigInt(dlcAccept.changeSerialId));
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
dlcTransactions.fundTxVout = serialIds
|
|
496
|
+
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
|
|
497
|
+
.findIndex((i) => BigInt(i) === BigInt(dlcOffer.fundOutputSerialId));
|
|
498
|
+
// Validate that the calculated fundTxVout is valid
|
|
499
|
+
if (dlcTransactions.fundTxVout < 0 ||
|
|
500
|
+
dlcTransactions.fundTxVout >= dlcTransactions.fundTx.outputs.length) {
|
|
501
|
+
throw new Error(`Invalid fundTxVout calculation: calculated=${dlcTransactions.fundTxVout}, ` +
|
|
502
|
+
`fundTx.outputs.length=${dlcTransactions.fundTx.outputs.length}, ` +
|
|
503
|
+
`fundOutputSerialId=${dlcOffer.fundOutputSerialId}, ` +
|
|
504
|
+
`serialIds=[${serialIds.join(', ')}], ` +
|
|
505
|
+
`offerCollateral=${dlcOffer.offerCollateral}, ` +
|
|
506
|
+
`acceptCollateral=${dlcAccept.acceptCollateral}`);
|
|
507
|
+
}
|
|
508
|
+
dlcTransactions.cets = dlcTxs.cets.map((cetTx) => bitcoin_1.Tx.decode(bufio_1.StreamReader.fromBuffer(cetTx.rawBytes)));
|
|
509
|
+
dlcTransactions.refundTx = bitcoin_1.Tx.decode(bufio_1.StreamReader.fromBuffer(dlcTxs.refund.rawBytes));
|
|
510
|
+
return { dlcTransactions, messagesList };
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Computes DLC-spec compliant tagged attestation message digest
|
|
514
|
+
* This matches what the oracle should sign according to the DLC specification
|
|
515
|
+
*/
|
|
516
|
+
computeTaggedAttestationMessage(outcome) {
|
|
517
|
+
// DLC spec: H(H("DLC/oracle/attestation/v0") || H("DLC/oracle/attestation/v0") || H(outcome))
|
|
518
|
+
const tag = Buffer.from('DLC/oracle/attestation/v0', 'utf8');
|
|
519
|
+
const tagHash = (0, crypto_1.sha256)(tag);
|
|
520
|
+
const outcomeBuffer = Buffer.from(outcome, 'utf8');
|
|
521
|
+
// Compute H(tagHash || tagHash || outcomeHash)
|
|
522
|
+
const message = (0, crypto_1.sha256)(Buffer.concat([tagHash, tagHash, outcomeBuffer]));
|
|
523
|
+
return message.toString('hex');
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Convert message lists to the format expected by DDK FFI
|
|
527
|
+
* DDK expects 32-byte message digests (tagged attestation messages)
|
|
528
|
+
*/
|
|
529
|
+
convertMessagesForDdk(tempMessagesList) {
|
|
530
|
+
return tempMessagesList.map((message) => [
|
|
531
|
+
message.messages.map((m) =>
|
|
532
|
+
// Convert outcome string to tagged attestation message (32-byte hash)
|
|
533
|
+
Buffer.from(this.computeTaggedAttestationMessage(m), 'hex')),
|
|
534
|
+
]);
|
|
535
|
+
}
|
|
536
|
+
GenerateEnumMessages(oracleEvent) {
|
|
537
|
+
const eventDescriptor = oracleEvent.eventDescriptor;
|
|
538
|
+
// For enum events, each oracle has one nonce and can attest to one of the possible outcomes
|
|
539
|
+
const messagesList = [];
|
|
540
|
+
// Pass raw outcome strings to dlcdevkit - it will handle the tagged hashing internally
|
|
541
|
+
// dlcdevkit expects raw strings and calls tagged_attestation_msg() internally
|
|
542
|
+
const messages = eventDescriptor.outcomes;
|
|
543
|
+
messagesList.push({ messages });
|
|
544
|
+
return messagesList;
|
|
545
|
+
}
|
|
546
|
+
convertToJsonSerializable(obj) {
|
|
547
|
+
if (obj === null || obj === undefined) {
|
|
548
|
+
return obj;
|
|
549
|
+
}
|
|
550
|
+
if (typeof obj === 'bigint') {
|
|
551
|
+
return Number(obj);
|
|
552
|
+
}
|
|
553
|
+
if (Buffer.isBuffer(obj)) {
|
|
554
|
+
return obj.toString('hex');
|
|
555
|
+
}
|
|
556
|
+
if (Array.isArray(obj)) {
|
|
557
|
+
return obj.map((item) => this.convertToJsonSerializable(item));
|
|
558
|
+
}
|
|
559
|
+
if (typeof obj === 'object') {
|
|
560
|
+
const result = {};
|
|
561
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
562
|
+
result[key] = this.convertToJsonSerializable(value);
|
|
563
|
+
}
|
|
564
|
+
return result;
|
|
565
|
+
}
|
|
566
|
+
return obj;
|
|
567
|
+
}
|
|
568
|
+
GenerateDigitDecompositionMessages(oracleEvent) {
|
|
569
|
+
const oracleNonces = oracleEvent.oracleNonces;
|
|
570
|
+
const eventDescriptor = oracleEvent.eventDescriptor;
|
|
571
|
+
const messagesList = [];
|
|
572
|
+
oracleNonces.forEach(() => {
|
|
573
|
+
const messages = [];
|
|
574
|
+
for (let i = 0; i < eventDescriptor.base; i++) {
|
|
575
|
+
const m = i.toString();
|
|
576
|
+
messages.push(m);
|
|
577
|
+
}
|
|
578
|
+
messagesList.push({ messages });
|
|
579
|
+
});
|
|
580
|
+
return messagesList;
|
|
581
|
+
}
|
|
582
|
+
GenerateMessages(oracleInfo) {
|
|
583
|
+
// Handle both SingleOracleInfo and MultiOracleInfo using type property instead of instanceof
|
|
584
|
+
let oracleEvent;
|
|
585
|
+
if (oracleInfo.type === messaging_1.MessageType.SingleOracleInfo) {
|
|
586
|
+
const singleOracleInfo = oracleInfo;
|
|
587
|
+
oracleEvent = singleOracleInfo.announcement.oracleEvent;
|
|
588
|
+
}
|
|
589
|
+
else if (oracleInfo.type === messaging_1.MessageType.MultiOracleInfo) {
|
|
590
|
+
const multiOracleInfo = oracleInfo;
|
|
591
|
+
// For multi-oracle, use the first announcement for now
|
|
592
|
+
// TODO: This might need more sophisticated handling for multi-oracle scenarios
|
|
593
|
+
if (multiOracleInfo.announcements.length === 0) {
|
|
594
|
+
throw Error('MultiOracleInfo must have at least one announcement');
|
|
595
|
+
}
|
|
596
|
+
oracleEvent = multiOracleInfo.announcements[0].oracleEvent;
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
throw Error(`OracleInfo must be SingleOracleInfo or MultiOracleInfo, got type: ${oracleInfo.type}`);
|
|
600
|
+
}
|
|
601
|
+
switch (oracleEvent.eventDescriptor.type) {
|
|
602
|
+
case messaging_1.MessageType.EnumEventDescriptor:
|
|
603
|
+
return this.GenerateEnumMessages(oracleEvent);
|
|
604
|
+
case messaging_1.MessageType.DigitDecompositionEventDescriptor:
|
|
605
|
+
return this.GenerateDigitDecompositionMessages(oracleEvent);
|
|
606
|
+
default:
|
|
607
|
+
throw Error('EventDescriptor must be Enum or DigitDecomposition');
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
GetContractOraclePairs(_contractInfo) {
|
|
611
|
+
// Use contractInfoType property instead of instanceof for more reliable type checking
|
|
612
|
+
if (_contractInfo.contractInfoType === messaging_1.ContractInfoType.Single) {
|
|
613
|
+
const singleInfo = _contractInfo;
|
|
614
|
+
return [
|
|
615
|
+
{
|
|
616
|
+
contractDescriptor: singleInfo.contractDescriptor,
|
|
617
|
+
oracleInfo: singleInfo.oracleInfo,
|
|
618
|
+
},
|
|
619
|
+
];
|
|
620
|
+
}
|
|
621
|
+
else if (_contractInfo.contractInfoType === messaging_1.ContractInfoType.Disjoint) {
|
|
622
|
+
const disjointInfo = _contractInfo;
|
|
623
|
+
return disjointInfo.contractOraclePairs;
|
|
624
|
+
}
|
|
625
|
+
else {
|
|
626
|
+
throw Error('ContractInfo must be Single or Disjoint');
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
getFundOutputValueSats(dlcTxs) {
|
|
630
|
+
const fundOutput = dlcTxs.fundTx.outputs[dlcTxs.fundTxVout];
|
|
631
|
+
if (!fundOutput || !fundOutput.value) {
|
|
632
|
+
throw new Error(`Invalid fund output at vout ${dlcTxs.fundTxVout}: ` +
|
|
633
|
+
`outputs.length=${dlcTxs.fundTx.outputs.length}, ` +
|
|
634
|
+
`output exists=${!!fundOutput}, ` +
|
|
635
|
+
`output.value exists=${!!(fundOutput && fundOutput.value)}`);
|
|
636
|
+
}
|
|
637
|
+
return fundOutput.value.sats;
|
|
638
|
+
}
|
|
639
|
+
async CreateCetAdaptorAndRefundSigs(dlcOffer, dlcAccept, dlcTxs, messagesList, isOfferer) {
|
|
640
|
+
const network = await this.getConnectedNetwork();
|
|
641
|
+
const cetsHex = dlcTxs.cets.map((cet) => cet.serialize().toString('hex'));
|
|
642
|
+
// Create the correct P2WSH multisig funding script
|
|
643
|
+
const fundingPubKeys = Buffer.compare(dlcOffer.fundingPubkey, dlcAccept.fundingPubkey) === -1
|
|
644
|
+
? [dlcOffer.fundingPubkey, dlcAccept.fundingPubkey]
|
|
645
|
+
: [dlcAccept.fundingPubkey, dlcOffer.fundingPubkey];
|
|
646
|
+
const p2ms = bitcoinjs_lib_1.payments.p2ms({
|
|
647
|
+
m: 2,
|
|
648
|
+
pubkeys: fundingPubKeys,
|
|
649
|
+
network,
|
|
650
|
+
});
|
|
651
|
+
// We need the redeem script (multisig), not the P2WSH output
|
|
652
|
+
const fundingSPK = p2ms.output;
|
|
653
|
+
// For finding the private key, we still need the individual P2WPKH address
|
|
654
|
+
const individualFundingSPK = bitcoin_1.Script.p2wpkhLock((0, crypto_1.hash160)(isOfferer ? dlcOffer.fundingPubkey : dlcAccept.fundingPubkey))
|
|
655
|
+
.serialize()
|
|
656
|
+
.slice(1);
|
|
657
|
+
const fundingAddress = bitcoinjs_lib_1.address.fromOutputScript(individualFundingSPK, network);
|
|
658
|
+
const { derivationPath } = await this.client.wallet.findAddress([
|
|
659
|
+
fundingAddress,
|
|
660
|
+
]);
|
|
661
|
+
const fundPrivateKeyPair = await this.getMethod('keyPair')(derivationPath);
|
|
662
|
+
const fundPrivateKey = Buffer.from(fundPrivateKeyPair.__D).toString('hex');
|
|
663
|
+
const contractOraclePairs = this.GetContractOraclePairs(dlcOffer.contractInfo);
|
|
664
|
+
const sigs = [];
|
|
665
|
+
if (dlcOffer.contractInfo.contractInfoType === messaging_1.ContractInfoType.Single &&
|
|
666
|
+
dlcOffer.contractInfo.contractDescriptor.type ===
|
|
667
|
+
messaging_1.MessageType.ContractDescriptorV0) {
|
|
668
|
+
for (const { oracleInfo } of contractOraclePairs) {
|
|
669
|
+
if (oracleInfo.type !== messaging_1.MessageType.SingleOracleInfo) {
|
|
670
|
+
throw new Error('Only SingleOracleInfo supported in this context');
|
|
671
|
+
}
|
|
672
|
+
const oracleAnnouncement = oracleInfo
|
|
673
|
+
.announcement;
|
|
674
|
+
const adaptorSigRequestPromises = [];
|
|
675
|
+
const tempMessagesList = messagesList;
|
|
676
|
+
const tempCetsHex = cetsHex;
|
|
677
|
+
const ddkOracleInfo = {
|
|
678
|
+
publicKey: oracleAnnouncement.oraclePubkey,
|
|
679
|
+
nonces: oracleAnnouncement.oracleEvent.oracleNonces,
|
|
680
|
+
};
|
|
681
|
+
adaptorSigRequestPromises.push((async () => {
|
|
682
|
+
const cetsForDdk = tempCetsHex.map((cetHex) => this.convertTxToDdkTransaction(bitcoin_1.Tx.decode(bufio_1.StreamReader.fromHex(cetHex))));
|
|
683
|
+
const messagesForDdk = this.convertMessagesForDdk(tempMessagesList);
|
|
684
|
+
const response = this._ddk.createCetAdaptorSigsFromOracleInfo(cetsForDdk, [ddkOracleInfo], Buffer.from(fundPrivateKey, 'hex'), fundingSPK, this.getFundOutputValueSats(dlcTxs), messagesForDdk);
|
|
685
|
+
return response;
|
|
686
|
+
})());
|
|
687
|
+
const adaptorPairs = (await Promise.all(adaptorSigRequestPromises)).flat();
|
|
688
|
+
sigs.push(adaptorPairs.map((adaptorPair) => {
|
|
689
|
+
return {
|
|
690
|
+
encryptedSig: adaptorPair.signature,
|
|
691
|
+
dleqProof: adaptorPair.proof,
|
|
692
|
+
};
|
|
693
|
+
}));
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
else {
|
|
697
|
+
const indices = this.GetIndicesFromPayouts(this.GetPayouts(dlcOffer));
|
|
698
|
+
for (const [index, { oracleInfo }] of contractOraclePairs.entries()) {
|
|
699
|
+
if (oracleInfo.type !== messaging_1.MessageType.SingleOracleInfo) {
|
|
700
|
+
throw new Error('Only SingleOracleInfo supported in this context');
|
|
701
|
+
}
|
|
702
|
+
const oracleAnnouncement = oracleInfo
|
|
703
|
+
.announcement;
|
|
704
|
+
const startingIndex = indices[index].startingMessagesIndex, endingIndex = indices[index + 1].startingMessagesIndex;
|
|
705
|
+
const oracleEventMessagesList = messagesList.slice(startingIndex, endingIndex);
|
|
706
|
+
const oracleEventCetsHex = cetsHex.slice(startingIndex, endingIndex);
|
|
707
|
+
const chunk = 100;
|
|
708
|
+
const adaptorSigRequestPromises = [];
|
|
709
|
+
for (let i = 0, j = oracleEventMessagesList.length; i < j; i += chunk) {
|
|
710
|
+
const tempMessagesList = oracleEventMessagesList.slice(i, i + chunk);
|
|
711
|
+
const tempCetsHex = oracleEventCetsHex.slice(i, i + chunk);
|
|
712
|
+
const ddkOracleInfo = {
|
|
713
|
+
publicKey: oracleAnnouncement.oraclePubkey,
|
|
714
|
+
nonces: oracleAnnouncement.oracleEvent.oracleNonces,
|
|
715
|
+
};
|
|
716
|
+
const messagesForDdk = this.convertMessagesForDdk(tempMessagesList);
|
|
717
|
+
adaptorSigRequestPromises.push((async () => {
|
|
718
|
+
const response = this._ddk.createCetAdaptorSigsFromOracleInfo(tempCetsHex.map((cetHex) => this.convertTxToDdkTransaction(bitcoin_1.Tx.decode(bufio_1.StreamReader.fromHex(cetHex)))), [ddkOracleInfo], Buffer.from(fundPrivateKey, 'hex'), fundingSPK, this.getFundOutputValueSats(dlcTxs), messagesForDdk);
|
|
719
|
+
return response;
|
|
720
|
+
})());
|
|
721
|
+
}
|
|
722
|
+
const adaptorPairs = (await Promise.all(adaptorSigRequestPromises)).flat();
|
|
723
|
+
sigs.push(adaptorPairs.map((adaptorPair) => {
|
|
724
|
+
return {
|
|
725
|
+
encryptedSig: adaptorPair.signature,
|
|
726
|
+
dleqProof: adaptorPair.proof,
|
|
727
|
+
};
|
|
728
|
+
}));
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
const refundSignature = this._ddk.getRawFundingTransactionInputSignature(this.convertTxToDdkTransaction(dlcTxs.refundTx), Buffer.from(fundPrivateKey, 'hex'), dlcTxs.fundTx.txId.toString(), dlcTxs.fundTxVout, dlcOffer.contractInfo.totalCollateral);
|
|
732
|
+
const cetSignatures = new messaging_1.CetAdaptorSignatures();
|
|
733
|
+
cetSignatures.sigs = sigs.flat();
|
|
734
|
+
return { cetSignatures, refundSignature };
|
|
735
|
+
}
|
|
736
|
+
async VerifyCetAdaptorAndRefundSigs(dlcOffer, dlcAccept, dlcSign, dlcTxs, messagesList, isOfferer) {
|
|
737
|
+
const cetsHex = dlcTxs.cets.map((cet) => cet.serialize().toString('hex'));
|
|
738
|
+
const contractOraclePairs = this.GetContractOraclePairs(dlcOffer.contractInfo);
|
|
739
|
+
if (dlcOffer.contractInfo.type === messaging_1.MessageType.SingleContractInfo &&
|
|
740
|
+
dlcOffer.contractInfo.contractDescriptor.type ===
|
|
741
|
+
messaging_1.MessageType.ContractDescriptorV0) {
|
|
742
|
+
for (const { oracleInfo } of contractOraclePairs) {
|
|
743
|
+
if (oracleInfo.type !== messaging_1.MessageType.SingleOracleInfo) {
|
|
744
|
+
throw new Error('Only SingleOracleInfo supported in this context');
|
|
745
|
+
}
|
|
746
|
+
const oracleAnnouncement = oracleInfo
|
|
747
|
+
.announcement;
|
|
748
|
+
const oracleEventSigs = isOfferer
|
|
749
|
+
? dlcAccept.cetAdaptorSignatures.sigs
|
|
750
|
+
: dlcSign.cetAdaptorSignatures.sigs;
|
|
751
|
+
const sigsValidity = [];
|
|
752
|
+
const tempMessagesList = messagesList;
|
|
753
|
+
const tempSigs = oracleEventSigs;
|
|
754
|
+
const tempAdaptorPairs = tempSigs.map((sig) => {
|
|
755
|
+
return {
|
|
756
|
+
signature: sig.encryptedSig,
|
|
757
|
+
proof: sig.dleqProof,
|
|
758
|
+
};
|
|
759
|
+
});
|
|
760
|
+
// Create the correct P2WSH multisig funding script for verification
|
|
761
|
+
const network = await this.getConnectedNetwork();
|
|
762
|
+
const verifyFundingPubKeys = Buffer.compare(dlcOffer.fundingPubkey, dlcAccept.fundingPubkey) === -1
|
|
763
|
+
? [dlcOffer.fundingPubkey, dlcAccept.fundingPubkey]
|
|
764
|
+
: [dlcAccept.fundingPubkey, dlcOffer.fundingPubkey];
|
|
765
|
+
const verifyP2ms = bitcoinjs_lib_1.payments.p2ms({
|
|
766
|
+
m: 2,
|
|
767
|
+
pubkeys: verifyFundingPubKeys,
|
|
768
|
+
network,
|
|
769
|
+
});
|
|
770
|
+
// We need the redeem script (multisig), not the P2WSH output
|
|
771
|
+
const fundingSPK = verifyP2ms.output;
|
|
772
|
+
const ddkOracleInfo = {
|
|
773
|
+
publicKey: oracleAnnouncement.oraclePubkey,
|
|
774
|
+
nonces: oracleAnnouncement.oracleEvent.oracleNonces,
|
|
775
|
+
};
|
|
776
|
+
const pubkey = isOfferer
|
|
777
|
+
? dlcAccept.fundingPubkey
|
|
778
|
+
: dlcOffer.fundingPubkey;
|
|
779
|
+
const messagesForDdk = this.convertMessagesForDdk(tempMessagesList);
|
|
780
|
+
sigsValidity.push((async () => {
|
|
781
|
+
const response = this._ddk.verifyCetAdaptorSigsFromOracleInfo(tempAdaptorPairs, dlcTxs.cets.map((cet) => this.convertTxToDdkTransaction(cet)), [ddkOracleInfo], pubkey, fundingSPK, this.getFundOutputValueSats(dlcTxs), messagesForDdk);
|
|
782
|
+
return response;
|
|
783
|
+
})());
|
|
784
|
+
let areSigsValid = (await Promise.all(sigsValidity)).every((b) => b);
|
|
785
|
+
await this.VerifyRefundSignatureAlt(dlcOffer, dlcAccept, dlcSign, dlcTxs, isOfferer);
|
|
786
|
+
if (!areSigsValid) {
|
|
787
|
+
throw new Error('Invalid signatures received');
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
else {
|
|
792
|
+
const chunk = 100;
|
|
793
|
+
const indices = this.GetIndicesFromPayouts(this.GetPayouts(dlcOffer));
|
|
794
|
+
for (const [index, { oracleInfo }] of contractOraclePairs.entries()) {
|
|
795
|
+
if (oracleInfo.type !== messaging_1.MessageType.SingleOracleInfo) {
|
|
796
|
+
throw new Error('Only SingleOracleInfo supported in this context');
|
|
797
|
+
}
|
|
798
|
+
const oracleAnnouncement = oracleInfo
|
|
799
|
+
.announcement;
|
|
800
|
+
const startingIndex = indices[index].startingMessagesIndex, endingIndex = indices[index + 1].startingMessagesIndex;
|
|
801
|
+
const oracleEventMessagesList = messagesList.slice(startingIndex, endingIndex);
|
|
802
|
+
const oracleEventCetsHex = cetsHex.slice(startingIndex, endingIndex);
|
|
803
|
+
const oracleEventSigs = (isOfferer
|
|
804
|
+
? dlcAccept.cetAdaptorSignatures.sigs
|
|
805
|
+
: dlcSign.cetAdaptorSignatures.sigs).slice(startingIndex, endingIndex);
|
|
806
|
+
const sigsValidity = [];
|
|
807
|
+
for (let i = 0, j = oracleEventMessagesList.length; i < j; i += chunk) {
|
|
808
|
+
const tempMessagesList = oracleEventMessagesList.slice(i, i + chunk);
|
|
809
|
+
const tempCetsHex = oracleEventCetsHex.slice(i, i + chunk);
|
|
810
|
+
const tempSigs = oracleEventSigs.slice(i, i + chunk);
|
|
811
|
+
const tempAdaptorPairs = tempSigs.map((sig) => {
|
|
812
|
+
return {
|
|
813
|
+
signature: sig.encryptedSig,
|
|
814
|
+
proof: sig.dleqProof,
|
|
815
|
+
};
|
|
816
|
+
});
|
|
817
|
+
// Create the correct P2WSH multisig funding script
|
|
818
|
+
const network = await this.getConnectedNetwork();
|
|
819
|
+
const nonEnumFundingPubKeys = Buffer.compare(dlcOffer.fundingPubkey, dlcAccept.fundingPubkey) ===
|
|
820
|
+
-1
|
|
821
|
+
? [dlcOffer.fundingPubkey, dlcAccept.fundingPubkey]
|
|
822
|
+
: [dlcAccept.fundingPubkey, dlcOffer.fundingPubkey];
|
|
823
|
+
const nonEnumP2ms = bitcoinjs_lib_1.payments.p2ms({
|
|
824
|
+
m: 2,
|
|
825
|
+
pubkeys: nonEnumFundingPubKeys,
|
|
826
|
+
network,
|
|
827
|
+
});
|
|
828
|
+
// We need the redeem script (multisig), not the P2WSH output
|
|
829
|
+
const fundingSPK = nonEnumP2ms.output;
|
|
830
|
+
const ddkOracleInfo = {
|
|
831
|
+
publicKey: oracleAnnouncement.oraclePubkey,
|
|
832
|
+
nonces: oracleAnnouncement.oracleEvent.oracleNonces,
|
|
833
|
+
};
|
|
834
|
+
const pubkey = isOfferer
|
|
835
|
+
? dlcAccept.fundingPubkey
|
|
836
|
+
: dlcOffer.fundingPubkey;
|
|
837
|
+
const messagesForDdk = this.convertMessagesForDdk(tempMessagesList);
|
|
838
|
+
sigsValidity.push((async () => {
|
|
839
|
+
const response = this._ddk.verifyCetAdaptorSigsFromOracleInfo(tempAdaptorPairs, tempCetsHex.map((cet) => this.convertTxToDdkTransaction(bitcoin_1.Tx.decode(bufio_1.StreamReader.fromHex(cet)))), [ddkOracleInfo], pubkey, fundingSPK, this.getFundOutputValueSats(dlcTxs), messagesForDdk);
|
|
840
|
+
return response;
|
|
841
|
+
})());
|
|
842
|
+
}
|
|
843
|
+
let areSigsValid = (await Promise.all(sigsValidity)).every((b) => b);
|
|
844
|
+
// Verify refund signature using PSBT approach
|
|
845
|
+
await this.VerifyRefundSignatureAlt(dlcOffer, dlcAccept, dlcSign, dlcTxs, isOfferer);
|
|
846
|
+
if (!areSigsValid) {
|
|
847
|
+
throw new Error('Invalid CET adaptor signatures received');
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
async CreateFundingSigsAlt(dlcOffer, dlcAccept, dlcTxs, isOfferer) {
|
|
853
|
+
const transaction = bitcoinjs_lib_1.Transaction.fromBuffer(dlcTxs.fundTx.serialize());
|
|
854
|
+
const network = await this.getConnectedNetwork();
|
|
855
|
+
const psbt = new bitcoinjs_lib_1.Psbt({ network });
|
|
856
|
+
// Combine all funding inputs from both parties
|
|
857
|
+
const allFundingInputs = [
|
|
858
|
+
...dlcOffer.fundingInputs,
|
|
859
|
+
...dlcAccept.fundingInputs,
|
|
860
|
+
];
|
|
861
|
+
// Sort by inputSerialId to reconstruct proper transaction order
|
|
862
|
+
allFundingInputs.sort((a, b) => Number(a.inputSerialId - b.inputSerialId));
|
|
863
|
+
// Add all inputs to PSBT with proper witnessUtxo
|
|
864
|
+
for (const fundingInput of allFundingInputs) {
|
|
865
|
+
const prevOut = fundingInput.prevTx.outputs[fundingInput.prevTxVout];
|
|
866
|
+
// Use the same pattern as existing code - slice(1) to remove length prefix
|
|
867
|
+
const witnessUtxo = {
|
|
868
|
+
script: prevOut.scriptPubKey.serialize().subarray(1),
|
|
869
|
+
value: Number(prevOut.value.sats),
|
|
870
|
+
};
|
|
871
|
+
// Use sequence from the original transaction to ensure consistency
|
|
872
|
+
const originalInput = transaction.ins.find((input) => input.hash.reverse().toString('hex') ===
|
|
873
|
+
fundingInput.prevTx.txId.toString() &&
|
|
874
|
+
input.index === fundingInput.prevTxVout);
|
|
875
|
+
const sequenceValue = originalInput
|
|
876
|
+
? originalInput.sequence
|
|
877
|
+
: Number(fundingInput.sequence);
|
|
878
|
+
psbt.addInput({
|
|
879
|
+
hash: fundingInput.prevTx.txId.toString(),
|
|
880
|
+
index: fundingInput.prevTxVout,
|
|
881
|
+
sequence: sequenceValue,
|
|
882
|
+
witnessUtxo,
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
// Add all outputs to PSBT (maintains transaction structure)
|
|
886
|
+
for (const output of transaction.outs) {
|
|
887
|
+
psbt.addOutput({
|
|
888
|
+
address: bitcoinjs_lib_1.address.fromOutputScript(output.script, network),
|
|
889
|
+
value: output.value,
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
// Determine which inputs belong to this party
|
|
893
|
+
const partyFundingInputs = isOfferer
|
|
894
|
+
? dlcOffer.fundingInputs
|
|
895
|
+
: dlcAccept.fundingInputs;
|
|
896
|
+
// Convert party's funding inputs to Input objects and get private keys
|
|
897
|
+
const partyInputs = await Promise.all(partyFundingInputs.map(async (fundingInput) => {
|
|
898
|
+
return this.fundingInputToInput(fundingInput);
|
|
899
|
+
}));
|
|
900
|
+
const inputPrivKeys = await this.GetPrivKeysForInputs(partyInputs);
|
|
901
|
+
// Create map of this party's inputs for efficient lookup
|
|
902
|
+
const partyInputMap = new Map();
|
|
903
|
+
partyInputs.forEach((input, index) => {
|
|
904
|
+
const key = `${input.txid}:${input.vout}`;
|
|
905
|
+
partyInputMap.set(key, { input, privKey: inputPrivKeys[index] });
|
|
906
|
+
});
|
|
907
|
+
// Initialize witness elements array to match all inputs
|
|
908
|
+
const witnessElements = [];
|
|
909
|
+
// Sign only this party's inputs
|
|
910
|
+
for (let inputIndex = 0; inputIndex < allFundingInputs.length; inputIndex++) {
|
|
911
|
+
const fundingInput = allFundingInputs[inputIndex];
|
|
912
|
+
const inputKey = `${fundingInput.prevTx.txId.toString()}:${fundingInput.prevTxVout}`;
|
|
913
|
+
if (partyInputMap.has(inputKey)) {
|
|
914
|
+
// This input belongs to this party - sign it
|
|
915
|
+
const { privKey } = partyInputMap.get(inputKey);
|
|
916
|
+
const keyPair = ECPair.fromPrivateKey(Buffer.from(privKey, 'hex'));
|
|
917
|
+
// Check if this is a DLC input (2-of-2 multisig from previous DLC)
|
|
918
|
+
if (fundingInput.dlcInput) {
|
|
919
|
+
// For DLC inputs, we'll need to handle 2-of-2 multisig signing differently
|
|
920
|
+
// For now, throw an error as this requires implementing multisig support
|
|
921
|
+
throw new Error('DLC input signing not yet implemented in CreateFundingSigsAlt');
|
|
922
|
+
}
|
|
923
|
+
else {
|
|
924
|
+
// For P2WPKH inputs, use PSBT signing
|
|
925
|
+
psbt.signInput(inputIndex, keyPair);
|
|
926
|
+
// Extract signature from partial signatures (more reliable than finalization)
|
|
927
|
+
const inputData = psbt.data.inputs[inputIndex];
|
|
928
|
+
const partialSigs = inputData.partialSig;
|
|
929
|
+
if (!partialSigs || partialSigs.length === 0) {
|
|
930
|
+
throw new Error(`No signatures found for input ${inputIndex} after signing`);
|
|
931
|
+
}
|
|
932
|
+
// For P2WPKH, create witness manually: [signature, publicKey]
|
|
933
|
+
const sigWitness = new messaging_1.ScriptWitnessV0();
|
|
934
|
+
sigWitness.witness = partialSigs[0].signature;
|
|
935
|
+
const pubKeyWitness = new messaging_1.ScriptWitnessV0();
|
|
936
|
+
pubKeyWitness.witness = keyPair.publicKey;
|
|
937
|
+
witnessElements.push([sigWitness, pubKeyWitness]);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
// Note: We don't add anything to witnessElements for other party's inputs
|
|
941
|
+
}
|
|
942
|
+
const fundingSignatures = new messaging_1.FundingSignatures();
|
|
943
|
+
fundingSignatures.witnessElements = witnessElements;
|
|
944
|
+
return fundingSignatures;
|
|
945
|
+
}
|
|
946
|
+
async VerifyFundingSigsAlt(dlcOffer, dlcAccept, dlcSign, dlcTxs, isOfferer) {
|
|
947
|
+
const network = await this.getConnectedNetwork();
|
|
948
|
+
const transaction = bitcoinjs_lib_1.Transaction.fromBuffer(dlcTxs.fundTx.serialize());
|
|
949
|
+
// Get the party whose signatures we're verifying
|
|
950
|
+
// If we're the offerer, we verify accepter's signatures (from dlcSign)
|
|
951
|
+
// If we're the accepter, we verify offerer's signatures (from dlcSign)
|
|
952
|
+
const signingPartyInputs = isOfferer
|
|
953
|
+
? dlcAccept.fundingInputs
|
|
954
|
+
: dlcOffer.fundingInputs;
|
|
955
|
+
// Combine all inputs to get the correct ordering
|
|
956
|
+
const allFundingInputs = [
|
|
957
|
+
...dlcOffer.fundingInputs,
|
|
958
|
+
...dlcAccept.fundingInputs,
|
|
959
|
+
];
|
|
960
|
+
allFundingInputs.sort((a, b) => Number(a.inputSerialId - b.inputSerialId));
|
|
961
|
+
// Compare transaction IDs
|
|
962
|
+
const dlcFundTxId = dlcTxs.fundTx.txId.toString();
|
|
963
|
+
const psbtBuilderTxId = transaction.getId();
|
|
964
|
+
(0, assert_1.default)(dlcFundTxId === psbtBuilderTxId, 'Transaction IDs do not match');
|
|
965
|
+
// Create a PSBT for signature verification
|
|
966
|
+
const psbt = new bitcoinjs_lib_1.Psbt({ network });
|
|
967
|
+
// Add all inputs (needed for proper sighash calculation)
|
|
968
|
+
for (const input of allFundingInputs) {
|
|
969
|
+
const prevOutput = input.prevTx.outputs[input.prevTxVout];
|
|
970
|
+
psbt.addInput({
|
|
971
|
+
hash: input.prevTx.txId.toString(),
|
|
972
|
+
index: input.prevTxVout,
|
|
973
|
+
sequence: 0,
|
|
974
|
+
witnessUtxo: {
|
|
975
|
+
script: prevOutput.scriptPubKey.serialize().subarray(1),
|
|
976
|
+
value: Number(prevOutput.value.sats),
|
|
977
|
+
},
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
// Add all outputs
|
|
981
|
+
for (const output of transaction.outs) {
|
|
982
|
+
psbt.addOutput({
|
|
983
|
+
address: bitcoinjs_lib_1.address.fromOutputScript(output.script, network),
|
|
984
|
+
value: output.value,
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
if (psbt.inputCount !== transaction.ins.length ||
|
|
988
|
+
psbt.txOutputs.length !== transaction.outs.length) {
|
|
989
|
+
throw new Error(`PSBT structure doesn't match original transaction`);
|
|
990
|
+
}
|
|
991
|
+
// Add the funding signatures to the PSBT as partial signatures
|
|
992
|
+
let witnessIndex = 0;
|
|
993
|
+
for (const fundingInput of signingPartyInputs) {
|
|
994
|
+
// Skip DLC inputs for now (same as CreateFundingSigsAlt)
|
|
995
|
+
if (fundingInput.dlcInput) {
|
|
996
|
+
continue;
|
|
997
|
+
}
|
|
998
|
+
if (witnessIndex >= dlcSign.fundingSignatures.witnessElements.length) {
|
|
999
|
+
throw new Error(`Not enough witness elements: expected at least ${witnessIndex + 1}, got ${dlcSign.fundingSignatures.witnessElements.length}`);
|
|
1000
|
+
}
|
|
1001
|
+
const witnessElement = dlcSign.fundingSignatures.witnessElements[witnessIndex];
|
|
1002
|
+
const signature = witnessElement[0].witness;
|
|
1003
|
+
const publicKey = witnessElement[1].witness;
|
|
1004
|
+
// Find this input's index in the sorted transaction
|
|
1005
|
+
const inputIndex = allFundingInputs.findIndex((input) => input.prevTx.txId.toString() ===
|
|
1006
|
+
fundingInput.prevTx.txId.toString() &&
|
|
1007
|
+
input.prevTxVout === fundingInput.prevTxVout);
|
|
1008
|
+
if (inputIndex === -1) {
|
|
1009
|
+
throw new Error(`Input not found in transaction: ${fundingInput.prevTx.txId.toString()}:${fundingInput.prevTxVout}`);
|
|
1010
|
+
}
|
|
1011
|
+
psbt.updateInput(inputIndex, {
|
|
1012
|
+
partialSig: [{ pubkey: publicKey, signature: signature }],
|
|
1013
|
+
});
|
|
1014
|
+
psbt.validateSignaturesOfInput(inputIndex, (pubkey, msghash, signature) => {
|
|
1015
|
+
return ecc.verify(msghash, pubkey, signature);
|
|
1016
|
+
});
|
|
1017
|
+
witnessIndex++;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
async VerifyRefundSignatureAlt(dlcOffer, dlcAccept, dlcSign, dlcTxs, isOfferer) {
|
|
1021
|
+
const network = await this.getConnectedNetwork();
|
|
1022
|
+
// Get the refund signature we need to verify
|
|
1023
|
+
// If we're the offerer, we verify accepter's refund signature (from dlcAccept)
|
|
1024
|
+
// If we're the accepter, we verify offerer's refund signature (from dlcSign)
|
|
1025
|
+
const refundSignature = isOfferer
|
|
1026
|
+
? dlcAccept.refundSignature
|
|
1027
|
+
: dlcSign.refundSignature;
|
|
1028
|
+
const signingPubkey = isOfferer
|
|
1029
|
+
? dlcAccept.fundingPubkey
|
|
1030
|
+
: dlcOffer.fundingPubkey;
|
|
1031
|
+
// Verify refund transaction locktime matches expected
|
|
1032
|
+
if (Number(dlcTxs.refundTx.locktime) !== dlcOffer.refundLocktime) {
|
|
1033
|
+
throw new Error(`Refund transaction locktime ${dlcTxs.refundTx.locktime} does not match expected ${dlcOffer.refundLocktime}`);
|
|
1034
|
+
}
|
|
1035
|
+
// Create a PSBT for the refund transaction verification
|
|
1036
|
+
const psbt = new bitcoinjs_lib_1.Psbt({ network });
|
|
1037
|
+
// Add the funding input
|
|
1038
|
+
const fundingOutput = dlcTxs.fundTx.outputs[dlcTxs.fundTxVout];
|
|
1039
|
+
psbt.addInput({
|
|
1040
|
+
hash: dlcTxs.fundTx.txId.toString(),
|
|
1041
|
+
index: dlcTxs.fundTxVout,
|
|
1042
|
+
sequence: 0,
|
|
1043
|
+
witnessUtxo: {
|
|
1044
|
+
script: fundingOutput.scriptPubKey.serialize().subarray(1), // Remove length prefix
|
|
1045
|
+
value: Number(fundingOutput.value.sats),
|
|
1046
|
+
},
|
|
1047
|
+
witnessScript: this.CreateFundingScript(dlcOffer, dlcAccept), // 2-of-2 multisig script
|
|
1048
|
+
});
|
|
1049
|
+
// Add the refund output
|
|
1050
|
+
const refundOutput = dlcTxs.refundTx.outputs[0];
|
|
1051
|
+
psbt.addOutput({
|
|
1052
|
+
address: bitcoinjs_lib_1.address.fromOutputScript(refundOutput.scriptPubKey.serialize().subarray(1), network),
|
|
1053
|
+
value: Number(refundOutput.value.sats),
|
|
1054
|
+
});
|
|
1055
|
+
// Set the locktime to match the refund transaction
|
|
1056
|
+
psbt.setLocktime(Number(dlcTxs.refundTx.locktime));
|
|
1057
|
+
// Add the refund signature as a partial signature
|
|
1058
|
+
psbt.updateInput(0, {
|
|
1059
|
+
partialSig: [{ pubkey: signingPubkey, signature: refundSignature }],
|
|
1060
|
+
});
|
|
1061
|
+
// Validate the refund signature
|
|
1062
|
+
try {
|
|
1063
|
+
psbt.validateSignaturesOfInput(0, (pubkey, msghash, signature) => {
|
|
1064
|
+
return ecc.verify(msghash, pubkey, signature);
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
catch (error) {
|
|
1068
|
+
throw new Error(`Refund signature validation failed for ${isOfferer ? 'accepter' : 'offerer'}: ${error.message}`);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
CreateFundingScript(dlcOffer, dlcAccept) {
|
|
1072
|
+
const network = this.getBitcoinJsNetwork();
|
|
1073
|
+
// Sort funding pubkeys in lexicographical order as per DLC spec
|
|
1074
|
+
const fundingPubKeys = Buffer.compare(dlcOffer.fundingPubkey, dlcAccept.fundingPubkey) === -1
|
|
1075
|
+
? [dlcOffer.fundingPubkey, dlcAccept.fundingPubkey]
|
|
1076
|
+
: [dlcAccept.fundingPubkey, dlcOffer.fundingPubkey];
|
|
1077
|
+
// Create 2-of-2 multisig script
|
|
1078
|
+
const p2ms = bitcoinjs_lib_1.payments.p2ms({
|
|
1079
|
+
m: 2,
|
|
1080
|
+
pubkeys: fundingPubKeys,
|
|
1081
|
+
network,
|
|
1082
|
+
});
|
|
1083
|
+
const paymentVariant = bitcoinjs_lib_1.payments.p2wsh({
|
|
1084
|
+
redeem: p2ms,
|
|
1085
|
+
network,
|
|
1086
|
+
});
|
|
1087
|
+
return paymentVariant.redeem.output;
|
|
1088
|
+
}
|
|
1089
|
+
async CreateFundingTx(dlcOffer, dlcAccept, dlcSign, dlcTxs, fundingSignatures) {
|
|
1090
|
+
const network = await this.getConnectedNetwork();
|
|
1091
|
+
const psbt = new bitcoinjs_lib_1.Psbt({ network });
|
|
1092
|
+
// Combine and sort all funding inputs by serial ID (same as CreateFundingSigsAlt)
|
|
1093
|
+
const allFundingInputs = [
|
|
1094
|
+
...dlcOffer.fundingInputs,
|
|
1095
|
+
...dlcAccept.fundingInputs,
|
|
1096
|
+
];
|
|
1097
|
+
allFundingInputs.sort((a, b) => Number(a.inputSerialId - b.inputSerialId));
|
|
1098
|
+
// Create a map of input txid:vout to witness elements
|
|
1099
|
+
const witnessMap = new Map();
|
|
1100
|
+
// Map witness elements correctly - CreateFundingSigsAlt only creates witness elements
|
|
1101
|
+
// for the party's own inputs, so we need to map them correctly
|
|
1102
|
+
// For dlcSign (offerer's signatures), map to offerer's inputs only
|
|
1103
|
+
let offererWitnessIndex = 0;
|
|
1104
|
+
dlcOffer.fundingInputs.forEach((fundingInput) => {
|
|
1105
|
+
// Skip DLC inputs for now as in CreateFundingSigsAlt
|
|
1106
|
+
if (fundingInput.dlcInput) {
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
if (offererWitnessIndex < dlcSign.fundingSignatures.witnessElements.length) {
|
|
1110
|
+
const key = `${fundingInput.prevTx.txId.toString()}:${fundingInput.prevTxVout}`;
|
|
1111
|
+
witnessMap.set(key, dlcSign.fundingSignatures.witnessElements[offererWitnessIndex]);
|
|
1112
|
+
offererWitnessIndex++;
|
|
1113
|
+
}
|
|
1114
|
+
});
|
|
1115
|
+
// For fundingSignatures (accepter's signatures), map to accepter's inputs only
|
|
1116
|
+
let accepterWitnessIndex = 0;
|
|
1117
|
+
dlcAccept.fundingInputs.forEach((fundingInput) => {
|
|
1118
|
+
// Skip DLC inputs for now as in CreateFundingSigsAlt
|
|
1119
|
+
if (fundingInput.dlcInput) {
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
if (accepterWitnessIndex < fundingSignatures.witnessElements.length) {
|
|
1123
|
+
const key = `${fundingInput.prevTx.txId.toString()}:${fundingInput.prevTxVout}`;
|
|
1124
|
+
witnessMap.set(key, fundingSignatures.witnessElements[accepterWitnessIndex]);
|
|
1125
|
+
accepterWitnessIndex++;
|
|
1126
|
+
}
|
|
1127
|
+
});
|
|
1128
|
+
// Add all inputs to PSBT with proper sequence values
|
|
1129
|
+
const originalTransaction = bitcoinjs_lib_1.Transaction.fromBuffer(dlcTxs.fundTx.serialize());
|
|
1130
|
+
for (const fundingInput of allFundingInputs) {
|
|
1131
|
+
const prevOut = fundingInput.prevTx.outputs[fundingInput.prevTxVout];
|
|
1132
|
+
// Use same script handling as CreateFundingSigsAlt
|
|
1133
|
+
const witnessUtxo = {
|
|
1134
|
+
script: prevOut.scriptPubKey.serialize().subarray(1),
|
|
1135
|
+
value: Number(prevOut.value.sats),
|
|
1136
|
+
};
|
|
1137
|
+
// Use sequence from the original transaction (same as CreateFundingSigsAlt)
|
|
1138
|
+
const originalInput = originalTransaction.ins.find((input) => input.hash.reverse().toString('hex') ===
|
|
1139
|
+
fundingInput.prevTx.txId.toString() &&
|
|
1140
|
+
input.index === fundingInput.prevTxVout);
|
|
1141
|
+
const sequenceValue = originalInput
|
|
1142
|
+
? originalInput.sequence
|
|
1143
|
+
: Number(fundingInput.sequence);
|
|
1144
|
+
psbt.addInput({
|
|
1145
|
+
hash: fundingInput.prevTx.txId.toString(),
|
|
1146
|
+
index: fundingInput.prevTxVout,
|
|
1147
|
+
sequence: sequenceValue,
|
|
1148
|
+
witnessUtxo,
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
// Add all outputs from the funding transaction
|
|
1152
|
+
for (const output of originalTransaction.outs) {
|
|
1153
|
+
psbt.addOutput({
|
|
1154
|
+
address: bitcoinjs_lib_1.address.fromOutputScript(output.script, network),
|
|
1155
|
+
value: output.value,
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
// Finalize inputs with their witness data
|
|
1159
|
+
for (let inputIndex = 0; inputIndex < allFundingInputs.length; inputIndex++) {
|
|
1160
|
+
const fundingInput = allFundingInputs[inputIndex];
|
|
1161
|
+
const inputKey = `${fundingInput.prevTx.txId.toString()}:${fundingInput.prevTxVout}`;
|
|
1162
|
+
const witnessElements = witnessMap.get(inputKey);
|
|
1163
|
+
if (witnessElements && witnessElements.length === 2) {
|
|
1164
|
+
// Skip DLC inputs for now as requested
|
|
1165
|
+
if (fundingInput.dlcInput) {
|
|
1166
|
+
continue;
|
|
1167
|
+
}
|
|
1168
|
+
// For P2WPKH inputs, finalize the input with witness data
|
|
1169
|
+
const signature = witnessElements[0].witness;
|
|
1170
|
+
const publicKey = witnessElements[1].witness;
|
|
1171
|
+
// Try a simpler approach - let bitcoinjs-lib handle witness construction
|
|
1172
|
+
psbt.finalizeInput(inputIndex, () => ({
|
|
1173
|
+
finalScriptSig: Buffer.alloc(0),
|
|
1174
|
+
finalScriptWitness: Buffer.concat([
|
|
1175
|
+
Buffer.from([0x02]), // witness stack count
|
|
1176
|
+
Buffer.from([signature.length]),
|
|
1177
|
+
signature,
|
|
1178
|
+
Buffer.from([publicKey.length]),
|
|
1179
|
+
publicKey,
|
|
1180
|
+
]),
|
|
1181
|
+
}));
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
// Extract the final transaction
|
|
1185
|
+
const finalTx = psbt.extractTransaction();
|
|
1186
|
+
// Convert back to the expected Tx format
|
|
1187
|
+
return bitcoin_1.Tx.decode(bufio_1.StreamReader.fromBuffer(finalTx.toBuffer()));
|
|
1188
|
+
}
|
|
1189
|
+
async FindOutcomeIndexFromPolynomialPayoutCurvePiece(dlcOffer, contractDescriptor, contractOraclePairIndex, polynomialPayoutCurvePiece, oracleAttestation, outcome) {
|
|
1190
|
+
const polynomialCurve = core_2.PolynomialPayoutCurve.fromPayoutCurvePiece(polynomialPayoutCurvePiece);
|
|
1191
|
+
const payouts = polynomialPayoutCurvePiece.points.map((point) => Number(point.outcomePayout));
|
|
1192
|
+
const minPayout = Math.min(...payouts);
|
|
1193
|
+
const maxPayout = Math.max(...payouts);
|
|
1194
|
+
const clampBN = (val) => bignumber_js_1.default.max(minPayout, bignumber_js_1.default.min(val, maxPayout));
|
|
1195
|
+
const payout = clampBN(polynomialCurve.getPayout(outcome));
|
|
1196
|
+
const payoutResponses = this.GetPayouts(dlcOffer);
|
|
1197
|
+
const payoutIndexOffset = this.GetIndicesFromPayouts(payoutResponses)[contractOraclePairIndex]
|
|
1198
|
+
.startingMessagesIndex;
|
|
1199
|
+
const { payoutGroups } = payoutResponses[contractOraclePairIndex];
|
|
1200
|
+
const intervalsSorted = [
|
|
1201
|
+
...contractDescriptor.roundingIntervals.intervals,
|
|
1202
|
+
].sort((a, b) => Number(b.beginInterval) - Number(a.beginInterval));
|
|
1203
|
+
const interval = intervalsSorted.find((interval) => Number(outcome) >= Number(interval.beginInterval));
|
|
1204
|
+
const roundedPayout = BigInt(clampBN(new bignumber_js_1.default((0, core_2.roundPayout)(payout, interval.roundingMod).toString())).toString());
|
|
1205
|
+
const outcomesFormatted = oracleAttestation.outcomes.map((outcome) => parseInt(outcome));
|
|
1206
|
+
let index = 0;
|
|
1207
|
+
let groupIndex = -1;
|
|
1208
|
+
let groupLength = 0;
|
|
1209
|
+
for (const payoutGroup of payoutGroups) {
|
|
1210
|
+
if (payoutGroup.payout === roundedPayout) {
|
|
1211
|
+
groupIndex = payoutGroup.groups.findIndex((group) => {
|
|
1212
|
+
return group.every((msg, i) => msg === outcomesFormatted[i]);
|
|
1213
|
+
});
|
|
1214
|
+
if (groupIndex === -1)
|
|
1215
|
+
throw Error('Failed to Find OutcomeIndex From PolynomialPayoutCurvePiece. \
|
|
1216
|
+
Payout Group found but incorrect group index');
|
|
1217
|
+
index += groupIndex;
|
|
1218
|
+
groupLength = payoutGroup.groups[groupIndex].length;
|
|
1219
|
+
break;
|
|
1220
|
+
}
|
|
1221
|
+
else {
|
|
1222
|
+
index += payoutGroup.groups.length;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
if (groupIndex === -1)
|
|
1226
|
+
throw Error('Failed to Find OutcomeIndex From PolynomialPayoutCurvePiece. \
|
|
1227
|
+
Payout Group not found');
|
|
1228
|
+
return { index: payoutIndexOffset + index, groupLength };
|
|
1229
|
+
}
|
|
1230
|
+
async FindOutcomeIndexFromHyperbolaPayoutCurvePiece(_dlcOffer, contractDescriptor, contractOraclePairIndex, hyperbolaPayoutCurvePiece, oracleAttestation, outcome) {
|
|
1231
|
+
const { dlcOffer } = (0, Utils_1.checkTypes)({ _dlcOffer });
|
|
1232
|
+
const hyperbolaCurve = core_2.HyperbolaPayoutCurve.fromPayoutCurvePiece(hyperbolaPayoutCurvePiece);
|
|
1233
|
+
const clampBN = (val) => bignumber_js_1.default.max(0, bignumber_js_1.default.min(val, dlcOffer.contractInfo.totalCollateral.toString()));
|
|
1234
|
+
const payout = clampBN(hyperbolaCurve.getPayout(outcome));
|
|
1235
|
+
const payoutResponses = this.GetPayouts(dlcOffer);
|
|
1236
|
+
const payoutIndexOffset = this.GetIndicesFromPayouts(payoutResponses)[contractOraclePairIndex]
|
|
1237
|
+
.startingMessagesIndex;
|
|
1238
|
+
const { payoutGroups } = payoutResponses[contractOraclePairIndex];
|
|
1239
|
+
const intervalsSorted = [
|
|
1240
|
+
...contractDescriptor.roundingIntervals.intervals,
|
|
1241
|
+
].sort((a, b) => Number(b.beginInterval) - Number(a.beginInterval));
|
|
1242
|
+
const interval = intervalsSorted.find((interval) => Number(outcome) >= Number(interval.beginInterval));
|
|
1243
|
+
const roundedPayout = BigInt(clampBN(new bignumber_js_1.default((0, core_2.roundPayout)(payout, interval.roundingMod).toString())).toString());
|
|
1244
|
+
const outcomesFormatted = oracleAttestation.outcomes.map((outcome) => parseInt(outcome));
|
|
1245
|
+
let index = 0;
|
|
1246
|
+
let groupIndex = -1;
|
|
1247
|
+
let groupLength = 0;
|
|
1248
|
+
for (const [i, payoutGroup] of payoutGroups.entries()) {
|
|
1249
|
+
if (payoutGroup.payout === roundedPayout) {
|
|
1250
|
+
groupIndex = payoutGroup.groups.findIndex((group) => {
|
|
1251
|
+
return group.every((msg, i) => msg === outcomesFormatted[i]);
|
|
1252
|
+
});
|
|
1253
|
+
if (groupIndex !== -1) {
|
|
1254
|
+
index += groupIndex;
|
|
1255
|
+
groupLength = payoutGroup.groups[groupIndex].length;
|
|
1256
|
+
break;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
else if (payoutGroup.payout === BigInt(Math.round(Number(payout.toString()))) &&
|
|
1260
|
+
i !== 0) {
|
|
1261
|
+
// Edge case to account for case where payout is maximum payout for DLC
|
|
1262
|
+
// But rounded payout does not round down
|
|
1263
|
+
if (payoutGroups[i - 1].payout === roundedPayout) {
|
|
1264
|
+
// Ensure that the previous payout group causes index to be incremented
|
|
1265
|
+
index += payoutGroups[i - 1].groups.length;
|
|
1266
|
+
}
|
|
1267
|
+
groupIndex = payoutGroup.groups.findIndex((group) => {
|
|
1268
|
+
return group.every((msg, i) => msg === outcomesFormatted[i]);
|
|
1269
|
+
});
|
|
1270
|
+
if (groupIndex !== -1) {
|
|
1271
|
+
index += groupIndex;
|
|
1272
|
+
groupLength = payoutGroup.groups[groupIndex].length;
|
|
1273
|
+
break;
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
else {
|
|
1277
|
+
index += payoutGroup.groups.length;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
if (groupIndex === -1) {
|
|
1281
|
+
// Fallback to brute force search if payout-based search fails
|
|
1282
|
+
index = 0;
|
|
1283
|
+
groupLength = 0;
|
|
1284
|
+
for (const [, payoutGroup] of payoutGroups.entries()) {
|
|
1285
|
+
groupIndex = payoutGroup.groups.findIndex((group) => {
|
|
1286
|
+
return group.every((msg, j) => msg === outcomesFormatted[j]);
|
|
1287
|
+
});
|
|
1288
|
+
if (groupIndex !== -1) {
|
|
1289
|
+
index += groupIndex;
|
|
1290
|
+
groupLength = payoutGroup.groups[groupIndex].length;
|
|
1291
|
+
break;
|
|
1292
|
+
}
|
|
1293
|
+
else {
|
|
1294
|
+
index += payoutGroup.groups.length;
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
if (groupIndex === -1) {
|
|
1298
|
+
throw Error('Failed to Find OutcomeIndex From HyperbolaPayoutCurvePiece. \
|
|
1299
|
+
Payout Group not found even with brute force search');
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
return { index: payoutIndexOffset + index, groupLength };
|
|
1303
|
+
}
|
|
1304
|
+
async FindOutcomeIndex(dlcOffer, oracleAttestation) {
|
|
1305
|
+
const contractOraclePairs = this.GetContractOraclePairs(dlcOffer.contractInfo);
|
|
1306
|
+
const contractOraclePairIndex = contractOraclePairs.findIndex(({ oracleInfo }) => {
|
|
1307
|
+
if (oracleInfo.type !== messaging_1.MessageType.SingleOracleInfo)
|
|
1308
|
+
return false;
|
|
1309
|
+
const singleOracleInfo = oracleInfo;
|
|
1310
|
+
return (singleOracleInfo.announcement.oracleEvent.eventId ===
|
|
1311
|
+
oracleAttestation.eventId);
|
|
1312
|
+
});
|
|
1313
|
+
(0, assert_1.default)(contractOraclePairIndex !== -1, 'OracleAttestation must be for an existing OracleEvent');
|
|
1314
|
+
const contractOraclePair = contractOraclePairs[contractOraclePairIndex];
|
|
1315
|
+
const { contractDescriptor: _contractDescriptor, oracleInfo } = contractOraclePair;
|
|
1316
|
+
(0, assert_1.default)(_contractDescriptor.contractDescriptorType ===
|
|
1317
|
+
messaging_1.ContractDescriptorType.NumericOutcome, 'ContractDescriptor must be NumericOutcome');
|
|
1318
|
+
const contractDescriptor = _contractDescriptor;
|
|
1319
|
+
const _payoutFunction = contractDescriptor.payoutFunction;
|
|
1320
|
+
(0, assert_1.default)(_payoutFunction.type === messaging_1.MessageType.PayoutFunction, 'PayoutFunction must be V0');
|
|
1321
|
+
if (oracleInfo.type !== messaging_1.MessageType.SingleOracleInfo) {
|
|
1322
|
+
throw new Error('Only SingleOracleInfo supported in this context');
|
|
1323
|
+
}
|
|
1324
|
+
const singleOracleInfo = oracleInfo;
|
|
1325
|
+
const eventDescriptor = singleOracleInfo.announcement.oracleEvent
|
|
1326
|
+
.eventDescriptor;
|
|
1327
|
+
const payoutFunction = _payoutFunction;
|
|
1328
|
+
const base = eventDescriptor.base;
|
|
1329
|
+
const outcome = [...oracleAttestation.outcomes]
|
|
1330
|
+
.reverse()
|
|
1331
|
+
.reduce((acc, val, i) => acc + Number(val) * base ** i, 0);
|
|
1332
|
+
const piecesSorted = payoutFunction.payoutFunctionPieces.sort((a, b) => Number(a.endPoint.eventOutcome) - Number(b.endPoint.eventOutcome));
|
|
1333
|
+
const piece = piecesSorted.find((piece) => outcome < piece.endPoint.eventOutcome);
|
|
1334
|
+
switch (piece.payoutCurvePiece.type) {
|
|
1335
|
+
case messaging_1.MessageType.PolynomialPayoutCurvePiece:
|
|
1336
|
+
return this.FindOutcomeIndexFromPolynomialPayoutCurvePiece(dlcOffer, contractDescriptor, contractOraclePairIndex, piece.payoutCurvePiece, oracleAttestation, BigInt(outcome));
|
|
1337
|
+
case messaging_1.MessageType.HyperbolaPayoutCurvePiece:
|
|
1338
|
+
return this.FindOutcomeIndexFromHyperbolaPayoutCurvePiece(dlcOffer, contractDescriptor, contractOraclePairIndex, piece.payoutCurvePiece, oracleAttestation, BigInt(outcome));
|
|
1339
|
+
case messaging_1.MessageType.OldHyperbolaPayoutCurvePiece:
|
|
1340
|
+
return this.FindOutcomeIndexFromHyperbolaPayoutCurvePiece(dlcOffer, contractDescriptor, contractOraclePairIndex, piece.payoutCurvePiece, oracleAttestation, BigInt(outcome));
|
|
1341
|
+
default:
|
|
1342
|
+
throw Error('Must be Hyperbola or Polynomial curve piece');
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
ValidateEvent(dlcOffer, oracleAttestation) {
|
|
1346
|
+
switch (dlcOffer.contractInfo.contractInfoType) {
|
|
1347
|
+
case messaging_1.ContractInfoType.Single: {
|
|
1348
|
+
const contractInfo = dlcOffer.contractInfo;
|
|
1349
|
+
switch (contractInfo.contractDescriptor.contractDescriptorType) {
|
|
1350
|
+
case messaging_1.ContractDescriptorType.Enumerated: {
|
|
1351
|
+
const oracleInfo = contractInfo.oracleInfo;
|
|
1352
|
+
if (oracleInfo.type !== messaging_1.MessageType.SingleOracleInfo) {
|
|
1353
|
+
throw Error('Only SingleOracleInfo supported in this context');
|
|
1354
|
+
}
|
|
1355
|
+
const singleOracleInfo = oracleInfo;
|
|
1356
|
+
if (singleOracleInfo.announcement.oracleEvent.eventId !==
|
|
1357
|
+
oracleAttestation.eventId)
|
|
1358
|
+
throw Error('Incorrect Oracle Attestation. Event Id must match.');
|
|
1359
|
+
break;
|
|
1360
|
+
}
|
|
1361
|
+
case messaging_1.ContractDescriptorType.NumericOutcome: {
|
|
1362
|
+
const oracleInfo = contractInfo.oracleInfo;
|
|
1363
|
+
if (oracleInfo.type !== messaging_1.MessageType.SingleOracleInfo) {
|
|
1364
|
+
throw Error('Only SingleOracleInfo supported in this context');
|
|
1365
|
+
}
|
|
1366
|
+
const singleOracleInfo = oracleInfo;
|
|
1367
|
+
if (singleOracleInfo.announcement.oracleEvent.eventId !==
|
|
1368
|
+
oracleAttestation.eventId)
|
|
1369
|
+
throw Error('Incorrect Oracle Attestation. Event Id must match.');
|
|
1370
|
+
break;
|
|
1371
|
+
}
|
|
1372
|
+
default:
|
|
1373
|
+
throw Error('ConractDescriptor must be V0 or V1');
|
|
1374
|
+
}
|
|
1375
|
+
break;
|
|
1376
|
+
}
|
|
1377
|
+
case messaging_1.ContractInfoType.Disjoint: {
|
|
1378
|
+
const contractInfo = dlcOffer.contractInfo;
|
|
1379
|
+
const attestedOracleEvent = contractInfo.contractOraclePairs.find(({ oracleInfo }) => {
|
|
1380
|
+
if (oracleInfo.type !== messaging_1.MessageType.SingleOracleInfo)
|
|
1381
|
+
return false;
|
|
1382
|
+
const singleOracleInfo = oracleInfo;
|
|
1383
|
+
return (singleOracleInfo.announcement.oracleEvent.eventId ===
|
|
1384
|
+
oracleAttestation.eventId);
|
|
1385
|
+
});
|
|
1386
|
+
if (!attestedOracleEvent)
|
|
1387
|
+
throw Error('Oracle event of attestation not found.');
|
|
1388
|
+
break;
|
|
1389
|
+
}
|
|
1390
|
+
default:
|
|
1391
|
+
throw Error('ContractInfo must be V0 or V1');
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
async FindAndSignCet(dlcOffer, dlcAccept, dlcSign, dlcTxs, oracleAttestation, isOfferer) {
|
|
1395
|
+
if (isOfferer === undefined)
|
|
1396
|
+
isOfferer = await this.isOfferer(dlcOffer, dlcAccept);
|
|
1397
|
+
const fundPrivateKey = await this.GetFundPrivateKey(dlcOffer, dlcAccept, isOfferer);
|
|
1398
|
+
let finalCet;
|
|
1399
|
+
if (dlcOffer.contractInfo.contractInfoType === messaging_1.ContractInfoType.Single &&
|
|
1400
|
+
dlcOffer.contractInfo.contractDescriptor
|
|
1401
|
+
.contractDescriptorType === messaging_1.ContractDescriptorType.Enumerated) {
|
|
1402
|
+
const contractDescriptor = dlcOffer.contractInfo
|
|
1403
|
+
.contractDescriptor;
|
|
1404
|
+
// Handle different contract descriptor outcome formats
|
|
1405
|
+
const attestedOutcome = oracleAttestation.outcomes[0];
|
|
1406
|
+
const outcomeIndex = contractDescriptor.outcomes.findIndex((outcome) => {
|
|
1407
|
+
// Try direct string match first (for DDK2 test: '1', '2', '3')
|
|
1408
|
+
if (outcome.outcome === attestedOutcome) {
|
|
1409
|
+
return true;
|
|
1410
|
+
}
|
|
1411
|
+
// Try sha256 hash match (for other tests with hashed outcomes)
|
|
1412
|
+
const attestedOutcomeHash = (0, crypto_1.sha256)(Buffer.from(attestedOutcome, 'utf8')).toString('hex');
|
|
1413
|
+
return outcome.outcome === attestedOutcomeHash;
|
|
1414
|
+
});
|
|
1415
|
+
finalCet = this._ddk
|
|
1416
|
+
.signCet(this.convertTxToDdkTransaction(dlcTxs.cets[outcomeIndex]), isOfferer
|
|
1417
|
+
? dlcAccept.cetAdaptorSignatures.sigs[outcomeIndex].encryptedSig
|
|
1418
|
+
: dlcSign.cetAdaptorSignatures.sigs[outcomeIndex].encryptedSig, oracleAttestation.signatures, Buffer.from(fundPrivateKey, 'hex'), isOfferer ? dlcAccept.fundingPubkey : dlcOffer.fundingPubkey, isOfferer ? dlcOffer.fundingPubkey : dlcAccept.fundingPubkey, this.getFundOutputValueSats(dlcTxs))
|
|
1419
|
+
.rawBytes.toString('hex');
|
|
1420
|
+
}
|
|
1421
|
+
else {
|
|
1422
|
+
const { index: outcomeIndex, groupLength } = await this.FindOutcomeIndex(dlcOffer, oracleAttestation);
|
|
1423
|
+
const sliceIndex = -(oracleAttestation.signatures.length - groupLength);
|
|
1424
|
+
const oracleSignatures = sliceIndex === 0
|
|
1425
|
+
? oracleAttestation.signatures
|
|
1426
|
+
: oracleAttestation.signatures.slice(0, sliceIndex);
|
|
1427
|
+
finalCet = this._ddk
|
|
1428
|
+
.signCet(this.convertTxToDdkTransaction(dlcTxs.cets[outcomeIndex]), isOfferer
|
|
1429
|
+
? dlcAccept.cetAdaptorSignatures.sigs[outcomeIndex].encryptedSig
|
|
1430
|
+
: dlcSign.cetAdaptorSignatures.sigs[outcomeIndex].encryptedSig, oracleSignatures, Buffer.from(fundPrivateKey, 'hex'), isOfferer ? dlcAccept.fundingPubkey : dlcOffer.fundingPubkey, isOfferer ? dlcOffer.fundingPubkey : dlcAccept.fundingPubkey, this.getFundOutputValueSats(dlcTxs))
|
|
1431
|
+
.rawBytes.toString('hex');
|
|
1432
|
+
}
|
|
1433
|
+
// const finalCet = (await this.SignCet(signCetRequest)).hex;
|
|
1434
|
+
return bitcoin_1.Tx.decode(bufio_1.StreamReader.fromHex(finalCet));
|
|
1435
|
+
}
|
|
1436
|
+
async GetFundAddress(dlcOffer, dlcAccept, isOfferer) {
|
|
1437
|
+
const network = await this.getConnectedNetwork();
|
|
1438
|
+
const fundingSPK = bitcoin_1.Script.p2wpkhLock((0, crypto_1.hash160)(isOfferer ? dlcOffer.fundingPubkey : dlcAccept.fundingPubkey))
|
|
1439
|
+
.serialize()
|
|
1440
|
+
.slice(1);
|
|
1441
|
+
const fundingAddress = bitcoinjs_lib_1.address.fromOutputScript(fundingSPK, network);
|
|
1442
|
+
return fundingAddress;
|
|
1443
|
+
}
|
|
1444
|
+
async GetFundKeyPair(dlcOffer, dlcAccept, isOfferer) {
|
|
1445
|
+
const fundingAddress = await this.GetFundAddress(dlcOffer, dlcAccept, isOfferer);
|
|
1446
|
+
const { derivationPath } = await this.getMethod('getWalletAddress')(fundingAddress);
|
|
1447
|
+
const keyPair = await this.getMethod('keyPair')(derivationPath);
|
|
1448
|
+
return keyPair;
|
|
1449
|
+
}
|
|
1450
|
+
async GetFundPrivateKey(dlcOffer, dlcAccept, isOfferer) {
|
|
1451
|
+
const fundPrivateKeyPair = await this.GetFundKeyPair(dlcOffer, dlcAccept, isOfferer);
|
|
1452
|
+
return Buffer.from(fundPrivateKeyPair.privateKey).toString('hex');
|
|
1453
|
+
}
|
|
1454
|
+
async CreateCloseRawTxs(dlcOffer, dlcAccept, dlcTxs, closeInputAmount, isOfferer, _dlcCloses = [], fundingInputs, initiatorPayouts) {
|
|
1455
|
+
const network = await this.getConnectedNetwork();
|
|
1456
|
+
let finalizer;
|
|
1457
|
+
if (_dlcCloses.length === 0) {
|
|
1458
|
+
finalizer = new core_2.DualClosingTxFinalizer(fundingInputs, dlcOffer.payoutSpk, dlcAccept.payoutSpk, dlcOffer.feeRatePerVb);
|
|
1459
|
+
}
|
|
1460
|
+
const rawTransactionRequestPromises = [];
|
|
1461
|
+
const rawCloseTxs = [];
|
|
1462
|
+
const numPayouts = _dlcCloses.length === 0 ? initiatorPayouts.length : _dlcCloses.length;
|
|
1463
|
+
for (let i = 0; i < numPayouts; i++) {
|
|
1464
|
+
let offerPayoutValue = BigInt(0);
|
|
1465
|
+
let acceptPayoutValue = BigInt(0);
|
|
1466
|
+
if (_dlcCloses.length === 0) {
|
|
1467
|
+
const payout = initiatorPayouts[i];
|
|
1468
|
+
const payoutMinusOfferFees = finalizer.offerInitiatorFees > payout
|
|
1469
|
+
? BigInt(0)
|
|
1470
|
+
: payout - finalizer.offerInitiatorFees;
|
|
1471
|
+
const collateralMinusPayout = payout > dlcOffer.contractInfo.totalCollateral
|
|
1472
|
+
? BigInt(0)
|
|
1473
|
+
: dlcOffer.contractInfo.totalCollateral - payout;
|
|
1474
|
+
offerPayoutValue = isOfferer
|
|
1475
|
+
? closeInputAmount + payoutMinusOfferFees
|
|
1476
|
+
: collateralMinusPayout;
|
|
1477
|
+
acceptPayoutValue = isOfferer
|
|
1478
|
+
? collateralMinusPayout
|
|
1479
|
+
: closeInputAmount + payoutMinusOfferFees;
|
|
1480
|
+
}
|
|
1481
|
+
else {
|
|
1482
|
+
const dlcClose = (0, Utils_1.checkTypes)({ _dlcClose: _dlcCloses[i] }).dlcClose;
|
|
1483
|
+
offerPayoutValue = dlcClose.offerPayoutSatoshis;
|
|
1484
|
+
acceptPayoutValue = dlcClose.acceptPayoutSatoshis;
|
|
1485
|
+
}
|
|
1486
|
+
const txOuts = [];
|
|
1487
|
+
if (Number(offerPayoutValue) > 0) {
|
|
1488
|
+
txOuts.push({
|
|
1489
|
+
address: bitcoinjs_lib_1.address.fromOutputScript(dlcOffer.payoutSpk, network),
|
|
1490
|
+
amount: Number(offerPayoutValue),
|
|
1491
|
+
});
|
|
1492
|
+
}
|
|
1493
|
+
if (Number(acceptPayoutValue) > 0) {
|
|
1494
|
+
txOuts.push({
|
|
1495
|
+
address: bitcoinjs_lib_1.address.fromOutputScript(dlcAccept.payoutSpk, network),
|
|
1496
|
+
amount: Number(acceptPayoutValue),
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
if (dlcOffer.payoutSerialId > dlcAccept.payoutSerialId)
|
|
1500
|
+
txOuts.reverse();
|
|
1501
|
+
const rawTransactionRequest = {
|
|
1502
|
+
version: 2,
|
|
1503
|
+
locktime: 0,
|
|
1504
|
+
txins: [
|
|
1505
|
+
{
|
|
1506
|
+
txid: dlcTxs.fundTx.txId.serialize().reverse().toString('hex'),
|
|
1507
|
+
vout: dlcTxs.fundTxVout,
|
|
1508
|
+
sequence: 0,
|
|
1509
|
+
},
|
|
1510
|
+
],
|
|
1511
|
+
txouts: txOuts,
|
|
1512
|
+
};
|
|
1513
|
+
rawTransactionRequestPromises.push((async () => {
|
|
1514
|
+
const response = await this.getMethod('CreateRawTransaction')(rawTransactionRequest);
|
|
1515
|
+
return response.hex;
|
|
1516
|
+
})());
|
|
1517
|
+
}
|
|
1518
|
+
const hexs = await Promise.all(rawTransactionRequestPromises);
|
|
1519
|
+
rawCloseTxs.push(hexs);
|
|
1520
|
+
return rawCloseTxs.flat();
|
|
1521
|
+
}
|
|
1522
|
+
async CreateSignatureHashes(_dlcOffer, _dlcAccept, _dlcTxs, rawCloseTxs) {
|
|
1523
|
+
const { dlcOffer, dlcAccept, dlcTxs } = (0, Utils_1.checkTypes)({
|
|
1524
|
+
_dlcOffer,
|
|
1525
|
+
_dlcAccept,
|
|
1526
|
+
_dlcTxs,
|
|
1527
|
+
});
|
|
1528
|
+
const network = await this.getConnectedNetwork();
|
|
1529
|
+
const fundingPubKeys = Buffer.compare(dlcOffer.fundingPubkey, dlcAccept.fundingPubkey) === -1
|
|
1530
|
+
? [dlcOffer.fundingPubkey, dlcAccept.fundingPubkey]
|
|
1531
|
+
: [dlcAccept.fundingPubkey, dlcOffer.fundingPubkey];
|
|
1532
|
+
const p2ms = bitcoinjs_lib_1.payments.p2ms({
|
|
1533
|
+
m: 2,
|
|
1534
|
+
pubkeys: fundingPubKeys,
|
|
1535
|
+
network,
|
|
1536
|
+
});
|
|
1537
|
+
const paymentVariant = bitcoinjs_lib_1.payments.p2wsh({
|
|
1538
|
+
redeem: p2ms,
|
|
1539
|
+
network,
|
|
1540
|
+
});
|
|
1541
|
+
const sigHashRequestPromises = [];
|
|
1542
|
+
const sigHashes = [];
|
|
1543
|
+
for (let i = 0; i < rawCloseTxs.length; i++) {
|
|
1544
|
+
const rawTx = rawCloseTxs[i];
|
|
1545
|
+
const sigHashRequest = {
|
|
1546
|
+
tx: rawTx,
|
|
1547
|
+
txin: {
|
|
1548
|
+
txid: dlcTxs.fundTx.txId.serialize().reverse().toString('hex'),
|
|
1549
|
+
vout: dlcTxs.fundTxVout,
|
|
1550
|
+
keyData: {
|
|
1551
|
+
hex: paymentVariant.redeem.output.toString('hex'),
|
|
1552
|
+
type: 'redeem_script',
|
|
1553
|
+
},
|
|
1554
|
+
amount: Number(this.getFundOutputValueSats(dlcTxs)),
|
|
1555
|
+
hashType: 'p2wsh',
|
|
1556
|
+
sighashType: 'all',
|
|
1557
|
+
sighashAnyoneCanPay: false,
|
|
1558
|
+
},
|
|
1559
|
+
};
|
|
1560
|
+
sigHashRequestPromises.push((async () => {
|
|
1561
|
+
const response = await this.getMethod('CreateSignatureHash')(sigHashRequest);
|
|
1562
|
+
return response.sighash;
|
|
1563
|
+
})());
|
|
1564
|
+
}
|
|
1565
|
+
const sighashes = await Promise.all(sigHashRequestPromises);
|
|
1566
|
+
sigHashes.push(sighashes);
|
|
1567
|
+
return sigHashes.flat();
|
|
1568
|
+
}
|
|
1569
|
+
async CalculateEcSignatureHashes(sigHashes, privKey) {
|
|
1570
|
+
const cfdNetwork = await this.GetCfdNetwork();
|
|
1571
|
+
const sigsRequestPromises = [];
|
|
1572
|
+
for (let i = 0; i < sigHashes.length; i++) {
|
|
1573
|
+
const sigHash = sigHashes[i];
|
|
1574
|
+
const calculateEcSignatureRequest = {
|
|
1575
|
+
sighash: sigHash,
|
|
1576
|
+
privkeyData: {
|
|
1577
|
+
privkey: privKey,
|
|
1578
|
+
wif: false,
|
|
1579
|
+
network: cfdNetwork,
|
|
1580
|
+
},
|
|
1581
|
+
isGrindR: true,
|
|
1582
|
+
};
|
|
1583
|
+
sigsRequestPromises.push((async () => {
|
|
1584
|
+
const response = await this.getMethod('CalculateEcSignature')(calculateEcSignatureRequest);
|
|
1585
|
+
return response.signature;
|
|
1586
|
+
})());
|
|
1587
|
+
}
|
|
1588
|
+
const sigs = await Promise.all(sigsRequestPromises);
|
|
1589
|
+
return sigs.flat();
|
|
1590
|
+
}
|
|
1591
|
+
async VerifySignatures(_dlcOffer, _dlcAccept, _dlcTxs, _dlcCloses, rawCloseTxs, isOfferer) {
|
|
1592
|
+
const { dlcOffer, dlcAccept, dlcTxs } = (0, Utils_1.checkTypes)({
|
|
1593
|
+
_dlcOffer,
|
|
1594
|
+
_dlcAccept,
|
|
1595
|
+
_dlcTxs,
|
|
1596
|
+
});
|
|
1597
|
+
const dlcCloses = _dlcCloses.map((_dlcClose) => (0, Utils_1.checkTypes)({ _dlcClose }).dlcClose);
|
|
1598
|
+
const network = await this.getConnectedNetwork();
|
|
1599
|
+
const fundingPubKeys = Buffer.compare(dlcOffer.fundingPubkey, dlcAccept.fundingPubkey) === -1
|
|
1600
|
+
? [dlcOffer.fundingPubkey, dlcAccept.fundingPubkey]
|
|
1601
|
+
: [dlcAccept.fundingPubkey, dlcOffer.fundingPubkey];
|
|
1602
|
+
const p2ms = bitcoinjs_lib_1.payments.p2ms({
|
|
1603
|
+
m: 2,
|
|
1604
|
+
pubkeys: fundingPubKeys,
|
|
1605
|
+
network,
|
|
1606
|
+
});
|
|
1607
|
+
const paymentVariant = bitcoinjs_lib_1.payments.p2wsh({
|
|
1608
|
+
redeem: p2ms,
|
|
1609
|
+
network,
|
|
1610
|
+
});
|
|
1611
|
+
const pubkey = isOfferer ? dlcAccept.fundingPubkey : dlcOffer.fundingPubkey;
|
|
1612
|
+
const sigsValidity = [];
|
|
1613
|
+
for (let i = 0; i < rawCloseTxs.length; i++) {
|
|
1614
|
+
const rawTx = rawCloseTxs[i];
|
|
1615
|
+
const dlcClose = dlcCloses[i];
|
|
1616
|
+
const verifySignatureRequest = {
|
|
1617
|
+
tx: rawTx,
|
|
1618
|
+
txin: {
|
|
1619
|
+
txid: dlcTxs.fundTx.txId.serialize().reverse().toString('hex'),
|
|
1620
|
+
vout: dlcTxs.fundTxVout,
|
|
1621
|
+
signature: dlcClose.closeSignature.toString('hex'),
|
|
1622
|
+
pubkey: pubkey.toString('hex'),
|
|
1623
|
+
redeemScript: paymentVariant.redeem.output.toString('hex'),
|
|
1624
|
+
hashType: 'p2wsh',
|
|
1625
|
+
sighashType: 'all',
|
|
1626
|
+
sighashAnyoneCanPay: false,
|
|
1627
|
+
amount: Number(this.getFundOutputValueSats(dlcTxs)),
|
|
1628
|
+
},
|
|
1629
|
+
};
|
|
1630
|
+
sigsValidity.push((async () => {
|
|
1631
|
+
const response = await this.getMethod('VerifySignature')(verifySignatureRequest);
|
|
1632
|
+
return response.success;
|
|
1633
|
+
})());
|
|
1634
|
+
}
|
|
1635
|
+
const areSigsValid = (await Promise.all(sigsValidity)).every((b) => b);
|
|
1636
|
+
return areSigsValid;
|
|
1637
|
+
}
|
|
1638
|
+
/**
|
|
1639
|
+
* Check whether wallet is offerer of DlcOffer or DlcAccept
|
|
1640
|
+
* @param dlcOffer Dlc Offer Message
|
|
1641
|
+
* @param dlcAccept Dlc Accept Message
|
|
1642
|
+
* @returns {Promise<boolean>}
|
|
1643
|
+
*/
|
|
1644
|
+
async isOfferer(_dlcOffer, _dlcAccept) {
|
|
1645
|
+
const { dlcOffer, dlcAccept } = (0, Utils_1.checkTypes)({
|
|
1646
|
+
_dlcOffer,
|
|
1647
|
+
_dlcAccept,
|
|
1648
|
+
});
|
|
1649
|
+
const network = await this.getConnectedNetwork();
|
|
1650
|
+
const offerFundingSPK = bitcoin_1.Script.p2wpkhLock((0, crypto_1.hash160)(dlcOffer.fundingPubkey))
|
|
1651
|
+
.serialize()
|
|
1652
|
+
.slice(1);
|
|
1653
|
+
const acceptFundingSPK = bitcoin_1.Script.p2wpkhLock((0, crypto_1.hash160)(dlcAccept.fundingPubkey))
|
|
1654
|
+
.serialize()
|
|
1655
|
+
.slice(1);
|
|
1656
|
+
const offerFundingAddress = bitcoinjs_lib_1.address.fromOutputScript(offerFundingSPK, network);
|
|
1657
|
+
const acceptFundingAddress = bitcoinjs_lib_1.address.fromOutputScript(acceptFundingSPK, network);
|
|
1658
|
+
let walletAddress = await this.client.wallet.findAddress([
|
|
1659
|
+
offerFundingAddress,
|
|
1660
|
+
]);
|
|
1661
|
+
if (walletAddress)
|
|
1662
|
+
return true;
|
|
1663
|
+
walletAddress = await this.client.wallet.findAddress([
|
|
1664
|
+
acceptFundingAddress,
|
|
1665
|
+
]);
|
|
1666
|
+
if (walletAddress)
|
|
1667
|
+
return false;
|
|
1668
|
+
throw Error('Wallet Address not found for DlcOffer or DlcAccept');
|
|
1669
|
+
}
|
|
1670
|
+
/**
|
|
1671
|
+
* Create DLC Offer Message
|
|
1672
|
+
* @param contractInfo ContractInfo TLV (V0 or V1)
|
|
1673
|
+
* @param offerCollateralSatoshis Amount DLC Initiator is putting into the contract
|
|
1674
|
+
* @param feeRatePerVb Fee rate in satoshi per virtual byte that both sides use to compute fees in funding tx
|
|
1675
|
+
* @param cetLocktime The nLockTime to be put on CETs
|
|
1676
|
+
* @param refundLocktime The nLockTime to be put on the refund transaction
|
|
1677
|
+
* @param fixedInputs Optional fixed inputs - can be Input[] for regular inputs or FundingInput[] for DLC inputs
|
|
1678
|
+
* @returns {Promise<DlcOffer>}
|
|
1679
|
+
*/
|
|
1680
|
+
async createDlcOffer(contractInfo, offerCollateralSatoshis, feeRatePerVb, cetLocktime, refundLocktime, fixedInputs, inputSupplementationMode) {
|
|
1681
|
+
contractInfo.validate();
|
|
1682
|
+
const network = await this.getConnectedNetwork();
|
|
1683
|
+
const dlcOffer = new messaging_1.DlcOffer();
|
|
1684
|
+
// Generate a random 32-byte temporary contract ID
|
|
1685
|
+
dlcOffer.temporaryContractId = crypto_2.default.randomBytes(32);
|
|
1686
|
+
// Check if we have FundingInput[] (DLC inputs) or Input[] (regular inputs)
|
|
1687
|
+
const hasFundingInputs = fixedInputs && fixedInputs.length > 0 && 'prevTx' in fixedInputs[0]; // FundingInput has prevTx, Input doesn't
|
|
1688
|
+
let fundingPubKey;
|
|
1689
|
+
let payoutSPK;
|
|
1690
|
+
let payoutSerialId;
|
|
1691
|
+
let _fundingInputs;
|
|
1692
|
+
let changeSPK;
|
|
1693
|
+
let changeSerialId;
|
|
1694
|
+
if (hasFundingInputs) {
|
|
1695
|
+
// Handle FundingInput[] directly (for DLC inputs)
|
|
1696
|
+
const fundingInputs = fixedInputs;
|
|
1697
|
+
// Generate addresses directly since we're bypassing Initialize()
|
|
1698
|
+
const payoutAddress = await this.client.wallet.getUnusedAddress(false);
|
|
1699
|
+
payoutSPK = bitcoinjs_lib_1.address.toOutputScript(payoutAddress.address, network);
|
|
1700
|
+
const changeAddress = await this.client.wallet.getUnusedAddress(true);
|
|
1701
|
+
changeSPK = bitcoinjs_lib_1.address.toOutputScript(changeAddress.address, network);
|
|
1702
|
+
const fundingAddress = await this.client.wallet.getUnusedAddress(false);
|
|
1703
|
+
fundingPubKey = Buffer.from(fundingAddress.publicKey, 'hex');
|
|
1704
|
+
if (fundingAddress.address === payoutAddress.address)
|
|
1705
|
+
throw Error('Address reuse');
|
|
1706
|
+
payoutSerialId = (0, Utils_1.generateSerialId)();
|
|
1707
|
+
changeSerialId = (0, Utils_1.generateSerialId)();
|
|
1708
|
+
_fundingInputs = fundingInputs;
|
|
1709
|
+
}
|
|
1710
|
+
else {
|
|
1711
|
+
// Handle Input[] through existing Initialize() flow
|
|
1712
|
+
const initResult = await this.Initialize(offerCollateralSatoshis, feeRatePerVb, fixedInputs, inputSupplementationMode || types_1.InputSupplementationMode.Required);
|
|
1713
|
+
fundingPubKey = initResult.fundingPubKey;
|
|
1714
|
+
payoutSPK = initResult.payoutSPK;
|
|
1715
|
+
payoutSerialId = initResult.payoutSerialId;
|
|
1716
|
+
_fundingInputs = initResult.fundingInputs;
|
|
1717
|
+
changeSPK = initResult.changeSPK;
|
|
1718
|
+
changeSerialId = initResult.changeSerialId;
|
|
1719
|
+
}
|
|
1720
|
+
_fundingInputs.forEach((input) => (0, assert_1.default)(input.type === messaging_1.MessageType.FundingInput, 'FundingInput must be V0'));
|
|
1721
|
+
const fundingInputs = _fundingInputs.map((input) => input);
|
|
1722
|
+
fundingInputs.sort((a, b) => Number(a.inputSerialId) - Number(b.inputSerialId));
|
|
1723
|
+
const fundOutputSerialId = (0, Utils_1.generateSerialId)();
|
|
1724
|
+
(0, assert_1.default)(changeSerialId !== fundOutputSerialId, 'changeSerialId cannot equal the fundOutputSerialId');
|
|
1725
|
+
dlcOffer.contractFlags = Buffer.from('00', 'hex');
|
|
1726
|
+
dlcOffer.chainHash = (0, bitcoin_networks_1.chainHashFromNetwork)(network);
|
|
1727
|
+
dlcOffer.contractInfo = contractInfo;
|
|
1728
|
+
dlcOffer.fundingPubkey = fundingPubKey;
|
|
1729
|
+
dlcOffer.payoutSpk = payoutSPK;
|
|
1730
|
+
dlcOffer.payoutSerialId = payoutSerialId;
|
|
1731
|
+
dlcOffer.offerCollateral = offerCollateralSatoshis;
|
|
1732
|
+
dlcOffer.fundingInputs = fundingInputs;
|
|
1733
|
+
dlcOffer.changeSpk = changeSPK;
|
|
1734
|
+
dlcOffer.changeSerialId = changeSerialId;
|
|
1735
|
+
dlcOffer.fundOutputSerialId = dlcOffer.fundOutputSerialId =
|
|
1736
|
+
fundOutputSerialId;
|
|
1737
|
+
dlcOffer.feeRatePerVb = feeRatePerVb;
|
|
1738
|
+
dlcOffer.cetLocktime = cetLocktime;
|
|
1739
|
+
dlcOffer.refundLocktime = refundLocktime;
|
|
1740
|
+
if (offerCollateralSatoshis === dlcOffer.contractInfo.totalCollateral) {
|
|
1741
|
+
dlcOffer.markAsSingleFunded();
|
|
1742
|
+
}
|
|
1743
|
+
(0, assert_1.default)((() => {
|
|
1744
|
+
const finalizer = new core_2.DualFundingTxFinalizer(dlcOffer.fundingInputs, dlcOffer.payoutSpk, dlcOffer.changeSpk, null, null, null, dlcOffer.feeRatePerVb);
|
|
1745
|
+
const funding = fundingInputs.reduce((total, input) => {
|
|
1746
|
+
return total + input.prevTx.outputs[input.prevTxVout].value.sats;
|
|
1747
|
+
}, BigInt(0));
|
|
1748
|
+
return funding >= offerCollateralSatoshis + finalizer.offerFees;
|
|
1749
|
+
})(), 'fundingInputs for dlcOffer must be greater than offerCollateralSatoshis plus offerFees');
|
|
1750
|
+
dlcOffer.validate();
|
|
1751
|
+
return dlcOffer;
|
|
1752
|
+
}
|
|
1753
|
+
/**
|
|
1754
|
+
* Accept DLC Offer (supports single-funded DLCs when accept collateral is 0)
|
|
1755
|
+
* @param _dlcOffer Dlc Offer Message
|
|
1756
|
+
* @param fixedInputs Optional inputs to use for Funding Inputs
|
|
1757
|
+
* @returns {Promise<AcceptDlcOfferResponse}
|
|
1758
|
+
*/
|
|
1759
|
+
async acceptDlcOffer(_dlcOffer, fixedInputs) {
|
|
1760
|
+
const { dlcOffer } = (0, Utils_1.checkTypes)({ _dlcOffer });
|
|
1761
|
+
dlcOffer.validate();
|
|
1762
|
+
const acceptCollateralSatoshis = dlcOffer.contractInfo.totalCollateral - dlcOffer.offerCollateral;
|
|
1763
|
+
(0, assert_1.default)(acceptCollateralSatoshis ===
|
|
1764
|
+
dlcOffer.contractInfo.totalCollateral - dlcOffer.offerCollateral, 'acceptCollaterialSatoshis should equal totalCollateral - offerCollateralSatoshis');
|
|
1765
|
+
let fundingPubKey;
|
|
1766
|
+
let payoutSPK;
|
|
1767
|
+
let payoutSerialId;
|
|
1768
|
+
let fundingInputs;
|
|
1769
|
+
let changeSPK;
|
|
1770
|
+
let changeSerialId;
|
|
1771
|
+
if (acceptCollateralSatoshis === BigInt(0)) {
|
|
1772
|
+
// Single-funded DLC: accept side provides no funding
|
|
1773
|
+
const network = await this.getConnectedNetwork();
|
|
1774
|
+
// Still need payout address for receiving DLC outcomes
|
|
1775
|
+
const payoutAddress = await this.client.wallet.getUnusedAddress(false);
|
|
1776
|
+
payoutSPK = bitcoinjs_lib_1.address.toOutputScript(payoutAddress.address, network);
|
|
1777
|
+
// Generate funding pubkey for DLC contract construction
|
|
1778
|
+
const fundingAddress = await this.client.wallet.getUnusedAddress(false);
|
|
1779
|
+
fundingPubKey = Buffer.from(fundingAddress.publicKey, 'hex');
|
|
1780
|
+
// Generate change address (even though not used)
|
|
1781
|
+
const changeAddress = await this.client.wallet.getUnusedAddress(true);
|
|
1782
|
+
changeSPK = bitcoinjs_lib_1.address.toOutputScript(changeAddress.address, network);
|
|
1783
|
+
if (fundingAddress.address === payoutAddress.address)
|
|
1784
|
+
throw Error('Address reuse');
|
|
1785
|
+
// Generate serial IDs
|
|
1786
|
+
payoutSerialId = (0, Utils_1.generateSerialId)();
|
|
1787
|
+
changeSerialId = (0, Utils_1.generateSerialId)();
|
|
1788
|
+
// No funding inputs for single-funded DLC
|
|
1789
|
+
fundingInputs = [];
|
|
1790
|
+
}
|
|
1791
|
+
else {
|
|
1792
|
+
// Standard DLC: accept side provides funding
|
|
1793
|
+
// Check if we have FundingInput[] (DLC inputs) or Input[] (regular inputs)
|
|
1794
|
+
const hasFundingInputs = fixedInputs && fixedInputs.length > 0 && 'prevTx' in fixedInputs[0]; // FundingInput has prevTx, Input doesn't
|
|
1795
|
+
let initResult;
|
|
1796
|
+
if (hasFundingInputs) {
|
|
1797
|
+
// Handle FundingInput[] directly (for DLC inputs)
|
|
1798
|
+
const fundingInputs = fixedInputs;
|
|
1799
|
+
const network = await this.getConnectedNetwork();
|
|
1800
|
+
// Generate addresses directly since we're bypassing Initialize()
|
|
1801
|
+
const payoutAddress = await this.client.wallet.getUnusedAddress(false);
|
|
1802
|
+
const payoutSPK = bitcoinjs_lib_1.address.toOutputScript(payoutAddress.address, network);
|
|
1803
|
+
const changeAddress = await this.client.wallet.getUnusedAddress(true);
|
|
1804
|
+
const changeSPK = bitcoinjs_lib_1.address.toOutputScript(changeAddress.address, network);
|
|
1805
|
+
const fundingAddress = await this.client.wallet.getUnusedAddress(false);
|
|
1806
|
+
const fundingPubKey = Buffer.from(fundingAddress.publicKey, 'hex');
|
|
1807
|
+
if (fundingAddress.address === payoutAddress.address)
|
|
1808
|
+
throw Error('Address reuse');
|
|
1809
|
+
const payoutSerialId = (0, Utils_1.generateSerialId)();
|
|
1810
|
+
const changeSerialId = (0, Utils_1.generateSerialId)();
|
|
1811
|
+
initResult = {
|
|
1812
|
+
fundingPubKey,
|
|
1813
|
+
payoutSPK,
|
|
1814
|
+
payoutSerialId,
|
|
1815
|
+
fundingInputs,
|
|
1816
|
+
changeSPK,
|
|
1817
|
+
changeSerialId,
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
else {
|
|
1821
|
+
// Handle Input[] through existing Initialize() flow
|
|
1822
|
+
// Use InputSupplementationMode.None when fixed inputs are provided
|
|
1823
|
+
// to avoid wallet lookup issues with unusual addresses
|
|
1824
|
+
const supplementationMode = fixedInputs && fixedInputs.length > 0
|
|
1825
|
+
? types_1.InputSupplementationMode.None
|
|
1826
|
+
: types_1.InputSupplementationMode.Required;
|
|
1827
|
+
initResult = await this.Initialize(acceptCollateralSatoshis, dlcOffer.feeRatePerVb, fixedInputs, supplementationMode);
|
|
1828
|
+
}
|
|
1829
|
+
fundingPubKey = initResult.fundingPubKey;
|
|
1830
|
+
payoutSPK = initResult.payoutSPK;
|
|
1831
|
+
payoutSerialId = initResult.payoutSerialId;
|
|
1832
|
+
changeSPK = initResult.changeSPK;
|
|
1833
|
+
changeSerialId = initResult.changeSerialId;
|
|
1834
|
+
const _fundingInputs = initResult.fundingInputs;
|
|
1835
|
+
_fundingInputs.forEach((input) => (0, assert_1.default)(input.type === messaging_1.MessageType.FundingInput, 'FundingInput must be V0'));
|
|
1836
|
+
fundingInputs = _fundingInputs.map((input) => input);
|
|
1837
|
+
fundingInputs.sort((a, b) => Number(a.inputSerialId) - Number(b.inputSerialId));
|
|
1838
|
+
}
|
|
1839
|
+
(0, assert_1.default)(Buffer.compare(dlcOffer.fundingPubkey, fundingPubKey) !== 0, 'DlcOffer and DlcAccept FundingPubKey cannot be the same');
|
|
1840
|
+
const dlcAccept = new messaging_1.DlcAccept();
|
|
1841
|
+
dlcAccept.temporaryContractId = (0, crypto_1.sha256)(dlcOffer.serialize());
|
|
1842
|
+
dlcAccept.acceptCollateral = acceptCollateralSatoshis;
|
|
1843
|
+
dlcAccept.fundingPubkey = fundingPubKey;
|
|
1844
|
+
dlcAccept.payoutSpk = payoutSPK;
|
|
1845
|
+
dlcAccept.payoutSerialId = payoutSerialId;
|
|
1846
|
+
dlcAccept.fundingInputs = fundingInputs;
|
|
1847
|
+
dlcAccept.changeSpk = changeSPK;
|
|
1848
|
+
dlcAccept.changeSerialId = changeSerialId;
|
|
1849
|
+
(0, assert_1.default)(dlcAccept.changeSerialId !== dlcOffer.fundOutputSerialId, 'changeSerialId cannot equal the fundOutputSerialId');
|
|
1850
|
+
(0, assert_1.default)(dlcOffer.payoutSerialId !== dlcAccept.payoutSerialId, 'offer.payoutSerialId cannot equal accept.payoutSerialId');
|
|
1851
|
+
(0, assert_1.default)((() => {
|
|
1852
|
+
const ids = [
|
|
1853
|
+
dlcOffer.changeSerialId,
|
|
1854
|
+
dlcAccept.changeSerialId,
|
|
1855
|
+
dlcOffer.fundOutputSerialId,
|
|
1856
|
+
];
|
|
1857
|
+
return new Set(ids).size === ids.length;
|
|
1858
|
+
})(), 'offer.changeSerialID, accept.changeSerialId and fundOutputSerialId must be unique');
|
|
1859
|
+
if (dlcOffer.singleFunded)
|
|
1860
|
+
dlcAccept.markAsSingleFunded();
|
|
1861
|
+
dlcAccept.validate();
|
|
1862
|
+
// Only validate funding requirements if accept side is providing collateral
|
|
1863
|
+
if (acceptCollateralSatoshis > BigInt(0)) {
|
|
1864
|
+
(0, assert_1.default)((() => {
|
|
1865
|
+
const finalizer = new core_2.DualFundingTxFinalizer(dlcOffer.fundingInputs, dlcOffer.payoutSpk, dlcOffer.changeSpk, dlcAccept.fundingInputs, dlcAccept.payoutSpk, dlcAccept.changeSpk, dlcOffer.feeRatePerVb);
|
|
1866
|
+
const funding = fundingInputs.reduce((total, input) => {
|
|
1867
|
+
return total + input.prevTx.outputs[input.prevTxVout].value.sats;
|
|
1868
|
+
}, BigInt(0));
|
|
1869
|
+
return funding >= acceptCollateralSatoshis + finalizer.acceptFees;
|
|
1870
|
+
})(), 'fundingInputs for dlcAccept must be greater than acceptCollateralSatoshis plus acceptFees');
|
|
1871
|
+
}
|
|
1872
|
+
const { dlcTransactions, messagesList } = await this.createDlcTxs(dlcOffer, dlcAccept);
|
|
1873
|
+
const { cetSignatures, refundSignature } = await this.CreateCetAdaptorAndRefundSigs(dlcOffer, dlcAccept, dlcTransactions, messagesList, false);
|
|
1874
|
+
const _dlcTransactions = dlcTransactions;
|
|
1875
|
+
const contractId = (0, crypto_1.xor)(_dlcTransactions.fundTx.txId.serialize(), dlcAccept.temporaryContractId);
|
|
1876
|
+
_dlcTransactions.contractId = contractId;
|
|
1877
|
+
dlcAccept.cetAdaptorSignatures = cetSignatures;
|
|
1878
|
+
dlcAccept.refundSignature = refundSignature;
|
|
1879
|
+
return { dlcAccept, dlcTransactions: _dlcTransactions };
|
|
1880
|
+
}
|
|
1881
|
+
/**
|
|
1882
|
+
* Sign Dlc Accept Message
|
|
1883
|
+
* @param _dlcOffer Dlc Offer Message
|
|
1884
|
+
* @param _dlcAccept Dlc Accept Message
|
|
1885
|
+
* @returns {Promise<SignDlcAcceptResponse}
|
|
1886
|
+
*/
|
|
1887
|
+
async signDlcAccept(dlcOffer, dlcAccept) {
|
|
1888
|
+
dlcOffer.validate();
|
|
1889
|
+
dlcAccept.validate();
|
|
1890
|
+
(0, assert_1.default)(Buffer.compare(dlcOffer.fundingPubkey, dlcAccept.fundingPubkey) !== 0, 'DlcOffer and DlcAccept FundingPubKey cannot be the same');
|
|
1891
|
+
const dlcSign = new messaging_1.DlcSign();
|
|
1892
|
+
const { dlcTransactions, messagesList } = await this.createDlcTxs(dlcOffer, dlcAccept);
|
|
1893
|
+
await this.VerifyCetAdaptorAndRefundSigs(dlcOffer, dlcAccept, dlcSign, dlcTransactions, messagesList, true);
|
|
1894
|
+
const { cetSignatures, refundSignature } = await this.CreateCetAdaptorAndRefundSigs(dlcOffer, dlcAccept, dlcTransactions, messagesList, true);
|
|
1895
|
+
const fundingSignatures = await this.CreateFundingSigsAlt(dlcOffer, dlcAccept, dlcTransactions, true);
|
|
1896
|
+
const dlcTxs = dlcTransactions;
|
|
1897
|
+
const contractId = (0, crypto_1.xor)(dlcTxs.fundTx.txId.serialize(), dlcAccept.temporaryContractId);
|
|
1898
|
+
(0, assert_1.default)(Buffer.compare(contractId, (0, crypto_1.xor)(dlcTxs.fundTx.txId.serialize(), dlcAccept.temporaryContractId)) === 0, 'contractId must be the xor of funding txid, fundingOutputIndex and the tempContractId');
|
|
1899
|
+
dlcTxs.contractId = contractId;
|
|
1900
|
+
dlcSign.contractId = contractId;
|
|
1901
|
+
dlcSign.cetAdaptorSignatures = cetSignatures;
|
|
1902
|
+
dlcSign.refundSignature = refundSignature;
|
|
1903
|
+
dlcSign.fundingSignatures = fundingSignatures;
|
|
1904
|
+
return { dlcSign, dlcTransactions: dlcTxs };
|
|
1905
|
+
}
|
|
1906
|
+
/**
|
|
1907
|
+
* Finalize Dlc Sign
|
|
1908
|
+
* @param dlcOffer Dlc Offer Message
|
|
1909
|
+
* @param dlcAccept Dlc Accept Message
|
|
1910
|
+
* @param dlcSign Dlc Sign Message
|
|
1911
|
+
* @param dlcTxs Dlc Transactions Message
|
|
1912
|
+
* @returns {Promise<Tx>}
|
|
1913
|
+
*/
|
|
1914
|
+
async finalizeDlcSign(dlcOffer, dlcAccept, dlcSign, dlcTxs) {
|
|
1915
|
+
let messagesList = [];
|
|
1916
|
+
if (dlcOffer.contractInfo.type === messaging_1.MessageType.SingleContractInfo &&
|
|
1917
|
+
dlcOffer.contractInfo.contractDescriptor.type ===
|
|
1918
|
+
messaging_1.MessageType.SingleContractInfo) {
|
|
1919
|
+
for (const outcome of dlcOffer.contractInfo
|
|
1920
|
+
.contractDescriptor.outcomes) {
|
|
1921
|
+
messagesList.push({ messages: [outcome.outcome] });
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
else {
|
|
1925
|
+
const payoutResponses = this.GetPayouts(dlcOffer);
|
|
1926
|
+
const { messagesList: oracleEventMessagesList } = this.FlattenPayouts(payoutResponses);
|
|
1927
|
+
messagesList = oracleEventMessagesList;
|
|
1928
|
+
}
|
|
1929
|
+
await this.VerifyCetAdaptorAndRefundSigs(dlcOffer, dlcAccept, dlcSign, dlcTxs, messagesList, false);
|
|
1930
|
+
await this.VerifyFundingSigsAlt(dlcOffer, dlcAccept, dlcSign, dlcTxs, false);
|
|
1931
|
+
const fundingSignatures = await this.CreateFundingSigsAlt(dlcOffer, dlcAccept, dlcTxs, false);
|
|
1932
|
+
const fundTx = await this.CreateFundingTx(dlcOffer, dlcAccept, dlcSign, dlcTxs, fundingSignatures);
|
|
1933
|
+
return fundTx;
|
|
1934
|
+
}
|
|
1935
|
+
/**
|
|
1936
|
+
* Execute DLC
|
|
1937
|
+
* @param _dlcOffer Dlc Offer Message
|
|
1938
|
+
* @param _dlcAccept Dlc Accept Message
|
|
1939
|
+
* @param _dlcSign Dlc Sign Message
|
|
1940
|
+
* @param _dlcTxs Dlc Transactions Message
|
|
1941
|
+
* @param oracleAttestation Oracle Attestations TLV (V0)
|
|
1942
|
+
* @param isOfferer Whether party is Dlc Offerer
|
|
1943
|
+
* @returns {Promise<Tx>}
|
|
1944
|
+
*/
|
|
1945
|
+
async execute(dlcOffer, dlcAccept, dlcSign, dlcTxs, oracleAttestation, isOfferer) {
|
|
1946
|
+
if (isOfferer === undefined)
|
|
1947
|
+
isOfferer = await this.isOfferer(dlcOffer, dlcAccept);
|
|
1948
|
+
this.ValidateEvent(dlcOffer, oracleAttestation);
|
|
1949
|
+
return this.FindAndSignCet(dlcOffer, dlcAccept, dlcSign, dlcTxs, oracleAttestation, isOfferer);
|
|
1950
|
+
}
|
|
1951
|
+
/**
|
|
1952
|
+
* Refund DLC
|
|
1953
|
+
* @param dlcOffer Dlc Offer Message
|
|
1954
|
+
* @param dlcAccept Dlc Accept Message
|
|
1955
|
+
* @param dlcSign Dlc Sign Message
|
|
1956
|
+
* @param dlcTxs Dlc Transactions message
|
|
1957
|
+
* @returns {Promise<Tx>}
|
|
1958
|
+
*/
|
|
1959
|
+
async refund(dlcOffer, dlcAccept, dlcSign, dlcTxs) {
|
|
1960
|
+
const network = await this.getConnectedNetwork();
|
|
1961
|
+
const psbt = new bitcoinjs_lib_1.Psbt({ network });
|
|
1962
|
+
// Verify refund transaction locktime matches expected
|
|
1963
|
+
if (Number(dlcTxs.refundTx.locktime) !== dlcOffer.refundLocktime) {
|
|
1964
|
+
throw new Error(`Refund transaction locktime ${dlcTxs.refundTx.locktime} does not match expected ${dlcOffer.refundLocktime}`);
|
|
1965
|
+
}
|
|
1966
|
+
// Add the funding input
|
|
1967
|
+
const fundingOutput = dlcTxs.fundTx.outputs[dlcTxs.fundTxVout];
|
|
1968
|
+
psbt.addInput({
|
|
1969
|
+
hash: dlcTxs.fundTx.txId.toString(),
|
|
1970
|
+
index: dlcTxs.fundTxVout,
|
|
1971
|
+
sequence: 0,
|
|
1972
|
+
witnessUtxo: {
|
|
1973
|
+
script: fundingOutput.scriptPubKey.serialize().subarray(1), // Remove length prefix
|
|
1974
|
+
value: Number(fundingOutput.value.sats),
|
|
1975
|
+
},
|
|
1976
|
+
witnessScript: this.CreateFundingScript(dlcOffer, dlcAccept), // 2-of-2 multisig script
|
|
1977
|
+
});
|
|
1978
|
+
// Add the refund output
|
|
1979
|
+
const refundOutput = dlcTxs.refundTx.outputs[0];
|
|
1980
|
+
psbt.addOutput({
|
|
1981
|
+
address: bitcoinjs_lib_1.address.fromOutputScript(refundOutput.scriptPubKey.serialize().subarray(1), network),
|
|
1982
|
+
value: Number(refundOutput.value.sats),
|
|
1983
|
+
});
|
|
1984
|
+
// Set the locktime to match the refund transaction
|
|
1985
|
+
psbt.setLocktime(Number(dlcTxs.refundTx.locktime));
|
|
1986
|
+
// Sort funding pubkeys for consistent signature ordering
|
|
1987
|
+
const fundingPubKeys = Buffer.compare(dlcOffer.fundingPubkey, dlcAccept.fundingPubkey) === -1
|
|
1988
|
+
? [dlcOffer.fundingPubkey, dlcAccept.fundingPubkey]
|
|
1989
|
+
: [dlcAccept.fundingPubkey, dlcOffer.fundingPubkey];
|
|
1990
|
+
// Add both refund signatures as partial signatures
|
|
1991
|
+
psbt.updateInput(0, {
|
|
1992
|
+
partialSig: [
|
|
1993
|
+
{ pubkey: fundingPubKeys[0], signature: dlcSign.refundSignature },
|
|
1994
|
+
{ pubkey: fundingPubKeys[1], signature: dlcAccept.refundSignature },
|
|
1995
|
+
],
|
|
1996
|
+
});
|
|
1997
|
+
// Validate all signatures
|
|
1998
|
+
psbt.validateSignaturesOfInput(0, (pubkey, msghash, signature) => {
|
|
1999
|
+
return ecc.verify(msghash, pubkey, signature);
|
|
2000
|
+
});
|
|
2001
|
+
// Finalize the input - this will create the final witness script
|
|
2002
|
+
psbt.finalizeInput(0);
|
|
2003
|
+
// Extract the final transaction
|
|
2004
|
+
const finalTx = psbt.extractTransaction();
|
|
2005
|
+
// Convert to the expected Tx format
|
|
2006
|
+
return bitcoin_1.Tx.decode(bufio_1.StreamReader.fromBuffer(finalTx.toBuffer()));
|
|
2007
|
+
}
|
|
2008
|
+
/**
|
|
2009
|
+
* Goal of createDlcClose is for alice (the initiator) to
|
|
2010
|
+
* 1. take dlcoffer, accept, and sign messages. Create a dlcClose message.
|
|
2011
|
+
* 2. Build a close tx, sign.
|
|
2012
|
+
* 3. return dlcClose message (no psbt)
|
|
2013
|
+
*/
|
|
2014
|
+
/**
|
|
2015
|
+
* Generate DlcClose messagetype for closing DLC with Mutual Consent
|
|
2016
|
+
* @param _dlcOffer DlcOffer TLV (V0)
|
|
2017
|
+
* @param _dlcAccept DlcAccept TLV (V0)
|
|
2018
|
+
* @param _dlcTxs DlcTransactions TLV (V0)
|
|
2019
|
+
* @param initiatorPayoutSatoshis Amount initiator expects as a payout
|
|
2020
|
+
* @param isOfferer Whether offerer or not
|
|
2021
|
+
* @param _inputs Optionally specified closing inputs
|
|
2022
|
+
* @returns {Promise<DlcClose>}
|
|
2023
|
+
*/
|
|
2024
|
+
async createDlcClose(_dlcOffer, _dlcAccept, _dlcTxs, initiatorPayoutSatoshis, isOfferer, _inputs) {
|
|
2025
|
+
const { dlcOffer, dlcAccept, dlcTxs } = (0, Utils_1.checkTypes)({
|
|
2026
|
+
_dlcOffer,
|
|
2027
|
+
_dlcAccept,
|
|
2028
|
+
_dlcTxs,
|
|
2029
|
+
});
|
|
2030
|
+
if (isOfferer === undefined)
|
|
2031
|
+
isOfferer = await this.isOfferer(dlcOffer, dlcAccept);
|
|
2032
|
+
const network = await this.getConnectedNetwork();
|
|
2033
|
+
const psbt = new bitcoinjs_lib_1.Psbt({ network });
|
|
2034
|
+
const fundingPubKeys = Buffer.compare(dlcOffer.fundingPubkey, dlcAccept.fundingPubkey) === -1
|
|
2035
|
+
? [dlcOffer.fundingPubkey, dlcAccept.fundingPubkey]
|
|
2036
|
+
: [dlcAccept.fundingPubkey, dlcOffer.fundingPubkey];
|
|
2037
|
+
const p2ms = bitcoinjs_lib_1.payments.p2ms({
|
|
2038
|
+
m: 2,
|
|
2039
|
+
pubkeys: fundingPubKeys,
|
|
2040
|
+
network,
|
|
2041
|
+
});
|
|
2042
|
+
const paymentVariant = bitcoinjs_lib_1.payments.p2wsh({
|
|
2043
|
+
redeem: p2ms,
|
|
2044
|
+
network,
|
|
2045
|
+
});
|
|
2046
|
+
// Initiate and build PSBT
|
|
2047
|
+
let inputs = _inputs;
|
|
2048
|
+
if (!_inputs) {
|
|
2049
|
+
const tempInputs = await this.GetInputsForAmountWithMode([BigInt(20000)], dlcOffer.feeRatePerVb, _inputs || [], types_1.InputSupplementationMode.Required);
|
|
2050
|
+
_inputs = tempInputs;
|
|
2051
|
+
}
|
|
2052
|
+
// Ensure all inputs have derivation paths by fetching from wallet
|
|
2053
|
+
const inputsWithPaths = await Promise.all(_inputs.map(async (input) => {
|
|
2054
|
+
const address = await this.getMethod('getWalletAddress')(input.address);
|
|
2055
|
+
const inputWithPath = new types_1.Input(input.txid, input.vout, input.address, input.amount, input.value, input.derivationPath || address.derivationPath, // Use derivationPath from wallet if not set
|
|
2056
|
+
input.maxWitnessLength, input.redeemScript, input.inputSerialId || (0, Utils_1.generateSerialId)(), input.scriptPubKey, input.label, input.confirmations, input.spendable, input.solvable, input.safe, input.dlcInput);
|
|
2057
|
+
return { input: inputWithPath, address };
|
|
2058
|
+
}));
|
|
2059
|
+
inputs = inputsWithPaths.map((item) => item.input);
|
|
2060
|
+
const pubkeys = inputsWithPaths.map((item) => Buffer.from(item.address.publicKey, 'hex'));
|
|
2061
|
+
const fundingInputSerialId = (0, Utils_1.generateSerialId)();
|
|
2062
|
+
// Make temporary array to hold all inputs and then sort them
|
|
2063
|
+
// this method can be improved later
|
|
2064
|
+
const psbtInputs = [];
|
|
2065
|
+
psbtInputs.push({
|
|
2066
|
+
hash: dlcTxs.fundTx.txId.serialize(),
|
|
2067
|
+
index: dlcTxs.fundTxVout,
|
|
2068
|
+
sequence: 0,
|
|
2069
|
+
witnessUtxo: {
|
|
2070
|
+
script: paymentVariant.output,
|
|
2071
|
+
value: Number(this.getFundOutputValueSats(dlcTxs)),
|
|
2072
|
+
},
|
|
2073
|
+
witnessScript: paymentVariant.redeem.output,
|
|
2074
|
+
inputSerialId: fundingInputSerialId,
|
|
2075
|
+
derivationPath: null,
|
|
2076
|
+
});
|
|
2077
|
+
// add all dlc close inputs
|
|
2078
|
+
inputs.forEach((input, i) => {
|
|
2079
|
+
const paymentVariant = bitcoinjs_lib_1.payments.p2wpkh({ pubkey: pubkeys[i], network });
|
|
2080
|
+
psbtInputs.push({
|
|
2081
|
+
hash: input.txid,
|
|
2082
|
+
index: input.vout,
|
|
2083
|
+
sequence: 0,
|
|
2084
|
+
witnessUtxo: {
|
|
2085
|
+
script: paymentVariant.output,
|
|
2086
|
+
value: input.value,
|
|
2087
|
+
},
|
|
2088
|
+
inputSerialId: input.inputSerialId,
|
|
2089
|
+
derivationPath: input.derivationPath,
|
|
2090
|
+
});
|
|
2091
|
+
});
|
|
2092
|
+
// sort all inputs in ascending order by serial ID
|
|
2093
|
+
// The only reason we are doing this is for privacy. If the fundingInput is
|
|
2094
|
+
// always first, it is very obvious. Hence, a serialId is randomly generated
|
|
2095
|
+
// and the inputs are sorted by that instead.
|
|
2096
|
+
const sortedPsbtInputs = psbtInputs.sort((a, b) => Number(a.inputSerialId - b.inputSerialId));
|
|
2097
|
+
// Get index of fundingInput
|
|
2098
|
+
const fundingInputIndex = sortedPsbtInputs.findIndex((input) => input.inputSerialId === fundingInputSerialId);
|
|
2099
|
+
// add to psbt
|
|
2100
|
+
sortedPsbtInputs.forEach((input) => psbt.addInput(input));
|
|
2101
|
+
const fundingInputs = await Promise.all(inputs.map(async (input) => {
|
|
2102
|
+
return this.inputToFundingInput(input);
|
|
2103
|
+
}));
|
|
2104
|
+
const finalizer = new core_2.DualClosingTxFinalizer(fundingInputs, dlcOffer.payoutSpk, dlcAccept.payoutSpk, dlcOffer.feeRatePerVb);
|
|
2105
|
+
const closeInputAmount = BigInt(inputs.reduce((acc, val) => acc + val.value, 0));
|
|
2106
|
+
const offerPayoutValue = isOfferer
|
|
2107
|
+
? closeInputAmount +
|
|
2108
|
+
initiatorPayoutSatoshis -
|
|
2109
|
+
finalizer.offerInitiatorFees
|
|
2110
|
+
: dlcOffer.contractInfo.totalCollateral - initiatorPayoutSatoshis;
|
|
2111
|
+
const acceptPayoutValue = isOfferer
|
|
2112
|
+
? dlcOffer.contractInfo.totalCollateral - initiatorPayoutSatoshis
|
|
2113
|
+
: closeInputAmount +
|
|
2114
|
+
initiatorPayoutSatoshis -
|
|
2115
|
+
finalizer.offerInitiatorFees;
|
|
2116
|
+
const offerFirst = dlcOffer.payoutSerialId < dlcAccept.payoutSerialId;
|
|
2117
|
+
psbt.addOutput({
|
|
2118
|
+
value: Number(offerFirst ? offerPayoutValue : acceptPayoutValue),
|
|
2119
|
+
address: bitcoinjs_lib_1.address.fromOutputScript(offerFirst ? dlcOffer.payoutSpk : dlcAccept.payoutSpk, network),
|
|
2120
|
+
});
|
|
2121
|
+
psbt.addOutput({
|
|
2122
|
+
value: Number(offerFirst ? acceptPayoutValue : offerPayoutValue),
|
|
2123
|
+
address: bitcoinjs_lib_1.address.fromOutputScript(offerFirst ? dlcAccept.payoutSpk : dlcOffer.payoutSpk, network),
|
|
2124
|
+
});
|
|
2125
|
+
// Generate keypair to sign inputs
|
|
2126
|
+
const fundPrivateKeyPair = await this.GetFundKeyPair(dlcOffer, dlcAccept, isOfferer);
|
|
2127
|
+
// Sign dlc fundinginput
|
|
2128
|
+
psbt.signInput(fundingInputIndex, fundPrivateKeyPair);
|
|
2129
|
+
// Sign dlcclose inputs
|
|
2130
|
+
await Promise.all(sortedPsbtInputs.map(async (input, i) => {
|
|
2131
|
+
if (i === fundingInputIndex)
|
|
2132
|
+
return;
|
|
2133
|
+
// derive keypair
|
|
2134
|
+
if (!input.derivationPath) {
|
|
2135
|
+
throw new Error(`Missing derivation path for input ${i}`);
|
|
2136
|
+
}
|
|
2137
|
+
const keyPair = await this.getMethod('keyPair')(input.derivationPath);
|
|
2138
|
+
psbt.signInput(i, keyPair);
|
|
2139
|
+
}));
|
|
2140
|
+
// Validate signatures
|
|
2141
|
+
psbt.validateSignaturesOfAllInputs((pubkey, msghash, signature) => {
|
|
2142
|
+
return ecc.verify(msghash, pubkey, signature);
|
|
2143
|
+
});
|
|
2144
|
+
// Extract close signature from psbt and decode it to only extract r and s values
|
|
2145
|
+
const closeSignature = await bitcoinjs_lib_1.script.signature.decode(psbt.data.inputs[fundingInputIndex].partialSig[0].signature).signature;
|
|
2146
|
+
// Extract funding signatures from psbt
|
|
2147
|
+
const inputSigs = psbt.data.inputs
|
|
2148
|
+
.filter((input) => input !== fundingInputIndex)
|
|
2149
|
+
.map((input) => input.partialSig[0]);
|
|
2150
|
+
// create fundingSignatures
|
|
2151
|
+
const witnessElements = [];
|
|
2152
|
+
for (let i = 0; i < inputSigs.length; i++) {
|
|
2153
|
+
const sigWitness = new messaging_1.ScriptWitnessV0();
|
|
2154
|
+
sigWitness.witness = inputSigs[i].signature;
|
|
2155
|
+
const pubKeyWitness = new messaging_1.ScriptWitnessV0();
|
|
2156
|
+
pubKeyWitness.witness = inputSigs[i].pubkey;
|
|
2157
|
+
witnessElements.push([sigWitness, pubKeyWitness]);
|
|
2158
|
+
}
|
|
2159
|
+
const fundingSignatures = new messaging_1.FundingSignatures();
|
|
2160
|
+
fundingSignatures.witnessElements = witnessElements;
|
|
2161
|
+
// Create DlcClose
|
|
2162
|
+
const dlcClose = new messaging_1.DlcClose();
|
|
2163
|
+
dlcClose.contractId = dlcTxs.contractId;
|
|
2164
|
+
dlcClose.offerPayoutSatoshis = BigInt(psbt.txOutputs[offerFirst ? 0 : 1].value); // You give collateral back to users
|
|
2165
|
+
dlcClose.acceptPayoutSatoshis = BigInt(psbt.txOutputs[offerFirst ? 1 : 0].value); // give collateral back to users
|
|
2166
|
+
dlcClose.fundInputSerialId = fundingInputSerialId; // randomly generated serial id
|
|
2167
|
+
dlcClose.closeSignature = closeSignature;
|
|
2168
|
+
dlcClose.fundingSignatures = fundingSignatures;
|
|
2169
|
+
dlcClose.fundingInputs = fundingInputs;
|
|
2170
|
+
dlcClose.validate();
|
|
2171
|
+
return dlcClose;
|
|
2172
|
+
}
|
|
2173
|
+
/**
|
|
2174
|
+
* Generate multiple DlcClose messagetypes for closing DLC with Mutual Consent
|
|
2175
|
+
* @param _dlcOffer DlcOffer TLV (V0)
|
|
2176
|
+
* @param _dlcAccept DlcAccept TLV (V0)
|
|
2177
|
+
* @param _dlcTxs DlcTransactions TLV (V0)
|
|
2178
|
+
* @param initiatorPayouts Array of amounts initiator expects as payouts
|
|
2179
|
+
* @param isOfferer Whether offerer or not
|
|
2180
|
+
* @param _inputs Optionally specified closing inputs
|
|
2181
|
+
* @returns {Promise<DlcClose[]>}
|
|
2182
|
+
*/
|
|
2183
|
+
async createBatchDlcClose(_dlcOffer, _dlcAccept, _dlcTxs, initiatorPayouts, isOfferer, _inputs) {
|
|
2184
|
+
const { dlcOffer, dlcAccept, dlcTxs } = (0, Utils_1.checkTypes)({
|
|
2185
|
+
_dlcOffer,
|
|
2186
|
+
_dlcAccept,
|
|
2187
|
+
_dlcTxs,
|
|
2188
|
+
});
|
|
2189
|
+
if (isOfferer === undefined)
|
|
2190
|
+
isOfferer = await this.isOfferer(dlcOffer, dlcAccept);
|
|
2191
|
+
if (_inputs && _inputs.length > 0)
|
|
2192
|
+
throw Error('funding inputs not supported on BatchDlcClose'); // TODO support multiple funding inputs
|
|
2193
|
+
const fundingInputSerialId = (0, Utils_1.generateSerialId)();
|
|
2194
|
+
const fundingInputs = []; // TODO: support multiple funding inputs
|
|
2195
|
+
const finalizer = new core_2.DualClosingTxFinalizer(fundingInputs, dlcOffer.payoutSpk, dlcAccept.payoutSpk, dlcOffer.feeRatePerVb);
|
|
2196
|
+
// Generate keypair to sign inputs
|
|
2197
|
+
const fundPrivateKeyPair = await this.GetFundKeyPair(dlcOffer, dlcAccept, isOfferer);
|
|
2198
|
+
const closeInputAmount = BigInt(0); // TODO support multiple funding inputs
|
|
2199
|
+
const privKey = Buffer.from(fundPrivateKeyPair.privateKey).toString('hex');
|
|
2200
|
+
const rawCloseTxs = await this.CreateCloseRawTxs(dlcOffer, dlcAccept, dlcTxs, closeInputAmount, isOfferer, [], fundingInputs, initiatorPayouts);
|
|
2201
|
+
const sigHashes = await this.CreateSignatureHashes(dlcOffer, dlcAccept, dlcTxs, rawCloseTxs);
|
|
2202
|
+
const signatures = await this.CalculateEcSignatureHashes(sigHashes, privKey);
|
|
2203
|
+
const dlcCloses = [];
|
|
2204
|
+
signatures.forEach((sig, i) => {
|
|
2205
|
+
const payout = initiatorPayouts[i];
|
|
2206
|
+
const payoutMinusOfferFees = finalizer.offerInitiatorFees > payout
|
|
2207
|
+
? BigInt(0)
|
|
2208
|
+
: payout - finalizer.offerInitiatorFees;
|
|
2209
|
+
const collateralMinusPayout = payout > dlcOffer.contractInfo.totalCollateral
|
|
2210
|
+
? BigInt(0)
|
|
2211
|
+
: dlcOffer.contractInfo.totalCollateral - payout;
|
|
2212
|
+
const offerPayoutValue = isOfferer
|
|
2213
|
+
? closeInputAmount + payoutMinusOfferFees
|
|
2214
|
+
: collateralMinusPayout;
|
|
2215
|
+
const acceptPayoutValue = isOfferer
|
|
2216
|
+
? collateralMinusPayout
|
|
2217
|
+
: closeInputAmount + payoutMinusOfferFees;
|
|
2218
|
+
const fundingSignatures = new messaging_1.FundingSignatures();
|
|
2219
|
+
const dlcClose = new messaging_1.DlcClose();
|
|
2220
|
+
dlcClose.contractId = dlcTxs.contractId;
|
|
2221
|
+
dlcClose.offerPayoutSatoshis = offerPayoutValue;
|
|
2222
|
+
dlcClose.acceptPayoutSatoshis = acceptPayoutValue;
|
|
2223
|
+
dlcClose.fundInputSerialId = fundingInputSerialId;
|
|
2224
|
+
dlcClose.closeSignature = Buffer.from(sig, 'hex');
|
|
2225
|
+
dlcClose.fundingSignatures = fundingSignatures;
|
|
2226
|
+
dlcClose.validate();
|
|
2227
|
+
dlcCloses.push(dlcClose);
|
|
2228
|
+
});
|
|
2229
|
+
return dlcCloses;
|
|
2230
|
+
}
|
|
2231
|
+
async verifyBatchDlcCloseUsingMetadata(dlcCloseMetadata, _dlcCloses, isOfferer) {
|
|
2232
|
+
const { dlcOffer, dlcAccept, dlcTxs } = dlcCloseMetadata.toDlcMessages();
|
|
2233
|
+
await this.verifyBatchDlcClose(dlcOffer, dlcAccept, dlcTxs, _dlcCloses, isOfferer);
|
|
2234
|
+
}
|
|
2235
|
+
/**
|
|
2236
|
+
* Verify multiple DlcClose messagetypes for closing DLC with Mutual Consent
|
|
2237
|
+
* @param _dlcOffer DlcOffer TLV (V0)
|
|
2238
|
+
* @param _dlcAccept DlcAccept TLV (V0)
|
|
2239
|
+
* @param _dlcTxs DlcTransactions TLV (V0)
|
|
2240
|
+
* @param _dlcCloses DlcClose[] TLV (V0)
|
|
2241
|
+
* @param isOfferer Whether offerer or not
|
|
2242
|
+
* @returns {Promise<void>}
|
|
2243
|
+
*/
|
|
2244
|
+
async verifyBatchDlcClose(_dlcOffer, _dlcAccept, _dlcTxs, _dlcCloses, isOfferer) {
|
|
2245
|
+
const { dlcOffer, dlcAccept, dlcTxs } = (0, Utils_1.checkTypes)({
|
|
2246
|
+
_dlcOffer,
|
|
2247
|
+
_dlcAccept,
|
|
2248
|
+
_dlcTxs,
|
|
2249
|
+
});
|
|
2250
|
+
const dlcCloses = _dlcCloses.map((_dlcClose) => (0, Utils_1.checkTypes)({ _dlcClose }).dlcClose);
|
|
2251
|
+
if (isOfferer === undefined)
|
|
2252
|
+
isOfferer = await this.isOfferer(dlcOffer, dlcAccept);
|
|
2253
|
+
(0, assert_1.default)(dlcCloses.every((dlcClose) => dlcClose.fundingInputs.length === 0), 'funding inputs not supported on verify BatchDlcClose'); // TODO support multiple funding inputs
|
|
2254
|
+
const closeInputAmount = BigInt(0); // TODO support multiple funding inputs
|
|
2255
|
+
const rawCloseTxs = await this.CreateCloseRawTxs(dlcOffer, dlcAccept, dlcTxs, closeInputAmount, isOfferer, dlcCloses);
|
|
2256
|
+
const areSigsValid = await this.VerifySignatures(dlcOffer, dlcAccept, dlcTxs, dlcCloses, rawCloseTxs, isOfferer);
|
|
2257
|
+
(0, assert_1.default)(areSigsValid, 'Signatures invalid in Verify Batch DlcClose');
|
|
2258
|
+
}
|
|
2259
|
+
/**
|
|
2260
|
+
* Goal of finalize Dlc Close is for bob to
|
|
2261
|
+
* 1. take the dlcClose created by alice using createDlcClose,
|
|
2262
|
+
* 2. Build a psbt using Alice's dlcClose message
|
|
2263
|
+
* 3. Sign psbt with bob's privkey
|
|
2264
|
+
* 4. return a tx ready to be broadcast
|
|
2265
|
+
*/
|
|
2266
|
+
/**
|
|
2267
|
+
* Finalize Dlc Close
|
|
2268
|
+
* @param _dlcOffer Dlc Offer Message
|
|
2269
|
+
* @param _dlcAccept Dlc Accept Message
|
|
2270
|
+
* @param _dlcClose Dlc Close Message
|
|
2271
|
+
* @param _dlcTxs Dlc Transactions Message
|
|
2272
|
+
* @returns {Promise<Tx>}
|
|
2273
|
+
*/
|
|
2274
|
+
async finalizeDlcClose(_dlcOffer, _dlcAccept, _dlcClose, _dlcTxs) {
|
|
2275
|
+
const { dlcOffer, dlcAccept, dlcClose, dlcTxs } = (0, Utils_1.checkTypes)({
|
|
2276
|
+
_dlcOffer,
|
|
2277
|
+
_dlcAccept,
|
|
2278
|
+
_dlcClose,
|
|
2279
|
+
_dlcTxs,
|
|
2280
|
+
});
|
|
2281
|
+
dlcOffer.validate();
|
|
2282
|
+
dlcAccept.validate();
|
|
2283
|
+
dlcClose.validate();
|
|
2284
|
+
const network = await this.getConnectedNetwork();
|
|
2285
|
+
const psbt = new bitcoinjs_lib_1.Psbt({ network });
|
|
2286
|
+
const fundingPubKeys = Buffer.compare(dlcOffer.fundingPubkey, dlcAccept.fundingPubkey) === -1
|
|
2287
|
+
? [dlcOffer.fundingPubkey, dlcAccept.fundingPubkey]
|
|
2288
|
+
: [dlcAccept.fundingPubkey, dlcOffer.fundingPubkey];
|
|
2289
|
+
const p2ms = bitcoinjs_lib_1.payments.p2ms({
|
|
2290
|
+
m: 2,
|
|
2291
|
+
pubkeys: fundingPubKeys,
|
|
2292
|
+
network,
|
|
2293
|
+
});
|
|
2294
|
+
const paymentVariant = bitcoinjs_lib_1.payments.p2wsh({
|
|
2295
|
+
redeem: p2ms,
|
|
2296
|
+
network,
|
|
2297
|
+
});
|
|
2298
|
+
// Make temporary array to hold all inputs and then sort them
|
|
2299
|
+
// this method can be improved later
|
|
2300
|
+
const psbtInputs = [];
|
|
2301
|
+
psbtInputs.push({
|
|
2302
|
+
hash: dlcTxs.fundTx.txId.serialize(),
|
|
2303
|
+
index: dlcTxs.fundTxVout,
|
|
2304
|
+
sequence: 0,
|
|
2305
|
+
witnessUtxo: {
|
|
2306
|
+
script: paymentVariant.output,
|
|
2307
|
+
value: Number(this.getFundOutputValueSats(dlcTxs)),
|
|
2308
|
+
},
|
|
2309
|
+
witnessScript: paymentVariant.redeem.output,
|
|
2310
|
+
inputSerialId: dlcClose.fundInputSerialId,
|
|
2311
|
+
});
|
|
2312
|
+
// add all dlc close inputs
|
|
2313
|
+
dlcClose.fundingInputs.forEach((input) => {
|
|
2314
|
+
psbtInputs.push({
|
|
2315
|
+
hash: input.prevTx.txId.serialize(),
|
|
2316
|
+
index: input.prevTxVout,
|
|
2317
|
+
sequence: 0,
|
|
2318
|
+
witnessUtxo: {
|
|
2319
|
+
script: input.prevTx.outputs[input.prevTxVout].scriptPubKey
|
|
2320
|
+
.serialize()
|
|
2321
|
+
.slice(1),
|
|
2322
|
+
value: Number(input.prevTx.outputs[input.prevTxVout].value.sats),
|
|
2323
|
+
},
|
|
2324
|
+
inputSerialId: input.inputSerialId,
|
|
2325
|
+
});
|
|
2326
|
+
});
|
|
2327
|
+
// sort all inputs in ascending order by serial ID
|
|
2328
|
+
// The only reason we are doing this is for privacy. If the fundingInput is
|
|
2329
|
+
// always first, it is very obvious. Hence, a serialId is randomly generated
|
|
2330
|
+
// and the inputs are sorted by that instead.
|
|
2331
|
+
const sortedPsbtInputs = psbtInputs.sort((a, b) => Number(a.inputSerialId - b.inputSerialId));
|
|
2332
|
+
// Get index of fundingInput
|
|
2333
|
+
const fundingInputIndex = sortedPsbtInputs.findIndex((input) => input.inputSerialId === dlcClose.fundInputSerialId);
|
|
2334
|
+
const offerFirst = dlcOffer.payoutSerialId < dlcAccept.payoutSerialId;
|
|
2335
|
+
psbt.addOutput({
|
|
2336
|
+
value: Number(offerFirst
|
|
2337
|
+
? dlcClose.offerPayoutSatoshis
|
|
2338
|
+
: dlcClose.acceptPayoutSatoshis),
|
|
2339
|
+
address: bitcoinjs_lib_1.address.fromOutputScript(offerFirst ? dlcOffer.payoutSpk : dlcAccept.payoutSpk, network),
|
|
2340
|
+
});
|
|
2341
|
+
psbt.addOutput({
|
|
2342
|
+
value: Number(offerFirst
|
|
2343
|
+
? dlcClose.acceptPayoutSatoshis
|
|
2344
|
+
: dlcClose.offerPayoutSatoshis),
|
|
2345
|
+
address: bitcoinjs_lib_1.address.fromOutputScript(offerFirst ? dlcAccept.payoutSpk : dlcOffer.payoutSpk, network),
|
|
2346
|
+
});
|
|
2347
|
+
// add to psbt
|
|
2348
|
+
sortedPsbtInputs.forEach((input) => psbt.addInput(input));
|
|
2349
|
+
const offerer = await this.isOfferer(dlcOffer, dlcAccept);
|
|
2350
|
+
// Generate keypair to sign inputs
|
|
2351
|
+
const fundPrivateKeyPair = await this.GetFundKeyPair(dlcOffer, dlcAccept, offerer);
|
|
2352
|
+
// Sign dlc fundinginput
|
|
2353
|
+
psbt.signInput(fundingInputIndex, fundPrivateKeyPair);
|
|
2354
|
+
const partialSig = [
|
|
2355
|
+
{
|
|
2356
|
+
pubkey: offerer ? dlcAccept.fundingPubkey : dlcOffer.fundingPubkey,
|
|
2357
|
+
signature: bitcoinjs_lib_1.script.signature.encode(dlcClose.closeSignature, 1), // encode using SIGHASH_ALL
|
|
2358
|
+
},
|
|
2359
|
+
];
|
|
2360
|
+
psbt.updateInput(fundingInputIndex, { partialSig });
|
|
2361
|
+
for (let i = 0; i < psbt.data.inputs.length; ++i) {
|
|
2362
|
+
if (i === fundingInputIndex)
|
|
2363
|
+
continue;
|
|
2364
|
+
if (!psbt.data.inputs[i].partialSig)
|
|
2365
|
+
psbt.data.inputs[i].partialSig = [];
|
|
2366
|
+
const witnessI = dlcClose.fundingSignatures.witnessElements.findIndex((el) => Buffer.compare(bitcoin_1.Script.p2wpkhLock((0, crypto_1.hash160)(el[1].witness)).serialize().subarray(1), psbt.data.inputs[i].witnessUtxo.script) === 0);
|
|
2367
|
+
const partialSig = [
|
|
2368
|
+
{
|
|
2369
|
+
pubkey: dlcClose.fundingSignatures.witnessElements[witnessI][1].witness,
|
|
2370
|
+
signature: dlcClose.fundingSignatures.witnessElements[witnessI][0].witness,
|
|
2371
|
+
},
|
|
2372
|
+
];
|
|
2373
|
+
psbt.updateInput(i, { partialSig });
|
|
2374
|
+
}
|
|
2375
|
+
psbt.validateSignaturesOfAllInputs((pubkey, msghash, signature) => {
|
|
2376
|
+
return ecc.verify(msghash, pubkey, signature);
|
|
2377
|
+
});
|
|
2378
|
+
psbt.finalizeAllInputs();
|
|
2379
|
+
return psbt.extractTransaction().toHex();
|
|
2380
|
+
}
|
|
2381
|
+
async fundingInputToInput(_input, findDerivationPath = true) {
|
|
2382
|
+
(0, assert_1.default)(_input.type === messaging_1.MessageType.FundingInput, 'FundingInput must be V0');
|
|
2383
|
+
const network = await this.getConnectedNetwork();
|
|
2384
|
+
const input = _input;
|
|
2385
|
+
const prevTx = input.prevTx;
|
|
2386
|
+
const prevTxOut = prevTx.outputs[input.prevTxVout];
|
|
2387
|
+
const scriptPubKey = prevTxOut.scriptPubKey.serialize().subarray(1);
|
|
2388
|
+
const _address = bitcoinjs_lib_1.address.fromOutputScript(scriptPubKey, network);
|
|
2389
|
+
let derivationPath;
|
|
2390
|
+
if (findDerivationPath) {
|
|
2391
|
+
const inputAddress = await this.client.wallet.findAddress([
|
|
2392
|
+
_address,
|
|
2393
|
+
]);
|
|
2394
|
+
if (inputAddress) {
|
|
2395
|
+
derivationPath = inputAddress.derivationPath;
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
// Check if this FundingInput has DLC input information to preserve
|
|
2399
|
+
const dlcInputMessage = input.dlcInput;
|
|
2400
|
+
let dlcInputInfo;
|
|
2401
|
+
if (dlcInputMessage) {
|
|
2402
|
+
dlcInputInfo = {
|
|
2403
|
+
localFundPubkey: dlcInputMessage.localFundPubkey.toString('hex'),
|
|
2404
|
+
remoteFundPubkey: dlcInputMessage.remoteFundPubkey.toString('hex'),
|
|
2405
|
+
contractId: dlcInputMessage.contractId.toString('hex'),
|
|
2406
|
+
};
|
|
2407
|
+
}
|
|
2408
|
+
return new types_1.Input(prevTx.txId.toString(), input.prevTxVout, _address, prevTxOut.value.bitcoin, Number(prevTxOut.value.sats), derivationPath, input.maxWitnessLen, input.redeemScript ? input.redeemScript.toString('hex') : '', input.inputSerialId, scriptPubKey.toString('hex'), undefined, // label
|
|
2409
|
+
undefined, // confirmations
|
|
2410
|
+
undefined, // spendable
|
|
2411
|
+
undefined, // solvable
|
|
2412
|
+
undefined, // safe
|
|
2413
|
+
dlcInputInfo);
|
|
2414
|
+
}
|
|
2415
|
+
async inputToFundingInput(input) {
|
|
2416
|
+
const fundingInput = new messaging_1.FundingInput();
|
|
2417
|
+
fundingInput.prevTxVout = input.vout;
|
|
2418
|
+
let txRaw = '';
|
|
2419
|
+
try {
|
|
2420
|
+
txRaw = await this.getMethod('getRawTransactionByHash')(input.txid);
|
|
2421
|
+
}
|
|
2422
|
+
catch {
|
|
2423
|
+
try {
|
|
2424
|
+
txRaw = (await this.getMethod('jsonrpc')('gettransaction', input.txid))
|
|
2425
|
+
.hex;
|
|
2426
|
+
}
|
|
2427
|
+
catch {
|
|
2428
|
+
throw Error(`Cannot find tx ${input.txid} in inputToFundingInput using getrawtransactionbyhash or gettransaction`);
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
const tx = bitcoin_1.Tx.decode(bufio_1.StreamReader.fromHex(txRaw));
|
|
2432
|
+
fundingInput.prevTx = tx;
|
|
2433
|
+
fundingInput.sequence = bitcoin_1.Sequence.default();
|
|
2434
|
+
fundingInput.maxWitnessLen = input.maxWitnessLength
|
|
2435
|
+
? input.maxWitnessLength
|
|
2436
|
+
: 108;
|
|
2437
|
+
fundingInput.redeemScript = input.redeemScript
|
|
2438
|
+
? Buffer.from(input.redeemScript, 'hex')
|
|
2439
|
+
: Buffer.from('', 'hex');
|
|
2440
|
+
fundingInput.inputSerialId = input.inputSerialId
|
|
2441
|
+
? input.inputSerialId
|
|
2442
|
+
: (0, Utils_1.generateSerialId)();
|
|
2443
|
+
// Preserve DLC input information if present
|
|
2444
|
+
if (input.isDlcInput()) {
|
|
2445
|
+
const dlcInputInfo = input.dlcInput;
|
|
2446
|
+
const dlcInput = new messaging_1.DlcInput();
|
|
2447
|
+
dlcInput.localFundPubkey = Buffer.from(dlcInputInfo.localFundPubkey, 'hex');
|
|
2448
|
+
dlcInput.remoteFundPubkey = Buffer.from(dlcInputInfo.remoteFundPubkey, 'hex');
|
|
2449
|
+
dlcInput.contractId = Buffer.alloc(32); // Placeholder contract ID
|
|
2450
|
+
fundingInput.dlcInput = dlcInput;
|
|
2451
|
+
}
|
|
2452
|
+
return fundingInput;
|
|
2453
|
+
}
|
|
2454
|
+
async getConnectedNetwork() {
|
|
2455
|
+
return this._network;
|
|
2456
|
+
}
|
|
2457
|
+
/**
|
|
2458
|
+
* Convert BitcoinNetwork to bitcoinjs-lib network format
|
|
2459
|
+
*/
|
|
2460
|
+
getBitcoinJsNetwork() {
|
|
2461
|
+
const network = this._network;
|
|
2462
|
+
if (network.name === 'bitcoin') {
|
|
2463
|
+
return bitcoinjs_lib_1.networks.bitcoin;
|
|
2464
|
+
}
|
|
2465
|
+
else if (network.name === 'testnet') {
|
|
2466
|
+
return bitcoinjs_lib_1.networks.testnet;
|
|
2467
|
+
}
|
|
2468
|
+
else if (network.name === 'regtest') {
|
|
2469
|
+
return bitcoinjs_lib_1.networks.regtest;
|
|
2470
|
+
}
|
|
2471
|
+
else {
|
|
2472
|
+
// Default to mainnet if unknown
|
|
2473
|
+
return bitcoinjs_lib_1.networks.bitcoin;
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
/**
|
|
2477
|
+
* Calculate the maximum collateral possible with given inputs
|
|
2478
|
+
* @param inputs Array of Input objects to use for funding
|
|
2479
|
+
* @param feeRatePerVb Fee rate in satoshis per virtual byte
|
|
2480
|
+
* @param contractCount Number of DLC contracts (default: 1)
|
|
2481
|
+
* @returns Maximum collateral amount in satoshis
|
|
2482
|
+
*/
|
|
2483
|
+
async calculateMaxCollateral(inputs, feeRatePerVb, contractCount = 1) {
|
|
2484
|
+
if (inputs.length === 0) {
|
|
2485
|
+
return BigInt(0);
|
|
2486
|
+
}
|
|
2487
|
+
try {
|
|
2488
|
+
// Convert Input[] to FundingInput[]
|
|
2489
|
+
const fundingInputs = await Promise.all(inputs.map((input) => this.inputToFundingInput(input)));
|
|
2490
|
+
// Use node-dlc's calculateMaxCollateral function
|
|
2491
|
+
// For single-funded DLC, pass only offerer inputs and fee rate
|
|
2492
|
+
return core_1.BatchDlcTxBuilder.calculateMaxCollateral(fundingInputs, feeRatePerVb, contractCount);
|
|
2493
|
+
}
|
|
2494
|
+
catch (error) {
|
|
2495
|
+
// If calculation fails, return 0 to indicate insufficient funds
|
|
2496
|
+
console.warn('calculateMaxCollateral failed:', error);
|
|
2497
|
+
return BigInt(0);
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
/**
|
|
2501
|
+
* Create a funding input with DLC input information for splicing
|
|
2502
|
+
* @param dlcInputInfo DLC input information
|
|
2503
|
+
* @param fundingTxHex Raw transaction hex of the funding transaction
|
|
2504
|
+
*/
|
|
2505
|
+
async createDlcFundingInput(dlcInputInfo, fundingTxHex) {
|
|
2506
|
+
const fundingInput = new messaging_1.FundingInput();
|
|
2507
|
+
const tx = bitcoin_1.Tx.decode(bufio_1.StreamReader.fromHex(fundingTxHex));
|
|
2508
|
+
fundingInput.prevTx = tx;
|
|
2509
|
+
fundingInput.prevTxVout = dlcInputInfo.fundVout;
|
|
2510
|
+
fundingInput.sequence = bitcoin_1.Sequence.default();
|
|
2511
|
+
fundingInput.maxWitnessLen = dlcInputInfo.maxWitnessLength || 220;
|
|
2512
|
+
fundingInput.redeemScript = Buffer.from('', 'hex'); // Empty for P2WSH
|
|
2513
|
+
fundingInput.inputSerialId = BigInt(dlcInputInfo.inputSerialId || (0, Utils_1.generateSerialId)());
|
|
2514
|
+
// Create the DLC multisig script for address generation
|
|
2515
|
+
const localPubkey = Buffer.from(dlcInputInfo.localFundPubkey, 'hex');
|
|
2516
|
+
const remotePubkey = Buffer.from(dlcInputInfo.remoteFundPubkey, 'hex');
|
|
2517
|
+
// Use the same deterministic ordering as cfd-dlc-js: lexicographic by hex
|
|
2518
|
+
// This matches GetOrderedPubkeys() in cfddlc_transactions.cpp
|
|
2519
|
+
const orderedPubkeys = dlcInputInfo.localFundPubkey < dlcInputInfo.remoteFundPubkey
|
|
2520
|
+
? [localPubkey, remotePubkey]
|
|
2521
|
+
: [remotePubkey, localPubkey];
|
|
2522
|
+
const network = await this.getConnectedNetwork();
|
|
2523
|
+
// Create 2-of-2 multisig payment using deterministic ordering
|
|
2524
|
+
const p2ms = bitcoinjs_lib_1.payments.p2ms({
|
|
2525
|
+
m: 2,
|
|
2526
|
+
pubkeys: orderedPubkeys,
|
|
2527
|
+
network,
|
|
2528
|
+
});
|
|
2529
|
+
const paymentVariant = bitcoinjs_lib_1.payments.p2wsh({
|
|
2530
|
+
redeem: p2ms,
|
|
2531
|
+
network,
|
|
2532
|
+
});
|
|
2533
|
+
const multisigAddress = paymentVariant.address;
|
|
2534
|
+
// Verify this matches the actual funding output address
|
|
2535
|
+
const actualFundingOutput = tx.outputs[dlcInputInfo.fundVout];
|
|
2536
|
+
const actualFundingAddress = bitcoinjs_lib_1.address.fromOutputScript(actualFundingOutput.scriptPubKey.serialize().subarray(1), network);
|
|
2537
|
+
if (actualFundingAddress !== multisigAddress) {
|
|
2538
|
+
throw new Error(`DLC funding address mismatch. ` +
|
|
2539
|
+
`Expected: ${actualFundingAddress}, ` +
|
|
2540
|
+
`Constructed: ${multisigAddress}`);
|
|
2541
|
+
}
|
|
2542
|
+
// Add toUtxo method that's expected by GetInputsForAmount
|
|
2543
|
+
fundingInput.toUtxo = () => {
|
|
2544
|
+
return new types_1.Utxo(dlcInputInfo.fundTxid, dlcInputInfo.fundVout, types_1.Amount.FromSatoshis(Number(dlcInputInfo.fundAmount)), multisigAddress, dlcInputInfo.maxWitnessLength || 220, undefined, // DLC inputs don't have derivation paths
|
|
2545
|
+
fundingInput.inputSerialId);
|
|
2546
|
+
};
|
|
2547
|
+
// Create proper DlcInput object for splicing detection and signing
|
|
2548
|
+
const dlcInput = new messaging_1.DlcInput();
|
|
2549
|
+
dlcInput.localFundPubkey = Buffer.from(dlcInputInfo.localFundPubkey, 'hex');
|
|
2550
|
+
dlcInput.remoteFundPubkey = Buffer.from(dlcInputInfo.remoteFundPubkey, 'hex');
|
|
2551
|
+
dlcInput.contractId = Buffer.from(dlcInputInfo.contractId, 'hex');
|
|
2552
|
+
fundingInput.dlcInput = dlcInput;
|
|
2553
|
+
return fundingInput;
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
exports.default = BitcoinDdkProvider;
|
|
2557
|
+
//# sourceMappingURL=BitcoinDdkProvider.js.map
|