961 lines
32 KiB
TypeScript
961 lines
32 KiB
TypeScript
import { action, observable, computed, runInAction, reaction } from 'mobx';
|
|
import ReactNativeBlobUtil from 'react-native-blob-util';
|
|
import { ECPairAPI, ECPairFactory } from 'ecpair';
|
|
import ecc from '@bitcoinerlab/secp256k1';
|
|
import { crypto, initEccLib } from 'bitcoinjs-lib';
|
|
import { HDKey } from '@scure/bip32';
|
|
import {
|
|
validateMnemonic,
|
|
mnemonicToSeedSync,
|
|
generateMnemonic
|
|
} from '@scure/bip39';
|
|
|
|
import { themeColor } from '../utils/ThemeUtils';
|
|
import { localeString } from '../utils/LocaleUtils';
|
|
import { BIP39_WORD_LIST } from '../utils/Bip39Utils';
|
|
import {
|
|
SWAPS_KEY,
|
|
REVERSE_SWAPS_KEY,
|
|
SWAPS_RESCUE_KEY,
|
|
SWAPS_LAST_USED_KEY
|
|
} from '../utils/SwapUtils';
|
|
|
|
import NodeInfoStore from './NodeInfoStore';
|
|
import SettingsStore, {
|
|
DEFAULT_SWAP_HOST_MAINNET,
|
|
DEFAULT_SWAP_HOST_TESTNET,
|
|
SWAP_HOST_KEYS_TESTNET,
|
|
SWAP_HOST_KEYS_MAINNET
|
|
} from './SettingsStore';
|
|
|
|
import Storage from '../storage';
|
|
|
|
import Swap, { SwapState, SwapType } from '../models/Swap';
|
|
|
|
interface SubmarineSwapInfo {
|
|
fees?: {
|
|
percentage?: number;
|
|
minerFees?: number;
|
|
};
|
|
limits?: {
|
|
minimal?: number;
|
|
maximal?: number;
|
|
};
|
|
}
|
|
|
|
interface ReverseSwapInfo {
|
|
fees?: {
|
|
percentage?: number;
|
|
minerFees?: {
|
|
claim?: number;
|
|
lockup?: number;
|
|
};
|
|
};
|
|
limits?: {
|
|
minimal?: number;
|
|
maximal?: number;
|
|
};
|
|
}
|
|
|
|
export default class SwapStore {
|
|
@observable public subInfo: SubmarineSwapInfo = {};
|
|
@observable public reverseInfo: ReverseSwapInfo = {};
|
|
@observable public loading = true;
|
|
@observable public apiError = '';
|
|
@observable public swaps: any = [];
|
|
@observable public swapsLoading = false;
|
|
@observable public DERIVATION_PATH = 'm/44/0/0/0';
|
|
@observable ECPair: ECPairAPI;
|
|
|
|
nodeInfoStore: NodeInfoStore;
|
|
settingsStore: SettingsStore;
|
|
|
|
constructor(nodeInfoStore: NodeInfoStore, settingsStore: SettingsStore) {
|
|
this.nodeInfoStore = nodeInfoStore;
|
|
this.settingsStore = settingsStore;
|
|
initEccLib(ecc);
|
|
this.ECPair = ECPairFactory(ecc);
|
|
|
|
reaction(
|
|
() => this.getHost,
|
|
() => this.getSwapFees()
|
|
);
|
|
}
|
|
|
|
@computed get claimMinerFee(): number {
|
|
return this.reverseInfo?.fees?.minerFees?.claim || 0;
|
|
}
|
|
|
|
getReverseSwapReceiveAmount = (
|
|
onchainAmount: number | undefined,
|
|
swapClaimMinerFee?: number
|
|
): number => {
|
|
const fee = swapClaimMinerFee ?? this.claimMinerFee;
|
|
return (onchainAmount || 0) - fee;
|
|
};
|
|
|
|
@action
|
|
public clearError = () => {
|
|
this.loading = true;
|
|
this.apiError = '';
|
|
};
|
|
|
|
@computed get getHeaders() {
|
|
const settings = this.settingsStore.settings;
|
|
return settings.swaps?.proEnabled
|
|
? {
|
|
'Content-Type': 'application/json',
|
|
Referral: 'pro'
|
|
}
|
|
: undefined;
|
|
}
|
|
|
|
@computed get referralId() {
|
|
const settings = this.settingsStore.settings;
|
|
return settings.swaps?.proEnabled ? 'pro' : undefined;
|
|
}
|
|
|
|
/** Returns the API host based on network type */
|
|
@computed
|
|
public get getHost() {
|
|
const isTestnet = this.nodeInfoStore?.nodeInfo?.isTestNet;
|
|
const settings = this.settingsStore.settings;
|
|
|
|
if (
|
|
settings.swaps?.customHost &&
|
|
(settings.swaps?.hostTestnet === 'Custom' ||
|
|
settings.swaps?.hostMainnet === 'Custom')
|
|
) {
|
|
return settings.swaps?.customHost;
|
|
}
|
|
|
|
return isTestnet
|
|
? settings.swaps?.hostTestnet || DEFAULT_SWAP_HOST_TESTNET
|
|
: settings.swaps?.hostMainnet || DEFAULT_SWAP_HOST_MAINNET;
|
|
}
|
|
|
|
/** Returns the name of the swap service based on host and network */
|
|
@computed
|
|
public get getServiceProvider() {
|
|
const isTestnet = this.nodeInfoStore?.nodeInfo?.isTestNet;
|
|
const endpoint = this.getHost;
|
|
|
|
const hostKeys = isTestnet
|
|
? SWAP_HOST_KEYS_TESTNET
|
|
: SWAP_HOST_KEYS_MAINNET;
|
|
const matchingHost = hostKeys.find(
|
|
(host: any) => host.value === endpoint
|
|
);
|
|
|
|
return matchingHost ? matchingHost.key : endpoint;
|
|
}
|
|
|
|
@action
|
|
public statusColor = (status: SwapState | string) => {
|
|
let stateColor;
|
|
switch (status) {
|
|
case SwapState.TransactionClaimed:
|
|
case SwapState.InvoiceSettled:
|
|
case SwapState.TransactionRefunded:
|
|
stateColor = 'green';
|
|
break;
|
|
case SwapState.InvoiceFailedToPay:
|
|
case SwapState.SwapExpired:
|
|
case SwapState.InvoiceExpired:
|
|
case SwapState.TransactionLockupFailed:
|
|
case 'invoice could not be paid':
|
|
case 'invoice expired':
|
|
stateColor = themeColor('error');
|
|
break;
|
|
default:
|
|
stateColor = 'orange';
|
|
break;
|
|
}
|
|
|
|
return stateColor;
|
|
};
|
|
|
|
@action
|
|
public formatStatus = (status: string): string => {
|
|
if (!status || typeof status !== 'string')
|
|
return localeString('views.Swaps.noUpdates');
|
|
|
|
return status
|
|
.replace(/\./g, ' ') // Replace dots with spaces
|
|
.replace(/([a-z])([A-Z])/g, '$1 $2') // Add space between camelCase
|
|
.toLowerCase() // Convert to lowercase
|
|
.replace(/\b[a-z]/g, (char) => char.toUpperCase()); // Capitalize first letter of each word
|
|
};
|
|
|
|
@action
|
|
public getSwapFees = async () => {
|
|
this.loading = true;
|
|
this.apiError = '';
|
|
console.log(`Fetching fees from: ${this.getHost}`);
|
|
try {
|
|
const response = await ReactNativeBlobUtil.fetch(
|
|
'GET',
|
|
`${this.getHost}/swap/submarine`,
|
|
this.getHeaders
|
|
);
|
|
const status = response.info().status;
|
|
if (status == 200) {
|
|
const data = response.json();
|
|
const subInfo = data?.BTC?.BTC;
|
|
if (subInfo) {
|
|
this.subInfo = subInfo;
|
|
console.log('Submarine rates', this.subInfo);
|
|
} else {
|
|
console.warn(
|
|
'Unexpected submarine swap fee response:',
|
|
JSON.stringify(data)
|
|
);
|
|
this.apiError = localeString('views.Swaps.fetchFeesFailed');
|
|
}
|
|
} else if (status == 403) {
|
|
const data = response.json();
|
|
this.apiError = data?.error || data?.message;
|
|
this.loading = false;
|
|
return;
|
|
}
|
|
} catch (e) {
|
|
console.error('Error fetching submarine swap fees:', e);
|
|
this.apiError = localeString('views.Swaps.fetchFeesFailed');
|
|
}
|
|
|
|
try {
|
|
const response = await ReactNativeBlobUtil.fetch(
|
|
'GET',
|
|
`${this.getHost}/swap/reverse`,
|
|
this.getHeaders
|
|
);
|
|
const status = response.info().status;
|
|
if (status == 200) {
|
|
const data = response.json();
|
|
const reverseInfo = data?.BTC?.BTC;
|
|
if (reverseInfo) {
|
|
this.reverseInfo = reverseInfo;
|
|
console.log('Reverse rates', this.reverseInfo);
|
|
} else {
|
|
console.warn(
|
|
'Unexpected reverse swap fee response:',
|
|
JSON.stringify(data)
|
|
);
|
|
this.apiError = localeString('views.Swaps.fetchFeesFailed');
|
|
}
|
|
} else if (status == 403) {
|
|
const data = response.json();
|
|
this.apiError = data?.error || data?.message;
|
|
this.loading = false;
|
|
return;
|
|
}
|
|
} catch (e) {
|
|
console.error('Error fetching reverse swap fees:', e);
|
|
this.apiError = localeString('views.Swaps.fetchFeesFailed');
|
|
}
|
|
this.loading = false;
|
|
};
|
|
|
|
@action
|
|
public getLockupTransaction = async (id: string) => {
|
|
try {
|
|
const response = await ReactNativeBlobUtil.fetch(
|
|
'GET',
|
|
`${this.getHost}/swap/submarine/${id}/transaction`,
|
|
this.getHeaders
|
|
);
|
|
|
|
const status = response.info().status;
|
|
const data = response.json();
|
|
if (status == 200) {
|
|
const lockupTransaction = {
|
|
id: data.id,
|
|
hex: data.hex,
|
|
timeoutBlockHeight: data.timeoutBlockHeight,
|
|
timeoutEta: data.timeoutEta
|
|
};
|
|
const storedSwaps = await Storage.getItem(SWAPS_KEY);
|
|
const swaps = storedSwaps ? JSON.parse(storedSwaps) : [];
|
|
const updatedSwaps = swaps.map((swap: any) =>
|
|
swap.id === id
|
|
? {
|
|
...swap,
|
|
status,
|
|
lockupTransaction
|
|
}
|
|
: swap
|
|
);
|
|
|
|
await Storage.setItem(SWAPS_KEY, JSON.stringify(updatedSwaps));
|
|
return lockupTransaction;
|
|
} else {
|
|
console.log('getLockupTransaction - not found', data);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error in getLockupTransaction:', error);
|
|
}
|
|
};
|
|
|
|
@action
|
|
public createSubmarineSwap = async (invoice: any, navigation: any) => {
|
|
runInAction(() => {
|
|
this.loading = true;
|
|
});
|
|
const { implementation } = this.settingsStore;
|
|
const { nodeInfo } = this.nodeInfoStore;
|
|
const nodePubkey = nodeInfo.nodeId;
|
|
try {
|
|
console.log('Creating submarine swap using rescue key...');
|
|
|
|
const { keys } = await this.generateNewKey();
|
|
const refundPrivateKey = Buffer.from(keys.privateKey!).toString(
|
|
'hex'
|
|
);
|
|
const refundPublicKey = Buffer.from(keys.publicKey).toString('hex');
|
|
|
|
const response = await ReactNativeBlobUtil.fetch(
|
|
'POST',
|
|
`${this.getHost}/swap/submarine`,
|
|
{
|
|
'Content-Type': 'application/json'
|
|
},
|
|
JSON.stringify({
|
|
invoice,
|
|
to: 'BTC',
|
|
from: 'BTC',
|
|
refundPublicKey,
|
|
...(this.referralId && { referralId: this.referralId })
|
|
})
|
|
);
|
|
|
|
const responseData = JSON.parse(response.data);
|
|
console.log('Parsed Response Data:', responseData);
|
|
|
|
// Check for errors in the response
|
|
if (responseData?.error) {
|
|
runInAction(() => {
|
|
this.apiError = responseData.error;
|
|
this.loading = false;
|
|
});
|
|
console.error('Error in API response:', responseData.error);
|
|
return;
|
|
}
|
|
|
|
// Add the creation date to the response
|
|
const createdAt = new Date().toISOString();
|
|
responseData.createdAt = createdAt;
|
|
|
|
// Add the swap type
|
|
responseData.type = SwapType.Submarine;
|
|
responseData.refundPrivateKey = refundPrivateKey;
|
|
responseData.refundPublicKey = refundPublicKey;
|
|
|
|
await this.saveSubmarineSwap(
|
|
responseData,
|
|
keys,
|
|
invoice,
|
|
this.getHost,
|
|
implementation,
|
|
nodePubkey
|
|
);
|
|
|
|
runInAction(() => {
|
|
this.loading = false;
|
|
});
|
|
|
|
console.log('Navigating to SwapDetails...');
|
|
navigation.navigate('SwapDetails', {
|
|
swapData: responseData,
|
|
keys,
|
|
endpoint: this.getHost,
|
|
serviceProvider: this.getServiceProvider,
|
|
invoice
|
|
});
|
|
} catch (error: any) {
|
|
runInAction(() => {
|
|
this.apiError = error.message || 'An unknown error occurred';
|
|
this.loading = false;
|
|
});
|
|
console.error('Error creating Submarine Swap:', error);
|
|
}
|
|
};
|
|
|
|
private saveSubmarineSwap = async (
|
|
newSwap: any,
|
|
keys: any,
|
|
invoice: any,
|
|
endpoint: string,
|
|
implementation: any,
|
|
nodePubkey: string
|
|
) => {
|
|
try {
|
|
// Retrieve existing swaps
|
|
const storedSwaps = await Storage.getItem(SWAPS_KEY);
|
|
const swaps = storedSwaps ? JSON.parse(storedSwaps) : [];
|
|
|
|
// Adding the new properties to the swap
|
|
const enrichedSwap = {
|
|
...newSwap,
|
|
keys,
|
|
invoice,
|
|
endpoint,
|
|
implementation,
|
|
nodePubkey,
|
|
serviceProvider: this.getServiceProvider
|
|
};
|
|
|
|
// Add the enriched swap to the beginning of array
|
|
swaps.unshift(enrichedSwap);
|
|
|
|
// Save the updated swaps array back to Encrypted Storage
|
|
await Storage.setItem(SWAPS_KEY, JSON.stringify(swaps));
|
|
console.log('Swap saved successfully to Encrypted Storage.');
|
|
} catch (error: any) {
|
|
console.error('Error saving swap to storage:', error);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
@action
|
|
public createReverseSwap = async (
|
|
destinationAddress: string,
|
|
invoiceAmount: any,
|
|
fee: string,
|
|
navigation: any
|
|
) => {
|
|
runInAction(() => {
|
|
this.loading = true;
|
|
});
|
|
const { implementation } = this.settingsStore;
|
|
const { nodeInfo } = this.nodeInfoStore;
|
|
const nodePubkey = nodeInfo.nodeId;
|
|
try {
|
|
initEccLib(ecc);
|
|
console.log('Creating reverse swap using rescue key...');
|
|
const { keys, index } = await this.generateNewKey();
|
|
|
|
const preimage = await this.derivePreimageFromRescueKey(index);
|
|
|
|
// Creating a reverse swap
|
|
const data = JSON.stringify({
|
|
invoiceAmount,
|
|
to: 'BTC',
|
|
from: 'BTC',
|
|
claimPublicKey: Buffer.from(keys.publicKey).toString('hex'),
|
|
preimageHash: crypto.sha256(preimage).toString('hex'),
|
|
...(this.referralId && { referralId: this.referralId })
|
|
});
|
|
|
|
console.log('Data before sending to API:', data);
|
|
|
|
const response = await ReactNativeBlobUtil.fetch(
|
|
'POST',
|
|
`${this.getHost}/swap/reverse`,
|
|
{
|
|
'Content-Type': 'application/json'
|
|
},
|
|
data
|
|
);
|
|
|
|
const responseData = JSON.parse(response.data);
|
|
console.log('Created reverse swap:', responseData);
|
|
|
|
// Handle API errors
|
|
if (responseData?.error) {
|
|
runInAction(() => {
|
|
this.apiError = responseData.error;
|
|
this.loading = false;
|
|
});
|
|
console.error('Error in API response:', responseData.error);
|
|
return;
|
|
}
|
|
|
|
// Add the creation date
|
|
const createdAt = new Date().toISOString();
|
|
responseData.createdAt = createdAt;
|
|
|
|
// Add the swap type
|
|
responseData.type = SwapType.Reverse;
|
|
responseData.preimage = preimage;
|
|
responseData.destinationAddress = destinationAddress;
|
|
responseData.claimMinerFee = this.claimMinerFee;
|
|
|
|
await this.saveReverseSwaps(
|
|
responseData,
|
|
keys,
|
|
destinationAddress,
|
|
preimage,
|
|
this.getHost,
|
|
implementation,
|
|
nodePubkey
|
|
);
|
|
|
|
runInAction(() => {
|
|
this.loading = false;
|
|
});
|
|
|
|
console.log('Navigating to SwapDetails...');
|
|
navigation.navigate('SwapDetails', {
|
|
swapData: responseData,
|
|
keys,
|
|
endpoint: this.getHost,
|
|
serviceProvider: this.getServiceProvider,
|
|
invoice: destinationAddress,
|
|
fee
|
|
});
|
|
} catch (error: any) {
|
|
runInAction(() => {
|
|
this.apiError = error.message || 'An unknown error occurred';
|
|
this.loading = false;
|
|
});
|
|
console.error('Error creating reverse swap:', error);
|
|
}
|
|
};
|
|
|
|
private saveReverseSwaps = async (
|
|
newSwap: any,
|
|
keys: any,
|
|
destinationAddress: string,
|
|
preimage: any,
|
|
endpoint: string,
|
|
implementation: any,
|
|
nodePubkey: string
|
|
) => {
|
|
try {
|
|
// Retrieve existing swaps
|
|
const storedSwaps = await Storage.getItem(REVERSE_SWAPS_KEY);
|
|
const swaps = storedSwaps ? JSON.parse(storedSwaps) : [];
|
|
|
|
// Adding the new properties to the swap
|
|
const enrichedSwap = {
|
|
...newSwap,
|
|
keys,
|
|
destinationAddress,
|
|
preimage,
|
|
endpoint,
|
|
implementation,
|
|
nodePubkey,
|
|
serviceProvider: this.getServiceProvider
|
|
};
|
|
|
|
// Add the enriched swap to the beginning of array
|
|
swaps.unshift(enrichedSwap);
|
|
|
|
// Save the updated swaps array back to Encrypted Storage
|
|
await Storage.setItem(REVERSE_SWAPS_KEY, JSON.stringify(swaps));
|
|
console.log(
|
|
'Reverse swap saved successfully to Encrypted Storage.'
|
|
);
|
|
} catch (error: any) {
|
|
console.error('Error saving reverse swap to storage:', error);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
@action
|
|
public fetchAndUpdateSwaps = async () => {
|
|
const { implementation } = this.settingsStore;
|
|
const { nodeInfo } = this.nodeInfoStore;
|
|
const pubkey = nodeInfo?.nodeId;
|
|
console.log('Fetching and updating swaps...');
|
|
this.swapsLoading = true;
|
|
try {
|
|
const storedSubmarineSwaps = await Storage.getItem(SWAPS_KEY);
|
|
const storedReverseSwaps = await Storage.getItem(REVERSE_SWAPS_KEY);
|
|
|
|
const submarineSwaps = storedSubmarineSwaps
|
|
? JSON.parse(storedSubmarineSwaps)
|
|
: [];
|
|
const reverseSwaps = storedReverseSwaps
|
|
? JSON.parse(storedReverseSwaps)
|
|
: [];
|
|
|
|
const allSwaps = [...submarineSwaps, ...reverseSwaps];
|
|
|
|
for (const swap of allSwaps) {
|
|
if (!swap?.id) continue;
|
|
|
|
const skipStatusesForSubmarineSwap = [
|
|
SwapState.InvoiceFailedToPay,
|
|
SwapState.TransactionRefunded,
|
|
SwapState.TransactionClaimed,
|
|
SwapState.SwapExpired
|
|
];
|
|
|
|
const skipStatusesForReverseSwap = [
|
|
SwapState.TransactionRefunded
|
|
];
|
|
|
|
const shouldSkip =
|
|
(swap.type === SwapType.Submarine &&
|
|
skipStatusesForSubmarineSwap.includes(swap.status)) ||
|
|
(swap.type === SwapType.Reverse &&
|
|
skipStatusesForReverseSwap.includes(swap.status));
|
|
|
|
if (shouldSkip) continue;
|
|
|
|
try {
|
|
const response = await ReactNativeBlobUtil.fetch(
|
|
'GET',
|
|
`${this.getHost}/swap/${swap.id}`,
|
|
this.getHeaders
|
|
);
|
|
|
|
const result = await response.json();
|
|
if (result?.status) {
|
|
swap.status = result.status;
|
|
}
|
|
} catch (err: any) {
|
|
console.warn(
|
|
`Failed to fetch status for swap ${swap.id}`,
|
|
err
|
|
);
|
|
}
|
|
}
|
|
|
|
const updatedSubmarineSwaps = allSwaps.filter(
|
|
(s) => s.type === SwapType.Submarine
|
|
);
|
|
const updatedReverseSwaps = allSwaps.filter(
|
|
(s) => s.type === SwapType.Reverse
|
|
);
|
|
|
|
await Storage.setItem(
|
|
SWAPS_KEY,
|
|
JSON.stringify(updatedSubmarineSwaps)
|
|
);
|
|
await Storage.setItem(
|
|
REVERSE_SWAPS_KEY,
|
|
JSON.stringify(updatedReverseSwaps)
|
|
);
|
|
|
|
// Filter swaps to current pubkey or implementation
|
|
const swaps = allSwaps.filter((swap: any) => {
|
|
return swap.nodePubkey
|
|
? swap.nodePubkey === pubkey
|
|
: swap.implementation === implementation;
|
|
});
|
|
|
|
swaps.sort(
|
|
(a, b) =>
|
|
new Date(b.createdAt).getTime() -
|
|
new Date(a.createdAt).getTime()
|
|
);
|
|
|
|
this.swaps = swaps.map((swap) => new Swap(swap));
|
|
this.swapsLoading = false;
|
|
} catch (error) {
|
|
console.error('Failed to fetch and update swaps:', error);
|
|
} finally {
|
|
this.swapsLoading = false;
|
|
}
|
|
};
|
|
|
|
@action
|
|
updateSwapStatus = async (
|
|
swapId: string,
|
|
status: SwapState,
|
|
isSubmarineSwap: boolean,
|
|
failureReason?: string
|
|
) => {
|
|
try {
|
|
let storedSwaps: any;
|
|
const key = isSubmarineSwap ? SWAPS_KEY : REVERSE_SWAPS_KEY;
|
|
storedSwaps = await Storage.getItem(key);
|
|
const swaps = storedSwaps ? JSON.parse(storedSwaps) : [];
|
|
|
|
const updatedSwaps = swaps.map((swap: any) =>
|
|
swap.id === swapId
|
|
? {
|
|
...swap,
|
|
status,
|
|
...(isSubmarineSwap && failureReason
|
|
? { failureReason }
|
|
: {})
|
|
}
|
|
: swap
|
|
);
|
|
|
|
await Storage.setItem(key, JSON.stringify(updatedSwaps));
|
|
console.log(
|
|
`Updated ${
|
|
isSubmarineSwap ? `swap` : `reverse swap`
|
|
} status for swap ID ${swapId} to "${status}"`
|
|
);
|
|
} catch (error) {
|
|
console.error('Error updating swap status in storage:', error);
|
|
}
|
|
};
|
|
|
|
@action
|
|
public updateSwapOnRefund = async (swapId: string, txid: string) => {
|
|
try {
|
|
// Retrieve the swaps from encrypted storage
|
|
const storedSwaps = await Storage.getItem(SWAPS_KEY);
|
|
if (!storedSwaps) {
|
|
throw new Error('No swaps found in storage');
|
|
}
|
|
|
|
// Parse the swaps array
|
|
const swaps = storedSwaps ? JSON.parse(storedSwaps) : [];
|
|
|
|
// Find the swap by swapId
|
|
const swapIndex = swaps.findIndex(
|
|
(swap: any) => swap.id === swapId
|
|
);
|
|
if (swapIndex === -1) {
|
|
throw new Error(`Swap with ID ${swapId} not found`);
|
|
}
|
|
|
|
// Update the swap
|
|
swaps[swapIndex].status = SwapState.TransactionRefunded;
|
|
swaps[swapIndex].txid = txid;
|
|
|
|
// Save the updated swaps back to encrypted storage
|
|
await Storage.setItem(SWAPS_KEY, JSON.stringify(swaps));
|
|
|
|
console.log('Swap updated in storage:', swaps[swapIndex]);
|
|
} catch (error) {
|
|
console.error('Error updating swap in storage:', error);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
@action
|
|
public getPath = (index: number) => `${this.DERIVATION_PATH}/${index}`;
|
|
|
|
@action
|
|
public mnemonicToHDKey = (mnemonic: string) => {
|
|
const seed = mnemonicToSeedSync(mnemonic);
|
|
const hdKey = HDKey.fromMasterSeed(seed);
|
|
return hdKey;
|
|
};
|
|
|
|
@action
|
|
public getXpub = (mnemonic: string) => {
|
|
return this.mnemonicToHDKey(mnemonic).publicExtendedKey;
|
|
};
|
|
|
|
@action
|
|
public deriveKey = async (index: number) => {
|
|
const mnemonic = await Storage.getItem(SWAPS_RESCUE_KEY);
|
|
if (!mnemonic) {
|
|
throw new Error('Rescue mnemonic not found in storage.');
|
|
}
|
|
|
|
const hdKey = this.mnemonicToHDKey(mnemonic);
|
|
const childKey = hdKey.derive(this.getPath(index));
|
|
|
|
if (!childKey.privateKey) {
|
|
throw new Error(`No private key at index ${index}`);
|
|
}
|
|
|
|
const ecPair = this.ECPair.fromPrivateKey(
|
|
Buffer.from(childKey.privateKey)
|
|
);
|
|
|
|
return ecPair;
|
|
};
|
|
|
|
@action
|
|
public derivePreimageFromRescueKey = async (index: number) => {
|
|
const mnemonic = await Storage.getItem(SWAPS_RESCUE_KEY);
|
|
if (!mnemonic) {
|
|
throw new Error('Rescue mnemonic not found in storage.');
|
|
}
|
|
|
|
const hdKey = this.mnemonicToHDKey(mnemonic);
|
|
const childKey = hdKey.derive(this.getPath(index));
|
|
|
|
if (!childKey.privateKey) {
|
|
throw new Error(`No private key at index ${index}`);
|
|
}
|
|
|
|
return crypto.sha256(Buffer.from(childKey.privateKey));
|
|
};
|
|
|
|
@action
|
|
public generateRescueKey = async () => {
|
|
console.log('GENERATING RESCUE FILE...');
|
|
// A 12-word LDK Node wallet already has a 12-word BIP-39 mnemonic -
|
|
// reuse it instead of generating a separate rescue key. 24-word LDK
|
|
// wallets (and every other backend) get an independent 12-word key.
|
|
const { implementation, ldkMnemonic } = this.settingsStore;
|
|
const reuseLdkSeed =
|
|
implementation === 'ldk-node' &&
|
|
!!ldkMnemonic &&
|
|
ldkMnemonic.trim().split(/\s+/).length === 12;
|
|
const mnemonic = reuseLdkSeed
|
|
? ldkMnemonic
|
|
: generateMnemonic(BIP39_WORD_LIST);
|
|
await Storage.setItem(SWAPS_RESCUE_KEY, mnemonic);
|
|
|
|
return mnemonic;
|
|
};
|
|
|
|
@action
|
|
public getLastUsedKey = async (): Promise<number> => {
|
|
const storedKey = await Storage.getItem(SWAPS_LAST_USED_KEY);
|
|
const index = storedKey ? parseInt(storedKey, 10) : 0;
|
|
|
|
return index;
|
|
};
|
|
|
|
@action
|
|
public setLastUsedKey = async (val: number): Promise<void> => {
|
|
await Storage.setItem(SWAPS_LAST_USED_KEY, val.toString());
|
|
};
|
|
|
|
@action
|
|
public generateNewKey = async () => {
|
|
const index = await this.getLastUsedKey();
|
|
await this.setLastUsedKey(index + 1);
|
|
const keys = await this.deriveKey(index);
|
|
return { index, keys };
|
|
};
|
|
|
|
@action
|
|
public getRescuableSwaps = async ({
|
|
seedArray,
|
|
host
|
|
}: {
|
|
seedArray: string[];
|
|
host: string;
|
|
}) => {
|
|
const mnemonic = seedArray.join(' ');
|
|
|
|
if (!validateMnemonic(mnemonic, BIP39_WORD_LIST)) {
|
|
return {
|
|
success: false,
|
|
error: localeString('views.Swaps.rescueKey.invalid')
|
|
};
|
|
}
|
|
|
|
const { implementation } = this.settingsStore;
|
|
const { nodeInfo } = this.nodeInfoStore;
|
|
const nodePubkey = nodeInfo.nodeId;
|
|
|
|
if (mnemonic) {
|
|
const xpub = this.getXpub(mnemonic);
|
|
|
|
try {
|
|
const response = await ReactNativeBlobUtil.fetch(
|
|
'POST',
|
|
`${host}/swap/restore`,
|
|
{
|
|
'Content-Type': 'application/json'
|
|
},
|
|
JSON.stringify({
|
|
xpub
|
|
})
|
|
);
|
|
|
|
const importedSwaps = JSON.parse(response.data || '[]');
|
|
|
|
if (importedSwaps.length > 0) {
|
|
const storedSwaps = await Storage.getItem(SWAPS_KEY);
|
|
const existingSwaps = storedSwaps
|
|
? JSON.parse(storedSwaps)
|
|
: [];
|
|
const existingSwapIds = existingSwaps.map((s: any) => s.id);
|
|
|
|
const rescuedSwaps = await Promise.all(
|
|
importedSwaps
|
|
.filter(
|
|
(swap: any) =>
|
|
!existingSwapIds.includes(swap.id)
|
|
)
|
|
.map(async (swap: any) => {
|
|
const isReverseSwap = swap.type === 'reverse';
|
|
const details = isReverseSwap
|
|
? swap.claimDetails
|
|
: swap.refundDetails;
|
|
|
|
const swapDetails: any = {};
|
|
|
|
for (const key in swap) {
|
|
if (
|
|
key !== 'claimDetails' &&
|
|
key !== 'refundDetails'
|
|
) {
|
|
swapDetails[key] = swap[key];
|
|
}
|
|
}
|
|
|
|
const { keyIndex } = details;
|
|
|
|
const hdKey = this.mnemonicToHDKey(mnemonic);
|
|
const childKey = hdKey.derive(
|
|
this.getPath(keyIndex)
|
|
);
|
|
|
|
if (!childKey.privateKey) {
|
|
throw new Error(
|
|
`No private key at index ${keyIndex}`
|
|
);
|
|
}
|
|
|
|
const ecPair = this.ECPair.fromPrivateKey(
|
|
Buffer.from(childKey.privateKey)
|
|
);
|
|
|
|
const refundPrivateKey = Buffer.from(
|
|
ecPair.privateKey!
|
|
).toString('hex');
|
|
const refundPublicKey = Buffer.from(
|
|
ecPair.publicKey
|
|
).toString('hex');
|
|
|
|
const rescuedSwapBase = {
|
|
...swapDetails,
|
|
...details,
|
|
imported: true,
|
|
implementation,
|
|
nodePubkey,
|
|
endpoint: host,
|
|
serviceProvider: this.getServiceProvider,
|
|
keys: ecPair
|
|
};
|
|
|
|
if (isReverseSwap) {
|
|
return {
|
|
...rescuedSwapBase,
|
|
type: SwapType.Reverse
|
|
};
|
|
} else {
|
|
return {
|
|
...rescuedSwapBase,
|
|
type: SwapType.Submarine,
|
|
refundPrivateKey,
|
|
refundPublicKey
|
|
};
|
|
}
|
|
})
|
|
);
|
|
|
|
const updatedSwaps = [...existingSwaps, ...rescuedSwaps];
|
|
|
|
await Storage.setItem(
|
|
SWAPS_KEY,
|
|
JSON.stringify(updatedSwaps)
|
|
);
|
|
console.log('Rescued swaps saved to storage');
|
|
return { success: true };
|
|
} else {
|
|
return {
|
|
success: false,
|
|
error: localeString(
|
|
'views.Swaps.rescueKey.noSwapsFound'
|
|
)
|
|
};
|
|
}
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
error: localeString('views.Swaps.rescueKey.incorrectHost')
|
|
};
|
|
}
|
|
}
|
|
};
|
|
}
|