CW-829 Solana Enhancements (#1858)

* feat: Solana enhancements with rent handling for accounts

* fix: Add exception classes with handled error messages to ensure proper error handling process

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
This commit is contained in:
David Adegoke 2024-12-14 00:31:10 +01:00 committed by GitHub
parent 9a60b0146f
commit c620d7f486
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 206 additions and 18 deletions

View file

@ -1,12 +1,13 @@
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'dart:math' as math;
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/node.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_solana/pending_solana_transaction.dart';
import 'package:cw_solana/solana_balance.dart';
import 'package:cw_solana/solana_exceptions.dart';
import 'package:cw_solana/solana_transaction_model.dart';
import 'package:http/http.dart' as http;
import 'package:solana/dto.dart';
@ -180,7 +181,7 @@ class SolanaWalletClient {
bool isOutgoingTx = transfer.source == publicKey.toBase58();
double amount = (double.tryParse(transfer.amount) ?? 0.0) /
pow(10, splTokenDecimal ?? 9);
math.pow(10, splTokenDecimal ?? 9);
transactions.add(
SolanaTransactionModel(
@ -276,6 +277,7 @@ class SolanaWalletClient {
required String destinationAddress,
required Ed25519HDKeyPair ownerKeypair,
required bool isSendAll,
required double solBalance,
String? tokenMint,
List<String> references = const [],
}) async {
@ -290,6 +292,7 @@ class SolanaWalletClient {
ownerKeypair: ownerKeypair,
commitment: commitment,
isSendAll: isSendAll,
solBalance: solBalance,
);
return pendingNativeTokenTransaction;
} else {
@ -301,6 +304,7 @@ class SolanaWalletClient {
destinationAddress: destinationAddress,
ownerKeypair: ownerKeypair,
commitment: commitment,
solBalance: solBalance,
);
return pendingSPLTokenTransaction;
}
@ -353,6 +357,23 @@ class SolanaWalletClient {
return fee;
}
Future<bool> hasSufficientFundsLeftForRent({
required double inputAmount,
required double solBalance,
required double fee,
}) async {
final rent =
await _client!.getMinimumBalanceForMintRentExemption(commitment: Commitment.confirmed);
final rentInSol = (rent / lamportsPerSol).toDouble();
final remnant = solBalance - (inputAmount + fee);
if (remnant > rentInSol) return true;
return false;
}
Future<PendingSolanaTransaction> _signNativeTokenTransaction({
required String tokenTitle,
required int tokenDecimals,
@ -361,6 +382,7 @@ class SolanaWalletClient {
required Ed25519HDKeyPair ownerKeypair,
required Commitment commitment,
required bool isSendAll,
required double solBalance,
}) async {
// Convert SOL to lamport
int lamports = (inputAmount * lamportsPerSol).toInt();
@ -378,6 +400,16 @@ class SolanaWalletClient {
commitment,
);
bool hasSufficientFundsLeft = await hasSufficientFundsLeftForRent(
inputAmount: inputAmount,
fee: fee,
solBalance: solBalance,
);
if (!hasSufficientFundsLeft) {
throw SolanaSignNativeTokenTransactionRentException();
}
SignedTx signedTx;
if (isSendAll) {
final feeInLamports = (fee * lamportsPerSol).toInt();
@ -425,6 +457,7 @@ class SolanaWalletClient {
required String destinationAddress,
required Ed25519HDKeyPair ownerKeypair,
required Commitment commitment,
required double solBalance,
}) async {
final destinationOwner = Ed25519HDPublicKey.fromBase58(destinationAddress);
final mint = Ed25519HDPublicKey.fromBase58(tokenMint);
@ -447,7 +480,7 @@ class SolanaWalletClient {
// Throw an appropriate exception if the sender has no associated
// token account
if (associatedSenderAccount == null) {
throw NoAssociatedTokenAccountException(ownerKeypair.address, mint.toBase58());
throw SolanaNoAssociatedTokenAccountException(ownerKeypair.address, mint.toBase58());
}
try {
@ -457,11 +490,11 @@ class SolanaWalletClient {
funder: ownerKeypair,
);
} catch (e) {
throw Exception('Insufficient SOL balance to complete this transaction: ${e.toString()}');
throw SolanaCreateAssociatedTokenAccountException(e.toString());
}
// Input by the user
final amount = (inputAmount * pow(10, tokenDecimals)).toInt();
final amount = (inputAmount * math.pow(10, tokenDecimals)).toInt();
final instruction = TokenInstruction.transfer(
source: Ed25519HDPublicKey.fromBase58(associatedSenderAccount.pubkey),
@ -483,6 +516,16 @@ class SolanaWalletClient {
commitment,
);
bool hasSufficientFundsLeft = await hasSufficientFundsLeftForRent(
inputAmount: inputAmount,
fee: fee,
solBalance: solBalance,
);
if (!hasSufficientFundsLeft) {
throw SolanaSignSPLTokenTransactionRentException();
}
final signedTx = await _signTransactionInternal(
message: message,
signers: signers,

View file

@ -19,3 +19,20 @@ class SolanaTransactionWrongBalanceException implements Exception {
@override
String toString() => exceptionMessage;
}
class SolanaSignNativeTokenTransactionRentException implements Exception {}
class SolanaCreateAssociatedTokenAccountException implements Exception {
final String exceptionMessage;
SolanaCreateAssociatedTokenAccountException(this.exceptionMessage);
}
class SolanaSignSPLTokenTransactionRentException implements Exception {}
class SolanaNoAssociatedTokenAccountException implements Exception {
const SolanaNoAssociatedTokenAccountException(this.account, this.mint);
final String account;
final String mint;
}

View file

@ -228,6 +228,8 @@ abstract class SolanaWalletBase
final walletBalanceForCurrency = balance[transactionCurrency]!.balance;
final solBalance = balance[CryptoCurrency.sol]!.balance;
double totalAmount = 0.0;
bool isSendAll = false;
@ -279,6 +281,7 @@ abstract class SolanaWalletBase
? solCredentials.outputs.first.extractedAddress!
: solCredentials.outputs.first.address,
isSendAll: isSendAll,
solBalance: solBalance,
);
return pendingSolanaTransaction;