@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,4294 @@
|
|
|
1
|
+
import Provider from '@atomicfinance/provider';
|
|
2
|
+
import {
|
|
3
|
+
AdaptorSignature,
|
|
4
|
+
Address,
|
|
5
|
+
Amount,
|
|
6
|
+
CalculateEcSignatureRequest,
|
|
7
|
+
CreateRawTransactionRequest,
|
|
8
|
+
CreateSignatureHashRequest,
|
|
9
|
+
DdkDlcInputInfo,
|
|
10
|
+
DdkDlcTransactions,
|
|
11
|
+
DdkInterface,
|
|
12
|
+
DdkOracleInfo,
|
|
13
|
+
DdkTransaction,
|
|
14
|
+
DlcInputInfo,
|
|
15
|
+
DlcInputInfoRequest,
|
|
16
|
+
Input,
|
|
17
|
+
InputSupplementationMode,
|
|
18
|
+
Messages,
|
|
19
|
+
PartyParams,
|
|
20
|
+
Payout,
|
|
21
|
+
PayoutRequest,
|
|
22
|
+
Utxo,
|
|
23
|
+
VerifySignatureRequest,
|
|
24
|
+
} from '@atomicfinance/types';
|
|
25
|
+
import { sleep } from '@atomicfinance/utils';
|
|
26
|
+
import { Script, Sequence, Tx } from '@node-dlc/bitcoin';
|
|
27
|
+
import { StreamReader } from '@node-dlc/bufio';
|
|
28
|
+
import { BatchDlcTxBuilder } from '@node-dlc/core';
|
|
29
|
+
import {
|
|
30
|
+
DualClosingTxFinalizer,
|
|
31
|
+
DualFundingTxFinalizer,
|
|
32
|
+
groupByIgnoringDigits,
|
|
33
|
+
HyperbolaPayoutCurve,
|
|
34
|
+
PolynomialPayoutCurve,
|
|
35
|
+
roundPayout,
|
|
36
|
+
} from '@node-dlc/core';
|
|
37
|
+
import { hash160, sha256, xor } from '@node-dlc/crypto';
|
|
38
|
+
import {
|
|
39
|
+
CetAdaptorSignatures,
|
|
40
|
+
ContractDescriptor,
|
|
41
|
+
ContractDescriptorType,
|
|
42
|
+
ContractInfo,
|
|
43
|
+
ContractInfoType,
|
|
44
|
+
DigitDecompositionEventDescriptor,
|
|
45
|
+
DisjointContractInfo,
|
|
46
|
+
DlcAccept,
|
|
47
|
+
DlcClose,
|
|
48
|
+
DlcCloseMetadata,
|
|
49
|
+
DlcInput,
|
|
50
|
+
DlcOffer,
|
|
51
|
+
DlcSign,
|
|
52
|
+
DlcTransactions,
|
|
53
|
+
EnumeratedDescriptor,
|
|
54
|
+
EnumEventDescriptor,
|
|
55
|
+
F64,
|
|
56
|
+
FundingInput,
|
|
57
|
+
FundingSignatures,
|
|
58
|
+
HyperbolaPayoutCurvePiece,
|
|
59
|
+
MessageType,
|
|
60
|
+
MultiOracleInfo,
|
|
61
|
+
NumericalDescriptor,
|
|
62
|
+
OracleAttestation,
|
|
63
|
+
OracleEvent,
|
|
64
|
+
OracleInfo,
|
|
65
|
+
PayoutCurvePieceType,
|
|
66
|
+
PayoutFunction,
|
|
67
|
+
PayoutFunctionV0,
|
|
68
|
+
PolynomialPayoutCurvePiece,
|
|
69
|
+
ScriptWitnessV0,
|
|
70
|
+
SingleContractInfo,
|
|
71
|
+
SingleOracleInfo,
|
|
72
|
+
} from '@node-dlc/messaging';
|
|
73
|
+
import assert from 'assert';
|
|
74
|
+
import BigNumber from 'bignumber.js';
|
|
75
|
+
import { BitcoinNetwork, chainHashFromNetwork } from 'bitcoin-networks';
|
|
76
|
+
import {
|
|
77
|
+
address,
|
|
78
|
+
networks,
|
|
79
|
+
payments,
|
|
80
|
+
Psbt,
|
|
81
|
+
script,
|
|
82
|
+
Transaction as btTransaction,
|
|
83
|
+
} from 'bitcoinjs-lib';
|
|
84
|
+
import crypto from 'crypto';
|
|
85
|
+
import { ECPairFactory, ECPairInterface } from 'ecpair';
|
|
86
|
+
import * as ecc from 'tiny-secp256k1';
|
|
87
|
+
|
|
88
|
+
import { checkTypes, generateSerialId, outputsToPayouts } from './utils/Utils';
|
|
89
|
+
|
|
90
|
+
const ECPair = ECPairFactory(ecc);
|
|
91
|
+
|
|
92
|
+
export default class BitcoinDdkProvider extends Provider {
|
|
93
|
+
private _network: BitcoinNetwork;
|
|
94
|
+
private _ddk: DdkInterface;
|
|
95
|
+
|
|
96
|
+
constructor(network: BitcoinNetwork, ddkLib: DdkInterface) {
|
|
97
|
+
super();
|
|
98
|
+
this._network = network;
|
|
99
|
+
this._ddk = ddkLib;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
public async DdkLoaded() {
|
|
103
|
+
while (!this._ddk) {
|
|
104
|
+
await sleep(10);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Find private key for DLC funding pubkey by deriving wallet addresses
|
|
110
|
+
*/
|
|
111
|
+
private async findDlcFundingPrivateKey(
|
|
112
|
+
localFundPubkey: string,
|
|
113
|
+
remoteFundPubkey: string,
|
|
114
|
+
): Promise<string> {
|
|
115
|
+
const targetPubkeys = [localFundPubkey, remoteFundPubkey];
|
|
116
|
+
|
|
117
|
+
// First check existing wallet addresses
|
|
118
|
+
const addresses = await this.getMethod('getAddresses')();
|
|
119
|
+
|
|
120
|
+
for (const addressInfo of addresses) {
|
|
121
|
+
if (addressInfo.derivationPath) {
|
|
122
|
+
try {
|
|
123
|
+
const keyPair = await this.getMethod('keyPair')(
|
|
124
|
+
addressInfo.derivationPath,
|
|
125
|
+
);
|
|
126
|
+
const pubkey = Buffer.from(keyPair.publicKey);
|
|
127
|
+
const pubkeyHex = pubkey.toString('hex');
|
|
128
|
+
|
|
129
|
+
if (targetPubkeys.includes(pubkeyHex)) {
|
|
130
|
+
return Buffer.from(keyPair.privateKey).toString('hex');
|
|
131
|
+
}
|
|
132
|
+
} catch {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// If not found in existing addresses, do comprehensive search
|
|
139
|
+
// For DLC splicing, funding pubkeys can be at much higher derivation paths
|
|
140
|
+
console.log('Searching extensively for DLC funding private key...');
|
|
141
|
+
|
|
142
|
+
for (const isChange of [false, true]) {
|
|
143
|
+
for (let i = 0; i < 1000; i++) {
|
|
144
|
+
// Search up to 1000 addresses for DLC keys
|
|
145
|
+
try {
|
|
146
|
+
const address = await this.client.wallet.getAddresses(i, 1, isChange);
|
|
147
|
+
if (address && address.length > 0) {
|
|
148
|
+
const addressInfo = address[0];
|
|
149
|
+
|
|
150
|
+
if (addressInfo.derivationPath) {
|
|
151
|
+
const keyPair = await this.getMethod('keyPair')(
|
|
152
|
+
addressInfo.derivationPath,
|
|
153
|
+
);
|
|
154
|
+
const pubkey = Buffer.from(keyPair.publicKey);
|
|
155
|
+
const pubkeyHex = pubkey.toString('hex');
|
|
156
|
+
|
|
157
|
+
if (targetPubkeys.includes(pubkeyHex)) {
|
|
158
|
+
console.log(
|
|
159
|
+
`Found DLC funding key at derivation path: ${addressInfo.derivationPath}`,
|
|
160
|
+
);
|
|
161
|
+
return Buffer.from(keyPair.privateKey).toString('hex');
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
} catch {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
throw new Error(
|
|
172
|
+
`Could not find private key for DLC funding pubkeys: local=${localFundPubkey}, remote=${remoteFundPubkey}`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private async GetPrivKeysForInputs(inputs: Input[]): Promise<string[]> {
|
|
177
|
+
const privKeys: string[] = [];
|
|
178
|
+
|
|
179
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
180
|
+
const input = inputs[i];
|
|
181
|
+
|
|
182
|
+
if (input.isDlcInput()) {
|
|
183
|
+
// Handle DLC input - use the dedicated method to find the funding private key
|
|
184
|
+
const dlcInput = input.dlcInput!;
|
|
185
|
+
const foundPrivKey = await this.findDlcFundingPrivateKey(
|
|
186
|
+
dlcInput.localFundPubkey,
|
|
187
|
+
dlcInput.remoteFundPubkey,
|
|
188
|
+
);
|
|
189
|
+
privKeys.push(foundPrivKey);
|
|
190
|
+
} else {
|
|
191
|
+
// Handle regular input
|
|
192
|
+
let derivationPath = input.derivationPath;
|
|
193
|
+
|
|
194
|
+
if (!derivationPath) {
|
|
195
|
+
try {
|
|
196
|
+
derivationPath = (
|
|
197
|
+
await this.getMethod('getWalletAddress')(input.address)
|
|
198
|
+
).derivationPath;
|
|
199
|
+
} catch (error) {
|
|
200
|
+
throw new Error(
|
|
201
|
+
`Unable to find address ${input.address} in wallet. ` +
|
|
202
|
+
`This may happen when using derivation paths outside the normal range. ` +
|
|
203
|
+
`Error: ${error.message}`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const keyPair = await this.getMethod('keyPair')(derivationPath);
|
|
209
|
+
const privKey = Buffer.from(keyPair.__D).toString('hex');
|
|
210
|
+
privKeys.push(privKey);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return privKeys;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async GetCfdNetwork(): Promise<string> {
|
|
218
|
+
const network = await this.getConnectedNetwork();
|
|
219
|
+
|
|
220
|
+
switch (network.name) {
|
|
221
|
+
case 'bitcoin_testnet':
|
|
222
|
+
return 'testnet';
|
|
223
|
+
case 'bitcoin_regtest':
|
|
224
|
+
return 'regtest';
|
|
225
|
+
default:
|
|
226
|
+
return 'bitcoin';
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Get inputs for amount with explicit supplementation control
|
|
232
|
+
*/
|
|
233
|
+
async GetInputsForAmountWithMode(
|
|
234
|
+
amounts: bigint[],
|
|
235
|
+
feeRatePerVb: bigint,
|
|
236
|
+
fixedInputs: Input[] = [],
|
|
237
|
+
supplementation: InputSupplementationMode = InputSupplementationMode.Required,
|
|
238
|
+
): Promise<Input[]> {
|
|
239
|
+
if (amounts.length === 0) return [];
|
|
240
|
+
|
|
241
|
+
// For "none" mode, use exactly the provided inputs
|
|
242
|
+
if (supplementation === InputSupplementationMode.None) {
|
|
243
|
+
return fixedInputs;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// For "required" and "optional" modes, attempt supplementation
|
|
247
|
+
const fixedUtxos = fixedInputs.map((input) => input.toUtxo());
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
const inputsForAmount: InputsForDualAmountResponse = await this.getMethod(
|
|
251
|
+
'getInputsForDualFunding',
|
|
252
|
+
)(amounts, feeRatePerVb, fixedUtxos);
|
|
253
|
+
|
|
254
|
+
// Convert UTXO objects to Input class instances
|
|
255
|
+
return inputsForAmount.inputs.map((utxo) => Input.fromUTXO(utxo));
|
|
256
|
+
} catch (e) {
|
|
257
|
+
const errorMessage = e instanceof Error ? e.message : 'Unknown error';
|
|
258
|
+
|
|
259
|
+
if (supplementation === InputSupplementationMode.Required) {
|
|
260
|
+
throw Error(
|
|
261
|
+
`Not enough balance GetInputsForAmountWithMode. Error: ${errorMessage}`,
|
|
262
|
+
);
|
|
263
|
+
} else {
|
|
264
|
+
// Optional mode: fallback to provided inputs
|
|
265
|
+
return fixedInputs;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async GetInputsForAmount(
|
|
271
|
+
amounts: bigint[],
|
|
272
|
+
feeRatePerVb: bigint,
|
|
273
|
+
fixedInputs: Input[] = [],
|
|
274
|
+
): Promise<Input[]> {
|
|
275
|
+
if (amounts.length === 0) return [];
|
|
276
|
+
|
|
277
|
+
const fixedUtxos = fixedInputs.map((input) => input.toUtxo());
|
|
278
|
+
|
|
279
|
+
let inputs: Input[];
|
|
280
|
+
try {
|
|
281
|
+
const inputsForAmount: InputsForDualAmountResponse = await this.getMethod(
|
|
282
|
+
'getInputsForDualFunding',
|
|
283
|
+
)(amounts, feeRatePerVb, fixedUtxos);
|
|
284
|
+
|
|
285
|
+
// Convert UTXO objects to Input class instances
|
|
286
|
+
inputs = inputsForAmount.inputs.map((utxo) => Input.fromUTXO(utxo));
|
|
287
|
+
} catch (e) {
|
|
288
|
+
const errorMessage = e instanceof Error ? e.message : 'Unknown error';
|
|
289
|
+
if (fixedInputs.length === 0) {
|
|
290
|
+
throw Error(
|
|
291
|
+
`Not enough balance getInputsForAmount. Error: ${errorMessage}`,
|
|
292
|
+
);
|
|
293
|
+
} else {
|
|
294
|
+
inputs = fixedInputs;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return inputs;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private async Initialize(
|
|
302
|
+
collateral: bigint,
|
|
303
|
+
feeRatePerVb: bigint,
|
|
304
|
+
fixedInputs: Input[],
|
|
305
|
+
inputSupplementationMode: InputSupplementationMode = InputSupplementationMode.Required,
|
|
306
|
+
): Promise<InitializeResponse> {
|
|
307
|
+
const network = await this.getConnectedNetwork();
|
|
308
|
+
const payoutAddress: Address =
|
|
309
|
+
await this.client.wallet.getUnusedAddress(false);
|
|
310
|
+
const payoutSPK: Buffer = address.toOutputScript(
|
|
311
|
+
payoutAddress.address,
|
|
312
|
+
network,
|
|
313
|
+
);
|
|
314
|
+
const changeAddress: Address =
|
|
315
|
+
await this.client.wallet.getUnusedAddress(true);
|
|
316
|
+
const changeSPK: Buffer = address.toOutputScript(
|
|
317
|
+
changeAddress.address,
|
|
318
|
+
network,
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
const fundingAddress: Address =
|
|
322
|
+
await this.client.wallet.getUnusedAddress(false);
|
|
323
|
+
const fundingPubKey: Buffer = Buffer.from(fundingAddress.publicKey, 'hex');
|
|
324
|
+
|
|
325
|
+
if (fundingAddress.address === payoutAddress.address)
|
|
326
|
+
throw Error('Address reuse');
|
|
327
|
+
|
|
328
|
+
const inputs: Input[] = await this.GetInputsForAmountWithMode(
|
|
329
|
+
[collateral],
|
|
330
|
+
feeRatePerVb,
|
|
331
|
+
fixedInputs,
|
|
332
|
+
inputSupplementationMode,
|
|
333
|
+
);
|
|
334
|
+
const fundingInputs: FundingInput[] = await Promise.all(
|
|
335
|
+
inputs.map(async (input) => {
|
|
336
|
+
return this.inputToFundingInput(input);
|
|
337
|
+
}),
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
const payoutSerialId: bigint = generateSerialId();
|
|
341
|
+
const changeSerialId: bigint = generateSerialId();
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
fundingPubKey,
|
|
345
|
+
payoutSPK,
|
|
346
|
+
payoutSerialId,
|
|
347
|
+
fundingInputs,
|
|
348
|
+
changeSPK,
|
|
349
|
+
changeSerialId,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* TODO: Add GetPayoutFromOutcomes
|
|
355
|
+
*
|
|
356
|
+
* private GetPayoutsFromOutcomes(
|
|
357
|
+
* contractDescriptor: ContractDescriptorV0,
|
|
358
|
+
* totalCollateral: bigint,
|
|
359
|
+
* ): PayoutRequest[] {}
|
|
360
|
+
*/
|
|
361
|
+
|
|
362
|
+
private GetPayoutsFromPayoutFunction(
|
|
363
|
+
dlcOffer: DlcOffer,
|
|
364
|
+
contractDescriptor: NumericalDescriptor,
|
|
365
|
+
oracleInfo: OracleInfo,
|
|
366
|
+
totalCollateral: bigint,
|
|
367
|
+
): GetPayoutsResponse {
|
|
368
|
+
const payoutFunction = contractDescriptor.payoutFunction as PayoutFunction;
|
|
369
|
+
if (payoutFunction.payoutFunctionPieces.length === 0)
|
|
370
|
+
throw Error('PayoutFunction must have at least once PayoutCurvePiece');
|
|
371
|
+
if (payoutFunction.payoutFunctionPieces.length > 1)
|
|
372
|
+
throw Error('More than one PayoutCurvePiece not supported');
|
|
373
|
+
const payoutCurvePiece = payoutFunction.payoutFunctionPieces[0]
|
|
374
|
+
.payoutCurvePiece as HyperbolaPayoutCurvePiece;
|
|
375
|
+
if (
|
|
376
|
+
payoutCurvePiece.payoutCurvePieceType !== PayoutCurvePieceType.Hyperbola
|
|
377
|
+
)
|
|
378
|
+
throw Error('Must be HyperbolaPayoutCurvePiece');
|
|
379
|
+
if (!payoutCurvePiece.b.eq(F64.ZERO) || !payoutCurvePiece.c.eq(F64.ZERO))
|
|
380
|
+
throw Error('b and c HyperbolaPayoutCurvePiece values must be 0');
|
|
381
|
+
// Cast to SingleOracleInfo to access announcement property
|
|
382
|
+
const singleOracleInfo = oracleInfo as SingleOracleInfo;
|
|
383
|
+
const eventDescriptor = singleOracleInfo.announcement.oracleEvent
|
|
384
|
+
.eventDescriptor as DigitDecompositionEventDescriptor;
|
|
385
|
+
if (eventDescriptor.type !== MessageType.DigitDecompositionEventDescriptor)
|
|
386
|
+
throw Error('Only DigitDecomposition Oracle Events supported');
|
|
387
|
+
|
|
388
|
+
const roundingIntervals = contractDescriptor.roundingIntervals;
|
|
389
|
+
const cetPayouts = HyperbolaPayoutCurve.computePayouts(
|
|
390
|
+
payoutFunction,
|
|
391
|
+
totalCollateral,
|
|
392
|
+
roundingIntervals,
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
const payoutGroups: PayoutGroup[] = [];
|
|
396
|
+
cetPayouts.forEach((p) => {
|
|
397
|
+
payoutGroups.push({
|
|
398
|
+
payout: p.payout,
|
|
399
|
+
groups: groupByIgnoringDigits(
|
|
400
|
+
p.indexFrom,
|
|
401
|
+
p.indexTo,
|
|
402
|
+
eventDescriptor.base,
|
|
403
|
+
contractDescriptor.numDigits,
|
|
404
|
+
),
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
const rValuesMessagesList = this.GenerateMessages(singleOracleInfo);
|
|
409
|
+
|
|
410
|
+
const { payouts, messagesList } = outputsToPayouts(
|
|
411
|
+
payoutGroups,
|
|
412
|
+
rValuesMessagesList,
|
|
413
|
+
dlcOffer.offerCollateral,
|
|
414
|
+
dlcOffer.contractInfo.totalCollateral - dlcOffer.offerCollateral,
|
|
415
|
+
true,
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
return { payouts, payoutGroups, messagesList };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
private GetPayoutsFromPolynomialPayoutFunction(
|
|
422
|
+
dlcOffer: DlcOffer,
|
|
423
|
+
contractDescriptor: NumericalDescriptor,
|
|
424
|
+
oracleInfo: SingleOracleInfo,
|
|
425
|
+
totalCollateral: bigint,
|
|
426
|
+
): GetPayoutsResponse {
|
|
427
|
+
const payoutFunction = contractDescriptor.payoutFunction as PayoutFunction;
|
|
428
|
+
if (payoutFunction.payoutFunctionPieces.length === 0)
|
|
429
|
+
throw Error('PayoutFunction must have at least once PayoutCurvePiece');
|
|
430
|
+
for (const piece of payoutFunction.payoutFunctionPieces) {
|
|
431
|
+
if (
|
|
432
|
+
piece.payoutCurvePiece.type !== MessageType.PolynomialPayoutCurvePiece
|
|
433
|
+
)
|
|
434
|
+
throw Error('Must be PolynomialPayoutCurvePiece');
|
|
435
|
+
}
|
|
436
|
+
const eventDescriptor = oracleInfo.announcement.oracleEvent
|
|
437
|
+
.eventDescriptor as DigitDecompositionEventDescriptor;
|
|
438
|
+
if (eventDescriptor.type !== MessageType.DigitDecompositionEventDescriptor)
|
|
439
|
+
throw Error('Only DigitDecomposition Oracle Events supported');
|
|
440
|
+
|
|
441
|
+
const roundingIntervals = contractDescriptor.roundingIntervals;
|
|
442
|
+
const cetPayouts = PolynomialPayoutCurve.computePayouts(
|
|
443
|
+
payoutFunction,
|
|
444
|
+
totalCollateral,
|
|
445
|
+
roundingIntervals,
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
const payoutGroups: PayoutGroup[] = [];
|
|
449
|
+
cetPayouts.forEach((p) => {
|
|
450
|
+
payoutGroups.push({
|
|
451
|
+
payout: p.payout,
|
|
452
|
+
groups: groupByIgnoringDigits(
|
|
453
|
+
p.indexFrom,
|
|
454
|
+
p.indexTo,
|
|
455
|
+
eventDescriptor.base,
|
|
456
|
+
contractDescriptor.numDigits,
|
|
457
|
+
),
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
const rValuesMessagesList = this.GenerateMessages(oracleInfo);
|
|
462
|
+
|
|
463
|
+
const { payouts, messagesList } = outputsToPayouts(
|
|
464
|
+
payoutGroups,
|
|
465
|
+
rValuesMessagesList,
|
|
466
|
+
dlcOffer.offerCollateral,
|
|
467
|
+
dlcOffer.contractInfo.totalCollateral - dlcOffer.offerCollateral,
|
|
468
|
+
true,
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
return { payouts, payoutGroups, messagesList };
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
private GetPayouts(dlcOffer: DlcOffer): GetPayoutsResponse[] {
|
|
475
|
+
const contractInfo = dlcOffer.contractInfo;
|
|
476
|
+
const totalCollateral = contractInfo.totalCollateral;
|
|
477
|
+
const contractOraclePairs = this.GetContractOraclePairs(contractInfo);
|
|
478
|
+
|
|
479
|
+
const payoutResponses = contractOraclePairs.map(
|
|
480
|
+
({ contractDescriptor, oracleInfo }) =>
|
|
481
|
+
this.GetPayoutsFromContractDescriptor(
|
|
482
|
+
dlcOffer,
|
|
483
|
+
contractDescriptor,
|
|
484
|
+
oracleInfo,
|
|
485
|
+
totalCollateral,
|
|
486
|
+
),
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
return payoutResponses;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
private FlattenPayouts(payoutResponses: GetPayoutsResponse[]) {
|
|
493
|
+
return payoutResponses.reduce(
|
|
494
|
+
(acc, { payouts, payoutGroups, messagesList }) => {
|
|
495
|
+
return {
|
|
496
|
+
payouts: acc.payouts.concat(payouts),
|
|
497
|
+
payoutGroups: acc.payoutGroups.concat(payoutGroups),
|
|
498
|
+
messagesList: acc.messagesList.concat(messagesList),
|
|
499
|
+
};
|
|
500
|
+
},
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
private GetIndicesFromPayouts(payoutResponses: GetPayoutsResponse[]) {
|
|
505
|
+
return payoutResponses.reduce(
|
|
506
|
+
(prev, acc) => {
|
|
507
|
+
return prev.concat({
|
|
508
|
+
startingMessagesIndex:
|
|
509
|
+
prev[prev.length - 1].startingMessagesIndex +
|
|
510
|
+
acc.messagesList.length,
|
|
511
|
+
startingPayoutGroupsIndex:
|
|
512
|
+
prev[prev.length - 1].startingPayoutGroupsIndex +
|
|
513
|
+
acc.payoutGroups.length,
|
|
514
|
+
});
|
|
515
|
+
},
|
|
516
|
+
[{ startingMessagesIndex: 0, startingPayoutGroupsIndex: 0 }],
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
private GetPayoutsFromEnumeratedDescriptor(
|
|
521
|
+
dlcOffer: DlcOffer,
|
|
522
|
+
contractDescriptor: EnumeratedDescriptor,
|
|
523
|
+
oracleInfo: OracleInfo,
|
|
524
|
+
totalCollateral: bigint,
|
|
525
|
+
): GetPayoutsResponse {
|
|
526
|
+
const payoutGroups: PayoutGroup[] = [];
|
|
527
|
+
const rValuesMessagesList = this.GenerateMessages(
|
|
528
|
+
oracleInfo as SingleOracleInfo,
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
// For enumerated descriptors, each outcome creates one payout
|
|
532
|
+
// Each outcome maps to one index in the oracle's possible outcomes
|
|
533
|
+
contractDescriptor.outcomes.forEach((outcome, index) => {
|
|
534
|
+
payoutGroups.push({
|
|
535
|
+
payout: outcome.localPayout,
|
|
536
|
+
groups: [[index]], // Simple index-based grouping for enum outcomes
|
|
537
|
+
});
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
const { payouts, messagesList } = outputsToPayouts(
|
|
541
|
+
payoutGroups,
|
|
542
|
+
rValuesMessagesList,
|
|
543
|
+
dlcOffer.offerCollateral,
|
|
544
|
+
totalCollateral - dlcOffer.offerCollateral,
|
|
545
|
+
true,
|
|
546
|
+
);
|
|
547
|
+
|
|
548
|
+
return { payouts, payoutGroups, messagesList };
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
private GetPayoutsFromContractDescriptor(
|
|
552
|
+
dlcOffer: DlcOffer,
|
|
553
|
+
contractDescriptor: ContractDescriptor,
|
|
554
|
+
oracleInfo: OracleInfo,
|
|
555
|
+
totalCollateral: bigint,
|
|
556
|
+
) {
|
|
557
|
+
switch (contractDescriptor.contractDescriptorType) {
|
|
558
|
+
case ContractDescriptorType.Enumerated: {
|
|
559
|
+
return this.GetPayoutsFromEnumeratedDescriptor(
|
|
560
|
+
dlcOffer,
|
|
561
|
+
contractDescriptor as EnumeratedDescriptor,
|
|
562
|
+
oracleInfo,
|
|
563
|
+
totalCollateral,
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
case ContractDescriptorType.NumericOutcome: {
|
|
567
|
+
const numericalDescriptor = contractDescriptor as NumericalDescriptor;
|
|
568
|
+
const payoutFunction = numericalDescriptor.payoutFunction;
|
|
569
|
+
|
|
570
|
+
// TODO: add a better check for this
|
|
571
|
+
const payoutCurvePiece =
|
|
572
|
+
payoutFunction.payoutFunctionPieces[0].payoutCurvePiece;
|
|
573
|
+
|
|
574
|
+
switch (payoutCurvePiece.payoutCurvePieceType) {
|
|
575
|
+
case PayoutCurvePieceType.Hyperbola:
|
|
576
|
+
return this.GetPayoutsFromPayoutFunction(
|
|
577
|
+
dlcOffer,
|
|
578
|
+
numericalDescriptor,
|
|
579
|
+
oracleInfo,
|
|
580
|
+
totalCollateral,
|
|
581
|
+
);
|
|
582
|
+
case PayoutCurvePieceType.Polynomial:
|
|
583
|
+
return this.GetPayoutsFromPolynomialPayoutFunction(
|
|
584
|
+
dlcOffer,
|
|
585
|
+
numericalDescriptor,
|
|
586
|
+
oracleInfo as SingleOracleInfo,
|
|
587
|
+
totalCollateral,
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Converts a @node-dlc/bitcoin Tx to a DDK Transaction
|
|
596
|
+
* @param tx The @node-dlc/bitcoin transaction
|
|
597
|
+
* @returns DDK Transaction object
|
|
598
|
+
*/
|
|
599
|
+
private convertTxToDdkTransaction(tx: Tx): DdkTransaction {
|
|
600
|
+
return {
|
|
601
|
+
version: tx.version,
|
|
602
|
+
lockTime: tx.locktime.value,
|
|
603
|
+
inputs: tx.inputs.map((input) => ({
|
|
604
|
+
txid: input.outpoint.txid.toString(),
|
|
605
|
+
vout: input.outpoint.outputIndex,
|
|
606
|
+
scriptSig: Buffer.from(''),
|
|
607
|
+
sequence: input.sequence.value,
|
|
608
|
+
witness: input.witness.map((witness) => witness.serialize()),
|
|
609
|
+
})),
|
|
610
|
+
outputs: tx.outputs.map((output) => ({
|
|
611
|
+
value: BigInt(output.value.sats),
|
|
612
|
+
scriptPubkey: output.scriptPubKey.serialize(),
|
|
613
|
+
})),
|
|
614
|
+
rawBytes: tx.serialize(),
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
public async createDlcTxs(
|
|
619
|
+
dlcOffer: DlcOffer,
|
|
620
|
+
dlcAccept: DlcAccept,
|
|
621
|
+
): Promise<CreateDlcTxsResponse> {
|
|
622
|
+
const localFundPubkey = dlcOffer.fundingPubkey.toString('hex');
|
|
623
|
+
const remoteFundPubkey = dlcAccept.fundingPubkey.toString('hex');
|
|
624
|
+
const localFinalScriptPubkey = dlcOffer.payoutSpk.toString('hex');
|
|
625
|
+
const remoteFinalScriptPubkey = dlcAccept.payoutSpk.toString('hex');
|
|
626
|
+
const localChangeScriptPubkey = dlcOffer.changeSpk.toString('hex');
|
|
627
|
+
const remoteChangeScriptPubkey = dlcAccept.changeSpk.toString('hex');
|
|
628
|
+
|
|
629
|
+
// Separate regular inputs from DLC inputs (only from offeror side)
|
|
630
|
+
const localRegularInputs: Utxo[] = [];
|
|
631
|
+
const localDlcInputs: DdkDlcInputInfo[] = [];
|
|
632
|
+
|
|
633
|
+
for (const fundingInput of dlcOffer.fundingInputs) {
|
|
634
|
+
if (fundingInput.dlcInput) {
|
|
635
|
+
// This is a DLC input for splicing
|
|
636
|
+
// The pubkeys should be from the original DLC to correctly spend its funding output
|
|
637
|
+
localDlcInputs.push({
|
|
638
|
+
fundTx: this.convertTxToDdkTransaction(fundingInput.prevTx),
|
|
639
|
+
fundVout: fundingInput.prevTxVout,
|
|
640
|
+
localFundPubkey: fundingInput.dlcInput.localFundPubkey,
|
|
641
|
+
remoteFundPubkey: fundingInput.dlcInput.remoteFundPubkey,
|
|
642
|
+
fundAmount:
|
|
643
|
+
fundingInput.prevTx.outputs[fundingInput.prevTxVout].value.sats,
|
|
644
|
+
maxWitnessLen: fundingInput.maxWitnessLen,
|
|
645
|
+
inputSerialId: fundingInput.inputSerialId,
|
|
646
|
+
contractId: fundingInput.dlcInput.contractId,
|
|
647
|
+
});
|
|
648
|
+
} else {
|
|
649
|
+
// Regular input
|
|
650
|
+
const input = await this.fundingInputToInput(fundingInput, false);
|
|
651
|
+
localRegularInputs.push(input.toUtxo());
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Process remote inputs (no DLC inputs from acceptor side)
|
|
656
|
+
const remoteInputs: Utxo[] = await Promise.all(
|
|
657
|
+
dlcAccept.fundingInputs.map(async (fundingInput) => {
|
|
658
|
+
const input = await this.fundingInputToInput(fundingInput, false);
|
|
659
|
+
return input.toUtxo();
|
|
660
|
+
}),
|
|
661
|
+
);
|
|
662
|
+
|
|
663
|
+
// Calculate input amounts
|
|
664
|
+
const localInputAmount = localRegularInputs.reduce<number>(
|
|
665
|
+
(prev, cur) => prev + cur.amount.GetSatoshiAmount(),
|
|
666
|
+
0,
|
|
667
|
+
);
|
|
668
|
+
|
|
669
|
+
const remoteInputAmount = remoteInputs.reduce<number>(
|
|
670
|
+
(prev, cur) => prev + cur.amount.GetSatoshiAmount(),
|
|
671
|
+
0,
|
|
672
|
+
);
|
|
673
|
+
|
|
674
|
+
let payouts: PayoutRequest[] = [];
|
|
675
|
+
let messagesList: Messages[] = [];
|
|
676
|
+
|
|
677
|
+
if (
|
|
678
|
+
dlcOffer.contractInfo.type === MessageType.SingleContractInfo &&
|
|
679
|
+
(dlcOffer.contractInfo as SingleContractInfo).contractDescriptor.type ===
|
|
680
|
+
ContractDescriptorType.Enumerated
|
|
681
|
+
) {
|
|
682
|
+
for (const outcome of (
|
|
683
|
+
(dlcOffer.contractInfo as SingleContractInfo)
|
|
684
|
+
.contractDescriptor as EnumeratedDescriptor
|
|
685
|
+
).outcomes) {
|
|
686
|
+
payouts.push({
|
|
687
|
+
local: outcome.localPayout,
|
|
688
|
+
remote:
|
|
689
|
+
dlcOffer.offerCollateral +
|
|
690
|
+
dlcAccept.acceptCollateral -
|
|
691
|
+
outcome.localPayout,
|
|
692
|
+
});
|
|
693
|
+
messagesList.push({ messages: [outcome.outcome] });
|
|
694
|
+
}
|
|
695
|
+
} else {
|
|
696
|
+
const payoutResponses = this.GetPayouts(dlcOffer);
|
|
697
|
+
const { payouts: tempPayouts, messagesList: tempMessagesList } =
|
|
698
|
+
this.FlattenPayouts(payoutResponses);
|
|
699
|
+
payouts = tempPayouts;
|
|
700
|
+
messagesList = tempMessagesList;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const outcomes: Payout[] = payouts.map((payout) => ({
|
|
704
|
+
offer: BigInt(payout.local),
|
|
705
|
+
accept: BigInt(payout.remote),
|
|
706
|
+
}));
|
|
707
|
+
|
|
708
|
+
const localParams: PartyParams = {
|
|
709
|
+
fundPubkey: Buffer.from(localFundPubkey, 'hex'),
|
|
710
|
+
changeScriptPubkey: Buffer.from(localChangeScriptPubkey, 'hex'),
|
|
711
|
+
changeSerialId: BigInt(dlcOffer.changeSerialId),
|
|
712
|
+
payoutScriptPubkey: Buffer.from(localFinalScriptPubkey, 'hex'),
|
|
713
|
+
payoutSerialId: BigInt(dlcOffer.payoutSerialId),
|
|
714
|
+
inputs: localRegularInputs.map((input) => input.toTxInputInfo()),
|
|
715
|
+
inputAmount: BigInt(localInputAmount),
|
|
716
|
+
collateral: BigInt(dlcOffer.offerCollateral),
|
|
717
|
+
dlcInputs: localDlcInputs,
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
const remoteParams: PartyParams = {
|
|
721
|
+
fundPubkey: Buffer.from(remoteFundPubkey, 'hex'),
|
|
722
|
+
changeScriptPubkey: Buffer.from(remoteChangeScriptPubkey, 'hex'),
|
|
723
|
+
changeSerialId: BigInt(dlcAccept.changeSerialId),
|
|
724
|
+
payoutScriptPubkey: Buffer.from(remoteFinalScriptPubkey, 'hex'),
|
|
725
|
+
payoutSerialId: BigInt(dlcAccept.payoutSerialId),
|
|
726
|
+
inputs: remoteInputs.map((input) => input.toTxInputInfo()),
|
|
727
|
+
inputAmount: BigInt(remoteInputAmount),
|
|
728
|
+
collateral: BigInt(dlcAccept.acceptCollateral),
|
|
729
|
+
dlcInputs: [],
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
// Determine whether to use regular or spliced DLC transactions
|
|
733
|
+
const hasDlcInputs = localDlcInputs.length > 0;
|
|
734
|
+
|
|
735
|
+
let dlcTxs: DdkDlcTransactions;
|
|
736
|
+
|
|
737
|
+
if (hasDlcInputs) {
|
|
738
|
+
// Use spliced DLC transactions when DLC inputs are present
|
|
739
|
+
dlcTxs = await this._ddk.createSplicedDlcTransactions(
|
|
740
|
+
outcomes,
|
|
741
|
+
localParams,
|
|
742
|
+
remoteParams,
|
|
743
|
+
dlcOffer.refundLocktime,
|
|
744
|
+
BigInt(dlcOffer.feeRatePerVb),
|
|
745
|
+
0,
|
|
746
|
+
dlcOffer.cetLocktime,
|
|
747
|
+
dlcOffer.fundOutputSerialId,
|
|
748
|
+
);
|
|
749
|
+
} else {
|
|
750
|
+
// Use regular DLC transactions when no DLC inputs
|
|
751
|
+
dlcTxs = this._ddk.createDlcTransactions(
|
|
752
|
+
outcomes,
|
|
753
|
+
localParams,
|
|
754
|
+
remoteParams,
|
|
755
|
+
dlcOffer.refundLocktime,
|
|
756
|
+
BigInt(dlcOffer.feeRatePerVb),
|
|
757
|
+
0,
|
|
758
|
+
dlcOffer.cetLocktime,
|
|
759
|
+
BigInt(dlcOffer.fundOutputSerialId),
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
const dlcTransactions = new DlcTransactions();
|
|
764
|
+
dlcTransactions.fundTx = Tx.decode(
|
|
765
|
+
StreamReader.fromBuffer(dlcTxs.fund.rawBytes),
|
|
766
|
+
);
|
|
767
|
+
|
|
768
|
+
// Build serial IDs based on actual outputs in the transaction
|
|
769
|
+
const actualOutputs = dlcTransactions.fundTx.outputs;
|
|
770
|
+
const serialIds: bigint[] = [];
|
|
771
|
+
|
|
772
|
+
// Always include the funding output serial ID
|
|
773
|
+
serialIds.push(BigInt(dlcOffer.fundOutputSerialId));
|
|
774
|
+
|
|
775
|
+
// Only include change serial IDs if there are actually change outputs
|
|
776
|
+
// For exact amount DLCs with no change, there will be only 1 output (the funding output)
|
|
777
|
+
if (actualOutputs.length > 1) {
|
|
778
|
+
// Multiple outputs means there are change outputs
|
|
779
|
+
if (dlcOffer.offerCollateral > 0n) {
|
|
780
|
+
serialIds.push(BigInt(dlcOffer.changeSerialId));
|
|
781
|
+
}
|
|
782
|
+
if (dlcAccept.acceptCollateral > 0n) {
|
|
783
|
+
serialIds.push(BigInt(dlcAccept.changeSerialId));
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
dlcTransactions.fundTxVout = serialIds
|
|
788
|
+
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
|
|
789
|
+
.findIndex((i) => BigInt(i) === BigInt(dlcOffer.fundOutputSerialId));
|
|
790
|
+
|
|
791
|
+
// Validate that the calculated fundTxVout is valid
|
|
792
|
+
if (
|
|
793
|
+
dlcTransactions.fundTxVout < 0 ||
|
|
794
|
+
dlcTransactions.fundTxVout >= dlcTransactions.fundTx.outputs.length
|
|
795
|
+
) {
|
|
796
|
+
throw new Error(
|
|
797
|
+
`Invalid fundTxVout calculation: calculated=${dlcTransactions.fundTxVout}, ` +
|
|
798
|
+
`fundTx.outputs.length=${dlcTransactions.fundTx.outputs.length}, ` +
|
|
799
|
+
`fundOutputSerialId=${dlcOffer.fundOutputSerialId}, ` +
|
|
800
|
+
`serialIds=[${serialIds.join(', ')}], ` +
|
|
801
|
+
`offerCollateral=${dlcOffer.offerCollateral}, ` +
|
|
802
|
+
`acceptCollateral=${dlcAccept.acceptCollateral}`,
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
dlcTransactions.cets = dlcTxs.cets.map((cetTx) =>
|
|
807
|
+
Tx.decode(StreamReader.fromBuffer(cetTx.rawBytes)),
|
|
808
|
+
);
|
|
809
|
+
dlcTransactions.refundTx = Tx.decode(
|
|
810
|
+
StreamReader.fromBuffer(dlcTxs.refund.rawBytes),
|
|
811
|
+
);
|
|
812
|
+
|
|
813
|
+
return { dlcTransactions, messagesList };
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* Computes DLC-spec compliant tagged attestation message digest
|
|
818
|
+
* This matches what the oracle should sign according to the DLC specification
|
|
819
|
+
*/
|
|
820
|
+
private computeTaggedAttestationMessage(outcome: string): string {
|
|
821
|
+
// DLC spec: H(H("DLC/oracle/attestation/v0") || H("DLC/oracle/attestation/v0") || H(outcome))
|
|
822
|
+
const tag = Buffer.from('DLC/oracle/attestation/v0', 'utf8');
|
|
823
|
+
const tagHash = sha256(tag);
|
|
824
|
+
const outcomeBuffer = Buffer.from(outcome, 'utf8');
|
|
825
|
+
|
|
826
|
+
// Compute H(tagHash || tagHash || outcomeHash)
|
|
827
|
+
const message = sha256(Buffer.concat([tagHash, tagHash, outcomeBuffer]));
|
|
828
|
+
return message.toString('hex');
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* Convert message lists to the format expected by DDK FFI
|
|
833
|
+
* DDK expects 32-byte message digests (tagged attestation messages)
|
|
834
|
+
*/
|
|
835
|
+
private convertMessagesForDdk(tempMessagesList: Messages[]): Buffer[][][] {
|
|
836
|
+
return tempMessagesList.map((message) => [
|
|
837
|
+
message.messages.map((m) =>
|
|
838
|
+
// Convert outcome string to tagged attestation message (32-byte hash)
|
|
839
|
+
Buffer.from(this.computeTaggedAttestationMessage(m), 'hex'),
|
|
840
|
+
),
|
|
841
|
+
]);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
private GenerateEnumMessages(oracleEvent: OracleEvent): Messages[] {
|
|
845
|
+
const eventDescriptor = oracleEvent.eventDescriptor as EnumEventDescriptor;
|
|
846
|
+
|
|
847
|
+
// For enum events, each oracle has one nonce and can attest to one of the possible outcomes
|
|
848
|
+
const messagesList: Messages[] = [];
|
|
849
|
+
|
|
850
|
+
// Pass raw outcome strings to dlcdevkit - it will handle the tagged hashing internally
|
|
851
|
+
// dlcdevkit expects raw strings and calls tagged_attestation_msg() internally
|
|
852
|
+
const messages = eventDescriptor.outcomes;
|
|
853
|
+
messagesList.push({ messages });
|
|
854
|
+
|
|
855
|
+
return messagesList;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
private convertToJsonSerializable(obj: any): any {
|
|
859
|
+
if (obj === null || obj === undefined) {
|
|
860
|
+
return obj;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
if (typeof obj === 'bigint') {
|
|
864
|
+
return Number(obj);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
if (Buffer.isBuffer(obj)) {
|
|
868
|
+
return obj.toString('hex');
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
if (Array.isArray(obj)) {
|
|
872
|
+
return obj.map((item) => this.convertToJsonSerializable(item));
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
if (typeof obj === 'object') {
|
|
876
|
+
const result: any = {};
|
|
877
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
878
|
+
result[key] = this.convertToJsonSerializable(value);
|
|
879
|
+
}
|
|
880
|
+
return result;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
return obj;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
private GenerateDigitDecompositionMessages(
|
|
887
|
+
oracleEvent: OracleEvent,
|
|
888
|
+
): Messages[] {
|
|
889
|
+
const oracleNonces = oracleEvent.oracleNonces;
|
|
890
|
+
const eventDescriptor =
|
|
891
|
+
oracleEvent.eventDescriptor as DigitDecompositionEventDescriptor;
|
|
892
|
+
|
|
893
|
+
const messagesList: Messages[] = [];
|
|
894
|
+
oracleNonces.forEach(() => {
|
|
895
|
+
const messages = [];
|
|
896
|
+
for (let i = 0; i < eventDescriptor.base; i++) {
|
|
897
|
+
const m = i.toString();
|
|
898
|
+
messages.push(m);
|
|
899
|
+
}
|
|
900
|
+
messagesList.push({ messages });
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
return messagesList;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
private GenerateMessages(oracleInfo: OracleInfo): Messages[] {
|
|
907
|
+
// Handle both SingleOracleInfo and MultiOracleInfo using type property instead of instanceof
|
|
908
|
+
let oracleEvent: OracleEvent;
|
|
909
|
+
|
|
910
|
+
if (oracleInfo.type === MessageType.SingleOracleInfo) {
|
|
911
|
+
const singleOracleInfo = oracleInfo as SingleOracleInfo;
|
|
912
|
+
oracleEvent = singleOracleInfo.announcement.oracleEvent;
|
|
913
|
+
} else if (oracleInfo.type === MessageType.MultiOracleInfo) {
|
|
914
|
+
const multiOracleInfo = oracleInfo as MultiOracleInfo;
|
|
915
|
+
// For multi-oracle, use the first announcement for now
|
|
916
|
+
// TODO: This might need more sophisticated handling for multi-oracle scenarios
|
|
917
|
+
if (multiOracleInfo.announcements.length === 0) {
|
|
918
|
+
throw Error('MultiOracleInfo must have at least one announcement');
|
|
919
|
+
}
|
|
920
|
+
oracleEvent = multiOracleInfo.announcements[0].oracleEvent;
|
|
921
|
+
} else {
|
|
922
|
+
throw Error(
|
|
923
|
+
`OracleInfo must be SingleOracleInfo or MultiOracleInfo, got type: ${oracleInfo.type}`,
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
switch (oracleEvent.eventDescriptor.type) {
|
|
928
|
+
case MessageType.EnumEventDescriptor:
|
|
929
|
+
return this.GenerateEnumMessages(oracleEvent);
|
|
930
|
+
case MessageType.DigitDecompositionEventDescriptor:
|
|
931
|
+
return this.GenerateDigitDecompositionMessages(oracleEvent);
|
|
932
|
+
default:
|
|
933
|
+
throw Error('EventDescriptor must be Enum or DigitDecomposition');
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
private GetContractOraclePairs(
|
|
938
|
+
_contractInfo: ContractInfo,
|
|
939
|
+
): { contractDescriptor: ContractDescriptor; oracleInfo: OracleInfo }[] {
|
|
940
|
+
// Use contractInfoType property instead of instanceof for more reliable type checking
|
|
941
|
+
if (_contractInfo.contractInfoType === ContractInfoType.Single) {
|
|
942
|
+
const singleInfo = _contractInfo as SingleContractInfo;
|
|
943
|
+
return [
|
|
944
|
+
{
|
|
945
|
+
contractDescriptor: singleInfo.contractDescriptor,
|
|
946
|
+
oracleInfo: singleInfo.oracleInfo,
|
|
947
|
+
},
|
|
948
|
+
];
|
|
949
|
+
} else if (_contractInfo.contractInfoType === ContractInfoType.Disjoint) {
|
|
950
|
+
const disjointInfo = _contractInfo as DisjointContractInfo;
|
|
951
|
+
return disjointInfo.contractOraclePairs;
|
|
952
|
+
} else {
|
|
953
|
+
throw Error('ContractInfo must be Single or Disjoint');
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
private getFundOutputValueSats(dlcTxs: DlcTransactions): bigint {
|
|
958
|
+
const fundOutput = dlcTxs.fundTx.outputs[dlcTxs.fundTxVout];
|
|
959
|
+
if (!fundOutput || !fundOutput.value) {
|
|
960
|
+
throw new Error(
|
|
961
|
+
`Invalid fund output at vout ${dlcTxs.fundTxVout}: ` +
|
|
962
|
+
`outputs.length=${dlcTxs.fundTx.outputs.length}, ` +
|
|
963
|
+
`output exists=${!!fundOutput}, ` +
|
|
964
|
+
`output.value exists=${!!(fundOutput && fundOutput.value)}`,
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
return fundOutput.value.sats;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
private async CreateCetAdaptorAndRefundSigs(
|
|
971
|
+
dlcOffer: DlcOffer,
|
|
972
|
+
dlcAccept: DlcAccept,
|
|
973
|
+
dlcTxs: DlcTransactions,
|
|
974
|
+
messagesList: Messages[],
|
|
975
|
+
isOfferer: boolean,
|
|
976
|
+
): Promise<CreateCetAdaptorAndRefundSigsResponse> {
|
|
977
|
+
const network = await this.getConnectedNetwork();
|
|
978
|
+
|
|
979
|
+
const cetsHex = dlcTxs.cets.map((cet) => cet.serialize().toString('hex'));
|
|
980
|
+
|
|
981
|
+
// Create the correct P2WSH multisig funding script
|
|
982
|
+
const fundingPubKeys =
|
|
983
|
+
Buffer.compare(dlcOffer.fundingPubkey, dlcAccept.fundingPubkey) === -1
|
|
984
|
+
? [dlcOffer.fundingPubkey, dlcAccept.fundingPubkey]
|
|
985
|
+
: [dlcAccept.fundingPubkey, dlcOffer.fundingPubkey];
|
|
986
|
+
|
|
987
|
+
const p2ms = payments.p2ms({
|
|
988
|
+
m: 2,
|
|
989
|
+
pubkeys: fundingPubKeys,
|
|
990
|
+
network,
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
// We need the redeem script (multisig), not the P2WSH output
|
|
994
|
+
const fundingSPK = p2ms.output!;
|
|
995
|
+
|
|
996
|
+
// For finding the private key, we still need the individual P2WPKH address
|
|
997
|
+
const individualFundingSPK = Script.p2wpkhLock(
|
|
998
|
+
hash160(isOfferer ? dlcOffer.fundingPubkey : dlcAccept.fundingPubkey),
|
|
999
|
+
)
|
|
1000
|
+
.serialize()
|
|
1001
|
+
.slice(1);
|
|
1002
|
+
|
|
1003
|
+
const fundingAddress: string = address.fromOutputScript(
|
|
1004
|
+
individualFundingSPK,
|
|
1005
|
+
network,
|
|
1006
|
+
);
|
|
1007
|
+
|
|
1008
|
+
const { derivationPath } = await this.client.wallet.findAddress([
|
|
1009
|
+
fundingAddress,
|
|
1010
|
+
]);
|
|
1011
|
+
|
|
1012
|
+
const fundPrivateKeyPair = await this.getMethod('keyPair')(derivationPath);
|
|
1013
|
+
const fundPrivateKey = Buffer.from(fundPrivateKeyPair.__D).toString('hex');
|
|
1014
|
+
|
|
1015
|
+
const contractOraclePairs = this.GetContractOraclePairs(
|
|
1016
|
+
dlcOffer.contractInfo,
|
|
1017
|
+
);
|
|
1018
|
+
|
|
1019
|
+
const sigs: ISig[][] = [];
|
|
1020
|
+
|
|
1021
|
+
if (
|
|
1022
|
+
dlcOffer.contractInfo.contractInfoType === ContractInfoType.Single &&
|
|
1023
|
+
(dlcOffer.contractInfo as SingleContractInfo).contractDescriptor.type ===
|
|
1024
|
+
MessageType.ContractDescriptorV0
|
|
1025
|
+
) {
|
|
1026
|
+
for (const { oracleInfo } of contractOraclePairs) {
|
|
1027
|
+
if (oracleInfo.type !== MessageType.SingleOracleInfo) {
|
|
1028
|
+
throw new Error('Only SingleOracleInfo supported in this context');
|
|
1029
|
+
}
|
|
1030
|
+
const oracleAnnouncement = (oracleInfo as SingleOracleInfo)
|
|
1031
|
+
.announcement;
|
|
1032
|
+
|
|
1033
|
+
const adaptorSigRequestPromises: Promise<AdaptorSignature[]>[] = [];
|
|
1034
|
+
|
|
1035
|
+
const tempMessagesList = messagesList;
|
|
1036
|
+
const tempCetsHex = cetsHex;
|
|
1037
|
+
|
|
1038
|
+
const ddkOracleInfo: DdkOracleInfo = {
|
|
1039
|
+
publicKey: oracleAnnouncement.oraclePubkey,
|
|
1040
|
+
nonces: oracleAnnouncement.oracleEvent.oracleNonces,
|
|
1041
|
+
};
|
|
1042
|
+
|
|
1043
|
+
adaptorSigRequestPromises.push(
|
|
1044
|
+
(async () => {
|
|
1045
|
+
const cetsForDdk = tempCetsHex.map((cetHex) =>
|
|
1046
|
+
this.convertTxToDdkTransaction(
|
|
1047
|
+
Tx.decode(StreamReader.fromHex(cetHex)),
|
|
1048
|
+
),
|
|
1049
|
+
);
|
|
1050
|
+
const messagesForDdk = this.convertMessagesForDdk(tempMessagesList);
|
|
1051
|
+
|
|
1052
|
+
const response = this._ddk.createCetAdaptorSigsFromOracleInfo(
|
|
1053
|
+
cetsForDdk,
|
|
1054
|
+
[ddkOracleInfo],
|
|
1055
|
+
Buffer.from(fundPrivateKey, 'hex'),
|
|
1056
|
+
fundingSPK,
|
|
1057
|
+
this.getFundOutputValueSats(dlcTxs),
|
|
1058
|
+
messagesForDdk,
|
|
1059
|
+
);
|
|
1060
|
+
return response;
|
|
1061
|
+
})(),
|
|
1062
|
+
);
|
|
1063
|
+
|
|
1064
|
+
const adaptorPairs: AdaptorSignature[] = (
|
|
1065
|
+
await Promise.all(adaptorSigRequestPromises)
|
|
1066
|
+
).flat();
|
|
1067
|
+
|
|
1068
|
+
sigs.push(
|
|
1069
|
+
adaptorPairs.map((adaptorPair) => {
|
|
1070
|
+
return {
|
|
1071
|
+
encryptedSig: adaptorPair.signature,
|
|
1072
|
+
dleqProof: adaptorPair.proof,
|
|
1073
|
+
};
|
|
1074
|
+
}),
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
} else {
|
|
1078
|
+
const indices = this.GetIndicesFromPayouts(this.GetPayouts(dlcOffer));
|
|
1079
|
+
|
|
1080
|
+
for (const [index, { oracleInfo }] of contractOraclePairs.entries()) {
|
|
1081
|
+
if (oracleInfo.type !== MessageType.SingleOracleInfo) {
|
|
1082
|
+
throw new Error('Only SingleOracleInfo supported in this context');
|
|
1083
|
+
}
|
|
1084
|
+
const oracleAnnouncement = (oracleInfo as SingleOracleInfo)
|
|
1085
|
+
.announcement;
|
|
1086
|
+
|
|
1087
|
+
const startingIndex = indices[index].startingMessagesIndex,
|
|
1088
|
+
endingIndex = indices[index + 1].startingMessagesIndex;
|
|
1089
|
+
|
|
1090
|
+
const oracleEventMessagesList = messagesList.slice(
|
|
1091
|
+
startingIndex,
|
|
1092
|
+
endingIndex,
|
|
1093
|
+
);
|
|
1094
|
+
const oracleEventCetsHex = cetsHex.slice(startingIndex, endingIndex);
|
|
1095
|
+
|
|
1096
|
+
const chunk = 100;
|
|
1097
|
+
const adaptorSigRequestPromises: Promise<AdaptorSignature[]>[] = [];
|
|
1098
|
+
|
|
1099
|
+
for (let i = 0, j = oracleEventMessagesList.length; i < j; i += chunk) {
|
|
1100
|
+
const tempMessagesList = oracleEventMessagesList.slice(i, i + chunk);
|
|
1101
|
+
const tempCetsHex = oracleEventCetsHex.slice(i, i + chunk);
|
|
1102
|
+
|
|
1103
|
+
const ddkOracleInfo: DdkOracleInfo = {
|
|
1104
|
+
publicKey: oracleAnnouncement.oraclePubkey,
|
|
1105
|
+
nonces: oracleAnnouncement.oracleEvent.oracleNonces,
|
|
1106
|
+
};
|
|
1107
|
+
|
|
1108
|
+
const messagesForDdk = this.convertMessagesForDdk(tempMessagesList);
|
|
1109
|
+
|
|
1110
|
+
adaptorSigRequestPromises.push(
|
|
1111
|
+
(async () => {
|
|
1112
|
+
const response = this._ddk.createCetAdaptorSigsFromOracleInfo(
|
|
1113
|
+
tempCetsHex.map((cetHex) =>
|
|
1114
|
+
this.convertTxToDdkTransaction(
|
|
1115
|
+
Tx.decode(StreamReader.fromHex(cetHex)),
|
|
1116
|
+
),
|
|
1117
|
+
),
|
|
1118
|
+
[ddkOracleInfo],
|
|
1119
|
+
Buffer.from(fundPrivateKey, 'hex'),
|
|
1120
|
+
fundingSPK,
|
|
1121
|
+
this.getFundOutputValueSats(dlcTxs),
|
|
1122
|
+
messagesForDdk,
|
|
1123
|
+
);
|
|
1124
|
+
return response;
|
|
1125
|
+
})(),
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
const adaptorPairs: AdaptorSignature[] = (
|
|
1130
|
+
await Promise.all(adaptorSigRequestPromises)
|
|
1131
|
+
).flat();
|
|
1132
|
+
|
|
1133
|
+
sigs.push(
|
|
1134
|
+
adaptorPairs.map((adaptorPair) => {
|
|
1135
|
+
return {
|
|
1136
|
+
encryptedSig: adaptorPair.signature,
|
|
1137
|
+
dleqProof: adaptorPair.proof,
|
|
1138
|
+
};
|
|
1139
|
+
}),
|
|
1140
|
+
);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
const refundSignature = this._ddk.getRawFundingTransactionInputSignature(
|
|
1145
|
+
this.convertTxToDdkTransaction(dlcTxs.refundTx),
|
|
1146
|
+
Buffer.from(fundPrivateKey, 'hex'),
|
|
1147
|
+
dlcTxs.fundTx.txId.toString(),
|
|
1148
|
+
dlcTxs.fundTxVout,
|
|
1149
|
+
dlcOffer.contractInfo.totalCollateral,
|
|
1150
|
+
);
|
|
1151
|
+
|
|
1152
|
+
const cetSignatures = new CetAdaptorSignatures();
|
|
1153
|
+
cetSignatures.sigs = sigs.flat();
|
|
1154
|
+
|
|
1155
|
+
return { cetSignatures, refundSignature };
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
private async VerifyCetAdaptorAndRefundSigs(
|
|
1159
|
+
dlcOffer: DlcOffer,
|
|
1160
|
+
dlcAccept: DlcAccept,
|
|
1161
|
+
dlcSign: DlcSign,
|
|
1162
|
+
dlcTxs: DlcTransactions,
|
|
1163
|
+
messagesList: Messages[],
|
|
1164
|
+
isOfferer: boolean,
|
|
1165
|
+
): Promise<void> {
|
|
1166
|
+
const cetsHex = dlcTxs.cets.map((cet) => cet.serialize().toString('hex'));
|
|
1167
|
+
|
|
1168
|
+
const contractOraclePairs = this.GetContractOraclePairs(
|
|
1169
|
+
dlcOffer.contractInfo,
|
|
1170
|
+
);
|
|
1171
|
+
|
|
1172
|
+
if (
|
|
1173
|
+
dlcOffer.contractInfo.type === MessageType.SingleContractInfo &&
|
|
1174
|
+
(dlcOffer.contractInfo as SingleContractInfo).contractDescriptor.type ===
|
|
1175
|
+
MessageType.ContractDescriptorV0
|
|
1176
|
+
) {
|
|
1177
|
+
for (const { oracleInfo } of contractOraclePairs) {
|
|
1178
|
+
if (oracleInfo.type !== MessageType.SingleOracleInfo) {
|
|
1179
|
+
throw new Error('Only SingleOracleInfo supported in this context');
|
|
1180
|
+
}
|
|
1181
|
+
const oracleAnnouncement = (oracleInfo as SingleOracleInfo)
|
|
1182
|
+
.announcement;
|
|
1183
|
+
|
|
1184
|
+
const oracleEventSigs = isOfferer
|
|
1185
|
+
? dlcAccept.cetAdaptorSignatures.sigs
|
|
1186
|
+
: dlcSign.cetAdaptorSignatures.sigs;
|
|
1187
|
+
|
|
1188
|
+
const sigsValidity: Promise<boolean>[] = [];
|
|
1189
|
+
|
|
1190
|
+
const tempMessagesList = messagesList;
|
|
1191
|
+
const tempSigs = oracleEventSigs;
|
|
1192
|
+
const tempAdaptorPairs = tempSigs.map((sig) => {
|
|
1193
|
+
return {
|
|
1194
|
+
signature: sig.encryptedSig,
|
|
1195
|
+
proof: sig.dleqProof,
|
|
1196
|
+
};
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
// Create the correct P2WSH multisig funding script for verification
|
|
1200
|
+
const network = await this.getConnectedNetwork();
|
|
1201
|
+
const verifyFundingPubKeys =
|
|
1202
|
+
Buffer.compare(dlcOffer.fundingPubkey, dlcAccept.fundingPubkey) === -1
|
|
1203
|
+
? [dlcOffer.fundingPubkey, dlcAccept.fundingPubkey]
|
|
1204
|
+
: [dlcAccept.fundingPubkey, dlcOffer.fundingPubkey];
|
|
1205
|
+
|
|
1206
|
+
const verifyP2ms = payments.p2ms({
|
|
1207
|
+
m: 2,
|
|
1208
|
+
pubkeys: verifyFundingPubKeys,
|
|
1209
|
+
network,
|
|
1210
|
+
});
|
|
1211
|
+
|
|
1212
|
+
// We need the redeem script (multisig), not the P2WSH output
|
|
1213
|
+
const fundingSPK = verifyP2ms.output!;
|
|
1214
|
+
|
|
1215
|
+
const ddkOracleInfo: DdkOracleInfo = {
|
|
1216
|
+
publicKey: oracleAnnouncement.oraclePubkey,
|
|
1217
|
+
nonces: oracleAnnouncement.oracleEvent.oracleNonces,
|
|
1218
|
+
};
|
|
1219
|
+
|
|
1220
|
+
const pubkey = isOfferer
|
|
1221
|
+
? dlcAccept.fundingPubkey
|
|
1222
|
+
: dlcOffer.fundingPubkey;
|
|
1223
|
+
|
|
1224
|
+
const messagesForDdk = this.convertMessagesForDdk(tempMessagesList);
|
|
1225
|
+
|
|
1226
|
+
sigsValidity.push(
|
|
1227
|
+
(async () => {
|
|
1228
|
+
const response = this._ddk.verifyCetAdaptorSigsFromOracleInfo(
|
|
1229
|
+
tempAdaptorPairs,
|
|
1230
|
+
dlcTxs.cets.map((cet) => this.convertTxToDdkTransaction(cet)),
|
|
1231
|
+
[ddkOracleInfo],
|
|
1232
|
+
pubkey,
|
|
1233
|
+
fundingSPK,
|
|
1234
|
+
this.getFundOutputValueSats(dlcTxs),
|
|
1235
|
+
messagesForDdk,
|
|
1236
|
+
);
|
|
1237
|
+
return response;
|
|
1238
|
+
})(),
|
|
1239
|
+
);
|
|
1240
|
+
|
|
1241
|
+
let areSigsValid = (await Promise.all(sigsValidity)).every((b) => b);
|
|
1242
|
+
|
|
1243
|
+
await this.VerifyRefundSignatureAlt(
|
|
1244
|
+
dlcOffer,
|
|
1245
|
+
dlcAccept,
|
|
1246
|
+
dlcSign,
|
|
1247
|
+
dlcTxs,
|
|
1248
|
+
isOfferer,
|
|
1249
|
+
);
|
|
1250
|
+
|
|
1251
|
+
if (!areSigsValid) {
|
|
1252
|
+
throw new Error('Invalid signatures received');
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
} else {
|
|
1256
|
+
const chunk = 100;
|
|
1257
|
+
|
|
1258
|
+
const indices = this.GetIndicesFromPayouts(this.GetPayouts(dlcOffer));
|
|
1259
|
+
|
|
1260
|
+
for (const [index, { oracleInfo }] of contractOraclePairs.entries()) {
|
|
1261
|
+
if (oracleInfo.type !== MessageType.SingleOracleInfo) {
|
|
1262
|
+
throw new Error('Only SingleOracleInfo supported in this context');
|
|
1263
|
+
}
|
|
1264
|
+
const oracleAnnouncement = (oracleInfo as SingleOracleInfo)
|
|
1265
|
+
.announcement;
|
|
1266
|
+
|
|
1267
|
+
const startingIndex = indices[index].startingMessagesIndex,
|
|
1268
|
+
endingIndex = indices[index + 1].startingMessagesIndex;
|
|
1269
|
+
|
|
1270
|
+
const oracleEventMessagesList = messagesList.slice(
|
|
1271
|
+
startingIndex,
|
|
1272
|
+
endingIndex,
|
|
1273
|
+
);
|
|
1274
|
+
const oracleEventCetsHex = cetsHex.slice(startingIndex, endingIndex);
|
|
1275
|
+
const oracleEventSigs = (
|
|
1276
|
+
isOfferer
|
|
1277
|
+
? dlcAccept.cetAdaptorSignatures.sigs
|
|
1278
|
+
: dlcSign.cetAdaptorSignatures.sigs
|
|
1279
|
+
).slice(startingIndex, endingIndex);
|
|
1280
|
+
|
|
1281
|
+
const sigsValidity: Promise<boolean>[] = [];
|
|
1282
|
+
|
|
1283
|
+
for (let i = 0, j = oracleEventMessagesList.length; i < j; i += chunk) {
|
|
1284
|
+
const tempMessagesList = oracleEventMessagesList.slice(i, i + chunk);
|
|
1285
|
+
const tempCetsHex = oracleEventCetsHex.slice(i, i + chunk);
|
|
1286
|
+
const tempSigs = oracleEventSigs.slice(i, i + chunk);
|
|
1287
|
+
const tempAdaptorPairs = tempSigs.map((sig) => {
|
|
1288
|
+
return {
|
|
1289
|
+
signature: sig.encryptedSig,
|
|
1290
|
+
proof: sig.dleqProof,
|
|
1291
|
+
};
|
|
1292
|
+
});
|
|
1293
|
+
|
|
1294
|
+
// Create the correct P2WSH multisig funding script
|
|
1295
|
+
const network = await this.getConnectedNetwork();
|
|
1296
|
+
const nonEnumFundingPubKeys =
|
|
1297
|
+
Buffer.compare(dlcOffer.fundingPubkey, dlcAccept.fundingPubkey) ===
|
|
1298
|
+
-1
|
|
1299
|
+
? [dlcOffer.fundingPubkey, dlcAccept.fundingPubkey]
|
|
1300
|
+
: [dlcAccept.fundingPubkey, dlcOffer.fundingPubkey];
|
|
1301
|
+
|
|
1302
|
+
const nonEnumP2ms = payments.p2ms({
|
|
1303
|
+
m: 2,
|
|
1304
|
+
pubkeys: nonEnumFundingPubKeys,
|
|
1305
|
+
network,
|
|
1306
|
+
});
|
|
1307
|
+
|
|
1308
|
+
// We need the redeem script (multisig), not the P2WSH output
|
|
1309
|
+
const fundingSPK = nonEnumP2ms.output!;
|
|
1310
|
+
|
|
1311
|
+
const ddkOracleInfo: DdkOracleInfo = {
|
|
1312
|
+
publicKey: oracleAnnouncement.oraclePubkey,
|
|
1313
|
+
nonces: oracleAnnouncement.oracleEvent.oracleNonces,
|
|
1314
|
+
};
|
|
1315
|
+
|
|
1316
|
+
const pubkey = isOfferer
|
|
1317
|
+
? dlcAccept.fundingPubkey
|
|
1318
|
+
: dlcOffer.fundingPubkey;
|
|
1319
|
+
|
|
1320
|
+
const messagesForDdk = this.convertMessagesForDdk(tempMessagesList);
|
|
1321
|
+
|
|
1322
|
+
sigsValidity.push(
|
|
1323
|
+
(async () => {
|
|
1324
|
+
const response = this._ddk.verifyCetAdaptorSigsFromOracleInfo(
|
|
1325
|
+
tempAdaptorPairs,
|
|
1326
|
+
tempCetsHex.map((cet) =>
|
|
1327
|
+
this.convertTxToDdkTransaction(
|
|
1328
|
+
Tx.decode(StreamReader.fromHex(cet)),
|
|
1329
|
+
),
|
|
1330
|
+
),
|
|
1331
|
+
[ddkOracleInfo],
|
|
1332
|
+
pubkey,
|
|
1333
|
+
fundingSPK,
|
|
1334
|
+
this.getFundOutputValueSats(dlcTxs),
|
|
1335
|
+
messagesForDdk,
|
|
1336
|
+
);
|
|
1337
|
+
return response;
|
|
1338
|
+
})(),
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
let areSigsValid = (await Promise.all(sigsValidity)).every((b) => b);
|
|
1343
|
+
|
|
1344
|
+
// Verify refund signature using PSBT approach
|
|
1345
|
+
await this.VerifyRefundSignatureAlt(
|
|
1346
|
+
dlcOffer,
|
|
1347
|
+
dlcAccept,
|
|
1348
|
+
dlcSign,
|
|
1349
|
+
dlcTxs,
|
|
1350
|
+
isOfferer,
|
|
1351
|
+
);
|
|
1352
|
+
|
|
1353
|
+
if (!areSigsValid) {
|
|
1354
|
+
throw new Error('Invalid CET adaptor signatures received');
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
private async CreateFundingSigsAlt(
|
|
1361
|
+
dlcOffer: DlcOffer,
|
|
1362
|
+
dlcAccept: DlcAccept,
|
|
1363
|
+
dlcTxs: DlcTransactions,
|
|
1364
|
+
isOfferer: boolean,
|
|
1365
|
+
): Promise<FundingSignatures> {
|
|
1366
|
+
const transaction = btTransaction.fromBuffer(dlcTxs.fundTx.serialize());
|
|
1367
|
+
const network = await this.getConnectedNetwork();
|
|
1368
|
+
const psbt = new Psbt({ network });
|
|
1369
|
+
|
|
1370
|
+
// Combine all funding inputs from both parties
|
|
1371
|
+
const allFundingInputs = [
|
|
1372
|
+
...dlcOffer.fundingInputs,
|
|
1373
|
+
...dlcAccept.fundingInputs,
|
|
1374
|
+
];
|
|
1375
|
+
|
|
1376
|
+
// Sort by inputSerialId to reconstruct proper transaction order
|
|
1377
|
+
allFundingInputs.sort((a, b) => Number(a.inputSerialId - b.inputSerialId));
|
|
1378
|
+
|
|
1379
|
+
// Add all inputs to PSBT with proper witnessUtxo
|
|
1380
|
+
for (const fundingInput of allFundingInputs) {
|
|
1381
|
+
const prevOut = fundingInput.prevTx.outputs[fundingInput.prevTxVout];
|
|
1382
|
+
|
|
1383
|
+
// Use the same pattern as existing code - slice(1) to remove length prefix
|
|
1384
|
+
const witnessUtxo = {
|
|
1385
|
+
script: prevOut.scriptPubKey.serialize().subarray(1),
|
|
1386
|
+
value: Number(prevOut.value.sats),
|
|
1387
|
+
};
|
|
1388
|
+
|
|
1389
|
+
// Use sequence from the original transaction to ensure consistency
|
|
1390
|
+
const originalInput = transaction.ins.find(
|
|
1391
|
+
(input) =>
|
|
1392
|
+
input.hash.reverse().toString('hex') ===
|
|
1393
|
+
fundingInput.prevTx.txId.toString() &&
|
|
1394
|
+
input.index === fundingInput.prevTxVout,
|
|
1395
|
+
);
|
|
1396
|
+
const sequenceValue = originalInput
|
|
1397
|
+
? originalInput.sequence
|
|
1398
|
+
: Number(fundingInput.sequence);
|
|
1399
|
+
|
|
1400
|
+
psbt.addInput({
|
|
1401
|
+
hash: fundingInput.prevTx.txId.toString(),
|
|
1402
|
+
index: fundingInput.prevTxVout,
|
|
1403
|
+
sequence: sequenceValue,
|
|
1404
|
+
witnessUtxo,
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
// Add all outputs to PSBT (maintains transaction structure)
|
|
1409
|
+
for (const output of transaction.outs) {
|
|
1410
|
+
psbt.addOutput({
|
|
1411
|
+
address: address.fromOutputScript(output.script, network),
|
|
1412
|
+
value: output.value,
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
// Determine which inputs belong to this party
|
|
1417
|
+
const partyFundingInputs = isOfferer
|
|
1418
|
+
? dlcOffer.fundingInputs
|
|
1419
|
+
: dlcAccept.fundingInputs;
|
|
1420
|
+
|
|
1421
|
+
// Convert party's funding inputs to Input objects and get private keys
|
|
1422
|
+
const partyInputs: Input[] = await Promise.all(
|
|
1423
|
+
partyFundingInputs.map(async (fundingInput) => {
|
|
1424
|
+
return this.fundingInputToInput(fundingInput);
|
|
1425
|
+
}),
|
|
1426
|
+
);
|
|
1427
|
+
|
|
1428
|
+
const inputPrivKeys = await this.GetPrivKeysForInputs(partyInputs);
|
|
1429
|
+
|
|
1430
|
+
// Create map of this party's inputs for efficient lookup
|
|
1431
|
+
const partyInputMap = new Map<string, { input: Input; privKey: string }>();
|
|
1432
|
+
partyInputs.forEach((input, index) => {
|
|
1433
|
+
const key = `${input.txid}:${input.vout}`;
|
|
1434
|
+
partyInputMap.set(key, { input, privKey: inputPrivKeys[index] });
|
|
1435
|
+
});
|
|
1436
|
+
|
|
1437
|
+
// Initialize witness elements array to match all inputs
|
|
1438
|
+
const witnessElements: ScriptWitnessV0[][] = [];
|
|
1439
|
+
|
|
1440
|
+
// Sign only this party's inputs
|
|
1441
|
+
for (
|
|
1442
|
+
let inputIndex = 0;
|
|
1443
|
+
inputIndex < allFundingInputs.length;
|
|
1444
|
+
inputIndex++
|
|
1445
|
+
) {
|
|
1446
|
+
const fundingInput = allFundingInputs[inputIndex];
|
|
1447
|
+
const inputKey = `${fundingInput.prevTx.txId.toString()}:${fundingInput.prevTxVout}`;
|
|
1448
|
+
|
|
1449
|
+
if (partyInputMap.has(inputKey)) {
|
|
1450
|
+
// This input belongs to this party - sign it
|
|
1451
|
+
const { privKey } = partyInputMap.get(inputKey)!;
|
|
1452
|
+
const keyPair = ECPair.fromPrivateKey(Buffer.from(privKey, 'hex'));
|
|
1453
|
+
|
|
1454
|
+
// Check if this is a DLC input (2-of-2 multisig from previous DLC)
|
|
1455
|
+
if (fundingInput.dlcInput) {
|
|
1456
|
+
// For DLC inputs, we'll need to handle 2-of-2 multisig signing differently
|
|
1457
|
+
// For now, throw an error as this requires implementing multisig support
|
|
1458
|
+
throw new Error(
|
|
1459
|
+
'DLC input signing not yet implemented in CreateFundingSigsAlt',
|
|
1460
|
+
);
|
|
1461
|
+
} else {
|
|
1462
|
+
// For P2WPKH inputs, use PSBT signing
|
|
1463
|
+
psbt.signInput(inputIndex, keyPair);
|
|
1464
|
+
|
|
1465
|
+
// Extract signature from partial signatures (more reliable than finalization)
|
|
1466
|
+
const inputData = psbt.data.inputs[inputIndex];
|
|
1467
|
+
const partialSigs = inputData.partialSig;
|
|
1468
|
+
if (!partialSigs || partialSigs.length === 0) {
|
|
1469
|
+
throw new Error(
|
|
1470
|
+
`No signatures found for input ${inputIndex} after signing`,
|
|
1471
|
+
);
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
// For P2WPKH, create witness manually: [signature, publicKey]
|
|
1475
|
+
const sigWitness = new ScriptWitnessV0();
|
|
1476
|
+
sigWitness.witness = partialSigs[0].signature;
|
|
1477
|
+
const pubKeyWitness = new ScriptWitnessV0();
|
|
1478
|
+
pubKeyWitness.witness = keyPair.publicKey;
|
|
1479
|
+
|
|
1480
|
+
witnessElements.push([sigWitness, pubKeyWitness]);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
// Note: We don't add anything to witnessElements for other party's inputs
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
const fundingSignatures = new FundingSignatures();
|
|
1487
|
+
fundingSignatures.witnessElements = witnessElements;
|
|
1488
|
+
|
|
1489
|
+
return fundingSignatures;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
private async VerifyFundingSigsAlt(
|
|
1493
|
+
dlcOffer: DlcOffer,
|
|
1494
|
+
dlcAccept: DlcAccept,
|
|
1495
|
+
dlcSign: DlcSign,
|
|
1496
|
+
dlcTxs: DlcTransactions,
|
|
1497
|
+
isOfferer: boolean,
|
|
1498
|
+
): Promise<void> {
|
|
1499
|
+
const network = await this.getConnectedNetwork();
|
|
1500
|
+
const transaction = btTransaction.fromBuffer(dlcTxs.fundTx.serialize());
|
|
1501
|
+
|
|
1502
|
+
// Get the party whose signatures we're verifying
|
|
1503
|
+
// If we're the offerer, we verify accepter's signatures (from dlcSign)
|
|
1504
|
+
// If we're the accepter, we verify offerer's signatures (from dlcSign)
|
|
1505
|
+
const signingPartyInputs = isOfferer
|
|
1506
|
+
? dlcAccept.fundingInputs
|
|
1507
|
+
: dlcOffer.fundingInputs;
|
|
1508
|
+
|
|
1509
|
+
// Combine all inputs to get the correct ordering
|
|
1510
|
+
const allFundingInputs = [
|
|
1511
|
+
...dlcOffer.fundingInputs,
|
|
1512
|
+
...dlcAccept.fundingInputs,
|
|
1513
|
+
];
|
|
1514
|
+
allFundingInputs.sort((a, b) => Number(a.inputSerialId - b.inputSerialId));
|
|
1515
|
+
|
|
1516
|
+
// Compare transaction IDs
|
|
1517
|
+
const dlcFundTxId = dlcTxs.fundTx.txId.toString();
|
|
1518
|
+
const psbtBuilderTxId = transaction.getId();
|
|
1519
|
+
|
|
1520
|
+
assert(dlcFundTxId === psbtBuilderTxId, 'Transaction IDs do not match');
|
|
1521
|
+
|
|
1522
|
+
// Create a PSBT for signature verification
|
|
1523
|
+
const psbt = new Psbt({ network });
|
|
1524
|
+
|
|
1525
|
+
// Add all inputs (needed for proper sighash calculation)
|
|
1526
|
+
for (const input of allFundingInputs) {
|
|
1527
|
+
const prevOutput = input.prevTx.outputs[input.prevTxVout];
|
|
1528
|
+
psbt.addInput({
|
|
1529
|
+
hash: input.prevTx.txId.toString(),
|
|
1530
|
+
index: input.prevTxVout,
|
|
1531
|
+
sequence: 0,
|
|
1532
|
+
witnessUtxo: {
|
|
1533
|
+
script: prevOutput.scriptPubKey.serialize().subarray(1),
|
|
1534
|
+
value: Number(prevOutput.value.sats),
|
|
1535
|
+
},
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
// Add all outputs
|
|
1540
|
+
for (const output of transaction.outs) {
|
|
1541
|
+
psbt.addOutput({
|
|
1542
|
+
address: address.fromOutputScript(output.script, network),
|
|
1543
|
+
value: output.value,
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
if (
|
|
1548
|
+
psbt.inputCount !== transaction.ins.length ||
|
|
1549
|
+
psbt.txOutputs.length !== transaction.outs.length
|
|
1550
|
+
) {
|
|
1551
|
+
throw new Error(`PSBT structure doesn't match original transaction`);
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
// Add the funding signatures to the PSBT as partial signatures
|
|
1555
|
+
let witnessIndex = 0;
|
|
1556
|
+
for (const fundingInput of signingPartyInputs) {
|
|
1557
|
+
// Skip DLC inputs for now (same as CreateFundingSigsAlt)
|
|
1558
|
+
if (fundingInput.dlcInput) {
|
|
1559
|
+
continue;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
if (witnessIndex >= dlcSign.fundingSignatures.witnessElements.length) {
|
|
1563
|
+
throw new Error(
|
|
1564
|
+
`Not enough witness elements: expected at least ${witnessIndex + 1}, got ${dlcSign.fundingSignatures.witnessElements.length}`,
|
|
1565
|
+
);
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
const witnessElement =
|
|
1569
|
+
dlcSign.fundingSignatures.witnessElements[witnessIndex];
|
|
1570
|
+
const signature = witnessElement[0].witness;
|
|
1571
|
+
const publicKey = witnessElement[1].witness;
|
|
1572
|
+
|
|
1573
|
+
// Find this input's index in the sorted transaction
|
|
1574
|
+
const inputIndex = allFundingInputs.findIndex(
|
|
1575
|
+
(input) =>
|
|
1576
|
+
input.prevTx.txId.toString() ===
|
|
1577
|
+
fundingInput.prevTx.txId.toString() &&
|
|
1578
|
+
input.prevTxVout === fundingInput.prevTxVout,
|
|
1579
|
+
);
|
|
1580
|
+
|
|
1581
|
+
if (inputIndex === -1) {
|
|
1582
|
+
throw new Error(
|
|
1583
|
+
`Input not found in transaction: ${fundingInput.prevTx.txId.toString()}:${fundingInput.prevTxVout}`,
|
|
1584
|
+
);
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
psbt.updateInput(inputIndex, {
|
|
1588
|
+
partialSig: [{ pubkey: publicKey, signature: signature }],
|
|
1589
|
+
});
|
|
1590
|
+
|
|
1591
|
+
psbt.validateSignaturesOfInput(
|
|
1592
|
+
inputIndex,
|
|
1593
|
+
(pubkey: Buffer, msghash: Buffer, signature: Buffer) => {
|
|
1594
|
+
return ecc.verify(msghash, pubkey, signature);
|
|
1595
|
+
},
|
|
1596
|
+
);
|
|
1597
|
+
|
|
1598
|
+
witnessIndex++;
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
private async VerifyRefundSignatureAlt(
|
|
1603
|
+
dlcOffer: DlcOffer,
|
|
1604
|
+
dlcAccept: DlcAccept,
|
|
1605
|
+
dlcSign: DlcSign,
|
|
1606
|
+
dlcTxs: DlcTransactions,
|
|
1607
|
+
isOfferer: boolean,
|
|
1608
|
+
): Promise<void> {
|
|
1609
|
+
const network = await this.getConnectedNetwork();
|
|
1610
|
+
|
|
1611
|
+
// Get the refund signature we need to verify
|
|
1612
|
+
// If we're the offerer, we verify accepter's refund signature (from dlcAccept)
|
|
1613
|
+
// If we're the accepter, we verify offerer's refund signature (from dlcSign)
|
|
1614
|
+
const refundSignature = isOfferer
|
|
1615
|
+
? dlcAccept.refundSignature
|
|
1616
|
+
: dlcSign.refundSignature;
|
|
1617
|
+
const signingPubkey = isOfferer
|
|
1618
|
+
? dlcAccept.fundingPubkey
|
|
1619
|
+
: dlcOffer.fundingPubkey;
|
|
1620
|
+
|
|
1621
|
+
// Verify refund transaction locktime matches expected
|
|
1622
|
+
if (Number(dlcTxs.refundTx.locktime) !== dlcOffer.refundLocktime) {
|
|
1623
|
+
throw new Error(
|
|
1624
|
+
`Refund transaction locktime ${dlcTxs.refundTx.locktime} does not match expected ${dlcOffer.refundLocktime}`,
|
|
1625
|
+
);
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
// Create a PSBT for the refund transaction verification
|
|
1629
|
+
const psbt = new Psbt({ network });
|
|
1630
|
+
|
|
1631
|
+
// Add the funding input
|
|
1632
|
+
const fundingOutput = dlcTxs.fundTx.outputs[dlcTxs.fundTxVout];
|
|
1633
|
+
psbt.addInput({
|
|
1634
|
+
hash: dlcTxs.fundTx.txId.toString(),
|
|
1635
|
+
index: dlcTxs.fundTxVout,
|
|
1636
|
+
sequence: 0,
|
|
1637
|
+
witnessUtxo: {
|
|
1638
|
+
script: fundingOutput.scriptPubKey.serialize().subarray(1), // Remove length prefix
|
|
1639
|
+
value: Number(fundingOutput.value.sats),
|
|
1640
|
+
},
|
|
1641
|
+
witnessScript: this.CreateFundingScript(dlcOffer, dlcAccept), // 2-of-2 multisig script
|
|
1642
|
+
});
|
|
1643
|
+
|
|
1644
|
+
// Add the refund output
|
|
1645
|
+
const refundOutput = dlcTxs.refundTx.outputs[0];
|
|
1646
|
+
psbt.addOutput({
|
|
1647
|
+
address: address.fromOutputScript(
|
|
1648
|
+
refundOutput.scriptPubKey.serialize().subarray(1),
|
|
1649
|
+
network,
|
|
1650
|
+
),
|
|
1651
|
+
value: Number(refundOutput.value.sats),
|
|
1652
|
+
});
|
|
1653
|
+
|
|
1654
|
+
// Set the locktime to match the refund transaction
|
|
1655
|
+
psbt.setLocktime(Number(dlcTxs.refundTx.locktime));
|
|
1656
|
+
|
|
1657
|
+
// Add the refund signature as a partial signature
|
|
1658
|
+
psbt.updateInput(0, {
|
|
1659
|
+
partialSig: [{ pubkey: signingPubkey, signature: refundSignature }],
|
|
1660
|
+
});
|
|
1661
|
+
|
|
1662
|
+
// Validate the refund signature
|
|
1663
|
+
try {
|
|
1664
|
+
psbt.validateSignaturesOfInput(
|
|
1665
|
+
0,
|
|
1666
|
+
(pubkey: Buffer, msghash: Buffer, signature: Buffer) => {
|
|
1667
|
+
return ecc.verify(msghash, pubkey, signature);
|
|
1668
|
+
},
|
|
1669
|
+
);
|
|
1670
|
+
} catch (error) {
|
|
1671
|
+
throw new Error(
|
|
1672
|
+
`Refund signature validation failed for ${isOfferer ? 'accepter' : 'offerer'}: ${error.message}`,
|
|
1673
|
+
);
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
private CreateFundingScript(
|
|
1678
|
+
dlcOffer: DlcOffer,
|
|
1679
|
+
dlcAccept: DlcAccept,
|
|
1680
|
+
): Buffer {
|
|
1681
|
+
const network = this.getBitcoinJsNetwork();
|
|
1682
|
+
|
|
1683
|
+
// Sort funding pubkeys in lexicographical order as per DLC spec
|
|
1684
|
+
const fundingPubKeys =
|
|
1685
|
+
Buffer.compare(dlcOffer.fundingPubkey, dlcAccept.fundingPubkey) === -1
|
|
1686
|
+
? [dlcOffer.fundingPubkey, dlcAccept.fundingPubkey]
|
|
1687
|
+
: [dlcAccept.fundingPubkey, dlcOffer.fundingPubkey];
|
|
1688
|
+
|
|
1689
|
+
// Create 2-of-2 multisig script
|
|
1690
|
+
const p2ms = payments.p2ms({
|
|
1691
|
+
m: 2,
|
|
1692
|
+
pubkeys: fundingPubKeys,
|
|
1693
|
+
network,
|
|
1694
|
+
});
|
|
1695
|
+
|
|
1696
|
+
const paymentVariant = payments.p2wsh({
|
|
1697
|
+
redeem: p2ms,
|
|
1698
|
+
network,
|
|
1699
|
+
});
|
|
1700
|
+
|
|
1701
|
+
return paymentVariant.redeem!.output!;
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
private async CreateFundingTx(
|
|
1705
|
+
dlcOffer: DlcOffer,
|
|
1706
|
+
dlcAccept: DlcAccept,
|
|
1707
|
+
dlcSign: DlcSign,
|
|
1708
|
+
dlcTxs: DlcTransactions,
|
|
1709
|
+
fundingSignatures: FundingSignatures,
|
|
1710
|
+
): Promise<Tx> {
|
|
1711
|
+
const network = await this.getConnectedNetwork();
|
|
1712
|
+
const psbt = new Psbt({ network });
|
|
1713
|
+
|
|
1714
|
+
// Combine and sort all funding inputs by serial ID (same as CreateFundingSigsAlt)
|
|
1715
|
+
const allFundingInputs = [
|
|
1716
|
+
...dlcOffer.fundingInputs,
|
|
1717
|
+
...dlcAccept.fundingInputs,
|
|
1718
|
+
];
|
|
1719
|
+
allFundingInputs.sort((a, b) => Number(a.inputSerialId - b.inputSerialId));
|
|
1720
|
+
|
|
1721
|
+
// Create a map of input txid:vout to witness elements
|
|
1722
|
+
const witnessMap = new Map<string, ScriptWitnessV0[]>();
|
|
1723
|
+
|
|
1724
|
+
// Map witness elements correctly - CreateFundingSigsAlt only creates witness elements
|
|
1725
|
+
// for the party's own inputs, so we need to map them correctly
|
|
1726
|
+
|
|
1727
|
+
// For dlcSign (offerer's signatures), map to offerer's inputs only
|
|
1728
|
+
let offererWitnessIndex = 0;
|
|
1729
|
+
dlcOffer.fundingInputs.forEach((fundingInput) => {
|
|
1730
|
+
// Skip DLC inputs for now as in CreateFundingSigsAlt
|
|
1731
|
+
if (fundingInput.dlcInput) {
|
|
1732
|
+
return;
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
if (
|
|
1736
|
+
offererWitnessIndex < dlcSign.fundingSignatures.witnessElements.length
|
|
1737
|
+
) {
|
|
1738
|
+
const key = `${fundingInput.prevTx.txId.toString()}:${fundingInput.prevTxVout}`;
|
|
1739
|
+
witnessMap.set(
|
|
1740
|
+
key,
|
|
1741
|
+
dlcSign.fundingSignatures.witnessElements[offererWitnessIndex],
|
|
1742
|
+
);
|
|
1743
|
+
offererWitnessIndex++;
|
|
1744
|
+
}
|
|
1745
|
+
});
|
|
1746
|
+
|
|
1747
|
+
// For fundingSignatures (accepter's signatures), map to accepter's inputs only
|
|
1748
|
+
let accepterWitnessIndex = 0;
|
|
1749
|
+
dlcAccept.fundingInputs.forEach((fundingInput) => {
|
|
1750
|
+
// Skip DLC inputs for now as in CreateFundingSigsAlt
|
|
1751
|
+
if (fundingInput.dlcInput) {
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
if (accepterWitnessIndex < fundingSignatures.witnessElements.length) {
|
|
1756
|
+
const key = `${fundingInput.prevTx.txId.toString()}:${fundingInput.prevTxVout}`;
|
|
1757
|
+
witnessMap.set(
|
|
1758
|
+
key,
|
|
1759
|
+
fundingSignatures.witnessElements[accepterWitnessIndex],
|
|
1760
|
+
);
|
|
1761
|
+
accepterWitnessIndex++;
|
|
1762
|
+
}
|
|
1763
|
+
});
|
|
1764
|
+
|
|
1765
|
+
// Add all inputs to PSBT with proper sequence values
|
|
1766
|
+
const originalTransaction = btTransaction.fromBuffer(
|
|
1767
|
+
dlcTxs.fundTx.serialize(),
|
|
1768
|
+
);
|
|
1769
|
+
|
|
1770
|
+
for (const fundingInput of allFundingInputs) {
|
|
1771
|
+
const prevOut = fundingInput.prevTx.outputs[fundingInput.prevTxVout];
|
|
1772
|
+
|
|
1773
|
+
// Use same script handling as CreateFundingSigsAlt
|
|
1774
|
+
const witnessUtxo = {
|
|
1775
|
+
script: prevOut.scriptPubKey.serialize().subarray(1),
|
|
1776
|
+
value: Number(prevOut.value.sats),
|
|
1777
|
+
};
|
|
1778
|
+
|
|
1779
|
+
// Use sequence from the original transaction (same as CreateFundingSigsAlt)
|
|
1780
|
+
const originalInput = originalTransaction.ins.find(
|
|
1781
|
+
(input) =>
|
|
1782
|
+
input.hash.reverse().toString('hex') ===
|
|
1783
|
+
fundingInput.prevTx.txId.toString() &&
|
|
1784
|
+
input.index === fundingInput.prevTxVout,
|
|
1785
|
+
);
|
|
1786
|
+
const sequenceValue = originalInput
|
|
1787
|
+
? originalInput.sequence
|
|
1788
|
+
: Number(fundingInput.sequence);
|
|
1789
|
+
|
|
1790
|
+
psbt.addInput({
|
|
1791
|
+
hash: fundingInput.prevTx.txId.toString(),
|
|
1792
|
+
index: fundingInput.prevTxVout,
|
|
1793
|
+
sequence: sequenceValue,
|
|
1794
|
+
witnessUtxo,
|
|
1795
|
+
});
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
// Add all outputs from the funding transaction
|
|
1799
|
+
for (const output of originalTransaction.outs) {
|
|
1800
|
+
psbt.addOutput({
|
|
1801
|
+
address: address.fromOutputScript(output.script, network),
|
|
1802
|
+
value: output.value,
|
|
1803
|
+
});
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
// Finalize inputs with their witness data
|
|
1807
|
+
for (
|
|
1808
|
+
let inputIndex = 0;
|
|
1809
|
+
inputIndex < allFundingInputs.length;
|
|
1810
|
+
inputIndex++
|
|
1811
|
+
) {
|
|
1812
|
+
const fundingInput = allFundingInputs[inputIndex];
|
|
1813
|
+
const inputKey = `${fundingInput.prevTx.txId.toString()}:${fundingInput.prevTxVout}`;
|
|
1814
|
+
|
|
1815
|
+
const witnessElements = witnessMap.get(inputKey);
|
|
1816
|
+
if (witnessElements && witnessElements.length === 2) {
|
|
1817
|
+
// Skip DLC inputs for now as requested
|
|
1818
|
+
if (fundingInput.dlcInput) {
|
|
1819
|
+
continue;
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
// For P2WPKH inputs, finalize the input with witness data
|
|
1823
|
+
const signature = witnessElements[0].witness;
|
|
1824
|
+
const publicKey = witnessElements[1].witness;
|
|
1825
|
+
|
|
1826
|
+
// Try a simpler approach - let bitcoinjs-lib handle witness construction
|
|
1827
|
+
psbt.finalizeInput(inputIndex, () => ({
|
|
1828
|
+
finalScriptSig: Buffer.alloc(0),
|
|
1829
|
+
finalScriptWitness: Buffer.concat([
|
|
1830
|
+
Buffer.from([0x02]), // witness stack count
|
|
1831
|
+
Buffer.from([signature.length]),
|
|
1832
|
+
signature,
|
|
1833
|
+
Buffer.from([publicKey.length]),
|
|
1834
|
+
publicKey,
|
|
1835
|
+
]),
|
|
1836
|
+
}));
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
// Extract the final transaction
|
|
1841
|
+
const finalTx = psbt.extractTransaction();
|
|
1842
|
+
|
|
1843
|
+
// Convert back to the expected Tx format
|
|
1844
|
+
return Tx.decode(StreamReader.fromBuffer(finalTx.toBuffer()));
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
async FindOutcomeIndexFromPolynomialPayoutCurvePiece(
|
|
1848
|
+
dlcOffer: DlcOffer,
|
|
1849
|
+
contractDescriptor: NumericalDescriptor,
|
|
1850
|
+
contractOraclePairIndex: number,
|
|
1851
|
+
polynomialPayoutCurvePiece: PolynomialPayoutCurvePiece,
|
|
1852
|
+
oracleAttestation: OracleAttestation,
|
|
1853
|
+
outcome: bigint,
|
|
1854
|
+
): Promise<FindOutcomeResponse> {
|
|
1855
|
+
const polynomialCurve = PolynomialPayoutCurve.fromPayoutCurvePiece(
|
|
1856
|
+
polynomialPayoutCurvePiece,
|
|
1857
|
+
);
|
|
1858
|
+
|
|
1859
|
+
const payouts = polynomialPayoutCurvePiece.points.map((point) =>
|
|
1860
|
+
Number(point.outcomePayout),
|
|
1861
|
+
);
|
|
1862
|
+
const minPayout = Math.min(...payouts);
|
|
1863
|
+
const maxPayout = Math.max(...payouts);
|
|
1864
|
+
|
|
1865
|
+
const clampBN = (val: BigNumber) =>
|
|
1866
|
+
BigNumber.max(minPayout, BigNumber.min(val, maxPayout));
|
|
1867
|
+
|
|
1868
|
+
const payout = clampBN(polynomialCurve.getPayout(outcome));
|
|
1869
|
+
|
|
1870
|
+
const payoutResponses = this.GetPayouts(dlcOffer);
|
|
1871
|
+
const payoutIndexOffset =
|
|
1872
|
+
this.GetIndicesFromPayouts(payoutResponses)[contractOraclePairIndex]
|
|
1873
|
+
.startingMessagesIndex;
|
|
1874
|
+
|
|
1875
|
+
const { payoutGroups } = payoutResponses[contractOraclePairIndex];
|
|
1876
|
+
|
|
1877
|
+
const intervalsSorted = [
|
|
1878
|
+
...contractDescriptor.roundingIntervals.intervals,
|
|
1879
|
+
].sort((a, b) => Number(b.beginInterval) - Number(a.beginInterval));
|
|
1880
|
+
|
|
1881
|
+
const interval = intervalsSorted.find(
|
|
1882
|
+
(interval) => Number(outcome) >= Number(interval.beginInterval),
|
|
1883
|
+
);
|
|
1884
|
+
|
|
1885
|
+
const roundedPayout = BigInt(
|
|
1886
|
+
clampBN(
|
|
1887
|
+
new BigNumber(roundPayout(payout, interval.roundingMod).toString()),
|
|
1888
|
+
).toString(),
|
|
1889
|
+
);
|
|
1890
|
+
|
|
1891
|
+
const outcomesFormatted = oracleAttestation.outcomes.map((outcome) =>
|
|
1892
|
+
parseInt(outcome),
|
|
1893
|
+
);
|
|
1894
|
+
|
|
1895
|
+
let index = 0;
|
|
1896
|
+
let groupIndex = -1;
|
|
1897
|
+
let groupLength = 0;
|
|
1898
|
+
|
|
1899
|
+
for (const payoutGroup of payoutGroups) {
|
|
1900
|
+
if (payoutGroup.payout === roundedPayout) {
|
|
1901
|
+
groupIndex = payoutGroup.groups.findIndex((group) => {
|
|
1902
|
+
return group.every((msg, i) => msg === outcomesFormatted[i]);
|
|
1903
|
+
});
|
|
1904
|
+
if (groupIndex === -1)
|
|
1905
|
+
throw Error(
|
|
1906
|
+
'Failed to Find OutcomeIndex From PolynomialPayoutCurvePiece. \
|
|
1907
|
+
Payout Group found but incorrect group index',
|
|
1908
|
+
);
|
|
1909
|
+
index += groupIndex;
|
|
1910
|
+
groupLength = payoutGroup.groups[groupIndex].length;
|
|
1911
|
+
break;
|
|
1912
|
+
} else {
|
|
1913
|
+
index += payoutGroup.groups.length;
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
if (groupIndex === -1)
|
|
1918
|
+
throw Error(
|
|
1919
|
+
'Failed to Find OutcomeIndex From PolynomialPayoutCurvePiece. \
|
|
1920
|
+
Payout Group not found',
|
|
1921
|
+
);
|
|
1922
|
+
|
|
1923
|
+
return { index: payoutIndexOffset + index, groupLength };
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
async FindOutcomeIndexFromHyperbolaPayoutCurvePiece(
|
|
1927
|
+
_dlcOffer: DlcOffer,
|
|
1928
|
+
contractDescriptor: NumericalDescriptor,
|
|
1929
|
+
contractOraclePairIndex: number,
|
|
1930
|
+
hyperbolaPayoutCurvePiece: HyperbolaPayoutCurvePiece,
|
|
1931
|
+
oracleAttestation: OracleAttestation,
|
|
1932
|
+
outcome: bigint,
|
|
1933
|
+
): Promise<FindOutcomeResponse> {
|
|
1934
|
+
const { dlcOffer } = checkTypes({ _dlcOffer });
|
|
1935
|
+
|
|
1936
|
+
const hyperbolaCurve = HyperbolaPayoutCurve.fromPayoutCurvePiece(
|
|
1937
|
+
hyperbolaPayoutCurvePiece,
|
|
1938
|
+
);
|
|
1939
|
+
|
|
1940
|
+
const clampBN = (val: BigNumber) =>
|
|
1941
|
+
BigNumber.max(
|
|
1942
|
+
0,
|
|
1943
|
+
BigNumber.min(val, dlcOffer.contractInfo.totalCollateral.toString()),
|
|
1944
|
+
);
|
|
1945
|
+
|
|
1946
|
+
const payout = clampBN(hyperbolaCurve.getPayout(outcome));
|
|
1947
|
+
|
|
1948
|
+
const payoutResponses = this.GetPayouts(dlcOffer);
|
|
1949
|
+
const payoutIndexOffset =
|
|
1950
|
+
this.GetIndicesFromPayouts(payoutResponses)[contractOraclePairIndex]
|
|
1951
|
+
.startingMessagesIndex;
|
|
1952
|
+
|
|
1953
|
+
const { payoutGroups } = payoutResponses[contractOraclePairIndex];
|
|
1954
|
+
|
|
1955
|
+
const intervalsSorted = [
|
|
1956
|
+
...contractDescriptor.roundingIntervals.intervals,
|
|
1957
|
+
].sort((a, b) => Number(b.beginInterval) - Number(a.beginInterval));
|
|
1958
|
+
|
|
1959
|
+
const interval = intervalsSorted.find(
|
|
1960
|
+
(interval) => Number(outcome) >= Number(interval.beginInterval),
|
|
1961
|
+
);
|
|
1962
|
+
|
|
1963
|
+
const roundedPayout = BigInt(
|
|
1964
|
+
clampBN(
|
|
1965
|
+
new BigNumber(roundPayout(payout, interval.roundingMod).toString()),
|
|
1966
|
+
).toString(),
|
|
1967
|
+
);
|
|
1968
|
+
|
|
1969
|
+
const outcomesFormatted = oracleAttestation.outcomes.map((outcome) =>
|
|
1970
|
+
parseInt(outcome),
|
|
1971
|
+
);
|
|
1972
|
+
|
|
1973
|
+
let index = 0;
|
|
1974
|
+
let groupIndex = -1;
|
|
1975
|
+
let groupLength = 0;
|
|
1976
|
+
|
|
1977
|
+
for (const [i, payoutGroup] of payoutGroups.entries()) {
|
|
1978
|
+
if (payoutGroup.payout === roundedPayout) {
|
|
1979
|
+
groupIndex = payoutGroup.groups.findIndex((group) => {
|
|
1980
|
+
return group.every((msg, i) => msg === outcomesFormatted[i]);
|
|
1981
|
+
});
|
|
1982
|
+
if (groupIndex !== -1) {
|
|
1983
|
+
index += groupIndex;
|
|
1984
|
+
groupLength = payoutGroup.groups[groupIndex].length;
|
|
1985
|
+
break;
|
|
1986
|
+
}
|
|
1987
|
+
} else if (
|
|
1988
|
+
payoutGroup.payout === BigInt(Math.round(Number(payout.toString()))) &&
|
|
1989
|
+
i !== 0
|
|
1990
|
+
) {
|
|
1991
|
+
// Edge case to account for case where payout is maximum payout for DLC
|
|
1992
|
+
// But rounded payout does not round down
|
|
1993
|
+
if (payoutGroups[i - 1].payout === roundedPayout) {
|
|
1994
|
+
// Ensure that the previous payout group causes index to be incremented
|
|
1995
|
+
index += payoutGroups[i - 1].groups.length;
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
groupIndex = payoutGroup.groups.findIndex((group) => {
|
|
1999
|
+
return group.every((msg, i) => msg === outcomesFormatted[i]);
|
|
2000
|
+
});
|
|
2001
|
+
if (groupIndex !== -1) {
|
|
2002
|
+
index += groupIndex;
|
|
2003
|
+
groupLength = payoutGroup.groups[groupIndex].length;
|
|
2004
|
+
break;
|
|
2005
|
+
}
|
|
2006
|
+
} else {
|
|
2007
|
+
index += payoutGroup.groups.length;
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
if (groupIndex === -1) {
|
|
2012
|
+
// Fallback to brute force search if payout-based search fails
|
|
2013
|
+
index = 0;
|
|
2014
|
+
groupLength = 0;
|
|
2015
|
+
|
|
2016
|
+
for (const [, payoutGroup] of payoutGroups.entries()) {
|
|
2017
|
+
groupIndex = payoutGroup.groups.findIndex((group) => {
|
|
2018
|
+
return group.every((msg, j) => msg === outcomesFormatted[j]);
|
|
2019
|
+
});
|
|
2020
|
+
|
|
2021
|
+
if (groupIndex !== -1) {
|
|
2022
|
+
index += groupIndex;
|
|
2023
|
+
groupLength = payoutGroup.groups[groupIndex].length;
|
|
2024
|
+
break;
|
|
2025
|
+
} else {
|
|
2026
|
+
index += payoutGroup.groups.length;
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
if (groupIndex === -1) {
|
|
2031
|
+
throw Error(
|
|
2032
|
+
'Failed to Find OutcomeIndex From HyperbolaPayoutCurvePiece. \
|
|
2033
|
+
Payout Group not found even with brute force search',
|
|
2034
|
+
);
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
return { index: payoutIndexOffset + index, groupLength };
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
async FindOutcomeIndex(
|
|
2042
|
+
dlcOffer: DlcOffer,
|
|
2043
|
+
oracleAttestation: OracleAttestation,
|
|
2044
|
+
): Promise<FindOutcomeResponse> {
|
|
2045
|
+
const contractOraclePairs = this.GetContractOraclePairs(
|
|
2046
|
+
dlcOffer.contractInfo,
|
|
2047
|
+
);
|
|
2048
|
+
const contractOraclePairIndex = contractOraclePairs.findIndex(
|
|
2049
|
+
({ oracleInfo }) => {
|
|
2050
|
+
if (oracleInfo.type !== MessageType.SingleOracleInfo) return false;
|
|
2051
|
+
const singleOracleInfo = oracleInfo as SingleOracleInfo;
|
|
2052
|
+
return (
|
|
2053
|
+
singleOracleInfo.announcement.oracleEvent.eventId ===
|
|
2054
|
+
oracleAttestation.eventId
|
|
2055
|
+
);
|
|
2056
|
+
},
|
|
2057
|
+
);
|
|
2058
|
+
assert(
|
|
2059
|
+
contractOraclePairIndex !== -1,
|
|
2060
|
+
'OracleAttestation must be for an existing OracleEvent',
|
|
2061
|
+
);
|
|
2062
|
+
|
|
2063
|
+
const contractOraclePair = contractOraclePairs[contractOraclePairIndex];
|
|
2064
|
+
|
|
2065
|
+
const { contractDescriptor: _contractDescriptor, oracleInfo } =
|
|
2066
|
+
contractOraclePair;
|
|
2067
|
+
assert(
|
|
2068
|
+
_contractDescriptor.contractDescriptorType ===
|
|
2069
|
+
ContractDescriptorType.NumericOutcome,
|
|
2070
|
+
'ContractDescriptor must be NumericOutcome',
|
|
2071
|
+
);
|
|
2072
|
+
const contractDescriptor = _contractDescriptor as NumericalDescriptor;
|
|
2073
|
+
const _payoutFunction = contractDescriptor.payoutFunction;
|
|
2074
|
+
assert(
|
|
2075
|
+
_payoutFunction.type === MessageType.PayoutFunction,
|
|
2076
|
+
'PayoutFunction must be V0',
|
|
2077
|
+
);
|
|
2078
|
+
|
|
2079
|
+
if (oracleInfo.type !== MessageType.SingleOracleInfo) {
|
|
2080
|
+
throw new Error('Only SingleOracleInfo supported in this context');
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
const singleOracleInfo = oracleInfo as SingleOracleInfo;
|
|
2084
|
+
const eventDescriptor = singleOracleInfo.announcement.oracleEvent
|
|
2085
|
+
.eventDescriptor as DigitDecompositionEventDescriptor;
|
|
2086
|
+
const payoutFunction = _payoutFunction as PayoutFunctionV0;
|
|
2087
|
+
|
|
2088
|
+
const base = eventDescriptor.base;
|
|
2089
|
+
const outcome: number = [...oracleAttestation.outcomes]
|
|
2090
|
+
.reverse()
|
|
2091
|
+
.reduce((acc, val, i) => acc + Number(val) * base ** i, 0);
|
|
2092
|
+
|
|
2093
|
+
const piecesSorted = payoutFunction.payoutFunctionPieces.sort(
|
|
2094
|
+
(a, b) =>
|
|
2095
|
+
Number(a.endPoint.eventOutcome) - Number(b.endPoint.eventOutcome),
|
|
2096
|
+
);
|
|
2097
|
+
|
|
2098
|
+
const piece = piecesSorted.find(
|
|
2099
|
+
(piece) => outcome < piece.endPoint.eventOutcome,
|
|
2100
|
+
);
|
|
2101
|
+
|
|
2102
|
+
switch (piece.payoutCurvePiece.type) {
|
|
2103
|
+
case MessageType.PolynomialPayoutCurvePiece:
|
|
2104
|
+
return this.FindOutcomeIndexFromPolynomialPayoutCurvePiece(
|
|
2105
|
+
dlcOffer,
|
|
2106
|
+
contractDescriptor,
|
|
2107
|
+
contractOraclePairIndex,
|
|
2108
|
+
piece.payoutCurvePiece as PolynomialPayoutCurvePiece,
|
|
2109
|
+
oracleAttestation,
|
|
2110
|
+
BigInt(outcome),
|
|
2111
|
+
);
|
|
2112
|
+
case MessageType.HyperbolaPayoutCurvePiece:
|
|
2113
|
+
return this.FindOutcomeIndexFromHyperbolaPayoutCurvePiece(
|
|
2114
|
+
dlcOffer,
|
|
2115
|
+
contractDescriptor,
|
|
2116
|
+
contractOraclePairIndex,
|
|
2117
|
+
piece.payoutCurvePiece as HyperbolaPayoutCurvePiece,
|
|
2118
|
+
oracleAttestation,
|
|
2119
|
+
BigInt(outcome),
|
|
2120
|
+
);
|
|
2121
|
+
case MessageType.OldHyperbolaPayoutCurvePiece:
|
|
2122
|
+
return this.FindOutcomeIndexFromHyperbolaPayoutCurvePiece(
|
|
2123
|
+
dlcOffer,
|
|
2124
|
+
contractDescriptor,
|
|
2125
|
+
contractOraclePairIndex,
|
|
2126
|
+
piece.payoutCurvePiece as HyperbolaPayoutCurvePiece,
|
|
2127
|
+
oracleAttestation,
|
|
2128
|
+
BigInt(outcome),
|
|
2129
|
+
);
|
|
2130
|
+
default:
|
|
2131
|
+
throw Error('Must be Hyperbola or Polynomial curve piece');
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
ValidateEvent(
|
|
2136
|
+
dlcOffer: DlcOffer,
|
|
2137
|
+
oracleAttestation: OracleAttestation,
|
|
2138
|
+
): void {
|
|
2139
|
+
switch (dlcOffer.contractInfo.contractInfoType) {
|
|
2140
|
+
case ContractInfoType.Single: {
|
|
2141
|
+
const contractInfo = dlcOffer.contractInfo as SingleContractInfo;
|
|
2142
|
+
switch (contractInfo.contractDescriptor.contractDescriptorType) {
|
|
2143
|
+
case ContractDescriptorType.Enumerated: {
|
|
2144
|
+
const oracleInfo = contractInfo.oracleInfo;
|
|
2145
|
+
if (oracleInfo.type !== MessageType.SingleOracleInfo) {
|
|
2146
|
+
throw Error('Only SingleOracleInfo supported in this context');
|
|
2147
|
+
}
|
|
2148
|
+
const singleOracleInfo = oracleInfo as SingleOracleInfo;
|
|
2149
|
+
if (
|
|
2150
|
+
singleOracleInfo.announcement.oracleEvent.eventId !==
|
|
2151
|
+
oracleAttestation.eventId
|
|
2152
|
+
)
|
|
2153
|
+
throw Error('Incorrect Oracle Attestation. Event Id must match.');
|
|
2154
|
+
break;
|
|
2155
|
+
}
|
|
2156
|
+
case ContractDescriptorType.NumericOutcome: {
|
|
2157
|
+
const oracleInfo = contractInfo.oracleInfo;
|
|
2158
|
+
if (oracleInfo.type !== MessageType.SingleOracleInfo) {
|
|
2159
|
+
throw Error('Only SingleOracleInfo supported in this context');
|
|
2160
|
+
}
|
|
2161
|
+
const singleOracleInfo = oracleInfo as SingleOracleInfo;
|
|
2162
|
+
if (
|
|
2163
|
+
singleOracleInfo.announcement.oracleEvent.eventId !==
|
|
2164
|
+
oracleAttestation.eventId
|
|
2165
|
+
)
|
|
2166
|
+
throw Error('Incorrect Oracle Attestation. Event Id must match.');
|
|
2167
|
+
break;
|
|
2168
|
+
}
|
|
2169
|
+
default:
|
|
2170
|
+
throw Error('ConractDescriptor must be V0 or V1');
|
|
2171
|
+
}
|
|
2172
|
+
break;
|
|
2173
|
+
}
|
|
2174
|
+
case ContractInfoType.Disjoint: {
|
|
2175
|
+
const contractInfo = dlcOffer.contractInfo as DisjointContractInfo;
|
|
2176
|
+
const attestedOracleEvent = contractInfo.contractOraclePairs.find(
|
|
2177
|
+
({ oracleInfo }) => {
|
|
2178
|
+
if (oracleInfo.type !== MessageType.SingleOracleInfo) return false;
|
|
2179
|
+
const singleOracleInfo = oracleInfo as SingleOracleInfo;
|
|
2180
|
+
return (
|
|
2181
|
+
singleOracleInfo.announcement.oracleEvent.eventId ===
|
|
2182
|
+
oracleAttestation.eventId
|
|
2183
|
+
);
|
|
2184
|
+
},
|
|
2185
|
+
);
|
|
2186
|
+
|
|
2187
|
+
if (!attestedOracleEvent)
|
|
2188
|
+
throw Error('Oracle event of attestation not found.');
|
|
2189
|
+
|
|
2190
|
+
break;
|
|
2191
|
+
}
|
|
2192
|
+
default:
|
|
2193
|
+
throw Error('ContractInfo must be V0 or V1');
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
async FindAndSignCet(
|
|
2198
|
+
dlcOffer: DlcOffer,
|
|
2199
|
+
dlcAccept: DlcAccept,
|
|
2200
|
+
dlcSign: DlcSign,
|
|
2201
|
+
dlcTxs: DlcTransactions,
|
|
2202
|
+
oracleAttestation: OracleAttestation,
|
|
2203
|
+
isOfferer?: boolean,
|
|
2204
|
+
): Promise<Tx> {
|
|
2205
|
+
if (isOfferer === undefined)
|
|
2206
|
+
isOfferer = await this.isOfferer(dlcOffer, dlcAccept);
|
|
2207
|
+
|
|
2208
|
+
const fundPrivateKey = await this.GetFundPrivateKey(
|
|
2209
|
+
dlcOffer,
|
|
2210
|
+
dlcAccept,
|
|
2211
|
+
isOfferer,
|
|
2212
|
+
);
|
|
2213
|
+
|
|
2214
|
+
let finalCet: string;
|
|
2215
|
+
|
|
2216
|
+
if (
|
|
2217
|
+
dlcOffer.contractInfo.contractInfoType === ContractInfoType.Single &&
|
|
2218
|
+
(dlcOffer.contractInfo as SingleContractInfo).contractDescriptor
|
|
2219
|
+
.contractDescriptorType === ContractDescriptorType.Enumerated
|
|
2220
|
+
) {
|
|
2221
|
+
const contractDescriptor = (dlcOffer.contractInfo as SingleContractInfo)
|
|
2222
|
+
.contractDescriptor as EnumeratedDescriptor;
|
|
2223
|
+
|
|
2224
|
+
// Handle different contract descriptor outcome formats
|
|
2225
|
+
const attestedOutcome = oracleAttestation.outcomes[0];
|
|
2226
|
+
|
|
2227
|
+
const outcomeIndex = contractDescriptor.outcomes.findIndex((outcome) => {
|
|
2228
|
+
// Try direct string match first (for DDK2 test: '1', '2', '3')
|
|
2229
|
+
if (outcome.outcome === attestedOutcome) {
|
|
2230
|
+
return true;
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
// Try sha256 hash match (for other tests with hashed outcomes)
|
|
2234
|
+
const attestedOutcomeHash = sha256(
|
|
2235
|
+
Buffer.from(attestedOutcome, 'utf8'),
|
|
2236
|
+
).toString('hex');
|
|
2237
|
+
return outcome.outcome === attestedOutcomeHash;
|
|
2238
|
+
});
|
|
2239
|
+
|
|
2240
|
+
finalCet = this._ddk
|
|
2241
|
+
.signCet(
|
|
2242
|
+
this.convertTxToDdkTransaction(dlcTxs.cets[outcomeIndex]),
|
|
2243
|
+
isOfferer
|
|
2244
|
+
? dlcAccept.cetAdaptorSignatures.sigs[outcomeIndex].encryptedSig
|
|
2245
|
+
: dlcSign.cetAdaptorSignatures.sigs[outcomeIndex].encryptedSig,
|
|
2246
|
+
oracleAttestation.signatures,
|
|
2247
|
+
Buffer.from(fundPrivateKey, 'hex'),
|
|
2248
|
+
isOfferer ? dlcAccept.fundingPubkey : dlcOffer.fundingPubkey,
|
|
2249
|
+
isOfferer ? dlcOffer.fundingPubkey : dlcAccept.fundingPubkey,
|
|
2250
|
+
this.getFundOutputValueSats(dlcTxs),
|
|
2251
|
+
)
|
|
2252
|
+
.rawBytes.toString('hex');
|
|
2253
|
+
} else {
|
|
2254
|
+
const { index: outcomeIndex, groupLength } = await this.FindOutcomeIndex(
|
|
2255
|
+
dlcOffer,
|
|
2256
|
+
oracleAttestation,
|
|
2257
|
+
);
|
|
2258
|
+
|
|
2259
|
+
const sliceIndex = -(oracleAttestation.signatures.length - groupLength);
|
|
2260
|
+
|
|
2261
|
+
const oracleSignatures =
|
|
2262
|
+
sliceIndex === 0
|
|
2263
|
+
? oracleAttestation.signatures
|
|
2264
|
+
: oracleAttestation.signatures.slice(0, sliceIndex);
|
|
2265
|
+
|
|
2266
|
+
finalCet = this._ddk
|
|
2267
|
+
.signCet(
|
|
2268
|
+
this.convertTxToDdkTransaction(dlcTxs.cets[outcomeIndex]),
|
|
2269
|
+
isOfferer
|
|
2270
|
+
? dlcAccept.cetAdaptorSignatures.sigs[outcomeIndex].encryptedSig
|
|
2271
|
+
: dlcSign.cetAdaptorSignatures.sigs[outcomeIndex].encryptedSig,
|
|
2272
|
+
oracleSignatures,
|
|
2273
|
+
Buffer.from(fundPrivateKey, 'hex'),
|
|
2274
|
+
isOfferer ? dlcAccept.fundingPubkey : dlcOffer.fundingPubkey,
|
|
2275
|
+
isOfferer ? dlcOffer.fundingPubkey : dlcAccept.fundingPubkey,
|
|
2276
|
+
this.getFundOutputValueSats(dlcTxs),
|
|
2277
|
+
)
|
|
2278
|
+
.rawBytes.toString('hex');
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
// const finalCet = (await this.SignCet(signCetRequest)).hex;
|
|
2282
|
+
|
|
2283
|
+
return Tx.decode(StreamReader.fromHex(finalCet));
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
private async GetFundAddress(
|
|
2287
|
+
dlcOffer: DlcOffer,
|
|
2288
|
+
dlcAccept: DlcAccept,
|
|
2289
|
+
isOfferer: boolean,
|
|
2290
|
+
): Promise<string> {
|
|
2291
|
+
const network = await this.getConnectedNetwork();
|
|
2292
|
+
|
|
2293
|
+
const fundingSPK = Script.p2wpkhLock(
|
|
2294
|
+
hash160(isOfferer ? dlcOffer.fundingPubkey : dlcAccept.fundingPubkey),
|
|
2295
|
+
)
|
|
2296
|
+
.serialize()
|
|
2297
|
+
.slice(1);
|
|
2298
|
+
|
|
2299
|
+
const fundingAddress: string = address.fromOutputScript(
|
|
2300
|
+
fundingSPK,
|
|
2301
|
+
network,
|
|
2302
|
+
);
|
|
2303
|
+
|
|
2304
|
+
return fundingAddress;
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
private async GetFundKeyPair(
|
|
2308
|
+
dlcOffer: DlcOffer,
|
|
2309
|
+
dlcAccept: DlcAccept,
|
|
2310
|
+
isOfferer: boolean,
|
|
2311
|
+
): Promise<ECPairInterface> {
|
|
2312
|
+
const fundingAddress = await this.GetFundAddress(
|
|
2313
|
+
dlcOffer,
|
|
2314
|
+
dlcAccept,
|
|
2315
|
+
isOfferer,
|
|
2316
|
+
);
|
|
2317
|
+
|
|
2318
|
+
const { derivationPath } =
|
|
2319
|
+
await this.getMethod('getWalletAddress')(fundingAddress);
|
|
2320
|
+
const keyPair: ECPairInterface =
|
|
2321
|
+
await this.getMethod('keyPair')(derivationPath);
|
|
2322
|
+
|
|
2323
|
+
return keyPair;
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
private async GetFundPrivateKey(
|
|
2327
|
+
dlcOffer: DlcOffer,
|
|
2328
|
+
dlcAccept: DlcAccept,
|
|
2329
|
+
isOfferer: boolean,
|
|
2330
|
+
): Promise<string> {
|
|
2331
|
+
const fundPrivateKeyPair: ECPairInterface = await this.GetFundKeyPair(
|
|
2332
|
+
dlcOffer,
|
|
2333
|
+
dlcAccept,
|
|
2334
|
+
isOfferer,
|
|
2335
|
+
);
|
|
2336
|
+
|
|
2337
|
+
return Buffer.from(fundPrivateKeyPair.privateKey).toString('hex');
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
async CreateCloseRawTxs(
|
|
2341
|
+
dlcOffer: DlcOffer,
|
|
2342
|
+
dlcAccept: DlcAccept,
|
|
2343
|
+
dlcTxs: DlcTransactions,
|
|
2344
|
+
closeInputAmount: bigint,
|
|
2345
|
+
isOfferer: boolean,
|
|
2346
|
+
_dlcCloses: DlcClose[] = [],
|
|
2347
|
+
fundingInputs?: FundingInput[],
|
|
2348
|
+
initiatorPayouts?: bigint[],
|
|
2349
|
+
): Promise<string[]> {
|
|
2350
|
+
const network = await this.getConnectedNetwork();
|
|
2351
|
+
|
|
2352
|
+
let finalizer: DualClosingTxFinalizer;
|
|
2353
|
+
if (_dlcCloses.length === 0) {
|
|
2354
|
+
finalizer = new DualClosingTxFinalizer(
|
|
2355
|
+
fundingInputs,
|
|
2356
|
+
dlcOffer.payoutSpk,
|
|
2357
|
+
dlcAccept.payoutSpk,
|
|
2358
|
+
dlcOffer.feeRatePerVb,
|
|
2359
|
+
);
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
const rawTransactionRequestPromises: Promise<string>[] = [];
|
|
2363
|
+
const rawCloseTxs = [];
|
|
2364
|
+
|
|
2365
|
+
const numPayouts =
|
|
2366
|
+
_dlcCloses.length === 0 ? initiatorPayouts.length : _dlcCloses.length;
|
|
2367
|
+
|
|
2368
|
+
for (let i = 0; i < numPayouts; i++) {
|
|
2369
|
+
let offerPayoutValue = BigInt(0);
|
|
2370
|
+
let acceptPayoutValue = BigInt(0);
|
|
2371
|
+
|
|
2372
|
+
if (_dlcCloses.length === 0) {
|
|
2373
|
+
const payout = initiatorPayouts[i];
|
|
2374
|
+
const payoutMinusOfferFees =
|
|
2375
|
+
finalizer.offerInitiatorFees > payout
|
|
2376
|
+
? BigInt(0)
|
|
2377
|
+
: payout - finalizer.offerInitiatorFees;
|
|
2378
|
+
const collateralMinusPayout =
|
|
2379
|
+
payout > dlcOffer.contractInfo.totalCollateral
|
|
2380
|
+
? BigInt(0)
|
|
2381
|
+
: dlcOffer.contractInfo.totalCollateral - payout;
|
|
2382
|
+
|
|
2383
|
+
offerPayoutValue = isOfferer
|
|
2384
|
+
? closeInputAmount + payoutMinusOfferFees
|
|
2385
|
+
: collateralMinusPayout;
|
|
2386
|
+
|
|
2387
|
+
acceptPayoutValue = isOfferer
|
|
2388
|
+
? collateralMinusPayout
|
|
2389
|
+
: closeInputAmount + payoutMinusOfferFees;
|
|
2390
|
+
} else {
|
|
2391
|
+
const dlcClose = checkTypes({ _dlcClose: _dlcCloses[i] }).dlcClose;
|
|
2392
|
+
|
|
2393
|
+
offerPayoutValue = dlcClose.offerPayoutSatoshis;
|
|
2394
|
+
acceptPayoutValue = dlcClose.acceptPayoutSatoshis;
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
const txOuts = [];
|
|
2398
|
+
|
|
2399
|
+
if (Number(offerPayoutValue) > 0) {
|
|
2400
|
+
txOuts.push({
|
|
2401
|
+
address: address.fromOutputScript(dlcOffer.payoutSpk, network),
|
|
2402
|
+
amount: Number(offerPayoutValue),
|
|
2403
|
+
});
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
if (Number(acceptPayoutValue) > 0) {
|
|
2407
|
+
txOuts.push({
|
|
2408
|
+
address: address.fromOutputScript(dlcAccept.payoutSpk, network),
|
|
2409
|
+
amount: Number(acceptPayoutValue),
|
|
2410
|
+
});
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
if (dlcOffer.payoutSerialId > dlcAccept.payoutSerialId) txOuts.reverse();
|
|
2414
|
+
|
|
2415
|
+
const rawTransactionRequest: CreateRawTransactionRequest = {
|
|
2416
|
+
version: 2,
|
|
2417
|
+
locktime: 0,
|
|
2418
|
+
txins: [
|
|
2419
|
+
{
|
|
2420
|
+
txid: dlcTxs.fundTx.txId.serialize().reverse().toString('hex'),
|
|
2421
|
+
vout: dlcTxs.fundTxVout,
|
|
2422
|
+
sequence: 0,
|
|
2423
|
+
},
|
|
2424
|
+
],
|
|
2425
|
+
txouts: txOuts,
|
|
2426
|
+
};
|
|
2427
|
+
|
|
2428
|
+
rawTransactionRequestPromises.push(
|
|
2429
|
+
(async () => {
|
|
2430
|
+
const response = await this.getMethod('CreateRawTransaction')(
|
|
2431
|
+
rawTransactionRequest,
|
|
2432
|
+
);
|
|
2433
|
+
return response.hex;
|
|
2434
|
+
})(),
|
|
2435
|
+
);
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
const hexs: string[] = await Promise.all(rawTransactionRequestPromises);
|
|
2439
|
+
|
|
2440
|
+
rawCloseTxs.push(hexs);
|
|
2441
|
+
|
|
2442
|
+
return rawCloseTxs.flat();
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
async CreateSignatureHashes(
|
|
2446
|
+
_dlcOffer: DlcOffer,
|
|
2447
|
+
_dlcAccept: DlcAccept,
|
|
2448
|
+
_dlcTxs: DlcTransactions,
|
|
2449
|
+
rawCloseTxs: string[],
|
|
2450
|
+
): Promise<string[]> {
|
|
2451
|
+
const { dlcOffer, dlcAccept, dlcTxs } = checkTypes({
|
|
2452
|
+
_dlcOffer,
|
|
2453
|
+
_dlcAccept,
|
|
2454
|
+
_dlcTxs,
|
|
2455
|
+
});
|
|
2456
|
+
|
|
2457
|
+
const network = await this.getConnectedNetwork();
|
|
2458
|
+
|
|
2459
|
+
const fundingPubKeys =
|
|
2460
|
+
Buffer.compare(dlcOffer.fundingPubkey, dlcAccept.fundingPubkey) === -1
|
|
2461
|
+
? [dlcOffer.fundingPubkey, dlcAccept.fundingPubkey]
|
|
2462
|
+
: [dlcAccept.fundingPubkey, dlcOffer.fundingPubkey];
|
|
2463
|
+
|
|
2464
|
+
const p2ms = payments.p2ms({
|
|
2465
|
+
m: 2,
|
|
2466
|
+
pubkeys: fundingPubKeys,
|
|
2467
|
+
network,
|
|
2468
|
+
});
|
|
2469
|
+
|
|
2470
|
+
const paymentVariant = payments.p2wsh({
|
|
2471
|
+
redeem: p2ms,
|
|
2472
|
+
network,
|
|
2473
|
+
});
|
|
2474
|
+
|
|
2475
|
+
const sigHashRequestPromises: Promise<string>[] = [];
|
|
2476
|
+
const sigHashes = [];
|
|
2477
|
+
|
|
2478
|
+
for (let i = 0; i < rawCloseTxs.length; i++) {
|
|
2479
|
+
const rawTx = rawCloseTxs[i];
|
|
2480
|
+
|
|
2481
|
+
const sigHashRequest: CreateSignatureHashRequest = {
|
|
2482
|
+
tx: rawTx,
|
|
2483
|
+
txin: {
|
|
2484
|
+
txid: dlcTxs.fundTx.txId.serialize().reverse().toString('hex'),
|
|
2485
|
+
vout: dlcTxs.fundTxVout,
|
|
2486
|
+
keyData: {
|
|
2487
|
+
hex: paymentVariant.redeem.output.toString('hex'),
|
|
2488
|
+
type: 'redeem_script',
|
|
2489
|
+
},
|
|
2490
|
+
amount: Number(this.getFundOutputValueSats(dlcTxs)),
|
|
2491
|
+
hashType: 'p2wsh',
|
|
2492
|
+
sighashType: 'all',
|
|
2493
|
+
sighashAnyoneCanPay: false,
|
|
2494
|
+
},
|
|
2495
|
+
};
|
|
2496
|
+
|
|
2497
|
+
sigHashRequestPromises.push(
|
|
2498
|
+
(async () => {
|
|
2499
|
+
const response = await this.getMethod('CreateSignatureHash')(
|
|
2500
|
+
sigHashRequest,
|
|
2501
|
+
);
|
|
2502
|
+
return response.sighash;
|
|
2503
|
+
})(),
|
|
2504
|
+
);
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
const sighashes: string[] = await Promise.all(sigHashRequestPromises);
|
|
2508
|
+
|
|
2509
|
+
sigHashes.push(sighashes);
|
|
2510
|
+
|
|
2511
|
+
return sigHashes.flat();
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
async CalculateEcSignatureHashes(
|
|
2515
|
+
sigHashes: string[],
|
|
2516
|
+
privKey: string,
|
|
2517
|
+
): Promise<string[]> {
|
|
2518
|
+
const cfdNetwork = await this.GetCfdNetwork();
|
|
2519
|
+
|
|
2520
|
+
const sigsRequestPromises: Promise<string>[] = [];
|
|
2521
|
+
|
|
2522
|
+
for (let i = 0; i < sigHashes.length; i++) {
|
|
2523
|
+
const sigHash = sigHashes[i];
|
|
2524
|
+
|
|
2525
|
+
const calculateEcSignatureRequest: CalculateEcSignatureRequest = {
|
|
2526
|
+
sighash: sigHash,
|
|
2527
|
+
privkeyData: {
|
|
2528
|
+
privkey: privKey,
|
|
2529
|
+
wif: false,
|
|
2530
|
+
network: cfdNetwork,
|
|
2531
|
+
},
|
|
2532
|
+
isGrindR: true,
|
|
2533
|
+
};
|
|
2534
|
+
|
|
2535
|
+
sigsRequestPromises.push(
|
|
2536
|
+
(async () => {
|
|
2537
|
+
const response = await this.getMethod('CalculateEcSignature')(
|
|
2538
|
+
calculateEcSignatureRequest,
|
|
2539
|
+
);
|
|
2540
|
+
return response.signature;
|
|
2541
|
+
})(),
|
|
2542
|
+
);
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
const sigs: string[] = await Promise.all(sigsRequestPromises);
|
|
2546
|
+
|
|
2547
|
+
return sigs.flat();
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
async VerifySignatures(
|
|
2551
|
+
_dlcOffer: DlcOffer,
|
|
2552
|
+
_dlcAccept: DlcAccept,
|
|
2553
|
+
_dlcTxs: DlcTransactions,
|
|
2554
|
+
_dlcCloses: DlcClose[],
|
|
2555
|
+
rawCloseTxs: string[],
|
|
2556
|
+
isOfferer: boolean,
|
|
2557
|
+
): Promise<boolean> {
|
|
2558
|
+
const { dlcOffer, dlcAccept, dlcTxs } = checkTypes({
|
|
2559
|
+
_dlcOffer,
|
|
2560
|
+
_dlcAccept,
|
|
2561
|
+
_dlcTxs,
|
|
2562
|
+
});
|
|
2563
|
+
|
|
2564
|
+
const dlcCloses = _dlcCloses.map(
|
|
2565
|
+
(_dlcClose) => checkTypes({ _dlcClose }).dlcClose,
|
|
2566
|
+
);
|
|
2567
|
+
|
|
2568
|
+
const network = await this.getConnectedNetwork();
|
|
2569
|
+
|
|
2570
|
+
const fundingPubKeys =
|
|
2571
|
+
Buffer.compare(dlcOffer.fundingPubkey, dlcAccept.fundingPubkey) === -1
|
|
2572
|
+
? [dlcOffer.fundingPubkey, dlcAccept.fundingPubkey]
|
|
2573
|
+
: [dlcAccept.fundingPubkey, dlcOffer.fundingPubkey];
|
|
2574
|
+
|
|
2575
|
+
const p2ms = payments.p2ms({
|
|
2576
|
+
m: 2,
|
|
2577
|
+
pubkeys: fundingPubKeys,
|
|
2578
|
+
network,
|
|
2579
|
+
});
|
|
2580
|
+
|
|
2581
|
+
const paymentVariant = payments.p2wsh({
|
|
2582
|
+
redeem: p2ms,
|
|
2583
|
+
network,
|
|
2584
|
+
});
|
|
2585
|
+
|
|
2586
|
+
const pubkey = isOfferer ? dlcAccept.fundingPubkey : dlcOffer.fundingPubkey;
|
|
2587
|
+
|
|
2588
|
+
const sigsValidity: Promise<boolean>[] = [];
|
|
2589
|
+
|
|
2590
|
+
for (let i = 0; i < rawCloseTxs.length; i++) {
|
|
2591
|
+
const rawTx = rawCloseTxs[i];
|
|
2592
|
+
const dlcClose = dlcCloses[i];
|
|
2593
|
+
|
|
2594
|
+
const verifySignatureRequest: VerifySignatureRequest = {
|
|
2595
|
+
tx: rawTx,
|
|
2596
|
+
txin: {
|
|
2597
|
+
txid: dlcTxs.fundTx.txId.serialize().reverse().toString('hex'),
|
|
2598
|
+
vout: dlcTxs.fundTxVout,
|
|
2599
|
+
signature: dlcClose.closeSignature.toString('hex'),
|
|
2600
|
+
pubkey: pubkey.toString('hex'),
|
|
2601
|
+
redeemScript: paymentVariant.redeem.output.toString('hex'),
|
|
2602
|
+
hashType: 'p2wsh',
|
|
2603
|
+
sighashType: 'all',
|
|
2604
|
+
sighashAnyoneCanPay: false,
|
|
2605
|
+
amount: Number(this.getFundOutputValueSats(dlcTxs)),
|
|
2606
|
+
},
|
|
2607
|
+
};
|
|
2608
|
+
|
|
2609
|
+
sigsValidity.push(
|
|
2610
|
+
(async () => {
|
|
2611
|
+
const response = await this.getMethod('VerifySignature')(
|
|
2612
|
+
verifySignatureRequest,
|
|
2613
|
+
);
|
|
2614
|
+
return response.success;
|
|
2615
|
+
})(),
|
|
2616
|
+
);
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
const areSigsValid = (await Promise.all(sigsValidity)).every((b) => b);
|
|
2620
|
+
return areSigsValid;
|
|
2621
|
+
}
|
|
2622
|
+
|
|
2623
|
+
/**
|
|
2624
|
+
* Check whether wallet is offerer of DlcOffer or DlcAccept
|
|
2625
|
+
* @param dlcOffer Dlc Offer Message
|
|
2626
|
+
* @param dlcAccept Dlc Accept Message
|
|
2627
|
+
* @returns {Promise<boolean>}
|
|
2628
|
+
*/
|
|
2629
|
+
async isOfferer(
|
|
2630
|
+
_dlcOffer: DlcOffer,
|
|
2631
|
+
_dlcAccept: DlcAccept,
|
|
2632
|
+
): Promise<boolean> {
|
|
2633
|
+
const { dlcOffer, dlcAccept } = checkTypes({
|
|
2634
|
+
_dlcOffer,
|
|
2635
|
+
_dlcAccept,
|
|
2636
|
+
});
|
|
2637
|
+
const network = await this.getConnectedNetwork();
|
|
2638
|
+
|
|
2639
|
+
const offerFundingSPK = Script.p2wpkhLock(hash160(dlcOffer.fundingPubkey))
|
|
2640
|
+
.serialize()
|
|
2641
|
+
.slice(1);
|
|
2642
|
+
const acceptFundingSPK = Script.p2wpkhLock(hash160(dlcAccept.fundingPubkey))
|
|
2643
|
+
.serialize()
|
|
2644
|
+
.slice(1);
|
|
2645
|
+
|
|
2646
|
+
const offerFundingAddress: string = address.fromOutputScript(
|
|
2647
|
+
offerFundingSPK,
|
|
2648
|
+
network,
|
|
2649
|
+
);
|
|
2650
|
+
|
|
2651
|
+
const acceptFundingAddress: string = address.fromOutputScript(
|
|
2652
|
+
acceptFundingSPK,
|
|
2653
|
+
network,
|
|
2654
|
+
);
|
|
2655
|
+
|
|
2656
|
+
let walletAddress: Address = await this.client.wallet.findAddress([
|
|
2657
|
+
offerFundingAddress,
|
|
2658
|
+
]);
|
|
2659
|
+
if (walletAddress) return true;
|
|
2660
|
+
walletAddress = await this.client.wallet.findAddress([
|
|
2661
|
+
acceptFundingAddress,
|
|
2662
|
+
]);
|
|
2663
|
+
if (walletAddress) return false;
|
|
2664
|
+
|
|
2665
|
+
throw Error('Wallet Address not found for DlcOffer or DlcAccept');
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
/**
|
|
2669
|
+
* Create DLC Offer Message
|
|
2670
|
+
* @param contractInfo ContractInfo TLV (V0 or V1)
|
|
2671
|
+
* @param offerCollateralSatoshis Amount DLC Initiator is putting into the contract
|
|
2672
|
+
* @param feeRatePerVb Fee rate in satoshi per virtual byte that both sides use to compute fees in funding tx
|
|
2673
|
+
* @param cetLocktime The nLockTime to be put on CETs
|
|
2674
|
+
* @param refundLocktime The nLockTime to be put on the refund transaction
|
|
2675
|
+
* @param fixedInputs Optional fixed inputs - can be Input[] for regular inputs or FundingInput[] for DLC inputs
|
|
2676
|
+
* @returns {Promise<DlcOffer>}
|
|
2677
|
+
*/
|
|
2678
|
+
async createDlcOffer(
|
|
2679
|
+
contractInfo: ContractInfo,
|
|
2680
|
+
offerCollateralSatoshis: bigint,
|
|
2681
|
+
feeRatePerVb: bigint,
|
|
2682
|
+
cetLocktime: number,
|
|
2683
|
+
refundLocktime: number,
|
|
2684
|
+
fixedInputs?: Input[] | FundingInput[],
|
|
2685
|
+
inputSupplementationMode?: InputSupplementationMode,
|
|
2686
|
+
): Promise<DlcOffer> {
|
|
2687
|
+
contractInfo.validate();
|
|
2688
|
+
const network = await this.getConnectedNetwork();
|
|
2689
|
+
|
|
2690
|
+
const dlcOffer = new DlcOffer();
|
|
2691
|
+
|
|
2692
|
+
// Generate a random 32-byte temporary contract ID
|
|
2693
|
+
dlcOffer.temporaryContractId = crypto.randomBytes(32);
|
|
2694
|
+
|
|
2695
|
+
// Check if we have FundingInput[] (DLC inputs) or Input[] (regular inputs)
|
|
2696
|
+
const hasFundingInputs =
|
|
2697
|
+
fixedInputs && fixedInputs.length > 0 && 'prevTx' in fixedInputs[0]; // FundingInput has prevTx, Input doesn't
|
|
2698
|
+
|
|
2699
|
+
let fundingPubKey: Buffer;
|
|
2700
|
+
let payoutSPK: Buffer;
|
|
2701
|
+
let payoutSerialId: bigint;
|
|
2702
|
+
let _fundingInputs: FundingInput[];
|
|
2703
|
+
let changeSPK: Buffer;
|
|
2704
|
+
let changeSerialId: bigint;
|
|
2705
|
+
|
|
2706
|
+
if (hasFundingInputs) {
|
|
2707
|
+
// Handle FundingInput[] directly (for DLC inputs)
|
|
2708
|
+
const fundingInputs = fixedInputs as FundingInput[];
|
|
2709
|
+
|
|
2710
|
+
// Generate addresses directly since we're bypassing Initialize()
|
|
2711
|
+
const payoutAddress: Address =
|
|
2712
|
+
await this.client.wallet.getUnusedAddress(false);
|
|
2713
|
+
payoutSPK = address.toOutputScript(payoutAddress.address, network);
|
|
2714
|
+
|
|
2715
|
+
const changeAddress: Address =
|
|
2716
|
+
await this.client.wallet.getUnusedAddress(true);
|
|
2717
|
+
changeSPK = address.toOutputScript(changeAddress.address, network);
|
|
2718
|
+
|
|
2719
|
+
const fundingAddress: Address =
|
|
2720
|
+
await this.client.wallet.getUnusedAddress(false);
|
|
2721
|
+
fundingPubKey = Buffer.from(fundingAddress.publicKey, 'hex');
|
|
2722
|
+
|
|
2723
|
+
if (fundingAddress.address === payoutAddress.address)
|
|
2724
|
+
throw Error('Address reuse');
|
|
2725
|
+
|
|
2726
|
+
payoutSerialId = generateSerialId();
|
|
2727
|
+
changeSerialId = generateSerialId();
|
|
2728
|
+
_fundingInputs = fundingInputs;
|
|
2729
|
+
} else {
|
|
2730
|
+
// Handle Input[] through existing Initialize() flow
|
|
2731
|
+
const initResult = await this.Initialize(
|
|
2732
|
+
offerCollateralSatoshis,
|
|
2733
|
+
feeRatePerVb,
|
|
2734
|
+
fixedInputs as Input[],
|
|
2735
|
+
inputSupplementationMode || InputSupplementationMode.Required,
|
|
2736
|
+
);
|
|
2737
|
+
|
|
2738
|
+
fundingPubKey = initResult.fundingPubKey;
|
|
2739
|
+
payoutSPK = initResult.payoutSPK;
|
|
2740
|
+
payoutSerialId = initResult.payoutSerialId;
|
|
2741
|
+
_fundingInputs = initResult.fundingInputs;
|
|
2742
|
+
changeSPK = initResult.changeSPK;
|
|
2743
|
+
changeSerialId = initResult.changeSerialId;
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2746
|
+
_fundingInputs.forEach((input) =>
|
|
2747
|
+
assert(
|
|
2748
|
+
input.type === MessageType.FundingInput,
|
|
2749
|
+
'FundingInput must be V0',
|
|
2750
|
+
),
|
|
2751
|
+
);
|
|
2752
|
+
|
|
2753
|
+
const fundingInputs: FundingInput[] = _fundingInputs.map(
|
|
2754
|
+
(input) => input as FundingInput,
|
|
2755
|
+
);
|
|
2756
|
+
|
|
2757
|
+
fundingInputs.sort(
|
|
2758
|
+
(a, b) => Number(a.inputSerialId) - Number(b.inputSerialId),
|
|
2759
|
+
);
|
|
2760
|
+
|
|
2761
|
+
const fundOutputSerialId = generateSerialId();
|
|
2762
|
+
|
|
2763
|
+
assert(
|
|
2764
|
+
changeSerialId !== fundOutputSerialId,
|
|
2765
|
+
'changeSerialId cannot equal the fundOutputSerialId',
|
|
2766
|
+
);
|
|
2767
|
+
|
|
2768
|
+
dlcOffer.contractFlags = Buffer.from('00', 'hex');
|
|
2769
|
+
dlcOffer.chainHash = chainHashFromNetwork(network);
|
|
2770
|
+
dlcOffer.contractInfo = contractInfo;
|
|
2771
|
+
dlcOffer.fundingPubkey = fundingPubKey;
|
|
2772
|
+
dlcOffer.payoutSpk = payoutSPK;
|
|
2773
|
+
dlcOffer.payoutSerialId = payoutSerialId;
|
|
2774
|
+
dlcOffer.offerCollateral = offerCollateralSatoshis;
|
|
2775
|
+
dlcOffer.fundingInputs = fundingInputs;
|
|
2776
|
+
dlcOffer.changeSpk = changeSPK;
|
|
2777
|
+
dlcOffer.changeSerialId = changeSerialId;
|
|
2778
|
+
dlcOffer.fundOutputSerialId = dlcOffer.fundOutputSerialId =
|
|
2779
|
+
fundOutputSerialId;
|
|
2780
|
+
dlcOffer.feeRatePerVb = feeRatePerVb;
|
|
2781
|
+
dlcOffer.cetLocktime = cetLocktime;
|
|
2782
|
+
dlcOffer.refundLocktime = refundLocktime;
|
|
2783
|
+
|
|
2784
|
+
if (offerCollateralSatoshis === dlcOffer.contractInfo.totalCollateral) {
|
|
2785
|
+
dlcOffer.markAsSingleFunded();
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2788
|
+
assert(
|
|
2789
|
+
(() => {
|
|
2790
|
+
const finalizer = new DualFundingTxFinalizer(
|
|
2791
|
+
dlcOffer.fundingInputs,
|
|
2792
|
+
dlcOffer.payoutSpk,
|
|
2793
|
+
dlcOffer.changeSpk,
|
|
2794
|
+
null,
|
|
2795
|
+
null,
|
|
2796
|
+
null,
|
|
2797
|
+
dlcOffer.feeRatePerVb,
|
|
2798
|
+
);
|
|
2799
|
+
const funding = fundingInputs.reduce((total, input) => {
|
|
2800
|
+
return total + input.prevTx.outputs[input.prevTxVout].value.sats;
|
|
2801
|
+
}, BigInt(0));
|
|
2802
|
+
|
|
2803
|
+
return funding >= offerCollateralSatoshis + finalizer.offerFees;
|
|
2804
|
+
})(),
|
|
2805
|
+
'fundingInputs for dlcOffer must be greater than offerCollateralSatoshis plus offerFees',
|
|
2806
|
+
);
|
|
2807
|
+
|
|
2808
|
+
dlcOffer.validate();
|
|
2809
|
+
|
|
2810
|
+
return dlcOffer;
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
/**
|
|
2814
|
+
* Accept DLC Offer (supports single-funded DLCs when accept collateral is 0)
|
|
2815
|
+
* @param _dlcOffer Dlc Offer Message
|
|
2816
|
+
* @param fixedInputs Optional inputs to use for Funding Inputs
|
|
2817
|
+
* @returns {Promise<AcceptDlcOfferResponse}
|
|
2818
|
+
*/
|
|
2819
|
+
async acceptDlcOffer(
|
|
2820
|
+
_dlcOffer: DlcOffer,
|
|
2821
|
+
fixedInputs?: Input[] | FundingInput[],
|
|
2822
|
+
): Promise<AcceptDlcOfferResponse> {
|
|
2823
|
+
const { dlcOffer } = checkTypes({ _dlcOffer });
|
|
2824
|
+
dlcOffer.validate();
|
|
2825
|
+
|
|
2826
|
+
const acceptCollateralSatoshis =
|
|
2827
|
+
dlcOffer.contractInfo.totalCollateral - dlcOffer.offerCollateral;
|
|
2828
|
+
|
|
2829
|
+
assert(
|
|
2830
|
+
acceptCollateralSatoshis ===
|
|
2831
|
+
dlcOffer.contractInfo.totalCollateral - dlcOffer.offerCollateral,
|
|
2832
|
+
'acceptCollaterialSatoshis should equal totalCollateral - offerCollateralSatoshis',
|
|
2833
|
+
);
|
|
2834
|
+
|
|
2835
|
+
let fundingPubKey: Buffer;
|
|
2836
|
+
let payoutSPK: Buffer;
|
|
2837
|
+
let payoutSerialId: bigint;
|
|
2838
|
+
let fundingInputs: FundingInput[];
|
|
2839
|
+
let changeSPK: Buffer;
|
|
2840
|
+
let changeSerialId: bigint;
|
|
2841
|
+
|
|
2842
|
+
if (acceptCollateralSatoshis === BigInt(0)) {
|
|
2843
|
+
// Single-funded DLC: accept side provides no funding
|
|
2844
|
+
const network = await this.getConnectedNetwork();
|
|
2845
|
+
|
|
2846
|
+
// Still need payout address for receiving DLC outcomes
|
|
2847
|
+
const payoutAddress: Address =
|
|
2848
|
+
await this.client.wallet.getUnusedAddress(false);
|
|
2849
|
+
payoutSPK = address.toOutputScript(payoutAddress.address, network);
|
|
2850
|
+
|
|
2851
|
+
// Generate funding pubkey for DLC contract construction
|
|
2852
|
+
const fundingAddress: Address =
|
|
2853
|
+
await this.client.wallet.getUnusedAddress(false);
|
|
2854
|
+
fundingPubKey = Buffer.from(fundingAddress.publicKey, 'hex');
|
|
2855
|
+
|
|
2856
|
+
// Generate change address (even though not used)
|
|
2857
|
+
const changeAddress: Address =
|
|
2858
|
+
await this.client.wallet.getUnusedAddress(true);
|
|
2859
|
+
changeSPK = address.toOutputScript(changeAddress.address, network);
|
|
2860
|
+
|
|
2861
|
+
if (fundingAddress.address === payoutAddress.address)
|
|
2862
|
+
throw Error('Address reuse');
|
|
2863
|
+
|
|
2864
|
+
// Generate serial IDs
|
|
2865
|
+
payoutSerialId = generateSerialId();
|
|
2866
|
+
changeSerialId = generateSerialId();
|
|
2867
|
+
|
|
2868
|
+
// No funding inputs for single-funded DLC
|
|
2869
|
+
fundingInputs = [];
|
|
2870
|
+
} else {
|
|
2871
|
+
// Standard DLC: accept side provides funding
|
|
2872
|
+
|
|
2873
|
+
// Check if we have FundingInput[] (DLC inputs) or Input[] (regular inputs)
|
|
2874
|
+
const hasFundingInputs =
|
|
2875
|
+
fixedInputs && fixedInputs.length > 0 && 'prevTx' in fixedInputs[0]; // FundingInput has prevTx, Input doesn't
|
|
2876
|
+
|
|
2877
|
+
let initResult: InitializeResponse;
|
|
2878
|
+
|
|
2879
|
+
if (hasFundingInputs) {
|
|
2880
|
+
// Handle FundingInput[] directly (for DLC inputs)
|
|
2881
|
+
const fundingInputs = fixedInputs as FundingInput[];
|
|
2882
|
+
const network = await this.getConnectedNetwork();
|
|
2883
|
+
|
|
2884
|
+
// Generate addresses directly since we're bypassing Initialize()
|
|
2885
|
+
const payoutAddress: Address =
|
|
2886
|
+
await this.client.wallet.getUnusedAddress(false);
|
|
2887
|
+
const payoutSPK = address.toOutputScript(
|
|
2888
|
+
payoutAddress.address,
|
|
2889
|
+
network,
|
|
2890
|
+
);
|
|
2891
|
+
|
|
2892
|
+
const changeAddress: Address =
|
|
2893
|
+
await this.client.wallet.getUnusedAddress(true);
|
|
2894
|
+
const changeSPK = address.toOutputScript(
|
|
2895
|
+
changeAddress.address,
|
|
2896
|
+
network,
|
|
2897
|
+
);
|
|
2898
|
+
|
|
2899
|
+
const fundingAddress: Address =
|
|
2900
|
+
await this.client.wallet.getUnusedAddress(false);
|
|
2901
|
+
const fundingPubKey = Buffer.from(fundingAddress.publicKey, 'hex');
|
|
2902
|
+
|
|
2903
|
+
if (fundingAddress.address === payoutAddress.address)
|
|
2904
|
+
throw Error('Address reuse');
|
|
2905
|
+
|
|
2906
|
+
const payoutSerialId = generateSerialId();
|
|
2907
|
+
const changeSerialId = generateSerialId();
|
|
2908
|
+
|
|
2909
|
+
initResult = {
|
|
2910
|
+
fundingPubKey,
|
|
2911
|
+
payoutSPK,
|
|
2912
|
+
payoutSerialId,
|
|
2913
|
+
fundingInputs,
|
|
2914
|
+
changeSPK,
|
|
2915
|
+
changeSerialId,
|
|
2916
|
+
};
|
|
2917
|
+
} else {
|
|
2918
|
+
// Handle Input[] through existing Initialize() flow
|
|
2919
|
+
// Use InputSupplementationMode.None when fixed inputs are provided
|
|
2920
|
+
// to avoid wallet lookup issues with unusual addresses
|
|
2921
|
+
const supplementationMode =
|
|
2922
|
+
fixedInputs && fixedInputs.length > 0
|
|
2923
|
+
? InputSupplementationMode.None
|
|
2924
|
+
: InputSupplementationMode.Required;
|
|
2925
|
+
|
|
2926
|
+
initResult = await this.Initialize(
|
|
2927
|
+
acceptCollateralSatoshis,
|
|
2928
|
+
dlcOffer.feeRatePerVb,
|
|
2929
|
+
fixedInputs as Input[],
|
|
2930
|
+
supplementationMode,
|
|
2931
|
+
);
|
|
2932
|
+
}
|
|
2933
|
+
|
|
2934
|
+
fundingPubKey = initResult.fundingPubKey;
|
|
2935
|
+
payoutSPK = initResult.payoutSPK;
|
|
2936
|
+
payoutSerialId = initResult.payoutSerialId;
|
|
2937
|
+
changeSPK = initResult.changeSPK;
|
|
2938
|
+
changeSerialId = initResult.changeSerialId;
|
|
2939
|
+
|
|
2940
|
+
const _fundingInputs = initResult.fundingInputs;
|
|
2941
|
+
|
|
2942
|
+
_fundingInputs.forEach((input) =>
|
|
2943
|
+
assert(
|
|
2944
|
+
input.type === MessageType.FundingInput,
|
|
2945
|
+
'FundingInput must be V0',
|
|
2946
|
+
),
|
|
2947
|
+
);
|
|
2948
|
+
|
|
2949
|
+
fundingInputs = _fundingInputs.map((input) => input as FundingInput);
|
|
2950
|
+
|
|
2951
|
+
fundingInputs.sort(
|
|
2952
|
+
(a, b) => Number(a.inputSerialId) - Number(b.inputSerialId),
|
|
2953
|
+
);
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2956
|
+
assert(
|
|
2957
|
+
Buffer.compare(dlcOffer.fundingPubkey, fundingPubKey) !== 0,
|
|
2958
|
+
'DlcOffer and DlcAccept FundingPubKey cannot be the same',
|
|
2959
|
+
);
|
|
2960
|
+
|
|
2961
|
+
const dlcAccept = new DlcAccept();
|
|
2962
|
+
|
|
2963
|
+
dlcAccept.temporaryContractId = sha256(dlcOffer.serialize());
|
|
2964
|
+
dlcAccept.acceptCollateral = acceptCollateralSatoshis;
|
|
2965
|
+
dlcAccept.fundingPubkey = fundingPubKey;
|
|
2966
|
+
dlcAccept.payoutSpk = payoutSPK;
|
|
2967
|
+
dlcAccept.payoutSerialId = payoutSerialId;
|
|
2968
|
+
dlcAccept.fundingInputs = fundingInputs;
|
|
2969
|
+
dlcAccept.changeSpk = changeSPK;
|
|
2970
|
+
dlcAccept.changeSerialId = changeSerialId;
|
|
2971
|
+
|
|
2972
|
+
assert(
|
|
2973
|
+
dlcAccept.changeSerialId !== dlcOffer.fundOutputSerialId,
|
|
2974
|
+
'changeSerialId cannot equal the fundOutputSerialId',
|
|
2975
|
+
);
|
|
2976
|
+
|
|
2977
|
+
assert(
|
|
2978
|
+
dlcOffer.payoutSerialId !== dlcAccept.payoutSerialId,
|
|
2979
|
+
'offer.payoutSerialId cannot equal accept.payoutSerialId',
|
|
2980
|
+
);
|
|
2981
|
+
|
|
2982
|
+
assert(
|
|
2983
|
+
(() => {
|
|
2984
|
+
const ids = [
|
|
2985
|
+
dlcOffer.changeSerialId,
|
|
2986
|
+
dlcAccept.changeSerialId,
|
|
2987
|
+
dlcOffer.fundOutputSerialId,
|
|
2988
|
+
];
|
|
2989
|
+
return new Set(ids).size === ids.length;
|
|
2990
|
+
})(),
|
|
2991
|
+
'offer.changeSerialID, accept.changeSerialId and fundOutputSerialId must be unique',
|
|
2992
|
+
);
|
|
2993
|
+
|
|
2994
|
+
if (dlcOffer.singleFunded) dlcAccept.markAsSingleFunded();
|
|
2995
|
+
|
|
2996
|
+
dlcAccept.validate();
|
|
2997
|
+
|
|
2998
|
+
// Only validate funding requirements if accept side is providing collateral
|
|
2999
|
+
if (acceptCollateralSatoshis > BigInt(0)) {
|
|
3000
|
+
assert(
|
|
3001
|
+
(() => {
|
|
3002
|
+
const finalizer = new DualFundingTxFinalizer(
|
|
3003
|
+
dlcOffer.fundingInputs,
|
|
3004
|
+
dlcOffer.payoutSpk,
|
|
3005
|
+
dlcOffer.changeSpk,
|
|
3006
|
+
dlcAccept.fundingInputs,
|
|
3007
|
+
dlcAccept.payoutSpk,
|
|
3008
|
+
dlcAccept.changeSpk,
|
|
3009
|
+
dlcOffer.feeRatePerVb,
|
|
3010
|
+
);
|
|
3011
|
+
const funding = fundingInputs.reduce((total, input) => {
|
|
3012
|
+
return total + input.prevTx.outputs[input.prevTxVout].value.sats;
|
|
3013
|
+
}, BigInt(0));
|
|
3014
|
+
|
|
3015
|
+
return funding >= acceptCollateralSatoshis + finalizer.acceptFees;
|
|
3016
|
+
})(),
|
|
3017
|
+
'fundingInputs for dlcAccept must be greater than acceptCollateralSatoshis plus acceptFees',
|
|
3018
|
+
);
|
|
3019
|
+
}
|
|
3020
|
+
|
|
3021
|
+
const { dlcTransactions, messagesList } = await this.createDlcTxs(
|
|
3022
|
+
dlcOffer,
|
|
3023
|
+
dlcAccept,
|
|
3024
|
+
);
|
|
3025
|
+
|
|
3026
|
+
const { cetSignatures, refundSignature } =
|
|
3027
|
+
await this.CreateCetAdaptorAndRefundSigs(
|
|
3028
|
+
dlcOffer,
|
|
3029
|
+
dlcAccept,
|
|
3030
|
+
dlcTransactions,
|
|
3031
|
+
messagesList,
|
|
3032
|
+
false,
|
|
3033
|
+
);
|
|
3034
|
+
|
|
3035
|
+
const _dlcTransactions = dlcTransactions;
|
|
3036
|
+
|
|
3037
|
+
const contractId = xor(
|
|
3038
|
+
_dlcTransactions.fundTx.txId.serialize(),
|
|
3039
|
+
dlcAccept.temporaryContractId,
|
|
3040
|
+
);
|
|
3041
|
+
_dlcTransactions.contractId = contractId;
|
|
3042
|
+
|
|
3043
|
+
dlcAccept.cetAdaptorSignatures = cetSignatures;
|
|
3044
|
+
dlcAccept.refundSignature = refundSignature;
|
|
3045
|
+
|
|
3046
|
+
return { dlcAccept, dlcTransactions: _dlcTransactions };
|
|
3047
|
+
}
|
|
3048
|
+
|
|
3049
|
+
/**
|
|
3050
|
+
* Sign Dlc Accept Message
|
|
3051
|
+
* @param _dlcOffer Dlc Offer Message
|
|
3052
|
+
* @param _dlcAccept Dlc Accept Message
|
|
3053
|
+
* @returns {Promise<SignDlcAcceptResponse}
|
|
3054
|
+
*/
|
|
3055
|
+
async signDlcAccept(
|
|
3056
|
+
dlcOffer: DlcOffer,
|
|
3057
|
+
dlcAccept: DlcAccept,
|
|
3058
|
+
): Promise<SignDlcAcceptResponse> {
|
|
3059
|
+
dlcOffer.validate();
|
|
3060
|
+
dlcAccept.validate();
|
|
3061
|
+
|
|
3062
|
+
assert(
|
|
3063
|
+
Buffer.compare(dlcOffer.fundingPubkey, dlcAccept.fundingPubkey) !== 0,
|
|
3064
|
+
'DlcOffer and DlcAccept FundingPubKey cannot be the same',
|
|
3065
|
+
);
|
|
3066
|
+
|
|
3067
|
+
const dlcSign = new DlcSign();
|
|
3068
|
+
|
|
3069
|
+
const { dlcTransactions, messagesList } = await this.createDlcTxs(
|
|
3070
|
+
dlcOffer,
|
|
3071
|
+
dlcAccept,
|
|
3072
|
+
);
|
|
3073
|
+
|
|
3074
|
+
await this.VerifyCetAdaptorAndRefundSigs(
|
|
3075
|
+
dlcOffer,
|
|
3076
|
+
dlcAccept,
|
|
3077
|
+
dlcSign,
|
|
3078
|
+
dlcTransactions,
|
|
3079
|
+
messagesList,
|
|
3080
|
+
true,
|
|
3081
|
+
);
|
|
3082
|
+
|
|
3083
|
+
const { cetSignatures, refundSignature } =
|
|
3084
|
+
await this.CreateCetAdaptorAndRefundSigs(
|
|
3085
|
+
dlcOffer,
|
|
3086
|
+
dlcAccept,
|
|
3087
|
+
dlcTransactions,
|
|
3088
|
+
messagesList,
|
|
3089
|
+
true,
|
|
3090
|
+
);
|
|
3091
|
+
|
|
3092
|
+
const fundingSignatures = await this.CreateFundingSigsAlt(
|
|
3093
|
+
dlcOffer,
|
|
3094
|
+
dlcAccept,
|
|
3095
|
+
dlcTransactions,
|
|
3096
|
+
true,
|
|
3097
|
+
);
|
|
3098
|
+
|
|
3099
|
+
const dlcTxs = dlcTransactions;
|
|
3100
|
+
|
|
3101
|
+
const contractId = xor(
|
|
3102
|
+
dlcTxs.fundTx.txId.serialize(),
|
|
3103
|
+
dlcAccept.temporaryContractId,
|
|
3104
|
+
);
|
|
3105
|
+
|
|
3106
|
+
assert(
|
|
3107
|
+
Buffer.compare(
|
|
3108
|
+
contractId,
|
|
3109
|
+
xor(dlcTxs.fundTx.txId.serialize(), dlcAccept.temporaryContractId),
|
|
3110
|
+
) === 0,
|
|
3111
|
+
'contractId must be the xor of funding txid, fundingOutputIndex and the tempContractId',
|
|
3112
|
+
);
|
|
3113
|
+
|
|
3114
|
+
dlcTxs.contractId = contractId;
|
|
3115
|
+
|
|
3116
|
+
dlcSign.contractId = contractId;
|
|
3117
|
+
dlcSign.cetAdaptorSignatures = cetSignatures;
|
|
3118
|
+
dlcSign.refundSignature = refundSignature;
|
|
3119
|
+
dlcSign.fundingSignatures = fundingSignatures;
|
|
3120
|
+
|
|
3121
|
+
return { dlcSign, dlcTransactions: dlcTxs };
|
|
3122
|
+
}
|
|
3123
|
+
|
|
3124
|
+
/**
|
|
3125
|
+
* Finalize Dlc Sign
|
|
3126
|
+
* @param dlcOffer Dlc Offer Message
|
|
3127
|
+
* @param dlcAccept Dlc Accept Message
|
|
3128
|
+
* @param dlcSign Dlc Sign Message
|
|
3129
|
+
* @param dlcTxs Dlc Transactions Message
|
|
3130
|
+
* @returns {Promise<Tx>}
|
|
3131
|
+
*/
|
|
3132
|
+
async finalizeDlcSign(
|
|
3133
|
+
dlcOffer: DlcOffer,
|
|
3134
|
+
dlcAccept: DlcAccept,
|
|
3135
|
+
dlcSign: DlcSign,
|
|
3136
|
+
dlcTxs: DlcTransactions,
|
|
3137
|
+
): Promise<Tx> {
|
|
3138
|
+
let messagesList: Messages[] = [];
|
|
3139
|
+
|
|
3140
|
+
if (
|
|
3141
|
+
dlcOffer.contractInfo.type === MessageType.SingleContractInfo &&
|
|
3142
|
+
(dlcOffer.contractInfo as SingleContractInfo).contractDescriptor.type ===
|
|
3143
|
+
MessageType.SingleContractInfo
|
|
3144
|
+
) {
|
|
3145
|
+
for (const outcome of (
|
|
3146
|
+
(dlcOffer.contractInfo as SingleContractInfo)
|
|
3147
|
+
.contractDescriptor as EnumeratedDescriptor
|
|
3148
|
+
).outcomes) {
|
|
3149
|
+
messagesList.push({ messages: [outcome.outcome] });
|
|
3150
|
+
}
|
|
3151
|
+
} else {
|
|
3152
|
+
const payoutResponses = this.GetPayouts(dlcOffer);
|
|
3153
|
+
const { messagesList: oracleEventMessagesList } =
|
|
3154
|
+
this.FlattenPayouts(payoutResponses);
|
|
3155
|
+
messagesList = oracleEventMessagesList;
|
|
3156
|
+
}
|
|
3157
|
+
|
|
3158
|
+
await this.VerifyCetAdaptorAndRefundSigs(
|
|
3159
|
+
dlcOffer,
|
|
3160
|
+
dlcAccept,
|
|
3161
|
+
dlcSign,
|
|
3162
|
+
dlcTxs,
|
|
3163
|
+
messagesList,
|
|
3164
|
+
false,
|
|
3165
|
+
);
|
|
3166
|
+
|
|
3167
|
+
await this.VerifyFundingSigsAlt(
|
|
3168
|
+
dlcOffer,
|
|
3169
|
+
dlcAccept,
|
|
3170
|
+
dlcSign,
|
|
3171
|
+
dlcTxs,
|
|
3172
|
+
false,
|
|
3173
|
+
);
|
|
3174
|
+
|
|
3175
|
+
const fundingSignatures = await this.CreateFundingSigsAlt(
|
|
3176
|
+
dlcOffer,
|
|
3177
|
+
dlcAccept,
|
|
3178
|
+
dlcTxs,
|
|
3179
|
+
false,
|
|
3180
|
+
);
|
|
3181
|
+
|
|
3182
|
+
const fundTx = await this.CreateFundingTx(
|
|
3183
|
+
dlcOffer,
|
|
3184
|
+
dlcAccept,
|
|
3185
|
+
dlcSign,
|
|
3186
|
+
dlcTxs,
|
|
3187
|
+
fundingSignatures,
|
|
3188
|
+
);
|
|
3189
|
+
|
|
3190
|
+
return fundTx;
|
|
3191
|
+
}
|
|
3192
|
+
|
|
3193
|
+
/**
|
|
3194
|
+
* Execute DLC
|
|
3195
|
+
* @param _dlcOffer Dlc Offer Message
|
|
3196
|
+
* @param _dlcAccept Dlc Accept Message
|
|
3197
|
+
* @param _dlcSign Dlc Sign Message
|
|
3198
|
+
* @param _dlcTxs Dlc Transactions Message
|
|
3199
|
+
* @param oracleAttestation Oracle Attestations TLV (V0)
|
|
3200
|
+
* @param isOfferer Whether party is Dlc Offerer
|
|
3201
|
+
* @returns {Promise<Tx>}
|
|
3202
|
+
*/
|
|
3203
|
+
async execute(
|
|
3204
|
+
dlcOffer: DlcOffer,
|
|
3205
|
+
dlcAccept: DlcAccept,
|
|
3206
|
+
dlcSign: DlcSign,
|
|
3207
|
+
dlcTxs: DlcTransactions,
|
|
3208
|
+
oracleAttestation: OracleAttestation,
|
|
3209
|
+
isOfferer?: boolean,
|
|
3210
|
+
): Promise<Tx> {
|
|
3211
|
+
if (isOfferer === undefined)
|
|
3212
|
+
isOfferer = await this.isOfferer(dlcOffer, dlcAccept);
|
|
3213
|
+
|
|
3214
|
+
this.ValidateEvent(dlcOffer, oracleAttestation);
|
|
3215
|
+
|
|
3216
|
+
return this.FindAndSignCet(
|
|
3217
|
+
dlcOffer,
|
|
3218
|
+
dlcAccept,
|
|
3219
|
+
dlcSign,
|
|
3220
|
+
dlcTxs,
|
|
3221
|
+
oracleAttestation,
|
|
3222
|
+
isOfferer,
|
|
3223
|
+
);
|
|
3224
|
+
}
|
|
3225
|
+
|
|
3226
|
+
/**
|
|
3227
|
+
* Refund DLC
|
|
3228
|
+
* @param dlcOffer Dlc Offer Message
|
|
3229
|
+
* @param dlcAccept Dlc Accept Message
|
|
3230
|
+
* @param dlcSign Dlc Sign Message
|
|
3231
|
+
* @param dlcTxs Dlc Transactions message
|
|
3232
|
+
* @returns {Promise<Tx>}
|
|
3233
|
+
*/
|
|
3234
|
+
async refund(
|
|
3235
|
+
dlcOffer: DlcOffer,
|
|
3236
|
+
dlcAccept: DlcAccept,
|
|
3237
|
+
dlcSign: DlcSign,
|
|
3238
|
+
dlcTxs: DlcTransactions,
|
|
3239
|
+
): Promise<Tx> {
|
|
3240
|
+
const network = await this.getConnectedNetwork();
|
|
3241
|
+
const psbt = new Psbt({ network });
|
|
3242
|
+
|
|
3243
|
+
// Verify refund transaction locktime matches expected
|
|
3244
|
+
if (Number(dlcTxs.refundTx.locktime) !== dlcOffer.refundLocktime) {
|
|
3245
|
+
throw new Error(
|
|
3246
|
+
`Refund transaction locktime ${dlcTxs.refundTx.locktime} does not match expected ${dlcOffer.refundLocktime}`,
|
|
3247
|
+
);
|
|
3248
|
+
}
|
|
3249
|
+
|
|
3250
|
+
// Add the funding input
|
|
3251
|
+
const fundingOutput = dlcTxs.fundTx.outputs[dlcTxs.fundTxVout];
|
|
3252
|
+
psbt.addInput({
|
|
3253
|
+
hash: dlcTxs.fundTx.txId.toString(),
|
|
3254
|
+
index: dlcTxs.fundTxVout,
|
|
3255
|
+
sequence: 0,
|
|
3256
|
+
witnessUtxo: {
|
|
3257
|
+
script: fundingOutput.scriptPubKey.serialize().subarray(1), // Remove length prefix
|
|
3258
|
+
value: Number(fundingOutput.value.sats),
|
|
3259
|
+
},
|
|
3260
|
+
witnessScript: this.CreateFundingScript(dlcOffer, dlcAccept), // 2-of-2 multisig script
|
|
3261
|
+
});
|
|
3262
|
+
|
|
3263
|
+
// Add the refund output
|
|
3264
|
+
const refundOutput = dlcTxs.refundTx.outputs[0];
|
|
3265
|
+
psbt.addOutput({
|
|
3266
|
+
address: address.fromOutputScript(
|
|
3267
|
+
refundOutput.scriptPubKey.serialize().subarray(1),
|
|
3268
|
+
network,
|
|
3269
|
+
),
|
|
3270
|
+
value: Number(refundOutput.value.sats),
|
|
3271
|
+
});
|
|
3272
|
+
|
|
3273
|
+
// Set the locktime to match the refund transaction
|
|
3274
|
+
psbt.setLocktime(Number(dlcTxs.refundTx.locktime));
|
|
3275
|
+
|
|
3276
|
+
// Sort funding pubkeys for consistent signature ordering
|
|
3277
|
+
const fundingPubKeys =
|
|
3278
|
+
Buffer.compare(dlcOffer.fundingPubkey, dlcAccept.fundingPubkey) === -1
|
|
3279
|
+
? [dlcOffer.fundingPubkey, dlcAccept.fundingPubkey]
|
|
3280
|
+
: [dlcAccept.fundingPubkey, dlcOffer.fundingPubkey];
|
|
3281
|
+
|
|
3282
|
+
// Add both refund signatures as partial signatures
|
|
3283
|
+
psbt.updateInput(0, {
|
|
3284
|
+
partialSig: [
|
|
3285
|
+
{ pubkey: fundingPubKeys[0], signature: dlcSign.refundSignature },
|
|
3286
|
+
{ pubkey: fundingPubKeys[1], signature: dlcAccept.refundSignature },
|
|
3287
|
+
],
|
|
3288
|
+
});
|
|
3289
|
+
|
|
3290
|
+
// Validate all signatures
|
|
3291
|
+
psbt.validateSignaturesOfInput(
|
|
3292
|
+
0,
|
|
3293
|
+
(pubkey: Buffer, msghash: Buffer, signature: Buffer) => {
|
|
3294
|
+
return ecc.verify(msghash, pubkey, signature);
|
|
3295
|
+
},
|
|
3296
|
+
);
|
|
3297
|
+
|
|
3298
|
+
// Finalize the input - this will create the final witness script
|
|
3299
|
+
psbt.finalizeInput(0);
|
|
3300
|
+
|
|
3301
|
+
// Extract the final transaction
|
|
3302
|
+
const finalTx = psbt.extractTransaction();
|
|
3303
|
+
|
|
3304
|
+
// Convert to the expected Tx format
|
|
3305
|
+
return Tx.decode(StreamReader.fromBuffer(finalTx.toBuffer()));
|
|
3306
|
+
}
|
|
3307
|
+
|
|
3308
|
+
/**
|
|
3309
|
+
* Goal of createDlcClose is for alice (the initiator) to
|
|
3310
|
+
* 1. take dlcoffer, accept, and sign messages. Create a dlcClose message.
|
|
3311
|
+
* 2. Build a close tx, sign.
|
|
3312
|
+
* 3. return dlcClose message (no psbt)
|
|
3313
|
+
*/
|
|
3314
|
+
|
|
3315
|
+
/**
|
|
3316
|
+
* Generate DlcClose messagetype for closing DLC with Mutual Consent
|
|
3317
|
+
* @param _dlcOffer DlcOffer TLV (V0)
|
|
3318
|
+
* @param _dlcAccept DlcAccept TLV (V0)
|
|
3319
|
+
* @param _dlcTxs DlcTransactions TLV (V0)
|
|
3320
|
+
* @param initiatorPayoutSatoshis Amount initiator expects as a payout
|
|
3321
|
+
* @param isOfferer Whether offerer or not
|
|
3322
|
+
* @param _inputs Optionally specified closing inputs
|
|
3323
|
+
* @returns {Promise<DlcClose>}
|
|
3324
|
+
*/
|
|
3325
|
+
async createDlcClose(
|
|
3326
|
+
_dlcOffer: DlcOffer,
|
|
3327
|
+
_dlcAccept: DlcAccept,
|
|
3328
|
+
_dlcTxs: DlcTransactions,
|
|
3329
|
+
initiatorPayoutSatoshis: bigint,
|
|
3330
|
+
isOfferer?: boolean,
|
|
3331
|
+
_inputs?: Input[],
|
|
3332
|
+
): Promise<DlcClose> {
|
|
3333
|
+
const { dlcOffer, dlcAccept, dlcTxs } = checkTypes({
|
|
3334
|
+
_dlcOffer,
|
|
3335
|
+
_dlcAccept,
|
|
3336
|
+
_dlcTxs,
|
|
3337
|
+
});
|
|
3338
|
+
|
|
3339
|
+
if (isOfferer === undefined)
|
|
3340
|
+
isOfferer = await this.isOfferer(dlcOffer, dlcAccept);
|
|
3341
|
+
|
|
3342
|
+
const network = await this.getConnectedNetwork();
|
|
3343
|
+
const psbt = new Psbt({ network });
|
|
3344
|
+
|
|
3345
|
+
const fundingPubKeys =
|
|
3346
|
+
Buffer.compare(dlcOffer.fundingPubkey, dlcAccept.fundingPubkey) === -1
|
|
3347
|
+
? [dlcOffer.fundingPubkey, dlcAccept.fundingPubkey]
|
|
3348
|
+
: [dlcAccept.fundingPubkey, dlcOffer.fundingPubkey];
|
|
3349
|
+
|
|
3350
|
+
const p2ms = payments.p2ms({
|
|
3351
|
+
m: 2,
|
|
3352
|
+
pubkeys: fundingPubKeys,
|
|
3353
|
+
network,
|
|
3354
|
+
});
|
|
3355
|
+
|
|
3356
|
+
const paymentVariant = payments.p2wsh({
|
|
3357
|
+
redeem: p2ms,
|
|
3358
|
+
network,
|
|
3359
|
+
});
|
|
3360
|
+
|
|
3361
|
+
// Initiate and build PSBT
|
|
3362
|
+
let inputs: Input[] = _inputs;
|
|
3363
|
+
if (!_inputs) {
|
|
3364
|
+
const tempInputs = await this.GetInputsForAmountWithMode(
|
|
3365
|
+
[BigInt(20000)],
|
|
3366
|
+
dlcOffer.feeRatePerVb,
|
|
3367
|
+
_inputs || [],
|
|
3368
|
+
InputSupplementationMode.Required,
|
|
3369
|
+
);
|
|
3370
|
+
_inputs = tempInputs;
|
|
3371
|
+
}
|
|
3372
|
+
// Ensure all inputs have derivation paths by fetching from wallet
|
|
3373
|
+
const inputsWithPaths: { input: Input; address: Address }[] =
|
|
3374
|
+
await Promise.all(
|
|
3375
|
+
_inputs.map(async (input) => {
|
|
3376
|
+
const address: Address = await this.getMethod('getWalletAddress')(
|
|
3377
|
+
input.address,
|
|
3378
|
+
);
|
|
3379
|
+
const inputWithPath = new Input(
|
|
3380
|
+
input.txid,
|
|
3381
|
+
input.vout,
|
|
3382
|
+
input.address,
|
|
3383
|
+
input.amount,
|
|
3384
|
+
input.value,
|
|
3385
|
+
input.derivationPath || address.derivationPath, // Use derivationPath from wallet if not set
|
|
3386
|
+
input.maxWitnessLength,
|
|
3387
|
+
input.redeemScript,
|
|
3388
|
+
input.inputSerialId || generateSerialId(),
|
|
3389
|
+
input.scriptPubKey,
|
|
3390
|
+
input.label,
|
|
3391
|
+
input.confirmations,
|
|
3392
|
+
input.spendable,
|
|
3393
|
+
input.solvable,
|
|
3394
|
+
input.safe,
|
|
3395
|
+
input.dlcInput,
|
|
3396
|
+
);
|
|
3397
|
+
return { input: inputWithPath, address };
|
|
3398
|
+
}),
|
|
3399
|
+
);
|
|
3400
|
+
|
|
3401
|
+
inputs = inputsWithPaths.map((item) => item.input);
|
|
3402
|
+
const pubkeys: Buffer[] = inputsWithPaths.map((item) =>
|
|
3403
|
+
Buffer.from(item.address.publicKey, 'hex'),
|
|
3404
|
+
);
|
|
3405
|
+
|
|
3406
|
+
const fundingInputSerialId = generateSerialId();
|
|
3407
|
+
|
|
3408
|
+
// Make temporary array to hold all inputs and then sort them
|
|
3409
|
+
// this method can be improved later
|
|
3410
|
+
const psbtInputs = [];
|
|
3411
|
+
psbtInputs.push({
|
|
3412
|
+
hash: dlcTxs.fundTx.txId.serialize(),
|
|
3413
|
+
index: dlcTxs.fundTxVout,
|
|
3414
|
+
sequence: 0,
|
|
3415
|
+
witnessUtxo: {
|
|
3416
|
+
script: paymentVariant.output,
|
|
3417
|
+
value: Number(this.getFundOutputValueSats(dlcTxs)),
|
|
3418
|
+
},
|
|
3419
|
+
witnessScript: paymentVariant.redeem.output,
|
|
3420
|
+
inputSerialId: fundingInputSerialId,
|
|
3421
|
+
derivationPath: null,
|
|
3422
|
+
});
|
|
3423
|
+
|
|
3424
|
+
// add all dlc close inputs
|
|
3425
|
+
inputs.forEach((input, i) => {
|
|
3426
|
+
const paymentVariant = payments.p2wpkh({ pubkey: pubkeys[i], network });
|
|
3427
|
+
|
|
3428
|
+
psbtInputs.push({
|
|
3429
|
+
hash: input.txid,
|
|
3430
|
+
index: input.vout,
|
|
3431
|
+
sequence: 0,
|
|
3432
|
+
witnessUtxo: {
|
|
3433
|
+
script: paymentVariant.output,
|
|
3434
|
+
value: input.value,
|
|
3435
|
+
},
|
|
3436
|
+
inputSerialId: input.inputSerialId,
|
|
3437
|
+
derivationPath: input.derivationPath,
|
|
3438
|
+
});
|
|
3439
|
+
});
|
|
3440
|
+
|
|
3441
|
+
// sort all inputs in ascending order by serial ID
|
|
3442
|
+
// The only reason we are doing this is for privacy. If the fundingInput is
|
|
3443
|
+
// always first, it is very obvious. Hence, a serialId is randomly generated
|
|
3444
|
+
// and the inputs are sorted by that instead.
|
|
3445
|
+
const sortedPsbtInputs = psbtInputs.sort((a, b) =>
|
|
3446
|
+
Number(a.inputSerialId - b.inputSerialId),
|
|
3447
|
+
);
|
|
3448
|
+
|
|
3449
|
+
// Get index of fundingInput
|
|
3450
|
+
const fundingInputIndex = sortedPsbtInputs.findIndex(
|
|
3451
|
+
(input) => input.inputSerialId === fundingInputSerialId,
|
|
3452
|
+
);
|
|
3453
|
+
|
|
3454
|
+
// add to psbt
|
|
3455
|
+
sortedPsbtInputs.forEach((input) => psbt.addInput(input));
|
|
3456
|
+
|
|
3457
|
+
const fundingInputs: FundingInput[] = await Promise.all(
|
|
3458
|
+
inputs.map(async (input) => {
|
|
3459
|
+
return this.inputToFundingInput(input);
|
|
3460
|
+
}),
|
|
3461
|
+
);
|
|
3462
|
+
|
|
3463
|
+
const finalizer = new DualClosingTxFinalizer(
|
|
3464
|
+
fundingInputs,
|
|
3465
|
+
dlcOffer.payoutSpk,
|
|
3466
|
+
dlcAccept.payoutSpk,
|
|
3467
|
+
dlcOffer.feeRatePerVb,
|
|
3468
|
+
);
|
|
3469
|
+
|
|
3470
|
+
const closeInputAmount = BigInt(
|
|
3471
|
+
inputs.reduce((acc, val) => acc + val.value, 0),
|
|
3472
|
+
);
|
|
3473
|
+
|
|
3474
|
+
const offerPayoutValue: bigint = isOfferer
|
|
3475
|
+
? closeInputAmount +
|
|
3476
|
+
initiatorPayoutSatoshis -
|
|
3477
|
+
finalizer.offerInitiatorFees
|
|
3478
|
+
: dlcOffer.contractInfo.totalCollateral - initiatorPayoutSatoshis;
|
|
3479
|
+
|
|
3480
|
+
const acceptPayoutValue: bigint = isOfferer
|
|
3481
|
+
? dlcOffer.contractInfo.totalCollateral - initiatorPayoutSatoshis
|
|
3482
|
+
: closeInputAmount +
|
|
3483
|
+
initiatorPayoutSatoshis -
|
|
3484
|
+
finalizer.offerInitiatorFees;
|
|
3485
|
+
|
|
3486
|
+
const offerFirst = dlcOffer.payoutSerialId < dlcAccept.payoutSerialId;
|
|
3487
|
+
|
|
3488
|
+
psbt.addOutput({
|
|
3489
|
+
value: Number(offerFirst ? offerPayoutValue : acceptPayoutValue),
|
|
3490
|
+
address: address.fromOutputScript(
|
|
3491
|
+
offerFirst ? dlcOffer.payoutSpk : dlcAccept.payoutSpk,
|
|
3492
|
+
network,
|
|
3493
|
+
),
|
|
3494
|
+
});
|
|
3495
|
+
|
|
3496
|
+
psbt.addOutput({
|
|
3497
|
+
value: Number(offerFirst ? acceptPayoutValue : offerPayoutValue),
|
|
3498
|
+
address: address.fromOutputScript(
|
|
3499
|
+
offerFirst ? dlcAccept.payoutSpk : dlcOffer.payoutSpk,
|
|
3500
|
+
network,
|
|
3501
|
+
),
|
|
3502
|
+
});
|
|
3503
|
+
|
|
3504
|
+
// Generate keypair to sign inputs
|
|
3505
|
+
const fundPrivateKeyPair = await this.GetFundKeyPair(
|
|
3506
|
+
dlcOffer,
|
|
3507
|
+
dlcAccept,
|
|
3508
|
+
isOfferer,
|
|
3509
|
+
);
|
|
3510
|
+
|
|
3511
|
+
// Sign dlc fundinginput
|
|
3512
|
+
psbt.signInput(fundingInputIndex, fundPrivateKeyPair);
|
|
3513
|
+
|
|
3514
|
+
// Sign dlcclose inputs
|
|
3515
|
+
await Promise.all(
|
|
3516
|
+
sortedPsbtInputs.map(async (input, i) => {
|
|
3517
|
+
if (i === fundingInputIndex) return;
|
|
3518
|
+
|
|
3519
|
+
// derive keypair
|
|
3520
|
+
if (!input.derivationPath) {
|
|
3521
|
+
throw new Error(`Missing derivation path for input ${i}`);
|
|
3522
|
+
}
|
|
3523
|
+
const keyPair = await this.getMethod('keyPair')(input.derivationPath);
|
|
3524
|
+
psbt.signInput(i, keyPair);
|
|
3525
|
+
}),
|
|
3526
|
+
);
|
|
3527
|
+
|
|
3528
|
+
// Validate signatures
|
|
3529
|
+
psbt.validateSignaturesOfAllInputs(
|
|
3530
|
+
(pubkey: Buffer, msghash: Buffer, signature: Buffer) => {
|
|
3531
|
+
return ecc.verify(msghash, pubkey, signature);
|
|
3532
|
+
},
|
|
3533
|
+
);
|
|
3534
|
+
|
|
3535
|
+
// Extract close signature from psbt and decode it to only extract r and s values
|
|
3536
|
+
const closeSignature = await script.signature.decode(
|
|
3537
|
+
psbt.data.inputs[fundingInputIndex].partialSig[0].signature,
|
|
3538
|
+
).signature;
|
|
3539
|
+
|
|
3540
|
+
// Extract funding signatures from psbt
|
|
3541
|
+
const inputSigs = psbt.data.inputs
|
|
3542
|
+
.filter((input) => input !== fundingInputIndex)
|
|
3543
|
+
.map((input) => input.partialSig[0]);
|
|
3544
|
+
|
|
3545
|
+
// create fundingSignatures
|
|
3546
|
+
const witnessElements: ScriptWitnessV0[][] = [];
|
|
3547
|
+
for (let i = 0; i < inputSigs.length; i++) {
|
|
3548
|
+
const sigWitness = new ScriptWitnessV0();
|
|
3549
|
+
sigWitness.witness = inputSigs[i].signature;
|
|
3550
|
+
const pubKeyWitness = new ScriptWitnessV0();
|
|
3551
|
+
pubKeyWitness.witness = inputSigs[i].pubkey;
|
|
3552
|
+
witnessElements.push([sigWitness, pubKeyWitness]);
|
|
3553
|
+
}
|
|
3554
|
+
const fundingSignatures = new FundingSignatures();
|
|
3555
|
+
fundingSignatures.witnessElements = witnessElements;
|
|
3556
|
+
|
|
3557
|
+
// Create DlcClose
|
|
3558
|
+
const dlcClose = new DlcClose();
|
|
3559
|
+
dlcClose.contractId = dlcTxs.contractId;
|
|
3560
|
+
dlcClose.offerPayoutSatoshis = BigInt(
|
|
3561
|
+
psbt.txOutputs[offerFirst ? 0 : 1].value,
|
|
3562
|
+
); // You give collateral back to users
|
|
3563
|
+
dlcClose.acceptPayoutSatoshis = BigInt(
|
|
3564
|
+
psbt.txOutputs[offerFirst ? 1 : 0].value,
|
|
3565
|
+
); // give collateral back to users
|
|
3566
|
+
dlcClose.fundInputSerialId = fundingInputSerialId; // randomly generated serial id
|
|
3567
|
+
dlcClose.closeSignature = closeSignature;
|
|
3568
|
+
dlcClose.fundingSignatures = fundingSignatures;
|
|
3569
|
+
dlcClose.fundingInputs = fundingInputs as FundingInput[];
|
|
3570
|
+
dlcClose.validate();
|
|
3571
|
+
|
|
3572
|
+
return dlcClose;
|
|
3573
|
+
}
|
|
3574
|
+
|
|
3575
|
+
/**
|
|
3576
|
+
* Generate multiple DlcClose messagetypes for closing DLC with Mutual Consent
|
|
3577
|
+
* @param _dlcOffer DlcOffer TLV (V0)
|
|
3578
|
+
* @param _dlcAccept DlcAccept TLV (V0)
|
|
3579
|
+
* @param _dlcTxs DlcTransactions TLV (V0)
|
|
3580
|
+
* @param initiatorPayouts Array of amounts initiator expects as payouts
|
|
3581
|
+
* @param isOfferer Whether offerer or not
|
|
3582
|
+
* @param _inputs Optionally specified closing inputs
|
|
3583
|
+
* @returns {Promise<DlcClose[]>}
|
|
3584
|
+
*/
|
|
3585
|
+
async createBatchDlcClose(
|
|
3586
|
+
_dlcOffer: DlcOffer,
|
|
3587
|
+
_dlcAccept: DlcAccept,
|
|
3588
|
+
_dlcTxs: DlcTransactions,
|
|
3589
|
+
initiatorPayouts: bigint[],
|
|
3590
|
+
isOfferer?: boolean,
|
|
3591
|
+
_inputs?: Input[],
|
|
3592
|
+
): Promise<DlcClose[]> {
|
|
3593
|
+
const { dlcOffer, dlcAccept, dlcTxs } = checkTypes({
|
|
3594
|
+
_dlcOffer,
|
|
3595
|
+
_dlcAccept,
|
|
3596
|
+
_dlcTxs,
|
|
3597
|
+
});
|
|
3598
|
+
|
|
3599
|
+
if (isOfferer === undefined)
|
|
3600
|
+
isOfferer = await this.isOfferer(dlcOffer, dlcAccept);
|
|
3601
|
+
|
|
3602
|
+
if (_inputs && _inputs.length > 0)
|
|
3603
|
+
throw Error('funding inputs not supported on BatchDlcClose'); // TODO support multiple funding inputs
|
|
3604
|
+
|
|
3605
|
+
const fundingInputSerialId = generateSerialId();
|
|
3606
|
+
|
|
3607
|
+
const fundingInputs: FundingInput[] = []; // TODO: support multiple funding inputs
|
|
3608
|
+
|
|
3609
|
+
const finalizer = new DualClosingTxFinalizer(
|
|
3610
|
+
fundingInputs,
|
|
3611
|
+
dlcOffer.payoutSpk,
|
|
3612
|
+
dlcAccept.payoutSpk,
|
|
3613
|
+
dlcOffer.feeRatePerVb,
|
|
3614
|
+
);
|
|
3615
|
+
|
|
3616
|
+
// Generate keypair to sign inputs
|
|
3617
|
+
const fundPrivateKeyPair = await this.GetFundKeyPair(
|
|
3618
|
+
dlcOffer,
|
|
3619
|
+
dlcAccept,
|
|
3620
|
+
isOfferer,
|
|
3621
|
+
);
|
|
3622
|
+
|
|
3623
|
+
const closeInputAmount = BigInt(0); // TODO support multiple funding inputs
|
|
3624
|
+
|
|
3625
|
+
const privKey = Buffer.from(fundPrivateKeyPair.privateKey).toString('hex');
|
|
3626
|
+
|
|
3627
|
+
const rawCloseTxs = await this.CreateCloseRawTxs(
|
|
3628
|
+
dlcOffer,
|
|
3629
|
+
dlcAccept,
|
|
3630
|
+
dlcTxs,
|
|
3631
|
+
closeInputAmount,
|
|
3632
|
+
isOfferer,
|
|
3633
|
+
[],
|
|
3634
|
+
fundingInputs,
|
|
3635
|
+
initiatorPayouts,
|
|
3636
|
+
);
|
|
3637
|
+
|
|
3638
|
+
const sigHashes = await this.CreateSignatureHashes(
|
|
3639
|
+
dlcOffer,
|
|
3640
|
+
dlcAccept,
|
|
3641
|
+
dlcTxs,
|
|
3642
|
+
rawCloseTxs,
|
|
3643
|
+
);
|
|
3644
|
+
|
|
3645
|
+
const signatures = await this.CalculateEcSignatureHashes(
|
|
3646
|
+
sigHashes,
|
|
3647
|
+
privKey,
|
|
3648
|
+
);
|
|
3649
|
+
|
|
3650
|
+
const dlcCloses = [];
|
|
3651
|
+
|
|
3652
|
+
signatures.forEach((sig, i) => {
|
|
3653
|
+
const payout = initiatorPayouts[i];
|
|
3654
|
+
const payoutMinusOfferFees =
|
|
3655
|
+
finalizer.offerInitiatorFees > payout
|
|
3656
|
+
? BigInt(0)
|
|
3657
|
+
: payout - finalizer.offerInitiatorFees;
|
|
3658
|
+
const collateralMinusPayout =
|
|
3659
|
+
payout > dlcOffer.contractInfo.totalCollateral
|
|
3660
|
+
? BigInt(0)
|
|
3661
|
+
: dlcOffer.contractInfo.totalCollateral - payout;
|
|
3662
|
+
|
|
3663
|
+
const offerPayoutValue: bigint = isOfferer
|
|
3664
|
+
? closeInputAmount + payoutMinusOfferFees
|
|
3665
|
+
: collateralMinusPayout;
|
|
3666
|
+
|
|
3667
|
+
const acceptPayoutValue: bigint = isOfferer
|
|
3668
|
+
? collateralMinusPayout
|
|
3669
|
+
: closeInputAmount + payoutMinusOfferFees;
|
|
3670
|
+
|
|
3671
|
+
const fundingSignatures = new FundingSignatures();
|
|
3672
|
+
|
|
3673
|
+
const dlcClose = new DlcClose();
|
|
3674
|
+
dlcClose.contractId = dlcTxs.contractId;
|
|
3675
|
+
dlcClose.offerPayoutSatoshis = offerPayoutValue;
|
|
3676
|
+
dlcClose.acceptPayoutSatoshis = acceptPayoutValue;
|
|
3677
|
+
dlcClose.fundInputSerialId = fundingInputSerialId;
|
|
3678
|
+
dlcClose.closeSignature = Buffer.from(sig, 'hex');
|
|
3679
|
+
dlcClose.fundingSignatures = fundingSignatures;
|
|
3680
|
+
dlcClose.validate();
|
|
3681
|
+
|
|
3682
|
+
dlcCloses.push(dlcClose);
|
|
3683
|
+
});
|
|
3684
|
+
|
|
3685
|
+
return dlcCloses;
|
|
3686
|
+
}
|
|
3687
|
+
|
|
3688
|
+
async verifyBatchDlcCloseUsingMetadata(
|
|
3689
|
+
dlcCloseMetadata: DlcCloseMetadata,
|
|
3690
|
+
_dlcCloses: DlcClose[],
|
|
3691
|
+
isOfferer?: boolean,
|
|
3692
|
+
): Promise<void> {
|
|
3693
|
+
const { dlcOffer, dlcAccept, dlcTxs } = dlcCloseMetadata.toDlcMessages();
|
|
3694
|
+
|
|
3695
|
+
await this.verifyBatchDlcClose(
|
|
3696
|
+
dlcOffer,
|
|
3697
|
+
dlcAccept,
|
|
3698
|
+
dlcTxs,
|
|
3699
|
+
_dlcCloses,
|
|
3700
|
+
isOfferer,
|
|
3701
|
+
);
|
|
3702
|
+
}
|
|
3703
|
+
|
|
3704
|
+
/**
|
|
3705
|
+
* Verify multiple DlcClose messagetypes for closing DLC with Mutual Consent
|
|
3706
|
+
* @param _dlcOffer DlcOffer TLV (V0)
|
|
3707
|
+
* @param _dlcAccept DlcAccept TLV (V0)
|
|
3708
|
+
* @param _dlcTxs DlcTransactions TLV (V0)
|
|
3709
|
+
* @param _dlcCloses DlcClose[] TLV (V0)
|
|
3710
|
+
* @param isOfferer Whether offerer or not
|
|
3711
|
+
* @returns {Promise<void>}
|
|
3712
|
+
*/
|
|
3713
|
+
async verifyBatchDlcClose(
|
|
3714
|
+
_dlcOffer: DlcOffer,
|
|
3715
|
+
_dlcAccept: DlcAccept,
|
|
3716
|
+
_dlcTxs: DlcTransactions,
|
|
3717
|
+
_dlcCloses: DlcClose[],
|
|
3718
|
+
isOfferer?: boolean,
|
|
3719
|
+
): Promise<void> {
|
|
3720
|
+
const { dlcOffer, dlcAccept, dlcTxs } = checkTypes({
|
|
3721
|
+
_dlcOffer,
|
|
3722
|
+
_dlcAccept,
|
|
3723
|
+
_dlcTxs,
|
|
3724
|
+
});
|
|
3725
|
+
|
|
3726
|
+
const dlcCloses = _dlcCloses.map(
|
|
3727
|
+
(_dlcClose) => checkTypes({ _dlcClose }).dlcClose,
|
|
3728
|
+
);
|
|
3729
|
+
|
|
3730
|
+
if (isOfferer === undefined)
|
|
3731
|
+
isOfferer = await this.isOfferer(dlcOffer, dlcAccept);
|
|
3732
|
+
|
|
3733
|
+
assert(
|
|
3734
|
+
dlcCloses.every((dlcClose) => dlcClose.fundingInputs.length === 0),
|
|
3735
|
+
'funding inputs not supported on verify BatchDlcClose',
|
|
3736
|
+
); // TODO support multiple funding inputs
|
|
3737
|
+
|
|
3738
|
+
const closeInputAmount = BigInt(0); // TODO support multiple funding inputs
|
|
3739
|
+
|
|
3740
|
+
const rawCloseTxs = await this.CreateCloseRawTxs(
|
|
3741
|
+
dlcOffer,
|
|
3742
|
+
dlcAccept,
|
|
3743
|
+
dlcTxs,
|
|
3744
|
+
closeInputAmount,
|
|
3745
|
+
isOfferer,
|
|
3746
|
+
dlcCloses,
|
|
3747
|
+
);
|
|
3748
|
+
|
|
3749
|
+
const areSigsValid = await this.VerifySignatures(
|
|
3750
|
+
dlcOffer,
|
|
3751
|
+
dlcAccept,
|
|
3752
|
+
dlcTxs,
|
|
3753
|
+
dlcCloses,
|
|
3754
|
+
rawCloseTxs,
|
|
3755
|
+
isOfferer,
|
|
3756
|
+
);
|
|
3757
|
+
|
|
3758
|
+
assert(areSigsValid, 'Signatures invalid in Verify Batch DlcClose');
|
|
3759
|
+
}
|
|
3760
|
+
|
|
3761
|
+
/**
|
|
3762
|
+
* Goal of finalize Dlc Close is for bob to
|
|
3763
|
+
* 1. take the dlcClose created by alice using createDlcClose,
|
|
3764
|
+
* 2. Build a psbt using Alice's dlcClose message
|
|
3765
|
+
* 3. Sign psbt with bob's privkey
|
|
3766
|
+
* 4. return a tx ready to be broadcast
|
|
3767
|
+
*/
|
|
3768
|
+
|
|
3769
|
+
/**
|
|
3770
|
+
* Finalize Dlc Close
|
|
3771
|
+
* @param _dlcOffer Dlc Offer Message
|
|
3772
|
+
* @param _dlcAccept Dlc Accept Message
|
|
3773
|
+
* @param _dlcClose Dlc Close Message
|
|
3774
|
+
* @param _dlcTxs Dlc Transactions Message
|
|
3775
|
+
* @returns {Promise<Tx>}
|
|
3776
|
+
*/
|
|
3777
|
+
async finalizeDlcClose(
|
|
3778
|
+
_dlcOffer: DlcOffer,
|
|
3779
|
+
_dlcAccept: DlcAccept,
|
|
3780
|
+
_dlcClose: DlcClose,
|
|
3781
|
+
_dlcTxs: DlcTransactions,
|
|
3782
|
+
): Promise<string> {
|
|
3783
|
+
const { dlcOffer, dlcAccept, dlcClose, dlcTxs } = checkTypes({
|
|
3784
|
+
_dlcOffer,
|
|
3785
|
+
_dlcAccept,
|
|
3786
|
+
_dlcClose,
|
|
3787
|
+
_dlcTxs,
|
|
3788
|
+
});
|
|
3789
|
+
|
|
3790
|
+
dlcOffer.validate();
|
|
3791
|
+
dlcAccept.validate();
|
|
3792
|
+
dlcClose.validate();
|
|
3793
|
+
|
|
3794
|
+
const network = await this.getConnectedNetwork();
|
|
3795
|
+
const psbt = new Psbt({ network });
|
|
3796
|
+
|
|
3797
|
+
const fundingPubKeys =
|
|
3798
|
+
Buffer.compare(dlcOffer.fundingPubkey, dlcAccept.fundingPubkey) === -1
|
|
3799
|
+
? [dlcOffer.fundingPubkey, dlcAccept.fundingPubkey]
|
|
3800
|
+
: [dlcAccept.fundingPubkey, dlcOffer.fundingPubkey];
|
|
3801
|
+
|
|
3802
|
+
const p2ms = payments.p2ms({
|
|
3803
|
+
m: 2,
|
|
3804
|
+
pubkeys: fundingPubKeys,
|
|
3805
|
+
network,
|
|
3806
|
+
});
|
|
3807
|
+
|
|
3808
|
+
const paymentVariant = payments.p2wsh({
|
|
3809
|
+
redeem: p2ms,
|
|
3810
|
+
network,
|
|
3811
|
+
});
|
|
3812
|
+
|
|
3813
|
+
// Make temporary array to hold all inputs and then sort them
|
|
3814
|
+
// this method can be improved later
|
|
3815
|
+
const psbtInputs = [];
|
|
3816
|
+
psbtInputs.push({
|
|
3817
|
+
hash: dlcTxs.fundTx.txId.serialize(),
|
|
3818
|
+
index: dlcTxs.fundTxVout,
|
|
3819
|
+
sequence: 0,
|
|
3820
|
+
witnessUtxo: {
|
|
3821
|
+
script: paymentVariant.output,
|
|
3822
|
+
value: Number(this.getFundOutputValueSats(dlcTxs)),
|
|
3823
|
+
},
|
|
3824
|
+
witnessScript: paymentVariant.redeem.output,
|
|
3825
|
+
inputSerialId: dlcClose.fundInputSerialId,
|
|
3826
|
+
});
|
|
3827
|
+
|
|
3828
|
+
// add all dlc close inputs
|
|
3829
|
+
dlcClose.fundingInputs.forEach((input) => {
|
|
3830
|
+
psbtInputs.push({
|
|
3831
|
+
hash: input.prevTx.txId.serialize(),
|
|
3832
|
+
index: input.prevTxVout,
|
|
3833
|
+
sequence: 0,
|
|
3834
|
+
witnessUtxo: {
|
|
3835
|
+
script: input.prevTx.outputs[input.prevTxVout].scriptPubKey
|
|
3836
|
+
.serialize()
|
|
3837
|
+
.slice(1),
|
|
3838
|
+
value: Number(input.prevTx.outputs[input.prevTxVout].value.sats),
|
|
3839
|
+
},
|
|
3840
|
+
inputSerialId: input.inputSerialId,
|
|
3841
|
+
});
|
|
3842
|
+
});
|
|
3843
|
+
|
|
3844
|
+
// sort all inputs in ascending order by serial ID
|
|
3845
|
+
// The only reason we are doing this is for privacy. If the fundingInput is
|
|
3846
|
+
// always first, it is very obvious. Hence, a serialId is randomly generated
|
|
3847
|
+
// and the inputs are sorted by that instead.
|
|
3848
|
+
const sortedPsbtInputs = psbtInputs.sort((a, b) =>
|
|
3849
|
+
Number(a.inputSerialId - b.inputSerialId),
|
|
3850
|
+
);
|
|
3851
|
+
|
|
3852
|
+
// Get index of fundingInput
|
|
3853
|
+
const fundingInputIndex = sortedPsbtInputs.findIndex(
|
|
3854
|
+
(input) => input.inputSerialId === dlcClose.fundInputSerialId,
|
|
3855
|
+
);
|
|
3856
|
+
|
|
3857
|
+
const offerFirst = dlcOffer.payoutSerialId < dlcAccept.payoutSerialId;
|
|
3858
|
+
|
|
3859
|
+
psbt.addOutput({
|
|
3860
|
+
value: Number(
|
|
3861
|
+
offerFirst
|
|
3862
|
+
? dlcClose.offerPayoutSatoshis
|
|
3863
|
+
: dlcClose.acceptPayoutSatoshis,
|
|
3864
|
+
),
|
|
3865
|
+
address: address.fromOutputScript(
|
|
3866
|
+
offerFirst ? dlcOffer.payoutSpk : dlcAccept.payoutSpk,
|
|
3867
|
+
network,
|
|
3868
|
+
),
|
|
3869
|
+
});
|
|
3870
|
+
|
|
3871
|
+
psbt.addOutput({
|
|
3872
|
+
value: Number(
|
|
3873
|
+
offerFirst
|
|
3874
|
+
? dlcClose.acceptPayoutSatoshis
|
|
3875
|
+
: dlcClose.offerPayoutSatoshis,
|
|
3876
|
+
),
|
|
3877
|
+
address: address.fromOutputScript(
|
|
3878
|
+
offerFirst ? dlcAccept.payoutSpk : dlcOffer.payoutSpk,
|
|
3879
|
+
network,
|
|
3880
|
+
),
|
|
3881
|
+
});
|
|
3882
|
+
|
|
3883
|
+
// add to psbt
|
|
3884
|
+
sortedPsbtInputs.forEach((input) => psbt.addInput(input));
|
|
3885
|
+
|
|
3886
|
+
const offerer = await this.isOfferer(dlcOffer, dlcAccept);
|
|
3887
|
+
|
|
3888
|
+
// Generate keypair to sign inputs
|
|
3889
|
+
const fundPrivateKeyPair = await this.GetFundKeyPair(
|
|
3890
|
+
dlcOffer,
|
|
3891
|
+
dlcAccept,
|
|
3892
|
+
offerer,
|
|
3893
|
+
);
|
|
3894
|
+
|
|
3895
|
+
// Sign dlc fundinginput
|
|
3896
|
+
psbt.signInput(fundingInputIndex, fundPrivateKeyPair);
|
|
3897
|
+
|
|
3898
|
+
const partialSig = [
|
|
3899
|
+
{
|
|
3900
|
+
pubkey: offerer ? dlcAccept.fundingPubkey : dlcOffer.fundingPubkey,
|
|
3901
|
+
signature: script.signature.encode(dlcClose.closeSignature, 1), // encode using SIGHASH_ALL
|
|
3902
|
+
},
|
|
3903
|
+
];
|
|
3904
|
+
psbt.updateInput(fundingInputIndex, { partialSig });
|
|
3905
|
+
|
|
3906
|
+
for (let i = 0; i < psbt.data.inputs.length; ++i) {
|
|
3907
|
+
if (i === fundingInputIndex) continue;
|
|
3908
|
+
if (!psbt.data.inputs[i].partialSig) psbt.data.inputs[i].partialSig = [];
|
|
3909
|
+
|
|
3910
|
+
const witnessI = dlcClose.fundingSignatures.witnessElements.findIndex(
|
|
3911
|
+
(el) =>
|
|
3912
|
+
Buffer.compare(
|
|
3913
|
+
Script.p2wpkhLock(hash160(el[1].witness)).serialize().subarray(1),
|
|
3914
|
+
psbt.data.inputs[i].witnessUtxo.script,
|
|
3915
|
+
) === 0,
|
|
3916
|
+
);
|
|
3917
|
+
|
|
3918
|
+
const partialSig = [
|
|
3919
|
+
{
|
|
3920
|
+
pubkey:
|
|
3921
|
+
dlcClose.fundingSignatures.witnessElements[witnessI][1].witness,
|
|
3922
|
+
signature:
|
|
3923
|
+
dlcClose.fundingSignatures.witnessElements[witnessI][0].witness,
|
|
3924
|
+
},
|
|
3925
|
+
];
|
|
3926
|
+
|
|
3927
|
+
psbt.updateInput(i, { partialSig });
|
|
3928
|
+
}
|
|
3929
|
+
|
|
3930
|
+
psbt.validateSignaturesOfAllInputs(
|
|
3931
|
+
(pubkey: Buffer, msghash: Buffer, signature: Buffer) => {
|
|
3932
|
+
return ecc.verify(msghash, pubkey, signature);
|
|
3933
|
+
},
|
|
3934
|
+
);
|
|
3935
|
+
psbt.finalizeAllInputs();
|
|
3936
|
+
|
|
3937
|
+
return psbt.extractTransaction().toHex();
|
|
3938
|
+
}
|
|
3939
|
+
|
|
3940
|
+
async fundingInputToInput(
|
|
3941
|
+
_input: FundingInput,
|
|
3942
|
+
findDerivationPath = true,
|
|
3943
|
+
): Promise<Input> {
|
|
3944
|
+
assert(_input.type === MessageType.FundingInput, 'FundingInput must be V0');
|
|
3945
|
+
const network = await this.getConnectedNetwork();
|
|
3946
|
+
const input = _input as FundingInput;
|
|
3947
|
+
const prevTx = input.prevTx;
|
|
3948
|
+
const prevTxOut = prevTx.outputs[input.prevTxVout];
|
|
3949
|
+
const scriptPubKey = prevTxOut.scriptPubKey.serialize().subarray(1);
|
|
3950
|
+
const _address = address.fromOutputScript(scriptPubKey, network);
|
|
3951
|
+
let derivationPath: string;
|
|
3952
|
+
|
|
3953
|
+
if (findDerivationPath) {
|
|
3954
|
+
const inputAddress: Address = await this.client.wallet.findAddress([
|
|
3955
|
+
_address,
|
|
3956
|
+
]);
|
|
3957
|
+
if (inputAddress) {
|
|
3958
|
+
derivationPath = inputAddress.derivationPath;
|
|
3959
|
+
}
|
|
3960
|
+
}
|
|
3961
|
+
|
|
3962
|
+
// Check if this FundingInput has DLC input information to preserve
|
|
3963
|
+
const dlcInputMessage = input.dlcInput;
|
|
3964
|
+
|
|
3965
|
+
let dlcInputInfo: DlcInputInfo | undefined;
|
|
3966
|
+
if (dlcInputMessage) {
|
|
3967
|
+
dlcInputInfo = {
|
|
3968
|
+
localFundPubkey: dlcInputMessage.localFundPubkey.toString('hex'),
|
|
3969
|
+
remoteFundPubkey: dlcInputMessage.remoteFundPubkey.toString('hex'),
|
|
3970
|
+
contractId: dlcInputMessage.contractId.toString('hex'),
|
|
3971
|
+
};
|
|
3972
|
+
}
|
|
3973
|
+
|
|
3974
|
+
return new Input(
|
|
3975
|
+
prevTx.txId.toString(),
|
|
3976
|
+
input.prevTxVout,
|
|
3977
|
+
_address,
|
|
3978
|
+
prevTxOut.value.bitcoin,
|
|
3979
|
+
Number(prevTxOut.value.sats),
|
|
3980
|
+
derivationPath,
|
|
3981
|
+
input.maxWitnessLen,
|
|
3982
|
+
input.redeemScript ? input.redeemScript.toString('hex') : '',
|
|
3983
|
+
input.inputSerialId,
|
|
3984
|
+
scriptPubKey.toString('hex'),
|
|
3985
|
+
undefined, // label
|
|
3986
|
+
undefined, // confirmations
|
|
3987
|
+
undefined, // spendable
|
|
3988
|
+
undefined, // solvable
|
|
3989
|
+
undefined, // safe
|
|
3990
|
+
dlcInputInfo, // Preserve DLC input information if present
|
|
3991
|
+
);
|
|
3992
|
+
}
|
|
3993
|
+
|
|
3994
|
+
async inputToFundingInput(input: Input): Promise<FundingInput> {
|
|
3995
|
+
const fundingInput = new FundingInput();
|
|
3996
|
+
fundingInput.prevTxVout = input.vout;
|
|
3997
|
+
|
|
3998
|
+
let txRaw = '';
|
|
3999
|
+
try {
|
|
4000
|
+
txRaw = await this.getMethod('getRawTransactionByHash')(input.txid);
|
|
4001
|
+
} catch {
|
|
4002
|
+
try {
|
|
4003
|
+
txRaw = (await this.getMethod('jsonrpc')('gettransaction', input.txid))
|
|
4004
|
+
.hex;
|
|
4005
|
+
} catch {
|
|
4006
|
+
throw Error(
|
|
4007
|
+
`Cannot find tx ${input.txid} in inputToFundingInput using getrawtransactionbyhash or gettransaction`,
|
|
4008
|
+
);
|
|
4009
|
+
}
|
|
4010
|
+
}
|
|
4011
|
+
|
|
4012
|
+
const tx = Tx.decode(StreamReader.fromHex(txRaw));
|
|
4013
|
+
|
|
4014
|
+
fundingInput.prevTx = tx;
|
|
4015
|
+
fundingInput.sequence = Sequence.default();
|
|
4016
|
+
fundingInput.maxWitnessLen = input.maxWitnessLength
|
|
4017
|
+
? input.maxWitnessLength
|
|
4018
|
+
: 108;
|
|
4019
|
+
fundingInput.redeemScript = input.redeemScript
|
|
4020
|
+
? Buffer.from(input.redeemScript, 'hex')
|
|
4021
|
+
: Buffer.from('', 'hex');
|
|
4022
|
+
fundingInput.inputSerialId = input.inputSerialId
|
|
4023
|
+
? input.inputSerialId
|
|
4024
|
+
: generateSerialId();
|
|
4025
|
+
|
|
4026
|
+
// Preserve DLC input information if present
|
|
4027
|
+
if (input.isDlcInput()) {
|
|
4028
|
+
const dlcInputInfo = input.dlcInput!;
|
|
4029
|
+
const dlcInput = new DlcInput();
|
|
4030
|
+
dlcInput.localFundPubkey = Buffer.from(
|
|
4031
|
+
dlcInputInfo.localFundPubkey,
|
|
4032
|
+
'hex',
|
|
4033
|
+
);
|
|
4034
|
+
dlcInput.remoteFundPubkey = Buffer.from(
|
|
4035
|
+
dlcInputInfo.remoteFundPubkey,
|
|
4036
|
+
'hex',
|
|
4037
|
+
);
|
|
4038
|
+
dlcInput.contractId = Buffer.alloc(32); // Placeholder contract ID
|
|
4039
|
+
|
|
4040
|
+
fundingInput.dlcInput = dlcInput;
|
|
4041
|
+
}
|
|
4042
|
+
|
|
4043
|
+
return fundingInput;
|
|
4044
|
+
}
|
|
4045
|
+
|
|
4046
|
+
async getConnectedNetwork(): Promise<BitcoinNetwork> {
|
|
4047
|
+
return this._network;
|
|
4048
|
+
}
|
|
4049
|
+
|
|
4050
|
+
/**
|
|
4051
|
+
* Convert BitcoinNetwork to bitcoinjs-lib network format
|
|
4052
|
+
*/
|
|
4053
|
+
private getBitcoinJsNetwork(): any {
|
|
4054
|
+
const network = this._network;
|
|
4055
|
+
if (network.name === 'bitcoin') {
|
|
4056
|
+
return networks.bitcoin;
|
|
4057
|
+
} else if (network.name === 'testnet') {
|
|
4058
|
+
return networks.testnet;
|
|
4059
|
+
} else if (network.name === 'regtest') {
|
|
4060
|
+
return networks.regtest;
|
|
4061
|
+
} else {
|
|
4062
|
+
// Default to mainnet if unknown
|
|
4063
|
+
return networks.bitcoin;
|
|
4064
|
+
}
|
|
4065
|
+
}
|
|
4066
|
+
|
|
4067
|
+
/**
|
|
4068
|
+
* Calculate the maximum collateral possible with given inputs
|
|
4069
|
+
* @param inputs Array of Input objects to use for funding
|
|
4070
|
+
* @param feeRatePerVb Fee rate in satoshis per virtual byte
|
|
4071
|
+
* @param contractCount Number of DLC contracts (default: 1)
|
|
4072
|
+
* @returns Maximum collateral amount in satoshis
|
|
4073
|
+
*/
|
|
4074
|
+
async calculateMaxCollateral(
|
|
4075
|
+
inputs: Input[],
|
|
4076
|
+
feeRatePerVb: bigint,
|
|
4077
|
+
contractCount: number = 1,
|
|
4078
|
+
): Promise<bigint> {
|
|
4079
|
+
if (inputs.length === 0) {
|
|
4080
|
+
return BigInt(0);
|
|
4081
|
+
}
|
|
4082
|
+
|
|
4083
|
+
try {
|
|
4084
|
+
// Convert Input[] to FundingInput[]
|
|
4085
|
+
const fundingInputs = await Promise.all(
|
|
4086
|
+
inputs.map((input) => this.inputToFundingInput(input)),
|
|
4087
|
+
);
|
|
4088
|
+
|
|
4089
|
+
// Use node-dlc's calculateMaxCollateral function
|
|
4090
|
+
// For single-funded DLC, pass only offerer inputs and fee rate
|
|
4091
|
+
return BatchDlcTxBuilder.calculateMaxCollateral(
|
|
4092
|
+
fundingInputs,
|
|
4093
|
+
feeRatePerVb,
|
|
4094
|
+
contractCount,
|
|
4095
|
+
);
|
|
4096
|
+
} catch (error) {
|
|
4097
|
+
// If calculation fails, return 0 to indicate insufficient funds
|
|
4098
|
+
console.warn('calculateMaxCollateral failed:', error);
|
|
4099
|
+
return BigInt(0);
|
|
4100
|
+
}
|
|
4101
|
+
}
|
|
4102
|
+
|
|
4103
|
+
/**
|
|
4104
|
+
* Create a funding input with DLC input information for splicing
|
|
4105
|
+
* @param dlcInputInfo DLC input information
|
|
4106
|
+
* @param fundingTxHex Raw transaction hex of the funding transaction
|
|
4107
|
+
*/
|
|
4108
|
+
async createDlcFundingInput(
|
|
4109
|
+
dlcInputInfo: DlcInputInfoRequest,
|
|
4110
|
+
fundingTxHex: string,
|
|
4111
|
+
): Promise<FundingInput> {
|
|
4112
|
+
const fundingInput = new FundingInput();
|
|
4113
|
+
const tx = Tx.decode(StreamReader.fromHex(fundingTxHex));
|
|
4114
|
+
|
|
4115
|
+
fundingInput.prevTx = tx;
|
|
4116
|
+
fundingInput.prevTxVout = dlcInputInfo.fundVout;
|
|
4117
|
+
fundingInput.sequence = Sequence.default();
|
|
4118
|
+
fundingInput.maxWitnessLen = dlcInputInfo.maxWitnessLength || 220;
|
|
4119
|
+
fundingInput.redeemScript = Buffer.from('', 'hex'); // Empty for P2WSH
|
|
4120
|
+
fundingInput.inputSerialId = BigInt(
|
|
4121
|
+
dlcInputInfo.inputSerialId || generateSerialId(),
|
|
4122
|
+
);
|
|
4123
|
+
|
|
4124
|
+
// Create the DLC multisig script for address generation
|
|
4125
|
+
const localPubkey = Buffer.from(dlcInputInfo.localFundPubkey, 'hex');
|
|
4126
|
+
const remotePubkey = Buffer.from(dlcInputInfo.remoteFundPubkey, 'hex');
|
|
4127
|
+
|
|
4128
|
+
// Use the same deterministic ordering as cfd-dlc-js: lexicographic by hex
|
|
4129
|
+
// This matches GetOrderedPubkeys() in cfddlc_transactions.cpp
|
|
4130
|
+
const orderedPubkeys =
|
|
4131
|
+
dlcInputInfo.localFundPubkey < dlcInputInfo.remoteFundPubkey
|
|
4132
|
+
? [localPubkey, remotePubkey]
|
|
4133
|
+
: [remotePubkey, localPubkey];
|
|
4134
|
+
|
|
4135
|
+
const network = await this.getConnectedNetwork();
|
|
4136
|
+
|
|
4137
|
+
// Create 2-of-2 multisig payment using deterministic ordering
|
|
4138
|
+
const p2ms = payments.p2ms({
|
|
4139
|
+
m: 2,
|
|
4140
|
+
pubkeys: orderedPubkeys,
|
|
4141
|
+
network,
|
|
4142
|
+
});
|
|
4143
|
+
|
|
4144
|
+
const paymentVariant = payments.p2wsh({
|
|
4145
|
+
redeem: p2ms,
|
|
4146
|
+
network,
|
|
4147
|
+
});
|
|
4148
|
+
|
|
4149
|
+
const multisigAddress = paymentVariant.address!;
|
|
4150
|
+
|
|
4151
|
+
// Verify this matches the actual funding output address
|
|
4152
|
+
const actualFundingOutput = tx.outputs[dlcInputInfo.fundVout];
|
|
4153
|
+
const actualFundingAddress = address.fromOutputScript(
|
|
4154
|
+
actualFundingOutput.scriptPubKey.serialize().subarray(1),
|
|
4155
|
+
network,
|
|
4156
|
+
);
|
|
4157
|
+
|
|
4158
|
+
if (actualFundingAddress !== multisigAddress) {
|
|
4159
|
+
throw new Error(
|
|
4160
|
+
`DLC funding address mismatch. ` +
|
|
4161
|
+
`Expected: ${actualFundingAddress}, ` +
|
|
4162
|
+
`Constructed: ${multisigAddress}`,
|
|
4163
|
+
);
|
|
4164
|
+
}
|
|
4165
|
+
|
|
4166
|
+
// Add toUtxo method that's expected by GetInputsForAmount
|
|
4167
|
+
(fundingInput as FundingInput & { toUtxo: () => Utxo }).toUtxo = () => {
|
|
4168
|
+
return new Utxo(
|
|
4169
|
+
dlcInputInfo.fundTxid,
|
|
4170
|
+
dlcInputInfo.fundVout,
|
|
4171
|
+
Amount.FromSatoshis(Number(dlcInputInfo.fundAmount)),
|
|
4172
|
+
multisigAddress,
|
|
4173
|
+
dlcInputInfo.maxWitnessLength || 220,
|
|
4174
|
+
undefined, // DLC inputs don't have derivation paths
|
|
4175
|
+
fundingInput.inputSerialId,
|
|
4176
|
+
);
|
|
4177
|
+
};
|
|
4178
|
+
|
|
4179
|
+
// Create proper DlcInput object for splicing detection and signing
|
|
4180
|
+
const dlcInput = new DlcInput();
|
|
4181
|
+
dlcInput.localFundPubkey = Buffer.from(dlcInputInfo.localFundPubkey, 'hex');
|
|
4182
|
+
dlcInput.remoteFundPubkey = Buffer.from(
|
|
4183
|
+
dlcInputInfo.remoteFundPubkey,
|
|
4184
|
+
'hex',
|
|
4185
|
+
);
|
|
4186
|
+
dlcInput.contractId = Buffer.from(dlcInputInfo.contractId, 'hex');
|
|
4187
|
+
|
|
4188
|
+
fundingInput.dlcInput = dlcInput;
|
|
4189
|
+
|
|
4190
|
+
return fundingInput;
|
|
4191
|
+
}
|
|
4192
|
+
}
|
|
4193
|
+
|
|
4194
|
+
export interface BasicInitializeResponse {
|
|
4195
|
+
fundingPubKey: Buffer;
|
|
4196
|
+
payoutSPK: Buffer;
|
|
4197
|
+
payoutSerialId: bigint;
|
|
4198
|
+
changeSPK: Buffer;
|
|
4199
|
+
changeSerialId: bigint;
|
|
4200
|
+
}
|
|
4201
|
+
|
|
4202
|
+
export interface InitializeResponse extends BasicInitializeResponse {
|
|
4203
|
+
fundingInputs: FundingInput[];
|
|
4204
|
+
}
|
|
4205
|
+
|
|
4206
|
+
export interface BatchBaseInitializeResponse {
|
|
4207
|
+
fundingPubKey: Buffer;
|
|
4208
|
+
payoutSPK: Buffer;
|
|
4209
|
+
payoutSerialId: bigint;
|
|
4210
|
+
}
|
|
4211
|
+
|
|
4212
|
+
export interface BatchInitializeResponse {
|
|
4213
|
+
initializeResponses: BatchBaseInitializeResponse[];
|
|
4214
|
+
fundingInputs: FundingInput[];
|
|
4215
|
+
changeSPK: Buffer;
|
|
4216
|
+
changeSerialId: bigint;
|
|
4217
|
+
}
|
|
4218
|
+
|
|
4219
|
+
export interface AcceptDlcOfferResponse {
|
|
4220
|
+
dlcAccept: DlcAccept;
|
|
4221
|
+
dlcTransactions: DlcTransactions;
|
|
4222
|
+
}
|
|
4223
|
+
|
|
4224
|
+
export interface BatchAcceptDlcOfferResponse {
|
|
4225
|
+
dlcAccepts: DlcAccept[];
|
|
4226
|
+
dlcTransactionsList: DlcTransactions[];
|
|
4227
|
+
}
|
|
4228
|
+
|
|
4229
|
+
export interface SignDlcAcceptResponse {
|
|
4230
|
+
dlcSign: DlcSign;
|
|
4231
|
+
dlcTransactions: DlcTransactions;
|
|
4232
|
+
}
|
|
4233
|
+
|
|
4234
|
+
export interface BatchSignDlcAcceptResponse {
|
|
4235
|
+
dlcSigns: DlcSign[];
|
|
4236
|
+
dlcTransactionsList: DlcTransactions[];
|
|
4237
|
+
}
|
|
4238
|
+
|
|
4239
|
+
export interface GetPayoutsResponse {
|
|
4240
|
+
payouts: PayoutRequest[];
|
|
4241
|
+
payoutGroups: PayoutGroup[];
|
|
4242
|
+
messagesList: Messages[];
|
|
4243
|
+
}
|
|
4244
|
+
|
|
4245
|
+
export interface CreateDlcTxsResponse {
|
|
4246
|
+
dlcTransactions: DlcTransactions;
|
|
4247
|
+
messagesList: Messages[];
|
|
4248
|
+
}
|
|
4249
|
+
|
|
4250
|
+
export interface CreateBatchDlcTxsResponse {
|
|
4251
|
+
dlcTransactionsList: DlcTransactions[];
|
|
4252
|
+
nestedMessagesList: Messages[][];
|
|
4253
|
+
}
|
|
4254
|
+
|
|
4255
|
+
interface ISig {
|
|
4256
|
+
encryptedSig: Buffer;
|
|
4257
|
+
dleqProof: Buffer;
|
|
4258
|
+
}
|
|
4259
|
+
|
|
4260
|
+
export interface CreateCetAdaptorAndRefundSigsResponse {
|
|
4261
|
+
cetSignatures: CetAdaptorSignatures;
|
|
4262
|
+
refundSignature: Buffer;
|
|
4263
|
+
}
|
|
4264
|
+
|
|
4265
|
+
interface PayoutGroup {
|
|
4266
|
+
payout: bigint;
|
|
4267
|
+
groups: number[][];
|
|
4268
|
+
}
|
|
4269
|
+
|
|
4270
|
+
interface FindOutcomeResponse {
|
|
4271
|
+
index: number;
|
|
4272
|
+
groupLength: number;
|
|
4273
|
+
}
|
|
4274
|
+
|
|
4275
|
+
export interface Change {
|
|
4276
|
+
value: number;
|
|
4277
|
+
}
|
|
4278
|
+
|
|
4279
|
+
export interface Output {
|
|
4280
|
+
value: number;
|
|
4281
|
+
id?: string;
|
|
4282
|
+
}
|
|
4283
|
+
|
|
4284
|
+
export interface InputsForAmountResponse {
|
|
4285
|
+
inputs: Input[];
|
|
4286
|
+
change: Change;
|
|
4287
|
+
outputs: Output[];
|
|
4288
|
+
fee: number;
|
|
4289
|
+
}
|
|
4290
|
+
|
|
4291
|
+
export interface InputsForDualAmountResponse {
|
|
4292
|
+
inputs: Input[];
|
|
4293
|
+
fee: number;
|
|
4294
|
+
}
|