Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
17 changes: 16 additions & 1 deletion assets/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,8 @@
"seedIntroWarning": "This phrase is the main access to your\nassets, save and never share this phrase",
"seedSettings": "Seed phrase",
"errorDescription": "Error description",
"tryAgain": "Oops! Something went wrong. \nPlease try again. \nIf it didn't help - contact us.",
"tryAgain": "Try again",
"errorTryAgainSupportHint": "Something went wrong. Try again, or contact us if the problem continues.",
"customFeesWarning": "Only use custom fees if you know what you are doing!",
"fiatExchange": "Exchange",
"bridgeExchange": "Exchange",
Expand Down Expand Up @@ -419,6 +420,7 @@
"setMin": "Set Min",
"timeout": "Timeout",
"notEnoughBalanceForGasError": "Not enough balance to pay gas.",
"cannotSendToSelf": "Cannot send funds to the same address you are sending from. Please use a different recipient address.",
"notEnoughFundsError": "Not enough funds to perform a trade",
"dexErrorMessage": "Something went wrong!",
"dexUnableToStartSwap": "Unable to start swap. Refresh the quote and try again.",
Expand Down Expand Up @@ -482,6 +484,19 @@
"withdrawPreview": "Preview Withdrawal",
"withdrawPreviewZhtlcNote": "ZHTLC transactions can take a while to generate.\nPlease stay on this page until the preview is ready, otherwise you will need to start over.",
"withdrawPreviewError": "Error occurred while fetching withdrawal preview",
"withdrawTronBandwidthUsed": "Bandwidth (NET) used",
"withdrawTronBandwidthFee": "Bandwidth fee",
"withdrawTronBandwidthSource": "Bandwidth source",
"withdrawTronEnergyUsed": "Energy used",
"withdrawTronEnergyFee": "Energy fee",
"withdrawTronEnergySource": "Energy source",
"withdrawTronFeeSummary": "TRON fee summary",
"withdrawTronFeePaidIn": "Paid in {}",
"withdrawTronBandwidthCovered": "Covered by free NET bandwidth",
"withdrawTronEnergyCovered": "Covered by free energy",
"withdrawTronResourceNotUsed": "Not used",
"withdrawTronFeeSummaryCharged": "Network will charge {} {}.",
"withdrawTronFeeSummaryCovered": "No {} fee will be charged (covered by resources).",
"txHistoryFetchError": "Error fetching tx history from the endpoint. Unsupported type: {}",
"txHistoryNoTransactions": "Transactions are not available",
"maxGapLimitReached": "Maximum gap limit reached - please use existing unused addresses first",
Expand Down
5 changes: 5 additions & 0 deletions lib/app_config/app_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ const bool isBitrefillIntegrationEnabled = false;
///! trading purposes where it is not legally compliant.
const bool kShowTradingWarning = false;

/// Controls whether the HD mode warning banner is shown on the wallet page.
/// TODO: Replace this static flag with conditional visibility once we can
/// determine whether the wallet has previously been used in legacy mode.
const bool kShowHdWalletWarningBanner = false;

const Duration kPerformanceLogInterval = Duration(minutes: 1);

/// Enable debug logging for electrum connections and RPC methods.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,6 @@ class PortfolioGrowthBloc
filteredEventCoins,
delay: kActivationPollingInterval,
);

