Skip to content

Large diffs are not rendered by default.

127 changes: 127 additions & 0 deletions lib/utilities/cryptonote_address.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
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) {
// Valid CryptoNote addresses use at most 106 encoded characters. Bound the
// input before decoding so pasted, untrusted strings cannot allocate a
// buffer proportional to arbitrary input length.
if (address.length > 106) {
return null;
}
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 @@ -55,12 +67,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 @@ -81,6 +101,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 @@ -117,6 +157,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
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 @@ -44,19 +44,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 @@ -65,13 +73,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 @@ -81,13 +91,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 @@ -442,6 +458,7 @@ abstract class LibMoneroWallet<T extends CryptonoteCurrency>
mnemonic: mnemonic,
height: height,
seedOffset: seedOffset,
network: getNetworkType(),
);

if (this.wallet != null) {
Expand Down Expand Up @@ -1564,6 +1581,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