Skip to content

fix(nft): add nft v2 and hd wallet support #2566

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

Merged
merged 22 commits into from
Apr 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c54cb3b
refactor(nft): replace CoinsRepo with SDK in NFT api and repositories
takenagain Mar 10, 2025
c6e20ce
refactor(nft): use SDK and auth bloc for authentication status checks
takenagain Mar 10, 2025
1dcf9f5
fix(nft-v2): add enable nft request before update nft request
takenagain Mar 11, 2025
dffb9b8
feat(nft): HD wallet address selection support
takenagain Apr 1, 2025
09a16b1
refactor(nft): use SDK Asset and PubkeyInfo for trezor NFT repositories
takenagain Apr 1, 2025
82f8519
fix(withdraw): evm address conversion format error
takenagain Apr 1, 2025
1f30db7
refactor(nft): rename NFT Main bloc events and states
takenagain Apr 2, 2025
a0e3d3d
fix(nft-image): add gateway pattern support to imageUrl parser
takenagain Apr 2, 2025
4a4cfb2
feat(nft): copyable address selection dialog
takenagain Apr 2, 2025
32ebdf4
Merge branch 'dev' into bugfix/nft-hd-support
CharlVS Apr 11, 2025
6897183
Merge remote-tracking branch 'origin/dev' into bugfix/nft-hd-support
takenagain Apr 16, 2025
7fad4d1
refactor: remove unused imports
takenagain Apr 16, 2025
c83c716
refactor: fix unused mixedcase event handler and clarify trezor todo
takenagain Apr 16, 2025
aaade1c
fix(nft): Fixed failing address conversions
CharlVS Apr 18, 2025
1ee054c
fix: Migrate withdrawal errors to SDK
CharlVS Apr 18, 2025
1021e55
Merge branch 'dev' into bugfix/nft-hd-support
CharlVS Apr 18, 2025
f0b839f
fix: nft request incorrect amount type
CharlVS Apr 21, 2025
ea6a4aa
fix(nft): Fix missing NFT preview URLs
CharlVS Apr 23, 2025
710272c
Merge branch 'dev' of https://github.com/KomodoPlatform/komodo-wallet…
CharlVS Apr 23, 2025
d9e958a
Merge branch 'dev' into bugfix/nft-hd-support
CharlVS Apr 24, 2025
82ed17f
fix(nft): show warning if selecting non-active receive address
CharlVS Apr 25, 2025
f540d8b
formatting: Apply formatting/fixes to NFT code
CharlVS Apr 25, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ class TextThemeExtension extends ThemeExtension<TextThemeExtension> {
fontWeight: FontWeight.w700,
color: textColor,
);

static TextThemeExtension of(BuildContext context) {
final textTheme = Theme.of(context).extension<TextThemeExtension>();
assert(textTheme != null, 'TextThemeExtension not found in context');
return textTheme!;
}

final TextStyle heading1;
final TextStyle heading2;
final TextStyle bodyM;
Expand Down
2 changes: 2 additions & 0 deletions assets/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,8 @@
"collectibles": "Collectibles",
"sendingProcess": "Sending process",
"ercStandardDisclaimer": "Send only ERC721 and ERC1155 standard tokens on this address",
"nftReceiveNonSwapAddressWarning": "Non-Active Swap Address Selected",
"nftReceiveNonSwapWalletDetails": "You will be able to receive NFTs to this address, but they will not show up in Komodo Wallet. You will need to use a different wallet with bip39/HD support to access these NFTs.",
"nftMainLoggedOut": "There's nothing here yet, please connect your wallet",
"confirmLogoutOnAnotherTab": "You are already logged in to this wallet. Do you want to log out from another session?",
"refreshList": "Refresh {} NFT list",
Expand Down
16 changes: 13 additions & 3 deletions lib/app_config/app_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ Map<String, int> priorityCoinsAbbrMap = {
'MOVR': 10,
};

