Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refund failed swaps #519

Merged
merged 41 commits into from
Jan 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
e2feadc
validate if address received from Boltz in submarine swap matches the…
bordalix Dec 26, 2023
12f7cab
Merge branch 'master' into refund_failed_swaps
bordalix Dec 26, 2023
58b1af4
Adds menu swaps
bordalix Dec 26, 2023
7ddceaa
boltz: add Refund method
tiero Dec 26, 2023
710dd8d
format
tiero Dec 26, 2023
417c250
WIP: allows user to refund failed swaps
bordalix Dec 27, 2023
1440cef
get key pair from refund public key
bordalix Dec 27, 2023
aac3c16
wip
bordalix Dec 28, 2023
dc8a6d1
bug fixes
bordalix Dec 28, 2023
ba5f83e
New utils function swapEndian
bordalix Dec 28, 2023
36208fb
Shows UTXO on console.log
bordalix Dec 28, 2023
e6e33ba
remove hack to force fail swaps
bordalix Dec 28, 2023
f8b5fbd
mini refactor
bordalix Dec 28, 2023
787f678
lint
bordalix Dec 28, 2023
80d12ed
check if timelock already expired
bordalix Dec 28, 2023
73032e1
change swapsRepository to refundableSwapsRepository
bordalix Dec 28, 2023
a0ae70a
rename swapParams to refundableSwapParams
bordalix Dec 28, 2023
7f9d80d
works on tx list, show refundable tx
bordalix Dec 29, 2023
2726e17
works on tx list, show refundable tx
bordalix Dec 29, 2023
621a05a
Removes refundable swaps from cache, we don't need it
bordalix Dec 29, 2023
b7d7d59
remove hack to fail swaps
bordalix Dec 29, 2023
5e8998d
remove console.logs
bordalix Dec 29, 2023
ca20b7c
tests
bordalix Dec 29, 2023
bb9cd3d
fix key pair generation
bordalix Dec 29, 2023
47fe1e5
fix bug with refund
bordalix Dec 31, 2023
3ec0511
works on boltz tests
bordalix Dec 31, 2023
0336551
try to find derivation path on next addesses
bordalix Jan 1, 2024
fee78cd
bug fix
bordalix Jan 2, 2024
4bac8c2
remove console.logs
bordalix Jan 2, 2024
7439dd9
back to Buffer.of()
bordalix Jan 2, 2024
37404d4
ignore, just debugging CI
bordalix Jan 2, 2024
8d51351
remove debug code
bordalix Jan 2, 2024
2d3279f
fix merge conflicts
bordalix Jan 2, 2024
8c7e843
fix test
bordalix Jan 2, 2024
4e31341
fix test?
bordalix Jan 2, 2024
2863d27
reverse changes
bordalix Jan 2, 2024
664a56c
fix test?
bordalix Jan 2, 2024
6d8476f
fix test?
bordalix Jan 2, 2024
4768224
fix test?
bordalix Jan 2, 2024
500b6df
debug, please ignore
bordalix Jan 2, 2024
a43a5d7
fix test? yes
bordalix Jan 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions public/assets/images/lightning-circle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions src/application/updater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ import type {
AssetRepository,
BlockheadersRepository,
Outpoint,
RefundableSwapsRepository,
} from '../domain/repository';
import { TxIDsKey } from '../infrastructure/storage/wallet-repository';
import type { NetworkString, UnblindingData } from 'marina-provider';
import { AppStorageKeys } from '../infrastructure/storage/app-repository';
import type { ChainSource } from '../domain/chainsource';
import { DefaultAssetRegistry } from '../port/asset-registry';
import { AccountFactory } from './account';
import { addressFromScript } from '../extension/utility/address';

