Skip to content

Commit

Permalink
feat: cancel invoices of expired reverse swaps
Browse files Browse the repository at this point in the history
  • Loading branch information
michael1011 committed Dec 21, 2019
1 parent 62e0bbb commit e6aef4f
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 51 deletions.
10 changes: 10 additions & 0 deletions lib/lightning/LndClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,16 @@ class LndClient extends BaseClient implements LndClient {
}
}

/**
* Cancel a hold invoice
*/
public cancelInvoice = (preimageHash: Buffer) => {
const request = new invoicesrpc.CancelInvoiceMsg();
request.setPaymentHash(Uint8Array.from(preimageHash));

return this.unaryInvoicesCall<invoicesrpc.CancelInvoiceMsg, invoicesrpc.CancelInvoiceResp>('cancelInvoice', request);
}

/**
* Settle a hold invoice with an already accepted HTLC
*/
Expand Down
13 changes: 7 additions & 6 deletions lib/swap/SwapManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Logger from '../Logger';
import { OrderSide } from '../consts/Enums';
import LndClient from '../lightning/LndClient';
import WalletManager, { Currency } from '../wallet/WalletManager';
import SwapNursery, { SwapMaps, SwapDetails, ReverseSwapDetails } from './SwapNursery';
import SwapNursery, { SwapMaps, SwapDetails, ReverseSwapDetails, MinimalReverseSwapDetails } from './SwapNursery';
import { getHexBuffer, getHexString, getScriptHashFunction, getSwapMemo, getSendingReceivingCurrency } from '../Utils';

