Merge branch 'solana-tor' into testing

This commit is contained in:
sneurlax 2024-04-23 16:47:01 -05:00
commit feef4d95ca
24 changed files with 740 additions and 53 deletions

View file

@ -205,4 +205,4 @@ Run with `-v` or `--verbose` to see a more detailed error. Certain exceptions (
## Tor
To test Tor usage, run Stack Wallet from Android Studio. Click the Flutter DevTools icon in the Run tab (next to the Hot Reload and Hot Restart buttons) and navigate to the Network tab. Connections using Tor will show as `GET InternetAddress('127.0.0.1', IPv4) 101 ws`. Connections outside of Tor will show the destination address directly.
To test Tor usage, run Stack Wallet from Android Studio. Click the Flutter DevTools icon in the Run tab (next to the Hot Reload and Hot Restart buttons) and navigate to the Network tab. Connections using Tor will show as `GET InternetAddress('127.0.0.1', IPv4) 101 ws`. Connections outside of Tor will show the destination address directly (although some Tor requests may also show the destination address directly, check the Headers take for *eg.* `{localPort: 59940, remoteAddress: 127.0.0.1, remotePort: 6725}`. `localPort` should match your Tor port.

View file

@ -164,6 +164,7 @@ enum AddressType {
stellar,
tezos,
frostMS,
solana,
p2tr;
String get readableName {
@ -196,6 +197,8 @@ enum AddressType {
return "Tezos";
case AddressType.frostMS:
return "FrostMS";
case AddressType.solana:
return "Solana";
case AddressType.p2tr:
return "Taproot"; // Why not use P2TR, P2PKH, etc.?
}

View file

@ -139,6 +139,11 @@ class _AddWalletViewState extends ConsumerState<AddWalletView> {
_coins.remove(Coin.bitcoinFrost);
}
// Remove Solana from the list of coins based on our frostEnabled preference.
if (!ref.read(prefsChangeNotifierProvider).solanaEnabled) {
_coins.remove(Coin.solana);
}
coinEntities.addAll(_coins.map((e) => CoinEntity(e)));
if (ref.read(prefsChangeNotifierProvider).showTestNetCoins) {

View file

@ -10,6 +10,7 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
@ -276,6 +277,36 @@ class HiddenSettings extends StatelessWidget {
const SizedBox(
height: 12,
),
Consumer(
builder: (_, ref, __) {
return GestureDetector(
onTap: () async {
ref
.read(prefsChangeNotifierProvider)
.solanaEnabled =
!(ref
.read(prefsChangeNotifierProvider)
.solanaEnabled);
if (kDebugMode) {
print(
"Solana enabled: ${ref.read(prefsChangeNotifierProvider).solanaEnabled}");
}
},
child: RoundedWhiteContainer(
child: Text(
"Toggle Solana",
style: STextStyles.button(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark),
),
),
);
},
),
const SizedBox(
height: 12,
),
Consumer(
builder: (_, ref, __) {
return GestureDetector(

View file

@ -14,6 +14,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:solana/solana.dart';
import 'package:stackwallet/models/node_model.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/providers/global/secure_store_provider.dart';
@ -216,6 +217,20 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
);
} catch (_) {}
break;
case Coin.solana:
try {
RpcClient rpcClient;
if (formData.host!.startsWith("http") || formData.host!.startsWith("https")) {
rpcClient = RpcClient("${formData.host}:${formData.port}");
} else {
rpcClient = RpcClient("http://${formData.host}:${formData.port}");
}
await rpcClient.getEpochInfo().then((value) => testPassed = true);
} catch (_) {
testPassed = false;
}
break;
}
if (showFlushBar && mounted) {
@ -756,6 +771,7 @@ class _NodeFormState extends ConsumerState<NodeForm> {
case Coin.nano:
case Coin.banano:
case Coin.eCash:
case Coin.solana:
case Coin.stellar:
case Coin.stellarTestnet:
case Coin.bitcoinFrost:

View file

@ -13,6 +13,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:solana/solana.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart';
import 'package:stackwallet/providers/global/secure_store_provider.dart';
@ -193,6 +194,20 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> {
testPassed = false;
}
break;
case Coin.solana:
try {
RpcClient rpcClient;
if (node!.host.startsWith("http") || node.host.startsWith("https")) {
rpcClient = RpcClient("${node.host}:${node.port}");
} else {
rpcClient = RpcClient("http://${node.host}:${node.port}");
}
await rpcClient.getEpochInfo().then((value) => testPassed = true);
} catch (_) {
testPassed = false;
}
break;
}
if (testPassed) {

View file

@ -101,7 +101,7 @@ class PriceAPI {
"https://api.coingecko.com/api/v3/coins/markets?vs_currency"
"=${baseCurrency.toLowerCase()}"
"&ids=monero,bitcoin,litecoin,ecash,epic-cash,zcoin,dogecoin,"
"bitcoin-cash,namecoin,wownero,ethereum,particl,nano,banano,stellar,tezos"
"bitcoin-cash,namecoin,wownero,ethereum,particl,nano,banano,stellar,tezos,solana"
"&order=market_cap_desc&per_page=50&page=1&sparkline=false");
final coinGeckoResponse = await client.get(

View file

@ -28,6 +28,7 @@ class CoinThemeColorDefault {
Color get namecoin => const Color(0xFF91B1E1);
Color get wownero => const Color(0xFFED80C1);
Color get particl => const Color(0xFF8175BD);
Color get solana => const Color(0xFFC696FF);
Color get stellar => const Color(0xFF6600FF);
Color get nano => const Color(0xFF209CE9);
Color get banano => const Color(0xFFFBDD11);
@ -66,6 +67,8 @@ class CoinThemeColorDefault {
return wownero;
case Coin.particl:
return particl;
case Coin.solana:
return solana;
case Coin.stellar:
case Coin.stellarTestnet:
return stellar;

View file

@ -1709,6 +1709,8 @@ class StackColors extends ThemeExtension<StackColors> {
return _coin.wownero;
case Coin.particl:
return _coin.particl;
case Coin.solana:
return _coin.solana;
case Coin.stellar:
case Coin.stellarTestnet:
return _coin.stellar;

View file

@ -26,6 +26,7 @@ import 'package:stackwallet/wallets/crypto_currency/coins/monero.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/namecoin.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/nano.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/particl.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/solana.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/stellar.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/tezos.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/wownero.dart';
@ -67,6 +68,8 @@ class AddressUtils {
return Namecoin(CryptoCurrencyNetwork.main).validateAddress(address);
case Coin.particl:
return Particl(CryptoCurrencyNetwork.main).validateAddress(address);
case Coin.solana:
return Solana(CryptoCurrencyNetwork.main).validateAddress(address);
case Coin.stellar:
return Stellar(CryptoCurrencyNetwork.main).validateAddress(address);
case Coin.nano:

View file

@ -55,6 +55,7 @@ enum AmountUnit {
case Coin.stellar: // TODO: check if this is correct
case Coin.stellarTestnet:
case Coin.tezos:
case Coin.solana:
return AmountUnit.values.sublist(0, 4);
case Coin.monero:

View file

@ -66,6 +66,8 @@ Uri getDefaultBlockExplorerUrlFor({
return Uri.parse("https://testnet.stellarchain.io/transactions/$txid");
case Coin.tezos:
return Uri.parse("https://tzstats.com/$txid");
case Coin.solana:
return Uri.parse("https://explorer.solana.com/tx/$txid");
}
}

View file

@ -46,6 +46,7 @@ abstract class Constants {
10000000); // https://developers.stellar.org/docs/fundamentals-and-concepts/stellar-data-structures/assets#amount-precision
static final BigInt _satsPerCoin = BigInt.from(100000000);
static final BigInt _satsPerCoinTezos = BigInt.from(1000000);
static final BigInt _satsPerCoinSolana = BigInt.from(1000000000);
static const int _decimalPlaces = 8;
static const int _decimalPlacesNano = 30;
static const int _decimalPlacesBanano = 29;
@ -55,6 +56,7 @@ abstract class Constants {
static const int _decimalPlacesECash = 2;
static const int _decimalPlacesStellar = 7;
static const int _decimalPlacesTezos = 6;
static const int _decimalPlacesSolana = 9;
static const int notificationsMax = 0xFFFFFFFF;
static const Duration networkAliveTimerDuration = Duration(seconds: 10);
@ -109,6 +111,9 @@ abstract class Constants {
case Coin.tezos:
return _satsPerCoinTezos;
case Coin.solana:
return _satsPerCoinSolana;
}
}
@ -155,6 +160,9 @@ abstract class Constants {
case Coin.tezos:
return _decimalPlacesTezos;
case Coin.solana:
return _decimalPlacesSolana;
}
}
@ -176,6 +184,7 @@ abstract class Constants {
case Coin.ethereum:
case Coin.namecoin:
case Coin.particl:
case Coin.solana:
case Coin.nano:
case Coin.stellar:
case Coin.stellarTestnet:
@ -245,6 +254,7 @@ abstract class Constants {
case Coin.nano: // TODO: Verify this
case Coin.banano: // TODO: Verify this
case Coin.solana:
return 1;
case Coin.stellar:
@ -272,6 +282,7 @@ abstract class Constants {
case Coin.namecoin:
case Coin.particl:
case Coin.ethereum:
case Coin.solana:
return 12;
case Coin.wownero:

View file

@ -188,6 +188,18 @@ abstract class DefaultNodes {
isDown: false,
);
static NodeModel get solana => NodeModel(
host: "https://api.mainnet-beta.solana.com", // TODO: Change this to stack wallet one
port: 443,
name: DefaultNodes.defaultName,
id: DefaultNodes.buildId(Coin.solana),
useSSL: true,
enabled: true,
coinName: Coin.solana.name,
isFailover: true,
isDown: false,
);
static NodeModel get stellar => NodeModel(
host: "https://horizon.stellar.org",
port: 443,
@ -348,6 +360,9 @@ abstract class DefaultNodes {
case Coin.particl:
return particl;
case Coin.solana:
return solana;
case Coin.stellar:
return stellar;

View file

@ -26,6 +26,7 @@ enum Coin {
namecoin,
nano,
particl,
solana,
stellar,
tezos,
wownero,
@ -69,6 +70,8 @@ extension CoinExt on Coin {
return "Monero";
case Coin.particl:
return "Particl";
case Coin.solana:
return "Solana";
case Coin.stellar:
return "Stellar";
case Coin.tezos:
@ -121,6 +124,8 @@ extension CoinExt on Coin {
return "XMR";
case Coin.particl:
return "PART";
case Coin.solana:
return "SOL";
case Coin.stellar:
return "XLM";
case Coin.tezos:
@ -173,6 +178,8 @@ extension CoinExt on Coin {
return "monero";
case Coin.particl:
return "particl";
case Coin.solana:
return "solana";
case Coin.stellar:
return "stellar";
case Coin.tezos:
@ -229,6 +236,7 @@ extension CoinExt on Coin {
case Coin.nano:
case Coin.banano:
case Coin.tezos:
case Coin.solana:
return false;
}
}
@ -259,6 +267,7 @@ extension CoinExt on Coin {
case Coin.firoTestNet:
case Coin.nano:
case Coin.banano:
case Coin.solana:
case Coin.stellar:
case Coin.stellarTestnet:
return false;
@ -284,6 +293,7 @@ extension CoinExt on Coin {
case Coin.banano:
case Coin.eCash:
case Coin.stellar:
case Coin.solana:
return false;
case Coin.dogecoinTestNet:
@ -327,6 +337,7 @@ extension CoinExt on Coin {
case Coin.banano:
case Coin.eCash:
case Coin.stellar:
case Coin.solana:
return this;
case Coin.dogecoinTestNet:
@ -400,6 +411,9 @@ extension CoinExt on Coin {
case Coin.stellar:
case Coin.stellarTestnet:
return AddressType.stellar;
case Coin.solana:
return AddressType.solana;
}
}
}
@ -448,6 +462,10 @@ Coin coinFromPrettyName(String name) {
case "particl":
return Coin.particl;
case "Solana":
case "solana":
return Coin.solana;
case "Stellar":
case "stellar":
return Coin.stellar;
@ -548,6 +566,8 @@ Coin coinFromTickerCaseInsensitive(String ticker) {
return Coin.namecoin;
case "part":
return Coin.particl;
case "sol":
return Coin.solana;
case "xlm":
return Coin.stellar;
case "xtz":

View file

@ -17,6 +17,7 @@ enum DerivePathType {
bip84,
eth,
eCash44,
solana,
bip86,
}
@ -45,6 +46,9 @@ extension DerivePathTypeExt on DerivePathType {
case Coin.ethereum: // TODO: do we need something here?
return DerivePathType.eth;
case Coin.solana:
return DerivePathType.solana;
case Coin.bitcoinFrost:
case Coin.bitcoinFrostTestNet:
case Coin.epicCash:

View file

@ -68,6 +68,7 @@ class Prefs extends ChangeNotifier {
await _setMaxDecimals();
_useTor = await _getUseTor();
_fusionServerInfo = await _getFusionServerInfo();
_solanaEnabled = await _getSolanaEnabled();
_frostEnabled = await _getFrostEnabled();
_initialized = true;
@ -1010,6 +1011,27 @@ class Prefs extends ChangeNotifier {
return actualMap;
}
// Solana
bool _solanaEnabled = false;
bool get solanaEnabled => _solanaEnabled;
set solanaEnabled(bool solanaEnabled) {
if (_solanaEnabled != solanaEnabled) {
DB.instance.put<dynamic>(
boxName: DB.boxNamePrefs, key: "solanaEnabled", value: solanaEnabled);
_solanaEnabled = solanaEnabled;
notifyListeners();
}
}
Future<bool> _getSolanaEnabled() async {
return await DB.instance.get<dynamic>(
boxName: DB.boxNamePrefs, key: "solanaEnabled") as bool? ??
false;
}
// FROST multisig
bool _frostEnabled = false;

View file

@ -0,0 +1,48 @@
import 'package:solana/solana.dart';
import 'package:stackwallet/models/node_model.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
import 'package:stackwallet/wallets/crypto_currency/intermediate/bip39_currency.dart';
import 'package:stackwallet/utilities/default_nodes.dart';
class Solana extends Bip39Currency {
Solana(super.network) {
switch (network) {
case CryptoCurrencyNetwork.main:
coin = Coin.solana;
default:
throw Exception("Unsupported network: $network");
}
}
@override
NodeModel get defaultNode {
switch (network) {
case CryptoCurrencyNetwork.main:
return NodeModel(
host: "https://api.mainnet-beta.solana.com/", // TODO: Change this to stack wallet one
port: 443,
name: DefaultNodes.defaultName,
id: DefaultNodes.buildId(Coin.solana),
useSSL: true,
enabled: true,
coinName: Coin.solana.name,
isFailover: true,
isDown: false,
);
default:
throw Exception("Unsupported network: $network");
}
}
@override
int get minConfirms => 21;
@override
bool validateAddress(String address) {
return isPointOnEd25519Curve(Ed25519HDPublicKey.fromBase58(address).toByteArray());
}
@override
String get genesisHash => throw UnimplementedError();
}

View file

@ -0,0 +1,431 @@
import 'dart:io';
import 'dart:math';
import 'package:isar/isar.dart';
import 'package:solana/dto.dart';
import 'package:solana/solana.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/node_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/default_nodes.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/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;
RpcClient? rpcClient; // The Solana RpcClient.
Future<Ed25519HDKeyPair> _getKeyPair() async {
return Ed25519HDKeyPair.fromMnemonic(await getMnemonic(),
account: 0, change: 0);
}
Future<Address> _getCurrentAddress() async {
var addressStruct = Address(
walletId: walletId,
value: (await _getKeyPair()).address,
publicKey: List<int>.empty(),
derivationIndex: 0,
derivationPath: DerivationPath()..value = "m/44'/501'/0'/0'",
type: cryptoCurrency.coin.primaryAddressType,
subType: AddressSubType.unknown);
return addressStruct;
}
Future<int> _getCurrentBalanceInLamports() async {
await _checkClient();
var balance = await rpcClient?.getBalance((await _getKeyPair()).address);
return balance!.value;
}
@override
FilterOperation? get changeAddressFilterOperation =>
throw UnimplementedError();
@override
Future<void> checkSaveInitialReceivingAddress() async {
try {
var address = (await _getKeyPair()).address;
await mainDB.updateOrPutAddresses([
Address(
walletId: walletId,
value: address,
publicKey: List<int>.empty(),
derivationIndex: 0,
derivationPath: DerivationPath()..value = "m/44'/501'/0'/0'",
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 {
await _checkClient();
if (txData.recipients == null || txData.recipients!.length != 1) {
throw Exception("$runtimeType prepareSend requires 1 recipient");
}
Amount sendAmount = txData.amount!;
if (sendAmount > info.cachedBalance.spendable) {
throw Exception("Insufficient available balance");
}
int feeAmount;
var currentFees = await fees;
switch (txData.feeRateType) {
case FeeRateType.fast:
feeAmount = currentFees.fast;
break;
case FeeRateType.slow:
feeAmount = currentFees.slow;
break;
case FeeRateType.average:
default:
feeAmount = currentFees.medium;
break;
}
// Rent exemption of Solana
final accInfo =
await rpcClient?.getAccountInfo((await _getKeyPair()).address);
int minimumRent = await rpcClient?.getMinimumBalanceForRentExemption(
accInfo!.value!.data.toString().length) ??
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(
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 {
await _checkClient();
final keyPair = await _getKeyPair();
var recipientAccount = txData.recipients!.first;
var recipientPubKey =
Ed25519HDPublicKey.fromBase58(recipientAccount.address);
final message = Message(
instructions: [
SystemInstruction.transfer(
fundingAccount: keyPair.publicKey,
recipientAccount: recipientPubKey,
lamports: txData.amount!.raw.toInt()),
ComputeBudgetInstruction.setComputeUnitPrice(
microLamports: txData.fee!.raw.toInt()),
],
);
final txid = await rpcClient?.signAndSendTransaction(message, [keyPair]);
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 {
await _checkClient();
if (info.cachedBalance.spendable.raw == BigInt.zero) {
return Amount(
rawValue: BigInt.zero,
fractionDigits: cryptoCurrency.fractionDigits,
);
}
final fee = await rpcClient?.getFees();
// TODO [prio=low]: handle null fee.
return Amount(
rawValue: BigInt.from(fee!.value.feeCalculator.lamportsPerSignature),
fractionDigits: cryptoCurrency.fractionDigits,
);
}
@override
Future<FeeObject> get fees async {
await _checkClient();
final fees = await rpcClient?.getFees();
// TODO [prio=low]: handle null fees.
return FeeObject(
numberOfBlocksFast: 1,
numberOfBlocksAverage: 1,
numberOfBlocksSlow: 1,
fast: fees!.value.feeCalculator.lamportsPerSignature,
medium: fees!.value.feeCalculator.lamportsPerSignature,
slow: fees!.value.feeCalculator.lamportsPerSignature);
}
@override
Future<bool> pingCheck() {
try {
_checkClient();
rpcClient?.getHealth();
return Future.value(true);
} catch (e, s) {
Logging.instance.log(
"$runtimeType Solana pingCheck failed: $e\n$s",
level: LogLevel.Error,
);
return Future.value(false);
}
}
@override
FilterOperation? get receivingAddressFilterOperation =>
FilterGroup.and(standardReceivingAddressFilters);
@override
Future<void> recover({required bool isRescan}) async {
await refreshMutex.protect(() async {
var addressStruct = await _getCurrentAddress();
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 {
await _checkClient();
var balance = await rpcClient?.getBalance(info.cachedReceivingAddress);
// Rent exemption of Solana
final accInfo =
await rpcClient?.getAccountInfo((await _getKeyPair()).address);
// 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(
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 {
await _checkClient();
int blockHeight = await rpcClient?.getSlot() ?? 0;
// TODO [prio=low]: Revisit null condition.
await info.updateCachedChainHeight(
newHeight: blockHeight,
isar: mainDB.isar,
);
} catch (e, s) {
Logging.instance.log(
"Error occurred in solana_wallet.dart while getting"
" chain height for solana: $e\n$s",
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 {
await _checkClient();
var transactionsList = await rpcClient?.getTransactionsList(
(await _getKeyPair()).publicKey,
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 txAmount = Amount(
rawValue:
BigInt.from(tx.meta!.postBalances[1] - tx.meta!.preBalances[1]),
fractionDigits: cryptoCurrency.fractionDigits,
);
if ((senderAddress == (await _getKeyPair()).address) &&
(receiverAddress == (await _getKeyPair()).address)) {
txType = isar.TransactionType.sentToSelf;
} else if (senderAddress == (await _getKeyPair()).address) {
txType = isar.TransactionType.outgoing;
} else if (receiverAddress == (await _getKeyPair()).address) {
txType = isar.TransactionType.incoming;
}
var transaction = isar.Transaction(
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,
);
var txAddress = Address(
walletId: walletId,
value: receiverAddress,
publicKey: List<int>.empty(),
derivationIndex: 0,
derivationPath: DerivationPath()..value = "m/44'/501'/0'/0'",
type: AddressType.solana,
subType: txType == isar.TransactionType.outgoing
? AddressSubType.unknown
: AddressSubType.receiving);
txsList.add(Tuple2(transaction, txAddress));
}
await mainDB.addNewTransactionData(txsList, walletId);
} catch (e, s) {
Logging.instance.log(
"Error occurred in solana_wallet.dart while getting"
" transactions for solana: $e\n$s",
level: LogLevel.Error,
);
}
}
@override
Future<bool> updateUTXOs() {
// No UTXOs in Solana
return Future.value(false);
}
/// Make sure the Solana RpcClient uses Tor if it's enabled.
///
/// TODO: Make synchronous.
Future<void> _checkClient() async {
if (prefs.useTor) {
final ({InternetAddress host, int port}) proxyInfo =
TorService.sharedInstance.getProxyInfo();
// If Tor is enabled, pass the optional proxyInfo to the Solana RpcClient.
rpcClient = RpcClient("${getCurrentNode().host}:${getCurrentNode().port}",
proxyInfo: {'host': proxyInfo.host.address, 'port': proxyInfo.port});
} else {
rpcClient ??=
RpcClient("${getCurrentNode().host}:${getCurrentNode().port}");
}
return;
}
}

View file

@ -52,6 +52,8 @@ import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/paynym_interf
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/private_key_interface.dart';
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart';
import 'impl/solana_wallet.dart';
abstract class Wallet<T extends CryptoCurrency> {
// default to Transaction class. For TransactionV2 set to 2
int get isarTransactionVersion => 1;
@ -362,6 +364,9 @@ abstract class Wallet<T extends CryptoCurrency> {
case Coin.particl:
return ParticlWallet(CryptoCurrencyNetwork.main);
case Coin.solana:
return SolanaWallet(CryptoCurrencyNetwork.main);
case Coin.stellar:
return StellarWallet(CryptoCurrencyNetwork.main);
case Coin.stellarTestnet:

View file

@ -13,6 +13,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:solana/solana.dart';
import 'package:stackwallet/models/node_model.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart';
@ -213,6 +214,20 @@ class _NodeCardState extends ConsumerState<NodeCard> {
testPassed = false;
}
break;
case Coin.solana:
try {
RpcClient rpcClient;
if (node.host.startsWith("http") || node.host.startsWith("https")) {
rpcClient = RpcClient("${node.host}:${node.port}");
} else {
rpcClient = RpcClient("http://${node.host}:${node.port}");
}
await rpcClient.getEpochInfo().then((value) => testPassed = true);
} catch (_) {
testPassed = false;
}
break;
}
if (testPassed) {

View file

@ -13,6 +13,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:solana/solana.dart';
import 'package:stackwallet/models/node_model.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart';
@ -182,6 +183,20 @@ class NodeOptionsSheet extends ConsumerWidget {
case Coin.stellarTestnet:
throw UnimplementedError();
//TODO: check network/node
case Coin.solana:
try {
RpcClient rpcClient;
if (node.host.startsWith("http") || node.host.startsWith("https")) {
rpcClient = RpcClient("${node.host}:${node.port}");
} else {
rpcClient = RpcClient("http://${node.host}:${node.port}");
}
await rpcClient.getEpochInfo().then((value) => testPassed = true);
} catch (_) {
testPassed = false;
}
break;
}
if (testPassed) {

View file

@ -158,6 +158,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.1"
borsh_annotation:
dependency: transitive
description:
name: borsh_annotation
sha256: "4a226cf8b7a165ecf8020c0c8d366b2728167fd102ef9b9e89d94d86f89ac57b"
url: "https://pub.dev"
source: hosted
version: "0.3.1+5"
bs58check:
dependency: "direct main"
description:
@ -322,10 +330,10 @@ packages:
dependency: transitive
description:
name: coverage
sha256: "8acabb8306b57a409bf4c83522065672ee13179297a6bb0cb9ead73948df7c76"
sha256: "595a29b55ce82d53398e1bcc2cba525d7bd7c59faeb2d2540e9d42c390cfeeeb"
url: "https://pub.dev"
source: hosted
version: "1.7.2"
version: "1.6.4"
cross_file:
dependency: transitive
description:
@ -506,6 +514,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.9"
ed25519_hd_key:
dependency: transitive
description:
name: ed25519_hd_key
sha256: c5c9f11a03f5789bf9dcd9ae88d641571c802640851f1cacdb13123f171b3a26
url: "https://pub.dev"
source: hosted
version: "2.2.1"
eip1559:
dependency: transitive
description:
@ -588,13 +604,13 @@ packages:
source: hosted
version: "2.1.0"
file:
dependency: transitive
dependency: "direct overridden"
description:
name: file
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
version: "6.1.4"
file_picker:
dependency: "direct main"
description:
@ -815,6 +831,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
freezed_annotation:
dependency: transitive
description:
name: freezed_annotation
sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d
url: "https://pub.dev"
source: hosted
version: "2.4.1"
frontend_server_client:
dependency: transitive
description:
@ -1041,30 +1065,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.0"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa"
url: "https://pub.dev"
source: hosted
version: "10.0.0"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0
url: "https://pub.dev"
source: hosted
version: "2.0.1"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47
url: "https://pub.dev"
source: hosted
version: "2.0.1"
lelantus:
dependency: "direct main"
description:
@ -1108,18 +1108,18 @@ packages:
dependency: transitive
description:
name: matcher
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
url: "https://pub.dev"
source: hosted
version: "0.12.16+1"
version: "0.12.16"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
url: "https://pub.dev"
source: hosted
version: "0.8.0"
version: "0.5.0"
memoize:
dependency: transitive
description:
@ -1132,10 +1132,10 @@ packages:
dependency: "direct main"
description:
name: meta
sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
url: "https://pub.dev"
source: hosted
version: "1.11.0"
version: "1.10.0"
mime:
dependency: transitive
description:
@ -1236,10 +1236,10 @@ packages:
dependency: transitive
description:
name: path
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
url: "https://pub.dev"
source: hosted
version: "1.9.0"
version: "1.8.3"
path_parsing:
dependency: transitive
description:
@ -1356,10 +1356,10 @@ packages:
dependency: transitive
description:
name: platform
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102
url: "https://pub.dev"
source: hosted
version: "3.1.4"
version: "3.1.2"
plugin_platform_interface:
dependency: transitive
description:
@ -1393,13 +1393,13 @@ packages:
source: hosted
version: "1.2.0-beta-1"
process:
dependency: transitive
dependency: "direct overridden"
description:
name: process
sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32"
sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09"
url: "https://pub.dev"
source: hosted
version: "5.0.2"
version: "4.2.4"
protobuf:
dependency: transitive
description:
@ -1545,10 +1545,10 @@ packages:
dependency: "direct main"
description:
name: socks5_proxy
sha256: e0cba6917cd374de6f6cb0ce081e50e6efc24c61644b8e9f20c8bf8b91bb0b75
sha256: "1d21b5606169654bbf4cfb904e8e6ed897e9f763358709f87310c757096d909a"
url: "https://pub.dev"
source: hosted
version: "1.0.3+dev.3"
version: "1.0.4"
socks_socket:
dependency: "direct main"
description:
@ -1558,6 +1558,15 @@ packages:
url: "https://github.com/cypherstack/socks_socket.git"
source: git
version: "0.1.0"
solana:
dependency: "direct main"
description:
path: "packages/solana"
ref: "2d7189d31f1bfd5d6779268c81a897f03f339f5d"
resolved-ref: "2d7189d31f1bfd5d6779268c81a897f03f339f5d"
url: "https://github.com/cypherstack/espresso-cash-public.git"
source: git
version: "0.30.4"
source_gen:
dependency: transitive
description:
@ -1901,10 +1910,10 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957
sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583
url: "https://pub.dev"
source: hosted
version: "13.0.0"
version: "11.10.0"
wakelock:
dependency: "direct main"
description:
@ -1998,10 +2007,10 @@ packages:
dependency: transitive
description:
name: webdriver
sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e"
sha256: "3c923e918918feeb90c4c9fdf1fe39220fa4c0e8e2c0fffaded174498ef86c49"
url: "https://pub.dev"
source: hosted
version: "3.0.3"
version: "3.0.2"
webkit_inspection_protocol:
dependency: transitive
description:
@ -2036,13 +2045,13 @@ packages:
source: git
version: "0.1.0"
xdg_directories:
dependency: transitive
dependency: "direct overridden"
description:
name: xdg_directories
sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d
sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86
url: "https://pub.dev"
source: hosted
version: "1.0.4"
version: "0.2.0+3"
xml:
dependency: transitive
description:

View file

@ -177,6 +177,11 @@ dependencies:
url: https://github.com/cypherstack/electrum_adapter.git
ref: 9e9441fc1e9ace8907256fff05fe2c607b0933b6
stream_channel: ^2.1.0
solana:
git: # TODO: Revert to official package once Tor support is merged upstream.
url: https://github.com/cypherstack/espresso-cash-public.git
ref: 2d7189d31f1bfd5d6779268c81a897f03f339f5d # tor branch.
path: packages/solana
dev_dependencies:
flutter_test:
@ -251,6 +256,12 @@ dependency_overrides:
crypto: 3.0.2
analyzer: ^5.2.0
pinenacl: ^0.3.3
xdg_directories: ^0.2.0
# flutter_local_notifications_linux: ^0.5.0+1 # Overridden by Solana's package (from espresso_cash
# _public). Disabled for compatibility reasons, may affect Linux desktop notifications.
process: ^4.0.0
file: ^6.0.0
http: ^0.13.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec