mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-26 06:08:40 +01:00
Merge branch 'cake-tech:dev' into dev
This commit is contained in:
@@ -176,16 +176,18 @@ jobs:
|
||||
|
||||
- name: Rename apk file
|
||||
run: |
|
||||
sanitized_branch_name=$(grep '^id=' android/app.properties | cut -d'=' -f2 | sed 's/com\.cakewallet\.test_//')
|
||||
cd build/app/outputs/flutter-apk
|
||||
mkdir test-apk
|
||||
cp app-arm64-v8a-release.apk test-apk/${BRANCH_NAME}.apk
|
||||
cp app-x86_64-release.apk test-apk/${BRANCH_NAME}_x86.apk
|
||||
cp app-arm64-v8a-release.apk test-apk/${sanitized_branch_name}.apk
|
||||
cp app-x86_64-release.apk test-apk/${sanitized_branch_name}_x86.apk
|
||||
|
||||
- name: Find APK file
|
||||
id: find_apk
|
||||
run: |
|
||||
set -x
|
||||
apk_file=$(ls build/app/outputs/flutter-apk/test-apk/${BRANCH_NAME}.apk || exit 1)
|
||||
sanitized_branch_name=$(grep '^id=' android/app.properties | cut -d'=' -f2 | sed 's/com\.cakewallet\.test_//')
|
||||
apk_file=$(ls build/app/outputs/flutter-apk/test-apk/${sanitized_branch_name}.apk || exit 1)
|
||||
echo "APK_FILE=$apk_file" >> $GITHUB_ENV
|
||||
- name: Write BRANCH_NAME to env
|
||||
run: echo "BRANCH_NAME=${BRANCH_NAME}" >> $GITHUB_ENV
|
||||
|
||||
@@ -17,6 +17,7 @@ import 'package:cw_bitcoin/electrum_wallet_snapshot.dart';
|
||||
import 'package:cw_bitcoin/hardware/bitcoin_hardware_wallet_service.dart';
|
||||
import 'package:cw_bitcoin/lightning/lightning_wallet.dart';
|
||||
import 'package:cw_bitcoin/hardware/bitcoin_ledger_service.dart';
|
||||
import 'package:cw_bitcoin/output_ordering.dart';
|
||||
import 'package:cw_bitcoin/payjoin/manager.dart';
|
||||
import 'package:cw_bitcoin/payjoin/storage.dart';
|
||||
import 'package:cw_bitcoin/pending_bitcoin_transaction.dart';
|
||||
@@ -467,8 +468,10 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
||||
final masterFingerprint =
|
||||
await (hardwareWalletService as BitcoinHardwareWalletService).getMasterFingerprint();
|
||||
|
||||
final orderedOutputs = orderOutputs(outputs, outputOrdering);
|
||||
|
||||
final psbt = await buildPsbt(
|
||||
outputs: outputs,
|
||||
outputs: orderedOutputs,
|
||||
fee: fee,
|
||||
network: network,
|
||||
utxos: utxos,
|
||||
@@ -478,7 +481,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
||||
memo: memo,
|
||||
enableRBF: enableRBF,
|
||||
inputOrdering: inputOrdering,
|
||||
outputOrdering: outputOrdering,
|
||||
// Already applied above; don't reorder again.
|
||||
outputOrdering: BitcoinOrdering.none,
|
||||
);
|
||||
|
||||
final psbtStr = base64Encode(psbt.serialize());
|
||||
|
||||
@@ -1485,7 +1485,9 @@ abstract class ElectrumWalletBase
|
||||
fee: estimatedTx.fee.amount,
|
||||
network: network,
|
||||
memo: estimatedTx.memo,
|
||||
outputOrdering: BitcoinOrdering.none,
|
||||
// Shuffle so the change output isn't placed deterministically last
|
||||
// (privacy fingerprint). Applied by orderOutputs in the builder.
|
||||
outputOrdering: BitcoinOrdering.shuffle,
|
||||
enableRBF: true,
|
||||
cwOutputs: transactionCredentials.outputs,
|
||||
);
|
||||
@@ -1518,7 +1520,10 @@ abstract class ElectrumWalletBase
|
||||
fee: estimatedTx.fee.amount,
|
||||
network: network,
|
||||
memo: estimatedTx.memo,
|
||||
outputOrdering: BitcoinOrdering.none,
|
||||
// Shuffle so the change output isn't placed deterministically last
|
||||
// (privacy fingerprint). Change is found by isChange, not position.
|
||||
inputOrdering: BitcoinOrdering.shuffle,
|
||||
outputOrdering: BitcoinOrdering.shuffle,
|
||||
enableRBF: !estimatedTx.spendsUnconfirmedTX,
|
||||
);
|
||||
} else {
|
||||
@@ -2507,10 +2512,16 @@ abstract class ElectrumWalletBase
|
||||
Map<String, ElectrumTransactionInfo> historiesWithDetails,
|
||||
BitcoinAddressType type,
|
||||
) async {
|
||||
final addressesByType = walletAddresses.allAddresses.where((addr) => addr.type == type);
|
||||
final hiddenAddresses = addressesByType.where((addr) => addr.isHidden == true);
|
||||
final receiveAddresses = addressesByType.where((addr) => addr.isHidden == false);
|
||||
walletAddresses.hiddenAddresses.addAll(hiddenAddresses.map((e) => e.address));
|
||||
|
||||
final addressesByType =
|
||||
walletAddresses.allAddresses.where((addr) => addr.type == type).toList();
|
||||
|
||||
final receiveStandard = getAddressBranchByType(hidden: false, legacy: false, type: type);
|
||||
final changeStandard = getAddressBranchByType(hidden: true, legacy: false, type: type);
|
||||
final receiveLegacy = getAddressBranchByType(hidden: false, legacy: true, type: type);
|
||||
final changeLegacy = getAddressBranchByType(hidden: true, legacy: true, type: type);
|
||||
|
||||
walletAddresses.hiddenAddresses.addAll([...changeStandard, ...changeLegacy].map((e) => e.address));
|
||||
await walletAddresses.saveAddressesInBox();
|
||||
await Future.wait(addressesByType.map((addressRecord) async {
|
||||
final history = await _fetchAddressHistory(addressRecord, await getCurrentChainTip());
|
||||
@@ -2519,7 +2530,9 @@ abstract class ElectrumWalletBase
|
||||
addressRecord.txCount = history.length;
|
||||
historiesWithDetails.addAll(history);
|
||||
|
||||
final matchedAddresses = addressRecord.isHidden ? hiddenAddresses : receiveAddresses;
|
||||
final matchedAddresses = addressRecord.isHidden
|
||||
? (addressRecord.isLegacyDerivation ? changeLegacy : changeStandard)
|
||||
: (addressRecord.isLegacyDerivation ? receiveLegacy : receiveStandard);
|
||||
final isUsedAddressAboveGap = matchedAddresses.toList().indexOf(addressRecord) >=
|
||||
matchedAddresses.length -
|
||||
(addressRecord.isHidden
|
||||
@@ -2623,18 +2636,19 @@ abstract class ElectrumWalletBase
|
||||
|
||||
Future<void> fetchTransactionsForAddressTypeBatch(
|
||||
Map<String, ElectrumTransactionInfo> historiesWithDetails, BitcoinAddressType type) async {
|
||||
final addressesByType =
|
||||
walletAddresses.allAddresses.where((addr) => addr.type == type).toList();
|
||||
final receiveAddresses = addressesByType.where((addr) => !addr.isHidden).toList();
|
||||
final hiddenAddresses = addressesByType.where((addr) => addr.isHidden).toList();
|
||||
|
||||
walletAddresses.hiddenAddresses.addAll(hiddenAddresses.map((e) => e.address));
|
||||
final receiveStandard = getAddressBranchByType(hidden: false, legacy: false, type: type);
|
||||
final changeStandard = getAddressBranchByType(hidden: true, legacy: false, type: type);
|
||||
final receiveLegacy = getAddressBranchByType(hidden: false, legacy: true, type: type);
|
||||
final changeLegacy = getAddressBranchByType(hidden: true, legacy: true, type: type);
|
||||
|
||||
walletAddresses.hiddenAddresses.addAll([...changeStandard, ...changeLegacy].map((e) => e.address));
|
||||
await walletAddresses.saveAddressesInBox();
|
||||
|
||||
await fetchTransactionsForAddressesBranchBatch(
|
||||
historiesWithDetails,
|
||||
type,
|
||||
receiveAddresses,
|
||||
receiveStandard,
|
||||
isHidden: false,
|
||||
isLegacyDerivation: false,
|
||||
);
|
||||
@@ -2642,7 +2656,7 @@ abstract class ElectrumWalletBase
|
||||
await fetchTransactionsForAddressesBranchBatch(
|
||||
historiesWithDetails,
|
||||
type,
|
||||
hiddenAddresses,
|
||||
changeStandard,
|
||||
isHidden: true,
|
||||
isLegacyDerivation: false,
|
||||
);
|
||||
@@ -2650,7 +2664,7 @@ abstract class ElectrumWalletBase
|
||||
await fetchTransactionsForAddressesBranchBatch(
|
||||
historiesWithDetails,
|
||||
type,
|
||||
receiveAddresses,
|
||||
receiveLegacy,
|
||||
isHidden: false,
|
||||
isLegacyDerivation: true,
|
||||
);
|
||||
@@ -2658,7 +2672,7 @@ abstract class ElectrumWalletBase
|
||||
await fetchTransactionsForAddressesBranchBatch(
|
||||
historiesWithDetails,
|
||||
type,
|
||||
hiddenAddresses,
|
||||
changeLegacy,
|
||||
isHidden: true,
|
||||
isLegacyDerivation: true,
|
||||
);
|
||||
@@ -2734,6 +2748,11 @@ abstract class ElectrumWalletBase
|
||||
}
|
||||
}
|
||||
|
||||
List<BitcoinAddressRecord> getAddressBranchByType({required bool hidden, required bool legacy, required BitcoinAddressType
|
||||
type}) => walletAddresses.allAddresses.where((addr) => addr.type == type && addr.isHidden == hidden && addr.isLegacyDerivation == legacy)
|
||||
.toList()
|
||||
..sort((a, b) => a.index.compareTo(b.index));
|
||||
|
||||
int _highestUsedIndex(List<BitcoinAddressRecord> addresses) {
|
||||
for (int i = addresses.length - 1; i >= 0; i--) {
|
||||
if (addresses[i].isUsed) return i;
|
||||
|
||||
@@ -30,6 +30,7 @@ import 'package:cw_bitcoin/electrum_wallet.dart';
|
||||
import 'package:cw_bitcoin/electrum_wallet_snapshot.dart';
|
||||
import 'package:cw_bitcoin/hardware/bitcoin_hardware_wallet_service.dart';
|
||||
import 'package:cw_bitcoin/litecoin_wallet_addresses.dart';
|
||||
import 'package:cw_bitcoin/output_ordering.dart';
|
||||
import 'package:cw_bitcoin/pending_bitcoin_transaction.dart';
|
||||
import 'package:cw_bitcoin/psbt/transaction_builder.dart';
|
||||
import 'package:cw_bitcoin/utils.dart';
|
||||
@@ -1584,8 +1585,11 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
|
||||
));
|
||||
}
|
||||
|
||||
final orderedOutputs = orderOutputs(outputs, outputOrdering);
|
||||
|
||||
final rawHex = await (hardwareWalletService as LitecoinHardwareWalletService)
|
||||
.signLitecoinTransaction(outputs: outputs, inputs: readyInputs, publicKeys: publicKeys);
|
||||
.signLitecoinTransaction(
|
||||
outputs: orderedOutputs, inputs: readyInputs, publicKeys: publicKeys);
|
||||
|
||||
return BtcTransaction.fromRaw(rawHex);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
|
||||
/// Applies [ordering] to a transaction's [outputs] before building.
|
||||
///
|
||||
/// Returns a new list; the input is never mutated. Only [BitcoinOrdering.shuffle]
|
||||
/// reorders (using a secure RNG by default) so the change output is no longer
|
||||
/// placed deterministically last. Any other value preserves the given order,
|
||||
/// matching the pre-existing hardware-wallet behavior.
|
||||
List<T> orderOutputs<T>(
|
||||
List<T> outputs,
|
||||
BitcoinOrdering ordering, {
|
||||
Random? rng,
|
||||
}) {
|
||||
final result = List<T>.of(outputs);
|
||||
if (ordering == BitcoinOrdering.shuffle) {
|
||||
result.shuffle(rng ?? Random.secure());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:cw_bitcoin/output_ordering.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('orderOutputs', () {
|
||||
test('none preserves the given order', () {
|
||||
final outputs = [0, 1, 2, 3, 4];
|
||||
expect(orderOutputs(outputs, BitcoinOrdering.none), outputs);
|
||||
});
|
||||
|
||||
test('does not mutate the input list', () {
|
||||
final outputs = [0, 1, 2, 3, 4];
|
||||
orderOutputs(outputs, BitcoinOrdering.shuffle, rng: Random(1));
|
||||
expect(outputs, [0, 1, 2, 3, 4]);
|
||||
});
|
||||
|
||||
test('returns a new list instance', () {
|
||||
final outputs = [0, 1, 2];
|
||||
expect(identical(orderOutputs(outputs, BitcoinOrdering.none), outputs), isFalse);
|
||||
});
|
||||
|
||||
test('shuffle preserves the multiset of outputs', () {
|
||||
final outputs = [0, 1, 2, 3, 4, 5, 6, 7];
|
||||
final shuffled = orderOutputs(outputs, BitcoinOrdering.shuffle, rng: Random(7));
|
||||
expect(shuffled.length, outputs.length);
|
||||
expect(shuffled.toSet(), outputs.toSet());
|
||||
});
|
||||
|
||||
test('shuffle is deterministic for a given seed', () {
|
||||
final outputs = [0, 1, 2, 3, 4, 5];
|
||||
final a = orderOutputs(outputs, BitcoinOrdering.shuffle, rng: Random(42));
|
||||
final b = orderOutputs(outputs, BitcoinOrdering.shuffle, rng: Random(42));
|
||||
expect(a, b);
|
||||
});
|
||||
|
||||
test('shuffle does not keep the change output deterministically last', () {
|
||||
// Last element (99) models the change output, appended last today.
|
||||
final outputs = [0, 1, 2, 3, 99];
|
||||
final changePositions = <int>{};
|
||||
for (var seed = 0; seed < 50; seed++) {
|
||||
final shuffled = orderOutputs(outputs, BitcoinOrdering.shuffle, rng: Random(seed));
|
||||
changePositions.add(shuffled.indexOf(99));
|
||||
}
|
||||
// Across seeds the change lands in more than one position, and not always last.
|
||||
expect(changePositions.length, greaterThan(1));
|
||||
expect(changePositions, isNot(equals({outputs.length - 1})));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -20,7 +20,7 @@ class YatAddressProvider extends AddressLookupProvider {
|
||||
bool canHandle(String q) => q.hasOnlyEmojis; // Yat handle example: 🐶🐾
|
||||
|
||||
@override
|
||||
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsWellKnown;
|
||||
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsYatService;
|
||||
|
||||
@override
|
||||
Future<List<ParsedAddress>> resolve({
|
||||
|
||||
@@ -15,7 +15,7 @@ class ZcashMeAddressProvider extends AddressLookupProvider {
|
||||
List<CryptoCurrency> get supportedCurrencies => [CryptoCurrency.zec];
|
||||
|
||||
@override
|
||||
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsZcashNames;
|
||||
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsZcashAddress;
|
||||
|
||||
@override
|
||||
bool canHandle(String query) {
|
||||
|
||||
@@ -628,6 +628,13 @@ Future<void> defaultSettingsMigration(
|
||||
oldUri: ['nodes.hashvault.pro:18081'],
|
||||
);
|
||||
break;
|
||||
case 69:
|
||||
_changeExchangeProviderAvailability(
|
||||
sharedPreferences,
|
||||
providerName: "Exolix",
|
||||
enabled: false,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -325,6 +325,8 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
|
||||
return 'btc';
|
||||
case CryptoCurrency.arb:
|
||||
return 'arbitrum';
|
||||
case CryptoCurrency.nano:
|
||||
return 'nano';
|
||||
default:
|
||||
return currency.tag != null ? _normalizeTag(currency.tag!) : currency.title.toLowerCase();
|
||||
}
|
||||
@@ -337,6 +339,8 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
|
||||
switch (currency) {
|
||||
case CryptoCurrency.zec:
|
||||
return 'zec';
|
||||
case CryptoCurrency.maticpoly:
|
||||
return 'matic';
|
||||
default:
|
||||
return currency.title.toLowerCase();
|
||||
}
|
||||
@@ -344,7 +348,7 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
|
||||
|
||||
String _normalizeTag(String tag) {
|
||||
switch (tag) {
|
||||
case 'POLY':
|
||||
case 'POL':
|
||||
return 'matic';
|
||||
case 'LN':
|
||||
return 'lightning';
|
||||
|
||||
+1
-1
@@ -315,7 +315,7 @@ Future<void> initializeAppConfigs({bool loadWallet = true}) async {
|
||||
payjoinSessionSource: payjoinSessionSource,
|
||||
anonpayInvoiceInfo: anonpayInvoiceInfo,
|
||||
havenSeedStore: havenSeedStore,
|
||||
initialMigrationVersion: 68,
|
||||
initialMigrationVersion: 69,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ abstract class SupportViewModelBase with Store {
|
||||
LinkListItem(
|
||||
title: 'Telegram',
|
||||
icon: 'assets/images/Telegram.png',
|
||||
linkTitle: 't.me/cakewallet',
|
||||
linkTitle: 't.me/cakewalletannouncements',
|
||||
link: 'https://t.me/cakewalletannouncements'),
|
||||
LinkListItem(
|
||||
title: 'Telegram Support Bot',
|
||||
@@ -75,7 +75,7 @@ abstract class SupportViewModelBase with Store {
|
||||
title: 'SwapTrade',
|
||||
icon: 'assets/images/swap_trade.png',
|
||||
linkTitle: 'help.swaptrade.io',
|
||||
link: 'mailto:support@exolix.com'),
|
||||
link: 'https://help.swaptrade.io/contact/'),
|
||||
LinkListItem(
|
||||
title: 'Trocador',
|
||||
icon: 'assets/images/trocador.png',
|
||||
|
||||
@@ -947,7 +947,7 @@
|
||||
"seed_alert_yes": "Yes, I have",
|
||||
"seed_and_keys": "Seed & Keys",
|
||||
"seed_choose": "Choose seed language",
|
||||
"seed_display_path": "Menu → Security and Backup → Show key/seeds",
|
||||
"seed_display_path": "Settings → Wallet settings → Seed & Keys",
|
||||
"seed_hex_form": "Wallet seed (hex form)",
|
||||
"seed_key": "Seed key",
|
||||
"seed_language": "Seed language",
|
||||
|
||||
Reference in New Issue
Block a user