-
Notifications
You must be signed in to change notification settings - Fork 13
/
Copy pathindex.js
219 lines (195 loc) · 8.14 KB
/
index.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
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
const { getChannels, pay, signMessage, getChainBalance, getPendingChannels, openChannel, getNode, addPeer, getPeers, getChainFeeRate } = require('ln-service')
const lnurlClient = require('./clients/lnurl')
const bitfinexClient = require('./clients/bitfinex')
const axios = require('axios')
const config = require('./config.json')
const { lnd } = require('./lnd')
const PATHFINDING_TIMEOUT_MS = config.PATHFINDING_TIMEOUT_MS || 60 * 1000 // 1 minute
const DEEZY_PUBKEY = '024bfaf0cabe7f874fd33ebf7c6f4e5385971fc504ef3f492432e9e3ec77e1b5cf'
const CHAIN_BALANCE_BUFFER = 50000
async function tryPayInvoice({ invoice, paymentAmountSats, maxRouteFeePpm, outChannelIds }) {
const maxRouteFeeSats = Math.floor(maxRouteFeePpm * paymentAmountSats / 1000000)
console.log(`Using max route fee sats ${maxRouteFeeSats}`)
// Sometimes LND hangs too long when trying to pay, and we need to kill the process.
function abortMission() {
console.error("Payment timeout exceeded without terminating. Exiting!")
process.exit(1)
}
const paymentTimeout = setTimeout(abortMission, PATHFINDING_TIMEOUT_MS * 2)
const paymentResult = await pay(
{
lnd,
request: invoice,
outgoing_channels: outChannelIds,
max_fee: maxRouteFeeSats,
pathfinding_timeout: PATHFINDING_TIMEOUT_MS,
}
).catch(err => {
console.error(err)
console.log(`Failed to pay invoice ${invoice}`)
return null
})
clearTimeout(paymentTimeout)
if (!paymentResult || !paymentResult.confirmed_at) return
const feePpm = Math.round(paymentResult.safe_fee * 1000000 / paymentAmountSats)
console.log(`Payment confirmed, with fee ${paymentResult.safe_fee} satoshis, and ppm ${feePpm}`)
}
async function attemptPaymentToDestination({ destination, outChannelIds }) {
let invoice
const paymentAmountSats = destination.PAYMENT_AMOUNT_SATS
switch (destination.TYPE) {
case 'LNURL':
invoice = await lnurlClient.fetchInvoice({
lnUrlOrAddress: destination.LNURL_OR_ADDRESS,
paymentAmountSats
})
break;
case 'BITFINEX':
invoice = await bitfinexClient.fetchInvoice({
paymentAmountSats,
apiSecret: destination.API_SECRET,
apiKey: destination.API_KEY
})
break;
default:
console.error(`Unknown type ${destination.type}`)
return
}
if (!invoice) {
console.log('no invoice returned')
return
}
await tryPayInvoice({
invoice,
paymentAmountSats,
maxRouteFeePpm: destination.MAX_ROUTE_FEE_PPM,
outChannelIds
}).catch(err => {
console.error(err)
})
}
function isReadyToEarnAndClose({ channel }) {
return channel.local_balance * 1.0 / channel.capacity < (1 - config.CLOSE_WHEN_CHANNEL_EXCEEDS_RATIO)
}
async function earnAndClose({ channel }) {
const channelPoint = `${channel.transaction_id}:${channel.transaction_vout}`
console.log(`Requesting earn and close for deezy channel: ${channelPoint}`)
const message = `close ${channelPoint}`
const { signature } = await signMessage({ lnd, message }).catch(err => {
console.error(err)
return {}
})
if (!signature) return
const body = {
channel_point: channelPoint,
signature
}
const response = await axios.post(`https://api.deezy.io/v1/earn/closechannel`, body).catch(err => {
console.error(err)
return {}
})
console.log(response.data)
}
async function maybeOpenChannel({ localInitiatedDeezyChannels }) {
const currentLocalSats = localInitiatedDeezyChannels.reduce((acc, it) => acc + it.local_balance, 0)
const { pending_channels } = await getPendingChannels({ lnd })
const pendingOpenLocalSats = pending_channels.reduce((acc, it) => acc + it.local_balance, 0)
const totalLocalSats = currentLocalSats + pendingOpenLocalSats
console.log(`Total local open or pending sats: ${totalLocalSats}`)
if (totalLocalSats > (config.OPEN_CHANNEL_WHEN_LOCAL_SATS_BELOW || 0)) {
console.log(`Not opening channel, total local sats ${totalLocalSats} is above threshold ${config.OPEN_CHANNEL_WHEN_LOCAL_SATS_BELOW}`)
return
}
const chainBalance = (await getChainBalance({ lnd })).chain_balance
console.log(`Chain balance is ${chainBalance}`)
if (chainBalance < config.DEEZY_CHANNEL_SIZE_SATS + CHAIN_BALANCE_BUFFER) {
console.log(`Not opening channel, chain balance ${chainBalance} is below threshold ${config.DEEZY_CHANNEL_SIZE_SATS} plus buffer ${CHAIN_BALANCE_BUFFER}`)
return
}
console.log(`Opening channel with ${DEEZY_PUBKEY} for ${config.DEEZY_CHANNEL_SIZE_SATS} sats`)
const { tokens_per_vbyte } = await getChainFeeRate({ lnd }).catch(err => {
console.error(err)
return {}
})
if (!tokens_per_vbyte) return
const channelOpenFeeRate = config.MAX_CHANNEL_OPEN_FEE_SATS_PER_VBYTE ? Math.min(tokens_per_vbyte, config.MAX_CHANNEL_OPEN_FEE_SATS_PER_VBYTE) : tokens_per_vbyte
const { transaction_id, transaction_vout } = await openChannel({
lnd,
local_tokens: config.DEEZY_CHANNEL_SIZE_SATS,
partner_public_key: DEEZY_PUBKEY,
chain_fee_tokens_per_vbyte: channelOpenFeeRate,
is_private: config.PRIVATE_CHANNEL,
}).catch(err => {
console.error(err)
return {}
})
if (!transaction_id || !transaction_vout) return false
console.log(`Initiated channel with deezy, txid ${transaction_id}, vout ${transaction_vout}`)
return true
}
async function ensureConnectedToDeezy() {
const { peers } = await getPeers({ lnd })
const deezyPeer = peers.find(it => it.public_key === DEEZY_PUBKEY)
if (deezyPeer) {
console.log(`Already connected to deezy`)
return
}
console.log(`Connecting to deezy`)
const deezyNodeInfo = await getNode({ lnd, is_omitting_channels: true, public_key: DEEZY_PUBKEY })
await addPeer({ lnd, public_key: DEEZY_PUBKEY, socket: deezyNodeInfo.sockets[0].socket }).catch(err => {
console.error(err)
})
}
async function maybeAutoWithdraw({ destination }) {
if (destination.type !== 'BITFINEX') {
console.log(`AUTO_WITHDRAW is currently only enabled for BITFINEX destinations`)
return
}
await bitfinexClient.maybeAutoWithdraw({
apiKey: destination.API_KEY,
apiSecret: destination.API_SECRET,
address: destination.ON_CHAIN_WITHDRAWAL_ADDRESS,
minWithdrawalSats: destination.ON_CHAIN_WITHDRAWAL_TARGET_SIZE_SATS
})
}
async function run() {
await ensureConnectedToDeezy()
console.log(`Fetching channel info`)
const { channels } = await getChannels({
lnd,
partner_public_key: DEEZY_PUBKEY
}).catch(err => {
console.error(err)
return {}
})
if (!channels) return
const localInitiatedDeezyChannels = channels.filter(it => !it.is_partner_initiated)
console.log(`Found ${localInitiatedDeezyChannels.length} locally initiated channel(s) with deezy`)
console.log(`Checking if any deezy channels are ready to close`)
for (const channel of localInitiatedDeezyChannels) {
if (isReadyToEarnAndClose({ channel })) {
await earnAndClose({ channel })
// Terminate here if we are closing a channel.
console.log(`Attempted to earn and close channel, terminating here.`)
return
}
}
console.log(`Checking if we should open a channel to deezy`)
await maybeOpenChannel({ localInitiatedDeezyChannels })
const outChannelIds = localInitiatedDeezyChannels.map(it => it.id)
if (outChannelIds.length === 0) {
console.log(`No locally initiated channels to deezy currently open, terminating here`)
return
}
for (const destination of config.DESTINATIONS) {
await attemptPaymentToDestination({ destination, outChannelIds }).catch(err => {
console.error(err)
})
if (destination.AUTO_WITHDRAW) {
await maybeAutoWithdraw({ destination }).catch(err => {
console.error(err)
})
}
}
}
run()