What changed, and why it matters
This is a large feature/refactoring commit that introduces a new shared wallet-core integration path behind a compile-time flag, while keeping the existing legacy wallet code intact. It also removes the old in-tree openalias_ffi plugin and switches OpenAlias resolution to a new wallet_openalias package. There is no direct evidence in the diff of a security vulnerability, malicious change, or undisclosed fix. The work appears to be a normal architectural migration.
Treat as a normal architectural migration. Review the new wallet-core packages (wallet_domain, wallet_monero, wallet_openalias, wallet_infra) and their supply-chain provenance before relying on the new path. Ensure the disabled background-sync TODO for wallet-core is tracked and completed. No immediate security response is indicated by this commit alone.
Security signals we found
Large refactor with feature flag; legacy path preserved
OpenAlias resolver replaced by external package
Background sync explicitly disabled for new wallet-core path to avoid concurrent wallet access
No secret material handling changes beyond moving key reads behind async AppWallet methods
Evidence from the diff
The commit adds an AppWallet abstraction, a MoneroWalletAdapter for wallet-core, and wallet_core_glue.dart that conditionally routes wallet operations to either WalletManager (wallet-core) or the legacy WalletModel. It disables background sync/foreground service paths under the new flag to avoid double wallet opens, and replaces openalias_ffi with wallet_openalias. The legacy WalletModel is updated to implement AppWallet. No security-relevant bug fixes or obviously dangerous patterns (e.g., secret logging, unsafe native calls, bypassed authentication) are visible in the diff.
Changed components
lib/main.dartlib/models/app_wallet.dartlib/models/monero_wallet_adapter.dartlib/models/wallet_model.dartlib/wallet_core_glue.dartlib/screens/*lib/services/foreground_sync_service.dartlib/periodic_tasks.dartplugins/openalias_ffi/*Inspect captured patch +918 / −3708
diff --git a/.gitignore b/.gitignore
index 166b3e0..76c4e0a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -46,3 +46,4 @@ app.*.map.json
# Local reproducibility test outputs (.so builds + logs)
/repro-out/
+pubspec_overrides.yaml
diff --git a/.gitmodules b/.gitmodules
index 54235b3..cfe02a0 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,6 +1,3 @@
[submodule "monero_c"]
path = monero_c
url = https://github.com/magicgrants/monero_c
-[submodule "plugins/openalias_ffi/cargokit"]
- path = plugins/openalias_ffi/cargokit
- url = https://github.com/irondash/cargokit
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..2b848d6
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,18 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "skylight (wallet-core)",
+ "request": "launch",
+ "type": "dart",
+ "program": "lib/main.dart",
+ "toolArgs": ["--dart-define=useSharedWalletCore=true"]
+ },
+ {
+ "name": "skylight (legacy WalletModel)",
+ "request": "launch",
+ "type": "dart",
+ "program": "lib/main.dart"
+ }
+ ]
+}
diff --git a/analysis_options.yaml b/analysis_options.yaml
index 118aca6..146e24b 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -9,6 +9,11 @@
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
+analyzer:
+ # Vendored/submodule code skylight consumes via pub-cache, not linted here.
+ exclude:
+ - monero_c/**
+
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart
index 0024dcd..c074197 100644
--- a/lib/l10n/app_localizations.dart
+++ b/lib/l10n/app_localizations.dart
@@ -486,7 +486,7 @@ abstract class AppLocalizations {
/// No description provided for @restoreWalletRestoreHeightLabel.
///
/// In en, this message translates to:
- /// **'Restore Height (Optional)'**
+ /// **'Restore Height (optional)'**
String get restoreWalletRestoreHeightLabel;
/// No description provided for @restoreWalletRestoreDateLabel.
diff --git a/lib/main.dart b/lib/main.dart
index e27e202..b63c300 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -45,6 +45,7 @@ import 'package:skylight_wallet/services/foreground_sync_service.dart';
import 'package:skylight_wallet/util/dirs.dart';
import 'package:skylight_wallet/util/logging.dart';
import 'package:skylight_wallet/util/cacert.dart';
+import 'package:skylight_wallet/wallet_core_glue.dart';
final isDesktop = Platform.isLinux || Platform.isWindows || Platform.isMacOS;
final isMobile = Platform.isAndroid || Platform.isIOS;
@@ -55,6 +56,10 @@ void main() async {
() async {
WidgetsFlutterBinding.ensureInitialized();
+ if (useSharedWalletCore) {
+ installWalletCore();
+ }
+
// Catch Flutter framework errors
FlutterError.onError = (FlutterErrorDetails details) {
log(LogLevel.error, 'Flutter error: ${details.exception}');
@@ -126,6 +131,7 @@ class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MultiProvider(
providers: [
+ if (useSharedWalletCore) walletManagerProvider(),
ChangeNotifierProvider(create: (context) => WalletModel()),
ChangeNotifierProvider(create: (context) => LanguageModel()),
ChangeNotifierProvider(create: (context) => ThemeModel()),
@@ -154,11 +160,11 @@ class _AppRootState extends State<_AppRoot> {
var _startedServices = false;
Future<List<Object>> _runStartup() {
- final wallet = Provider.of<WalletModel>(context, listen: false);
-
- // We need to check for wallet existence to determine the correct initial route,
- // but we'll do this quickly without loading the wallet to avoid startup delay.
- return Future.wait([SharedPreferences.getInstance(), loadExistingWalletIfExists(wallet)]);
+ // Wallet existence drives the initial route; done quickly without a full load.
+ final walletExists = useSharedWalletCore
+ ? startupWalletManager(context)
+ : loadExistingWalletIfExists(Provider.of<WalletModel>(context, listen: false));
+ return Future.wait([SharedPreferences.getInstance(), walletExists]);
}
@override
diff --git a/lib/models/app_wallet.dart b/lib/models/app_wallet.dart
new file mode 100644
index 0000000..8bff4dc
--- /dev/null
+++ b/lib/models/app_wallet.dart
@@ -0,0 +1,116 @@
+import 'package:flutter/foundation.dart' show Listenable;
+
+import 'package:skylight_wallet/models/wallet_model.dart'
+ show TxDetails, LWSConnectionDetails, ResolvedOpenAlias;
+
+/// A transaction built but not yet broadcast, in display (XMR) units. The
+/// concrete wallet keeps the native handle; pass it back to the SAME wallet's
+/// [AppWallet.commitTx].
+abstract interface class AppPendingTx {
+ double get amount;
+ double get fee;
+}
+
+/// The original mnemonic the user backed up; `format` is a [SeedFormat] name
+/// ('polyseed' | 'bip39' | 'moneroLegacy').
+typedef StoredSeed = ({String mnemonic, String format});
+typedef StoredSeedReader = Future<StoredSeed?> Function();
+
+/// The wallet surface screens use. Both the legacy [WalletModel] and the
+/// wallet-core adapter implement it, so a screen reads one type regardless of
+/// useSharedWalletCore. Divergent flows (send/restore/create) are handled at
+/// their screens, not here.
+abstract interface class AppWallet implements Listenable {
+ // Connection
+ String get connectionAddress;
+ String get connectionProxyPort;
+ bool get connectionUseTor;
+ bool get connectionUseSsl;
+ bool get usingTor;
+ String get connectionType;
+ bool get isNodeMode;
+ bool get hasAttemptedConnection;
+ bool get torRequirementBroken;
+
+ // Sync
+ bool get isConnected;
+ bool get isSynced;
+ int? get syncedHeight;
+ int? get syncBlocksRemaining;
+
+ // Balances + history
+ double? get unlockedBalance;
+ double? get totalBalance;
+ List<TxDetails> get txHistory;
+ bool? get serverSupportsSubaddresses;
+
+ // Addresses
+ String getPrimaryAddress();
+ String? getReceiveAddress();
+
+ // Lifecycle
+ Future<bool> hasExistingWallet();
+ Future<void> load();
+ Future<int> getRestoreHeight();
+ Future<void> pauseSyncAndStore();
+
+ // Connection ops
+ Future<LWSConnectionDetails> getPersistedConnection();
+ void setConnection({
+ required String address,
+ required String proxyPort,
+ required bool useTor,
+ required bool useSsl,
+ String connectionType,
+ });
+ Future<void> persistCurrentConnection();
+ Future<void> loadPersistedConnection();
+ Future<void> testConnection({
+ required String address,
+ String? proxyPort,
+ required bool useSsl,
+ required bool useTor,
+ String connectionType,
+ });
+ Future<void> connectToDaemon();
+ void onGlobalTorDisabled();
+
+ // Notifications
+ Future<void> markExistingTxsAsNotified();
+ Future<void> notifyNewIncomingTxs();
+
+ // Key export (secret_keys / lws screens)
+ Future<String> readSecretViewKey();
+ Future<String> readSecretSpendKey();
+ Future<String> readPublicViewKey();
+ Future<String> readPublicSpendKey();
+ Future<String> readLegacySeed();
+ Future<String> readPolyseed();
+ /// The original backed-up mnemonic (bip39 shows its own words, not the
+ /// derived legacy seed), or null when no seed store is kept.
+ Future<StoredSeed?> readStoredSeed();
+
+ // Receive (serverSupportsSubaddresses is declared with the sync getters above)
+ String? getUnusedSubaddress();
+ bool? get unusedSubaddressIndexIsSupported;
+
+ // Send
+ bool isAddressValid(String address);
+ /// Estimated network fee in base units (piconero), or null when the backend
+ /// can't estimate it (typically insufficient balance for that priority).
+ Future<int?> estimateFee(
+ String destinationAddress,
+ double amount, {
+ int priority,
+ String? amountText,
+ });
+ Future<AppPendingTx> createTx(
+ String destinationAddress,
+ double amount,
+ bool isSweepAll, {
+ int priority,
+ String? amountText,
+ });
+ Future<void> commitTx(AppPendingTx tx, String destinationAddress);
+ Future<ResolvedOpenAlias?> resolveOpenAlias(String alias);
+}
diff --git a/lib/models/monero_wallet_adapter.dart b/lib/models/monero_wallet_adapter.dart
new file mode 100644
index 0000000..d479883
--- /dev/null
+++ b/lib/models/monero_wallet_adapter.dart
@@ -0,0 +1,255 @@
+import 'package:flutter/foundation.dart';
+
+import 'package:skylight_wallet/models/app_wallet.dart';
+import 'package:skylight_wallet/models/wallet_model.dart'
+ show TxDetails, TxRecipient, LWSConnectionDetails, ResolvedOpenAlias;
+
+import 'package:wallet_domain/wallet_domain.dart' as domain;
+import 'package:wallet_monero/wallet_monero.dart' show MoneroWallet;
+
+const _moneroDecimals = 12;
+
+/// Presents a wallet-core [MoneroWallet] through skylight's [AppWallet] surface,
+/// converting wallet-core types (BigInt amounts, TxDetails, connection details)
+/// to skylight's. Re-fires the wrapped wallet's notifications.
+class MoneroWalletAdapter extends ChangeNotifier implements AppWallet {
+ MoneroWalletAdapter(this._wallet, {StoredSeedReader? readStoredSeed})
+ : _readStoredSeed = readStoredSeed {
+ _wallet.addListener(notifyListeners);
+ }
+
+ final MoneroWallet _wallet;
+ final StoredSeedReader? _readStoredSeed;
+
+ @override
+ void dispose() {
+ _wallet.removeListener(notifyListeners);
+ super.dispose();
+ }
+
+ static double _toXmr(BigInt units) =>
+ double.parse(domain.baseUnitsToDecimalString(units, _moneroDecimals));
+
+ // Connection
+ @override
+ String get connectionAddress => _wallet.connectionAddress;
+ @override
+ String get connectionProxyPort => _wallet.connectionProxyPort;
+ @override
+ bool get connectionUseTor => _wallet.connectionUseTor;
+ @override
+ bool get connectionUseSsl => _wallet.connectionUseSsl;
+ @override
+ bool get usingTor => _wallet.usingTor;
+ @override
+ String get connectionType => _wallet.connectionType;
+ @override
+ bool get isNodeMode => _wallet.connectionType == 'node';
+ @override
+ bool get hasAttemptedConnection => _wallet.hasAttemptedConnection;
+ @override
+ bool get torRequirementBroken => _wallet.torRequirementBroken;
+
+ // Sync
+ @override
+ bool get isConnected => _wallet.isConnected;
+ @override
+ bool get isSynced => _wallet.isSynced;
+ @override
+ int? get syncedHeight => _wallet.syncedHeight;
+ @override
+ int? get syncBlocksRemaining => _wallet.syncBlocksRemaining;
+
+ // Balances + history
+ @override
+ double? get unlockedBalance => _wallet.unlockedBalance;
+ @override
+ double? get totalBalance => _wallet.totalBalance;
+ @override
+ bool? get serverSupportsSubaddresses => _wallet.serverSupportsSubaddresses;
+ @override
+ String? getUnusedSubaddress() => _wallet.getUnusedSubaddress();
+ @override
+ bool? get unusedSubaddressIndexIsSupported => _wallet.unusedSubaddressIndexIsSupported;
+ @override
+ List<TxDetails> get txHistory => [for (final t in _wallet.txHistory) _toSkylightTx(t)];
+
+ TxDetails _toSkylightTx(domain.TxDetails t) => TxDetails(
+ index: t.index,
+ direction: t.direction,
+ hash: t.hash,
+ amount: _toXmr(t.amountBaseUnits),
+ fee: _toXmr(t.feeBaseUnits),
+ recipients: [for (final r in t.recipients) TxRecipient(r.address, _toXmr(r.amountBaseUnits))],
+ accountIndex: t.accountIndex,
+ subaddrIndexList: t.subaddrIndexList,
+ timestamp: t.timestamp,
+ height: t.height,
+ confirmations: t.confirmations,
+ key: t.key,
+ );
+
+ // Addresses
+ @override
+ String getPrimaryAddress() => _wallet.getPrimaryAddress();
+ @override
+ String? getReceiveAddress() => _wallet.getReceiveAddress();
+
+ // Lifecycle
+ @override
+ Future<bool> hasExistingWallet() => _wallet.hasExistingWallet();
+ @override
+ Future<void> load() => _wallet.load();
+ @override
+ Future<int> getRestoreHeight() => _wallet.getRestoreHeight();
+ @override
+ Future<void> pauseSyncAndStore() => _wallet.pauseSyncAndStore();
+
+ // Connection ops
+ @override
+ Future<LWSConnectionDetails> getPersistedConnection() async {
+ final c = await _wallet.getPersistedConnection();
+ return LWSConnectionDetails(
+ address: c.address,
+ proxyPort: c.proxyPort,
+ useTor: c.useTor,
+ useSsl: c.useSsl,
+ connectionType: c.connectionType,
+ );
+ }
+
+ @override
+ void setConnection({
+ required String address,
+ required String proxyPort,
+ required bool useTor,
+ required bool useSsl,
+ String connectionType = '',
+ }) => _wallet.setConnection(
+ address: address,
+ proxyPort: proxyPort,
+ useTor: useTor,
+ useSsl: useSsl,
+ connectionType: connectionType,
+ );
+
+ @override
+ Future<void> persistCurrentConnection() => _wallet.persistCurrentConnection();
+ @override
+ Future<void> loadPersistedConnection() => _wallet.loadPersistedConnection();
+
+ @override
+ Future<void> testConnection({
+ required String address,
+ String? proxyPort,
+ required bool useSsl,
+ required bool useTor,
+ String connectionType = '',
+ }) => _wallet.testConnection(
+ address: address,
+ proxyPort: proxyPort,
+ useSsl: useSsl,
+ useTor: useTor,
+ connectionType: connectionType,
+ );
+
+ @override
+ Future<void> connectToDaemon() => _wallet.connectToDaemon();
+ @override
+ void onGlobalTorDisabled() => _wallet.onGlobalTorDisabled();
+
+ // Notifications
+ @override
+ Future<void> markExistingTxsAsNotified() => _wallet.markExistingTxsAsNotified();
+ @override
+ Future<void> notifyNewIncomingTxs() => _wallet.notifyNewIncomingTxs();
+
+ // Key export
+ @override
+ Future<String> readSecretViewKey() => _wallet.readSecretViewKey();
+ @override
+ Future<String> readSecretSpendKey() => _wallet.readSecretSpendKey();
+ @override
+ Future<String> readPublicViewKey() => _wallet.readPublicViewKey();
+ @override
+ Future<String> readPublicSpendKey() => _wallet.readPublicSpendKey();
+ @override
+ Future<String> readLegacySeed() => _wallet.readLegacySeed();
+ @override
+ Future<String> readPolyseed() => _wallet.readPolyseed();
+ @override
+ Future<StoredSeed?> readStoredSeed() async => await _readStoredSeed?.call();
+
+ // Send
+ @override
+ bool isAddressValid(String address) => _wallet.isAddressValid(address);
+
+ @override
+ Future<int?> estimateFee(
+ String destinationAddress,
+ double amount, {
+ int priority = 0,
+ String? amountText,
+ }) async {
+ final fee = await _wallet.estimateFee(
+ destinationAddress,
+ _toBaseUnits(amount, amountText),
+ priority: priority,
+ );
+ return fee?.toInt();
+ }
+
+ @override
+ Future<AppPendingTx> createTx(
+ String destinationAddress,
+ double amount,
+ bool isSweepAll, {
+ int priority = 0,
+ String? amountText,
+ }) async {
+ final tx = await _wallet.createTx(
+ destinationAddress,
+ _toBaseUnits(amount, amountText),
+ isSweepAll,
+ priority: priority,
+ );
+ return _CorePendingTx(tx);
+ }
+
+ @override
+ Future<void> commitTx(AppPendingTx tx, String destinationAddress) =>
+ _wallet.commitTx((tx as _CorePendingTx).raw, destinationAddress);
+
+ @override
+ Future<ResolvedOpenAlias?> resolveOpenAlias(String alias) async {
+ // resolveAlias throws when Tor is down or the input is a raw address;
+ // skylight's send flow expects null on any failure.
+ try {
+ final resolved = await _wallet.resolveAlias(alias);
+ if (resolved == null) return null;
+ return ResolvedOpenAlias(
+ address: resolved.address,
+ version: 2,
+ recipientName: resolved.recipientName,
+ );
+ } catch (_) {
+ return null;
+ }
+ }
+
+ // Prefer the exact decimal string; a double loses precision (D4).
+ static BigInt _toBaseUnits(double amount, String? amountText) => domain.decimalToBaseUnits(
+ amountText ?? amount.toStringAsFixed(_moneroDecimals),
+ _moneroDecimals,
+ );
+}
+
+/// Wraps a wallet-core [domain.PendingTransaction] as the neutral [AppPendingTx].
+class _CorePendingTx implements AppPendingTx {
+ _CorePendingTx(this.raw);
+ final domain.PendingTransaction raw;
+ @override
+ double get amount => MoneroWalletAdapter._toXmr(raw.amountBaseUnits);
+ @override
+ double get fee => MoneroWalletAdapter._toXmr(raw.feeBaseUnits);
+}
diff --git a/lib/models/wallet_model.dart b/lib/models/wallet_model.dart
index 918d8c5..3591d65 100644
--- a/lib/models/wallet_model.dart
+++ b/lib/models/wallet_model.dart
@@ -1,4 +1,4 @@
-// ignore_for_file: implementation_imports
+// ignore_for_file: implementation_imports, annotate_overrides
import 'dart:async';
import 'dart:convert';
import 'dart:ffi';
@@ -10,13 +10,14 @@ import 'package:flutter/foundation.dart';
import 'package:monero/monero.dart' as monero;
import 'package:monero/src/monero.dart';
import 'package:monero/src/wallet2.dart';
-import 'package:openalias_ffi/openalias_ffi.dart';
+import 'package:wallet_openalias/wallet_openalias.dart';
import 'package:http/http.dart' as http;
import 'package:polyseed/polyseed.dart';
import 'package:bip39/bip39.dart' as bip39;
import 'package:skylight_wallet/services/notifications_service.dart';
import 'package:skylight_wallet/services/shared_preferences_service.dart';
+import 'package:skylight_wallet/models/app_wallet.dart';
import 'package:skylight_wallet/services/tor_service.dart';
import 'package:skylight_wallet/services/tor_settings_service.dart';
import 'package:skylight_wallet/util/amount_units.dart';
@@ -59,6 +60,16 @@ class ResolvedOpenAlias {
final String? recipientName;
}
+/// Wraps a native pending tx as the neutral [AppPendingTx].
+class _MoneroPendingTx implements AppPendingTx {
+ _MoneroPendingTx(this.raw);
+ final MoneroPendingTransaction raw;
+ @override
+ double get amount => doubleAmountFromInt(raw.amount());
+ @override
+ double get fee => doubleAmountFromInt(raw.fee());
+}
+
class TxDetails {
final int? index;
final int direction;
@@ -149,7 +160,7 @@ class LWSConnectionDetails {
});
}
-class WalletModel with ChangeNotifier {
+class WalletModel with ChangeNotifier implements AppWallet {
// Which factory built the cached manager: 'lws' (LWSF) or 'node' (wallet2).
Wallet2WalletManager? _w2WalletManager;
String? _managerType;
@@ -1736,8 +1747,28 @@ class WalletModel with ChangeNotifier {
return subaddress;
}
+ @override
+ String? getReceiveAddress() => getUnusedSubaddress() ?? getPrimaryAddress();
+
+ @override
+ Future<String> readSecretViewKey() async => _w2Wallet?.secretViewKey() ?? '';
+ @override
+ Future<String> readSecretSpendKey() async => _w2Wallet?.secretSpendKey() ?? '';
+ @override
+ Future<String> readPublicViewKey() async => _w2Wallet?.publicViewKey() ?? '';
+ @override
+ Future<String> readPublicSpendKey() async => _w2Wallet?.publicSpendKey() ?? '';
+ @override
+ Future<String> readLegacySeed() async => _w2Wallet?.seed(seedOffset: '') ?? '';
+ @override
+ Future<String> readPolyseed() async => _w2Wallet?.getPolyseed(passphrase: '') ?? '';
+ @override
+ Future<StoredSeed?> readStoredSeed() async => null;
+
/// Estimates the network fee (in piconero) for a send at [priority] via the
/// native estimator. Returns null on failure or when fee info isn't cached yet
+ bool isAddressValid(String address) => _w2Wallet?.addressValid(address, 0) ?? false;
+
Future<int?> estimateFee(
String destinationAddress,
double amount, {
@@ -1769,7 +1800,7 @@ class WalletModel with ChangeNotifier {
}
}
- Future<MoneroPendingTransaction> createTx(
+ Future<AppPendingTx> createTx(
String destinationAddress,
double amount,
bool isSweepAll, {
@@ -1821,11 +1852,12 @@ class WalletModel with ChangeNotifier {
throw Exception(pendingTx.errorString());
}
- return pendingTx;
+ return _MoneroPendingTx(pendingTx);
}
- Future<void> commitTx(MoneroPendingTransaction tx, String destinationAddress) async {
- final txFfiAddr = tx.ffiAddress();
+ Future<void> commitTx(AppPendingTx tx, String destinationAddress) async {
+ final raw = (tx as _MoneroPendingTx).raw;
+ final txFfiAddr = raw.ffiAddress();
final filename = '';
final overwrite = false;
@@ -1843,10 +1875,10 @@ class WalletModel with ChangeNotifier {
);
});
- final status = tx.status();
+ final status = raw.status();
log(LogLevel.info, 'PendingTransaction_commit result: $commitResult, status: $status');
- final errorMsg = tx.errorString();
+ final errorMsg = raw.errorString();
if (errorMsg != '' && errorMsg != 'Schema expected string') {
log(LogLevel.error, 'PendingTransaction_commit error: $errorMsg');
@@ -1872,7 +1904,7 @@ class WalletModel with ChangeNotifier {
/// Resolves an OpenAlias (an FQDN or an email-style `name@domain`) to a
/// Monero address with end-to-end DNSSEC validation, over Tor (via the
- /// openalias_ffi Rust/hickory resolver).
+ /// wallet_openalias Rust/hickory resolver).
///
/// OpenAlias v2 records are preferred and v1 is the fallback, per the spec's
/// compatibility rule. Returns null on any failure (incl. Tor unavailable) so
diff --git a/lib/periodic_tasks.dart b/lib/periodic_tasks.dart
index a4cbe24..9a63f24 100644
--- a/lib/periodic_tasks.dart
+++ b/lib/periodic_tasks.dart
@@ -4,6 +4,7 @@ import 'package:skylight_wallet/models/wallet_model.dart';
import 'package:skylight_wallet/services/shared_preferences_service.dart';
import 'package:skylight_wallet/services/tor_service.dart';
import 'package:skylight_wallet/util/logging.dart';
+import 'package:skylight_wallet/wallet_core_glue.dart' show useSharedWalletCore;
import 'package:workmanager/workmanager.dart';
class PeriodicTasks {
@@ -192,6 +193,9 @@ void _callbackDispatcher() {
/// Call after anything that changes the answer: the notifications toggle, the
/// background-sync toggle, or the connection itself.
Future<void> applyBackgroundTaskRegistration() async {
+ // These tasks still drive the legacy WalletModel; under wallet-core they would
+ // open the wallet a second time. Skip until they're migrated (Phase 6).
+ if (useSharedWalletCore) return;
if (Platform.isIOS) return _applyIosBackgroundTasks();
if (!Platform.isAndroid) return;
@@ -268,6 +272,16 @@ Future<void> registerPeriodicTasks() async {
return;
}
+ if (useSharedWalletCore) {
+ // Not migrated to wallet-core yet. Cancel anything a prior legacy-mode
+ // install left scheduled, so WorkManager can't wake a second WalletModel in
+ // the background isolate (and stops tracking its constraints).
+ await Workmanager().cancelByUniqueName(PeriodicTasks.txNotifier);
+ await Workmanager().cancelByUniqueName(_iosRefreshTaskId);
+ await Workmanager().cancelByUniqueName(_iosProcessingTaskId);
+ return;
+ }
+
Workmanager().initialize(_callbackDispatcher);
await applyBackgroundTaskRegistration();
}
diff --git a/lib/screens/confirm_send.dart b/lib/screens/confirm_send.dart
index d1e6e74..37122d2 100644
--- a/lib/screens/confirm_send.dart
+++ b/lib/screens/confirm_send.dart
@@ -1,16 +1,14 @@
import 'package:flutter/material.dart';
-// ignore: implementation_imports
-import 'package:monero/src/monero.dart';
import 'package:skylight_wallet/l10n/app_localizations.dart';
+import 'package:skylight_wallet/models/app_wallet.dart';
import 'package:skylight_wallet/models/fiat_rate_model.dart';
-import 'package:skylight_wallet/models/wallet_model.dart';
import 'package:skylight_wallet/widgets/loading_button.dart';
-import 'package:skylight_wallet/util/formatting.dart';
import 'package:skylight_wallet/util/logging.dart';
+import 'package:skylight_wallet/wallet_core_glue.dart';
import 'package:provider/provider.dart';
class ConfirmSendScreenArgs {
- MoneroPendingTransaction tx;
+ AppPendingTx tx;
String destinationAddress;
String? destinationOpenAlias;
@@ -36,7 +34,7 @@ class ConfirmSendScreen extends StatefulWidget {
class _ConfirmSendScreenState extends State<ConfirmSendScreen> {
bool _isLoading = false;
- MoneroPendingTransaction? _tx;
+ AppPendingTx? _tx;
double _amount = 0.0;
double _fee = 0.0;
String? _destinationOpenAlias;
@@ -66,8 +64,8 @@ class _ConfirmSendScreenState extends State<ConfirmSendScreen> {
setState(() {
_tx = args.tx;
- _amount = doubleAmountFromInt(args.tx.amount());
- _fee = doubleAmountFromInt(args.tx.fee());
+ _amount = args.tx.amount;
+ _fee = args.tx.fee;
_destinationOpenAlias = args.destinationOpenAlias;
_destinationOpenAliasName = args.destinationOpenAliasName;
_destinationAddress = args.destinationAddress;
@@ -90,7 +88,7 @@ class _ConfirmSendScreenState extends State<ConfirmSendScreen> {
Future<void> _confirmSend() async {
final i18n = AppLocalizations.of(context)!;
- final wallet = Provider.of<WalletModel>(context, listen: false);
+ final wallet = appWalletOf(context);
if (_tx == null) return;
diff --git a/lib/screens/create_wallet.dart b/lib/screens/create_wallet.dart
index 1b61484..ea405df 100644
--- a/lib/screens/create_wallet.dart
+++ b/lib/screens/create_wallet.dart
@@ -1,8 +1,7 @@
import 'package:flutter/material.dart';
-import 'package:provider/provider.dart';
import 'package:skylight_wallet/l10n/app_localizations.dart';
-import 'package:skylight_wallet/models/wallet_model.dart';
+import 'package:skylight_wallet/wallet_core_glue.dart';
class CreateWalletScreenArgs {
String toastMessage;
@@ -41,7 +40,7 @@ class _CreateWalletScreenState extends State<CreateWalletScreen> {
/// history from the view key. A full node scans locally and is told nothing,
/// so restoring against one goes straight to the seed form.
void _restoreExisting() {
- final wallet = Provider.of<WalletModel>(context, listen: false);
+ final wallet = appWalletOf(context);
Navigator.pushNamed(
context,
diff --git a/lib/screens/create_wallet_password.dart b/lib/screens/create_wallet_password.dart
index 93ca9c5..66c188c 100644
--- a/lib/screens/create_wallet_password.dart
+++ b/lib/screens/create_wallet_password.dart
@@ -1,8 +1,7 @@
import 'package:flutter/material.dart';
import 'package:skylight_wallet/l10n/app_localizations.dart';
-import 'package:skylight_wallet/models/wallet_model.dart';
import 'package:skylight_wallet/widgets/loading_button.dart';
-import 'package:provider/provider.dart';
+import 'package:skylight_wallet/wallet_core_glue.dart';
class CreateWalletPasswordScreen extends StatefulWidget {
const CreateWalletPasswordScreen({super.key});
@@ -39,8 +38,7 @@ class _CreateWalletPasswordScreenState extends State<CreateWalletPasswordScreen>
final password = _passwordController.text;
if (mounted) {
- final wallet = Provider.of<WalletModel>(context, listen: false);
- wallet.setWalletPassword(password);
+ setWalletPassword(context, password);
Navigator.pushNamed(context, '/create_wallet');
}
} catch (e) {
diff --git a/lib/screens/generate_seed.dart b/lib/screens/generate_seed.dart
index 32c14d7..7114ac3 100644
--- a/lib/screens/generate_seed.dart
+++ b/lib/screens/generate_seed.dart
@@ -3,8 +3,8 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:skylight_wallet/l10n/app_localizations.dart';
import 'package:skylight_wallet/models/fiat_rate_model.dart';
-import 'package:skylight_wallet/models/wallet_model.dart';
import 'package:skylight_wallet/screens/create_wallet.dart';
+import 'package:skylight_wallet/wallet_core_glue.dart';
import 'package:skylight_wallet/util/logging.dart';
import 'package:skylight_wallet/util/secure_screen.dart';
import 'package:provider/provider.dart';
@@ -27,11 +27,8 @@ class _GenerateSeedScreenState extends State<GenerateSeedScreen> with SecureScre
}
Future<void> _createWallet() async {
- final wallet = Provider.of<WalletModel>(context, listen: false);
-
try {
- final (seed, restoreHeight) = await wallet.create();
- wallet.load();
+ final (seed, restoreHeight) = await createWallet(context);
setState(() {
_seed = seed.split(' ');
diff --git a/lib/screens/lws_details.dart b/lib/screens/lws_details.dart
index bc51f51..666aa12 100644
--- a/lib/screens/lws_details.dart
+++ b/lib/screens/lws_details.dart
@@ -1,18 +1,35 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:skylight_wallet/l10n/app_localizations.dart';
-import 'package:skylight_wallet/models/wallet_model.dart';
-import 'package:provider/provider.dart';
+import 'package:skylight_wallet/wallet_core_glue.dart';
-class LwsDetailsScreen extends StatelessWidget {
+class LwsDetailsScreen extends StatefulWidget {
const LwsDetailsScreen({super.key});
+ @override
+ State<LwsDetailsScreen> createState() => _LwsDetailsScreenState();
+}
+
+class _LwsDetailsScreenState extends State<LwsDetailsScreen> {
+ String _secretViewKey = '';
+
+ @override
+ void initState() {
+ super.initState();
+ _load();
+ }
+
+ Future<void> _load() async {
+ final key = await appWalletOf(context).readSecretViewKey();
+ if (!mounted) return;
+ setState(() => _secretViewKey = key);
+ }
+
@override
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
- final wallet = context.watch<WalletModel>();
- final primaryAddress = wallet.getPrimaryAddress();
- final secretViewKey = wallet.w2Wallet!.secretViewKey();
+ final primaryAddress = appWalletOf(context).getPrimaryAddress();
+ final secretViewKey = _secretViewKey;
final restoreHeight = ModalRoute.of(context)!.settings.arguments as int;
return Scaffold(
diff --git a/lib/screens/lws_keys.dart b/lib/screens/lws_keys.dart
index 9ea45c2..c0326a9 100644
--- a/lib/screens/lws_keys.dart
+++ b/lib/screens/lws_keys.dart
@@ -1,10 +1,9 @@
import 'package:flutter/material.dart';
-import 'package:provider/provider.dart';
import 'package:skylight_wallet/l10n/app_localizations.dart';
-import 'package:skylight_wallet/models/wallet_model.dart';
import 'package:skylight_wallet/util/secure_clipboard.dart';
import 'package:skylight_wallet/util/secure_screen.dart';
+import 'package:skylight_wallet/wallet_core_glue.dart';
class LwsKeysScreen extends StatefulWidget {
const LwsKeysScreen({super.key});
@@ -15,28 +14,32 @@ class LwsKeysScreen extends StatefulWidget {
class _LwsKeysScreenState extends State<LwsKeysScreen> with SecureScreenMixin {
var _restoreHeight = 0;
+ var _primaryAddress = '';
+ var _secretViewKey = '';
@override
void initState() {
super.initState();
- _loadRestoreHeight();
+ _load();
}
- Future<void> _loadRestoreHeight() async {
- final wallet = Provider.of<WalletModel>(context, listen: false);
+ Future<void> _load() async {
+ final wallet = appWalletOf(context);
final restoreHeight = await wallet.getRestoreHeight();
-
+ final secretViewKey = await wallet.readSecretViewKey();
+ if (!mounted) return;
setState(() {
_restoreHeight = restoreHeight;
+ _primaryAddress = wallet.getPrimaryAddress();
+ _secretViewKey = secretViewKey;
});
}
@override
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
- final wallet = context.watch<WalletModel>();
- final primaryAddress = wallet.getPrimaryAddress();
- final secretViewKey = wallet.w2Wallet!.secretViewKey();
+ final primaryAddress = _primaryAddress;
+ final secretViewKey = _secretViewKey;
return Scaffold(
appBar: AppBar(title: Text(i18n.lwsKeysTitle)),
diff --git a/lib/screens/receive.dart b/lib/screens/receive.dart
index eb327b2..7e4ae1a 100644
--- a/lib/screens/receive.dart
+++ b/lib/screens/receive.dart
@@ -3,8 +3,7 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:skylight_wallet/l10n/app_localizations.dart';
-import 'package:skylight_wallet/models/wallet_model.dart';
-import 'package:provider/provider.dart';
+import 'package:skylight_wallet/wallet_core_glue.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:share_plus/share_plus.dart';
import 'package:screen_brightness/screen_brightness.dart';
@@ -65,7 +64,7 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
final i18n = AppLocalizations.of(context)!;
final brightness = Theme.of(context).brightness;
final isDarkTheme = brightness == Brightness.dark;
- final wallet = Provider.of<WalletModel>(context);
+ final wallet = appWalletOf(context, listen: true);
final primaryAddress = wallet.getPrimaryAddress();
final subaddress = wallet.getUnusedSubaddress();
final isDemoMode = wallet.connectionAddress == 'demo';
diff --git a/lib/screens/restore_wallet.dart b/lib/screens/restore_wallet.dart
index 0b1da24..d6de064 100644
--- a/lib/screens/restore_wallet.dart
+++ b/lib/screens/restore_wallet.dart
@@ -12,7 +12,7 @@ import 'package:skylight_wallet/util/restore_qr.dart';
import 'package:skylight_wallet/util/secure_screen.dart';
import 'package:skylight_wallet/widgets/loading_button.dart';
import 'package:skylight_wallet/util/logging.dart';
-import 'package:skylight_wallet/models/wallet_model.dart';
+import 'package:skylight_wallet/wallet_core_glue.dart';
class RestoreWalletScreen extends StatefulWidget {
const RestoreWalletScreen({super.key});
@@ -78,8 +78,6 @@ class _RestoreWalletScreenState extends State<RestoreWalletScreen> with SecureSc
return;
}
- final wallet = Provider.of<WalletModel>(context, listen: false);
-
final mnemonic = _mnemonicController.text.trim();
final restoreHeight = int.tryParse(_restoreHeightController.text) ?? 0;
@@ -88,7 +86,7 @@ class _RestoreWalletScreenState extends State<RestoreWalletScreen> with SecureSc
});
try {
- await wallet.restoreFromMnemonic(mnemonic, restoreHeight);
+ await restoreWallet(context, mnemonic: mnemonic, restoreHeight: restoreHeight);
} on Exception catch (error) {
final errorMsg = error.toString().replaceFirst('Exception: ', '');
@@ -130,8 +128,6 @@ class _RestoreWalletScreenState extends State<RestoreWalletScreen> with SecureSc
_isLoading = false;
});
- wallet.load();
-
if (mounted) {
Provider.of<FiatRateModel>(context, listen: false).startService();
Navigator.pushNamedAndRemoveUntil(context, '/wallet_home', (Route<dynamic> route) => false);
diff --git a/lib/screens/secret_keys.dart b/lib/screens/secret_keys.dart
index 574c7c9..0447705 100644
--- a/lib/screens/secret_keys.dart
+++ b/lib/screens/secret_keys.dart
@@ -1,10 +1,9 @@
import 'package:flutter/material.dart';
-import 'package:provider/provider.dart';
import 'package:skylight_wallet/l10n/app_localizations.dart';
-import 'package:skylight_wallet/models/wallet_model.dart';
import 'package:skylight_wallet/util/secure_clipboard.dart';
import 'package:skylight_wallet/util/secure_screen.dart';
+import 'package:skylight_wallet/wallet_core_glue.dart';
class SecretKeysScreen extends StatefulWidget {
const SecretKeysScreen({super.key});
@@ -14,99 +13,87 @@ class SecretKeysScreen extends StatefulWidget {
}
class _SecretKeysScreenState extends State<SecretKeysScreen> with SecureScreenMixin {
+ ({
+ String? bip39,
+ String legacy,
+ String polyseed,
+ String publicSpendKey,
+ String secretSpendKey,
+ String publicViewKey,
+ })?
+ _data;
+
+ @override
+ void initState() {
+ super.initState();
+ _load();
+ }
+
+ Future<void> _load() async {
+ final wallet = appWalletOf(context);
+ // Sequential to avoid concurrent native reads on the same wallet.
+ final stored = await wallet.readStoredSeed();
+ final legacy = await wallet.readLegacySeed();
+ final polyseed = await wallet.readPolyseed();
+ final publicSpendKey = await wallet.readPublicSpendKey();
+ final secretSpendKey = await wallet.readSecretSpendKey();
+ final publicViewKey = await wallet.readPublicViewKey();
+ if (!mounted) return;
+ setState(() {
+ _data = (
+ // The derived legacy seed can't reconstruct a bip39, so show the
+ // original words when they were persisted (see SeedStore).
+ bip39: stored?.format == 'bip39' ? stored!.mnemonic : null,
+ legacy: legacy,
+ polyseed: polyseed,
+ publicSpendKey: publicSpendKey,
+ secretSpendKey: secretSpendKey,
+ publicViewKey: publicViewKey,
+ );
+ });
+ }
+
+ Widget _field(String label, String value) => TextFormField(
+ readOnly: true,
+ decoration: InputDecoration(
+ labelText: label,
+ border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.0)),
+ suffixIcon: IconButton(
+ onPressed: () => SecureClipboard.copy(value),
+ icon: const Icon(Icons.copy),
+ ),
+ ),
+ controller: TextEditingController(text: value),
+ );
+
@override
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
- final wallet = context.watch<WalletModel>();
- final legacySeed = wallet.w2Wallet!.seed(seedOffset: '');
- final polyseed = wallet.w2Wallet!.getPolyseed(passphrase: '');
- final secretSpendKey = wallet.w2Wallet!.secretSpendKey();
- final publicSpendKey = wallet.w2Wallet!.publicSpendKey();
- final publicViewKey = wallet.w2Wallet!.publicViewKey();
+ final data = _data;
return Scaffold(
appBar: AppBar(title: Text(i18n.secretKeysTitle)),
body: SafeArea(
- child: Padding(
- padding: EdgeInsets.symmetric(horizontal: 20),
- child: Column(
- spacing: 20,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Container(),
- TextFormField(
- readOnly: true,
- decoration: InputDecoration(
- labelText: '${i18n.secretKeysMnemonic} (legacy)',
- border: OutlineInputBorder(
- borderRadius: BorderRadius.circular(8.0),
- ),
- suffixIcon: IconButton(
- onPressed: () => SecureClipboard.copy(legacySeed),
- icon: Icon(Icons.copy),
- ),
- ),
- controller: TextEditingController(text: legacySeed),
- ),
- TextFormField(
- readOnly: true,
- decoration: InputDecoration(
- labelText: '${i18n.secretKeysMnemonic} (polyseed)',
- border: OutlineInputBorder(
- borderRadius: BorderRadius.circular(8.0),
- ),
- suffixIcon: IconButton(
- onPressed: () => SecureClipboard.copy(polyseed),
- icon: Icon(Icons.copy),
- ),
- ),
- controller: TextEditingController(text: polyseed),
- ),
- TextFormField(
- readOnly: true,
- decoration: InputDecoration(
- labelText: i18n.secretKeysPublicSpendKey,
- border: OutlineInputBorder(
- borderRadius: BorderRadius.circular(8.0),
- ),
- suffixIcon: IconButton(
- onPressed: () => SecureClipboard.copy(publicSpendKey),
- icon: Icon(Icons.copy),
- ),
- ),
- controller: TextEditingController(text: publicSpendKey),
- ),
- TextFormField(
- readOnly: true,
- decoration: InputDecoration(
- labelText: i18n.secretKeysSecretSpendKey,
- border: OutlineInputBorder(
- borderRadius: BorderRadius.circular(8.0),
- ),
- suffixIcon: IconButton(
- onPressed: () => SecureClipboard.copy(secretSpendKey),
- icon: Icon(Icons.copy),
- ),
- ),
- controller: TextEditingController(text: secretSpendKey),
- ),
- TextFormField(
- readOnly: true,
- decoration: InputDecoration(
- labelText: i18n.secretKeysPublicViewKey,
- border: OutlineInputBorder(
- borderRadius: BorderRadius.circular(8.0),
- ),
- suffixIcon: IconButton(
- onPressed: () => SecureClipboard.copy(publicViewKey),
- icon: Icon(Icons.copy),
- ),
+ child: data == null
+ ? const Center(child: CircularProgressIndicator())
+ : SingleChildScrollView(
+ padding: const EdgeInsets.symmetric(horizontal: 20),
+ child: Column(
+ spacing: 20,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Container(),
+ if (data.bip39 != null)
+ _field('${i18n.secretKeysMnemonic} (bip39)', data.bip39!),
+ _field('${i18n.secretKeysMnemonic} (legacy)', data.legacy),
+ if (data.polyseed.isNotEmpty)
+ _field('${i18n.secretKeysMnemonic} (polyseed)', data.polyseed),
+ _field(i18n.secretKeysPublicSpendKey, data.publicSpendKey),
+ _field(i18n.secretKeysSecretSpendKey, data.secretSpendKey),
+ _field(i18n.secretKeysPublicViewKey, data.publicViewKey),
+ ],
),
- controller: TextEditingController(text: publicViewKey),
),
- ],
- ),
- ),
),
);
}
diff --git a/lib/screens/send.dart b/lib/screens/send.dart
index 23f7cf6..505dbe1 100644
--- a/lib/screens/send.dart
+++ b/lib/screens/send.dart
@@ -13,6 +13,7 @@ import 'package:skylight_wallet/widgets/fiat_amount.dart';
import 'package:skylight_wallet/widgets/loading_button.dart';
import 'package:skylight_wallet/widgets/monero_amount.dart';
import 'package:skylight_wallet/models/wallet_model.dart';
+import 'package:skylight_wallet/wallet_core_glue.dart';
import 'package:skylight_wallet/models/contact_model.dart';
class SendScreenArgs {
@@ -82,7 +83,7 @@ class _SendScreenState extends State<SendScreen> {
void didChangeDependencies() {
super.didChangeDependencies();
- // Run once. didChangeDependencies fires on every WalletModel notify (via
+ // Run once. didChangeDependencies fires on every AppWallet notify (via
// context.watch), and re-adding listeners / re-resolving OpenAlias here
// would loop the (slow, over-Tor) resolver forever.
if (_didInit) return;
@@ -113,7 +114,7 @@ class _SendScreenState extends State<SendScreen> {
}
Future<void> _scanQrCode() async {
- final wallet = Provider.of<WalletModel>(context, listen: false);
+ final wallet = appWalletOf(context);
final i18n = AppLocalizations.of(context)!;
final result = await Navigator.pushNamed(context, '/scan_qr');
@@ -125,7 +126,7 @@ class _SendScreenState extends State<SendScreen> {
final uri = Uri.tryParse(result);
if (uri != null && uri.scheme == 'monero') {
- if (!wallet.w2Wallet!.addressValid(uri.path, 0)) {
+ if (!wallet.isAddressValid(uri.path)) {
if (mounted) {
ScaffoldMessenger.of(
context,
@@ -139,7 +140,7 @@ class _SendScreenState extends State<SendScreen> {
if (uri.queryParameters.containsKey('tx_amount')) {
amount = double.tryParse(uri.queryParameters['tx_amount']!);
}
- } else if (wallet.w2Wallet!.addressValid(result, 0)) {
+ } else if (wallet.isAddressValid(result)) {
address = result;
} else {
if (mounted) {
@@ -197,7 +198,7 @@ class _SendScreenState extends State<SendScreen> {
return _resolveInFlight!;
}
- final wallet = Provider.of<WalletModel>(context, listen: false);
+ final wallet = appWalletOf(context);
if (mounted) setState(() => _openAliasResolving++);
try {
@@ -245,7 +246,7 @@ class _SendScreenState extends State<SendScreen> {
return false;
}
- final wallet = Provider.of<WalletModel>(context, listen: false);
+ final wallet = appWalletOf(context);
final i18n = AppLocalizations.of(context)!;
if (domainRegex.hasMatch(unresolvedDestinationAddress)) {
@@ -259,7 +260,7 @@ class _SendScreenState extends State<SendScreen> {
}
return false;
}
- } else if (!wallet.w2Wallet!.addressValid(unresolvedDestinationAddress, 0)) {
+ } else if (!wallet.isAddressValid(unresolvedDestinationAddress)) {
if (setErrors) {
setState(() {
_destinationAddressError = i18n.sendInvalidAddressError;
@@ -290,7 +291,7 @@ class _SendScreenState extends State<SendScreen> {
_lastFeeFetchKey = feeFetchKey;
final i18n = AppLocalizations.of(context)!;
- final wallet = Provider.of<WalletModel>(context, listen: false);
+ final wallet = appWalletOf(context);
// Increment counter to mark this as the latest request
_feeCalculationCounter++;
@@ -346,7 +347,7 @@ class _SendScreenState extends State<SendScreen> {
}
Future<void> _send() async {
- final wallet = Provider.of<WalletModel>(context, listen: false);
+ final wallet = appWalletOf(context);
final i18n = AppLocalizations.of(context)!;
setState(() {
@@ -442,7 +443,7 @@ class _SendScreenState extends State<SendScreen> {
}
void _setBalanceAsSendAmount() {
- final wallet = Provider.of<WalletModel>(context, listen: false);
+ final wallet = appWalletOf(context);
_amountController.text = (wallet.unlockedBalance ?? 0).toString();
setState(() {
@@ -530,7 +531,7 @@ class _SendScreenState extends State<SendScreen> {
}
Future<void> _onAmountChanged() async {
- final wallet = Provider.of<WalletModel>(context, listen: false);
+ final wallet = appWalletOf(context);
final amount = double.tryParse(_amountController.text) ?? 0;
if (amount == wallet.unlockedBalance! && !_isSweepAll) {
@@ -551,7 +552,7 @@ class _SendScreenState extends State<SendScreen> {
@override
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
- final wallet = context.watch<WalletModel>();
+ final wallet = appWalletOf(context, listen: true);
return Scaffold(
appBar: AppBar(title: Text(i18n.sendTitle)),
diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart
index 6d2a031..ac3401b 100644
--- a/lib/screens/settings.dart
+++ b/lib/screens/settings.dart
@@ -13,7 +13,7 @@ import 'package:skylight_wallet/widgets/fiat_api_settings_form.dart';
import 'package:skylight_wallet/widgets/tor_settings_form.dart';
import 'package:skylight_wallet/models/language_model.dart';
import 'package:skylight_wallet/models/theme_model.dart';
-import 'package:skylight_wallet/models/wallet_model.dart';
+import 'package:skylight_wallet/wallet_core_glue.dart';
import 'package:skylight_wallet/periodic_tasks.dart';
import 'package:skylight_wallet/services/notifications_service.dart';
import 'package:skylight_wallet/services/shared_preferences_service.dart';
@@ -68,7 +68,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
}
void _setTxNotificationsEnabled(bool value) async {
- final wallet = Provider.of<WalletModel>(context, listen: false);
+ final wallet = appWalletOf(context);
setState(() {
_newTxNotificationsEnabled = value;
@@ -331,8 +331,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
}
Future<void> _deleteWallet() async {
- final wallet = Provider.of<WalletModel>(context, listen: false);
- await wallet.delete();
+ await deleteWallet(context);
if (mounted) {
Navigator.pushNamedAndRemoveUntil(context, '/welcome', (Route<dynamic> route) => false);
}
@@ -344,7 +343,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
final dialogWidth = screenWidth.clamp(0.0, 500.0);
void onSaved() {
- final wallet = Provider.of<WalletModel>(context, listen: false);
+ final wallet = appWalletOf(context);
wallet.load();
final fiatRate = Provider.of<FiatRateModel>(context, listen: false);
@@ -395,9 +394,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
final dialogWidth = screenWidth.clamp(0.0, 500.0);
void onSaved() {
- final wallet = Provider.of<WalletModel>(context, listen: false);
// Rebuilds the wallet if the server kind (LWS↔node) changed, then resyncs.
- wallet.applyConnectionChange();
+ applyConnectionChange(context);
final fiatRate = Provider.of<FiatRateModel>(context, listen: false);
fiatRate.startService();
@@ -478,7 +476,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
// On iOS this is offered for LWS only: a remote node can't be
// scanned inside either background window iOS grants.
- if (Platform.isAndroid || (Platform.isIOS && !context.watch<WalletModel>().isNodeMode))
+ if (Platform.isAndroid ||
+ (Platform.isIOS && !appWalletOf(context, listen: true).isNodeMode))
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
diff --git a/lib/screens/unlock.dart b/lib/screens/unlock.dart
index 4627ba0..f21ed65 100644
--- a/lib/screens/unlock.dart
+++ b/lib/screens/unlock.dart
@@ -1,12 +1,11 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:local_auth/local_auth.dart';
-import 'package:provider/provider.dart';
import 'package:skylight_wallet/l10n/app_localizations.dart';
import 'package:skylight_wallet/util/logging.dart';
-import 'package:skylight_wallet/models/wallet_model.dart';
import 'package:skylight_wallet/widgets/loading_button.dart';
+import 'package:skylight_wallet/wallet_core_glue.dart';
class UnlockScreen extends StatefulWidget {
const UnlockScreen({super.key});
@@ -74,11 +73,7 @@ class _UnlockScreenState extends State<UnlockScreen> {
try {
final enteredPassword = _passwordController.text;
- final wallet = Provider.of<WalletModel>(context, listen: false);
-
- await wallet.loadPersistedConnection();
- await wallet.openExisting(desktopWalletPassword: enteredPassword);
- wallet.load();
+ await unlockWithPassword(context, enteredPassword);
if (mounted) {
Navigator.pushNamedAndRemoveUntil(context, '/wallet_home', (Route<dynamic> route) => false);
diff --git a/lib/screens/wallet_home.dart b/lib/screens/wallet_home.dart
index d5640db..8ee7e40 100644
--- a/lib/screens/wallet_home.dart
+++ b/lib/screens/wallet_home.dart
@@ -11,7 +11,9 @@ import 'package:provider/provider.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:timeago/timeago.dart' as timeago;
import 'package:skylight_wallet/l10n/app_localizations.dart';
-import 'package:skylight_wallet/models/wallet_model.dart';
+import 'package:skylight_wallet/models/wallet_model.dart' show TxDetails;
+import 'package:skylight_wallet/models/app_wallet.dart';
+import 'package:skylight_wallet/wallet_core_glue.dart';
import 'package:skylight_wallet/consts.dart' as consts;
import 'package:skylight_wallet/widgets/wallet_navigation_bar.dart';
import 'package:skylight_wallet/widgets/tx_details.dart';
@@ -196,7 +198,7 @@ class _WalletHomeScreenState extends State<WalletHomeScreen> {
Widget _buildStatusIcons(
BuildContext context,
- WalletModel wallet,
+ AppWallet wallet,
StatusIconStatus lwsConnectionIconStatus,
) {
final i18n = AppLocalizations.of(context)!;
@@ -244,7 +246,7 @@ class _WalletHomeScreenState extends State<WalletHomeScreen> {
Widget _buildBalanceDisplay(
BuildContext context,
AppLocalizations i18n,
- WalletModel wallet,
+ AppWallet wallet,
double? unlockedBalanceFiat,
double lockedBalance,
String fiatSymbol,
@@ -331,7 +333,7 @@ class _WalletHomeScreenState extends State<WalletHomeScreen> {
BuildContext context,
AppLocalizations i18n,
Locale currentLocale,
- WalletModel wallet,
+ AppWallet wallet,
FiatRateModel fiatRate,
String fiatSymbol,
) {
@@ -356,7 +358,7 @@ class _WalletHomeScreenState extends State<WalletHomeScreen> {
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
final currentLocale = Localizations.localeOf(context);
- final wallet = context.watch<WalletModel>();
+ final wallet = appWalletOf(context, listen: true);
final fiatRate = context.watch<FiatRateModel>();
final deviceType = _getDeviceType(context);
final unlockedBalanceFiat = fiatRate.rate is double && wallet.unlockedBalance is double
@@ -412,7 +414,7 @@ class _WalletHomeScreenState extends State<WalletHomeScreen> {
DeviceType deviceType,
AppLocalizations i18n,
Locale currentLocale,
- WalletModel wallet,
+ AppWallet wallet,
FiatRateModel fiatRate,
StatusIconStatus lwsConnectionIconStatus,
StatusIconStatus fiatApiIconStatus,
@@ -467,7 +469,7 @@ class _WalletHomeScreenState extends State<WalletHomeScreen> {
BuildContext context,
AppLocalizations i18n,
Locale currentLocale,
- WalletModel wallet,
+ AppWallet wallet,
FiatRateModel fiatRate,
StatusIconStatus lwsConnectionIconStatus,
StatusIconStatus fiatApiIconStatus,
@@ -532,7 +534,7 @@ class _WalletHomeScreenState extends State<WalletHomeScreen> {
BuildContext context,
AppLocalizations i18n,
Locale currentLocale,
- WalletModel wallet,
+ AppWallet wallet,
FiatRateModel fiatRate,
StatusIconStatus lwsConnectionIconStatus,
StatusIconStatus fiatApiIconStatus,
@@ -600,7 +602,7 @@ class _WalletHomeScreenState extends State<WalletHomeScreen> {
BuildContext context,
AppLocalizations i18n,
Locale currentLocale,
- WalletModel wallet,
+ AppWallet wallet,
FiatRateModel fiatRate,
StatusIconStatus lwsConnectionIconStatus,
StatusIconStatus fiatApiIconStatus,
diff --git a/lib/screens/welcome.dart b/lib/screens/welcome.dart
index 9bad85f..248c0c7 100644
--- a/lib/screens/welcome.dart
+++ b/lib/screens/welcome.dart
@@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:skylight_wallet/l10n/app_localizations.dart';
-import 'package:skylight_wallet/models/wallet_model.dart';
-import 'package:provider/provider.dart';
+import 'package:skylight_wallet/wallet_core_glue.dart';
class WelcomeScreen extends StatefulWidget {
const WelcomeScreen({super.key});
@@ -20,15 +19,8 @@ class _WalletHomeScreenState extends State<WelcomeScreen> {
}
Future _pushHomeIfWalletExists() async {
- final wallet = Provider.of<WalletModel>(context, listen: false);
-
- if (await wallet.hasExistingWallet()) {
- await wallet.loadPersistedConnection();
- await wallet.openExisting();
-
- if (mounted) {
- Navigator.pushNamedAndRemoveUntil(context, '/wallet_home', (Route<dynamic> route) => false);
- }
+ if (await openExistingWallet(context) && mounted) {
+ Navigator.pushNamedAndRemoveUntil(context, '/wallet_home', (Route<dynamic> route) => false);
}
}
diff --git a/lib/services/foreground_sync_service.dart b/lib/services/foreground_sync_service.dart
index 0cc217e..59f6538 100644
--- a/lib/services/foreground_sync_service.dart
+++ b/lib/services/foreground_sync_service.dart
@@ -7,6 +7,7 @@ import 'package:skylight_wallet/models/wallet_model.dart';
import 'package:skylight_wallet/services/shared_preferences_service.dart';
import 'package:skylight_wallet/services/tor_service.dart';
import 'package:skylight_wallet/util/logging.dart';
+import 'package:skylight_wallet/wallet_core_glue.dart' show useSharedWalletCore;
/// Android foreground service that keeps the Monero wallet syncing while the
/// app is backgrounded — a persistent-notification alternative to the
@@ -119,6 +120,9 @@ void initForegroundSync() {
}
Future<void> startForegroundSync() async {
+ // Runs the legacy WalletModel in its own isolate; under wallet-core that opens
+ // the wallet a second time. Skip until it's migrated (Phase 6).
+ if (useSharedWalletCore) return;
if (!Platform.isAndroid) return;
initForegroundSync();
await FlutterForegroundTask.requestNotificationPermission();
diff --git a/lib/wallet_core_glue.dart b/lib/wallet_core_glue.dart
new file mode 100644
index 0000000..3c82dbc
--- /dev/null
+++ b/lib/wallet_core_glue.dart
@@ -0,0 +1,201 @@
+import 'dart:async';
+import 'dart:io';
+
+import 'package:flutter/widgets.dart';
+import 'package:provider/provider.dart';
+
+import 'package:skylight_wallet/models/app_wallet.dart';
+import 'package:skylight_wallet/models/monero_wallet_adapter.dart';
+import 'package:skylight_wallet/models/wallet_model.dart';
+import 'package:skylight_wallet/services/notifications_service.dart';
+import 'package:skylight_wallet/services/shared_preferences_service.dart';
+import 'package:skylight_wallet/util/logging.dart';
+
+import 'package:wallet_infra/wallet_infra.dart' as wcore;
+import 'package:wallet_domain/wallet_domain.dart'
+ show
+ WalletAppConfig,
+ WalletManager,
+ CryptoWallet,
+ SeedSource,
+ RestorePoint,
+ baseUnitsToDecimalString;
+import 'package:wallet_monero/wallet_monero.dart' show MoneroWallet;
+import 'package:wallet_openalias/wallet_openalias.dart' show resolveOpenAlias;
+
+/// Phase 6 flag: route the wallet layer through wallet-core.
+const useSharedWalletCore = bool.fromEnvironment('useSharedWalletCore');
+
+const _moneroDecimals = 12;
+
+bool get _isMobile => Platform.isAndroid || Platform.isIOS;
+
+/// Installs wallet-core's app config + injectable seams. Call once from main().
+void installWalletCore() {
+ WalletAppConfig.install(WalletAppConfig.skylight);
+ CryptoWallet.aliasResolver = resolveOpenAlias;
+
+ wcore.WalletLog.sink = const _SkylightLogSink();
+ wcore.WalletLog.isVerbose = () async =>
+ await SharedPreferencesService.get<bool>(SharedPreferencesKeys.verboseLoggingEnabled) ?? false;
+
+ CryptoWallet.incomingTxNotifier = (tx, _) {
+ final amount = double.tryParse(baseUnitsToDecimalString(tx.amountBaseUnits, _moneroDecimals)) ?? 0;
+ NotificationService().showIncomingTxNotification(amount);
+ };
+}
+
+/// WalletManager provider, gated. Coexists with WalletModel during migration.
+ChangeNotifierProvider<WalletManager> walletManagerProvider() =>
+ ChangeNotifierProvider(create: (_) => WalletManager(coins: () => [MoneroWallet()]));
+
+/// Startup for the wallet-core stack: whether a wallet exists (for the initial
+/// route) and, on mobile, opening + starting its sync.
+Future<bool> startupWalletManager(BuildContext context) =>
+ _loadExistingWalletManager(Provider.of<WalletManager>(context, listen: false));
+
+Future<bool> _loadExistingWalletManager(WalletManager manager) async {
+ if (await manager.hasAnyExistingWallet()) {
+ if (_isMobile) {
+ await manager.loadCachedDisplayState();
+ manager.openWalletFilesAndSync();
+ }
+ return true;
+ }
+ return false;
+}
+
+final _adapters = Expando<MoneroWalletAdapter>('appWalletAdapter');
+
+/// The neutral [AppWallet] for the active stack: the shared-core adapter under
+/// the flag, else the legacy [WalletModel]. The adapter is cached per wallet so
+/// repeated lookups don't stack duplicate listeners.
+AppWallet appWalletOf(BuildContext context, {bool listen = false}) {
+ if (!useSharedWalletCore) {
+ return Provider.of<WalletModel>(context, listen: listen);
+ }
+ final manager = Provider.of<WalletManager>(context, listen: listen);
+ final wallet = manager.getWallet('XMR') as MoneroWallet;
+ return _adapters[wallet] ??= MoneroWalletAdapter(
+ wallet,
+ readStoredSeed: () async {
+ final stored = await manager.loadStoredSeed();
+ if (stored == null) return null;
+ return (mnemonic: stored.seed.mnemonic, format: stored.seed.format.name);
+ },
+ );
+}
+
+/// Sets the wallet-encryption password (desktop-entered). Mobile mints a random
+/// one at restore/create time instead.
+void setWalletPassword(BuildContext context, String password) {
+ if (useSharedWalletCore) {
+ Provider.of<WalletManager>(context, listen: false).setWalletPassword(password);
+ } else {
+ Provider.of<WalletModel>(context, listen: false).setWalletPassword(password);
+ }
+}
+
+/// Restores the wallet from a mnemonic at [restoreHeight], then opens + syncs.
+/// Throws Exception('Invalid mnemonic.') on an unrecognized seed.
+Future<void> restoreWallet(
+ BuildContext context, {
+ required String mnemonic,
+ required int restoreHeight,
+}) async {
+ if (useSharedWalletCore) {
+ final manager = Provider.of<WalletManager>(context, listen: false);
+ final seed = SeedSource.detect(mnemonic);
+ if (seed == null) throw Exception('Invalid mnemonic.');
+ if (!manager.hasPassword) manager.useGeneratedPassword();
+ await manager.restoreAll(seed: seed, from: RestorePoint.height(restoreHeight));
+ manager.syncInBackground();
+ } else {
+ final wallet = Provider.of<WalletModel>(context, listen: false);
+ await wallet.restoreFromMnemonic(mnemonic, restoreHeight);
+ wallet.load();
+ }
+}
+
+/// Creates a brand-new wallet, then opens + syncs. Returns its seed words and
+/// restore height (for the seed-backup screen and the connection step).
+Future<(String seed, int restoreHeight)> createWallet(BuildContext context) async {
+ if (useSharedWalletCore) {
+ final manager = Provider.of<WalletManager>(context, listen: false);
+ if (!manager.hasPassword) manager.useGeneratedPassword();
+ final generated = manager.generateSeed();
+ await manager.restoreAll(seed: generated.seed, from: RestorePoint.date(generated.restoreDate));
+ manager.syncInBackground();
+ final height = await manager.getWallet('XMR')!.getRestoreHeight();
+ return (generated.seed.mnemonic, height);
+ } else {
+ final wallet = Provider.of<WalletModel>(context, listen: false);
+ final result = await wallet.create();
+ wallet.load();
+ return result;
+ }
+}
+
+/// Opens an already-existing wallet (used by the welcome safety-net). Returns
+/// false when there is none. Mobile only — desktop unlocks with a password.
+Future<bool> openExistingWallet(BuildContext context) async {
+ if (useSharedWalletCore) {
+ final manager = Provider.of<WalletManager>(context, listen: false);
+ if (!await manager.hasAnyExistingWallet()) return false;
+ manager.openWalletFilesAndSync();
+ return true;
+ }
+ final wallet = Provider.of<WalletModel>(context, listen: false);
+ if (!await wallet.hasExistingWallet()) return false;
+ await wallet.loadPersistedConnection();
+ await wallet.openExisting();
+ return true;
+}
+
+/// Opens the wallet with a desktop-entered password, then syncs. Throws on a
+/// wrong password (the unlock screen shows the error).
+Future<void> unlockWithPassword(BuildContext context, String password) async {
+ if (useSharedWalletCore) {
+ final manager = Provider.of<WalletManager>(context, listen: false);
+ await manager.openAll(password: password);
+ manager.syncInBackground();
+ } else {
+ final wallet = Provider.of<WalletModel>(context, listen: false);
+ await wallet.loadPersistedConnection();
+ await wallet.openExisting(desktopWalletPassword: password);
+ wallet.load();
+ }
+}
+
+/// Deletes the wallet and everything derived from it.
+Future<void> deleteWallet(BuildContext context) async {
+ if (useSharedWalletCore) {
+ // TODO(wallet-core): pass skylight's own pref keys (contacts, pending tx,
+ // notification state) once the flag-on delete path is validated on device.
+ await Provider.of<WalletManager>(context, listen: false).deleteAll();
+ } else {
+ await Provider.of<WalletModel>(context, listen: false).delete();
+ }
+}
+
+/// Rebuilds the wallet if the server kind (LWS↔node) changed, then resyncs.
+void applyConnectionChange(BuildContext context) {
+ if (useSharedWalletCore) {
+ final manager = Provider.of<WalletManager>(context, listen: false);
+ unawaited(manager.reopenWallet('XMR').then((_) => manager.syncInBackground()));
+ } else {
+ Provider.of<WalletModel>(context, listen: false).applyConnectionChange();
+ }
+}
+
+/// Routes wallet-core log lines into skylight's logger.
+class _SkylightLogSink extends wcore.LogSink {
+ const _SkylightLogSink();
+
+ @override
+ Future<void> write(wcore.LogLevel level, String line) => log(switch (level) {
+ wcore.LogLevel.info => LogLevel.info,
+ wcore.LogLevel.warn => LogLevel.warn,
+ wcore.LogLevel.error => LogLevel.error,
+ }, line);
+}
diff --git a/lib/widgets/connection_settings_form.dart b/lib/widgets/connection_settings_form.dart
index 1ba0293..fd77b46 100644
--- a/lib/widgets/connection_settings_form.dart
+++ b/lib/widgets/connection_settings_form.dart
@@ -8,10 +8,9 @@ import 'package:skylight_wallet/services/foreground_sync_service.dart';
import 'package:skylight_wallet/services/shared_preferences_service.dart';
import 'package:skylight_wallet/services/tor_settings_service.dart';
import 'package:skylight_wallet/util/logging.dart';
-import 'package:provider/provider.dart';
import 'package:skylight_wallet/l10n/app_localizations.dart';
-import 'package:skylight_wallet/models/wallet_model.dart';
+import 'package:skylight_wallet/wallet_core_glue.dart';
import 'package:skylight_wallet/services/tor_service.dart';
const isDemoMode = String.fromEnvironment('DEMO_MODE') == 'true';
@@ -146,7 +145,7 @@ class _ConnectionSettingsFormState extends State<ConnectionSettingsForm> {
}
Future<void> _loadPersistedConnection() async {
- final wallet = Provider.of<WalletModel>(context, listen: false);
+ final wallet = appWalletOf(context);
final conn = await wallet.getPersistedConnection();
setState(() {
@@ -354,7 +353,7 @@ class _ConnectionSettingsFormState extends State<ConnectionSettingsForm> {
Future _testConnection() async {
final i18n = AppLocalizations.of(context)!;
- final wallet = Provider.of<WalletModel>(context, listen: false);
+ final wallet = appWalletOf(context);
final daemonAddress = _cleanAddress(_addressController.text);
if (isDemoMode && daemonAddress == 'demo') {
@@ -425,7 +424,7 @@ class _ConnectionSettingsFormState extends State<ConnectionSettingsForm> {
return;
}
- final wallet = Provider.of<WalletModel>(context, listen: false);
+ final wallet = appWalletOf(context);
wallet.setConnection(
address: daemonAddress,
diff --git a/lib/widgets/tor_settings_form.dart b/lib/widgets/tor_settings_form.dart
index 0faeeaa..5f9a31e 100644
--- a/lib/widgets/tor_settings_form.dart
+++ b/lib/widgets/tor_settings_form.dart
@@ -2,10 +2,9 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
-import 'package:provider/provider.dart';
import 'package:skylight_wallet/l10n/app_localizations.dart';
-import 'package:skylight_wallet/models/wallet_model.dart';
+import 'package:skylight_wallet/wallet_core_glue.dart';
import 'package:skylight_wallet/services/tor_settings_service.dart';
import 'package:skylight_wallet/util/socks_http.dart';
@@ -57,7 +56,7 @@ class _TorSettingsFormState extends State<TorSettingsForm> {
// Read before the await: a Tor-only connection has to be told immediately
// that its requirement can no longer be met, or it goes on presenting
// itself as connected over Tor until something tries to reconnect.
- final wallet = Provider.of<WalletModel>(context, listen: false);
+ final wallet = appWalletOf(context);
final disablingTor = _selectedMode == TorMode.disabled;
await _saveSettings();
diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc
index 3ccd551..652d1a1 100644
--- a/linux/flutter/generated_plugin_registrant.cc
+++ b/linux/flutter/generated_plugin_registrant.cc
@@ -9,6 +9,7 @@
#include <file_selector_linux/file_selector_plugin.h>
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
+#include <webcrypto/webcrypto_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
@@ -20,4 +21,7 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
+ g_autoptr(FlPluginRegistrar) webcrypto_registrar =
+ fl_plugin_registry_get_registrar_for_plugin(registry, "WebcryptoPlugin");
+ webcrypto_plugin_register_with_registrar(webcrypto_registrar);
}
diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake
index d3be569..a3391a0 100644
--- a/linux/flutter/generated_plugins.cmake
+++ b/linux/flutter/generated_plugins.cmake
@@ -6,12 +6,13 @@ list(APPEND FLUTTER_PLUGIN_LIST
file_selector_linux
flutter_secure_storage_linux
url_launcher_linux
+ webcrypto
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
flutter_zxing
- openalias_ffi
tor_ffi_plugin
+ wallet_openalias
)
set(PLUGIN_BUNDLED_LIBRARIES)
diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift
index 8afa86e..9184f3e 100644
--- a/macos/Flutter/GeneratedPluginRegistrant.swift
+++ b/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -14,6 +14,7 @@ import path_provider_foundation
import screen_brightness_macos
import share_plus
import shared_preferences_foundation
+import webcrypto
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
@@ -25,4 +26,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
ScreenBrightnessMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenBrightnessMacosPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
+ WebcryptoPlugin.register(with: registry.registrar(forPlugin: "WebcryptoPlugin"))
}
diff --git a/plugins/openalias_ffi/analysis_options.yaml b/plugins/openalias_ffi/analysis_options.yaml
deleted file mode 100644
index a5744c1..0000000
--- a/plugins/openalias_ffi/analysis_options.yaml
+++ /dev/null
@@ -1,4 +0,0 @@
-include: package:flutter_lints/flutter.yaml
-
-# Additional information about this file can be found at
-# https://dart.dev/guides/language/analysis-options
diff --git a/plugins/openalias_ffi/android/.gitignore b/plugins/openalias_ffi/android/.gitignore
deleted file mode 100644
index 161bdcd..0000000
--- a/plugins/openalias_ffi/android/.gitignore
+++ /dev/null
@@ -1,9 +0,0 @@
-*.iml
-.gradle
-/local.properties
-/.idea/workspace.xml
-/.idea/libraries
-.DS_Store
-/build
-/captures
-.cxx
diff --git a/plugins/openalias_ffi/android/build.gradle b/plugins/openalias_ffi/android/build.gradle
deleted file mode 100644
index 758942d..0000000
--- a/plugins/openalias_ffi/android/build.gradle
+++ /dev/null
@@ -1,77 +0,0 @@
-// The Android Gradle Plugin builds the native code with the Android NDK.
-
-group 'org.magicgrants.openalias_ffi'
-version '1.0'
-
-buildscript {
- ext.kotlin_version = '1.8.0'
- repositories {
- google()
- mavenCentral()
- }
-
- dependencies {
- classpath("com.android.tools.build:gradle:8.1.0")
- classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
- }
-}
-
-allprojects {
- repositories {
- google()
- mavenCentral()
- }
-}
-
-apply plugin: 'com.android.library'
-apply plugin: 'kotlin-android'
-
-android {
- if (project.android.hasProperty("namespace")) {
- namespace 'org.magicgrants.openalias_ffi'
- }
-
- compileSdk = 34
-
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_1_8
- targetCompatibility = JavaVersion.VERSION_1_8
- }
-
- kotlinOptions {
- jvmTarget = JavaVersion.VERSION_1_8
- }
-
- sourceSets {
- main.java.srcDirs += "src/main/kotlin"
- test.java.srcDirs += "src/test/kotlin"
- }
-
- defaultConfig {
- minSdk = 21
- }
-
- dependencies {
- testImplementation("org.jetbrains.kotlin:kotlin-test")
- testImplementation("org.mockito:mockito-core:5.0.0")
- }
-
- testOptions {
- unitTests.all {
- useJUnitPlatform()
-
- testLogging {
- events "passed", "skipped", "failed", "standardOut", "standardError"
- outputs.upToDateWhen {false}
- showStandardStreams = true
- }
- }
- }
-}
-
-apply from: "../cargokit/gradle/plugin.gradle"
-
-cargokit {
- manifestDir = "../rust"
- libname = "openalias_ffi"
-}
\ No newline at end of file
diff --git a/plugins/openalias_ffi/android/settings.gradle b/plugins/openalias_ffi/android/settings.gradle
deleted file mode 100644
index 82e0391..0000000
--- a/plugins/openalias_ffi/android/settings.gradle
+++ /dev/null
@@ -1 +0,0 @@
-rootProject.name = 'openalias_ffi'
diff --git a/plugins/openalias_ffi/android/src/main/AndroidManifest.xml b/plugins/openalias_ffi/android/src/main/AndroidManifest.xml
deleted file mode 100644
index ab49f99..0000000
--- a/plugins/openalias_ffi/android/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="org.magicgrants.openalias_ffi">
-</manifest>
diff --git a/plugins/openalias_ffi/ffigen.yaml b/plugins/openalias_ffi/ffigen.yaml
deleted file mode 100644
index 1225e07..0000000
--- a/plugins/openalias_ffi/ffigen.yaml
+++ /dev/null
@@ -1,19 +0,0 @@
-# Run with `flutter pub run ffigen --config ffigen.yaml`.
-name: OpenaliasFfiBindings
-description: |
- Bindings for `src/openalias_ffi.h`.
-
- Regenerate bindings with `flutter pub run ffigen --config ffigen.yaml`.
-output: 'lib/openalias_ffi_bindings_generated.dart'
-headers:
- entry-points:
- - 'rust/target/openalias_ffi.h'
- include-directives:
- - 'rust/target/openalias_ffi.h'
-preamble: |
- // ignore_for_file: always_specify_types
- // ignore_for_file: camel_case_types
- // ignore_for_file: non_constant_identifier_names
-comments:
- style: any
- length: full
diff --git a/plugins/openalias_ffi/ios/Classes/openalias_ffi.c b/plugins/openalias_ffi/ios/Classes/openalias_ffi.c
deleted file mode 100644
index 09168e8..0000000
--- a/plugins/openalias_ffi/ios/Classes/openalias_ffi.c
+++ /dev/null
@@ -1,2 +0,0 @@
-// Relative import to be able to reuse the C sources.
-// See the comment in ../{projectName}}.podspec for more information.
diff --git a/plugins/openalias_ffi/ios/openalias_ffi.podspec b/plugins/openalias_ffi/ios/openalias_ffi.podspec
deleted file mode 100644
index 023c0d9..0000000
--- a/plugins/openalias_ffi/ios/openalias_ffi.podspec
+++ /dev/null
@@ -1,41 +0,0 @@
-#
-# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
-# Run `pod lib lint openalias_ffi.podspec` to validate before publishing.
-#
-Pod::Spec.new do |s|
- s.name = 'openalias_ffi'
- s.version = '0.0.1'
- s.summary = 'A new Flutter FFI plugin project.'
- s.description = <<-DESC
-A new Flutter FFI plugin project.
- DESC
- s.homepage = 'http://example.com'
- s.license = { :file => '../LICENSE' }
- s.author = { 'Your Company' => 'email@example.com' }
-
- s.source = { :path => '.' }
- s.source_files = 'Classes/**/*'
- s.dependency 'Flutter'
- s.platform = :ios, '15.0'
-
- s.script_phase = {
- :name => 'Build Rust library',
- # First argument is relative path to the `rust` folder, second is name of rust library
- :script => 'sh "$PODS_TARGET_SRCROOT/../cargokit/build_pod.sh" ../rust openalias_ffi',
- :execution_position => :before_compile,
- :input_files => ['${BUILT_PRODUCTS_DIR}/cargokit_phony'],
- # Let XCode know that the static library referenced in -force_load below is
- # created by this build step.
- :output_files => ["${BUILT_PRODUCTS_DIR}/libopenalias_ffi.a"],
- }
- s.pod_target_xcconfig = {
- 'DEFINES_MODULE' => 'YES',
- # Flutter.framework does not contain a i386 slice.
- 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386',
- 'EXCLUDED_ARCHS' => 'armv7',
- 'OTHER_LDFLAGS' => '-force_load ${BUILT_PRODUCTS_DIR}/libopenalias_ffi.a',
- }
- s.user_target_xcconfig = {
- 'EXCLUDED_ARCHS' => 'armv7',
- }
-end
diff --git a/plugins/openalias_ffi/lib/openalias_ffi.dart b/plugins/openalias_ffi/lib/openalias_ffi.dart
deleted file mode 100644
index 91879c7..0000000
--- a/plugins/openalias_ffi/lib/openalias_ffi.dart
+++ /dev/null
@@ -1,160 +0,0 @@
-import 'dart:convert';
-import 'dart:ffi';
-import 'dart:io';
-import 'dart:isolate';
-
-import 'package:ffi/ffi.dart';
-
-import 'src/openalias_records.dart';
-
-export 'src/openalias_records.dart';
-
-const _libName = 'openalias_ffi';
-
-DynamicLibrary _load() {
- if (Platform.isAndroid || Platform.isLinux) {
- return DynamicLibrary.open('lib$_libName.so');
- } else if (Platform.isIOS || Platform.isMacOS) {
- return DynamicLibrary.open('$_libName.framework/$_libName');
- } else if (Platform.isWindows) {
- return DynamicLibrary.open('$_libName.dll');
- }
- throw UnsupportedError('${Platform.operatingSystem} is not supported');
-}
-
-typedef _SecureTxtNative = Pointer<Utf8> Function(Pointer<Utf8>, Uint16);
-typedef _SecureTxtDart = Pointer<Utf8> Function(Pointer<Utf8>, int);
-typedef _FreeNative = Void Function(Pointer<Utf8>);
-typedef _FreeDart = void Function(Pointer<Utf8>);
-typedef _LastErrorNative = Pointer<Utf8> Function();
-typedef _LastErrorDart = Pointer<Utf8> Function();
-
-/// Resolves OpenAlias aliases to addresses with end-to-end DNSSEC validation,
-/// over Tor. Backed by a Rust (hickory) native library.
-class OpenAliasFfi {
- /// Resolves [alias] — an FQDN or an email-style `name@domain` — to a payment
- /// record, requiring a DNSSEC-secure answer and routing every query through
- /// the Tor SOCKS proxy at [socksPort].
- ///
- /// OpenAlias v2 is preferred and v1 is the fallback, as the spec's
- /// compatibility rule requires: when the recipient publishes any usable
- /// `_openalias-payment` record, only those are considered, and the `oa1:`
- /// record on the FQDN is used only when they publish none.
- ///
- /// [network] and [asset] say what the caller can pay (`xmr` and `xmr` for
- /// Monero). [nativeAsset] is that network's native asset per the OA2 network
- /// list, which is what a v2 record that omits `asset` denotes. [asset] also
- /// selects the v1 prefix to look for (`oa1:xmr`).
- ///
- /// Throws [NotAnAliasException] if [alias] is a raw address rather than an
- /// alias, and [OpenAliasException] on anything else that leaves us without an
- /// address: no record, an answer that is not DNSSEC-secure, a network
- /// failure, or a recipient who publishes v2 records but none this wallet can
- /// pay.
- ///
- /// The native calls block while they query Tor, so each one runs in a
- /// background isolate.
- static Future<OpenAliasResult> resolve({
- required String alias,
- required String network,
- required String asset,
- required int socksPort,
- String? nativeAsset,
- bool fetchMetadata = true,
- }) async {
- final fqdn = normalizeAlias(alias);
-
- // The three names are independent, and each lookup is a slow round trip
- // over Tor, so they run concurrently rather than one after another;
- // resolveFromLookups then decides which answer gets used. Metadata is
- // fetched for its display name only, and never blocks a payment.
- final lookups = await Future.wait([
- _lookup('$oa2PaymentPrefix.$fqdn', socksPort),
- fetchMetadata
- ? _lookup('$oa2MetadataPrefix.$fqdn', socksPort)
- : Future.value(const _TxtLookup.skipped()),
- _lookup(fqdn, socksPort),
- ]);
- final oa2Payment = lookups[0];
- final oa2Metadata = lookups[1];
- final oa1 = lookups[2];
-
- return resolveFromLookups(
- OpenAliasLookups(
- paymentRecords: oa2Payment.records,
- metadataRecords: oa2Metadata.records,
- oa1Records: oa1.records,
- paymentProblem: oa2Payment.problem,
- oa1Problem: oa1.problem,
- ),
- alias: alias,
- network: network,
- asset: asset,
- nativeAsset: nativeAsset,
- );
- }
-}
-
-/// One name's TXT records, or why there are none. A failed lookup is not fatal
-/// on its own — the caller decides, since v1 can still answer when v2 does not
-/// (and the reason is kept for the error message if nothing answers).
-class _TxtLookup {
- const _TxtLookup(this.records) : problem = null;
- const _TxtLookup.failed(this.problem) : records = const [];
- const _TxtLookup.skipped() : records = const [], problem = 'not looked up';
-
- final List<String> records;
- final String? problem;
-}
-
-Future<_TxtLookup> _lookup(String name, int socksPort) async {
- try {
- return _TxtLookup(await Isolate.run(() => _secureTxtSync(name, socksPort)));
- } on OpenAliasException catch (e) {
- return _TxtLookup.failed(e.message);
- } catch (e) {
- return _TxtLookup.failed(e.toString());
- }
-}
-
-/// Fetches the DNSSEC-validated TXT records at [name] through the native
-/// resolver. Each record comes back with its DNS character-strings already
-/// concatenated. Throws [OpenAliasException] when the answer could not be
-/// validated, the name has no TXT records, or the lookup failed.
-List<String> _secureTxtSync(String name, int socksPort) {
- final lib = _load();
- final secureTxt = lib.lookupFunction<_SecureTxtNative, _SecureTxtDart>('openalias_secure_txt');
- final freeStr = lib.lookupFunction<_FreeNative, _FreeDart>('openalias_string_free');
- final lastError = lib.lookupFunction<_LastErrorNative, _LastErrorDart>(
- 'openalias_last_error_message',
- );
-
- final namePtr = name.toNativeUtf8();
- try {
- final result = secureTxt(namePtr, socksPort);
- if (result == nullptr) {
- final errPtr = lastError();
- final message = errPtr == nullptr || errPtr.toDartString().isEmpty
- ? 'OpenAlias lookup failed'
- : errPtr.toDartString();
- if (errPtr != nullptr) freeStr(errPtr);
- throw OpenAliasException(message);
- }
-
- final String encoded;
- try {
- encoded = result.toDartString();
- } finally {
- // The buffer was allocated by Rust; hand it back even if decoding throws.
- freeStr(result);
- }
-
- final decoded = jsonDecode(encoded);
- if (decoded is! List) {
- throw OpenAliasException('resolver returned malformed output for $name');
- }
- return [for (final record in decoded) record as String];
- } finally {
- malloc.free(namePtr);
- }
-}
diff --git a/plugins/openalias_ffi/lib/src/openalias_records.dart b/plugins/openalias_ffi/lib/src/openalias_records.dart
deleted file mode 100644
index 277f37c..0000000
--- a/plugins/openalias_ffi/lib/src/openalias_records.dart
+++ /dev/null
@@ -1,381 +0,0 @@
-/// Pure-Dart OpenAlias record handling: alias normalization, OpenAlias v1 and
-/// v2 record parsing, and v2 record selection.
-///
-/// Deliberately free of `dart:ffi` so the grammar can be unit-tested without a
-/// native build (see `test/openalias_records_test.dart`). The DNSSEC-validated
-/// lookup that feeds it lives in `openalias_ffi.dart`.
-library;
-
-/// OA2 records live under these prefixes on the alias FQDN, while OA1 records
-/// live on the FQDN itself — which is how both can coexist for one recipient.
-const String oa2PaymentPrefix = '_openalias-payment';
-const String oa2MetadataPrefix = '_openalias-metadata';
-
-/// A record with no `priority` is the lowest priority there is.
-const int _noPriority = 1 << 30;
-
-class OpenAliasException implements Exception {
- OpenAliasException(this.message);
-
- final String message;
-
- @override
- String toString() => message;
-}
-
-/// The input is a raw address rather than an alias, so nothing was looked up.
-class NotAnAliasException extends OpenAliasException {
- NotAnAliasException(super.message);
-}
-
-/// A payment destination parsed from an OpenAlias record.
-class OpenAliasPayment {
- const OpenAliasPayment({
- required this.version,
- required this.network,
- required this.address,
- this.asset,
- this.addressType,
- this.priority,
- this.amount,
- this.memo,
- this.recipientName,
- this.description,
- this.fields = const {},
- });
-
- /// 1 for an `oa1:` record, 2 for an `_openalias-payment` record.
- final int version;
-
- /// OA2 `network`. OA1 has no network field, so its asset prefix stands in
- /// (`oa1:xmr` → `xmr`), which is what that prefix means in practice.
- final String network;
-
- final String address;
-
- /// OA2 `asset`; null when the record omits it, which denotes the network's
- /// native asset.
- final String? asset;
-
- /// OA2 `address_type`, e.g. `bip352`. Informational.
- final String? addressType;
-
- /// OA2 `priority`; lower is preferred. Null when the record omits it.
- final int? priority;
-
- /// A requested amount in the asset's standard units, as published. A request
- /// to prefill, never a constraint on what the sender sends.
- final String? amount;
-
- /// A network-specific identifier (destination tag, Stellar memo, legacy
- /// Monero payment ID). OA1 publishes this as `tx_payment_id`.
- final String? memo;
-
- /// OA1 `recipient_name`. OA2 publishes the name in its metadata record
- /// instead, so this is null for v2 records.
- final String? recipientName;
-
- /// OA1 `tx_description`.
- final String? description;
-
- /// Every parsed pair, including keys this client does not recognize.
- final Map<String, String> fields;
-
- /// The asset this record pays. An omitted `asset` denotes [nativeAsset] — the
- /// network's native asset per the OA2 network list — which is why the caller
- /// has to supply it: on some networks the native asset is not the network
- /// code (the native asset on `base` is `eth`).
- String? effectiveAsset(String? nativeAsset) => asset ?? nativeAsset;
-}
-
-/// A resolved alias: the record to pay, plus what else the recipient published
-/// for the user to check before sending.
-class OpenAliasResult {
- const OpenAliasResult({required this.payment, this.alternatives = const [], this.metadata});
-
- /// The record this wallet should pay.
- final OpenAliasPayment payment;
-
- /// The other records this wallet could have paid, next-preferred first.
- final List<OpenAliasPayment> alternatives;
-
- /// The recipient's `_openalias-metadata` record, when they publish one (v2
- /// only).
- final Map<String, String>? metadata;
-
- /// 1 if this came from an `oa1:` record, 2 from `_openalias-payment`.
- int get version => payment.version;
-
- /// A display name for the recipient: the v2 metadata `name`, or the v1
- /// `recipient_name`. Recipient-supplied text, for display only — it never
- /// affects which address is paid.
- String? get recipientName => metadata?['name'] ?? payment.recipientName;
-}
-
-/// The raw TXT records found at an alias's three names, plus why a lookup came
-/// back empty. Separating this from the lookups themselves keeps the choice
-/// below testable without DNS.
-class OpenAliasLookups {
- const OpenAliasLookups({
- this.paymentRecords = const [],
- this.metadataRecords = const [],
- this.oa1Records = const [],
- this.paymentProblem,
- this.oa1Problem,
- });
-
- /// TXT records at `_openalias-payment.<fqdn>`.
- final List<String> paymentRecords;
-
- /// TXT records at `_openalias-metadata.<fqdn>`.
- final List<String> metadataRecords;
-
- /// TXT records at the alias FQDN itself, where OA1 records live.
- final List<String> oa1Records;
-
- /// Why the v2 payment lookup returned nothing, if it failed.
- final String? paymentProblem;
-
- /// Why the v1 lookup returned nothing, if it failed.
- final String? oa1Problem;
-}
-
-/// Decides what to pay from already-fetched records.
-///
-/// OpenAlias v2 is preferred and v1 is the fallback, as the spec's
-/// compatibility rule requires: when the recipient publishes any usable
-/// `_openalias-payment` record, only those are considered, and the `oa1:`
-/// record on the FQDN is used only when they publish none.
-///
-/// [network], [asset] and [nativeAsset] describe what the caller can pay; see
-/// [selectPayments]. [alias] is used only in error messages. Throws
-/// [OpenAliasException] when nothing payable was published.
-OpenAliasResult resolveFromLookups(
- OpenAliasLookups lookups, {
- required String alias,
- required String network,
- required String asset,
- String? nativeAsset,
-}) {
- // A record that fails to parse is skipped rather than fatal: only records
- // that are actually usable v2 payments count as "the recipient publishes v2",
- // so one malformed record can't strand a working v1 alias.
- final v2 = <OpenAliasPayment>[];
- for (final text in lookups.paymentRecords) {
- final fields = parseKeyValueRecord(text);
- if (fields == null) continue;
- final record = parseOa2Payment(fields);
- if (record != null) v2.add(record);
- }
-
- if (v2.isNotEmpty) {
- final payable = selectPayments(v2, network: network, asset: asset, nativeAsset: nativeAsset);
- if (payable.isEmpty) {
- // Falling back to v1 here would pay an address the recipient has since
- // superseded, so this fails instead. Record content is publisher-chosen
- // and this message is logged, so only a bounded summary of it is quoted.
- final offered = v2.map((record) => _clip(record.network)).toSet().take(4).join(', ');
- throw OpenAliasException(
- '$alias publishes ${v2.length} OpenAlias v2 payment record(s) '
- '($offered) but none for $asset on $network',
- );
- }
- return OpenAliasResult(
- payment: payable.first,
- alternatives: payable.skip(1).toList(),
- metadata: parseMetadata(lookups.metadataRecords),
- );
- }
-
- for (final text in lookups.oa1Records) {
- final record = parseOa1Payment(text, asset);
- if (record != null) return OpenAliasResult(payment: record);
- }
-
- throw OpenAliasException(
- 'no OpenAlias record for $asset at $alias '
- '(v2: ${lookups.paymentProblem ?? 'no usable payment record'}; '
- 'v1: ${lookups.oa1Problem ?? 'no oa1:$asset record'})',
- );
-}
-
-/// Normalizes user input into the alias FQDN to query (OA2 "Resolving
-/// OpenAlias Records", step 1).
-///
-/// `donate@openalias.org` becomes `donate.openalias.org`, and a trailing root
-/// dot is dropped. Throws [NotAnAliasException] when the input contains no `.`
-/// — per the spec that is a raw address, not an alias — and
-/// [OpenAliasException] when it is malformed. Internationalized names are
-/// passed through as typed: the native resolver converts non-ASCII labels to
-/// their A-label (Punycode) form when it parses the name.
-String normalizeAlias(String input) {
- var alias = input.trim();
- while (alias.endsWith('.')) {
- alias = alias.substring(0, alias.length - 1);
- }
- if (alias.isEmpty) {
- throw NotAnAliasException('empty input');
- }
-
- final ats = '@'.allMatches(alias).length;
- if (ats > 1) {
- throw OpenAliasException("malformed alias '$input': more than one '@'");
- }
- if (ats == 1) {
- alias = alias.replaceFirst('@', '.');
- }
- if (!alias.contains('.')) {
- throw NotAnAliasException("'$input' looks like a raw address, not an alias");
- }
-
- final labels = alias.split('.');
- if (labels.any((label) => label.isEmpty)) {
- throw OpenAliasException("malformed alias '$input': contains an empty label");
- }
- if (alias.length > 253 || labels.any((label) => label.length > 63)) {
- throw OpenAliasException("malformed alias '$input': name is too long for DNS");
- }
-
- // DNS names are case-insensitive; lower-casing keeps the caller's resolve
- // cache from missing on the same alias typed differently.
- return alias.toLowerCase();
-}
-
-/// Parses one TXT record into `{key: value}` pairs per the OA2 Key-Value
-/// Encoding rules.
-///
-/// Returns null when the record is not key-value data at all, uses a key
-/// outside the allowed characters, or repeats a key — the spec requires
-/// rejecting a repeated key rather than guessing which value was meant.
-Map<String, String>? parseKeyValueRecord(String text) {
- final fields = <String, String>{};
-
- // Pairs are separated by ';', with an optional space after it and an optional
- // trailing ';'. A value may itself contain '=' (only the first is
- // significant) but never ';'.
- for (final pair in text.split(';')) {
- final trimmed = pair.trim();
- if (trimmed.isEmpty) continue;
-
- final eq = trimmed.indexOf('=');
- if (eq < 0) return null;
-
- final key = trimmed.substring(0, eq).trim().toLowerCase();
- final value = trimmed.substring(eq + 1).trim();
- if (!_keyPattern.hasMatch(key)) return null;
- if (fields.containsKey(key)) return null;
-
- fields[key] = value;
- }
-
- return fields;
-}
-
-/// Keys are ASCII letters, digits and underscores, matched case-insensitively.
-final RegExp _keyPattern = RegExp(r'^[a-z0-9_]+$');
-
-/// Builds a payment record from the fields of an `_openalias-payment` record,
-/// or null when it is not a usable v2 payment: a version other than 2, or a
-/// missing required field, must be rejected rather than interpreted.
-///
-/// Unrecognized keys are kept in [OpenAliasPayment.fields] and otherwise
-/// ignored, so records can gain keys without breaking this client.
-OpenAliasPayment? parseOa2Payment(Map<String, String> fields) {
- if (fields['oa_version'] != '2') return null;
-
- final network = _nonEmpty(fields['network']);
- final address = _nonEmpty(fields['address']);
- if (network == null || address == null) return null;
-
- final priority = fields['priority'];
-
- return OpenAliasPayment(
- version: 2,
- network: network,
- address: address,
- asset: _nonEmpty(fields['asset']),
- addressType: _nonEmpty(fields['address_type']),
- priority: priority == null ? null : int.tryParse(priority),
- amount: _nonEmpty(fields['amount']),
- memo: _nonEmpty(fields['memo']),
- fields: Map.unmodifiable(fields),
- );
-}
-
-/// Parses an OA1 record (`oa1:<asset> recipient_address=...;`) for [asset].
-/// Returns null when the record is for a different asset, is not an OA1 record,
-/// or has no recipient address.
-OpenAliasPayment? parseOa1Payment(String text, String asset) {
- final prefix = 'oa1:${asset.toLowerCase()}';
- final trimmed = text.trimLeft();
- if (!trimmed.toLowerCase().startsWith(prefix)) return null;
-
- // The prefix is its own token: `oa1:xmrfoo` is not an `oa1:xmr` record.
- final rest = trimmed.substring(prefix.length);
- if (rest.isNotEmpty && !rest.startsWith(' ')) return null;
-
- final fields = parseKeyValueRecord(rest);
- if (fields == null) return null;
-
- final address = _nonEmpty(fields['recipient_address']);
- if (address == null) return null;
-
- return OpenAliasPayment(
- version: 1,
- network: asset.toLowerCase(),
- asset: asset.toLowerCase(),
- address: address,
- amount: _nonEmpty(fields['tx_amount']),
- memo: _nonEmpty(fields['tx_payment_id']),
- recipientName: _nonEmpty(fields['recipient_name']),
- description: _nonEmpty(fields['tx_description']),
- fields: Map.unmodifiable(fields),
- );
-}
-
-/// The subset of [records] this wallet can pay, most-preferred first.
-///
-/// Filters to [network] and [asset] — where a record that omits `asset` denotes
-/// [nativeAsset] — and then orders by `priority`, lower first, with records
-/// that publish no priority last (OA2 "Choosing Priorities": prefer the
-/// recipient's stated priority among the records the sender supports).
-List<OpenAliasPayment> selectPayments(
- List<OpenAliasPayment> records, {
- required String network,
- required String asset,
- String? nativeAsset,
-}) {
- final payable = <OpenAliasPayment>[];
- for (final record in records) {
- if (!_sameToken(record.network, network)) continue;
- final effective = record.effectiveAsset(nativeAsset);
- if (effective != null && _sameToken(effective, asset)) payable.add(record);
- }
-
- // Sorted on (priority, published order): List.sort is not stable, and records
- // sharing a priority should stay in the order the zone published them.
- final ordered = payable.asMap().entries.toList();
- ordered.sort((a, b) {
- final byPriority = (a.value.priority ?? _noPriority).compareTo(b.value.priority ?? _noPriority);
- return byPriority != 0 ? byPriority : a.key.compareTo(b.key);
- });
-
- return [for (final entry in ordered) entry.value];
-}
-
-/// The first `_openalias-metadata` record among [texts], or null. Metadata is
-/// optional and must never block a payment.
-Map<String, String>? parseMetadata(List<String> texts) {
- for (final text in texts) {
- final fields = parseKeyValueRecord(text);
- if (fields != null && fields['oa_version'] == '2') return fields;
- }
- return null;
-}
-
-bool _sameToken(String a, String b) => a.toLowerCase() == b.toLowerCase();
-
-/// Bounds a publisher-supplied value quoted in an error message.
-String _clip(String value) => value.length <= 16 ? value : '${value.substring(0, 16)}…';
-
-String? _nonEmpty(String? value) => (value == null || value.isEmpty) ? null : value;
diff --git a/plugins/openalias_ffi/linux/CMakeLists.txt b/plugins/openalias_ffi/linux/CMakeLists.txt
deleted file mode 100644
index 9726df4..0000000
--- a/plugins/openalias_ffi/linux/CMakeLists.txt
+++ /dev/null
@@ -1,23 +0,0 @@
-# The Flutter tooling requires that developers have CMake 3.10 or later
-# installed. You should not increase this version, as doing so will cause
-# the plugin to fail to compile for some customers of the plugin.
-cmake_minimum_required(VERSION 3.10)
-
-# Project-level configuration.
-set(PROJECT_NAME "openalias_ffi")
-project(${PROJECT_NAME} LANGUAGES CXX)
-
-# Invoke the build for native code shared with the other target platforms.
-# This can be changed to accommodate different builds.
-include("../cargokit/cmake/cargokit.cmake")
-apply_cargokit(${PROJECT_NAME} ../rust openalias_ffi "")
-
-# List of absolute paths to libraries that should be bundled with the plugin.
-# This list could contain prebuilt libraries, or libraries created by an
-# external build triggered from this build file.
-set(openalias_ffi_bundled_libraries
- # Defined in ../src/CMakeLists.txt.
- # This can be changed to accommodate different builds.
- "${${PROJECT_NAME}_cargokit_lib}"
- PARENT_SCOPE
-)
diff --git a/plugins/openalias_ffi/macos/Classes/openalias_ffi.c b/plugins/openalias_ffi/macos/Classes/openalias_ffi.c
deleted file mode 100644
index 09168e8..0000000
--- a/plugins/openalias_ffi/macos/Classes/openalias_ffi.c
+++ /dev/null
@@ -1,2 +0,0 @@
-// Relative import to be able to reuse the C sources.
-// See the comment in ../{projectName}}.podspec for more information.
diff --git a/plugins/openalias_ffi/macos/openalias_ffi.podspec b/plugins/openalias_ffi/macos/openalias_ffi.podspec
deleted file mode 100644
index 39395a3..0000000
--- a/plugins/openalias_ffi/macos/openalias_ffi.podspec
+++ /dev/null
@@ -1,39 +0,0 @@
-#
-# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
-# Run `pod lib lint openalias_ffi.podspec` to validate before publishing.
-#
-Pod::Spec.new do |s|
- s.name = 'openalias_ffi'
- s.version = '0.0.1'
- s.summary = 'A new Flutter FFI plugin project.'
- s.description = <<-DESC
-A new Flutter FFI plugin project.
- DESC
- s.homepage = 'http://example.com'
- s.license = { :file => '../LICENSE' }
- s.author = { 'Your Company' => 'email@example.com' }
-
- s.source = { :path => '.' }
- s.source_files = 'Classes/**/*'
- s.platform = :osx, '10.13'
-
- s.script_phase = {
- :name => 'Build Rust library',
- # First argument is relative path to the `rust` folder, second is name of rust library
- :script => 'sh "$PODS_TARGET_SRCROOT/../cargokit/build_pod.sh" ../rust openalias_ffi',
- :execution_position => :before_compile,
- :input_files => ['${BUILT_PRODUCTS_DIR}/cargokit_phony'],
- # Let XCode know that the static library referenced in -force_load below is
- # created by this build step.
- :output_files => ["${BUILT_PRODUCTS_DIR}/libopenalias_ffi.a"],
- }
- s.pod_target_xcconfig = {
- 'DEFINES_MODULE' => 'YES',
- # We use `-force_load` instead of `-l` since Xcode strips out unused symbols from static libraries.
- 'OTHER_LDFLAGS' => '-force_load ${BUILT_PRODUCTS_DIR}/libopenalias_ffi.a',
- 'DEAD_CODE_STRIPPING' => 'YES',
- 'STRIP_INSTALLED_PRODUCT[config=Release][sdk=*][arch=*]' => "YES",
- 'STRIP_STYLE[config=Release][sdk=*][arch=*]' => "non-global",
- 'DEPLOYMENT_POSTPROCESSING[config=Release][sdk=*][arch=*]' => "YES",
- }
-end
diff --git a/plugins/openalias_ffi/pubspec.yaml b/plugins/openalias_ffi/pubspec.yaml
deleted file mode 100644
index e3fb960..0000000
--- a/plugins/openalias_ffi/pubspec.yaml
+++ /dev/null
@@ -1,34 +0,0 @@
-name: openalias_ffi
-description: OpenAlias resolver with end-to-end DNSSEC validation over Tor, via Rust (hickory) FFI.
-version: 0.0.1
-publish_to: none
-
-environment:
- sdk: '>=3.5.3 <4.0.0'
- flutter: ">=3.24.3"
-
-dependencies:
- flutter:
- sdk: flutter
- ffi: ^2.0.1
- plugin_platform_interface: ^2.0.2
-
-dev_dependencies:
- ffigen: ^6.1.2
- flutter_test:
- sdk: flutter
- flutter_lints: ^2.0.0
-
-flutter:
- plugin:
- platforms:
- android:
- ffiPlugin: true
- ios:
- ffiPlugin: true
- linux:
- ffiPlugin: true
- macos:
- ffiPlugin: true
- windows:
- ffiPlugin: true
diff --git a/plugins/openalias_ffi/rust/.gitignore b/plugins/openalias_ffi/rust/.gitignore
deleted file mode 100644
index ea8c4bf..0000000
--- a/plugins/openalias_ffi/rust/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/target
diff --git a/plugins/openalias_ffi/rust/Cargo.lock b/plugins/openalias_ffi/rust/Cargo.lock
deleted file mode 100644
index 250c095..0000000
--- a/plugins/openalias_ffi/rust/Cargo.lock
+++ /dev/null
@@ -1,1722 +0,0 @@
-# This file is automatically @generated by Cargo.
-# It is not intended for manual editing.
-version = 4
-
-[[package]]
-name = "async-trait"
-version = "0.1.89"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.118",
-]
-
-[[package]]
-name = "atomic-waker"
-version = "1.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
-
-[[package]]
-name = "atty"
-version = "0.2.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
-dependencies = [
- "hermit-abi",
- "libc",
- "winapi",
-]
-
-[[package]]
-name = "autocfg"
-version = "1.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
-
-[[package]]
-name = "bitflags"
-version = "1.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
-
-[[package]]
-name = "bitflags"
-version = "2.13.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
-
-[[package]]
-name = "bumpalo"
-version = "3.20.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
-
-[[package]]
-name = "bytes"
-version = "1.12.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593"
-
-[[package]]
-name = "cbindgen"
-version = "0.24.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4b922faaf31122819ec80c4047cc684c6979a087366c069611e33649bf98e18d"
-dependencies = [
- "clap",
- "heck",
- "indexmap 1.9.3",
- "log",
- "proc-macro2",
- "quote",
- "serde",
- "serde_json",
- "syn 1.0.109",
- "tempfile",
- "toml",
-]
-
-[[package]]
-name = "cc"
-version = "1.2.65"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96"
-dependencies = [
- "find-msvc-tools",
- "shlex",
-]
-
-[[package]]
-name = "cfg-if"
-version = "1.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
-
-[[package]]
-name = "chacha20"
-version = "0.10.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
-dependencies = [
- "cfg-if",
- "cpufeatures",
- "rand_core",
-]
-
-[[package]]
-name = "clap"
-version = "3.2.25"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123"
-dependencies = [
- "atty",
- "bitflags 1.3.2",
- "clap_lex",
- "indexmap 1.9.3",
- "strsim",
- "termcolor",
- "textwrap",
-]
-
-[[package]]
-name = "clap_lex"
-version = "0.2.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
-dependencies = [
- "os_str_bytes",
-]
-
-[[package]]
-name = "combine"
-version = "4.6.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
-dependencies = [
- "bytes",
- "memchr",
-]
-
-[[package]]
-name = "cpufeatures"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
-dependencies = [
- "libc",
-]
-
-[[package]]
-name = "critical-section"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
-
-[[package]]
-name = "crossbeam-channel"
-version = "0.5.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
-dependencies = [
- "crossbeam-utils",
-]
-
-[[package]]
-name = "crossbeam-epoch"
-version = "0.9.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
-dependencies = [
- "crossbeam-utils",
-]
-
-[[package]]
-name = "crossbeam-utils"
-version = "0.8.21"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
-
-[[package]]
-name = "data-encoding"
-version = "2.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
-
-[[package]]
-name = "deranged"
-version = "0.5.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
-
-[[package]]
-name = "displaydoc"
-version = "0.2.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.118",
-]
-
-[[package]]
-name = "either"
-version = "1.16.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
-
-[[package]]
-name = "equivalent"
-version = "1.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
-
-[[package]]
-name = "errno"
-version = "0.3.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
-dependencies = [
- "libc",
- "windows-sys 0.61.2",
-]
-
-[[package]]
-name = "fastrand"
-version = "2.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
-
-[[package]]
-name = "find-msvc-tools"
-version = "0.1.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
-
-[[package]]
-name = "fnv"
-version = "1.0.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
-
-[[package]]
-name = "form_urlencoded"
-version = "1.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
-dependencies = [
- "percent-encoding",
-]
-
-[[package]]
-name = "futures-channel"
-version = "0.3.32"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
-dependencies = [
- "futures-core",
-]
-
-[[package]]
-name = "futures-core"
-version = "0.3.32"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
-
-[[package]]
-name = "futures-io"
-version = "0.3.32"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
-
-[[package]]
-name = "futures-macro"
-version = "0.3.32"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.118",
-]
-
-[[package]]
-name = "futures-sink"
-version = "0.3.32"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
-
-[[package]]
-name = "futures-task"
-version = "0.3.32"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
-
-[[package]]
-name = "futures-util"
-version = "0.3.32"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
-dependencies = [
- "futures-core",
- "futures-macro",
- "futures-task",
- "pin-project-lite",
- "slab",
-]
-
-[[package]]
-name = "getrandom"
-version = "0.2.17"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
-dependencies = [
- "cfg-if",
- "libc",
- "wasi",
-]
-
-[[package]]
-name = "getrandom"
-version = "0.4.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
-dependencies = [
- "cfg-if",
- "libc",
- "r-efi",
- "rand_core",
-]
-
-[[package]]
-name = "glob"
-version = "0.3.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
-
-[[package]]
-name = "h2"
-version = "0.4.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54"
-dependencies = [
- "atomic-waker",
- "bytes",
- "fnv",
- "futures-core",
- "futures-sink",
- "http",
- "indexmap 2.14.0",
- "slab",
- "tokio",
- "tokio-util",
- "tracing",
-]
-
-[[package]]
-name = "hashbrown"
-version = "0.12.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
-
-[[package]]
-name = "hashbrown"
-version = "0.17.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
-
-[[package]]
-name = "heck"
-version = "0.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
-
-[[package]]
-name = "hermit-abi"
-version = "0.1.19"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
-dependencies = [
- "libc",
-]
-
-[[package]]
-name = "hickory-net"
-version = "0.26.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183"
-dependencies = [
- "async-trait",
- "bitflags 2.13.0",
- "bytes",
- "cfg-if",
- "data-encoding",
- "futures-channel",
- "futures-io",
- "futures-util",
- "h2",
- "hickory-proto",
- "http",
- "idna",
- "ipnet",
- "jni",
- "lru-cache",
- "parking_lot",
- "rand",
- "ring",
- "rustls",
- "rustls-pki-types",
- "thiserror 2.0.18",
- "time",
- "tinyvec",
- "tokio",
- "tokio-rustls",
- "tracing",
- "url",
- "webpki-roots",
-]
-
-[[package]]
-name = "hickory-proto"
-version = "0.26.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0bab31817bfb44672a252e97fe81cd0c18d1b2cf892108922f6818820df8c643"
-dependencies = [
- "bitflags 2.13.0",
- "data-encoding",
- "idna",
- "ipnet",
- "jni",
- "once_cell",
- "prefix-trie",
- "rand",
- "ring",
- "rustls-pki-types",
- "thiserror 2.0.18",
- "time",
- "tinyvec",
- "tracing",
- "url",
-]
-
-[[package]]
-name = "hickory-resolver"
-version = "0.26.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f0d58d28879ceecde6607729660c2667a081ccdc082e082675042793960f178c"
-dependencies = [
- "cfg-if",
- "futures-util",
- "hickory-net",
- "hickory-proto",
- "ipnet",
- "moka",
- "once_cell",
- "parking_lot",
- "rand",
- "rustls",
- "smallvec",
- "thiserror 2.0.18",
- "tokio",
- "tokio-rustls",
- "tracing",
- "webpki-roots",
-]
-
-[[package]]
-name = "http"
-version = "1.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
-dependencies = [
- "bytes",
- "itoa",
-]
-
-[[package]]
-name = "icu_collections"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526"
-dependencies = [
- "displaydoc",
- "yoke",
- "zerofrom",
- "zerovec",
-]
-
-[[package]]
-name = "icu_locid"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637"
-dependencies = [
- "displaydoc",
- "litemap",
- "tinystr",
- "writeable",
- "zerovec",
-]
-
-[[package]]
-name = "icu_locid_transform"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e"
-dependencies = [
- "displaydoc",
- "icu_locid",
- "icu_locid_transform_data",
- "icu_provider",
- "tinystr",
- "zerovec",
-]
-
-[[package]]
-name = "icu_locid_transform_data"
-version = "1.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d"
-
-[[package]]
-name = "icu_normalizer"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f"
-dependencies = [
- "displaydoc",
- "icu_collections",
- "icu_normalizer_data",
- "icu_properties",
- "icu_provider",
- "smallvec",
- "utf16_iter",
- "utf8_iter",
- "write16",
- "zerovec",
-]
-
-[[package]]
-name = "icu_normalizer_data"
-version = "1.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7"
-
-[[package]]
-name = "icu_properties"
-version = "1.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5"
-dependencies = [
- "displaydoc",
- "icu_collections",
- "icu_locid_transform",
- "icu_properties_data",
- "icu_provider",
- "tinystr",
- "zerovec",
-]
-
-[[package]]
-name = "icu_properties_data"
-version = "1.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2"
-
-[[package]]
-name = "icu_provider"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9"
-dependencies = [
- "displaydoc",
- "icu_locid",
- "icu_provider_macros",
- "stable_deref_trait",
- "tinystr",
- "writeable",
- "yoke",
- "zerofrom",
- "zerovec",
-]
-
-[[package]]
-name = "icu_provider_macros"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.118",
-]
-
-[[package]]
-name = "idna"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
-dependencies = [
- "idna_adapter",
- "smallvec",
- "utf8_iter",
-]
-
-[[package]]
-name = "idna_adapter"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71"
-dependencies = [
- "icu_normalizer",
- "icu_properties",
-]
-
-[[package]]
-name = "indexmap"
-version = "1.9.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
-dependencies = [
- "autocfg",
- "hashbrown 0.12.3",
-]
-
-[[package]]
-name = "indexmap"
-version = "2.14.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
-dependencies = [
- "equivalent",
- "hashbrown 0.17.1",
-]
-
-[[package]]
-name = "ipnet"
-version = "2.12.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
-dependencies = [
- "serde",
-]
-
-[[package]]
-name = "itoa"
-version = "1.0.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
-
-[[package]]
-name = "jni"
-version = "0.22.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
-dependencies = [
- "cfg-if",
- "combine",
- "jni-macros",
- "jni-sys",
- "log",
- "simd_cesu8",
- "thiserror 2.0.18",
- "walkdir",
- "windows-link",
-]
-
-[[package]]
-name = "jni-macros"
-version = "0.22.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
-dependencies = [
- "proc-macro2",
- "quote",
- "rustc_version",
- "simd_cesu8",
- "syn 2.0.118",
-]
-
-[[package]]
-name = "jni-sys"
-version = "0.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
-dependencies = [
- "jni-sys-macros",
-]
-
-[[package]]
-name = "jni-sys-macros"
-version = "0.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
-dependencies = [
- "quote",
- "syn 2.0.118",
-]
-
-[[package]]
-name = "js-sys"
-version = "0.3.102"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31"
-dependencies = [
- "cfg-if",
- "futures-util",
- "wasm-bindgen",
-]
-
-[[package]]
-name = "lazy_static"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
-
-[[package]]
-name = "libc"
-version = "0.2.186"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
-
-[[package]]
-name = "linked-hash-map"
-version = "0.5.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
-
-[[package]]
-name = "linux-raw-sys"
-version = "0.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
-
-[[package]]
-name = "litemap"
-version = "0.7.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856"
-
-[[package]]
-name = "lock_api"
-version = "0.4.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
-dependencies = [
- "scopeguard",
-]
-
-[[package]]
-name = "log"
-version = "0.4.33"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
-
-[[package]]
-name = "lru-cache"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c"
-dependencies = [
- "linked-hash-map",
-]
-
-[[package]]
-name = "memchr"
-version = "2.8.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
-
-[[package]]
-name = "mio"
-version = "1.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda"
-dependencies = [
- "libc",
- "wasi",
- "windows-sys 0.61.2",
-]
-
-[[package]]
-name = "moka"
-version = "0.12.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046"
-dependencies = [
- "crossbeam-channel",
- "crossbeam-epoch",
- "crossbeam-utils",
- "equivalent",
- "parking_lot",
- "portable-atomic",
- "smallvec",
- "tagptr",
- "uuid",
-]
-
-[[package]]
-name = "num-conv"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
-
-[[package]]
-name = "num-traits"
-version = "0.2.19"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
-dependencies = [
- "autocfg",
-]
-
-[[package]]
-name = "once_cell"
-version = "1.21.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
-dependencies = [
- "critical-section",
- "portable-atomic",
-]
-
-[[package]]
-name = "openalias_ffi"
-version = "0.1.0"
-dependencies = [
- "cbindgen",
- "glob",
- "hickory-proto",
- "hickory-resolver",
- "lazy_static",
- "log",
- "tokio",
- "tokio-socks",
-]
-
-[[package]]
-name = "os_str_bytes"
-version = "6.6.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1"
-
-[[package]]
-name = "parking_lot"
-version = "0.12.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
-dependencies = [
- "lock_api",
- "parking_lot_core",
-]
-
-[[package]]
-name = "parking_lot_core"
-version = "0.9.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
-dependencies = [
- "cfg-if",
- "libc",
- "redox_syscall",
- "smallvec",
- "windows-link",
-]
-
-[[package]]
-name = "percent-encoding"
-version = "2.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
-
-[[package]]
-name = "pin-project-lite"
-version = "0.2.17"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
-
-[[package]]
-name = "portable-atomic"
-version = "1.13.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
-
-[[package]]
-name = "powerfmt"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
-
-[[package]]
-name = "prefix-trie"
-version = "0.8.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4cf6e3177f0684016a5c209b00882e15f8bdd3f3bb48f0491df10cd102d0c6e7"
-dependencies = [
- "either",
- "ipnet",
- "num-traits",
-]
-
-[[package]]
-name = "proc-macro2"
-version = "1.0.106"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
-dependencies = [
- "unicode-ident",
-]
-
-[[package]]
-name = "quote"
-version = "1.0.46"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
-dependencies = [
- "proc-macro2",
-]
-
-[[package]]
-name = "r-efi"
-version = "6.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
-
-[[package]]
-name = "rand"
-version = "0.10.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
-dependencies = [
- "chacha20",
- "getrandom 0.4.3",
- "rand_core",
-]
-
-[[package]]
-name = "rand_core"
-version = "0.10.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
-
-[[package]]
-name = "redox_syscall"
-version = "0.5.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
-dependencies = [
- "bitflags 2.13.0",
-]
-
-[[package]]
-name = "ring"
-version = "0.17.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
-dependencies = [
- "cc",
- "cfg-if",
- "getrandom 0.2.17",
- "libc",
- "untrusted",
- "windows-sys 0.52.0",
-]
-
-[[package]]
-name = "rustc_version"
-version = "0.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
-dependencies = [
- "semver",
-]
-
-[[package]]
-name = "rustix"
-version = "1.1.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
-dependencies = [
- "bitflags 2.13.0",
- "errno",
- "libc",
- "linux-raw-sys",
- "windows-sys 0.61.2",
-]
-
-[[package]]
-name = "rustls"
-version = "0.23.36"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b"
-dependencies = [
- "log",
- "once_cell",
- "ring",
- "rustls-pki-types",
- "rustls-webpki",
- "subtle",
- "zeroize",
-]
-
-[[package]]
-name = "rustls-pki-types"
-version = "1.14.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
-dependencies = [
- "zeroize",
-]
-
-[[package]]
-name = "rustls-webpki"
-version = "0.103.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53"
-dependencies = [
- "ring",
- "rustls-pki-types",
- "untrusted",
-]
-
-[[package]]
-name = "rustversion"
-version = "1.0.22"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
-
-[[package]]
-name = "same-file"
-version = "1.0.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
-dependencies = [
- "winapi-util",
-]
-
-[[package]]
-name = "scopeguard"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
-
-[[package]]
-name = "semver"
-version = "1.0.28"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
-
-[[package]]
-name = "serde"
-version = "1.0.228"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
-dependencies = [
- "serde_core",
- "serde_derive",
-]
-
-[[package]]
-name = "serde_core"
-version = "1.0.228"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
-dependencies = [
- "serde_derive",
-]
-
-[[package]]
-name = "serde_derive"
-version = "1.0.228"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.118",
-]
-
-[[package]]
-name = "serde_json"
-version = "1.0.150"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
-dependencies = [
- "itoa",
- "memchr",
- "serde",
- "serde_core",
- "zmij",
-]
-
-[[package]]
-name = "shlex"
-version = "2.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
-
-[[package]]
-name = "simd_cesu8"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520"
-dependencies = [
- "rustc_version",
- "simdutf8",
-]
-
-[[package]]
-name = "simdutf8"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
-
-[[package]]
-name = "slab"
-version = "0.4.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
-
-[[package]]
-name = "smallvec"
-version = "1.15.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
-
-[[package]]
-name = "socket2"
-version = "0.6.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
-dependencies = [
- "libc",
- "windows-sys 0.61.2",
-]
-
-[[package]]
-name = "stable_deref_trait"
-version = "1.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
-
-[[package]]
-name = "strsim"
-version = "0.10.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
-
-[[package]]
-name = "subtle"
-version = "2.6.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
-
-[[package]]
-name = "syn"
-version = "1.0.109"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
-dependencies = [
- "proc-macro2",
- "quote",
- "unicode-ident",
-]
-
-[[package]]
-name = "syn"
-version = "2.0.118"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
-dependencies = [
- "proc-macro2",
- "quote",
- "unicode-ident",
-]
-
-[[package]]
-name = "synstructure"
-version = "0.13.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.118",
-]
-
-[[package]]
-name = "tagptr"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
-
-[[package]]
-name = "tempfile"
-version = "3.27.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
-dependencies = [
- "fastrand",
- "getrandom 0.4.3",
- "once_cell",
- "rustix",
- "windows-sys 0.61.2",
-]
-
-[[package]]
-name = "termcolor"
-version = "1.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
-dependencies = [
- "winapi-util",
-]
-
-[[package]]
-name = "textwrap"
-version = "0.16.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
-
-[[package]]
-name = "thiserror"
-version = "1.0.69"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
-dependencies = [
- "thiserror-impl 1.0.69",
-]
-
-[[package]]
-name = "thiserror"
-version = "2.0.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
-dependencies = [
- "thiserror-impl 2.0.18",
-]
-
-[[package]]
-name = "thiserror-impl"
-version = "1.0.69"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.118",
-]
-
-[[package]]
-name = "thiserror-impl"
-version = "2.0.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.118",
-]
-
-[[package]]
-name = "time"
-version = "0.3.51"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327"
-dependencies = [
- "deranged",
- "num-conv",
- "powerfmt",
- "serde_core",
- "time-core",
-]
-
-[[package]]
-name = "time-core"
-version = "0.1.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
-
-[[package]]
-name = "tinystr"
-version = "0.7.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
-dependencies = [
- "displaydoc",
- "zerovec",
-]
-
-[[package]]
-name = "tinyvec"
-version = "1.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
-dependencies = [
- "tinyvec_macros",
-]
-
-[[package]]
-name = "tinyvec_macros"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
-
-[[package]]
-name = "tokio"
-version = "1.52.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
-dependencies = [
- "bytes",
- "libc",
- "mio",
- "pin-project-lite",
- "socket2",
- "tokio-macros",
- "windows-sys 0.61.2",
-]
-
-[[package]]
-name = "tokio-macros"
-version = "2.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.118",
-]
-
-[[package]]
-name = "tokio-rustls"
-version = "0.26.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
-dependencies = [
- "rustls",
- "tokio",
-]
-
-[[package]]
-name = "tokio-socks"
-version = "0.5.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7e2948f60dbe26b35f2c7fb74ac2854c1fddded0fe9d7548fcc674a246f7615"
-dependencies = [
- "either",
- "futures-util",
- "thiserror 1.0.69",
- "tokio",
-]
-
-[[package]]
-name = "tokio-util"
-version = "0.7.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
-dependencies = [
- "bytes",
- "futures-core",
- "futures-sink",
- "pin-project-lite",
- "tokio",
-]
-
-[[package]]
-name = "toml"
-version = "0.5.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
-dependencies = [
- "serde",
-]
-
-[[package]]
-name = "tracing"
-version = "0.1.44"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
-dependencies = [
- "pin-project-lite",
- "tracing-core",
-]
-
-[[package]]
-name = "tracing-core"
-version = "0.1.36"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
-dependencies = [
- "once_cell",
-]
-
-[[package]]
-name = "unicode-ident"
-version = "1.0.24"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
-
-[[package]]
-name = "untrusted"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
-
-[[package]]
-name = "url"
-version = "2.5.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
-dependencies = [
- "form_urlencoded",
- "idna",
- "percent-encoding",
- "serde",
-]
-
-[[package]]
-name = "utf16_iter"
-version = "1.0.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
-
-[[package]]
-name = "utf8_iter"
-version = "1.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
-
-[[package]]
-name = "uuid"
-version = "1.23.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7"
-dependencies = [
- "getrandom 0.4.3",
- "js-sys",
- "wasm-bindgen",
-]
-
-[[package]]
-name = "walkdir"
-version = "2.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
-dependencies = [
- "same-file",
- "winapi-util",
-]
-
-[[package]]
-name = "wasi"
-version = "0.11.1+wasi-snapshot-preview1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
-
-[[package]]
-name = "wasm-bindgen"
-version = "0.2.125"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a"
-dependencies = [
- "cfg-if",
- "once_cell",
- "rustversion",
- "wasm-bindgen-macro",
- "wasm-bindgen-shared",
-]
-
-[[package]]
-name = "wasm-bindgen-macro"
-version = "0.2.125"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d"
-dependencies = [
- "quote",
- "wasm-bindgen-macro-support",
-]
-
-[[package]]
-name = "wasm-bindgen-macro-support"
-version = "0.2.125"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd"
-dependencies = [
- "bumpalo",
- "proc-macro2",
- "quote",
- "syn 2.0.118",
- "wasm-bindgen-shared",
-]
-
-[[package]]
-name = "wasm-bindgen-shared"
-version = "0.2.125"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f"
-dependencies = [
- "unicode-ident",
-]
-
-[[package]]
-name = "webpki-roots"
-version = "1.0.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed"
-dependencies = [
- "rustls-pki-types",
-]
-
-[[package]]
-name = "winapi"
-version = "0.3.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
-dependencies = [
- "winapi-i686-pc-windows-gnu",
- "winapi-x86_64-pc-windows-gnu",
-]
-
-[[package]]
-name = "winapi-i686-pc-windows-gnu"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
-
-[[package]]
-name = "winapi-util"
-version = "0.1.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
-dependencies = [
- "windows-sys 0.61.2",
-]
-
-[[package]]
-name = "winapi-x86_64-pc-windows-gnu"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
-
-[[package]]
-name = "windows-link"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
-
-[[package]]
-name = "windows-sys"
-version = "0.52.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
-dependencies = [
- "windows-targets",
-]
-
-[[package]]
-name = "windows-sys"
-version = "0.61.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
-dependencies = [
- "windows-link",
-]
-
-[[package]]
-name = "windows-targets"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
-dependencies = [
- "windows_aarch64_gnullvm",
- "windows_aarch64_msvc",
- "windows_i686_gnu",
- "windows_i686_gnullvm",
- "windows_i686_msvc",
- "windows_x86_64_gnu",
- "windows_x86_64_gnullvm",
- "windows_x86_64_msvc",
-]
-
-[[package]]
-name = "windows_aarch64_gnullvm"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
-
-[[package]]
-name = "windows_aarch64_msvc"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
-
-[[package]]
-name = "windows_i686_gnu"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
-
-[[package]]
-name = "windows_i686_gnullvm"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
-
-[[package]]
-name = "windows_i686_msvc"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
-
-[[package]]
-name = "windows_x86_64_gnu"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
-
-[[package]]
-name = "windows_x86_64_gnullvm"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
-
-[[package]]
-name = "windows_x86_64_msvc"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
-
-[[package]]
-name = "write16"
-version = "1.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936"
-
-[[package]]
-name = "writeable"
-version = "0.5.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
-
-[[package]]
-name = "yoke"
-version = "0.7.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40"
-dependencies = [
- "serde",
- "stable_deref_trait",
- "yoke-derive",
- "zerofrom",
-]
-
-[[package]]
-name = "yoke-derive"
-version = "0.7.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.118",
- "synstructure",
-]
-
-[[package]]
-name = "zerofrom"
-version = "0.1.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
-dependencies = [
- "zerofrom-derive",
-]
-
-[[package]]
-name = "zerofrom-derive"
-version = "0.1.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.118",
- "synstructure",
-]
-
-[[package]]
-name = "zeroize"
-version = "1.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
-
-[[package]]
-name = "zerovec"
-version = "0.10.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079"
-dependencies = [
- "yoke",
- "zerofrom",
- "zerovec-derive",
-]
-
-[[package]]
-name = "zerovec-derive"
-version = "0.10.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.118",
-]
-
-[[package]]
-name = "zmij"
-version = "1.0.21"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
diff --git a/plugins/openalias_ffi/rust/Cargo.toml b/plugins/openalias_ffi/rust/Cargo.toml
deleted file mode 100644
index babd998..0000000
--- a/plugins/openalias_ffi/rust/Cargo.toml
+++ /dev/null
@@ -1,22 +0,0 @@
-[package]
-name = "openalias_ffi"
-version = "0.1.0"
-edition = "2021"
-
-[lib]
-crate-type = ["cdylib", "staticlib"]
-
-[dependencies]
-tokio = { version = "1", features = ["rt-multi-thread", "net", "io-util", "time"] }
-tokio-socks = "0.5"
-# DoH (https-ring) + bundled CA roots (webpki-roots): Tor exits reject plain DNS
-# (53) and many restrict 853, but 443 is almost always allowed — so queries go
-# over DNS-over-HTTPS through the SOCKS proxy. DNSSEC is still validated locally.
-hickory-resolver = { version = "0.26", default-features = false, features = ["dnssec-ring", "tokio", "https-ring", "webpki-roots"] }
-hickory-proto = { version = "0.26", default-features = false, features = ["dnssec-ring"] }
-lazy_static = "1.4"
-log = "0.4"
-
-[build-dependencies]
-cbindgen = "0.24"
-glob = "0.3"
diff --git a/plugins/openalias_ffi/rust/build.rs b/plugins/openalias_ffi/rust/build.rs
deleted file mode 100644
index 6117a8d..0000000
--- a/plugins/openalias_ffi/rust/build.rs
+++ /dev/null
@@ -1,78 +0,0 @@
-// SPDX-FileCopyrightText: 2023 Foundation Devices Inc.
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-use cbindgen::{Config, Language};
-use std::env;
-use std::path::PathBuf;
-use glob::glob;
-
-fn main() {
- android_on_linux_check();
-
- // Create C header files for Dart
- let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
- let package_name = env::var("CARGO_PKG_NAME").unwrap();
-
- let output_file = target_dir()
- .join(format!("{}.h", package_name))
- .display()
- .to_string();
-
- let config = Config {
- language: Language::C,
- ..Default::default()
- };
-
- cbindgen::generate_with_config(crate_dir, config)
- .unwrap()
- .write_to_file(output_file);
-}
-
-fn target_dir() -> PathBuf {
- if let Ok(target) = env::var("CARGO_TARGET_DIR") {
- PathBuf::from(target)
- } else {
- PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()).join("target")
- }
-}
-
-
-fn android_on_linux_check() {
- let target = env::var("TARGET").unwrap();
- if target == "x86_64-linux-android" {
- let os = match env::consts::OS {
- "macos" => "darwin",
- "windows" => "windows",
- _ => "linux",
- };
-
- let ndk_home_result = env::var("ANDROID_NDK_HOME");
- let ndk_home = if let Some(value) = ndk_home_result.ok() {
- value
- } else {
- println!("ANDROID_NDK_HOME not set. Trying _CARGOKIT_NDK_LINK_CLANG");
- let path_to_parse_with_hack = env::var("_CARGOKIT_NDK_LINK_CLANG")
- .expect("_CARGOKIT_NDK_LINK_CLANG not set");
-
- path_to_parse_with_hack
- .split("/toolchains/")
- .next()
- .expect("Failed to parse path to get NDK home")
- .to_string()
- };
-
- let link_search_glob = format!(
- "{}/toolchains/llvm/prebuilt/{}-x86_64/lib/clang/**/lib/linux",
- ndk_home, os
- );
-
- let link_search_path = glob(&link_search_glob)
- .expect("failed to read link_search_glob")
- .next()
- .expect("failed to find link_search_path")
- .expect("link_search_path glob result failed");
- println!("cargo:rustc-link-lib=static=clang_rt.builtins-x86_64-android");
- println!("cargo:rustc-link-search={}", link_search_path.display());
- }
-}
\ No newline at end of file
diff --git a/plugins/openalias_ffi/rust/rust-toolchain.toml b/plugins/openalias_ffi/rust/rust-toolchain.toml
deleted file mode 100644
index 3662df8..0000000
--- a/plugins/openalias_ffi/rust/rust-toolchain.toml
+++ /dev/null
@@ -1,5 +0,0 @@
-# hickory 0.25's dependency tree pulls edition2024 crates, which need cargo >= 1.85.
-# Pin so cargokit (via rustup) builds with a new-enough toolchain regardless of
-# the machine default. rustup auto-installs this channel if missing.
-[toolchain]
-channel = "1.88.0"
diff --git a/plugins/openalias_ffi/rust/src/error.rs b/plugins/openalias_ffi/rust/src/error.rs
deleted file mode 100644
index 0195e38..0000000
--- a/plugins/openalias_ffi/rust/src/error.rs
+++ /dev/null
@@ -1,20 +0,0 @@
-use std::cell::RefCell;
-use std::ffi::{c_char, CString};
-
-thread_local! {
- static LAST_ERROR: RefCell<Option<String>> = const { RefCell::new(None) };
-}
-
-/// Returns the most recent error message (empty string if none), as a freshly
-/// allocated C string the caller must free with `openalias_string_free`.
-#[no_mangle]
-pub unsafe extern "C" fn openalias_last_error_message() -> *mut c_char {
- let msg = LAST_ERROR.with(|prev| prev.borrow_mut().take()).unwrap_or_default();
- CString::new(msg).unwrap_or_default().into_raw()
-}
-
-pub fn set_last_error(msg: impl Into<String>) {
- let msg = msg.into();
- log::warn!("openalias error: {msg}");
- LAST_ERROR.with(|prev| *prev.borrow_mut() = Some(msg));
-}
diff --git a/plugins/openalias_ffi/rust/src/lib.rs b/plugins/openalias_ffi/rust/src/lib.rs
deleted file mode 100644
index 5ca5b94..0000000
--- a/plugins/openalias_ffi/rust/src/lib.rs
+++ /dev/null
@@ -1,289 +0,0 @@
-//! OpenAlias resolver with end-to-end DNSSEC validation, routed over Tor's
-//! SOCKS proxy. DNS is fetched via TCP through the proxy (Tor has no UDP) and
-//! validated locally by hickory (RRSIG → DS → root trust anchor), so no
-//! resolver is trusted and no DNS leaks outside Tor.
-//!
-//! The FFI surface is deliberately thin: it returns the DNSSEC-validated TXT
-//! records at a name, and Dart parses the OpenAlias v1 and v2 grammars on top
-//! (see `lib/src/openalias_records.dart`). That split keeps the record parsing
-//! and selection unit-testable without a native build, while the part that has
-//! to be trustworthy — "these bytes really are what the signed zone published"
-//! — stays here.
-
-mod error;
-
-use std::ffi::{c_char, CStr, CString};
-use std::future::Future;
-use std::io;
-use std::net::{IpAddr, Ipv4Addr, SocketAddr};
-use std::pin::Pin;
-use std::sync::Arc;
-use std::time::Duration;
-
-use hickory_proto::dnssec::Proof;
-use hickory_proto::rr::{RData, RecordType};
-use hickory_resolver::config::{NameServerConfig, ResolverConfig, ResolverOpts};
-use hickory_resolver::net::runtime::iocompat::AsyncIoTokioAsStd;
-use hickory_resolver::net::runtime::{RuntimeProvider, TokioHandle, TokioTime};
-use hickory_resolver::Resolver;
-use lazy_static::lazy_static;
-use tokio::net::{TcpStream as TokioTcpStream, UdpSocket as TokioUdpSocket};
-use tokio::runtime::{Builder, Runtime};
-use tokio_socks::tcp::Socks5Stream;
-
-use crate::error::set_last_error;
-
-lazy_static! {
- static ref RUNTIME: io::Result<Runtime> = Builder::new_multi_thread().enable_all().build();
-}
-
-/// Public recursive resolvers queried (over Tor) for the signed records, as
-/// DNS-over-HTTPS (port 443) — Tor exits reject plain DNS (53). They only
-/// transport the signed data — validation happens locally — so a malicious
-/// resolver can withhold an answer but cannot forge one. Each entry is the
-/// resolver IP and the TLS/host name used for its certificate + DoH endpoint.
-const UPSTREAMS: &[(Ipv4Addr, &str)] = &[
- (Ipv4Addr::new(9, 9, 9, 9), "dns.quad9.net"),
- (Ipv4Addr::new(1, 1, 1, 1), "cloudflare-dns.com"),
-];
-
-const TCP_TIMEOUT: Duration = Duration::from_secs(30);
-
-/// A hickory runtime provider whose TCP connections are dialed through a
-/// SOCKS5 proxy (Tor). UDP is unused (resolver is configured TCP-only).
-#[derive(Clone)]
-struct SocksRuntimeProvider {
- proxy: SocketAddr,
- // A persistent handle whose JoinSet outlives the spawned connection driver.
- // Returning a fresh TokioHandle per call would drop the JoinSet and abort
- // the h2 driver task → "receiver was canceled". hickory's own provider keeps
- // one shared handle for exactly this reason.
- handle: TokioHandle,
-}
-
-impl RuntimeProvider for SocksRuntimeProvider {
- type Handle = TokioHandle;
- type Timer = TokioTime;
- type Udp = TokioUdpSocket;
- type Tcp = AsyncIoTokioAsStd<Socks5Stream<TokioTcpStream>>;
-
- fn create_handle(&self) -> Self::Handle {
- self.handle.clone()
- }
-
- fn connect_tcp(
- &self,
- server_addr: SocketAddr,
- _bind_addr: Option<SocketAddr>,
- _timeout: Option<Duration>,
- ) -> Pin<Box<dyn Send + Future<Output = io::Result<Self::Tcp>>>> {
- let proxy = self.proxy;
- Box::pin(async move {
- let stream = Socks5Stream::connect(proxy, server_addr)
- .await
- .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
- Ok(AsyncIoTokioAsStd(stream))
- })
- }
-
- fn bind_udp(
- &self,
- _local_addr: SocketAddr,
- _server_addr: SocketAddr,
- ) -> Pin<Box<dyn Send + Future<Output = io::Result<Self::Udp>>>> {
- // Refused, never bound. Tor carries no UDP, so a UDP query could only
- // leave over clearnet. The resolver is configured with DoH name servers
- // exclusively and never asks for UDP; failing here means that if that
- // ever changed, the lookup would fail closed instead of leaking which
- // alias the user is resolving.
- Box::pin(async move {
- Err(io::Error::new(
- io::ErrorKind::Unsupported,
- "UDP is unavailable: OpenAlias DNS must go through the Tor SOCKS proxy",
- ))
- })
- }
-}
-
-/// Fetches the TXT records at `name` over Tor, requiring a DNSSEC-secure answer.
-///
-/// Returns a JSON array of strings — one entry per TXT record, each already
-/// concatenated from its DNS character-strings (RFC 7208 §3.3) — or NULL on any
-/// failure, which includes "no such name" and "name has no TXT records" (see
-/// `openalias_last_error_message`). Callers treat NULL as "nothing usable
-/// here": the OpenAlias v2 → v1 fallback is driven by which names returned
-/// records, and an answer that could not be validated never returns records.
-///
-/// Caller frees the returned string with `openalias_string_free`.
-///
-/// # Safety
-/// `name` must be a valid NUL-terminated C string.
-#[no_mangle]
-pub unsafe extern "C" fn openalias_secure_txt(
- name: *const c_char,
- socks_port: u16,
-) -> *mut c_char {
- let name = match cstr(name) {
- Some(s) => s,
- None => return ret_err("invalid name"),
- };
-
- let runtime = match RUNTIME.as_ref() {
- Ok(rt) => rt,
- Err(e) => return ret_err(format!("tokio runtime: {e}")),
- };
-
- match runtime.block_on(secure_txt(&name, socks_port)) {
- Ok(records) => match CString::new(json_string_array(&records)) {
- Ok(c) => c.into_raw(),
- Err(_) => ret_err("record contained NUL"),
- },
- Err(msg) => ret_err(msg),
- }
-}
-
-/// Frees a string returned by this library.
-///
-/// # Safety
-/// `ptr` must have been returned by this library and not already freed.
-#[no_mangle]
-pub unsafe extern "C" fn openalias_string_free(ptr: *mut c_char) {
- if !ptr.is_null() {
- drop(CString::from_raw(ptr));
- }
-}
-
-async fn secure_txt(name: &str, socks_port: u16) -> Result<Vec<String>, String> {
- let proxy = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), socks_port);
-
- // DNS-over-HTTPS (default /dns-query on 443). The TLS cert is verified
- // against `host`; the answer's DNSSEC chain is validated locally.
- let name_servers = UPSTREAMS
- .iter()
- .map(|(ip, host)| NameServerConfig::https(IpAddr::V4(*ip), Arc::from(*host), None))
- .collect();
- let config = ResolverConfig::from_parts(None, vec![], name_servers);
-
- let mut opts = ResolverOpts::default();
- opts.validate = true; // local DNSSEC validation against the root trust anchor
- opts.timeout = TCP_TIMEOUT;
-
- let provider = SocksRuntimeProvider { proxy, handle: TokioHandle::default() };
- let resolver = Resolver::builder_with_config(config, provider)
- .with_options(opts)
- .build()
- .map_err(|e| format!("resolver build failed: {e}"))?;
-
- // Non-ASCII labels are converted to their A-label (Punycode) form by
- // hickory when it parses the name, as OpenAlias requires (RFC 5890).
- let fqdn = if name.ends_with('.') { name.to_string() } else { format!("{name}.") };
-
- let lookup = resolver
- .lookup(fqdn, RecordType::TXT)
- .await
- .map_err(|e| format!("lookup failed: {e}"))?;
-
- // Require DNSSEC-secure: reject unsigned (insecure) and bogus answers.
- // hickory's `validate` only rejects *bogus* answers on its own — an unsigned
- // zone still yields records proven Insecure — so this check is what makes
- // the lookup fail closed.
- let proofs: Vec<Proof> = lookup.answers().iter().map(|r| r.proof).collect();
- if proofs.is_empty() || !proofs.iter().all(|p| *p == Proof::Secure) {
- return Err(format!(
- "answer is not DNSSEC-secure (records={}, proofs={:?})",
- proofs.len(),
- proofs
- ));
- }
-
- let mut records = Vec::new();
- for record in lookup.answers() {
- if let RData::TXT(txt) = &record.data {
- // A TXT record is one or more character-strings; concatenate them in
- // order, with no separator, before the record is parsed.
- records.push(
- txt.txt_data
- .iter()
- .map(|b| String::from_utf8_lossy(b).into_owned())
- .collect::<String>(),
- );
- }
- }
-
- if records.is_empty() {
- return Err(format!("no TXT records at {name}"));
- }
- Ok(records)
-}
-
-/// Encodes `items` as a JSON array of strings. TXT records are attacker-chosen
-/// bytes, so they are escaped rather than framed with a separator that a record
-/// could contain.
-fn json_string_array(items: &[String]) -> String {
- let mut out = String::from("[");
- for (i, item) in items.iter().enumerate() {
- if i > 0 {
- out.push(',');
- }
- json_escape_into(item, &mut out);
- }
- out.push(']');
- out
-}
-
-fn json_escape_into(s: &str, out: &mut String) {
- out.push('"');
- for c in s.chars() {
- match c {
- '"' => out.push_str("\\\""),
- '\\' => out.push_str("\\\\"),
- '\n' => out.push_str("\\n"),
- '\r' => out.push_str("\\r"),
- '\t' => out.push_str("\\t"),
- c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
- c => out.push(c),
- }
- }
- out.push('"');
-}
-
-unsafe fn cstr(ptr: *const c_char) -> Option<String> {
- if ptr.is_null() {
- return None;
- }
- CStr::from_ptr(ptr).to_str().ok().map(|s| s.to_string())
-}
-
-fn ret_err(msg: impl Into<String>) -> *mut c_char {
- set_last_error(msg);
- std::ptr::null_mut()
-}
-
-#[cfg(test)]
-mod tests {
- use super::json_string_array;
-
- #[test]
- fn encodes_records() {
- let records = vec![
- "oa_version=2; network=xmr; address=888tNk;".to_string(),
- "oa1:xmr recipient_address=888tNk;".to_string(),
- ];
- assert_eq!(
- json_string_array(&records),
- r#"["oa_version=2; network=xmr; address=888tNk;","oa1:xmr recipient_address=888tNk;"]"#
- );
- }
-
- #[test]
- fn escapes_quotes_backslashes_and_controls() {
- let records = vec!["a\"b\\c\nd\te\u{1}f".to_string()];
- assert_eq!(json_string_array(&records), r#"["a\"b\\c\nd\te\u0001f"]"#);
- }
-
- #[test]
- fn encodes_empty_and_unicode() {
- assert_eq!(json_string_array(&[]), "[]");
- assert_eq!(json_string_array(&["münchen".to_string()]), "[\"münchen\"]");
- }
-}
diff --git a/plugins/openalias_ffi/test/openalias_records_test.dart b/plugins/openalias_ffi/test/openalias_records_test.dart
deleted file mode 100644
index b02c66b..0000000
--- a/plugins/openalias_ffi/test/openalias_records_test.dart
+++ /dev/null
@@ -1,510 +0,0 @@
-import 'package:flutter_test/flutter_test.dart';
-import 'package:openalias_ffi/openalias_ffi.dart';
-
-/// The OA2 test vectors from the spec, plus the OA1 records this wallet has to
-/// keep resolving. Everything here is the pure record layer: no DNS, no FFI.
-void main() {
- group('normalizeAlias', () {
- test('turns an email-style alias into an FQDN', () {
- expect(normalizeAlias('donate@openalias.org'), 'donate.openalias.org');
- });
-
- test('passes an FQDN through and drops the root dot', () {
- expect(normalizeAlias('donate.openalias.org'), 'donate.openalias.org');
- expect(normalizeAlias('donate.openalias.org.'), 'donate.openalias.org');
- });
-
- test('trims and lower-cases', () {
- expect(normalizeAlias(' Donate@OpenAlias.ORG '), 'donate.openalias.org');
- });
-
- test('rejects input with no dot as a raw address', () {
- expect(
- () => normalizeAlias('888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjy'),
- throwsA(isA<NotAnAliasException>()),
- );
- expect(() => normalizeAlias('openalias'), throwsA(isA<NotAnAliasException>()));
- expect(() => normalizeAlias(' '), throwsA(isA<NotAnAliasException>()));
- });
-
- test('replaces the @ before deciding whether there is a dot', () {
- // Per the spec the substitution comes first, so this is a (two-label)
- // alias, not a raw address.
- expect(normalizeAlias('donate@openalias'), 'donate.openalias');
- });
-
- test('rejects malformed aliases', () {
- expect(() => normalizeAlias('a@b@openalias.org'), throwsA(isA<OpenAliasException>()));
- expect(() => normalizeAlias('donate..openalias.org'), throwsA(isA<OpenAliasException>()));
- expect(() => normalizeAlias('${'a' * 64}.openalias.org'), throwsA(isA<OpenAliasException>()));
- });
-
- test('leaves an internationalized name for the resolver to punycode', () {
- // The native resolver converts non-ASCII labels to their A-label form;
- // normalization must not mangle them first.
- expect(normalizeAlias('bob@münchen.example'), 'bob.münchen.example');
- });
- });
-
- group('parseKeyValueRecord', () {
- test('parses pairs, tolerating spacing and a trailing semicolon', () {
- final fields = parseKeyValueRecord('oa_version=2; network=btc;address=abc;');
- expect(fields, {'oa_version': '2', 'network': 'btc', 'address': 'abc'});
- });
-
- test('keeps everything after the first equals sign', () {
- final fields = parseKeyValueRecord('oa_version=2; image=https://x.example/i?a=1&b=2;');
- expect(fields!['image'], 'https://x.example/i?a=1&b=2');
- });
-
- test('lower-cases keys but not values', () {
- final fields = parseKeyValueRecord('OA_Version=2; Name=OpenAlias Project;');
- expect(fields, {'oa_version': '2', 'name': 'OpenAlias Project'});
- });
-
- test('rejects a repeated key rather than guessing', () {
- expect(parseKeyValueRecord('oa_version=2; address=a; address=b;'), isNull);
- });
-
- test('rejects records that are not key-value data', () {
- // An early OA2 draft used a bare `oa2 <network>` prefix; it is not valid
- // under the final spec, and must not be half-parsed into a payment.
- expect(parseKeyValueRecord('oa2 xmr address=888tNk;'), isNull);
- expect(parseKeyValueRecord('oa1:xmr recipient_address=888tNk;'), isNull);
- expect(parseKeyValueRecord('just some text'), isNull);
- });
-
- test('parses an unrelated TXT record that happens to be key-value shaped', () {
- // An SPF record is well-formed key-value data; what disqualifies it is the
- // missing oa_version, which the payment/metadata parsers check.
- final fields = parseKeyValueRecord('v=spf1 include:_spf.example.com ~all')!;
- expect(fields, {'v': 'spf1 include:_spf.example.com ~all'});
- expect(parseOa2Payment(fields), isNull);
- });
- });
-
- group('parseOa2Payment', () {
- OpenAliasPayment? parse(String text) {
- final fields = parseKeyValueRecord(text);
- return fields == null ? null : parseOa2Payment(fields);
- }
-
- test('parses the spec example', () {
- final record = parse(
- 'oa_version=2; priority=10; network=btc; '
- 'address=sp1qqfk0ag4gmq87agdy8lawrlt2mf3p8myhkuxgp5s7kdck4ywwg7mjjq; address_type=bip352;',
- )!;
- expect(record.version, 2);
- expect(record.network, 'btc');
- expect(record.asset, isNull);
- expect(record.address, 'sp1qqfk0ag4gmq87agdy8lawrlt2mf3p8myhkuxgp5s7kdck4ywwg7mjjq');
- expect(record.addressType, 'bip352');
- expect(record.priority, 10);
- });
-
- test('ignores unrecognized keys but keeps them', () {
- final record = parse('oa_version=2; network=btc; address=abc; future_field=somevalue;')!;
- expect(record.address, 'abc');
- expect(record.fields['future_field'], 'somevalue');
- });
-
- test('parses a multi-string TXT record once concatenated', () {
- // DNS may split one record into 255-byte character-strings; the native
- // resolver concatenates them in order, with no separator, before parsing.
- const first = 'oa_version=2; network=xmr; address=46BeWrHpwXmHDpDEUmZBWZfoQpdc6HaERCNmx1p';
- const second = 'EYL2rAcuwufPN9rXHHtyUA4QVy66qeFQkn6sfK8aHYjA3jk3o1Bv16em;';
- final record = parse('$first$second')!;
- expect(
- record.address,
- '46BeWrHpwXmHDpDEUmZBWZfoQpdc6HaERCNmx1pEYL2rAcuwufPN9rXHHtyUA4QV'
- 'y66qeFQkn6sfK8aHYjA3jk3o1Bv16em',
- );
- });
-
- test('rejects a record that is not version 2', () {
- expect(parse('oa_version=3; network=xmr; address=abc;'), isNull);
- expect(parse('network=xmr; address=abc;'), isNull);
- });
-
- test('rejects a record missing a required field', () {
- expect(parse('oa_version=2; address=abc;'), isNull);
- expect(parse('oa_version=2; network=xmr;'), isNull);
- expect(parse('oa_version=2; network=xmr; address=;'), isNull);
- });
-
- test('parses amount and memo', () {
- final record = parse('oa_version=2; network=xmr; address=abc; amount=0.01; memo=12345;')!;
- expect(record.amount, '0.01');
- expect(record.memo, '12345');
- });
- });
-
- group('selectPayments', () {
- OpenAliasPayment record(String text) => parseOa2Payment(parseKeyValueRecord(text)!)!;
-
- test('prefers the lower priority number among payable records', () {
- // The spec's two-record example: BTC at priority 10, XMR at priority 20.
- final records = [
- record(
- 'oa_version=2; priority=10; network=btc; '
- 'address=sp1qqfk0ag4gmq87agdy8lawrlt2mf3p8myhkuxgp5s7kdck4ywwg7mjjq;',
- ),
- record('oa_version=2; priority=20; network=xmr; address=46BeWrHpwXmHDpDEUmZBWZfoQ;'),
- record('oa_version=2; priority=5; network=xmr; address=888tNkZrPN6JsEgekjMnABU4TB;'),
- ];
-
- final payable = selectPayments(records, network: 'xmr', asset: 'xmr', nativeAsset: 'xmr');
-
- // The BTC record is filtered out even though it is the highest priority
- // overall: priority applies after filtering to what the sender can pay.
- expect(payable.map((r) => r.address), [
- '888tNkZrPN6JsEgekjMnABU4TB',
- '46BeWrHpwXmHDpDEUmZBWZfoQ',
- ]);
- });
-
- test('sorts records with no priority last, keeping published order', () {
- final records = [
- record('oa_version=2; network=xmr; address=none1;'),
- record('oa_version=2; priority=30; network=xmr; address=p30;'),
- record('oa_version=2; network=xmr; address=none2;'),
- ];
-
- final payable = selectPayments(records, network: 'xmr', asset: 'xmr', nativeAsset: 'xmr');
- expect(payable.map((r) => r.address), ['p30', 'none1', 'none2']);
- });
-
- test('treats an omitted asset and the network native asset as the same', () {
- // On Base the native asset is eth, not base: both of these are native.
- final records = [
- record('oa_version=2; network=base; address=0xA;'),
- record('oa_version=2; network=base; asset=eth; address=0xB;'),
- record('oa_version=2; network=base; asset=usdc; address=0xC;'),
- ];
-
- final payable = selectPayments(records, network: 'base', asset: 'eth', nativeAsset: 'eth');
- expect(payable.map((r) => r.address), ['0xA', '0xB']);
- });
-
- test('matches an explicitly stated native asset for xmr', () {
- final records = [
- record('oa_version=2; network=xmr; asset=xmr; address=explicit;'),
- record('oa_version=2; network=xmr; address=omitted;'),
- ];
- expect(selectPayments(records, network: 'xmr', asset: 'xmr', nativeAsset: 'xmr').length, 2);
- });
-
- test('does not match a token on the network when the native asset is wanted', () {
- final records = [record('oa_version=2; network=eth; asset=usdt; address=0xUSDT;')];
- expect(selectPayments(records, network: 'eth', asset: 'eth', nativeAsset: 'eth'), isEmpty);
- });
-
- test('matches network and asset case-insensitively', () {
- final records = [record('oa_version=2; network=XMR; asset=XMR; address=abc;')];
- expect(
- selectPayments(records, network: 'xmr', asset: 'xmr', nativeAsset: 'xmr').single.address,
- 'abc',
- );
- });
-
- test('does not treat an omitted asset as native when the caller cannot say', () {
- final records = [record('oa_version=2; network=eth; address=0xA;')];
- expect(selectPayments(records, network: 'eth', asset: 'eth'), isEmpty);
- });
- });
-
- group('parseOa1Payment', () {
- test('parses a live-style oa1:xmr record', () {
- const text =
- 'oa1:xmr recipient_address=888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1'
- 'UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H; recipient_name=Monero Development; '
- 'tx_description=Donation to Monero Core Team;';
-
- final record = parseOa1Payment(text, 'xmr')!;
- expect(record.version, 1);
- expect(record.network, 'xmr');
- expect(record.asset, 'xmr');
- expect(
- record.address,
- '888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1'
- 'UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H',
- );
- expect(record.recipientName, 'Monero Development');
- expect(record.description, 'Donation to Monero Core Team');
- });
-
- test('ignores records for other assets', () {
- const text = 'oa1:btc recipient_address=1KTexdemPdxSBcG55heUuTjDRYqbC5ZL8H;';
- expect(parseOa1Payment(text, 'xmr'), isNull);
- expect(parseOa1Payment(text, 'btc')!.address, '1KTexdemPdxSBcG55heUuTjDRYqbC5ZL8H');
- });
-
- test('does not match a prefix that is only a prefix', () {
- expect(parseOa1Payment('oa1:xmrx recipient_address=abc;', 'xmr'), isNull);
- });
-
- test('ignores unrelated TXT records', () {
- expect(parseOa1Payment('v=spf1 include:_spf.example.com ~all', 'xmr'), isNull);
- expect(parseOa1Payment('oa_version=2; network=xmr; address=abc;', 'xmr'), isNull);
- });
-
- test('rejects a record with no recipient address', () {
- expect(parseOa1Payment('oa1:xmr recipient_name=Nobody;', 'xmr'), isNull);
- });
-
- test('parses the optional amount and payment id', () {
- const text = 'oa1:xmr recipient_address=888tNk; tx_amount=1.5; tx_payment_id=deadbeef;';
- final record = parseOa1Payment(text, 'xmr')!;
- expect(record.amount, '1.5');
- expect(record.memo, 'deadbeef');
- });
- });
-
- group('resolveFromLookups', () {
- // Shaped after the live records on privacyguides.magicgrants.org, with
- // distinct addresses per version so it is clear which one was used.
- const oa2Xmr =
- 'oa_version=2; priority=10; network=xmr; asset=xmr; address=882XLsoGHjXipTq8oKF35H2;';
- const oa2Btc = 'oa_version=2; priority=5; network=btc; address=bc1q3hrzx7h45m5jjqu5rlkl6ct;';
- const oa2Metadata = 'oa_version=2; name=MAGIC Privacy Guides Fund;';
- const oa1Xmr =
- 'oa1:xmr recipient_address=888tNkZrPN6JsEgekjMnABU4TB; recipient_name=Older Record;';
-
- OpenAliasResult resolve(OpenAliasLookups lookups) => resolveFromLookups(
- lookups,
- alias: 'privacyguides.magicgrants.org',
- network: 'xmr',
- asset: 'xmr',
- nativeAsset: 'xmr',
- );
-
- test('prefers a v2 record over the v1 record on the same alias', () {
- final result = resolve(
- const OpenAliasLookups(
- paymentRecords: [oa2Xmr],
- metadataRecords: [oa2Metadata],
- oa1Records: [oa1Xmr],
- ),
- );
-
- expect(result.version, 2);
- expect(result.payment.address, '882XLsoGHjXipTq8oKF35H2');
- expect(result.recipientName, 'MAGIC Privacy Guides Fund');
- });
-
- test('falls back to v1 when the alias publishes no v2 records', () {
- final result = resolve(
- const OpenAliasLookups(
- oa1Records: [oa1Xmr],
- paymentProblem: 'lookup failed: no records found',
- ),
- );
-
- expect(result.version, 1);
- expect(result.payment.address, '888tNkZrPN6JsEgekjMnABU4TB');
- expect(result.recipientName, 'Older Record');
- expect(result.metadata, isNull);
- });
-
- test('falls back to v1 when every v2 record is unparseable', () {
- // Some zones still publish the pre-final OA2 draft syntax; it must not
- // strand an alias whose v1 record still works.
- final result = resolve(
- const OpenAliasLookups(
- paymentRecords: ['oa2 xmr address=882XLsoGHjXipTq8oKF35H2;'],
- oa1Records: [oa1Xmr],
- ),
- );
-
- expect(result.version, 1);
- });
-
- test('does not fall back to v1 when v2 records exist but none are payable', () {
- // The v2 records are the recipient's current statement of where to pay
- // them; quietly using the superseded v1 address instead would be wrong.
- expect(
- () => resolve(const OpenAliasLookups(paymentRecords: [oa2Btc], oa1Records: [oa1Xmr])),
- throwsA(isA<OpenAliasException>()),
- );
- });
-
- test('picks the highest-priority payable v2 record and keeps the rest', () {
- final result = resolve(
- const OpenAliasLookups(
- paymentRecords: [
- oa2Btc,
- 'oa_version=2; priority=20; network=xmr; address=lowerPriority;',
- oa2Xmr,
- ],
- ),
- );
-
- expect(result.payment.address, '882XLsoGHjXipTq8oKF35H2'); // priority 10
- expect(result.alternatives.map((r) => r.address), ['lowerPriority']); // priority 20
- });
-
- test('throws and reports both lookups when nothing was published', () {
- expect(
- () => resolve(
- const OpenAliasLookups(paymentProblem: 'timed out', oa1Problem: 'no TXT records'),
- ),
- throwsA(
- isA<OpenAliasException>().having(
- (e) => e.message,
- 'message',
- allOf(contains('timed out'), contains('no TXT records')),
- ),
- ),
- );
- });
-
- test('ignores v1 records for other assets', () {
- expect(
- () => resolve(
- const OpenAliasLookups(oa1Records: ['oa1:btc recipient_address=1KTexdemPdxSBcG55he;']),
- ),
- throwsA(isA<OpenAliasException>()),
- );
- });
- });
-
- group('hostile records', () {
- // Record content is whatever the zone publishes. DNSSEC proves who
- // published it, not that it is sane, so nothing here may throw.
- const nasty = [
- '',
- ';;;;',
- '=',
- '=value',
- 'oa_version=2',
- 'oa_version=2;;;; network=xmr;; address=abc;;;;',
- 'oa_version=2; network=xmr; address=;',
- 'oa_version=2; network=; address=abc;',
- 'oa_version=2; network=xmr; address=abc; priority=notanumber;',
- 'oa_version=2; network=xmr; address=abc; priority=99999999999999999999999;',
- 'oa_version=2; network=xmr; address=abc; priority=-2147483648;',
- 'oa_version=2; network=xmr; address=abc; amount=NaN; memo=;',
- 'oa_version=2; network=xmr; address=abc; = ;',
- 'oa1:xmr',
- 'oa1:xmr ',
- 'oa1:xmr recipient_address=;',
- 'oa1:xmr recipient_address=a; recipient_address=b;',
- 'oa1:xmr=weird',
- '\u0000\u0001\u001f',
- '🙂=🙃; oa_version=2;',
- ];
-
- test('parsing never throws', () {
- for (final text in nasty) {
- expect(
- () {
- final fields = parseKeyValueRecord(text);
- if (fields != null) parseOa2Payment(fields);
- parseOa1Payment(text, 'xmr');
- parseMetadata([text]);
- },
- returnsNormally,
- reason: 'input: $text',
- );
- }
- });
-
- test('resolution never throws anything but OpenAliasException', () {
- for (final text in nasty) {
- expect(
- () => resolveFromLookups(
- OpenAliasLookups(paymentRecords: [text], metadataRecords: [text], oa1Records: [text]),
- alias: 'example.com',
- network: 'xmr',
- asset: 'xmr',
- nativeAsset: 'xmr',
- ),
- anyOf(returnsNormally, throwsA(isA<OpenAliasException>())),
- reason: 'input: $text',
- );
- }
- });
-
- test('an unparseable or absurd priority sorts last instead of throwing', () {
- final records = [
- parseOa2Payment(
- parseKeyValueRecord('oa_version=2; priority=notanumber; network=xmr; address=bad;')!,
- )!,
- parseOa2Payment(
- parseKeyValueRecord('oa_version=2; priority=7; network=xmr; address=good;')!,
- )!,
- ];
- final payable = selectPayments(records, network: 'xmr', asset: 'xmr', nativeAsset: 'xmr');
- expect(payable.map((r) => r.address), ['good', 'bad']);
- });
-
- test('an oversized address is returned verbatim for the caller to reject', () {
- // The plugin does not know what a valid address looks like; the wallet
- // validates before anything downstream sees it.
- final huge = 'A' * 60000;
- final result = resolveFromLookups(
- OpenAliasLookups(paymentRecords: ['oa_version=2; network=xmr; address=$huge;']),
- alias: 'example.com',
- network: 'xmr',
- asset: 'xmr',
- nativeAsset: 'xmr',
- );
- expect(result.payment.address, huge);
- });
-
- test('a record set far larger than a DNS answer still resolves in order', () {
- final many = [
- for (var i = 0; i < 2000; i++)
- 'oa_version=2; priority=${2000 - i}; network=xmr; address=addr$i;',
- ];
- final result = resolveFromLookups(
- OpenAliasLookups(paymentRecords: many),
- alias: 'example.com',
- network: 'xmr',
- asset: 'xmr',
- nativeAsset: 'xmr',
- );
- expect(result.payment.address, 'addr1999'); // priority 1
- expect(result.alternatives, hasLength(1999));
- });
-
- test('a publisher cannot flood the error message with record content', () {
- final flood = [
- for (var i = 0; i < 50; i++) 'oa_version=2; network=${'n' * 500}$i; address=a$i;',
- ];
- expect(
- () => resolveFromLookups(
- OpenAliasLookups(paymentRecords: flood),
- alias: 'example.com',
- network: 'xmr',
- asset: 'xmr',
- nativeAsset: 'xmr',
- ),
- throwsA(
- isA<OpenAliasException>().having(
- (e) => e.message.length,
- 'message length',
- lessThan(200),
- ),
- ),
- );
- });
- });
-
- group('parseMetadata', () {
- test('returns the first version 2 metadata record', () {
- final metadata = parseMetadata([
- 'v=spf1 -all',
- 'oa_version=2; name=OpenAlias Project; image=https://openalias.org/image.png;',
- ])!;
- expect(metadata['name'], 'OpenAlias Project');
- expect(metadata['image'], 'https://openalias.org/image.png');
- });
-
- test('ignores draft-format and non-OpenAlias records', () {
- expect(parseMetadata(['oa2 name=MAGIC Grants; image=https://x.example/i.png']), isNull);
- expect(parseMetadata(['name=No version;']), isNull);
- expect(parseMetadata([]), isNull);
- });
- });
-}
diff --git a/plugins/openalias_ffi/windows/.gitignore b/plugins/openalias_ffi/windows/.gitignore
deleted file mode 100644
index b3eb2be..0000000
--- a/plugins/openalias_ffi/windows/.gitignore
+++ /dev/null
@@ -1,17 +0,0 @@
-flutter/
-
-# Visual Studio user-specific files.
-*.suo
-*.user
-*.userosscache
-*.sln.docstates
-
-# Visual Studio build-related files.
-x64/
-x86/
-
-# Visual Studio cache files
-# files ending in .cache can be ignored
-*.[Cc]ache
-# but keep track of directories ending in .cache
-!*.[Cc]ache/
diff --git a/plugins/openalias_ffi/windows/CMakeLists.txt b/plugins/openalias_ffi/windows/CMakeLists.txt
deleted file mode 100644
index 305d791..0000000
--- a/plugins/openalias_ffi/windows/CMakeLists.txt
+++ /dev/null
@@ -1,24 +0,0 @@
-# The Flutter tooling requires that developers have a version of Visual Studio
-# installed that includes CMake 3.14 or later. You should not increase this
-# version, as doing so will cause the plugin to fail to compile for some
-# customers of the plugin.
-cmake_minimum_required(VERSION 3.14)
-
-# Project-level configuration.
-set(PROJECT_NAME "openalias_ffi")
-project(${PROJECT_NAME} LANGUAGES CXX)
-
-# Invoke the build for native code shared with the other target platforms.
-# This can be changed to accommodate different builds.
-include("../cargokit/cmake/cargokit.cmake")
-apply_cargokit(${PROJECT_NAME} ../rust openalias_ffi "")
-
-# List of absolute paths to libraries that should be bundled with the plugin.
-# This list could contain prebuilt libraries, or libraries created by an
-# external build triggered from this build file.
-set(openalias_ffi_bundled_libraries
- # Defined in ../src/CMakeLists.txt.
- # This can be changed to accommodate different builds.
- "${${PROJECT_NAME}_cargokit_lib}"
- PARENT_SCOPE
-)
diff --git a/pubspec.lock b/pubspec.lock
index 51cc224..697fec1 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -36,10 +36,11 @@ packages:
bip39:
dependency: "direct main"
description:
- name: bip39
- sha256: de1ee27ebe7d96b84bb3a04a4132a0a3007dcdd5ad27dd14aa87a29d97c45edc
- url: "https://pub.dev"
- source: hosted
+ path: "."
+ ref: "0cd6d54e2860bea68fc50c801cb9db2a760192fb"
+ resolved-ref: "0cd6d54e2860bea68fc50c801cb9db2a760192fb"
+ url: "https://github.com/cypherstack/stack-bip39"
+ source: git
version: "1.0.6"
boolean_selector:
dependency: transitive
@@ -197,10 +198,10 @@ packages:
dependency: transitive
description:
name: ffi
- sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
+ sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
url: "https://pub.dev"
source: hosted
- version: "2.2.0"
+ version: "2.1.4"
file:
dependency: transitive
description:
@@ -678,13 +679,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.0"
- openalias_ffi:
- dependency: "direct main"
- description:
- path: "plugins/openalias_ffi"
- relative: true
- source: path
- version: "0.0.1"
package_info_plus:
dependency: "direct main"
description:
@@ -1126,7 +1120,7 @@ packages:
dependency: "direct main"
description:
path: "."
- ref: main
+ ref: c2706eeb66174273a4a98113d7a8cd910ba124e6
resolved-ref: c2706eeb66174273a4a98113d7a8cd910ba124e6
url: "https://github.com/cypherstack/tor"
source: git
@@ -1235,6 +1229,34 @@ packages:
url: "https://pub.dev"
source: hosted
version: "15.0.2"
+ wallet_domain:
+ dependency: "direct main"
+ description:
+ path: "../wallet-core/packages/wallet_domain"
+ relative: true
+ source: path
+ version: "0.1.0"
+ wallet_infra:
+ dependency: "direct main"
+ description:
+ path: "../wallet-core/packages/wallet_infra"
+ relative: true
+ source: path
+ version: "0.1.0"
+ wallet_monero:
+ dependency: "direct main"
+ description:
+ path: "../wallet-core/packages/wallet_monero"
+ relative: true
+ source: path
+ version: "0.1.0"
+ wallet_openalias:
+ dependency: "direct main"
+ description:
+ path: "../wallet-core/packages/wallet_openalias"
+ relative: true
+ source: path
+ version: "0.1.0"
web:
dependency: transitive
description:
@@ -1243,6 +1265,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.1"
+ webcrypto:
+ dependency: transitive
+ description:
+ name: webcrypto
+ sha256: "6b43001c4110856ff7fa5e5e65e7b2d44bec1d8b54a4d84d5fa2c7622267c5c1"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.6.0"
win32:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index 7de4792..d0533f7 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -59,8 +59,30 @@ dependencies:
url: https://github.com/magicgrants/monero_c
ref: 757bf8ef3874953a076b07a2407b17bf9eeef5c0
path: impls/monero.dart
- openalias_ffi:
- path: plugins/openalias_ffi
+
+ # wallet-core packages; SHA-pinned for release, redirected to the local sibling
+ # clone by the gitignored pubspec_overrides.yaml for dev.
+ wallet_infra:
+ git:
+ url: https://github.com/MAGICGrants/wallet-core
+ ref: 6bbf5059257f18e65dc7fcdf089e355181a2d578
+ path: packages/wallet_infra
+ wallet_domain:
+ git:
+ url: https://github.com/MAGICGrants/wallet-core
+ ref: 6bbf5059257f18e65dc7fcdf089e355181a2d578
+ path: packages/wallet_domain
+ wallet_monero:
+ git:
+ url: https://github.com/MAGICGrants/wallet-core
+ ref: 6bbf5059257f18e65dc7fcdf089e355181a2d578
+ path: packages/wallet_monero
+ wallet_openalias:
+ git:
+ url: https://github.com/MAGICGrants/wallet-core
+ ref: 6bbf5059257f18e65dc7fcdf089e355181a2d578
+ path: packages/wallet_openalias
+
cupertino_icons: 1.0.8
path: 1.9.1
provider: 6.1.5+1
diff --git a/scripts/repro/fdroid-repro-check.sh b/scripts/repro/fdroid-repro-check.sh
index b5c016f..955e04e 100755
--- a/scripts/repro/fdroid-repro-check.sh
+++ b/scripts/repro/fdroid-repro-check.sh
@@ -28,7 +28,8 @@ VC="${1:-4022}"
APPID="org.magicgrants.skylight"
IMAGE="registry.gitlab.com/fdroid/fdroidserver:buildserver-trixie"
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
-FDD="$ROOT/fdroiddata"
+# fdroiddata is a sibling clone (../fdroiddata); override with $FDROIDDATA.
+FDD="${FDROIDDATA:-$(cd "$ROOT/.." && pwd)/fdroiddata}"
RECIPE="$FDD/metadata/$APPID.yml"
OUT="$ROOT/repro-out/fdroid-$VC"
RUNS="${RUNS:-2}"
diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc
index 3cba7be..f60a4fe 100644
--- a/windows/flutter/generated_plugin_registrant.cc
+++ b/windows/flutter/generated_plugin_registrant.cc
@@ -13,6 +13,7 @@
#include <screen_brightness_windows/screen_brightness_windows_plugin.h>
#include <share_plus/share_plus_windows_plugin_c_api.h>
#include <url_launcher_windows/url_launcher_windows.h>
+#include <webcrypto/webcrypto_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
FileSelectorWindowsRegisterWithRegistrar(
@@ -29,4 +30,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
+ WebcryptoPluginRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("WebcryptoPlugin"));
}
diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake
index 76df8ed..284fac1 100644
--- a/windows/flutter/generated_plugins.cmake
+++ b/windows/flutter/generated_plugins.cmake
@@ -10,13 +10,14 @@ list(APPEND FLUTTER_PLUGIN_LIST
screen_brightness_windows
share_plus
url_launcher_windows
+ webcrypto
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
flutter_local_notifications_windows
flutter_zxing
- openalias_ffi
tor_ffi_plugin
+ wallet_openalias
)
set(PLUGIN_BUNDLED_LIBRARIES)
Why this scored 10/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.