mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2025-06-28 12:29:51 +00:00
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:
parent
698c222291
commit
a9b8c03e55
13 changed files with 297 additions and 118 deletions
|
@ -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,21 +331,75 @@ 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(
|
||||
message: message,
|
||||
signers: signers,
|
||||
commitment: commitment,
|
||||
recentBlockhash: recentBlockhash,
|
||||
);
|
||||
RecentBlockhash recentBlockhash = await _getRecentBlockhash(commitment);
|
||||
|
||||
final fee = await _getFeeFromCompiledMessage(
|
||||
message,
|
||||
recentBlockhash,
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
sendTx() async => await sendTransaction(
|
||||
signedTransaction: signedTx,
|
||||
commitment: commitment,
|
||||
|
@ -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,13 +519,18 @@ class SolanaWalletClient {
|
|||
required SignedTx signedTransaction,
|
||||
required Commitment commitment,
|
||||
}) async {
|
||||
final signature = await _client!.rpcClient.sendTransaction(
|
||||
signedTransaction.encode(),
|
||||
preflightCommitment: commitment,
|
||||
);
|
||||
try {
|
||||
final signature = await _client!.rpcClient.sendTransaction(
|
||||
signedTransaction.encode(),
|
||||
preflightCommitment: commitment,
|
||||
);
|
||||
|
||||
_client!.waitForSignatureStatus(signature, status: commitment);
|
||||
_client!.waitForSignatureStatus(signature, status: commitment);
|
||||
|
||||
return signature;
|
||||
return signature;
|
||||
} catch (e) {
|
||||
print('Error while sending transaction: ${e.toString()}');
|
||||
throw Exception(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
final totalOriginalAmount = double.parse(output.cryptoAmount ?? '0.0');
|
||||
isSendAll = output.sendAll;
|
||||
|
||||
totalAmount = output.sendAll ? walletBalanceForCurrency : totalOriginalAmount;
|
||||
if (isSendAll) {
|
||||
totalAmount = walletBalanceForCurrency;
|
||||
} else {
|
||||
final totalOriginalAmount = double.parse(output.cryptoAmount ?? '0.0');
|
||||
|
||||
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,18 +457,22 @@ abstract class SolanaWalletBase
|
|||
final mintPublicKey = Ed25519HDPublicKey.fromBase58(mintAddress);
|
||||
|
||||
// Fetch token's metadata account
|
||||
final token = await solanaClient!.rpcClient.getMetadata(mint: mintPublicKey);
|
||||
try {
|
||||
final token = await solanaClient!.rpcClient.getMetadata(mint: mintPublicKey);
|
||||
|
||||
if (token == null) {
|
||||
if (token == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return SPLToken.fromMetadata(
|
||||
name: token.name,
|
||||
mint: token.mint,
|
||||
symbol: token.symbol,
|
||||
mintAddress: mintAddress,
|
||||
);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return SPLToken.fromMetadata(
|
||||
name: token.name,
|
||||
mint: token.mint,
|
||||
symbol: token.symbol,
|
||||
mintAddress: mintAddress,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -475,9 +503,9 @@ abstract class SolanaWalletBase
|
|||
}
|
||||
|
||||
_transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 20), (_) {
|
||||
_updateSPLTokenTransactions();
|
||||
_updateNativeSOLTransactions();
|
||||
_updateBalance();
|
||||
_updateNativeSOLTransactions();
|
||||
_updateSPLTokenTransactions();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ class SolanaWalletService extends WalletService<SolanaNewWalletCredentials,
|
|||
|
||||
await wallet.init();
|
||||
wallet.addInitialTokens();
|
||||
await wallet.save();
|
||||
return wallet;
|
||||
}
|
||||
|
||||
|
@ -46,16 +47,31 @@ 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()));
|
||||
final wallet = await SolanaWalletBase.open(
|
||||
name: name,
|
||||
password: password,
|
||||
walletInfo: walletInfo,
|
||||
);
|
||||
|
||||
await wallet.init();
|
||||
await wallet.save();
|
||||
try {
|
||||
final wallet = await SolanaWalletBase.open(
|
||||
name: name,
|
||||
password: password,
|
||||
walletInfo: walletInfo,
|
||||
);
|
||||
|
||||
return wallet;
|
||||
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());
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue