Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions lib/utilities/cryptonote_address.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import 'dart:typed_data';

import 'package:pointycastle/digests/keccak.dart';

/// CryptoNote base58: 8 byte blocks encode to 11 characters, the trailing
/// partial block to one of the lengths in [_decodedBlockSize].
const _alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
const _fullBlockSize = 8;
const _fullEncodedBlockSize = 11;

/// Encoded block length -> decoded byte length. `-1` marks lengths CryptoNote
/// base58 can never emit.
const _decodedBlockSize = <int>[0, -1, 1, 2, -1, 3, 4, 5, -1, 6, 7, 8];

const _checksumSize = 4;
const _keySize = 32;
const _paymentIdSize = 8;

final _alphabetIndex = <int, int>{
for (int i = 0; i < _alphabet.length; i++) _alphabet.codeUnitAt(i): i,
};

/// The base58 network tag of [address] (which identifies both the network and
/// whether the address is standard, integrated or a subaddress), or `null` if
/// [address] is not a structurally valid, checksum correct CryptoNote address.
int? cryptonoteAddressTag(String address) {
final raw = _decodeBase58(address);
if (raw == null || raw.length <= _checksumSize) {
return null;
}

final body = raw.sublist(0, raw.length - _checksumSize);
final expected = KeccakDigest(256).process(body);
for (int i = 0; i < _checksumSize; i++) {
if (raw[body.length + i] != expected[i]) {
return null;
}
}

final tag = _readVarint(body);
if (tag == null) {
return null;
}

final payload = body.length - tag.size;
if (payload != 2 * _keySize && payload != 2 * _keySize + _paymentIdSize) {
return null;
}

return tag.value;
}

({int value, int size})? _readVarint(Uint8List bytes) {
int value = 0;
int shift = 0;
for (int i = 0; i < bytes.length; i++) {
value |= (bytes[i] & 0x7f) << shift;
if (bytes[i] & 0x80 == 0) {
return (value: value, size: i + 1);
}
shift += 7;
if (shift > 56) {
return null;
}
}
return null;
}

Uint8List? _decodeBase58(String input) {
if (input.isEmpty) {
return null;
}

final fullBlocks = input.length ~/ _fullEncodedBlockSize;
final lastEncodedSize = input.length % _fullEncodedBlockSize;
final lastSize = _decodedBlockSize[lastEncodedSize];
if (lastSize < 0) {
return null;
}

final out = Uint8List(fullBlocks * _fullBlockSize + lastSize);
for (int i = 0; i <= fullBlocks; i++) {
final start = i * _fullEncodedBlockSize;
final size = i < fullBlocks ? _fullBlockSize : lastSize;
if (size == 0) {
break;
}
final block = input.substring(
start,
i < fullBlocks ? start + _fullEncodedBlockSize : input.length,
);
if (!_decodeBlock(block, out, i * _fullBlockSize, size)) {
return null;
}
}

return out;
}

bool _decodeBlock(String block, Uint8List out, int offset, int size) {
BigInt value = BigInt.zero;
for (final unit in block.codeUnits) {
final digit = _alphabetIndex[unit];
if (digit == null) {
return false;
}
value = value * BigInt.from(_alphabet.length) + BigInt.from(digit);
}

// Reject overlong encodings, which would otherwise decode to a different
// byte string than the one that produced them.
if (value.bitLength > 8 * size) {
return false;
}

for (int i = size - 1; i >= 0; i--) {
out[offset + i] = (value & BigInt.from(0xff)).toInt();
value >>= 8;
}
return true;
}
44 changes: 43 additions & 1 deletion lib/wallets/crypto_currency/coins/monero.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import '../../../models/node_model.dart';
import '../../../utilities/cryptonote_address.dart';
import '../../../utilities/default_nodes.dart';
import '../../../utilities/enums/derive_path_type_enum.dart';
import '../../../wl_gen/interfaces/cs_monero_interface.dart';
import '../crypto_currency.dart';
import '../intermediate/cryptonote_currency.dart';

int moneroNetworkType(CryptoCurrencyNetwork network) => switch (network) {
CryptoCurrencyNetwork.main => 0,
CryptoCurrencyNetwork.test => 1,
CryptoCurrencyNetwork.stage => 2,
_ => throw ArgumentError.value(network, "network", "Unsupported network"),
};

