fix salvium interface and add salvium staking tx call
What changed, and why it matters
This commit updates the Salvium cryptocurrency wallet integration in Stack Wallet. It mainly changes how wallet objects are passed around internally (using a new WrappedWallet object instead of a wallet ID string) and adds support for creating Salvium staking transactions. There is no clear security fix or vulnerability being patched in the visible code changes.
Review the new WrappedWallet lifecycle to ensure wallet objects are properly closed and not leaked; verify that createStakeTx validation is sufficient and that the staking transaction path cannot be abused to send funds to arbitrary addresses.
Security signals we found
Refactor of wallet handle management from string IDs to WrappedWallet objects
Addition of staking transaction creation path with basic input validation
Removal of internal wallet instance map in generated interface implementation
Evidence from the diff
The commit refactors the Salvium interface layer to pass WrappedWallet references rather than walletId strings to the underlying lib_salvium FFI calls. It also adds a new createStakeTx method, a salviumStakeTx flag in TxData, and validation that stake transactions use the wallet’s own address with a single output. The change removes the internal Map
Changed components
lib/wallets/models/tx_data.dartlib/wallets/wallet/impl/salvium_wallet.dartlib/wallets/wallet/intermediate/lib_salvium_wallet.dartlib/wl_gen/interfaces/cs_monero_interface.dartlib/wl_gen/interfaces/cs_salvium_interface.darttool/wl_templates/SAL_cs_salvium_interface_impl.template.dartInspect captured patch +339 / −226
diff --git a/lib/wallets/models/tx_data.dart b/lib/wallets/models/tx_data.dart
index 68ba7b7..5db6d94 100644
--- a/lib/wallets/models/tx_data.dart
+++ b/lib/wallets/models/tx_data.dart
@@ -100,6 +100,8 @@ class TxData {
final TxType type;
+ final bool salviumStakeTx;
+
TxData({
this.feeRateType,
this.feeRateAmount,
@@ -135,6 +137,7 @@ class TxData {
this.opNameState,
this.sparkNameInfo,
this.type = TxType.regular,
+ this.salviumStakeTx = false,
});
Amount? get amount {
diff --git a/lib/wallets/wallet/impl/salvium_wallet.dart b/lib/wallets/wallet/impl/salvium_wallet.dart
index 94aef46..c345fb0 100644
--- a/lib/wallets/wallet/impl/salvium_wallet.dart
+++ b/lib/wallets/wallet/impl/salvium_wallet.dart
@@ -1,7 +1,7 @@
import 'dart:async';
import '../../../utilities/amount/amount.dart';
-import '../../../wl_gen/generated/cs_salvium_interface_impl.dart';
+import '../../../wl_gen/interfaces/cs_salvium_interface.dart';
import '../../crypto_currency/crypto_currency.dart';
import '../intermediate/lib_salvium_wallet.dart';
@@ -10,7 +10,8 @@ class SalviumWallet extends LibSalviumWallet {
@override
Future<Amount> estimateFeeFor(Amount amount, BigInt feeRate) async {
- if (!csSalvium.walletInstanceExists(walletId) /*||
+ if (wallet ==
+ null /*||
syncStatus is! lib_monero_compat.SyncedSyncStatus*/ ) {
return Amount.zeroWith(fractionDigits: cryptoCurrency.fractionDigits);
}
@@ -20,7 +21,7 @@ class SalviumWallet extends LibSalviumWallet {
approximateFee = await csSalvium.estimateFee(
feeRate.toInt(),
amount.raw,
- walletId: walletId,
+ wallet: wallet!,
);
});
@@ -34,27 +35,26 @@ class SalviumWallet extends LibSalviumWallet {
bool walletExists(String path) => csSalvium.walletExists(path);
@override
- Future<void> loadWallet({required String path, required String password}) =>
- csSalvium.loadWallet(walletId, path: path, password: password);
+ Future<WrappedWallet> loadWallet({
+ required String path,
+ required String password,
+ }) => csSalvium.loadWallet(walletId, path: path, password: password);
@override
- Future<void> getCreatedWallet({
+ Future<WrappedWallet> getCreatedWallet({
required String path,
required String password,
required int wordCount,
required String seedOffset,
- required final void Function(int refreshFromBlockHeight, String seed)
- onCreated,
}) => csSalvium.getCreatedWallet(
path: path,
password: password,
wordCount: wordCount,
seedOffset: seedOffset,
- onCreated: onCreated,
);
@override
- Future<void> getRestoredWallet({
+ Future<WrappedWallet> getRestoredWallet({
required String path,
required String password,
required String mnemonic,
@@ -70,7 +70,7 @@ class SalviumWallet extends LibSalviumWallet {
);
@override
- Future<void> getRestoredFromViewKeyWallet({
+ Future<WrappedWallet> getRestoredFromViewKeyWallet({
required String path,
required String password,
required String address,
diff --git a/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart b/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart
index 4aa0598..c3da088 100644
--- a/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart
+++ b/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart
@@ -49,6 +49,8 @@ abstract class LibSalviumWallet<T extends CryptonoteCurrency>
@override
int get isarTransactionVersion => 2;
+ WrappedWallet? wallet;
+
LibSalviumWallet(super.currency) {
final bus = GlobalEventBus.instance;
@@ -132,18 +134,19 @@ abstract class LibSalviumWallet<T extends CryptonoteCurrency>
int currentKnownChainHeight = 0;
double highestPercentCached = 0;
- Future<void> loadWallet({required String path, required String password});
+ Future<WrappedWallet> loadWallet({
+ required String path,
+ required String password,
+ });
- Future<void> getCreatedWallet({
+ Future<WrappedWallet> getCreatedWallet({
required String path,
required String password,
required int wordCount,
required String seedOffset,
- required final void Function(int refreshFromBlockHeight, String seed)
- onCreated,
});
- Future<void> getRestoredWallet({
+ Future<WrappedWallet> getRestoredWallet({
required String path,
required String password,
required String mnemonic,
@@ -151,7 +154,7 @@ abstract class LibSalviumWallet<T extends CryptonoteCurrency>
int height = 0,
});
- Future<void> getRestoredFromViewKeyWallet({
+ Future<WrappedWallet> getRestoredFromViewKeyWallet({
required String path,
required String password,
required String address,
@@ -164,17 +167,16 @@ abstract class LibSalviumWallet<T extends CryptonoteCurrency>
bool walletExists(String path);
String getTxKeyFor({required String txid}) {
- if (!csSalvium.walletInstanceExists(walletId)) {
+ if (wallet == null) {
throw Exception("Cannot get tx key in uninitialized libSalviumWallet");
}
- return csSalvium.getTxKey(walletId, txid);
+ return csSalvium.getTxKey(wallet!, txid);
}
void _setListener() {
- if (csSalvium.walletInstanceExists(walletId) &&
- !csSalvium.hasListeners(walletId)) {
+ if (wallet != null && !csSalvium.hasListeners(wallet!)) {
csSalvium.addListener(
- walletId,
+ wallet!,
CsWalletListener(
onSyncingUpdate: onSyncingUpdate,
onNewBlock: onNewBlock,
@@ -191,7 +193,7 @@ abstract class LibSalviumWallet<T extends CryptonoteCurrency>
Future<void> open() async {
bool wasNull = false;
- if (!csSalvium.walletInstanceExists(walletId)) {
+ if (wallet == null) {
wasNull = true;
// await libSalviumWallet?.close();
final path = await pathForWallet(name: walletId);
@@ -205,7 +207,7 @@ abstract class LibSalviumWallet<T extends CryptonoteCurrency>
throw Exception("Password not found $e, $s");
}
- await loadWallet(path: path, password: password);
+ wallet = await loadWallet(path: path, password: password);
_setListener();
@@ -227,15 +229,15 @@ abstract class LibSalviumWallet<T extends CryptonoteCurrency>
if (wasNull) {
try {
_setSyncStatus(ConnectingSyncStatus());
- csSalvium.startSyncing(walletId);
+ csSalvium.startSyncing(wallet!);
} catch (_) {
_setSyncStatus(FailedSyncStatus());
// TODO log
}
}
_setListener();
- csSalvium.startListeners(walletId);
- csSalvium.startAutoSaving(walletId);
+ csSalvium.startListeners(wallet!);
+ csSalvium.startAutoSaving(wallet!);
unawaited(refresh());
}
@@ -245,12 +247,12 @@ abstract class LibSalviumWallet<T extends CryptonoteCurrency>
final appRoot = await StackFileSystem.applicationRootDirectory();
await _backupWalletFiles(name: walletId, appRoot: appRoot);
}
- await csSalvium.save(walletId);
+ await csSalvium.save(wallet!);
}
Address addressFor({required int index, int account = 0}) {
final address = csSalvium.getAddress(
- walletId,
+ wallet!,
accountIndex: account,
addressIndex: index,
);
@@ -273,16 +275,16 @@ abstract class LibSalviumWallet<T extends CryptonoteCurrency>
}
Future<CWKeyData?> getKeys() async {
- if (!csSalvium.walletInstanceExists(walletId)) {
+ if (wallet == null) {
return null;
}
try {
return CWKeyData(
walletId: walletId,
- publicViewKey: csSalvium.getPublicViewKey(walletId),
- privateViewKey: csSalvium.getPrivateViewKey(walletId),
- publicSpendKey: csSalvium.getPublicSpendKey(walletId),
- privateSpendKey: csSalvium.getPrivateSpendKey(walletId),
+ publicViewKey: csSalvium.getPublicViewKey(wallet!),
+ privateViewKey: csSalvium.getPrivateViewKey(wallet!),
+ publicSpendKey: csSalvium.getPublicSpendKey(wallet!),
+ privateSpendKey: csSalvium.getPrivateSpendKey(wallet!),
);
} catch (e, s) {
Logging.instance.f("getKeys failed: ", error: e, stackTrace: s);
@@ -307,10 +309,10 @@ abstract class LibSalviumWallet<T extends CryptonoteCurrency>
} catch (e, s) {
throw Exception("Password not found $e, $s");
}
- await loadWallet(path: path, password: password);
+ wallet = await loadWallet(path: path, password: password);
return (
- csSalvium.getAddress(walletId),
- csSalvium.getPrivateViewKey(walletId),
+ csSalvium.getAddress(wallet!),
+ csSalvium.getPrivateViewKey(wallet!),
);
}
@@ -326,22 +328,15 @@ abstract class LibSalviumWallet<T extends CryptonoteCurrency>
value: password,
);
- late final int refreshFromBlockHeight;
- late final String seedPhrase;
-
- await getCreatedWallet(
+ final wallet = await getCreatedWallet(
path: path,
password: password,
wordCount: wordCount,
seedOffset: "", // default for non restored wallets for now
- onCreated: (height, seed) {
- refreshFromBlockHeight = height;
- seedPhrase = seed;
- },
);
await info.updateRestoreHeight(
- newRestoreHeight: refreshFromBlockHeight,
+ newRestoreHeight: csSalvium.getRefreshFromBlockHeight(wallet),
isar: mainDB.isar,
);
@@ -349,7 +344,7 @@ abstract class LibSalviumWallet<T extends CryptonoteCurrency>
// before wallet.init() is called
await secureStorageInterface.write(
key: Wallet.mnemonicKey(walletId: walletId),
- value: seedPhrase,
+ value: csSalvium.getSeed(wallet),
);
await secureStorageInterface.write(
key: Wallet.mnemonicPassphraseKey(walletId: walletId),
@@ -372,8 +367,8 @@ abstract class LibSalviumWallet<T extends CryptonoteCurrency>
await mainDB.deleteWalletBlockchainData(walletId);
highestPercentCached = 0;
- unawaited(csSalvium.rescanBlockchain(walletId));
- csSalvium.startSyncing(walletId);
+ unawaited(csSalvium.rescanBlockchain(wallet!));
+ csSalvium.startSyncing(wallet!);
// unawaited(save());
});
unawaited(refresh());
@@ -411,11 +406,7 @@ abstract class LibSalviumWallet<T extends CryptonoteCurrency>
value: password,
);
- if (!csSalvium.walletInstanceExists(walletId)) {
- await exit();
- }
-
- await getRestoredWallet(
+ final wallet = await getRestoredWallet(
path: path,
password: password,
mnemonic: mnemonic,
@@ -423,6 +414,12 @@ abstract class LibSalviumWallet<T extends CryptonoteCurrency>
seedOffset: seedOffset,
);
+ if (this.wallet != null) {
+ await exit();
+ }
+
+ this.wallet = wallet;
+
_setListener();
final newReceivingAddress =
@@ -431,7 +428,7 @@ abstract class LibSalviumWallet<T extends CryptonoteCurrency>
walletId: walletId,
derivationIndex: 0,
derivationPath: null,
- value: csSalvium.getAddress(walletId),
+ value: csSalvium.getAddress(this.wallet!),
publicKey: [],
type: AddressType.cryptonote,
subType: AddressSubType.receiving,
@@ -450,12 +447,12 @@ abstract class LibSalviumWallet<T extends CryptonoteCurrency>
_setListener();
// libSalviumWallet?.setRecoveringFromSeed(isRecovery: true);
- unawaited(csSalvium.rescanBlockchain(walletId));
- csSalvium.startSyncing(walletId);
+ unawaited(csSalvium.rescanBlockchain(wallet!));
+ csSalvium.startSyncing(wallet!);
// await save();
- csSalvium.startListeners(walletId);
- csSalvium.startAutoSaving(walletId);
+ csSalvium.startListeners(wallet!);
+ csSalvium.startAutoSaving(wallet!);
} catch (e, s) {
Logging.instance.e(
"Exception rethrown from recoverFromMnemonic(): ",
@@ -472,7 +469,7 @@ abstract class LibSalviumWallet<T extends CryptonoteCurrency>
@override
Future<bool> pingCheck() async {
if (_canPing) {
- return csSalvium.isConnectedToDaemon(walletId);
+ return csSalvium.isConnectedToDaemon(wallet!);
} else {
return false;
}
@@ -499,7 +496,7 @@ abstract class LibSalviumWallet<T extends CryptonoteCurrency>
if (_requireMutex) {
await _torConnectingLock.protect(() async {
await csSalvium.connect(
- walletId,
+ wallet!,
daemonAddress: "$host:${node.port}",
daemonUsername: node.loginName,
daemonPassword: await node.getPassword(secureStorageInterface),
@@ -514,7 +511,7 @@ abstract class LibSalviumWallet<T extends CryptonoteCurrency>
});
} else {
await csSalvium.connect(
- walletId,
+ wallet!,
daemonAddress: "$host:${node.port}",
daemonUsername: node.loginName,
daemonPassword: await node.getPassword(secureStorageInterface),
@@ -527,9 +524,9 @@ abstract class LibSalviumWallet<T extends CryptonoteCurrency>
: "${proxy.host.address}:${proxy.port}",
);
}
- csSalvium.startSyncing(walletId);
- csSalvium.startListeners(walletId);
- csSalvium.startAutoSaving(walletId);
+ csSalvium.startSyncing(wallet!);
+ csSalvium.startListeners(wallet!);
+ csSalvium.startAutoSaving(wallet!);
// _setSyncStatus(ConnectedSyncStatus());
} catch (e, s) {
@@ -546,7 +543,7 @@ abstract class LibSalviumWallet<T extends CryptonoteCurrency>
@override
Future<void> updateTransactions() async {
- if (!csSalvium.walletInstanceExists(walletId)) {
+ if (wallet == null) {
return;
}
@@ -558,7 +555,7 @@ abstract class LibSalviumWallet<T extends CryptonoteCurrency>
.txidProperty()
.findAll();
- final allTxids = await csSalvium.getAllTxids(walletId, refresh: true);
+ final allTxids = await csSalvium.getAllTxids(wallet!, refresh: true);
final txidsToFetch = allTxids.toSet().difference(localTxids.toSet());
@@ -567,13 +564,13 @@ abstract class LibSalviumWallet<T extends CryptonoteCurrency>
}
final transactions = await csSalvium.getTxs(
- walletId,
+ wallet!,
txids: txidsToFetch,
refresh: false,
);
final allOutputs = await csSalvium.getOutputs(
- walletId,
+ wallet!,
includeSpent: true,
refresh: true,
);
@@ -684,7 +681,7 @@ abstract class LibSalviumWallet<T extends CryptonoteCurrency>
Future<Amount> get availableBalance async {
try {
return Amount(
- rawValue: csSalvium.getUnlockedBalance(walletId)!,
+ rawValue: csSalvium.getUnlockedBalance(wallet!)!,
fractionDigits: cryptoCurrency.fractionDigits,
);
} catch (_) {
@@ -694,14 +691,14 @@ abstract class LibSalviumWallet<T extends CryptonoteCurrency>
Future<Amount> get totalBalance async {
try {
- final full = csSalvium.getBalance(walletId);
+ final full = csSalvium.getBalance(wallet!);
if (full != null) {
return Amount(
rawValue: full,
fractionDigits: cryptoCurrency.fractionDigits,
);
} else {
- final transactions = await csSalvium.getAllTxs(walletId, refresh: true);
+ final transactions = await csSalvium.getAllTxs(wallet!, refresh: true);
BigInt transactionBalance = BigInt.zero;
for (final tx in transactions) {
if (!tx.isSpend) {
@@ -724,10 +721,12 @@ abstract class LibSalviumWallet<T extends CryptonoteCurrency>
@override
Future<void> exit() async {
Logging.instance.i("exit called on $walletId");
- csSalvium.stopAutoSaving(walletId);
- csSalvium.stopListeners(walletId);
- csSalvium.stopSyncing(walletId);
- await csSalvium.save(walletId);
+ if (wallet != null) {
+ csSalvium.stopAutoSaving(wallet!);
+ csSalvium.stopListeners(wallet!);
+ csSalvium.stopSyncing(wallet!);
+ await csSalvium.save(wallet!);
+ }
}
Future<String> pathForWalletDir({required String name}) async {
@@ -797,7 +796,7 @@ abstract class LibSalviumWallet<T extends CryptonoteCurrency>
final _utxosUpdateLock = Mutex();
Future<void> onUTXOsChanged(List<UTXO> utxos) async {
await _utxosUpdateLock.protect(() async {
- final cwUtxos = await csSalvium.getOutputs(walletId, refresh: true);
+ final cwUtxos = await csSalvium.getOutputs(wallet!, refresh: true);
// bool changed = false;
@@ -814,12 +813,12 @@ abstract class LibSalviumWallet<T extends CryptonoteCurrency>
if (u.isBlocked) {
if (!cw.isFrozen) {
- await csSalvium.freezeOutput(walletId, cw.keyImage);
+ await csSalvium.freezeOutput(wallet!, cw.keyImage);
// changed = true;
}
} else {
if (cw.isFrozen) {
- await csSalvium.thawOutput(walletId, cw.keyImage);
+ await csSalvium.thawOutput(wallet!, cw.keyImage);
// changed = true;
}
}
@@ -975,9 +974,11 @@ abstract class LibSalviumWallet<T extends CryptonoteCurrency>
if (mismatch) {
_canPing = false;
- csSalvium.stopAutoSaving(walletId);
- csSalvium.stopListeners(walletId);
- csSalvium.stopSyncing(walletId);
+ if (wallet != null) {
+ csSalvium.stopAutoSaving(wallet!);
+ csSalvium.stopListeners(wallet!);
+ csSalvium.stopSyncing(wallet!);
+ }
_setSyncStatus(FailedSyncStatus());
}
@@ -995,7 +996,9 @@ abstract class LibSalviumWallet<T extends CryptonoteCurrency>
@override
Future<bool> updateUTXOs() async {
final List<UTXO> outputArray = [];
- final utxos = await csSalvium.getOutputs(walletId, refresh: true);
+ final utxos = wallet == null
+ ? <CsOutput>[]
+ : await csSalvium.getOutputs(wallet!, refresh: true);
for (final rawUTXO in utxos) {
if (!rawUTXO.spent) {
final current = await mainDB.isar.utxos
@@ -1084,7 +1087,7 @@ abstract class LibSalviumWallet<T extends CryptonoteCurrency>
// Slight possibility of race but should be irrelevant
await refreshMutex.acquire();
- csSalvium.startSyncing(walletId);
+ csSalvium.startSyncing(wallet!);
_setSyncStatus(StartingSyncStatus());
await updateTransactions();
@@ -1098,7 +1101,7 @@ abstract class LibSalviumWallet<T extends CryptonoteCurrency>
refreshMutex.release();
}
- final synced = await csSalvium.isSynced(walletId);
+ final synced = wallet != null && await csSalvium.isSynced(wallet!);
if (synced == true) {
_setSyncStatus(SyncedSyncStatus());
@@ -1147,7 +1150,7 @@ abstract class LibSalviumWallet<T extends CryptonoteCurrency>
try {
int highestIndex = -1;
- final entries = await csSalvium.getAllTxs(walletId, refresh: true);
+ final entries = await csSalvium.getAllTxs(wallet!, refresh: true);
for (final element in entries) {
if (!element.isSpend) {
@@ -1273,6 +1276,20 @@ abstract class LibSalviumWallet<T extends CryptonoteCurrency>
throw Exception("Send all not supported with multiple recipients");
}
+ if (txData.salviumStakeTx) {
+ if (sweep) {
+ throw Exception("Send all not supported for stake transactions");
+ }
+ if (txData.recipients!.length != 1) {
+ throw Exception("Stake transactions require one output only");
+ }
+
+ final mainAddress = csSalvium.getAddress(wallet!);
+ if (txData.recipients!.first.address != mainAddress) {
+ throw Exception("Stake transactions should use your own address");
+ }
+ }
+
final List<CsRecipient> outputs = [];
for (final recipient in txData.recipients!) {
final output = CsRecipient(recipient.address, recipient.amount.raw);
@@ -1289,9 +1306,19 @@ abstract class LibSalviumWallet<T extends CryptonoteCurrency>
return await prepareSendMutex.protect(() async {
final CsPendingTransaction pendingTransaction;
- if (outputs.length == 1) {
+
+ if (txData.salviumStakeTx) {
+ pendingTransaction = await csSalvium.createStakeTx(
+ wallet!,
+ output: outputs.first,
+ priority: feePriority,
+ accountIndex: 0, // sw only uses account 0 at this time
+ minConfirms: cryptoCurrency.minConfirms,
+ currentHeight: height,
+ );
+ } else if (outputs.length == 1) {
pendingTransaction = await csSalvium.createTx(
- walletId,
+ wallet!,
output: outputs.first,
minConfirms: cryptoCurrency.minConfirms,
currentHeight: height,
@@ -1302,7 +1329,7 @@ abstract class LibSalviumWallet<T extends CryptonoteCurrency>
);
} else {
pendingTransaction = await csSalvium.createTxMultiDest(
- walletId,
+ wallet!,
outputs: outputs,
minConfirms: cryptoCurrency.minConfirms,
currentHeight: height,
@@ -1348,7 +1375,7 @@ abstract class LibSalviumWallet<T extends CryptonoteCurrency>
Future<TxData> confirmSend({required TxData txData}) async {
try {
try {
- await csSalvium.commitTx(walletId, txData.pendingSalviumTransaction!);
+ await csSalvium.commitTx(wallet!, txData.pendingSalviumTransaction!);
Logging.instance.d(
"transaction ${txData.pendingSalviumTransaction!.txid} has been sent",
@@ -1398,11 +1425,7 @@ abstract class LibSalviumWallet<T extends CryptonoteCurrency>
value: password,
);
- if (csSalvium.walletInstanceExists(walletId)) {
- await exit();
- }
-
- await getRestoredFromViewKeyWallet(
+ final wallet = await getRestoredFromViewKeyWallet(
path: path,
password: password,
address: data.address,
@@ -1410,6 +1433,12 @@ abstract class LibSalviumWallet<T extends CryptonoteCurrency>
height: height,
);
+ if (this.wallet != null) {
+ await exit();
+ }
+
+ this.wallet = wallet;
+
_setListener();
final newReceivingAddress =
@@ -1418,7 +1447,7 @@ abstract class LibSalviumWallet<T extends CryptonoteCurrency>
walletId: walletId,
derivationIndex: 0,
derivationPath: null,
- value: csSalvium.getAddress(walletId),
+ value: csSalvium.getAddress(this.wallet!),
publicKey: [],
type: AddressType.cryptonote,
subType: AddressSubType.receiving,
@@ -1433,12 +1462,12 @@ abstract class LibSalviumWallet<T extends CryptonoteCurrency>
await updateNode();
_setListener();
- unawaited(csSalvium.rescanBlockchain(walletId));
- csSalvium.startSyncing(walletId);
+ unawaited(csSalvium.rescanBlockchain(this.wallet!));
+ csSalvium.startSyncing(this.wallet!);
// await save();
- csSalvium.startListeners(walletId);
- csSalvium.startAutoSaving(walletId);
+ csSalvium.startListeners(this.wallet!);
+ csSalvium.startAutoSaving(this.wallet!);
} catch (e, s) {
Logging.instance.e(
"Exception rethrown from recoverViewOnly(): ",
diff --git a/lib/wl_gen/interfaces/cs_monero_interface.dart b/lib/wl_gen/interfaces/cs_monero_interface.dart
index 9ed2bb4..4589a71 100644
--- a/lib/wl_gen/interfaces/cs_monero_interface.dart
+++ b/lib/wl_gen/interfaces/cs_monero_interface.dart
@@ -221,7 +221,7 @@ final class CsPendingTransaction {
// forwarding class
final class CsTransaction {
- CsTransaction({
+ CsTransaction( {
required this.displayLabel,
required this.description,
required this.fee,
@@ -236,6 +236,7 @@ final class CsTransaction {
required this.key,
required this.timeStamp,
required this.minConfirms,
+ this.salviumData,
}) {
if (fee.isNegative) throw Exception("negative fee");
if (confirmations.isNegative) throw Exception("negative confirmations");
@@ -290,6 +291,9 @@ final class CsTransaction {
/// Flag indicating whether the transaction is pending (i.e., not yet confirmed).
bool get isPending => confirmations < minConfirms;
+
+ final ({int type, String asset})? salviumData;
+
}
// forwarding class
diff --git a/lib/wl_gen/interfaces/cs_salvium_interface.dart b/lib/wl_gen/interfaces/cs_salvium_interface.dart
index 108ff16..0b7a509 100644
--- a/lib/wl_gen/interfaces/cs_salvium_interface.dart
+++ b/lib/wl_gen/interfaces/cs_salvium_interface.dart
@@ -11,37 +11,35 @@ abstract class CsSalviumInterface {
int getTxPriorityMedium();
int getTxPriorityNormal();
- bool walletInstanceExists(String walletId);
-
bool walletExists(String path);
- Future<int> estimateFee(int rate, BigInt amount, {required String walletId});
+ Future<int> estimateFee(
+ int rate,
+ BigInt amount, {
+ required WrappedWallet wallet,
+ });
- Future<void> loadWallet(
+ Future<WrappedWallet> loadWallet(
String walletId, {
-
required String path,
required String password,
});
String getAddress(
- String walletId, {
+ WrappedWallet wallet, {
int accountIndex = 0,
int addressIndex = 0,
});
- Future<void> getCreatedWallet({
+ Future<WrappedWallet> getCreatedWallet({
required String path,
required String password,
required int wordCount,
required String seedOffset,
- required final void Function(int refreshFromBlockHeight, String seed)
- onCreated,
});
- Future<void> getRestoredWallet({
+ Future<WrappedWallet> getRestoredWallet({
required String walletId,
-
required String path,
required String password,
required String mnemonic,
@@ -49,9 +47,8 @@ abstract class CsSalviumInterface {
int height = 0,
});
- Future<void> getRestoredFromViewKeyWallet({
+ Future<WrappedWallet> getRestoredFromViewKeyWallet({
required String walletId,
-
required String path,
required String password,
required String address,
@@ -59,35 +56,35 @@ abstract class CsSalviumInterface {
int height = 0,
});
- String getTxKey(String walletId, String txid);
+ String getTxKey(WrappedWallet wallet, String txid);
- Future<void> save(String walletId);
+ Future<void> save(WrappedWallet wallet);
- String getPublicViewKey(String walletId);
- String getPrivateViewKey(String walletId);
- String getPublicSpendKey(String walletId);
- String getPrivateSpendKey(String walletId);
+ String getPublicViewKey(WrappedWallet wallet);
+ String getPrivateViewKey(WrappedWallet wallet);
+ String getPublicSpendKey(WrappedWallet wallet);
+ String getPrivateSpendKey(WrappedWallet wallet);
- Future<bool> isSynced(String walletId);
- void startSyncing(String walletId);
- void stopSyncing(String walletId);
+ Future<bool> isSynced(WrappedWallet wallet);
+ void startSyncing(WrappedWallet wallet);
+ void stopSyncing(WrappedWallet wallet);
- void startAutoSaving(String walletId);
- void stopAutoSaving(String walletId);
+ void startAutoSaving(WrappedWallet wallet);
+ void stopAutoSaving(WrappedWallet wallet);
- bool hasListeners(String walletId);
- void addListener(String walletId, CsWalletListener listener);
- void startListeners(String walletId);
- void stopListeners(String walletId);
+ bool hasListeners(WrappedWallet wallet);
+ void addListener(WrappedWallet wallet, CsWalletListener listener);
+ void startListeners(WrappedWallet wallet);
+ void stopListeners(WrappedWallet wallet);
- Future<bool> rescanBlockchain(String walletId);
- Future<bool> isConnectedToDaemon(String walletId);
+ Future<bool> rescanBlockchain(WrappedWallet wallet);
+ Future<bool> isConnectedToDaemon(WrappedWallet wallet);
- int getRefreshFromBlockHeight(String walletId);
- void setRefreshFromBlockHeight(String walletId, int height);
+ int getRefreshFromBlockHeight(WrappedWallet wallet);
+ void setRefreshFromBlockHeight(WrappedWallet wallet, int height);
Future<void> connect(
- String walletId, {
+ WrappedWallet wallet, {
required String daemonAddress,
required bool trusted,
String? daemonUsername,
@@ -97,24 +94,27 @@ abstract class CsSalviumInterface {
String? socksProxyAddress,
});
- Future<List<String>> getAllTxids(String walletId, {bool refresh = false});
+ Future<List<String>> getAllTxids(
+ WrappedWallet wallet, {
+ bool refresh = false,
+ });
- BigInt? getBalance(String walletId, {int accountIndex = 0});
- BigInt? getUnlockedBalance(String walletId, {int accountIndex = 0});
+ BigInt? getBalance(WrappedWallet wallet, {int accountIndex = 0});
+ BigInt? getUnlockedBalance(WrappedWallet wallet, {int accountIndex = 0});
Future<List<CsTransaction>> getAllTxs(
- String walletId, {
+ WrappedWallet wallet, {
bool refresh = false,
});
Future<List<CsTransaction>> getTxs(
- String walletId, {
+ WrappedWallet wallet, {
required Set<String> txids,
bool refresh = false,
});
Future<CsPendingTransaction> createTx(
- String walletId, {
+ WrappedWallet wallet, {
required CsRecipient output,
required int priority,
required bool sweep,
@@ -124,8 +124,18 @@ abstract class CsSalviumInterface {
required int currentHeight,
});
+ Future<CsPendingTransaction> createStakeTx(
+ WrappedWallet wallet, {
+ required CsRecipient output,
+ required int priority,
+ List<StandardInput>? preferredInputs,
+ required int accountIndex,
+ required int minConfirms,
+ required int currentHeight,
+ });
+
Future<CsPendingTransaction> createTxMultiDest(
- String walletId, {
+ WrappedWallet wallet, {
required List<CsRecipient> outputs,
required int priority,
required bool sweep,
@@ -135,20 +145,31 @@ abstract class CsSalviumInterface {
required int currentHeight,
});
- Future<void> commitTx(String walletId, CsPendingTransaction tx);
+ Future<void> commitTx(WrappedWallet wallet, CsPendingTransaction tx);
Future<List<CsOutput>> getOutputs(
- String walletId, {
+ WrappedWallet wallet, {
bool refresh = false,
bool includeSpent = false,
});
- Future<void> freezeOutput(String walletId, String keyImage);
- Future<void> thawOutput(String walletId, String keyImage);
+ Future<void> freezeOutput(WrappedWallet wallet, String keyImage);
+ Future<void> thawOutput(WrappedWallet wallet, String keyImage);
List<String> getSalviumWordList(String language);
int getHeightByDate(DateTime date);
bool validateAddress(String address, int network);
+
+ String getSeed(WrappedWallet wallet);
+}
+
+// lol...
+class WrappedWallet {
+ final Object _wallet;
+
+ const WrappedWallet(this._wallet);
+
+ T get<T>() => _wallet as T;
}
diff --git a/tool/wl_templates/SAL_cs_salvium_interface_impl.template.dart b/tool/wl_templates/SAL_cs_salvium_interface_impl.template.dart
index 6d56cb0..b3e88da 100644
--- a/tool/wl_templates/SAL_cs_salvium_interface_impl.template.dart
+++ b/tool/wl_templates/SAL_cs_salvium_interface_impl.template.dart
@@ -19,22 +19,25 @@ CsSalviumInterface _getInterface() => throw Exception("XMR/WOW not enabled!");
//ON
CsSalviumInterface _getInterface() => _CsSalviumInterfaceImpl();
-class _CsSalviumInterfaceImpl extends CsSalviumInterface {
- final Map<String, lib_salvium.Wallet> _wallets = {};
+extension _WrappedWalletExt on WrappedWallet {
+ lib_salvium.SalviumWallet get actual => get();
+}
+class _CsSalviumInterfaceImpl extends CsSalviumInterface {
@override
void setUseCsSalviumLoggerInternal(bool enable) =>
lib_salvium.Logging.useLogger = enable;
- @override
- bool walletInstanceExists(String walletId) => _wallets[walletId] != null;
-
@override
bool walletExists(String path) =>
lib_salvium.SalviumWallet.isWalletExist(path);
@override
- Future<int> estimateFee(int rate, BigInt amount, {required String walletId}) {
+ Future<int> estimateFee(
+ int rate,
+ BigInt amount, {
+ required WrappedWallet wallet,
+ }) {
lib_salvium.TransactionPriority priority;
switch (rate) {
case 1:
@@ -55,19 +58,20 @@ class _CsSalviumInterfaceImpl extends CsSalviumInterface {
break;
}
- return _wallets[walletId]!.estimateFee(priority, amount.toInt());
+ return wallet.actual.estimateFee(priority, amount.toInt());
}
@override
- Future<void> loadWallet(
+ Future<WrappedWallet> loadWallet(
String walletId, {
required String path,
required String password,
}) async {
- _wallets[walletId] = await lib_salvium.SalviumWallet.loadWallet(
+ final wallet = await lib_salvium.SalviumWallet.loadWallet(
path: path,
password: password,
);
+ return WrappedWallet(wallet);
}
@override
@@ -81,21 +85,19 @@ class _CsSalviumInterfaceImpl extends CsSalviumInterface {
@override
String getAddress(
- String walletId, {
+ WrappedWallet wallet, {
int accountIndex = 0,
int addressIndex = 0,
- }) => _wallets[walletId]!
+ }) => wallet.actual
.getAddress(accountIndex: accountIndex, addressIndex: addressIndex)
.value;
@override
- Future<void> getCreatedWallet({
+ Future<WrappedWallet> getCreatedWallet({
required String path,
required String password,
required int wordCount,
required String seedOffset,
- required final void Function(int refreshFromBlockHeight, String seed)
- onCreated,
}) async {
final type = switch (wordCount) {
16 => lib_salvium.SalviumSeedType.sixteen,
@@ -110,14 +112,11 @@ class _CsSalviumInterfaceImpl extends CsSalviumInterface {
seedOffset: seedOffset,
);
- onCreated(
- wallet.getRefreshFromBlockHeight(),
- wallet.getSeed(seedOffset: seedOffset).trim(),
- );
+ return WrappedWallet(wallet);
}
@override
- Future<void> getRestoredWallet({
+ Future<WrappedWallet> getRestoredWallet({
required String walletId,
required String path,
required String password,
@@ -125,17 +124,19 @@ class _CsSalviumInterfaceImpl extends CsSalviumInterface {
required String seedOffset,
int height = 0,
}) async {
- _wallets[walletId] = await lib_salvium.SalviumWallet.restoreWalletFromSeed(
+ final wallet = await lib_salvium.SalviumWallet.restoreWalletFromSeed(
path: path,
password: password,
seed: mnemonic,
restoreHeight: height,
seedOffset: seedOffset,
);
+
+ return WrappedWallet(wallet);
}
@override
- Future<void> getRestoredFromViewKeyWallet({
+ Future<WrappedWallet> getRestoredFromViewKeyWallet({
required String walletId,
required String path,
required String password,
@@ -143,62 +144,63 @@ class _CsSalviumInterfaceImpl extends CsSalviumInterface {
required String privateViewKey,
int height = 0,
}) async {
- _wallets[walletId] = await lib_salvium.SalviumWallet.createViewOnlyWallet(
+ final wallet = await lib_salvium.SalviumWallet.createViewOnlyWallet(
path: path,
password: password,
address: address,
viewKey: privateViewKey,
restoreHeight: height,
);
+
+ return WrappedWallet(wallet);
}
@override
- String getTxKey(String walletId, String txid) =>
- _wallets[walletId]!.getTxKey(txid);
+ String getTxKey(WrappedWallet wallet, String txid) =>
+ wallet.actual.getTxKey(txid);
@override
- Future<void> save(String walletId) => _wallets[walletId]!.save();
+ Future<void> save(WrappedWallet wallet) => wallet.actual.save();
@override
- String getPublicViewKey(String walletId) =>
- _wallets[walletId]!.getPublicViewKey();
+ String getPublicViewKey(WrappedWallet wallet) =>
+ wallet.actual.getPublicViewKey();
@override
- String getPrivateViewKey(String walletId) =>
- _wallets[walletId]!.getPrivateViewKey();
+ String getPrivateViewKey(WrappedWallet wallet) =>
+ wallet.actual.getPrivateViewKey();
@override
- String getPublicSpendKey(String walletId) =>
- _wallets[walletId]!.getPublicSpendKey();
+ String getPublicSpendKey(WrappedWallet wallet) =>
+ wallet.actual.getPublicSpendKey();
@override
- String getPrivateSpendKey(String walletId) =>
- _wallets[walletId]!.getPrivateSpendKey();
+ String getPrivateSpendKey(WrappedWallet wallet) =>
+ wallet.actual.getPrivateSpendKey();
@override
- Future<bool> isSynced(String walletId) =>
- _wallets[walletId]?.isSynced() ?? Future.value(false);
+ Future<bool> isSynced(WrappedWallet wallet) =>
+ wallet.actual.isSynced() ?? Future.value(false);
@override
- void startSyncing(String walletId) => _wallets[walletId]?.startSyncing();
+ void startSyncing(WrappedWallet wallet) => wallet.actual.startSyncing();
@override
- void stopSyncing(String walletId) => _wallets[walletId]?.stopSyncing();
+ void stopSyncing(WrappedWallet wallet) => wallet.actual.stopSyncing();
@override
- void startAutoSaving(String walletId) =>
- _wallets[walletId]?.startAutoSaving();
+ void startAutoSaving(WrappedWallet wallet) => wallet.actual.startAutoSaving();
@override
- void stopAutoSaving(String walletId) => _wallets[walletId]?.stopAutoSaving();
+ void stopAutoSaving(WrappedWallet wallet) => wallet.actual.stopAutoSaving();
@override
- bool hasListeners(String walletId) =>
- _wallets[walletId]!.getListeners().isNotEmpty;
+ bool hasListeners(WrappedWallet wallet) =>
+ wallet.actual.getListeners().isNotEmpty;
@override
- void addListener(String walletId, CsWalletListener listener) =>
- _wallets[walletId]?.addListener(
+ void addListener(WrappedWallet wallet, CsWalletListener listener) =>
+ wallet.actual.addListener(
lib_salvium.WalletListener(
onSyncingUpdate: listener.onSyncingUpdate,
onNewBlock: listener.onNewBlock,
@@ -208,30 +210,30 @@ class _CsSalviumInterfaceImpl extends CsSalviumInterface {
);
@override
- void startListeners(String walletId) => _wallets[walletId]?.startListeners();
+ void startListeners(WrappedWallet wallet) => wallet.actual.startListeners();
@override
- void stopListeners(String walletId) => _wallets[walletId]?.stopListeners();
+ void stopListeners(WrappedWallet wallet) => wallet.actual.stopListeners();
@override
- int getRefreshFromBlockHeight(String walletId) =>
- _wallets[walletId]!.getRefreshFromBlockHeight();
+ int getRefreshFromBlockHeight(WrappedWallet wallet) =>
+ wallet.actual.getRefreshFromBlockHeight();
@override
- void setRefreshFromBlockHeight(String walletId, int height) =>
- _wallets[walletId]!.setRefreshFromBlockHeight(height);
+ void setRefreshFromBlockHeight(WrappedWallet wallet, int height) =>
+ wallet.actual.setRefreshFromBlockHeight(height);
@override
- Future<bool> rescanBlockchain(String walletId) =>
- _wallets[walletId]?.rescanBlockchain() ?? Future.value(false);
+ Future<bool> rescanBlockchain(WrappedWallet wallet) =>
+ wallet.actual.rescanBlockchain() ?? Future.value(false);
@override
- Future<bool> isConnectedToDaemon(String walletId) =>
- _wallets[walletId]?.isConnectedToDaemon() ?? Future.value(false);
+ Future<bool> isConnectedToDaemon(WrappedWallet wallet) =>
+ wallet.actual.isConnectedToDaemon() ?? Future.value(false);
@override
Future<void> connect(
- String walletId, {
+ WrappedWallet wallet, {
required String daemonAddress,
required bool trusted,
String? daemonUsername,
@@ -240,7 +242,7 @@ class _CsSalviumInterfaceImpl extends CsSalviumInterface {
bool isLightWallet = false,
String? socksProxyAddress,
}) async {
- await _wallets[walletId]?.connect(
+ await wallet.actual.connect(
daemonAddress: daemonAddress,
trusted: trusted,
daemonUsername: daemonUsername,
@@ -252,23 +254,25 @@ class _CsSalviumInterfaceImpl extends CsSalviumInterface {
}
@override
- Future<List<String>> getAllTxids(String walletId, {bool refresh = false}) =>
- _wallets[walletId]!.getAllTxids(refresh: refresh);
+ Future<List<String>> getAllTxids(
+ WrappedWallet wallet, {
+ bool refresh = false,
+ }) => wallet.actual.getAllTxids(refresh: refresh);
@override
- BigInt? getBalance(String walletId, {int accountIndex = 0}) =>
- _wallets[walletId]?.getBalance(accountIndex: accountIndex);
+ BigInt? getBalance(WrappedWallet wallet, {int accountIndex = 0}) =>
+ wallet.actual.getBalance(accountIndex: accountIndex);
@override
- BigInt? getUnlockedBalance(String walletId, {int accountIndex = 0}) =>
- _wallets[walletId]?.getUnlockedBalance(accountIndex: accountIndex);
+ BigInt? getUnlockedBalance(WrappedWallet wallet, {int accountIndex = 0}) =>
+ wallet.actual.getUnlockedBalance(accountIndex: accountIndex);
@override
Future<List<CsTransaction>> getAllTxs(
- String walletId, {
+ WrappedWallet wallet, {
bool refresh = false,
}) async {
- final transactions = await _wallets[walletId]?.getAllTxs(refresh: refresh);
+ final transactions = await wallet.actual.getAllTxs(refresh: refresh);
if (transactions == null) return [];
return transactions
.map(
@@ -287,6 +291,7 @@ class _CsSalviumInterfaceImpl extends CsSalviumInterface {
key: e.key,
timeStamp: e.timeStamp,
minConfirms: e.minConfirms.value,
+ salviumData: (asset: e.asset, type: e.type),
),
)
.toList();
@@ -294,15 +299,15 @@ class _CsSalviumInterfaceImpl extends CsSalviumInterface {
@override
Future<List<CsTransaction>> getTxs(
- String walletId, {
+ WrappedWallet wallet, {
required Set<String> txids,
bool refresh = false,
}) async {
- final transactions = await _wallets[walletId]?.getTxs(
+ final transactions = await wallet.actual.getTxs(
txids: txids,
refresh: refresh,
);
- if (transactions == null) return [];
+
return transactions
.map(
(e) => CsTransaction(
@@ -320,6 +325,7 @@ class _CsSalviumInterfaceImpl extends CsSalviumInterface {
key: e.key,
timeStamp: e.timeStamp,
minConfirms: e.minConfirms.value,
+ salviumData: (asset: e.asset, type: e.type),
),
)
.toList();
@@ -327,7 +333,7 @@ class _CsSalviumInterfaceImpl extends CsSalviumInterface {
@override
Future<CsPendingTransaction> createTx(
- String walletId, {
+ WrappedWallet wallet, {
required CsRecipient output,
required int priority,
required bool sweep,
@@ -336,7 +342,7 @@ class _CsSalviumInterfaceImpl extends CsSalviumInterface {
required int minConfirms,
required int currentHeight,
}) async {
- final pending = await _wallets[walletId]!.createTx(
+ final pending = await wallet.actual.createTx(
output: lib_salvium.Recipient(
address: output.address,
amount: output.amount,
@@ -376,9 +382,58 @@ class _CsSalviumInterfaceImpl extends CsSalviumInterface {
);
}
+ @override
+ Future<CsPendingTransaction> createStakeTx(
+ WrappedWallet wallet, {
+ required CsRecipient output,
+ required int priority,
+ List<StandardInput>? preferredInputs,
+ required int accountIndex,
+ required int minConfirms,
+ required int currentHeight,
+ }) async {
+ final pending = await wallet.actual.createStakeTx(
+ output: lib_salvium.Recipient(
+ address: output.address,
+ amount: output.amount,
+ ),
+ paymentId: "",
+ priority: lib_salvium.TransactionPriority.values.firstWhere(
+ (e) => e.value == priority,
+ ),
+ preferredInputs: preferredInputs
+ ?.map(
+ (e) => lib_salvium.Output(
+ address: e.address!,
+ hash: e.utxo.txid,
+ keyImage: e.utxo.keyImage!,
+ value: e.value,
+ isFrozen: e.utxo.isBlocked,
+ isUnlocked:
+ e.utxo.blockHeight != null &&
+ (currentHeight - (e.utxo.blockHeight ?? 0)) >= minConfirms,
+ height: e.utxo.blockHeight ?? 0,
+ vout: e.utxo.vout,
+ spent: e.utxo.used ?? false,
+ spentHeight: null, // doesn't matter here
+ coinbase: e.utxo.isCoinbase,
+ ),
+ )
+ .toList(),
+ accountIndex: accountIndex,
+ );
+
+ return CsPendingTransaction(
+ pending,
+ pending.amount,
+ pending.fee,
+ pending.txid,
+ );
+ }
+
@override
Future<CsPendingTransaction> createTxMultiDest(
- String walletId, {
+ WrappedWallet wallet, {
required List<CsRecipient> outputs,
required int priority,
required bool sweep,
@@ -387,7 +442,7 @@ class _CsSalviumInterfaceImpl extends CsSalviumInterface {
required int minConfirms,
required int currentHeight,
}) async {
- final pending = await _wallets[walletId]!.createTxMultiDest(
+ final pending = await wallet.actual.createTxMultiDest(
outputs: outputs
.map(
(e) => lib_salvium.Recipient(address: e.address, amount: e.amount),
@@ -429,22 +484,20 @@ class _CsSalviumInterfaceImpl extends CsSalviumInterface {
}
@override
- Future<void> commitTx(String walletId, CsPendingTransaction tx) =>
- _wallets[walletId]!.commitTx(tx.value as lib_salvium.PendingTransaction);
+ Future<void> commitTx(WrappedWallet wallet, CsPendingTransaction tx) =>
+ wallet.actual.commitTx(tx.value as lib_salvium.PendingTransaction);
@override
Future<List<CsOutput>> getOutputs(
- String walletId, {
+ WrappedWallet wallet, {
bool refresh = false,
bool includeSpent = false,
}) async {
- final outputs = await _wallets[walletId]?.getOutputs(
+ final outputs = await wallet.actual.getOutputs(
includeSpent: includeSpent,
refresh: refresh,
);
- if (outputs == null) return [];
-
return outputs
.map(
(e) => CsOutput(
@@ -465,12 +518,12 @@ class _CsSalviumInterfaceImpl extends CsSalviumInterface {
}
@override
- Future<void> freezeOutput(String walletId, String keyImage) =>
- _wallets[walletId]!.freezeOutput(keyImage);
+ Future<void> freezeOutput(WrappedWallet wallet, String keyImage) =>
+ wallet.actual.freezeOutput(keyImage);
@override
- Future<void> thawOutput(String walletId, String keyImage) =>
- _wallets[walletId]!.thawOutput(keyImage);
+ Future<void> thawOutput(WrappedWallet wallet, String keyImage) =>
+ wallet.actual.thawOutput(keyImage);
@override
List<String> getSalviumWordList(String language) =>
@@ -483,6 +536,9 @@ class _CsSalviumInterfaceImpl extends CsSalviumInterface {
@override
bool validateAddress(String address, int network) =>
sal_wallet_ffi.validateAddress(address, network);
+
+ @override
+ String getSeed(WrappedWallet wallet) => wallet.actual.getSeed();
}
//END_ON
Why this scored 27/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.