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

Feat/wif sweep #9 #33

Merged
merged 9 commits into from
Nov 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
118 changes: 115 additions & 3 deletions src/pages/AppsAndTools.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Button } from '../components/Button';
import { ForwardButton as RightChevron } from '../components/ForwardButton';
import { PageLoader } from '../components/PageLoader';
import yoursLogo from '../assets/logos/icon.png';
import { HeaderText, Text } from '../components/Reusable';
import { HeaderText, Text, Warning } from '../components/Reusable';
import { SettingsRow as AppsRow } from '../components/SettingsRow';
import { Show } from '../components/Show';
import { useBottomMenu } from '../hooks/useBottomMenu';
Expand Down Expand Up @@ -146,14 +146,30 @@ const TextArea = styled.textarea<WhiteLabelTheme>`
}
`;

type AppsPage = 'main' | 'sponsor' | 'sponsor-thanks' | 'discover-apps' | 'unlock' | 'decode-broadcast' | 'decode';
const SweepInfo = styled.div`
width: 80%;
padding: 1rem;
margin: 1rem 0;
border-radius: 0.5rem;
background-color: ${({ theme }) => theme.color.global.row};
`;

type AppsPage =
| 'main'
| 'sponsor'
| 'sponsor-thanks'
| 'discover-apps'
| 'unlock'
| 'decode-broadcast'
| 'decode'
| 'sweep-wif';

