Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 29 additions & 6 deletions lib/electrumx_rpc/cached_electrumx_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,42 @@ class CachedElectrumXClient {

static const minCacheConfirms = 30;

static const _rawTxKeyPrefix = "raw:";

CachedElectrumXClient({required this.electrumXClient});

factory CachedElectrumXClient.from({
required ElectrumXClient electrumXClient,
}) => CachedElectrumXClient(electrumXClient: electrumXClient);

String base64ToHex(String source) => base64Decode(
LineSplitter.split(source).join(),
).map((e) => e.toRadixString(16).padLeft(2, '0')).join();
String base64ToHex(String source) =>
base64Decode(LineSplitter.split(source).join())
.map((e) => e.toRadixString(16).padLeft(2, '0'))
.join();

String base64ToReverseHex(String source) =>
base64Decode(LineSplitter.split(source).join()).reversed
.map((e) => e.toRadixString(16).padLeft(2, '0'))
.join();

String base64ToReverseHex(String source) => base64Decode(
LineSplitter.split(source).join(),
).reversed.map((e) => e.toRadixString(16).padLeft(2, '0')).join();
/// The raw hex stored for [txHash] by [putAuthenticatedRawTransaction].
Future<String?> getAuthenticatedRawTransaction({
required String txHash,
required CryptoCurrency cryptoCurrency,
}) async {
final box = await DB.instance.getTxCacheBox(currency: cryptoCurrency);
return box.get("$_rawTxKeyPrefix$txHash") as String?;
}

/// Stores [rawTxHex], which must already be checked to hash to [txHash].
Future<void> putAuthenticatedRawTransaction({
required String txHash,
required String rawTxHex,
required CryptoCurrency cryptoCurrency,
}) async {
final box = await DB.instance.getTxCacheBox(currency: cryptoCurrency);
await box.put("$_rawTxKeyPrefix$txHash", rawTxHex);
}

/// Call electrumx getTransaction on a per coin basis, storing the result in local db if not already there.
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,20 +117,42 @@ class _TransactionV2DetailsViewState
}
_boostButtonLock = true;
try {
if (Util.isDesktop) {
if (_transaction.vSize == null) {
final updatedTx = await showLoading(
whileFuture: wallet.updateVSize(_transaction),
context: context,
message: "Fetching transaction vSize...",
);

// TODO handle errors if null
_transaction = updatedTx!;
// Re-check the transaction against the current server before pricing a
// bump off its size. One the server cannot prove we signed is not
// boosted, and the refresh drops it from history.
Exception? failure;
final updatedTx = await showLoading(
whileFuture: wallet.updateVSize(_transaction),
context: context,
message: "Verifying transaction...",
onException: (e) => failure = e,
);
if (!mounted) {
return;
}
if (updatedTx == null) {
final error = failure;
await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: error == null
? "Could not verify transaction"
: "Failed to verify transaction",
message: error == null
? "The current server did not return a transaction signed by "
"this wallet, so it cannot be boosted."
: error.toString(),
maxWidth: Util.isDesktop ? 400 : null,
),
);
if (error == null) {
unawaited(wallet.refresh());
}
return;
}
_transaction = updatedTx;

// TODO pass the size in to the rbf screen

if (Util.isDesktop) {
if (mounted) {
await showDialog<void>(
context: context,
Expand Down Expand Up @@ -511,9 +533,9 @@ class _TransactionV2DetailsViewState
appBar: isDesktop
? null
: AppBar(
backgroundColor: Theme.of(
context,
).extension<StackColors>()!.background,
backgroundColor: Theme.of(context)
.extension<StackColors>()!
.background,
leading: AppBarBackButton(
onPressed: () async {
// if (FocusScope.of(context).hasFocus) {
Expand Down Expand Up @@ -559,9 +581,9 @@ class _TransactionV2DetailsViewState
builder: (child) {
return RoundedWhiteContainer(
borderColor: isDesktop
? Theme.of(
context,
).extension<StackColors>()!.backgroundAppBar
? Theme.of(context)
.extension<StackColors>()!
.backgroundAppBar
: null,
padding: const EdgeInsets.all(0),
child: child,
Expand Down Expand Up @@ -816,36 +838,30 @@ class _TransactionV2DetailsViewState
fetchContactNameFor(
e,
),
builder:
(
builderContext,
AsyncSnapshot<
String
>
snapshot,
) {
final String
addressOrContactName;
if (snapshot.connectionState ==
ConnectionState.done &&
snapshot
.hasData) {
addressOrContactName =
snapshot.data!;
} else {
addressOrContactName =
e;
}

return OutputCard(
address:
addressOrContactName,
amount:
data[i].amount,
coin:
coin,
);
},
builder: (builderContext, AsyncSnapshot<String> snapshot) {
final String
addressOrContactName;
if (snapshot.connectionState ==
ConnectionState
.done &&
snapshot
.hasData) {
addressOrContactName =
snapshot
.data!;
} else {
addressOrContactName =
e;
}

return OutputCard(
address:
addressOrContactName,
amount: data[i]
.amount,
coin: coin,
);
},
);
}),
],
Expand Down Expand Up @@ -1658,9 +1674,9 @@ class OutputCard extends ConsumerWidget {
ref.watch(pAmountFormatter(coin)).format(amount),
style: Util.isDesktop
? STextStyles.desktopTextExtraExtraSmall(context).copyWith(
color: Theme.of(
context,
).extension<StackColors>()!.textDark,
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
)
: STextStyles.itemSubtitle12(context),
),
Expand Down Expand Up @@ -1696,9 +1712,9 @@ class IconCopyButton extends StatelessWidget {
height: 26,
width: 26,
child: RawMaterialButton(
fillColor: Theme.of(
context,
).extension<StackColors>()!.buttonBackSecondary,
fillColor: Theme.of(context)
.extension<StackColors>()!
.buttonBackSecondary,
elevation: 0,
hoverElevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)),
Expand Down Expand Up @@ -1738,9 +1754,9 @@ class IconPencilButton extends StatelessWidget {
height: 26,
width: 26,
child: RawMaterialButton(
fillColor: Theme.of(
context,
).extension<StackColors>()!.buttonBackSecondary,
fillColor: Theme.of(context)
.extension<StackColors>()!
.buttonBackSecondary,
elevation: 0,
hoverElevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)),
Expand Down Expand Up @@ -1889,16 +1905,16 @@ class _TxidDetailItemState extends ConsumerState<_TxidDetailItem> {
child: Text(
"Cancel",
style: STextStyles.button(context).copyWith(
color: Theme.of(
context,
).extension<StackColors>()!.accentColorDark,
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
),
),
),
rightButton: TextButton(
style: Theme.of(
context,
).extension<StackColors>()!.getPrimaryEnabledButtonStyle(context),
style: Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonStyle(context),
onPressed: () {
Navigator.of(context).pop(true);
},
Expand Down Expand Up @@ -2026,13 +2042,12 @@ class _TxidDetailItemState extends ConsumerState<_TxidDetailItem> {
SelectableText(
widget.txid.replaceFirst("mweb_outputId_", ""),
style: isDesktop
? STextStyles.desktopTextExtraExtraSmall(
context,
).copyWith(
color: Theme.of(
context,
).extension<StackColors>()!.textDark,
)
? STextStyles.desktopTextExtraExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
)
: STextStyles.itemSubtitle12(context),
),
],
Expand Down Expand Up @@ -2073,13 +2088,12 @@ class _TxidDetailItemState extends ConsumerState<_TxidDetailItem> {
SelectableText(
widget.txid,
style: isDesktop
? STextStyles.desktopTextExtraExtraSmall(
context,
).copyWith(
color: Theme.of(
context,
).extension<StackColors>()!.textDark,
)
? STextStyles.desktopTextExtraExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
)
: STextStyles.itemSubtitle12(context),
),
if (widget.coin is! Epiccash &&
Expand Down Expand Up @@ -2176,9 +2190,9 @@ class _TxDetailsAmountHeader extends ConsumerWidget {
child: Container(
decoration: isDesktop
? BoxDecoration(
color: Theme.of(
context,
).extension<StackColors>()!.backgroundAppBar,
color: Theme.of(context)
.extension<StackColors>()!
.backgroundAppBar,
borderRadius: BorderRadius.vertical(
top: Radius.circular(Constants.size.circularBorderRadius),
),
Expand Down
3 changes: 3 additions & 0 deletions lib/wallets/crypto_currency/coins/particl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,9 @@ class Particl extends Bip39HDCurrency with ElectrumXCurrencyInterface {
@override
int get transactionVersion => 160;

@override
bool get hasBitcoinTransactionLayout => false;

@override
BigInt get defaultFeeRate => BigInt.from(20000);
}
3 changes: 3 additions & 0 deletions lib/wallets/crypto_currency/coins/peercoin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,9 @@ class Peercoin extends Bip39HDCurrency with ElectrumXCurrencyInterface {
@override
int get transactionVersion => 3;

@override
bool get legacyTransactionTimestamp => true;

@override
BigInt get defaultFeeRate => BigInt.from(10000);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ mixin ElectrumXCurrencyInterface on Bip39HDCurrency {
/// The default fee rate in satoshis per kilobyte.
BigInt get defaultFeeRate;

/// Whether raw transactions follow Bitcoin's layout, which the parser in
/// owned_input_signatures.dart reads, allowing for Peercoin's timestamp and
/// the Dash and Firo extra payload. Particl's layout differs, so its coins
/// are not authenticated against the parent transaction; its signature hash
/// commits to the amount anyway.
bool get hasBitcoinTransactionLayout => true;

/// Whether transactions below version 3 carry a timestamp after the
/// version field, as Peercoin's do.
bool get legacyTransactionTimestamp => false;

@override
AddressType? getAddressType(String address) {
try {
Expand Down
24 changes: 23 additions & 1 deletion lib/wallets/wallet/impl/litecoin_wallet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import '../../crypto_currency/crypto_currency.dart';
import '../../crypto_currency/interfaces/electrumx_currency_interface.dart';
import '../intermediate/bip39_hd_wallet.dart';
import '../intermediate/external_wallet.dart';
import '../supporting/owned_input_signatures.dart';
import '../wallet_mixin_interfaces/coin_control_interface.dart';
import '../wallet_mixin_interfaces/electrumx_interface.dart';
import '../wallet_mixin_interfaces/extended_keys_interface.dart';
Expand Down Expand Up @@ -252,6 +253,22 @@ class LitecoinWallet<T extends ElectrumXCurrencyInterface>
}
}

// A pending spend of our coins is kept, and sized, only from raw bytes
// the server proves this wallet signed. Anything else is dropped.
final height = txData["height"] as int?;
RawTransactionSize? verifiedSize;
if (wasSentFromThisWallet && (height == null || height <= 0)) {
verifiedSize = await verifyPendingSpend(
txid: txData["txid"] as String,
inputs: inputs,
outputs: outputs,
);
if (verifiedSize == null) {
await deleteUnverifiedTransaction(txData["txid"] as String);
continue;
}
}

final totalOut = outputs
.map((e) => e.value)
.fold(BigInt.zero, (value, element) => value + element);
Expand Down Expand Up @@ -328,7 +345,12 @@ class LitecoinWallet<T extends ElectrumXCurrencyInterface>
}

String? otherData;
if (txData["size"] is int || txData["vsize"] is int) {
if (verifiedSize != null) {
otherData = jsonEncode({
TxV2OdKeys.size: verifiedSize.size,
TxV2OdKeys.vSize: verifiedSize.vSize,
});
} else if (txData["size"] is int || txData["vsize"] is int) {
otherData = jsonEncode({
TxV2OdKeys.size: txData["size"] as int?,
TxV2OdKeys.vSize: txData["vsize"] as int?,
Expand Down
Loading
Loading