mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2024-12-22 19:49:22 +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
|
@ -82,7 +82,7 @@ abstract class EVMChainClient {
|
||||||
Future<PendingEVMChainTransaction> signTransaction({
|
Future<PendingEVMChainTransaction> signTransaction({
|
||||||
required EthPrivateKey privateKey,
|
required EthPrivateKey privateKey,
|
||||||
required String toAddress,
|
required String toAddress,
|
||||||
required String amount,
|
required BigInt amount,
|
||||||
required int gas,
|
required int gas,
|
||||||
required EVMChainTransactionPriority priority,
|
required EVMChainTransactionPriority priority,
|
||||||
required CryptoCurrency currency,
|
required CryptoCurrency currency,
|
||||||
|
@ -103,7 +103,7 @@ abstract class EVMChainClient {
|
||||||
from: privateKey.address,
|
from: privateKey.address,
|
||||||
to: EthereumAddress.fromHex(toAddress),
|
to: EthereumAddress.fromHex(toAddress),
|
||||||
maxPriorityFeePerGas: EtherAmount.fromInt(EtherUnit.gwei, priority.tip),
|
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,
|
data: data != null ? hexToBytes(data) : null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -124,7 +124,7 @@ abstract class EVMChainClient {
|
||||||
_sendTransaction = () async {
|
_sendTransaction = () async {
|
||||||
await erc20.transfer(
|
await erc20.transfer(
|
||||||
EthereumAddress.fromHex(toAddress),
|
EthereumAddress.fromHex(toAddress),
|
||||||
BigInt.parse(amount),
|
amount,
|
||||||
credentials: privateKey,
|
credentials: privateKey,
|
||||||
transaction: transaction,
|
transaction: transaction,
|
||||||
);
|
);
|
||||||
|
@ -133,7 +133,7 @@ abstract class EVMChainClient {
|
||||||
|
|
||||||
return PendingEVMChainTransaction(
|
return PendingEVMChainTransaction(
|
||||||
signedTransaction: signedTransaction,
|
signedTransaction: signedTransaction,
|
||||||
amount: amount,
|
amount: amount.toString(),
|
||||||
fee: BigInt.from(gas) * (await price).getInWei,
|
fee: BigInt.from(gas) * (await price).getInWei,
|
||||||
sendTransaction: _sendTransaction,
|
sendTransaction: _sendTransaction,
|
||||||
exponent: exponent,
|
exponent: exponent,
|
||||||
|
|
|
@ -9,3 +9,14 @@ class EVMChainTransactionCreationException implements Exception {
|
||||||
@override
|
@override
|
||||||
String toString() => exceptionMessage;
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -234,7 +234,7 @@ abstract class EVMChainWalletBase
|
||||||
final CryptoCurrency transactionCurrency =
|
final CryptoCurrency transactionCurrency =
|
||||||
balance.keys.firstWhere((element) => element.title == _credentials.currency.title);
|
balance.keys.firstWhere((element) => element.title == _credentials.currency.title);
|
||||||
|
|
||||||
final _erc20Balance = balance[transactionCurrency]!;
|
final erc20Balance = balance[transactionCurrency]!;
|
||||||
BigInt totalAmount = BigInt.zero;
|
BigInt totalAmount = BigInt.zero;
|
||||||
int exponent = transactionCurrency is Erc20Token ? transactionCurrency.decimal : 18;
|
int exponent = transactionCurrency is Erc20Token ? transactionCurrency.decimal : 18;
|
||||||
num amountToEVMChainMultiplier = pow(10, exponent);
|
num amountToEVMChainMultiplier = pow(10, exponent);
|
||||||
|
@ -249,7 +249,7 @@ abstract class EVMChainWalletBase
|
||||||
outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)));
|
outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)));
|
||||||
totalAmount = BigInt.from(totalOriginalAmount * amountToEVMChainMultiplier);
|
totalAmount = BigInt.from(totalOriginalAmount * amountToEVMChainMultiplier);
|
||||||
|
|
||||||
if (_erc20Balance.balance < totalAmount) {
|
if (erc20Balance.balance < totalAmount) {
|
||||||
throw EVMChainTransactionCreationException(transactionCurrency);
|
throw EVMChainTransactionCreationException(transactionCurrency);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -258,18 +258,27 @@ abstract class EVMChainWalletBase
|
||||||
// then no need to subtract the fees from the amount if send all
|
// then no need to subtract the fees from the amount if send all
|
||||||
final BigInt allAmount;
|
final BigInt allAmount;
|
||||||
if (transactionCurrency is Erc20Token) {
|
if (transactionCurrency is Erc20Token) {
|
||||||
allAmount = _erc20Balance.balance;
|
allAmount = erc20Balance.balance;
|
||||||
} else {
|
} else {
|
||||||
allAmount = _erc20Balance.balance -
|
final estimatedFee = BigInt.from(calculateEstimatedFee(_credentials.priority!, null));
|
||||||
BigInt.from(calculateEstimatedFee(_credentials.priority!, null));
|
|
||||||
}
|
|
||||||
final totalOriginalAmount =
|
|
||||||
EVMChainFormatter.parseEVMChainAmountToDouble(output.formattedCryptoAmount ?? 0);
|
|
||||||
totalAmount = output.sendAll
|
|
||||||
? allAmount
|
|
||||||
: BigInt.from(totalOriginalAmount * amountToEVMChainMultiplier);
|
|
||||||
|
|
||||||
if (_erc20Balance.balance < totalAmount) {
|
if (estimatedFee > erc20Balance.balance) {
|
||||||
|
throw EVMChainTransactionFeesException();
|
||||||
|
}
|
||||||
|
|
||||||
|
allAmount = erc20Balance.balance - estimatedFee;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (output.sendAll) {
|
||||||
|
totalAmount = allAmount;
|
||||||
|
} else {
|
||||||
|
final totalOriginalAmount =
|
||||||
|
EVMChainFormatter.parseEVMChainAmountToDouble(output.formattedCryptoAmount ?? 0);
|
||||||
|
|
||||||
|
totalAmount = BigInt.from(totalOriginalAmount * amountToEVMChainMultiplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (erc20Balance.balance < totalAmount) {
|
||||||
throw EVMChainTransactionCreationException(transactionCurrency);
|
throw EVMChainTransactionCreationException(transactionCurrency);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -279,7 +288,7 @@ abstract class EVMChainWalletBase
|
||||||
toAddress: _credentials.outputs.first.isParsedAddress
|
toAddress: _credentials.outputs.first.isParsedAddress
|
||||||
? _credentials.outputs.first.extractedAddress!
|
? _credentials.outputs.first.extractedAddress!
|
||||||
: _credentials.outputs.first.address,
|
: _credentials.outputs.first.address,
|
||||||
amount: totalAmount.toString(),
|
amount: totalAmount,
|
||||||
gas: _estimatedGas!,
|
gas: _estimatedGas!,
|
||||||
priority: _credentials.priority!,
|
priority: _credentials.priority!,
|
||||||
currency: transactionCurrency,
|
currency: transactionCurrency,
|
||||||
|
|
|
@ -96,16 +96,30 @@ class SolanaWalletClient {
|
||||||
return SolanaBalance(totalBalance);
|
return SolanaBalance(totalBalance);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<double> getGasForMessage(String message) async {
|
Future<double> getFeeForMessage(String message, Commitment commitment) async {
|
||||||
try {
|
try {
|
||||||
final gasPrice = await _client!.rpcClient.getFeeForMessage(message) ?? 0;
|
final feeForMessage =
|
||||||
final fee = gasPrice / lamportsPerSol;
|
await _client!.rpcClient.getFeeForMessage(message, commitment: commitment);
|
||||||
|
final fee = (feeForMessage ?? 0.0) / lamportsPerSol;
|
||||||
return fee;
|
return fee;
|
||||||
} catch (_) {
|
} 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
|
/// Load the Address's transactions into the account
|
||||||
Future<List<SolanaTransactionModel>> fetchTransactions(
|
Future<List<SolanaTransactionModel>> fetchTransactions(
|
||||||
Ed25519HDPublicKey publicKey, {
|
Ed25519HDPublicKey publicKey, {
|
||||||
|
@ -257,24 +271,15 @@ class SolanaWalletClient {
|
||||||
Future<PendingSolanaTransaction> signSolanaTransaction({
|
Future<PendingSolanaTransaction> signSolanaTransaction({
|
||||||
required String tokenTitle,
|
required String tokenTitle,
|
||||||
required int tokenDecimals,
|
required int tokenDecimals,
|
||||||
String? tokenMint,
|
|
||||||
required double inputAmount,
|
required double inputAmount,
|
||||||
required String destinationAddress,
|
required String destinationAddress,
|
||||||
required Ed25519HDKeyPair ownerKeypair,
|
required Ed25519HDKeyPair ownerKeypair,
|
||||||
|
required bool isSendAll,
|
||||||
|
String? tokenMint,
|
||||||
List<String> references = const [],
|
List<String> references = const [],
|
||||||
}) async {
|
}) async {
|
||||||
const commitment = Commitment.confirmed;
|
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) {
|
if (tokenTitle == CryptoCurrency.sol.title) {
|
||||||
final pendingNativeTokenTransaction = await _signNativeTokenTransaction(
|
final pendingNativeTokenTransaction = await _signNativeTokenTransaction(
|
||||||
tokenTitle: tokenTitle,
|
tokenTitle: tokenTitle,
|
||||||
|
@ -282,8 +287,8 @@ class SolanaWalletClient {
|
||||||
inputAmount: inputAmount,
|
inputAmount: inputAmount,
|
||||||
destinationAddress: destinationAddress,
|
destinationAddress: destinationAddress,
|
||||||
ownerKeypair: ownerKeypair,
|
ownerKeypair: ownerKeypair,
|
||||||
recentBlockhash: recentBlockhash,
|
|
||||||
commitment: commitment,
|
commitment: commitment,
|
||||||
|
isSendAll: isSendAll,
|
||||||
);
|
);
|
||||||
return pendingNativeTokenTransaction;
|
return pendingNativeTokenTransaction;
|
||||||
} else {
|
} else {
|
||||||
|
@ -294,25 +299,29 @@ class SolanaWalletClient {
|
||||||
inputAmount: inputAmount,
|
inputAmount: inputAmount,
|
||||||
destinationAddress: destinationAddress,
|
destinationAddress: destinationAddress,
|
||||||
ownerKeypair: ownerKeypair,
|
ownerKeypair: ownerKeypair,
|
||||||
recentBlockhash: recentBlockhash,
|
|
||||||
commitment: commitment,
|
commitment: commitment,
|
||||||
);
|
);
|
||||||
return pendingSPLTokenTransaction;
|
return pendingSPLTokenTransaction;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<PendingSolanaTransaction> _signNativeTokenTransaction({
|
Future<RecentBlockhash> _getRecentBlockhash(Commitment commitment) async {
|
||||||
required String tokenTitle,
|
final latestBlockhash =
|
||||||
required int tokenDecimals,
|
await _client!.rpcClient.getLatestBlockhash(commitment: commitment).value;
|
||||||
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();
|
|
||||||
|
|
||||||
|
final recentBlockhash = RecentBlockhash(
|
||||||
|
blockhash: latestBlockhash.blockhash,
|
||||||
|
feeCalculator: const FeeCalculator(lamportsPerSignature: 500),
|
||||||
|
);
|
||||||
|
|
||||||
|
return recentBlockhash;
|
||||||
|
}
|
||||||
|
|
||||||
|
Message _getMessageForNativeTransaction(
|
||||||
|
Ed25519HDKeyPair ownerKeypair,
|
||||||
|
String destinationAddress,
|
||||||
|
int lamports,
|
||||||
|
) {
|
||||||
final instructions = [
|
final instructions = [
|
||||||
SystemInstruction.transfer(
|
SystemInstruction.transfer(
|
||||||
fundingAccount: ownerKeypair.publicKey,
|
fundingAccount: ownerKeypair.publicKey,
|
||||||
|
@ -322,21 +331,75 @@ class SolanaWalletClient {
|
||||||
];
|
];
|
||||||
|
|
||||||
final message = Message(instructions: instructions);
|
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 signers = [ownerKeypair];
|
||||||
|
|
||||||
final signedTx = await _signTransactionInternal(
|
RecentBlockhash recentBlockhash = await _getRecentBlockhash(commitment);
|
||||||
message: message,
|
|
||||||
signers: signers,
|
|
||||||
commitment: commitment,
|
|
||||||
recentBlockhash: recentBlockhash,
|
|
||||||
);
|
|
||||||
|
|
||||||
final fee = await _getFeeFromCompiledMessage(
|
final fee = await _getFeeFromCompiledMessage(
|
||||||
message,
|
message,
|
||||||
recentBlockhash,
|
|
||||||
signers.first.publicKey,
|
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(
|
sendTx() async => await sendTransaction(
|
||||||
signedTransaction: signedTx,
|
signedTransaction: signedTx,
|
||||||
commitment: commitment,
|
commitment: commitment,
|
||||||
|
@ -360,7 +423,6 @@ class SolanaWalletClient {
|
||||||
required double inputAmount,
|
required double inputAmount,
|
||||||
required String destinationAddress,
|
required String destinationAddress,
|
||||||
required Ed25519HDKeyPair ownerKeypair,
|
required Ed25519HDKeyPair ownerKeypair,
|
||||||
required RecentBlockhash recentBlockhash,
|
|
||||||
required Commitment commitment,
|
required Commitment commitment,
|
||||||
}) async {
|
}) async {
|
||||||
final destinationOwner = Ed25519HDPublicKey.fromBase58(destinationAddress);
|
final destinationOwner = Ed25519HDPublicKey.fromBase58(destinationAddress);
|
||||||
|
@ -408,8 +470,18 @@ class SolanaWalletClient {
|
||||||
);
|
);
|
||||||
|
|
||||||
final message = Message(instructions: [instruction]);
|
final message = Message(instructions: [instruction]);
|
||||||
|
|
||||||
final signers = [ownerKeypair];
|
final signers = [ownerKeypair];
|
||||||
|
|
||||||
|
RecentBlockhash recentBlockhash = await _getRecentBlockhash(commitment);
|
||||||
|
|
||||||
|
final fee = await _getFeeFromCompiledMessage(
|
||||||
|
message,
|
||||||
|
signers.first.publicKey,
|
||||||
|
recentBlockhash,
|
||||||
|
commitment,
|
||||||
|
);
|
||||||
|
|
||||||
final signedTx = await _signTransactionInternal(
|
final signedTx = await _signTransactionInternal(
|
||||||
message: message,
|
message: message,
|
||||||
signers: signers,
|
signers: signers,
|
||||||
|
@ -417,12 +489,6 @@ class SolanaWalletClient {
|
||||||
recentBlockhash: recentBlockhash,
|
recentBlockhash: recentBlockhash,
|
||||||
);
|
);
|
||||||
|
|
||||||
final fee = await _getFeeFromCompiledMessage(
|
|
||||||
message,
|
|
||||||
recentBlockhash,
|
|
||||||
signers.first.publicKey,
|
|
||||||
);
|
|
||||||
|
|
||||||
sendTx() async => await sendTransaction(
|
sendTx() async => await sendTransaction(
|
||||||
signedTransaction: signedTx,
|
signedTransaction: signedTx,
|
||||||
commitment: commitment,
|
commitment: commitment,
|
||||||
|
@ -438,19 +504,6 @@ class SolanaWalletClient {
|
||||||
return pendingTransaction;
|
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({
|
Future<SignedTx> _signTransactionInternal({
|
||||||
required Message message,
|
required Message message,
|
||||||
required List<Ed25519HDKeyPair> signers,
|
required List<Ed25519HDKeyPair> signers,
|
||||||
|
@ -466,13 +519,18 @@ class SolanaWalletClient {
|
||||||
required SignedTx signedTransaction,
|
required SignedTx signedTransaction,
|
||||||
required Commitment commitment,
|
required Commitment commitment,
|
||||||
}) async {
|
}) async {
|
||||||
final signature = await _client!.rpcClient.sendTransaction(
|
try {
|
||||||
signedTransaction.encode(),
|
final signature = await _client!.rpcClient.sendTransaction(
|
||||||
preflightCommitment: commitment,
|
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;
|
late SolanaWalletClient _client;
|
||||||
|
|
||||||
|
@observable
|
||||||
|
double? estimatedFee;
|
||||||
|
|
||||||
Timer? _transactionsUpdateTimer;
|
Timer? _transactionsUpdateTimer;
|
||||||
|
|
||||||
late final Box<SPLToken> splTokensBox;
|
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
|
@override
|
||||||
Future<PendingTransaction> createTransaction(Object credentials) async {
|
Future<PendingTransaction> createTransaction(Object credentials) async {
|
||||||
final solCredentials = credentials as SolanaTransactionCredentials;
|
final solCredentials = credentials as SolanaTransactionCredentials;
|
||||||
|
@ -188,6 +199,8 @@ abstract class SolanaWalletBase
|
||||||
|
|
||||||
double totalAmount = 0.0;
|
double totalAmount = 0.0;
|
||||||
|
|
||||||
|
bool isSendAll = false;
|
||||||
|
|
||||||
if (hasMultiDestination) {
|
if (hasMultiDestination) {
|
||||||
if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) {
|
if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) {
|
||||||
throw SolanaTransactionWrongBalanceException(transactionCurrency);
|
throw SolanaTransactionWrongBalanceException(transactionCurrency);
|
||||||
|
@ -204,9 +217,15 @@ abstract class SolanaWalletBase
|
||||||
} else {
|
} else {
|
||||||
final output = outputs.first;
|
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) {
|
if (walletBalanceForCurrency < totalAmount) {
|
||||||
throw SolanaTransactionWrongBalanceException(transactionCurrency);
|
throw SolanaTransactionWrongBalanceException(transactionCurrency);
|
||||||
|
@ -228,6 +247,7 @@ abstract class SolanaWalletBase
|
||||||
destinationAddress: solCredentials.outputs.first.isParsedAddress
|
destinationAddress: solCredentials.outputs.first.isParsedAddress
|
||||||
? solCredentials.outputs.first.extractedAddress!
|
? solCredentials.outputs.first.extractedAddress!
|
||||||
: solCredentials.outputs.first.address,
|
: solCredentials.outputs.first.address,
|
||||||
|
isSendAll: isSendAll,
|
||||||
);
|
);
|
||||||
|
|
||||||
return pendingSolanaTransaction;
|
return pendingSolanaTransaction;
|
||||||
|
@ -269,7 +289,10 @@ abstract class SolanaWalletBase
|
||||||
Future<void> _updateSPLTokenTransactions() async {
|
Future<void> _updateSPLTokenTransactions() async {
|
||||||
List<SolanaTransactionModel> splTokenTransactions = [];
|
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) {
|
if (token is SPLToken) {
|
||||||
final tokenTxs = await _client.getSPLTokenTransfers(
|
final tokenTxs = await _client.getSPLTokenTransfers(
|
||||||
token.mintAddress,
|
token.mintAddress,
|
||||||
|
@ -326,6 +349,7 @@ abstract class SolanaWalletBase
|
||||||
_updateBalance(),
|
_updateBalance(),
|
||||||
_updateNativeSOLTransactions(),
|
_updateNativeSOLTransactions(),
|
||||||
_updateSPLTokenTransactions(),
|
_updateSPLTokenTransactions(),
|
||||||
|
_getEstimatedFees(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
syncStatus = SyncedSyncStatus();
|
syncStatus = SyncedSyncStatus();
|
||||||
|
@ -433,18 +457,22 @@ abstract class SolanaWalletBase
|
||||||
final mintPublicKey = Ed25519HDPublicKey.fromBase58(mintAddress);
|
final mintPublicKey = Ed25519HDPublicKey.fromBase58(mintAddress);
|
||||||
|
|
||||||
// Fetch token's metadata account
|
// 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 null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return SPLToken.fromMetadata(
|
|
||||||
name: token.name,
|
|
||||||
mint: token.mint,
|
|
||||||
symbol: token.symbol,
|
|
||||||
mintAddress: mintAddress,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -475,9 +503,9 @@ abstract class SolanaWalletBase
|
||||||
}
|
}
|
||||||
|
|
||||||
_transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 20), (_) {
|
_transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 20), (_) {
|
||||||
_updateSPLTokenTransactions();
|
|
||||||
_updateNativeSOLTransactions();
|
|
||||||
_updateBalance();
|
_updateBalance();
|
||||||
|
_updateNativeSOLTransactions();
|
||||||
|
_updateSPLTokenTransactions();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,7 @@ class SolanaWalletService extends WalletService<SolanaNewWalletCredentials,
|
||||||
|
|
||||||
await wallet.init();
|
await wallet.init();
|
||||||
wallet.addInitialTokens();
|
wallet.addInitialTokens();
|
||||||
|
await wallet.save();
|
||||||
return wallet;
|
return wallet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,16 +47,31 @@ class SolanaWalletService extends WalletService<SolanaNewWalletCredentials,
|
||||||
Future<SolanaWallet> openWallet(String name, String password) async {
|
Future<SolanaWallet> openWallet(String name, String password) async {
|
||||||
final walletInfo =
|
final walletInfo =
|
||||||
walletInfoSource.values.firstWhere((info) => info.id == WalletBase.idFor(name, getType()));
|
walletInfoSource.values.firstWhere((info) => info.id == WalletBase.idFor(name, getType()));
|
||||||
final wallet = await SolanaWalletBase.open(
|
|
||||||
name: name,
|
|
||||||
password: password,
|
|
||||||
walletInfo: walletInfo,
|
|
||||||
);
|
|
||||||
|
|
||||||
await wallet.init();
|
try {
|
||||||
await wallet.save();
|
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
|
@override
|
||||||
|
@ -110,6 +126,7 @@ class SolanaWalletService extends WalletService<SolanaNewWalletCredentials,
|
||||||
password: password, name: currentName, walletInfo: currentWalletInfo);
|
password: password, name: currentName, walletInfo: currentWalletInfo);
|
||||||
|
|
||||||
await currentWallet.renameWalletFiles(newName);
|
await currentWallet.renameWalletFiles(newName);
|
||||||
|
await saveBackup(newName);
|
||||||
|
|
||||||
final newWalletInfo = currentWalletInfo;
|
final newWalletInfo = currentWalletInfo;
|
||||||
newWalletInfo.id = WalletBase.idFor(newName, getType());
|
newWalletInfo.id = WalletBase.idFor(newName, getType());
|
||||||
|
|
|
@ -74,8 +74,22 @@ class CWSolana extends Solana {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> addSPLToken(WalletBase wallet, CryptoCurrency token) async =>
|
Future<void> addSPLToken(
|
||||||
await (wallet as SolanaWallet).addSPLToken(token as SPLToken);
|
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
|
@override
|
||||||
Future<void> deleteSPLToken(WalletBase wallet, CryptoCurrency token) async =>
|
Future<void> deleteSPLToken(WalletBase wallet, CryptoCurrency token) async =>
|
||||||
|
@ -115,4 +129,9 @@ class CWSolana extends Solana {
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
double? getEstimateFees(WalletBase wallet) {
|
||||||
|
return (wallet as SolanaWallet).estimatedFee;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -195,12 +195,14 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
if (_formKey.currentState!.validate() &&
|
if (_formKey.currentState!.validate() &&
|
||||||
(!_showDisclaimer || _disclaimerChecked)) {
|
(!_showDisclaimer || _disclaimerChecked)) {
|
||||||
await widget.homeSettingsViewModel.addToken(Erc20Token(
|
await widget.homeSettingsViewModel.addToken(
|
||||||
name: _tokenNameController.text,
|
token: CryptoCurrency(
|
||||||
symbol: _tokenSymbolController.text,
|
name: _tokenNameController.text,
|
||||||
|
title: _tokenSymbolController.text.toUpperCase(),
|
||||||
|
decimals: int.parse(_tokenDecimalController.text),
|
||||||
|
),
|
||||||
contractAddress: _contractAddressController.text,
|
contractAddress: _contractAddressController.text,
|
||||||
decimal: int.parse(_tokenDecimalController.text),
|
);
|
||||||
));
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
}
|
}
|
||||||
|
|
|
@ -323,8 +323,7 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
|
||||||
? sendViewModel.allAmountValidator
|
? sendViewModel.allAmountValidator
|
||||||
: sendViewModel.amountValidator,
|
: sendViewModel.amountValidator,
|
||||||
),
|
),
|
||||||
if (!sendViewModel.isBatchSending &&
|
if (!sendViewModel.isBatchSending)
|
||||||
sendViewModel.shouldDisplaySendALL)
|
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 2,
|
top: 2,
|
||||||
right: 0,
|
right: 0,
|
||||||
|
@ -456,7 +455,9 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
|
||||||
if (sendViewModel.hasFees)
|
if (sendViewModel.hasFees)
|
||||||
Observer(
|
Observer(
|
||||||
builder: (_) => GestureDetector(
|
builder: (_) => GestureDetector(
|
||||||
onTap: () => _setTransactionPriority(context),
|
onTap: sendViewModel.hasFeesPriority
|
||||||
|
? () => _setTransactionPriority(context)
|
||||||
|
: () {},
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: EdgeInsets.only(top: 24),
|
padding: EdgeInsets.only(top: 24),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
|
|
@ -44,17 +44,37 @@ abstract class HomeSettingsViewModelBase with Store {
|
||||||
@action
|
@action
|
||||||
void setPinNativeToken(bool value) => _settingsStore.pinNativeTokenAtTop = value;
|
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) {
|
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) {
|
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) {
|
if (_balanceViewModel.wallet.type == WalletType.solana) {
|
||||||
await solana!.addSPLToken(_balanceViewModel.wallet, token);
|
await solana!.addSPLToken(
|
||||||
|
_balanceViewModel.wallet,
|
||||||
|
token,
|
||||||
|
contractAddress,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateTokensList();
|
_updateTokensList();
|
||||||
|
@ -117,7 +137,8 @@ abstract class HomeSettingsViewModelBase with Store {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_balanceViewModel.wallet.type == WalletType.solana) {
|
if (_balanceViewModel.wallet.type == WalletType.solana) {
|
||||||
solana!.addSPLToken(_balanceViewModel.wallet, token);
|
final address = solana!.getTokenAddress(token);
|
||||||
|
solana!.addSPLToken(_balanceViewModel.wallet, token, address);
|
||||||
}
|
}
|
||||||
|
|
||||||
_refreshTokensList();
|
_refreshTokensList();
|
||||||
|
|
|
@ -6,6 +6,7 @@ import 'package:cake_wallet/ethereum/ethereum.dart';
|
||||||
import 'package:cake_wallet/haven/haven.dart';
|
import 'package:cake_wallet/haven/haven.dart';
|
||||||
import 'package:cake_wallet/polygon/polygon.dart';
|
import 'package:cake_wallet/polygon/polygon.dart';
|
||||||
import 'package:cake_wallet/reactions/wallet_connect.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:cake_wallet/src/screens/send/widgets/extract_address_from_parsed.dart';
|
||||||
import 'package:cw_core/crypto_currency.dart';
|
import 'package:cw_core/crypto_currency.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
@ -116,6 +117,10 @@ abstract class OutputBase with Store {
|
||||||
@computed
|
@computed
|
||||||
double get estimatedFee {
|
double get estimatedFee {
|
||||||
try {
|
try {
|
||||||
|
if (_wallet.type == WalletType.solana) {
|
||||||
|
return solana!.getEstimateFees(_wallet) ?? 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
final fee = _wallet.calculateEstimatedFee(
|
final fee = _wallet.calculateEstimatedFee(
|
||||||
_settingsStore.priority[_wallet.type]!, formattedCryptoAmount);
|
_settingsStore.priority[_wallet.type]!, formattedCryptoAmount);
|
||||||
|
|
||||||
|
|
|
@ -106,8 +106,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||||
@computed
|
@computed
|
||||||
bool get isBatchSending => outputs.length > 1;
|
bool get isBatchSending => outputs.length > 1;
|
||||||
|
|
||||||
bool get shouldDisplaySendALL => walletType != WalletType.solana;
|
|
||||||
|
|
||||||
@computed
|
@computed
|
||||||
String get pendingTransactionFiatAmount {
|
String get pendingTransactionFiatAmount {
|
||||||
if (pendingTransaction == null) {
|
if (pendingTransaction == null) {
|
||||||
|
@ -208,6 +206,11 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||||
@computed
|
@computed
|
||||||
bool get hasFees => wallet.type != WalletType.nano && wallet.type != WalletType.banano;
|
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
|
@observable
|
||||||
CryptoCurrency selectedCryptoCurrency;
|
CryptoCurrency selectedCryptoCurrency;
|
||||||
|
|
||||||
|
|
|
@ -948,7 +948,11 @@ abstract class Solana {
|
||||||
required CryptoCurrency currency,
|
required CryptoCurrency currency,
|
||||||
});
|
});
|
||||||
List<CryptoCurrency> getSPLTokenCurrencies(WalletBase wallet);
|
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<void> deleteSPLToken(WalletBase wallet, CryptoCurrency token);
|
||||||
Future<CryptoCurrency?> getSPLToken(WalletBase wallet, String contractAddress);
|
Future<CryptoCurrency?> getSPLToken(WalletBase wallet, String contractAddress);
|
||||||
|
|
||||||
|
@ -956,6 +960,7 @@ abstract class Solana {
|
||||||
double getTransactionAmountRaw(TransactionInfo transactionInfo);
|
double getTransactionAmountRaw(TransactionInfo transactionInfo);
|
||||||
String getTokenAddress(CryptoCurrency asset);
|
String getTokenAddress(CryptoCurrency asset);
|
||||||
List<int>? getValidationLength(CryptoCurrency type);
|
List<int>? getValidationLength(CryptoCurrency type);
|
||||||
|
double? getEstimateFees(WalletBase wallet);
|
||||||
}
|
}
|
||||||
|
|
||||||
""";
|
""";
|
||||||
|
|
Loading…
Reference in a new issue