solana tor wip

This commit is contained in:
sneurlax 2024-03-21 13:57:27 -05:00
parent 00cff96131
commit 11a5ed33e5

View file

@ -1,33 +1,40 @@
import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
import 'package:solana/dto.dart'; import 'package:solana/dto.dart';
import 'package:solana/solana.dart'; import 'package:solana/solana.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart' as isar; import 'package:stackwallet/electrumx_rpc/electrumx_client.dart';
import 'package:stackwallet/models/balance.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart'
as isar;
import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/models/node_model.dart';
import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart';
import 'package:stackwallet/services/node_service.dart';
import 'package:stackwallet/services/tor_service.dart';
import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/default_nodes.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/solana.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/solana.dart';
import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
import 'package:stackwallet/wallets/models/tx_data.dart'; import 'package:stackwallet/wallets/models/tx_data.dart';
import 'package:stackwallet/wallets/wallet/intermediate/bip39_wallet.dart'; import 'package:stackwallet/wallets/wallet/intermediate/bip39_wallet.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/models/balance.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart';
import 'package:stackwallet/models/node_model.dart';
import 'package:stackwallet/services/node_service.dart';
class SolanaWallet extends Bip39Wallet<Solana> { class SolanaWallet extends Bip39Wallet<Solana> {
SolanaWallet(CryptoCurrencyNetwork network) : super(Solana(network)); SolanaWallet(CryptoCurrencyNetwork network) : super(Solana(network));
NodeModel? _solNode; NodeModel? _solNode;
ElectrumXClient? electrumXClient; // Used for Tor.
RpcClient? rpcClient; // The Solana RpcClient.
Future<Ed25519HDKeyPair> _getKeyPair() async { Future<Ed25519HDKeyPair> _getKeyPair() async {
return Ed25519HDKeyPair.fromMnemonic(await getMnemonic(), account: 0, change: 0); return Ed25519HDKeyPair.fromMnemonic(await getMnemonic(),
account: 0, change: 0);
} }
Future<Address> _getCurrentAddress() async { Future<Address> _getCurrentAddress() async {
@ -43,9 +50,9 @@ class SolanaWallet extends Bip39Wallet<Solana> {
} }
Future<int> _getCurrentBalanceInLamports() async { Future<int> _getCurrentBalanceInLamports() async {
var rpcClient = RpcClient("${getCurrentNode().host}:${getCurrentNode().port}"); await _checkClients(); // Check electrumXClient and rpcClient.
var balance = await rpcClient.getBalance((await _getKeyPair()).address); var balance = await rpcClient?.getBalance((await _getKeyPair()).address);
return balance.value; return balance!.value;
} }
@override @override
@ -78,6 +85,8 @@ class SolanaWallet extends Bip39Wallet<Solana> {
@override @override
Future<TxData> prepareSend({required TxData txData}) async { Future<TxData> prepareSend({required TxData txData}) async {
try { try {
await _checkClients(); // Check ElectrumXClient and Solana RpcClient.
if (txData.recipients == null || txData.recipients!.length != 1) { if (txData.recipients == null || txData.recipients!.length != 1) {
throw Exception("$runtimeType prepareSend requires 1 recipient"); throw Exception("$runtimeType prepareSend requires 1 recipient");
} }
@ -104,11 +113,17 @@ class SolanaWallet extends Bip39Wallet<Solana> {
} }
// Rent exemption of Solana // Rent exemption of Solana
final rpcClient = RpcClient("${getCurrentNode().host}:${getCurrentNode().port}"); final accInfo =
final accInfo = await rpcClient.getAccountInfo((await _getKeyPair()).address); await rpcClient?.getAccountInfo((await _getKeyPair()).address);
final minimumRent = await rpcClient.getMinimumBalanceForRentExemption(accInfo.value!.data.toString().length); int minimumRent = await rpcClient?.getMinimumBalanceForRentExemption(
if (minimumRent > ((await _getCurrentBalanceInLamports()) - txData.amount!.raw.toInt() - feeAmount)) { accInfo!.value!.data.toString().length) ??
throw Exception("Insufficient remaining balance for rent exemption, minimum rent: ${minimumRent / pow(10, cryptoCurrency.fractionDigits)}"); 0; // TODO revisit null condition.
if (minimumRent >
((await _getCurrentBalanceInLamports()) -
txData.amount!.raw.toInt() -
feeAmount)) {
throw Exception(
"Insufficient remaining balance for rent exemption, minimum rent: ${minimumRent / pow(10, cryptoCurrency.fractionDigits)}");
} }
return txData.copyWith( return txData.copyWith(
@ -129,18 +144,24 @@ class SolanaWallet extends Bip39Wallet<Solana> {
@override @override
Future<TxData> confirmSend({required TxData txData}) async { Future<TxData> confirmSend({required TxData txData}) async {
try { try {
await _checkClients(); // Check ElectrumXClient and Solana RpcClient.
final keyPair = await _getKeyPair(); final keyPair = await _getKeyPair();
final rpcClient = RpcClient("${getCurrentNode().host}:${getCurrentNode().port}");
var recipientAccount = txData.recipients!.first; var recipientAccount = txData.recipients!.first;
var recipientPubKey = Ed25519HDPublicKey.fromBase58(recipientAccount.address); var recipientPubKey =
Ed25519HDPublicKey.fromBase58(recipientAccount.address);
final message = Message( final message = Message(
instructions: [ instructions: [
SystemInstruction.transfer(fundingAccount: keyPair.publicKey, recipientAccount: recipientPubKey, lamports: txData.amount!.raw.toInt()), SystemInstruction.transfer(
ComputeBudgetInstruction.setComputeUnitPrice(microLamports: txData.fee!.raw.toInt()), fundingAccount: keyPair.publicKey,
recipientAccount: recipientPubKey,
lamports: txData.amount!.raw.toInt()),
ComputeBudgetInstruction.setComputeUnitPrice(
microLamports: txData.fee!.raw.toInt()),
], ],
); );
final txid = await rpcClient.signAndSendTransaction(message, [keyPair]); final txid = await rpcClient?.signAndSendTransaction(message, [keyPair]);
return txData.copyWith( return txData.copyWith(
txid: txid, txid: txid,
); );
@ -155,6 +176,8 @@ class SolanaWallet extends Bip39Wallet<Solana> {
@override @override
Future<Amount> estimateFeeFor(Amount amount, int feeRate) async { Future<Amount> estimateFeeFor(Amount amount, int feeRate) async {
await _checkClients(); // Check ElectrumXClient and Solana RpcClient.
if (info.cachedBalance.spendable.raw == BigInt.zero) { if (info.cachedBalance.spendable.raw == BigInt.zero) {
return Amount( return Amount(
rawValue: BigInt.zero, rawValue: BigInt.zero,
@ -162,27 +185,28 @@ class SolanaWallet extends Bip39Wallet<Solana> {
); );
} }
final rpcClient = RpcClient("${getCurrentNode().host}:${getCurrentNode().port}"); final fee = await rpcClient?.getFees();
final fee = await rpcClient.getFees(); // TODO [prio=low]: handle null fee.
return Amount( return Amount(
rawValue: BigInt.from(fee.value.feeCalculator.lamportsPerSignature), rawValue: BigInt.from(fee!.value.feeCalculator.lamportsPerSignature),
fractionDigits: cryptoCurrency.fractionDigits, fractionDigits: cryptoCurrency.fractionDigits,
); );
} }
@override @override
Future<FeeObject> get fees async { Future<FeeObject> get fees async {
final rpcClient = RpcClient("${getCurrentNode().host}:${getCurrentNode().port}"); await _checkClients(); // Check ElectrumXClient and Solana RpcClient.
final fees = await rpcClient.getFees();
final fees = await rpcClient?.getFees();
// TODO [prio=low]: handle null fees.
return FeeObject( return FeeObject(
numberOfBlocksFast: 1, numberOfBlocksFast: 1,
numberOfBlocksAverage: 1, numberOfBlocksAverage: 1,
numberOfBlocksSlow: 1, numberOfBlocksSlow: 1,
fast: fees.value.feeCalculator.lamportsPerSignature, fast: fees!.value.feeCalculator.lamportsPerSignature,
medium: fees.value.feeCalculator.lamportsPerSignature, medium: fees!.value.feeCalculator.lamportsPerSignature,
slow: fees.value.feeCalculator.lamportsPerSignature slow: fees!.value.feeCalculator.lamportsPerSignature);
);
} }
@override @override
@ -219,13 +243,20 @@ class SolanaWallet extends Bip39Wallet<Solana> {
@override @override
Future<void> updateBalance() async { Future<void> updateBalance() async {
try { try {
var rpcClient = RpcClient("${getCurrentNode().host}:${getCurrentNode().port}"); await _checkClients(); // Check ElectrumXClient and Solana RpcClient.
var balance = await rpcClient.getBalance(info.cachedReceivingAddress);
var balance = await rpcClient?.getBalance(info.cachedReceivingAddress);
// Rent exemption of Solana // Rent exemption of Solana
final accInfo = await rpcClient.getAccountInfo((await _getKeyPair()).address); final accInfo =
final minimumRent = await rpcClient.getMinimumBalanceForRentExemption(accInfo.value!.data.toString().length); await rpcClient?.getAccountInfo((await _getKeyPair()).address);
var spendableBalance = balance.value - minimumRent; // TODO [prio=low]: handle null account info.
final int minimumRent =
await rpcClient?.getMinimumBalanceForRentExemption(
accInfo!.value!.data.toString().length) ??
0;
// TODO [prio=low]: revisit null condition.
var spendableBalance = balance!.value - minimumRent;
final newBalance = Balance( final newBalance = Balance(
total: Amount( total: Amount(
@ -258,8 +289,10 @@ class SolanaWallet extends Bip39Wallet<Solana> {
@override @override
Future<void> updateChainHeight() async { Future<void> updateChainHeight() async {
try { try {
var rpcClient = RpcClient("${getCurrentNode().host}:${getCurrentNode().port}"); await _checkClients(); // Check ElectrumXClient and Solana RpcClient.
var blockHeight = await rpcClient.getSlot();
int blockHeight = await rpcClient?.getSlot() ?? 0;
// TODO [prio=low]: Revisit null condition.
await info.updateCachedChainHeight( await info.updateCachedChainHeight(
newHeight: blockHeight, newHeight: blockHeight,
@ -268,7 +301,7 @@ class SolanaWallet extends Bip39Wallet<Solana> {
} catch (e, s) { } catch (e, s) {
Logging.instance.log( Logging.instance.log(
"Error occurred in solana_wallet.dart while getting" "Error occurred in solana_wallet.dart while getting"
" chain height for solana: $e\n$s", " chain height for solana: $e\n$s",
level: LogLevel.Error, level: LogLevel.Error,
); );
} }
@ -291,20 +324,30 @@ class SolanaWallet extends Bip39Wallet<Solana> {
@override @override
Future<void> updateTransactions() async { Future<void> updateTransactions() async {
try { try {
var rpcClient = RpcClient("${getCurrentNode().host}:${getCurrentNode().port}"); await _checkClients(); // Check ElectrumXClient and Solana RpcClient.
var transactionsList = await rpcClient.getTransactionsList((await _getKeyPair()).publicKey, encoding: Encoding.jsonParsed);
var txsList = List<Tuple2<isar.Transaction, Address>>.empty(growable: true);
for (final tx in transactionsList) { var transactionsList = await rpcClient?.getTransactionsList(
var senderAddress = (tx.transaction as ParsedTransaction).message.accountKeys[0].pubkey; (await _getKeyPair()).publicKey,
var receiverAddress = (tx.transaction as ParsedTransaction).message.accountKeys[1].pubkey; encoding: Encoding.jsonParsed);
var txsList =
List<Tuple2<isar.Transaction, Address>>.empty(growable: true);
// TODO [prio=low]: Revisit null assertion below.
for (final tx in transactionsList!) {
var senderAddress =
(tx.transaction as ParsedTransaction).message.accountKeys[0].pubkey;
var receiverAddress =
(tx.transaction as ParsedTransaction).message.accountKeys[1].pubkey;
var txType = isar.TransactionType.unknown; var txType = isar.TransactionType.unknown;
var txAmount = Amount( var txAmount = Amount(
rawValue: BigInt.from(tx.meta!.postBalances[1] - tx.meta!.preBalances[1]), rawValue:
BigInt.from(tx.meta!.postBalances[1] - tx.meta!.preBalances[1]),
fractionDigits: cryptoCurrency.fractionDigits, fractionDigits: cryptoCurrency.fractionDigits,
); );
if ((senderAddress == (await _getKeyPair()).address) && (receiverAddress == (await _getKeyPair()).address) ){ if ((senderAddress == (await _getKeyPair()).address) &&
(receiverAddress == (await _getKeyPair()).address)) {
txType = isar.TransactionType.sentToSelf; txType = isar.TransactionType.sentToSelf;
} else if (senderAddress == (await _getKeyPair()).address) { } else if (senderAddress == (await _getKeyPair()).address) {
txType = isar.TransactionType.outgoing; txType = isar.TransactionType.outgoing;
@ -339,8 +382,9 @@ class SolanaWallet extends Bip39Wallet<Solana> {
derivationIndex: 0, derivationIndex: 0,
derivationPath: null, derivationPath: null,
type: AddressType.solana, type: AddressType.solana,
subType: txType == isar.TransactionType.outgoing ? AddressSubType.unknown : AddressSubType.receiving subType: txType == isar.TransactionType.outgoing
); ? AddressSubType.unknown
: AddressSubType.receiving);
txsList.add(Tuple2(transaction, txAddress)); txsList.add(Tuple2(transaction, txAddress));
} }
@ -348,7 +392,7 @@ class SolanaWallet extends Bip39Wallet<Solana> {
} catch (e, s) { } catch (e, s) {
Logging.instance.log( Logging.instance.log(
"Error occurred in solana_wallet.dart while getting" "Error occurred in solana_wallet.dart while getting"
" transactions for solana: $e\n$s", " transactions for solana: $e\n$s",
level: LogLevel.Error, level: LogLevel.Error,
); );
} }
@ -359,4 +403,25 @@ class SolanaWallet extends Bip39Wallet<Solana> {
// No UTXOs in Solana // No UTXOs in Solana
return Future.value(false); return Future.value(false);
} }
/// Check that the ElectrumXClient is active and usable by a Solana RpcClient.
Future<void> _checkClients() async {
if (prefs.useTor) {
electrumXClient ??= ElectrumXClient(
host: getCurrentNode().host,
port: getCurrentNode().port,
useSSL: getCurrentNode().useSSL,
failovers: [],
prefs: prefs,
coin: Coin.solana,
);
int torPort = electrumXClient?.rpcClient?.proxyInfo?.port ??
TorService.sharedInstance.getProxyInfo().port;
rpcClient = RpcClient("${InternetAddress.loopbackIPv4}:$torPort");
} else {
rpcClient =
RpcClient("${getCurrentNode().host}:${getCurrentNode().port}");
}
return;
}
} }