mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2025-04-01 11:59:06 +00:00
add solana
This commit is contained in:
parent
ff86cbccf6
commit
68210b2765
20 changed files with 580 additions and 5 deletions
lib
models/isar/models/blockchain_data
pages/settings_views/global_settings_view/manage_nodes_views
services
themes
utilities
wallets
widgets
|
@ -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.?
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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":
|
||||
|
|
|
@ -55,6 +55,7 @@ extension DerivePathTypeExt on DerivePathType {
|
|||
case Coin.stellar:
|
||||
case Coin.stellarTestnet:
|
||||
case Coin.tezos: // TODO: Is this true?
|
||||
case Coin.solana:
|
||||
throw UnsupportedError(
|
||||
"$coin does not use bitcoin style derivation paths");
|
||||
}
|
||||
|
|
48
lib/wallets/crypto_currency/coins/solana.dart
Normal file
48
lib/wallets/crypto_currency/coins/solana.dart
Normal file
|
@ -0,0 +1,48 @@
|
|||
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) {
|
||||
RegExp regex = RegExp(r'^[a-zA-Z0-9]{44}$');
|
||||
return regex.hasMatch(address);
|
||||
}
|
||||
|
||||
@override
|
||||
String get genesisHash => throw UnimplementedError();
|
||||
}
|
362
lib/wallets/wallet/impl/solana_wallet.dart
Normal file
362
lib/wallets/wallet/impl/solana_wallet.dart
Normal file
|
@ -0,0 +1,362 @@
|
|||
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:solana/dto.dart';
|
||||
import 'package:solana/solana.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/paymint/fee_object_model.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/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:stackwallet/utilities/logger.dart';
|
||||
import 'package:stackwallet/models/balance.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> {
|
||||
SolanaWallet(CryptoCurrencyNetwork network) : super(Solana(network));
|
||||
|
||||
NodeModel? _solNode;
|
||||
|
||||
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: null,
|
||||
type: cryptoCurrency.coin.primaryAddressType,
|
||||
subType: AddressSubType.unknown);
|
||||
return addressStruct;
|
||||
}
|
||||
|
||||
Future<int> _getCurrentBalanceInLamports() async {
|
||||
var rpcClient = RpcClient("${getCurrentNode().host}:${getCurrentNode().port}");
|
||||
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: null,
|
||||
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 {
|
||||
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 rpcClient = RpcClient("${getCurrentNode().host}:${getCurrentNode().port}");
|
||||
final accInfo = await rpcClient.getAccountInfo((await _getKeyPair()).address);
|
||||
final minimumRent = await rpcClient.getMinimumBalanceForRentExemption(accInfo.value!.data.toString().length);
|
||||
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 {
|
||||
final keyPair = await _getKeyPair();
|
||||
final rpcClient = RpcClient("${getCurrentNode().host}:${getCurrentNode().port}");
|
||||
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 {
|
||||
if (info.cachedBalance.spendable.raw == BigInt.zero) {
|
||||
return Amount(
|
||||
rawValue: BigInt.zero,
|
||||
fractionDigits: cryptoCurrency.fractionDigits,
|
||||
);
|
||||
}
|
||||
|
||||
final rpcClient = RpcClient("${getCurrentNode().host}:${getCurrentNode().port}");
|
||||
final fee = await rpcClient.getFees();
|
||||
|
||||
return Amount(
|
||||
rawValue: BigInt.from(fee.value.feeCalculator.lamportsPerSignature),
|
||||
fractionDigits: cryptoCurrency.fractionDigits,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FeeObject> get fees async {
|
||||
final rpcClient = RpcClient("${getCurrentNode().host}:${getCurrentNode().port}");
|
||||
final fees = await rpcClient.getFees();
|
||||
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() {
|
||||
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 {
|
||||
var rpcClient = RpcClient("${getCurrentNode().host}:${getCurrentNode().port}");
|
||||
var balance = await rpcClient.getBalance(info.cachedReceivingAddress);
|
||||
|
||||
// Rent exemption of Solana
|
||||
final accInfo = await rpcClient.getAccountInfo((await _getKeyPair()).address);
|
||||
final minimumRent = await rpcClient.getMinimumBalanceForRentExemption(accInfo.value!.data.toString().length);
|
||||
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 {
|
||||
var rpcClient = RpcClient("${getCurrentNode().host}:${getCurrentNode().port}");
|
||||
var blockHeight = await rpcClient.getSlot();
|
||||
|
||||
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 {
|
||||
var rpcClient = RpcClient("${getCurrentNode().host}:${getCurrentNode().port}");
|
||||
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 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: null,
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -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:
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
40
pubspec.lock
40
pubspec.lock
|
@ -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:
|
||||
|
@ -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,7 +604,7 @@ packages:
|
|||
source: hosted
|
||||
version: "2.1.0"
|
||||
file:
|
||||
dependency: transitive
|
||||
dependency: "direct overridden"
|
||||
description:
|
||||
name: file
|
||||
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
|
||||
|
@ -694,7 +710,7 @@ packages:
|
|||
source: hosted
|
||||
version: "17.0.0"
|
||||
flutter_local_notifications_linux:
|
||||
dependency: transitive
|
||||
dependency: "direct overridden"
|
||||
description:
|
||||
name: flutter_local_notifications_linux
|
||||
sha256: "33f741ef47b5f63cc7f78fe75eeeac7e19f171ff3c3df054d84c1e38bedb6a03"
|
||||
|
@ -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:
|
||||
|
@ -1393,7 +1417,7 @@ packages:
|
|||
source: hosted
|
||||
version: "1.2.0-beta-1"
|
||||
process:
|
||||
dependency: transitive
|
||||
dependency: "direct overridden"
|
||||
description:
|
||||
name: process
|
||||
sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32"
|
||||
|
@ -1558,6 +1582,14 @@ packages:
|
|||
url: "https://github.com/cypherstack/socks_socket.git"
|
||||
source: git
|
||||
version: "0.1.0"
|
||||
solana:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: solana
|
||||
sha256: "99a6a40a847f57ccf4687a730413d67fcaef4fc6778ddd9c3258e7fe8e4c6743"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.30.3"
|
||||
source_gen:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -2036,7 +2068,7 @@ packages:
|
|||
source: git
|
||||
version: "0.1.0"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
dependency: "direct overridden"
|
||||
description:
|
||||
name: xdg_directories
|
||||
sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d
|
||||
|
|
|
@ -177,6 +177,7 @@ dependencies:
|
|||
url: https://github.com/cypherstack/electrum_adapter.git
|
||||
ref: 9e9441fc1e9ace8907256fff05fe2c607b0933b6
|
||||
stream_channel: ^2.1.0
|
||||
solana: ^0.30.3
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
@ -251,6 +252,11 @@ 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
|
||||
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
|
||||
|
||||
|
|
Loading…
Reference in a new issue