class Monero extends CryptonoteCurrency {
Monero(super.network) {
_idMain = "monero";
Expand All @@ -14,6 +22,10 @@ class Monero extends CryptonoteCurrency {
_id = _idMain;
_name = "Monero";
_ticker = "XMR";
case CryptoCurrencyNetwork.stage:
_id = "${_idMain}Stagenet";
_name = "sMonero";
_ticker = "sXMR";
default:
throw Exception("Unsupported network: $network");
}
Expand Down Expand Up @@ -52,12 +64,20 @@ class Monero extends CryptonoteCurrency {
}
switch (network) {
case CryptoCurrencyNetwork.main:
return csMonero.validateAddress(address, 0);
return csMonero.validateAddress(address, moneroNetworkType(network));
case CryptoCurrencyNetwork.stage:
// monero_c's addressValid() takes an int nettype, which binds to
// wallet2's deprecated addressValid(str, bool testnet) overload, so
// every non zero nettype validates as testnet. Decode here instead.
return _stagenetAddressTags.contains(cryptonoteAddressTag(address));
default:
throw Exception("Unsupported network: $network");
}
}

/// Stagenet base58 tags: standard, integrated, subaddress.
static const _stagenetAddressTags = {24, 25, 36};

@override
NodeModel defaultNode({required bool isPrimary}) {
switch (network) {
Expand All @@ -78,6 +98,26 @@ class Monero extends CryptonoteCurrency {
isPrimary: isPrimary,
);

case CryptoCurrencyNetwork.stage:
// Third party stagenet RPC over plaintext HTTP: untrusted both as a
// daemon and on the wire. Acceptable only because stagenet coins are
// worthless.
return NodeModel(
host: "http://node3.monerodevs.org",
port: 38089,
name: DefaultNodes.defaultName,
id: DefaultNodes.buildId(this),
useSSL: false,
enabled: true,
coinName: identifier,
isFailover: true,
isDown: false,
trusted: false,
torEnabled: true,
clearnetEnabled: true,
isPrimary: isPrimary,
);

default:
throw UnimplementedError();
}
Expand Down Expand Up @@ -114,6 +154,8 @@ class Monero extends CryptonoteCurrency {
switch (network) {
case CryptoCurrencyNetwork.main:
return Uri.parse("https://xmrchain.net/tx/$txid");
case CryptoCurrencyNetwork.stage:
return Uri.parse("https://stagenet.xmrchain.net/tx/$txid");
default:
throw Exception(
"Unsupported network for defaultBlockExplorer(): $network",
Expand Down
2 changes: 1 addition & 1 deletion lib/wallets/crypto_currency/coins/xelis.dart
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ class Xelis extends ElectrumCurrency {
isPrimary: isPrimary,
);

case CryptoCurrencyNetwork.test:
case CryptoCurrencyNetwork.stage:
return NodeModel(
host: "stagenet-node.xelis.io",
port: 443,
Expand Down
4 changes: 3 additions & 1 deletion lib/wallets/crypto_currency/crypto_currency.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ enum CryptoCurrencyNetwork {
test4;

bool get isTestNet =>
this == CryptoCurrencyNetwork.test || this == CryptoCurrencyNetwork.test4;
this == CryptoCurrencyNetwork.test ||
this == CryptoCurrencyNetwork.test4 ||
this == CryptoCurrencyNetwork.stage;
}

abstract class CryptoCurrency {
Expand Down
14 changes: 13 additions & 1 deletion lib/wallets/wallet/impl/monero_wallet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,19 +41,27 @@ class MoneroWallet extends LibMoneroWallet {
Future<WrappedWallet> loadWallet({
required String path,
required String password,
}) => csMonero.loadWallet(walletId, path: path, password: password);
required int network,
}) => csMonero.loadWallet(
walletId,
path: path,
password: password,
network: network,
);

@override
Future<WrappedWallet> getCreatedWallet({
required String path,
required String password,
required int wordCount,
required String seedOffset,
required int network,
}) => csMonero.getCreatedWallet(
path: path,
password: password,
wordCount: wordCount,
seedOffset: seedOffset,
network: network,
);

@override
Expand All @@ -62,13 +70,15 @@ class MoneroWallet extends LibMoneroWallet {
required String password,
required String mnemonic,
required String seedOffset,
required int network,
int height = 0,
}) => csMonero.getRestoredWallet(
path: path,
password: password,
mnemonic: mnemonic,
height: height,
seedOffset: seedOffset,
network: network,
walletId: walletId,
);

Expand All @@ -78,13 +88,15 @@ class MoneroWallet extends LibMoneroWallet {
required String password,
required String address,
required String privateViewKey,
required int network,
int height = 0,
}) => csMonero.getRestoredFromViewKeyWallet(
walletId: walletId,
path: path,
password: password,
address: address,
privateViewKey: privateViewKey,
network: network,
height: height,
);

Expand Down
22 changes: 20 additions & 2 deletions lib/wallets/wallet/intermediate/lib_monero_wallet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import '../../../utilities/stack_file_system.dart';
import '../../../wl_gen/interfaces/cs_monero_interface.dart';
import '../../../wl_gen/interfaces/cs_salvium_interface.dart'
show WrappedWallet;
import '../../crypto_currency/coins/monero.dart';
import '../../crypto_currency/intermediate/cryptonote_currency.dart';
import '../../isar/models/wallet_info.dart';
import '../../models/tx_data.dart';
Expand Down Expand Up @@ -110,6 +111,8 @@ abstract class LibMoneroWallet<T extends CryptonoteCurrency>

final lib_monero_compat.WalletType compatType;

int getNetworkType() => moneroNetworkType(cryptoCurrency.network);

lib_monero_compat.SyncStatus? get syncStatus => _syncStatus;
lib_monero_compat.SyncStatus? _syncStatus;
int _syncedCount = 0;
Expand Down Expand Up @@ -138,20 +141,23 @@ abstract class LibMoneroWallet<T extends CryptonoteCurrency>
Future<WrappedWallet> loadWallet({
required String path,
required String password,
required int network,
});

Future<WrappedWallet> getCreatedWallet({
required String path,
required String password,
required int wordCount,
required String seedOffset,
required int network,
});

Future<WrappedWallet> getRestoredWallet({
required String path,
required String password,
required String mnemonic,
required String seedOffset,
required int network,
int height = 0,
});

Expand All @@ -160,6 +166,7 @@ abstract class LibMoneroWallet<T extends CryptonoteCurrency>
required String password,
required String address,
required String privateViewKey,
required int network,
int height = 0,
});

Expand Down Expand Up @@ -209,7 +216,11 @@ abstract class LibMoneroWallet<T extends CryptonoteCurrency>
throw Exception("Password not found $e, $s");
}

wallet = await loadWallet(path: path, password: password);
wallet = await loadWallet(
path: path,
password: password,
network: getNetworkType(),
);

_setListener();

Expand Down Expand Up @@ -329,7 +340,11 @@ abstract class LibMoneroWallet<T extends CryptonoteCurrency>
} catch (e, s) {
throw Exception("Password not found $e, $s");
}
wallet = await loadWallet(path: path, password: password);
wallet = await loadWallet(
path: path,
password: password,
network: getNetworkType(),
);
return (
await csMonero.getAddress(wallet!),
await csMonero.getPrivateViewKey(wallet!),
Expand All @@ -355,6 +370,7 @@ abstract class LibMoneroWallet<T extends CryptonoteCurrency>
password: password,
wordCount: wordCount,
seedOffset: "", // default for non restored wallets for now
network: getNetworkType(),
);

await info.updateRestoreHeight(
Expand Down Expand Up @@ -438,6 +454,7 @@ abstract class LibMoneroWallet<T extends CryptonoteCurrency>
mnemonic: mnemonic,
height: height,
seedOffset: seedOffset,
network: getNetworkType(),
);

if (this.wallet != null) {
Expand Down Expand Up @@ -1575,6 +1592,7 @@ abstract class LibMoneroWallet<T extends CryptonoteCurrency>
address: data.address,
privateViewKey: data.privateViewKey,
height: height,
network: getNetworkType(),
);

if (this.wallet == null) {
Expand Down
Loading
Loading