v6.5.0 Release Candidate (#3707)

* v6.5.0 Release Candidate
Added things:
- Account education
- Account hiding
- OnRamper fix
- iOS send report fix
- Bitbox fix
- lightning toggle fix

* v6.5.0 Release Candidate
Added things:
- Account education
- Account hiding
- OnRamper fix
- iOS send report fix
- Bitbox fix
- lightning toggle fix

* v6.5.0 Release Candidate
Added things:
- Account education
- Account hiding
- OnRamper fix
- iOS send report fix
- Bitbox fix
- lightning toggle fix

* v6.5.0 Release Candidate
Added things:
- Account education
- Account hiding
- OnRamper fix
- iOS send report fix
- Bitbox fix
- lightning toggle fix

* undo remove decred [skip ci]
This commit is contained in:
Omar Hatem
2026-10-12 14:32:16 +02:00
committed by GitHub
parent 09da422470
commit 196f5a15a9
13 changed files with 367 additions and 100 deletions
+4 -2
View File
@@ -27,6 +27,8 @@ class TronClient {
// This is an internal tracker, so we don't have to "refetch".
int _nativeTxEstimatedFee = 0;
static const _historyRequestTimeout = Duration(seconds: 30);
Future<List<TronTransactionModel>> fetchTransactions(String address,
{String? contractAddress}) async {
try {
@@ -43,7 +45,7 @@ class TronClient {
'Content-Type': 'application/json',
'TRON-PRO-API-KEY': secrets.tronGridApiKey,
},
);
).timeout(_historyRequestTimeout);
final jsonResponse = json.decode(response.body) as Map<String, dynamic>;
if (response.statusCode >= 200 &&
@@ -76,7 +78,7 @@ class TronClient {
'Content-Type': 'application/json',
'TRON-PRO-API-KEY': secrets.tronGridApiKey,
},
);
).timeout(_historyRequestTimeout);
final jsonResponse = json.decode(response.body) as Map<String, dynamic>;
if (response.statusCode >= 200 &&
+44 -16
View File
@@ -82,6 +82,8 @@ class TronTransactionModel {
String? get contractAddress => contracts?.first.parameter?.value?.contractAddress;
bool get isTrc20TransferCall => contracts?.first.parameter?.value?.isTrc20TransferCall ?? false;
TronTransactionModel({
this.ret,
this.txID,
@@ -147,29 +149,58 @@ class Value {
String? ownerAddress;
String? contractAddress;
int? amount;
int? callValue;
String? toAddress;
String? assetName;
/// Function selector of TRC20 `transfer(address,uint256)`.
static const _transferSelector = "a9059cbb";
/// Hex length of `transfer` calldata: the selector plus two 32-byte arguments.
static const _transferCallDataLength = 8 + 64 + 64;
String? get _callData {
final callData = data?.toLowerCase();
if (callData != null && callData.startsWith("0x")) {
return callData.substring(2);
}
return callData;
}
/// Only a TRC20 `transfer` carries a receiver and a token amount in its calldata.
/// Any other contract call (approve, swaps, deposit(), claim(), ...) has a different layout.
bool get isTrc20TransferCall {
final callData = _callData;
return contractAddress != null &&
callData != null &&
callData.length >= _transferCallDataLength &&
callData.startsWith(_transferSelector);
}
//Getters to extract address for tron transactions
/// If the contract address is null, it returns the toAddress
/// If it's not null, it decodes the data field and gets the receiver address.
/// For a TRC20 transfer, it decodes the data field and gets the receiver address.
/// For any other contract call, it returns the called contract.
String? get receiverAddress {
if (contractAddress == null) return toAddress;
if (data == null) return null;
if (!isTrc20TransferCall) return contractAddress;
return _decodeAddressFromEncodedDataField(data!);
return _decodeAddressFromEncodedDataField(_callData!);
}
//Getters to extract amount for tron transactions
/// If the contract address is null, it returns the amount
/// If it's not null, it decodes the data field and gets the tx amount.
/// For a TRC20 transfer, it decodes the data field and gets the token amount.
/// For any other contract call, it returns the TRX sent along with the call.
BigInt? get txAmount {
if (contractAddress == null) return BigInt.from(amount ?? 0);
if (data == null) return null;
if (!isTrc20TransferCall) return BigInt.from(callValue ?? 0);
return _decodeAmountInvolvedFromEncodedDataField(data!);
return _decodeAmountInvolvedFromEncodedDataField(_callData!);
}
Value(
@@ -177,6 +208,7 @@ class Value {
this.ownerAddress,
this.contractAddress,
this.amount,
this.callValue,
this.toAddress,
this.assetName});
@@ -185,6 +217,7 @@ class Value {
ownerAddress = json['owner_address'];
contractAddress = json['contract_address'];
amount = json['amount'];
callValue = int.tryParse(json['call_value']?.toString() ?? "");
toAddress = json['to_address'];
assetName = json['asset_name'];
}
@@ -192,7 +225,7 @@ class Value {
/// To get the address from the encoded data field
String _decodeAddressFromEncodedDataField(String output) {
// To get the receiver address from the encoded params
output = output.replaceFirst('0x', '').substring(8);
output = output.substring(8);
final abiCoder = ABICoder.fromType('address');
final decoded = abiCoder.decode(AbiParameter.bytes, hex.decode(output));
final tronAddress = TronAddress.fromEthAddress((decoded.result as ETHAddress).toBytes());
@@ -200,13 +233,8 @@ class Value {
return tronAddress.toString();
}
/// To get the amount from the encoded data field
BigInt _decodeAmountInvolvedFromEncodedDataField(String output) {
output = output.replaceFirst('0x', '').substring(72);
final amountAbiCoder = ABICoder.fromType('uint256');
final decodedA = amountAbiCoder.decode(AbiParameter.uint256, hex.decode(output));
final amount = decodedA.result as BigInt;
return amount;
}
/// To get the amount from the encoded data field.
/// Parsed directly, as on_chain's uint256 decoder rejects any value of 2^255 or more.
BigInt _decodeAmountInvolvedFromEncodedDataField(String output) =>
BigInt.parse(output.substring(72, _transferCallDataLength), radix: 16);
}
+124 -63
View File
@@ -85,6 +85,11 @@ abstract class TronWalletBase
Timer? _transactionsUpdateTimer;
bool _isTimerUpdatingHistory = false;
/// TRC20 symbol and decimals by contract address; they never change on-chain.
final Map<String, CryptoCurrency> _trc20Currencies = {};
@override
WalletAddresses walletAddresses;
@@ -305,8 +310,14 @@ abstract class TronWalletBase
return;
}
await _updateBalance();
await fetchTransactions();
fetchTrc20ExcludedTransactions();
// History is best-effort: a TronGrid or parsing failure must not fail the sync.
try {
await fetchTransactions();
fetchTrc20ExcludedTransactions();
} catch (e) {
printV("Failed to fetch the Tron transaction history: $e");
}
syncStatus = SyncedSyncStatus();
} catch (e) {
@@ -400,52 +411,44 @@ abstract class TronWalletBase
final ownerAddress = TronAddress(_tronAddress);
// Looked up once per token contract in this update, not once per transaction.
final Map<String, CryptoCurrency> trc20Currencies = {};
for (var transactionModel in transactions) {
if (transactionModel.isError) {
continue;
// A transaction that can't be parsed must not drop the rest of the history.
try {
if (transactionModel.isError) {
continue;
}
// Filter out spam transactions that involve receiving TRC10 assets transaction, we deal with TRX and TRC20 transactions
if (transactionModel.contracts?.first.type == "TransferAssetContract") {
continue;
}
// Only a TRC20 transfer moves tokens, any other contract call is a TRX transaction.
var txCurrency = currency;
if (transactionModel.isTrc20TransferCall) {
final contractAddress = transactionModel.contractAddress!;
txCurrency = trc20Currencies[contractAddress] ??=
await _getTrc20Currency(contractAddress, contract, ownerAddress);
}
result[transactionModel.hash] = TronTransactionInfo(
id: transactionModel.hash,
amount: Money(transactionModel.amount ?? BigInt.zero, txCurrency),
direction: TronAddress(transactionModel.from!, visible: false).toAddress() == address
? TransactionDirection.outgoing
: TransactionDirection.incoming,
blockTime: transactionModel.date,
fee: transactionModel.fee != null ? Money.fromInt(transactionModel.fee!, currency) : null,
to: transactionModel.to,
from: transactionModel.from,
isPending: false,
);
} catch (e) {
printV("Skipping Tron transaction ${transactionModel.txID}: $e");
}
// Filter out spam transactions that involve receiving TRC10 assets transaction, we deal with TRX and TRC20 transactions
if (transactionModel.contracts?.first.type == "TransferAssetContract") {
continue;
}
var txCurrency = currency;
if (transactionModel.contractAddress != null) {
final tokenAddress = TronAddress(transactionModel.contractAddress!);
final tokenSymbol = (await _client.getTokenDetail(
contract,
"symbol",
ownerAddress,
tokenAddress,
) as String?) ??
'';
final decimals = (await _client.getTokenDetail(
contract,
"decimals",
ownerAddress,
tokenAddress,
) as BigInt?)
?.toInt() ??
txCurrency.decimals;
txCurrency = CryptoCurrency(name: tokenSymbol, title: tokenSymbol, decimals: decimals);
}
result[transactionModel.hash] = TronTransactionInfo(
id: transactionModel.hash,
amount: Money(transactionModel.amount ?? BigInt.zero, txCurrency),
direction: TronAddress(transactionModel.from!, visible: false).toAddress() == address
? TransactionDirection.outgoing
: TransactionDirection.incoming,
blockTime: transactionModel.date,
fee: transactionModel.fee != null ? Money.fromInt(transactionModel.fee!, currency) : null,
to: transactionModel.to,
from: transactionModel.from,
isPending: false,
);
}
transactionHistory.addMany(result);
@@ -455,6 +458,47 @@ abstract class TronWalletBase
return transactionHistory.transactions;
}
Future<CryptoCurrency> _getTrc20Currency(
String contractAddress,
ContractABI contract,
TronAddress ownerAddress,
) async {
final cachedCurrency = _trc20Currencies[contractAddress];
if (cachedCurrency != null) {
return cachedCurrency;
}
final tokenAddress = TronAddress(contractAddress);
final tokenSymbol = await _client.getTokenDetail(
contract,
"symbol",
ownerAddress,
tokenAddress,
) as String?;
final decimals = (await _client.getTokenDetail(
contract,
"decimals",
ownerAddress,
tokenAddress,
) as BigInt?)
?.toInt();
final txCurrency = CryptoCurrency(
name: tokenSymbol ?? "",
title: tokenSymbol ?? "",
decimals: decimals ?? currency.decimals,
);
// Failed lookups aren't cached, so the next history update retries them.
if (tokenSymbol != null && decimals != null) {
_trc20Currencies[contractAddress] = txCurrency;
}
return txCurrency;
}
Future<void> fetchTrc20ExcludedTransactions() async {
if (!await checkIfScanProviderIsEnabled()) {
return;
@@ -467,22 +511,26 @@ abstract class TronWalletBase
final Map<String, TronTransactionInfo> result = {};
for (final transactionModel in transactions) {
if (transactionHistory.transactions.containsKey(transactionModel.hash)) {
continue;
}
try {
if (transactionHistory.transactions.containsKey(transactionModel.hash)) {
continue;
}
result[transactionModel.hash] = TronTransactionInfo(
id: transactionModel.hash,
amount: Money(transactionModel.amount ?? BigInt.zero, transactionModel.currency),
direction: transactionModel.from! == address
? TransactionDirection.outgoing
: TransactionDirection.incoming,
blockTime: transactionModel.date,
fee: transactionModel.fee != null ? Money.fromInt(transactionModel.fee!, currency) : null,
to: transactionModel.to,
from: transactionModel.from,
isPending: false,
);
result[transactionModel.hash] = TronTransactionInfo(
id: transactionModel.hash,
amount: Money(transactionModel.amount ?? BigInt.zero, transactionModel.currency),
direction: transactionModel.from! == address
? TransactionDirection.outgoing
: TransactionDirection.incoming,
blockTime: transactionModel.date,
fee: transactionModel.fee != null ? Money.fromInt(transactionModel.fee!, currency) : null,
to: transactionModel.to,
from: transactionModel.from,
isPending: false,
);
} catch (e) {
printV("Skipping Tron TRC20 transaction ${transactionModel.transactionId}: $e");
}
}
transactionHistory.addMany(result);
@@ -639,8 +687,21 @@ abstract class TronWalletBase
_transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 30), (_) async {
_updateBalance();
await fetchTransactions();
fetchTrc20ExcludedTransactions();
// A history update can outlast the period; don't stack another one on top of it.
if (_isTimerUpdatingHistory) {
return;
}
_isTimerUpdatingHistory = true;
try {
await fetchTransactions();
await fetchTrc20ExcludedTransactions();
} catch (e) {
printV("Failed to update the Tron transaction history: $e");
} finally {
_isTimerUpdatingHistory = false;
}
});
}
@@ -0,0 +1,179 @@
import "package:cw_tron/tron_transaction_model.dart";
import "package:flutter_test/flutter_test.dart";
// Wallet TCFuFnoos25WL99j9y3u92LR67mVwrTvrQ and the USDT contract TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t.
const _owner = "41191882614f605dd84703252b196c0c006b5482f8";
const _usdtContract = "41a614f803b6fd780986a42c78ec9c7f77e6ded13c";
// Real TronGrid entries of TCFuFnoos25WL99j9y3u92LR67mVwrTvrQ.
const _unlimitedApprove = {
"ret": [
{"contractRet": "SUCCESS", "fee": 9976400},
],
"txID": "fc20c9c9489842cf105c56c80e9fce540e55f1158d68459bc625332bae5b9806",
"block_timestamp": 1769113440000,
"raw_data": {
"contract": [
{
"parameter": {
"value": {
"data": "095ea7b3000000000000000000000000bbbd3bd87c268b8030bc2edae7021830af599ed2"
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"owner_address": _owner,
"contract_address": _usdtContract,
},
"type_url": "type.googleapis.com/protocol.TriggerSmartContract",
},
"type": "TriggerSmartContract",
},
],
},
};
const _usdtTransfer = {
"ret": [
{"contractRet": "SUCCESS", "fee": 6428500},
],
"txID": "0584051e5208526f48d05965a4e15c48f6ec84c23c71441ed6e38a8a49c17faf",
"block_timestamp": 1768779375000,
"raw_data": {
"contract": [
{
"parameter": {
"value": {
"data": "a9059cbb000000000000000000000000ca5125c5947162497b1b924d6c5e282282e3351f"
"00000000000000000000000000000000000000000000000000000000139c2440",
"owner_address": _owner,
"contract_address": _usdtContract,
},
"type_url": "type.googleapis.com/protocol.TriggerSmartContract",
},
"type": "TriggerSmartContract",
},
],
},
};
const _trxTransfer = {
"ret": [
{"contractRet": "SUCCESS", "fee": 0},
],
"txID": "a5bf8fb4aa16496e53ad36fcfbe3f49c92c0e0cc0caffc97d3cc2fe8a9e16ede",
"block_timestamp": 1791643617000,
"raw_data": {
"contract": [
{
"parameter": {
"value": {
"amount": 1,
"owner_address": "415576b7edd2a7cb0ebb6f794d7f2c28aa5dc85a82",
"to_address": _owner,
},
"type_url": "type.googleapis.com/protocol.TransferContract",
},
"type": "TransferContract",
},
],
},
};
TronTransactionModel _contractCall(String? data, {Object? callValue}) =>
TronTransactionModel.fromJson({
"ret": [
{"contractRet": "SUCCESS", "fee": 1},
],
"txID": "test",
"block_timestamp": 1,
"raw_data": {
"contract": [
{
"parameter": {
"value": {
if (data != null) "data": data,
if (callValue != null) "call_value": callValue,
"owner_address": _owner,
"contract_address": _usdtContract,
},
},
"type": "TriggerSmartContract",
},
],
},
});
String _word(String hex) => hex.padLeft(64, "0");
void main() {
group("TronTransactionModel", () {
test("decodes the receiver and token amount of a TRC20 transfer", () {
final transaction = TronTransactionModel.fromJson(_usdtTransfer);
expect(transaction.isTrc20TransferCall, isTrue);
expect(transaction.amount, BigInt.from(329000000));
expect(transaction.to, "TUQxi3DkhbrL9qL5EHEfAmkZbsPBGP1UnH");
expect(transaction.from, _owner);
expect(transaction.contractAddress, _usdtContract);
});
test("reads an unlimited approve as a TRX call to the contract instead of throwing", () {
final transaction = TronTransactionModel.fromJson(_unlimitedApprove);
expect(transaction.isError, isFalse);
expect(transaction.isTrc20TransferCall, isFalse);
expect(transaction.amount, BigInt.zero);
expect(transaction.to, _usdtContract);
expect(transaction.fee, 9976400);
});
test("decodes transfer amounts of 2^255 and above", () {
final maxUint256 = (BigInt.one << 256) - BigInt.one;
final transaction = _contractCall(
"a9059cbb${_word("ca5125c5947162497b1b924d6c5e282282e3351f")}${"f" * 64}",
);
expect(transaction.isTrc20TransferCall, isTrue);
expect(transaction.amount, maxUint256);
});
test("decodes 0x-prefixed, upper-case transfer calldata", () {
final transaction = _contractCall(
"0xA9059CBB${_word("CA5125C5947162497B1B924D6C5E282282E3351F")}${_word("139C2440")}",
);
expect(transaction.isTrc20TransferCall, isTrue);
expect(transaction.amount, BigInt.from(329000000));
expect(transaction.to, "TUQxi3DkhbrL9qL5EHEfAmkZbsPBGP1UnH");
});
test("reads other contract calls as the TRX sent with them", () {
final deposit = _contractCall("d0e30db0", callValue: 5000000);
final withdraw = _contractCall("2e1a7d4d${_word("3e8")}");
final noCalldata = _contractCall(null, callValue: 7);
final truncatedTransfer =
_contractCall("a9059cbb${_word("ca5125c5947162497b1b924d6c5e282282e3351f")}");
for (final transaction in [deposit, withdraw, noCalldata, truncatedTransfer]) {
expect(transaction.isTrc20TransferCall, isFalse);
expect(transaction.to, _usdtContract);
}
expect(deposit.amount, BigInt.from(5000000));
expect(withdraw.amount, BigInt.zero);
expect(noCalldata.amount, BigInt.from(7));
expect(truncatedTransfer.amount, BigInt.zero);
});
test("parses call_value sent as a string", () {
expect(_contractCall("d0e30db0", callValue: "42").amount, BigInt.from(42));
});
test("keeps TRX transfers unchanged", () {
final transaction = TronTransactionModel.fromJson(_trxTransfer);
expect(transaction.isTrc20TransferCall, isFalse);
expect(transaction.contractAddress, isNull);
expect(transaction.amount, BigInt.one);
expect(transaction.to, _owner);
expect(transaction.from, "415576b7edd2a7cb0ebb6f794d7f2c28aa5dc85a82");
});
});
}
+3 -2
View File
@@ -924,9 +924,10 @@ class CWBitcoin extends Bitcoin {
}
@override
Balance balanceForAccount(Object wallet, int accountIndex) {
Money balanceForAccount(Object wallet, int accountIndex) {
final bitcoinWallet = wallet as ElectrumWallet;
return bitcoinWallet.balanceForAccount(accountIndex);
final balance = bitcoinWallet.balanceForAccount(accountIndex);
return balance.confirmed + balance.unconfirmed;
}
@override
@@ -1727,7 +1727,6 @@ abstract class DashboardViewModelBase with Store {
static const walletTypesToBeRemoved = [
WalletType.zano,
WalletType.decred,
];
Future<bool> shouldShowRemovalPopup() async {
@@ -303,7 +303,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
ChainflipExchangeProvider(),
if (FeatureFlag.isExolixEnabled) ExolixExchangeProvider(),
SwapTradeExchangeProvider(),
LetsExchangeExchangeProvider(),
// LetsExchangeExchangeProvider(),
// StealthExExchangeProvider(),
XOSwapExchangeProvider(),
SwapsXyzExchangeProvider(),
@@ -99,10 +99,7 @@ abstract class BitcoinAccountListViewModelBase with Store implements WalletAccou
});
}
Money _fullBalance(int accountIndex) {
final balance = bitcoin!.balanceForAccount(_wallet, accountIndex);
return balance.confirmed + balance.unconfirmed;
}
Money _fullBalance(int accountIndex) => bitcoin!.balanceForAccount(_wallet, accountIndex);
@override
@action
+2 -2
View File
@@ -15,14 +15,14 @@ APP_ANDROID_TYPE=$1
MONERO_COM_NAME="Monero.com"
MONERO_COM_VERSION="6.5.0"
MONERO_COM_BUILD_NUMBER=4170
MONERO_COM_BUILD_NUMBER=4171
MONERO_COM_BUNDLE_ID="com.monero.app"
MONERO_COM_PACKAGE="com.monero.app"
MONERO_COM_SCHEME="monero.com"
CAKEWALLET_NAME="Cake Wallet"
CAKEWALLET_VERSION="6.5.0"
CAKEWALLET_BUILD_NUMBER=4426
CAKEWALLET_BUILD_NUMBER=4428
CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet"
CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet"
CAKEWALLET_SCHEME="cakewallet"
+2 -2
View File
@@ -13,12 +13,12 @@ APP_IOS_TYPE=$1
MONERO_COM_NAME="Monero.com"
MONERO_COM_VERSION="6.5.0"
MONERO_COM_BUILD_NUMBER=177
MONERO_COM_BUILD_NUMBER=178
MONERO_COM_BUNDLE_ID="com.cakewallet.monero"
CAKEWALLET_NAME="Cake Wallet"
CAKEWALLET_VERSION="6.5.0"
CAKEWALLET_BUILD_NUMBER=446
CAKEWALLET_BUILD_NUMBER=448
CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet"
+1 -1
View File
@@ -15,7 +15,7 @@ fi
CAKEWALLET_NAME="Cake Wallet"
CAKEWALLET_VERSION="6.5.0"
CAKEWALLET_BUILD_NUMBER=83
CAKEWALLET_BUILD_NUMBER=84
if ! [[ " ${TYPES[*]} " =~ " ${APP_LINUX_TYPE} " ]]; then
echo "Wrong app type."
+2 -2
View File
@@ -17,12 +17,12 @@ fi
MONERO_COM_NAME="Monero.com"
MONERO_COM_VERSION="6.5.0"
MONERO_COM_BUILD_NUMBER=86
MONERO_COM_BUILD_NUMBER=87
MONERO_COM_BUNDLE_ID="com.cakewallet.monero"
CAKEWALLET_NAME="Cake Wallet"
CAKEWALLET_VERSION="6.5.0"
CAKEWALLET_BUILD_NUMBER=154
CAKEWALLET_BUILD_NUMBER=155
CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet"
if ! [[ " ${TYPES[*]} " =~ " ${APP_MACOS_TYPE} " ]]; then
+4 -4
View File
@@ -308,7 +308,7 @@ abstract class Bitcoin {
Future<String?> getLightningInvoice(Object wallet, BigInt amount);
String? getBreezSdkError(Object exception);
Future<Account> getCurrentAccount(Object wallet);
Balance balanceForAccount(Object wallet, int accountIndex);
Money balanceForAccount(Object wallet, int accountIndex);
Map<int, Object> accountBalancesSnapshot(Object wallet);
Future<void> setCurrentAccount(Object wallet, int accountIndex);
List<TransactionInfo> getCurrentAccountBitcoinTransactions(Object wallet);
@@ -2092,9 +2092,9 @@ Future<void> generateWalletTypes({
outputContent += '\tWalletType.nano,\n';
}
// if (hasDecred) {
// outputContent += '\tWalletType.decred,\n';
// }
if (hasDecred) {
outputContent += '\tWalletType.decred,\n';
}
// if (hasZano) {
// outputContent += '\tWalletType.zano,\n';