2024-04-29 16:19:01 +00:00
import ' dart:convert ' ;
2024-03-21 18:57:27 +00:00
import ' dart:io ' ;
2024-03-20 00:50:42 +00:00
import ' dart:math ' ;
2024-04-29 17:01:26 +00:00
import ' package:decimal/decimal.dart ' ;
2024-03-20 00:50:42 +00:00
import ' package:isar/isar.dart ' ;
2024-04-24 03:03:46 +00:00
import ' package:socks5_proxy/socks_client.dart ' ;
2024-03-20 00:50:42 +00:00
import ' package:solana/dto.dart ' ;
import ' package:solana/solana.dart ' ;
2024-03-21 18:57:27 +00:00
import ' package:stackwallet/models/balance.dart ' ;
import ' package:stackwallet/models/isar/models/blockchain_data/transaction.dart '
as isar ;
2024-03-20 00:50:42 +00:00
import ' package:stackwallet/models/isar/models/isar_models.dart ' ;
2024-03-21 18:57:27 +00:00
import ' package:stackwallet/models/node_model.dart ' ;
2024-03-20 00:50:42 +00:00
import ' package:stackwallet/models/paymint/fee_object_model.dart ' ;
2024-03-21 18:57:27 +00:00
import ' package:stackwallet/services/node_service.dart ' ;
import ' package:stackwallet/services/tor_service.dart ' ;
2024-03-20 00:50:42 +00:00
import ' package:stackwallet/utilities/amount/amount.dart ' ;
import ' package:stackwallet/utilities/default_nodes.dart ' ;
import ' package:stackwallet/utilities/enums/coin_enum.dart ' ;
2024-03-21 18:57:27 +00:00
import ' package:stackwallet/utilities/logger.dart ' ;
2024-03-20 00:50:42 +00:00
import ' package:stackwallet/wallets/crypto_currency/coins/solana.dart ' ;
import ' package:stackwallet/wallets/crypto_currency/crypto_currency.dart ' ;
import ' package:stackwallet/wallets/models/tx_data.dart ' ;
import ' package:stackwallet/wallets/wallet/intermediate/bip39_wallet.dart ' ;
import ' package:tuple/tuple.dart ' ;
class SolanaWallet extends Bip39Wallet < Solana > {
SolanaWallet ( CryptoCurrencyNetwork network ) : super ( Solana ( network ) ) ;
NodeModel ? _solNode ;
2024-04-24 15:36:27 +00:00
RpcClient ? _rpcClient ; // The Solana RpcClient.
2024-03-21 18:57:27 +00:00
2024-03-20 00:50:42 +00:00
Future < Ed25519HDKeyPair > _getKeyPair ( ) async {
2024-03-21 18:57:27 +00:00
return Ed25519HDKeyPair . fromMnemonic ( await getMnemonic ( ) ,
account: 0 , change: 0 ) ;
2024-03-20 00:50:42 +00:00
}
Future < Address > _getCurrentAddress ( ) async {
2024-04-24 15:47:35 +00:00
final addressStruct = Address (
2024-03-20 00:50:42 +00:00
walletId: walletId ,
value: ( await _getKeyPair ( ) ) . address ,
publicKey: List < int > . empty ( ) ,
derivationIndex: 0 ,
2024-03-21 19:38:59 +00:00
derivationPath: DerivationPath ( ) . . value = " m/44'/501'/0'/0' " ,
2024-03-20 00:50:42 +00:00
type: cryptoCurrency . coin . primaryAddressType ,
subType: AddressSubType . unknown ) ;
return addressStruct ;
}
Future < int > _getCurrentBalanceInLamports ( ) async {
2024-04-24 15:36:27 +00:00
_checkClient ( ) ;
2024-04-24 15:47:35 +00:00
final balance = await _rpcClient ? . getBalance ( ( await _getKeyPair ( ) ) . address ) ;
2024-03-21 18:57:27 +00:00
return balance ! . value ;
2024-03-20 00:50:42 +00:00
}
2024-04-29 17:01:26 +00:00
2024-04-29 16:19:01 +00:00
Future < int ? > _getEstimatedNetworkFee ( Amount transferAmount ) async {
final latestBlockhash = await _rpcClient ? . getLatestBlockhash ( ) ;
2024-04-29 17:01:26 +00:00
final pubKey = ( await _getKeyPair ( ) ) . publicKey ;
2024-04-29 16:19:01 +00:00
final compiledMessage = Message ( instructions: [
SystemInstruction . transfer (
2024-04-29 17:01:26 +00:00
fundingAccount: pubKey ,
recipientAccount: pubKey ,
lamports: transferAmount . raw . toInt ( ) ,
2024-04-29 21:18:54 +00:00
)
2024-04-29 17:19:09 +00:00
] ) . compile ( recentBlockhash: latestBlockhash ! . value . blockhash , feePayer: ( await _getKeyPair ( ) ) . publicKey ) ;
2024-04-29 16:19:01 +00:00
return await _rpcClient ? . getFeeForMessage (
base64Encode ( compiledMessage . toByteArray ( ) . toList ( ) ) ,
) ;
}
2024-03-20 00:50:42 +00:00
@ override
FilterOperation ? get changeAddressFilterOperation = >
throw UnimplementedError ( ) ;
@ override
Future < void > checkSaveInitialReceivingAddress ( ) async {
try {
2024-04-24 15:47:35 +00:00
final address = ( await _getKeyPair ( ) ) . address ;
2024-03-20 00:50:42 +00:00
await mainDB . updateOrPutAddresses ( [
Address (
walletId: walletId ,
value: address ,
publicKey: List < int > . empty ( ) ,
derivationIndex: 0 ,
2024-03-21 19:38:59 +00:00
derivationPath: DerivationPath ( ) . . value = " m/44'/501'/0'/0' " ,
2024-03-20 00:50:42 +00:00
type: cryptoCurrency . coin . primaryAddressType ,
subType: AddressSubType . unknown )
] ) ;
} catch ( e , s ) {
Logging . instance . log (
" $ runtimeType checkSaveInitialReceivingAddress() failed: $ e \n $ s " ,
level: LogLevel . Error ,
) ;
}
}
@ override
Future < TxData > prepareSend ( { required TxData txData } ) async {
try {
2024-04-24 15:36:27 +00:00
_checkClient ( ) ;
2024-03-21 18:57:27 +00:00
2024-03-20 00:50:42 +00:00
if ( txData . recipients = = null | | txData . recipients ! . length ! = 1 ) {
throw Exception ( " $ runtimeType prepareSend requires 1 recipient " ) ;
}
2024-04-24 15:47:35 +00:00
final Amount sendAmount = txData . amount ! ;
2024-03-20 00:50:42 +00:00
if ( sendAmount > info . cachedBalance . spendable ) {
throw Exception ( " Insufficient available balance " ) ;
}
2024-04-29 17:19:09 +00:00
final feeAmount = await _getEstimatedNetworkFee ( sendAmount ) ;
if ( feeAmount = = null ) {
throw Exception (
" Failed to get fees, please check your node connection. " ) ;
2024-03-20 00:50:42 +00:00
}
// Rent exemption of Solana
2024-03-21 18:57:27 +00:00
final accInfo =
2024-04-24 15:36:27 +00:00
await _rpcClient ? . getAccountInfo ( ( await _getKeyPair ( ) ) . address ) ;
2024-04-24 15:47:35 +00:00
final int minimumRent =
await _rpcClient ? . getMinimumBalanceForRentExemption (
accInfo ! . value ! . data . toString ( ) . length ) ? ?
0 ; // TODO revisit null condition.
2024-03-21 18:57:27 +00:00
if ( minimumRent >
( ( await _getCurrentBalanceInLamports ( ) ) -
txData . amount ! . raw . toInt ( ) -
feeAmount ) ) {
throw Exception (
" Insufficient remaining balance for rent exemption, minimum rent: ${ minimumRent / pow ( 10 , cryptoCurrency . fractionDigits ) } " ) ;
2024-03-20 00:50:42 +00:00
}
return txData . copyWith (
fee: Amount (
rawValue: BigInt . from ( feeAmount ) ,
fractionDigits: cryptoCurrency . fractionDigits ,
) ,
) ;
} catch ( e , s ) {
Logging . instance . log (
" $ runtimeType Solana prepareSend failed: $ e \n $ s " ,
level: LogLevel . Error ,
) ;
rethrow ;
}
}
@ override
Future < TxData > confirmSend ( { required TxData txData } ) async {
try {
2024-04-24 15:36:27 +00:00
_checkClient ( ) ;
2024-03-21 18:57:27 +00:00
2024-03-20 00:50:42 +00:00
final keyPair = await _getKeyPair ( ) ;
2024-04-24 15:47:35 +00:00
final recipientAccount = txData . recipients ! . first ;
final recipientPubKey =
2024-03-21 18:57:27 +00:00
Ed25519HDPublicKey . fromBase58 ( recipientAccount . address ) ;
2024-03-20 00:50:42 +00:00
final message = Message (
instructions: [
2024-03-21 18:57:27 +00:00
SystemInstruction . transfer (
fundingAccount: keyPair . publicKey ,
recipientAccount: recipientPubKey ,
lamports: txData . amount ! . raw . toInt ( ) ) ,
ComputeBudgetInstruction . setComputeUnitPrice (
2024-04-29 21:18:54 +00:00
microLamports: txData . fee ! . raw . toInt ( ) - 5000 ) ,
// 5000 lamports is the base fee for a transaction. This instruction adds the necessary fee on top of base fee if it is needed.
ComputeBudgetInstruction . setComputeUnitLimit ( units: 1000000 ) ,
// 1000000 is the multiplication number to turn the compute unit price of microLamports to lamports.
// These instructions also help the user to not pay more than the shown fee.
// See: https://solanacookbook.com/references/basic-transactions.html#how-to-change-compute-budget-fee-priority-for-a-transaction
2024-03-20 00:50:42 +00:00
] ,
) ;
2024-04-24 15:36:27 +00:00
final txid = await _rpcClient ? . signAndSendTransaction ( message , [ keyPair ] ) ;
2024-03-20 00:50:42 +00:00
return txData . copyWith (
txid: txid ,
) ;
} catch ( e , s ) {
Logging . instance . log (
" $ runtimeType Solana confirmSend failed: $ e \n $ s " ,
level: LogLevel . Error ,
) ;
rethrow ;
}
}
@ override
Future < Amount > estimateFeeFor ( Amount amount , int feeRate ) async {
2024-04-24 15:36:27 +00:00
_checkClient ( ) ;
2024-03-21 18:57:27 +00:00
2024-03-20 00:50:42 +00:00
if ( info . cachedBalance . spendable . raw = = BigInt . zero ) {
return Amount (
rawValue: BigInt . zero ,
fractionDigits: cryptoCurrency . fractionDigits ,
) ;
}
2024-04-29 17:01:26 +00:00
2024-04-29 16:19:01 +00:00
final fee = await _getEstimatedNetworkFee ( amount ) ;
if ( fee = = null ) {
throw Exception ( " Failed to get fees, please check your node connection. " ) ;
}
2024-04-29 17:01:26 +00:00
2024-03-20 00:50:42 +00:00
return Amount (
2024-04-29 17:01:26 +00:00
rawValue: BigInt . from ( fee ) ,
2024-03-20 00:50:42 +00:00
fractionDigits: cryptoCurrency . fractionDigits ,
) ;
}
@ override
Future < FeeObject > get fees async {
2024-04-24 15:36:27 +00:00
_checkClient ( ) ;
2024-04-29 17:01:26 +00:00
final fee = await _getEstimatedNetworkFee (
Amount . fromDecimal (
Decimal . one , // 1 SOL
fractionDigits: cryptoCurrency . fractionDigits ,
) ,
) ;
2024-04-29 16:19:01 +00:00
if ( fee = = null ) {
throw Exception ( " Failed to get fees, please check your node connection. " ) ;
}
2024-04-29 17:01:26 +00:00
2024-03-20 00:50:42 +00:00
return FeeObject (
numberOfBlocksFast: 1 ,
numberOfBlocksAverage: 1 ,
numberOfBlocksSlow: 1 ,
2024-04-29 16:19:01 +00:00
fast: fee ,
medium: fee ,
slow: fee ) ;
2024-03-20 00:50:42 +00:00
}
@ override
Future < bool > pingCheck ( ) {
2024-03-21 19:38:59 +00:00
try {
2024-04-19 21:05:24 +00:00
_checkClient ( ) ;
2024-04-24 15:36:27 +00:00
_rpcClient ? . getHealth ( ) ;
2024-03-21 19:38:59 +00:00
return Future . value ( true ) ;
} catch ( e , s ) {
Logging . instance . log (
" $ runtimeType Solana pingCheck failed: $ e \n $ s " ,
level: LogLevel . Error ,
) ;
return Future . value ( false ) ;
}
2024-03-20 00:50:42 +00:00
}
@ override
FilterOperation ? get receivingAddressFilterOperation = >
FilterGroup . and ( standardReceivingAddressFilters ) ;
@ override
Future < void > recover ( { required bool isRescan } ) async {
await refreshMutex . protect ( ( ) async {
2024-04-24 15:47:35 +00:00
final addressStruct = await _getCurrentAddress ( ) ;
2024-03-20 00:50:42 +00:00
await mainDB . updateOrPutAddresses ( [ addressStruct ] ) ;
if ( info . cachedReceivingAddress ! = addressStruct . value ) {
await info . updateReceivingAddress (
newAddress: addressStruct . value ,
isar: mainDB . isar ,
) ;
}
await Future . wait ( [
updateBalance ( ) ,
updateChainHeight ( ) ,
updateTransactions ( ) ,
] ) ;
} ) ;
}
@ override
Future < void > updateBalance ( ) async {
try {
2024-04-24 15:36:27 +00:00
_checkClient ( ) ;
2024-03-21 18:57:27 +00:00
2024-04-24 15:47:35 +00:00
final balance = await _rpcClient ? . getBalance ( info . cachedReceivingAddress ) ;
2024-03-20 00:50:42 +00:00
// Rent exemption of Solana
2024-03-21 18:57:27 +00:00
final accInfo =
2024-04-24 15:36:27 +00:00
await _rpcClient ? . getAccountInfo ( ( await _getKeyPair ( ) ) . address ) ;
2024-03-21 18:57:27 +00:00
// TODO [prio=low]: handle null account info.
final int minimumRent =
2024-04-24 15:36:27 +00:00
await _rpcClient ? . getMinimumBalanceForRentExemption (
2024-03-21 18:57:27 +00:00
accInfo ! . value ! . data . toString ( ) . length ) ? ?
0 ;
// TODO [prio=low]: revisit null condition.
2024-04-24 15:47:35 +00:00
final spendableBalance = balance ! . value - minimumRent ;
2024-03-20 00:50:42 +00:00
final newBalance = Balance (
total: Amount (
rawValue: BigInt . from ( balance . value ) ,
fractionDigits: Coin . solana . decimals ,
) ,
spendable: Amount (
rawValue: BigInt . from ( spendableBalance ) ,
fractionDigits: Coin . solana . decimals ,
) ,
blockedTotal: Amount (
rawValue: BigInt . from ( minimumRent ) ,
fractionDigits: Coin . solana . decimals ,
) ,
pendingSpendable: Amount (
rawValue: BigInt . zero ,
fractionDigits: Coin . solana . decimals ,
) ,
) ;
await info . updateBalance ( newBalance: newBalance , isar: mainDB . isar ) ;
} catch ( e , s ) {
Logging . instance . log (
" Error getting balance in solana_wallet.dart: $ e \n $ s " ,
level: LogLevel . Error ,
) ;
}
}
@ override
Future < void > updateChainHeight ( ) async {
try {
2024-04-24 15:36:27 +00:00
_checkClient ( ) ;
2024-03-21 18:57:27 +00:00
2024-04-24 15:47:35 +00:00
final int blockHeight = await _rpcClient ? . getSlot ( ) ? ? 0 ;
2024-03-21 18:57:27 +00:00
// TODO [prio=low]: Revisit null condition.
2024-03-20 00:50:42 +00:00
await info . updateCachedChainHeight (
newHeight: blockHeight ,
isar: mainDB . isar ,
) ;
} catch ( e , s ) {
Logging . instance . log (
" Error occurred in solana_wallet.dart while getting "
2024-03-21 18:57:27 +00:00
" chain height for solana: $ e \n $ s " ,
2024-03-20 00:50:42 +00:00
level: LogLevel . Error ,
) ;
}
}
@ override
Future < void > updateNode ( ) async {
_solNode = getCurrentNode ( ) ;
await refresh ( ) ;
}
@ override
NodeModel getCurrentNode ( ) {
return _solNode ? ?
NodeService ( secureStorageInterface: secureStorageInterface )
. getPrimaryNodeFor ( coin: info . coin ) ? ?
DefaultNodes . getNodeFor ( info . coin ) ;
}
@ override
Future < void > updateTransactions ( ) async {
try {
2024-04-24 15:36:27 +00:00
_checkClient ( ) ;
2024-03-21 18:57:27 +00:00
2024-04-24 15:47:35 +00:00
final transactionsList = await _rpcClient ? . getTransactionsList (
2024-03-21 18:57:27 +00:00
( await _getKeyPair ( ) ) . publicKey ,
encoding: Encoding . jsonParsed ) ;
2024-04-24 15:47:35 +00:00
final txsList =
2024-03-21 18:57:27 +00:00
List < Tuple2 < isar . Transaction , Address > > . empty ( growable: true ) ;
2024-03-20 00:50:42 +00:00
2024-03-21 18:57:27 +00:00
// TODO [prio=low]: Revisit null assertion below.
for ( final tx in transactionsList ! ) {
2024-04-24 15:47:35 +00:00
final senderAddress =
2024-03-21 18:57:27 +00:00
( tx . transaction as ParsedTransaction ) . message . accountKeys [ 0 ] . pubkey ;
2024-04-29 21:18:54 +00:00
var receiverAddress =
2024-03-21 18:57:27 +00:00
( tx . transaction as ParsedTransaction ) . message . accountKeys [ 1 ] . pubkey ;
2024-03-20 00:50:42 +00:00
var txType = isar . TransactionType . unknown ;
2024-04-24 15:47:35 +00:00
final txAmount = Amount (
2024-03-21 18:57:27 +00:00
rawValue:
BigInt . from ( tx . meta ! . postBalances [ 1 ] - tx . meta ! . preBalances [ 1 ] ) ,
2024-03-20 00:50:42 +00:00
fractionDigits: cryptoCurrency . fractionDigits ,
) ;
2024-04-29 21:18:54 +00:00
if ( ( senderAddress = = ( await _getKeyPair ( ) ) . address ) & & ( receiverAddress = = " 11111111111111111111111111111111 " ) ) {
// The account that is only 1's are System Program accounts which means there is no receiver except the sender, see: https://explorer.solana.com/address/11111111111111111111111111111111
2024-03-20 00:50:42 +00:00
txType = isar . TransactionType . sentToSelf ;
2024-04-29 21:18:54 +00:00
receiverAddress = senderAddress ;
2024-03-20 00:50:42 +00:00
} else if ( senderAddress = = ( await _getKeyPair ( ) ) . address ) {
txType = isar . TransactionType . outgoing ;
} else if ( receiverAddress = = ( await _getKeyPair ( ) ) . address ) {
txType = isar . TransactionType . incoming ;
}
2024-04-24 15:47:35 +00:00
final transaction = isar . Transaction (
2024-03-20 00:50:42 +00:00
walletId: walletId ,
txid: ( tx . transaction as ParsedTransaction ) . signatures [ 0 ] ,
timestamp: tx . blockTime ! ,
type: txType ,
subType: isar . TransactionSubType . none ,
amount: tx . meta ! . postBalances [ 1 ] - tx . meta ! . preBalances [ 1 ] ,
amountString: txAmount . toJsonString ( ) ,
fee: tx . meta ! . fee ,
height: tx . slot ,
isCancelled: false ,
isLelantus: false ,
slateId: null ,
otherData: null ,
inputs: [ ] ,
outputs: [ ] ,
nonce: null ,
numberOfMessages: 0 ,
) ;
2024-04-24 15:47:35 +00:00
final txAddress = Address (
2024-03-20 00:50:42 +00:00
walletId: walletId ,
value: receiverAddress ,
publicKey: List < int > . empty ( ) ,
derivationIndex: 0 ,
2024-03-21 19:38:59 +00:00
derivationPath: DerivationPath ( ) . . value = " m/44'/501'/0'/0' " ,
2024-03-20 00:50:42 +00:00
type: AddressType . solana ,
2024-03-21 18:57:27 +00:00
subType: txType = = isar . TransactionType . outgoing
? AddressSubType . unknown
: AddressSubType . receiving ) ;
2024-03-20 00:50:42 +00:00
txsList . add ( Tuple2 ( transaction , txAddress ) ) ;
}
await mainDB . addNewTransactionData ( txsList , walletId ) ;
} catch ( e , s ) {
Logging . instance . log (
" Error occurred in solana_wallet.dart while getting "
2024-03-21 18:57:27 +00:00
" transactions for solana: $ e \n $ s " ,
2024-03-20 00:50:42 +00:00
level: LogLevel . Error ,
) ;
}
}
@ override
Future < bool > updateUTXOs ( ) {
// No UTXOs in Solana
return Future . value ( false ) ;
}
2024-03-21 18:57:27 +00:00
2024-04-19 21:05:24 +00:00
/// Make sure the Solana RpcClient uses Tor if it's enabled.
///
2024-04-24 15:36:27 +00:00
void _checkClient ( ) async {
2024-04-24 03:03:46 +00:00
HttpClient ? httpClient ;
2024-03-21 18:57:27 +00:00
if ( prefs . useTor ) {
2024-04-24 03:03:46 +00:00
// Make proxied HttpClient.
2024-04-19 21:05:24 +00:00
final ( { InternetAddress host , int port } ) proxyInfo =
TorService . sharedInstance . getProxyInfo ( ) ;
2024-04-24 03:03:46 +00:00
final proxySettings = ProxySettings ( proxyInfo . host , proxyInfo . port ) ;
httpClient = HttpClient ( ) ;
SocksTCPClient . assignToHttpClient ( httpClient , [ proxySettings ] ) ;
2024-04-19 21:27:33 +00:00
}
2024-04-24 03:03:46 +00:00
2024-04-24 15:36:27 +00:00
_rpcClient = RpcClient (
2024-04-24 03:03:46 +00:00
" ${ getCurrentNode ( ) . host } : ${ getCurrentNode ( ) . port } " ,
timeout: const Duration ( seconds: 30 ) ,
customHeaders: { } ,
httpClient: httpClient ,
) ;
2024-03-21 18:57:27 +00:00
return ;
}
}