diff --git a/assets/default_themes/dark.zip b/assets/default_themes/dark.zip index f69e936f2..c6d417500 100644 Binary files a/assets/default_themes/dark.zip and b/assets/default_themes/dark.zip differ diff --git a/assets/default_themes/light.zip b/assets/default_themes/light.zip index 672fe6d49..d94ce2ac8 100644 Binary files a/assets/default_themes/light.zip and b/assets/default_themes/light.zip differ diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart index ef7ae7f31..7a656c288 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart @@ -169,6 +169,8 @@ class _AddEditNodeViewState extends ConsumerState { case Coin.firo: case Coin.namecoin: case Coin.particl: + case Coin.peercoin: + case Coin.peercoinTestNet: case Coin.bitcoinFrost: case Coin.bitcoinFrostTestNet: case Coin.bitcoinTestNet: @@ -221,7 +223,8 @@ class _AddEditNodeViewState extends ConsumerState { case Coin.solana: try { RpcClient rpcClient; - if (formData.host!.startsWith("http") || formData.host!.startsWith("https")) { + if (formData.host!.startsWith("http") || + formData.host!.startsWith("https")) { rpcClient = RpcClient("${formData.host}:${formData.port}"); } else { rpcClient = RpcClient("http://${formData.host}:${formData.port}"); @@ -761,6 +764,8 @@ class _NodeFormState extends ConsumerState { case Coin.namecoin: case Coin.bitcoincash: case Coin.particl: + case Coin.peercoin: + case Coin.peercoinTestNet: case Coin.tezos: case Coin.bitcoinTestNet: case Coin.litecoinTestNet: diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart index c5516db55..1034d986c 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart @@ -142,6 +142,8 @@ class _NodeDetailsViewState extends ConsumerState { case Coin.dogecoin: case Coin.firo: case Coin.particl: + case Coin.peercoin: + case Coin.peercoinTestNet: case Coin.bitcoinTestNet: case Coin.firoTestNet: case Coin.dogecoinTestNet: diff --git a/lib/supported_coins.dart b/lib/supported_coins.dart index 48960c03e..70ee44ad8 100644 --- a/lib/supported_coins.dart +++ b/lib/supported_coins.dart @@ -13,6 +13,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/peercoin.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'; @@ -52,6 +53,8 @@ class SupportedCoins { return Monero(CryptoCurrencyNetwork.main); case Coin.particl: return Particl(CryptoCurrencyNetwork.main); + case Coin.peercoin: + return Peercoin(CryptoCurrencyNetwork.main); case Coin.solana: return Solana(CryptoCurrencyNetwork.main); case Coin.stellar: @@ -80,6 +83,8 @@ class SupportedCoins { return Dogecoin(CryptoCurrencyNetwork.test); case Coin.stellarTestnet: return Stellar(CryptoCurrencyNetwork.test); + case Coin.peercoinTestNet: + return Peercoin(CryptoCurrencyNetwork.test); } } } diff --git a/lib/themes/color_theme.dart b/lib/themes/color_theme.dart index c67f08ce8..e4dbcabc3 100644 --- a/lib/themes/color_theme.dart +++ b/lib/themes/color_theme.dart @@ -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 peercoin => const Color(0xFF3CB054); Color get solana => const Color(0xFFC696FF); Color get stellar => const Color(0xFF6600FF); Color get nano => const Color(0xFF209CE9); @@ -67,6 +68,10 @@ class CoinThemeColorDefault { return wownero; case Coin.particl: return particl; + case Coin.peercoin: + return peercoin; + case Coin.peercoinTestNet: + return peercoin; case Coin.solana: return solana; case Coin.stellar: diff --git a/lib/themes/stack_colors.dart b/lib/themes/stack_colors.dart index 1f994934e..11e146d1b 100644 --- a/lib/themes/stack_colors.dart +++ b/lib/themes/stack_colors.dart @@ -1709,6 +1709,9 @@ class StackColors extends ThemeExtension { return _coin.wownero; case Coin.particl: return _coin.particl; + case Coin.peercoin: + case Coin.peercoinTestNet: + return _coin.peercoin; case Coin.solana: return _coin.solana; case Coin.stellar: diff --git a/lib/themes/theme_service.dart b/lib/themes/theme_service.dart index b41775ee6..b5882ecc5 100644 --- a/lib/themes/theme_service.dart +++ b/lib/themes/theme_service.dart @@ -29,7 +29,7 @@ final pThemeService = Provider((ref) { }); class ThemeService { - static const _currentDefaultThemeVersion = 8; + static const _currentDefaultThemeVersion = 9; ThemeService._(); static ThemeService? _instance; static ThemeService get instance => _instance ??= ThemeService._(); diff --git a/lib/utilities/address_utils.dart b/lib/utilities/address_utils.dart index 722d9f9af..65e0231fe 100644 --- a/lib/utilities/address_utils.dart +++ b/lib/utilities/address_utils.dart @@ -32,6 +32,8 @@ import 'package:stackwallet/wallets/crypto_currency/coins/tezos.dart'; import 'package:stackwallet/wallets/crypto_currency/coins/wownero.dart'; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; +import '../wallets/crypto_currency/coins/peercoin.dart'; + class AddressUtils { static String condenseAddress(String address) { return '${address.substring(0, 5)}...${address.substring(address.length - 5)}'; @@ -68,6 +70,8 @@ class AddressUtils { return Namecoin(CryptoCurrencyNetwork.main).validateAddress(address); case Coin.particl: return Particl(CryptoCurrencyNetwork.main).validateAddress(address); + case Coin.peercoin: + return Peercoin(CryptoCurrencyNetwork.main).validateAddress(address); case Coin.solana: return Solana(CryptoCurrencyNetwork.main).validateAddress(address); case Coin.stellar: @@ -91,6 +95,8 @@ class AddressUtils { return Firo(CryptoCurrencyNetwork.test).validateAddress(address); case Coin.dogecoinTestNet: return Dogecoin(CryptoCurrencyNetwork.test).validateAddress(address); + case Coin.peercoinTestNet: + return Peercoin(CryptoCurrencyNetwork.test).validateAddress(address); case Coin.stellarTestnet: return Stellar(CryptoCurrencyNetwork.test).validateAddress(address); } diff --git a/lib/utilities/amount/amount_unit.dart b/lib/utilities/amount/amount_unit.dart index cfa9fec30..e74de6510 100644 --- a/lib/utilities/amount/amount_unit.dart +++ b/lib/utilities/amount/amount_unit.dart @@ -39,6 +39,7 @@ enum AmountUnit { case Coin.firo: case Coin.litecoin: case Coin.particl: + case Coin.peercoin: case Coin.namecoin: case Coin.bitcoinFrost: case Coin.bitcoinFrostTestNet: @@ -47,6 +48,7 @@ enum AmountUnit { case Coin.bitcoincashTestnet: case Coin.dogecoinTestNet: case Coin.firoTestNet: + case Coin.peercoinTestNet: case Coin.bitcoin: case Coin.bitcoincash: case Coin.dogecoin: diff --git a/lib/utilities/block_explorers.dart b/lib/utilities/block_explorers.dart index fcb2f5717..cf67697b6 100644 --- a/lib/utilities/block_explorers.dart +++ b/lib/utilities/block_explorers.dart @@ -68,6 +68,11 @@ Uri getDefaultBlockExplorerUrlFor({ return Uri.parse("https://tzstats.com/$txid"); case Coin.solana: return Uri.parse("https://explorer.solana.com/tx/$txid"); + case Coin.peercoin: + return Uri.parse("https://chainz.cryptoid.info/ppc/tx.dws?$txid.htm"); + case Coin.peercoinTestNet: + return Uri.parse( + "https://chainz.cryptoid.info/ppc-test/search.dws?q=$txid.htm"); } } diff --git a/lib/utilities/constants.dart b/lib/utilities/constants.dart index f60a0fe1e..d3fedc666 100644 --- a/lib/utilities/constants.dart +++ b/lib/utilities/constants.dart @@ -47,6 +47,7 @@ abstract class Constants { static final BigInt _satsPerCoin = BigInt.from(100000000); static final BigInt _satsPerCoinTezos = BigInt.from(1000000); static final BigInt _satsPerCoinSolana = BigInt.from(1000000000); + static final BigInt _satsPerCoinPeercoin = BigInt.from(1000000); // 1*10^6. static const int _decimalPlaces = 8; static const int _decimalPlacesNano = 30; static const int _decimalPlacesBanano = 29; @@ -57,6 +58,7 @@ abstract class Constants { static const int _decimalPlacesStellar = 7; static const int _decimalPlacesTezos = 6; static const int _decimalPlacesSolana = 9; + static const int _decimalPlacesPeercoin = 6; static const int notificationsMax = 0xFFFFFFFF; static const Duration networkAliveTimerDuration = Duration(seconds: 10); @@ -114,6 +116,10 @@ abstract class Constants { case Coin.solana: return _satsPerCoinSolana; + + case Coin.peercoin: + case Coin.peercoinTestNet: + return _satsPerCoinPeercoin; } } @@ -163,6 +169,10 @@ abstract class Constants { case Coin.solana: return _decimalPlacesSolana; + + case Coin.peercoin: + case Coin.peercoinTestNet: + return _decimalPlacesPeercoin; } } @@ -184,6 +194,8 @@ abstract class Constants { case Coin.ethereum: case Coin.namecoin: case Coin.particl: + values.addAll([12, 24]); + break; case Coin.solana: case Coin.nano: case Coin.stellar: @@ -206,6 +218,10 @@ abstract class Constants { case Coin.bitcoinFrost: case Coin.bitcoinFrostTestNet: throw ArgumentError("Frost mnemonic lengths unsupported"); + case Coin.peercoin: + case Coin.peercoinTestNet: + values.addAll([12, /*15, 18, 21,*/ 24]); // TODO [prio=low]: Test rest. + break; } return values; } @@ -220,6 +236,8 @@ abstract class Constants { case Coin.bitcoincash: case Coin.bitcoincashTestnet: case Coin.eCash: + case Coin.peercoin: + case Coin.peercoinTestNet: return 600; case Coin.dogecoin: @@ -291,6 +309,8 @@ abstract class Constants { case Coin.nano: case Coin.banano: case Coin.epicCash: + case Coin.peercoin: // TODO [prio=low]: Verify default seed length. + case Coin.peercoinTestNet: case Coin.stellar: case Coin.stellarTestnet: case Coin.tezos: diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart index 25d730c43..2e2bd71cc 100644 --- a/lib/utilities/default_nodes.dart +++ b/lib/utilities/default_nodes.dart @@ -17,30 +17,9 @@ abstract class DefaultNodes { static const String defaultName = "Stack Default"; @Deprecated("old and decrepit") - static List get all => [ - bitcoin, - litecoin, - dogecoin, - firo, - monero, - eCash, - epicCash, - ethereum, - bitcoincash, - namecoin, - wownero, - particl, - stellar, - nano, - banano, - tezos, - bitcoinTestnet, - litecoinTestNet, - bitcoincashTestnet, - dogecoinTestnet, - firoTestnet, - stellarTestnet, - ]; + static List get all => Coin.values + .map((e) => DefaultNodes.getNodeFor(e)) + .toList(growable: false); static NodeModel get bitcoin => NodeModel( host: "bitcoin.stackwallet.com", @@ -188,8 +167,21 @@ abstract class DefaultNodes { isDown: false, ); + static NodeModel get peercoin => NodeModel( + host: "electrum.peercoinexplorer.net", + port: 50002, + name: DefaultNodes.defaultName, + id: DefaultNodes.buildId(Coin.peercoin), + useSSL: true, + enabled: true, + coinName: Coin.peercoin.name, + isFailover: true, + isDown: false, + ); + static NodeModel get solana => NodeModel( - host: "https://api.mainnet-beta.solana.com", // TODO: Change this to stack wallet one + host: + "https://api.mainnet-beta.solana.com", // TODO: Change this to stack wallet one port: 443, name: DefaultNodes.defaultName, id: DefaultNodes.buildId(Coin.solana), @@ -309,6 +301,18 @@ abstract class DefaultNodes { isDown: false, ); + static NodeModel get peercoinTestNet => NodeModel( + host: "testnet-electrum.peercoinexplorer.net", + port: 50002, + name: DefaultNodes.defaultName, + id: DefaultNodes.buildId(Coin.peercoinTestNet), + useSSL: true, + enabled: true, + coinName: Coin.peercoinTestNet.name, + isFailover: true, + isDown: false, + ); + static NodeModel get stellarTestnet => NodeModel( host: "https://horizon-testnet.stellar.org/", port: 50022, @@ -360,6 +364,12 @@ abstract class DefaultNodes { case Coin.particl: return particl; + case Coin.peercoin: + return peercoin; + + case Coin.peercoinTestNet: + return peercoinTestNet; + case Coin.solana: return solana; diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index 6551dbc62..903054b09 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -26,6 +26,7 @@ enum Coin { namecoin, nano, particl, + peercoin, solana, stellar, tezos, @@ -42,6 +43,7 @@ enum Coin { dogecoinTestNet, firoTestNet, litecoinTestNet, + peercoinTestNet, stellarTestnet, } @@ -70,6 +72,8 @@ extension CoinExt on Coin { return "Monero"; case Coin.particl: return "Particl"; + case Coin.peercoin: + return "Peercoin"; case Coin.solana: return "Solana"; case Coin.stellar: @@ -96,6 +100,8 @@ extension CoinExt on Coin { return "tFiro"; case Coin.dogecoinTestNet: return "tDogecoin"; + case Coin.peercoinTestNet: + return "tPeercoin"; case Coin.stellarTestnet: return "tStellar"; } @@ -124,6 +130,8 @@ extension CoinExt on Coin { return "XMR"; case Coin.particl: return "PART"; + case Coin.peercoin: + return "PPC"; case Coin.solana: return "SOL"; case Coin.stellar: @@ -149,6 +157,8 @@ extension CoinExt on Coin { return "tFIRO"; case Coin.dogecoinTestNet: return "tDOGE"; + case Coin.peercoinTestNet: + return "tPPC"; case Coin.stellarTestnet: return "tXLM"; } @@ -178,6 +188,8 @@ extension CoinExt on Coin { return "monero"; case Coin.particl: return "particl"; + case Coin.peercoin: + return "peercoin"; case Coin.solana: return "solana"; case Coin.stellar: @@ -203,6 +215,8 @@ extension CoinExt on Coin { return "firo"; case Coin.dogecoinTestNet: return "dogecoin"; + case Coin.peercoinTestNet: + return "peercoin"; case Coin.stellarTestnet: return "stellar"; } @@ -222,6 +236,8 @@ extension CoinExt on Coin { case Coin.firoTestNet: case Coin.namecoin: case Coin.particl: + case Coin.peercoin: + case Coin.peercoinTestNet: case Coin.ethereum: case Coin.eCash: case Coin.stellar: @@ -255,6 +271,8 @@ extension CoinExt on Coin { case Coin.firo: case Coin.namecoin: case Coin.particl: + case Coin.peercoin: + case Coin.peercoinTestNet: case Coin.eCash: case Coin.epicCash: case Coin.monero: @@ -284,6 +302,7 @@ extension CoinExt on Coin { case Coin.firo: case Coin.namecoin: case Coin.particl: + case Coin.peercoin: case Coin.epicCash: case Coin.ethereum: case Coin.monero: @@ -302,6 +321,7 @@ extension CoinExt on Coin { case Coin.litecoinTestNet: case Coin.bitcoincashTestnet: case Coin.firoTestNet: + case Coin.peercoinTestNet: case Coin.stellarTestnet: return true; } @@ -328,6 +348,7 @@ extension CoinExt on Coin { case Coin.firo: case Coin.namecoin: case Coin.particl: + case Coin.peercoin: case Coin.epicCash: case Coin.ethereum: case Coin.monero: @@ -358,6 +379,9 @@ extension CoinExt on Coin { case Coin.firoTestNet: return Coin.firo; + case Coin.peercoinTestNet: + return Coin.peercoin; + case Coin.stellarTestnet: return Coin.stellar; } @@ -374,6 +398,8 @@ extension CoinExt on Coin { case Coin.litecoinTestNet: case Coin.namecoin: case Coin.particl: + case Coin.peercoin: + case Coin.peercoinTestNet: return AddressType.p2wpkh; case Coin.bitcoinFrost: @@ -462,6 +488,15 @@ Coin coinFromPrettyName(String name) { case "particl": return Coin.particl; + case "Peercoin": + case "peercoin": + return Coin.peercoin; + + case "tPeercoin": + case "Peercoin Testnet": + case "peercoinTestNet": + return Coin.peercoinTestNet; + case "Solana": case "solana": return Coin.solana; diff --git a/lib/utilities/enums/derive_path_type_enum.dart b/lib/utilities/enums/derive_path_type_enum.dart index 50ec1ab59..4dcaef022 100644 --- a/lib/utilities/enums/derive_path_type_enum.dart +++ b/lib/utilities/enums/derive_path_type_enum.dart @@ -63,6 +63,8 @@ extension DerivePathTypeExt on DerivePathType { case Coin.litecoinTestNet: case Coin.namecoin: case Coin.particl: + case Coin.peercoin: + case Coin.peercoinTestNet: return DerivePathType.bip84; case Coin.eCash: diff --git a/lib/wallets/crypto_currency/coins/peercoin.dart b/lib/wallets/crypto_currency/coins/peercoin.dart new file mode 100644 index 000000000..4e54329de --- /dev/null +++ b/lib/wallets/crypto_currency/coins/peercoin.dart @@ -0,0 +1,171 @@ +import 'package:coinlib/src/network.dart'; +import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib; +import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; +import 'package:stackwallet/models/node_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/utilities/enums/derive_path_type_enum.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; +import 'package:stackwallet/wallets/crypto_currency/intermediate/bip39_hd_currency.dart'; + +class Peercoin extends Bip39HDCurrency { + Peercoin(super.network) { + switch (network) { + case CryptoCurrencyNetwork.main: + coin = Coin.peercoin; + case CryptoCurrencyNetwork.test: + coin = Coin.peercoinTestNet; + default: + throw Exception("Unsupported network: $network"); + } + } + + @override + int get minConfirms => 1; + + @override + bool get torSupport => true; + + @override + String constructDerivePath({ + required DerivePathType derivePathType, + int account = 0, + required int chain, + required int index, + }) { + String coinType; + switch (networkParams.wifPrefix) { + case 183: // PPC mainnet wif. + coinType = + "6"; // according to https://github.com/satoshilabs/slips/blob/master/slip-0044.md + break; + case 239: // PPC testnet wif. + coinType = "1"; + break; + default: + throw Exception("Invalid Peercoin network wif used!"); + } + + int purpose; + switch (derivePathType) { + case DerivePathType.bip44: + purpose = 44; + break; + case DerivePathType.bip84: + purpose = 84; + break; + default: + throw Exception("DerivePathType $derivePathType not supported"); + } + + return "m/$purpose'/$coinType'/$account'/$chain/$index"; + } + + @override + NodeModel get defaultNode { + switch (network) { + case CryptoCurrencyNetwork.main: + return DefaultNodes.peercoin; + case CryptoCurrencyNetwork.test: + return DefaultNodes.peercoinTestNet; + default: + throw UnimplementedError(); + } + } + + @override + Amount get dustLimit => Amount( + rawValue: BigInt.from(294), + fractionDigits: Coin.peercoin.decimals, + ); + + @override + String get genesisHash { + switch (network) { + case CryptoCurrencyNetwork.main: + return "0000000032fe677166d54963b62a4677d8957e87c508eaa4fd7eb1c880cd27e3"; + case CryptoCurrencyNetwork.test: + return "00000001f757bb737f6596503e17cd17b0658ce630cc727c0cca81aec47c9f06"; + default: + throw Exception("Unsupported network: $network"); + } + } + + @override + ({coinlib.Address address, AddressType addressType}) getAddressForPublicKey( + {required coinlib.ECPublicKey publicKey, + required DerivePathType derivePathType}) { + switch (derivePathType) { + // case DerivePathType.bip16: + + case DerivePathType.bip44: + final addr = coinlib.P2PKHAddress.fromPublicKey( + publicKey, + version: networkParams.p2pkhPrefix, + ); + + return (address: addr, addressType: AddressType.p2pkh); + + case DerivePathType.bip49: + final p2wpkhScript = coinlib.P2WPKHAddress.fromPublicKey( + publicKey, + hrp: networkParams.bech32Hrp, + ).program.script; + + final addr = coinlib.P2SHAddress.fromRedeemScript( + p2wpkhScript, + version: networkParams.p2shPrefix, + ); + + return (address: addr, addressType: AddressType.p2sh); + + case DerivePathType.bip84: + final addr = coinlib.P2WPKHAddress.fromPublicKey( + publicKey, + hrp: networkParams.bech32Hrp, + ); + + return (address: addr, addressType: AddressType.p2wpkh); + + default: + throw Exception("DerivePathType $derivePathType not supported"); + } + } + + @override + coinlib.Network get networkParams { + switch (network) { + case CryptoCurrencyNetwork.main: + return Network.mainnet; + case CryptoCurrencyNetwork.test: + return Network.testnet; + default: + throw Exception("Unsupported network: $network"); + } + } + + @override + List get supportedDerivationPathTypes => [ + DerivePathType.bip44, + DerivePathType.bip84, + ]; + + @override + bool validateAddress(String address) { + try { + coinlib.Address.fromString(address, networkParams); + return true; + } catch (_) { + return false; + } + } + + @override + bool operator ==(Object other) { + return other is Peercoin && other.network == network; + } + + @override + int get hashCode => Object.hash(Peercoin, network); +} diff --git a/lib/wallets/wallet/impl/peercoin_wallet.dart b/lib/wallets/wallet/impl/peercoin_wallet.dart new file mode 100644 index 000000000..e08566f45 --- /dev/null +++ b/lib/wallets/wallet/impl/peercoin_wallet.dart @@ -0,0 +1,297 @@ +import 'package:isar/isar.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/v2/input_v2.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/v2/output_v2.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/wallets/crypto_currency/coins/peercoin.dart'; +import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart'; +import 'package:stackwallet/wallets/wallet/intermediate/bip39_hd_wallet.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart'; +import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart'; + +class PeercoinWallet extends Bip39HDWallet + with ElectrumXInterface, CoinControlInterface { + @override + int get isarTransactionVersion => 2; + + PeercoinWallet(CryptoCurrencyNetwork network) : super(Peercoin(network)); + + @override + FilterOperation? get changeAddressFilterOperation => + FilterGroup.and(standardChangeAddressFilters); + + @override + FilterOperation? get receivingAddressFilterOperation => + FilterGroup.and(standardReceivingAddressFilters); + + // =========================================================================== + + @override + Future> fetchAddressesForElectrumXScan() async { + final allAddresses = await mainDB + .getAddresses(walletId) + .filter() + .not() + .group( + (q) => q + .typeEqualTo(AddressType.nonWallet) + .or() + .subTypeEqualTo(AddressSubType.nonWallet), + ) + .findAll(); + return allAddresses; + } + + // =========================================================================== + + @override + Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + // TODO: actually do this properly for peercoin + // this is probably wrong for peercoin + return Amount( + rawValue: BigInt.from( + ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * + (feeRatePerKB / 1000).ceil(), + ), + fractionDigits: cryptoCurrency.fractionDigits, + ); + } + + /// we can just pretend vSize is size for peercoin + @override + int estimateTxFee({required int vSize, required int feeRatePerKB}) { + return vSize * (feeRatePerKB / 1000).ceil(); + } + + // =========================================================================== + + @override + Future< + ({ + bool blocked, + String? blockedReason, + String? utxoLabel, + })> checkBlockUTXO( + Map jsonUTXO, + String? scriptPubKeyHex, + Map jsonTX, + String? utxoOwnerAddress, + ) async { + // TODO [prio=high]: Check if Peercoin has outputs (eg stakes etc) to block. + return (blocked: false, blockedReason: null, utxoLabel: null); + } + + @override + Future updateTransactions() async { + // Get all addresses. + final List
allAddressesOld = + await fetchAddressesForElectrumXScan(); + + // Separate receiving and change addresses. + final Set receivingAddresses = allAddressesOld + .where((e) => e.subType == AddressSubType.receiving) + .map((e) => e.value) + .toSet(); + final Set changeAddresses = allAddressesOld + .where((e) => e.subType == AddressSubType.change) + .map((e) => e.value) + .toSet(); + + // Remove duplicates. + final allAddressesSet = {...receivingAddresses, ...changeAddresses}; + + // Fetch history from ElectrumX. + final List> allTxHashes = + await fetchHistory(allAddressesSet); + + // Only parse new txs (not in db yet). + final List> allTransactions = []; + for (final txHash in allTxHashes) { + // Check for duplicates by searching for tx by tx_hash in db. + final storedTx = await mainDB.isar.transactionV2s + .where() + .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) + .findFirst(); + + if (storedTx == null || + storedTx.height == null || + (storedTx.height != null && storedTx.height! <= 0)) { + // Tx not in db yet. + final tx = await electrumXCachedClient.getTransaction( + txHash: txHash["tx_hash"] as String, + verbose: true, + coin: cryptoCurrency.coin, + ); + + // Only tx to list once. + if (allTransactions + .indexWhere((e) => e["txid"] == tx["txid"] as String) == + -1) { + tx["height"] = txHash["height"]; + allTransactions.add(tx); + } + } + } + + // Parse all new txs. + final List txns = []; + for (final txData in allTransactions) { + bool wasSentFromThisWallet = false; + // Set to true if any inputs were detected as owned by this wallet. + + bool wasReceivedInThisWallet = false; + // Set to true if any outputs were detected as owned by this wallet. + + // Parse inputs. + BigInt amountReceivedInThisWallet = BigInt.zero; + BigInt changeAmountReceivedInThisWallet = BigInt.zero; + final List inputs = []; + for (final jsonInput in txData["vin"] as List) { + final map = Map.from(jsonInput as Map); + + final List addresses = []; + String valueStringSats = "0"; + OutpointV2? outpoint; + + final coinbase = map["coinbase"] as String?; + + if (coinbase == null) { + // Not a coinbase (ie a typical input). + final txid = map["txid"] as String; + final vout = map["vout"] as int; + + final inputTx = await electrumXCachedClient.getTransaction( + txHash: txid, + coin: cryptoCurrency.coin, + ); + + final prevOutJson = Map.from( + (inputTx["vout"] as List).firstWhere((e) => e["n"] == vout) as Map, + ); + + final prevOut = OutputV2.fromElectrumXJson( + prevOutJson, + decimalPlaces: cryptoCurrency.fractionDigits, + isFullAmountNotSats: true, + walletOwns: false, // Doesn't matter here as this is not saved. + ); + + outpoint = OutpointV2.isarCantDoRequiredInDefaultConstructor( + txid: txid, + vout: vout, + ); + valueStringSats = prevOut.valueStringSats; + addresses.addAll(prevOut.addresses); + } + + InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor( + scriptSigHex: map["scriptSig"]?["hex"] as String?, + scriptSigAsm: map["scriptSig"]?["asm"] as String?, + sequence: map["sequence"] as int?, + outpoint: outpoint, + valueStringSats: valueStringSats, + addresses: addresses, + witness: map["witness"] as String?, + coinbase: coinbase, + innerRedeemScriptAsm: map["innerRedeemscriptAsm"] as String?, + // Need addresses before we can know if the wallet owns this input. + walletOwns: false, + ); + + // Check if input was from this wallet. + if (allAddressesSet.intersection(input.addresses.toSet()).isNotEmpty) { + wasSentFromThisWallet = true; + input = input.copyWith(walletOwns: true); + } + + inputs.add(input); + } + + // Parse outputs. + final List outputs = []; + for (final outputJson in txData["vout"] as List) { + OutputV2 output = OutputV2.fromElectrumXJson( + Map.from(outputJson as Map), + decimalPlaces: cryptoCurrency.fractionDigits, + isFullAmountNotSats: true, + // Need addresses before we can know if the wallet owns this input. + walletOwns: false, + ); + + // If output was to my wallet, add value to amount received. + if (receivingAddresses + .intersection(output.addresses.toSet()) + .isNotEmpty) { + wasReceivedInThisWallet = true; + amountReceivedInThisWallet += output.value; + output = output.copyWith(walletOwns: true); + } else if (changeAddresses + .intersection(output.addresses.toSet()) + .isNotEmpty) { + wasReceivedInThisWallet = true; + changeAmountReceivedInThisWallet += output.value; + output = output.copyWith(walletOwns: true); + } + + outputs.add(output); + } + + final totalOut = outputs + .map((e) => e.value) + .fold(BigInt.zero, (value, element) => value + element); + + TransactionType type; + const TransactionSubType subType = TransactionSubType.none; + + // At least one input was owned by this wallet. + if (wasSentFromThisWallet) { + type = TransactionType.outgoing; + + if (wasReceivedInThisWallet) { + if (changeAmountReceivedInThisWallet + amountReceivedInThisWallet == + totalOut) { + // Definitely sent all to self. + type = TransactionType.sentToSelf; + } else if (amountReceivedInThisWallet == BigInt.zero) { + // Most likely just a typical send, do nothing here yet. + } + + // Namecoin doesn't have special outputs like tokens, ordinals, etc. + // But this is where you'd check for special outputs. + } + } else if (wasReceivedInThisWallet) { + // Only found outputs owned by this wallet. + type = TransactionType.incoming; + } else { + Logging.instance.log( + "Unexpected tx found (ignoring it): $txData", + level: LogLevel.Error, + ); + continue; + } + + final tx = TransactionV2( + walletId: walletId, + blockHash: txData["blockhash"] as String?, + hash: txData["hash"] as String, + txid: txData["txid"] as String, + height: txData["height"] as int?, + version: txData["version"] as int, + timestamp: txData["blocktime"] as int? ?? + DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, + inputs: List.unmodifiable(inputs), + outputs: List.unmodifiable(outputs), + type: type, + subType: subType, + otherData: null, + ); + + txns.add(tx); + } + + await mainDB.updateOrPutTransactionV2s(txns); + } +} diff --git a/lib/wallets/wallet/wallet.dart b/lib/wallets/wallet/wallet.dart index 850e2449e..549bfefb2 100644 --- a/lib/wallets/wallet/wallet.dart +++ b/lib/wallets/wallet/wallet.dart @@ -39,6 +39,7 @@ import 'package:stackwallet/wallets/wallet/impl/monero_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/namecoin_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/nano_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/particl_wallet.dart'; +import 'package:stackwallet/wallets/wallet/impl/peercoin_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/solana_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/stellar_wallet.dart'; import 'package:stackwallet/wallets/wallet/impl/sub_wallets/eth_token_wallet.dart'; @@ -363,6 +364,11 @@ abstract class Wallet { case Coin.particl: return ParticlWallet(CryptoCurrencyNetwork.main); + case Coin.peercoin: + return PeercoinWallet(CryptoCurrencyNetwork.main); + case Coin.peercoinTestNet: + return PeercoinWallet(CryptoCurrencyNetwork.test); + case Coin.solana: return SolanaWallet(CryptoCurrencyNetwork.main); diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index b4fdb7a9d..026e9f5fc 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -24,6 +24,7 @@ import 'package:stackwallet/wallets/crypto_currency/coins/firo.dart'; import 'package:stackwallet/wallets/crypto_currency/intermediate/bip39_hd_currency.dart'; import 'package:stackwallet/wallets/models/tx_data.dart'; import 'package:stackwallet/wallets/wallet/impl/bitcoin_wallet.dart'; +import 'package:stackwallet/wallets/wallet/impl/peercoin_wallet.dart'; import 'package:stackwallet/wallets/wallet/intermediate/bip39_hd_wallet.dart'; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart'; @@ -768,7 +769,8 @@ mixin ElectrumXInterface on Bip39HDWallet { return txData.copyWith( raw: clTx.toHex(), - vSize: clTx.vSize(), + // dirty shortcut for peercoin's weirdness + vSize: this is PeercoinWallet ? clTx.size : clTx.vSize(), tempTx: TransactionV2( walletId: walletId, blockHash: null, diff --git a/lib/widgets/node_card.dart b/lib/widgets/node_card.dart index 006364342..a6ac18302 100644 --- a/lib/widgets/node_card.dart +++ b/lib/widgets/node_card.dart @@ -173,6 +173,8 @@ class _NodeCardState extends ConsumerState { case Coin.eCash: case Coin.bitcoinFrost: case Coin.bitcoinFrostTestNet: + case Coin.peercoin: + case Coin.peercoinTestNet: try { testPassed = await checkElectrumServer( host: node.host, diff --git a/lib/widgets/node_options_sheet.dart b/lib/widgets/node_options_sheet.dart index a91f63fba..31bdec456 100644 --- a/lib/widgets/node_options_sheet.dart +++ b/lib/widgets/node_options_sheet.dart @@ -154,6 +154,8 @@ class NodeOptionsSheet extends ConsumerWidget { case Coin.eCash: case Coin.bitcoinFrost: case Coin.bitcoinFrostTestNet: + case Coin.peercoin: + case Coin.peercoinTestNet: try { testPassed = await checkElectrumServer( host: node.host,