/// List of coins that are excluded from the list of coins displayed on the
/// coin lists (e.g. wallet page, coin selection dropdowns, etc.)
const List<String> excludedAssetList = [
'ADEXBSCT',
'ADEXBSC',
Expand All @@ -78,9 +80,8 @@ const List<String> excludedAssetList = [
'RICK',
'MORTY',

// NFT v2 coins: https://github.com/KomodoPlatform/coins/pull/1061
// NFT upgrade is not merged yet, and the coins will likely be used in the
// background, so users do not need to see them.
// NFT v2 coins: https://github.com/KomodoPlatform/coins/pull/1061 will be
// used in the background, so users do not need to see them.
'NFT_ETH',
'NFT_AVAX',
'NFT_BNB',
Expand Down Expand Up @@ -113,6 +114,8 @@ const List<String> appWalletOnlyAssetList = [
'SUPERNET',
];

/// Coins that are enabled by default on restore from seed or registration.
/// This will not affect existing wallets.
List<String> get enabledByDefaultCoins => [
'BTC-segwit',
'KMD',
Expand All @@ -124,6 +127,13 @@ List<String> get enabledByDefaultCoins => [
'FTM',
if (kDebugMode) 'DOC',
if (kDebugMode) 'MARTY',

// NFT v2 methods require the new NFT coins to be enabled by default.
'NFT_ETH',
'NFT_AVAX',
'NFT_BNB',
'NFT_FTM',
'NFT_MATIC',
];

List<String> get enabledByDefaultTrezorCoins => [
Expand Down
5 changes: 1 addition & 4 deletions lib/bloc/app_bloc_root.dart
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ import 'package:web_dex/blocs/trezor_coins_bloc.dart';
import 'package:web_dex/blocs/wallets_repository.dart';
import 'package:web_dex/main.dart';
import 'package:web_dex/mm2/mm2_api/mm2_api.dart';
import 'package:web_dex/model/authorize_mode.dart';
import 'package:web_dex/model/main_menu_value.dart';
import 'package:web_dex/model/stored_settings.dart';
import 'package:web_dex/router/navigators/app_router_delegate.dart';
Expand Down Expand Up @@ -263,9 +262,7 @@ class AppBlocRoot extends StatelessWidget {
lazy: false,
create: (context) => NftMainBloc(
repo: context.read<NftsRepo>(),
kdfSdk: komodoDefiSdk,
isLoggedIn:
context.read<AuthBloc>().state.mode == AuthorizeMode.logIn,
sdk: komodoDefiSdk,
),
),
if (isBitrefillIntegrationEnabled)
Expand Down
1 change: 1 addition & 0 deletions lib/bloc/coins_bloc/coins_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:flutter/foundation.dart';
import 'package:komodo_defi_sdk/komodo_defi_sdk.dart';
import 'package:komodo_defi_types/komodo_defi_types.dart';
import 'package:logging/logging.dart';
import 'package:web_dex/app_config/app_config.dart';
import 'package:web_dex/bloc/coins_bloc/coins_repo.dart';
import 'package:web_dex/blocs/trezor_coins_bloc.dart';
import 'package:web_dex/mm2/mm2_api/mm2_api.dart';
Expand Down
47 changes: 36 additions & 11 deletions lib/bloc/coins_bloc/coins_state.dart
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
part of 'coins_bloc.dart';

class CoinsState extends Equatable {
const CoinsState({
required this.coins,
required this.walletCoins,
/// The list of available and activated assets to be displayed in the app.
/// This list is filtered to exclude assets not intended to be shown to the
/// user. E.g. NFT assets.
CoinsState({
required Map<String, Coin> coins,
required Map<String, Coin> walletCoins,
required this.loginActivationFinished,
required this.pubkeys,
required this.prices,
});
}) : coins = _filterExcludedAssets(coins),
walletCoins = _filterExcludedAssets(walletCoins);

factory CoinsState.initial() => const CoinsState(
coins: {},
walletCoins: {},
factory CoinsState.initial() => CoinsState(
coins: const {},
walletCoins: const {},
loginActivationFinished: false,
pubkeys: {},
prices: {},
pubkeys: const {},
prices: const {},
);

final Map<String, Coin> coins;
Expand All @@ -27,23 +31,44 @@ class CoinsState extends Equatable {
List<Object> get props =>
[coins, walletCoins, loginActivationFinished, pubkeys, prices];

/// Creates a copy of the current state with the option to update
/// specific fields.
/// NOTE: This method filters the coins and walletCoins maps to exclude
/// assets that should not be shown to the user.
CoinsState copyWith({
Map<String, Coin>? coins,
Map<String, Coin>? walletCoins,
bool? loginActivationFinished,
Map<String, AssetPubkeys>? pubkeys,
Map<String, CexPrice>? prices,
}) {
// Filtering is required to avoid including "NFT_*" assets in the coins
// or walletCoins maps. The user should not see these assets, as they are
// only needed to support the NFT feature.
final walletCoinsWithoutExcludedCoins =
_filterExcludedAssets(walletCoins ?? this.walletCoins);
final coinsWithoutExcludedCoins =
_filterExcludedAssets(coins ?? this.coins);

return CoinsState(
coins: coins ?? this.coins,
walletCoins: walletCoins ?? this.walletCoins,
coins: coinsWithoutExcludedCoins,
walletCoins: walletCoinsWithoutExcludedCoins,
loginActivationFinished:
loginActivationFinished ?? this.loginActivationFinished,
pubkeys: pubkeys ?? this.pubkeys,
prices: prices ?? this.prices,
);
}

static Map<String, Coin> _filterExcludedAssets(Map<String, Coin> coins) {
return Map.fromEntries(
coins.entries.where((entry) {
final coinId = entry.key;
return !excludedAssetList.contains(coinId);
}),
);
}

/// Gets the price for a given asset ID
CexPrice? getPriceForAsset(AssetId assetId) {
return prices[assetId.symbol.configSymbol.toUpperCase()];
Expand Down
121 changes: 72 additions & 49 deletions lib/bloc/nft_receive/bloc/nft_receive_bloc.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:komodo_defi_sdk/komodo_defi_sdk.dart';
import 'package:komodo_defi_types/komodo_defi_types.dart';
import 'package:logging/logging.dart';
import 'package:web_dex/bloc/coins_bloc/coins_repo.dart';
import 'package:web_dex/model/coin.dart';
import 'package:web_dex/model/kdf_auth_metadata_extension.dart';
import 'package:web_dex/model/nft.dart';
import 'package:web_dex/views/dex/dex_helpers.dart';

part 'nft_receive_event.dart';
part 'nft_receive_state.dart';
Expand All @@ -17,72 +17,95 @@ class NftReceiveBloc extends Bloc<NftReceiveEvent, NftReceiveState> {
}) : _coinsRepo = coinsRepo,
_sdk = sdk,
super(NftReceiveInitial()) {
on<NftReceiveEventInitial>(_onInitial);
on<NftReceiveEventRefresh>(_onRefresh);
on<NftReceiveEventChangedAddress>(_onChangeAddress);
on<NftReceiveStarted>(_onInitial);
on<NftReceiveRefreshRequested>(_onRefresh);
on<NftReceiveAddressChanged>(_onChangeAddress);
}

final CoinsRepo _coinsRepo;
final KomodoDefiSdk _sdk;
final _log = Logger('NftReceiveBloc');
NftBlockchains? chain;

Future<void> _onInitial(NftReceiveEventInitial event, Emitter emit) async {
if (state is! NftReceiveAddress) {
chain = event.chain;
final abbr = event.chain.coinAbbr();
var coin = _coinsRepo.getCoin(abbr);

if (coin != null) {
final walletConfig = (await _sdk.currentWallet())?.config;
if (walletConfig?.hasBackup == false && !coin.isTestCoin) {
return emit(
NftReceiveHasBackup(),
);
}
Future<void> _onInitial(
NftReceiveStarted event,
Emitter<NftReceiveState> emit,
) async {
if (state is NftReceiveLoadSuccess) {
_log.fine('Already in NftReceiveAddress state, skipping initialization');
return;
}

if (coin.address?.isEmpty ?? true) {
final activationErrors =
await activateCoinIfNeeded(coin.abbr, _coinsRepo);
if (activationErrors.isNotEmpty) {
return emit(
NftReceiveFailure(
message: activationErrors.first.error,
),
);
}
coin = _coinsRepo.getCoin(abbr)!;
}
chain = event.chain;
final abbr = event.chain.coinAbbr();
final coin = _coinsRepo.getCoin(abbr);
if (coin == null) {
_log.warning('Failed to find coin for chain: ${event.chain}');
return emit(const NftReceiveLoadFailure());
}

return emit(
NftReceiveAddress(
coin: coin,
address: coin.defaultAddress,
),
);
}
final walletConfig = (await _sdk.currentWallet())?.config;
if (walletConfig?.hasBackup == false && !coin.isTestCoin) {
_log.warning('Wallet does not have backup and is not a test coin');
return emit(
NftReceiveBackupSuccess(),
);
}

return emit(const NftReceiveFailure());
final asset = _sdk.assets.available[coin.id]!;
final pubkeys = await _sdk.pubkeys.getPubkeys(asset);
if (pubkeys.keys.isEmpty) {
_log.warning('No pubkey found for the asset: ${coin.id.id}');
return emit(
const NftReceiveLoadFailure(message: 'No pubkey found for the asset'),
);
}

// Select the first address by default
final selectedAddress = pubkeys.keys.first;
_log.info('Successfully initialized, address: ${selectedAddress.address}');
return emit(
NftReceiveLoadSuccess(
asset: asset,
pubkeys: pubkeys,
selectedAddress: selectedAddress,
),
);
}

Future<void> _onRefresh(NftReceiveEventRefresh event, Emitter emit) async {
Future<void> _onRefresh(
NftReceiveRefreshRequested event,
Emitter<NftReceiveState> emit,
) async {
_log.info('Refreshing NFT receive data');
final localChain = chain;
if (localChain != null) {
emit(NftReceiveEventInitial(chain: localChain));
add(NftReceiveEventInitial(chain: localChain));
_log.fine('Chain is available, reinitializing with chain: $localChain');
emit(NftReceiveInitial());
add(NftReceiveStarted(chain: localChain));
} else {
return emit(const NftReceiveFailure());
_log.warning('Cannot refresh - chain is null');
return emit(const NftReceiveLoadFailure());
}
}

void _onChangeAddress(NftReceiveEventChangedAddress event, Emitter emit) {
void _onChangeAddress(
NftReceiveAddressChanged event,
Emitter<NftReceiveState> emit,
) {
_log.info('Changing selected address to: ${event.address}');
final state = this.state;
if (state is NftReceiveAddress) {
return emit(
state.copyWith(
address: event.address,
),
);
if (state is! NftReceiveLoadSuccess) {
_log.warning('Cannot change address - not in NftReceiveAddress state');
return;
}
// Find the matching pubkey info from pubkeys
final selectedPubkey = state.pubkeys.keys.firstWhere(
(PubkeyInfo key) => key.address == event.address?.address,
orElse: () => state.selectedAddress!,
);

_log.fine('Selected pubkey: ${selectedPubkey.address}');
return emit(state.copyWith(selectedAddress: selectedPubkey));
}
}
20 changes: 11 additions & 9 deletions lib/bloc/nft_receive/bloc/nft_receive_event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,30 @@ abstract class NftReceiveEvent extends Equatable {
const NftReceiveEvent();

@override
List<Object> get props => [];
List<Object?> get props => [];
}

class NftReceiveEventInitial extends NftReceiveEvent {
class NftReceiveStarted extends NftReceiveEvent {
const NftReceiveStarted({required this.chain});

final NftBlockchains chain;
const NftReceiveEventInitial({required this.chain});

@override
List<Object> get props => [chain];
}

class NftReceiveEventRefresh extends NftReceiveEvent {
const NftReceiveEventRefresh();
class NftReceiveRefreshRequested extends NftReceiveEvent {
const NftReceiveRefreshRequested();

@override
List<Object> get props => [];
}

class NftReceiveEventChangedAddress extends NftReceiveEvent {
final String? address;
const NftReceiveEventChangedAddress({required this.address});
class NftReceiveAddressChanged extends NftReceiveEvent {
const NftReceiveAddressChanged({required this.address});

final PubkeyInfo? address;

@override
List<Object> get props => [address ?? ''];
List<Object?> get props => [address];
}
Loading
Loading