@@ -13,6 +13,9 @@ import '../../../app_config.dart';
1313import '../../../exceptions/wallet/node_tor_mismatch_config_exception.dart' ;
1414import '../../../models/balance.dart' ;
1515import '../../../models/isar/models/blockchain_data/transaction.dart' as isar;
16+ import '../../../models/isar/models/blockchain_data/v2/input_v2.dart' ;
17+ import '../../../models/isar/models/blockchain_data/v2/output_v2.dart' ;
18+ import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart' ;
1619import '../../../models/isar/models/isar_models.dart' ;
1720import '../../../models/node_model.dart' ;
1821import '../../../models/paymint/fee_object_model.dart' ;
@@ -217,6 +220,52 @@ class SolanaWallet extends Bip39Wallet<Solana> {
217220 );
218221
219222 final txid = await _rpcClient? .signAndSendTransaction (message, [keyPair]);
223+
224+ // Persist pending transaction immediately so UI shows "Sending" status.
225+ if (txid != null ) {
226+ final senderAddress = keyPair.address;
227+ final isToSelf = senderAddress == recipientAccount.address;
228+
229+ final tempTx = TransactionV2 (
230+ walletId: walletId,
231+ blockHash: null , // CRITICAL: indicates pending.
232+ hash: txid,
233+ txid: txid,
234+ timestamp: DateTime .now ().millisecondsSinceEpoch ~ / 1000 ,
235+ height: null , // CRITICAL: indicates pending.
236+ inputs: [
237+ InputV2 .isarCantDoRequiredInDefaultConstructor (
238+ scriptSigHex: null ,
239+ scriptSigAsm: null ,
240+ sequence: null ,
241+ outpoint: null ,
242+ addresses: [senderAddress],
243+ valueStringSats: txData.amount! .raw.toString (),
244+ witness: null ,
245+ innerRedeemScriptAsm: null ,
246+ coinbase: null ,
247+ walletOwns: true ,
248+ ),
249+ ],
250+ outputs: [
251+ OutputV2 .isarCantDoRequiredInDefaultConstructor (
252+ scriptPubKeyHex: "00" ,
253+ valueStringSats: txData.amount! .raw.toString (),
254+ addresses: [recipientAccount.address],
255+ walletOwns: isToSelf,
256+ ),
257+ ],
258+ version: - 1 ,
259+ type: isToSelf ? isar.TransactionType .sentToSelf : isar.TransactionType .outgoing,
260+ subType: isar.TransactionSubType .none,
261+ otherData: jsonEncode ({
262+ "overrideFee" : txData.fee! .toJsonString (),
263+ }),
264+ );
265+
266+ await mainDB.updateOrPutTransactionV2s ([tempTx]);
267+ }
268+
220269 return txData.copyWith (txid: txid);
221270 } catch (e, s) {
222271 Logging .instance.e (
@@ -253,7 +302,7 @@ class SolanaWallet extends Bip39Wallet<Solana> {
253302
254303 final fee = await _getEstimatedNetworkFee (
255304 Amount .fromDecimal (
256- Decimal .one, // 1 SOL
305+ Decimal .one, // 1 SOL.
257306 fractionDigits: cryptoCurrency.fractionDigits,
258307 ),
259308 );
@@ -411,81 +460,157 @@ class SolanaWallet extends Bip39Wallet<Solana> {
411460 (await _getKeyPair ()).publicKey,
412461 encoding: Encoding .jsonParsed,
413462 );
414- final txsList = List <Tuple2 <isar.Transaction , Address >>.empty (
415- growable: true ,
416- );
417463
418464 final myAddress = (await getCurrentReceivingAddress ())! ;
419465
420- // TODO [prio=low]: Revisit null assertion below.
466+ if (transactionsList == null ) {
467+ return ;
468+ }
421469
422- for (final tx in transactionsList! ) {
423- final senderAddress =
424- (tx.transaction as ParsedTransaction ).message.accountKeys[0 ].pubkey;
425- var receiverAddress =
426- (tx.transaction as ParsedTransaction ).message.accountKeys[1 ].pubkey;
427- var txType = isar.TransactionType .unknown;
428- final txAmount = Amount (
429- rawValue: BigInt .from (
470+ final txns = < TransactionV2 > [];
471+ int skippedCount = 0 ;
472+
473+ for (final tx in transactionsList) {
474+ try {
475+ // Skip transactions without metadata.
476+ if (tx.meta == null ) {
477+ skippedCount++ ;
478+ continue ;
479+ }
480+
481+ if (tx.transaction is ! ParsedTransaction ) {
482+ skippedCount++ ;
483+ continue ;
484+ }
485+
486+ final parsedTx = tx.transaction as ParsedTransaction ;
487+ final txid = parsedTx.signatures.isNotEmpty ? parsedTx.signatures[0 ] : null ;
488+ if (txid == null ) {
489+ skippedCount++ ;
490+ continue ;
491+ }
492+
493+ // Determine transaction direction.
494+ final senderAddress = parsedTx.message.accountKeys[0 ].pubkey;
495+ var receiverAddress =
496+ parsedTx.message.accountKeys.length > 1
497+ ? parsedTx.message.accountKeys[1 ].pubkey
498+ : senderAddress;
499+ var txType = isar.TransactionType .unknown;
500+
501+ if ((senderAddress == myAddress.value) &&
502+ (receiverAddress == "11111111111111111111111111111111" )) {
503+ // System Program account means sent to self.
504+ txType = isar.TransactionType .sentToSelf;
505+ receiverAddress = senderAddress;
506+ } else if (senderAddress == myAddress.value) {
507+ txType = isar.TransactionType .outgoing;
508+ } else if (receiverAddress == myAddress.value) {
509+ txType = isar.TransactionType .incoming;
510+ }
511+
512+ // Calculate transfer amount.
513+ final amount = BigInt .from (
430514 tx.meta! .postBalances[1 ] - tx.meta! .preBalances[1 ],
431- ),
432- fractionDigits: cryptoCurrency.fractionDigits,
433- );
515+ );
434516
435- if ((senderAddress == myAddress.value) &&
436- (receiverAddress == "11111111111111111111111111111111" )) {
437- // The account that is only 1's are System Program accounts which
438- // means there is no receiver except the sender,
439- // see: https://explorer.solana.com/address/11111111111111111111111111111111
440- txType = isar.TransactionType .sentToSelf;
441- receiverAddress = senderAddress;
442- } else if (senderAddress == myAddress.value) {
443- txType = isar.TransactionType .outgoing;
444- } else if (receiverAddress == myAddress.value) {
445- txType = isar.TransactionType .incoming;
446- }
517+ // Check if this transaction already exists.
518+ // If it does, preserve the overrideFee from the pending transaction.
519+ dynamic existingOverrideFee;
520+ try {
521+ final allTxsForWallet = await mainDB.isar.transactionV2s
522+ .where ()
523+ .walletIdEqualTo (walletId)
524+ .findAll ();
525+ for (final existingTx in allTxsForWallet) {
526+ if (existingTx.txid == txid) {
527+ final existingOtherData = existingTx.otherData;
528+ if (existingOtherData != null && existingOtherData.isNotEmpty) {
529+ try {
530+ final otherDataMap = jsonDecode (existingOtherData);
531+ if (otherDataMap is Map &&
532+ otherDataMap.containsKey ('overrideFee' )) {
533+ existingOverrideFee = otherDataMap['overrideFee' ];
534+ }
535+ } catch (e) {
536+ // Ignore parsing errors.
537+ }
538+ }
539+ break ;
540+ }
541+ }
542+ } catch (e) {
543+ // Ignore database query errors.
544+ }
545+
546+ // Build otherData, preserving overrideFee if it existed.
547+ final otherDataMap = < String , dynamic > {};
548+ if (existingOverrideFee != null ) {
549+ otherDataMap["overrideFee" ] = existingOverrideFee;
550+ }
551+
552+ // Create TransactionV2 object.
553+ final txn = TransactionV2 (
554+ walletId: walletId,
555+ blockHash: null ,
556+ hash: txid,
557+ txid: txid,
558+ timestamp: tx.blockTime ?? DateTime .now ().millisecondsSinceEpoch ~ / 1000 ,
559+ height: tx.slot,
560+ inputs: [
561+ InputV2 .isarCantDoRequiredInDefaultConstructor (
562+ scriptSigHex: null ,
563+ scriptSigAsm: null ,
564+ sequence: null ,
565+ outpoint: null ,
566+ addresses: [senderAddress],
567+ valueStringSats: amount.toString (),
568+ witness: null ,
569+ innerRedeemScriptAsm: null ,
570+ coinbase: null ,
571+ walletOwns: senderAddress == myAddress.value,
572+ ),
573+ ],
574+ outputs: [
575+ OutputV2 .isarCantDoRequiredInDefaultConstructor (
576+ scriptPubKeyHex: "00" ,
577+ valueStringSats: amount.toString (),
578+ addresses: [receiverAddress],
579+ walletOwns: receiverAddress == myAddress.value,
580+ ),
581+ ],
582+ version: - 1 ,
583+ type: txType,
584+ subType: isar.TransactionSubType .none,
585+ otherData: otherDataMap.isNotEmpty ? jsonEncode (otherDataMap) : null ,
586+ );
447587
448- final transaction = isar.Transaction (
449- walletId: walletId,
450- txid: (tx.transaction as ParsedTransaction ).signatures[0 ],
451- timestamp: tx.blockTime! ,
452- type: txType,
453- subType: isar.TransactionSubType .none,
454- amount: tx.meta! .postBalances[1 ] - tx.meta! .preBalances[1 ],
455- amountString: txAmount.toJsonString (),
456- fee: tx.meta! .fee,
457- height: tx.slot,
458- isCancelled: false ,
459- isLelantus: false ,
460- slateId: null ,
461- otherData: null ,
462- inputs: [],
463- outputs: [],
464- nonce: null ,
465- numberOfMessages: 0 ,
466- );
588+ txns.add (txn);
589+ } catch (e, s) {
590+ Logging .instance.w (
591+ "$runtimeType updateTransactions: Failed to parse transaction" ,
592+ error: e,
593+ stackTrace: s,
594+ );
595+ skippedCount++ ;
596+ continue ;
597+ }
598+ }
467599
468- final txAddress = Address (
469- walletId: walletId,
470- value: receiverAddress,
471- publicKey: List <int >.empty (),
472- derivationIndex: 0 ,
473- derivationPath: DerivationPath ()..value = _addressDerivationPath,
474- type: AddressType .solana,
475- subType: txType == isar.TransactionType .outgoing
476- ? AddressSubType .unknown
477- : AddressSubType .receiving,
600+ // Persist all transactions if any were parsed.
601+ if (txns.isNotEmpty) {
602+ await mainDB.updateOrPutTransactionV2s (txns);
603+ Logging .instance.i (
604+ "$runtimeType updateTransactions: Synced ${txns .length } transactions (skipped $skippedCount )" ,
478605 );
479-
480- txsList.add (Tuple2 (transaction, txAddress));
481606 }
482- await mainDB.addNewTransactionData (txsList, walletId);
483607 } on NodeTorMismatchConfigException {
484608 rethrow ;
485609 } catch (e, s) {
486610 Logging .instance.e (
487- "Error occurred in solana_wallet.dart while getting"
488- " transactions for solana: $e \n $s " ,
611+ "$runtimeType updateTransactions failed: " ,
612+ error: e,
613+ stackTrace: s,
489614 );
490615 }
491616 }
0 commit comments