-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathswap.js
200 lines (172 loc) · 5.52 KB
/
swap.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
import zkpInit from "@vulpemventures/secp256k1-zkp";
import axios from "axios";
import {
Transaction,
address,
crypto,
initEccLib,
networks,
} from "bitcoinjs-lib";
import {
Musig,
OutputType,
SwapTreeSerializer,
TaprootUtils,
constructClaimTransaction,
detectSwap,
targetFee,
} from "boltz-core";
import { randomBytes } from "crypto";
import { ECPairFactory } from "ecpair";
import * as ecc from "tiny-secp256k1";
import ws from "ws";
// Endpoint of the Boltz instance to be used
const endpoint = "https://api.testnet.boltz.exchange";
// Amount you want to swap
const invoiceAmount = 50_000;
// Address to which the swap should be claimed
const destinationAddress =
"tb1pfjh36n5ksntze6dnzlexy2slda4uzx5z7pkrrs8shnd3k9hrtnss7dwgwh";
const network = networks.testnet;
const reverseSwap = async () => {
initEccLib(ecc);
// Create a random preimage for the swap; has to have a length of 32 bytes
const preimage = randomBytes(32);
const keys = ECPairFactory(ecc).makeRandom();
// Create a Submarine Swap
const createdResponse = (
await axios.post(`${endpoint}/v2/swap/reverse`, {
invoiceAmount,
to: "BTC",
from: "BTC",
claimPublicKey: keys.publicKey.toString("hex"),
preimageHash: crypto.sha256(preimage).toString("hex"),
})
).data;
console.log("Created swap");
console.log(JSON.stringify({
id: createdResponse.id,
onchainAmount: createdResponse.onchainAmount,
invoice: createdResponse.invoice,
}, undefined, 2));
console.log();
// Create a WebSocket and subscribe to updates for the created swap
const webSocket = new ws(`${endpoint.replace("http://", "ws://")}/v2/ws`);
webSocket.on("open", () => {
webSocket.send(
JSON.stringify({
op: "subscribe",
channel: "swap.update",
args: [createdResponse.id],
})
);
});
webSocket.on("message", async (rawMsg) => {
const msg = JSON.parse(rawMsg.toString("utf-8"));
if (msg.event !== "update") {
return;
}
console.log();
console.log("-----");
console.log("Got WebSocket update");
console.log(JSON.stringify(msg.args[0], undefined, 2));
console.log("-----");
console.log();
switch (msg.args[0].status) {
// "swap.created" means Boltz is waiting for the invoice to be paid
case "swap.created": {
console.log("Waiting invoice to be paid");
break;
}
// "transaction.mempool" means that Boltz send an onchain transaction
case "transaction.mempool": {
const boltzPublicKey = Buffer.from(
createdResponse.refundPublicKey,
"hex"
);
// Create a musig signing session and tweak it with the Taptree of the swap scripts
const musig = new Musig(await zkpInit.default(), keys, randomBytes(32), [
boltzPublicKey,
keys.publicKey,
]);
const tweakedKey = TaprootUtils.tweakMusig(
musig,
SwapTreeSerializer.deserializeSwapTree(createdResponse.swapTree).tree
);
// Parse the lockup transaction and find the output relevant for the swap
const lockupTx = Transaction.fromHex(msg.args[0].transaction.hex);
console.log(`Got lockup transaction: ${lockupTx.getId()}`);
const swapOutput = detectSwap(tweakedKey, lockupTx);
if (swapOutput === undefined) {
console.error("No swap output found in lockup transaction");
return;
}
console.log("Creating claim transaction");
// Create a claim transaction to be signed cooperatively via a key path spend
const claimTx = targetFee(2, (fee) =>
constructClaimTransaction(
[
{
...swapOutput,
keys,
preimage,
cooperative: true,
type: OutputType.Taproot,
txHash: lockupTx.getHash(),
},
],
address.toOutputScript(destinationAddress, network),
fee
)
);
// Get the partial signature from Boltz
const boltzSig = (
await axios.post(
`${endpoint}/v2/swap/reverse/${createdResponse.id}/claim`,
{
index: 0,
transaction: claimTx.toHex(),
preimage: preimage.toString("hex"),
pubNonce: Buffer.from(musig.getPublicNonce()).toString("hex"),
}
)
).data;
// Aggregate the nonces
musig.aggregateNonces([
[boltzPublicKey, Buffer.from(boltzSig.pubNonce, "hex")],
]);
// Initialize the session to sign the claim transaction
musig.initializeSession(
claimTx.hashForWitnessV1(
0,
[swapOutput.script],
[swapOutput.value],
Transaction.SIGHASH_DEFAULT
)
);
// Add the partial signature from Boltz
musig.addPartial(
boltzPublicKey,
Buffer.from(boltzSig.partialSignature, "hex")
);
// Create our partial signature
musig.signPartial();
// Witness of the input to the aggregated signature
claimTx.ins[0].witness = [musig.aggregatePartials()];
// Broadcast the finalized transaction
await axios.post(`${endpoint}/v2/chain/BTC/transaction`, {
hex: claimTx.toHex(),
});
break;
}
case "invoice.settled":
console.log();
console.log("Swap successful!");
webSocket.close();
break;
}
});
};
(async () => {
await reverseSwap();
})();