What changed, and why it matters
This commit adds a feature that immediately records a pending Solana token transfer in the local wallet database before the blockchain confirms it. It is a normal UI/UX improvement so users see outgoing transactions faster. There is no direct security vulnerability in the diff, but the code silently ignores database write failures, which could in rare cases leave the wallet's transaction history inconsistent with what was actually broadcast.
Treat as a routine feature commit. As a defensive measure, review whether persistence failures should be surfaced to the user and whether pending records are correctly updated or removed when on-chain confirmation succeeds, fails, or times out, to avoid stale or misleading transaction history.
Security signals we found
Silent failure path: database persistence errors are caught and logged but do not abort or surface the failure to the caller
New enum value TransactionSubType.splToken added to transaction classification
Pending transaction state derived from local variables (senderTokenAccount, recipientTokenAccount, txData.amount, txData.fee) without additional on-chain verification before persistence
No explicit rollback or cleanup of the pending record if _waitForConfirmation later fails
Evidence from the diff
The change introduces a temporary TransactionV2 record with blockHash and height set to null to mark it as pending, subType set to the new TransactionSubType.splToken, and otherData carrying mint, token accounts, fee, and cancellation state. The record is persisted via mainDB.updateOrPutTransactionV2s inside a try/catch that only logs a warning if persistence fails. The actual send logic and confirmation wait are unchanged. No input validation, serialization, or cryptographic changes are present.
Changed components
lib/wallets/wallet/impl/sub_wallets/solana_token_wallet.dartlib/models/isar/models/blockchain_data/transaction.dartInspect captured patch +73 / −0
diff --git a/lib/models/isar/models/blockchain_data/transaction.dart b/lib/models/isar/models/blockchain_data/transaction.dart
index 3e43ffb..07b4b91 100644
--- a/lib/models/isar/models/blockchain_data/transaction.dart
+++ b/lib/models/isar/models/blockchain_data/transaction.dart
@@ -261,4 +261,5 @@ enum TransactionSubType {
sparkSpend, // firo specific
ordinal,
mweb,
+ splToken, // Solana token (SPL).
}
diff --git a/lib/wallets/wallet/impl/sub_wallets/solana_token_wallet.dart b/lib/wallets/wallet/impl/sub_wallets/solana_token_wallet.dart
index 2de296b..720e238 100644
--- a/lib/wallets/wallet/impl/sub_wallets/solana_token_wallet.dart
+++ b/lib/wallets/wallet/impl/sub_wallets/solana_token_wallet.dart
@@ -15,6 +15,10 @@ import 'package:solana/solana.dart' hide Wallet;
import '../../../../db/isar/main_db.dart';
import '../../../../models/balance.dart';
+import '../../../../models/isar/models/blockchain_data/transaction.dart';
+import '../../../../models/isar/models/blockchain_data/v2/input_v2.dart';
+import '../../../../models/isar/models/blockchain_data/v2/output_v2.dart';
+import '../../../../models/isar/models/blockchain_data/v2/transaction_v2.dart';
import '../../../../models/paymint/fee_object_model.dart';
import '../../../../services/solana/solana_token_api.dart';
import '../../../../utilities/amount/amount.dart';
@@ -279,6 +283,74 @@ class SolanaTokenWallet extends Wallet {
);
}
+ // Create temporary transaction (pending = unconfirmed) and save to db.
+ try {
+ // Build inputs and outputs for the transaction record.
+ final inputs = [
+ InputV2.isarCantDoRequiredInDefaultConstructor(
+ scriptSigHex: null,
+ scriptSigAsm: null,
+ sequence: null,
+ outpoint: null,
+ addresses: [senderTokenAccount],
+ valueStringSats: txData.amount!.raw.toString(),
+ witness: null,
+ innerRedeemScriptAsm: null,
+ coinbase: null,
+ walletOwns: true,
+ ),
+ ];
+
+ final outputs = [
+ OutputV2.isarCantDoRequiredInDefaultConstructor(
+ scriptPubKeyHex: "00",
+ valueStringSats: txData.amount!.raw.toString(),
+ addresses: [recipientTokenAccount],
+ walletOwns: false, // We don't own recipient account.
+ ),
+ ];
+
+ // Determine if this is a self-transfer.
+ final isToSelf = senderTokenAccount == recipientTokenAccount;
+
+ // Create the temporary transaction record.
+ final tempTx = TransactionV2(
+ walletId: walletId,
+ blockHash: null, // CRITICAL: null indicates pending.
+ hash: txid,
+ txid: txid,
+ timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
+ height: null, // CRITICAL: null indicates pending.
+ inputs: List.unmodifiable(inputs),
+ outputs: List.unmodifiable(outputs),
+ version: -1,
+ type: isToSelf
+ ? TransactionType.sentToSelf
+ : TransactionType.outgoing,
+ subType: TransactionSubType.splToken,
+ otherData: jsonEncode({
+ "mint": tokenMint,
+ "senderTokenAccount": senderTokenAccount,
+ "recipientTokenAccount": recipientTokenAccount,
+ "isCancelled": false,
+ "overrideFee": txData.fee!.toJsonString(),
+ }),
+ );
+
+ // Persist immediately to database so UI shows transaction right away.
+ await mainDB.updateOrPutTransactionV2s([tempTx]);
+ Logging.instance.i(
+ "$runtimeType confirmSend: Persisted pending transaction $txid to database",
+ );
+ } catch (e, s) {
+ // Log persistence error but don't fail the send operation.
+ Logging.instance.w(
+ "$runtimeType confirmSend: Failed to persist pending transaction to database: ",
+ error: e,
+ stackTrace: s,
+ );
+ }
+
// Wait for confirmation.
final confirmed = await _waitForConfirmation(
signature: txid,
Why this scored 16/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.