diff --git a/cw_core/lib/exceptions.dart b/cw_core/lib/exceptions.dart index cc0729ee5..76d4782ad 100644 --- a/cw_core/lib/exceptions.dart +++ b/cw_core/lib/exceptions.dart @@ -55,6 +55,12 @@ class SignSPLTokenTransactionRentException implements Exception {} class NoAssociatedTokenAccountException implements Exception {} +class AmbiguousTokenSymbolException implements Exception { + AmbiguousTokenSymbolException(this.symbol); + + final String symbol; +} + class RestoreFromSeedException implements Exception { final String message; diff --git a/cw_core/lib/pending_transaction.dart b/cw_core/lib/pending_transaction.dart index 3d8912fa3..fb4031936 100644 --- a/cw_core/lib/pending_transaction.dart +++ b/cw_core/lib/pending_transaction.dart @@ -12,6 +12,7 @@ mixin PendingTransaction { Money get amount; Money get fee; + Money? get additionalCost => null; String get amountFormatted; String get feeFormatted => fee.toStringWithSymbol(fractionalDigits: 8); diff --git a/cw_core/lib/spl_token.dart b/cw_core/lib/spl_token.dart index 5ac9330fb..140b50bb4 100644 --- a/cw_core/lib/spl_token.dart +++ b/cw_core/lib/spl_token.dart @@ -3,7 +3,6 @@ import "package:cw_core/db/sqlite.dart"; import "package:sqflite/sqflite.dart"; class SPLToken extends CryptoCurrency { - SPLToken({ required this.name, required this.symbol, @@ -33,17 +32,19 @@ class SPLToken extends CryptoCurrency { required String mint, required String symbol, required String mintAddress, + required int decimal, String? iconPath, bool isPotentialScam = false, - }) => SPLToken( - name: name, - symbol: symbol, - mintAddress: mintAddress, - decimal: 0, - mint: mint, - iconPath: iconPath, - isPotentialScam: isPotentialScam, - ); + }) => + SPLToken( + name: name, + symbol: symbol, + mintAddress: mintAddress, + decimal: decimal, + mint: mint, + iconPath: iconPath, + isPotentialScam: isPotentialScam, + ); SPLToken.copyWith(SPLToken other, {String? icon, String? tag, bool? enabled, String? walletName}) : name = other.name, @@ -125,18 +126,18 @@ class SPLToken extends CryptoCurrency { } Map toMap() => { - selfIdColumn: id, - "walletName": walletName, - "name": name, - "symbol": symbol, - "mintAddress": mintAddress, - "decimal": decimal, - "mint": mint, - "enabled": _enabled ? 1 : 0, - "iconPath": iconPath, - "tag": tag, - "isPotentialScam": isPotentialScam ? 1 : 0, - }; + selfIdColumn: id, + "walletName": walletName, + "name": name, + "symbol": symbol, + "mintAddress": mintAddress, + "decimal": decimal, + "mint": mint, + "enabled": _enabled ? 1 : 0, + "iconPath": iconPath, + "tag": tag, + "isPotentialScam": isPotentialScam ? 1 : 0, + }; static String get tableName => "SPLToken"; static String get selfIdColumn => "${tableName}Id"; @@ -166,7 +167,8 @@ class SPLToken extends CryptoCurrency { return List.generate(list.length, (index) => SPLToken.fromMap(list[index])); } - static Future> getAllForWallet(String walletName) async => selectList("walletName = ?", [walletName]); + static Future> getAllForWallet(String walletName) async => + selectList("walletName = ?", [walletName]); static Future getByMint(String walletName, String mintAddress) async { final list = await selectList("walletName = ? AND mintAddress = ?", [walletName, mintAddress]); @@ -174,12 +176,13 @@ class SPLToken extends CryptoCurrency { } static Future deleteForWallet(String walletName, String mintAddress) => db!.delete( - tableName, - where: "walletName = ? AND mintAddress = ?", - whereArgs: [walletName, mintAddress], - ); + tableName, + where: "walletName = ? AND mintAddress = ?", + whereArgs: [walletName, mintAddress], + ); - static Future deleteAllForWallet(String walletName) => db!.delete(tableName, where: "walletName = ?", whereArgs: [walletName]); + static Future deleteAllForWallet(String walletName) => + db!.delete(tableName, where: "walletName = ?", whereArgs: [walletName]); static Future renameWallet(String oldName, String newName) async { await db!.delete(tableName, where: "walletName = ?", whereArgs: [newName]); diff --git a/cw_core/test/parse_fixed_test.dart b/cw_core/test/parse_fixed_test.dart index c102618e1..b9e905208 100644 --- a/cw_core/test/parse_fixed_test.dart +++ b/cw_core/test/parse_fixed_test.dart @@ -45,6 +45,19 @@ void main() { test('should fail to parse `.`, missing value', () => expect(() => parseFixed(".", 6), throwsFormatException)); }); + + group("parseFixed, zero decimal currency", () { + test("should parse 5 as 5", () => expect(parseFixed("5", 0), BigInt.from(5))); + + test("should parse 0 as 0", () => expect(parseFixed("0", 0), BigInt.from(0))); + + test("should parse 1. as 1", () => expect(parseFixed("1.", 0), BigInt.from(1))); + + test("should parse -3 as -3", () => expect(parseFixed("-3", 0), BigInt.from(-3))); + + test("should fail to parse 5.5, fractional component exceeds decimals", + () => expect(() => parseFixed("5.5", 0), throwsFormatException)); + }); }); group('tryParseFixed', () { diff --git a/cw_core/test/spl_token_amount_test.dart b/cw_core/test/spl_token_amount_test.dart new file mode 100644 index 000000000..d686b8c46 --- /dev/null +++ b/cw_core/test/spl_token_amount_test.dart @@ -0,0 +1,104 @@ +import "package:cw_core/amount/money.dart"; +import "package:cw_core/spl_token.dart"; +import "package:flutter_test/flutter_test.dart"; + +void main() { + group("SPL token raw amounts", () { + test("6 decimal token renders raw base units", () { + final jup = SPLToken( + name: "Jupiter", + symbol: "JUP", + mint: "jup", + mintAddress: "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN", + decimal: 6, + ); + + expect(Money(BigInt.from(100000), jup).toStringWithPrecision(), "0.1"); + expect(Money(BigInt.from(931614), jup).toStringWithPrecision(), "0.931614"); + }); + + test("8 decimal token renders raw base units", () { + final xstock = SPLToken( + name: "Amazon xStock", + symbol: "AMZNX", + mint: "amznx", + mintAddress: "mintAddressWithEightDecimals", + decimal: 8, + ); + + expect(Money(BigInt.from(341694), xstock).toStringWithPrecision(), "0.00341694"); + }); + + test("the same raw amount means different values per decimals", () { + final raw = BigInt.from(100000); + + final six = SPLToken( + name: "Six", + symbol: "SIX", + mint: "six", + mintAddress: "mintAddressWithSixDecimals", + decimal: 6, + ); + + final nine = SPLToken( + name: "Nine", + symbol: "NINE", + mint: "nine", + mintAddress: "mintAddressWithNineDecimals", + decimal: 9, + ); + + final zero = SPLToken( + name: "Zero", + symbol: "ZERO", + mint: "zero", + mintAddress: "mintAddressWithZeroDecimals", + decimal: 0, + ); + + expect(Money(raw, six).toStringWithPrecision(), "0.1"); + expect(Money(raw, nine).toStringWithPrecision(), "0.0001"); + expect(Money(raw, zero).toStringWithPrecision(), "100000"); + }); + }); + + group("SPL token amount parsing", () { + test("parses whole amounts for a zero decimal token", () { + final zero = SPLToken( + name: "Zero", + symbol: "ZERO", + mint: "zero", + mintAddress: "mintAddressWithZeroDecimals", + decimal: 0, + ); + + expect(Money.parse("5", zero).amount, BigInt.from(5)); + expect(Money.parse("0", zero).amount, BigInt.zero); + }); + + test("parses fractional amounts for a 6 decimal token", () { + final jup = SPLToken( + name: "Jupiter", + symbol: "JUP", + mint: "jup", + mintAddress: "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN", + decimal: 6, + ); + + expect(Money.parse("0.1", jup).amount, BigInt.from(100000)); + expect(Money.parse("10.339089", jup).amount, BigInt.from(10339089)); + }); + + test("rejects more fractional digits than the token has decimals", () { + final zero = SPLToken( + name: "Zero", + symbol: "ZERO", + mint: "zero", + mintAddress: "mintAddressWithZeroDecimals", + decimal: 0, + ); + + expect(() => Money.parse("5.5", zero), throwsFormatException); + }); + }); +} diff --git a/cw_solana/lib/default_spl_tokens.dart b/cw_solana/lib/default_spl_tokens.dart index c9dd6fac0..48ca274a3 100644 --- a/cw_solana/lib/default_spl_tokens.dart +++ b/cw_solana/lib/default_spl_tokens.dart @@ -87,7 +87,7 @@ class DefaultSPLTokens { symbol: 'GMT', mintAddress: '7i5KKsX2weiTkry7jA4ZwSuXGhs5eJBEjY8vVxR4pfRx', decimal: 9, - mint: 'ray', + mint: 'gmt', iconPath: 'assets/images/gmt_icon.png', enabled: false, ), diff --git a/cw_solana/lib/pending_solana_transaction.dart b/cw_solana/lib/pending_solana_transaction.dart index 2f4f614d5..4594633b8 100644 --- a/cw_solana/lib/pending_solana_transaction.dart +++ b/cw_solana/lib/pending_solana_transaction.dart @@ -13,6 +13,7 @@ class PendingSolanaTransaction with PendingTransaction { required this.serializedTransaction, required this.destinationAddress, required this.sendTransaction, + this.additionalCost, }); @override @@ -22,15 +23,10 @@ class PendingSolanaTransaction with PendingTransaction { final Money fee; @override - String get amountFormatted { - String stringifiedAmount = amount.toString(); + final Money? additionalCost; - if (stringifiedAmount.toString().length >= 6) { - stringifiedAmount = stringifiedAmount.substring(0, 6); - } - - return stringifiedAmount; - } + @override + String get amountFormatted => amount.toString(); @override Future commit() async { diff --git a/cw_solana/lib/solana_client.dart b/cw_solana/lib/solana_client.dart index 015cc013c..7e218179c 100644 --- a/cw_solana/lib/solana_client.dart +++ b/cw_solana/lib/solana_client.dart @@ -3,9 +3,7 @@ import 'dart:convert'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_core/amount/money.dart'; -import 'package:cw_core/amount/money_double.dart'; import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/currency.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/solana_rpc_http_service.dart'; @@ -215,17 +213,10 @@ class SolanaWalletClient { // For native solana transactions if (instruction.accounts.length < 2) continue; - // Get the fee payer index based on transaction type - // For legacy transfers, the first account is usually the fee payer - // For versioned, the first account in instruction is usually the fee payer - final feePayerIndex = - txResponse.version == TransactionType.legacy ? 0 : instruction.accounts[0]; - final transactionModel = await _parseNativeTransaction( message: message, meta: meta, fee: fee, - feePayerIndex: feePayerIndex, walletAddress: walletAddress, signature: signature, blockTime: blockTime, @@ -234,8 +225,8 @@ class SolanaWalletClient { if (transactionModel != null) { return [transactionModel]; } - } else if (programId == SPLTokenProgramConst.tokenProgramId) { - // For SPL Token transactions + } else if (programId == SPLTokenProgramConst.tokenProgramId || + programId == SPLTokenProgramConst.token2022ProgramId) { if (instruction.accounts.length < 2) continue; final transactionModel = await _parseSPLTokenTransaction( @@ -265,7 +256,8 @@ class SolanaWalletClient { bool hasTokenTransfer = false; for (final otherInstruction in instructions) { final otherProgramId = message.accountKeys[otherInstruction.programIdIndex]; - if (otherProgramId == SPLTokenProgramConst.tokenProgramId) { + if (otherProgramId == SPLTokenProgramConst.tokenProgramId || + otherProgramId == SPLTokenProgramConst.token2022ProgramId) { hasTokenTransfer = true; break; } @@ -389,6 +381,18 @@ class SolanaWalletClient { return walletSent && walletReceived; } + static CryptoCurrency currencyForRawAmount(SPLToken? token, int mintDecimals) { + if (token != null && token.decimals == mintDecimals) { + return token; + } + + return CryptoCurrency( + name: (token?.title ?? "TOKEN").toLowerCase(), + title: token?.title ?? "TOKEN", + decimals: mintDecimals, + ); + } + /// Parses a swap transaction and creates dual entries (outgoing and incoming) Future> _parseSwapTransaction({ required VersionedMessage message, @@ -407,6 +411,9 @@ class SolanaWalletClient { final preTokenBalances = meta.preTokenBalances; final postTokenBalances = meta.postTokenBalances; + final walletPaidFee = accountKeys.isNotEmpty && accountKeys.first.address == walletAddress; + final feeAdjustment = walletPaidFee ? BigInt.from(fee) : BigInt.zero; + String? decreasedMintForWallet; String? increasedMintForWallet; @@ -442,8 +449,7 @@ class SolanaWalletClient { decreasedMintForWallet != increasedMintForWallet; // Parse outgoing side (what was sent) - double outgoingAmount = 0.0; - Currency outgoingToken = CryptoCurrency.sol; + Money? outgoingMoney; String? outgoingMintAddress; String? outgoingFrom; String? outgoingTo; @@ -461,12 +467,12 @@ class SolanaWalletClient { if (accountAddress == walletAddress) { final preBalance = preBalances[i]; final postBalance = postBalances[i]; - final balanceChange = preBalance - postBalance; + + final balanceChange = preBalance - postBalance - feeAdjustment; if (balanceChange > BigInt.zero) { // The wallet sent SOL - outgoingAmount = balanceChange.toDouble() / SolanaUtils.lamportsPerSol; - outgoingToken = CryptoCurrency.sol; + outgoingMoney = Money(balanceChange, CryptoCurrency.sol); outgoingMintAddress = null; outgoingFrom = walletAddress; // We find the intermediate account or swap program account @@ -484,7 +490,7 @@ class SolanaWalletClient { } // If no SOL outgoing, we check if there are any SPL token balance changes for the wallet - if (outgoingAmount == 0.0 && preTokenBalances != null) { + if (outgoingMoney == null && preTokenBalances != null && postTokenBalances != null) { for (final preTokenBal in preTokenBalances) { final owner = preTokenBal.owner?.address ?? ''; @@ -494,24 +500,23 @@ class SolanaWalletClient { if (isSplToSplSwap && mint != decreasedMintForWallet) { continue; } - final preAmount = preTokenBal.uiTokenAmount.uiAmount ?? 0.0; + final preRaw = BigInt.tryParse(preTokenBal.uiTokenAmount.amount) ?? BigInt.zero; // We find the corresponding post balance - for (final postTokenBal in postTokenBalances ?? []) { + for (final postTokenBal in postTokenBalances) { final postOwner = postTokenBal.owner?.address ?? ''; final postMint = postTokenBal.mint.address; - final postAmount = postTokenBal.uiTokenAmount.uiAmount ?? 0.0; if (postOwner == walletAddress && postMint == mint) { - final diff = preAmount - postAmount; + final postRaw = BigInt.tryParse(postTokenBal.uiTokenAmount.amount) ?? BigInt.zero; + final diff = preRaw - postRaw; - if (diff > 0) { + if (diff > BigInt.zero) { // The wallet sent tokens - outgoingAmount = diff.toDouble(); - outgoingMintAddress = mint; final token = await getTokenInfo(mint); - outgoingToken = - token ?? const CryptoCurrency(name: "TOKEN", title: "TOKEN", decimals: 6); + outgoingMoney = + Money(diff, currencyForRawAmount(token, preTokenBal.uiTokenAmount.decimals)); + outgoingMintAddress = mint; outgoingFrom = walletAddress; // We find the intermediate account if (instructions.isNotEmpty && instructions[0].accounts.isNotEmpty) { @@ -526,14 +531,15 @@ class SolanaWalletClient { } } - if (outgoingAmount > 0) break; + if (outgoingMoney != null) { + break; + } } } } // Parse incoming side (what was received) - double incomingAmount = 0.0; - Currency incomingToken = CryptoCurrency.sol; + Money? incomingMoney; String? incomingMintAddress; String? incomingFrom; String? incomingTo; @@ -550,12 +556,11 @@ class SolanaWalletClient { if (accountAddress == walletAddress) { final preBalance = preBalances[i]; final postBalance = postBalances[i]; - final balanceChange = postBalance - preBalance; + final balanceChange = postBalance - preBalance + feeAdjustment; if (balanceChange > BigInt.zero) { // The wallet received SOL - incomingAmount = balanceChange.toDouble() / SolanaUtils.lamportsPerSol; - incomingToken = CryptoCurrency.sol; + incomingMoney = Money(balanceChange, CryptoCurrency.sol); incomingMintAddress = null; incomingTo = walletAddress; // We find the intermediate account @@ -573,7 +578,7 @@ class SolanaWalletClient { } // If no SOL incoming, check SPL token incoming using ATA derivation - if (incomingAmount == 0.0 && preTokenBalances != null && postTokenBalances != null) { + if (incomingMoney == null && preTokenBalances != null && postTokenBalances != null) { // Collect all unique mints from token balances (excluding wrapped SOL) final mints = {}; for (final tokenBal in preTokenBalances) { @@ -595,17 +600,22 @@ class SolanaWalletClient { final walletSolAddress = SolAddress(walletAddress); final mintSolAddress = SolAddress(mint); - final ata = AssociatedTokenAccountProgramUtils.associatedTokenAccount( + final standardAta = AssociatedTokenAccountProgramUtils.associatedTokenAccount( mint: mintSolAddress, owner: walletSolAddress, ); - final ataAddress = ata.address.address; + final token2022Ata = AssociatedTokenAccountProgramUtils.associatedTokenAccount( + mint: mintSolAddress, + owner: walletSolAddress, + tokenProgramId: SPLTokenProgramConst.token2022ProgramId, + ); + final ataAddresses = [standardAta.address.address, token2022Ata.address.address]; - // We check if this ATA address appears in the account keys + // We check if either ATA address appears in the account keys int? ataAccountIndex; for (int i = 0; i < accountKeys.length; i++) { final accountKey = accountKeys[i]; - if (accountKey.address == ataAddress) { + if (ataAddresses.contains(accountKey.address)) { ataAccountIndex = i; break; } @@ -613,15 +623,17 @@ class SolanaWalletClient { // If ATA is in the transaction, we check for balance changes if (ataAccountIndex != null) { - double preAmount = 0.0; - double postAmount = 0.0; + BigInt preRaw = BigInt.zero; + BigInt postRaw = BigInt.zero; + int? mintDecimals; // We find the pre balance for (final preTokenBal in preTokenBalances) { final accountIndex = preTokenBal.accountIndex; final tokenMint = preTokenBal.mint.address; if (accountIndex == ataAccountIndex && tokenMint == mint) { - preAmount = preTokenBal.uiTokenAmount.uiAmount?.toDouble() ?? 0.0; + preRaw = BigInt.tryParse(preTokenBal.uiTokenAmount.amount) ?? BigInt.zero; + mintDecimals = preTokenBal.uiTokenAmount.decimals; break; } } @@ -631,18 +643,18 @@ class SolanaWalletClient { final accountIndex = postTokenBal.accountIndex; final tokenMint = postTokenBal.mint.address; if (accountIndex == ataAccountIndex && tokenMint == mint) { - postAmount = postTokenBal.uiTokenAmount.uiAmount?.toDouble() ?? 0.0; + postRaw = BigInt.tryParse(postTokenBal.uiTokenAmount.amount) ?? BigInt.zero; + mintDecimals = postTokenBal.uiTokenAmount.decimals; break; } } - final diff = postAmount - preAmount; - if (diff > 0) { + final diff = postRaw - preRaw; + if (diff > BigInt.zero && mintDecimals != null) { // The wallet received tokens - incomingAmount = diff.toDouble(); - incomingMintAddress = mint; final token = await getTokenInfo(mint); - incomingToken = token ?? const CryptoCurrency(name: "TOKEN", title: "TOKEN", decimals: 6); + incomingMoney = Money(diff, currencyForRawAmount(token, mintDecimals)); + incomingMintAddress = mint; incomingTo = walletAddress; // We find the intermediate account if (instructions.isNotEmpty && instructions[0].accounts.isNotEmpty) { @@ -663,7 +675,7 @@ class SolanaWalletClient { } // Outgoing transaction model - if (outgoingAmount > 0.0 && outgoingFrom != null && outgoingTo != null) { + if (outgoingMoney != null && outgoingFrom != null && outgoingTo != null) { final outgoingId = '${signature}_outgoing'; // We create a composite ID for the outgoing transaction swapTransactions.add(SolanaTransactionModel( @@ -671,7 +683,7 @@ class SolanaWalletClient { from: outgoingFrom, to: outgoingTo, id: outgoingId, - amount: outgoingAmount.toMoney(outgoingToken), + amount: outgoingMoney, programId: outgoingMintAddress == null ? SystemProgramConst.programId.address : SPLTokenProgramConst.tokenProgramId.address, @@ -681,7 +693,7 @@ class SolanaWalletClient { } // Incoming transaction model - if (incomingAmount > 0.0 && incomingFrom != null && incomingTo != null) { + if (incomingMoney != null && incomingFrom != null && incomingTo != null) { final incomingId = '${signature}_incoming'; // We create a composite ID for the incoming transaction swapTransactions.add(SolanaTransactionModel( @@ -689,7 +701,7 @@ class SolanaWalletClient { from: incomingFrom, to: incomingTo, id: incomingId, - amount: incomingAmount.toMoney(incomingToken), + amount: incomingMoney, programId: incomingMintAddress == null ? SystemProgramConst.programId.address : SPLTokenProgramConst.tokenProgramId.address, @@ -705,7 +717,6 @@ class SolanaWalletClient { required VersionedMessage message, required ConfirmedTransactionMeta meta, required int fee, - required int feePayerIndex, required String walletAddress, required String signature, required BigInt? blockTime, @@ -736,8 +747,7 @@ class SolanaWalletClient { // Positive = wallet lost SOL, negative = wallet gained. final walletChange = walletPre - walletPost; - final bool walletPaidFee = - feePayerIndex < accountKeys.length && accountKeys[feePayerIndex].address == walletAddress; + final walletPaidFee = accountKeys.first.address == walletAddress; // Net transfer amount excluding the fee. final netChange = walletPaidFee ? walletChange - BigInt.from(fee) : walletChange; @@ -808,8 +818,9 @@ class SolanaWalletClient { mintAddress = accountKeys[accounts[1]].address; } - double userPreAmount = 0.0; - double userPostAmount = 0.0; + BigInt userPreRaw = BigInt.zero; + BigInt userPostRaw = BigInt.zero; + int? mintDecimals; if (preTokenBalances != null) { for (final preBal in preTokenBalances) { @@ -820,7 +831,8 @@ class SolanaWalletClient { continue; } mintAddress ??= preBal.mint.address; - userPreAmount = preBal.uiTokenAmount.uiAmount ?? 0.0; + mintDecimals = preBal.uiTokenAmount.decimals; + userPreRaw = BigInt.tryParse(preBal.uiTokenAmount.amount) ?? BigInt.zero; break; } } @@ -836,19 +848,21 @@ class SolanaWalletClient { continue; } mintAddress ??= postBal.mint.address; - userPostAmount = postBal.uiTokenAmount.uiAmount ?? 0.0; + mintDecimals = postBal.uiTokenAmount.decimals; + userPostRaw = BigInt.tryParse(postBal.uiTokenAmount.amount) ?? BigInt.zero; break; } } } } - final diff = userPreAmount - userPostAmount; - final rawAmount = diff.abs(); + final diff = userPreRaw - userPostRaw; - final amountInString = rawAmount.toStringAsFixed(6); - final amount = double.parse(amountInString); - final isOutgoing = diff > 0; + if (diff == BigInt.zero || mintDecimals == null) { + return null; + } + + final isOutgoing = diff > BigInt.zero; // Resolve sender/receiver from token balance owners String? senderOwner; @@ -886,7 +900,7 @@ class SolanaWalletClient { from: sender, to: receiver, id: signature, - amount: amount.toMoney(splToken ?? CryptoCurrency.sol), + amount: Money(diff.abs(), currencyForRawAmount(splToken, mintDecimals)), programId: SPLTokenProgramConst.tokenProgramId.address, blockTimeInInt: blockTime?.toInt() ?? 0, fee: Money.fromInt(fee, CryptoCurrency.sol), @@ -1013,8 +1027,7 @@ class SolanaWalletClient { final transactions = []; try { - final signatures = - await _getAllSignaturesSinceLastFetch(address, untilSignature, commitment); + final signatures = await _getAllSignaturesSinceLastFetch(address, untilSignature, commitment); if (signatures.isEmpty) return TransactionSyncResult(transactions: transactions); @@ -1096,11 +1109,9 @@ class SolanaWalletClient { if (associatedTokenAccount == null) { try { - associatedTokenAccount = await _getOrCreateAssociatedTokenAccount( - payerPrivateKey: privateKey, + associatedTokenAccount = await _findAssociatedTokenAccount( mintAddress: SolAddress(mintAddress), ownerAddress: ownerWalletAddress, - shouldCreateATA: false, ); } catch (e, s) { printV('$e \n $s'); @@ -1122,12 +1133,33 @@ class SolanaWalletClient { ); } - final Map tokenInfoCache = {}; + final Map tokenInfoCache = {}; Future getTokenInfo(String mintAddress) async { - if (tokenInfoCache.containsKey(mintAddress)) return tokenInfoCache[mintAddress]; + final cached = tokenInfoCache[mintAddress]; + if (cached != null) { + return cached; + } - return tokenInfoCache[mintAddress] = await fetchSPLTokenInfo(mintAddress); + final fetched = await fetchSPLTokenInfo(mintAddress); + if (fetched != null) { + tokenInfoCache[mintAddress] = fetched; + } + + return fetched; + } + + Future _fetchMintDecimals(String mintAddress) async { + try { + final supply = await _provider!.request( + SolanaRPCGetTokenSupply(account: SolAddress(mintAddress)), + ); + + return supply.decimals; + } catch (e) { + printV("Could not read decimals for mint $mintAddress: ${e.toString()}"); + return null; + } } Future fetchSPLTokenInfo(String mintAddress) async { @@ -1150,18 +1182,29 @@ class SolanaWalletClient { final symbol = decodedResponse['symbol'] ?? ''; final name = decodedResponse['name'] ?? ''; - final decimal = decodedResponse['decimals'] ?? '0'; + final rawDecimals = decodedResponse["decimals"]; final iconPath = decodedResponse['logo'] ?? ''; final filteredTokenSymbol = symbol.replaceFirst(RegExp('^\\\$'), '').replaceAll('\u0000', ''); + final reportedDecimals = + rawDecimals is num ? rawDecimals.toInt() : int.tryParse("${rawDecimals ?? ""}"); + + final decimals = (reportedDecimals != null && reportedDecimals > 0) + ? reportedDecimals + : await _fetchMintDecimals(mintAddress); + + if (decimals == null) { + return null; + } + return SPLToken( name: name, mint: symbol, symbol: filteredTokenSymbol, mintAddress: mintAddress, iconPath: iconPath, - decimal: int.tryParse(decimal) ?? 0, + decimal: decimals, ); } catch (e, s) { printV('Error fetching token info: $e \n $s'); @@ -1189,11 +1232,18 @@ class SolanaWalletClient { String filteredTokenSymbol = metadata.symbol.replaceFirst(RegExp('^\\\$'), '').replaceAll('\u0000', ''); + final decimals = await _fetchMintDecimals(token.mint.address); + + if (decimals == null) { + return null; + } + return SPLToken.fromMetadata( name: metadata.name, mint: metadata.symbol, symbol: filteredTokenSymbol, mintAddress: token.mint.address, + decimal: decimals, iconPath: iconPath, ); } catch (_) {} @@ -1217,7 +1267,7 @@ class SolanaWalletClient { }) async { const commitment = Commitment.confirmed; - if (inputAmount.currency == CryptoCurrency.sol) { + if (tokenMint == null) { return _signNativeTokenTransaction( inputAmount: inputAmount, destinationAddress: destinationAddress, @@ -1229,7 +1279,7 @@ class SolanaWalletClient { } else { return _signSPLTokenTransaction( tokenDecimals: inputAmount.currency.decimals, - tokenMint: tokenMint!, + tokenMint: tokenMint, inputAmount: inputAmount, ownerPrivateKey: ownerPrivateKey, destinationAddress: destinationAddress, @@ -1270,53 +1320,28 @@ class SolanaWalletClient { ); } - Future _getMessageForSPLTokenTransaction({ - required SolAddress ownerAddress, - required SolAddress destinationAddress, - required int tokenDecimals, - required SolAddress mintAddress, - required SolAddress sourceAccount, - required Money amount, - required Commitment commitment, - required SolAddress tokenProgramId, - }) async { - final instructions = [ - SPLTokenProgram.transferChecked( - layout: SPLTokenTransferCheckedLayout( - amount: amount.amount, - decimals: tokenDecimals, - ), - mint: mintAddress, - source: sourceAccount, - destination: destinationAddress, - owner: ownerAddress, - ) - ]; - - final latestBlockhash = await _getLatestBlockhash(commitment); - - return Message.compile( - transactionInstructions: instructions, - payer: ownerAddress, - recentBlockhash: latestBlockhash, - ); - } - Future _getFeeFromCompiledMessage(Message message, Commitment commitment) { final base64Message = base64Encode(message.serialize()); return getFeeForMessage(base64Message, commitment); } - Future hasSufficientFundsLeftForRent({ - required Money inputAmount, - required Money solBalance, - required Money fee, - }) async { + Future _getRentExemptionAmount(int space) async { final rent = await _provider!.request( + SolanaRPCGetMinimumBalanceForRentExemption(size: space), + ); + + return Money(rent, CryptoCurrency.sol); + } + + Future hasSufficientFundsLeftForRent({ + required Money totalOutflow, + required Money solBalance, + }) async { + final rentBuffer = await _provider!.request( SolanaRPCGetMinimumBalanceForRentExemption(size: SolanaTokenAccountUtils.accountSize), ); - return (solBalance - (inputAmount + fee)) > Money(rent, CryptoCurrency.sol); + return (solBalance - totalOutflow) > Money(rentBuffer, CryptoCurrency.sol); } Future _signNativeTokenTransaction({ @@ -1343,8 +1368,7 @@ class SolanaWalletClient { if (!isSendAll) { final hasSufficientFundsLeft = await hasSufficientFundsLeftForRent( - inputAmount: inputAmount, - fee: fee, + totalOutflow: inputAmount + fee, solBalance: solBalance, ); @@ -1357,6 +1381,10 @@ class SolanaWalletClient { if (isSendAll) { final updatedLamports = inputAmount - fee; + if (updatedLamports.isNegative || updatedLamports.isZero) { + throw SolanaTransactionWrongBalanceException(CryptoCurrency.sol); + } + final transaction = _constructNativeTransaction( ownerPrivateKey: ownerPrivateKey, destinationAddress: destinationAddress, @@ -1502,21 +1530,10 @@ class SolanaWalletClient { return SPLTokenProgramConst.tokenProgramId; } - Future _getOrCreateAssociatedTokenAccount({ - required SolanaPrivateKey payerPrivateKey, + Future _findAssociatedTokenAccount({ required SolAddress ownerAddress, required SolAddress mintAddress, - required bool shouldCreateATA, }) async { - // For transaction history loading (shouldCreateATA: false), try standard token program first - // to avoid unnecessary RPC call. Only fetch token program ID when creating accounts. - SolAddress tokenProgramId = SPLTokenProgramConst.tokenProgramId; - - if (shouldCreateATA) { - // Only fetch token program ID when we need to create an account - tokenProgramId = await _getTokenProgramId(mintAddress); - } - // Try with standard token program first (most common case) var associatedTokenAccount = AssociatedTokenAccountProgramUtils.associatedTokenAccount( mint: mintAddress, @@ -1539,71 +1556,29 @@ class SolanaWalletClient { // If account exists with standard program, return it if (accountInfo != null) return associatedTokenAccount; - // If not found and we're not creating, try Token-2022 as fallback - if (!shouldCreateATA) { - try { - final token2022ProgramId = await _getTokenProgramId(mintAddress); - if (token2022ProgramId.address != SPLTokenProgramConst.tokenProgramId.address) { - associatedTokenAccount = AssociatedTokenAccountProgramUtils.associatedTokenAccount( - mint: mintAddress, - owner: ownerAddress, - tokenProgramId: token2022ProgramId, + // if its not found under the standard program, then we try Token-2022, which derives a different address + try { + final token2022ProgramId = await _getTokenProgramId(mintAddress); + if (token2022ProgramId.address != SPLTokenProgramConst.tokenProgramId.address) { + associatedTokenAccount = AssociatedTokenAccountProgramUtils.associatedTokenAccount( + mint: mintAddress, + owner: ownerAddress, + tokenProgramId: token2022ProgramId, + ); + + try { + accountInfo = await _provider!.request( + SolanaRPCGetAccountInfo( + account: associatedTokenAccount.address, + commitment: Commitment.confirmed, + ), ); + if (accountInfo != null) return associatedTokenAccount; + } catch (_) {} + } + } catch (_) {} - try { - accountInfo = await _provider!.request( - SolanaRPCGetAccountInfo( - account: associatedTokenAccount.address, - commitment: Commitment.confirmed, - ), - ); - if (accountInfo != null) return associatedTokenAccount; - } catch (_) {} - } - } catch (_) {} - return null; - } - - // For account creation, use the detected token program ID - associatedTokenAccount = AssociatedTokenAccountProgramUtils.associatedTokenAccount( - mint: mintAddress, - owner: ownerAddress, - tokenProgramId: tokenProgramId, - ); - - final payerAddress = payerPrivateKey.publicKey().toAddress(); - - final createAssociatedTokenAccount = AssociatedTokenAccountProgram.associatedTokenAccount( - payer: payerAddress, - associatedToken: associatedTokenAccount.address, - owner: ownerAddress, - mint: mintAddress, - tokenProgramId: tokenProgramId, - ); - - final blockhash = await _getLatestBlockhash(Commitment.confirmed); - - final transaction = SolanaTransaction( - payerKey: payerAddress, - instructions: [createAssociatedTokenAccount], - recentBlockhash: blockhash, - type: TransactionType.v0, - ); - - final serializedTransaction = await _signTransactionInternal( - ownerPrivateKey: payerPrivateKey, - transaction: transaction, - ); - - await sendTransaction( - serializedTransaction: serializedTransaction, - commitment: Commitment.confirmed, - ); - - // Wait for confirmation - await Future.delayed(const Duration(seconds: 2)); - - return associatedTokenAccount; + return null; } Future _signSPLTokenTransaction({ @@ -1620,6 +1595,7 @@ class SolanaWalletClient { ProgramDerivedAddress? associatedSenderAccount; SolAddress senderTokenProgramId = tokenProgramId; + int? senderAccountSpace; try { associatedSenderAccount = AssociatedTokenAccountProgramUtils.associatedTokenAccount( @@ -1638,6 +1614,7 @@ class SolanaWalletClient { if (accountInfo != null) { senderTokenProgramId = accountInfo.owner; + senderAccountSpace = accountInfo.space; } else { associatedSenderAccount = null; } @@ -1664,6 +1641,7 @@ class SolanaWalletClient { if (accountInfo != null) { senderTokenProgramId = accountInfo.owner; + senderAccountSpace = accountInfo.space; } else { associatedSenderAccount = null; } @@ -1679,18 +1657,16 @@ class SolanaWalletClient { ); } - // Get or create recipient account using the sender's token program ID - // This ensures both accounts use the same program - ProgramDerivedAddress? associatedRecipientAccount; + final ProgramDerivedAddress associatedRecipientAccount; + bool shouldCreateRecipientAccount = false; + try { - // First, try to get/create with the sender's actual program ID final recipientPDA = AssociatedTokenAccountProgramUtils.associatedTokenAccount( mint: mintAddress, owner: SolAddress(destinationAddress), tokenProgramId: senderTokenProgramId, ); - // Check if account exists with correct program SolanaAccountInfo? recipientInfo; try { recipientInfo = await _provider!.request( @@ -1703,39 +1679,16 @@ class SolanaWalletClient { recipientInfo = null; } - if (recipientInfo != null && recipientInfo.owner.address == senderTokenProgramId.address) { - associatedRecipientAccount = recipientPDA; - } else { - // Create the account with the correct program ID - final createATA = AssociatedTokenAccountProgram.associatedTokenAccount( - payer: ownerPrivateKey.publicKey().toAddress(), - associatedToken: recipientPDA.address, - owner: SolAddress(destinationAddress), - mint: mintAddress, - tokenProgramId: senderTokenProgramId, + if (recipientInfo != null && recipientInfo.owner.address != senderTokenProgramId.address) { + throw SolanaCreateAssociatedTokenAccountException( + "Recipient token account is owned by ${recipientInfo.owner.address}", ); - - final blockhash = await _getLatestBlockhash(Commitment.confirmed); - final createTransaction = SolanaTransaction( - payerKey: ownerPrivateKey.publicKey().toAddress(), - instructions: [createATA], - recentBlockhash: blockhash, - type: TransactionType.v0, - ); - - final serializedCreateTx = await _signTransactionInternal( - ownerPrivateKey: ownerPrivateKey, - transaction: createTransaction, - ); - - await sendTransaction( - serializedTransaction: serializedCreateTx, - commitment: Commitment.confirmed, - ); - - await Future.delayed(const Duration(seconds: 2)); - associatedRecipientAccount = recipientPDA; } + + shouldCreateRecipientAccount = recipientInfo == null; + associatedRecipientAccount = recipientPDA; + } on SolanaCreateAssociatedTokenAccountException { + rethrow; } catch (e) { throw SolanaCreateAssociatedTokenAccountException(e.toString()); } @@ -1751,30 +1704,42 @@ class SolanaWalletClient { decimals: tokenDecimals, ); + final instructions = [ + if (shouldCreateRecipientAccount) + AssociatedTokenAccountProgram.associatedTokenAccountIdempotent( + payer: ownerPrivateKey.publicKey().toAddress(), + associatedToken: associatedRecipientAccount.address, + owner: SolAddress(destinationAddress), + mint: mintAddress, + tokenProgramId: senderTokenProgramId, + ), + transferInstructions, + ]; + final latestBlockHash = await _getLatestBlockhash(commitment); final transaction = SolanaTransaction( payerKey: ownerPrivateKey.publicKey().toAddress(), - instructions: [transferInstructions], + instructions: instructions, recentBlockhash: latestBlockHash, ); - final message = await _getMessageForSPLTokenTransaction( - ownerAddress: ownerPrivateKey.publicKey().toAddress(), - tokenDecimals: tokenDecimals, - mintAddress: mintAddress, - destinationAddress: associatedRecipientAccount.address, - sourceAccount: associatedSenderAccount.address, - amount: inputAmount, - commitment: commitment, - tokenProgramId: tokenProgramId, + final message = Message.compile( + transactionInstructions: instructions, + payer: ownerPrivateKey.publicKey().toAddress(), + recentBlockhash: latestBlockHash, ); final fee = await _getFeeFromCompiledMessage(message, commitment); + // The sender account exists by this point, so its space is set, and the recipient + // account is the same size because it holds the same mint under the same program. + final accountCreationCost = shouldCreateRecipientAccount + ? await _getRentExemptionAmount(senderAccountSpace!) + : Money.zero(CryptoCurrency.sol); + final hasSufficientFundsLeft = await hasSufficientFundsLeftForRent( - inputAmount: Money.zero(CryptoCurrency.sol), - fee: fee, + totalOutflow: accountCreationCost + fee, solBalance: solBalance, ); @@ -1796,6 +1761,7 @@ class SolanaWalletClient { destinationAddress: destinationAddress, sendTransaction: sendTx, fee: fee, + additionalCost: shouldCreateRecipientAccount ? accountCreationCost : null, ); } @@ -1884,19 +1850,10 @@ class SolanaWalletClient { final mint = tokenData['mint'] as String? ?? ''; if (mint.isEmpty) continue; - final amountRaw = tokenData['amountRaw'] as String? ?? '0'; - - final decimals = tokenData['decimals'] as int? ?? 0; - - final associatedTokenAddress = tokenData['associatedTokenAddress'] as String? ?? ''; - tokens.add( MoralisSolanaTokenBalance( mint: mint, amount: amount, - amountRaw: amountRaw, - decimals: decimals, - associatedTokenAddress: associatedTokenAddress, ), ); } @@ -1954,15 +1911,9 @@ class SolanaWalletClient { class MoralisSolanaTokenBalance { final String mint; final double amount; - final String amountRaw; - final int decimals; - final String associatedTokenAddress; const MoralisSolanaTokenBalance({ required this.mint, required this.amount, - required this.amountRaw, - required this.decimals, - required this.associatedTokenAddress, }); } diff --git a/cw_solana/lib/solana_exceptions.dart b/cw_solana/lib/solana_exceptions.dart index 96ba0bb6f..a3873a37a 100644 --- a/cw_solana/lib/solana_exceptions.dart +++ b/cw_solana/lib/solana_exceptions.dart @@ -36,3 +36,7 @@ class SolanaNoAssociatedTokenAccountException extends NoAssociatedTokenAccountEx final String account; final String mint; } + +class SolanaAmbiguousTokenSymbolException extends AmbiguousTokenSymbolException { + SolanaAmbiguousTokenSymbolException(super.symbol); +} diff --git a/cw_solana/lib/solana_transaction_history.dart b/cw_solana/lib/solana_transaction_history.dart index 00fcbec90..bb96ab8b6 100644 --- a/cw_solana/lib/solana_transaction_history.dart +++ b/cw_solana/lib/solana_transaction_history.dart @@ -88,9 +88,14 @@ abstract class SolanaTransactionHistoryBase extends TransactionHistoryBase) { - final tx = SolanaTransactionInfo.fromJson(val); - _update(tx); + if (val is! Map) { + return; + } + + try { + _update(SolanaTransactionInfo.fromJson(val)); + } catch (e) { + printV("Skipping unreadable solana transaction ${entry.key}: ${e.toString()}"); } }); } catch (e) { diff --git a/cw_solana/lib/solana_wallet.dart b/cw_solana/lib/solana_wallet.dart index b41ee421c..55ad9f9f3 100644 --- a/cw_solana/lib/solana_wallet.dart +++ b/cw_solana/lib/solana_wallet.dart @@ -251,12 +251,7 @@ abstract class SolanaWalletBase await updateTokenBalance(); - final transactionCurrency = balance.keys.firstWhere( - (currency) => - currency.title == credentials.currency.title && - currency.tag == credentials.currency.tag, - orElse: () => throw Exception( - 'Currency ${credentials.currency.title} ${credentials.currency.tag} is not accessible in the wallet, try to enable it first.')); + final transactionCurrency = resolveTransactionCurrency(credentials.currency, balance.keys); final walletBalanceForCurrency = balance[transactionCurrency]!.available; @@ -285,9 +280,8 @@ abstract class SolanaWalletBase } String? tokenMint; - // Token Mint is only needed for transactions that are not native tokens(non-SOL transactions) - if (transactionCurrency.title != CryptoCurrency.sol.title) { - tokenMint = (transactionCurrency as SPLToken).mintAddress; + if (transactionCurrency is SPLToken) { + tokenMint = transactionCurrency.mintAddress; } return _client.signSolanaTransaction( @@ -302,6 +296,33 @@ abstract class SolanaWalletBase ); } + static CryptoCurrency resolveTransactionCurrency( + CryptoCurrency requestedCurrency, + Iterable availableCurrencies, + ) { + final matches = requestedCurrency is SPLToken + ? availableCurrencies + .where((currency) => + currency is SPLToken && currency.mintAddress == requestedCurrency.mintAddress) + .toList(growable: false) + : availableCurrencies + .where((currency) => + currency.title == requestedCurrency.title && currency.tag == requestedCurrency.tag) + .toList(growable: false); + + if (matches.isEmpty) { + throw Exception( + "Currency ${requestedCurrency.title} ${requestedCurrency.tag} is not accessible in the wallet, try to enable it first.", + ); + } + + if (matches.length > 1) { + throw SolanaAmbiguousTokenSymbolException(requestedCurrency.title); + } + + return matches.first; + } + @override Future> fetchTransactions() async => {}; @@ -448,6 +469,8 @@ abstract class SolanaWalletBase } } + static final _swapIdSuffixPattern = RegExp(r"_(outgoing|incoming)$"); + void _addTransactions(List transactions) { final Map result = {}; @@ -464,6 +487,11 @@ abstract class SolanaWalletBase isPending: false, fee: transactionModel.fee, ); + + final baseSignature = transactionModel.id.replaceFirst(_swapIdSuffixPattern, ""); + if (baseSignature != transactionModel.id) { + transactionHistory.transactions.remove(baseSignature); + } } transactionHistory.addMany(result); @@ -588,7 +616,9 @@ abstract class SolanaWalletBase // Fetch SOL and SPL token balances in parallel for better performance await Future.wait([ _fetchSOLBalance().then((solBalance) { - balance[CryptoCurrency.sol] = solBalance; + if (solBalance != null) { + balance[CryptoCurrency.sol] = solBalance; + } }), _updateSplTokenBalancesInternal(tokenMints: tokenMints), ]); @@ -596,10 +626,15 @@ abstract class SolanaWalletBase await save(); } - Future _fetchSOLBalance() async { - final balance = await _client.getBalance(solanaAddress); - - return SolanaBalance(balance); + Future _fetchSOLBalance() async { + try { + return SolanaBalance( + await _client.getBalance(solanaAddress, throwOnError: true), + ); + } catch (e) { + printV("Error fetching SOL balance: ${e.toString()}"); + return null; + } } /// Internal helper to update SPL token balances. @@ -743,7 +778,7 @@ abstract class SolanaWalletBase name: tokenInfo.name, symbol: tokenInfo.symbol, mintAddress: mint, - decimal: moralisToken.decimals, + decimal: tokenInfo.decimal, mint: tokenInfo.mint, iconPath: tokenInfo.iconPath, tag: 'SOL', @@ -886,7 +921,7 @@ abstract class SolanaWalletBase await _clearLastSyncedSignature(source); } - updateTokenBalance(); + await updateTokenBalance(); } Future _removeTokenTransactionsInHistory(SPLToken token) async { @@ -932,47 +967,25 @@ abstract class SolanaWalletBase return Base58Encoder.encode(signature); } - List> bytesFromSigString(String signatureString) { - final regex = RegExp(r'Signature\(\[(.+)\], publicKey: (.+)\)'); - final match = regex.firstMatch(signatureString); - - if (match != null) { - final bytesString = match.group(1)!; - final base58EncodedPublicKeyString = match.group(2)!; - final sigBytes = bytesString.split(', ').map(int.parse).toList(); - - List pubKeyBytes = SolAddrDecoder().decodeAddr(base58EncodedPublicKeyString); - - return [sigBytes, pubKeyBytes]; - } else { - throw const FormatException('Invalid Signature string format'); - } - } - @override Future verifyMessage(String message, String signature, {String? address}) async { - String signatureString = utf8.decode(HEX.decode(signature)); - - List> bytes = bytesFromSigString(signatureString); - - final messageBytes = utf8.encode(message); - final sigBytes = bytes[0]; - final pubKeyBytes = bytes[1]; - - if (address == null) { + if (address == null || address.isEmpty) { return false; } - // make sure the address derived from the public key provided matches the one we expect - final pub = SolanaPublicKey.fromBytes(pubKeyBytes); - if (address != pub.toAddress().address) { + try { + final signatureBytes = Base58Decoder.decode(signature); + + final publicKey = SolanaPublicKey.fromBytes(SolAddrDecoder().decodeAddr(address)); + + return publicKey.verify( + message: utf8.encode(message), + signature: signatureBytes, + ); + } catch (e) { + printV("Error verifying solana message: ${e.toString()}"); return false; } - - return pub.verify( - message: messageBytes, - signature: sigBytes, - ); } SolanaRPC? get solanaProvider => _client.getSolanaProvider; diff --git a/cw_solana/lib/solana_wallet_service.dart b/cw_solana/lib/solana_wallet_service.dart index a557507f5..85eae6024 100644 --- a/cw_solana/lib/solana_wallet_service.dart +++ b/cw_solana/lib/solana_wallet_service.dart @@ -92,7 +92,7 @@ class SolanaWalletService extends WalletService< @override Future remove(String wallet) async { - File(await pathForWalletDir(name: wallet, type: getType())).delete(recursive: true); + await File(await pathForWalletDir(name: wallet, type: getType())).delete(recursive: true); final walletInfo = await WalletInfo.get(wallet, getType()); if (walletInfo == null) { throw Exception('Wallet not found'); @@ -104,8 +104,8 @@ class SolanaWalletService extends WalletService< } final prefs = await SharedPreferences.getInstance(); - for (final key in prefs.getKeys().where( - (k) => k.startsWith('solana_last_synced_signature_${wallet}_'))) { + for (final key + in prefs.getKeys().where((k) => k.startsWith('solana_last_synced_signature_${wallet}_'))) { await prefs.remove(key); } } diff --git a/cw_solana/test/solana_currency_resolution_test.dart b/cw_solana/test/solana_currency_resolution_test.dart new file mode 100644 index 000000000..ef97ec399 --- /dev/null +++ b/cw_solana/test/solana_currency_resolution_test.dart @@ -0,0 +1,108 @@ +import "package:cw_core/crypto_currency.dart"; +import "package:cw_core/spl_token.dart"; +import "package:cw_solana/solana_client.dart"; +import "package:cw_solana/solana_exceptions.dart"; +import "package:cw_solana/solana_wallet.dart"; +import "package:flutter_test/flutter_test.dart"; + +void main() { + group("currencyForRawAmount", () { + test("keeps the stored token when its decimals match the mint", () { + final jup = SPLToken( + name: "Jupiter", + symbol: "JUP", + mint: "jup", + mintAddress: "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN", + decimal: 6, + ); + + expect(SolanaWalletClient.currencyForRawAmount(jup, 6), same(jup)); + }); + + test("prefers the mint decimals when the stored token disagrees", () { + final staleJup = SPLToken( + name: "Jupiter", + symbol: "JUP", + mint: "jup", + mintAddress: "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN", + decimal: 0, + ); + + final resolved = SolanaWalletClient.currencyForRawAmount(staleJup, 6); + + expect(resolved.decimals, 6); + expect(resolved.title, "JUP"); + expect(resolved, isNot(same(staleJup))); + }); + + test("falls back to a placeholder title when the token is unknown", () { + final resolved = SolanaWalletClient.currencyForRawAmount(null, 8); + + expect(resolved.decimals, 8); + expect(resolved.title, "TOKEN"); + expect(resolved.name, "token"); + }); + + test("always reports the mint decimals", () { + for (final decimals in [0, 1, 6, 8, 9, 18]) { + expect(SolanaWalletClient.currencyForRawAmount(null, decimals).decimals, decimals); + } + }); + }); + + group("resolveTransactionCurrency", () { + final realUsdc = SPLToken( + name: "USD Coin", + symbol: "USDC", + mint: "usdc", + mintAddress: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + decimal: 6, + ); + + final scamUsdc = SPLToken( + name: "USD Coin", + symbol: "USDC", + mint: "usdc", + mintAddress: "scamMintAddressClaimingTheUsdcSymbol", + decimal: 9, + ); + + test("picks the requested mint even when another token claims the symbol", () { + final resolved = SolanaWalletBase.resolveTransactionCurrency(realUsdc, [scamUsdc, realUsdc]); + + expect(resolved, same(realUsdc)); + expect((resolved as SPLToken).mintAddress, realUsdc.mintAddress); + }); + + test("picks the scam mint only when the scam mint is the one requested", () { + final resolved = SolanaWalletBase.resolveTransactionCurrency(scamUsdc, [scamUsdc, realUsdc]); + + expect(resolved, same(scamUsdc)); + }); + + test("throws when the requested mint is not in the wallet", () { + expect( + () => SolanaWalletBase.resolveTransactionCurrency(realUsdc, [scamUsdc]), + throwsA(isA()), + ); + }); + + test("throws on ambiguity when a symbol lookup matches two tokens", () { + expect( + () => SolanaWalletBase.resolveTransactionCurrency( + CryptoCurrency(name: "usdc", title: "USDC", decimals: 6, tag: "SOL"), + [scamUsdc, realUsdc], + ), + throwsA(isA()), + ); + }); + + test("resolves a native currency by title and tag", () { + expect( + SolanaWalletBase.resolveTransactionCurrency( + CryptoCurrency.sol, [CryptoCurrency.sol, realUsdc]), + same(CryptoCurrency.sol), + ); + }); + }); +} diff --git a/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart b/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart index d10233a34..8e0a470cd 100644 --- a/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart +++ b/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart @@ -631,6 +631,7 @@ class CakePayBuyCardPage extends BasePage { amountValue: _sendViewModel.amountParsingProxy .asDisplayStringWithSymbol(_sendViewModel.pendingTransaction!.amount), quantity: 'QTY: ${cakePayBuyCardViewModel.quantity}', + explanation: _sendViewModel.pendingTransactionAdditionalCostNotice, fiatAmountValue: _sendViewModel.pendingTransactionFiatAmountFormatted, fee: S.of(bottomSheetContext).send_fee, feeValue: _sendViewModel.amountParsingProxy diff --git a/lib/exchange/provider/jupiter_exchange_provider.dart b/lib/exchange/provider/jupiter_exchange_provider.dart index a8ea814a4..2f1fc9070 100644 --- a/lib/exchange/provider/jupiter_exchange_provider.dart +++ b/lib/exchange/provider/jupiter_exchange_provider.dart @@ -138,6 +138,10 @@ class JupiterExchangeProvider extends ExchangeProvider { if (!_isSolanaCurrency(from) || !_isSolanaCurrency(to)) { return 0.0; } + + if (amount <= 0) { + return 0.0; + } final inputMint = _getTokenMint(from); final outputMint = _getTokenMint(to); diff --git a/lib/new-ui/widgets/coins_page/assets_history/history_tile.dart b/lib/new-ui/widgets/coins_page/assets_history/history_tile.dart index f26b0790e..426957a9b 100644 --- a/lib/new-ui/widgets/coins_page/assets_history/history_tile.dart +++ b/lib/new-ui/widgets/coins_page/assets_history/history_tile.dart @@ -100,9 +100,9 @@ class HistoryTile extends StatelessWidget { children: [ Opacity( opacity: pending ? 0.5 : 1, - child: CakeImageWidget( + child: TokenImageWidget( imageUrl: asset?.iconPath ?? "", - width: 34, + size: 34, ), ), Align( diff --git a/lib/new-ui/widgets/send_page/send_confirm_sheet.dart b/lib/new-ui/widgets/send_page/send_confirm_sheet.dart index 5b34686d3..35f710209 100644 --- a/lib/new-ui/widgets/send_page/send_confirm_sheet.dart +++ b/lib/new-ui/widgets/send_page/send_confirm_sheet.dart @@ -201,6 +201,7 @@ class SendTransactionDetails extends StatelessWidget { Widget _buildMainContent(BuildContext context) { return Observer(builder: (context) { final transaction = sendViewModel.pendingTransaction; + final additionalCostNotice = sendViewModel.pendingTransactionAdditionalCostNotice; final currencySymbol = sendViewModel.amountParsingProxy.getCryptoSymbol(sendViewModel.selectedCryptoCurrency); @@ -398,6 +399,26 @@ class SendTransactionDetails extends StatelessWidget { ), ), ), + if (additionalCostNotice != null) ...[ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Container( + height: 1, + color: Theme.of(context).colorScheme.surfaceContainerHigh, + ), + ), + Padding( + padding: const EdgeInsets.all(12), + child: Text( + additionalCostNotice, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + ], if (sendViewModel.isElectrumWallet) ...[ Padding( padding: EdgeInsets.symmetric(horizontal: 12), diff --git a/lib/solana/cw_solana.dart b/lib/solana/cw_solana.dart index 77656732c..46edfca2a 100644 --- a/lib/solana/cw_solana.dart +++ b/lib/solana/cw_solana.dart @@ -130,11 +130,11 @@ class CWSolana extends Solana { final token = (wallet as SolanaWallet).splTokenBySymbol(transaction.amount.currency.symbol); - if (token == null) { - throw StateError('No SPL token for symbol ${transaction.amount.currency.symbol}'); + if (token != null) { + return token; } - return token; + return transaction.amount.currency as CryptoCurrency; } @override @@ -276,6 +276,8 @@ class CWSolana extends Solana { 'Jupiter swap returned unknown status: $status. Error: $errorMessage. Code: $errorCode', ); } + } on JupiterSwapFailedException { + rethrow; } catch (e) { throw Exception('Failed to execute Jupiter swap: $e'); } @@ -420,7 +422,9 @@ class CWSolana extends Solana { if (discoveredMints.isNotEmpty) { await wallet.updateSPLTokenTransactions(specificMints: discoveredMints); } - } catch (_) {} + } catch (e) { + printV("Error discovering wallet tokens: ${e.toString()}"); + } } @override diff --git a/lib/src/screens/exchange_trade/exchange_trade_page.dart b/lib/src/screens/exchange_trade/exchange_trade_page.dart index 73966ab33..87bbc39f7 100644 --- a/lib/src/screens/exchange_trade/exchange_trade_page.dart +++ b/lib/src/screens/exchange_trade/exchange_trade_page.dart @@ -328,6 +328,7 @@ class ExchangeTradeState extends State { amount: S.of(bottomSheetContext).send_amount, amountValue: sendVM.amountParsingProxy .getDisplayCryptoAmount(amountValue, sendVM.selectedCryptoCurrency), + explanation: sendVM.pendingTransactionAdditionalCostNotice, fiatAmountValue: fiatAmountValue, fee: isEVMCompatibleChain(sendVM.walletType) ? S.of(bottomSheetContext).send_estimated_fee diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index df85d0cac..9e3fa7f55 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -336,6 +336,18 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor @observable PendingTransaction? pendingTransaction; + String? get pendingTransactionAdditionalCostNotice { + final additionalCost = pendingTransaction?.additionalCost; + + if (additionalCost == null) { + return null; + } + + return S.current.recipient_account_creation_fee( + _appStore.amountParsingProxy.asDisplayStringWithSymbol(additionalCost), + ); + } + @computed String get balance { if (walletType == WalletType.litecoin && coinTypeToSpendFrom == UnspentCoinType.mweb) { @@ -1179,11 +1191,11 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor await sharedPreferences.setString(PreferencesKey.backgroundSyncLastTrigger(wallet.name), DateTime.now().add(Duration(minutes: 1)).toIso8601String()); } catch (e) { - if (e is JupiterSwapFailedException) { - await _updateSolanaTrade(signature: e.signature, isSuccess: false); - } state = FailureState(translateErrorMessage(e, wallet.type, wallet.currency)); - await _updateSolanaTrade(signature: '', isSuccess: false); + + final failedSignature = e is JupiterSwapFailedException ? e.signature : ""; + + await _updateSolanaTrade(signature: failedSignature, isSuccess: false); } } @@ -1509,6 +1521,10 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor return S.current.solana_no_associated_token_account_exception; } + if (error is AmbiguousTokenSymbolException) { + return S.current.ambiguous_token_symbol_exception(error.symbol); + } + if (errorMessage.contains('found no record of a prior credit')) { return S.current.insufficient_funds_for_tx; } diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 5d3eb9f2d..24f501597 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -61,6 +61,7 @@ "already_have_account": "هل لديك حساب بالفعل؟", "already_your_username": "هذا هو اسم المستخدم الخاص بك بالفعل!", "always": "دائمًا", + "ambiguous_token_symbol_exception": "يستخدم أكثر من رمز مميز في هذه المحفظة الرمز ${symbol}. قم بتعطيل الرمز المميز المكرر حتى يمكن التعرف على الرمز الصحيح قبل الإرسال.", "amount": "الكمية: ", "amount_is_below_minimum_limit": "سيكون رصيدك بعد الرسوم أقل من الحد الأدنى للمبلغ المطلوب للتبادل (${min})", "amount_is_estimate": "مبلغ الاستلام تقديري", @@ -845,6 +846,7 @@ "received": "تم الاستلام", "receiving": "الاستلام", "recieving": "جارٍ الاستلام", + "recipient_account_creation_fee": "هناك حاجة إلى ${amount} إضافية لإنشاء حساب رمزي للمستلم، لأنه ليس لديه حساب حتى الآن.", "recipient_address": "عنوان المستلم", "reconnect": "إعادة الاتصال", "reconnect_alert_text": "هل أنت متأكد أنك تريد إعادة الاتصال؟", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index d14612fb7..b443d1af8 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -61,6 +61,7 @@ "already_have_account": "Вече имате акаунт?", "already_your_username": "Това вече е вашето потребителско име!", "always": "Винаги", + "ambiguous_token_symbol_exception": "Повече от един токен в този портфейл използва символа ${symbol}. Деактивирайте дублиращия се токен, за да може правилният да бъде идентифициран преди изпращане.", "amount": "Количество: ", "amount_is_below_minimum_limit": "Вашият баланс след таксите ще бъде по-малък от минималната сума, необходима за обмяната (${min})", "amount_is_estimate": "Сумата за получаване е приблизителна", @@ -845,6 +846,7 @@ "received": "Получено", "receiving": "Получаване", "recieving": "Получаване", + "recipient_account_creation_fee": "Необходими са допълнителни ${amount} за създаване на токен акаунт за получателя, тъй като той все още няма такъв.", "recipient_address": "Адрес на получателя", "reconnect": "Повторно свързване", "reconnect_alert_text": "Сигурни ли сте, че искате да се свържете отново?", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index c0c0815d6..f62daa6d3 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -61,6 +61,7 @@ "already_have_account": "Už máte účet?", "already_your_username": "Toto už je vaše uživatelské jméno!", "always": "Vždy", + "ambiguous_token_symbol_exception": "Více než jeden token v této peněžence používá symbol ${symbol}. Deaktivujte duplicitní token, aby bylo možné před odesláním identifikovat ten správný.", "amount": "Částka: ", "amount_is_below_minimum_limit": "Váš zůstatek po poplatcích by byl nižší než minimální částka potřebná pro směnu (${min})", "amount_is_estimate": "Přijatá částka je pouze odhad.", @@ -845,6 +846,7 @@ "received": "Přijato", "receiving": "Přijímání", "recieving": "Přijímání", + "recipient_account_creation_fee": "K vytvoření tokenového účtu pro příjemce je potřeba další ${amount}, protože ještě žádný nemá.", "recipient_address": "Adresa příjemce", "reconnect": "Znovu připojit", "reconnect_alert_text": "Opravdu se chcete znovu připojit?", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 9b953104e..dd7abc180 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -63,6 +63,7 @@ "already_have_account": "Haben Sie bereits ein Konto?", "already_your_username": "Das ist bereits Ihr Benutzername!", "always": "Immer", + "ambiguous_token_symbol_exception": "Mehr als ein Token in dieser Wallet verwendet das Symbol ${symbol}. Deaktivieren Sie das Duplikat-Token, damit das richtige vor dem Senden identifiziert werden kann.", "amount": "Betrag: ", "amount_hidden": "Versteckter Betrag", "amount_is_below_minimum_limit": "Ihr Guthaben nach Gebühren läge unter dem für den Swap erforderlichen Mindestbetrag (${min}).", @@ -866,6 +867,7 @@ "received": "Empfangen", "receiving": "Empfangen", "recieving": "Empfangen", + "recipient_account_creation_fee": "Für die Erstellung eines Token-Kontos für den Empfänger ist ein zusätzlicher Betrag von ${amount} erforderlich, da dieser noch keins hat.", "recipient_address": "Empfängeradresse", "recipient_number": "Empfänger ${number}", "reconnect": "Erneut verbinden", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 7b65547c6..9c04a21c4 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -63,6 +63,7 @@ "already_have_account": "Already have an account?", "already_your_username": "This is already your username!", "always": "Always", + "ambiguous_token_symbol_exception": "More than one token in this wallet uses the symbol ${symbol}. Disable the duplicate token so the correct one can be identified before sending.", "amount": "Amount: ", "amount_hidden": "Amount hidden", "amount_is_below_minimum_limit": "Your balance after fees would be less than the minimum amount needed for the exchange (${min})", @@ -868,6 +869,7 @@ "received": "Received", "receiving": "Receiving", "recieving": "Recieving", + "recipient_account_creation_fee": "An extra ${amount} is needed to create a token account for the recipient, because they do not have one yet.", "recipient_address": "Recipient address", "recipient_number": "Recipient ${number}", "reconnect": "Reconnect", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 1e29a5c19..78321bd2c 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -61,6 +61,7 @@ "already_have_account": "¿Ya tienes una cuenta?", "already_your_username": "¡Este ya es tu nombre de usuario!", "always": "Siempre", + "ambiguous_token_symbol_exception": "Más de un token en esta billetera usa el símbolo ${symbol}. Deshabilite el token duplicado para que se pueda identificar el correcto antes de enviarlo.", "amount": "Cantidad: ", "amount_is_below_minimum_limit": "Tu saldo después de las comisiones sería inferior al importe mínimo necesario para el intercambio (${min})", "amount_is_estimate": "La cantidad a recibir es una estimación", @@ -847,6 +848,7 @@ "received": "Recibido", "receiving": "Recibiendo", "recieving": "Recibiendo", + "recipient_account_creation_fee": "Se necesita ${amount} adicional para crear una cuenta simbólica para el destinatario, porque aún no tiene una.", "recipient_address": "Dirección del destinatario", "reconnect": "Reconectar", "reconnect_alert_text": "¿Estás seguro de que quieres reconectar?", diff --git a/res/values/strings_fa.arb b/res/values/strings_fa.arb index f40a8e113..cf886a43b 100644 --- a/res/values/strings_fa.arb +++ b/res/values/strings_fa.arb @@ -61,6 +61,7 @@ "already_have_account": "از قبل حساب کاربری دارید؟", "already_your_username": "این از قبل نام کاربری شماست!", "always": "همیشه", + "ambiguous_token_symbol_exception": "بیش از یک توکن در این کیف پول از نماد ${symbol} استفاده می‌کند. رمز تکراری را غیرفعال کنید تا قبل از ارسال، کد صحیح شناسایی شود.", "amount": "مقدار: ", "amount_is_below_minimum_limit": "موجودی شما پس از کسر کارمزد کمتر از حداقل مقدار مورد نیاز برای مبادله (${min}) خواهد بود", "amount_is_estimate": "مقدار دریافتی تقریبی است", @@ -845,6 +846,7 @@ "received": "دریافت‌شده", "receiving": "دریافت", "recieving": "دریافت", + "recipient_account_creation_fee": "یک ${amount} اضافی برای ایجاد یک حساب رمزی برای گیرنده مورد نیاز است، زیرا آنها هنوز حسابی ندارند.", "recipient_address": "آدرس دریافت‌کننده", "reconnect": "اتصال دوباره", "reconnect_alert_text": "آیا مطمئن هستید که می‌خواهید دوباره وصل شوید؟", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 0ced5ec4d..d3f135c88 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -61,6 +61,7 @@ "already_have_account": "Vous avez déjà un compte ?", "already_your_username": "C'est déjà votre nom d'utilisateur !", "always": "Toujours", + "ambiguous_token_symbol_exception": "Plus d'un jeton dans ce portefeuille utilise le symbole ${symbol}. Désactivez le jeton en double afin que le bon puisse être identifié avant l'envoi.", "amount": "Montant : ", "amount_is_below_minimum_limit": "Votre solde après les frais serait inférieur au montant minimum requis pour l'échange (${min})", "amount_is_estimate": "Le montant à recevoir est une estimation", @@ -845,6 +846,7 @@ "received": "Reçu", "receiving": "Réception", "recieving": "Réception", + "recipient_account_creation_fee": "Un ${amount} supplémentaire est nécessaire pour créer un compte token pour le destinataire, car il n'en a pas encore.", "recipient_address": "Adresse du destinataire", "reconnect": "Reconnecter", "reconnect_alert_text": "Êtes-vous sûr de vouloir vous reconnecter ?", diff --git a/res/values/strings_gn.arb b/res/values/strings_gn.arb index 679d73244..551e16d42 100644 --- a/res/values/strings_gn.arb +++ b/res/values/strings_gn.arb @@ -61,6 +61,7 @@ "already_have_account": "¿Erekóma peteĩ cuenta?", "already_your_username": "¡Kóva ha'e nde puruhára réra voi!", "always": "Tapia", + "ambiguous_token_symbol_exception": "Hetave peteĩ token ko billetera-pe oipuru símbolo ${symbol}. Embogue pe token duplicado ikatu hag̃uáicha ojekuaa pe hekopetegua oñemondo mboyve.", "amount": "Hetakue: ", "amount_is_below_minimum_limit": "Nde saldo, pe comisión rire, saʼivéta pe mínimo oñeikotevẽva hag̃ua pe intercambio (${min})", "amount_is_estimate": "Pe monto oñemog̃uahẽtáva ha’e peteĩ jehecha hag̃a.", @@ -849,6 +850,7 @@ "received": "Ojapyhy", "receiving": "Oñemog̃uahẽ", "recieving": "Oñemog̃uahẽ", + "recipient_account_creation_fee": "Oñeikotevẽ peteĩ ${amount} extra ojejapo hag̃ua peteĩ cuenta token rehegua pe ohupytysévape g̃uarã, ndorekóigui gueteri hikuái peteĩ.", "recipient_address": "Ojapyhýva kundaharape", "reconnect": "Eñembojoaju jey", "reconnect_alert_text": "¿Reime añetehápe reñembojoaju jeyse?", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 1df9ce6fa..ea901d7c4 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -61,6 +61,7 @@ "already_have_account": "Kana da asusu tuni?", "already_your_username": "Wannan tuni sunan mai amfani naka ne!", "always": "Koyaushe", + "ambiguous_token_symbol_exception": "Alama fiye da ɗaya a cikin wannan jakar tana amfani da alamar ${symbol}. Kashe alamar kwafin don a iya gano madaidaicin kafin aikawa.", "amount": "Adadi: ", "amount_is_below_minimum_limit": "Ma'auninka bayan kuɗaɗen ma'amala zai zama ƙasa da mafi ƙarancin adadin da ake buƙata don musayar (${min})", "amount_is_estimate": "Adadin da za a karɓa kimantawa ne", @@ -847,6 +848,7 @@ "received": "An karɓa", "receiving": "Karɓa", "recieving": "Ana karɓa", + "recipient_account_creation_fee": "Ana buƙatar ƙarin ${amount} don ƙirƙirar asusun token ga mai karɓa, saboda ba su da ɗaya tukuna.", "recipient_address": "Adireshin mai karɓa", "reconnect": "Sake haɗawa", "reconnect_alert_text": "Shin kun tabbata kuna son sake haɗawa?", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 0aa460024..15edf0080 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -61,6 +61,7 @@ "already_have_account": "क्या आपके पास पहले से एक खाता है?", "already_your_username": "यह पहले से ही आपका यूज़रनेम है!", "always": "हमेशा", + "ambiguous_token_symbol_exception": "इस वॉलेट में एक से अधिक टोकन प्रतीक ${symbol} का उपयोग करते हैं। डुप्लिकेट टोकन को अक्षम करें ताकि भेजने से पहले सही टोकन की पहचान की जा सके।", "amount": "राशि: ", "amount_is_below_minimum_limit": "फीस के बाद आपका बैलेंस एक्सचेंज के लिए आवश्यक न्यूनतम राशि (${min}) से कम होगा", "amount_is_estimate": "प्राप्त होने वाली राशि अनुमानित है", @@ -847,6 +848,7 @@ "received": "प्राप्त", "receiving": "प्राप्त करना", "recieving": "प्राप्त हो रहा है", + "recipient_account_creation_fee": "प्राप्तकर्ता के लिए एक टोकन खाता बनाने के लिए अतिरिक्त ${amount} की आवश्यकता होती है, क्योंकि उनके पास अभी तक कोई खाता नहीं है।", "recipient_address": "प्राप्तकर्ता का पता", "reconnect": "पुनः कनेक्ट करें", "reconnect_alert_text": "क्या आप वाकई फिर से कनेक्ट करना चाहते हैं?", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 4f5271137..2d99234eb 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -61,6 +61,7 @@ "already_have_account": "Već imate račun?", "already_your_username": "Ovo je već vaše korisničko ime!", "always": "Uvijek", + "ambiguous_token_symbol_exception": "Više od jednog tokena u ovom novčaniku koristi simbol ${symbol}. Onemogućite dvostruki token kako bi se ispravan mogao identificirati prije slanja.", "amount": "Iznos: ", "amount_is_below_minimum_limit": "Vaš saldo nakon naknada bio bi manji od minimalnog iznosa potrebnog za razmjenu (${min})", "amount_is_estimate": "Primljeni iznos je procijenjen", @@ -845,6 +846,7 @@ "received": "Primljeno", "receiving": "Primanje", "recieving": "Primanje", + "recipient_account_creation_fee": "Dodatni iznos od ${amount} je potreban za stvaranje token računa za primatelja jer ga on još nema.", "recipient_address": "Adresa primatelja", "reconnect": "Ponovno poveži", "reconnect_alert_text": "Jeste li sigurni da se želite ponovno povezati?", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index a5930373f..f1a209c91 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -61,6 +61,7 @@ "already_have_account": "Արդեն ունե՞ք հաշիվ։", "already_your_username": "Սա արդեն ձեր օգտանունն է։", "always": "Միշտ", + "ambiguous_token_symbol_exception": "Այս դրամապանակում մեկից ավելի թոքեններ օգտագործում են ${symbol} խորհրդանիշը: Անջատեք կրկնօրինակ նշանը, որպեսզի ճիշտը հնարավոր լինի նույնացնել նախքան ուղարկելը:", "amount": "Գումար՝ ", "amount_is_below_minimum_limit": "Միջնորդավճարներից հետո Ձեր մնացորդը կլինի փոխանակման համար անհրաժեշտ նվազագույն գումարից (${min}) պակաս", "amount_is_estimate": "Ստացվող գումարը մոտավոր է", @@ -845,6 +846,7 @@ "received": "Ստացված", "receiving": "Ստացում", "recieving": "Ստացում", + "recipient_account_creation_fee": "Ստացողի համար խորհրդանշական հաշիվ ստեղծելու համար անհրաժեշտ է լրացուցիչ ${amount}, քանի որ նրանք դեռ չունեն:", "recipient_address": "Ստացողի հասցե", "reconnect": "Վերամիանալ", "reconnect_alert_text": "Վստա՞հ եք, որ ցանկանում եք կրկին միանալ:", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 16fcb4881..c1fea5b4d 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -61,6 +61,7 @@ "already_have_account": "Sudah punya akun?", "already_your_username": "Ini sudah merupakan nama pengguna Anda!", "always": "Selalu", + "ambiguous_token_symbol_exception": "Lebih dari satu token di dompet ini menggunakan simbol ${symbol}. Nonaktifkan token duplikat sehingga token yang benar dapat diidentifikasi sebelum dikirim.", "amount": "Jumlah: ", "amount_is_below_minimum_limit": "Saldo Anda setelah biaya akan lebih rendah dari jumlah minimum yang diperlukan untuk penukaran (${min})", "amount_is_estimate": "Jumlah yang diterima adalah perkiraan", @@ -847,6 +848,7 @@ "received": "Diterima", "receiving": "Menerima", "recieving": "Menerima", + "recipient_account_creation_fee": "${amount} tambahan diperlukan untuk membuat akun token bagi penerima, karena mereka belum memilikinya.", "recipient_address": "Alamat penerima", "reconnect": "Hubungkan kembali", "reconnect_alert_text": "Apakah Anda yakin ingin terhubung kembali?", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index a7f73b629..d7ae0b9c0 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -61,6 +61,7 @@ "already_have_account": "Hai già un account?", "already_your_username": "Questo è già il tuo nome utente!", "always": "Sempre", + "ambiguous_token_symbol_exception": "Più di un token in questo portafoglio utilizza il simbolo ${symbol}. Disabilitare il token duplicato in modo da poter identificare quello corretto prima dell'invio.", "amount": "Importo: ", "amount_is_below_minimum_limit": "Il saldo dopo le commissioni sarebbe inferiore all'importo minimo richiesto per lo scambio (${min})", "amount_is_estimate": "L'importo da ricevere è una stima", @@ -1476,5 +1477,6 @@ "zcash_card_enable_later": "Puoi sempre abilitare questa carta in seguito nelle impostazioni", "zcash_card_missing_funds": "Fondi mancanti?", "zcash_card_scan": "Scansiona", - "zcash_card_warning": "Non chiudere l'app finché la procedura non è completata, altrimenti sarà necessario riavviare il processo da zero." + "zcash_card_warning": "Non chiudere l'app finché la procedura non è completata, altrimenti sarà necessario riavviare il processo da zero.", + "recipient_account_creation_fee": "È necessario un extra di ${amount} per creare un account token per il destinatario, perché non ne ha ancora uno." } \ No newline at end of file diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 8acfdaf15..e9708e48d 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -61,6 +61,7 @@ "already_have_account": "すでにアカウントをお持ちですか?", "already_your_username": "これはすでにあなたのユーザー名です!", "always": "常に", + "ambiguous_token_symbol_exception": "このウォレット内の複数のトークンは、シンボル ${symbol} を使用しています。送信前に正しいトークンを識別できるように、重複したトークンを無効にします。", "amount": "金額: ", "amount_is_below_minimum_limit": "手数料差し引き後の残高が、交換に必要な最低金額(${min})を下回ります", "amount_is_estimate": "受け取り金額は概算です", @@ -1474,5 +1475,6 @@ "zcash_card_enable_later": "このカードは後から設定でいつでも有効にできます", "zcash_card_missing_funds": "資金が見当たりませんか?", "zcash_card_scan": "スキャン", - "zcash_card_warning": "手順が完了するまでアプリを閉じないでください。閉じると、このプロセスを最初からやり直す必要があります。" + "zcash_card_warning": "手順が完了するまでアプリを閉じないでください。閉じると、このプロセスを最初からやり直す必要があります。", + "recipient_account_creation_fee": "受信者はまだトークン アカウントを持っていないため、追加の ${amount} が必要です。" } \ No newline at end of file diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 2777da024..35dd2e704 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -61,6 +61,7 @@ "already_have_account": "이미 계정이 있으신가요?", "already_your_username": "이미 사용 중인 사용자 이름입니다!", "always": "항상", + "ambiguous_token_symbol_exception": "이 지갑에 있는 두 개 이상의 토큰이 ${symbol} 기호를 사용합니다. 전송하기 전에 올바른 토큰을 식별할 수 있도록 중복 토큰을 비활성화합니다.", "amount": "수량: ", "amount_is_below_minimum_limit": "수수료 차감 후 잔액이 교환에 필요한 최소 금액(${min})보다 적을 것입니다", "amount_is_estimate": "수신 금액은 예상치입니다", @@ -1473,5 +1474,6 @@ "zcash_card_enable_later": "설정에서 언제든지 나중에 이 카드를 활성화할 수 있습니다.", "zcash_card_missing_funds": "자금이 사라졌나요?", "zcash_card_scan": "스캔", - "zcash_card_warning": "절차가 완료될 때까지 앱을 닫지 마십시오. 닫으면 이 과정을 처음부터 다시 시작해야 합니다." + "zcash_card_warning": "절차가 완료될 때까지 앱을 닫지 마십시오. 닫으면 이 과정을 처음부터 다시 시작해야 합니다.", + "recipient_account_creation_fee": "아직 토큰 계정이 없기 때문에 수신자를 위한 토큰 계정을 생성하려면 추가 ${amount}가 필요합니다." } \ No newline at end of file diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index e8e9eac61..990a568e2 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -61,6 +61,7 @@ "already_have_account": "အကောင့်ရှိပြီးသားလား?", "already_your_username": "ဒါက သင့်အသုံးပြုသူအမည် ဖြစ်ပြီးသားပါ!", "always": "အမြဲတမ်း", + "ambiguous_token_symbol_exception": "ဤပိုက်ဆံအိတ်ရှိ တိုကင်တစ်ခုထက်ပိုသော သင်္ကေတ ${symbol} ကို အသုံးပြုသည်။ မပို့မီ မှန်ကန်သောတစ်ခုကို ဖော်ထုတ်နိုင်စေရန် ပွားနေသော တိုကင်ကို ပိတ်ပါ။", "amount": "ပမာဏ: ", "amount_is_below_minimum_limit": "အခကြေးငွေများနုတ်ပြီးနောက် သင့်လက်ကျန်ငွေသည် လဲလှယ်ရန်လိုအပ်သည့် အနိမ့်ဆုံးပမာဏ (${min}) ထက် နည်းသွားမည်", "amount_is_estimate": "လက်ခံရရှိမည့်ပမာဏသည် ခန့်မှန်းတန်ဖိုးဖြစ်သည်။", @@ -1473,5 +1474,6 @@ "zcash_card_enable_later": "သင်သည် ဤကတ်ကို ဆက်တင်များတွင် နောက်မှ အချိန်မရွေး ဖွင့်နိုင်ပါသည်။", "zcash_card_missing_funds": "ငွေကြေး ပျောက်ဆုံးနေပါသလား?", "zcash_card_scan": "စကင်", - "zcash_card_warning": "လုပ်ငန်းစဉ် ပြီးဆုံးသည်အထိ အက်ပ်ကို မပိတ်ပါနှင့်။ ပိတ်လိုက်ပါက ဤလုပ်ငန်းစဉ်ကို အစမှ ပြန်လည်စတင်ရမည်ဖြစ်သည်။" + "zcash_card_warning": "လုပ်ငန်းစဉ် ပြီးဆုံးသည်အထိ အက်ပ်ကို မပိတ်ပါနှင့်။ ပိတ်လိုက်ပါက ဤလုပ်ငန်းစဉ်ကို အစမှ ပြန်လည်စတင်ရမည်ဖြစ်သည်။", + "recipient_account_creation_fee": "လက်ခံသူအတွက် တိုကင်အကောင့်တစ်ခုဖန်တီးရန် ၎င်းတို့တွင် တစ်ခုမရှိသေးသောကြောင့် အပို ${amount} လိုအပ်ပါသည်။" } \ No newline at end of file diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index d9d677a95..0671d7e14 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -61,6 +61,7 @@ "already_have_account": "Heb je al een account?", "already_your_username": "Dit is al je gebruikersnaam!", "always": "Altijd", + "ambiguous_token_symbol_exception": "Meer dan één token in deze portemonnee gebruikt het symbool ${symbol}. Schakel het dubbele token uit, zodat het juiste token kan worden geïdentificeerd voordat het wordt verzonden.", "amount": "Bedrag: ", "amount_is_below_minimum_limit": "Je saldo na kosten zou lager zijn dan het minimale bedrag dat nodig is voor de exchange (${min})", "amount_is_estimate": "Het te ontvangen bedrag is een schatting", @@ -1474,5 +1475,6 @@ "zcash_card_enable_later": "Je kunt deze kaart later altijd inschakelen via de instellingen", "zcash_card_missing_funds": "Fondsen ontbreken?", "zcash_card_scan": "Scannen", - "zcash_card_warning": "Sluit de app niet voordat de procedure is voltooid. Als je dit wel doet, moet dit proces helemaal opnieuw worden gestart." + "zcash_card_warning": "Sluit de app niet voordat de procedure is voltooid. Als je dit wel doet, moet dit proces helemaal opnieuw worden gestart.", + "recipient_account_creation_fee": "Er is een extra ${amount} nodig om een ​​tokenaccount voor de ontvanger aan te maken, omdat deze er nog geen heeft." } \ No newline at end of file diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 8650bd0dc..e0de2a835 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -61,6 +61,7 @@ "already_have_account": "Masz już konto?", "already_your_username": "To już jest Twoja nazwa użytkownika!", "always": "Zawsze", + "ambiguous_token_symbol_exception": "Więcej niż jeden token w tym portfelu używa symbolu ${symbol}. Wyłącz duplikat tokena, aby przed wysłaniem można było zidentyfikować właściwy.", "amount": "Kwota: ", "amount_is_below_minimum_limit": "Twoje saldo po opłatach byłoby niższe niż minimalna kwota wymagana do wymiany (${min})", "amount_is_estimate": "Otrzymywana kwota jest szacunkowa", @@ -1473,5 +1474,6 @@ "zcash_card_enable_later": "Zawsze możesz włączyć tę kartę później w ustawieniach", "zcash_card_missing_funds": "Brakuje środków?", "zcash_card_scan": "Skanuj", - "zcash_card_warning": "Nie zamykaj aplikacji do czasu zakończenia procedury. Jeśli to zrobisz, proces będzie musiał rozpocząć się od nowa." + "zcash_card_warning": "Nie zamykaj aplikacji do czasu zakończenia procedury. Jeśli to zrobisz, proces będzie musiał rozpocząć się od nowa.", + "recipient_account_creation_fee": "Do założenia konta tokenowego dla odbiorcy potrzebna jest dodatkowa ${amount}, ponieważ jeszcze takiego nie posiada." } \ No newline at end of file diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 2a392da2b..d9204e345 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -61,6 +61,7 @@ "already_have_account": "Já tem uma conta?", "already_your_username": "Este já é o seu nome de utilizador!", "always": "Sempre", + "ambiguous_token_symbol_exception": "Mais de um token nesta carteira usa o símbolo ${symbol}. Desative o token duplicado para que o correto possa ser identificado antes do envio.", "amount": "Quantidade: ", "amount_is_below_minimum_limit": "Seu saldo após as taxas seria menor do que o valor mínimo necessário para a troca (${min})", "amount_is_estimate": "O valor a receber é uma estimativa", @@ -1476,5 +1477,6 @@ "zcash_card_enable_later": "Você sempre pode ativar este cartão mais tarde nas configurações", "zcash_card_missing_funds": "Fundos em falta?", "zcash_card_scan": "Escanear", - "zcash_card_warning": "Não feche o aplicativo até que o procedimento seja concluído; se você fizer isso, esse processo precisará ser reiniciado do zero." + "zcash_card_warning": "Não feche o aplicativo até que o procedimento seja concluído; se você fizer isso, esse processo precisará ser reiniciado do zero.", + "recipient_account_creation_fee": "É necessário um ${amount} extra para criar uma conta token para o destinatário, porque ele ainda não tem uma." } \ No newline at end of file diff --git a/res/values/strings_pt_BR.arb b/res/values/strings_pt_BR.arb index 51e816979..d703a3ec8 100644 --- a/res/values/strings_pt_BR.arb +++ b/res/values/strings_pt_BR.arb @@ -58,6 +58,7 @@ "already_have_account": "Já tem uma conta?", "already_your_username": "Este já é o seu nome de usuário!", "always": "Sempre", + "ambiguous_token_symbol_exception": "Mais de um token nesta carteira usa o símbolo ${symbol}. Desative o token duplicado para que o correto possa ser identificado antes do envio.", "amount": "Quantidade: ", "amount_is_below_minimum_limit": "Seu saldo após as taxas seria menor do que o valor mínimo necessário para a troca (${min})", "amount_is_estimate": "O valor a receber é uma estimativa", @@ -802,6 +803,7 @@ "received": "Recebido", "receiving": "Recebendo", "recieving": "Recebendo", + "recipient_account_creation_fee": "É necessário um ${amount} extra para criar uma conta token para o destinatário, porque ele ainda não tem uma.", "recipient_address": "Endereço do destinatário", "reconnect": "Reconectar", "reconnect_alert_text": "Tem certeza de que deseja reconectar?", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 1bdeee2a1..aeb56f31c 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -61,6 +61,7 @@ "already_have_account": "У вас уже есть аккаунт?", "already_your_username": "Это уже ваше имя пользователя!", "always": "Всегда", + "ambiguous_token_symbol_exception": "Более одного токена в этом кошельке используют символ ${symbol}. Отключите дубликат токена, чтобы можно было определить правильный перед отправкой.", "amount": "Сумма: ", "amount_is_below_minimum_limit": "Ваш баланс после комиссий будет меньше минимальной суммы, необходимой для обмена (${min})", "amount_is_estimate": "Сумма к получению является приблизительной", @@ -1474,5 +1475,6 @@ "zcash_card_enable_later": "Вы всегда можете включить эту карту позже в настройках", "zcash_card_missing_funds": "Не хватает средств?", "zcash_card_scan": "Сканировать", - "zcash_card_warning": "Не закрывайте приложение до завершения процедуры. Если вы это сделаете, процесс придется перезапустить с нуля." + "zcash_card_warning": "Не закрывайте приложение до завершения процедуры. Если вы это сделаете, процесс придется перезапустить с нуля.", + "recipient_account_creation_fee": "Для создания учетной записи токена для получателя требуется дополнительная сумма ${amount}, поскольку у него ее еще нет." } \ No newline at end of file diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index b8e98d11b..44096cbd1 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -61,6 +61,7 @@ "already_have_account": "มีบัญชีอยู่แล้วใช่ไหม?", "already_your_username": "นี่คือชื่อผู้ใช้ของคุณอยู่แล้ว!", "always": "เสมอ", + "ambiguous_token_symbol_exception": "โทเค็นมากกว่าหนึ่งรายการในกระเป๋าเงินนี้ใช้สัญลักษณ์ ${symbol} ปิดใช้งานโทเค็นที่ซ้ำกันเพื่อให้สามารถระบุโทเค็นที่ถูกต้องก่อนส่ง", "amount": "จำนวน: ", "amount_is_below_minimum_limit": "ยอดคงเหลือของคุณหลังหักค่าธรรมเนียมจะน้อยกว่าจำนวนขั้นต่ำที่จำเป็นสำหรับการแลกเปลี่ยน (${min})", "amount_is_estimate": "จำนวนเงินที่ได้รับเป็นการประมาณการ", @@ -1473,5 +1474,6 @@ "zcash_card_enable_later": "คุณสามารถเปิดใช้งานการ์ดนี้ได้เสมอในภายหลังในการตั้งค่า", "zcash_card_missing_funds": "ยอดเงินหายไป?", "zcash_card_scan": "สแกน", - "zcash_card_warning": "อย่าปิดแอปจนกว่าขั้นตอนจะเสร็จสิ้น หากคุณปิดแอป กระบวนการนี้จะต้องเริ่มใหม่ตั้งแต่ต้น" + "zcash_card_warning": "อย่าปิดแอปจนกว่าขั้นตอนจะเสร็จสิ้น หากคุณปิดแอป กระบวนการนี้จะต้องเริ่มใหม่ตั้งแต่ต้น", + "recipient_account_creation_fee": "จำเป็นต้องมี ${amount} เพิ่มเติมเพื่อสร้างบัญชีโทเค็นสำหรับผู้รับ เนื่องจากยังไม่มีบัญชี" } \ No newline at end of file diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 86a0d4519..bdb4d46a6 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -61,6 +61,7 @@ "already_have_account": "Mayroon ka nang account?", "already_your_username": "Ito na ang username mo!", "always": "Palagi", + "ambiguous_token_symbol_exception": "Higit sa isang token sa wallet na ito ang gumagamit ng simbolo na ${symbol}. I-disable ang duplicate na token para matukoy ang tama bago ipadala.", "amount": "Halaga: ", "amount_is_below_minimum_limit": "Ang iyong balanse pagkatapos ng mga bayarin ay magiging mas mababa kaysa sa minimum na halagang kailangan para sa palitan (${min})", "amount_is_estimate": "Ang halagang matatanggap ay isang pagtatantya", @@ -1473,5 +1474,6 @@ "zcash_card_enable_later": "Maaari mo itong paganahin anumang oras sa ibang pagkakataon sa mga setting", "zcash_card_missing_funds": "Nawawalang pondo?", "zcash_card_scan": "I-scan", - "zcash_card_warning": "Huwag isara ang app hanggang sa makumpleto ang proseso; kung gagawin mo ito, kakailanganing magsimula muli mula sa simula." + "zcash_card_warning": "Huwag isara ang app hanggang sa makumpleto ang proseso; kung gagawin mo ito, kakailanganing magsimula muli mula sa simula.", + "recipient_account_creation_fee": "Kailangan ng dagdag na ${amount} para gumawa ng token account para sa tatanggap, dahil wala pa sila nito." } \ No newline at end of file diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index a2f6a18b6..67684f6df 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -61,6 +61,7 @@ "already_have_account": "Zaten bir hesabınız var mı?", "already_your_username": "Bu zaten kullanıcı adınız!", "always": "Her zaman", + "ambiguous_token_symbol_exception": "Bu cüzdanda birden fazla token ${symbol} sembolünü kullanıyor. Göndermeden önce doğru jetonun belirlenebilmesi için yinelenen jetonu devre dışı bırakın.", "amount": "Miktar: ", "amount_is_below_minimum_limit": "Ücretlerden sonra bakiyeniz, takas için gereken minimum miktardan daha az olur (${min})", "amount_is_estimate": "Alınacak tutar tahminidir", @@ -1473,5 +1474,6 @@ "zcash_card_enable_later": "Bu kartı daha sonra ayarlardan her zaman etkinleştirebilirsiniz", "zcash_card_missing_funds": "Bakiye eksik mi?", "zcash_card_scan": "Tara", - "zcash_card_warning": "İşlem tamamlanana kadar uygulamayı kapatmayın; aksi takdirde bu işlemin baştan yeniden başlatılması gerekecektir." + "zcash_card_warning": "İşlem tamamlanana kadar uygulamayı kapatmayın; aksi takdirde bu işlemin baştan yeniden başlatılması gerekecektir.", + "recipient_account_creation_fee": "Alıcının henüz bir token hesabı olmadığı için, alıcı için bir token hesabı oluşturmak için fazladan ${amount} gerekiyor." } \ No newline at end of file diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 93ae72c1e..14fa38f59 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -61,6 +61,7 @@ "already_have_account": "Вже маєте обліковий запис?", "already_your_username": "Це вже ваше ім’я користувача!", "always": "Завжди", + "ambiguous_token_symbol_exception": "Більш ніж один токен у цьому гаманці використовує символ ${symbol}. Вимкніть повторюваний маркер, щоб можна було визначити правильний перед надсиланням.", "amount": "Сума: ", "amount_is_below_minimum_limit": "Ваш баланс після комісій буде меншим за мінімальну суму, необхідну для обміну (${min})", "amount_is_estimate": "Сума отримання є приблизною", @@ -1474,5 +1475,6 @@ "zcash_card_enable_later": "Ви завжди можете ввімкнути цю картку пізніше в налаштуваннях", "zcash_card_missing_funds": "Зникли кошти?", "zcash_card_scan": "Сканувати", - "zcash_card_warning": "Не закривайте застосунок, доки процедура не завершиться. Якщо ви це зробите, цей процес потрібно буде перезапустити з нуля." + "zcash_card_warning": "Не закривайте застосунок, доки процедура не завершиться. Якщо ви це зробите, цей процес потрібно буде перезапустити з нуля.", + "recipient_account_creation_fee": "Потрібна додаткова сума в розмірі ${amount}, щоб створити обліковий запис-токен для одержувача, оскільки він його ще не має." } \ No newline at end of file diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 80e93d207..ba77da2bf 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -61,6 +61,7 @@ "already_have_account": "کیا آپ کے پاس پہلے سے اکاؤنٹ ہے؟", "already_your_username": "یہ پہلے ہی آپ کا صارف نام ہے!", "always": "ہمیشہ", + "ambiguous_token_symbol_exception": "اس بٹوے میں ایک سے زیادہ ٹوکن ${symbol} کی علامت استعمال کرتا ہے۔ ڈپلیکیٹ ٹوکن کو غیر فعال کریں تاکہ بھیجنے سے پہلے صحیح کی شناخت ہو سکے۔", "amount": "رقم: ", "amount_is_below_minimum_limit": "فیس کے بعد آپ کا بیلنس ایکسچینج کے لیے درکار کم از کم رقم (${min}) سے کم ہوگا", "amount_is_estimate": "وصول ہونے والی رقم ایک تخمینہ ہے۔", @@ -1475,5 +1476,6 @@ "zcash_card_enable_later": "آپ اس کارڈ کو بعد میں بھی ترتیبات میں فعال کر سکتے ہیں", "zcash_card_missing_funds": "فنڈز غائب ہیں؟", "zcash_card_scan": "اسکین کریں", - "zcash_card_warning": "طریقہ کار مکمل ہونے تک ایپ بند نہ کریں۔ اگر آپ ایسا کریں گے تو یہ عمل شروع سے دوبارہ شروع کرنا پڑے گا۔" + "zcash_card_warning": "طریقہ کار مکمل ہونے تک ایپ بند نہ کریں۔ اگر آپ ایسا کریں گے تو یہ عمل شروع سے دوبارہ شروع کرنا پڑے گا۔", + "recipient_account_creation_fee": "وصول کنندہ کے لیے ٹوکن اکاؤنٹ بنانے کے لیے ایک اضافی ${amount} کی ضرورت ہے، کیونکہ ان کے پاس ابھی تک کوئی اکاؤنٹ نہیں ہے۔" } \ No newline at end of file diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index 68fd9afd1..41b269d47 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -61,6 +61,7 @@ "already_have_account": "Bạn đã có tài khoản chưa?", "already_your_username": "Đây đã là tên người dùng của bạn!", "always": "Luôn", + "ambiguous_token_symbol_exception": "Nhiều mã thông báo trong ví này sử dụng ký hiệu ${symbol}. Vô hiệu hóa mã thông báo trùng lặp để có thể xác định mã thông báo chính xác trước khi gửi.", "amount": "Số lượng: ", "amount_is_below_minimum_limit": "Số dư của bạn sau khi trừ phí sẽ thấp hơn số tiền tối thiểu cần thiết để trao đổi (${min})", "amount_is_estimate": "Số tiền nhận được chỉ là ước tính", @@ -1474,5 +1475,6 @@ "zcash_card_enable_later": "Bạn luôn có thể bật thẻ này sau trong phần cài đặt", "zcash_card_missing_funds": "Thiếu tiền?", "zcash_card_scan": "Quét", - "zcash_card_warning": "Không đóng ứng dụng cho đến khi quy trình hoàn tất; nếu bạn làm vậy, quy trình này sẽ phải bắt đầu lại từ đầu." + "zcash_card_warning": "Không đóng ứng dụng cho đến khi quy trình hoàn tất; nếu bạn làm vậy, quy trình này sẽ phải bắt đầu lại từ đầu.", + "recipient_account_creation_fee": "Cần thêm ${amount} để tạo tài khoản mã thông báo cho người nhận vì họ chưa có tài khoản." } \ No newline at end of file diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 6ca70f8e1..15f858b93 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -61,6 +61,7 @@ "already_have_account": "Ṣé o ti ní àkántì tẹ́lẹ̀?", "already_your_username": "Eyi ti jẹ orúkọ olumulo rẹ tẹlẹ!", "always": "Nígbà gbogbo", + "ambiguous_token_symbol_exception": "Die e sii ju ami-ami kan ninu apamọwọ yii nlo aami ${symbol}. Pa àmi àdáwòkọ rẹ́ kí ó lè mọ èyí tí ó tọ́ kí ó tó fi ránṣẹ́.", "amount": "Iye: ", "amount_is_below_minimum_limit": "Iwontunwonsi rẹ lẹ́yìn àwọn owó ìdúná yóò kéré ju iye kéréjù tí a nílò fún paṣipaarọ (${min})", "amount_is_estimate": "Iye tí a máa gba jẹ́ àfojúsùn", @@ -1474,5 +1475,6 @@ "zcash_card_enable_later": "O le ma a mu kaadi yii ṣiṣẹ nigbamii ninu awọn eto", "zcash_card_missing_funds": "Ṣe owó rẹ sọnù?", "zcash_card_scan": "Ṣàwárí", - "zcash_card_warning": "Maṣe pa ohun elo naa titi ilana naa yoo fi pari; bí o bá ṣe bẹ́ẹ̀, ilana yìí yóò ní láti tún bẹ̀rẹ̀ láti ìbẹ̀rẹ̀." + "zcash_card_warning": "Maṣe pa ohun elo naa titi ilana naa yoo fi pari; bí o bá ṣe bẹ́ẹ̀, ilana yìí yóò ní láti tún bẹ̀rẹ̀ láti ìbẹ̀rẹ̀.", + "recipient_account_creation_fee": "A nilo afikun ${amount} lati ṣẹda akọọlẹ ami-ami kan fun olugba, nitori wọn ko ni ọkan sibẹsibẹ." } \ No newline at end of file diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 8e3256b97..711735023 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -61,6 +61,7 @@ "already_have_account": "已经有账户了?", "already_your_username": "这已经是您的用户名!", "always": "始终", + "ambiguous_token_symbol_exception": "该钱包中有多个代币使用符号 ${symbol}。禁用重复的令牌,以便在发送之前可以识别正确的令牌。", "amount": "数量:", "amount_is_below_minimum_limit": "扣除手续费后,您的余额将低于兑换所需的最低金额(${min})", "amount_is_estimate": "接收金额为预估值", @@ -1474,5 +1475,6 @@ "zcash_card_enable_later": "您随时可以在设置中启用此卡", "zcash_card_missing_funds": "资金不见了?", "zcash_card_scan": "扫描", - "zcash_card_warning": "在该过程完成之前请勿关闭应用程序,否则该过程将需要从头重新开始。" + "zcash_card_warning": "在该过程完成之前请勿关闭应用程序,否则该过程将需要从头重新开始。", + "recipient_account_creation_fee": "需要额外的 ${amount} 来为收件人创建一个令牌帐户,因为他们还没有令牌帐户。" } \ No newline at end of file diff --git a/tool/configure.dart b/tool/configure.dart index 93cb2d598..b0a884969 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -922,6 +922,7 @@ import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/spl_token.dart'; import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/utils/print_verbose.dart'; """; const solanaCWHeaders = """