From c50d0fc6dd0299443f046bef6eb0197f859d841e Mon Sep 17 00:00:00 2001 From: sethforprivacy <40500387+sethforprivacy@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:56:21 -0400 Subject: [PATCH 1/8] ci: sanitize branch name when naming test APK in reusable-build The 'Rename apk file'/'Find APK file' steps used the raw ${BRANCH_NAME} as an APK filename, so any branch containing a '/' (e.g. claude/*) made cp target a non-existent test-apk// subdirectory and failed the build. Derive the sanitized branch name from android/app.properties, the same approach already used in automated_integration_test.yml. --- .github/workflows/reusable-build.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/reusable-build.yml b/.github/workflows/reusable-build.yml index c6082d4e0..269619cf3 100644 --- a/.github/workflows/reusable-build.yml +++ b/.github/workflows/reusable-build.yml @@ -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 From 6641549d6489927e5167394e13af1b914b8c1b85 Mon Sep 17 00:00:00 2001 From: Seth For Privacy <40500387+sethforprivacy@users.noreply.github.com> Date: Fri, 24 Jul 2026 19:28:41 -0400 Subject: [PATCH 2/8] Fix mismatched support links for SwapTrade and Telegram (#3441) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two entries in SupportViewModel's link list displayed one destination but opened another. SwapTrade pointed at mailto:support@exolix.com, so a user with a SwapTrade trade problem emailed Exolix — who cannot help, and who absorbed another provider's support traffic. The entry was added as Quantex with Exolix's mailto already copy-pasted in from the entry above it; the Quantex -> SwapTrade rename updated the title, icon and linkTitle but carried the wrong link forward. Point it at https://help.swaptrade.io/contact/, which matches what docs.cakewallet.com/support/swap lists for SwapTrade. Live chat is the only contact method that help center publishes — there is no SwapTrade support email — so the mailto form is not an option. Using a path below the displayed host follows the existing SideShift entry in this file. Telegram had the inverse problem: linkTitle read t.me/cakewallet while the link went to the announcements channel. Here the link was correct — t.me/cakewallet is a group that self-describes as the inactive Cake Wallet Community and directs visitors to the announcements channel — so the stale label was updated to match the destination rather than the reverse. The other 15 entries were audited and are consistent. --- lib/view_model/support_view_model.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/view_model/support_view_model.dart b/lib/view_model/support_view_model.dart index 96cf66fb9..4f1120158 100644 --- a/lib/view_model/support_view_model.dart +++ b/lib/view_model/support_view_model.dart @@ -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', From 1a0c3e0d6b5c0c1bad90cdd4bef1c2e118015886 Mon Sep 17 00:00:00 2001 From: Seth For Privacy <40500387+sethforprivacy@users.noreply.github.com> Date: Fri, 24 Jul 2026 19:29:14 -0400 Subject: [PATCH 3/8] Fix stale seed location path in seed_display_path (#3440) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The string still described the old UI navigation ("Menu → Security and Backup → Show key/seeds"). With FeatureFlag.hasNewUi there is no Menu, and the Show seed/keys row is commented out of the Security page. Seeds and keys now live at Settings → Wallet settings → Seed & Keys. Rendered in two places, both reachable: - seed_verification_success_view.dart:54 - wallet_group_existing_seed_description_page.dart:57 --- res/values/strings_en.arb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 7a5bbfc29..f1ab85bee 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -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", From 7f090c3a3a7c01a009d2e82651df8c88c8a23754 Mon Sep 17 00:00:00 2001 From: Seth For Privacy <40500387+sethforprivacy@users.noreply.github.com> Date: Fri, 24 Jul 2026 19:43:06 -0400 Subject: [PATCH 4/8] Fix domain lookup toggles for Yat and Zcash.me providers (#3439) Both providers checked the wrong SettingsStore flag in isEnabled(), so their toggles in Settings > Connection and sync > Domain lookups had no effect: - YatAddressProvider read lookupsWellKnown instead of lookupsYatService, so the 'Yat' toggle did nothing and '.wellknown' unexpectedly gated it. - ZcashMeAddressProvider read lookupsZcashNames instead of lookupsZcashAddress, so the 'Zcash.me' toggle did nothing and it was gated by the 'Zcash Names' toggle. Each provider now reads the flag its own toggle writes, matching the mapping in ConnectionSyncViewModel.lookupValue/setLookupValue. --- lib/core/address_resolver/yat/yat_address_provider.dart | 2 +- lib/core/address_resolver/zcash/zcash_me_address_provider.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/core/address_resolver/yat/yat_address_provider.dart b/lib/core/address_resolver/yat/yat_address_provider.dart index d8ee3454b..73363fa1a 100644 --- a/lib/core/address_resolver/yat/yat_address_provider.dart +++ b/lib/core/address_resolver/yat/yat_address_provider.dart @@ -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> resolve({ diff --git a/lib/core/address_resolver/zcash/zcash_me_address_provider.dart b/lib/core/address_resolver/zcash/zcash_me_address_provider.dart index 6dd0968d9..3836ee46f 100644 --- a/lib/core/address_resolver/zcash/zcash_me_address_provider.dart +++ b/lib/core/address_resolver/zcash/zcash_me_address_provider.dart @@ -15,7 +15,7 @@ class ZcashMeAddressProvider extends AddressLookupProvider { List get supportedCurrencies => [CryptoCurrency.zec]; @override - bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsZcashNames; + bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsZcashAddress; @override bool canHandle(String query) { From 24a884b390595d8b39b0aab96e95d75e317b207e Mon Sep 17 00:00:00 2001 From: Serhii <17529954+serhii-bor@users.noreply.github.com> Date: Sun, 26 Jul 2026 01:52:41 +0300 Subject: [PATCH 5/8] fix btc address lookup (#3422) --- cw_bitcoin/lib/electrum_wallet.dart | 42 +++++++++++++++++++---------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 641feb673..6396abedb 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -2507,10 +2507,16 @@ abstract class ElectrumWalletBase Map 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 +2525,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 +2631,19 @@ abstract class ElectrumWalletBase Future fetchTransactionsForAddressTypeBatch( Map 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 +2651,7 @@ abstract class ElectrumWalletBase await fetchTransactionsForAddressesBranchBatch( historiesWithDetails, type, - hiddenAddresses, + changeStandard, isHidden: true, isLegacyDerivation: false, ); @@ -2650,7 +2659,7 @@ abstract class ElectrumWalletBase await fetchTransactionsForAddressesBranchBatch( historiesWithDetails, type, - receiveAddresses, + receiveLegacy, isHidden: false, isLegacyDerivation: true, ); @@ -2658,7 +2667,7 @@ abstract class ElectrumWalletBase await fetchTransactionsForAddressesBranchBatch( historiesWithDetails, type, - hiddenAddresses, + changeLegacy, isHidden: true, isLegacyDerivation: true, ); @@ -2734,6 +2743,11 @@ abstract class ElectrumWalletBase } } + List 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 addresses) { for (int i = addresses.length - 1; i >= 0; i--) { if (addresses[i].isUsed) return i; From 037c21e638aebe37f539fd06154975f9db68eb9c Mon Sep 17 00:00:00 2001 From: Serhii <17529954+serhii-bor@users.noreply.github.com> Date: Sun, 26 Jul 2026 01:52:59 +0300 Subject: [PATCH 6/8] Update changenow_exchange_provider.dart (#3416) --- lib/exchange/provider/changenow_exchange_provider.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/exchange/provider/changenow_exchange_provider.dart b/lib/exchange/provider/changenow_exchange_provider.dart index 15913b29e..e6c94d559 100644 --- a/lib/exchange/provider/changenow_exchange_provider.dart +++ b/lib/exchange/provider/changenow_exchange_provider.dart @@ -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'; From 6c54a4ee0039a08a9f912cddd050cc8a7d86051e Mon Sep 17 00:00:00 2001 From: Omar Date: Sun, 26 Jul 2026 02:20:03 +0200 Subject: [PATCH 7/8] disable exolix [skip ci] --- lib/entities/default_settings_migration.dart | 7 +++++++ lib/main.dart | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index ef8a213c8..c4011172c 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -628,6 +628,13 @@ Future defaultSettingsMigration( oldUri: ['nodes.hashvault.pro:18081'], ); break; + case 69: + _changeExchangeProviderAvailability( + sharedPreferences, + providerName: "Exolix", + enabled: false, + ); + break; default: break; } diff --git a/lib/main.dart b/lib/main.dart index f292731f8..923ace77d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -315,7 +315,7 @@ Future initializeAppConfigs({bool loadWallet = true}) async { payjoinSessionSource: payjoinSessionSource, anonpayInvoiceInfo: anonpayInvoiceInfo, havenSeedStore: havenSeedStore, - initialMigrationVersion: 68, + initialMigrationVersion: 69, ); } From b73c46ac59af0f3902be0f94b771a98045698531 Mon Sep 17 00:00:00 2001 From: Cindy Date: Sat, 25 Jul 2026 21:30:41 -0300 Subject: [PATCH 8/8] Shuffle change output on hardware wallet and Bitcoin Cash sends (#3432) * Shuffle change output on hardware wallet sends Hardware wallet BTC/LTC sends passed outputs to the PSBT/device in the given order (change last), a position fingerprint. The outputOrdering param was plumbed but ignored. Add orderOutputs() and apply it in both buildHardwareWalletTransaction paths; set the send call site to shuffle. Refs #3376 (the software send + RBF paths were covered by #3420). * Shuffle change output on Bitcoin Cash sends The BCH send path built via ForkedTransactionBuilder with outputOrdering: none, leaving change deterministically last. The builder shuffles natively and change is found by isChange, not position, so flip it to shuffle (matches the BTC software path from #3420). Refs #3376. * Update cw_bitcoin/lib/electrum_wallet.dart Co-authored-by: Omar Hatem --------- Co-authored-by: Omar Hatem --- cw_bitcoin/lib/bitcoin_wallet.dart | 8 +++- cw_bitcoin/lib/electrum_wallet.dart | 9 +++- cw_bitcoin/lib/litecoin_wallet.dart | 6 ++- cw_bitcoin/lib/output_ordering.dart | 21 +++++++++ cw_bitcoin/test/output_ordering_test.dart | 52 +++++++++++++++++++++++ 5 files changed, 91 insertions(+), 5 deletions(-) create mode 100644 cw_bitcoin/lib/output_ordering.dart create mode 100644 cw_bitcoin/test/output_ordering_test.dart diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 7e76132a8..b8e53d11b 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -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()); diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 6396abedb..ceba0eccd 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -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 { diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 23959374c..aa8fcd330 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -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); } diff --git a/cw_bitcoin/lib/output_ordering.dart b/cw_bitcoin/lib/output_ordering.dart new file mode 100644 index 000000000..36a980e93 --- /dev/null +++ b/cw_bitcoin/lib/output_ordering.dart @@ -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 orderOutputs( + List outputs, + BitcoinOrdering ordering, { + Random? rng, +}) { + final result = List.of(outputs); + if (ordering == BitcoinOrdering.shuffle) { + result.shuffle(rng ?? Random.secure()); + } + return result; +} diff --git a/cw_bitcoin/test/output_ordering_test.dart b/cw_bitcoin/test/output_ordering_test.dart new file mode 100644 index 000000000..d0fc7de05 --- /dev/null +++ b/cw_bitcoin/test/output_ordering_test.dart @@ -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 = {}; + 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}))); + }); + }); +}