CW-596-Solana-Bug-Fixes (#1340)

* fix: Generic bug fixes across solana

* fix: Remove back and forth parsing

* fix: Add check to cut flow when estimated fee is higher than wallet balance

* Update error message for fees exception

* Remove logs

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
This commit is contained in:
Adegoke David 2024-03-29 19:55:29 +01:00 committed by GitHub
parent 698c222291
commit a9b8c03e55
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 297 additions and 118 deletions

View file

@ -82,7 +82,7 @@ abstract class EVMChainClient {
Future<PendingEVMChainTransaction> signTransaction({
required EthPrivateKey privateKey,
required String toAddress,
required String amount,
required BigInt amount,
required int gas,
required EVMChainTransactionPriority priority,
required CryptoCurrency currency,
@ -103,7 +103,7 @@ abstract class EVMChainClient {
from: privateKey.address,
to: EthereumAddress.fromHex(toAddress),
maxPriorityFeePerGas: EtherAmount.fromInt(EtherUnit.gwei, priority.tip),
amount: isEVMCompatibleChain ? EtherAmount.inWei(BigInt.parse(amount)) : EtherAmount.zero(),
amount: isEVMCompatibleChain ? EtherAmount.inWei(amount) : EtherAmount.zero(),
data: data != null ? hexToBytes(data) : null,
);
@ -124,7 +124,7 @@ abstract class EVMChainClient {
_sendTransaction = () async {
await erc20.transfer(
EthereumAddress.fromHex(toAddress),
BigInt.parse(amount),
amount,
credentials: privateKey,
transaction: transaction,
);
@ -133,7 +133,7 @@ abstract class EVMChainClient {
return PendingEVMChainTransaction(
signedTransaction: signedTransaction,
amount: amount,
amount: amount.toString(),
fee: BigInt.from(gas) * (await price).getInWei,
sendTransaction: _sendTransaction,
exponent: exponent,

View file

@ -9,3 +9,14 @@ class EVMChainTransactionCreationException implements Exception {
@override
String toString() => exceptionMessage;
}
class EVMChainTransactionFeesException implements Exception {
final String exceptionMessage;
EVMChainTransactionFeesException()
: exceptionMessage = 'Current balance is less than the estimated fees for this transaction.';
@override
String toString() => exceptionMessage;
}

View file

@ -234,7 +234,7 @@ abstract class EVMChainWalletBase
final CryptoCurrency transactionCurrency =
balance.keys.firstWhere((element) => element.title == _credentials.currency.title);
final _erc20Balance = balance[transactionCurrency]!;
final erc20Balance = balance[transactionCurrency]!;
BigInt totalAmount = BigInt.zero;
int exponent = transactionCurrency is Erc20Token ? transactionCurrency.decimal : 18;
num amountToEVMChainMultiplier = pow(10, exponent);
@ -249,7 +249,7 @@ abstract class EVMChainWalletBase
outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)));
totalAmount = BigInt.from(totalOriginalAmount * amountToEVMChainMultiplier);
if (_erc20Balance.balance < totalAmount) {
if (erc20Balance.balance < totalAmount) {
throw EVMChainTransactionCreationException(transactionCurrency);
}
} else {
@ -258,18 +258,27 @@ abstract class EVMChainWalletBase
// then no need to subtract the fees from the amount if send all
final BigInt allAmount;
if (transactionCurrency is Erc20Token) {
allAmount = _erc20Balance.balance;
allAmount = erc20Balance.balance;
} else {
allAmount = _erc20Balance.balance -
BigInt.from(calculateEstimatedFee(_credentials.priority!, null));
final estimatedFee = BigInt.from(calculateEstimatedFee(_credentials.priority!, null));
if (estimatedFee > erc20Balance.balance) {
throw EVMChainTransactionFeesException();
}
allAmount = erc20Balance.balance - estimatedFee;
}
if (output.sendAll) {
totalAmount = allAmount;
} else {
final totalOriginalAmount =
EVMChainFormatter.parseEVMChainAmountToDouble(output.formattedCryptoAmount ?? 0);
totalAmount = output.sendAll
? allAmount
: BigInt.from(totalOriginalAmount * amountToEVMChainMultiplier);
if (_erc20Balance.balance < totalAmount) {
totalAmount = BigInt.from(totalOriginalAmount * amountToEVMChainMultiplier);
}
if (erc20Balance.balance < totalAmount) {
throw EVMChainTransactionCreationException(transactionCurrency);
}
}
@ -279,7 +288,7 @@ abstract class EVMChainWalletBase
toAddress: _credentials.outputs.first.isParsedAddress
? _credentials.outputs.first.extractedAddress!
: _credentials.outputs.first.address,
amount: totalAmount.toString(),
amount: totalAmount,
gas: _estimatedGas!,
priority: _credentials.priority!,
currency: transactionCurrency,

View file

@ -96,16 +96,30 @@ class SolanaWalletClient {
return SolanaBalance(totalBalance);
}
Future<double> getGasForMessage(String message) async {
Future<double> getFeeForMessage(String message, Commitment commitment) async {
try {
final gasPrice = await _client!.rpcClient.getFeeForMessage(message) ?? 0;
final fee = gasPrice / lamportsPerSol;
final feeForMessage =
await _client!.rpcClient.getFeeForMessage(message, commitment: commitment);
final fee = (feeForMessage ?? 0.0) / lamportsPerSol;
return fee;
} catch (_) {
return 0;
return 0.0;
}
}
Future<double> getEstimatedFee(Ed25519HDKeyPair ownerKeypair) async {
const commitment = Commitment.confirmed;
final message =
_getMessageForNativeTransaction(ownerKeypair, ownerKeypair.address, lamportsPerSol);
final recentBlockhash = await _getRecentBlockhash(commitment);
final estimatedFee =
_getFeeFromCompiledMessage(message, ownerKeypair.publicKey, recentBlockhash, commitment);
return estimatedFee;
}
/// Load the Address's transactions into the account
Future<List<SolanaTransactionModel>> fetchTransactions(
Ed25519HDPublicKey publicKey, {
@ -257,24 +271,15 @@ class SolanaWalletClient {
Future<PendingSolanaTransaction> signSolanaTransaction({
required String tokenTitle,
required int tokenDecimals,
String? tokenMint,
required double inputAmount,
required String destinationAddress,
required Ed25519HDKeyPair ownerKeypair,
required bool isSendAll,
String? tokenMint,
List<String> references = const [],
}) async {
const commitment = Commitment.confirmed;
final latestBlockhash =
await _client!.rpcClient.getLatestBlockhash(commitment: commitment).value;
final recentBlockhash = RecentBlockhash(
blockhash: latestBlockhash.blockhash,
feeCalculator: const FeeCalculator(
lamportsPerSignature: 500,
),
);
if (tokenTitle == CryptoCurrency.sol.title) {
final pendingNativeTokenTransaction = await _signNativeTokenTransaction(
tokenTitle: tokenTitle,
@ -282,8 +287,8 @@ class SolanaWalletClient {
inputAmount: inputAmount,
destinationAddress: destinationAddress,
ownerKeypair: ownerKeypair,
recentBlockhash: recentBlockhash,
commitment: commitment,
isSendAll: isSendAll,
);
return pendingNativeTokenTransaction;
} else {
@ -294,25 +299,29 @@ class SolanaWalletClient {
inputAmount: inputAmount,
destinationAddress: destinationAddress,
ownerKeypair: ownerKeypair,
recentBlockhash: recentBlockhash,
commitment: commitment,
);
return pendingSPLTokenTransaction;
}
}
Future<PendingSolanaTransaction> _signNativeTokenTransaction({
required String tokenTitle,
required int tokenDecimals,
required double inputAmount,
required String destinationAddress,
required Ed25519HDKeyPair ownerKeypair,
required RecentBlockhash recentBlockhash,
required Commitment commitment,
}) async {
// Convert SOL to lamport
int lamports = (inputAmount * lamportsPerSol).toInt();
Future<RecentBlockhash> _getRecentBlockhash(Commitment commitment) async {
final latestBlockhash =
await _client!.rpcClient.getLatestBlockhash(commitment: commitment).value;
final recentBlockhash = RecentBlockhash(
blockhash: latestBlockhash.blockhash,
feeCalculator: const FeeCalculator(lamportsPerSignature: 500),
);
return recentBlockhash;
}
Message _getMessageForNativeTransaction(
Ed25519HDKeyPair ownerKeypair,
String destinationAddress,
int lamports,
) {
final instructions = [
SystemInstruction.transfer(
fundingAccount: ownerKeypair.publicKey,
@ -322,20 +331,74 @@ class SolanaWalletClient {
];
final message = Message(instructions: instructions);
return message;
}
Future<double> _getFeeFromCompiledMessage(
Message message,
Ed25519HDPublicKey feePayer,
RecentBlockhash recentBlockhash,
Commitment commitment,
) async {
final compile = message.compile(
recentBlockhash: recentBlockhash.blockhash,
feePayer: feePayer,
);
final base64Message = base64Encode(compile.toByteArray().toList());
final fee = await getFeeForMessage(base64Message, commitment);
return fee;
}
Future<PendingSolanaTransaction> _signNativeTokenTransaction({
required String tokenTitle,
required int tokenDecimals,
required double inputAmount,
required String destinationAddress,
required Ed25519HDKeyPair ownerKeypair,
required Commitment commitment,
required bool isSendAll,
}) async {
// Convert SOL to lamport
int lamports = (inputAmount * lamportsPerSol).toInt();
Message message = _getMessageForNativeTransaction(ownerKeypair, destinationAddress, lamports);
final signers = [ownerKeypair];
final signedTx = await _signTransactionInternal(
RecentBlockhash recentBlockhash = await _getRecentBlockhash(commitment);
final fee = await _getFeeFromCompiledMessage(
message,
signers.first.publicKey,
recentBlockhash,
commitment,
);
SignedTx signedTx;
if (isSendAll) {
final feeInLamports = (fee * lamportsPerSol).toInt();
final updatedLamports = lamports - feeInLamports;
final updatedMessage =
_getMessageForNativeTransaction(ownerKeypair, destinationAddress, updatedLamports);
signedTx = await _signTransactionInternal(
message: updatedMessage,
signers: signers,
commitment: commitment,
recentBlockhash: recentBlockhash,
);
} else {
signedTx = await _signTransactionInternal(
message: message,
signers: signers,
commitment: commitment,
recentBlockhash: recentBlockhash,
);
final fee = await _getFeeFromCompiledMessage(
message,
recentBlockhash,
signers.first.publicKey,
);
}
sendTx() async => await sendTransaction(
signedTransaction: signedTx,
@ -360,7 +423,6 @@ class SolanaWalletClient {
required double inputAmount,
required String destinationAddress,
required Ed25519HDKeyPair ownerKeypair,
required RecentBlockhash recentBlockhash,
required Commitment commitment,
}) async {
final destinationOwner = Ed25519HDPublicKey.fromBase58(destinationAddress);
@ -408,8 +470,18 @@ class SolanaWalletClient {
);
final message = Message(instructions: [instruction]);
final signers = [ownerKeypair];
RecentBlockhash recentBlockhash = await _getRecentBlockhash(commitment);
final fee = await _getFeeFromCompiledMessage(
message,
signers.first.publicKey,
recentBlockhash,
commitment,
);
final signedTx = await _signTransactionInternal(
message: message,
signers: signers,
@ -417,12 +489,6 @@ class SolanaWalletClient {
recentBlockhash: recentBlockhash,
);
final fee = await _getFeeFromCompiledMessage(
message,
recentBlockhash,
signers.first.publicKey,
);
sendTx() async => await sendTransaction(
signedTransaction: signedTx,
commitment: commitment,
@ -438,19 +504,6 @@ class SolanaWalletClient {
return pendingTransaction;
}
Future<double> _getFeeFromCompiledMessage(
Message message, RecentBlockhash recentBlockhash, Ed25519HDPublicKey feePayer) async {
final compile = message.compile(
recentBlockhash: recentBlockhash.blockhash,
feePayer: feePayer,
);
final base64Message = base64Encode(compile.toByteArray().toList());
final fee = await getGasForMessage(base64Message);
return fee;
}
Future<SignedTx> _signTransactionInternal({
required Message message,
required List<Ed25519HDKeyPair> signers,
@ -466,6 +519,7 @@ class SolanaWalletClient {
required SignedTx signedTransaction,
required Commitment commitment,
}) async {
try {
final signature = await _client!.rpcClient.sendTransaction(
signedTransaction.encode(),
preflightCommitment: commitment,
@ -474,5 +528,9 @@ class SolanaWalletClient {
_client!.waitForSignatureStatus(signature, status: commitment);
return signature;
} catch (e) {
print('Error while sending transaction: ${e.toString()}');
throw Exception(e);
}
}
}

View file

@ -75,6 +75,9 @@ abstract class SolanaWalletBase
late SolanaWalletClient _client;
@observable
double? estimatedFee;
Timer? _transactionsUpdateTimer;
late final Box<SPLToken> splTokensBox;
@ -171,6 +174,14 @@ abstract class SolanaWalletBase
}
}
Future<void> _getEstimatedFees() async {
try {
estimatedFee = await _client.getEstimatedFee(_walletKeyPair!);
} catch (e) {
estimatedFee = 0.0;
}
}
@override
Future<PendingTransaction> createTransaction(Object credentials) async {
final solCredentials = credentials as SolanaTransactionCredentials;
@ -188,6 +199,8 @@ abstract class SolanaWalletBase
double totalAmount = 0.0;
bool isSendAll = false;
if (hasMultiDestination) {
if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) {
throw SolanaTransactionWrongBalanceException(transactionCurrency);
@ -204,9 +217,15 @@ abstract class SolanaWalletBase
} else {
final output = outputs.first;
isSendAll = output.sendAll;
if (isSendAll) {
totalAmount = walletBalanceForCurrency;
} else {
final totalOriginalAmount = double.parse(output.cryptoAmount ?? '0.0');
totalAmount = output.sendAll ? walletBalanceForCurrency : totalOriginalAmount;
totalAmount = totalOriginalAmount;
}
if (walletBalanceForCurrency < totalAmount) {
throw SolanaTransactionWrongBalanceException(transactionCurrency);
@ -228,6 +247,7 @@ abstract class SolanaWalletBase
destinationAddress: solCredentials.outputs.first.isParsedAddress
? solCredentials.outputs.first.extractedAddress!
: solCredentials.outputs.first.address,
isSendAll: isSendAll,
);
return pendingSolanaTransaction;
@ -269,7 +289,10 @@ abstract class SolanaWalletBase
Future<void> _updateSPLTokenTransactions() async {
List<SolanaTransactionModel> splTokenTransactions = [];
for (var token in balance.keys) {
// Make a copy of keys to avoid concurrent modification
var tokenKeys = List<CryptoCurrency>.from(balance.keys);
for (var token in tokenKeys) {
if (token is SPLToken) {
final tokenTxs = await _client.getSPLTokenTransfers(
token.mintAddress,
@ -326,6 +349,7 @@ abstract class SolanaWalletBase
_updateBalance(),
_updateNativeSOLTransactions(),
_updateSPLTokenTransactions(),
_getEstimatedFees(),
]);
syncStatus = SyncedSyncStatus();
@ -433,6 +457,7 @@ abstract class SolanaWalletBase
final mintPublicKey = Ed25519HDPublicKey.fromBase58(mintAddress);
// Fetch token's metadata account
try {
final token = await solanaClient!.rpcClient.getMetadata(mint: mintPublicKey);
if (token == null) {
@ -445,6 +470,9 @@ abstract class SolanaWalletBase
symbol: token.symbol,
mintAddress: mintAddress,
);
} catch (e) {
return null;
}
}
@override
@ -475,9 +503,9 @@ abstract class SolanaWalletBase
}
_transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 20), (_) {
_updateSPLTokenTransactions();
_updateNativeSOLTransactions();
_updateBalance();
_updateNativeSOLTransactions();
_updateSPLTokenTransactions();
});
}

View file

@ -32,6 +32,7 @@ class SolanaWalletService extends WalletService<SolanaNewWalletCredentials,
await wallet.init();
wallet.addInitialTokens();
await wallet.save();
return wallet;
}
@ -46,6 +47,8 @@ class SolanaWalletService extends WalletService<SolanaNewWalletCredentials,
Future<SolanaWallet> openWallet(String name, String password) async {
final walletInfo =
walletInfoSource.values.firstWhere((info) => info.id == WalletBase.idFor(name, getType()));
try {
final wallet = await SolanaWalletBase.open(
name: name,
password: password,
@ -54,8 +57,21 @@ class SolanaWalletService extends WalletService<SolanaNewWalletCredentials,
await wallet.init();
await wallet.save();
saveBackup(name);
return wallet;
} catch (_) {
await restoreWalletFilesFromBackup(name);
final wallet = await SolanaWalletBase.open(
name: name,
password: password,
walletInfo: walletInfo,
);
await wallet.init();
await wallet.save();
return wallet;
}
}
@override
@ -110,6 +126,7 @@ class SolanaWalletService extends WalletService<SolanaNewWalletCredentials,
password: password, name: currentName, walletInfo: currentWalletInfo);
await currentWallet.renameWalletFiles(newName);
await saveBackup(newName);
final newWalletInfo = currentWalletInfo;
newWalletInfo.id = WalletBase.idFor(newName, getType());

View file

@ -74,8 +74,22 @@ class CWSolana extends Solana {
}
@override
Future<void> addSPLToken(WalletBase wallet, CryptoCurrency token) async =>
await (wallet as SolanaWallet).addSPLToken(token as SPLToken);
Future<void> addSPLToken(
WalletBase wallet,
CryptoCurrency token,
String contractAddress,
) async {
final splToken = SPLToken(
name: token.name,
symbol: token.title,
mintAddress: contractAddress,
decimal: token.decimals,
mint: token.name.toUpperCase(),
enabled: token.enabled,
);
await (wallet as SolanaWallet).addSPLToken(splToken);
}
@override
Future<void> deleteSPLToken(WalletBase wallet, CryptoCurrency token) async =>
@ -115,4 +129,9 @@ class CWSolana extends Solana {
return null;
}
@override
double? getEstimateFees(WalletBase wallet) {
return (wallet as SolanaWallet).estimatedFee;
}
}

View file

@ -195,12 +195,14 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
onPressed: () async {
if (_formKey.currentState!.validate() &&
(!_showDisclaimer || _disclaimerChecked)) {
await widget.homeSettingsViewModel.addToken(Erc20Token(
await widget.homeSettingsViewModel.addToken(
token: CryptoCurrency(
name: _tokenNameController.text,
symbol: _tokenSymbolController.text,
title: _tokenSymbolController.text.toUpperCase(),
decimals: int.parse(_tokenDecimalController.text),
),
contractAddress: _contractAddressController.text,
decimal: int.parse(_tokenDecimalController.text),
));
);
if (context.mounted) {
Navigator.pop(context);
}

View file

@ -323,8 +323,7 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
? sendViewModel.allAmountValidator
: sendViewModel.amountValidator,
),
if (!sendViewModel.isBatchSending &&
sendViewModel.shouldDisplaySendALL)
if (!sendViewModel.isBatchSending)
Positioned(
top: 2,
right: 0,
@ -456,7 +455,9 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
if (sendViewModel.hasFees)
Observer(
builder: (_) => GestureDetector(
onTap: () => _setTransactionPriority(context),
onTap: sendViewModel.hasFeesPriority
? () => _setTransactionPriority(context)
: () {},
child: Container(
padding: EdgeInsets.only(top: 24),
child: Row(

View file

@ -44,17 +44,37 @@ abstract class HomeSettingsViewModelBase with Store {
@action
void setPinNativeToken(bool value) => _settingsStore.pinNativeTokenAtTop = value;
Future<void> addToken(CryptoCurrency token) async {
Future<void> addToken({
required String contractAddress,
required CryptoCurrency token,
}) async {
if (_balanceViewModel.wallet.type == WalletType.ethereum) {
await ethereum!.addErc20Token(_balanceViewModel.wallet, token);
final erc20token = Erc20Token(
name: token.name,
symbol: token.title,
decimal: token.decimals,
contractAddress: contractAddress,
);
await ethereum!.addErc20Token(_balanceViewModel.wallet, erc20token);
}
if (_balanceViewModel.wallet.type == WalletType.polygon) {
await polygon!.addErc20Token(_balanceViewModel.wallet, token);
final polygonToken = Erc20Token(
name: token.name,
symbol: token.title,
decimal: token.decimals,
contractAddress: contractAddress,
);
await polygon!.addErc20Token(_balanceViewModel.wallet, polygonToken);
}
if (_balanceViewModel.wallet.type == WalletType.solana) {
await solana!.addSPLToken(_balanceViewModel.wallet, token);
await solana!.addSPLToken(
_balanceViewModel.wallet,
token,
contractAddress,
);
}
_updateTokensList();
@ -117,7 +137,8 @@ abstract class HomeSettingsViewModelBase with Store {
}
if (_balanceViewModel.wallet.type == WalletType.solana) {
solana!.addSPLToken(_balanceViewModel.wallet, token);
final address = solana!.getTokenAddress(token);
solana!.addSPLToken(_balanceViewModel.wallet, token, address);
}
_refreshTokensList();

View file

@ -6,6 +6,7 @@ import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cake_wallet/haven/haven.dart';
import 'package:cake_wallet/polygon/polygon.dart';
import 'package:cake_wallet/reactions/wallet_connect.dart';
import 'package:cake_wallet/solana/solana.dart';
import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:flutter/material.dart';
@ -116,6 +117,10 @@ abstract class OutputBase with Store {
@computed
double get estimatedFee {
try {
if (_wallet.type == WalletType.solana) {
return solana!.getEstimateFees(_wallet) ?? 0.0;
}
final fee = _wallet.calculateEstimatedFee(
_settingsStore.priority[_wallet.type]!, formattedCryptoAmount);

View file

@ -106,8 +106,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
@computed
bool get isBatchSending => outputs.length > 1;
bool get shouldDisplaySendALL => walletType != WalletType.solana;
@computed
String get pendingTransactionFiatAmount {
if (pendingTransaction == null) {
@ -208,6 +206,11 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
@computed
bool get hasFees => wallet.type != WalletType.nano && wallet.type != WalletType.banano;
@computed
bool get hasFeesPriority =>
wallet.type != WalletType.nano &&
wallet.type != WalletType.banano &&
wallet.type != WalletType.solana;
@observable
CryptoCurrency selectedCryptoCurrency;

View file

@ -948,7 +948,11 @@ abstract class Solana {
required CryptoCurrency currency,
});
List<CryptoCurrency> getSPLTokenCurrencies(WalletBase wallet);
Future<void> addSPLToken(WalletBase wallet, CryptoCurrency token);
Future<void> addSPLToken(
WalletBase wallet,
CryptoCurrency token,
String contractAddress,
);
Future<void> deleteSPLToken(WalletBase wallet, CryptoCurrency token);
Future<CryptoCurrency?> getSPLToken(WalletBase wallet, String contractAddress);
@ -956,6 +960,7 @@ abstract class Solana {
double getTransactionAmountRaw(TransactionInfo transactionInfo);
String getTokenAddress(CryptoCurrency asset);
List<int>? getValidationLength(CryptoCurrency type);
double? getEstimateFees(WalletBase wallet);
}
""";