Skip to content

Commit c3a1cdc

Browse files
committed
feat(spl): cache local sol & spl txs and update txs as they confirm
1 parent aa87ab1 commit c3a1cdc

2 files changed

Lines changed: 351 additions & 63 deletions

File tree

lib/wallets/wallet/impl/solana_wallet.dart

Lines changed: 187 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import '../../../app_config.dart';
1313
import '../../../exceptions/wallet/node_tor_mismatch_config_exception.dart';
1414
import '../../../models/balance.dart';
1515
import '../../../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';
1619
import '../../../models/isar/models/isar_models.dart';
1720
import '../../../models/node_model.dart';
1821
import '../../../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

Comments
 (0)