// Only remove inactivate/activating coins after an attempt to load the
// cached chart, as the cached chart may contain inactive coins.
await _loadChart(
Expand Down Expand Up @@ -196,9 +195,9 @@ class PortfolioGrowthBloc
PortfolioGrowthLoadRequested event, {
required bool useCache,
}) async {
final activeCoins = await coins.removeInactiveCoins(_sdk);
final chartCoins = useCache ? coins : await coins.removeInactiveCoins(_sdk);
final chart = await _portfolioGrowthRepository.getPortfolioGrowthChart(
activeCoins,
chartCoins,
fiatCoinId: event.fiatCoinId,
walletId: event.walletId,
useCache: useCache,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,26 @@ class PortfolioGrowthRepository {
);

if (transactions.isEmpty) {
_log.fine('No transactions found for ${coin.id}, caching empty chart');
// Insert an empty chart into the cache to avoid fetching transactions
// again for each invocation. The assumption is that this function is
// called later with useCache set to false to fetch the transactions again
_log.fine('No transactions found for ${coin.id}');

final String compoundKey = GraphCache.getPrimaryKey(
coinId: coinId.id,
fiatCoinId: fiatCoinId,
graphType: GraphType.balanceGrowth,
walletId: walletId,
isHdWallet: currentUser.isHd,
);
final existingCache = await _graphCache.get(compoundKey);
if (existingCache != null && existingCache.graph.isNotEmpty) {
_log.fine(
'Keeping existing non-empty cache for ${coin.id} '
'(${existingCache.graph.length} points) '
'instead of overwriting with empty transactions',
);
methodStopwatch.stop();
return existingCache.graph;
}

final cacheInsertStopwatch = Stopwatch()..start();
await _graphCache.insert(
GraphCache(
Expand Down
8 changes: 0 additions & 8 deletions lib/bloc/cex_market_data/profit_loss/profit_loss_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,6 @@ class ProfitLossBloc extends Bloc<ProfitLossEvent, ProfitLossState> {
final supportedCoins = await event.coins.filterSupportedCoins();
final filteredEventCoins = event.coins.withoutTestCoins();
final initialActiveCoins = await supportedCoins.removeInactiveCoins(_sdk);
// Charts for individual coins (coin details) are parsed here as well,
// and should be hidden if not supported.
if (supportedCoins.isEmpty && filteredEventCoins.length <= 1) {
return emit(
PortfolioProfitLossChartUnsupported(
Expand All @@ -78,8 +76,6 @@ class ProfitLossBloc extends Bloc<ProfitLossEvent, ProfitLossState> {
).then(emit.call).catchError((Object error, StackTrace stackTrace) {
const errorMessage = 'Failed to load CACHED portfolio profit/loss';
_log.warning(errorMessage, error, stackTrace);
// ignore cached errors, as the periodic refresh attempts should recover
// at the cost of a longer first loading time.
});

// Fetch the un-cached version of the chart to update the cache.
Expand All @@ -97,10 +93,6 @@ class ProfitLossBloc extends Bloc<ProfitLossEvent, ProfitLossState> {
useCache: false,
).then(emit.call).catchError((Object e, StackTrace s) {
_log.severe('Failed to load uncached profit/loss chart', e, s);
// Ignore un-cached errors, as a transaction loading exception should not
// make the graph disappear with a load failure emit, as the cached data
// is already displayed. The periodic updates will still try to fetch the
// data and update the graph.
});
}
} catch (error, stackTrace) {
Expand Down
19 changes: 18 additions & 1 deletion lib/bloc/cex_market_data/profit_loss/profit_loss_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,24 @@ class ProfitLossRepository {
);

if (transactions.isEmpty) {
_log.fine('No transactions found for ${coinId.id}, caching empty result');
_log.fine('No transactions found for ${coinId.id}');

final String compoundKey = ProfitLossCache.getPrimaryKey(
coinId: coinId.id,
fiatCurrency: fiatCoinId,
walletId: walletId,
isHdWallet: currentUser.isHd,
);
final existingCache = await _profitLossCacheProvider.get(compoundKey);
if (existingCache != null && existingCache.profitLosses.isNotEmpty) {
_log.fine(
'Keeping existing non-empty cache for ${coinId.id} '
'(${existingCache.profitLosses.length} entries) '
'instead of overwriting with empty transactions',
);
methodStopwatch.stop();
return existingCache.profitLosses;
}

final cacheInsertStopwatch = Stopwatch()..start();
await _profitLossCacheProvider.insert(
Expand Down
12 changes: 7 additions & 5 deletions lib/bloc/coins_bloc/asset_coin_extension.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ extension AssetCoinExtension on Asset {
platform: id.parentId?.id ?? platform ?? '',
contractAddress: contractAddress ?? '',
);
final explorerPattern = protocol.explorerPattern;

return Coin(
type: protocol.subClass.toCoinType(),
Expand All @@ -32,10 +33,9 @@ extension AssetCoinExtension on Asset {
name: id.name,
logoImageUrl: logoImageUrl ?? '',
isCustomCoin: isCustomToken,
explorerUrl: config.valueOrNull<String>('explorer_url') ?? '',
explorerTxUrl: config.valueOrNull<String>('explorer_tx_url') ?? '',
explorerAddressUrl:
config.valueOrNull<String>('explorer_address_url') ?? '',
explorerUrl: explorerPattern.baseUrl?.toString() ?? '',
explorerTxUrl: explorerPattern.txPattern ?? '',
explorerAddressUrl: explorerPattern.addressPattern ?? '',
protocolType: protocol.subClass.ticker,
protocolData: protocolData,
isTestCoin: protocol.isTestnet,
Expand Down Expand Up @@ -113,7 +113,7 @@ extension CoinTypeExtension on CoinSubClass {
case CoinSubClass.erc20:
return CoinType.erc20;
case CoinSubClass.grc20:
return CoinType.erc20;
return CoinType.grc20;
case CoinSubClass.krc20:
return CoinType.krc20;
case CoinSubClass.zhtlc:
Expand Down Expand Up @@ -209,6 +209,8 @@ extension CoinSubClassExtension on CoinType {
return CoinSubClass.smartBch;
case CoinType.erc20:
return CoinSubClass.erc20;
case CoinType.grc20:
return CoinSubClass.grc20;
case CoinType.krc20:
return CoinSubClass.krc20;
case CoinType.zhtlc:
Expand Down
99 changes: 87 additions & 12 deletions lib/bloc/withdraw_form/withdraw_form_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ class WithdrawFormBloc extends Bloc<WithdrawFormEvent, WithdrawFormState> {
on<WithdrawFormSubmitted>(_onSubmitted);
on<WithdrawFormCancelled>(_onCancelled);
on<WithdrawFormReset>(_onReset);
on<WithdrawFormStepReverted>(_onStepReverted);
on<WithdrawFormSourcesLoadRequested>(_onSourcesLoadRequested);
on<WithdrawFormFeeOptionsRequested>(_onFeeOptionsRequested);
on<WithdrawFormConvertAddressRequested>(_onConvertAddress);
Expand Down Expand Up @@ -86,9 +87,37 @@ class WithdrawFormBloc extends Bloc<WithdrawFormEvent, WithdrawFormState> {
return fallbackPrefix == null ? message : '$fallbackPrefix: $message';
}

String _extractTechnicalDetails(Object error) {
if (error is SdkError) {
return error.fallbackMessage;
}
if (error is MmRpcException) {
return error.message ?? error.toString();
}
if (error is GeneralErrorResponse) {
return error.error ?? error.toString();
}
if (error is WithdrawalException) {
return error.message;
}
return error.toString();
}

TextError _buildTextError(Object error, {String? fallbackPrefix}) {
return TextError(
error: _formatErrorMessage(error, fallbackPrefix: fallbackPrefix),
technicalDetails: _extractTechnicalDetails(error),
);
}

String _normalizeCommonErrors(String message) {
final normalized = message.toLowerCase();

if (normalized.contains('cannot transfer') &&
normalized.contains('to yourself')) {
return LocaleKeys.cannotSendToSelf.tr();
}

if (normalized.contains('insufficient') &&
(normalized.contains('gas') || normalized.contains('fee'))) {
return LocaleKeys.notEnoughBalanceForGasError.tr();
Expand Down Expand Up @@ -603,6 +632,17 @@ class WithdrawFormBloc extends Bloc<WithdrawFormEvent, WithdrawFormState> {
return;
}

if (_isSelfTransfer) {
emit(
state.copyWith(
previewError: () =>
TextError(error: LocaleKeys.cannotSendToSelf.tr()),
isSending: false,
),
);
return;
}

try {
emit(
state.copyWith(
Expand All @@ -617,9 +657,8 @@ class WithdrawFormBloc extends Bloc<WithdrawFormEvent, WithdrawFormState> {
emit(state.copyWith(isAwaitingTrezorConfirmation: true));
}

final preview = await _sdk.withdrawals.previewWithdrawal(
state.toWithdrawParameters(),
);
final params = state.toWithdrawParameters();
final preview = await _sdk.withdrawals.previewWithdrawal(params);

emit(
state.copyWith(
Expand All @@ -645,12 +684,8 @@ class WithdrawFormBloc extends Bloc<WithdrawFormEvent, WithdrawFormState> {

emit(
state.copyWith(
previewError: () => TextError(
error: _formatErrorMessage(
e,
fallbackPrefix: 'Failed to generate preview',
),
),
previewError: () =>
_buildTextError(e, fallbackPrefix: 'Failed to generate preview'),
isSending: false,
isAwaitingTrezorConfirmation: false,
),
Expand Down Expand Up @@ -749,9 +784,8 @@ class WithdrawFormBloc extends Bloc<WithdrawFormEvent, WithdrawFormState> {

emit(
state.copyWith(
transactionError: () => TextError(
error: _formatErrorMessage(e, fallbackPrefix: 'Transaction failed'),
),
transactionError: () =>
_buildTextError(e, fallbackPrefix: 'Transaction failed'),
step: WithdrawFormStep.failed,
isSending: false,
isAwaitingTrezorConfirmation: false,
Expand All @@ -763,6 +797,13 @@ class WithdrawFormBloc extends Bloc<WithdrawFormEvent, WithdrawFormState> {
bool get _isUnsupportedSiaHardwareWalletFlow =>
_walletType == WalletType.trezor && state.asset.protocol is SiaProtocol;

bool get _isSelfTransfer {
final source = state.selectedSourceAddress?.address;
final recipient = state.recipientAddress.trim();
if (source == null || recipient.isEmpty) return false;
return source == recipient;
}

void _onCancelled(
WithdrawFormCancelled event,
Emitter<WithdrawFormState> emit,
Expand All @@ -785,6 +826,40 @@ class WithdrawFormBloc extends Bloc<WithdrawFormEvent, WithdrawFormState> {
);
}

void _onStepReverted(
WithdrawFormStepReverted event,
Emitter<WithdrawFormState> emit,
) {
if (state.step == WithdrawFormStep.confirm) {
emit(
state.copyWith(
step: WithdrawFormStep.fill,
preview: () => null,
previewError: () => null,
transactionError: () => null,
isSending: false,
isAwaitingTrezorConfirmation: false,
),
);
return;
}

if (state.step != WithdrawFormStep.failed) return;

final nextStep = state.preview != null
? WithdrawFormStep.confirm
: WithdrawFormStep.fill;

emit(
state.copyWith(
step: nextStep,
transactionError: () => null,
isSending: false,
isAwaitingTrezorConfirmation: false,
),
);
}

bool _hasEthAddressMixedCase(String address) {
if (!address.startsWith('0x')) return false;
final chars = address.substring(2).split('');
Expand Down
15 changes: 15 additions & 0 deletions lib/generated/codegen_loader.g.dart
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,7 @@ abstract class LocaleKeys {
static const seedSettings = 'seedSettings';
static const errorDescription = 'errorDescription';
static const tryAgain = 'tryAgain';
static const errorTryAgainSupportHint = 'errorTryAgainSupportHint';
static const customFeesWarning = 'customFeesWarning';
static const fiatExchange = 'fiatExchange';
static const bridgeExchange = 'bridgeExchange';
Expand Down Expand Up @@ -451,6 +452,7 @@ abstract class LocaleKeys {
static const setMin = 'setMin';
static const timeout = 'timeout';
static const notEnoughBalanceForGasError = 'notEnoughBalanceForGasError';
static const cannotSendToSelf = 'cannotSendToSelf';
static const notEnoughFundsError = 'notEnoughFundsError';
static const dexErrorMessage = 'dexErrorMessage';
static const dexUnableToStartSwap = 'dexUnableToStartSwap';
Expand Down Expand Up @@ -519,6 +521,19 @@ abstract class LocaleKeys {
static const withdrawPreview = 'withdrawPreview';
static const withdrawPreviewZhtlcNote = 'withdrawPreviewZhtlcNote';
static const withdrawPreviewError = 'withdrawPreviewError';
static const withdrawTronBandwidthUsed = 'withdrawTronBandwidthUsed';
static const withdrawTronBandwidthFee = 'withdrawTronBandwidthFee';
static const withdrawTronBandwidthSource = 'withdrawTronBandwidthSource';
static const withdrawTronEnergyUsed = 'withdrawTronEnergyUsed';
static const withdrawTronEnergyFee = 'withdrawTronEnergyFee';
static const withdrawTronEnergySource = 'withdrawTronEnergySource';
static const withdrawTronFeeSummary = 'withdrawTronFeeSummary';
static const withdrawTronFeePaidIn = 'withdrawTronFeePaidIn';
static const withdrawTronBandwidthCovered = 'withdrawTronBandwidthCovered';
static const withdrawTronEnergyCovered = 'withdrawTronEnergyCovered';
static const withdrawTronResourceNotUsed = 'withdrawTronResourceNotUsed';
static const withdrawTronFeeSummaryCharged = 'withdrawTronFeeSummaryCharged';
static const withdrawTronFeeSummaryCovered = 'withdrawTronFeeSummaryCovered';
static const txHistoryFetchError = 'txHistoryFetchError';
static const txHistoryNoTransactions = 'txHistoryNoTransactions';
static const maxGapLimitReached = 'maxGapLimitReached';
Expand Down
1 change: 1 addition & 0 deletions lib/model/coin_type.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ enum CoinType {
sbch,
ubiq,
krc20,
grc20,
tendermintToken,
tendermint,
slp,
Expand Down
Loading
Loading