Skip to content
Closed
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
6 changes: 6 additions & 0 deletions lavamoat/webpack/policy-override.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
{
"resources": {
"cypress>lodash": {
"globals": {
"setTimeout": true,
"clearTimeout": true
}
},
"bootstrap": {
"globals": {
"document": true,
Expand Down
51 changes: 47 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"productName": "Hathor Wallet",
"description": "Light wallet for Hathor Network",
"author": "Hathor Labs <contact@hathor.network> (https://hathor.network/)",
"version": "0.34.0",
"version": "0.35.0",
"engines": {
"node": ">=22.0.0",
"npm": ">=10.0.0"
Expand All @@ -38,7 +38,7 @@
},
"dependencies": {
"@hathor/hathor-rpc-handler": "4.4.0",
"@hathor/wallet-lib": "3.0.1",
"@hathor/wallet-lib": "3.1.1",
"@ledgerhq/hw-transport-node-hid": "6.28.1",
"@reduxjs/toolkit": "2.2.3",
"@reown/walletkit": "1.1.2",
Expand Down
6 changes: 5 additions & 1 deletion src/components/ModalTokenImport.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { GlobalModalContext, MODAL_TYPES } from './GlobalModal';
import { tokenRegisterRequested } from '../actions/index';
import { getGlobalWallet } from '../modules/wallet';
import walletUtils from '../utils/wallet';
import helpers from '../utils/helpers';
import { colors } from '../constants';

/**
Expand Down Expand Up @@ -337,7 +338,10 @@ export default function ModalTokenImport({ onClose, manageDomLifecycle }) {
<a
className="token-uid"
href={explorerLink}
target="_blank"
onClick={(e) => {
e.preventDefault();
helpers.openExternalURL(explorerLink);
}}
rel="noopener noreferrer"
title={uid}
>
Expand Down
39 changes: 38 additions & 1 deletion src/components/ModalTransactionOverview.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/

import React, { useEffect, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { t } from 'ttag';
import $ from 'jquery';
import PropTypes from 'prop-types';
Expand All @@ -15,6 +15,7 @@ import { useSelector } from 'react-redux';
import helpers from '../utils/helpers';
import { TOKEN_FEE_RFC_URL, colors } from '../constants';
import SendTxHandler from './SendTxHandler';
import { getGlobalWallet } from '../modules/wallet';

const MODAL_ID = 'transactionOverviewModal';

Expand All @@ -39,13 +40,27 @@ function ModalTransactionOverview({
const [pin, setPin] = useState('');
const [phase, setPhase] = useState('review');
const [errorMessage, setErrorMessage] = useState('');
const [pinError, setPinError] = useState('');
const [preparedTx, setPreparedTx] = useState(null);
const pinInputRef = useRef(null);
const tokenMetadata = useSelector((state) => state.tokenMetadata);

useEffect(() => {
manageDomLifecycle(`#${MODAL_ID}`);
}, [manageDomLifecycle]);

// Focus the PIN field after the modal finishes fading in. `autoFocus` alone
// fails on first open: the input mounts while the modal is still `display:none`,
// so the browser drops the focus. `autoFocus` is still kept — it covers the
// error -> "Try again" -> review remount, where the modal is already visible.
useEffect(() => {
const onShown = () => pinInputRef.current?.focus();
$(`#${MODAL_ID}`).on('shown.bs.modal.focuspin', onShown);
return () => {
$(`#${MODAL_ID}`).off('shown.bs.modal.focuspin', onShown);
};
}, []);

const fee = typeof totalFee === 'bigint' ? totalFee : BigInt(totalFee || 0);
const hasAnyFee = fee > 0n;

Expand Down Expand Up @@ -95,10 +110,28 @@ function ModalTransactionOverview({
const handleCancel = () => {
$(`#${MODAL_ID}`).modal('hide');
setPin('');
setPinError('');
onCancel();
};

const handleConfirm = async () => {
// Validate the PIN before attempting the send so a wrong PIN surfaces as
// "Invalid PIN" on the review phase instead of a generic send failure. A
// missing wallet or a checkPin failure is a real error, not a wrong PIN, so
// route it to the error phase instead of leaving the rejection unhandled.
try {
const wallet = getGlobalWallet();
if (!await wallet.checkPin(pin)) {
setPinError(t`Invalid PIN`);
return;
}
} catch (e) {
setErrorMessage(e.message || t`Error validating PIN.`);
setPhase('error');
return;
}

setPinError('');
setPhase('sending');
setErrorMessage('');

Expand Down Expand Up @@ -129,13 +162,15 @@ function ModalTransactionOverview({
const handleRetry = () => {
setPreparedTx(null);
setErrorMessage('');
setPinError('');
setPin('');
setPhase('review');
};

const handlePinChange = (e) => {
const value = e.target.value.replace(/\D/g, '').slice(0, 6);
setPin(value);
setPinError('');
};

// --- Render helpers for the review phase ---
Expand Down Expand Up @@ -291,6 +326,7 @@ function ModalTransactionOverview({
<div className="form-group">
<label htmlFor="pinInput">{t`Pin* (6 digit password)`}</label>
<input
ref={pinInputRef}
type="password"
className="form-control"
id="pinInput"
Expand All @@ -300,6 +336,7 @@ function ModalTransactionOverview({
autoFocus
autoComplete="off"
/>
{pinError && <p className="text-danger mt-2 mb-0">{pinError}</p>}
</div>
</>
);
Expand Down
17 changes: 11 additions & 6 deletions src/reducers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -670,18 +670,23 @@ const removeTokenMetadata = (state, action) => {
delete newMeta[uid];
}

// If the token has zero balance we should remove the balance data
// Drop a zero-balance unregistered token from allTokens and tokensBalance
// together (fetchUnknownTokens reads tokensBalance[uid].data for every allTokens
// entry): a leftover keeps the "Import Tokens" banner advertising a token the
// modal can't resolve. Tokens with a balance stay, still re-importable.
const newBalance = Object.assign({}, state.tokensBalance);
if (uid in newBalance && (!!newBalance[uid].data)) {
const balance = newBalance[uid].data;
if ((balance.available + balance.locked) === 0n) {
delete newBalance[uid];
}
const newAllTokens = Object.assign({}, state.allTokens);
const balance = newBalance[uid]?.data;
if (balance && (balance.available + balance.locked) === 0n) {
delete newBalance[uid];
delete newAllTokens[uid];
}

return {
...state,
tokenMetadata: newMeta,
tokensBalance: newBalance,
allTokens: newAllTokens,
};
};

Expand Down
17 changes: 13 additions & 4 deletions src/sagas/featureToggle.js
Original file line number Diff line number Diff line change
Expand Up @@ -250,18 +250,27 @@ export function mapFeatureToggles(toggles) {
}, {});
}

export function* handleToggleUpdate() {
/**
* Re-hydrate state.featureToggles from the Unleash singleton after clean_data
* wipes it, so a post-restart checkForFeatureFlag read isn't stale.
*
* Doesn't dispatch FEATURE_TOGGLE_UPDATED — it can reload the wallet, unsafe
* from inside startWallet.
*/
export function* syncFeatureTogglesFromClient() {
const unleashClient = getUnleashClient();
const featureTogglesInitialized = yield select((state) => state.featureTogglesInitialized);

if (!unleashClient || !featureTogglesInitialized) {
return;
}

const { toggles } = unleashClient;
const featureToggles = mapFeatureToggles(toggles);

const featureToggles = mapFeatureToggles(unleashClient.toggles);
yield put(setFeatureToggles(featureToggles));
}

export function* handleToggleUpdate() {
yield call(syncFeatureTogglesFromClient);
yield put({ type: 'FEATURE_TOGGLE_UPDATED' });
}

Expand Down
7 changes: 6 additions & 1 deletion src/sagas/wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ import {
dispatchLedgerTokenSignatureVerification,
} from './helpers';
import { fetchTokenData, restoreTokensForNetwork } from './tokens';
import { updateUnleashClientContext } from './featureToggle';
import { updateUnleashClientContext, syncFeatureTogglesFromClient } from './featureToggle';
import walletUtils from '../utils/wallet';
import tokensUtils from '../utils/tokens';
import nanoUtils from '../utils/nanoContracts';
Expand Down Expand Up @@ -159,6 +159,11 @@ export function* startWallet(action) {

yield put(loadingAddresses(true));

// Refresh the mirror before the checkForFeatureFlag reads below: the passphrase
// flow reaches startWallet right after clean_data wiped it, which would make the
// single-address read fall back to multi.
yield call(syncFeatureTogglesFromClient);

if (hardware) {
// We need to ensure that the hardware wallet storage is always generated here since we may be
// starting the wallet with a second device and so we cannot trust the xpub saved on storage.
Expand Down
16 changes: 1 addition & 15 deletions src/screens/SendTokens.js
Original file line number Diff line number Diff line change
Expand Up @@ -522,7 +522,7 @@ function SendTokens() {
decimalPlaces,
onClose: () => {
globalModalContext.hideModal();
resetForm();
navigate('/wallet/');
},
onViewDetails: () => {
console.log('View tx details:', tx.hash, tx);
Expand Down Expand Up @@ -596,20 +596,6 @@ function SendTokens() {
});
};

/**
* Reset form to initial state after successful send
*/
const resetForm = () => {
setTxTokens([...getSelectedToken()]);
setDataOutputs([]);
setTokenFees({});
setTokenChangeOutputs({});
setFeeError('');
setErrorMessage('');
references.current = [React.createRef()];
dataOutputRefs.current = {};
};

/**
* Group outputs by token for display in the transaction overview modal.
* Returns an array of { token, outputs, total } where total only includes
Expand Down
Loading