class SwapManager {
Expand All @@ -22,8 +22,8 @@ class SwapManager {
swapTimeouts: new Map<number, string[]>(),

reverseSwaps: new Map<string, ReverseSwapDetails>(),
reverseSwapTransactions: new Map<string, string>(),
reverseSwapTimeouts: new Map<number, string[]>(),
reverseSwapTransactions: new Map<string, MinimalReverseSwapDetails>(),
reverseSwapTimeouts: new Map<number, MinimalReverseSwapDetails[]>(),
};

this.currencies.set(currency.symbol, {
Expand Down Expand Up @@ -104,7 +104,6 @@ class SwapManager {
acceptZeroConf,
expectedAmount,
claimKeys: keys,
lndClient: sendingCurrency.lndClient,
},
outputScript,
timeoutBlockHeight,
Expand Down Expand Up @@ -177,15 +176,17 @@ class SwapManager {
sendingCurrency.chainClient.updateOutputFilter([outputScript]);

this.nursery.addReverseSwap(
receivingCurrency,
{
outputType,
preimageHash,
redeemScript,
refundKeys: keys,
sendingSymbol: sendingCurrency.symbol,
receivingSymbol: receivingCurrency.symbol,

sendingDetails: {
address,
amount: onchainAmount,
sendingCurrency: sendingCurrency.symbol,
},
},
paymentRequest,
Expand Down
116 changes: 77 additions & 39 deletions lib/swap/SwapNursery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,33 @@ type BaseSwapDetails = {

type SwapDetails = BaseSwapDetails & {
invoice: string;
lndClient: LndClient;
outputType: OutputType;
expectedAmount: number;
acceptZeroConf: boolean;
claimKeys: BIP32Interface;
};

type SendingDetails = {
sendingCurrency: string;

amount: number;
address: string;
};

type ReverseSwapDetails = BaseSwapDetails & {
outputType: OutputType,
sendingSymbol: string;
receivingSymbol: string;

preimageHash: Buffer;
outputType: OutputType;
output?: TransactionOutput;
refundKeys: BIP32Interface;
sendingDetails: SendingDetails;
};

type MinimalReverseSwapDetails = {
invoice: string;
receivingSymbol: string;
};

type SwapMaps = {
// A map between the output scripts and the swaps details
swaps: Map<string, SwapDetails>;
Expand All @@ -51,11 +57,11 @@ type SwapMaps = {
// A map between the invoices and the reverse swaps details
reverseSwaps: Map<string, ReverseSwapDetails>;

// A map between the lock up transaction id of a reverse swap and its invoice
reverseSwapTransactions: Map<string, string>;
// A map between the lock up transaction id of a reverse swap and its invoice and the symbol of the reiceiving currency
reverseSwapTransactions: Map<string, MinimalReverseSwapDetails>;

// A map betwee the timeout block heights and the invoices of the reverse swaps
reverseSwapTimeouts: Map<number, string[]>;
reverseSwapTimeouts: Map<number, MinimalReverseSwapDetails[]>;
};

interface SwapNursery {
Expand Down Expand Up @@ -83,9 +89,12 @@ interface SwapNursery {
emit(event: 'refund', lockupTransactionId: string, lockupVout: number, minerFee: number): boolean;
}

// TODO: test LTC/BTC reverse swaps
// TODO: make sure swaps work after restarts (save to and read from database)
class SwapNursery extends EventEmitter {
private maps = new Map<string, SwapMaps>();
private lndClients = new Map<string, LndClient>();
private chainClients = new Map<string, ChainClient>();

constructor(private logger: Logger, private walletManager: WalletManager) {
super();
}
Expand All @@ -109,24 +118,35 @@ class SwapNursery extends EventEmitter {
}

public addReverseSwap = (
maps: SwapMaps,
details: ReverseSwapDetails,
invoice: string,
timeoutBlockHeight: number,
) => {
maps.reverseSwaps.set(invoice, details);
const sendingMaps = this.maps.get(details.sendingSymbol)!;
const receivingMaps = this.maps.get(details.receivingSymbol)!;

receivingMaps.reverseSwaps.set(invoice, details);

const pendingReverseSwaps = maps.reverseSwapTimeouts.get(timeoutBlockHeight);
const minimalDetails = { invoice, receivingSymbol: details.receivingSymbol };
const pendingReverseSwaps = sendingMaps.reverseSwapTimeouts.get(timeoutBlockHeight);

if (pendingReverseSwaps) {
pendingReverseSwaps.push(invoice);
pendingReverseSwaps.push(minimalDetails);
} else {
maps.reverseSwapTimeouts.set(timeoutBlockHeight, [invoice]);
sendingMaps.reverseSwapTimeouts.set(timeoutBlockHeight, [minimalDetails]);
}
}

public bindCurrency = (currency: Currency, maps: SwapMaps) => {
const { chainClient, lndClient } = currency;
const { symbol } = chainClient;

this.maps.set(symbol, maps);
this.chainClients.set(symbol, chainClient);

if (lndClient) {
this.lndClients.set(symbol, lndClient);
}

chainClient.on('transaction', async (transaction: Transaction, confirmed: boolean) => {
let zeroConfRejectedReason: string | undefined = undefined;
Expand Down Expand Up @@ -197,17 +217,19 @@ class SwapNursery extends EventEmitter {
const input = transaction.ins[i];

const inputTransactionId = transactionHashToId(input.hash);
const reverseSwapInvoice = maps.reverseSwapTransactions.get(inputTransactionId);
const reverseSwapSentInfo = maps.reverseSwapTransactions.get(inputTransactionId);

if (reverseSwapInvoice !== undefined) {
if (reverseSwapSentInfo !== undefined) {
await this.settleReverseSwap(
transaction,
i,
lndClient!,
reverseSwapSentInfo.receivingSymbol,
);

maps.reverseSwapTransactions.delete(inputTransactionId);
maps.reverseSwaps.delete(reverseSwapInvoice);

const receivingMaps = this.maps.get(reverseSwapSentInfo.receivingSymbol)!;
receivingMaps.reverseSwaps.delete(reverseSwapSentInfo.invoice);
}
}
});
Expand Down Expand Up @@ -247,7 +269,6 @@ class SwapNursery extends EventEmitter {
invoice,
reverseSwapDetails,
maps,
chainClient,
);
}
});
Expand Down Expand Up @@ -306,14 +327,18 @@ class SwapNursery extends EventEmitter {
private sendReverseSwapCoins = async (
invoice: string,
details: ReverseSwapDetails,
maps: SwapMaps,
chainClient: ChainClient,
receivingMaps: SwapMaps,
) => {
const { sendingCurrency, address, amount } = details.sendingDetails;
const wallet = this.walletManager.wallets.get(sendingCurrency)!;
const { sendingSymbol } = details;
const { address, amount } = details.sendingDetails;

const chainClient = this.chainClients.get(sendingSymbol)!;

const wallet = this.walletManager.wallets.get(sendingSymbol)!;

// TODO: handle errors
const { fee, vout, transaction, transactionId } = await wallet.sendToAddress(address, amount);
this.logger.verbose(`Locked up ${sendingCurrency} to reverse swap in transaction: ${transactionId}`);
this.logger.verbose(`Locked up ${sendingSymbol} to reverse swap in transaction: ${transactionId}`);

chainClient.updateInputFilter([transaction.getHash()]);

Expand All @@ -324,34 +349,34 @@ class SwapNursery extends EventEmitter {
txHash: transaction.getHash(),
script: wallet.decodeAddress(address),
};
maps.reverseSwaps.set(invoice, details);
receivingMaps.reverseSwaps.set(invoice, details);

maps.reverseSwapTransactions.set(transactionId, invoice);
const sendingMaps = this.maps.get(sendingSymbol)!;
sendingMaps.reverseSwapTransactions.set(transactionId, { invoice, receivingSymbol: details.receivingSymbol });

this.emit('coins.sent', invoice, transactionId, fee);
}

private settleReverseSwap = async (
transaction: Transaction,
vin: number,
lndClient: LndClient,
receivingSymbol: string,
) => {
const preimage = detectPreimage(vin, transaction);
this.logger.verbose(`Got preimage for reverse swap: ${getHexString(preimage)}`);
this.logger.verbose(`Got preimage of reverse swap: ${getHexString(preimage)}`);

await lndClient.settleInvoice(preimage);
this.logger.debug('Settled LND invoice');
await this.lndClients.get(receivingSymbol)!.settleInvoice(preimage);
}

// TODO: cancel all pending HTLC for this reverse swap (also immediately cancel all incoming ones that are sent after the refund initiated)
private handleExpiredReverseSwaps = async (
currency: Currency,
maps: SwapMaps,
reverseSwapInvoices: string[],
sendingMaps: SwapMaps,
reverseSwapInvoices: MinimalReverseSwapDetails[],
timeoutBlockHeight: number,
) => {
for (const invoice of reverseSwapInvoices) {
const details = maps.reverseSwaps.get(invoice);
for (const { invoice, receivingSymbol } of reverseSwapInvoices) {
const reiceivingMaps = this.maps.get(receivingSymbol)!;
const details = reiceivingMaps.reverseSwaps.get(invoice);

if (details !== undefined) {
if (details.output !== undefined) {
Expand All @@ -372,8 +397,8 @@ class SwapNursery extends EventEmitter {
);
const minerFee = await this.calculateTransactionFee(refundTx, currency.chainClient, details.output.value);

maps.reverseSwaps.delete(invoice);
maps.reverseSwapTransactions.delete(transactionId);
sendingMaps.reverseSwapTransactions.delete(transactionId);
reiceivingMaps.reverseSwaps.delete(invoice);

this.logger.verbose(`Broadcasting ${currency.symbol} refund transaction: ${refundTx.getId()}`);

Expand All @@ -383,17 +408,25 @@ class SwapNursery extends EventEmitter {
} catch (error) {
this.logger.warn(`Could not broadcast ${currency.symbol} refund transaction: ${error.message}`);
}

await this.cancelInvoice(this.lndClients.get(receivingSymbol)!, details.preimageHash);
} else {
maps.reverseSwaps.delete(invoice);
reiceivingMaps.reverseSwaps.delete(invoice);

this.logger.verbose(`Aborting reverse swap: ${invoice}`);
this.emit('expiration', invoice, true);
}
}

}
}

private cancelInvoice = (lndClient: LndClient, preimageHash: Buffer) => {
this.logger.verbose(`Cancelling hold invoice with preimage hash: ${getHexString(preimageHash)}`);

return lndClient.cancelInvoice(preimageHash);

}

private payInvoice = async (lndClient: LndClient, invoice: string) => {
try {
const response = await lndClient.sendPayment(invoice);
Expand Down Expand Up @@ -484,4 +517,9 @@ class SwapNursery extends EventEmitter {
}

export default SwapNursery;
export { SwapMaps, SwapDetails, ReverseSwapDetails };
export {
SwapMaps,
SwapDetails,
ReverseSwapDetails,
MinimalReverseSwapDetails,
};
10 changes: 5 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
"otplib": "^11.0.1",
"sequelize": "^5.21.3",
"sqlite3": "^4.1.1",
"typescript": "^3.7.3",
"typescript": "^3.7.4",
"uuid": "^3.3.3",
"winston": "^3.2.1",
"yargs": "^15.0.2",
Expand Down

0 comments on commit e6aef4f

Please sign in to comment.