Skip to content

Commit

Permalink
feat: set CLTV expiry of hold invoices
Browse files Browse the repository at this point in the history
  • Loading branch information
michael1011 committed Mar 1, 2020
1 parent a10771a commit e8e2591
Show file tree
Hide file tree
Showing 9 changed files with 201 additions and 103 deletions.
7 changes: 5 additions & 2 deletions lib/lightning/LndClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,12 +223,14 @@ class LndClient extends BaseClient implements LndClient {
* Creates a hold invoice with the supplied preimage hash
*
* @param value the value of this invoice in satoshis
* @param cltvExpiry expiry delta of the last hop
* @param preimageHash the hash of the preimage
* @param memo optional memo to attach along with the invoice
*/
public addHoldInvoice = async (value: number, preimageHash: Buffer, memo?: string) => {
public addHoldInvoice = async (value: number, preimageHash: Buffer, cltvExpiry: number, memo?: string) => {
const request = new invoicesrpc.AddHoldInvoiceRequest();
request.setValue(value);
request.setCltvExpiry(cltvExpiry);
request.setHash(Uint8Array.from(preimageHash));

if (memo) {
Expand Down Expand Up @@ -432,7 +434,8 @@ class LndClient extends BaseClient implements LndClient {
invoiceSubscription
.on('data', (invoice: lndrpc.Invoice) => {
if (invoice.getState() === lndrpc.Invoice.InvoiceState.ACCEPTED) {
// TODO: check amount of htlc?
// TODO: check amount of HTLC
// TODO: handle multiple HTLCs
this.logger.debug(`${LndClient.serviceName} ${this.symbol} accepted HTLC for invoice: ${invoice.getPaymentRequest()}`);
this.emit('htlc.accepted', invoice.getPaymentRequest());
} else if (invoice.getState() === lndrpc.Invoice.InvoiceState.SETTLED) {
Expand Down
8 changes: 8 additions & 0 deletions lib/service/Errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,12 @@ export default {
message: 'a swap with this preimage hash exists already',
code: concatErrorCode(ErrorCodePrefix.Service, 17),
}),
SWAP_HAS_INVOICE_ALREADY: (id: string): Error => ({
message: `swap ${id} has an invoice already`,
code: concatErrorCode(ErrorCodePrefix.Service, 18),
}),
REFUNDED_COINS: (transactionId: string): Error => ({
message: `refunded onchain coins: ${transactionId}`,
code: concatErrorCode(ErrorCodePrefix.Service, 19),
}),
};
2 changes: 1 addition & 1 deletion lib/service/EventHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ class EventHandler extends EventEmitter {
});

this.nursery.on('refund', (reverseSwap) => {
this.handleFailedReverseSwap(reverseSwap, Errors.ONCHAIN_HTLC_TIMED_OUT().message, SwapUpdateEvent.TransactionRefunded);
this.handleFailedReverseSwap(reverseSwap, Errors.REFUNDED_COINS(reverseSwap.transactionId).message, SwapUpdateEvent.TransactionRefunded);
});
}

Expand Down
56 changes: 37 additions & 19 deletions lib/service/Service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
getSwapOutputType,
getLightningCurrency,
getInvoicePreimageHash,
getSendingReceivingCurrency,
} from '../Utils';
import {
Balance,
Expand Down Expand Up @@ -389,6 +390,10 @@ class Service {
throw Errors.SWAP_NOT_FOUND(id);
}

if (swap.invoice) {
throw Errors.SWAP_HAS_INVOICE_ALREADY(id);
}

const { base, quote, rate: pairRate } = this.getPair(swap.pair);

const chainCurrency = getChainCurrency(base, quote, swap.orderSide, false);
Expand Down Expand Up @@ -424,7 +429,7 @@ class Service {
refundPublicKey: Buffer,
invoice: string,
) => {
const swap = await this.swapManager.swapRepository.getSwap({
let swap = await this.swapManager.swapRepository.getSwap({
invoice: {
[Op.eq]: invoice,
},
Expand All @@ -443,22 +448,32 @@ class Service {
timeoutBlockHeight,
} = await this.createSwap(pairId, orderSide, refundPublicKey, preimageHash);

// TODO: remove from database if this step fails
const {
bip21,
acceptZeroConf,
expectedAmount,
} = await this.setSwapInvoice(id, invoice);
try {
const {
bip21,
acceptZeroConf,
expectedAmount,
} = await this.setSwapInvoice(id, invoice);

return {
id,
bip21,
address,
redeemScript,
acceptZeroConf,
expectedAmount,
timeoutBlockHeight,
};
return {
id,
bip21,
address,
redeemScript,
acceptZeroConf,
expectedAmount,
timeoutBlockHeight,
};
} catch (error) {
swap = await this.swapManager.swapRepository.getSwap({
id: {
[Op.eq]: id,
},
});
await swap!.destroy();

throw error;
}
}

/**
Expand All @@ -475,11 +490,13 @@ class Service {
throw Errors.REVERSE_SWAPS_DISABLED();
}

const side = this.getOrderSide(orderSide);
const { base, quote, rate: pairRate } = this.getPair(pairId);
const { sending, receiving } = getSendingReceivingCurrency(base, quote, side);

const side = this.getOrderSide(orderSide);
const rate = getRate(pairRate, side, true);
const timeoutBlockDelta = this.timeoutDeltaProvider.getTimeout(pairId, side, true);
const onchainTimeoutBlockDelta = this.timeoutDeltaProvider.getTimeout(pairId, side, true);
const lightningTimeoutBlockDelta = TimeoutDeltaProvider.convertBlocks(sending, receiving, onchainTimeoutBlockDelta + 3);

this.verifyAmount(pairId, rate, invoiceAmount, side, true);

Expand Down Expand Up @@ -508,7 +525,8 @@ class Service {
getSwapOutputType(
true,
),
timeoutBlockDelta,
onchainTimeoutBlockDelta,
lightningTimeoutBlockDelta,
percentageFee,
);

Expand Down
17 changes: 16 additions & 1 deletion lib/service/TimeoutDeltaProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,27 @@ class TimeoutDeltaProvider {
// A map of the symbols of currencies and their block times in minutes
private static blockTimes = new Map<string, number>([
['BTC', 10],
['DOGE', 1],
['LTC', 2.5],
]);

constructor(private logger: Logger, private config: ConfigType) {}

public static convertBlocks = (fromSymbol: string, toSymbol: string, blocks: number) => {
if (!TimeoutDeltaProvider.blockTimes.has(fromSymbol)) {
throw Errors.BLOCK_TIME_NOT_FOUND(fromSymbol);
}

if (!TimeoutDeltaProvider.blockTimes.has(toSymbol)) {
throw Errors.BLOCK_TIME_NOT_FOUND(toSymbol);
}

const minutes = blocks * TimeoutDeltaProvider.blockTimes.get(fromSymbol)!;

// In the context this function is used, we calculate the timeout of the first leg of a
// reverse swap which has to be longer than the second one
return Math.ceil(minutes / TimeoutDeltaProvider.blockTimes.get(toSymbol)!);
}

public init = (pairs: PairConfig[]) => {
for (const pair of pairs) {
const pairId = getPairId(pair);
Expand Down
32 changes: 26 additions & 6 deletions lib/swap/SwapManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@ import {
splitPairId,
getHexBuffer,
getHexString,
getSwapOutputType,
getScriptHashFunction,
getSendingReceivingCurrency,
reverseBuffer,
getChainCurrency,
getSwapOutputType,
getLightningCurrency,
getScriptHashFunction,
getInvoicePreimageHash,
getSendingReceivingCurrency,
} from '../Utils';

class SwapManager {
Expand Down Expand Up @@ -65,6 +66,21 @@ class SwapManager {

const { lndClient } = this.currencies.get(lightningCurrency)!;
lndClient!.subscribeSingleInvoice(preimageHash);

} else if ((swap.status === SwapUpdateEvent.TransactionMempool || swap.status === SwapUpdateEvent.TransactionConfirmed) && isReverse) {
const { base, quote } = splitPairId(swap.pair);
const chainCurrency = getChainCurrency(base, quote, swap.orderSide, false);

const { chainClient } = this.currencies.get(chainCurrency)!;

const transactionId = reverseBuffer(getHexBuffer((swap as ReverseSwap).transactionId!));
chainClient.addInputFilter(transactionId);

if (swap.status === SwapUpdateEvent.TransactionMempool) {
const wallet = this.walletManager.wallets.get(chainCurrency)!;
chainClient.addOutputFilter(wallet.decodeAddress(swap.lockupAddress!));
}

} else {
const encodeFunction = getScriptHashFunction(getSwapOutputType(isReverse));
const outputScript = encodeFunction(getHexBuffer(swap.redeemScript));
Expand Down Expand Up @@ -94,6 +110,7 @@ class SwapManager {
[Op.or]: [
SwapUpdateEvent.SwapCreated,
SwapUpdateEvent.TransactionMempool,
SwapUpdateEvent.TransactionConfirmed,
],
},
}),
Expand Down Expand Up @@ -224,7 +241,8 @@ class SwapManager {
* @param onchainAmount amount of coins that should be sent onchain
* @param claimPublicKey public key of the keypair needed for claiming
* @param outputType type of the lockup address
* @param timeoutBlockDelta after how many blocks the onchain script should time out
* @param onchainTimeoutBlockDelta after how many blocks the onchain script should time out
* @param lightningTimeoutBlockDelta timeout delta of the last hop
* @param percentageFee the fee Boltz charges for the Swap
*/
public createReverseSwap = async (
Expand All @@ -236,7 +254,8 @@ class SwapManager {
onchainAmount: number,
claimPublicKey: Buffer,
outputType: OutputType,
timeoutBlockDelta: number,
onchainTimeoutBlockDelta: number,
lightningTimeoutBlockDelta: number,
percentageFee: number,
) => {
const { sendingCurrency, receivingCurrency } = this.getCurrencies(baseCurrency, quoteCurrency, orderSide);
Expand All @@ -252,13 +271,14 @@ class SwapManager {
const { paymentRequest } = await receivingCurrency.lndClient.addHoldInvoice(
invoiceAmount,
preimageHash,
lightningTimeoutBlockDelta,
getSwapMemo(sendingCurrency.symbol, true),
);
receivingCurrency.lndClient.subscribeSingleInvoice(preimageHash);

const { keys, index } = sendingCurrency.wallet.getNewKeys();
const { blocks } = await sendingCurrency.chainClient.getBlockchainInfo();
const timeoutBlockHeight = blocks + timeoutBlockDelta;
const timeoutBlockHeight = blocks + onchainTimeoutBlockDelta;

const redeemScript = reverseSwapScript(
preimageHash,
Expand Down
15 changes: 7 additions & 8 deletions lib/swap/SwapNursery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ interface SwapNursery {
on(event: 'coins.failedToSend', listener: (reverseSwap: ReverseSwap) => void): this;
emit(event: 'coins.failedToSend', reverseSwap: ReverseSwap): boolean;

on(event: 'refund', listener: (reverseSwap: ReverseSwap) => void): this;
emit(event: 'refund', reverseSwap: ReverseSwap): boolean;
on(event: 'refund', listener: (reverseSwap: ReverseSwap, refundTransactionId: string) => void): this;
emit(event: 'refund', reverseSwap: ReverseSwap, refundTransactionId: string): boolean;

on(event: 'invoice.settled', listener: (reverseSwap: ReverseSwap) => void): this;
emit(event: 'invoice.settled', reverseSwap: ReverseSwap): boolean;
Expand Down Expand Up @@ -342,7 +342,7 @@ class SwapNursery extends EventEmitter {
await currency.chainClient.estimateFee(),
true,
);
const minerFee = await this.calculateTransactionFee(currency.chainClient, claimTx, output.value);
const minerFee = await this.calculateTransactionFee(currency.chainClient, claimTx);

this.logger.silly(`Broadcasting ${currency.symbol} claim transaction: ${claimTx.getId()}`);

Expand Down Expand Up @@ -441,12 +441,12 @@ class SwapNursery extends EventEmitter {
blockHeight,
await chainClient.estimateFee(),
);
const minerFee = await this.calculateTransactionFee(chainClient, lockupTransaction, lockupTransaction.outs[vout].value);
const minerFee = await this.calculateTransactionFee(chainClient, refundTransaction, lockupTransaction.outs[vout].value);

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

await chainClient.sendRawTransaction(refundTransaction.toHex());
this.emit('refund', await this.reverseSwapRepository.setTransactionRefunded(reverseSwap, minerFee));
this.emit('refund', await this.reverseSwapRepository.setTransactionRefunded(reverseSwap, minerFee), refundTransaction.getId());
}

private cancelInvoice = (lndClient: LndClient, preimageHash: Buffer) => {
Expand Down Expand Up @@ -487,7 +487,7 @@ class SwapNursery extends EventEmitter {
const rawInputTransaction = await chainClient.getRawTransaction(inputId);
const inputTransaction = Transaction.fromHex(rawInputTransaction);

const relevantOutput = inputTransaction.outs[input.index] as TxOutput;
const relevantOutput = inputTransaction.outs[input.index];

queriedInputSum += relevantOutput.value;
}
Expand All @@ -497,8 +497,7 @@ class SwapNursery extends EventEmitter {

let fee = inputSum || await queryInputSum();

transaction.outs.forEach((out) => {
const output = out as TxOutput;
transaction.outs.forEach((output) => {
fee -= output.value;
});

Expand Down
Loading

0 comments on commit e8e2591

Please sign in to comment.