Skip to content

Commit

Permalink
Add Lighting Network support via Boltz submarine swaps (#482)
Browse files Browse the repository at this point in the history
* Add Networks bottom sheet modal

* Add screens and logic

* rm comments + small fixes

* Review fixes

* Fix popup test

* reverse submarine swap wip

* Blind pset wip

* wip

* refactor into BoltzService class

* refactor into BoltzService class

* refactor into BoltzService class

* refactor into BoltzService class

* refactor

* fix receiving address

* fix boltz liquid api endpoint

* Fix getInvoiceExpireDate()

There's a bug on boltz mainnet api, where invoices DON'T include field `timeExpireDate`, resulting in invoices from mainnet to be considered expired.

BoltzExchange/boltz-backend#437

This changes try to find out the correct expiration date cycling through different strategies and returning by default `Date.now() + 3,600,000`

* fix bug on claim tx signing: wrong preimage

* add working test

* fix bug: using wrong brodcast tx function

* add finalize

* removes don't needed finalizer

* new boltz testnet api endpoint

* Removes DEFAULT_LIGHTNING_LIMITS
Always query boltz api for swap limits
Show spinner while querying boltz api for limits

* throw error if invoice has no timestamp

* removes nsequence and block height timeout

* fix variable name

* removes timeoutBlockHeight from makeClaimTransaction params: not needed anymore

* fix error message

* fix bug with claim transaction

* fix success message after paying lightning invoice

* update yarn.lock

* update caniuse-lite

* always show lbtc, fusd and usdt on list of assets

* fix playwright test

* change success message

* removes modal unlock from receiving with LN

* remove space before bang

* fix alignment of SEND ALL button

* fix bug where closing popup when entering lightning invoice and re-opening it would send user to liquid send

* bug fix: on send don't open network modal for new_asset

* calculate fee for claim tx

* fix error when extracting axios error message

* fix test error

* warn user if he's spending too much with ln invoice

* fix console error when history pushing for the same pathname

* Several bug fixes and improvements:
- validate all values in satoshis
- show boltz fees right to invoice value
- warning of low funds doesn't disable proceed button

* show swap fees on new line

* change 'Value' to 'Invoice value'

* change invoice value unit from L-BTC to BTC

* changes multiple of relay fee from 2 to 1.1

---------

Co-authored-by: João Bordalo <bordalix@users.noreply.github.com>
  • Loading branch information
Janaka-Steph and bordalix authored Dec 20, 2023
1 parent 7eb7f5a commit 265a362
Show file tree
Hide file tree
Showing 30 changed files with 1,467 additions and 46 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"bip32": "^3.0.1",
"bip39": "^3.0.4",
"bitcoinjs-message": "^2.2.0",
"bolt11": "^1.4.1",
"buffer": "^6.0.3",
"coinselect": "^3.1.13",
"crypto-browserify": "^3.12.0",
Expand Down
3 changes: 2 additions & 1 deletion playwright-tests/popup.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ pwTest(
await page.waitForSelector('text=1.00 000 000 L-BTC');
await page.getByRole('button', { name: 'Send' }).click(); // go to send
await page.getByText('Liquid Bitcoin').click(); // select L-BTC
await page.getByText('Liquid Network').click();
await page
.getByPlaceholder('el1...')
.fill(
Expand All @@ -78,7 +79,7 @@ pwTest(
await page.waitForSelector('text=Unlock');
await page.getByPlaceholder('Password').fill(PASSWORD);
await page.getByRole('button', { name: 'Unlock' }).click();
await page.waitForSelector('text=Payment successful !');
await page.waitForSelector('text=Payment successful!');
await page.waitForTimeout(2000);

await page.getByAltText('marina logo').click(); // go to home page
Expand Down
9 changes: 9 additions & 0 deletions public/assets/images/networks/lightning.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions public/assets/images/networks/liquid.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 5 additions & 1 deletion src/application/presenter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { TxDetails } from '../domain/transaction';
import { computeBalances, computeTxDetailsExtended } from '../domain/transaction';
import type { BlockHeader } from '../domain/chainsource';
import { MainAccount, MainAccountLegacy, MainAccountTest } from './account';
import { alwaysPresentAssets } from '../domain/constants';

function createLoadingValue<T>(value: T): LoadingValue<T> {
return {
Expand Down Expand Up @@ -295,10 +296,13 @@ export class PresenterImpl implements Presenter {
(acc, tx) => [...acc, ...Object.keys(tx.txFlow)],
[] as string[]
);

return {
...this.state,
transactions: setValue(extendedTxDetails.sort(sortTxDetails)),
walletAssets: setValue(new Set(assetsInTransactions)),
walletAssets: setValue(
new Set([...alwaysPresentAssets[this.state.network], ...assetsInTransactions])
),
};
}

Expand Down
6 changes: 6 additions & 0 deletions src/domain/chainsource.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { Utxo } from 'marina-provider';

export type TransactionHistory = Array<{
tx_hash: string;
height: number;
Expand All @@ -11,6 +13,8 @@ export interface BlockHeader {
height: number;
}

export type Unspent = Omit<Utxo, 'scriptDetails'>;

export interface ChainSource {
subscribeScriptStatus(
script: Buffer,
Expand All @@ -24,4 +28,6 @@ export interface ChainSource {
broadcastTransaction(hex: string): Promise<string>;
getRelayFee(): Promise<number>;
close(): Promise<void>;
waitForAddressReceivesTx(addr: string): Promise<void>;
listUnspents(addr: string): Promise<Unspent[]>;
}
39 changes: 28 additions & 11 deletions src/domain/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,39 +7,54 @@ export const SOMETHING_WENT_WRONG_ERROR = 'Oops, something went wrong...';
const getLocalImagePath = (asset: string) => `/assets/images/liquid-assets/${asset}`;

// featured assets
const featuredAssets = {
export const featuredAssets = {
lbtc: {
mainnet: networks.liquid.assetHash,
liquid: networks.liquid.assetHash,
testnet: networks.testnet.assetHash,
regtest: networks.regtest.assetHash,
},
lcad: {
mainnet: '0e99c1a6da379d1f4151fb9df90449d40d0608f6cb33a5bcbfc8c265f42bab0a',
liquid: '0e99c1a6da379d1f4151fb9df90449d40d0608f6cb33a5bcbfc8c265f42bab0a',
testnet: 'ac3e0ff248c5051ffd61e00155b7122e5ebc04fd397a0ecbdd4f4e4a56232926',
regtest: '',
},
usdt: {
mainnet: 'ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2',
liquid: 'ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2',
testnet: 'f3d1ec678811398cd2ae277cbe3849c6f6dbd72c74bc542f7c4b11ff0e820958',
regtest: '',
},
fusd: {
mainnet: '0dea022a8a25abb128b42b0f8e98532bc8bd74f8a77dc81251afcc13168acef7',
liquid: '0dea022a8a25abb128b42b0f8e98532bc8bd74f8a77dc81251afcc13168acef7',
testnet: '0d86b2f6a8c3b02a8c7c8836b83a081e68b7e2b4bcdfc58981fc5486f59f7518',
regtest: '',
},
};

export const alwaysPresentAssets = {
liquid: [
'6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d', // lbtc
'0dea022a8a25abb128b42b0f8e98532bc8bd74f8a77dc81251afcc13168acef7', // fusd
'ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2', // usdt
],
regtest: [],
testnet: [
'144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49', // lbtc
'0d86b2f6a8c3b02a8c7c8836b83a081e68b7e2b4bcdfc58981fc5486f59f7518', // fusd
'f3d1ec678811398cd2ae277cbe3849c6f6dbd72c74bc542f7c4b11ff0e820958', // usdt
],
};

// featured assets map: from an asset hash, get local image path
const featuredAssetsMap = new Map<string, string>();
featuredAssetsMap.set(featuredAssets.lbtc.mainnet, getLocalImagePath('lbtc.png'));
featuredAssetsMap.set(featuredAssets.lbtc.liquid, getLocalImagePath('lbtc.png'));
featuredAssetsMap.set(featuredAssets.lbtc.testnet, getLocalImagePath('lbtc.png'));
featuredAssetsMap.set(featuredAssets.lbtc.regtest, getLocalImagePath('lbtc.png'));
featuredAssetsMap.set(featuredAssets.usdt.mainnet, getLocalImagePath('usdt.png'));
featuredAssetsMap.set(featuredAssets.usdt.liquid, getLocalImagePath('usdt.png'));
featuredAssetsMap.set(featuredAssets.usdt.testnet, getLocalImagePath('usdt.png'));
featuredAssetsMap.set(featuredAssets.lcad.mainnet, getLocalImagePath('lcad.png'));
featuredAssetsMap.set(featuredAssets.lcad.liquid, getLocalImagePath('lcad.png'));
featuredAssetsMap.set(featuredAssets.lcad.testnet, getLocalImagePath('lcad.png'));
featuredAssetsMap.set(featuredAssets.fusd.testnet, getLocalImagePath('fusd.png'));
featuredAssetsMap.set(featuredAssets.fusd.mainnet, getLocalImagePath('fusd.png'));

export const FEATURED_ASSETS = Array.from(featuredAssetsMap.keys());
featuredAssetsMap.set(featuredAssets.fusd.liquid, getLocalImagePath('fusd.png'));

// given an asset hash, return url for image path from mempool
const getRemoteImagePath = (hash: string) => `https://liquid.network/api/v1/asset/${hash}/icon`;
Expand All @@ -52,4 +67,6 @@ export function getAssetImagePath(assetHash: string): string {
return getRemoteImagePath(assetHash);
}

export const UNKNOWN_ASSET_HASH = 'new_asset';

export const defaultPrecision = 8;
2 changes: 1 addition & 1 deletion src/domain/pset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ export class PsetBuilder {
if (!chainSource) throw new Error('chain source not set');
// we add 100% to the min relay fee in order to be sure that the transaction will be accepted by the network
// some inputs and outputs may be added later to pay the fees
const relayFee = (await chainSource.getRelayFee()) * 2;
const relayFee = (await chainSource.getRelayFee()) * 1.1;
await chainSource.close();
const sats1000Bytes = relayFee * 10 ** 8;
const estimatedSize = estimateVirtualSize(updater.pset, true);
Expand Down
2 changes: 2 additions & 0 deletions src/domain/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ export enum SendFlowStep {
AssetSelected,
AddressAmountFormDone,
FeeFormDone,
Lightning,
}

// this repository is used to cache data during the UI send flow
Expand All @@ -206,6 +207,7 @@ export interface SendFlowRepository {
setUnsignedPset(pset: string): Promise<void>;
getUnsignedPset(): Promise<string | undefined>;
getStep(): Promise<SendFlowStep>;
setLightning(bool: boolean): Promise<void>;
}

// this repository aims to cache the block headers
Expand Down
2 changes: 1 addition & 1 deletion src/extension/components/address-amount-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ const BaseForm = (props: FormProps & FormikProps<FormValues>) => {
<div className="text-primary text-right">
<button
onClick={setMaxAmount}
className="background-transparent focus:outline-none px-3 py-1 mt-1 mb-1 mr-1 text-xs font-bold uppercase transition-all duration-150 ease-linear outline-none"
className="background-transparent focus:outline-none py-1 mt-1 mb-1 text-xs font-bold uppercase transition-all duration-150 ease-linear outline-none"
type="button"
>
SEND ALL
Expand Down
31 changes: 27 additions & 4 deletions src/extension/components/asset-list-screen.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import { useHistory } from 'react-router';
import { DEFAULT_ROUTE } from '../routes/constants';
import ButtonAsset from './button-asset';
import InputIcon from './input-icon';
import ShellPopUp from './shell-popup';
import ButtonList from './button-list';
import type { Asset } from 'marina-provider';
import type { Asset, NetworkString } from 'marina-provider';
import ModalSelectNetwork from './modal-select-network';
import { networks } from 'liquidjs-lib';

export interface AssetListProps {
assets: Array<Asset>; // the assets to display
onClick: (assetHash: string) => Promise<void>;
network: NetworkString;
onClick: (assetHash: string, isSubmarineSwap: boolean) => Promise<void>;
balances?: Record<string, number>;
title: string;
emptyText?: string;
Expand All @@ -19,11 +22,16 @@ const AssetListScreen: React.FC<AssetListProps> = ({
title,
onClick,
assets,
network,
balances,
emptyText,
}) => {
const history = useHistory();

// bottom sheet modal
const [showBottomSheet, setShowBottomSheet] = useState(false);
const [selectedAsset, setSelectedAsset] = useState('');

useEffect(() => {
setSearchResults(assets);
}, [assets]);
Expand Down Expand Up @@ -54,6 +62,15 @@ const AssetListScreen: React.FC<AssetListProps> = ({
setSearchTerm(searchTerm);
};

const handleClick = async ({ assetHash }: any) => {
if (assetHash === networks[network].assetHash) {
setShowBottomSheet(true);
setSelectedAsset(assetHash);
} else {
await onClick(assetHash as string, false);
}
};

const handleBackBtn = () => {
history.push(DEFAULT_ROUTE);
};
Expand All @@ -80,11 +97,17 @@ const AssetListScreen: React.FC<AssetListProps> = ({
asset={asset}
quantity={balances ? balances[asset.assetHash] : undefined}
key={index}
handleClick={({ assetHash }) => onClick(assetHash)}
handleClick={handleClick}
/>
))}
</ButtonList>
</div>
<ModalSelectNetwork
isOpen={showBottomSheet}
onClose={() => setShowBottomSheet(false)}
onLightning={() => onClick(selectedAsset, true)}
onLiquid={() => onClick(selectedAsset, false)}
></ModalSelectNetwork>
</ShellPopUp>
);
};
Expand Down
46 changes: 46 additions & 0 deletions src/extension/components/modal-select-network.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React, { useRef } from 'react';
import useOnClickOutside from '../hooks/use-onclick-outside';

interface Props {
isOpen: boolean;
onClose: () => any;
onLightning: any;
onLiquid: any;
}

const ModalSelectNetwork: React.FC<Props> = ({ isOpen, onClose, onLightning, onLiquid }) => {
const ref = useRef<HTMLDivElement | null>(null);
useOnClickOutside(ref, onClose);

if (!isOpen) return <></>;

return (
<div className="fixed bottom-0 z-50 flex">
<div className="min-h-60 p-8 m-auto bg-white rounded-t-lg shadow-md" ref={ref}>
<div className="flex flex-col justify-between flex-1">
<h1 className="mb-4 text-lg">Select network</h1>
<div className="flex justify-center">
<div className="h-15 p-2 cursor-pointer" onClick={onLiquid}>
<img
className="h-10 mt-0.5 block mx-auto mb-2"
src="assets/images/networks/liquid.svg"
alt="liquid network logo"
/>
<p className="text-xs">Liquid Network</p>
</div>
<div className="h-15 p-2 cursor-pointer" onClick={onLightning}>
<img
className="h-10 mt-0.5 block mx-auto mb-2"
src="assets/images/networks/lightning.svg"
alt="lightning network logo"
/>
<p className="text-xs">Lightning Network</p>
</div>
</div>
</div>
</div>
</div>
);
};

export default ModalSelectNetwork;
7 changes: 7 additions & 0 deletions src/extension/routes/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ const SEND_END_OF_FLOW_ROUTE = '/send/end-of-flow';
const SEND_PAYMENT_SUCCESS_ROUTE = '/send/payment-success';
const SEND_PAYMENT_ERROR_ROUTE = '/send/payment-error';

// Lightning Receive
const LIGHTNING_ENTER_AMOUNT_ROUTE = '/lightning/invoice-amount';
const LIGHTNING_ENTER_INVOICE_ROUTE = '/lightning/invoice';

// Settings
const SETTINGS_MENU_SECURITY_ROUTE = '/settings/security';
const SETTINGS_SHOW_MNEMONIC_ROUTE = '/settings/security/show-mnemonic';
Expand Down Expand Up @@ -82,6 +86,9 @@ export {
SEND_END_OF_FLOW_ROUTE,
SEND_PAYMENT_SUCCESS_ROUTE,
SEND_PAYMENT_ERROR_ROUTE,
// Lightning Receive
LIGHTNING_ENTER_AMOUNT_ROUTE,
LIGHTNING_ENTER_INVOICE_ROUTE,
// Settings
SETTINGS_MENU_SECURITY_ROUTE,
SETTINGS_MENU_SETTINGS_ROUTE,
Expand Down
Loading

0 comments on commit 265a362

Please sign in to comment.