export const AppsAndTools = () => {
const { theme } = useTheme();
const { addSnackbar } = useSnackbar();
const { query } = useBottomMenu();
const { keysService, bsvService, chromeStorageService, oneSatSPV } = useServiceContext();
const { bsvAddress, ordAddress, identityAddress } = keysService;
const { bsvAddress, ordAddress, identityAddress, getWifBalance, sweepWif } = keysService;
const exchangeRate = chromeStorageService.getCurrentAccountObject().exchangeRateCache?.rate ?? 0;
const [isProcessing, setIsProcessing] = useState(false);
const [page, setPage] = useState<AppsPage>(query === 'pending-locks' ? 'unlock' : 'main');
Expand All @@ -169,6 +185,60 @@ export const AppsAndTools = () => {
const [satsOut, setSatsOut] = useState(0);
const [isBroadcasting, setIsBroadcasting] = useState(false);

const [wifKey, setWifKey] = useState('');
const [sweepBalance, setSweepBalance] = useState(0);
const [isSweeping, setIsSweeping] = useState(false);

const checkWIFBalance = async (wif: string) => {
const balance = await getWifBalance(wif);
if (balance === undefined) {
addSnackbar('Error checking balance. Please ensure the WIF key is valid.', 'error');
return;
}
if (balance === 0) {
addSnackbar('No balance found for this WIF key', 'info');
setSweepBalance(balance);
return;
}

addSnackbar(`Balance found: ${balance / BSV_DECIMAL_CONVERSION} BSV`, 'success');
setSweepBalance(balance);
};

const sweepFunds = async () => {
try {
if (!wifKey) return;
setIsSweeping(true);
const res = await sweepWif(wifKey);
if (res?.txid) {
addSnackbar('Successfully swept funds to your wallet', 'success');
handleResetSweep();
return;
} else {
addSnackbar('Error sweeping funds. Please try again.', 'error');
}
} catch (error) {
addSnackbar('Error sweeping funds. Please try again.', 'error');
console.error('Sweep error:', error);
} finally {
setIsSweeping(false);
}
};

const handleWifChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const wif = e.target.value;
if (!wif) return;
checkWIFBalance(wif);
setWifKey(e.target.value);
};

const handleResetSweep = () => {
setWifKey('');
setSweepBalance(0);
setIsSweeping(false);
setPage('main');
};

const getLockData = async () => {
setIsProcessing(true);
setCurrentBlockHeight(await bsvService.getCurrentHeight());
Expand Down Expand Up @@ -282,6 +352,12 @@ export const AppsAndTools = () => {
<FaExternalLinkAlt color={theme.color.global.contrast} size={'1rem'} style={{ margin: '0.5rem' }} />
}
/>
<AppsRow
name="Sweep Private Key"
description="Import funds from WIF private key"
onClick={() => setPage('sweep-wif')}
jsxElement={<RightChevron color={theme.color.global.contrast} />}
/>
</>
);

Expand Down Expand Up @@ -483,6 +559,41 @@ export const AppsAndTools = () => {
</PageWrapper>
);

const wifSweepPage = (
<PageWrapper $marginTop={'0'}>
<HeaderText theme={theme}>Sweep Private Key</HeaderText>
<Text theme={theme}>Enter a private key in WIF format to sweep all funds to your wallet.</Text>
<Input theme={theme} placeholder="Enter WIF private key" value={wifKey} onChange={handleWifChange} />

{sweepBalance > 0 && (
<SweepInfo theme={theme}>
<Text theme={theme}>Available to sweep:</Text>
<Text style={{ fontWeight: 700 }} theme={theme}>
{sweepBalance / BSV_DECIMAL_CONVERSION} BSV
</Text>
</SweepInfo>
)}

<ButtonsWrapper>
<Button
theme={theme}
type="secondary-outline"
label="Cancel"
onClick={handleResetSweep}
disabled={isSweeping}
/>
<Button
theme={theme}
type="primary"
label={isSweeping ? 'Sweeping...' : 'Sweep Funds'}
onClick={sweepFunds}
disabled={isSweeping || sweepBalance === 0}
/>
</ButtonsWrapper>
<Warning theme={theme}>This will only sweep funds. 1Sat Ordinals could be lost!</Warning>
</PageWrapper>
);

const decode = !!txData && (
<>
<TxPreview txData={txData} />
Expand Down Expand Up @@ -515,6 +626,7 @@ export const AppsAndTools = () => {
<Show when={page === 'sponsor-thanks'}>{thankYouSponsorPage}</Show>
<Show when={!isProcessing && page === 'unlock'}>{unlockPage}</Show>
<Show when={page === 'discover-apps'}>{discoverAppsPage}</Show>
<Show when={page === 'sweep-wif'}>{wifSweepPage}</Show>
<Show when={page === 'sponsor' && didSubmit}>
<BsvSendRequest
request={[{ address: YOURS_DEV_WALLET, satoshis: satAmount }]}
Expand Down
47 changes: 47 additions & 0 deletions src/services/Keys.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,53 @@ export class KeysService {
}
};

getWifBalance = async (wif: string) => {
try {
const privKey = PrivateKey.fromWif(wif);
const { data } = await axios.get<WocUtxo[]>(`${WOC_BASE_URL}/address/${privKey.toAddress()}/unspent`);
const utxos = data;
if (utxos.length === 0) return 0;
const balance = utxos.reduce((acc, u) => acc + u.value, 0);
return balance;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
console.log(error);
return;
}
};

sweepWif = async (wif: string) => {
try {
const privKey = PrivateKey.fromWif(wif);
const tx = new Transaction();
const outScript = new P2PKH().lock(this.bsvAddress);
tx.addOutput({ lockingScript: outScript, change: true });

const { data } = await axios.get<WocUtxo[]>(`${WOC_BASE_URL}/address/${privKey.toAddress()}/unspent`);
const utxos = data;
if (utxos.length === 0) return;
const feeModel = new SatoshisPerKilobyte(this.chromeStorageService.getCustomFeeRate());
for await (const u of utxos || []) {
tx.addInput({
sourceTransaction: await this.oneSatSPV.getTx(u.tx_hash, true),
sourceOutputIndex: u.tx_pos,
sequence: 0xffffffff,
unlockingScriptTemplate: new P2PKH().unlock(privKey),
});
}
await tx.fee(feeModel);
await tx.sign();
const response = await this.oneSatSPV.broadcast(tx);
if (response.status == 'error') return { error: response.description };
const txid = tx.id('hex');
console.log('Change sweep:', txid);
return { txid, rawtx: Utils.toHex(tx.toBinary()) };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
console.log(error);
}
};

generateKeysFromWifAndStoreEncrypted = async (password: string, wifs: WifKeys, isNewWallet: boolean) => {
const { passKey, salt } = await this.getPassKeyAndSalt(password, isNewWallet);
const keys = getKeysFromWifs(wifs);
Expand Down
Loading