/**
* Updater is a class that listens to the chrome storage changes and triggers the right actions
Expand All @@ -35,6 +37,7 @@ export class UpdaterService {
private appRepository: AppRepository,
private blockHeadersRepository: BlockheadersRepository,
private assetRepository: AssetRepository,
private refundableSwapsRepository: RefundableSwapsRepository,
zkpLib: confidential.Confidential['zkp']
) {
this.unblinder = new WalletRepositoryUnblinder(
Expand Down Expand Up @@ -85,6 +88,24 @@ export class UpdaterService {
}
}

async checkRefundableSwaps(network: NetworkString) {
this.processingCount += 1;
try {
const swaps = await this.refundableSwapsRepository.getSwaps();
if (swaps.length === 0) return;
const chainSource = await this.appRepository.getChainSource(network);
if (!chainSource) throw new Error('Chain source not found for network ' + network);
for (const swap of swaps) {
if (!swap.redeemScript || swap.network !== network) return;
const fundingAddress = addressFromScript(swap.redeemScript);
const [utxo] = await chainSource.listUnspents(fundingAddress);
if (!utxo) await this.refundableSwapsRepository.removeSwap(swap);
}
} finally {
this.processingCount -= 1;
}
}

private async updateLastestHistory(network: NetworkString) {
const LAST_ADDRESSES_COUNT = 30;
const chainSource = await this.appRepository.getChainSource(network);
Expand Down
10 changes: 10 additions & 0 deletions src/application/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
export function h2b(hex: string): Buffer {
return Buffer.from(hex, 'hex');
}

export function num2hex(num: number): string {
let hex = num.toString(16);
if (hex.length % 2 > 0) hex = '0' + hex;
return hex;
}

export function swapEndian(hex: string): string {
return Buffer.from(hex, 'hex').reverse().toString('hex');
}
3 changes: 3 additions & 0 deletions src/background/background-script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { BlockHeadersAPI } from '../infrastructure/storage/blockheaders-reposito
import type { ChainSource } from '../domain/chainsource';
import { WalletRepositoryUnblinder } from '../application/unblinder';
import { Transaction } from 'liquidjs-lib';
import { RefundableSwapsStorageAPI } from '../infrastructure/storage/refundable-swaps-repository';

// manifest v2 needs BrowserAction, v3 needs action
const action = Browser.browserAction ?? Browser.action;
Expand All @@ -43,6 +44,7 @@ const backgroundPort = getBackgroundPortImplementation();
const walletRepository = new WalletStorageAPI();
const appRepository = new AppStorageAPI();
const assetRepository = new AssetStorageAPI(walletRepository);
const refundableSwapsRepository = new RefundableSwapsStorageAPI();
const taxiRepository = new TaxiStorageAPI(assetRepository, appRepository);
const blockHeadersRepository = new BlockHeadersAPI();

Expand All @@ -51,6 +53,7 @@ const updaterService = new UpdaterService(
appRepository,
blockHeadersRepository,
assetRepository,
refundableSwapsRepository,
zkpLib
);
const subscriberService = new SubscriberService(
Expand Down
37 changes: 37 additions & 0 deletions src/domain/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,3 +282,40 @@ export async function initWalletRepository(
defaultMainAccountXPubTestnet,
};
}

export interface RefundableSwapParams {
blindingKey: string;
confidentialAddress?: string;
derivationPath?: string;
fundingAddress?: string;
id?: string;
network?: NetworkString;
redeemScript: string;
refundPublicKey?: string;
timeoutBlockHeight?: number;
timestamp?: number;
txid?: string;
}

export interface RefundableSwapsRepository {
addSwap(swap: RefundableSwapParams): Promise<void>;
findSwapWithAddress(address: string): Promise<RefundableSwapParams | undefined>;
findSwapWithTxid(txid: string): Promise<RefundableSwapParams | undefined>;
getSwaps(): Promise<RefundableSwapParams[]>;
updateSwap(swap: RefundableSwapParams): Promise<void>;
removeSwap(swap: RefundableSwapParams): Promise<void>;
}

export enum RefundSwapFlowStep {
None,
ParamsEntered,
Confirm,
}

// this repository is used to cache data during the UI refund swap flow
export interface RefundSwapFlowRepository {
reset(): Promise<void>;
getParams(): Promise<RefundableSwapParams | undefined>;
setParams(params: RefundableSwapParams | undefined): Promise<void>;
getStep(): Promise<RefundSwapFlowStep>;
}
65 changes: 47 additions & 18 deletions src/extension/components/button-transaction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import Button from './button';
import Browser from 'webextension-polyfill';
import type { Asset } from 'marina-provider';
import { useStorageContext } from '../context/storage-context';
import type { RefundableSwapParams } from '../../domain/repository';
import { useHistory } from 'react-router';
import { SETTINGS_MENU_SWAPS_ROUTE } from '../routes/constants';

function txTypeFromTransfer(transfer?: number): TxType {
if (transfer === undefined) return TxType.Unknow;
Expand All @@ -20,12 +23,14 @@ function txTypeFromTransfer(transfer?: number): TxType {
}

interface Props {
txDetails: TxDetailsExtended;
assetSelected: Asset;
swap: RefundableSwapParams | undefined;
txDetails: TxDetailsExtended;
}

const ButtonTransaction: React.FC<Props> = ({ txDetails, assetSelected }) => {
const { walletRepository, appRepository, cache } = useStorageContext();
const ButtonTransaction: React.FC<Props> = ({ assetSelected, swap, txDetails }: Props) => {
const history = useHistory();
const { appRepository, cache, refundSwapFlowRepository, walletRepository } = useStorageContext();
const [modalOpen, setModalOpen] = useState(false);

const handleClick = () => {
Expand All @@ -42,9 +47,17 @@ const ButtonTransaction: React.FC<Props> = ({ txDetails, assetSelected }) => {
await Browser.tabs.create({ url, active: false });
};

const handleRefund = async () => {
await refundSwapFlowRepository.setParams(swap);
history.push(SETTINGS_MENU_SWAPS_ROUTE);
};

const transferAmount = () => txDetails.txFlow[assetSelected.assetHash];
const transferAmountIsDefined = () => transferAmount() !== undefined;

const confirmed =
txDetails.height && txDetails.height >= 0 && cache?.blockHeaders.value[txDetails.height];

return (
<>
<button
Expand All @@ -54,27 +67,32 @@ const ButtonTransaction: React.FC<Props> = ({ txDetails, assetSelected }) => {
>
<div className="flex items-center">
<TxIcon txType={txTypeFromTransfer(transferAmount())} />
{txDetails.height &&
txDetails.height >= 0 &&
cache?.blockHeaders.value[txDetails.height] ? (
<span className="text-grayDark items-center mr-2 text-xs font-medium text-left">
{txDetails.height && confirmed ? (
<span className="text-grayDark items-center text-xs font-medium text-left">
{moment(cache.blockHeaders.value[txDetails.height].timestamp * 1000).format(
'DD MMM YYYY'
)}
</span>
) : (
<span className="bg-red text-xxs inline-flex items-center justify-center px-1 py-1 font-semibold leading-none text-white rounded-full">
<span className="bg-red text-xxs inline-flex px-1 py-1 font-semibold leading-none text-white rounded-full">
unconfirmed
</span>
)}
</div>
<div className="flex">
<div className="text-primary whitespace-nowrap text-sm font-medium">
{transferAmountIsDefined() ? (transferAmount() > 0 ? '+' : '') : ''}
{transferAmountIsDefined()
? formatDecimalAmount(fromSatoshi(transferAmount(), assetSelected.precision), false)
: '??'}{' '}
{assetSelected.ticker}
<div className="flex items-center">
<div className="flex flex-col">
<span className="text-primary whitespace-nowrap text-sm font-medium">
{transferAmountIsDefined() ? (transferAmount() > 0 ? '+' : '') : ''}
{transferAmountIsDefined()
? formatDecimalAmount(fromSatoshi(transferAmount(), assetSelected.precision), false)
: '??'}{' '}
{assetSelected.ticker}
</span>
{swap && confirmed && (
<span className="bg-smokeLight text-xxs px-1 py-0 font-semibold text-white rounded-full">
Refundable
</span>
)}
</div>
<img src="assets/images/chevron-right.svg" alt="chevron-right" />
</div>
Expand Down Expand Up @@ -132,9 +150,20 @@ const ButtonTransaction: React.FC<Props> = ({ txDetails, assetSelected }) => {
<p className="wrap text-xs font-light break-all">{txDetails.txid}</p>
</div>
</div>
<Button className="w-full" onClick={() => handleOpenExplorer()}>
See in Explorer
</Button>
{swap ? (
<div className="flex justify-between">
<Button
isOutline={true}
className="bg-secondary hover:bg-secondary-light"
onClick={handleRefund}
>
Refund
</Button>
<Button onClick={handleOpenExplorer}>Explorer</Button>
</div>
) : (
<Button onClick={handleOpenExplorer}>See in Explorer</Button>
)}
</Modal>
</>
);
Expand Down
15 changes: 14 additions & 1 deletion src/extension/components/modal-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
SETTINGS_MENU_INFO_ROUTE,
SETTINGS_MENU_SECURITY_ROUTE,
SETTINGS_MENU_SETTINGS_ROUTE,
SETTINGS_MENU_SWAPS_ROUTE,
} from '../routes/constants';
import { useBackgroundPortContext } from '../context/background-port-context';
import { useStorageContext } from '../context/storage-context';
Expand All @@ -25,6 +26,7 @@ const ModalMenu: React.FC<Props> = ({ isOpen, handleClose }) => {
useOnClickOutside(ref, useCallback(handleClose, [ref, handleClose]));
const handleSecurity = () => history.push(SETTINGS_MENU_SECURITY_ROUTE);
const handleSettings = () => history.push(SETTINGS_MENU_SETTINGS_ROUTE);
const handleSwaps = () => history.push(SETTINGS_MENU_SWAPS_ROUTE);
const handleInfo = () => history.push(SETTINGS_MENU_INFO_ROUTE);
const handleLogOut = async () => {
await appRepository.updateStatus({ isAuthenticated: false });
Expand All @@ -39,7 +41,7 @@ const ModalMenu: React.FC<Props> = ({ isOpen, handleClose }) => {
return (
<div className="bg-smokeLight fixed inset-0 z-50 flex pr-2">
<div
className="rounded-xl w-36 h-44 top-10 relative flex flex-col px-6 py-4 ml-auto bg-white"
className="rounded-xl w-36 top-10 relative flex flex-col h-48 px-6 py-4 ml-auto bg-white"
ref={ref}
>
<ul className="flex flex-col justify-between h-full">
Expand All @@ -57,6 +59,17 @@ const ModalMenu: React.FC<Props> = ({ isOpen, handleClose }) => {
</li>
</button>

<button onClick={handleSwaps}>
<li className="flex flex-row items-center gap-2">
<img
className="w-6 h-6"
src="assets/images/lightning-circle.svg"
alt="lightning swaps"
/>
<span className="font-regular text-sm">Swaps</span>
</li>
</button>

<button onClick={handleInfo}>
<li className="flex flex-row items-center gap-2">
<img className="w-6 h-6" src="assets/images/information-circle.svg" alt="info" />
Expand Down
3 changes: 3 additions & 0 deletions src/extension/components/shell-popup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const ShellPopUp: React.FC<Props> = ({
blockHeadersRepository,
appRepository,
sendFlowRepository,
refundableSwapsRepository,
cache,
} = useStorageContext();
const [isRestorerLoading, setIsRestorerLoading] = useState(false);
Expand Down Expand Up @@ -75,10 +76,12 @@ const ShellPopUp: React.FC<Props> = ({
appRepository,
blockHeadersRepository,
assetRepository,
refundableSwapsRepository,
await zkp()
);
if (!cache?.network) throw new Error('Network not found');
await updater.checkAndFixMissingTransactionsData(cache.network);
await updater.checkRefundableSwaps(cache.network);
} catch (e) {
console.error(e);
} finally {
Expand Down
12 changes: 12 additions & 0 deletions src/extension/context/storage-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import type {
BlockheadersRepository,
OnboardingRepository,
SendFlowRepository,
RefundSwapFlowRepository,
RefundableSwapsRepository,
TaxiRepository,
WalletRepository,
} from '../../domain/repository';
Expand All @@ -18,6 +20,8 @@ import { WalletStorageAPI } from '../../infrastructure/storage/wallet-repository
import type { PresentationCache } from '../../domain/presenter';
import { PresenterImpl } from '../../application/presenter';
import { useToastContext } from './toast-context';
import { RefundableSwapsStorageAPI } from '../../infrastructure/storage/refundable-swaps-repository';
import { RefundSwapFlowStorageAPI } from '../../infrastructure/storage/refund-swap-flow-repository';

const walletRepository = new WalletStorageAPI();
const appRepository = new AppStorageAPI();
Expand All @@ -26,6 +30,8 @@ const taxiRepository = new TaxiStorageAPI(assetRepository, appRepository);
const onboardingRepository = new OnboardingStorageAPI();
const sendFlowRepository = new SendFlowStorageAPI();
const blockHeadersRepository = new BlockHeadersAPI();
const refundableSwapsRepository = new RefundableSwapsStorageAPI();
const refundSwapFlowRepository = new RefundSwapFlowStorageAPI();

interface StorageContextProps {
walletRepository: WalletRepository;
Expand All @@ -36,6 +42,8 @@ interface StorageContextProps {
sendFlowRepository: SendFlowRepository;
blockHeadersRepository: BlockheadersRepository;
cache?: PresentationCache;
refundableSwapsRepository: RefundableSwapsRepository;
refundSwapFlowRepository: RefundSwapFlowRepository;
}

const StorageContext = createContext<StorageContextProps>({
Expand All @@ -46,6 +54,8 @@ const StorageContext = createContext<StorageContextProps>({
onboardingRepository,
sendFlowRepository,
blockHeadersRepository,
refundableSwapsRepository,
refundSwapFlowRepository,
});

const presenter = new PresenterImpl(
Expand Down Expand Up @@ -84,6 +94,8 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
sendFlowRepository,
blockHeadersRepository,
cache,
refundableSwapsRepository,
refundSwapFlowRepository,
}}
>
{children}
Expand Down
2 changes: 2 additions & 0 deletions src/extension/onboarding/end-of-flow/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const EndOfFlowOnboarding: React.FC = () => {
walletRepository,
assetRepository,
blockHeadersRepository,
refundableSwapsRepository,
} = useStorageContext();
const isFromPopup = useSelectIsFromPopupFlow();

Expand Down Expand Up @@ -89,6 +90,7 @@ const EndOfFlowOnboarding: React.FC = () => {
appRepository,
blockHeadersRepository,
assetRepository,
refundableSwapsRepository,
await zkp()
);
walletRepository.onNewTransaction(() => {
Expand Down
3 changes: 3 additions & 0 deletions src/extension/routes/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ const SETTINGS_EXPLORER_ROUTE = '/settings/settings/explorer';
const SETTINGS_EXPLORER_CUSTOM_ROUTE = '/settings/settings/explorer/custom';
const SETTINGS_NETWORKS_ROUTE = '/settings/settings/networks';

const SETTINGS_MENU_SWAPS_ROUTE = '/settings/swaps';

const SETTINGS_MENU_INFO_ROUTE = '/settings/info';
const SETTINGS_CREDITS_ROUTE = '/settings/info/credits';
const SETTINGS_TERMS_ROUTE = '/settings/info/terms-of-service';
Expand Down Expand Up @@ -105,4 +107,5 @@ export {
CONNECT_CREATE_ACCOUNT,
SETTINGS_EXPLORER_CUSTOM_ROUTE,
SETTINGS_ACCOUNTS_RESTORE_IONIO_ROUTE,
SETTINGS_MENU_SWAPS_ROUTE,
};
Loading
Loading