fix: solana wallet bugs and security issues (#3484)
What changed, and why it matters
This commit fixes several Solana wallet bugs and security issues in Cake Wallet. The main security-relevant changes are: (1) preventing users from accidentally sending the wrong token when two tokens share the same symbol, by matching on the unique mint address instead of the symbol; (2) using the correct token decimals from the blockchain rather than defaulting to zero, so amounts are parsed and displayed accurately; (3) adding a warning and proper accounting for the extra SOL cost when a recipient's token account must be created; and (4) improving signature verification and transaction-history parsing. The commit title and message explicitly call these 'security issues' and mention an audit list.
Review the Solana send/swap flow to confirm the duplicate-symbol disambiguation and additional-cost UI behave correctly in production. Consider whether the new AmbiguousTokenSymbolException needs handling in any other call sites. Verify that the on-chain decimal fallback does not introduce RPC latency or failure modes for tokens with missing metadata.
Security signals we found
Commit title/message explicitly labels changes as 'security issues' and 'security audit list'
Disambiguation of duplicate token symbols by mint address prevents wrong-token sends
Use of on-chain mint decimals prevents amount miscalculation/parsing errors
Additional cost handling for recipient token account creation prevents hidden rent shortfalls
verifyMessage rewritten to derive public key from expected address and verify base58 signature
Token-2022 program support added to transaction parsing
Transaction history JSON parsing now catches per-entry errors instead of failing wholesale
Evidence from the diff
The patch addresses Solana-specific security and correctness issues. Key changes include: resolving transaction currencies by mint address (SPLToken.mintAddress) instead of title/tag to disambiguate duplicate symbols, throwing SolanaAmbiguousTokenSymbolException when ambiguity remains; using on-chain mint decimals (via getTokenSupply fallback) rather than defaulting SPLToken.decimal to 0, and using Money/parseFixed with the mint decimals when parsing swap and transaction amounts; surfacing an additionalCost field on PendingTransaction for recipient ATA rent-exemption and showing it in the send confirmation UI; switching recipient ATA creation to an idempotent instruction bundled in the transfer transaction instead of a separate pre-create transaction; fixing verifyMessage to decode base58 signatures and derive the public key from the supplied address; adding Token-2022 program support in transaction parsing; and hardening transaction-history deserialization against corrupt entries. Unit tests cover currency resolution and SPL token amount parsing.
Changed components
cw_solana/lib/solana_wallet.dartcw_solana/lib/solana_client.dartcw_solana/lib/pending_solana_transaction.dartcw_core/lib/spl_token.dartcw_core/lib/pending_transaction.dartlib/view_model/send/send_view_model.dartlib/new-ui/widgets/send_page/send_confirm_sheet.dartcw_solana/lib/solana_transaction_history.dartcw_solana/lib/solana_wallet_service.dartInspect captured patch +685 / −371
diff --git a/cw_core/lib/exceptions.dart b/cw_core/lib/exceptions.dart
index cc0729e..76d4782 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 3d8912f..fb40319 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 5ac9330..140b50b 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<String, dynamic> 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<List<SPLToken>> getAllForWallet(String walletName) async => selectList("walletName = ?", [walletName]);
+ static Future<List<SPLToken>> getAllForWallet(String walletName) async =>
+ selectList("walletName = ?", [walletName]);
static Future<SPLToken?> 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<int> 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<int> deleteAllForWallet(String walletName) => db!.delete(tableName, where: "walletName = ?", whereArgs: [walletName]);
+ static Future<int> deleteAllForWallet(String walletName) =>
+ db!.delete(tableName, where: "walletName = ?", whereArgs: [walletName]);
static Future<void> 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 c102618..b9e9052 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 0000000..d686b8c
--- /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 c9dd6fa..48ca274 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 2f4f614..4594633 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<void> commit() async {
diff --git a/cw_solana/lib/solana_client.dart b/cw_solana/lib/solana_client.dart
index 015cc01..7e21817 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<List<SolanaTransactionModel>> _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 = <String>{};
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 = <SolanaTransactionModel>[];
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<String, SPLToken?> tokenInfoCache = {};
+ final Map<String, SPLToken> tokenInfoCache = {};
Future<SPLToken?> getTokenInfo(String mintAddress) async {
- if (tokenInfoCache.containsKey(mintAddress)) return tokenInfoCache[mintAddress];
+ final cached = tokenInfoCache[mintAddress];
+ if (cached != null) {
+ return cached;
+ }
+
+ final fetched = await fetchSPLTokenInfo(mintAddress);
+ if (fetched != null) {
+ tokenInfoCache[mintAddress] = fetched;
+ }
- return tokenInfoCache[mintAddress] = await fetchSPLTokenInfo(mintAddress);
+ return fetched;
+ }
+
+ Future<int?> _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<SPLToken?> 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<Message> _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<Money> _getFeeFromCompiledMessage(Message message, Commitment commitment) {
final base64Message = base64Encode(message.serialize());
return getFeeForMessage(base64Message, commitment);
}
+ Future<Money> _getRentExemptionAmount(int space) async {
+ final rent = await _provider!.request(
+ SolanaRPCGetMinimumBalanceForRentExemption(size: space),
+ );
+
+ return Money(rent, CryptoCurrency.sol);
+ }
+
Future<bool> hasSufficientFundsLeftForRent({
- required Money inputAmount,
+ required Money totalOutflow,
required Money solBalance,
- required Money fee,
}) async {
- final rent = await _provider!.request(
+ 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<PendingSolanaTransaction> _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<ProgramDerivedAddress?> _getOrCreateAssociatedTokenAccount({
- required SolanaPrivateKey payerPrivateKey,
+ Future<ProgramDerivedAddress?> _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,
- );
-
- 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,
- );
+ // 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,
+ );
- // Wait for confirmation
- await Future.delayed(const Duration(seconds: 2));
+ try {
+ accountInfo = await _provider!.request(
+ SolanaRPCGetAccountInfo(
+ account: associatedTokenAccount.address,
+ commitment: Commitment.confirmed,
+ ),
+ );
+ if (accountInfo != null) return associatedTokenAccount;
+ } catch (_) {}
+ }
+ } catch (_) {}
- return associatedTokenAccount;
+ return null;
}
Future<PendingSolanaTransaction> _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,
- );
-
- 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,
+ if (recipientInfo != null && recipientInfo.owner.address != senderTokenProgramId.address) {
+ throw SolanaCreateAssociatedTokenAccountException(
+ "Recipient token account is owned by ${recipientInfo.owner.address}",
);
-
- 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 = <TransactionInstruction>[
+ 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 96ba0bb..a3873a3 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 00fcbec..bb96ab8 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<Solan
txs.entries.forEach((entry) {
final val = entry.value;
- if (val is Map<String, dynamic>) {
- final tx = SolanaTransactionInfo.fromJson(val);
- _update(tx);
+ if (val is! Map<String, dynamic>) {
+ 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 b41ee42..55ad9f9 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<CryptoCurrency> 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<Map<String, SolanaTransactionInfo>> fetchTransactions() async => {};
@@ -448,6 +469,8 @@ abstract class SolanaWalletBase
}
}
+ static final _swapIdSuffixPattern = RegExp(r"_(outgoing|incoming)$");
+
void _addTransactions(List<SolanaTransactionModel> transactions) {
final Map<String, SolanaTransactionInfo> 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<SolanaBalance> _fetchSOLBalance() async {
- final balance = await _client.getBalance(solanaAddress);
-
- return SolanaBalance(balance);
+ Future<SolanaBalance?> _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<void> _removeTokenTransactionsInHistory(SPLToken token) async {
@@ -932,47 +967,25 @@ abstract class SolanaWalletBase
return Base58Encoder.encode(signature);
}
- List<List<int>> 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<int> pubKeyBytes = SolAddrDecoder().decodeAddr(base58EncodedPublicKeyString);
-
- return [sigBytes, pubKeyBytes];
- } else {
- throw const FormatException('Invalid Signature string format');
- }
- }
-
@override
Future<bool> verifyMessage(String message, String signature, {String? address}) async {
- String signatureString = utf8.decode(HEX.decode(signature));
-
- List<List<int>> 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 a557507..85eae60 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<void> 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 0000000..ef97ec3
--- /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<Exception>()),
+ );
+ });
+
+ 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<SolanaAmbiguousTokenSymbolException>()),
+ );
+ });
+
+ 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 d10233a..8e0a470 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 a8ea814..2f1fc90 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 f26b079..426957a 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 5b34686..35f7102 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 7765673..46edfca 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 73966ab..87bbc39 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<ExchangeTradeForm> {
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 df85d0c..9e3fa7f 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 5d3eb9f..24f5015 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 d14612f..b443d1a 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 c0c0815..f62daa6 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 9b95310..dd7abc1 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 7b65547..9c04a21 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 1e29a5c..78321bd 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 f40a8e1..cf886a4 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 0ced5ec..d3f135c 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 679d732..551e16d 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 1df9ce6..ea901d7 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 0aa4600..15edf00 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 4f52711..2d99234 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 a593037..f1a209c 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 16fcb48..c1fea5b 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 a7f73b6..d7ae0b9 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 8acfdaf..e9708e4 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 2777da0..35dd2e7 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 e8e9eac..990a568 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 d9d677a..0671d7e 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 8650bd0..e0de2a8 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 2a392da..d9204e3 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 51e8169..d703a3e 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 1bdeee2..aeb56f3 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 b8e98d1..44096cb 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 86a0d45..bdb4d46 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 a2f6a18..67684f6 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 93ae72c..14fa38f 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 80e93d2..ba77da2 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 68fd9af..41b269d 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 6ca70f8..15f858b 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 8e3256b..7117350 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 93cb2d5..b0a8849 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 = """
Why this scored 59/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.