admin swaps
This commit is contained in:
parent
07868a5a14
commit
a596e186fe
11 changed files with 817 additions and 665 deletions
|
|
@ -1,657 +0,0 @@
|
|||
import zkpInit from '@vulpemventures/secp256k1-zkp';
|
||||
import axios from 'axios';
|
||||
import { crypto, initEccLib, Transaction, address, Network } from 'bitcoinjs-lib';
|
||||
// import bolt11 from 'bolt11';
|
||||
import {
|
||||
Musig, SwapTreeSerializer, TaprootUtils, detectSwap,
|
||||
constructClaimTransaction, targetFee, OutputType,
|
||||
Networks,
|
||||
} from 'boltz-core';
|
||||
import { randomBytes, createHash } from 'crypto';
|
||||
import { ECPairFactory, ECPairInterface } from 'ecpair';
|
||||
import * as ecc from 'tiny-secp256k1';
|
||||
import ws from 'ws';
|
||||
import { getLogger, PubLogger, ERROR } from '../helpers/logger.js';
|
||||
import SettingsManager from '../main/settingsManager.js';
|
||||
import * as Types from '../../../proto/autogenerated/ts/types.js';
|
||||
import { BTCNetwork } from '../main/settings.js';
|
||||
import Storage from '../storage/index.js';
|
||||
import LND from './lnd.js';
|
||||
import { UserInvoicePayment } from '../storage/entity/UserInvoicePayment.js';
|
||||
type InvoiceSwapResponse = { id: string, claimPublicKey: string, swapTree: string }
|
||||
type InvoiceSwapInfo = { paymentHash: string, keys: ECPairInterface }
|
||||
type InvoiceSwapData = { createdResponse: InvoiceSwapResponse, info: InvoiceSwapInfo }
|
||||
|
||||
type TransactionSwapFees = {
|
||||
percentage: number,
|
||||
minerFees: {
|
||||
claim: number,
|
||||
lockup: number,
|
||||
}
|
||||
}
|
||||
|
||||
type TransactionSwapFeesRes = {
|
||||
BTC?: {
|
||||
BTC?: {
|
||||
fees: TransactionSwapFees
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
type TransactionSwapResponse = {
|
||||
id: string, refundPublicKey: string, swapTree: string,
|
||||
timeoutBlockHeight: number, lockupAddress: string, invoice: string,
|
||||
onchainAmount?: number
|
||||
}
|
||||
type TransactionSwapInfo = { destinationAddress: string, preimage: Buffer, keys: ECPairInterface, chainFee: number }
|
||||
export type TransactionSwapData = { createdResponse: TransactionSwapResponse, info: TransactionSwapInfo }
|
||||
export class Swaps {
|
||||
settings: SettingsManager
|
||||
revSwappers: Record<string, ReverseSwaps>
|
||||
// submarineSwaps: SubmarineSwaps
|
||||
storage: Storage
|
||||
lnd: LND
|
||||
log = getLogger({ component: 'swaps' })
|
||||
constructor(settings: SettingsManager, storage: Storage) {
|
||||
this.settings = settings
|
||||
this.revSwappers = {}
|
||||
const network = settings.getSettings().lndSettings.network
|
||||
const { boltzHttpUrl, boltzWebSocketUrl, boltsHttpUrlAlt, boltsWebSocketUrlAlt } = settings.getSettings().swapsSettings
|
||||
if (boltzHttpUrl && boltzWebSocketUrl) {
|
||||
this.revSwappers[boltzHttpUrl] = new ReverseSwaps({ httpUrl: boltzHttpUrl, wsUrl: boltzWebSocketUrl, network })
|
||||
}
|
||||
if (boltsHttpUrlAlt && boltsWebSocketUrlAlt) {
|
||||
this.revSwappers[boltsHttpUrlAlt] = new ReverseSwaps({ httpUrl: boltsHttpUrlAlt, wsUrl: boltsWebSocketUrlAlt, network })
|
||||
}
|
||||
this.storage = storage
|
||||
}
|
||||
|
||||
SetLnd = (lnd: LND) => {
|
||||
this.lnd = lnd
|
||||
}
|
||||
|
||||
Stop = () => { }
|
||||
|
||||
GetKeys = (privateKey: string) => {
|
||||
const keys = ECPairFactory(ecc).fromPrivateKey(Buffer.from(privateKey, 'hex'))
|
||||
return keys
|
||||
}
|
||||
|
||||
ListSwaps = async (appUserId: string, payments: UserInvoicePayment[], newOp: (p: UserInvoicePayment) => Types.UserOperation | undefined, getServiceFee: (amt: number) => number): Promise<Types.SwapsList> => {
|
||||
const completedSwaps = await this.storage.paymentStorage.ListCompletedSwaps(appUserId, payments)
|
||||
const pendingSwaps = await this.storage.paymentStorage.ListPendingTransactionSwaps(appUserId)
|
||||
return {
|
||||
swaps: completedSwaps.map(s => {
|
||||
const p = s.payment
|
||||
const op = p ? newOp(p) : undefined
|
||||
return {
|
||||
operation_payment: op,
|
||||
swap_operation_id: s.swap.swap_operation_id,
|
||||
address_paid: s.swap.address_paid,
|
||||
failure_reason: s.swap.failure_reason,
|
||||
}
|
||||
}),
|
||||
quotes: pendingSwaps.map(s => {
|
||||
const serviceFee = getServiceFee(s.invoice_amount)
|
||||
return {
|
||||
swap_operation_id: s.swap_operation_id,
|
||||
invoice_amount_sats: s.invoice_amount,
|
||||
transaction_amount_sats: s.transaction_amount,
|
||||
chain_fee_sats: s.chain_fee_sats,
|
||||
service_fee_sats: serviceFee,
|
||||
swap_fee_sats: s.swap_fee_sats,
|
||||
service_url: s.service_url,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
GetTxSwapQuotes = async (appUserId: string, amt: number, getServiceFee: (decodedAmt: number) => number): Promise<Types.TransactionSwapQuote[]> => {
|
||||
if (!this.settings.getSettings().swapsSettings.enableSwaps) {
|
||||
throw new Error("Swaps are not enabled")
|
||||
}
|
||||
const swappers = Object.values(this.revSwappers)
|
||||
if (swappers.length === 0) {
|
||||
throw new Error("No swap services available")
|
||||
}
|
||||
const res = await Promise.allSettled(swappers.map(sw => this.getTxSwapQuote(sw, appUserId, amt, getServiceFee)))
|
||||
const failures: string[] = []
|
||||
const success: Types.TransactionSwapQuote[] = []
|
||||
for (const r of res) {
|
||||
if (r.status === 'fulfilled') {
|
||||
success.push(r.value)
|
||||
} else {
|
||||
failures.push(r.reason.message ? r.reason.message : r.reason.toString())
|
||||
}
|
||||
}
|
||||
if (success.length === 0) {
|
||||
throw new Error(failures.join("\n"))
|
||||
}
|
||||
return success
|
||||
}
|
||||
|
||||
private async getTxSwapQuote(swapper: ReverseSwaps, appUserId: string, amt: number, getServiceFee: (decodedAmt: number) => number): Promise<Types.TransactionSwapQuote> {
|
||||
this.log("getting transaction swap quote")
|
||||
const feesRes = await swapper.GetFees()
|
||||
if (!feesRes.ok) {
|
||||
throw new Error(feesRes.error)
|
||||
}
|
||||
const { claim, lockup } = feesRes.fees.minerFees
|
||||
const minerFee = claim + lockup
|
||||
const chainTotal = amt + minerFee
|
||||
const res = await swapper.SwapTransaction(chainTotal)
|
||||
if (!res.ok) {
|
||||
throw new Error(res.error)
|
||||
}
|
||||
const decoded = await this.lnd.DecodeInvoice(res.createdResponse.invoice)
|
||||
const swapFee = decoded.numSatoshis - chainTotal
|
||||
const serviceFee = getServiceFee(decoded.numSatoshis)
|
||||
const newSwap = await this.storage.paymentStorage.AddTransactionSwap({
|
||||
app_user_id: appUserId,
|
||||
swap_quote_id: res.createdResponse.id,
|
||||
swap_tree: JSON.stringify(res.createdResponse.swapTree),
|
||||
lockup_address: res.createdResponse.lockupAddress,
|
||||
refund_public_key: res.createdResponse.refundPublicKey,
|
||||
timeout_block_height: res.createdResponse.timeoutBlockHeight,
|
||||
invoice: res.createdResponse.invoice,
|
||||
invoice_amount: decoded.numSatoshis,
|
||||
transaction_amount: chainTotal,
|
||||
swap_fee_sats: swapFee,
|
||||
chain_fee_sats: minerFee,
|
||||
preimage: res.preimage,
|
||||
ephemeral_private_key: res.privKey,
|
||||
ephemeral_public_key: res.pubkey,
|
||||
service_url: swapper.getHttpUrl(),
|
||||
})
|
||||
return {
|
||||
swap_operation_id: newSwap.swap_operation_id,
|
||||
swap_fee_sats: swapFee,
|
||||
invoice_amount_sats: decoded.numSatoshis,
|
||||
transaction_amount_sats: amt,
|
||||
chain_fee_sats: minerFee,
|
||||
service_fee_sats: serviceFee,
|
||||
service_url: swapper.getHttpUrl(),
|
||||
}
|
||||
}
|
||||
|
||||
async PayAddrWithSwap(appUserId: string, swapOpId: string, address: string, payInvoice: (invoice: string, amt: number) => Promise<void>) {
|
||||
if (!this.settings.getSettings().swapsSettings.enableSwaps) {
|
||||
throw new Error("Swaps are not enabled")
|
||||
}
|
||||
this.log("paying address with swap", { appUserId, swapOpId, address })
|
||||
if (!swapOpId) {
|
||||
throw new Error("request a swap quote before paying an external address")
|
||||
}
|
||||
const txSwap = await this.storage.paymentStorage.GetTransactionSwap(swapOpId, appUserId)
|
||||
if (!txSwap) {
|
||||
throw new Error("swap quote not found")
|
||||
}
|
||||
const info = await this.lnd.GetInfo()
|
||||
if (info.blockHeight >= txSwap.timeout_block_height) {
|
||||
throw new Error("swap timeout")
|
||||
}
|
||||
const swapper = this.revSwappers[txSwap.service_url]
|
||||
if (!swapper) {
|
||||
throw new Error("swapper service not found")
|
||||
}
|
||||
const keys = this.GetKeys(txSwap.ephemeral_private_key)
|
||||
const data: TransactionSwapData = {
|
||||
createdResponse: {
|
||||
id: txSwap.swap_quote_id,
|
||||
invoice: txSwap.invoice,
|
||||
lockupAddress: txSwap.lockup_address,
|
||||
refundPublicKey: txSwap.refund_public_key,
|
||||
swapTree: txSwap.swap_tree,
|
||||
timeoutBlockHeight: txSwap.timeout_block_height,
|
||||
onchainAmount: txSwap.transaction_amount,
|
||||
},
|
||||
info: {
|
||||
destinationAddress: address,
|
||||
keys,
|
||||
chainFee: txSwap.chain_fee_sats,
|
||||
preimage: Buffer.from(txSwap.preimage, 'hex'),
|
||||
}
|
||||
}
|
||||
// the swap and the invoice payment are linked, swap will not start until the invoice payment is started, and will not complete once the invoice payment is completed
|
||||
let swapResult = { ok: false, error: "swap never completed" } as { ok: true, txId: string } | { ok: false, error: string }
|
||||
swapper.SubscribeToTransactionSwap(data, result => {
|
||||
swapResult = result
|
||||
})
|
||||
try {
|
||||
await payInvoice(txSwap.invoice, txSwap.invoice_amount)
|
||||
if (!swapResult.ok) {
|
||||
this.log("invoice payment successful, but swap failed")
|
||||
await this.storage.paymentStorage.FailTransactionSwap(swapOpId, address, swapResult.error)
|
||||
throw new Error(swapResult.error)
|
||||
}
|
||||
this.log("swap completed successfully")
|
||||
await this.storage.paymentStorage.FinalizeTransactionSwap(swapOpId, address, swapResult.txId)
|
||||
} catch (err: any) {
|
||||
if (swapResult.ok) {
|
||||
this.log("failed to pay swap invoice, but swap completed successfully", swapResult.txId)
|
||||
await this.storage.paymentStorage.FailTransactionSwap(swapOpId, address, err.message)
|
||||
} else {
|
||||
this.log("failed to pay swap invoice and swap failed", swapResult.error)
|
||||
await this.storage.paymentStorage.FailTransactionSwap(swapOpId, address, swapResult.error)
|
||||
}
|
||||
throw err
|
||||
}
|
||||
const networkFeesTotal = txSwap.chain_fee_sats + txSwap.swap_fee_sats
|
||||
return {
|
||||
txId: swapResult.txId,
|
||||
network_fee: networkFeesTotal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export class ReverseSwaps {
|
||||
// settings: SettingsManager
|
||||
private httpUrl: string
|
||||
private wsUrl: string
|
||||
log: PubLogger
|
||||
private network: BTCNetwork
|
||||
constructor({ httpUrl, wsUrl, network }: { httpUrl: string, wsUrl: string, network: BTCNetwork }) {
|
||||
this.httpUrl = httpUrl
|
||||
this.wsUrl = wsUrl
|
||||
this.network = network
|
||||
this.log = getLogger({ component: 'ReverseSwaps' })
|
||||
initEccLib(ecc)
|
||||
}
|
||||
|
||||
getHttpUrl = () => {
|
||||
return this.httpUrl
|
||||
}
|
||||
getWsUrl = () => {
|
||||
return this.wsUrl
|
||||
}
|
||||
|
||||
calculateFees = (fees: TransactionSwapFees, receiveAmount: number) => {
|
||||
const pct = fees.percentage / 100
|
||||
const minerFee = fees.minerFees.claim + fees.minerFees.lockup
|
||||
|
||||
const preFee = receiveAmount + minerFee
|
||||
const fee = Math.ceil(preFee * pct)
|
||||
const total = preFee + fee
|
||||
return { total, fee, minerFee }
|
||||
}
|
||||
|
||||
GetFees = async (): Promise<{ ok: true, fees: TransactionSwapFees, } | { ok: false, error: string }> => {
|
||||
const url = `${this.httpUrl}/v2/swap/reverse`
|
||||
const feesRes = await loggedGet<TransactionSwapFeesRes>(this.log, url)
|
||||
if (!feesRes.ok) {
|
||||
return { ok: false, error: feesRes.error }
|
||||
}
|
||||
if (!feesRes.data.BTC?.BTC?.fees) {
|
||||
return { ok: false, error: 'No fees found for BTC to BTC swap' }
|
||||
}
|
||||
|
||||
return { ok: true, fees: feesRes.data.BTC.BTC.fees }
|
||||
}
|
||||
|
||||
SwapTransaction = async (txAmount: number): Promise<{ ok: true, createdResponse: TransactionSwapResponse, preimage: string, pubkey: string, privKey: string } | { ok: false, error: string }> => {
|
||||
const preimage = randomBytes(32);
|
||||
const keys = ECPairFactory(ecc).makeRandom()
|
||||
if (!keys.privateKey) {
|
||||
return { ok: false, error: 'Failed to generate keys' }
|
||||
}
|
||||
const url = `${this.httpUrl}/v2/swap/reverse`
|
||||
const req: any = {
|
||||
onchainAmount: txAmount,
|
||||
to: 'BTC',
|
||||
from: 'BTC',
|
||||
claimPublicKey: Buffer.from(keys.publicKey).toString('hex'),
|
||||
preimageHash: createHash('sha256').update(preimage).digest('hex'),
|
||||
}
|
||||
const createdResponseRes = await loggedPost<TransactionSwapResponse>(this.log, url, req)
|
||||
if (!createdResponseRes.ok) {
|
||||
return createdResponseRes
|
||||
}
|
||||
const createdResponse = createdResponseRes.data
|
||||
this.log('Created transaction swap');
|
||||
this.log(createdResponse);
|
||||
return {
|
||||
ok: true, createdResponse,
|
||||
preimage: Buffer.from(preimage).toString('hex'),
|
||||
pubkey: Buffer.from(keys.publicKey).toString('hex'),
|
||||
privKey: Buffer.from(keys.privateKey).toString('hex')
|
||||
}
|
||||
}
|
||||
|
||||
SubscribeToTransactionSwap = async (data: TransactionSwapData, swapDone: (result: { ok: true, txId: string } | { ok: false, error: string }) => void) => {
|
||||
const webSocket = new ws(`${this.wsUrl}/v2/ws`)
|
||||
const subReq = { op: 'subscribe', channel: 'swap.update', args: [data.createdResponse.id] }
|
||||
webSocket.on('open', () => {
|
||||
webSocket.send(JSON.stringify(subReq))
|
||||
})
|
||||
let txId = "", isDone = false
|
||||
const done = () => {
|
||||
isDone = true
|
||||
webSocket.close()
|
||||
swapDone({ ok: true, txId })
|
||||
}
|
||||
webSocket.on('error', (err) => {
|
||||
this.log(ERROR, 'Error in WebSocket', err.message)
|
||||
})
|
||||
webSocket.on('close', () => {
|
||||
if (!isDone) {
|
||||
this.log(ERROR, 'WebSocket closed before swap was done');
|
||||
swapDone({ ok: false, error: 'WebSocket closed before swap was done' })
|
||||
}
|
||||
})
|
||||
webSocket.on('message', async (rawMsg) => {
|
||||
try {
|
||||
const result = await this.handleSwapTransactionMessage(rawMsg, data, done)
|
||||
if (result) {
|
||||
txId = result
|
||||
}
|
||||
} catch (err: any) {
|
||||
this.log(ERROR, 'Error handling transaction WebSocket message', err.message)
|
||||
isDone = true
|
||||
webSocket.close()
|
||||
swapDone({ ok: false, error: err.message })
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
handleSwapTransactionMessage = async (rawMsg: ws.RawData, data: TransactionSwapData, done: () => void) => {
|
||||
const msg = JSON.parse(rawMsg.toString('utf-8'));
|
||||
if (msg.event !== 'update') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.log('Got WebSocket update');
|
||||
this.log(msg);
|
||||
switch (msg.args[0].status) {
|
||||
// "swap.created" means Boltz is waiting for the invoice to be paid
|
||||
case 'swap.created':
|
||||
this.log('Waiting invoice to be paid');
|
||||
return;
|
||||
|
||||
// "transaction.mempool" means that Boltz sent an onchain transaction
|
||||
case 'transaction.mempool':
|
||||
const txIdRes = await this.handleTransactionMempool(data, msg.args[0].transaction.hex)
|
||||
if (!txIdRes.ok) {
|
||||
throw new Error(txIdRes.error)
|
||||
}
|
||||
return txIdRes.txId
|
||||
case 'invoice.settled':
|
||||
this.log('Transaction swap successful');
|
||||
done()
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
handleTransactionMempool = async (data: TransactionSwapData, txHex: string): Promise<{ ok: true, txId: string } | { ok: false, error: string }> => {
|
||||
this.log('Creating claim transaction');
|
||||
const { createdResponse, info } = data
|
||||
const { destinationAddress, keys, preimage, chainFee } = info
|
||||
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(), keys, randomBytes(32), [
|
||||
boltzPublicKey,
|
||||
Buffer.from(keys.publicKey),
|
||||
]);
|
||||
const tweakedKey = TaprootUtils.tweakMusig(
|
||||
musig,
|
||||
// swap tree can either be a string or an object
|
||||
SwapTreeSerializer.deserializeSwapTree(createdResponse.swapTree).tree,
|
||||
);
|
||||
|
||||
// Parse the lockup transaction and find the output relevant for the swap
|
||||
const lockupTx = Transaction.fromHex(txHex);
|
||||
const swapOutput = detectSwap(tweakedKey, lockupTx);
|
||||
if (swapOutput === undefined) {
|
||||
this.log(ERROR, 'No swap output found in lockup transaction');
|
||||
return { ok: false, error: 'No swap output found in lockup transaction' }
|
||||
}
|
||||
const network = getNetwork(this.network)
|
||||
// Create a claim transaction to be signed cooperatively via a key path spend
|
||||
const claimTx = constructClaimTransaction(
|
||||
[
|
||||
{
|
||||
...swapOutput,
|
||||
keys,
|
||||
preimage,
|
||||
cooperative: true,
|
||||
type: OutputType.Taproot,
|
||||
txHash: lockupTx.getHash(),
|
||||
},
|
||||
],
|
||||
address.toOutputScript(destinationAddress, network),
|
||||
chainFee,
|
||||
)
|
||||
// Get the partial signature from Boltz
|
||||
const claimUrl = `${this.httpUrl}/v2/swap/reverse/${createdResponse.id}/claim`
|
||||
const claimReq = {
|
||||
index: 0,
|
||||
transaction: claimTx.toHex(),
|
||||
preimage: preimage.toString('hex'),
|
||||
pubNonce: Buffer.from(musig.getPublicNonce()).toString('hex'),
|
||||
}
|
||||
const boltzSigRes = await loggedPost<{ pubNonce: string, partialSignature: string }>(this.log, claimUrl, claimReq)
|
||||
if (!boltzSigRes.ok) {
|
||||
return boltzSigRes
|
||||
}
|
||||
const boltzSig = boltzSigRes.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
|
||||
const broadcastUrl = `${this.httpUrl}/v2/chain/BTC/transaction`
|
||||
const broadcastReq = {
|
||||
hex: claimTx.toHex(),
|
||||
}
|
||||
|
||||
const broadcastResponse = await loggedPost<any>(this.log, broadcastUrl, broadcastReq)
|
||||
if (!broadcastResponse.ok) {
|
||||
return broadcastResponse
|
||||
}
|
||||
this.log('Transaction broadcasted', broadcastResponse.data)
|
||||
const txId = claimTx.getId()
|
||||
return { ok: true, txId }
|
||||
}
|
||||
}
|
||||
|
||||
const loggedPost = async <T>(log: PubLogger, url: string, req: any): Promise<{ ok: true, data: T } | { ok: false, error: string }> => {
|
||||
try {
|
||||
const { data } = await axios.post(url, req)
|
||||
return { ok: true, data: data as T }
|
||||
} catch (err: any) {
|
||||
if (err.response?.data) {
|
||||
log(ERROR, 'Error sending request', err.response.data)
|
||||
return { ok: false, error: JSON.stringify(err.response.data) }
|
||||
}
|
||||
log(ERROR, 'Error sending request', err.message)
|
||||
return { ok: false, error: err.message }
|
||||
}
|
||||
}
|
||||
|
||||
const loggedGet = async <T>(log: PubLogger, url: string): Promise<{ ok: true, data: T } | { ok: false, error: string }> => {
|
||||
try {
|
||||
const { data } = await axios.get(url)
|
||||
return { ok: true, data: data as T }
|
||||
} catch (err: any) {
|
||||
if (err.response?.data) {
|
||||
log(ERROR, 'Error getting request', err.response.data)
|
||||
return { ok: false, error: err.response.data }
|
||||
}
|
||||
log(ERROR, 'Error getting request', err.message)
|
||||
return { ok: false, error: err.message }
|
||||
}
|
||||
}
|
||||
|
||||
const getNetwork = (network: BTCNetwork): Network => {
|
||||
switch (network) {
|
||||
case 'mainnet':
|
||||
return Networks.bitcoinMainnet
|
||||
case 'testnet':
|
||||
return Networks.bitcoinTestnet
|
||||
case 'regtest':
|
||||
return Networks.bitcoinRegtest
|
||||
default:
|
||||
throw new Error(`Invalid network: ${network}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Submarine swaps currently not supported, keeping the code for future reference
|
||||
/*
|
||||
export class SubmarineSwaps {
|
||||
settings: SettingsManager
|
||||
log: PubLogger
|
||||
constructor(settings: SettingsManager) {
|
||||
this.settings = settings
|
||||
this.log = getLogger({ component: 'SubmarineSwaps' })
|
||||
}
|
||||
|
||||
SwapInvoice = async (invoice: string, paymentHash: string) => {
|
||||
if (!this.settings.getSettings().swapsSettings.enableSwaps) {
|
||||
this.log(ERROR, 'Swaps are not enabled');
|
||||
return;
|
||||
}
|
||||
const keys = ECPairFactory(ecc).makeRandom()
|
||||
const refundPublicKey = Buffer.from(keys.publicKey).toString('hex')
|
||||
const req = { invoice, to: 'BTC', from: 'BTC', refundPublicKey }
|
||||
const url = `${this.settings.getSettings().swapsSettings.boltzHttpUrl}/v2/swap/submarine`
|
||||
this.log('Sending invoice swap request to', url);
|
||||
const createdResponseRes = await loggedPost<InvoiceSwapResponse>(this.log, url, req)
|
||||
if (!createdResponseRes.ok) {
|
||||
return createdResponseRes
|
||||
}
|
||||
const createdResponse = createdResponseRes.data
|
||||
this.log('Created invoice swap');
|
||||
this.log(createdResponse);
|
||||
|
||||
const webSocket = new ws(`${this.settings.getSettings().swapsSettings.boltzWebSocketUrl}/v2/ws`)
|
||||
const subReq = { op: 'subscribe', channel: 'swap.update', args: [createdResponse.id] }
|
||||
webSocket.on('open', () => {
|
||||
webSocket.send(JSON.stringify(subReq))
|
||||
})
|
||||
|
||||
webSocket.on('message', async (rawMsg) => {
|
||||
try {
|
||||
await this.handleSwapInvoiceMessage(rawMsg, { createdResponse, info: { paymentHash, keys } }, () => webSocket.close())
|
||||
} catch (err: any) {
|
||||
this.log(ERROR, 'Error handling invoice WebSocket message', err.message)
|
||||
webSocket.close()
|
||||
return
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleSwapInvoiceMessage = async (rawMsg: ws.RawData, data: InvoiceSwapData, closeWebSocket: () => void) => {
|
||||
const msg = JSON.parse(rawMsg.toString('utf-8'));
|
||||
if (msg.event !== 'update') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.log('Got invoice WebSocket update');
|
||||
this.log(msg);
|
||||
switch (msg.args[0].status) {
|
||||
// "invoice.set" means Boltz is waiting for an onchain transaction to be sent
|
||||
case 'invoice.set':
|
||||
this.log('Waiting for onchain transaction');
|
||||
return;
|
||||
// Create a partial signature to allow Boltz to do a key path spend to claim the mainchain coins
|
||||
case 'transaction.claim.pending':
|
||||
await this.handleInvoiceClaimPending(data)
|
||||
return;
|
||||
|
||||
case 'transaction.claimed':
|
||||
this.log('Invoice swap successful');
|
||||
closeWebSocket()
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
handleInvoiceClaimPending = async (data: InvoiceSwapData) => {
|
||||
this.log('Creating cooperative claim transaction');
|
||||
const { createdResponse, info } = data
|
||||
const { paymentHash, keys } = info
|
||||
const { boltzHttpUrl } = this.settings.getSettings().swapsSettings
|
||||
// Get the information request to create a partial signature
|
||||
const url = `${boltzHttpUrl}/v2/swap/submarine/${createdResponse.id}/claim`
|
||||
const claimTxDetailsRes = await loggedGet<{ preimage: string, transactionHash: string, pubNonce: string }>(this.log, url)
|
||||
if (!claimTxDetailsRes.ok) {
|
||||
return claimTxDetailsRes
|
||||
}
|
||||
const claimTxDetails = claimTxDetailsRes.data
|
||||
// Verify that Boltz actually paid the invoice by comparing the preimage hash
|
||||
// of the invoice to the SHA256 hash of the preimage from the response
|
||||
const claimTxPreimageHash = createHash('sha256').update(Buffer.from(claimTxDetails.preimage, 'hex')).digest()
|
||||
const invoicePreimageHash = Buffer.from(paymentHash, 'hex')
|
||||
|
||||
if (!claimTxPreimageHash.equals(invoicePreimageHash)) {
|
||||
this.log(ERROR, 'Boltz provided invalid preimage');
|
||||
return;
|
||||
}
|
||||
|
||||
const boltzPublicKey = Buffer.from(createdResponse.claimPublicKey, 'hex')
|
||||
|
||||
// Create a musig signing instance
|
||||
const musig = new Musig(await zkpInit(), keys, randomBytes(32), [
|
||||
boltzPublicKey,
|
||||
Buffer.from(keys.publicKey),
|
||||
]);
|
||||
// Tweak that musig with the Taptree of the swap scripts
|
||||
TaprootUtils.tweakMusig(
|
||||
musig,
|
||||
SwapTreeSerializer.deserializeSwapTree(createdResponse.swapTree).tree,
|
||||
);
|
||||
|
||||
// Aggregate the nonces
|
||||
musig.aggregateNonces([
|
||||
[boltzPublicKey, Buffer.from(claimTxDetails.pubNonce, 'hex')],
|
||||
]);
|
||||
// Initialize the session to sign the transaction hash from the response
|
||||
musig.initializeSession(
|
||||
Buffer.from(claimTxDetails.transactionHash, 'hex'),
|
||||
);
|
||||
|
||||
// Give our public nonce and the partial signature to Boltz
|
||||
const claimUrl = `${boltzHttpUrl}/v2/swap/submarine/${createdResponse.id}/claim`
|
||||
const claimReq = {
|
||||
pubNonce: Buffer.from(musig.getPublicNonce()).toString('hex'),
|
||||
partialSignature: Buffer.from(musig.signPartial()).toString('hex'),
|
||||
}
|
||||
const claimResponseRes = await loggedPost<{ pubNonce: string, partialSignature: string }>(this.log, claimUrl, claimReq)
|
||||
if (!claimResponseRes.ok) {
|
||||
return claimResponseRes
|
||||
}
|
||||
const claimResponse = claimResponseRes.data
|
||||
this.log('Claim response', claimResponse)
|
||||
}
|
||||
}
|
||||
*/
|
||||
280
src/services/lnd/swaps/reverseSwaps.ts
Normal file
280
src/services/lnd/swaps/reverseSwaps.ts
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
import zkpInit from '@vulpemventures/secp256k1-zkp';
|
||||
import { initEccLib, Transaction, address } from 'bitcoinjs-lib';
|
||||
// import bolt11 from 'bolt11';
|
||||
import {
|
||||
Musig, SwapTreeSerializer, TaprootUtils, detectSwap,
|
||||
constructClaimTransaction, OutputType,
|
||||
} from 'boltz-core';
|
||||
import { randomBytes, createHash } from 'crypto';
|
||||
import { ECPairFactory, ECPairInterface } from 'ecpair';
|
||||
import * as ecc from 'tiny-secp256k1';
|
||||
import ws from 'ws';
|
||||
import { getLogger, PubLogger, ERROR } from '../../helpers/logger.js';
|
||||
import { BTCNetwork } from '../../main/settings.js';
|
||||
import { loggedGet, loggedPost, getNetwork } from './swapHelpers.js';
|
||||
|
||||
|
||||
type TransactionSwapFees = {
|
||||
percentage: number,
|
||||
minerFees: {
|
||||
claim: number,
|
||||
lockup: number,
|
||||
}
|
||||
}
|
||||
|
||||
type TransactionSwapFeesRes = {
|
||||
BTC?: {
|
||||
BTC?: {
|
||||
fees: TransactionSwapFees
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
type TransactionSwapResponse = {
|
||||
id: string, refundPublicKey: string, swapTree: string,
|
||||
timeoutBlockHeight: number, lockupAddress: string, invoice: string,
|
||||
onchainAmount?: number
|
||||
}
|
||||
type TransactionSwapInfo = { destinationAddress: string, preimage: Buffer, keys: ECPairInterface, chainFee: number }
|
||||
export type TransactionSwapData = { createdResponse: TransactionSwapResponse, info: TransactionSwapInfo }
|
||||
|
||||
|
||||
|
||||
export class ReverseSwaps {
|
||||
// settings: SettingsManager
|
||||
private httpUrl: string
|
||||
private wsUrl: string
|
||||
log: PubLogger
|
||||
private network: BTCNetwork
|
||||
constructor({ httpUrl, wsUrl, network }: { httpUrl: string, wsUrl: string, network: BTCNetwork }) {
|
||||
this.httpUrl = httpUrl
|
||||
this.wsUrl = wsUrl
|
||||
this.network = network
|
||||
this.log = getLogger({ component: 'ReverseSwaps' })
|
||||
initEccLib(ecc)
|
||||
}
|
||||
|
||||
getHttpUrl = () => {
|
||||
return this.httpUrl
|
||||
}
|
||||
getWsUrl = () => {
|
||||
return this.wsUrl
|
||||
}
|
||||
|
||||
calculateFees = (fees: TransactionSwapFees, receiveAmount: number) => {
|
||||
const pct = fees.percentage / 100
|
||||
const minerFee = fees.minerFees.claim + fees.minerFees.lockup
|
||||
|
||||
const preFee = receiveAmount + minerFee
|
||||
const fee = Math.ceil(preFee * pct)
|
||||
const total = preFee + fee
|
||||
return { total, fee, minerFee }
|
||||
}
|
||||
|
||||
GetFees = async (): Promise<{ ok: true, fees: TransactionSwapFees, } | { ok: false, error: string }> => {
|
||||
const url = `${this.httpUrl}/v2/swap/reverse`
|
||||
const feesRes = await loggedGet<TransactionSwapFeesRes>(this.log, url)
|
||||
if (!feesRes.ok) {
|
||||
return { ok: false, error: feesRes.error }
|
||||
}
|
||||
if (!feesRes.data.BTC?.BTC?.fees) {
|
||||
return { ok: false, error: 'No fees found for BTC to BTC swap' }
|
||||
}
|
||||
|
||||
return { ok: true, fees: feesRes.data.BTC.BTC.fees }
|
||||
}
|
||||
|
||||
SwapTransaction = async (txAmount: number): Promise<{ ok: true, createdResponse: TransactionSwapResponse, preimage: string, pubkey: string, privKey: string } | { ok: false, error: string }> => {
|
||||
const preimage = randomBytes(32);
|
||||
const keys = ECPairFactory(ecc).makeRandom()
|
||||
if (!keys.privateKey) {
|
||||
return { ok: false, error: 'Failed to generate keys' }
|
||||
}
|
||||
const url = `${this.httpUrl}/v2/swap/reverse`
|
||||
const req: any = {
|
||||
onchainAmount: txAmount,
|
||||
to: 'BTC',
|
||||
from: 'BTC',
|
||||
claimPublicKey: Buffer.from(keys.publicKey).toString('hex'),
|
||||
preimageHash: createHash('sha256').update(preimage).digest('hex'),
|
||||
}
|
||||
const createdResponseRes = await loggedPost<TransactionSwapResponse>(this.log, url, req)
|
||||
if (!createdResponseRes.ok) {
|
||||
return createdResponseRes
|
||||
}
|
||||
const createdResponse = createdResponseRes.data
|
||||
this.log('Created transaction swap');
|
||||
this.log(createdResponse);
|
||||
return {
|
||||
ok: true, createdResponse,
|
||||
preimage: Buffer.from(preimage).toString('hex'),
|
||||
pubkey: Buffer.from(keys.publicKey).toString('hex'),
|
||||
privKey: Buffer.from(keys.privateKey).toString('hex')
|
||||
}
|
||||
}
|
||||
|
||||
SubscribeToTransactionSwap = async (data: TransactionSwapData, swapDone: (result: { ok: true, txId: string } | { ok: false, error: string }) => void) => {
|
||||
const webSocket = new ws(`${this.wsUrl}/v2/ws`)
|
||||
const subReq = { op: 'subscribe', channel: 'swap.update', args: [data.createdResponse.id] }
|
||||
webSocket.on('open', () => {
|
||||
webSocket.send(JSON.stringify(subReq))
|
||||
})
|
||||
let txId = "", isDone = false
|
||||
const done = () => {
|
||||
isDone = true
|
||||
webSocket.close()
|
||||
swapDone({ ok: true, txId })
|
||||
}
|
||||
webSocket.on('error', (err) => {
|
||||
this.log(ERROR, 'Error in WebSocket', err.message)
|
||||
})
|
||||
webSocket.on('close', () => {
|
||||
if (!isDone) {
|
||||
this.log(ERROR, 'WebSocket closed before swap was done');
|
||||
swapDone({ ok: false, error: 'WebSocket closed before swap was done' })
|
||||
}
|
||||
})
|
||||
webSocket.on('message', async (rawMsg) => {
|
||||
try {
|
||||
const result = await this.handleSwapTransactionMessage(rawMsg, data, done)
|
||||
if (result) {
|
||||
txId = result
|
||||
}
|
||||
} catch (err: any) {
|
||||
this.log(ERROR, 'Error handling transaction WebSocket message', err.message)
|
||||
isDone = true
|
||||
webSocket.close()
|
||||
swapDone({ ok: false, error: err.message })
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
handleSwapTransactionMessage = async (rawMsg: ws.RawData, data: TransactionSwapData, done: () => void) => {
|
||||
const msg = JSON.parse(rawMsg.toString('utf-8'));
|
||||
if (msg.event !== 'update') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.log('Got WebSocket update');
|
||||
this.log(msg);
|
||||
switch (msg.args[0].status) {
|
||||
// "swap.created" means Boltz is waiting for the invoice to be paid
|
||||
case 'swap.created':
|
||||
this.log('Waiting invoice to be paid');
|
||||
return;
|
||||
|
||||
// "transaction.mempool" means that Boltz sent an onchain transaction
|
||||
case 'transaction.mempool':
|
||||
const txIdRes = await this.handleTransactionMempool(data, msg.args[0].transaction.hex)
|
||||
if (!txIdRes.ok) {
|
||||
throw new Error(txIdRes.error)
|
||||
}
|
||||
return txIdRes.txId
|
||||
case 'invoice.settled':
|
||||
this.log('Transaction swap successful');
|
||||
done()
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
handleTransactionMempool = async (data: TransactionSwapData, txHex: string): Promise<{ ok: true, txId: string } | { ok: false, error: string }> => {
|
||||
this.log('Creating claim transaction');
|
||||
const { createdResponse, info } = data
|
||||
const { destinationAddress, keys, preimage, chainFee } = info
|
||||
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(), keys, randomBytes(32), [
|
||||
boltzPublicKey,
|
||||
Buffer.from(keys.publicKey),
|
||||
]);
|
||||
const tweakedKey = TaprootUtils.tweakMusig(
|
||||
musig,
|
||||
// swap tree can either be a string or an object
|
||||
SwapTreeSerializer.deserializeSwapTree(createdResponse.swapTree).tree,
|
||||
);
|
||||
|
||||
// Parse the lockup transaction and find the output relevant for the swap
|
||||
const lockupTx = Transaction.fromHex(txHex);
|
||||
const swapOutput = detectSwap(tweakedKey, lockupTx);
|
||||
if (swapOutput === undefined) {
|
||||
this.log(ERROR, 'No swap output found in lockup transaction');
|
||||
return { ok: false, error: 'No swap output found in lockup transaction' }
|
||||
}
|
||||
const network = getNetwork(this.network)
|
||||
// Create a claim transaction to be signed cooperatively via a key path spend
|
||||
const claimTx = constructClaimTransaction(
|
||||
[
|
||||
{
|
||||
...swapOutput,
|
||||
keys,
|
||||
preimage,
|
||||
cooperative: true,
|
||||
type: OutputType.Taproot,
|
||||
txHash: lockupTx.getHash(),
|
||||
},
|
||||
],
|
||||
address.toOutputScript(destinationAddress, network),
|
||||
chainFee,
|
||||
)
|
||||
// Get the partial signature from Boltz
|
||||
const claimUrl = `${this.httpUrl}/v2/swap/reverse/${createdResponse.id}/claim`
|
||||
const claimReq = {
|
||||
index: 0,
|
||||
transaction: claimTx.toHex(),
|
||||
preimage: preimage.toString('hex'),
|
||||
pubNonce: Buffer.from(musig.getPublicNonce()).toString('hex'),
|
||||
}
|
||||
const boltzSigRes = await loggedPost<{ pubNonce: string, partialSignature: string }>(this.log, claimUrl, claimReq)
|
||||
if (!boltzSigRes.ok) {
|
||||
return boltzSigRes
|
||||
}
|
||||
const boltzSig = boltzSigRes.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
|
||||
const broadcastUrl = `${this.httpUrl}/v2/chain/BTC/transaction`
|
||||
const broadcastReq = {
|
||||
hex: claimTx.toHex(),
|
||||
}
|
||||
|
||||
const broadcastResponse = await loggedPost<any>(this.log, broadcastUrl, broadcastReq)
|
||||
if (!broadcastResponse.ok) {
|
||||
return broadcastResponse
|
||||
}
|
||||
this.log('Transaction broadcasted', broadcastResponse.data)
|
||||
const txId = claimTx.getId()
|
||||
return { ok: true, txId }
|
||||
}
|
||||
}
|
||||
167
src/services/lnd/swaps/submarineSwaps.ts
Normal file
167
src/services/lnd/swaps/submarineSwaps.ts
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
import zkpInit from '@vulpemventures/secp256k1-zkp';
|
||||
// import bolt11 from 'bolt11';
|
||||
import {
|
||||
Musig, SwapTreeSerializer, TaprootUtils
|
||||
} from 'boltz-core';
|
||||
import { randomBytes, createHash } from 'crypto';
|
||||
import { ECPairFactory, ECPairInterface } from 'ecpair';
|
||||
import * as ecc from 'tiny-secp256k1';
|
||||
import ws from 'ws';
|
||||
import { getLogger, PubLogger, ERROR } from '../../helpers/logger.js';
|
||||
import { loggedGet, loggedPost } from './swapHelpers.js';
|
||||
import { BTCNetwork } from '../../main/settings.js';
|
||||
|
||||
type InvoiceSwapResponse = { id: string, claimPublicKey: string, swapTree: string }
|
||||
type InvoiceSwapInfo = { paymentHash: string, keys: ECPairInterface }
|
||||
type InvoiceSwapData = { createdResponse: InvoiceSwapResponse, info: InvoiceSwapInfo }
|
||||
|
||||
|
||||
export class SubmarineSwaps {
|
||||
private httpUrl: string
|
||||
private wsUrl: string
|
||||
log: PubLogger
|
||||
constructor({ httpUrl, wsUrl }: { httpUrl: string, wsUrl: string, network: BTCNetwork }) {
|
||||
this.httpUrl = httpUrl
|
||||
this.wsUrl = wsUrl
|
||||
this.log = getLogger({ component: 'SubmarineSwaps' })
|
||||
}
|
||||
|
||||
getHttpUrl = () => {
|
||||
return this.httpUrl
|
||||
}
|
||||
getWsUrl = () => {
|
||||
return this.wsUrl
|
||||
}
|
||||
|
||||
SwapInvoice = async (invoice: string, paymentHash: string) => {
|
||||
const keys = ECPairFactory(ecc).makeRandom()
|
||||
const refundPublicKey = Buffer.from(keys.publicKey).toString('hex')
|
||||
const req = { invoice, to: 'BTC', from: 'BTC', refundPublicKey }
|
||||
const url = `${this.httpUrl}/v2/swap/submarine`
|
||||
this.log('Sending invoice swap request to', url);
|
||||
const createdResponseRes = await loggedPost<InvoiceSwapResponse>(this.log, url, req)
|
||||
if (!createdResponseRes.ok) {
|
||||
return createdResponseRes
|
||||
}
|
||||
const createdResponse = createdResponseRes.data
|
||||
this.log('Created invoice swap');
|
||||
this.log(createdResponse);
|
||||
|
||||
|
||||
}
|
||||
|
||||
SubscribeToInvoiceSwap = async (data: InvoiceSwapData, swapDone: (result: { ok: true, txId: string } | { ok: false, error: string }) => void) => {
|
||||
const webSocket = new ws(`${this.wsUrl}/v2/ws`)
|
||||
const subReq = { op: 'subscribe', channel: 'swap.update', args: [data.createdResponse.id] }
|
||||
webSocket.on('open', () => {
|
||||
webSocket.send(JSON.stringify(subReq))
|
||||
})
|
||||
let txId = "", isDone = false
|
||||
const done = () => {
|
||||
isDone = true
|
||||
webSocket.close()
|
||||
swapDone({ ok: true, txId })
|
||||
}
|
||||
webSocket.on('error', (err) => {
|
||||
this.log(ERROR, 'Error in WebSocket', err.message)
|
||||
})
|
||||
webSocket.on('close', () => {
|
||||
if (!isDone) {
|
||||
this.log(ERROR, 'WebSocket closed before swap was done');
|
||||
swapDone({ ok: false, error: 'WebSocket closed before swap was done' })
|
||||
}
|
||||
})
|
||||
webSocket.on('message', async (rawMsg) => {
|
||||
try {
|
||||
await this.handleSwapInvoiceMessage(rawMsg, data, done)
|
||||
} catch (err: any) {
|
||||
this.log(ERROR, 'Error handling invoice WebSocket message', err.message)
|
||||
webSocket.close()
|
||||
return
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleSwapInvoiceMessage = async (rawMsg: ws.RawData, data: InvoiceSwapData, closeWebSocket: () => void) => {
|
||||
const msg = JSON.parse(rawMsg.toString('utf-8'));
|
||||
if (msg.event !== 'update') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.log('Got invoice WebSocket update');
|
||||
this.log(msg);
|
||||
switch (msg.args[0].status) {
|
||||
// "invoice.set" means Boltz is waiting for an onchain transaction to be sent
|
||||
case 'invoice.set':
|
||||
this.log('Waiting for onchain transaction');
|
||||
return;
|
||||
// Create a partial signature to allow Boltz to do a key path spend to claim the mainchain coins
|
||||
case 'transaction.claim.pending':
|
||||
await this.handleInvoiceClaimPending(data)
|
||||
return;
|
||||
|
||||
case 'transaction.claimed':
|
||||
this.log('Invoice swap successful');
|
||||
closeWebSocket()
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
handleInvoiceClaimPending = async (data: InvoiceSwapData) => {
|
||||
this.log('Creating cooperative claim transaction');
|
||||
const { createdResponse, info } = data
|
||||
const { paymentHash, keys } = info
|
||||
// Get the information request to create a partial signature
|
||||
const url = `${this.httpUrl}/v2/swap/submarine/${createdResponse.id}/claim`
|
||||
const claimTxDetailsRes = await loggedGet<{ preimage: string, transactionHash: string, pubNonce: string }>(this.log, url)
|
||||
if (!claimTxDetailsRes.ok) {
|
||||
return claimTxDetailsRes
|
||||
}
|
||||
const claimTxDetails = claimTxDetailsRes.data
|
||||
// Verify that Boltz actually paid the invoice by comparing the preimage hash
|
||||
// of the invoice to the SHA256 hash of the preimage from the response
|
||||
const claimTxPreimageHash = createHash('sha256').update(Buffer.from(claimTxDetails.preimage, 'hex')).digest()
|
||||
const invoicePreimageHash = Buffer.from(paymentHash, 'hex')
|
||||
|
||||
if (!claimTxPreimageHash.equals(invoicePreimageHash)) {
|
||||
this.log(ERROR, 'Boltz provided invalid preimage');
|
||||
return;
|
||||
}
|
||||
|
||||
const boltzPublicKey = Buffer.from(createdResponse.claimPublicKey, 'hex')
|
||||
|
||||
// Create a musig signing instance
|
||||
const musig = new Musig(await zkpInit(), keys, randomBytes(32), [
|
||||
boltzPublicKey,
|
||||
Buffer.from(keys.publicKey),
|
||||
]);
|
||||
// Tweak that musig with the Taptree of the swap scripts
|
||||
TaprootUtils.tweakMusig(
|
||||
musig,
|
||||
SwapTreeSerializer.deserializeSwapTree(createdResponse.swapTree).tree,
|
||||
);
|
||||
|
||||
// Aggregate the nonces
|
||||
musig.aggregateNonces([
|
||||
[boltzPublicKey, Buffer.from(claimTxDetails.pubNonce, 'hex')],
|
||||
]);
|
||||
// Initialize the session to sign the transaction hash from the response
|
||||
musig.initializeSession(
|
||||
Buffer.from(claimTxDetails.transactionHash, 'hex'),
|
||||
);
|
||||
|
||||
// Give our public nonce and the partial signature to Boltz
|
||||
const claimUrl = `${this.httpUrl}/v2/swap/submarine/${createdResponse.id}/claim`
|
||||
const claimReq = {
|
||||
pubNonce: Buffer.from(musig.getPublicNonce()).toString('hex'),
|
||||
partialSignature: Buffer.from(musig.signPartial()).toString('hex'),
|
||||
}
|
||||
const claimResponseRes = await loggedPost<{ pubNonce: string, partialSignature: string }>(this.log, claimUrl, claimReq)
|
||||
if (!claimResponseRes.ok) {
|
||||
return claimResponseRes
|
||||
}
|
||||
const claimResponse = claimResponseRes.data
|
||||
this.log('Claim response', claimResponse)
|
||||
}
|
||||
}
|
||||
50
src/services/lnd/swaps/swapHelpers.ts
Normal file
50
src/services/lnd/swaps/swapHelpers.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import axios from 'axios';
|
||||
import { Network } from 'bitcoinjs-lib';
|
||||
// import bolt11 from 'bolt11';
|
||||
import {
|
||||
Networks,
|
||||
} from 'boltz-core';
|
||||
import { PubLogger, ERROR } from '../../helpers/logger.js';
|
||||
import { BTCNetwork } from '../../main/settings.js';
|
||||
|
||||
|
||||
export const loggedPost = async <T>(log: PubLogger, url: string, req: any): Promise<{ ok: true, data: T } | { ok: false, error: string }> => {
|
||||
try {
|
||||
const { data } = await axios.post(url, req)
|
||||
return { ok: true, data: data as T }
|
||||
} catch (err: any) {
|
||||
if (err.response?.data) {
|
||||
log(ERROR, 'Error sending request', err.response.data)
|
||||
return { ok: false, error: JSON.stringify(err.response.data) }
|
||||
}
|
||||
log(ERROR, 'Error sending request', err.message)
|
||||
return { ok: false, error: err.message }
|
||||
}
|
||||
}
|
||||
|
||||
export const loggedGet = async <T>(log: PubLogger, url: string): Promise<{ ok: true, data: T } | { ok: false, error: string }> => {
|
||||
try {
|
||||
const { data } = await axios.get(url)
|
||||
return { ok: true, data: data as T }
|
||||
} catch (err: any) {
|
||||
if (err.response?.data) {
|
||||
log(ERROR, 'Error getting request', err.response.data)
|
||||
return { ok: false, error: err.response.data }
|
||||
}
|
||||
log(ERROR, 'Error getting request', err.message)
|
||||
return { ok: false, error: err.message }
|
||||
}
|
||||
}
|
||||
|
||||
export const getNetwork = (network: BTCNetwork): Network => {
|
||||
switch (network) {
|
||||
case 'mainnet':
|
||||
return Networks.bitcoinMainnet
|
||||
case 'testnet':
|
||||
return Networks.bitcoinTestnet
|
||||
case 'regtest':
|
||||
return Networks.bitcoinRegtest
|
||||
default:
|
||||
throw new Error(`Invalid network: ${network}`)
|
||||
}
|
||||
}
|
||||
226
src/services/lnd/swaps/swaps.ts
Normal file
226
src/services/lnd/swaps/swaps.ts
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
import { ECPairFactory } from 'ecpair';
|
||||
import * as ecc from 'tiny-secp256k1';
|
||||
import { getLogger } from '../../helpers/logger.js';
|
||||
import SettingsManager from '../../main/settingsManager.js';
|
||||
import * as Types from '../../../../proto/autogenerated/ts/types.js';
|
||||
import Storage from '../../storage/index.js';
|
||||
import LND from '../lnd.js';
|
||||
import { UserInvoicePayment } from '../../storage/entity/UserInvoicePayment.js';
|
||||
import { ReverseSwaps, TransactionSwapData } from './reverseSwaps.js';
|
||||
import { SubmarineSwaps } from './submarineSwaps.js';
|
||||
|
||||
|
||||
export class Swaps {
|
||||
settings: SettingsManager
|
||||
revSwappers: Record<string, ReverseSwaps>
|
||||
subSwappers: Record<string, SubmarineSwaps>
|
||||
storage: Storage
|
||||
lnd: LND
|
||||
log = getLogger({ component: 'swaps' })
|
||||
constructor(settings: SettingsManager, storage: Storage) {
|
||||
this.settings = settings
|
||||
this.revSwappers = {}
|
||||
this.subSwappers = {}
|
||||
const network = settings.getSettings().lndSettings.network
|
||||
const { boltzHttpUrl, boltzWebSocketUrl, boltsHttpUrlAlt, boltsWebSocketUrlAlt } = settings.getSettings().swapsSettings
|
||||
if (boltzHttpUrl && boltzWebSocketUrl) {
|
||||
this.revSwappers[boltzHttpUrl] = new ReverseSwaps({ httpUrl: boltzHttpUrl, wsUrl: boltzWebSocketUrl, network })
|
||||
this.subSwappers[boltzHttpUrl] = new SubmarineSwaps({ httpUrl: boltzHttpUrl, wsUrl: boltzWebSocketUrl, network })
|
||||
}
|
||||
if (boltsHttpUrlAlt && boltsWebSocketUrlAlt) {
|
||||
this.revSwappers[boltsHttpUrlAlt] = new ReverseSwaps({ httpUrl: boltsHttpUrlAlt, wsUrl: boltsWebSocketUrlAlt, network })
|
||||
this.subSwappers[boltsHttpUrlAlt] = new SubmarineSwaps({ httpUrl: boltsHttpUrlAlt, wsUrl: boltsWebSocketUrlAlt, network })
|
||||
}
|
||||
this.storage = storage
|
||||
}
|
||||
|
||||
SetLnd = (lnd: LND) => {
|
||||
this.lnd = lnd
|
||||
}
|
||||
|
||||
Stop = () => { }
|
||||
|
||||
GetKeys = (privateKey: string) => {
|
||||
const keys = ECPairFactory(ecc).fromPrivateKey(Buffer.from(privateKey, 'hex'))
|
||||
return keys
|
||||
}
|
||||
|
||||
GetInvoiceSwapQuotes = async (appUserId: string, payments: UserInvoicePayment[], getServiceFee: (amt: number) => number): Promise<Types.InvoiceSwapQuote[]> => {
|
||||
if (!this.settings.getSettings().swapsSettings.enableSwaps) {
|
||||
throw new Error("Swaps are not enabled")
|
||||
}
|
||||
const swappers = Object.values(this.subSwappers)
|
||||
if (swappers.length === 0) {
|
||||
throw new Error("No swap services available")
|
||||
}
|
||||
const res = await Promise.allSettled(swappers.map(sw => this.getInvoiceSwapQuote(sw, appUserId, payments, getServiceFee)))
|
||||
const failures: string[] = []
|
||||
const success: Types.InvoiceSwapQuote[] = []
|
||||
for (const r of res) { }
|
||||
}
|
||||
|
||||
ListTxSwaps = async (appUserId: string, payments: UserInvoicePayment[], newOp: (p: UserInvoicePayment) => Types.UserOperation | undefined, getServiceFee: (amt: number) => number): Promise<Types.SwapsList> => {
|
||||
const completedSwaps = await this.storage.paymentStorage.ListCompletedTxSwaps(appUserId, payments)
|
||||
const pendingSwaps = await this.storage.paymentStorage.ListPendingTransactionSwaps(appUserId)
|
||||
return {
|
||||
swaps: completedSwaps.map(s => {
|
||||
const p = s.payment
|
||||
const op = p ? newOp(p) : undefined
|
||||
return {
|
||||
operation_payment: op,
|
||||
swap_operation_id: s.swap.swap_operation_id,
|
||||
address_paid: s.swap.address_paid,
|
||||
failure_reason: s.swap.failure_reason,
|
||||
}
|
||||
}),
|
||||
quotes: pendingSwaps.map(s => {
|
||||
const serviceFee = getServiceFee(s.invoice_amount)
|
||||
return {
|
||||
swap_operation_id: s.swap_operation_id,
|
||||
invoice_amount_sats: s.invoice_amount,
|
||||
transaction_amount_sats: s.transaction_amount,
|
||||
chain_fee_sats: s.chain_fee_sats,
|
||||
service_fee_sats: serviceFee,
|
||||
swap_fee_sats: s.swap_fee_sats,
|
||||
service_url: s.service_url,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
GetTxSwapQuotes = async (appUserId: string, amt: number, getServiceFee: (decodedAmt: number) => number): Promise<Types.TransactionSwapQuote[]> => {
|
||||
if (!this.settings.getSettings().swapsSettings.enableSwaps) {
|
||||
throw new Error("Swaps are not enabled")
|
||||
}
|
||||
const swappers = Object.values(this.revSwappers)
|
||||
if (swappers.length === 0) {
|
||||
throw new Error("No swap services available")
|
||||
}
|
||||
const res = await Promise.allSettled(swappers.map(sw => this.getTxSwapQuote(sw, appUserId, amt, getServiceFee)))
|
||||
const failures: string[] = []
|
||||
const success: Types.TransactionSwapQuote[] = []
|
||||
for (const r of res) {
|
||||
if (r.status === 'fulfilled') {
|
||||
success.push(r.value)
|
||||
} else {
|
||||
failures.push(r.reason.message ? r.reason.message : r.reason.toString())
|
||||
}
|
||||
}
|
||||
if (success.length === 0) {
|
||||
throw new Error(failures.join("\n"))
|
||||
}
|
||||
return success
|
||||
}
|
||||
|
||||
private async getTxSwapQuote(swapper: ReverseSwaps, appUserId: string, amt: number, getServiceFee: (decodedAmt: number) => number): Promise<Types.TransactionSwapQuote> {
|
||||
this.log("getting transaction swap quote")
|
||||
const feesRes = await swapper.GetFees()
|
||||
if (!feesRes.ok) {
|
||||
throw new Error(feesRes.error)
|
||||
}
|
||||
const { claim, lockup } = feesRes.fees.minerFees
|
||||
const minerFee = claim + lockup
|
||||
const chainTotal = amt + minerFee
|
||||
const res = await swapper.SwapTransaction(chainTotal)
|
||||
if (!res.ok) {
|
||||
throw new Error(res.error)
|
||||
}
|
||||
const decoded = await this.lnd.DecodeInvoice(res.createdResponse.invoice)
|
||||
const swapFee = decoded.numSatoshis - chainTotal
|
||||
const serviceFee = getServiceFee(decoded.numSatoshis)
|
||||
const newSwap = await this.storage.paymentStorage.AddTransactionSwap({
|
||||
app_user_id: appUserId,
|
||||
swap_quote_id: res.createdResponse.id,
|
||||
swap_tree: JSON.stringify(res.createdResponse.swapTree),
|
||||
lockup_address: res.createdResponse.lockupAddress,
|
||||
refund_public_key: res.createdResponse.refundPublicKey,
|
||||
timeout_block_height: res.createdResponse.timeoutBlockHeight,
|
||||
invoice: res.createdResponse.invoice,
|
||||
invoice_amount: decoded.numSatoshis,
|
||||
transaction_amount: chainTotal,
|
||||
swap_fee_sats: swapFee,
|
||||
chain_fee_sats: minerFee,
|
||||
preimage: res.preimage,
|
||||
ephemeral_private_key: res.privKey,
|
||||
ephemeral_public_key: res.pubkey,
|
||||
service_url: swapper.getHttpUrl(),
|
||||
})
|
||||
return {
|
||||
swap_operation_id: newSwap.swap_operation_id,
|
||||
swap_fee_sats: swapFee,
|
||||
invoice_amount_sats: decoded.numSatoshis,
|
||||
transaction_amount_sats: amt,
|
||||
chain_fee_sats: minerFee,
|
||||
service_fee_sats: serviceFee,
|
||||
service_url: swapper.getHttpUrl(),
|
||||
}
|
||||
}
|
||||
|
||||
async PayAddrWithSwap(appUserId: string, swapOpId: string, address: string, payInvoice: (invoice: string, amt: number) => Promise<void>) {
|
||||
if (!this.settings.getSettings().swapsSettings.enableSwaps) {
|
||||
throw new Error("Swaps are not enabled")
|
||||
}
|
||||
this.log("paying address with swap", { appUserId, swapOpId, address })
|
||||
if (!swapOpId) {
|
||||
throw new Error("request a swap quote before paying an external address")
|
||||
}
|
||||
const txSwap = await this.storage.paymentStorage.GetTransactionSwap(swapOpId, appUserId)
|
||||
if (!txSwap) {
|
||||
throw new Error("swap quote not found")
|
||||
}
|
||||
const info = await this.lnd.GetInfo()
|
||||
if (info.blockHeight >= txSwap.timeout_block_height) {
|
||||
throw new Error("swap timeout")
|
||||
}
|
||||
const swapper = this.revSwappers[txSwap.service_url]
|
||||
if (!swapper) {
|
||||
throw new Error("swapper service not found")
|
||||
}
|
||||
const keys = this.GetKeys(txSwap.ephemeral_private_key)
|
||||
const data: TransactionSwapData = {
|
||||
createdResponse: {
|
||||
id: txSwap.swap_quote_id,
|
||||
invoice: txSwap.invoice,
|
||||
lockupAddress: txSwap.lockup_address,
|
||||
refundPublicKey: txSwap.refund_public_key,
|
||||
swapTree: txSwap.swap_tree,
|
||||
timeoutBlockHeight: txSwap.timeout_block_height,
|
||||
onchainAmount: txSwap.transaction_amount,
|
||||
},
|
||||
info: {
|
||||
destinationAddress: address,
|
||||
keys,
|
||||
chainFee: txSwap.chain_fee_sats,
|
||||
preimage: Buffer.from(txSwap.preimage, 'hex'),
|
||||
}
|
||||
}
|
||||
// the swap and the invoice payment are linked, swap will not start until the invoice payment is started, and will not complete once the invoice payment is completed
|
||||
let swapResult = { ok: false, error: "swap never completed" } as { ok: true, txId: string } | { ok: false, error: string }
|
||||
swapper.SubscribeToTransactionSwap(data, result => {
|
||||
swapResult = result
|
||||
})
|
||||
try {
|
||||
await payInvoice(txSwap.invoice, txSwap.invoice_amount)
|
||||
if (!swapResult.ok) {
|
||||
this.log("invoice payment successful, but swap failed")
|
||||
await this.storage.paymentStorage.FailTransactionSwap(swapOpId, address, swapResult.error)
|
||||
throw new Error(swapResult.error)
|
||||
}
|
||||
this.log("swap completed successfully")
|
||||
await this.storage.paymentStorage.FinalizeTransactionSwap(swapOpId, address, swapResult.txId)
|
||||
} catch (err: any) {
|
||||
if (swapResult.ok) {
|
||||
this.log("failed to pay swap invoice, but swap completed successfully", swapResult.txId)
|
||||
await this.storage.paymentStorage.FailTransactionSwap(swapOpId, address, err.message)
|
||||
} else {
|
||||
this.log("failed to pay swap invoice and swap failed", swapResult.error)
|
||||
await this.storage.paymentStorage.FailTransactionSwap(swapOpId, address, swapResult.error)
|
||||
}
|
||||
throw err
|
||||
}
|
||||
const networkFeesTotal = txSwap.chain_fee_sats + txSwap.swap_fee_sats
|
||||
return {
|
||||
txId: swapResult.txId,
|
||||
network_fee: networkFeesTotal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ import Storage from "../storage/index.js";
|
|||
import * as Types from '../../../proto/autogenerated/ts/types.js'
|
||||
import LND from "../lnd/lnd.js";
|
||||
import SettingsManager from "./settingsManager.js";
|
||||
import { Swaps } from "../lnd/swaps.js";
|
||||
import { Swaps } from "../lnd/swaps/swaps.js";
|
||||
export class AdminManager {
|
||||
settings: SettingsManager
|
||||
storage: Storage
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { AdminManager } from "./adminManager.js"
|
|||
import SettingsManager from "./settingsManager.js"
|
||||
import { LoadStorageSettingsFromEnv } from "../storage/index.js"
|
||||
import { NostrSender } from "../nostr/sender.js"
|
||||
import { Swaps } from "../lnd/swaps.js"
|
||||
import { Swaps } from "../lnd/swaps/swaps.js"
|
||||
export type AppData = {
|
||||
privateKey: string;
|
||||
publicKey: string;
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import { LiquidityManager } from './liquidityManager.js'
|
|||
import { Utils } from '../helpers/utilsWrapper.js'
|
||||
import { UserInvoicePayment } from '../storage/entity/UserInvoicePayment.js'
|
||||
import SettingsManager from './settingsManager.js'
|
||||
import { Swaps, TransactionSwapData } from '../lnd/swaps.js'
|
||||
import { Swaps, TransactionSwapData } from '../lnd/swaps/swaps.js'
|
||||
import { Transaction, OutputDetail } from '../../../proto/lnd/lightning.js'
|
||||
import { LndAddress } from '../lnd/lnd.js'
|
||||
import Metrics from '../metrics/index.js'
|
||||
|
|
@ -618,10 +618,10 @@ export default class {
|
|||
}
|
||||
|
||||
async ListSwaps(ctx: Types.UserContext): Promise<Types.SwapsList> {
|
||||
const payments = await this.storage.paymentStorage.ListSwapPayments(ctx.app_user_id)
|
||||
const payments = await this.storage.paymentStorage.ListTxSwapPayments(ctx.app_user_id)
|
||||
const app = await this.storage.applicationStorage.GetApplication(ctx.app_id)
|
||||
const isManagedUser = ctx.user_id !== app.owner.user_id
|
||||
return this.swaps.ListSwaps(ctx.app_user_id, payments, p => {
|
||||
return this.swaps.ListTxSwaps(ctx.app_user_id, payments, p => {
|
||||
const opId = `${Types.UserOperationType.OUTGOING_TX}-${p.serial_id}`
|
||||
return this.newInvoicePaymentOperation({ amount: p.paid_amount, confirmed: p.paid_at_unix !== 0, invoice: p.invoice, opId, networkFee: p.routing_fees, serviceFee: p.service_fees, paidAtUnix: p.paid_at_unix })
|
||||
}, amt => this.getSendServiceFee(Types.UserOperationType.OUTGOING_INVOICE, amt, isManagedUser))
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import * as fs from 'fs'
|
|||
import { UserAccess } from "../entity/UserAccess.js"
|
||||
import { AdminSettings } from "../entity/AdminSettings.js"
|
||||
import { TransactionSwap } from "../entity/TransactionSwap.js"
|
||||
import { InvoiceSwap } from "../entity/InvoiceSwap.js"
|
||||
|
||||
|
||||
export type DbSettings = {
|
||||
|
|
@ -76,7 +77,8 @@ export const MainDbEntities = {
|
|||
'AppUserDevice': AppUserDevice,
|
||||
'UserAccess': UserAccess,
|
||||
'AdminSettings': AdminSettings,
|
||||
'TransactionSwap': TransactionSwap
|
||||
'TransactionSwap': TransactionSwap,
|
||||
'InvoiceSwap': InvoiceSwap
|
||||
}
|
||||
export type MainDbNames = keyof typeof MainDbEntities
|
||||
export const MainDbEntitiesNames = Object.keys(MainDbEntities)
|
||||
|
|
|
|||
74
src/services/storage/entity/InvoiceSwap.ts
Normal file
74
src/services/storage/entity/InvoiceSwap.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, ManyToOne, JoinColumn, UpdateDateColumn } from "typeorm";
|
||||
import { User } from "./User";
|
||||
|
||||
@Entity()
|
||||
export class InvoiceSwap {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
swap_operation_id: string
|
||||
|
||||
@Column()
|
||||
app_user_id: string
|
||||
|
||||
@Column()
|
||||
swap_quote_id: string
|
||||
|
||||
@Column()
|
||||
swap_tree: string
|
||||
|
||||
@Column()
|
||||
lockup_address: string
|
||||
|
||||
@Column()
|
||||
refund_public_key: string
|
||||
|
||||
@Column()
|
||||
timeout_block_height: number
|
||||
|
||||
@Column()
|
||||
invoice: string
|
||||
|
||||
@Column()
|
||||
invoice_amount: number
|
||||
|
||||
@Column()
|
||||
transaction_amount: number
|
||||
|
||||
@Column()
|
||||
swap_fee_sats: number
|
||||
|
||||
@Column()
|
||||
chain_fee_sats: number
|
||||
|
||||
@Column()
|
||||
preimage: string
|
||||
|
||||
@Column()
|
||||
ephemeral_public_key: string
|
||||
|
||||
// the private key is used on to perform a swap, it does not hold any funds once the swap is completed
|
||||
// the swap should only last a few seconds, so it is not a security risk to store the private key in the database
|
||||
// the key is stored here mostly for recovery purposes, in case something goes wrong with the swap
|
||||
@Column()
|
||||
ephemeral_private_key: string
|
||||
|
||||
@Column({ default: false })
|
||||
used: boolean
|
||||
|
||||
@Column({ default: "" })
|
||||
failure_reason: string
|
||||
|
||||
@Column({ default: "" })
|
||||
tx_id: string
|
||||
|
||||
@Column({ default: "" })
|
||||
address_paid: string
|
||||
|
||||
@Column({ default: "" })
|
||||
service_url: string
|
||||
|
||||
@CreateDateColumn()
|
||||
created_at: Date
|
||||
|
||||
@UpdateDateColumn()
|
||||
updated_at: Date
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ import TransactionsQueue from "./db/transactionsQueue.js";
|
|||
import { LoggedEvent } from './eventsLog.js';
|
||||
import { StorageInterface } from './db/storageInterface.js';
|
||||
import { TransactionSwap } from './entity/TransactionSwap.js';
|
||||
import { InvoiceSwap } from './entity/InvoiceSwap.js';
|
||||
export type InboundOptionals = { product?: Product, callbackUrl?: string, expiry: number, expectedPayer?: User, linkedApplication?: Application, zapInfo?: ZapInfo, offerId?: string, payerData?: Record<string, string>, rejectUnauthorized?: boolean, token?: string, blind?: boolean, clinkRequesterPub?: string, clinkRequesterEventId?: string }
|
||||
export const defaultInvoiceExpiry = 60 * 60
|
||||
export default class {
|
||||
|
|
@ -500,11 +501,11 @@ export default class {
|
|||
return this.dbs.Find<TransactionSwap>('TransactionSwap', { where: { used: false, app_user_id: appUserId } }, txId)
|
||||
}
|
||||
|
||||
async ListSwapPayments(userId: string, txId?: string) {
|
||||
async ListTxSwapPayments(userId: string, txId?: string) {
|
||||
return this.dbs.Find<UserInvoicePayment>('UserInvoicePayment', { where: { swap_operation_id: Not(IsNull()), user: { user_id: userId } } }, txId)
|
||||
}
|
||||
|
||||
async ListCompletedSwaps(appUserId: string, payments: UserInvoicePayment[], txId?: string) {
|
||||
async ListCompletedTxSwaps(appUserId: string, payments: UserInvoicePayment[], txId?: string) {
|
||||
const completed = await this.dbs.Find<TransactionSwap>('TransactionSwap', { where: { used: true, app_user_id: appUserId } }, txId)
|
||||
// const payments = await this.dbs.Find<UserInvoicePayment>('UserInvoicePayment', { where: { swap_operation_id: Not(IsNull()), } }, txId)
|
||||
const paymentsMap = new Map<string, UserInvoicePayment>()
|
||||
|
|
@ -515,6 +516,15 @@ export default class {
|
|||
swap: c, payment: paymentsMap.get(c.swap_operation_id)
|
||||
}))
|
||||
}
|
||||
|
||||
async AddInvoiceSwap(swap: Partial<InvoiceSwap>) {
|
||||
return this.dbs.CreateAndSave<InvoiceSwap>('InvoiceSwap', swap)
|
||||
}
|
||||
|
||||
async GetInvoiceSwap(swapOperationId: string, appUserId: string, txId?: string) {
|
||||
return this.dbs.FindOne<InvoiceSwap>('InvoiceSwap', { where: { swap_operation_id: swapOperationId, used: false, app_user_id: appUserId } }, txId)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const orFail = async <T>(resultPromise: Promise<T | null>) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue