Skip to content
This repository has been archived by the owner on Jan 6, 2025. It is now read-only.

Commit

Permalink
feat: Allow to switch between refund policies (internal wallet / exte…
Browse files Browse the repository at this point in the history
…rnal wallet) (#213)

* feat: Allow to switch between refund policies (internal wallet / external wallet)

* feat: Upgrade CLI to 0.13.4

* feat: Refresh Bitcoin balance as soon as transaction is published (withdraw, refund ...)
  • Loading branch information
binarybaron authored Jul 25, 2024
1 parent 9400b86 commit d176c36
Show file tree
Hide file tree
Showing 7 changed files with 147 additions and 51 deletions.
4 changes: 2 additions & 2 deletions .erb/scripts/download-swap-binaries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ async function makeFileExecutable(binary: Binary) {
);
}

const CLI_VERSION = '0.13.3';
const CLI_VERSION = '0.13.4';
// Ensure the value here matches with the one in src/main/cli/dirs.ts
const CLI_FILE_NAME_VERSION_PREFIX = '0_13_3_';
const CLI_FILE_NAME_VERSION_PREFIX = '0_13_4_';

const binaries = [
{
Expand Down
2 changes: 1 addition & 1 deletion src/main/cli/dirs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { RpcProcessStateType } from 'models/rpcModel';
// Ensure the CLI version is updated in both places when updating the CLI version.
// We need a different name for each CLI version to make sure, that the CLI is definitely updated
// electron-builder sometimes doesn't update the CLI binary
const CLI_FILE_NAME_VERSION_PREFIX = '0_13_3_';
const CLI_FILE_NAME_VERSION_PREFIX = '0_13_4_';

// Be consistent with the way the cli generates the
// data-dir on linux
Expand Down
20 changes: 14 additions & 6 deletions src/main/cli/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ export async function getSwapInfoBatch(

export async function buyXmr(
redeemAddress: string,
refundAddress: string,
refundAddress: string | null,
provider: Provider,
) {
store.dispatch(
Expand All @@ -289,14 +289,22 @@ export async function buyXmr(
}),
);

let params =
refundAddress === null
? {
monero_receive_address: redeemAddress,
seller: providerToConcatenatedMultiAddr(provider),
}
: {
bitcoin_change_address: refundAddress,
monero_receive_address: redeemAddress,
seller: providerToConcatenatedMultiAddr(provider),
};

try {
await makeRpcRequest<BuyXmrResponse>(
RpcMethod.BUY_XMR,
{
bitcoin_change_address: refundAddress,
monero_receive_address: redeemAddress,
seller: providerToConcatenatedMultiAddr(provider),
},
params,
(logs) => {
store.dispatch(
swapAddLog({
Expand Down
44 changes: 40 additions & 4 deletions src/main/store/mainStoreListeners.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { createListenerMiddleware } from '@reduxjs/toolkit';
import { getRawSwapInfo, getRawSwapInfos } from '../cli/rpc';
import {
checkBitcoinBalance,
getRawSwapInfo,
getRawSwapInfos,
} from '../cli/rpc';
import {
CliLog,
getCliLogSpanSwapId,
isCliLog,
isCliLogAdvancingState,
isCliLogGotNotificationForNewBlock,
isCliLogPublishedBtcTx,
isCliLogReleasingSwapLockLog,
} from '../../models/cliModel';
import logger from '../../utils/logger';
Expand Down Expand Up @@ -45,11 +51,13 @@ export function createMainListeners() {
// If we get a new block, we fetch all swap infos because the state of the timelocks might have changed
listener.startListening({
predicate: (action) => {
return action.type === 'rpc/rpcAddLog';
return action.type === 'rpc/rpcAddLogs';
},
effect: async (action) => {
const logs = action.payload.logs as CliLog[];
const newBlockLog = logs.find(isCliLogGotNotificationForNewBlock);
const logs = action.payload as (CliLog | string)[];
const newBlockLog = logs
.filter(isCliLog)
.find(isCliLogGotNotificationForNewBlock);

if (newBlockLog) {
logger.debug('Fetching all swap infos because a new block was found');
Expand All @@ -58,5 +66,33 @@ export function createMainListeners() {
},
});

// Listener for "Published Bitcoin transaction" log
// We refresh the Bitcoin balance because the balance might have changed
listener.startListening({
predicate: (action) => {
return action.type === 'rpc/rpcAddLogs';
},
effect: async (action) => {
const logs = action.payload as (CliLog | string)[];
const publishedBitcoinTransactionLog = logs
.filter(isCliLog)
.find(isCliLogPublishedBtcTx);

if (publishedBitcoinTransactionLog) {
logger.info(
'Refreshing Bitcoin balance because a Bitcoin transaction was published which might have changed the balance',
);

// Refresh 3 times with a delay of 5 seconds in between
// This is because the balance might not be updated immediately after the transaction is published
const delay = 5 * 1000;
for (let _ in [1, 2, 3]) {
await checkBitcoinBalance(true);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
},
});

return listener;
}
10 changes: 9 additions & 1 deletion src/renderer/components/inputs/BitcoinAddressTextField.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useEffect } from 'react';
import { TextField } from '@material-ui/core';
import { InputAdornment, TextField } from '@material-ui/core';
import { TextFieldProps } from '@material-ui/core/TextField/TextField';
import { isBtcAddressValid } from 'utils/conversionUtils';
import { isTestnet } from 'store/config';
import BitcoinIcon from '../icons/BitcoinIcon';

export default function BitcoinAddressTextField({
address,
Expand Down Expand Up @@ -35,6 +36,13 @@ export default function BitcoinAddressTextField({
helperText={address.length > 0 ? errorText || helperText : helperText}
placeholder={placeholder}
variant="outlined"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<BitcoinIcon />
</InputAdornment>
),
}}
{...props}
/>
);
Expand Down
10 changes: 9 additions & 1 deletion src/renderer/components/inputs/MoneroAddressTextField.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useEffect } from 'react';
import { TextField } from '@material-ui/core';
import { InputAdornment, TextField } from '@material-ui/core';
import { TextFieldProps } from '@material-ui/core/TextField/TextField';
import { isXmrAddressValid } from 'utils/conversionUtils';
import { isTestnet } from 'store/config';
import MoneroIcon from '../icons/MoneroIcon';

export default function MoneroAddressTextField({
address,
Expand Down Expand Up @@ -33,6 +34,13 @@ export default function MoneroAddressTextField({
helperText={address.length > 0 ? errorText || helperText : helperText}
placeholder={placeholder}
variant="outlined"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<MoneroIcon />
</InputAdornment>
),
}}
{...props}
/>
);
Expand Down
108 changes: 72 additions & 36 deletions src/renderer/components/modal/swap/pages/init/InitPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { Box, DialogContentText, makeStyles } from '@material-ui/core';
import {
Box,
DialogContentText,
makeStyles,
Paper,
Tab,
Tabs,
Typography,
} from '@material-ui/core';
import { useState } from 'react';
import BitcoinAddressTextField from 'renderer/components/inputs/BitcoinAddressTextField';
import MoneroAddressTextField from 'renderer/components/inputs/MoneroAddressTextField';
Expand All @@ -13,14 +21,19 @@ const useStyles = makeStyles((theme) => ({
marginTop: theme.spacing(1),
},
fieldsOuter: {
paddingTop: theme.spacing(1),
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(2),
gap: theme.spacing(1),
},
}));

export default function InitPage() {
const selectedProvider = useAppSelector(
(state) => state.providers.selectedProvider,
);
const classes = useStyles();

const [redeemAddress, setRedeemAddress] = useState(
isTestnet() && process.env.TESTNET_AUTOFILL_XMR_ADDRESS
? process.env.TESTNET_AUTOFILL_XMR_ADDRESS
Expand All @@ -33,54 +46,77 @@ export default function InitPage() {
);
const [redeemAddressValid, setRedeemAddressValid] = useState(false);
const [refundAddressValid, setRefundAddressValid] = useState(false);
const selectedProvider = useAppSelector(
(state) => state.providers.selectedProvider,
);
const [useExternalRefundAddress, setUseExternalRefundAddress] =
useState(false);

return (
<Box>
<RemainingFundsWillBeUsedAlert />
<DialogContentText>
Please specify the address to which the Monero should be sent upon
completion of the swap and the address for receiving a Bitcoin refund if
the swap fails.
</DialogContentText>

<Box className={classes.fieldsOuter}>
<MoneroAddressTextField
label="Monero redeem address"
address={redeemAddress}
onAddressChange={setRedeemAddress}
onAddressValidityChange={setRedeemAddressValid}
helperText="The monero will be sent to this address"
helperText="The monero will be sent to this address if the swap is successful."
fullWidth
/>

<BitcoinAddressTextField
label="Bitcoin refund address"
address={refundAddress}
onAddressChange={setRefundAddress}
onAddressValidityChange={setRefundAddressValid}
helperText="In case something goes terribly wrong, all Bitcoin will be refunded to this address"
fullWidth
/>
<Paper variant="outlined" style={{}}>
<Tabs
value={useExternalRefundAddress ? 1 : 0}
indicatorColor="primary"
variant="fullWidth"
onChange={(_, newValue) =>
setUseExternalRefundAddress(newValue === 1)
}
>
<Tab label="Refund to internal Bitcoin wallet" value={0} />
<Tab label="Refund to external Bitcoin address" value={1} />
</Tabs>
<Box style={{ padding: '16px' }}>
{useExternalRefundAddress ? (
<BitcoinAddressTextField
label="External Bitcoin refund address"
address={refundAddress}
onAddressChange={setRefundAddress}
onAddressValidityChange={setRefundAddressValid}
helperText="In case something goes wrong, the Bitcoin will be refunded to this address."
fullWidth
/>
) : (
<Typography variant="caption">
In case something goes wrong, the Bitcoin will be refunded to
the internal Bitcoin wallet of the GUI. You can then withdraw
them from there or use them for another swap directly.
</Typography>
)}
</Box>
</Paper>
</Box>
<Box style={{ display: 'flex', justifyContent: 'center' }}>
<IpcInvokeButton
disabled={
(!refundAddressValid && useExternalRefundAddress) ||
!redeemAddressValid ||
!selectedProvider
}
variant="contained"
color="primary"
size="large"
className={classes.initButton}
endIcon={<PlayArrowIcon />}
ipcChannel="spawn-buy-xmr"
ipcArgs={[
selectedProvider,
redeemAddress,
useExternalRefundAddress ? refundAddress : null,
]}
displayErrorSnackbar={false}
>
Request quote and start swap
</IpcInvokeButton>
</Box>

<IpcInvokeButton
disabled={
!refundAddressValid || !redeemAddressValid || !selectedProvider
}
variant="contained"
color="primary"
size="large"
className={classes.initButton}
endIcon={<PlayArrowIcon />}
ipcChannel="spawn-buy-xmr"
ipcArgs={[selectedProvider, redeemAddress, refundAddress]}
displayErrorSnackbar={false}
>
Start swap
</IpcInvokeButton>
</Box>
);
}

0 comments on commit d176c36

Please sign in to comment.