fix: solana wallet bugs and security issues (#3484)

* fix android CI * fix: duplicate outgoing tx for jup swaps and stuck pending state * fix solana security risk by handling duplicate token symbols in wallet transactions * feat: implement additional cost handling for pending transactions in Solana wallet. * fix: Items on security audit list for solana wallet * refactor: streamline fee payer index handling and improve error logging * fix: verifySignature for solana and handle wrong mint on default token * fix: token decimals defaulting to zero and breaking amount parsing in solana * fix: use token mask in tx history * refactor: apply lint to modified code * fix: merge conflicts * fix: use Money and mint decimals when parsing sol swaps * test: add unit tests for SPL token amount handling and parsing * test: add more tests * fix: update decimal handling for fetched token and remove unused fields

David Adegoke committed Aug 24, 2026 at 21:21 UTC 703cc22b33c400ab953e1a679e5f7c41148d2b08
52 files changed +685 -371
cw_core/lib/exceptions.dart
+6
@@ -55,6 +55,12 @@ class SignSPLTokenTransactionRentException implements Exception {}
55
56 class NoAssociatedTokenAccountException implements Exception {}
57
58 +class AmbiguousTokenSymbolException implements Exception {
59 + AmbiguousTokenSymbolException(this.symbol);
60 +
61 + final String symbol;
62 +}
63 +
64 class RestoreFromSeedException implements Exception {
65 final String message;
66
cw_core/lib/pending_transaction.dart
+1
@@ -12,6 +12,7 @@ mixin PendingTransaction {
12
13 Money get amount;
14 Money get fee;
15 + Money? get additionalCost => null;
16
17 String get amountFormatted;
18 String get feeFormatted => fee.toStringWithSymbol(fractionalDigits: 8);
cw_core/lib/spl_token.dart
+31 -28
@@ -3,7 +3,6 @@ import "package:cw_core/db/sqlite.dart";
3 import "package:sqflite/sqflite.dart";
4
5 class SPLToken extends CryptoCurrency {
6 -
6 SPLToken({
7 required this.name,
8 required this.symbol,
@@ -33,17 +32,19 @@ class SPLToken extends CryptoCurrency {
32 required String mint,
33 required String symbol,
34 required String mintAddress,
35 + required int decimal,
36 String? iconPath,
37 bool isPotentialScam = false,
38 - }) => SPLToken(
39 - name: name,
40 - symbol: symbol,
41 - mintAddress: mintAddress,
42 - decimal: 0,
43 - mint: mint,
44 - iconPath: iconPath,
45 - isPotentialScam: isPotentialScam,
46 - );
38 + }) =>
39 + SPLToken(
40 + name: name,
41 + symbol: symbol,
42 + mintAddress: mintAddress,
43 + decimal: decimal,
44 + mint: mint,
45 + iconPath: iconPath,
46 + isPotentialScam: isPotentialScam,
47 + );
48
49 SPLToken.copyWith(SPLToken other, {String? icon, String? tag, bool? enabled, String? walletName})
50 : name = other.name,
@@ -125,18 +126,18 @@ class SPLToken extends CryptoCurrency {
126 }
127
128 Map<String, dynamic> toMap() => {
128 - selfIdColumn: id,
129 - "walletName": walletName,
130 - "name": name,
131 - "symbol": symbol,
132 - "mintAddress": mintAddress,
133 - "decimal": decimal,
134 - "mint": mint,
135 - "enabled": _enabled ? 1 : 0,
136 - "iconPath": iconPath,
137 - "tag": tag,
138 - "isPotentialScam": isPotentialScam ? 1 : 0,
139 - };
129 + selfIdColumn: id,
130 + "walletName": walletName,
131 + "name": name,
132 + "symbol": symbol,
133 + "mintAddress": mintAddress,
134 + "decimal": decimal,
135 + "mint": mint,
136 + "enabled": _enabled ? 1 : 0,
137 + "iconPath": iconPath,
138 + "tag": tag,
139 + "isPotentialScam": isPotentialScam ? 1 : 0,
140 + };
141
142 static String get tableName => "SPLToken";
143 static String get selfIdColumn => "${tableName}Id";
@@ -166,7 +167,8 @@ class SPLToken extends CryptoCurrency {
167 return List.generate(list.length, (index) => SPLToken.fromMap(list[index]));
168 }
169
169 - static Future<List<SPLToken>> getAllForWallet(String walletName) async => selectList("walletName = ?", [walletName]);
170 + static Future<List<SPLToken>> getAllForWallet(String walletName) async =>
171 + selectList("walletName = ?", [walletName]);
172
173 static Future<SPLToken?> getByMint(String walletName, String mintAddress) async {
174 final list = await selectList("walletName = ? AND mintAddress = ?", [walletName, mintAddress]);
@@ -174,12 +176,13 @@ class SPLToken extends CryptoCurrency {
176 }
177
178 static Future<int> deleteForWallet(String walletName, String mintAddress) => db!.delete(
177 - tableName,
178 - where: "walletName = ? AND mintAddress = ?",
179 - whereArgs: [walletName, mintAddress],
180 - );
179 + tableName,
180 + where: "walletName = ? AND mintAddress = ?",
181 + whereArgs: [walletName, mintAddress],
182 + );
183
182 - static Future<int> deleteAllForWallet(String walletName) => db!.delete(tableName, where: "walletName = ?", whereArgs: [walletName]);
184 + static Future<int> deleteAllForWallet(String walletName) =>
185 + db!.delete(tableName, where: "walletName = ?", whereArgs: [walletName]);
186
187 static Future<void> renameWallet(String oldName, String newName) async {
188 await db!.delete(tableName, where: "walletName = ?", whereArgs: [newName]);
cw_core/test/parse_fixed_test.dart
+13
@@ -45,6 +45,19 @@ void main() {
45 test('should fail to parse `.`, missing value',
46 () => expect(() => parseFixed(".", 6), throwsFormatException));
47 });
48 +
49 + group("parseFixed, zero decimal currency", () {
50 + test("should parse 5 as 5", () => expect(parseFixed("5", 0), BigInt.from(5)));
51 +
52 + test("should parse 0 as 0", () => expect(parseFixed("0", 0), BigInt.from(0)));
53 +
54 + test("should parse 1. as 1", () => expect(parseFixed("1.", 0), BigInt.from(1)));
55 +
56 + test("should parse -3 as -3", () => expect(parseFixed("-3", 0), BigInt.from(-3)));
57 +
58 + test("should fail to parse 5.5, fractional component exceeds decimals",
59 + () => expect(() => parseFixed("5.5", 0), throwsFormatException));
60 + });
61 });
62
63 group('tryParseFixed', () {
cw_core/test/spl_token_amount_test.dart new
+104
@@ -0,0 +1,104 @@
1 +import "package:cw_core/amount/money.dart";
2 +import "package:cw_core/spl_token.dart";
3 +import "package:flutter_test/flutter_test.dart";
4 +
5 +void main() {
6 + group("SPL token raw amounts", () {
7 + test("6 decimal token renders raw base units", () {
8 + final jup = SPLToken(
9 + name: "Jupiter",
10 + symbol: "JUP",
11 + mint: "jup",
12 + mintAddress: "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN",
13 + decimal: 6,
14 + );
15 +
16 + expect(Money(BigInt.from(100000), jup).toStringWithPrecision(), "0.1");
17 + expect(Money(BigInt.from(931614), jup).toStringWithPrecision(), "0.931614");
18 + });
19 +
20 + test("8 decimal token renders raw base units", () {
21 + final xstock = SPLToken(
22 + name: "Amazon xStock",
23 + symbol: "AMZNX",
24 + mint: "amznx",
25 + mintAddress: "mintAddressWithEightDecimals",
26 + decimal: 8,
27 + );
28 +
29 + expect(Money(BigInt.from(341694), xstock).toStringWithPrecision(), "0.00341694");
30 + });
31 +
32 + test("the same raw amount means different values per decimals", () {
33 + final raw = BigInt.from(100000);
34 +
35 + final six = SPLToken(
36 + name: "Six",
37 + symbol: "SIX",
38 + mint: "six",
39 + mintAddress: "mintAddressWithSixDecimals",
40 + decimal: 6,
41 + );
42 +
43 + final nine = SPLToken(
44 + name: "Nine",
45 + symbol: "NINE",
46 + mint: "nine",
47 + mintAddress: "mintAddressWithNineDecimals",
48 + decimal: 9,
49 + );
50 +
51 + final zero = SPLToken(
52 + name: "Zero",
53 + symbol: "ZERO",
54 + mint: "zero",
55 + mintAddress: "mintAddressWithZeroDecimals",
56 + decimal: 0,
57 + );
58 +
59 + expect(Money(raw, six).toStringWithPrecision(), "0.1");
60 + expect(Money(raw, nine).toStringWithPrecision(), "0.0001");
61 + expect(Money(raw, zero).toStringWithPrecision(), "100000");
62 + });
63 + });
64 +
65 + group("SPL token amount parsing", () {
66 + test("parses whole amounts for a zero decimal token", () {
67 + final zero = SPLToken(
68 + name: "Zero",
69 + symbol: "ZERO",
70 + mint: "zero",
71 + mintAddress: "mintAddressWithZeroDecimals",
72 + decimal: 0,
73 + );
74 +
75 + expect(Money.parse("5", zero).amount, BigInt.from(5));
76 + expect(Money.parse("0", zero).amount, BigInt.zero);
77 + });
78 +
79 + test("parses fractional amounts for a 6 decimal token", () {
80 + final jup = SPLToken(
81 + name: "Jupiter",
82 + symbol: "JUP",
83 + mint: "jup",
84 + mintAddress: "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN",
85 + decimal: 6,
86 + );
87 +
88 + expect(Money.parse("0.1", jup).amount, BigInt.from(100000));
89 + expect(Money.parse("10.339089", jup).amount, BigInt.from(10339089));
90 + });
91 +
92 + test("rejects more fractional digits than the token has decimals", () {
93 + final zero = SPLToken(
94 + name: "Zero",
95 + symbol: "ZERO",
96 + mint: "zero",
97 + mintAddress: "mintAddressWithZeroDecimals",
98 + decimal: 0,
99 + );
100 +
101 + expect(() => Money.parse("5.5", zero), throwsFormatException);
102 + });
103 + });
104 +}
cw_solana/lib/default_spl_tokens.dart
+1 -1
@@ -87,7 +87,7 @@ class DefaultSPLTokens {
87 symbol: 'GMT',
88 mintAddress: '7i5KKsX2weiTkry7jA4ZwSuXGhs5eJBEjY8vVxR4pfRx',
89 decimal: 9,
90 - mint: 'ray',
90 + mint: 'gmt',
91 iconPath: 'assets/images/gmt_icon.png',
92 enabled: false,
93 ),
cw_solana/lib/pending_solana_transaction.dart
+4 -8
@@ -13,6 +13,7 @@ class PendingSolanaTransaction with PendingTransaction {
13 required this.serializedTransaction,
14 required this.destinationAddress,
15 required this.sendTransaction,
16 + this.additionalCost,
17 });
18
19 @override
@@ -22,15 +23,10 @@ class PendingSolanaTransaction with PendingTransaction {
23 final Money fee;
24
25 @override
25 - String get amountFormatted {
26 - String stringifiedAmount = amount.toString();
26 + final Money? additionalCost;
27
28 - if (stringifiedAmount.toString().length >= 6) {
29 - stringifiedAmount = stringifiedAmount.substring(0, 6);
30 - }
31 -
32 - return stringifiedAmount;
33 - }
28 + @override
29 + String get amountFormatted => amount.toString();
30
31 @override
32 Future<void> commit() async {
cw_solana/lib/solana_client.dart
+203 -252
@@ -3,9 +3,7 @@ import 'dart:convert';
3
4 import 'package:blockchain_utils/blockchain_utils.dart';
5 import 'package:cw_core/amount/money.dart';
6 -import 'package:cw_core/amount/money_double.dart';
6 import 'package:cw_core/crypto_currency.dart';
8 -import 'package:cw_core/currency.dart';
7 import 'package:cw_core/node.dart';
8 import 'package:cw_core/utils/proxy_wrapper.dart';
9 import 'package:cw_core/solana_rpc_http_service.dart';
@@ -215,17 +213,10 @@ class SolanaWalletClient {
213 // For native solana transactions
214 if (instruction.accounts.length < 2) continue;
215
218 - // Get the fee payer index based on transaction type
219 - // For legacy transfers, the first account is usually the fee payer
220 - // For versioned, the first account in instruction is usually the fee payer
221 - final feePayerIndex =
222 - txResponse.version == TransactionType.legacy ? 0 : instruction.accounts[0];
223 -
216 final transactionModel = await _parseNativeTransaction(
217 message: message,
218 meta: meta,
219 fee: fee,
228 - feePayerIndex: feePayerIndex,
220 walletAddress: walletAddress,
221 signature: signature,
222 blockTime: blockTime,
@@ -234,8 +225,8 @@ class SolanaWalletClient {
225 if (transactionModel != null) {
226 return [transactionModel];
227 }
237 - } else if (programId == SPLTokenProgramConst.tokenProgramId) {
238 - // For SPL Token transactions
228 + } else if (programId == SPLTokenProgramConst.tokenProgramId ||
229 + programId == SPLTokenProgramConst.token2022ProgramId) {
230 if (instruction.accounts.length < 2) continue;
231
232 final transactionModel = await _parseSPLTokenTransaction(
@@ -265,7 +256,8 @@ class SolanaWalletClient {
256 bool hasTokenTransfer = false;
257 for (final otherInstruction in instructions) {
258 final otherProgramId = message.accountKeys[otherInstruction.programIdIndex];
268 - if (otherProgramId == SPLTokenProgramConst.tokenProgramId) {
259 + if (otherProgramId == SPLTokenProgramConst.tokenProgramId ||
260 + otherProgramId == SPLTokenProgramConst.token2022ProgramId) {
261 hasTokenTransfer = true;
262 break;
263 }
@@ -389,6 +381,18 @@ class SolanaWalletClient {
381 return walletSent && walletReceived;
382 }
383
384 + static CryptoCurrency currencyForRawAmount(SPLToken? token, int mintDecimals) {
385 + if (token != null && token.decimals == mintDecimals) {
386 + return token;
387 + }
388 +
389 + return CryptoCurrency(
390 + name: (token?.title ?? "TOKEN").toLowerCase(),
391 + title: token?.title ?? "TOKEN",
392 + decimals: mintDecimals,
393 + );
394 + }
395 +
396 /// Parses a swap transaction and creates dual entries (outgoing and incoming)
397 Future<List<SolanaTransactionModel>> _parseSwapTransaction({
398 required VersionedMessage message,
@@ -407,6 +411,9 @@ class SolanaWalletClient {
411 final preTokenBalances = meta.preTokenBalances;
412 final postTokenBalances = meta.postTokenBalances;
413
414 + final walletPaidFee = accountKeys.isNotEmpty && accountKeys.first.address == walletAddress;
415 + final feeAdjustment = walletPaidFee ? BigInt.from(fee) : BigInt.zero;
416 +
417 String? decreasedMintForWallet;
418 String? increasedMintForWallet;
419
@@ -442,8 +449,7 @@ class SolanaWalletClient {
449 decreasedMintForWallet != increasedMintForWallet;
450
451 // Parse outgoing side (what was sent)
445 - double outgoingAmount = 0.0;
446 - Currency outgoingToken = CryptoCurrency.sol;
452 + Money? outgoingMoney;
453 String? outgoingMintAddress;
454 String? outgoingFrom;
455 String? outgoingTo;
@@ -461,12 +467,12 @@ class SolanaWalletClient {
467 if (accountAddress == walletAddress) {
468 final preBalance = preBalances[i];
469 final postBalance = postBalances[i];
464 - final balanceChange = preBalance - postBalance;
470 +
471 + final balanceChange = preBalance - postBalance - feeAdjustment;
472
473 if (balanceChange > BigInt.zero) {
474 // The wallet sent SOL
468 - outgoingAmount = balanceChange.toDouble() / SolanaUtils.lamportsPerSol;
469 - outgoingToken = CryptoCurrency.sol;
475 + outgoingMoney = Money(balanceChange, CryptoCurrency.sol);
476 outgoingMintAddress = null;
477 outgoingFrom = walletAddress;
478 // We find the intermediate account or swap program account
@@ -484,7 +490,7 @@ class SolanaWalletClient {
490 }
491
492 // If no SOL outgoing, we check if there are any SPL token balance changes for the wallet
487 - if (outgoingAmount == 0.0 && preTokenBalances != null) {
493 + if (outgoingMoney == null && preTokenBalances != null && postTokenBalances != null) {
494 for (final preTokenBal in preTokenBalances) {
495 final owner = preTokenBal.owner?.address ?? '';
496
@@ -494,24 +500,23 @@ class SolanaWalletClient {
500 if (isSplToSplSwap && mint != decreasedMintForWallet) {
501 continue;
502 }
497 - final preAmount = preTokenBal.uiTokenAmount.uiAmount ?? 0.0;
503 + final preRaw = BigInt.tryParse(preTokenBal.uiTokenAmount.amount) ?? BigInt.zero;
504
505 // We find the corresponding post balance
500 - for (final postTokenBal in postTokenBalances ?? []) {
506 + for (final postTokenBal in postTokenBalances) {
507 final postOwner = postTokenBal.owner?.address ?? '';
508 final postMint = postTokenBal.mint.address;
503 - final postAmount = postTokenBal.uiTokenAmount.uiAmount ?? 0.0;
509
510 if (postOwner == walletAddress && postMint == mint) {
506 - final diff = preAmount - postAmount;
511 + final postRaw = BigInt.tryParse(postTokenBal.uiTokenAmount.amount) ?? BigInt.zero;
512 + final diff = preRaw - postRaw;
513
508 - if (diff > 0) {
514 + if (diff > BigInt.zero) {
515 // The wallet sent tokens
510 - outgoingAmount = diff.toDouble();
511 - outgoingMintAddress = mint;
516 final token = await getTokenInfo(mint);
513 - outgoingToken =
514 - token ?? const CryptoCurrency(name: "TOKEN", title: "TOKEN", decimals: 6);
517 + outgoingMoney =
518 + Money(diff, currencyForRawAmount(token, preTokenBal.uiTokenAmount.decimals));
519 + outgoingMintAddress = mint;
520 outgoingFrom = walletAddress;
521 // We find the intermediate account
522 if (instructions.isNotEmpty && instructions[0].accounts.isNotEmpty) {
@@ -526,14 +531,15 @@ class SolanaWalletClient {
531 }
532 }
533
529 - if (outgoingAmount > 0) break;
534 + if (outgoingMoney != null) {
535 + break;
536 + }
537 }
538 }
539 }
540
541 // Parse incoming side (what was received)
535 - double incomingAmount = 0.0;
536 - Currency incomingToken = CryptoCurrency.sol;
542 + Money? incomingMoney;
543 String? incomingMintAddress;
544 String? incomingFrom;
545 String? incomingTo;
@@ -550,12 +556,11 @@ class SolanaWalletClient {
556 if (accountAddress == walletAddress) {
557 final preBalance = preBalances[i];
558 final postBalance = postBalances[i];
553 - final balanceChange = postBalance - preBalance;
559 + final balanceChange = postBalance - preBalance + feeAdjustment;
560
561 if (balanceChange > BigInt.zero) {
562 // The wallet received SOL
557 - incomingAmount = balanceChange.toDouble() / SolanaUtils.lamportsPerSol;
558 - incomingToken = CryptoCurrency.sol;
563 + incomingMoney = Money(balanceChange, CryptoCurrency.sol);
564 incomingMintAddress = null;
565 incomingTo = walletAddress;
566 // We find the intermediate account
@@ -573,7 +578,7 @@ class SolanaWalletClient {
578 }
579
580 // If no SOL incoming, check SPL token incoming using ATA derivation
576 - if (incomingAmount == 0.0 && preTokenBalances != null && postTokenBalances != null) {
581 + if (incomingMoney == null && preTokenBalances != null && postTokenBalances != null) {
582 // Collect all unique mints from token balances (excluding wrapped SOL)
583 final mints = <String>{};
584 for (final tokenBal in preTokenBalances) {
@@ -595,17 +600,22 @@ class SolanaWalletClient {
600 final walletSolAddress = SolAddress(walletAddress);
601 final mintSolAddress = SolAddress(mint);
602
598 - final ata = AssociatedTokenAccountProgramUtils.associatedTokenAccount(
603 + final standardAta = AssociatedTokenAccountProgramUtils.associatedTokenAccount(
604 mint: mintSolAddress,
605 owner: walletSolAddress,
606 );
602 - final ataAddress = ata.address.address;
607 + final token2022Ata = AssociatedTokenAccountProgramUtils.associatedTokenAccount(
608 + mint: mintSolAddress,
609 + owner: walletSolAddress,
610 + tokenProgramId: SPLTokenProgramConst.token2022ProgramId,
611 + );
612 + final ataAddresses = [standardAta.address.address, token2022Ata.address.address];
613
604 - // We check if this ATA address appears in the account keys
614 + // We check if either ATA address appears in the account keys
615 int? ataAccountIndex;
616 for (int i = 0; i < accountKeys.length; i++) {
617 final accountKey = accountKeys[i];
608 - if (accountKey.address == ataAddress) {
618 + if (ataAddresses.contains(accountKey.address)) {
619 ataAccountIndex = i;
620 break;
621 }
@@ -613,15 +623,17 @@ class SolanaWalletClient {
623
624 // If ATA is in the transaction, we check for balance changes
625 if (ataAccountIndex != null) {
616 - double preAmount = 0.0;
617 - double postAmount = 0.0;
626 + BigInt preRaw = BigInt.zero;
627 + BigInt postRaw = BigInt.zero;
628 + int? mintDecimals;
629
630 // We find the pre balance
631 for (final preTokenBal in preTokenBalances) {
632 final accountIndex = preTokenBal.accountIndex;
633 final tokenMint = preTokenBal.mint.address;
634 if (accountIndex == ataAccountIndex && tokenMint == mint) {
624 - preAmount = preTokenBal.uiTokenAmount.uiAmount?.toDouble() ?? 0.0;
635 + preRaw = BigInt.tryParse(preTokenBal.uiTokenAmount.amount) ?? BigInt.zero;
636 + mintDecimals = preTokenBal.uiTokenAmount.decimals;
637 break;
638 }
639 }
@@ -631,18 +643,18 @@ class SolanaWalletClient {
643 final accountIndex = postTokenBal.accountIndex;
644 final tokenMint = postTokenBal.mint.address;
645 if (accountIndex == ataAccountIndex && tokenMint == mint) {
634 - postAmount = postTokenBal.uiTokenAmount.uiAmount?.toDouble() ?? 0.0;
646 + postRaw = BigInt.tryParse(postTokenBal.uiTokenAmount.amount) ?? BigInt.zero;
647 + mintDecimals = postTokenBal.uiTokenAmount.decimals;
648 break;
649 }
650 }
651
639 - final diff = postAmount - preAmount;
640 - if (diff > 0) {
652 + final diff = postRaw - preRaw;
653 + if (diff > BigInt.zero && mintDecimals != null) {
654 // The wallet received tokens
642 - incomingAmount = diff.toDouble();
643 - incomingMintAddress = mint;
655 final token = await getTokenInfo(mint);
645 - incomingToken = token ?? const CryptoCurrency(name: "TOKEN", title: "TOKEN", decimals: 6);
656 + incomingMoney = Money(diff, currencyForRawAmount(token, mintDecimals));
657 + incomingMintAddress = mint;
658 incomingTo = walletAddress;
659 // We find the intermediate account
660 if (instructions.isNotEmpty && instructions[0].accounts.isNotEmpty) {
@@ -663,7 +675,7 @@ class SolanaWalletClient {
675 }
676
677 // Outgoing transaction model
666 - if (outgoingAmount > 0.0 && outgoingFrom != null && outgoingTo != null) {
678 + if (outgoingMoney != null && outgoingFrom != null && outgoingTo != null) {
679 final outgoingId =
680 '${signature}_outgoing'; // We create a composite ID for the outgoing transaction
681 swapTransactions.add(SolanaTransactionModel(
@@ -671,7 +683,7 @@ class SolanaWalletClient {
683 from: outgoingFrom,
684 to: outgoingTo,
685 id: outgoingId,
674 - amount: outgoingAmount.toMoney(outgoingToken),
686 + amount: outgoingMoney,
687 programId: outgoingMintAddress == null
688 ? SystemProgramConst.programId.address
689 : SPLTokenProgramConst.tokenProgramId.address,
@@ -681,7 +693,7 @@ class SolanaWalletClient {
693 }
694
695 // Incoming transaction model
684 - if (incomingAmount > 0.0 && incomingFrom != null && incomingTo != null) {
696 + if (incomingMoney != null && incomingFrom != null && incomingTo != null) {
697 final incomingId =
698 '${signature}_incoming'; // We create a composite ID for the incoming transaction
699 swapTransactions.add(SolanaTransactionModel(
@@ -689,7 +701,7 @@ class SolanaWalletClient {
701 from: incomingFrom,
702 to: incomingTo,
703 id: incomingId,
692 - amount: incomingAmount.toMoney(incomingToken),
704 + amount: incomingMoney,
705 programId: incomingMintAddress == null
706 ? SystemProgramConst.programId.address
707 : SPLTokenProgramConst.tokenProgramId.address,
@@ -705,7 +717,6 @@ class SolanaWalletClient {
717 required VersionedMessage message,
718 required ConfirmedTransactionMeta meta,
719 required int fee,
708 - required int feePayerIndex,
720 required String walletAddress,
721 required String signature,
722 required BigInt? blockTime,
@@ -736,8 +747,7 @@ class SolanaWalletClient {
747 // Positive = wallet lost SOL, negative = wallet gained.
748 final walletChange = walletPre - walletPost;
749
739 - final bool walletPaidFee =
740 - feePayerIndex < accountKeys.length && accountKeys[feePayerIndex].address == walletAddress;
750 + final walletPaidFee = accountKeys.first.address == walletAddress;
751
752 // Net transfer amount excluding the fee.
753 final netChange = walletPaidFee ? walletChange - BigInt.from(fee) : walletChange;
@@ -808,8 +818,9 @@ class SolanaWalletClient {
818 mintAddress = accountKeys[accounts[1]].address;
819 }
820
811 - double userPreAmount = 0.0;
812 - double userPostAmount = 0.0;
821 + BigInt userPreRaw = BigInt.zero;
822 + BigInt userPostRaw = BigInt.zero;
823 + int? mintDecimals;
824
825 if (preTokenBalances != null) {
826 for (final preBal in preTokenBalances) {
@@ -820,7 +831,8 @@ class SolanaWalletClient {
831 continue;
832 }
833 mintAddress ??= preBal.mint.address;
823 - userPreAmount = preBal.uiTokenAmount.uiAmount ?? 0.0;
834 + mintDecimals = preBal.uiTokenAmount.decimals;
835 + userPreRaw = BigInt.tryParse(preBal.uiTokenAmount.amount) ?? BigInt.zero;
836 break;
837 }
838 }
@@ -836,19 +848,21 @@ class SolanaWalletClient {
848 continue;
849 }
850 mintAddress ??= postBal.mint.address;
839 - userPostAmount = postBal.uiTokenAmount.uiAmount ?? 0.0;
851 + mintDecimals = postBal.uiTokenAmount.decimals;
852 + userPostRaw = BigInt.tryParse(postBal.uiTokenAmount.amount) ?? BigInt.zero;
853 break;
854 }
855 }
856 }
857 }
858
846 - final diff = userPreAmount - userPostAmount;
847 - final rawAmount = diff.abs();
859 + final diff = userPreRaw - userPostRaw;
860
849 - final amountInString = rawAmount.toStringAsFixed(6);
850 - final amount = double.parse(amountInString);
851 - final isOutgoing = diff > 0;
861 + if (diff == BigInt.zero || mintDecimals == null) {
862 + return null;
863 + }
864 +
865 + final isOutgoing = diff > BigInt.zero;
866
867 // Resolve sender/receiver from token balance owners
868 String? senderOwner;
@@ -886,7 +900,7 @@ class SolanaWalletClient {
900 from: sender,
901 to: receiver,
902 id: signature,
889 - amount: amount.toMoney(splToken ?? CryptoCurrency.sol),
903 + amount: Money(diff.abs(), currencyForRawAmount(splToken, mintDecimals)),
904 programId: SPLTokenProgramConst.tokenProgramId.address,
905 blockTimeInInt: blockTime?.toInt() ?? 0,
906 fee: Money.fromInt(fee, CryptoCurrency.sol),
@@ -1013,8 +1027,7 @@ class SolanaWalletClient {
1027 final transactions = <SolanaTransactionModel>[];
1028
1029 try {
1016 - final signatures =
1017 - await _getAllSignaturesSinceLastFetch(address, untilSignature, commitment);
1030 + final signatures = await _getAllSignaturesSinceLastFetch(address, untilSignature, commitment);
1031
1032 if (signatures.isEmpty) return TransactionSyncResult(transactions: transactions);
1033
@@ -1096,11 +1109,9 @@ class SolanaWalletClient {
1109
1110 if (associatedTokenAccount == null) {
1111 try {
1099 - associatedTokenAccount = await _getOrCreateAssociatedTokenAccount(
1100 - payerPrivateKey: privateKey,
1112 + associatedTokenAccount = await _findAssociatedTokenAccount(
1113 mintAddress: SolAddress(mintAddress),
1114 ownerAddress: ownerWalletAddress,
1103 - shouldCreateATA: false,
1115 );
1116 } catch (e, s) {
1117 printV('$e \n $s');
@@ -1122,12 +1133,33 @@ class SolanaWalletClient {
1133 );
1134 }
1135
1125 - final Map<String, SPLToken?> tokenInfoCache = {};
1136 + final Map<String, SPLToken> tokenInfoCache = {};
1137
1138 Future<SPLToken?> getTokenInfo(String mintAddress) async {
1128 - if (tokenInfoCache.containsKey(mintAddress)) return tokenInfoCache[mintAddress];
1139 + final cached = tokenInfoCache[mintAddress];
1140 + if (cached != null) {
1141 + return cached;
1142 + }
1143 +
1144 + final fetched = await fetchSPLTokenInfo(mintAddress);
1145 + if (fetched != null) {
1146 + tokenInfoCache[mintAddress] = fetched;
1147 + }
1148
1130 - return tokenInfoCache[mintAddress] = await fetchSPLTokenInfo(mintAddress);
1149 + return fetched;
1150 + }
1151 +
1152 + Future<int?> _fetchMintDecimals(String mintAddress) async {
1153 + try {
1154 + final supply = await _provider!.request(
1155 + SolanaRPCGetTokenSupply(account: SolAddress(mintAddress)),
1156 + );
1157 +
1158 + return supply.decimals;
1159 + } catch (e) {
1160 + printV("Could not read decimals for mint $mintAddress: ${e.toString()}");
1161 + return null;
1162 + }
1163 }
1164
1165 Future<SPLToken?> fetchSPLTokenInfo(String mintAddress) async {
@@ -1150,18 +1182,29 @@ class SolanaWalletClient {
1182
1183 final symbol = decodedResponse['symbol'] ?? '';
1184 final name = decodedResponse['name'] ?? '';
1153 - final decimal = decodedResponse['decimals'] ?? '0';
1185 + final rawDecimals = decodedResponse["decimals"];
1186 final iconPath = decodedResponse['logo'] ?? '';
1187
1188 final filteredTokenSymbol = symbol.replaceFirst(RegExp('^\\\$'), '').replaceAll('\u0000', '');
1189
1190 + final reportedDecimals =
1191 + rawDecimals is num ? rawDecimals.toInt() : int.tryParse("${rawDecimals ?? ""}");
1192 +
1193 + final decimals = (reportedDecimals != null && reportedDecimals > 0)
1194 + ? reportedDecimals
1195 + : await _fetchMintDecimals(mintAddress);
1196 +
1197 + if (decimals == null) {
1198 + return null;
1199 + }
1200 +
1201 return SPLToken(
1202 name: name,
1203 mint: symbol,
1204 symbol: filteredTokenSymbol,
1205 mintAddress: mintAddress,
1206 iconPath: iconPath,
1164 - decimal: int.tryParse(decimal) ?? 0,
1207 + decimal: decimals,
1208 );
1209 } catch (e, s) {
1210 printV('Error fetching token info: $e \n $s');
@@ -1189,11 +1232,18 @@ class SolanaWalletClient {
1232 String filteredTokenSymbol =
1233 metadata.symbol.replaceFirst(RegExp('^\\\$'), '').replaceAll('\u0000', '');
1234
1235 + final decimals = await _fetchMintDecimals(token.mint.address);
1236 +
1237 + if (decimals == null) {
1238 + return null;
1239 + }
1240 +
1241 return SPLToken.fromMetadata(
1242 name: metadata.name,
1243 mint: metadata.symbol,
1244 symbol: filteredTokenSymbol,
1245 mintAddress: token.mint.address,
1246 + decimal: decimals,
1247 iconPath: iconPath,
1248 );
1249 } catch (_) {}
@@ -1217,7 +1267,7 @@ class SolanaWalletClient {
1267 }) async {
1268 const commitment = Commitment.confirmed;
1269
1220 - if (inputAmount.currency == CryptoCurrency.sol) {
1270 + if (tokenMint == null) {
1271 return _signNativeTokenTransaction(
1272 inputAmount: inputAmount,
1273 destinationAddress: destinationAddress,
@@ -1229,7 +1279,7 @@ class SolanaWalletClient {
1279 } else {
1280 return _signSPLTokenTransaction(
1281 tokenDecimals: inputAmount.currency.decimals,
1232 - tokenMint: tokenMint!,
1282 + tokenMint: tokenMint,
1283 inputAmount: inputAmount,
1284 ownerPrivateKey: ownerPrivateKey,
1285 destinationAddress: destinationAddress,
@@ -1270,53 +1320,28 @@ class SolanaWalletClient {
1320 );
1321 }
1322
1273 - Future<Message> _getMessageForSPLTokenTransaction({
1274 - required SolAddress ownerAddress,
1275 - required SolAddress destinationAddress,
1276 - required int tokenDecimals,
1277 - required SolAddress mintAddress,
1278 - required SolAddress sourceAccount,
1279 - required Money amount,
1280 - required Commitment commitment,
1281 - required SolAddress tokenProgramId,
1282 - }) async {
1283 - final instructions = [
1284 - SPLTokenProgram.transferChecked(
1285 - layout: SPLTokenTransferCheckedLayout(
1286 - amount: amount.amount,
1287 - decimals: tokenDecimals,
1288 - ),
1289 - mint: mintAddress,
1290 - source: sourceAccount,
1291 - destination: destinationAddress,
1292 - owner: ownerAddress,
1293 - )
1294 - ];
1295 -
1296 - final latestBlockhash = await _getLatestBlockhash(commitment);
1297 -
1298 - return Message.compile(
1299 - transactionInstructions: instructions,
1300 - payer: ownerAddress,
1301 - recentBlockhash: latestBlockhash,
1302 - );
1303 - }
1304 -
1323 Future<Money> _getFeeFromCompiledMessage(Message message, Commitment commitment) {
1324 final base64Message = base64Encode(message.serialize());
1325 return getFeeForMessage(base64Message, commitment);
1326 }
1327
1328 + Future<Money> _getRentExemptionAmount(int space) async {
1329 + final rent = await _provider!.request(
1330 + SolanaRPCGetMinimumBalanceForRentExemption(size: space),
1331 + );
1332 +
1333 + return Money(rent, CryptoCurrency.sol);
1334 + }
1335 +
1336 Future<bool> hasSufficientFundsLeftForRent({
1311 - required Money inputAmount,
1337 + required Money totalOutflow,
1338 required Money solBalance,
1313 - required Money fee,
1339 }) async {
1315 - final rent = await _provider!.request(
1340 + final rentBuffer = await _provider!.request(
1341 SolanaRPCGetMinimumBalanceForRentExemption(size: SolanaTokenAccountUtils.accountSize),
1342 );
1343
1319 - return (solBalance - (inputAmount + fee)) > Money(rent, CryptoCurrency.sol);
1344 + return (solBalance - totalOutflow) > Money(rentBuffer, CryptoCurrency.sol);
1345 }
1346
1347 Future<PendingSolanaTransaction> _signNativeTokenTransaction({
@@ -1343,8 +1368,7 @@ class SolanaWalletClient {
1368
1369 if (!isSendAll) {
1370 final hasSufficientFundsLeft = await hasSufficientFundsLeftForRent(
1346 - inputAmount: inputAmount,
1347 - fee: fee,
1371 + totalOutflow: inputAmount + fee,
1372 solBalance: solBalance,
1373 );
1374
@@ -1357,6 +1381,10 @@ class SolanaWalletClient {
1381 if (isSendAll) {
1382 final updatedLamports = inputAmount - fee;
1383
1384 + if (updatedLamports.isNegative || updatedLamports.isZero) {
1385 + throw SolanaTransactionWrongBalanceException(CryptoCurrency.sol);
1386 + }
1387 +
1388 final transaction = _constructNativeTransaction(
1389 ownerPrivateKey: ownerPrivateKey,
1390 destinationAddress: destinationAddress,
@@ -1502,21 +1530,10 @@ class SolanaWalletClient {
1530 return SPLTokenProgramConst.tokenProgramId;
1531 }
1532
1505 - Future<ProgramDerivedAddress?> _getOrCreateAssociatedTokenAccount({
1506 - required SolanaPrivateKey payerPrivateKey,
1533 + Future<ProgramDerivedAddress?> _findAssociatedTokenAccount({
1534 required SolAddress ownerAddress,
1535 required SolAddress mintAddress,
1509 - required bool shouldCreateATA,
1536 }) async {
1511 - // For transaction history loading (shouldCreateATA: false), try standard token program first
1512 - // to avoid unnecessary RPC call. Only fetch token program ID when creating accounts.
1513 - SolAddress tokenProgramId = SPLTokenProgramConst.tokenProgramId;
1514 -
1515 - if (shouldCreateATA) {
1516 - // Only fetch token program ID when we need to create an account
1517 - tokenProgramId = await _getTokenProgramId(mintAddress);
1518 - }
1519 -
1537 // Try with standard token program first (most common case)
1538 var associatedTokenAccount = AssociatedTokenAccountProgramUtils.associatedTokenAccount(
1539 mint: mintAddress,
@@ -1539,71 +1556,29 @@ class SolanaWalletClient {
1556 // If account exists with standard program, return it
1557 if (accountInfo != null) return associatedTokenAccount;
1558
1542 - // If not found and we're not creating, try Token-2022 as fallback
1543 - if (!shouldCreateATA) {
1544 - try {
1545 - final token2022ProgramId = await _getTokenProgramId(mintAddress);
1546 - if (token2022ProgramId.address != SPLTokenProgramConst.tokenProgramId.address) {
1547 - associatedTokenAccount = AssociatedTokenAccountProgramUtils.associatedTokenAccount(
1548 - mint: mintAddress,
1549 - owner: ownerAddress,
1550 - tokenProgramId: token2022ProgramId,
1551 - );
1552 -
1553 - try {
1554 - accountInfo = await _provider!.request(
1555 - SolanaRPCGetAccountInfo(
1556 - account: associatedTokenAccount.address,
1557 - commitment: Commitment.confirmed,
1558 - ),
1559 - );
1560 - if (accountInfo != null) return associatedTokenAccount;
1561 - } catch (_) {}
1562 - }
1563 - } catch (_) {}
1564 - return null;
1565 - }
1566 -
1567 - // For account creation, use the detected token program ID
1568 - associatedTokenAccount = AssociatedTokenAccountProgramUtils.associatedTokenAccount(
1569 - mint: mintAddress,
1570 - owner: ownerAddress,
1571 - tokenProgramId: tokenProgramId,
1572 - );
1573 -
1574 - final payerAddress = payerPrivateKey.publicKey().toAddress();
1575 -
1576 - final createAssociatedTokenAccount = AssociatedTokenAccountProgram.associatedTokenAccount(
1577 - payer: payerAddress,
1578 - associatedToken: associatedTokenAccount.address,
1579 - owner: ownerAddress,
1580 - mint: mintAddress,
1581 - tokenProgramId: tokenProgramId,
1582 - );
1583 -
1584 - final blockhash = await _getLatestBlockhash(Commitment.confirmed);
1585 -
1586 - final transaction = SolanaTransaction(
1587 - payerKey: payerAddress,
1588 - instructions: [createAssociatedTokenAccount],
1589 - recentBlockhash: blockhash,
1590 - type: TransactionType.v0,
1591 - );
1592 -
1593 - final serializedTransaction = await _signTransactionInternal(
1594 - ownerPrivateKey: payerPrivateKey,
1595 - transaction: transaction,
1596 - );
1597 -
1598 - await sendTransaction(
1599 - serializedTransaction: serializedTransaction,
1600 - commitment: Commitment.confirmed,
1601 - );
1559 + // if its not found under the standard program, then we try Token-2022, which derives a different address
1560 + try {
1561 + final token2022ProgramId = await _getTokenProgramId(mintAddress);
1562 + if (token2022ProgramId.address != SPLTokenProgramConst.tokenProgramId.address) {
1563 + associatedTokenAccount = AssociatedTokenAccountProgramUtils.associatedTokenAccount(
1564 + mint: mintAddress,
1565 + owner: ownerAddress,
1566 + tokenProgramId: token2022ProgramId,
1567 + );
1568
1603 - // Wait for confirmation
1604 - await Future.delayed(const Duration(seconds: 2));
1569 + try {
1570 + accountInfo = await _provider!.request(
1571 + SolanaRPCGetAccountInfo(
1572 + account: associatedTokenAccount.address,
1573 + commitment: Commitment.confirmed,
1574 + ),
1575 + );
1576 + if (accountInfo != null) return associatedTokenAccount;
1577 + } catch (_) {}
1578 + }
1579 + } catch (_) {}
1580
1606 - return associatedTokenAccount;
1581 + return null;
1582 }
1583
1584 Future<PendingSolanaTransaction> _signSPLTokenTransaction({
@@ -1620,6 +1595,7 @@ class SolanaWalletClient {
1595
1596 ProgramDerivedAddress? associatedSenderAccount;
1597 SolAddress senderTokenProgramId = tokenProgramId;
1598 + int? senderAccountSpace;
1599
1600 try {
1601 associatedSenderAccount = AssociatedTokenAccountProgramUtils.associatedTokenAccount(
@@ -1638,6 +1614,7 @@ class SolanaWalletClient {
1614
1615 if (accountInfo != null) {
1616 senderTokenProgramId = accountInfo.owner;
1617 + senderAccountSpace = accountInfo.space;
1618 } else {
1619 associatedSenderAccount = null;
1620 }
@@ -1664,6 +1641,7 @@ class SolanaWalletClient {
1641
1642 if (accountInfo != null) {
1643 senderTokenProgramId = accountInfo.owner;
1644 + senderAccountSpace = accountInfo.space;
1645 } else {
1646 associatedSenderAccount = null;
1647 }
@@ -1679,18 +1657,16 @@ class SolanaWalletClient {
1657 );
1658 }
1659
1682 - // Get or create recipient account using the sender's token program ID
1683 - // This ensures both accounts use the same program
1684 - ProgramDerivedAddress? associatedRecipientAccount;
1660 + final ProgramDerivedAddress associatedRecipientAccount;
1661 + bool shouldCreateRecipientAccount = false;
1662 +
1663 try {
1686 - // First, try to get/create with the sender's actual program ID
1664 final recipientPDA = AssociatedTokenAccountProgramUtils.associatedTokenAccount(
1665 mint: mintAddress,
1666 owner: SolAddress(destinationAddress),
1667 tokenProgramId: senderTokenProgramId,
1668 );
1669
1693 - // Check if account exists with correct program
1670 SolanaAccountInfo? recipientInfo;
1671 try {
1672 recipientInfo = await _provider!.request(
@@ -1703,39 +1679,16 @@ class SolanaWalletClient {
1679 recipientInfo = null;
1680 }
1681
1706 - if (recipientInfo != null && recipientInfo.owner.address == senderTokenProgramId.address) {
1707 - associatedRecipientAccount = recipientPDA;
1708 - } else {
1709 - // Create the account with the correct program ID
1710 - final createATA = AssociatedTokenAccountProgram.associatedTokenAccount(
1711 - payer: ownerPrivateKey.publicKey().toAddress(),
1712 - associatedToken: recipientPDA.address,
1713 - owner: SolAddress(destinationAddress),
1714 - mint: mintAddress,
1715 - tokenProgramId: senderTokenProgramId,
1716 - );
1717 -
1718 - final blockhash = await _getLatestBlockhash(Commitment.confirmed);
1719 - final createTransaction = SolanaTransaction(
1720 - payerKey: ownerPrivateKey.publicKey().toAddress(),
1721 - instructions: [createATA],
1722 - recentBlockhash: blockhash,
1723 - type: TransactionType.v0,
1724 - );
1725 -
1726 - final serializedCreateTx = await _signTransactionInternal(
1727 - ownerPrivateKey: ownerPrivateKey,
1728 - transaction: createTransaction,
1682 + if (recipientInfo != null && recipientInfo.owner.address != senderTokenProgramId.address) {
1683 + throw SolanaCreateAssociatedTokenAccountException(
1684 + "Recipient token account is owned by ${recipientInfo.owner.address}",
1685 );
1730 -
1731 - await sendTransaction(
1732 - serializedTransaction: serializedCreateTx,
1733 - commitment: Commitment.confirmed,
1734 - );
1735 -
1736 - await Future.delayed(const Duration(seconds: 2));
1737 - associatedRecipientAccount = recipientPDA;
1686 }
1687 +
1688 + shouldCreateRecipientAccount = recipientInfo == null;
1689 + associatedRecipientAccount = recipientPDA;
1690 + } on SolanaCreateAssociatedTokenAccountException {
1691 + rethrow;
1692 } catch (e) {
1693 throw SolanaCreateAssociatedTokenAccountException(e.toString());
1694 }
@@ -1751,30 +1704,42 @@ class SolanaWalletClient {
1704 decimals: tokenDecimals,
1705 );
1706
1707 + final instructions = <TransactionInstruction>[
1708 + if (shouldCreateRecipientAccount)
1709 + AssociatedTokenAccountProgram.associatedTokenAccountIdempotent(
1710 + payer: ownerPrivateKey.publicKey().toAddress(),
1711 + associatedToken: associatedRecipientAccount.address,
1712 + owner: SolAddress(destinationAddress),
1713 + mint: mintAddress,
1714 + tokenProgramId: senderTokenProgramId,
1715 + ),
1716 + transferInstructions,
1717 + ];
1718 +
1719 final latestBlockHash = await _getLatestBlockhash(commitment);
1720
1721 final transaction = SolanaTransaction(
1722 payerKey: ownerPrivateKey.publicKey().toAddress(),
1758 - instructions: [transferInstructions],
1723 + instructions: instructions,
1724 recentBlockhash: latestBlockHash,
1725 );
1726
1762 - final message = await _getMessageForSPLTokenTransaction(
1763 - ownerAddress: ownerPrivateKey.publicKey().toAddress(),
1764 - tokenDecimals: tokenDecimals,
1765 - mintAddress: mintAddress,
1766 - destinationAddress: associatedRecipientAccount.address,
1767 - sourceAccount: associatedSenderAccount.address,
1768 - amount: inputAmount,
1769 - commitment: commitment,
1770 - tokenProgramId: tokenProgramId,
1727 + final message = Message.compile(
1728 + transactionInstructions: instructions,
1729 + payer: ownerPrivateKey.publicKey().toAddress(),
1730 + recentBlockhash: latestBlockHash,
1731 );
1732
1733 final fee = await _getFeeFromCompiledMessage(message, commitment);
1734
1735 + // The sender account exists by this point, so its space is set, and the recipient
1736 + // account is the same size because it holds the same mint under the same program.
1737 + final accountCreationCost = shouldCreateRecipientAccount
1738 + ? await _getRentExemptionAmount(senderAccountSpace!)
1739 + : Money.zero(CryptoCurrency.sol);
1740 +
1741 final hasSufficientFundsLeft = await hasSufficientFundsLeftForRent(
1776 - inputAmount: Money.zero(CryptoCurrency.sol),
1777 - fee: fee,
1742 + totalOutflow: accountCreationCost + fee,
1743 solBalance: solBalance,
1744 );
1745
@@ -1796,6 +1761,7 @@ class SolanaWalletClient {
1761 destinationAddress: destinationAddress,
1762 sendTransaction: sendTx,
1763 fee: fee,
1764 + additionalCost: shouldCreateRecipientAccount ? accountCreationCost : null,
1765 );
1766 }
1767
@@ -1884,19 +1850,10 @@ class SolanaWalletClient {
1850 final mint = tokenData['mint'] as String? ?? '';
1851 if (mint.isEmpty) continue;
1852
1887 - final amountRaw = tokenData['amountRaw'] as String? ?? '0';
1888 -
1889 - final decimals = tokenData['decimals'] as int? ?? 0;
1890 -
1891 - final associatedTokenAddress = tokenData['associatedTokenAddress'] as String? ?? '';
1892 -
1853 tokens.add(
1854 MoralisSolanaTokenBalance(
1855 mint: mint,
1856 amount: amount,
1897 - amountRaw: amountRaw,
1898 - decimals: decimals,
1899 - associatedTokenAddress: associatedTokenAddress,
1857 ),
1858 );
1859 }
@@ -1954,15 +1911,9 @@ class SolanaWalletClient {
1911 class MoralisSolanaTokenBalance {
1912 final String mint;
1913 final double amount;
1957 - final String amountRaw;
1958 - final int decimals;
1959 - final String associatedTokenAddress;
1914
1915 const MoralisSolanaTokenBalance({
1916 required this.mint,
1917 required this.amount,
1964 - required this.amountRaw,
1965 - required this.decimals,
1966 - required this.associatedTokenAddress,
1918 });
1919 }
cw_solana/lib/solana_exceptions.dart
+4
@@ -36,3 +36,7 @@ class SolanaNoAssociatedTokenAccountException extends NoAssociatedTokenAccountEx
36 final String account;
37 final String mint;
38 }
39 +
40 +class SolanaAmbiguousTokenSymbolException extends AmbiguousTokenSymbolException {
41 + SolanaAmbiguousTokenSymbolException(super.symbol);
42 +}
cw_solana/lib/solana_transaction_history.dart
+8 -3
@@ -88,9 +88,14 @@ abstract class SolanaTransactionHistoryBase extends TransactionHistoryBase<Solan
88 txs.entries.forEach((entry) {
89 final val = entry.value;
90
91 - if (val is Map<String, dynamic>) {
92 - final tx = SolanaTransactionInfo.fromJson(val);
93 - _update(tx);
91 + if (val is! Map<String, dynamic>) {
92 + return;
93 + }
94 +
95 + try {
96 + _update(SolanaTransactionInfo.fromJson(val));
97 + } catch (e) {
98 + printV("Skipping unreadable solana transaction ${entry.key}: ${e.toString()}");
99 }
100 });
101 } catch (e) {
cw_solana/lib/solana_wallet.dart
+63 -50
@@ -251,12 +251,7 @@ abstract class SolanaWalletBase
251
252 await updateTokenBalance();
253
254 - final transactionCurrency = balance.keys.firstWhere(
255 - (currency) =>
256 - currency.title == credentials.currency.title &&
257 - currency.tag == credentials.currency.tag,
258 - orElse: () => throw Exception(
259 - 'Currency ${credentials.currency.title} ${credentials.currency.tag} is not accessible in the wallet, try to enable it first.'));
254 + final transactionCurrency = resolveTransactionCurrency(credentials.currency, balance.keys);
255
256 final walletBalanceForCurrency = balance[transactionCurrency]!.available;
257
@@ -285,9 +280,8 @@ abstract class SolanaWalletBase
280 }
281
282 String? tokenMint;
288 - // Token Mint is only needed for transactions that are not native tokens(non-SOL transactions)
289 - if (transactionCurrency.title != CryptoCurrency.sol.title) {
290 - tokenMint = (transactionCurrency as SPLToken).mintAddress;
283 + if (transactionCurrency is SPLToken) {
284 + tokenMint = transactionCurrency.mintAddress;
285 }
286
287 return _client.signSolanaTransaction(
@@ -302,6 +296,33 @@ abstract class SolanaWalletBase
296 );
297 }
298
299 + static CryptoCurrency resolveTransactionCurrency(
300 + CryptoCurrency requestedCurrency,
301 + Iterable<CryptoCurrency> availableCurrencies,
302 + ) {
303 + final matches = requestedCurrency is SPLToken
304 + ? availableCurrencies
305 + .where((currency) =>
306 + currency is SPLToken && currency.mintAddress == requestedCurrency.mintAddress)
307 + .toList(growable: false)
308 + : availableCurrencies
309 + .where((currency) =>
310 + currency.title == requestedCurrency.title && currency.tag == requestedCurrency.tag)
311 + .toList(growable: false);
312 +
313 + if (matches.isEmpty) {
314 + throw Exception(
315 + "Currency ${requestedCurrency.title} ${requestedCurrency.tag} is not accessible in the wallet, try to enable it first.",
316 + );
317 + }
318 +
319 + if (matches.length > 1) {
320 + throw SolanaAmbiguousTokenSymbolException(requestedCurrency.title);
321 + }
322 +
323 + return matches.first;
324 + }
325 +
326 @override
327 Future<Map<String, SolanaTransactionInfo>> fetchTransactions() async => {};
328
@@ -448,6 +469,8 @@ abstract class SolanaWalletBase
469 }
470 }
471
472 + static final _swapIdSuffixPattern = RegExp(r"_(outgoing|incoming)$");
473 +
474 void _addTransactions(List<SolanaTransactionModel> transactions) {
475 final Map<String, SolanaTransactionInfo> result = {};
476
@@ -464,6 +487,11 @@ abstract class SolanaWalletBase
487 isPending: false,
488 fee: transactionModel.fee,
489 );
490 +
491 + final baseSignature = transactionModel.id.replaceFirst(_swapIdSuffixPattern, "");
492 + if (baseSignature != transactionModel.id) {
493 + transactionHistory.transactions.remove(baseSignature);
494 + }
495 }
496
497 transactionHistory.addMany(result);
@@ -588,7 +616,9 @@ abstract class SolanaWalletBase
616 // Fetch SOL and SPL token balances in parallel for better performance
617 await Future.wait([
618 _fetchSOLBalance().then((solBalance) {
591 - balance[CryptoCurrency.sol] = solBalance;
619 + if (solBalance != null) {
620 + balance[CryptoCurrency.sol] = solBalance;
621 + }
622 }),
623 _updateSplTokenBalancesInternal(tokenMints: tokenMints),
624 ]);
@@ -596,10 +626,15 @@ abstract class SolanaWalletBase
626 await save();
627 }
628
599 - Future<SolanaBalance> _fetchSOLBalance() async {
600 - final balance = await _client.getBalance(solanaAddress);
601 -
602 - return SolanaBalance(balance);
629 + Future<SolanaBalance?> _fetchSOLBalance() async {
630 + try {
631 + return SolanaBalance(
632 + await _client.getBalance(solanaAddress, throwOnError: true),
633 + );
634 + } catch (e) {
635 + printV("Error fetching SOL balance: ${e.toString()}");
636 + return null;
637 + }
638 }
639
640 /// Internal helper to update SPL token balances.
@@ -743,7 +778,7 @@ abstract class SolanaWalletBase
778 name: tokenInfo.name,
779 symbol: tokenInfo.symbol,
780 mintAddress: mint,
746 - decimal: moralisToken.decimals,
781 + decimal: tokenInfo.decimal,
782 mint: tokenInfo.mint,
783 iconPath: tokenInfo.iconPath,
784 tag: 'SOL',
@@ -886,7 +921,7 @@ abstract class SolanaWalletBase
921 await _clearLastSyncedSignature(source);
922 }
923
889 - updateTokenBalance();
924 + await updateTokenBalance();
925 }
926
927 Future<void> _removeTokenTransactionsInHistory(SPLToken token) async {
@@ -932,47 +967,25 @@ abstract class SolanaWalletBase
967 return Base58Encoder.encode(signature);
968 }
969
935 - List<List<int>> bytesFromSigString(String signatureString) {
936 - final regex = RegExp(r'Signature\(\[(.+)\], publicKey: (.+)\)');
937 - final match = regex.firstMatch(signatureString);
938 -
939 - if (match != null) {
940 - final bytesString = match.group(1)!;
941 - final base58EncodedPublicKeyString = match.group(2)!;
942 - final sigBytes = bytesString.split(', ').map(int.parse).toList();
943 -
944 - List<int> pubKeyBytes = SolAddrDecoder().decodeAddr(base58EncodedPublicKeyString);
945 -
946 - return [sigBytes, pubKeyBytes];
947 - } else {
948 - throw const FormatException('Invalid Signature string format');
949 - }
950 - }
951 -
970 @override
971 Future<bool> verifyMessage(String message, String signature, {String? address}) async {
954 - String signatureString = utf8.decode(HEX.decode(signature));
955 -
956 - List<List<int>> bytes = bytesFromSigString(signatureString);
957 -
958 - final messageBytes = utf8.encode(message);
959 - final sigBytes = bytes[0];
960 - final pubKeyBytes = bytes[1];
961 -
962 - if (address == null) {
972 + if (address == null || address.isEmpty) {
973 return false;
974 }
975
966 - // make sure the address derived from the public key provided matches the one we expect
967 - final pub = SolanaPublicKey.fromBytes(pubKeyBytes);
968 - if (address != pub.toAddress().address) {
976 + try {
977 + final signatureBytes = Base58Decoder.decode(signature);
978 +
979 + final publicKey = SolanaPublicKey.fromBytes(SolAddrDecoder().decodeAddr(address));
980 +
981 + return publicKey.verify(
982 + message: utf8.encode(message),
983 + signature: signatureBytes,
984 + );
985 + } catch (e) {
986 + printV("Error verifying solana message: ${e.toString()}");
987 return false;
988 }
971 -
972 - return pub.verify(
973 - message: messageBytes,
974 - signature: sigBytes,
975 - );
989 }
990
991 SolanaRPC? get solanaProvider => _client.getSolanaProvider;
cw_solana/lib/solana_wallet_service.dart
+3 -3
@@ -92,7 +92,7 @@ class SolanaWalletService extends WalletService<
92
93 @override
94 Future<void> remove(String wallet) async {
95 - File(await pathForWalletDir(name: wallet, type: getType())).delete(recursive: true);
95 + await File(await pathForWalletDir(name: wallet, type: getType())).delete(recursive: true);
96 final walletInfo = await WalletInfo.get(wallet, getType());
97 if (walletInfo == null) {
98 throw Exception('Wallet not found');
@@ -104,8 +104,8 @@ class SolanaWalletService extends WalletService<
104 }
105
106 final prefs = await SharedPreferences.getInstance();
107 - for (final key in prefs.getKeys().where(
108 - (k) => k.startsWith('solana_last_synced_signature_${wallet}_'))) {
107 + for (final key
108 + in prefs.getKeys().where((k) => k.startsWith('solana_last_synced_signature_${wallet}_'))) {
109 await prefs.remove(key);
110 }
111 }
cw_solana/test/solana_currency_resolution_test.dart new
+108
@@ -0,0 +1,108 @@
1 +import "package:cw_core/crypto_currency.dart";
2 +import "package:cw_core/spl_token.dart";
3 +import "package:cw_solana/solana_client.dart";
4 +import "package:cw_solana/solana_exceptions.dart";
5 +import "package:cw_solana/solana_wallet.dart";
6 +import "package:flutter_test/flutter_test.dart";
7 +
8 +void main() {
9 + group("currencyForRawAmount", () {
10 + test("keeps the stored token when its decimals match the mint", () {
11 + final jup = SPLToken(
12 + name: "Jupiter",
13 + symbol: "JUP",
14 + mint: "jup",
15 + mintAddress: "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN",
16 + decimal: 6,
17 + );
18 +
19 + expect(SolanaWalletClient.currencyForRawAmount(jup, 6), same(jup));
20 + });
21 +
22 + test("prefers the mint decimals when the stored token disagrees", () {
23 + final staleJup = SPLToken(
24 + name: "Jupiter",
25 + symbol: "JUP",
26 + mint: "jup",
27 + mintAddress: "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN",
28 + decimal: 0,
29 + );
30 +
31 + final resolved = SolanaWalletClient.currencyForRawAmount(staleJup, 6);
32 +
33 + expect(resolved.decimals, 6);
34 + expect(resolved.title, "JUP");
35 + expect(resolved, isNot(same(staleJup)));
36 + });
37 +
38 + test("falls back to a placeholder title when the token is unknown", () {
39 + final resolved = SolanaWalletClient.currencyForRawAmount(null, 8);
40 +
41 + expect(resolved.decimals, 8);
42 + expect(resolved.title, "TOKEN");
43 + expect(resolved.name, "token");
44 + });
45 +
46 + test("always reports the mint decimals", () {
47 + for (final decimals in [0, 1, 6, 8, 9, 18]) {
48 + expect(SolanaWalletClient.currencyForRawAmount(null, decimals).decimals, decimals);
49 + }
50 + });
51 + });
52 +
53 + group("resolveTransactionCurrency", () {
54 + final realUsdc = SPLToken(
55 + name: "USD Coin",
56 + symbol: "USDC",
57 + mint: "usdc",
58 + mintAddress: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
59 + decimal: 6,
60 + );
61 +
62 + final scamUsdc = SPLToken(
63 + name: "USD Coin",
64 + symbol: "USDC",
65 + mint: "usdc",
66 + mintAddress: "scamMintAddressClaimingTheUsdcSymbol",
67 + decimal: 9,
68 + );
69 +
70 + test("picks the requested mint even when another token claims the symbol", () {
71 + final resolved = SolanaWalletBase.resolveTransactionCurrency(realUsdc, [scamUsdc, realUsdc]);
72 +
73 + expect(resolved, same(realUsdc));
74 + expect((resolved as SPLToken).mintAddress, realUsdc.mintAddress);
75 + });
76 +
77 + test("picks the scam mint only when the scam mint is the one requested", () {
78 + final resolved = SolanaWalletBase.resolveTransactionCurrency(scamUsdc, [scamUsdc, realUsdc]);
79 +
80 + expect(resolved, same(scamUsdc));
81 + });
82 +
83 + test("throws when the requested mint is not in the wallet", () {
84 + expect(
85 + () => SolanaWalletBase.resolveTransactionCurrency(realUsdc, [scamUsdc]),
86 + throwsA(isA<Exception>()),
87 + );
88 + });
89 +
90 + test("throws on ambiguity when a symbol lookup matches two tokens", () {
91 + expect(
92 + () => SolanaWalletBase.resolveTransactionCurrency(
93 + CryptoCurrency(name: "usdc", title: "USDC", decimals: 6, tag: "SOL"),
94 + [scamUsdc, realUsdc],
95 + ),
96 + throwsA(isA<SolanaAmbiguousTokenSymbolException>()),
97 + );
98 + });
99 +
100 + test("resolves a native currency by title and tag", () {
101 + expect(
102 + SolanaWalletBase.resolveTransactionCurrency(
103 + CryptoCurrency.sol, [CryptoCurrency.sol, realUsdc]),
104 + same(CryptoCurrency.sol),
105 + );
106 + });
107 + });
108 +}
lib/cake_pay/src/cards/cake_pay_buy_card_page.dart
+1
@@ -631,6 +631,7 @@ class CakePayBuyCardPage extends BasePage {
631 amountValue: _sendViewModel.amountParsingProxy
632 .asDisplayStringWithSymbol(_sendViewModel.pendingTransaction!.amount),
633 quantity: 'QTY: ${cakePayBuyCardViewModel.quantity}',
634 + explanation: _sendViewModel.pendingTransactionAdditionalCostNotice,
635 fiatAmountValue: _sendViewModel.pendingTransactionFiatAmountFormatted,
636 fee: S.of(bottomSheetContext).send_fee,
637 feeValue: _sendViewModel.amountParsingProxy
lib/exchange/provider/jupiter_exchange_provider.dart
+4
@@ -138,6 +138,10 @@ class JupiterExchangeProvider extends ExchangeProvider {
138 if (!_isSolanaCurrency(from) || !_isSolanaCurrency(to)) {
139 return 0.0;
140 }
141 +
142 + if (amount <= 0) {
143 + return 0.0;
144 + }
145 final inputMint = _getTokenMint(from);
146 final outputMint = _getTokenMint(to);
147
lib/new-ui/widgets/coins_page/assets_history/history_tile.dart
+2 -2
@@ -100,9 +100,9 @@ class HistoryTile extends StatelessWidget {
100 children: [
101 Opacity(
102 opacity: pending ? 0.5 : 1,
103 - child: CakeImageWidget(
103 + child: TokenImageWidget(
104 imageUrl: asset?.iconPath ?? "",
105 - width: 34,
105 + size: 34,
106 ),
107 ),
108 Align(
lib/new-ui/widgets/send_page/send_confirm_sheet.dart
+21
@@ -201,6 +201,7 @@ class SendTransactionDetails extends StatelessWidget {
201 Widget _buildMainContent(BuildContext context) {
202 return Observer(builder: (context) {
203 final transaction = sendViewModel.pendingTransaction;
204 + final additionalCostNotice = sendViewModel.pendingTransactionAdditionalCostNotice;
205
206 final currencySymbol =
207 sendViewModel.amountParsingProxy.getCryptoSymbol(sendViewModel.selectedCryptoCurrency);
@@ -398,6 +399,26 @@ class SendTransactionDetails extends StatelessWidget {
399 ),
400 ),
401 ),
402 + if (additionalCostNotice != null) ...[
403 + Padding(
404 + padding: const EdgeInsets.symmetric(horizontal: 12),
405 + child: Container(
406 + height: 1,
407 + color: Theme.of(context).colorScheme.surfaceContainerHigh,
408 + ),
409 + ),
410 + Padding(
411 + padding: const EdgeInsets.all(12),
412 + child: Text(
413 + additionalCostNotice,
414 + style: TextStyle(
415 + fontSize: 14,
416 + fontWeight: FontWeight.w400,
417 + color: Theme.of(context).colorScheme.onSurfaceVariant,
418 + ),
419 + ),
420 + ),
421 + ],
422 if (sendViewModel.isElectrumWallet) ...[
423 Padding(
424 padding: EdgeInsets.symmetric(horizontal: 12),
lib/solana/cw_solana.dart
+8 -4
@@ -130,11 +130,11 @@ class CWSolana extends Solana {
130
131 final token = (wallet as SolanaWallet).splTokenBySymbol(transaction.amount.currency.symbol);
132
133 - if (token == null) {
134 - throw StateError('No SPL token for symbol ${transaction.amount.currency.symbol}');
133 + if (token != null) {
134 + return token;
135 }
136
137 - return token;
137 + return transaction.amount.currency as CryptoCurrency;
138 }
139
140 @override
@@ -276,6 +276,8 @@ class CWSolana extends Solana {
276 'Jupiter swap returned unknown status: $status. Error: $errorMessage. Code: $errorCode',
277 );
278 }
279 + } on JupiterSwapFailedException {
280 + rethrow;
281 } catch (e) {
282 throw Exception('Failed to execute Jupiter swap: $e');
283 }
@@ -420,7 +422,9 @@ class CWSolana extends Solana {
422 if (discoveredMints.isNotEmpty) {
423 await wallet.updateSPLTokenTransactions(specificMints: discoveredMints);
424 }
423 - } catch (_) {}
425 + } catch (e) {
426 + printV("Error discovering wallet tokens: ${e.toString()}");
427 + }
428 }
429
430 @override
lib/src/screens/exchange_trade/exchange_trade_page.dart
+1
@@ -328,6 +328,7 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
328 amount: S.of(bottomSheetContext).send_amount,
329 amountValue: sendVM.amountParsingProxy
330 .getDisplayCryptoAmount(amountValue, sendVM.selectedCryptoCurrency),
331 + explanation: sendVM.pendingTransactionAdditionalCostNotice,
332 fiatAmountValue: fiatAmountValue,
333 fee: isEVMCompatibleChain(sendVM.walletType)
334 ? S.of(bottomSheetContext).send_estimated_fee
lib/view_model/send/send_view_model.dart
+20 -4
@@ -336,6 +336,18 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
336 @observable
337 PendingTransaction? pendingTransaction;
338
339 + String? get pendingTransactionAdditionalCostNotice {
340 + final additionalCost = pendingTransaction?.additionalCost;
341 +
342 + if (additionalCost == null) {
343 + return null;
344 + }
345 +
346 + return S.current.recipient_account_creation_fee(
347 + _appStore.amountParsingProxy.asDisplayStringWithSymbol(additionalCost),
348 + );
349 + }
350 +
351 @computed
352 String get balance {
353 if (walletType == WalletType.litecoin && coinTypeToSpendFrom == UnspentCoinType.mweb) {
@@ -1179,11 +1191,11 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
1191 await sharedPreferences.setString(PreferencesKey.backgroundSyncLastTrigger(wallet.name),
1192 DateTime.now().add(Duration(minutes: 1)).toIso8601String());
1193 } catch (e) {
1182 - if (e is JupiterSwapFailedException) {
1183 - await _updateSolanaTrade(signature: e.signature, isSuccess: false);
1184 - }
1194 state = FailureState(translateErrorMessage(e, wallet.type, wallet.currency));
1186 - await _updateSolanaTrade(signature: '', isSuccess: false);
1195 +
1196 + final failedSignature = e is JupiterSwapFailedException ? e.signature : "";
1197 +
1198 + await _updateSolanaTrade(signature: failedSignature, isSuccess: false);
1199 }
1200 }
1201
@@ -1509,6 +1521,10 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
1521 return S.current.solana_no_associated_token_account_exception;
1522 }
1523
1524 + if (error is AmbiguousTokenSymbolException) {
1525 + return S.current.ambiguous_token_symbol_exception(error.symbol);
1526 + }
1527 +
1528 if (errorMessage.contains('found no record of a prior credit')) {
1529 return S.current.insufficient_funds_for_tx;
1530 }
res/values/strings_ar.arb
+2
@@ -61,6 +61,7 @@
61 "already_have_account": "هل لديك حساب بالفعل؟",
62 "already_your_username": "هذا هو اسم المستخدم الخاص بك بالفعل!",
63 "always": "دائمًا",
64 + "ambiguous_token_symbol_exception": "يستخدم أكثر من رمز مميز في هذه المحفظة الرمز ${symbol}. قم بتعطيل الرمز المميز المكرر حتى يمكن التعرف على الرمز الصحيح قبل الإرسال.",
65 "amount": "الكمية: ",
66 "amount_is_below_minimum_limit": "سيكون رصيدك بعد الرسوم أقل من الحد الأدنى للمبلغ المطلوب للتبادل (${min})",
67 "amount_is_estimate": "مبلغ الاستلام تقديري",
@@ -845,6 +846,7 @@
846 "received": "تم الاستلام",
847 "receiving": "الاستلام",
848 "recieving": "جارٍ الاستلام",
849 + "recipient_account_creation_fee": "هناك حاجة إلى ${amount} إضافية لإنشاء حساب رمزي للمستلم، لأنه ليس لديه حساب حتى الآن.",
850 "recipient_address": "عنوان المستلم",
851 "reconnect": "إعادة الاتصال",
852 "reconnect_alert_text": "هل أنت متأكد أنك تريد إعادة الاتصال؟",
res/values/strings_bg.arb
+2
@@ -61,6 +61,7 @@
61 "already_have_account": "Вече имате акаунт?",
62 "already_your_username": "Това вече е вашето потребителско име!",
63 "always": "Винаги",
64 + "ambiguous_token_symbol_exception": "Повече от един токен в този портфейл използва символа ${symbol}. Деактивирайте дублиращия се токен, за да може правилният да бъде идентифициран преди изпращане.",
65 "amount": "Количество: ",
66 "amount_is_below_minimum_limit": "Вашият баланс след таксите ще бъде по-малък от минималната сума, необходима за обмяната (${min})",
67 "amount_is_estimate": "Сумата за получаване е приблизителна",
@@ -845,6 +846,7 @@
846 "received": "Получено",
847 "receiving": "Получаване",
848 "recieving": "Получаване",
849 + "recipient_account_creation_fee": "Необходими са допълнителни ${amount} за създаване на токен акаунт за получателя, тъй като той все още няма такъв.",
850 "recipient_address": "Адрес на получателя",
851 "reconnect": "Повторно свързване",
852 "reconnect_alert_text": "Сигурни ли сте, че искате да се свържете отново?",
res/values/strings_cs.arb
+2
@@ -61,6 +61,7 @@
61 "already_have_account": "Už máte účet?",
62 "already_your_username": "Toto už je vaše uživatelské jméno!",
63 "always": "Vždy",
64 + "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ý.",
65 "amount": "Částka: ",
66 "amount_is_below_minimum_limit": "Váš zůstatek po poplatcích by byl nižší než minimální částka potřebná pro směnu (${min})",
67 "amount_is_estimate": "Přijatá částka je pouze odhad.",
@@ -845,6 +846,7 @@
846 "received": "Přijato",
847 "receiving": "Přijímání",
848 "recieving": "Přijímání",
849 + "recipient_account_creation_fee": "K vytvoření tokenového účtu pro příjemce je potřeba další ${amount}, protože ještě žádný nemá.",
850 "recipient_address": "Adresa příjemce",
851 "reconnect": "Znovu připojit",
852 "reconnect_alert_text": "Opravdu se chcete znovu připojit?",
res/values/strings_de.arb
+2
@@ -63,6 +63,7 @@
63 "already_have_account": "Haben Sie bereits ein Konto?",
64 "already_your_username": "Das ist bereits Ihr Benutzername!",
65 "always": "Immer",
66 + "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.",
67 "amount": "Betrag: ",
68 "amount_hidden": "Versteckter Betrag",
69 "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 @@
867 "received": "Empfangen",
868 "receiving": "Empfangen",
869 "recieving": "Empfangen",
870 + "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.",
871 "recipient_address": "Empfängeradresse",
872 "recipient_number": "Empfänger ${number}",
873 "reconnect": "Erneut verbinden",
res/values/strings_en.arb
+2
@@ -63,6 +63,7 @@
63 "already_have_account": "Already have an account?",
64 "already_your_username": "This is already your username!",
65 "always": "Always",
66 + "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.",
67 "amount": "Amount: ",
68 "amount_hidden": "Amount hidden",
69 "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 @@
869 "received": "Received",
870 "receiving": "Receiving",
871 "recieving": "Recieving",
872 + "recipient_account_creation_fee": "An extra ${amount} is needed to create a token account for the recipient, because they do not have one yet.",
873 "recipient_address": "Recipient address",
874 "recipient_number": "Recipient ${number}",
875 "reconnect": "Reconnect",
res/values/strings_es.arb
+2
@@ -61,6 +61,7 @@
61 "already_have_account": "¿Ya tienes una cuenta?",
62 "already_your_username": "¡Este ya es tu nombre de usuario!",
63 "always": "Siempre",
64 + "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.",
65 "amount": "Cantidad: ",
66 "amount_is_below_minimum_limit": "Tu saldo después de las comisiones sería inferior al importe mínimo necesario para el intercambio (${min})",
67 "amount_is_estimate": "La cantidad a recibir es una estimación",
@@ -847,6 +848,7 @@
848 "received": "Recibido",
849 "receiving": "Recibiendo",
850 "recieving": "Recibiendo",
851 + "recipient_account_creation_fee": "Se necesita ${amount} adicional para crear una cuenta simbólica para el destinatario, porque aún no tiene una.",
852 "recipient_address": "Dirección del destinatario",
853 "reconnect": "Reconectar",
854 "reconnect_alert_text": "¿Estás seguro de que quieres reconectar?",
res/values/strings_fa.arb
+2
@@ -61,6 +61,7 @@
61 "already_have_account": "از قبل حساب کاربری دارید؟",
62 "already_your_username": "این از قبل نام کاربری شماست!",
63 "always": "همیشه",
64 + "ambiguous_token_symbol_exception": "بیش از یک توکن در این کیف پول از نماد ${symbol} استفاده می‌کند. رمز تکراری را غیرفعال کنید تا قبل از ارسال، کد صحیح شناسایی شود.",
65 "amount": "مقدار: ",
66 "amount_is_below_minimum_limit": "موجودی شما پس از کسر کارمزد کمتر از حداقل مقدار مورد نیاز برای مبادله (${min}) خواهد بود",
67 "amount_is_estimate": "مقدار دریافتی تقریبی است",
@@ -845,6 +846,7 @@
846 "received": "دریافت‌شده",
847 "receiving": "دریافت",
848 "recieving": "دریافت",
849 + "recipient_account_creation_fee": "یک ${amount} اضافی برای ایجاد یک حساب رمزی برای گیرنده مورد نیاز است، زیرا آنها هنوز حسابی ندارند.",
850 "recipient_address": "آدرس دریافت‌کننده",
851 "reconnect": "اتصال دوباره",
852 "reconnect_alert_text": "آیا مطمئن هستید که می‌خواهید دوباره وصل شوید؟",
res/values/strings_fr.arb
+2
@@ -61,6 +61,7 @@
61 "already_have_account": "Vous avez déjà un compte ?",
62 "already_your_username": "C'est déjà votre nom d'utilisateur !",
63 "always": "Toujours",
64 + "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.",
65 "amount": "Montant : ",
66 "amount_is_below_minimum_limit": "Votre solde après les frais serait inférieur au montant minimum requis pour l'échange (${min})",
67 "amount_is_estimate": "Le montant à recevoir est une estimation",
@@ -845,6 +846,7 @@
846 "received": "Reçu",
847 "receiving": "Réception",
848 "recieving": "Réception",
849 + "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.",
850 "recipient_address": "Adresse du destinataire",
851 "reconnect": "Reconnecter",
852 "reconnect_alert_text": "Êtes-vous sûr de vouloir vous reconnecter ?",
res/values/strings_gn.arb
+2
@@ -61,6 +61,7 @@
61 "already_have_account": "¿Erekóma peteĩ cuenta?",
62 "already_your_username": "¡Kóva ha'e nde puruhára réra voi!",
63 "always": "Tapia",
64 + "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.",
65 "amount": "Hetakue: ",
66 "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})",
67 "amount_is_estimate": "Pe monto oñemog̃uahẽtáva ha’e peteĩ jehecha hag̃a.",
@@ -849,6 +850,7 @@
850 "received": "Ojapyhy",
851 "receiving": "Oñemog̃uahẽ",
852 "recieving": "Oñemog̃uahẽ",
853 + "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ĩ.",
854 "recipient_address": "Ojapyhýva kundaharape",
855 "reconnect": "Eñembojoaju jey",
856 "reconnect_alert_text": "¿Reime añetehápe reñembojoaju jeyse?",
res/values/strings_ha.arb
+2
@@ -61,6 +61,7 @@
61 "already_have_account": "Kana da asusu tuni?",
62 "already_your_username": "Wannan tuni sunan mai amfani naka ne!",
63 "always": "Koyaushe",
64 + "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.",
65 "amount": "Adadi: ",
66 "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})",
67 "amount_is_estimate": "Adadin da za a karɓa kimantawa ne",
@@ -847,6 +848,7 @@
848 "received": "An karɓa",
849 "receiving": "Karɓa",
850 "recieving": "Ana karɓa",
851 + "recipient_account_creation_fee": "Ana buƙatar ƙarin ${amount} don ƙirƙirar asusun token ga mai karɓa, saboda ba su da ɗaya tukuna.",
852 "recipient_address": "Adireshin mai karɓa",
853 "reconnect": "Sake haɗawa",
854 "reconnect_alert_text": "Shin kun tabbata kuna son sake haɗawa?",
res/values/strings_hi.arb
+2
@@ -61,6 +61,7 @@
61 "already_have_account": "क्या आपके पास पहले से एक खाता है?",
62 "already_your_username": "यह पहले से ही आपका यूज़रनेम है!",
63 "always": "हमेशा",
64 + "ambiguous_token_symbol_exception": "इस वॉलेट में एक से अधिक टोकन प्रतीक ${symbol} का उपयोग करते हैं। डुप्लिकेट टोकन को अक्षम करें ताकि भेजने से पहले सही टोकन की पहचान की जा सके।",
65 "amount": "राशि: ",
66 "amount_is_below_minimum_limit": "फीस के बाद आपका बैलेंस एक्सचेंज के लिए आवश्यक न्यूनतम राशि (${min}) से कम होगा",
67 "amount_is_estimate": "प्राप्त होने वाली राशि अनुमानित है",
@@ -847,6 +848,7 @@
848 "received": "प्राप्त",
849 "receiving": "प्राप्त करना",
850 "recieving": "प्राप्त हो रहा है",
851 + "recipient_account_creation_fee": "प्राप्तकर्ता के लिए एक टोकन खाता बनाने के लिए अतिरिक्त ${amount} की आवश्यकता होती है, क्योंकि उनके पास अभी तक कोई खाता नहीं है।",
852 "recipient_address": "प्राप्तकर्ता का पता",
853 "reconnect": "पुनः कनेक्ट करें",
854 "reconnect_alert_text": "क्या आप वाकई फिर से कनेक्ट करना चाहते हैं?",
res/values/strings_hr.arb
+2
@@ -61,6 +61,7 @@
61 "already_have_account": "Već imate račun?",
62 "already_your_username": "Ovo je već vaše korisničko ime!",
63 "always": "Uvijek",
64 + "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.",
65 "amount": "Iznos: ",
66 "amount_is_below_minimum_limit": "Vaš saldo nakon naknada bio bi manji od minimalnog iznosa potrebnog za razmjenu (${min})",
67 "amount_is_estimate": "Primljeni iznos je procijenjen",
@@ -845,6 +846,7 @@
846 "received": "Primljeno",
847 "receiving": "Primanje",
848 "recieving": "Primanje",
849 + "recipient_account_creation_fee": "Dodatni iznos od ${amount} je potreban za stvaranje token računa za primatelja jer ga on još nema.",
850 "recipient_address": "Adresa primatelja",
851 "reconnect": "Ponovno poveži",
852 "reconnect_alert_text": "Jeste li sigurni da se želite ponovno povezati?",
res/values/strings_hy.arb
+2
@@ -61,6 +61,7 @@
61 "already_have_account": "Արդեն ունե՞ք հաշիվ։",
62 "already_your_username": "Սա արդեն ձեր օգտանունն է։",
63 "always": "Միշտ",
64 + "ambiguous_token_symbol_exception": "Այս դրամապանակում մեկից ավելի թոքեններ օգտագործում են ${symbol} խորհրդանիշը: Անջատեք կրկնօրինակ նշանը, որպեսզի ճիշտը հնարավոր լինի նույնացնել նախքան ուղարկելը:",
65 "amount": "Գումար՝ ",
66 "amount_is_below_minimum_limit": "Միջնորդավճարներից հետո Ձեր մնացորդը կլինի փոխանակման համար անհրաժեշտ նվազագույն գումարից (${min}) պակաս",
67 "amount_is_estimate": "Ստացվող գումարը մոտավոր է",
@@ -845,6 +846,7 @@
846 "received": "Ստացված",
847 "receiving": "Ստացում",
848 "recieving": "Ստացում",
849 + "recipient_account_creation_fee": "Ստացողի համար խորհրդանշական հաշիվ ստեղծելու համար անհրաժեշտ է լրացուցիչ ${amount}, քանի որ նրանք դեռ չունեն:",
850 "recipient_address": "Ստացողի հասցե",
851 "reconnect": "Վերամիանալ",
852 "reconnect_alert_text": "Վստա՞հ եք, որ ցանկանում եք կրկին միանալ:",
res/values/strings_id.arb
+2
@@ -61,6 +61,7 @@
61 "already_have_account": "Sudah punya akun?",
62 "already_your_username": "Ini sudah merupakan nama pengguna Anda!",
63 "always": "Selalu",
64 + "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.",
65 "amount": "Jumlah: ",
66 "amount_is_below_minimum_limit": "Saldo Anda setelah biaya akan lebih rendah dari jumlah minimum yang diperlukan untuk penukaran (${min})",
67 "amount_is_estimate": "Jumlah yang diterima adalah perkiraan",
@@ -847,6 +848,7 @@
848 "received": "Diterima",
849 "receiving": "Menerima",
850 "recieving": "Menerima",
851 + "recipient_account_creation_fee": "${amount} tambahan diperlukan untuk membuat akun token bagi penerima, karena mereka belum memilikinya.",
852 "recipient_address": "Alamat penerima",
853 "reconnect": "Hubungkan kembali",
854 "reconnect_alert_text": "Apakah Anda yakin ingin terhubung kembali?",
res/values/strings_it.arb
+3 -1
@@ -61,6 +61,7 @@
61 "already_have_account": "Hai già un account?",
62 "already_your_username": "Questo è già il tuo nome utente!",
63 "always": "Sempre",
64 + "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.",
65 "amount": "Importo: ",
66 "amount_is_below_minimum_limit": "Il saldo dopo le commissioni sarebbe inferiore all'importo minimo richiesto per lo scambio (${min})",
67 "amount_is_estimate": "L'importo da ricevere è una stima",
@@ -1476,5 +1477,6 @@
1477 "zcash_card_enable_later": "Puoi sempre abilitare questa carta in seguito nelle impostazioni",
1478 "zcash_card_missing_funds": "Fondi mancanti?",
1479 "zcash_card_scan": "Scansiona",
1479 - "zcash_card_warning": "Non chiudere l'app finché la procedura non è completata, altrimenti sarà necessario riavviare il processo da zero."
1480 + "zcash_card_warning": "Non chiudere l'app finché la procedura non è completata, altrimenti sarà necessario riavviare il processo da zero.",
1481 + "recipient_account_creation_fee": "È necessario un extra di ${amount} per creare un account token per il destinatario, perché non ne ha ancora uno."
1482 }
\ No newline at end of file
res/values/strings_ja.arb
+3 -1
@@ -61,6 +61,7 @@
61 "already_have_account": "すでにアカウントをお持ちですか?",
62 "already_your_username": "これはすでにあなたのユーザー名です!",
63 "always": "常に",
64 + "ambiguous_token_symbol_exception": "このウォレット内の複数のトークンは、シンボル ${symbol} を使用しています。送信前に正しいトークンを識別できるように、重複したトークンを無効にします。",
65 "amount": "金額: ",
66 "amount_is_below_minimum_limit": "手数料差し引き後の残高が、交換に必要な最低金額(${min})を下回ります",
67 "amount_is_estimate": "受け取り金額は概算です",
@@ -1474,5 +1475,6 @@
1475 "zcash_card_enable_later": "このカードは後から設定でいつでも有効にできます",
1476 "zcash_card_missing_funds": "資金が見当たりませんか?",
1477 "zcash_card_scan": "スキャン",
1477 - "zcash_card_warning": "手順が完了するまでアプリを閉じないでください。閉じると、このプロセスを最初からやり直す必要があります。"
1478 + "zcash_card_warning": "手順が完了するまでアプリを閉じないでください。閉じると、このプロセスを最初からやり直す必要があります。",
1479 + "recipient_account_creation_fee": "受信者はまだトークン アカウントを持っていないため、追加の ${amount} が必要です。"
1480 }
\ No newline at end of file
res/values/strings_ko.arb
+3 -1
@@ -61,6 +61,7 @@
61 "already_have_account": "이미 계정이 있으신가요?",
62 "already_your_username": "이미 사용 중인 사용자 이름입니다!",
63 "always": "항상",
64 + "ambiguous_token_symbol_exception": "이 지갑에 있는 두 개 이상의 토큰이 ${symbol} 기호를 사용합니다. 전송하기 전에 올바른 토큰을 식별할 수 있도록 중복 토큰을 비활성화합니다.",
65 "amount": "수량: ",
66 "amount_is_below_minimum_limit": "수수료 차감 후 잔액이 교환에 필요한 최소 금액(${min})보다 적을 것입니다",
67 "amount_is_estimate": "수신 금액은 예상치입니다",
@@ -1473,5 +1474,6 @@
1474 "zcash_card_enable_later": "설정에서 언제든지 나중에 이 카드를 활성화할 수 있습니다.",
1475 "zcash_card_missing_funds": "자금이 사라졌나요?",
1476 "zcash_card_scan": "스캔",
1476 - "zcash_card_warning": "절차가 완료될 때까지 앱을 닫지 마십시오. 닫으면 이 과정을 처음부터 다시 시작해야 합니다."
1477 + "zcash_card_warning": "절차가 완료될 때까지 앱을 닫지 마십시오. 닫으면 이 과정을 처음부터 다시 시작해야 합니다.",
1478 + "recipient_account_creation_fee": "아직 토큰 계정이 없기 때문에 수신자를 위한 토큰 계정을 생성하려면 추가 ${amount}가 필요합니다."
1479 }
\ No newline at end of file
res/values/strings_my.arb
+3 -1
@@ -61,6 +61,7 @@
61 "already_have_account": "အကောင့်ရှိပြီးသားလား?",
62 "already_your_username": "ဒါက သင့်အသုံးပြုသူအမည် ဖြစ်ပြီးသားပါ!",
63 "always": "အမြဲတမ်း",
64 + "ambiguous_token_symbol_exception": "ဤပိုက်ဆံအိတ်ရှိ တိုကင်တစ်ခုထက်ပိုသော သင်္ကေတ ${symbol} ကို အသုံးပြုသည်။ မပို့မီ မှန်ကန်သောတစ်ခုကို ဖော်ထုတ်နိုင်စေရန် ပွားနေသော တိုကင်ကို ပိတ်ပါ။",
65 "amount": "ပမာဏ: ",
66 "amount_is_below_minimum_limit": "အခကြေးငွေများနုတ်ပြီးနောက် သင့်လက်ကျန်ငွေသည် လဲလှယ်ရန်လိုအပ်သည့် အနိမ့်ဆုံးပမာဏ (${min}) ထက် နည်းသွားမည်",
67 "amount_is_estimate": "လက်ခံရရှိမည့်ပမာဏသည် ခန့်မှန်းတန်ဖိုးဖြစ်သည်။",
@@ -1473,5 +1474,6 @@
1474 "zcash_card_enable_later": "သင်သည် ဤကတ်ကို ဆက်တင်များတွင် နောက်မှ အချိန်မရွေး ဖွင့်နိုင်ပါသည်။",
1475 "zcash_card_missing_funds": "ငွေကြေး ပျောက်ဆုံးနေပါသလား?",
1476 "zcash_card_scan": "စကင်",
1476 - "zcash_card_warning": "လုပ်ငန်းစဉ် ပြီးဆုံးသည်အထိ အက်ပ်ကို မပိတ်ပါနှင့်။ ပိတ်လိုက်ပါက ဤလုပ်ငန်းစဉ်ကို အစမှ ပြန်လည်စတင်ရမည်ဖြစ်သည်။"
1477 + "zcash_card_warning": "လုပ်ငန်းစဉ် ပြီးဆုံးသည်အထိ အက်ပ်ကို မပိတ်ပါနှင့်။ ပိတ်လိုက်ပါက ဤလုပ်ငန်းစဉ်ကို အစမှ ပြန်လည်စတင်ရမည်ဖြစ်သည်။",
1478 + "recipient_account_creation_fee": "လက်ခံသူအတွက် တိုကင်အကောင့်တစ်ခုဖန်တီးရန် ၎င်းတို့တွင် တစ်ခုမရှိသေးသောကြောင့် အပို ${amount} လိုအပ်ပါသည်။"
1479 }
\ No newline at end of file
res/values/strings_nl.arb
+3 -1
@@ -61,6 +61,7 @@
61 "already_have_account": "Heb je al een account?",
62 "already_your_username": "Dit is al je gebruikersnaam!",
63 "always": "Altijd",
64 + "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.",
65 "amount": "Bedrag: ",
66 "amount_is_below_minimum_limit": "Je saldo na kosten zou lager zijn dan het minimale bedrag dat nodig is voor de exchange (${min})",
67 "amount_is_estimate": "Het te ontvangen bedrag is een schatting",
@@ -1474,5 +1475,6 @@
1475 "zcash_card_enable_later": "Je kunt deze kaart later altijd inschakelen via de instellingen",
1476 "zcash_card_missing_funds": "Fondsen ontbreken?",
1477 "zcash_card_scan": "Scannen",
1477 - "zcash_card_warning": "Sluit de app niet voordat de procedure is voltooid. Als je dit wel doet, moet dit proces helemaal opnieuw worden gestart."
1478 + "zcash_card_warning": "Sluit de app niet voordat de procedure is voltooid. Als je dit wel doet, moet dit proces helemaal opnieuw worden gestart.",
1479 + "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."
1480 }
\ No newline at end of file
res/values/strings_pl.arb
+3 -1
@@ -61,6 +61,7 @@
61 "already_have_account": "Masz już konto?",
62 "already_your_username": "To już jest Twoja nazwa użytkownika!",
63 "always": "Zawsze",
64 + "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.",
65 "amount": "Kwota: ",
66 "amount_is_below_minimum_limit": "Twoje saldo po opłatach byłoby niższe niż minimalna kwota wymagana do wymiany (${min})",
67 "amount_is_estimate": "Otrzymywana kwota jest szacunkowa",
@@ -1473,5 +1474,6 @@
1474 "zcash_card_enable_later": "Zawsze możesz włączyć tę kartę później w ustawieniach",
1475 "zcash_card_missing_funds": "Brakuje środków?",
1476 "zcash_card_scan": "Skanuj",
1476 - "zcash_card_warning": "Nie zamykaj aplikacji do czasu zakończenia procedury. Jeśli to zrobisz, proces będzie musiał rozpocząć się od nowa."
1477 + "zcash_card_warning": "Nie zamykaj aplikacji do czasu zakończenia procedury. Jeśli to zrobisz, proces będzie musiał rozpocząć się od nowa.",
1478 + "recipient_account_creation_fee": "Do założenia konta tokenowego dla odbiorcy potrzebna jest dodatkowa ${amount}, ponieważ jeszcze takiego nie posiada."
1479 }
\ No newline at end of file
res/values/strings_pt.arb
+3 -1
@@ -61,6 +61,7 @@
61 "already_have_account": "Já tem uma conta?",
62 "already_your_username": "Este já é o seu nome de utilizador!",
63 "always": "Sempre",
64 + "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.",
65 "amount": "Quantidade: ",
66 "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})",
67 "amount_is_estimate": "O valor a receber é uma estimativa",
@@ -1476,5 +1477,6 @@
1477 "zcash_card_enable_later": "Você sempre pode ativar este cartão mais tarde nas configurações",
1478 "zcash_card_missing_funds": "Fundos em falta?",
1479 "zcash_card_scan": "Escanear",
1479 - "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."
1480 + "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.",
1481 + "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."
1482 }
\ No newline at end of file
res/values/strings_pt_BR.arb
+2
@@ -58,6 +58,7 @@
58 "already_have_account": "Já tem uma conta?",
59 "already_your_username": "Este já é o seu nome de usuário!",
60 "always": "Sempre",
61 + "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.",
62 "amount": "Quantidade: ",
63 "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})",
64 "amount_is_estimate": "O valor a receber é uma estimativa",
@@ -802,6 +803,7 @@
803 "received": "Recebido",
804 "receiving": "Recebendo",
805 "recieving": "Recebendo",
806 + "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.",
807 "recipient_address": "Endereço do destinatário",
808 "reconnect": "Reconectar",
809 "reconnect_alert_text": "Tem certeza de que deseja reconectar?",
res/values/strings_ru.arb
+3 -1
@@ -61,6 +61,7 @@
61 "already_have_account": "У вас уже есть аккаунт?",
62 "already_your_username": "Это уже ваше имя пользователя!",
63 "always": "Всегда",
64 + "ambiguous_token_symbol_exception": "Более одного токена в этом кошельке используют символ ${symbol}. Отключите дубликат токена, чтобы можно было определить правильный перед отправкой.",
65 "amount": "Сумма: ",
66 "amount_is_below_minimum_limit": "Ваш баланс после комиссий будет меньше минимальной суммы, необходимой для обмена (${min})",
67 "amount_is_estimate": "Сумма к получению является приблизительной",
@@ -1474,5 +1475,6 @@
1475 "zcash_card_enable_later": "Вы всегда можете включить эту карту позже в настройках",
1476 "zcash_card_missing_funds": "Не хватает средств?",
1477 "zcash_card_scan": "Сканировать",
1477 - "zcash_card_warning": "Не закрывайте приложение до завершения процедуры. Если вы это сделаете, процесс придется перезапустить с нуля."
1478 + "zcash_card_warning": "Не закрывайте приложение до завершения процедуры. Если вы это сделаете, процесс придется перезапустить с нуля.",
1479 + "recipient_account_creation_fee": "Для создания учетной записи токена для получателя требуется дополнительная сумма ${amount}, поскольку у него ее еще нет."
1480 }
\ No newline at end of file
res/values/strings_th.arb
+3 -1
@@ -61,6 +61,7 @@
61 "already_have_account": "มีบัญชีอยู่แล้วใช่ไหม?",
62 "already_your_username": "นี่คือชื่อผู้ใช้ของคุณอยู่แล้ว!",
63 "always": "เสมอ",
64 + "ambiguous_token_symbol_exception": "โทเค็นมากกว่าหนึ่งรายการในกระเป๋าเงินนี้ใช้สัญลักษณ์ ${symbol} ปิดใช้งานโทเค็นที่ซ้ำกันเพื่อให้สามารถระบุโทเค็นที่ถูกต้องก่อนส่ง",
65 "amount": "จำนวน: ",
66 "amount_is_below_minimum_limit": "ยอดคงเหลือของคุณหลังหักค่าธรรมเนียมจะน้อยกว่าจำนวนขั้นต่ำที่จำเป็นสำหรับการแลกเปลี่ยน (${min})",
67 "amount_is_estimate": "จำนวนเงินที่ได้รับเป็นการประมาณการ",
@@ -1473,5 +1474,6 @@
1474 "zcash_card_enable_later": "คุณสามารถเปิดใช้งานการ์ดนี้ได้เสมอในภายหลังในการตั้งค่า",
1475 "zcash_card_missing_funds": "ยอดเงินหายไป?",
1476 "zcash_card_scan": "สแกน",
1476 - "zcash_card_warning": "อย่าปิดแอปจนกว่าขั้นตอนจะเสร็จสิ้น หากคุณปิดแอป กระบวนการนี้จะต้องเริ่มใหม่ตั้งแต่ต้น"
1477 + "zcash_card_warning": "อย่าปิดแอปจนกว่าขั้นตอนจะเสร็จสิ้น หากคุณปิดแอป กระบวนการนี้จะต้องเริ่มใหม่ตั้งแต่ต้น",
1478 + "recipient_account_creation_fee": "จำเป็นต้องมี ${amount} เพิ่มเติมเพื่อสร้างบัญชีโทเค็นสำหรับผู้รับ เนื่องจากยังไม่มีบัญชี"
1479 }
\ No newline at end of file
res/values/strings_tl.arb
+3 -1
@@ -61,6 +61,7 @@
61 "already_have_account": "Mayroon ka nang account?",
62 "already_your_username": "Ito na ang username mo!",
63 "always": "Palagi",
64 + "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.",
65 "amount": "Halaga: ",
66 "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})",
67 "amount_is_estimate": "Ang halagang matatanggap ay isang pagtatantya",
@@ -1473,5 +1474,6 @@
1474 "zcash_card_enable_later": "Maaari mo itong paganahin anumang oras sa ibang pagkakataon sa mga setting",
1475 "zcash_card_missing_funds": "Nawawalang pondo?",
1476 "zcash_card_scan": "I-scan",
1476 - "zcash_card_warning": "Huwag isara ang app hanggang sa makumpleto ang proseso; kung gagawin mo ito, kakailanganing magsimula muli mula sa simula."
1477 + "zcash_card_warning": "Huwag isara ang app hanggang sa makumpleto ang proseso; kung gagawin mo ito, kakailanganing magsimula muli mula sa simula.",
1478 + "recipient_account_creation_fee": "Kailangan ng dagdag na ${amount} para gumawa ng token account para sa tatanggap, dahil wala pa sila nito."
1479 }
\ No newline at end of file
res/values/strings_tr.arb
+3 -1
@@ -61,6 +61,7 @@
61 "already_have_account": "Zaten bir hesabınız var mı?",
62 "already_your_username": "Bu zaten kullanıcı adınız!",
63 "always": "Her zaman",
64 + "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.",
65 "amount": "Miktar: ",
66 "amount_is_below_minimum_limit": "Ücretlerden sonra bakiyeniz, takas için gereken minimum miktardan daha az olur (${min})",
67 "amount_is_estimate": "Alınacak tutar tahminidir",
@@ -1473,5 +1474,6 @@
1474 "zcash_card_enable_later": "Bu kartı daha sonra ayarlardan her zaman etkinleştirebilirsiniz",
1475 "zcash_card_missing_funds": "Bakiye eksik mi?",
1476 "zcash_card_scan": "Tara",
1476 - "zcash_card_warning": "İşlem tamamlanana kadar uygulamayı kapatmayın; aksi takdirde bu işlemin baştan yeniden başlatılması gerekecektir."
1477 + "zcash_card_warning": "İşlem tamamlanana kadar uygulamayı kapatmayın; aksi takdirde bu işlemin baştan yeniden başlatılması gerekecektir.",
1478 + "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."
1479 }
\ No newline at end of file
res/values/strings_uk.arb
+3 -1
@@ -61,6 +61,7 @@
61 "already_have_account": "Вже маєте обліковий запис?",
62 "already_your_username": "Це вже ваше ім’я користувача!",
63 "always": "Завжди",
64 + "ambiguous_token_symbol_exception": "Більш ніж один токен у цьому гаманці використовує символ ${symbol}. Вимкніть повторюваний маркер, щоб можна було визначити правильний перед надсиланням.",
65 "amount": "Сума: ",
66 "amount_is_below_minimum_limit": "Ваш баланс після комісій буде меншим за мінімальну суму, необхідну для обміну (${min})",
67 "amount_is_estimate": "Сума отримання є приблизною",
@@ -1474,5 +1475,6 @@
1475 "zcash_card_enable_later": "Ви завжди можете ввімкнути цю картку пізніше в налаштуваннях",
1476 "zcash_card_missing_funds": "Зникли кошти?",
1477 "zcash_card_scan": "Сканувати",
1477 - "zcash_card_warning": "Не закривайте застосунок, доки процедура не завершиться. Якщо ви це зробите, цей процес потрібно буде перезапустити з нуля."
1478 + "zcash_card_warning": "Не закривайте застосунок, доки процедура не завершиться. Якщо ви це зробите, цей процес потрібно буде перезапустити з нуля.",
1479 + "recipient_account_creation_fee": "Потрібна додаткова сума в розмірі ${amount}, щоб створити обліковий запис-токен для одержувача, оскільки він його ще не має."
1480 }
\ No newline at end of file
res/values/strings_ur.arb
+3 -1
@@ -61,6 +61,7 @@
61 "already_have_account": "کیا آپ کے پاس پہلے سے اکاؤنٹ ہے؟",
62 "already_your_username": "یہ پہلے ہی آپ کا صارف نام ہے!",
63 "always": "ہمیشہ",
64 + "ambiguous_token_symbol_exception": "اس بٹوے میں ایک سے زیادہ ٹوکن ${symbol} کی علامت استعمال کرتا ہے۔ ڈپلیکیٹ ٹوکن کو غیر فعال کریں تاکہ بھیجنے سے پہلے صحیح کی شناخت ہو سکے۔",
65 "amount": "رقم: ",
66 "amount_is_below_minimum_limit": "فیس کے بعد آپ کا بیلنس ایکسچینج کے لیے درکار کم از کم رقم (${min}) سے کم ہوگا",
67 "amount_is_estimate": "وصول ہونے والی رقم ایک تخمینہ ہے۔",
@@ -1475,5 +1476,6 @@
1476 "zcash_card_enable_later": "آپ اس کارڈ کو بعد میں بھی ترتیبات میں فعال کر سکتے ہیں",
1477 "zcash_card_missing_funds": "فنڈز غائب ہیں؟",
1478 "zcash_card_scan": "اسکین کریں",
1478 - "zcash_card_warning": "طریقہ کار مکمل ہونے تک ایپ بند نہ کریں۔ اگر آپ ایسا کریں گے تو یہ عمل شروع سے دوبارہ شروع کرنا پڑے گا۔"
1479 + "zcash_card_warning": "طریقہ کار مکمل ہونے تک ایپ بند نہ کریں۔ اگر آپ ایسا کریں گے تو یہ عمل شروع سے دوبارہ شروع کرنا پڑے گا۔",
1480 + "recipient_account_creation_fee": "وصول کنندہ کے لیے ٹوکن اکاؤنٹ بنانے کے لیے ایک اضافی ${amount} کی ضرورت ہے، کیونکہ ان کے پاس ابھی تک کوئی اکاؤنٹ نہیں ہے۔"
1481 }
\ No newline at end of file
res/values/strings_vi.arb
+3 -1
@@ -61,6 +61,7 @@
61 "already_have_account": "Bạn đã có tài khoản chưa?",
62 "already_your_username": "Đây đã là tên người dùng của bạn!",
63 "always": "Luôn",
64 + "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.",
65 "amount": "Số lượng: ",
66 "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})",
67 "amount_is_estimate": "Số tiền nhận được chỉ là ước tính",
@@ -1474,5 +1475,6 @@
1475 "zcash_card_enable_later": "Bạn luôn có thể bật thẻ này sau trong phần cài đặt",
1476 "zcash_card_missing_funds": "Thiếu tiền?",
1477 "zcash_card_scan": "Quét",
1477 - "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."
1478 + "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.",
1479 + "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."
1480 }
\ No newline at end of file
res/values/strings_yo.arb
+3 -1
@@ -61,6 +61,7 @@
61 "already_have_account": "Ṣé o ti ní àkántì tẹ́lẹ̀?",
62 "already_your_username": "Eyi ti jẹ orúkọ olumulo rẹ tẹlẹ!",
63 "always": "Nígbà gbogbo",
64 + "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ṣẹ́.",
65 "amount": "Iye: ",
66 "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})",
67 "amount_is_estimate": "Iye tí a máa gba jẹ́ àfojúsùn",
@@ -1474,5 +1475,6 @@
1475 "zcash_card_enable_later": "O le ma a mu kaadi yii ṣiṣẹ nigbamii ninu awọn eto",
1476 "zcash_card_missing_funds": "Ṣe owó rẹ sọnù?",
1477 "zcash_card_scan": "Ṣàwárí",
1477 - "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ẹ̀."
1478 + "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ẹ̀.",
1479 + "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ẹ."
1480 }
\ No newline at end of file
res/values/strings_zh.arb
+3 -1
@@ -61,6 +61,7 @@
61 "already_have_account": "已经有账户了?",
62 "already_your_username": "这已经是您的用户名!",
63 "always": "始终",
64 + "ambiguous_token_symbol_exception": "该钱包中有多个代币使用符号 ${symbol}。禁用重复的令牌,以便在发送之前可以识别正确的令牌。",
65 "amount": "数量:",
66 "amount_is_below_minimum_limit": "扣除手续费后,您的余额将低于兑换所需的最低金额(${min})",
67 "amount_is_estimate": "接收金额为预估值",
@@ -1474,5 +1475,6 @@
1475 "zcash_card_enable_later": "您随时可以在设置中启用此卡",
1476 "zcash_card_missing_funds": "资金不见了?",
1477 "zcash_card_scan": "扫描",
1477 - "zcash_card_warning": "在该过程完成之前请勿关闭应用程序,否则该过程将需要从头重新开始。"
1478 + "zcash_card_warning": "在该过程完成之前请勿关闭应用程序,否则该过程将需要从头重新开始。",
1479 + "recipient_account_creation_fee": "需要额外的 ${amount} 来为收件人创建一个令牌帐户,因为他们还没有令牌帐户。"
1480 }
\ No newline at end of file
tool/configure.dart
+1
@@ -922,6 +922,7 @@ import 'package:cw_core/wallet_info.dart';
922 import 'package:cw_core/wallet_service.dart';
923 import 'package:cw_core/spl_token.dart';
924 import 'package:cw_core/transaction_direction.dart';
925 +import 'package:cw_core/utils/print_verbose.dart';
926
927 """;
928 const solanaCWHeaders = """