From c494713268e082a79f03e773a32ecbe865b65873 Mon Sep 17 00:00:00 2001 From: Makoto Takemiya Date: Sun, 26 Jul 2026 11:11:22 +0400 Subject: [PATCH 01/30] Fix Core Data migration startup crashes --- fearless.xcodeproj/project.pbxproj | 117 +- .../xcschemes/fearless.tests.xcscheme | 7 +- .../Common/CoreData/CoreDataAliases.swift | 261 +- .../FearlessObjectiveCExceptionCatcher.h | 27 + .../FearlessObjectiveCExceptionCatcher.m | 39 + .../ManagedObjectRuntimeClassRegistry.swift | 74 + .../SafeTransformableValueReader.swift | 487 + .../CDAccountInfo+CoreDataCodable.swift | 6 +- .../Helpers/AccountProviderFactory.swift | 318 +- .../Helpers/AccountRepositoryFactory.swift | 546 +- .../Common/Helpers/AssetModelMapper.swift | 6 +- .../Helpers/ChainRepositoryFactory.swift | 627 ++ .../Common/Helpers/EthereumNodeFetching.swift | 24 +- .../Model/ChainRegistry/ChainModel.swift | 73 + .../Common/Model/RuntimeMetadataItem.swift | 56 + ...niversalWalletAccountAddressResolver.swift | 34 + .../ChainRegistry/ChainRegistry.swift | 361 +- .../ChainRegistry/ChainRegistryFactory.swift | 11 +- .../ChainRegistry/ChainSyncService.swift | 383 +- .../EthereumConnectionPool.swift | 25 +- .../Compiled/CompatibleUserDataModel_v13.mom | Bin 0 -> 7578 bytes .../LegacyEcosystemUserDataModel_v12.mom | Bin 0 -> 7468 bytes .../Storage/CompatibilityModels/README.md | 34 + .../contents | 62 + .../contents | 61 + .../EntityToModel/ChainModelMapper.swift | 392 +- .../ManagedMetaAccountMapper.swift | 4 + .../EntityToModel/MetaAccountMapper.swift | 500 +- .../PolkaswapSettingMapper.swift | 19 +- .../Storage/Migration/KeystoreMigrator.swift | 1021 ++- .../Storage/Migration/SettingsMigrator.swift | 261 +- .../ChainModelV4MigrationPolicy.swift | 157 +- .../ChainSubstrateV2MigrationPolicy.swift | 27 +- .../SubstrateStorageMigrator+Sync.swift | 15 +- .../SubstrateStorageMigrator.swift | 6270 ++++++++++++- .../MultiassetV2MigrationPolicy.swift | 12 +- .../MultiassetV9MigrationPolicy.swift | 6 +- .../SingleToMultiassetMigrationPolicy.swift | 476 +- .../UserStorage/StorageMigrator+Sync.swift | 10 +- .../UserStorage/StorageMigrator.swift | 2804 +++++- .../UserStorage/UserStorageVersion.swift | 36 + .../xcmapping.xml | 6 +- .../xcmapping.xml | 2 +- ...ersistentStoreCoordinator+Extensions.swift | 2432 +++++ .../Storage/PersistentValueSettings.swift | 174 +- .../Storage/SelectedWalletSettings.swift | 315 +- .../Storage/SubstrateDataStorageFacade.swift | 31 +- .../Storage/UserDataStorageFacade.swift | 40 +- fearless/Info.plist | 6 + .../Onboarding/OnboardingPresenter.swift | 2 +- fearless/Modules/Root/RootInteractor.swift | 762 +- fearless/Modules/Root/RootPresenter.swift | 281 +- .../Modules/Root/RootPresenterFactory.swift | 28 +- fearless/Modules/Root/RootProtocol.swift | 5 +- fearless/Modules/Root/StartViewHelper.swift | 57 +- fearless/fearless-Bridging-Header.h | 1 + ...stentStoreReplacerResourceLimitTests.swift | 552 ++ ...SingleToMultiassetUserMigrationTests.swift | 1193 ++- ...SubstrateStorageClassResolutionTests.swift | 1320 +++ ...SubstrateStorageMigrationSafetyTests.swift | 7174 +++++++++++++++ ...erStorageCompatibilityMigrationTests.swift | 7815 +++++++++++++++++ .../ChainRegistry/ChainRegistryTests.swift | 598 +- .../ChainSyncServiceCompatibilityTests.swift | 1931 +++- .../ChainRegistry/MockConnection.swift | 14 +- .../Storage/ChainModelMapperTests.swift | 1754 ++++ .../Storage/MetaAccountMapperTests.swift | 527 ++ .../SelectedAccountSettingsTests.swift | 3416 +++++++ .../TransformableArchiveSafetyTests.swift | 846 ++ .../Helper/UserDataStorageTestFacade.swift | 46 +- fearlessTests/Mocks/ModuleMocks.swift | 65 +- fearlessTests/Modules/Root/RootTests.swift | 3583 +++++++- scripts/ci/run-coredata-release-gate.sh | 545 ++ .../ci/run-coredata-simulator-rehearsal.sh | 1352 +++ ...audit-user-storage-compatibility-models.sh | 137 + ...mpile-user-storage-compatibility-models.sh | 90 + .../storage/core-data-model-checksum.swift | 21 + ...-user-storage-compatibility-model-audit.sh | 92 + scripts/test-coredata-release-gate.sh | 423 + scripts/test-coredata-simulator-rehearsal.sh | 671 ++ 79 files changed, 52773 insertions(+), 1183 deletions(-) create mode 100644 fearless/Common/CoreData/FearlessObjectiveCExceptionCatcher.h create mode 100644 fearless/Common/CoreData/FearlessObjectiveCExceptionCatcher.m create mode 100644 fearless/Common/CoreData/ManagedObjectRuntimeClassRegistry.swift create mode 100644 fearless/Common/CoreData/SafeTransformableValueReader.swift create mode 100644 fearless/Common/Storage/CompatibilityModels/Compiled/CompatibleUserDataModel_v13.mom create mode 100644 fearless/Common/Storage/CompatibilityModels/Compiled/LegacyEcosystemUserDataModel_v12.mom create mode 100644 fearless/Common/Storage/CompatibilityModels/README.md create mode 100644 fearless/Common/Storage/CompatibilityModels/Source/CompatibleUserDataModel_v13.xcdatamodel/contents create mode 100644 fearless/Common/Storage/CompatibilityModels/Source/LegacyEcosystemUserDataModel_v12.xcdatamodel/contents create mode 100644 fearless/fearless-Bridging-Header.h create mode 100644 fearlessTests/Common/Migration/CrashConsistentStoreReplacerResourceLimitTests.swift create mode 100644 fearlessTests/Common/Migration/SubstrateStorageClassResolutionTests.swift create mode 100644 fearlessTests/Common/Migration/SubstrateStorageMigrationSafetyTests.swift create mode 100644 fearlessTests/Common/Migration/UserStorageCompatibilityMigrationTests.swift create mode 100644 fearlessTests/Common/Storage/TransformableArchiveSafetyTests.swift create mode 100644 scripts/ci/run-coredata-release-gate.sh create mode 100755 scripts/ci/run-coredata-simulator-rehearsal.sh create mode 100755 scripts/storage/audit-user-storage-compatibility-models.sh create mode 100755 scripts/storage/compile-user-storage-compatibility-models.sh create mode 100644 scripts/storage/core-data-model-checksum.swift create mode 100755 scripts/storage/test-user-storage-compatibility-model-audit.sh create mode 100644 scripts/test-coredata-release-gate.sh create mode 100755 scripts/test-coredata-simulator-rehearsal.sh diff --git a/fearless.xcodeproj/project.pbxproj b/fearless.xcodeproj/project.pbxproj index ba9a2d4864..f640fe0762 100644 --- a/fearless.xcodeproj/project.pbxproj +++ b/fearless.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ 04F9CF04588676D79EFB8570 /* ClaimCrowdloanRewardsAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBAA28C551344F1457D76DD5 /* ClaimCrowdloanRewardsAssembly.swift */; }; 054C4BCDEC29ED5F74A36E8B /* ExportMnemonicPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61EBE466BDCF77E65FDCDF81 /* ExportMnemonicPresenter.swift */; }; 05A6BB4F8F0912404A4D8413 /* WalletTransactionDetailsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82FD4E0BB1ABA364AFD2E891 /* WalletTransactionDetailsPresenter.swift */; }; + 060167927467FDFE3B939051 /* SubstrateStorageClassResolutionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADB5A97E2CECA69B16DCE8E3 /* SubstrateStorageClassResolutionTests.swift */; }; 06197BBE4299DC971C42DB92 /* WalletSendConfirmPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A687FBDA0912F8727CE0D81 /* WalletSendConfirmPresenter.swift */; }; 0678271BE1BA5BBC084F478A /* RecommendedValidatorListWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57C624E71FCE0FFF8EAD5BA9 /* RecommendedValidatorListWireframe.swift */; }; 06826AA6DBAEA5BBF45BB5CA /* LiquidityPoolSupplyConfirmInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4594686D10DBB7627B8C9A12 /* LiquidityPoolSupplyConfirmInteractor.swift */; }; @@ -230,6 +231,7 @@ 152AC909C26E809ACCA55B35 /* CreateContactInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D482753E0D75C5F5E0617998 /* CreateContactInteractor.swift */; }; 1550A6E8789263C0D734091A /* StakingUnbondSetupWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC5083A5751A1A3CC95F4F6F /* StakingUnbondSetupWireframe.swift */; }; 159A8702C30A21988AD76805 /* StakingPoolCreateConfirmAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 408D33DEE675B00ED511517A /* StakingPoolCreateConfirmAssembly.swift */; }; + 16BD370B2E1EBEB7FF26A839 /* ManagedObjectRuntimeClassRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD40C25879A8EC87DA2178B8 /* ManagedObjectRuntimeClassRegistry.swift */; }; 175D58C44C007FFA22679B5F /* NftDetailsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC012CF1C792B34BD5FF45A2 /* NftDetailsProtocols.swift */; }; 179591AAC5B3B7115BB21E53 /* ChainAccountBalanceListTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80DE21374A7FA5AE4D26CEB2 /* ChainAccountBalanceListTests.swift */; }; 1795E946F1E386442E96E2BC /* StakingPayoutConfirmationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17C0AC11A4A195BB697578CE /* StakingPayoutConfirmationPresenter.swift */; }; @@ -253,6 +255,7 @@ 20F28EF4AD17FC56A5A6697B /* NftSendPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C62522E202A1C5EE60D25122 /* NftSendPresenter.swift */; }; 225493AF467A54A56F74FFF5 /* LiquidityPoolsOverviewProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = A692D227372B24F922EFA058 /* LiquidityPoolsOverviewProtocols.swift */; }; 23398AEC756086FEEEE91E65 /* WalletsManagmentRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B55A4C7E8BD87A7C8634ADD /* WalletsManagmentRouter.swift */; }; + 23418E85C0C9C5A35F115B95 /* UserStorageCompatibilityMigrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36C64B63B4C227F2467C5C20 /* UserStorageCompatibilityMigrationTests.swift */; }; 237AD34CD1C2778834D7B330 /* AnalyticsValidatorsViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6F8BBBA9EABA266B288333F /* AnalyticsValidatorsViewFactory.swift */; }; 23E30A21620831C600CBC1D6 /* LiquidityPoolDetailsRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28E3B4BA1160B87C435C2AAF /* LiquidityPoolDetailsRouter.swift */; }; 24E6794278FEC01DFFE7C69C /* LiquidityPoolSupplyConfirmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D430E8B808864B281A62AB43 /* LiquidityPoolSupplyConfirmViewController.swift */; }; @@ -269,6 +272,7 @@ 27CD06DA03B268E2C6A90B15 /* PolkaswapAdjustmentRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D2C2FC3E31C03D08BDEC7A1 /* PolkaswapAdjustmentRouter.swift */; }; 27FA1D57A06AA3A030D226B6 /* StakingUnbondConfirmWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = A84638893DC99974E098719E /* StakingUnbondConfirmWireframe.swift */; }; 281D17EF8C45A1FC49FD1523 /* StakingPoolInfoViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97E2A7A3731FAC0C965A898A /* StakingPoolInfoViewLayout.swift */; }; + 2825B0FA4C33C5645A0DD2A7 /* SubstrateV3toV4.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = F191AB8E98289B7B14F71208 /* SubstrateV3toV4.xcmappingmodel */; }; 28FC2FE3E3BE82BF1965B9C1 /* ClaimCrowdloanRewardsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523339F3ED67EDEB8C5D2110 /* ClaimCrowdloanRewardsPresenter.swift */; }; 2918DCAEB91F8276446873C8 /* StakingRewardPayoutsWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = E57E3EA5C070C64000ABCDC0 /* StakingRewardPayoutsWireframe.swift */; }; 2A66CF4F25D109780006E4C1 /* CDPhishingItem+CoreDataDecodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A66CF4E25D109770006E4C1 /* CDPhishingItem+CoreDataDecodable.swift */; }; @@ -317,6 +321,7 @@ 3B9314DE6AFC01CA7EF0DAAA /* SelectValidatorsConfirmFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 364C90F7AD36FD6F6E690D7D /* SelectValidatorsConfirmFlow.swift */; }; 3CA86739CB09801714B194BD /* PurchaseWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C52C6CD7112BF0E1E3A98CE /* PurchaseWireframe.swift */; }; 3D1FB0EF87D42F08D9250552 /* PurchasePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56FF8DBE5C32EE4C68ECD623 /* PurchasePresenter.swift */; }; + 3D764B6213C7A3B3F42C9170 /* SubstrateStorageMigrationSafetyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61421DD86EF588DEC4B9ED42 /* SubstrateStorageMigrationSafetyTests.swift */; }; 3DCDF9784283313336CC0505 /* NftSendConfirmPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE826F356F6D72EACFB0AE31 /* NftSendConfirmPresenter.swift */; }; 3DF50E6F78F1B1052625BA7D /* ChainSelectionPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2222D628B9EA092D1C6B1CAE /* ChainSelectionPresenter.swift */; }; 3E053332BA9D170FB1569ABB /* WalletTransactionDetailsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = B264116DA8F64446D46E12DE /* WalletTransactionDetailsProtocols.swift */; }; @@ -353,11 +358,13 @@ 4A957B3BAC231B70CBC00EC3 /* LiquidityPoolsOverviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8B69E9E18C11EAEC9284B3 /* LiquidityPoolsOverviewViewController.swift */; }; 4C7AC4E214171478AC98A898 /* StakingRewardDestConfirmTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9403C5F9C88A4690C62A204B /* StakingRewardDestConfirmTests.swift */; }; 4D44ACFB841F7CE18CE98559 /* MultichainAssetSelectionAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9596692C164228636164C830 /* MultichainAssetSelectionAssembly.swift */; }; + 4D56353FBB1160698C542499 /* CrashConsistentStoreReplacerResourceLimitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EEC2067F2D6F4E9D194E239 /* CrashConsistentStoreReplacerResourceLimitTests.swift */; }; 4D822D169784790EBF173EEE /* WalletMainContainerPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D738FE711DD760B47C0BA65 /* WalletMainContainerPresenter.swift */; }; 4E5CD7B8821FA5298EA1598E /* CrowdloanContributionSetupWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3574BADE9CF77599048C7010 /* CrowdloanContributionSetupWireframe.swift */; }; 4F12420C17B321F52D694E92 /* AddCustomNodeInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 900E67A76C0702F8123EBB47 /* AddCustomNodeInteractor.swift */; }; 4F66A13E327CBDD04A2411FA /* WalletTransactionHistoryViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFB1161D25AF1FC90AA23B7A /* WalletTransactionHistoryViewLayout.swift */; }; 4FBD73DD27CAA339727616B5 /* StakingUnbondSetupPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FCECA20E6DCA5D228F44477 /* StakingUnbondSetupPresenter.swift */; }; + 4FD1A1541952770AA5A153CB /* FearlessObjectiveCExceptionCatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 9B1494192371496FAB4895B8 /* FearlessObjectiveCExceptionCatcher.m */; }; 501347BEA67CD30FE484358E /* AccountImportViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E60EF37AC0A7646ED8FE64 /* AccountImportViewFactory.swift */; }; 502DF063219C7211C2225BD4 /* AddCustomNodeViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4952E0B8F8DCD92FE4A37892 /* AddCustomNodeViewLayout.swift */; }; 503DFF0EFCAD0A8B526FEC3A /* SelectMarketViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 320E2207FB549F7C31A80441 /* SelectMarketViewController.swift */; }; @@ -385,6 +392,7 @@ 5C796EF8ED29F564B5D1126B /* CrowdloanContributionConfirmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F75722D2F921FD1C2D4105D /* CrowdloanContributionConfirmViewController.swift */; }; 5D0665A581B9F8DFDBD0CF7B /* WalletChainAccountDashboardWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885A9E2D3619FEFC5ED0C093 /* WalletChainAccountDashboardWireframe.swift */; }; 5DDD2206DF795CF205610455 /* AccountExportPasswordPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31226053044986BC828AA912 /* AccountExportPasswordPresenter.swift */; }; + 5E1DDD7E1A2F74184B3C0CE4 /* TransformableArchiveSafetyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15F0F954ADDD5A4E480392B0 /* TransformableArchiveSafetyTests.swift */; }; 5E8504507116E0177D70314B /* LiquidityPoolSupplyViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438E01C5C877428168E9F3F8 /* LiquidityPoolSupplyViewLayout.swift */; }; 5E9402965D385607E04156DC /* NftDetailsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32DEDC1A0ED429DD43EC621E /* NftDetailsPresenter.swift */; }; 5E974C26655D3E64AD6A923D /* AccountStatisticsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0542BF70B1BADBF1459D57FB /* AccountStatisticsInteractor.swift */; }; @@ -440,6 +448,7 @@ 76214649137E7061F701FE38 /* WalletMainContainerAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E096A576B747C09B14FD38D /* WalletMainContainerAssembly.swift */; }; 762BB1AC2F45142B6319B59F /* NftSendProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = F55184167D22A33EF7FF77AE /* NftSendProtocols.swift */; }; 76C5FD0685615E602696B23D /* SelectValidatorsConfirmTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28EF0F9F97C89137F642016E /* SelectValidatorsConfirmTests.swift */; }; + 76CD2DF3F59652CC0B3DB5BA /* ChainModelMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3675C75F2FBA7D24EBD5750A /* ChainModelMapperTests.swift */; }; 76F74188F16A370D79033A12 /* AnalyticsRewardDetailsWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB5E8FAB4C12D7BFEEF576AD /* AnalyticsRewardDetailsWireframe.swift */; }; 775C4C720600DAE242C67192 /* WalletSendConfirmViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A4416B96A9DD5FB5EEA086E /* WalletSendConfirmViewLayout.swift */; }; 78314B269F1CF1A499DE5CCB /* StakingBondMoreFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 784D20E16EEE55C2CF7B319B /* StakingBondMoreFlow.swift */; }; @@ -1304,6 +1313,7 @@ 9F4A48B1BE3A1110A0CF9F36 /* ReferralCrowdloanViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DDDB2B35CD3299F50613141 /* ReferralCrowdloanViewController.swift */; }; 9F664DA212363F5F2C2B530B /* SelectMarketRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C05A688EA7379572BBCE545 /* SelectMarketRouter.swift */; }; A090FF206B56A0E465C62072 /* CrowdloanListPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86F7A369E31DCB9ABD556EE9 /* CrowdloanListPresenter.swift */; }; + A1161E67E9F24527E15AC229 /* SafeTransformableValueReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDEB6F26DCFB53EA1547948 /* SafeTransformableValueReader.swift */; }; A14F444FA808457C16EF826C /* WalletOptionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 193908913A5B241C82672D8A /* WalletOptionViewController.swift */; }; A1B2C3D4E5F60718293A4B5E /* TonChainSelectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60718293A4B5D /* TonChainSelectionTests.swift */; }; A1C05D0028CD04C16AB6082F /* LiquidityPoolDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67BDE520860A67C800E7F4AB /* LiquidityPoolDetailsViewController.swift */; }; @@ -1315,6 +1325,7 @@ A565F118B7ED356099662F03 /* ExportMnemonicTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E32C2DC4CC106A3509BE651D /* ExportMnemonicTests.swift */; }; A5AB7027E1E73E39E4026C5C /* Pods_fearlessTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 59FDAE57EE0A97872E76E6CE /* Pods_fearlessTests.framework */; }; A5C7F51539B8D93D9186DA2C /* LiquidityPoolRemoveLiquidityViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419D75A346CE10236161522 /* LiquidityPoolRemoveLiquidityViewLayout.swift */; }; + A5EF098C5D53D306A7E84844 /* LegacyEcosystemUserDataModel_v12.mom in Resources */ = {isa = PBXBuildFile; fileRef = CDB0BA173951B82DCA58B603 /* LegacyEcosystemUserDataModel_v12.mom */; }; A64E3CA61C6CEE74F2FD9825 /* PolkaswapTransaktionSettingsAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7931155840DACB340284ABBB /* PolkaswapTransaktionSettingsAssembly.swift */; }; A6855830B76B0782A696583A /* AssetNetworksInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C554924081754A981EB4243E /* AssetNetworksInteractor.swift */; }; A69D3B3F6BF76FA9C3070BBD /* AssetNetworksViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFBFF377F35B254AB3141100 /* AssetNetworksViewLayout.swift */; }; @@ -1463,6 +1474,7 @@ AEFC6D672600A808000BD310 /* StoriesPreviewCollectionItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEFC6D662600A808000BD310 /* StoriesPreviewCollectionItem.swift */; }; AEFC6D6F2600D7CD000BD310 /* NetworkInfoViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEFC6D6E2600D7CD000BD310 /* NetworkInfoViewModelFactory.swift */; }; AF1FD8891DC8B68A9C17C5B2 /* CreateContactViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 198C6349BED4E521C6D7B3AB /* CreateContactViewLayout.swift */; }; + AF76DAB9F6DF8789FCF083E9 /* ChainSyncServiceCompatibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 189CE1DE8D520FEB61152078 /* ChainSyncServiceCompatibilityTests.swift */; }; AF8193D9F818638254854232 /* StakingMainProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 470B64C45E547C25FCCCFC33 /* StakingMainProtocols.swift */; }; B02EAF42C91E069FE6872EE0 /* SelectValidatorsConfirmWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D0E02AA5D3EBA9B94950241 /* SelectValidatorsConfirmWireframe.swift */; }; B071927DF8DD5C3CA84494BA /* RecommendedValidatorListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F61D8973ADEB461DE2AD3E13 /* RecommendedValidatorListViewController.swift */; }; @@ -1494,6 +1506,7 @@ C20ED4531583D0C8E38715E0 /* PurchaseProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 782CC21A2F9EEF5DBA3AB1AA /* PurchaseProtocols.swift */; }; C3C7D60B36C778DA0A307BCC /* AddCustomNodeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CD8A37F219A0BCC0C6063E /* AddCustomNodeViewController.swift */; }; C3D915637D26E807B85957CF /* NodeSelectionPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ABDA8952766DE724CD078D6 /* NodeSelectionPresenter.swift */; }; + C3E367BDACDDDB4358823EE9 /* CompatibleUserDataModel_v13.mom in Resources */ = {isa = PBXBuildFile; fileRef = 550065CFAAF0723F31DDE3B5 /* CompatibleUserDataModel_v13.mom */; }; C46EEF6A9A9A601694E72DB1 /* StakingMainWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96F09665083031502F9693F8 /* StakingMainWireframe.swift */; }; C481665C170F4D9523DC73AF /* WarningAlertViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55EEBFA4A54B3AAF9F52855 /* WarningAlertViewLayout.swift */; }; C4F8BEB6DFA03A374135BD6B /* FiltersInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E692235C8CF576F69971D118 /* FiltersInteractor.swift */; }; @@ -1690,6 +1703,7 @@ E2F9D7BDDE961B162EF100AB /* StakingMainViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A825B6368073B06F32D7C8F /* StakingMainViewFactory.swift */; }; E320ACDB8E3B3CBDAC28691D /* PolkaswapSwapConfirmationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F0FEFEAEC5CDCD7C9BF77D5 /* PolkaswapSwapConfirmationPresenter.swift */; }; E40F3F3C0FE753970B8745DD /* SelectMarketAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7525C3C310DC88A7FA598B9 /* SelectMarketAssembly.swift */; }; + E49A768AD7230A1844DD6C8D /* SubstrateV2toV4.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = 3F646753DCEB6599248D9A31 /* SubstrateV2toV4.xcmappingmodel */; }; E58F94758A6B8BCF3BE0EAEA /* MainNftContainerViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BCBF6D6D48704B27B2DBB79 /* MainNftContainerViewLayout.swift */; }; E5A07BDF3D37C761F453696A /* AddCustomNodeViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D81DBDDD34EA20C3270EDB4 /* AddCustomNodeViewFactory.swift */; }; E5B4512B99ED86EF398B648E /* WarningAlertTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E587507F8CDA7F84A1A4EA95 /* WarningAlertTests.swift */; }; @@ -2964,15 +2978,6 @@ FAE601882ECA700100000001 /* BitcoinTransactionHistorySyncTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE601892ECA700100000001 /* BitcoinTransactionHistorySyncTests.swift */; }; FAE6018A2ECA700100000001 /* BitcoinSendService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE6018B2ECA700100000001 /* BitcoinSendService.swift */; }; FAE6018C2ECA700100000001 /* BitcoinSendServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE6018D2ECA700100000001 /* BitcoinSendServiceTests.swift */; }; - FAE601A22ECA700100000001 /* BitcoinHistoryOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE601A32ECA700100000001 /* BitcoinHistoryOperationFactory.swift */; }; - FAE601A42ECA700100000001 /* BitcoinHistoryOperationFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE601A52ECA700100000001 /* BitcoinHistoryOperationFactoryTests.swift */; }; - FAE601A62ECA700100000001 /* UniversalWalletAccountAddressResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE601A72ECA700100000001 /* UniversalWalletAccountAddressResolver.swift */; }; - FAE601A82ECA700100000001 /* UniversalWalletAccountAddressResolverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE601A92ECA700100000001 /* UniversalWalletAccountAddressResolverTests.swift */; }; - FAE601AA2ECA700100000001 /* SolanaHistoryOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE601AB2ECA700100000001 /* SolanaHistoryOperationFactory.swift */; }; - FAE601AC2ECA700100000001 /* SolanaHistoryOperationFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE601AD2ECA700100000001 /* SolanaHistoryOperationFactoryTests.swift */; }; - FAE601AE2ECA700100000001 /* IrohaHistoryOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE601AF2ECA700100000001 /* IrohaHistoryOperationFactory.swift */; }; - FAE601B02ECA700100000001 /* IrohaHistoryOperationFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE601B12ECA700100000001 /* IrohaHistoryOperationFactoryTests.swift */; }; - FAE601B22ECA700100000001 /* PasskeyBackupContract.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE601B32ECA700100000001 /* PasskeyBackupContract.swift */; }; FAE6018E2ECA700100000001 /* SolanaBalanceSync.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE6018F2ECA700100000001 /* SolanaBalanceSync.swift */; }; FAE601902ECA700100000001 /* SolanaBalanceSyncTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE601912ECA700100000001 /* SolanaBalanceSyncTests.swift */; }; FAE601922ECA700100000001 /* SolanaTransactionHistorySync.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE601932ECA700100000001 /* SolanaTransactionHistorySync.swift */; }; @@ -2983,6 +2988,15 @@ FAE6019C2ECA700100000001 /* SolanaSendService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE6019D2ECA700100000001 /* SolanaSendService.swift */; }; FAE6019E2ECA700100000001 /* SolanaTransferTransactionBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE6019F2ECA700100000001 /* SolanaTransferTransactionBuilderTests.swift */; }; FAE601A02ECA700100000001 /* SolanaSendServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE601A12ECA700100000001 /* SolanaSendServiceTests.swift */; }; + FAE601A22ECA700100000001 /* BitcoinHistoryOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE601A32ECA700100000001 /* BitcoinHistoryOperationFactory.swift */; }; + FAE601A42ECA700100000001 /* BitcoinHistoryOperationFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE601A52ECA700100000001 /* BitcoinHistoryOperationFactoryTests.swift */; }; + FAE601A62ECA700100000001 /* UniversalWalletAccountAddressResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE601A72ECA700100000001 /* UniversalWalletAccountAddressResolver.swift */; }; + FAE601A82ECA700100000001 /* UniversalWalletAccountAddressResolverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE601A92ECA700100000001 /* UniversalWalletAccountAddressResolverTests.swift */; }; + FAE601AA2ECA700100000001 /* SolanaHistoryOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE601AB2ECA700100000001 /* SolanaHistoryOperationFactory.swift */; }; + FAE601AC2ECA700100000001 /* SolanaHistoryOperationFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE601AD2ECA700100000001 /* SolanaHistoryOperationFactoryTests.swift */; }; + FAE601AE2ECA700100000001 /* IrohaHistoryOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE601AF2ECA700100000001 /* IrohaHistoryOperationFactory.swift */; }; + FAE601B02ECA700100000001 /* IrohaHistoryOperationFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE601B12ECA700100000001 /* IrohaHistoryOperationFactoryTests.swift */; }; + FAE601B22ECA700100000001 /* PasskeyBackupContract.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE601B32ECA700100000001 /* PasskeyBackupContract.swift */; }; FAE9EB9D288AB74D009390B6 /* AnalyticsRewardsFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE9EB9C288AB74D009390B6 /* AnalyticsRewardsFlow.swift */; }; FAE9EB9F288ABBBE009390B6 /* AnalyticRewardsParachainViewModelState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE9EB9E288ABBBE009390B6 /* AnalyticRewardsParachainViewModelState.swift */; }; FAE9EBA1288ABBC7009390B6 /* AnalyticRewardsParachainStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE9EBA0288ABBC7009390B6 /* AnalyticRewardsParachainStrategy.swift */; }; @@ -3334,11 +3348,13 @@ 14611E105279789A149B3755 /* AssetNetworksProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetNetworksProtocols.swift; sourceTree = ""; }; 14E3337CDD7C831AEAA4582F /* CustomValidatorListPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CustomValidatorListPresenter.swift; sourceTree = ""; }; 15E7F3E6BBE50797A9EB9145 /* WalletTransactionHistoryViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletTransactionHistoryViewController.swift; sourceTree = ""; }; + 15F0F954ADDD5A4E480392B0 /* TransformableArchiveSafetyTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TransformableArchiveSafetyTests.swift; sourceTree = ""; }; 169ADB0FB6C83C9CEED2F780 /* NetworkIssuesNotificationViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NetworkIssuesNotificationViewLayout.swift; sourceTree = ""; }; 172B3E9BE51A339D7A09BDA3 /* UsernameSetupPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = UsernameSetupPresenter.swift; sourceTree = ""; }; 17717EAA891DDB5A0223718F /* AccountCreateTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountCreateTests.swift; sourceTree = ""; }; 17C0AC11A4A195BB697578CE /* StakingPayoutConfirmationPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingPayoutConfirmationPresenter.swift; sourceTree = ""; }; 18228EC4A56493660DC7DBB1 /* AccountImportTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountImportTests.swift; sourceTree = ""; }; + 189CE1DE8D520FEB61152078 /* ChainSyncServiceCompatibilityTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ChainSyncServiceCompatibilityTests.swift; sourceTree = ""; }; 193908913A5B241C82672D8A /* WalletOptionViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletOptionViewController.swift; sourceTree = ""; }; 194C9BFEE9BA8C9E448D79AA /* AccountExportPasswordInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountExportPasswordInteractor.swift; sourceTree = ""; }; 194E6EDD65CA3FB0B1F6727C /* NftDetailsAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftDetailsAssembly.swift; sourceTree = ""; }; @@ -3415,7 +3431,9 @@ 3574BADE9CF77599048C7010 /* CrowdloanContributionSetupWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanContributionSetupWireframe.swift; sourceTree = ""; }; 35EAD41DB1444DA38D8C65E2 /* NetworkIssuesNotificationPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NetworkIssuesNotificationPresenter.swift; sourceTree = ""; }; 364C90F7AD36FD6F6E690D7D /* SelectValidatorsConfirmFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SelectValidatorsConfirmFlow.swift; sourceTree = ""; }; + 3675C75F2FBA7D24EBD5750A /* ChainModelMapperTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ChainModelMapperTests.swift; sourceTree = ""; }; 36A3D64FF58C40E5CC6A6E89 /* AssetNetworksRouter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetNetworksRouter.swift; sourceTree = ""; }; + 36C64B63B4C227F2467C5C20 /* UserStorageCompatibilityMigrationTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = UserStorageCompatibilityMigrationTests.swift; sourceTree = ""; }; 375E9A7A1206E366E862D81D /* WalletTransactionHistoryInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletTransactionHistoryInteractor.swift; sourceTree = ""; }; 3837CE5CB2D48D8A694A7EE0 /* StakingPoolCreateConfirmPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingPoolCreateConfirmPresenter.swift; sourceTree = ""; }; 385FE8691EA37DE9F562B34E /* CrowdloanContributionConfirmTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanContributionConfirmTests.swift; sourceTree = ""; }; @@ -3434,6 +3452,7 @@ 3F25F3A88B4BEB4DE498220C /* LiquidityPoolsOverviewRouter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolsOverviewRouter.swift; sourceTree = ""; }; 3F47E5832513985B89D06155 /* AccountExportPasswordWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountExportPasswordWireframe.swift; sourceTree = ""; }; 3F5C47F8D6CB97D16DFF06B7 /* WalletsManagmentInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletsManagmentInteractor.swift; sourceTree = ""; }; + 3F646753DCEB6599248D9A31 /* SubstrateV2toV4.xcmappingmodel */ = {isa = PBXFileReference; includeInIndex = 1; path = SubstrateV2toV4.xcmappingmodel; sourceTree = ""; }; 3F7068913923A6DEEE9D8EA0 /* CrowdloanContributionSetupPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanContributionSetupPresenter.swift; sourceTree = ""; }; 3F75722D2F921FD1C2D4105D /* CrowdloanContributionConfirmViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanContributionConfirmViewController.swift; sourceTree = ""; }; 40728E7BBC553AFA8FF4142B /* NetworkIssuesNotificationViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NetworkIssuesNotificationViewController.swift; sourceTree = ""; }; @@ -3480,6 +3499,7 @@ 5408FF305E4A49A683BC43E0 /* WalletChainAccountDashboardViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletChainAccountDashboardViewLayout.swift; sourceTree = ""; }; 54776237A20227DFE025E3AC /* AllDoneViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AllDoneViewLayout.swift; sourceTree = ""; }; 54FB887490A8B33890B4E0E4 /* ControllerAccountConfirmationPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ControllerAccountConfirmationPresenter.swift; sourceTree = ""; }; + 550065CFAAF0723F31DDE3B5 /* CompatibleUserDataModel_v13.mom */ = {isa = PBXFileReference; includeInIndex = 1; path = CompatibleUserDataModel_v13.mom; sourceTree = ""; }; 55499F69824A67FA32C02C77 /* StakingPoolInfoRouter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingPoolInfoRouter.swift; sourceTree = ""; }; 5555FA26DF1BD5483F7544B5 /* StakingPoolCreateConfirmViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingPoolCreateConfirmViewLayout.swift; sourceTree = ""; }; 55CC5A63AC07644409E997C1 /* StakingMainViewController.xib */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = file.xib; path = StakingMainViewController.xib; sourceTree = ""; }; @@ -3514,6 +3534,7 @@ 5FFEB823BE0057CFB78CC033 /* NetworkManagementTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NetworkManagementTests.swift; sourceTree = ""; }; 605CA30BCCB5F23C64E6D6EC /* NodeSelectionViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NodeSelectionViewController.swift; sourceTree = ""; }; 610A7425DF3F20BE0AB05ECD /* NftCollectionViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftCollectionViewLayout.swift; sourceTree = ""; }; + 61421DD86EF588DEC4B9ED42 /* SubstrateStorageMigrationSafetyTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SubstrateStorageMigrationSafetyTests.swift; sourceTree = ""; }; 61A5A7C701EF966BF48D6B9E /* AllDoneViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AllDoneViewController.swift; sourceTree = ""; }; 61BE50276590067C81F4761F /* ChainAccountBalanceTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ChainAccountBalanceTests.swift; sourceTree = ""; }; 61E1CD099F7C30ABE0E8A001 /* StakingUnbondSetupTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingUnbondSetupTests.swift; sourceTree = ""; }; @@ -3537,6 +3558,7 @@ 67B1037AEC97DBEAF9FD50C1 /* AssetNetworksPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetNetworksPresenter.swift; sourceTree = ""; }; 67B3E1906EEBE32E71E82BB6 /* NodeSelectionViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NodeSelectionViewLayout.swift; sourceTree = ""; }; 67BDE520860A67C800E7F4AB /* LiquidityPoolDetailsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolDetailsViewController.swift; sourceTree = ""; }; + 67DC38DE23BBEF83BC331009 /* fearless-Bridging-Header.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "fearless-Bridging-Header.h"; sourceTree = ""; }; 6887529305794E17D9434D44 /* LiquidityPoolRemoveLiquidityInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolRemoveLiquidityInteractor.swift; sourceTree = ""; }; 6897929D244B5C29E3FD0727 /* StakingPoolCreateRouter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingPoolCreateRouter.swift; sourceTree = ""; }; 69B00AC48FB7D11855875EB9 /* SwapTransactionDetailPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapTransactionDetailPresenter.swift; sourceTree = ""; }; @@ -4396,6 +4418,7 @@ 8D51D60F19284936A6E9F47D /* ReferralCrowdloanWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferralCrowdloanWireframe.swift; sourceTree = ""; }; 8E4DF941DE0EDEF99A843A9D /* ContactsInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ContactsInteractor.swift; sourceTree = ""; }; 8E83833EB33E51A12F96F83B /* WalletChainAccountDashboardPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletChainAccountDashboardPresenter.swift; sourceTree = ""; }; + 8EEC2067F2D6F4E9D194E239 /* CrashConsistentStoreReplacerResourceLimitTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrashConsistentStoreReplacerResourceLimitTests.swift; sourceTree = ""; }; 8F04623D6CE10669D914CA2F /* PolkaswapTransaktionSettingsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PolkaswapTransaktionSettingsViewController.swift; sourceTree = ""; }; 8F0FEFEAEC5CDCD7C9BF77D5 /* PolkaswapSwapConfirmationPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PolkaswapSwapConfirmationPresenter.swift; sourceTree = ""; }; 8FB7091F743BBACC09553298 /* LiquidityPoolSupplyProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolSupplyProtocols.swift; sourceTree = ""; }; @@ -4427,10 +4450,12 @@ 9A22A2EB487E282DCA93C676 /* WalletSendConfirmWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletSendConfirmWireframe.swift; sourceTree = ""; }; 9A9EBD3B7AEA5EF594DFEB49 /* PolkaswapAdjustmentProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PolkaswapAdjustmentProtocols.swift; sourceTree = ""; }; 9AEA7ECB8434DF494D2B25B9 /* ChainAccountTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ChainAccountTests.swift; sourceTree = ""; }; + 9B1494192371496FAB4895B8 /* FearlessObjectiveCExceptionCatcher.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = FearlessObjectiveCExceptionCatcher.m; sourceTree = ""; }; 9B5626189788682A84D4E9D7 /* SelectValidatorsConfirmPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SelectValidatorsConfirmPresenter.swift; sourceTree = ""; }; 9BA528679A82B9A327853804 /* LiquidityPoolSupplyInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolSupplyInteractor.swift; sourceTree = ""; }; 9C01DCD4DA014E8FB50B9F11 /* CrowdloanContributionSetupTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanContributionSetupTests.swift; sourceTree = ""; }; 9C05A688EA7379572BBCE545 /* SelectMarketRouter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SelectMarketRouter.swift; sourceTree = ""; }; + 9FDEB6F26DCFB53EA1547948 /* SafeTransformableValueReader.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SafeTransformableValueReader.swift; sourceTree = ""; }; 9FED48DE9B681995E6E4A581 /* LiquidityPoolDetailsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolDetailsProtocols.swift; sourceTree = ""; }; A14CA4551FCC2EBD078E2242 /* AccountConfirmViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountConfirmViewFactory.swift; sourceTree = ""; }; A1B2C3D4E5F60718293A4B5D /* TonChainSelectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = fearlessTests/ApplicationLayer/Services/FeatureToggle/TonChainSelectionTests.swift; sourceTree = ""; }; @@ -4461,6 +4486,7 @@ AC404A4071AF571FAC4C1994 /* AccountExportPasswordProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountExportPasswordProtocols.swift; sourceTree = ""; }; ACAEDA02F409FE23749A1551 /* AccountCreateWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountCreateWireframe.swift; sourceTree = ""; }; AD0EAB8749661CB4428685FB /* SelectMarketProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SelectMarketProtocols.swift; sourceTree = ""; }; + ADB5A97E2CECA69B16DCE8E3 /* SubstrateStorageClassResolutionTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SubstrateStorageClassResolutionTests.swift; sourceTree = ""; }; ADD19595322BF8FEC0F1F746 /* StakingPoolCreatePresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingPoolCreatePresenter.swift; sourceTree = ""; }; ADD348E749EC6A7E3BB069DE /* StakingUnbondConfirmProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingUnbondConfirmProtocols.swift; sourceTree = ""; }; AE20602B2636EA5800357578 /* MoonPayKeys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoonPayKeys.swift; sourceTree = ""; }; @@ -4611,11 +4637,13 @@ BB837A15BAAED64BC32F3F44 /* SelectMarketInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SelectMarketInteractor.swift; sourceTree = ""; }; BB86E65E22C0AF7EDD0701A4 /* AccountStatisticsPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountStatisticsPresenter.swift; sourceTree = ""; }; BC2C9D26B9F9CC048C67796F /* AnalyticsRewardDetailsViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AnalyticsRewardDetailsViewLayout.swift; sourceTree = ""; }; + BD40C25879A8EC87DA2178B8 /* ManagedObjectRuntimeClassRegistry.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManagedObjectRuntimeClassRegistry.swift; sourceTree = ""; }; BE7D061906CF67230BF5393A /* NftSendConfirmTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftSendConfirmTests.swift; sourceTree = ""; }; C0EE58376751B23A9CEAEE1A /* StakingPoolCreateConfirmRouter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingPoolCreateConfirmRouter.swift; sourceTree = ""; }; C16219B3D0D2A658185C0850 /* MainNftContainerInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MainNftContainerInteractor.swift; sourceTree = ""; }; C191A3875F3255B72E01FA92 /* CrowdloanListWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanListWireframe.swift; sourceTree = ""; }; C23EA8830B337C4F8142A395 /* AddCustomNodeTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddCustomNodeTests.swift; sourceTree = ""; }; + C24D2309D225410A85B67B4A /* FearlessObjectiveCExceptionCatcher.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = FearlessObjectiveCExceptionCatcher.h; sourceTree = ""; }; C2956D0C69019DDCDAB2EB34 /* CustomValidatorListViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CustomValidatorListViewLayout.swift; sourceTree = ""; }; C316BE4F5A0342D379F783E8 /* StartSelectValidatorsTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StartSelectValidatorsTests.swift; sourceTree = ""; }; C323602A21644DCB1B2EEFF6 /* Pods_fearlessAll_fearlessIntegrationTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_fearlessAll_fearlessIntegrationTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -4773,6 +4801,7 @@ CD098B40697FD6CC08F9A6AC /* UsernameSetupProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = UsernameSetupProtocols.swift; sourceTree = ""; }; CD730FFB75B5D76EA939042D /* PolkaswapTransaktionSettingsInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PolkaswapTransaktionSettingsInteractor.swift; sourceTree = ""; }; CD8B6213B00597B3F56F650D /* StakingUnbondSetupFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingUnbondSetupFlow.swift; sourceTree = ""; }; + CDB0BA173951B82DCA58B603 /* LegacyEcosystemUserDataModel_v12.mom */ = {isa = PBXFileReference; includeInIndex = 1; path = LegacyEcosystemUserDataModel_v12.mom; sourceTree = ""; }; CE29083D5CE7EA0D886D069A /* WalletTransactionHistoryViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletTransactionHistoryViewFactory.swift; sourceTree = ""; }; CE294DDEAB7902D7CE1F1BA1 /* AnalyticsRewardDetailsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AnalyticsRewardDetailsProtocols.swift; sourceTree = ""; }; CF4A813A6FB09F9FE5891578 /* WalletSendConfirmViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletSendConfirmViewController.swift; sourceTree = ""; }; @@ -4848,6 +4877,7 @@ F0A1B2C3D4E5F60718293A4B /* AssetRepositoryFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetRepositoryFactoryTests.swift; sourceTree = ""; }; F12B2D844B474F810C807451 /* AccountManagementTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountManagementTests.swift; sourceTree = ""; }; F15D1C78B2844216802DA000 /* ChainSelectionViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ChainSelectionViewFactory.swift; sourceTree = ""; }; + F191AB8E98289B7B14F71208 /* SubstrateV3toV4.xcmappingmodel */ = {isa = PBXFileReference; includeInIndex = 1; path = SubstrateV3toV4.xcmappingmodel; sourceTree = ""; }; F1A2B3C4D5E6F708192A3B4D /* NetworkWorkerCompatibilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkWorkerCompatibilityTests.swift; sourceTree = ""; }; F1E3F963A56923FD036280BD /* WalletOptionAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletOptionAssembly.swift; sourceTree = ""; }; F23E38DCBC74C528D7839B76 /* CrowdloanContributionSetupInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanContributionSetupInteractor.swift; sourceTree = ""; }; @@ -6099,15 +6129,6 @@ FAE601892ECA700100000001 /* BitcoinTransactionHistorySyncTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitcoinTransactionHistorySyncTests.swift; sourceTree = ""; }; FAE6018B2ECA700100000001 /* BitcoinSendService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitcoinSendService.swift; sourceTree = ""; }; FAE6018D2ECA700100000001 /* BitcoinSendServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitcoinSendServiceTests.swift; sourceTree = ""; }; - FAE601A32ECA700100000001 /* BitcoinHistoryOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitcoinHistoryOperationFactory.swift; sourceTree = ""; }; - FAE601A52ECA700100000001 /* BitcoinHistoryOperationFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitcoinHistoryOperationFactoryTests.swift; sourceTree = ""; }; - FAE601A72ECA700100000001 /* UniversalWalletAccountAddressResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UniversalWalletAccountAddressResolver.swift; sourceTree = ""; }; - FAE601A92ECA700100000001 /* UniversalWalletAccountAddressResolverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UniversalWalletAccountAddressResolverTests.swift; sourceTree = ""; }; - FAE601AB2ECA700100000001 /* SolanaHistoryOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SolanaHistoryOperationFactory.swift; sourceTree = ""; }; - FAE601AD2ECA700100000001 /* SolanaHistoryOperationFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SolanaHistoryOperationFactoryTests.swift; sourceTree = ""; }; - FAE601AF2ECA700100000001 /* IrohaHistoryOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IrohaHistoryOperationFactory.swift; sourceTree = ""; }; - FAE601B12ECA700100000001 /* IrohaHistoryOperationFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IrohaHistoryOperationFactoryTests.swift; sourceTree = ""; }; - FAE601B32ECA700100000001 /* PasskeyBackupContract.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasskeyBackupContract.swift; sourceTree = ""; }; FAE6018F2ECA700100000001 /* SolanaBalanceSync.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SolanaBalanceSync.swift; sourceTree = ""; }; FAE601912ECA700100000001 /* SolanaBalanceSyncTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SolanaBalanceSyncTests.swift; sourceTree = ""; }; FAE601932ECA700100000001 /* SolanaTransactionHistorySync.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SolanaTransactionHistorySync.swift; sourceTree = ""; }; @@ -6118,6 +6139,15 @@ FAE6019D2ECA700100000001 /* SolanaSendService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SolanaSendService.swift; sourceTree = ""; }; FAE6019F2ECA700100000001 /* SolanaTransferTransactionBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SolanaTransferTransactionBuilderTests.swift; sourceTree = ""; }; FAE601A12ECA700100000001 /* SolanaSendServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SolanaSendServiceTests.swift; sourceTree = ""; }; + FAE601A32ECA700100000001 /* BitcoinHistoryOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitcoinHistoryOperationFactory.swift; sourceTree = ""; }; + FAE601A52ECA700100000001 /* BitcoinHistoryOperationFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitcoinHistoryOperationFactoryTests.swift; sourceTree = ""; }; + FAE601A72ECA700100000001 /* UniversalWalletAccountAddressResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UniversalWalletAccountAddressResolver.swift; sourceTree = ""; }; + FAE601A92ECA700100000001 /* UniversalWalletAccountAddressResolverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UniversalWalletAccountAddressResolverTests.swift; sourceTree = ""; }; + FAE601AB2ECA700100000001 /* SolanaHistoryOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SolanaHistoryOperationFactory.swift; sourceTree = ""; }; + FAE601AD2ECA700100000001 /* SolanaHistoryOperationFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SolanaHistoryOperationFactoryTests.swift; sourceTree = ""; }; + FAE601AF2ECA700100000001 /* IrohaHistoryOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IrohaHistoryOperationFactory.swift; sourceTree = ""; }; + FAE601B12ECA700100000001 /* IrohaHistoryOperationFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IrohaHistoryOperationFactoryTests.swift; sourceTree = ""; }; + FAE601B32ECA700100000001 /* PasskeyBackupContract.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasskeyBackupContract.swift; sourceTree = ""; }; FAE9EB9C288AB74D009390B6 /* AnalyticsRewardsFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsRewardsFlow.swift; sourceTree = ""; }; FAE9EB9E288ABBBE009390B6 /* AnalyticRewardsParachainViewModelState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticRewardsParachainViewModelState.swift; sourceTree = ""; }; FAE9EBA0288ABBC7009390B6 /* AnalyticRewardsParachainStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticRewardsParachainStrategy.swift; sourceTree = ""; }; @@ -7390,6 +7420,16 @@ path = ExportMnemonic; sourceTree = ""; }; + 3FB1F51B726C8907AE195C3D /* Compiled */ = { + isa = PBXGroup; + children = ( + CDB0BA173951B82DCA58B603 /* LegacyEcosystemUserDataModel_v12.mom */, + 550065CFAAF0723F31DDE3B5 /* CompatibleUserDataModel_v13.mom */, + ); + name = Compiled; + path = Compiled; + sourceTree = ""; + }; 41E829DF2B0F955B28BB6367 /* ExportMnemonic */ = { isa = PBXGroup; children = ( @@ -8205,6 +8245,7 @@ 84BEE62F26CBFDBC00757009 /* ChainRegistryTests.swift */, 8469936A26CD1BBE002CC786 /* RuntimePoolTests.swift */, 84D9C41026CD361C004AB2AB /* SpecVersionSubscriptionTests.swift */, + 189CE1DE8D520FEB61152078 /* ChainSyncServiceCompatibilityTests.swift */, ); path = ChainRegistry; sourceTree = ""; @@ -8245,6 +8286,8 @@ 84F13F0D26F1D9B1006725FF /* CrowdloadSettingsTests.swift */, 84F13F1326F20AA2006725FF /* StakingSettingsTests.swift */, 0A90ED622F74447700F741B6 /* StorageRequestTests.swift */, + 3675C75F2FBA7D24EBD5750A /* ChainModelMapperTests.swift */, + 15F0F954ADDD5A4E480392B0 /* TransformableArchiveSafetyTests.swift */, ); path = Storage; sourceTree = ""; @@ -8817,6 +8860,7 @@ 845B822826F0BB6700D25C72 /* CrowdloanChainSettings.swift */, 84F13F0726F13898006725FF /* StakingAssetSettings.swift */, 841AAC2E26F73E0C00F0A25E /* LocalStorageKeyFactory.swift */, + 9DA2374947A8488BE1F1CB17 /* CompatibilityModels */, ); path = Storage; sourceTree = ""; @@ -9014,6 +9058,10 @@ children = ( F418E87B264D14DA00699085 /* NetworkConnectionsMigratorTests.swift */, 849ECD3426DE70B900F542A3 /* SingleToMultiassetUserMigrationTests.swift */, + 36C64B63B4C227F2467C5C20 /* UserStorageCompatibilityMigrationTests.swift */, + 61421DD86EF588DEC4B9ED42 /* SubstrateStorageMigrationSafetyTests.swift */, + ADB5A97E2CECA69B16DCE8E3 /* SubstrateStorageClassResolutionTests.swift */, + 8EEC2067F2D6F4E9D194E239 /* CrashConsistentStoreReplacerResourceLimitTests.swift */, ); path = Migration; sourceTree = ""; @@ -9095,6 +9143,7 @@ 84C07EDA24AF1924008FC35B /* InfoPlist.strings */, 8490146024A94373008F705E /* Localizable.strings */, AE74EC1725FCF09000C494E7 /* Localizable.stringsdict */, + 67DC38DE23BBEF83BC331009 /* fearless-Bridging-Header.h */, ); path = fearless; sourceTree = ""; @@ -10268,6 +10317,8 @@ FA9278A727C382C600FF7B5B /* MultiassetV2.xcmappingmodel */, FA69A94627CE3476000352A6 /* SubstrateV2Mapping.xcmappingmodel */, FA28A9B129D2E451005AA42E /* MultiassetV9.xcmappingmodel */, + F191AB8E98289B7B14F71208 /* SubstrateV3toV4.xcmappingmodel */, + 3F646753DCEB6599248D9A31 /* SubstrateV2toV4.xcmappingmodel */, ); path = MigrationMappings; sourceTree = ""; @@ -10870,6 +10921,15 @@ path = CreateContact; sourceTree = ""; }; + 9DA2374947A8488BE1F1CB17 /* CompatibilityModels */ = { + isa = PBXGroup; + children = ( + 3FB1F51B726C8907AE195C3D /* Compiled */, + ); + name = CompatibilityModels; + path = CompatibilityModels; + sourceTree = ""; + }; A17E1D8ADC21C651CE346F73 /* CrowdloanContributionConfirm */ = { isa = PBXGroup; children = ( @@ -11969,6 +12029,10 @@ isa = PBXGroup; children = ( 46F82B507C2844DA90822C42 /* CoreDataAliases.swift */, + 9B1494192371496FAB4895B8 /* FearlessObjectiveCExceptionCatcher.m */, + C24D2309D225410A85B67B4A /* FearlessObjectiveCExceptionCatcher.h */, + 9FDEB6F26DCFB53EA1547948 /* SafeTransformableValueReader.swift */, + BD40C25879A8EC87DA2178B8 /* ManagedObjectRuntimeClassRegistry.swift */, ); path = CoreData; sourceTree = ""; @@ -15822,6 +15886,8 @@ FA93754A2AE928BE002CA482 /* types.json in Resources */, 742374EE778D76ABC965E107 /* StakingAmountViewController.xib in Resources */, EA8ECCD37FE5D6478018D3FC /* RecommendedValidatorListViewController.xib in Resources */, + A5EF098C5D53D306A7E84844 /* LegacyEcosystemUserDataModel_v12.mom in Resources */, + C3E367BDACDDDB4358823EE9 /* CompatibleUserDataModel_v13.mom in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -18938,6 +19004,11 @@ 68477096FFF2FE210D9C94B3 /* MultichainAssetSelectionViewController.swift in Sources */, 49F6A6C2A56A3DE9D456FE7D /* MultichainAssetSelectionViewLayout.swift in Sources */, 4D44ACFB841F7CE18CE98559 /* MultichainAssetSelectionAssembly.swift in Sources */, + 4FD1A1541952770AA5A153CB /* FearlessObjectiveCExceptionCatcher.m in Sources */, + A1161E67E9F24527E15AC229 /* SafeTransformableValueReader.swift in Sources */, + 16BD370B2E1EBEB7FF26A839 /* ManagedObjectRuntimeClassRegistry.swift in Sources */, + 2825B0FA4C33C5645A0DD2A7 /* SubstrateV3toV4.xcmappingmodel in Sources */, + E49A768AD7230A1844DD6C8D /* SubstrateV2toV4.xcmappingmodel in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -19153,6 +19224,13 @@ B40863AA7377BFE5F8A30259 /* LiquidityPoolSupplyConfirmTests.swift in Sources */, 6BF307ADE63FA92389340779 /* LiquidityPoolRemoveLiquidityTests.swift in Sources */, ECA54AB8148BBA63084353FD /* LiquidityPoolRemoveLiquidityConfirmTests.swift in Sources */, + 23418E85C0C9C5A35F115B95 /* UserStorageCompatibilityMigrationTests.swift in Sources */, + 3D764B6213C7A3B3F42C9170 /* SubstrateStorageMigrationSafetyTests.swift in Sources */, + 060167927467FDFE3B939051 /* SubstrateStorageClassResolutionTests.swift in Sources */, + 4D56353FBB1160698C542499 /* CrashConsistentStoreReplacerResourceLimitTests.swift in Sources */, + 76CD2DF3F59652CC0B3DB5BA /* ChainModelMapperTests.swift in Sources */, + 5E1DDD7E1A2F74184B3C0CE4 /* TransformableArchiveSafetyTests.swift in Sources */, + AF76DAB9F6DF8789FCF083E9 /* ChainSyncServiceCompatibilityTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -19454,6 +19532,7 @@ PRODUCT_BUNDLE_IDENTIFIER = jp.co.soramitsu.fearless; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ENABLE_EXPLICIT_MODULES = NO; + SWIFT_OBJC_BRIDGING_HEADER = "fearless/fearless-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; }; @@ -19484,6 +19563,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_ENABLE_EXPLICIT_MODULES = NO; + SWIFT_OBJC_BRIDGING_HEADER = "fearless/fearless-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; }; @@ -19626,6 +19706,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "fearlesswallet-dev-adhoc"; SWIFT_ENABLE_EXPLICIT_MODULES = NO; + SWIFT_OBJC_BRIDGING_HEADER = "fearless/fearless-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; }; diff --git a/fearless.xcodeproj/xcshareddata/xcschemes/fearless.tests.xcscheme b/fearless.xcodeproj/xcshareddata/xcschemes/fearless.tests.xcscheme index 83640ec2b0..da6f1dfedc 100644 --- a/fearless.xcodeproj/xcshareddata/xcschemes/fearless.tests.xcscheme +++ b/fearless.xcodeproj/xcshareddata/xcschemes/fearless.tests.xcscheme @@ -48,9 +48,12 @@ - + + + + + - diff --git a/fearless/Common/CoreData/CoreDataAliases.swift b/fearless/Common/CoreData/CoreDataAliases.swift index a9885ac577..4e9b6fa723 100644 --- a/fearless/Common/CoreData/CoreDataAliases.swift +++ b/fearless/Common/CoreData/CoreDataAliases.swift @@ -5,23 +5,39 @@ import Foundation import CoreData import RobinHood - // Lightweight stand-in for the private SSFAssetManagmentStorage pod. - // These classes only need to exist so the rest of the app can compile when - // the private pod is not available (e.g. in OSS builds or PRs). + // Runtime-compatible fallback for the unavailable SSFAssetManagmentStorage + // product. Objective-C names and managed properties must stay aligned with + // SubstrateDataModel_v8 so existing stores materialize the expected types. public enum SSFAssetManagmentStorageStub { + public enum CodingError: LocalizedError { + case unsupportedEntity(String) + + public var errorDescription: String? { + switch self { + case let .unsupportedEntity(entityName): + return "Core Data coding is unavailable for \(entityName)" + } + } + } + open class CoreDataStubObject: NSManagedObject, CoreDataCodable { - open func populate(from _: Decoder, using _: NSManagedObjectContext) throws {} + open func populate(from _: Decoder, using _: NSManagedObjectContext) throws { + throw CodingError.unsupportedEntity(entity.name ?? String(describing: type(of: self))) + } - open func encode(to _: Encoder) throws {} + open func encode(to _: Encoder) throws { + throw CodingError.unsupportedEntity(entity.name ?? String(describing: type(of: self))) + } } + @objc(CDChain) public class CDChain: CoreDataStubObject { @NSManaged public var rank: String? @NSManaged public var disabled: Bool @NSManaged public var chainId: String? @NSManaged public var parentId: String? @NSManaged public var name: String? - @NSManaged public var types: String? + @NSManaged public var types: URL? @NSManaged public var typesOverrideCommon: NSNumber? @NSManaged public var addressPrefix: Int16 @NSManaged public var icon: URL? @@ -41,10 +57,16 @@ import Foundation @NSManaged public var historyApiUrl: URL? @NSManaged public var crowdloansApiType: String? @NSManaged public var crowdloansApiUrl: URL? + @NSManaged public var pricingApiType: String? + @NSManaged public var pricingApiUrl: URL? + @NSManaged public var identityChain: String? + @NSManaged public var paraId: String? + @NSManaged public var isOrml: Bool @NSManaged public var xcmConfig: CDChainXcmConfig? @NSManaged public var explorers: NSSet? } + @objc(CDAsset) public class CDAsset: CoreDataStubObject { @NSManaged public var id: String? @NSManaged public var icon: URL? @@ -62,28 +84,101 @@ import Foundation @NSManaged public var ethereumType: String? @NSManaged public var priceProvider: CDPriceProvider? @NSManaged public var purchaseProviders: [String]? + @NSManaged public var chain: CDChain? + @NSManaged public var priceData: NSSet? } public class CDChainAsset: CoreDataStubObject {} + @objc(CDChainNode) public class CDChainNode: CoreDataStubObject { @NSManaged public var url: URL? @NSManaged public var name: String? @NSManaged public var apiKeyName: String? @NSManaged public var apiQueryName: String? + @NSManaged public var chain: CDChain? } + @objc(CDChainStorageItem) public class CDChainStorageItem: CoreDataStubObject { @NSManaged public var identifier: String? + @NSManaged public var data: Data? + + override public func populate(from decoder: Decoder, using _: NSManagedObjectContext) throws { + let container = try decoder.container(keyedBy: ChainStorageItem.CodingKeys.self) + + identifier = try container.decode(String.self, forKey: .identifier) + data = try container.decode(Data.self, forKey: .data) + } + + override public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: ChainStorageItem.CodingKeys.self) + + try container.encodeIfPresent(identifier, forKey: .identifier) + try container.encodeIfPresent(data, forKey: .data) + } + } + + @objc(CDRuntimeMetadataItem) + public class CDRuntimeMetadataItem: CoreDataStubObject { + private enum CodingKeys: String, CodingKey { + case chain + case version + case txVersion + case metadata + } + + @NSManaged public var identifier: String? + @NSManaged public var metadata: Data? + @NSManaged public var resolver: Data? + @NSManaged public var txVersion: Int32 + @NSManaged public var version: Int32 + + override public func populate(from decoder: Decoder, using _: NSManagedObjectContext) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + identifier = try container.decode(String.self, forKey: .chain) + metadata = try container.decode(Data.self, forKey: .metadata) + version = Int32(bitPattern: try container.decode(UInt32.self, forKey: .version)) + txVersion = Int32(bitPattern: try container.decode(UInt32.self, forKey: .txVersion)) + } + + override public func encode(to encoder: Encoder) throws { + guard let identifier, let metadata else { + return + } + + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(identifier, forKey: .chain) + try container.encode(UInt32(bitPattern: version), forKey: .version) + try container.encode(UInt32(bitPattern: txVersion), forKey: .txVersion) + try container.encode(metadata, forKey: .metadata) + } } - public class CDRuntimeMetadataItem: CoreDataStubObject {} public class CDAccountInfo: CoreDataStubObject { @NSManaged public var identifier: String? } + @objc(CDStashItem) public class CDStashItem: CoreDataStubObject { @NSManaged public var stash: String? @NSManaged public var controller: String? + + override public func populate(from decoder: Decoder, using _: NSManagedObjectContext) throws { + let stashItem = try StashItem(from: decoder) + + stash = stashItem.stash + controller = stashItem.controller + } + + override public func encode(to encoder: Encoder) throws { + guard let stash, let controller else { + return + } + + try StashItem(stash: stash, controller: controller).encode(to: encoder) + } } public class CDSingleValue: CoreDataStubObject {} @@ -140,6 +235,7 @@ import Foundation } public class CDAccountItem: CoreDataStubObject {} + @objc(CDPolkaswapRemoteSettings) public class CDPolkaswapRemoteSettings: CoreDataStubObject { @NSManaged public var version: String? @NSManaged public var availableSources: [String]? @@ -148,23 +244,130 @@ import Foundation @NSManaged public var xstusdId: String? } + @objc(CDPolkaswapDex) public class CDPolkaswapDex: CoreDataStubObject { @NSManaged public var name: String? @NSManaged public var code: Int32 @NSManaged public var assetId: String? } - public class CDPhishingItem: CoreDataStubObject {} - public class CDScamInfo: CoreDataStubObject {} - public class CDPriceData: CoreDataStubObject {} + @objc(CDPhishingItem) + public class CDPhishingItem: CoreDataStubObject { + @NSManaged public var identifier: String? + @NSManaged public var publicKey: String? + @NSManaged public var source: String? + + override public func populate(from decoder: Decoder, using _: NSManagedObjectContext) throws { + let phishingItem = try PhishingItem(from: decoder) + + identifier = phishingItem.identifier + source = phishingItem.source + publicKey = phishingItem.publicKey + } + + override public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: PhishingItem.CodingKeys.self) + + try container.encode(source, forKey: .source) + try container.encode(publicKey, forKey: .publicKey) + } + } + + @objc(CDScamInfo) + public class CDScamInfo: CoreDataStubObject { + @NSManaged public var address: String? + @NSManaged public var name: String? + @NSManaged public var subtype: String? + @NSManaged public var type: String? + + override public func populate(from decoder: Decoder, using _: NSManagedObjectContext) throws { + let container = try decoder.container(keyedBy: ScamInfo.CodingKeys.self) + + name = try container.decode(String.self, forKey: .name) + address = try container.decode(String.self, forKey: .address) + type = try container.decode(String.self, forKey: .type) + subtype = try container.decode(String.self, forKey: .subtype) + } + + override public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: ScamInfo.CodingKeys.self) + + try container.encode(name, forKey: .name) + try container.encode(address, forKey: .address) + try container.encode(type, forKey: .type) + try container.encode(subtype, forKey: .subtype) + } + } + + @objc(CDPriceData) + public class CDPriceData: CoreDataStubObject { + @NSManaged public var coingeckoPriceId: String? + @NSManaged public var currencyId: String? + @NSManaged public var fiatDayByChange: String? + @NSManaged public var price: String? + @NSManaged public var priceId: String? + @NSManaged public var asset: CDAsset? + } + + @objc(CDPriceProvider) public class CDPriceProvider: CoreDataStubObject { @NSManaged public var type: String? @NSManaged public var id: String? @NSManaged public var precision: String? + @NSManaged public var asset: CDAsset? + } + + @objc(CDContactItem) + public class CDContactItem: CoreDataStubObject { + @NSManaged public var identifier: String? + @NSManaged public var peerAddress: String? + @NSManaged public var peerName: String? + @NSManaged public var targetAddress: String? + @NSManaged public var updatedAt: Int64 + + override public func populate(from decoder: Decoder, using _: NSManagedObjectContext) throws { + let contact = try ContactItem(from: decoder) + + identifier = contact.identifier + peerAddress = contact.peerAddress + peerName = contact.peerName + targetAddress = contact.targetAddress + updatedAt = contact.updatedAt + } + + override public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: ContactItem.CodingKeys.self) + + try container.encodeIfPresent(peerAddress, forKey: .peerAddress) + try container.encodeIfPresent(peerName, forKey: .peerName) + try container.encodeIfPresent(targetAddress, forKey: .targetAddress) + try container.encode(updatedAt, forKey: .updatedAt) + } + } + + @objc(CDContact) + public class CDContact: CoreDataStubObject { + @NSManaged public var address: String? + @NSManaged public var chainId: String? + @NSManaged public var name: String? + + override public func populate(from decoder: Decoder, using _: NSManagedObjectContext) throws { + let container = try decoder.container(keyedBy: Contact.CodingKeys.self) + + name = try container.decode(String.self, forKey: .name) + address = try container.decode(String.self, forKey: .address) + chainId = try container.decode(String.self, forKey: .chainId) + } + + override public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: Contact.CodingKeys.self) + + try container.encode(name, forKey: .name) + try container.encode(address, forKey: .address) + try container.encode(chainId, forKey: .chainId) + } } - public class CDContactItem: CoreDataStubObject {} - public class CDContact: CoreDataStubObject {} public class CDCurrency: CoreDataStubObject { @NSManaged public var id: String? @NSManaged public var symbol: String? @@ -173,28 +376,36 @@ import Foundation @NSManaged public var isSelected: Bool } + @objc(CDExternalApi) public class CDExternalApi: CoreDataStubObject { @NSManaged public var type: String? @NSManaged public var types: NSArray? - @NSManaged public var url: URL? + @NSManaged public var url: String? + @NSManaged public var chain: CDChain? } + @objc(CDXcmAvailableDestination) public class CDXcmAvailableDestination: CoreDataStubObject { @NSManaged public var chainId: String? @NSManaged public var bridgeParachainId: String? @NSManaged public var assets: NSSet? + @NSManaged public var config: CDChainXcmConfig? } + @objc(CDXcmAvailableAsset) public class CDXcmAvailableAsset: CoreDataStubObject { @NSManaged public var id: String? + @NSManaged public var minAmount: String? @NSManaged public var symbol: String? } + @objc(CDChainXcmConfig) public class CDChainXcmConfig: CoreDataStubObject { @NSManaged public var xcmVersion: String? @NSManaged public var destWeightIsPrimitive: Bool @NSManaged public var availableAssets: NSSet? @NSManaged public var availableDestinations: NSSet? + @NSManaged public var chain: CDChain? } } @@ -216,23 +427,33 @@ public typealias CDChainAsset = SSFAssetManagmentStorage.CDChainAsset public typealias CDChainNode = SSFAssetManagmentStorage.CDChainNode public typealias CDChainStorageItem = SSFAssetManagmentStorage.CDChainStorageItem public typealias CDRuntimeMetadataItem = SSFAssetManagmentStorage.CDRuntimeMetadataItem -public typealias CDAccountInfo = SSFAssetManagmentStorage.CDAccountInfo public typealias CDStashItem = SSFAssetManagmentStorage.CDStashItem #if canImport(SSFSingleValueCache) public typealias CDSingleValue = SSFSingleValueCache.CDSingleValue #else public typealias CDSingleValue = SSFAssetManagmentStorage.CDSingleValue #endif -public typealias CDChainSettings = SSFAssetManagmentStorage.CDChainSettings -public typealias CDAssetVisibility = SSFAssetManagmentStorage.CDAssetVisibility -public typealias CDMetaAccount = SSFAssetManagmentStorage.CDMetaAccount -public typealias CDChainAccount = SSFAssetManagmentStorage.CDChainAccount -public typealias CDCustomChainNode = SSFAssetManagmentStorage.CDCustomChainNode +#if canImport(SSFAccountManagmentStorage) + public typealias CDAccountInfo = SSFAccountManagmentStorage.CDAccountInfo + public typealias CDChainSettings = SSFAccountManagmentStorage.CDChainSettings + public typealias CDAssetVisibility = SSFAccountManagmentStorage.CDAssetVisibility + public typealias CDMetaAccount = SSFAccountManagmentStorage.CDMetaAccount + public typealias CDChainAccount = SSFAccountManagmentStorage.CDChainAccount + public typealias CDCustomChainNode = SSFAccountManagmentStorage.CDCustomChainNode + public typealias CDCurrency = SSFAccountManagmentStorage.CDCurrency +#else + public typealias CDAccountInfo = SSFAssetManagmentStorage.CDAccountInfo + public typealias CDChainSettings = SSFAssetManagmentStorage.CDChainSettings + public typealias CDAssetVisibility = SSFAssetManagmentStorage.CDAssetVisibility + public typealias CDMetaAccount = SSFAssetManagmentStorage.CDMetaAccount + public typealias CDChainAccount = SSFAssetManagmentStorage.CDChainAccount + public typealias CDCustomChainNode = SSFAssetManagmentStorage.CDCustomChainNode + public typealias CDCurrency = SSFAssetManagmentStorage.CDCurrency +#endif public typealias CDPolkaswapRemoteSettings = SSFAssetManagmentStorage.CDPolkaswapRemoteSettings public typealias CDPhishingItem = SSFAssetManagmentStorage.CDPhishingItem public typealias CDPriceData = SSFAssetManagmentStorage.CDPriceData public typealias CDPriceProvider = SSFAssetManagmentStorage.CDPriceProvider -public typealias CDCurrency = SSFAssetManagmentStorage.CDCurrency public typealias CDContactItem = SSFAssetManagmentStorage.CDContactItem public typealias CDContact = SSFAssetManagmentStorage.CDContact public typealias CDAccountItem = SSFAssetManagmentStorage.CDAccountItem diff --git a/fearless/Common/CoreData/FearlessObjectiveCExceptionCatcher.h b/fearless/Common/CoreData/FearlessObjectiveCExceptionCatcher.h new file mode 100644 index 0000000000..ddb2b0c89d --- /dev/null +++ b/fearless/Common/CoreData/FearlessObjectiveCExceptionCatcher.h @@ -0,0 +1,27 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +FOUNDATION_EXPORT NSErrorDomain const FearlessObjectiveCExceptionErrorDomain; + +typedef NS_ERROR_ENUM( + FearlessObjectiveCExceptionErrorDomain, + FearlessObjectiveCExceptionErrorCode +) { + FearlessObjectiveCExceptionErrorCodeRaised = 1 +}; + +typedef id _Nullable (^FearlessObjectiveCObjectReadBlock)(void); + +/// Executes an Objective-C object read behind an exception boundary. +/// +/// The returned error is intentionally generic. Exception names, reasons, +/// userInfo, call stacks, and the object payload must never cross this boundary. +@interface FearlessObjectiveCExceptionCatcher : NSObject + ++ (nullable id)performObjectRead:(FearlessObjectiveCObjectReadBlock)block + caughtError:(NSError * _Nullable * _Nullable)caughtError; + +@end + +NS_ASSUME_NONNULL_END diff --git a/fearless/Common/CoreData/FearlessObjectiveCExceptionCatcher.m b/fearless/Common/CoreData/FearlessObjectiveCExceptionCatcher.m new file mode 100644 index 0000000000..672f7a17f4 --- /dev/null +++ b/fearless/Common/CoreData/FearlessObjectiveCExceptionCatcher.m @@ -0,0 +1,39 @@ +#import "FearlessObjectiveCExceptionCatcher.h" + +NSErrorDomain const FearlessObjectiveCExceptionErrorDomain = + @"jp.co.soramitsu.fearlesswallet.objective-c-exception"; + +@implementation FearlessObjectiveCExceptionCatcher + ++ (nullable id)performObjectRead:(FearlessObjectiveCObjectReadBlock)block + caughtError:(NSError * _Nullable * _Nullable)caughtError { + if (caughtError != NULL) { + *caughtError = nil; + } + + @try { + id value = block(); + + // A nil Objective-C result is conventionally interpreted by Swift as + // failure for NSError-imported methods. Use a private sentinel so a + // legitimate nil stored value remains distinguishable from an + // exception without manufacturing an error. + return value ?: [NSNull null]; + } @catch (NSException *exception) { + (void)exception; + + if (caughtError != NULL) { + *caughtError = [NSError + errorWithDomain:FearlessObjectiveCExceptionErrorDomain + code:FearlessObjectiveCExceptionErrorCodeRaised + userInfo:@{ + NSLocalizedDescriptionKey: + @"A stored value could not be decoded safely." + }]; + } + + return nil; + } +} + +@end diff --git a/fearless/Common/CoreData/ManagedObjectRuntimeClassRegistry.swift b/fearless/Common/CoreData/ManagedObjectRuntimeClassRegistry.swift new file mode 100644 index 0000000000..b358cd867c --- /dev/null +++ b/fearless/Common/CoreData/ManagedObjectRuntimeClassRegistry.swift @@ -0,0 +1,74 @@ +import CoreData +import Foundation + +#if canImport(SSFAssetManagmentStorage) + import SSFAssetManagmentStorage + private typealias RuntimeCDPolkaswapDex = + SSFAssetManagmentStorage.CDPolkaswapDex + private typealias RuntimeCDScamInfo = + SSFAssetManagmentStorage.CDScamInfo +#else + private typealias RuntimeCDPolkaswapDex = + SSFAssetManagmentStorageStub.CDPolkaswapDex + private typealias RuntimeCDScamInfo = + SSFAssetManagmentStorageStub.CDScamInfo +#endif + +/// Strong, production-used references to every managed-object class named by +/// the current bundled stores. This prevents normal Release dead stripping +/// from making a model class disappear only outside ENABLE_TESTABILITY builds. +enum ManagedObjectRuntimeClassRegistry { + static let substrateV8Classes: [NSManagedObject.Type] = [ + CDAsset.self, + CDChain.self, + CDChainNode.self, + CDChainStorageItem.self, + CDChainXcmConfig.self, + CDContact.self, + CDContactItem.self, + CDExternalApi.self, + CDPhishingItem.self, + RuntimeCDPolkaswapDex.self, + CDPolkaswapRemoteSettings.self, + CDPriceData.self, + CDPriceProvider.self, + CDRuntimeMetadataItem.self, + RuntimeCDScamInfo.self, + CDStashItem.self, + CDTransactionHistoryItem.self, + CDXcmAvailableAsset.self, + CDXcmAvailableDestination.self + ] + + static let userV11Classes: [NSManagedObject.Type] = [ + CDAccountInfo.self, + CDAssetVisibility.self, + CDChainAccount.self, + CDChainSettings.self, + CDCurrency.self, + CDCustomChainNode.self, + CDMetaAccount.self + ] + + static let classesByRuntimeName: [String: NSManagedObject.Type] = { + Dictionary( + uniqueKeysWithValues: (substrateV8Classes + userV11Classes).map { + (NSStringFromClass($0), $0) + } + ) + }() + + static func resolve(className: String) -> NSManagedObject.Type? { + if let registeredClass = classesByRuntimeName[className] { + return registeredClass + } + + if let registeredClass = classesByRuntimeName.first(where: { + $0.key.split(separator: ".").last.map(String.init) == className + })?.value { + return registeredClass + } + + return NSClassFromString(className) as? NSManagedObject.Type + } +} diff --git a/fearless/Common/CoreData/SafeTransformableValueReader.swift b/fearless/Common/CoreData/SafeTransformableValueReader.swift new file mode 100644 index 0000000000..670ef74972 --- /dev/null +++ b/fearless/Common/CoreData/SafeTransformableValueReader.swift @@ -0,0 +1,487 @@ +import CoreData +import Foundation +import SQLite3 + +enum SafeTransformableValueReaderError: LocalizedError, Equatable { + case objectiveCException + case unexpectedStoredType + + var errorDescription: String? { + switch self { + case .objectiveCException: + return "A stored value could not be decoded safely" + case .unexpectedStoredType: + return "A stored value has an unexpected type" + } + } +} + +enum SafeObjectiveCExceptionBoundary { + /// Preserves ordinary Swift errors while converting an Objective-C + /// exception into the single generic, payload-free storage error. + static func perform( + _ operation: @escaping () throws -> Void + ) throws { + var operationError: Error? + + do { + _ = try FearlessObjectiveCExceptionCatcher.performObjectRead { + do { + try operation() + } catch { + operationError = error + } + + return nil + } + } catch { + throw SafeTransformableValueReaderError.objectiveCException + } + + if let operationError { + throw operationError + } + } +} + +enum SafeTransformableValueReader { + static func readObject( + from object: NSManagedObject, + key: String + ) throws -> Any? { + do { + let value = try FearlessObjectiveCExceptionCatcher.performObjectRead { + object.value(forKey: key) + } + + return value is NSNull ? nil : value + } catch { + throw SafeTransformableValueReaderError.objectiveCException + } + } + + static func read( + from object: NSManagedObject, + key: String, + as _: Value.Type = Value.self + ) throws -> Value? { + let rawValue = try readObject(from: object, key: key) + + guard let rawValue else { + return nil + } + + guard let value = rawValue as? Value else { + throw SafeTransformableValueReaderError.unexpectedStoredType + } + + return value + } +} + +struct SQLiteTransformableArchiveColumn: Equatable { + let entityName: String + let attributeName: String + let isOptional: Bool + let valueTransformerName: String? +} + +enum SQLiteTransformableArchiveRepairError: LocalizedError, Equatable { + case invalidLimit + case unsafeIdentifier + case databaseUnavailable + case schemaMismatch(String, String) + case transformerUnavailable(String, String) + case sqliteOperationFailed(Int32) + case repairedValueCountOverflow + + var errorDescription: String? { + switch self { + case .invalidLimit: + return "The transformable archive byte limit is invalid" + case .unsafeIdentifier: + return "A transformable archive has an unsafe storage identifier" + case .databaseUnavailable: + return "The private Core Data store could not be opened safely" + case let .schemaMismatch(entityName, attributeName): + return """ + The private Core Data store is missing the expected \ + \(entityName).\(attributeName) archive column + """ + case let .transformerUnavailable(entityName, attributeName): + return """ + The private Core Data store cannot encode an empty \ + \(entityName).\(attributeName) archive + """ + case .sqliteOperationFailed: + return "The private Core Data archive repair failed safely" + case .repairedValueCountOverflow: + return "The private Core Data archive repair count overflowed" + } + } +} + +/// Bounds raw transformable BLOBs before Core Data can invoke a value +/// transformer. The caller must pass only model-verified, allowlisted columns +/// from a disposable SQLite store copy. +enum SQLiteTransformableArchivePreflight { + private static let sqliteTransient = unsafeBitCast( + -1, + to: sqlite3_destructor_type.self + ) + + @discardableResult + static func repairOversizedArchives( + at storeURL: URL, + columns: [SQLiteTransformableArchiveColumn], + maximumArchiveByteCount: Int + ) throws -> Int { + guard + maximumArchiveByteCount >= 0, + UInt64(maximumArchiveByteCount) <= UInt64(Int64.max) + else { + throw SQLiteTransformableArchiveRepairError.invalidLimit + } + + guard !columns.isEmpty else { + return 0 + } + + var database: OpaquePointer? + let openResult = sqlite3_open_v2( + storeURL.path, + &database, + SQLITE_OPEN_READWRITE | SQLITE_OPEN_FULLMUTEX, + nil + ) + guard openResult == SQLITE_OK, let database else { + if let database { + sqlite3_close_v2(database) + } + throw SQLiteTransformableArchiveRepairError + .databaseUnavailable + } + defer { + sqlite3_close_v2(database) + } + + sqlite3_extended_result_codes(database, 1) + try execute("BEGIN IMMEDIATE TRANSACTION", in: database) + + do { + var repairedValueCount = 0 + for column in columns { + try validateSchema( + for: column, + in: database + ) + let repairedCount = try repairOversizedArchives( + in: column, + maximumArchiveByteCount: + maximumArchiveByteCount, + database: database + ) + let addition = repairedValueCount + .addingReportingOverflow(repairedCount) + guard !addition.overflow else { + throw SQLiteTransformableArchiveRepairError + .repairedValueCountOverflow + } + repairedValueCount = addition.partialValue + } + + try execute("COMMIT TRANSACTION", in: database) + return repairedValueCount + } catch { + _ = try? execute("ROLLBACK TRANSACTION", in: database) + throw error + } + } + + private static func validateSchema( + for column: SQLiteTransformableArchiveColumn, + in database: OpaquePointer + ) throws { + let tableName = try physicalIdentifier( + for: column.entityName + ) + let columnName = try physicalIdentifier( + for: column.attributeName + ) + + var tableStatement: OpaquePointer? + let tableSQL = """ + SELECT COUNT(*) FROM sqlite_master + WHERE type = 'table' AND name = ?1 COLLATE BINARY + """ + try prepare( + tableSQL, + statement: &tableStatement, + in: database + ) + guard let tableStatement else { + throw SQLiteTransformableArchiveRepairError + .sqliteOperationFailed(SQLITE_INTERNAL) + } + defer { + sqlite3_finalize(tableStatement) + } + + guard + sqlite3_bind_text( + tableStatement, + 1, + tableName, + -1, + sqliteTransient + ) == SQLITE_OK, + sqlite3_step(tableStatement) == SQLITE_ROW, + sqlite3_column_int64(tableStatement, 0) == 1, + sqlite3_step(tableStatement) == SQLITE_DONE + else { + throw SQLiteTransformableArchiveRepairError + .schemaMismatch( + column.entityName, + column.attributeName + ) + } + + let quotedTableName = try quotedIdentifier(tableName) + var columnStatement: OpaquePointer? + try prepare( + "PRAGMA table_info(\(quotedTableName))", + statement: &columnStatement, + in: database + ) + guard let columnStatement else { + throw SQLiteTransformableArchiveRepairError + .sqliteOperationFailed(SQLITE_INTERNAL) + } + defer { + sqlite3_finalize(columnStatement) + } + + var matchingColumns = 0 + while true { + let stepResult = sqlite3_step(columnStatement) + if stepResult == SQLITE_DONE { + break + } + guard stepResult == SQLITE_ROW else { + throw SQLiteTransformableArchiveRepairError + .sqliteOperationFailed(stepResult) + } + + guard + let rawName = sqlite3_column_text( + columnStatement, + 1 + ) + else { + throw SQLiteTransformableArchiveRepairError + .sqliteOperationFailed(SQLITE_CORRUPT) + } + let name = String(cString: rawName) + if name == columnName { + guard + let rawType = sqlite3_column_text( + columnStatement, + 2 + ), + String(cString: rawType).uppercased() == "BLOB" + else { + throw SQLiteTransformableArchiveRepairError + .schemaMismatch( + column.entityName, + column.attributeName + ) + } + matchingColumns += 1 + } + } + + guard matchingColumns == 1 else { + throw SQLiteTransformableArchiveRepairError + .schemaMismatch( + column.entityName, + column.attributeName + ) + } + } + + private static func repairOversizedArchives( + in column: SQLiteTransformableArchiveColumn, + maximumArchiveByteCount: Int, + database: OpaquePointer + ) throws -> Int { + let tableName = try quotedIdentifier( + physicalIdentifier(for: column.entityName) + ) + let columnName = try quotedIdentifier( + physicalIdentifier(for: column.attributeName) + ) + let sql = """ + UPDATE \(tableName) + SET \(columnName) = ?1 + WHERE \(columnName) IS NOT NULL + AND ( + typeof(\(columnName)) != 'blob' + OR length(\(columnName)) > ?2 + ) + """ + + var statement: OpaquePointer? + try prepare(sql, statement: &statement, in: database) + guard let statement else { + throw SQLiteTransformableArchiveRepairError + .sqliteOperationFailed(SQLITE_INTERNAL) + } + defer { + sqlite3_finalize(statement) + } + + let replacementResult: Int32 + if column.isOptional { + replacementResult = sqlite3_bind_null(statement, 1) + } else if column.valueTransformerName != nil { + let replacement = try emptyArchiveData(for: column) + guard + replacement.count <= maximumArchiveByteCount + else { + throw SQLiteTransformableArchiveRepairError + .invalidLimit + } + replacementResult = replacement.withUnsafeBytes { + rawBuffer in + + sqlite3_bind_blob( + statement, + 1, + rawBuffer.baseAddress, + Int32(rawBuffer.count), + sqliteTransient + ) + } + } else { + throw SQLiteTransformableArchiveRepairError + .transformerUnavailable( + column.entityName, + column.attributeName + ) + } + + guard + replacementResult == SQLITE_OK, + sqlite3_bind_int64( + statement, + 2, + Int64(maximumArchiveByteCount) + ) == SQLITE_OK + else { + throw SQLiteTransformableArchiveRepairError + .sqliteOperationFailed(sqlite3_errcode(database)) + } + + let stepResult = sqlite3_step(statement) + guard stepResult == SQLITE_DONE else { + throw SQLiteTransformableArchiveRepairError + .sqliteOperationFailed(stepResult) + } + + return Int(sqlite3_changes(database)) + } + + private static func emptyArchiveData( + for column: SQLiteTransformableArchiveColumn + ) throws -> Data { + guard + let transformerName = column.valueTransformerName, + let transformer = ValueTransformer( + forName: NSValueTransformerName( + rawValue: transformerName + ) + ), + let data = transformer.reverseTransformedValue( + NSArray() + ) as? Data + else { + throw SQLiteTransformableArchiveRepairError + .transformerUnavailable( + column.entityName, + column.attributeName + ) + } + + return data + } + + private static func execute( + _ sql: String, + in database: OpaquePointer + ) throws { + let result = sqlite3_exec( + database, + sql, + nil, + nil, + nil + ) + guard result == SQLITE_OK else { + throw SQLiteTransformableArchiveRepairError + .sqliteOperationFailed(result) + } + } + + private static func prepare( + _ sql: String, + statement: inout OpaquePointer?, + in database: OpaquePointer + ) throws { + let result = sqlite3_prepare_v2( + database, + sql, + -1, + &statement, + nil + ) + guard result == SQLITE_OK else { + throw SQLiteTransformableArchiveRepairError + .sqliteOperationFailed(result) + } + } + + private static func physicalIdentifier( + for modelIdentifier: String + ) throws -> String { + let identifier = "Z\(modelIdentifier.uppercased())" + guard isSafeIdentifier(identifier) else { + throw SQLiteTransformableArchiveRepairError + .unsafeIdentifier + } + return identifier + } + + private static func quotedIdentifier( + _ identifier: String + ) throws -> String { + guard isSafeIdentifier(identifier) else { + throw SQLiteTransformableArchiveRepairError + .unsafeIdentifier + } + return "\"\(identifier)\"" + } + + private static func isSafeIdentifier( + _ identifier: String + ) -> Bool { + !identifier.isEmpty && + identifier.unicodeScalars.allSatisfy { + scalar in + + let value = scalar.value + return value == 95 || + (48 ... 57).contains(value) || + (65 ... 90).contains(value) + } + } +} diff --git a/fearless/Common/Extension/Storage/CDAccountInfo+CoreDataCodable.swift b/fearless/Common/Extension/Storage/CDAccountInfo+CoreDataCodable.swift index bac0585997..99939d8b15 100644 --- a/fearless/Common/Extension/Storage/CDAccountInfo+CoreDataCodable.swift +++ b/fearless/Common/Extension/Storage/CDAccountInfo+CoreDataCodable.swift @@ -1,11 +1,11 @@ -#if canImport(SSFAssetManagmentStorage) +#if canImport(SSFAccountManagmentStorage) import Foundation import CoreData import RobinHood import IrohaCrypto - import SSFAssetManagmentStorage + import SSFAccountManagmentStorage - extension CDAccountInfo: CoreDataCodable { + extension CDAccountInfo: @retroactive CoreDataCodable { public func populate(from decoder: Decoder, using _: NSManagedObjectContext) throws { let container = try decoder.container(keyedBy: ChainStorageItem.CodingKeys.self) diff --git a/fearless/Common/Helpers/AccountProviderFactory.swift b/fearless/Common/Helpers/AccountProviderFactory.swift index 1064a6ba3b..745d352feb 100644 --- a/fearless/Common/Helpers/AccountProviderFactory.swift +++ b/fearless/Common/Helpers/AccountProviderFactory.swift @@ -1,3 +1,4 @@ +import CoreData import Foundation import IrohaCrypto import RobinHood @@ -34,15 +35,11 @@ final class AccountProviderFactory: AccountProviderFactoryProtocol { let mapper = MetaAccountMapper() let filter = NSPredicate.filterAccountItemByAccountId(accountId) - let repository: CoreDataRepository = storageFacade - .createRepository( - filter: filter, - sortDescriptors: [], - mapper: AnyCoreDataMapper(mapper) - ) + let repository = AccountRepositoryFactory(storageFacade: storageFacade) + .createMetaAccountRepository(for: filter, sortDescriptors: []) let hexAccountId = accountId.toHex() - let observable = CoreDataContextObservable( + let observable = TolerantMetaAccountContextObservable( service: storageFacade.databaseService, mapper: AnyCoreDataMapper(mapper), predicate: { [hexAccountId] metaAccount in @@ -58,7 +55,42 @@ final class AccountProviderFactory: AccountProviderFactoryProtocol { return StreamableProvider( source: AnyStreamableSource(EmptyStreamableSource()), - repository: AnyDataProviderRepository(repository), + repository: repository, + observable: AnyDataProviderRepositoryObservable(observable), + operationManager: operationManager + ) + } + + /// Builds the same observable managed-wallet stream as `StorageFacade.createStreamableProvider`, + /// while using the tolerant repository for its initial fetch and paging. + /// + /// The observable deliberately watches every wallet change. Consumers such as banners use an + /// `isSelected` repository filter but still need to observe a wallet becoming unselected. + func createManagedMetaAccountProvider( + for filter: NSPredicate?, + sortDescriptors: [NSSortDescriptor] + ) -> StreamableProvider { + let mapper = ManagedMetaAccountMapper() + let repository = AccountRepositoryFactory(storageFacade: storageFacade) + .createManagedMetaAccountRepository( + for: filter, + sortDescriptors: sortDescriptors + ) + let observable = TolerantMetaAccountContextObservable( + service: storageFacade.databaseService, + mapper: AnyCoreDataMapper(mapper), + predicate: { _ in true } + ) + + observable.start { [weak self] error in + if let error { + self?.logger?.error("Did receive error: \(error)") + } + } + + return StreamableProvider( + source: AnyStreamableSource(EmptyStreamableSource()), + repository: repository, observable: AnyDataProviderRepositoryObservable(observable), operationManager: operationManager ) @@ -82,3 +114,273 @@ final class AccountProviderFactory: AccountProviderFactoryProtocol { } } } + +/// A Core Data observable that quarantines malformed wallet rows for their full lifecycle. +/// +/// RobinHood's generic observable drops mapper failures for inserts and updates, but emits deletes +/// directly from the raw identifier. A corrupt row that duplicates a healthy wallet identifier can +/// therefore remove the healthy item from a stream when that corrupt row is deleted. Tracking +/// successfully mapped rows by object ID makes deletes unambiguous and also turns a valid-to-corrupt +/// update into removal of that one previously valid object. +private final class TolerantMetaAccountContextObservable: + DataProviderRepositoryObservable { + private struct MappedObject { + let objectId: NSManagedObjectID + let model: Model? + } + + private let service: CoreDataServiceProtocol + private let mapper: AnyCoreDataMapper + private let predicate: (CDMetaAccount) -> Bool + private let processingQueue: DispatchQueue + + private var observers: [RepositoryObserver] = [] + private var trackedModels: [NSManagedObjectID: Model] = [:] + private var notificationToken: NSObjectProtocol? + + init( + service: CoreDataServiceProtocol, + mapper: AnyCoreDataMapper, + predicate: @escaping (CDMetaAccount) -> Bool, + processingQueue: DispatchQueue? = nil + ) { + self.service = service + self.mapper = mapper + self.predicate = predicate + self.processingQueue = processingQueue ?? DispatchQueue( + label: "jp.co.fearless.wallet-observable.\(UUID().uuidString)", + qos: .utility + ) + } + + func start(completionBlock: @escaping (Error?) -> Void) { + service.performAsync { [weak self] optionalContext, optionalError in + guard let self else { + completionBlock(optionalError) + return + } + guard optionalError == nil, let context = optionalContext else { + completionBlock(optionalError) + return + } + + let token = NotificationCenter.default.addObserver( + forName: .NSManagedObjectContextDidSave, + object: context, + queue: nil + ) { [weak self] notification in + self?.didReceive(notification: notification) + } + self.notificationToken = token + + do { + let request = NSFetchRequest( + entityName: String(describing: CDMetaAccount.self) + ) + request.includesSubentities = false + let initialModels = try context.fetch(request).compactMap { + self.mappedObject(for: $0) + } + + self.processingQueue.async { + self.trackedModels = Dictionary( + uniqueKeysWithValues: initialModels.compactMap { + guard let model = $0.model else { + return nil + } + + return ($0.objectId, model) + } + ) + completionBlock(nil) + } + } catch { + NotificationCenter.default.removeObserver(token) + self.notificationToken = nil + completionBlock(error) + } + } + } + + func stop(completionBlock: @escaping (Error?) -> Void) { + service.performAsync { [weak self] _, optionalError in + guard let self else { + completionBlock(optionalError) + return + } + + if let notificationToken = self.notificationToken { + NotificationCenter.default.removeObserver(notificationToken) + self.notificationToken = nil + } + + self.processingQueue.async { + self.trackedModels.removeAll() + completionBlock(optionalError) + } + } + } + + func addObserver( + _ observer: AnyObject, + deliverOn queue: DispatchQueue, + executing updateBlock: @escaping ([DataProviderChange]) -> Void + ) { + processingQueue.async { + self.observers = self.observers.filter { $0.observer != nil } + + guard !self.observers.contains(where: { $0.observer === observer }) else { + return + } + + self.observers.append( + RepositoryObserver( + observer: observer, + queue: queue, + updateBlock: updateBlock + ) + ) + } + } + + func removeObserver(_ observer: AnyObject) { + processingQueue.async { + self.observers = self.observers.filter { + $0.observer != nil && $0.observer !== observer + } + } + } + + private func didReceive(notification: Notification) { + let updatedObjects = metaAccounts( + in: notification, + userInfoKey: NSUpdatedObjectsKey + ).map(mappedObject) + let deletedObjectIds = metaAccounts( + in: notification, + userInfoKey: NSDeletedObjectsKey + ).map(\.objectID) + let insertedObjects = metaAccounts( + in: notification, + userInfoKey: NSInsertedObjectsKey + ).map(mappedObject) + + guard + !updatedObjects.isEmpty || + !deletedObjectIds.isEmpty || + !insertedObjects.isEmpty + else { + return + } + + processingQueue.async { + var changes: [DataProviderChange] = [] + + for object in updatedObjects { + self.applyMappedObject( + object, + preferredChange: .update, + to: &changes + ) + } + for objectId in deletedObjectIds { + if let previousModel = self.trackedModels.removeValue(forKey: objectId) { + changes.append( + .delete(deletedIdentifier: previousModel.identifier) + ) + } + } + for object in insertedObjects { + self.applyMappedObject( + object, + preferredChange: .insert, + to: &changes + ) + } + + self.deliver(changes) + } + } + + private enum PreferredChange { + case insert + case update + } + + private func applyMappedObject( + _ object: MappedObject, + preferredChange: PreferredChange, + to changes: inout [DataProviderChange] + ) { + guard let model = object.model else { + if let previousModel = trackedModels.removeValue(forKey: object.objectId) { + changes.append( + .delete(deletedIdentifier: previousModel.identifier) + ) + } + return + } + + if let previousModel = trackedModels[object.objectId] { + if previousModel.identifier == model.identifier { + changes.append(.update(newItem: model)) + } else { + changes.append( + .delete(deletedIdentifier: previousModel.identifier) + ) + changes.append(.insert(newItem: model)) + } + } else { + switch preferredChange { + case .insert, .update: + // A previously corrupt/nonmatching object becoming valid is new to the stream, + // even though Core Data reports it as an update. + changes.append(.insert(newItem: model)) + } + } + + trackedModels[object.objectId] = model + } + + private func mappedObject(for entity: CDMetaAccount) -> MappedObject { + let model: Model? + if predicate(entity) { + model = try? mapper.transform(entity: entity) + } else { + model = nil + } + + return MappedObject( + objectId: entity.objectID, + model: model + ) + } + + private func metaAccounts( + in notification: Notification, + userInfoKey: String + ) -> [CDMetaAccount] { + guard let objects = notification.userInfo?[userInfoKey] as? NSSet else { + return [] + } + + return objects.allObjects.compactMap { $0 as? CDMetaAccount } + } + + private func deliver(_ changes: [DataProviderChange]) { + guard !changes.isEmpty else { + return + } + + observers = observers.filter { $0.observer != nil } + for observer in observers { + if processingQueue == observer.queue { + observer.updateBlock(changes) + } else { + observer.queue.async { + observer.updateBlock(changes) + } + } + } + } +} diff --git a/fearless/Common/Helpers/AccountRepositoryFactory.swift b/fearless/Common/Helpers/AccountRepositoryFactory.swift index 07dc5671ce..1c58085f93 100644 --- a/fearless/Common/Helpers/AccountRepositoryFactory.swift +++ b/fearless/Common/Helpers/AccountRepositoryFactory.swift @@ -29,43 +29,106 @@ final class AccountRepositoryFactory: AccountRepositoryFactoryProtocol { for filter: NSPredicate?, sortDescriptors: [NSSortDescriptor] ) -> AnyDataProviderRepository { - let mapper = MetaAccountMapper() - - let repository = storageFacade.createRepository( - filter: filter, - sortDescriptors: sortDescriptors, - mapper: AnyCoreDataMapper(mapper) + AnyDataProviderRepository( + createTolerantRepository( + filter: filter, + sortDescriptors: sortDescriptors, + project: \.wallet, + makeUpdate: { + MetaAccountSelectionModel( + identifier: $0.identifier, + wallet: $0, + isSelected: false, + order: ManagedMetaAccountModel.noOrder, + updatesWalletPayload: true, + updatesSelection: false + ) + } + ) ) - - return AnyDataProviderRepository(repository) } func createManagedMetaAccountRepository( for filter: NSPredicate?, sortDescriptors: [NSSortDescriptor] ) -> AnyDataProviderRepository { - let mapper = ManagedMetaAccountMapper() - - let repository = storageFacade.createRepository( - filter: filter, - sortDescriptors: sortDescriptors, - mapper: AnyCoreDataMapper(mapper) + AnyDataProviderRepository( + createTolerantRepository( + filter: filter, + sortDescriptors: sortDescriptors, + project: { projection in + projection.wallet.map { + ManagedMetaAccountModel( + info: $0, + isSelected: projection.isSelected, + order: projection.order + ) + } + }, + makeUpdate: { + MetaAccountSelectionModel( + identifier: $0.identifier, + wallet: $0.info, + isSelected: $0.isSelected, + order: $0.order, + updatesWalletPayload: true + ) + } + ) ) - - return AnyDataProviderRepository(repository) } func createAsyncMetaAccountRepository( for filter: NSPredicate?, sortDescriptors: [NSSortDescriptor] ) -> AsyncAnyRepository { - let mapper = MetaAccountMapper() - let repository = storageFacade.createAsyncRepository( - filter: filter, + let repository = createMetaAccountRepository( + for: filter, + sortDescriptors: sortDescriptors + ) + + return AsyncAnyRepository( + AsyncOperationBackedRepository(repository: repository) + ) + } + + private func createTolerantRepository( + filter: NSPredicate?, + sortDescriptors: [NSSortDescriptor], + project: @escaping (MetaAccountSelectionModel) -> Model?, + makeUpdate: @escaping (Model) -> MetaAccountSelectionModel + ) -> TolerantMetaAccountRepository { + let supportedWalletFilter = NSPredicate( + format: "%K != nil AND %K != nil", + #keyPath(CDMetaAccount.substrateAccountId), + #keyPath(CDMetaAccount.substratePublicKey) + ) + let readFilter = filter.map { + NSCompoundPredicate( + andPredicateWithSubpredicates: [$0, supportedWalletFilter] + ) + } ?? supportedWalletFilter + let readRepository = storageFacade.createRepository( + filter: readFilter, sortDescriptors: sortDescriptors, - mapper: AnyCoreDataMapper(mapper) + mapper: AnyCoreDataMapper(MetaAccountSelectionMapper()) + ) + let writeRepository = storageFacade.createRepository( + mapper: AnyCoreDataMapper(MetaAccountSelectionMapper()) + ) + let deleteRepository = storageFacade.createRepository( + filter: filter, + sortDescriptors: [], + mapper: AnyCoreDataMapper(MetaAccountSelectionMapper()) + ) + + return TolerantMetaAccountRepository( + readRepository: readRepository, + writeRepository: writeRepository, + deleteRepository: deleteRepository, + project: project, + makeUpdate: makeUpdate ) - return AsyncAnyRepository(repository) } } @@ -73,9 +136,444 @@ extension AccountRepositoryFactory { static func createRepository( for storageFacade: StorageFacadeProtocol = UserDataStorageFacade.shared ) -> AnyDataProviderRepository { - let mapper = MetaAccountMapper() - let repository = storageFacade.createRepository(mapper: AnyCoreDataMapper(mapper)) + AccountRepositoryFactory(storageFacade: storageFacade) + .createMetaAccountRepository(for: nil, sortDescriptors: []) + } +} + +enum TolerantMetaAccountRepositoryError: LocalizedError, Equatable { + case invalidSliceRequest + + var errorDescription: String? { + switch self { + case .invalidSliceRequest: + return "The repository slice request has an unsupported shape or range" + } + } +} + +/// RobinHood keeps `RepositorySliceRequest` fields internal. Decode its reflected value through +/// one validated boundary so paging can happen after damaged rows have been projected out. +/// Any dependency shape change fails closed instead of silently returning the wrong page. +struct ValidatedRepositorySlice { + typealias ReflectedField = (label: String?, value: Any) + + let offset: Int + let count: Int + let reversed: Bool + + var isEmpty: Bool { + count == 0 + } + + init(request: RepositorySliceRequest) throws { + let mirror = Mirror(reflecting: request) + guard mirror.displayStyle == .struct else { + throw TolerantMetaAccountRepositoryError.invalidSliceRequest + } + + try self.init(reflectedFields: Array(mirror.children)) + } + + init(reflectedFields: [ReflectedField]) throws { + guard reflectedFields.count == 3 else { + throw TolerantMetaAccountRepositoryError.invalidSliceRequest + } + + var values: [String: Any] = [:] + let expectedLabels = Set(["offset", "count", "reversed"]) + + for field in reflectedFields { + guard + let label = field.label, + expectedLabels.contains(label), + values.updateValue(field.value, forKey: label) == nil + else { + throw TolerantMetaAccountRepositoryError.invalidSliceRequest + } + } + + guard + Set(values.keys) == expectedLabels, + let offset = values["offset"] as? Int, + let count = values["count"] as? Int, + let reversed = values["reversed"] as? Bool, + offset >= 0, + count >= 0 + else { + throw TolerantMetaAccountRepositoryError.invalidSliceRequest + } + + self.offset = offset + self.count = count + self.reversed = reversed + } +} + +private final class TolerantMetaAccountRepository: DataProviderRepositoryProtocol { + fileprivate typealias ProjectionRepository = CoreDataRepository< + MetaAccountSelectionModel, + CDMetaAccount + > + + private let readRepository: ProjectionRepository + private let writeRepository: ProjectionRepository + private let deleteRepository: ProjectionRepository + private let project: (MetaAccountSelectionModel) -> Model? + private let makeUpdate: (Model) -> MetaAccountSelectionModel + private let operationQueue = OperationQueue() + + fileprivate init( + readRepository: ProjectionRepository, + writeRepository: ProjectionRepository, + deleteRepository: ProjectionRepository, + project: @escaping (MetaAccountSelectionModel) -> Model?, + makeUpdate: @escaping (Model) -> MetaAccountSelectionModel + ) { + self.readRepository = readRepository + self.writeRepository = writeRepository + self.deleteRepository = deleteRepository + self.project = project + self.makeUpdate = makeUpdate + } + + func fetchOperation( + by modelIdsClosure: @escaping () throws -> [String], + options: RepositoryFetchOptions + ) -> BaseOperation<[Model]> { + ClosureOperation { + let identifiers = Set(try modelIdsClosure()) + let projections = try self.fetchAllVisibleProjections(options: options) + + return try self.projectModels( + projections.filter { identifiers.contains($0.identifier) } + ) + } + } + + func fetchOperation( + by modelIdClosure: @escaping () throws -> String, + options: RepositoryFetchOptions + ) -> BaseOperation { + ClosureOperation { + let identifier = try modelIdClosure() + let projections = try self.fetchAllVisibleProjections(options: options) + + return try self.projectModels( + projections.filter { $0.identifier == identifier } + ).first + } + } + + func fetchAllOperation( + with options: RepositoryFetchOptions + ) -> BaseOperation<[Model]> { + ClosureOperation { + try self.projectModels( + self.fetchAllVisibleProjections(options: options) + ) + } + } + + func fetchOperation( + by request: RepositorySliceRequest, + options: RepositoryFetchOptions + ) -> BaseOperation<[Model]> { + ClosureOperation { + let slice = try ValidatedRepositorySlice(request: request) + let visibleModels = try self.projectModels( + self.fetchAllVisibleProjections(options: options) + ) + let orderedModels = slice.reversed + ? Array(visibleModels.reversed()) + : visibleModels + + guard + !slice.isEmpty, + slice.offset < orderedModels.count + else { + return [] + } + + return Array( + orderedModels + .dropFirst(slice.offset) + .prefix(slice.count) + ) + } + } + + func saveOperation( + _ updateModelsBlock: @escaping () throws -> [Model], + _ deleteIdsBlock: @escaping () throws -> [String] + ) -> BaseOperation { + ClosureOperation { + let models = try updateModelsBlock() + let requestedDeleteIds = try deleteIdsBlock() + try self.save(models: models, requestedDeleteIds: requestedDeleteIds) + } + } + + func saveBatchOperation( + _ updateModelsBlock: @escaping () throws -> [Model], + _ deleteIdsBlock: @escaping () throws -> [String] + ) -> BaseOperation { + saveOperation(updateModelsBlock, deleteIdsBlock) + } + + func replaceOperation( + _ newModelsBlock: @escaping () throws -> [Model] + ) -> BaseOperation { + ClosureOperation { + let models = try newModelsBlock() + try self.validateUniqueIdentifiers(models.map(\.identifier)) + try self.validateNoUnsupportedCollisions(for: models.map(\.identifier)) + + let existingOperation = self.readRepository.fetchAllOperation( + with: RepositoryFetchOptions( + includesProperties: true, + includesSubentities: true + ) + ) + let existing = try self.execute(existingOperation) + let replacementIds = Set(models.map(\.identifier)) + let supportedIdsToDelete: [String] = existing.compactMap { projection -> String? in + guard + projection.wallet != nil, + !replacementIds.contains(projection.identifier) + else { + return nil + } + + return projection.identifier + } + try self.validateUnambiguousStoredIdentifiers( + supportedIdsToDelete + ) + + try self.persist( + models: models, + deleteIds: supportedIdsToDelete + ) + } + } + + func fetchCountOperation() -> BaseOperation { + ClosureOperation { + let operation = self.readRepository.fetchAllOperation( + with: RepositoryFetchOptions( + includesProperties: true, + includesSubentities: true + ) + ) + + return try self.projectModels(self.execute(operation)).count + } + } + + func deleteAllOperation() -> BaseOperation { + ClosureOperation { + let operation = self.deleteRepository.deleteAllOperation() + _ = try self.execute(operation) + } + } + + private func save( + models: [Model], + requestedDeleteIds: [String] + ) throws { + try validateUniqueIdentifiers(models.map(\.identifier)) + try validateNoUnsupportedCollisions(for: models.map(\.identifier)) + try validateUnambiguousStoredIdentifiers(requestedDeleteIds) + + let requestedDeleteIdSet = Set(requestedDeleteIds) + let visibleProjections = requestedDeleteIdSet.isEmpty + ? [] + : try fetchAllVisibleProjections( + options: RepositoryFetchOptions( + includesProperties: true, + includesSubentities: true + ) + ) + let safeDeleteIds: [String] = visibleProjections.compactMap { projection -> String? in + guard + projection.wallet != nil, + requestedDeleteIdSet.contains(projection.identifier) + else { + return nil + } + + return projection.identifier + } + + try persist(models: models, deleteIds: safeDeleteIds) + } + + private func validateUniqueIdentifiers(_ identifiers: [String]) throws { + guard Set(identifiers).count == identifiers.count else { + throw SelectedWalletSettingsError.duplicateWalletIdentifier + } + } + + private func projectModels( + _ projections: [MetaAccountSelectionModel] + ) throws -> [Model] { + let models = projections.compactMap(project) + try validateUniqueIdentifiers(models.map(\.identifier)) + return models + } + + private func validateNoUnsupportedCollisions( + for identifiers: [String] + ) throws { + guard !identifiers.isEmpty else { + return + } + + let identifierSet = Set(identifiers) + let operation = writeRepository.fetchAllOperation( + with: RepositoryFetchOptions( + includesProperties: true, + includesSubentities: true + ) + ) + let existing = try execute(operation).filter { + identifierSet.contains($0.identifier) + } + + guard Set(existing.map(\.identifier)).count == existing.count else { + throw SelectedWalletSettingsError.duplicateWalletIdentifier + } + + guard !existing.contains(where: { $0.wallet == nil }) else { + throw SelectedWalletSettingsError.unsupportedWalletIdentifierConflict + } + } + + private func validateUnambiguousStoredIdentifiers( + _ identifiers: [String] + ) throws { + let identifierSet = Set(identifiers) + guard !identifierSet.isEmpty else { + return + } + + let operation = writeRepository.fetchAllOperation( + with: RepositoryFetchOptions( + includesProperties: true, + includesSubentities: true + ) + ) + let matchingProjections = try execute(operation).filter { + identifierSet.contains($0.identifier) + } + let projectionsByIdentifier = Dictionary( + grouping: matchingProjections, + by: \.identifier + ) + + guard projectionsByIdentifier.values.allSatisfy({ $0.count <= 1 }) else { + // CoreDataRepository deletes the first matching entity. Duplicate identifiers + // would therefore make deletion depend on persistent-store fetch order and could + // destroy a quarantined or unsupported wallet instead of its healthy sibling. + throw SelectedWalletSettingsError.duplicateWalletIdentifier + } + } + + private func fetchAllVisibleProjections( + options: RepositoryFetchOptions + ) throws -> [MetaAccountSelectionModel] { + let operation = readRepository.fetchAllOperation(with: options) + return try execute(operation) + } + + private func persist( + models: [Model], + deleteIds: [String] + ) throws { + let updates = models.map(makeUpdate) + let operation = writeRepository.saveOperation( + { updates }, + { deleteIds } + ) + + _ = try execute(operation) + } + + private func execute( + _ operation: BaseOperation + ) throws -> Result { + operationQueue.addOperations([operation], waitUntilFinished: true) + return try operation.extractNoCancellableResultData() + } +} + +private final class AsyncOperationBackedRepository: AsyncCoreDataRepository { + private let repository: AnyDataProviderRepository + private let operationQueue = OperationQueue() + + init(repository: AnyDataProviderRepository) { + self.repository = repository + } + + func fetch( + by modelIds: [String], + options: RepositoryFetchOptions + ) async throws -> [Model] { + try await execute( + repository.fetchOperation( + by: { modelIds }, + options: options + ) + ) + } + + func fetch( + by modelId: String, + options: RepositoryFetchOptions + ) async throws -> Model? { + try await execute( + repository.fetchOperation( + by: { modelId }, + options: options + ) + ) + } + + func fetchAll( + with options: RepositoryFetchOptions + ) async throws -> [Model] { + try await execute(repository.fetchAllOperation(with: options)) + } + + func save(models: [Model], deleteIds: [String]) async { + let operation = repository.saveOperation( + { models }, + { deleteIds } + ) + + await withCheckedContinuation { continuation in + operation.completionBlock = { + continuation.resume() + } + operationQueue.addOperation(operation) + } + } - return AnyDataProviderRepository(repository) + private func execute( + _ operation: BaseOperation + ) async throws -> Result { + try await withCheckedThrowingContinuation { continuation in + operation.completionBlock = { + do { + continuation.resume( + returning: try operation.extractNoCancellableResultData() + ) + } catch { + continuation.resume(throwing: error) + } + } + operationQueue.addOperation(operation) + } } } diff --git a/fearless/Common/Helpers/AssetModelMapper.swift b/fearless/Common/Helpers/AssetModelMapper.swift index a1517cc134..980bfb5ed0 100644 --- a/fearless/Common/Helpers/AssetModelMapper.swift +++ b/fearless/Common/Helpers/AssetModelMapper.swift @@ -51,7 +51,11 @@ extension AssetModelMapper: CoreDataMapperProtocol { } else { staking = nil } - let purchaseProviders: [SSFModels.PurchaseProvider]? = entity.purchaseProviders?.compactMap { + let storedPurchaseProviders: [String]? = try? SafeTransformableValueReader.read( + from: entity, + key: "purchaseProviders" + ) + let purchaseProviders: [SSFModels.PurchaseProvider]? = storedPurchaseProviders?.compactMap { SSFModels.PurchaseProvider(rawValue: $0) } diff --git a/fearless/Common/Helpers/ChainRepositoryFactory.swift b/fearless/Common/Helpers/ChainRepositoryFactory.swift index 0f35a5ff67..462af4b93c 100644 --- a/fearless/Common/Helpers/ChainRepositoryFactory.swift +++ b/fearless/Common/Helpers/ChainRepositoryFactory.swift @@ -1,3 +1,4 @@ +import CoreData import Foundation import RobinHood #if canImport(SSFAssetManagmentStorage) @@ -36,3 +37,629 @@ final class ChainRepositoryFactory { ) } } + +extension ChainRepositoryFactory { + func createMalformedChainCleanupOperation() -> BaseOperation { + ClosureOperation { [storageFacade] in + let semaphore = DispatchSemaphore(value: 0) + let result = ChainCleanupResultBox() + + storageFacade.databaseService.performAsync { context, error in + defer { semaphore.signal() } + + do { + if let error { + throw error + } + + guard let context else { + throw CoreDataRepositoryError.undefined + } + + // Cleanup traverses every persisted CDChain scalar and + // relationship. Treat that entire graph, including save, + // as one exception-safe transaction so a damaged generated + // getter cannot terminate cold boot or leave a partial + // quarantine/merge in the context. + try SafeObjectiveCExceptionBoundary.perform { + let request = NSFetchRequest( + entityName: String(describing: CDChain.self) + ) + let chains = try context.fetch(request) + self.cleanupMalformedChains( + chains, + in: context + ) + + if context.hasChanges { + try context.save() + } + } + + result.set(.success(())) + } catch { + // A damaged generated accessor can also be touched while + // Core Data unwinds a failed transaction. Keep rollback + // inside the same exception-safe contract so the recovery + // path cannot terminate the process. + try? SafeObjectiveCExceptionBoundary.perform { + context?.rollback() + } + result.set(.failure(error)) + } + } + + semaphore.wait() + return try result.get().get() + } + } + + private func cleanupMalformedChains( + _ chains: [CDChain], + in context: NSManagedObjectContext + ) { + var chainsByCanonicalIdentifier: [String: [CDChain]] = [:] + var chainsWithoutIdentifier: [CDChain] = [] + + for chain in chains { + guard let rawIdentifier = chain.chainId else { + chainsWithoutIdentifier.append(chain) + continue + } + + let canonicalIdentifier = rawIdentifier.trimmingCharacters( + in: .whitespacesAndNewlines + ) + + guard canonicalIdentifier.isNotEmpty else { + chainsWithoutIdentifier.append(chain) + continue + } + + chainsByCanonicalIdentifier[ + canonicalIdentifier, + default: [] + ].append(chain) + } + + var occupiedIdentifiers = Set( + chainsByCanonicalIdentifier.keys + ) + for chain in chainsWithoutIdentifier.sorted(by: { + $0.objectID.uriRepresentation().absoluteString + < $1.objectID.uriRepresentation().absoluteString + }) { + quarantineChain( + chain, + occupiedIdentifiers: &occupiedIdentifiers + ) + } + + for (canonicalIdentifier, collidingChains) in + chainsByCanonicalIdentifier { + let orderedChains = collidingChains.sorted { + $0.objectID.uriRepresentation().absoluteString + < $1.objectID.uriRepresentation().absoluteString + } + guard + let target = orderedChains.first(where: { + $0.chainId == canonicalIdentifier + }) ?? orderedChains.first + else { + continue + } + + for source in orderedChains where source !== target { + if let mergePlan = makeLosslessMergePlan( + from: source, + into: target + ) { + applyLosslessMerge( + mergePlan, + from: source, + into: target + ) + context.delete(source) + } else { + quarantineChain( + source, + occupiedIdentifiers: &occupiedIdentifiers + ) + } + } + + target.chainId = canonicalIdentifier + } + } + + private func makeLosslessMergePlan( + from source: CDChain, + into target: CDChain + ) -> ChainMergePlan? { + do { + let targetOptions: [String]? = + try SafeTransformableValueReader.read( + from: target, + key: "options" + ) + let sourceOptions: [String]? = + try SafeTransformableValueReader.read( + from: source, + key: "options" + ) + + guard + scalarValuesCanMerge( + from: source, + into: target, + sourceOptions: sourceOptions, + targetOptions: targetOptions + ), + target.xcmConfig == nil || + source.xcmConfig == nil || + target.xcmConfig === source.xcmConfig, + let targetNodeGraph = makeNodeGraph(for: target), + let sourceNodeGraph = makeNodeGraph(for: source), + nodeGraphsCanMerge( + sourceNodeGraph, + into: targetNodeGraph + ), + assetsCanMerge(from: source, into: target) + else { + return nil + } + + return ChainMergePlan( + targetOptions: targetOptions, + sourceOptions: sourceOptions, + targetNodeGraph: targetNodeGraph, + sourceNodeGraph: sourceNodeGraph, + sourceAssets: managedObjects( + in: source.assets, + as: CDAsset.self + ), + sourceExplorers: managedObjects( + in: source.explorers, + as: CDExternalApi.self + ) + ) + } catch { + return nil + } + } + + private func scalarValuesCanMerge( + from source: CDChain, + into target: CDChain, + sourceOptions: [String]?, + targetOptions: [String]? + ) -> Bool { + guard + !valuesConflict(target.rank, source.rank), + target.disabled == source.disabled, + !valuesConflict(target.parentId, source.parentId), + !valuesConflict(target.name, source.name), + !valuesConflict(target.types, source.types), + !valuesConflict( + target.typesOverrideCommon?.boolValue, + source.typesOverrideCommon?.boolValue + ), + target.addressPrefix == source.addressPrefix, + !valuesConflict(target.icon, source.icon), + target.isEthereumBased == source.isEthereumBased, + target.isTestnet == source.isTestnet, + target.hasCrowdloans == source.hasCrowdloans, + target.isTipRequired == source.isTipRequired, + !valuesConflict( + target.minimalAppVersion, + source.minimalAppVersion + ), + !valuesConflict(targetOptions, sourceOptions), + !valuesConflict(target.stakingApiType, source.stakingApiType), + !valuesConflict(target.stakingApiUrl, source.stakingApiUrl), + !valuesConflict(target.historyApiType, source.historyApiType), + !valuesConflict(target.historyApiUrl, source.historyApiUrl), + !valuesConflict( + target.crowdloansApiType, + source.crowdloansApiType + ), + !valuesConflict( + target.crowdloansApiUrl, + source.crowdloansApiUrl + ), + !valuesConflict(target.pricingApiType, source.pricingApiType), + !valuesConflict(target.pricingApiUrl, source.pricingApiUrl), + !valuesConflict(target.identityChain, source.identityChain), + !valuesConflict(target.paraId, source.paraId), + target.isOrml == source.isOrml + else { + return false + } + + return true + } + + private func valuesConflict( + _ target: Value?, + _ source: Value? + ) -> Bool { + guard let target, let source else { + return false + } + + return target != source + } +} + +private extension ChainRepositoryFactory { + private func makeNodeGraph(for chain: CDChain) -> ChainNodeGraph? { + let defaultNodes = managedObjects( + in: chain.nodes, + as: CDChainNode.self + ) + let customNodes = managedObjects( + in: chain.customNodes, + as: CDChainNode.self + ) + let rawSelectedNode = chain.selectedNode + let selectedNode = rawSelectedNode.flatMap { + isValidNode($0) ? $0 : nil + } + let orderedValidNodes = ( + defaultNodes.filter(isValidNode) + + customNodes.filter(isValidNode) + + [selectedNode].compactMap { $0 } + ).sorted { + $0.objectID.uriRepresentation().absoluteString + < $1.objectID.uriRepresentation().absoluteString + } + + var nodesByURL: [URL: CDChainNode] = [:] + var fingerprintsByURL: [URL: ChainNodeFingerprint] = [:] + + for node in orderedValidNodes { + guard + let url = node.url, + let fingerprint = nodeFingerprint(for: node) + else { + continue + } + + if let existing = fingerprintsByURL[url], + existing != fingerprint { + return nil + } + + if nodesByURL[url] == nil { + nodesByURL[url] = node + fingerprintsByURL[url] = fingerprint + } + } + + return ChainNodeGraph( + allDefaultNodes: defaultNodes, + allCustomNodes: customNodes, + rawSelectedNode: rawSelectedNode, + nodesByURL: nodesByURL, + fingerprintsByURL: fingerprintsByURL + ) + } + + private func nodeGraphsCanMerge( + _ source: ChainNodeGraph, + into target: ChainNodeGraph + ) -> Bool { + for (url, sourceFingerprint) in source.fingerprintsByURL { + if let targetFingerprint = target.fingerprintsByURL[url], + targetFingerprint != sourceFingerprint { + return false + } + } + + if let targetSelection = target.rawSelectedNode, + let sourceSelection = source.rawSelectedNode { + guard + targetSelection === sourceSelection || + ( + nodeFingerprint(for: targetSelection) != nil && + nodeFingerprint(for: targetSelection) == + nodeFingerprint(for: sourceSelection) + ) + else { + return false + } + } + + return true + } + + private func nodeFingerprint( + for node: CDChainNode + ) -> ChainNodeFingerprint? { + guard + let url = node.url, + let name = node.name, + ChainModelMapper.isUsableNodeURL(url), + name.trimmingCharacters( + in: .whitespacesAndNewlines + ).isNotEmpty + else { + return nil + } + + return ChainNodeFingerprint( + url: url, + name: name, + apiQueryName: node.apiQueryName, + apiKeyName: node.apiKeyName + ) + } + + private func assetsCanMerge( + from source: CDChain, + into target: CDChain + ) -> Bool { + let targetIdentifiers = validAssetIdentifiers(in: target.assets) + let sourceIdentifiers = validAssetIdentifiers(in: source.assets) + + guard + targetIdentifiers.count == + Set(targetIdentifiers).count, + sourceIdentifiers.count == + Set(sourceIdentifiers).count + else { + return false + } + + return Set(targetIdentifiers) + .isDisjoint(with: sourceIdentifiers) + } + + private func validAssetIdentifiers(in relationship: NSSet?) -> [String] { + managedObjects(in: relationship, as: CDAsset.self).compactMap { + guard + let identifier = $0.id, + identifier.trimmingCharacters( + in: .whitespacesAndNewlines + ).isNotEmpty + else { + return nil + } + + return identifier + } + } +} + +private extension ChainRepositoryFactory { + private func applyLosslessMerge( + _ plan: ChainMergePlan, + from source: CDChain, + into target: CDChain + ) { + mergeScalarValues(plan, from: source, into: target) + + var preferredNodesByURL = plan.targetNodeGraph.nodesByURL + let resolveNode: (CDChainNode) -> CDChainNode = { node in + guard + let url = node.url, + plan.sourceNodeGraph.fingerprintsByURL[url] != nil + else { + return node + } + + if let existingNode = preferredNodesByURL[url] { + return existingNode + } + + preferredNodesByURL[url] = node + return node + } + + var targetDefaultNodes = Set( + managedObjects(in: target.nodes, as: CDChainNode.self) + ) + for node in plan.sourceNodeGraph.allDefaultNodes { + targetDefaultNodes.insert(resolveNode(node)) + } + + var targetCustomNodes = Set( + managedObjects( + in: target.customNodes, + as: CDChainNode.self + ) + ) + for node in plan.sourceNodeGraph.allCustomNodes { + targetCustomNodes.insert(resolveNode(node)) + } + + if plan.targetNodeGraph.rawSelectedNode == nil, + let sourceSelectedNode = + plan.sourceNodeGraph.rawSelectedNode { + target.selectedNode = resolveNode(sourceSelectedNode) + } + + target.nodes = NSSet(set: targetDefaultNodes) + target.customNodes = NSSet(set: targetCustomNodes) + + var targetAssets = Set( + managedObjects(in: target.assets, as: CDAsset.self) + ) + targetAssets.formUnion(plan.sourceAssets) + target.assets = NSSet(set: targetAssets) + + var targetExplorers = Set( + managedObjects( + in: target.explorers, + as: CDExternalApi.self + ) + ) + targetExplorers.formUnion(plan.sourceExplorers) + target.explorers = NSSet(set: targetExplorers) + + if target.xcmConfig == nil { + target.xcmConfig = source.xcmConfig + } + + source.nodes = NSSet() + source.customNodes = NSSet() + source.selectedNode = nil + source.assets = NSSet() + source.explorers = NSSet() + source.xcmConfig = nil + } + + private func mergeScalarValues( + _ plan: ChainMergePlan, + from source: CDChain, + into target: CDChain + ) { + if target.rank == nil { + target.rank = source.rank + } + if target.parentId == nil { + target.parentId = source.parentId + } + if target.name == nil { + target.name = source.name + } + if target.types == nil { + target.types = source.types + } + if target.typesOverrideCommon == nil { + target.typesOverrideCommon = source.typesOverrideCommon + } + if target.icon == nil { + target.icon = source.icon + } + if target.minimalAppVersion == nil { + target.minimalAppVersion = source.minimalAppVersion + } + if plan.targetOptions == nil, let sourceOptions = plan.sourceOptions { + target.setValue( + NSArray(array: sourceOptions), + forKey: "options" + ) + } + if target.stakingApiType == nil { + target.stakingApiType = source.stakingApiType + } + if target.stakingApiUrl == nil { + target.stakingApiUrl = source.stakingApiUrl + } + if target.historyApiType == nil { + target.historyApiType = source.historyApiType + } + if target.historyApiUrl == nil { + target.historyApiUrl = source.historyApiUrl + } + if target.crowdloansApiType == nil { + target.crowdloansApiType = source.crowdloansApiType + } + if target.crowdloansApiUrl == nil { + target.crowdloansApiUrl = source.crowdloansApiUrl + } + if target.pricingApiType == nil { + target.pricingApiType = source.pricingApiType + } + if target.pricingApiUrl == nil { + target.pricingApiUrl = source.pricingApiUrl + } + if target.identityChain == nil { + target.identityChain = source.identityChain + } + if target.paraId == nil { + target.paraId = source.paraId + } + } + + private func quarantineChain( + _ chain: CDChain, + occupiedIdentifiers: inout Set + ) { + let baseIdentifier = + ChainModelMapper.quarantinedChainIdentifierPrefix + + chain.objectID.uriRepresentation().absoluteString + var identifier = baseIdentifier + var collisionIndex = 0 + + while occupiedIdentifiers.contains(identifier) { + collisionIndex += 1 + identifier = "\(baseIdentifier)#\(collisionIndex)" + } + + chain.chainId = identifier + occupiedIdentifiers.insert(identifier) + chain.disabled = true + + if chain.name? + .trimmingCharacters(in: .whitespacesAndNewlines) + .isEmpty != false { + chain.name = ChainModelMapper.quarantinedChainName + } + } + + private func managedObjects( + in relationship: NSSet?, + as _: Object.Type + ) -> [Object] { + relationship?.compactMap { $0 as? Object } ?? [] + } + + private func isValidNode(_ node: CDChainNode) -> Bool { + guard + let url = node.url, + ChainModelMapper.isUsableNodeURL(url), + let name = node.name, + name.trimmingCharacters( + in: .whitespacesAndNewlines + ).isNotEmpty + else { + return false + } + + return true + } +} + +private struct ChainMergePlan { + let targetOptions: [String]? + let sourceOptions: [String]? + let targetNodeGraph: ChainNodeGraph + let sourceNodeGraph: ChainNodeGraph + let sourceAssets: [CDAsset] + let sourceExplorers: [CDExternalApi] +} + +private struct ChainNodeGraph { + let allDefaultNodes: [CDChainNode] + let allCustomNodes: [CDChainNode] + let rawSelectedNode: CDChainNode? + let nodesByURL: [URL: CDChainNode] + let fingerprintsByURL: [URL: ChainNodeFingerprint] +} + +private struct ChainNodeFingerprint: Equatable { + let url: URL + let name: String + let apiQueryName: String? + let apiKeyName: String? +} + +private final class ChainCleanupResultBox: @unchecked Sendable { + private let lock = NSLock() + private var result: Result? + + func set(_ result: Result) { + lock.lock() + self.result = result + lock.unlock() + } + + func get() -> Result { + lock.lock() + defer { lock.unlock() } + + return result ?? .failure(CoreDataRepositoryError.undefined) + } +} diff --git a/fearless/Common/Helpers/EthereumNodeFetching.swift b/fearless/Common/Helpers/EthereumNodeFetching.swift index 51749b2b89..ba08a2d797 100644 --- a/fearless/Common/Helpers/EthereumNodeFetching.swift +++ b/fearless/Common/Helpers/EthereumNodeFetching.swift @@ -95,14 +95,21 @@ enum EthereumChain: String { } } -final class EthereumNodeFetching { +protocol EthereumNodeFetchingProtocol { + func getNode(for chain: ChainModel) throws -> Web3.Eth +} + +final class EthereumNodeFetching: EthereumNodeFetchingProtocol { func getNode(for chain: ChainModel) throws -> Web3.Eth { if let https = try? getHttps(for: chain) { return https } - let randomWssNode = chain.nodes.filter { $0.url.absoluteString.contains("wss") }.randomElement() - let hasSelectedWssNode = chain.selectedNode?.url.absoluteString.contains("wss") == true + let randomWssNode = chain.nodes.filter { + $0.url.scheme?.lowercased() == "wss" + }.randomElement() + let hasSelectedWssNode = + chain.selectedNode?.url.scheme?.lowercased() == "wss" let node = hasSelectedWssNode ? chain.selectedNode : randomWssNode guard var wssURL = node?.url else { @@ -119,9 +126,14 @@ final class EthereumNodeFetching { } func getHttps(for chain: ChainModel) throws -> Web3.Eth { - let randomWssNode = chain.nodes.filter { $0.url.absoluteString.contains("https") }.randomElement() - let hasSelectedWssNode = chain.selectedNode?.url.absoluteString.contains("https") == true - let node = hasSelectedWssNode ? chain.selectedNode : randomWssNode + let randomHttpsNode = chain.nodes.filter { + $0.url.scheme?.lowercased() == "https" + }.randomElement() + let hasSelectedHttpsNode = + chain.selectedNode?.url.scheme?.lowercased() == "https" + let node = hasSelectedHttpsNode + ? chain.selectedNode + : randomHttpsNode guard let httpsURL = node?.url else { throw ConvenienceError(error: "cannot obtain eth https url for chain: \(chain.name)") diff --git a/fearless/Common/Model/ChainRegistry/ChainModel.swift b/fearless/Common/Model/ChainRegistry/ChainModel.swift index 57c1bd0cdc..a83dfd0e5d 100644 --- a/fearless/Common/Model/ChainRegistry/ChainModel.swift +++ b/fearless/Common/Model/ChainRegistry/ChainModel.swift @@ -3,6 +3,79 @@ import SSFModels import RobinHood extension ChainModel { + func replacingNodeConfiguration( + nodes: Set, + selectedNode: ChainNodeModel?, + customNodes: Set? + ) -> ChainModel { + ChainModel( + rank: rank, + disabled: disabled, + chainId: chainId, + parentId: parentId, + paraId: paraId, + name: name, + assets: assets, + xcm: xcm, + nodes: nodes, + addressPrefix: addressPrefix, + types: types, + icon: icon, + options: options, + externalApi: externalApi, + selectedNode: selectedNode, + customNodes: customNodes, + iosMinAppVersion: iosMinAppVersion, + identityChain: identityChain + ) + } + + func replacingNodes(_ nodes: Set) -> ChainModel { + ChainModel( + rank: rank, + disabled: disabled, + chainId: chainId, + parentId: parentId, + paraId: paraId, + name: name, + assets: assets, + xcm: xcm, + nodes: nodes, + addressPrefix: addressPrefix, + types: types, + icon: icon, + options: options, + externalApi: externalApi, + selectedNode: selectedNode, + customNodes: customNodes, + iosMinAppVersion: iosMinAppVersion, + identityChain: identityChain + ) + } + + func replacingDisabled(_ disabled: Bool) -> ChainModel { + ChainModel( + rank: rank, + disabled: disabled, + chainId: chainId, + parentId: parentId, + paraId: paraId, + name: name, + assets: assets, + xcm: xcm, + nodes: nodes, + addressPrefix: addressPrefix, + types: types, + icon: icon, + options: options, + externalApi: externalApi, + selectedNode: selectedNode, + customNodes: customNodes, + iosMinAppVersion: iosMinAppVersion, + identityChain: identityChain + ) + } + var isSupported: Bool { AppVersion.stringValue?.versionLowerThan(iosMinAppVersion) == false } diff --git a/fearless/Common/Model/RuntimeMetadataItem.swift b/fearless/Common/Model/RuntimeMetadataItem.swift index 2e48b2c5ff..cab8dff367 100644 --- a/fearless/Common/Model/RuntimeMetadataItem.swift +++ b/fearless/Common/Model/RuntimeMetadataItem.swift @@ -1,3 +1,4 @@ +import CoreData import Foundation import RobinHood import SSFUtils @@ -52,3 +53,58 @@ extension RuntimeMetadataItem: Identifiable { } extension RuntimeMetadataItem: RuntimeMetadataItemProtocol {} + +/// Keeps the generic Codable mapper's valid serialization behavior while +/// converting damaged Core Data getter exceptions into a catchable, +/// payload-free Swift error during runtime hot boot. +final class RuntimeMetadataMapper: CoreDataMapperProtocol { + typealias DataProviderModel = RuntimeMetadataItem + typealias CoreDataEntity = CDRuntimeMetadataItem + + private let mapper = + CodableCoreDataMapper< + RuntimeMetadataItem, + CDRuntimeMetadataItem + >() + + var entityIdentifierFieldName: String { + mapper.entityIdentifierFieldName + } + + func transform( + entity: CDRuntimeMetadataItem + ) throws -> RuntimeMetadataItem { + var item: RuntimeMetadataItem? + + try SafeObjectiveCExceptionBoundary.perform { + item = try self.mapper.transform(entity: entity) + } + + guard let item else { + throw SafeTransformableValueReaderError + .objectiveCException + } + + return item + } + + func populate( + entity: CDRuntimeMetadataItem, + from model: RuntimeMetadataItem, + using context: NSManagedObjectContext + ) throws { + try SafeObjectiveCExceptionBoundary.perform { + try self.mapper.populate( + entity: entity, + from: model, + using: context + ) + } + } + + func dict( + for model: RuntimeMetadataItem + ) throws -> [String: Any] { + try mapper.dict(for: model) + } +} diff --git a/fearless/Common/Model/UniversalWalletAccountAddressResolver.swift b/fearless/Common/Model/UniversalWalletAccountAddressResolver.swift index 897aa09f9a..20675225fd 100644 --- a/fearless/Common/Model/UniversalWalletAccountAddressResolver.swift +++ b/fearless/Common/Model/UniversalWalletAccountAddressResolver.swift @@ -25,6 +25,40 @@ enum UniversalWalletChainAccountSupport { equivalentChainIds(for: chainId) != nil } + /// Returns the stable storage identity for a universal-wallet chain alias. + /// + /// Non-universal chain identifiers are intentionally returned unchanged: + /// their alias semantics belong to the remote registry and must not be + /// guessed during migration or stored-wallet validation. + static func canonicalChainId(for chainId: String) -> String { + switch chainId.lowercased() { + case UniversalWalletRegistry.bitcoinMainnet.chainId, + UniversalWalletRegistry.bitcoinMainnet.id: + return UniversalWalletRegistry.bitcoinMainnet.chainId + case UniversalWalletRegistry.bitcoinTestnet.chainId, + UniversalWalletRegistry.bitcoinTestnet.id: + return UniversalWalletRegistry.bitcoinTestnet.chainId + case UniversalWalletRegistry.solanaMainnet.chainId, + UniversalWalletRegistry.solanaMainnet.id: + return UniversalWalletRegistry.solanaMainnet.chainId + case UniversalWalletRegistry.solanaDevnet.chainId, + UniversalWalletRegistry.solanaDevnet.id: + return UniversalWalletRegistry.solanaDevnet.chainId + case TonChainSelection.mainnetChainId, + UniversalWalletRegistry.tonMainnetRegistryEntry.chainId, + UniversalWalletRegistry.tonMainnetRegistryEntry.id: + return UniversalWalletRegistry.tonMainnetRegistryEntry.chainId + case UniversalWalletRegistry.taira.chainId, + UniversalWalletRegistry.taira.id: + return UniversalWalletRegistry.taira.chainId + case UniversalWalletRegistry.nexus.chainId, + UniversalWalletRegistry.nexus.id: + return UniversalWalletRegistry.nexus.chainId + default: + return chainId + } + } + static func chainId(_ storedChainId: String, matches requestedChainId: String) -> Bool { guard let equivalentIds = equivalentChainIds(for: requestedChainId) else { return storedChainId == requestedChainId diff --git a/fearless/Common/Services/ChainRegistry/ChainRegistry.swift b/fearless/Common/Services/ChainRegistry/ChainRegistry.swift index bedd59f67c..5fd143d037 100644 --- a/fearless/Common/Services/ChainRegistry/ChainRegistry.swift +++ b/fearless/Common/Services/ChainRegistry/ChainRegistry.swift @@ -16,15 +16,212 @@ import HTTPTypes enum TonAPITransportError: Error { case invalidRequestURL(path: String, method: HTTPRequest.Method, baseURL: URL) case notHTTPResponse(URLResponse) + case missingResponse + case responseTooLarge(actualBytes: Int, maximumBytes: Int) } -private final class TonAPIURLSessionTransport: ClientTransport, @unchecked Sendable { - private let session: URLSession +enum TonAPITransportPolicy { + static let requestTimeout: TimeInterval = 15 + static let resourceTimeout: TimeInterval = 30 + static let maximumResponseBytes = 2 * 1024 * 1024 + static let maximumRequestBodyBytes = 1 * 1024 * 1024 + static let followsRedirects = false +} + +/// Authentication-bearing API calls must never follow a redirect because Foundation otherwise +/// decides header forwarding after the server has already selected the next origin. +final class TonAPINoRedirectDelegate: NSObject, URLSessionTaskDelegate, @unchecked Sendable { + static func redirectedRequest(_: URLRequest) -> URLRequest? { + nil + } + + func urlSession( + _: URLSession, + task _: URLSessionTask, + willPerformHTTPRedirection _: HTTPURLResponse, + newRequest request: URLRequest, + completionHandler: @escaping (URLRequest?) -> Void + ) { + completionHandler(Self.redirectedRequest(request)) + } +} + +final class TonAPIBoundedSessionDelegate: NSObject, URLSessionDataDelegate, @unchecked Sendable { + private final class RequestCancellationState: @unchecked Sendable { + let lock = NSLock() + var isCancelled = false + } + + private struct RequestState { + var data = Data() + var response: URLResponse? + let continuation: CheckedContinuation<(Data, URLResponse), Error> + } + + private let maximumResponseBytes: Int + private let lock = NSLock() + private var states: [Int: RequestState] = [:] + + init(maximumResponseBytes: Int) { + self.maximumResponseBytes = maximumResponseBytes + } + + var inFlightRequestCount: Int { + lock.lock() + defer { lock.unlock() } + return states.count + } + + func data(for request: URLRequest, session: URLSession) async throws -> (Data, URLResponse) { + let task = session.dataTask(with: request) + let cancellationState = RequestCancellationState() + + return try await withTaskCancellationHandler { + try await withCheckedThrowingContinuation { continuation in + cancellationState.lock.lock() + let wasCancelled = cancellationState.isCancelled + if !wasCancelled { + lock.lock() + states[task.taskIdentifier] = RequestState(continuation: continuation) + lock.unlock() + } + cancellationState.lock.unlock() + + if wasCancelled { + task.cancel() + continuation.resume(throwing: CancellationError()) + } else { + task.resume() + } + } + } onCancel: { + self.cancel(task: task, cancellationState: cancellationState) + } + } + + func urlSession( + _: URLSession, + task _: URLSessionTask, + willPerformHTTPRedirection _: HTTPURLResponse, + newRequest _: URLRequest, + completionHandler: @escaping (URLRequest?) -> Void + ) { + completionHandler(nil) + } + + func urlSession( + _: URLSession, + dataTask: URLSessionDataTask, + didReceive response: URLResponse, + completionHandler: @escaping (URLSession.ResponseDisposition) -> Void + ) { + if response.expectedContentLength > Int64(maximumResponseBytes) { + resolveOversized(task: dataTask, actualBytes: Int(response.expectedContentLength)) + completionHandler(.cancel) + return + } + + lock.lock() + if var state = states[dataTask.taskIdentifier] { + state.response = response + states[dataTask.taskIdentifier] = state + } + lock.unlock() + completionHandler(.allow) + } + + func urlSession(_: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { + var oversizedActual: Int? + lock.lock() + if var state = states[dataTask.taskIdentifier] { + let (nextCount, overflowed) = state.data.count.addingReportingOverflow(data.count) + if overflowed || nextCount > maximumResponseBytes { + oversizedActual = overflowed ? Int.max : nextCount + } else { + state.data.append(data) + states[dataTask.taskIdentifier] = state + } + } + lock.unlock() + + if let oversizedActual { + resolveOversized(task: dataTask, actualBytes: oversizedActual) + dataTask.cancel() + } + } + + func urlSession( + _: URLSession, + task: URLSessionTask, + didCompleteWithError error: Error? + ) { + lock.lock() + let state = states.removeValue(forKey: task.taskIdentifier) + lock.unlock() + guard let state else { + return + } + + if let error { + state.continuation.resume(throwing: error) + } else if let response = state.response { + state.continuation.resume(returning: (state.data, response)) + } else { + state.continuation.resume(throwing: TonAPITransportError.missingResponse) + } + } + + private func resolveOversized(task: URLSessionTask, actualBytes: Int) { + lock.lock() + let state = states.removeValue(forKey: task.taskIdentifier) + lock.unlock() + state?.continuation.resume( + throwing: TonAPITransportError.responseTooLarge( + actualBytes: actualBytes, + maximumBytes: maximumResponseBytes + ) + ) + } + + private func cancel( + task: URLSessionTask, + cancellationState: RequestCancellationState + ) { + cancellationState.lock.lock() + cancellationState.isCancelled = true + lock.lock() + let state = states.removeValue(forKey: task.taskIdentifier) + lock.unlock() + cancellationState.lock.unlock() + + task.cancel() + state?.continuation.resume(throwing: CancellationError()) + } +} - init(configuration: URLSessionConfiguration = .default) { - session = URLSession(configuration: configuration) +final class TonAPIURLSessionTransport: ClientTransport, @unchecked Sendable { + private let session: URLSession + private let delegate: TonAPIBoundedSessionDelegate + + init(configuration: URLSessionConfiguration? = nil) { + let configuration = configuration ?? .ephemeral + configuration.timeoutIntervalForRequest = TonAPITransportPolicy.requestTimeout + configuration.timeoutIntervalForResource = TonAPITransportPolicy.resourceTimeout + configuration.requestCachePolicy = .reloadIgnoringLocalAndRemoteCacheData + configuration.urlCache = nil + configuration.httpCookieStorage = nil + delegate = TonAPIBoundedSessionDelegate( + maximumResponseBytes: TonAPITransportPolicy.maximumResponseBytes + ) + session = URLSession( + configuration: configuration, + delegate: delegate, + delegateQueue: nil + ) } + var inFlightRequestCount: Int { delegate.inFlightRequestCount } + func send( _ request: HTTPRequest, body: HTTPBody?, @@ -34,7 +231,7 @@ private final class TonAPIURLSessionTransport: ClientTransport, @unchecked Senda var urlRequest = try await URLRequest(request, body: body, baseURL: baseURL) urlRequest.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData - let (data, response) = try await session.data(for: urlRequest) + let (data, response) = try await delegate.data(for: urlRequest, session: session) let httpResponse = try HTTPResponse.from(urlResponse: response) let responseBody: HTTPBody? = data.isEmpty ? nil : HTTPBody(data) return (httpResponse, responseBody) @@ -51,7 +248,9 @@ private struct TonAPIAuthorizationMiddleware: ClientMiddleware { operationID _: String, next: @Sendable(HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody?) ) async throws -> (HTTPResponse, HTTPBody?) { - guard !token.isEmpty else { + guard TonAPIClientFactory.isValidAuthorizationToken(token), + TonAPIClientFactory.canAttachAuthorization(to: baseURL) + else { return try await next(request, body, baseURL) } @@ -62,16 +261,73 @@ private struct TonAPIAuthorizationMiddleware: ClientMiddleware { } final class TonAPIClientFactory { + static let canonicalAuthenticatedOrigin = URL(string: "https://tonapi.io")! + /// Binary-owned allowlist for operations that expose a signed bearer BOC. + /// Read-only balance clients may still use other valid registry nodes, but native + /// submission must remain pinned to an independently reviewed exact origin. + static let reviewedProductionSendOrigins = [canonicalAuthenticatedOrigin] + private let tonAPIURL: URL private let token: String + var serverURL: URL { tonAPIURL } + var usesAuthorization: Bool { Self.isValidAuthorizationToken(token) } + var hasReviewedProductionSendCredential: Bool { + usesAuthorization && Self.isReviewedProductionSendServerURL(tonAPIURL) + } + init(tonAPIURL: URL, token: String) { self.tonAPIURL = tonAPIURL - self.token = token + self.token = Self.canAttachAuthorization(to: tonAPIURL) && Self.isValidAuthorizationToken(token) ? token : "" + } + + static func isValidServerURL(_ url: URL) -> Bool { + guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false), + components.scheme?.lowercased() == "https", + components.host?.isEmpty == false, + components.user == nil, + components.password == nil, + components.query == nil, + components.fragment == nil, + components.percentEncodedPath.isEmpty || components.percentEncodedPath == "/" + else { + return false + } + return true + } + + static func canAttachAuthorization(to url: URL) -> Bool { + hasExactOrigin(url, canonical: canonicalAuthenticatedOrigin) + } + + static func isValidAuthorizationToken(_ token: String) -> Bool { + let bytes = Array(token.utf8) + return !bytes.isEmpty && bytes.count <= 4096 && bytes.allSatisfy { byte in + (0x21 ... 0x7E).contains(byte) + } } - func tonAPIClient() -> Client { - let transport = TonAPIURLSessionTransport() + static func isReviewedProductionSendServerURL(_ url: URL) -> Bool { + reviewedProductionSendOrigins.contains { canonical in + hasExactOrigin(url, canonical: canonical) + } + } + + private static func hasExactOrigin(_ url: URL, canonical: URL) -> Bool { + guard isValidServerURL(url), + let components = URLComponents(url: url, resolvingAgainstBaseURL: false), + let canonical = URLComponents(url: canonical, resolvingAgainstBaseURL: false) + else { + return false + } + + return components.scheme?.lowercased() == canonical.scheme?.lowercased() && + components.host?.lowercased() == canonical.host?.lowercased() && + components.port == canonical.port + } + + func tonAPIClient(configuration: URLSessionConfiguration? = nil) -> Client { + let transport = TonAPIURLSessionTransport(configuration: configuration) let middlewares: [any ClientMiddleware] = token.isEmpty ? [] : [TonAPIAuthorizationMiddleware(token: token)] return Client( @@ -88,9 +344,24 @@ private extension URLRequest { body: HTTPBody?, baseURL: URL ) async throws { + let requestPath = request.path ?? "" guard + TonAPIClientFactory.isValidServerURL(baseURL), var baseURLComponents = URLComponents(string: baseURL.absoluteString), - let requestURLComponents = URLComponents(string: request.path ?? "") + let requestURLComponents = URLComponents(string: requestPath), + requestURLComponents.scheme == nil, + requestURLComponents.host == nil, + requestURLComponents.user == nil, + requestURLComponents.password == nil, + requestURLComponents.fragment == nil, + requestURLComponents.percentEncodedPath.hasPrefix("/"), + !requestURLComponents.percentEncodedPath.hasPrefix("//"), + let decodedPath = requestURLComponents.percentEncodedPath.removingPercentEncoding, + decodedPath.hasPrefix("/"), + !decodedPath.hasPrefix("//"), + !decodedPath.contains("\\"), + !decodedPath.split(separator: "/", omittingEmptySubsequences: false).contains("."), + !decodedPath.split(separator: "/", omittingEmptySubsequences: false).contains("..") else { throw TonAPITransportError.invalidRequestURL( path: request.path ?? "", @@ -99,7 +370,10 @@ private extension URLRequest { ) } - baseURLComponents.percentEncodedPath += requestURLComponents.percentEncodedPath + // Valid server URLs may use either an empty path or a single trailing slash. + // The generated operation path is already absolute, so replacement avoids a + // double slash such as https://tonapi.io//v2/... without permitting base paths. + baseURLComponents.percentEncodedPath = requestURLComponents.percentEncodedPath baseURLComponents.percentEncodedQuery = requestURLComponents.percentEncodedQuery guard let resolvedURL = baseURLComponents.url else { @@ -118,7 +392,10 @@ private extension URLRequest { } if let body { - httpBody = try await Data(collecting: body, upTo: .max) + httpBody = try await Data( + collecting: body, + upTo: TonAPITransportPolicy.maximumRequestBodyBytes + ) } } } @@ -248,12 +525,22 @@ final class ChainRegistry { do { switch change { case let .insert(newChain): - try self.handleInsert(newChain) + if !newChain.disabled { + try self.handleInsert(newChain) + } case let .update(updatedChain): let currentChain = self.chains.first { $0.chainId == updatedChain.chainId } - if let chain = currentChain, - chain.nodes != updatedChain.nodes || chain.selectedNode != updatedChain.selectedNode { - try self.handleUpdate(updatedChain) + if updatedChain.disabled { + if currentChain != nil { + self.handleDelete(updatedChain.chainId) + } + } else if let chain = currentChain { + if chain.nodes != updatedChain.nodes || + chain.selectedNode != updatedChain.selectedNode { + try self.handleUpdate(updatedChain) + } + } else { + try self.handleInsert(updatedChain) } case let .delete(chainId): self.handleDelete(chainId) @@ -349,14 +636,18 @@ final class ChainRegistry { } clearRuntimeSubscription(for: updatedChain.chainId) + runtimeProviderPool.destroyRuntimeProvider(for: updatedChain.chainId) + runtimeSyncService.unregister(chainId: updatedChain.chainId) + substrateConnectionPool.resetConnection(for: updatedChain.chainId) + chains = chains.filter { $0.chainId != updatedChain.chainId } let connection = try substrateConnectionPool.setupConnection(for: updatedChain) let chainTypes = chainsTypesMap[updatedChain.chainId] runtimeProviderPool.setupRuntimeProvider(for: updatedChain, chainTypes: chainTypes) + runtimeSyncService.register(chain: updatedChain, with: connection) setupRuntimeVersionSubscription(for: updatedChain, connection: connection) - chains = chains.filter { $0.chainId != updatedChain.chainId } chains.append(updatedChain) } @@ -364,6 +655,7 @@ final class ChainRegistry { runtimeProviderPool.destroyRuntimeProvider(for: chainId) clearRuntimeSubscription(for: chainId) runtimeSyncService.unregister(chainId: chainId) + resetSubstrateConnection(for: chainId) chains = chains.filter { $0.chainId != chainId } } @@ -395,13 +687,15 @@ final class ChainRegistry { return } + ethereumConnectionPool.resetConnection(for: updatedChain.chainId) + chains = chains.filter { $0.chainId != updatedChain.chainId } + do { _ = try ethereumConnectionPool.setupConnection(for: updatedChain) } catch { logger?.customError(error) } - chains = chains.filter { $0.chainId != updatedChain.chainId } chains.append(updatedChain) } @@ -422,8 +716,7 @@ final class ChainRegistry { chains = chains.filter { $0.chainId != chain.chainId } chains.append(chain) - let token = currentTonApiKey - guard let node = Self.resolveTonNode(for: chain) else { + guard let baseURL = try? Self.tonAPIBaseURL(for: chain) else { logger?.error("Missing TON node URL") if tonApiChainId == chain.chainId { tonApiClientFactory = nil @@ -440,7 +733,8 @@ final class ChainRegistry { return } - tonApiClientFactory = TonAPIClientFactory(tonAPIURL: node.url, token: token) + let token = TonAPIClientFactory.canAttachAuthorization(to: baseURL) ? currentTonApiKey : "" + tonApiClientFactory = TonAPIClientFactory(tonAPIURL: baseURL, token: token) tonApiChainId = chain.chainId } @@ -569,12 +863,13 @@ extension ChainRegistry: ChainRegistryProtocol { return cachedTonApiClientFactory } - guard let node = Self.resolveTonNode(for: selectedTonChain) else { + guard let baseURL = try? Self.tonAPIBaseURL(for: selectedTonChain) else { logger?.error("Missing TON node URL") return nil } - return TonAPIClientFactory(tonAPIURL: node.url, token: currentTonApiKey) + let token = TonAPIClientFactory.canAttachAuthorization(to: baseURL) ? currentTonApiKey : "" + return TonAPIClientFactory(tonAPIURL: baseURL, token: token) } guard let resolvedTonApiClientFactory else { @@ -584,6 +879,26 @@ extension ChainRegistry: ChainRegistryProtocol { return resolvedTonApiClientFactory } + /// Creates a client for the exact chain being sent from. This deliberately does not use + /// the environment-toggle/global TON selection used by balance subscriptions. + func getTonApiClientFactory(for chain: ChainModel) throws -> TonAPIClientFactory { + let baseURL = try Self.tonAPIBaseURL(for: chain) + guard TonAPIClientFactory.isReviewedProductionSendServerURL(baseURL) else { + throw ChainRegistryError.connectionUnavailable + } + let token = TonAPIClientFactory.canAttachAuthorization(to: baseURL) ? currentTonApiKey : "" + return TonAPIClientFactory(tonAPIURL: baseURL, token: token) + } + + static func tonAPIBaseURL(for chain: ChainModel) throws -> URL { + guard let node = resolveTonNode(for: chain), + TonAPIClientFactory.isValidServerURL(node.url) + else { + throw ChainRegistryError.connectionUnavailable + } + return node.url + } + func chainsSubscribe( _ target: AnyObject, runningInQueue: DispatchQueue, diff --git a/fearless/Common/Services/ChainRegistry/ChainRegistryFactory.swift b/fearless/Common/Services/ChainRegistry/ChainRegistryFactory.swift index ec2af91c86..0832749602 100644 --- a/fearless/Common/Services/ChainRegistry/ChainRegistryFactory.swift +++ b/fearless/Common/Services/ChainRegistry/ChainRegistryFactory.swift @@ -80,7 +80,11 @@ enum ChainRegistryFactory { dependencies: ChainRegistryFactoryDependencies = .live ) -> ChainRegistryProtocol & SSFChainRegistry.ChainRegistryProtocol { let runtimeMetadataRepository: CoreDataRepository = - repositoryFacade.createRepository() + repositoryFacade.createRepository( + mapper: AnyCoreDataMapper( + RuntimeMetadataMapper() + ) + ) let dataFetchOperationFactory = dependencies.dataOperationFactory @@ -119,7 +123,10 @@ enum ChainRegistryFactory { eventCenter: dependencies.eventCenter, operationQueue: dependencies.syncQueue, logger: dependencies.logger, - applicationHandler: dependencies.applicationHandler + applicationHandler: dependencies.applicationHandler, + malformedChainCleanupOperationFactory: { + chainRepositoryFactory.createMalformedChainCleanupOperation() + } ) let specVersionSubscriptionFactory = SpecVersionSubscriptionFactory( diff --git a/fearless/Common/Services/ChainRegistry/ChainSyncService.swift b/fearless/Common/Services/ChainRegistry/ChainSyncService.swift index 34dd92fa18..434425f2ad 100644 --- a/fearless/Common/Services/ChainRegistry/ChainSyncService.swift +++ b/fearless/Common/Services/ChainRegistry/ChainSyncService.swift @@ -12,6 +12,9 @@ protocol ChainSyncServiceProtocol { enum ChainSyncServiceError: Error { case missingLocalFile + case emptyRemotePayload + case invalidRemoteChain(String) + case duplicateRemoteChainIdentifier(String) } final class ChainSyncService { @@ -34,11 +37,16 @@ final class ChainSyncService { private let applicationHandler: ApplicationHandlerProtocol private let operationQueue: OperationQueue private let logger: LoggerProtocol? + private let malformedChainCleanupOperationFactory: (() -> BaseOperation)? private var retryAttempt: Int = 0 - private var isSyncing: Bool = false + private var isSyncInFlight = false + private var isCooldownActive = false + private var reservationIdentifier: UInt = 0 + private var timerReservationIdentifier: UInt? private let mutex = NSLock() - private var timer = CountdownTimer(notificationInterval: 300) + private let timer: CountdownTimerProtocol + private let cooldownTimerQueue: DispatchQueue private lazy var scheduler = Scheduler(with: self, callbackQueue: DispatchQueue.global()) @@ -50,7 +58,12 @@ final class ChainSyncService { operationQueue: OperationQueue, retryStrategy: ReconnectionStrategyProtocol = ExponentialReconnection(), logger: LoggerProtocol? = nil, - applicationHandler: ApplicationHandlerProtocol + applicationHandler: ApplicationHandlerProtocol, + malformedChainCleanupOperationFactory: (() -> BaseOperation)? = nil, + cooldownTimer: CountdownTimerProtocol = CountdownTimer( + notificationInterval: 300 + ), + cooldownTimerQueue: DispatchQueue = .main ) { self.chainsUrl = chainsUrl self.dataFetchFactory = dataFetchFactory @@ -60,21 +73,75 @@ final class ChainSyncService { self.retryStrategy = retryStrategy self.logger = logger self.applicationHandler = applicationHandler + self.malformedChainCleanupOperationFactory = malformedChainCleanupOperationFactory + timer = cooldownTimer + self.cooldownTimerQueue = cooldownTimerQueue timer.delegate = self } - private func performSyncUpIfNeeded() { - guard !isSyncing else { + private func performSyncUpIfNeeded( + cancelScheduledRetry: Bool = false, + installApplicationDelegate: Bool = false + ) { + mutex.lock() + + if cancelScheduledRetry, retryAttempt > 0 { + scheduler.cancel() + } + + if installApplicationDelegate, + applicationHandler.delegate == nil { + applicationHandler.delegate = self + } + + guard !isSyncInFlight, !isCooldownActive else { + mutex.unlock() logger?.debug("Tried to sync up chains but already syncing") return } - DispatchQueue.main.async { - self.timer.start(with: 300) - } + isSyncInFlight = true + isCooldownActive = true + reservationIdentifier &+= 1 + let currentReservationIdentifier = reservationIdentifier retryAttempt += 1 + let currentRetryAttempt = retryAttempt + mutex.unlock() + + cooldownTimerQueue.async { + self.mutex.lock() + let shouldStartTimer = + self.isCooldownActive && + self.reservationIdentifier == currentReservationIdentifier + self.mutex.unlock() + + guard shouldStartTimer else { + return + } - logger?.debug("Will start chain sync with attempt \(retryAttempt)") + // CountdownTimer.start() synchronously stops its previous run. + // Keep every timer mutation on this serial queue, and stop the old + // run before publishing the new reservation. + self.timer.stop() + + self.mutex.lock() + let timerIsCurrent = + self.isCooldownActive && + self.reservationIdentifier == currentReservationIdentifier + if timerIsCurrent { + self.timerReservationIdentifier = + currentReservationIdentifier + } + self.mutex.unlock() + + if timerIsCurrent { + self.timer.start(with: 300) + } + } + + logger?.debug( + "Will start chain sync with attempt \(currentRetryAttempt)" + ) let event = ChainSyncDidStart() eventCenter.notify(with: event) @@ -82,14 +149,29 @@ final class ChainSyncService { executeSync() } - private func setApplicationDelegateIfNeeded() { - guard applicationHandler.delegate == nil else { + private func executeSync() { + guard let malformedChainCleanupOperationFactory else { + executeRemoteSync() return } - applicationHandler.delegate = self + + let cleanupOperation = malformedChainCleanupOperationFactory() + cleanupOperation.completionBlock = { [weak self, weak cleanupOperation] in + do { + guard let cleanupOperation else { + throw BaseOperationError.parentOperationCancelled + } + + _ = try cleanupOperation.extractNoCancellableResultData() + self?.executeRemoteSync() + } catch { + self?.complete(result: .failure(error)) + } + } + operationQueue.addOperation(cleanupOperation) } - private func executeSync() { + private func executeRemoteSync() { if Self.fetchLocalData { do { let localData = try fetchLocalData() @@ -220,7 +302,21 @@ final class ChainSyncService { } private func handle(remoteChains: [ChainModel]) { - let normalizedRemoteChains = remoteChains.map { normalizeSoraNexusChainAssets($0) } + let normalizedRemoteChains: [ChainModel] + do { + normalizedRemoteChains = try Self.sanitizingRemoteChains( + remoteChains.map { normalizeSoraNexusChainAssets($0) } + ) + } catch { + complete(result: .failure(error)) + return + } + + guard normalizedRemoteChains.isNotEmpty else { + complete(result: .failure(ChainSyncServiceError.emptyRemotePayload)) + return + } + let localFetchOperation = repository.fetchAllOperation(with: RepositoryFetchOptions()) let processingOperation: BaseOperation<( @@ -262,6 +358,84 @@ final class ChainSyncService { ) } + static func sanitizingRemoteChains( + _ remoteChains: [ChainModel] + ) throws -> [ChainModel] { + var seenIdentifiers = Set() + + return try remoteChains.map { chain in + let canonicalIdentifier = chain.chainId.trimmingCharacters( + in: .whitespacesAndNewlines + ) + guard + canonicalIdentifier.isNotEmpty, + canonicalIdentifier == chain.chainId, + !canonicalIdentifier.hasPrefix( + ChainModelMapper.quarantinedChainIdentifierPrefix + ), + chain.name.trimmingCharacters( + in: .whitespacesAndNewlines + ).isNotEmpty + else { + throw ChainSyncServiceError.invalidRemoteChain( + chain.chainId + ) + } + + guard seenIdentifiers.insert(chain.chainId).inserted else { + throw ChainSyncServiceError + .duplicateRemoteChainIdentifier(chain.chainId) + } + + let usableNodesByURL = chain.nodes + .filter { node in + ChainModelMapper.isUsableNodeURL(node.url) + && node.name.trimmingCharacters( + in: .whitespacesAndNewlines + ).isNotEmpty + && ChainModelMapper.isNodeCompatibleWithRuntime( + node, + for: chain + ) + } + .sorted { lhs, rhs in + if lhs.url.absoluteString != rhs.url.absoluteString { + return lhs.url.absoluteString + < rhs.url.absoluteString + } + + if lhs.name != rhs.name { + return lhs.name < rhs.name + } + + let lhsQueryName = lhs.apikey?.queryName ?? "" + let rhsQueryName = rhs.apikey?.queryName ?? "" + if lhsQueryName != rhsQueryName { + return lhsQueryName < rhsQueryName + } + + return (lhs.apikey?.keyName ?? "") + < (rhs.apikey?.keyName ?? "") + } + .reduce(into: [URL: ChainNodeModel]()) { result, node in + if result[node.url] == nil { + result[node.url] = node + } + } + + guard chain.disabled || usableNodesByURL.isNotEmpty else { + throw ChainSyncServiceError.invalidRemoteChain( + chain.chainId + ) + } + + return chain + .replacingNodes(Set(usableNodesByURL.values)) + .replacingCustomNodes([]) + .replacingSelectedNode(nil) + } + } + private func normalizeSoraNexusChainAssets(_ chain: ChainModel) -> ChainModel { guard isSoraNexus(chain) else { return chain @@ -275,7 +449,7 @@ final class ChainSyncService { return chain } - var updatedChain = chain + let updatedChain = chain let xorAsset = AssetModel( id: "b5a44630-920e-43ee-809f-61890d0888b0", name: "sora", @@ -311,11 +485,12 @@ final class ChainSyncService { remoteChains: [ChainModel], localChains: [ChainModel] ) { - remoteChains.forEach { chain in - chain.selectedNode = localChains.first(where: { $0.chainId == chain.chainId })?.selectedNode - } + let mergedRemoteChains = Self.preservingLocalNodePreferences( + remoteChains: remoteChains, + localChains: localChains + ) - let remoteMapping = remoteChains.reduce(into: [ChainModel.Id: ChainModel]()) { mapping, item in + let remoteMapping = mergedRemoteChains.reduce(into: [ChainModel.Id: ChainModel]()) { mapping, item in mapping[item.chainId] = item } @@ -323,7 +498,7 @@ final class ChainSyncService { mapping[item.chainId] = item } - let newOrUpdated: [ChainModel] = remoteChains.compactMap { remoteItem in + var newOrUpdated: [ChainModel] = mergedRemoteChains.compactMap { remoteItem in if let localItem = localMapping[remoteItem.chainId] { return localItem != remoteItem ? remoteItem : nil } else { @@ -331,15 +506,78 @@ final class ChainSyncService { } } - let removed = localChains.compactMap { localItem in - let isRemoved = remoteMapping[localItem.chainId] == nil - return isRemoved ? localItem : nil + let disabledOmittedChains: [ChainModel] = localChains.compactMap { + localItem -> ChainModel? in + guard + !localItem.chainId.hasPrefix( + ChainModelMapper.quarantinedChainIdentifierPrefix + ), + remoteMapping[localItem.chainId] == nil, + !localItem.disabled + else { + return nil + } + + return localItem.replacingDisabled(true) } - let syncChanges = SyncChanges(newOrUpdatedItems: newOrUpdated, removedItems: removed) + newOrUpdated.append(contentsOf: disabledOmittedChains) + + let syncChanges = SyncChanges( + newOrUpdatedItems: newOrUpdated, + removedItems: [] + ) handle(syncChanges: syncChanges) } + static func preservingLocalNodePreferences( + remoteChains: [ChainModel], + localChains: [ChainModel] + ) -> [ChainModel] { + let localMapping = localChains.reduce(into: [ChainModel.Id: ChainModel]()) { mapping, chain in + mapping[chain.chainId] = chain + } + + return remoteChains.map { remoteChain in + guard let localChain = localMapping[remoteChain.chainId] else { + return remoteChain + } + + let localCustomNodes = Set( + (localChain.customNodes ?? []).filter { + ChainModelMapper.isUsableNodeURL($0.url) + && $0.name.trimmingCharacters( + in: .whitespacesAndNewlines + ).isNotEmpty + && ChainModelMapper + .isNodeCompatibleWithRuntime( + $0, + for: remoteChain + ) + } + ) + let selectedNode: ChainNodeModel? + + if let localSelectedNode = localChain.selectedNode, + ChainModelMapper.isNodeCompatibleWithRuntime( + localSelectedNode, + for: remoteChain + ) { + selectedNode = remoteChain.nodes.first { + $0.url == localSelectedNode.url + } ?? localCustomNodes.first { + $0.url == localSelectedNode.url + } + } else { + selectedNode = nil + } + + return remoteChain + .replacingCustomNodes(Array(localCustomNodes)) + .replacingSelectedNode(selectedNode) + } + } + private func handle(syncChanges: SyncChanges) { let localSaveOperation = repository.saveOperation({ syncChanges.newOrUpdatedItems @@ -347,9 +585,22 @@ final class ChainSyncService { syncChanges.removedItems.map { $0.identifier } }) - localSaveOperation.completionBlock = { + localSaveOperation.completionBlock = { [weak self, weak localSaveOperation] in + let result: Result + + do { + guard let localSaveOperation else { + throw BaseOperationError.parentOperationCancelled + } + + _ = try localSaveOperation.extractNoCancellableResultData() + result = .success(syncChanges) + } catch { + result = .failure(error) + } + DispatchQueue.global(qos: .userInitiated).async { [weak self] in - self?.complete(result: .success(syncChanges)) + self?.complete(result: result) } } @@ -388,7 +639,10 @@ final class ChainSyncService { ) } + mutex.lock() + isSyncInFlight = false retryAttempt = 0 + mutex.unlock() let event = ChainSyncDidComplete( newOrUpdatedChains: changes.newOrUpdatedItems, @@ -398,7 +652,15 @@ final class ChainSyncService { eventCenter.notify(with: event) case let .failure(error): logger?.error("Sync failed with error: \(error)") - timer.stop() + mutex.lock() + let failedReservationIdentifier = reservationIdentifier + isSyncInFlight = false + isCooldownActive = false + reservationIdentifier &+= 1 + mutex.unlock() + stopCooldownTimer( + forFailedReservation: failedReservationIdentifier + ) let event = ChainSyncDidFail(error: error) eventCenter.notify(with: event) @@ -406,8 +668,33 @@ final class ChainSyncService { } } + private func stopCooldownTimer( + forFailedReservation failedReservationIdentifier: UInt + ) { + cooldownTimerQueue.async { + self.mutex.lock() + let shouldStopTimer = + self.timerReservationIdentifier == + failedReservationIdentifier + if shouldStopTimer { + self.timerReservationIdentifier = nil + } + self.mutex.unlock() + + if shouldStopTimer { + self.timer.stop() + } + } + } + private func retry() { - if let nextDelay = retryStrategy.reconnectAfter(attempt: retryAttempt) { + mutex.lock() + let currentRetryAttempt = retryAttempt + mutex.unlock() + + if let nextDelay = retryStrategy.reconnectAfter( + attempt: currentRetryAttempt + ) { logger?.debug("Scheduling chain sync retry after \(nextDelay)") scheduler.notifyAfter(nextDelay) @@ -417,42 +704,38 @@ final class ChainSyncService { extension ChainSyncService: ChainSyncServiceProtocol { func syncUp() { - mutex.lock() - - defer { - mutex.unlock() - } - - if retryAttempt > 0 { - scheduler.cancel() - } - - setApplicationDelegateIfNeeded() - performSyncUpIfNeeded() + performSyncUpIfNeeded( + cancelScheduledRetry: true, + installApplicationDelegate: true + ) } } extension ChainSyncService: SchedulerDelegate { func didTrigger(scheduler _: SchedulerProtocol) { - mutex.lock() - - defer { - mutex.unlock() - } - performSyncUpIfNeeded() } } extension ChainSyncService: CountdownTimerDelegate { - func didStart(with _: TimeInterval) { - isSyncing = true - } + func didStart(with _: TimeInterval) {} func didCountdown(remainedInterval _: TimeInterval) {} func didStop(with _: TimeInterval) { - isSyncing = false + mutex.lock() + defer { mutex.unlock() } + + guard case .stopped = timer.state else { + return + } + + guard timerReservationIdentifier == reservationIdentifier else { + return + } + + isCooldownActive = false + timerReservationIdentifier = nil } } diff --git a/fearless/Common/Services/ChainRegistry/ConnectionPool/EthereumConnectionPool.swift b/fearless/Common/Services/ChainRegistry/ConnectionPool/EthereumConnectionPool.swift index 768d67444c..8def7f1ac3 100644 --- a/fearless/Common/Services/ChainRegistry/ConnectionPool/EthereumConnectionPool.swift +++ b/fearless/Common/Services/ChainRegistry/ConnectionPool/EthereumConnectionPool.swift @@ -8,21 +8,29 @@ final class EthereumConnectionPool: ConnectionPoolProtocol { private(set) var connectionsByChainIds: [ChainModel.Id: Web3.Eth] = [:] private weak var delegate: ConnectionPoolDelegate? + private let nodeFetching: EthereumNodeFetchingProtocol private lazy var lock = NSLock() - func setupConnection(for chain: SSFModels.ChainModel) throws -> Web3.Eth { - if let connection = connectionsByChainIds[chain.chainId] { - return connection - } - - let ws = try EthereumNodeFetching().getNode(for: chain) + init( + nodeFetching: EthereumNodeFetchingProtocol = + EthereumNodeFetching() + ) { + self.nodeFetching = nodeFetching + } + func setupConnection(for chain: SSFModels.ChainModel) throws -> Web3.Eth { lock.lock() defer { lock.unlock() } + if let connection = connectionsByChainIds[chain.chainId] { + return connection + } + + let ws = try nodeFetching.getNode(for: chain) + connectionsByChainIds[chain.chainId] = ws return ws @@ -42,6 +50,11 @@ final class EthereumConnectionPool: ConnectionPoolProtocol { } func resetConnection(for chainId: ChainModel.Id) { + lock.lock() + defer { + lock.unlock() + } + connectionsByChainIds = connectionsByChainIds.filter { $0.key != chainId } } } diff --git a/fearless/Common/Storage/CompatibilityModels/Compiled/CompatibleUserDataModel_v13.mom b/fearless/Common/Storage/CompatibilityModels/Compiled/CompatibleUserDataModel_v13.mom new file mode 100644 index 0000000000000000000000000000000000000000..bb4949a3c467ad4a7d0b049538c0afae3410538b GIT binary patch literal 7578 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 FzX5H-2<-p> literal 0 HcmV?d00001 diff --git a/fearless/Common/Storage/CompatibilityModels/Compiled/LegacyEcosystemUserDataModel_v12.mom b/fearless/Common/Storage/CompatibilityModels/Compiled/LegacyEcosystemUserDataModel_v12.mom new file mode 100644 index 0000000000000000000000000000000000000000..f091bb186b673e7403033162fc492ecf25e6c778 GIT binary patch literal 7468 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 literal 0 HcmV?d00001 diff --git a/fearless/Common/Storage/CompatibilityModels/README.md b/fearless/Common/Storage/CompatibilityModels/README.md new file mode 100644 index 0000000000..29cd70c8d3 --- /dev/null +++ b/fearless/Common/Storage/CompatibilityModels/README.md @@ -0,0 +1,34 @@ +# User storage compatibility models + +These models repair the launch migration path for installations whose existing +database was created by the ecosystem/TON storage schema. + +- `LegacyEcosystemUserDataModel_v12` is an immutable compatibility source. Its + Core Data model checksum must remain + `+oGDYB1AZIk54P/liFbJ3aXArqvhW7YLFAy04AChJ+s=`. +- `CompatibleUserDataModel_v13` is the active superset model. It retains the + legacy ecosystem and TON fields and the public app's `ethereumBased` field. + Its checksum is + `dX3y/Aa+rMRI3ZxRibOGIkyCHwrc0zgNO9OoBnS5G/Y=`. + +The compiled `.mom` files in `Compiled/` are generated with `momc`, copied into +the app bundle, and verified by migration tests. Never edit the legacy source +model in place; add a new destination model and migration edge instead. + +From the iOS repository root, regenerate and audit the checked-in resources: + +```sh +scripts/storage/compile-user-storage-compatibility-models.sh +scripts/storage/audit-user-storage-compatibility-models.sh +``` + +Run the destructive-fixture test suite for the audit itself: + +```sh +scripts/storage/test-user-storage-compatibility-model-audit.sh +``` + +The audit compiles fresh resources, compares Core Data's semantic model +checksums, validates that both schemas retain the ecosystem and TON fields, +validates that the destination also retains `ethereumBased`, and rejects empty, +corrupt, stale, malformed, missing, or duplicated compatibility data. diff --git a/fearless/Common/Storage/CompatibilityModels/Source/CompatibleUserDataModel_v13.xcdatamodel/contents b/fearless/Common/Storage/CompatibilityModels/Source/CompatibleUserDataModel_v13.xcdatamodel/contents new file mode 100644 index 0000000000..b2aa27c19a --- /dev/null +++ b/fearless/Common/Storage/CompatibilityModels/Source/CompatibleUserDataModel_v13.xcdatamodel/contents @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fearless/Common/Storage/CompatibilityModels/Source/LegacyEcosystemUserDataModel_v12.xcdatamodel/contents b/fearless/Common/Storage/CompatibilityModels/Source/LegacyEcosystemUserDataModel_v12.xcdatamodel/contents new file mode 100644 index 0000000000..a95b22480c --- /dev/null +++ b/fearless/Common/Storage/CompatibilityModels/Source/LegacyEcosystemUserDataModel_v12.xcdatamodel/contents @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fearless/Common/Storage/EntityToModel/ChainModelMapper.swift b/fearless/Common/Storage/EntityToModel/ChainModelMapper.swift index cba753abb2..aa6c17cf29 100644 --- a/fearless/Common/Storage/EntityToModel/ChainModelMapper.swift +++ b/fearless/Common/Storage/EntityToModel/ChainModelMapper.swift @@ -7,7 +7,28 @@ import RobinHood import SSFModels import SSFUtils +enum ChainModelMapperError: LocalizedError, Equatable { + case missingNodeURL + case missingNodeName + case invalidNodeURL + + var errorDescription: String? { + switch self { + case .missingNodeURL: + return "Stored chain node is missing its URL" + case .missingNodeName: + return "Stored chain node is missing its name" + case .invalidNodeURL: + return "Stored chain node has an unusable URL" + } + } +} + final class ChainModelMapper { + static let quarantinedChainIdentifierPrefix = "__invalid_local_chain__:" + static let quarantinedChainName = "Unavailable local chain" + private static let supportedNodeSchemes = Set(["http", "https", "ws", "wss"]) + private let assetModelMapper = AssetModelMapper() var entityIdentifierFieldName: String { #keyPath(CDChain.chainId) } @@ -15,7 +36,36 @@ final class ChainModelMapper { typealias DataProviderModel = ChainModel typealias CoreDataEntity = CDChain - private func createPriceData(from object: NSManagedObject) -> PriceData? { + func createPriceData(from object: NSManagedObject) -> PriceData? { + var priceData: PriceData? + + do { + try SafeObjectiveCExceptionBoundary.perform { + let attributes = object.entity.attributesByName + guard + attributes["currencyId"] != nil, + attributes["priceId"] != nil, + attributes["price"] != nil + else { + return + } + + priceData = self.createPriceDataWithoutExceptionBoundary( + from: object, + attributes: attributes + ) + } + } catch { + return nil + } + + return priceData + } + + private func createPriceDataWithoutExceptionBoundary( + from object: NSManagedObject, + attributes: [String: NSAttributeDescription] + ) -> PriceData? { guard let currencyId = object.value(forKey: "currencyId") as? String, let priceId = object.value(forKey: "priceId") as? String @@ -27,10 +77,19 @@ final class ChainModelMapper { if let s = object.value(forKey: "price") as? String { return s } return nil }() - guard let price = priceString else { return nil } + guard + let price = priceString, + Self.isValidCachedPrice(price) + else { + return nil + } - let fiatDayStr = object.value(forKey: "fiatDayByChange") as? String - let coingeckoPriceId = object.value(forKey: "coingeckoPriceId") as? String + let fiatDayStr = attributes["fiatDayByChange"].flatMap { _ in + object.value(forKey: "fiatDayByChange") as? String + } + let coingeckoPriceId = attributes["coingeckoPriceId"].flatMap { _ in + object.value(forKey: "coingeckoPriceId") as? String + } return PriceData( currencyId: currencyId, @@ -41,7 +100,67 @@ final class ChainModelMapper { ) } - private func createChainNode(from entity: CDChainNode) -> ChainNodeModel { + private func cachedPriceObjects( + from assetEntity: CDAsset + ) -> [NSManagedObject] { + guard + assetEntity.entity.relationshipsByName["priceData"] != nil + else { + return [] + } + + do { + let rawValue = try SafeTransformableValueReader.readObject( + from: assetEntity, + key: "priceData" + ) + + return (rawValue as? NSSet)? + .compactMap { $0 as? NSManagedObject } ?? [] + } catch { + return [] + } + } + + private static func isValidCachedPrice(_ price: String) -> Bool { + let normalizedPrice = price.trimmingCharacters( + in: .whitespacesAndNewlines + ) + guard normalizedPrice.isNotEmpty else { + return false + } + + let scanner = Scanner(string: normalizedPrice) + scanner.locale = Locale(identifier: "en_US_POSIX") + scanner.charactersToBeSkipped = nil + + guard + let decimalPrice = scanner.scanDecimal(), + scanner.isAtEnd, + !decimalPrice.isNaN + else { + return false + } + + return decimalPrice >= 0 + } + + private func createChainNode(from entity: CDChainNode) throws -> ChainNodeModel { + guard let url = entity.url else { + throw ChainModelMapperError.missingNodeURL + } + + guard Self.isUsableNodeURL(url) else { + throw ChainModelMapperError.invalidNodeURL + } + + guard + let name = entity.name, + !name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + else { + throw ChainModelMapperError.missingNodeName + } + let apiKey: ChainNodeModel.ApiKey? if let queryName = entity.apiQueryName, let keyName = entity.apiKeyName { @@ -51,12 +170,68 @@ final class ChainModelMapper { } return ChainNodeModel( - url: entity.url!, - name: entity.name!, + url: url, + name: name, apikey: apiKey ) } + static func isUsableNodeURL(_ url: URL) -> Bool { + guard + let components = URLComponents( + url: url, + resolvingAgainstBaseURL: false + ), + let scheme = components.scheme?.lowercased(), + supportedNodeSchemes.contains(scheme), + components.host?.isEmpty == false + else { + return false + } + + return true + } + + static func isNodeCompatibleWithRuntime( + _ node: ChainNodeModel, + for chain: ChainModel + ) -> Bool { + if chain.isTonCompatibilityChain { + return TonAPIClientFactory.isValidServerURL(node.url) + } + + let scheme = node.url.scheme?.lowercased() + if chain.chainBaseType == .ethereum { + return scheme == "https" || scheme == "wss" + } + + return scheme == "ws" || scheme == "wss" + } + + private func createQuarantinedChain(from entity: CDChain) -> ChainModel { + let objectIdentifier = entity.objectID.uriRepresentation().absoluteString + + return ChainModel( + rank: nil, + disabled: true, + chainId: Self.quarantinedChainIdentifierPrefix + objectIdentifier, + parentId: nil, + paraId: nil, + name: Self.quarantinedChainName, + xcm: nil, + nodes: [], + addressPrefix: UInt16(bitPattern: entity.addressPrefix), + types: nil, + icon: nil, + options: nil, + externalApi: nil, + selectedNode: nil, + customNodes: nil, + iosMinAppVersion: nil, + identityChain: nil + ) + } + private func updateEntityAsset( for entity: CDChain, from model: ChainModel, @@ -101,7 +276,10 @@ final class ChainModelMapper { let maybeExistingEntity = entity.nodes? .first { ($0 as? CDChainNode)?.url == node.url } as? CDChainNode - if let existingEntity = maybeExistingEntity { + let existingCustomEntity = entity.customNodes? + .first { ($0 as? CDChainNode)?.url == node.url } as? CDChainNode + + if let existingEntity = maybeExistingEntity ?? existingCustomEntity { nodeEntity = existingEntity } else { nodeEntity = CDChainNode(context: context) @@ -115,11 +293,26 @@ final class ChainModelMapper { return nodeEntity } - let existingNodeIds = Set(model.nodes.map(\.url)) + let retainedNodeEntities = Set(nodeEntities) if let oldNodes = entity.nodes as? Set { for oldNode in oldNodes { - if !existingNodeIds.contains(oldNode.url!) { + let oldNodeURL = oldNode.url + let remainsDefault = retainedNodeEntities.contains(oldNode) + let remainsSharedCustom = oldNodeURL.map { url in + model.customNodes?.contains { + $0.url == url + } == true && + (entity.customNodes as? Set)? + .contains(oldNode) == true + } ?? false + let remainsSharedSelection = + oldNodeURL == model.selectedNode?.url && + entity.selectedNode === oldNode + + if !remainsDefault, + !remainsSharedCustom, + !remainsSharedSelection { context.delete(oldNode) } } @@ -143,7 +336,10 @@ final class ChainModelMapper { let maybeExistingEntity = entity.customNodes? .first { ($0 as? CDChainNode)?.url == node.url } as? CDChainNode - if let existingEntity = maybeExistingEntity { + let existingDefaultEntity = entity.nodes? + .first { ($0 as? CDChainNode)?.url == node.url } as? CDChainNode + + if let existingEntity = maybeExistingEntity ?? existingDefaultEntity { nodeEntity = existingEntity } else { nodeEntity = CDChainNode(context: context) @@ -157,11 +353,26 @@ final class ChainModelMapper { return nodeEntity } - let existingNodeIds = Set(customNodes.map(\.url)) + let retainedNodeEntities = Set(nodeEntities) if let oldNodes = entity.customNodes as? Set { for oldNode in oldNodes { - if !existingNodeIds.contains(oldNode.url!) { + let oldNodeURL = oldNode.url + let remainsCustom = retainedNodeEntities.contains(oldNode) + let remainsSharedDefault = oldNodeURL.map { url in + model.nodes.contains { + $0.url == url + } && + (entity.nodes as? Set)? + .contains(oldNode) == true + } ?? false + let remainsSharedSelection = + oldNodeURL == model.selectedNode?.url && + entity.selectedNode === oldNode + + if !remainsCustom, + !remainsSharedDefault, + !remainsSharedSelection { context.delete(oldNode) } } @@ -295,9 +506,13 @@ final class ChainModelMapper { let explorerEntities = entityExplorers.allObjects as? [CDExternalApi] ?? [] let explorers: [ChainModel.ExternalApiExplorer] = explorerEntities.reduce(into: []) { result, explorer in + let storedTypes: [String]? = try? SafeTransformableValueReader.read( + from: explorer, + key: "types" + ) guard let type = explorer.type, - let types = explorer.types as? [String], + let types = storedTypes, let url = explorer.url else { return @@ -308,7 +523,7 @@ final class ChainModelMapper { result.append(ChainModel.ExternalApiExplorer( type: ChainModel.ExternalApiExplorerType(rawValue: type) ?? .unknown, types: externapApiTypes, - url: url.absoluteString + url: url )) } return explorers @@ -326,7 +541,7 @@ final class ChainModelMapper { let explorer = CDExternalApi(context: context) explorer.type = api.type.rawValue explorer.types = api.types.compactMap { $0.rawValue } as? NSArray - explorer.url = URL(string: api.url) + explorer.url = api.url return explorer } entity.explorers = Set(explorers) as NSSet @@ -391,12 +606,60 @@ final class ChainModelMapper { extension ChainModelMapper: CoreDataMapperProtocol { func transform(entity: CDChain) throws -> ChainModel { + var mappedChain: ChainModel? + + // Mapping a chain traverses several generated Core Data relationships. + // Any one of those getters can raise NSException for a damaged legacy + // row, so the complete startup read is an atomic exception boundary. + try SafeObjectiveCExceptionBoundary.perform { + mappedChain = try self.transformWithoutExceptionBoundary( + entity: entity + ) + } + + guard let mappedChain else { + throw SafeTransformableValueReaderError.objectiveCException + } + + return mappedChain + } + + private func transformWithoutExceptionBoundary( + entity: CDChain + ) throws -> ChainModel { + guard + let rawChainId = entity.chainId, + rawChainId.isNotEmpty + else { + return createQuarantinedChain(from: entity) + } + + let storedChainId = rawChainId.trimmingCharacters( + in: .whitespacesAndNewlines + ) + guard + storedChainId.isNotEmpty, + storedChainId == rawChainId + else { + return createQuarantinedChain(from: entity) + } + + let storedName = entity.name? + .trimmingCharacters(in: .whitespacesAndNewlines) + let hasValidName = storedName?.isNotEmpty == true + let name: String + if let storedName, storedName.isNotEmpty { + name = storedName + } else { + name = Self.quarantinedChainName + } + let nodes: [ChainNodeModel] = entity.nodes?.compactMap { anyNode in guard let node = anyNode as? CDChainNode else { return nil } - return createChainNode(from: node) + return try? createChainNode(from: node) } ?? [] var customNodesSet: Set? @@ -406,7 +669,7 @@ extension ChainModelMapper: CoreDataMapperProtocol { return nil } - return createChainNode(from: node) + return try? createChainNode(from: node) } if let nodes = customNodes { @@ -417,18 +680,21 @@ extension ChainModelMapper: CoreDataMapperProtocol { var selectedNode: ChainNodeModel? if let selectedNodeEntity = entity.selectedNode { - selectedNode = createChainNode(from: selectedNodeEntity) + selectedNode = try? createChainNode(from: selectedNodeEntity) } let types: ChainModel.TypesSettings? - if let url = entity.types.flatMap(URL.init(string:)), let overridesCommon = entity.typesOverrideCommon { + if let url = entity.types, let overridesCommon = entity.typesOverrideCommon { types = .init(url: url, overridesCommon: overridesCommon.boolValue) } else { types = nil } - let options = entity.options as? [String] + let options: [String]? = try? SafeTransformableValueReader.read( + from: entity, + key: "options" + ) let externalApiSet = createExternalApi(from: entity) let xcm = createXcmConfig(from: entity) let paraId: String? = { @@ -449,17 +715,19 @@ extension ChainModelMapper: CoreDataMapperProtocol { rank = UInt16(rankString) } - let assetsArray: [AssetModel] = try (entity.assets?.compactMap { anyAsset in + let assetsArray: [AssetModel] = (entity.assets?.compactMap { anyAsset in guard let assetEntity = anyAsset as? CDAsset else { return nil } - let asset = try assetModelMapper.transform(entity: assetEntity) - let priceDataEntities = (assetEntity.value(forKey: "priceData") as? NSSet)? - .compactMap { $0 as? NSManagedObject } ?? [] - let cachedPrices = priceDataEntities.compactMap(createPriceData(from:)) + guard let asset = try? assetModelMapper.transform(entity: assetEntity) else { + return nil + } + let cachedPrices = cachedPriceObjects(from: assetEntity) + .compactMap(createPriceData(from:)) + .sorted(by: Self.cachedPricePrecedes) - guard !cachedPrices.isEmpty else { + guard let firstCachedPrice = cachedPrices.first else { return asset } @@ -472,16 +740,16 @@ extension ChainModelMapper: CoreDataMapperProtocol { return asset.replacingPrice(matchingCurrency) } - return asset.replacingPrice(cachedPrices[0]) + return asset.replacingPrice(firstCachedPrice) }) ?? [] let chainModel = ChainModel( rank: rank, - disabled: entity.disabled, - chainId: entity.chainId!, + disabled: entity.disabled || !hasValidName, + chainId: storedChainId, parentId: entity.parentId, paraId: paraId, - name: entity.name!, + name: name, xcm: xcm, nodes: Set(nodes), addressPrefix: UInt16(bitPattern: entity.addressPrefix), @@ -497,7 +765,67 @@ extension ChainModelMapper: CoreDataMapperProtocol { chainModel.assets = Set(assetsArray) + let compatibleNodes = Set( + chainModel.nodes.filter { + Self.isNodeCompatibleWithRuntime($0, for: chainModel) + } + ) + let compatibleCustomNodes = chainModel.customNodes.map { + Set( + $0.filter { + Self.isNodeCompatibleWithRuntime( + $0, + for: chainModel + ) + } + ) + } + let compatibleSelectedNode = chainModel.selectedNode.flatMap { + Self.isNodeCompatibleWithRuntime($0, for: chainModel) + ? $0 + : nil + } + let hasRuntimeEndpoint = + compatibleNodes.isNotEmpty || + compatibleSelectedNode != nil + return chainModel + .replacingNodeConfiguration( + nodes: compatibleNodes, + selectedNode: compatibleSelectedNode, + customNodes: compatibleCustomNodes + ) + .replacingDisabled( + chainModel.disabled || !hasRuntimeEndpoint + ) + } + + private static func cachedPricePrecedes( + _ lhs: PriceData, + _ rhs: PriceData + ) -> Bool { + let lhsDayChange = lhs.fiatDayChange.map { + NSDecimalNumber(decimal: $0).stringValue + } ?? "" + let rhsDayChange = rhs.fiatDayChange.map { + NSDecimalNumber(decimal: $0).stringValue + } ?? "" + let lhsKey = [ + lhs.priceId, + lhs.currencyId, + lhs.coingeckoPriceId ?? "", + lhs.price, + lhsDayChange + ] + let rhsKey = [ + rhs.priceId, + rhs.currencyId, + rhs.coingeckoPriceId ?? "", + rhs.price, + rhsDayChange + ] + + return lhsKey.lexicographicallyPrecedes(rhsKey) } func populate( @@ -515,7 +843,7 @@ extension ChainModelMapper: CoreDataMapperProtocol { } entity.parentId = model.parentId entity.name = model.name - entity.types = model.types?.url.absoluteString + entity.types = model.types?.url entity.typesOverrideCommon = model.types.map { NSNumber(value: $0.overridesCommon) } entity.addressPrefix = Int16(bitPattern: model.addressPrefix) diff --git a/fearless/Common/Storage/EntityToModel/ManagedMetaAccountMapper.swift b/fearless/Common/Storage/EntityToModel/ManagedMetaAccountMapper.swift index e2b5442840..360ff50b76 100644 --- a/fearless/Common/Storage/EntityToModel/ManagedMetaAccountMapper.swift +++ b/fearless/Common/Storage/EntityToModel/ManagedMetaAccountMapper.swift @@ -51,6 +51,10 @@ extension ManagedMetaAccountMapper: CoreDataMapperProtocol { order = maybeLastItem?.order ?? 0 + guard order < Int32.max else { + throw MetaAccountMapperError.walletOrderOverflow + } + entity.order = order + 1 } else { entity.order = Int32(bitPattern: model.order) diff --git a/fearless/Common/Storage/EntityToModel/MetaAccountMapper.swift b/fearless/Common/Storage/EntityToModel/MetaAccountMapper.swift index dc34ffc805..31c4fa04da 100644 --- a/fearless/Common/Storage/EntityToModel/MetaAccountMapper.swift +++ b/fearless/Common/Storage/EntityToModel/MetaAccountMapper.swift @@ -6,6 +6,23 @@ import SSFModels import SSFAccountManagmentStorage #endif +enum MetaAccountMapperError: LocalizedError { + case unsupportedWalletRecord + case invalidWalletRecord + case walletOrderOverflow + + var errorDescription: String? { + switch self { + case .unsupportedWalletRecord: + return "The stored wallet does not contain the account fields supported by this app version" + case .invalidWalletRecord: + return "The stored wallet record is invalid" + case .walletOrderOverflow: + return "The wallet order cannot be incremented safely" + } + } +} + final class MetaAccountMapper { var entityIdentifierFieldName: String { #keyPath(CDMetaAccount.metaId) } @@ -15,26 +32,116 @@ final class MetaAccountMapper { extension MetaAccountMapper: CoreDataMapperProtocol { func transform(entity: CoreDataEntity) throws -> DataProviderModel { + var mappedAccount: DataProviderModel? + + // A generated Core Data getter may raise NSException when an old or + // damaged row cannot materialize. Keep the complete read graph, + // including every child relationship traversal, inside one Objective-C + // exception boundary so startup receives a catchable Swift error. + try SafeObjectiveCExceptionBoundary.perform { + mappedAccount = try self.transformWithoutExceptionBoundary( + entity: entity + ) + } + + guard let mappedAccount else { + throw MetaAccountMapperError.invalidWalletRecord + } + + return mappedAccount + } + + private func transformWithoutExceptionBoundary( + entity: CoreDataEntity + ) throws -> DataProviderModel { + guard + let metaId = entity.metaId, + !metaId.isEmpty, + metaId == metaId.trimmingCharacters(in: .whitespacesAndNewlines), + let name = entity.name, + !name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + else { + throw MetaAccountMapperError.invalidWalletRecord + } + + guard + let substrateAccountIdHex = entity.substrateAccountId, + let substratePublicKey = entity.substratePublicKey + else { + throw MetaAccountMapperError.unsupportedWalletRecord + } + + guard + !substrateAccountIdHex.isEmpty, + let substrateCryptoType = CryptoType( + rawValue: UInt8(truncatingIfNeeded: entity.substrateCryptoType) + ), + Int16(substrateCryptoType.rawValue) == entity.substrateCryptoType + else { + throw MetaAccountMapperError.invalidWalletRecord + } + + let expectedSubstratePublicKeyLength = substrateCryptoType == .ecdsa ? 33 : 32 + guard substratePublicKey.count == expectedSubstratePublicKeyLength else { + throw MetaAccountMapperError.invalidWalletRecord + } + let chainAccountEntities = entity.chainAccounts?.allObjects as? [CDChainAccount] ?? [] var chainAccounts: [ChainAccountModel] = [] + var canonicalChainIds = Set() for chainAccountEntity in chainAccountEntities { guard let accountIdHex = chainAccountEntity.accountId, let chainId = chainAccountEntity.chainId, - let publicKey = chainAccountEntity.publicKey + !chainId.isEmpty, + chainId == chainId.trimmingCharacters(in: .whitespacesAndNewlines), + let publicKey = chainAccountEntity.publicKey, + let cryptoType = CryptoType( + rawValue: UInt8(truncatingIfNeeded: chainAccountEntity.cryptoType) + ), + Int16(cryptoType.rawValue) == chainAccountEntity.cryptoType else { - continue + throw MetaAccountMapperError.invalidWalletRecord } let accountId = try Data(hexStringSSF: accountIdHex) + guard !accountId.isEmpty else { + throw MetaAccountMapperError.invalidWalletRecord + } + + let expectedPublicKeyLength = cryptoType == .ecdsa ? 33 : 32 + guard publicKey.count == expectedPublicKeyLength else { + throw MetaAccountMapperError.invalidWalletRecord + } + + let canonicalChainId = + UniversalWalletChainAccountSupport.canonicalChainId(for: chainId) + guard canonicalChainIds.insert(canonicalChainId).inserted else { + // `chainAccounts` is stored as an unordered Core Data relationship. Allowing + // two rows for one chain would make the selected account depend on fetch order. + throw MetaAccountMapperError.invalidWalletRecord + } + + let storedEcosystem: String? + if chainAccountEntity.entity.propertiesByName["ecosystem"] != nil { + storedEcosystem = chainAccountEntity.value(forKey: "ecosystem") as? String + } else { + storedEcosystem = nil + } + + let isEthereumBased = + storedEcosystem == UniversalWalletEcosystem.evm.rawValue || + storedEcosystem == "ethereum" || + storedEcosystem == "ethereumBased" || + chainAccountEntity.ethereumBased chainAccounts.append( ChainAccountModel( chainId: chainId, accountId: accountId, publicKey: publicKey, - cryptoType: UInt8(truncatingIfNeeded: chainAccountEntity.cryptoType), - ethereumBased: chainAccountEntity.ethereumBased + cryptoType: cryptoType.rawValue, + ethereumBased: isEthereumBased ) ) } @@ -54,8 +161,25 @@ extension MetaAccountMapper: CoreDataMapperProtocol { ) } - let substrateAccountId = try Data(hexStringSSF: entity.substrateAccountId!) - let ethereumAddress = try entity.ethereumAddress.map { try Data(hexStringSSF: $0) } + let substrateAccountId = try Data(hexStringSSF: substrateAccountIdHex) + guard substrateAccountId.count == 32 else { + throw MetaAccountMapperError.invalidWalletRecord + } + + let ethereumAddress = try entity.ethereumAddress.map { + let address = try Data(hexStringSSF: $0) + guard address.count == 20 else { + throw MetaAccountMapperError.invalidWalletRecord + } + + return address + } + + if let ethereumPublicKey = entity.ethereumPublicKey, + ethereumPublicKey.isEmpty { + throw MetaAccountMapperError.invalidWalletRecord + } + // Read assetsVisibility relationship via KVC, but only if the property exists in the loaded model var assetsVisibility: [AssetVisibility] = [] if entity.entity.propertiesByName["assetsVisibility"] != nil, @@ -66,23 +190,35 @@ extension MetaAccountMapper: CoreDataMapperProtocol { return AssetVisibility(assetId: assetId, hidden: hidden) } } - var favouriteChainIds: [String] = [] - if let entityFavouriteChainIds = entity.favouriteChainIds { - favouriteChainIds = (entityFavouriteChainIds as? [String]) ?? [] - } + // These are user preferences, not wallet identity. Decode each behind + // its own exception boundary so one damaged archive cannot hide an + // otherwise usable wallet or discard the other valid preferences. + let assetKeysOrder: [String]? = try? SafeTransformableValueReader.read( + from: entity, + key: "assetKeysOrder" + ) + let unusedChainIds: [String]? = try? SafeTransformableValueReader.read( + from: entity, + key: "unusedChainIds" + ) + let favouriteChainIds: [String] = + (try? SafeTransformableValueReader.read( + from: entity, + key: "favouriteChainIds" + )) ?? [] return DataProviderModel( - metaId: entity.metaId!, - name: entity.name!, + metaId: metaId, + name: name, substrateAccountId: substrateAccountId, - substrateCryptoType: UInt8(truncatingIfNeeded: entity.substrateCryptoType), - substratePublicKey: entity.substratePublicKey!, + substrateCryptoType: substrateCryptoType.rawValue, + substratePublicKey: substratePublicKey, ethereumAddress: ethereumAddress, ethereumPublicKey: entity.ethereumPublicKey, chainAccounts: Set(chainAccounts), - assetKeysOrder: entity.assetKeysOrder as? [String], + assetKeysOrder: assetKeysOrder, canExportEthereumMnemonic: entity.canExportEthereumMnemonic, - unusedChainIds: entity.unusedChainIds as? [String], + unusedChainIds: unusedChainIds, selectedCurrency: selectedCurrency ?? Currency.defaultCurrency(), networkManagmentFilter: entity.networkManagmentFilter, assetsVisibility: assetsVisibility, @@ -96,6 +232,13 @@ extension MetaAccountMapper: CoreDataMapperProtocol { from model: DataProviderModel, using context: NSManagedObjectContext ) throws { + // Validate unordered child identities before changing the managed object. A model can + // contain distinct `ChainAccountModel` values whose chain IDs are aliases of one chain. + // Persisting both would make subsequent account selection depend on NSSet iteration order. + try validateChainAccountsBeforeMutation(model.chainAccounts) + let storedChainAccounts = entity.chainAccounts?.allObjects as? [CDChainAccount] ?? [] + try validateStoredChainAccountsBeforeMutation(storedChainAccounts) + entity.metaId = model.metaId entity.name = model.name entity.substrateAccountId = model.substrateAccountId.toHex() @@ -132,9 +275,13 @@ extension MetaAccountMapper: CoreDataMapperProtocol { } for chainAccount in model.chainAccounts { + let canonicalChainId = + UniversalWalletChainAccountSupport.canonicalChainId(for: chainAccount.chainId) var chainAccountEntity = entity.chainAccounts?.first { if let entity = $0 as? CDChainAccount, - entity.chainId == chainAccount.chainId { + let storedChainId = entity.chainId, + UniversalWalletChainAccountSupport.canonicalChainId(for: storedChainId) == + canonicalChainId { return true } else { return false @@ -152,11 +299,90 @@ extension MetaAccountMapper: CoreDataMapperProtocol { chainAccountEntity?.cryptoType = Int16(bitPattern: UInt16(chainAccount.cryptoType)) chainAccountEntity?.publicKey = chainAccount.publicKey chainAccountEntity?.ethereumBased = chainAccount.ethereumBased + + if + let chainAccountEntity, + chainAccountEntity.entity.propertiesByName["ecosystem"] != nil, + chainAccountEntity.value(forKey: "ecosystem") == nil { + chainAccountEntity.setValue( + chainAccount.ethereumBased + ? UniversalWalletEcosystem.evm.rawValue + : UniversalWalletEcosystem.substrate.rawValue, + forKey: "ecosystem" + ) + } } updatedEntityCurrency(for: entity, from: model, context: context) } + private func validateChainAccountsBeforeMutation( + _ chainAccounts: Set + ) throws { + var canonicalChainIds = Set() + + for chainAccount in chainAccounts { + let chainId = chainAccount.chainId + let cryptoType = CryptoType(rawValue: chainAccount.cryptoType) + guard + !chainId.isEmpty, + chainId == chainId.trimmingCharacters(in: .whitespacesAndNewlines), + !chainAccount.accountId.isEmpty, + let cryptoType + else { + throw MetaAccountMapperError.invalidWalletRecord + } + + let expectedPublicKeyLength = cryptoType == .ecdsa ? 33 : 32 + guard chainAccount.publicKey.count == expectedPublicKeyLength else { + throw MetaAccountMapperError.invalidWalletRecord + } + + let canonicalChainId = + UniversalWalletChainAccountSupport.canonicalChainId(for: chainId) + guard canonicalChainIds.insert(canonicalChainId).inserted else { + throw MetaAccountMapperError.invalidWalletRecord + } + } + } + + private func validateStoredChainAccountsBeforeMutation( + _ chainAccounts: [CDChainAccount] + ) throws { + var canonicalChainIds = Set() + + for chainAccount in chainAccounts { + guard + let accountIdHex = chainAccount.accountId, + let chainId = chainAccount.chainId, + !chainId.isEmpty, + chainId == chainId.trimmingCharacters(in: .whitespacesAndNewlines), + let publicKey = chainAccount.publicKey, + let cryptoType = CryptoType( + rawValue: UInt8(truncatingIfNeeded: chainAccount.cryptoType) + ), + Int16(cryptoType.rawValue) == chainAccount.cryptoType + else { + throw MetaAccountMapperError.invalidWalletRecord + } + + let accountId = try Data(hexStringSSF: accountIdHex) + let expectedPublicKeyLength = cryptoType == .ecdsa ? 33 : 32 + guard + !accountId.isEmpty, + publicKey.count == expectedPublicKeyLength + else { + throw MetaAccountMapperError.invalidWalletRecord + } + + let canonicalChainId = + UniversalWalletChainAccountSupport.canonicalChainId(for: chainId) + guard canonicalChainIds.insert(canonicalChainId).inserted else { + throw MetaAccountMapperError.invalidWalletRecord + } + } + } + private func updatedEntityCurrency( for entity: CoreDataEntity, from model: DataProviderModel, @@ -172,3 +398,243 @@ extension MetaAccountMapper: CoreDataMapperProtocol { entity.selectedCurrency = currencyEntity } } + +/// A selection-safe projection of a stored wallet. Unsupported or corrupt rows are represented +/// without manufacturing a `MetaAccountModel`; corrupt projections are strictly read-only. +enum MetaAccountSelectionRecordState: Equatable { + case supported + case unsupported + case corrupt + + var allowsStoredRecordUpdates: Bool { + self != .corrupt + } +} + +struct MetaAccountSelectionModel: Identifiable { + let identifier: String + let wallet: MetaAccountModel? + let isSelected: Bool + let order: UInt32 + let recordState: MetaAccountSelectionRecordState + let updatesWalletPayload: Bool + let updatesSelection: Bool + + init( + identifier: String, + wallet: MetaAccountModel?, + isSelected: Bool, + order: UInt32, + recordState: MetaAccountSelectionRecordState = .supported, + updatesWalletPayload: Bool = false, + updatesSelection: Bool = true + ) { + self.identifier = identifier + self.wallet = wallet + self.isSelected = isSelected + self.order = order + self.recordState = recordState + self.updatesWalletPayload = updatesWalletPayload + self.updatesSelection = updatesSelection + } + + func replacingSelection(_ isSelected: Bool) -> MetaAccountSelectionModel { + MetaAccountSelectionModel( + identifier: identifier, + wallet: wallet, + isSelected: isSelected, + order: order, + recordState: recordState, + updatesSelection: recordState.allowsStoredRecordUpdates + ) + } +} + +final class MetaAccountSelectionMapper { + var entityIdentifierFieldName: String { #keyPath(CDMetaAccount.metaId) } + + typealias DataProviderModel = MetaAccountSelectionModel + typealias CoreDataEntity = CDMetaAccount + + private lazy var metaAccountMapper = MetaAccountMapper() + + private func quarantineIdentifier(for entity: CDMetaAccount) -> String { + "fearless.quarantined-wallet:\(entity.objectID.uriRepresentation().absoluteString)" + } +} + +extension MetaAccountSelectionMapper: CoreDataMapperProtocol { + func transform(entity: CDMetaAccount) throws -> MetaAccountSelectionModel { + var mappedSelection: MetaAccountSelectionModel? + + // Selection is the first wallet read on startup. It must be protected + // independently of MetaAccountMapper because corrupt rows may throw + // while reading the identifier, selection flag, or ordering fields + // before the nested wallet mapper is reached. + try SafeObjectiveCExceptionBoundary.perform { + mappedSelection = try self.transformWithoutExceptionBoundary( + entity: entity + ) + } + + guard let mappedSelection else { + throw MetaAccountMapperError.invalidWalletRecord + } + + return mappedSelection + } + + private func transformWithoutExceptionBoundary( + entity: CDMetaAccount + ) throws -> MetaAccountSelectionModel { + let storedIdentifier: String? + let storedIsSelected: Bool + let storedOrder: UInt32 + + do { + storedIdentifier = try SafeTransformableValueReader.read( + from: entity, + key: "metaId" + ) + let selectedNumber: NSNumber? = + try SafeTransformableValueReader.read( + from: entity, + key: "isSelected" + ) + let orderNumber: NSNumber? = + try SafeTransformableValueReader.read( + from: entity, + key: "order" + ) + storedIsSelected = selectedNumber?.boolValue ?? false + storedOrder = UInt32( + bitPattern: orderNumber?.int32Value ?? 0 + ) + } catch { + // The selection repository must keep mapping healthy siblings even + // if this row cannot materialize its own projection fields. + return MetaAccountSelectionModel( + identifier: quarantineIdentifier(for: entity), + wallet: nil, + isSelected: false, + order: 0, + recordState: .corrupt + ) + } + + let identifier = storedIdentifier.flatMap { + let trimmedIdentifier = $0.trimmingCharacters(in: .whitespacesAndNewlines) + return !$0.isEmpty && $0 == trimmedIdentifier ? $0 : nil + } ?? quarantineIdentifier(for: entity) + let wallet: MetaAccountModel? + let recordState: MetaAccountSelectionRecordState + + guard storedIdentifier.map({ + !$0.isEmpty && + $0 == $0.trimmingCharacters(in: .whitespacesAndNewlines) + }) == true + else { + wallet = nil + recordState = .corrupt + + return MetaAccountSelectionModel( + identifier: identifier, + wallet: wallet, + isSelected: storedIsSelected, + order: storedOrder, + recordState: recordState + ) + } + + do { + wallet = try metaAccountMapper.transform(entity: entity) + recordState = .supported + } catch MetaAccountMapperError.unsupportedWalletRecord { + wallet = nil + recordState = .unsupported + } catch { + // A damaged app-local row must not abort mapping of its healthy siblings. + // Keep it as an immutable projection so no read or selection repair rewrites it. + wallet = nil + recordState = .corrupt + } + + return MetaAccountSelectionModel( + identifier: identifier, + wallet: wallet, + isSelected: storedIsSelected, + order: storedOrder, + recordState: recordState + ) + } + + func populate( + entity: CDMetaAccount, + from model: MetaAccountSelectionModel, + using context: NSManagedObjectContext + ) throws { + if model.updatesWalletPayload { + guard + let wallet = model.wallet, + wallet.metaId == model.identifier, + entity.metaId == nil || entity.metaId == model.identifier + else { + throw MetaAccountMapperError.invalidWalletRecord + } + + let isNew = entity.metaId == nil + + if !isNew { + do { + _ = try metaAccountMapper.transform(entity: entity) + } catch { + throw SelectedWalletSettingsError.unsupportedWalletIdentifierConflict + } + } + + try metaAccountMapper.populate(entity: entity, from: wallet, using: context) + + if isNew { + entity.order = try nextOrder(in: context) + } else if model.order != ManagedMetaAccountModel.noOrder { + entity.order = Int32(bitPattern: model.order) + } + } else { + guard entity.metaId == model.identifier else { + // A selection repair must never manufacture a partial wallet row. + throw MetaAccountMapperError.invalidWalletRecord + } + + guard + !model.updatesSelection || + model.recordState.allowsStoredRecordUpdates + else { + // Corrupt projections are read-only, including their selection metadata. + throw MetaAccountMapperError.invalidWalletRecord + } + } + + if model.updatesSelection { + entity.isSelected = model.isSelected + } + } + + private func nextOrder(in context: NSManagedObjectContext) throws -> Int32 { + let fetchRequest = NSFetchRequest(entityName: "CDMetaAccount") + fetchRequest.includesPendingChanges = true + fetchRequest.includesSubentities = false + fetchRequest.sortDescriptors = [ + NSSortDescriptor(key: #keyPath(CDMetaAccount.order), ascending: false) + ] + fetchRequest.predicate = NSPredicate(format: "%K > 0", #keyPath(CDMetaAccount.order)) + fetchRequest.fetchLimit = 1 + + let lastOrder = try context.fetch(fetchRequest).first?.order ?? 0 + + guard lastOrder < Int32.max else { + throw MetaAccountMapperError.walletOrderOverflow + } + + return lastOrder + 1 + } +} diff --git a/fearless/Common/Storage/EntityToModel/PolkaswapSettingMapper.swift b/fearless/Common/Storage/EntityToModel/PolkaswapSettingMapper.swift index 584f12ea05..96692cce37 100644 --- a/fearless/Common/Storage/EntityToModel/PolkaswapSettingMapper.swift +++ b/fearless/Common/Storage/EntityToModel/PolkaswapSettingMapper.swift @@ -19,11 +19,26 @@ final class PolkaswapSettingMapper { extension PolkaswapSettingMapper: CoreDataMapperProtocol { func transform(entity: SSFAssetManagmentStorage.CDPolkaswapRemoteSettings) throws -> PolkaswapRemoteSettings { + let storedAvailableSources: [String]? + let forceSmartIds: [String]? + do { + storedAvailableSources = try SafeTransformableValueReader.read( + from: entity, + key: "availableSources" + ) + forceSmartIds = try SafeTransformableValueReader.read( + from: entity, + key: "forceSmartIds" + ) + } catch { + throw PolkaswapSettingMapperError.requiredFieldsMissing + } + guard let version = entity.version, - let availableSources = entity.availableSources?.compactMap({ + let availableSources = storedAvailableSources?.compactMap({ LiquiditySourceType(rawValue: $0) }), - let forceSmartIds = entity.forceSmartIds, + let forceSmartIds, let availableDexIdsSet = entity.availableDexIds, let xstusdId = entity.xstusdId else { diff --git a/fearless/Common/Storage/Migration/KeystoreMigrator.swift b/fearless/Common/Storage/Migration/KeystoreMigrator.swift index d9fa22cda1..a95e28de1c 100644 --- a/fearless/Common/Storage/Migration/KeystoreMigrator.swift +++ b/fearless/Common/Storage/Migration/KeystoreMigrator.swift @@ -1,43 +1,208 @@ import Foundation +import CryptoKit +import IrohaCrypto import SoraKeystore protocol KeystoreMigrating: AnyObject { func switchVersion() throws - func fetchKey(for identifier: String) -> Data? + func registerLegacyAccountRow() throws + func throwIfResourceLimitExceeded() throws + func fetchKey(for identifier: String) throws -> Data? func deleteKey(for identifier: String) func save(key: Data, for identifier: String) + func prepare() throws func finalize() throws } -enum KeystoreMigratingError: Error { +extension KeystoreMigrating { + func registerLegacyAccountRow() throws {} + func throwIfResourceLimitExceeded() throws {} +} + +enum KeystoreMigrationResource: String { + case legacyAccountRows + case stagedKeyCount + case identifierUTF8Bytes + case stagedKeyBytes + case deletionCount + case cleanupJournalBytes +} + +struct KeystoreMigrationResourceLimits { + // A v1 row can produce at most seven v2 keys and four legacy deletions. + // The byte ceilings remain intentionally independent so a small number of + // adversarially large keychain values or identifiers still fail closed. + static let production = KeystoreMigrationResourceLimits( + maximumLegacyAccountRows: 1024, + maximumStagedKeyCount: 7168, + maximumIdentifierUTF8Bytes: 2 * 1024 * 1024, + maximumStagedKeyBytes: 8 * 1024 * 1024, + maximumDeletionCount: 4096, + maximumCleanupJournalBytes: 2 * 1024 * 1024 + ) + + let maximumLegacyAccountRows: Int + let maximumStagedKeyCount: Int + let maximumIdentifierUTF8Bytes: Int + let maximumStagedKeyBytes: Int + let maximumDeletionCount: Int + let maximumCleanupJournalBytes: Int + + init( + maximumLegacyAccountRows: Int, + maximumStagedKeyCount: Int, + maximumIdentifierUTF8Bytes: Int, + maximumStagedKeyBytes: Int, + maximumDeletionCount: Int, + maximumCleanupJournalBytes: Int + ) { + precondition(maximumLegacyAccountRows >= 0) + precondition(maximumStagedKeyCount >= 0) + precondition(maximumIdentifierUTF8Bytes >= 0) + precondition(maximumStagedKeyBytes >= 0) + precondition(maximumDeletionCount >= 0) + precondition(maximumCleanupJournalBytes >= 0) + + self.maximumLegacyAccountRows = maximumLegacyAccountRows + self.maximumStagedKeyCount = maximumStagedKeyCount + self.maximumIdentifierUTF8Bytes = maximumIdentifierUTF8Bytes + self.maximumStagedKeyBytes = maximumStagedKeyBytes + self.maximumDeletionCount = maximumDeletionCount + self.maximumCleanupJournalBytes = maximumCleanupJournalBytes + } +} + +enum KeystoreMigratingError: LocalizedError { case nextVersionMissing case destinationNotReached + case notPrepared + case resourceLimitExceeded( + resource: KeystoreMigrationResource, + maximum: Int + ) + case stagedKeyConflict(String) + case stagedKeyVerificationFailed(String) + case cleanupJournalVerificationFailed + case invalidCleanupJournalVersion(String) + case invalidCleanupJournal(String) + case cleanupDestinationKeyMissing(String) + case cleanupDestinationKeyVerificationFailed(String) + + var errorDescription: String? { + switch self { + case .nextVersionMissing: + return "The next user-storage version is unavailable" + case .destinationNotReached: + return "The user-storage migration did not reach its destination version" + case .notPrepared: + return "Replacement keys were not staged before user-storage cleanup" + case let .resourceLimitExceeded(resource, maximum): + return """ + The user-storage key migration exceeded the \(resource.rawValue) \ + safety limit of \(maximum) + """ + case let .stagedKeyConflict(identifier): + return "Refusing to overwrite an unrelated keychain item at \(identifier)" + case let .stagedKeyVerificationFailed(identifier): + return "Unable to verify staged keychain item at \(identifier)" + case .cleanupJournalVerificationFailed: + return "Unable to verify the user-storage key cleanup journal" + case let .invalidCleanupJournalVersion(version): + return "The user-storage key cleanup journal contains an invalid version: \(version)" + case let .invalidCleanupJournal(reason): + return "The user-storage key cleanup journal is invalid: \(reason)" + case let .cleanupDestinationKeyMissing(identifier): + return "The migrated keychain item required for cleanup is missing: \(identifier)" + case let .cleanupDestinationKeyVerificationFailed(identifier): + return "The migrated keychain item required for cleanup failed verification: \(identifier)" + } + } } class KeystoreMigrator { + static let pendingCleanupIdentifier = "io.fearless.user-storage-migration.pending-key-cleanup" + private static let cleanupJournalSchemaVersion = 1 + private static let legacyDeletionSuffixes = [ + "-secretKey", + "-entropy", + "-deriv", + "-seed" + ] + private static let destinationKeySuffixes = [ + "-substrateSecretKey", + "-ethereumSecretKey", + "-substrateSeed", + "-ethereumSeed", + "-substrateDeriv", + "-ethereumDeriv", + "-entropy" + ] + + private struct CleanupJournal: Codable { + let schemaVersion: Int + let sourceVersion: String + let destinationVersion: String + let identifiersToRemove: [String] + let stagedIdentifiers: [String] + let destinationKeyProofs: [DestinationKeyProof] + } + + private struct DestinationKeyProof: Codable { + let identifier: String + let sha256: Data + } + + private struct CleanupJournalEnvelope: Decodable { + let schemaVersion: Int? + let destinationKeyProofs: [DestinationKeyProof]? + } + + private struct LegacyCleanupJournal: Decodable { + let sourceVersion: String + let destinationVersion: String + let identifiersToRemove: [String] + let stagedIdentifiers: [String] + } + let sourceVersion: UserStorageVersion let destinationVersion: UserStorageVersion let keystore: KeystoreProtocol + let resourceLimits: KeystoreMigrationResourceLimits private(set) var currentVersion: UserStorageVersion private(set) var identifiersToRemoveOnFinalize: Set = [] private(set) var tempKeystore: [String: Data] = [:] + private(set) var isPrepared = false + private var identifiersStagedDuringPrepare = Set() + private var stagedKeyConflicts = Set() + private var migratedLegacyAccountRowCount = 0 + private var retainedIdentifierUTF8ByteCount = 0 + private var stagedKeyByteCount = 0 + // save/delete cannot throw. The first violation is retained and freezes + // every later mutation until the throwing migration boundary observes it. + private var resourceLimitError: KeystoreMigratingError? init( sourceVersion: UserStorageVersion, destinationVersion: UserStorageVersion, - keystore: KeystoreProtocol + keystore: KeystoreProtocol, + resourceLimits: KeystoreMigrationResourceLimits = .production ) { self.sourceVersion = sourceVersion self.destinationVersion = destinationVersion currentVersion = sourceVersion self.keystore = keystore + self.resourceLimits = resourceLimits } } extension KeystoreMigrator: KeystoreMigrating { func switchVersion() throws { + if let resourceLimitError { + throw resourceLimitError + } + guard let nextVersion = currentVersion.nextVersion() else { throw KeystoreMigratingError.nextVersionMissing } @@ -45,35 +210,871 @@ extension KeystoreMigrator: KeystoreMigrating { currentVersion = nextVersion } - func fetchKey(for identifier: String) -> Data? { + func registerLegacyAccountRow() throws { + if let resourceLimitError { + throw resourceLimitError + } + + guard + let nextCount = checkedAdding( + 1, + to: migratedLegacyAccountRowCount, + resource: .legacyAccountRows, + maximum: resourceLimits.maximumLegacyAccountRows + ) + else { + throw resourceLimitError ?? KeystoreMigratingError + .resourceLimitExceeded( + resource: .legacyAccountRows, + maximum: resourceLimits.maximumLegacyAccountRows + ) + } + + migratedLegacyAccountRowCount = nextCount + } + + func throwIfResourceLimitExceeded() throws { + if let resourceLimitError { + throw resourceLimitError + } + } + + func fetchKey(for identifier: String) throws -> Data? { + if let resourceLimitError { + throw resourceLimitError + } + if sourceVersion.nextVersion() == currentVersion { - return try? keystore.fetchKey(for: identifier) + do { + return try keystore.fetchKey(for: identifier) + } catch KeystoreError.noKeyFound { + return nil + } } else { return tempKeystore[identifier] } } func deleteKey(for identifier: String) { - tempKeystore[identifier] = nil + guard resourceLimitError == nil else { + return + } + let stagedKey = tempKeystore[identifier] + let isAlreadyScheduledForDeletion = + identifiersToRemoveOnFinalize.contains(identifier) + + guard !isAlreadyScheduledForDeletion else { + return + } + + guard + checkedAdding( + 1, + to: identifiersToRemoveOnFinalize.count, + resource: .deletionCount, + maximum: resourceLimits.maximumDeletionCount + ) != nil + else { + return + } + + if stagedKey == nil { + guard + checkedAdding( + identifier.utf8.count, + to: retainedIdentifierUTF8ByteCount, + resource: .identifierUTF8Bytes, + maximum: resourceLimits.maximumIdentifierUTF8Bytes + ) != nil + else { + return + } + } + + tempKeystore[identifier] = nil identifiersToRemoveOnFinalize.insert(identifier) + + if let stagedKey { + stagedKeyByteCount -= stagedKey.count + } else { + retainedIdentifierUTF8ByteCount += identifier.utf8.count + } } func save(key: Data, for identifier: String) { + guard resourceLimitError == nil else { + return + } + + if let stagedKey = tempKeystore[identifier], stagedKey != key { + stagedKeyConflicts.insert(identifier) + return + } + + if tempKeystore[identifier] != nil { + return + } + + guard + checkedAdding( + 1, + to: tempKeystore.count, + resource: .stagedKeyCount, + maximum: resourceLimits.maximumStagedKeyCount + ) != nil + else { + return + } + + let isMovingFromDeletion = + identifiersToRemoveOnFinalize.contains(identifier) + + if !isMovingFromDeletion { + guard + checkedAdding( + identifier.utf8.count, + to: retainedIdentifierUTF8ByteCount, + resource: .identifierUTF8Bytes, + maximum: resourceLimits.maximumIdentifierUTF8Bytes + ) != nil + else { + return + } + } + + guard + checkedAdding( + key.count, + to: stagedKeyByteCount, + resource: .stagedKeyBytes, + maximum: resourceLimits.maximumStagedKeyBytes + ) != nil + else { + return + } + tempKeystore[identifier] = key + identifiersToRemoveOnFinalize.remove(identifier) + stagedKeyByteCount += key.count + + if !isMovingFromDeletion { + retainedIdentifierUTF8ByteCount += identifier.utf8.count + } + } + + func prepare() throws { + if let resourceLimitError { + throw resourceLimitError + } + + guard currentVersion == destinationVersion else { + throw KeystoreMigratingError.destinationNotReached + } + + if let identifier = stagedKeyConflicts.sorted().first { + throw KeystoreMigratingError.stagedKeyConflict(identifier) + } + + guard !tempKeystore.isEmpty || !identifiersToRemoveOnFinalize.isEmpty else { + isPrepared = true + return + } + + var identifiersNeedingWrite = Set() + + for identifier in tempKeystore.keys.sorted() { + guard let key = tempKeystore[identifier] else { + continue + } + + if try keystore.checkKey(for: identifier) { + guard try keystore.fetchKey(for: identifier) == key else { + throw KeystoreMigratingError.stagedKeyConflict(identifier) + } + } else { + identifiersNeedingWrite.insert(identifier) + } + } + + identifiersStagedDuringPrepare.formUnion(identifiersNeedingWrite) + + let journal = CleanupJournal( + schemaVersion: Self.cleanupJournalSchemaVersion, + sourceVersion: sourceVersion.rawValue, + destinationVersion: destinationVersion.rawValue, + identifiersToRemove: identifiersToRemoveOnFinalize.sorted(), + stagedIdentifiers: identifiersStagedDuringPrepare.sorted(), + destinationKeyProofs: tempKeystore + .map { identifier, key in + DestinationKeyProof( + identifier: identifier, + sha256: Self.digest(for: key) + ) + } + .sorted { $0.identifier < $1.identifier } + ) + try Self.validate( + journal, + resourceLimits: resourceLimits + ) + + let journalData = try Self.encode(journal) + guard + journalData.count <= + resourceLimits.maximumCleanupJournalBytes + else { + let error = recordResourceLimitError( + resource: .cleanupJournalBytes, + maximum: resourceLimits.maximumCleanupJournalBytes + ) + throw error + } + + try keystore.saveKey(journalData, with: Self.pendingCleanupIdentifier) + + guard + try keystore.fetchKey(for: Self.pendingCleanupIdentifier) == journalData + else { + throw KeystoreMigratingError.cleanupJournalVerificationFailed + } + + for identifier in identifiersNeedingWrite.sorted() { + guard let key = tempKeystore[identifier] else { + continue + } + + try keystore.addKey(key, with: identifier) + } + + for identifier in tempKeystore.keys.sorted() { + guard + let expectedKey = tempKeystore[identifier], + try keystore.fetchKey(for: identifier) == expectedKey + else { + throw KeystoreMigratingError.stagedKeyVerificationFailed(identifier) + } + } + + isPrepared = true } func finalize() throws { + if let resourceLimitError { + throw resourceLimitError + } + guard currentVersion == destinationVersion else { throw KeystoreMigratingError.destinationNotReached } - try identifiersToRemoveOnFinalize.forEach { identifier in - try keystore.deleteKeyIfExists(for: identifier) + guard isPrepared else { + throw KeystoreMigratingError.notPrepared + } + + guard !tempKeystore.isEmpty || !identifiersToRemoveOnFinalize.isEmpty else { + return + } + + try Self.verifyDestinationKeys( + CleanupJournal( + schemaVersion: Self.cleanupJournalSchemaVersion, + sourceVersion: sourceVersion.rawValue, + destinationVersion: destinationVersion.rawValue, + identifiersToRemove: identifiersToRemoveOnFinalize.sorted(), + stagedIdentifiers: identifiersStagedDuringPrepare.sorted(), + destinationKeyProofs: tempKeystore + .map { identifier, key in + DestinationKeyProof( + identifier: identifier, + sha256: Self.digest(for: key) + ) + } + .sorted { $0.identifier < $1.identifier } + ).destinationKeyProofs, + in: keystore, + resourceLimits: resourceLimits + ) + + try Self.removeKeys( + identifiersToRemoveOnFinalize.sorted(), + from: keystore + ) + try Self.deleteKeyIfPresent( + Self.pendingCleanupIdentifier, + from: keystore + ) + } +} + +private extension KeystoreMigrator { + func checkedAdding( + _ increment: Int, + to current: Int, + resource: KeystoreMigrationResource, + maximum: Int + ) -> Int? { + let (result, overflow) = current.addingReportingOverflow(increment) + + guard !overflow, result <= maximum else { + _ = recordResourceLimitError( + resource: resource, + maximum: maximum + ) + + return nil + } + + return result + } + + func recordResourceLimitError( + resource: KeystoreMigrationResource, + maximum: Int + ) -> KeystoreMigratingError { + if let resourceLimitError { + return resourceLimitError + } + + let error = KeystoreMigratingError.resourceLimitExceeded( + resource: resource, + maximum: maximum + ) + resourceLimitError = error + + return error + } +} + +extension KeystoreMigrator { + static func recoverPendingCleanup( + keystore: KeystoreProtocol, + currentVersion: UserStorageVersion + ) throws { + let journalData: Data + + do { + journalData = try keystore.fetchKey(for: pendingCleanupIdentifier) + } catch KeystoreError.noKeyFound { + return + } + + guard + journalData.count <= + KeystoreMigrationResourceLimits.production + .maximumCleanupJournalBytes + else { + throw KeystoreMigratingError.resourceLimitExceeded( + resource: .cleanupJournalBytes, + maximum: KeystoreMigrationResourceLimits.production + .maximumCleanupJournalBytes + ) + } + + let decoder = JSONDecoder() + let envelope = try decoder.decode( + CleanupJournalEnvelope.self, + from: journalData + ) + + if envelope.schemaVersion == nil { + guard envelope.destinationKeyProofs == nil else { + throw KeystoreMigratingError.invalidCleanupJournal( + "a destination proof list has no schema version" + ) + } + + let legacyJournal = try decoder.decode( + LegacyCleanupJournal.self, + from: journalData + ) + try recoverLegacyCleanupJournal( + legacyJournal, + keystore: keystore, + currentVersion: currentVersion + ) + return + } + + let journal = try decoder.decode(CleanupJournal.self, from: journalData) + try validate( + journal, + resourceLimits: .production + ) + + guard let sourceVersion = UserStorageVersion(rawValue: journal.sourceVersion) else { + throw KeystoreMigratingError.invalidCleanupJournalVersion(journal.sourceVersion) + } + + guard let destinationVersion = UserStorageVersion(rawValue: journal.destinationVersion) else { + throw KeystoreMigratingError.invalidCleanupJournalVersion(journal.destinationVersion) + } + + if currentVersion.isSameOrNewer(than: destinationVersion) { + try verifyDestinationKeys( + journal.destinationKeyProofs, + in: keystore, + resourceLimits: .production + ) + try removeKeys(journal.identifiersToRemove, from: keystore) + try deleteKeyIfPresent(pendingCleanupIdentifier, from: keystore) + } else if currentVersion == sourceVersion { + try verifyAndRemoveRollbackKeys( + journal.stagedIdentifiers, + destinationKeyProofs: journal.destinationKeyProofs, + from: keystore, + resourceLimits: .production + ) + try deleteKeyIfPresent(pendingCleanupIdentifier, from: keystore) + } else { + throw KeystoreMigratingError.invalidCleanupJournal( + "the database version does not match the journal boundary" + ) + } + } + + private static func recoverLegacyCleanupJournal( + _ journal: LegacyCleanupJournal, + keystore: KeystoreProtocol, + currentVersion: UserStorageVersion + ) throws { + try validateResourceLimits( + identifiersToRemove: journal.identifiersToRemove, + stagedIdentifiers: journal.stagedIdentifiers, + destinationProofIdentifiers: journal.stagedIdentifiers, + resourceLimits: .production + ) + + let versions = try validateVersions( + sourceVersion: journal.sourceVersion, + destinationVersion: journal.destinationVersion + ) + let identifiersToRemove = Set(journal.identifiersToRemove) + let stagedIdentifiers = Set(journal.stagedIdentifiers) + + guard identifiersToRemove.count == journal.identifiersToRemove.count else { + throw KeystoreMigratingError.invalidCleanupJournal( + "duplicate legacy identifiers" + ) + } + + guard stagedIdentifiers.count == journal.stagedIdentifiers.count else { + throw KeystoreMigratingError.invalidCleanupJournal( + "duplicate staged identifiers" + ) + } + + let allIdentifiers = identifiersToRemove.union(stagedIdentifiers) + + guard !allIdentifiers.contains(where: \.isEmpty) else { + throw KeystoreMigratingError.invalidCleanupJournal( + "empty keychain identifier" + ) + } + + guard !allIdentifiers.contains(pendingCleanupIdentifier) else { + throw KeystoreMigratingError.invalidCleanupJournal( + "the cleanup journal identifier is reserved" + ) + } + + guard identifiersToRemove.isDisjoint(with: stagedIdentifiers) else { + throw KeystoreMigratingError.invalidCleanupJournal( + "a staged identifier appears in the legacy deletion list" + ) + } + + try validateLegacyDeletionIdentifiers(identifiersToRemove) + try validateDestinationIdentifiers(stagedIdentifiers) + + guard + currentVersion == versions.source || + currentVersion.isSameOrNewer(than: versions.destination) + else { + throw KeystoreMigratingError.invalidCleanupJournal( + "the database version does not match the journal boundary" + ) + } + + // Legacy journals contain no integrity proof. Clearing only the journal + // preserves every user and staged key while allowing a source store to + // rerun migration with the current verified two-phase protocol. + try deleteKeyIfPresent(pendingCleanupIdentifier, from: keystore) + } + + private static func validate( + _ journal: CleanupJournal, + resourceLimits: KeystoreMigrationResourceLimits + ) throws { + try validateResourceLimits( + identifiersToRemove: journal.identifiersToRemove, + stagedIdentifiers: journal.stagedIdentifiers, + destinationProofIdentifiers: + journal.destinationKeyProofs.map(\.identifier), + resourceLimits: resourceLimits + ) + + guard journal.schemaVersion == cleanupJournalSchemaVersion else { + throw KeystoreMigratingError.invalidCleanupJournal( + "unsupported schema version \(journal.schemaVersion)" + ) + } + + _ = try validateVersions( + sourceVersion: journal.sourceVersion, + destinationVersion: journal.destinationVersion + ) + + let identifiersToRemove = Set(journal.identifiersToRemove) + let stagedIdentifiers = Set(journal.stagedIdentifiers) + let destinationIdentifiers = Set(journal.destinationKeyProofs.map(\.identifier)) + + guard identifiersToRemove.count == journal.identifiersToRemove.count else { + throw KeystoreMigratingError.invalidCleanupJournal( + "duplicate legacy identifiers" + ) + } + + guard stagedIdentifiers.count == journal.stagedIdentifiers.count else { + throw KeystoreMigratingError.invalidCleanupJournal( + "duplicate staged identifiers" + ) + } + + guard destinationIdentifiers.count == journal.destinationKeyProofs.count else { + throw KeystoreMigratingError.invalidCleanupJournal( + "duplicate destination key proofs" + ) + } + + let allIdentifiers = identifiersToRemove + .union(stagedIdentifiers) + .union(destinationIdentifiers) + + guard !allIdentifiers.contains(where: \.isEmpty) else { + throw KeystoreMigratingError.invalidCleanupJournal( + "empty keychain identifier" + ) + } + + guard !allIdentifiers.contains(pendingCleanupIdentifier) else { + throw KeystoreMigratingError.invalidCleanupJournal( + "the cleanup journal identifier is reserved" + ) + } + + guard identifiersToRemove.isDisjoint(with: destinationIdentifiers) else { + throw KeystoreMigratingError.invalidCleanupJournal( + "a destination identifier appears in the legacy deletion list" + ) + } + + try validateLegacyDeletionIdentifiers(identifiersToRemove) + try validateDestinationIdentifiers(destinationIdentifiers) + + guard stagedIdentifiers.isSubset(of: destinationIdentifiers) else { + throw KeystoreMigratingError.invalidCleanupJournal( + "a staged identifier has no destination integrity proof" + ) } - for (identifier, key) in tempKeystore { - try keystore.saveKey(key, with: identifier) + guard identifiersToRemove.isEmpty || !destinationIdentifiers.isEmpty else { + throw KeystoreMigratingError.invalidCleanupJournal( + "legacy deletion has no destination integrity proof" + ) } + + guard journal.destinationKeyProofs.allSatisfy({ $0.sha256.count == SHA256.byteCount }) else { + throw KeystoreMigratingError.invalidCleanupJournal( + "a destination integrity proof has an invalid digest" + ) + } + } + + private static func validateVersions( + sourceVersion: String, + destinationVersion: String + ) throws -> ( + source: UserStorageVersion, + destination: UserStorageVersion + ) { + guard + let source = UserStorageVersion(rawValue: sourceVersion), + let destination = UserStorageVersion(rawValue: destinationVersion), + source != destination, + destination.isSameOrNewer(than: source) + else { + throw KeystoreMigratingError.invalidCleanupJournal( + "source and destination versions do not form a valid forward migration" + ) + } + + return (source, destination) + } + + private static func validateResourceLimits( + identifiersToRemove: [String], + stagedIdentifiers: [String], + destinationProofIdentifiers: [String], + resourceLimits: KeystoreMigrationResourceLimits + ) throws { + guard + identifiersToRemove.count <= + resourceLimits.maximumDeletionCount + else { + throw KeystoreMigratingError.resourceLimitExceeded( + resource: .deletionCount, + maximum: resourceLimits.maximumDeletionCount + ) + } + + guard + stagedIdentifiers.count <= + resourceLimits.maximumStagedKeyCount, + destinationProofIdentifiers.count <= + resourceLimits.maximumStagedKeyCount + else { + throw KeystoreMigratingError.resourceLimitExceeded( + resource: .stagedKeyCount, + maximum: resourceLimits.maximumStagedKeyCount + ) + } + + let uniqueIdentifiers = Set( + identifiersToRemove + + stagedIdentifiers + + destinationProofIdentifiers + ) + var aggregateIdentifierBytes = 0 + + for identifier in uniqueIdentifiers { + let (nextCount, overflow) = + aggregateIdentifierBytes.addingReportingOverflow( + identifier.utf8.count + ) + + guard + !overflow, + nextCount <= + resourceLimits.maximumIdentifierUTF8Bytes + else { + throw KeystoreMigratingError.resourceLimitExceeded( + resource: .identifierUTF8Bytes, + maximum: resourceLimits + .maximumIdentifierUTF8Bytes + ) + } + + aggregateIdentifierBytes = nextCount + } + } + + private static func encode(_ journal: CleanupJournal) throws -> Data { + let encoder = JSONEncoder() + encoder.outputFormatting = [.sortedKeys] + + return try encoder.encode(journal) + } + + private static func validateLegacyDeletionIdentifiers( + _ identifiers: Set + ) throws { + guard identifiers.allSatisfy(isLegacyDeletionIdentifier) else { + throw KeystoreMigratingError.invalidCleanupJournal( + "the legacy deletion list contains a non-migration keychain identifier" + ) + } + } + + private static func validateDestinationIdentifiers( + _ identifiers: Set + ) throws { + guard identifiers.allSatisfy(isDestinationMigrationIdentifier) else { + throw KeystoreMigratingError.invalidCleanupJournal( + "the destination list contains a non-migration keychain identifier" + ) + } + } + + private static func isLegacyDeletionIdentifier( + _ identifier: String + ) -> Bool { + guard + let suffix = legacyDeletionSuffixes.first( + where: identifier.hasSuffix + ), + identifier.count > suffix.count + else { + return false + } + + let address = String(identifier.dropLast(suffix.count)) + + do { + _ = try SS58AddressFactory().accountId(from: address) + return true + } catch { + return false + } + } + + private static func isDestinationMigrationIdentifier( + _ identifier: String + ) -> Bool { + guard + let suffix = destinationKeySuffixes.first( + where: identifier.hasSuffix + ), + identifier.count > suffix.count + else { + return false + } + + let metaId = String(identifier.dropLast(suffix.count)) + + return UUID(uuidString: metaId) != nil + } + + private static func verifyDestinationKeys( + _ proofs: [DestinationKeyProof], + in keystore: KeystoreProtocol, + resourceLimits: KeystoreMigrationResourceLimits + ) throws { + var aggregateKeyBytes = 0 + + for proof in proofs { + let key: Data + + do { + key = try keystore.fetchKey(for: proof.identifier) + } catch KeystoreError.noKeyFound { + throw KeystoreMigratingError.cleanupDestinationKeyMissing( + proof.identifier + ) + } + + aggregateKeyBytes = try checkedResourceTotal( + aggregateKeyBytes, + adding: key.count, + resource: .stagedKeyBytes, + maximum: resourceLimits.maximumStagedKeyBytes + ) + + guard digest(for: key) == proof.sha256 else { + throw KeystoreMigratingError.cleanupDestinationKeyVerificationFailed( + proof.identifier + ) + } + } + } + + private static func verifyAndRemoveRollbackKeys( + _ identifiers: [String], + destinationKeyProofs: [DestinationKeyProof], + from keystore: KeystoreProtocol, + resourceLimits: KeystoreMigrationResourceLimits + ) throws { + let proofsByIdentifier = Dictionary( + uniqueKeysWithValues: destinationKeyProofs.map { + ($0.identifier, $0) + } + ) + var identifiersPresent = [String]() + var aggregateKeyBytes = 0 + + for identifier in identifiers { + guard let proof = proofsByIdentifier[identifier] else { + throw KeystoreMigratingError.invalidCleanupJournal( + "a staged identifier has no destination integrity proof" + ) + } + + let key: Data + + do { + key = try keystore.fetchKey(for: identifier) + } catch KeystoreError.noKeyFound { + continue + } + + aggregateKeyBytes = try checkedResourceTotal( + aggregateKeyBytes, + adding: key.count, + resource: .stagedKeyBytes, + maximum: resourceLimits.maximumStagedKeyBytes + ) + + guard digest(for: key) == proof.sha256 else { + throw KeystoreMigratingError.cleanupDestinationKeyVerificationFailed( + identifier + ) + } + + identifiersPresent.append(identifier) + } + + try removeKeys(identifiersPresent, from: keystore) + } + + private static func checkedResourceTotal( + _ current: Int, + adding increment: Int, + resource: KeystoreMigrationResource, + maximum: Int + ) throws -> Int { + let (result, overflow) = current.addingReportingOverflow(increment) + + guard !overflow, result <= maximum else { + throw KeystoreMigratingError.resourceLimitExceeded( + resource: resource, + maximum: maximum + ) + } + + return result + } + + private static func digest(for key: Data) -> Data { + Data(SHA256.hash(data: key)) + } + + private static func removeKeys( + _ identifiers: [String], + from keystore: KeystoreProtocol + ) throws { + for identifier in identifiers { + try deleteKeyIfPresent(identifier, from: keystore) + } + } + + private static func deleteKeyIfPresent( + _ identifier: String, + from keystore: KeystoreProtocol + ) throws { + guard try keystore.checkKey(for: identifier) else { + return + } + + do { + try keystore.deleteKey(for: identifier) + } catch KeystoreError.noKeyFound { + // Another cleanup attempt already removed it. + } + } +} + +private extension UserStorageVersion { + func isSameOrNewer(than version: UserStorageVersion) -> Bool { + var candidate: UserStorageVersion? = version + + while let unwrappedCandidate = candidate { + if unwrappedCandidate == self { + return true + } + + candidate = unwrappedCandidate.nextVersion() + } + + return false } } diff --git a/fearless/Common/Storage/Migration/SettingsMigrator.swift b/fearless/Common/Storage/Migration/SettingsMigrator.swift index 5646ed4d6c..24e6b00c7f 100644 --- a/fearless/Common/Storage/Migration/SettingsMigrator.swift +++ b/fearless/Common/Storage/Migration/SettingsMigrator.swift @@ -6,10 +6,56 @@ protocol SettingsMigrating: AnyObject { func value(for key: String) -> Any? func remove(key: String) func set(value: Any, for key: String) + func prepare() throws func finalize() throws } +enum SettingsMigratingError: LocalizedError { + case destinationNotReached + case notPrepared + case unsupportedPendingValues + case cleanupJournalVerificationFailed + case invalidCleanupJournal + + var errorDescription: String? { + switch self { + case .destinationNotReached: + return "The settings migration did not reach its destination version" + case .notPrepared: + return "The settings cleanup was not prepared before database replacement" + case .unsupportedPendingValues: + return "The settings migration contains unsupported pending values" + case .cleanupJournalVerificationFailed: + return "Unable to verify the pending settings cleanup" + case .invalidCleanupJournal: + return "The pending settings cleanup is invalid" + } + } +} + class SettingsMigrator { + static let pendingCleanupKey = + "io.fearless.user-storage-migration.pending-settings-cleanup" + private static let cleanupJournalSchemaVersion = 1 + private static let maximumCleanupJournalBytes = 4096 + private static let cleanupJournalFields: Set = [ + "schemaVersion", + "sourceVersion", + "destinationVersion", + "keysToRemove" + ] + private static let allowedCleanupKeys: Set = [ + SettingsKey.selectedAccount.rawValue, + SettingsKey.selectedConnection.rawValue + ] + + private struct CleanupJournal: Codable { + let schemaVersion: Int + let sourceVersion: String + let destinationVersion: String + let keysToRemove: [String] + } + let sourceVersion: UserStorageVersion let destinationVersion: UserStorageVersion private(set) var settings: SettingsManagerProtocol @@ -18,6 +64,7 @@ class SettingsMigrator { private(set) var keysToRemoveOnFinalize: Set = [] private(set) var tempKeystore: [String: Any] = [:] + private(set) var isPrepared = false init( sourceVersion: UserStorageVersion, @@ -58,17 +105,221 @@ extension SettingsMigrator: SettingsMigrating { tempKeystore[key] = value } + func prepare() throws { + guard currentVersion == destinationVersion else { + throw SettingsMigratingError.destinationNotReached + } + + // No current user-storage mapping stages settings values. Refuse to + // cross the database replacement boundary if a future mapping starts + // doing so without extending the durable journal format first. + guard tempKeystore.isEmpty else { + throw SettingsMigratingError.unsupportedPendingValues + } + + guard !keysToRemoveOnFinalize.isEmpty else { + isPrepared = true + return + } + + let journal = CleanupJournal( + schemaVersion: Self.cleanupJournalSchemaVersion, + sourceVersion: sourceVersion.rawValue, + destinationVersion: destinationVersion.rawValue, + keysToRemove: keysToRemoveOnFinalize.sorted() + ) + try Self.validate(journal) + + let encoder = JSONEncoder() + encoder.outputFormatting = [.sortedKeys] + let data = try encoder.encode(journal) + settings.set(value: data, for: Self.pendingCleanupKey) + + guard settings.data(for: Self.pendingCleanupKey) == data else { + throw SettingsMigratingError + .cleanupJournalVerificationFailed + } + + isPrepared = true + } + func finalize() throws { guard currentVersion == destinationVersion else { - throw KeystoreMigratingError.destinationNotReached + throw SettingsMigratingError.destinationNotReached + } + + guard isPrepared else { + throw SettingsMigratingError.notPrepared + } + + guard !keysToRemoveOnFinalize.isEmpty else { + return + } + + guard settings.data(for: Self.pendingCleanupKey) != nil else { + throw SettingsMigratingError + .cleanupJournalVerificationFailed + } + + try Self.recoverPendingCleanup( + settings: settings, + currentVersion: destinationVersion + ) + } +} + +extension SettingsMigrator { + static func recoverPendingCleanup( + settings: SettingsManagerProtocol, + currentVersion: UserStorageVersion + ) throws { + guard let storedValue = settings.anyValue( + for: pendingCleanupKey + ) else { + return + } + + guard let data = storedValue as? Data else { + throw SettingsMigratingError.invalidCleanupJournal + } + try validateEncodedJournalShape(data) + + let journal: CleanupJournal + do { + journal = try JSONDecoder().decode( + CleanupJournal.self, + from: data + ) + } catch { + throw SettingsMigratingError.invalidCleanupJournal + } + + try validate(journal) + + guard + let sourceVersion = + UserStorageVersion(rawValue: journal.sourceVersion), + let destinationVersion = + UserStorageVersion(rawValue: journal.destinationVersion) + else { + throw SettingsMigratingError.invalidCleanupJournal } - keysToRemoveOnFinalize.forEach { key in - settings.removeValue(for: key) + if isVersion( + currentVersion, + sameOrNewerThan: destinationVersion + ) { + for key in journal.keysToRemove { + settings.removeValue(for: key) + } + + guard journal.keysToRemove.allSatisfy({ + settings.anyValue(for: $0) == nil + }) else { + throw SettingsMigratingError + .cleanupJournalVerificationFailed + } + + settings.removeValue(for: pendingCleanupKey) + } else if currentVersion == sourceVersion { + // Database replacement did not occur. Discard the cleanup intent; + // the next migration attempt will reconstruct it. + settings.removeValue(for: pendingCleanupKey) + } else { + throw SettingsMigratingError.invalidCleanupJournal + } + } + + private static func validate( + _ journal: CleanupJournal + ) throws { + guard + journal.schemaVersion == cleanupJournalSchemaVersion, + let sourceVersion = + UserStorageVersion(rawValue: journal.sourceVersion), + let destinationVersion = + UserStorageVersion(rawValue: journal.destinationVersion), + isVersion( + destinationVersion, + sameOrNewerThan: sourceVersion + ), + sourceVersion != destinationVersion, + Set(journal.keysToRemove).count == + journal.keysToRemove.count, + Set(journal.keysToRemove) == allowedCleanupKeys + else { + throw SettingsMigratingError.invalidCleanupJournal } + } - for (key, value) in tempKeystore { - settings.set(anyValue: value, for: key) + private static func validateEncodedJournalShape( + _ data: Data + ) throws { + guard + !data.isEmpty, + data.count <= maximumCleanupJournalBytes, + let object = try? JSONSerialization.jsonObject( + with: data, + options: [] + ) as? [String: Any], + Set(object.keys) == cleanupJournalFields, + object["schemaVersion"] is Int, + object["sourceVersion"] is String, + object["destinationVersion"] is String, + object["keysToRemove"] is [String] + else { + throw SettingsMigratingError.invalidCleanupJournal } + + // JSONSerialization accepts duplicate members and keeps one value. + // Require every exact ASCII field token once so a duplicate or escaped + // spelling can never smuggle an alternate cleanup request. + for field in cleanupJournalFields { + let token = Data("\"\(field)\"".utf8) + guard data.nonOverlappingOccurrenceCount(of: token) == 1 else { + throw SettingsMigratingError.invalidCleanupJournal + } + } + } + + private static func isVersion( + _ candidate: UserStorageVersion, + sameOrNewerThan version: UserStorageVersion + ) -> Bool { + var current: UserStorageVersion? = version + + while let currentVersion = current { + if currentVersion == candidate { + return true + } + + current = currentVersion.nextVersion() + } + + return false + } +} + +private extension Data { + func nonOverlappingOccurrenceCount(of token: Data) -> Int { + guard !token.isEmpty else { + return 0 + } + + var count = 0 + var searchStart = startIndex + + while + searchStart < endIndex, + let range = range( + of: token, + options: [], + in: searchStart ..< endIndex + ) { + count += 1 + searchStart = range.upperBound + } + + return count } } diff --git a/fearless/Common/Storage/Migration/SubstrateStorage/ChainModelV4MigrationPolicy.swift b/fearless/Common/Storage/Migration/SubstrateStorage/ChainModelV4MigrationPolicy.swift index 4e21586134..0ac9ca70bc 100644 --- a/fearless/Common/Storage/Migration/SubstrateStorage/ChainModelV4MigrationPolicy.swift +++ b/fearless/Common/Storage/Migration/SubstrateStorage/ChainModelV4MigrationPolicy.swift @@ -2,6 +2,20 @@ import Foundation import CoreData class ChainModelV4MigrationPolicy: NSEntityMigrationPolicy { + private enum Constants { + static let assetEntityName = "CDAsset" + static let assetsRelationshipName = "assets" + static let disabledAttributeName = "disabled" + static let legacyAssetRelationshipName = "asset" + static let wrapperMetadataKeys = [ + "isNative", + "isUtility", + "purchaseProviders", + "staking", + "type" + ] + } + override func createDestinationInstances( forSource chainModel: NSManagedObject, in mapping: NSEntityMapping, @@ -9,75 +23,106 @@ class ChainModelV4MigrationPolicy: NSEntityMigrationPolicy { ) throws { try super.createDestinationInstances(forSource: chainModel, in: mapping, manager: manager) - guard let updatedChainModel = manager.destinationInstances( + guard let destinationChain = manager.destinationInstances( forEntityMappingName: mapping.name, sourceInstances: [chainModel] ).first else { throw ConvenienceError(error: "Can't create destination instance") } - guard let chainAssetsModels = chainModel.value(forKey: "assets") as? Set else { - throw ConvenienceError(error: "No assets value") + destinationChain.setValue( + false, + forKey: Constants.disabledAttributeName + ) + } + + override func createRelationships( + forDestination destinationChain: NSManagedObject, + in mapping: NSEntityMapping, + manager: NSMigrationManager + ) throws { + try super.createRelationships( + forDestination: destinationChain, + in: mapping, + manager: manager + ) + + guard let sourceChain = manager.sourceInstances( + forEntityMappingName: mapping.name, + destinationInstances: [destinationChain] + ).first else { + return } - let assetModels: [NSManagedObject] = chainAssetsModels.compactMap { - guard - let assetModel = $0.value(forKey: "asset") as? NSManagedObject, - let id = assetModel.value(forKey: "id") as? String, - let symbol = assetModel.value(forKey: "symbol") as? String, - let precision = assetModel.value(forKey: "precision") as? UInt16 - else { - return nil + let legacyWrappers = + sourceChain.value(forKey: Constants.assetsRelationshipName) as? Set + ?? [] + guard !legacyWrappers.isEmpty else { + return + } + + guard + let assetMappingName = manager.mappingModel.entityMappings.first(where: { + $0.sourceEntityName == Constants.assetEntityName && + $0.destinationEntityName == Constants.assetEntityName + })?.name + else { + throw ConvenienceError(error: "Can't find the CDAsset entity mapping") + } + + let destinationAssets = destinationChain.mutableSetValue( + forKey: Constants.assetsRelationshipName + ) + + for wrapper in legacyWrappers { + guard let sourceAsset = wrapper.value( + forKey: Constants.legacyAssetRelationshipName + ) as? NSManagedObject else { + continue } - let icon = assetModel.value(forKey: "icon") as? URL - let priceId = assetModel.value(forKey: "priceId") as? String - let price = assetModel.value(forKey: "price") as? Decimal - let fiatDayChange = assetModel.value(forKey: "fiatDayChange") as? String - let currencyId = assetModel.value(forKey: "currencyId") as? String - let existentialDeposit = assetModel.value(forKey: "existentialDeposit") as? String - let color = assetModel.value(forKey: "color") as? String - - let isNative: Bool = ($0.value(forKey: "isNative") as? Bool) ?? false - let isUtility: Bool = ($0.value(forKey: "isUtility") as? Bool) ?? false - let purchaseProviders: [String]? = $0.value(forKey: "purchaseProviders") as? [String] - let staking: String? = $0.value(forKey: "staking") as? String - let type: String = ($0.value(forKey: "type") as? String) ?? "normal" - - let updatedAssetModel = NSEntityDescription.insertNewObject( - forEntityName: "CDAsset", - into: manager.destinationContext - ) - updatedAssetModel.setValue(id, forKey: "id") - updatedAssetModel.setValue(symbol, forKey: "symbol") - updatedAssetModel.setValue(precision, forKey: "precision") - updatedAssetModel.setValue(icon, forKey: "icon") - updatedAssetModel.setValue(priceId, forKey: "priceId") - updatedAssetModel.setValue(price, forKey: "price") - updatedAssetModel.setValue(fiatDayChange, forKey: "fiatDayChange") - updatedAssetModel.setValue(currencyId, forKey: "currencyId") - updatedAssetModel.setValue(existentialDeposit, forKey: "existentialDeposit") - updatedAssetModel.setValue(color, forKey: "color") - updatedAssetModel.setValue(isNative, forKey: "isNative") - updatedAssetModel.setValue(isUtility, forKey: "isUtility") - updatedAssetModel.setValue(purchaseProviders, forKey: "purchaseProviders") - updatedAssetModel.setValue(staking, forKey: "staking") - updatedAssetModel.setValue(type, forKey: "type") - - return updatedAssetModel + guard let destinationAsset = manager.destinationInstances( + forEntityMappingName: assetMappingName, + sourceInstances: [sourceAsset] + ).first else { + throw ConvenienceError( + error: "Can't find migrated CDAsset destination instance" + ) + } + + copyWrapperMetadata( + from: wrapper, + to: destinationAsset + ) + destinationAssets.add(destinationAsset) } - let assetsSet = Set(assetModels) as NSSet + } - let updatedAssetsSet = updatedChainModel.mutableSetValue(forKey: "assets") - updatedAssetsSet.addObjects(from: assetsSet.allObjects) + private func copyWrapperMetadata( + from wrapper: NSManagedObject, + to destinationAsset: NSManagedObject + ) { + for key in Constants.wrapperMetadataKeys { + let value: Any? + if key == "purchaseProviders" { + value = try? SafeTransformableValueReader.read( + from: wrapper, + key: key, + as: NSArray.self + ) + } else { + value = wrapper.value(forKey: key) + } - updatedChainModel.setValue(updatedAssetsSet, forKey: "assets") - updatedChainModel.setValue(false, forKey: "disabled") + guard let value else { + continue + } - manager.associate( - sourceInstance: chainModel, - withDestinationInstance: updatedChainModel, - for: mapping - ) + destinationAsset.setValue(value, forKey: key) + } + + if destinationAsset.value(forKey: "type") == nil { + destinationAsset.setValue("normal", forKey: "type") + } } } diff --git a/fearless/Common/Storage/Migration/SubstrateStorage/ChainSubstrateV2MigrationPolicy.swift b/fearless/Common/Storage/Migration/SubstrateStorage/ChainSubstrateV2MigrationPolicy.swift index 944b5d320e..ee0977ab00 100644 --- a/fearless/Common/Storage/Migration/SubstrateStorage/ChainSubstrateV2MigrationPolicy.swift +++ b/fearless/Common/Storage/Migration/SubstrateStorage/ChainSubstrateV2MigrationPolicy.swift @@ -10,11 +10,34 @@ class ChainSubstrateV2MigrationPolicy: NSEntityMigrationPolicy { ) throws { try super.createRelationships(forDestination: chainModel, in: mapping, manager: manager) - guard let nodes = chainModel.value(forKey: "nodes") as? Set, - let node = nodes.first else { + guard chainModel.value(forKey: "selectedNode") == nil, + let nodes = chainModel.value(forKey: "nodes") as? Set, + let node = nodes.min(by: isOrderedBefore) else { return } chainModel.setValue(node, forKey: "selectedNode") } + + private func isOrderedBefore( + _ lhs: NSManagedObject, + _ rhs: NSManagedObject + ) -> Bool { + let lhsURL = (lhs.value(forKey: "url") as? URL)?.absoluteString ?? "" + let rhsURL = (rhs.value(forKey: "url") as? URL)?.absoluteString ?? "" + + if lhsURL != rhsURL { + return lhsURL < rhsURL + } + + let lhsName = lhs.value(forKey: "name") as? String ?? "" + let rhsName = rhs.value(forKey: "name") as? String ?? "" + + if lhsName != rhsName { + return lhsName < rhsName + } + + return lhs.objectID.uriRepresentation().absoluteString < + rhs.objectID.uriRepresentation().absoluteString + } } diff --git a/fearless/Common/Storage/Migration/SubstrateStorage/SubstrateStorageMigrator+Sync.swift b/fearless/Common/Storage/Migration/SubstrateStorage/SubstrateStorageMigrator+Sync.swift index 1a7a8e47e6..f86ed03af1 100644 --- a/fearless/Common/Storage/Migration/SubstrateStorage/SubstrateStorageMigrator+Sync.swift +++ b/fearless/Common/Storage/Migration/SubstrateStorage/SubstrateStorageMigrator+Sync.swift @@ -2,12 +2,17 @@ import Foundation extension SubstrateStorageMigrator: Migrating { func migrate() throws { - guard requiresMigration() else { - return - } + let recoveryURL = try performMigrationWithRecovery() - performMigration() + if let recoveryURL { + Logger.shared.warning( + """ + Recovered from a Substrate cache migration error by quarantining a verified \ + cache-only store at \(recoveryURL.path) + """ + ) + } - Logger.shared.info("Db migration completed") + Logger.shared.info("Substrate DB migration check completed") } } diff --git a/fearless/Common/Storage/Migration/SubstrateStorage/SubstrateStorageMigrator.swift b/fearless/Common/Storage/Migration/SubstrateStorage/SubstrateStorageMigrator.swift index 5b2b806c9f..7c30ebe44b 100644 --- a/fearless/Common/Storage/Migration/SubstrateStorage/SubstrateStorageMigrator.swift +++ b/fearless/Common/Storage/Migration/SubstrateStorage/SubstrateStorageMigrator.swift @@ -1,150 +1,5914 @@ -import Foundation import CoreData -import SoraKeystore +import CryptoKit +import Darwin +import Foundation +import SQLite3 + +enum SubstrateStorageMigrationOperation: String { + case sourceMetadata = "source metadata inspection" + case sourceCheckpoint = "private source checkpoint" + case transformableSanitization = "private source transformable repair" + case sourceRowInspection = "private source row inspection" + case protectedDataInspection = "protected local-data inspection" + case stagedMigration = "staged migration" + case stagedValidation = "staged-store validation" +} + +enum SubstrateStorageMigrationError: LocalizedError { + case metadataUnreadable(URL, Error) + case unknownStoreVersion(URL) + case modelUnavailable(SubstrateStorageVersion) + case migrationPathUnavailable(SubstrateStorageVersion, SubstrateStorageVersion) + case mappingUnavailable(SubstrateStorageVersion, SubstrateStorageVersion, Error) + case sourceSnapshotFailed(URL, Error) + case walCheckpointFailed(URL, Error) + case transformableSanitizationFailed(SubstrateStorageVersion, Error) + case unsupportedTransformableAttribute(SubstrateStorageVersion, String, String) + case objectiveCException(SubstrateStorageMigrationOperation) + case temporaryDirectoryCreationFailed(URL, Error) + case temporaryStoreCleanupFailed(URL, Error) + case migrationStepFailed(SubstrateStorageVersion, SubstrateStorageVersion, Error) + case sourceStoreInspectionFailed(URL, Error) + case protectedDataInspectionRejected(URL, Error) + case stagedStoreMetadataUnreadable(URL, Error) + case stagedStoreInvalid(URL, SubstrateStorageVersion) + case stagedStoreInspectionFailed(URL, Error) + case stagedStoreRowCountMismatch(String, Int, Int) + case stagedStoreProtectedDataMismatch + case storeReplacementFailed(URL, Error) + case cacheRecoveryBlockedByProtectedData(URL, Error, [String: Int]) + case pendingCacheRecoveryFailed(URL, Error) + case cacheRecoveryFailed(URL, Error, Error) + + var errorDescription: String? { + switch self { + case let .metadataUnreadable(url, error): + return "Unable to read Substrate store metadata at \(url.path): \(error.localizedDescription)" + case let .unknownStoreVersion(url): + return "Unsupported Substrate store version at \(url.path)" + case let .modelUnavailable(version): + return "Substrate store model is unavailable for \(version.rawValue)" + case let .migrationPathUnavailable(source, destination): + return "No forward Substrate store migration path from \(source.rawValue) to \(destination.rawValue)" + case let .mappingUnavailable(source, destination, error): + return """ + Unable to create the Substrate store mapping from \(source.rawValue) \ + to \(destination.rawValue): \(error.localizedDescription) + """ + case let .sourceSnapshotFailed(url, error): + return """ + Unable to create a private Substrate store snapshot for \(url.path): \ + \(error.localizedDescription) + """ + case let .walCheckpointFailed(url, error): + return "Unable to checkpoint Substrate store at \(url.path): \(error.localizedDescription)" + case let .transformableSanitizationFailed(version, error): + return """ + Unable to safely repair cached values in the private \(version.rawValue) \ + Substrate store copy: \(error.localizedDescription) + """ + case let .unsupportedTransformableAttribute(version, entityName, attributeName): + return """ + The \(version.rawValue) Substrate model contains an unsupported cached value at \ + \(entityName).\(attributeName) + """ + case let .objectiveCException(operation): + return "Substrate storage safely stopped during \(operation.rawValue)" + case let .temporaryDirectoryCreationFailed(url, error): + return "Unable to create Substrate migration directory at \(url.path): \(error.localizedDescription)" + case let .temporaryStoreCleanupFailed(url, error): + return """ + Unable to remove a completed temporary Substrate migration store at \ + \(url.path): \(error.localizedDescription) + """ + case let .migrationStepFailed(source, destination, error): + return """ + Substrate store migration from \(source.rawValue) to \(destination.rawValue) \ + failed: \(error.localizedDescription) + """ + case let .sourceStoreInspectionFailed(url, error): + return "Unable to inspect Substrate store rows at \(url.path): \(error.localizedDescription)" + case let .protectedDataInspectionRejected(url, error): + return """ + Protected-data inspection rejected the store at \(url.path): \ + \(error.localizedDescription) + """ + case let .stagedStoreMetadataUnreadable(url, error): + return "Unable to read staged Substrate store metadata at \(url.path): \(error.localizedDescription)" + case let .stagedStoreInvalid(url, destination): + return "Staged Substrate store at \(url.path) is not compatible with \(destination.rawValue)" + case let .stagedStoreInspectionFailed(url, error): + return "Unable to inspect staged Substrate store rows at \(url.path): \(error.localizedDescription)" + case let .stagedStoreRowCountMismatch(entity, expected, actual): + return """ + Staged Substrate store changed the \(entity) row count from \(expected) to \(actual) + """ + case .stagedStoreProtectedDataMismatch: + return """ + Staged Substrate store changed protected local values or node relationships + """ + case let .storeReplacementFailed(url, error): + return "Unable to replace Substrate store at \(url.path): \(error.localizedDescription)" + case let .cacheRecoveryBlockedByProtectedData(url, migrationError, counts): + let protectedData = counts + .filter { $0.value > 0 } + .sorted { $0.key < $1.key } + .map { "\($0.key)=\($0.value)" } + .joined(separator: ", ") + + return """ + Refusing to rebuild the Substrate store at \(url.path) after \ + \(migrationError.localizedDescription) because it contains protected local data: \ + \(protectedData) + """ + case let .pendingCacheRecoveryFailed(url, error): + return """ + Unable to reconcile an interrupted Substrate cache recovery at \(url.path): \ + \(error.localizedDescription) + """ + case let .cacheRecoveryFailed(url, migrationError, recoveryError): + return """ + Unable to quarantine the rebuildable Substrate cache at \(url.path) after \ + \(migrationError.localizedDescription): \(recoveryError.localizedDescription) + """ + } + } + + var allowsCacheRebuild: Bool { + switch self { + case .modelUnavailable, + .migrationPathUnavailable, + .sourceSnapshotFailed, + .temporaryDirectoryCreationFailed, + .temporaryStoreCleanupFailed, + .metadataUnreadable, + .unknownStoreVersion, + .transformableSanitizationFailed, + .unsupportedTransformableAttribute, + .objectiveCException, + .sourceStoreInspectionFailed, + .protectedDataInspectionRejected, + .stagedStoreProtectedDataMismatch, + .storeReplacementFailed, + .cacheRecoveryBlockedByProtectedData, + .pendingCacheRecoveryFailed, + .cacheRecoveryFailed: + return false + case .mappingUnavailable, + .walCheckpointFailed, + .migrationStepFailed, + .stagedStoreMetadataUnreadable, + .stagedStoreInvalid, + .stagedStoreInspectionFailed, + .stagedStoreRowCountMismatch: + return true + } + } +} + +private enum SubstrateCustomMappingError: LocalizedError { + case resourceUnavailable(String) + case resourceUnreadable(String) + case incompatible(String) + case policyUnavailable(String, String) + + var errorDescription: String? { + switch self { + case let .resourceUnavailable(resourceName): + return "Required Substrate mapping \(resourceName).cdm is not bundled" + case let .resourceUnreadable(resourceName): + return "Required Substrate mapping \(resourceName).cdm cannot be loaded" + case let .incompatible(resourceName): + return "Required Substrate mapping \(resourceName).cdm does not match the bundled models" + case let .policyUnavailable(resourceName, policyClassName): + return """ + Required Substrate mapping \(resourceName).cdm does not use \ + \(policyClassName) for CDChain + """ + } + } +} + +enum SubstrateCacheRecoveryBoundary: Equatable { + case markerPersisted + case familyMemberMoved(String) + case legacyFamilyMemberRestored(String) + case markerRemoved + case retentionMarkerPersisted(String) + case retentionFamilyMemberRemoved(String, String) + case retentionMarkerRemoved(String) + case retentionArchiveRemoved(String) +} + +enum SubstrateCacheRecoveryInterruption: Error { + /// Test-only fault used to emulate process death without Swift unwinding. + case simulatedProcessDeath +} + +enum SubstrateCacheRecoveryTransactionError: LocalizedError { + case sourceStoreMissing + case unsafeFile(URL) + case unsafeDirectory(URL) + case invalidMarker + case markerTooLarge + case familyMismatch + case multiplePendingTransactions + case archiveScanLimitExceeded(Int) + case archiveByteScanLimitExceeded(UInt64) + case backupExclusionFailed(URL, Error) + case fileSynchronizationFailed(URL, Error) + case directorySynchronizationFailed(URL, Error) + + var errorDescription: String? { + switch self { + case .sourceStoreMissing: + return "The rebuildable Substrate cache store is missing" + case .unsafeFile: + return "The Substrate cache recovery transaction contains an unsafe file" + case .unsafeDirectory: + return "The Substrate cache recovery transaction contains an unsafe directory" + case .invalidMarker: + return "The Substrate cache recovery marker is invalid" + case .markerTooLarge: + return "The Substrate cache recovery marker is too large" + case .familyMismatch: + return "The Substrate cache family does not match its durable recovery manifest" + case .multiplePendingTransactions: + return "More than one Substrate cache recovery transaction is pending" + case let .archiveScanLimitExceeded(maximum): + return """ + Substrate cache recovery contains more than \(maximum) archive \ + directories and cannot be scanned safely + """ + case let .archiveByteScanLimitExceeded(maximum): + return """ + Substrate cache recovery exceeds the safe \(maximum)-byte archive \ + inspection budget + """ + case let .backupExclusionFailed(url, error): + return """ + Unable to exclude the Substrate cache recovery archive at \(url.path) \ + from device backup: \(error.localizedDescription) + """ + case let .fileSynchronizationFailed(_, error): + return "Unable to synchronize a Substrate cache recovery file: \(error.localizedDescription)" + case let .directorySynchronizationFailed(_, error): + return "Unable to synchronize a Substrate cache recovery directory: \(error.localizedDescription)" + } + } +} + +/// Moves a verified cache-only SQLite family into quarantine using a durable, +/// bounded transaction. A relaunch deterministically finishes any family move +/// or retention deletion interrupted after its marker became durable. +/// Completed archives are excluded from backup and the newest two are retained +/// subject to a combined 128 MiB byte budget. +final class CrashConsistentSubstrateCacheRecovery { + private enum Operation: String { + case quarantine + case restoreLegacy = "restore-legacy" + case deleteArchive = "delete-archive" + } + + private struct FamilyMember: Equatable { + let suffix: String + let byteCount: UInt64 + let sha256: String + } + + private struct Marker: Equatable { + let operation: Operation + let storePathSHA256: String + let family: [FamilyMember] + let restoreSuffixes: [String] + } + + private enum PathKind { + case missing + case regularFile + case directory + case symbolicLink + case other + } + + private struct FileIdentity: Hashable { + let deviceID: UInt64 + let inode: UInt64 + } + + private struct ArchiveCompletionTime: Equatable { + let seconds: Int64 + let nanoseconds: Int64 + } + + private struct CompletedArchive { + let url: URL + let family: [FamilyMember] + let byteCount: UInt64 + let completionTime: ArchiveCompletionTime + } + + private struct CompletedArchivePreflight { + let url: URL + let byteCount: UInt64 + let completionTime: ArchiveCompletionTime + } + + static let storeFamilySuffixes = ["", "-wal", "-shm", "-journal"] + + private static let markerHeader = "fearless-substrate-cache-recovery-v1" + private static let markerFileName = "recovery-transaction" + private static let markerTemporaryFileName = "recovery-transaction.pending" + private static let maximumMarkerByteCount: UInt64 = 4 * 1024 + private static let digestChunkByteCount = 1024 * 1024 + private static let recoveryRootName = "SubstrateStoreRecovery" + private static let restorationTemporarySuffix = + ".cache-recovery-restore.pending" + private static let maximumArchiveDirectoryScanCount = 64 + private static let maximumArchiveByteScanCount: UInt64 = + 512 * 1024 * 1024 + static let defaultRetainedArchiveCount = 2 + static let defaultRetainedArchiveByteCount: UInt64 = + 128 * 1024 * 1024 + + let storeURL: URL + let recoveryRootURL: URL + + private let fileManager: FileManager + private let boundaryHook: (SubstrateCacheRecoveryBoundary) throws -> Void + private let restorationTemporaryFileSynchronizationHook: + (URL) throws -> Void + private let directorySynchronizationHook: (URL) throws -> Void + private let backupExclusionHook: (URL) throws -> Void + private let retainedArchiveCount: Int + private let retainedArchiveByteCount: UInt64 + + private var databaseDirectoryURL: URL { + storeURL.deletingLastPathComponent() + } + + private var storePathSHA256: String { + Self.digest(data: Data(storeURL.path.utf8)) + } + + var hasPendingTransaction: Bool { + switch pathKind(at: recoveryRootURL) { + case .missing: + return false + case .directory: + break + case .regularFile, .symbolicLink, .other: + return true + } + + guard + let candidateURLs = try? + boundedRecoveryDirectoryURLs() + else { + return true + } + + return candidateURLs.contains { candidateURL in + guard pathKind(at: candidateURL) == .directory else { + return true + } + + if pathKind( + at: markerURL(in: candidateURL) + ) != .missing || + pathKind( + at: markerTemporaryURL(in: candidateURL) + ) != .missing { + return true + } + + guard + let contents = try? boundedImmediateChildURLs( + at: candidateURL, + maximumCount: Self.storeFamilySuffixes.count, + overflowError: + SubstrateCacheRecoveryTransactionError + .unsafeDirectory(candidateURL) + ) + else { + return true + } + return contents.isEmpty + } + } + + init( + storeURL: URL, + fileManager: FileManager, + boundaryHook: @escaping ( + SubstrateCacheRecoveryBoundary + ) throws -> Void = { _ in }, + restorationTemporaryFileSynchronizationHook: + @escaping (URL) throws -> Void = { _ in }, + directorySynchronizationHook: + @escaping (URL) throws -> Void = { _ in }, + backupExclusionHook: ((URL) throws -> Void)? = nil, + retainedArchiveCount: Int = + CrashConsistentSubstrateCacheRecovery + .defaultRetainedArchiveCount, + retainedArchiveByteCount: UInt64 = + CrashConsistentSubstrateCacheRecovery + .defaultRetainedArchiveByteCount + ) { + precondition(retainedArchiveCount >= 0) + + self.storeURL = storeURL.standardizedFileURL + self.fileManager = fileManager + self.boundaryHook = boundaryHook + self.restorationTemporaryFileSynchronizationHook = + restorationTemporaryFileSynchronizationHook + self.directorySynchronizationHook = + directorySynchronizationHook + self.backupExclusionHook = backupExclusionHook ?? { + url in + var resourceURL = url + var values = URLResourceValues() + values.isExcludedFromBackup = true + try resourceURL.setResourceValues(values) + } + self.retainedArchiveCount = retainedArchiveCount + self.retainedArchiveByteCount = retainedArchiveByteCount + recoveryRootURL = self.storeURL + .deletingLastPathComponent() + .appendingPathComponent( + Self.recoveryRootName, + isDirectory: true + ) + } + + func quarantine() throws -> URL? { + try requireSafeDatabaseDirectory() + guard try pendingTransactionDirectories().isEmpty else { + throw SubstrateCacheRecoveryTransactionError + .multiplePendingTransactions + } + try maintainCompletedArchives() + + let sourceFamily = try manifest( + for: storeURL, + requiringMainStore: false + ) + guard !sourceFamily.isEmpty else { + return nil + } + guard sourceFamily.first?.suffix.isEmpty == true else { + throw SubstrateCacheRecoveryTransactionError.sourceStoreMissing + } + + try createRecoveryRootIfNeeded() + + let transactionDirectoryURL = recoveryRootURL + .appendingPathComponent( + UUID().uuidString, + isDirectory: true + ) + var markerBecameDurable = false + + do { + try fileManager.createDirectory( + at: transactionDirectoryURL, + withIntermediateDirectories: false + ) + try synchronizeDirectory(recoveryRootURL) + try excludeFromBackup(transactionDirectoryURL) + + let marker = Marker( + operation: .quarantine, + storePathSHA256: storePathSHA256, + family: sourceFamily, + restoreSuffixes: [] + ) + try persist( + marker, + in: transactionDirectoryURL + ) + markerBecameDurable = true + try boundaryHook(.markerPersisted) + + try completeTransaction( + at: transactionDirectoryURL, + marker: marker + ) + try maintainCompletedArchives() + return pathKind(at: transactionDirectoryURL) == .directory + ? transactionDirectoryURL + : nil + } catch is SubstrateCacheRecoveryInterruption { + throw SubstrateCacheRecoveryInterruption + .simulatedProcessDeath + } catch { + if !markerBecameDurable { + try? removeUncommittedPreparation( + at: transactionDirectoryURL + ) + } + throw error + } + } + + @discardableResult + func reconcile() throws -> URL? { + try requireSafeDatabaseDirectory() + + switch pathKind(at: recoveryRootURL) { + case .missing: + return nil + case .directory: + break + case .regularFile, .symbolicLink, .other: + throw SubstrateCacheRecoveryTransactionError + .unsafeDirectory(recoveryRootURL) + } + try excludeFromBackup(recoveryRootURL) + + let directoryNames = try boundedRecoveryDirectoryURLs() + .map(\.lastPathComponent) + var pendingTransactions: [(URL, Marker)] = [] + var markerlessArchiveURLs: [URL] = [] + + for directoryName in directoryNames { + guard UUID(uuidString: directoryName) != nil else { + throw SubstrateCacheRecoveryTransactionError + .unsafeDirectory( + recoveryRootURL.appendingPathComponent( + directoryName + ) + ) + } + + let transactionDirectoryURL = recoveryRootURL + .appendingPathComponent( + directoryName, + isDirectory: true + ) + guard pathKind(at: transactionDirectoryURL) == .directory else { + throw SubstrateCacheRecoveryTransactionError + .unsafeDirectory(transactionDirectoryURL) + } + + let markerURL = markerURL( + in: transactionDirectoryURL + ) + let temporaryMarkerURL = markerTemporaryURL( + in: transactionDirectoryURL + ) + + switch pathKind(at: markerURL) { + case .regularFile: + guard pathKind(at: temporaryMarkerURL) == .missing else { + throw SubstrateCacheRecoveryTransactionError + .unsafeFile(temporaryMarkerURL) + } + pendingTransactions.append( + ( + transactionDirectoryURL, + try readMarker(at: markerURL) + ) + ) + case .missing: + switch pathKind(at: temporaryMarkerURL) { + case .missing: + let contents = try boundedImmediateChildURLs( + at: transactionDirectoryURL, + maximumCount: Self.storeFamilySuffixes.count, + overflowError: + SubstrateCacheRecoveryTransactionError + .unsafeDirectory( + transactionDirectoryURL + ) + ).map(\.lastPathComponent) + if contents.isEmpty { + try fileManager.removeItem( + at: transactionDirectoryURL + ) + try synchronizeDirectory(recoveryRootURL) + } else { + markerlessArchiveURLs.append( + transactionDirectoryURL + ) + } + case .regularFile: + let contents = Set( + try boundedImmediateChildURLs( + at: transactionDirectoryURL, + maximumCount: + Self.storeFamilySuffixes.count + 1, + overflowError: + SubstrateCacheRecoveryTransactionError + .unsafeDirectory( + transactionDirectoryURL + ) + ).map(\.lastPathComponent) + ) + if contents == [Self.markerTemporaryFileName] { + try removeUncommittedPreparation( + at: transactionDirectoryURL + ) + } else { + let allowedLegacyNames = Set( + Self.storeFamilySuffixes.map { + self.storeURL.lastPathComponent + $0 + } + [Self.markerTemporaryFileName] + ) + try requireExactTree( + at: transactionDirectoryURL, + allowedFileNames: allowedLegacyNames + ) + guard contents.contains( + storeURL.lastPathComponent + ) else { + throw SubstrateCacheRecoveryTransactionError + .familyMismatch + } + + try fileManager.removeItem( + at: temporaryMarkerURL + ) + try synchronizeDirectory( + transactionDirectoryURL + ) + markerlessArchiveURLs.append( + transactionDirectoryURL + ) + } + case .directory, .symbolicLink, .other: + throw SubstrateCacheRecoveryTransactionError + .unsafeFile(temporaryMarkerURL) + } + case .directory, .symbolicLink, .other: + throw SubstrateCacheRecoveryTransactionError + .unsafeFile(markerURL) + } + } + + guard pendingTransactions.count <= 1 else { + throw SubstrateCacheRecoveryTransactionError + .multiplePendingTransactions + } + + guard let pendingTransaction = pendingTransactions.first else { + let recoveredLegacyURL = + try reconcileLegacyInterruptedTransaction( + archiveURLs: markerlessArchiveURLs + ) + + try maintainCompletedArchives() + return recoveredLegacyURL.flatMap { + pathKind(at: $0) == .directory ? $0 : nil + } + } + + try excludeFromBackup(pendingTransaction.0) + let reconciledArchiveURL: URL? + switch pendingTransaction.1.operation { + case .quarantine: + try completeTransaction( + at: pendingTransaction.0, + marker: pendingTransaction.1 + ) + reconciledArchiveURL = pendingTransaction.0 + case .restoreLegacy: + try restoreLegacyTransaction( + at: pendingTransaction.0, + marker: pendingTransaction.1 + ) + reconciledArchiveURL = pendingTransaction.0 + case .deleteArchive: + try completeArchiveDeletion( + at: pendingTransaction.0, + marker: pendingTransaction.1 + ) + reconciledArchiveURL = nil + } + + try maintainCompletedArchives() + return reconciledArchiveURL.flatMap { + pathKind(at: $0) == .directory ? $0 : nil + } + } + + private func reconcileLegacyInterruptedTransaction( + archiveURLs: [URL] + ) throws -> URL? { + // A live main store means the app either still has its original + // family or already rebuilt the cache. Avoid hashing a potentially + // large healthy cache merely because historical archives remain. + switch pathKind(at: storeURL) { + case .regularFile: + return nil + case .missing: + break + case .directory, .symbolicLink, .other: + throw SubstrateCacheRecoveryTransactionError + .unsafeFile(storeURL) + } + + let liveFamily = try manifest( + for: storeURL, + requiringMainStore: false + ) + + // No live family is the fully completed legacy outcome. + guard !liveFamily.isEmpty else { + return nil + } + + var candidates: [(URL, Marker)] = [] + for archiveURL in archiveURLs { + if let marker = try legacyMarker( + archiveURL: archiveURL, + liveFamily: liveFamily + ) { + candidates.append((archiveURL, marker)) + } + } + + guard candidates.count <= 1 else { + throw SubstrateCacheRecoveryTransactionError + .multiplePendingTransactions + } + guard let candidate = candidates.first else { + throw SubstrateCacheRecoveryTransactionError.familyMismatch + } + + try persist(candidate.1, in: candidate.0) + try boundaryHook(.markerPersisted) + try restoreLegacyTransaction( + at: candidate.0, + marker: candidate.1 + ) + return candidate.0 + } + + private func legacyMarker( + archiveURL: URL, + liveFamily: [FamilyMember] + ) throws -> Marker? { + let archiveStoreURL = quarantineStoreURL(in: archiveURL) + let allowedNames = Set( + Self.storeFamilySuffixes.map { + storeURL.lastPathComponent + $0 + } + ) + let archiveNames = Set( + try boundedImmediateChildURLs( + at: archiveURL, + maximumCount: allowedNames.count, + overflowError: + SubstrateCacheRecoveryTransactionError + .unsafeDirectory(archiveURL) + ).map(\.lastPathComponent) + ) + guard archiveNames.isSubset(of: allowedNames) else { + return nil + } + + let archivedFamily = try manifest( + for: archiveStoreURL, + requiringMainStore: false + ) + guard archivedFamily.first?.suffix.isEmpty == true else { + return nil + } + + let archivedSuffixes = Set(archivedFamily.map(\.suffix)) + let liveSuffixes = Set(liveFamily.map(\.suffix)) + guard archivedSuffixes.isDisjoint(with: liveSuffixes) else { + return nil + } + + let unionFamily = Self.storeFamilySuffixes.compactMap { + suffix in + archivedFamily.first { $0.suffix == suffix } ?? + liveFamily.first { $0.suffix == suffix } + } + guard + Array(unionFamily.prefix(archivedFamily.count)) == + archivedFamily, + Array(unionFamily.dropFirst(archivedFamily.count)) == + liveFamily + else { + return nil + } + + var identities = Set() + for member in archivedFamily { + let values = try lstatValues( + at: familyURL( + storeURL: archiveStoreURL, + suffix: member.suffix + ) + ) + guard identities.insert( + FileIdentity( + deviceID: values.deviceID, + inode: values.inode + ) + ).inserted else { + throw SubstrateCacheRecoveryTransactionError + .unsafeFile(archiveStoreURL) + } + } + for member in liveFamily { + let liveURL = familyURL( + storeURL: storeURL, + suffix: member.suffix + ) + let values = try lstatValues(at: liveURL) + guard identities.insert( + FileIdentity( + deviceID: values.deviceID, + inode: values.inode + ) + ).inserted else { + throw SubstrateCacheRecoveryTransactionError + .unsafeFile(liveURL) + } + } + + return Marker( + operation: .restoreLegacy, + storePathSHA256: storePathSHA256, + family: unionFamily, + restoreSuffixes: archivedFamily.map(\.suffix) + ) + } + + private func restoreLegacyTransaction( + at transactionDirectoryURL: URL, + marker: Marker + ) throws { + guard + marker.operation == .restoreLegacy, + marker.storePathSHA256 == storePathSHA256, + !marker.restoreSuffixes.isEmpty, + marker.restoreSuffixes == + Array( + marker.family + .prefix(marker.restoreSuffixes.count) + .map(\.suffix) + ) + else { + throw SubstrateCacheRecoveryTransactionError.invalidMarker + } + + try requireExactLegacyRestoreTree( + at: transactionDirectoryURL, + marker: marker + ) + let restoreSuffixes = Set(marker.restoreSuffixes) + let archivedStoreURL = quarantineStoreURL( + in: transactionDirectoryURL + ) + let expectedArchivedFamily = marker.family.filter { + restoreSuffixes.contains($0.suffix) + } + guard + try manifest( + for: archivedStoreURL, + requiringMainStore: true + ) == expectedArchivedFamily + else { + throw SubstrateCacheRecoveryTransactionError.familyMismatch + } + + for member in marker.family { + let liveURL = familyURL( + storeURL: storeURL, + suffix: member.suffix + ) + + if restoreSuffixes.contains(member.suffix) { + let archivedURL = familyURL( + storeURL: archivedStoreURL, + suffix: member.suffix + ) + guard + try familyMember( + at: archivedURL, + suffix: member.suffix + ) == member + else { + throw SubstrateCacheRecoveryTransactionError + .familyMismatch + } + + switch pathKind(at: liveURL) { + case .missing: + try installRestoredMember( + member, + from: archivedURL, + to: liveURL, + transactionDirectoryURL: + transactionDirectoryURL + ) + try boundaryHook( + .legacyFamilyMemberRestored(member.suffix) + ) + case .regularFile: + guard + try familyMember( + at: liveURL, + suffix: member.suffix + ) == member + else { + throw SubstrateCacheRecoveryTransactionError + .familyMismatch + } + case .directory, .symbolicLink, .other: + throw SubstrateCacheRecoveryTransactionError + .unsafeFile(liveURL) + } + } else { + switch pathKind(at: liveURL) { + case .regularFile: + guard + try familyMember( + at: liveURL, + suffix: member.suffix + ) == member + else { + throw SubstrateCacheRecoveryTransactionError + .familyMismatch + } + case .missing: + throw SubstrateCacheRecoveryTransactionError + .familyMismatch + case .directory, .symbolicLink, .other: + throw SubstrateCacheRecoveryTransactionError + .unsafeFile(liveURL) + } + } + } + + guard + try manifest( + for: storeURL, + requiringMainStore: true + ) == marker.family + else { + throw SubstrateCacheRecoveryTransactionError.familyMismatch + } + + // A previous attempt may have completed its final rename and then + // failed while syncing either directory. Re-establish both directory + // durability obligations even when this retry had no rename to do. + try synchronizeDirectory(databaseDirectoryURL) + try synchronizeDirectory(transactionDirectoryURL) + + let markerURL = markerURL(in: transactionDirectoryURL) + guard pathKind(at: markerURL) == .regularFile else { + throw SubstrateCacheRecoveryTransactionError + .unsafeFile(markerURL) + } + try fileManager.removeItem(at: markerURL) + try synchronizeDirectory(transactionDirectoryURL) + try boundaryHook(.markerRemoved) + } + + private func installRestoredMember( + _ member: FamilyMember, + from archivedURL: URL, + to liveURL: URL, + transactionDirectoryURL: URL + ) throws { + let temporaryURL = try restorationTemporaryURL( + for: member.suffix, + in: transactionDirectoryURL + ) + + switch pathKind(at: temporaryURL) { + case .missing: + try fileManager.copyItem( + at: archivedURL, + to: temporaryURL + ) + guard + try familyMember( + at: temporaryURL, + suffix: member.suffix + ) == member + else { + throw SubstrateCacheRecoveryTransactionError + .familyMismatch + } + case .regularFile: + if try familyMember( + at: temporaryURL, + suffix: member.suffix + ) != member { + // The durable archive remains authoritative. A regular + // partial copy can be discarded and recreated after power + // loss; unsafe path types are never removed. + try fileManager.removeItem(at: temporaryURL) + try synchronizeDirectory(transactionDirectoryURL) + try fileManager.copyItem( + at: archivedURL, + to: temporaryURL + ) + guard + try familyMember( + at: temporaryURL, + suffix: member.suffix + ) == member + else { + throw SubstrateCacheRecoveryTransactionError + .familyMismatch + } + } + case .directory, .symbolicLink, .other: + throw SubstrateCacheRecoveryTransactionError + .unsafeFile(temporaryURL) + } + + try restorationTemporaryFileSynchronizationHook( + temporaryURL + ) + try synchronizeFile(temporaryURL) + try synchronizeDirectory(transactionDirectoryURL) + + guard pathKind(at: liveURL) == .missing else { + throw SubstrateCacheRecoveryTransactionError + .familyMismatch + } + try fileManager.moveItem(at: temporaryURL, to: liveURL) + guard + pathKind(at: temporaryURL) == .missing, + try familyMember( + at: liveURL, + suffix: member.suffix + ) == member + else { + throw SubstrateCacheRecoveryTransactionError.familyMismatch + } + try synchronizeDirectory(databaseDirectoryURL) + try synchronizeDirectory(transactionDirectoryURL) + } + + private func completeTransaction( + at transactionDirectoryURL: URL, + marker: Marker + ) throws { + guard + marker.operation == .quarantine, + marker.restoreSuffixes.isEmpty, + marker.storePathSHA256 == storePathSHA256 + else { + throw SubstrateCacheRecoveryTransactionError.invalidMarker + } + + try requireExactActiveTree( + at: transactionDirectoryURL, + marker: marker + ) + + for member in marker.family { + let sourceURL = familyURL( + storeURL: storeURL, + suffix: member.suffix + ) + let destinationURL = transactionDirectoryURL + .appendingPathComponent( + sourceURL.lastPathComponent + ) + + let sourceKind = pathKind(at: sourceURL) + let destinationKind = pathKind(at: destinationURL) + + switch (sourceKind, destinationKind) { + case (.regularFile, .missing): + guard try familyMember( + at: sourceURL, + suffix: member.suffix + ) == member else { + throw SubstrateCacheRecoveryTransactionError + .familyMismatch + } + + try fileManager.moveItem( + at: sourceURL, + to: destinationURL + ) + guard + pathKind(at: sourceURL) == .missing, + try familyMember( + at: destinationURL, + suffix: member.suffix + ) == member + else { + throw SubstrateCacheRecoveryTransactionError + .familyMismatch + } + + try synchronizeDirectory(transactionDirectoryURL) + try synchronizeDirectory(databaseDirectoryURL) + try boundaryHook( + .familyMemberMoved(member.suffix) + ) + case (.missing, .regularFile): + guard try familyMember( + at: destinationURL, + suffix: member.suffix + ) == member else { + throw SubstrateCacheRecoveryTransactionError + .familyMismatch + } + case (.missing, .missing), (.regularFile, .regularFile): + throw SubstrateCacheRecoveryTransactionError + .familyMismatch + case (.directory, _), (.symbolicLink, _), (.other, _): + throw SubstrateCacheRecoveryTransactionError + .unsafeFile(sourceURL) + case (_, .directory), (_, .symbolicLink), (_, .other): + throw SubstrateCacheRecoveryTransactionError + .unsafeFile(destinationURL) + } + } + + guard + try manifest( + for: quarantineStoreURL( + in: transactionDirectoryURL + ), + requiringMainStore: true + ) == marker.family + else { + throw SubstrateCacheRecoveryTransactionError.familyMismatch + } + + for suffix in Self.storeFamilySuffixes { + let sourceURL = familyURL( + storeURL: storeURL, + suffix: suffix + ) + guard pathKind(at: sourceURL) == .missing else { + throw SubstrateCacheRecoveryTransactionError + .familyMismatch + } + } + + // A previous attempt may have completed its final rename and then + // failed while syncing either directory. Re-establish both directory + // durability obligations even when this retry had no rename to do. + try synchronizeDirectory(transactionDirectoryURL) + try synchronizeDirectory(databaseDirectoryURL) + + let markerURL = markerURL(in: transactionDirectoryURL) + guard pathKind(at: markerURL) == .regularFile else { + throw SubstrateCacheRecoveryTransactionError + .unsafeFile(markerURL) + } + try fileManager.removeItem(at: markerURL) + try synchronizeDirectory(transactionDirectoryURL) + try boundaryHook(.markerRemoved) + } + + private func completeArchiveDeletion( + at transactionDirectoryURL: URL, + marker: Marker + ) throws { + guard + marker.operation == .deleteArchive, + marker.restoreSuffixes.isEmpty, + marker.storePathSHA256 == storePathSHA256 + else { + throw SubstrateCacheRecoveryTransactionError.invalidMarker + } + + try requireExactActiveTree( + at: transactionDirectoryURL, + marker: marker + ) + + let archiveName = transactionDirectoryURL.lastPathComponent + let archivedStoreURL = quarantineStoreURL( + in: transactionDirectoryURL + ) + + for member in marker.family { + let archivedURL = familyURL( + storeURL: archivedStoreURL, + suffix: member.suffix + ) + + switch pathKind(at: archivedURL) { + case .missing: + continue + case .regularFile: + guard + try familyMember( + at: archivedURL, + suffix: member.suffix + ) == member, + try lstatValues(at: archivedURL).linkCount == 1 + else { + throw SubstrateCacheRecoveryTransactionError + .familyMismatch + } + + try fileManager.removeItem(at: archivedURL) + try synchronizeDirectory(transactionDirectoryURL) + try boundaryHook( + .retentionFamilyMemberRemoved( + archiveName, + member.suffix + ) + ) + case .directory, .symbolicLink, .other: + throw SubstrateCacheRecoveryTransactionError + .unsafeFile(archivedURL) + } + } + + let markerURL = markerURL(in: transactionDirectoryURL) + guard + pathKind(at: markerURL) == .regularFile, + try fileManager.contentsOfDirectory( + atPath: transactionDirectoryURL.path + ) == [Self.markerFileName] + else { + throw SubstrateCacheRecoveryTransactionError + .familyMismatch + } + + try fileManager.removeItem(at: markerURL) + try synchronizeDirectory(transactionDirectoryURL) + try boundaryHook(.retentionMarkerRemoved(archiveName)) + + guard try fileManager.contentsOfDirectory( + atPath: transactionDirectoryURL.path + ).isEmpty else { + throw SubstrateCacheRecoveryTransactionError + .unsafeDirectory(transactionDirectoryURL) + } + try fileManager.removeItem(at: transactionDirectoryURL) + try synchronizeDirectory(recoveryRootURL) + try boundaryHook(.retentionArchiveRemoved(archiveName)) + } + + private func maintainCompletedArchives() throws { + switch pathKind(at: recoveryRootURL) { + case .missing: + return + case .directory: + break + case .regularFile, .symbolicLink, .other: + throw SubstrateCacheRecoveryTransactionError + .unsafeDirectory(recoveryRootURL) + } + + guard try pendingTransactionDirectories().isEmpty else { + throw SubstrateCacheRecoveryTransactionError + .multiplePendingTransactions + } + + let directoryNames = try boundedRecoveryDirectoryURLs() + .map(\.lastPathComponent) + var preflights: [CompletedArchivePreflight] = [] + var preflightByteCount: UInt64 = 0 + + for directoryName in directoryNames { + guard UUID(uuidString: directoryName) != nil else { + throw SubstrateCacheRecoveryTransactionError + .unsafeDirectory( + recoveryRootURL.appendingPathComponent( + directoryName + ) + ) + } + + let archiveURL = recoveryRootURL.appendingPathComponent( + directoryName, + isDirectory: true + ) + guard pathKind(at: archiveURL) == .directory else { + throw SubstrateCacheRecoveryTransactionError + .unsafeDirectory(archiveURL) + } + + let children = try boundedImmediateChildURLs( + at: archiveURL, + maximumCount: Self.storeFamilySuffixes.count, + overflowError: + SubstrateCacheRecoveryTransactionError + .unsafeDirectory(archiveURL) + ) + if children.isEmpty { + try fileManager.removeItem(at: archiveURL) + try synchronizeDirectory(recoveryRootURL) + continue + } + + let preflight = try completedArchivePreflight( + at: archiveURL + ) + let addition = preflightByteCount + .addingReportingOverflow(preflight.byteCount) + guard + !addition.overflow, + addition.partialValue <= + Self.maximumArchiveByteScanCount + else { + throw SubstrateCacheRecoveryTransactionError + .archiveByteScanLimitExceeded( + Self.maximumArchiveByteScanCount + ) + } + preflightByteCount = addition.partialValue + preflights.append(preflight) + } + + try excludeFromBackup(recoveryRootURL) + for preflight in preflights { + try excludeFromBackup(preflight.url) + } + + // Every candidate tree and its aggregate byte footprint is bounded + // before the first SHA-256 read or deletion authorization is written. + let archives = try preflights.map { + try completedArchive(after: $0) + } + + var retainedArchives = archives.sorted { + if $0.completionTime.seconds != + $1.completionTime.seconds { + return $0.completionTime.seconds < + $1.completionTime.seconds + } + if $0.completionTime.nanoseconds != + $1.completionTime.nanoseconds { + return $0.completionTime.nanoseconds < + $1.completionTime.nanoseconds + } + return $0.url.lastPathComponent < + $1.url.lastPathComponent + } + + var retainedByteCount: UInt64 = 0 + for archive in retainedArchives { + let addition = retainedByteCount.addingReportingOverflow( + archive.byteCount + ) + guard !addition.overflow else { + throw SubstrateCacheRecoveryTransactionError.familyMismatch + } + retainedByteCount = addition.partialValue + } + + while + retainedArchives.count > retainedArchiveCount || + retainedByteCount > retainedArchiveByteCount + { + let archive = retainedArchives.removeFirst() + retainedByteCount -= archive.byteCount + try deleteCompletedArchive(archive) + } + + try removeRecoveryRootIfEmpty() + } + + private func completedArchivePreflight( + at archiveURL: URL + ) throws -> CompletedArchivePreflight { + let allowedNames = Set( + Self.storeFamilySuffixes.map { + storeURL.lastPathComponent + $0 + } + ) + try requireExactTree( + at: archiveURL, + allowedFileNames: allowedNames + ) + + let childURLs = try boundedImmediateChildURLs( + at: archiveURL, + maximumCount: allowedNames.count, + overflowError: SubstrateCacheRecoveryTransactionError + .unsafeDirectory(archiveURL) + ) + let names = Set(childURLs.map(\.lastPathComponent)) + guard names.contains(storeURL.lastPathComponent) else { + throw SubstrateCacheRecoveryTransactionError + .sourceStoreMissing + } + + var identities = Set() + var byteCount: UInt64 = 0 + for childURL in childURLs { + let values = try lstatValues(at: childURL) + guard + values.linkCount == 1, + identities.insert( + FileIdentity( + deviceID: values.deviceID, + inode: values.inode + ) + ).inserted + else { + throw SubstrateCacheRecoveryTransactionError + .unsafeFile(childURL) + } + let addition = byteCount.addingReportingOverflow( + values.byteCount + ) + guard + !addition.overflow, + addition.partialValue <= + Self.maximumArchiveByteScanCount + else { + throw SubstrateCacheRecoveryTransactionError + .archiveByteScanLimitExceeded( + Self.maximumArchiveByteScanCount + ) + } + byteCount = addition.partialValue + } + + return CompletedArchivePreflight( + url: archiveURL, + byteCount: byteCount, + completionTime: try directoryCompletionTime( + at: archiveURL + ) + ) + } + + private func completedArchive( + after preflight: CompletedArchivePreflight + ) throws -> CompletedArchive { + let archiveURL = preflight.url + let family = try manifest( + for: quarantineStoreURL(in: archiveURL), + requiringMainStore: true + ) + let expectedNames = Set( + family.map { + storeURL.lastPathComponent + $0.suffix + } + ) + guard + Set( + try boundedImmediateChildURLs( + at: archiveURL, + maximumCount: Self.storeFamilySuffixes.count, + overflowError: + SubstrateCacheRecoveryTransactionError + .unsafeDirectory(archiveURL) + ).map(\.lastPathComponent) + ) == expectedNames + else { + throw SubstrateCacheRecoveryTransactionError.familyMismatch + } + + var byteCount: UInt64 = 0 + for member in family { + let memberURL = familyURL( + storeURL: quarantineStoreURL(in: archiveURL), + suffix: member.suffix + ) + guard try lstatValues(at: memberURL).linkCount == 1 else { + throw SubstrateCacheRecoveryTransactionError + .unsafeFile(memberURL) + } + let addition = byteCount.addingReportingOverflow( + member.byteCount + ) + guard !addition.overflow else { + throw SubstrateCacheRecoveryTransactionError.familyMismatch + } + byteCount = addition.partialValue + } + guard byteCount == preflight.byteCount else { + throw SubstrateCacheRecoveryTransactionError.familyMismatch + } + + return CompletedArchive( + url: archiveURL, + family: family, + byteCount: byteCount, + completionTime: preflight.completionTime + ) + } + + private func deleteCompletedArchive( + _ archive: CompletedArchive + ) throws { + let marker = Marker( + operation: .deleteArchive, + storePathSHA256: storePathSHA256, + family: archive.family, + restoreSuffixes: [] + ) + try persist(marker, in: archive.url) + try boundaryHook( + .retentionMarkerPersisted( + archive.url.lastPathComponent + ) + ) + try completeArchiveDeletion( + at: archive.url, + marker: marker + ) + } + + private func persist( + _ marker: Marker, + in transactionDirectoryURL: URL + ) throws { + let markerURL = markerURL(in: transactionDirectoryURL) + let temporaryMarkerURL = markerTemporaryURL( + in: transactionDirectoryURL + ) + guard + pathKind(at: markerURL) == .missing, + pathKind(at: temporaryMarkerURL) == .missing + else { + throw SubstrateCacheRecoveryTransactionError + .unsafeFile(markerURL) + } + + let data = try encode(marker) + guard data.count <= Self.maximumMarkerByteCount else { + throw SubstrateCacheRecoveryTransactionError.markerTooLarge + } + + try data.write( + to: temporaryMarkerURL, + options: .withoutOverwriting + ) + try synchronizeFile(temporaryMarkerURL) + try fileManager.moveItem( + at: temporaryMarkerURL, + to: markerURL + ) + try synchronizeDirectory(transactionDirectoryURL) + } + + private func readMarker(at markerURL: URL) throws -> Marker { + let values = try lstatValues(at: markerURL) + guard values.byteCount <= Self.maximumMarkerByteCount else { + throw SubstrateCacheRecoveryTransactionError.markerTooLarge + } + + let data = try Data(contentsOf: markerURL) + guard UInt64(data.count) == values.byteCount else { + throw SubstrateCacheRecoveryTransactionError.invalidMarker + } + + return try decodeMarker(data) + } + + private func encode(_ marker: Marker) throws -> Data { + let restoreSuffixes = marker.restoreSuffixes.isEmpty + ? "-" + : try marker.restoreSuffixes.map { + try token(for: $0) + }.joined( + separator: "," + ) + var lines = [ + Self.markerHeader, + "operation\t\(marker.operation.rawValue)", + "store-path-sha256\t\(marker.storePathSHA256)", + "restore-suffixes\t\(restoreSuffixes)" + ] + + for member in marker.family { + lines.append( + [ + try token(for: member.suffix), + String(member.byteCount), + member.sha256 + ].joined(separator: "\t") + ) + } + + return Data((lines.joined(separator: "\n") + "\n").utf8) + } + + private func decodeMarker(_ data: Data) throws -> Marker { + guard + let text = String(data: data, encoding: .utf8), + Data(text.utf8) == data + else { + throw SubstrateCacheRecoveryTransactionError.invalidMarker + } + + let lines = text.split( + separator: "\n", + omittingEmptySubsequences: false + ) + guard + lines.count >= 6, + lines.count <= 9, + lines.last?.isEmpty == true, + lines[0] == Self.markerHeader + else { + throw SubstrateCacheRecoveryTransactionError.invalidMarker + } + + let operationFields = lines[1].split( + separator: "\t", + omittingEmptySubsequences: false + ) + guard + operationFields.count == 2, + operationFields[0] == "operation", + let operation = Operation( + rawValue: String(operationFields[1]) + ) + else { + throw SubstrateCacheRecoveryTransactionError.invalidMarker + } + + let pathFields = lines[2].split( + separator: "\t", + omittingEmptySubsequences: false + ) + guard + pathFields.count == 2, + pathFields[0] == "store-path-sha256", + isLowercaseSHA256(String(pathFields[1])) + else { + throw SubstrateCacheRecoveryTransactionError.invalidMarker + } + + let restoreFields = lines[3].split( + separator: "\t", + omittingEmptySubsequences: false + ) + guard + restoreFields.count == 2, + restoreFields[0] == "restore-suffixes" + else { + throw SubstrateCacheRecoveryTransactionError.invalidMarker + } + let restoreSuffixes: [String] + if restoreFields[1] == "-" { + restoreSuffixes = [] + } else { + restoreSuffixes = try restoreFields[1] + .split( + separator: ",", + omittingEmptySubsequences: false + ).map { + guard let suffix = suffix(for: String($0)) else { + throw SubstrateCacheRecoveryTransactionError + .invalidMarker + } + return suffix + } + } + + var family: [FamilyMember] = [] + for line in lines.dropFirst(4).dropLast() { + let fields = line.split( + separator: "\t", + omittingEmptySubsequences: false + ) + guard + fields.count == 3, + let suffix = suffix(for: String(fields[0])), + let byteCount = UInt64(fields[1]), + String(byteCount) == fields[1], + isLowercaseSHA256(String(fields[2])) + else { + throw SubstrateCacheRecoveryTransactionError + .invalidMarker + } + + family.append( + FamilyMember( + suffix: suffix, + byteCount: byteCount, + sha256: String(fields[2]) + ) + ) + } + + let canonicalSuffixes = Self.storeFamilySuffixes.filter { + suffix in + family.contains { $0.suffix == suffix } + } + guard + !family.isEmpty, + family.first?.suffix.isEmpty == true, + family.map(\.suffix) == canonicalSuffixes, + Set(family.map(\.suffix)).count == family.count, + ( + operation == .quarantine && + restoreSuffixes.isEmpty + ) || ( + operation == .deleteArchive && + restoreSuffixes.isEmpty + ) || ( + operation == .restoreLegacy && + !restoreSuffixes.isEmpty && + restoreSuffixes == + Array( + family + .prefix(restoreSuffixes.count) + .map(\.suffix) + ) + ) + else { + throw SubstrateCacheRecoveryTransactionError.invalidMarker + } + + let marker = Marker( + operation: operation, + storePathSHA256: String(pathFields[1]), + family: family, + restoreSuffixes: restoreSuffixes + ) + guard try encode(marker) == data else { + throw SubstrateCacheRecoveryTransactionError.invalidMarker + } + + return marker + } + + private func requireExactActiveTree( + at transactionDirectoryURL: URL, + marker: Marker + ) throws { + let allowedNames = Set( + marker.family.map { + storeURL.lastPathComponent + $0.suffix + } + [Self.markerFileName] + ) + try requireExactTree( + at: transactionDirectoryURL, + allowedFileNames: allowedNames + ) + } + + private func requireExactLegacyRestoreTree( + at transactionDirectoryURL: URL, + marker: Marker + ) throws { + let archivedNames = marker.restoreSuffixes.map { + storeURL.lastPathComponent + $0 + } + let temporaryNames = try marker.restoreSuffixes.map { + try restorationTemporaryURL( + for: $0, + in: transactionDirectoryURL + ).lastPathComponent + } + let allowedNames = Set( + archivedNames + + temporaryNames + + [Self.markerFileName] + ) + try requireExactTree( + at: transactionDirectoryURL, + allowedFileNames: allowedNames + ) + } + + private func requireExactTree( + at directoryURL: URL, + allowedFileNames: Set + ) throws { + guard pathKind(at: directoryURL) == .directory else { + throw SubstrateCacheRecoveryTransactionError + .unsafeDirectory(directoryURL) + } + + let childURLs = try boundedImmediateChildURLs( + at: directoryURL, + maximumCount: allowedFileNames.count, + overflowError: SubstrateCacheRecoveryTransactionError + .unsafeDirectory(directoryURL) + ) + let names = Set(childURLs.map(\.lastPathComponent)) + guard names.isSubset(of: allowedFileNames) else { + throw SubstrateCacheRecoveryTransactionError + .unsafeDirectory(directoryURL) + } + + for url in childURLs { + guard pathKind(at: url) == .regularFile else { + throw SubstrateCacheRecoveryTransactionError + .unsafeFile(url) + } + } + } + + private func manifest( + for familyStoreURL: URL, + requiringMainStore: Bool + ) throws -> [FamilyMember] { + var members: [FamilyMember] = [] + var identities = Set() + + for suffix in Self.storeFamilySuffixes { + let url = familyURL( + storeURL: familyStoreURL, + suffix: suffix + ) + + switch pathKind(at: url) { + case .missing: + if suffix.isEmpty, requiringMainStore { + throw SubstrateCacheRecoveryTransactionError + .sourceStoreMissing + } + case .regularFile: + let values = try lstatValues(at: url) + guard identities.insert( + FileIdentity( + deviceID: values.deviceID, + inode: values.inode + ) + ).inserted else { + throw SubstrateCacheRecoveryTransactionError + .unsafeFile(url) + } + members.append( + try familyMember( + at: url, + suffix: suffix + ) + ) + case .directory, .symbolicLink, .other: + throw SubstrateCacheRecoveryTransactionError + .unsafeFile(url) + } + } + + if requiringMainStore { + guard members.first?.suffix.isEmpty == true else { + throw SubstrateCacheRecoveryTransactionError + .sourceStoreMissing + } + } + + return members + } + + private func familyMember( + at url: URL, + suffix: String + ) throws -> FamilyMember { + guard pathKind(at: url) == .regularFile else { + throw SubstrateCacheRecoveryTransactionError.unsafeFile(url) + } + let valuesBefore = try lstatValues(at: url) + let sha256 = try digest(at: url) + let valuesAfter = try lstatValues(at: url) + guard + valuesBefore.byteCount == valuesAfter.byteCount, + valuesBefore.deviceID == valuesAfter.deviceID, + valuesBefore.inode == valuesAfter.inode + else { + throw SubstrateCacheRecoveryTransactionError.familyMismatch + } + + return FamilyMember( + suffix: suffix, + byteCount: valuesAfter.byteCount, + sha256: sha256 + ) + } + + private func digest(at url: URL) throws -> String { + let handle = try FileHandle(forReadingFrom: url) + defer { + try? handle.close() + } + + var hasher = SHA256() + while true { + let data = try handle.read( + upToCount: Self.digestChunkByteCount + ) ?? Data() + guard !data.isEmpty else { + break + } + hasher.update(data: data) + } + + return hasher.finalize().map { + String(format: "%02x", $0) + }.joined() + } + + private static func digest(data: Data) -> String { + SHA256.hash(data: data).map { + String(format: "%02x", $0) + }.joined() + } + + private func token(for suffix: String) throws -> String { + switch suffix { + case "": + return "main" + case "-wal": + return "wal" + case "-shm": + return "shm" + case "-journal": + return "journal" + default: + throw SubstrateCacheRecoveryTransactionError.invalidMarker + } + } + + private func suffix(for token: String) -> String? { + switch token { + case "main": + return "" + case "wal": + return "-wal" + case "shm": + return "-shm" + case "journal": + return "-journal" + default: + return nil + } + } + + private func isLowercaseSHA256(_ value: String) -> Bool { + value.count == 64 && value.utf8.allSatisfy { + (0x30 ... 0x39).contains($0) || + (0x61 ... 0x66).contains($0) + } + } + + private func createRecoveryRootIfNeeded() throws { + switch pathKind(at: recoveryRootURL) { + case .missing: + try fileManager.createDirectory( + at: recoveryRootURL, + withIntermediateDirectories: false + ) + try synchronizeDirectory(databaseDirectoryURL) + case .directory: + break + case .regularFile, .symbolicLink, .other: + throw SubstrateCacheRecoveryTransactionError + .unsafeDirectory(recoveryRootURL) + } + + try excludeFromBackup(recoveryRootURL) + } + + private func removeUncommittedPreparation( + at transactionDirectoryURL: URL + ) throws { + guard pathKind(at: transactionDirectoryURL) == .directory else { + throw SubstrateCacheRecoveryTransactionError + .unsafeDirectory(transactionDirectoryURL) + } + + let names = try fileManager.contentsOfDirectory( + atPath: transactionDirectoryURL.path + ) + guard Set(names).isSubset( + of: [ + Self.markerFileName, + Self.markerTemporaryFileName + ] + ) else { + throw SubstrateCacheRecoveryTransactionError + .unsafeDirectory(transactionDirectoryURL) + } + + for name in names { + let url = transactionDirectoryURL.appendingPathComponent(name) + guard pathKind(at: url) == .regularFile else { + throw SubstrateCacheRecoveryTransactionError + .unsafeFile(url) + } + try fileManager.removeItem(at: url) + try synchronizeDirectory(transactionDirectoryURL) + } + + try fileManager.removeItem(at: transactionDirectoryURL) + try synchronizeDirectory(recoveryRootURL) + try removeRecoveryRootIfEmpty() + } + + private func removeRecoveryRootIfEmpty() throws { + guard pathKind(at: recoveryRootURL) == .directory else { + return + } + guard try fileManager.contentsOfDirectory( + atPath: recoveryRootURL.path + ).isEmpty else { + return + } + + try fileManager.removeItem(at: recoveryRootURL) + try synchronizeDirectory(databaseDirectoryURL) + } + + private func synchronizeFile(_ url: URL) throws { + do { + guard pathKind(at: url) == .regularFile else { + throw SubstrateCacheRecoveryTransactionError + .unsafeFile(url) + } + let handle = try FileHandle(forUpdating: url) + defer { + try? handle.close() + } + try handle.synchronize() + } catch { + throw SubstrateCacheRecoveryTransactionError + .fileSynchronizationFailed(url, error) + } + } + + private func synchronizeDirectory(_ url: URL) throws { + do { + guard pathKind(at: url) == .directory else { + throw SubstrateCacheRecoveryTransactionError + .unsafeDirectory(url) + } + + try directorySynchronizationHook(url) + + let descriptor = Darwin.open(url.path, O_RDONLY) + guard descriptor >= 0 else { + throw POSIXError( + POSIXErrorCode(rawValue: errno) ?? .EIO + ) + } + defer { + _ = Darwin.close(descriptor) + } + + guard Darwin.fsync(descriptor) == 0 else { + throw POSIXError( + POSIXErrorCode(rawValue: errno) ?? .EIO + ) + } + } catch { + throw SubstrateCacheRecoveryTransactionError + .directorySynchronizationFailed(url, error) + } + } + + private func excludeFromBackup(_ url: URL) throws { + do { + guard pathKind(at: url) == .directory else { + throw SubstrateCacheRecoveryTransactionError + .unsafeDirectory(url) + } + try backupExclusionHook(url) + } catch let transactionError + as SubstrateCacheRecoveryTransactionError { + throw transactionError + } catch { + throw SubstrateCacheRecoveryTransactionError + .backupExclusionFailed(url, error) + } + } + + private func directoryCompletionTime( + at url: URL + ) throws -> ArchiveCompletionTime { + var directoryStatus = stat() + guard + Darwin.lstat(url.path, &directoryStatus) == 0, + directoryStatus.st_mode & S_IFMT == S_IFDIR + else { + throw SubstrateCacheRecoveryTransactionError + .unsafeDirectory(url) + } + + return ArchiveCompletionTime( + seconds: Int64(directoryStatus.st_mtimespec.tv_sec), + nanoseconds: Int64(directoryStatus.st_mtimespec.tv_nsec) + ) + } + + private func requireSafeDatabaseDirectory() throws { + guard pathKind(at: databaseDirectoryURL) == .directory else { + throw SubstrateCacheRecoveryTransactionError + .unsafeDirectory(databaseDirectoryURL) + } + + switch pathKind(at: recoveryRootURL) { + case .missing, .directory: + return + case .regularFile, .symbolicLink, .other: + throw SubstrateCacheRecoveryTransactionError + .unsafeDirectory(recoveryRootURL) + } + } + + private func familyURL( + storeURL: URL, + suffix: String + ) -> URL { + URL(fileURLWithPath: storeURL.path + suffix) + } + + private func quarantineStoreURL( + in transactionDirectoryURL: URL + ) -> URL { + transactionDirectoryURL.appendingPathComponent( + storeURL.lastPathComponent + ) + } + + private func markerURL( + in transactionDirectoryURL: URL + ) -> URL { + transactionDirectoryURL.appendingPathComponent( + Self.markerFileName + ) + } + + private func markerTemporaryURL( + in transactionDirectoryURL: URL + ) -> URL { + transactionDirectoryURL.appendingPathComponent( + Self.markerTemporaryFileName + ) + } + + private func restorationTemporaryURL( + for suffix: String, + in transactionDirectoryURL: URL + ) throws -> URL { + transactionDirectoryURL.appendingPathComponent( + "\(try token(for: suffix))\(Self.restorationTemporarySuffix)" + ) + } + + private func boundedRecoveryDirectoryURLs() throws -> [URL] { + try boundedImmediateChildURLs( + at: recoveryRootURL, + maximumCount: Self.maximumArchiveDirectoryScanCount, + overflowError: SubstrateCacheRecoveryTransactionError + .archiveScanLimitExceeded( + Self.maximumArchiveDirectoryScanCount + ) + ) + } + + private func boundedImmediateChildURLs( + at directoryURL: URL, + maximumCount: Int, + overflowError: Error + ) throws -> [URL] { + guard pathKind(at: directoryURL) == .directory else { + throw SubstrateCacheRecoveryTransactionError + .unsafeDirectory(directoryURL) + } + + var enumerationFailed = false + guard + let enumerator = fileManager.enumerator( + at: directoryURL, + includingPropertiesForKeys: nil, + options: [.skipsSubdirectoryDescendants], + errorHandler: { _, _ in + enumerationFailed = true + return false + } + ) + else { + throw SubstrateCacheRecoveryTransactionError + .unsafeDirectory(directoryURL) + } + + var childURLs: [URL] = [] + while let childURL = enumerator.nextObject() as? URL { + childURLs.append(childURL) + guard childURLs.count <= maximumCount else { + throw overflowError + } + } + guard !enumerationFailed else { + throw SubstrateCacheRecoveryTransactionError + .unsafeDirectory(directoryURL) + } + + return childURLs.sorted { + $0.lastPathComponent < $1.lastPathComponent + } + } + + private func pendingTransactionDirectories() throws -> [URL] { + guard pathKind(at: recoveryRootURL) == .directory else { + return [] + } + + return try boundedRecoveryDirectoryURLs().filter { + candidateURL in + pathKind(at: markerURL(in: candidateURL)) != .missing || + pathKind( + at: markerTemporaryURL(in: candidateURL) + ) != .missing + } + } + + private func pathKind(at url: URL) -> PathKind { + var fileStatus = stat() + guard Darwin.lstat(url.path, &fileStatus) == 0 else { + return errno == ENOENT ? .missing : .other + } + + switch fileStatus.st_mode & S_IFMT { + case S_IFREG: + return .regularFile + case S_IFDIR: + return .directory + case S_IFLNK: + return .symbolicLink + default: + return .other + } + } + + private func lstatValues( + at url: URL + ) throws -> ( + byteCount: UInt64, + deviceID: UInt64, + inode: UInt64, + linkCount: UInt64 + ) { + var fileStatus = stat() + guard Darwin.lstat(url.path, &fileStatus) == 0 else { + throw POSIXError( + POSIXErrorCode(rawValue: errno) ?? .EIO + ) + } + guard + fileStatus.st_mode & S_IFMT == S_IFREG, + fileStatus.st_size >= 0 + else { + throw SubstrateCacheRecoveryTransactionError + .unsafeFile(url) + } + + return ( + byteCount: UInt64(fileStatus.st_size), + deviceID: UInt64( + truncatingIfNeeded: fileStatus.st_dev + ), + inode: UInt64( + truncatingIfNeeded: fileStatus.st_ino + ), + linkCount: UInt64( + truncatingIfNeeded: fileStatus.st_nlink + ) + ) + } +} + +struct SubstrateProtectedDataInspectionLimits { + static let production = SubstrateProtectedDataInspectionLimits( + fetchPageSize: 256, + maximumRowsPerEntity: 250_000, + maximumTotalRows: 1_000_000, + maximumStartupRootRowsPerEntity: 4096, + maximumStartupRootPayloadByteCount: 256 * 1024 * 1024, + maximumStartupChildValueByteCount: 256 * 1024, + maximumStartupChildPayloadByteCount: 256 * 1024 * 1024, + maximumRelationshipMembers: 10000, + maximumTotalRelationshipMembers: 1_000_000, + maximumTransformableElements: 4096, + maximumTransformableArchiveByteCount: 256 * 1024, + maximumValueByteCount: 256 * 1024, + maximumRecordByteCount: 1024 * 1024 + ) + + let fetchPageSize: Int + let maximumRowsPerEntity: Int + let maximumTotalRows: Int + let maximumStartupRootRowsPerEntity: Int + let maximumStartupRootPayloadByteCount: UInt64 + let maximumStartupChildValueByteCount: UInt64 + let maximumStartupChildPayloadByteCount: UInt64 + let maximumRelationshipMembers: Int + let maximumTotalRelationshipMembers: Int + let maximumTransformableElements: Int + let maximumTransformableArchiveByteCount: Int + let maximumValueByteCount: Int + let maximumRecordByteCount: Int + + init( + fetchPageSize: Int, + maximumRowsPerEntity: Int, + maximumTotalRows: Int, + maximumStartupRootRowsPerEntity: Int, + maximumStartupRootPayloadByteCount: UInt64, + maximumStartupChildValueByteCount: UInt64, + maximumStartupChildPayloadByteCount: UInt64, + maximumRelationshipMembers: Int, + maximumTotalRelationshipMembers: Int, + maximumTransformableElements: Int, + maximumTransformableArchiveByteCount: Int, + maximumValueByteCount: Int, + maximumRecordByteCount: Int + ) { + precondition(fetchPageSize > 0) + precondition(maximumRowsPerEntity >= 0) + precondition(maximumTotalRows >= 0) + precondition(maximumStartupRootRowsPerEntity >= 0) + precondition(maximumRelationshipMembers >= 0) + precondition(maximumTotalRelationshipMembers >= 0) + precondition(maximumTransformableElements >= 0) + precondition(maximumTransformableArchiveByteCount >= 0) + precondition(maximumValueByteCount >= 0) + precondition(maximumRecordByteCount >= 0) + + self.fetchPageSize = fetchPageSize + self.maximumRowsPerEntity = maximumRowsPerEntity + self.maximumTotalRows = maximumTotalRows + self.maximumStartupRootRowsPerEntity = + maximumStartupRootRowsPerEntity + self.maximumStartupRootPayloadByteCount = + maximumStartupRootPayloadByteCount + self.maximumStartupChildValueByteCount = + maximumStartupChildValueByteCount + self.maximumStartupChildPayloadByteCount = + maximumStartupChildPayloadByteCount + self.maximumRelationshipMembers = + maximumRelationshipMembers + self.maximumTotalRelationshipMembers = + maximumTotalRelationshipMembers + self.maximumTransformableElements = + maximumTransformableElements + self.maximumTransformableArchiveByteCount = + maximumTransformableArchiveByteCount + self.maximumValueByteCount = maximumValueByteCount + self.maximumRecordByteCount = maximumRecordByteCount + } +} + +enum SubstrateProtectedDataInspectionError: LocalizedError { + case entityUnavailable(String) + case relationshipUnavailable(String, String) + case countUnavailable(String) + case entityRowLimitExceeded(String, Int, Int) + case totalRowLimitExceeded(Int, Int) + case relationshipLimitExceeded(String, String, Int, Int) + case relationshipCountUnavailable(String, String) + case totalRelationshipLimitExceeded(Int, Int) + case valueByteLimitExceeded(String, String, Int, Int) + case recordByteLimitExceeded(String, Int, Int) + case relationshipValueInvalid(String, String) + case unsupportedAttributeValue(String, String, String) + case startupRootPayloadByteLimitExceeded(UInt64, UInt64) + case startupChildValueByteLimitExceeded( + String, + String, + UInt64, + UInt64 + ) + case startupChildPayloadByteLimitExceeded(UInt64, UInt64) + + var errorDescription: String? { + switch self { + case let .entityUnavailable(entityName): + return "Protected-data inspection requires the \(entityName) entity" + case let .relationshipUnavailable(entityName, relationshipName): + return """ + Protected-data inspection requires the \(entityName).\(relationshipName) relationship + """ + case let .countUnavailable(entityName): + return "Protected-data inspection could not count \(entityName)" + case let .entityRowLimitExceeded( + entityName, + actual, + maximum + ): + return """ + Protected-data inspection found \(actual) \(entityName) rows; \ + the deterministic safety limit is \(maximum) + """ + case let .totalRowLimitExceeded(actual, maximum): + return """ + Protected-data inspection found \(actual) total protected rows; \ + the deterministic safety limit is \(maximum) + """ + case let .relationshipLimitExceeded( + entityName, + relationshipName, + actual, + maximum + ): + return """ + Protected-data inspection found \(actual) members in \ + \(entityName).\(relationshipName); the deterministic safety limit \ + is \(maximum) + """ + case let .relationshipCountUnavailable( + entityName, + relationshipName + ): + return """ + Protected-data inspection could not count \ + \(entityName).\(relationshipName) without materializing it + """ + case let .totalRelationshipLimitExceeded(actual, maximum): + return """ + Protected-data inspection found \(actual) protected relationship \ + members; the deterministic safety limit is \(maximum) + """ + case let .valueByteLimitExceeded( + entityName, + attributeName, + actual, + maximum + ): + return """ + Protected-data inspection found a \(actual)-byte value at \ + \(entityName).\(attributeName); the deterministic safety limit is \ + \(maximum) bytes + """ + case let .recordByteLimitExceeded( + category, + actual, + maximum + ): + return """ + Protected-data inspection produced a \(actual)-byte \(category) \ + record; the deterministic safety limit is \(maximum) bytes + """ + case let .relationshipValueInvalid( + entityName, + relationshipName + ): + return """ + Protected-data inspection found an invalid relationship value at \ + \(entityName).\(relationshipName) + """ + case let .unsupportedAttributeValue( + entityName, + attributeName, + className + ): + return """ + Protected-data inspection cannot safely compare \(className) at \ + \(entityName).\(attributeName) + """ + case let .startupRootPayloadByteLimitExceeded( + actual, + maximum + ): + return """ + Startup root payloads use \(actual) bytes; the deterministic \ + safety limit is \(maximum) + """ + case let .startupChildValueByteLimitExceeded( + entityName, + attributeName, + actual, + maximum + ): + return """ + Startup child payload \(entityName).\(attributeName) uses \ + \(actual) bytes; the deterministic safety limit is \(maximum) + """ + case let .startupChildPayloadByteLimitExceeded( + actual, + maximum + ): + return """ + Startup child payloads use \(actual) bytes; the deterministic \ + safety limit is \(maximum) + """ + } + } +} + +private enum SubstrateLegacyXcmMappingError: LocalizedError { + case entityMappingUnavailable(String) + case sourceAssociationUnavailable(String) + case destinationRelationshipUnavailable(String, String) + case destinationEntityUnavailable(String) + + var errorDescription: String? { + switch self { + case let .entityMappingUnavailable(entityName): + return "Legacy XCM migration requires the \(entityName) entity mapping" + case let .sourceAssociationUnavailable(entityName): + return "Legacy XCM migration cannot find the source \(entityName) object" + case let .destinationRelationshipUnavailable(entityName, relationshipName): + return "Legacy XCM migration requires \(entityName).\(relationshipName)" + case let .destinationEntityUnavailable(entityName): + return "Legacy XCM migration requires the \(entityName) destination entity" + } + } +} + +private enum SubstrateTransformableRepairError: LocalizedError { + case countUnavailable(String) + case entityRowLimitExceeded(String, Int, Int) + case totalRowLimitExceeded(Int, Int) + case objectIDEnumerationMismatch(String) + case unexpectedBatchUpdateResult(String, String) + case repairedValueCountOverflow + + var errorDescription: String? { + switch self { + case let .countUnavailable(entityName): + return "Unable to count \(entityName) rows for private cache repair" + case let .entityRowLimitExceeded( + entityName, + actual, + maximum + ): + return """ + Private cache repair found \(actual) \(entityName) rows; \ + the deterministic safety limit is \(maximum) + """ + case let .totalRowLimitExceeded(actual, maximum): + return """ + Private cache repair found \(actual) total rows; \ + the deterministic safety limit is \(maximum) + """ + case let .objectIDEnumerationMismatch(entityName): + return "Unable to enumerate every \(entityName) row for private cache repair" + case let .unexpectedBatchUpdateResult(entityName, attributeName): + return """ + Private cache repair did not update exactly one \ + \(entityName).\(attributeName) row + """ + case .repairedValueCountOverflow: + return "Private cache repair count exceeded its bounded counter" + } + } +} + +final class SubstrateStorageMigrator { + private struct StartupToManyRelationshipFamily { + let entityName: String + let relationshipNames: [String] + } + + private static let intentionallyTransformedEntityNames: Set = [ + "CDChainAsset" + ] + + private static let storeFamilySuffixes = ["", "-wal", "-shm", "-journal"] + + // These collections are mapped eagerly during normal v8 startup. Audit + // their SQL counts on the disposable store before application mapping can + // materialize any unbounded relationship value. + private static let version8StartupRootEntityNames = [ + "CDChain", + "CDRuntimeMetadataItem" + ] + // ChainModelMapper traverses every one of these entities from CDChain + // during the first repository fetch. Keep the raw SQLite audit explicit: + // to-one children such as priceProvider and selected/custom nodes are not + // covered by the to-many relationship-count audit below. + private static let version8StartupChildEntityNames = [ + "CDAsset", + "CDChainNode", + "CDChainXcmConfig", + "CDExternalApi", + "CDPriceData", + "CDPriceProvider", + "CDXcmAvailableAsset", + "CDXcmAvailableDestination" + ] + private static let version8StartupToManyRelationshipFamilies = [ + StartupToManyRelationshipFamily( + entityName: "CDAsset", + relationshipNames: ["priceData"] + ), + StartupToManyRelationshipFamily( + entityName: "CDChain", + relationshipNames: [ + "assets", + "customNodes", + "explorers", + "nodes" + ] + ), + StartupToManyRelationshipFamily( + entityName: "CDChainXcmConfig", + relationshipNames: [ + "availableAssets", + "availableDestinations" + ] + ), + StartupToManyRelationshipFamily( + entityName: "CDXcmAvailableDestination", + relationshipNames: ["assets"] + ) + ] + + private static let resilientStringArrayTransformables: Set = [ + "CDAsset.purchaseProviders", + "CDChain.options", + "CDChainAsset.purchaseProviders", + "CDChainXcmConfig.availableAssets", + "CDExternalApi.types", + "CDPolkaswapRemoteSettings.availableSources", + "CDPolkaswapRemoteSettings.forceSmartIds", + "CDXcmAvailableDestination.assets" + ] + + private enum ProtectedDataCategory { + static let contacts = "CDContact" + static let contactItems = "CDContactItem" + static let transactionHistory = "CDTransactionHistoryItem" + static let customNodeRelationships = "CDChain.customNodes" + static let selectedNodeRelationships = "CDChain.selectedNode" + static let orphanNodes = "CDChainNode.orphan" + } + + private struct ProtectedDataFootprint { + let counts: [String: Int] + + var isEmpty: Bool { + counts.values.allSatisfy { $0 == 0 } + } + } + + private struct ProtectedDataDigest: Equatable { + let recordCount: UInt64 + let additiveWords: [UInt64] + } + + private struct ProtectedDataDigestAccumulator { + private(set) var recordCount: UInt64 = 0 + private var additiveWords = Array( + repeating: UInt64(0), + count: 8 + ) + + mutating func append( + _ record: String, + category: String, + maximumByteCount: Int + ) throws { + let recordData = Data(record.utf8) + guard recordData.count <= maximumByteCount else { + throw SubstrateProtectedDataInspectionError + .recordByteLimitExceeded( + category, + recordData.count, + maximumByteCount + ) + } + let domainData = Data(category.utf8) + for discriminator in UInt8(0) ... UInt8(1) { + var framedData = Data([discriminator]) + var domainLength = UInt64(domainData.count).bigEndian + withUnsafeBytes(of: &domainLength) { + framedData.append(contentsOf: $0) + } + framedData.append(domainData) + var recordLength = UInt64(recordData.count).bigEndian + withUnsafeBytes(of: &recordLength) { + framedData.append(contentsOf: $0) + } + framedData.append(recordData) + + let digest = Array(SHA256.hash(data: framedData)) + let wordOffset = Int(discriminator) * 4 + for wordIndex in 0 ..< 4 { + let byteOffset = wordIndex * 8 + var word: UInt64 = 0 + for byte in digest[ + byteOffset ..< byteOffset + 8 + ] { + word = (word << 8) | UInt64(byte) + } + additiveWords[wordOffset + wordIndex] &+= word + } + } + + recordCount += 1 + } + + func finalize() -> ProtectedDataDigest { + ProtectedDataDigest( + recordCount: recordCount, + additiveWords: additiveWords + ) + } + } + + private struct ProtectedDataSnapshot: Equatable { + let contacts: ProtectedDataDigest + let contactItems: ProtectedDataDigest + let transactionHistory: ProtectedDataDigest + let chainNodeTopologies: ProtectedDataDigest + let orphanNodes: ProtectedDataDigest + } + + private struct CompatibleStore { + let version: SubstrateStorageVersion + let model: NSManagedObjectModel + } + + private struct SourceSnapshot { + let rootURL: URL + let workingStoreURL: URL + } + + private struct PreparedSource { + let snapshot: SourceSnapshot + let compatibleStore: CompatibleStore + let repairedTransformableValueCount: Int + } + + private struct MigrationStep { + let sourceVersion: SubstrateStorageVersion + let destinationVersion: SubstrateStorageVersion + let sourceModel: NSManagedObjectModel + let destinationModel: NSManagedObjectModel + let mapping: NSMappingModel + } + + private struct RequiredCustomMapping { + let resourceName: String + let policyClassName: String + } + + let storeURL: URL + let modelDirectory: String + let fileManager: FileManager + let targetVersion: SubstrateStorageVersion + let modelBundle: Bundle + private let crashConsistentStoreReplacer: CrashConsistentStoreReplacer + private let crashConsistentCacheRecovery: + CrashConsistentSubstrateCacheRecovery + private let stagedStoreMutationHook: (URL, NSManagedObjectModel) throws -> Void + private let checkpointStoreHook: ((URL, NSManagedObjectModel) throws -> Void)? + private let privateSourceCopyLimits: + SQLiteStoreFamilyCopyLimits + private let privateSourceAvailableCapacityProvider: + ((URL) throws -> UInt64)? + private let protectedDataInspectionLimits: + SubstrateProtectedDataInspectionLimits + private let transformableObjectIDPageDidFetch: ( + String, + Int, + Int + ) -> Void + private let protectedRelationshipCountDidFetch: ( + NSManagedObject, + String, + Int + ) -> Void + private let protectedRelationshipWillMaterialize: ( + String, + String, + Int + ) -> Void + + init( + targetVersion: SubstrateStorageVersion, + storeURL: URL, + modelDirectory: String, + fileManager: FileManager, + modelBundle: Bundle = .main, + storeReplacer: @escaping (URL, URL) throws -> Void = { targetURL, sourceURL in + try NSPersistentStoreCoordinator.replaceStore( + at: targetURL, + withStoreAt: sourceURL + ) + }, + stagedStoreMutationHook: @escaping ( + URL, + NSManagedObjectModel + ) throws -> Void = { _, _ in }, + checkpointStoreHook: ((URL, NSManagedObjectModel) throws -> Void)? = nil, + privateSourceCopyLimits: + SQLiteStoreFamilyCopyLimits = + .substrateStorageProduction, + privateSourceAvailableCapacityProvider: + ((URL) throws -> UInt64)? = nil, + storeReplacementBoundaryHook: @escaping ( + CrashConsistentStoreReplacementBoundary + ) throws -> Void = { _ in }, + cacheRecoveryBoundaryHook: @escaping ( + SubstrateCacheRecoveryBoundary + ) throws -> Void = { _ in }, + protectedDataInspectionLimits: + SubstrateProtectedDataInspectionLimits = .production, + transformableObjectIDPageDidFetch: @escaping ( + String, + Int, + Int + ) -> Void = { _, _, _ in }, + protectedRelationshipCountDidFetch: @escaping ( + NSManagedObject, + String, + Int + ) -> Void = { _, _, _ in }, + protectedRelationshipWillMaterialize: @escaping ( + String, + String, + Int + ) -> Void = { _, _, _ in } + ) { + self.targetVersion = targetVersion + self.storeURL = storeURL + self.modelDirectory = modelDirectory + self.fileManager = fileManager + self.modelBundle = modelBundle + crashConsistentStoreReplacer = CrashConsistentStoreReplacer( + storeURL: storeURL, + fileManager: fileManager, + storeReplacer: storeReplacer, + boundaryHook: storeReplacementBoundaryHook, + maximumStoreFamilyByteCount: + privateSourceCopyLimits.maximumFamilyByteCount, + minimumFreeStorageReserveByteCount: + privateSourceCopyLimits + .minimumFreeStorageReserveByteCount, + availableCapacityProvider: + privateSourceAvailableCapacityProvider + ) + crashConsistentCacheRecovery = + CrashConsistentSubstrateCacheRecovery( + storeURL: storeURL, + fileManager: fileManager, + boundaryHook: cacheRecoveryBoundaryHook + ) + self.stagedStoreMutationHook = stagedStoreMutationHook + self.checkpointStoreHook = checkpointStoreHook + self.privateSourceCopyLimits = privateSourceCopyLimits + self.privateSourceAvailableCapacityProvider = + privateSourceAvailableCapacityProvider + self.protectedDataInspectionLimits = + protectedDataInspectionLimits + self.transformableObjectIDPageDidFetch = + transformableObjectIDPageDidFetch + self.protectedRelationshipCountDidFetch = + protectedRelationshipCountDidFetch + self.protectedRelationshipWillMaterialize = + protectedRelationshipWillMaterialize + } + + func performMigrationWithRecovery() throws -> URL? { + try reconcilePendingCacheRecovery() + try reconcilePendingStoreReplacement() + + guard try crashConsistentStoreReplacer.liveStoreExistsSafely() else { + return nil + } + + var initialFootprint: ProtectedDataFootprint? + + do { + try withPreparedSource( + afterModelDetection: { source in + guard source.compatibleStore.version != self.targetVersion else { + return + } + + initialFootprint = try self + .inspectProtectedDataUsingSanitizedPrivateCopy( + of: source + ) + } + ) { source in + guard source.compatibleStore.version != targetVersion else { + try repairCurrentStoreIfNeeded(from: source) + return + } + + try performMigration(from: source) + } + + return nil + } catch let migrationError as SubstrateStorageMigrationError { + guard migrationError.allowsCacheRebuild else { + throw migrationError + } + + guard let initialFootprint else { + throw migrationError + } + + guard initialFootprint.isEmpty else { + throw SubstrateStorageMigrationError.cacheRecoveryBlockedByProtectedData( + storeURL, + migrationError, + initialFootprint.counts + ) + } + + let finalFootprint = try inspectProtectedDataInExistingStore() + guard finalFootprint.isEmpty else { + throw SubstrateStorageMigrationError.cacheRecoveryBlockedByProtectedData( + storeURL, + migrationError, + finalFootprint.counts + ) + } + + return try recoverRebuildableCache(after: migrationError) + } + } + + func performMigration() throws { + try reconcilePendingCacheRecovery() + try reconcilePendingStoreReplacement() + + guard try crashConsistentStoreReplacer.liveStoreExistsSafely() else { + return + } + + try withPreparedSource { source in + guard source.compatibleStore.version != targetVersion else { + try repairCurrentStoreIfNeeded(from: source) + return + } + + try performMigration(from: source) + } + } + + private func repairCurrentStoreIfNeeded( + from source: PreparedSource + ) throws { + let model = source.compatibleStore.model + let entityNames = Set( + model.entities.compactMap(\.name) + ) + let expectedRowCounts: [String: Int] + let expectedProtectedData: ProtectedDataSnapshot + + do { + if source.compatibleStore.version == .version8 { + try inspectVersion8StartupGraphBounds( + at: source.snapshot.workingStoreURL, + model: model + ) + } + expectedRowCounts = try inspectStoreRowCounts( + at: source.snapshot.workingStoreURL, + model: model, + entityNames: entityNames, + operation: .sourceRowInspection + ) + expectedProtectedData = try inspectProtectedDataSnapshot( + at: source.snapshot.workingStoreURL, + model: model + ) + } catch let migrationError as SubstrateStorageMigrationError { + throw migrationError + } catch { + throw SubstrateStorageMigrationError + .sourceStoreInspectionFailed(storeURL, error) + } + + // A clean current store is still fully inspected above, but remains a + // byte-identical no-op: neither the staged hook nor replacer runs. + guard source.repairedTransformableValueCount > 0 else { + return + } + + do { + try stagedStoreMutationHook( + source.snapshot.workingStoreURL, + model + ) + } catch { + throw SubstrateStorageMigrationError + .stagedStoreInspectionFailed( + source.snapshot.workingStoreURL, + error + ) + } + + try validateStagedStore( + at: source.snapshot.workingStoreURL, + targetModel: model, + expectedRowCounts: expectedRowCounts, + expectedProtectedData: expectedProtectedData + ) + + do { + try crashConsistentStoreReplacer.replaceStore( + with: source.snapshot.workingStoreURL + ) { liveStoreURL in + try self.validateStagedStore( + at: liveStoreURL, + targetModel: model, + expectedRowCounts: expectedRowCounts, + expectedProtectedData: expectedProtectedData + ) + } + } catch let migrationError as SubstrateStorageMigrationError { + throw migrationError + } catch { + throw SubstrateStorageMigrationError + .storeReplacementFailed(storeURL, error) + } + } + + private func performMigration(from source: PreparedSource) throws { + let compatibleStore = source.compatibleStore + let migrationPlan = try createMigrationPlan( + from: compatibleStore, + to: targetVersion + ) + + guard let targetModel = migrationPlan.last?.destinationModel else { + throw SubstrateStorageMigrationError.migrationPathUnavailable( + compatibleStore.version, + targetVersion + ) + } + + let preservedEntityNames = Set(compatibleStore.model.entitiesByName.keys) + .intersection(targetModel.entitiesByName.keys) + .subtracting(Self.intentionallyTransformedEntityNames) + let sourceRowCounts: [String: Int] + let sourceProtectedData: ProtectedDataSnapshot + + do { + sourceRowCounts = try inspectStoreRowCounts( + at: source.snapshot.workingStoreURL, + model: compatibleStore.model, + entityNames: preservedEntityNames, + operation: .sourceRowInspection + ) + sourceProtectedData = try inspectProtectedDataSnapshot( + at: source.snapshot.workingStoreURL, + model: compatibleStore.model, + normalizingMissingSelectedNode: + compatibleStore.version == .version1 + ) + } catch let migrationError as SubstrateStorageMigrationError { + throw migrationError + } catch { + throw SubstrateStorageMigrationError.sourceStoreInspectionFailed( + storeURL, + error + ) + } + + let tmpMigrationDirURL = source.snapshot.rootURL + .appendingPathComponent("Migration", isDirectory: true) + + do { + try fileManager.createDirectory( + at: tmpMigrationDirURL, + withIntermediateDirectories: true + ) + } catch { + throw SubstrateStorageMigrationError.temporaryDirectoryCreationFailed( + tmpMigrationDirURL, + error + ) + } + + let stagedStore = try createStagedStore( + using: migrationPlan, + sourceStoreURL: source.snapshot.workingStoreURL, + migrationDirectoryURL: tmpMigrationDirURL + ) + + do { + try stagedStoreMutationHook(stagedStore, targetModel) + } catch { + throw SubstrateStorageMigrationError.stagedStoreInspectionFailed( + stagedStore, + error + ) + } + + try validateStagedStore( + at: stagedStore, + targetModel: targetModel, + expectedRowCounts: sourceRowCounts, + expectedProtectedData: sourceProtectedData + ) + + do { + try crashConsistentStoreReplacer.replaceStore( + with: stagedStore + ) { liveStoreURL in + try self.validateStagedStore( + at: liveStoreURL, + targetModel: targetModel, + expectedRowCounts: sourceRowCounts, + expectedProtectedData: sourceProtectedData + ) + } + } catch let migrationError as SubstrateStorageMigrationError { + throw migrationError + } catch { + throw SubstrateStorageMigrationError.storeReplacementFailed(storeURL, error) + } + } + + private func reconcilePendingStoreReplacement() throws { + do { + try crashConsistentStoreReplacer.reconcile { + liveStoreURL in + + let compatibleStore = try self.loadCompatibleStore( + at: liveStoreURL + ) + guard compatibleStore.version == self.targetVersion else { + throw SubstrateStorageMigrationError.stagedStoreInvalid( + liveStoreURL, + self.targetVersion + ) + } + } + } catch let migrationError as SubstrateStorageMigrationError { + throw migrationError + } catch { + throw SubstrateStorageMigrationError + .storeReplacementFailed(storeURL, error) + } + } + + private func reconcilePendingCacheRecovery() throws { + do { + try crashConsistentCacheRecovery.reconcile() + } catch is SubstrateCacheRecoveryInterruption { + throw SubstrateCacheRecoveryInterruption + .simulatedProcessDeath + } catch { + throw SubstrateStorageMigrationError + .pendingCacheRecoveryFailed(storeURL, error) + } + } + + private func withPreparedSource( + afterModelDetection: ((PreparedSource) throws -> Void)? = nil, + _ operation: (PreparedSource) throws -> Void + ) throws { + let snapshot = try createPrivateSourceSnapshot() + + defer { + do { + if fileManager.fileExists(atPath: snapshot.rootURL.path) { + try fileManager.removeItem(at: snapshot.rootURL) + } + } catch { + Logger.shared.error( + "Unable to clean up a private Substrate store snapshot" + ) + } + } + + let compatibleStore = try loadCompatibleStore( + at: snapshot.workingStoreURL + ) + let detectedSource = PreparedSource( + snapshot: snapshot, + compatibleStore: compatibleStore, + repairedTransformableValueCount: 0 + ) + + // Recovery decisions use a second isolated copy repaired only in + // explicitly rebuildable cache fields. The migration working copy + // remains untouched until this inspection succeeds. + try afterModelDetection?(detectedSource) + + if compatibleStore.version != targetVersion { + try forceWALCheckpointingForStore( + at: snapshot.workingStoreURL, + sourceModel: compatibleStore.model + ) + } + + let repairedTransformableValueCount = + try sanitizeTransformableValues( + at: snapshot.workingStoreURL, + compatibleStore: compatibleStore + ) + let preparedSource = PreparedSource( + snapshot: snapshot, + compatibleStore: compatibleStore, + repairedTransformableValueCount: + repairedTransformableValueCount + ) + + try operation(preparedSource) + } + + private func inspectProtectedDataUsingSanitizedPrivateCopy( + of source: PreparedSource + ) throws -> ProtectedDataFootprint { + let inspectionRootURL = fileManager.temporaryDirectory + .appendingPathComponent( + "SubstrateProtectedInspection-\(UUID().uuidString)", + isDirectory: true + ) + let inspectionStoreURL = inspectionRootURL + .appendingPathComponent(storeURL.lastPathComponent) + defer { + try? fileManager.removeItem(at: inspectionRootURL) + } + + do { + try fileManager.createDirectory( + at: inspectionRootURL, + withIntermediateDirectories: false + ) + try copyStoreFamily( + from: source.snapshot.workingStoreURL, + to: inspectionStoreURL, + includingSharedMemory: false + ) + } catch { + throw SubstrateStorageMigrationError + .sourceStoreInspectionFailed(storeURL, error) + } + + _ = try sanitizeTransformableValues( + at: inspectionStoreURL, + compatibleStore: source.compatibleStore + ) + + return try inspectProtectedData( + at: inspectionStoreURL, + model: source.compatibleStore.model + ) + } + + private func createPrivateSourceSnapshot() throws -> SourceSnapshot { + let rootURL = fileManager.temporaryDirectory.appendingPathComponent( + "SubstrateStorageSource-\(UUID().uuidString)", + isDirectory: true + ) + let workingStoreURL = rootURL.appendingPathComponent( + storeURL.lastPathComponent + ) + + do { + try fileManager.createDirectory( + at: rootURL, + withIntermediateDirectories: true + ) + + // Never open or checkpoint the installed store. The sole private + // copy is disposable and intentionally omits process-local SHM. + try copyStoreFamily( + from: storeURL, + to: workingStoreURL, + includingSharedMemory: false + ) + } catch { + try? fileManager.removeItem(at: rootURL) + throw SubstrateStorageMigrationError.sourceSnapshotFailed( + storeURL, + error + ) + } + + return SourceSnapshot( + rootURL: rootURL, + workingStoreURL: workingStoreURL + ) + } + + private func copyStoreFamily( + from sourceURL: URL, + to destinationURL: URL, + includingSharedMemory: Bool + ) throws { + try SQLiteStoreFamilyCopier.copy( + from: sourceURL, + to: destinationURL, + includingSharedMemory: includingSharedMemory, + fileManager: fileManager, + limits: privateSourceCopyLimits, + availableCapacityProvider: + privateSourceAvailableCapacityProvider + ) + } + + private func sanitizeTransformableValues( + at url: URL, + compatibleStore: CompatibleStore + ) throws -> Int { + let model = compatibleStore.model + let transformables = try transformableAttributes( + in: model, + version: compatibleStore.version + ) + + guard !transformables.isEmpty else { + return 0 + } + + var repairedValueCount = 0 + do { + try performWithObjectiveCExceptionBoundary( + operation: .transformableSanitization + ) { + let archiveColumns = transformables.flatMap { + transformable in + + transformable.1.map { attribute in + SQLiteTransformableArchiveColumn( + entityName: transformable.0, + attributeName: attribute.name, + isOptional: attribute.isOptional, + valueTransformerName: + attribute.valueTransformerName + ) + } + } + + // This runs against the disposable, checkpointed source copy + // before a Core Data coordinator can load or decode any of + // these allowlisted archives. + repairedValueCount = + try SQLiteTransformableArchivePreflight + .repairOversizedArchives( + at: url, + columns: archiveColumns, + maximumArchiveByteCount: + self.protectedDataInspectionLimits + .maximumTransformableArchiveByteCount + ) + + let coordinator = NSPersistentStoreCoordinator( + managedObjectModel: model + ) + let store = try coordinator.addPersistentStore( + ofType: NSSQLiteStoreType, + configurationName: nil, + at: url, + options: [ + NSSQLitePragmasOption: ["journal_mode": "DELETE"], + NSMigratePersistentStoresAutomaticallyOption: false, + NSInferMappingModelAutomaticallyOption: false + ] + ) + defer { + try? coordinator.remove(store) + } + + let context = NSManagedObjectContext( + concurrencyType: .privateQueueConcurrencyType + ) + context.persistentStoreCoordinator = coordinator + + var contextError: Error? + context.performAndWait { + do { + let rowCounts = + try self.transformableSanitizationRowCounts( + transformables: transformables, + context: context + ) + for (entityName, attributes) in transformables { + let repairedEntityValueCount = + try self.sanitizeTransformableEntity( + entityName: entityName, + attributes: attributes, + rowCount: rowCounts[entityName] ?? 0, + context: context, + persistentStore: store + ) + try self.addRepairedValueCount( + repairedEntityValueCount, + to: &repairedValueCount + ) + } + } catch { + contextError = error + } + } + + if let contextError { + throw contextError + } + } + } catch let migrationError as SubstrateStorageMigrationError { + throw migrationError + } catch { + throw SubstrateStorageMigrationError.transformableSanitizationFailed( + compatibleStore.version, + error + ) + } + + return repairedValueCount + } + + private func addRepairedValueCount( + _ count: Int, + to totalCount: inout Int + ) throws { + let addition = totalCount.addingReportingOverflow(count) + guard !addition.overflow else { + throw SubstrateTransformableRepairError + .repairedValueCountOverflow + } + totalCount = addition.partialValue + } + + private func transformableSanitizationRowCounts( + transformables: [(String, [NSAttributeDescription])], + context: NSManagedObjectContext + ) throws -> [String: Int] { + var rowCounts = [String: Int]() + var totalRowCount = 0 + + // Count and enforce every cap before the first repair write. This + // prevents an adversarial store from forcing an unbounded ID array or + // receiving a partially sanitized private copy before rejection. + for (entityName, _) in transformables { + let request = NSFetchRequest( + entityName: entityName + ) + request.includesPendingChanges = false + request.includesSubentities = false + let rowCount = try context.count(for: request) + guard rowCount != NSNotFound else { + throw SubstrateTransformableRepairError + .countUnavailable(entityName) + } + guard + rowCount <= + protectedDataInspectionLimits.maximumRowsPerEntity + else { + throw SubstrateTransformableRepairError + .entityRowLimitExceeded( + entityName, + rowCount, + protectedDataInspectionLimits + .maximumRowsPerEntity + ) + } + + let addition = + totalRowCount.addingReportingOverflow(rowCount) + guard + !addition.overflow, + addition.partialValue <= + protectedDataInspectionLimits.maximumTotalRows + else { + throw SubstrateTransformableRepairError + .totalRowLimitExceeded( + addition.overflow + ? Int.max + : addition.partialValue, + protectedDataInspectionLimits.maximumTotalRows + ) + } + totalRowCount = addition.partialValue + rowCounts[entityName] = rowCount + } + + return rowCounts + } + + private func sanitizeTransformableEntity( + entityName: String, + attributes: [NSAttributeDescription], + rowCount: Int, + context: NSManagedObjectContext, + persistentStore: NSPersistentStore + ) throws -> Int { + guard rowCount > 0 else { + return 0 + } + + let request = NSFetchRequest( + entityName: entityName + ) + request.resultType = .managedObjectIDResultType + request.includesPendingChanges = false + request.includesSubentities = false + request.affectedStores = [persistentStore] + + guard rowCount < Int.max else { + throw SubstrateTransformableRepairError + .objectIDEnumerationMismatch(entityName) + } + + // Fetch one bounded, exact cohort before the first repair write. + // Slicing this immutable ID list avoids offset pagination over rows + // whose transformable columns are being changed between pages. + request.fetchLimit = rowCount + 1 + request.fetchBatchSize = min( + protectedDataInspectionLimits.fetchPageSize, + rowCount + 1 + ) + let cohortResults = try context.fetch(request) + let cohortObjectIDs = cohortResults.compactMap { + $0 as? NSManagedObjectID + } + guard + cohortObjectIDs.count == cohortResults.count, + cohortObjectIDs.count == rowCount, + Set(cohortObjectIDs).count == rowCount + else { + throw SubstrateTransformableRepairError + .objectIDEnumerationMismatch(entityName) + } + + var pageStart = 0 + var repairedValueCount = 0 + while pageStart < cohortObjectIDs.count { + let pageLength = min( + protectedDataInspectionLimits.fetchPageSize, + cohortObjectIDs.count - pageStart + ) + let pageEnd = pageStart + pageLength + let pageObjectIDs = Array( + cohortObjectIDs[pageStart ..< pageEnd] + ) + let pageRequest = NSFetchRequest( + entityName: entityName + ) + pageRequest.resultType = .managedObjectIDResultType + pageRequest.includesPendingChanges = false + pageRequest.includesSubentities = false + pageRequest.predicate = NSPredicate( + format: "SELF IN %@", + pageObjectIDs + ) + pageRequest.affectedStores = [persistentStore] + pageRequest.fetchLimit = pageObjectIDs.count + pageRequest.fetchBatchSize = pageObjectIDs.count + + let fetchedPageResults = try context.fetch(pageRequest) + let fetchedPageObjectIDs = fetchedPageResults.compactMap { + $0 as? NSManagedObjectID + } + transformableObjectIDPageDidFetch( + entityName, + fetchedPageObjectIDs.count, + pageObjectIDs.count + ) + guard + fetchedPageObjectIDs.count == + fetchedPageResults.count, + fetchedPageObjectIDs.count == + pageObjectIDs.count, + Set(fetchedPageObjectIDs) == Set(pageObjectIDs) + else { + throw SubstrateTransformableRepairError + .objectIDEnumerationMismatch(entityName) + } + + for objectID in pageObjectIDs { + for attribute in attributes { + if try sanitizeStringArray( + objectID: objectID, + entityName: entityName, + attribute: attribute, + context: context, + persistentStore: persistentStore + ) { + try addRepairedValueCount( + 1, + to: &repairedValueCount + ) + } + } + } + pageStart = pageEnd + } + + request.fetchLimit = 0 + request.fetchBatchSize = 0 + guard + pageStart == rowCount, + try context.count(for: request) == rowCount + else { + throw SubstrateTransformableRepairError + .objectIDEnumerationMismatch(entityName) + } + + return repairedValueCount + } + + private func transformableAttributes( + in model: NSManagedObjectModel, + version: SubstrateStorageVersion + ) throws -> [(String, [NSAttributeDescription])] { + try model.entities.compactMap { entity in + guard let entityName = entity.name else { + return nil + } + + let attributes = try entity.attributesByName.values + .filter { $0.attributeType == .transformableAttributeType } + .sorted { $0.name < $1.name } + .map { attribute -> NSAttributeDescription in + let identifier = "\(entityName).\(attribute.name)" + + guard Self.resilientStringArrayTransformables.contains(identifier) else { + throw SubstrateStorageMigrationError + .unsupportedTransformableAttribute( + version, + entityName, + attribute.name + ) + } + + return attribute + } + + return attributes.isEmpty ? nil : (entityName, attributes) + } + .sorted { $0.0 < $1.0 } + } + + private func sanitizeStringArray( + objectID: NSManagedObjectID, + entityName: String, + attribute: NSAttributeDescription, + context: NSManagedObjectContext, + persistentStore: NSPersistentStore + ) throws -> Bool { + let needsRepair: Bool + let request = NSFetchRequest( + entityName: entityName + ) + request.resultType = .dictionaryResultType + request.propertiesToFetch = [attribute.name] + request.predicate = NSPredicate( + format: "SELF == %@", + objectID + ) + request.affectedStores = [persistentStore] + request.fetchLimit = 2 + + do { + var rows: [NSDictionary]? + try SafeObjectiveCExceptionBoundary.perform { + rows = try context.fetch(request) + } + guard + let rows, + rows.count == 1 + else { + throw SubstrateTransformableRepairError + .unexpectedBatchUpdateResult( + entityName, + attribute.name + ) + } + + let value = rows[0][attribute.name] + if let value { + if value is NSNull { + needsRepair = !attribute.isOptional + } else { + needsRepair = !isAcceptableStringArray( + value + ) + } + } else { + needsRepair = !attribute.isOptional + } + } catch is SafeTransformableValueReaderError { + needsRepair = true + } catch let error as NSError + where error.domain == NSCocoaErrorDomain && + error.code == NSCoderReadCorruptError { + needsRepair = true + } + + // A failed transformable decode can leave fetched state unusable. + // Discard it before issuing a direct store-level repair. + context.reset() + + guard needsRepair else { + return false + } + + guard !context.hasChanges else { + throw SubstrateTransformableRepairError + .unexpectedBatchUpdateResult( + entityName, + attribute.name + ) + } + + let update = NSBatchUpdateRequest(entityName: entityName) + update.affectedStores = [persistentStore] + update.predicate = NSPredicate( + format: "SELF == %@", + objectID + ) + update.propertiesToUpdate = [attribute.name: NSArray()] + update.resultType = .updatedObjectIDsResultType + + let result = try context.execute(update) + guard + let batchResult = result as? NSBatchUpdateResult, + let updatedObjectIDs = batchResult.result as? [NSManagedObjectID], + updatedObjectIDs == [objectID] + else { + throw SubstrateTransformableRepairError + .unexpectedBatchUpdateResult( + entityName, + attribute.name + ) + } + + context.reset() + return true + } + + private func isAcceptableStringArray(_ value: Any) -> Bool { + guard + let array = value as? NSArray, + array.count <= + protectedDataInspectionLimits + .maximumTransformableElements + else { + return false + } + + var cumulativeByteCount = 0 + for element in array { + guard let string = element as? String else { + return false + } + + let byteCount = string.utf8.count + guard + byteCount <= + protectedDataInspectionLimits.maximumValueByteCount, + cumulativeByteCount <= + protectedDataInspectionLimits.maximumValueByteCount - + byteCount + else { + return false + } + cumulativeByteCount += byteCount + } + + return true + } + + private func performWithObjectiveCExceptionBoundary( + operation: SubstrateStorageMigrationOperation, + _ block: @escaping () throws -> Void + ) throws { + do { + try SafeObjectiveCExceptionBoundary.perform(block) + } catch SafeTransformableValueReaderError.objectiveCException { + Logger.shared.error( + "Substrate storage safely stopped during \(operation.rawValue)" + ) + throw SubstrateStorageMigrationError.objectiveCException(operation) + } catch { + throw error + } + } + + private func createMigrationPlan( + from source: CompatibleStore, + to destinationVersion: SubstrateStorageVersion + ) throws -> [MigrationStep] { + let versions = SubstrateStorageVersion.allCases + + guard + let sourceIndex = versions.firstIndex(of: source.version), + let destinationIndex = versions.firstIndex(of: destinationVersion), + sourceIndex < destinationIndex + else { + throw SubstrateStorageMigrationError.migrationPathUnavailable( + source.version, + destinationVersion + ) + } + + var currentVersion = source.version + var currentModel = source.model + var visitedVersions = Set() + var migrationPlan: [MigrationStep] = [] + + while currentVersion != destinationVersion { + guard + visitedVersions.insert(currentVersion).inserted, + let nextVersion = currentVersion.nextVersion() + else { + throw SubstrateStorageMigrationError.migrationPathUnavailable( + source.version, + destinationVersion + ) + } + + let nextModel = try createManagedObjectModel(for: nextVersion) + let mapping: NSMappingModel + + do { + mapping = try createMapping( + from: currentModel, + sourceVersion: currentVersion, + nextModel: nextModel, + destinationVersion: nextVersion + ) + } catch { + throw SubstrateStorageMigrationError.mappingUnavailable( + currentVersion, + nextVersion, + error + ) + } + + migrationPlan.append( + MigrationStep( + sourceVersion: currentVersion, + destinationVersion: nextVersion, + sourceModel: currentModel, + destinationModel: nextModel, + mapping: mapping + ) + ) + + currentVersion = nextVersion + currentModel = nextModel + } + + guard !migrationPlan.isEmpty else { + throw SubstrateStorageMigrationError.migrationPathUnavailable( + source.version, + destinationVersion + ) + } + + return migrationPlan + } + + private func createStagedStore( + using migrationPlan: [MigrationStep], + sourceStoreURL: URL, + migrationDirectoryURL: URL + ) throws -> URL { + var currentURL = sourceStoreURL + + for step in migrationPlan { + let nextStepURL = migrationDirectoryURL.appendingPathComponent( + "\(step.destinationVersion.rawValue)-\(UUID().uuidString).sqlite" + ) + let manager = NSMigrationManager( + sourceModel: step.sourceModel, + destinationModel: step.destinationModel + ) + + do { + try performWithObjectiveCExceptionBoundary( + operation: .stagedMigration + ) { + try manager.migrateStore( + from: currentURL, + sourceType: NSSQLiteStoreType, + options: nil, + with: step.mapping, + toDestinationURL: nextStepURL, + destinationType: NSSQLiteStoreType, + destinationOptions: nil + ) + } + } catch let migrationError as SubstrateStorageMigrationError { + throw migrationError + } catch { + throw SubstrateStorageMigrationError.migrationStepFailed( + step.sourceVersion, + step.destinationVersion, + error + ) + } + + do { + // At most the current input and its successfully produced + // output coexist. Do not retain a chain of full SQLite stores. + try removeStoreFamily(at: currentURL) + } catch { + // Best effort prevents the new output from adding to the + // footprint while the enclosing source-root cleanup runs. + try? removeStoreFamily(at: nextStepURL) + throw SubstrateStorageMigrationError.temporaryStoreCleanupFailed( + currentURL, + error + ) + } + + currentURL = nextStepURL + } + + return currentURL + } + + private func removeStoreFamily(at url: URL) throws { + for suffix in Self.storeFamilySuffixes { + let familyURL = URL(fileURLWithPath: url.path + suffix) + + guard fileManager.fileExists(atPath: familyURL.path) else { + continue + } + + try fileManager.removeItem(at: familyURL) + } + } + + private func validateStagedStore( + at stagedStoreURL: URL, + targetModel: NSManagedObjectModel, + expectedRowCounts: [String: Int], + expectedProtectedData: ProtectedDataSnapshot + ) throws { + let metadata: [String: Any] + do { + var inspectedMetadata: [String: Any]? + try performWithObjectiveCExceptionBoundary( + operation: .stagedValidation + ) { + inspectedMetadata = try NSPersistentStoreCoordinator + .metadataForPersistentStore( + ofType: NSSQLiteStoreType, + at: stagedStoreURL, + options: nil + ) + } + metadata = inspectedMetadata ?? [:] + } catch let migrationError as SubstrateStorageMigrationError { + throw migrationError + } catch { + throw SubstrateStorageMigrationError.stagedStoreMetadataUnreadable( + stagedStoreURL, + error + ) + } + + guard targetModel.isConfiguration( + withName: targetVersion.rawValue, + compatibleWithStoreMetadata: metadata + ) else { + throw SubstrateStorageMigrationError.stagedStoreInvalid( + stagedStoreURL, + targetVersion + ) + } + + if targetVersion == .version8 { + do { + try inspectVersion8StartupGraphBounds( + at: stagedStoreURL, + model: targetModel + ) + } catch let migrationError as SubstrateStorageMigrationError { + throw migrationError + } catch { + throw SubstrateStorageMigrationError + .stagedStoreInspectionFailed( + stagedStoreURL, + error + ) + } + } + + let stagedRowCounts: [String: Int] + do { + stagedRowCounts = try inspectStoreRowCounts( + at: stagedStoreURL, + model: targetModel, + entityNames: Set(expectedRowCounts.keys), + operation: .stagedValidation + ) + } catch let migrationError as SubstrateStorageMigrationError { + throw migrationError + } catch { + throw SubstrateStorageMigrationError.stagedStoreInspectionFailed( + stagedStoreURL, + error + ) + } + + for (entityName, expectedCount) in expectedRowCounts { + let actualCount = stagedRowCounts[entityName] ?? 0 + + guard actualCount == expectedCount else { + throw SubstrateStorageMigrationError.stagedStoreRowCountMismatch( + entityName, + expectedCount, + actualCount + ) + } + } + + let stagedProtectedData: ProtectedDataSnapshot + do { + stagedProtectedData = try inspectProtectedDataSnapshot( + at: stagedStoreURL, + model: targetModel + ) + } catch let migrationError as SubstrateStorageMigrationError { + throw migrationError + } catch let inspectionError + as SubstrateProtectedDataInspectionError { + throw SubstrateStorageMigrationError + .protectedDataInspectionRejected( + stagedStoreURL, + inspectionError + ) + } catch { + throw SubstrateStorageMigrationError.stagedStoreInspectionFailed( + stagedStoreURL, + error + ) + } + + guard stagedProtectedData == expectedProtectedData else { + throw SubstrateStorageMigrationError + .stagedStoreProtectedDataMismatch + } + } + + private func inspectProtectedDataInExistingStore() throws -> ProtectedDataFootprint { + guard fileManager.fileExists(atPath: storeURL.path) else { + throw SubstrateStorageMigrationError.sourceStoreInspectionFailed( + storeURL, + CocoaError( + .fileNoSuchFile, + userInfo: [NSFilePathErrorKey: storeURL.path] + ) + ) + } + + let snapshot = try createPrivateSourceSnapshot() + defer { + do { + if fileManager.fileExists(atPath: snapshot.rootURL.path) { + try fileManager.removeItem(at: snapshot.rootURL) + } + } catch { + Logger.shared.error( + "Unable to clean up a private Substrate recovery-inspection copy" + ) + } + } + + // This is the final race-resistant recovery check. Only the private + // copy's explicitly rebuildable transformables may be repaired before + // protected-data inspection; the installed family remains untouched. + let compatibleStore = try loadCompatibleStore( + at: snapshot.workingStoreURL + ) + _ = try sanitizeTransformableValues( + at: snapshot.workingStoreURL, + compatibleStore: compatibleStore + ) + + return try inspectProtectedData( + at: snapshot.workingStoreURL, + model: compatibleStore.model + ) + } + + private func inspectProtectedData( + at url: URL, + model: NSManagedObjectModel + ) throws -> ProtectedDataFootprint { + do { + return try inspectProtectedDataUnchecked( + at: url, + model: model + ) + } catch let migrationError as SubstrateStorageMigrationError { + throw migrationError + } catch { + throw SubstrateStorageMigrationError.sourceStoreInspectionFailed( + storeURL, + error + ) + } + } + + private func inspectProtectedDataUnchecked( + at url: URL, + model: NSManagedObjectModel + ) throws -> ProtectedDataFootprint { + var footprint: ProtectedDataFootprint? + try performWithObjectiveCExceptionBoundary( + operation: .protectedDataInspection + ) { + footprint = try self.inspectProtectedDataWithoutExceptionBoundary( + at: url, + model: model + ) + } + + guard let footprint else { + throw SubstrateProtectedDataInspectionError + .countUnavailable("protected-data footprint") + } + return footprint + } + + private func inspectProtectedDataWithoutExceptionBoundary( + at url: URL, + model: NSManagedObjectModel + ) throws -> ProtectedDataFootprint { + guard let chainEntity = model.entitiesByName["CDChain"] else { + throw SubstrateProtectedDataInspectionError.entityUnavailable("CDChain") + } + guard model.entitiesByName["CDChainNode"] != nil else { + throw SubstrateProtectedDataInspectionError.entityUnavailable("CDChainNode") + } + + for relationshipName in ["nodes", "customNodes", "selectedNode"] { + guard chainEntity.relationshipsByName[relationshipName] != nil else { + throw SubstrateProtectedDataInspectionError.relationshipUnavailable( + "CDChain", + relationshipName + ) + } + } + + let coordinator = NSPersistentStoreCoordinator(managedObjectModel: model) + let store = try coordinator.addPersistentStore( + ofType: NSSQLiteStoreType, + configurationName: nil, + at: url, + options: [ + NSReadOnlyPersistentStoreOption: true, + NSMigratePersistentStoresAutomaticallyOption: false, + NSInferMappingModelAutomaticallyOption: false + ] + ) + defer { + try? coordinator.remove(store) + } + + let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) + context.persistentStoreCoordinator = coordinator + + var result: Result? + context.performAndWait { + result = Result { + let rowCounts = try self.protectedEntityRowCounts( + model: model, + context: context + ) + var counts: [String: Int] = [ + ProtectedDataCategory.contacts: + rowCounts[ProtectedDataCategory.contacts] ?? 0, + ProtectedDataCategory.contactItems: + rowCounts[ProtectedDataCategory.contactItems] ?? 0, + ProtectedDataCategory.transactionHistory: + rowCounts[ + ProtectedDataCategory.transactionHistory + ] ?? 0 + ] + + var referencedNodeIDs = Set() + var customNodeRelationshipCount = 0 + var selectedNodeRelationshipCount = 0 + var totalRelationshipCount = 0 + + try self.forEachManagedObjectPage( + entityName: "CDChain", + rowCount: rowCounts["CDChain"] ?? 0, + context: context + ) { chains in + for chain in chains { + let defaultNodeCount = try self + .boundedRelationshipMemberCount( + inRelationship: "nodes", + of: chain + ) + let customNodeCount = try self + .boundedRelationshipMemberCount( + inRelationship: "customNodes", + of: chain + ) + try self.addRelationshipMembers( + defaultNodeCount, + to: &totalRelationshipCount + ) + try self.addRelationshipMembers( + customNodeCount, + to: &totalRelationshipCount + ) + + let defaultNodes = try self + .boundedManagedObjects( + inRelationship: "nodes", + of: chain, + expectedMemberCount: defaultNodeCount + ) + let customNodes = try self + .boundedManagedObjects( + inRelationship: "customNodes", + of: chain, + expectedMemberCount: customNodeCount + ) + referencedNodeIDs.formUnion( + defaultNodes.map(\.objectID) + ) + referencedNodeIDs.formUnion( + customNodes.map(\.objectID) + ) + customNodeRelationshipCount += + customNodes.count + + if let selectedNode = chain.value( + forKey: "selectedNode" + ) as? NSManagedObject { + try self.addRelationshipMembers( + 1, + to: &totalRelationshipCount + ) + referencedNodeIDs.insert( + selectedNode.objectID + ) + selectedNodeRelationshipCount += 1 + } + } + } + + var orphanNodeCount = 0 + try self.forEachManagedObjectPage( + entityName: "CDChainNode", + rowCount: rowCounts["CDChainNode"] ?? 0, + context: context + ) { nodes in + for node in nodes where + !referencedNodeIDs.contains(node.objectID) { + orphanNodeCount += 1 + } + } + + counts[ProtectedDataCategory.customNodeRelationships] = + customNodeRelationshipCount + counts[ProtectedDataCategory.selectedNodeRelationships] = + selectedNodeRelationshipCount + counts[ProtectedDataCategory.orphanNodes] = orphanNodeCount + + return ProtectedDataFootprint(counts: counts) + } + } + + guard let result else { + throw SubstrateProtectedDataInspectionError + .countUnavailable("protected data footprint") + } + return try result.get() + } + + private func protectedEntityRowCounts( + model: NSManagedObjectModel, + context: NSManagedObjectContext + ) throws -> [String: Int] { + let entityNames = [ + ProtectedDataCategory.contacts, + ProtectedDataCategory.contactItems, + ProtectedDataCategory.transactionHistory, + "CDChain", + "CDChainNode" + ] + var counts: [String: Int] = [:] + var totalCount = 0 + + for entityName in entityNames { + let count = try countRowsIfPresent( + entityName: entityName, + model: model, + context: context + ) + guard + count <= protectedDataInspectionLimits + .maximumRowsPerEntity + else { + throw SubstrateProtectedDataInspectionError + .entityRowLimitExceeded( + entityName, + count, + protectedDataInspectionLimits + .maximumRowsPerEntity + ) + } + + let addition = totalCount.addingReportingOverflow(count) + guard + !addition.overflow, + addition.partialValue <= + protectedDataInspectionLimits.maximumTotalRows + else { + throw SubstrateProtectedDataInspectionError + .totalRowLimitExceeded( + addition.overflow + ? Int.max + : addition.partialValue, + protectedDataInspectionLimits.maximumTotalRows + ) + } + totalCount = addition.partialValue + counts[entityName] = count + } + + return counts + } + + private func countRowsIfPresent( + entityName: String, + model: NSManagedObjectModel, + context: NSManagedObjectContext + ) throws -> Int { + guard model.entitiesByName[entityName] != nil else { + return 0 + } + + let request = NSFetchRequest(entityName: entityName) + request.includesPendingChanges = false + request.includesSubentities = false + let count = try context.count(for: request) + + guard count != NSNotFound else { + throw SubstrateProtectedDataInspectionError.countUnavailable(entityName) + } + + return count + } + + private func inspectVersion8StartupGraphBounds( + at url: URL, + model: NSManagedObjectModel + ) throws { + try performWithObjectiveCExceptionBoundary( + operation: .protectedDataInspection + ) { + try self + .inspectVersion8StartupGraphBoundsWithoutExceptionBoundary( + at: url, + model: model + ) + } + } + + private func inspectVersion8StartupGraphBoundsWithoutExceptionBoundary( + at url: URL, + model: NSManagedObjectModel + ) throws { + for entityName in Self.version8StartupRootEntityNames { + guard model.entitiesByName[entityName] != nil else { + throw SubstrateProtectedDataInspectionError + .entityUnavailable(entityName) + } + } + for entityName in Self.version8StartupChildEntityNames { + guard model.entitiesByName[entityName] != nil else { + throw SubstrateProtectedDataInspectionError + .entityUnavailable(entityName) + } + } + for family in Self.version8StartupToManyRelationshipFamilies { + guard let entity = model.entitiesByName[family.entityName] else { + throw SubstrateProtectedDataInspectionError + .entityUnavailable(family.entityName) + } + for relationshipName in family.relationshipNames { + guard + let relationship = + entity.relationshipsByName[relationshipName], + relationship.isToMany + else { + throw SubstrateProtectedDataInspectionError + .relationshipUnavailable( + family.entityName, + relationshipName + ) + } + } + } + + try inspectVersion8StartupRootRawBounds( + at: url, + model: model + ) + try inspectVersion8StartupChildRawBounds( + at: url, + model: model + ) + + let coordinator = NSPersistentStoreCoordinator( + managedObjectModel: model + ) + let store = try coordinator.addPersistentStore( + ofType: NSSQLiteStoreType, + configurationName: nil, + at: url, + options: [ + NSReadOnlyPersistentStoreOption: true, + NSMigratePersistentStoresAutomaticallyOption: false, + NSInferMappingModelAutomaticallyOption: false + ] + ) + defer { + try? coordinator.remove(store) + } + + let context = NSManagedObjectContext( + concurrencyType: .privateQueueConcurrencyType + ) + context.persistentStoreCoordinator = coordinator + + var result: Result? + context.performAndWait { + result = Result { + let rowCounts = try self.version8StartupEntityRowCounts( + context: context, + persistentStore: store + ) + var totalRelationshipMemberCount = 0 + + for family in + Self.version8StartupToManyRelationshipFamilies { + let objectIDs = try self + .version8StartupObjectIDs( + entityName: family.entityName, + rowCount: + rowCounts[family.entityName] ?? 0, + context: context, + persistentStore: store + ) + var pageStart = 0 + while pageStart < objectIDs.count { + let pageLength = min( + self.protectedDataInspectionLimits + .fetchPageSize, + objectIDs.count - pageStart + ) + let pageEnd = pageStart + pageLength + + do { + for objectID in + objectIDs[pageStart ..< pageEnd] { + let object = context.object( + with: objectID + ) + for relationshipName in + family.relationshipNames { + let memberCount = try self + .boundedRelationshipMemberCount( + inRelationship: + relationshipName, + of: object + ) + try self.addRelationshipMembers( + memberCount, + to: + &totalRelationshipMemberCount + ) + } + } + } catch { + context.reset() + throw error + } + + pageStart = pageEnd + context.reset() + } + } + + return () + } + } + + guard let result else { + throw SubstrateProtectedDataInspectionError + .countUnavailable("v8 startup graph") + } + try result.get() + } + + private func inspectVersion8StartupRootRawBounds( + at url: URL, + model: NSManagedObjectModel + ) throws { + var database: OpaquePointer? + let openResult = sqlite3_open_v2( + url.path, + &database, + SQLITE_OPEN_READONLY | SQLITE_OPEN_FULLMUTEX, + nil + ) + guard openResult == SQLITE_OK, let database else { + if let database { + sqlite3_close_v2(database) + } + throw SubstrateProtectedDataInspectionError + .countUnavailable("v8 startup roots") + } + defer { + sqlite3_close_v2(database) + } + + var aggregatePayloadByteCount: UInt64 = 0 + for entityName in Self.version8StartupRootEntityNames.sorted() { + guard let entity = model.entitiesByName[entityName] else { + throw SubstrateProtectedDataInspectionError + .entityUnavailable(entityName) + } + let tableName = try version8PhysicalIdentifier( + entityName + ) + let columnNames = try entity.attributesByName.values + .sorted { $0.name < $1.name } + .map { + try version8PhysicalIdentifier($0.name) + } + guard !columnNames.isEmpty else { + throw SubstrateProtectedDataInspectionError + .countUnavailable(entityName) + } + + let payloadExpressions = columnNames.map { + """ + COALESCE(length(CAST("\($0)" AS BLOB)), 0) + """ + }.joined(separator: ", ") + let sql = """ + SELECT \(payloadExpressions) + FROM "\(tableName)" + """ + var statement: OpaquePointer? + guard + sqlite3_prepare_v2( + database, + sql, + -1, + &statement, + nil + ) == SQLITE_OK, + let statement + else { + if let statement { + sqlite3_finalize(statement) + } + throw SubstrateProtectedDataInspectionError + .countUnavailable(entityName) + } + defer { + sqlite3_finalize(statement) + } + + var rowCount = 0 + while true { + let stepResult = sqlite3_step(statement) + if stepResult == SQLITE_DONE { + break + } + guard stepResult == SQLITE_ROW else { + throw SubstrateProtectedDataInspectionError + .countUnavailable(entityName) + } + + let rowAddition = rowCount.addingReportingOverflow(1) + guard + !rowAddition.overflow, + rowAddition.partialValue <= + protectedDataInspectionLimits + .maximumStartupRootRowsPerEntity + else { + throw SubstrateProtectedDataInspectionError + .entityRowLimitExceeded( + entityName, + rowAddition.overflow + ? Int.max + : rowAddition.partialValue, + protectedDataInspectionLimits + .maximumStartupRootRowsPerEntity + ) + } + rowCount = rowAddition.partialValue + + for columnIndex in 0 ..< columnNames.count { + guard + sqlite3_column_type( + statement, + Int32(columnIndex) + ) == SQLITE_INTEGER + else { + throw SubstrateProtectedDataInspectionError + .countUnavailable(entityName) + } + let signedByteCount = sqlite3_column_int64( + statement, + Int32(columnIndex) + ) + guard signedByteCount >= 0 else { + throw SubstrateProtectedDataInspectionError + .countUnavailable(entityName) + } + + let addition = aggregatePayloadByteCount + .addingReportingOverflow( + UInt64(signedByteCount) + ) + guard + !addition.overflow, + addition.partialValue <= + protectedDataInspectionLimits + .maximumStartupRootPayloadByteCount + else { + throw SubstrateProtectedDataInspectionError + .startupRootPayloadByteLimitExceeded( + addition.overflow + ? UInt64.max + : addition.partialValue, + protectedDataInspectionLimits + .maximumStartupRootPayloadByteCount + ) + } + aggregatePayloadByteCount = + addition.partialValue + } + } + } + } + + private func inspectVersion8StartupChildRawBounds( + at url: URL, + model: NSManagedObjectModel + ) throws { + var database: OpaquePointer? + let openResult = sqlite3_open_v2( + url.path, + &database, + SQLITE_OPEN_READONLY | SQLITE_OPEN_FULLMUTEX, + nil + ) + guard openResult == SQLITE_OK, let database else { + if let database { + sqlite3_close_v2(database) + } + throw SubstrateProtectedDataInspectionError + .countUnavailable("v8 startup children") + } + defer { + sqlite3_close_v2(database) + } + + var totalRowCount = 0 + var aggregatePayloadByteCount: UInt64 = 0 + + for entityName in Self.version8StartupChildEntityNames.sorted() { + guard let entity = model.entitiesByName[entityName] else { + throw SubstrateProtectedDataInspectionError + .entityUnavailable(entityName) + } + let tableName = try version8PhysicalIdentifier( + entityName + ) + let attributes = try entity.attributesByName.values + .sorted { $0.name < $1.name } + .map { + ( + name: $0.name, + columnName: try version8PhysicalIdentifier( + $0.name + ) + ) + } + guard !attributes.isEmpty else { + throw SubstrateProtectedDataInspectionError + .countUnavailable(entityName) + } + + let payloadExpressions = attributes.map { + """ + COALESCE(length(CAST("\($0.columnName)" AS BLOB)), 0) + """ + }.joined(separator: ", ") + let sql = """ + SELECT \(payloadExpressions) + FROM "\(tableName)" + """ + var statement: OpaquePointer? + guard + sqlite3_prepare_v2( + database, + sql, + -1, + &statement, + nil + ) == SQLITE_OK, + let statement + else { + if let statement { + sqlite3_finalize(statement) + } + throw SubstrateProtectedDataInspectionError + .countUnavailable(entityName) + } + + do { + defer { + sqlite3_finalize(statement) + } + + var entityRowCount = 0 + while true { + let stepResult = sqlite3_step(statement) + if stepResult == SQLITE_DONE { + break + } + guard stepResult == SQLITE_ROW else { + throw SubstrateProtectedDataInspectionError + .countUnavailable(entityName) + } + + let entityRowAddition = + entityRowCount.addingReportingOverflow(1) + guard + !entityRowAddition.overflow, + entityRowAddition.partialValue <= + protectedDataInspectionLimits + .maximumRowsPerEntity + else { + throw SubstrateProtectedDataInspectionError + .entityRowLimitExceeded( + entityName, + entityRowAddition.overflow + ? Int.max + : entityRowAddition.partialValue, + protectedDataInspectionLimits + .maximumRowsPerEntity + ) + } + entityRowCount = + entityRowAddition.partialValue + + let totalRowAddition = + totalRowCount.addingReportingOverflow(1) + guard + !totalRowAddition.overflow, + totalRowAddition.partialValue <= + protectedDataInspectionLimits + .maximumTotalRows + else { + throw SubstrateProtectedDataInspectionError + .totalRowLimitExceeded( + totalRowAddition.overflow + ? Int.max + : totalRowAddition.partialValue, + protectedDataInspectionLimits + .maximumTotalRows + ) + } + totalRowCount = totalRowAddition.partialValue + + for ( + columnIndex, + attribute + ) in attributes.enumerated() { + guard + sqlite3_column_type( + statement, + Int32(columnIndex) + ) == SQLITE_INTEGER + else { + throw SubstrateProtectedDataInspectionError + .countUnavailable(entityName) + } + let signedByteCount = sqlite3_column_int64( + statement, + Int32(columnIndex) + ) + guard signedByteCount >= 0 else { + throw SubstrateProtectedDataInspectionError + .countUnavailable(entityName) + } + let valueByteCount = UInt64( + signedByteCount + ) + + guard + valueByteCount <= + protectedDataInspectionLimits + .maximumStartupChildValueByteCount + else { + throw SubstrateProtectedDataInspectionError + .startupChildValueByteLimitExceeded( + entityName, + attribute.name, + valueByteCount, + protectedDataInspectionLimits + .maximumStartupChildValueByteCount + ) + } + + let aggregateAddition = + aggregatePayloadByteCount + .addingReportingOverflow( + valueByteCount + ) + guard + !aggregateAddition.overflow, + aggregateAddition.partialValue <= + protectedDataInspectionLimits + .maximumStartupChildPayloadByteCount + else { + throw SubstrateProtectedDataInspectionError + .startupChildPayloadByteLimitExceeded( + aggregateAddition.overflow + ? UInt64.max + : aggregateAddition.partialValue, + protectedDataInspectionLimits + .maximumStartupChildPayloadByteCount + ) + } + aggregatePayloadByteCount = + aggregateAddition.partialValue + } + } + } + } + } + + private func version8PhysicalIdentifier( + _ logicalName: String + ) throws -> String { + guard + !logicalName.isEmpty, + logicalName.unicodeScalars.allSatisfy({ + (0x30 ... 0x39).contains($0.value) || + (0x41 ... 0x5A).contains($0.value) || + (0x61 ... 0x7A).contains($0.value) || + $0.value == 0x5F + }) + else { + throw SubstrateProtectedDataInspectionError + .countUnavailable("v8 startup schema") + } + + return "Z\(logicalName.uppercased())" + } + + private func version8StartupEntityRowCounts( + context: NSManagedObjectContext, + persistentStore: NSPersistentStore + ) throws -> [String: Int] { + let entityNames = Set( + Self.version8StartupRootEntityNames + + Self.version8StartupToManyRelationshipFamilies.map( + \.entityName + ) + ) + var rowCounts = [String: Int]() + var totalRowCount = 0 + + for entityName in entityNames.sorted() { + let request = NSFetchRequest( + entityName: entityName + ) + request.includesPendingChanges = false + request.includesSubentities = false + request.affectedStores = [persistentStore] + let rowCount = try context.count(for: request) + guard rowCount != NSNotFound else { + throw SubstrateProtectedDataInspectionError + .countUnavailable(entityName) + } + guard + rowCount <= + protectedDataInspectionLimits.maximumRowsPerEntity + else { + throw SubstrateProtectedDataInspectionError + .entityRowLimitExceeded( + entityName, + rowCount, + protectedDataInspectionLimits + .maximumRowsPerEntity + ) + } + + let addition = totalRowCount.addingReportingOverflow( + rowCount + ) + guard + !addition.overflow, + addition.partialValue <= + protectedDataInspectionLimits.maximumTotalRows + else { + throw SubstrateProtectedDataInspectionError + .totalRowLimitExceeded( + addition.overflow + ? Int.max + : addition.partialValue, + protectedDataInspectionLimits.maximumTotalRows + ) + } + totalRowCount = addition.partialValue + rowCounts[entityName] = rowCount + } + + return rowCounts + } + + private func version8StartupObjectIDs( + entityName: String, + rowCount: Int, + context: NSManagedObjectContext, + persistentStore: NSPersistentStore + ) throws -> [NSManagedObjectID] { + guard rowCount > 0 else { + return [] + } + guard rowCount < Int.max else { + throw SubstrateProtectedDataInspectionError + .countUnavailable(entityName) + } + + let request = NSFetchRequest( + entityName: entityName + ) + request.resultType = .managedObjectIDResultType + request.includesPendingChanges = false + request.includesSubentities = false + request.affectedStores = [persistentStore] + request.fetchLimit = rowCount + 1 + request.fetchBatchSize = min( + protectedDataInspectionLimits.fetchPageSize, + rowCount + 1 + ) + + let fetchedResults = try context.fetch(request) + let objectIDs = fetchedResults.compactMap { + $0 as? NSManagedObjectID + } + guard + objectIDs.count == fetchedResults.count, + objectIDs.count == rowCount, + Set(objectIDs).count == rowCount, + objectIDs.allSatisfy({ + $0.persistentStore === persistentStore + }) + else { + throw SubstrateProtectedDataInspectionError + .countUnavailable(entityName) + } + + return objectIDs + } + + private func forEachManagedObjectPage( + entityName: String, + rowCount: Int, + context: NSManagedObjectContext, + _ operation: ([NSManagedObject]) throws -> Void + ) throws { + guard rowCount > 0 else { + return + } -final class SubstrateStorageMigrator { - let storeURL: URL - let modelDirectory: String - let fileManager: FileManager - let targetVersion: SubstrateStorageVersion + // Materialize the capped ID list once for exact-once enumeration; + // managed objects and their values are hydrated only one page at a + // time below. + let identifierRequest = + NSFetchRequest( + entityName: entityName + ) + identifierRequest.resultType = .managedObjectIDResultType + identifierRequest.includesPendingChanges = false + identifierRequest.includesSubentities = false + let objectIDs = try context.fetch(identifierRequest) + .compactMap { $0 as? NSManagedObjectID } + guard + objectIDs.count == rowCount, + Set(objectIDs).count == rowCount + else { + throw SubstrateProtectedDataInspectionError + .countUnavailable(entityName) + } - init( - targetVersion: SubstrateStorageVersion, - storeURL: URL, - modelDirectory: String, - fileManager: FileManager - ) { - self.targetVersion = targetVersion - self.storeURL = storeURL - self.modelDirectory = modelDirectory - self.fileManager = fileManager + var pageStart = 0 + while pageStart < objectIDs.count { + let pageEnd = min( + pageStart + + protectedDataInspectionLimits.fetchPageSize, + objectIDs.count + ) + let pageObjectIDs = Array( + objectIDs[pageStart ..< pageEnd] + ) + do { + try autoreleasepool { + let request = NSFetchRequest( + entityName: entityName + ) + request.includesPendingChanges = false + request.includesSubentities = false + request.predicate = NSPredicate( + format: "SELF IN %@", + pageObjectIDs + ) + request.fetchLimit = pageObjectIDs.count + request.fetchBatchSize = pageObjectIDs.count + + let page = try context.fetch(request) + guard + page.count == pageObjectIDs.count, + Set(page.map(\.objectID)) == + Set(pageObjectIDs) + else { + throw SubstrateProtectedDataInspectionError + .countUnavailable(entityName) + } + try operation(page) + } + } catch { + context.reset() + throw error + } + pageStart = pageEnd + context.reset() + } } - func performMigration() { - forceWALCheckpointingForStore(at: storeURL) + private func boundedRelationshipMemberCount( + inRelationship relationshipName: String, + of object: NSManagedObject + ) throws -> Int { + let entityName = object.entity.name ?? "unknown" + guard + let relationship = + object.entity.relationshipsByName[relationshipName], + relationship.isToMany, + let context = object.managedObjectContext, + !object.objectID.isTemporaryID, + let persistentStore = object.objectID.persistentStore + else { + throw SubstrateProtectedDataInspectionError + .relationshipCountUnavailable( + entityName, + relationshipName + ) + } - let maybeMetadata = try? NSPersistentStoreCoordinator.metadataForPersistentStore( - ofType: NSSQLiteStoreType, - at: storeURL, - options: nil + let countKey = "__fearlessRelationshipMemberCount" + let countDescription = NSExpressionDescription() + countDescription.name = countKey + countDescription.expression = NSExpression( + forFunction: "count:", + arguments: [ + NSExpression(forKeyPath: relationshipName) + ] ) + countDescription.expressionResultType = + .integer64AttributeType + let request = NSFetchRequest( + entityName: entityName + ) + request.resultType = .dictionaryResultType + request.propertiesToFetch = [countDescription] + request.predicate = NSPredicate( + format: "SELF == %@", + object.objectID + ) + request.includesPendingChanges = false + request.includesSubentities = false + request.affectedStores = [persistentStore] + request.fetchLimit = 2 + + let rows = try context.fetch(request) guard - let metadata = maybeMetadata, - let sourceVersion = compatibleVersionForStoreMetadata(metadata) else { - fatalError("Unknown store version at URL \(storeURL)") + rows.count == 1, + let number = rows[0][countKey] as? NSNumber + else { + throw SubstrateProtectedDataInspectionError + .relationshipCountUnavailable( + entityName, + relationshipName + ) } - do { - let migrationDirName = UUID().uuidString - let tmpMigrationDirURL = URL( - fileURLWithPath: NSTemporaryDirectory(), - isDirectory: true - ).appendingPathComponent(migrationDirName) + let count = number.int64Value + guard + count >= 0, + UInt64(count) <= UInt64(Int.max) + else { + throw SubstrateProtectedDataInspectionError + .relationshipCountUnavailable( + entityName, + relationshipName + ) + } + let memberCount = Int(count) - try fileManager.createDirectory(at: tmpMigrationDirURL, withIntermediateDirectories: true) + protectedRelationshipCountDidFetch( + object, + relationshipName, + memberCount + ) - try performMigration( - from: sourceVersion, - to: targetVersion, - storeURL: storeURL, - tmpMigrationDirURL: tmpMigrationDirURL - ) + guard + memberCount <= protectedDataInspectionLimits + .maximumRelationshipMembers + else { + throw SubstrateProtectedDataInspectionError + .relationshipLimitExceeded( + entityName, + relationshipName, + memberCount, + protectedDataInspectionLimits + .maximumRelationshipMembers + ) + } - try fileManager.removeItem(at: tmpMigrationDirURL) - } catch { - fatalError("Migration failed with error \(error)") + return memberCount + } + + private func boundedManagedObjects( + inRelationship relationshipName: String, + of object: NSManagedObject, + expectedMemberCount: Int + ) throws -> [NSManagedObject] { + let entityName = object.entity.name ?? "unknown" + + // The SQL-backed count and both the per-relationship and aggregate + // caps have succeeded before this is called. KVC may fire the + // relationship only after every allocation is therefore bounded. + protectedRelationshipWillMaterialize( + entityName, + relationshipName, + expectedMemberCount + ) + + guard let value = object.value(forKey: relationshipName) else { + guard expectedMemberCount == 0 else { + throw SubstrateProtectedDataInspectionError + .relationshipValueInvalid( + entityName, + relationshipName + ) + } + return [] + } + guard + let objects = value as? NSSet, + objects.count == expectedMemberCount + else { + throw SubstrateProtectedDataInspectionError + .relationshipValueInvalid( + entityName, + relationshipName + ) + } + + var managedObjects = [NSManagedObject]() + managedObjects.reserveCapacity(expectedMemberCount) + for member in objects { + guard let managedObject = member as? NSManagedObject else { + throw SubstrateProtectedDataInspectionError + .relationshipValueInvalid( + entityName, + relationshipName + ) + } + managedObjects.append(managedObject) } + guard managedObjects.count == expectedMemberCount else { + throw SubstrateProtectedDataInspectionError + .relationshipValueInvalid( + entityName, + relationshipName + ) + } + + return managedObjects } - private func performMigration( - from sourceVersion: SubstrateStorageVersion, - to destinationVersion: SubstrateStorageVersion, - storeURL: URL, - tmpMigrationDirURL: URL + private func addRelationshipMembers( + _ count: Int, + to totalCount: inout Int ) throws { - var currentVersion = sourceVersion - var currentURL = storeURL + let addition = totalCount.addingReportingOverflow(count) + guard + !addition.overflow, + addition.partialValue <= protectedDataInspectionLimits + .maximumTotalRelationshipMembers + else { + throw SubstrateProtectedDataInspectionError + .totalRelationshipLimitExceeded( + addition.overflow ? Int.max : addition.partialValue, + protectedDataInspectionLimits + .maximumTotalRelationshipMembers + ) + } + totalCount = addition.partialValue + } + + private func inspectProtectedDataSnapshot( + at url: URL, + model: NSManagedObjectModel, + normalizingMissingSelectedNode: Bool = false + ) throws -> ProtectedDataSnapshot { + var snapshot: ProtectedDataSnapshot? + try performWithObjectiveCExceptionBoundary( + operation: .protectedDataInspection + ) { + snapshot = try self + .inspectProtectedDataSnapshotWithoutExceptionBoundary( + at: url, + model: model, + normalizingMissingSelectedNode: + normalizingMissingSelectedNode + ) + } + + guard let snapshot else { + throw SubstrateProtectedDataInspectionError + .countUnavailable("protected-data snapshot") + } + return snapshot + } - while currentVersion != destinationVersion, let nextVersion = currentVersion.nextVersion() { - let currentModel = createManagedObjectModel(forResource: currentVersion.rawValue) - let nextModel = createManagedObjectModel(forResource: nextVersion.rawValue) + private func inspectProtectedDataSnapshotWithoutExceptionBoundary( + at url: URL, + model: NSManagedObjectModel, + normalizingMissingSelectedNode: Bool + ) throws -> ProtectedDataSnapshot { + let coordinator = NSPersistentStoreCoordinator( + managedObjectModel: model + ) + let store = try coordinator.addPersistentStore( + ofType: NSSQLiteStoreType, + configurationName: nil, + at: url, + options: [ + NSReadOnlyPersistentStoreOption: true, + NSMigratePersistentStoresAutomaticallyOption: false, + NSInferMappingModelAutomaticallyOption: false + ] + ) + defer { + try? coordinator.remove(store) + } + + let context = NSManagedObjectContext( + concurrencyType: .privateQueueConcurrencyType + ) + context.persistentStoreCoordinator = coordinator + + var result: Result? + context.performAndWait { + result = Result { + let rowCounts = try self.protectedEntityRowCounts( + model: model, + context: context + ) + let contacts = try self.semanticDigest( + entityName: "CDContact", + attributeNames: [ + "address", + "chainId", + "name" + ], + model: model, + context: context, + rowCount: rowCounts["CDContact"] ?? 0 + ) + let contactItems = try self.semanticDigest( + entityName: "CDContactItem", + attributeNames: [ + "identifier", + "peerAddress", + "peerName", + "targetAddress", + "updatedAt" + ], + model: model, + context: context, + rowCount: rowCounts["CDContactItem"] ?? 0 + ) + let transactionHistory = try self.semanticDigest( + entityName: "CDTransactionHistoryItem", + attributeNames: [ + "blockNumber", + "call", + "callName", + "fee", + "identifier", + "moduleName", + "receiver", + "sender", + "status", + "timestamp", + "txIndex" + ], + model: model, + context: context, + rowCount: + rowCounts["CDTransactionHistoryItem"] ?? 0 + ) + + guard let chainEntity = + model.entitiesByName["CDChain"] else { + throw SubstrateProtectedDataInspectionError + .entityUnavailable("CDChain") + } + guard model.entitiesByName["CDChainNode"] != nil else { + throw SubstrateProtectedDataInspectionError + .entityUnavailable("CDChainNode") + } + + for relationshipName in [ + "nodes", + "customNodes", + "selectedNode" + ] { + guard + chainEntity.relationshipsByName[ + relationshipName + ] != nil + else { + throw SubstrateProtectedDataInspectionError + .relationshipUnavailable( + "CDChain", + relationshipName + ) + } + } + + let nodeAttributeNames = [ + "apiKeyName", + "apiQueryName", + "name", + "url" + ] + var referencedNodeIDs = Set() + var topologyAccumulator = + ProtectedDataDigestAccumulator() + var totalRelationshipCount = 0 + + try self.forEachManagedObjectPage( + entityName: "CDChain", + rowCount: rowCounts["CDChain"] ?? 0, + context: context + ) { chains in + for chain in chains { + let defaultNodeCount = try self + .boundedRelationshipMemberCount( + inRelationship: "nodes", + of: chain + ) + let customNodeCount = try self + .boundedRelationshipMemberCount( + inRelationship: "customNodes", + of: chain + ) + try self.addRelationshipMembers( + defaultNodeCount, + to: &totalRelationshipCount + ) + try self.addRelationshipMembers( + customNodeCount, + to: &totalRelationshipCount + ) + + let defaultNodes = try self + .boundedManagedObjects( + inRelationship: "nodes", + of: chain, + expectedMemberCount: defaultNodeCount + ) + let customNodes = try self + .boundedManagedObjects( + inRelationship: "customNodes", + of: chain, + expectedMemberCount: customNodeCount + ) + + let defaultNodeDigest = try self + .semanticDigest( + objects: defaultNodes, + attributeNames: nodeAttributeNames, + category: "CDChain.nodes" + ) + let customNodeDigest = try self + .semanticDigest( + objects: customNodes, + attributeNames: nodeAttributeNames, + category: "CDChain.customNodes" + ) + let storedSelectedNode = chain.value( + forKey: "selectedNode" + ) as? NSManagedObject + let selectedNode: NSManagedObject? + if let storedSelectedNode { + selectedNode = storedSelectedNode + } else if normalizingMissingSelectedNode { + selectedNode = try self + .stableSemanticMinimum( + defaultNodes, + attributeNames: + nodeAttributeNames + ) + } else { + selectedNode = nil + } + if selectedNode != nil { + try self.addRelationshipMembers( + 1, + to: &totalRelationshipCount + ) + } + let selectedNodeDigest = try self + .semanticDigest( + objects: selectedNode.map { [$0] } ?? [], + attributeNames: nodeAttributeNames, + category: "CDChain.selectedNode" + ) + + referencedNodeIDs.formUnion( + defaultNodes.map(\.objectID) + ) + referencedNodeIDs.formUnion( + customNodes.map(\.objectID) + ) + if let selectedNode { + referencedNodeIDs.insert( + selectedNode.objectID + ) + } + + let chainIdentifier = try self + .canonicalAttributeValue( + in: chain, + key: "chainId" + ) + let topology = [ + "chain=\(chainIdentifier)", + """ + nodes=\(self.canonicalDigest(defaultNodeDigest)) + """, + """ + custom=\(self.canonicalDigest(customNodeDigest)) + """, + """ + selected=\(self.canonicalDigest(selectedNodeDigest)) + """ + ].joined(separator: ";") + try topologyAccumulator.append( + topology, + category: "CDChain.topology", + maximumByteCount: + self.protectedDataInspectionLimits + .maximumRecordByteCount + ) + } + } + + var orphanNodeAccumulator = + ProtectedDataDigestAccumulator() + try self.forEachManagedObjectPage( + entityName: "CDChainNode", + rowCount: rowCounts["CDChainNode"] ?? 0, + context: context + ) { nodes in + for node in nodes where + !referencedNodeIDs.contains(node.objectID) { + let record = try self.canonicalRecord( + for: node, + attributeNames: nodeAttributeNames + ) + try orphanNodeAccumulator.append( + record, + category: "CDChainNode.orphan", + maximumByteCount: + self.protectedDataInspectionLimits + .maximumRecordByteCount + ) + } + } - let mapping = try createMapping(from: currentModel, nextModel: nextModel) + return ProtectedDataSnapshot( + contacts: contacts, + contactItems: contactItems, + transactionHistory: transactionHistory, + chainNodeTopologies: + topologyAccumulator.finalize(), + orphanNodes: orphanNodeAccumulator.finalize() + ) + } + } - let manager = NSMigrationManager(sourceModel: currentModel, destinationModel: nextModel) + guard let result else { + throw SubstrateProtectedDataInspectionError + .countUnavailable("protected data snapshot") + } + return try result.get() + } - let nextStepURL = tmpMigrationDirURL.appendingPathComponent(UUID().uuidString) + private func stableSemanticMinimum( + _ objects: [NSManagedObject], + attributeNames: [String] + ) throws -> NSManagedObject? { + var selected: ( + record: String, + objectID: String, + object: NSManagedObject + )? - try manager.migrateStore( - from: currentURL, - sourceType: NSSQLiteStoreType, - options: nil, - with: mapping, - toDestinationURL: nextStepURL, - destinationType: NSSQLiteStoreType, - destinationOptions: nil + for object in objects { + let candidate = ( + record: try canonicalRecord( + for: object, + attributeNames: attributeNames + ), + objectID: object.objectID + .uriRepresentation().absoluteString, + object: object ) + guard let currentSelection = selected else { + selected = candidate + continue + } + if + candidate.record < currentSelection.record || + ( + candidate.record == + currentSelection.record && + candidate.objectID < + currentSelection.objectID + ) { + selected = candidate + } + } + + return selected?.object + } + + private func semanticDigest( + entityName: String, + attributeNames: [String], + model: NSManagedObjectModel, + context: NSManagedObjectContext, + rowCount: Int + ) throws -> ProtectedDataDigest { + guard model.entitiesByName[entityName] != nil else { + return ProtectedDataDigestAccumulator().finalize() + } - if currentURL != storeURL { - try NSPersistentStoreCoordinator.destroyStore(at: currentURL) + var accumulator = ProtectedDataDigestAccumulator() + try forEachManagedObjectPage( + entityName: entityName, + rowCount: rowCount, + context: context + ) { objects in + for object in objects { + let record = try self.canonicalRecord( + for: object, + attributeNames: attributeNames + ) + try accumulator.append( + record, + category: entityName, + maximumByteCount: + self.protectedDataInspectionLimits + .maximumRecordByteCount + ) } + } - currentVersion = nextVersion - currentURL = nextStepURL + return accumulator.finalize() + } + + private func semanticDigest( + objects: [NSManagedObject], + attributeNames: [String], + category: String + ) throws -> ProtectedDataDigest { + var accumulator = ProtectedDataDigestAccumulator() + for object in objects { + let record = try canonicalRecord( + for: object, + attributeNames: attributeNames + ) + try accumulator.append( + record, + category: category, + maximumByteCount: protectedDataInspectionLimits + .maximumRecordByteCount + ) + } + + return accumulator.finalize() + } + + private func canonicalRecord( + for object: NSManagedObject, + attributeNames: [String] + ) throws -> String { + try attributeNames.compactMap { key in + guard object.entity.attributesByName[key] != nil else { + return nil + } + + return """ + \(key)=\(try canonicalAttributeValue(in: object, key: key)) + """ + } + .joined(separator: "|") + } + + private func canonicalAttributeValue( + in object: NSManagedObject, + key: String + ) throws -> String { + guard object.entity.attributesByName[key] != nil else { + return "absent" + } + + guard let value = object.value(forKey: key) else { + return "nil" + } + + switch value { + case let string as String: + try requireValueByteLimit( + Data(string.utf8).count, + object: object, + key: key + ) + return "string:\(canonicalString(string))" + case let url as URL: + let absoluteString = url.absoluteString + try requireValueByteLimit( + Data(absoluteString.utf8).count, + object: object, + key: key + ) + return "url:\(canonicalString(absoluteString))" + case let data as Data: + try requireValueByteLimit( + data.count, + object: object, + key: key + ) + return "data:\(data.base64EncodedString())" + case let date as Date: + return "date:\(date.timeIntervalSinceReferenceDate.bitPattern)" + case let decimal as NSDecimalNumber: + let stringValue = decimal.stringValue + try requireValueByteLimit( + Data(stringValue.utf8).count, + object: object, + key: key + ) + return "decimal:\(canonicalString(stringValue))" + case let number as NSNumber: + let stringValue = number.stringValue + try requireValueByteLimit( + Data(stringValue.utf8).count, + object: object, + key: key + ) + return """ + number:\(String(cString: number.objCType)):\(canonicalString(stringValue)) + """ + default: + let className = NSStringFromClass( + type(of: value as AnyObject) + ) + throw SubstrateProtectedDataInspectionError + .unsupportedAttributeValue( + object.entity.name ?? "unknown", + key, + className + ) + } + } + + private func requireValueByteLimit( + _ byteCount: Int, + object: NSManagedObject, + key: String + ) throws { + guard + byteCount <= + protectedDataInspectionLimits.maximumValueByteCount + else { + throw SubstrateProtectedDataInspectionError + .valueByteLimitExceeded( + object.entity.name ?? "unknown", + key, + byteCount, + protectedDataInspectionLimits + .maximumValueByteCount + ) + } + } + + private func canonicalDigest( + _ digest: ProtectedDataDigest + ) -> String { + let words = digest.additiveWords.map { + String(format: "%016llx", $0) + }.joined(separator: ",") + return "\(digest.recordCount):\(words)" + } + + private func canonicalString(_ value: String) -> String { + Data(value.utf8).base64EncodedString() + } + + private func inspectStoreRowCounts( + at url: URL, + model: NSManagedObjectModel, + entityNames: Set, + operation: SubstrateStorageMigrationOperation + ) throws -> [String: Int] { + var counts: [String: Int]? + try performWithObjectiveCExceptionBoundary(operation: operation) { + counts = try self.inspectStoreRowCountsWithoutExceptionBoundary( + at: url, + model: model, + entityNames: entityNames + ) + } + + guard let counts else { + throw SubstrateProtectedDataInspectionError + .countUnavailable("store row counts") + } + return counts + } + + private func inspectStoreRowCountsWithoutExceptionBoundary( + at url: URL, + model: NSManagedObjectModel, + entityNames: Set + ) throws -> [String: Int] { + let coordinator = NSPersistentStoreCoordinator(managedObjectModel: model) + let store = try coordinator.addPersistentStore( + ofType: NSSQLiteStoreType, + configurationName: nil, + at: url, + options: [ + NSReadOnlyPersistentStoreOption: true, + NSMigratePersistentStoresAutomaticallyOption: false, + NSInferMappingModelAutomaticallyOption: false + ] + ) + defer { + try? coordinator.remove(store) } - try NSPersistentStoreCoordinator.replaceStore(at: storeURL, withStoreAt: currentURL) + let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) + context.persistentStoreCoordinator = coordinator + + var result: Result<[String: Int], Error>? + context.performAndWait { + result = Result { + var counts: [String: Int] = [:] + + for entityName in entityNames.sorted() { + let request = NSFetchRequest( + entityName: entityName + ) + counts[entityName] = try context.count(for: request) + } + + return counts + } + } - if currentURL != storeURL { - try NSPersistentStoreCoordinator.destroyStore(at: currentURL) + guard let result else { + throw SubstrateProtectedDataInspectionError + .countUnavailable("store row counts") } + return try result.get() } private func checkIfMigrationNeeded(to version: SubstrateStorageVersion) -> Bool { - let storageExists = fileManager.fileExists(atPath: storeURL.path) + if crashConsistentCacheRecovery.hasPendingTransaction { + return true + } - guard storageExists else { - return false + if crashConsistentStoreReplacer.hasPendingTransaction { + return true } - guard let metadata = NSPersistentStoreCoordinator.metadata(at: storeURL) else { - return false + do { + guard try crashConsistentStoreReplacer.liveStoreExistsSafely() else { + return false + } + } catch { + return true } - let compatibleVersion = compatibleVersionForStoreMetadata(metadata) + do { + let snapshot = try createPrivateSourceSnapshot() + defer { + try? fileManager.removeItem(at: snapshot.rootURL) + } + let compatibleStore = try loadCompatibleStore( + at: snapshot.workingStoreURL + ) + + guard compatibleStore.version == version else { + return true + } + + let repairedTransformableValueCount = + try sanitizeTransformableValues( + at: snapshot.workingStoreURL, + compatibleStore: compatibleStore + ) + + if compatibleStore.version == .version8 { + try inspectVersion8StartupGraphBounds( + at: snapshot.workingStoreURL, + model: compatibleStore.model + ) + _ = try inspectProtectedDataSnapshot( + at: snapshot.workingStoreURL, + model: compatibleStore.model + ) + } - return compatibleVersion != version + return repairedTransformableValueCount > 0 + } catch { + return true + } } - private func compatibleVersionForStoreMetadata(_ metadata: [String: Any]) -> SubstrateStorageVersion? { - let compatibleVersion = SubstrateStorageVersion.allCases.first { - let model = createManagedObjectModel(forResource: $0.rawValue) - return model.isConfiguration(withName: $0.rawValue, compatibleWithStoreMetadata: metadata) + private func loadCompatibleStore(at url: URL) throws -> CompatibleStore { + let metadata: [String: Any] + do { + var inspectedMetadata: [String: Any]? + try performWithObjectiveCExceptionBoundary( + operation: .sourceMetadata + ) { + inspectedMetadata = try NSPersistentStoreCoordinator + .metadataForPersistentStore( + ofType: NSSQLiteStoreType, + at: url, + options: nil + ) + } + metadata = inspectedMetadata ?? [:] + } catch let migrationError as SubstrateStorageMigrationError { + throw migrationError + } catch { + throw SubstrateStorageMigrationError.metadataUnreadable(storeURL, error) + } + + guard let compatibleStore = try compatibleStore(for: metadata) else { + throw SubstrateStorageMigrationError.unknownStoreVersion(storeURL) } - return compatibleVersion + return compatibleStore } - private func createManagedObjectModel(forResource resource: String) -> NSManagedObjectModel { - let bundle = Bundle.main + private func compatibleStore( + for metadata: [String: Any] + ) throws -> CompatibleStore? { + for version in SubstrateStorageVersion.allCases { + let model = try createManagedObjectModel(for: version) + + if model.isConfiguration( + withName: version.rawValue, + compatibleWithStoreMetadata: metadata + ) { + return CompatibleStore(version: version, model: model) + } + } + + return nil + } - let omoURL = bundle.url( - forResource: resource, + private func createManagedObjectModel( + for version: SubstrateStorageVersion + ) throws -> NSManagedObjectModel { + let omoURL = modelBundle.url( + forResource: version.rawValue, withExtension: "omo", subdirectory: modelDirectory ) - let momURL = bundle.url( - forResource: resource, + let momURL = modelBundle.url( + forResource: version.rawValue, withExtension: "mom", subdirectory: modelDirectory ) guard let modelURL = omoURL ?? momURL, - let model = NSManagedObjectModel(contentsOf: modelURL) else { - fatalError("Unable to load model in bundle for resource \(resource)") + let model = NSManagedObjectModel(contentsOf: modelURL) + else { + throw SubstrateStorageMigrationError.modelUnavailable(version) } return model @@ -152,48 +5916,311 @@ final class SubstrateStorageMigrator { private func createMapping( from sourceModel: NSManagedObjectModel, - nextModel: NSManagedObjectModel + sourceVersion: SubstrateStorageVersion, + nextModel: NSManagedObjectModel, + destinationVersion: SubstrateStorageVersion ) throws -> NSMappingModel { - let maybeCustomMapping = NSMappingModel( - from: [Bundle.main], + if let requiredMapping = requiredCustomMapping( + from: sourceVersion, + to: destinationVersion + ) { + return try loadRequiredCustomMapping( + requiredMapping, + sourceModel: sourceModel, + destinationModel: nextModel + ) + } + + let mapping = try NSMappingModel.inferredMappingModel( forSourceModel: sourceModel, destinationModel: nextModel ) - if let customMapping = maybeCustomMapping { + if sourceVersion == .version4, destinationVersion == .version5 { + try configureLegacyXcmTransforms(in: mapping) + } + + return mapping + } + + private func configureLegacyXcmTransforms( + in mapping: NSMappingModel + ) throws { + let entityNames = [ + "CDChainXcmConfig", + "CDXcmAvailableDestination" + ] + let policyClassName = NSStringFromClass( + SubstrateLegacyXcmMigrationPolicy.self + ) + var transformedEntityNames = Set() + + mapping.entityMappings = mapping.entityMappings.map { + inferredMapping in + guard + let entityName = inferredMapping.sourceEntityName, + entityNames.contains(entityName), + inferredMapping.destinationEntityName == entityName + else { + return inferredMapping + } + + transformedEntityNames.insert(entityName) + + // Merely assigning a policy to an inferred "transform" mapping + // does not make Core Data invoke that policy. Rebuild these two + // mappings as complete custom mappings, retaining every inferred + // property expression and both version hashes. + let customMapping = NSEntityMapping() + customMapping.name = "LegacyXcm_\(entityName)" + customMapping.mappingType = .customEntityMappingType + customMapping.sourceEntityName = inferredMapping.sourceEntityName + customMapping.destinationEntityName = + inferredMapping.destinationEntityName + customMapping.sourceEntityVersionHash = + inferredMapping.sourceEntityVersionHash + customMapping.destinationEntityVersionHash = + inferredMapping.destinationEntityVersionHash + customMapping.sourceExpression = NSExpression( + format: "FETCH(FUNCTION($manager, " + + "'fetchRequestForSourceEntityNamed:predicateString:', " + + "'\(entityName)', 'TRUEPREDICATE'), " + + "$manager.sourceContext, NO)" + ) + customMapping.attributeMappings = + inferredMapping.attributeMappings + customMapping.relationshipMappings = + inferredMapping.relationshipMappings + customMapping.entityMigrationPolicyClassName = policyClassName + return customMapping } - return try NSMappingModel.inferredMappingModel( + for entityName in entityNames + where !transformedEntityNames.contains(entityName) { + throw SubstrateLegacyXcmMappingError + .entityMappingUnavailable(entityName) + } + } + + private func requiredCustomMapping( + from sourceVersion: SubstrateStorageVersion, + to destinationVersion: SubstrateStorageVersion + ) -> RequiredCustomMapping? { + switch (sourceVersion, destinationVersion) { + case (.version1, .version2): + return RequiredCustomMapping( + resourceName: "SubstrateV2Mapping", + policyClassName: NSStringFromClass( + ChainSubstrateV2MigrationPolicy.self + ) + ) + case (.version3, .version4): + return RequiredCustomMapping( + resourceName: "SubstrateV3toV4", + policyClassName: NSStringFromClass( + ChainModelV4MigrationPolicy.self + ) + ) + default: + return nil + } + } + + private func loadRequiredCustomMapping( + _ requiredMapping: RequiredCustomMapping, + sourceModel: NSManagedObjectModel, + destinationModel: NSManagedObjectModel + ) throws -> NSMappingModel { + guard let mappingURL = modelBundle.url( + forResource: requiredMapping.resourceName, + withExtension: "cdm" + ) else { + throw SubstrateCustomMappingError.resourceUnavailable( + requiredMapping.resourceName + ) + } + + guard let mapping = NSMappingModel(contentsOf: mappingURL) else { + throw SubstrateCustomMappingError.resourceUnreadable( + requiredMapping.resourceName + ) + } + + let bundleMapping = NSMappingModel( + from: [modelBundle], forSourceModel: sourceModel, - destinationModel: nextModel + destinationModel: destinationModel ) + let entityHashesMatch = mapping.entityMappings.allSatisfy { entityMapping in + let sourceMatches = entityMapping.sourceEntityName.map { + sourceModel.entitiesByName[$0]?.versionHash == + entityMapping.sourceEntityVersionHash + } ?? true + let destinationMatches = entityMapping.destinationEntityName.map { + destinationModel.entitiesByName[$0]?.versionHash == + entityMapping.destinationEntityVersionHash + } ?? true + + return sourceMatches && destinationMatches + } + + guard bundleMapping != nil, entityHashesMatch else { + throw SubstrateCustomMappingError.incompatible( + requiredMapping.resourceName + ) + } + + let chainMapping = mapping.entityMappings.first { + $0.sourceEntityName == "CDChain" && + $0.destinationEntityName == "CDChain" + } + + guard + chainMapping?.entityMigrationPolicyClassName == + requiredMapping.policyClassName + else { + throw SubstrateCustomMappingError.policyUnavailable( + requiredMapping.resourceName, + requiredMapping.policyClassName + ) + } + + return mapping } - private func forceWALCheckpointingForStore(at storeURL: URL) { - let maybeMetadata = try? NSPersistentStoreCoordinator.metadataForPersistentStore( - ofType: NSSQLiteStoreType, - at: storeURL, - options: nil + private func forceWALCheckpointingForStore( + at storeURL: URL, + sourceModel: NSManagedObjectModel + ) throws { + do { + try performWithObjectiveCExceptionBoundary( + operation: .sourceCheckpoint + ) { + if let checkpointStoreHook = self.checkpointStoreHook { + try checkpointStoreHook(storeURL, sourceModel) + return + } + + let persistentStoreCoordinator = NSPersistentStoreCoordinator( + managedObjectModel: sourceModel + ) + let options = [NSSQLitePragmasOption: ["journal_mode": "DELETE"]] + let store = try persistentStoreCoordinator.addPersistentStore( + at: storeURL, + options: options + ) + try persistentStoreCoordinator.remove(store) + } + } catch let migrationError as SubstrateStorageMigrationError { + throw migrationError + } catch { + throw SubstrateStorageMigrationError.walCheckpointFailed(storeURL, error) + } + } + + private func recoverRebuildableCache( + after migrationError: SubstrateStorageMigrationError + ) throws -> URL? { + guard migrationError.allowsCacheRebuild else { + throw migrationError + } + + do { + return try crashConsistentCacheRecovery.quarantine() + } catch is SubstrateCacheRecoveryInterruption { + throw SubstrateCacheRecoveryInterruption + .simulatedProcessDeath + } catch { + throw SubstrateStorageMigrationError.cacheRecoveryFailed( + storeURL, + migrationError, + error + ) + } + } +} + +@objc(SubstrateLegacyXcmMigrationPolicy) +private final class SubstrateLegacyXcmMigrationPolicy: NSEntityMigrationPolicy { + private static let availableAssetEntityName = "CDXcmAvailableAsset" + + override func createRelationships( + forDestination destination: NSManagedObject, + in mapping: NSEntityMapping, + manager: NSMigrationManager + ) throws { + try super.createRelationships( + forDestination: destination, + in: mapping, + manager: manager ) - guard - let metadata = maybeMetadata, - let currentModel = NSManagedObjectModel.mergedModel( - from: [Bundle.main], - forStoreMetadata: metadata - ) else { + let relationshipName: String + switch mapping.sourceEntityName { + case "CDChainXcmConfig": + relationshipName = "availableAssets" + case "CDXcmAvailableDestination": + relationshipName = "assets" + default: return } - do { - let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: currentModel) + let entityName = mapping.sourceEntityName ?? "legacy XCM" + guard destination.entity.relationshipsByName[relationshipName] != nil else { + throw SubstrateLegacyXcmMappingError + .destinationRelationshipUnavailable( + entityName, + relationshipName + ) + } + + guard let source = manager.sourceInstances( + forEntityMappingName: mapping.name, + destinationInstances: [destination] + ).first else { + throw SubstrateLegacyXcmMappingError + .sourceAssociationUnavailable(entityName) + } - let options = [NSSQLitePragmasOption: ["journal_mode": "DELETE"]] - let store = try persistentStoreCoordinator.addPersistentStore(at: storeURL, options: options) - try persistentStoreCoordinator.remove(store) + let symbols: [String]? + do { + symbols = try SafeTransformableValueReader.read( + from: source, + key: relationshipName + ) } catch { - fatalError("Failed to force WAL checkpointing, error: \(error)") + // The disposable source is sanitized before migration. A residual + // decode failure must fail closed inside the outer ObjC boundary. + throw error + } + + guard let symbols else { + return + } + + guard + manager.destinationModel.entitiesByName[ + Self.availableAssetEntityName + ] != nil + else { + throw SubstrateLegacyXcmMappingError + .destinationEntityUnavailable( + Self.availableAssetEntityName + ) + } + + let destinationAssets = destination.mutableSetValue( + forKey: relationshipName + ) + + for symbol in symbols { + let availableAsset = NSEntityDescription.insertNewObject( + forEntityName: Self.availableAssetEntityName, + into: manager.destinationContext + ) + availableAsset.setValue(symbol, forKey: "symbol") + destinationAssets.add(availableAsset) } } } @@ -205,7 +6232,22 @@ extension SubstrateStorageMigrator: StorageMigrating { func migrate(_ completion: @escaping () -> Void) { DispatchQueue.global(qos: .utility).async { [weak self] in - self?.performMigration() + guard let self else { + return + } + + var completedSafely = false + + do { + try self.migrate() + completedSafely = true + } catch { + Logger.shared.error(error.localizedDescription) + } + + guard completedSafely else { + return + } DispatchQueue.main.async { completion() diff --git a/fearless/Common/Storage/Migration/UserStorage/MultiassetV2MigrationPolicy.swift b/fearless/Common/Storage/Migration/UserStorage/MultiassetV2MigrationPolicy.swift index d5fa4adeba..8fdb6bbfea 100644 --- a/fearless/Common/Storage/Migration/UserStorage/MultiassetV2MigrationPolicy.swift +++ b/fearless/Common/Storage/Migration/UserStorage/MultiassetV2MigrationPolicy.swift @@ -4,10 +4,14 @@ import IrohaCrypto class MultiassetV2MigrationPolicy: NSEntityMigrationPolicy { override func createDestinationInstances( - forSource _: NSManagedObject, - in _: NSEntityMapping, - manager _: NSMigrationManager + forSource source: NSManagedObject, + in mapping: NSEntityMapping, + manager: NSMigrationManager ) throws { - // TODO: tech debt + try super.createDestinationInstances( + forSource: source, + in: mapping, + manager: manager + ) } } diff --git a/fearless/Common/Storage/Migration/UserStorage/MultiassetV9MigrationPolicy.swift b/fearless/Common/Storage/Migration/UserStorage/MultiassetV9MigrationPolicy.swift index bac31a3458..b7ad898130 100644 --- a/fearless/Common/Storage/Migration/UserStorage/MultiassetV9MigrationPolicy.swift +++ b/fearless/Common/Storage/Migration/UserStorage/MultiassetV9MigrationPolicy.swift @@ -10,7 +10,11 @@ class MultiassetV9MigrationPolicy: NSEntityMigrationPolicy { ) throws { try super.createDestinationInstances(forSource: wallet, in: mapping, manager: manager) - guard let assetIdsEnabled = wallet.value(forKey: "assetIdsEnabled") as? [String] else { + let assetIdsEnabled: [String]? = try? SafeTransformableValueReader.read( + from: wallet, + key: "assetIdsEnabled" + ) + guard let assetIdsEnabled else { return } diff --git a/fearless/Common/Storage/Migration/UserStorage/SingleToMultiassetMigrationPolicy.swift b/fearless/Common/Storage/Migration/UserStorage/SingleToMultiassetMigrationPolicy.swift index 674f786791..150f0059b1 100644 --- a/fearless/Common/Storage/Migration/UserStorage/SingleToMultiassetMigrationPolicy.swift +++ b/fearless/Common/Storage/Migration/UserStorage/SingleToMultiassetMigrationPolicy.swift @@ -1,148 +1,334 @@ -import Foundation import CoreData -import SSFUtils -import SoraKeystore +import Foundation import IrohaCrypto +import SoraKeystore +import SSFUtils + +enum SingleToMultiassetMigrationError: LocalizedError { + case keystoreMigratorMissing + case settingsMigratorMissing + case sourceAddressMissing + case destinationAccountMissing + + var errorDescription: String? { + switch self { + case .keystoreMigratorMissing: + return "The v1-to-v2 migration is missing its keystore migrator" + case .settingsMigratorMissing: + return "The v1-to-v2 migration is missing its settings migrator" + case .sourceAddressMissing: + return "The v1-to-v2 migration encountered an account without an address" + case .destinationAccountMissing: + return "The v1-to-v2 migration failed to create a destination account" + } + } +} + +final class SingleToMultiassetMigrationPolicy: NSEntityMigrationPolicy { + private struct LegacySecrets { + let entropy: Data? + let seed: Data? + let secretKey: Data? + let derivationPath: Data? + + var signingRank: Int { + if secretKey != nil { + return 3 + } + + if entropy != nil { + return 2 + } + + if seed != nil { + return 1 + } + + return 0 + } + + var materialCount: Int { + [entropy, seed, secretKey, derivationPath] + .compactMap { $0 } + .count + } + } + + private struct SourceCandidate { + let address: AccountAddress + let name: Any? + let publicKey: Any? + let cryptoType: Any? + let secrets: LegacySecrets + + func isPreferred(over candidate: SourceCandidate) -> Bool { + if secrets.signingRank != candidate.secrets.signingRank { + return secrets.signingRank > candidate.secrets.signingRank + } + + if secrets.materialCount != candidate.secrets.materialCount { + return secrets.materialCount > candidate.secrets.materialCount + } + + return address < candidate.address + } + } + + private struct AccountMigration { + let metaAccount: NSManagedObject + let metaId: String + var preferredCandidate: SourceCandidate + } + + private var isSelected = false + private var order: Int32 = 0 + private var migratedAccounts = [Data: AccountMigration]() + + private lazy var addressFactory = SS58AddressFactory() + + override func createDestinationInstances( + forSource accountItem: NSManagedObject, + in mapping: NSEntityMapping, + manager: NSMigrationManager + ) throws { + guard + let keystoreMigrator = manager + .userInfo?[UserStorageMigratorKeys.keystoreMigrator] as? KeystoreMigrating + else { + throw SingleToMultiassetMigrationError.keystoreMigratorMissing + } + + try keystoreMigrator.registerLegacyAccountRow() + + guard let sourceAddress = accountItem.value(forKey: "identifier") as? AccountAddress else { + throw SingleToMultiassetMigrationError.sourceAddressMissing + } + + let accountId = try addressFactory.accountId(from: sourceAddress) + let secrets = try loadLegacySecrets( + for: sourceAddress, + keystoreMigrator: keystoreMigrator + ) + let candidate = SourceCandidate( + address: sourceAddress, + name: accountItem.value(forKey: "username"), + publicKey: accountItem.value(forKey: "publicKey"), + cryptoType: accountItem.value(forKey: "cryptoType"), + secrets: secrets + ) + + if var accountMigration = migratedAccounts[accountId] { + if candidate.isPreferred(over: accountMigration.preferredCandidate) { + applyMappedValues( + from: candidate, + to: accountMigration.metaAccount + ) + accountMigration.preferredCandidate = candidate + migratedAccounts[accountId] = accountMigration + } + + try migrateKeystore( + secrets, + sourceAddress: sourceAddress, + metaId: accountMigration.metaId, + metaAccount: accountMigration.metaAccount, + keystoreMigrator: keystoreMigrator + ) + return + } + + try super.createDestinationInstances( + forSource: accountItem, + in: mapping, + manager: manager + ) + + guard + let metaAccount = manager.destinationInstances( + forEntityMappingName: mapping.name, + sourceInstances: [accountItem] + ).first + else { + throw SingleToMultiassetMigrationError.destinationAccountMissing + } + + let metaId = UUID().uuidString + metaAccount.setValue(metaId, forKey: "metaId") + metaAccount.setValue(accountId.toHex(), forKey: "substrateAccountId") + metaAccount.setValue(!isSelected, forKey: "isSelected") + metaAccount.setValue(order, forKey: "order") + + migratedAccounts[accountId] = AccountMigration( + metaAccount: metaAccount, + metaId: metaId, + preferredCandidate: candidate + ) + + isSelected = true + order += 1 + + try migrateKeystore( + secrets, + sourceAddress: sourceAddress, + metaId: metaId, + metaAccount: metaAccount, + keystoreMigrator: keystoreMigrator + ) + } + + override func end( + _ mapping: NSEntityMapping, + manager: NSMigrationManager + ) throws { + guard + let settingsMigrator = manager + .userInfo?[UserStorageMigratorKeys.settingsMigrator] as? SettingsMigrating + else { + throw SingleToMultiassetMigrationError.settingsMigratorMissing + } + + settingsMigrator.remove(key: SettingsKey.selectedAccount.rawValue) + settingsMigrator.remove(key: SettingsKey.selectedConnection.rawValue) + + try super.end(mapping, manager: manager) + } + + private func loadLegacySecrets( + for sourceAddress: AccountAddress, + keystoreMigrator: KeystoreMigrating + ) throws -> LegacySecrets { + try LegacySecrets( + entropy: keystoreMigrator.fetchKey( + for: KeystoreTag.entropyTagForAddress(sourceAddress) + ), + seed: keystoreMigrator.fetchKey( + for: KeystoreTag.seedTagForAddress(sourceAddress) + ), + secretKey: keystoreMigrator.fetchKey( + for: KeystoreTag.secretKeyTagForAddress(sourceAddress) + ), + derivationPath: keystoreMigrator.fetchKey( + for: KeystoreTag.deriviationTagForAddress(sourceAddress) + ) + ) + } + + private func applyMappedValues( + from candidate: SourceCandidate, + to metaAccount: NSManagedObject + ) { + metaAccount.setValue(candidate.name, forKey: "name") + metaAccount.setValue(candidate.publicKey, forKey: "substratePublicKey") + metaAccount.setValue(candidate.cryptoType, forKey: "substrateCryptoType") + } + + private func migrateKeystore( + _ secrets: LegacySecrets, + sourceAddress: AccountAddress, + metaId: String, + metaAccount: NSManagedObject, + keystoreMigrator: KeystoreMigrating + ) throws { + let oldEntropyTag = KeystoreTag.entropyTagForAddress(sourceAddress) + if let entropy = secrets.entropy { + let newEntropyTag = KeystoreTagV2.entropyTagForMetaId(metaId) + + try stage( + key: entropy, + for: newEntropyTag, + keystoreMigrator: keystoreMigrator + ) + try scheduleDeletion( + oldEntropyTag, + keystoreMigrator: keystoreMigrator + ) + + let ethereumDerivationPath = DerivationPathConstants.defaultEthereum + let secrets = try EthereumAccountImportWrapper().importEntropy( + entropy, + derivationPath: ethereumDerivationPath + ) + + try stage( + key: secrets.seed, + for: KeystoreTagV2.ethereumSeedTagForMetaId(metaId), + keystoreMigrator: keystoreMigrator + ) + try stage( + key: secrets.keypair.privateKey().rawData(), + for: KeystoreTagV2.ethereumSecretKeyTagForMetaId(metaId), + keystoreMigrator: keystoreMigrator + ) + + if let derivationPathData = ethereumDerivationPath.data(using: .utf8) { + try stage( + key: derivationPathData, + for: KeystoreTagV2.ethereumDerivationTagForMetaId(metaId), + keystoreMigrator: keystoreMigrator + ) + } + + let rawPublicKey = secrets.keypair.publicKey().rawData() + metaAccount.setValue(rawPublicKey, forKey: "ethereumPublicKey") + + let ethereumAddress = try rawPublicKey.ethereumAddressFromPublicKey() + metaAccount.setValue(ethereumAddress.toHex(), forKey: "ethereumAddress") + } + + let oldSeedTag = KeystoreTag.seedTagForAddress(sourceAddress) + if let seed = secrets.seed { + try stage( + key: seed, + for: KeystoreTagV2.substrateSeedTagForMetaId(metaId), + keystoreMigrator: keystoreMigrator + ) + try scheduleDeletion( + oldSeedTag, + keystoreMigrator: keystoreMigrator + ) + } + + let oldSecretKeyTag = KeystoreTag.secretKeyTagForAddress(sourceAddress) + if let secretKey = secrets.secretKey { + try stage( + key: secretKey, + for: KeystoreTagV2.substrateSecretKeyTagForMetaId(metaId), + keystoreMigrator: keystoreMigrator + ) + try scheduleDeletion( + oldSecretKeyTag, + keystoreMigrator: keystoreMigrator + ) + } + + let oldDerivationTag = KeystoreTag.deriviationTagForAddress(sourceAddress) + if let derivationPath = secrets.derivationPath { + try stage( + key: derivationPath, + for: KeystoreTagV2.substrateDerivationTagForMetaId(metaId), + keystoreMigrator: keystoreMigrator + ) + try scheduleDeletion( + oldDerivationTag, + keystoreMigrator: keystoreMigrator + ) + } + } + + private func stage( + key: Data, + for identifier: String, + keystoreMigrator: KeystoreMigrating + ) throws { + keystoreMigrator.save(key: key, for: identifier) + try keystoreMigrator.throwIfResourceLimitExceeded() + } -// class SingleToMultiassetMigrationPolicy: NSEntityMigrationPolicy { -// var isSelected: Bool = false -// var order: Int32 = 0 -// private var privateKeysUsed: [Data] = [] -// -// private lazy var addressFactory = SS58AddressFactory() -// -// override func createDestinationInstances( -// forSource accountItem: NSManagedObject, -// in mapping: NSEntityMapping, -// manager: NSMigrationManager -// ) throws { -// guard let keystoreMigrator = manager -// .userInfo?[UserStorageMigratorKeys.keystoreMigrator] as? KeystoreMigrating else { -// fatalError("No keystore migrator found in context") -// } -// -// guard let sourceAddress = accountItem.value(forKey: "identifier") as? AccountAddress else { -// fatalError("Unexpected empty source address") -// } -// -// let accountId = try addressFactory.accountId(from: sourceAddress) -// -// if privateKeysUsed.contains(accountId) { -// return -// } -// -// try super.createDestinationInstances(forSource: accountItem, in: mapping, manager: manager) -// -// guard let metaAccount = manager.destinationInstances( -// forEntityMappingName: mapping.name, -// sourceInstances: [accountItem] -// ).first else { -// return -// } -// -// privateKeysUsed.append(accountId) -// -// let metaId = UUID().uuidString -// metaAccount.setValue(metaId, forKey: "metaId") -// -// metaAccount.setValue(accountId.toHex(), forKey: "substrateAccountId") -// -// if !isSelected { -// isSelected = true -// -// metaAccount.setValue(true, forKey: "isSelected") -// } else { -// metaAccount.setValue(false, forKey: "isSelected") -// } -// -// metaAccount.setValue(order, forKey: "order") -// order += 1 -// -// if let ethereumPublicKey = try migrateKeystore( -// for: sourceAddress, -// metaId: metaId, -// keystoreMigrator: keystoreMigrator -// ) { -// let rawPublicKey = ethereumPublicKey.rawData() -// metaAccount.setValue(rawPublicKey, forKey: "ethereumPublicKey") -// -// let ethereumAddress = try rawPublicKey.ethereumAddressFromPublicKey() -// metaAccount.setValue(ethereumAddress.toHex(), forKey: "ethereumAddress") -// } -// } -// -// override func end(_ mapping: NSEntityMapping, manager: NSMigrationManager) throws { -// guard let settingsMigrator = manager -// .userInfo?[UserStorageMigratorKeys.settingsMigrator] as? SettingsMigrating else { -// fatalError("No settings migrator found in context") -// } -// -// settingsMigrator.remove(key: SettingsKey.selectedAccount.rawValue) -// settingsMigrator.remove(key: SettingsKey.selectedConnection.rawValue) -// -// try super.end(mapping, manager: manager) -// } -// -// // MARK: Private -// -// private func migrateKeystore( -// for sourceAddress: AccountAddress, -// metaId: String, -// keystoreMigrator: KeystoreMigrating -// ) throws -> IRPublicKeyProtocol? { -// var publicKey: IRPublicKeyProtocol? -// -// let oldEntropyTag = KeystoreTag.entropyTagForAddress(sourceAddress) -// if let entropy = keystoreMigrator.fetchKey(for: oldEntropyTag) { -// let newEntropyTag = KeystoreTagV2.entropyTagForMetaId(metaId) -// -// keystoreMigrator.deleteKey(for: oldEntropyTag) -// keystoreMigrator.save(key: entropy, for: newEntropyTag) -// -// let ethereumDPString = DerivationPathConstants.defaultEthereum -// let secrets = try EthereumAccountImportWrapper().importEntropy( -// entropy, -// derivationPath: ethereumDPString -// ) -// -// let ethSeedTag = KeystoreTagV2.ethereumSeedTagForMetaId(metaId) -// keystoreMigrator.save(key: secrets.seed, for: ethSeedTag) -// -// let ethSecretKeyTag = KeystoreTagV2.ethereumSecretKeyTagForMetaId(metaId) -// keystoreMigrator.save(key: secrets.keypair.privateKey().rawData(), for: ethSecretKeyTag) -// -// if let ethereumDP = ethereumDPString.data(using: .utf8) { -// let ethDPTag = KeystoreTagV2.ethereumDerivationTagForMetaId(metaId) -// keystoreMigrator.save(key: ethereumDP, for: ethDPTag) -// } -// -// publicKey = secrets.keypair.publicKey() -// } -// -// let oldSeedTag = KeystoreTag.seedTagForAddress(sourceAddress) -// if let seed = keystoreMigrator.fetchKey(for: oldSeedTag) { -// let newSeedTag = KeystoreTagV2.substrateSeedTagForMetaId(metaId) -// -// keystoreMigrator.deleteKey(for: oldSeedTag) -// keystoreMigrator.save(key: seed, for: newSeedTag) -// } -// -// let oldSecretKeyTag = KeystoreTag.secretKeyTagForAddress(sourceAddress) -// if let secretKey = keystoreMigrator.fetchKey(for: oldSecretKeyTag) { -// let newSecretKeyTag = KeystoreTagV2.substrateSecretKeyTagForMetaId(metaId) -// -// keystoreMigrator.deleteKey(for: oldSecretKeyTag) -// keystoreMigrator.save(key: secretKey, for: newSecretKeyTag) -// } -// -// let oldDPTag = KeystoreTag.deriviationTagForAddress(sourceAddress) -// if let derivationPath = keystoreMigrator.fetchKey(for: oldDPTag) { -// keystoreMigrator.deleteKey(for: oldDPTag) -// -// let newDPTag = KeystoreTagV2.substrateDerivationTagForMetaId(metaId) -// keystoreMigrator.save(key: derivationPath, for: newDPTag) -// } -// -// return publicKey -// } -// } + private func scheduleDeletion( + _ identifier: String, + keystoreMigrator: KeystoreMigrating + ) throws { + keystoreMigrator.deleteKey(for: identifier) + try keystoreMigrator.throwIfResourceLimitExceeded() + } +} diff --git a/fearless/Common/Storage/Migration/UserStorage/StorageMigrator+Sync.swift b/fearless/Common/Storage/Migration/UserStorage/StorageMigrator+Sync.swift index 9b6c8bad09..cdce3919a3 100644 --- a/fearless/Common/Storage/Migration/UserStorage/StorageMigrator+Sync.swift +++ b/fearless/Common/Storage/Migration/UserStorage/StorageMigrator+Sync.swift @@ -3,12 +3,10 @@ import Foundation // TODO: Move this logic to app loading state extension UserStorageMigrator: Migrating { func migrate() throws { - guard requiresMigration() else { - return - } - - performMigration() + let migrationPerformed = try performMigration() - Logger.shared.info("Db migration completed") + if migrationPerformed { + Logger.shared.info("Db migration completed") + } } } diff --git a/fearless/Common/Storage/Migration/UserStorage/StorageMigrator.swift b/fearless/Common/Storage/Migration/UserStorage/StorageMigrator.swift index 905543fcdc..46cf9239f6 100644 --- a/fearless/Common/Storage/Migration/UserStorage/StorageMigrator.swift +++ b/fearless/Common/Storage/Migration/UserStorage/StorageMigrator.swift @@ -1,5 +1,7 @@ -import Foundation import CoreData +import CryptoKit +import Foundation +import IrohaCrypto import SoraKeystore protocol StorageMigrating { @@ -12,13 +14,257 @@ enum UserStorageMigratorKeys { static let settingsMigrator = "settingsMigrator" } +enum UserStorageMigrationError: LocalizedError { + case metadataUnreadable(URL, Error) + case unknownStoreVersion(URL) + case modelUnavailable(UserStorageVersion) + case migrationPathUnavailable(UserStorageVersion, UserStorageVersion) + case objectiveCException + case privateSourceRepairFailed + case stagedStoreValidationFailed + + var errorDescription: String? { + switch self { + case .metadataUnreadable: + return "Unable to read user store metadata safely" + case .unknownStoreVersion: + return "Unsupported user store version" + case let .modelUnavailable(version): + return "User store model is unavailable for \(version.rawValue)" + case let .migrationPathUnavailable(source, destination): + return "No user store migration path from \(source.rawValue) to \(destination.rawValue)" + case .objectiveCException: + return "A stored value could not be processed safely" + case .privateSourceRepairFailed: + return "The private user-storage copy could not be repaired safely" + case .stagedStoreValidationFailed: + return "The migrated user store did not pass integrity validation" + } + } +} + +struct UserStorageIntegrityValidationLimits: Equatable { + static let production = UserStorageIntegrityValidationLimits( + maximumRowsPerEntity: 250_000, + maximumRowsAcrossStore: 1_000_000, + fetchBatchSize: 256, + maximumCanonicalRowBytes: 1 * 1024 * 1024, + maximumAttributeBytes: 256 * 1024, + maximumRelationshipDestinations: 10000, + maximumTransformableElements: 4096, + maximumTransformableArchiveBytes: 1 * 1024 * 1024, + maximumRelationshipDestinationsAcrossStore: 4_000_000 + ) + + let maximumRowsPerEntity: Int + let maximumRowsAcrossStore: Int + let fetchBatchSize: Int + let maximumCanonicalRowBytes: Int + let maximumAttributeBytes: Int + let maximumRelationshipDestinations: Int + let maximumTransformableElements: Int + let maximumTransformableArchiveBytes: Int + let maximumRelationshipDestinationsAcrossStore: Int + + init( + maximumRowsPerEntity: Int, + maximumRowsAcrossStore: Int, + fetchBatchSize: Int, + maximumCanonicalRowBytes: Int, + maximumAttributeBytes: Int, + maximumRelationshipDestinations: Int, + maximumTransformableElements: Int, + maximumTransformableArchiveBytes: Int = + 1 * 1024 * 1024, + maximumRelationshipDestinationsAcrossStore: Int = + 4_000_000 + ) { + self.maximumRowsPerEntity = max(1, maximumRowsPerEntity) + self.maximumRowsAcrossStore = max(1, maximumRowsAcrossStore) + self.fetchBatchSize = max(1, fetchBatchSize) + self.maximumCanonicalRowBytes = max(1, maximumCanonicalRowBytes) + self.maximumAttributeBytes = max(1, maximumAttributeBytes) + self.maximumRelationshipDestinations = max( + 1, + maximumRelationshipDestinations + ) + self.maximumTransformableElements = max( + 1, + maximumTransformableElements + ) + self.maximumTransformableArchiveBytes = max( + 1, + maximumTransformableArchiveBytes + ) + self.maximumRelationshipDestinationsAcrossStore = max( + 1, + maximumRelationshipDestinationsAcrossStore + ) + } +} + final class UserStorageMigrator { + private struct IntegrityField: Equatable { + let name: String + let value: String + + var sortKey: String { + "\(Data(name.utf8).base64EncodedString()):\(value)" + } + } + + private struct IntegrityIdentity: Equatable { + let entityName: String + let fields: [IntegrityField] + + var sortKey: String { + [ + Data(entityName.utf8).base64EncodedString(), + fields.map(\.sortKey).joined(separator: "|") + ].joined(separator: ":") + } + } + + private struct IntegrityRelationship: Equatable { + let name: String + let destinations: [IntegrityIdentity] + + var sortKey: String { + [ + Data(name.utf8).base64EncodedString(), + destinations.map(\.sortKey).joined(separator: ",") + ].joined(separator: ":") + } + } + + private struct IntegrityRow: Equatable { + let identity: IntegrityIdentity + let relationships: [IntegrityRelationship] + + var sortKey: String { + [ + identity.sortKey, + relationships.map(\.sortKey).joined(separator: "|") + ].joined(separator: "#") + } + } + + private struct IntegrityDigest: Equatable { + let rowCount: Int + let xor: Data + let sum: Data + } + + private struct IntegrityDigestAccumulator { + private(set) var rowCount = 0 + private var xor = [UInt8]( + repeating: 0, + count: SHA256.byteCount + ) + private var sum = [UInt8]( + repeating: 0, + count: SHA256.byteCount + ) + + mutating func append(canonicalRow: String) { + let digest = Array( + SHA256.hash(data: Data(canonicalRow.utf8)) + ) + var carry = 0 + + for index in digest.indices.reversed() { + xor[index] ^= digest[index] + + let total = + Int(sum[index]) + + Int(digest[index]) + + carry + sum[index] = UInt8(truncatingIfNeeded: total) + carry = total >> 8 + } + + rowCount += 1 + } + + var digest: IntegrityDigest { + IntegrityDigest( + rowCount: rowCount, + xor: Data(xor), + sum: Data(sum) + ) + } + } + + private struct StagedStoreIntegritySnapshot: Equatable { + let rowsByEntity: [String: IntegrityDigest] + } + + private struct StagedStoreIntegrityAllowlist { + static let none = StagedStoreIntegrityAllowlist( + entityNames: [], + attributeNamesByEntity: [:], + relationshipNamesByEntity: [:] + ) + + let entityNames: Set + let attributeNamesByEntity: [String: Set] + let relationshipNamesByEntity: [String: Set] + } + + private enum StagedStoreSemanticSnapshot: Equatable { + case legacyAccountIds([String]) + case assetVisibility( + walletRows: IntegrityDigest, + visibilityRowCount: Int + ) + case networkManagementFilters(IntegrityDigest) + } + + private static let resilientMetaAccountTransformableKeys = [ + "assetFilterOptions", + "assetIdsEnabled", + "assetKeysOrder", + "favouriteChainIds", + "unusedChainIds" + ] + let storeURL: URL let modelDirectory: String let keystore: KeystoreProtocol let settings: SettingsManagerProtocol let fileManager: FileManager let targetVersion: UserStorageVersion + let modelBundle: Bundle + private let metadataReader: (URL) throws -> [String: Any] + private let crashConsistentStoreReplacer: CrashConsistentStoreReplacer + private let keystoreMigratorFactory: ( + UserStorageVersion, + UserStorageVersion, + KeystoreProtocol + ) -> KeystoreMigrating + private let settingsMigratorFactory: ( + UserStorageVersion, + UserStorageVersion, + SettingsManagerProtocol + ) -> SettingsMigrating + private let stagedStoreWillValidate: ( + URL, + UserStorageVersion + ) throws -> Void + private let privateSourceObjectIDPageDidFetch: ( + Int, + Int + ) -> Void + private let integrityObjectPageDidFetch: ( + Int, + Int + ) -> Void + private let privateSourceCopyLimits: + SQLiteStoreFamilyCopyLimits + private let privateSourceAvailableCapacityProvider: + ((URL) throws -> UInt64)? + private let integrityValidationLimits: + UserStorageIntegrityValidationLimits init( targetVersion: UserStorageVersion, @@ -26,7 +272,65 @@ final class UserStorageMigrator { modelDirectory: String, keystore: KeystoreProtocol, settings: SettingsManagerProtocol, - fileManager: FileManager + fileManager: FileManager, + modelBundle: Bundle = .main, + integrityValidationLimits: + UserStorageIntegrityValidationLimits = .production, + privateSourceCopyLimits: + SQLiteStoreFamilyCopyLimits = + .userStorageProduction, + privateSourceAvailableCapacityProvider: + ((URL) throws -> UInt64)? = nil, + metadataReader: @escaping (URL) throws -> [String: Any] = { url in + try NSPersistentStoreCoordinator.metadataForPersistentStore( + ofType: NSSQLiteStoreType, + at: url, + options: nil + ) + }, + storeReplacer: @escaping (URL, URL) throws -> Void = { targetURL, sourceURL in + try NSPersistentStoreCoordinator.replaceStore( + at: targetURL, + withStoreAt: sourceURL + ) + }, + keystoreMigratorFactory: @escaping ( + UserStorageVersion, + UserStorageVersion, + KeystoreProtocol + ) -> KeystoreMigrating = { source, destination, keystore in + KeystoreMigrator( + sourceVersion: source, + destinationVersion: destination, + keystore: keystore + ) + }, + settingsMigratorFactory: @escaping ( + UserStorageVersion, + UserStorageVersion, + SettingsManagerProtocol + ) -> SettingsMigrating = { source, destination, settings in + SettingsMigrator( + sourceVersion: source, + destinationVersion: destination, + settings: settings + ) + }, + stagedStoreWillValidate: @escaping ( + URL, + UserStorageVersion + ) throws -> Void = { _, _ in }, + storeReplacementBoundaryHook: @escaping ( + CrashConsistentStoreReplacementBoundary + ) throws -> Void = { _ in }, + privateSourceObjectIDPageDidFetch: @escaping ( + Int, + Int + ) -> Void = { _, _ in }, + integrityObjectPageDidFetch: @escaping ( + Int, + Int + ) -> Void = { _, _ in } ) { self.targetVersion = targetVersion self.storeURL = storeURL @@ -34,69 +338,146 @@ final class UserStorageMigrator { self.keystore = keystore self.settings = settings self.fileManager = fileManager + self.modelBundle = modelBundle + self.integrityValidationLimits = integrityValidationLimits + self.privateSourceCopyLimits = + privateSourceCopyLimits + self.privateSourceAvailableCapacityProvider = + privateSourceAvailableCapacityProvider + self.metadataReader = metadataReader + crashConsistentStoreReplacer = CrashConsistentStoreReplacer( + storeURL: storeURL, + fileManager: fileManager, + storeReplacer: storeReplacer, + boundaryHook: storeReplacementBoundaryHook, + maximumStoreFamilyByteCount: + privateSourceCopyLimits.maximumFamilyByteCount, + minimumFreeStorageReserveByteCount: + privateSourceCopyLimits + .minimumFreeStorageReserveByteCount, + availableCapacityProvider: + privateSourceAvailableCapacityProvider + ) + self.keystoreMigratorFactory = keystoreMigratorFactory + self.settingsMigratorFactory = settingsMigratorFactory + self.stagedStoreWillValidate = stagedStoreWillValidate + self.privateSourceObjectIDPageDidFetch = + privateSourceObjectIDPageDidFetch + self.integrityObjectPageDidFetch = + integrityObjectPageDidFetch } - func performMigration() { - forceWALCheckpointingForStore(at: storeURL) - - let maybeMetadata = try? NSPersistentStoreCoordinator.metadataForPersistentStore( - ofType: NSSQLiteStoreType, - at: storeURL, - options: nil - ) + @discardableResult + func performMigration() throws -> Bool { + try reconcilePendingStoreReplacement() - guard - let metadata = maybeMetadata, - let sourceVersion = compatibleVersionForStoreMetadata(metadata) else { - fatalError("Unknown store version at URL \(storeURL)") + guard try crashConsistentStoreReplacer.liveStoreExistsSafely() else { + return false } - do { - let migrationDirName = UUID().uuidString - let tmpMigrationDirURL = URL( - fileURLWithPath: NSTemporaryDirectory(), - isDirectory: true - ).appendingPathComponent(migrationDirName) + return try withPrivateSourceCopy { privateSourceURL, tmpMigrationDirURL in + let sourceVersion = try readSourceVersion( + at: privateSourceURL + ) + + try KeystoreMigrator.recoverPendingCleanup( + keystore: keystore, + currentVersion: sourceVersion + ) + try SettingsMigrator.recoverPendingCleanup( + settings: settings, + currentVersion: sourceVersion + ) + + let privateSourceWasRepaired = + try sanitizePrivateSourceCopy( + at: privateSourceURL, + version: sourceVersion + ) + + guard sourceVersion != targetVersion else { + guard privateSourceWasRepaired else { + return false + } - try fileManager.createDirectory(at: tmpMigrationDirURL, withIntermediateDirectories: true) + try replaceCurrentVersionStore( + with: privateSourceURL, + version: sourceVersion + ) + return true + } try performMigration( from: sourceVersion, to: targetVersion, - storeURL: storeURL, + sourceStoreURL: privateSourceURL, + destinationStoreURL: storeURL, tmpMigrationDirURL: tmpMigrationDirURL ) - try fileManager.removeItem(at: tmpMigrationDirURL) - } catch { - fatalError("Migration failed with error \(error)") + return true } } private func performMigration( from sourceVersion: UserStorageVersion, to destinationVersion: UserStorageVersion, - storeURL: URL, + sourceStoreURL: URL, + destinationStoreURL: URL, tmpMigrationDirURL: URL ) throws { var currentVersion = sourceVersion - var currentURL = storeURL + var currentURL = sourceStoreURL - let keystoreMigrator = KeystoreMigrator( - sourceVersion: sourceVersion, - destinationVersion: destinationVersion, - keystore: keystore + let keystoreMigrator = keystoreMigratorFactory( + sourceVersion, + destinationVersion, + keystore ) - let settingsMigrator = SettingsMigrator( - sourceVersion: sourceVersion, - destinationVersion: destinationVersion, - settings: settings + let settingsMigrator = settingsMigratorFactory( + sourceVersion, + destinationVersion, + settings ) - while currentVersion != destinationVersion, let nextVersion = currentVersion.nextVersion() { - let currentModel = createManagedObjectModel(forResource: currentVersion.rawValue) - let nextModel = createManagedObjectModel(forResource: nextVersion.rawValue) + while currentVersion != destinationVersion { + guard let nextVersion = currentVersion.nextVersion() else { + throw UserStorageMigrationError.migrationPathUnavailable( + currentVersion, + destinationVersion + ) + } + + let currentModel = try createManagedObjectModel(for: currentVersion) + let nextModel = try createManagedObjectModel(for: nextVersion) + let integrityAllowlist = try commonIntegrityAllowlist( + from: currentVersion, + to: nextVersion + ) + let expectedHopIntegrity = try stagedStoreIntegritySnapshot( + at: currentURL, + model: currentModel, + comparedWith: nextModel, + allowlist: integrityAllowlist + ) + let expectedSemanticIntegrity = + try stagedStoreSemanticSnapshot( + at: currentURL, + model: currentModel, + storeVersion: currentVersion, + migratingFrom: currentVersion, + to: nextVersion + ) + let expectedMetaAccountCount: Int? + if shouldValidateMetaAccountCount(from: currentVersion) { + expectedMetaAccountCount = try stagedMetaAccountCount( + at: currentURL, + model: currentModel + ) + } else { + expectedMetaAccountCount = nil + } try keystoreMigrator.switchVersion() try settingsMigrator.switchVersion() @@ -112,138 +493,2317 @@ final class UserStorageMigrator { let nextStepURL = tmpMigrationDirURL.appendingPathComponent(UUID().uuidString) - try manager.migrateStore( - from: currentURL, - sourceType: NSSQLiteStoreType, - options: nil, - with: mapping, - toDestinationURL: nextStepURL, - destinationType: NSSQLiteStoreType, - destinationOptions: nil - ) + try performWithObjectiveCExceptionBoundary( + phase: "staged migration" + ) { + try manager.migrateStore( + from: currentURL, + sourceType: NSSQLiteStoreType, + options: nil, + with: mapping, + toDestinationURL: nextStepURL, + destinationType: NSSQLiteStoreType, + destinationOptions: nil + ) + } - if currentURL != storeURL { - try NSPersistentStoreCoordinator.destroyStore(at: currentURL) + try stagedStoreWillValidate(nextStepURL, nextVersion) + + if let expectedMetaAccountCount { + let actualMetaAccountCount = try stagedMetaAccountCount( + at: nextStepURL, + model: nextModel + ) + + guard + actualMetaAccountCount == expectedMetaAccountCount + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + } + + let actualSemanticIntegrity = + try stagedStoreSemanticSnapshot( + at: nextStepURL, + model: nextModel, + storeVersion: nextVersion, + migratingFrom: currentVersion, + to: nextVersion + ) + guard + actualSemanticIntegrity == expectedSemanticIntegrity + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + + let actualHopIntegrity = + try stagedStoreIntegritySnapshot( + at: nextStepURL, + model: nextModel, + comparedWith: currentModel, + allowlist: integrityAllowlist + ) + + guard actualHopIntegrity == expectedHopIntegrity else { + throw UserStorageMigrationError + .stagedStoreValidationFailed } + try NSPersistentStoreCoordinator.destroyStore(at: currentURL) + currentVersion = nextVersion currentURL = nextStepURL } - try keystoreMigrator.finalize() - try settingsMigrator.finalize() + try keystoreMigrator.prepare() + try settingsMigrator.prepare() - try NSPersistentStoreCoordinator.replaceStore(at: storeURL, withStoreAt: currentURL) + let destinationModel = try createManagedObjectModel( + for: destinationVersion + ) + let expectedCommittedIntegrity = try stagedStoreIntegritySnapshot( + at: currentURL, + model: destinationModel, + comparedWith: destinationModel + ) + try crashConsistentStoreReplacer.replaceStore( + with: currentURL + ) { liveStoreURL in + let actualCommittedIntegrity = + try self.stagedStoreIntegritySnapshot( + at: liveStoreURL, + model: destinationModel, + comparedWith: destinationModel + ) - if currentURL != storeURL { - try NSPersistentStoreCoordinator.destroyStore(at: currentURL) + guard + actualCommittedIntegrity == + expectedCommittedIntegrity + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } } - } - private func checkIfMigrationNeeded(to version: UserStorageVersion) -> Bool { - let storageExists = fileManager.fileExists(atPath: storeURL.path) + do { + try keystoreMigrator.finalize() + } catch { + Logger.shared.error( + "Unable to finish user-storage key cleanup" + ) + throw error + } - guard storageExists else { - return false + do { + try settingsMigrator.finalize() + } catch { + Logger.shared.error( + "Unable to finish user-storage settings cleanup" + ) + throw error } - guard let metadata = NSPersistentStoreCoordinator.metadata(at: storeURL) else { - return false + if currentURL != destinationStoreURL { + do { + try NSPersistentStoreCoordinator.destroyStore(at: currentURL) + } catch { + Logger.shared.error( + "Unable to remove temporary user-storage migration store" + ) + } + } + } + + private func replaceCurrentVersionStore( + with sanitizedStoreURL: URL, + version: UserStorageVersion + ) throws { + let model = try createManagedObjectModel(for: version) + let expectedIntegrity = try stagedStoreIntegritySnapshot( + at: sanitizedStoreURL, + model: model, + comparedWith: model + ) + try stagedStoreWillValidate( + sanitizedStoreURL, + version + ) + let validatedIntegrity = try stagedStoreIntegritySnapshot( + at: sanitizedStoreURL, + model: model, + comparedWith: model + ) + guard validatedIntegrity == expectedIntegrity else { + throw UserStorageMigrationError + .stagedStoreValidationFailed } - let compatibleVersion = compatibleVersionForStoreMetadata(metadata) + try crashConsistentStoreReplacer.replaceStore( + with: sanitizedStoreURL + ) { liveStoreURL in + let actualIntegrity = + try self.stagedStoreIntegritySnapshot( + at: liveStoreURL, + model: model, + comparedWith: model + ) - return compatibleVersion != version + guard actualIntegrity == expectedIntegrity else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + } } - private func compatibleVersionForStoreMetadata(_ metadata: [String: Any]) -> UserStorageVersion? { - let compatibleVersion = UserStorageVersion.allCases.first { - let model = createManagedObjectModel(forResource: $0.rawValue) - return model.isConfiguration(withName: nil, compatibleWithStoreMetadata: metadata) + /// Migration is always performed from a disposable source copy. Unsafe + /// preference archives are repaired only in that copy; the user's original + /// store remains untouched until a validated atomic replacement. + private func sanitizePrivateSourceCopy( + at privateSourceURL: URL, + version: UserStorageVersion + ) throws -> Bool { + let model = try createManagedObjectModel(for: version) + let rawRepairedValueCount: Int + + do { + rawRepairedValueCount = + try SQLiteTransformableArchivePreflight + .repairOversizedArchives( + at: privateSourceURL, + columns: preferenceTransformableArchiveColumns( + in: model + ), + maximumArchiveByteCount: + integrityValidationLimits + .maximumTransformableArchiveBytes + ) + } catch { + throw UserStorageMigrationError + .privateSourceRepairFailed } + var didRepair = rawRepairedValueCount > 0 - return compatibleVersion - } + let coordinator = NSPersistentStoreCoordinator( + managedObjectModel: model + ) + var persistentStore: NSPersistentStore? + + try performWithObjectiveCExceptionBoundary( + phase: "private source open" + ) { + persistentStore = try coordinator.addPersistentStore( + ofType: NSSQLiteStoreType, + configurationName: nil, + at: privateSourceURL, + options: [ + NSSQLitePragmasOption: ["journal_mode": "DELETE"], + NSMigratePersistentStoresAutomaticallyOption: false, + NSInferMappingModelAutomaticallyOption: false + ] + ) + } + + defer { + if let persistentStore { + try? coordinator.remove(persistentStore) + } + } - private func createManagedObjectModel(forResource resource: String) -> NSManagedObjectModel { - let bundle = Bundle.main - let omoURL = bundle.url( - forResource: resource, - withExtension: "omo", - subdirectory: modelDirectory + let context = NSManagedObjectContext( + concurrencyType: .privateQueueConcurrencyType ) + context.persistentStoreCoordinator = coordinator + + if version == .version1 { + try enforceVersion1PrivateSourceRowLimits( + model: model, + context: context + ) + return didRepair + } + + guard model.entitiesByName["CDMetaAccount"] != nil else { + return didRepair + } + + var contextError: Error? + context.performAndWait { + do { + try self.performWithObjectiveCExceptionBoundary( + phase: "private source preference repair" + ) { + let request = NSFetchRequest( + entityName: "CDMetaAccount" + ) + request.resultType = .managedObjectIDResultType + request.includesPendingChanges = false + request.includesSubentities = false + request.affectedStores = + persistentStore.map { [$0] } + + // Count and enforce the cap before fetching any IDs. The + // immutable exact-once cohort below can therefore never + // grow beyond a deterministic production bound. + let objectCount = try context.count(for: request) + guard + objectCount != NSNotFound, + objectCount <= + self.integrityValidationLimits + .maximumRowsPerEntity, + objectCount <= + self.integrityValidationLimits + .maximumRowsAcrossStore, + objectCount < Int.max + else { + throw UserStorageMigrationError + .privateSourceRepairFailed + } - let momURL = bundle.url( - forResource: resource, - withExtension: "mom", - subdirectory: modelDirectory + request.fetchLimit = objectCount + 1 + request.fetchBatchSize = min( + self.integrityValidationLimits.fetchBatchSize, + objectCount + 1 + ) + let cohortResults = try context.fetch(request) + let cohortObjectIDs = cohortResults.compactMap { + $0 as? NSManagedObjectID + } + guard + cohortObjectIDs.count == + cohortResults.count, + cohortObjectIDs.count == objectCount, + Set(cohortObjectIDs).count == objectCount + else { + throw UserStorageMigrationError + .privateSourceRepairFailed + } + + var pageStart = 0 + while pageStart < cohortObjectIDs.count { + let remainingObjectCount = + cohortObjectIDs.count - pageStart + let pageLength = min( + self.integrityValidationLimits + .fetchBatchSize, + remainingObjectCount + ) + let pageEnd = pageStart + pageLength + let pageObjectIDs = Array( + cohortObjectIDs[pageStart ..< pageEnd] + ) + let pageRequest = + NSFetchRequest( + entityName: "CDMetaAccount" + ) + pageRequest.resultType = + .managedObjectIDResultType + pageRequest.includesPendingChanges = false + pageRequest.includesSubentities = false + pageRequest.predicate = NSPredicate( + format: "SELF IN %@", + pageObjectIDs + ) + pageRequest.affectedStores = + persistentStore.map { [$0] } + pageRequest.fetchLimit = + pageObjectIDs.count + pageRequest.fetchBatchSize = + pageObjectIDs.count + + let fetchedResults = + try context.fetch(pageRequest) + let fetchedObjectIDs = + fetchedResults.compactMap { + $0 as? NSManagedObjectID + } + self.privateSourceObjectIDPageDidFetch( + fetchedObjectIDs.count, + pageObjectIDs.count + ) + guard + fetchedObjectIDs.count == + fetchedResults.count, + fetchedObjectIDs.count == + pageObjectIDs.count, + Set(fetchedObjectIDs) == + Set(pageObjectIDs) + else { + throw UserStorageMigrationError + .privateSourceRepairFailed + } + + for objectID in pageObjectIDs { + if try self.sanitizePreferenceTransformables( + objectID: objectID, + model: model, + context: context, + persistentStore: + persistentStore + ) { + didRepair = true + } + } + pageStart = pageEnd + } + + request.fetchLimit = 0 + request.fetchBatchSize = 0 + guard + pageStart == objectCount, + try context.count(for: request) == objectCount + else { + throw UserStorageMigrationError + .privateSourceRepairFailed + } + } + } catch { + contextError = error + } + } + + if let contextError { + throw contextError + } + + return didRepair + } + + private func enforceVersion1PrivateSourceRowLimits( + model: NSManagedObjectModel, + context: NSManagedObjectContext + ) throws { + guard model.entitiesByName["CDAccountItem"] != nil else { + throw UserStorageMigrationError + .privateSourceRepairFailed + } + let maximumLegacyAccountRows = min( + min( + integrityValidationLimits.maximumRowsPerEntity, + integrityValidationLimits.maximumRowsAcrossStore + ), + KeystoreMigrationResourceLimits.production + .maximumLegacyAccountRows ) + var contextError: Error? + context.performAndWait { + do { + try self.performWithObjectiveCExceptionBoundary( + phase: "private version-1 source row inspection" + ) { + let request = + NSFetchRequest( + entityName: "CDAccountItem" + ) + request.includesPendingChanges = false + request.includesSubentities = false + let rowCount = try context.count(for: request) + + guard + rowCount != NSNotFound, + rowCount <= maximumLegacyAccountRows + else { + throw UserStorageMigrationError + .privateSourceRepairFailed + } + } + } catch { + contextError = error + } + } + + if let contextError { + throw contextError + } + } + + private func preferenceTransformableArchiveColumns( + in model: NSManagedObjectModel + ) -> [SQLiteTransformableArchiveColumn] { guard - let modelURL = omoURL ?? momURL, - let model = NSManagedObjectModel(contentsOf: modelURL) else { - fatalError("Unable to load model in bundle for resource \(resource)") + let entity = model.entitiesByName["CDMetaAccount"] + else { + return [] } - return model + return Self.resilientMetaAccountTransformableKeys + .compactMap { + key in + + guard + let attribute = entity.attributesByName[key], + attribute.attributeType == + .transformableAttributeType + else { + return nil + } + + return SQLiteTransformableArchiveColumn( + entityName: "CDMetaAccount", + attributeName: key, + isOptional: attribute.isOptional, + valueTransformerName: + attribute.valueTransformerName + ) + } + .sorted { + $0.attributeName < $1.attributeName + } } - private func createMapping( - from sourceModel: NSManagedObjectModel, - nextModel: NSManagedObjectModel - ) throws -> NSMappingModel { - let maybeCustomMapping = NSMappingModel( - from: [Bundle.main], - forSourceModel: sourceModel, - destinationModel: nextModel + func copyStoreFamily( + from sourceURL: URL, + to destinationURL: URL + ) throws { + try SQLiteStoreFamilyCopier.copy( + from: sourceURL, + to: destinationURL, + includingSharedMemory: false, + fileManager: fileManager, + limits: privateSourceCopyLimits, + availableCapacityProvider: + privateSourceAvailableCapacityProvider ) + } - if let customMapping = maybeCustomMapping { - return customMapping + private func withPrivateSourceCopy( + _ operation: ( + URL, + URL + ) throws -> Result + ) throws -> Result { + let tmpMigrationDirURL = URL( + fileURLWithPath: NSTemporaryDirectory(), + isDirectory: true + ).appendingPathComponent(UUID().uuidString) + + try fileManager.createDirectory( + at: tmpMigrationDirURL, + withIntermediateDirectories: true + ) + defer { + try? fileManager.removeItem(at: tmpMigrationDirURL) } - return try NSMappingModel.inferredMappingModel( - forSourceModel: sourceModel, - destinationModel: nextModel + let privateSourceURL = tmpMigrationDirURL + .appendingPathComponent("SanitizedSource.sqlite") + try copyStoreFamily( + from: storeURL, + to: privateSourceURL ) - } - private func forceWALCheckpointingForStore(at storeURL: URL) { - let maybeMetadata = try? NSPersistentStoreCoordinator.metadataForPersistentStore( - ofType: NSSQLiteStoreType, - at: storeURL, - options: nil + return try operation( + privateSourceURL, + tmpMigrationDirURL ) + } - guard - let metadata = maybeMetadata, - let currentModel = NSManagedObjectModel.mergedModel( - from: [Bundle.main], - forStoreMetadata: metadata - ) else { - return - } + private func readSourceVersion( + at privateSourceURL: URL + ) throws -> UserStorageVersion { + var metadata: [String: Any]? do { - let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: currentModel) + try performWithObjectiveCExceptionBoundary( + phase: "private source metadata read" + ) { + metadata = try self.metadataReader(privateSourceURL) + } + } catch let error as UserStorageMigrationError { + guard case .objectiveCException = error else { + throw UserStorageMigrationError.metadataUnreadable( + storeURL, + error + ) + } - let options = [NSSQLitePragmasOption: ["journal_mode": "DELETE"]] - let store = try persistentStoreCoordinator.addPersistentStore(at: storeURL, options: options) - try persistentStoreCoordinator.remove(store) + throw error } catch { - fatalError("Failed to force WAL checkpointing, error: \(error)") + throw UserStorageMigrationError.metadataUnreadable( + storeURL, + error + ) + } + + guard + let metadata, + let sourceVersion = + compatibleVersionForStoreMetadata(metadata) + else { + throw UserStorageMigrationError.unknownStoreVersion( + storeURL + ) } + + return sourceVersion } -} -extension UserStorageMigrator: StorageMigrating { - func requiresMigration() -> Bool { - checkIfMigrationNeeded(to: targetVersion) + private func reconcilePendingStoreReplacement() throws { + try crashConsistentStoreReplacer.reconcile { + liveStoreURL in + + guard + try self.readSourceVersion(at: liveStoreURL) == + self.targetVersion + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + } } - func migrate(_ completion: @escaping () -> Void) { - DispatchQueue.global(qos: .utility).async { [weak self] in - self?.performMigration() + private func commonIntegrityAllowlist( + from sourceVersion: UserStorageVersion, + to destinationVersion: UserStorageVersion + ) throws -> StagedStoreIntegrityAllowlist { + switch (sourceVersion, destinationVersion) { + case (.version1, .version2), + (.version2, .version3), + (.version3, .version4), + (.version4, .version5), + (.version5, .version6), + (.version6, .version7), + (.version7, .version8), + (.version8, .version9), + (.version9, .version10), + (.version10, .version11), + (.version11, .version12), + (.version12, .version14), + (.version13, .version14): + // Intentional renames, additions, and removals are outside the + // common-schema contract. No same-named field is allowed to + // change value on any supported hop. + return .none + default: + throw UserStorageMigrationError.migrationPathUnavailable( + sourceVersion, + destinationVersion + ) + } + } + + private func stagedStoreSemanticSnapshot( + at storeURL: URL, + model: NSManagedObjectModel, + storeVersion: UserStorageVersion, + migratingFrom sourceVersion: UserStorageVersion, + to destinationVersion: UserStorageVersion + ) throws -> StagedStoreSemanticSnapshot? { + switch (sourceVersion, destinationVersion) { + case (.version1, .version2): + guard + storeVersion == sourceVersion || + storeVersion == destinationVersion + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + + return try readStagedStoreSemanticSnapshot( + at: storeURL, + model: model, + phase: "legacy account semantic validation" + ) { context in + if storeVersion == sourceVersion { + return try self.legacyAccountSourceSnapshot( + context: context + ) + } + + return try self.legacyAccountDestinationSnapshot( + context: context + ) + } + case (.version9, .version10): + guard + storeVersion == sourceVersion || + storeVersion == destinationVersion + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + + return try readStagedStoreSemanticSnapshot( + at: storeURL, + model: model, + phase: "asset visibility semantic validation" + ) { context in + if storeVersion == sourceVersion { + return try self.assetVisibilitySourceSnapshot( + context: context + ) + } + + return try self.assetVisibilityDestinationSnapshot( + context: context + ) + } + case (.version11, .version12): + guard + storeVersion == sourceVersion || + storeVersion == destinationVersion + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + + return try readStagedStoreSemanticSnapshot( + at: storeURL, + model: model, + phase: "network filter semantic validation" + ) { context in + if storeVersion == sourceVersion { + return try self.networkFilterSourceSnapshot( + context: context + ) + } + + return try self.networkFilterDestinationSnapshot( + context: context + ) + } + default: + return nil + } + } + + private func readStagedStoreSemanticSnapshot( + at storeURL: URL, + model: NSManagedObjectModel, + phase: String, + makeSnapshot: @escaping ( + NSManagedObjectContext + ) throws -> StagedStoreSemanticSnapshot + ) throws -> StagedStoreSemanticSnapshot { + let coordinator = NSPersistentStoreCoordinator( + managedObjectModel: model + ) + var persistentStore: NSPersistentStore? + + try performWithObjectiveCExceptionBoundary( + phase: "\(phase) open" + ) { + persistentStore = try coordinator.addPersistentStore( + ofType: NSSQLiteStoreType, + configurationName: nil, + at: storeURL, + options: [ + NSMigratePersistentStoresAutomaticallyOption: false, + NSInferMappingModelAutomaticallyOption: false + ] + ) + } + + defer { + if let persistentStore { + try? coordinator.remove(persistentStore) + } + } + + let context = NSManagedObjectContext( + concurrencyType: .privateQueueConcurrencyType + ) + context.persistentStoreCoordinator = coordinator + + var snapshot: StagedStoreSemanticSnapshot? + var contextError: Error? + context.performAndWait { + do { + try self.performWithObjectiveCExceptionBoundary( + phase: "\(phase) read" + ) { + snapshot = try makeSnapshot(context) + } + } catch { + contextError = error + } + } + + if let contextError { + throw contextError + } + + guard let snapshot else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + + return snapshot + } + + private func legacyAccountSourceSnapshot( + context: NSManagedObjectContext + ) throws -> StagedStoreSemanticSnapshot { + let addressFactory = SS58AddressFactory() + var accountIds = Set() + + _ = try stagedSemanticDigest( + context: context, + entityName: "CDAccountItem" + ) { account in + guard + let address = account.value( + forKey: "identifier" + ) as? AccountAddress + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + + let accountId = try addressFactory.accountId( + from: address + ).toHex() + accountIds.insert(accountId) + + return try self.canonicalSemanticStrings( + [address, accountId] + ) + } + + return .legacyAccountIds(accountIds.sorted()) + } + + private func legacyAccountDestinationSnapshot( + context: NSManagedObjectContext + ) throws -> StagedStoreSemanticSnapshot { + var accountIds = Set() + let digest = try stagedSemanticDigest( + context: context, + entityName: "CDMetaAccount" + ) { account in + guard + let accountId = account.value( + forKey: "substrateAccountId" + ) as? String + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + + accountIds.insert(accountId) + return try self.canonicalSemanticStrings([accountId]) + } + + guard digest.rowCount == accountIds.count else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + + let destinationModel = + context.persistentStoreCoordinator? + .managedObjectModel + guard let destinationModel else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + for entityName in destinationModel.entitiesByName.keys + where entityName != "CDMetaAccount" { + let request = + NSFetchRequest( + entityName: entityName + ) + request.includesPendingChanges = false + request.includesSubentities = false + guard try context.count(for: request) == 0 else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + } + + return .legacyAccountIds(accountIds.sorted()) + } + + private func assetVisibilitySourceSnapshot( + context: NSManagedObjectContext + ) throws -> StagedStoreSemanticSnapshot { + var visibilityRowCount = 0 + let digest = try stagedSemanticDigest( + context: context, + entityName: "CDMetaAccount" + ) { wallet in + guard + let metaId = wallet.value( + forKey: "metaId" + ) as? String + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + + let assetIds: [String] = + try SafeTransformableValueReader.read( + from: wallet, + key: "assetIdsEnabled" + ) ?? [] + try self.validateSemanticStrings( + assetIds, + maximumCount: + self.integrityValidationLimits + .maximumTransformableElements + ) + guard + visibilityRowCount <= + self.integrityValidationLimits + .maximumRelationshipDestinationsAcrossStore - + assetIds.count + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + visibilityRowCount += assetIds.count + + return try self.canonicalSemanticStrings( + [metaId] + assetIds.sorted() + ) + } + + return .assetVisibility( + walletRows: digest, + visibilityRowCount: visibilityRowCount + ) + } + + private func assetVisibilityDestinationSnapshot( + context: NSManagedObjectContext + ) throws -> StagedStoreSemanticSnapshot { + var visibilityRowCount = 0 + let digest = try stagedSemanticDigest( + context: context, + entityName: "CDMetaAccount" + ) { wallet in + guard + let metaId = wallet.value( + forKey: "metaId" + ) as? String + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + + let preflightCount = + try self.preflightRelationshipDestinationCount( + for: wallet, + relationshipName: "assetsVisibility" + ) + guard + preflightCount <= + self.integrityValidationLimits + .maximumRelationshipDestinations, + visibilityRowCount <= + self.integrityValidationLimits + .maximumRelationshipDestinationsAcrossStore - + preflightCount, + let visibilitySet = wallet.value( + forKey: "assetsVisibility" + ) as? NSSet, + visibilitySet.count == preflightCount + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + + var assetIds = [String]() + assetIds.reserveCapacity(preflightCount) + for case let visibility as NSManagedObject + in visibilitySet { + guard + let assetId = visibility.value( + forKey: "assetId" + ) as? String, + let hidden = visibility.value( + forKey: "hidden" + ) as? NSNumber, + hidden.boolValue + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + assetIds.append(assetId) + } + guard assetIds.count == preflightCount else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + try self.validateSemanticStrings( + assetIds, + maximumCount: + self.integrityValidationLimits + .maximumTransformableElements + ) + visibilityRowCount += assetIds.count + + return try self.canonicalSemanticStrings( + [metaId] + assetIds.sorted() + ) + } + + let visibilityRequest = + NSFetchRequest( + entityName: "CDAssetVisibility" + ) + visibilityRequest.includesPendingChanges = false + visibilityRequest.includesSubentities = false + let storedVisibilityRowCount = try context.count( + for: visibilityRequest + ) + guard + storedVisibilityRowCount != NSNotFound, + storedVisibilityRowCount == visibilityRowCount + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + + return .assetVisibility( + walletRows: digest, + visibilityRowCount: visibilityRowCount + ) + } + + private func networkFilterSourceSnapshot( + context: NSManagedObjectContext + ) throws -> StagedStoreSemanticSnapshot { + let digest = try stagedSemanticDigest( + context: context, + entityName: "CDMetaAccount" + ) { wallet in + guard + let metaId = wallet.value( + forKey: "metaId" + ) as? String + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + let filter = wallet.value( + forKey: "chainIdForFilter" + ) as? String + + return try self.canonicalSemanticStrings( + [metaId, filter] + ) + } + + return .networkManagementFilters(digest) + } + + private func networkFilterDestinationSnapshot( + context: NSManagedObjectContext + ) throws -> StagedStoreSemanticSnapshot { + let digest = try stagedSemanticDigest( + context: context, + entityName: "CDMetaAccount" + ) { wallet in + guard + let metaId = wallet.value( + forKey: "metaId" + ) as? String + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + let filter = wallet.value( + forKey: "networkManagmentFilter" + ) as? String + let favouriteChainIds: [String]? = + try SafeTransformableValueReader.read( + from: wallet, + key: "favouriteChainIds" + ) + guard favouriteChainIds?.isEmpty == true else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + + return try self.canonicalSemanticStrings( + [metaId, filter] + ) + } + + return .networkManagementFilters(digest) + } + + private func stagedSemanticDigest( + context: NSManagedObjectContext, + entityName: String, + canonicalRow: (NSManagedObject) throws -> String + ) throws -> IntegrityDigest { + guard + context.persistentStoreCoordinator? + .managedObjectModel.entitiesByName[entityName] != nil + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + + let countRequest = + NSFetchRequest( + entityName: entityName + ) + countRequest.includesPendingChanges = false + countRequest.includesSubentities = false + let rowCount = try context.count(for: countRequest) + guard + rowCount != NSNotFound, + rowCount <= + integrityValidationLimits.maximumRowsPerEntity, + rowCount <= + integrityValidationLimits.maximumRowsAcrossStore + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + + let identifierRequest = + NSFetchRequest( + entityName: entityName + ) + identifierRequest.resultType = + .managedObjectIDResultType + identifierRequest.includesPendingChanges = false + identifierRequest.includesSubentities = false + identifierRequest.fetchLimit = rowCount + 1 + identifierRequest.fetchBatchSize = min( + integrityValidationLimits.fetchBatchSize, + rowCount + 1 + ) + let fetchedIdentifiers = + try context.fetch(identifierRequest) + let objectIDs = fetchedIdentifiers.compactMap { + $0 as? NSManagedObjectID + } + guard + objectIDs.count == fetchedIdentifiers.count, + objectIDs.count == rowCount, + Set(objectIDs).count == rowCount + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + + var accumulator = IntegrityDigestAccumulator() + var pageStart = 0 + while pageStart < objectIDs.count { + let pageEnd = min( + pageStart + + integrityValidationLimits.fetchBatchSize, + objectIDs.count + ) + let pageObjectIDs = Array( + objectIDs[pageStart ..< pageEnd] + ) + let request = NSFetchRequest( + entityName: entityName + ) + request.includesPendingChanges = false + request.includesSubentities = false + request.predicate = NSPredicate( + format: "SELF IN %@", + pageObjectIDs + ) + request.fetchLimit = pageObjectIDs.count + request.fetchBatchSize = pageObjectIDs.count + let objects = try context.fetch(request) + integrityObjectPageDidFetch( + objects.count, + pageObjectIDs.count + ) + guard + objects.count == pageObjectIDs.count, + Set(objects.map(\.objectID)) == + Set(pageObjectIDs) + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + + for object in objects { + let row = try canonicalRow(object) + guard + row.utf8.count <= + integrityValidationLimits + .maximumCanonicalRowBytes + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + accumulator.append(canonicalRow: row) + } + + pageStart = pageEnd + context.reset() + } + + guard + accumulator.rowCount == rowCount, + try context.count(for: countRequest) == rowCount + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + + return accumulator.digest + } + + private func validateSemanticStrings( + _ values: [String], + maximumCount: Int? = nil + ) throws { + if let maximumCount { + guard values.count <= maximumCount else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + } + + var cumulativeByteCount = 0 + for value in values { + let byteCount = value.utf8.count + guard + byteCount <= + integrityValidationLimits.maximumAttributeBytes, + cumulativeByteCount <= + integrityValidationLimits.maximumCanonicalRowBytes - + byteCount + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + cumulativeByteCount += byteCount + } + } + + private func canonicalSemanticStrings( + _ values: [String?] + ) throws -> String { + try validateSemanticStrings(values.compactMap { $0 }) + + return values.map { value in + guard let value else { + return "nil" + } + + return "value:" + + Data(value.utf8).base64EncodedString() + }.joined(separator: "|") + } + + private func shouldValidateMetaAccountCount( + from sourceVersion: UserStorageVersion + ) -> Bool { + sourceVersion != .version1 + } + + private func stagedMetaAccountCount( + at storeURL: URL, + model: NSManagedObjectModel + ) throws -> Int { + guard model.entitiesByName["CDMetaAccount"] != nil else { + throw UserStorageMigrationError.stagedStoreValidationFailed + } + + let coordinator = NSPersistentStoreCoordinator( + managedObjectModel: model + ) + var persistentStore: NSPersistentStore? + + try performWithObjectiveCExceptionBoundary( + phase: "staged wallet-count validation open" + ) { + persistentStore = try coordinator.addPersistentStore( + ofType: NSSQLiteStoreType, + configurationName: nil, + at: storeURL, + options: [ + NSMigratePersistentStoresAutomaticallyOption: false, + NSInferMappingModelAutomaticallyOption: false + ] + ) + } + + defer { + if let persistentStore { + try? coordinator.remove(persistentStore) + } + } + + let context = NSManagedObjectContext( + concurrencyType: .privateQueueConcurrencyType + ) + context.persistentStoreCoordinator = coordinator + + var count: Int? + var contextError: Error? + context.performAndWait { + do { + try self.performWithObjectiveCExceptionBoundary( + phase: "staged wallet-count validation read" + ) { + let request = + NSFetchRequest( + entityName: "CDMetaAccount" + ) + request.includesPendingChanges = false + request.includesSubentities = false + let fetchedCount = try context.count(for: request) + + guard + fetchedCount != NSNotFound, + fetchedCount <= + self.integrityValidationLimits + .maximumRowsPerEntity, + fetchedCount <= + self.integrityValidationLimits + .maximumRowsAcrossStore + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + + count = fetchedCount + } + } catch { + contextError = error + } + } + + if let contextError { + throw contextError + } + + guard let count else { + throw UserStorageMigrationError.stagedStoreValidationFailed + } + + return count + } + + private func stagedStoreIntegritySnapshot( + at storeURL: URL, + model: NSManagedObjectModel, + comparedWith comparisonModel: NSManagedObjectModel, + allowlist: StagedStoreIntegrityAllowlist = .none + ) throws -> StagedStoreIntegritySnapshot { + let coordinator = NSPersistentStoreCoordinator( + managedObjectModel: model + ) + var persistentStore: NSPersistentStore? + + try performWithObjectiveCExceptionBoundary( + phase: "staged store validation open" + ) { + persistentStore = try coordinator.addPersistentStore( + ofType: NSSQLiteStoreType, + configurationName: nil, + at: storeURL, + options: [ + NSMigratePersistentStoresAutomaticallyOption: false, + NSInferMappingModelAutomaticallyOption: false + ] + ) + } + + defer { + if let persistentStore { + try? coordinator.remove(persistentStore) + } + } + + let context = NSManagedObjectContext( + concurrencyType: .privateQueueConcurrencyType + ) + context.persistentStoreCoordinator = coordinator + + var snapshot: StagedStoreIntegritySnapshot? + var contextError: Error? + context.performAndWait { + do { + try self.performWithObjectiveCExceptionBoundary( + phase: "staged store validation read" + ) { + snapshot = try self.makeStagedStoreIntegritySnapshot( + context: context, + model: model, + comparedWith: comparisonModel, + allowlist: allowlist + ) + } + } catch { + contextError = error + } + } + + if let contextError { + throw contextError + } + + guard let snapshot else { + throw UserStorageMigrationError.stagedStoreValidationFailed + } + + return snapshot + } + + private func makeStagedStoreIntegritySnapshot( + context: NSManagedObjectContext, + model: NSManagedObjectModel, + comparedWith comparisonModel: NSManagedObjectModel, + allowlist: StagedStoreIntegrityAllowlist + ) throws -> StagedStoreIntegritySnapshot { + let commonEntityNames = Set(model.entitiesByName.keys) + .intersection(comparisonModel.entitiesByName.keys) + .subtracting(allowlist.entityNames) + .sorted() + var rowsByEntity = [String: IntegrityDigest]() + var totalRowCount = 0 + var totalRelationshipDestinationCount = 0 + + for entityName in commonEntityNames { + let attributeNames = try commonAttributeNames( + entityName: entityName, + model: model, + comparedWith: comparisonModel, + excluding: + allowlist.attributeNamesByEntity[entityName] ?? [] + ) + let relationshipNames = + try commonRelationshipNames( + entityName: entityName, + model: model, + comparedWith: comparisonModel, + excluding: + allowlist.relationshipNamesByEntity[entityName] ?? [] + ) + let countRequest = + NSFetchRequest( + entityName: entityName + ) + countRequest.includesPendingChanges = false + countRequest.includesSubentities = false + let rowCount = try context.count(for: countRequest) + guard + rowCount != NSNotFound, + rowCount < Int.max, + rowCount <= + integrityValidationLimits.maximumRowsPerEntity, + totalRowCount <= + integrityValidationLimits.maximumRowsAcrossStore - + rowCount + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + totalRowCount += rowCount + + let identifierRequest = + NSFetchRequest( + entityName: entityName + ) + identifierRequest.resultType = + .managedObjectIDResultType + identifierRequest.includesPendingChanges = false + identifierRequest.includesSubentities = false + identifierRequest.fetchLimit = rowCount + 1 + identifierRequest.fetchBatchSize = min( + integrityValidationLimits.fetchBatchSize, + rowCount + 1 + ) + let fetchedIdentifiers = + try context.fetch(identifierRequest) + let objectIDs = fetchedIdentifiers.compactMap { + $0 as? NSManagedObjectID + } + guard + objectIDs.count == fetchedIdentifiers.count, + objectIDs.count == rowCount, + Set(objectIDs).count == rowCount + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + + var accumulator = IntegrityDigestAccumulator() + var pageStart = 0 + + while pageStart < objectIDs.count { + let remainingObjectCount = + objectIDs.count - pageStart + let pageLength = min( + integrityValidationLimits.fetchBatchSize, + remainingObjectCount + ) + let pageEnd = pageStart + pageLength + let pageObjectIDs = Array( + objectIDs[pageStart ..< pageEnd] + ) + let request = NSFetchRequest( + entityName: entityName + ) + request.includesPendingChanges = false + request.includesSubentities = false + request.predicate = NSPredicate( + format: "SELF IN %@", + pageObjectIDs + ) + request.fetchLimit = pageObjectIDs.count + request.fetchBatchSize = pageObjectIDs.count + let objects = try context.fetch(request) + integrityObjectPageDidFetch( + objects.count, + pageObjectIDs.count + ) + + guard + objects.count == pageObjectIDs.count, + Set(objects.map(\.objectID)) == + Set(pageObjectIDs) + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + + for object in objects { + let row = try integrityRow( + for: object, + entityName: entityName, + attributeNames: attributeNames, + relationshipNames: relationshipNames, + model: model, + comparedWith: comparisonModel, + allowlist: allowlist, + totalRelationshipDestinationCount: + &totalRelationshipDestinationCount + ) + let canonicalRow = row.sortKey + guard + canonicalRow.utf8.count <= + integrityValidationLimits + .maximumCanonicalRowBytes + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + accumulator.append(canonicalRow: canonicalRow) + } + + pageStart = pageEnd + context.reset() + } + + guard + accumulator.rowCount == rowCount, + try context.count(for: countRequest) == rowCount + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + rowsByEntity[entityName] = accumulator.digest + } + + return StagedStoreIntegritySnapshot( + rowsByEntity: rowsByEntity + ) + } + + private func commonAttributeNames( + entityName: String, + model: NSManagedObjectModel, + comparedWith comparisonModel: NSManagedObjectModel, + excluding excludedNames: Set + ) throws -> [String] { + guard + let entity = model.entitiesByName[entityName], + let comparisonEntity = + comparisonModel.entitiesByName[entityName] + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + + let names = Set(entity.attributesByName.keys) + .intersection(comparisonEntity.attributesByName.keys) + .subtracting(excludedNames) + .sorted() + + for name in names { + guard + entity.attributesByName[name]?.attributeType == + comparisonEntity.attributesByName[name]?.attributeType + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + } + + return names + } + + private func commonRelationshipNames( + entityName: String, + model: NSManagedObjectModel, + comparedWith comparisonModel: NSManagedObjectModel, + excluding excludedNames: Set + ) throws -> [String] { + guard + let entity = model.entitiesByName[entityName], + let comparisonEntity = + comparisonModel.entitiesByName[entityName] + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + + let names = Set(entity.relationshipsByName.keys) + .intersection( + comparisonEntity.relationshipsByName.keys + ) + .subtracting(excludedNames) + .sorted() + + for name in names { + guard + let relationship = + entity.relationshipsByName[name], + let comparisonRelationship = + comparisonEntity.relationshipsByName[name], + relationship.isToMany == + comparisonRelationship.isToMany, + relationship.isOrdered == + comparisonRelationship.isOrdered, + relationship.destinationEntity?.name == + comparisonRelationship.destinationEntity?.name, + let destinationName = + relationship.destinationEntity?.name, + model.entitiesByName[destinationName] != nil, + comparisonModel.entitiesByName[destinationName] != nil + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + } + + return names + } + + private func integrityRow( + for object: NSManagedObject, + entityName: String, + attributeNames: [String], + relationshipNames: [String], + model: NSManagedObjectModel, + comparedWith comparisonModel: NSManagedObjectModel, + allowlist: StagedStoreIntegrityAllowlist, + totalRelationshipDestinationCount: inout Int + ) throws -> IntegrityRow { + let identity = try integrityIdentity( + for: object, + entityName: entityName, + attributeNames: attributeNames, + model: model + ) + let identityByteCount = identity.sortKey.utf8.count + guard + identityByteCount <= + integrityValidationLimits.maximumCanonicalRowBytes + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + + var relationships = [IntegrityRelationship]() + var cumulativeByteCount = identityByteCount + for relationshipName in relationshipNames { + let relationship = try integrityRelationship( + for: object, + entityName: entityName, + relationshipName: relationshipName, + model: model, + comparedWith: comparisonModel, + allowlist: allowlist, + totalRelationshipDestinationCount: + &totalRelationshipDestinationCount + ) + let relationshipByteCount = + relationship.sortKey.utf8.count + guard + relationshipByteCount <= + integrityValidationLimits.maximumCanonicalRowBytes, + cumulativeByteCount <= + integrityValidationLimits.maximumCanonicalRowBytes - + relationshipByteCount + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + cumulativeByteCount += relationshipByteCount + relationships.append(relationship) + } + + return IntegrityRow( + identity: identity, + relationships: relationships + ) + } + + private func integrityRelationship( + for object: NSManagedObject, + entityName: String, + relationshipName: String, + model: NSManagedObjectModel, + comparedWith comparisonModel: NSManagedObjectModel, + allowlist: StagedStoreIntegrityAllowlist, + totalRelationshipDestinationCount: inout Int + ) throws -> IntegrityRelationship { + guard + let relationship = + model.entitiesByName[entityName]? + .relationshipsByName[relationshipName], + let comparisonRelationship = + comparisonModel.entitiesByName[entityName]? + .relationshipsByName[relationshipName], + relationship.isToMany == + comparisonRelationship.isToMany, + relationship.isOrdered == + comparisonRelationship.isOrdered, + let destinationName = + relationship.destinationEntity?.name, + destinationName == + comparisonRelationship.destinationEntity?.name + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + + let relatedObjects: [NSManagedObject] + if relationship.isToMany { + let preflightCount = + try preflightRelationshipDestinationCount( + for: object, + relationshipName: relationshipName + ) + guard + preflightCount <= + integrityValidationLimits + .maximumRelationshipDestinations + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + try accumulateRelationshipDestinationCount( + preflightCount, + total: &totalRelationshipDestinationCount + ) + + if preflightCount == 0 { + relatedObjects = [] + } else { + let value = object.value( + forKey: relationshipName + ) + if relationship.isOrdered, + let orderedSet = value as? NSOrderedSet { + relatedObjects = + orderedSet.array.compactMap { + $0 as? NSManagedObject + } + guard + orderedSet.count == preflightCount, + relatedObjects.count == preflightCount + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + } else if !relationship.isOrdered, + let set = value as? NSSet { + relatedObjects = + set.allObjects.compactMap { + $0 as? NSManagedObject + } + guard + set.count == preflightCount, + relatedObjects.count == preflightCount + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + } else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + } + } else { + let value = object.value(forKey: relationshipName) + if let relatedObject = value as? NSManagedObject { + relatedObjects = [relatedObject] + } else if value == nil { + relatedObjects = [] + } else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + } + + let destinationAttributeNames = + try commonAttributeNames( + entityName: destinationName, + model: model, + comparedWith: comparisonModel, + excluding: + allowlist.attributeNamesByEntity[destinationName] ?? [] + ) + var destinations = [IntegrityIdentity]() + var cumulativeDestinationByteCount = 0 + for relatedObject in relatedObjects { + guard relatedObject.entity.name == destinationName else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + + let destination = try integrityIdentity( + for: relatedObject, + entityName: destinationName, + attributeNames: destinationAttributeNames, + model: model + ) + let destinationByteCount = + destination.sortKey.utf8.count + guard + destinationByteCount <= + integrityValidationLimits.maximumCanonicalRowBytes, + cumulativeDestinationByteCount <= + integrityValidationLimits.maximumCanonicalRowBytes - + destinationByteCount + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + cumulativeDestinationByteCount += destinationByteCount + destinations.append(destination) + } + + if !relationship.isOrdered { + destinations.sort { + $0.sortKey < $1.sortKey + } + } + + return IntegrityRelationship( + name: relationshipName, + destinations: destinations + ) + } + + func accumulateRelationshipDestinationCount( + _ count: Int, + total: inout Int + ) throws { + guard count >= 0, total >= 0 else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + let addition = total.addingReportingOverflow(count) + guard + !addition.overflow, + addition.partialValue <= + integrityValidationLimits + .maximumRelationshipDestinationsAcrossStore + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + + total = addition.partialValue + } + + func preflightRelationshipDestinationCount( + for object: NSManagedObject, + relationshipName: String + ) throws -> Int { + guard + let context = object.managedObjectContext, + let entityName = object.entity.name, + let relationship = + object.entity.relationshipsByName[relationshipName], + relationship.isToMany + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + guard + let persistentStore = + object.objectID.persistentStore + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + + let countDescription = NSExpressionDescription() + countDescription.name = "relationshipDestinationCount" + countDescription.expression = NSExpression( + forFunction: "count:", + arguments: [ + NSExpression(forKeyPath: relationshipName) + ] + ) + countDescription.expressionResultType = + .integer64AttributeType + + let request = NSFetchRequest( + entityName: entityName + ) + request.resultType = .dictionaryResultType + request.propertiesToFetch = [countDescription] + request.predicate = NSPredicate( + format: "SELF == %@", + object.objectID + ) + request.includesPendingChanges = false + request.includesSubentities = false + request.affectedStores = [persistentStore] + request.fetchLimit = 2 + + let rows = try context.fetch(request) + guard + rows.count == 1, + let number = + rows[0][countDescription.name] as? NSNumber + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + + let count = number.int64Value + guard + count >= 0, + UInt64(count) <= UInt64(Int.max) + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + + return Int(count) + } + + private func integrityIdentity( + for object: NSManagedObject, + entityName: String, + attributeNames: [String], + model: NSManagedObjectModel + ) throws -> IntegrityIdentity { + guard + object.entity.name == entityName, + let entity = model.entitiesByName[entityName] + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + + var fields = [IntegrityField]() + var cumulativeFieldByteCount = + Data(entityName.utf8).base64EncodedString().utf8.count + + for attributeName in attributeNames { + guard + let attribute = + entity.attributesByName[attributeName] + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + + let field = IntegrityField( + name: attributeName, + value: try canonicalIntegrityValue( + from: object, + entityName: entityName, + attribute: attribute + ) + ) + let fieldByteCount = field.sortKey.utf8.count + guard + fieldByteCount <= + integrityValidationLimits.maximumCanonicalRowBytes, + cumulativeFieldByteCount <= + integrityValidationLimits.maximumCanonicalRowBytes - + fieldByteCount + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + cumulativeFieldByteCount += fieldByteCount + fields.append(field) + } + + return IntegrityIdentity( + entityName: entityName, + fields: fields + ) + } + + private func canonicalIntegrityValue( + from object: NSManagedObject, + entityName: String, + attribute: NSAttributeDescription + ) throws -> String { + if attribute.attributeType == + .transformableAttributeType { + guard + entityName == "CDMetaAccount", + Self.resilientMetaAccountTransformableKeys + .contains(attribute.name) + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + + let value: [String]? = + try SafeTransformableValueReader.read( + from: object, + key: attribute.name + ) + + guard let value else { + return "nil" + } + + guard + value.count <= + integrityValidationLimits + .maximumTransformableElements + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + var cumulativeByteCount = 0 + for item in value { + let itemByteCount = item.utf8.count + guard + itemByteCount <= + integrityValidationLimits.maximumAttributeBytes, + cumulativeByteCount <= + integrityValidationLimits.maximumAttributeBytes - + itemByteCount + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + cumulativeByteCount += itemByteCount + } + + return "string-array:" + value.map { + Data($0.utf8).base64EncodedString() + }.joined(separator: ",") + } + + let value = object.value(forKey: attribute.name) + + guard let value else { + return "nil" + } + + if let value = value as? String { + guard + value.utf8.count <= + integrityValidationLimits.maximumAttributeBytes + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + return "string:\(Data(value.utf8).base64EncodedString())" + } + + if let value = value as? Data { + guard + value.count <= + integrityValidationLimits.maximumAttributeBytes + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + return "data:\(value.base64EncodedString())" + } + + if let value = value as? NSNumber { + return "number:\(value.stringValue)" + } + + if let value = value as? Date { + return "date:\(value.timeIntervalSinceReferenceDate.bitPattern)" + } + + if let value = value as? URL { + guard + value.absoluteString.utf8.count <= + integrityValidationLimits.maximumAttributeBytes + else { + throw UserStorageMigrationError + .stagedStoreValidationFailed + } + return "url:\(Data(value.absoluteString.utf8).base64EncodedString())" + } + + if let value = value as? UUID { + return "uuid:\(value.uuidString)" + } + + throw UserStorageMigrationError.stagedStoreValidationFailed + } + + private func sanitizePreferenceTransformables( + objectID: NSManagedObjectID, + model: NSManagedObjectModel, + context: NSManagedObjectContext, + persistentStore: NSPersistentStore? + ) throws -> Bool { + guard let entity = model.entitiesByName["CDMetaAccount"] else { + return false + } + + var didRepair = false + for key in Self.resilientMetaAccountTransformableKeys { + guard + let attribute = entity.attributesByName[key], + attribute.attributeType == .transformableAttributeType + else { + continue + } + + let needsRepair: Bool + let request = NSFetchRequest( + entityName: "CDMetaAccount" + ) + request.resultType = .dictionaryResultType + request.propertiesToFetch = [key] + request.predicate = NSPredicate( + format: "SELF == %@", + objectID + ) + request.affectedStores = persistentStore.map { [$0] } + request.fetchLimit = 2 + + do { + var rows: [NSDictionary]? + try SafeObjectiveCExceptionBoundary.perform { + rows = try context.fetch(request) + } + + guard + let rows, + rows.count == 1 + else { + throw UserStorageMigrationError + .privateSourceRepairFailed + } + + let value = rows[0][key] + if value == nil || value is NSNull { + needsRepair = !attribute.isOptional + } else if let strings = value as? [String] { + needsRepair = + !isBoundedPreferenceStringArray(strings) + } else { + needsRepair = true + } + } catch is SafeTransformableValueReaderError { + needsRepair = true + } catch let error as NSError + where error.domain == NSCocoaErrorDomain && + error.code == NSCoderReadCorruptError { + needsRepair = true + } + + // A failed transformable decode can leave fetched state unusable. + // Discard it before issuing a store-level repair. + context.reset() + + guard needsRepair else { + continue + } + + // NSBatchUpdateRequest writes the replacement directly to the + // disposable store. No managed object is changed or saved, so + // Core Data never attempts to decode the corrupt prior archive. + guard !context.hasChanges else { + throw UserStorageMigrationError.privateSourceRepairFailed + } + + let update = NSBatchUpdateRequest( + entityName: "CDMetaAccount" + ) + update.affectedStores = persistentStore.map { [$0] } + update.predicate = NSPredicate( + format: "SELF == %@", + objectID + ) + update.propertiesToUpdate = [key: NSArray()] + update.resultType = .updatedObjectIDsResultType + + let result = try context.execute(update) + guard + let batchResult = result as? NSBatchUpdateResult, + let updatedObjectIDs = + batchResult.result as? [NSManagedObjectID], + updatedObjectIDs == [objectID] + else { + throw UserStorageMigrationError.privateSourceRepairFailed + } + + context.reset() + didRepair = true + } + + return didRepair + } + + private func isBoundedPreferenceStringArray( + _ value: [String] + ) -> Bool { + guard + value.count <= + integrityValidationLimits + .maximumTransformableElements + else { + return false + } + + var cumulativeByteCount = 0 + for item in value { + let itemByteCount = item.utf8.count + guard + itemByteCount <= + integrityValidationLimits.maximumAttributeBytes, + cumulativeByteCount <= + integrityValidationLimits.maximumAttributeBytes - + itemByteCount + else { + return false + } + cumulativeByteCount += itemByteCount + } + + return true + } + + private func performWithObjectiveCExceptionBoundary( + phase: String, + _ operation: @escaping () throws -> Void + ) throws { + do { + try SafeObjectiveCExceptionBoundary.perform(operation) + } catch SafeTransformableValueReaderError.objectiveCException { + Logger.shared.error( + "User-storage recovery safely stopped during \(phase)" + ) + throw UserStorageMigrationError.objectiveCException + } catch { + throw error + } + } + + private func checkIfMigrationNeeded(to version: UserStorageVersion) -> Bool { + if crashConsistentStoreReplacer.hasPendingTransaction { + return true + } + + do { + guard try crashConsistentStoreReplacer.liveStoreExistsSafely() else { + return false + } + } catch { + return true + } + + do { + return try withPrivateSourceCopy { + privateSourceURL, + _ in + + let sourceVersion = try readSourceVersion( + at: privateSourceURL + ) + guard sourceVersion == version else { + return true + } + + return try sanitizePrivateSourceCopy( + at: privateSourceURL, + version: sourceVersion + ) + } + } catch { + return true + } + } + + private func compatibleVersionForStoreMetadata(_ metadata: [String: Any]) -> UserStorageVersion? { + let compatibleVersion = UserStorageVersion.allCases.first { + guard let model = try? createManagedObjectModel(for: $0) else { + return false + } + + return model.isConfiguration(withName: nil, compatibleWithStoreMetadata: metadata) + } + + return compatibleVersion + } + + private func createManagedObjectModel( + for version: UserStorageVersion + ) throws -> NSManagedObjectModel { + guard + let modelURL = version.modelURL( + in: modelBundle, + legacyModelDirectory: modelDirectory + ), + let model = NSManagedObjectModel(contentsOf: modelURL) else { + throw UserStorageMigrationError.modelUnavailable(version) + } + + return model + } + + private func createMapping( + from sourceModel: NSManagedObjectModel, + nextModel: NSManagedObjectModel + ) throws -> NSMappingModel { + let maybeCustomMapping = NSMappingModel( + from: [modelBundle], + forSourceModel: sourceModel, + destinationModel: nextModel + ) + + if let customMapping = maybeCustomMapping { + return customMapping + } + + return try NSMappingModel.inferredMappingModel( + forSourceModel: sourceModel, + destinationModel: nextModel + ) + } +} + +extension UserStorageMigrator: StorageMigrating { + func requiresMigration() -> Bool { + checkIfMigrationNeeded(to: targetVersion) + } + + func migrate(_ completion: @escaping () -> Void) { + DispatchQueue.global(qos: .utility).async { [weak self] in + guard let self else { + return + } + + do { + try self.performMigration() + } catch { + Logger.shared.error( + "User-storage migration failed safely" + ) + return + } DispatchQueue.main.async { completion() diff --git a/fearless/Common/Storage/Migration/UserStorage/UserStorageVersion.swift b/fearless/Common/Storage/Migration/UserStorage/UserStorageVersion.swift index b1535d2386..9a3d6fe4d2 100644 --- a/fearless/Common/Storage/Migration/UserStorage/UserStorageVersion.swift +++ b/fearless/Common/Storage/Migration/UserStorage/UserStorageVersion.swift @@ -13,6 +13,12 @@ enum UserStorageVersion: String, CaseIterable { case version10 = "MultiassetUserDataModel_v9" case version11 = "MultiassetUserDataModel_v10" case version12 = "MultiassetUserDataModel_v11" + /// Schema shipped by the ecosystem/TON app line. It is a migration source, + /// not an active destination for this app. + case version13 = "LegacyEcosystemUserDataModel_v12" + /// Superset schema that accepts both the public v11 and ecosystem v12 + /// layouts without dropping ecosystem or TON fields. + case version14 = "CompatibleUserDataModel_v13" static var current: UserStorageVersion { guard let currentVersion = allCases.last else { @@ -47,7 +53,37 @@ enum UserStorageVersion: String, CaseIterable { case .version11: return .version12 case .version12: + return .version14 + case .version13: + return .version14 + case .version14: return nil } } + + var isCompatibilityResource: Bool { + switch self { + case .version13, .version14: + return true + default: + return false + } + } + + func modelURL( + in bundle: Bundle, + legacyModelDirectory: String + ) -> URL? { + let subdirectory = isCompatibilityResource ? nil : legacyModelDirectory + + return bundle.url( + forResource: rawValue, + withExtension: "omo", + subdirectory: subdirectory + ) ?? bundle.url( + forResource: rawValue, + withExtension: "mom", + subdirectory: subdirectory + ) + } } diff --git a/fearless/Common/Storage/MigrationMappings/SubstrateV2Mapping.xcmappingmodel/xcmapping.xml b/fearless/Common/Storage/MigrationMappings/SubstrateV2Mapping.xcmappingmodel/xcmapping.xml index bfda5f6d3b..fb4323cbc5 100644 --- a/fearless/Common/Storage/MigrationMappings/SubstrateV2Mapping.xcmappingmodel/xcmapping.xml +++ b/fearless/Common/Storage/MigrationMappings/SubstrateV2Mapping.xcmappingmodel/xcmapping.xml @@ -432,12 +432,12 @@ cxIAAYagXxAPTlNLZXllZEFyY2hpdmVy0QAIAAlUcm9vdIABrxEHMgALAAwAGwA3ADgAOQBVAFYAVwBY fearless/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel_v2.xcdatamodel YnBsaXN0MDDUAAEAAgADAAQABQAGAAcAClgkdmVyc2lvblkkYXJjaGl2ZXJUJHRvcFgkb2JqZWN0 -cxIAAYagXxAPTlNLZXllZEFyY2hpdmVy0QAIAAlUcm9vdIABrxEHRwALAAwAGwA3ADgAOQBXAFgAWQBaAFsAXABdAF4AXwBgAGEAYgBjAH4AfwCAAIYAhwCTAKkAqgCrAKwArQCuAK8AsACxALIAywDOANUA2wDqAPkA/AELARoBHQB9AS0BPAFAAUQBUwFZAVoBYgFxAXIBewGDAYQBhQGaAZsBowGkAaUBsQHFAcYBxwHIAckBygHLAcwBzQHcAesB+gH+Ag0CHAIdAiwCOwJKAlYCaAJpAmoCawJsAm0CbgJvAn4CjQKcAqsCrAK7AsoC2QLhAvYC9wL/AwsDHwMuAz0DTANQA18DbgN9A4wDmwOnA7kDyAPXA+YD9QP2BAUEFAQjBCQEJwQwBEoESwRRBF0EcwSCBIUElASjBKYEtQTEBMcE1gTlBOkE+AUHBQgFFgUXBRgFGQUaBRsFMAUxBTkFOgVGBVoFaQV4BYcFiwWaBakFuAXHBdYF4gXyBfMF9AX1BfYF9wX4BgcGIQYiBigGNAZKBlkGXAZrBnoGfQaMBpsGngatBrwGwAbPBt4G7AbtBu4G7wbwBvEHBgcHBw8HGwcvBz4HTQdcB2AHbwd+B40HnAerB7cHyQfYB+cH9ggFCBQIIwgyCEcISAhQCFwIcAh/CI4InQihCLAIvwjOCN0I7Aj4CQoJGQkoCTcJRglHCVYJZQl0CYkJigmSCZ4JsgnBCdAJ3wnjCfIKAQoQCh8KLgo6CkwKWwpqCnkKiAqXCqYKtQrKCssK0wrfCvMLAgsRCyALJAszC0ILUQtgC28LewuNC5wLnQusC7sLygvLC9oL6Qv4DA0MDgwWDCIMNgxFDFQMYwxnDHYMhQyUDKMMsgy+DNAM3wzuDP0NDA0bDSoNOQ08DUsAMQ1aDWkNag15DYgNkA2lDaYNrg26Dc4N3Q3sDfsN/w4ODh0OLA47DkoOVg5oDmkOeA6HDpYOpQ6/DsAOxg7SDugO9w76DwkPGA8cDyAPLw8+D0EPUA9fD2MPcg+BD60Prg+vD7APsQ+yD7MPtA+1D7YPtw+4D7kPug+7D7wPvQ++D78P1A/VD90P6Q/9EAwQGxAqEC4QPRBMEFsQahB5EIUQlxCmELUQxBDTEOIQ8REAERURFhEeESoRPhFNEVwRaxFvEX4RjRGcEasRuhHGEdgR5xH2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WChYN1hGWEdYUVhSWFNYaFhpWHFYfViRWKBYr1i+WMJY0VjgWO9Y/lkNWRlZK1k6WUlZWFlnWXZZhVmUWalZqlmyWb5Z0lnhWfBZ/1oDWhJaIVowWj9aTlpaWmxae1qKWplaqFq3WsZa1VrqWuta81r/WxNbIlsxW0BbRFtTW2JbcVuAW49bm1utW7xby1vaW+lb+FwHXBZcGVwdXCFcJVwtXDBcNFUkbnVsbNcADQAOAA8AEAARABIAEwAUABUAFgAXABgAFwAaXxAPX3hkX3Jvb3RQYWNrYWdlViRjbGFzc1xfeGRfY29tbWVudHNfEBBfeGRfbW9kZWxNYW5hZ2VyXxAVX2NvbmZpZ3VyYXRpb25zQnlOYW1lXV94ZF9tb2RlbE5hbWVfEBdfbW9kZWxWZXJzaW9uSWRlbnRpZmllcoACgQdGgQdEgACBB0WAAIEC7N4AHAAdAB4AHwAgACEAIgAOACMAJAAlACYAJwAoACkAKgArAAkAKQAXAC8AMAAxADIAMwApACkAF18QHFhEQnVja2V0Rm9yQ2xhc3Nlc3dhc0VuY29kZWRfEBpYREJ1Y2tldEZvclBhY2thZ2Vzc3RvcmFnZV8QHFhEQnVja2V0Rm9ySW50ZXJmYWNlc3N0b3JhZ2VfEA9feGRfb3duaW5nTW9kZWxfEB1YREJ1Y2tldEZvclBhY2thZ2Vzd2FzRW5jb2RlZFZfb3duZXJfEBtYREJ1Y2tldEZvckRhdGFUeXBlc3N0b3JhZ2VbX3Zpc2liaWxpdHlfEBlYREJ1Y2tldEZvckNsYXNzZXNzdG9yYWdlVV9uYW1lXxAfWERCdWNrZXRGb3JJbnRlcmZhY2Vzd2FzRW5jb2RlZF8QHlhEQnVja2V0Rm9yRGF0YVR5cGVzd2FzRW5jb2RlZF8QEF91bmlxdWVFbGVtZW50SUSABIEHQoEHQIABgASAAIEHQYEHQxAAgAWAA4AEgASAAFBTWUVT0wA6ADsADgA8AEkAVldOUy5rZXlzWk5TLm9iamVjdHOsAD0APgA/AEAAQQBCAEMARABFAEYARwBIgAaAB4AIgAmACoALgAyADYAOgA+AEIARrABKAEsATABNAE4ATwBQAFEAUgBTAFQAVYASgIGBBCmAsoEEbYEBXoEE+oEFP4ECfYEFtIEG0IEG5IAwXxASQ0RDaGFpblN0b3JhZ2VJdGVtXENEQ2hhaW5Bc3NldF1DRFNpbmdsZVZhbHVlV0NEQXNzZXRdQ0RDb250YWN0SXRlbVdDRENoYWluW0NEU3Rhc2hJdGVtXxAVQ0RSdW50aW1lTWV0YWRhdGFJdGVtW0NEQ2hhaW5Ob2RlXxAYQ0RUcmFuc2FjdGlvbkhpc3RvcnlJdGVtXENETW9ja0VudGl0eV5DRFBoaXNoaW5nSXRlbd8QEABkAGUAZgBnACEAaABpACMAagBrAA4AJQBsAG0AKABuAG8AcAApACkAFAB0AHUAMQApAG8AeAA9AG8AewB8AH1fECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc29yZGVyZWRfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3N0b3JhZ2VfECBYREJ1Y2tldEZvclN0ZXJlb3R5cGVzd2FzRW5jb2RlZF8QJFhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzd2FzRW5jb2RlZF8QHVhEQnVja2V0Rm9yU3RlcmVvdHlwZXNzdG9yYWdlXxAkWERCdWNrZXRGb3JHZW5lcmFsaXphdGlvbnNkdXBsaWNhdGVzXxAkWERCdWNrZXRGb3JHZW5lcmFsaXphdGlvbnN3YXNFbmNvZGVkXxAdWERCdWNrZXRGb3JTdGVyZW90eXBlc29yZGVyZWRfECFYREJ1Y2tldEZvckdlbmVyYWxpemF0aW9uc29yZGVyZWRfECFYREJ1Y2tldEZvckdlbmVyYWxpemF0aW9uc3N0b3JhZ2VbX2lzQWJzdHJhY3SAFIA4gASABIACgBWAfoAEgBSAgIAGgBSAf4ATCBMAAAABHlljYFdvcmRlcmVk0wA6ADsADgCBAIMAVqEAgoAWoQCEgBeAMF5YRF9QU3RlcmVvdHlwZdkAIQAlAIgADgAoAIkAIwBuAIoASgCCAG8AjgAXACkAMQB9AJJfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc29yZGVyZWRfECRYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3dhc0VuY29kZWRfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3N0b3JhZ2WAEoAWgBSAN4AAgAQIgBjTADoAOwAOAJQAngBWqQCVAJYAlwCYAJkAmgCbAJwAnYAZgBqAG4AcgB2AHoAf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tAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcAFwAXAv0AfQB9AH0AMQB9AMUCXgB9AH0AFwB9gACAAIAAgHQICAgIgCWAXAgIgAAIWmR1cGxpY2F0ZXPSADsADgQlAM2ggCTSAM8A0AQoBClaWERQTUVudGl0eacEKgQrBCwELQQuBC8A1FpYRFBNRW50aXR5XVhEVU1MQ2xhc3NJbXBfEBJYRFVNTENsYXNzaWZpZXJJbXBfEBFYRFVNTE5hbWVzcGFjZUltcF8QFFhEVU1MTmFtZWRFbGVtZW50SW1wXxAPWERVTUxFbGVtZW50SW1w3xAQBDEEMgQzBDQAIQQ1BDYAIwQ3BDgADgAlBDkEOgAoAG4AbwQ8ACkAKQAUBEAAdQAxACkAbwB4AD4AbwRHBEgAfV8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzb3JkZXJlZF8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzc3RvcmFnZV8QIFhEQnVja2V0Rm9yU3RlcmVvdHlwZXN3YXNFbmNvZGVkXxAkWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXN3YXNFbmNvZGVkXxAdWERCdWNrZXRGb3JTdGVyZW90eXBlc3N0b3JhZ2VfECRYREJ1Y2tldEZvckdlbmVyYWxpemF0aW9uc2R1cGxpY2F0ZXNfECRYREJ1Y2tldEZvckdlbmVyYWxpemF0aW9uc3dhc0VuY29kZWRfEB1YREJ1Y2tldEZvclN0ZXJlb3R5cGVzb3JkZXJlZF8QIVhEQnVja2V0Rm9yR2VuZXJhbGl6YXRpb25zb3JkZXJlZF8QIVhEQnVja2V0Rm9yR2VuZXJhbGl6YXRpb25zc3RvcmFnZYAUgJSABIAEgAKAg4B+gASAFICAgAeAFIEEKICCCBJ/s4PS0wA6ADsADgRMBE4AVqEAgoAWoQRPgISAMNkAIQAlBFIADgAoBFMAIwBuBFQASwCCAG8AjgAXACkAMQB9BFxfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc29yZGVyZWRfECRYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3dhc0VuY29kZWRfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3N0b3JhZ2WAgYAWgBSAN4AAgAQIgIXTADoAOwAOBF4EaABWqQCVAJYAlwCYAJkAmgCbAJwAnYAZgBqAG4AcgB2AHoAfgCCAIakEaQRqBGsEbARtBG4EbwRwBHGAhoCIgImAi4CMgI6Aj4CRgJKAMN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXBHUAFwRPAH0AfQB9ADEAfQDFAJUAfQB9ABcAfYAAgIeAAICECAgICIAlgBkICIAACNIAOwAOBIMAzaCAJN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXABcAFwRPAH0AfQB9ADEAfQDFAJYAfQB9ABcAfYAAgACAAICECAgICIAlgBoICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXBJYAFwRPAH0AfQB9ADEAfQDFAJcAfQB9ABcAfYAAgIqAAICECAgICIAlgBsICIAACNIAOwAOBKQAzaCAJN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXABcAFwRPAH0AfQB9ADEAfQDFAJgAfQB9ABcAfYAAgACAAICECAgICIAlgBwICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXBLcAFwRPAH0AfQB9ADEAfQDFAJkAfQB9ABcAfYAAgI2AAICECAgICIAlgB0ICIAACNIAOwAOBMUAzaCAJN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXAR8AFwRPAH0AfQB9ADEAfQDFAJoAfQB9ABcAfYAAgC2AAICECAgICIAlgB4ICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXBNgAFwRPAH0AfQB9ADEAfQDFAJsAfQB9ABcAfYAAgJCAAICECAgICIAlgB8ICIAACNMAOgA7AA4E5gTnAFagoIAw3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcBRgAXBE8AfQB9AH0AMQB9AMUAnAB9AH0AFwB9gACAMoAAgIQICAgIgCWAIAgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcE+gAXBE8AfQB9AH0AMQB9AMUAnQB9AH0AFwB9gACAk4AAgIQICAgIgCWAIQgIgAAIXENEQ2hhaW5Bc3NldNMAOgA7AA4FCQUPAFalBQoFCwUMBQ0FDoCVgJaAl4CYgJmlBRAFEQUSBRMFFICagQFJgQPigQP6gQQRgDBVYXNzZXRVY2hhaW5fEBFwdXJjaGFzZVByb3ZpZGVyc1dzdGFraW5nV2Fzc2V0SWTfEBIAswC0ALUFHAAhALcAuAUdACMAtgUeALkADgAlALoAuwAoALwAFwAXABcAKQBLAH0AfQUmADEAfQBvAH0FKgUKAH0AfQUuAH1fECBYREJ1Y2tldEZvclN0ZXJlb3R5cGVzd2FzRW5jb2RlZF8QHVhEQnVja2V0Rm9yU3RlcmVvdHlwZXNzdG9yYWdlXxAdWERCdWNrZXRGb3JTdGVyZW90eXBlc29yZGVyZWSAAIAAgACABICBCAiAnAiAFAiBAUiAlQgIgJsIEtn4JgXTADoAOwAOBTIFNQBWogGdBTSAPoCdogU2BTeAnoCpgDBfEBBYRF9QUl9TdGVyZW90eXBl2QAhACUFOwAOACgFPAAjAG4FPQUQAZ0AbwCOABcAKQAxAH0FRV8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzb3JkZXJlZF8QJFhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzd2FzRW5jb2RlZF8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzc3RvcmFnZYCagD6AFIA3gACABAiAn9MAOgA7AA4FRwVQAFaoAbMBtAG1AbYBtwG4AbkBuoBCgEOARIBFgEaAR4BIgEmoBVEFUgVTBVQFVQVWBVcFWICggKGAooCkgKWApoCngKiAMN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXAR8AFwU2AH0AfQB9ADEAfQDFAbMAfQB9ABcAfYAAgC2AAICeCAgICIAlgEIICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXABcAFwU2AH0AfQB9ADEAfQDFAbQAfQB9ABcAfYAAgACAAICeCAgICIAlgEMICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXBXoAFwU2AH0AfQB9ADEAfQDFAbUAfQB9ABcAfYAAgKOAAICeCAgICIAlgEQICIAACNMAOgA7AA4FiAWJAFagoIAw3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcBHwAXBTYAfQB9AH0AMQB9AMUBtgB9AH0AFwB9gACALYAAgJ4ICAgIgCWARQgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcCDwAXBTYAfQB9AH0AMQB9AMUBtwB9AH0AFwB9gACAUIAAgJ4ICAgIgCWARggIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcBHwAXBTYAfQB9AH0AMQB9AMUBuAB9AH0AFwB9gACALYAAgJ4ICAgIgCWARwgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcAFwAXBTYAfQB9AH0AMQB9AMUBuQB9AH0AFwB9gACAAIAAgJ4ICAgIgCWASAgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcBHwAXBTYAfQB9AH0AMQB9AMUBugB9AH0AFwB9gACALYAAgJ4ICAgIgCWASQgIgAAI2QAhACUF1wAOACgF2AAjAG4F2QUQBTQAbwCOABcAKQAxAH0F4V8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzb3JkZXJlZF8QJFhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzd2FzRW5jb2RlZF8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzc3RvcmFnZYCagJ2AFIA3gACABAiAqtMAOgA7AA4F4wXqAFamBeQF5QXmBecF6AXpgKuArICtgK6Ar4CwpgXrBewF7QXuBe8F8ICxgQFBgQFCgQFEgQFGgQFHgDBfEBJYRF9QUktfZGVzdGluYXRpb25eWERfUFJLX29yZGVyZWRfEA9YRF9QUktfbWluQ291bnRfEBFYRF9QUktfZGVsZXRlUnVsZV8QD1hEX1BSS19tYXhDb3VudF8QD1hEX1BSS19pc1RvTWFued8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXAE0AFwU3AH0AfQB9ADEAfQDFBeQAfQB9ABcAfYAAgLKAAICpCAgICIAlgKsICIAACN8QEAYIBgkGCgYLACEGDAYNACMGDgYPAA4AJQYQBhEAKABuAG8GEwApACkAFAYXAHUAMQApAG8AeABAAG8GHgYfAH1fECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc29yZGVyZWRfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3N0b3JhZ2VfECBYREJ1Y2tldEZvclN0ZXJlb3R5cGVzd2FzRW5jb2RlZF8QJFhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzd2FzRW5jb2RlZF8QHVhEQnVja2V0Rm9yU3RlcmVvdHlwZXNzdG9yYWdlXxAkWERCdWNrZXRGb3JHZW5lcmFsaXphdGlvbnNkdXBsaWNhdGVzXxAkWERCdWNrZXRGb3JHZW5lcmFsaXphdGlvbnN3YXNFbmNvZGVkXxAdWERCdWNrZXRGb3JTdGVyZW90eXBlc29yZGVyZWRfECFYREJ1Y2tldEZvckdlbmVyYWxpemF0aW9uc29yZGVyZWRfECFYREJ1Y2tldEZvckdlbmVyYWxpemF0aW9uc3N0b3JhZ2WAFIDEgASABIACgLSAfoAEgBSAgIAJgBSBAUCAswgS1adrp9MAOgA7AA4GIwYlAFahAIKAFqEGJoC1gDDZACEAJQYpAA4AKAYqACMAbgYrAE0AggBvAI4AFwApADEAfQYzXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNvcmRlcmVkXxAkWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXN3YXNFbmNvZGVkXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNzdG9yYWdlgLKAFoAUgDeAAIAECIC20wA6ADsADgY1Bj8AVqkAlQCWAJcAmACZAJoAmwCcAJ2AGYAagBuAHIAdgB6AH4AggCGpBkAGQQZCBkMGRAZFBkYGRwZIgLeAuYC6gLyAvYC/gMCAwoDDgDDfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwZMABcGJgB9AH0AfQAxAH0AxQCVAH0AfQAXAH2AAIC4gACAtQgICAiAJYAZCAiAAAjSADsADgZaAM2ggCTfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwAXABcGJgB9AH0AfQAxAH0AxQCWAH0AfQAXAH2AAIAAgACAtQgICAiAJYAaCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwZtABcGJgB9AH0AfQAxAH0AxQCXAH0AfQAXAH2AAIC7gACAtQgICAiAJYAbCAiAAAjSADsADgZ7AM2ggCTfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwAXABcGJgB9AH0AfQAxAH0AxQCYAH0AfQAXAH2AAIAAgACAtQgICAiAJYAcCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwaOABcGJgB9AH0AfQAxAH0AxQCZAH0AfQAXAH2AAIC+gACAtQgICAiAJYAdCAiAAAjSADsADgacAM2ggCTfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwEfABcGJgB9AH0AfQAxAH0AxQCaAH0AfQAXAH2AAIAtgACAtQgICAiAJYAeCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwavABcGJgB9AH0AfQAxAH0AxQCbAH0AfQAXAH2AAIDBgACAtQgICAiAJYAfCAiAAAjTADoAOwAOBr0GvgBWoKCAMN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXAUYAFwYmAH0AfQB9ADEAfQDFAJwAfQB9ABcAfYAAgDKAAIC1CAgICIAlgCAICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXAEAAFwYmAH0AfQB9ADEAfQDFAJ0AfQB9ABcAfYAAgAmAAIC1CAgICIAlgCEICIAACNMAOgA7AA4G3wblAFalBuAG4QbiBuMG5IDFgMaAx4DIgMmlBuYG5wboBukG6oDKgOGA+YEBEIEBKYAwV2NoYWluSWRUaWNvblJpZFlwcmVjaXNpb25XcHJpY2VJZN8QEgCzALQAtQbyACEAtwC4BvMAIwC2BvQAuQAOACUAugC7ACgAvAAXABcAFwApAE0AfQB9BvwAMQB9AG8AfQGUBuAAfQB9BwQAfV8QIFhEQnVja2V0Rm9yU3RlcmVvdHlwZXN3YXNFbmNvZGVkXxAdWERCdWNrZXRGb3JTdGVyZW90eXBlc3N0b3JhZ2VfEB1YREJ1Y2tldEZvclN0ZXJlb3R5cGVzb3JkZXJlZIAAgACAAIAEgLIICIDMCIAUCIBlgMUICIDLCBJuEXXZ0wA6ADsADgcIBwsAVqIBnQGegD6AP6IHDAcNgM2A2IAw2QAhACUHEAAOACgHEQAjAG4HEgbmAZ0AbwCOABcAKQAxAH0HGl8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzb3JkZXJlZF8QJFhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzd2FzRW5jb2RlZF8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzc3RvcmFnZYDKgD6AFIA3gACABAiAztMAOgA7AA4HHAclAFaoAbMBtAG1AbYBtwG4AbkBuoBCgEOARIBFgEaAR4BIgEmoByYHJwcoBykHKgcrBywHLYDPgNCA0YDTgNSA1YDWgNeAMN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXAR8AFwcMAH0AfQB9ADEAfQDFAbMAfQB9ABcAfYAAgC2AAIDNCAgICIAlgEIICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXABcAFwcMAH0AfQB9ADEAfQDFAbQAfQB9ABcAfYAAgACAAIDNCAgICIAlgEMICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXB08AFwcMAH0AfQB9ADEAfQDFAbUAfQB9ABcAfYAAgNKAAIDNCAgICIAlgEQICIAACNMAOgA7AA4HXQdeAFagoIAw3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcBHwAXBwwAfQB9AH0AMQB9AMUBtgB9AH0AFwB9gACALYAAgM0ICAgIgCWARQgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcBHwAXBwwAfQB9AH0AMQB9AMUBtwB9AH0AFwB9gACALYAAgM0ICAgIgCWARggIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcBHwAXBwwAfQB9AH0AMQB9AMUBuAB9AH0AFwB9gACALYAAgM0ICAgIgCWARwgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcAFwAXBwwAfQB9AH0AMQB9AMUBuQB9AH0AFwB9gACAAIAAgM0ICAgIgCWASAgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcBHwAXBwwAfQB9AH0AMQB9AMUBugB9AH0AFwB9gACALYAAgM0ICAgIgCWASQgIgAAI2QAhACUHrAAOACgHrQAjAG4HrgbmAZ4AbwCOABcAKQAxAH0Htl8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzb3JkZXJlZF8QJFhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzd2FzRW5jb2RlZF8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzc3RvcmFnZYDKgD+AFIA3gACABAiA2dMAOgA7AA4HuAfAAFanAlgCWQJaAlsCXAJdAl6AVoBXgFiAWYBagFuAXKcHwQfCB8MHxAfFB8YHx4DagNuA3IDdgN6A34DggDDfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwAXABcHDQB9AH0AfQAxAH0AxQJYAH0AfQAXAH2AAIAAgACA2AgICAiAJYBWCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwEfABcHDQB9AH0AfQAxAH0AxQJZAH0AfQAXAH2AAIAtgACA2AgICAiAJYBXCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwAXABcHDQB9AH0AfQAxAH0AxQJaAH0AfQAXAH2AAIAAgACA2AgICAiAJYBYCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwPoABcHDQB9AH0AfQAxAH0AxQJbAH0AfQAXAH2AAIB6gACA2AgICAiAJYBZCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwAXABcHDQB9AH0AfQAxAH0AxQJcAH0AfQAXAH2AAIAAgACA2AgICAiAJYBaCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwAXABcHDQB9AH0AfQAxAH0AxQJdAH0AfQAXAH2AAIAAgACA2AgICAiAJYBbCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwAXABcHDQB9AH0AfQAxAH0AxQJeAH0AfQAXAH2AAIAAgACA2AgICAiAJYBcCAiAAAjfEBIAswC0ALUIMwAhALcAuAg0ACMAtgg1ALkADgAlALoAuwAoALwAFwAXABcAKQBNAH0AfQg9ADEAfQBvAH0BlAbhAH0AfQhFAH1fECBYREJ1Y2tldEZvclN0ZXJlb3R5cGVzd2FzRW5jb2RlZF8QHVhEQnVja2V0Rm9yU3RlcmVvdHlwZXNzdG9yYWdlXxAdWERCdWNrZXRGb3JTdGVyZW90eXBlc29yZGVyZWSAAIAAgACABICyCAiA4wiAFAiAZYDGCAiA4ggSZSeNNNMAOgA7AA4ISQhMAFaiAZ0BnoA+gD+iCE0IToDkgO+AMNkAIQAlCFEADgAoCFIAIwBuCFMG5wGdAG8AjgAXACkAMQB9CFtfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc29yZGVyZWRfECRYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3dhc0VuY29kZWRfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3N0b3JhZ2WA4YA+gBSAN4AAgAQIgOXTADoAOwAOCF0IZgBWqAGzAbQBtQG2AbcBuAG5AbqAQoBDgESARYBGgEeASIBJqAhnCGgIaQhqCGsIbAhtCG6A5oDngOiA6oDrgOyA7YDugDDfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwEfABcITQB9AH0AfQAxAH0AxQGzAH0AfQAXAH2AAIAtgACA5AgICAiAJYBCCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwAXABcITQB9AH0AfQAxAH0AxQG0AH0AfQAXAH2AAIAAgACA5AgICAiAJYBDCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwiQABcITQB9AH0AfQAxAH0AxQG1AH0AfQAXAH2AAIDpgACA5AgICAiAJYBECAiAAAjTADoAOwAOCJ4InwBWoKCAMN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXAR8AFwhNAH0AfQB9ADEAfQDFAbYAfQB9ABcAfYAAgC2AAIDkCAgICIAlgEUICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXAg8AFwhNAH0AfQB9ADEAfQDFAbcAfQB9ABcAfYAAgFCAAIDkCAgICIAlgEYICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXAR8AFwhNAH0AfQB9ADEAfQDFAbgAfQB9ABcAfYAAgC2AAIDkCAgICIAlgEcICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXABcAFwhNAH0AfQB9ADEAfQDFAbkAfQB9ABcAfYAAgACAAIDkCAgICIAlgEgICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXAR8AFwhNAH0AfQB9ADEAfQDFAboAfQB9ABcAfYAAgC2AAIDkCAgICIAlgEkICIAACNkAIQAlCO0ADgAoCO4AIwBuCO8G5wGeAG8AjgAXACkAMQB9CPdfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc29yZGVyZWRfECRYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3dhc0VuY29kZWRfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3N0b3JhZ2WA4YA/gBSAN4AAgAQIgPDTADoAOwAOCPkJAQBWpwJYAlkCWgJbAlwCXQJegFaAV4BYgFmAWoBbgFynCQIJAwkECQUJBgkHCQiA8YDygPOA9ID2gPeA+IAw3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcAFwAXCE4AfQB9AH0AMQB9AMUCWAB9AH0AFwB9gACAAIAAgO8ICAgIgCWAVggIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcBHwAXCE4AfQB9AH0AMQB9AMUCWQB9AH0AFwB9gACALYAAgO8ICAgIgCWAVwgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcAFwAXCE4AfQB9AH0AMQB9AMUCWgB9AH0AFwB9gACAAIAAgO8ICAgIgCWAWAgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcJOQAXCE4AfQB9AH0AMQB9AMUCWwB9AH0AFwB9gACA9YAAgO8ICAgIgCWAWQgIgAAIEQSw3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcAFwAXCE4AfQB9AH0AMQB9AMUCXAB9AH0AFwB9gACAAIAAgO8ICAgIgCWAWggIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcAFwAXCE4AfQB9AH0AMQB9AMUCXQB9AH0AFwB9gACAAIAAgO8ICAgIgCWAWwgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcAFwAXCE4AfQB9AH0AMQB9AMUCXgB9AH0AFwB9gACAAIAAgO8ICAgIgCWAXAgIgAAI3xASALMAtAC1CXUAIQC3ALgJdgAjALYJdwC5AA4AJQC6ALsAKAC8ABcAFwAXACkATQB9AH0JfwAxAH0AbwB9AZQG4gB9AH0JhwB9XxAgWERCdWNrZXRGb3JTdGVyZW90eXBlc3dhc0VuY29kZWRfEB1YREJ1Y2tldEZvclN0ZXJlb3R5cGVzc3RvcmFnZV8QHVhEQnVja2V0Rm9yU3RlcmVvdHlwZXNvcmRlcmVkgACAAIAAgASAsggIgPsIgBQIgGWAxwgIgPoIEko/5sPTADoAOwAOCYsJjgBWogGdAZ6APoA/ogmPCZCA/IEBB4Aw2QAhACUJkwAOACgJlAAjAG4JlQboAZ0AbwCOABcAKQAxAH0JnV8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzb3JkZXJlZF8QJFhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzd2FzRW5jb2RlZF8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzc3RvcmFnZYD5gD6AFIA3gACABAiA/dMAOgA7AA4JnwmoAFaoAbMBtAG1AbYBtwG4AbkBuoBCgEOARIBFgEaAR4BIgEmoCakJqgmrCawJrQmuCa8JsID+gP+BAQCBAQKBAQOBAQSBAQWBAQaAMN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXAR8AFwmPAH0AfQB9ADEAfQDFAbMAfQB9ABcAfYAAgC2AAID8CAgICIAlgEIICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXABcAFwmPAH0AfQB9ADEAfQDFAbQAfQB9ABcAfYAAgACAAID8CAgICIAlgEMICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXCdIAFwmPAH0AfQB9ADEAfQDFAbUAfQB9ABcAfYAAgQEBgACA/AgICAiAJYBECAiAAAjTADoAOwAOCeAJ4QBWoKCAMN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXAR8AFwmPAH0AfQB9ADEAfQDFAbYAfQB9ABcAfYAAgC2AAID8CAgICIAlgEUICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXAR8AFwmPAH0AfQB9ADEAfQDFAbcAfQB9ABcAfYAAgC2AAID8CAgICIAlgEYICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXAR8AFwmPAH0AfQB9ADEAfQDFAbgAfQB9ABcAfYAAgC2AAID8CAgICIAlgEcICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXABcAFwmPAH0AfQB9ADEAfQDFAbkAfQB9ABcAfYAAgACAAID8CAgICIAlgEgICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXAR8AFwmPAH0AfQB9ADEAfQDFAboAfQB9ABcAfYAAgC2AAID8CAgICIAlgEkICIAACNkAIQAlCi8ADgAoCjAAIwBuCjEG6AGeAG8AjgAXACkAMQB9CjlfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc29yZGVyZWRfECRYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3dhc0VuY29kZWRfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3N0b3JhZ2WA+YA/gBSAN4AAgAQIgQEI0wA6ADsADgo7CkMAVqcCWAJZAloCWwJcAl0CXoBWgFeAWIBZgFqAW4BcpwpECkUKRgpHCkgKSQpKgQEJgQEKgQELgQEMgQENgQEOgQEPgDDfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwAXABcJkAB9AH0AfQAxAH0AxQJYAH0AfQAXAH2AAIAAgACBAQcICAgIgCWAVggIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcBHwAXCZAAfQB9AH0AMQB9AMUCWQB9AH0AFwB9gACALYAAgQEHCAgICIAlgFcICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXABcAFwmQAH0AfQB9ADEAfQDFAloAfQB9ABcAfYAAgACAAIEBBwgICAiAJYBYCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwPoABcJkAB9AH0AfQAxAH0AxQJbAH0AfQAXAH2AAIB6gACBAQcICAgIgCWAWQgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcAFwAXCZAAfQB9AH0AMQB9AMUCXAB9AH0AFwB9gACAAIAAgQEHCAgICIAlgFoICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXABcAFwmQAH0AfQB9ADEAfQDFAl0AfQB9ABcAfYAAgACAAIEBBwgICAiAJYBbCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwAXABcJkAB9AH0AfQAxAH0AxQJeAH0AfQAXAH2AAIAAgACBAQcICAgIgCWAXAgIgAAI3xASALMAtAC1CrYAIQC3ALgKtwAjALYKuAC5AA4AJQC6ALsAKAC8ABcAFwAXACkATQB9AH0KwAAxAH0AbwB9AZQG4wB9AH0KyAB9XxAgWERCdWNrZXRGb3JTdGVyZW90eXBlc3dhc0VuY29kZWRfEB1YREJ1Y2tldEZvclN0ZXJlb3R5cGVzc3RvcmFnZV8QHVhEQnVja2V0Rm9yU3RlcmVvdHlwZXNvcmRlcmVkgACAAIAAgASAsggIgQESCIAUCIBlgMgICIEBEQgTAAAAAQdfHF7TADoAOwAOCswKzwBWogGdAZ6APoA/ogrQCtGBAROBAR6AMNkAIQAlCtQADgAoCtUAIwBuCtYG6QGdAG8AjgAXACkAMQB9Ct5fECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc29yZGVyZWRfECRYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3dhc0VuY29kZWRfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3N0b3JhZ2WBARCAPoAUgDeAAIAECIEBFNMAOgA7AA4K4ArpAFaoAbMBtAG1AbYBtwG4AbkBuoBCgEOARIBFgEaAR4BIgEmoCuoK6wrsCu0K7grvCvAK8YEBFYEBFoEBF4EBGYEBGoEBG4EBHIEBHYAw3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcBHwAXCtAAfQB9AH0AMQB9AMUBswB9AH0AFwB9gACALYAAgQETCAgICIAlgEIICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXABcAFwrQAH0AfQB9ADEAfQDFAbQAfQB9ABcAfYAAgACAAIEBEwgICAiAJYBDCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwsTABcK0AB9AH0AfQAxAH0AxQG1AH0AfQAXAH2AAIEBGIAAgQETCAgICIAlgEQICIAACNMAOgA7AA4LIQsiAFagoIAw3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcBHwAXCtAAfQB9AH0AMQB9AMUBtgB9AH0AFwB9gACALYAAgQETCAgICIAlgEUICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXAR8AFwrQAH0AfQB9ADEAfQDFAbcAfQB9ABcAfYAAgC2AAIEBEwgICAiAJYBGCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwEfABcK0AB9AH0AfQAxAH0AxQG4AH0AfQAXAH2AAIAtgACBARMICAgIgCWARwgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcAFwAXCtAAfQB9AH0AMQB9AMUBuQB9AH0AFwB9gACAAIAAgQETCAgICIAlgEgICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXAR8AFwrQAH0AfQB9ADEAfQDFAboAfQB9ABcAfYAAgC2AAIEBEwgICAiAJYBJCAiAAAjZACEAJQtwAA4AKAtxACMAbgtyBukBngBvAI4AFwApADEAfQt6XxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNvcmRlcmVkXxAkWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXN3YXNFbmNvZGVkXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNzdG9yYWdlgQEQgD+AFIA3gACABAiBAR/TADoAOwAOC3wLhABWpwJYAlkCWgJbAlwCXQJegFaAV4BYgFmAWoBbgFynC4ULhguHC4gLiQuKC4uBASCBASKBASOBASSBASaBASeBASiAMN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXC48AFwrRAH0AfQB9ADEAfQDFAlgAfQB9ABcAfYAAgQEhgACBAR4ICAgIgCWAVggIgAAIUTDfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwEfABcK0QB9AH0AfQAxAH0AxQJZAH0AfQAXAH2AAIAtgACBAR4ICAgIgCWAVwgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcAFwAXCtEAfQB9AH0AMQB9AMUCWgB9AH0AFwB9gACAAIAAgQEeCAgICIAlgFgICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXC70AFwrRAH0AfQB9ADEAfQDFAlsAfQB9ABcAfYAAgQElgACBAR4ICAgIgCWAWQgIgAAIEGTfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwAXABcK0QB9AH0AfQAxAH0AxQJcAH0AfQAXAH2AAIAAgACBAR4ICAgIgCWAWggIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcAFwAXCtEAfQB9AH0AMQB9AMUCXQB9AH0AFwB9gACAAIAAgQEeCAgICIAlgFsICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXABcAFwrRAH0AfQB9ADEAfQDFAl4AfQB9ABcAfYAAgACAAIEBHggICAiAJYBcCAiAAAjfEBIAswC0ALUL+QAhALcAuAv6ACMAtgv7ALkADgAlALoAuwAoALwAFwAXABcAKQBNAH0AfQwDADEAfQBvAH0BlAbkAH0AfQwLAH1fECBYREJ1Y2tldEZvclN0ZXJlb3R5cGVzd2FzRW5jb2RlZF8QHVhEQnVja2V0Rm9yU3RlcmVvdHlwZXNzdG9yYWdlXxAdWERCdWNrZXRGb3JTdGVyZW90eXBlc29yZGVyZWSAAIAAgACABICyCAiBASsIgBQIgGWAyQgIgQEqCBMAAAABCdg3e9MAOgA7AA4MDwwSAFaiAZ0BnoA+gD+iDBMMFIEBLIEBN4Aw2QAhACUMFwAOACgMGAAjAG4MGQbqAZ0AbwCOABcAKQAxAH0MIV8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzb3JkZXJlZF8QJFhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzd2FzRW5jb2RlZF8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzc3RvcmFnZYEBKYA+gBSAN4AAgAQIgQEt0wA6ADsADgwjDCwAVqgBswG0AbUBtgG3AbgBuQG6gEKAQ4BEgEWARoBHgEiASagMLQwuDC8MMAwxDDIMMww0gQEugQEvgQEwgQEygQEzgQE0gQE1gQE2gDDfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwEfABcMEwB9AH0AfQAxAH0AxQGzAH0AfQAXAH2AAIAtgACBASwICAgIgCWAQggIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcAFwAXDBMAfQB9AH0AMQB9AMUBtAB9AH0AFwB9gACAAIAAgQEsCAgICIAlgEMICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXDFYAFwwTAH0AfQB9ADEAfQDFAbUAfQB9ABcAfYAAgQExgACBASwICAgIgCWARAgIgAAI0wA6ADsADgxkDGUAVqCggDDfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwEfABcMEwB9AH0AfQAxAH0AxQG2AH0AfQAXAH2AAIAtgACBASwICAgIgCWARQgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcCDwAXDBMAfQB9AH0AMQB9AMUBtwB9AH0AFwB9gACAUIAAgQEsCAgICIAlgEYICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXAR8AFwwTAH0AfQB9ADEAfQDFAbgAfQB9ABcAfYAAgC2AAIEBLAgICAiAJYBHCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwAXABcMEwB9AH0AfQAxAH0AxQG5AH0AfQAXAH2AAIAAgACBASwICAgIgCWASAgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcBHwAXDBMAfQB9AH0AMQB9AMUBugB9AH0AFwB9gACALYAAgQEsCAgICIAlgEkICIAACNkAIQAlDLMADgAoDLQAIwBuDLUG6gGeAG8AjgAXACkAMQB9DL1fECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc29yZGVyZWRfECRYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3dhc0VuY29kZWRfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3N0b3JhZ2WBASmAP4AUgDeAAIAECIEBONMAOgA7AA4MvwzHAFanAlgCWQJaAlsCXAJdAl6AVoBXgFiAWYBagFuAXKcMyAzJDMoMywzMDM0MzoEBOYEBOoEBO4EBPIEBPYEBPoEBP4Aw3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcAFwAXDBQAfQB9AH0AMQB9AMUCWAB9AH0AFwB9gACAAIAAgQE3CAgICIAlgFYICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXAR8AFwwUAH0AfQB9ADEAfQDFAlkAfQB9ABcAfYAAgC2AAIEBNwgICAiAJYBXCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwAXABcMFAB9AH0AfQAxAH0AxQJaAH0AfQAXAH2AAIAAgACBATcICAgIgCWAWAgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcD6AAXDBQAfQB9AH0AMQB9AMUCWwB9AH0AFwB9gACAeoAAgQE3CAgICIAlgFkICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXABcAFwwUAH0AfQB9ADEAfQDFAlwAfQB9ABcAfYAAgACAAIEBNwgICAiAJYBaCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwAXABcMFAB9AH0AfQAxAH0AxQJdAH0AfQAXAH2AAIAAgACBATcICAgIgCWAWwgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcAFwAXDBQAfQB9AH0AMQB9AMUCXgB9AH0AFwB9gACAAIAAgQE3CAgICIAlgFwICIAACNIAOwAODToAzaCAJN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXAR8AFwU3AH0AfQB9ADEAfQDFBeUAfQB9ABcAfYAAgC2AAICpCAgICIAlgKwICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXDU0AFwU3AH0AfQB9ADEAfQDFBeYAfQB9ABcAfYAAgQFDgACAqQgICAiAJYCtCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFw1cABcFNwB9AH0AfQAxAH0AxQXnAH0AfQAXAH2AAIEBRYAAgKkICAgIgCWArggIgAAIEAHfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFw1cABcFNwB9AH0AfQAxAH0AxQXoAH0AfQAXAH2AAIEBRYAAgKkICAgIgCWArwgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcBHwAXBTcAfQB9AH0AMQB9AMUF6QB9AH0AFwB9gACALYAAgKkICAgIgCWAsAgIgAAI0gDPANANiQ2KXxAQWERQTVJlbGF0aW9uc2hpcKYNiw2MDY0Njg2PANRfEBBYRFBNUmVsYXRpb25zaGlwXFhEUE1Qcm9wZXJ0eV8QEFhEVU1MUHJvcGVydHlJbXBfEBRYRFVNTE5hbWVkRWxlbWVudEltcF8QD1hEVU1MRWxlbWVudEltcN8QEgCzALQAtQ2RACEAtwC4DZIAIwC2DZMAuQAOACUAugC7ACgAvAAXABcAFwApAEsAfQB9DZsAMQB9AG8AfQUqBQsAfQB9DaMAfV8QIFhEQnVja2V0Rm9yU3RlcmVvdHlwZXN3YXNFbmNvZGVkXxAdWERCdWNrZXRGb3JTdGVyZW90eXBlc3N0b3JhZ2VfEB1YREJ1Y2tldEZvclN0ZXJlb3R5cGVzb3JkZXJlZIAAgACAAIAEgIEICIEBSwiAFAiBAUiAlggIgQFKCBIy1ri20wA6ADsADg2nDaoAVqIBnQU0gD6AnaINqw2sgQFMgQFXgDDZACEAJQ2vAA4AKA2wACMAbg2xBREBnQBvAI4AFwApADEAfQ25XxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNvcmRlcmVkXxAkWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXN3YXNFbmNvZGVkXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNzdG9yYWdlgQFJgD6AFIA3gACABAiBAU3TADoAOwAODbsNxABWqAGzAbQBtQG2AbcBuAG5AbqAQoBDgESARYBGgEeASIBJqA3FDcYNxw3IDckNyg3LDcyBAU6BAU+BAVCBAVKBAVOBAVSBAVWBAVaAMN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXAR8AFw2rAH0AfQB9ADEAfQDFAbMAfQB9ABcAfYAAgC2AAIEBTAgICAiAJYBCCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwAXABcNqwB9AH0AfQAxAH0AxQG0AH0AfQAXAH2AAIAAgACBAUwICAgIgCWAQwgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcN7gAXDasAfQB9AH0AMQB9AMUBtQB9AH0AFwB9gACBAVGAAIEBTAgICAiAJYBECAiAAAjTADoAOwAODfwN/QBWoKCAMN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXAR8AFw2rAH0AfQB9ADEAfQDFAbYAfQB9ABcAfYAAgC2AAIEBTAgICAiAJYBFCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwIPABcNqwB9AH0AfQAxAH0AxQG3AH0AfQAXAH2AAIBQgACBAUwICAgIgCWARggIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcBHwAXDasAfQB9AH0AMQB9AMUBuAB9AH0AFwB9gACALYAAgQFMCAgICIAlgEcICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXABcAFw2rAH0AfQB9ADEAfQDFAbkAfQB9ABcAfYAAgACAAIEBTAgICAiAJYBICAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwEfABcNqwB9AH0AfQAxAH0AxQG6AH0AfQAXAH2AAIAtgACBAUwICAgIgCWASQgIgAAI2QAhACUOSwAOACgOTAAjAG4OTQURBTQAbwCOABcAKQAxAH0OVV8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzb3JkZXJlZF8QJFhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzd2FzRW5jb2RlZF8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzc3RvcmFnZYEBSYCdgBSAN4AAgAQIgQFY0wA6ADsADg5XDl8AVqcF5gXnBegF5AXpBeUOXoCtgK6Ar4CrgLCArIEBWacOYA5hDmIOYw5kDmUOZoEBWoEBW4EBXIEBXYED34ED4IED4YAwXxAaWERfUFJLX2ludmVyc2VSZWxhdGlvbnNoaXDfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFw1NABcNrAB9AH0AfQAxAH0AxQXmAH0AfQAXAH2AAIEBQ4AAgQFXCAgICIAlgK0ICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXDVwAFw2sAH0AfQB9ADEAfQDFBecAfQB9ABcAfYAAgQFFgACBAVcICAgIgCWArggIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcNXAAXDawAfQB9AH0AMQB9AMUF6AB9AH0AFwB9gACBAUWAAIEBVwgICAiAJYCvCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwBPABcNrAB9AH0AfQAxAH0AxQXkAH0AfQAXAH2AAIEBXoAAgQFXCAgICIAlgKsICIAACN8QEA6mDqcOqA6pACEOqg6rACMOrA6tAA4AJQ6uDq8AKABuAG8OsQApACkAFA61AHUAMQApAG8AeABCAG8OvA69AH1fECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc29yZGVyZWRfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3N0b3JhZ2VfECBYREJ1Y2tldEZvclN0ZXJlb3R5cGVzd2FzRW5jb2RlZF8QJFhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzd2FzRW5jb2RlZF8QHVhEQnVja2V0Rm9yU3RlcmVvdHlwZXNzdG9yYWdlXxAkWERCdWNrZXRGb3JHZW5lcmFsaXphdGlvbnNkdXBsaWNhdGVzXxAkWERCdWNrZXRGb3JHZW5lcmFsaXphdGlvbnN3YXNFbmNvZGVkXxAdWERCdWNrZXRGb3JTdGVyZW90eXBlc29yZGVyZWRfECFYREJ1Y2tldEZvckdlbmVyYWxpemF0aW9uc29yZGVyZWRfECFYREJ1Y2tldEZvckdlbmVyYWxpemF0aW9uc3N0b3JhZ2WAFIEBcYAEgASAAoEBYIB+gASAFICAgAuAFIED3oEBXwgTAAAAARd/LZfTADoAOwAODsEOwwBWoQCCgBahDsSBAWGAMNkAIQAlDscADgAoDsgAIwBuDskATwCCAG8AjgAXACkAMQB9DtFfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc29yZGVyZWRfECRYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3dhc0VuY29kZWRfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3N0b3JhZ2WBAV6AFoAUgDeAAIAECIEBYtMAOgA7AA4O0w7dAFapAJUAlgCXAJgAmQCaAJsAnACdgBmAGoAbgByAHYAegB+AIIAhqQ7eDt8O4A7hDuIO4w7kDuUO5oEBY4EBZYEBZoEBaYEBaoEBbIEBbYEBb4EBcIAw3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcO6gAXDsQAfQB9AH0AMQB9AMUAlQB9AH0AFwB9gACBAWSAAIEBYQgICAiAJYAZCAiAAAjSADsADg74AM2ggCTfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwAXABcOxAB9AH0AfQAxAH0AxQCWAH0AfQAXAH2AAIAAgACBAWEICAgIgCWAGggIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcPCwAXDsQAfQB9AH0AMQB9AMUAlwB9AH0AFwB9gACBAWeAAIEBYQgICAiAJYAbCAiAAAjSADsADg8ZAM2hDxqBAWiAJNIAOwAODx0AzaEG4IDFgCTfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwAXABcOxAB9AH0AfQAxAH0AxQCYAH0AfQAXAH2AAIAAgACBAWEICAgIgCWAHAgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcPMQAXDsQAfQB9AH0AMQB9AMUAmQB9AH0AFwB9gACBAWuAAIEBYQgICAiAJYAdCAiAAAjSADsADg8/AM2ggCTfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwEfABcOxAB9AH0AfQAxAH0AxQCaAH0AfQAXAH2AAIAtgACBAWEICAgIgCWAHggIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcPUgAXDsQAfQB9AH0AMQB9AMUAmwB9AH0AFwB9gACBAW6AAIEBYQgICAiAJYAfCAiAAAjTADoAOwAOD2APYQBWoKCAMN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXAUYAFw7EAH0AfQB9ADEAfQDFAJwAfQB9ABcAfYAAgDKAAIEBYQgICAiAJYAgCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwBCABcOxAB9AH0AfQAxAH0AxQCdAH0AfQAXAH2AAIALgACBAWEICAgIgCWAIQgIgAAI0wA6ADsADg+CD5cAVq8QFA+DBuAPhQ+GBuEPiA+JD4oPiw+MD40Pjg+PD5APkQ+SD5MPlA+VD5aBAXKAxYEBc4EBdIDGgQF1gQF2gQF3gQF4gQF5gQF6gQF7gQF8gQF9gQF+gQF/gQGAgQGBgQGCgQGDrxAUD5gPmQ+aD5sPnA+dD54Pnw+gD6EPog+jD6QPpQ+mD6cPqA+pD6oPq4EBhIEBm4EBsoEByYEB4oEB+YECEIECJ4ECPoECVYECbIECw4EDJ4EDPoEDVYEDbIEDg4EDmoEDsYEDyIAwXWFkZHJlc3NQcmVmaXhdc3Rha2luZ0FwaVVybF1oYXNDcm93ZGxvYW5zXmhpc3RvcnlBcGlUeXBlVmFzc2V0c1RuYW1lXxATdHlwZXNPdmVycmlkZUNvbW1vblV0eXBlc1xzZWxlY3RlZE5vZGVVbm9kZXNZaXNUZXN0bmV0XnN0YWtpbmdBcGlUeXBlWHBhcmVudElkXxARY3Jvd2Rsb2Fuc0FwaVR5cGVfEBBjcm93ZGxvYW5zQXBpVXJsXxAPaXNFdGhlcmV1bUJhc2VkXWhpc3RvcnlBcGlVcmxbY3VzdG9tTm9kZXPfEBIAswC0ALUPwAAhALcAuA/BACMAtg/CALkADgAlALoAuwAoALwAFwAXABcAKQBPAH0AfQ/KADEAfQBvAH0BlA+DAH0AfQ/SAH1fECBYREJ1Y2tldEZvclN0ZXJlb3R5cGVzd2FzRW5jb2RlZF8QHVhEQnVja2V0Rm9yU3RlcmVvdHlwZXNzdG9yYWdlXxAdWERCdWNrZXRGb3JTdGVyZW90eXBlc29yZGVyZWSAAIAAgACABIEBXggIgQGGCIAUCIBlgQFyCAiBAYUIEnK94qnTADoAOwAOD9YP2QBWogGdAZ6APoA/og/aD9uBAYeBAZKAMNkAIQAlD94ADgAoD98AIwBuD+APmAGdAG8AjgAXACkAMQB9D+hfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc29yZGVyZWRfECRYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3dhc0VuY29kZWRfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3N0b3JhZ2WBAYSAPoAUgDeAAIAECIEBiNMAOgA7AA4P6g/zAFaoAbMBtAG1AbYBtwG4AbkBuoBCgEOARIBFgEaAR4BIgEmoD/QP9Q/2D/cP+A/5D/oP+4EBiYEBioEBi4EBjYEBjoEBj4EBkIEBkYAw3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcBHwAXD9oAfQB9AH0AMQB9AMUBswB9AH0AFwB9gACALYAAgQGHCAgICIAlgEIICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXABcAFw/aAH0AfQB9ADEAfQDFAbQAfQB9ABcAfYAAgACAAIEBhwgICAiAJYBDCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFxAdABcP2gB9AH0AfQAxAH0AxQG1AH0AfQAXAH2AAIEBjIAAgQGHCAgICIAlgEQICIAACNMAOgA7AA4QKxAsAFagoIAw3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcBHwAXD9oAfQB9AH0AMQB9AMUBtgB9AH0AFwB9gACALYAAgQGHCAgICIAlgEUICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXAR8AFw/aAH0AfQB9ADEAfQDFAbcAfQB9ABcAfYAAgC2AAIEBhwgICAiAJYBGCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwEfABcP2gB9AH0AfQAxAH0AxQG4AH0AfQAXAH2AAIAtgACBAYcICAgIgCWARwgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcAFwAXD9oAfQB9AH0AMQB9AMUBuQB9AH0AFwB9gACAAIAAgQGHCAgICIAlgEgICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXAR8AFw/aAH0AfQB9ADEAfQDFAboAfQB9ABcAfYAAgC2AAIEBhwgICAiAJYBJCAiAAAjZACEAJRB6AA4AKBB7ACMAbhB8D5gBngBvAI4AFwApADEAfRCEXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNvcmRlcmVkXxAkWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXN3YXNFbmNvZGVkXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNzdG9yYWdlgQGEgD+AFIA3gACABAiBAZPTADoAOwAOEIYQjgBWpwJYAlkCWgJbAlwCXQJegFaAV4BYgFmAWoBbgFynEI8QkBCREJIQkxCUEJWBAZSBAZWBAZaBAZeBAZiBAZmBAZqAMN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXC48AFw/bAH0AfQB9ADEAfQDFAlgAfQB9ABcAfYAAgQEhgACBAZIICAgIgCWAVggIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcBHwAXD9sAfQB9AH0AMQB9AMUCWQB9AH0AFwB9gACALYAAgQGSCAgICIAlgFcICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXABcAFw/bAH0AfQB9ADEAfQDFAloAfQB9ABcAfYAAgACAAIEBkggICAiAJYBYCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwu9ABcP2wB9AH0AfQAxAH0AxQJbAH0AfQAXAH2AAIEBJYAAgQGSCAgICIAlgFkICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXABcAFw/bAH0AfQB9ADEAfQDFAlwAfQB9ABcAfYAAgACAAIEBkggICAiAJYBaCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwAXABcP2wB9AH0AfQAxAH0AxQJdAH0AfQAXAH2AAIAAgACBAZIICAgIgCWAWwgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcAFwAXD9sAfQB9AH0AMQB9AMUCXgB9AH0AFwB9gACAAIAAgQGSCAgICIAlgFwICIAACN8QEgCzALQAtREBACEAtwC4EQIAIwC2EQMAuQAOACUAugC7ACgAvAAXABcAFwApAE8AfQB9EQsAMQB9AG8AfQGUBuAAfQB9ERMAfV8QIFhEQnVja2V0Rm9yU3RlcmVvdHlwZXN3YXNFbmNvZGVkXxAdWERCdWNrZXRGb3JTdGVyZW90eXBlc3N0b3JhZ2VfEB1YREJ1Y2tldEZvclN0ZXJlb3R5cGVzb3JkZXJlZIAAgACAAIAEgQFeCAiBAZ0IgBQIgGWAxQgIgQGcCBJseDvY0wA6ADsADhEXERoAVqIBnQGegD6AP6IRGxEcgQGegQGpgDDZACEAJREfAA4AKBEgACMAbhEhD5kBnQBvAI4AFwApADEAfREpXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNvcmRlcmVkXxAkWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXN3YXNFbmNvZGVkXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNzdG9yYWdlgQGbgD6AFIA3gACABAiBAZ/TADoAOwAOESsRNABWqAGzAbQBtQG2AbcBuAG5AbqAQoBDgESARYBGgEeASIBJqBE1ETYRNxE4ETkROhE7ETyBAaCBAaGBAaKBAaSBAaWBAaaBAaeBAaiAMN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXAR8AFxEbAH0AfQB9ADEAfQDFAbMAfQB9ABcAfYAAgC2AAIEBnggICAiAJYBCCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwAXABcRGwB9AH0AfQAxAH0AxQG0AH0AfQAXAH2AAIAAgACBAZ4ICAgIgCWAQwgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcRXgAXERsAfQB9AH0AMQB9AMUBtQB9AH0AFwB9gACBAaOAAIEBnggICAiAJYBECAiAAAjTADoAOwAOEWwRbQBWoKCAMN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXAR8AFxEbAH0AfQB9ADEAfQDFAbYAfQB9ABcAfYAAgC2AAIEBnggICAiAJYBFCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwEfABcRGwB9AH0AfQAxAH0AxQG3AH0AfQAXAH2AAIAtgACBAZ4ICAgIgCWARggIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcBHwAXERsAfQB9AH0AMQB9AMUBuAB9AH0AFwB9gACALYAAgQGeCAgICIAlgEcICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXABcAFxEbAH0AfQB9ADEAfQDFAbkAfQB9ABcAfYAAgACAAIEBnggICAiAJYBICAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwEfABcRGwB9AH0AfQAxAH0AxQG6AH0AfQAXAH2AAIAtgACBAZ4ICAgIgCWASQgIgAAI2QAhACURuwAOACgRvAAjAG4RvQ+ZAZ4AbwCOABcAKQAxAH0RxV8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzb3JkZXJlZF8QJFhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzd2FzRW5jb2RlZF8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzc3RvcmFnZYEBm4A/gBSAN4AAgAQIgQGq0wA6ADsADhHHEc8AVqcCWAJZAloCWwJcAl0CXoBWgFeAWIBZgFqAW4BcpxHQEdER0hHTEdQR1RHWgQGrgQGsgQGtgQGugQGvgQGwgQGxgDDfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwAXABcRHAB9AH0AfQAxAH0AxQJYAH0AfQAXAH2AAIAAgACBAakICAgIgCWAVggIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcBHwAXERwAfQB9AH0AMQB9AMUCWQB9AH0AFwB9gACALYAAgQGpCAgICIAlgFcICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXABcAFxEcAH0AfQB9ADEAfQDFAloAfQB9ABcAfYAAgACAAIEBqQgICAiAJYBYCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwPoABcRHAB9AH0AfQAxAH0AxQJbAH0AfQAXAH2AAIB6gACBAakICAgIgCWAWQgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcAFwAXERwAfQB9AH0AMQB9AMUCXAB9AH0AFwB9gACAAIAAgQGpCAgICIAlgFoICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXABcAFxEcAH0AfQB9ADEAfQDFAl0AfQB9ABcAfYAAgACAAIEBqQgICAiAJYBbCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwAXABcRHAB9AH0AfQAxAH0AxQJeAH0AfQAXAH2AAIAAgACBAakICAgIgCWAXAgIgAAI3xASALMAtAC1EkIAIQC3ALgSQwAjALYSRAC5AA4AJQC6ALsAKAC8ABcAFwAXACkATwB9AH0STAAxAH0AbwB9AZQPhQB9AH0SVAB9XxAgWERCdWNrZXRGb3JTdGVyZW90eXBlc3dhc0VuY29kZWRfEB1YREJ1Y2tldEZvclN0ZXJlb3R5cGVzc3RvcmFnZV8QHVhEQnVja2V0Rm9yU3RlcmVvdHlwZXNvcmRlcmVkgACAAIAAgASBAV4ICIEBtAiAFAiAZYEBcwgIgQGzCBLguxW10wA6ADsADhJYElsAVqIBnQGegD6AP6ISXBJdgQG1gQHAgDDZACEAJRJgAA4AKBJhACMAbhJiD5oBnQBvAI4AFwApADEAfRJqXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNvcmRlcmVkXxAkWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXN3YXNFbmNvZGVkXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNzdG9yYWdlgQGygD6AFIA3gACABAiBAbbTADoAOwAOEmwSdQBWqAGzAbQBtQG2AbcBuAG5AbqAQoBDgESARYBGgEeASIBJqBJ2EncSeBJ5EnoSexJ8En2BAbeBAbiBAbmBAbuBAbyBAb2BAb6BAb+AMN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXAR8AFxJcAH0AfQB9ADEAfQDFAbMAfQB9ABcAfYAAgC2AAIEBtQgICAiAJYBCCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwAXABcSXAB9AH0AfQAxAH0AxQG0AH0AfQAXAH2AAIAAgACBAbUICAgIgCWAQwgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcSnwAXElwAfQB9AH0AMQB9AMUBtQB9AH0AFwB9gACBAbqAAIEBtQgICAiAJYBECAiAAAjTADoAOwAOEq0SrgBWoKCAMN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXAR8AFxJcAH0AfQB9ADEAfQDFAbYAfQB9ABcAfYAAgC2AAIEBtQgICAiAJYBFCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwIPABcSXAB9AH0AfQAxAH0AxQG3AH0AfQAXAH2AAIBQgACBAbUICAgIgCWARggIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcBHwAXElwAfQB9AH0AMQB9AMUBuAB9AH0AFwB9gACALYAAgQG1CAgICIAlgEcICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXABcAFxJcAH0AfQB9ADEAfQDFAbkAfQB9ABcAfYAAgACAAIEBtQgICAiAJYBICAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwEfABcSXAB9AH0AfQAxAH0AxQG6AH0AfQAXAH2AAIAtgACBAbUICAgIgCWASQgIgAAI2QAhACUS/AAOACgS/QAjAG4S/g+aAZ4AbwCOABcAKQAxAH0TBl8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzb3JkZXJlZF8QJFhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzd2FzRW5jb2RlZF8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzc3RvcmFnZYEBsoA/gBSAN4AAgAQIgQHB0wA6ADsADhMIExAAVqcCWAJZAloCWwJcAl0CXoBWgFeAWIBZgFqAW4BcpxMRExITExMUExUTFhMXgQHCgQHDgQHEgQHFgQHGgQHHgQHIgDDfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwAXABcSXQB9AH0AfQAxAH0AxQJYAH0AfQAXAH2AAIAAgACBAcAICAgIgCWAVggIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcBHwAXEl0AfQB9AH0AMQB9AMUCWQB9AH0AFwB9gACALYAAgQHACAgICIAlgFcICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXABcAFxJdAH0AfQB9ADEAfQDFAloAfQB9ABcAfYAAgACAAIEBwAgICAiAJYBYCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwk5ABcSXQB9AH0AfQAxAH0AxQJbAH0AfQAXAH2AAID1gACBAcAICAgIgCWAWQgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcAFwAXEl0AfQB9AH0AMQB9AMUCXAB9AH0AFwB9gACAAIAAgQHACAgICIAlgFoICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXABcAFxJdAH0AfQB9ADEAfQDFAl0AfQB9ABcAfYAAgACAAIEBwAgICAiAJYBbCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwAXABcSXQB9AH0AfQAxAH0AxQJeAH0AfQAXAH2AAIAAgACBAcAICAgIgCWAXAgIgAAI3xASALMAtAC1E4MAIQC3ALgThAAjALYThQC5AA4AJQC6ALsAKAC8ABcAFwAXACkATwB9AH0TjQAxAH0AbwB9AZQPhgB9AH0TlQB9XxAgWERCdWNrZXRGb3JTdGVyZW90eXBlc3dhc0VuY29kZWRfEB1YREJ1Y2tldEZvclN0ZXJlb3R5cGVzc3RvcmFnZV8QHVhEQnVja2V0Rm9yU3RlcmVvdHlwZXNvcmRlcmVkgACAAIAAgASBAV4ICIEBywiAFAiAZYEBdAgIgQHKCBJ8okD70wA6ADsADhOZE5wAVqIBnQGegD6AP6ITnROegQHMgQHXgDDZACEAJROhAA4AKBOiACMAbhOjD5sBnQBvAI4AFwApADEAfROrXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNvcmRlcmVkXxAkWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXN3YXNFbmNvZGVkXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNzdG9yYWdlgQHJgD6AFIA3gACABAiBAc3TADoAOwAOE60TtgBWqAGzAbQBtQG2AbcBuAG5AbqAQoBDgESARYBGgEeASIBJqBO3E7gTuRO6E7sTvBO9E76BAc6BAc+BAdCBAdKBAdOBAdSBAdWBAdaAMN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXAR8AFxOdAH0AfQB9ADEAfQDFAbMAfQB9ABcAfYAAgC2AAIEBzAgICAiAJYBCCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwAXABcTnQB9AH0AfQAxAH0AxQG0AH0AfQAXAH2AAIAAgACBAcwICAgIgCWAQwgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcT4AAXE50AfQB9AH0AMQB9AMUBtQB9AH0AFwB9gACBAdGAAIEBzAgICAiAJYBECAiAAAjTADoAOwAOE+4T7wBWoKCAMN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXAR8AFxOdAH0AfQB9ADEAfQDFAbYAfQB9ABcAfYAAgC2AAIEBzAgICAiAJYBFCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwEfABcTnQB9AH0AfQAxAH0AxQG3AH0AfQAXAH2AAIAtgACBAcwICAgIgCWARggIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcBHwAXE50AfQB9AH0AMQB9AMUBuAB9AH0AFwB9gACALYAAgQHMCAgICIAlgEcICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXABcAFxOdAH0AfQB9ADEAfQDFAbkAfQB9ABcAfYAAgACAAIEBzAgICAiAJYBICAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwEfABcTnQB9AH0AfQAxAH0AxQG6AH0AfQAXAH2AAIAtgACBAcwICAgIgCWASQgIgAAI2QAhACUUPQAOACgUPgAjAG4UPw+bAZ4AbwCOABcAKQAxAH0UR18QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzb3JkZXJlZF8QJFhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzd2FzRW5jb2RlZF8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzc3RvcmFnZYEByYA/gBSAN4AAgAQIgQHY0wA6ADsADhRJFFEAVqcCWAJZAloCWwJcAl0CXoBWgFeAWIBZgFqAW4BcpxRSFFMUVBRVFFYUVxRYgQHZgQHbgQHcgQHdgQHfgQHggQHhgDDfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFxRcABcTngB9AH0AfQAxAH0AxQJYAH0AfQAXAH2AAIEB2oAAgQHXCAgICIAlgFYICIAACFJOT98QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXAR8AFxOeAH0AfQB9ADEAfQDFAlkAfQB9ABcAfYAAgC2AAIEB1wgICAiAJYBXCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwAXABcTngB9AH0AfQAxAH0AxQJaAH0AfQAXAH2AAIAAgACBAdcICAgIgCWAWAgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcUigAXE54AfQB9AH0AMQB9AMUCWwB9AH0AFwB9gACBAd6AAIEB1wgICAiAJYBZCAiAAAgRAyDfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwAXABcTngB9AH0AfQAxAH0AxQJcAH0AfQAXAH2AAIAAgACBAdcICAgIgCWAWggIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcAFwAXE54AfQB9AH0AMQB9AMUCXQB9AH0AFwB9gACAAIAAgQHXCAgICIAlgFsICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXABcAFxOeAH0AfQB9ADEAfQDFAl4AfQB9ABcAfYAAgACAAIEB1wgICAiAJYBcCAiAAAjfEBIAswC0ALUUxgAhALcAuBTHACMAthTIALkADgAlALoAuwAoALwAFwAXABcAKQBPAH0AfRTQADEAfQBvAH0BlAbhAH0AfRTYAH1fECBYREJ1Y2tldEZvclN0ZXJlb3R5cGVzd2FzRW5jb2RlZF8QHVhEQnVja2V0Rm9yU3RlcmVvdHlwZXNzdG9yYWdlXxAdWERCdWNrZXRGb3JTdGVyZW90eXBlc29yZGVyZWSAAIAAgACABIEBXggIgQHkCIAUCIBlgMYICIEB4wgSQoghm9MAOgA7AA4U3BTfAFaiAZ0BnoA+gD+iFOAU4YEB5YEB8IAw2QAhACUU5AAOACgU5QAjAG4U5g+cAZ0AbwCOABcAKQAxAH0U7l8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzb3JkZXJlZF8QJFhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzd2FzRW5jb2RlZF8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzc3RvcmFnZYEB4oA+gBSAN4AAgAQIgQHm0wA6ADsADhTwFPkAVqgBswG0AbUBtgG3AbgBuQG6gEKAQ4BEgEWARoBHgEiASagU+hT7FPwU/RT+FP8VABUBgQHngQHogQHpgQHrgQHsgQHtgQHugQHvgDDfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwEfABcU4AB9AH0AfQAxAH0AxQGzAH0AfQAXAH2AAIAtgACBAeUICAgIgCWAQggIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcAFwAXFOAAfQB9AH0AMQB9AMUBtAB9AH0AFwB9gACAAIAAgQHlCAgICIAlgEMICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXFSMAFxTgAH0AfQB9ADEAfQDFAbUAfQB9ABcAfYAAgQHqgACBAeUICAgIgCWARAgIgAAI0wA6ADsADhUxFTIAVqCggDDfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwEfABcU4AB9AH0AfQAxAH0AxQG2AH0AfQAXAH2AAIAtgACBAeUICAgIgCWARQgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcBHwAXFOAAfQB9AH0AMQB9AMUBtwB9AH0AFwB9gACALYAAgQHlCAgICIAlgEYICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXAR8AFxTgAH0AfQB9ADEAfQDFAbgAfQB9ABcAfYAAgC2AAIEB5QgICAiAJYBHCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwAXABcU4AB9AH0AfQAxAH0AxQG5AH0AfQAXAH2AAIAAgACBAeUICAgIgCWASAgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcBHwAXFOAAfQB9AH0AMQB9AMUBugB9AH0AFwB9gACALYAAgQHlCAgICIAlgEkICIAACNkAIQAlFYAADgAoFYEAIwBuFYIPnAGeAG8AjgAXACkAMQB9FYpfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc29yZGVyZWRfECRYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3dhc0VuY29kZWRfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3N0b3JhZ2WBAeKAP4AUgDeAAIAECIEB8dMAOgA7AA4VjBWUAFanAlgCWQJaAlsCXAJdAl6AVoBXgFiAWYBagFuAXKcVlRWWFZcVmBWZFZoVm4EB8oEB84EB9IEB9YEB9oEB94EB+IAw3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcAFwAXFOEAfQB9AH0AMQB9AMUCWAB9AH0AFwB9gACAAIAAgQHwCAgICIAlgFYICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXAR8AFxThAH0AfQB9ADEAfQDFAlkAfQB9ABcAfYAAgC2AAIEB8AgICAiAJYBXCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwAXABcU4QB9AH0AfQAxAH0AxQJaAH0AfQAXAH2AAIAAgACBAfAICAgIgCWAWAgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcJOQAXFOEAfQB9AH0AMQB9AMUCWwB9AH0AFwB9gACA9YAAgQHwCAgICIAlgFkICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXABcAFxThAH0AfQB9ADEAfQDFAlwAfQB9ABcAfYAAgACAAIEB8AgICAiAJYBaCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwAXABcU4QB9AH0AfQAxAH0AxQJdAH0AfQAXAH2AAIAAgACBAfAICAgIgCWAWwgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcAFwAXFOEAfQB9AH0AMQB9AMUCXgB9AH0AFwB9gACAAIAAgQHwCAgICIAlgFwICIAACN8QEgCzALQAtRYHACEAtwC4FggAIwC2FgkAuQAOACUAugC7ACgAvAAXABcAFwApAE8AfQB9FhEAMQB9AG8AfQGUD4gAfQB9FhkAfV8QIFhEQnVja2V0Rm9yU3RlcmVvdHlwZXN3YXNFbmNvZGVkXxAdWERCdWNrZXRGb3JTdGVyZW90eXBlc3N0b3JhZ2VfEB1YREJ1Y2tldEZvclN0ZXJlb3R5cGVzb3JkZXJlZIAAgACAAIAEgQFeCAiBAfsIgBQIgGWBAXUICIEB+ggTAAAAAQFNuqHTADoAOwAOFh0WIABWogGdAZ6APoA/ohYhFiKBAfyBAgeAMNkAIQAlFiUADgAoFiYAIwBuFicPnQGdAG8AjgAXACkAMQB9Fi9fECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc29yZGVyZWRfECRYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3dhc0VuY29kZWRfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3N0b3JhZ2WBAfmAPoAUgDeAAIAECIEB/dMAOgA7AA4WMRY6AFaoAbMBtAG1AbYBtwG4AbkBuoBCgEOARIBFgEaAR4BIgEmoFjsWPBY9Fj4WPxZAFkEWQoEB/oEB/4ECAIECAoECA4ECBIECBYECBoAw3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcBHwAXFiEAfQB9AH0AMQB9AMUBswB9AH0AFwB9gACALYAAgQH8CAgICIAlgEIICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXABcAFxYhAH0AfQB9ADEAfQDFAbQAfQB9ABcAfYAAgACAAIEB/AgICAiAJYBDCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFxZkABcWIQB9AH0AfQAxAH0AxQG1AH0AfQAXAH2AAIECAYAAgQH8CAgICIAlgEQICIAACNMAOgA7AA4WchZzAFagoIAw3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcBHwAXFiEAfQB9AH0AMQB9AMUBtgB9AH0AFwB9gACALYAAgQH8CAgICIAlgEUICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXAg8AFxYhAH0AfQB9ADEAfQDFAbcAfQB9ABcAfYAAgFCAAIEB/AgICAiAJYBGCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwEfABcWIQB9AH0AfQAxAH0AxQG4AH0AfQAXAH2AAIAtgACBAfwICAgIgCWARwgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcAFwAXFiEAfQB9AH0AMQB9AMUBuQB9AH0AFwB9gACAAIAAgQH8CAgICIAlgEgICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXAR8AFxYhAH0AfQB9ADEAfQDFAboAfQB9ABcAfYAAgC2AAIEB/AgICAiAJYBJCAiAAAjZACEAJRbBAA4AKBbCACMAbhbDD50BngBvAI4AFwApADEAfRbLXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNvcmRlcmVkXxAkWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXN3YXNFbmNvZGVkXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNzdG9yYWdlgQH5gD+AFIA3gACABAiBAgjTADoAOwAOFs0W1QBWpwJYAlkCWgJbAlwCXQJegFaAV4BYgFmAWoBbgFynFtYW1xbYFtkW2hbbFtyBAgmBAgqBAguBAgyBAg2BAg6BAg+AMN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXABcAFxYiAH0AfQB9ADEAfQDFAlgAfQB9ABcAfYAAgACAAIECBwgICAiAJYBWCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwEfABcWIgB9AH0AfQAxAH0AxQJZAH0AfQAXAH2AAIAtgACBAgcICAgIgCWAVwgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcAFwAXFiIAfQB9AH0AMQB9AMUCWgB9AH0AFwB9gACAAIAAgQIHCAgICIAlgFgICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXA+gAFxYiAH0AfQB9ADEAfQDFAlsAfQB9ABcAfYAAgHqAAIECBwgICAiAJYBZCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwAXABcWIgB9AH0AfQAxAH0AxQJcAH0AfQAXAH2AAIAAgACBAgcICAgIgCWAWggIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcAFwAXFiIAfQB9AH0AMQB9AMUCXQB9AH0AFwB9gACAAIAAgQIHCAgICIAlgFsICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXABcAFxYiAH0AfQB9ADEAfQDFAl4AfQB9ABcAfYAAgACAAIECBwgICAiAJYBcCAiAAAjfEBIAswC0ALUXSAAhALcAuBdJACMAthdKALkADgAlALoAuwAoALwAFwAXABcAKQBPAH0AfRdSADEAfQBvAH0FKg+JAH0AfRdaAH1fECBYREJ1Y2tldEZvclN0ZXJlb3R5cGVzd2FzRW5jb2RlZF8QHVhEQnVja2V0Rm9yU3RlcmVvdHlwZXNzdG9yYWdlXxAdWERCdWNrZXRGb3JTdGVyZW90eXBlc29yZGVyZWSAAIAAgACABIEBXggIgQISCIAUCIEBSIEBdggIgQIRCBKnOt5p0wA6ADsADhdeF2EAVqIBnQU0gD6AnaIXYhdjgQITgQIegDDZACEAJRdmAA4AKBdnACMAbhdoD54BnQBvAI4AFwApADEAfRdwXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNvcmRlcmVkXxAkWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXN3YXNFbmNvZGVkXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNzdG9yYWdlgQIQgD6AFIA3gACABAiBAhTTADoAOwAOF3IXewBWqAGzAbQBtQG2AbcBuAG5AbqAQoBDgESARYBGgEeASIBJqBd8F30Xfhd/F4AXgReCF4OBAhWBAhaBAheBAhmBAhqBAhuBAhyBAh2AMN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXAR8AFxdiAH0AfQB9ADEAfQDFAbMAfQB9ABcAfYAAgC2AAIECEwgICAiAJYBCCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwAXABcXYgB9AH0AfQAxAH0AxQG0AH0AfQAXAH2AAIAAgACBAhMICAgIgCWAQwgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcXpQAXF2IAfQB9AH0AMQB9AMUBtQB9AH0AFwB9gACBAhiAAIECEwgICAiAJYBECAiAAAjTADoAOwAOF7MXtABWoKCAMN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXAR8AFxdiAH0AfQB9ADEAfQDFAbYAfQB9ABcAfYAAgC2AAIECEwgICAiAJYBFCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwIPABcXYgB9AH0AfQAxAH0AxQG3AH0AfQAXAH2AAIBQgACBAhMICAgIgCWARggIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcBHwAXF2IAfQB9AH0AMQB9AMUBuAB9AH0AFwB9gACALYAAgQITCAgICIAlgEcICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXABcAFxdiAH0AfQB9ADEAfQDFAbkAfQB9ABcAfYAAgACAAIECEwgICAiAJYBICAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwEfABcXYgB9AH0AfQAxAH0AxQG6AH0AfQAXAH2AAIAtgACBAhMICAgIgCWASQgIgAAI2QAhACUYAgAOACgYAwAjAG4YBA+eBTQAbwCOABcAKQAxAH0YDF8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzb3JkZXJlZF8QJFhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzd2FzRW5jb2RlZF8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzc3RvcmFnZYECEICdgBSAN4AAgAQIgQIf0wA6ADsADhgOGBYAVqcF5gXnBegF5AXpBeUOXoCtgK6Ar4CrgLCArIEBWacYFxgYGBkYGhgbGBwYHYECIIECIYECIoECI4ECJIECJYECJoAw3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcNTQAXF2MAfQB9AH0AMQB9AMUF5gB9AH0AFwB9gACBAUOAAIECHggICAiAJYCtCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFw1cABcXYwB9AH0AfQAxAH0AxQXnAH0AfQAXAH2AAIEBRYAAgQIeCAgICIAlgK4ICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXDU0AFxdjAH0AfQB9ADEAfQDFBegAfQB9ABcAfYAAgQFDgACBAh4ICAgIgCWArwgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcASwAXF2MAfQB9AH0AMQB9AMUF5AB9AH0AFwB9gACAgYAAgQIeCAgICIAlgKsICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXAg8AFxdjAH0AfQB9ADEAfQDFBekAfQB9ABcAfYAAgFCAAIECHggICAiAJYCwCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwEfABcXYwB9AH0AfQAxAH0AxQXlAH0AfQAXAH2AAIAtgACBAh4ICAgIgCWArAgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcFEQAXF2MAfQB9AH0AMQB9AMUOXgB9AH0AFwB9gACBAUmAAIECHggICAiAJYEBWQgIgAAI3xASALMAtAC1GIkAIQC3ALgYigAjALYYiwC5AA4AJQC6ALsAKAC8ABcAFwAXACkATwB9AH0YkwAxAH0AbwB9AZQPigB9AH0YmwB9XxAgWERCdWNrZXRGb3JTdGVyZW90eXBlc3dhc0VuY29kZWRfEB1YREJ1Y2tldEZvclN0ZXJlb3R5cGVzc3RvcmFnZV8QHVhEQnVja2V0Rm9yU3RlcmVvdHlwZXNvcmRlcmVkgACAAIAAgASBAV4ICIECKQiAFAiAZYEBdwgIgQIoCBMAAAABFx58+dMAOgA7AA4YnxiiAFaiAZ0BnoA+gD+iGKMYpIECKoECNYAw2QAhACUYpwAOACgYqAAjAG4YqQ+fAZ0AbwCOABcAKQAxAH0YsV8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzb3JkZXJlZF8QJFhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzd2FzRW5jb2RlZF8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzc3RvcmFnZYECJ4A+gBSAN4AAgAQIgQIr0wA6ADsADhizGLwAVqgBswG0AbUBtgG3AbgBuQG6gEKAQ4BEgEWARoBHgEiASagYvRi+GL8YwBjBGMIYwxjEgQIsgQItgQIugQIwgQIxgQIygQIzgQI0gDDfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwEfABcYowB9AH0AfQAxAH0AxQGzAH0AfQAXAH2AAIAtgACBAioICAgIgCWAQggIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcAFwAXGKMAfQB9AH0AMQB9AMUBtAB9AH0AFwB9gACAAIAAgQIqCAgICIAlgEMICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXGOYAFxijAH0AfQB9ADEAfQDFAbUAfQB9ABcAfYAAgQIvgACBAioICAgIgCWARAgIgAAI0wA6ADsADhj0GPUAVqCggDDfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwEfABcYowB9AH0AfQAxAH0AxQG2AH0AfQAXAH2AAIAtgACBAioICAgIgCWARQgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcBHwAXGKMAfQB9AH0AMQB9AMUBtwB9AH0AFwB9gACALYAAgQIqCAgICIAlgEYICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXAR8AFxijAH0AfQB9ADEAfQDFAbgAfQB9ABcAfYAAgC2AAIECKggICAiAJYBHCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwAXABcYowB9AH0AfQAxAH0AxQG5AH0AfQAXAH2AAIAAgACBAioICAgIgCWASAgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcBHwAXGKMAfQB9AH0AMQB9AMUBugB9AH0AFwB9gACALYAAgQIqCAgICIAlgEkICIAACNkAIQAlGUMADgAoGUQAIwBuGUUPnwGeAG8AjgAXACkAMQB9GU1fECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc29yZGVyZWRfECRYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3dhc0VuY29kZWRfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3N0b3JhZ2WBAieAP4AUgDeAAIAECIECNtMAOgA7AA4ZTxlXAFanAlgCWQJaAlsCXAJdAl6AVoBXgFiAWYBagFuAXKcZWBlZGVoZWxlcGV0ZXoECN4ECOIECOYECOoECO4ECPIECPYAw3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcAFwAXGKQAfQB9AH0AMQB9AMUCWAB9AH0AFwB9gACAAIAAgQI1CAgICIAlgFYICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXAR8AFxikAH0AfQB9ADEAfQDFAlkAfQB9ABcAfYAAgC2AAIECNQgICAiAJYBXCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwAXABcYpAB9AH0AfQAxAH0AxQJaAH0AfQAXAH2AAIAAgACBAjUICAgIgCWAWAgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcD6AAXGKQAfQB9AH0AMQB9AMUCWwB9AH0AFwB9gACAeoAAgQI1CAgICIAlgFkICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXABcAFxikAH0AfQB9ADEAfQDFAlwAfQB9ABcAfYAAgACAAIECNQgICAiAJYBaCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwAXABcYpAB9AH0AfQAxAH0AxQJdAH0AfQAXAH2AAIAAgACBAjUICAgIgCWAWwgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcAFwAXGKQAfQB9AH0AMQB9AMUCXgB9AH0AFwB9gACAAIAAgQI1CAgICIAlgFwICIAACN8QEgCzALQAtRnKACEAtwC4GcsAIwC2GcwAuQAOACUAugC7ACgAvAAXABcAFwApAE8AfQB9GdQAMQB9AG8AfQGUD4sAfQB9GdwAfV8QIFhEQnVja2V0Rm9yU3RlcmVvdHlwZXN3YXNFbmNvZGVkXxAdWERCdWNrZXRGb3JTdGVyZW90eXBlc3N0b3JhZ2VfEB1YREJ1Y2tldEZvclN0ZXJlb3R5cGVzb3JkZXJlZIAAgACAAIAEgQFeCAiBAkAIgBQIgGWBAXgICIECPwgS7JCIhdMAOgA7AA4Z4BnjAFaiAZ0BnoA+gD+iGeQZ5YECQYECTIAw2QAhACUZ6AAOACgZ6QAjAG4Z6g+gAZ0AbwCOABcAKQAxAH0Z8l8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzb3JkZXJlZF8QJFhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzd2FzRW5jb2RlZF8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzc3RvcmFnZYECPoA+gBSAN4AAgAQIgQJC0wA6ADsADhn0Gf0AVqgBswG0AbUBtgG3AbgBuQG6gEKAQ4BEgEWARoBHgEiASagZ/hn/GgAaARoCGgMaBBoFgQJDgQJEgQJFgQJHgQJIgQJJgQJKgQJLgDDfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwEfABcZ5AB9AH0AfQAxAH0AxQGzAH0AfQAXAH2AAIAtgACBAkEICAgIgCWAQggIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcAFwAXGeQAfQB9AH0AMQB9AMUBtAB9AH0AFwB9gACAAIAAgQJBCAgICIAlgEMICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXGicAFxnkAH0AfQB9ADEAfQDFAbUAfQB9ABcAfYAAgQJGgACBAkEICAgIgCWARAgIgAAI0wA6ADsADho1GjYAVqCggDDfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwEfABcZ5AB9AH0AfQAxAH0AxQG2AH0AfQAXAH2AAIAtgACBAkEICAgIgCWARQgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcCDwAXGeQAfQB9AH0AMQB9AMUBtwB9AH0AFwB9gACAUIAAgQJBCAgICIAlgEYICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXAR8AFxnkAH0AfQB9ADEAfQDFAbgAfQB9ABcAfYAAgC2AAIECQQgICAiAJYBHCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwAXABcZ5AB9AH0AfQAxAH0AxQG5AH0AfQAXAH2AAIAAgACBAkEICAgIgCWASAgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcBHwAXGeQAfQB9AH0AMQB9AMUBugB9AH0AFwB9gACALYAAgQJBCAgICIAlgEkICIAACNkAIQAlGoQADgAoGoUAIwBuGoYPoAGeAG8AjgAXACkAMQB9Go5fECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc29yZGVyZWRfECRYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3dhc0VuY29kZWRfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3N0b3JhZ2WBAj6AP4AUgDeAAIAECIECTdMAOgA7AA4akBqYAFanAlgCWQJaAlsCXAJdAl6AVoBXgFiAWYBagFuAXKcamRqaGpsanBqdGp4an4ECToECT4ECUIECUYECUoECU4ECVIAw3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcAFwAXGeUAfQB9AH0AMQB9AMUCWAB9AH0AFwB9gACAAIAAgQJMCAgICIAlgFYICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXAR8AFxnlAH0AfQB9ADEAfQDFAlkAfQB9ABcAfYAAgC2AAIECTAgICAiAJYBXCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwAXABcZ5QB9AH0AfQAxAH0AxQJaAH0AfQAXAH2AAIAAgACBAkwICAgIgCWAWAgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcUigAXGeUAfQB9AH0AMQB9AMUCWwB9AH0AFwB9gACBAd6AAIECTAgICAiAJYBZCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwAXABcZ5QB9AH0AfQAxAH0AxQJcAH0AfQAXAH2AAIAAgACBAkwICAgIgCWAWggIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcAFwAXGeUAfQB9AH0AMQB9AMUCXQB9AH0AFwB9gACAAIAAgQJMCAgICIAlgFsICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXABcAFxnlAH0AfQB9ADEAfQDFAl4AfQB9ABcAfYAAgACAAIECTAgICAiAJYBcCAiAAAjfEBIAswC0ALUbCwAhALcAuBsMACMAthsNALkADgAlALoAuwAoALwAFwAXABcAKQBPAH0AfRsVADEAfQBvAH0BlA+MAH0AfRsdAH1fECBYREJ1Y2tldEZvclN0ZXJlb3R5cGVzd2FzRW5jb2RlZF8QHVhEQnVja2V0Rm9yU3RlcmVvdHlwZXNzdG9yYWdlXxAdWERCdWNrZXRGb3JTdGVyZW90eXBlc29yZGVyZWSAAIAAgACABIEBXggIgQJXCIAUCIBlgQF5CAiBAlYIEjbqFebTADoAOwAOGyEbJABWogGdAZ6APoA/ohslGyaBAliBAmOAMNkAIQAlGykADgAoGyoAIwBuGysPoQGdAG8AjgAXACkAMQB9GzNfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc29yZGVyZWRfECRYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3dhc0VuY29kZWRfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3N0b3JhZ2WBAlWAPoAUgDeAAIAECIECWdMAOgA7AA4bNRs+AFaoAbMBtAG1AbYBtwG4AbkBuoBCgEOARIBFgEaAR4BIgEmoGz8bQBtBG0IbQxtEG0UbRoECWoECW4ECXIECXoECX4ECYIECYYECYoAw3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcBHwAXGyUAfQB9AH0AMQB9AMUBswB9AH0AFwB9gACALYAAgQJYCAgICIAlgEIICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXABcAFxslAH0AfQB9ADEAfQDFAbQAfQB9ABcAfYAAgACAAIECWAgICAiAJYBDCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFxtoABcbJQB9AH0AfQAxAH0AxQG1AH0AfQAXAH2AAIECXYAAgQJYCAgICIAlgEQICIAACNMAOgA7AA4bdht3AFagoIAw3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcBHwAXGyUAfQB9AH0AMQB9AMUBtgB9AH0AFwB9gACALYAAgQJYCAgICIAlgEUICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXAg8AFxslAH0AfQB9ADEAfQDFAbcAfQB9ABcAfYAAgFCAAIECWAgICAiAJYBGCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwEfABcbJQB9AH0AfQAxAH0AxQG4AH0AfQAXAH2AAIAtgACBAlgICAgIgCWARwgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcAFwAXGyUAfQB9AH0AMQB9AMUBuQB9AH0AFwB9gACAAIAAgQJYCAgICIAlgEgICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXAR8AFxslAH0AfQB9ADEAfQDFAboAfQB9ABcAfYAAgC2AAIECWAgICAiAJYBJCAiAAAjZACEAJRvFAA4AKBvGACMAbhvHD6EBngBvAI4AFwApADEAfRvPXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNvcmRlcmVkXxAkWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXN3YXNFbmNvZGVkXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNzdG9yYWdlgQJVgD+AFIA3gACABAiBAmTTADoAOwAOG9Eb2QBWpwJYAlkCWgJbAlwCXQJegFaAV4BYgFmAWoBbgFynG9ob2xvcG90b3hvfG+CBAmWBAmaBAmeBAmiBAmmBAmqBAmuAMN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXABcAFxsmAH0AfQB9ADEAfQDFAlgAfQB9ABcAfYAAgACAAIECYwgICAiAJYBWCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwEfABcbJgB9AH0AfQAxAH0AxQJZAH0AfQAXAH2AAIAtgACBAmMICAgIgCWAVwgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcAFwAXGyYAfQB9AH0AMQB9AMUCWgB9AH0AFwB9gACAAIAAgQJjCAgICIAlgFgICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXCTkAFxsmAH0AfQB9ADEAfQDFAlsAfQB9ABcAfYAAgPWAAIECYwgICAiAJYBZCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwAXABcbJgB9AH0AfQAxAH0AxQJcAH0AfQAXAH2AAIAAgACBAmMICAgIgCWAWggIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcAFwAXGyYAfQB9AH0AMQB9AMUCXQB9AH0AFwB9gACAAIAAgQJjCAgICIAlgFsICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXABcAFxsmAH0AfQB9ADEAfQDFAl4AfQB9ABcAfYAAgACAAIECYwgICAiAJYBcCAiAAAjfEBIAswC0ALUcTAAhALcAuBxNACMAthxOALkADgAlALoAuwAoALwAFwAXABcAKQBPAH0AfRxWADEAfQBvAH0FKg+NAH0AfRxeAH1fECBYREJ1Y2tldEZvclN0ZXJlb3R5cGVzd2FzRW5jb2RlZF8QHVhEQnVja2V0Rm9yU3RlcmVvdHlwZXNzdG9yYWdlXxAdWERCdWNrZXRGb3JTdGVyZW90eXBlc29yZGVyZWSAAIAAgACABIEBXggIgQJuCIAUCIEBSIEBeggIgQJtCBMAAAABIA/y/NMAOgA7AA4cYhxlAFaiAZ0FNIA+gJ2iHGYcZ4ECb4ECeoAw2QAhACUcagAOACgcawAjAG4cbA+iAZ0AbwCOABcAKQAxAH0cdF8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzb3JkZXJlZF8QJFhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzd2FzRW5jb2RlZF8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzc3RvcmFnZYECbIA+gBSAN4AAgAQIgQJw0wA6ADsADhx2HH8AVqgBswG0AbUBtgG3AbgBuQG6gEKAQ4BEgEWARoBHgEiASagcgByBHIIcgxyEHIUchhyHgQJxgQJygQJzgQJ1gQJ2gQJ3gQJ4gQJ5gDDfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwEfABccZgB9AH0AfQAxAH0AxQGzAH0AfQAXAH2AAIAtgACBAm8ICAgIgCWAQggIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcAFwAXHGYAfQB9AH0AMQB9AMUBtAB9AH0AFwB9gACAAIAAgQJvCAgICIAlgEMICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXHKkAFxxmAH0AfQB9ADEAfQDFAbUAfQB9ABcAfYAAgQJ0gACBAm8ICAgIgCWARAgIgAAI0wA6ADsADhy3HLgAVqCggDDfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwEfABccZgB9AH0AfQAxAH0AxQG2AH0AfQAXAH2AAIAtgACBAm8ICAgIgCWARQgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcCDwAXHGYAfQB9AH0AMQB9AMUBtwB9AH0AFwB9gACAUIAAgQJvCAgICIAlgEYICIAACN8QDwCzALQAtQAhALYAtwC4ACMAuQAOACUAugC7ACgAvAAXAR8AFxxmAH0AfQB9ADEAfQDFAbgAfQB9ABcAfYAAgC2AAIECbwgICAiAJYBHCAiAAAjfEA8AswC0ALUAIQC2ALcAuAAjALkADgAlALoAuwAoALwAFwAXABccZgB9AH0AfQAxAH0AxQG5AH0AfQAXAH2AAIAAgACBAm8ICAgIgCWASAgIgAAI3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcBHwAXHGYAfQB9AH0AMQB9AMUBugB9AH0AFwB9gACALYAAgQJvCAgICIAlgEkICIAACNkAIQAlHQYADgAoHQcAIwBuHQgPogU0AG8AjgAXACkAMQB9HRBfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc29yZGVyZWRfECRYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3dhc0VuY29kZWRfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3N0b3JhZ2WBAmyAnYAUgDeAAIAECIECe9MAOgA7AA4dEh0ZAFamBeQF5QXmBecF6AXpgKuArICtgK6Ar4Cwph0aHRsdHB0dHR4dH4ECfIEDIoEDI4EDJIEDJYEDJoAw3xAPALMAtAC1ACEAtgC3ALgAIwC5AA4AJQC6ALsAKAC8ABcAUgAXHGcAfQB9AH0AMQB9AMUF5AB9AH0AFwB9gACBAn2AAIECeggICAiAJYCrCAiAAAjfEBAdMR0yHTMdNAAhHTUdNgAjHTcdOAAOACUdOR06ACgAbgBvHTwAKQApABQdQAB1ADEAKQBvAHgARQBvHUcdSAB9XxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNvcmRlcmVkXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNzdG9yYWdlXxAgWERCdWNrZXRGb3JTdGVyZW90eXBlc3dhc0VuY29kZWRfECRYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3dhc0VuY29kZWRfEB1YREJ1Y2tldEZvclN0ZXJlb3R5cGVzc3RvcmFnZV8QJFhEQnVja2V0Rm9yR2VuZXJhbGl6YXRpb25zZHVwbGljYXRlc18QJFhEQnVja2V0Rm9yR2VuZXJhbGl6YXRpb25zd2FzRW5jb2RlZF8QHVhEQnVja2V0Rm9yU3RlcmVvdHlwZXNvcmRlcmVkXxAhWERCdWNrZXRGb3JHZW5lcmFsaXphdGlvbnNvcmRlcmVkXxAhWERCdWNrZXRGb3JHZW5lcmFsaXphdGlvbnNzdG9yYWdlgBSBApKABIAEgAKBAn+AfoAEgBSAgIAOgBSBAyGBAn4IElJaQ4jTADoAOwAOHUwdTgBWoQCCgBahHU+BAoCAMNkAIQAlHVIADgAoHVMAIwBuHVQAUgCCAG8AjgAXACkAMQB9HVxfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc29yZGVyZWRfECRYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3dhc0VuY29kZWRfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3N0b3JhZ2WBAn2AFoAUgDeAAIAECIECgdMAOgA7AA4dXh1oAFapAJUA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ABlkAAAZZgAAGWgAABlqAAAZawAAGWwAABltAAAZbgAAGXAAABlyAAAZcwAAGXQAABl2AAAZdwAAGXgAABm3AAAZuQAAGbsAABm9AAAZvwAAGcAAABnBAAAZwgAAGcMAABnFAAAZxwAAGcgAABnJAAAZywAAGcwAABnZAAAZ2gAAGdsAABndAAAZ5gAAGfwAABoDAAAaEAAAGk8AABpRAAAaUwAAGlUAABpXAAAaWAAAGlkAABpaAAAaWwAAGl0AABpfAAAaYAAAGmEAABpjAAAaZAAAGn0AABp/AAAagQAAGoMAABqEAAAahgAAGp0AABqmAAAatAAAGsEAABrPAAAa5AAAGvgAABsPAAAbIQAAG2AAABtiAAAbZAAAG2YAABtoAAAbaQAAG2oAABtrAAAbbAAAG24AABtwAAAbcQAAG3IAABt0AAAbdQAAG4oAABuTAAAbqAAAG7cAABvMAAAb2gAAG+8AABwDAAAcGgAAHCwAABw5AAAcPgAAHEAAABxCAAAcRwAAHEkAABxLAAAcTQAAHFIAABxdAAAcqAAAHMsAABzrAAAdCwAAHQ0AAB0PAAAdEQAAHRMAAB0VAAAdFgAAHRcAAB0ZAAAdGgAAHRwAAB0dAAAdHwAAHSEAAB0iAAAdIwAAHSUAAB0mAAAdKwAAHTgAAB09AAAdPwAAHUEAAB1GAAAdSAAAHUoAAB1MAAAdYQAAHXYAAB2bAAAdvwAAHeYAAB4KAAAeDAAAHg4AAB4QAAAeEgAAHhQAAB4WAAAeFwAAHhkAAB4mAAAeNwAAHjkAAB47AAAePQAAHj8AAB5BAAAeQwAAHkUAAB5HAAAeWAAAHloAAB5cAAAeXgAAHmAAAB5iAAAeZAAAHmYAAB5oAAAeagAAHogAAB6mAAAeuQAAHs0AAB7iAAAe/wAAHxMAAB8pAAAfaAAAH2oAAB9sAAAfbgAAH3AAAB9xAAAfcgAAH3MAAB90AAAfdgAAH3gAAB95AAAfegAAH3wAAB99AAAfvAAAH74AAB/AAAAfwgAAH8QAAB/FAAAfxgAAH8cAAB/IAAAfygAAH8wAAB/NAAAfzgAAH9AAAB/RAAAgEAAAIBIAACAUAAAgFgAAIBgAACAZAAAgGgAAIBsAACAcAAAgHgAAICAAACAhAAAgIgAAICQAACAlAAAgMgAAIDMAACA0AAAgNgAAIHUAACB3AAAgeQAAIHsAACB9AAAgfgAAIH8AACCAAAAggQAAIIMAACCFAAAghgAAIIcAACCJAAAgigAAIMkAACDLAAAgzQAAIM8AACDRAAAg0gAAINMAACDUAAAg1QAAINcAACDZAAAg2gAAINsAACDdAAAg3gAAIN8AACEeAAAhIAAAISIAACEkAAAhJgAAIScAACEoAAAhKQAAISoAACEsAAAhLgAAIS8AACEwAAAhMgAAITMAACFyAAAhdAAAIXYAACF4AAAhegAAIXsAACF8AAAhfQAAIX4AACGAAAAhggAAIYMAACGEAAAhhgAAIYcAACHGAAAhyAAAIcoAACHMAAAhzgAAIc8AACHQAAAh0QAAIdIAACHUAAAh1gAAIdcAACHYAAAh2gAAIdsAACIAAAAiJAAAIksAACJvAAAicQAAInMAACJ1AAAidwAAInkAACJ7AAAifAAAIn4AACKLAAAimgAAIpwAACKeAAAioAAAIqIAACKkAAAipgAAIqgAACK3AAAiuQAAIrsAACK9AAAivwAAIsEAACLDAAAixQAAIscAACLnAAAjEgAAIywAACNFAAAjXwAAI38AACOiAAAj4QAAI+MAACPlAAAj5wAAI+kAACPqAAAj6wAAI+wAACPtAAAj7wAAI/EAACPyAAAj8wAAI/UAACP2AAAkNQAAJDcAACQ5AAAkOwAAJD0AACQ+AAAkPwAAJEAAACRBAAAkQwAAJEUAACRGAAAkRwAAJEkAACRKAAAkiQAAJIsAACSNAAAkjwAAJJEAACSSAAAkkwAAJJQAACSVAAAklwAAJJkAACSaAAAkmwAAJJ0AACSeAAAk3QAAJN8AACThAAAk4wAAJOUAACTmAAAk5wAAJOgAACTpAAAk6wAAJO0AACTuAAAk7wAAJPEAACTyAAAk9QAAJTQAACU2AAAlOAAAJToAACU8AAAlPQAAJT4AACU/AAAlQAAAJUIAACVEAAAlRQAAJUYAACVIAAAlSQAAJYgAACWKAAAljAAAJY4AACWQAAAlkQAAJZIAACWTAAAllAAAJZYAACWYAAAlmQAAJZoAACWcAAAlnQAAJdwAACXeAAAl4AAAJeIAACXkAAAl5QAAJeYAACXnAAAl6AAAJeoAACXsAAAl7QAAJe4AACXwAAAl8QAAJfoAACYIAAAmFQAAJiMAACYwAAAmQwAAJloAACZsAAAmtwAAJtoAACb6AAAnGgAAJxwAACceAAAnIAAAJyIAACckAAAnJQAAJyYAACcoAAAnKQAAJysAACcsAAAnLgAAJzAAACcxAAAnMgAAJzQAACc1AAAnOgAAJ0cAACdMAAAnTgAAJ1AAACdVAAAnVwAAJ1kAACdbAAAngAAAJ6QAACfLAAAn7wAAJ/EAACfzAAAn9QAAJ/cAACf5AAAn+wAAJ/wAACf+AAAoCwAAKBwAACgeAAAoIAAAKCIAACgkAAAoJgAAKCgAACgqAAAoLAAAKD0AACg/AAAoQQAAKEMAAChFAAAoRwAAKEkAAChLAAAoTQAAKE8AACiOAAAokAAAKJIAACiUAAAolgAAKJcAACiYAAAomQAAKJoAACicAAAongAAKJ8AACigAAAoogAAKKMAACjiAAAo5AAAKOYAACjoAAAo6gAAKOsAACjsAAAo7QAAKO4AACjwAAAo8gAAKPMAACj0AAAo9gAAKPcAACk2AAApOAAAKToAACk8AAApPgAAKT8AAClAAAApQQAAKUIAAClEAAApRgAAKUcAAClIAAApSgAAKUsAAClYAAApWQAAKVoAAClcAAApmwAAKZ0AACmfAAApoQAAKaMAACmkAAAppQAAKaYAACmnAAApqQAAKasAACmsAAAprQAAKa8AACmwAAAp7wAAKfEAACnzAAAp9QAAKfcAACn4AAAp+QAAKfoAACn7AAAp/QAAKf8AACoAAAAqAQAAKgMAACoEAAAqQwAAKkUAACpHAAAqSQAAKksAACpMAAAqTQAAKk4AACpPAAAqUQAAKlMAACpUAAAqVQAAKlcAACpYAAAqlwAAKpkAACqbAAAqnQAAKp8AACqgAAAqoQAAKqIAACqjAAAqpQAAKqcAACqoAAAqqQAAKqsAACqsAAAq6wAAKu0AACrvAAAq8QAAKvMAACr0AAAq9QAAKvYAACr3AAAq+QAAKvsAACr8AAAq/QAAKv8AACsAAAArJQAAK0kAACtwAAArlAAAK5YAACuYAAArmgAAK5wAACueAAAroAAAK6EAACujAAArsAAAK78AACvBAAArwwAAK8UAACvHAAAryQAAK8sAACvNAAAr3AAAK94AACvgAAAr4gAAK+QAACvmAAAr6AAAK+oAACvsAAAsKwAALC0AACwvAAAsMQAALDMAACw0AAAsNQAALDYAACw3AAAsOQAALDsAACw8AAAsPQAALD8AACxAAAAsfwAALIEAACyDAAAshQAALIcAACyIAAAsiQAALIoAACyLAAAsjQAALI8AACyQAAAskQAALJMAACyUAAAs0wAALNUAACzXAAAs2QAALNsAACzcAAAs3QAALN4AACzfAAAs4QAALOMAACzkAAAs5QAALOcAACzoAAAtJwAALSkAAC0rAAAtLQAALS8AAC0wAAAtMQAALTIAAC0zAAAtNQAALTcAAC04AAAtOQAALTsAAC08AAAtPwAALX4AAC2AAAAtggAALYQAAC2GAAAthwAALYgAAC2JAAAtigAALYwAAC2OAAAtjwAALZAAAC2SAAAtkwAALdIAAC3UAAAt1gAALdgAAC3aAAAt2wAALdwAAC3dAAAt3gAALeAAAC3iAAAt4wAALeQAAC3mAAAt5wAALiYAAC4oAAAuKgAALiwAAC4uAAAuLwAALjAAAC4xAAAuMgAALjQAAC42AAAuNwAALjgAAC46AAAuOwAALkYAAC5PAAAuUAAALlIAAC5bAAAuZgAALnUAAC6AAAAujgAALqMAAC63AAAuzgAALuAAAC8jAAAvRwAAL2sAAC+OAAAvtQAAL9UAAC/8AAAwIwAAMEMAADBnAAAwiwAAMI0AADCPAAAwkQAAMJMAADCVAAAwlwAAMJkAADCbAAAwnQAAMJ8AADChAAAwowAAMKYAADCoAAAwqQAAMK4AADC7AAAwvgAAMMAAADDDAAAwxQAAMMcAADDsAAAxEAAAMTcAADFbAAAxXQAAMV8AADFhAAAxYwAAMWUAADFnAAAxaAAAMWoAADF3AAAxigAAMYwAADGOAAAxkAAAMZIAADGUAAAxlgAAMZgAADGaAAAxnAAAMa8AADGxAAAxswAAMbUAADG3AAAxuQAAMbsAADG9AAAxvwAAMcEAADHDAAAyAgAAMgQAADIGAAAyCAAAMgoAADILAAAyDAAAMg0AADIOAAAyEAAAMhIAADITAAAyFAAAMhYAADIXAAAyIAAAMiEAADIjAAAyYgAAMmQAADJmAAAyaAAAMmoAADJrAAAybAAAMm0AADJuAAAycAAAMnIAADJzAAAydAAAMnYAADJ3AAAytgAAMrgAADK6AAAyvAAAMr4AADK/AAAywAAAMsEAADLCAAAyxAAAMsYAADLHAAAyyAAAMsoAADLLAAAy1AAAMtUAADLXAAAzFgAAMxgAADMaAAAzHAAAMx4AADMfAAAzIAAAMyEAADMiAAAzJAAAMyYAADMnAAAzKAAAMyoAADMrAAAzagAAM2wAADNuAAAzcAAAM3IAADNzAAAzdAAAM3UAADN2AAAzeAAAM3oAADN7AAAzfAAAM34AADN/AAAziAAAM4kAADOLAAAzygAAM8wAADPOAAAz0AAAM9IAADPTAAAz1AAAM9UAADPWAAAz2AAAM9oAADPbAAAz3AAAM94AADPfAAA0HgAANCAAADQiAAA0JAAANCYAADQnAAA0KAAANCkAADQqAAA0LAAANC4AADQvAAA0MAAANDIAADQzAAA0QAAANEEAADRCAAA0RAAANIMAADSFAAA0hwAANIkAADSLAAA0jAAANI0AADSOAAA0jwAANJEAADSTAAA0lAAANJUAADSXAAA0mAAANNcAADTZAAA02wAANN0AADTfAAA04AAANOEAADTiAAA04wAANOUAADTnAAA06AAANOkAADTrAAA07AAANPkAADUGAAA1EQAANRMAADUVAAA1FwAANRkAADUbAAA1JgAANSgAADUrAAA1LgAANTEAADU0AAA1NgAANTwAADVCAAA1VgAANV4AADVmAAA1sQAANdQAADX0AAA2FAAANhYAADYYAAA2GgAANhwAADYeAAA2HwAANiAAADYiAAA2IwAANiUAADYmAAA2KQAANisAADYsAAA2LQAANi8AADYwAAA2NQAANkIAADZHAAA2SQAANksAADZQAAA2UgAANlQAADZWAAA2aQAANo4AADayAAA22QAANv0AADb/AAA3AQAANwMAADcFAAA3BwAANwkAADcKAAA3DAAANxkAADcqAAA3LAAANy4AADcwAAA3MgAANzQAADc2AAA3OAAANzoAADdLAAA3TQAAN08AADdRAAA3UwAAN1UAADdXAAA3WQAAN1sAADddAAA3nAAAN54AADegAAA3ogAAN6QAADelAAA3pgAAN6cAADeoAAA3qgAAN6wAADetAAA3rgAAN7AAADexAAA38AAAN/IAADf0AAA39gAAN/gAADf5AAA3+gAAN/sAADf8AAA3/gAAOAAAADgBAAA4AgAAOAQAADgFAAA4RAAAOEYAADhIAAA4SgAAOEwAADhNAAA4TgAAOE8AADhQAAA4UgAAOFQA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AESjAABEpAAARKYAAESoAABEqQAARKoAAESsAABErQAAROwAAETuAABE8AAARPIAAET0AABE9QAARPYAAET3AABE+AAARPoAAET8AABE/QAARP4AAEUAAABFAQAARQ4AAEUPAABFEAAARRIAAEVRAABFUwAARVUAAEVXAABFWQAARVoAAEVbAABFXAAARV0AAEVfAABFYQAARWIAAEVjAABFZQAARWYAAEWlAABFpwAARakAAEWrAABFrQAARa4AAEWvAABFsAAARbEAAEWzAABFtQAARbYAAEW3AABFuQAARboAAEX5AABF+wAARf0AAEX/AABGAQAARgIAAEYDAABGBAAARgUAAEYHAABGCQAARgoAAEYLAABGDQAARg4AAEZNAABGTwAARlEAAEZTAABGVQAARlYAAEZXAABGWAAARlkAAEZbAABGXQAARl4AAEZfAABGYQAARmIAAEahAABGowAARqUAAEanAABGqQAARqoAAEarAABGrAAARq0AAEavAABGsQAARrIAAEazAABGtQAARrYAAEbbAABG/wAARyYAAEdKAABHTAAAR04AAEdQAABHUgAAR1QAAEdWAABHVwAAR1kAAEdmAABHdQAAR3cAAEd5AABHewAAR30AAEd/AABHgQAAR4MAAEeSAABHlAAAR5YAAEeYAABHmgAAR5wAAEeeAABHoAAAR6IAAEfhAABH4wAAR+UAAEfnAABH6QAAR+oAAEfrAABH7AAAR+0AAEfvAABH8QAAR/IAAEfzAABH9QAAR/YAAEg1AABINwAASDkAAEg7AABIPQAASD4AAEg/AABIQAAASEEAAEhDAABIRQAASEYAAEhHAABISQAASEoAAEiJAABIiwAASI0AAEiPAABIkQAASJIAAEiTAABIlAAASJUAAEiXAABImQAASJoAAEibAABInQAASJ4AAEjdAABI3wAASOEAAEjjAABI5QAASOYAAEjnAABI6AAASOkAAEjrAABI7QAASO4AAEjvAABI8QAASPIAAEkxAABJMwAASTUAAEk3AABJOQAASToAAEk7AABJPAAAST0AAEk/AABJQQAASUIAAElDAABJRQAASUYAAEmFAABJhwAASYkAAEmLAABJjQAASY4AAEmPAABJkAAASZEAAEmTAABJlQAASZYAAEmXAABJmQAASZoAAEnZAABJ2wAASd0AAEnfAABJ4QAASeIAAEnjAABJ5AAASeUAAEnnAABJ6QAASeoAAEnrAABJ7QAASe4AAEo5AABKXAAASnwAAEqcAABKngAASqAAAEqiAABKpAAASqYAAEqnAABKqAAASqoAAEqrAABKrQAASq4AAEqwAABKsgAASrMAAEq0AABKtgAASrcAAEq8AABKyQAASs4AAErQAABK0gAAStcAAErZAABK2wAASt0AAEsCAABLJgAAS00AAEtxAABLcwAAS3UAAEt3AABLeQAAS3sAAEt9AABLfgAAS4AAAEuNAABLngAAS6AAAEuiAABLpAAAS6YAAEuoAABLqgAAS6wAAEuuAABLvwAAS8EAAEvDAABLxQAAS8cAAEvJAABLywAAS80AAEvPAABL0QAATBAAAEwSAABMFAAATBYAAEwYAABMGQAATBoAAEwbAABMHAAATB4AAEwgAABMIQAATCIAAEwkAABMJQAATGQAAExmAABMaAAATGoAAExsAABMbQAATG4AAExvAABMcAAATHIAAEx0AABMdQAATHYAAEx4AABMeQAATLgAAEy6AABMvAAATL4AAEzAAABMwQAATMIAAEzDAABMxAAATMYAAEzIAABMyQAATMoAAEzMAABMzQAATNoAAEzbAABM3AAATN4AAE0dAABNHwAATSEAAE0jAABNJQAATSYAAE0nAABNKAAATSkAAE0rAABNLQAATS4AAE0vAABNMQAATTIAAE1xAABNcwAATXUAAE13AABNeQAATXoAAE17AABNfAAATX0AAE1/AABNgQAATYIAAE2DAABNhQAATYYAAE3FAABNxwAATckAAE3LAABNzQAATc4AAE3PAABN0AAATdEAAE3TAABN1QAATdYAAE3XAABN2QAATdoAAE4ZAABOGwAATh0AAE4fAABOIQAATiIAAE4jAABOJAAATiUAAE4nAABOKQAATioAAE4rAABOLQAATi4AAE5tAABObwAATnEAAE5zAABOdQAATnYAAE53AABOeAAATnkAAE57AABOfQAATn4AAE5/AABOgQAAToIAAE6nAABOywAATvIAAE8WAABPGAAATxoAAE8cAABPHgAATyAAAE8iAABPIwAATyUAAE8yAABPQQAAT0MAAE9FAABPRwAAT0kAAE9LAABPTQAAT08AAE9eAABPYAAAT2IAAE9kAABPZgAAT2gAAE9qAABPbAAAT24AAE+tAABPrwAAT7EAAE+zAABPtQAAT7YAAE+3AABPuAAAT7kAAE+7AABPvQAAT74AAE+/AABPwQAAT8IAAFABAABQAwAAUAUAAFAHAABQCQAAUAoAAFALAABQDAAAUA0AAFAPAABQEQAAUBIAAFATAABQFQAAUBYAAFBVAABQVwAAUFkAAFBbAABQXQAAUF4AAFBfAABQYAAAUGEAAFBjAABQZQAAUGYAAFBnAABQaQAAUGoAAFCpAABQqwAAUK0AAFCvAABQsQAAULIAAFCzAABQtAAAULUAAFC3AABQuQAAULoAAFC7AABQvQAAUL4AAFDBAABRAAAAUQIAAFEEAABRBgAAUQgAAFEJAABRCgAAUQsAAFEMAABRDgAAURAAAFERAABREgAAURQAAFEVAABRVAAAUVYAAFFYAABRWgAAUVwAAFFdAABRXgAAUV8AAFFgAABRYgAAUWQAAFFlAABRZgAAUWgAAFFpAABRqAAAUaoAAFGsAABRrgAAUbAAAFGxAABRsgAAUbMAAFG0AABRtgAAUbgAAFG5AABRugAAUbwAAFG9AABSCAAAUisAAFJLAABSawAAUm0AAFJvAABScQAAUnMAAFJ1AABSdgAAUncAAFJ5AABSegAAUnwAAFJ9AABSfwAAUoEAAFKCAABSgwAAUoUAAFKGAABSiwAAUpgAAFKdAABSnwAAUqEAAFKmAABSqAAAUqsAAFKtAABS0gAAUvYAAFMdAABTQQAAU0MAAFNFAABTRwAAU0kAAFNLAABTTQAAU04AAFNQAABTXQAAU24AAFNwAABTcgAAU3QAAFN2AABTeAAAU3oAAFN8AABTfgAAU48AAFORAABTkwAAU5YAAFOZAABTnAAAU58AAFOiAABTpQAAU6cAAFPmAABT6AAAU+oAAFPsAABT7gAAU+8AAFPwAABT8QAAU/IAAFP0AABT9gAAU/cAAFP4AABT+gAAU/sAAFQ6AABUPAAAVD4AAFRAAABUQgAAVEMAAFREAABURQAAVEYAAFRIAABUSgAAVEsAAFRMAABUTgAAVE8AAFSOAABUkAAAVJMAAFSVAABUlwAAVJgAAFSZAABUmgAAVJsAAFSdAABUnwAAVKAAAFShAABUowAAVKQAAFSxAABUsgAAVLMAAFS1AABU9AAAVPYAAFT4AABU+gAAVPwAAFT9AABU/gAAVP8AAFUAAABVAgAAVQQAAFUFAABVBgAAVQgAAFUJAABVSAAAVUoAAFVMAABVTgAAVVAAAFVRAABVUgAAVVMAAFVUAABVVgAAVVgAAFVZAABVWgAAVVwAAFVdAABVnAAAVZ4AAFWgAABVogAAVaQAAFWlAABVpgAAVacAAFWoAABVqgAAVawAAFWtAABVrgAAVbAAAFWxAABV8AAAVfIAAFX0AABV9gAAVfgAAFX5AABV+gAAVfsAAFX8AABV/gAAVgAAAFYBAABWAgAAVgQAAFYFAABWRAAAVkYAAFZIAABWSgAAVkwAAFZNAABWTgAAVk8AAFZQAABWUgAAVlQAAFZVAABWVgAAVlgAAFZZAABWfgAAVqIAAFbJAABW7QAAVu8AAFbxAABW8wAAVvUAAFb3AABW+QAAVvoAAFb9AABXCgAAVxkAAFcbAABXHQAAVx8AAFchAABXIwAAVyUAAFcnAABXNgAAVzkAAFc8AABXPwAAV0IAAFdFAABXSAAAV0sAAFdNAABXjAAAV44AAFeQAABXkgAAV5UAAFeWAABXlwAAV5gAAFeZAABXmwAAV50AAFeeAABXnwAAV6EAAFeiAABX4QAAV+MAAFflAABX5wAAV+oAAFfrAABX7AAAV+0AAFfuAABX8AAAV/IAAFfzAABX9AAAV/YAAFf3AABYNgAAWDgAAFg6AABYPAAAWD8AAFhAAABYQQAAWEIAAFhDAABYRQAAWEcAAFhIAABYSQAAWEsAAFhMAABYiwAAWI0AAFiPAABYkQAAWJQAAFiVAABYlgAAWJcAAFiYAABYmgAAWJwAAFidAABYngAAWKAAAFihAABY4AAAWOIAAFjkAABY5gAAWOkAAFjqAABY6wAAWOwAAFjtAABY7wAAWPEAAFjyAABY8wAAWPUAAFj2AABZNQAAWTcAAFk5AABZOwAAWT4AAFk/AABZQAAAWUEAAFlCAABZRAAAWUYAAFlHAABZSAAAWUoAAFlLAABZigAAWYwAAFmOAABZkAAAWZMAAFmUAABZlQAAWZYAAFmXAABZmQAAWZsAAFmcAABZnQAAWZ8AAFmgAABZ6wAAWg4AAFouAABaTgAAWlAAAFpSAABaVAAAWlYAAFpYAABaWQAAWloAAFpdAABaXgAAWmAAAFphAABaYwAAWmUAAFpmAABaZwAAWmoAAFprAABadAAAWoEAAFqGAABaiAAAWooAAFqPAABakgAAWpUAAFqXAABavAAAWuAAAFsHAABbKwAAWy4AAFswAABbMgAAWzQAAFs2AABbOAAAWzkAAFs8AABbSQAAW1oAAFtcAABbXgAAW2AAAFtiAABbZAAAW2YAAFtoAABbagAAW3sAAFt+AABbgQAAW4QAAFuHAABbigAAW40AAFuQAABbkwAAW5UAAFvUAABb1gAAW9gAAFvaAABb3QAAW94AAFvfAABb4AAAW+EAAFvjAABb5QAAW+YAAFvnAABb6QAAW+oAAFwpAABcKwAAXC0AAFwvAABcMgAAXDMAAFw0AABcNQAAXDYAAFw4AABcOgAAXDsAAFw8AABcPgAAXD8AAFx+AABcgAAAXIMAAFyFAABciAAAXIkAAFyKAABciwAAXIwAAFyOAABckAAAXJEAAFySAABclAAAXJUAAFyiAABcowAAXKQAAFymAABc5QAAXOcAAFzpAABc6wAAXO4AAFzvAABc8AAAXPEAAFzyAABc9AAAXPYAAFz3AABc+AAAXPoAAFz7AABdOgAAXTwAAF0+AABdQAAAXUMAAF1EAABdRQAAXUYAAF1HAABdSQAAXUsAAF1MAABdTQAAXU8AAF1QAABdjwAAXZEAAF2TAABdlQAAXZgAAF2ZAABdmgAAXZsAAF2cAABdngAAXaAAAF2hAABdogAAXaQAAF2lAABd5AAAXeYAAF3oAABd6gAAXe0AAF3uAABd7wAAXfAAAF3xAABd8wAAXfUAAF32AABd9wAAXfkAAF36AABeOQAAXjsAAF49AABePwAAXkIAAF5DAABeRAAAXkUAAF5GAABeSAAAXkoAAF5LAABeTAAAXk4AAF5PAABedAAAXpgAAF6/AABe4wAAXuYAAF7oAABe6gAAXuwAAF7uAABe8AAAXvEAAF70AABfAQAAXxAAAF8SAABfFAAAXxYAAF8YAABfGgAAXxwAAF8eAABfLQAAXzAAAF8zAABfNgAAXzkAAF88AABfPwAAX0IAAF9EAABfgwAAX4UAAF+IAABfigAAX40AAF+OAABfjwAAX5AAAF+RAABfkwAAX5UAAF+WAABflwAAX5kAAF+aAABfnAAAX9sAAF/dAABf3wAAX+EAAF/kAABf5QAAX+YAAF/nAABf6AAAX+oAAF/sAABf7QAAX+4AAF/wAABf8QAAYDAAAGAyAABgNAAAYDYAAGA5AABgOgAAYDsAAGA8AABgPQAAYD8AAGBBAABgQgAAYEMAAGBFAABgRgAAYIUAAGCHAABgigAAYIwAAGCPAABgkAAAYJEAAGCSAABgkwAAYJUAAGCXAABgmAAAYJkAAGCbAABgnAAAYJ4AAGDdAABg3wAAYOEAAGDjAABg5gAAYOcAAGDoAABg6QAAYOoAAGDsAABg7gAAYO8AAGDwAABg8gAAYPMAAGEyAABhNAAAYTYAAGE4AABhOwAAYTwAAGE9AABhPgAAYT8AAGFBAABhQwAAYUQAAGFFAABhRwAAYUgAAGGHAABhiQAAYYsAAGGNAABhkAAAYZEAAGGSAABhkwAAYZQAAGGWAABhmAAAYZkAAGGaAABhnAAAYZ0AAGHoAABiCwAAYisAAGJLAABiTQAAYk8AAGJRAABiUwAAYlUAAGJWAABiVwAAYloAAGJbAABiXQAAYl4AAGJgAABiYgAAYmMAAGJkAABiZwAAYmgAAGJxAABifgAAYoMAAGKFAABihwAAYowAAGKPAABikgAAYpQAAGK5AABi3QAAYwQAAGMoAABjKwAAYy0AAGMvAABjMQAAYzMAAGM1AABjNgAAYzkAAGNGAABjVwAAY1kAAGNbAABjXQAAY18AAGNhAABjYwAAY2UAAGNnAABjeAAAY3sAAGN+AABjgQAAY4QAAGOHAABjigAAY40AAGOQAABjkgAAY9EAAGPTAABj1QAAY9cAAGPaAABj2wAAY9wAAGPdAABj3gAAY+AAAGPiAABj4wAAY+QAAGPmAABj5wAAZCYAAGQoAABkKgAAZCwAAGQvAABkMAAAZDEAAGQyAABkMwAAZDUAAGQ3AABkOAAAZDkAAGQ7AABkPAAAZHsAAGR9AABkgAAAZIIAAGSFAABkhgAAZIcAAGSIAABkiQAAZIsAAGSNAABkjgAAZI8AAGSRAABkkgAAZJ8AAGSgAABkoQAAZKMAAGTiAABk5AAAZOYAAGToAABk6wAAZOwAAGTtAABk7gAAZO8AAGTxAABk8wAAZPQAAGT1AABk9wAAZPgAAGU3AABlOQAAZTsAAGU9AABlQAAAZUEAAGVCAABlQwAAZUQAAGVGAABlSAAAZUkAAGVKAABlTAAAZU0AAGWMAABljgAAZZAAAGWSAABllQAAZZYAAGWXAABlmAAAZZkAAGWbAABlnQAAZZ4AAGWfAABloQAAZaIAAGXhAABl4wAAZeUAAGXnAABl6gAAZesAAGXsAABl7QAAZe4AAGXwAABl8gAAZfMAAGX0AABl9gAAZfcAAGY2AABmOAAAZjoAAGY8AABmPwAAZkAAAGZBAABmQgAAZkMAAGZFAABmRwAAZkgAAGZJAABmSwAAZkwAAGZxAABmlQAAZrwAAGbgAABm4wAAZuUAAGbnAABm6QAAZusAAGbtAABm7gAAZvEAAGb+AABnDQAAZw8AAGcRAABnEwAAZxUAAGcXAABnGQAAZxsAAGcqAABnLQAAZzAAAGczAABnNgAAZzkAAGc8AABnPwAAZ0EAAGeAAABnggAAZ4QAAGeGAABniQAAZ4oAAGeLAABnjAAAZ40AAGePAABnkQAAZ5IAAGeTAABnlQAAZ5YAAGfVAABn1wAAZ9kAAGfbAABn3gAAZ98AAGfgAABn4QAAZ+IAAGfkAABn5gAAZ+cAAGfoAABn6gAAZ+sAAGgqAABoLAAAaC4AAGgwAABoMwAAaDQAAGg1AABoNgAAaDcAAGg5AABoOwAAaDwAAGg9AABoPwAAaEAAAGh/AABogQAAaIMAAGiFAABoiAAAaIkAAGiKAABoiwAAaIwAAGiOAABokAAAaJEAAGiSAABolAAAaJUAAGjUAABo1gAAaNgAAGjaAABo3QAAaN4AAGjfAABo4AAAaOEAAGjjAABo5QAAaOYAAGjnAABo6QAAaOoAAGkpAABpKwAAaS0AAGkvAABpMgAAaTMAAGk0AABpNQAAaTYAAGk4AABpOgAAaTsAAGk8AABpPgAAaT8AAGl+AABpgAAAaYIAAGmEAABphwAAaYgAAGmJAABpigAAaYsAAGmNAABpjwAAaZAAAGmRAABpkwAAaZQAAGmdAABpngAAaaAAAGnfAABp4QAAaeMAAGnlAABp5wAAaegAAGnpAABp6gAAaesAAGntAABp7wAAafAAAGnxAABp8wAAafQAAGozAABqNQAAajgAAGo6AABqPAAAaj0AAGo+AABqPwAAakAAAGpCAABqRAAAakUAAGpGAABqSAAAakkAAGqIAABqigAAao0AAGqPAABqkQAAapIAAGqTAABqlAAAapUAAGqXAABqmQAAapoAAGqbAABqnQAAap4AAGqgAABq3wAAauEAAGrkAABq5gAAaugAAGrpAABq6gAAausAAGrsAABq7gAAavAAAGrxAABq8gAAavQAAGr1AABrNAAAazYAAGs4AABrOgAAazwAAGs9AABrPgAAaz8AAGtAAABrQgAAa0QAAGtFAABrRgAAa0gAAGtJAABrUgAAa2UAAGtyAABrhQAAa5IAAGulAABrvAAAa84AAGwZAABsPAAAbFwAAGx8AABsfgAAbIAAAGyCAABshAAAbIYAAGyHAABsiAAAbIsAAGyMAABsjgAAbI8AAGySAABslAAAbJUAAGyWAABsmQAAbJoAAGyfAABsrAAAbLEAAGyzAABstQAAbLoAAGy9AABswAAAbMIAAGznAABtCwAAbTIAAG1WAABtWQAAbVsAAG1dAABtXwAAbWEAAG1jAABtZAAAbWcAAG10AABthQAAbYcAAG2JAABtiwAAbY0AAG2PAABtkQAAbZMAAG2VAABtpgAAbakAAG2sAABtrwAAbbIAAG21AABtuAAAbbsAAG2+AABtwAAAbf8AAG4BAABuAwAAbgUAAG4IAABuCQAAbgoAAG4LAABuDAAAbg4AAG4QAABuEQAAbhIAAG4UAABuFQAAblQAAG5WAABuWAAAbloAAG5dAABuXgAAbl8AAG5gAABuYQAAbmMAAG5lAABuZgAAbmcAAG5pAABuagAAbqkAAG6rAABurgAAbrAAAG6zAAButAAAbrUAAG62AAButwAAbrkAAG67AABuvAAAbr0AAG6/AABuwAAAbs0AAG7OAABuzwAAbtEAAG8QAABvEgAAbxQAAG8WAABvGQAAbxoAAG8bAABvHAAAbx0AAG8fAABvIQAAbyIAAG8jAABvJQAAbyYAAG9lAABvZwAAb2kAAG9rAABvbgAAb28AAG9wAABvcQAAb3IAAG90AABvdgAAb3cAAG94AABvegAAb3sAAG+6AABvvAAAb74AAG/AAABvwwAAb8QAAG/FAABvxgAAb8cAAG/JAABvywAAb8wAAG/NAABvzwAAb9AAAHAPAABwEQAAcBMAAHAVAABwGAAAcBkAAHAaAABwGwAAcBwAAHAeAABwIAAAcCEAAHAiAABwJAAAcCUAAHBkAABwZgAAcGgAAHBqAABwbQAAcG4AAHBvAABwcAAAcHEAAHBzAABwdQAAcHYAAHB3AABweQAAcHoAAHCfAABwwwAAcOoAAHEOAABxEQAAcRMAAHEVAABxFwAAcRkAAHEbAABxHAAAcR8AAHEsAABxOwAAcT0AAHE/AABxQQAAcUMAAHFFAABxRwAAcUoAAHFZAABxXAAAcV8AAHFiAABxZQAAcWgAAHFrAABxbgAAcXAAAHGNAABxzAAAcc4AAHHRAABx0wAAcdYAAHHXAABx2AAAcdkAAHHaAABx3AAAcd4AAHHfAABx4AAAceIAAHHjAAByIgAAciQAAHInAAByKQAAciwAAHItAAByLgAAci8AAHIwAAByMgAAcjQAAHI1AAByNgAAcjgAAHI5AAByeAAAcnoAAHJ9AAByfwAAcoIAAHKDAAByhAAAcoUAAHKGAAByiAAAcooAAHKLAAByjAAAco4AAHKPAAByzgAActAAAHLTAABy1QAActgAAHLZAABy2gAActsAAHLcAABy3gAAcuAAAHLhAABy4gAAcuQAAHLlAABzKAAAc0wAAHNwAABzkwAAc7oAAHPaAAB0AQAAdCgAAHRIAAB0bAAAdJAAAHSSAAB0lQAAdJcAAHSZAAB0mwAAdJ4AAHSgAAB0ogAAdKQAAHSmAAB0qAAAdKoAAHStAAB0sAAAdLEAAHS6AAB0xwAAdMoAAHTMAAB0zwAAdNIAAHTUAAB0+QAAdR0AAHVEAAB1aAAAdWsAAHVtAAB1bwAAdXEAAHVzAAB1dQAAdXYAAHV5AAB1hgAAdZkAAHWbAAB1nQAAdZ8AAHWhAAB1owAAdaUAAHWnAAB1qQAAdasAAHW+AAB1wQAAdcQAAHXHAAB1ygAAdc0AAHXQAAB10wAAddYAAHXZAAB12wAAdhoAAHYcAAB2HwAAdiEAAHYkAAB2JQAAdiYAAHYnAAB2KAAAdioAAHYsAAB2LQAAdi4AAHYwAAB2MQAAdjoAAHY7AAB2PQAAdnwAAHZ+AAB2gAAAdoIAAHaFAAB2hgAAdocAAHaIAAB2iQAAdosAAHaNAAB2jgAAdo8AAHaRAAB2kgAAdtEAAHbTAAB21gAAdtgAAHbbAAB23AAAdt0AAHbeAAB23wAAduEAAHbjAAB25AAAduUAAHbnAAB26AAAdvEAAHb0AAB29wAAdvkAAHcCAAB3BQAAdwcAAHcJAAB3SAAAd0oAAHdMAAB3TgAAd1EAAHdSAAB3UwAAd1QAAHdVAAB3VwAAd1kAAHdaAAB3WwAAd10AAHdeAAB3nQAAd58AAHeiAAB3pAAAd6cAAHeoAAB3qQAAd6oAAHerAAB3rQAAd68AAHewAAB3sQAAd7MAAHe0AAB3vQAAd74AAHfAAAB3/wAAeAEAAHgDAAB4BQAAeAgAAHgJAAB4CgAAeAsAAHgMAAB4DgAAeBAAAHgRAAB4EgAAeBQAAHgVAAB4VAAAeFYAAHhZAAB4WwAAeF4AAHhfAAB4YAAAeGEAAHhiAAB4ZAAAeGYAAHhnAAB4aAAAeGoAAHhrAAB4eAAAeHkAAHh6AAB4fAAAeLsAAHi9AAB4vwAAeMEAAHjEAAB4xQAAeMYAAHjHAAB4yAAAeMoAAHjMAAB4zQAAeM4AAHjQAAB40QAAeRAAAHkSAAB5FAAAeRYAAHkZAAB5GgAAeRsAAHkcAAB5HQAAeR8AAHkhAAB5IgAAeSMAAHklAAB5JgAAeTMAAHleAAB5YQAAeWMAAHlmAAB5aQAAeWsAAHluAAB5cQAAeXQAAHl3AAB5egAAeX0AAHmAAAB5gwAAeYYAAHmJAAB5jAAAeY8AAHmSAAB5lQAAeZgAAHnDAAB5xgAAeckAAHnMAAB5zwAAedIAAHnVAAB52AAAedsAAHneAAB54QAAeeQAAHnnAAB56gAAee0AAHnwAAB58wAAefYAAHn5AAB5/AAAef8AAHoBAAB6DwAAeh0AAHorAAB6OgAAekEAAHpGAAB6XAAAemIAAHpvAAB6dQAAen8AAHqOAAB6lwAAeqsAAHq+AAB60AAAet4AAHrqAAB7NQAAe1gAAHt4AAB7mAAAe5oAAHucAAB7ngAAe6AAAHujAAB7pAAAe6UAAHuoAAB7qQAAe6sAAHusAAB7rgAAe7EAAHuyAAB7swAAe7YAAHu3AAB7vAAAe8kAAHvOAAB70AAAe9IAAHvXAAB72gAAe90AAHvfAAB8BAAAfCgAAHxPAAB8cwAAfHYAAHx4AAB8egAAfHwAAHx+AAB8gAAAfIEAAHyEAAB8kQAAfKIAAHykAAB8pgAAfKgAAHyqAAB8rAAAfK4AAHywAAB8sgAAfMMAAHzGAAB8yQAAfMwAAHzPAAB80gAAfNUAAHzYAAB82wAAfN0AAH0cAAB9HgAAfSAAAH0iAAB9JQAAfSYAAH0nAAB9KAAAfSkAAH0rAAB9LQAAfS4AAH0vAAB9MQAAfTIAAH1xAAB9cwAAfXUAAH13AAB9egAAfXsAAH18AAB9fQAAfX4AAH2AAAB9ggAAfYMAAH2EAAB9hgAAfYcAAH3GAAB9yAAAfcsAAH3NAAB90AAAfdEAAH3SAAB90wAAfdQAAH3WAAB92AAAfdkAAH3aAAB93AAAfd0AAH3qAAB96wAAfewAAH3uAAB+LQAAfi8AAH4xAAB+MwAAfjYAAH43AAB+OAAAfjkAAH46AAB+PAAAfj4AAH4/AAB+QAAAfkIAAH5DAAB+ggAAfoQAAH6GAAB+iAAAfosAAH6MAAB+jQAAfo4AAH6PAAB+kQAAfpMAAH6UAAB+lQAAfpcAAH6YAAB+1wAAftkAAH7bAAB+3QAAfuAAAH7hAAB+4gAAfuMAAH7kAAB+5gAAfugAAH7pAAB+6gAAfuwAAH7tAAB/LAAAfy4AAH8wAAB/MgAAfzUAAH82AAB/NwAAfzgAAH85AAB/OwAAfz0AAH8+AAB/PwAAf0EAAH9CAAB/gQAAf4MAAH+FAAB/hwAAf4oAAH+LAAB/jAAAf40AAH+OAAB/kAAAf5IAAH+TAAB/lAAAf5YAAH+XAAB/vAAAf+AAAIAHAACAKwAAgC4AAIAwAACAMgAAgDQAAIA2AACAOAAAgDkAAIA8AACASQAAgFgAAIBaAACAXAAAgF4AAIBgAACAYgAAgGQAAIBmAACAdQAAgHgAAIB7AACAfgAAgIEAAICEAACAhwAAgIoAAICMAACAywAAgM0AAIDQAACA0gAAgNUAAIDWAACA1wAAgNgAAIDZAACA2wAAgN0AAIDeAACA3wAAgOEAAIDiAACBIQAAgSMAAIElAACBJwAAgSoAAIErAACBLAAAgS0AAIEuAACBMAAAgTIAAIEzAACBNAAAgTYAAIE3AACBdgAAgXgAAIF6AACBfAAAgX8AAIGAAACBgQAAgYIAAIGDAACBhQAAgYcAAIGIAACBiQAAgYsAAIGMAACBywAAgc0AAIHQAACB0gAAgdUAAIHWAACB1wAAgdgAAIHZAACB2wAAgd0AAIHeAACB3wAAgeEAAIHiAACCIQAAgiMAAIIlAACCJwAAgioAAIIrAACCLAAAgi0AAIIuAACCMAAAgjIAAIIzAACCNAAAgjYAAII3AACCdgAAgngAAIJ6AACCfAAAgn8AAIKAAACCgQAAgoIAAIKDAACChQAAgocAAIKIAACCiQAAgosAAIKMAACCywAAgs0AAILPAACC0QAAgtQAAILVAACC1gAAgtcAAILYAACC2gAAgtwAAILdAACC3gAAguAAAILhAACDLAAAg08AAINvAACDjwAAg5EAAIOTAACDlQAAg5cAAIOaAACDmwAAg5wAAIOfAACDoAAAg6IAAIOjAACDpQAAg6cAAIOoAACDqQAAg6wAAIOtAACDsgAAg78AAIPEAACDxgAAg8gAAIPNAACD0AAAg9MAAIPVAACD+gAAhB4AAIRFAACEaQAAhGwAAIRuAACEcAAAhHIAAIR0AACEdgAAhHcAAIR6AACEhwAAhJgAAISaAACEnAAAhJ4AAISgAACEogAAhKQAAISmAACEqAAAhLkAAIS8AACEvwAAhMIAAITFAACEyAAAhMsAAITOAACE0QAAhNMAAIUSAACFFAAAhRYAAIUYAACFGwAAhRwAAIUdAACFHgAAhR8AAIUhAACFIwAAhSQAAIUlAACFJwAAhSgAAIVnAACFaQAAhWsAAIVtAACFcAAAhXEAAIVyAACFcwAAhXQAAIV2AACFeAAAhXkAAIV6AACFfAAAhX0AAIW8AACFvgAAhcEAAIXDAACFxgAAhccAAIXIAACFyQAAhcoAAIXMAACFzgAAhc8AAIXQAACF0gAAhdMAAIXgAACF4QAAheIAAIXkAACGIwAAhiUAAIYnAACGKQAAhiwAAIYtAACGLgAAhi8AAIYwAACGMgAAhjQAAIY1AACGNgAAhjgAAIY5AACGeAAAhnoAAIZ8AACGfgAAhoEAAIaCAACGgwAAhoQAAIaFAACGhwAAhokAAIaKAACGiwAAho0AAIaOAACGzQAAhs8AAIbRAACG0wAAhtYAAIbXAACG2AAAhtkAAIbaAACG3AAAht4AAIbfAACG4AAAhuIAAIbjAACHIgAAhyQAAIcmAACHKAAAhysAAIcsAACHLQAAhy4AAIcvAACHMQAAhzMAAIc0AACHNQAAhzcAAIc4AACHdwAAh3kAAId7AACHfQAAh4AAAIeBAACHggAAh4MAAIeEAACHhgAAh4gAAIeJAACHigAAh4wAAIeNAACHsgAAh9YAAIf9AACIIQAAiCQAAIgmAACIKAAAiCoAAIgsAACILgAAiC8AAIgyAACIPwAAiE4AAIhQAACIUgAAiFQAAIhWAACIWAAAiFoAAIhcAACIawAAiG4AAIhxAACIdAAAiHcAAIh6AACIfQAAiIAAAIiCAACIwQAAiMMAAIjFAACIxwAAiMoAAIjLAACIzAAAiM0AAIjOAACI0AAAiNIAAIjTAACI1AAAiNYAAIjXAACJFgAAiRgAAIkaAACJHAAAiR8AAIkgAACJIQAAiSIAAIkjAACJJQAAiScAAIkoAACJKQAAiSsAAIksAACJawAAiW0AAIlvAACJcQAAiXQAAIl1AACJdgAAiXcAAIl4AACJegAAiXwAAIl9AACJfgAAiYAAAImBAACJwAAAicIAAInEAACJxgAAickAAInKAACJywAAicwAAInNAACJzwAAidEAAInSAACJ0wAAidUAAInWAACKFQAAihcAAIoZAACKGwAAih4AAIofAACKIAAAiiEAAIoiAACKJAAAiiYAAIonAACKKAAAiioAAIorAACKagAAimwAAIpuAACKcAAAinMAAIp0AACKdQAAinYAAIp3AACKeQAAinsAAIp8AACKfQAAin8AAIqAAACKvwAAisEAAIrDAACKxQAAisgAAIrJAACKygAAissAAIrMAACKzgAAitAAAIrRAACK0gAAitQAAIrVAACLIAAAi0MAAItjAACLgwAAi4UAAIuHAACLiQAAi4sAAIuOAACLjwAAi5AAAIuTAACLlAAAi5YAAIuXAACLmQAAi5wAAIudAACLngAAi6EAAIuiAACLpwAAi7QAAIu5AACLuwAAi70AAIvCAACLxQAAi8gAAIvKAACL7wAAjBMAAIw6AACMXgAAjGEAAIxjAACMZQAAjGcAAIxpAACMawAAjGwAAIxvAACMfAAAjI0AAIyPAACMkQAAjJMAAIyVAACMlwAAjJkAAIybAACMnQAAjK4AAIyxAACMtAAAjLcAAIy6AACMvQAAjMAAAIzDAACMxgAAjMgAAI0HAACNCQAAjQsAAI0NAACNEAAAjREAAI0SAACNEwAAjRQAAI0WAACNGAAAjRkAAI0aAACNHAAAjR0AAI1cAACNXgAAjWAAAI1iAACNZQAAjWYAAI1nAACNaAAAjWkAAI1rAACNbQAAjW4AAI1vAACNcQAAjXIAAI2xAACNswAAjbYAAI24AACNuwAAjbwAAI29AACNvgAAjb8AAI3BAACNwwAAjcQAAI3FAACNxwAAjcgAAI3VAACN1gAAjdcAAI3ZAACOGAAAjhoAAI4cAACOHgAAjiEAAI4iAACOIwAAjiQAAI4lAACOJwAAjikAAI4qAACOKwAAji0AAI4uAACObQAAjm8AAI5xAACOcwAAjnYAAI53AACOeAAAjnkAAI56AACOfAAAjn4AAI5/AACOgAAAjoIAAI6DAACOwgAAjsQAAI7GAACOyAAAjssAAI7MAACOzQAAjs4AAI7PAACO0QAAjtMAAI7UAACO1QAAjtcAAI7YAACPFwAAjxkAAI8bAACPHQAAjyAAAI8hAACPIgAAjyMAAI8kAACPJgAAjygAAI8pAACPKgAAjywAAI8tAACPbAAAj24AAI9wAACPcgAAj3UAAI92AACPdwAAj3gAAI95AACPewAAj30AAI9+AACPfwAAj4EAAI+CAACPpwAAj8sAAI/yAACQFgAAkBkAAJAbAACQHQAAkB8AAJAhAACQIwAAkCQAAJAnAACQNAAAkEMAAJBFAACQRwAAkEkAAJBLAACQTQAAkE8AAJBRAACQYAAAkGMAAJBmAACQaQAAkGwAAJBvAACQcgAAkHUAAJB3AACQtgAAkLgAAJC6AACQvAAAkL8AAJDAAACQwQAAkMIAAJDDAACQxQAAkMcAAJDIAACQyQAAkMsAAJDMAACRCwAAkQ0AAJEPAACREQAAkRQAAJEVAACRFgAAkRcAAJEYAACRGgAAkRwAAJEdAACRHgAAkSAAAJEhAACRYAAAkWIAAJFkAACRZgAAkWkAAJFqAACRawAAkWwAAJFtAACRbwAAkXEAAJFyAACRcwAAkXUAAJF2AACRtQAAkbcAAJG5AACRuwAAkb4AAJG/AACRwAAAkcEAAJHCAACRxAAAkcYAAJHHAACRyAAAkcoAAJHLAACSCgAAkgwAAJIOAACSEAAAkhMAAJIUAACSFQAAkhYAAJIXAACSGQAAkhsAAJIcAACSHQAAkh8AAJIgAACSXwAAkmEAAJJjAACSZQAAkmgAAJJpAACSagAAkmsAAJJsAACSbgAAknAAAJJxAACScgAAknQAAJJ1AACStAAAkrYAAJK4AACSugAAkr0AAJK+AACSvwAAksAAAJLBAACSwwAAksUAAJLGAACSxwAAkskAAJLKAACTFQAAkzgAAJNYAACTeAAAk3oAAJN8AACTfgAAk4AAAJODAACThAAAk4UAAJOIAACTiQAAk4sAAJOMAACTjgAAk5EAAJOSAACTkwAAk5YAAJOXAACTnAAAk6kAAJOuAACTsAAAk7IAAJO3AACTugAAk70AAJO/AACT5AAAlAgAAJQvAACUUwAAlFYAAJRYAACUWgAAlFwAAJReAACUYAAAlGEAAJRkAACUcQAAlIIAAJSEAACUhgAAlIgAAJSKAACUjAAAlI4AAJSQAACUkgAAlKMAAJSmAACUqQAAlKwAAJSvAACUsgAAlLUAAJS4AACUuwAAlL0AAJT8AACU/gAAlQAAAJUCAACVBQAAlQYAAJUHAACVCAAAlQkAAJULAACVDQAAlQ4AAJUPAACVEQAAlRIAAJVRAACVUwAAlVUAAJVXAACVWgAAlVsAAJVcAACVXQAAlV4AAJVgAACVYgAAlWMAAJVkAACVZgAAlWcAAJWmAACVqAAAlasAAJWtAACVsAAAlbEAAJWyAACVswAAlbQAAJW2AACVuAAAlbkAAJW6AACVvAAAlb0AAJXKAACVywAAlcwAAJXOAACWDQAAlg8AAJYRAACWEwAAlhYAAJYXAACWGAAAlhkAAJYaAACWHAAAlh4AAJYfAACWIAAAliIAAJYjAACWYgAAlmQAAJZmAACWaAAAlmsAAJZsAACWbQAAlm4AAJZvAACWcQAAlnMAAJZ0AACWdQAAlncAAJZ4AACWtwAAlrkAAJa7AACWvQAAlsAAAJbBAACWwgAAlsMAAJbEAACWxgAAlsgAAJbJAACWygAAlswAAJbNAACXDAAAlw4AAJcQAACXEgAAlxUAAJcWAACXFwAAlxgAAJcZAACXGwAAlx0AAJceAACXHwAAlyEAAJciAACXYQAAl2MAAJdlAACXZwAAl2oAAJdrAACXbAAAl20AAJduAACXcAAAl3IAAJdzAACXdAAAl3YAAJd3AACXnAAAl8AAAJfnAACYCwAAmA4AAJgQAACYEgAAmBQAAJgWAACYGAAAmBkAAJgcAACYKQAAmDgAAJg6AACYPAAAmD4AAJhAAACYQgAAmEQAAJhGAACYVQAAmFgAAJhbAACYXgAAmGEAAJhkAACYZwAAmGoAAJhsAACYqwAAmK0AAJiwAACYsgAAmLUAAJi2AACYtwAAmLgAAJi5AACYuwAAmL0AAJi+AACYvwAAmMEAAJjCAACYxQAAmQQAAJkGAACZCAAAmQoAAJkNAACZDgAAmQ8AAJkQAACZEQAAmRMAAJkVAACZFgAAmRcAAJkZAACZGgAAmVkAAJlbAACZXQAAmV8AAJliAACZYwAAmWQAAJllAACZZgAAmWgAAJlqAACZawAAmWwAAJluAACZbwAAma4AAJmwAACZswAAmbUAAJm4AACZuQAAmboAAJm7AACZvAAAmb4AAJnAAACZwQAAmcIAAJnEAACZxQAAmcgAAJoHAACaCQAAmgsAAJoNAACaEAAAmhEAAJoSAACaEwAAmhQAAJoWAACaGAAAmhkAAJoaAACaHAAAmh0AAJpcAACaXgAAmmAAAJpiAACaZQAAmmYAAJpnAACaaAAAmmkAAJprAACabQAAmm4AAJpvAACacQAAmnIAAJqxAACaswAAmrUAAJq3AACaugAAmrsAAJq8AACavQAAmr4AAJrAAACawgAAmsMAAJrEAACaxgAAmscAAJsSAACbNQAAm1UAAJt1AACbdwAAm3kAAJt7AACbfQAAm4AAAJuBAACbggAAm4UAAJuGAACbiAAAm4kAAJuLAACbjQAAm44AAJuPAACbkgAAm5MAAJuYAACbpQAAm6oAAJusAACbrgAAm7MAAJu2AACbuQAAm7sAAJvgAACcBAAAnCsAAJxPAACcUgAAnFQAAJxWAACcWAAAnFoAAJxcAACcXQAAnGAAAJxtAACcfgAAnIAAAJyCAACchAAAnIYAAJyIAACcigAAnIwAAJyOAACcnwAAnKIAAJylAACcqAAAnKsAAJyuAACcsQAAnLQAAJy3AACcuQAAnPgAAJz6AACc/AAAnP4AAJ0BAACdAgAAnQMAAJ0EAACdBQAAnQcAAJ0JAACdCgAAnQsAAJ0NAACdDgAAnU0AAJ1PAACdUQAAnVMAAJ1WAACdVwAAnVgAAJ1ZAACdWgAAnVwAAJ1eAACdXwAAnWAAAJ1iAACdYwAAnaIAAJ2kAACdpwAAnakAAJ2sAACdrQAAna4AAJ2vAACdsAAAnbIAAJ20AACdtQAAnbYAAJ24AACduQAAncYAAJ3HAACdyAAAncoAAJ4JAACeCwAAng0AAJ4PAACeEgAAnhMAAJ4UAACeFQAAnhYAAJ4YAACeGgAAnhsAAJ4cAACeHgAAnh8AAJ5eAACeYAAAnmIAAJ5kAACeZwAAnmgAAJ5pAACeagAAnmsAAJ5tAACebwAAnnAAAJ5xAACecwAAnnQAAJ6zAACetQAAnrcAAJ65AACevAAAnr0AAJ6+AACevwAAnsAAAJ7CAACexAAAnsUAAJ7GAACeyAAAnskAAJ8IAACfCgAAnwwAAJ8OAACfEQAAnxIAAJ8TAACfFAAAnxUAAJ8XAACfGQAAnxoAAJ8bAACfHQAAnx4AAJ9dAACfXwAAn2EAAJ9jAACfZgAAn2cAAJ9oAACfaQAAn2oAAJ9sAACfbgAAn28AAJ9wAACfcgAAn3MAAJ+YAACfvAAAn+MAAKAHAACgCgAAoAwAAKAOAACgEAAAoBIAAKAUAACgFQAAoBgAAKAlAACgNAAAoDYAAKA4AACgOgAAoDwAAKA+AACgQAAAoEIAAKBRAACgVAAAoFcAAKBaAACgXQAAoGAAAKBjAACgZgAAoGgAAKCnAACgqQAAoKsAAKCtAACgsAAAoLEAAKCyAACgswAAoLQAAKC2AACguAAAoLkAAKC6AACgvAAAoL0AAKD8AACg/gAAoQAAAKECAAChBQAAoQYAAKEHAAChCAAAoQkAAKELAAChDQAAoQ4AAKEPAAChEQAAoRIAAKFRAAChUwAAoVUAAKFXAAChWgAAoVsAAKFcAAChXQAAoV4AAKFgAAChYgAAoWMAAKFkAAChZgAAoWcAAKGmAAChqAAAoaoAAKGsAAChrwAAobAAAKGxAAChsgAAobMAAKG1AAChtwAAobgAAKG5AAChuwAAobwAAKH7AACh/QAAof8AAKIBAACiBAAAogUAAKIGAACiBwAAoggAAKIKAACiDAAAog0AAKIOAACiEAAAohEAAKJQAACiUgAAolQAAKJWAACiWQAAoloAAKJbAACiXAAAol0AAKJfAACiYQAAomIAAKJjAACiZQAAomYAAKKlAACipwAAoqkAAKKrAACirgAAoq8AAKKwAACisQAAorIAAKK0AACitgAAorcAAKK4AACiugAAorsAAKMGAACjKQAAo0kAAKNpAACjawAAo20AAKNvAACjcQAAo3QAAKN1AACjdgAAo3kAAKN6AACjfAAAo30AAKN/AACjggAAo4MAAKOEAACjhwAAo4gAAKORAACjngAAo6MAAKOlAACjpwAAo6wAAKOvAACjsgAAo7QAAKPZAACj/QAApCQAAKRIAACkSwAApE0AAKRPAACkUQAApFMAAKRVAACkVgAApFkAAKRmAACkdwAApHkAAKR7AACkfQAApH8AAKSBAACkgwAApIUAAKSHAACkmAAApJsAAKSeAACkoQAApKQAAKSnAACkqgAApK0AAKSwAACksgAApPEAAKTzAACk9QAApPcAAKT6AACk+wAApPwAAKT9AACk/gAApQAAAKUCAAClAwAApQQAAKUGAAClBwAApUYAAKVIAAClSgAApUwAAKVPAAClUAAApVEAAKVSAAClUwAApVUAAKVXAAClWAAApVkAAKVbAAClXAAApZsAAKWdAACloAAApaIAAKWlAAClpgAApacAAKWoAAClqQAApasAAKWtAAClrgAApa8AAKWxAAClsgAApb8AAKXAAAClwQAApcMAAKYCAACmBAAApgYAAKYIAACmCwAApgwAAKYNAACmDgAApg8AAKYRAACmEwAAphQAAKYVAACmFwAAphgAAKZXAACmWQAAplsAAKZdAACmYAAApmEAAKZiAACmYwAApmQAAKZmAACmaAAApmkAAKZqAACmbAAApm0AAKasAACmrgAAprAAAKayAACmtQAAprYAAKa3AACmuAAAprkAAKa7AACmvQAApr4AAKa/AACmwQAApsIAAKcBAACnAwAApwUAAKcHAACnCgAApwsAAKcMAACnDQAApw4AAKcQAACnEgAApxMAAKcUAACnFgAApxcAAKdWAACnWAAAp1oAAKdcAACnXwAAp2AAAKdhAACnYgAAp2MAAKdlAACnZwAAp2gAAKdpAACnawAAp2wAAKeRAACntQAAp9wAAKgAAACoAwAAqAUAAKgHAACoCQAAqAsAAKgNAACoDgAAqBEAAKgeAACoLQAAqC8AAKgxAACoMwAAqDUAAKg3AACoOQAAqDsAAKhKAACoTQAAqFAAAKhTAACoVgAAqFkAAKhcAACoXwAAqGEAAKigAACoogAAqKQAAKimAACoqQAAqKoAAKirAACorAAAqK0AAKivAACosQAAqLIAAKizAACotQAAqLYAAKj1AACo9wAAqPkAAKj7AACo/gAAqP8AAKkAAACpAQAAqQIAAKkEAACpBgAAqQcAAKkIAACpCgAAqQsAAKlKAACpTAAAqU4AAKlQAACpUwAAqVQAAKlVAACpVgAAqVcAAKlZAACpWwAAqVwAAKldAACpXwAAqWAAAKmfAACpoQAAqaMAAKmlAACpqAAAqakAAKmqAACpqwAAqawAAKmuAACpsAAAqbEAAKmyAACptAAAqbUAAKn0AACp9gAAqfgAAKn6AACp/QAAqf4AAKn/AACqAAAAqgEAAKoDAACqBQAAqgYAAKoHAACqCQAAqgoAAKpJAACqSwAAqk0AAKpPAACqUgAAqlMAAKpUAACqVQAAqlYAAKpYAACqWgAAqlsAAKpcAACqXgAAql8AAKqeAACqoAAAqqIAAKqkAACqpwAAqqgAAKqpAACqqgAAqqsAAKqtAACqrwAAqrAAAKqxAACqswAAqrQAAKr/AACrIgAAq0IAAKtiAACrZAAAq2YAAKtoAACragAAq20AAKtuAACrbwAAq3IAAKtzAACrdQAAq3YAAKt5AACrfAAAq30AAKt+AACrgQAAq4IAAKuHAACrlAAAq5kAAKubAACrnQAAq6IAAKulAACrqAAAq6oAAKvPAACr8wAArBoAAKw+AACsQQAArEMAAKxFAACsRwAArEkAAKxLAACsTAAArE8AAKxcAACsbQAArG8AAKxxAACscwAArHUAAKx3AACseQAArHsAAKx9AACsjgAArJEAAKyUAACslwAArJoAAKydAACsoAAArKMAAKymAACsqAAArOcAAKzpAACs6wAArO0AAKzwAACs8QAArPIAAKzzAACs9AAArPYAAKz4AACs+QAArPoAAKz8AACs/QAArTwAAK0+AACtQAAArUIAAK1FAACtRgAArUcAAK1IAACtSQAArUsAAK1NAACtTgAArU8AAK1RAACtUgAArZEAAK2TAACtlgAArZgAAK2bAACtnAAArZ0AAK2eAACtnwAAraEAAK2jAACtpAAAraUAAK2nAACtqAAArbUAAK22AACttwAArbkAAK34AACt+gAArfwAAK3+AACuAQAArgIAAK4DAACuBAAArgUAAK4HAACuCQAArgoAAK4LAACuDQAArg4AAK5NAACuTwAArlEAAK5TAACuVgAArlcAAK5YAACuWQAArloAAK5cAACuXgAArl8AAK5gAACuYgAArmMAAK6iAACupAAArqYAAK6oAACuqwAArqwAAK6tAACurgAArq8AAK6xAACuswAArrQAAK61AACutwAArrgAAK73AACu+QAArvsAAK79AACvAAAArwEAAK8CAACvAwAArwQAAK8GAACvCAAArwkAAK8KAACvDAAArw0AAK9MAACvTgAAr1AAAK9SAACvVQAAr1YAAK9XAACvWAAAr1kAAK9bAACvXQAAr14AAK9fAACvYQAAr2IAAK+HAACvqwAAr9IAAK/2AACv+QAAr/sAAK/9AACv/wAAsAEAALADAACwBAAAsAcAALAUAACwIwAAsCUAALAnAACwKQAAsCsAALAtAACwLwAAsDIAALBBAACwRAAAsEcAALBKAACwTQAAsFAAALBTAACwVgAAsFgAALCXAACwmQAAsJwAALCeAACwoQAAsKIAALCjAACwpAAAsKUAALCnAACwqQAAsKoAALCrAACwrQAAsK4AALDtAACw7wAAsPIAALD0AACw9wAAsPgAALD5AACw+gAAsPsAALD9AACw/wAAsQAAALEBAACxAwAAsQQAALFDAACxRQAAsUgAALFKAACxTQAAsU4AALFPAACxUAAAsVEAALFTAACxVQAAsVYAALFXAACxWQAAsVoAALGZAACxmwAAsZ0AALGfAACxogAAsaMAALGkAACxpQAAsaYAALGoAACxqgAAsasAALGsAACxrgAAsa8AALHuAACx8AAAsfIAALH0AACx9wAAsfgAALH5AACx+gAAsfsAALH9AACx/wAAsgAAALIBAACyAwAAsgQAALJDAACyRQAAskcAALJJAACyTAAAsk0AALJOAACyTwAAslAAALJSAACyVAAAslUAALJWAACyWAAAslkAALKYAACymgAAsp0AALKfAACyogAAsqMAALKkAACypQAAsqYAALKoAACyqwAAsqwAALKtAACyrwAAsrAAALL7AACzHgAAsz4AALNeAACzYAAAs2IAALNkAACzZgAAs2kAALNqAACzawAAs24AALNvAACzcQAAs3IAALN0AACzdwAAs3gAALN5AACzfAAAs30AALOGAACzkwAAs5gAALOaAACznAAAs6EAALOkAACzpwAAs6kAALPOAACz8gAAtBkAALQ9AAC0QAAAtEIAALREAAC0RgAAtEgAALRKAAC0SwAAtE4AALRbAAC0bAAAtG4AALRwAAC0cgAAtHQAALR2AAC0eAAAtHoAALR8AAC0jQAAtJAAALSTAAC0lgAAtJkAALScAAC0nwAAtKIAALSlAAC0pwAAtOYAALToAAC06gAAtOwAALTvAAC08AAAtPEAALTyAAC08wAAtPUAALT3AAC0+AAAtPkAALT7AAC0/AAAtTsAALU9AAC1PwAAtUEAALVEAAC1RQAAtUYAALVHAAC1SAAAtUoAALVMAAC1TQAAtU4AALVQAAC1UQAAtZAAALWSAAC1lQAAtZcAALWaAAC1mwAAtZwAALWdAAC1ngAAtaAAALWiAAC1owAAtaQAALWmAAC1pwAAtbQAALW1AAC1tgAAtbgAALX3AAC1+QAAtfsAALX9AAC2AAAAtgEAALYCAAC2AwAAtgQAALYGAAC2CAAAtgkAALYKAAC2DAAAtg0AALZMAAC2TgAAtlAAALZSAAC2VQAAtlYAALZXAAC2WAAAtlkAALZbAAC2XQAAtl4AALZfAAC2YQAAtmIAALahAAC2owAAtqUAALanAAC2qgAAtqsAALasAAC2rQAAtq4AALawAAC2sgAAtrMAALa0AAC2tgAAtrcAALb2AAC2+AAAtvoAALb8AAC2/wAAtwAAALcBAAC3AgAAtwMAALcFAAC3BwAAtwgAALcJAAC3CwAAtwwAALdLAAC3TQAAt08AALdRAAC3VAAAt1UAALdWAAC3VwAAt1gAALdaAAC3XAAAt10AALdeAAC3YAAAt2EAALeGAAC3qgAAt9EAALf1AAC3+AAAt/oAALf8AAC3/gAAuAAAALgCAAC4AwAAuAYAALgTAAC4IgAAuCQAALgmAAC4KAAAuCoAALgsAAC4LgAAuDAAALg/AAC4QgAAuEUAALhIAAC4SwAAuE4AALhRAAC4VAAAuFYAALiVAAC4lwAAuJkAALibAAC4ngAAuJ8AALigAAC4oQAAuKIAALikAAC4pgAAuKcAALioAAC4qgAAuKsAALjqAAC47AAAuO4AALjwAAC48wAAuPQAALj1AAC49gAAuPcAALj5AAC4+wAAuPwAALj9AAC4/wAAuQAAALk/AAC5QQAAuUMAALlFAAC5SAAAuUkAALlKAAC5SwAAuUwAALlOAAC5UAAAuVEAALlSAAC5VAAAuVUAALmUAAC5lgAAuZgAALmaAAC5nQAAuZ4AALmfAAC5oAAAuaEAALmjAAC5pQAAuaYAALmnAAC5qQAAuaoAALnpAAC56wAAue0AALnvAAC58gAAufMAALn0AAC59QAAufYAALn4AAC5+gAAufsAALn8AAC5/gAAuf8AALo+AAC6QAAAukIAALpEAAC6RwAAukgAALpJAAC6SgAAuksAALpNAAC6TwAAulAAALpRAAC6UwAAulQAALqTAAC6lQAAupcAALqZAAC6nAAAup0AALqeAAC6nwAAuqAAALqiAAC6pAAAuqUAALqmAAC6qAAAuqkAALr0AAC7FwAAuzcAALtXAAC7WQAAu1sAALtdAAC7XwAAu2IAALtjAAC7ZAAAu2cAALtoAAC7agAAu2sAALttAAC7cAAAu3EAALtyAAC7dQAAu3YAALt7AAC7iAAAu40AALuPAAC7kQAAu5YAALuZAAC7nAAAu54AALvDAAC75wAAvA4AALwyAAC8NQAAvDcAALw5AAC8OwAAvD0AALw/AAC8QAAAvEMAALxQAAC8YQAAvGMAALxlAAC8ZwAAvGkAALxrAAC8bQAAvG8AALxxAAC8ggAAvIUAALyIAAC8iwAAvI4AALyRAAC8lAAAvJcAALyaAAC8nAAAvNsAALzdAAC83wAAvOEAALzkAAC85QAAvOYAALznAAC86AAAvOoAALzsAAC87QAAvO4AALzwAAC88QAAvTAAAL0yAAC9NAAAvTYAAL05AAC9OgAAvTsAAL08AAC9PQAAvT8AAL1BAAC9QgAAvUMAAL1FAAC9RgAAvYUAAL2HAAC9igAAvYwAAL2PAAC9kAAAvZEAAL2SAAC9kwAAvZUAAL2XAAC9mAAAvZkAAL2bAAC9nAAAvakAAL2qAAC9qwAAva0AAL3sAAC97gAAvfAAAL3yAAC99QAAvfYAAL33AAC9+AAAvfkAAL37AAC9/QAAvf4AAL3/AAC+AQAAvgIAAL5BAAC+QwAAvkUAAL5HAAC+SgAAvksAAL5MAAC+TQAAvk4AAL5QAAC+UgAAvlMAAL5UAAC+VgAAvlcAAL6WAAC+mAAAvpoAAL6cAAC+nwAAvqAAAL6hAAC+ogAAvqMAAL6lAAC+pwAAvqgAAL6pAAC+qwAAvqwAAL7rAAC+7QAAvu8AAL7xAAC+9AAAvvUAAL72AAC+9wAAvvgAAL76AAC+/AAAvv0AAL7+AAC/AAAAvwEAAL9AAAC/QgAAv0QAAL9GAAC/SQAAv0oAAL9LAAC/TAAAv00AAL9PAAC/UQAAv1IAAL9TAAC/VQAAv1YAAL97AAC/nwAAv8YAAL/qAAC/7QAAv+8AAL/xAAC/8wAAv/UAAL/3AAC/+AAAv/sAAMAIAADAFwAAwBkAAMAbAADAHQAAwB8AAMAhAADAIwAAwCUAAMA0AADANwAAwDoAAMA9AADAQAAAwEMAAMBGAADASQAAwEsAAMCKAADAjAAAwI4AAMCQAADAkwAAwJQAAMCVAADAlgAAwJcAAMCZAADAmwAAwJwAAMCdAADAnwAAwKAAAMDfAADA4QAAwOMAAMDlAADA6AAAwOkAAMDqAADA6wAAwOwAAMDuAADA8AAAwPEAAMDyAADA9AAAwPUAAME0AADBNgAAwTgAAME6AADBPQAAwT4AAME/AADBQAAAwUEAAMFDAADBRQAAwUYAAMFHAADBSQAAwUoAAMGJAADBiwAAwY4AAMGQAADBkwAAwZQAAMGVAADBlgAAwZcAAMGZAADBmwAAwZwAAMGdAADBnwAAwaAAAMHfAADB4QAAweMAAMHlAADB6AAAwekAAMHqAADB6wAAwewAAMHuAADB8AAAwfEAAMHyAADB9AAAwfUAAMI0AADCNgAAwjgAAMI6AADCPQAAwj4AAMI/AADCQAAAwkEAAMJDAADCRQAAwkYAAMJHAADCSQAAwkoAAMKJAADCiwAAwo0AAMKPAADCkgAAwpMAAMKUAADClQAAwpYAAMKYAADCmgAAwpsAAMKcAADCngAAwp8AAMLqAADDDQAAwy0AAMNNAADDTwAAw1EAAMNTAADDVQAAw1gAAMNZAADDWgAAw10AAMNeAADDYAAAw2EAAMNjAADDZgAAw2cAAMNoAADDawAAw2wAAMNxAADDfgAAw4MAAMOFAADDhwAAw4wAAMOPAADDkgAAw5QAAMO5AADD3QAAxAQAAMQoAADEKwAAxC0AAMQvAADEMQAAxDMAAMQ1AADENgAAxDkAAMRGAADEVwAAxFkAAMRbAADEXQAAxF8AAMRhAADEYwAAxGUAAMRnAADEeAAAxHsAAMR+AADEgQAAxIQAAMSHAADEigAAxI0AAMSQAADEkgAAxNEAAMTTAADE1QAAxNcAAMTaAADE2wAAxNwAAMTdAADE3gAAxOAAAMTiAADE4wAAxOQAAMTmAADE5wAAxSYAAMUoAADFKgAAxSwAAMUvAADFMAAAxTEAAMUyAADFMwAAxTUAAMU3AADFOAAAxTkAAMU7AADFPAAAxXsAAMV9AADFgAAAxYIAAMWFAADFhgAAxYcAAMWIAADFiQAAxYsAAMWNAADFjgAAxY8AAMWRAADFkgAAxZ8AAMWgAADFoQAAxaMAAMXiAADF5AAAxeYAAMXoAADF6wAAxewAAMXtAADF7gAAxe8AAMXxAADF8wAAxfQAAMX1AADF9wAAxfgAAMY3AADGOQAAxjsAAMY9AADGQAAAxkEAAMZCAADGQwAAxkQAAMZGAADGSAAAxkkAAMZKAADGTAAAxk0AAMaMAADGjgAAxpAAAMaSAADGlQAAxpYAAMaXAADGmAAAxpkAAMabAADGnQAAxp4AAMafAADGoQAAxqIAAMbhAADG4wAAxuUAAMbnAADG6gAAxusAAMbsAADG7QAAxu4AAMbwAADG8gAAxvMAAMb0AADG9gAAxvcAAMc2AADHOAAAxzoAAMc8AADHPwAAx0AAAMdBAADHQgAAx0MAAMdFAADHRwAAx0gAAMdJAADHSwAAx0wAAMdxAADHlQAAx7wAAMfgAADH4wAAx+UAAMfnAADH6QAAx+sAAMftAADH7gAAx/EAAMf+AADIDQAAyA8AAMgRAADIEwAAyBUAAMgXAADIGQAAyBsAAMgqAADILQAAyDAAAMgzAADINgAAyDkAAMg8AADIPwAAyEEAAMiAAADIggAAyIQAAMiGAADIiQAAyIoAAMiLAADIjAAAyI0AAMiPAADIkQAAyJIAAMiTAADIlQAAyJYAAMjVAADI1wAAyNkAAMjbAADI3gAAyN8AAMjgAADI4QAAyOIAAMjkAADI5gAAyOcAAMjoAADI6gAAyOsAAMkqAADJLAAAyS4AAMkwAADJMwAAyTQAAMk1AADJNgAAyTcAAMk5AADJOwAAyTwAAMk9AADJPwAAyUAAAMl/AADJgQAAyYMAAMmFAADJiAAAyYkAAMmKAADJiwAAyYwAAMmOAADJkAAAyZEAAMmSAADJlAAAyZUAAMnUAADJ1gAAydgAAMnaAADJ3QAAyd4AAMnfAADJ4AAAyeEAAMnjAADJ5QAAyeYAAMnnAADJ6QAAyeoAAMopAADKKwAAyi0AAMovAADKMgAAyjMAAMo0AADKNQAAyjYAAMo4AADKOgAAyjsAAMo8AADKPgAAyj8AAMp+AADKgAAAyoIAAMqEAADKhwAAyogAAMqJAADKigAAyosAAMqNAADKjwAAypAAAMqRAADKkwAAypQAAMrfAADLAgAAyyIAAMtCAADLRAAAy0YAAMtIAADLSgAAy00AAMtOAADLTwAAy1IAAMtTAADLVQAAy1YAAMtZAADLXAAAy10AAMteAADLYQAAy2IAAMtrAADLeAAAy30AAMt/AADLgQAAy4YAAMuJAADLjAAAy44AAMuzAADL1wAAy/4AAMwiAADMJQAAzCcAAMwpAADMKwAAzC0AAMwvAADMMAAAzDMAAMxAAADMUQAAzFMAAMxVAADMVwAAzFkAAMxbAADMXQAAzF8AAMxhAADMcgAAzHUAAMx4AADMewAAzH4AAMyBAADMhAAAzIcAAMyKAADMjAAAzMsAAMzNAADMzwAAzNEAAMzUAADM1QAAzNYAAMzXAADM2AAAzNoAAMzcAADM3QAAzN4AAMzgAADM4QAAzSAAAM0iAADNJAAAzSYAAM0pAADNKgAAzSsAAM0sAADNLQAAzS8AAM0xAADNMgAAzTMAAM01AADNNgAAzXUAAM13AADNegAAzXwAAM1/AADNgAAAzYEAAM2CAADNgwAAzYUAAM2HAADNiAAAzYkAAM2LAADNjAAAzZkAAM2aAADNmwAAzZ0AAM3cAADN3gAAzeAAAM3iAADN5QAAzeYAAM3nAADN6AAAzekAAM3rAADN7QAAze4AAM3vAADN8QAAzfIAAM4xAADOMwAAzjUAAM43AADOOgAAzjsAAM48AADOPQAAzj4AAM5AAADOQgAAzkMAAM5EAADORgAAzkcAAM6GAADOiAAAzooAAM6MAADOjwAAzpAAAM6RAADOkgAAzpMAAM6VAADOlwAAzpgAAM6ZAADOmwAAzpwAAM7bAADO3QAAzt8AAM7hAADO5AAAzuUAAM7mAADO5wAAzugAAM7qAADO7AAAzu0AAM7uAADO8AAAzvEAAM8wAADPMgAAzzQAAM82AADPOQAAzzoAAM87AADPPAAAzz0AAM8/AADPQQAAz0IAAM9DAADPRQAAz0YAAM9rAADPjwAAz7YAAM/aAADP3QAAz98AAM/hAADP4wAAz+UAAM/nAADP6AAAz+sAAM/4AADQBQAA0AcAANAJAADQCwAA0A0AANAPAADQEQAA0B4AANAhAADQJAAA0CcAANAqAADQLQAA0DAAANAyAADQcQAA0HMAANB2AADQeAAA0HsAANB8AADQfQAA0H4AANB/AADQgQAA0IMAANCEAADQhQAA0IcAANCIAADQywAA0O8AANETAADRNgAA0V0AANF9AADRpAAA0csAANHrAADSDwAA0jMAANI1AADSOAAA0joAANI8AADSPgAA0kEAANJDAADSRQAA0kcAANJJAADSSwAA0k0AANJQAADSUwAA0lQAANJZAADSZgAA0mkAANJrAADSbgAA0nEAANJzAADSmAAA0rwAANLjAADTBwAA0woAANMMAADTDgAA0xAAANMSAADTFAAA0xUAANMYAADTJQAA0zgAANM6AADTPAAA0z4AANNAAADTQgAA00QAANNGAADTSAAA00oAANNdAADTYAAA02MAANNmAADTaQAA02wAANNvAADTcgAA03UAANN4AADTegAA07kAANO7AADTvgAA08AAANPDAADTxAAA08UAANPGAADTxwAA08kAANPLAADTzAAA080AANPPAADT0AAA09kAANPaAADT3AAA1BsAANQdAADUHwAA1CEAANQkAADUJQAA1CYAANQnAADUKAAA1CoAANQsAADULQAA1C4AANQwAADUMQAA1HAAANRyAADUdQAA1HcAANR6AADUewAA1HwAANR9AADUfgAA1IAAANSCAADUgwAA1IQAANSGAADUhwAA1JAAANSTAADUlgAA1JgAANShAADUpAAA1KcAANSpAADUrQAA1OwAANTuAADU8AAA1PIAANT1AADU9gAA1PcAANT4AADU+QAA1PsAANT9AADU/gAA1P8AANUBAADVAgAA1UEAANVDAADVRgAA1UgAANVLAADVTAAA1U0AANVOAADVTwAA1VEAANVTAADVVAAA1VUAANVXAADVWAAA1WEAANViAADVZAAA1aMAANWlAADVpwAA1akAANWsAADVrQAA1a4AANWvAADVsAAA1bIAANW0AADVtQAA1bYAANW4AADVuQAA1fgAANX6AADV/QAA1f8AANYCAADWAwAA1gQAANYFAADWBgAA1ggAANYKAADWCwAA1gwAANYOAADWDwAA1hwAANYdAADWHgAA1iAAANZfAADWYQAA1mMAANZlAADWaAAA1mkAANZqAADWawAA1mwAANZuAADWcAAA1nEAANZyAADWdAAA1nUAANa0AADWtgAA1rkAANa7AADWvgAA1r8AANbAAADWwQAA1sIAANbEAADWxgAA1scAANbIAADWygAA1ssAANbXAADW5AAA1u8AANbyAADW9AAA1vcAANb6AADW/QAA1wgAANcLAADXDgAA1xEAANcUAADXFwAA1xkAANckAADXMQAA13wAANefAADXvwAA198AANfhAADX4wAA1+UAANfnAADX6gAA1+sAANfsAADX7wAA1/AAANfyAADX8wAA1/UAANf4AADX+QAA1/oAANf9AADX/gAA2AMAANgQAADYFQAA2BcAANgZAADYHgAA2CEAANgkAADYJgAA2EsAANhvAADYlgAA2LoAANi9AADYvwAA2MEAANjDAADYxQAA2McAANjIAADYywAA2NgAANjpAADY6wAA2O0AANjvAADY8QAA2PMAANj1AADY9wAA2PkAANkKAADZDQAA2RAAANkTAADZFgAA2RkAANkcAADZHwAA2SIAANkkAADZYwAA2WUAANlnAADZaQAA2WwAANltAADZbgAA2W8AANlwAADZcgAA2XQAANl1AADZdgAA2XgAANl5AADZuAAA2boAANm8AADZvgAA2cEAANnCAADZwwAA2cQAANnFAADZxwAA2ckAANnKAADZywAA2c0AANnOAADaDQAA2g8AANoSAADaFAAA2hcAANoYAADaGQAA2hoAANobAADaHQAA2h8AANogAADaIQAA2iMAANokAADaMQAA2jIAANozAADaNQAA2nQAANp2AADaeAAA2noAANp9AADafgAA2n8AANqAAADagQAA2oMAANqFAADahgAA2ocAANqJAADaigAA2skAANrLAADazQAA2s8AANrSAADa0wAA2tQAANrVAADa1gAA2tgAANraAADa2wAA2twAANreAADa3wAA2x4AANsgAADbIgAA2yQAANsnAADbKAAA2ykAANsqAADbKwAA2y0AANsvAADbMAAA2zEAANszAADbNAAA23MAANt1AADbdwAA23kAANt8AADbfQAA234AANt/AADbgAAA24IAANuEAADbhQAA24YAANuIAADbiQAA28gAANvKAADbzAAA284AANvRAADb0gAA29MAANvUAADb1QAA29cAANvZAADb2gAA29sAANvdAADb3gAA3AMAANwnAADcTgAA3HIAANx1AADcdwAA3HkAANx7AADcfQAA3H8AANyAAADcgwAA3JAAANyfAADcoQAA3KMAANylAADcpwAA3KkAANyrAADcrQAA3LwAANy/AADcwgAA3MUAANzIAADcywAA3M4AANzRAADc0wAA3RIAAN0UAADdFgAA3RgAAN0bAADdHAAA3R0AAN0eAADdHwAA3SEAAN0jAADdJAAA3SUAAN0nAADdKAAA3WcAAN1pAADdawAA3W0AAN1wAADdcQAA3XIAAN1zAADddAAA3XYAAN14AADdeQAA3XoAAN18AADdfQAA3bwAAN2+AADdwAAA3cIAAN3FAADdxgAA3ccAAN3IAADdyQAA3csAAN3NAADdzgAA3c8AAN3RAADd0gAA3hEAAN4TAADeFQAA3hcAAN4aAADeGwAA3hwAAN4dAADeHgAA3iAAAN4iAADeIwAA3iQAAN4mAADeJwAA3mYAAN5oAADeagAA3mwAAN5vAADecAAA3nEAAN5yAADecwAA3nUAAN53AADeeAAA3nkAAN57AADefAAA3rsAAN69AADevwAA3sEAAN7EAADexQAA3sYAAN7HAADeyAAA3soAAN7MAADezQAA3s4AAN7QAADe0QAA3xAAAN8SAADfFAAA3xYAAN8ZAADfGgAA3xsAAN8cAADfHQAA3x8AAN8hAADfIgAA3yMAAN8lAADfJgAA33EAAN+UAADftAAA39QAAN/WAADf2AAA39oAAN/cAADf3wAA3+AAAN/hAADf5AAA3+UAAN/nAADf6AAA3+sAAN/tAADf7gAA3+8AAN/yAADf8wAA3/gAAOAFAADgCgAA4AwAAOAOAADgEwAA4BYAAOAZAADgGwAA4EAAAOBkAADgiwAA4K8AAOCyAADgtAAA4LYAAOC4AADgugAA4LwAAOC9AADgwAAA4M0AAODeAADg4AAA4OIAAODkAADg5gAA4OgAAODqAADg7AAA4O4AAOD/AADhAgAA4QUAAOEIAADhCwAA4Q4AAOERAADhFAAA4RcAAOEZAADhWAAA4VoAAOFcAADhXgAA4WEAAOFiAADhYwAA4WQAAOFlAADhZwAA4WkAAOFqAADhawAA4W0AAOFuAADhrQAA4a8AAOGxAADhswAA4bYAAOG3AADhuAAA4bkAAOG6AADhvAAA4b4AAOG/AADhwAAA4cIAAOHDAADiAgAA4gQAAOIHAADiCQAA4gwAAOINAADiDgAA4g8AAOIQAADiEgAA4hQAAOIVAADiFgAA4hgAAOIZAADiJgAA4icAAOIoAADiKgAA4mkAAOJrAADibQAA4m8AAOJyAADicwAA4nQAAOJ1AADidgAA4ngAAOJ6AADiewAA4nwAAOJ+AADifwAA4r4AAOLAAADiwgAA4sQAAOLHAADiyAAA4skAAOLKAADiywAA4s0AAOLPAADi0AAA4tEAAOLTAADi1AAA4xMAAOMVAADjFwAA4xkAAOMcAADjHQAA4x4AAOMfAADjIAAA4yIAAOMkAADjJQAA4yYAAOMoAADjKQAA42gAAONqAADjbAAA424AAONxAADjcgAA43MAAON0AADjdQAA43cAAON5AADjegAA43sAAON9AADjfgAA470AAOO/AADjwQAA48MAAOPGAADjxwAA48gAAOPJAADjygAA48wAAOPOAADjzwAA49AAAOPSAADj0wAA4/gAAOQcAADkQwAA5GcAAORqAADkbAAA5G4AAORwAADkcgAA5HQAAOR1AADkeAAA5IUAAOSUAADklgAA5JgAAOSaAADknAAA5J4AAOSgAADkowAA5LIAAOS1AADkuAAA5LsAAOS+AADkwQAA5MQAAOTHAADkyQAA5QgAAOUKAADlDQAA5Q8AAOUSAADlEwAA5RQAAOUVAADlFgAA5RgAAOUaAADlGwAA5RwAAOUeAADlHwAA5V4AAOVgAADlYwAA5WUAAOVoAADlaQAA5WoAAOVrAADlbAAA5W4AAOVwAADlcQAA5XIAAOV0AADldQAA5bQAAOW2AADluQAA5bsAAOW+AADlvwAA5cAAAOXBAADlwgAA5cQAAOXGAADlxwAA5cgAAOXKAADlywAA5goAAOYMAADmDwAA5hEAAOYUAADmFQAA5hYAAOYXAADmGAAA5hoAAOYcAADmHQAA5h4AAOYgAADmIQAA5mAAAOZiAADmZAAA5mYAAOZpAADmagAA5msAAOZsAADmbQAA5m8AAOZxAADmcgAA5nMAAOZ1AADmdgAA5rUAAOa3AADmuQAA5rsAAOa+AADmvwAA5sAAAObBAADmwgAA5sQAAObGAADmxwAA5sgAAObKAADmywAA5woAAOcMAADnDwAA5xEAAOcUAADnFQAA5xYAAOcXAADnGAAA5xoAAOcdAADnHgAA5x8AAOchAADnIgAA520AAOeQAADnsAAA59AAAOfSAADn1AAA59YAAOfYAADn2wAA59wAAOfdAADn4AAA5+EAAOfjAADn5AAA5+cAAOfqAADn6wAA5+wAAOfvAADn8AAA5/UAAOgCAADoBwAA6AkAAOgLAADoEAAA6BMAAOgWAADoGAAA6D0AAOhhAADoiAAA6KwAAOivAADosQAA6LMAAOi1AADotwAA6LkAAOi6AADovQAA6MoAAOjbAADo3QAA6N8AAOjhAADo4wAA6OUAAOjnAADo6QAA6OsAAOj8AADo/wAA6QIAAOkFAADpCAAA6QsAAOkOAADpEQAA6RQAAOkWAADpVQAA6VcAAOlZAADpWwAA6V4AAOlfAADpYAAA6WEAAOliAADpZAAA6WYAAOlnAADpaAAA6WoAAOlrAADpqgAA6awAAOmuAADpsAAA6bMAAOm0AADptQAA6bYAAOm3AADpuQAA6bsAAOm8AADpvQAA6b8AAOnAAADp/wAA6gEAAOoEAADqBgAA6gkAAOoKAADqCwAA6gwAAOoNAADqDwAA6hEAAOoSAADqEwAA6hUAAOoWAADqIwAA6iQAAOolAADqJwAA6mYAAOpoAADqagAA6mwAAOpvAADqcAAA6nEAAOpyAADqcwAA6nUAAOp3AADqeAAA6nkAAOp7AADqfAAA6rsAAOq9AADqvwAA6sEAAOrEAADqxQAA6sYAAOrHAADqyAAA6soAAOrMAADqzQAA6s4AAOrQAADq0QAA6xAAAOsSAADrFAAA6xYAAOsZAADrGgAA6xsAAOscAADrHQAA6x8AAOshAADrIgAA6yMAAOslAADrJgAA62UAAOtnAADraQAA62sAAOtuAADrbwAA63AAAOtxAADrcgAA63QAAOt2AADrdwAA63gAAOt6AADrewAA67oAAOu8AADrvgAA68AAAOvDAADrxAAA68UAAOvGAADrxwAA68kAAOvLAADrzAAA680AAOvPAADr0AAA6/UAAOwZAADsQAAA7GQAAOxnAADsaQAA7GsAAOxtAADsbwAA7HEAAOxyAADsdQAA7IIAAOyRAADskwAA7JUAAOyXAADsmQAA7JsAAOydAADsoAAA7K8AAOyyAADstQAA7LgAAOy7AADsvgAA7MEAAOzEAADsxgAA7QUAAO0HAADtCgAA7QwAAO0PAADtEAAA7REAAO0SAADtEwAA7RUAAO0XAADtGAAA7RkAAO0bAADtHAAA7VsAAO1dAADtYAAA7WIAAO1lAADtZgAA7WcAAO1oAADtaQAA7WsAAO1tAADtbgAA7W8AAO1xAADtcgAA7XQAAO2zAADttQAA7bgAAO26AADtvQAA7b4AAO2/AADtwAAA7cEAAO3DAADtxQAA7cYAAO3HAADtyQAA7coAAO4JAADuCwAA7g4AAO4QAADuEwAA7hQAAO4VAADuFgAA7hcAAO4ZAADuGwAA7hwAAO4dAADuHwAA7iAAAO5fAADuYQAA7mMAAO5lAADuaAAA7mkAAO5qAADuawAA7mwAAO5uAADucAAA7nEAAO5yAADudAAA7nUAAO60AADutgAA7rgAAO66AADuvQAA7r4AAO6/AADuwAAA7sEAAO7DAADuxQAA7sYAAO7HAADuyQAA7soAAO8JAADvCwAA7w4AAO8QAADvEwAA7xQAAO8VAADvFgAA7xcAAO8ZAADvHAAA7x0AAO8eAADvIAAA7yEAAO9sAADvjwAA768AAO/PAADv0QAA79MAAO/VAADv1wAA79oAAO/bAADv3AAA798AAO/gAADv4gAA7+MAAO/lAADv6AAA7+kAAO/qAADv7QAA7+4AAO/zAADwAAAA8AUAAPAHAADwCQAA8A4AAPARAADwFAAA8BYAAPA7AADwXwAA8IYAAPCqAADwrQAA8K8AAPCxAADwswAA8LUAAPC3AADwuAAA8LsAAPDIAADw2QAA8NsAAPDdAADw3wAA8OEAAPDjAADw5QAA8OcAAPDpAADw+gAA8P0AAPEAAADxAwAA8QYAAPEJAADxDAAA8Q8AAPESAADxFAAA8VMAAPFVAADxVwAA8VkAAPFcAADxXQAA8V4AAPFfAADxYAAA8WIAAPFkAADxZQAA8WYAAPFoAADxaQAA8agAAPGqAADxrAAA8a4AAPGxAADxsgAA8bMAAPG0AADxtQAA8bcAAPG5AADxugAA8bsAAPG9AADxvgAA8f0AAPH/AADyAgAA8gQAAPIHAADyCAAA8gkAAPIKAADyCwAA8g0AAPIPAADyEAAA8hEAAPITAADyFAAA8iEAAPIiAADyIwAA8iUAAPJkAADyZgAA8mgAAPJqAADybQAA8m4AAPJvAADycAAA8nEAAPJzAADydQAA8nYAAPJ3AADyeQAA8noAAPK5AADyuwAA8r0AAPK/AADywgAA8sMAAPLEAADyxQAA8sYAAPLIAADyygAA8ssAAPLMAADyzgAA8s8AAPMOAADzEAAA8xIAAPMUAADzFwAA8xgAAPMZAADzGgAA8xsAAPMdAADzHwAA8yAAAPMhAADzIwAA8yQAAPNjAADzZQAA82cAAPNpAADzbAAA820AAPNuAADzbwAA83AAAPNyAADzdAAA83UAAPN2AADzeAAA83kAAPO4AADzugAA87wAAPO+AADzwQAA88IAAPPDAADzxAAA88UAAPPHAADzyQAA88oAAPPLAADzzQAA884AAPPzAAD0FwAA9D4AAPRiAAD0ZQAA9GcAAPRpAAD0awAA9G0AAPRvAAD0cAAA9HMAAPSAAAD0jwAA9JEAAPSTAAD0lQAA9JcAAPSZAAD0mwAA9J0AAPSsAAD0rwAA9LIAAPS1AAD0uAAA9LsAAPS+AAD0wQAA9MMAAPUCAAD1BAAA9QcAAPUJAAD1DAAA9Q0AAPUOAAD1DwAA9RAAAPUSAAD1FAAA9RUAAPUWAAD1GAAA9RkAAPUaAAD1WQAA9VsAAPVdAAD1XwAA9WIAAPVjAAD1ZAAA9WUAAPVmAAD1aAAA9WoAAPVrAAD1bAAA9W4AAPVvAAD1rgAA9bAAAPWyAAD1tAAA9bcAAPW4AAD1uQAA9boAAPW7AAD1vQAA9b8AAPXAAAD1wQAA9cMAAPXEAAD2AwAA9gUAAPYHAAD2CQAA9gwAAPYNAAD2DgAA9g8AAPYQAAD2EgAA9hQAAPYVAAD2FgAA9hgAAPYZAAD2WAAA9loAAPZcAAD2XgAA9mEAAPZiAAD2YwAA9mQAAPZlAAD2ZwAA9mkAAPZqAAD2awAA9m0AAPZuAAD2rQAA9q8AAPaxAAD2swAA9rYAAPa3AAD2uAAA9rkAAPa6AAD2vAAA9r4AAPa/AAD2wAAA9sIAAPbDAAD3AgAA9wQAAPcGAAD3CAAA9wsAAPcMAAD3DQAA9w4AAPcPAAD3EQAA9xMAAPcUAAD3FQAA9xcAAPcYAAD3YwAA94YAAPemAAD3xgAA98gAAPfKAAD3zAAA984AAPfRAAD30gAA99MAAPfWAAD31wAA99kAAPfaAAD33AAA998AAPfgAAD34QAA9+QAAPflAAD36gAA9/cAAPf8AAD3/gAA+AAAAPgFAAD4CAAA+AsAAPgNAAD4MgAA+FYAAPh9AAD4oQAA+KQAAPimAAD4qAAA+KoAAPisAAD4rgAA+K8AAPiyAAD4vwAA+NAAAPjSAAD41AAA+NYAAPjYAAD42gAA+NwAAPjeAAD44AAA+PEAAPj0AAD49wAA+PoAAPj9AAD5AAAA+QMAAPkGAAD5CQAA+QsAAPlKAAD5TAAA+U4AAPlQAAD5UwAA+VQAAPlVAAD5VgAA+VcAAPlZAAD5WwAA+VwAAPldAAD5XwAA+WAAAPmfAAD5oQAA+aMAAPmlAAD5qAAA+akAAPmqAAD5qwAA+awAAPmuAAD5sAAA+bEAAPmyAAD5tAAA+bUAAPn0AAD59gAA+fkAAPn7AAD5/gAA+f8AAPoAAAD6AQAA+gIAAPoEAAD6BgAA+gcAAPoIAAD6CgAA+gsAAPoYAAD6GQAA+hoAAPocAAD6WwAA+l0AAPpfAAD6YQAA+mQAAPplAAD6ZgAA+mcAAPpoAAD6agAA+mwAAPptAAD6bgAA+nAAAPpxAAD6sAAA+rIAAPq0AAD6tgAA+rkAAPq6AAD6uwAA+rwAAPq9AAD6vwAA+sEAAPrCAAD6wwAA+sUAAPrGAAD7BQAA+wcAAPsJAAD7CwAA+w4AAPsPAAD7EAAA+xEAAPsSAAD7FAAA+xYAAPsXAAD7GAAA+xoAAPsbAAD7WgAA+1wAAPteAAD7YAAA+2MAAPtkAAD7ZQAA+2YAAPtnAAD7aQAA+2sAAPtsAAD7bQAA+28AAPtwAAD7rwAA+7EAAPuzAAD7tQAA+7gAAPu5AAD7ugAA+7sAAPu8AAD7vgAA+8AAAPvBAAD7wgAA+8QAAPvFAAD76gAA/A4AAPw1AAD8WQAA/FwAAPxeAAD8YAAA/GIAAPxkAAD8ZgAA/GcAAPxqAAD8dwAA/IYAAPyIAAD8igAA/IwAAPyOAAD8kAAA/JIAAPyUAAD8owAA/KYAAPypAAD8rAAA/K8AAPyyAAD8tQAA/LgAAPy6AAD8+QAA/PsAAPz9AAD8/wAA/QIAAP0DAAD9BAAA/QUAAP0GAAD9CAAA/QoAAP0LAAD9DAAA/Q4AAP0PAAD9TgAA/VAAAP1SAAD9VAAA/VcAAP1YAAD9WQAA/VoAAP1bAAD9XQAA/V8AAP1gAAD9YQAA/WMAAP1kAAD9owAA/aUAAP2nAAD9qQAA/awAAP2tAAD9rgAA/a8AAP2wAAD9sgAA/bQAAP21AAD9tgAA/bgAAP25AAD9+AAA/foAAP38AAD9/gAA/gEAAP4CAAD+AwAA/gQAAP4FAAD+BwAA/gkAAP4KAAD+CwAA/g0AAP4OAAD+TQAA/k8AAP5RAAD+UwAA/lYAAP5XAAD+WAAA/lkAAP5aAAD+XAAA/l4AAP5fAAD+YAAA/mIAAP5jAAD+ogAA/qQAAP6mAAD+qAAA/qsAAP6sAAD+rQAA/q4AAP6vAAD+sQAA/rMAAP60AAD+tQAA/rcAAP64AAD+9wAA/vkAAP77AAD+/QAA/wAAAP8BAAD/AgAA/wMAAP8EAAD/BgAA/wgAAP8JAAD/CgAA/wwAAP8NAAD/WAAA/3sAAP+bAAD/uwAA/70AAP+/AAD/wQAA/8MAAP/GAAD/xwAA/8gAAP/LAAD/zAAA/84AAP/PAAD/0QAA/9QAAP/VAAD/1gAA/9kAAP/aAAD/3wAA/+wAAP/xAAD/8wAA//UAAP/6AAD//QABAAAAAQACAAEAJwABAEsAAQByAAEAlgABAJkAAQCbAAEAnQABAJ8AAQChAAEAowABAKQAAQCnAAEAtAABAMUAAQDHAAEAyQABAMsAAQDNAAEAzwABANEAAQDTAAEA1QABAOYAAQDpAAEA7AABAO8AAQDyAAEA9QABAPgAAQD7AAEA/gABAQAAAQE/AAEBQQABAUMAAQFFAAEBSAABAUkAAQFKAAEBSwABAUwAAQFOAAEBUAABAVEAAQFSAAEBVAABAVUAAQGUAAEBlgABAZgAAQGaAAEBnQABAZ4AAQGfAAEBoAABAaEAAQGjAAEBpQABAaYAAQGnAAEBqQABAaoAAQHpAAEB6wABAe4AAQHwAAEB8wABAfQAAQH1AAEB9gABAfcAAQH5AAEB+wABAfwAAQH9AAEB/wABAgAAAQINAAECDgABAg8AAQIRAAECUAABAlIAAQJUAAECVgABAlkAAQJaAAECWwABAlwAAQJdAAECXwABAmEAAQJiAAECYwABAmUAAQJmAAECpQABAqcAAQKpAAECqwABAq4AAQKvAAECsAABArEAAQKyAAECtAABArYAAQK3AAECuAABAroAAQK7AAEC+gABAvwAAQL+AAEDAAABAwMAAQMEAAEDBQABAwYAAQMHAAEDCQABAwsAAQMMAAEDDQABAw8AAQMQAAEDTwABA1EAAQNTAAEDVQABA1gAAQNZAAEDWgABA1sAAQNcAAEDXgABA2AAAQNhAAEDYgABA2QAAQNlAAEDpAABA6YAAQOoAAEDqgABA60AAQOuAAEDrwABA7AAAQOxAAEDswABA7UAAQO2AAEDtwABA7kAAQO6AAED3wABBAMAAQQqAAEETgABBFEAAQRTAAEEVQABBFcAAQRZAAEEWwABBFwAAQRfAAEEbAABBHsAAQR9AAEEfwABBIEAAQSDAAEEhQABBIcAAQSJAAEEmAABBJsAAQSeAAEEoQABBKQAAQSnAAEEqgABBK0AAQSvAAEE7gABBPAAAQTyAAEE9AABBPcAAQT4AAEE+QABBPoAAQT7AAEE/QABBP8AAQUAAAEFAQABBQMAAQUEAAEFQwABBUUAAQVHAAEFSQABBUwAAQVNAAEFTgABBU8AAQVQAAEFUgABBVQAAQVVAAEFVgABBVgAAQVZAAEFmAABBZoAAQWcAAEFngABBaEAAQWiAAEFowABBaQAAQWlAAEFpwABBakAAQWqAAEFqwABBa0AAQWuAAEF7QABBe8AAQXxAAEF8wABBfYAAQX3AAEF+AABBfkAAQX6AAEF/AABBf4AAQX/AAEGAAABBgIAAQYDAAEGQgABBkQAAQZGAAEGSAABBksAAQZMAAEGTQABBk4AAQZPAAEGUQABBlMAAQZUAAEGVQABBlcAAQZYAAEGlwABBpkAAQabAAEGnQABBqAAAQahAAEGogABBqMAAQakAAEGpgABBqgAAQapAAEGqgABBqwAAQatAAEG7AABBu4AAQbwAAEG8gABBvUAAQb2AAEG9wABBvgAAQb5AAEG+wABBv0AAQb+AAEG/wABBwEAAQcCAAEHCwABBwwAAQcOAAEHTQABB08AAQdRAAEHUwABB1YAAQdXAAEHWAABB1kAAQdaAAEHXAABB14AAQdfAAEHYAABB2IAAQdjAAEHogABB6QAAQenAAEHqQABB6wAAQetAAEHrgABB68AAQewAAEHsgABB7QAAQe1AAEHtgABB7gAAQe5AAEH+AABB/oAAQf9AAEH/wABCAIAAQgDAAEIBAABCAUAAQgGAAEICAABCAoAAQgLAAEIDAABCA4AAQgPAAEITgABCFAAAQhTAAEIVQABCFgAAQhZAAEIWgABCFsAAQhcAAEIXgABCGAAAQhhAAEIYgABCGQAAQhlAAEIpAABCKYAAQioAAEIqgABCK0AAQiuAAEIrwABCLAAAQixAAEIswABCLUAAQi2AAEItwABCLkAAQi6AAEJBQABCSgAAQlIAAEJaAABCWoAAQlsAAEJbgABCXAAAQlzAAEJdAABCXUAAQl4AAEJeQABCXsAAQl8AAEJfgABCYEAAQmCAAEJgwABCYYAAQmHAAEJkAABCZ0AAQmiAAEJpAABCaYAAQmrAAEJrgABCbEAAQmzAAEJ2AABCfwAAQojAAEKRwABCkoAAQpMAAEKTgABClAAAQpSAAEKVAABClUAAQpYAAEKZQABCnYAAQp4AAEKegABCnwAAQp+AAEKgAABCoIAAQqEAAEKhgABCpcAAQqaAAEKnQABCqAAAQqjAAEKpgABCqkAAQqsAAEKrwABCrEAAQrwAAEK8gABCvQAAQr2AAEK+QABCvoAAQr7AAEK/AABCv0AAQr/AAELAQABCwIAAQsDAAELBQABCwYAAQtFAAELRwABC0kAAQtLAAELTgABC08AAQtQAAELUQABC1IAAQtUAAELVgABC1cAAQtYAAELWgABC1sAAQuaAAELnAABC58AAQuhAAELpAABC6UAAQumAAELpwABC6gAAQuqAAELrAABC60AAQuuAAELsAABC7EAAQu+AAELvwABC8AAAQvCAAEMAQABDAMAAQwFAAEMBwABDAoAAQwLAAEMDAABDA0AAQwOAAEMEAABDBIAAQwTAAEMFAABDBYAAQwXAAEMVgABDFgAAQxaAAEMXAABDF8AAQxgAAEMYQABDGIAAQxjAAEMZQABDGcAAQxoAAEMaQABDGsAAQxsAAEMqwABDK0AAQyvAAEMsQABDLQAAQy1AAEMtgABDLcAAQy4AAEMugABDLwAAQy9AAEMvgABDMAAAQzBAAENAAABDQIAAQ0EAAENBgABDQkAAQ0KAAENCwABDQwAAQ0NAAENDwABDREAAQ0SAAENEwABDRUAAQ0WAAENVQABDVcAAQ1ZAAENWwABDV4AAQ1fAAENYAABDWEAAQ1iAAENZAABDWYAAQ1nAAENaAABDWoAAQ1rAAENkAABDbQAAQ3bAAEN/wABDgIAAQ4EAAEOBgABDggAAQ4KAAEODAABDg0AAQ4QAAEOHQABDiwAAQ4uAAEOMAABDjIAAQ40AAEONgABDjgAAQ46AAEOSQABDkwAAQ5PAAEOUgABDlUAAQ5YAAEOWwABDl4AAQ5gAAEOnwABDqEAAQ6kAAEOpgABDqkAAQ6qAAEOqwABDqwAAQ6tAAEOrwABDrEAAQ6yAAEOswABDrUAAQ62AAEO9QABDvcAAQ75AAEO+wABDv4AAQ7/AAEPAAABDwEAAQ8CAAEPBAABDwYAAQ8HAAEPCAABDwoAAQ8LAAEPSgABD0wAAQ9OAAEPUAABD1MAAQ9UAAEPVQABD1YAAQ9XAAEPWQABD1sAAQ9cAAEPXQABD18AAQ9gAAEPnwABD6EAAQ+kAAEPpgABD6kAAQ+qAAEPqwABD6wAAQ+tAAEPrwABD7EAAQ+yAAEPswABD7UAAQ+2AAEP9QABD/cAAQ/5AAEP+wABD/4AAQ//AAEQAAABEAEAARACAAEQBAABEAYAARAHAAEQCAABEAoAARALAAEQSgABEEwAARBOAAEQUAABEFMAARBUAAEQVQABEFYAARBXAAEQWQABEFsAARBcAAEQXQABEF8AARBgAAEQnwABEKEAARCjAAEQpQABEKgAARCpAAEQqgABEKsAARCsAAEQrgABELAAARCxAAEQsgABELQAARC1AAERAAABESMAARFDAAERYwABEWUAARFnAAERaQABEWsAARFuAAERbwABEXAAARFzAAERdAABEXYAARF3AAEReQABEXwAARF9AAERfgABEYEAARGCAAERhwABEZQAARGZAAERmwABEZ0AARGiAAERpQABEagAARGqAAERzwABEfMAARIaAAESPgABEkEAARJDAAESRQABEkcAARJJAAESSwABEkwAARJPAAESXAABEm0AARJvAAEScQABEnMAARJ1AAESdwABEnkAARJ7AAESfQABEo4AARKRAAESlAABEpcAARKaAAESnQABEqAAARKjAAESpgABEqgAARLnAAES6QABEusAARLtAAES8AABEvEAARLyAAES8wABEvQAARL2AAES+AABEvkAARL6AAES/AABEv0AARM8AAETPgABE0AAARNCAAETRQABE0YAARNHAAETSAABE0kAARNLAAETTQABE04AARNPAAETUQABE1IAARORAAETkwABE5YAAROYAAETmwABE5wAAROdAAETngABE58AAROhAAETowABE6QAAROlAAETpwABE6gAARO1AAETtgABE7cAARO5AAET+AABE/oAARP8AAET/gABFAEAARQCAAEUAwABFAQAARQFAAEUBwABFAkAARQKAAEUCwABFA0AARQOAAEUTQABFE8AARRRAAEUUwABFFYAARRXAAEUWAABFFkAARRaAAEUXAABFF4AARRfAAEUYAABFGIAARRjAAEUogABFKQAARSmAAEUqAABFKsAARSsAAEUrQABFK4AARSvAAEUsQABFLMAARS0AAEUtQABFLcAARS4AAEU9wABFPkAART7AAEU/QABFQAAARUBAAEVAgABFQMAARUEAAEVBgABFQgAARUJAAEVCgABFQwAARUNAAEVTAABFU4AARVQAAEVUgABFVUAARVWAAEVVwABFVgAARVZAAEVWwABFV0AARVeAAEVXwABFWEAARViAAEVhwABFasAARXSAAEV9gABFfkAARX7AAEV/QABFf8AARYBAAEWAwABFgQAARYHAAEWFAABFiMAARYlAAEWJwABFikAARYrAAEWLQABFi8AARYxAAEWQAABFkMAARZGAAEWSQABFkwAARZPAAEWUgABFlUAARZXAAEWlgABFpgAARaaAAEWnAABFp8AARagAAEWoQABFqIAARajAAEWpQABFqcAARaoAAEWqQABFqsAARasAAEW6wABFu0AARbvAAEW8QABFvQAARb1AAEW9gABFvcAARb4AAEW+gABFvwAARb9AAEW/gABFwAAARcBAAEXQAABF0IAARdEAAEXRgABF0kAARdKAAEXSwABF0wAARdNAAEXTwABF1EAARdSAAEXUwABF1UAARdWAAEXlQABF5cAAReZAAEXmwABF54AARefAAEXoAABF6EAAReiAAEXpAABF6YAARenAAEXqAABF6oAARerAAEX6gABF+wAARfuAAEX8AABF/MAARf0AAEX9QABF/YAARf3AAEX+QABF/sAARf8AAEX/QABF/8AARgAAAEYPwABGEEAARhDAAEYRQABGEgAARhJAAEYSgABGEsAARhMAAEYTgABGFAAARhRAAEYUgABGFQAARhVAAEYlAABGJYAARiYAAEYmgABGJ0AARieAAEYnwABGKAAARihAAEYowABGKUAARimAAEYpwABGKkAARiqAAEY9QABGRgAARk4AAEZWAABGVoAARlcAAEZXgABGWAAARljAAEZZAABGWUAARloAAEZaQABGWsAARlsAAEZbgABGXEAARlyAAEZcwABGXYAARl3AAEZfAABGYkAARmOAAEZkAABGZIAARmXAAEZmgABGZ0AARmfAAEZxAABGegAARoPAAEaMwABGjYAARo4AAEaOgABGjwAARo+AAEaQAABGkEAARpEAAEaUQABGmIAARpkAAEaZgABGmgAARpqAAEabAABGm4AARpwAAEacgABGoMAARqGAAEaiQABGowAARqPAAEakgABGpUAARqYAAEamwABGp0AARrcAAEa3gABGuAAARriAAEa5QABGuYAARrnAAEa6AABGukAARrrAAEa7QABGu4AARrvAAEa8QABGvIAARsxAAEbMwABGzUAARs3AAEbOgABGzsAARs8AAEbPQABGz4AARtAAAEbQgABG0MAARtEAAEbRgABG0cAARuGAAEbiAABG4sAARuNAAEbkAABG5EAARuSAAEbkwABG5QAARuWAAEbmAABG5kAARuaAAEbnAABG50AARuqAAEbqwABG6wAARuuAAEb7QABG+8AARvxAAEb8wABG/YAARv3AAEb+AABG/kAARv6AAEb/AABG/4AARv/AAEcAAABHAIAARwDAAEcQgABHEQAARxGAAEcSAABHEsAARxMAAEcTQABHE4AARxPAAEcUQABHFMAARxUAAEcVQABHFcAARxYAAEclwABHJkAARybAAEcnQABHKAAARyhAAEcogABHKMAARykAAEcpgABHKgAARypAAEcqgABHKwAARytAAEc7AABHO4AARzwAAEc8gABHPUAARz2AAEc9wABHPgAARz5AAEc+wABHP0AARz+AAEc/wABHQEAAR0CAAEdQQABHUMAAR1FAAEdRwABHUoAAR1LAAEdTAABHU0AAR1OAAEdUAABHVIAAR1TAAEdVAABHVYAAR1XAAEdfAABHaAAAR3HAAEd6wABHe4AAR3wAAEd8gABHfQAAR32AAEd+AABHfkAAR38AAEeCQABHhgAAR4aAAEeHAABHh4AAR4gAAEeIgABHiQAAR4mAAEeNQABHjgAAR47AAEePgABHkEAAR5EAAEeRwABHkoAAR5MAAEeiwABHo0AAR6PAAEekQABHpQAAR6VAAEelgABHpcAAR6YAAEemgABHpwAAR6dAAEengABHqAAAR6hAAEe4AABHuIAAR7kAAEe5gABHukAAR7qAAEe6wABHuwAAR7tAAEe7wABHvEAAR7yAAEe8wABHvUAAR72AAEfNQABHzcAAR85AAEfOwABHz4AAR8/AAEfQAABH0EAAR9CAAEfRAABH0YAAR9HAAEfSAABH0oAAR9LAAEfigABH4wAAR+OAAEfkAABH5MAAR+UAAEflQABH5YAAR+XAAEfmQABH5sAAR+cAAEfnQABH58AAR+gAAEf3wABH+EAAR/jAAEf5QABH+gAAR/pAAEf6gABH+sAAR/sAAEf7gABH/AAAR/xAAEf8gABH/QAAR/1AAEgNAABIDYAASA4AAEgOgABID0AASA+AAEgPwABIEAAASBBAAEgQwABIEUAASBGAAEgRwABIEkAASBKAAEgiQABIIsAASCNAAEgjwABIJIAASCTAAEglAABIJUAASCWAAEgmAABIJoAASCbAAEgnAABIJ4AASCfAAEg6gABIQ0AASEtAAEhTQABIU8AASFRAAEhUwABIVUAASFYAAEhWQABIVoAASFdAAEhXgABIWAAASFhAAEhYwABIWYAASFnAAEhaAABIWsAASFsAAEhcQABIX4AASGDAAEhhQABIYcAASGMAAEhjwABIZIAASGUAAEhuQABId0AASIEAAEiKAABIisAASItAAEiLwABIjEAASIzAAEiNQABIjYAASI5AAEiRgABIlcAASJZAAEiWwABIl0AASJfAAEiYQABImMAASJlAAEiZwABIngAASJ7AAEifgABIoEAASKEAAEihwABIooAASKNAAEikAABIpIAASLRAAEi0wABItUAASLXAAEi2gABItsAASLcAAEi3QABIt4AASLgAAEi4gABIuMAASLkAAEi5gABIucAASMmAAEjKAABIyoAASMsAAEjLwABIzAAASMxAAEjMgABIzMAASM1AAEjNwABIzgAASM5AAEjOwABIzwAASN7AAEjfQABI4AAASOCAAEjhQABI4YAASOHAAEjiAABI4kAASOLAAEjjQABI44AASOPAAEjkQABI5IAASOfAAEjoAABI6EAASOjAAEj4gABI+QAASPmAAEj6AABI+sAASPsAAEj7QABI+4AASPvAAEj8QABI/MAASP0AAEj9QABI/cAASP4AAEkNwABJDkAASQ7AAEkPQABJEAAASRBAAEkQgABJEMAASREAAEkRgABJEgAASRJAAEkSgABJEwAASRNAAEkjAABJI4AASSQAAEkkgABJJUAASSWAAEklwABJJgAASSZAAEkmwABJJ0AASSeAAEknwABJKEAASSiAAEk4QABJOMAASTlAAEk5wABJOoAASTrAAEk7AABJO0AASTuAAEk8AABJPIAASTzAAEk9AABJPYAAST3AAElNgABJTgAASU6AAElPAABJT8AASVAAAElQQABJUIAASVDAAElRQABJUcAASVIAAElSQABJUsAASVMAAElcQABJZUAASW8AAEl4AABJeMAASXlAAEl5wABJekAASXrAAEl7QABJe4AASXxAAEl/gABJg0AASYPAAEmEQABJhMAASYVAAEmFwABJhkAASYbAAEmKgABJi0AASYwAAEmMwABJjYAASY5AAEmPAABJj8AASZBAAEmgAABJoIAASaEAAEmhgABJokAASaKAAEmiwABJowAASaNAAEmjwABJpEAASaSAAEmkwABJpUAASaWAAEm1QABJtcAASbZAAEm2wABJt4AASbfAAEm4AABJuEAASbiAAEm5AABJuYAASbnAAEm6AABJuoAASbrAAEnKgABJywAAScuAAEnMAABJzMAASc0AAEnNQABJzYAASc3AAEnOQABJzsAASc8AAEnPQABJz8AASdAAAEnfwABJ4EAASeDAAEnhQABJ4gAASeJAAEnigABJ4sAASeMAAEnjgABJ5AAASeRAAEnkgABJ5QAASeVAAEn1AABJ9YAASfYAAEn2gABJ90AASfeAAEn3wABJ+AAASfhAAEn4wABJ+UAASfmAAEn5wABJ+kAASfqAAEoKQABKCsAASgtAAEoLwABKDIAASgzAAEoNAABKDUAASg2AAEoOAABKDoAASg7AAEoPAABKD4AASg/AAEofgABKIAAASiCAAEohAABKIcAASiIAAEoiQABKIoAASiLAAEojQABKI8AASiQAAEokQABKJMAASiUAAEo3wABKQIAASkiAAEpQgABKUQAASlGAAEpSAABKUoAASlNAAEpTgABKU8AASlSAAEpUwABKVUAASlWAAEpWAABKVsAASlcAAEpXQABKWAAASlhAAEpZgABKXMAASl4AAEpegABKXwAASmBAAEphAABKYcAASmJAAEprgABKdIAASn5AAEqHQABKiAAASoiAAEqJAABKiYAASooAAEqKgABKisAASouAAEqOwABKkwAASpOAAEqUAABKlIAASpUAAEqVgABKlgAASpaAAEqXAABKm0AASpwAAEqcwABKnYAASp5AAEqfAABKn8AASqCAAEqhQABKocAASrGAAEqyAABKsoAASrMAAEqzwABKtAAASrRAAEq0gABKtMAASrVAAEq1wABKtgAASrZAAEq2wABKtwAASsbAAErHQABKx8AASshAAErJAABKyUAASsmAAErJwABKygAASsqAAErLAABKy0AASsuAAErMAABKzEAAStwAAErcgABK3UAASt3AAEregABK3sAASt8AAErfQABK34AASuAAAErggABK4MAASuEAAErhgABK4cAASuUAAErlQABK5YAASuYAAEr1wABK9kAASvbAAEr3QABK+AAASvhAAEr4gABK+MAASvkAAEr5gABK+gAASvpAAEr6gABK+wAASvtAAEsLAABLC4AASwwAAEsMgABLDUAASw2AAEsNwABLDgAASw5AAEsOwABLD0AASw+AAEsPwABLEEAASxCAAEsgQABLIMAASyFAAEshwABLIoAASyLAAEsjAABLI0AASyOAAEskAABLJIAASyTAAEslAABLJYAASyXAAEs1gABLNgAASzaAAEs3AABLN8AASzgAAEs4QABLOIAASzjAAEs5QABLOcAASzoAAEs6QABLOsAASzsAAEtKwABLS0AAS0vAAEtMQABLTQAAS01AAEtNgABLTcAAS04AAEtOgABLTwAAS09AAEtPgABLUAAAS1BAAEtZgABLYoAAS2xAAEt1QABLdgAAS3aAAEt3AABLd4AAS3gAAEt4gABLeMAAS3mAAEt8wABLgIAAS4EAAEuBgABLggAAS4KAAEuDAABLg4AAS4QAAEuHwABLiIAAS4lAAEuKAABLisAAS4uAAEuMQABLjQAAS42AAEudQABLncAAS55AAEuewABLn4AAS5/AAEugAABLoEAAS6CAAEuhAABLoYAAS6HAAEuiAABLooAAS6LAAEuygABLswAAS7OAAEu0AABLtMAAS7UAAEu1QABLtYAAS7XAAEu2QABLtsAAS7cAAEu3QABLt8AAS7gAAEvHwABLyEAAS8jAAEvJQABLygAAS8pAAEvKgABLysAAS8sAAEvLgABLzAAAS8xAAEvMgABLzQAAS81AAEvdAABL3YAAS94AAEvegABL30AAS9+AAEvfwABL4AAAS+BAAEvgwABL4UAAS+GAAEvhwABL4kAAS+KAAEvyQABL8sAAS/NAAEvzwABL9IAAS/TAAEv1AABL9UAAS/WAAEv2AABL9oAAS/bAAEv3AABL94AAS/fAAEwHgABMCAAATAiAAEwJAABMCcAATAoAAEwKQABMCoAATArAAEwLQABMC8AATAwAAEwMQABMDMAATA0AAEwcwABMHUAATB3AAEweQABMHwAATB9AAEwfgABMH8AATCAAAEwggABMIQAATCFAAEwhgABMIgAATCJAAEw1AABMPcAATEXAAExNwABMTkAATE7AAExPQABMT8AATFCAAExQwABMUQAATFHAAExSAABMUoAATFLAAExTQABMVAAATFRAAExUgABMVUAATFWAAExWwABMWgAATFtAAExbwABMXEAATF2AAExeQABMXwAATF+AAExowABMccAATHuAAEyEgABMhUAATIXAAEyGQABMhsAATIdAAEyHwABMiAAATIjAAEyMAABMkEAATJDAAEyRQABMkcAATJJAAEySwABMk0AATJPAAEyUQABMmIAATJlAAEyaAABMmsAATJuAAEycQABMnQAATJ3AAEyegABMnwAATK7AAEyvQABMr8AATLBAAEyxAABMsUAATLGAAEyxwABMsgAATLKAAEyzAABMs0AATLOAAEy0AABMtEAATMQAAEzEgABMxQAATMWAAEzGQABMxoAATMbAAEzHAABMx0AATMfAAEzIQABMyIAATMjAAEzJQABMyYAATNlAAEzZwABM2oAATNsAAEzbwABM3AAATNxAAEzcgABM3MAATN1AAEzdwABM3gAATN5AAEzewABM3wAATOJAAEzigABM4sAATONAAEzzAABM84AATPQAAEz0gABM9UAATPWAAEz1wABM9gAATPZAAEz2wABM90AATPeAAEz3wABM+EAATPiAAE0IQABNCMAATQlAAE0JwABNCoAATQrAAE0LAABNC0AATQuAAE0MAABNDIAATQzAAE0NAABNDYAATQ3AAE0dgABNHgAATR6AAE0fAABNH8AATSAAAE0gQABNIIAATSDAAE0hQABNIcAATSIAAE0iQABNIsAATSMAAE0ywABNM0AATTPAAE00QABNNQAATTVAAE01gABNNcAATTYAAE02gABNNwAATTdAAE03gABNOAAATThAAE1IAABNSIAATUkAAE1JgABNSkAATUqAAE1KwABNSwAATUtAAE1LwABNTEAATUyAAE1MwABNTUAATU2AAE1WwABNX8AATWmAAE1ygABNc0AATXPAAE10QABNdMAATXVAAE11wABNdgAATXbAAE16AABNfcAATX5AAE1+wABNf0AATX/AAE2AQABNgMAATYFAAE2FAABNhcAATYaAAE2HQABNiAAATYjAAE2JgABNikAATYrAAE2agABNmwAATZvAAE2cQABNnQAATZ1AAE2dgABNncAATZ4AAE2egABNnwAATZ9AAE2fgABNoAAATaBAAE2wAABNsIAATbEAAE2xgABNskAATbKAAE2ywABNswAATbNAAE2zwABNtEAATbSAAE20wABNtUAATbWAAE3FQABNxcAATcZAAE3GwABNx4AATcfAAE3IAABNyEAATciAAE3JAABNyYAATcnAAE3KAABNyoAATcrAAE3agABN2wAATdvAAE3cQABN3QAATd1AAE3dgABN3cAATd4AAE3egABN3wAATd9AAE3fgABN4AAATeBAAE3wAABN8IAATfEAAE3xgABN8kAATfKAAE3ywABN8wAATfNAAE3zwABN9EAATfSAAE30wABN9UAATfWAAE4FQABOBcAATgZAAE4GwABOB4AATgfAAE4IAABOCEAATgiAAE4JAABOCYAATgnAAE4KAABOCoAATgrAAE4agABOGwAAThuAAE4cAABOHMAATh0AAE4dQABOHYAATh3AAE4eQABOHsAATh8AAE4fQABOH8AATiAAAE4ywABOO4AATkOAAE5LgABOTAAATkyAAE5NAABOTYAATk5AAE5OgABOTsAATk+AAE5PwABOUEAATlCAAE5RAABOUcAATlIAAE5SQABOUwAATlNAAE5VgABOWMAATloAAE5agABOWwAATlxAAE5dAABOXcAATl5AAE5ngABOcIAATnpAAE6DQABOhAAAToSAAE6FAABOhYAAToYAAE6GgABOhsAAToeAAE6KwABOjwAATo+AAE6QAABOkIAATpEAAE6RgABOkgAATpKAAE6TAABOl0AATpgAAE6YwABOmYAATppAAE6bAABOm8AATpyAAE6dQABOncAATq2AAE6uAABOroAATq8AAE6vwABOsAAATrBAAE6wgABOsMAATrFAAE6xwABOsgAATrJAAE6ywABOswAATsLAAE7DQABOw8AATsRAAE7FAABOxUAATsWAAE7FwABOxgAATsaAAE7HAABOx0AATseAAE7IAABOyEAATtgAAE7YgABO2UAATtnAAE7agABO2sAATtsAAE7bQABO24AATtwAAE7cgABO3MAATt0AAE7dgABO3cAATuEAAE7hQABO4YAATuIAAE7xwABO8kAATvLAAE7zQABO9AAATvRAAE70gABO9MAATvUAAE71gABO9gAATvZAAE72gABO9wAATvdAAE8HAABPB4AATwgAAE8IgABPCUAATwmAAE8JwABPCgAATwpAAE8KwABPC0AATwuAAE8LwABPDEAATwyAAE8cQABPHMAATx1AAE8dwABPHoAATx7AAE8fAABPH0AATx+AAE8gAABPIIAATyDAAE8hAABPIYAATyHAAE8xgABPMgAATzKAAE8zAABPM8AATzQAAE80QABPNIAATzTAAE81QABPNcAATzYAAE82QABPNsAATzcAAE9GwABPR0AAT0fAAE9IQABPSQAAT0lAAE9JgABPScAAT0oAAE9KgABPSwAAT0tAAE9LgABPTAAAT0xAAE9VgABPXoAAT2hAAE9xQABPcgAAT3KAAE9zAABPc4AAT3QAAE90gABPdMAAT3WAAE94wABPfIAAT30AAE99gABPfgAAT36AAE9/AABPf4AAT4AAAE+DwABPhIAAT4VAAE+GAABPhsAAT4eAAE+IQABPiQAAT4mAAE+ZQABPmcAAT5pAAE+awABPm4AAT5vAAE+cAABPnEAAT5yAAE+dAABPnYAAT53AAE+eAABPnoAAT57AAE+ugABPrwAAT6+AAE+wAABPsMAAT7EAAE+xQABPsYAAT7HAAE+yQABPssAAT7MAAE+zQABPs8AAT7QAAE/DwABPxEAAT8TAAE/FQABPxgAAT8ZAAE/GgABPxsAAT8cAAE/HgABPyAAAT8hAAE/IgABPyQAAT8lAAE/ZAABP2YAAT9oAAE/agABP20AAT9uAAE/bwABP3AAAT9xAAE/cwABP3UAAT92AAE/dwABP3kAAT96AAE/uQABP7sAAT+9AAE/vwABP8IAAT/DAAE/xAABP8UAAT/GAAE/yAABP8oAAT/LAAE/zAABP84AAT/PAAFADgABQBAAAUASAAFAFAABQBcAAUAYAAFAGQABQBoAAUAbAAFAHQABQB8AAUAgAAFAIQABQCMAAUAkAAFAYwABQGUAAUBnAAFAaQABQGwAAUBtAAFAbgABQG8AAUBwAAFAcgABQHQAAUB1AAFAdgABQHgAAUB5AAFAxAABQOcAAUEHAAFBJwABQSkAAUErAAFBLQABQS8AAUEyAAFBMwABQTQAAUE3AAFBOAABQToAAUE7AAFBPgABQUEAAUFCAAFBQwABQUYAAUFHAAFBTAABQVkA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AVH9AAFR/wABUgQAAVIHAAFSCgABUgwAAVIxAAFSVQABUnwAAVKgAAFSowABUqUAAVKnAAFSqQABUqsAAVKtAAFSrgABUrEAAVK+AAFSzwABUtEAAVLTAAFS1QABUtcAAVLZAAFS2wABUt0AAVLfAAFS8AABUvMAAVL2AAFS+QABUvwAAVL/AAFTAgABUwUAAVMIAAFTCgABU0kAAVNLAAFTTQABU08AAVNSAAFTUwABU1QAAVNVAAFTVgABU1gAAVNaAAFTWwABU1wAAVNeAAFTXwABU54AAVOgAAFTogABU6QAAVOnAAFTqAABU6kAAVOqAAFTqwABU60AAVOvAAFTsAABU7EAAVOzAAFTtAABU/MAAVP1AAFT+AABU/oAAVP9AAFT/gABU/8AAVQAAAFUAQABVAMAAVQFAAFUBgABVAcAAVQJAAFUCgABVBcAAVQYAAFUGQABVBsAAVRaAAFUXAABVF4AAVRgAAFUYwABVGQAAVRlAAFUZgABVGcAAVRpAAFUawABVGwAAVRtAAFUbwABVHAAAVSvAAFUsQABVLMAAVS1AAFUuAABVLkAAVS6AAFUuwABVLwAAVS+AAFUwAABVMEAAVTCAAFUxAABVMUAAVUEAAFVBgABVQgAAVUKAAFVDQABVQ4AAVUPAAFVEAABVREAAVUTAAFVFQABVRYAAVUXAAFVGQABVRoAAVVZAAFVWwABVV0AAVVfAAFVYgABVWMAAVVkAAFVZQABVWYAAVVoAAFVagABVWsAAVVsAAFVbgABVW8AAVWuAAFVsAABVbIAAVW0AAFVtwABVbgAAVW5AAFVugABVbsAAVW9AAFVvwABVcAAAVXBAAFVwwABVcQAAVXpAAFWDQABVjQAAVZYAAFWWwABVl0AAVZfAAFWYQABVmMAAVZlAAFWZgABVmkAAVZ2AAFWhQABVocAAVaJAAFWiwABVo0AAVaPAAFWkQABVpMAAVaiAAFWpQABVqgAAVarAAFWrgABVrEAAVa0AAFWtwABVrkAAVb4AAFW+gABVvwAAVb+AAFXAQABVwIAAVcDAAFXBAABVwUAAVcHAAFXCQABVwoAAVcLAAFXDQABVw4AAVdNAAFXTwABV1EAAVdTAAFXVgABV1cAAVdYAAFXWQABV1oAAVdcAAFXXgABV18AAVdgAAFXYgABV2MAAVeiAAFXpAABV6YAAVeoAAFXqwABV6wAAVetAAFXrgABV68AAVexAAFXswABV7QAAVe1AAFXtwABV7gAAVf3AAFX+QABV/sAAVf9AAFYAAABWAEAAVgCAAFYAwABWAQAAVgGAAFYCAABWAkAAVgKAAFYDAABWA0AAVhMAAFYTgABWFAAAVhSAAFYVQABWFYAAVhXAAFYWAABWFkAAVhbAAFYXQABWF4AAVhfAAFYYQABWGIAAVihAAFYowABWKUAAVinAAFYqgABWKsAAVisAAFYrQABWK4AAViwAAFYsgABWLMAAVi0AAFYtgABWLcAAVj2AAFY+AABWPoAAVj8AAFY/wABWQAAAVkBAAFZAgABWQMAAVkFAAFZBwABWQgAAVkJAAFZCwABWQwAAVlXAAFZegABWZoAAVm6AAFZvAABWb4AAVnAAAFZwgABWcQAAVnFAAFZxgABWckAAVnKAAFZzAABWc0AAVnPAAFZ0QABWdIAAVnTAAFZ1gABWdcAAVncAAFZ6QABWe4AAVnwAAFZ8gABWfcAAVn6AAFZ/QABWf8AAVokAAFaSAABWm8AAVqTAAFalgABWpgAAVqaAAFanAABWp4AAVqgAAFaoQABWqQAAVqxAAFawgABWsQAAVrGAAFayAABWsoAAVrMAAFazgABWtAAAVrSAAFa4wABWuYAAVrpAAFa7AABWu8AAVryAAFa9QABWvgAAVr7AAFa/QABWzwAAVs+AAFbQAABW0IAAVtFAAFbRgABW0cAAVtIAAFbSQABW0sAAVtNAAFbTgABW08AAVtRAAFbUgABW5EAAVuTAAFblQABW5cAAVuaAAFbmwABW5wAAVudAAFbngABW6AAAVuiAAFbowABW6QAAVumAAFbpwABW+YAAVvoAAFb6wABW+0AAVvwAAFb8QABW/IAAVvzAAFb9AABW/YAAVv4AAFb+QABW/oAAVv8AAFb/QABXAoAAVwLAAFcDAABXA4AAVxNAAFcTwABXFEAAVxTAAFcVgABXFcAAVxYAAFcWQABXFoAAVxcAAFcXgABXF8AAVxgAAFcYgABXGMAAVyiAAFcpAABXKYAAVyoAAFcqwABXKwAAVytAAFcrgABXK8AAVyxAAFcswABXLQAAVy1AAFctwABXLgAAVz3AAFc+QABXPsAAVz9AAFdAAABXQEAAV0CAAFdAwABXQQAAV0GAAFdCAABXQkAAV0KAAFdDAABXQ0AAV1MAAFdTgABXVAAAV1SAAFdVQABXVYAAV1XAAFdWAABXVkAAV1bAAFdXQABXV4AAV1fAAFdYQABXWIAAV2hAAFdowABXaUAAV2nAAFdqgABXasAAV2sAAFdrQABXa4AAV2wAAFdsgABXbMAAV20AAFdtgABXbcAAV3cAAFeAAABXicAAV5LAAFeTgABXlAAAV5SAAFeVAABXlYAAV5YAAFeWQABXlwAAV5pAAFeeAABXnoAAV58AAFefgABXoAAAV6CAAFehAABXoYAAV6VAAFemAABXpsAAV6eAAFeoQABXqQAAV6nAAFeqgABXqwAAV7rAAFe7QABXu8AAV7xAAFe9AABXvUAAV72AAFe9wABXvgAAV76AAFe/AABXv0AAV7+AAFfAAABXwEAAV9AAAFfQgABX0QAAV9GAAFfSQABX0oAAV9LAAFfTAABX00AAV9PAAFfUQABX1IAAV9TAAFfVQABX1YAAV+VAAFflwABX5kAAV+bAAFfngABX58AAV+gAAFfoQABX6IAAV+kAAFfpgABX6cAAV+oAAFfqgABX6sAAV/qAAFf7AABX+4AAV/wAAFf8wABX/QAAV/1AAFf9gABX/cAAV/5AAFf+wABX/wAAV/9AAFf/wABYAAAAWA/AAFgQQABYEMAAWBFAAFgSAABYEkAAWBKAAFgSwABYEwAAWBOAAFgUAABYFEAAWBSAAFgVAABYFUAAWCUAAFglgABYJgAAWCaAAFgnQABYJ4AAWCfAAFgoAABYKEAAWCjAAFgpQABYKYAAWCnAAFgqQABYKoAAWDpAAFg6wABYO0AAWDvAAFg8gABYPMAAWD0AAFg9QABYPYAAWD4AAFg+gABYPsAAWD8AAFg/gABYP8AAWEIAAFhCQABYQsAAWFOAAFhcgABYZYAAWG5AAFh4AABYgAAAWInAAFiTgABYm4AAWKSAAFitgABYrgAAWK7AAFivQABYr8AAWLBAAFixAABYsYAAWLIAAFiygABYswAAWLOAAFi0AABYtMAAWLWAAFi1wABYuAAAWLtAAFi8AABYvIAAWL1AAFi+AABYvoAAWMfAAFjQwABY2oAAWOOAAFjkQABY5MAAWOVAAFjlwABY5kAAWObAAFjnAABY58AAWOsAAFjvwABY8EAAWPDAAFjxQABY8cAAWPJAAFjywABY80AAWPPAAFj0QABY+QAAWPnAAFj6gABY+0AAWPwAAFj8wABY/YAAWP5AAFj/AABY/8AAWQBAAFkQAABZEIAAWRFAAFkRwABZEoAAWRLAAFkTAABZE0AAWROAAFkUAABZFIAAWRTAAFkVAABZFYAAWRXAAFkYAABZGEAAWRjAAFkogABZKQAAWSmAAFkqAABZKsAAWSsAAFkrQABZK4AAWSvAAFksQABZLMAAWS0AAFktQABZLcAAWS4AAFk9wABZPkAAWT8AAFk/gABZQEAAWUCAAFlAwABZQQAAWUFAAFlBwABZQkAAWUKAAFlCwABZQ0AAWUOAAFlFwABZRgAAWUaAAFlWQABZVsAAWVdAAFlXwABZWIAAWVjAAFlZAABZWUAAWVmAAFlaAABZWoAAWVrAAFlbAABZW4AAWVvAAFlrgABZbAAAWWzAAFltQABZbgAAWW5AAFlugABZbsAAWW8AAFlvgABZcAAAWXBAAFlwgABZcQAAWXFAAFlzgABZc8AAWXRAAFmEAABZhIAAWYUAAFmFgABZhkAAWYaAAFmGwABZhwAAWYdAAFmHwABZiEAAWYiAAFmIwABZiUAAWYmAAFmZQABZmcAAWZqAAFmbAABZm8AAWZwAAFmcQABZnIAAWZzAAFmdQABZncAAWZ4AAFmeQABZnsAAWZ8AAFmiQABZooAAWaLAAFmjQABZswAAWbOAAFm0AABZtIAAWbVAAFm1gABZtcAAWbYAAFm2QABZtsAAWbdAAFm3gABZt8AAWbhAAFm4gABZyEAAWcjAAFnJgABZygAAWcrAAFnLAABZy0AAWcuAAFnLwABZzEAAWczAAFnNAABZzUAAWc3AAFnOAABZ0YAAWdTAAFnWAABZ1sAAWddAAFnYgABZ2UAAWdoAAFnagABZ3IAAWe9AAFn4AABaAAAAWggAAFoIgABaCQAAWgmAAFoKAABaCsAAWgsAAFoLQABaDAAAWgxAAFoMwABaDQAAWg2AAFoOQABaDoAAWg7AAFoPgABaD8AAWhEAAFoUQABaFYAAWhYAAFoWgABaF8AAWhiAAFoZQABaGcAAWiMAAFosAABaNcAAWj7AAFo/gABaQAAAWkCAAFpBAABaQYAAWkIAAFpCQABaQwAAWkZAAFpKgABaSwAAWkuAAFpMAABaTIAAWk0AAFpNgABaTgAAWk6AAFpSwABaU4AAWlRAAFpVAABaVcAAWlaAAFpXQABaWAAAWljAAFpZQABaaQAAWmmAAFpqAABaaoAAWmtAAFprgABaa8AAWmwAAFpsQABabMAAWm1AAFptgABabcAAWm5AAFpugABafkAAWn7AAFp/QABaf8AAWoCAAFqAwABagQAAWoFAAFqBgABaggAAWoKAAFqCwABagwAAWoOAAFqDwABak4AAWpQAAFqUwABalUAAWpYAAFqWQABaloAAWpbAAFqXAABal4AAWpgAAFqYQABamIAAWpkAAFqZQABanIAAWpzAAFqdAABanYAAWq1AAFqtwABarkAAWq7AAFqvgABar8AAWrAAAFqwQABasIAAWrEAAFqxgABascAAWrIAAFqygABassAAWsKAAFrDAABaw4AAWsQAAFrEwABaxQAAWsVAAFrFgABaxcAAWsZAAFrGwABaxwAAWsdAAFrHwABayAAAWtfAAFrYQABa2MAAWtlAAFraAABa2kAAWtqAAFrawABa2wAAWtuAAFrcAABa3EAAWtyAAFrdAABa3UAAWu0AAFrtgABa7gAAWu6AAFrvQABa74AAWu/AAFrwAABa8EAAWvDAAFrxQABa8YAAWvHAAFryQABa8oAAWwJAAFsCwABbA0AAWwPAAFsEgABbBMAAWwUAAFsFQABbBYAAWwYAAFsGgABbBsAAWwcAAFsHgABbB8AAWxEAAFsaAABbI8AAWyzAAFstgABbLgAAWy6AAFsvAABbL4AAWzAAAFswQABbMQAAWzRAAFs4AABbOIAAWzkAAFs5gABbOgAAWzqAAFs7AABbO4AAWz9AAFtAAABbQMAAW0GAAFtCQABbQwAAW0PAAFtEgABbRQAAW1TAAFtVQABbVcAAW1ZAAFtXAABbV0AAW1eAAFtXwABbWAAAW1iAAFtZAABbWUAAW1mAAFtaAABbWkAAW2oAAFtqgABbawAAW2uAAFtsQABbbIAAW2zAAFttAABbbUAAW23AAFtuQABbboAAW27AAFtvQABbb4AAW39AAFt/wABbgEAAW4DAAFuBgABbgcAAW4IAAFuCQABbgoAAW4MAAFuDgABbg8AAW4QAAFuEgABbhMAAW5SAAFuVAABblYAAW5YAAFuWwABblwAAW5dAAFuXgABbl8AAW5hAAFuYwABbmQAAW5lAAFuZwABbmgAAW6nAAFuqQABbqsAAW6tAAFusAABbrEAAW6yAAFuswABbrQAAW62AAFuuAABbrkAAW66AAFuvAABbr0AAW78AAFu/gABbwAAAW8CAAFvBQABbwYAAW8HAAFvCAABbwkAAW8LAAFvDQABbw4AAW8PAAFvEQABbxIAAW9RAAFvUwABb1UAAW9XAAFvWgABb1sAAW9cAAFvXQABb14AAW9gAAFvYgABb2MAAW9kAAFvZgABb2cAAW+yAAFv1QABb/UAAXAVAAFwFwABcBkAAXAbAAFwHQABcCAAAXAhAAFwIgABcCUAAXAmAAFwKAABcCkAAXArAAFwLQABcC4AAXAvAAFwMgABcDMAAXA8AAFwSQABcE4AAXBQAAFwUgABcFcAAXBaAAFwXQABcF8AAXCEAAFwqAABcM8AAXDzAAFw9gABcPgAAXD6AAFw/AABcP4AAXEAAAFxAQABcQQAAXERAAFxIgABcSQAAXEmAAFxKAABcSoAAXEsAAFxLgABcTAAAXEyAAFxQwABcUYAAXFJAAFxTAABcU8AAXFSAAFxVQABcVgAAXFbAAFxXQABcZwAAXGeAAFxoAABcaIAAXGlAAFxpgABcacAAXGoAAFxqQABcasAAXGtAAFxrgABca8AAXGxAAFxsgABcfEAAXHzAAFx9QABcfcAAXH6AAFx+wABcfwAAXH9AAFx/gABcgAAAXICAAFyAwABcgQAAXIGAAFyBwABckYAAXJIAAFySwABck0AAXJQAAFyUQABclIAAXJTAAFyVAABclYAAXJYAAFyWQABcloAAXJcAAFyXQABcmoAAXJrAAFybAABcm4AAXKtAAFyrwABcrEAAXKzAAFytgABcrcAAXK4AAFyuQABcroAAXK8AAFyvgABcr8AAXLAAAFywgABcsMAAXMCAAFzBAABcwYAAXMIAAFzCwABcwwAAXMNAAFzDgABcw8AAXMRAAFzEwABcxQAAXMVAAFzFwABcxgAAXNXAAFzWQABc1sAAXNdAAFzYAABc2EAAXNiAAFzYwABc2QAAXNmAAFzaAABc2kAAXNqAAFzbAABc20AAXOsAAFzrgABc7AAAXOyAAFztQABc7YAAXO3AAFzuAABc7kAAXO7AAFzvQABc74AAXO/AAFzwQABc8IAAXQBAAF0AwABdAUAAXQHAAF0CgABdAsAAXQMAAF0DQABdA4AAXQQAAF0EgABdBMAAXQUAAF0FgABdBcAAXQ8AAF0YAABdIcAAXSrAAF0rgABdLAAAXSyAAF0tAABdLYAAXS4AAF0uQABdLwAAXTJAAF02AABdNoAAXTcAAF03gABdOAAAXTiAAF05AABdOYAAXT1AAF0+AABdPsAAXT+AAF1AQABdQQAAXUHAAF1CgABdQwAAXVLAAF1TQABdU8AAXVRAAF1VAABdVUAAXVWAAF1VwABdVgAAXVaAAF1XAABdV0AAXVeAAF1YAABdWEAAXWgAAF1ogABdaQAAXWmAAF1qQABdaoAAXWrAAF1rAABda0AAXWvAAF1sQABdbIAAXWzAAF1tQABdbYAAXX1AAF19wABdfkAAXX7AAF1/gABdf8AAXYAAAF2AQABdgIAAXYEAAF2BgABdgcAAXYIAAF2CgABdgsAAXZKAAF2TAABdk4AAXZQAAF2UwABdlQAAXZVAAF2VgABdlcAAXZZAAF2WwABdlwAAXZdAAF2XwABdmAAAXafAAF2oQABdqMAAXalAAF2qAABdqkAAXaqAAF2qwABdqwAAXauAAF2sAABdrEAAXayAAF2tAABdrUAAXb0AAF29gABdvgAAXb6AAF2/QABdv4AAXb/AAF3AAABdwEAAXcDAAF3BQABdwYAAXcHAAF3CQABdwoAAXdJAAF3SwABd00AAXdPAAF3UgABd1MAAXdUAAF3VQABd1YAAXdYAAF3WgABd1sAAXdcAAF3XgABd18AAXdoAAF3aQABd2sAAXeuAAF30gABd/YAAXgZAAF4QAABeGAAAXiHAAF4rgABeM4AAXjyAAF5FgABeRgAAXkbAAF5HQABeR8AAXkhAAF5JAABeSYAAXkoAAF5KgABeSwAAXkuAAF5MAABeTMAAXk2AAF5NwABeTwAAXlJAAF5TAABeU4AAXlRAAF5VAABeVYAAXl7AAF5nwABecYAAXnqAAF57QABee8AAXnxAAF58wABefUAAXn3AAF5+AABefsAAXoIAAF6GwABeh0AAXofAAF6IQABeiMAAXolAAF6JwABeikAAXorAAF6LQABekAAAXpDAAF6RgABekkAAXpMAAF6TwABelIAAXpVAAF6WAABelsAAXpdAAF6nAABep4AAXqhAAF6owABeqYAAXqnAAF6qAABeqkAAXqqAAF6rAABeq4AAXqvAAF6sAABerIAAXqzAAF6vAABer0AAXq/AAF6/gABewAAAXsCAAF7BAABewcAAXsIAAF7CQABewoAAXsLAAF7DQABew8AAXsQAAF7EQABexMAAXsUAAF7UwABe1UAAXtYAAF7WgABe10AAXteAAF7XwABe2AAAXthAAF7YwABe2UAAXtmAAF7ZwABe2kAAXtqAAF7cwABe3QAAXt2AAF7tQABe7cAAXu5AAF7uwABe74AAXu/AAF7wAABe8EAAXvCAAF7xAABe8YAAXvHAAF7yAABe8oAAXvLAAF8CgABfAwAAXwPAAF8EQABfBQAAXwVAAF8FgABfBcAAXwYAAF8GgABfBwAAXwdAAF8HgABfCAAAXwhAAF8KgABfCsAAXwtAAF8bAABfG4AAXxwAAF8cgABfHUAAXx2AAF8dwABfHgAAXx5AAF8ewABfH0AAXx+AAF8fwABfIEAAXyCAAF8wQABfMMAAXzGAAF8yAABfMsAAXzMAAF8zQABfM4AAXzPAAF80QABfNMAAXzUAAF81QABfNcAAXzYAAF85QABfOYAAXznAAF86QABfSgAAX0qAAF9LAABfS4AAX0xAAF9MgABfTMAAX00AAF9NQABfTcAAX05AAF9OgABfTsAAX09AAF9PgABfX0AAX1/AAF9ggABfYQAAX2HAAF9iAABfYkAAX2KAAF9iwABfY0AAX2PAAF9kAABfZEAAX2TAAF9lAABfaIAAX2vAAF9ugABfb0AAX3AAAF9wwABfcUAAX3IAAF90wABfdYAAX3ZAAF93AABfd8AAX3iAAF95AABffAAAX3+AAF+CAABfhEAAX5cAAF+fwABfp8AAX6/AAF+wQABfsMAAX7FAAF+xwABfsoAAX7LAAF+zAABfs8AAX7QAAF+0gABftMAAX7VAAF+2AABftkAAX7aAAF+3QABft4AAX7jAAF+8AABfvUAAX73AAF++QABfv4AAX8BAAF/BAABfwYAAX8rAAF/TwABf3YAAX+aAAF/nQABf58AAX+hAAF/owABf6UAAX+nAAF/qAABf6sAAX+4AAF/yQABf8sAAX/NAAF/zwABf9EAAX/TAAF/1QABf9cAAX/ZAAF/6gABf+0AAX/wAAF/8wABf/YAAX/5AAF//AABf/8AAYACAAGABAABgEMAAYBFAAGARwABgEkAAYBMAAGATQABgE4AAYBPAAGAUAABgFIAAYBUAAGAVQABgFYAAYBYAAGAWQABgJgAAYCaAAGAnAABgJ4AAYChAAGAogABgKMAAYCkAAGApQABgKcAAYCpAAGAqgABgKsAAYCtAAGArgABgO0AAYDvAAGA8gABgPQAAYD3AAGA+AABgPkAAYD6AAGA+wABgP0AAYD/AAGBAAABgQEAAYEDAAGBBAABgREAAYESAAGBEwABgRUAAYFUAAGBVgABgVgAAYFaAAGBXQABgV4AAYFfAAGBYAABgWEAAYFjAAGBZQABgWYAAYFnAAGBaQABgWoAAYGpAAGBqwABga0AAYGvAAGBsgABgbMAAYG0AAGBtQABgbYAAYG4AAGBugABgbsAAYG8AAGBvgABgb8AAYH+AAGCAAABggIAAYIEAAGCBwABgggAAYIJAAGCCgABggsAAYINAAGCDwABghAAAYIRAAGCEwABghQAAYJTAAGCVQABglcAAYJZAAGCXAABgl0AAYJeAAGCXwABgmAAAYJiAAGCZAABgmUAAYJmAAGCaAABgmkAAYKoAAGCqgABgqwAAYKuAAGCsQABgrIAAYKzAAGCtAABgrUAAYK3AAGCuQABgroAAYK7AAGCvQABgr4AAYLjAAGDBwABgy4AAYNSAAGDVQABg1cAAYNZAAGDWwABg10AAYNfAAGDYAABg2MAAYNwAAGDfwABg4EAAYODAAGDhQABg4cAAYOJAAGDiwABg40AAYOcAAGDnwABg6IAAYOlAAGDqAABg6sAAYOuAAGDsQABg7MAAYPyAAGD9AABg/YAAYP4AAGD+wABg/wAAYP9AAGD/gABg/8AAYQBAAGEAwABhAQAAYQFAAGEBwABhAgAAYRHAAGESQABhEsAAYRNAAGEUAABhFEAAYRSAAGEUwABhFQAAYRWAAGEWAABhFkAAYRaAAGEXAABhF0AAYScAAGEngABhKAAAYSiAAGEpQABhKYAAYSnAAGEqAABhKkAAYSrAAGErQABhK4AAYSvAAGEsQABhLIAAYTxAAGE8wABhPUAAYT3AAGE+gABhPsAAYT8AAGE/QABhP4AAYUAAAGFAgABhQMAAYUEAAGFBgABhQcAAYVGAAGFSAABhUoAAYVMAAGFTwABhVAAAYVRAAGFUgABhVMAAYVVAAGFVwABhVgAAYVZAAGFWwABhVwAAYWbAAGFnQABhZ8AAYWhAAGFpAABhaUAAYWmAAGFpwABhagAAYWqAAGFrAABha0AAYWuAAGFsAABhbEAAYXwAAGF8gABhfQAAYX2AAGF+QABhfoAAYX7AAGF/AABhf0AAYX/AAGGAQABhgIAAYYDAAGGBQABhgYAAYZRAAGGdAABhpQAAYa0AAGGtgABhrgAAYa6AAGGvAABhr8AAYbAAAGGwQABhsQAAYbFAAGGxwABhsgAAYbKAAGGzQABhs4AAYbPAAGG0gABhtMAAYbYAAGG5QABhuoAAYbsAAGG7gABhvMAAYb2AAGG+QABhvsAAYcgAAGHRAABh2sAAYePAAGHkgABh5QAAYeWAAGHmAABh5oAAYecAAGHnQABh6AAAYetAAGHvgABh8AAAYfCAAGHxAABh8YAAYfIAAGHygABh8wAAYfOAAGH3wABh+IAAYflAAGH6AABh+sAAYfuAAGH8QABh/QAAYf3AAGH+QABiDgAAYg6AAGIPAABiD4AAYhBAAGIQgABiEMAAYhEAAGIRQABiEcAAYhJAAGISgABiEsAAYhNAAGITgABiI0AAYiPAAGIkQABiJMAAYiWAAGIlwABiJgAAYiZAAGImgABiJwAAYieAAGInwABiKAAAYiiAAGIowABiOIAAYjkAAGI5wABiOkAAYjsAAGI7QABiO4AAYjvAAGI8AABiPIAAYj0AAGI9QABiPYAAYj4AAGI+QABiQYAAYkHAAGJCAABiQoAAYlJAAGJSwABiU0AAYlPAAGJUgABiVMAAYlUAAGJVQABiVYAAYlYAAGJWgABiVsAAYlcAAGJXgABiV8AAYmeAAGJoAABiaIAAYmkAAGJpwABiagAAYmpAAGJqgABiasAAYmtAAGJrwABibAAAYmxAAGJswABibQAAYnzAAGJ9QABifcAAYn5AAGJ/AABif0AAYn+AAGJ/wABigAAAYoCAAGKBAABigUAAYoGAAGKCAABigkAAYpIAAGKSgABikwAAYpOAAGKUQABilIAAYpTAAGKVAABilUAAYpXAAGKWQABiloAAYpbAAGKXQABil4AAYqdAAGKnwABiqEAAYqjAAGKpgABiqcAAYqoAAGKqQABiqoAAYqsAAGKrgABiq8AAYqwAAGKsgABirMAAYrYAAGK/AABiyMAAYtHAAGLSgABi0wAAYtOAAGLUAABi1IAAYtUAAGLVQABi1gAAYtlAAGLdAABi3YAAYt4AAGLegABi3wAAYt+AAGLgAABi4IAAYuRAAGLlAABi5cAAYuaAAGLnQABi6AAAYujAAGLpgABi6gAAYvnAAGL6QABi+sAAYvtAAGL8AABi/EAAYvyAAGL8wABi/QAAYv2AAGL+AABi/kAAYv6AAGL/AABi/0AAYw8AAGMPgABjEAAAYxCAAGMRQABjEYAAYxHAAGMSAABjEkAAYxLAAGMTQABjE4AAYxPAAGMUQABjFIAAYyRAAGMkwABjJUAAYyXAAGMmgABjJsAAYycAAGMnQABjJ4AAYygAAGMogABjKMAAYykAAGMpgABjKcAAYzmAAGM6AABjOoAAYzsAAGM7wABjPAAAYzxAAGM8gABjPMAAYz1AAGM9wABjPgAAYz5AAGM+wABjPwAAY07AAGNPQABjT8AAY1BAAGNRAABjUUAAY1GAAGNRwABjUgAAY1KAAGNTAABjU0AAY1OAAGNUAABjVEAAY2QAAGNkgABjZQAAY2WAAGNmQABjZoAAY2bAAGNnAABjZ0AAY2fAAGNoQABjaIAAY2jAAGNpQABjaYAAY3lAAGN5wABjekAAY3rAAGN7gABje8AAY3wAAGN8QABjfIAAY30AAGN9gABjfcAAY34AAGN+gABjfsAAY5GAAGOaQABjokAAY6pAAGOqwABjq0AAY6vAAGOsQABjrQAAY61AAGOtgABjrkAAY66AAGOvAABjr0AAY6/AAGOwgABjsMAAY7EAAGOxwABjsgAAY7NAAGO2gABjt8AAY7hAAGO4wABjugAAY7rAAGO7gABjvAAAY8VAAGPOQABj2AAAY+EAAGPhwABj4kAAY+LAAGPjQABj48AAY+RAAGPkgABj5UAAY+iAAGPswABj7UAAY+3AAGPuQABj7sAAY+9AAGPvwABj8EAAY/DAAGP1AABj9cAAY/aAAGP3QABj+AAAY/jAAGP5gABj+kAAY/sAAGP7gABkC0AAZAvAAGQMQABkDMAAZA2AAGQNwABkDgAAZA5AAGQOgABkDwAAZA+AAGQPwABkEAAAZBCAAGQQwABkIIAAZCEAAGQhgABkIgAAZCLAAGQjAABkI0AAZCOAAGQjwABkJEAAZCTAAGQlAABkJUAAZCXAAGQmAABkNcAAZDZAAGQ3AABkN4AAZDhAAGQ4gABkOMAAZDkAAGQ5QABkOcAAZDpAAGQ6gABkOsAAZDtAAGQ7gABkPsAAZD8AAGQ/QABkP8AAZE+AAGRQAABkUIAAZFEAAGRRwABkUgAAZFJAAGRSgABkUsAAZFNAAGRTwABkVAAAZFRAAGRUwABkVQAAZGTAAGRlQABkZcAAZGZAAGRnAABkZ0AAZGeAAGRnwABkaAAAZGiAAGRpAABkaUAAZGmAAGRqAABkakAAZHoAAGR6gABkewAAZHuAAGR8QABkfIAAZHzAAGR9AABkfUAAZH3AAGR+QABkfoAAZH7AAGR/QABkf4AAZI9AAGSPwABkkEAAZJDAAGSRgABkkcAAZJIAAGSSQABkkoAAZJMAAGSTgABkk8AAZJQAAGSUgABklMAAZKSAAGSlAABkpYAAZKYAAGSmwABkpwAAZKdAAGSngABkp8AAZKhAAGSowABkqQAAZKlAAGSpwABkqgAAZLNAAGS8QABkxgAAZM8AAGTPwABk0EAAZNDAAGTRQABk0cAAZNJAAGTSgABk00AAZNaAAGTaQABk2sAAZNtAAGTbwABk3EAAZNzAAGTdQABk3cAAZOGAAGTiQABk4wAAZOPAAGTkgABk5UAAZOYAAGTmwABk50AAZPcAAGT3gABk+EAAZPjAAGT5gABk+cAAZPoAAGT6QABk+oAAZPsAAGT7gABk+8AAZPwAAGT8gABk/MAAZQyAAGUNAABlDYAAZQ4AAGUOwABlDwAAZQ9AAGUPgABlD8AAZRBAAGUQwABlEQAAZRFAAGURwABlEgAAZSHAAGUiQABlIsAAZSNAAGUkAABlJEAAZSSAAGUkwABlJQAAZSWAAGUmAABlJkAAZSaAAGUnAABlJ0AAZTcAAGU3gABlOEAAZTjAAGU5gABlOcAAZToAAGU6QABlOoAAZTsAAGU7gABlO8AAZTwAAGU8gABlPMAAZT2AAGVNQABlTcAAZU5AAGVOwABlT4AAZU/AAGVQAABlUEAAZVCAAGVRAABlUYAAZVHAAGVSAABlUoAAZVLAAGVigABlYwAAZWOAAGVkAABlZMAAZWUAAGVlQABlZYAAZWXAAGVmQABlZsAAZWcAAGVnQABlZ8AAZWgAAGV3wABleEAAZXjAAGV5QABlegAAZXpAAGV6gABlesAAZXsAAGV7gABlfAAAZXxAAGV8gABlfQAAZX1AAGWQAABlmMAAZaDAAGWowABlqUAAZanAAGWqQABlqsAAZauAAGWrwABlrAAAZazAAGWtAABlrYAAZa3AAGWuQABlrsAAZa8AAGWvQABlsAAAZbBAAGWygABltcAAZbcAAGW3gABluAAAZblAAGW6AABlusAAZbtAAGXEgABlzYAAZddAAGXgQABl4QAAZeGAAGXiAABl4oAAZeMAAGXjgABl48AAZeSAAGXnwABl7AAAZeyAAGXtAABl7YAAZe4AAGXugABl7wAAZe+AAGXwAABl9EAAZfUAAGX1wABl9oAAZfdAAGX4AABl+MAAZfmAAGX6QABl+sAAZgqAAGYLAABmC4AAZgwAAGYMwABmDQAAZg1AAGYNgABmDcAAZg5AAGYOwABmDwAAZg9AAGYPwABmEAAAZh/AAGYgQABmIMAAZiFAAGYiAABmIkAAZiKAAGYiwABmIwAAZiOAAGYkAABmJEAAZiSAAGYlAABmJUAAZjUAAGY1gABmNkAAZjbAAGY3gABmN8AAZjgAAGY4QABmOIAAZjkAAGY5gABmOcAAZjoAAGY6gABmOsAAZj4AAGY+QABmPoAAZj8AAGZOwABmT0AAZk/AAGZQQABmUQAAZlFAAGZRgABmUcAAZlIAAGZSgABmUwAAZlNAAGZTgABmVAAAZlRAAGZkAABmZIAAZmUAAGZlgABmZkAAZmaAAGZmwABmZwAAZmdAAGZnwABmaEAAZmiAAGZowABmaUAAZmmAAGZ5QABmecAAZnpAAGZ6wABme4AAZnvAAGZ8AABmfEAAZnyAAGZ9AABmfYAAZn3AAGZ+AABmfoAAZn7AAGaOgABmjwAAZo+AAGaQAABmkMAAZpEAAGaRQABmkYAAZpHAAGaSQABmksAAZpMAAGaTQABmk8AAZpQAAGajwABmpEAAZqTAAGalQABmpgAAZqZAAGamgABmpsAAZqcAAGangABmqAAAZqhAAGaogABmqQAAZqlAAGaygABmu4AAZsVAAGbOQABmzwAAZs+AAGbQAABm0IAAZtEAAGbRgABm0cAAZtKAAGbVwABm2YAAZtoAAGbagABm2wAAZtuAAGbcAABm3IAAZt0AAGbgwABm4YAAZuJAAGbjAABm48AAZuSAAGblQABm5gAAZuaAAGb2QABm9sAAZvdAAGb3wABm+IAAZvjAAGb5AABm+UAAZvmAAGb6AABm+oAAZvrAAGb7AABm+4AAZvvAAGcLgABnDAAAZwyAAGcNAABnDcAAZw4AAGcOQABnDoAAZw7AAGcPQABnD8AAZxAAAGcQQABnEMAAZxEAAGcgwABnIUAAZyHAAGciQABnIwAAZyNAAGcjgABnI8AAZyQAAGckgABnJQAAZyVAAGclgABnJgAAZyZAAGc2AABnNoAAZzcAAGc3gABnOEAAZziAAGc4wABnOQAAZzlAAGc5wABnOkAAZzqAAGc6wABnO0AAZzuAAGdLQABnS8AAZ0xAAGdMwABnTYAAZ03AAGdOAABnTkAAZ06AAGdPAABnT4AAZ0/AAGdQAABnUIAAZ1DAAGdggABnYQAAZ2GAAGdiAABnYsAAZ2MAAGdjQABnY4AAZ2PAAGdkQABnZMAAZ2UAAGdlQABnZcAAZ2YAAGd1wABndkAAZ3bAAGd3QABneAAAZ3hAAGd4gABneMAAZ3kAAGd5gABnegAAZ3pAAGd6gABnewAAZ3tAAGeOAABnlsAAZ57AAGemwABnp0AAZ6fAAGeoQABnqMAAZ6mAAGepwABnqgAAZ6rAAGerAABnq4AAZ6vAAGesQABnrQAAZ61AAGetgABnrkAAZ66AAGewwABntAAAZ7VAAGe1wABntkAAZ7eAAGe4QABnuQAAZ7mAAGfCwABny8AAZ9WAAGfegABn30AAZ9/AAGfgQABn4MAAZ+FAAGfhwABn4gAAZ+LAAGfmAABn6kAAZ+rAAGfrQABn68AAZ+xAAGfswABn7UAAZ+3AAGfuQABn8oAAZ/NAAGf0AABn9MAAZ/WAAGf2QABn9wAAZ/fAAGf4gABn+QAAaAjAAGgJQABoCcAAaApAAGgLAABoC0AAaAuAAGgLwABoDAAAaAyAAGgNAABoDUAAaA2AAGgOAABoDkAAaB4AAGgegABoHwAAaB+AAGggQABoIIAAaCDAAGghAABoIUAAaCHAAGgiQABoIoAAaCLAAGgjQABoI4AAaDNAAGgzwABoNIAAaDUAAGg1wABoNgAAaDZAAGg2gABoNsAAaDdAAGg3wABoOAAAaDhAAGg4wABoOQAAaDxAAGg8gABoPMAAaD1AAGhNAABoTYAAaE4AAGhOgABoT0AAaE+AAGhPwABoUAAAaFBAAGhQwABoUUAAaFGAAGhRwABoUkAAaFKAAGhiQABoYsAAaGNAAGhjwABoZIAAaGTAAGhlAABoZUAAaGWAAGhmAABoZoAAaGbAAGhnAABoZ4AAaGfAAGh3gABoeAAAaHiAAGh5AABoecAAaHoAAGh6QABoeoAAaHrAAGh7QABoe8AAaHwAAGh8QABofMAAaH0AAGiMwABojUAAaI3AAGiOQABojwAAaI9AAGiPgABoj8AAaJAAAGiQgABokQAAaJFAAGiRgABokgAAaJJAAGiiAABoooAAaKMAAGijgABopEAAaKSAAGikwABopQAAaKVAAGilwABopkAAaKaAAGimwABop0AAaKeAAGiwwABoucAAaMOAAGjMgABozUAAaM3AAGjOQABozsAAaM9AAGjPwABo0AAAaNDAAGjUAABo18AAaNhAAGjYwABo2UAAaNnAAGjaQABo2sAAaNtAAGjfAABo38AAaOCAAGjhQABo4gAAaOLAAGjjgABo5EAAaOTAAGj0gABo9QAAaPWAAGj2AABo9sAAaPcAAGj3QABo94AAaPfAAGj4QABo+MAAaPkAAGj5QABo+cAAaPoAAGkJwABpCkAAaQrAAGkLQABpDAAAaQxAAGkMgABpDMAAaQ0AAGkNgABpDgAAaQ5AAGkOgABpDwAAaQ9AAGkfAABpH4AAaSAAAGkggABpIUAAaSGAAGkhwABpIgAAaSJAAGkiwABpI0AAaSOAAGkjwABpJEAAaSSAAGk0QABpNMAAaTVAAGk1wABpNoAAaTbAAGk3AABpN0AAaTeAAGk4AABpOIAAaTjAAGk5AABpOYAAaTnAAGlJgABpSgAAaUqAAGlLAABpS8AAaUwAAGlMQABpTIAAaUzAAGlNQABpTcAAaU4AAGlOQABpTsAAaU8AAGlewABpX0AAaV/AAGlgQABpYQAAaWFAAGlhgABpYcAAaWIAAGligABpYwAAaWNAAGljgABpZAAAaWRAAGl0AABpdIAAaXUAAGl1gABpdkAAaXaAAGl2wABpdwAAaXdAAGl3wABpeEAAaXiAAGl4wABpeUAAaXmAAGl7wABpfAAAaXyAAGmNQABplkAAaZ9AAGmoAABpscAAabnAAGnDgABpzUAAadVAAGneQABp50AAaefAAGnogABp6QAAaemAAGnqAABp6sAAaetAAGnrwABp7EAAaezAAGntQABp7cAAae6AAGnvQABp74AAafDAAGn0AABp9MAAafVAAGn2AABp9sAAafdAAGoAgABqCYAAahNAAGocQABqHQAAah2AAGoeAABqHoAAah8AAGofgABqH8AAaiCAAGojwABqKIAAaikAAGopgABqKgAAaiqAAGorAABqK4AAaiwAAGosgABqLQAAajHAAGoygABqM0AAajQAAGo0wABqNYAAajZAAGo3AABqN8AAajiAAGo5AABqSMAAaklAAGpKAABqSoAAaktAAGpLgABqS8AAakwAAGpMQABqTMAAak1AAGpNgABqTcAAak5AAGpOgABqUMAAalEAAGpRgABqYUAAamHAAGpiQABqYsAAamOAAGpjwABqZAAAamRAAGpkgABqZQAAamWAAGplwABqZgAAamaAAGpmwABqdoAAancAAGp3wABqeEAAankAAGp5QABqeYAAannAAGp6AABqeoAAansAAGp7QABqe4AAanwAAGp8QABqfoAAan7AAGp/QABqjwAAao+AAGqQAABqkIAAapFAAGqRgABqkcAAapIAAGqSQABqksAAapNAAGqTgABqk8AAapRAAGqUgABqpEAAaqTAAGqlgABqpgAAaqbAAGqnAABqp0AAaqeAAGqnwABqqEAAaqjAAGqpAABqqUAAaqnAAGqqAABqrEAAaqyAAGqtAABqvMAAar1AAGq9wABqvkAAar8AAGq/QABqv4AAar/AAGrAAABqwIAAasEAAGrBQABqwYAAasIAAGrCQABq0gAAatKAAGrTQABq08AAatSAAGrUwABq1QAAatVAAGrVgABq1gAAataAAGrWwABq1wAAateAAGrXwABq2wAAattAAGrbgABq3AAAauvAAGrsQABq7MAAau1AAGruAABq7kAAau6AAGruwABq7wAAau+AAGrwAABq8EAAavCAAGrxAABq8UAAawEAAGsBgABrAkAAawLAAGsDgABrA8AAawQAAGsEQABrBIAAawUAAGsFgABrBcAAawYAAGsGgABrBsAAawnAAGsNAABrDkAAaw8AAGsPwABrEQAAaxHAAGsSgABrEwAAaxSAAGsXQABrKgAAazLAAGs6wABrQsAAa0NAAGtDwABrREAAa0TAAGtFgABrRcAAa0YAAGtGwABrRwAAa0eAAGtHwABrSEAAa0kAAGtJQABrSYAAa0pAAGtKgABrS8AAa08AAGtQQABrUMAAa1FAAGtSgABrU0AAa1QAAGtUgABrXcAAa2bAAGtwgABreYAAa3pAAGt6wABre0AAa3vAAGt8QABrfMAAa30AAGt9wABrgQAAa4VAAGuFwABrhkAAa4bAAGuHQABrh8AAa4hAAGuIwABriUAAa42AAGuOQABrjwAAa4/AAGuQgABrkUAAa5IAAGuSwABrk4AAa5QAAGujwABrpEAAa6TAAGulQABrpgAAa6ZAAGumgABrpsAAa6cAAGungABrqAAAa6hAAGuogABrqQAAa6lAAGu5AABruYAAa7oAAGu6gABru0AAa7uAAGu7wABrvAAAa7xAAGu8wABrvUAAa72AAGu9wABrvkAAa76AAGvOQABrzsAAa8+AAGvQAABr0MAAa9EAAGvRQABr0YAAa9HAAGvSQABr0sAAa9MAAGvTQABr08AAa9QAAGvXQABr14AAa9fAAGvYQABr6AAAa+iAAGvpAABr6YAAa+pAAGvqgABr6sAAa+sAAGvrQABr68AAa+xAAGvsgABr7MAAa+1AAGvtgABr/UAAa/3AAGv+QABr/sAAa/+AAGv/wABsAAAAbABAAGwAgABsAQAAbAGAAGwBwABsAgAAbAKAAGwCwABsEoAAbBMAAGwTgABsFAAAbBTAAGwVAABsFUAAbBWAAGwVwABsFkAAbBbAAGwXAABsF0AAbBfAAGwYAABsJ8AAbChAAGwowABsKUAAbCoAAGwqQABsKoAAbCrAAGwrAABsK4AAbCwAAGwsQABsLIAAbC0AAGwtQABsPQAAbD2AAGw+AABsPoAAbD9AAGw/gABsP8AAbEAAAGxAQABsQMAAbEFAAGxBgABsQcAAbEJAAGxCgABsS8AAbFTAAGxegABsZ4AAbGhAAGxowABsaUAAbGnAAGxqQABsasAAbGsAAGxrwABsbwAAbHLAAGxzQABsc8AAbHRAAGx0wABsdUAAbHXAAGx2QABsegAAbHrAAGx7gABsfEAAbH0AAGx9wABsfoAAbH9AAGx/wABsj4AAbJAAAGyQgABskQAAbJHAAGySAABskkAAbJKAAGySwABsk0AAbJPAAGyUAABslEAAbJTAAGyVAABspMAAbKVAAGylwABspkAAbKcAAGynQABsp4AAbKfAAGyoAABsqIAAbKkAAGypQABsqYAAbKoAAGyqQABsugAAbLqAAGy7AABsu4AAbLxAAGy8gABsvMAAbL0AAGy9QABsvcAAbL5AAGy+gABsvsAAbL9AAGy/gABsz0AAbM/AAGzQQABs0MAAbNGAAGzRwABs0gAAbNJAAGzSgABs0wAAbNOAAGzTwABs1AAAbNSAAGzUwABs5IAAbOUAAGzlgABs5gAAbObAAGznAABs50AAbOeAAGznwABs6EAAbOjAAGzpAABs6UAAbOnAAGzqAABs+cAAbPpAAGz6wABs+0AAbPwAAGz8QABs/IAAbPzAAGz9AABs/YAAbP4AAGz+QABs/oAAbP8AAGz/QABtDwAAbQ+AAG0QAABtEIAAbRFAAG0RgABtEcAAbRIAAG0SQABtEsAAbRNAAG0TgABtE8AAbRRAAG0UgABtJ0AAbTAAAG04AABtQAAAbUCAAG1BAABtQYAAbUIAAG1CwABtQwAAbUNAAG1EAABtREAAbUTAAG1FAABtRYAAbUZAAG1GgABtRsAAbUeAAG1HwABtSgAAbU1AAG1OgABtTwAAbU+AAG1QwABtUYAAbVJAAG1SwABtXAAAbWUAAG1uwABtd8AAbXiAAG15AABteYAAbXoAAG16gABtewAAbXtAAG18AABtf0AAbYOAAG2EAABthIAAbYUAAG2FgABthgAAbYaAAG2HAABth4AAbYvAAG2MgABtjUAAbY4AAG2OwABtj4AAbZBAAG2RAABtkcAAbZJAAG2iAABtooAAbaMAAG2jgABtpEAAbaSAAG2kwABtpQAAbaVAAG2lwABtpkAAbaaAAG2mwABtp0AAbaeAAG23QABtt8AAbbhAAG24wABtuYAAbbnAAG26AABtukAAbbqAAG27AABtu4AAbbvAAG28AABtvIAAbbzAAG3MgABtzQAAbc3AAG3OQABtzwAAbc9AAG3PgABtz8AAbdAAAG3QgABt0QAAbdFAAG3RgABt0gAAbdJAAG3VgABt1cAAbdYAAG3WgABt5kAAbebAAG3nQABt58AAbeiAAG3owABt6QAAbelAAG3pgABt6gAAbeqAAG3qwABt6wAAbeuAAG3rwABt+4AAbfwAAG38gABt/QAAbf3AAG3+AABt/kAAbf6AAG3+wABt/0AAbf/AAG4AAABuAEAAbgDAAG4BAABuEMAAbhFAAG4RwABuEkAAbhMAAG4TQABuE4AAbhPAAG4UAABuFIAAbhUAAG4VQABuFYAAbhYAAG4WQABuJgAAbiaAAG4nAABuJ4AAbihAAG4ogABuKMAAbikAAG4pQABuKcAAbipAAG4qgABuKsAAbitAAG4rgABuO0AAbjvAAG48QABuPMAAbj2AAG49wABuPgAAbj5AAG4+gABuPwAAbj+AAG4/wABuQAAAbkCAAG5AwABuSgAAblMAAG5cwABuZcAAbmaAAG5nAABuZ4AAbmgAAG5ogABuaQAAbmlAAG5qAABubUAAbnEAAG5xgABucgAAbnKAAG5zAABuc4AAbnQAAG50gABueEAAbnkAAG55wABueoAAbntAAG58AABufMAAbn2AAG5+AABujcAAbo5AAG6OwABuj0AAbpAAAG6QQABukIAAbpDAAG6RAABukYAAbpIAAG6SQABukoAAbpMAAG6TQABuowAAbqOAAG6kAABupIAAbqVAAG6lgABupcAAbqYAAG6mQABupsAAbqdAAG6ngABup8AAbqhAAG6ogABuuEAAbrjAAG65QABuucAAbrqAAG66wABuuwAAbrtAAG67gABuvAAAbryAAG68wABuvQAAbr2AAG69wABuzYAAbs4AAG7OgABuzwAAbs/AAG7QAABu0EAAbtCAAG7QwABu0UAAbtHAAG7SAABu0kAAbtLAAG7TAABu4sAAbuNAAG7jwABu5EAAbuUAAG7lQABu5YAAbuXAAG7mAABu5oAAbucAAG7nQABu54AAbugAAG7oQABu+AAAbviAAG75AABu+YAAbvpAAG76gABu+sAAbvsAAG77QABu+8AAbvxAAG78gABu/MAAbv1AAG79gABvDUAAbw3AAG8OQABvDsAAbw+AAG8PwABvEAAAbxBAAG8QgABvEQAAbxGAAG8RwABvEgAAbxKAAG8SwABvFQAAbxVAAG8VwABvJoAAby+AAG84gABvQUAAb0sAAG9TAABvXMAAb2aAAG9ugABvd4AAb4CAAG+BAABvgcAAb4JAAG+CwABvg0AAb4QAAG+EgABvhQAAb4WAAG+GAABvhoAAb4cAAG+HwABviIAAb4jAAG+KAABvjUAAb44AAG+OgABvj0AAb5AAAG+QgABvmcAAb6LAAG+sgABvtYAAb7ZAAG+2wABvt0AAb7fAAG+4QABvuMAAb7kAAG+5wABvvQAAb8HAAG/CQABvwsAAb8NAAG/DwABvxEAAb8TAAG/FQABvxcAAb8ZAAG/LAABvy8AAb8yAAG/NQABvzgAAb87AAG/PgABv0EAAb9EAAG/RwABv0kAAb+IAAG/igABv40AAb+PAAG/kgABv5MAAb+UAAG/lQABv5YAAb+YAAG/mgABv5sAAb+cAAG/ngABv58AAb+oAAG/qQABv6sAAb/qAAG/7AABv+4AAb/wAAG/8wABv/QAAb/1AAG/9gABv/cAAb/5AAG/+wABv/wAAb/9AAG//wABwAAAAcA/AAHAQQABwEQAAcBGAAHASQABwEoAAcBLAAHATAABwE0AAcBPAAHAUQABwFIAAcBTAAHAVQABwFYAAcBfAAHAYAABwGIAAcChAAHAowABwKUAAcCnAAHAqgABwKsAAcCsAAHArQABwK4AAcCwAAHAsgABwLMAAcC0AAHAtgABwLcAAcD2AAHA+AABwPsAAcD9AAHBAAABwQEAAcECAAHBAwABwQQAAcEGAAHBCAABwQkAAcEKAAHBDAABwQ0AAcEWAAHBFwABwRkAAcFYAAHBWgABwVwAAcFeAAHBYQABwWIAAcFjAAHBZAABwWUAAcFnAAHBaQABwWoAAcFrAAHBbQABwW4AAcGtAAHBrwABwbIAAcG0AAHBtwABwbgAAcG5AAHBugABwbsAAcG9AAHBvwABwcAAAcHBAAHBwwABwcQAAcHRAAHB0gABwdMAAcHVAAHCFAABwhYAAcIYAAHCGgABwh0AAcIeAAHCHwABwiAAAcIhAAHCIwABwiUAAcImAAHCJwABwikAAcIqAAHCaQABwmsAAcJuAAHCcAABwnMAAcJ0AAHCdQABwnYAAcJ3AAHCeQABwnsAAcJ8AAHCfQABwn8AAcKAAAHCmAABwqUAAcKuAAHCsQABwrQAAcK2AAHCuQABwsIAAcLFAAHCyAABwssAAcLOAAHC0AABwtkAAcLjAAHC6wABwzYAAcNZAAHDeQABw5kAAcObAAHDnQABw58AAcOhAAHDpAABw6UAAcOmAAHDqQABw6oAAcOsAAHDrQABw68AAcOyAAHDswABw7QAAcO3AAHDuAABw70AAcPKAAHDzwABw9EAAcPTAAHD2AABw9sAAcPeAAHD4AABxAUAAcQpAAHEUAABxHQAAcR3AAHEeQABxHsAAcR9AAHEfwABxIEAAcSCAAHEhQABxJIAAcSjAAHEpQABxKcAAcSpAAHEqwABxK0AAcSvAAHEsQABxLMAAcTEAAHExwABxMoAAcTNAAHE0AABxNMAAcTWAAHE2QABxNwAAcTeAAHFHQABxR8AAcUhAAHFIwABxSYAAcUnAAHFKAABxSkAAcUqAAHFLAABxS4AAcUvAAHFMAABxTIAAcUzAAHFcgABxXQAAcV2AAHFeAABxXsAAcV8AAHFfQABxX4AAcV/AAHFgQABxYMAAcWEAAHFhQABxYcAAcWIAAHFxwABxckAAcXMAAHFzgABxdEAAcXSAAHF0wABxdQAAcXVAAHF1wABxdkAAcXaAAHF2wABxd0AAcXeAAHF6wABxewAAcXtAAHF7wABxi4AAcYwAAHGMgABxjQAAcY3AAHGOAABxjkAAcY6AAHGOwABxj0AAcY/AAHGQAABxkEAAcZDAAHGRAABxoMAAcaFAAHGhwABxokAAcaMAAHGjQABxo4AAcaPAAHGkAABxpIAAcaUAAHGlQABxpYAAcaYAAHGmQABxtgAAcbaAAHG3AABxt4AAcbhAAHG4gABxuMAAcbkAAHG5QABxucAAcbpAAHG6gABxusAAcbtAAHG7gABxy0AAccvAAHHMQABxzMAAcc2AAHHNwABxzgAAcc5AAHHOgABxzwAAcc+AAHHPwABx0AAAcdCAAHHQwABx4IAAceEAAHHhgABx4gAAceLAAHHjAABx40AAceOAAHHjwABx5EAAceTAAHHlAABx5UAAceXAAHHmAABx70AAcfhAAHICAAByCwAAcgvAAHIMQAByDMAAcg1AAHINwAByDkAAcg6AAHIPQAByEoAAchZAAHIWwAByF0AAchfAAHIYQAByGMAAchlAAHIZwAByHYAAch5AAHIfAAByH8AAciCAAHIhQAByIgAAciLAAHIjQAByMwAAcjOAAHI0AAByNIAAcjVAAHI1gAByNcAAcjYAAHI2QAByNsAAcjdAAHI3gAByN8AAcjhAAHI4gABySEAAckjAAHJJQAByScAAckqAAHJKwABySwAAcktAAHJLgAByTAAAckyAAHJMwAByTQAAck2AAHJNwAByXYAAcl4AAHJegAByXwAAcl/AAHJgAAByYEAAcmCAAHJgwAByYUAAcmHAAHJiAAByYkAAcmLAAHJjAABycsAAcnNAAHJzwABydEAAcnUAAHJ1QABydYAAcnXAAHJ2AABydoAAcncAAHJ3QAByd4AAcngAAHJ4QAByiAAAcoiAAHKJAAByiYAAcopAAHKKgAByisAAcosAAHKLQAByi8AAcoxAAHKMgAByjMAAco1AAHKNgABynUAAcp3AAHKeQABynsAAcp+AAHKfwAByoAAAcqBAAHKggAByoQAAcqGAAHKhwAByogAAcqKAAHKiwABysoAAcrMAAHKzgABytAAAcrTAAHK1AABytUAAcrWAAHK1wABytkAAcrbAAHK3AAByt0AAcrfAAHK4AAByysAActOAAHLbgABy44AAcuQAAHLkgABy5QAAcuWAAHLmQABy5oAAcubAAHLngABy58AAcuhAAHLogABy6QAAcunAAHLqAABy6kAAcusAAHLrQABy7IAAcu/AAHLxAABy8YAAcvIAAHLzQABy9AAAcvTAAHL1QABy/oAAcweAAHMRQABzGkAAcxsAAHMbgABzHAAAcxyAAHMdAABzHYAAcx3AAHMegABzIcAAcyYAAHMmgABzJwAAcyeAAHMoAABzKIAAcykAAHMpgABzKgAAcy5AAHMvAABzL8AAczCAAHMxQABzMgAAczLAAHMzgABzNEAAczTAAHNEgABzRQAAc0WAAHNGAABzRsAAc0cAAHNHQABzR4AAc0fAAHNIQABzSMAAc0kAAHNJQABzScAAc0oAAHNZwABzWkAAc1rAAHNbQABzXAAAc1xAAHNcgABzXMAAc10AAHNdgABzXgAAc15AAHNegABzXwAAc19AAHNvAABzb4AAc3BAAHNwwABzcYAAc3HAAHNyAABzckAAc3KAAHNzAABzc4AAc3PAAHN0AABzdIAAc3TAAHN4AABzeEAAc3iAAHN5AABziMAAc4lAAHOJwABzikAAc4sAAHOLQABzi4AAc4vAAHOMAABzjIAAc40AAHONQABzjYAAc44AAHOOQABzngAAc56AAHOfAABzn4AAc6BAAHOggABzoMAAc6EAAHOhQABzocAAc6JAAHOigABzosAAc6NAAHOjgABzs0AAc7PAAHO0QABztMAAc7WAAHO1wABztgAAc7ZAAHO2gABztwAAc7eAAHO3wABzuAAAc7iAAHO4wABzyIAAc8kAAHPJgABzygAAc8rAAHPLAABzy0AAc8uAAHPLwABzzEAAc8zAAHPNAABzzUAAc83AAHPOAABz3cAAc95AAHPewABz30AAc+AAAHPgQABz4IAAc+DAAHPhAABz4YAAc+IAAHPiQABz4oAAc+MAAHPjQABz7IAAc/WAAHP/QAB0CEAAdAkAAHQJgAB0CgAAdAqAAHQLAAB0C4AAdAvAAHQMgAB0D8AAdBOAAHQUAAB0FIAAdBUAAHQVgAB0FgAAdBaAAHQXAAB0GsAAdBuAAHQcQAB0HQAAdB3AAHQegAB0H0AAdCAAAHQggAB0MEAAdDDAAHQxgAB0MgAAdDLAAHQzAAB0M0AAdDOAAHQzwAB0NEAAdDTAAHQ1AAB0NUAAdDXAAHQ2AAB0RcAAdEZAAHRGwAB0R0AAdEgAAHRIQAB0SIAAdEjAAHRJAAB0SYAAdEoAAHRKQAB0SoAAdEsAAHRLQAB0WwAAdFuAAHRcAAB0XIAAdF1AAHRdgAB0XcAAdF4AAHReQAB0XsAAdF9AAHRfgAB0X8AAdGBAAHRggAB0cEAAdHDAAHRxgAB0cgAAdHLAAHRzAAB0c0AAdHOAAHRzwAB0dEAAdHTAAHR1AAB0dUAAdHXAAHR2AAB0doAAdIZAAHSGwAB0h0AAdIfAAHSIgAB0iMAAdIkAAHSJQAB0iYAAdIoAAHSKgAB0isAAdIsAAHSLgAB0i8AAdJuAAHScAAB0nIAAdJ0AAHSdwAB0ngAAdJ5AAHSegAB0nsAAdJ9AAHSfwAB0oAAAdKBAAHSgwAB0oQAAdLDAAHSxQAB0scAAdLJAAHSzAAB0s0AAdLOAAHSzwAB0tAAAdLSAAHS1AAB0tUAAdLWAAHS2AAB0tkAAdMkAAHTRwAB02cAAdOHAAHTiQAB04sAAdONAAHTjwAB05IAAdOTAAHTlAAB05cAAdOYAAHTmgAB05sAAdOdAAHTnwAB06AAAdOhAAHTpAAB06UAAdOqAAHTtwAB07wAAdO+AAHTwAAB08UAAdPIAAHTywAB080AAdPyAAHUFgAB1D0AAdRhAAHUZAAB1GYAAdRoAAHUagAB1GwAAdRuAAHUbwAB1HIAAdR/AAHUkAAB1JIAAdSUAAHUlgAB1JgAAdSaAAHUnAAB1J4AAdSgAAHUsQAB1LQAAdS3AAHUugAB1L0AAdTAAAHUwwAB1MYAAdTJAAHUywAB1QoAAdUMAAHVDgAB1RAAAdUTAAHVFAAB1RUAAdUWAAHVFwAB1RkAAdUbAAHVHAAB1R0AAdUfAAHVIAAB1V8AAdVhAAHVYwAB1WUAAdVoAAHVaQAB1WoAAdVrAAHVbAAB1W4AAdVwAAHVcQAB1XIAAdV0AAHVdQAB1bQAAdW2AAHVuQAB1bsAAdW+AAHVvwAB1cAAAdXBAAHVwgAB1cQAAdXGAAHVxwAB1cgAAdXKAAHVywAB1dgAAdXZAAHV2gAB1dwAAdYbAAHWHQAB1h8AAdYhAAHWJAAB1iUAAdYmAAHWJwAB1igAAdYqAAHWLAAB1i0AAdYuAAHWMAAB1jEAAdZwAAHWcgAB1nQAAdZ2AAHWeQAB1noAAdZ7AAHWfAAB1n0AAdZ/AAHWgQAB1oIAAdaDAAHWhQAB1oYAAdbFAAHWxwAB1skAAdbLAAHWzgAB1s8AAdbQAAHW0QAB1tIAAdbUAAHW1gAB1tcAAdbYAAHW2gAB1tsAAdcaAAHXHAAB1x4AAdcgAAHXIwAB1yQAAdclAAHXJgAB1ycAAdcpAAHXKwAB1ywAAdctAAHXLwAB1zAAAddvAAHXcQAB13MAAdd1AAHXeAAB13kAAdd6AAHXewAB13wAAdd+AAHXgAAB14EAAdeCAAHXhAAB14UAAdeqAAHXzgAB1/UAAdgZAAHYHAAB2B4AAdggAAHYIgAB2CQAAdgmAAHYJwAB2CoAAdg3AAHYRgAB2EgAAdhKAAHYTAAB2E4AAdhQAAHYUgAB2FQAAdhjAAHYZgAB2GkAAdhsAAHYbwAB2HIAAdh1AAHYeAAB2HoAAdi5AAHYuwAB2L0AAdi/AAHYwgAB2MMAAdjEAAHYxQAB2MYAAdjIAAHYygAB2MsAAdjMAAHYzgAB2M8AAdkOAAHZEAAB2RIAAdkUAAHZFwAB2RgAAdkZAAHZGgAB2RsAAdkdAAHZHwAB2SAAAdkhAAHZIwAB2SQAAdljAAHZZQAB2WcAAdlpAAHZbAAB2W0AAdluAAHZbwAB2XAAAdlyAAHZdAAB2XUAAdl2AAHZeAAB2XkAAdm4AAHZugAB2bwAAdm+AAHZwQAB2cIAAdnDAAHZxAAB2cUAAdnHAAHZyQAB2coAAdnLAAHZzQAB2c4AAdoNAAHaDwAB2hEAAdoTAAHaFgAB2hcAAdoYAAHaGQAB2hoAAdocAAHaHgAB2h8AAdogAAHaIgAB2iMAAdpiAAHaZAAB2mYAAdpoAAHaawAB2mwAAdptAAHabgAB2m8AAdpxAAHacwAB2nQAAdp1AAHadwAB2ngAAdq3AAHauQAB2rsAAdq9AAHawAAB2sEAAdrCAAHawwAB2sQAAdrGAAHayAAB2skAAdrKAAHazAAB2s0AAdsYAAHbOwAB21sAAdt7AAHbfQAB238AAduBAAHbgwAB24YAAduHAAHbiAAB24sAAduMAAHbjgAB248AAduRAAHblAAB25UAAduWAAHbmQAB25oAAdufAAHbrAAB27EAAduzAAHbtQAB27oAAdu9AAHbwAAB28IAAdvnAAHcCwAB3DIAAdxWAAHcWQAB3FsAAdxdAAHcXwAB3GEAAdxjAAHcZAAB3GcAAdx0AAHchQAB3IcAAdyJAAHciwAB3I0AAdyPAAHckQAB3JMAAdyVAAHcpgAB3KkAAdysAAHcrwAB3LIAAdy1AAHcuAAB3LsAAdy+AAHcwAAB3P8AAd0BAAHdAwAB3QUAAd0IAAHdCQAB3QoAAd0LAAHdDAAB3Q4AAd0QAAHdEQAB3RIAAd0UAAHdFQAB3VQAAd1WAAHdWAAB3VoAAd1dAAHdXgAB3V8AAd1gAAHdYQAB3WMAAd1lAAHdZgAB3WcAAd1pAAHdagAB3akAAd2rAAHdrgAB3bAAAd2zAAHdtAAB3bUAAd22AAHdtwAB3bkAAd27AAHdvAAB3b0AAd2/AAHdwAAB3c0AAd3OAAHdzwAB3dEAAd4QAAHeEgAB3hQAAd4WAAHeGQAB3hoAAd4bAAHeHAAB3h0AAd4fAAHeIQAB3iIAAd4jAAHeJQAB3iYAAd5lAAHeZwAB3mkAAd5rAAHebgAB3m8AAd5wAAHecQAB3nIAAd50AAHedgAB3ncAAd54AAHeegAB3nsAAd66AAHevAAB3r4AAd7AAAHewwAB3sQAAd7FAAHexgAB3scAAd7JAAHeywAB3swAAd7NAAHezwAB3tAAAd8PAAHfEQAB3xMAAd8VAAHfGAAB3xkAAd8aAAHfGwAB3xwAAd8eAAHfIAAB3yEAAd8iAAHfJAAB3yUAAd9kAAHfZgAB32gAAd9qAAHfbQAB324AAd9vAAHfcAAB33EAAd9zAAHfdQAB33YAAd93AAHfeQAB33oAAd+fAAHfwwAB3+oAAeAOAAHgEQAB4BMAAeAVAAHgFwAB4BkAAeAbAAHgHAAB4B8AAeAsAAHgOwAB4D0AAeA/AAHgQQAB4EMAAeBFAAHgRwAB4EkAAeBYAAHgWwAB4F4AAeBhAAHgZAAB4GcAAeBqAAHgbQAB4G8AAeCuAAHgsAAB4LMAAeC1AAHguAAB4LkAAeC6AAHguwAB4LwAAeC+AAHgwAAB4MEAAeDCAAHgxAAB4MUAAeEEAAHhBgAB4QgAAeEKAAHhDQAB4Q4AAeEPAAHhEAAB4REAAeETAAHhFQAB4RYAAeEXAAHhGQAB4RoAAeFZAAHhWwAB4V0AAeFfAAHhYgAB4WMAAeFkAAHhZQAB4WYAAeFoAAHhagAB4WsAAeFsAAHhbgAB4W8AAeGuAAHhsAAB4bMAAeG1AAHhuAAB4bkAAeG6AAHhuwAB4bwAAeG+AAHhwAAB4cEAAeHCAAHhxAAB4cUAAeIEAAHiBgAB4ggAAeIKAAHiDQAB4g4AAeIPAAHiEAAB4hEAAeITAAHiFQAB4hYAAeIXAAHiGQAB4hoAAeJZAAHiWwAB4l0AAeJfAAHiYgAB4mMAAeJkAAHiZQAB4mYAAeJoAAHiagAB4msAAeJsAAHibgAB4m8AAeKuAAHisAAB4rIAAeK0AAHitwAB4rgAAeK5AAHiugAB4rsAAeK9AAHivwAB4sAAAeLBAAHiwwAB4sQAAeLNAAHizgAB4tAAAeMTAAHjNwAB41sAAeN+AAHjpQAB48UAAePsAAHkEwAB5DMAAeRXAAHkewAB5H0AAeSAAAHkggAB5IQAAeSGAAHkiQAB5IsAAeSNAAHkjwAB5JEAAeSTAAHklQAB5JgAAeSbAAHknAAB5KEAAeSuAAHksQAB5LMAAeS2AAHkuQAB5LsAAeTgAAHlBAAB5SsAAeVPAAHlUgAB5VQAAeVWAAHlWAAB5VoAAeVcAAHlXQAB5WAAAeVtAAHlgAAB5YIAAeWEAAHlhgAB5YgAAeWKAAHljAAB5Y4AAeWQAAHlkgAB5aUAAeWoAAHlqwAB5a4AAeWxAAHltAAB5bcAAeW6AAHlvQAB5cAAAeXCAAHmAQAB5gMAAeYGAAHmCAAB5gsAAeYMAAHmDQAB5g4AAeYPAAHmEQAB5hMAAeYUAAHmFQAB5hcAAeYYAAHmIQAB5iIAAeYkAAHmYwAB5mUAAeZnAAHmaQAB5mwAAeZtAAHmbgAB5m8AAeZwAAHmcgAB5nQAAeZ1AAHmdgAB5ngAAeZ5AAHmuAAB5roAAea9AAHmvwAB5sIAAebDAAHmxAAB5sUAAebGAAHmyAAB5soAAebLAAHmzAAB5s4AAebPAAHm2AAB5tkAAebbAAHnGgAB5xwAAeceAAHnIAAB5yMAAeckAAHnJQAB5yYAAecnAAHnKQAB5ysAAecsAAHnLQAB5y8AAecwAAHnbwAB53EAAed0AAHndgAB53kAAed6AAHnewAB53wAAed9AAHnfwAB54EAAeeCAAHngwAB54UAAeeGAAHnjwAB55AAAeeSAAHn0QAB59MAAefVAAHn1wAB59oAAefbAAHn3AAB590AAefeAAHn4AAB5+IAAefjAAHn5AAB5+YAAefnAAHoJgAB6CgAAegrAAHoLQAB6DAAAegxAAHoMgAB6DMAAeg0AAHoNgAB6DgAAeg5AAHoOgAB6DwAAeg9AAHoSgAB6EsAAehMAAHoTgAB6I0AAeiPAAHokQAB6JMAAeiWAAHolwAB6JgAAeiZAAHomgAB6JwAAeieAAHonwAB6KAAAeiiAAHoowAB6OIAAejkAAHo5wAB6OkAAejsAAHo7QAB6O4AAejvAAHo8AAB6PIAAej0AAHo9QAB6PYAAej4AAHo+QAB6RQAAekhAAHpOAAB6TsAAek+AAHpQQAB6UQAAelHAAHpSgAB6U0AAelPAAHpUgAB6VUAAelYAAHpbwAB6XIAAel1AAHpeAAB6XsAAel+AAHpgQAB6YQAAemHAAHpigAB6Y0AAemQAAHpkgAB6ZkAAemgAAHpqQAB6a4AAem5AAHpxQAB6c4AAenSAAHp3AAB6eQAAeovAAHqUgAB6nIAAeqSAAHqlAAB6pYAAeqYAAHqmgAB6p0AAeqeAAHqnwAB6qIAAeqjAAHqpQAB6qYAAeqoAAHqqwAB6qwAAeqtAAHqsAAB6rEAAeq2AAHqwwAB6sgAAerKAAHqzAAB6tEAAerUAAHq1wAB6tkAAer+AAHrIgAB60kAAettAAHrcAAB63IAAet0AAHrdgAB63gAAet6AAHrewAB634AAeuLAAHrnAAB654AAeugAAHrogAB66QAAeumAAHrqAAB66oAAeusAAHrvQAB68AAAevDAAHrxgAB68kAAevMAAHrzwAB69IAAevVAAHr1wAB7BYAAewYAAHsGgAB7BwAAewfAAHsIAAB7CEAAewiAAHsIwAB7CUAAewnAAHsKAAB7CkAAewrAAHsLAAB7GsAAextAAHsbwAB7HEAAex0AAHsdQAB7HYAAex3AAHseAAB7HoAAex8AAHsfQAB7H4AAeyAAAHsgQAB7MAAAezCAAHsxQAB7McAAezKAAHsywAB7MwAAezNAAHszgAB7NAAAezSAAHs0wAB7NQAAezWAAHs1wAB7OQAAezlAAHs5gAB7OgAAe0nAAHtKQAB7SsAAe0tAAHtMAAB7TEAAe0yAAHtMwAB7TQAAe02AAHtOAAB7TkAAe06AAHtPAAB7T0AAe18AAHtfgAB7YAAAe2CAAHthQAB7YYAAe2HAAHtiAAB7YkAAe2LAAHtjQAB7Y4AAe2PAAHtkQAB7ZIAAe3RAAHt0wAB7dUAAe3XAAHt2gAB7dsAAe3cAAHt3QAB7d4AAe3gAAHt4gAB7eMAAe3kAAHt5gAB7ecAAe4mAAHuKAAB7ioAAe4sAAHuLwAB7jAAAe4xAAHuMgAB7jMAAe41AAHuNwAB7jgAAe45AAHuOwAB7jwAAe57AAHufQAB7n8AAe6BAAHuhAAB7oUAAe6GAAHuhwAB7ogAAe6KAAHujAAB7o0AAe6OAAHukAAB7pEAAe62AAHu2gAB7wEAAe8lAAHvKAAB7yoAAe8sAAHvLgAB7zAAAe8yAAHvMwAB7zYAAe9DAAHvUgAB71QAAe9WAAHvWAAB71oAAe9cAAHvXgAB72AAAe9vAAHvcgAB73UAAe94AAHvewAB734AAe+BAAHvhAAB74YAAe/FAAHvxwAB78kAAe/LAAHvzgAB788AAe/QAAHv0QAB79IAAe/UAAHv1gAB79cAAe/YAAHv2gAB79sAAfAaAAHwHAAB8B4AAfAgAAHwIwAB8CQAAfAlAAHwJgAB8CcAAfApAAHwKwAB8CwAAfAtAAHwLwAB8DAAAfBvAAHwcQAB8HMAAfB1AAHweAAB8HkAAfB6AAHwewAB8HwAAfB+AAHwgAAB8IEAAfCCAAHwhAAB8IUAAfDEAAHwxgAB8MgAAfDKAAHwzQAB8M4AAfDPAAHw0AAB8NEAAfDTAAHw1QAB8NYAAfDXAAHw2QAB8NoAAfEZAAHxGwAB8R0AAfEfAAHxIgAB8SMAAfEkAAHxJQAB8SYAAfEoAAHxKgAB8SsAAfEsAAHxLgAB8S8AAfFuAAHxcAAB8XIAAfF0AAHxdwAB8XgAAfF5AAHxegAB8XsAAfF9AAHxfwAB8YAAAfGBAAHxgwAB8YQAAfHDAAHxxQAB8ccAAfHJAAHxzAAB8c0AAfHOAAHxzwAB8dAAAfHSAAHx1AAB8dUAAfHWAAHx2AAB8dkAAfIkAAHyRwAB8mcAAfKHAAHyiQAB8osAAfKNAAHyjwAB8pIAAfKTAAHylAAB8pcAAfKYAAHymgAB8psAAfKdAAHyoAAB8qEAAfKiAAHypQAB8qYAAfKrAAHyuAAB8r0AAfK/AAHywQAB8sYAAfLJAAHyzAAB8s4AAfLzAAHzFwAB8z4AAfNiAAHzZQAB82cAAfNpAAHzawAB820AAfNvAAHzcAAB83MAAfOAAAHzkQAB85MAAfOVAAHzlwAB85kAAfObAAHznQAB858AAfOhAAHzsgAB87UAAfO4AAHzuwAB874AAfPBAAHzxAAB88cAAfPKAAHzzAAB9AsAAfQNAAH0DwAB9BEAAfQUAAH0FQAB9BYAAfQXAAH0GAAB9BoAAfQcAAH0HQAB9B4AAfQgAAH0IQAB9GAAAfRiAAH0ZAAB9GYAAfRpAAH0agAB9GsAAfRsAAH0bQAB9G8AAfRxAAH0cgAB9HMAAfR1AAH0dgAB9LUAAfS3AAH0ugAB9LwAAfS/AAH0wAAB9MEAAfTCAAH0wwAB9MUAAfTHAAH0yAAB9MkAAfTLAAH0zAAB9NkAAfTaAAH02wAB9N0AAfUcAAH1HgAB9SAAAfUiAAH1JQAB9SYAAfUnAAH1KAAB9SkAAfUrAAH1LQAB9S4AAfUvAAH1MQAB9TIAAfVxAAH1cwAB9XUAAfV3AAH1egAB9XsAAfV8AAH1fQAB9X4AAfWAAAH1ggAB9YMAAfWEAAH1hgAB9YcAAfXGAAH1yAAB9coAAfXMAAH1zwAB9dAAAfXRAAH10gAB9dMAAfXVAAH11wAB9dgAAfXZAAH12wAB9dwAAfYbAAH2HQAB9h8AAfYhAAH2JAAB9iUAAfYmAAH2JwAB9igAAfYqAAH2LAAB9i0AAfYuAAH2MAAB9jEAAfZwAAH2cgAB9nQAAfZ2AAH2eQAB9noAAfZ7AAH2fAAB9n0AAfZ/AAH2gQAB9oIAAfaDAAH2hQAB9oYAAfarAAH2zwAB9vYAAfcaAAH3HQAB9x8AAfchAAH3IwAB9yUAAfcnAAH3KAAB9ysAAfc4AAH3RwAB90kAAfdLAAH3TQAB908AAfdRAAH3UwAB91UAAfdkAAH3ZwAB92oAAfdtAAH3cAAB93MAAfd2AAH3eQAB93sAAfe6AAH3vAAB978AAffBAAH3xAAB98UAAffGAAH3xwAB98gAAffKAAH3zAAB980AAffOAAH30AAB99EAAfgQAAH4EgAB+BQAAfgWAAH4GQAB+BoAAfgbAAH4HAAB+B0AAfgfAAH4IQAB+CIAAfgjAAH4JQAB+CYAAfhlAAH4ZwAB+GkAAfhrAAH4bgAB+G8AAfhwAAH4cQAB+HIAAfh0AAH4dgAB+HcAAfh4AAH4egAB+HsAAfi6AAH4vAAB+L8AAfjBAAH4xAAB+MUAAfjGAAH4xwAB+MgAAfjKAAH4zAAB+M0AAfjOAAH40AAB+NEAAfkQAAH5EgAB+RQAAfkWAAH5GQAB+RoAAfkbAAH5HAAB+R0AAfkfAAH5IQAB+SIAAfkjAAH5JQAB+SYAAfllAAH5ZwAB+WkAAflrAAH5bgAB+W8AAflwAAH5cQAB+XIAAfl0AAH5dgAB+XcAAfl4AAH5egAB+XsAAfm6AAH5vAAB+b4AAfnAAAH5wwAB+cQAAfnFAAH5xgAB+ccAAfnJAAH5ywAB+cwAAfnNAAH5zwAB+dAAAfobAAH6PgAB+l4AAfp+AAH6gAAB+oIAAfqEAAH6hgAB+okAAfqKAAH6iwAB+o4AAfqPAAH6kQAB+pIAAfqUAAH6lwAB+pgAAfqZAAH6nAAB+p0AAfqiAAH6rwAB+rQAAfq2AAH6uAAB+r0AAfrAAAH6wwAB+sUAAfrqAAH7DgAB+zUAAftZAAH7XAAB+14AAftgAAH7YgAB+2QAAftmAAH7ZwAB+2oAAft3AAH7iAAB+4oAAfuMAAH7jgAB+5AAAfuSAAH7lAAB+5YAAfuYAAH7qQAB+6wAAfuvAAH7sgAB+7UAAfu4AAH7uwAB+74AAfvBAAH7wwAB/AIAAfwEAAH8BgAB/AgAAfwLAAH8DAAB/A0AAfwOAAH8DwAB/BEAAfwTAAH8FAAB/BUAAfwXAAH8GAAB/FcAAfxZAAH8WwAB/F0AAfxgAAH8YQAB/GIAAfxjAAH8ZAAB/GYAAfxoAAH8aQAB/GoAAfxsAAH8bQAB/KwAAfyuAAH8sQAB/LMAAfy2AAH8twAB/LgAAfy5AAH8ugAB/LwAAfy+AAH8vwAB/MAAAfzCAAH8wwAB/NAAAfzRAAH80gAB/NQAAf0TAAH9FQAB/RcAAf0ZAAH9HAAB/R0AAf0eAAH9HwAB/SAAAf0iAAH9JAAB/SUAAf0mAAH9KAAB/SkAAf1oAAH9agAB/WwAAf1uAAH9cQAB/XIAAf1zAAH9dAAB/XUAAf13AAH9eQAB/XoAAf17AAH9fQAB/X4AAf29AAH9vwAB/cEAAf3DAAH9xgAB/ccAAf3IAAH9yQAB/coAAf3MAAH9zgAB/c8AAf3QAAH90gAB/dMAAf4SAAH+FAAB/hYAAf4YAAH+GwAB/hwAAf4dAAH+HgAB/h8AAf4hAAH+IwAB/iQAAf4lAAH+JwAB/igAAf5nAAH+aQAB/msAAf5tAAH+cAAB/nEAAf5yAAH+cwAB/nQAAf52AAH+eAAB/nkAAf56AAH+fAAB/n0AAf6iAAH+xgAB/u0AAf8RAAH/FAAB/xYAAf8YAAH/GgAB/xwAAf8eAAH/HwAB/yIAAf8vAAH/PgAB/0AAAf9CAAH/RAAB/0YAAf9IAAH/SgAB/0wAAf9bAAH/XgAB/2EAAf9kAAH/ZwAB/2oAAf9tAAH/cAAB/3IAAf+xAAH/swAB/7UAAf+3AAH/ugAB/7sAAf+8AAH/vQAB/74AAf/AAAH/wgAB/8MAAf/EAAH/xgAB/8cAAgAGAAIACAACAAoAAgAMAAIADwACABAAAgARAAIAEgACABMAAgAVAAIAFwACABgAAgAZAAIAGwACABwAAgBbAAIAXQACAF8AAgBhAAIAZAACAGUAAgBmAAIAZwACAGgAAgBqAAIAbAACAG0AAgBuAAIAcAACAHEAAgCwAAIAsgACALQAAgC2AAIAuQACALoAAgC7AAIAvAACAL0AAgC/AAIAwQACAMIAAgDDAAIAxQACAMYAAgEFAAIBBwACAQkAAgELAAIBDgACAQ8AAgEQAAIBEQACARIAAgEUAAIBFgACARcAAgEYAAIBGgACARsAAgFaAAIBXAACAV4AAgFgAAIBYwACAWQAAgFlAAIBZgACAWcAAgFpAAIBawACAWwAAgFtAAIBbwACAXAAAgGvAAIBsQACAbMAAgG1AAIBuAACAbkAAgG6AAIBuwACAbwAAgG+AAIBwAACAcEAAgHCAAIBxAACAcUAAgIQAAICMwACAlMAAgJzAAICdQACAncAAgJ5AAICewACAn4AAgJ/AAICgAACAoMAAgKEAAIChgACAocAAgKJAAICjAACAo0AAgKOAAICkQACApIAAgKbAAICqAACAq0AAgKvAAICsQACArYAAgK5AAICvAACAr4AAgLjAAIDBwACAy4AAgNSAAIDVQACA1cAAgNZAAIDWwACA10AAgNfAAIDYAACA2MAAgNwAAIDgQACA4MAAgOFAAIDhwACA4kAAgOLAAIDjQACA48AAgORAAIDogACA6UAAgOoAAIDqwACA64AAgOxAAIDtAACA7cAAgO6AAIDvAACA/sAAgP9AAID/wACBAEAAgQEAAIEBQACBAYAAgQHAAIECAACBAoAAgQMAAIEDQACBA4AAgQQAAIEEQACBFAAAgRSAAIEVAACBFYAAgRZAAIEWgACBFsAAgRcAAIEXQACBF8AAgRhAAIEYgACBGMAAgRlAAIEZgACBKUAAgSnAAIEqgACBKwAAgSvAAIEsAACBLEAAgSyAAIEswACBLUAAgS3AAIEuAACBLkAAgS7AAIEvAACBMkAAgTKAAIEywACBM0AAgUMAAIFDgACBRAAAgUSAAIFFQACBRYAAgUXAAIFGAACBRkAAgUbAAIFHQACBR4AAgUfAAIFIQACBSIAAgVhAAIFYwACBWUAAgVnAAIFagACBWsAAgVsAAIFbQACBW4AAgVwAAIFcgACBXMAAgV0AAIFdgACBXcAAgW2AAIFuAACBboAAgW8AAIFvwACBcAAAgXBAAIFwgACBcMAAgXFAAIFxwACBcgAAgXJAAIFywACBcwAAgYLAAIGDQACBg8AAgYRAAIGFAACBhUAAgYWAAIGFwACBhgAAgYaAAIGHAACBh0AAgYeAAIGIAACBiEAAgZgAAIGYgACBmQAAgZmAAIGaQACBmoAAgZrAAIGbAACBm0AAgZvAAIGcQACBnIAAgZzAAIGdQACBnYAAgabAAIGvwACBuYAAgcKAAIHDQACBw8AAgcRAAIHEwACBxUAAgcXAAIHGAACBxsAAgcoAAIHNwACBzkAAgc7AAIHPQACBz8AAgdBAAIHQwACB0UAAgdUAAIHVwACB1oAAgddAAIHYAACB2MAAgdmAAIHaQACB2sAAgeqAAIHrAACB64AAgewAAIHswACB7QAAge1AAIHtgACB7cAAge5AAIHuwACB7wAAge9AAIHvwACB8AAAgf/AAIIAQACCAMAAggFAAIICAACCAkAAggKAAIICwACCAwAAggOAAIIEAACCBEAAggSAAIIFAACCBUAAghUAAIIVgACCFgAAghaAAIIXQACCF4AAghfAAIIYAACCGEAAghjAAIIZQACCGYAAghnAAIIaQACCGoAAgipAAIIqwACCK0AAgivAAIIsgACCLMAAgi0AAIItQACCLYAAgi4AAIIugACCLsAAgi8AAIIvgACCL8AAgj+AAIJAAACCQIAAgkEAAIJBwACCQgAAgkJAAIJCgACCQsAAgkNAAIJDwACCRAAAgkRAAIJEwACCRQAAglTAAIJVQACCVcAAglZAAIJXAACCV0AAgleAAIJXwACCWAAAgliAAIJZAACCWUAAglmAAIJaAACCWkAAgmoAAIJqgACCawAAgmuAAIJsQACCbIAAgmzAAIJtAACCbUAAgm3AAIJuQACCboAAgm7AAIJvQACCb4AAgoJAAIKLAACCkwAAgpsAAIKbgACCnAAAgpyAAIKdAACCncAAgp4AAIKeQACCnwAAgp9AAIKfwACCoAAAgqCAAIKhQACCoYAAgqHAAIKigACCosAAgqQAAIKnQACCqIAAgqkAAIKpgACCqsAAgquAAIKsQACCrMAAgrYAAIK/AACCyMAAgtHAAILSgACC0wAAgtOAAILUAACC1IAAgtUAAILVQACC1gAAgtlAAILdgACC3gAAgt6AAILfAACC34AAguAAAILggACC4QAAguGAAILlwACC5oAAgudAAILoAACC6MAAgumAAILqQACC6wAAguvAAILsQACC/AAAgvyAAIL9AACC/YAAgv5AAIL+gACC/sAAgv8AAIL/QACC/8AAgwBAAIMAgACDAMAAgwFAAIMBgACDEUAAgxHAAIMSQACDEsAAgxOAAIMTwACDFAAAgxRAAIMUgACDFQAAgxWAAIMVwACDFgAAgxaAAIMWwACDJoAAgycAAIMnwACDKEAAgykAAIMpQACDKYAAgynAAIMqAACDKoAAgysAAIMrQACDK4AAgywAAIMsQACDL4AAgy/AAIMwAACDMIAAg0BAAINAwACDQUAAg0HAAINCgACDQsAAg0MAAINDQACDQ4AAg0QAAINEgACDRMAAg0UAAINFgACDRcAAg1WAAINWAACDVoAAg1cAAINXwACDWAAAg1hAAINYgACDWMAAg1lAAINZwACDWgAAg1pAAINawACDWwAAg2rAAINrQACDa8AAg2xAAINtAACDbUAAg22AAINtwACDbgAAg26AAINvAACDb0AAg2+AAINwAACDcEAAg4AAAIOAgACDgQAAg4GAAIOCQACDgoAAg4LAAIODAACDg0AAg4PAAIOEQACDhIAAg4TAAIOFQACDhYAAg5VAAIOVwACDlkAAg5bAAIOXgACDl8AAg5gAAIOYQACDmIAAg5kAAIOZgACDmcAAg5oAAIOagACDmsAAg6QAAIOtAACDtsAAg7/AAIPAgACDwQAAg8GAAIPCAACDwoAAg8MAAIPDQACDxAAAg8dAAIPLAACDy4AAg8wAAIPMgACDzQAAg82AAIPOAACDzoAAg9JAAIPTAACD08AAg9SAAIPVQACD1gAAg9bAAIPXgACD2AAAg+fAAIPoQACD6MAAg+lAAIPqAACD6kAAg+qAAIPqwACD6wAAg+uAAIPsAACD7EAAg+yAAIPtAACD7UAAg/0AAIP9gACD/gAAg/6AAIP/QACD/4AAg//AAIQAAACEAEAAhADAAIQBQACEAYAAhAHAAIQCQACEAoAAhBJAAIQSwACEE0AAhBPAAIQUgACEFMAAhBUAAIQVQACEFYAAhBYAAIQWgACEFsAAhBcAAIQXgACEF8AAhCeAAIQoAACEKIAAhCkAAIQpwACEKgAAhCpAAIQqgACEKsAAhCtAAIQrwACELAAAhCxAAIQswACELQAAhDzAAIQ9QACEPcAAhD5AAIQ/AACEP0AAhD+AAIQ/wACEQAAAhECAAIRBAACEQUAAhEGAAIRCAACEQkAAhFIAAIRSgACEUwAAhFOAAIRUQACEVIAAhFTAAIRVAACEVUAAhFXAAIRWQACEVoAAhFbAAIRXQACEV4AAhGdAAIRnwACEaEAAhGjAAIRpgACEacAAhGoAAIRqQACEaoAAhGsAAIRrgACEa8AAhGwAAIRsgACEbMAAhH+AAISIQACEkEAAhJhAAISYwACEmUAAhJnAAISaQACEmwAAhJtAAISbgACEnEAAhJyAAISdAACEnUAAhJ3AAISegACEnsAAhJ8AAISfwACEoAAAhKFAAISkgACEpcAAhKZAAISmwACEqAAAhKjAAISpgACEqgAAhLNAAIS8QACExgAAhM8AAITPwACE0EAAhNDAAITRQACE0cAAhNJAAITSgACE00AAhNaAAITawACE20AAhNvAAITcQACE3MAAhN1AAITdwACE3kAAhN7AAITjAACE48AAhOSAAITlQACE5gAAhObAAITngACE6EAAhOkAAITpgACE+UAAhPnAAIT6QACE+sAAhPuAAIT7wACE/AAAhPxAAIT8gACE/QAAhP2AAIT9wACE/gAAhP6AAIT+wACFDoAAhQ8AAIUPgACFEAAAhRDAAIURAACFEUAAhRGAAIURwACFEkAAhRLAAIUTAACFE0AAhRPAAIUUAACFI8AAhSRAAIUlAACFJYAAhSZAAIUmgACFJsAAhScAAIUnQACFJ8AAhShAAIUogACFKMAAhSlAAIUpgACFLMAAhS0AAIUtQACFLcAAhT2AAIU+AACFPoAAhT8AAIU/wACFQAAAhUBAAIVAgACFQMAAhUFAAIVBwACFQgAAhUJAAIVCwACFQwAAhVLAAIVTQACFU8AAhVRAAIVVAACFVUAAhVWAAIVVwACFVgAAhVaAAIVXAACFV0AAhVeAAIVYAACFWEAAhWgAAIVogACFaQAAhWmAAIVqQACFaoAAhWrAAIVrAACFa0AAhWvAAIVsQACFbIAAhWzAAIVtQACFbYAAhX1AAIV9wACFfkAAhX7AAIV/gACFf8AAhYAAAIWAQACFgIAAhYEAAIWBgACFgcAAhYIAAIWCgACFgsAAhZKAAIWTAACFk4AAhZQAAIWUwACFlQAAhZVAAIWVgACFlcAAhZZAAIWWwACFlwAAhZdAAIWXwACFmAAAhaFAAIWqQACFtAAAhb0AAIW9wACFvkAAhb7AAIW/QACFv8AAhcBAAIXAgACFwUAAhcSAAIXIQACFyMAAhclAAIXJwACFykAAhcrAAIXLQACFy8AAhc+AAIXQQACF0QAAhdHAAIXSgACF00AAhdQAAIXUwACF1UAAheUAAIXlgACF5gAAheaAAIXnQACF54AAhefAAIXoAACF6EAAhejAAIXpQACF6YAAhenAAIXqQACF6oAAhfpAAIX6wACF+0AAhfvAAIX8gACF/MAAhf0AAIX9QACF/YAAhf4AAIX+gACF/sAAhf8AAIX/gACF/8AAhg+AAIYQAACGEIAAhhEAAIYRwACGEgAAhhJAAIYSgACGEsAAhhNAAIYTwACGFAAAhhRAAIYUwACGFQAAhiTAAIYlQACGJgAAhiaAAIYnQACGJ4AAhifAAIYoAACGKEAAhijAAIYpQACGKYAAhinAAIYqQACGKoAAhjpAAIY6wACGO0AAhjvAAIY8gACGPMAAhj0AAIY9QACGPYAAhj4AAIY+gACGPsAAhj8AAIY/gACGP8AAhk+AAIZQAACGUIAAhlEAAIZRwACGUgAAhlJAAIZSgACGUsAAhlNAAIZTwACGVAAAhlRAAIZUwACGVQAAhmTAAIZlQACGZcAAhmZAAIZnAACGZ0AAhmeAAIZnwACGaAAAhmiAAIZpAACGaUAAhmmAAIZqAACGakAAhn0AAIaFwACGjcAAhpXAAIaWQACGlsAAhpdAAIaXwACGmIAAhpjAAIaZAACGmcAAhpoAAIaagACGmsAAhptAAIacAACGnEAAhpyAAIadQACGnYAAhp7AAIaiAACGo0AAhqPAAIakQACGpYAAhqZAAIanAACGp4AAhrDAAIa5wACGw4AAhsyAAIbNQACGzcAAhs5AAIbOwACGz0AAhs/AAIbQAACG0MAAhtQAAIbYQACG2MAAhtlAAIbZwACG2kAAhtrAAIbbQACG28AAhtxAAIbggACG4UAAhuIAAIbiwACG44AAhuRAAIblAACG5cAAhuaAAIbnAACG9sAAhvdAAIb3wACG+EAAhvkAAIb5QACG+YAAhvnAAIb6AACG+oAAhvsAAIb7QACG+4AAhvwAAIb8QACHDAAAhwyAAIcNAACHDYAAhw5AAIcOgACHDsAAhw8AAIcPQACHD8AAhxBAAIcQgACHEMAAhxFAAIcRgACHIUAAhyHAAIcigACHIwAAhyPAAIckAACHJEAAhySAAIckwACHJUAAhyXAAIcmAACHJkAAhybAAIcnAACHKkAAhyqAAIcqwACHK0AAhzsAAIc7gACHPAAAhzyAAIc9QACHPYAAhz3AAIc+AACHPkAAhz7AAIc/QACHP4AAhz/AAIdAQACHQIAAh1BAAIdQwACHUUAAh1HAAIdSgACHUsAAh1MAAIdTQACHU4AAh1QAAIdUgACHVMAAh1UAAIdVgACHVcAAh2WAAIdmAACHZoAAh2cAAIdnwACHaAAAh2hAAIdogACHaMAAh2lAAIdpwACHagAAh2pAAIdqwACHawAAh3rAAId7QACHe8AAh3xAAId9AACHfUAAh32AAId9wACHfgAAh36AAId/AACHf0AAh3+AAIeAAACHgEAAh5AAAIeQgACHkQAAh5GAAIeSQACHkoAAh5LAAIeTAACHk0AAh5PAAIeUQACHlIAAh5TAAIeVQACHlYAAh57AAIenwACHsYAAh7qAAIe7QACHu8AAh7xAAIe8wACHvUAAh73AAIe+AACHvsAAh8IAAIfFwACHxkAAh8bAAIfHQACHx8AAh8hAAIfIwACHyUAAh80AAIfNwACHzoAAh89AAIfQAACH0MAAh9GAAIfSQACH0sAAh+KAAIfjAACH44AAh+QAAIfkwACH5QAAh+VAAIflgACH5cAAh+ZAAIfmwACH5wAAh+dAAIfnwACH6AAAh/fAAIf4QACH+MAAh/lAAIf6AACH+kAAh/qAAIf6wACH+wAAh/uAAIf8AACH/EAAh/yAAIf9AACH/UAAiA0AAIgNgACIDgAAiA6AAIgPQACID4AAiA/AAIgQAACIEEAAiBDAAIgRQACIEYAAiBHAAIgSQACIEoAAiCJAAIgiwACII0AAiCPAAIgkgACIJMAAiCUAAIglQACIJYAAiCYAAIgmgACIJsAAiCcAAIgngACIJ8AAiDeAAIg4AACIOIAAiDkAAIg5wACIOgAAiDpAAIg6gACIOsAAiDtAAIg7wACIPAAAiDxAAIg8wACIPQAAiEzAAIhNQACITcAAiE5AAIhPAACIT0AAiE+AAIhPwACIUAAAiFCAAIhRAACIUUAAiFGAAIhSAACIUkAAiGIAAIhigACIYwAAiGOAAIhkQACIZIAAiGTAAIhlAACIZUAAiGXAAIhmQACIZoAAiGbAAIhnQACIZ4AAiHpAAIiDAACIiwAAiJMAAIiTgACIlAAAiJSAAIiVAACIlcAAiJYAAIiWQACIlwAAiJdAAIiXwACImAAAiJiAAIiZAACImUAAiJmAAIiaQACImoAAiJzAAIigAACIoUAAiKHAAIiiQACIo4AAiKRAAIilAACIpYAAiK7AAIi3wACIwYAAiMqAAIjLQACIy8AAiMxAAIjMwACIzUAAiM3AAIjOAACIzsAAiNIAAIjWQACI1sAAiNdAAIjXwACI2EAAiNjAAIjZQACI2cAAiNpAAIjegACI30AAiOAAAIjgwACI4YAAiOJAAIjjAACI48AAiOSAAIjlAACI9MAAiPVAAIj1wACI9kAAiPcAAIj3QACI94AAiPfAAIj4AACI+IAAiPkAAIj5QACI+YAAiPoAAIj6QACJCgAAiQqAAIkLAACJC4AAiQxAAIkMgACJDMAAiQ0AAIkNQACJDcAAiQ5AAIkOgACJDsAAiQ9AAIkPgACJH0AAiR/AAIkggACJIQAAiSHAAIkiAACJIkAAiSKAAIkiwACJI0AAiSPAAIkkAACJJEAAiSTAAIklAACJKEAAiSiAAIkowACJKUAAiTkAAIk5gACJOgAAiTqAAIk7QACJO4AAiTvAAIk8AACJPEAAiTzAAIk9QACJPYAAiT3AAIk+QACJPoAAiU5AAIlOwACJT0AAiU/AAIlQgACJUMAAiVEAAIlRQACJUYAAiVIAAIlSgACJUsAAiVMAAIlTgACJU8AAiWOAAIlkAACJZIAAiWUAAIllwACJZgAAiWZAAIlmgACJZsAAiWdAAIlnwACJaAAAiWhAAIlowACJaQAAiXjAAIl5QACJecAAiXpAAIl7AACJe0AAiXuAAIl7wACJfAAAiXyAAIl9AACJfUAAiX2AAIl+AACJfkAAiY4AAImOgACJjwAAiY+AAImQQACJkIAAiZDAAImRAACJkUAAiZHAAImSQACJkoAAiZLAAImTQACJk4AAiZzAAImlwACJr4AAibiAAIm5QACJucAAibpAAIm6wACJu0AAibvAAIm8AACJvMAAicAAAInDwACJxEAAicTAAInFQACJxcAAicZAAInGwACJx0AAicsAAInLwACJzIAAic1AAInOAACJzsAAic+AAInQQACJ0MAAieCAAInhAACJ4YAAieIAAIniwACJ4wAAieNAAInjgACJ48AAieRAAInkwACJ5QAAieVAAInlwACJ5gAAifXAAIn2QACJ9sAAifdAAIn4AACJ+EAAifiAAIn4wACJ+QAAifmAAIn6AACJ+kAAifqAAIn7AACJ+0AAigsAAIoLgACKDAAAigyAAIoNQACKDYAAig3AAIoOAACKDkAAig7AAIoPQACKD4AAig/AAIoQQACKEIAAiiBAAIogwACKIUAAiiHAAIoigACKIsAAiiMAAIojQACKI4AAiiQAAIokgACKJMAAiiUAAIolgACKJcAAijWAAIo2AACKNoAAijcAAIo3wACKOAAAijhAAIo4gACKOMAAijlAAIo5wACKOgAAijpAAIo6wACKOwAAikrAAIpLQACKS8AAikxAAIpNAACKTUAAik2AAIpNwACKTgAAik6AAIpPAACKT0AAik+AAIpQAACKUEAAimAAAIpggACKYQAAimGAAIpiQACKYoAAimLAAIpjAACKY0AAimPAAIpkQACKZIAAimTAAIplQACKZYAAinhAAIqBAACKiQAAipEAAIqRgACKkgAAipKAAIqTAACKk8AAipQAAIqUQACKlQAAipVAAIqVwACKlgAAipaAAIqXQACKl4AAipfAAIqYgACKmMAAipoAAIqdQACKnoAAip8AAIqfgACKoMAAiqGAAIqiQACKosAAiqwAAIq1AACKvsAAisfAAIrIgACKyQAAismAAIrKAACKyoAAissAAIrLQACKzAAAis9AAIrTgACK1AAAitSAAIrVAACK1YAAitYAAIrWgACK1wAAiteAAIrbwACK3IAAit1AAIreAACK3sAAit+AAIrgQACK4QAAiuHAAIriQACK8gAAivKAAIrzAACK84AAivRAAIr0gACK9MAAivUAAIr1QACK9cAAivZAAIr2gACK9sAAivdAAIr3gACLB0AAiwfAAIsIQACLCMAAiwmAAIsJwACLCgAAiwpAAIsKgACLCwAAiwuAAIsLwACLDAAAiwyAAIsMwACLHIAAix0AAIsdwACLHkAAix8AAIsfQACLH4AAix/AAIsgAACLIIAAiyEAAIshQACLIYAAiyIAAIsiQACLJYAAiyXAAIsmAACLJoAAizZAAIs2wACLN0AAizfAAIs4gACLOMAAizkAAIs5QACLOYAAizoAAIs6gACLOsAAizsAAIs7gACLO8AAi0uAAItMAACLTIAAi00AAItNwACLTgAAi05AAItOgACLTsAAi09AAItPwACLUAAAi1BAAItQwACLUQAAi2DAAIthQACLYcAAi2JAAItjAACLY0AAi2OAAItjwACLZAAAi2SAAItlAACLZUAAi2WAAItmAACLZkAAi3YAAIt2gACLdwAAi3eAAIt4QACLeIAAi3jAAIt5AACLeUAAi3nAAIt6QACLeoAAi3rAAIt7QACLe4AAi4tAAIuLwACLjEAAi4zAAIuNgACLjcAAi44AAIuOQACLjoAAi48AAIuPgACLj8AAi5AAAIuQgACLkMAAi5oAAIujAACLrMAAi7XAAIu2gACLtwAAi7eAAIu4AACLuIAAi7kAAIu5QACLugAAi71AAIvBAACLwYAAi8IAAIvCgACLwwAAi8OAAIvEAACLxIAAi8hAAIvJAACLycAAi8qAAIvLQACLzAAAi8zAAIvNgACLzgAAi93AAIveQACL3sAAi99AAIvgAACL4EAAi+CAAIvgwACL4QAAi+GAAIviAACL4kAAi+KAAIvjAACL40AAi/MAAIvzgACL9AAAi/SAAIv1QACL9YAAi/XAAIv2AACL9kAAi/bAAIv3QACL94AAi/fAAIv4QACL+IAAjAhAAIwIwACMCUAAjAnAAIwKgACMCsAAjAsAAIwLQACMC4AAjAwAAIwMgACMDMAAjA0AAIwNgACMDcAAjB2AAIweAACMHoAAjB8AAIwfwACMIAAAjCBAAIwggACMIMAAjCFAAIwhwACMIgAAjCJAAIwiwACMIwAAjDLAAIwzQACMM8AAjDRAAIw1AACMNUAAjDWAAIw1wACMNgAAjDaAAIw3AACMN0AAjDeAAIw4AACMOEAAjEgAAIxIgACMSQAAjEmAAIxKQACMSoAAjErAAIxLAACMS0AAjEvAAIxMQACMTIAAjEzAAIxNQACMTYAAjF1AAIxdwACMXkAAjF7AAIxfgACMX8AAjGAAAIxgQACMYIAAjGEAAIxhgACMYcAAjGIAAIxigACMYsAAjHWAAIx+QACMhkAAjI5AAIyOwACMj0AAjI/AAIyQQACMkQAAjJFAAIyRgACMkkAAjJKAAIyTAACMk0AAjJPAAIyUgACMlMAAjJUAAIyVwACMlgAAjJhAAIybgACMnMAAjJ1AAIydwACMnwAAjJ/AAIyggACMoQAAjKpAAIyzQACMvQAAjMYAAIzGwACMx0AAjMfAAIzIQACMyMAAjMlAAIzJgACMykAAjM2AAIzRwACM0kAAjNLAAIzTQACM08AAjNRAAIzUwACM1UAAjNXAAIzaAACM2sAAjNuAAIzcQACM3QAAjN3AAIzegACM30AAjOAAAIzggACM8EAAjPDAAIzxQACM8cAAjPKAAIzywACM8wAAjPNAAIzzgACM9AAAjPSAAIz0wACM9QAAjPWAAIz1wACNBYAAjQYAAI0GgACNBwAAjQfAAI0IAACNCEAAjQiAAI0IwACNCUAAjQnAAI0KAACNCkAAjQrAAI0LAACNGsAAjRtAAI0cAACNHIAAjR1AAI0dgACNHcAAjR4AAI0eQACNHsAAjR9AAI0fgACNH8AAjSBAAI0ggACNI8AAjSQAAI0kQACNJMAAjTSAAI01AACNNYAAjTYAAI02wACNNwAAjTdAAI03gACNN8AAjThAAI04wACNOQAAjTlAAI05wACNOgAAjUnAAI1KQACNSsAAjUtAAI1MAACNTEAAjUyAAI1MwACNTQAAjU2AAI1OAACNTkAAjU6AAI1PAACNT0AAjV8AAI1fgACNYAAAjWCAAI1hQACNYYAAjWHAAI1iAACNYkAAjWLAAI1jQACNY4AAjWPAAI1kQACNZIAAjXRAAI10wACNdUAAjXXAAI12gACNdsAAjXcAAI13QACNd4AAjXgAAI14gACNeMAAjXkAAI15gACNecAAjYmAAI2KAACNioAAjYsAAI2LwACNjAAAjYxAAI2MgACNjMAAjY1AAI2NwACNjgAAjY5AAI2OwACNjwAAjZhAAI2hQACNqwAAjbQAAI20wACNtUAAjbXAAI22QACNtsAAjbdAAI23gACNuEAAjbuAAI2/QACNv8AAjcBAAI3AwACNwUAAjcHAAI3CQACNwsAAjcaAAI3HQACNyAAAjcjAAI3JgACNykAAjcsAAI3LwACNzEAAjdwAAI3cgACN3UAAjd3AAI3egACN3sAAjd8AAI3fQACN34AAjeAAAI3ggACN4MAAjeEAAI3hgACN4cAAjfGAAI3yAACN8oAAjfMAAI3zwACN9AAAjfRAAI30gACN9MAAjfVAAI31wACN9gAAjfZAAI32wACN9wAAjgbAAI4HQACOB8AAjghAAI4JAACOCUAAjgmAAI4JwACOCgAAjgqAAI4LAACOC0AAjguAAI4MAACODEAAjhwAAI4cgACOHUAAjh3AAI4egACOHsAAjh8AAI4fQACOH4AAjiAAAI4ggACOIMAAjiEAAI4hgACOIcAAjjGAAI4yAACOMoAAjjMAAI4zwACONAAAjjRAAI40gACONMAAjjVAAI41wACONgAAjjZAAI42wACONwAAjkbAAI5HQACOR8AAjkhAAI5JAACOSUAAjkmAAI5JwACOSgAAjkqAAI5LAACOS0AAjkuAAI5MAACOTEAAjlwAAI5cgACOXQAAjl2AAI5eQACOXoAAjl7AAI5fAACOX0AAjl/AAI5gQACOYIAAjmDAAI5hQACOYYAAjnRAAI59AACOhQAAjo0AAI6NgACOjgAAjo6AAI6PAACOj8AAjpAAAI6QQACOkQAAjpFAAI6RwACOkgAAjpKAAI6TQACOk4AAjpPAAI6UgACOlMAAjpcAAI6aQACOm4AAjpwAAI6cgACOncAAjp6AAI6fQACOn8AAjqkAAI6yAACOu8AAjsTAAI7FgACOxgAAjsaAAI7HAACOx4A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AkY+AAJGPwACRkEAAkZDAAJGRAACRkUAAkZHAAJGSAACRlUAAkZWAAJGVwACRlkAAkaYAAJGmgACRpwAAkaeAAJGoQACRqIAAkajAAJGpAACRqUAAkanAAJGqQACRqoAAkarAAJGrQACRq4AAkbtAAJG7wACRvIAAkb0AAJG9wACRvgAAkb5AAJG+gACRvsAAkb9AAJG/wACRwAAAkcBAAJHAwACRwQAAkcRAAJHHgACRx8AAkcgAAJHIgACR2UAAkeJAAJHrQACR9AAAkf3AAJIFwACSD4AAkhlAAJIhQACSKkAAkjNAAJIzwACSNIAAkjUAAJI1gACSNgAAkjbAAJI3QACSN8AAkjhAAJI4wACSOUAAkjnAAJI6gACSO0AAkjuAAJI8wACSQAAAkkDAAJJBQACSQgAAkkLAAJJDQACSTIAAklWAAJJfQACSaEAAkmkAAJJpgACSagAAkmqAAJJrAACSa4AAkmvAAJJsgACSb8AAknSAAJJ1AACSdYAAknYAAJJ2gACSdwAAkneAAJJ4AACSeIAAknkAAJJ9wACSfoAAkn9AAJKAAACSgMAAkoGAAJKCQACSgwAAkoPAAJKEgACShQAAkpTAAJKVQACSlgAAkpaAAJKXQACSl4AAkpfAAJKYAACSmEAAkpjAAJKZQACSmYAAkpnAAJKaQACSmoAAkpzAAJKdAACSnYAAkq1AAJKtwACSrkAAkq7AAJKvgACSr8AAkrAAAJKwQACSsIAAkrEAAJKxgACSscAAkrIAAJKygACSssAAksKAAJLDAACSw8AAksRAAJLFAACSxUAAksWAAJLFwACSxgAAksaAAJLHAACSx0AAkseAAJLIAACSyEAAksqAAJLKwACSy0AAktsAAJLbgACS3AAAktyAAJLdQACS3YAAkt3AAJLeAACS3kAAkt7AAJLfQACS34AAkt/AAJLgQACS4IAAkvBAAJLwwACS8YAAkvIAAJLywACS8wAAkvNAAJLzgACS88AAkvRAAJL0wACS9QAAkvVAAJL1wACS9gAAkvhAAJL4gACS+QAAkwjAAJMJQACTCcAAkwpAAJMLAACTC0AAkwuAAJMLwACTDAAAkwyAAJMNAACTDUAAkw2AAJMOAACTDkAAkx4AAJMegACTH0AAkx/AAJMggACTIMAAkyEAAJMhQACTIYAAkyIAAJMigACTIsAAkyMAAJMjgACTI8AAkycAAJMnQACTJ4AAkygAAJM3wACTOEAAkzjAAJM5QACTOgAAkzpAAJM6gACTOsAAkzsAAJM7gACTPAAAkzxAAJM8gACTPQAAkz1AAJNNAACTTYAAk05AAJNOwACTT4AAk0/AAJNQAACTUEAAk1CAAJNRAACTUYAAk1HAAJNSAACTUoAAk1LAAJNWgACTWcAAk1uAAJNcQACTXQAAk12AAJNfQACTYAAAk2DAAJNhgACTYgAAk2PAAJNmQACTeQAAk4HAAJOJwACTkcAAk5JAAJOSwACTk0AAk5PAAJOUgACTlMAAk5UAAJOVwACTlgAAk5aAAJOWwACTl0AAk5gAAJOYQACTmIAAk5lAAJOZgACTmsAAk54AAJOfQACTn8AAk6BAAJOhgACTokAAk6MAAJOjgACTrMAAk7XAAJO/gACTyIAAk8lAAJPJwACTykAAk8rAAJPLQACTy8AAk8wAAJPMwACT0AAAk9RAAJPUwACT1UAAk9XAAJPWQACT1sAAk9dAAJPXwACT2EAAk9yAAJPdQACT3gAAk97AAJPfgACT4EAAk+EAAJPhwACT4oAAk+MAAJPywACT80AAk/PAAJP0QACT9QAAk/VAAJP1gACT9cAAk/YAAJP2gACT9wAAk/dAAJP3gACT+AAAk/hAAJQIAACUCIAAlAkAAJQJgACUCkAAlAqAAJQKwACUCwAAlAtAAJQLwACUDEAAlAyAAJQMwACUDUAAlA2AAJQdQACUHcAAlB6AAJQfAACUH8AAlCAAAJQgQACUIIAAlCDAAJQhQACUIcAAlCIAAJQiQACUIsAAlCMAAJQmQACUJoAAlCbAAJQnQACUNwAAlDeAAJQ4AACUOIAAlDlAAJQ5gACUOcAAlDoAAJQ6QACUOsAAlDtAAJQ7gACUO8AAlDxAAJQ8gACUTEAAlEzAAJRNQACUTcAAlE6AAJROwACUTwAAlE9AAJRPgACUUAAAlFCAAJRQwACUUQAAlFGAAJRRwACUYYAAlGIAAJRigACUYwAAlGPAAJRkAACUZEAAlGSAAJRkwACUZUAAlGXAAJRmAACUZkAAlGbAAJRnAACUdsAAlHdAAJR3wACUeEAAlHkAAJR5QACUeYAAlHnAAJR6AACUeoAAlHsAAJR7QACUe4AAlHwAAJR8QACUjAAAlIyAAJSNAACUjYAAlI5AAJSOgACUjsAAlI8AAJSPQACUj8AAlJBAAJSQgACUkMAAlJFAAJSRgACUmsAAlKPAAJStgACUtoAAlLdAAJS3wACUuEAAlLjAAJS5QACUucAAlLoAAJS6wACUvgAAlMHAAJTCQACUwsAAlMNAAJTDwACUxEAAlMTAAJTFQACUyQAAlMnAAJTKgACUy0AAlMwAAJTMwACUzYAAlM5AAJTOwACU3oAAlN8AAJTfgACU4AAAlODAAJThAACU4UAAlOGAAJThwACU4kAAlOLAAJTjAACU40AAlOPAAJTkAACU88AAlPRAAJT0wACU9UAAlPYAAJT2QACU9oAAlPbAAJT3AACU94AAlPgAAJT4QACU+IAAlPkAAJT5QACVCQAAlQmAAJUKAACVCoAAlQtAAJULgACVC8AAlQwAAJUMQACVDMAAlQ1AAJUNgACVDcAAlQ5AAJUOgACVHkAAlR7AAJUfQACVH8AAlSCAAJUgwACVIQAAlSFAAJUhgACVIgAAlSKAAJUiwACVIwAAlSOAAJUjwACVM4AAlTQAAJU0gACVNQAAlTXAAJU2AACVNkAAlTaAAJU2wACVN0AAlTfAAJU4AACVOEAAlTjAAJU5AACVSMAAlUlAAJVJwACVSkAAlUsAAJVLQACVS4AAlUvAAJVMAACVTIAAlU0AAJVNQACVTYAAlU4AAJVOQACVXgAAlV6AAJVfAACVX4AAlWBAAJVggACVYMAAlWEAAJVhQACVYcAAlWJAAJVigACVYsAAlWNAAJVjgACVdkAAlX8AAJWHAACVjwAAlY+AAJWQAACVkIAAlZEAAJWRwACVkgAAlZJAAJWTAACVk0AAlZPAAJWUAACVlIAAlZVAAJWVgACVlcAAlZaAAJWWwACVmAAAlZtAAJWcgACVnQAAlZ2AAJWewACVn4AAlaBAAJWgwACVqgAAlbMAAJW8wACVxcAAlcaAAJXHAACVx4AAlcgAAJXIgACVyQAAlclAAJXKAACVzUAAldGAAJXSAACV0oAAldMAAJXTgACV1AAAldSAAJXVAACV1YAAldnAAJXagACV20AAldwAAJXcwACV3YAAld5AAJXfAACV38AAleBAAJXwAACV8IAAlfEAAJXxgACV8kAAlfKAAJXywACV8wAAlfNAAJXzwACV9EAAlfSAAJX0wACV9UAAlfWAAJYFQACWBcAAlgZAAJYGwACWB4AAlgfAAJYIAACWCEAAlgiAAJYJAACWCYAAlgnAAJYKAACWCoAAlgrAAJYagACWGwAAlhvAAJYcQACWHQAAlh1AAJYdgACWHcAAlh4AAJYegACWHwAAlh9AAJYfgACWIAAAliBAAJYjgACWI8AAliQAAJYkgACWNEAAljTAAJY1QACWNcAAljaAAJY2wACWNwAAljdAAJY3gACWOAAAljiAAJY4wACWOQAAljmAAJY5wACWSYAAlkoAAJZKgACWSwAAlkvAAJZMAACWTEAAlkyAAJZMwACWTUAAlk3AAJZOAACWTkAAlk7AAJZPAACWXsAAll9AAJZfwACWYEAAlmEAAJZhQACWYYAAlmHAAJZiAACWYoAAlmMAAJZjQACWY4AAlmQAAJZkQACWdAAAlnSAAJZ1AACWdYAAlnZAAJZ2gACWdsAAlncAAJZ3QACWd8AAlnhAAJZ4gACWeMAAlnlAAJZ5gACWiUAAlonAAJaKQACWisAAlouAAJaLwACWjAAAloxAAJaMgACWjQAAlo2AAJaNwACWjgAAlo6AAJaOwACWmAAAlqEAAJaqwACWs8AAlrSAAJa1AACWtYAAlrYAAJa2gACWtwAAlrdAAJa4AACWu0AAlr8AAJa/gACWwAAAlsCAAJbBAACWwYAAlsIAAJbCgACWxkAAlscAAJbHwACWyIAAlslAAJbKAACWysAAlsuAAJbMAACW28AAltxAAJbcwACW3UAAlt4AAJbeQACW3oAAlt7AAJbfAACW34AAluAAAJbgQACW4IAAluEAAJbhQACW8QAAlvGAAJbyAACW8oAAlvNAAJbzgACW88AAlvQAAJb0QACW9MAAlvVAAJb1gACW9cAAlvZAAJb2gACXBkAAlwbAAJcHQACXB8AAlwiAAJcIwACXCQAAlwlAAJcJgACXCgAAlwqAAJcKwACXCwAAlwuAAJcLwACXG4AAlxwAAJccgACXHQAAlx3AAJceAACXHkAAlx6AAJcewACXH0AAlx/AAJcgAACXIEAAlyDAAJchAACXMMAAlzFAAJcxwACXMkAAlzMAAJczQACXM4AAlzPAAJc0AACXNIAAlzUAAJc1QACXNYAAlzYAAJc2QACXRgAAl0aAAJdHAACXR4AAl0hAAJdIgACXSMAAl0kAAJdJQACXScAAl0pAAJdKgACXSsAAl0tAAJdLgACXW0AAl1vAAJdcQACXXMAAl12AAJddwACXXgAAl15AAJdegACXXwAAl1+AAJdfwACXYAAAl2CAAJdgwACXc4AAl3xAAJeEQACXjEAAl4zAAJeNQACXjcAAl45AAJePAACXj0AAl4+AAJeQQACXkIAAl5EAAJeRQACXkcAAl5JAAJeSgACXksAAl5OAAJeTwACXlgAAl5lAAJeagACXmwAAl5uAAJecwACXnYAAl55AAJeewACXqAAAl7EAAJe6wACXw8AAl8SAAJfFAACXxYAAl8YAAJfGgACXxwAAl8dAAJfIAACXy0AAl8+AAJfQAACX0IAAl9EAAJfRgACX0gAAl9KAAJfTAACX04AAl9fAAJfYgACX2UAAl9oAAJfawACX24AAl9xAAJfdAACX3cAAl95AAJfuAACX7oAAl+8AAJfvgACX8EAAl/CAAJfwwACX8QAAl/FAAJfxwACX8kAAl/KAAJfywACX80AAl/OAAJgDQACYA8AAmARAAJgEwACYBYAAmAXAAJgGAACYBkAAmAaAAJgHAACYB4AAmAfAAJgIAACYCIAAmAjAAJgYgACYGQAAmBnAAJgaQACYGwAAmBtAAJgbgACYG8AAmBwAAJgcgACYHQAAmB1AAJgdgACYHgAAmB5AAJghgACYIcAAmCIAAJgigACYMkAAmDLAAJgzQACYM8AAmDSAAJg0wACYNQAAmDVAAJg1gACYNgAAmDaAAJg2wACYNwAAmDeAAJg3wACYR4AAmEgAAJhIgACYSQAAmEnAAJhKAACYSkAAmEqAAJhKwACYS0AAmEvAAJhMAACYTEAAmEzAAJhNAACYXMAAmF1AAJhdwACYXkAAmF8AAJhfQACYX4AAmF/AAJhgAACYYIAAmGEAAJhhQACYYYAAmGIAAJhiQACYcgAAmHKAAJhzAACYc4AAmHRAAJh0gACYdMAAmHUAAJh1QACYdcAAmHZAAJh2gACYdsAAmHdAAJh3gACYh0AAmIfAAJiIQACYiMAAmImAAJiJwACYigAAmIpAAJiKgACYiwAAmIuAAJiLwACYjAAAmIyAAJiMwACYlgAAmJ8AAJiowACYscAAmLKAAJizAACYs4AAmLQAAJi0gACYtQAAmLVAAJi2AACYuUAAmL0AAJi9gACYvgAAmL6AAJi/AACYv4AAmMAAAJjAgACYxEAAmMUAAJjFwACYxoAAmMdAAJjIAACYyMAAmMmAAJjKAACY2cAAmNpAAJjawACY20AAmNwAAJjcQACY3IAAmNzAAJjdAACY3YAAmN4AAJjeQACY3oAAmN8AAJjfQACY7wAAmO+AAJjwAACY8IAAmPFAAJjxgACY8cAAmPIAAJjyQACY8sAAmPNAAJjzgACY88AAmPRAAJj0gACZBEAAmQTAAJkFQACZBcAAmQaAAJkGwACZBwAAmQdAAJkHgACZCAAAmQiAAJkIwACZCQAAmQmAAJkJwACZGYAAmRoAAJkagACZGwAAmRvAAJkcAACZHEAAmRyAAJkcwACZHUAAmR3AAJkeAACZHkAAmR7AAJkfAACZLsAAmS9AAJkvwACZMEAAmTEAAJkxQACZMYAAmTHAAJkyAACZMoAAmTMAAJkzQACZM4AAmTQAAJk0QACZRAAAmUSAAJlFAACZRYAAmUZAAJlGgACZRsAAmUcAAJlHQACZR8AAmUhAAJlIgACZSMAAmUlAAJlJgACZWUAAmVnAAJlaQACZWsAAmVuAAJlbwACZXAAAmVxAAJlcgACZXQAAmV2AAJldwACZXgAAmV6AAJlewACZYQAAmWFAAJlhwACZZQAAmWVAAJllgACZZgAAmWlAAJlpgACZacAAmWpAAJltgACZbcAAmW4AAJlugACZcMAAmXSAAJl3wACZe4AAmYAAAJmFAACZisAAmY9AAJmRgACZkcAAmZJAAJmVgACZlcAAmZYAAJmWgACZmMAAmZtAAJmdAAAAAAAAAQCAAAAAAAAXDgAAAAAAAAAAAAAAAAAAmZ8 +cxIAAYagXxAPTlNLZXllZEFyY2hpdmVy0QAIAAlUcm9vdIABrxEK+QALAAwAGwA3ADgAOQBfAGAAYQBiAGMAZABlAGYAZwBoAGkAagBrAGwAbQBuAG8AigCLAIwAkgCTAJ8AtQC2ALcAuAC5ALoAuwC8AL0AvgDXANoA4QDnAPYBBQEIARcBJgEpAIkBOQFIAUwBUAFfAWUBZgFuAX0BfgGHAZMBlAGVAZYBlwGsAa0BtQG2AbcBwwHXAdgB2QHaAdsB3AHdAd4B3wHuAf0CDAIQAh8CLgIvAj4CTQJcAmgCegJ7AnwCfQJ+An8CgAKBADECkAKfAqACrwK+AtgC2QLfAusDAQMQAxMDIgMxAzUDOQM6A0kDWANbA2oDeQN9A4wDmwPRA9ID0wPUA9UD1gPXA9gD2QPaA9sD3APdA94D3wPgA+ED4gPjA+QD5QPmA+cD6AP9A/4EBgQSBCYENQREBFMEVwRmBHUEhASTBKIErgTABM8E3gTtBPwFCwUaBSkFMQVGBUcFTwVQBVwFcAV/BY4FnQWhBbAFvwXOBd0F7AX4BgoGCwYMBg0GDgYPBhAGEQYgBiEGMAY/Bk4GTwZeBm0GfAaEBpkGmgaiBq4GwgbRBuAG7wbzBwIHEQcgBy8HPgdKB1wHawd6B4kHmAeZB6gHtwfGB9sH3AfkB/AIBAgTCCIIMQg1CEQIUwhiCHEIgAiMCJ4IrQi8CMsI2gjbCOoI+QkICR0JHgkmCTIJRglVCWQJcwl3CYYJlQmkCbMJwgnOCeAJ7wn+Cg0KHAodCiwKOwo8CksKYAphCmkKdQqJCpgKpwq2CroKyQrYCucK9gsFCxELIwsyCzMLQgtRC2ALYQtwC38LjgujC6QLrAu4C8wL2wvqC/kL/QwMDBsMKgw5DEgMVAxmDHUMhAyTDKIMsQzADM8M5AzlDO0M+Q0NDRwNKw06DT4NTQ1cDWsNeg2JDZUNpw22DcUN1A3jDfIOAQ4QDiUOJg4uDjoOTg5dDmwOew5/Do4OnQ6sDrsOyg7WDugO9w8GDxUPJA8zD0IPUQ9mD2cPbw97D48Png+tD7wPwA/PD94P7Q/8EAsQFxApEDgQRxBWEGUQdBCDEJIQpxCoELAQvBDQEN8Q7hD9EQEREBEfES4RPRFMEVgRahF5EYgRlxGmEbURxBHTEegR6RHxEf0SERIgEi8SPhJ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4WIBYjFigWK9YvljNWNFY4FjvWP5ZDVkcWShZOllJWVhZZ1l2WYVZlFmjWbhZuVnBWc1Z4VnwWf9aDloSWiFaMFo/Wk5aXVppWntailqZWqhat1rGWtVa5Fr5WvpbAlsOWyJbMVtAW09bU1tiW3FbgFuPW55bqlu8W8tb2lvpW/hb+VwIXBdcJlwpXDhcR1xWXGVcdFyJXIpcklyeXLJcwVzQXN9c41zyXQFdEF0fXS5dOl1MXVtdal15XYhdl12mXbVduF3SXdNd2V3lXfteCl4NXhxeK14uXj1eTF5PXl5ebV5xXoBej16QXphemV6uXq9et17DXtde5l71XwRfCF8XXyZfNV9EX1NfX19xX4Bfj1+eX61frl+9X8xf21/wX/Ff+WAFYBlgKGA3YEZgSmBZYGhgd2CGYJVgoWCzYMJg0WDgYO9g/mENYRxhH2E5YTphQGFMYWJhcWF0YYNhkmGVYaRhs2G2YcVh1GHYYedh9mH3Yf9iAGIBYhZiF2IfYitiP2JOYl1ibGJwYn9ijmKdYqxiu2LHYtli6GL3YwZjFWMkYzNjQmNXY1hjYGNsY4Bjj2OeY61jsWPAY89j3mPtY/xkCGQaZClkOGRHZFZkZWR0ZINkhmSgZKFkp2SzZMlk2GTbZOpk+WT8ZQtlGmUdZSxlO2U/ZU5lXWVeZWxlbWVuZW9lhGWFZY1lmWWtZbxly2XaZd5l7WX8ZgtmGmYpZjVmR2ZWZmVmdGaDZpJmoWawZsVmxmbOZtpm7mb9ZwxnG2cfZy5nPWdMZ1tnamd2Z4hnl2emZ7VnxGfTZ+Jn8WgGaAdoD2gbaC9oPmhNaFxoYGhvaH5ojWicaKtot2jJaNho52j2aQVpFGkjaTJpR2lIaVBpXGlwaX9pjmmdaaFpsGm/ac5p3WnsafhqCmoZaihqN2pGalVqZGpzaohqiWqRap1qsWrAas9q3mriavFrAGsPax5rLWs5a0trWmtpa3hrh2uWa6VrtGu3a9Fr0mvYa+Rr+mwJbAxsG2wqbC1sPGxLbE5sXWxsbHBsf2yObI9sm2ycbJ1ssmyzbLtsx2zbbOps+W0IbQxtG20qbTltSG1XbWNtdW2EbZNtom2xbcBtz23ebfNt9G38bghuHG4rbjpuSW5Nblxua256bolumG6kbrZuxW7UbuNu8m8BbxBvH280bzVvPW9Jb11vbG97b4pvjm+db6xvu2/Kb9lv5W/3cAZwFXAkcDNwQnBRcGBwdXB2cH5winCecK1wvHDLcM9w3nDtcPxxC3EacSZxOHFHcVZxZXF0cYNxknGhcaRxvnG/ccVx0XHncfZx+XIIchdyGnIpcjhyO3JKcllyXXJscntyfHKWcpdymHKZcppym3Kccp1ynnKfcqBytXK2cr5yynLecu1y/HMLcw9zHnMtczxzS3Nac2ZzeHOHc5ZzpXO0c8Nz0nPhc/Zz93P/dAt0H3QudD10THRQdF90bnR9dIx0m3SndLl0yHTXdOZ09XUEdRN1InU3dTh1QHVMdWB1b3V+dY11kXWgda91vnXNddx16HX6dgl2GHYndjZ2RXZUdmN2eHZ5doF2jXahdrB2v3bOdtJ24Xbwdv93Dncddyl3O3dKd1l3aHd3d4Z3lXekd7l3unfCd8534nfxeAB4D3gTeCJ4MXhAeE94XnhqeHx4i3iaeKl4uHjHeNZ45Xj6ePt5A3kPeSN5MnlBeVB5VHljeXJ5gXmQeZ95q3m9ecx523nqefl6CHoXeiZ6O3o8ekR6UHpkenN6gnqRepV6pHqzesJ60Xrgeux6/nsNexx7K3s6e0l7WHtne3x7fXuFe5F7pXu0e8N70nvWe+V79HwDfBJ8IXwtfD98TnxdfGx8e3yKfJl8qHy9fL58xnzSfOZ89X0EfRN9F30mfTV9RH1TfWJ9bn2AfY99nn2tfbx9y33afel9/n3/fgd+E34nfjZ+RX5Uflh+Z352foV+lH6jfq9+wX7Qft9+7n79fwx/G38qfz9/QH9If1R/aH93f4Z/lX+Zf6h/t3/Gf9V/5H/wgAKAEYAggC+APoBNgFyAa4BugIiAiYCPgJuAsYDAgMOA0oDhgOSA84ECgQWBFIEjgSeBNoFFgU+BZIFlgW2BeYGNgZyBq4G6gb6BzYHcgeuB+oIJghWCJ4I2gkWCVIJjgnKCgYKQgqWCpoKugrqCzoLdguyC+4L/gw6DHYMsgzuDSoNWg2iDd4OGg5WDpIOzg8KD0YPmg+eD74P7hA+EHoQthDyEQIRPhF6EbYR8hIuEl4SphLiEx4TWhOWE9IUDhRKFFYUvhTCFNoVChViFZ4VqhXmFiIWLhZqFqYWshbuFyoXOhd2F7IXthfeF+IX5hg6GD4YXhiOGN4ZGhlWGZIZohneGhoaVhqSGs4a/htGG4Ibvhv6HDYcchyuHOodPh1CHWIdkh3iHh4eWh6WHqYe4h8eH1oflh/SIAIgSiCGIMIg/iE6IXYhsiHuIkIiRiJmIpYi5iMiI14jmiOqI+YkIiReJJok1iUGJU4liiXGJgImPiZ6JrYm8ib+J2YnaieCJ7IoCihGKFIojijKKNYpEilOKVoplinSKeIqHipaKl4qfiqCKtYq2ir6Kyoreiu2K/IsLiw+LHostizyLS4tai2aLeIuHi5aLpYu0i8OL0ovhi/aL94v/jAuMH4wujD2MTIxQjF+Mbox9jIyMm4ynjLmMyIzXjOaM9Y0EjRONIo0ljSmNLY0xjTmNPI1AVSRudWxs1wANAA4ADwAQABEAEgATABQAFQAWABcAGAAXABpfEA9feGRfcm9vdFBhY2thZ2VWJGNsYXNzXF94ZF9jb21tZW50c18QEF94ZF9tb2RlbE1hbmFnZXJfEBVfY29uZmlndXJhdGlvbnNCeU5hbWVdX3hkX21vZGVsTmFtZV8QF19tb2RlbFZlcnNpb25JZGVudGlmaWVygAKBCviBCvaAAIEK94AAgQSj3gAcAB0AHgAfACAAIQAiAA4AIwAkACUAJgAnACgAKQAqACsACQApABcALwAwADEAMgAzACkAKQAXXxAcWERCdWNrZXRGb3JDbGFzc2Vzd2FzRW5jb2RlZF8QGlhEQnVja2V0Rm9yUGFja2FnZXNzdG9yYWdlXxAcWERCdWNrZXRGb3JJbnRlcmZhY2Vzc3RvcmFnZV8QD194ZF9vd25pbmdNb2RlbF8QHVhEQnVja2V0Rm9yUGFja2FnZXN3YXNFbmNvZGVkVl9vd25lcl8QG1hEQnVja2V0Rm9yRGF0YVR5cGVzc3RvcmFnZVtfdmlzaWJpbGl0eV8QGVhEQnVja2V0Rm9yQ2xhc3Nlc3N0b3JhZ2VVX25hbWVfEB9YREJ1Y2tldEZvckludGVyZmFjZXN3YXNFbmNvZGVkXxAeWERCdWNrZXRGb3JEYXRhVHlwZXN3YXNFbmNvZGVkXxAQX3VuaXF1ZUVsZW1lbnRJRIAEgQr0gQrygAGABIAAgQrzgQr1EACABYADgASABIAAUFNZRVPTADoAOwAOADwATQBeV05TLmtleXNaTlMub2JqZWN0c68QEAA9AD4APwBAAEEAQgBDAEQARQBGAEcASABJAEoASwBMgAaAB4AIgAmACoALgAyADYAOgA+AEIARgBKAE4AUgBWvEBAATgBPAFAAUQBSAFMAVABVAFYAVwBYAFkAWgBbAFwAXYAWgQG4gGmBAm6BBd2BBmuBB1WBB5qBB9+BBtyBCGqBBE2BCN2BCfmBClKBCq6ANF1DREV4dGVybmFsQXBpXENEQ2hhaW5Bc3NldFdDRENoYWluV0NEQXNzZXRdQ0RDb250YWN0SXRlbV8QGUNEUG9sa2Fzd2FwUmVtb3RlU2V0dGluZ3NdQ0RTaW5nbGVWYWx1ZVtDRFN0YXNoSXRlbV8QFUNEUnVudGltZU1ldGFkYXRhSXRlbV5DRFBvbGthc3dhcERleFpDRFNjYW1JbmZvW0NEQ2hhaW5Ob2RlXxAYQ0RUcmFuc2FjdGlvbkhpc3RvcnlJdGVtWUNEQ29udGFjdF5DRFBoaXNoaW5nSXRlbV8QEkNEQ2hhaW5TdG9yYWdlSXRlbd8QEABwAHEAcgBzACEAdAB1ACMAdgB3AA4AJQB4AHkAKAB6AHsAfAApACkAFACAAIEAMQApAHsAhAA9AHsAhwCIAIlfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc29yZGVyZWRfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3N0b3JhZ2VfECBYREJ1Y2tldEZvclN0ZXJlb3R5cGVzd2FzRW5jb2RlZF8QJFhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzd2FzRW5jb2RlZF8QHVhEQnVja2V0Rm9yU3RlcmVvdHlwZXNzdG9yYWdlXxAkWERCdWNrZXRGb3JHZW5lcmFsaXphdGlvbnNkdXBsaWNhdGVzXxAkWERCdWNrZXRGb3JHZW5lcmFsaXphdGlvbnN3YXNFbmNvZGVkXxAdWERCdWNrZXRGb3JTdGVyZW90eXBlc29yZGVyZWRfECFYREJ1Y2tldEZvckdlbmVyYWxpemF0aW9uc29yZGVyZWRfECFYREJ1Y2tldEZvckdlbmVyYWxpemF0aW9uc3N0b3JhZ2VbX2lzQWJzdHJhY3SAGIA8gASABIACgBmBA7mABIAYgQO7gAaAGIEF3IAXCBMAAAABDKV2PFdvcmRlcmVk0wA6ADsADgCNAI8AXqEAjoAaoQCQgBuANF5YRF9QU3RlcmVvdHlwZdkAIQAlAJQADgAoAJUAIwB6AJYATgCOAHsAmgAXACkAMQCJAJ5fECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc29yZGVyZWRfECRYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3dhc0VuY29kZWRfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3N0b3JhZ2WAFoAagBiAO4AAgAQIgBzTADoAOwAOAKAAqgBeqQChAKIAowCkAKUApgCnAKgAqYAdgB6AH4AggCGAIoAjgCSAJakAqwCsAK0ArgCvALAAsQCyALOAJoAqgCuALYAugDCAMoA1gDmANF8QE1hEUE1Db21wb3VuZEluZGV4ZXNfEBBYRF9QU0tfZWxlbWVudElEXxAZWERQTVVuaXF1ZW5lc3NDb25zdHJhaW50c18QGlhEX1BTS192ZXJzaW9uSGFzaE1vZGlmaWVyXxAZWERfUFNLX2ZldGNoUmVxdWVzdHNBcnJheV8QEVhEX1BTS19pc0Fic3RyYWN0XxAPWERfUFNLX3VzZXJJbmZvXxATWERfUFNLX2NsYXNzTWFwcGluZ18QFlhEX1BTS19lbnRpdHlDbGFzc05hbWXfEA8AvwDAAMEAIQDCAMMAxAAjAMUADgAlAMYAxwAoAMgAFwDKABcAkACJAIkAiQAxAIkA0QChAIkAiQAXAIlVX3R5cGVYX2RlZmF1bHRcX2Fzc29jaWF0aW9uW19pc1JlYWRPbmx5WV9pc1N0YXRpY1lfaXNVbmlxdWVaX2lzRGVyaXZlZFpfaXNPcmRlcmVkXF9pc0NvbXBvc2l0ZVdfaXNMZWFmgACAJ4AAgBsICAgIgCmAHQgIgAAI0gA7AA4A2ADZoIAo0gDbANwA3QDeWiRjbGFzc25hbWVYJGNsYXNzZXNeTlNNdXRhYmxlQXJyYXmjAN0A3wDgV05TQXJyYXlYTlNPYmplY3TSANsA3ADiAONfEBBYRFVNTFByb3BlcnR5SW1wpADkAOUA5gDgXxAQWERVTUxQcm9wZXJ0eUltcF8QFFhEVU1MTmFtZWRFbGVtZW50SW1wXxAPWERVTUxFbGVtZW50SW1w3xAPAL8AwADBACEAwgDDAMQAIwDFAA4AJQDGAMcAKADIABcAFwAXAJAAiQCJAIkAMQCJANEAogCJAIkAFwCJgACAAIAAgBsICAgIgCmAHggIgAAI3xAPAL8AwADBACEAwgDDAMQAIwDFAA4AJQDGAMcAKADIABcA+AAXAJAAiQCJAIkAMQCJANEAowCJAIkAFwCJgACALIAAgBsICAgIgCmAHwgIgAAI0gA7AA4BBgDZoIAo3xAPAL8AwADBACEAwgDDAMQAIwDFAA4AJQDGAMcAKADIABcAFwAXAJAAiQCJAIkAMQCJANEApACJAIkAFwCJgACAAIAAgBsICAgIgCmAIAgIgAAI3xAPAL8AwADBACEAwgDDAMQAIwDFAA4AJQDGAMcAKADIABcBGQAXAJAAiQCJAIkAMQCJANEApQCJAIkAFwCJgACAL4AAgBsICAgIgCmAIQgIgAAI0gA7AA4BJwDZoIAo3xAPAL8AwADBACEAwgDDAMQAIwDFAA4AJQDGAMcAKADIABcBKwAXAJAAiQCJAIkAMQCJANEApgCJAIkAFwCJgACAMYAAgBsICAgIgCmAIggIgAAICN8QDwC/AMAAwQAhAMIAwwDEACMAxQAOACUAxgDHACgAyAAXATsAFwCQAIkAiQCJADEAiQDRAKcAiQCJABcAiYAAgDOAAIAbCAgICIApgCMICIAACNMAOgA7AA4BSQFKAF6goIA00gDbANwBTQFOXxATTlNNdXRhYmxlRGljdGlvbmFyeaMBTQFPAOBcTlNEaWN0aW9uYXJ53xAPAL8AwADBACEAwgDDAMQAIwDFAA4AJQDGAMcAKADIABcBUgAXAJAAiQCJAIkAMQCJANEAqACJAIkAFwCJgACANoAAgBsICAgIgCmAJAgIgAAI1gAlAA4AKAB6ACEAIwFgAWEAFwCJABcAMYA3gDiAAAiAAF8QFFhER2VuZXJpY1JlY29yZENsYXNz0gDbANwBZwFoXVhEVU1MQ2xhc3NJbXCmAWkBagFrAWwBbQDgXVhEVU1MQ2xhc3NJbXBfEBJYRFVNTENsYXNzaWZpZXJJbXBfEBFYRFVNTE5hbWVzcGFjZUltcF8QFFhEVU1MTmFtZWRFbGVtZW50SW1wXxAPWERVTUxFbGVtZW50SW1w3xAPAL8AwADBACEAwgDDAMQAIwDFAA4AJQDGAMcAKADIABcBcAAXAJAAiQCJAIkAMQCJANEAqQCJAIkAFwCJgACAOoAAgBsICAgIgCmAJQgIgAAIXUNERXh0ZXJuYWxBcGnSANsA3AF/AYBfEBJYRFVNTFN0ZXJlb3R5cGVJbXCnAYEBggGDAYQBhQGGAOBfEBJYRFVNTFN0ZXJlb3R5cGVJbXBdWERVTUxDbGFzc0ltcF8QElhEVU1MQ2xhc3NpZmllckltcF8QEVhEVU1MTmFtZXNwYWNlSW1wXxAUWERVTUxOYW1lZEVsZW1lbnRJbXBfEA9YRFVNTEVsZW1lbnRJbXDTADoAOwAOAYgBjQBepAGJAYoBiwGMgD2APoA/gECkAY4BjwGQAZGAQYEFloEFroEFxYA0VWNoYWluVXR5cGVzVHR5cGVTdXJs3xASAL8AwADBAZgAIQDDAMQBmQAjAMIBmgDFAA4AJQDGAMcAKADIABcAFwAXACkATgCJAIkBogAxAIkAewCJAaYBiQCJAIkBqgCJXxAgWERCdWNrZXRGb3JTdGVyZW90eXBlc3dhc0VuY29kZWRfEB1YREJ1Y2tldEZvclN0ZXJlb3R5cGVzc3RvcmFnZV8QHVhEQnVja2V0Rm9yU3RlcmVvdHlwZXNvcmRlcmVkgACAAIAAgASAFggIgEMIgBgIgKyAPQgIgEIIEwAAAAEQUsGd0wA6ADsADgGuAbEAXqIBrwGwgESARaIBsgGzgEaAWoA0XxASWERfUFByb3BTdGVyZW90eXBlXxAQWERfUFJfU3RlcmVvdHlwZdkAIQAlAbgADgAoAbkAIwB6AboBjgGvAHsAmgAXACkAMQCJAcJfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc29yZGVyZWRfECRYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3dhc0VuY29kZWRfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3N0b3JhZ2WAQYBEgBiAO4AAgAQIgEfTADoAOwAOAcQBzQBeqAHFAcYBxwHIAckBygHLAcyASIBJgEqAS4BMgE2AToBPqAHOAc8B0AHRAdIB0wHUAdWAUIBRgFKAVIBVgFeAWIBZgDRfEBtYRF9QUFNLX2lzU3RvcmVkSW5UcnV0aEZpbGVfEBtYRF9QUFNLX3ZlcnNpb25IYXNoTW9kaWZpZXJfEBBYRF9QUFNLX3VzZXJJbmZvXxARWERfUFBTS19pc0luZGV4ZWRfEBJYRF9QUFNLX2lzT3B0aW9uYWxfEBpYRF9QUFNLX2lzU3BvdGxpZ2h0SW5kZXhlZF8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Gb3JTdGVyZW90eXBlc29yZGVyZWSAAIAAgACABIECbggIgQKlCIAYCIDOgQKCCAiBAqQIElruAS/TADoAOwAOHp0eoABeogGvBUqARICwoh6hHqKBAqaBArGANNkAIQAlHqUADgAoHqYAIwB6HqcdLQGvAHsAmgAXACkAMQCJHq9fECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc29yZGVyZWRfECRYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3dhc0VuY29kZWRfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3N0b3JhZ2WBAqOARIAYgDuAAIAECIECp9MAOgA7AA4esR66AF6oAcUBxgHHAcgByQHKAcsBzIBIgEmASoBLgEyATYBOgE+oHrsevB69Hr4evx7AHsEewoECqIECqYECqoECrIECrYECroECr4ECsIA03xAPAL8AwADBACEAwgDDAMQAIwDFAA4AJQDGAMcAKADIABcBKwAXHqEAiQCJAIkAMQCJANEBxQCJAIkAFwCJgACAMYAAgQKmCAgICIApgEgICIAACN8QDwC/AMAAwQAhAMIAwwDEACMAxQAOACUAxgDHACgAyAAXABcAFx6hAIkAiQCJADEAiQDRAcYAiQCJABcAiYAAgACAAIECpggICAiAKYBJCAiAAAjfEA8AvwDAAMEAIQDCAMMAxAAjAMUADgAlAMYAxwAoAMgAFx7kABceoQCJAIkAiQAxAIkA0QHHAIkAiQAXAImAAIECq4AAgQKmCAgICIApgEoICIAACNMAOgA7AA4e8h7zAF6goIA03xAPAL8AwADBACEAwgDDAMQAIwDFAA4AJQDGAMcAKADIABcBKwAXHqEAiQCJAIkAMQCJANEByACJAIkAFwCJgACAMYAAgQKmCAgICIApgEsICIAACN8QDwC/AMAAwQAhAMIAwwDEACMAxQAOACUAxgDHACgAyAAXAiEAFx6hAIkAiQCJADEAiQDRAckAiQCJABcAiYAAgFaAAIECpggICAiAKYBMCAiAAAjfEA8AvwDAAMEAIQDCAMMAxAAjAMUADgAlAMYAxwAoAMgAFwErABceoQCJAIkAiQAxAIkA0QHKAIkAiQAXAImAAIAxgACBAqYICAgIgCmATQgIgAAI3xAPAL8AwADBACEAwgDDAMQAIwDFAA4AJQD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AxAAjAMUADgAlAMYAxwAoAMgAFzVKABc0fQCJAIkAiQAxAIkA0QJrAIkAiQAXAImAAIEESoAAgQRGCAgICIApgF0ICIAACBAC3xAPAL8AwADBACEAwgDDAMQAIwDFAA4AJQDGAMcAKADIABcCgwAXNH0AiQCJAIkAMQCJANECbACJAIkAFwCJgACAZIAAgQRGCAgICIApgF4ICIAACN8QDwC/AMAAwQAhAMIAwwDEACMAxQAOACUAxgDHACgAyAAXAFkAFzR9AIkAiQCJADEAiQDRAm0AiQCJABcAiYAAgQRNgACBBEYICAgIgCmAXwgIgAAI3xAQNXc1eDV5NXoAITV7NXwAIzV9NX4ADgAlNX81gAAoAHoAezWCACkAKQAUNYYAgQAxACkAewCEAEgAezWNNY4AiV8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzb3JkZXJlZF8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzc3RvcmFnZV8QIFhEQnVja2V0Rm9yU3RlcmVvdHlwZXN3YXNFbmNvZGVkXxAkWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXN3YXNFbmNvZGVkXxAdWERCdWNrZXRGb3JTdGVyZW90eXBlc3N0b3JhZ2VfECRYREJ1Y2tldEZvckdlbmVyYWxpemF0aW9uc2R1cGxpY2F0ZXNfECRYREJ1Y2tldEZvckdlbmVyYWxpemF0aW9uc3dhc0VuY29kZWRfEB1YREJ1Y2tldEZvclN0ZXJlb3R5cGVzb3JkZXJlZF8QIVhEQnVja2V0Rm9yR2VuZXJhbGl6YXRpb25zb3JkZXJlZF8QIVhEQnVja2V0Rm9yR2VuZXJhbGl6YXRpb25zc3RvcmFnZYAYgQRhgASABIACgQRPgQO5gASAGIEDu4ARgBiBBNiBBE4IEwAAAAEPEWM+0wA6ADsADjWSNZQAXqEAjoAaoTWVgQRQgDTZACEAJTWYAA4AKDWZACMAejWaAFkAjgB7AJoAFwApADEAiTWiXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNvcmRlcmVkXxAkWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXN3YXNFbmNvZGVkXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNzdG9yYWdlgQRNgBqAGIA7gAC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AGPIAABj0AAAY9gAAGPgAABkbAAAZHQAAGSAAABkiAAAZJQAAGSgAABkrAAAZLgAAGTEAABk0AAAZNwAAGToAABk9AAAZQAAAGUMAABlGAAAZSQAAGUsAABlZAAAZZgAAGW4AABl2AAAZhAAAGaAAABmuAAAZugAAGdIAABnhAAAZ7AAAGfgAABoTAAAaHQAAGiwAABpBAAAahAAAGqgAABrMAAAa7wAAGxYAABs2AAAbXQAAG4QAABukAAAbyAAAG+wAABv4AAAb+gAAG/wAABv+AAAcAAAAHAIAABwEAAAcBwAAHAkAABwLAAAcDgAAHBAAABwSAAAcFQAAHBcAABwYAAAcIQAAHCkAABw2AAAcOQAAHDsAABw+AAAcQAAAHEIAABxRAAAcdgAAHJoAABzBAAAc5QAAHOcAABzpAAAc6wAAHO0AABzvAAAc8QAAHPIAABz0AAAdAQAAHRQAAB0WAAAdGAAAHRoAAB0cAAAdHgAAHSAAAB0iAAAdJAAAHSYAAB05AAAdOwAAHT0AAB0/AAAdQQAAHUMAAB1FAAAdRwAAHUkAAB1LAAAdTQAAHWMAAB12AAAdkgAAHa8AAB3LAAAd3wAAHfEAAB4HAAAeIAAAHl8AAB5lAAAebgAAHnsAAB6HAAAekQAAHpsAAB6mAAAesQAAHr4AAB7GAAAeyAAAHsoAAB7MAAAezgAAHs8AAB7QAAAe0QAAHtIAAB7UAAAe1gAAHtcAAB7YAAAe2gAAHtsAAB7kAAAe5QAAHucAAB7wAAAe+wAAHwQAAB8TAAAfGgAAHyIAAB8rAAAfNAAAH0cAAB9QAAAfYwAAH3oAAB+MAAAfywAAH80AAB/PAAAf0QAAH9MAAB/UAAAf1QAAH9YAAB/XAAAf2QAAH9sAAB/cAAAf3QAAH98AAB/gAAAgHwAAICEAACAjAAAgJQAAICcAACAoAAAgKQAAICoAACArAAAgLQAAIC8AACAwAAAgMQAAIDMAACA0AAAgPQAAID4AACBAAAAgfwAAIIEAACCDAAAghQAAIIcAACCIAAAgiQAAIIoAACCLAAAgjQAAII8AACCQAAAgkQAAIJMAACCUAAAg0wAAINUAACDXAAAg2QAAINsAACDcAAAg3QAAIN4AACDfAAAg4QAAIOMAACDkAAAg5QAAIOcAACDoAAAg8QAAIPIAACD0AAAhMwAAITUAACE3AAAhOQAAITsAACE8AAAhPQAAIT4AACE/AAAhQQAAIUMAACFEAAAhRQAAIUcAACFIAAAhSQAAIYgAACGKAAAhjAAAIY4AACGQAAAhkQAAIZIAACGTAAAhlAAAIZYAACGYAAAhmQAAIZoAACGcAAAhnQAAIaoAACGrAAAhrAAAIa4AACG3AAAhzQAAIdQAACHhAAAiIAAAIiIAACIkAAAiJgAAIigAACIpAAAiKgAAIisAACIsAAAiLgAAIjAAACIxAAAiMgAAIjQAACI1AAAiTgAAIlAAACJSAAAiVAAAIlUAACJXAAAibgAAIncAACKFAAAikgAAIqAAACK1AAAiyQAAIuAAACLyAAAjMQAAIzMAACM1AAAjNwAAIzkAACM6AAAjOwAAIzwAACM9AAAjPwAAI0EAACNCAAAjQwAAI0UAACNGAAAjVAAAI10AACNyAAAjgQAAI5YAACOkAAAjuQAAI80AACPkAAAj9gAAJAMAACQMAAAkDgAAJBAAACQSAAAkFAAAJB0AACQfAAAkIgAAJCUAACQoAAAkKgAAJDAAACQ2AAAkOwAAJD8AACSKAAAkrQAAJM0AACTtAAAk7wAAJPEAACTzAAAk9QAAJPcAACT4AAAk+QAAJPsAACT8AAAk/gAAJP8AACUBAAAlAwAAJQQAACUFAAAlBwAAJQgAACURAAAlHgAAJSMAACUlAAAlJwAAJSwAACUuAAAlMAAAJTIAACVHAAAlWgAAJX8AACWjAAAlygAAJe4AACXwAAAl8gAAJfQAACX2AAAl+AAAJfoAACX7AAAl/QAAJgoAACYbAAAmHQAAJh8AACYhAAAmIwAAJiUAACYnAAAmKQAAJisAACY8AAAmPgAAJkAAACZCAAAmRAAAJkYAACZIAAAmSgAAJkwAACZOAAAmbAAAJooAACadAAAmsQAAJsYAACbjAAAm9wAAJw0AACdMAAAnTgAAJ1AAACdSAAAnVAAAJ1UAACdWAAAnVwAAJ1gAACdaAAAnXAAAJ10AACdeAAAnYAAAJ2EAACegAAAnogAAJ6QAACemAAAnqAAAJ6kAACeqAAAnqwAAJ6wAACeuAAAnsAAAJ7EAACeyAAAntAAAJ7UAACf0AAAn9gAAJ/gAACf6AAAn/AAAJ/0AACf+AAAn/wAAKAAAACgCAAAoBAAAKAUAACgGAAAoCAAAKAkAACgWAAAoFwAAKBgAACgaAAAoWQAAKFsAAChdAAAoXwAAKGEAAChiAAAoYwAAKGQAAChlAAAoZwAAKGkAAChqAAAoawAAKG0AAChuAAAorQAAKK8AACixAAAoswAAKLUAACi2AAAotwAAKLgAACi5AAAouwAAKL0AACi+AAAovwAAKMEAACjCAAAowwAAKQIAACkEAAApBgAAKQgAACkKAAApCwAAKQwAACkNAAApDgAAKRAAACkSAAApEwAAKRQAACkWAAApFwAAKVYAAClYAAApWgAAKVwAACleAAApXwAAKWAAAClhAAApYgAAKWQAAClmAAApZwAAKWgAAClqAAApawAAKaoAACmsAAAprgAAKbAAACmyAAApswAAKbQAACm1AAAptgAAKbgAACm6AAApuwAAKbwAACm+AAApvwAAKeQAACoIAAAqLwAAKlMAACpVAAAqVwAAKlkAACpbAAAqXQAAKl8AACpgAAAqYgAAKm8AACp+AAAqgAAAKoIAACqEAAAqhgAAKogAACqKAAAqjAAAKpsAACqdAAAqnwAAKqEAACqjAAAqpgAAKqkAACqsAAAqrgAAKsAAACrUAAAq5gAAKvsAACsNAAArHAAAKzkAACt4AAAregAAK3wAACt+AAArgAAAK4EAACuCAAArgwAAK4QAACuGAAAriAAAK4kAACuKAAArjAAAK40AACvMAAArzgAAK9AAACvSAAAr1AAAK9UAACvWAAAr1wAAK9gAACvaAAAr3AAAK90AACveAAAr4AAAK+EAACvjAAAsIgAALCQAACwmAAAsKAAALCoAACwrAAAsLAAALC0AACwuAAAsMAAALDIAACwzAAAsNAAALDYAACw3AAAsdgAALHgAACx6AAAsfAAALH4AACx/AAAsgAAALIEAACyCAAAshAAALIYAACyHAAAsiAAALIoAACyLAAAszgAALPIAAC0WAAAtOQAALWAAAC2AAAAtpwAALc4AAC3uAAAuEgAALjYAAC44AAAuOgAALjwAAC4+AAAuQAAALkIAAC5FAAAuRwAALkkAAC5MAAAuTgAALlAAAC5TAAAuVQAALlYAAC5bAAAuaAAALmsAAC5tAAAucAAALnIAAC50AAAumQAALr0AAC7kAAAvCAAALwoAAC8MAAAvDgAALxAAAC8SAAAvFAAALxUAAC8XAAAvJAAALzcAAC85AAAvOwAALz0AAC8/AAAvQQAAL0MAAC9FAAAvRwAAL0kAAC9cAAAvXgAAL2AAAC9iAAAvZAAAL2YAAC9oAAAvagAAL2wAAC9uAAAvcAAAL68AAC+xAAAvswAAL7UAAC+3AAAvuAAAL7kAAC+6AAAvuwAAL70AAC+/AAAvwAAAL8EAAC/DAAAvxAAAL80AAC/OAAAv0AAAMA8AADARAAAwEwAAMBUAADAXAAAwGAAAMBkAADAaAAAwGwAAMB0AADAfAAAwIAAAMCEAADAjAAAwJAAAMGMAADBlAAAwZwAAMGkAADBrAAAwbAAAMG0AADBuAAAwbwAAMHEAADBzAAAwdAAAMHUAADB3AAAweAAAMIEAADCEAAAwhgAAMIgAADCRAAAwlAAAMJYAADCYAAAwoAAAMN8AADDhAAAw4wAAMOUAADDnAAAw6AAAMOkAADDqAAAw6wAAMO0AADDvAAAw8AAAMPEAADDzAAAw9AAAMTMAADE1AAAxNwAAMTkAADE7AAAxPAAAMT0AADE+AAAxPwAAMUEAADFDAAAxRAAAMUUAADFHAAAxSAAAMVEAADFSAAAxVAAAMZMAADGVAAAxlwAAMZkAADGbAAAxnAAAMZ0AADGeAAAxnwAAMaEAADGjAAAxpAAAMaUAADGnAAAxqAAAMecAADHpAAAx6wAAMe0AADHvAAAx8AAAMfEAADHyAAAx8wAAMfUAADH3AAAx+AAAMfkAADH7AAAx/AAAMgkAADIKAAAyCwAAMg0AADJMAAAyTgAAMlAAADJSAAAyVAAAMlUAADJWAAAyVwAAMlgAADJaAAAyXAAAMl0AADJeAAAyYAAAMmEAADKgAAAyogAAMqQAADKmAAAyqAAAMqkAADKqAAAyqwAAMqwAADKuAAAysAAAMrEAADKyAAAytAAAMrUAADLCAAAy9wAAMvkAADL7AAAy/QAAMv8AADMBAAAzAwAAMwUAADMHAAAzCQAAMwsAADMNAAAzDwAAMxEAADMTAAAzFQAAMxcAADMZAAAzGwAAMx0AADMfAAAzIQAAMyMAADMlAAAzJwAAMykAADNeAAAzYAAAM2IAADNkAAAzZgAAM2gAADNrAAAzbgAAM3EAADN0AAAzdwAAM3oAADN9AAAzgAAAM4MAADOGAAAziQAAM4wAADOPAAAzkgAAM5UAADOYAAAzmwAAM54AADOhAAAzpAAAM6YAADOwAAAzvgAAM8wAADPUAAAz4gAAM+cAADP2AAA0BAAANAkAADQdAAA0JAAANDoAADREAAA0UwAANFkAADRmAAA0bwAANHYAADSKAAA0nQAANK8AADS9AAA0yQAANRQAADU3AAA1VwAANXcAADV5AAA1ewAANX0AADV/AAA1gQAANYIAADWDAAA1hQAANYYAADWIAAA1iQAANYsAADWNAAA1jgAANY8AADWRAAA1kgAANZcAADWkAAA1qQAANasAADWtAAA1sgAANbQAADW2AAA1uAAANd0AADYBAAA2KAAANkwAADZOAAA2UAAANlIAADZUAAA2VgAANlgAADZZAAA2WwAANmgAADZ5AAA2ewAANn0AADZ/AAA2gQAANoMAADaFAAA2hwAANokAADaaAAA2nAAANp4AADagAAA2ogAANqQAADamAAA2qAAANqoAADasAAA26wAANu0AADbvAAA28QAANvMAADb0AAA29QAANvYAADb3AAA2+QAANvsAADb8AAA2/QAANv8AADcAAAA3PwAAN0EAADdDAAA3RQAAN0cAADdIAAA3SQAAN0oAADdLAAA3TQAAN08AADdQAAA3UQAAN1MAADdUAAA3kwAAN5UAADeXAAA3mQAAN5sAADecAAA3nQAAN54AADefAAA3oQAAN6MAADekAAA3pQAAN6cAADeoAAA3tQAAN7YAADe3AAA3uQAAN/gAADf6AAA3/AAAN/4AADgAAAA4AQAAOAIAADgDAAA4BAAAOAYAADgIAAA4CQAAOAoAADgMAAA4DQAAOEwAADhOAAA4UAAAOFIAADhUAAA4VQAAOFYAADhXAAA4WAAAOFoAADhcAAA4XQAAOF4AADhgAAA4YQAAOKAAADiiAAA4pAAAOKYAADioAAA4qQAAOKoAADirAAA4rAAAOK4AADiwAAA4sQAAOLIAADi0AAA4tQAAOPQAADj2AAA4+AAAOPoAADj8AAA4/QAAOP4AADj/AAA5AAAAOQIAADkEAAA5BQAAOQYAADkIAAA5CQAAOUgAADlKAAA5TAAAOU4AADlQAAA5UQAAOVIAADlTAAA5VAAAOVYAADlYAAA5WQAAOVoAADlcAAA5XQAAOYIAADmmAAA5zQAAOfEAADnzAAA59QAAOfcAADn5AAA5+wAAOf0AADn+AAA6AAAAOg0AADocAAA6HgAAOiAAADoiAAA6JAAAOiYAADooAAA6KgAAOjkAADo7AAA6PQAAOj8AADpBAAA6QwAAOkUAADpHAAA6SQAAOogAADqKAAA6jAAAOo4AADqQAAA6kQAAOpIAADqTAAA6lAAAOpYAADqYAAA6mQAAOpoAADqcAAA6nQAAOtwAADreAAA64AAAOuIAADrkAAA65QAAOuYAADrnAAA66AAAOuoAADrsAAA67QAAOu4AADrwAAA68QAAOzAAADsyAAA7NAAAOzYAADs4AAA7OQAAOzoAADs7AAA7PAAAOz4AADtAAAA7QQAAO0IAADtEAAA7RQAAO4QAADuGAAA7iAAAO4oAADuMAAA7jQAAO44AADuPAAA7kAAAO5IAADuUAAA7lQAAO5YAADuYAAA7mQAAO9gAADvaAAA73AAAO94AADvgAAA74QAAO+IAADvjAAA75AAAO+YAADvoAAA76QAAO+oAADvsAAA77QAAPCwAADwuAAA8MAAAPDIAADw0AAA8NQAAPDYAADw3AAA8OAAAPDoAADw8AAA8PQAAPD4AADxAAAA8QQAAPIAAADyCAAA8hAAAPIYAADyIAAA8iQAAPIoAADyLAAA8jAAAPI4AADyQAAA8kQAAPJIAADyUAAA8lQAAPJ4AADyxAAA8vgAAPNEAADzeAAA88QAAPQgAAD0aAAA9ZQAAPYgAAD2oAAA9yAAAPcoAAD3MAAA9zgAAPdAAAD3SAAA90wAAPdQAAD3WAAA91wAAPdkAAD3aAAA93AAAPd4AAD3fAAA94AAAPeIAAD3jAAA97AAAPfkAAD3+AAA+AAAAPgIAAD4HAAA+CQAAPgsAAD4NAAA+IgAAPkcAAD5rAAA+kgAAPrYAAD64AAA+ugAAPrwAAD6+AAA+wAAAPsIAAD7DAAA+xQAAPtIAAD7jAAA+5QAAPucAAD7pAAA+6wAAPu0AAD7vAAA+8QAAPvMAAD8EAAA/BgAAPwgAAD8KAAA/DAAAPw4AAD8QAAA/EgAAPxQAAD8WAAA/VQAAP1cAAD9ZAAA/WwAAP10AAD9eAAA/XwAAP2AAAD9hAAA/YwAAP2UAAD9mAAA/ZwAAP2kAAD9qAAA/qQAAP6sAAD+tAAA/rwAAP7EAAD+yAAA/swAAP7QAAD+1AAA/twAAP7kAAD+6AAA/uwAAP70AAD++AAA//QAAP/8AAEABAABAAwAAQAUAAEAGAABABwAAQAgAAEAJAABACwAAQA0AAEAOAABADwAAQBEAAEASAABAHwAAQCAAAEAhAABAIwAAQGIAAEBkAABAZgAAQGgAAEBqAABAawAAQGwAAEBtAABAbgAAQHAAAEByAABAcwAAQHQAAEB2AABAdwAAQLYAAEC4AABAugAAQLwAAEC+AABAvwAAQMAAAEDBAABAwgAAQMQAAEDGAABAxwAAQMgAAEDKAABAywAAQQoAAEEMAABBDgAAQRAAAEESAABBEwAAQRQAAEEVAABBFgAAQRgAAEEaAABBGwAAQRwAAEEeAABBHwAAQV4AAEFgAABBYgAAQWQAAEFmAABBZwAAQWgAAEFpAABBagAAQWwAAEFuAABBbwAAQXAAAEFyAABBcwAAQbIAAEG0AABBtgAAQbgAAEG6AABBuwAAQbwAAEG9AABBvgAAQcAAAEHCAABBwwAAQcQAAEHGAABBxwAAQewAAEIQAABCNwAAQlsAAEJdAABCXwAAQmEAAEJjAABCZQAAQmcAAEJoAABCagAAQncAAEKGAABCiAAAQooAAEKMAABCjgAAQpAAAEKSAABClAAAQqMAAEKlAABCpwAAQqkAAEKrAABCrQAAQq8AAEKxAABCswAAQtMAAEL+AABDGAAAQzEAAENLAABDawAAQ44AAEPNAABDzwAAQ9EAAEPTAABD1QAAQ9YAAEPXAABD2AAAQ9kAAEPbAABD3QAAQ94AAEPfAABD4QAAQ+IAAEPkAABEIwAARCUAAEQnAABEKQAARCsAAEQsAABELQAARC4AAEQvAABEMQAARDMAAEQ0AABENQAARDcAAEQ4AABEdwAARHkAAER7AABEfQAARH8AAESAAABEgQAARIIAAESDAABEhQAARIcAAESIAABEiQAARIsAAESMAABEywAARM0AAETPAABE0QAARNMAAETUAABE1QAARNYAAETXAABE2QAARNsAAETcAABE3QAARN8AAETgAABE4gAARSEAAEUjAABFJQAARScAAEUpAABFKgAARSsAAEUsAABFLQAARS8AAEUxAABFMgAARTMAAEU1AABFNgAARXUAAEV3AABFeQAARXsAAEV9AABFfgAARX8AAEWAAABFgQAARYMAAEWFAABFhgAARYcAAEWJAABFigAARckAAEXLAABFzQAARc8AAEXRAABF0gAARdMAAEXUAABF1QAARdcAAEXZAABF2gAARdsAAEXdAABF3gAARecAAEX1AABGAgAARhAAAEYdAABGMAAARkcAAEZZAABGpAAARscAAEbnAABHBwAARwkAAEcLAABHDQAARw8AAEcRAABHEgAARxMAAEcVAABHFgAARxgAAEcZAABHGwAARx0AAEceAABHHwAARyEAAEciAABHJwAARzQAAEc5AABHOwAARz0AAEdCAABHRAAAR0YAAEdIAABHbQAAR5EAAEe4AABH3AAAR94AAEfgAABH4gAAR+QAAEfmAABH6AAAR+kAAEfrAABH+AAASAkAAEgLAABIDQAASA8AAEgRAABIEwAASBUAAEgXAABIGQAASCoAAEgsAABILgAASDAAAEgyAABINAAASDYAAEg4AABIOgAASDwAAEh7AABIfQAASH8AAEiBAABIgwAASIQAAEiFAABIhgAASIcAAEiJAABIiwAASIwAAEiNAABIjwAASJAAAEjPAABI0QAASNMAAEjVAABI1wAASNgAAEjZAABI2gAASNsAAEjdAABI3wAASOAAAEjhAABI4wAASOQAAEkjAABJJQAASScAAEkpAABJKwAASSwAAEktAABJLgAASS8AAEkxAABJMwAASTQAAEk1AABJNwAASTgAAElFAABJRgAASUcAAElJAABJiAAASYoAAEmMAABJjgAASZAAAEmRAABJkgAASZMAAEmUAABJlgAASZgAAEmZAABJmgAASZwAAEmdAABJ3AAASd4AAEngAABJ4gAASeQAAEnlAABJ5gAASecAAEnoAABJ6gAASewAAEntAABJ7gAASfAAAEnxAABKMAAASjIAAEo0AABKNgAASjgAAEo5AABKOgAASjsAAEo8AABKPgAASkAAAEpBAABKQgAASkQAAEpFAABKhAAASoYAAEqIAABKigAASowAAEqNAABKjgAASo8AAEqQAABKkgAASpQAAEqVAABKlgAASpgAAEqZAABK2AAAStoAAErcAABK3gAASuAAAErhAABK4gAASuMAAErkAABK5gAASugAAErpAABK6gAASuwAAErtAABLEgAASzYAAEtdAABLgQAAS4MAAEuFAABLhwAAS4kAAEuLAABLjQAAS44AAEuQAABLnQAAS6wAAEuuAABLsAAAS7IAAEu0AABLtgAAS7gAAEu6AABLyQAAS8sAAEvNAABLzwAAS9EAAEvTAABL1QAAS9cAAEvZAABMGAAATBoAAEwcAABMHgAATCAAAEwhAABMIgAATCMAAEwkAABMJgAATCgAAEwpAABMKgAATCwAAEwtAABMbAAATG4AAExwAABMcgAATHQAAEx1AABMdgAATHcAAEx4AABMegAATHwAAEx9AABMfgAATIAAAEyBAABMwAAATMIAAEzEAABMxgAATMgAAEzJAABMygAATMsAAEzMAABMzgAATNAAAEzRAABM0gAATNQAAEzVAABNFAAATRYAAE0YAABNGgAATRwAAE0dAABNHgAATR8AAE0gAABNIgAATSQAAE0lAABNJgAATSgAAE0pAABNLAAATWsAAE1tAABNbwAATXEAAE1zAABNdAAATXUAAE12AABNdwAATXkAAE17AABNfAAATX0AAE1/AABNgAAATb8AAE3BAABNwwAATcUAAE3HAABNyAAATckAAE3KAABNywAATc0AAE3PAABN0AAATdEAAE3TAABN1AAAThMAAE4VAABOFwAAThkAAE4bAABOHAAATh0AAE4eAABOHwAATiEAAE4jAABOJAAATiUAAE4nAABOKAAATnMAAE6WAABOtgAATtYAAE7YAABO2gAATtwAAE7eAABO4AAATuEAAE7iAABO5AAATuUAAE7nAABO6AAATuoAAE7sAABO7QAATu4AAE7wAABO8QAATvYAAE8DAABPCAAATwoAAE8MAABPEQAATxMAAE8VAABPFwAATzwAAE9gAABPhwAAT6sAAE+tAABPrwAAT7EAAE+zAABPtQAAT7cAAE+4AABPugAAT8cAAE/YAABP2gAAT9wAAE/eAABP4AAAT+IAAE/kAABP5gAAT+gAAE/5AABP+wAAT/0AAE//AABQAQAAUAMAAFAFAABQBwAAUAkAAFALAABQSgAAUEwAAFBOAABQUAAAUFIAAFBTAABQVAAAUFUAAFBWAABQWAAAUFoAAFBbAABQXAAAUF4AAFBfAABQngAAUKAAAFCiAABQpAAAUKYAAFCnAABQqAAAUKkAAFCqAABQrAAAUK4AAFCvAABQsAAAULIAAFCzAABQ8gAAUPQAAFD2AABQ+AAAUPoAAFD7AABQ/AAAUP0AAFD+AABRAAAAUQIAAFEDAABRBAAAUQYAAFEHAABRFAAAURUAAFEWAABRGAAAUVcAAFFZAABRWwAAUV0AAFFfAABRYAAAUWEAAFFiAABRYwAAUWUAAFFnAABRaAAAUWkAAFFrAABRbAAAUasAAFGtAABRrwAAUbEAAFGzAABRtAAAUbUAAFG2AABRtwAAUbkAAFG7AABRvAAAUb0AAFG/AABRwAAAUf8AAFIBAABSAwAAUgUAAFIHAABSCAAAUgkAAFIKAABSCwAAUg0AAFIPAABSEAAAUhEAAFITAABSFAAAUlMAAFJVAABSVwAAUlkAAFJbAABSXAAAUl0AAFJeAABSXwAAUmEAAFJjAABSZAAAUmUAAFJnAABSaAAAUqcAAFKpAABSqwAAUq0AAFKvAABSsAAAUrEAAFKyAABSswAAUrUAAFK3AABSuAAAUrkAAFK7AABSvAAAUuEAAFMFAABTLAAAU1AAAFNSAABTVAAAU1YAAFNYAABTWgAAU1wAAFNdAABTXwAAU2wAAFN7AABTfQAAU38AAFOBAABTgwAAU4UAAFOHAABTiQAAU5gAAFOaAABTnAAAU54AAFOgAABTogAAU6QAAFOmAABTqAAAU+cAAFPpAABT6wAAU+0AAFPvAABT8AAAU/EAAFPyAABT8wAAU/UAAFP3AABT+AAAU/kAAFP7AABT/AAAVDsAAFQ9AABUPwAAVEEAAFRDAABURAAAVEUAAFRGAABURwAAVEkAAFRLAABUTAAAVE0AAFRPAABUUAAAVI8AAFSRAABUkwAAVJUAAFSXAABUmAAAVJkAAFSaAABUmwAAVJ0AAFSfAABUoAAAVKEAAFSjAABUpAAAVOMAAFTlAABU5wAAVOkAAFTrAABU7AAAVO0AAFTuAABU7wAAVPEAAFTzAABU9AAAVPUAAFT3AABU+AAAVPsAAFU6AABVPAAAVT4AAFVAAABVQgAAVUMAAFVEAABVRQAAVUYAAFVIAABVSgAAVUsAAFVMAABVTgAAVU8AAFWOAABVkAAAVZIAAFWUAABVlgAAVZcAAFWYAABVmQAAVZoAAFWcAABVngAAVZ8AAFWgAABVogAAVaMAAFXiAABV5AAAVeYAAFXoAABV6gAAVesAAFXsAABV7QAAVe4AAFXwAABV8gAAVfMAAFX0AABV9gAAVfcAAFZCAABWZQAAVoUAAFalAABWpwAAVqkAAFarAABWrQAAVq8AAFawAABWsQAAVrQAAFa1AABWtwAAVrgAAFa6AABWvAAAVr0AAFa+AABWwQAAVsIAAFbHAABW1AAAVtkAAFbbAABW3QAAVuIAAFblAABW6AAAVuoAAFcPAABXMwAAV1oAAFd+AABXgAAAV4IAAFeEAABXhgAAV4gAAFeKAABXiwAAV44AAFebAABXrAAAV64AAFewAABXsgAAV7QAAFe2AABXuAAAV7oAAFe8AABXzQAAV9AAAFfTAABX1gAAV9kAAFfcAABX3wAAV+IAAFflAABX5wAAWCYAAFgoAABYKgAAWCwAAFgvAABYMAAAWDEAAFgyAABYMwAAWDUAAFg3AABYOAAAWDkAAFg7AABYPAAAWHsAAFh9AABYfwAAWIEAAFiEAABYhQAAWIYAAFiHAABYiAAAWIoAAFiMAABYjQAAWI4AAFiQAABYkQAAWNAAAFjSAABY1QAAWNcAAFjaAABY2wAAWNwAAFjdAABY3gAAWOAAAFjiAABY4wAAWOQAAFjmAABY5wAAWPQAAFj1AABY9gAAWPgAAFk3AABZOQAAWTsAAFk9AABZQAAAWUEAAFlCAABZQwAAWUQAAFlGAABZSAAAWUkAAFlKAABZTAAAWU0AAFmMAABZjgAAWZAAAFmSAABZlQAAWZYAAFmXAABZmAAAWZkAAFmbAABZnQAAWZ4AAFmfAABZoQAAWaIAAFnhAABZ4wAAWeUAAFnnAABZ6gAAWesAAFnsAABZ7QAAWe4AAFnwAABZ8gAAWfMAAFn0AABZ9gAAWfcAAFo2AABaOAAAWjoAAFo8AABaPwAAWkAAAFpBAABaQgAAWkMAAFpFAABaRwAAWkgAAFpJAABaSwAAWkwAAFqLAABajQAAWo8AAFqRAABalAAAWpUAAFqWAABalwAAWpgAAFqaAABanAAAWp0AAFqeAABaoAAAWqEAAFrGAABa6gAAWxEAAFs1AABbNwAAWzkAAFs7AABbPQAAWz8AAFtBAABbQgAAW0UAAFtSAABbYQAAW2MAAFtlAABbZwAAW2kAAFtrAABbbQAAW28AAFt+AABbgQAAW4QAAFuHAABbigAAW40AAFuQAABbkwAAW5UAAFvUAABb1gAAW9gAAFvaAABb3QAAW94AAFvfAABb4AAAW+EAAFvjAABb5QAAW+YAAFvnAABb6QAAW+oAAFwpAABcKwAAXC0AAFwvAABcMgAAXDMAAFw0AABcNQAAXDYAAFw4AABcOgAAXDsAAFw8AABcPgAAXD8AAFx+AABcgAAAXIIAAFyEAABchwAAXIgAAFyJAABcigAAXIsAAFyNAABcjwAAXJAAAFyRAABckwAAXJQAAFzTAABc1QAAXNgAAFzaAABc3QAAXN4AAFzfAABc4AAAXOEAAFzjAABc5QAAXOYAAFznAABc6QAAXOoAAFztAABdLAAAXS4AAF0wAABdMgAAXTUAAF02AABdNwAAXTgAAF05AABdOwAAXT0AAF0+AABdPwAAXUEAAF1CAABdgQAAXYMAAF2GAABdiAAAXYsAAF2MAABdjQAAXY4AAF2PAABdkQAAXZMAAF2UAABdlQAAXZcAAF2YAABdvwAAXf4AAF4AAABeAgAAXgQAAF4HAABeCAAAXgkAAF4KAABeCwAAXg0AAF4PAABeEAAAXhEAAF4TAABeFAAAXl8AAF6CAABeogAAXsIAAF7EAABexgAAXsgAAF7KAABezAAAXs0AAF7OAABe0QAAXtIAAF7UAABe1QAAXtcAAF7ZAABe2gAAXtsAAF7eAABe3wAAXugAAF71AABe+gAAXvwAAF7+AABfAwAAXwYAAF8JAABfCwAAXzAAAF9UAABfewAAX58AAF+iAABfpAAAX6YAAF+oAABfqgAAX6wAAF+tAABfsAAAX70AAF/OAABf0AAAX9IAAF/UAABf1gAAX9gAAF/aAABf3AAAX94AAF/vAABf8gAAX/UAAF/4AABf+wAAX/4AAGABAABgBAAAYAcAAGAJAABgSAAAYEoAAGBMAABgTgAAYFEAAGBSAABgUwAAYFQAAGBVAABgVwAAYFkAAGBaAABgWwAAYF0AAGBeAABgnQAAYJ8AAGChAABgowAAYKYAAGCnAABgqAAAYKkAAGCqAABgrAAAYK4AAGCvAABgsAAAYLIAAGCzAABg8gAAYPQAAGD3AABg+QAAYPwAAGD9AABg/gAAYP8AAGEAAABhAgAAYQQAAGEFAABhBgAAYQgAAGEJAABhFgAAYRcAAGEYAABhGgAAYVkAAGFbAABhXQAAYV8AAGFiAABhYwAAYWQAAGFlAABhZgAAYWgAAGFqAABhawAAYWwAAGFuAABhbwAAYa4AAGGwAABhsgAAYbQAAGG3AABhuAAAYbkAAGG6AABhuwAAYb0AAGG/AABhwAAAYcEAAGHDAABhxAAAYgMAAGIFAABiBwAAYgkAAGIMAABiDQAAYg4AAGIPAABiEAAAYhIAAGIUAABiFQAAYhYAAGIYAABiGQAAYlgAAGJaAABiXAAAYl4AAGJhAABiYgAAYmMAAGJkAABiZQAAYmcAAGJpAABiagAAYmsAAGJtAABibgAAYq0AAGKvAABisQAAYrMAAGK2AABitwAAYrgAAGK5AABiugAAYrwAAGK+AABivwAAYsAAAGLCAABiwwAAYugAAGMMAABjMwAAY1cAAGNaAABjXAAAY14AAGNgAABjYgAAY2QAAGNlAABjaAAAY3UAAGOEAABjhgAAY4gAAGOKAABjjAAAY44AAGOQAABjkgAAY6EAAGOkAABjpwAAY6oAAGOtAABjsAAAY7MAAGO2AABjuAAAY/cAAGP5AABj/AAAY/4AAGQBAABkAgAAZAMAAGQEAABkBQAAZAcAAGQJAABkCgAAZAsAAGQNAABkDgAAZBEAAGRQAABkUgAAZFQAAGRWAABkWQAAZFoAAGRbAABkXAAAZF0AAGRfAABkYQAAZGIAAGRjAABkZQAAZGYAAGSlAABkpwAAZKkAAGSrAABkrgAAZK8AAGSwAABksQAAZLIAAGS0AABktgAAZLcAAGS4AABkugAAZLsAAGT6AABk/AAAZP8AAGUBAABlBAAAZQUAAGUGAABlBwAAZQgAAGUKAABlDAAAZQ0AAGUOAABlEAAAZREAAGUUAABlUwAAZVUAAGVXAABlWQAAZVwAAGVdAABlXgAAZV8AAGVgAABlYgAAZWQAAGVlAABlZgAAZWgAAGVpAABlqAAAZaoAAGWsAABlrgAAZbEAAGWyAABlswAAZbQAAGW1AABltwAAZbkAAGW6AABluwAAZb0AAGW+AABl/QAAZf8AAGYBAABmAwAAZgYAAGYHAABmCAAAZgkAAGYKAABmDAAAZg4AAGYPAABmEAAAZhIAAGYTAABmXgAAZoEAAGahAABmwQAAZsMAAGbFAABmxwAAZskAAGbLAABmzAAAZs0AAGbQAABm0QAAZtMAAGbUAABm1gAAZtgAAGbZAABm2gAAZt0AAGbeAABm4wAAZvAAAGb1AABm9wAAZvkAAGb+AABnAQAAZwQAAGcGAABnKwAAZ08AAGd2AABnmgAAZ50AAGefAABnoQAAZ6MAAGelAABnpwAAZ6gAAGerAABnuAAAZ8kAAGfLAABnzQAAZ88AAGfRAABn0wAAZ9UAAGfXAABn2QAAZ+oAAGftAABn8AAAZ/MAAGf2AABn+QAAZ/wAAGf/AABoAgAAaAQAAGhDAABoRQAAaEcAAGhJAABoTAAAaE0AAGhOAABoTwAAaFAAAGhSAABoVAAAaFUAAGhWAABoWAAAaFkAAGiYAABomgAAaJwAAGieAABooQAAaKIAAGijAABopAAAaKUAAGinAABoqQAAaKoAAGirAABorQAAaK4AAGjtAABo7wAAaPIAAGj0AABo9wAAaPgAAGj5AABo+gAAaPsAAGj9AABo/wAAaQAAAGkBAABpAwAAaQQAAGkRAABpEgAAaRMAAGkVAABpVAAAaVYAAGlYAABpWgAAaV0AAGleAABpXwAAaWAAAGlhAABpYwAAaWUAAGlmAABpZwAAaWkAAGlqAABpqQAAaasAAGmtAABprwAAabIAAGmzAABptAAAabUAAGm2AABpuAAAaboAAGm7AABpvAAAab4AAGm/AABp/gAAagAAAGoCAABqBAAAagcAAGoIAABqCQAAagoAAGoLAABqDQAAag8AAGoQAABqEQAAahMAAGoUAABqUwAAalUAAGpXAABqWQAAalwAAGpdAABqXgAAal8AAGpgAABqYgAAamQAAGplAABqZgAAamgAAGppAABqqAAAaqoAAGqsAABqrgAAarEAAGqyAABqswAAarQAAGq1AABqtwAAarkAAGq6AABquwAAar0AAGq+AABq4wAAawcAAGsuAABrUgAAa1UAAGtXAABrWQAAa1sAAGtdAABrXwAAa2AAAGtjAABrcAAAa38AAGuBAABrgwAAa4UAAGuHAABriQAAa4sAAGuNAABrnAAAa58AAGuiAABrpQAAa6gAAGurAABrrgAAa7EAAGuzAABr8gAAa/QAAGv2AABr+AAAa/sAAGv8AABr/QAAa/4AAGv/AABsAQAAbAMAAGwEAABsBQAAbAcAAGwIAABsRwAAbEkAAGxLAABsTQAAbFAAAGxRAABsUgAAbFMAAGxUAABsVgAAbFgAAGxZAABsWgAAbFwAAGxdAABsnAAAbJ4AAGygAABsogAAbKUAAGymAABspwAAbKgAAGypAABsqwAAbK0AAGyuAABsrwAAbLEAAGyyAABs8QAAbPMAAGz1AABs9wAAbPoAAGz7AABs/AAAbP0AAGz+AABtAAAAbQIAAG0DAABtBAAAbQYAAG0HAABtRgAAbUgAAG1KAABtTAAAbU8AAG1QAABtUQAAbVIAAG1TAABtVQAAbVcAAG1YAABtWQAAbVsAAG1cAABtmwAAbZ0AAG2fAABtoQAAbaQAAG2lAABtpgAAbacAAG2oAABtqgAAbawAAG2tAABtrgAAbbAAAG2xAABt8AAAbfIAAG30AABt9gAAbfkAAG36AABt+wAAbfwAAG39AABt/wAAbgEAAG4CAABuAwAAbgUAAG4GAABuUQAAbnQAAG6UAAButAAAbrYAAG64AABuugAAbrwAAG6+AABuvwAAbsAAAG7DAABuxAAAbsYAAG7HAABuyQAAbssAAG7MAABuzQAAbtAAAG7RAABu2gAAbucAAG7sAABu7gAAbvAAAG71AABu+AAAbvsAAG79AABvIgAAb0YAAG9tAABvkQAAb5QAAG+WAABvmAAAb5oAAG+cAABvngAAb58AAG+iAABvrwAAb8AAAG/CAABvxAAAb8YAAG/IAABvygAAb8wAAG/OAABv0AAAb+EAAG/kAABv5wAAb+oAAG/tAABv8AAAb/MAAG/2AABv+QAAb/sAAHA6AABwPAAAcD4AAHBAAABwQwAAcEQAAHBFAABwRgAAcEcAAHBJAABwSwAAcEwAAHBNAABwTwAAcFAAAHCPAABwkQAAcJMAAHCVAABwmAAAcJkAAHCaAABwmwAAcJwAAHCeAABwoAAAcKEAAHCiAABwpAAAcKUAAHDkAABw5gAAcOkAAHDrAABw7gAAcO8AAHDwAABw8QAAcPIAAHD0AABw9gAAcPcAAHD4AABw+gAAcPsAAHEIAABxCQAAcQoAAHEMAABxSwAAcU0AAHFPAABxUQAAcVQAAHFVAABxVgAAcVcAAHFYAABxWgAAcVwAAHFdAABxXgAAcWAAAHFhAABxoAAAcaIAAHGkAABxpgAAcakAAHGqAABxqwAAcawAAHGtAABxrwAAcbEAAHGyAABxswAAcbUAAHG2AABx9QAAcfcAAHH5AABx+wAAcf4AAHH/AAByAAAAcgEAAHICAAByBAAAcgYAAHIHAAByCAAAcgoAAHILAABySgAAckwAAHJOAAByUAAAclMAAHJUAAByVQAAclYAAHJXAAByWQAAclsAAHJcAAByXQA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AiNUAAIjiAACI4wAAiOQAAIjmAACJJQAAiScAAIkpAACJKwAAiS4AAIkvAACJMAAAiTEAAIkyAACJNAAAiTYAAIk3AACJOAAAiToAAIk7AACJegAAiXwAAIl+AACJgAAAiYMAAImEAACJhQAAiYYAAImHAACJiQAAiYsAAImMAACJjQAAiY8AAImQAACJzwAAidEAAInTAACJ1QAAidgAAInZAACJ2gAAidsAAIncAACJ3gAAieAAAInhAACJ4gAAieQAAInlAACKJAAAiiYAAIooAACKKgAAii0AAIouAACKLwAAijAAAIoxAACKMwAAijUAAIo2AACKNwAAijkAAIo6AACKeQAAinsAAIp9AACKfwAAioIAAIqDAACKhAAAioUAAIqGAACKiAAAiooAAIqLAACKjAAAio4AAIqPAACKtAAAitgAAIr/AACLIwAAiyYAAIsoAACLKgAAiywAAIsuAACLMAAAizEAAIs0AACLQQAAi1AAAItSAACLVAAAi1YAAItYAACLWgAAi1wAAIteAACLbQAAi3AAAItzAACLdgAAi3kAAIt8AACLfwAAi4IAAIuEAACLwwAAi8UAAIvHAACLyQAAi8wAAIvNAACLzgAAi88AAIvQAACL0gAAi9QAAIvVAACL1gAAi9gAAIvZAACMGAAAjBoAAIwcAACMHgAAjCEAAIwiAACMIwAAjCQAAIwlAACMJwAAjCkAAIwqAACMKwAAjC0AAIwuAACMbQAAjG8AAIxxAACMcwAAjHYAAIx3AACMeAAAjHkAAIx6AACMfAAAjH4AAIx/AACMgAAAjIIAAIyDAACMwgAAjMQAAIzGAACMyAAAjMsAAIzMAACMzQAAjM4AAIzPAACM0QAAjNMAAIzUAACM1QAAjNcAAIzYAACNFwAAjRkAAI0bAACNHQAAjSAAAI0hAACNIgAAjSMAAI0kAACNJgAAjSgAAI0pAACNKgAAjSwAAI0tAACNbAAAjW4AAI1wAACNcgAAjXUAAI12AACNdwAAjXgAAI15AACNewAAjX0AAI1+AACNfwAAjYEAAI2CAACNwQAAjcMAAI3FAACNxwAAjcoAAI3LAACNzAAAjc0AAI3OAACN0AAAjdIAAI3TAACN1AAAjdYAAI3XAACOIgAAjkUAAI5lAACOhQAAjocAAI6JAACOiwAAjo0AAI6PAACOkAAAjpEAAI6UAACOlQAAjpcAAI6YAACOmgAAjpwAAI6dAACOngAAjqEAAI6iAACOpwAAjrQAAI65AACOuwAAjr0AAI7CAACOxQAAjsgAAI7KAACO7wAAjxMAAI86AACPXgAAj2EAAI9jAACPZQAAj2cAAI9pAACPawAAj2wAAI9vAACPfAAAj40AAI+PAACPkQAAj5MAAI+VAACPlwAAj5kAAI+bAACPnQAAj64AAI+xAACPtAAAj7cAAI+6AACPvQAAj8AAAI/DAACPxgAAj8gAAJAHAACQCQAAkAsAAJANAACQEAAAkBEAAJASAACQEwAAkBQAAJAWAACQGAAAkBkAAJAaAACQHAAAkB0AAJBcAACQXgAAkGAAAJBiAACQZQAAkGYAAJBnAACQaAAAkGkAAJBrAACQbQAAkG4AAJBvAACQcQAAkHIAAJCxAACQswAAkLYAAJC4AACQuwAAkLwAAJC9AACQvgAAkL8AAJDBAACQwwAAkMQAAJDFAACQxwAAkMgAAJDVAACQ1gAAkNcAAJDZAACRGAAAkRoAAJEcAACRHgAAkSEAAJEiAACRIwAAkSQAAJElAACRJwAAkSkAAJEqAACRKwAAkS0AAJEuAACRbQAAkW8AAJFxAACRcwAAkXYAAJF3AACReAAAkXkAAJF6AACRfAAAkX4AAJF/AACRgAAAkYIAAJGDAACRwgAAkcQAAJHGAACRyAAAkcsAAJHMAACRzQAAkc4AAJHPAACR0QAAkdMAAJHUAACR1QAAkdcAAJHYAACSFwAAkhkAAJIbAACSHQAAkiAAAJIhAACSIgAAkiMAAJIkAACSJgAAkigAAJIpAACSKgAAkiwAAJItAACSbAAAkm4AAJJwAACScgAAknUAAJJ2AACSdwAAkngAAJJ5AACSewAAkn0AAJJ+AACSfwAAkoEAAJKCAACSpwAAkssAAJLyAACTFgAAkxkAAJMbAACTHQAAkx8AAJMhAACTIwAAkyQAAJMnAACTNAAAk0MAAJNFAACTRwAAk0kAAJNLAACTTQAAk08AAJNRAACTYAAAk2MAAJNmAACTaQAAk2wAAJNvAACTcgAAk3UAAJN3AACTtgAAk7gAAJO6AACTvAAAk78AAJPAAACTwQAAk8IAAJPDAACTxQAAk8cAAJPIAACTyQAAk8sAAJPMAACUCwAAlA0AAJQPAACUEQAAlBQAAJQVAACUFgAAlBcAAJQYAACUGgAAlBwAAJQdAACUHgAAlCAAAJQhAACUYAAAlGIAAJRkAACUZgAAlGkAAJRqAACUawAAlGwAAJRtAACUbwAAlHEAAJRyAACUcwAAlHUAAJR2AACUtQAAlLcAAJS6AACUvAAAlL8AAJTAAACUwQAAlMIAAJTDAACUxQAAlMcAAJTIAACUyQAAlMsAAJTMAACVDwAAlTMAAJVXAACVegAAlaEAAJXBAACV6AAAlg8AAJYvAACWUwAAlncAAJZ5AACWfAAAln4AAJaAAACWggAAloUAAJaIAACWigAAlowAAJaPAACWkQAAlpMAAJaWAACWmQAAlpoAAJafAACWrAAAlq8AAJaxAACWtAAAlrcAAJa5AACW3gAAlwIAAJcpAACXTQAAl1AAAJdSAACXVAAAl1YAAJdYAACXWgAAl1sAAJdeAACXawAAl34AAJeAAACXggAAl4QAAJeGAACXiAAAl4oAAJeMAACXjgAAl5AAAJejAACXpgAAl6kAAJesAACXrwAAl7IAAJe1AACXuAAAl7sAAJe+AACXwAAAl/8AAJgBAACYBAAAmAYAAJgJAACYCgAAmAsAAJgMAACYDQAAmA8AAJgRAACYEgAAmBMAAJgVAACYFgAAmB8AAJggAACYIgAAmGEAAJhjAACYZQAAmGcAAJhqAACYawAAmGwAAJhtAACYbgAAmHAAAJhyAACYcwAAmHQAAJh2AACYdwAAmLYAAJi4AACYuwAAmL0AAJjAAACYwQAAmMIAAJjDAACYxAAAmMYAAJjIAACYyQAAmMoAAJjMAACYzQAAmNYAAJjXAACY2QAAmRgAAJkaAACZHAAAmR4AAJkhAACZIgAAmSMAAJkkAACZJQAAmScAAJkpAACZKgAAmSsAAJktAACZLgAAmW0AAJlvAACZcgAAmXQAAJl3AACZeAAAmXkAAJl6AACZewAAmX0AAJl/AACZgAAAmYEAAJmDAACZhAAAmY0AAJmOAACZkAAAmc8AAJnRAACZ0wAAmdUAAJnYAACZ2QAAmdoAAJnbAACZ3AAAmd4AAJngAACZ4QAAmeIAAJnkAACZ5QAAmiQAAJomAACaKQAAmisAAJouAACaLwAAmjAAAJoxAACaMgAAmjQAAJo2AACaNwAAmjgAAJo6AACaOwAAmkgAAJpJAACaSgAAmkwAAJqLAACajQAAmo8AAJqRAACalAAAmpUAAJqWAACalwAAmpgAAJqaAACanAAAmp0AAJqeAACaoAAAmqEAAJrgAACa4gAAmuUAAJrnAACa6gAAmusAAJrsAACa7QAAmu4AAJrwAACa8gAAmvMAAJr0AACa9gAAmvcAAJsEAACbEQAAmyIAAJslAACbKAAAmysAAJstAACbMAAAmzIAAJs1AACbOAAAm0kAAJtMAACbTwAAm1IAAJtVAACbWAAAm1sAAJteAACbYQAAm2MAAJtrAACbcwAAm3wAAJuQAACblgAAm6AAAJvrAACcDgAAnC4AAJxOAACcUAAAnFIAAJxUAACcVgAAnFkAAJxaAACcWwAAnF4AAJxfAACcYQAAnGIAAJxkAACcZwAAnGgAAJxpAACcbAAAnG0AAJxyAACcfwAAnIQAAJyGAACciAAAnI0AAJyQAACckwAAnJUAAJy6AACc3gAAnQUAAJ0pAACdLAAAnS4AAJ0wAACdMgAAnTQAAJ02AACdNwAAnToAAJ1HAACdWAAAnVoAAJ1cAACdXgAAnWAAAJ1iAACdZAAAnWYAAJ1oAACdeQAAnXwAAJ1/AACdggAAnYUAAJ2IAACdiwAAnY4AAJ2RAACdkwAAndIAAJ3UAACd1gAAndgAAJ3bAACd3AAAnd0AAJ3eAACd3wAAneEAAJ3jAACd5AAAneUAAJ3nAACd6AAAnicAAJ4pAACeKwAAni0AAJ4wAACeMQAAnjIAAJ4zAACeNAAAnjYAAJ44AACeOQAAnjoAAJ48AACePQAAnnwAAJ5+AACegQAAnoMAAJ6GAACehwAAnogAAJ6JAACeigAAnowAAJ6OAACejwAAnpAAAJ6SAACekwAAnqAAAJ6hAACeogAAnqQAAJ7jAACe5QAAnucAAJ7pAACe7AAAnu0AAJ7uAACe7wAAnvAAAJ7yAACe9AAAnvUAAJ72AACe+AAAnvkAAJ84AACfOgAAnzwAAJ8+AACfQQAAn0IAAJ9DAACfRAAAn0UAAJ9HAACfSQAAn0oAAJ9LAACfTQAAn04AAJ+NAACfjwAAn5EAAJ+TAACflgAAn5cAAJ+YAACfmQAAn5oAAJ+cAACfngAAn58AAJ+gAACfogAAn6MAAJ/iAACf5AAAn+YAAJ/oAACf6wAAn+wAAJ/tAACf7gAAn+8AAJ/xAACf8wAAn/QAAJ/1AACf9wAAn/gAAKA3AACgOQAAoDsAAKA9AACgQAAAoEEAAKBCAACgQwAAoEQAAKBGAACgSAAAoEkAAKBKAACgTAAAoE0AAKByAACglgAAoL0AAKDhAACg5AAAoOYAAKDoAACg6gAAoOwAAKDuAACg7wAAoPIAAKD/AAChDgAAoRAAAKESAAChFAAAoRYAAKEYAAChGgAAoRwAAKErAAChLgAAoTEAAKE0AAChNwAAoToAAKE9AAChQAAAoUIAAKGBAAChgwAAoYUAAKGHAAChigAAoYsAAKGMAAChjQAAoY4AAKGQAAChkgAAoZMAAKGUAAChlgAAoZcAAKHWAACh2AAAodoAAKHcAACh3wAAoeAAAKHhAACh4gAAoeMAAKHlAACh5wAAoegAAKHpAACh6wAAoewAAKIrAACiLQAAoi8AAKIxAACiNAAAojUAAKI2AACiNwAAojgAAKI6AACiPAAAoj0AAKI+AACiQAAAokEAAKKAAACiggAAooQAAKKGAACiiQAAoooAAKKLAACijAAAoo0AAKKPAACikQAAopIAAKKTAACilQAAopYAAKLVAACi1wAAotkAAKLbAACi3gAAot8AAKLgAACi4QAAouIAAKLkAACi5gAAoucAAKLoAACi6gAAousAAKMqAACjLAAAoy4AAKMwAACjMwAAozQAAKM1AACjNgAAozcAAKM5AACjOwAAozwAAKM9AACjPwAAo0AAAKN/AACjgQAAo4MAAKOFAACjiAAAo4kAAKOKAACjiwAAo4wAAKOOAACjkAAAo5EAAKOSAACjlAAAo5UAAKPgAACkAwAApCMAAKRDAACkRQAApEcAAKRJAACkSwAApE4AAKRPAACkUAAApFMAAKRUAACkVgAApFcAAKRZAACkXAAApF0AAKReAACkYQAApGIAAKRnAACkdAAApHkAAKR7AACkfQAApIIAAKSFAACkiAAApIoAAKSvAACk0wAApPoAAKUeAAClIQAApSMAAKUlAAClJwAApSkAAKUrAAClLAAApS8AAKU8AAClTQAApU8AAKVRAAClUwAApVUAAKVXAAClWQAApVsAAKVdAAClbgAApXEAAKV0AACldwAApXoAAKV9AAClgAAApYMAAKWGAACliAAApccAAKXJAAClywAApc0AAKXQAACl0QAApdIAAKXTAACl1AAApdYAAKXYAACl2QAApdoAAKXcAACl3QAAphwAAKYeAACmIAAApiIAAKYlAACmJgAApicAAKYoAACmKQAApisAAKYtAACmLgAApi8AAKYxAACmMgAApnEAAKZzAACmdgAApngAAKZ7AACmfAAApn0AAKZ+AACmfwAApoEAAKaDAACmhAAApoUAAKaHAACmiAAAppUAAKaWAACmlwAAppkAAKbYAACm2gAAptwAAKbeAACm4QAApuIAAKbjAACm5AAApuUAAKbnAACm6QAApuoAAKbrAACm7QAApu4AAKctAACnLwAApzEAAKczAACnNgAApzcAAKc4AACnOQAApzoAAKc8AACnPgAApz8AAKdAAACnQgAAp0MAAKeCAACnhAAAp4YAAKeIAACniwAAp4wAAKeNAACnjgAAp48AAKeRAACnkwAAp5QAAKeVAACnlwAAp5gAAKfXAACn2QAAp9sAAKfdAACn4AAAp+EAAKfiAACn4wAAp+QAAKfmAACn6AAAp+kAAKfqAACn7AAAp+0AAKgsAACoLgAAqDAAAKgyAACoNQAAqDYAAKg3AACoOAAAqDkAAKg7AACoPQAAqD4AAKg/AACoQQAAqEIAAKhnAACoiwAAqLIAAKjWAACo2QAAqNsAAKjdAACo3wAAqOEAAKjjAACo5AAAqOcAAKj0AACpAwAAqQUAAKkHAACpCQAAqQsAAKkNAACpDwAAqREAAKkgAACpIwAAqSYAAKkpAACpLAAAqS8AAKkyAACpNQAAqTcAAKl2AACpeAAAqXoAAKl8AACpfwAAqYAAAKmBAACpggAAqYMAAKmFAACphwAAqYgAAKmJAACpiwAAqYwAAKnLAACpzQAAqc8AAKnRAACp1AAAqdUAAKnWAACp1wAAqdgAAKnaAACp3AAAqd0AAKneAACp4AAAqeEAAKogAACqIgAAqiQAAKomAACqKQAAqioAAKorAACqLAAAqi0AAKovAACqMQAAqjIAAKozAACqNQAAqjYAAKp1AACqdwAAqnkAAKp7AACqfgAAqn8AAKqAAACqgQAAqoIAAKqEAACqhgAAqocAAKqIAACqigAAqosAAKrKAACqzAAAqs4AAKrQAACq0wAAqtQAAKrVAACq1gAAqtcAAKrZAACq2wAAqtwAAKrdAACq3wAAquAAAKsfAACrIQAAqyMAAKslAACrKAAAqykAAKsqAACrKwAAqywAAKsuAACrMAAAqzEAAKsyAACrNAAAqzUAAKt0AACrdgAAq3gAAKt6AACrfQAAq34AAKt/AACrgAAAq4EAAKuDAACrhQAAq4YAAKuHAACriQAAq4oAAKvVAACr+AAArBgAAKw4AACsOgAArDwAAKw+AACsQAAArEMAAKxEAACsRQAArEgAAKxJAACsSwAArEwAAKxOAACsUQAArFIAAKxTAACsVgAArFcAAKxcAACsaQAArG4AAKxwAACscgAArHcAAKx6AACsfQAArH8AAKykAACsyAAArO8AAK0TAACtFgAArRgAAK0aAACtHAAArR4AAK0gAACtIQAArSQAAK0xAACtQgAArUQAAK1GAACtSAAArUoAAK1MAACtTgAArVAAAK1SAACtYwAArWYAAK1pAACtbAAArW8AAK1yAACtdQAArXgAAK17AACtfQAArbwAAK2+AACtwAAArcIAAK3FAACtxgAArccAAK3IAACtyQAArcsAAK3NAACtzgAArc8AAK3RAACt0gAArhEAAK4TAACuFQAArhcAAK4aAACuGwAArhwAAK4dAACuHgAAriAAAK4iAACuIwAAriQAAK4mAACuJwAArmYAAK5oAACuawAArm0AAK5wAACucQAArnIAAK5zAACudAAArnYAAK54AACueQAArnoAAK58AACufQAArooAAK6LAACujAAAro4AAK7NAACuzwAArtEAAK7TAACu1gAArtcAAK7YAACu2QAArtoAAK7cAACu3gAArt8AAK7gAACu4gAAruMAAK8iAACvJAAAryYAAK8oAACvKwAArywAAK8tAACvLgAAry8AAK8xAACvMwAArzQAAK81AACvNwAArzgAAK93AACveQAAr3sAAK99AACvgAAAr4EAAK+CAACvgwAAr4QAAK+GAACviAAAr4kAAK+KAACvjAAAr40AAK/MAACvzgAAr9AAAK/SAACv1QAAr9YAAK/XAACv2AAAr9kAAK/bAACv3QAAr94AAK/fAACv4QAAr+IAALAhAACwIwAAsCUAALAnAACwKgAAsCsAALAsAACwLQAAsC4AALAwAACwMgAAsDMAALA0AACwNgAAsDcAALBcAACwgAAAsKcAALDLAACwzgAAsNAAALDSAACw1AAAsNYAALDYAACw2QAAsNwAALDpAACw+AAAsPoAALD8AACw/gAAsQAAALECAACxBAAAsQYAALEVAACxGAAAsRsAALEeAACxIQAAsSQAALEnAACxKgAAsSwAALFrAACxbQAAsW8AALFxAACxdAAAsXUAALF2AACxdwAAsXgAALF6AACxfAAAsX0AALF+AACxgAAAsYEAALHAAACxwgAAscQAALHGAACxyQAAscoAALHLAACxzAAAsc0AALHPAACx0QAAsdIAALHTAACx1QAAsdYAALIVAACyFwAAshkAALIbAACyHgAAsh8AALIgAACyIQAAsiIAALIkAACyJgAAsicAALIoAACyKgAAsisAALJqAACybAAAsm8AALJxAACydAAAsnUAALJ2AACydwAAsngAALJ6AACyfAAAsn0AALJ+AACygAAAsoEAALLAAACywgAAssQAALLGAACyyQAAssoAALLLAACyzAAAss0AALLPAACy0QAAstIAALLTAACy1QAAstYAALMVAACzFwAAsxkAALMbAACzHgAAsx8AALMgAACzIQAAsyIAALMkAACzJgAAsycAALMoAACzKgAAsysAALNqAACzbAAAs24AALNwAACzcwAAs3QAALN1AACzdgAAs3cAALN5AACzewAAs3wAALN9AACzfwAAs4AAALPLAACz7gAAtA4AALQuAAC0MAAAtDIAALQ0AAC0NgAAtDkAALQ6AAC0OwAAtD4AALQ/AAC0QQAAtEIAALREAAC0RgAAtEcAALRIAAC0SwAAtEwAALRRAAC0XgAAtGMAALRlAAC0ZwAAtGwAALRvAAC0cgAAtHQAALSZAAC0vQAAtOQAALUIAAC1CwAAtQ0AALUPAAC1EQAAtRMAALUVAAC1FgAAtRkAALUmAAC1NwAAtTkAALU7AAC1PQAAtT8AALVBAAC1QwAAtUUAALVHAAC1WAAAtVsAALVeAAC1YQAAtWQAALVnAAC1agAAtW0AALVwAAC1cgAAtbEAALWzAAC1tQAAtbcAALW6AAC1uwAAtbwAALW9AAC1vgAAtcAAALXCAAC1wwAAtcQAALXGAAC1xwAAtgYAALYIAAC2CgAAtgwAALYPAAC2EAAAthEAALYSAAC2EwAAthUAALYXAAC2GAAAthkAALYbAAC2HAAAtlsAALZdAAC2YAAAtmIAALZlAAC2ZgAAtmcAALZoAAC2aQAAtmsAALZtAAC2bgAAtm8AALZxAAC2cgAAtn8AALaAAAC2gQAAtoMAALbCAAC2xAAAtsYAALbIAAC2ywAAtswAALbNAAC2zgAAts8AALbRAAC20wAAttQAALbVAAC21wAAttgAALcXAAC3GQAAtxsAALcdAAC3IAAAtyEAALciAAC3IwAAtyQAALcmAAC3KAAAtykAALcqAAC3LAAAty0AALdsAAC3bgAAt3AAALdyAAC3dQAAt3YAALd3AAC3eAAAt3kAALd7AAC3fQAAt34AALd/AAC3gQAAt4IAALfBAAC3wwAAt8UAALfHAAC3ygAAt8sAALfMAAC3zQAAt84AALfQAAC30gAAt9MAALfUAAC31gAAt9cAALgWAAC4GAAAuBoAALgcAAC4HwAAuCAAALghAAC4IgAAuCMAALglAAC4JwAAuCgAALgpAAC4KwAAuCwAALhRAAC4dQAAuJwAALjAAAC4wwAAuMUAALjHAAC4yQAAuMsAALjNAAC4zgAAuNEAALjeAAC47QAAuO8AALjxAAC48wAAuPUAALj3AAC4+QAAuPsAALkKAAC5DQAAuRAAALkTAAC5FgAAuRkAALkcAAC5HwAAuSEAALlgAAC5YgAAuWQAALlmAAC5aQAAuWoAALlrAAC5bAAAuW0AALlvAAC5cQAAuXIAALlzAAC5dQAAuXYAALm1AAC5twAAubkAALm7AAC5vgAAub8AALnAAAC5wQAAucIAALnEAAC5xgAAuccAALnIAAC5ygAAucsAALoKAAC6DAAAug4AALoQAAC6EwAAuhQAALoVAAC6FgAAuhcAALoZAAC6GwAAuhwAALodAAC6HwAAuiAAALpfAAC6YQAAumMAALplAAC6aAAAumkAALpqAAC6awAAumwAALpuAAC6cAAAunEAALpyAAC6dAAAunUAALq0AAC6tgAAurgAALq6AAC6vQAAur4AALq/AAC6wAAAusEAALrDAAC6xQAAusYAALrHAAC6yQAAusoAALsJAAC7CwAAuw0AALsPAAC7EgAAuxMAALsUAAC7FQAAuxYAALsYAAC7GgAAuxsAALscAAC7HgAAux8AALteAAC7YAAAu2MAALtlAAC7aAAAu2kAALtqAAC7awAAu2wAALtuAAC7cAAAu3EAALtyAAC7dAAAu3UAALvAAAC74wAAvAMAALwjAAC8JQAAvCcAALwpAAC8KwA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AzSIAAM0lAADNMgAAzUMAAM1FAADNRwAAzUkAAM1LAADNTQAAzU8AAM1RAADNUwAAzWQAAM1nAADNagAAzW0AAM1wAADNcwAAzXYAAM15AADNfAAAzX4AAM29AADNvwAAzcEAAM3DAADNxgAAzccAAM3IAADNyQAAzcoAAM3MAADNzgAAzc8AAM3QAADN0gAAzdMAAM4SAADOFAAAzhYAAM4YAADOGwAAzhwAAM4dAADOHgAAzh8AAM4hAADOIwAAziQAAM4lAADOJwAAzigAAM5nAADOaQAAzmwAAM5uAADOcQAAznIAAM5zAADOdAAAznUAAM53AADOeQAAznoAAM57AADOfQAAzn4AAM6LAADOjAAAzo0AAM6PAADOzgAAztAAAM7SAADO1AAAztcAAM7YAADO2QAAztoAAM7bAADO3QAAzt8AAM7gAADO4QAAzuMAAM7kAADPIwAAzyUAAM8nAADPKQAAzywAAM8tAADPLgAAzy8AAM8wAADPMgAAzzQAAM81AADPNgAAzzgAAM85AADPeAAAz3oAAM98AADPfgAAz4EAAM+CAADPgwAAz4QAAM+FAADPhwAAz4kAAM+KAADPiwAAz40AAM+OAADPzQAAz88AAM/RAADP0wAAz9YAAM/XAADP2AAAz9kAAM/aAADP3AAAz94AAM/fAADP4AAAz+IAAM/jAADQIgAA0CQAANAmAADQKAAA0CsAANAsAADQLQAA0C4AANAvAADQMQAA0DMAANA0AADQNQAA0DcAANA4AADQXQAA0IEAANCoAADQzAAA0M8AANDRAADQ0wAA0NUAANDXAADQ2QAA0NoAANDdAADQ6gAA0PcAAND5AADQ+wAA0P0AAND/AADRAQAA0QMAANEQAADREwAA0RYAANEZAADRHAAA0R8AANEiAADRJAAA0WMAANFlAADRaAAA0WoAANFtAADRbgAA0W8AANFwAADRcQAA0XMAANF1AADRdgAA0XcAANF5AADRegAA0b0AANHhAADSBQAA0igAANJPAADSbwAA0pYAANK9AADS3QAA0wEAANMlAADTJwAA0yoAANMsAADTLgAA0zAAANMzAADTNgAA0zgAANM6AADTPQAA0z8AANNBAADTRAAA00cAANNIAADTTQAA01oAANNdAADTXwAA02IAANNlAADTZwAA04wAANOwAADT1wAA0/sAANP+AADUAAAA1AIAANQEAADUBgAA1AgAANQJAADUDAAA1BkAANQsAADULgAA1DAAANQyAADUNAAA1DYAANQ4AADUOgAA1DwAANQ+AADUUQAA1FQAANRXAADUWgAA1F0AANRgAADUYwAA1GYAANRpAADUbAAA1G4AANStAADUrwAA1LIAANS0AADUtwAA1LgAANS5AADUugAA1LsAANS9AADUvwAA1MAAANTBAADUwwAA1MQAANTNAADUzgAA1NAAANUPAADVEQAA1RMAANUVAADVGAAA1RkAANUaAADVGwAA1RwAANUeAADVIAAA1SEAANUiAADVJAAA1SUAANVkAADVZgAA1WkAANVrAADVbgAA1W8AANVwAADVcQAA1XIAANV0AADVdgAA1XcAANV4AADVegAA1XsAANWEAADVhQAA1YcAANXGAADVyAAA1coAANXMAADVzwAA1dAAANXRAADV0gAA1dMAANXVAADV1wAA1dgAANXZAADV2wAA1dwAANYbAADWHQAA1iAAANYiAADWJQAA1iYAANYnAADWKAAA1ikAANYrAADWLQAA1i4AANYvAADWMQAA1jIAANY7AADWPAAA1j4AANZ9AADWfwAA1oEAANaDAADWhgAA1ocAANaIAADWiQAA1ooAANaMAADWjgAA1o8AANaQAADWkgAA1pMAANbSAADW1AAA1tcAANbZAADW3AAA1t0AANbeAADW3wAA1uAAANbiAADW5AAA1uUAANbmAADW6AAA1ukAANb2AADW9wAA1vgAANb6AADXOQAA1zsAANc9AADXPwAA10IAANdDAADXRAAA10UAANdGAADXSAAA10oAANdLAADXTAAA104AANdPAADXjgAA15AAANeSAADXlAAA15cAANeYAADXmQAA15oAANebAADXnQAA158AANegAADXoQAA16MAANekAADXsQAA18wAANfPAADX0gAA19QAANfXAADX2gAA190AANfgAADX4wAA1+YAANfoAADX6wAA1+4AANfxAADYDAAA2A8AANgSAADYFQAA2BgAANgbAADYHgAA2CEAANgkAADYJwAA2CoAANgtAADYMAAA2DMAANg1AADYOAAA2D8AANhNAADYUwAA2FkAANhhAADYbAAA2HYAANiLAADYngAA2KoAANj1AADZGAAA2TgAANlYAADZWgAA2VwAANleAADZYAAA2WMAANlkAADZZQAA2WgAANlpAADZawAA2WwAANluAADZcQAA2XIAANlzAADZdgAA2XcAANl8AADZiQAA2Y4AANmQAADZkgAA2ZcAANmaAADZnQAA2Z8AANnEAADZ6AAA2g8AANozAADaNgAA2jgAANo6AADaPAAA2j4AANpAAADaQQAA2kQAANpRAADaYgAA2mQAANpmAADaaAAA2moAANpsAADabgAA2nAAANpyAADagwAA2oYAANqJAADajAAA2o8AANqSAADalQAA2pgAANqbAADanQAA2twAANreAADa4AAA2uIAANrlAADa5gAA2ucAANroAADa6QAA2usAANrtAADa7gAA2u8AANrxAADa8gAA2zEAANszAADbNQAA2zcAANs6AADbOwAA2zwAANs9AADbPgAA20AAANtCAADbQwAA20QAANtGAADbRwAA24YAANuIAADbiwAA240AANuQAADbkQAA25IAANuTAADblAAA25YAANuYAADbmQAA25oAANucAADbnQAA26oAANurAADbrAAA264AANvtAADb7wAA2/EAANvzAADb9gAA2/cAANv4AADb+QAA2/oAANv8AADb/gAA2/8AANwAAADcAgAA3AMAANxCAADcRAAA3EYAANxIAADcSwAA3EwAANxNAADcTgAA3E8AANxRAADcUwAA3FQAANxVAADcVwAA3FgAANyXAADcmQAA3JsAANydAADcoAAA3KEAANyiAADcowAA3KQAANymAADcqAAA3KkAANyqAADcrAAA3K0AANzsAADc7gAA3PAAANzyAADc9QAA3PYAANz3AADc+AAA3PkAANz7AADc/QAA3P4AANz/AADdAQAA3QIAAN1BAADdQwAA3UUAAN1HAADdSgAA3UsAAN1MAADdTQAA3U4AAN1QAADdUgAA3VMAAN1UAADdVgAA3VcAAN18AADdoAAA3ccAAN3rAADd7gAA3fAAAN3yAADd9AAA3fYAAN34AADd+QAA3fwAAN4JAADeGAAA3hoAAN4cAADeHgAA3iAAAN4iAADeJAAA3iYAAN41AADeOAAA3jsAAN4+AADeQQAA3kQAAN5HAADeSgAA3kwAAN6LAADejQAA3o8AAN6RAADelAAA3pUAAN6WAADelwAA3pgAAN6aAADenAAA3p0AAN6eAADeoAAA3qEAAN7gAADe4gAA3uQAAN7mAADe6QAA3uoAAN7rAADe7AAA3u0AAN7vAADe8QAA3vIAAN7zAADe9QAA3vYAAN81AADfNwAA3zkAAN87AADfPgAA3z8AAN9AAADfQQAA30IAAN9EAADfRgAA30cAAN9IAADfSgAA30sAAN+KAADfjAAA344AAN+QAADfkwAA35QAAN+VAADflgAA35cAAN+ZAADfmwAA35wAAN+dAADfnwAA36AAAN/fAADf4QAA3+MAAN/lAADf6AAA3+kAAN/qAADf6wAA3+wAAN/uAADf8AAA3/EAAN/yAADf9AAA3/UAAOA0AADgNgAA4DgAAOA6AADgPQAA4D4AAOA/AADgQAAA4EEAAOBDAADgRQAA4EYAAOBHAADgSQAA4EoAAOCJAADgiwAA4I0AAOCPAADgkgAA4JMAAOCUAADglQAA4JYAAOCYAADgmgAA4JsAAOCcAADgngAA4J8AAODqAADhDQAA4S0AAOFNAADhTwAA4VEAAOFTAADhVQAA4VgAAOFZAADhWgAA4V0AAOFeAADhYAAA4WEAAOFjAADhZgAA4WcAAOFoAADhawAA4WwAAOFxAADhfgAA4YMAAOGFAADhhwAA4YwAAOGPAADhkgAA4ZQAAOG5AADh3QAA4gQAAOIoAADiKwAA4i0AAOIvAADiMQAA4jMAAOI1AADiNgAA4jkAAOJGAADiVwAA4lkAAOJbAADiXQAA4l8AAOJhAADiYwAA4mUAAOJnAADieAAA4nsAAOJ+AADigQAA4oQAAOKHAADiigAA4o0AAOKQAADikgAA4tEAAOLTAADi1QAA4tcAAOLaAADi2wAA4twAAOLdAADi3gAA4uAAAOLiAADi4wAA4uQAAOLmAADi5wAA4yYAAOMoAADjKgAA4ywAAOMvAADjMAAA4zEAAOMyAADjMwAA4zUAAOM3AADjOAAA4zkAAOM7AADjPAAA43sAAON9AADjgAAA44IAAOOFAADjhgAA44cAAOOIAADjiQAA44sAAOONAADjjgAA448AAOORAADjkgAA458AAOOgAADjoQAA46MAAOPiAADj5AAA4+YAAOPoAADj6wAA4+wAAOPtAADj7gAA4+8AAOPxAADj8wAA4/QAAOP1AADj9wAA4/gAAOQ3AADkOQAA5DsAAOQ9AADkQAAA5EEAAORCAADkQwAA5EQAAORGAADkSAAA5EkAAORKAADkTAAA5E0AAOSMAADkjgAA5JAAAOSSAADklQAA5JYAAOSXAADkmAAA5JkAAOSbAADknQAA5J4AAOSfAADkoQAA5KIAAOThAADk4wAA5OUAAOTnAADk6gAA5OsAAOTsAADk7QAA5O4AAOTwAADk8gAA5PMAAOT0AADk9gAA5PcAAOU2AADlOAAA5ToAAOU8AADlPwAA5UAAAOVBAADlQgAA5UMAAOVFAADlRwAA5UgAAOVJAADlSwAA5UwAAOVxAADllQAA5bwAAOXgAADl4wAA5eUAAOXnAADl6QAA5esAAOXtAADl7gAA5fEAAOX+AADmDQAA5g8AAOYRAADmEwAA5hUAAOYXAADmGQAA5hsAAOYqAADmLQAA5jAAAOYzAADmNgAA5jkAAOY8AADmPwAA5kEAAOaAAADmggAA5oQAAOaGAADmiQAA5ooAAOaLAADmjAAA5o0AAOaPAADmkQAA5pIAAOaTAADmlQAA5pYAAObVAADm1wAA5tkAAObbAADm3gAA5t8AAObgAADm4QAA5uIAAObkAADm5gAA5ucAAOboAADm6gAA5usAAOcqAADnLAAA5y4AAOcwAADnMwAA5zQAAOc1AADnNgAA5zcAAOc5AADnOwAA5zwAAOc9AADnPwAA50AAAOd/AADngQAA54MAAOeFAADniAAA54kAAOeKAADniwAA54wAAOeOAADnkAAA55EAAOeSAADnlAAA55UAAOfUAADn1gAA59gAAOfaAADn3QAA594AAOffAADn4AAA5+EAAOfjAADn5QAA5+YAAOfnAADn6QAA5+oAAOgpAADoKwAA6C0AAOgvAADoMgAA6DMAAOg0AADoNQAA6DYAAOg4AADoOgAA6DsAAOg8AADoPgAA6D8AAOh+AADogAAA6IIAAOiEAADohwAA6IgAAOiJAADoigAA6IsAAOiNAADojwAA6JAAAOiRAADokwAA6JQAAOjfAADpAgAA6SIAAOlCAADpRAAA6UYAAOlIAADpSgAA6U0AAOlOAADpTwAA6VIAAOlTAADpVQAA6VYAAOlYAADpWgAA6VsAAOlcAADpXwAA6WAAAOlpAADpdgAA6XsAAOl9AADpfwAA6YQAAOmHAADpigAA6YwAAOmxAADp1QAA6fwAAOogAADqIwAA6iUAAOonAADqKQAA6isAAOotAADqLgAA6jEAAOo+AADqTwAA6lEAAOpTAADqVQAA6lcAAOpZAADqWwAA6l0AAOpfAADqcAAA6nMAAOp2AADqeQAA6nwAAOp/AADqggAA6oUAAOqIAADqigAA6skAAOrLAADqzQAA6s8AAOrSAADq0wAA6tQAAOrVAADq1gAA6tgAAOraAADq2wAA6twAAOreAADq3wAA6x4AAOsgAADrIgAA6yQAAOsnAADrKAAA6ykAAOsqAADrKwAA6y0AAOsvAADrMAAA6zEAAOszAADrNAAA63MAAOt1AADreAAA63oAAOt9AADrfgAA638AAOuAAADrgQAA64MAAOuFAADrhgAA64cAAOuJAADrigAA65cAAOuYAADrmQAA65sAAOvaAADr3AAA694AAOvgAADr4wAA6+QAAOvlAADr5gAA6+cAAOvpAADr6wAA6+wAAOvtAADr7wAA6/AAAOwvAADsMQAA7DMAAOw1AADsOAAA7DkAAOw6AADsOwAA7DwAAOw+AADsQAAA7EEAAOxCAADsRAAA7EUAAOyEAADshgAA7IgAAOyKAADsjQAA7I4AAOyPAADskAAA7JEAAOyTAADslQAA7JYAAOyXAADsmQAA7JoAAOzZAADs2wAA7N0AAOzfAADs4gAA7OMAAOzkAADs5QAA7OYAAOzoAADs6gAA7OsAAOzsAADs7gAA7O8AAO0uAADtMAAA7TIAAO00AADtNwAA7TgAAO05AADtOgAA7TsAAO09AADtPwAA7UAAAO1BAADtQwAA7UQAAO1pAADtjQAA7bQAAO3YAADt2wAA7d0AAO3fAADt4QAA7eMAAO3lAADt5gAA7ekAAO32AADuBQAA7gcAAO4JAADuCwAA7g0AAO4PAADuEQAA7hMAAO4iAADuJQAA7igAAO4rAADuLgAA7jEAAO40AADuNwAA7jkAAO54AADuegAA7nwAAO5+AADugQAA7oIAAO6DAADuhAAA7oUAAO6HAADuiQAA7ooAAO6LAADujQAA7o4AAO7NAADuzwAA7tEAAO7TAADu1gAA7tcAAO7YAADu2QAA7toAAO7cAADu3gAA7t8AAO7gAADu4gAA7uMAAO8iAADvJAAA7yYAAO8oAADvKwAA7ywAAO8tAADvLgAA7y8AAO8xAADvMwAA7zQAAO81AADvNwAA7zgAAO93AADveQAA73sAAO99AADvgAAA74EAAO+CAADvgwAA74QAAO+GAADviAAA74kAAO+KAADvjAAA740AAO/MAADvzgAA79AAAO/SAADv1QAA79YAAO/XAADv2AAA79kAAO/bAADv3QAA794AAO/fAADv4QAA7+IAAPAhAADwIwAA8CUAAPAnAADwKgAA8CsAAPAsAADwLQAA8C4AAPAwAADwMgAA8DMAAPA0AADwNgAA8DcAAPB2AADweAAA8HoAAPB8AADwfwAA8IAAAPCBAADwggAA8IMAAPCFAADwhwAA8IgAAPCJAADwiwAA8IwAAPDXAADw+gAA8RoAAPE6AADxPAAA8T4AAPFAAADxQgAA8UUAAPFGAADxRwAA8UoAAPFLAADxTQAA8U4AAPFQAADxUwAA8VQAAPFVAADxWAAA8VkAAPFeAADxawAA8XAAAPFyAADxdAAA8XkAAPF8AADxfwAA8YEAAPGmAADxygAA8fEAAPIVAADyGAAA8hoAAPIcAADyHgAA8iAAAPIiAADyIwAA8iYAAPIzAADyRAAA8kYAAPJIAADySgAA8kwAAPJOAADyUAAA8lIAAPJUAADyZQAA8mgAAPJrAADybgAA8nEAAPJ0AADydwAA8noAAPJ9AADyfwAA8r4AAPLAAADywgAA8sQAAPLHAADyyAAA8skAAPLKAADyywAA8s0AAPLPAADy0AAA8tEAAPLTAADy1AAA8xMAAPMVAADzFwAA8xkAAPMcAADzHQAA8x4AAPMfAADzIAAA8yIAAPMkAADzJQAA8yYAAPMoAADzKQAA82gAAPNqAADzbQAA828AAPNyAADzcwAA83QAAPN1AADzdgAA83gAAPN6AADzewAA83wAAPN+AADzfwAA84wAAPONAADzjgAA85AAAPPPAADz0QAA89MAAPPVAADz2AAA89kAAPPaAADz2wAA89wAAPPeAADz4AAA8+EAAPPiAADz5AAA8+UAAPQkAAD0JgAA9CgAAPQqAAD0LQAA9C4AAPQvAAD0MAAA9DEAAPQzAAD0NQAA9DYAAPQ3AAD0OQAA9DoAAPR5AAD0ewAA9H0AAPR/AAD0ggAA9IMAAPSEAAD0hQAA9IYAAPSIAAD0igAA9IsAAPSMAAD0jgAA9I8AAPTOAAD00AAA9NIAAPTUAAD01wAA9NgAAPTZAAD02gAA9NsAAPTdAAD03wA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BESMAARElAAERKAABESkAAREqAAERLQABES4AAREwAAERMQABETMAARE2AAERNwABETgAARE7AAERPAABEUEAARFOAAERUwABEVUAARFXAAERXAABEV8AARFiAAERZAABEYkAARGtAAER1AABEfgAARH7AAER/QABEf8AARIBAAESAwABEgUAARIGAAESCQABEhYAARInAAESKQABEisAARItAAESLwABEjEAARIzAAESNQABEjcAARJIAAESSwABEk4AARJRAAESVAABElcAARJaAAESXQABEmAAARJiAAESoQABEqMAARKlAAESpwABEqoAARKrAAESrAABEq0AARKuAAESsAABErIAARKzAAEStAABErYAARK3AAES9gABEvgAARL6AAES/AABEv8AARMAAAETAQABEwIAARMDAAETBQABEwcAARMIAAETCQABEwsAARMMAAETSwABE00AARNQAAETUgABE1UAARNWAAETVwABE1gAARNZAAETWwABE10AARNeAAETXwABE2EAARNiAAETbwABE3AAARNxAAETcwABE7IAARO0AAETtgABE7gAARO7AAETvAABE70AARO+AAETvwABE8EAARPDAAETxAABE8UAARPHAAETyAABFAcAARQJAAEUCwABFA0AARQQAAEUEQABFBIAARQTAAEUFAABFBYAARQYAAEUGQABFBoAARQcAAEUHQABFFwAARReAAEUYAABFGIAARRlAAEUZgABFGcAARRoAAEUaQABFGsAARRtAAEUbgABFG8AARRxAAEUcgABFLEAARSzAAEUtQABFLcAARS6AAEUuwABFLwAARS9AAEUvgABFMAAARTCAAEUwwABFMQAARTGAAEUxwABFQYAARUIAAEVCgABFQwAARUPAAEVEAABFREAARUSAAEVEwABFRUAARUXAAEVGAABFRkAARUbAAEVHAABFUEAARVlAAEVjAABFbAAARWzAAEVtQABFbcAARW5AAEVuwABFb0AARW+AAEVwQABFc4AARXdAAEV3wABFeEAARXjAAEV5QABFecAARXpAAEV6wABFfoAARX9AAEWAAABFgMAARYGAAEWCQABFgwAARYPAAEWEQABFlAAARZSAAEWVAABFlYAARZZAAEWWgABFlsAARZcAAEWXQABFl8AARZhAAEWYgABFmMAARZlAAEWZgABFqUAARanAAEWqQABFqsAARauAAEWrwABFrAAARaxAAEWsgABFrQAARa2AAEWtwABFrgAARa6AAEWuwABFvoAARb8AAEW/gABFwAAARcDAAEXBAABFwUAARcGAAEXBwABFwkAARcLAAEXDAABFw0AARcPAAEXEAABF08AARdRAAEXUwABF1UAARdYAAEXWQABF1oAARdbAAEXXAABF14AARdgAAEXYQABF2IAARdkAAEXZQABF6QAARemAAEXqAABF6oAARetAAEXrgABF68AARewAAEXsQABF7MAARe1AAEXtgABF7cAARe5AAEXugABF/kAARf7AAEX/QABF/8AARgCAAEYAwABGAQAARgFAAEYBgABGAgAARgKAAEYCwABGAwAARgOAAEYDwABGE4AARhQAAEYUgABGFQAARhXAAEYWAABGFkAARhaAAEYWwABGF0AARhfAAEYYAABGGEAARhjAAEYZAABGK8AARjSAAEY8gABGRIAARkUAAEZFgABGRgAARkaAAEZHQABGR4AARkfAAEZIgABGSMAARklAAEZJgABGSgAARkrAAEZLAABGS0AARkwAAEZMQABGTYAARlDAAEZSAABGUoAARlMAAEZUQABGVQAARlXAAEZWQABGX4AARmiAAEZyQABGe0AARnwAAEZ8gABGfQAARn2AAEZ+AABGfoAARn7AAEZ/gABGgsAARocAAEaHgABGiAAARoiAAEaJAABGiYAARooAAEaKgABGiwAARo9AAEaQAABGkMAARpGAAEaSQABGkwAARpPAAEaUgABGlUAARpXAAEalgABGpgAARqaAAEanAABGp8AARqgAAEaoQABGqIAARqjAAEapQABGqcAARqoAAEaqQABGqsAARqsAAEa6wABGu0AARrvAAEa8QABGvQAARr1AAEa9gABGvcAARr4AAEa+gABGvwAARr9AAEa/gABGwAAARsBAAEbQAABG0IAARtFAAEbRwABG0oAARtLAAEbTAABG00AARtOAAEbUAABG1IAARtTAAEbVAABG1YAARtXAAEbZAABG2UAARtmAAEbaAABG6cAARupAAEbqwABG60AARuwAAEbsQABG7IAARuzAAEbtAABG7YAARu4AAEbuQABG7oAARu8AAEbvQABG/wAARv+AAEcAAABHAIAARwFAAEcBgABHAcAARwIAAEcCQABHAsAARwNAAEcDgABHA8AARwRAAEcEgABHFEAARxTAAEcVQABHFcAARxaAAEcWwABHFwAARxdAAEcXgABHGAAARxiAAEcYwABHGQAARxmAAEcZwABHKYAARyoAAEcqgABHKwAARyvAAEcsAABHLEAARyyAAEcswABHLUAARy3AAEcuAABHLkAARy7AAEcvAABHPsAARz9AAEc/wABHQEAAR0EAAEdBQABHQYAAR0HAAEdCAABHQoAAR0MAAEdDQABHQ4AAR0QAAEdEQABHTYAAR1aAAEdgQABHaUAAR2oAAEdqgABHawAAR2uAAEdsAABHbIAAR2zAAEdtgABHcMAAR3SAAEd1AABHdYAAR3YAAEd2gABHdwAAR3eAAEd4AABHe8AAR3yAAEd9QABHfgAAR37AAEd/gABHgEAAR4EAAEeBgABHkUAAR5HAAEeSQABHksAAR5OAAEeTwABHlAAAR5RAAEeUgABHlQAAR5WAAEeVwABHlgAAR5aAAEeWwABHpoAAR6cAAEengABHqAAAR6jAAEepAABHqUAAR6mAAEepwABHqkAAR6rAAEerAABHq0AAR6vAAEesAABHu8AAR7xAAEe8wABHvUAAR74AAEe+QABHvoAAR77AAEe/AABHv4AAR8AAAEfAQABHwIAAR8EAAEfBQABH0QAAR9GAAEfSAABH0oAAR9NAAEfTgABH08AAR9QAAEfUQABH1MAAR9VAAEfVgABH1cAAR9ZAAEfWgABH5kAAR+bAAEfnQABH58AAR+iAAEfowABH6QAAR+lAAEfpgABH6gAAR+qAAEfqwABH6wAAR+uAAEfrwABH+4AAR/wAAEf8gABH/QAAR/3AAEf+AABH/kAAR/6AAEf+wABH/0AAR//AAEgAAABIAEAASADAAEgBAABIEMAASBFAAEgRwABIEkAASBMAAEgTQABIE4AASBPAAEgUAABIFIAASBUAAEgVQABIFYAASBYAAEgWQABIKQAASDHAAEg5wABIQcAASEJAAEhCwABIQ0AASEPAAEhEgABIRMAASEUAAEhFwABIRgAASEaAAEhGwABIR0AASEfAAEhIAABISEAASEkAAEhJQABISoAASE3AAEhPAABIT4AASFAAAEhRQABIUgAASFLAAEhTQABIXIAASGWAAEhvQABIeEAASHkAAEh5gABIegAASHqAAEh7AABIe4AASHvAAEh8gABIf8AASIQAAEiEgABIhQAASIWAAEiGAABIhoAASIcAAEiHgABIiAAASIxAAEiNAABIjcAASI6AAEiPQABIkAAASJDAAEiRgABIkkAASJLAAEiigABIowAASKOAAEikAABIpMAASKUAAEilQABIpYAASKXAAEimQABIpsAASKcAAEinQABIp8AASKgAAEi3wABIuEAASLjAAEi5QABIugAASLpAAEi6gABIusAASLsAAEi7gABIvAAASLxAAEi8gABIvQAASL1AAEjNAABIzYAASM5AAEjOwABIz4AASM/AAEjQAABI0EAASNCAAEjRAABI0YAASNHAAEjSAABI0oAASNLAAEjWAABI1kAASNaAAEjXAABI5sAASOdAAEjnwABI6EAASOkAAEjpQABI6YAASOnAAEjqAABI6oAASOsAAEjrQABI64AASOwAAEjsQABI/AAASPyAAEj9AABI/YAASP5AAEj+gABI/sAASP8AAEj/QABI/8AASQBAAEkAgABJAMAASQFAAEkBgABJEUAASRHAAEkSQABJEsAASROAAEkTwABJFAAASRRAAEkUgABJFQAASRWAAEkVwABJFgAASRaAAEkWwABJJoAASScAAEkngABJKAAASSjAAEkpAABJKUAASSmAAEkpwABJKkAASSrAAEkrAABJK0AASSvAAEksAABJO8AASTxAAEk8wABJPUAAST4AAEk+QABJPoAAST7AAEk/AABJP4AASUAAAElAQABJQIAASUEAAElBQABJSoAASVOAAEldQABJZkAASWcAAElngABJaAAASWiAAElpAABJaYAASWnAAElqgABJbcAASXGAAElyAABJcoAASXMAAElzgABJdAAASXSAAEl1AABJeMAASXmAAEl6QABJewAASXvAAEl8gABJfUAASX4AAEl+gABJjkAASY7AAEmPQABJj8AASZCAAEmQwABJkQAASZFAAEmRgABJkgAASZKAAEmSwABJkwAASZOAAEmTwABJo4AASaQAAEmkgABJpQAASaXAAEmmAABJpkAASaaAAEmmwABJp0AASafAAEmoAABJqEAASajAAEmpAABJuMAASblAAEm5wABJukAASbsAAEm7QABJu4AASbvAAEm8AABJvIAASb0AAEm9QABJvYAASb4AAEm+QABJzgAASc6AAEnPAABJz4AASdBAAEnQgABJ0MAASdEAAEnRQABJ0cAASdJAAEnSgABJ0sAASdNAAEnTgABJ40AASePAAEnkQABJ5MAASeWAAEnlwABJ5gAASeZAAEnmgABJ5wAASeeAAEnnwABJ6AAASeiAAEnowABJ+IAASfkAAEn5gABJ+gAASfrAAEn7AABJ+0AASfuAAEn7wABJ/EAASfzAAEn9AABJ/UAASf3AAEn+AABKDcAASg5AAEoOwABKD0AAShAAAEoQQABKEIAAShDAAEoRAABKEYAAShIAAEoSQABKEoAAShMAAEoTQABKJgAASi7AAEo2wABKPsAASj9AAEo/wABKQEAASkDAAEpBgABKQcAASkIAAEpCwABKQwAASkOAAEpDwABKREAASkUAAEpFQABKRYAASkZAAEpGgABKR8AASksAAEpMQABKTMAASk1AAEpOgABKT0AASlAAAEpQgABKWcAASmLAAEpsgABKdYAASnZAAEp2wABKd0AASnfAAEp4QABKeMAASnkAAEp5wABKfQAASoFAAEqBwABKgkAASoLAAEqDQABKg8AASoRAAEqEwABKhUAASomAAEqKQABKiwAASovAAEqMgABKjUAASo4AAEqOwABKj4AASpAAAEqfwABKoEAASqDAAEqhQABKogAASqJAAEqigABKosAASqMAAEqjgABKpAAASqRAAEqkgABKpQAASqVAAEq1AABKtYAASrYAAEq2gABKt0AASreAAEq3wABKuAAASrhAAEq4wABKuUAASrmAAEq5wABKukAASrqAAErKQABKysAASsuAAErMAABKzMAASs0AAErNQABKzYAASs3AAErOQABKzsAASs8AAErPQABKz8AAStAAAErTQABK04AAStPAAErUQABK5AAASuSAAErlAABK5YAASuZAAErmgABK5sAASucAAErnQABK58AASuhAAErogABK6MAASulAAErpgABK+UAASvnAAEr6QABK+sAASvuAAEr7wABK/AAASvxAAEr8gABK/QAASv2AAEr9wABK/gAASv6AAEr+wABLDoAASw8AAEsPgABLEAAASxDAAEsRAABLEUAASxGAAEsRwABLEkAASxLAAEsTAABLE0AASxPAAEsUAABLI8AASyRAAEskwABLJUAASyYAAEsmQABLJoAASybAAEsnAABLJ4AASygAAEsoQABLKIAASykAAEspQABLOQAASzmAAEs6AABLOoAASztAAEs7gABLO8AASzwAAEs8QABLPMAASz1AAEs9gABLPcAASz5AAEs+gABLR8AAS1DAAEtagABLY4AAS2RAAEtkwABLZUAAS2XAAEtmQABLZsAAS2cAAEtnwABLawAAS27AAEtvQABLb8AAS3BAAEtwwABLcUAAS3HAAEtyQABLdgAAS3bAAEt3gABLeEAAS3kAAEt5wABLeoAAS3tAAEt7wABLi4AAS4wAAEuMgABLjQAAS43AAEuOAABLjkAAS46AAEuOwABLj0AAS4/AAEuQAABLkEAAS5DAAEuRAABLoMAAS6FAAEuhwABLokAAS6MAAEujQABLo4AAS6PAAEukAABLpIAAS6UAAEulQABLpYAAS6YAAEumQABLtgAAS7aAAEu3AABLt4AAS7hAAEu4gABLuMAAS7kAAEu5QABLucAAS7pAAEu6gABLusAAS7tAAEu7gABLy0AAS8vAAEvMQABLzMAAS82AAEvNwABLzgAAS85AAEvOgABLzwAAS8+AAEvPwABL0AAAS9CAAEvQwABL4IAAS+EAAEvhgABL4gAAS+LAAEvjAABL40AAS+OAAEvjwABL5EAAS+TAAEvlAABL5UAAS+XAAEvmAABL9cAAS/ZAAEv2wABL90AAS/gAAEv4QABL+IAAS/jAAEv5AABL+YAAS/oAAEv6QABL+oAAS/sAAEv7QABMCwAATAuAAEwMAABMDIAATA1AAEwNgABMDcAATA4AAEwOQABMDsAATA9AAEwPgABMD8AATBBAAEwQgABMI0AATCwAAEw0AABMPAAATDyAAEw9AABMPYAATD4AAEw+wABMPwAATD9AAExAAABMQEAATEDAAExBAABMQYAATEJAAExCgABMQsAATEOAAExDwABMRQAATEhAAExJgABMSgAATEqAAExLwABMTIAATE1AAExNwABMVwAATGAAAExpwABMcsAATHOAAEx0AABMdIAATHUAAEx1gABMdgAATHZAAEx3AABMekAATH6AAEx/AABMf4AATIAAAEyAgABMgQAATIGAAEyCAABMgoAATIbAAEyHgABMiEAATIkAAEyJwABMioAATItAAEyMAABMjMAATI1AAEydAABMnYAATJ4AAEyegABMn0AATJ+AAEyfwABMoAAATKBAAEygwABMoUAATKGAAEyhwABMokAATKKAAEyyQABMssAATLNAAEyzwABMtIAATLTAAEy1AABMtUAATLWAAEy2AABMtoAATLbAAEy3AABMt4AATLfAAEzHgABMyAAATMjAAEzJQABMygAATMpAAEzKgABMysAATMsAAEzLgABMzAAATMxAAEzMgABMzQAATM1AAEzQgABM0MAATNEAAEzRgABM4UAATOHAAEziQABM4sAATOOAAEzjwABM5AAATORAAEzkgABM5QAATOWAAEzlwABM5gAATOaAAEzmwABM9oAATPcAAEz3gABM+AAATPjAAEz5AABM+UAATPmAAEz5wABM+kAATPrAAEz7AABM+0AATPvAAEz8AABNC8AATQxAAE0MwABNDUAATQ4AAE0OQABNDoAATQ7AAE0PAABND4AATRAAAE0QQABNEIAATREAAE0RQABNIQAATSGAAE0iAABNIoAATSNAAE0jgABNI8AATSQAAE0kQABNJMAATSVAAE0lgABNJcAATSZAAE0mgABNNkAATTbAAE03QABNN8AATTiAAE04wABNOQAATTlAAE05gABNOgAATTqAAE06wABNOwAATTuAAE07wABNRQAATU4AAE1XwABNYMAATWGAAE1iAABNYoAATWMAAE1jgABNZAAATWRAAE1lAABNaEAATWwAAE1sgABNbQAATW2AAE1uAABNboAATW8AAE1vgABNc0AATXQAAE10wABNdYAATXZAAE13AABNd8AATXiAAE15AABNiMAATYlAAE2JwABNikAATYsAAE2LQABNi4AATYvAAE2MAABNjIAATY0AAE2NQABNjYAATY4AAE2OQABNngAATZ6AAE2fAABNn4AATaBAAE2ggABNoMAATaEAAE2hQABNocAATaJAAE2igABNosAATaNAAE2jgABNs0AATbPAAE20QABNtMAATbWAAE21wABNtgAATbZAAE22gABNtwAATbeAAE23wABNuAAATbiAAE24wABNyIAATckAAE3JwABNykAATcsAAE3LQABNy4AATcvAAE3MAABNzIAATc0AAE3NQABNzYAATc4AAE3OQABN3gAATd6AAE3fAABN34AATeBAAE3ggABN4MAATeEAAE3hQABN4cAATeJAAE3igABN4sAATeNAAE3jgABN80AATfPAAE30QABN9MAATfWAAE31wABN9gAATfZAAE32gABN9wAATfeAAE33wABN+AAATfiAAE34wABOCIAATgkAAE4JgABOCgAATgrAAE4LAABOC0AATguAAE4LwABODEAATgzAAE4NAABODUAATg3AAE4OAABOIMAATimAAE4xgABOOYAATjoAAE46gABOOwAATjuAAE48QABOPIAATjzAAE49gABOPcAATj5AAE4+gABOPwAATj/AAE5AAABOQEAATkEAAE5BQABOQ4AATkbAAE5IAABOSIAATkkAAE5KQABOSwAATkvAAE5MQABOVYAATl6AAE5oQABOcUAATnIAAE5ygABOcwAATnOAAE50AABOdIAATnTAAE51gABOeMAATn0AAE59gABOfgAATn6AAE5/AABOf4AAToAAAE6AgABOgQAAToVAAE6GAABOhsAAToeAAE6IQABOiQAATonAAE6KgABOi0AATovAAE6bgABOnAAATpyAAE6dAABOncAATp4AAE6eQABOnoAATp7AAE6fQABOn8AATqAAAE6gQABOoMAATqEAAE6wwABOsUAATrHAAE6yQABOswAATrNAAE6zgABOs8AATrQAAE60gABOtQAATrVAAE61gABOtgAATrZAAE7GAABOxoAATsdAAE7HwABOyIAATsjAAE7JAABOyUAATsmAAE7KAABOyoAATsrAAE7LAABOy4AATsvAAE7PAABOz0AATs+AAE7QAABO38AATuBAAE7gwABO4UAATuIAAE7iQABO4oAATuLAAE7jAABO44AATuQAAE7kQABO5IAATuUAAE7lQABO9QAATvWAAE72AABO9oAATvdAAE73gABO98AATvgAAE74QABO+MAATvlAAE75gABO+cAATvpAAE76gABPCkAATwrAAE8LQABPC8AATwyAAE8MwABPDQAATw1AAE8NgABPDgAATw6AAE8OwABPDwAATw+AAE8PwABPH4AATyAAAE8ggABPIQAATyHAAE8iAABPIkAATyKAAE8iwABPI0AATyPAAE8kAABPJEAATyTAAE8lAABPNMAATzVAAE81wABPNkAATzcAAE83QABPN4AATzfAAE84AABPOIAATzkAAE85QABPOYAATzoAAE86QABPQ4AAT0yAAE9WQABPX0AAT2AAAE9ggABPYQAAT2GAAE9iAABPYoAAT2LAAE9jgABPZsAAT2qAAE9rAA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BVAMAAVQjAAFUJQABVCcAAVQpAAFUKwABVC0AAVQuAAFULwABVDIAAVQzAAFUNQABVDYAAVQ4AAFUOgABVDsAAVQ8AAFUPwABVEAAAVRFAAFUUgABVFcAAVRZAAFUWwABVGAAAVRjAAFUZgABVGgAAVSNAAFUsQABVNgAAVT8AAFU/wABVQEAAVUDAAFVBQABVQcAAVUJAAFVCgABVQ0AAVUaAAFVKwABVS0AAVUvAAFVMQABVTMAAVU1AAFVNwABVTkAAVU7AAFVTAABVU8AAVVSAAFVVQABVVgAAVVbAAFVXgABVWEAAVVkAAFVZgABVaUAAVWnAAFVqQABVasAAVWuAAFVrwABVbAAAVWxAAFVsgABVbQAAVW2AAFVtwABVbgAAVW6AAFVuwABVfoAAVX8AAFV/gABVgAAAVYDAAFWBAABVgUAAVYGAAFWBwABVgkAAVYLAAFWDAABVg0AAVYPAAFWEAABVk8AAVZRAAFWVAABVlYAAVZZAAFWWgABVlsAAVZcAAFWXQABVl8AAVZhAAFWYgABVmMAAVZlAAFWZgABVnMAAVZ0AAFWdQABVncAAVa2AAFWuAABVroAAVa8AAFWvwABVsAAAVbBAAFWwgABVsMAAVbFAAFWxwABVsgAAVbJAAFWywABVswAAVcLAAFXDQABVw8AAVcRAAFXFAABVxUAAVcWAAFXFwABVxgAAVcaAAFXHAABVx0AAVceAAFXIAABVyEAAVdgAAFXYgABV2QAAVdmAAFXaQABV2oAAVdrAAFXbAABV20AAVdvAAFXcQABV3IAAVdzAAFXdQABV3YAAVe1AAFXtwABV7kAAVe7AAFXvgABV78AAVfAAAFXwQABV8IAAVfEAAFXxgABV8cAAVfIAAFXygABV8sAAVgKAAFYDAABWA4AAVgQAAFYEwABWBQAAVgVAAFYFgABWBcAAVgZAAFYGwABWBwAAVgdAAFYHwABWCAAAVhFAAFYaQABWJAAAVi0AAFYtwABWLkAAVi7AAFYvQABWL8AAVjBAAFYwgABWMUAAVjSAAFY4QABWOMAAVjlAAFY5wABWOkAAVjrAAFY7QABWO8AAVj+AAFZAQABWQQAAVkHAAFZCgABWQ0AAVkQAAFZEwABWRUAAVlUAAFZVgABWVgAAVlaAAFZXQABWV4AAVlfAAFZYAABWWEAAVljAAFZZQABWWYAAVlnAAFZaQABWWoAAVmpAAFZqwABWa0AAVmvAAFZsgABWbMAAVm0AAFZtQABWbYAAVm4AAFZugABWbsAAVm8AAFZvgABWb8AAVn+AAFaAAABWgIAAVoEAAFaBwABWggAAVoJAAFaCgABWgsAAVoNAAFaDwABWhAAAVoRAAFaEwABWhQAAVpTAAFaVQABWlcAAVpZAAFaXAABWl0AAVpeAAFaXwABWmAAAVpiAAFaZAABWmUAAVpmAAFaaAABWmkAAVqoAAFaqgABWqwAAVquAAFasQABWrIAAVqzAAFatAABWrUAAVq3AAFauQABWroAAVq7AAFavQABWr4AAVr9AAFa/wABWwEAAVsDAAFbBgABWwcAAVsIAAFbCQABWwoAAVsMAAFbDgABWw8AAVsQAAFbEgABWxMAAVtSAAFbVAABW1YAAVtYAAFbWwABW1wAAVtdAAFbXgABW18AAVthAAFbYwABW2QAAVtlAAFbZwABW2gAAVuzAAFb1gABW/YAAVwWAAFcGAABXBoAAVwcAAFcHgABXCAAAVwhAAFcIgABXCUAAVwmAAFcKAABXCkAAVwrAAFcLQABXC4AAVwvAAFcMgABXDMAAVw8AAFcSQABXE4AAVxQAAFcUgABXFcAAVxaAAFcXQABXF8AAVyEAAFcqAABXM8AAVzzAAFc9gABXPgAAVz6AAFc/AABXP4AAV0AAAFdAQABXQQAAV0RAAFdIgABXSQAAV0mAAFdKAABXSoAAV0sAAFdLgABXTAAAV0yAAFdQwABXUYAAV1JAAFdTAABXU8AAV1SAAFdVQABXVgAAV1bAAFdXQABXZwAAV2eAAFdoAABXaIAAV2lAAFdpgABXacAAV2oAAFdqQABXasAAV2tAAFdrgABXa8AAV2xAAFdsgABXfEAAV3zAAFd9QABXfcAAV36AAFd+wABXfwAAV39AAFd/gABXgAAAV4CAAFeAwABXgQAAV4GAAFeBwABXkYAAV5IAAFeSwABXk0AAV5QAAFeUQABXlIAAV5TAAFeVAABXlYAAV5YAAFeWQABXloAAV5cAAFeXQABXmoAAV5rAAFebAABXm4AAV6tAAFerwABXrEAAV6zAAFetgABXrcAAV64AAFeuQABXroAAV68AAFevgABXr8AAV7AAAFewgABXsMAAV8CAAFfBAABXwYAAV8IAAFfCwABXwwAAV8NAAFfDgABXw8AAV8RAAFfEwABXxQAAV8VAAFfFwABXxgAAV9XAAFfWQABX1sAAV9dAAFfYAABX2EAAV9iAAFfYwABX2QAAV9mAAFfaAABX2kAAV9qAAFfbAABX20AAV+sAAFfrgABX7AAAV+yAAFftQABX7YAAV+3AAFfuAABX7kAAV+7AAFfvQABX74AAV+/AAFfwQABX8IAAWABAAFgAwABYAUAAWAHAAFgCgABYAsAAWAMAAFgDQABYA4AAWAQAAFgEgABYBMAAWAUAAFgFgABYBcAAWA8AAFgYAABYIcAAWCrAAFgrgABYLAAAWCyAAFgtAABYLYAAWC4AAFguQABYLwAAWDJAAFg2AABYNoAAWDcAAFg3gABYOAAAWDiAAFg5AABYOYAAWD1AAFg+AABYPsAAWD+AAFhAQABYQQAAWEHAAFhCgABYQwAAWFLAAFhTQABYVAAAWFSAAFhVQABYVYAAWFXAAFhWAABYVkAAWFbAAFhXQABYV4AAWFfAAFhYQABYWIAAWGhAAFhowABYaUAAWGnAAFhqgABYasAAWGsAAFhrQABYa4AAWGwAAFhsgABYbMAAWG0AAFhtgABYbcAAWH2AAFh+AABYfoAAWH8AAFh/wABYgAAAWIBAAFiAgABYgMAAWIFAAFiBwABYggAAWIJAAFiCwABYgwAAWJLAAFiTQABYlAAAWJSAAFiVQABYlYAAWJXAAFiWAABYlkAAWJbAAFiXQABYl4AAWJfAAFiYQABYmIAAWKhAAFiowABYqUAAWKnAAFiqgABYqsAAWKsAAFirQABYq4AAWKwAAFisgABYrMAAWK0AAFitgABYrcAAWL2AAFi+AABYvoAAWL8AAFi/wABYwAAAWMBAAFjAgABYwMAAWMFAAFjBwABYwgAAWMJAAFjCwABYwwAAWNLAAFjTQABY08AAWNRAAFjVAABY1UAAWNWAAFjVwABY1gAAWNaAAFjXAABY10AAWNeAAFjYAABY2EAAWOsAAFjzwABY+8AAWQPAAFkEQABZBMAAWQVAAFkFwABZBkAAWQaAAFkGwABZB4AAWQfAAFkIQABZCIAAWQkAAFkJgABZCcAAWQoAAFkKwABZCwAAWQ1AAFkQgABZEcAAWRJAAFkSwABZFAAAWRTAAFkVgABZFgAAWR9AAFkoQABZMgAAWTsAAFk7wABZPEAAWTzAAFk9QABZPcAAWT5AAFk+gABZP0AAWUKAAFlGwABZR0AAWUfAAFlIQABZSMAAWUlAAFlJwABZSkAAWUrAAFlPAABZT8AAWVCAAFlRQABZUgAAWVLAAFlTgABZVEAAWVUAAFlVgABZZUAAWWXAAFlmQABZZsAAWWeAAFlnwABZaAAAWWhAAFlogABZaQAAWWmAAFlpwABZagAAWWqAAFlqwABZeoAAWXsAAFl7gABZfAAAWXzAAFl9AABZfUAAWX2AAFl9wABZfkAAWX7AAFl/AABZf0AAWX/AAFmAAABZj8AAWZBAAFmRAABZkYAAWZJAAFmSgABZksAAWZMAAFmTQABZk8AAWZRAAFmUgABZlMAAWZVAAFmVgABZmMAAWZkAAFmZQABZmcAAWamAAFmqAABZqoAAWasAAFmrwABZrAAAWaxAAFmsgABZrMAAWa1AAFmtwABZrgAAWa5AAFmuwABZrwAAWb7AAFm/QABZv8AAWcBAAFnBAABZwUAAWcGAAFnBwABZwgAAWcKAAFnDAABZw0AAWcOAAFnEAABZxEAAWdQAAFnUgABZ1QAAWdWAAFnWQABZ1oAAWdbAAFnXAABZ10AAWdfAAFnYQABZ2IAAWdjAAFnZQABZ2YAAWelAAFnpwABZ6kAAWerAAFnrgABZ68AAWewAAFnsQABZ7IAAWe0AAFntgABZ7cAAWe4AAFnugABZ7sAAWf6AAFn/AABZ/4AAWgAAAFoAwABaAQAAWgFAAFoBgABaAcAAWgJAAFoCwABaAwAAWgNAAFoDwABaBAAAWg1AAFoWQABaIAAAWikAAFopwABaKkAAWirAAForQABaK8AAWixAAFosgABaLUAAWjCAAFo0QABaNMAAWjVAAFo1wABaNkAAWjbAAFo3QABaN8AAWjuAAFo8QABaPQAAWj3AAFo+gABaP0AAWkAAAFpAwABaQUAAWlEAAFpRgABaUgAAWlKAAFpTQABaU4AAWlPAAFpUAABaVEAAWlTAAFpVQABaVYAAWlXAAFpWQABaVoAAWmZAAFpmwABaZ0AAWmfAAFpogABaaMAAWmkAAFppQABaaYAAWmoAAFpqgABaasAAWmsAAFprgABaa8AAWnuAAFp8AABafIAAWn0AAFp9wABafgAAWn5AAFp+gABafsAAWn9AAFp/wABagAAAWoBAAFqAwABagQAAWpDAAFqRQABakcAAWpJAAFqTAABak0AAWpOAAFqTwABalAAAWpSAAFqVAABalUAAWpWAAFqWAABalkAAWqYAAFqmgABapwAAWqeAAFqoQABaqIAAWqjAAFqpAABaqUAAWqnAAFqqQABaqoAAWqrAAFqrQABaq4AAWrtAAFq7wABavEAAWrzAAFq9gABavcAAWr4AAFq+QABavoAAWr8AAFq/gABav8AAWsAAAFrAgABawMAAWtCAAFrRAABa0YAAWtIAAFrSwABa0wAAWtNAAFrTgABa08AAWtRAAFrUwABa1QAAWtVAAFrVwABa1gAAWujAAFrxgABa+YAAWwGAAFsCAABbAoAAWwMAAFsDgABbBAAAWwRAAFsEgABbBUAAWwWAAFsGAABbBkAAWwbAAFsHQABbB4AAWwfAAFsIgABbCMAAWwoAAFsNQABbDoAAWw8AAFsPgABbEMAAWxGAAFsSQABbEsAAWxwAAFslAABbLsAAWzfAAFs4gABbOQAAWzmAAFs6AABbOoAAWzsAAFs7QABbPAAAWz9AAFtDgABbRAAAW0SAAFtFAABbRYAAW0YAAFtGgABbRwAAW0eAAFtLwABbTIAAW01AAFtOAABbTsAAW0+AAFtQQABbUQAAW1HAAFtSQABbYgAAW2KAAFtjAABbY4AAW2RAAFtkgABbZMAAW2UAAFtlQABbZcAAW2ZAAFtmgABbZsAAW2dAAFtngABbd0AAW3fAAFt4QABbeMAAW3mAAFt5wABbegAAW3pAAFt6gABbewAAW3uAAFt7wABbfAAAW3yAAFt8wABbjIAAW40AAFuNwABbjkAAW48AAFuPQABbj4AAW4/AAFuQAABbkIAAW5EAAFuRQABbkYAAW5IAAFuSQABblYAAW5XAAFuWAABbloAAW6ZAAFumwABbp0AAW6fAAFuogABbqMAAW6kAAFupQABbqYAAW6oAAFuqgABbqsAAW6sAAFurgABbq8AAW7uAAFu8AABbvIAAW70AAFu9wABbvgAAW75AAFu+gABbvsAAW79AAFu/wABbwAAAW8BAAFvAwABbwQAAW9DAAFvRQABb0cAAW9JAAFvTAABb00AAW9OAAFvTwABb1AAAW9SAAFvVAABb1UAAW9WAAFvWAABb1kAAW+YAAFvmgABb5wAAW+eAAFvoQABb6IAAW+jAAFvpAABb6UAAW+nAAFvqQABb6oAAW+rAAFvrQABb64AAW/tAAFv7wABb/EAAW/zAAFv9gABb/cAAW/4AAFv+QABb/oAAW/8AAFv/gABb/8AAXAAAAFwAgABcAMAAXAoAAFwTAABcHMAAXCXAAFwmgABcJwAAXCeAAFwoAABcKIAAXCkAAFwpQABcKgAAXC1AAFwxAABcMYAAXDIAAFwygABcMwAAXDOAAFw0AABcNIAAXDhAAFw5AABcOcAAXDqAAFw7QABcPAAAXDzAAFw9gABcPgAAXE3AAFxOQABcTsAAXE9AAFxQAABcUEAAXFCAAFxQwABcUQAAXFGAAFxSAABcUkAAXFKAAFxTAABcU0AAXGMAAFxjgABcZEAAXGTAAFxlgABcZcAAXGYAAFxmQABcZoAAXGcAAFxngABcZ8AAXGgAAFxogABcaMAAXGlAAFx5AABceYAAXHoAAFx6gABce0AAXHuAAFx7wABcfAAAXHxAAFx8wABcfUAAXH2AAFx9wABcfkAAXH6AAFyOQABcjsAAXI+AAFyQAABckMAAXJEAAFyRQABckYAAXJHAAFySQABcksAAXJMAAFyTQABck8AAXJQAAFykwABcrcAAXLbAAFy/gABcyUAAXNFAAFzbAABc5MAAXOzAAFz1wABc/sAAXP9AAF0AAABdAIAAXQEAAF0BgABdAkAAXQMAAF0DgABdBAAAXQTAAF0FQABdBcAAXQaAAF0HQABdB4AAXQnAAF0NAABdDcAAXQ5AAF0PAABdD8AAXRBAAF0ZgABdIoAAXSxAAF01QABdNgAAXTaAAF03AABdN4AAXTgAAF04gABdOMAAXTmAAF08wABdQYAAXUIAAF1CgABdQwAAXUOAAF1EAABdRIAAXUUAAF1FgABdRgAAXUrAAF1LgABdTEAAXU0AAF1NwABdToAAXU9AAF1QAABdUMAAXVGAAF1SAABdYcAAXWJAAF1jAABdY4AAXWRAAF1kgABdZMAAXWUAAF1lQABdZcAAXWZAAF1mgABdZsAAXWdAAF1ngABdacAAXWoAAF1qgABdekAAXXrAAF17QABde8AAXXyAAF18wABdfQAAXX1AAF19gABdfgAAXX6AAF1+wABdfwAAXX+AAF1/wABdj4AAXZAAAF2QwABdkUAAXZIAAF2SQABdkoAAXZLAAF2TAABdk4AAXZQAAF2UQABdlIAAXZUAAF2VQABdl4AAXZhAAF2ZAABdmYAAXZvAAF2cgABdnQAAXZ2AAF2tQABdrcAAXa5AAF2uwABdr4AAXa/AAF2wAABdsEAAXbCAAF2xAABdsYAAXbHAAF2yAABdsoAAXbLAAF3CgABdwwAAXcPAAF3EQABdxQAAXcVAAF3FgABdxcAAXcYAAF3GgABdxwAAXcdAAF3HgABdyAAAXchAAF3KgABdysAAXctAAF3bAABd24AAXdwAAF3cgABd3UAAXd2AAF3dwABd3gAAXd5AAF3ewABd30AAXd+AAF3fwABd4EAAXeCAAF3wQABd8MAAXfGAAF3yAABd8sAAXfMAAF3zQABd84AAXfPAAF30QABd9MAAXfUAAF31QABd9cAAXfYAAF35QABd+YAAXfnAAF36QABeCgAAXgqAAF4LAABeC4AAXgxAAF4MgABeDMAAXg0AAF4NQABeDcAAXg5AAF4OgABeDsAAXg9AAF4PgABeH0AAXh/AAF4ggABeIQAAXiHAAF4iAABeIkAAXiKAAF4iwABeI0AAXiPAAF4kAABeJEAAXiTAAF4lAABeKAAAXitAAF4uAABeLoAAXi8AAF4vgABeMEAAXjEAAF4zwABeNIAAXjVAAF42AABeNsAAXjeAAF44AABeOsAAXj4AAF5QwABeWYAAXmGAAF5pgABeagAAXmqAAF5rAABea4AAXmxAAF5sgABebMAAXm2AAF5twABebkAAXm6AAF5vAABeb4AAXm/AAF5wAABecMAAXnEAAF5zQABedoAAXnfAAF54QABeeMAAXnoAAF56wABee4AAXnwAAF6FQABejkAAXpgAAF6hAABeocAAXqJAAF6iwABeo0AAXqPAAF6kQABepIAAXqVAAF6ogABerMAAXq1AAF6twABerkAAXq7AAF6vQABer8AAXrBAAF6wwABetQAAXrXAAF62gABet0AAXrgAAF64wABeuYAAXrpAAF67AABeu4AAXstAAF7LwABezEAAXszAAF7NgABezcAAXs4AAF7OQABezoAAXs8AAF7PgABez8AAXtAAAF7QgABe0MAAXuCAAF7hAABe4YAAXuIAAF7iwABe4wAAXuNAAF7jgABe48AAXuRAAF7kwABe5QAAXuVAAF7lwABe5gAAXvXAAF72QABe9wAAXveAAF74QABe+IAAXvjAAF75AA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Bl78AAZfBAAGXwwABl8UAAZfIAAGXyQABl8oAAZfLAAGXzAABl84AAZfQAAGX0QABl9IAAZfUAAGX1QABmBQAAZgWAAGYGAABmBoAAZgdAAGYHgABmB8AAZggAAGYIQABmCMAAZglAAGYJgABmCcAAZgpAAGYKgABmGkAAZhrAAGYbQABmG8AAZhyAAGYcwABmHQAAZh1AAGYdgABmHgAAZh6AAGYewABmHwAAZh+AAGYfwABmL4AAZjAAAGYwgABmMQAAZjHAAGYyAABmMkAAZjKAAGYywABmM0AAZjPAAGY0AABmNEAAZjTAAGY1AABmR8AAZlCAAGZYgABmYIAAZmEAAGZhgABmYgAAZmKAAGZjQABmY4AAZmPAAGZkgABmZMAAZmVAAGZlgABmZgAAZmbAAGZnAABmZ0AAZmgAAGZoQABmaYAAZmzAAGZuAABmboAAZm8AAGZwQABmcQAAZnHAAGZyQABme4AAZoSAAGaOQABml0AAZpgAAGaYgABmmQAAZpmAAGaaAABmmoAAZprAAGabgABmnsAAZqMAAGajgABmpAAAZqSAAGalAABmpYAAZqYAAGamgABmpwAAZqtAAGasAABmrMAAZq2AAGauQABmrwAAZq/AAGawgABmsUAAZrHAAGbBgABmwgAAZsKAAGbDAABmw8AAZsQAAGbEQABmxIAAZsTAAGbFQABmxcAAZsYAAGbGQABmxsAAZscAAGbWwABm10AAZtfAAGbYQABm2QAAZtlAAGbZgABm2cAAZtoAAGbagABm2wAAZttAAGbbgABm3AAAZtxAAGbsAABm7IAAZu1AAGbtwABm7oAAZu7AAGbvAABm70AAZu+AAGbwAABm8IAAZvDAAGbxAABm8YAAZvHAAGb1AABm9UAAZvWAAGb2AABnBcAAZwZAAGcGwABnB0AAZwgAAGcIQABnCIAAZwjAAGcJAABnCYAAZwoAAGcKQABnCoAAZwsAAGcLQABnGwAAZxuAAGccAABnHIAAZx1AAGcdgABnHcAAZx4AAGceQABnHsAAZx9AAGcfgABnH8AAZyBAAGcggABnMEAAZzDAAGcxQABnMcAAZzKAAGcywABnMwAAZzNAAGczgABnNAAAZzSAAGc0wABnNQAAZzWAAGc1wABnRYAAZ0YAAGdGgABnRwAAZ0fAAGdIAABnSEAAZ0iAAGdIwABnSUAAZ0nAAGdKAABnSkAAZ0rAAGdLAABnWsAAZ1tAAGdbwABnXEAAZ10AAGddQABnXYAAZ13AAGdeAABnXoAAZ18AAGdfQABnX4AAZ2AAAGdgQABnaYAAZ3KAAGd8QABnhUAAZ4YAAGeGgABnhwAAZ4eAAGeIAABniIAAZ4jAAGeJgABnjMAAZ5CAAGeRAABnkYAAZ5IAAGeSgABnkwAAZ5OAAGeUAABnl8AAZ5iAAGeZQABnmgAAZ5rAAGebgABnnEAAZ50AAGedgABnrUAAZ63AAGeuQABnrsAAZ6+AAGevwABnsAAAZ7BAAGewgABnsQAAZ7GAAGexwABnsgAAZ7KAAGeywABnwoAAZ8MAAGfDgABnxAAAZ8TAAGfFAABnxUAAZ8WAAGfFwABnxkAAZ8bAAGfHAABnx0AAZ8fAAGfIAABn18AAZ9hAAGfYwABn2UAAZ9oAAGfaQABn2oAAZ9rAAGfbAABn24AAZ9wAAGfcQABn3IAAZ90AAGfdQABn7QAAZ+2AAGfuAABn7oAAZ+9AAGfvgABn78AAZ/AAAGfwQABn8MAAZ/FAAGfxgABn8cAAZ/JAAGfygABoAkAAaALAAGgDQABoA8AAaASAAGgEwABoBQAAaAVAAGgFgABoBgAAaAaAAGgGwABoBwAAaAeAAGgHwABoF4AAaBgAAGgYgABoGQAAaBnAAGgaAABoGkAAaBqAAGgawABoG0AAaBvAAGgcAABoHEAAaBzAAGgdAABoLMAAaC1AAGgtwABoLkAAaC8AAGgvQABoL4AAaC/AAGgwAABoMIAAaDEAAGgxQABoMYAAaDIAAGgyQABoNIAAaDTAAGg1QABoRQAAaEWAAGhGAABoRoAAaEdAAGhHgABoR8AAaEgAAGhIQABoSMAAaElAAGhJgABoScAAaEpAAGhKgABoWkAAaFrAAGhbQABoW8AAaFyAAGhcwABoXQAAaF1AAGhdgABoXgAAaF6AAGhewABoXwAAaF+AAGhfwABob4AAaHAAAGhwwABocUAAaHIAAGhyQABocoAAaHLAAGhzAABoc4AAaHQAAGh0QABodIAAaHUAAGh1QABoiAAAaJDAAGiYwABooMAAaKFAAGihwABookAAaKLAAGijQABoo4AAaKPAAGikgABopMAAaKVAAGilgABopgAAaKaAAGimwABopwAAaKfAAGioAABoqUAAaKyAAGitwABorkAAaK7AAGiwAABosMAAaLGAAGiyAABou0AAaMRAAGjOAABo1wAAaNfAAGjYQABo2MAAaNlAAGjZwABo2kAAaNqAAGjbQABo3oAAaOLAAGjjQABo48AAaORAAGjkwABo5UAAaOXAAGjmQABo5sAAaOsAAGjrwABo7IAAaO1AAGjuAABo7sAAaO+AAGjwQABo8QAAaPGAAGkBQABpAcAAaQJAAGkCwABpA4AAaQPAAGkEAABpBEAAaQSAAGkFAABpBYAAaQXAAGkGAABpBoAAaQbAAGkWgABpFwAAaReAAGkYAABpGMAAaRkAAGkZQABpGYAAaRnAAGkaQABpGsAAaRsAAGkbQABpG8AAaRwAAGkrwABpLEAAaS0AAGktgABpLkAAaS6AAGkuwABpLwAAaS9AAGkvwABpMEAAaTCAAGkwwABpMUAAaTGAAGk0wABpNQAAaTVAAGk1wABpRYAAaUYAAGlGgABpRwAAaUfAAGlIAABpSEAAaUiAAGlIwABpSUAAaUnAAGlKAABpSkAAaUrAAGlLAABpWsAAaVtAAGlbwABpXEAAaV0AAGldQABpXYAAaV3AAGleAABpXoAAaV8AAGlfQABpX4AAaWAAAGlgQABpcAAAaXCAAGlxAABpcYAAaXJAAGlygABpcsAAaXMAAGlzQABpc8AAaXRAAGl0gABpdMAAaXVAAGl1gABphUAAaYXAAGmGQABphsAAaYeAAGmHwABpiAAAaYhAAGmIgABpiQAAaYmAAGmJwABpigAAaYqAAGmKwABpmoAAaZsAAGmbgABpnAAAaZzAAGmdAABpnUAAaZ2AAGmdwABpnkAAaZ7AAGmfAABpn0AAaZ/AAGmgAABpqUAAabJAAGm8AABpxQAAacXAAGnGQABpxsAAacdAAGnHwABpyEAAaciAAGnJQABpzIAAac/AAGnQQABp0MAAadFAAGnRwABp0kAAadLAAGnWAABp1sAAadeAAGnYQABp2QAAadnAAGnagABp2wAAaerAAGnrQABp7AAAaeyAAGntQABp7YAAae3AAGnuAABp7kAAae7AAGnvQABp74AAae/AAGnwQABp8IAAagBAAGoAwABqAUAAagHAAGoCgABqAsAAagMAAGoDQABqA4AAagQAAGoEgABqBMAAagUAAGoFgABqBcAAahWAAGoWAABqFoAAahcAAGoXwABqGAAAahhAAGoYgABqGMAAahlAAGoZwABqGgAAahpAAGoawABqGwAAairAAGorQABqK8AAaixAAGotAABqLUAAai2AAGotwABqLgAAai6AAGovAABqL0AAai+AAGowAABqMEAAakAAAGpAgABqQQAAakGAAGpCQABqQoAAakLAAGpDAABqQ0AAakPAAGpEQABqRIAAakTAAGpFQABqRYAAalVAAGpVwABqVkAAalbAAGpXgABqV8AAalgAAGpYQABqWIAAalkAAGpZgABqWcAAaloAAGpagABqWsAAam2AAGp2QABqfkAAaoZAAGqGwABqh0AAaofAAGqIQABqiMAAaokAAGqJQABqigAAaopAAGqKwABqiwAAaouAAGqMAABqjEAAaoyAAGqNQABqjYAAao7AAGqSAABqk0AAapPAAGqUQABqlYAAapZAAGqXAABql4AAaqDAAGqpwABqs4AAaryAAGq9QABqvcAAar5AAGq+wABqv0AAar/AAGrAAABqwMAAasQAAGrIQABqyMAAaslAAGrJwABqykAAasrAAGrLQABqy8AAasxAAGrQgABq0UAAatIAAGrSwABq04AAatRAAGrVAABq1cAAataAAGrXAABq5sAAaudAAGrnwABq6EAAaukAAGrpQABq6YAAaunAAGrqAABq6oAAausAAGrrQABq64AAauwAAGrsQABq/AAAavyAAGr9AABq/YAAav5AAGr+gABq/sAAav8AAGr/QABq/8AAawBAAGsAgABrAMAAawFAAGsBgABrEUAAaxHAAGsSgABrEwAAaxPAAGsUAABrFEAAaxSAAGsUwABrFUAAaxXAAGsWAABrFkAAaxbAAGsXAABrGkAAaxqAAGsawABrG0AAaysAAGsrgABrLAAAayyAAGstQABrLYAAay3AAGsuAABrLkAAay7AAGsvQABrL4AAay/AAGswQABrMIAAa0BAAGtAwABrQUAAa0HAAGtCgABrQsAAa0MAAGtDQABrQ4AAa0QAAGtEgABrRMAAa0UAAGtFgABrRcAAa1WAAGtWAABrVoAAa1cAAGtXwABrWAAAa1hAAGtYgABrWMAAa1lAAGtZwABrWgAAa1pAAGtawABrWwAAa2rAAGtrQABra8AAa2xAAGttAABrbUAAa22AAGttwABrbgAAa26AAGtvAABrb0AAa2+AAGtwAABrcEAAa4AAAGuAgABrgQAAa4GAAGuCQABrgoAAa4LAAGuDAABrg0AAa4PAAGuEQABrhIAAa4TAAGuFQABrhYAAa47AAGuXwABroYAAa6qAAGurQABrq8AAa6xAAGuswABrrUAAa63AAGuuAABrrsAAa7IAAGu1wABrtkAAa7bAAGu3QABrt8AAa7hAAGu4wABruUAAa70AAGu9wABrvoAAa79AAGvAAABrwMAAa8GAAGvCQABrwsAAa9KAAGvTAABr04AAa9QAAGvUwABr1QAAa9VAAGvVgABr1cAAa9ZAAGvWwABr1wAAa9dAAGvXwABr2AAAa+fAAGvoQABr6MAAa+lAAGvqAABr6kAAa+qAAGvqwABr6wAAa+uAAGvsAABr7EAAa+yAAGvtAABr7UAAa/0AAGv9gABr/gAAa/6AAGv/QABr/4AAa//AAGwAAABsAEAAbADAAGwBQABsAYAAbAHAAGwCQABsAoAAbBJAAGwSwABsE0AAbBPAAGwUgABsFMAAbBUAAGwVQABsFYAAbBYAAGwWgABsFsAAbBcAAGwXgABsF8AAbCeAAGwoAABsKIAAbCkAAGwpwABsKgAAbCpAAGwqgABsKsAAbCtAAGwrwABsLAAAbCxAAGwswABsLQAAbDzAAGw9QABsPcAAbD5AAGw/AABsP0AAbD+AAGw/wABsQAAAbECAAGxBAABsQUAAbEGAAGxCAABsQkAAbFIAAGxSgABsUwAAbFOAAGxUQABsVIAAbFTAAGxVAABsVUAAbFXAAGxWQABsVoAAbFbAAGxXQABsV4AAbGpAAGxzAABsewAAbIMAAGyDgABshAAAbISAAGyFAABshYAAbIXAAGyGAABshsAAbIcAAGyHgABsh8AAbIhAAGyIwABsiQAAbIlAAGyKAABsikAAbIuAAGyOwABskAAAbJCAAGyRAABskkAAbJMAAGyTwABslEAAbJ2AAGymgABssEAAbLlAAGy6AABsuoAAbLsAAGy7gABsvAAAbLyAAGy8wABsvYAAbMDAAGzFAABsxYAAbMYAAGzGgABsxwAAbMeAAGzIAABsyIAAbMkAAGzNQABszgAAbM7AAGzPgABs0EAAbNEAAGzRwABs0oAAbNNAAGzTwABs44AAbOQAAGzkgABs5QAAbOXAAGzmAABs5kAAbOaAAGzmwABs50AAbOfAAGzoAABs6EAAbOjAAGzpAABs+MAAbPlAAGz5wABs+kAAbPsAAGz7QABs+4AAbPvAAGz8AABs/IAAbP0AAGz9QABs/YAAbP4AAGz+QABtDgAAbQ6AAG0PQABtD8AAbRCAAG0QwABtEQAAbRFAAG0RgABtEgAAbRKAAG0SwABtEwAAbROAAG0TwABtFwAAbRdAAG0XgABtGAAAbSfAAG0oQABtKMAAbSlAAG0qAABtKkAAbSqAAG0qwABtKwAAbSuAAG0sAABtLEAAbSyAAG0tAABtLUAAbT0AAG09gABtPgAAbT6AAG0/QABtP4AAbT/AAG1AAABtQEAAbUDAAG1BQABtQYAAbUHAAG1CQABtQoAAbVJAAG1SwABtU0AAbVPAAG1UgABtVMAAbVUAAG1VQABtVYAAbVYAAG1WgABtVsAAbVcAAG1XgABtV8AAbWeAAG1oAABtaIAAbWkAAG1pwABtagAAbWpAAG1qgABtasAAbWtAAG1rwABtbAAAbWxAAG1swABtbQAAbXzAAG19QABtfcAAbX5AAG1/AABtf0AAbX+AAG1/wABtgAAAbYCAAG2BAABtgUAAbYGAAG2CAABtgkAAbYuAAG2UgABtnkAAbadAAG2oAABtqIAAbakAAG2pgABtqgAAbaqAAG2qwABtq4AAba7AAG2ygABtswAAbbOAAG20AABttIAAbbUAAG21gABttgAAbbnAAG26gABtu0AAbbwAAG28wABtvYAAbb5AAG2/AABtv4AAbc9AAG3PwABt0IAAbdEAAG3RwABt0gAAbdJAAG3SgABt0sAAbdNAAG3TwABt1AAAbdRAAG3UwABt1QAAbeTAAG3lQABt5cAAbeZAAG3nAABt50AAbeeAAG3nwABt6AAAbeiAAG3pAABt6UAAbemAAG3qAABt6kAAbfoAAG36gABt+wAAbfuAAG38QABt/IAAbfzAAG39AABt/UAAbf3AAG3+QABt/oAAbf7AAG3/QABt/4AAbg9AAG4PwABuEIAAbhEAAG4RwABuEgAAbhJAAG4SgABuEsAAbhNAAG4TwABuFAAAbhRAAG4UwABuFQAAbiTAAG4lQABuJcAAbiZAAG4nAABuJ0AAbieAAG4nwABuKAAAbiiAAG4pAABuKUAAbimAAG4qAABuKkAAbjoAAG46gABuOwAAbjuAAG48QABuPIAAbjzAAG49AABuPUAAbj3AAG4+QABuPoAAbj7AAG4/QABuP4AAbk9AAG5PwABuUEAAblDAAG5RgABuUcAAblIAAG5SQABuUoAAblMAAG5TgABuU8AAblQAAG5UgABuVMAAbmeAAG5wQABueEAAboBAAG6AwABugUAAboHAAG6CQABugsAAboMAAG6DQABuhAAAboRAAG6EwABuhQAAboWAAG6GAABuhkAAboaAAG6HQABuh4AAbonAAG6NAABujkAAbo7AAG6PQABukIAAbpFAAG6SAABukoAAbpvAAG6kwABuroAAbreAAG64QABuuMAAbrlAAG65wABuukAAbrrAAG67AABuu8AAbr8AAG7DQABuw8AAbsRAAG7EwABuxUAAbsXAAG7GQABuxsAAbsdAAG7LgABuzEAAbs0AAG7NwABuzoAAbs9AAG7QAABu0MAAbtGAAG7SAABu4cAAbuJAAG7iwABu40AAbuQAAG7kQABu5IAAbuTAAG7lAABu5YAAbuYAAG7mQABu5oAAbucAAG7nQABu9wAAbveAAG74AABu+IAAbvlAAG75gABu+cAAbvoAAG76QABu+sAAbvtAAG77gABu+8AAbvxAAG78gABvDEAAbwzAAG8NgABvDgAAbw7AAG8PAABvD0AAbw+AAG8PwABvEEAAbxDAAG8RAABvEUAAbxHAAG8SAABvFUAAbxWAAG8VwABvFkAAbyYAAG8mgABvJwAAbyeAAG8oQABvKIAAbyjAAG8pAABvKUAAbynAAG8qQABvKoAAbyrAAG8rQABvK4AAbztAAG87wABvPEAAbzzAAG89gABvPcAAbz4AAG8+QABvPoAAbz8AAG8/gABvP8AAb0AAAG9AgABvQMAAb1CAAG9RAABvUYAAb1IAAG9SwABvUwAAb1NAAG9TgABvU8AAb1RAAG9UwABvVQAAb1VAAG9VwABvVgAAb2XAAG9mQABvZsAAb2dAAG9oAABvaEAAb2iAAG9owABvaQAAb2mAAG9qAABvakAAb2qAAG9rAABva0AAb3sAAG97gABvfAAAb3yAAG99QABvfYAAb33AAG9+AABvfkAAb37AAG9/QABvf4AAb3/AAG+AQABvgIAAb4nAAG+SwABvnIAAb6WAAG+mQABvpsAAb6dAAG+nwABvqEAAb6jAAG+pAABvqcAAb60AAG+wwA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B1NoAAdTbAAHU3QAB1N4AAdUdAAHVHwAB1SEAAdUjAAHVJgAB1ScAAdUoAAHVKQAB1SoAAdUsAAHVLgAB1S8AAdUwAAHVMgAB1TMAAdVyAAHVdAAB1XYAAdV4AAHVewAB1XwAAdV9AAHVfgAB1X8AAdWBAAHVgwAB1YQAAdWFAAHVhwAB1YgAAdXHAAHVyQAB1csAAdXNAAHV0AAB1dEAAdXSAAHV0wAB1dQAAdXWAAHV2AAB1dkAAdXaAAHV3AAB1d0AAdYCAAHWJgAB1k0AAdZxAAHWdAAB1nYAAdZ4AAHWegAB1nwAAdZ+AAHWfwAB1oIAAdaPAAHWngAB1qAAAdaiAAHWpAAB1qYAAdaoAAHWqgAB1qwAAda7AAHWvgAB1sEAAdbEAAHWxwAB1soAAdbNAAHW0AAB1tIAAdcRAAHXEwAB1xUAAdcXAAHXGgAB1xsAAdccAAHXHQAB1x4AAdcgAAHXIgAB1yMAAdckAAHXJgAB1ycAAddmAAHXaAAB12oAAddsAAHXbwAB13AAAddxAAHXcgAB13MAAdd1AAHXdwAB13gAAdd5AAHXewAB13wAAde7AAHXvQAB178AAdfBAAHXxAAB18UAAdfGAAHXxwAB18gAAdfKAAHXzAAB180AAdfOAAHX0AAB19EAAdgQAAHYEgAB2BQAAdgWAAHYGQAB2BoAAdgbAAHYHAAB2B0AAdgfAAHYIQAB2CIAAdgjAAHYJQAB2CYAAdhlAAHYZwAB2GkAAdhrAAHYbgAB2G8AAdhwAAHYcQAB2HIAAdh0AAHYdgAB2HcAAdh4AAHYegAB2HsAAdi6AAHYvAAB2L4AAdjAAAHYwwAB2MQAAdjFAAHYxgAB2McAAdjJAAHYywAB2MwAAdjNAAHYzwAB2NAAAdkPAAHZEQAB2RMAAdkVAAHZGAAB2RkAAdkaAAHZGwAB2RwAAdkeAAHZIAAB2SEAAdkiAAHZJAAB2SUAAdlwAAHZkwAB2bMAAdnTAAHZ1QAB2dcAAdnZAAHZ2wAB2d0AAdneAAHZ3wAB2eIAAdnjAAHZ5QAB2eYAAdnoAAHZ6gAB2esAAdnsAAHZ7wAB2fAAAdn1AAHaAgAB2gcAAdoJAAHaCwAB2hAAAdoTAAHaFgAB2hgAAdo9AAHaYQAB2ogAAdqsAAHarwAB2rEAAdqzAAHatQAB2rcAAdq5AAHaugAB2r0AAdrKAAHa2wAB2t0AAdrfAAHa4QAB2uMAAdrlAAHa5wAB2ukAAdrrAAHa/AAB2v8AAdsCAAHbBQAB2wgAAdsLAAHbDgAB2xEAAdsUAAHbFgAB21UAAdtXAAHbWQAB21sAAdteAAHbXwAB22AAAdthAAHbYgAB22QAAdtmAAHbZwAB22gAAdtqAAHbawAB26oAAdusAAHbrgAB27AAAduzAAHbtAAB27UAAdu2AAHbtwAB27kAAdu7AAHbvAAB270AAdu/AAHbwAAB2/8AAdwBAAHcBAAB3AYAAdwJAAHcCgAB3AsAAdwMAAHcDQAB3A8AAdwRAAHcEgAB3BMAAdwVAAHcFgAB3CMAAdwkAAHcJQAB3CcAAdxmAAHcaAAB3GoAAdxsAAHcbwAB3HAAAdxxAAHccgAB3HMAAdx1AAHcdwAB3HgAAdx5AAHcewAB3HwAAdy7AAHcvQAB3L8AAdzBAAHcxAAB3MUAAdzGAAHcxwAB3MgAAdzKAAHczAAB3M0AAdzOAAHc0AAB3NEAAd0QAAHdEgAB3RQAAd0WAAHdGQAB3RoAAd0bAAHdHAAB3R0AAd0fAAHdIQAB3SIAAd0jAAHdJQAB3SYAAd1lAAHdZwAB3WkAAd1rAAHdbgAB3W8AAd1wAAHdcQAB3XIAAd10AAHddgAB3XcAAd14AAHdegAB3XsAAd26AAHdvAAB3b4AAd3AAAHdwwAB3cQAAd3FAAHdxgAB3ccAAd3JAAHdywAB3cwAAd3NAAHdzwAB3dAAAd31AAHeGQAB3kAAAd5kAAHeZwAB3mkAAd5rAAHebQAB3m8AAd5xAAHecgAB3nUAAd6CAAHejwAB3pEAAd6TAAHelQAB3pcAAd6ZAAHemwAB3qgAAd6rAAHergAB3rEAAd60AAHetwAB3roAAd68AAHe+wAB3v0AAd8AAAHfAgAB3wUAAd8GAAHfBwAB3wgAAd8JAAHfCwAB3w0AAd8OAAHfDwAB3xEAAd8SAAHfUQAB31MAAd9VAAHfVwAB31oAAd9bAAHfXAAB310AAd9eAAHfYAAB32IAAd9jAAHfZAAB32YAAd9nAAHfpgAB36gAAd+qAAHfrAAB368AAd+wAAHfsQAB37IAAd+zAAHftQAB37cAAd+4AAHfuQAB37sAAd+8AAHf+wAB3/0AAeAAAAHgAgAB4AUAAeAGAAHgBwAB4AgAAeAJAAHgCwAB4A0AAeAOAAHgDwAB4BEAAeASAAHgUQAB4FMAAeBVAAHgVwAB4FoAAeBbAAHgXAAB4F0AAeBeAAHgYAAB4GIAAeBjAAHgZAAB4GYAAeBnAAHgpgAB4KgAAeCqAAHgrAAB4K8AAeCwAAHgsQAB4LIAAeCzAAHgtQAB4LcAAeC4AAHguQAB4LsAAeC8AAHgxQAB4MYAAeDIAAHhBwAB4QkAAeELAAHhDQAB4Q8AAeEQAAHhEQAB4RIAAeETAAHhFQAB4RcAAeEYAAHhGQAB4RsAAeEcAAHhWwAB4V0AAeFfAAHhYQAB4WMAAeFkAAHhZQAB4WYAAeFnAAHhaQAB4WsAAeFsAAHhbQAB4W8AAeFwAAHhrwAB4bEAAeGzAAHhtQAB4bcAAeG4AAHhuQAB4boAAeG7AAHhvQAB4b8AAeHAAAHhwQAB4cMAAeHEAAHiDwAB4jIAAeJSAAHicgAB4nQAAeJ2AAHieAAB4noAAeJ8AAHifQAB4n4AAeKBAAHiggAB4oQAAeKFAAHihwAB4okAAeKKAAHiiwAB4o4AAeKPAAHilAAB4qEAAeKmAAHiqAAB4qoAAeKvAAHisgAB4rUAAeK3AAHi3AAB4wAAAeMnAAHjSwAB404AAeNQAAHjUgAB41QAAeNWAAHjWAAB41kAAeNcAAHjaQAB43oAAeN8AAHjfgAB44AAAeOCAAHjhAAB44YAAeOIAAHjigAB45sAAeOeAAHjoQAB46QAAeOnAAHjqgAB460AAeOwAAHjswAB47UAAeP0AAHj9gAB4/gAAeP6AAHj/QAB4/4AAeP/AAHkAAAB5AEAAeQDAAHkBQAB5AYAAeQHAAHkCQAB5AoAAeRJAAHkSwAB5E0AAeRPAAHkUgAB5FMAAeRUAAHkVQAB5FYAAeRYAAHkWgAB5FsAAeRcAAHkXgAB5F8AAeSeAAHkoAAB5KMAAeSlAAHkqAAB5KkAAeSqAAHkqwAB5KwAAeSuAAHksAAB5LEAAeSyAAHktAAB5LUAAeTCAAHkwwAB5MQAAeTGAAHlBQAB5QcAAeUJAAHlCwAB5Q4AAeUPAAHlEAAB5REAAeUSAAHlFAAB5RYAAeUXAAHlGAAB5RoAAeUbAAHlWgAB5VwAAeVeAAHlYAAB5WMAAeVkAAHlZQAB5WYAAeVnAAHlaQAB5WsAAeVsAAHlbQAB5W8AAeVwAAHlrwAB5bEAAeWzAAHltQAB5bgAAeW5AAHlugAB5bsAAeW8AAHlvgAB5cAAAeXBAAHlwgAB5cQAAeXFAAHmBAAB5gYAAeYIAAHmCgAB5g0AAeYOAAHmDwAB5hAAAeYRAAHmEwAB5hUAAeYWAAHmFwAB5hkAAeYaAAHmWQAB5lsAAeZdAAHmXwAB5mIAAeZjAAHmZAAB5mUAAeZmAAHmaAAB5moAAeZrAAHmbAAB5m4AAeZvAAHmlAAB5rgAAebfAAHnAwAB5wYAAecIAAHnCgAB5wwAAecOAAHnEAAB5xEAAecUAAHnIQAB5zAAAecyAAHnNAAB5zYAAec4AAHnOgAB5zwAAec+AAHnTQAB51AAAedTAAHnVgAB51kAAedcAAHnXwAB52IAAedkAAHnowAB56UAAeenAAHnqQAB56wAAeetAAHnrgAB568AAeewAAHnsgAB57QAAee1AAHntgAB57gAAee5AAHn+AAB5/oAAef8AAHn/gAB6AEAAegCAAHoAwAB6AQAAegFAAHoBwAB6AkAAegKAAHoCwAB6A0AAegOAAHoTQAB6E8AAehRAAHoUwAB6FYAAehXAAHoWAAB6FkAAehaAAHoXAAB6F4AAehfAAHoYAAB6GIAAehjAAHoogAB6KQAAeinAAHoqQAB6KwAAeitAAHorgAB6K8AAeiwAAHosgAB6LQAAei1AAHotgAB6LgAAei5AAHo+AAB6PoAAej8AAHo/gAB6QEAAekCAAHpAwAB6QQAAekFAAHpBwAB6QkAAekKAAHpCwAB6Q0AAekOAAHpTQAB6U8AAelSAAHpVAAB6VcAAelYAAHpWQAB6VoAAelbAAHpXQAB6V8AAelgAAHpYQAB6WMAAelkAAHpgAAB6b8AAenBAAHpwwAB6cUAAenIAAHpyQAB6coAAenLAAHpzAAB6c4AAenQAAHp0QAB6dIAAenUAAHp1QAB6iAAAepDAAHqYwAB6oMAAeqFAAHqhwAB6okAAeqLAAHqjQAB6o4AAeqPAAHqkgAB6pMAAeqVAAHqlgAB6pgAAeqaAAHqmwAB6pwAAeqfAAHqoAAB6qUAAeqyAAHqtwAB6rkAAeq7AAHqwAAB6sMAAerGAAHqyAAB6u0AAesRAAHrOAAB61wAAetfAAHrYQAB62MAAetlAAHrZwAB62kAAetqAAHrbQAB63oAAeuLAAHrjQAB648AAeuRAAHrkwAB65UAAeuXAAHrmQAB65sAAeusAAHrrwAB67IAAeu1AAHruAAB67sAAeu+AAHrwQAB68QAAevGAAHsBQAB7AcAAewJAAHsCwAB7A4AAewPAAHsEAAB7BEAAewSAAHsFAAB7BYAAewXAAHsGAAB7BoAAewbAAHsWgAB7FwAAexeAAHsYAAB7GMAAexkAAHsZQAB7GYAAexnAAHsaQAB7GsAAexsAAHsbQAB7G8AAexwAAHsrwAB7LEAAey0AAHstgAB7LkAAey6AAHsuwAB7LwAAey9AAHsvwAB7MEAAezCAAHswwAB7MUAAezGAAHs0wAB7NQAAezVAAHs1wAB7RYAAe0YAAHtGgAB7RwAAe0fAAHtIAAB7SEAAe0iAAHtIwAB7SUAAe0nAAHtKAAB7SkAAe0rAAHtLAAB7WsAAe1tAAHtbwAB7XEAAe10AAHtdQAB7XYAAe13AAHteAAB7XoAAe18AAHtfQAB7X4AAe2AAAHtgQAB7cAAAe3CAAHtxAAB7cYAAe3JAAHtygAB7csAAe3MAAHtzQAB7c8AAe3RAAHt0gAB7dMAAe3VAAHt1gAB7hUAAe4XAAHuGQAB7hsAAe4eAAHuHwAB7iAAAe4hAAHuIgAB7iQAAe4mAAHuJwAB7igAAe4qAAHuKwAB7moAAe5sAAHubgAB7nAAAe5zAAHudAAB7nUAAe52AAHudwAB7nkAAe57AAHufAAB7n0AAe5/AAHugAAB7qUAAe7JAAHu8AAB7xQAAe8XAAHvGQAB7xsAAe8dAAHvHwAB7yEAAe8iAAHvJQAB7zIAAe9BAAHvQwAB70UAAe9HAAHvSQAB70sAAe9NAAHvTwAB714AAe9hAAHvZAAB72cAAe9qAAHvbQAB73AAAe9zAAHvdQAB77QAAe+2AAHvuAAB77oAAe+9AAHvvgAB778AAe/AAAHvwQAB78MAAe/FAAHvxgAB78cAAe/JAAHvygAB8AkAAfALAAHwDQAB8A8AAfASAAHwEwAB8BQAAfAVAAHwFgAB8BgAAfAaAAHwGwAB8BwAAfAeAAHwHwAB8F4AAfBgAAHwYgAB8GQAAfBnAAHwaAAB8GkAAfBqAAHwawAB8G0AAfBvAAHwcAAB8HEAAfBzAAHwdAAB8LMAAfC1AAHwtwAB8LkAAfC8AAHwvQAB8L4AAfC/AAHwwAAB8MIAAfDEAAHwxQAB8MYAAfDIAAHwyQAB8QgAAfEKAAHxDAAB8Q4AAfERAAHxEgAB8RMAAfEUAAHxFQAB8RcAAfEZAAHxGgAB8RsAAfEdAAHxHgAB8V0AAfFfAAHxYQAB8WMAAfFmAAHxZwAB8WgAAfFpAAHxagAB8WwAAfFuAAHxbwAB8XAAAfFyAAHxcwAB8bIAAfG0AAHxtgAB8bgAAfG7AAHxvAAB8b0AAfG+AAHxvwAB8cEAAfHDAAHxxAAB8cUAAfHHAAHxyAAB8hMAAfI2AAHyVgAB8nYAAfJ4AAHyegAB8nwAAfJ+AAHygAAB8oEAAfKCAAHyhQAB8oYAAfKIAAHyiQAB8osAAfKNAAHyjgAB8o8AAfKSAAHykwAB8pwAAfKpAAHyrgAB8rAAAfKyAAHytwAB8roAAfK9AAHyvwAB8uQAAfMIAAHzLwAB81MAAfNWAAHzWAAB81oAAfNcAAHzXgAB82AAAfNhAAHzZAAB83EAAfOCAAHzhAAB84YAAfOIAAHzigAB84wAAfOOAAHzkAAB85IAAfOjAAHzpgAB86kAAfOsAAHzrwAB87IAAfO1AAHzuAAB87sAAfO9AAHz/AAB8/4AAfQAAAH0AgAB9AUAAfQGAAH0BwAB9AgAAfQJAAH0CwAB9A0AAfQOAAH0DwAB9BEAAfQSAAH0UQAB9FMAAfRVAAH0VwAB9FoAAfRbAAH0XAAB9F0AAfReAAH0YAAB9GIAAfRjAAH0ZAAB9GYAAfRnAAH0pgAB9KgAAfSrAAH0rQAB9LAAAfSxAAH0sgAB9LMAAfS0AAH0tgAB9LgAAfS5AAH0ugAB9LwAAfS9AAH0ygAB9MsAAfTMAAH0zgAB9Q0AAfUPAAH1EQAB9RMAAfUWAAH1FwAB9RgAAfUZAAH1GgAB9RwAAfUeAAH1HwAB9SAAAfUiAAH1IwAB9WIAAfVkAAH1ZgAB9WgAAfVrAAH1bAAB9W0AAfVuAAH1bwAB9XEAAfVzAAH1dAAB9XUAAfV3AAH1eAAB9bcAAfW5AAH1uwAB9b0AAfXAAAH1wQAB9cIAAfXDAAH1xAAB9cYAAfXIAAH1yQAB9coAAfXMAAH1zQAB9gwAAfYOAAH2EAAB9hIAAfYVAAH2FgAB9hcAAfYYAAH2GQAB9hsAAfYdAAH2HgAB9h8AAfYhAAH2IgAB9mEAAfZjAAH2ZQAB9mcAAfZqAAH2awAB9mwAAfZtAAH2bgAB9nAAAfZyAAH2cwAB9nQAAfZ2AAH2dwAB9pwAAfbAAAH25wAB9wsAAfcOAAH3EAAB9xIAAfcUAAH3FgAB9xgAAfcZAAH3HAAB9ykAAfc4AAH3OgAB9zwAAfc+AAH3QAAB90IAAfdEAAH3RgAB91UAAfdYAAH3WwAB914AAfdhAAH3ZAAB92cAAfdqAAH3bAAB96sAAfetAAH3rwAB97EAAfe0AAH3tQAB97YAAfe3AAH3uAAB97oAAfe8AAH3vQAB974AAffAAAH3wQAB+AAAAfgCAAH4BAAB+AYAAfgJAAH4CgAB+AsAAfgMAAH4DQAB+A8AAfgRAAH4EgAB+BMAAfgVAAH4FgAB+FUAAfhXAAH4WQAB+FsAAfheAAH4XwAB+GAAAfhhAAH4YgAB+GQAAfhmAAH4ZwAB+GgAAfhqAAH4awAB+KoAAfisAAH4rgAB+LAAAfizAAH4tAAB+LUAAfi2AAH4twAB+LkAAfi7AAH4vAAB+L0AAfi/AAH4wAAB+P8AAfkBAAH5AwAB+QUAAfkIAAH5CQAB+QoAAfkLAAH5DAAB+Q4AAfkQAAH5EQAB+RIAAfkUAAH5FQAB+VQAAflWAAH5WAAB+VoAAfldAAH5XgAB+V8AAflgAAH5YQAB+WMAAfllAAH5ZgAB+WcAAflpAAH5agAB+akAAfmrAAH5rQAB+a8AAfmyAAH5swAB+bQAAfm1AAH5tgAB+bgAAfm6AAH5uwAB+bwAAfm+AAH5vwAB+cgAAfnJAAH5ywAB+g4AAfoyAAH6VgAB+nkAAfqgAAH6wAAB+ucAAfsOAAH7LgAB+1IAAft2AAH7eAAB+3sAAft9AAH7fwAB+4EAAfuEAAH7hwAB+4kAAfuLAAH7jgAB+5AAAfuSAAH7lQAB+5gAAfuZAAH7ngAB+6sAAfuuAAH7sAAB+7MAAfu2AAH7uAAB+90AAfwBAAH8KAAB/EwAAfxPAAH8UQAB/FMAAfxVAAH8VwAB/FkAAfxaAAH8XQAB/GoAAfx9AAH8fwAB/IEAAfyDAAH8hQAB/IcAAfyJAAH8iwAB/I0AAfyPAAH8ogAB/KUAAfyoAAH8qwAB/K4AAfyxAAH8tAAB/LcAAfy6AAH8vQAB/L8AAfz+AAH9AAAB/QMAAf0FAAH9CAA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CB8YAAgfHAAIHyQACB8oAAggJAAIICwACCA0AAggPAAIIEgACCBMAAggUAAIIFQACCBYAAggYAAIIGgACCBsAAggcAAIIHgACCB8AAgheAAIIYAACCGIAAghkAAIIZwACCGgAAghpAAIIagACCGsAAghtAAIIbwACCHAAAghxAAIIcwACCHQAAgi/AAII4gACCQIAAgkiAAIJJAACCSYAAgkoAAIJKgACCS0AAgkuAAIJLwACCTIAAgkzAAIJNQACCTYAAgk4AAIJOwACCTwAAgk9AAIJQAACCUEAAglGAAIJUwACCVgAAglaAAIJXAACCWEAAglkAAIJZwACCWkAAgmOAAIJsgACCdkAAgn9AAIKAAACCgIAAgoEAAIKBgACCggAAgoKAAIKCwACCg4AAgobAAIKLAACCi4AAgowAAIKMgACCjQAAgo2AAIKOAACCjoAAgo8AAIKTQACClAAAgpTAAIKVgACClkAAgpcAAIKXwACCmIAAgplAAIKZwACCqYAAgqoAAIKqgACCqwAAgqvAAIKsAACCrEAAgqyAAIKswACCrUAAgq3AAIKuAACCrkAAgq7AAIKvAACCvsAAgr9AAIK/wACCwEAAgsEAAILBQACCwYAAgsHAAILCAACCwoAAgsMAAILDQACCw4AAgsQAAILEQACC1AAAgtSAAILVQACC1cAAgtaAAILWwACC1wAAgtdAAILXgACC2AAAgtiAAILYwACC2QAAgtmAAILZwACC3QAAgt1AAILdgACC3gAAgu3AAILuQACC7sAAgu9AAILwAACC8EAAgvCAAILwwACC8QAAgvGAAILyAACC8kAAgvKAAILzAACC80AAgwMAAIMDgACDBAAAgwSAAIMFQACDBYAAgwXAAIMGAACDBkAAgwbAAIMHQACDB4AAgwfAAIMIQACDCIAAgxhAAIMYwACDGUAAgxnAAIMagACDGsAAgxsAAIMbQACDG4AAgxwAAIMcgACDHMAAgx0AAIMdgACDHcAAgy2AAIMuAACDLoAAgy8AAIMvwACDMAAAgzBAAIMwgACDMMAAgzFAAIMxwACDMgAAgzJAAIMywACDMwAAg0LAAINDQACDQ8AAg0RAAINFAACDRUAAg0WAAINFwACDRgAAg0aAAINHAACDR0AAg0eAAINIAACDSEAAg1GAAINagACDZEAAg21AAINuAACDboAAg28AAINvgACDcAAAg3CAAINwwACDcYAAg3TAAIN4gACDeQAAg3mAAIN6AACDeoAAg3sAAIN7gACDfAAAg3/AAIOAgACDgUAAg4IAAIOCwACDg4AAg4RAAIOFAACDhYAAg5VAAIOVwACDlkAAg5bAAIOXgACDl8AAg5gAAIOYQACDmIAAg5kAAIOZgACDmcAAg5oAAIOagACDmsAAg6qAAIOrAACDq4AAg6wAAIOswACDrQAAg61AAIOtgACDrcAAg65AAIOuwACDrwAAg69AAIOvwACDsAAAg7/AAIPAQACDwMAAg8FAAIPCAACDwkAAg8KAAIPCwACDwwAAg8OAAIPEAACDxEAAg8SAAIPFAACDxUAAg9UAAIPVgACD1gAAg9aAAIPXQACD14AAg9fAAIPYAACD2EAAg9jAAIPZQACD2YAAg9nAAIPaQACD2oAAg+pAAIPqwACD60AAg+vAAIPsgACD7MAAg+0AAIPtQACD7YAAg+4AAIPugACD7sAAg+8AAIPvgACD78AAg/+AAIQAAACEAIAAhAEAAIQBwACEAgAAhAJAAIQCgACEAsAAhANAAIQDwACEBAAAhARAAIQEwACEBQAAhBTAAIQVQACEFcAAhBZAAIQXAACEF0AAhBeAAIQXwACEGAAAhBiAAIQZAACEGUAAhBmAAIQaAACEGkAAhC0AAIQ1wACEPcAAhEXAAIRGQACERsAAhEdAAIRHwACESIAAhEjAAIRJAACEScAAhEoAAIRKgACESsAAhEtAAIRMAACETEAAhEyAAIRNQACETYAAhE7AAIRSAACEU0AAhFPAAIRUQACEVYAAhFZAAIRXAACEV4AAhGDAAIRpwACEc4AAhHyAAIR9QACEfcAAhH5AAIR+wACEf0AAhH/AAISAAACEgMAAhIQAAISIQACEiMAAhIlAAISJwACEikAAhIrAAISLQACEi8AAhIxAAISQgACEkUAAhJIAAISSwACEk4AAhJRAAISVAACElcAAhJaAAISXAACEpsAAhKdAAISnwACEqEAAhKkAAISpQACEqYAAhKnAAISqAACEqoAAhKsAAISrQACEq4AAhKwAAISsQACEvAAAhLyAAIS9AACEvYAAhL5AAIS+gACEvsAAhL8AAIS/QACEv8AAhMBAAITAgACEwMAAhMFAAITBgACE0UAAhNHAAITSgACE0wAAhNPAAITUAACE1EAAhNSAAITUwACE1UAAhNXAAITWAACE1kAAhNbAAITXAACE2kAAhNqAAITawACE20AAhOsAAITrgACE7AAAhOyAAITtQACE7YAAhO3AAITuAACE7kAAhO7AAITvQACE74AAhO/AAITwQACE8IAAhQBAAIUAwACFAUAAhQHAAIUCgACFAsAAhQMAAIUDQACFA4AAhQQAAIUEgACFBMAAhQUAAIUFgACFBcAAhRWAAIUWAACFFoAAhRcAAIUXwACFGAAAhRhAAIUYgACFGMAAhRlAAIUZwACFGgAAhRpAAIUawACFGwAAhSrAAIUrQACFK8AAhSxAAIUtAACFLUAAhS2AAIUtwACFLgAAhS6AAIUvAACFL0AAhS+AAIUwAACFMEAAhUAAAIVAgACFQQAAhUGAAIVCQACFQoAAhULAAIVDAACFQ0AAhUPAAIVEQACFRIAAhUTAAIVFQACFRYAAhU7AAIVXwACFYYAAhWqAAIVrQACFa8AAhWxAAIVswACFbUAAhW3AAIVuAACFbsAAhXIAAIV1wACFdkAAhXbAAIV3QACFd8AAhXhAAIV4wACFeUAAhX0AAIV9wACFfoAAhX9AAIWAAACFgMAAhYGAAIWCQACFgsAAhZKAAIWTAACFk4AAhZQAAIWUwACFlQAAhZVAAIWVgACFlcAAhZZAAIWWwACFlwAAhZdAAIWXwACFmAAAhafAAIWoQACFqMAAhalAAIWqAACFqkAAhaqAAIWqwACFqwAAhauAAIWsAACFrEAAhayAAIWtAACFrUAAhb0AAIW9gACFvgAAhb6AAIW/QACFv4AAhb/AAIXAAACFwEAAhcDAAIXBQACFwYAAhcHAAIXCQACFwoAAhdJAAIXSwACF04AAhdQAAIXUwACF1QAAhdVAAIXVgACF1cAAhdZAAIXWwACF1wAAhddAAIXXwACF2AAAhdjAAIXogACF6QAAhemAAIXqAACF6sAAhesAAIXrQACF64AAhevAAIXsQACF7MAAhe0AAIXtQACF7cAAhe4AAIX9wACF/kAAhf7AAIX/QACGAAAAhgBAAIYAgACGAMAAhgEAAIYBgACGAgAAhgJAAIYCgACGAwAAhgNAAIYTAACGE4AAhhQAAIYUgACGFUAAhhWAAIYVwACGFgAAhhZAAIYWwACGF0AAhheAAIYXwACGGEAAhhiAAIYrQACGNAAAhjwAAIZEAACGRIAAhkUAAIZFgACGRgAAhkbAAIZHAACGR0AAhkgAAIZIQACGSMAAhkkAAIZJgACGSkAAhkqAAIZKwACGS4AAhkvAAIZOAACGUUAAhlKAAIZTAACGU4AAhlTAAIZVgACGVkAAhlbAAIZgAACGaQAAhnLAAIZ7wACGfIAAhn0AAIZ9gACGfgAAhn6AAIZ/AACGf0AAhoAAAIaDQACGh4AAhogAAIaIgACGiQAAhomAAIaKAACGioAAhosAAIaLgACGj8AAhpCAAIaRQACGkgAAhpLAAIaTgACGlEAAhpUAAIaVwACGlkAAhqYAAIamgACGpwAAhqeAAIaoQACGqIAAhqjAAIapAACGqUAAhqnAAIaqQACGqoAAhqrAAIarQACGq4AAhrtAAIa7wACGvEAAhrzAAIa9gACGvcAAhr4AAIa+QACGvoAAhr8AAIa/gACGv8AAhsAAAIbAgACGwMAAhtCAAIbRAACG0cAAhtJAAIbTAACG00AAhtOAAIbTwACG1AAAhtSAAIbVAACG1UAAhtWAAIbWAACG1kAAhtmAAIbZwACG2gAAhtqAAIbqQACG6sAAhutAAIbrwACG7IAAhuzAAIbtAACG7UAAhu2AAIbuAACG7oAAhu7AAIbvAACG74AAhu/AAIb/gACHAAAAhwCAAIcBAACHAcAAhwIAAIcCQACHAoAAhwLAAIcDQACHA8AAhwQAAIcEQACHBMAAhwUAAIcUwACHFUAAhxXAAIcWQACHFwAAhxdAAIcXgACHF8AAhxgAAIcYgACHGQAAhxlAAIcZgACHGgAAhxpAAIcqAACHKoAAhysAAIcrgACHLEAAhyyAAIcswACHLQAAhy1AAIctwACHLkAAhy6AAIcuwACHL0AAhy+AAIc/QACHP8AAh0BAAIdAwACHQYAAh0HAAIdCAACHQkAAh0KAAIdDAACHQ4AAh0PAAIdEAACHRIAAh0TAAIdOAACHVwAAh2DAAIdpwACHaoAAh2sAAIdrgACHbAAAh2yAAIdtAACHbUAAh24AAIdxQACHdQAAh3WAAId2AACHdoAAh3cAAId3gACHeAAAh3iAAId8QACHfQAAh33AAId+gACHf0AAh4AAAIeAwACHgYAAh4IAAIeRwACHkkAAh5LAAIeTQACHlAAAh5RAAIeUgACHlMAAh5UAAIeVgACHlgAAh5ZAAIeWgACHlwAAh5dAAIenAACHp4AAh6gAAIeogACHqUAAh6mAAIepwACHqgAAh6pAAIeqwACHq0AAh6uAAIerwACHrEAAh6yAAIe8QACHvMAAh71AAIe9wACHvoAAh77AAIe/AACHv0AAh7+AAIfAAACHwIAAh8DAAIfBAACHwYAAh8HAAIfRgACH0gAAh9KAAIfTAACH08AAh9QAAIfUQACH1IAAh9TAAIfVQACH1cAAh9YAAIfWQACH1sAAh9cAAIfmwACH50AAh+fAAIfoQACH6QAAh+lAAIfpgACH6cAAh+oAAIfqgACH6wAAh+tAAIfrgACH7AAAh+xAAIf8AACH/IAAh/0AAIf9gACH/kAAh/6AAIf+wACH/wAAh/9AAIf/wACIAEAAiACAAIgAwACIAUAAiAGAAIgRQACIEcAAiBJAAIgSwACIE4AAiBPAAIgUAACIFEAAiBSAAIgVAACIFYAAiBXAAIgWAACIFoAAiBbAAIgpgACIMkAAiDpAAIhCQACIQsAAiENAAIhDwACIREAAiEUAAIhFQACIRYAAiEZAAIhGgACIRwAAiEdAAIhHwACISIAAiEjAAIhJAACIScAAiEoAAIhLQACIToAAiE/AAIhQQACIUMAAiFIAAIhSwACIU4AAiFQAAIhdQACIZkAAiHAAAIh5AACIecAAiHpAAIh6wACIe0AAiHvAAIh8QACIfIAAiH1AAIiAgACIhMAAiIVAAIiFwACIhkAAiIbAAIiHQACIh8AAiIhAAIiIwACIjQAAiI3AAIiOgACIj0AAiJAAAIiQwACIkYAAiJJAAIiTAACIk4AAiKNAAIijwACIpEAAiKTAAIilgACIpcAAiKYAAIimQACIpoAAiKcAAIingACIp8AAiKgAAIiogACIqMAAiLiAAIi5AACIuYAAiLoAAIi6wACIuwAAiLtAAIi7gACIu8AAiLxAAIi8wACIvQAAiL1AAIi9wACIvgAAiM3AAIjOQACIzwAAiM+AAIjQQACI0IAAiNDAAIjRAACI0UAAiNHAAIjSQACI0oAAiNLAAIjTQACI04AAiNbAAIjXAACI10AAiNfAAIjngACI6AAAiOiAAIjpAACI6cAAiOoAAIjqQACI6oAAiOrAAIjrQACI68AAiOwAAIjsQACI7MAAiO0AAIj8wACI/UAAiP3AAIj+QACI/wAAiP9AAIj/gACI/8AAiQAAAIkAgACJAQAAiQFAAIkBgACJAgAAiQJAAIkSAACJEoAAiRMAAIkTgACJFEAAiRSAAIkUwACJFQAAiRVAAIkVwACJFkAAiRaAAIkWwACJF0AAiReAAIknQACJJ8AAiShAAIkowACJKYAAiSnAAIkqAACJKkAAiSqAAIkrAACJK4AAiSvAAIksAACJLIAAiSzAAIk8gACJPQAAiT2AAIk+AACJPsAAiT8AAIk/QACJP4AAiT/AAIlAQACJQMAAiUEAAIlBQACJQcAAiUIAAIlLQACJVEAAiV4AAIlnAACJZ8AAiWhAAIlowACJaUAAiWnAAIlqQACJaoAAiWtAAIlugACJckAAiXLAAIlzQACJc8AAiXRAAIl0wACJdUAAiXXAAIl5gACJekAAiXsAAIl7wACJfIAAiX1AAIl+AACJfsAAiX9AAImPAACJj4AAiZAAAImQgACJkUAAiZGAAImRwACJkgAAiZJAAImSwACJk0AAiZOAAImTwACJlEAAiZSAAImkQACJpMAAiaVAAImlwACJpoAAiabAAImnAACJp0AAiaeAAImoAACJqIAAiajAAImpAACJqYAAianAAIm5gACJugAAibqAAIm7AACJu8AAibwAAIm8QACJvIAAibzAAIm9QACJvcAAib4AAIm+QACJvsAAib8AAInOwACJz0AAic/AAInQQACJ0QAAidFAAInRgACJ0cAAidIAAInSgACJ0wAAidNAAInTgACJ1AAAidRAAInkAACJ5IAAieUAAInlgACJ5kAAieaAAInmwACJ5wAAiedAAInnwACJ6EAAieiAAInowACJ6UAAiemAAIn5QACJ+cAAifpAAIn6wACJ+4AAifvAAIn8AACJ/EAAifyAAIn9AACJ/YAAif3AAIn+AACJ/oAAif7AAIoOgACKDwAAig+AAIoQAACKEMAAihEAAIoRQACKEYAAihHAAIoSQACKEsAAihMAAIoTQACKE8AAihQAAIoWQACKFoAAihcAAIonwACKMMAAijnAAIpCgACKTEAAilRAAIpeAACKZ8AAim/AAIp4wACKgcAAioJAAIqDAACKg4AAioQAAIqEgACKhUAAioYAAIqGgACKhwAAiofAAIqIQACKiMAAiomAAIqKQACKioAAiovAAIqPAACKj8AAipBAAIqRAACKkcAAipJAAIqbgACKpIAAiq5AAIq3QACKuAAAiriAAIq5AACKuYAAiroAAIq6gACKusAAiruAAIq+wACKw4AAisQAAIrEgACKxQAAisWAAIrGAACKxoAAiscAAIrHgACKyAAAiszAAIrNgACKzkAAis8AAIrPwACK0IAAitFAAIrSAACK0sAAitOAAIrUAACK48AAiuRAAIrlAACK5YAAiuZAAIrmgACK5sAAiucAAIrnQACK58AAiuhAAIrogACK6MAAiulAAIrpgACK68AAiuwAAIrsgACK/EAAivzAAIr9QACK/cAAiv6AAIr+wACK/wAAiv9AAIr/gACLAAAAiwCAAIsAwACLAQAAiwGAAIsBwACLEYAAixIAAIsSwACLE0AAixQAAIsUQACLFIAAixTAAIsVAACLFYAAixYAAIsWQACLFoAAixcAAIsXQACLGYAAixnAAIsaQACLKgAAiyqAAIsrAACLK4AAiyxAAIssgACLLMAAiy0AAIstQACLLcAAiy5AAIsugACLLsAAiy9AAIsvgACLP0AAiz/AAItAgACLQQAAi0HAAItCAACLQkAAi0KAAItCwACLQ0AAi0PAAItEAACLREAAi0TAAItFAACLR0AAi0eAAItIAACLV8AAi1hAAItYwACLWUAAi1oAAItaQACLWoAAi1rAAItbAACLW4AAi1wAAItcQACLXIAAi10AAItdQACLbQAAi22AAItuQACLbsAAi2+AAItvwACLcAAAi3BAAItwgACLcQAAi3GAAItxwACLcgAAi3KAAItywACLdgAAi3ZAAIt2gACLdwAAi4bAAIuHQACLh8AAi4hAAIuJAACLiUAAi4mAAIuJwACLigAAi4qAAIuLAACLi0AAi4uAAIuMAACLjEAAi5wAAIucgACLnUAAi53AAIuegACLnsAAi58AAIufQACLn4AAi6AAAIuggACLoMAAi6EAAIuhgACLocAAi6jAAIusAACLrsAAi6+AAIuwQACLsQAAi7HAAIuygACLtUAAi7YAAIu2wACLt4AAi7hAAIu5AACLuYAAi75AAIvBwACLxAAAi8iAAIvKgACL3UAAi+YAAIvuAACL9gAAi/aAAIv3AACL94AAi/gAAIv4wACL+QAAi/lAAIv6AACL+kAAi/rAAIv7AACL+4AAi/xAAIv8gACL/MAAi/2AAIv9wACL/wAAjAJAAIwDgACMBAAAjASAAIwFwACMBoAAjAdAAIwHwACMEQAAjBoAAIwjwACMLMAAjC2AAIwuAACMLoAAjC8AAIwvgACMMAAAjDBAAIwxAACMNEAAjDiAAIw5AACMOYAAjDoAAIw6gACMOwAAjDuAAIw8AACMPIAAjEDAAIxBgACMQkAAjEMAAIxDwACMRIAAjEVAAIxGAACMRsAAjEdAAIxXAACMV4AAjFgAAIxYgACMWUAAjFmAAIxZwACMWgAAjFpAAIxawACMW0AAjFuAAIxbwACMXEAAjFyAAIxsQACMbMAAjG1AAIxtwACMboAAjG7AAIxvAACMb0AAjG+AAIxwAACMcIAAjHDAAIxxAACMcYAAjHHAAIyBgACMggAAjILAAIyDQACMhAAAjIRAAIyEgACMhMAAjIUAAIyFgACMhgAAjIZAAIyGgACMhwAAjIdAAIyKgACMisAAjIsAAIyLgACMm0AAjJvAAIycQACMnMAAjJ2AAIydwACMngAAjJ5AAIyegACMnwAAjJ+AAIyfwACMoAAAjKCAAIygwACMsIAAjLEAAIyxgACMsgAAjLLAAIyzAACMs0AAjLOAAIyzwACMtEAAjLTAAIy1AACMtUAAjLXAAIy2AACMxcAAjMZAAIzGwACMx0AAjMgAAIzIQACMyIAAjMjAAIzJAACMyYAAjMoAAIzKQACMyoAAjMsAAIzLQACM2wAAjNuAAIzcAACM3IAAjN1AAIzdgACM3cAAjN4AAIzeQACM3sAAjN9AAIzfgACM38AAjOBAAIzggACM8EAAjPDAAIzxQACM8cAAjPKAAIzywACM8wAAjPNAAIzzgACM9AAAjPSAAIz0wACM9QAAjPWAAIz1wACM/wAAjQgAAI0RwACNGsAAjRuAAI0cAACNHIAAjR0AAI0dgACNHgAAjR5AAI0fAACNIkAAjSYAAI0mgACNJwAAjSeAAI0oAACNKIAAjSkAAI0pgACNLUAAjS4AAI0uwACNL4AAjTBAAI0xAACNMcAAjTKAAI0zAACNQsAAjUNAAI1DwACNREAAjUUAAI1FQACNRYAAjUXAAI1GAACNRoAAjUcAAI1HQACNR4AAjUgAAI1IQACNWAAAjViAAI1ZAACNWYAAjVpAAI1agACNWsAAjVsAAI1bQACNW8AAjVxAAI1cgA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CWFQAAlhWAAJYWAACWFsAAlhcAAJYXQACWF4AAlhfAAJYYQACWGMAAlhkAAJYZQACWGcAAlhoAAJYjQACWLEAAljYAAJY/AACWP8AAlkBAAJZAwACWQUAAlkHAAJZCQACWQoAAlkNAAJZGgACWSkAAlkrAAJZLQACWS8AAlkxAAJZMwACWTUAAlk3AAJZRgACWUkAAllMAAJZTwACWVIAAllVAAJZWAACWVsAAlldAAJZnAACWZ4AAlmgAAJZogACWaUAAlmmAAJZpwACWagAAlmpAAJZqwACWa0AAlmuAAJZrwACWbEAAlmyAAJZ8QACWfMAAln1AAJZ9wACWfoAAln7AAJZ/AACWf0AAln+AAJaAAACWgIAAloDAAJaBAACWgYAAloHAAJaRgACWkgAAlpKAAJaTAACWk8AAlpQAAJaUQACWlIAAlpTAAJaVQACWlcAAlpYAAJaWQACWlsAAlpcAAJamwACWp0AAlqfAAJaoQACWqQAAlqlAAJapgACWqcAAlqoAAJaqgACWqwAAlqtAAJargACWrAAAlqxAAJa8AACWvIAAlr0AAJa9gACWvkAAlr6AAJa+wACWvwAAlr9AAJa/wACWwEAAlsCAAJbAwACWwUAAlsGAAJbRQACW0cAAltJAAJbSwACW04AAltPAAJbUAACW1EAAltSAAJbVAACW1YAAltXAAJbWAACW1oAAltbAAJbmgACW5wAAlueAAJboAACW6MAAlukAAJbpQACW6YAAlunAAJbqQACW6sAAlusAAJbrQACW68AAluwAAJb+wACXB4AAlw+AAJcXgACXGAAAlxiAAJcZAACXGYAAlxpAAJcagACXGsAAlxuAAJcbwACXHEAAlxyAAJcdAACXHYAAlx3AAJceAACXHsAAlx8AAJcgQACXI4AAlyTAAJclQACXJcAAlycAAJcnwACXKIAAlykAAJcyQACXO0AAl0UAAJdOAACXTsAAl09AAJdPwACXUEAAl1DAAJdRQACXUYAAl1JAAJdVgACXWcAAl1pAAJdawACXW0AAl1vAAJdcQACXXMAAl11AAJddwACXYgAAl2LAAJdjgACXZEAAl2UAAJdlwACXZoAAl2dAAJdoAACXaIAAl3hAAJd4wACXeUAAl3nAAJd6gACXesAAl3sAAJd7QACXe4AAl3wAAJd8gACXfMAAl30AAJd9gACXfcAAl42AAJeOAACXjoAAl48AAJePwACXkAAAl5BAAJeQgACXkMAAl5FAAJeRwACXkgAAl5JAAJeSwACXkwAAl6LAAJejQACXpAAAl6SAAJelQACXpYAAl6XAAJemAACXpkAAl6bAAJenQACXp4AAl6fAAJeoQACXqIAAl6vAAJesAACXrEAAl6zAAJe8gACXvQAAl72AAJe+AACXvsAAl78AAJe/QACXv4AAl7/AAJfAQACXwMAAl8EAAJfBQACXwcAAl8IAAJfRwACX0kAAl9LAAJfTQACX1AAAl9RAAJfUgACX1MAAl9UAAJfVgACX1gAAl9ZAAJfWgACX1wAAl9dAAJfnAACX54AAl+gAAJfogACX6UAAl+mAAJfpwACX6gAAl+pAAJfqwACX60AAl+uAAJfrwACX7EAAl+yAAJf8QACX/MAAl/1AAJf9wACX/oAAl/7AAJf/AACX/0AAl/+AAJgAAACYAIAAmADAAJgBAACYAYAAmAHAAJgRgACYEgAAmBKAAJgTAACYE8AAmBQAAJgUQACYFIAAmBTAAJgVQACYFcAAmBYAAJgWQACYFsAAmBcAAJggQACYKUAAmDMAAJg8AACYPMAAmD1AAJg9wACYPkAAmD7AAJg/QACYP4AAmEBAAJhDgACYR0AAmEfAAJhIQACYSMAAmElAAJhJwACYSkAAmErAAJhOgACYT0AAmFAAAJhQwACYUYAAmFJAAJhTAACYU8AAmFRAAJhkAACYZIAAmGUAAJhlgACYZkAAmGaAAJhmwACYZwAAmGdAAJhnwACYaEAAmGiAAJhowACYaUAAmGmAAJh5QACYecAAmHpAAJh6wACYe4AAmHvAAJh8AACYfEAAmHyAAJh9AACYfYAAmH3AAJh+AACYfoAAmH7AAJiOgACYjwAAmI+AAJiQAACYkMAAmJEAAJiRQACYkYAAmJHAAJiSQACYksAAmJMAAJiTQACYk8AAmJQAAJijwACYpEAAmKTAAJilQACYpgAAmKZAAJimgACYpsAAmKcAAJingACYqAAAmKhAAJiogACYqQAAmKlAAJi5AACYuYAAmLoAAJi6gACYu0AAmLuAAJi7wACYvAAAmLxAAJi8wACYvUAAmL2AAJi9wACYvkAAmL6AAJjOQACYzsAAmM9AAJjPwACY0IAAmNDAAJjRAACY0UAAmNGAAJjSAA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Cbm8AAm6TAAJuugACbt4AAm7hAAJu4wACbuUAAm7nAAJu6QACbusAAm7sAAJu7wACbvwAAm8NAAJvDwACbxEAAm8TAAJvFQACbxcAAm8ZAAJvGwACbx0AAm8uAAJvMQACbzQAAm83AAJvOgACbz0AAm9AAAJvQwACb0YAAm9IAAJvhwACb4kAAm+LAAJvjQACb5AAAm+RAAJvkgACb5MAAm+UAAJvlgACb5gAAm+ZAAJvmgACb5wAAm+dAAJv3AACb94AAm/gAAJv4gACb+UAAm/mAAJv5wACb+gAAm/pAAJv6wACb+0AAm/uAAJv7wACb/EAAm/yAAJwMQACcDMAAnA2AAJwOAACcDsAAnA8AAJwPQACcD4AAnA/AAJwQQACcEMAAnBEAAJwRQACcEcAAnBIAAJwVQACcFYAAnBXAAJwWQACcJgAAnCaAAJwnAACcJ4AAnChAAJwogACcKMAAnCkAAJwpQACcKcAAnCpAAJwqgACcKsAAnCtAAJwrgACcO0AAnDvAAJw8QACcPMAAnD2AAJw9wACcPgAAnD5AAJw+gACcPwAAnD+AAJw/wACcQAAAnECAAJxAwACcUIAAnFEAAJxRgACcUgAAnFLAAJxTAACcU0AAnFOAAJxTwACcVEAAnFTAAJxVAACcVUAAnFXAAJxWAACcZcAAnGZAAJxmwACcZ0AAnGgAAJxoQACcaIAAnGjAAJxpAACcaYAAnGoAAJxqQACcaoAAnGsAAJxrQACcewAAnHuAAJx8AACcfIAAnH1AAJx9gACcfcAAnH4AAJx+QACcfsAAnH9AAJx/gACcf8AAnIBAAJyAgACcicAAnJLAAJycgACcpYAAnKZAAJymwACcp0AAnKfAAJyoQACcqMAAnKkAAJypwACcrQAAnLDAAJyxQACcscAAnLJAAJyywACcs0AAnLPAAJy0QACcuAAAnLjAAJy5gACcukAAnLsAAJy7wACcvIAAnL1AAJy9wACczYAAnM4AAJzOgACczwAAnM/AAJzQAACc0EAAnNCAAJzQwACc0UAAnNHAAJzSAACc0kAAnNLAAJzTAACc4sAAnONAAJzjwACc5EAAnOUAAJzlQACc5YAAnOXAAJzmAACc5oAAnOcAAJznQACc54AAnOgAAJzoQACc+AAAnPiAAJz5AACc+YAAnPpAAJz6gACc+sAAnPsAAJz7QA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ChaMAAoWmAAKFqAAChecAAoXpAAKF6wAChe0AAoXwAAKF8QAChfIAAoXzAAKF9AAChfYAAoX4AAKF+QAChfoAAoX8AAKF/QAChjwAAoY+AAKGQAAChkIAAoZFAAKGRgAChkcAAoZIAAKGSQAChksAAoZNAAKGTgAChk8AAoZRAAKGUgAChpEAAoaTAAKGlgAChpgAAoabAAKGnAAChp0AAoaeAAKGnwAChqEAAoajAAKGpAAChqUAAoanAAKGqAAChrUAAoa2AAKGtwAChrkAAob4AAKG+gAChvwAAob+AAKHAQAChwIAAocDAAKHBAAChwUAAocHAAKHCQAChwoAAocLAAKHDQAChw4AAodNAAKHTwACh1EAAodTAAKHVgACh1cAAodYAAKHWQACh1oAAodcAAKHXgACh18AAodgAAKHYgACh2MAAoeiAAKHpAACh6YAAoeoAAKHqwACh6wAAoetAAKHrgACh68AAoexAAKHswACh7QAAoe1AAKHtwACh7gAAof3AAKH+QACh/sAAof9AAKIAAACiAEAAogCAAKIAwACiAQAAogGAAKICAACiAkAAogKAAKIDAACiA0AAohMAAKITgACiFAAAohSAAKIVQACiFYAAohXAAKIWAACiFkAAohbAAKIXQACiF4AAohfAAKIYQACiGIAAoiHAAKIqwACiNIAAoj2AAKI+QACiPsAAoj9AAKI/wACiQEAAokDAAKJBAACiQcAAokUAAKJIwACiSUAAoknAAKJKQACiSsAAoktAAKJLwACiTEAAolAAAKJQwACiUYAAolJAAKJTAACiU8AAolSAAKJVQACiVcAAomWAAKJmAACiZoAAomcAAKJnwACiaAAAomhAAKJogACiaMAAomlAAKJpwACiagAAompAAKJqwACiawAAonrAAKJ7QACie8AAonxAAKJ9AACifUAAon2AAKJ9wACifgAAon6AAKJ/AACif0AAon+AAKKAAACigEAAopAAAKKQgACikQAAopGAAKKSQACikoAAopLAAKKTAACik0AAopPAAKKUQACilIAAopTAAKKVQACilYAAoqVAAKKlwACipkAAoqbAAKKngACip8AAoqgAAKKoQACiqIAAoqkAAKKpgACiqcAAoqoAAKKqgACiqsAAorqAAKK7AACiu4AAorwAAKK8wACivQAAor1AAKK9gACivcAAor5AAKK+wACivwAAor9AAKK/wACiwAAAos/AAKLQQACi0MAAotFAAKLSAACi0kAAotKAAKLSwACi0wAAotOAAKLUAACi1EAAotSAAKLVAACi1UAAouUAAKLlgACi5gAAouaAAKLnQACi54AAoufAAKLoAACi6EAAoujAAKLpQACi6YAAounAAKLqQACi6oAAouzAAKLtAACi7YAAov5AAKMHQACjEEAAoxkAAKMiwACjKsAAozSAAKM+QACjRkAAo09AAKNYQACjWMAAo1mAAKNaAACjWoAAo1sAAKNbwACjXIAAo10AAKNdgACjXkAAo17AAKNfQACjYAAAo2DAAKNhAACjYkAAo2WAAKNmQACjZsAAo2eAAKNoQACjaMAAo3IAAKN7AACjhMAAo43AAKOOgACjjwAAo4+AAKOQAACjkIAAo5EAAKORQACjkgAAo5VAAKOaAACjmoAAo5sAAKObgACjnAAAo5yAAKOdAACjnYAAo54AAKOegACjo0AAo6QAAKOkwACjpYAAo6ZAAKOnAACjp8AAo6iAAKOpQACjqgAAo6qAAKO6QACjusAAo7uAAKO8AACjvMAAo70AAKO9QACjvYAAo73AAKO+QACjvsAAo78AAKO/QACjv8AAo8AAAKPCQACjwoAAo8MAAKPSwACj00AAo9PAAKPUQACj1QAAo9VAAKPVgACj1cAAo9YAAKPWgACj1wAAo9dAAKPXgACj2AAAo9hAAKPoAACj6IAAo+lAAKPpwACj6oAAo+rAAKPrAACj60AAo+uAAKPsAACj7IAAo+zAAKPtAACj7YAAo+3AAKPwAACj8EAAo/DAAKQAgACkAQAApAGAAKQCAACkAsAApAMAAKQDQACkA4AApAPAAKQEQACkBMAApAUAAKQFQACkBcAApAYAAKQVwACkFkAApBcAAKQXgACkGEAApBiAAKQYwACkGQAApBlAAKQZwACkGkAApBqAAKQawACkG0AApBuAAKQdwACkHgAApB6AAKQuQACkLsAApC9AAKQvwACkMIAApDDAAKQxAACkMUAApDGAAKQyAACkMoAApDLAAKQzAACkM4AApDPAAKRDgACkRAAApETAAKRFQACkRgAApEZAAKRGgACkRsAApEcAAKRHgACkSAAApEhAAKRIgACkSQAApElAAKRMgACkTMAApE0AAKRNgACkXUAApF3AAKReQACkXsAApF+AAKRfwACkYAAApGBAAKRggACkYQAApGGAAKRhwACkYgAApGKAAKRiwACkcoAApHMAAKRzwACkdEAApHUAAKR1QACkdYAApHXAAKR2AACkdoAApHcAAKR3QACkd4AApHgAAKR4QACke0AApH6AAKR/wACkgIAApIFAAKSCgACkg0AApIQAAKSEgACkhgAApIjAAKSbgACkpEAApKxAAKS0QACktMAApLVAAKS1wACktkAApLcAAKS3QACkt4AApLhAAKS4gACkuQAApLlAAKS5wACkuoAApLrAAKS7AACku8AApLwAAKS9QACkwIAApMHAAKTCQACkwsAApMQAAKTEwACkxYAApMYAAKTPQACk2EAApOIAAKTrAACk68AApOxAAKTswACk7UAApO3AAKTuQACk7oAApO9AAKTygACk9sAApPdAAKT3wACk+EAApPjAAKT5QACk+cAApPpAAKT6wACk/wAApP/AAKUAgAClAUAApQIAAKUCwAClA4AApQRAAKUFAAClBYAApRVAAKUVwAClFkAApRbAAKUXgAClF8AApRgAAKUYQAClGIAApRkAAKUZgAClGcAApRoAAKUagAClGsAApSqAAKUrAAClK4AApSwAAKUswAClLQAApS1AAKUtgAClLcAApS5AAKUuwAClLwAApS9AAKUvwAClMAAApT/AAKVAQAClQQAApUGAAKVCQAClQoAApULAAKVDAAClQ0AApUPAAKVEQAClRIAApUTAAKVFQAClRYAApUjAAKVJAAClSUAApUnAAKVZgAClWgAApVqAAKVbAAClW8AApVwAAKVcQAClXIAApVzAAKVdQAClXcAApV4AAKVeQAClXsAApV8AAKVuwAClb0AApW/AAKVwQAClcQAApXFAAKVxgAClccAApXIAAKVygAClcwAApXNAAKVzgACldAAApXRAAKWEAAClhIAApYUAAKWFgAClhkAApYaAAKWGwAClhwAApYdAAKWHwACliEAApYiAAKWIwACliUAApYmAAKWZQAClmcAApZpAAKWawAClm4AApZvAAKWcAAClnEAApZyAAKWdAAClnYAApZ3AAKWeAAClnoAApZ7AAKWugAClrwAApa+AAKWwAAClsMAApbEAAKWxQAClsYAApbHAAKWyQAClssAApbMAAKWzQACls8AApbQAAKW9QAClxkAApdAAAKXZAACl2cAApdpAAKXawACl20AApdvAAKXcQACl3IAApd1AAKXggACl5EAApeTAAKXlQACl5cAApeZAAKXmwACl50AApefAAKXrgACl7EAApe0AAKXtwACl7oAApe9AAKXwAACl8MAApfFAAKYBAACmAYAApgIAAKYCgACmA0AApgOAAKYDwACmBAAApgRAAKYEwACmBUAApgWAAKYFwACmBkAApgaAAKYWQACmFsAAphdAAKYXwACmGIAAphjAAKYZAACmGUAAphmAAKYaAACmGoAAphrAAKYbAACmG4AAphvAAKYrgACmLAAApiyAAKYtAACmLcAApi4AAKYuQACmLoAApi7AAKYvQACmL8AApjAAAKYwQACmMMAApjEAAKZAwACmQUAApkHAAKZCQACmQwAApkNAAKZDgACmQ8AApkQAAKZEgACmRQAApkVAAKZFgACmRgAApkZAAKZWAACmVoAAplcAAKZXgACmWEAApliAAKZYwACmWQAApllAAKZZwACmWkAAplqAAKZawACmW0AApluAAKZrQACma8AApmxAAKZswACmbYAApm3AAKZuAACmbkAApm6AAKZvAACmb4AApm/AAKZwAACmcIAApnDAAKaAgACmgQAApoGAAKaCAACmgsAApoMAAKaDQACmg4AApoPAAKaEQACmhMAApoUAAKaFQACmhcAApoYAAKaYwACmoYAApqmAAKaxgACmsgAAprKAAKazAACms4AAprRAAKa0gACmtMAAprWAAKa1wACmtkAApraAAKa3AACmt8AAprgAAKa4QACmuQAAprlAAKa6gACmvcAApr8AAKa/gACmwAAApsFAAKbCAACmwsAApsNAAKbMgACm1YAApt9AAKboQACm6QAApumAAKbqAACm6oAApusAAKbrgACm68AApuyAAKbvwACm9AAApvSAAKb1AACm9YAApvYAAKb2gACm9wAApveAAKb4AACm/EAApv0AAKb9wACm/oAApv9AAKcAAACnAMAApwGAAKcCQACnAsAApxKAAKcTAACnE4AApxQAAKcUwACnFQAApxVAAKcVgACnFcAApxZAAKcWwACnFwAApxdAAKcXwACnGAAApyfAAKcoQACnKMAApylAAKcqAACnKkAApyqAAKcqwACnKwAApyuAAKcsAACnLEAApyyAAKctAACnLUAApz0AAKc9gACnPkAApz7AAKc/gACnP8AAp0AAAKdAQACnQIAAp0EAAKdBgACnQcAAp0IAAKdCgACnQsAAp0YAAKdGQACnRoAAp0cAAKdWwACnV0AAp1fAAKdYQACnWQAAp1lAAKdZgACnWcAAp1oAAKdagACnWwAAp1tAAKdbgACnXAAAp1xAAKdsAACnbIAAp20AAKdtgACnbkAAp26AAKduwACnbwAAp29AAKdvwACncEAAp3CAAKdwwACncUAAp3GAAKeBQACngcAAp4JAAKeCwACng4AAp4PAAKeEAACnhEAAp4SAAKeFAACnhYAAp4XAAKeGAACnhoAAp4bAAKeWgACnlwAAp5eAAKeYAACnmMAAp5kAAKeZQACnmYAAp5nAAKeaQACnmsAAp5sAAKebQACnm8AAp5wAAKerwACnrEAAp6zAAKetQACnrgAAp65AAKeugACnrsAAp68AAKevgACnsAAAp7BAAKewgACnsQAAp7FAAKe6gACnw4AAp81AAKfWQACn1wAAp9eAAKfYAACn2IAAp9kAAKfZgACn2cAAp9qAAKfdwACn4YAAp+IAAKfigACn4wAAp+OAAKfkAACn5IAAp+UAAKfowACn6YAAp+pAAKfrAACn68AAp+yAAKftQACn7gAAp+6AAKf+QACn/sAAp/9AAKf/wACoAIAAqADAAKgBAACoAUAAqAGAAKgCAACoAoAAqALAAKgDAACoA4AAqAPAAKgTgACoFAAAqBSAAKgVAACoFcAAqBYAAKgWQACoFoAAqBbAAKgXQACoF8AAqBgAAKgYQACoGMAAqBkAAKgowACoKUAAqCnAAKgqQACoKwAAqCtAAKgrgACoK8AAqCwAAKgsgACoLQAAqC1AAKgtgACoLgAAqC5AAKg+AACoPoAAqD8AAKg/gACoQEAAqECAAKhAwACoQQAAqEFAAKhBwACoQkAAqEKAAKhCwACoQ0AAqEOAAKhTQACoU8AAqFRAAKhUwACoVYAAqFXAAKhWAACoVkAAqFaAAKhXAACoV4AAqFfAAKhYAACoWIAAqFjAAKhogACoaQAAqGmAAKhqAACoasAAqGsAAKhrQACoa4AAqGvAAKhsQACobMAAqG0AAKhtQACobcAAqG4AAKh9wACofkAAqH7AAKh/QACogAAAqIBAAKiAgA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CrjcAAq5GAAKuSQACrkwAAq5PAAKuUgACrlUAAq5YAAKuWwACrl0AAq6cAAKungACrqAAAq6iAAKupQACrqYAAq6nAAKuqAACrqkAAq6rAAKurQACrq4AAq6vAAKusQACrrIAAq7xAAKu8wACrvUAAq73AAKu+gACrvsAAq78AAKu/QACrv4AAq8AAAKvAgACrwMAAq8EAAKvBgACrwcAAq9GAAKvSAACr0oAAq9MAAKvTwACr1AAAq9RAAKvUgACr1MAAq9VAAKvVwACr1gAAq9ZAAKvWwACr1wAAq+bAAKvnQACr6AAAq+iAAKvpQACr6YAAq+nAAKvqAACr6kAAq+rAAKvrQACr64AAq+vAAKvsQACr7IAAq/xAAKv8wACr/UAAq/3AAKv+gACr/sAAq/8AAKv/QACr/4AArAAAAKwAgACsAMAArAEAAKwBgACsAcAArBGAAKwSAACsEoAArBMAAKwTwACsFAAArBRAAKwUgACsFMAArBVAAKwVwACsFgAArBZAAKwWwACsFwAArCbAAKwnQACsJ8AArChAAKwpAACsKUAArCmAAKwpwACsKgAArCqAAKwrAACsK0AArCuAAKwsAACsLEAArD8AAKxHwACsT8AArFfAAKxYQACsWMAArFlAAKxZwACsWoAArFrAAKxbAACsW8AArFwAAKxcgACsXMAArF1AAKxeAACsXkAArF6AAKxfQACsX4AArGHAAKxlAACsZkAArGbAAKxnQACsaIAArGlAAKxqAACsaoAArHPAAKx8wACshoAArI+AAKyQQACskMAArJFAAKyRwACskkAArJLAAKyTAACsk8AArJcAAKybQACsm8AArJxAAKycwACsnUAArJ3AAKyeQACsnsAArJ9AAKyjgACspEAArKUAAKylwACspoAArKdAAKyoAACsqMAArKmAAKyqAACsucAArLpAAKy6wACsu0AArLwAAKy8QACsvIAArLzAAKy9AACsvYAArL4AAKy+QACsvoAArL8AAKy/QACszwAArM+AAKzQAACs0IAArNFAAKzRgACs0cAArNIAAKzSQACs0sAArNNAAKzTgACs08AArNRAAKzUgACs5EAArOTAAKzlgACs5gAArObAAKznAACs50AArOeAAKznwACs6EAArOjAAKzpAACs6UAArOnAAKzqAACs7UAArO2AAKztwACs7kAArP4AAKz+gACs/wAArP+AAK0AQACtAIAArQDAAK0BAACtAUAArQHAAK0CQACtAoAArQLAAK0DQACtA4AArRNAAK0TwACtFEAArRTAAK0VgACtFcAArRYAAK0WQACtFoAArRcAAK0XgACtF8AArRgAAK0YgACtGMAArSiAAK0pAACtKYAArSoAAK0qwACtKwAArStAAK0rgACtK8AArSxAAK0swACtLQAArS1AAK0twACtLgAArT3AAK0+QACtPsAArT9AAK1AAACtQEAArUCAAK1AwACtQQAArUGAAK1CAACtQkAArUKAAK1DAACtQ0AArVMAAK1TgACtVAAArVSAAK1VQACtVYAArVXAAK1WAACtVkAArVbAAK1XQACtV4AArVfAAK1YQACtWIAArWHAAK1qwACtdIAArX2AAK1+QACtfsAArX9AAK1/wACtgEAArYDAAK2BAACtgcAArYUAAK2IwACtiUAArYnAAK2KQACtisAArYtAAK2LwACtjEAArZAAAK2QwACtkYAArZJAAK2TAACtk8AArZSAAK2VQACtlcAAraWAAK2mAACtpoAAracAAK2nwACtqAAArahAAK2ogACtqMAAralAAK2pwACtqgAArapAAK2qwACtqwAArbrAAK27QACtu8AArbxAAK29AACtvUAArb2AAK29wACtvgAArb6AAK2/AACtv0AArb+AAK3AAACtwEAArdAAAK3QgACt0QAArdGAAK3SQACt0oAArdLAAK3TAACt00AArdPAAK3UQACt1IAArdTAAK3VQACt1YAAreVAAK3lwACt5oAArecAAK3nwACt6AAArehAAK3ogACt6MAArelAAK3pwACt6gAArepAAK3qwACt6wAArfrAAK37QACt+8AArfxAAK39AACt/UAArf2AAK39wACt/gAArf6AAK3/AACt/0AArf+AAK4AAACuAEAArhAAAK4QgACuEQAArhGAAK4SQACuEoAArhLAAK4TAACuE0AArhPAAK4UQACuFIAArhTAAK4VQACuFYAAriVAAK4lwACuJkAAribAAK4ngACuJ8AArigAAK4oQACuKIAArikAAK4pgACuKcAArioAAK4qgACuKsAArj2AAK5GQACuTkAArlZAAK5WwACuV0AArlfAAK5YQACuWQAArllAAK5ZgACuWkAArlqAAK5bAACuW0AArlvAAK5cgACuXMAArl0AAK5dwACuXgAArl9AAK5igACuY8AArmRAAK5kwACuZgAArmbAAK5ngACuaAAArnFAAK56QACuhAAAro0AAK6NwACujkAAro7AAK6PQACuj8AArpBAAK6QgACukUAArpSAAK6YwACumUAArpnAAK6aQACumsAArptAAK6bwACunEAArpzAAK6hAACuocAArqKAAK6jQACupAAArqTAAK6lgACupkAArqcAAK6ngACut0AArrfAAK64QACuuMAArrmAAK65wACuugAArrpAAK66gACuuwAArruAAK67wACuvAAArryAAK68wACuzIAArs0AAK7NgACuzgAArs7AAK7PAACuz0AArs+AAK7PwACu0EAArtDAAK7RAACu0UAArtHAAK7SAACu4cAAruJAAK7jAACu44AAruRAAK7kgACu5MAAruUAAK7lQACu5cAAruZAAK7mgACu5sAArudAAK7ngACu6sAArusAAK7rQACu68AArvuAAK78AACu/IAArv0AAK79wACu/gAArv5AAK7+gACu/sAArv9AAK7/wACvAAAArwBAAK8AwACvAQAArxDAAK8RQACvEcAArxJAAK8TAACvE0AArxOAAK8TwACvFAAArxSAAK8VAACvFUAArxWAAK8WAACvFkAAryYAAK8mgACvJwAAryeAAK8oQACvKIAAryjAAK8pAACvKUAArynAAK8qQACvKoAAryrAAK8rQACvK4AArztAAK87wACvPEAArzzAAK89gACvPcAArz4AAK8+QACvPoAArz8AAK8/gACvP8AAr0AAAK9AgACvQMAAr1CAAK9RAACvUYAAr1IAAK9SwACvUwAAr1NAAK9TgACvU8AAr1RAAK9UwACvVQAAr1VAAK9VwACvVgAAr19AAK9oQACvcgAAr3sAAK97wACvfEAAr3zAAK99QACvfcAAr35AAK9+gACvf0AAr4KAAK+GQACvhsAAr4dAAK+HwACviEAAr4jAAK+JQACvicAAr42AAK+OQACvjwAAr4/AAK+QgACvkUAAr5IAAK+SwACvk0AAr6MAAK+jgACvpAAAr6SAAK+lQACvpYAAr6XAAK+mAACvpkAAr6bAAK+nQACvp4AAr6fAAK+oQACvqIAAr7hAAK+4wACvuUAAr7nAAK+6gACvusAAr7sAAK+7QACvu4AAr7wAAK+8gACvvMAAr70AAK+9gACvvcAAr82AAK/OAACvzoAAr88AAK/PwACv0AAAr9BAAK/QgACv0MAAr9FAAK/RwACv0gAAr9JAAK/SwACv0wAAr+LAAK/jQACv48AAr+RAAK/lAACv5UAAr+WAAK/lwACv5gAAr+aAAK/nAACv50AAr+eAAK/oAACv6EAAr/gAAK/4gACv+QAAr/mAAK/6QACv+oAAr/rAAK/7AACv+0AAr/vAAK/8QACv/IAAr/zAAK/9QACv/YAAsA1AALANwACwDkAAsA7AALAPgACwD8AAsBAAALAQQACwEIAAsBEAALARgACwEcAAsBIAALASgACwEsAAsCKAALAjAACwI4AAsCQAALAkwACwJQAAsCVAALAlgACwJcAAsCZAALAmwACwJwAAsCdAALAnwACwKAAAsDrAALBDgACwS4AAsFOAALBUAACwVIAAsFUAALBVgACwVkAAsFaAALBWwACwV4AAsFfAALBYQACwWIAAsFkAALBZwACwWgAAsFpAALBbAACwW0AAsFyAALBfwACwYQAAsGGAALBiAACwY0AAsGQAALBkwACwZUAAsG6AALB3gACwgUAAsIpAALCLAACwi4AAsIwAALCMgACwjQAAsI2AALCNwACwjoAAsJHAALCWAACwloAAsJcAALCXgACwmAAAsJiAALCZAACwmYAAsJoAALCeQACwnwAAsJ/AALCggACwoUAAsKIAALCiwACwo4AAsKRAALCkwACwtIAAsLUAALC1gACwtgAAsLbAALC3AACwt0AAsLeAALC3wACwuEAAsLjAALC5AACwuUAAsLnAALC6AACwycAAsMpAALDKwACwy0AAsMwAALDMQACwzIAAsMzAALDNAACwzYAAsM4AALDOQACwzoAAsM8AALDPQACw3wAAsN+AALDgQACw4MAAsOGAALDhwACw4gAAsOJAALDigACw4wAAsOOAALDjwACw5AAAsOSAALDkwACw6AAAsOhAALDogACw6QAAsPjAALD5QACw+cAAsPpAALD7AACw+0AAsPuAALD7wACw/AAAsPyAALD9AACw/UAAsP2AALD+AACw/kAAsQ4AALEOgACxDwAAsQ+AALEQQACxEIAAsRDAALERAACxEUAAsRHAALESQACxEoAAsRLAALETQACxE4AAsSNAALEjwACxJEAAsSTAALElgACxJcAAsSYAALEmQACxJoAAsScAALEngACxJ8AAsSgAALEogACxKMAAsTiAALE5AACxOYAAsToAALE6wACxOwAAsTtAALE7gACxO8AAsTxAALE8wACxPQAAsT1AALE9wACxPgAAsU3AALFOQACxTsAAsU9AALFQAACxUEAAsVCAALFQwACxUQAAsVGAALFSAACxUkAAsVKAALFTAACxU0AAsVyAALFlgACxb0AAsXhAALF5AACxeYAAsXoAALF6gACxewAAsXuAALF7wACxfIAAsX/AALGDgACxhAAAsYSAALGFAACxhYAAsYYAALGGgACxhwAAsYrAALGLgACxjEAAsY0AALGNwACxjoAAsY9AALGQAACxkIAAsaBAALGgwACxoUAAsaHAALGigACxosAAsaMAALGjQACxo4AAsaQAALGkgACxpMAAsaUAALGlgACxpcAAsbWAALG2AACxtoAAsbcAALG3wACxuAAAsbhAALG4gACxuMAAsblAALG5wACxugAAsbpAALG6wACxuwAAscrAALHLQACxy8AAscxAALHNAACxzUAAsc2AALHNwACxzgAAsc6AALHPAACxz0AAsc+AALHQAACx0EAAseAAALHggACx4UAAseHAALHigACx4sAAseMAALHjQACx44AAseQAALHkgACx5MAAseUAALHlgACx5cAAsfWAALH2AACx9oAAsfcAALH3wACx+AAAsfhAALH4gACx+MAAsflAALH5wACx+gAAsfpAALH6wACx+wAAsgrAALILQACyC8AAsgxAALINAACyDUAAsg2AALINwACyDgAAsg6AALIPAACyD0AAsg+AALIQAACyEEAAsiAAALIggACyIQAAsiGAALIiQACyIoAAsiLAALIjAACyI0AAsiPAALIkQACyJIAAsiTAALIlQACyJYAAsjhAALJBAACySQAAslEAALJRgACyUgAAslKAALJTAACyU8AAslQAALJUQACyVQAAslVAALJVwACyVgAAslaAALJXQACyV4AAslfAALJYgACyWMAAsloAALJdQACyXoAAsl8AALJfgACyYMAAsmGAALJiQACyYsAAsmwAALJ1AACyfsAAsofAALKIgACyiQAAsomAALKKAACyioAAsosAALKLQACyjAAAso9AALKTgACylAAAspSAALKVAACylYAAspYAALKWgACylwAAspeAALKbwACynIAAsp1AALKeAACynsAAsp+AALKgQACyoQAAsqHAALKiQACysgAAsrKAALKzAACys4AAsrRAALK0gACytMAAsrUAALK1QACytcAAsrZAALK2gACytsAAsrdAALK3gACyx0AAssfAALLIQACyyMAAssmAALLJwACyygAAsspAALLKgACyywAAssuAALLLwACyzAAAssyAALLMwACy3IAAst0AALLdwACy3kAAst8AALLfQACy34AAst/AALLgAACy4IAAsuEAALLhQACy4YAAsuIAALLiQACy5YAAsuXAALLmAACy5oAAsvZAALL2wACy90AAsvfAALL4gACy+MAAsvkAALL5QACy+YAAsvoAALL6gACy+sAAsvsAALL7gACy+8AAswuAALMMAACzDIAAsw0AALMNwACzDgAAsw5AALMOgACzDsAAsw9AALMPwACzEAAAsxBAALMQwACzEQAAsyDAALMhQACzIcAAsyJAALMjAACzI0AAsyOAALMjwACzJAAAsySAALMlAACzJUAAsyWAALMmAACzJkAAszYAALM2gACzNwAAszeAALM4QACzOIAAszjAALM5AACzOUAAsznAALM6QACzOoAAszrAALM7QACzO4AAs0tAALNLwACzTEAAs0zAALNNgACzTcAAs04AALNOQACzToAAs08AALNPgACzT8AAs1AAALNQgACzUMAAs1oAALNjAACzbMAAs3XAALN2gACzdwAAs3eAALN4AACzeIAAs3kAALN5QACzegAAs31AALOBAACzgYAAs4IAALOCgACzgwAAs4OAALOEAACzhIAAs4hAALOJAACzicAAs4qAALOLQACzjAAAs4zAALONgACzjgAAs53AALOeQACznsAAs59AALOgAACzoEAAs6CAALOgwACzoQAAs6GAALOiAACzokAAs6KAALOjAACzo0AAs7MAALOzgACztAAAs7SAALO1QACztYAAs7XAALO2AACztkAAs7bAALO3QACzt4AAs7fAALO4QACzuIAAs8hAALPIwACzyUAAs8nAALPKgACzysAAs8sAALPLQACzy4AAs8wAALPMgACzzMAAs80AALPNgACzzcAAs92AALPeAACz3sAAs99AALPgAACz4EAAs+CAALPgwACz4QAAs+GAALPiAACz4kAAs+KAALPjAACz40AAs/MAALPzgACz9AAAs/SAALP1QACz9YAAs/XAALP2AACz9kAAs/bAALP3QACz94AAs/fAALP4QACz+IAAtAhAALQIwAC0CUAAtAnAALQKgAC0CsAAtAsAALQLQAC0C4AAtAwAALQMgAC0DMAAtA0AALQNgAC0DcAAtB2AALQeAAC0HoAAtB8AALQfwAC0IAAAtCBAALQggAC0IMAAtCFAALQhwAC0IgAAtCJAALQiwAC0IwAAtCVAALQlgAC0JgAAtDbAALQ/wAC0SMAAtFGAALRbQAC0Y0AAtG0AALR2wAC0fsAAtIfAALSQwAC0kUAAtJIAALSSgAC0kwAAtJOAALSUQAC0lQAAtJWAALSWAAC0lsAAtJdAALSXwAC0mIAAtJlAALSZgAC0m8AAtJ8AALSfwAC0oEAAtKEAALShwAC0okAAtKuAALS0gAC0vkAAtMdAALTIAAC0yIAAtMkAALTJgAC0ygAAtMqAALTKwAC0y4AAtM7AALTTgAC01AAAtNSAALTVAAC01YAAtNYAALTWgAC01wAAtNeAALTYAAC03MAAtN2AALTeQAC03wAAtN/AALTggAC04UAAtOIAALTiwAC044AAtOQAALTzwAC09EAAtPUAALT1gAC09kAAtPaAALT2wAC09wAAtPdAALT3wAC0+EAAtPiAALT4wAC0+UAAtPmAALT7wAC0/AAAtPyAALUMQAC1DMAAtQ1AALUNwAC1DoAAtQ7AALUPAAC1D0AAtQ+AALUQAAC1EIAAtRDAALURAAC1EYAAtRHAALUhgAC1IgAAtSLAALUjQAC1JAAAtSRAALUkgAC1JMAAtSUAALUlgAC1JgAAtSZAALUmgAC1JwAAtSdAALUpgAC1KcAAtSpAALU6AAC1OoAAtTsAALU7gAC1PEAAtTyAALU8wAC1PQAAtT1AALU9wAC1PkAAtT6AALU+wAC1P0AAtT+AALVPQAC1T8AAtVCAALVRAAC1UcAAtVIAALVSQAC1UoAAtVLAALVTQAC1U8AAtVQAALVUQAC1VMAAtVUAALVXQAC1V4AAtVgAALVnwAC1aEAAtWjAALVpQAC1agAAtWpAALVqgAC1asAAtWsAALVrgAC1bAAAtWxAALVsgAC1bQAAtW1AALV9AAC1fYAAtX5AALV+wAC1f4AAtX/AALWAAAC1gEAAtYCAALWBAAC1gYAAtYHAALWCAAC1goAAtYLAALWGAAC1hkAAtYaAALWHAAC1lsAAtZdAALWXwAC1mEAAtZkAALWZQAC1mYAAtZnAALWaAAC1moAAtZsAALWbQAC1m4AAtZwAALWcQAC1rAAAtayAALWtQAC1rcAAta6AALWuwAC1rwAAta9AALWvgAC1sAAAtbCAALWwwAC1sQAAtbGAALWxwAC1tIAAtbfAALW6AAC1uoAAtbsAALW7wAC1vIAAtb7AALW/gAC1wEAAtcEAALXBwAC1wkAAtcRAALXGQAC12QAAteHAALXpwAC18cAAtfJAALXywAC180AAtfPAALX0gAC19MAAtfUAALX1wAC19gAAtfaAALX2wAC190AAtffAALX4AAC1+EAAtfkAALX5QAC1+oAAtf3AALX/AAC1/4AAtgAAALYBQAC2AgAAtgLAALYDQAC2DIAAthWAALYfQAC2KEAAtikAALYpgAC2KgAAtiqAALYrAAC2K4AAtivAALYsgAC2L8AAtjQAALY0gAC2NQAAtjWAALY2AAC2NoAAtjcAALY3gAC2OAAAtjxAALY9AAC2PcAAtj6AALY/QAC2QAAAtkDAALZBgAC2QkAAtkLAALZSgAC2UwAAtlOAALZUAAC2VMAAtlUAALZVQAC2VYAAtlXAALZWQAC2VsAAtlcAALZXQAC2V8AAtlgAALZnwAC2aEAAtmjAALZpQAC2agAAtmpAALZqgAC2asAAtmsAALZrgAC2bAAAtmxAALZsgAC2bQAAtm1AALZ9AAC2fYAAtn5AALZ+wAC2f4AAtn/AALaAAAC2gEAAtoCAALaBAAC2gYAAtoHAALaCAAC2goAAtoLAALaGAAC2hkAAtoaAALaHAAC2lsAAtpdAALaXwAC2mEAAtpkAALaZQAC2mYAAtpnAALaaAAC2moAAtpsAALabQAC2m4AAtpwAALacQAC2rAAAtqyAALatAAC2rYAAtq5AALaugAC2rsAAtq8AALavQAC2r8AAtrBAALawgAC2sMAAtrFAALaxgAC2wUAAtsHAALbCQAC2wsAAtsOAALbDwAC2xAAAtsRAALbEgAC2xQAAtsWAALbFwAC2xgAAtsaAALbGwAC21oAAttcAALbXgAC22AAAttjAALbZAAC22UAAttmAALbZwAC22kAAttrAALbbAAC220AAttvAALbcAAC268AAtuxAALbswAC27UAAtu4AALbuQAC27oAAtu7AALbvAAC274AAtvAAALbwQAC28IAAtvEAALbxQAC2+oAAtwOAALcNQAC3FkAAtxcAALcXgAC3GAAAtxiAALcZAAC3GYAAtxnAALcagAC3HcAAtyGAALciAAC3IoAAtyMAALcjgAC3JAAAtySAALclAAC3KMAAtymAALcqQAC3KwAAtyvAALcsgAC3LUAAty4AALcugAC3PkAAtz7AALc/QAC3P8AAt0CAALdAwAC3QQAAt0FAALdBgAC3QgAAt0KAALdCwAC3QwAAt0OAALdDwAC3U4AAt1QAALdUgAC3VQAAt1XAALdWAAC3VkAAt1aAALdWwAC3V0AAt1fAALdYAAC3WEAAt1jAALdZAAC3aMAAt2lAALdpwAC3akAAt2sAALdrQAC3a4AAt2vAALdsAAC3bIAAt20AALdtQAC3bYAAt24AALduQAC3fgAAt36AALd/AAC3f4AAt4BAALeAgAC3gMAAt4EAALeBQAC3gcAAt4JAALeCgAC3gsAAt4NAALeDgAC3k0AAt5PAALeUQAC3lMAAt5WAALeVwAC3lgAAt5ZAALeWgAC3lwAAt5eAALeXwAC3mAAAt5iAALeYwAC3qIAAt6kAALepgAC3qgAAt6rAALerAAC3q0AAt6uAALerwAC3rEAAt6zAALetAAC3rUAAt63AALeuAAC3vcAAt75AALe+wAC3v0AAt8AAALfAQAC3wIAAt8DAALfBAAC3wYAAt8IAALfCQAC3woAAt8MAALfDQAC31gAAt97AALfmwAC37sAAt+9AALfvwAC38EAAt/DAALfxgAC38cAAt/IAALfywAC38wAAt/OAALfzwAC39EAAt/TAALf1AAC39UAAt/YAALf2QAC394AAt/rAALf8AAC3/IAAt/0AALf+QAC3/wAAt//AALgAQAC4CYAAuBKAALgcQAC4JUAAuCYAALgmgAC4JwAAuCeAALgoAAC4KIAAuCjAALgpgAC4LMAAuDEAALgxgAC4MgAAuDKAALgzAAC4M4AAuDQAALg0gAC4NQAAuDlAALg6AAC4OsAAuDuAALg8QAC4PQAAuD3AALg+gAC4P0AAuD/AALhPgAC4UAAAuFCAALhRAAC4UcAAuFIAALhSQAC4UoAAuFLAALhTQAC4U8AAuFQAALhUQAC4VMAAuFUAALhkwAC4ZUAAuGXAALhmQAC4ZwAAuGdAALhngAC4Z8AAuGgAALhogAC4aQAAuGlAALhpgAC4agAAuGpAALh6AAC4eoAAuHtAALh7wAC4fIAAuHzAALh9AAC4fUAAuH2AALh+AAC4foAAuH7AALh/AAC4f4AAuH/AALiDAAC4g0AAuIOAALiEAAC4k8AAuJRAALiUwAC4lUAAuJYAALiWQAC4loAAuJbAALiXAAC4l4AAuJgAALiYQAC4mIAAuJkAALiZQAC4qQAAuKmAALiqAAC4qoAAuKtAALirgAC4q8AAuKwAALisQAC4rMAAuK1AALitgAC4rcAAuK5AALiugAC4vkAAuL7AALi/QAC4v8AAuMCAALjAwAC4wQAAuMFAALjBgAC4wgAAuMKAALjCwAC4wwAAuMOAALjDwAC404AAuNQAALjUgAC41QAAuNXAALjWAAC41kAAuNaAALjWwAC410AAuNfAALjYAAC42EAAuNjAALjZAAC46MAAuOlAALjpwAC46kAAuOsAALjrQAC464AAuOvAALjsAAC47IAAuO0AALjtQAC47YAAuO4AALjuQAC494AAuQCAALkKQAC5E0AAuRQAALkUgAC5FQAAuRWAALkWAAC5FoAAuRbAALkXgAC5GsAAuR6AALkfAAC5H4AAuSAAALkggAC5IQAAuSGAALkiAAC5JcAAuSaAALknQAC5KAAAuSjAALkpgAC5KkAAuSsAALkrgAC5O0AAuTvAALk8QAC5PMAAuT2AALk9wAC5PgAAuT5AALk+gAC5PwAAuT+AALk/wAC5QAAAuUCAALlAwAC5UIAAuVEAALlRgAC5UgAAuVLAALlTAAC5U0AAuVOAALlTwAC5VEAAuVTAALlVAAC5VUAAuVXAALlWAAC5ZcAAuWZAALlmwAC5Z0AAuWgAALloQAC5aIAAuWjAALlpAAC5aYAAuWoAALlqQAC5aoAAuWsAALlrQAC5ewAAuXuAALl8AAC5fIAAuX1AALl9gAC5fcAAuX4AALl+QAC5fsAAuX9AALl/gAC5f8AAuYBAALmAgAC5kEAAuZDAALmRQAC5kcAAuZKAALmSwAC5kwAAuZNAALmTgAC5lAAAuZSAALmUwAC5lQAAuZWAALmVwAC5pYAAuaYAALmmgAC5pwAAuafAALmoAAC5qEAAuaiAALmowAC5qUAAuanAALmqAAC5qkAAuarAALmrAAC5usAAubtAALm7wAC5vEAAub0AALm9QAC5vYAAub3AALm+AAC5voAAub8AALm/QAC5v4AAucAAALnAQAC50wAAudvAALnjwAC568AAuexAALnswAC57UAAue3AALnugAC57sAAue8AALnvwAC58AAAufCAALnwwAC58UAAufIAALnyQAC58oAAufNAALnzgAC59cAAufkAALn6QAC5+sAAuftAALn8gAC5/UAAuf4AALn+gAC6B8AAuhDAALoagAC6I4AAuiRAALokwAC6JUAAuiXAALomQAC6JsAAuicAALonwAC6KwAAui9AALovwAC6MEAAujDAALoxQAC6McAAujJAALoywAC6M0AAujeAALo4QAC6OQAAujnAALo6gAC6O0AAujwAALo8wAC6PYAAuj4AALpNwAC6TkAAuk7AALpPQAC6UAAAulBAALpQgAC6UMAAulEAALpRgAC6UgAAulJAALpSgAC6UwAAulNAALpjAAC6Y4AAumQAALpkgAC6ZUAAumWAALplwAC6ZgAAumZAALpmwAC6Z0AAumeAALpnwAC6aEAAumiAALp4QAC6eMAAunmAALp6AAC6esAAunsAALp7QAC6e4AAunvAALp8QAC6fMAAun0AALp9QAC6fcAAun4AALqBQAC6gYAAuoHAALqCQAC6kgAAupKAALqTAAC6k4AAupRAALqUgAC6lMAAupUAALqVQAC6lcAAupZAALqWgAC6lsAAupdAALqXgAC6p0AAuqfAALqoQAC6qMAAuqmAALqpwAC6qgAAuqpAALqqgAC6qwAAuquAALqrwAC6rAAAuqyAALqswAC6vIAAur0AALq9gAC6vgAAur7AALq/AAC6v0AAur+AALq/wAC6wEAAusDAALrBAAC6wUAAusHAALrCAAC60cAAutJAALrSwAC600AAutQAALrUQAC61IAAutTAALrVAAC61YAAutYAALrWQAC61oAAutcAALrXQAC65wAAuueAALroAAC66IAAuulAALrpgAC66cAAuuoAALrqQAC66sAAuutAALrrgAC668AAuuxAALrsgAC69cAAuv7AALsIgAC7EYAAuxJAALsSwAC7E0AAuxPAALsUQAC7FMAAuxUAALsVwAC7GQAAuxzAALsdQAC7HcAAux5AALsewAC7H0AAux/AALsgQAC7JAAAuyTAALslgAC7JkAAuycAALsnwAC7KIAAuylAALspwAC7OYAAuzoAALs6gAC7OwAAuzvAALs8AAC7PEAAuzyAALs8wAC7PUAAuz3AALs+AAC7PkAAuz7AALs/AAC7TsAAu09AALtPwAC7UEAAu1EAALtRQAC7UYAAu1HAALtSAAC7UoAAu1MAALtTQAC7U4AAu1QAALtUQAC7ZAAAu2SAALtlAAC7ZYAAu2ZAALtmgAC7ZsAAu2cAALtnQAC7Z8AAu2hAALtogAC7aMAAu2lAALtpgAC7eUAAu3nAALt6QAC7esAAu3uAALt7wAC7fAAAu3xAALt8gAC7fQAAu32AALt9wAC7fgAAu36AALt+wAC7joAAu48AALuPgAC7kAAAu5DAALuRAAC7kUAAu5GAALuRwAC7kkAAu5LAALuTAAC7k0AAu5PAALuUAAC7o8AAu6RAALukwAC7pUAAu6YAALumQAC7poAAu6bAALunAAC7p4AAu6gAALuoQAC7qIAAu6kAALupQAC7uQAAu7mAALu6AAC7uoAAu7tAALu7gAC7u8AAu7wAALu8QAC7vMAAu71AALu9gAC7vcAAu75AALu+gAC70UAAu9oAALviAAC76gAAu+qAALvrAAC764AAu+wAALvswAC77QAAu+1AALvuAAC77kAAu+7AALvvAAC774AAu/BAALvwgAC78MAAu/GAALvxwAC78wAAu/ZAALv3gAC7+AAAu/iAALv5wAC7+oAAu/tAALv7wAC8BQAAvA4AALwXwAC8IMAAvCGAALwiAAC8IoAAvCMAALwjgAC8JAAAvCRAALwlAAC8KEAAvCyAALwtAAC8LYAAvC4AALwugAC8LwAAvC+AALwwAAC8MIAAvDTAALw1gAC8NkAAvDcAALw3wAC8OIAAvDlAALw6AAC8OsAAvDtAALxLAAC8S4AAvEwAALxMgAC8TUAAvE2AALxNwAC8TgAAvE5AALxOwAC8T0AAvE+AALxPwAC8UEAAvFCAALxgQAC8YMAAvGFAALxhwAC8YoAAvGLAALxjAAC8Y0AAvGOAALxkAAC8ZIAAvGTAALxlAAC8ZYAAvGXAALx1gAC8dgAAvHbAALx3QAC8eAAAvHhAALx4gAC8eMAAvHkAALx5gAC8egAAvHpAALx6gA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DFD0AAxQ+AAMUPwADFEEAAxRDAAMURAADFEUAAxRHAAMUSAADFIcAAxSJAAMUiwADFI0AAxSQAAMUkQADFJIAAxSTAAMUlAADFJYAAxSYAAMUmQADFJoAAxScAAMUnQADFNwAAxTeAAMU4AADFOIAAxTlAAMU5gADFOcAAxToAAMU6QADFOsAAxTtAAMU7gADFO8AAxTxAAMU8gADFTEAAxUzAAMVNQADFTcAAxU6AAMVOwADFTwAAxU9AAMVPgADFUAAAxVCAAMVQwADFUQAAxVGAAMVRwADFYYAAxWIAAMVigADFYwAAxWPAAMVkAADFZEAAxWSAAMVkwADFZUAAxWXAAMVmAADFZkAAxWbAAMVnAADFdsAAxXdAAMV3wADFeEAAxXkAAMV5QADFeYAAxXnAAMV6AADFeoAAxXsAAMV7QADFe4AAxXwAAMV8QADFjwAAxZfAAMWfwADFp8AAxahAAMWowADFqUAAxanAAMWqgADFqsAAxasAAMWrwADFrAAAxayAAMWswADFrUAAxa4AAMWuQADFroAAxa9AAMWvgADFsMAAxbQAAMW1QADFtcAAxbZAAMW3gADFuEAAxbkAAMW5gADFwsAAxcvAAMXVgADF3oAAxd9AAMXfwADF4EAAxeDAAMXhQADF4cAAxeIAAMXiwADF5gAAxepAAMXqwADF60AAxevAAMXsQADF7MAAxe1AAMXtwADF7kAAxfKAAMXzQADF9AAAxfTAAMX1gADF9kAAxfcAAMX3wADF+IAAxfkAAMYIwADGCUAAxgnAAMYKQADGCwAAxgtAAMYLgADGC8AAxgwAAMYMgADGDQAAxg1AAMYNgADGDgAAxg5AAMYeAADGHoAAxh8AAMYfgADGIEAAxiCAAMYgwADGIQAAxiFAAMYhwADGIkAAxiKAAMYiwADGI0AAxiOAAMYzQADGM8AAxjSAAMY1AADGNcAAxjYAAMY2QADGNoAAxjbAAMY3QADGN8AAxjgAAMY4QADGOMAAxjkAAMY8QADGPIAAxjzAAMY9QADGTQAAxk2AAMZOAADGToAAxk9AAMZPgADGT8AAxlAAAMZQQADGUMAAxlFAAMZRgADGUcAAxlJAAMZSgADGYkAAxmLAAMZjQADGY8AAxmSAAMZkwADGZQAAxmVAAMZlgADGZgAAxmaAAMZmwADGZwAAxmeAAMZnwADGd4AAxngAAMZ4gADGeQAAxnnAAMZ6AADGekAAxnqAAMZ6wADGe0AAxnvAAMZ8AADGfEAAxnzAAMZ9AADGjMAAxo1AAMaNwADGjkAAxo8AAMaPQADGj4AAxo/AAMaQAADGkIAAxpEAAMaRQADGkYAAxpIAAMaSQADGogAAxqKAAMajAADGo4AAxqRAAMakgADGpMAAxqUAAMalQADGpcAAxqZAAMamgADGpsAAxqdAAMangADGsMAAxrnAAMbDgADGzIAAxs1AAMbNwADGzkAAxs7AAMbPQADGz8AAxtAAAMbQwADG1AAAxtfAAMbYQADG2MAAxtlAAMbZwADG2kAAxtrAAMbbQADG3wAAxt/AAMbggADG4UAAxuIAAMbiwADG44AAxuRAAMbkwADG9IAAxvUAAMb1gADG9gAAxvbAAMb3AADG90AAxveAAMb3wADG+EAAxvjAAMb5AADG+UAAxvnAAMb6AADHCcAAxwpAAMcKwADHC0AAxwwAAMcMQADHDIAAxwzAAMcNAADHDYAAxw4AAMcOQADHDoAAxw8AAMcPQADHHwAAxx+AAMcgAADHIIAAxyFAAMchgADHIcAAxyIAAMciQADHIsAAxyNAAMcjgADHI8AAxyRAAMckgADHNEAAxzTAAMc1gADHNgAAxzbAAMc3AADHN0AAxzeAAMc3wADHOEAAxzjAAMc5AADHOUAAxznAAMc6AADHScAAx0pAAMdKwADHS0AAx0wAAMdMQADHTIAAx0zAAMdNAADHTYAAx04AAMdOQADHToAAx08AAMdPQADHXwAAx1+AAMdgAADHYIAAx2FAAMdhgADHYcAAx2IAAMdiQADHYsAAx2NAAMdjgADHY8AAx2RAAMdkgADHdEAAx3TAAMd1QADHdcAAx3aAAMd2wADHdwAAx3dAAMd3gADHeAAAx3iAAMd4wADHeQAAx3mAAMd5wADHjIAAx5VAAMedQADHpUAAx6XAAMemQADHpsAAx6dAAMeoAADHqEAAx6iAAMepQADHqYAAx6oAAMeqQADHqsAAx6uAAMerwADHrAAAx6zAAMetAADHr0AAx7KAAMezwADHtEAAx7TAAMe2AADHtsAAx7eAAMe4AADHwUAAx8pAAMfUAADH3QAAx93AAMfeQADH3sAAx99AAMffwADH4EAAx+CAAMfhQADH5IAAx+jAAMfpQADH6cAAx+pAAMfqwADH60AAx+vAAMfsQADH7MAAx/EAAMfxwADH8oAAx/NAAMf0AADH9MAAx/WAAMf2QADH9wAAx/eAAMgHQADIB8AAyAhAAMgIwADICYAAyAnAAMgKAADICkAAyAqAAMgLAADIC4AAyAvAAMgMAADIDIAAyAzAAMgcgADIHQAAyB2AAMgeAADIHsAAyB8AAMgfQADIH4AAyB/AAMggQADIIMAAyCEAAMghQADIIcAAyCIAAMgxwADIMkAAyDMAAMgzgADINEAAyDSAAMg0wADINQAAyDVAAMg1wADINkAAyDaAAMg2wADIN0AAyDeAAMg6wADIOwAAyDtAAMg7wADIS4AAyEwAAMhMgADITQAAyE3AAMhOAADITkAAyE6AAMhOwADIT0AAyE/AAMhQAADIUEAAyFDAAMhRAADIYMAAyGFAAMhhwADIYkAAyGMAAMhjQADIY4AAyGPAAMhkAADIZIAAyGUAAMhlQADIZYAAyGYAAMhmQADIdgAAyHaAAMh3AADId4AAyHhAAMh4gADIeMAAyHkAAMh5QADIecAAyHpAAMh6gADIesAAyHtAAMh7gADIi0AAyIvAAMiMQADIjMAAyI2AAMiNwADIjgAAyI5AAMiOgADIjwAAyI+AAMiPwADIkAAAyJCAAMiQwADIoIAAyKEAAMihgADIogAAyKLAAMijAADIo0AAyKOAAMijwADIpEAAyKTAAMilAADIpUAAyKXAAMimAADIr0AAyLhAAMjCAADIywAAyMvAAMjMQADIzMAAyM1AAMjNwADIzkAAyM6AAMjPQADI0oAAyNZAAMjWwADI10AAyNfAAMjYQADI2MAAyNlAAMjZwADI3YAAyN5AAMjfAADI38AAyOCAAMjhQADI4gAAyOLAAMjjQADI8wAAyPOAAMj0AADI9IAAyPVAAMj1gADI9cAAyPYAAMj2QADI9sAAyPdAAMj3gADI98AAyPhAAMj4gADJCEAAyQjAAMkJQADJCcAAyQqAAMkKwADJCwAAyQtAAMkLgADJDAAAyQyAAMkMwADJDQAAyQ2AAMkNwADJHYAAyR4AAMkegADJHwAAyR/AAMkgAADJIEAAySCAAMkgwADJIUAAySHAAMkiAADJIkAAySLAAMkjAADJMsAAyTNAAMkzwADJNEAAyTUAAMk1QADJNYAAyTXAAMk2AADJNoAAyTcAAMk3QADJN4AAyTgAAMk4QADJSAAAyUiAAMlJAADJSYAAyUpAAMlKgADJSsAAyUsAAMlLQADJS8AAyUxAAMlMgADJTMAAyU1AAMlNgADJXUAAyV3AAMleQADJXsAAyV+AAMlfwADJYAAAyWBAAMlggADJYQAAyWGAAMlhwADJYgAAyWKAAMliwADJcoAAyXMAAMlzgADJdAAAyXTAAMl1AADJdUAAyXWAAMl1wADJdkAAyXbAAMl3AADJd0AAyXfAAMl4AADJisAAyZOAAMmbgADJo4AAyaQAAMmkgADJpQAAyaWAAMmmQADJpoAAyabAAMmngADJp8AAyahAAMmogADJqQAAyanAAMmqAADJqkAAyasAAMmrQADJrYAAybDAAMmyAADJsoAAybMAAMm0QADJtQAAybXAAMm2QADJv4AAyciAAMnSQADJ20AAydwAAMncgADJ3QAAyd2AAMneAADJ3oAAyd7AAMnfgADJ4sAAyecAAMnngADJ6AAAyeiAAMnpAADJ6YAAyeoAAMnqgADJ6wAAye9AAMnwAADJ8MAAyfGAAMnyQADJ8wAAyfPAAMn0gADJ9UAAyfXAAMoFgADKBgAAygaAAMoHAADKB8AAyggAAMoIQADKCIAAygjAAMoJQADKCcAAygoAAMoKQADKCsAAygsAAMoawADKG0AAyhvAAMocQADKHQAAyh1AAModgADKHcAAyh4AAMoegADKHwAAyh9AAMofgADKIAAAyiBAAMowAADKMIAAyjFAAMoxwADKMoAAyjLAAMozAADKM0AAyjOAAMo0AADKNIAAyjTAAMo1AADKNYAAyjXAAMo5AADKOUAAyjmAAMo6AADKScAAykpAAMpKwADKS0AAykwAAMpMQADKTIAAykzAAMpNAADKTYAAyk4AAMpOQADKToAAyk8AAMpPQADKXwAAyl+AAMpgAADKYIAAymFAAMphgADKYcAAymIAAMpiQADKYsAAymNAAMpjgADKY8AAymRAAMpkgADKdEAAynTAAMp1QADKdcAAynaAAMp2wADKdwAAyndAAMp3gADKeAAAyniAAMp4wADKeQAAynmAAMp5wADKiYAAyooAAMqKgADKiwAAyovAAMqMAADKjEAAyoyAAMqMwADKjUAAyo3AAMqOAADKjkAAyo7AAMqPAADKnsAAyp9AAMqfwADKoEAAyqEAAMqhQADKoYAAyqHAAMqiAADKooAAyqMAAMqjQADKo4AAyqQAAMqkQADKrYAAyraAAMrAQADKyUAAysoAAMrKgADKywAAysuAAMrMAADKzIAAyszAAMrNgADK0MAAytSAAMrVAADK1YAAytYAAMrWgADK1wAAyteAAMrYAADK28AAytyAAMrdQADK3gAAyt7AAMrfgADK4EAAyuEAAMrhgADK8UAAyvHAAMryQADK8sAAyvOAAMrzwADK9AAAyvRAAMr0gADK9QAAyvWAAMr1wADK9gAAyvaAAMr2wADLBoAAywcAAMsHgADLCAAAywjAAMsJAADLCUAAywmAAMsJwADLCkAAywrAAMsLAADLC0AAywvAAMsMAADLG8AAyxxAAMscwADLHUAAyx4AAMseQADLHoAAyx7AAMsfAADLH4AAyyAAAMsgQADLIIAAyyEAAMshQADLMQAAyzGAAMsyQADLMsAAyzOAAMszwADLNAAAyzRAAMs0gADLNQAAyzWAAMs1wADLNgAAyzaAAMs2wADLRoAAy0cAAMtHgADLSAAAy0jAAMtJAADLSUAAy0mAAMtJwADLSkAAy0rAAMtLAADLS0AAy0vAAMtMAADLW8AAy1xAAMtcwADLXUAAy14AAMteQADLXoAAy17AAMtfAADLX4AAy2AAAMtgQADLYIAAy2EAAMthQADLcQAAy3GAAMtyAADLcoAAy3NAAMtzgADLc8AAy3QAAMt0QADLdMAAy3VAAMt1gADLdcAAy3ZAAMt2gADLiUAAy5IAAMuaAADLogAAy6KAAMujAADLo4AAy6QAAMukwADLpQAAy6VAAMumAADLpkAAy6bAAMunAADLp4AAy6hAAMuogADLqMAAy6mAAMupwADLqwAAy65AAMuvgADLsAAAy7CAAMuxwADLsoAAy7NAAMuzwADLvQAAy8YAAMvPwADL2MAAy9mAAMvaAADL2oAAy9sAAMvbgADL3AAAy9xAAMvdAADL4EAAy+SAAMvlAADL5YAAy+YAAMvmgADL5wAAy+eAAMvoAADL6IAAy+zAAMvtgADL7kAAy+8AAMvvwADL8IAAy/FAAMvyAADL8sAAy/NAAMwDAADMA4AAzAQAAMwEgADMBUAAzAWAAMwFwADMBgAAzAZAAMwGwADMB0AAzAeAAMwHwADMCEAAzAiAAMwYQADMGMAAzBlAAMwZwADMGoAAzBrAAMwbAADMG0AAzBuAAMwcAADMHIAAzBzAAMwdAADMHYAAzB3AAMwtgADMLgAAzC7AAMwvQADMMAAAzDBAAMwwgADMMMAAzDEAAMwxgADMMgAAzDJAAMwygADMMwAAzDNAAMw2gADMNsAAzDcAAMw3gADMR0AAzEfAAMxIQADMSMAAzEmAAMxJwADMSgAAzEpAAMxKgADMSwAAzEuAAMxLwADMTAAAzEyAAMxMwADMXIAAzF0AAMxdgADMXgAAzF7AAMxfAADMX0AAzF+AAMxfwADMYEAAzGDAAMxhAADMYUAAzGHAAMxiAADMccAAzHJAAMxywADMc0AAzHQAAMx0QADMdIAAzHTAAMx1AADMdYAAzHYAAMx2QADMdoAAzHcAAMx3QADMhwAAzIeAAMyIAADMiIAAzIlAAMyJgADMicAAzIoAAMyKQADMisAAzItAAMyLgADMi8AAzIxAAMyMgADMnEAAzJzAAMydQADMncAAzJ6AAMyewADMnwAAzJ9AAMyfgADMoAAAzKCAAMygwADMoQAAzKGAAMyhwADMqwAAzLQAAMy9wADMxsAAzMeAAMzIAADMyIAAzMkAAMzJgADMygAAzMpAAMzLAADMzkAAzNIAAMzSgADM0wAAzNOAAMzUAADM1IAAzNUAAMzVgADM2UAAzNoAAMzawADM24AAzNxAAMzdAADM3cAAzN6AAMzfAADM7sAAzO9AAMzvwADM8EAAzPEAAMzxQADM8YAAzPHAAMzyAADM8oAAzPMAAMzzQADM84AAzPQAAMz0QADNBAAAzQSAAM0FAADNBYAAzQZAAM0GgADNBsAAzQcAAM0HQADNB8AAzQhAAM0IgADNCMAAzQlAAM0JgADNGUAAzRnAAM0aQADNGsAAzRuAAM0bwADNHAAAzRxAAM0cgADNHQAAzR2AAM0dwADNHgAAzR6AAM0ewADNLoAAzS8AAM0vgADNMAAAzTDAAM0xAADNMUAAzTGAAM0xwADNMkAAzTLAAM0zAADNM0AAzTPAAM00AADNQ8AAzURAAM1EwADNRUAAzUYAAM1GQADNRoAAzUbAAM1HAADNR4AAzUgAAM1IQADNSIAAzUkAAM1JQADNWQAAzVmAAM1aAADNWoAAzVtAAM1bgADNW8AAzVwAAM1cQADNXMAAzV1AAM1dgADNXcAAzV5AAM1egADNbkAAzW7AAM1vQADNb8AAzXCAAM1wwADNcQAAzXFAAM1xgADNcgAAzXKAAM1ywADNcwAAzXOAAM1zwADNhoAAzY9AAM2XQADNn0AAzZ/AAM2gQADNoMAAzaFAAM2iAADNokAAzaKAAM2jQADNo4AAzaQAAM2kQADNpMAAzaWAAM2lwADNpgAAzabAAM2nAADNqEAAzauAAM2swADNrUAAza3AAM2vAADNr8AAzbCAAM2xAADNukAAzcNAAM3NAADN1gAAzdbAAM3XQADN18AAzdhAAM3YwADN2UAAzdmAAM3aQADN3YAAzeHAAM3iQADN4sAAzeNAAM3jwADN5EAAzeTAAM3lQADN5cAAzeoAAM3qwADN64AAzexAAM3tAADN7cAAze6AAM3vQADN8AAAzfCAAM4AQADOAMAAzgFAAM4BwADOAoAAzgLAAM4DAADOA0AAzgOAAM4EAADOBIAAzgTAAM4FAADOBYAAzgXAAM4VgADOFgAAzhaAAM4XAADOF8AAzhgAAM4YQADOGIAAzhjAAM4ZQADOGcAAzhoAAM4aQADOGsAAzhsAAM4qwADOK0AAziwAAM4sgADOLUAAzi2AAM4twADOLgAAzi5AAM4uwADOL0AAzi+AAM4vwADOMEAAzjCAAM4zwADONAAAzjRAAM40wADORIAAzkUAAM5FgADORgAAzkbAAM5HAADOR0AAzkeAAM5HwADOSEAAzkjAAM5JAADOSUAAzknAAM5KAADOWcAAzlpAAM5awADOW0AAzlwAAM5cQADOXIAAzlzAAM5dAADOXYAAzl4AAM5eQADOXoAAzl8AAM5fQADObwAAzm+AAM5wAADOcIAAznFAAM5xgADOccAAznIAAM5yQADOcsAAznNAAM5zgADOc8AAznRAAM50gADOhEAAzoTAAM6FQADOhcAAzoaAAM6GwADOhwAAzodAAM6HgADOiAAAzoiAAM6IwADOiQAAzomAAM6JwADOmYAAzpoAAM6agADOmwAAzpvAAM6cAADOnEAAzpyAAM6cwADOnUAAzp3AAM6eAADOnkAAzp7AAM6fAADOqEAAzrFAAM67AADOxAAAzsTAAM7FQADOxcAAzsZAAM7GwADOx0AAzseAAM7IQADOy4AAzs9AAM7PwADO0EAAztDAAM7RQADO0cAAztJAAM7SwADO1oAAztdAAM7YAADO2MAAztmAAM7aQADO2wAAztvAAM7cQADO7AAAzuyAAM7tAADO7YAAzu5AAM7ugADO7sAAzu8AAM7vQADO78AAzvBAAM7wgADO8MAAzvFAAM7xgADPAUAAzwHAAM8CQADPAsAAzwOAAM8DwADPBAAAzwRAAM8EgADPBQAAzwWAAM8FwADPBgAAzwaAAM8GwADPFoAAzxcAAM8XgADPGAAAzxjAAM8ZAADPGUAAzxmAAM8ZwADPGkAAzxrAAM8bAADPG0AAzxvAAM8cAADPK8AAzyxAAM8swADPLUAAzy4AAM8uQADPLoAAzy7AAM8vAADPL4AAzzAAAM8wQADPMIAAzzEAAM8xQADPQQAAz0GAAM9CAADPQoAAz0NAAM9DgADPQ8AAz0QAAM9EQADPRMAAz0VAAM9FgADPRcAAz0ZAAM9GgADPVkAAz1bAAM9XQADPV8AAz1iAAM9YwADPWQAAz1lAAM9ZgADPWgAAz1qAAM9awADPWwAAz1uAAM9bwADPa4AAz2wAAM9sgADPbQAAz23AAM9uAADPbkAAz26AAM9uwADPb0AAz2/AAM9wAADPcEAAz3DAAM9xAADPg8AAz4yAAM+UgADPnIAAz50AAM+dgADPngAAz56AAM+fQADPn4AAz5/AAM+ggADPoMAAz6FAAM+hgADPogAAz6LAAM+jAADPo0AAz6QAAM+kQADPpYAAz6jAAM+qAADPqoAAz6sAAM+sQADPrQAAz63AAM+uQADPt4AAz8CAAM/KQADP00AAz9QAAM/UgADP1QAAz9WAAM/WAADP1oAAz9bAAM/XgADP2sAAz98AAM/fgADP4AAAz+CAAM/hAADP4YAAz+IAAM/igADP4wAAz+dAAM/oAADP6MAAz+mAAM/qQADP6wAAz+vAAM/sgADP7UAAz+3AAM/9gADP/gAAz/6AAM//AADP/8AA0AAAANAAQADQAIAA0ADAANABQADQAcAA0AIAANACQADQAsAA0AMAANASwADQE0AA0BPAANAUQADQFQAA0BVAANAVgADQFcAA0BYAANAWgADQFwAA0BdAANAXgADQGAAA0BhAANAoAADQKIAA0ClAANApwADQKoAA0CrAANArAADQK0AA0CuAANAsAADQLIAA0CzAANAtAADQLYAA0C3AANAxAADQMUAA0DGAANAyAADQQcAA0EJAANBCwADQQ0AA0EQAANBEQADQRIAA0ETAANBFAADQRYAA0EYAANBGQADQRoAA0EcAANBHQADQVwAA0FeAANBYAADQWIAA0FlAANBZgADQWcAA0FoAANBaQADQWsAA0FtAANBbgADQW8AA0FxAANBcgADQbEAA0GzAANBtQADQbcAA0G6AANBuwADQbwAA0G9AANBvgADQcAAA0HCAANBwwADQcQAA0HGAANBxwADQgYAA0IIAANCCgADQgwAA0IPAANCEAADQhEAA0ISAANCEwADQhUAA0IXAANCGAADQhkAA0IbAANCHAADQlsAA0JdAANCXwADQmEAA0JkAANCZQADQmYAA0JnAANCaAADQmoAA0JsAANCbQADQm4AA0JwAANCcQADQpYAA0K6AANC4QADQwUAA0MIAANDCgADQwwAA0MOAANDEAADQxIAA0MTAANDFgADQyMAA0MyAANDNAADQzYAA0M4AANDOgADQzwAA0M+AANDQAADQ08AA0NSAANDVQADQ1gAA0NbAANDXgADQ2EAA0NkAANDZgADQ6UAA0OnAANDqQADQ6sAA0OuAANDrwADQ7AAA0OxAANDsgADQ7QAA0O2AANDtwADQ7gAA0O6AANDuwADQ/oAA0P8AAND/gADRAAAA0QDAANEBAADRAUAA0QGAANEBwADRAkAA0QLAANEDAADRA0AA0QPAANEEAADRE8AA0RRAANEUwADRFUAA0RYAANEWQADRFoAA0RbAANEXAADRF4AA0RgAANEYQADRGIAA0RkAANEZQADRKQAA0SmAANEqAADRKoAA0StAANErgADRK8AA0SwAANEsQADRLMAA0S1AANEtgADRLcAA0S5AANEugADRPkAA0T7AANE/QADRP8AA0UCAANFAwADRQQAA0UFAANFBgADRQgAA0UKAANFCwADRQwAA0UOAANFDwADRU4AA0VQAANFUgADRVQAA0VXAANFWAADRVkAA0VaAANFWwADRV0AA0VfAANFYAADRWEAA0VjAANFZAADRaMAA0WlAANFpwADRakAA0WsAANFrQADRa4AA0WvAANFsAADRbIAA0W0AANFtQADRbYAA0W4AANFuQADRgQAA0YnAANGRwADRmcAA0ZpAANGawADRm0AA0ZvAANGcgADRnMAA0Z0AANGdwADRngAA0Z6AANGewADRn0AA0aAAANGgQADRoIAA0aFAANGhgADRosAA0aYAANGnQADRp8AA0ahAANGpgADRqkAA0asAANGrgADRtMAA0b3AANHHgADR0IAA0dFAANHRwADR0kAA0dLAANHTQADR08AA0dQAANHUwADR2AAA0dxAANHcwADR3UAA0d3AANHeQADR3sAA0d9AANHfwADR4EAA0eSAANHlQADR5gAA0ebAANHngADR6EAA0ekAANHpwADR6oAA0esAANH6wADR+0AA0fvAANH8QADR/QAA0f1AANH9gADR/cAA0f4AANH+gADR/wAA0f9AANH/gADSAAAA0gBAANIQAADSEIAA0hEAANIRgADSEkAA0hKAANISwADSEwAA0hNAANITwADSFEAA0hSAANIUwADSFUAA0hWAANIlQADSJcAA0iaAANInAADSJ8AA0igAANIoQADSKIAA0ijAANIpQADSKcAA0ioAANIqQADSKsAA0isAANIuQADSLoAA0i7AANIvQADSPwAA0j+AANJAAADSQIAA0kFAANJBgADSQcAA0kIAANJCQADSQsAA0kNAANJDgADSQ8AA0kRAANJEgADSVEAA0lTAANJVQADSVcAA0laAANJWwADSVwAA0ldAANJXgADSWAAA0liAANJYwADSWQAA0lmAANJZwADSaYAA0moAANJqgADSawAA0mvAANJsAADSbEAA0myAANJswADSbUAA0m3AANJuAADSbkAA0m7AANJvAADSfsAA0n9AANJ/wADSgEAA0oEAANKBQADSgYAA0oHAANKCAADSgoAA0oMAANKDQADSg4AA0oQAANKEQADSlAAA0pSAANKVAADSlYAA0pZAANKWgADSlsAA0pcAANKXQADSl8AA0phAANKYgADSmMAA0plAANKZgADSosAA0qvAANK1gADSvoAA0r9AANK/wADSwEAA0sDAANLBQADSwcAA0sIAANLCwADSxgAA0snAANLKQADSysAA0stAANLLwADSzEAA0szAANLNQADS0QAA0tHAANLSgADS00AA0tQAANLUwADS1YAA0tZAANLWwADS5oAA0ucAANLngADS6AAA0ujAANLpAADS6UAA0umAANLpwADS6kAA0urAANLrAADS60AA0uvAANLsAADS+8AA0vxAANL8wADS/UAA0v4AANL+QADS/oAA0v7AANL/AADS/4AA0wAAANMAQADTAIAA0wEAANMBQADTEQAA0xGAANMSAADTEoAA0xNAANMTgADTE8AA0xQAANMUQADTFMAA0xVAANMVgADTFcAA0xZAANMWgADTJkAA0ybAANMngADTKAAA0yjAANMpAADTKUAA0ymAANMpwADTKkAA0yrAANMrAADTK0AA0yvAANMsAADTO8AA0zxAANM8wADTPUAA0z4AANM+QADTPoAA0z7AANM/AADTP4AA00AAANNAQADTQIAA00EAANNBQADTUQAA01GAANNSAADTUoAA01NAANNTgADTU8AA01QAANNUQADTVMAA01VAANNVgADTVcAA01ZAANNWgADTZkAA02bAANNnQADTZ8AA02iAANNowADTaQAA02lAANNpgADTagAA02qAANNqwADTawAA02uAANNrwADTfoAA04dAANOPQADTl0AA05fAANOYQADTmMAA05lAANOaAADTmkAA05qAANObQADTm4AA05wAANOcQADTnMAA052AANOdwADTngAA057AANOfAADToUAA06SAANOlwADTpkAA06bAANOoAADTqMAA06mAANOqAADTs0AA07xAANPGAADTzwAA08/AANPQQADT0MAA09FAANPRwADT0kAA09KAANPTQADT1oAA09rAANPbQADT28AA09xAANPcwADT3UAA093AANPeQADT3sAA0+MAANPjwADT5IAA0+VAANPmAADT5sAA0+eAANPoQADT6QAA0+mAANP5QADT+cAA0/pAANP6wADT+4AA0/vAANP8AADT/EAA0/yAANP9AADT/YAA0/3AANP+AADT/oAA0/7AANQOgADUDwAA1A+AANQQAADUEMAA1BEAANQRQADUEYAA1BHAANQSQADUEsAA1BMAANQTQADUE8AA1BQAANQjwADUJEAA1CUAANQlgADUJkAA1CaAANQmwADUJwAA1CdAANQnwADUKEAA1CiAANQowADUKUAA1CmAANQswADULQAA1C1AANQtwADUPYAA1D4AANQ+gADUPwAA1D/AANRAAADUQEAA1ECAANRAwADUQUAA1EHAANRCAADUQkAA1ELAANRDAADUUsAA1FNAANRTwADUVEAA1FUAANRVQADUVYAA1FXAANRWAADUVoAA1FcAANRXQADUV4AA1FgAANRYQADUaAAA1GiAANRpAADUaYAA1GpAANRqgADUasAA1GsAANRrQADUa8AA1GxAANRsgADUbMAA1G1AANRtgADUfUAA1H3AANR+QADUfsAA1H+AANR/wADUgAAA1IBAANSAgADUgQAA1IGAANSBwADUggAA1IKAANSCwADUkoAA1JMAANSTgADUlAAA1JTAANSVAADUlUAA1JWAANSVwADUlkAA1JbAANSXAADUl0AA1JfAANSYAADUoUAA1KpAANS0AADUvQAA1L3AANS+QADUvsAA1L9AANS/wADUwEAA1MCAANTBQADUxIAA1MhAANTIwADUyUAA1MnAANTKQADUysAA1MtAANTLwADUz4AA1NBAANTRAADU0cAA1NKAANTTQADU1AAA1NTAANTVQADU5QAA1OWAANTmAADU5oAA1OdAANTngADU58AA1OgAANToQADU6MAA1OlAANTpgADU6cAA1OpAANTqgADU+kAA1PrAANT7QADU+8AA1PyAANT8wADU/QAA1P1AANT9gADU/gAA1P6AANT+wADU/wAA1P+AANT/wADVD4AA1RAAANUQgADVEQAA1RHAANUSAADVEkAA1RKAANUSwADVE0AA1RPAANUUAADVFEAA1RTAANUVAADVJMAA1SVAANUlwADVJkAA1ScAANUnQADVJ4AA1SfAANUoAADVKIAA1SkAANUpQADVKYAA1SoAANUqQADVOgAA1TqAANU7AADVO4AA1TxAANU8gADVPMAA1T0AANU9QADVPcAA1T5AANU+gADVPsAA1T9AANU/gADVT0AA1U/AANVQQADVUMAA1VGAANVRwADVUgAA1VJAANVSgADVUwAA1VOAANVTwADVVAAA1VSAANVUwADVZIAA1WUAANVlgADVZgAA1WbAANVnAADVZ0AA1WeAANVnwADVaEAA1WjAANVpAADVaUAA1WnAANVqAADVbEAA1WyAANVtAADVfcAA1YbAANWPwADVmIAA1aJAANWqQADVtAAA1b3AANXFwADVzsAA1dfAANXYQADV2QAA1dmAANXaAADV2oAA1dtAANXcAADV3IAA1d0AANXdwADV3kAA1d7AANXfgADV4EAA1eCAANXiwADV5gAA1ebAANXnQADV6AAA1ejAANXpQADV8oAA1fuAANYFQADWDkAA1g8AANYPgADWEAAA1hCAANYRAADWEYAA1hHAANYSgADWFcAA1hqAANYbAADWG4AA1hwAANYcgADWHQAA1h2AANYeAADWHoAA1h8AANYjwADWJIAA1iVAANYmAADWJsAA1ieAANYoQADWKQAA1inAANYqgADWKwAA1jrAANY7QADWPAAA1jyAANY9QADWPYAA1j3AANY+AADWPkAA1j7AANY/QADWP4AA1j/AANZAQADWQIAA1kLAANZDAADWQ4AA1lNAANZTwADWVEAA1lTAANZVgADWVcAA1lYAANZWQADWVoAA1lcAANZXgADWV8AA1lgAANZYgADWWMAA1miAANZpAADWacAA1mpAANZrAADWa0AA1muAANZrwADWbAAA1myAANZtAADWbUAA1m2AANZuAADWbkAA1nCAANZwwADWcUAA1oEAANaBgADWggAA1oKAANaDQADWg4AA1oPAANaEAADWhEAA1oTAANaFQADWhYAA1oXAANaGQADWhoAA1pZAANaWwADWl4AA1pgAANaYwADWmQAA1plAANaZgADWmcAA1ppAANaawADWmwAA1ptAANabwADWnAAA1p5AANaegADWnwAA1q7AANavQADWr8AA1rBAANaxAADWsUAA1rGAANaxwADWsgAA1rKAANazAADWs0AA1rOAANa0AADWtEAA1sQAANbEgADWxUAA1sXAANbGgADWxsAA1scAANbHQADWx4AA1sgAANbIgADWyMAA1skAANbJgADWycAA1s0AANbNQADWzYAA1s4AANbdwADW3kAA1t7AANbfQADW4AAA1uBAANbggADW4MAA1uEAANbhgADW4gAA1uJAANbigADW4wAA1uNAANbzAADW84AA1vQAANb0gADW9UAA1vWAANb1wADW9gAA1vZAANb2wADW90AA1veAANb3wADW+EAA1viAANb7wADW/YAA1v5AANb+wADW/0AA1wEAANcBwADXAoAA1wNAANcDwADXFoAA1x9AANcnQADXL0AA1y/AANcwQADXMMAA1zFAANcyAADXMkAA1zKAANczQADXM4AA1zQAANc0QADXNMAA1zWAANc1wADXNgAA1zbAANc3AADXOEAA1zuAANc8wADXPUAA1z3AANc/AADXP8AA10CAANdBAADXSkAA11NAANddAADXZgAA12bAANdnQADXZ8AA12hAANdowADXaUAA12mAANdqQADXbYAA13HAANdyQADXcsAA13NAANdzwADXdEAA13TAANd1QADXdcAA13oAANd6wADXe4AA13xAANd9AADXfcAA136AANd/QADXgAAA14CAANeQQADXkMAA15FAANeRwADXkoAA15LAANeTAADXk0AA15OAANeUAADXlIAA15TAANeVAADXlYAA15XAANelgADXpgAA16aAANenAADXp8AA16gAANeoQADXqIAA16jAANepQADXqcAA16oAANeqQADXqsAA16sAANe6wADXu0AA17wAANe8gADXvUAA172AANe9wADXvgAA175AANe+wADXv0AA17+AANe/wADXwEAA18CAANfDwADXxAAA18RAANfEwADX1IAA19UAANfVgADX1gAA19bAANfXAADX10AA19eAANfXwADX2EAA19jAANfZAADX2UAA19nAANfaAADX6cAA1+pAANfqwADX60AA1+wAANfsQADX7IAA1+zAANftAADX7YAA1+4AANfuQADX7oAA1+8AANfvQADX/wAA1/+AANgAAADYAIAA2AFAANgBgADYAcAA2AIAANgCQADYAsAA2ANAANgDgADYA8AA2ARAANgEgADYFEAA2BTAANgVQADYFcAA2BaAANgWwADYFwAA2BdAANgXgADYGAAA2BiAANgYwADYGQAA2BmAANgZwADYKYAA2CoAANgqgADYKwAA2CvAANgsAADYLEAA2CyAANgswADYLUAA2C3AANguAADYLkAA2C7AANgvAADYOEAA2EFAANhLAADYVAAA2FTAANhVQADYVcAA2FZAANhWwADYV0AA2FeAANhYQADYW4AA2F9AANhfwADYYEAA2GDAANhhQADYYcAA2GJAANhiwADYZoAA2GdAANhoAADYaMAA2GmAANhqQADYawAA2GvAANhsQADYfAAA2HyAANh9AADYfYAA2H5AANh+gADYfsAA2H8AANh/QADYf8AA2IBAANiAgADYgMAA2IFAANiBgADYkUAA2JHAANiSQADYksAA2JOAANiTwADYlAAA2JRAANiUgADYlQAA2JWAANiVwADYlgAA2JaAANiWwADYpoAA2KcAANingADYqAAA2KjAANipAADYqUAA2KmAANipwADYqkAA2KrAANirAADYq0AA2KvAANisAADYu8AA2LxAANi8wADYvUAA2L4AANi+QADYvoAA2L7AANi/AADYv4AA2MAAANjAQADYwIAA2MEAANjBQADY0QAA2NGAANjSAADY0oAA2NNAANjTgADY08AA2NQAANjUQADY1MAA2NVAANjVgADY1cAA2NZAANjWgADY5kAA2ObAANjnQADY58AA2OiAANjowADY6QAA2OlAANjpgADY6gAA2OqAANjqwADY6wAA2OuAANjrwADY+4AA2PwAANj8gADY/QAA2P3AANj+AADY/kAA2P6AANj+wADY/0AA2P/AANkAAADZAEAA2QDAANkBAADZE8AA2RyAANkkgADZLIAA2S0AANktgADZLgAA2S6AANkvQADZL4AA2S/AANkwgADZMMAA2TFAANkxgADZMgAA2TKAANkywADZMwAA2TPAANk0AADZNUAA2TiAANk5wADZOkAA2TrAANk8AADZPMAA2T2AANk+AADZR0AA2VBAANlaAADZYwAA2WPAANlkQADZZMAA2WVAANllwADZZkAA2WaAANlnQADZaoAA2W7AANlvQADZb8AA2XBAANlwwADZcUAA2XHAANlyQADZcsAA2XcAANl3wADZeIAA2XlAANl6AADZesAA2XuAANl8QADZfQAA2X2AANmNQADZjcAA2Y5AANmOwADZj4AA2Y/AANmQAADZkEAA2ZCAANmRAADZkYAA2ZHAANmSAADZkoAA2ZLAANmigADZowAA2aOAANmkAADZpMAA2aUAANmlQADZpYAA2aXAANmmQADZpsAA2acAANmnQADZp8AA2agAANm3wADZuEAA2bkAANm5gADZukAA2bqAANm6wADZuwAA2btAANm7wADZvEAA2byAANm8wADZvUAA2b2AANnAwADZwQAA2cFAANnBwADZ0YAA2dIAANnSgADZ0wAA2dPAANnUAADZ1EAA2dSAANnUwADZ1UAA2dXAANnWAADZ1kAA2dbAANnXAADZ5sAA2edAANnnwADZ6EAA2ekAANnpQADZ6YAA2enAANnqAADZ6oAA2esAANnrQADZ64AA2ewAANnsQADZ/AAA2fyAANn9AADZ/YAA2f5AANn+gADZ/sAA2f8AANn/QADZ/8AA2gBAANoAgADaAMAA2gFAANoBgADaEUAA2hHAANoSQADaEsAA2hOAANoTwADaFAAA2hRAANoUgADaFQAA2hWAANoVwADaFgAA2haAANoWwADaJoAA2icAANongADaKAAA2ijAANopAADaKUAA2imAANopwADaKkAA2irAANorAADaK0AA2ivAANosAADaNUAA2j5AANpIAADaUQAA2lHAANpSQADaUsAA2lNAANpTwADaVEAA2lSAANpVQADaWIAA2lxAANpcwADaXUAA2l3AANpeQADaXsAA2l9AANpfwADaY4AA2mRAANplAADaZcAA2maAANpnQADaaAAA2mjAANppQADaeQAA2nmAANp6AADaeoAA2ntAANp7gADae8AA2nwAANp8QADafMAA2n1AANp9gADafcAA2n5AANp+gADajkAA2o7AANqPQADaj8AA2pCAANqQwADakQAA2pFAANqRgADakgAA2pKAANqSwADakwAA2pOAANqTwADao4AA2qQAANqkgADapQAA2qXAANqmAADapkAA2qaAANqmwADap0AA2qfAANqoAADaqEAA2qjAANqpAADauMAA2rlAANq5wADaukAA2rsAANq7QADau4AA2rvAANq8AADavIAA2r0AANq9QADavYAA2r4AANq+QADazgAA2s6AANrPAADaz4AA2tBAANrQgADa0MAA2tEAANrRQADa0cAA2tJAANrSgADa0sAA2tNAANrTgADa40AA2uPAANrkQADa5MAA2uWAANrlwADa5gAA2uZAANrmgADa5wAA2ueAANrnwADa6AAA2uiAANrowADa+IAA2vkAANr5gADa+gAA2vrAANr7AADa+0AA2vuAANr7wADa/EAA2vzAANr9AADa/UAA2v3AANr+AADbEMAA2xmAANshgADbKYAA2yoAANsqgADbKwAA2yuAANssQADbLIAA2yzAANstgADbLcAA2y5AANsugADbLwAA2y+AANsvwADbMAAA2zDAANsxAADbMkAA2zWAANs2wADbN0AA2zfAANs5AADbOcAA2zqAANs7AADbREAA201AANtXAADbYAAA22DAANthQADbYcAA22JAANtiwADbY0AA22OAANtkQADbZ4AA22vAANtsQADbbMAA221AANttwADbbkAA227AANtvQADbb8AA23QAANt0wADbdYAA23ZAANt3AADbd8AA23iAANt5QADbegAA23qAANuKQADbisAA24tAANuLwADbjIAA24zAANuNAADbjUAA242AANuOAADbjoAA247AANuPAADbj4AA24/AANufgADboAAA26CAANuhAADbocAA26IAANuiQADbooAA26LAANujQADbo8AA26QAANukQADbpMAA26UAANu0wADbtUAA27YAANu2gADbt0AA27eAANu3wADbuAAA27hAANu4wADbuUAA27mAANu5wADbukAA27qAANu9wADbvgAA275AANu+wADbzoAA288AANvPgADb0AAA29DAANvRAADb0UAA29GAANvRwADb0kAA29LAANvTAADb00AA29PAANvUAADb48AA2+RAANvkwADb5UAA2+YAANvmQADb5oAA2+bAANvnAADb54AA2+gAANvoQADb6IAA2+kAANvpQADb+QAA2/mAANv6AADb+oAA2/tAANv7gADb+8AA2/wAANv8QADb/MAA2/1AANv9gADb/cAA2/5AANv+gADcDkAA3A7AANwPQADcD8AA3BCAANwQwADcEQAA3BFAANwRgADcEgAA3BKAANwSwADcEwAA3BOAANwTwADcI4AA3CQAANwkgADcJQAA3CXAANwmAADcJkAA3CaAANwmwADcJ0AA3CfAANwoAADcKEAA3CjAANwpAADcMkAA3DtAANxFAADcTgAA3E7AANxPQADcT8AA3FBAANxQwADcUUAA3FGAANxSQADcVYAA3FlAANxZwADcWkAA3FrAANxbQADcW8AA3FxAANxcwADcYIAA3GFAANxiAADcYsAA3GOAANxkQADcZQAA3GXAANxmQADcdgAA3HaAANx3AADcd4AA3HhAANx4gADceMAA3HkAANx5QADcecAA3HpAANx6gADcesAA3HtAANx7gADci0AA3IvAANyMQADcjMAA3I2AANyNwADcjgAA3I5AANyOgADcjwAA3I+AANyPwADckAAA3JCAANyQwADcoIAA3KEAANyhgADcogAA3KLAANyjAADco0AA3KOAANyjwADcpEAA3KTAANylAADcpUAA3KXAANymAADctcAA3LZAANy2wADct0AA3LgAANy4QADcuIAA3LjAANy5AADcuYAA3LoAANy6QADcuoAA3LsAANy7QADcywAA3MuAANzMAADczIAA3M1AANzNgADczcAA3M4AANzOQADczsAA3M9AANzPgADcz8AA3NBAANzQgADc4EAA3ODAANzhQADc4cAA3OKAANziwADc4wAA3ONAANzjgADc5AAA3OSAANzkwADc5QAA3OWAANzlwADc9YAA3PYAANz2gADc9wAA3PfAANz4AADc+EAA3PiAANz4wADc+UAA3PnAANz6AADc+kAA3PrAANz7AADc/UAA3P2AANz+AADdDsAA3RfAAN0gwADdKYAA3TNAAN07QADdRQAA3U7AAN1WwADdX8AA3WjAAN1pQADdagAA3WqAAN1rAADda4AA3WxAAN1tAADdbYAA3W4AAN1uwADdb0AA3W/AAN1wgADdcUAA3XGAAN1ywADddgAA3XbAAN13QADdeAAA3XjAAN15QADdgoAA3YuAAN2VQADdnkAA3Z8AAN2fgADdoAAA3aCAAN2hAADdoYAA3aHAAN2igADdpcAA3aqAAN2rAADdq4AA3awAAN2sgADdrQAA3a2AAN2uAADdroAA3a8AAN2zwADdtIAA3bVAAN22AADdtsAA3beAAN24QADduQAA3bnAAN26gADduwAA3crAAN3LQADdzAAA3cyAAN3NQADdzYAA3c3AAN3OAADdzkAA3c7AAN3PQADdz4AA3c/AAN3QQADd0IAA3dLAAN3TAADd04AA3eNAAN3jwADd5EAA3eTAAN3lgADd5cAA3eYAAN3mQADd5oAA3ecAAN3ngADd58AA3egAAN3ogADd6MAA3fiAAN35AADd+cAA3fpAAN37AADd+0AA3fuAAN37wADd/AAA3fyAAN39AADd/UAA3f2AAN3+AADd/kAA3gCAAN4AwADeAUAA3hEAAN4RgADeEgAA3hKAAN4TQADeE4AA3hPAAN4UAADeFEAA3hTAAN4VQADeFYAA3hXAAN4WQADeFoAA3iZAAN4mwADeJ4AA3igAAN4owADeKQAA3ilAAN4pgADeKcAA3ipAAN4qwADeKwAA3itAAN4rwADeLAAA3i5AAN4ugADeLwAA3j7AAN4/QADeP8AA3kBAAN5BAADeQUAA3kGAAN5BwADeQgAA3kKAAN5DAADeQ0AA3kOAAN5EAADeREAA3lQAAN5UgADeVUAA3lXAAN5WgADeVsAA3lcAAN5XQADeV4AA3lgAAN5YgADeWMAA3lkAAN5ZgADeWcAA3l0AAN5dQADeXYAA3l4AAN5twADebkAA3m7AAN5vQADecAAA3nBAAN5wgADecMAA3nEAAN5xgADecgAA3nJAAN5ygADecwAA3nNAAN6DAADeg4AA3oRAAN6EwADehYAA3oXAAN6GAADehkAA3oaAAN6HAADeh4AA3ofAAN6IAADeiIAA3ojAAN6MgADej8AA3pGAAN6SQADekwAA3pPAAN6VgADelkAA3pcAAN6XwADemEAA3poAAN6cgADer0AA3rgAAN7AAADeyAAA3siAAN7JAADeyYAA3soAAN7KwADeywAA3stAAN7MAADezEAA3szAAN7NAADezYAA3s5AAN7OgADezsAA3s+AAN7PwADe0QAA3tRAAN7VgADe1gAA3taAAN7XwADe2IAA3tlAAN7ZwADe4wAA3uwAAN71wADe/sAA3v+AAN8AAADfAIAA3wEAAN8BgADfAgAA3wJAAN8DAADfBkAA3wqAAN8LAADfC4AA3wwAAN8MgADfDQAA3w2AAN8OAADfDoAA3xLAAN8TgADfFEAA3xUAAN8VwADfFoAA3xdAAN8YAADfGMAA3xlAAN8pAADfKYAA3yoAAN8qgADfK0AA3yuAAN8rwADfLAAA3yxAAN8swADfLUAA3y2AAN8twADfLkAA3y6AAN8+QADfPsAA3z9AAN8/wADfQIAA30DAAN9BAADfQUAA30GAAN9CAADfQoAA30LAAN9DAADfQ4AA30PAAN9TgADfVAAA31TAAN9VQADfVgAA31ZAAN9WgADfVsAA31cAAN9XgADfWAAA31hAAN9YgADfWQAA31lAAN9cgADfXMAA310AAN9dgADfbUAA323AAN9uQADfbsAA32+AAN9vwADfcAAA33BAAN9wgADfcQAA33GAAN9xwADfcgAA33KAAN9ywADfgoAA34MAAN+DgADfhAAA34TAAN+FAADfhUAA34WAAN+FwADfhkAA34bAAN+HAADfh0AA34fAAN+IAADfl8AA35hAAN+YwADfmUAA35oAAN+aQADfmoAA35rAAN+bAADfm4AA35wAAN+cQADfnIAA350AAN+dQADfrQAA362AAN+uAADfroAA369AAN+vgADfr8AA37AAAN+wQADfsMAA37FAAN+xgADfscAA37JAAN+ygADfwkAA38LAAN/DQADfw8AA38SAAN/EwADfxQAA38VAAN/FgADfxgAA38aAAN/GwADfxwAA38eAAN/HwADf0QAA39oAAN/jwADf7MAA3+2AAN/uAADf7oAA3+8AAN/vgADf8AAA3/BAAN/xAADf9EAA3/gAAN/4gA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DixYAA4sXAAOLGgADixsAA4sdAAOLHgADiyAAA4sjAAOLJAADiyUAA4soAAOLKQADiy4AA4s7AAOLQAADi0IAA4tEAAOLSQADi0wAA4tPAAOLUQADi3YAA4uaAAOLwQADi+UAA4voAAOL6gADi+wAA4vuAAOL8AADi/IAA4vzAAOL9gADjAMAA4wUAAOMFgADjBgAA4waAAOMHAADjB4AA4wgAAOMIgADjCQAA4w1AAOMOAADjDsAA4w+AAOMQQADjEQAA4xHAAOMSgADjE0AA4xPAAOMjgADjJAAA4ySAAOMlAADjJcAA4yYAAOMmQADjJoAA4ybAAOMnQADjJ8AA4ygAAOMoQADjKMAA4ykAAOM4wADjOUAA4znAAOM6QADjOwAA4ztAAOM7gADjO8AA4zwAAOM8gADjPQAA4z1AAOM9gADjPgAA4z5AAONOAADjToAA409AAONPwADjUIAA41DAAONRAADjUUAA41GAAONSAADjUoAA41LAAONTAADjU4AA41PAAONXAADjV0AA41eAAONYAADjZ8AA42hAAONowADjaUAA42oAAONqQADjaoAA42rAAONrAADja4AA42wAAONsQADjbIAA420AAONtQADjfQAA432AAON+AADjfoAA439AAON/gADjf8AA44AAAOOAQADjgMAA44FAAOOBgADjgcAA44JAAOOCgADjkkAA45LAAOOTQADjk8AA45SAAOOUwADjlQAA45VAAOOVgADjlgAA45aAAOOWwADjlwAA45eAAOOXwADjp4AA46gAAOOogADjqQAA46nAAOOqAADjqkAA46qAAOOqwADjq0AA46vAAOOsAADjrEAA46zAAOOtAADjvMAA471AAOO9wADjvkAA478AAOO/QADjv4AA47/AAOPAAADjwIAA48EAAOPBQADjwYAA48IAAOPCQADjy4AA49SAAOPeQADj50AA4+gAAOPogADj6QAA4+mAAOPqAADj6oAA4+rAAOPrgADj7sAA4/KAAOPzAADj84AA4/QAAOP0gADj9QAA4/WAAOP2AADj+cAA4/qAAOP7QADj/AAA4/zAAOP9gADj/kAA4/8AAOP/gADkD0AA5A/AAOQQQADkEMAA5BGAAOQRwADkEgAA5BJAAOQSgADkEwAA5BOAAOQTwADkFAAA5BSAAOQUwADkJIAA5CUAAOQlgADkJgAA5CbAAOQnAADkJ0AA5CeAAOQnwADkKEAA5CjAAOQpAADkKUAA5CnAAOQqAADkOcAA5DpAAOQ6wADkO0AA5DwAAOQ8QADkPIAA5DzAAOQ9AADkPYAA5D4AAOQ+QADkPoAA5D8AAOQ/QADkTwAA5E+AAORQAADkUIAA5FFAAORRgADkUcAA5FIAAORSQADkUsAA5FNAAORTgADkU8AA5FRAAORUgADkZEAA5GTAAORlQADkZcAA5GaAAORmwADkZwAA5GdAAORngADkaAAA5GiAAORowADkaQAA5GmAAORpwADkeYAA5HoAAOR6gADkewAA5HvAAOR8AADkfEAA5HyAAOR8wADkfUAA5H3AAOR+AADkfkAA5H7AAOR/AADkjsAA5I9AAOSPwADkkEAA5JEAAOSRQADkkYAA5JHAAOSSAADkkoAA5JMAAOSTQADkk4AA5JQAAOSUQADkloAA5JbAAOSXQADkqAAA5LEAAOS6AADkwsAA5MyAAOTUgADk3kAA5OgAAOTwAADk+QAA5QIAAOUCgADlA0AA5QPAAOUEQADlBMAA5QWAAOUGQADlBsAA5QdAAOUIAADlCIAA5QkAAOUJwADlCoAA5QrAAOUNAADlEEAA5REAAOURgADlEkAA5RMAAOUTgADlHMAA5SXAAOUvgADlOIAA5TlAAOU5wADlOkAA5TrAAOU7QADlO8AA5TwAAOU8wADlQAAA5UTAAOVFQADlRcAA5UZAAOVGwADlR0AA5UfAAOVIQADlSMAA5UlAAOVOAADlTsAA5U+AAOVQQADlUQAA5VHAAOVSgADlU0AA5VQAAOVUwADlVUAA5WUAAOVlgADlZkAA5WbAAOVngADlZ8AA5WgAAOVoQADlaIAA5WkAAOVpgADlacAA5WoAAOVqgADlasAA5W0AAOVtQADlbcAA5X2AAOV+AADlfoAA5X8AAOV/wADlgAAA5YBAAOWAgADlgMAA5YFAAOWBwADlggAA5YJAAOWCwADlgwAA5ZLAAOWTQADllAAA5ZSAAOWVQADllYAA5ZXAAOWWAADllkAA5ZbAAOWXQADll4AA5ZfAAOWYQADlmIAA5ZrAAOWbAADlm4AA5atAAOWrwADlrEAA5azAAOWtgADlrcAA5a4AAOWuQADlroAA5a8AAOWvgADlr8AA5bAAAOWwgADlsMAA5cCAAOXBAADlwcAA5cJAAOXDAADlw0AA5cOAAOXDwADlxAAA5cSAAOXFAADlxUAA5cWAAOXGAADlxkAA5ciAAOXIwADlyUAA5dkAAOXZgADl2gAA5dqAAOXbQADl24AA5dvAAOXcAADl3EAA5dzAAOXdQADl3YAA5d3AAOXeQADl3oAA5e5AAOXuwADl74AA5fAAAOXwwADl8QAA5fFAAOXxgADl8cAA5fJAAOXywADl8wAA5fNAAOXzwADl9AAA5fdAAOX3gADl98AA5fhAAOYIAADmCIAA5gkAAOYJgADmCkAA5gqAAOYKwADmCwAA5gtAAOYLwADmDEAA5gyAAOYMwADmDUAA5g2AAOYdQADmHcAA5h6AAOYfAADmH8AA5iAAAOYgQADmIIAA5iDAAOYhQADmIcAA5iIAAOYiQADmIsAA5iMAAOYoQADmK4AA5izAAOYtgADmLkAA5i+AAOYwQADmMQAA5jGAAOYywADmRYAA5k5AAOZWQADmXkAA5l7AAOZfQADmX8AA5mBAAOZhAADmYUAA5mGAAOZiQADmYoAA5mMAAOZjQADmY8AA5mSAAOZkwADmZQAA5mXAAOZmAADmaEAA5muAAOZswADmbUAA5m3AAOZvAADmb8AA5nCAAOZxAADmekAA5oNAAOaNAADmlgAA5pbAAOaXQADml8AA5phAAOaYwADmmUAA5pmAAOaaQADmnYAA5qHAAOaiQADmosAA5qNAAOajwADmpEAA5qTAAOalQADmpcAA5qoAAOaqwADmq4AA5qxAAOatAADmrcAA5q6AAOavQADmsAAA5rCAAObAQADmwMAA5sFAAObBwADmwoAA5sLAAObDAADmw0AA5sOAAObEAADmxIAA5sTAAObFAADmxYAA5sXAAObVgADm1gAA5taAAObXAADm18AA5tgAAObYQADm2IAA5tjAAObZQADm2cAA5toAAObaQADm2sAA5tsAAObqwADm60AA5uwAAObsgADm7UAA5u2AAObtwADm7gAA5u5AAObuwADm70AA5u+AAObvwADm8EAA5vCAAObzwADm9AAA5vRAAOb0wADnBIAA5wUAAOcFgADnBgAA5wbAAOcHAADnB0AA5weAAOcHwADnCEAA5wjAAOcJAADnCUAA5wnAAOcKAADnGcAA5xpAAOcawADnG0AA5xwAAOccQADnHIAA5xzAAOcdAADnHYAA5x4AAOceQADnHoAA5x8AAOcfQADnLwAA5y+AAOcwAADnMIAA5zFAAOcxgADnMcAA5zIAAOcyQADnMsAA5zNAAOczgADnM8AA5zRAAOc0gADnREAA50TAAOdFQADnRcAA50aAAOdGwADnRwAA50dAAOdHgADnSAAA50iAAOdIwADnSQAA50mAAOdJwADnWYAA51oAAOdagADnWwAA51vAAOdcAADnXEAA51yAAOdcwADnXUAA513AAOdeAADnXkAA517AAOdfAADnaEAA53FAAOd7AADnhAAA54TAAOeFQADnhcAA54ZAAOeGwADnh0AA54eAAOeIQADni4AA549AAOePwADnkEAA55DAAOeRQADnkcAA55JAAOeSwADnloAA55dAAOeYAADnmMAA55mAAOeaQADnmwAA55vAAOecQADnrAAA56yAAOetAADnrYAA565AAOeugADnrsAA568AAOevQADnr8AA57BAAOewgADnsMAA57FAAOexgADnwUAA58HAAOfCQADnwsAA58OAAOfDwADnxAAA58RAAOfEgADnxQAA58WAAOfFwADnxgAA58aAAOfGwADn1oAA59cAAOfXgADn2AAA59jAAOfZAADn2UAA59mAAOfZwADn2kAA59rAAOfbAADn20AA59vAAOfcAADn68AA5+xAAOftAADn7YAA5+5AAOfugADn7sAA5+8AAOfvQADn78AA5/BAAOfwgADn8MAA5/FAAOfxgADoAUAA6AHAAOgCQADoAsAA6AOAAOgDwADoBAAA6ARAAOgEgADoBQAA6AWAAOgFwADoBgAA6AaAAOgGwADoFoAA6BcAAOgXgADoGAAA6BjAAOgZAADoGUAA6BmAAOgZwADoGkAA6BrAAOgbAADoG0AA6BvAAOgcAADoK8AA6CxAAOgswADoLUAA6C4AAOguQADoLoAA6C7AAOgvAADoL4AA6DAAAOgwQADoMIAA6DEAAOgxQADoRAAA6EzAAOhUwADoXMAA6F1AAOhdwADoXkAA6F7AAOhfgADoX8AA6GAAAOhgwADoYQAA6GGAAOhhwADoYkAA6GMAAOhjQADoY4AA6GRAAOhkgADoZcAA6GkAAOhqQADoasAA6GtAAOhsgADobUAA6G4AAOhugADod8AA6IDAAOiKgADok4AA6JRAAOiUwADolUAA6JXAAOiWQADolsAA6JcAAOiXwADomwAA6J9AAOifwADooEAA6KDAAOihQADoocAA6KJAAOiiwADoo0AA6KeAAOioQADoqQAA6KnAAOiqgADoq0AA6KwAAOiswADorYAA6K4AAOi9wADovkAA6L7AAOi/QADowAAA6MBAAOjAgADowMAA6MEAAOjBgADowgAA6MJAAOjCgADowwAA6MNAAOjTAADo04AA6NQAAOjUgADo1UAA6NWAAOjVwADo1gAA6NZAAOjWwADo10AA6NeAAOjXwADo2EAA6NiAAOjoQADo6MAA6OmAAOjqAADo6sAA6OsAAOjrQADo64AA6OvAAOjsQADo7MAA6O0AAOjtQADo7cAA6O4AAOjxQADo8YAA6PHAAOjyQADpAgAA6QKAAOkDAADpA4AA6QRAAOkEgADpBMAA6QUAAOkFQADpBcAA6QZAAOkGgADpBsAA6QdAAOkHgADpF0AA6RfAAOkYQADpGMAA6RmAAOkZwADpGgAA6RpAAOkagADpGwAA6RuAAOkbwADpHAAA6RyAAOkcwADpLIAA6S0AAOktgADpLgAA6S7AAOkvAADpL0AA6S+AAOkvwADpMEAA6TDAAOkxAADpMUAA6THAAOkyAADpQcAA6UJAAOlCwADpQ0AA6UQAAOlEQADpRIAA6UTAAOlFAADpRYAA6UYAAOlGQADpRoAA6UcAAOlHQADpVwAA6VeAAOlYAADpWIAA6VlAAOlZgADpWcAA6VoAAOlaQADpWsAA6VtAAOlbgADpW8AA6VxAAOlcgADpZcAA6W7AAOl4gADpgYAA6YJAAOmCwADpg0AA6YPAAOmEQADphMAA6YUAAOmFwADpiQAA6YzAAOmNQADpjcAA6Y5AAOmOwADpj0AA6Y/AAOmQQADplAAA6ZTAAOmVgADplkAA6ZcAAOmXwADpmIAA6ZlAAOmZwADpqYAA6aoAAOmqgADpqwAA6avAAOmsAADprEAA6ayAAOmswADprUAA6a3AAOmuAADprkAA6a7AAOmvAADpvsAA6b9AAOm/wADpwEAA6cEAAOnBQADpwYAA6cHAAOnCAADpwoAA6cMAAOnDQADpw4AA6cQAAOnEQADp1AAA6dSAAOnVAADp1YAA6dZAAOnWgADp1sAA6dcAAOnXQADp18AA6dhAAOnYgADp2MAA6dlAAOnZgADp6UAA6enAAOnqQADp6sAA6euAAOnrwADp7AAA6exAAOnsgADp7QAA6e2AAOntwADp7gAA6e6AAOnuwADp/oAA6f8AAOn/gADqAAAA6gDAAOoBAADqAUAA6gGAAOoBwADqAkAA6gLAAOoDAADqA0AA6gPAAOoEAADqE8AA6hRAAOoUwADqFUAA6hYAAOoWQADqFoAA6hbAAOoXAADqF4AA6hgAAOoYQADqGIAA6hkAAOoZQADqKQAA6imAAOoqAADqKoAA6itAAOorgADqK8AA6iwAAOosQADqLMAA6i1AAOotgADqLcAA6i5AAOougADqMMAA6jEAAOoxgADqNMAA6jUAAOo1QADqNcAA6jkAAOo5QADqOYAA6joAAOo9QADqPYAA6j3AAOo+QADqQIAA6kRAAOpHgADqS0AA6k/AAOpUwADqWoAA6l8AAOphQADqYYAA6mIAAOplQADqZYAA6mXAAOpmQADqaIAA6msAAOpswAAAAAAAAQCAAAAAAAAjUQAAAAAAAAAAAAAAAAAA6m7 - + url - \ No newline at end of file + diff --git a/fearless/Common/Storage/MigrationMappings/SubstrateV3toV4.xcmappingmodel/xcmapping.xml b/fearless/Common/Storage/MigrationMappings/SubstrateV3toV4.xcmappingmodel/xcmapping.xml index 201ab37810..7f4bf53aa4 100644 --- a/fearless/Common/Storage/MigrationMappings/SubstrateV3toV4.xcmappingmodel/xcmapping.xml +++ b/fearless/Common/Storage/MigrationMappings/SubstrateV3toV4.xcmappingmodel/xcmapping.xml @@ -108,7 +108,7 @@ cxIAAYagXxAPTlNLZXllZEFyY2hpdmVy0QAIAAlUcm9vdIABrxEL+wALAAwAGwA3ADgAOQBjAGQAZQBm fearless/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel_v4.xcdatamodel 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WJFYoFivWL5Y01jUWNxY6Fj8WQtZGlkpWS1ZPFlLWVpZaVl4WYRZllmlWbRZw1nSWeFZ8Fn/WhRaFVodWilaPVpMWltaalpuWn1ajFqbWqpauVrFWtda5lr1WwRbE1siWzFbQFtDW11bXltkW3BbhluVW5hbp1u2W7lbyFvXW9pb6Vv4W/xcC1waXBtcKVwqXCtcLFwtXC5cQ1xEXExcWFxsXHtcilyZXJ1crFy7XMpc2VzoXPRdBl0VXSRdM11CXVFdYF1hXXBdhV2GXY5dml2uXb1dzF3bXd9d7l39XgxeG14qXjZeSF5XXmZedV6EXpNeol6jXrJex17IXtBe3F7wXv9fDl8dXyFfMF8/X05fXV9sX3hfil+ZX6hft1/GX9Vf5F/zYAhgCWARYB1gMWBAYE9gXmBiYHFggGCPYJ5grWC5YMlg2GDyYPNg+WEFYRthKmEtYTxhS2FOYV1hbGFvYX5hjWGRYaBhr2GwYbphu2G8YdFh0mHaYeZh+mIJYhhiJ2IrYjpiSWJYYmdidmKCYpRio2KyYsFi0GLfYu5i/WMSYxNjG2MnYztjSmNZY2hjbGN7Y4pjmWOoY7djw2PVY+Rj82QCZBFkIGQvZD5kU2RUZFxkaGR8ZItkmmSpZK1kvGTLZNpk6WT4ZQRlFmUlZTRlQ2VSZVNlYmVxZYBlg2WSZaFlsGW/Zc5l42XkZexl+GYMZhtmKmY5Zj1mTGZbZmpmeWaIZpRmpma1ZsRm02biZvFnAGcPZxJnLGctZzNnP2dVZ2RnZ2d2Z4VniGeXZ6ZnqWe4Z8dny2faZ+ln6mfyZ/Nn9GgJaApoEmgeaDJoQWhQaF9oY2hyaIFokGifaK5oumjMaNto6mj5aQhpF2kmaTVpSmlLaVNpX2lzaYJpkWmgaaRps2nCadFp4GnvaftqDWocaitqOmpJalhqZ2p2anlqk2qUappqpmq8astqzmrdauxq72r+aw1rEGsfay5rMmtBa1BrUWtfa2BrYWtia3dreGuAa4xroGuva75rzWvRa+Br72v+bA1sHGwobDpsSWxYbGdsdmyFbJRso2y4bLlswWzNbOFs8Gz/bQ5tEm0hbTBtP21ObV1taW17bYptmW2obbdtxm3VbeRt+W36bgJuDm4ibjFuQG5PblNuYm5xboBuj26ebqpuvG7Lbtpu6W74bwdvFm8lbzpvO29Db09vY29yb4FvkG+Ub6Nvsm/Bb9Bv32/rb/1wDHAbcCpwOXBIcFdwZnB7cHxwhHCQcKRws3DCcNFw1XDkcPNxAnERcSBxLHE+cU1xXHFrcXpxiXGYcadxqnHEccVxy3HXce1x/HH/cg5yHXIgci9yPnJBclByX3JjcnJygXKCco5yj3KQcqVypnKucrpyznLdcuxy+3L/cw5zHXMscztzSnNWc2hzd3OGc5VzpHOzc8Jz0XPmc+dz73P7dA90HnQtdDx0QHRPdF50bXR8dIt0l3SpdLh0x3TWdOV09HUDdRJ1J3UodTB1PHVQdV91bnV9dYF1kHWfda51vXXMddh16nX5dgh2F3YmdjV2RHZTdmh2aXZxdn12kXagdq92vnbCdtF24Hbvdv53DXcZdyt3OndJd1h3Z3d2d4V3lHeXd7F3sne4d8R32nfpd+x3+3gKeA14HHgreC54PXhMeFB4X3hueG94iXiKeIt4jHiNeI54j3iQeJF4kniTeKh4qXixeL140XjgeO94/nkCeRF5IHkveT55TXlZeWt5enmJeZh5p3m2ecV51Hnpeep58nn+ehJ6IXowej96Q3pSemF6cHp/eo56mnqsert6ynrZeuh693sGexV7KnsrezN7P3tTe2J7cXuAe4R7k3uie7F7wHvPe9t77Xv8fAt8GnwpfDh8R3xWfGt8bHx0fIB8lHyjfLJ8wXzFfNR843zyfQF9EH0cfS59PX1MfVt9an15fYh9l32sfa19tX3BfdV95H3zfgJ+Bn4VfiR+M35CflF+XX5vfn5+jX6cfqt+un7Jfth+7X7ufvZ/An8WfyV/NH9Df0d/Vn9lf3R/g3+Sf55/sH+/f85/3X/sf/uACoAZgC6AL4A3gEOAV4BmgHWAhICIgJeApoC1gMSA04DfgPGBAIEPgR6BLYE8gUuBWoFvgXCBeIGEgZiBp4G2gcWByYHYgeeB9oIFghSCIIIygkGCUIJfgm6CfYKMgpuCsIKxgrmCxYLZguiC94MGgwqDGYMogzeDRoNVg2GDc4OCg5GDoIOvg76DzYPcg/GD8oP6hAaEGoQphDiER4RLhFqEaYR4hIeEloSihLSEw4TShOGE8IT/hQ6FHYUyhTOFO4VHhVuFaoV5hYiFjIWbhaqFuYXIhdeF44X1hgSGE4YihjGGQIZPhl6GYYZ7hnyGgoaOhqSGs4a2hsWG1IbXhuaG9Yb4hweHFocahymHOIdCh1eHWIdgh2yHgIePh56HrYexh8CHz4feh+2H/IgIiBqIKYg4iEeIVohliHSIg4iYiJmIoYitiMGI0IjfiO6I8okBiRCJH4kuiT2JSYlbiWqJeYmIiZeJpom1icSJ2YnaieKJ7ooCihGKIIovijOKQopRimCKb4p+ioqKnIqrirqKyYrYiueK9osFiwiLIosjiymLNYtLi1qLXYtsi3uLfouNi5yLn4uui72LwYvQi9+L4Ivqi+uL7IwBjAKMCowWjCqMOYxIjFeMW4xqjHmMiIyXjKaMsozEjNOM4ozxjQCND40ejS2NQo1DjUuNV41rjXqNiY2YjZyNq426jcmN2I3njfOOBY4UjiOOMo5BjlCOX45ujoOOhI6MjpiOrI67jsqO2Y7djuyO+48KjxmPKI80j0aPVY9kj3OPgo+Rj6CPr4+yj8yPzY/Tj9+P9ZAEkAeQFpAlkCiQN5BGkEmQWJBnkGuQepCJkIqQkpCTkKiQqZCxkL2Q0ZDgkO+Q/pECkRGRIJEvkT6RTZFZkWuRepGJkZiRp5G2kcWR1JHpkeqR8pH+khKSIZIwkj+SQ5JSkmGScJJ/ko6SmpKskruSypLZkuiS95MGkxWTGJMckyCTJJMsky+TM1UkbnVsbNcADQAOAA8AEAARABIAEwAUABUAFgAXABgAFwAaXxAPX3hkX3Jvb3RQYWNrYWdlViRjbGFzc1xfeGRfY29tbWVudHNfEBBfeGRfbW9kZWxNYW5hZ2VyXxAVX2NvbmZpZ3VyYXRpb25zQnlOYW1lXV94ZF9tb2RlbE5hbWVfEBdfbW9kZWxWZXJzaW9uSWRlbnRpZmllcoACgQtvgQttgACBC26AAIEEGt4AHAAdAB4AHwAgACEAIgAOACMAJAAlACYAJwAoACkAKgArAAkAKQAXAC8AMAAxADIAMwApACkAF18QHFhEQnVja2V0Rm9yQ2xhc3Nlc3dhc0VuY29kZWRfEBpYREJ1Y2tldEZvclBhY2thZ2Vzc3RvcmFnZV8QHFhEQnVja2V0Rm9ySW50ZXJmYWNlc3N0b3JhZ2VfEA9feGRfb3duaW5nTW9kZWxfEB1YREJ1Y2tldEZvclBhY2thZ2Vzd2FzRW5jb2RlZFZfb3duZXJfEBtYREJ1Y2tldEZvckRhdGFUeXBlc3N0b3JhZ2VbX3Zpc2liaWxpdHlfEBlYREJ1Y2tldEZvckNsYXNzZXNzdG9yYWdlVV9uYW1lXxAfWERCdWNrZXRGb3JJbnRlcmZhY2Vzd2FzRW5jb2RlZF8QHlhEQnVja2V0Rm9yRGF0YVR5cGVzd2FzRW5jb2RlZF8QEF91bmlxdWVFbGVtZW50SUSABIELa4ELaYABgASAAIELaoELbBAAgAWAA4AEgASAAFBTWUVT0wA6ADsADgA8AE4AYFdOUy5rZXlzWk5TLm9iamVjdHOvEBEAPQA+AD8AQABBAEIAQwBEAEUARgBHAEgASQBKAEsATABNgAaAB4AIgAmACoALgAyADYAOgA+AEIARgBKAE4AUgBWAFq8QEQBPAFAAUQBSAFMAVABVAFYAVwBYAFkAWgBbAFwAXQBeAF+AF4BqgQZTgQG4gQaZgQcmgQTwgQgRgQhWgQeXgQjhgQPEgQlUgQVhgQpwgQrJgQslgDVdQ0RFeHRlcm5hbEFwaVdDRENoYWluXUNEU2luZ2xlVmFsdWVXQ0RBc3NldF1DRENvbnRhY3RJdGVtXxAZQ0RQb2xrYXN3YXBSZW1vdGVTZXR0aW5nc18QEENEQ2hhaW5YY21Db25maWdbQ0RTdGFzaEl0ZW1fEBVDRFJ1bnRpbWVNZXRhZGF0YUl0ZW1eQ0RQb2xrYXN3YXBEZXhaQ0RTY2FtSW5mb1tDRENoYWluTm9kZV8QGENEVHJhbnNhY3Rpb25IaXN0b3J5SXRlbV8QGUNEWGNtQXZhaWxhYmxlRGVzdGluYXRpb25ZQ0RDb250YWN0XkNEUGhpc2hpbmdJdGVtXxASQ0RDaGFpblN0b3JhZ2VJdGVt3xAQAHMAdAB1AHYAIQB3AHgAIwB5AHoADgAlAHsAfAAoAH0AfgB/ACkAKQAUAIMAhAAxACkAfgCHAD0AfgCKAIsAjF8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzb3JkZXJlZF8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzc3RvcmFnZV8QIFhEQnVja2V0Rm9yU3RlcmVvdHlwZXN3YXNFbmNvZGVkXxAkWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXN3YXNFbmNvZGVkXxAdWERCdWNrZXRGb3JTdGVyZW90eXBlc3N0b3JhZ2VfECRYREJ1Y2tldEZvckdlbmVyYWxpemF0aW9uc2R1cGxpY2F0ZXNfECRYREJ1Y2tldEZvckdlbmVyYWxpemF0aW9uc3dhc0VuY29kZWRfEB1YREJ1Y2tldEZvclN0ZXJlb3R5cGVzb3JkZXJlZF8QIVhEQnVja2V0Rm9yR2VuZXJhbGl6YXRpb25zb3JkZXJlZF8QIVhEQnVja2V0Rm9yR2VuZXJhbGl6YXRpb25zc3RvcmFnZVtfaXNBYnN0cmFjdIAZgD2ABIAEgAKAGoEDS4AEgBmBA02ABoAZgQZSgBgIEmGLcH1Xb3JkZXJlZNMAOgA7AA4AkACSAGChAJGAG6EAk4AcgDVeWERfUFN0ZXJlb3R5cGXZACEAJQCXAA4AKACYACMAfQCZAE8AkQB+AJ0AFwApADEAjAChXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNvcmRlcmVkXxAkWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXN3YXNFbmNvZGVkXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNzdG9yYWdlgBeAG4AZgDyAAIAECIAd0wA6ADsADgCjAK0AYKkApAClAKYApwCoAKkAqgCrAKyAHoAfgCCAIYAigCOAJIAlgCapAK4ArwCwALEAsgCzALQAtQC2gCeAK4AsgC6AL4AxgDOANoA6gDVfEBNYRFBNQ29tcG91bmRJbmRleGVzXxAQWERfUFNLX2VsZW1lbnRJRF8QGVhEUE1VbmlxdWVuZXNzQ29uc3RyYWludHNfEBpYRF9QU0tfdmVyc2lvbkhhc2hNb2RpZmllcl8QGVhEX1BTS19mZXRjaFJlcXVlc3RzQXJyYXlfEBFYRF9QU0tfaXNBYnN0cmFjdF8QD1hEX1BTS191c2VySW5mb18QE1hEX1BTS19jbGFzc01hcHBpbmdfEBZYRF9QU0tfZW50aXR5Q2xhc3NOYW1l3xAPAMIAwwDEACEAxQDGAMcAIwDIAA4AJQDJAMoAKADLABcAzQAXAJMAjACMAIwAMQCMANQApACMAIwAFwCMVV90eXBlWF9kZWZhdWx0XF9hc3NvY2lhdGlvbltfaXNSZWFkT25seVlfaXNTdGF0aWNZX2lzVW5pcXVlWl9pc0Rlcml2ZWRaX2lzT3JkZXJlZFxfaXNDb21wb3NpdGVXX2lzTGVhZoAAgCiAAIAcCAgICIAqgB4ICIAACNIAOwAOANsA3KCAKdIA3gDfAOAA4VokY2xhc3NuYW1lWCRjbGFzc2VzXk5TTXV0YWJsZUFycmF5owDgAOIA41dOU0FycmF5WE5TT2JqZWN00gDeAN8A5QDmXxAQWERVTUxQcm9wZXJ0eUltcKQA5wDoAOkA418QEFhEVU1MUHJvcGVydHlJbXBfEBRYRFVNTE5hbWVkRWxlbWVudEltcF8QD1hEVU1MRWxlbWVudEltcN8QDwDCAMMAxAAhAMUAxgDHACMAyAAOACUAyQDKACgAywAXABcAFwCTAIwAjACMADEAjADUAKUAjACMABcAjIAAgACAAIAcCAgICIAqgB8ICIAACN8QDwDCAMMAxAAhAMUAxgDHACMAyAAOACUAyQDKACgAywAXAPsAFwCTAIwAjACMADEAjADUAKYAjACMABcAjIAAgC2AAIAcCAgICIAqgCAICIAACNIAOwAOAQkA3KCAKd8QDwDCAMMAxAAhAMUAxgDHACMAyAAOACUAyQDKACgAywAXABcAFwCTAIwAjACMADEAjADUAKcAjACMABcAjIAAgACAAIAcCAgICIAqgCEICIAACN8QDwDCAMMAxAAhAMUAxgDHACMAyAAOACUAyQDKACgAywAXARwAFwCTAIwAjACMADEAjADUAKgAjACMABcAjIAAgDCAAIAcCAgICIAqgCIICIAACNIAOwAOASoA3KCAKd8QDwDCAMMAxAAhAMUAxgDHACMAyAAOACUAyQDKACgAywAXAS4AFwCTAIwAjACMADEAjADUAKkAjACMABcAjIAAgDKAAIAcCAgICIAqgCMICIAACAjfEA8AwgDDAMQAIQDFAMYAxwAjAMgADgAlAMkAygAoAMsAFwE+ABcAkwCMAIwAjAAxAIwA1ACqAIwAjAAXAIyAAIA0gACAHAgICAiAKoAkCAiAAAjTADoAOwAOAUwBTQBgoKCANdIA3gDfAVABUV8QE05TTXV0YWJsZURpY3Rpb25hcnmjAVABUgDjXE5TRGljdGlvbmFyed8QDwDCAMMAxAAhAMUAxgDHACMAyAAOACUAyQDKACgAywAXAVUAFwCTAIwAjACMADEAjADUAKsAjACMABcAjIAAgDeAAIAcCAgICIAqgCUICIAACNYAJQAOACgAfQAhACMBYwFkABcAjAAXADGAOIA5gAAIgABfEBRYREdlbmVyaWNSZWNvcmRDbGFzc9IA3gDfAWoBa11YRFVNTENsYXNzSW1wpgFsAW0BbgFvAXAA411YRFVNTENsYXNzSW1wXxASWERVTUxDbGFzc2lmaWVySW1wXxARWERVTUxOYW1lc3BhY2VJbXBfEBRYRFVNTE5hbWVkRWxlbWVudEltcF8QD1hEVU1MRWxlbWVudEltcN8QDwDCAMMAxAAhAMUAxgDHACMAyAAOACUAyQDKACgAywAXAXMAFwCTAIwAjACMADEAjADUAKwAjACMABcAjIAAgDuAAIAcCAgICIAqgCYICIAACF1DREV4dGVybmFsQXBp0gDeAN8BggGDXxASWERVTUxTdGVyZW90eXBlSW1wpwGEAYUBhgGHAYgBiQDjXxASWERVTUxTdGVyZW90eXBlSW1wXVhEVU1MQ2xhc3NJbXBfEBJYRFVNTENsYXNzaWZpZXJJbXBfEBFYRFVNTE5hbWVzcGFjZUltcF8QFFhEVU1MTmFtZWRFbGVtZW50SW1wXxAPWERVTUxFbGVtZW50SW1w0wA6ADsADgGLAZAAYKQBjAGNAY4Bj4A+gD+AQIBBpAGRAZIBkwGUgEKBBgyBBiSBBjuANVVjaGFpblV0eXBlc1R0eXBlU3VybN8QEgDCAMMAxAGbACEAxgDHAZwAIwDFAZ0AyAAOACUAyQDKACgAywAXABcAFwApAE8AjACMAaUAMQCMAH4AjAGpAYwAjACMAa0AjF8QIFhEQnVja2V0Rm9yU3RlcmVvdHlwZXN3YXNFbmNvZGVkXxAdWERCdWNrZXRGb3JTdGVyZW90eXBlc3N0b3JhZ2VfEB1YREJ1Y2tldEZvclN0ZXJlb3R5cGVzb3JkZXJlZIAAgACAAIAEgBcICIBECIAZCICvgD4ICIBDCBL4hOVq0wA6ADsADgGxAbQAYKIBsgGzgEWARqIBtQG2gEeAW4A1XxASWERfUFByb3BTdGVyZW90eXBlXxAQWERfUFJfU3RlcmVvdHlwZdkAIQAlAbsADgAoAbwAIwB9Ab0BkQGyAH4AnQAXACkAMQCMAcVfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc29yZGVyZWRfECRYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3dhc0VuY29kZWRfECFYREJ1Y2tl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Y2tldEZvck93bmVkQXR0cmlidXRlc29yZGVyZWRfECRYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3dhc0VuY29kZWRfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3N0b3JhZ2WBAaeARoAZgDyAAIAECIEBttMAOgA7AA4SoxKqAGCmAnACcgJtAm4CbwJxgGCAYoBdgF6AX4BhphKrEqwSrRKuEq8SsIEBt4EDToEDT4EDUIEDUYEDUoA13xAPAMIAwwDEACEAxQDGAMcAIwDIAA4AJQDJAMoAKADLABcAUgAXEfgAjACMAIwAMQCMANQCcACMAIwAFwCMgACBAbiAAIEBtQgICAiAKoBgCAiAAAjfEBASwhLDEsQSxQAhEsYSxwAjEsgSyQAOACUSyhLLACgAfQB+Es0AKQApABQS0QCEADEAKQB+AIcAQAB+EtgS2QCMXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNvcmRlcmVkXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNzdG9yYWdlXxAgWERCdWNrZXRGb3JTdGVyZW90eXBlc3dhc0VuY29kZWRfECRYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3dhc0VuY29kZWRfEB1YREJ1Y2tldEZvclN0ZXJlb3R5cGVzc3RvcmFnZV8QJFhEQnVja2V0Rm9yR2VuZXJhbGl6YXRpb25zZHVwbGljYXRlc18QJFhEQnVja2V0Rm9yR2VuZXJhbGl6YXRpb25zd2FzRW5jb2RlZF8QHVhEQnVja2V0Rm9yU3RlcmVvdHlwZXNvcmRlcmVkXxAhWERCdWNrZXRGb3JHZW5lcmFsaXphdGlvbnNvcmRlcmVkXxAhWERCdWNrZXRGb3JHZW5lcmFsaXphdGlvbnNzdG9yYWdlgBmBAcqABIAEgAKBAbqBA0uABIAZgQNNgAmAGYEDTIEBuQgSSbQAydMAOgA7AA4S3RLfAGChAJGAG6ES4IEBu4A12QAhACUS4wAOACgS5AAjAH0S5QBSAJEAfgCdABcAKQAxAIwS7V8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzb3JkZXJlZF8QJFhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzd2FzRW5jb2RlZF8QIVhEQnVja2V0Rm9yT3du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ADEAjADUAnAAjACMABcAjIAAgQVhgACBBVsICAgIgCqAYAgIgAAI3xAQQ/lD+kP7Q/wAIUP9Q/4AI0P/RAAADgAlRAFEAgAoAH0AfkQEACkAKQAURAgAhAAxACkAfgCHAEoAfkQPRBAAjF8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzb3JkZXJlZF8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzc3RvcmFnZV8QIFhEQnVja2V0Rm9yU3RlcmVvdHlwZXN3YXNFbmNvZGVkXxAkWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXN3YXNFbmNvZGVkXxAdWERCdWNrZXRGb3JTdGVyZW90eXBlc3N0b3JhZ2VfECRYREJ1Y2tldEZvckdlbmVyYWxpemF0aW9uc2R1cGxpY2F0ZXNfECRYREJ1Y2tldEZvckdlbmVyYWxpemF0aW9uc3dhc0VuY29kZWRfEB1YREJ1Y2tldEZvclN0ZXJlb3R5cGVzb3JkZXJlZF8QIVhEQnVja2V0Rm9yR2VuZXJhbGl6YXRpb25zb3JkZXJlZF8QIVhEQnVja2V0Rm9yR2VuZXJhbGl6YXRpb25zc3RvcmFnZYAZgQV0gASABIACgQVjgQNLgASAGYEDTYATgBmBBbyBBWIIEjaNuqbTADoAOwAORBREFgBgoQCRgBuhRBeBBWSANdkAIQAlRBoADgAoRBsAIwB9RBwAXACRAH4AnQAXACkAMQCMRCRfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc29yZGVyZWRfECRYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3dhc0VuY29kZWRfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3N0b3JhZ2WBBWGAG4AZgDyAAIAECIEFZdMAOgA7AA5EJkQwAGCpAKQApQCmAKcAqACpAKoAqwCsgB6AH4AggCGAIoAjgCSAJYAmqUQxRDJEM0Q0RDVENkQ3RDhEOYEFZoEFaIEFaYEFa4EFbIEFboEFb4EFcYEFcoA13xAPAMIAwwDEACEAxQDGAMcAIwDIAA4AJQDJAMoAKADLABdEPQAXRBcAjACMAIwAMQCMANQApACMAIwAFwCMgACBBWeAAIEFZAgICAiAKoAeCAiAAAjSADsADkRLANyggCnfEA8AwgDD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cmlidXRlc3dhc0VuY29kZWRfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3N0b3JhZ2WBB4aARoAZgDyAAIAECIEHldMAOgA7AA5gumDBAGCmAnACcgJtAm4CbwJxgGCAYoBdgF6AX4BhpmDCYMNgxGDFYMZgx4EHloEH9IEH9YEH9oEH94EH+IA13xAPAMIAwwDEACEAxQDGAMcAIwDIAA4AJQDJAMoAKADLABcAWAAXYA8AjACMAIwAMQCMANQCcACMAIwAFwCMgACBB5eAAIEHlAgICAiAKoBgCAiAAAjfEBBg2WDaYNtg3AAhYN1g3gAjYN9g4AAOACVg4WDiACgAfQB+YOQAKQApABRg6ACEADEAKQB+AIcARgB+YO9g8ACMXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNvcmRlcmVkXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNzdG9yYWdlXxAgWERCdWNrZXRGb3JTdGVyZW90eXBlc3dhc0VuY29kZWRfECRYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3dhc0VuY29kZWRfEB1YREJ1Y2tldEZvclN0ZXJlb3R5cGVzc3RvcmFnZV8QJFhEQnVja2V0Rm9yR2VuZXJhbGl6YXRpb25zZHVwbGljYXRlc18QJFhEQnVja2V0Rm9yR2VuZXJhbGl6YXRpb25zd2FzRW5jb2RlZF8QHVhEQnVja2V0Rm9yU3RlcmVvdHlwZXNvcmRlcmVkXxAhWERCdWNrZXRGb3JHZW5lcmFsaXphdGlvbnNvcmRlcmVkXxAhWERCdWNrZXRGb3JHZW5lcmFsaXphdGlvbnNzdG9yYWdlgBmBB6qABIAEgAKBB5mBA0uABIAZgQNNgA+AGYEH84EHmAgSRteJf9MAOgA7AA5g9GD2AGChAJGAG6Fg94EHmoA12QAhACVg+gAOAChg+wAjAH1g/ABYAJEAfgCdABcAKQAxAIxhBF8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzb3JkZXJlZF8QJFhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzd2FzRW5jb2RlZF8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzc3RvcmFnZYEHl4AbgBmAPIAAgAQIgQeb0wA6ADsADmEGYRAAYKkA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AAAnDAAAJw4AACcbAAAnLAAAJy4AACcwAAAnMgAAJzQAACc2AAAnOAAAJzoAACc8AAAnTQAAJ08AACdRAAAnUwAAJ1UAACdXAAAnWQAAJ1sAACddAAAnXwAAJ30AACebAAAnrgAAJ8IAACfXAAAn9AAAKAgAACgeAAAoXQAAKF8AAChhAAAoYwAAKGUAAChmAAAoZwAAKGgAAChpAAAoawAAKG0AAChuAAAobwAAKHEAAChyAAAosQAAKLMAACi1AAAotwAAKLkAACi6AAAouwAAKLwAACi9AAAovwAAKMEAACjCAAAowwAAKMUAACjGAAApBQAAKQcAACkJAAApCwAAKQ0AACkOAAApDwAAKRAAACkRAAApEwAAKRUAACkWAAApFwAAKRkAACkaAAApJwAAKSgAACkpAAApKwAAKWoAAClsAAApbgAAKXAAAClyAAApcwAAKXQAACl1AAApdgAAKXgAACl6AAApewAAKXwAACl+AAApfwAAKb4AACnAAAApwgAAKcQAACnGAAApxwAAKcgAACnJAAApygAAKcwAACnOAAApzwAAKdAAACnSAAAp0wAAKdQAACoTAAAqFQAAKhcAACoZAAAqGwAAKhwAACodAAAqHgAAKh8AACohAAAqIwAAKiQAAColAAAqJwAAKigAACpnAAAqaQAAKmsAACptAAAqbwAAKnAAACpxAAAqcgAAKnMAACp1AAAqdwAAKngAACp5AAAqewAAKnwAACq7AAAqvQAAKr8AACrBAAAqwwAAKsQAACrFAAAqxgAAKscAACrJAAAqywAAKswAACrNAAAqzwAAKtAAACr1AAArGQAAK0AAACtkAAArZgAAK2gAACtqAAArbAAAK24AACtwAAArcQAAK3MAACuAAAArjwAAK5EAACuTAAArlQAAK5cAACuZAAArmwAAK50AACusAAArrgAAK7AAACuyAAArtAAAK7cAACu6AAArvQAAK78AACvRAAAr5QAAK/cAACwMAAAsHgAALC0AACxKAAAsiQAALIsAACyNAAAsjwAALJEAACySAAAskwAALJQAACyVAAAslwAALJkAACyaAAAsmwAALJ0AACyeAAAs3QAALN8AACzhAAAs4wAALOUAACzmAAAs5wAALOgAACzpAAAs6wAALO0AACzuAAAs7wAALPEAACzyAAAs9AAALTMAAC01AAAtNwAALTkAAC07AAAtPAAALT0AAC0+AAAtPwAALUEAAC1DAAAtRAAALUUAAC1HAAAtSAAALYcAAC2JAAAtiwAALY0AAC2PAAAtkAAALZEAAC2SAAAtkwAALZUAAC2XAAAtmAAALZkAAC2bAAAtnAAALd8AAC4DAAAuJwAALkoAAC5xAAAukQAALrgAAC7fAAAu/wAALyMAAC9HAAAvSQAAL0sAAC9NAAAvTwAAL1EAAC9TAAAvVgAAL1gAAC9aAAAvXQAAL18AAC9hAAAvZAAAL2YAAC9nAAAvbAAAL3kAAC98AAAvfgAAL4EAAC+DAAAvhQAAL6oAAC/OAAAv9QAAMBkAADAbAAAwHQAAMB8AADAhAAAwIwAAMCUAADAmAAAwKAAAMDUAADBIAAAwSgAAMEwAADBOAAAwUAAAMFIAADBUAAAwVgAAMFgAADBaAAAwbQAAMG8AADBxAAAwcwAAMHUAADB3AAAweQAAMHsAADB9AAAwfwAAMIEAADDAAAAwwgAAMMQAADDGAAAwyAAAMMkAADDKAAAwywAAMMwAADDOAAAw0AAAMNEAADDSAAAw1AAAMNUAADDeAAAw3wAAMOEAADEgAAAxIgAAMSQAADEmAAAxKAAAMSkAADEqAAAxKwAAMSwAADEuAAAxMAAAMTEAADEyAAAxNAAAMTUAADF0AAAxdgAAMXgAADF6AAAxfAAAMX0AADF+AAAxfwAAMYAAADGCAAAxhAAAMYUAADGGAAAxiAAAMYkAADGSAAAxlQAAMZcAADGZAAAxogAAMaUAADGnAAAxqQAAMbEAADHwAAAx8gAAMfQAADH2AAAx+AAAMfkAADH6AAAx+wAAMfwAADH+AAAyAAAAMgEAADICAAAyBAAAMgUAADJEAAAyRgAAMkgAADJKAAAyTAAAMk0AADJOAAAyTwAAMlAAADJSAAAyVAAAMlUAADJWAAAyWAAAMlkAADJiAAAyYwAAMmUAADKkAAAypgAAMqgAADKqAAAyrAAAMq0AADKuAAAyrwAAMrAAADKyAAAytAAAMrUAADK2AAAyuAAAMrkAADL4AAAy+gAAMvwAADL+AAAzAAAAMwEAADMCAAAzAwAAMwQAADMGAAAzCAAAMwkAADMKAAAzDAAAMw0AADMaAAAzGwAAMxwAADMeAAAzXQAAM18AADNhAAAzYwAAM2UAADNmAAAzZwAAM2gAADNpAAAzawAAM20AADNuAAAzbwAAM3EAADNyAAAzsQAAM7MAADO1AAAztwAAM7kAADO6AAAzuwAAM7wAADO9AAAzvwAAM8EAADPCAAAzwwAAM8UAADPGAAAz0wAANAwAADQOAAA0EAAANBIAADQUAAA0FgAANBgAADQaAAA0HAAANB4AADQgAAA0IgAANCQAADQmAAA0KAAANCoAADQsAAA0LgAANDAAADQyAAA0NAAANDYAADQ4AAA0OgAANDwAADQ+AAA0QAAANEIAADR7AAA0fQAANH8AADSBAAA0gwAANIYAADSJAAA0jAAANI8AADSSAAA0lQAANJgAADSbAAA0ngAANKEAADSkAAA0pwAANKoAADStAAA0sAAANLMAADS2AAA0uQAANLwAADS/AAA0wgAANMUAADTIAAA0ygAANNQAADTiAAA08AAANPgAADUGAAA1CwAANRoAADUoAAA1LQAANUEAADVIAAA1XgAANWgAADV3AAA1fQAANYoAADWTAAA1mgAANaMAADW3AAA1ygAANdQAADXmAAA19AAANgAAADZLAAA2bgAANo4AADauAAA2sAAANrIAADa0AAA2tgAANrgAADa5AAA2ugAANrwAADa9AAA2vwAANsAAADbCAAA2xAAANsUAADbGAAA2yAAANskAADbOAAA22wAANuAAADbiAAA25AAANukAADbrAAA27QAANu8AADcUAAA3OAAAN18AADeDAAA3hQAAN4cAADeJAAA3iwAAN40AADePAAA3kAAAN5IAADefAAA3sAAAN7IAADe0AAA3tgAAN7gAADe6AAA3vAAAN74AADfAAAA30QAAN9MAADfVAAA31wAAN9kAADfbAAA33QAAN98AADfhAAA34wAAOCIAADgkAAA4JgAAOCgAADgqAAA4KwAAOCwAADgtAAA4LgAAODAAADgyAAA4MwAAODQAADg2AAA4NwAAOHYAADh4AAA4egAAOHwAADh+AAA4fwAAOIAAADiBAAA4ggAAOIQAADiGAAA4hwAAOIgAADiKAAA4iwAAOMoAADjMAAA4zgAAONAAADjSAAA40wAAONQAADjVAAA41gAAONgAADjaAAA42wAAONwAADjeAAA43wAAOOwAADjtAAA47gAAOPAAADkvAAA5MQAAOTMAADk1AAA5NwAAOTgAADk5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AABFFwAARVYAAEVYAABFWgAARVwAAEVeAABFXwAARWAAAEVhAABFYgAARWQAAEVmAABFZwAARWgAAEVqAABFawAARaoAAEWsAABFrgAARbAAAEWyAABFswAARbQAAEW1AABFtgAARbgAAEW6AABFuwAARbwAAEW+AABFvwAARf4AAEYAAABGAgAARgQAAEYGAABGBwAARggAAEYJAABGCgAARgwAAEYOAABGDwAARhAAAEYSAABGEwAARhUAAEZUAABGVgAARlgAAEZaAABGXAAARl0AAEZeAABGXwAARmAAAEZiAABGZAAARmUAAEZmAABGaAAARmkAAEaoAABGqgAARqwAAEauAABGsAAARrEAAEayAABGswAARrQAAEa2AABGuAAARrkAAEa6AABGvAAARr0AAEb8AABG/gAARwAAAEcCAABHBAAARwUAAEcGAABHBwAARwgAAEcKAABHDAAARw0AAEcOAABHEAAARxEAAEcaAABHKAAARzUAAEdDAABHUAAAR2MAAEd6AABHjAAAR9cAAEf6AABIGgAASDoAAEg8AABIPgAASEAAAEhCAABIRAAASEUAAEhGAABISAAASEkAAEhLAABITAAASE4AAEhQAABIUQAASFIAAEhUAABIVQAASFoAAEhnAABIbAAASG4AAEhwAABIdQAASHcAAEh5AABIewAASKAAAEjEAABI6wAASQ8AAEkRAABJEwAASRUAAEkXAABJGQAASRsAAEkcAABJHgAASSsAAEk8AABJPgAASUAAAElCAABJRAAASUYAAElIAABJSgAASUwAAEldAABJXwAASWEAAEljAABJZQAASWcAAElpAABJawAASW0AAElvAABJrgAASbAAAEmyAABJtAAASbYAAEm3AABJuAAASbkAAEm6AABJvAAASb4AAEm/AABJwAAAScIAAEnDAABKAgAASgQAAEoGAABKCAAASgoAAEoLAABKDAAASg0AAEoOAABKEAAAShIAAEoTAABKFAAAShYAAEoXAABKVgAASlgAAEpaAABKXAAASl4AAEpfAABKYAAASmEAAEpiAABKZAAASmYAAEpnAABKaAAASmoAAEprAABKeAAASnkAAEp6AABKfAAASrsAAEq9AABKvwAASsEAAErDAABKxAAASsUAAErGAABKxwAASskAAErLAABKzAAASs0AAErPAABK0AAASw8AAEsRAABLEwAASxUAAEsXAABLGAAASxkAAEsaAABLGwAASx0AAEsfAABLIAAASyEAAEsjAABLJAAAS2MAAEtlAABLZwAAS2kAAEtrAABLbAAAS20AAEtuAABLbwAAS3EAAEtzAABLdAAAS3UAAEt3AABLeAAAS7cAAEu5AABLuwAAS70AAEu/AABLwAAAS8EAAEvCAABLwwAAS8UAAEvHAABLyAAAS8kAAEvLAABLzAAATAsAAEwNAABMDwAATBEAAEwTAABMFAAATBUAAEwWAABMFwAATBkAAEwbAABMHAAATB0AAEwfAABMIAAATEUAAExpAABMkAAATLQAAEy2AABMuAAATLoAAEy8AABMvgAATMAAAEzBAABMwwAATNAAAEzfAABM4QAATOMAAEzlAABM5wAATOkAAEzrAABM7QAATPwAAEz+AABNAAAATQIAAE0EAABNBgAATQgAAE0KAABNDAAATUsAAE1NAABNTwAATVEAAE1TAABNVAAATVUAAE1WAABNVwAATVkAAE1bAABNXAAATV0AAE1fAABNYAAATZ8AAE2hAABNowAATaUAAE2nAABNqAAATakAAE2qAABNqwAATa0AAE2vAABNsAAATbEAAE2zAABNtAAATfMAAE31AABN9wAATfkAAE37AABN/AAATf0AAE3+AABN/wAATgEAAE4DAABOBAAATgUAAE4HAABOCAAATkcAAE5JAABOSwAATk0AAE5PAABOUAAATlEAAE5SAABOUwAATlUAAE5XAABOWAAATlkAAE5bAABOXAAATl8AAE6eAABOoAAATqIAAE6kAABOpgAATqcAAE6oAABOqQAATqoAAE6sAABOrgAATq8AAE6wAABOsgAATrMAAE7yAABO9AAATvYAAE74AABO+gAATvsAAE78AABO/QAATv4AAE8AAABPAgAATwMAAE8EAABPBgAATwcAAE9GAABPSAAAT0oAAE9MAABPTgAAT08AAE9QAABPUQAAT1IAAE9UAABPVgAAT1cAAE9YAABPWgAAT1sAAE+mAABPyQAAT+kAAFAJAABQCwAAUA0AAFAPAABQEQAAUBMAAFAUAABQFQAAUBcAAFAYAABQGgAAUBsAAFAdAABQHwAAUCAAAFAhAABQIwAAUCQAAFAtAABQOgAAUD8AAFBBAABQQwAAUEgAAFBK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AACIAgAAiAUAAIgIAACICgAAiC8AAIhTAACIegAAiJ4AAIihAACIowAAiKUAAIinAACIqQAAiKsAAIisAACIrwAAiLwAAIjNAACIzwAAiNEAAIjTAACI1QAAiNcAAIjZAACI2wAAiN0AAIjuAACI8QAAiPQAAIj3AACI+gAAiP0AAIkAAACJAwAAiQYAAIkIAACJRwAAiUkAAIlLAACJTQAAiVAAAIlRAACJUgAAiVMAAIlUAACJVgAAiVgAAIlZAACJWgAAiVwAAIldAACJnAAAiZ4AAImgAACJogAAiaUAAImmAACJpwAAiagAAImpAACJqwAAia0AAImuAACJrwAAibEAAImyAACJ8QAAifMAAIn2AACJ+AAAifsAAIn8AACJ/QAAif4AAIn/AACKAQAAigMAAIoEAACKBQAAigcAAIoIAACKFQAAihYAAIoXAACKGQAAilgAAIpaAACKXAAAil4AAIphAACKYgAAimMAAIpkAACKZQAAimcAAIppAACKagAAimsAAIptAACKbgAAiq0AAIqvAACKsQAAirMAAIq2AACKtwAAirgAAIq5AACKugAAirwAAIq+AACKvwAAisAAAIrCAACKwwAAiwIAAIsEAACLBgAAiwgAAIsLAACLDAAAiw0AAIsOAACLDwAAixEAAIsTAACLFAAAixUAAIsXAACLGAAAi1cAAItZAACLWwAAi10AAItgAACLYQAAi2IAAItjAACLZAAAi2YAAItoAACLaQAAi2oAAItsAACLbQAAi6wAAIuuAACLsAAAi7IAAIu1AACLtgAAi7cAAIu4AACLuQAAi7sAAIu9AACLvgAAi78AAIvBAACLwgAAi+cAAIwLAACMMgAAjFYAAIxZAACMWwAAjF0AAIxfAACMYQAAjGMAAIxkAACMZwAAjHQAAIyDAACMhQAAjIcAAIyJAACMiwAAjI0AAIyPAACMkQAAjKAAAIyjAACMpgAAjKkAAIysAACMrwAAjLIAAIy1AACMtwAAjPYAAIz4AACM+gAAjPwAAIz/AACNAAAAjQEAAI0CAACNAwAAjQUAAI0HAACNCAAAjQkAAI0LAACNDAAAjUsAAI1NAACNTwAAjVEAAI1UAACNVQAAjVYAAI1XAACNWAAAjVoAAI1cAACNXQAAjV4AAI1gAACNYQAAjaAAAI2iAACNpAAAjaYAAI2p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AAClEwAApRQAAKUVAAClGAAApRkAAKUeAAClKwAApTAAAKUyAAClNAAApTkAAKU8AAClPwAApUEAAKVmAACligAApbEAAKXVAACl2AAApdoAAKXcAACl3gAApeAAAKXiAACl4wAApeYAAKXzAACmBAAApgYAAKYIAACmCgAApgwAAKYOAACmEAAAphIAAKYUAACmJQAApigAAKYrAACmLgAApjEAAKY0AACmNwAApjoAAKY9AACmPwAApn4AAKaAAACmggAApoQAAKaHAACmiAAApokAAKaKAACmiwAApo0AAKaPAACmkAAAppEAAKaTAACmlAAAptMAAKbVAACm1wAAptkAAKbcAACm3QAApt4AAKbfAACm4AAApuIAAKbkAACm5QAApuYAAKboAACm6QAApygAAKcqAACnLQAApy8AAKcyAACnMwAApzQAAKc1AACnNgAApzgAAKc6AACnOwAApzwAAKc+AACnPwAAp0wAAKdNAACnTgAAp1AAAKePAACnkQAAp5MAAKeVAACnmAAAp5kAAKeaAACnmwAAp5wAAKeeAACnoAAAp6EAAKeiAACnpAAAp6UAAKfkAACn5gAAp+gAAKfqAACn7QAAp+4AAKfvAACn8AAAp/EAAKfzAACn9QAAp/YAAKf3AACn+QAAp/oAAKg5AACoOwAAqD0AAKg/AACoQgAAqEMAAKhEAACoRQAAqEYAAKhIAACoSgAAqEsAAKhMAACoTgAAqE8AAKiOAACokAAAqJIAAKiUAAColwAAqJgAAKiZAAComgAAqJsAAKidAAConwAAqKAAAKihAACoowAAqKQAAKjjAACo5QAAqOcAAKjpAACo7AAAqO0AAKjuAACo7wAAqPAAAKjyAACo9AAAqPUAAKj2AACo+AAAqPkAAKkeAACpQgAAqWkAAKmNAACpkAAAqZIAAKmUAACplgAAqZgAAKmaAACpmwAAqZ4AAKmrAACpugAAqbwAAKm+AACpwAAAqcIAAKnEAACpxgAAqcgAAKnXAACp2gAAqd0AAKngAACp4wAAqeYAAKnpAACp7AAAqe4AAKotAACqLwAAqjEAAKozAACqNgAAqjcAAKo4AACqOQAAqjoAAKo8AACqPgAAqj8AAKpAAACqQgAAqkMAAKqCAACqhAAAqoYAAKqIAACqiwAAqowAAKqNAACqjgAAqo8AAKqRAACqkwAAqpQAAKqVAACqlwAAqpgAAKrXAACq2QAAqtsAAKrdAACq4AAAquEAAKriAACq4wAAquQAAKrmAACq6AAAqukAAKrqAACq7AAAqu0AAKssAACrLgAAqzAAAKsyAACrNQAAqzYAAKs3AACrOAAAqzkAAKs7AACrPQAAqz4AAKs/AACrQQAAq0IAAKuBAACrgwAAq4UAAKuHAACrigAAq4sAAKuMAACrjQAAq44AAKuQAACrkgAAq5MAAKuUAACrlgAAq5cAAKvWAACr2AAAq9oAAKvcAACr3wAAq+AAAKvhAACr4gAAq+MAAKvlAACr5wAAq+gAAKvpAACr6wAAq+wAAKwrAACsLQAArC8AAKwxAACsNAAArDUAAKw2AACsNwAArDgAAKw6AACsPAAArD0AAKw+AACsQAAArEEAAKyMAACsrwAArM8AAKzvAACs8QAArPMAAKz1AACs9wAArPoAAKz7AACs/AAArP8AAK0AAACtAgAArQMAAK0FAACtCAAArQkAAK0KAACtDQAArQ4AAK0TAACtIAAArSUAAK0nAACtKQAArS4AAK0xAACtNAAArTYAAK1bAACtfwAAraYAAK3KAACtzQAArc8AAK3RAACt0wAArdUAAK3XAACt2AAArdsAAK3oAACt+QAArfsAAK39AACt/wAArgEAAK4DAACuBQAArgcAAK4JAACuGgAArh0AAK4gAACuIwAAriYAAK4pAACuLAAAri8AAK4yAACuNAAArnMAAK51AACudwAArnkAAK58AACufQAArn4AAK5/AACugAAAroIAAK6EAACuhQAAroYAAK6IAACuiQAArsgAAK7KAACuzAAArs4AAK7RAACu0gAArtMAAK7UAACu1QAArtcAAK7ZAACu2gAArtsAAK7dAACu3gAArx0AAK8fAACvIgAAryQAAK8nAACvKAAArykAAK8qAACvKwAAry0AAK8vAACvMAAArzEAAK8zAACvNAAAr0EAAK9CAACvQwAAr0UAAK+EAACvhgAAr4gAAK+KAACvjQAAr44AAK+PAACvkAAAr5EAAK+TAACvlQAAr5YAAK+XAACvmQAAr5oAAK/ZAACv2wAAr90AAK/fAACv4gAAr+MAAK/kAACv5QAAr+YAAK/oAACv6gAAr+sAAK/sAACv7gAAr+8AALAuAACwMAAAsDIAALA0AACwNwAAsDgAALA5AACwOgAAsDsAALA9AACwPwAAsEAAALBBAACwQwAAsEQAALCDAACwhQAAsIcAALCJAACwjAAAsI0AALCOAACwjwAAsJAAALCSAACwlAAAsJUAALCWAACwmAAAsJkAALDYAACw2gAAsNwAALDeAACw4QAAsOIAALDjAACw5AAAsOUAALDnAACw6QAAsOoAALDrAACw7QAAsO4AALETAACxNwAAsV4AALGCAACxhQAAsYcAALGJAACxiwAAsY0AALGPAACxkAAAsZMAALGgAACxrwAAsbEAALGzAACxtQAAsbcAALG5AACxuwAAsb0AALHMAACxzwAAsdIAALHVAACx2AAAsdsAALHeAACx4QAAseMAALIiAACyJAAAsiYAALIoAACyKwAAsiwAALItAACyLgAAsi8AALIxAACyMwAAsjQAALI1AACyNwAAsjgAALJ3AACyeQAAsnsAALJ9AACygAAAsoEAALKCAACygwAAsoQAALKGAACyiAAAsokAALKKAACyjAAAso0AALLMAACyzgAAstAAALLSAACy1QAAstYAALLXAACy2AAAstkAALLbAACy3QAAst4AALLfAACy4QAAsuIAALMhAACzIwAAsyUAALMnAACzKgAAsysAALMsAACzLQAAsy4AALMwAACzMgAAszMAALM0AACzNgAAszcAALN2AACzeAAAs3oAALN8AACzfwAAs4AAALOBAACzggAAs4MAALOFAACzhwAAs4gAALOJAACziwAAs4wAALPLAACzzQAAs88AALPRAACz1AAAs9UAALPWAACz1wAAs9gAALPaAACz3AAAs90AALPeAACz4AAAs+EAALQgAAC0IgAAtCQAALQmAAC0KQAAtCoAALQrAAC0LAAAtC0AALQvAAC0MQAAtDIAALQzAAC0NQAAtDYAALSBAAC0pAAAtMQAALTkAAC05gAAtOgAALTqAAC07AAAtO8AALTwAAC08QAAtPQAALT1AAC09wAAtPgAALT6AAC0/QAAtP4AALT/AAC1AgAAtQMAALUIAAC1FQAAtRoAALUcAAC1HgAAtSMAALUmAAC1KQAAtSsAALVQAAC1dAAAtZsAALW/AAC1wgAAtcQAALXGAAC1yAAAtcoAALXMAAC1zQAAtdAAALXdAAC17gAAtfAAALXyAAC19AAAtfYAALX4AAC1+gAAtfwAALX+AAC2DwAAthIAALYVAAC2GAAAthsAALYeAAC2IQAAtiQAALYnAAC2KQAAtmgAALZqAAC2bAAAtm4AALZxAAC2cgAAtnMAALZ0AAC2dQAAtncAALZ5AAC2egAAtnsAALZ9AAC2fgAAtr0AALa/AAC2wQAAtsMAALbGAAC2xwAAtsgAALbJAAC2ygAAtswAALbOAAC2zwAAttAAALbSAAC20wAAtxIAALcUAAC3FwAAtxkAALccAAC3HQAAtx4AALcfAAC3IAAAtyIAALckAAC3JQAAtyYAALcoAAC3KQAAtzYAALc3AAC3OAAAtzoAALd5AAC3ewAAt30AALd/AAC3ggAAt4MAALeEAAC3hQAAt4YAALeIAAC3igAAt4sAALeMAAC3jgAAt48AALfOAAC30AAAt9IAALfUAAC31wAAt9gAALfZAAC32gAAt9sAALfdAAC33wAAt+AAALfhAAC34wAAt+QAALgjAAC4JQAAuCcAALgpAAC4LAAAuC0AALguAAC4LwAAuDAAALgyAAC4NAAAuDUAALg2AAC4OAAAuDkAALh4AAC4egAAuHwAALh+AAC4gQAAuIIAALiDAAC4hAAAuIUAALiHAAC4iQAAuIoAALiLAAC4jQAAuI4AALjNAAC4zwAAuNEAALjTAAC41gAAuNcAALjYAAC42QAAuNoAALjcAAC43gAAuN8AALjgAAC44gAAuOMAALkIAAC5LAAAuVMAALl3AAC5egAAuXwAALl+AAC5gAAAuYIAALmEAAC5hQAAuYgAALmVAAC5pAAAuaYAALmoAAC5qgAAuawAALmuAAC5sAAAubIAALnBAAC5xAAAuccAALnKAAC5zQAAudAAALnTAAC51gAAudgAALoXAAC6GQAAuhwAALoeAAC6IQAAuiIAALojAAC6JAAAuiUAALonAAC6KQAAuioAALorAAC6LQAAui4AALoyAAC6cQAAunMAALp1AAC6dwAAunoAALp7AAC6fAAAun0AALp+AAC6gAAAuoIAALqDAAC6hAAAuoYAALqHAAC6xgAAusgAALrKAAC6zAAAus8AALrQAAC60QAAutIAALrTAAC61QAAutcAALrYAAC62QAAutsAALrcAAC7GwAAux0AALsgAAC7IgAAuyUAALsmAAC7JwAAuygAALspAAC7KwAAuy0AALsu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AADLywAAzAoAAMwMAADMDgAAzBAAAMwTAADMFAAAzBUAAMwWAADMFwAAzBkAAMwbAADMHAAAzB0AAMwfAADMIAAAzGsAAMyOAADMrgAAzM4AAMzQAADM0gAAzNQAAMzWAADM2QAAzNoAAMzbAADM3gAAzN8AAMzhAADM4gAAzOQAAMznAADM6AAAzOkAAMzsAADM7QAAzPIAAMz/AADNBAAAzQYAAM0IAADNDQAAzRAAAM0TAADNFQAAzToAAM1eAADNhQAAzakAAM2sAADNrgAAzbAAAM2yAADNtAAAzbYAAM23AADNugAAzccAAM3YAADN2gAAzdwAAM3eAADN4AAAzeIAAM3kAADN5gAAzegAAM35AADN/AAAzf8AAM4CAADOBQAAzggAAM4LAADODgAAzhEAAM4TAADOUgAAzlQAAM5WAADOWAAAzlsAAM5cAADOXQAAzl4AAM5fAADOYQAAzmMAAM5kAADOZQAAzmcAAM5oAADOpwAAzqkAAM6rAADOrQAAzrAAAM6xAADOsgAAzrMAAM60AADOtgAAzrgAAM65AADOugAAzrwAAM69AADO/AAAzv4AAM8BAADPAwAAzwYAAM8HAADPCAAAzwkAAM8KAADPDAAAzw4AAM8PAADPEAAAzxIAAM8TAADPIAAAzyEAAM8iAADPJAAAz2MAAM9lAADPZwAAz2kAAM9sAADPbQAAz24AAM9vAADPcAAAz3IAAM90AADPdQAAz3YAAM94AADPeQAAz7gAAM+6AADPvAAAz74AAM/BAADPwgAAz8MAAM/EAADPxQAAz8cAAM/JAADPygAAz8sAAM/NAADPzgAA0A0AANAPAADQEQAA0BMAANAWAADQFwAA0BgAANAZAADQGgAA0BwAANAeAADQHwAA0CAAANAiAADQIwAA0GIAANBkAADQZgAA0GgAANBrAADQbAAA0G0AANBuAADQbwAA0HEAANBzAADQdAAA0HUAANB3AADQeAAA0LcAANC5AADQuwAA0L0AANDAAADQwQAA0MIAANDDAADQxAAA0MYAANDIAADQyQAA0MoAANDMAADQzQAA0PIAANEWAADRPQAA0WEAANFkAADRZgAA0WgAANFqAADRbAAA0W4AANFvAADRcgAA0X8AANGOAADRkAAA0ZIAANGUAADRlgAA0ZgAANGaAADRnAAA0asAANGuAADRsQAA0bQAANG3AADRugAA0b0AANHAAADRwgAA0gEAANIDAADSBQAA0gcAANIKAADSCwAA0gwAANINAADSDgAA0hAAANISAADSEwAA0hQAANIWAADSFwAA0lYAANJYAADSWgAA0lwAANJfAADSYAAA0mEAANJiAADSYwAA0mUAANJnAADSaAAA0mkAANJrAADSbAAA0qsAANKtAADSrwAA0rEAANK0AADStQAA0rYAANK3AADSuAAA0roAANK8AADSvQAA0r4AANLAAADSwQAA0wAAANMCAADTBAAA0wYAANMJAADTCgAA0wsAANMMAADTDQAA0w8AANMRAADTEgAA0xMAANMVAADTFgAA01UAANNXAADTWQAA01sAANNeAADTXwAA02AAANNhAADTYgAA02QAANNmAADTZwAA02gAANNqAADTawAA06oAANOsAADTrgAA07AAANOzAADTtAAA07UAANO2AADTtwAA07kAANO7AADTvAAA070AANO/AADTwAAA0/8AANQBAADUAwAA1AUAANQIAADUCQAA1AoAANQLAADUDAAA1A4AANQQAADUEQAA1BIAANQUAADUFQAA1GAAANSDAADUowAA1MMAANTFAADUxwAA1MkAANTLAADUzgAA1M8AANTQAADU0wAA1NQAANTWAADU1wAA1NkAANTcAADU3QAA1N4AANThAADU4gAA1OcAANT0AADU+QAA1PsAANT9AADVAgAA1QUAANUIAADVCgAA1S8AANVTAADVegAA1Z4AANWhAADVowAA1aUAANWnAADVqQAA1asAANWsAADVrwAA1bwAANXNAADVzwAA1dEAANXTAADV1QAA1dcAANXZAADV2wAA1d0AANXuAADV8QAA1fQAANX3AADV+gAA1f0AANYAAADWAwAA1gYAANYIAADWRwAA1kkAANZLAADWTQAA1lAAANZRAADWUgAA1lMAANZUAADWVgAA1lgAANZZAADWWgAA1lwAANZdAADWnAAA1p4AANagAADWogAA1qUAANamAADWpwAA1qgAANapAADWqwAA1q0AANauAADWrwAA1rEAANayAADW8QAA1vMAANb2AADW+AAA1vsAANb8AADW/QAA1v4AANb/AADXAQAA1wMAANcEAADXBQAA1wcAANcIAADXFQAA1xYAANcXAADXGQAA11gAANdaAADXXAAA114AANdhAADXYgAA12MAANdkAADXZQAA12cAANdpAADXagAA12sAANdtAADXbgAA160AANevAADXsQAA17MAANe2AADXtwAA17gAANe5AADXugAA17wAANe+AADXvwAA18AAANfCAADXwwAA2AIAANgEAADYBgAA2AgAANgLAADYDAAA2A0AANgOAADYDwAA2BEAANgTAADYFAAA2BUAANgXAADYGAAA2FcAANhZAADYWwAA2F0AANhgAADYYQAA2GIAANhjAADYZAAA2GYAANhoAADYaQAA2GoAANhsAADYbQAA2KwAANiuAADYsAAA2LIAANi1AADYtgAA2LcAANi4AADYuQAA2LsAANi9AADYvgAA2L8AANjBAADYwgAA2OcAANkLAADZMgAA2VYAANlZAADZWwAA2V0AANlfAADZYQAA2WMAANlkAADZZwAA2XQAANmDAADZhQAA2YcAANmJAADZiwAA2Y0AANmPAADZkQAA2aAAANmjAADZpgAA2akAANmsAADZrwAA2bIAANm1AADZtwAA2fYAANn4AADZ+gAA2fwAANn/AADaAAAA2gEAANoCAADaAwAA2gUAANoHAADaCAAA2gkAANoLAADaDAAA2ksAANpNAADaTwAA2lEAANpUAADaVQAA2lYAANpXAADaWAAA2loAANpcAADaXQAA2l4AANpgAADaYQAA2qAAANqiAADapAAA2qYAANqpAADaqgAA2qsAANqsAADarQAA2q8AANqxAADasgAA2rMAANq1AADatgAA2vUAANr3AADa+QAA2vsAANr+AADa/wAA2wAAANsBAADbAgAA2wQAANsGAADbBwAA2wgAANsKAADbCwAA20oAANtMAADbTgAA21AAANtTAADbVAAA21UAANtWAADbVwAA21kAANtbAADbXAAA210AANtfAADbYAAA258AANuhAADbowAA26UAANuoAADbqQAA26oAANurAADbrAAA264AANuwAADbsQAA27IAANu0AADbtQAA2/QAANv2AADb+AAA2/oAANv9AADb/gAA2/8AANwAAADcAQAA3AMAANwFAADcBgAA3AcAANwJAADcCgAA3FUAANx4AADcmAAA3LgAANy6AADcvAAA3L4AANzAAADcwwAA3MQAANzFAADcyAAA3MkAANzLAADczAAA3M4AANzRAADc0gAA3NMAANzWAADc1wAA3NwAANzpAADc7gAA3PAAANzyAADc9wAA3PoAANz9AADc/wAA3SQAAN1IAADdbwAA3ZMAAN2WAADdmAAA3ZoAAN2cAADdngAA3aAAAN2hAADdpAAA3bEAAN3CAADdxAAA3cYAAN3IAADdygAA3cwAAN3OAADd0AAA3dIAAN3jAADd5gAA3ekAAN3sAADd7wAA3fIAAN31AADd+AAA3fsAAN39AADePAAA3j4AAN5AAADeQgAA3kUAAN5GAADeRwAA3kgAAN5JAADeSwAA3k0AAN5OAADeTwAA3lEAAN5SAADekQAA3pMAAN6VAADelwAA3poAAN6bAADenAAA3p0AAN6eAADeoAAA3qIAAN6jAADepAAA3qYAAN6nAADe5gAA3ugAAN7rAADe7QAA3vAAAN7xAADe8gAA3vMAAN70AADe9gAA3vgAAN75AADe+gAA3vwAAN79AADfCgAA3wsAAN8MAADfDgAA300AAN9PAADfUQAA31MAAN9WAADfVwAA31gAAN9ZAADfWgAA31wAAN9eAADfXwAA32AAAN9iAADfYwAA36IAAN+kAADfpgAA36gAAN+rAADfrAAA360AAN+uAADfrwAA37EAAN+zAADftAAA37UAAN+3AADfuAAA3/cAAN/5AADf+wAA3/0AAOAAAADgAQAA4AIAAOADAADgBAAA4AYAAOAIAADgCQAA4AoAAOAMAADgDQAA4EwAAOBOAADgUAAA4FIAAOBVAADgVgAA4FcAAOBYAADgWQAA4FsAAOBdAADgXgAA4F8AAOBhAADgYgAA4KEAAOCjAADgpQAA4KcAAOCqAADgqwAA4KwAAOCtAADgrgAA4LAAAOCyAADgswAA4LQAAOC2AADgtwAA4NwAAOEAAADhJwAA4UsAAOFOAADhUAAA4VIAAOFUAADhVgAA4VgAAOFZAADhXAAA4WkAAOF4AADhegAA4XwAAOF+AADhgAAA4YIAAOGEAADhhgAA4ZUAAOGYAADhmwAA4Z4AAOGhAADhpAAA4acAAOGqAADhrAAA4esAAOHtAADh7wAA4fEAAOH0AADh9QAA4fYAAOH3AADh+AAA4foAAOH8AADh/QAA4f4AAOIAAADiAQAA4kAAAOJCAADiRAAA4kYAAOJJAADiSgAA4ksAAOJMAADiTQAA4k8AAOJRAADiUgAA4lMAAOJVAADiVgAA4pUAAOKXAADimQAA4psAAOKeAADinwAA4qAAAOKhAADiogAA4qQAAOKmAADipwAA4qgAAOKqAADiqwAA4uoAAOLsAADi7gAA4vAAAOLzAADi9AAA4vUAAOL2AADi9wAA4vkAAOL7AADi/AAA4v0AAOL/AADjAAAA4z8AAONBAADjQwAA40UAAONIAADjSQAA40oAAONLAADjTAAA404AAONQAADjUQAA41IAAONUAADjVQAA45QAAOOWAADjmAAA45oAAOOdAADjngAA458AAOOgAADjoQAA46MAAOOlAADjpgAA46cAAOOpAADjqgAA4+kAAOPrAADj7QAA4+8AAOPyAADj8wAA4/QAAOP1AADj9gAA4/gAAOP6AADj+wAA4/wAAOP+AADj/wAA5EoAAORtAADkjQAA5K0AAOSvAADksQAA5LMAAOS1AADkuAAA5LkAAOS6AADkvQAA5L4AAOTAAADkwQAA5MMAAOTGAADkxwAA5MgAAOTLAADkzAAA5NEAAOTeAADk4wAA5OUAAOTnAADk7AAA5O8AAOTyAADk9AAA5RkAAOU9AADlZAAA5YgAAOWLAADljQAA5Y8AAOWRAADlkwAA5ZUAAOWWAADlmQAA5aYAAOW3AADluQAA5bsAAOW9AADlvwAA5cEAAOXDAADlxQAA5ccAAOXYAADl2wAA5d4AAOXhAADl5AAA5ecAAOXqAADl7QAA5fAAAOXyAADmMQAA5jMAAOY1AADmNwAA5joAAOY7AADmPAAA5j0AAOY+AADmQAAA5kIAAOZDAADmRAAA5kYAAOZHAADmhgAA5ogAAOaKAADmjAAA5o8AAOaQAADmkQAA5pIAAOaTAADmlQAA5pcAAOaYAADmmQAA5psAAOacAADm2wAA5t0AAObgAADm4gAA5uUAAObmAADm5wAA5ugAAObpAADm6wAA5u0AAObuAADm7wAA5vEAAObyAADm/wAA5wAAAOcBAADnAwAA50IAAOdEAADnRgAA50gAAOdLAADnTAAA500AAOdOAADnTwAA51EAAOdTAADnVAAA51UAAOdXAADnWAAA55cAAOeZAADnmwAA550AAOegAADnoQAA56IAAOejAADnpAAA56YAAOeoAADnqQAA56oAAOesAADnrQAA5+wAAOfuAADn8AAA5/IAAOf1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AAEPQQABD0IAAQ9DAAEPRAABD0UAAQ9HAAEPSQABD0oAAQ9LAAEPTQABD04AAQ+NAAEPjwABD5EAAQ+TAAEPlgABD5cAAQ+YAAEPmQABD5oAAQ+cAAEPngABD58AAQ+gAAEPogABD6MAAQ/iAAEP5AABD+YAAQ/oAAEP6wABD+wAAQ/tAAEP7gABD+8AAQ/xAAEP8wABD/QAAQ/1AAEP9wABD/gAARA3AAEQOQABEDsAARA9AAEQQAABEEEAARBCAAEQQwABEEQAARBGAAEQSAABEEkAARBKAAEQTAABEE0AARCMAAEQjgABEJAAARCSAAEQlQABEJYAARCXAAEQmAABEJkAARCbAAEQnQABEJ4AARCfAAEQoQABEKIAARDHAAEQ6wABERIAARE2AAEROQABETsAARE9AAERPwABEUEAARFDAAERRAABEUcAARFUAAERYwABEWUAARFnAAERaQABEWsAARFtAAERbwABEXEAARGAAAERgwABEYYAARGJAAERjAABEY8AARGSAAERlQABEZcAARHWAAER2AABEdoAARHcAAER3wABEeAAARHhAAER4gABEeMAARHlAAER5wABEegAARHpAAER6wABEewAARIrAAESLQABEi8AARIxAAESNAABEjUAARI2AAESNwABEjgAARI6AAESPAABEj0AARI+AAESQAABEkEAARKAAAESggABEoQAARKGAAESiQABEooAARKLAAESjAABEo0AARKPAAESkQABEpIAARKTAAESlQABEpYAARLVAAES1wABEtkAARLbAAES3gABEt8AARLgAAES4QABEuIAARLkAAES5gABEucAARLoAAES6gABEusAARMqAAETLAABEy4AARMwAAETMwABEzQAARM1AAETNgABEzcAARM5AAETOwABEzwAARM9AAETPwABE0AAARN/AAETgQABE4MAAROFAAETiAABE4kAAROKAAETiwABE4wAAROOAAETkAABE5EAAROSAAETlAABE5UAARPUAAET1gABE9gAARPaAAET3QABE94AARPfAAET4AABE+EAARPjAAET5QABE+YAARPnAAET6QABE+oAARQ1AAEUWAABFHgAARSYAAEUmgABFJwAARSeAAEUoAABFKMAARSkAAEUpQABFKgAARSpAAEUqwABFKwAARSuAAEUsQABFLIAARSzAAEUtgABFLcAARS8AAEUyQABFM4AARTQAAEU0gABFNcAARTaAAEU3QABFN8AARUEAAEVKAABFU8AARVzAAEVdgABFXgAARV6AAEVfAABFX4AARWAAAEVgQABFYQAARWRAAEVogABFaQAARWmAAEVqAABFaoAARWsAAEVrgABFbAAARWyAAEVwwABFcYAARXJAAEVzAABFc8AARXSAAEV1QABFdgAARXbAAEV3QABFhwAARYeAAEWIAABFiIAARYlAAEWJgABFicAARYoAAEWKQABFisAARYtAAEWLgABFi8AARYxAAEWMgABFnEAARZzAAEWdQABFncAARZ6AAEWewABFnwAARZ9AAEWfgABFoAAARaCAAEWgwABFoQAARaGAAEWhwABFsYAARbIAAEWywABFs0AARbQAAEW0QABFtIAARbTAAEW1AABFtYAARbYAAEW2QABFtoAARbcAAEW3QABFuoAARbrAAEW7AABFu4AARctAAEXLwABFzEAARczAAEXNgABFzcAARc4AAEXOQABFzoAARc8AAEXPgABFz8AARdAAAEXQgABF0MAAReCAAEXhAABF4YAAReIAAEXiwABF4wAAReNAAEXjgABF48AAReRAAEXkwABF5QAAReVAAEXlwABF5gAARfXAAEX2QABF9sAARfdAAEX4AABF+EAARfiAAEX4wABF+QAARfmAAEX6AABF+kAARfqAAEX7AABF+0AARgsAAEYLgABGDAAARgyAAEYNQABGDYAARg3AAEYOAABGDkAARg7AAEYPQABGD4AARg/AAEYQQABGEIAARiBAAEYgwABGIUAARiHAAEYigABGIsAARiMAAEYjQABGI4AARiQAAEYkgABGJMAARiUAAEYlgABGJcAARi8AAEY4AABGQcAARkrAAEZLgABGTAAARkyAAEZNAABGTYAARk4AAEZOQABGTwAARlJAAEZWAABGVoAARlcAAEZXgABGWAAARliAAEZZAABGWYAARl1AAEZeAABGXsAARl+AAEZgQABGYQAARmHAAEZigABGYwAARnLAAEZzQABGc8AARnRAAEZ1AABGdUAARnWAAEZ1wABGdgAARnaAAEZ3AABGd0AARneAAEZ4AABGeEAARogAAEaIgABGiQAARomAAEaKQABGioAARorAAEaLAABGi0AARovAAEaMQABGjIAARozAAEaNQABGjYAARp1AAEadwABGnkAARp7AAEafgABGn8AARqAAAEagQABGoIAARqEAAEahgABGocAARqIAAEaigABGosAARrKAAEazAABGs4AARrQAAEa0wABGtQAARrVAAEa1gABGtcAARrZAAEa2wABGtwAARrdAAEa3wABGuAAARsfAAEbIQABGyMAARslAAEbKAABGykAARsqAAEbKwABGywAARsuAAEbMAABGzEAARsyAAEbNAABGzUAARt0AAEbdgABG3gAARt6AAEbfQABG34AARt/AAEbgAABG4EAARuDAAEbhQABG4YAARuHAAEbiQABG4oAARvJAAEbywABG80AARvPAAEb0gABG9MAARvUAAEb1QABG9YAARvYAAEb2gABG9sAARvcAAEb3gABG98AARvqAAEb8wABG/QAARv2AAEb/wABHAoAARwZAAEcJAABHDIAARxHAAEcWwABHHIAARyEAAEcwwABHMUAARzHAAEcyQABHMwAARzNAAEczgABHM8AARzQAAEc0gABHNQAARzVAAEc1gABHNgAARzZAAEdGAABHRoAAR0cAAEdHgABHSEAAR0iAAEdIwABHSQAAR0lAAEdJwABHSkAAR0qAAEdKwABHS0AAR0uAAEdbQABHW8AAR1xAAEdcwABHXYAAR13AAEdeAABHXkAAR16AAEdfAABHX4AAR1/AAEdgAABHYIAAR2DAAEdwgABHcQAAR3GAAEdyAABHcsAAR3MAAEdzQABHc4AAR3PAAEd0QABHdMAAR3UAAEd1QABHdcAAR3YAAEeFwABHhkAAR4bAAEeHQABHiAAAR4hAAEeIgABHiMAAR4kAAEeJgABHigAAR4pAAEeKgABHiwAAR4tAAEeeAABHpsAAR67AAEe2wABHt0AAR7fAAEe4QABHuMAAR7lAAEe5gABHucAAR7qAAEe6wABHu0AAR7uAAEe8AABHvIAAR7zAAEe9AABHvcAAR74AAEe/QABHwoAAR8PAAEfEQABHxMAAR8YAAEfGwABHx4AAR8gAAEfRQABH2kAAR+QAAEftAABH7cAAR+5AAEfuwABH70AAR+/AAEfwQABH8IAAR/FAAEf0gABH+MAAR/lAAEf5wABH+kAAR/rAAEf7QABH+8AAR/xAAEf8wABIAQAASAHAAEgCgABIA0AASAQAAEgEwABIBYAASAZAAEgHAABIB4AASBdAAEgXwABIGEAASBjAAEgZgABIGcAASBoAAEgaQABIGoAASBsAAEgbgABIG8AASBwAAEgcgABIHMAASCyAAEgtAABILYAASC4AAEguwABILwAASC9AAEgvgABIL8AASDBAAEgwwABIMQAASDFAAEgxwABIMgAASEHAAEhCQABIQwAASEOAAEhEQABIRIAASETAAEhFAABIRUAASEXAAEhGQABIRoAASEbAAEhHQABIR4AASErAAEhLAABIS0AASEvAAEhbgABIXAAASFyAAEhdAABIXcAASF4AAEheQABIXoAASF7AAEhfQABIX8AASGAAAEhgQABIYMAASGEAAEhwwABIcUAASHHAAEhyQABIcwAASHNAAEhzgABIc8AASHQAAEh0gABIdQAASHVAAEh1gABIdgAASHZAAEiGAABIhoAASIcAAEiHgABIiEAASIiAAEiIwABIiQAASIlAAEiJwABIikAASIqAAEiKwABIi0AASIuAAEibQABIm8AASJxAAEicwABInYAASJ3AAEieAABInkAASJ6AAEifAABIn4AASJ/AAEigAABIoIAASKDAAEiwgABIsQAASLGAAEiyAABIssAASLMAAEizQABIs4AASLPAAEi0QABItMAASLUAAEi1QABItcAASLYAAEi/QABIyEAASNIAAEjbAABI28AASNxAAEjcwABI3UAASN3AAEjeQABI3oAASN9AAEjigABI5kAASObAAEjnQABI58AASOhAAEjowABI6UAASOnAAEjtgABI7kAASO8AAEjvwABI8IAASPFAAEjyAABI8sAASPNAAEkDAABJA4AASQQAAEkEgABJBUAASQWAAEkFwABJBgAASQZAAEkGwABJB0AASQeAAEkHwABJCEAASQiAAEkYQABJGMAASRlAAEkZwABJGoAASRrAAEkbAABJG0AASRuAAEkcAABJHIAASRzAAEkdAABJHYAASR3AAEktgABJLgAASS6AAEkvAABJL8AASTAAAEkwQABJMIAASTDAAEkxQABJMcAASTIAAEkyQABJMsAASTMAAElCwABJQ0AASUPAAElEQABJRQAASUVAAElFgABJRcAASUYAAElGgABJRwAASUdAAElHgABJSAAASUhAAElYAABJWIAASVkAAElZgABJWkAASVqAAElawABJWwAASVtAAElbwABJXEAASVyAAElcwABJXUAASV2AAEltQABJbcAASW5AAEluwABJb4AASW/AAElwAABJcEAASXCAAElxAABJcYAASXHAAElyAABJcoAASXLAAEmCgABJgwAASYOAAEmEAABJhMAASYUAAEmFQABJhYAASYXAAEmGQABJhsAASYcAAEmHQABJh8AASYgAAEmawABJo4AASauAAEmzgABJtAAASbSAAEm1AABJtYAASbYAAEm2QABJtoAASbdAAEm3gABJuAAASbhAAEm4wABJuUAASbmAAEm5wABJuoAASbrAAEm9AABJwEAAScGAAEnCAABJwoAAScPAAEnEgABJxUAAScXAAEnPAABJ2AAASeHAAEnqwABJ64AASewAAEnsgABJ7QAASe2AAEnuAABJ7kAASe8AAEnyQABJ9oAASfcAAEn3gABJ+AAASfiAAEn5AABJ+YAASfoAAEn6gABJ/sAASf+AAEoAQABKAQAASgHAAEoCgABKA0AASgQAAEoEwABKBUAAShUAAEoVgABKFgAAShaAAEoXQABKF4AAShfAAEoYAABKGEAAShjAAEoZQABKGYAAShnAAEoaQABKGoAASipAAEoqwABKK0AASivAAEosgABKLMAASi0AAEotQABKLYAASi4AAEougABKLsAASi8AAEovgABKL8AASj+AAEpAAABKQMAASkFAAEpCAABKQkAASkKAAEpCwABKQwAASkOAAEpEAABKREAASkSAAEpFAABKRUAASkiAAEpIwABKSQAASkmAAEpZQABKWcAASlpAAEpawABKW4AASlvAAEpcAABKXEAASlyAAEpdAABKXYAASl3AAEpeAABKXoAASl7AAEpugABKbwAASm+AAEpwAABKcMAASnEAAEpxQABKcYAASnHAAEpyQABKcsAASnMAAEpzQABKc8AASnQAAEqDwABKhEAASoTAAEqFQABKhgAASoZAAEqGgABKhsAASocAAEqHgABKiAAASohAAEqIgABKiQAASolAAEqZAABKmYAASpoAAEqagABKm0AASpuAAEqbwABKnAAASpxAAEqcwABKnUAASp2AAEqdwABKnkAASp6AAEquQABKrsAASq9AAEqvwABKsIAASrDAAEqxAABKsUAASrGAAEqyAABKsoAASrLAAEqzAABKs4AASrPAAEq9AABKxgAASs/AAErYwABK2YAAStoAAEragABK2wAAStuAAErcAABK3EAASt0AAErgQABK5AAASuSAAErlAABK5YAASuYAAErmgABK5wAASueAAErrQABK7AAASuzAAErtgABK7kAASu8AAErvwABK8IAASvEAAEsAwABLAUAASwHAAEsCQABLAwAASwNAAEsDgABLA8AASwQAAEsEgABLBQAASwVAAEsFgABLBgAASwZAAEsWAABLFoAASxcAAEsXgABLGEAASxiAAEsYwABLGQAASxlAAEsZwABLGkAASxqAAEsawABLG0AASxuAAEsrQABLK8AASyxAAEsswABLLYAASy3AAEsuAABLLkAASy6AAEsvAABLL4AASy/AAEswAABLMIAASzDAAEtAgABLQQAAS0HAAEtCQABLQwAAS0NAAEtDgABLQ8AAS0QAAEtEgABLRQAAS0VAAEtFgABLRgAAS0ZAAEtWAABLVoAAS1cAAEtXgABLWEAAS1iAAEtYwABLWQAAS1lAAEtZwABLWkAAS1qAAEtawABLW0AAS1uAAEtrQABLa8AAS2xAAEtswABLbYAAS23AAEtuAABLbkAAS26AAEtvAABLb4AAS2/AAEtwAABLcIAAS3DAAEuAgABLgQAAS4GAAEuCAABLgsAAS4MAAEuDQABLg4AAS4PAAEuEQABLhMAAS4UAAEuFQABLhcAAS4YAAEuYwABLoYAAS6mAAEuxgABLsgAAS7KAAEuzAABLs4AAS7QAAEu0QABLtIAAS7VAAEu1gABLtgAAS7ZAAEu2wABLt0AAS7eAAEu3wABLuIAAS7jAAEu6AABLvUAAS76AAEu/AABLv4AAS8DAAEvBgABLwkAAS8LAAEvMAABL1QAAS97AAEvnwABL6IAAS+kAAEvpgABL6gAAS+qAAEvrAABL60AAS+wAAEvvQABL84AAS/QAAEv0gABL9QAAS/WAAEv2AABL9oAAS/cAAEv3gABL+8AAS/yAAEv9QABL/gAAS/7AAEv/gABMAEAATAEAAEwBwABMAkAATBIAAEwSgABMEwAATBOAAEwUQABMFIAATBTAAEwVAABMFUAATBXAAEwWQABMFoAATBbAAEwXQABMF4AATCdAAEwnwABMKEAATCjAAEwpgABMKcAATCoAAEwqQABMKoAATCsAAEwrgABMK8AATCwAAEwsgABMLMAATDyAAEw9AABMPcAATD5AAEw/AABMP0AATD+AAEw/wABMQAAATECAAExBAABMQUAATEGAAExCAABMQkAATEWAAExFwABMRgAATEaAAExWQABMVsAATFdAAExXwABMWIAATFjAAExZAABMWUAATFmAAExaAABMWoAATFrAAExbAABMW4AATFvAAExrgABMbAAATGyAAExtAABMbcAATG4AAExuQABMboAATG7AAExvQABMb8AATHAAAExwQABMcMAATHEAAEyAwABMgUAATIHAAEyCQABMgwAATINAAEyDgABMg8AATIQAAEyEgABMhQAATIVAAEyFgABMhgAATIZAAEyWAABMloAATJcAAEyXgABMmEAATJiAAEyYwABMmQAATJlAAEyZwABMmkAATJqAAEyawABMm0AATJuAAEyrQABMq8AATKxAAEyswABMrYAATK3AAEyuAABMrkAATK6AAEyvAABMr4AATK/AAEywAABMsIAATLDAAEy6AABMwwAATMzAAEzVwABM1oAATNcAAEzXgABM2AAATNiAAEzZAABM2UAATNoAAEzdQABM4QAATOGAAEziAABM4oAATOMAAEzjgABM5AAATOSAAEzoQABM6QAATOnAAEzqgABM60AATOwAAEzswABM7YAATO4AAEz9wABM/kAATP8AAEz/gABNAEAATQCAAE0AwABNAQAATQFAAE0BwABNAkAATQKAAE0CwABNA0AATQOAAE0TQABNE8AATRRAAE0UwABNFYAATRXAAE0WAABNFkAATRaAAE0XAABNF4AATRfAAE0YAABNGIAATRjAAE0ogABNKQAATSmAAE0qAABNKsAATSsAAE0rQABNK4AATSvAAE0sQABNLMAATS0AAE0tQABNLcAATS4AAE09wABNPkAATT8AAE0/gABNQEAATUCAAE1AwABNQQAATUFAAE1BwABNQkAATUKAAE1CwABNQ0AATUOAAE1TQABNU8AATVRAAE1UwABNVYAATVXAAE1WAABNVkAATVaAAE1XAABNV4AATVfAAE1YAABNWIAATVjAAE1ogABNaQAATWmAAE1qAABNasAATWsAAE1rQABNa4AATWvAAE1sQABNbMAATW0AAE1tQABNbcAATW4AAE19wABNfkAATX7AAE1/QABNgAAATYBAAE2AgABNgMAATYEAAE2BgABNggAATYJAAE2CgABNgwAATYNAAE2WAABNnsAATabAAE2uwABNr0AATa/AAE2wQABNsMAATbFAAE2xgABNscAATbKAAE2ywABNs0AATbOAAE20AABNtIAATbTAAE21AABNtcAATbYAAE23QABNuoAATbvAAE28QABNvMAATb4AAE2+wABNv4AATcAAAE3JQABN0kAATdwAAE3lAABN5cAATeZAAE3mwABN50AATefAAE3oQABN6IAATelAAE3sgABN8MAATfFAAE3xwABN8kAATfLAAE3zQABN88AATfRAAE30wABN+QAATfnAAE36gABN+0AATfwAAE38wABN/YAATf5AAE3/AABN/4AATg9AAE4PwABOEEAAThDAAE4RgABOEcAAThIAAE4SQABOEoAAThMAAE4TgABOE8AAThQAAE4UgABOFMAATiSAAE4lAABOJYAATiYAAE4mwABOJwAATidAAE4ngABOJ8AATihAAE4owABOKQAATilAAE4pwABOKgAATjnAAE46QABOOwAATjuAAE48QABOPIAATjzAAE49AABOPUAATj3AAE4+QABOPoAATj7AAE4/QABOP4AATkLAAE5DAABOQ0AATkPAAE5TgABOVAAATlSAAE5VAABOVcAATlYAAE5WQABOVoAATlbAAE5XQABOV8AATlgAAE5YQABOWMAATlkAAE5owABOaUAATmnAAE5qQABOawAATmtAAE5rgABOa8AATmwAAE5sgABObQAATm1AAE5tgABObgAATm5AAE5+AABOfoAATn8AAE5/gABOgEAAToCAAE6AwABOgQAAToFAAE6BwABOgkAAToKAAE6CwABOg0AAToOAAE6TQABOk8AATpRAAE6UwABOlYAATpXAAE6WAABOlkAATpaAAE6XAABOl4AATpfAAE6YAABOmIAATpjAAE6ogABOqQAATqmAAE6qAABOqsAATqsAAE6rQABOq4AATqvAAE6sQABOrMAATq0AAE6tQABOrcAATq4AAE63QABOwEAATsoAAE7TAABO08AATtRAAE7UwABO1UAATtXAAE7WQABO1oAATtdAAE7agABO3kAATt7AAE7fQABO38AATuBAAE7gwABO4UAATuHAAE7lgABO5kAATucAAE7nwABO6IAATulAAE7qAABO6sAATutAAE77AABO+4AATvwAAE78gABO/UAATv2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AAFpaAABaWkAAWlrAAFpbQABaW4AAWlvAAFpcQABaXIAAWmxAAFpswABabUAAWm3AAFpugABabsAAWm8AAFpvQABab4AAWnAAAFpwgABacMAAWnEAAFpxgABaccAAWoGAAFqCAABagoAAWoMAAFqDwABahAAAWoRAAFqEgABahMAAWoVAAFqFwABahgAAWoZAAFqGwABahwAAWpbAAFqXQABal8AAWphAAFqZAABamUAAWpmAAFqZwABamgAAWpqAAFqbAABam0AAWpuAAFqcAABanEAAWqwAAFqsgABarQAAWq2AAFquQABaroAAWq7AAFqvAABar0AAWq/AAFqwQABasIAAWrDAAFqxQABasYAAWsFAAFrBwABawkAAWsLAAFrDgABaw8AAWsQAAFrEQABaxIAAWsUAAFrFgABaxcAAWsYAAFrGgABaxsAAWtaAAFrXAABa14AAWtgAAFrYwABa2QAAWtlAAFrZgABa2cAAWtpAAFrawABa2wAAWttAAFrbwABa3AAAWu7AAFr3gABa/4AAWweAAFsIAABbCIAAWwkAAFsJgABbCkAAWwqAAFsKwABbC4AAWwvAAFsMQABbDIAAWw0AAFsNwABbDgAAWw5AAFsPAABbD0AAWxCAAFsTwABbFQAAWxWAAFsWAABbF0AAWxgAAFsYwABbGUAAWyKAAFsrgABbNUAAWz5AAFs/AABbP4AAW0AAAFtAgABbQQAAW0GAAFtBwABbQoAAW0XAAFtKAABbSoAAW0sAAFtLgABbTAAAW0yAAFtNAABbTYAAW04AAFtSQABbUwAAW1PAAFtUgABbVUAAW1YAAFtWwABbV4AAW1hAAFtYwABbaIAAW2kAAFtpgABbagAAW2rAAFtrAABba0AAW2uAAFtrwABbbEAAW2zAAFttAABbbUAAW23AAFtuAABbfcAAW35AAFt+wABbf0AAW4AAAFuAQABbgIAAW4DAAFuBAABbgYAAW4IAAFuCQABbgoAAW4MAAFuDQABbkwAAW5OAAFuUQABblMAAW5WAAFuVwABblgAAW5ZAAFuWgABblwAAW5eAAFuXwABbmAAAW5iAAFuYwABbnAAAW5xAAFucgABbnQAAW6zAAFutQABbrcAAW65AAFuvAABbr0AAW6+AAFuvwABbsAAAW7CAAFuxAABbsUAAW7GAAFuyAABbskAAW8I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AAF5+gABef0AAXoAAAF6AwABegYAAXoIAAF6RwABekkAAXpMAAF6TgABelEAAXpSAAF6UwABelQAAXpVAAF6VwABelkAAXpaAAF6WwABel0AAXpeAAF6nQABep8AAXqhAAF6owABeqYAAXqnAAF6qAABeqkAAXqqAAF6rAABeq4AAXqvAAF6sAABerIAAXqzAAF68gABevQAAXr2AAF6+AABevsAAXr8AAF6/QABev4AAXr/AAF7AQABewMAAXsEAAF7BQABewcAAXsIAAF7RwABe0kAAXtLAAF7TQABe1AAAXtRAAF7UgABe1MAAXtUAAF7VgABe1gAAXtZAAF7WgABe1wAAXtdAAF7nAABe54AAXugAAF7ogABe6UAAXumAAF7pwABe6gAAXupAAF7qwABe60AAXuuAAF7rwABe7EAAXuyAAF78QABe/MAAXv1AAF79wABe/oAAXv7AAF7/AABe/0AAXv+AAF8AAABfAIAAXwDAAF8BAABfAYAAXwHAAF8UgABfHUAAXyVAAF8tQABfLcAAXy5AAF8uwABfL0AAXy/AAF8wAABfMEAAXzEAAF8xQABfMcAAXzIAAF8ygABfMwAAXzNAAF8zgABfNEAAXzSAAF82wABfOgAAXztAAF87wABfPEAAXz2AAF8+QABfPwAAXz+AAF9IwABfUcAAX1uAAF9kgABfZUAAX2XAAF9mQABfZsAAX2dAAF9nwABfaAAAX2jAAF9sAABfcEAAX3DAAF9xQABfccAAX3JAAF9ywABfc0AAX3PAAF90QABfeIAAX3lAAF96AABfesAAX3uAAF98QABffQAAX33AAF9+gABffwAAX47AAF+PQABfj8AAX5BAAF+RAABfkUAAX5GAAF+RwABfkgAAX5KAAF+TAABfk0AAX5OAAF+UAABflEAAX6QAAF+kgABfpQAAX6WAAF+mQABfpoAAX6bAAF+nAABfp0AAX6fAAF+oQABfqIAAX6jAAF+pQABfqYAAX7lAAF+5wABfuoAAX7sAAF+7wABfvAAAX7xAAF+8gABfvMAAX71AAF+9wABfvgAAX75AAF++wABfvwAAX8JAAF/CgABfwsAAX8NAAF/TAABf04AAX9QAAF/UgABf1UAAX9WAAF/VwABf1gAAX9ZAAF/WwABf10AAX9eAAF/XwABf2EAAX9iAAF/oQABf6MAAX+l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AAGhKgABoSsAAaEuAAGhOwABoUoAAaFMAAGhTgABoVAAAaFSAAGhVAABoVYAAaFYAAGhZwABoWoAAaFtAAGhcAABoXMAAaF2AAGheQABoXwAAaF+AAGhvQABob8AAaHBAAGhwwABocYAAaHHAAGhyAABockAAaHKAAGhzAABoc4AAaHPAAGh0AABodIAAaHTAAGiEgABohQAAaIWAAGiGAABohsAAaIcAAGiHQABoh4AAaIfAAGiIQABoiMAAaIkAAGiJQABoicAAaIoAAGiZwABomkAAaJrAAGibQABonAAAaJxAAGicgABonMAAaJ0AAGidgABongAAaJ5AAGiegABonwAAaJ9AAGivAABor4AAaLAAAGiwgABosUAAaLGAAGixwABosgAAaLJAAGiywABos0AAaLOAAGizwABotEAAaLSAAGjEQABoxMAAaMVAAGjFwABoxoAAaMbAAGjHAABox0AAaMeAAGjIAABoyIAAaMjAAGjJAABoyYAAaMnAAGjZgABo2gAAaNqAAGjbAABo28AAaNwAAGjcQABo3IAAaNzAAGjdQABo3cAAaN4AAGjeQABo3sAAaN8AAGjuwABo70AAaO/AAGjwQABo8QAAaPFAAGjxgABo8cAAaPIAAGjygABo8wAAaPNAAGjzgABo9AAAaPRAAGkHAABpD8AAaRfAAGkfwABpIEAAaSDAAGkhQABpIcAAaSJAAGkigABpIsAAaSOAAGkjwABpJEAAaSSAAGklAABpJYAAaSXAAGkmAABpJsAAaScAAGkoQABpK4AAaSzAAGktQABpLcAAaS8AAGkvwABpMIAAaTEAAGk6QABpQ0AAaU0AAGlWAABpVsAAaVdAAGlXwABpWEAAaVjAAGlZQABpWYAAaVpAAGldgABpYcAAaWJAAGliwABpY0AAaWPAAGlkQABpZMAAaWVAAGllwABpagAAaWrAAGlrgABpbEAAaW0AAGltwABpboAAaW9AAGlwAABpcIAAaYBAAGmAwABpgUAAaYHAAGmCgABpgsAAaYMAAGmDQABpg4AAaYQAAGmEgABphMAAaYUAAGmFgABphcAAaZWAAGmWAABploAAaZcAAGmXwABpmAAAaZhAAGmYgABpmMAAaZlAAGmZwABpmgAAaZpAAGmawABpmwAAaarAAGmrQABprAAAaayAAGmtQABprYAAaa3AAGmuAABprkAAaa7AAGmvQABpr4AAaa/AAGmwQABpsIAAabPAAGm0AABptEAAabTAAGnEgABpxQAAacWAAGnGAABpxsAAaccAAGnHQABpx4AAacfAAGnIQABpyMAAackAAGnJQABpycAAacoAAGnZwABp2kAAadrAAGnbQABp3AAAadxAAGncgABp3MAAad0AAGndgABp3gAAad5AAGnegABp3wAAad9AAGnvAABp74AAafAAAGnwgABp8UAAafGAAGnxwABp8gAAafJAAGnywABp80AAafOAAGnzwABp9EAAafSAAGoEQABqBMAAagVAAGoFwABqBoAAagbAAGoHAABqB0AAageAAGoIAABqCIAAagjAAGoJAABqCYAAagnAAGoZgABqGgAAahqAAGobAABqG8AAahwAAGocQABqHIAAahzAAGodQABqHcAAah4AAGoeQABqHsAAah8AAGooQABqMUAAajsAAGpEAABqRMAAakVAAGpFwABqRkAAakbAAGpHQABqR4AAakhAAGpLgABqT0AAak/AAGpQQABqUMAAalFAAGpRwABqUkAAalLAAGpWgABqV0AAalgAAGpYwABqWYAAalpAAGpbAABqW8AAalxAAGpsAABqbIAAam0AAGptgABqbkAAam6AAGpuwABqbwAAam9AAGpvwABqcEAAanCAAGpwwABqcUAAanGAAGqBQABqgcAAaoJAAGqCwABqg4AAaoPAAGqEAABqhEAAaoSAAGqFAABqhYAAaoXAAGqGAABqhoAAaobAAGqWgABqlwAAapeAAGqYAABqmMAAapkAAGqZQABqmYAAapnAAGqaQABqmsAAapsAAGqbQABqm8AAapwAAGqrwABqrEAAaq0AAGqtgABqrkAAaq6AAGquwABqrwAAaq9AAGqvwABqsEAAarCAAGqwwABqsUAAarGAAGrCQABqy0AAatRAAGrdAABq5sAAau7AAGr4gABrAkAAawpAAGsTQABrHEAAaxzAAGsdgABrHgAAax6AAGsfAABrH8AAayCAAGshAABrIYAAayJAAGsiwABrI0AAayQAAGskwABrJQAAayZAAGspgABrKkAAayrAAGsrgABrLEAAayzAAGs2AABrPwAAa0jAAGtRwABrUoAAa1MAAGtTgABrVAAAa1SAAGtVAABrVUAAa1YAAGtZQABrXgAAa16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AAG4WwABuFwAAbhdAAG4XwABuGEAAbhiAAG4YwABuGUAAbhmAAG4pQABuKcAAbipAAG4qwABuK4AAbivAAG4sAABuLEAAbiyAAG4tAABuLYAAbi3AAG4uAABuLoAAbi7AAG4+gABuPwAAbj+AAG5AAABuQMAAbkEAAG5BQABuQYAAbkHAAG5CQABuQsAAbkMAAG5DQABuQ8AAbkQAAG5TwABuVEAAblUAAG5VgABuVkAAblaAAG5WwABuVwAAbldAAG5XwABuWEAAbliAAG5YwABuWUAAblmAAG5sQABudQAAbn0AAG6FAABuhYAAboYAAG6GgABuhwAAbofAAG6IAABuiEAAbokAAG6JQABuicAAbooAAG6KgABui0AAbouAAG6LwABujIAAbozAAG6OAABukUAAbpKAAG6TAABuk4AAbpTAAG6VgABulkAAbpbAAG6gAABuqQAAbrLAAG67wABuvIAAbr0AAG69gABuvgAAbr6AAG6/AABuv0AAbsAAAG7DQABux4AAbsgAAG7IgABuyQAAbsmAAG7KAABuyoAAbssAAG7LgABuz8AAbtCAAG7RQABu0gAAbtLAAG7TgABu1EAAbtUAAG7VwABu1kAAbuYAAG7mgABu5wAAbueAAG7oQABu6IAAbujAAG7pAABu6UAAbunAAG7qQABu6oAAburAAG7rQABu64AAbvtAAG77wABu/EAAbvzAAG79gABu/cAAbv4AAG7+QABu/oAAbv8AAG7/gABu/8AAbwAAAG8AgABvAMAAbxCAAG8RAABvEcAAbxJAAG8TAABvE0AAbxOAAG8TwABvFAAAbxSAAG8VAABvFUAAbxWAAG8WAABvFkAAbxmAAG8ZwABvGgAAbxqAAG8qQABvKsAAbytAAG8rwABvLIAAbyzAAG8tAABvLUAAby2AAG8uAABvLoAAby7AAG8vAABvL4AAby/AAG8/gABvQAAAb0CAAG9BAABvQcAAb0IAAG9CQABvQoAAb0LAAG9DQABvQ8AAb0QAAG9EQABvRMAAb0UAAG9UwABvVUAAb1XAAG9WQABvVwAAb1dAAG9XgABvV8AAb1gAAG9YgABvWQAAb1lAAG9ZgABvWgAAb1pAAG9qAABvaoAAb2sAAG9rgABvbEAAb2yAAG9swABvbQAAb21AAG9twABvbkAAb26AAG9uwABvb0AAb2+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AAHJGwAByR0AAckfAAHJIAABySEAAckjAAHJJAAByWMAAcllAAHJZwAByWkAAclsAAHJbQAByW4AAclvAAHJcAAByXIAAcl0AAHJdQAByXYAAcl4AAHJeQABycQAAcnnAAHKBwAByicAAcopAAHKKwAByi0AAcovAAHKMgAByjMAAco0AAHKNwAByjgAAco6AAHKOwAByj0AAcpAAAHKQQABykIAAcpFAAHKRgAByksAAcpYAAHKXQAByl8AAcphAAHKZgABymkAAcpsAAHKbgABypMAAcq3AAHK3gABywIAAcsFAAHLBwABywkAAcsLAAHLDQAByw8AAcsQAAHLEwAByyAAAcsxAAHLMwAByzUAAcs3AAHLOQAByzsAAcs9AAHLPwABy0EAActSAAHLVQABy1gAActbAAHLXgABy2EAActkAAHLZwABy2oAActsAAHLqwABy60AAcuvAAHLsQABy7QAAcu1AAHLtgABy7cAAcu4AAHLugABy7wAAcu9AAHLvgABy8AAAcvBAAHMAAABzAIAAcwEAAHMBgABzAkAAcwKAAHMCwABzAwAAcwNAAHMDwABzBEAAcwSAAHMEwABzBUAAcwWAAHMVQABzFcAAcxaAAHMXAABzF8AAcxgAAHMYQABzGIAAcxjAAHMZQABzGcAAcxoAAHMaQABzGsAAcxsAAHMeQABzHoAAcx7AAHMfQABzLwAAcy+AAHMwAABzMIAAczFAAHMxgABzMcAAczIAAHMyQABzMsAAczNAAHMzgABzM8AAczRAAHM0gABzREAAc0TAAHNFQABzRcAAc0aAAHNGwABzRwAAc0dAAHNHgABzSAAAc0iAAHNIwABzSQAAc0mAAHNJwABzWYAAc1oAAHNagABzWwAAc1vAAHNcAABzXEAAc1yAAHNcwABzXUAAc13AAHNeAABzXkAAc17AAHNfAABzbsAAc29AAHNvwABzcEAAc3EAAHNxQABzcYAAc3HAAHNyAABzcoAAc3MAAHNzQABzc4AAc3QAAHN0QABzhAAAc4SAAHOFAABzhYAAc4ZAAHOGgABzhsAAc4cAAHOHQABzh8AAc4hAAHOIgABziMAAc4lAAHOJgABzksAAc5vAAHOlgABzroAAc69AAHOvwABzsEAAc7DAAHOxQABzscAAc7IAAHOywABztgAAc7nAAHO6QABzusAAc7t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AAH2swAB9vIAAfb0AAH29wAB9vkAAfb8AAH2/QAB9v4AAfb/AAH3AAAB9wIAAfcEAAH3BQAB9wYAAfcIAAH3CQAB90gAAfdKAAH3TAAB904AAfdRAAH3UgAB91MAAfdUAAH3VQAB91cAAfdZAAH3WgAB91sAAfddAAH3XgAB950AAfefAAH3oQAB96MAAfemAAH3pwAB96gAAfepAAH3qgAB96wAAfeuAAH3rwAB97AAAfeyAAH3swAB9/IAAff0AAH39wAB9/kAAff8AAH3/QAB9/4AAff/AAH4AAAB+AIAAfgEAAH4BQAB+AYAAfgIAAH4CQAB+EgAAfhKAAH4TAAB+E4AAfhRAAH4UgAB+FMAAfhUAAH4VQAB+FcAAfhZAAH4WgAB+FsAAfhdAAH4XgAB+J0AAfifAAH4oQAB+KMAAfimAAH4pwAB+KgAAfipAAH4qgAB+KwAAfiuAAH4rwAB+LAAAfiyAAH4swAB+PIAAfj0AAH49gAB+PgAAfj7AAH4/AAB+P0AAfj+AAH4/wAB+QEAAfkDAAH5BAAB+QUAAfkHAAH5CAAB+VMAAfl2AAH5lgAB+bYAAfm4AAH5ugAB+bwAAfm+AAH5wAAB+cEAAfnCAAH5xQAB+cYAAfnIAAH5yQAB+csAAfnNAAH5zgAB+c8AAfnSAAH50wAB+dgAAfnlAAH56gAB+ewAAfnuAAH58wAB+fYAAfn5AAH5+wAB+iAAAfpEAAH6awAB+o8AAfqSAAH6lAAB+pYAAfqYAAH6mgAB+pwAAfqdAAH6oAAB+q0AAfq+AAH6wAAB+sIAAfrEAAH6xgAB+sgAAfrKAAH6zAAB+s4AAfrfAAH64gAB+uUAAfroAAH66wAB+u4AAfrxAAH69AAB+vcAAfr5AAH7OAAB+zoAAfs8AAH7PgAB+0EAAftCAAH7QwAB+0QAAftFAAH7RwAB+0kAAftKAAH7SwAB+00AAftOAAH7jQAB+48AAfuRAAH7kwAB+5YAAfuXAAH7mAAB+5kAAfuaAAH7nAAB+54AAfufAAH7oAAB+6IAAfujAAH74gAB++QAAfvnAAH76QAB++wAAfvtAAH77gAB++8AAfvwAAH78gAB+/QAAfv1AAH79gAB+/gAAfv5AAH8BgAB/AcAAfwIAAH8CgAB/EkAAfxLAAH8TQAB/E8AAfxSAAH8UwAB/FQAAfxV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AAIHhQACB4YAAgeHAAIHiAACB4kAAgeLAAIHjQACB44AAgePAAIHkQACB5IAAgfRAAIH0wACB9YAAgfYAAIH2wACB9wAAgfdAAIH3gACB98AAgfhAAIH4wACB+QAAgflAAIH5wACB+gAAggnAAIIKQACCCsAAggtAAIIMAACCDEAAggyAAIIMwACCDQAAgg2AAIIOAACCDkAAgg6AAIIPAACCD0AAgh8AAIIfgACCIAAAgiCAAIIhQACCIYAAgiHAAIIiAACCIkAAgiLAAIIjQACCI4AAgiPAAIIkQACCJIAAgibAAIInAACCJ4AAgjdAAII3wACCOEAAgjjAAII5QACCOYAAgjnAAII6AACCOkAAgjrAAII7QACCO4AAgjvAAII8QACCPIAAgkxAAIJMwACCTUAAgk3AAIJOQACCToAAgk7AAIJPAACCT0AAgk/AAIJQQACCUIAAglDAAIJRQACCUYAAgmFAAIJhwACCYkAAgmLAAIJjQACCY4AAgmPAAIJkAACCZEAAgmTAAIJlQACCZYAAgmXAAIJmQACCZoAAgnlAAIKCAACCigAAgpIAAIKSgACCkwAAgpOAAIKUAACClIAAgpTAAIKVAACClcAAgpYAAIKWgACClsAAgpdAAIKXwACCmAAAgphAAIKZAACCmUAAgpuAAIKewACCoAAAgqCAAIKhAACCokAAgqMAAIKjwACCpEAAgq2AAIK2gACCwEAAgslAAILKAACCyoAAgssAAILLgACCzAAAgsyAAILMwACCzYAAgtDAAILVAACC1YAAgtYAAILWgACC1wAAgteAAILYAACC2IAAgtkAAILdQACC3gAAgt7AAILfgACC4EAAguEAAILhwACC4oAAguNAAILjwACC84AAgvQAAIL0gACC9QAAgvXAAIL2AACC9kAAgvaAAIL2wACC90AAgvfAAIL4AACC+EAAgvjAAIL5AACDCMAAgwlAAIMJwACDCkAAgwsAAIMLQACDC4AAgwvAAIMMAACDDIAAgw0AAIMNQACDDYAAgw4AAIMOQACDHgAAgx6AAIMfQACDH8AAgyCAAIMgwACDIQAAgyFAAIMhgACDIgAAgyKAAIMiwACDIwAAgyOAAIMjwACDJwAAgydAAIMngACDKAAAgzfAAIM4QACDOMAAgzlAAIM6AACDOkAAgzqAAIM6wACDOwAAgzu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AAIkYwACJGUAAiR4AAIkewACJH4AAiSBAAIkhAACJIcAAiSKAAIkjQACJJAAAiSTAAIklQACJNQAAiTWAAIk2QACJNsAAiTeAAIk3wACJOAAAiThAAIk4gACJOQAAiTmAAIk5wACJOgAAiTqAAIk6wACJPQAAiT1AAIk9wACJTYAAiU4AAIlOgACJTwAAiU/AAIlQAACJUEAAiVCAAIlQwACJUUAAiVHAAIlSAACJUkAAiVLAAIlTAACJYsAAiWNAAIlkAACJZIAAiWVAAIllgACJZcAAiWYAAIlmQACJZsAAiWdAAIlngACJZ8AAiWhAAIlogACJasAAiWsAAIlrgACJe0AAiXvAAIl8QACJfMAAiX2AAIl9wACJfgAAiX5AAIl+gACJfwAAiX+AAIl/wACJgAAAiYCAAImAwACJkIAAiZEAAImRwACJkkAAiZMAAImTQACJk4AAiZPAAImUAACJlIAAiZUAAImVQACJlYAAiZYAAImWQACJmIAAiZjAAImZQACJqQAAiamAAImqAACJqoAAiatAAImrgACJq8AAiawAAImsQACJrMAAia1AAImtgACJrcAAia5AAImugACJvkAAib7AAIm/gACJwAAAicDAAInBAACJwUAAicGAAInBwACJwkAAicLAAInDAACJw0AAicPAAInEAACJx0AAiceAAInHwACJyEAAidgAAInYgACJ2QAAidmAAInaQACJ2oAAidrAAInbAACJ20AAidvAAIncQACJ3IAAidzAAIndQACJ3YAAie1AAIntwACJ7oAAie8AAInvwACJ8AAAifBAAInwgACJ8MAAifFAAInxwACJ8gAAifJAAInywACJ8wAAifaAAIn5wACJ+wAAifvAAIn8gACJ/cAAif6AAIn/QACJ/8AAigHAAIoEgACKF0AAiiAAAIooAACKMAAAijCAAIoxAACKMYAAijIAAIoywACKMwAAijNAAIo0AACKNEAAijTAAIo1AACKNYAAijZAAIo2gACKNsAAijeAAIo3wACKOQAAijxAAIo9gACKPgAAij6AAIo/wACKQIAAikFAAIpBwACKSwAAilQAAIpdwACKZsAAimeAAIpoAACKaIAAimkAAIppgACKagAAimpAAIprAACKbkAAinKAAIpzAACKc4AAinQAAIp0gACKdQAAinWAAIp2AACKdoAAinrAAIp7gACKfEAAin0AAIp9wACKfoAAin9AAIqAAACKgMAAioFAAIqRAACKkYAAipIAAIqSgACKk0AAipOAAIqTwACKlAAAipRAAIqUwACKlUAAipWAAIqVwACKlkAAipaAAIqmQACKpsAAiqdAAIqnwACKqIAAiqjAAIqpAACKqUAAiqmAAIqqAACKqoAAiqrAAIqrAACKq4AAiqvAAIq7gACKvAAAirzAAIq9QACKvgAAir5AAIq+gACKvsAAir8AAIq/gACKwAAAisBAAIrAgACKwQAAisFAAIrEgACKxMAAisUAAIrFgACK1UAAitXAAIrWQACK1sAAiteAAIrXwACK2AAAithAAIrYgACK2QAAitmAAIrZwACK2gAAitqAAIrawACK6oAAiusAAIrrgACK7AAAiuzAAIrtAACK7UAAiu2AAIrtwACK7kAAiu7AAIrvAACK70AAiu/AAIrwAACK/8AAiwBAAIsAwACLAUAAiwIAAIsCQACLAoAAiwLAAIsDAACLA4AAiwQAAIsEQACLBIAAiwUAAIsFQACLFQAAixWAAIsWAACLFoAAixdAAIsXgACLF8AAixgAAIsYQACLGMAAixlAAIsZgACLGcAAixpAAIsagACLKkAAiyrAAIsrQACLK8AAiyyAAIsswACLLQAAiy1AAIstgACLLgAAiy6AAIsuwACLLwAAiy+AAIsvwACLOQAAi0IAAItLwACLVMAAi1WAAItWAACLVoAAi1cAAItXgACLWAAAi1hAAItZAACLXEAAi2AAAItggACLYQAAi2GAAItiAACLYoAAi2MAAItjgACLZ0AAi2gAAItowACLaYAAi2pAAItrAACLa8AAi2yAAIttAACLfMAAi31AAIt9wACLfkAAi38AAIt/QACLf4AAi3/AAIuAAACLgIAAi4EAAIuBQACLgYAAi4IAAIuCQACLkgAAi5KAAIuTAACLk4AAi5RAAIuUgACLlMAAi5UAAIuVQACLlcAAi5ZAAIuWgACLlsAAi5dAAIuXgACLp0AAi6fAAIuoQACLqMAAi6mAAIupwACLqgAAi6pAAIuqgACLqwAAi6uAAIurwACLrAAAi6yAAIuswACLvIAAi70AAIu9wACLvkAAi78AAIu/QACLv4AAi7/AAIvAAACLwIAAi8EAAIvBQACLwYAAi8IAAIvCQACLwwAAi9L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AAJXVgACV1cAAldaAAJXWwACV10AAldeAAJXYAACV2MAAldkAAJXZQACV2gAAldpAAJXbgACV3sAAleAAAJXggACV4QAAleJAAJXjAACV48AAleRAAJXtgACV9oAAlgBAAJYJQACWCgAAlgqAAJYLAACWC4AAlgwAAJYMgACWDMAAlg2AAJYQwACWFQAAlhWAAJYWAACWFoAAlhcAAJYXgACWGAAAlhiAAJYZAACWHUAAlh4AAJYewACWH4AAliBAAJYhAACWIcAAliKAAJYjQACWI8AAljOAAJY0AACWNIAAljUAAJY1wACWNgAAljZAAJY2gACWNsAAljdAAJY3wACWOAAAljhAAJY4wACWOQAAlkjAAJZJQACWScAAlkpAAJZLAACWS0AAlkuAAJZLwACWTAAAlkyAAJZNAACWTUAAlk2AAJZOAACWTkAAll4AAJZegACWX0AAll/AAJZggACWYMAAlmEAAJZhQACWYYAAlmIAAJZigACWYsAAlmMAAJZjgACWY8AAlmcAAJZnQACWZ4AAlmgAAJZ3wACWeEAAlnjAAJZ5QACWegAAlnpAAJZ6gACWesAAlnsAAJZ7gACWfAAAlnxAAJZ8gACWfQAAln1AAJaNAACWjYAAlo4AAJaOgACWj0AAlo+AAJaPwACWkAAAlpBAAJaQwACWkUAAlpGAAJaRwACWkkAAlpKAAJaiQACWosAAlqNAAJajwACWpIAAlqTAAJalAACWpUAAlqWAAJamAACWpoAAlqbAAJanAACWp4AAlqfAAJa3gACWuAAAlriAAJa5AACWucAAlroAAJa6QACWuoAAlrrAAJa7QACWu8AAlrwAAJa8QACWvMAAlr0AAJbMwACWzUAAls3AAJbOQACWzwAAls9AAJbPgACWz8AAltAAAJbQgACW0QAAltFAAJbRgACW0gAAltJAAJbbgACW5IAAlu5AAJb3QACW+AAAlviAAJb5AACW+YAAlvoAAJb6gACW+sAAlvuAAJb+wACXAoAAlwMAAJcDgACXBAAAlwSAAJcFAACXBYAAlwYAAJcJwACXCoAAlwtAAJcMAACXDMAAlw2AAJcOQACXDwAAlw+AAJcfQACXH8AAlyBAAJcgwACXIYAAlyHAAJciAACXIkAAlyKAAJcjAACXI4AAlyPAAJckAACXJIAAlyTAAJc0gACXNQAAlzWAAJc2AACXNsAAlzcAAJc3QACXN4AAlzfAAJc4QACXOMAAlzkAAJc5QACXOcAAlzoAAJdJwACXSkAAl0rAAJdLQACXTAAAl0xAAJdMgACXTMAAl00AAJdNgACXTgAAl05AAJdOgACXTwAAl09AAJdfAACXX4AAl2AAAJdggACXYUAAl2GAAJdhwACXYgAAl2JAAJdiwACXY0AAl2OAAJdjwACXZEAAl2SAAJd0QACXdMAAl3VAAJd1wACXdoAAl3bAAJd3AACXd0AAl3eAAJd4AACXeIAAl3jAAJd5AACXeYAAl3nAAJeJgACXigAAl4qAAJeLAACXi8AAl4wAAJeMQACXjIAAl4zAAJeNQACXjcAAl44AAJeOQACXjsAAl48AAJeewACXn0AAl5/AAJegQACXoQAAl6FAAJehgACXocAAl6IAAJeigACXowAAl6NAAJejgACXpAAAl6RAAJe3AACXv8AAl8fAAJfPwACX0EAAl9DAAJfRQACX0cAAl9KAAJfSwACX0wAAl9PAAJfUAACX1IAAl9TAAJfVQACX1gAAl9ZAAJfWgACX10AAl9eAAJfYwACX3AAAl91AAJfdwACX3kAAl9+AAJfgQACX4QAAl+GAAJfqwACX88AAl/2AAJgGgACYB0AAmAfAAJgIQACYCMAAmAlAAJgJwACYCgAAmArAAJgOAACYEkAAmBLAAJgTQACYE8AAmBRAAJgUwACYFUAAmBXAAJgWQACYGoAAmBtAAJgcAACYHMAAmB2AAJgeQACYHwAAmB/AAJgggACYIQAAmDDAAJgxQACYMcAAmDJAAJgzAACYM0AAmDOAAJgzwACYNAAAmDSAAJg1AACYNUAAmDWAAJg2AACYNkAAmEYAAJhGgACYRwAAmEeAAJhIQACYSIAAmEjAAJhJAACYSUAAmEnAAJhKQACYSoAAmErAAJhLQACYS4AAmFtAAJhbwACYXIAAmF0AAJhdwACYXgAAmF5AAJhegACYXsAAmF9AAJhfwACYYAAAmGBAAJhgwACYYQAAmGRAAJhkgACYZMAAmGVAAJh1AACYdYAAmHYAAJh2gACYd0AAmHeAAJh3wACYeAAAmHhAAJh4wACYeUAAmHmAAJh5wACYekAAmHqAAJiKQACYisAAmItAAJiLwACYjIAAmIzAAJiNAACYjUAAmI2AAJiOAACYjoAAmI7AAJiPAACYj4AAmI/AAJifgACYoAAAmKCAAJihAACYocAAmKIAAJiiQACYooAAmKLAAJijQACYo8AAmKQAAJikQACYpMAAmKUAAJi0wACYtUAAmLXAAJi2QACYtwAAmLdAAJi3gACYt8AAmLgAAJi4gACYuQAAmLlAAJi5gACYugAAmLpAAJjKAACYyoAAmMsAAJjLgACYzEAAmMyAAJjMwACYzQAAmM1AAJjNwACYzkAAmM6AAJjOwACYz0AAmM+AAJjYwACY4cAAmOuAAJj0gACY9UAAmPXAAJj2QACY9sAAmPdAAJj3wACY+AAAmPjAAJj8AACY/8AAmQBAAJkAwACZAUAAmQHAAJkCQACZAsAAmQNAAJkHAACZB8AAmQiAAJkJQACZCgAAmQrAAJkLgACZDEAAmQzAAJkcgACZHQAAmR2AAJkeAACZHsAAmR8AAJkfQACZH4AAmR/AAJkgQACZIMAAmSEAAJkhQACZIcAAmSIAAJkxwACZMkAAmTLAAJkzQACZNAAAmTRAAJk0gACZNMAAmTUAAJk1gACZNgAAmTZAAJk2gACZNwAAmTdAAJlHAACZR4AAmUgAAJlIgACZSUAAmUmAAJlJwACZSgAAmUpAAJlKwACZS0AAmUuAAJlLwACZTEAAmUyAAJlcQACZXMAAmV1AAJldwACZXoAAmV7AAJlfAACZX0AAmV+AAJlgAACZYIAAmWDAAJlhAACZYYAAmWHAAJlxgACZcgAAmXKAAJlzAACZc8AAmXQAAJl0QACZdIAAmXTAAJl1QACZdcAAmXYAAJl2QACZdsAAmXcAAJmGwACZh0AAmYfAAJmIQACZiQAAmYlAAJmJgACZicAAmYoAAJmKgACZiwAAmYtAAJmLgACZjAAAmYxAAJmcAACZnIAAmZ0AAJmdgACZnkAAmZ6AAJmewACZnwAAmZ9AAJmfwACZoEAAmaCAAJmgwACZoUAAmaGAAJmjwACZpAAAmaSAAJm1QACZvkAAmcdAAJnQAACZ2cAAmeHAAJnrgACZ9UAAmf1AAJoGQACaD0AAmg/AAJoQgACaEQAAmhGAAJoSAACaEsAAmhOAAJoUAACaFIAAmhVAAJoVwACaFkAAmhcAAJoXwACaGAAAmhlAAJocgACaHUAAmh3AAJoegACaH0AAmh/AAJopAACaMgAAmjvAAJpEwACaRYAAmkYAAJpGgACaRwAAmkeAAJpIAACaSEAAmkkAAJpMQACaUQAAmlGAAJpSAACaUoAAmlMAAJpTgACaVAAAmlSAAJpVAACaVYAAmlpAAJpbAACaW8AAmlyAAJpdQACaXgAAml7AAJpfgACaYEAAmmEAAJphgACacUAAmnHAAJpygACacwAAmnPAAJp0AACadEAAmnSAAJp0wACadUAAmnXAAJp2AACadkAAmnbAAJp3AACaeUAAmnmAAJp6AACaicAAmopAAJqKwACai0AAmowAAJqMQACajIAAmozAAJqNAACajYAAmo4AAJqOQACajoAAmo8AAJqPQACanwAAmp+AAJqgQACaoMAAmqGAAJqhwACaogAAmqJAAJqigACaowAAmqOAAJqjwACapAAAmqSAAJqkwACapwAAmqdAAJqnwACat4AAmrgAAJq4gACauQAAmrnAAJq6AACaukAAmrqAAJq6wACau0AAmrvAAJq8AACavEAAmrzAAJq9AACazMAAms1AAJrOAACazoAAms9AAJrPgACaz8AAmtAAAJrQQACa0MAAmtFAAJrRgACa0cAAmtJAAJrSgACa1MAAmtUAAJrVgACa5UAAmuXAAJrmQACa5sAAmueAAJrnwACa6AAAmuhAAJrogACa6QAAmumAAJrpwACa6gAAmuqAAJrqwACa+oAAmvsAAJr7wACa/EAAmv0AAJr9QACa/YAAmv3AAJr+AACa/oAAmv8AAJr/QACa/4AAmwAAAJsAQACbA4AAmwPAAJsEAACbBIAAmxRAAJsUwACbFUAAmxXAAJsWgACbFsAAmxcAAJsXQACbF4AAmxgAAJsYgACbGMAAmxkAAJsZgACbGcAAmymAAJsqAACbKsAAmytAAJssAACbLEAAmyyAAJsswACbLQAAmy2AAJsuAACbLkAAmy6AAJsvAACbL0AAmzZAAJs5gACbPEAAmz0AAJs9wACbPoAAmz9AAJtAAACbQsAAm0OAAJtEQACbRQAAm0XAAJtGgACbRwAAm0vAAJtPQACbUYAAm1YAAJtYAACbasAAm3OAAJt7gACbg4AAm4QAAJuEgACbhQAAm4WAAJuGQACbhoAAm4bAAJuHgACbh8AAm4hAAJuIgACbiQAAm4nAAJuKAACbikAAm4sAAJuLQACbjIAAm4/AAJuRAACbkYAAm5IAAJuTQACblAAAm5TAAJuVQACbnoAAm6eAAJuxQACbukAAm7sAAJu7gACbvAAAm7yAAJu9AACbvYAAm73AAJu+gACbwcAAm8YAAJvGgACbxwAAm8eAAJvIAACbyIAAm8kAAJvJgACbygAAm85AAJvPAACbz8AAm9CAAJvRQACb0gAAm9LAAJvTgACb1EAAm9TAAJvkgACb5QAAm+WAAJvmAACb5sAAm+cAAJvnQACb54AAm+fAAJvoQACb6MAAm+kAAJvpQACb6cAAm+oAAJv5wACb+kAAm/rAAJv7QACb/AAAm/xAAJv8gACb/MAAm/0AAJv9gACb/gAAm/5AAJv+gACb/wAAm/9AAJwPAACcD4AAnBBAAJwQwACcEYAAnBHAAJwSAACcEkAAnBKAAJwTAACcE4AAnBPAAJwUAACcFIAAnBTAAJwYAACcGEAAnBiAAJwZAACcKMAAnClAAJwpwACcKkAAnCsAAJwrQACcK4AAnCvAAJwsAACcLIAAnC0AAJwtQACcLYAAnC4AAJwuQACcPgAAnD6AAJw/AACcP4AAnEBAAJxAgACcQMAAnEEAAJxBQACcQcAAnEJAAJxCgACcQsAAnENAAJxDgACcU0AAnFPAAJxUQACcVMAAnFWAAJxVwACcVgAAnFZAAJxWgACcVwAAnFeAAJxXwACcWAAAnFiAAJxYwACcaIAAnGkAAJxpgACcagAAnGrAAJxrAACca0AAnGuAAJxrwACcbEAAnGzAAJxtAACcbUAAnG3AAJxuAACcfcAAnH5AAJx+wACcf0AAnIAAAJyAQACcgIAAnIDAAJyBAACcgYAAnIIAAJyCQACcgoAAnIMAAJyDQACcjIAAnJWAAJyfQACcqEAAnKkAAJypgACcqgAAnKqAAJyrAACcq4AAnKvAAJysgACcr8AAnLOAAJy0AACctIAAnLUAAJy1gACctgAAnLaAAJy3AACcusAAnLuAAJy8QACcvQAAnL3AAJy+gACcv0AAnMAAAJzAgACc0EAAnNDAAJzRQACc0cAAnNKAAJzSwACc0wAAnNNAAJzTgACc1AAAnNSAAJzUwACc1QAAnNWAAJzVwACc5YAAnOYAAJzmgACc5wAAnOfAAJzoAACc6EAAnOiAAJzowACc6UAAnOnAAJzqAACc6kAAnOrAAJzrAACc+sAAnPtAAJz7wACc/EAAnP0AAJz9QACc/YAAnP3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AAJ/jQACf48AAn/OAAJ/0AACf9IAAn/UAAJ/1wACf9gAAn/ZAAJ/2gACf9sAAn/dAAJ/3wACf+AAAn/hAAJ/4wACf+QAAoAjAAKAJQACgCcAAoApAAKALAACgC0AAoAuAAKALwACgDAAAoAyAAKANAACgDUAAoA2AAKAOAACgDkAAoB4AAKAegACgH0AAoB/AAKAggACgIMAAoCEAAKAhQACgIYAAoCIAAKAigACgIsAAoCMAAKAjgACgI8AAoCcAAKAnQACgJ4AAoCgAAKA3wACgOEAAoDjAAKA5QACgOgAAoDpAAKA6gACgOsAAoDsAAKA7gACgPAAAoDxAAKA8gACgPQAAoD1AAKBNAACgTYAAoE4AAKBOgACgT0AAoE+AAKBPwACgUAAAoFBAAKBQwACgUUAAoFGAAKBRwACgUkAAoFKAAKBiQACgYsAAoGNAAKBjwACgZIAAoGTAAKBlAACgZUAAoGWAAKBmAACgZoAAoGbAAKBnAACgZ4AAoGfAAKB3gACgeAAAoHiAAKB5AACgecAAoHoAAKB6QACgeoAAoHrAAKB7QACge8AAoHwAAKB8QACgfMAAoH0AAKCMwACgjUAAoI3AAKCOQACgjwAAoI9AAKCPgACgj8AAoJAAAKCQgACgkQAAoJFAAKCRgACgkgAAoJJAAKCbgACgpIAAoK5AAKC3QACguAAAoLiAAKC5AACguYAAoLoAAKC6gACgusAAoLuAAKC+wACgwoAAoMMAAKDDgACgxAAAoMSAAKDFAACgxYAAoMYAAKDJwACgyoAAoMtAAKDMAACgzMAAoM2AAKDOQACgzwAAoM+AAKDfQACg38AAoOBAAKDgwACg4YAAoOHAAKDiAACg4kAAoOKAAKDjAACg44AAoOPAAKDkAACg5IAAoOTAAKD0gACg9QAAoPWAAKD2AACg9sAAoPcAAKD3QACg94AAoPfAAKD4QACg+MAAoPkAAKD5QACg+cAAoPoAAKEJwAChCkAAoQrAAKELQAChDAAAoQxAAKEMgAChDMAAoQ0AAKENgAChDgAAoQ5AAKEOgAChDwAAoQ9AAKEfAAChH4AAoSAAAKEggAChIUAAoSGAAKEhwAChIgAAoSJAAKEiwAChI0AAoSOAAKEjwAChJEAAoSSAAKE0QAChNMAAoTVAAKE1wAChNoAAoTbAAKE3AAChN0AAoTeAAKE4AAChOIAAoTjAAKE5AAChOYAAoTnAAKFJgAChSgAAoUqAAKFLAAChS8AAoUwAAKFMQAChTIAAoUzAAKFNQAChTcAAoU4AAKFOQAChTsAAoU8AAKFewAChX0AAoV/AAKFgQAChYQAAoWFAAKFhgAChYcAAoWIAAKFigAChYwAAoWNAAKFjgAChZAAAoWRAAKF3AAChf8AAoYfAAKGPwAChkEAAoZDAAKGRQAChkcAAoZKAAKGSwAChkwAAoZPAAKGUAAChlIAAoZTAAKGVQAChlgAAoZZAAKGWgAChl0AAoZeAAKGYwAChnAAAoZ1AAKGdwAChnkAAoZ+AAKGgQAChoQAAoaGAAKGqwAChs8AAob2AAKHGgAChx0AAocfAAKHIQAChyMAAoclAAKHJwAChygAAocrAAKHOAACh0kAAodLAAKHTQACh08AAodRAAKHUwACh1UAAodXAAKHWQACh2oAAodtAAKHcAACh3MAAod2AAKHeQACh3wAAod/AAKHggACh4QAAofDAAKHxQACh8cAAofJAAKHzAACh80AAofOAAKHzwACh9AAAofSAAKH1AACh9UAAofWAAKH2AACh9kAAogYAAKIGgACiBwAAogeAAKIIQACiCIAAogjAAKIJAACiCUAAognAAKIKQACiCoAAogrAAKILQACiC4AAohtAAKIbwACiHIAAoh0AAKIdwACiHgAAoh5AAKIegACiHsAAoh9AAKIfwACiIAAAoiBAAKIgwACiIQAAoiRAAKIkgACiJMAAoiVAAKI1AACiNYAAojYAAKI2gACiN0AAojeAAKI3wACiOAAAojhAAKI4wACiOUAAojmAAKI5wACiOkAAojqAAKJKQACiSsAAoktAAKJLwACiTIAAokzAAKJNAACiTUAAok2AAKJOAACiToAAok7AAKJPAACiT4AAok/AAKJfgACiYAAAomCAAKJhAACiYcAAomIAAKJiQACiYoAAomLAAKJjQACiY8AAomQAAKJkQACiZMAAomUAAKJ0wACidUAAonXAAKJ2QACidwAAondAAKJ3gACid8AAongAAKJ4gACieQAAonlAAKJ5gACiegAAonpAAKKKAACiioAAoosAAKKLgACijEAAooyAAKKMwACijQAAoo1AAKKNwACijkAAoo6AAKKOwACij0AAoo+AAKKYwACiocAAoquAAKK0gACitUAAorXAAKK2QACitsAAordAAKK3wACiuAAAorjAAKK8AACiv0AAor/AAKLAQACiwMAAosFAAKLBwACiwkAAosWAAKLGQACixwAAosfAAKLIgACiyUAAosoAAKLKgACi2kAAotrAAKLbgACi3AAAotzAAKLdAACi3UAAot2AAKLdwACi3kAAot7AAKLfAACi30AAot/AAKLgAACi8MAAovnAAKMCwACjC4AAoxVAAKMdQACjJwAAozDAAKM4wACjQcAAo0rAAKNLQACjTAAAo0yAAKNNAACjTYAAo05AAKNPAACjT4AAo1AAAKNQwACjUUAAo1HAAKNSgACjU0AAo1OAAKNUwACjWAAAo1jAAKNZQACjWgAAo1rAAKNbQACjZIAAo22AAKN3QACjgEAAo4EAAKOBgACjggAAo4KAAKODAACjg4AAo4PAAKOEgACjh8AAo4yAAKONAACjjYAAo44AAKOOgACjjwAAo4+AAKOQAACjkIAAo5EAAKOVwACjloAAo5dAAKOYAACjmMAAo5mAAKOaQACjmwAAo5vAAKOcgACjnQAAo6zAAKOtQACjrgAAo66AAKOvQACjr4AAo6/AAKOwAACjsEAAo7DAAKOxQACjsYAAo7HAAKOyQACjsoAAo7TAAKO1AACjtYAAo8VAAKPFwACjxkAAo8bAAKPHgACjx8AAo8gAAKPIQACjyIAAo8kAAKPJgACjycAAo8oAAKPKgACjysAAo9qAAKPbAACj28AAo9xAAKPdAACj3UAAo92AAKPdwACj3gAAo96AAKPfAACj30AAo9+AAKPgAACj4EAAo+KAAKPiwACj40AAo/MAAKPzgACj9AAAo/SAAKP1QACj9YAAo/XAAKP2AACj9kAAo/bAAKP3QACj94AAo/fAAKP4QACj+IAApAhAAKQIwACkCYAApAoAAKQKwACkCwAApAtAAKQLgACkC8AApAxAAKQMwACkDQAApA1AAKQNwACkDgAApBBAAKQQgACkEQAApCDAAKQhQACkIcAApCJAAKQjAACkI0AApCOAAKQjwACkJAAApCSAAKQlAACkJUAApCWAAKQmAACkJkAApDYAAKQ2gACkN0AApDfAAKQ4gACkOMAApDkAAKQ5QACkOYAApDoAAKQ6gACkOsAApDsAAKQ7gACkO8AApD8AAKQ/QACkP4AApEAAAKRPwACkUEAApFDAAKRRQACkUgAApFJAAKRSgACkUsAApFMAAKRTgACkVAAApFRAAKRUgACkVQAApFVAAKRlAACkZYAApGZAAKRmwACkZ4AApGfAAKRoAACkaEAApGiAAKRpAACkaYAApGnAAKRqAACkaoAApGrAAKRugACkccAApHOAAKR0QACkdMAApHWAAKR3QACkeAAApHjAAKR5gACkegAApHwAAKR9QACkkAAApJjAAKSgwACkqMAApKlAAKSpwACkqkAApKrAAKSrgACkq8AApKwAAKSswACkrQAApK2AAKStwACkrkAApK8AAKSvQACkr4AApLBAAKSwgACkssAApLYAAKS3QACkt8AApLhAAKS5gACkukAApLsAAKS7gACkxMAApM3AAKTXgACk4IAApOFAAKThwACk4kAApOLAAKTjQACk48AApOQAAKTkwACk6AAApOxAAKTswACk7UAApO3AAKTuQACk7sAApO9AAKTvwACk8EAApPSAAKT1QACk9gAApPbAAKT3gACk+EAApPkAAKT5wACk+oAApPsAAKUKwAClC0AApQvAAKUMQAClDQAApQ1AAKUNgAClDcAApQ4AAKUOgAClDwAApQ9AAKUPgAClEAAApRBAAKUgAAClIIAApSEAAKUhgAClIkAApSKAAKUiwAClIwAApSNAAKUjwAClJEAApSSAAKUkwAClJUAApSWAAKU1QAClNcAApTaAAKU3AAClN8AApTgAAKU4QAClOIAApTjAAKU5QAClOcAApToAAKU6QAClOsAApTsAAKU+QAClPoAApT7AAKU/QAClTwAApU+AAKVQAAClUIAApVFAAKVRgAClUcAApVIAAKVSQAClUsAApVNAAKVTgAClU8AApVRAAKVUgAClZEAApWTAAKVlQAClZcAApWaAAKVmwAClZwAApWdAAKVngAClaAAApWiAAKVowAClaQAApWmAAKVpwACleYAApXoAAKV6gAClewAApXvAAKV8AAClfEAApXyAAKV8wAClfUAApX3AAKV+AAClfkAApX7AAKV/AACljsAApY9AAKWPwAClkEAApZEAAKWRQAClkYAApZHAAKWSAAClkoAApZMAAKWTQAClk4AApZQAAKWUQAClpAAApaSAAKWlAAClpYAApaZAAKWmgAClpsAApacAAKWnQAClp8AApahAAKWogAClqMAApalAAKWpgAClssAApbvAAKXFgAClzoAApc9AAKXPwACl0EAApdDAAKXRQACl0cAApdIAAKXSwACl1gAApdnAAKXaQACl2sAApdtAAKXbwACl3EAApdzAAKXdQACl4QAApeHAAKXigACl40AApeQAAKXkwACl5YAApeZAAKXmwACl9oAApfcAAKX3gACl+AAApfjAAKX5AACl+UAApfmAAKX5wACl+kAApfrAAKX7AACl+0AApfvAAKX8AACmC8AApgxAAKYMwACmDUAApg4AAKYOQACmDoAApg7AAKYPAACmD4AAphAAAKYQQACmEIAAphEAAKYRQACmIQAApiGAAKYiAACmIoAApiNAAKYjgACmI8AApiQAAKYkQACmJMAApiVAAKYlgACmJcAApiZAAKYmgACmNkAApjbAAKY3QACmN8AApjiAAKY4wACmOQAApjlAAKY5gACmOgAApjqAAKY6wACmOwAApjuAAKY7wACmS4AApkwAAKZMgACmTQAApk3AAKZOAACmTkAApk6AAKZOwACmT0AApk/AAKZQAACmUEAAplDAAKZRAACmYMAApmFAAKZhwACmYkAApmMAAKZjQACmY4AApmPAAKZkAACmZIAApmUAAKZlQACmZYAApmYAAKZmQACmdgAApnaAAKZ3AACmd4AApnhAAKZ4gACmeMAApnkAAKZ5QACmecAApnpAAKZ6gACmesAApntAAKZ7gACmjkAAppcAAKafAACmpwAApqeAAKaoAACmqIAApqkAAKapwACmqgAApqpAAKarAACmq0AApqvAAKasAACmrIAApq0AAKatQACmrYAApq5AAKaugACmr8AAprMAAKa0QACmtMAAprVAAKa2gACmt0AAprgAAKa4gACmwcAApsrAAKbUgACm3YAApt5AAKbewACm30AApt/AAKbgQACm4MAApuEAAKbhwACm5QAApulAAKbpwACm6kAApurAAKbrQACm68AApuxAAKbswACm7UAApvGAAKbyQACm8wAApvPAAKb0gACm9UAApvYAAKb2wACm94AApvgAAKcHwACnCEAApwjAAKcJQACnCgAApwpAAKcKgACnCsAApwsAAKcLgACnDAAApwxAAKcMgACnDQAApw1AAKcdAACnHYAApx4AAKcegACnH0AApx+AAKcfwACnIAAApyBAAKcgwACnIUAApyGAAKchwACnIkAApyKAAKcyQACnMsAApzOAAKc0AACnNMAApzUAAKc1QACnNYAApzXAAKc2QACnNsAApzcAAKc3QACnN8AApzgAAKc7QACnO4AApzvAAKc8QACnTAAAp0yAAKdNAACnTYAAp05AAKdOgACnTsAAp08AAKdPQACnT8AAp1BAAKdQgACnUMAAp1FAAKdRgACnYUAAp2HAAKdiQACnYsAAp2OAAKdjwACnZAAAp2RAAKdkgACnZQAAp2WAAKdlwACnZgAAp2aAAKdmwACndoAAp3cAAKd3gACneAAAp3jAAKd5AACneUAAp3mAAKd5wACnekAAp3rAAKd7AACne0AAp3vAAKd8AACni8AAp4xAAKeMwACnjUAAp44AAKeOQACnjoAAp47AAKePAACnj4AAp5AAAKeQQACnkIAAp5EAAKeRQACnoQAAp6GAAKeiAACnooAAp6NAAKejgACno8AAp6QAAKekQACnpMAAp6VAAKelgACnpcAAp6ZAAKemgACnr8AAp7jAAKfCgACny4AAp8xAAKfMwACnzUAAp83AAKfOQACnzsAAp88AAKfPwACn0wAAp9bAAKfXQACn18AAp9hAAKfYwACn2UAAp9nAAKfaQACn3gAAp97AAKffgACn4EAAp+EAAKfhwACn4oAAp+NAAKfjwACn84AAp/QAAKf0gACn9QAAp/XAAKf2AACn9kAAp/aAAKf2wACn90AAp/fAAKf4AACn+EAAp/jAAKf5AACoCMAAqAlAAKgJwACoCkAAqAsAAKgLQACoC4AAqAvAAKgMAACoDIAAqA0AAKgNQACoDYAAqA4AAKgOQACoHgAAqB6AAKgfAACoH4AAqCBAAKgggACoIMAAqCEAAKghQACoIcAAqCJAAKgigACoIsAAqCNAAKgjgACoM0AAqDPAAKg0QACoNMAAqDWAAKg1wACoNgAAqDZAAKg2gACoNwAAqDeAAKg3wACoOAAAqDiAAKg4wACoSIAAqEkAAKhJgACoSgAAqErAAKhLAACoS0AAqEuAAKhLwACoTEAAqEzAAKhNAACoTUAAqE3AAKhOAACoXcAAqF5AAKhewACoX0AAqGAAAKhgQACoYIAAqGDAAKhhAACoYYAAqGIAAKhiQACoYoAAqGMAAKhjQACocwAAqHOAAKh0AACodIAAqHVAAKh1gACodcAAqHYAAKh2QACodsAAqHdAAKh3gACod8AAqHhAAKh4gACoi0AAqJQAAKicAACopAAAqKSAAKilAACopYAAqKYAAKimwACopwAAqKdAAKioAACoqEAAqKjAAKipAACoqYAAqKpAAKiqgACoqsAAqKuAAKirwACorQAAqLBAAKixgACosgAAqLKAAKizwACotIAAqLVAAKi1wACovwAAqMgAAKjRwACo2sAAqNuAAKjcAACo3IAAqN0AAKjdgACo3gAAqN5AAKjfAACo4kAAqOaAAKjnAACo54AAqOgAAKjogACo6QAAqOmAAKjqAACo6oAAqO7AAKjvgACo8EAAqPEAAKjxwACo8oAAqPNAAKj0AACo9MAAqPVAAKkFAACpBYAAqQYAAKkGgACpB0AAqQeAAKkHwACpCAAAqQhAAKkIwACpCUAAqQmAAKkJwACpCkAAqQqAAKkaQACpGsAAqRtAAKkbwACpHIAAqRzAAKkdAACpHUAAqR2AAKkeAACpHoAAqR7AAKkfAACpH4AAqR/AAKkvgACpMAAAqTDAAKkxQACpMgAAqTJAAKkygACpMsAAqTMAAKkzgACpNAAAqTRAAKk0gACpNQAAqTVAAKk4gACpOMAAqTkAAKk5gACpSUAAqUnAAKlKQACpSsAAqUuAAKlLwACpTAAAqUxAAKlMgACpTQAAqU2AAKlNwACpTgAAqU6AAKlOwACpXoAAqV8AAKlfgACpYAAAqWDAAKlhAACpYUAAqWGAAKlhwACpYkAAqWLAAKljAACpY0AAqWPAAKlkAACpc8AAqXRAAKl0wACpdUAAqXYAAKl2QACpdoAAqXbAAKl3AACpd4AAqXgAAKl4QACpeIAAqXkAAKl5QACpiQAAqYmAAKmKAACpioAAqYtAAKmLgACpi8AAqYwAAKmMQACpjMAAqY1AAKmNgACpjcAAqY5AAKmOgACpnkAAqZ7AAKmfQACpn8AAqaCAAKmgwACpoQAAqaFAAKmhgACpogAAqaKAAKmiwACpowAAqaOAAKmjwACprQAAqbYAAKm/wACpyMAAqcmAAKnKAACpyoAAqcsAAKnLgACpzAAAqcxAAKnNAACp0EAAqdQAAKnUgACp1QAAqdWAAKnWAACp1oAAqdcAAKnXgACp20AAqdwAAKncwACp3YAAqd5AAKnfAACp38AAqeCAAKnhAACp8MAAqfFAAKnxwACp8kAAqfMAAKnzQACp84AAqfPAAKn0AACp9IAAqfUAAKn1QACp9YAAqfYAAKn2QACqBgAAqgaAAKoHAACqB4AAqghAAKoIgACqCMAAqgkAAKoJQACqCcAAqgpAAKoKgACqCsAAqgtAAKoLgACqG0AAqhvAAKocQACqHMAAqh2AAKodwACqHgAAqh5AAKoegACqHwAAqh+AAKofwACqIAAAqiCAAKogwACqMIAAqjEAAKoxwACqMkAAqjMAAKozQACqM4AAqjPAAKo0AACqNIAAqjUAAKo1QACqNYAAqjYAAKo2QACqNsAAqkaAAKpHAACqR4AAqkgAAKpIwACqSQAAqklAAKpJgACqScAAqkpAAKpKwACqSwAAqktAAKpLwACqTAAAqlvAAKpcQACqXMAAql1AAKpeAACqXkAAql6AAKpewACqXwAAql+AAKpgAACqYEAAqmCAAKphAACqYUAAqnEAAKpxgACqcgAAqnKAAKpzQACqc4AAqnPAAKp0AACqdEAAqnTAAKp1QACqdYAAqnXAAKp2QACqdoAAqnjAAKp5AACqeYAAqolAAKqJwACqikAAqorAAKqLgACqi8AAqowAAKqMQACqjIAAqo0AAKqNgACqjcAAqo4AAKqOgACqjsAAqp6AAKqfAACqn4AAqqAAAKqgwACqoQAAqqFAAKqhgACqocAAqqJAAKqiwACqowAAqqNAAKqjwACqpAAAqrPAAKq0QACqtMAAqrVAAKq2AACqtkAAqraAAKq2wACqtwAAqreAAKq4AACquEAAqriAAKq5AACquUAAqskAAKrJgACqygAAqsqAAKrLQACqy4AAqsvAAKrMAACqzEAAqszAAKrNQACqzYAAqs3AAKrOQACqzoAAqt5AAKrewACq30AAqt/AAKrggACq4MAAquEAAKrhQACq4YAAquIAAKrigACq4sAAquMAAKrjgACq48AAqvaAAKr/QACrB0AAqw9AAKsPwACrEEAAqxDAAKsRQACrEgAAqxJAAKsSgACrE0AAqxOAAKsUAACrFEAAqxTAAKsVgACrFcAAqxYAAKsWwACrFwAAqxhAAKsbgACrHMAAqx1AAKsdwACrHwAAqx/AAKsggACrIQAAqypAAKszQACrPQAAq0YAAKtGwACrR0AAq0fAAKtIQACrSMAAq0lAAKtJgACrSkAAq02AAKtRwACrUkAAq1LAAKtTQACrU8AAq1RAAKtUwACrVUAAq1XAAKtaAACrWsAAq1uAAKtcQACrXQAAq13AAKtegACrX0AAq2AAAKtggACrcEAAq3DAAKtxQACrccAAq3KAAKtywACrcwAAq3NAAKtzgACrdAAAq3SAAKt0wACrdQAAq3WAAKt1wACrhYAAq4YAAKuGgACrhwAAq4fAAKuIAACriEAAq4iAAKuIwACriUAAq4nAAKuKAACrikAAq4rAAKuLAACrmsAAq5tAAKucAACrnIAAq51AAKudgACrncAAq54AAKueQACrnsAAq59AAKufgACrn8AAq6BAAKuggACro8AAq6QAAKukQACrpMAAq7SAAKu1AACrtYAAq7YAAKu2wACrtwAAq7dAAKu3gACrt8AAq7hAAKu4wACruQAAq7lAAKu5wACrugAAq8nAAKvKQACrysAAq8tAAKvMAACrzEAAq8yAAKvMwACrzQAAq82AAKvOAACrzkAAq86AAKvPAACrz0AAq98AAKvfgACr4AAAq+CAAKvhQACr4YAAq+HAAKviAACr4kAAq+LAAKvjQACr44AAq+PAAKvkQACr5IAAq/RAAKv0wACr9UAAq/XAAKv2gACr9sAAq/cAAKv3QACr94AAq/gAAKv4gACr+MAAq/kAAKv5gACr+cAArAmAAKwKAACsCoAArAsAAKwLwACsDAAArAxAAKwMgACsDMAArA1AAKwNwACsDgAArA5AAKwOwACsDwAArBhAAKwhQACsKwAArDQAAKw0wACsNUAArDXAAKw2QACsNsAArDdAAKw3gACsOEAArDuAAKw/QACsP8AArEBAAKxAwACsQUAArEHAAKxCQACsQsAArEaAAKxHQACsSAAArEjAAKxJgACsSkAArEsAAKxLwACsTEAArFwAAKxcgACsXQAArF2AAKxeQACsXoAArF7AAKxfAACsX0AArF/AAKxgQACsYIAArGDAAKxhQACsYYAArHFAAKxxwACsckAArHLAAKxzgACsc8AArHQAAKx0QACsdIAArHUAAKx1gACsdcAArHYAAKx2gACsdsAArIaAAKyHAACsh4AArIgAAKyIwACsiQAArIlAAKyJgACsicAArIpAAKyKwACsiwAArItAAKyLwACsjAAArJvAAKycQACsnMAArJ1AAKyeAACsnkAArJ6AAKyewACsnwAArJ+AAKygAACsoEAArKCAAKyhAACsoUAArLEAAKyxgACssgAArLKAAKyzQACss4AArLPAAKy0AACstEAArLTAAKy1QACstYAArLXAAKy2QACstoAArMZAAKzGwACsx0AArMfAAKzIgACsyMAArMkAAKzJQACsyYAArMoAAKzKgACsysAArMsAAKzLgACsy8AArNuAAKzcAACs3IAArN0AAKzdwACs3gAArN5AAKzegACs3sAArN9AAKzfwACs4AAArOBAAKzgwACs4QAArONAAKzjgACs5AAArPTAAKz9wACtBsAArQ+AAK0ZQACtIUAArSsAAK00wACtPMAArUXAAK1OwACtT0AArVAAAK1QgACtUQAArVGAAK1SQACtUwAArVOAAK1UAACtVMAArVVAAK1VwACtVoAArVdAAK1XgACtWMAArVwAAK1cwACtXUAArV4AAK1ewACtX0AArWiAAK1xgACte0AArYRAAK2FAACthYAArYYAAK2GgACthwAArYeAAK2HwACtiIAArYvAAK2QgACtkQAArZGAAK2SAACtkoAArZMAAK2TgACtlAAArZSAAK2VAACtmcAArZqAAK2bQACtnAAArZzAAK2dgACtnkAArZ8AAK2fwACtoIAAraEAAK2wwACtsUAArbIAAK2ygACts0AArbOAAK2zwACttAAArbRAAK20wACttUAArbWAAK21wACttkAArbaAAK24wACtuQAArbmAAK3JQACtycAArcpAAK3KwACty4AArcvAAK3MAACtzEAArcyAAK3NAACtzYAArc3AAK3OAACtzoAArc7AAK3egACt3wAArd/AAK3gQACt4QAAreFAAK3hgACt4cAAreIAAK3igACt4wAAreNAAK3jgACt5AAAreRAAK3mgACt5sAAredAAK33AACt94AArfgAAK34gACt+UAArfmAAK35wACt+gAArfpAAK36wACt+0AArfuAAK37wACt/EAArfyAAK4MQACuDMAArg2AAK4OAACuDsAArg8AAK4PQACuD4AArg/AAK4QQACuEMAArhEAAK4RQACuEcAArhIAAK4UQACuFIAArhUAAK4kwACuJUAAriXAAK4mQACuJwAAridAAK4ngACuJ8AArigAAK4ogACuKQAArilAAK4pgACuKgAAripAAK46AACuOoAArjtAAK47wACuPIAArjzAAK49AACuPUAArj2AAK4+AACuPoAArj7AAK4/AACuP4AArj/AAK5DAACuQ0AArkOAAK5EAACuU8AArlRAAK5UwACuVUAArlYAAK5WQACuVoAArlbAAK5XAACuV4AArlgAAK5YQACuWIAArlkAAK5ZQACuaQAArmmAAK5qQACuasAArmuAAK5rwACubAAArmxAAK5sgACubQAArm2AAK5twACubgAArm6AAK5uwACuccAArnUAAK52QACudwAArnfAAK55AACuecAArnqAAK57AACufIAArn9AAK6SAACumsAArqLAAK6qwACuq0AArqvAAK6sQACurMAArq2AAK6twACurgAArq7AAK6vAACur4AArq/AAK6wQACusQAArrFAAK6xgACuskAArrKAAK6zwACutwAArrhAAK64wACuuUAArrqAAK67QACuvAAArryAAK7FwACuzsAArtiAAK7hgACu4kAAruLAAK7jQACu48AAruRAAK7kwACu5QAAruXAAK7pAACu7UAAru3AAK7uQACu7sAAru9AAK7vwACu8EAArvDAAK7xQACu9YAArvZAAK73AACu98AArviAAK75QACu+gAArvrAAK77gACu/AAArwvAAK8MQACvDMAArw1AAK8OAACvDkAArw6AAK8OwACvDwAArw+AAK8QAACvEEAArxCAAK8RAACvEUAAryEAAK8hgACvIgAAryKAAK8jQACvI4AAryPAAK8kAACvJEAAryTAAK8lQACvJYAAryXAAK8mQACvJoAArzZAAK82wACvN4AArzgAAK84wACvOQAArzlAAK85gACvOcAArzpAAK86wACvOwAArztAAK87wACvPAAArz9AAK8/gACvP8AAr0BAAK9QAACvUIAAr1EAAK9RgACvUkAAr1KAAK9SwACvUwAAr1NAAK9TwACvVEAAr1SAAK9UwACvVUAAr1WAAK9lQACvZcAAr2ZAAK9mwACvZ4AAr2fAAK9oAACvaEAAr2iAAK9pAACvaYAAr2nAAK9qAACvaoAAr2rAAK96gACvewAAr3uAAK98AACvfMAAr30AAK99QACvfYAAr33AAK9+QACvfsAAr38AAK9/QACvf8AAr4AAAK+PwACvkEAAr5DAAK+RQACvkgAAr5JAAK+SgACvksAAr5MAAK+TgACvlAAAr5RAAK+UgACvlQAAr5VAAK+lAACvpYAAr6YAAK+mgACvp0AAr6eAAK+nwACvqAAAr6hAAK+owACvqUAAr6mAAK+pwACvqkAAr6qAAK+zwACvvMAAr8aAAK/PgACv0EAAr9DAAK/RQACv0cAAr9JAAK/SwACv0wAAr9PAAK/XAACv2sAAr9tAAK/bwACv3EAAr9zAAK/dQACv3cAAr95AAK/iAACv4sAAr+OAAK/kQACv5QAAr+XAAK/mgACv50AAr+fAAK/3gACv+AAAr/iAAK/5AACv+cAAr/oAAK/6QACv+oAAr/rAAK/7QACv+8AAr/wAAK/8QACv/MAAr/0AALAMwACwDUAAsA3AALAOQACwDwAAsA9AALAPgACwD8AAsBAAALAQgACwEQAAsBFAALARgACwEgAAsBJAALAiAACwIoAAsCMAALAjgACwJEAAsCSAALAkwACwJQAAsCVAALAlwACwJkAAsCaAALAmwACwJ0AAsCeAALA3QACwN8AAsDhAALA4wACwOYAAsDnAALA6AACwOkAAsDqAALA7AACwO4AAsDvAALA8AACwPIAAsDzAALBMgACwTQAAsE2AALBOAACwTsAAsE8AALBPQACwT4AAsE/AALBQQACwUMAAsFEAALBRQACwUcAAsFIAALBhwACwYkAAsGLAALBjQACwZAAAsGRAALBkgACwZMAAsGUAALBlgACwZgAAsGZAALBmgACwZwAAsGdAALB3AACwd4AAsHgAALB4gACweUAAsHmAALB5wACwegAAsHpAALB6wACwe0AAsHuAALB7wACwfEAAsHyAALCPQACwmAAAsKAAALCoAACwqIAAsKkAALCpgACwqgAAsKrAALCrAACwq0AAsKwAALCsQACwrMAAsK0AALCtgACwrkAAsK6AALCuwACwr4AAsK/AALCxAACwtEAAsLWAALC2AACwtoAAsLfAALC4gACwuUAAsLnAALDDAACwzAAAsNXAALDewACw34AAsOAAALDggACw4QAAsOGAALDiAACw4kAAsOMAALDmQACw6oAAsOsAALDrgACw7AAAsOyAALDtAACw7YAAsO4AALDugACw8sAAsPOAALD0QACw9QAAsPXAALD2gACw90AAsPgAALD4wACw+UAAsQkAALEJgACxCgAAsQqAALELQACxC4AAsQvAALEMAACxDEAAsQzAALENQACxDYAAsQ3AALEOQACxDoAAsR5AALEewACxH0AAsR/AALEggACxIMAAsSEAALEhQACxIYAAsSIAALEigACxIsAAsSMAALEjgACxI8AAsTOAALE0AACxNMAAsTVAALE2AACxNkAAsTaAALE2wACxNwAAsTeAALE4AACxOEAAsTiAALE5AACxOUAAsTyAALE8wACxPQAAsT2AALFNQACxTcAAsU5AALFOwACxT4AAsU/AALFQAACxUEAAsVCAALFRAACxUYAAsVHAALFSAACxUoAAsVLAALFigACxYwAAsWOAALFkAACxZMAAsWUAALFlQACxZYAAsWXAALFmQACxZsAAsWcAALFnQACxZ8AAsWgAALF3wACxeEAAsXjAALF5QACxegAAsXpAALF6gACxesAAsXsAALF7gACxfAAAsXxAALF8gACxfQAAsX1AALGNAACxjYAAsY4AALGOgACxj0AAsY+AALGPwACxkAAAsZBAALGQwACxkUAAsZGAALGRwACxkkAAsZKAALGiQACxosAAsaNAALGjwACxpIAAsaTAALGlAACxpUAAsaWAALGmAACxpoAAsabAALGnAACxp4AAsafAALGxAACxugAAscPAALHMwACxzYAAsc4AALHOgACxzwAAsc+AALHQAACx0EAAsdEAALHUQACx2AAAsdiAALHZAACx2YAAsdoAALHagACx2wAAsduAALHfQACx4AAAseDAALHhgACx4kAAseMAALHjwACx5IAAseUAALH0wACx9UAAsfXAALH2QACx9wAAsfdAALH3gACx98AAsfgAALH4gACx+QAAsflAALH5gACx+gAAsfpAALIKAACyCoAAsgsAALILgACyDEAAsgyAALIMwACyDQAAsg1AALINwACyDkAAsg6AALIOwACyD0AAsg+AALIfQACyH8AAsiBAALIgwACyIYAAsiHAALIiAACyIkAAsiKAALIjAACyI4AAsiPAALIkAACyJIAAsiTAALI0gACyNQAAsjWAALI2AACyNsAAsjcAALI3QACyN4AAsjfAALI4QACyOMAAsjkAALI5QACyOcAAsjoAALJJwACySkAAskrAALJLQACyTAAAskxAALJMgACyTMAAsk0AALJNgACyTgAAsk5AALJOgACyTwAAsk9AALJfAACyX4AAsmAAALJggACyYUAAsmGAALJhwACyYgAAsmJAALJiwACyY0AAsmOAALJjwACyZEAAsmSAALJ0QACydMAAsnVAALJ1wACydoAAsnbAALJ3AACyd0AAsneAALJ4AACyeIAAsnjAALJ5AACyeYAAsnnAALJ8AACyfEAAsnzAALKNgACyloAAsp+AALKoQACysgAAsroAALLDwACyzYAAstWAALLegACy54AAsugAALLowACy6UAAsunAALLqQACy6wAAsuvAALLsQACy7MAAsu2AALLuAACy7oAAsu9AALLwAACy8EAAsvGAALL0wACy9YAAsvYAALL2wACy94AAsvgAALMBQACzCkAAsxQAALMdAACzHcAAsx5AALMewACzH0AAsx/AALMgQACzIIAAsyFAALMkgACzKUAAsynAALMqQACzKsAAsytAALMrwACzLEAAsyzAALMtQACzLcAAszKAALMzQACzNAAAszTAALM1gACzNkAAszcAALM3wACzOIAAszlAALM5wACzSYAAs0oAALNKwACzS0AAs0wAALNMQACzTIAAs0zAALNNAACzTYAAs04AALNOQACzToAAs08AALNPQACzUYAAs1HAALNSQACzYgAAs2KAALNjAACzY4AAs2RAALNkgACzZMAAs2UAALNlQACzZcAAs2ZAALNmgACzZsAAs2dAALNngACzd0AAs3fAALN4gACzeQAAs3nAALN6AACzekAAs3qAALN6wACze0AAs3vAALN8AACzfEAAs3zAALN9AACzf0AAs3+AALOAAACzj8AAs5BAALOQwACzkUAAs5IAALOSQACzkoAAs5LAALOTAACzk4AAs5QAALOUQACzlIAAs5UAALOVQACzpQAAs6WAALOmQACzpsAAs6eAALOnwACzqAAAs6hAALOogACzqQAAs6mAALOpwACzqgAAs6qAALOqwACzrQAAs61AALOtwACzvYAAs74AALO+gACzvwAAs7/AALPAAACzwEAAs8CAALPAwACzwUAAs8HAALPCAACzwkAAs8LAALPDAACz0sAAs9NAALPUAACz1IAAs9VAALPVgACz1cAAs9YAALPWQACz1sAAs9dAALPXgACz18AAs9hAALPYgACz28AAs9wAALPcQACz3MAAs+yAALPtAACz7YAAs+4AALPuwACz7wAAs+9AALPvgACz78AAs/BAALPwwACz8QAAs/FAALPxwACz8gAAtAHAALQCQAC0AwAAtAOAALQEQAC0BIAAtATAALQFAAC0BUAAtAXAALQGQAC0BoAAtAbAALQHQAC0B4AAtA2AALQQwAC0E4AAtBRAALQVAAC0FcAAtBaAALQXQAC0GgAAtBrAALQbgAC0HEAAtB0AALQdwAC0HkAAtCCAALQiwAC0JUAAtDgAALRAwAC0SMAAtFDAALRRQAC0UcAAtFJAALRSwAC0U4AAtFPAALRUAAC0VMAAtFUAALRVgAC0VcAAtFZAALRXAAC0V0AAtFeAALRYQAC0WIAAtFnAALRdAAC0XkAAtF7AALRfQAC0YIAAtGFAALRiAAC0YoAAtGvAALR0wAC0foAAtIeAALSIQAC0iMAAtIlAALSJwAC0ikAAtIrAALSLAAC0i8AAtI8AALSTQAC0k8AAtJRAALSUwAC0lUAAtJXAALSWQAC0lsAAtJdAALSbgAC0nEAAtJ0AALSdwAC0noAAtJ9AALSgAAC0oMAAtKGAALSiAAC0scAAtLJAALSywAC0s0AAtLQAALS0QAC0tIAAtLTAALS1AAC0tYAAtLYAALS2QAC0toAAtLcAALS3QAC0xwAAtMeAALTIAAC0yIAAtMlAALTJgAC0ycAAtMoAALTKQAC0ysAAtMtAALTLgAC0y8AAtMxAALTMgAC03EAAtNzAALTdgAC03gAAtN7AALTfAAC030AAtN+AALTfwAC04EAAtODAALThAAC04UAAtOHAALTiAAC05UAAtOWAALTlwAC05kAAtPYAALT2gAC09wAAtPeAALT4QAC0+IAAtPjAALT5AAC0+UAAtPnAALT6QAC0+oAAtPrAALT7QAC0+4AAtQtAALULwAC1DEAAtQzAALUNgAC1DcAAtQ4AALUOQAC1DoAAtQ8AALUPgAC1D8AAtRAAALUQgAC1EMAAtSCAALUhAAC1IYAAtSIAALUiwAC1IwAAtSNAALUjgAC1I8AAtSRAALUkwAC1JQAAtSVAALUlwAC1JgAAtTXAALU2QAC1NsAAtTdAALU4AAC1OEAAtTiAALU4wAC1OQAAtTmAALU6AAC1OkAAtTqAALU7AAC1O0AAtUsAALVLgAC1TAAAtUyAALVNQAC1TYAAtU3AALVOAAC1TkAAtU7AALVPQAC1T4AAtU/AALVQQAC1UIAAtVnAALViwAC1bIAAtXWAALV2QAC1dsAAtXdAALV3wAC1eEAAtXjAALV5AAC1ecAAtX0AALWAwAC1gUAAtYHAALWCQAC1gsAAtYNAALWDwAC1hEAAtYgAALWIwAC1iYAAtYpAALWLAAC1i8AAtYyAALWNQAC1jcAAtZ2AALWeAAC1noAAtZ8AALWfwAC1oAAAtaBAALWggAC1oMAAtaFAALWhwAC1ogAAtaJAALWiwAC1owAAtbLAALWzQAC1s8AAtbRAALW1AAC1tUAAtbWAALW1wAC1tgAAtbaAALW3AAC1t0AAtbeAALW4AAC1uEAAtcgAALXIgAC1yQAAtcmAALXKQAC1yoAAtcrAALXLAAC1y0AAtcvAALXMQAC1zIAAtczAALXNQAC1zYAAtd1AALXdwAC13oAAtd8AALXfwAC14AAAteBAALXggAC14MAAteFAALXhwAC14gAAteJAALXiwAC14wAAtfLAALXzQAC188AAtfRAALX1AAC19UAAtfWAALX1wAC19gAAtfaAALX3AAC190AAtfeAALX4AAC1+EAAtggAALYIgAC2CQAAtgmAALYKQAC2CoAAtgrAALYLAAC2C0AAtgvAALYMQAC2DIAAtgzAALYNQAC2DYAAth1AALYdwAC2HkAAth7AALYfgAC2H8AAtiAAALYgQAC2IIAAtiEAALYhgAC2IcAAtiIAALYigAC2IsAAtjWAALY+QAC2RkAAtk5AALZOwAC2T0AAtk/AALZQQAC2UQAAtlFAALZRgAC2UkAAtlKAALZTAAC2U0AAtlPAALZUgAC2VMAAtlUAALZVwAC2VgAAtlhAALZbgAC2XMAAtl1AALZdwAC2XwAAtl/AALZggAC2YQAAtmpAALZzQAC2fQAAtoYAALaGwAC2h0AAtofAALaIQAC2iMAAtolAALaJgAC2ikAAto2AALaRwAC2kkAAtpLAALaTQAC2k8AAtpRAALaUwAC2lUAAtpXAALaaAAC2msAAtpuAALacQAC2nQAAtp3AALaegAC2n0AAtqAAALaggAC2sEAAtrDAALaxQAC2scAAtrKAALaywAC2swAAtrNAALazgAC2tAAAtrSAALa0wAC2tQAAtrWAALa1wAC2xYAAtsYAALbGgAC2xwAAtsfAALbIAAC2yEAAtsiAALbIwAC2yUAAtsnAALbKAAC2ykAAtsrAALbLAAC22sAAtttAALbcAAC23IAAtt1AALbdgAC23cAAtt4AALbeQAC23sAAtt9AALbfgAC238AAtuBAALbggAC248AAtuQAALbkQAC25MAAtvSAALb1AAC29YAAtvYAALb2wAC29wAAtvdAALb3gAC298AAtvhAALb4wAC2+QAAtvlAALb5wAC2+gAAtwnAALcKQAC3CsAAtwtAALcMAAC3DEAAtwyAALcMwAC3DQAAtw2AALcOAAC3DkAAtw6AALcPAAC3D0AAtx8AALcfgAC3IAAAtyCAALchQAC3IYAAtyHAALciAAC3IkAAtyLAALcjQAC3I4AAtyPAALckQAC3JIAAtzRAALc0wAC3NUAAtzXAALc2gAC3NsAAtzcAALc3QAC3N4AAtzgAALc4gAC3OMAAtzkAALc5gAC3OcAAt0mAALdKAAC3SoAAt0sAALdLwAC3TAAAt0xAALdMgAC3TMAAt01AALdNwAC3TgAAt05AALdOwAC3TwAAt1hAALdhQAC3awAAt3QAALd0wAC3dUAAt3XAALd2QAC3dsAAt3dAALd3gAC3eEAAt3uAALd/QAC3f8AAt4BAALeAwAC3gUAAt4HAALeCQAC3gsAAt4aAALeHQAC3iAAAt4jAALeJgAC3ikAAt4sAALeLwAC3jEAAt5wAALecgAC3nQAAt52AALeeQAC3noAAt57AALefAAC3n0AAt5/AALegQAC3oIAAt6DAALehQAC3oYAAt7FAALexwAC3skAAt7LAALezgAC3s8AAt7QAALe0QAC3tIAAt7UAALe1gAC3tcAAt7YAALe2gAC3tsAAt8aAALfHAAC3x4AAt8gAALfIwAC3yQAAt8lAALfJgAC3ycAAt8pAALfKwAC3ywAAt8tAALfLwAC3zAAAt9vAALfcQAC33QAAt92AALfeQAC33oAAt97AALffAAC330AAt9/AALfgQAC34IAAt+DAALfhQAC34YAAt/FAALfxwAC38kAAt/LAALfzgAC388AAt/QAALf0QAC39IAAt/UAALf1gAC39cAAt/YAALf2gAC39sAAuAaAALgHAAC4B4AAuAgAALgIwAC4CQAAuAlAALgJgAC4CcAAuApAALgKwAC4CwAAuAtAALgLwAC4DAAAuBvAALgcQAC4HMAAuB1AALgeAAC4HkAAuB6AALgewAC4HwAAuB+AALggAAC4IEAAuCCAALghAAC4IUAAuDQAALg8wAC4RMAAuEzAALhNQAC4TcAAuE5AALhOwAC4T4AAuE/AALhQAAC4UMAAuFEAALhRgAC4UcAAuFJAALhTAAC4U0AAuFOAALhUQAC4VIAAuFXAALhZAAC4WkAAuFrAALhbQAC4XIAAuF1AALheAAC4XoAAuGfAALhwwAC4eoAAuIOAALiEQAC4hMAAuIVAALiFwAC4hkAAuIbAALiHAAC4h8AAuIsAALiPQAC4j8AAuJBAALiQwAC4kUAAuJHAALiSQAC4ksAAuJNAALiXgAC4mEAAuJkAALiZwAC4moAAuJtAALicAAC4nMAAuJ2AALieAAC4rcAAuK5AALiuwAC4r0AAuLAAALiwQAC4sIAAuLDAALixAAC4sYAAuLIAALiyQAC4soAAuLMAALizQAC4wwAAuMOAALjEAAC4xIAAuMVAALjFgAC4xcAAuMYAALjGQAC4xsAAuMdAALjHgAC4x8AAuMhAALjIgAC42EAAuNjAALjZgAC42gAAuNrAALjbAAC420AAuNuAALjbwAC43EAAuNzAALjdAAC43UAAuN3AALjeAAC44UAAuOGAALjhwAC44kAAuPIAALjygAC48wAAuPOAALj0QAC49IAAuPTAALj1AAC49UAAuPXAALj2QAC49oAAuPbAALj3QAC494AAuQdAALkHwAC5CEAAuQjAALkJgAC5CcAAuQoAALkKQAC5CoAAuQsAALkLgAC5C8AAuQwAALkMgAC5DMAAuRyAALkdAAC5HYAAuR4AALkewAC5HwAAuR9AALkfgAC5H8AAuSBAALkgwAC5IQAAuSFAALkhwAC5IgAAuTHAALkyQAC5MsAAuTNAALk0AAC5NEAAuTSAALk0wAC5NQAAuTWAALk2AAC5NkAAuTaAALk3AAC5N0AAuUcAALlHgAC5SAAAuUiAALlJQAC5SYAAuUnAALlKAAC5SkAAuUrAALlLQAC5S4AAuUvAALlMQAC5TIAAuVXAALlewAC5aIAAuXGAALlyQAC5csAAuXNAALlzwAC5dEAAuXTAALl1AAC5dcAAuXkAALl8wAC5fUAAuX3AALl+QAC5fsAAuX9AALl/wAC5gEAAuYQAALmEwAC5hYAAuYZAALmHAAC5h8AAuYiAALmJQAC5icAAuZmAALmaAAC5moAAuZsAALmbwAC5nAAAuZxAALmcgAC5nMAAuZ1AALmdwAC5ngAAuZ5AALmewAC5nwAAua7AALmvQAC5r8AAubBAALmxAAC5sUAAubGAALmxwAC5sgAAubKAALmzAAC5s0AAubOAALm0AAC5tEAAucQAALnEgAC5xQAAucWAALnGQAC5xoAAucbAALnHAAC5x0AAucfAALnIQAC5yIAAucjAALnJQAC5yYAAudlAALnZwAC52kAAudrAALnbgAC528AAudwAALncQAC53IAAud0AALndgAC53cAAud4AALnegAC53sAAue6AALnvAAC574AAufAAALnwwAC58QAAufFAALnxgAC58cAAufJAALnywAC58wAAufNAALnzwAC59AAAugPAALoEQAC6BMAAugVAALoGAAC6BkAAugaAALoGwAC6BwAAugeAALoIAAC6CEAAugiAALoJAAC6CUAAuhkAALoZgAC6GgAAuhqAALobQAC6G4AAuhvAALocAAC6HEAAuhzAALodQAC6HYAAuh3AALoeQAC6HoAAujFAALo6AAC6QgAAukoAALpKgAC6SwAAukuAALpMAAC6TMAAuk0AALpNQAC6TgAAuk5AALpOwAC6TwAAuk+AALpQQAC6UIAAulDAALpRgAC6UcAAulMAALpWQAC6V4AAulgAALpYgAC6WcAAulqAALpbQAC6W8AAumUAALpuAAC6d8AAuoDAALqBgAC6ggAAuoKAALqDAAC6g4AAuoQAALqEQAC6hQAAuohAALqMgAC6jQAAuo2AALqOAAC6joAAuo8AALqPgAC6kAAAupCAALqUwAC6lYAAupZAALqXAAC6l8AAupiAALqZQAC6mgAAuprAALqbQAC6qwAAuquAALqsAAC6rIAAuq1AALqtgAC6rcAAuq4AALquQAC6rsAAuq9AALqvgAC6r8AAurBAALqwgAC6wEAAusDAALrBQAC6wcAAusKAALrCwAC6wwAAusNAALrDgAC6xAAAusSAALrEwAC6xQAAusWAALrFwAC61YAAutYAALrWwAC610AAutgAALrYQAC62IAAutjAALrZAAC62YAAutoAALraQAC62oAAutsAALrbQAC63oAAut7AALrfAAC634AAuu9AALrvwAC68EAAuvDAALrxgAC68cAAuvIAALryQAC68oAAuvMAALrzgAC688AAuvQAALr0gAC69MAAuwSAALsFAAC7BYAAuwYAALsGwAC7BwAAuwdAALsHgAC7B8AAuwhAALsIwAC7CQAAuwlAALsJwAC7CgAAuxnAALsaQAC7GsAAuxtAALscAAC7HEAAuxyAALscwAC7HQAAux2AALseAAC7HkAAux6AALsfAAC7H0AAuy8AALsvgAC7MAAAuzCAALsxQAC7MYAAuzHAALsyAAC7MkAAuzLAALszQAC7M4AAuzPAALs0QAC7NIAAu0RAALtEwAC7RUAAu0XAALtGgAC7RsAAu0cAALtHQAC7R4AAu0gAALtIgAC7SMAAu0kAALtJgAC7ScAAu1MAALtcAAC7ZcAAu27AALtvgAC7cAAAu3CAALtxAAC7cYAAu3IAALtyQAC7cwAAu3ZAALt6AAC7eoAAu3sAALt7gAC7fAAAu3yAALt9AAC7fYAAu4FAALuCAAC7gsAAu4OAALuEQAC7hQAAu4XAALuGgAC7hwAAu5bAALuXQAC7l8AAu5hAALuZAAC7mUAAu5mAALuZwAC7mgAAu5qAALubAAC7m0AAu5uAALucAAC7nEAAu6wAALusgAC7rQAAu62AALuuQAC7roAAu67AALuvAAC7r0AAu6/AALuwQAC7sIAAu7DAALuxQAC7sYAAu8FAALvBwAC7wkAAu8LAALvDgAC7w8AAu8QAALvEQAC7xIAAu8UAALvFgAC7xcAAu8YAALvGgAC7xsAAu9aAALvXAAC718AAu9hAALvZAAC72UAAu9mAALvZwAC72gAAu9qAALvbAAC720AAu9uAALvcAAC73EAAu+wAALvsgAC77QAAu+2AALvuQAC77oAAu+7AALvvAAC770AAu+/AALvwQAC78IAAu/DAALvxQAC78YAAvAFAALwBwAC8AkAAvALAALwDgAC8A8AAvAQAALwEQAC8BIAAvAUAALwFgAC8BcAAvAYAALwGgAC8BsAAvBaAALwXAAC8F4AAvBgAALwYwAC8GQAAvBlAALwZgAC8GcAAvBpAALwawAC8GwAAvBtAALwbwAC8HAAAvC7AALw3gAC8P4AAvEeAALxIAAC8SIAAvEkAALxJgAC8SkAAvEqAALxKwAC8S4AAvEvAALxMQAC8TIAAvE0AALxNwAC8TgAAvE5AALxPAAC8T0AAvFCAALxTwAC8VQAAvFWAALxWAAC8V0AAvFgAALxYwAC8WUAAvGKAALxrgAC8dUAAvH5AALx/AAC8f4AAvIAAALyAgAC8gQAAvIGAALyBwAC8goAAvIXAALyKAAC8ioAAvIsAALyLgAC8jAAAvIyAALyNAAC8jYAAvI4AALySQAC8kwAAvJPAALyUgAC8lUAAvJYAALyWwAC8l4AAvJhAALyYwAC8qIAAvKkAALypgAC8qgAAvKrAALyrAAC8q0AAvKuAALyrwAC8rEAAvKzAALytAAC8rUAAvK3AALyuAAC8vcAAvL5AALy+wAC8v0AAvMAAALzAQAC8wIAAvMDAALzBAAC8wYAAvMIAALzCQAC8woAAvMMAALzDQAC80wAAvNOAALzUQAC81MAAvNWAALzVwAC81gAAvNZAALzWgAC81wAAvNeAALzXwAC82AAAvNiAALzYwAC83AAAvNxAALzcgAC83QAAvOzAALztQAC87cAAvO5AALzvAAC870AAvO+AALzvwAC88AAAvPCAALzxAAC88UAAvPGAALzyAAC88kAAvQIAAL0CgAC9AwAAvQOAAL0EQAC9BIAAvQTAAL0FAAC9BUAAvQXAAL0GQAC9BoAAvQbAAL0HQAC9B4AAvRdAAL0XwAC9GEAAvRjAAL0ZgAC9GcAAvRoAAL0aQAC9GoAAvRsAAL0bgAC9G8AAvRwAAL0cgAC9HMAAvSyAAL0tAAC9LYAAvS4AAL0uwAC9LwAAvS9AAL0vgAC9L8AAvTBAAL0wwAC9MQAAvTFAAL0xwAC9MgAAvUHAAL1CQAC9QsAAvUNAAL1EAAC9REAAvUSAAL1EwAC9RQAAvUWAAL1GAAC9RkAAvUaAAL1HAAC9R0AAvVCAAL1ZgAC9Y0AAvWxAAL1tAAC9bYAAvW4AAL1ugAC9bwAAvW+AAL1vwAC9cIAAvXPAAL13gAC9eAAAvXiAAL15AAC9eYAAvXoAAL16gAC9ewAAvX7AAL1/gAC9gEAAvYEAAL2BwAC9goAAvYNAAL2EAAC9hIAAvZRAAL2UwAC9lUAAvZXAAL2WgAC9lsAAvZcAAL2XQAC9l4AAvZgAAL2YgAC9mMAAvZkAAL2ZgAC9mcAAvamAAL2qAAC9qoAAvasAAL2rwAC9rAAAvaxAAL2sgAC9rMAAva1AAL2twAC9rgAAva5AAL2uwAC9rwAAvb7AAL2/QAC9v8AAvcBAAL3BAAC9wUAAvcGAAL3BwAC9wgAAvcKAAL3DAAC9w0AAvcOAAL3EAAC9xEAAvdQAAL3UgAC91UAAvdXAAL3WgAC91sAAvdcAAL3XQAC914AAvdgAAL3YgAC92MAAvdkAAL3ZgAC92cAAvemAAL3qAAC96oAAvesAAL3rwAC97AAAvexAAL3sgAC97MAAve1AAL3twAC97gAAve5AAL3uwAC97wAAvf7AAL3/QAC9/8AAvgBAAL4BAAC+AUAAvgGAAL4BwAC+AgAAvgKAAL4DAAC+A0AAvgOAAL4EAAC+BEAAvhQAAL4UgAC+FQAAvhWAAL4WQAC+FoAAvhbAAL4XAAC+F0AAvhfAAL4YQAC+GIAAvhjAAL4ZQAC+GYAAvhvAAL4cAAC+HIAAvi1AAL42QAC+P0AAvkgAAL5RwAC+WcAAvmOAAL5tQAC+dUAAvn5AAL6HQAC+h8AAvoiAAL6JAAC+iYAAvooAAL6KwAC+i4AAvowAAL6MgAC+jUAAvo3AAL6OQAC+jwAAvo/AAL6QAAC+kUAAvpSAAL6VQAC+lcAAvpaAAL6XQAC+l8AAvqEAAL6qAAC+s8AAvrzAAL69gAC+vgAAvr6AAL6/AAC+v4AAvsAAAL7AQAC+wQAAvsRAAL7JAAC+yYAAvsoAAL7KgAC+ywAAvsuAAL7MAAC+zIAAvs0AAL7NgAC+0kAAvtMAAL7TwAC+1IAAvtVAAL7WAAC+1sAAvteAAL7YQAC+2QAAvtmAAL7pQAC+6cAAvuqAAL7rAAC+68AAvuwAAL7sQAC+7IAAvuzAAL7tQAC+7cAAvu4AAL7uQAC+7sAAvu8AAL7xQAC+8YAAvvIAAL8BwAC/AkAAvwLAAL8DQAC/BAAAvwRAAL8EgAC/BMAAvwUAAL8FgAC/BgAAvwZAAL8GgAC/BwAAvwdAAL8XAAC/F4AAvxhAAL8YwAC/GYAAvxnAAL8aAAC/GkAAvxqAAL8bAAC/G4AAvxvAAL8cAAC/HIAAvxzAAL8fAAC/H0AAvx/AAL8vgAC/MAAAvzCAAL8xAAC/McAAvzIAAL8yQAC/MoAAvzLAAL8zQAC/M8AAvzQAAL80QAC/NMAAvzUAAL9EwAC/RUAAv0YAAL9GgAC/R0AAv0eAAL9HwAC/SAAAv0hAAL9IwAC/SUAAv0mAAL9JwAC/SkAAv0qAAL9MwAC/TQAAv02AAL9dQAC/XcAAv15AAL9ewAC/X4AAv1/AAL9gAAC/YEAAv2CAAL9hAAC/YYAAv2HAAL9iAAC/YoAAv2LAAL9ygAC/cwAAv3PAAL90QAC/dQAAv3VAAL91gAC/dcAAv3YAAL92gAC/dwAAv3dAAL93gAC/eAAAv3hAAL97gAC/e8AAv3wAAL98gAC/jEAAv4zAAL+NQAC/jcAAv46AAL+OwAC/jwAAv49AAL+PgAC/kAAAv5CAAL+QwAC/kQAAv5GAAL+RwAC/oYAAv6IAAL+iwAC/o0AAv6QAAL+kQAC/pIAAv6TAAL+lAAC/pYAAv6YAAL+mQAC/poAAv6cAAL+nQAC/qgAAv61AAL+vgAC/sAAAv7CAAL+xQAC/sgAAv7RAAL+1AAC/tcAAv7aAAL+3QAC/t8AAv7nAAL+7wAC/zoAAv9dAAL/fQAC/50AAv+fAAL/oQAC/6MAAv+lAAL/qAAC/6kAAv+qAAL/rQAC/64AAv+wAAL/sQAC/7MAAv+1AAL/tgAC/7cAAv+6AAL/uwAC/8AAAv/NAAL/0gAC/9QAAv/WAAL/2wAC/94AAv/hAAL/4wADAAgAAwAsAAMAUwADAHcAAwB6AAMAfAADAH4AAwCAAAMAggADAIQAAwCFAAMAiAADAJUAAwCmAAMAqAADAKoAAwCsAAMArgADALAAAwCyAAMAtAADALYAAwDHAAMAygADAM0AAwDQAAMA0wADANYAAwDZAAMA3AADAN8AAwDhAAMBIAADASIAAwEkAAMBJgADASkAAwEqAAMBKwADASwAAwEtAAMBLwADATEAAwEyAAMBMwADATUAAwE2AAMBdQADAXcAAwF5AAMBewADAX4AAwF/AAMBgAADAYEAAwGCAAMBhAADAYYAAwGHAAMBiAADAYoAAwGLAAMBygADAcwAAwHPAAMB0QADAdQAAwHVAAMB1gADAdcAAwHYAAMB2gADAdwAAwHdAAMB3gADAeAAAwHhAAMB7gADAe8AAwHwAAMB8gADAjEAAwIzAAMCNQADAjcAAwI6AAMCOwADAjwAAwI9AAMCPgADAkAAAwJCAAMCQwADAkQAAwJGAAMCRwADAoYAAwKIAAMCigADAowAAwKPAAMCkAADApEAAwKSAAMCkwADApUAAwKXAAMCmAADApkAAwKbAAMCnAADAtsAAwLdAAMC3wADAuEAAwLkAAMC5QADAuYAAwLnAAMC6AADAuoAAwLsAAMC7QADAu4AAwLwAAMC8QADAzAAAwMyAAMDNAADAzYAAwM5AAMDOgADAzsAAwM8AAMDPQADAz8AAwNBAAMDQgADA0MAAwNFAAMDRgADA4UAAwOHAAMDiQADA4sAAwOOAAMDjwADA5AAAwORAAMDkgADA5QAAwOWAAMDlwADA5gAAwOaAAMDmwADA8AAAwPkAAMECwADBC8AAwQyAAMENAADBDYAAwQ4AAMEOgADBDwAAwQ9AAMEQAADBE0AAwRcAAMEXgADBGAAAwRiAAMEZAADBGYAAwRoAAMEagADBHkAAwR8AAMEfwADBIIAAwSFAAMEiAADBIsAAwSOAAMEkAADBM8AAwTRAAME0wADBNUAAwTYAAME2QADBNoAAwTbAAME3AADBN4AAwTgAAME4QADBOIAAwTkAAME5QADBSQAAwUmAAMFKAADBSoAAwUtAAMFLgADBS8AAwUwAAMFMQADBTMAAwU1AAMFNgADBTcAAwU5AAMFOgADBXkAAwV7AAMFfQADBX8AAwWCAAMFgwADBYQAAwWFAAMFhgADBYgAAwWKAAMFiwADBYwAAwWOAAMFjwADBc4AAwXQAAMF0gADBdQAAwXXAAMF2AADBdkAAwXaAAMF2wADBd0AAwXfAAMF4AADBeEAAwXjAAMF5AADBiMAAwYlAAMGJwADBikAAwYsAAMGLQADBi4AAwYvAAMGMAADBjIAAwY0AAMGNQADBjYAAwY4AAMGOQADBngAAwZ6AAMGfAADBn4AAwaBAAMGggADBoMAAwaEAAMGhQADBocAAwaJAAMGigADBosAAwaNAAMGjgADBs0AAwbPAAMG0QADBtMAAwbWAAMG1wADBtgAAwbZAAMG2gADBtwAAwbeAAMG3wADBuAAAwbiAAMG4wADBy4AAwdRAAMHcQADB5EAAweTAAMHlQADB5cAAweZAAMHnAADB50AAweeAAMHoQADB6IAAwekAAMHpQADB6cAAwepAAMHqgADB6sAAweuAAMHrwADB7QAAwfBAAMHxgADB8gAAwfKAAMHzwADB9IAAwfVAAMH1wADB/wAAwggAAMIRwADCGsAAwhuAAMIcAADCHIAAwh0AAMIdgADCHgAAwh5AAMIfAADCIkAAwiaAAMInAADCJ4AAwigAAMIogADCKQAAwimAAMIqAADCKoAAwi7AAMIvgADCMEAAwjEAAMIxwADCMoAAwjNAAMI0AADCNMAAwjVAAMJFAADCRYAAwkYAAMJGgADCR0AAwkeAAMJHwADCSAAAwkhAAMJIwADCSUAAwkmAAMJJwADCSkAAwkqAAMJaQADCWsAAwltAAMJbwADCXIAAwlzAAMJdAADCXUAAwl2AAMJeAADCXoAAwl7AAMJfAADCX4AAwl/AAMJvgADCcAAAwnDAAMJxQADCcgAAwnJAAMJygADCcsAAwnMAAMJzgADCdAAAwnRAAMJ0gADCdQAAwnVAAMJ4gADCeMAAwnkAAMJ5gADCiUAAwonAAMKKQADCisAAwouAAMKLwADCjAAAwoxAAMKMgADCjQAAwo2AAMKNwADCjgAAwo6AAMKOwADCnoAAwp8AAMKfgADCoAAAwqDAAMKhAADCoUAAwqGAAMKhwADCokAAwqLAAMKjAADCo0AAwqPAAMKkAADCs8AAwrRAAMK0wADCtUAAwrYAAMK2QADCtoAAwrbAAMK3AADCt4AAwrgAAMK4QADCuIAAwrkAAMK5QADCyQAAwsmAAMLKAADCyoAAwstAAMLLgADCy8AAwswAAMLMQADCzMAAws1AAMLNgADCzcAAws5AAMLOgADC3kAAwt7AAMLfQADC38AAwuCAAMLgwADC4QAAwuFAAMLhgADC4gAAwuKAAMLiwADC4wAAwuOAAMLjwADC7QAAwvYAAML/wADDCMAAwwmAAMMKAADDCoAAwwsAAMMLgADDDAAAwwxAAMMNAADDEEAAwxQAAMMUgADDFQAAwxWAAMMWAADDFoAAwxcAAMMXgADDG0AAwxwAAMMcwADDHYAAwx5AAMMfAADDH8AAwyCAAMMhAADDMMAAwzFAAMMxwADDMkAAwzMAAMMzQADDM4AAwzPAAMM0AADDNIAAwzUAAMM1QADDNYAAwzYAAMM2QADDRgAAw0aAAMNHAADDR4AAw0hAAMNIgADDSMAAw0kAAMNJQADDScAAw0pAAMNKgADDSsAAw0tAAMNLgADDW0AAw1vAAMNcQADDXMAAw12AAMNdwADDXgAAw15AAMNegADDXwAAw1+AAMNfwADDYAAAw2CAAMNgwADDcIAAw3EAAMNxgADDcgAAw3LAAMNzAADDc0AAw3OAAMNzwADDdEAAw3TAAMN1AADDdUAAw3XAAMN2AADDhcAAw4ZAAMOGwADDh0AAw4gAAMOIQADDiIAAw4jAAMOJAADDiYAAw4oAAMOKQADDioAAw4sAAMOLQADDmwAAw5uAAMOcAADDnIAAw51AAMOdgADDncAAw54AAMOeQADDnsAAw59AAMOfgADDn8AAw6BAAMOggADDsEAAw7DAAMOxQADDscAAw7KAAMOywADDswAAw7NAAMOzgADDtAAAw7SAAMO0wADDtQAAw7WAAMO1wADDyIAAw9FAAMPZQADD4UAAw+HAAMPiQADD4sAAw+NAAMPkAADD5EAAw+SAAMPlQADD5YAAw+YAAMPmQADD5sAAw+eAAMPnwADD6AAAw+jAAMPpAADD6kAAw+2AAMPuwADD70AAw+/AAMPxAADD8cAAw/KAAMPzAADD/EAAxAVAAMQPAADEGAAAxBjAAMQZQADEGcAAxBpAAMQawADEG0AAxBuAAMQcQADEH4AAxCPAAMQkQADEJMAAxCVAAMQlwADEJkAAxCbAAMQnQADEJ8AAxCwAAMQswADELYAAxC5AAMQvAADEL8AAxDCAAMQxQADEMgAAxDKAAMRCQADEQsAAxENAAMRDwADERIAAxETAAMRFAADERUAAxEWAAMRGAADERoAAxEbAAMRHAADER4AAxEfAAMRXgADEWAAAxFiAAMRZAADEWcAAxFoAAMRaQADEWoAAxFrAAMRbQADEW8AAxFwAAMRcQADEXMAAxF0AAMRswADEbUAAxG4AAMRugADEb0AAxG+AAMRvwADEcAAAxHBAAMRwwADEcUAAxHGAAMRxwADEckAAxHKAAMR1wADEdgAAxHZAAMR2wADEhoAAxIcAAMSHgADEiAAAxIjAAMSJAADEiUAAxImAAMSJwADEikAAxIrAAMSLAADEi0AAxIvAAMSMAADEm8AAxJxAAMScwADEnUAAxJ4AAMSeQADEnoAAxJ7AAMSfAADEn4AAxKAAAMSgQADEoIAAxKEAAMShQADEsQAAxLGAAMSyAADEsoAAxLNAAMSzgADEs8AAxLQAAMS0QADEtMAAxLVAAMS1gADEtcAAxLZAAMS2gADExkAAxMbAAMTHQADEx8AAxMiAAMTIwADEyQAAxMlAAMTJgADEygAAxMqAAMTKwADEywAAxMuAAMTLwADE24AAxNwAAMTcgADE3QAAxN3AAMTeAADE3kAAxN6AAMTewADE30AAxN/AAMTgAADE4EAAxODAAMThAADE6kAAxPNAAMT9AADFBgAAxQbAAMUHQADFB8AAxQhAAMUIwADFCUAAxQmAAMUKQADFDYAAxRFAAMURwADFEkAAxRLAAMUTQADFE8AAxRRAAMUUwADFGIAAxRlAAMUaAADFGsAAxRuAAMUcQADFHQAAxR3AAMUeQADFLgAAxS6AAMUvAADFL4AAxTBAAMUwgADFMMAAxTEAAMUxQADFMcAAxTJAAMUygADFMsAAxTNAAMUzgADFQ0AAxUPAAMVEQADFRMAAxUWAAMVFwADFRgAAxUZAAMVGgADFRwAAxUeAAMVHwADFSAAAxUiAAMVIwADFWIAAxVkAAMVZgADFWgAAxVrAAMVbAADFW0AAxVuAAMVbwADFXEAAxVzAAMVdAADFXUAAxV3AAMVeAADFbcAAxW5AAMVuwADFb0AAxXAAAMVwQADFcIAAxXDAAMVxAADFcYAAxXIAAMVyQADFcoAAxXMAAMVzQADFgwAAxYOAAMWEAADFhIAAxYVAAMWFgADFhcAAxYYAAMWGQADFhsAAxYdAAMWHgADFh8AAxYhAAMWIgADFmEAAxZjAAMWZQADFmcAAxZqAAMWawADFmwAAxZtAAMWbgADFnAAAxZyAAMWcwADFnQAAxZ2AAMWdwADFrYAAxa4AAMWugADFrwAAxa/AAMWwAADFsEAAxbCAAMWwwADFsUAAxbHAAMWyAADFskAAxbLAAMWzAADFxcAAxc6AAMXWgADF3oAAxd8AAMXfgADF4AAAxeCAAMXhQADF4YAAxeHAAMXigADF4sAAxeNAAMXjgADF5AAAxeTAAMXlAADF5UAAxeYAAMXmQADF54AAxerAAMXsAADF7IAAxe0AAMXuQADF7wAAxe/AAMXwQADF+YAAxgKAAMYMQADGFUAAxhYAAMYWgADGFwAAxheAAMYYAADGGIAAxhjAAMYZgADGHMAAxiEAAMYhgADGIgAAxiKAAMYjAADGI4AAxiQAAMYkgADGJQAAxilAAMYqAADGKsAAxiuAAMYsQADGLQAAxi3AAMYugADGL0AAxi/AAMY/gADGQAAAxkCAAMZBAADGQcAAxkIAAMZCQADGQoAAxkLAAMZDQADGQ8AAxkQAAMZEQADGRMAAxkUAAMZUwADGVUAAxlXAAMZWQADGVwAAxldAAMZXgADGV8AAxlgAAMZYgADGWQAAxllAAMZZgADGWgAAxlpAAMZqAADGaoAAxmtAAMZrwADGbIAAxmzAAMZtAADGbUAAxm2AAMZuAADGboAAxm7AAMZvAADGb4AAxm/AAMZzAADGc0AAxnOAAMZ0AADGg8AAxoRAAMaEwADGhUAAxoYAAMaGQADGhoAAxobAAMaHAADGh4AAxogAAMaIQADGiIAAxokAAMaJQADGmQAAxpmAAMaaAADGmoAAxptAAMabgADGm8AAxpwAAMacQADGnMAAxp1AAMadgADGncAAxp5AAMaegADGrkAAxq7AAMavQADGr8AAxrCAAMawwADGsQAAxrFAAMaxgADGsgAAxrKAAMaywADGswAAxrOAAMazwADGw4AAxsQAAMbEgADGxQAAxsXAAMbGAADGxkAAxsaAAMbGwADGx0AAxsfAAMbIAADGyEAAxsjAAMbJAADG2MAAxtlAAMbZwADG2kAAxtsAAMbbQADG24AAxtvAAMbcAADG3IAAxt0AAMbdQADG3YAAxt4AAMbeQADG54AAxvCAAMb6QADHA0AAxwQAAMcEgADHBQAAxwWAAMcGAADHBoAAxwbAAMcHgADHCsAAxw6AAMcPAADHD4AAxxAAAMcQgADHEQAAxxGAAMcSAADHFcAAxxaAAMcXQADHGAAAxxjAAMcZgADHGkAAxxsAAMcbgADHK0AAxyvAAMcsQADHLMAAxy2AAMctwADHLgAAxy5AAMcugADHLwAAxy+AAMcvwADHMAAAxzCAAMcwwADHQIAAx0EAAMdBgADHQgAAx0LAAMdDAADHQ0AAx0OAAMdDwADHREAAx0TAAMdFAADHRUAAx0XAAMdGAADHVcAAx1ZAAMdWwADHV0AAx1gAAMdYQADHWIAAx1jAAMdZAADHWYAAx1oAAMdaQADHWoAAx1sAAMdbQADHawAAx2uAAMdsAADHbIAAx21AAMdtgADHbcAAx24AAMduQADHbsAAx29AAMdvgADHb8AAx3BAAMdwgADHgEAAx4DAAMeBQADHgcAAx4KAAMeCwADHgwAAx4NAAMeDgADHhAAAx4SAAMeEwADHhQAAx4WAAMeFwADHlYAAx5YAAMeWgADHlwAAx5fAAMeYAADHmEAAx5iAAMeYwADHmUAAx5nAAMeaAADHmkAAx5rAAMebAADHqsAAx6tAAMerwADHrEAAx60AAMetQADHrYAAx63AAMeuAADHroAAx68AAMevQADHr4AAx7AAAMewQADHsoAAx7LAAMezQADHxAAAx80AAMfWAADH3sAAx+iAAMfwgADH+kAAyAQAAMgMAADIFQAAyB4AAMgegADIH0AAyB/AAMggQADIIMAAyCGAAMgiQADIIsAAyCNAAMgkAADIJIAAyCUAAMglwADIJoAAyCbAAMgoAADIK0AAyCwAAMgsgADILUAAyC4AAMgugADIN8AAyEDAAMhKgADIU4AAyFRAAMhUwADIVUAAyFXAAMhWQADIVsAAyFcAAMhXwADIWwAAyF/AAMhgQADIYMAAyGFAAMhhwADIYkAAyGLAAMhjQADIY8AAyGRAAMhpAADIacAAyGqAAMhrQADIbAAAyGzAAMhtgADIbkAAyG8AAMhvwADIcEAAyIAAAMiAgADIgUAAyIHAAMiCgADIgsAAyIMAAMiDQADIg4AAyIQAAMiEgADIhMAAyIUAAMiFgADIhcAAyIgAAMiIQADIiMAAyJiAAMiZAADImYAAyJoAAMiawADImwAAyJtAAMibgADIm8AAyJxAAMicwADInQAAyJ1AAMidwADIngAAyK3AAMiuQADIrwAAyK+AAMiwQADIsIAAyLDAAMixAADIsUAAyLHAAMiyQADIsoAAyLLAAMizQADIs4AAyLXAAMi2AADItoAAyMZAAMjGwADIx0AAyMfAAMjIgADIyMAAyMkAAMjJQADIyYAAyMoAAMjKgADIysAAyMsAAMjLgADIy8AAyNuAAMjcAADI3MAAyN1AAMjeAADI3kAAyN6AAMjewADI3wAAyN+AAMjgAADI4EAAyOCAAMjhAADI4UAAyOOAAMjjwADI5EAAyPQAAMj0gADI9QAAyPWAAMj2QADI9oAAyPbAAMj3AADI90AAyPfAAMj4QADI+IAAyPjAAMj5QADI+YAAyQlAAMkJwADJCoAAyQsAAMkLwADJDAAAyQxAAMkMgADJDMAAyQ1AAMkNwADJDgAAyQ5AAMkOwADJDwAAyRJAAMkSgADJEsAAyRNAAMkjAADJI4AAySQAAMkkgADJJUAAySWAAMklwADJJgAAySZAAMkmwADJJ0AAySeAAMknwADJKEAAySiAAMk4QADJOMAAyTmAAMk6AADJOsAAyTsAAMk7QADJO4AAyTvAAMk8QADJPMAAyT0AAMk9QADJPcAAyT4AAMlEwADJSAAAyU3AAMlOgADJT0AAyVAAAMlQwADJUYAAyVJAAMlTAADJU8AAyVSAAMlVQADJVgAAyVvAAMlcgADJXUAAyV4AAMlewADJX4AAyWBAAMlhAADJYcAAyWKAAMljQADJZAAAyWSAAMlmQADJaAAAyWpAAMlrgADJbkAAyXFAAMlzgADJdIAAyXcAAMl5AADJi8AAyZSAAMmcgADJpIAAyaUAAMmlgADJpgAAyaaAAMmnQADJp4AAyafAAMmogADJqMAAyalAAMmpgADJqgAAyarAAMmrAADJq0AAyawAAMmsQADJrYAAybDAAMmyAADJsoAAybMAAMm0QADJtQAAybXAAMm2QADJv4AAyciAAMnSQADJ20AAydwAAMncgADJ3QAAyd2AAMneAADJ3oAAyd7AAMnfgADJ4sAAyecAAMnngADJ6AAAyeiAAMnpAADJ6YAAyeoAAMnqgADJ6wAAye9AAMnwAADJ8MAAyfGAAMnyQADJ8wAAyfPAAMn0gADJ9UAAyfXAAMoFgADKBgAAygaAAMoHAADKB8AAyggAAMoIQADKCIAAygjAAMoJQADKCcAAygoAAMoKQADKCsAAygsAAMoawADKG0AAyhvAAMocQADKHQAAyh1AAModgADKHcAAyh4AAMoegADKHwAAyh9AAMofgADKIAAAyiBAAMowAADKMIAAyjFAAMoxwADKMoAAyjLAAMozAADKM0AAyjOAAMo0AADKNIAAyjTAAMo1AADKNYAAyjXAAMo5AADKOUAAyjmAAMo6AADKScAAykpAAMpKwADKS0AAykwAAMpMQADKTIAAykzAAMpNAADKTYAAyk4AAMpOQADKToAAyk8AAMpPQADKXwAAyl+AAMpgAADKYIAAymFAAMphgADKYcAAymIAAMpiQADKYsAAymNAAMpjgADKY8AAymRAAMpkgADKdEAAynTAAMp1QADKdcAAynaAAMp2wADKdwAAyndAAMp3gADKeAAAyniAAMp4wADKeQAAynmAAMp5wADKiYAAyooAAMqKgADKiwAAyovAAMqMAADKjEAAyoyAAMqMwADKjUAAyo3AAMqOAADKjkAAyo7AAMqPAADKnsAAyp9AAMqfwADKoEAAyqEAAMqhQADKoYAAyqHAAMqiAADKooAAyqMAAMqjQADKo4AAyqQAAMqkQADKrYAAyraAAMrAQADKyUAAysoAAMrKgADKywAAysuAAMrMAADKzIAAyszAAMrNgADK0MAAytSAAMrVAADK1YAAytYAAMrWgADK1wAAyteAAMrYAADK28AAytyAAMrdQADK3gAAyt7AAMrfgADK4EAAyuEAAMrhgADK8UAAyvHAAMryQADK8sAAyvOAAMrzwADK9AAAyvRAAMr0gADK9QAAyvWAAMr1wADK9gAAyvaAAMr2wADLBoAAywcAAMsHgADLCAAAywjAAMsJAADLCUAAywmAAMsJwADLCkAAywrAAMsLAADLC0AAywvAAMsMAADLG8AAyxxAAMscwADLHUAAyx4AAMseQADLHoAAyx7AAMsfAADLH4AAyyAAAMsgQADLIIAAyyEAAMshQADLMQAAyzGAAMsyAADLMoAAyzNAAMszgADLM8AAyzQAAMs0QADLNMAAyzVAAMs1gADLNcAAyzZAAMs2gADLRkAAy0bAAMtHQADLR8AAy0iAAMtIwADLSQAAy0lAAMtJgADLSgAAy0qAAMtKwADLSwAAy0uAAMtLwADLW4AAy1wAAMtcgADLXQAAy13AAMteAADLXkAAy16AAMtewADLX0AAy1/AAMtgAADLYEAAy2DAAMthAADLcMAAy3FAAMtxwADLckAAy3MAAMtzQADLc4AAy3PAAMt0AADLdIAAy3UAAMt1QADLdYAAy3YAAMt2QADLiQAAy5HAAMuZwADLocAAy6JAAMuiwADLo0AAy6PAAMukgADLpMAAy6UAAMulwADLpgAAy6aAAMumwADLp0AAy6gAAMuoQADLqIAAy6lAAMupgADLqsAAy64AAMuvQADLr8AAy7BAAMuxgADLskAAy7MAAMuzgADLvMAAy8XAAMvPgADL2IAAy9lAAMvZwADL2kAAy9rAAMvbQADL28AAy9wAAMvcwADL4AAAy+RAAMvkwADL5UAAy+XAAMvmQADL5sAAy+dAAMvnwADL6EAAy+yAAMvtQADL7gAAy+7AAMvvgADL8EAAy/EAAMvxwADL8oAAy/MAAMwCwADMA0AAzAPAAMwEQADMBQAAzAVAAMwFgADMBcAAzAYAAMwGgADMBwAAzAdAAMwHgADMCAAAzAhAAMwYAADMGIAAzBkAAMwZgADMGkAAzBqAAMwawADMGwAAzBtAAMwbwADMHEAAzByAAMwcwADMHUAAzB2AAMwtQADMLcAAzC6AAMwvAADML8AAzDAAAMwwQADMMIAAzDDAAMwxQADMMcAAzDIAAMwyQADMMsAAzDMAAMw2QADMNoAAzDbAAMw3QADMRwAAzEeAAMxIAADMSIAAzElAAMxJgADMScAAzEoAAMxKQADMSsAAzEtAAMxLgADMS8AAzExAAMxMgADMXEAAzFzAAMxdQADMXcAAzF6AAMxewADMXwAAzF9AAMxfgADMYAAAzGCAAMxgwADMYQAAzGGAAMxhwADMcYAAzHIAAMxygADMcwAAzHPAAMx0AADMdEAAzHSAAMx0wADMdUAAzHXAAMx2AADMdkAAzHbAAMx3AADMhsAAzIdAAMyHwADMiEAAzIkAAMyJQADMiYAAzInAAMyKAADMioAAzIsAAMyLQADMi4AAzIwAAMyMQADMnAAAzJyAAMydAADMnYAAzJ5AAMyegADMnsAAzJ8AAMyfQADMn8AAzKBAAMyggADMoMAAzKFAAMyhgADMqsAAzLPAAMy9gADMxoAAzMdAAMzHwADMyEAAzMjAAMzJQADMycAAzMoAAMzKwADMzgAAzNHAAMzSQADM0sAAzNNAAMzTwADM1EAAzNTAAMzVQADM2QAAzNnAAMzagADM20AAzNwAAMzcwADM3YAAzN5AAMzewADM7oAAzO8AAMzvgADM8AAAzPDAAMzxAADM8UAAzPGAAMzxwADM8kAAzPLAAMzzAADM80AAzPPAAMz0AADNA8AAzQRAAM0EwADNBUAAzQYAAM0GQADNBoAAzQbAAM0HAADNB4AAzQgAAM0IQADNCIAAzQkAAM0JQADNGQAAzRmAAM0aAADNGoAAzRtAAM0bgADNG8AAzRwAAM0cQADNHMAAzR1AAM0dgADNHcAAzR5AAM0egADNLkAAzS7AAM0vQADNL8AAzTCAAM0wwADNMQAAzTFAAM0xgADNMgAAzTKAAM0ywADNMwAAzTOAAM0zwADNQ4AAzUQAAM1EgADNRQAAzUXAAM1GAADNRkAAzUaAAM1GwADNR0AAzUfAAM1IAADNSEAAzUjAAM1JAADNWMAAzVlAAM1ZwADNWkAAzVsAAM1bQADNW4AAzVvAAM1cAADNXIAAzV0AAM1dQADNXYAAzV4AAM1eQADNbgAAzW6AAM1vAADNb4AAzXBAAM1wgADNcMAAzXEAAM1xQADNccAAzXJAAM1ygADNcsAAzXNAAM1zgADNhkAAzY8AAM2XAADNnwAAzZ+AAM2gAADNoIAAzaEAAM2hwADNogAAzaJAAM2jAADNo0AAzaPAAM2kAADNpIAAzaVAAM2lgADNpcAAzaaAAM2mwADNqAAAzatAAM2sgADNrQAAza2AAM2uwADNr4AAzbBAAM2wwADNugAAzcMAAM3MwADN1cAAzdaAAM3XAADN14AAzdgAAM3YgADN2QAAzdlAAM3aAADN3UAAzeGAAM3iAADN4oAAzeMAAM3jgADN5AAAzeSAAM3lAADN5YAAzenAAM3qgADN60AAzewAAM3swADN7YAAze5AAM3vAADN78AAzfBAAM4AAADOAIAAzgEAAM4BgADOAkAAzgKAAM4CwADOAwAAzgNAAM4DwADOBEAAzgSAAM4EwADOBUAAzgWAAM4VQADOFcAAzhZAAM4WwADOF4AAzhfAAM4YAADOGEAAzhiAAM4ZAADOGYAAzhnAAM4aAADOGoAAzhrAAM4qgADOKwAAzivAAM4sQADOLQAAzi1AAM4tgADOLcAAzi4AAM4ugADOLwAAzi9AAM4vgADOMAAAzjBAAM4zgADOM8AAzjQAAM40gADOREAAzkTAAM5FQADORcAAzkaAAM5GwADORwAAzkdAAM5HgADOSAAAzkiAAM5IwADOSQAAzkmAAM5JwADOWYAAzloAAM5agADOWwAAzlvAAM5cAADOXEAAzlyAAM5cwADOXUAAzl3AAM5eAADOXkAAzl7AAM5fAADObsAAzm9AAM5vwADOcEAAznEAAM5xQADOcYAAznHAAM5yAADOcoAAznMAAM5zQADOc4AAznQAAM50QADOhAAAzoSAAM6FAADOhYAAzoZAAM6GgADOhsAAzocAAM6HQADOh8AAzohAAM6IgADOiMAAzolAAM6JgADOmUAAzpnAAM6aQADOmsAAzpuAAM6bwADOnAAAzpxAAM6cgADOnQAAzp2AAM6dwADOngAAzp6AAM6ewADOqAAAzrEAAM66wADOw8AAzsSAAM7FAADOxYAAzsYAAM7GgADOxwAAzsdAAM7IAADOy0AAzs8AAM7PgADO0AAAztCAAM7RAADO0YAAztIAAM7SgADO1kAAztcAAM7XwADO2IAAztlAAM7aAADO2sAAztuAAM7cAADO68AAzuxAAM7swADO7UAAzu4AAM7uQADO7oAAzu7AAM7vAADO74AAzvAAAM7wQADO8IAAzvEAAM7xQADPAQAAzwGAAM8CAADPAoAAzwNAAM8DgADPA8AAzwQAAM8EQADPBMAAzwVAAM8FgADPBcAAzwZAAM8GgADPFkAAzxbAAM8XQADPF8AAzxiAAM8YwADPGQAAzxlAAM8ZgADPGgAAzxqAAM8awADPGwAAzxuAAM8bwADPK4AAzywAAM8sgADPLQAAzy3AAM8uAADPLkAAzy6AAM8uwADPL0AAzy/AAM8wAADPMEAAzzDAAM8xAADPQMAAz0FAAM9BwADPQkAAz0MAAM9DQADPQ4AAz0PAAM9EAADPRIAAz0UAAM9FQADPRYAAz0YAAM9GQADPVgAAz1aAAM9XAADPV4AAz1hAAM9YgADPWMAAz1kAAM9ZQADPWcAAz1pAAM9agADPWsAAz1tAAM9bgADPa0AAz2vAAM9sQADPbMAAz22AAM9twADPbgAAz25AAM9ugADPbwAAz2+AAM9vwADPcAAAz3CAAM9wwADPg4AAz4xAAM+UQADPnEAAz5zAAM+dQADPncAAz55AAM+fAADPn0AAz5+AAM+gQADPoIAAz6EAAM+hQADPocAAz6KAAM+iwADPowAAz6PAAM+kAADPpUAAz6iAAM+pwADPqkAAz6rAAM+sAADPrMAAz62AAM+uAADPt0AAz8BAAM/KAADP0wAAz9PAAM/UQADP1MAAz9VAAM/VwADP1kAAz9aAAM/XQADP2oAAz97AAM/fQADP38AAz+BAAM/gwADP4UAAz+HAAM/iQADP4sAAz+cAAM/nwADP6IAAz+lAAM/qAADP6sAAz+uAAM/sQADP7QAAz+2AAM/9QADP/cAAz/5AAM/+wADP/4AAz//AANAAAADQAEAA0ACAANABAADQAYAA0AHAANACAADQAoAA0ALAANASgADQEwAA0BOAANAUAADQFMAA0BUAANAVQADQFYAA0BXAANAWQADQFsAA0BcAANAXQADQF8AA0BgAANAnwADQKEAA0CkAANApgADQKkAA0CqAANAqwADQKwAA0CtAANArwADQLEAA0CyAANAswADQLUAA0C2AANAwwADQMQAA0DFAANAxwADQQYAA0EIAANBCgADQQwAA0EPAANBEAADQREAA0ESAANBEwADQRUAA0EXAANBGAADQRkAA0EbAANBHAADQVsAA0FdAANBXwADQWEAA0FkAANBZQADQWYAA0FnAANBaAADQWoAA0FsAANBbQADQW4AA0FwAANBcQADQbAAA0GyAANBtAADQbYAA0G5AANBugADQbsAA0G8AANBvQADQb8AA0HBAANBwgADQcMAA0HFAANBxgADQgUAA0IHAANCCQADQgsAA0IOAANCDwADQhAAA0IRAANCEgADQhQAA0IWAANCFwADQhgAA0IaAANCGwADQloAA0JcAANCXgADQmAAA0JjAANCZAADQmUAA0JmAANCZwADQmkAA0JrAANCbAADQm0AA0JvAANCcAADQpUAA0K5AANC4AADQwQAA0MHAANDCQADQwsAA0MNAANDDwADQxEAA0MSAANDFQADQyIAA0MxAANDMwADQzUAA0M3AANDOQADQzsAA0M9AANDPwADQ04AA0NRAANDVAADQ1cAA0NaAANDXQADQ2AAA0NjAANDZQADQ6QAA0OmAANDqAADQ6oAA0OtAANDrgADQ68AA0OwAANDsQADQ7MAA0O1AANDtgADQ7cAA0O5AANDugADQ/kAA0P7AAND/QADQ/8AA0QCAANEAwADRAQAA0QFAANEBgADRAgAA0QKAANECwADRAwAA0QOAANEDwADRE4AA0RQAANEUgADRFQAA0RXAANEWAADRFkAA0RaAANEWwADRF0AA0RfAANEYAADRGEAA0RjAANEZAADRKMAA0SlAANEqAADRKoAA0StAANErgADRK8AA0SwAANEsQADRLMAA0S1AANEtgADRLcAA0S5AANEugADRPkAA0T7AANE/QADRP8AA0UCAANFAwADRQQAA0UFAANFBgADRQgAA0UKAANFCwADRQwAA0UOAANFDwADRU4AA0VQAANFUgADRVQAA0VXAANFWAADRVkAA0VaAANFWwADRV0AA0VfAANFYAADRWEAA0VjAANFZAADRaMAA0WlAANFpwADRakAA0WsAANFrQADRa4AA0WvAANFsAADRbIAA0W0AANFtQADRbYAA0W4AANFuQADRgQAA0YnAANGRwADRmcAA0ZpAANGawADRm0AA0ZvAANGcgADRnMAA0Z0AANGdwADRngAA0Z6AANGewADRn0AA0aAAANGgQADRoIAA0aFAANGhgADRosAA0aYAANGnQADRp8AA0ahAANGpgADRqkAA0asAANGrgADRtMAA0b3AANHHgADR0IAA0dFAANHRwADR0kAA0dLAANHTQADR08AA0dQAANHUwADR2AAA0dxAANHcwADR3UAA0d3AANHeQADR3sAA0d9AANHfwADR4EAA0eSAANHlQADR5gAA0ebAANHngADR6EAA0ekAANHpwADR6oAA0esAANH6wADR+0AA0fvAANH8QADR/QAA0f1AANH9gADR/cAA0f4AANH+gADR/wAA0f9AANH/gADSAAAA0gBAANIQAADSEIAA0hEAANIRgADSEkAA0hKAANISwADSEwAA0hNAANITwADSFEAA0hSAANIUwADSFUAA0hWAANIlQADSJcAA0iaAANInAADSJ8AA0igAANIoQADSKIAA0ijAANIpQADSKcAA0ioAANIqQADSKsAA0isAANIuQADSLoAA0i7AANIvQADSPwAA0j+AANJAAADSQIAA0kFAANJBgADSQcAA0kIAANJCQADSQsAA0kNAANJDgADSQ8AA0kRAANJEgADSVEAA0lTAANJVQADSVcAA0laAANJWwADSVwAA0ldAANJXgADSWAAA0liAANJYwADSWQAA0lmAANJZwADSaYAA0moAANJqgADSawAA0mvAANJsAADSbEAA0myAANJswADSbUAA0m3AANJuAADSbkAA0m7AANJvAADSfsAA0n9AANJ/wADSgEAA0oEAANKBQADSgYAA0oHAANKCAADSgoAA0oMAANKDQADSg4AA0oQAANKEQADSlAAA0pSAANKVAADSlYAA0pZAANKWgADSlsAA0pcAANKXQADSl8AA0phAANKYgADSmMAA0plAANKZgADSosAA0qvAANK1gADSvoAA0r9AANK/wADSwEAA0sDAANLBQADSwcAA0sIAANLCwADSxgAA0snAANLKQADSysAA0stAANLLwADSzEAA0szAANLNQADS0QAA0tHAANLSgADS00AA0tQAANLUwADS1YAA0tZAANLWwADS5oAA0ucAANLngADS6AAA0ujAANLpAADS6UAA0umAANLpwADS6kAA0urAANLrAADS60AA0uvAANLsAADS+8AA0vxAANL8wADS/UAA0v4AANL+QADS/oAA0v7AANL/AADS/4AA0wAAANMAQADTAIAA0wEAANMBQADTEQAA0xGAANMSAADTEoAA0xNAANMTgADTE8AA0xQAANMUQADTFMAA0xVAANMVgADTFcAA0xZAANMWgADTJkAA0ybAANMnQADTJ8AA0yiAANMowADTKQAA0ylAANMpgADTKgAA0yqAANMqwADTKwAA0yuAANMrwADTO4AA0zwAANM8gADTPQAA0z3AANM+AADTPkAA0z6AANM+wADTP0AA0z/AANNAAADTQEAA00DAANNBAADTUMAA01FAANNRwADTUkAA01MAANNTQADTU4AA01PAANNUAADTVIAA01UAANNVQADTVYAA01YAANNWQADTZgAA02aAANNnAADTZ4AA02hAANNogADTaMAA02kAANNpQADTacAA02pAANNqgADTasAA02tAANNrgADTfkAA04cAANOPAADTlwAA05eAANOYAADTmIAA05kAANOZwADTmgAA05pAANObAADTm0AA05vAANOcAADTnIAA051AANOdgADTncAA056AANOewADToQAA06RAANOlgADTpgAA06aAANOnwADTqIAA06lAANOpwADTswAA07wAANPFwADTzsAA08+AANPQAADT0IAA09EAANPRgADT0gAA09JAANPTAADT1kAA09qAANPbAADT24AA09wAANPcgADT3QAA092AANPeAADT3oAA0+LAANPjgADT5EAA0+UAANPlwADT5oAA0+dAANPoAADT6MAA0+lAANP5AADT+YAA0/oAANP6gADT+0AA0/uAANP7wADT/AAA0/xAANP8wADT/UAA0/2AANP9wADT/kAA0/6AANQOQADUDsAA1A9AANQPwADUEIAA1BDAANQRAADUEUAA1BGAANQSAADUEoAA1BLAANQTAADUE4AA1BPAANQjgADUJAAA1CTAANQlQADUJgAA1CZAANQmgADUJsAA1CcAANQngADUKAAA1ChAANQogADUKQAA1ClAANQsgADULMAA1C0AANQtgADUPUAA1D3AANQ+QADUPsAA1D+AANQ/wADUQAAA1EBAANRAgADUQQAA1EGAANRBwADUQgAA1EKAANRCwADUUoAA1FMAANRTgADUVAAA1FTAANRVAADUVUAA1FWAANRVwADUVkAA1FbAANRXAADUV0AA1FfAANRYAADUZ8AA1GhAANRowADUaUAA1GoAANRqQADUaoAA1GrAANRrAADUa4AA1GwAANRsQADUbIAA1G0AANRtQADUfQAA1H2AANR+AADUfoAA1H9AANR/gADUf8AA1IAAANSAQADUgMAA1IFAANSBgADUgcAA1IJAANSCgADUkkAA1JLAANSTQADUk8AA1JSAANSUwADUlQAA1JVAANSVgADUlgAA1JaAANSWwADUlwAA1JeAANSXwADUoQAA1KoAANSzwADUvMAA1L2AANS+AADUvoAA1L8AANS/gADUwAAA1MBAANTBAADUxEAA1MgAANTIgADUyQAA1MmAANTKAADUyoAA1MsAANTLgADUz0AA1NAAANTQwADU0YAA1NJAANTTAADU08AA1NSAANTVAADU5MAA1OVAANTlwADU5kAA1OcAANTnQADU54AA1OfAANToAADU6IAA1OkAANTpQADU6YAA1OoAANTqQADU+gAA1PqAANT7AADU+4AA1PxAANT8gADU/MAA1P0AANT9QADU/cAA1P5AANT+gADU/sAA1P9AANT/gADVD0AA1Q/AANUQQADVEMAA1RGAANURwADVEgAA1RJAANUSgADVEwAA1ROAANUTwADVFAAA1RSAANUUwADVJIAA1SUAANUlwADVJkAA1ScAANUnQADVJ4AA1SfAANUoAADVKIAA1SkAANUpQADVKYAA1SoAANUqQADVOgAA1TqAANU7AADVO4AA1TxAANU8gADVPMAA1T0AANU9QADVPcAA1T5AANU+gADVPsAA1T9AANU/gADVT0AA1U/AANVQQADVUMAA1VGAANVRwADVUgAA1VJAANVSgADVUwAA1VOAANVTwADVVAAA1VSAANVUwADVZIAA1WUAANVlgADVZgAA1WbAANVnAADVZ0AA1WeAANVnwADVaEAA1WjAANVpAADVaUAA1WnAANVqAADVfMAA1YWAANWNgADVlYAA1ZYAANWWgADVlwAA1ZeAANWYQADVmIAA1ZjAANWZgADVmcAA1ZpAANWagADVmwAA1ZvAANWcAADVnEAA1Z0AANWdQADVnoAA1aHAANWjAADVo4AA1aQAANWlQADVpgAA1abAANWnQADVsIAA1bmAANXDQADVzEAA1c0AANXNgADVzgAA1c6AANXPAADVz4AA1c/AANXQgADV08AA1dgAANXYgADV2QAA1dmAANXaAADV2oAA1dsAANXbgADV3AAA1eBAANXhAADV4cAA1eKAANXjQADV5AAA1eTAANXlgADV5kAA1ebAANX2gADV9wAA1feAANX4AADV+MAA1fkAANX5QADV+YAA1fnAANX6QADV+sAA1fsAANX7QADV+8AA1fwAANYLwADWDEAA1gzAANYNQADWDgAA1g5AANYOgADWDsAA1g8AANYPgADWEAAA1hBAANYQgADWEQAA1hFAANYhAADWIYAA1iJAANYiwADWI4AA1iPAANYkAADWJEAA1iSAANYlAADWJYAA1iXAANYmAADWJoAA1ibAANYqAADWKkAA1iqAANYrAADWOsAA1jtAANY7wADWPEAA1j0AANY9QADWPYAA1j3AANY+AADWPoAA1j8AANY/QADWP4AA1kAAANZAQADWUAAA1lCAANZRAADWUYAA1lJAANZSgADWUsAA1lMAANZTQADWU8AA1lRAANZUgADWVMAA1lVAANZVgADWZUAA1mXAANZmQADWZsAA1meAANZnwADWaAAA1mhAANZogADWaQAA1mmAANZpwADWagAA1mqAANZqwADWeoAA1nsAANZ7gADWfAAA1nzAANZ9AADWfUAA1n2AANZ9wADWfkAA1n7AANZ/AADWf0AA1n/AANaAAADWj8AA1pBAANaQwADWkUAA1pIAANaSQADWkoAA1pLAANaTAADWk4AA1pQAANaUQADWlIAA1pUAANaVQADWnoAA1qeAANaxQADWukAA1rsAANa7gADWvAAA1ryAANa9AADWvYAA1r3AANa+gADWwcAA1sWAANbGAADWxoAA1scAANbHgADWyAAA1siAANbJAADWzMAA1s2AANbOQADWzwAA1s/AANbQgADW0UAA1tIAANbSgADW4kAA1uLAANbjQADW48AA1uSAANbkwADW5QAA1uVAANblgADW5gAA1uaAANbmwADW5wAA1ueAANbnwADW94AA1vgAANb4gADW+QAA1vnAANb6AADW+kAA1vqAANb6wADW+0AA1vvAANb8AADW/EAA1vzAANb9AADXDMAA1w1AANcNwADXDkAA1w8AANcPQADXD4AA1w/AANcQAADXEIAA1xEAANcRQADXEYAA1xIAANcSQADXIgAA1yKAANcjAADXI4AA1yRAANckgADXJMAA1yUAANclQADXJcAA1yZAANcmgADXJsAA1ydAANcngADXN0AA1zfAANc4QADXOMAA1zmAANc5wADXOgAA1zpAANc6gADXOwAA1zuAANc7wADXPAAA1zyAANc8wADXTIAA100AANdNgADXTgAA107AANdPAADXT0AA10+AANdPwADXUEAA11DAANdRAADXUUAA11HAANdSAADXYcAA12JAANdiwADXY0AA12QAANdkQADXZIAA12TAANdlAADXZYAA12YAANdmQADXZoAA12cAANdnQADXegAA14LAANeKwADXksAA15NAANeTwADXlEAA15TAANeVgADXlcAA15YAANeWwADXlwAA15eAANeXwADXmEAA15kAANeZQADXmYAA15pAANeagADXm8AA158AANegQADXoMAA16FAANeigADXo0AA16QAANekgADXrcAA17bAANfAgADXyYAA18pAANfKwADXy0AA18vAANfMQADXzMAA180AANfNwADX0QAA19VAANfVwADX1kAA19bAANfXQADX18AA19hAANfYwADX2UAA192AANfeQADX3wAA19/AANfggADX4UAA1+IAANfiwADX44AA1+QAANfzwADX9EAA1/TAANf1QADX9gAA1/ZAANf2gADX9sAA1/cAANf3gADX+AAA1/hAANf4gADX+QAA1/lAANgJAADYCYAA2AoAANgKgADYC0AA2AuAANgLwADYDAAA2AxAANgMwADYDUAA2A2AANgNwADYDkAA2A6AANgeQADYHsAA2B+AANggAADYIMAA2CEAANghQADYIYAA2CHAANgiQADYIsAA2CMAANgjQADYI8AA2CQAANgnQADYJ4AA2CfAANgoQADYOAAA2DiAANg5AADYOYAA2DpAANg6gADYOsAA2DsAANg7QADYO8AA2DxAANg8gADYPMAA2D1AANg9gADYTUAA2E3AANhOQADYTsAA2E+AANhPwADYUAAA2FBAANhQgADYUQAA2FGAANhRwADYUgAA2FKAANhSwADYYoAA2GMAANhjgADYZAAA2GTAANhlAADYZUAA2GWAANhlwADYZkAA2GbAANhnAADYZ0AA2GfAANhoAADYd8AA2HhAANh4wADYeUAA2HoAANh6QADYeoAA2HrAANh7AADYe4AA2HwAANh8QADYfIAA2H0AANh9QADYjQAA2I2AANiOAADYjoAA2I9AANiPgADYj8AA2JAAANiQQADYkMAA2JFAANiRgADYkcAA2JJAANiSgADYm8AA2KTAANiugADYt4AA2LhAANi4wADYuUAA2LnAANi6QADYusAA2LsAANi7wADYvwAA2MLAANjDQADYw8AA2MRAANjEwADYxUAA2MXAANjGQADYygAA2MrAANjLgADYzEAA2M0AANjNwADYzoAA2M9AANjPwADY34AA2OAAANjggADY4QAA2OHAANjiAADY4kAA2OKAANjiwADY40AA2OPAANjkAADY5EAA2OTAANjlAADY9MAA2PVAANj1wADY9kAA2PcAANj3QADY94AA2PfAANj4AADY+IAA2PkAANj5QADY+YAA2PoAANj6QADZCgAA2QqAANkLAADZC4AA2QxAANkMgADZDMAA2Q0AANkNQADZDcAA2Q5AANkOgADZDsAA2Q9AANkPgADZH0AA2R/AANkgQADZIMAA2SGAANkhwADZIgAA2SJAANkigADZIwAA2SOAANkjwADZJAAA2SSAANkkwADZNIAA2TUAANk1gADZNgAA2TbAANk3AADZN0AA2TeAANk3wADZOEAA2TjAANk5AADZOUAA2TnAANk6AADZScAA2UpAANlKwADZS0AA2UwAANlMQADZTIAA2UzAANlNAADZTYAA2U4AANlOQADZToAA2U8AANlPQADZXwAA2V+AANlgAADZYIAA2WFAANlhgADZYcAA2WIAANliQADZYsAA2WNAANljgADZY8AA2WRAANlkgADZd0AA2YAAANmIAADZkAAA2ZCAANmRAADZkYAA2ZIAANmSwADZkwAA2ZNAANmUAADZlEAA2ZTAANmVAADZlYAA2ZZAANmWgADZlsAA2ZeAANmXwADZmQAA2ZxAANmdgADZngAA2Z6AANmfwADZoIAA2aFAANmhwADZqwAA2bQAANm9wADZxsAA2ceAANnIAADZyIAA2ckAANnJgADZygAA2cpAANnLAADZzkAA2dKAANnTAADZ04AA2dQAANnUgADZ1QAA2dWAANnWAADZ1oAA2drAANnbgADZ3EAA2d0AANndwADZ3oAA2d9AANngAADZ4MAA2eFAANnxAADZ8YAA2fIAANnygADZ80AA2fOAANnzwADZ9AAA2fRAANn0wADZ9UAA2fWAANn1wADZ9kAA2faAANoGQADaBsAA2gdAANoHwADaCIAA2gjAANoJAADaCUAA2gmAANoKAADaCoAA2grAANoLAADaC4AA2gvAANobgADaHAAA2hzAANodQADaHgAA2h5AANoegADaHsAA2h8AANofgADaIAAA2iBAANoggADaIQAA2iFAANokgADaJMAA2iUAANolgADaNUAA2jXAANo2QADaNsAA2jeAANo3wADaOAAA2jhAANo4gADaOQAA2jmAANo5wADaOgAA2jqAANo6wADaSoAA2ksAANpLgADaTAAA2kzAANpNAADaTUAA2k2AANpNwADaTkAA2k7AANpPAADaT0AA2k/AANpQAADaX8AA2mBAANpgwADaYUAA2mIAANpiQADaYoAA2mLAANpjAADaY4AA2mQAANpkQADaZIAA2mUAANplQADadQAA2nWAANp2AADadoAA2ndAANp3gADad8AA2ngAANp4QADaeMAA2nlAANp5gADaecAA2npAANp6gADaikAA2orAANqLQADai8AA2oyAANqMwADajQAA2o1AANqNgADajgAA2o6AANqOwADajwAA2o+AANqPwADamQAA2qIAANqrwADatMAA2rWAANq2AADatoAA2rcAANq3gADauAAA2rhAANq5AADavEAA2sAAANrAgADawQAA2sGAANrCAADawoAA2sMAANrDgADax0AA2sgAANrIwADayYAA2spAANrLAADay8AA2syAANrNAADa3MAA2t1AANrdwADa3kAA2t8AANrfQADa34AA2t/AANrgAADa4IAA2uEAANrhQADa4YAA2uIAANriQADa8gAA2vKAANrzAADa84AA2vRAANr0gADa9MAA2vUAANr1QADa9cAA2vZAANr2gADa9sAA2vdAANr3gADbB0AA2wfAANsIQADbCMAA2wmAANsJwADbCgAA2wpAANsKgADbCwAA2wuAANsLwADbDAAA2wyAANsMwADbHIAA2x0AANsdgADbHgAA2x7AANsfAADbH0AA2x+AANsfwADbIEAA2yDAANshAADbIUAA2yHAANsiAADbMcAA2zJAANsywADbM0AA2zQAANs0QADbNIAA2zTAANs1AADbNYAA2zYAANs2QADbNoAA2zcAANs3QADbRwAA20eAANtIAADbSIAA20lAANtJgADbScAA20oAANtKQADbSsAA20tAANtLgADbS8AA20xAANtMgADbXEAA21zAANtdQADbXcAA216AANtewADbXwAA219AANtfgADbYAAA22CAANtgwADbYQAA22GAANthwADbdIAA231AANuFQADbjUAA243AANuOQADbjsAA249AANuQAADbkEAA25CAANuRQADbkYAA25IAANuSQADbksAA25OAANuTwADblAAA25TAANuVAADblkAA25mAANuawADbm0AA25vAANudAADbncAA256AANufAADbqEAA27FAANu7AADbxAAA28TAANvFQADbxcAA28ZAANvGwADbx0AA28eAANvIQADby4AA28/AANvQQADb0MAA29FAANvRwADb0kAA29LAANvTQADb08AA29gAANvYwADb2YAA29pAANvbAADb28AA29yAANvdQADb3gAA296AANvuQADb7sAA2+9AANvvwADb8IAA2/DAANvxAADb8UAA2/GAANvyAADb8oAA2/LAANvzAADb84AA2/PAANwDgADcBAAA3ASAANwFAADcBcAA3AYAANwGQADcBoAA3AbAANwHQADcB8AA3AgAANwIQADcCMAA3AkAANwYwADcGUAA3BoAANwagADcG0AA3BuAANwbwADcHAAA3BxAANwcwADcHUAA3B2AANwdwADcHkAA3B6AANwhwADcIgAA3CJAANwiwADcMoAA3DMAANwzgADcNAAA3DTAANw1AADcNUAA3DWAANw1wADcNkAA3DbAANw3AADcN0AA3DfAANw4AADcR8AA3EhAANxIwADcSUAA3EoAANxKQADcSoAA3ErAANxLAADcS4AA3EwAANxMQADcTIAA3E0AANxNQADcXQAA3F2AANxeAADcXoAA3F9AANxfgADcX8AA3GAAANxgQADcYMAA3GFAANxhgADcYcAA3GJAANxigADcckAA3HLAANxzQADcc8AA3HSAANx0wADcdQAA3HVAANx1gADcdgAA3HaAANx2wADcdwAA3HeAANx3wADch4AA3IgAANyIgADciQAA3InAANyKAADcikAA3IqAANyKwADci0AA3IvAANyMAADcjEAA3IzAANyNAADclkAA3J9AANypAADcsgAA3LLAANyzQADcs8AA3LRAANy0wADctUAA3LWAANy2QADcuYAA3L1AANy9wADcvkAA3L7AANy/QADcv8AA3MBAANzAwADcxIAA3MVAANzGAADcxsAA3MeAANzIQADcyQAA3MnAANzKQADc2gAA3NqAANzbAADc24AA3NxAANzcgADc3MAA3N0AANzdQADc3cAA3N5AANzegADc3sAA3N9AANzfgADc70AA3O/AANzwQADc8MAA3PGAANzxwADc8gAA3PJAANzygADc8wAA3POAANzzwADc9AAA3PSAANz0wADdBIAA3QUAAN0FgADdBgAA3QbAAN0HAADdB0AA3QeAAN0HwADdCEAA3QjAAN0JAADdCUAA3QnAAN0KAADdGcAA3RpAAN0bAADdG4AA3RxAAN0cgADdHMAA3R0AAN0dQADdHcAA3R5AAN0egADdHsAA3R9AAN0fgADdL0AA3S/AAN0wQADdMMAA3TGAAN0xwADdMgAA3TJAAN0ygADdMwAA3TOAAN0zwADdNAAA3TSAAN00wADdRIAA3UUAAN1FgADdRgAA3UbAAN1HAADdR0AA3UeAAN1HwADdSEAA3UjAAN1JAADdSUAA3UnAAN1KAADdWcAA3VpAAN1awADdW0AA3VwAAN1cQADdXIAA3VzAAN1dAADdXYAA3V4AAN1eQADdXoAA3V8AAN1fQADdcgAA3XrAAN2CwADdisAA3YtAAN2LwADdjEAA3YzAAN2NgADdjcAA3Y4AAN2OwADdjwAA3Y+AAN2PwADdkEAA3ZEAAN2RQADdkYAA3ZJAAN2SgADdk8AA3ZcAAN2YQADdmMAA3ZlAAN2agADdm0AA3ZwAAN2cgADdpcAA3a7AAN24gADdwYAA3cJAAN3CwADdw0AA3cPAAN3EQADdxMAA3cUAAN3FwADdyQAA3c1AAN3NwADdzkAA3c7AAN3PQADdz8AA3dBAAN3QwADd0UAA3dWAAN3WQADd1wAA3dfAAN3YgADd2UAA3doAAN3awADd24AA3dwAAN3rwADd7EAA3ezAAN3tQADd7gAA3e5AAN3ugADd7sAA3e8AAN3vgADd8AAA3fBAAN3wgADd8QAA3fFAAN4BAADeAYAA3gIAAN4CgADeA0AA3gOAAN4DwADeBAAA3gRAAN4EwADeBUAA3gWAAN4FwADeBkAA3gaAAN4WQADeFsAA3heAAN4YAADeGMAA3hkAAN4ZQADeGYAA3hnAAN4aQADeGsAA3hsAAN4bQADeG8AA3hwAAN4fQADeH4AA3h/AAN4gQADeMAAA3jCAAN4xAADeMYAA3jJAAN4ygADeMsAA3jMAAN4zQADeM8AA3jRAAN40gADeNMAA3jVAAN41gADeRUAA3kXAAN5GQADeRsAA3keAAN5HwADeSAAA3khAAN5IgADeSQAA3kmAAN5JwADeSgAA3kqAAN5KwADeWoAA3lsAAN5bgADeXAAA3lzAAN5dAADeXUAA3l2AAN5dwADeXkAA3l7AAN5fAADeX0AA3l/AAN5gAADeb8AA3nBAAN5wwADecUAA3nIAAN5yQADecoAA3nLAAN5zAADec4AA3nQAAN50QADedIAA3nUAAN51QADehQAA3oWAAN6GAADehoAA3odAAN6HgADeh8AA3ogAAN6IQADeiMAA3olAAN6JgADeicAA3opAAN6KgADek8AA3pzAAN6mgADer4AA3rBAAN6wwADesUAA3rHAAN6yQADessAA3rMAAN6zwADetwAA3rrAAN67QADeu8AA3rxAAN68wADevUAA3r3AAN6+QADewgAA3sLAAN7DgADexEAA3sUAAN7FwADexoAA3sdAAN7HwADe14AA3tgAAN7YgADe2QAA3tnAAN7aAADe2kAA3tqAAN7awADe20AA3tvAAN7cAADe3EAA3tzAAN7dAADe7MAA3u1AAN7twADe7kAA3u8AAN7vQADe74AA3u/AAN7wAADe8IAA3vEAAN7xQADe8YAA3vIAAN7yQADfAgAA3wKAAN8DAADfA4AA3wRAAN8EgADfBMAA3wUAAN8FQADfBcAA3wZAAN8GgADfBsAA3wdAAN8HgADfF0AA3xfAAN8YQADfGMAA3xmAAN8ZwADfGgAA3xpAAN8agADfGwAA3xuAAN8bwADfHAAA3xyAAN8cwADfLIAA3y0AAN8tgADfLgAA3y7AAN8vAADfL0AA3y+AAN8vwADfMEAA3zDAAN8xAADfMUAA3zHAAN8yAADfQcAA30JAAN9CwADfQ0AA30QAAN9EQADfRIAA30TAAN9FAADfRYAA30YAAN9GQADfRoAA30cAAN9HQADfVwAA31eAAN9YAADfWIAA31lAAN9ZgADfWcAA31oAAN9aQADfWsAA31tAAN9bgADfW8AA31xAAN9cgADfXsAA318AAN9fgADfcEAA33lAAN+CQADfiwAA35TAAN+cwADfpoAA37BAAN+4QADfwUAA38pAAN/KwADfy4AA38wAAN/MgADfzQAA383AAN/OgADfzwAA38+AAN/QQADf0MAA39FAAN/SAADf0sAA39M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AAOKagADimsAA4psAAOKbQADim8AA4pxAAOKcgADinMAA4p1AAOKdgADirUAA4q3AAOKuQADirsAA4q+AAOKvwADisAAA4rBAAOKwgADisQAA4rGAAOKxwADisgAA4rKAAOKywADiwoAA4sMAAOLDgADixAAA4sTAAOLFAADixUAA4sWAAOLFwADixkAA4sbAAOLHAADix0AA4sfAAOLIAADi18AA4thAAOLYwADi2UAA4toAAOLaQADi2oAA4trAAOLbAADi24AA4twAAOLcQADi3IAA4t0AAOLdQADi7QAA4u2AAOLuAADi7oAA4u9AAOLvgADi78AA4vAAAOLwQADi8MAA4vFAAOLxgADi8cAA4vJAAOLygADjBUAA4w4AAOMWAADjHgAA4x6AAOMfAADjH4AA4yAAAOMgwADjIQAA4yFAAOMiAADjIkAA4yLAAOMjAADjI4AA4yQAAOMkQADjJIAA4yVAAOMlgADjJsAA4yoAAOMrQADjK8AA4yxAAOMtgADjLkAA4y8AAOMvgADjOMAA40HAAONLgADjVIAA41VAAONVwADjVkAA41bAAONXQADjV8AA41gAAONYwADjXAAA42BAAONgwADjYUAA42HAAONiQADjYsAA42NAAONjwADjZEAA42iAAONpQADjagAA42rAAONrgADjbEAA420AAONtwADjboAA428AAON+wADjf0AA43/AAOOAQADjgQAA44FAAOOBgADjgcAA44IAAOOCgADjgwAA44NAAOODgADjhAAA44RAAOOUAADjlIAA45UAAOOVgADjlkAA45aAAOOWwADjlwAA45dAAOOXwADjmEAA45iAAOOYwADjmUAA45mAAOOpQADjqcAA46qAAOOrAADjq8AA46wAAOOsQADjrIAA46zAAOOtQADjrcAA464AAOOuQADjrsAA468AAOOyQADjsoAA47LAAOOzQADjwwAA48OAAOPEAADjxIAA48VAAOPFgADjxcAA48YAAOPGQADjxsAA48dAAOPHgADjx8AA48hAAOPIgADj2EAA49jAAOPZQADj2cAA49qAAOPawADj2wAA49tAAOPbgADj3AAA49yAAOPcwADj3QAA492AAOPdwADj7YAA4+4AAOPugADj7wAA4+/AAOPwAADj8EAA4/CAAOPwwADj8UAA4/HAAOPyAADj8kAA4/L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AAOa/wADmwEAA5sDAAObBAADmwUAA5sHAAObCAADm0cAA5tJAAObSwADm00AA5tQAAObUQADm1IAA5tTAAObVAADm1YAA5tYAAObWQADm1oAA5tcAAObXQADm5wAA5ueAAOboAADm6IAA5ulAAObpgADm6cAA5uoAAObqQADm6sAA5utAAObrgADm68AA5uxAAObsgADm7sAA5u8AAObvgADnAEAA5wlAAOcSQADnGwAA5yTAAOcswADnNoAA50BAAOdIQADnUUAA51pAAOdawADnW4AA51wAAOdcgADnXQAA513AAOdegADnXwAA51+AAOdgQADnYMAA52FAAOdiAADnYsAA52MAAOdkQADnZ4AA52hAAOdowADnaYAA52pAAOdqwADndAAA530AAOeGwADnj8AA55CAAOeRAADnkYAA55IAAOeSgADnkwAA55NAAOeUAADnl0AA55wAAOecgADnnQAA552AAOeeAADnnoAA558AAOefgADnoAAA56CAAOelQADnpgAA56bAAOengADnqEAA56kAAOepwADnqoAA56tAAOesAADnrIAA57xAAOe8wADnvYAA574AAOe+wADnvwAA579AAOe/gADnv8AA58BAAOfAwADnwQAA58FAAOfBwADnwgAA58RAAOfEgADnxQAA59TAAOfVQADn1cAA59ZAAOfXAADn10AA59eAAOfXwADn2AAA59iAAOfZAADn2UAA59mAAOfaAADn2kAA5+oAAOfqgADn60AA5+vAAOfsgADn7MAA5+0AAOftQADn7YAA5+4AAOfugADn7sAA5+8AAOfvgADn78AA5/IAAOfyQADn8sAA6AKAAOgDAADoA4AA6AQAAOgEwADoBQAA6AVAAOgFgADoBcAA6AZAAOgGwADoBwAA6AdAAOgHwADoCAAA6BfAAOgYQADoGQAA6BmAAOgaQADoGoAA6BrAAOgbAADoG0AA6BvAAOgcQADoHIAA6BzAAOgdQADoHYAA6B/AAOggAADoIIAA6DBAAOgwwADoMUAA6DHAAOgygADoMsAA6DMAAOgzQADoM4AA6DQAAOg0gADoNMAA6DUAAOg1gADoNcAA6EWAAOhGAADoRsAA6EdAAOhIAADoSEAA6EiAAOhIwADoSQAA6EmAAOhKAADoSkAA6EqAAOhLAADoS0AA6E6AAOhOwADoTwAA6E+AAOhfQADoX8AA6GBAAOhgwADoYYAA6GHAAOhiAADoYkAA6GKAAOhjAADoY4AA6GPAAOhkAADoZIAA6GTAAOh0gADodQAA6HXAAOh2QADodwAA6HdAAOh3gADod8AA6HgAAOh4gADoeQAA6HlAAOh5gADoegAA6HpAAOh+AADogUAA6IMAAOiDwADohIAA6IVAAOiHAADoh8AA6IiAAOiJQADoicAA6IuAAOiOAADooMAA6KmAAOixgADouYAA6LoAAOi6gADouwAA6LuAAOi8QADovIAA6LzAAOi9gADovcAA6L5AAOi+gADovwAA6L/AAOjAAADowEAA6MEAAOjBQADowoAA6MXAAOjHAADox4AA6MgAAOjJQADoygAA6MrAAOjLQADo1IAA6N2AAOjnQADo8EAA6PEAAOjxgADo8gAA6PKAAOjzAADo84AA6PPAAOj0gADo98AA6PwAAOj8gADo/QAA6P2AAOj+AADo/oAA6P8AAOj/gADpAAAA6QRAAOkFAADpBcAA6QaAAOkHQADpCAAA6QjAAOkJgADpCkAA6QrAAOkagADpGwAA6RuAAOkcAADpHMAA6R0AAOkdQADpHYAA6R3AAOkeQADpHsAA6R8AAOkfQADpH8AA6SAAAOkvwADpMEAA6TDAAOkxQADpMgAA6TJAAOkygADpMsAA6TMAAOkzgADpNAAA6TRAAOk0gADpNQAA6TVAAOlFAADpRYAA6UZAAOlGwADpR4AA6UfAAOlIAADpSEAA6UiAAOlJAADpSYAA6UnAAOlKAADpSoAA6UrAAOlOAADpTkAA6U6AAOlPAADpXsAA6V9AAOlfwADpYEAA6WEAAOlhQADpYYAA6WHAAOliAADpYoAA6WMAAOljQADpY4AA6WQAAOlkQADpdAAA6XSAAOl1AADpdYAA6XZAAOl2gADpdsAA6XcAAOl3QADpd8AA6XhAAOl4gADpeMAA6XlAAOl5gADpiUAA6YnAAOmKQADpisAA6YuAAOmLwADpjAAA6YxAAOmMgADpjQAA6Y2AAOmNwADpjgAA6Y6AAOmOwADpnoAA6Z8AAOmfgADpoAAA6aDAAOmhAADpoUAA6aGAAOmhwADpokAA6aLAAOmjAADpo0AA6aPAAOmkAADps8AA6bRAAOm0wADptUAA6bYAAOm2QADptoAA6bbAAOm3AADpt4AA6bgAAOm4QADpuIAA6bkAAOm5QADpwoAA6cuAAOnVQADp3kAA6d8AAOnfgADp4AAA6eCAAOnhAADp4YAA6eHAAOnigADp5cAA6emAAOnqAADp6oAA6esAAOnrgADp7AAA6eyAAOntAADp8MAA6fGAAOnyQADp8wAA6fPAAOn0gADp9UAA6fYAAOn2gADqBkAA6gbAAOoHQADqB8AA6giAAOoIwADqCQAA6glAAOoJgADqCgAA6gqAAOoKwADqCwAA6guAAOoLwADqG4AA6hwAAOocgADqHQAA6h3AAOoeAADqHkAA6h6AAOoewADqH0AA6h/AAOogAADqIEAA6iDAAOohAADqMMAA6jFAAOoxwADqMkAA6jMAAOozQADqM4AA6jPAAOo0AADqNIAA6jUAAOo1QADqNYAA6jYAAOo2QADqRgAA6kaAAOpHAADqR4AA6khAAOpIgADqSMAA6kkAAOpJQADqScAA6kpAAOpKgADqSsAA6ktAAOpLgADqW0AA6lvAAOpcQADqXMAA6l2AAOpdwADqXgAA6l5AAOpegADqXwAA6l+AAOpfwADqYAAA6mCAAOpgwADqcIAA6nEAAOpxgADqcgAA6nLAAOpzAADqc0AA6nOAAOpzwADqdEAA6nTAAOp1AADqdUAA6nXAAOp2AADqhcAA6oZAAOqGwADqh0AA6ogAAOqIQADqiIAA6ojAAOqJAADqiYAA6ooAAOqKQADqioAA6osAAOqLQADqngAA6qbAAOquwADqtsAA6rdAAOq3wADquEAA6rjAAOq5gADqucAA6roAAOq6wADquwAA6ruAAOq7wADqvEAA6r0AAOq9QADqvYAA6r5AAOq+gADqwMAA6sQAAOrFQADqxcAA6sZAAOrHgADqyEAA6skAAOrJgADq0sAA6tvAAOrlgADq7oAA6u9AAOrvwADq8EAA6vDAAOrxQADq8cAA6vIAAOrywADq9gAA6vpAAOr6wADq+0AA6vvAAOr8QADq/MAA6v1AAOr9wADq/kAA6wKAAOsDQADrBAAA6wTAAOsFgADrBkAA6wcAAOsHwADrCIAA6wkAAOsYwADrGUAA6xnAAOsaQADrGwAA6xtAAOsbgADrG8AA6xwAAOscgADrHQAA6x1AAOsdgADrHgAA6x5AAOsuAADrLoAA6y8AAOsvgADrMEAA6zCAAOswwADrMQAA6zFAAOsxwADrMkAA6zKAAOsywADrM0AA6zOAAOtDQADrQ8AA60SAAOtFAADrRcAA60YAAOtGQADrRoAA60bAAOtHQADrR8AA60gAAOtIQADrSMAA60kAAOtMQADrTIAA60zAAOtNQADrXQAA612AAOteAADrXoAA619AAOtfgADrX8AA62AAAOtgQADrYMAA62FAAOthgADrYcAA62JAAOtigADrckAA63LAAOtzQADrc8AA63SAAOt0wADrdQAA63VAAOt1gADrdgAA63aAAOt2wADrdwAA63eAAOt3wADrh4AA64gAAOuIgADriQAA64nAAOuKAADrikAA64qAAOuKwADri0AA64vAAOuMAADrjEAA64zAAOuNAADrnMAA651AAOudwADrnkAA658AAOufQADrn4AA65/AAOugAADroIAA66EAAOuhQADroYAA66IAAOuiQADrsgAA67KAAOuzAADrs4AA67RAAOu0gADrtMAA67UAAOu1QADrtcAA67ZAAOu2gADrtsAA67dAAOu3gADrwMAA68nAAOvTgADr3IAA691AAOvdwADr3kAA697AAOvfQADr38AA6+AAAOvgwADr5AAA6+fAAOvoQADr6MAA6+lAAOvpwADr6kAA6+rAAOvrQADr7wAA6+/AAOvwgADr8UAA6/IAAOvywADr84AA6/RAAOv0wADsBIAA7AUAAOwFgADsBgAA7AbAAOwHAADsB0AA7AeAAOwHwADsCEAA7AjAAOwJAADsCUAA7AnAAOwKAADsGcAA7BpAAOwawADsG0AA7BwAAOwcQADsHIAA7BzAAOwdAADsHYAA7B4AAOweQADsHoAA7B8AAOwfQADsLwAA7C+AAOwwAADsMIAA7DFAAOwxgADsMcAA7DIAAOwyQADsMsAA7DNAAOwzgADsM8AA7DRAAOw0gADsREAA7ETAAOxFQADsRcAA7EaAAOxGwADsRwAA7EdAAOxHgADsSAAA7EiAAOxIwADsSQAA7EmAAOxJwADsWYAA7FoAAOxagADsWwAA7FvAAOxcAADsXEAA7FyAAOxcwADsXUAA7F3AAOxeAADsXkAA7F7AAOxfAADsbsAA7G9AAOxvwADscEAA7HEAAOxxQADscYAA7HHAAOxyAADscoAA7HMAAOxzQADsc4AA7HQAAOx0QADshAAA7ISAAOyFAADshYAA7IZAAOyGgADshsAA7IcAAOyHQADsh8AA7IhAAOyIgADsiMAA7IlAAOyJgADsnEAA7KUAAOytAADstQAA7LWAAOy2AADstoAA7LcAAOy3wADsuAAA7LhAAOy5AADsuUAA7LnAAOy6AADsuoAA7LtAAOy7gADsu8AA7LyAAOy8wADsvgAA7MFAAOzCgADswwAA7MOAAOzEwADsxYAA7MZAAOzGwADs0AAA7NkAAOziwADs68AA7OyAAOztAADs7YAA7O4AAOzugADs7wAA7O9AAOzwAADs80AA7PeAAOz4AADs+IAA7PkAAOz5gADs+gAA7PqAAOz7AADs+4AA7P/AAO0AgADtAUAA7QIAAO0CwADtA4AA7QRAAO0FAADtBcAA7QZAAO0WAADtFoAA7RcAAO0XgADtGEAA7RiAAO0YwADtGQAA7RlAAO0ZwADtGkAA7RqAAO0awADtG0AA7RuAAO0rQADtK8AA7SxAAO0swADtLYAA7S3AAO0uAADtLkAA7S6AAO0vAADtL4AA7S/AAO0wAADtMIAA7TDAAO1AgADtQQAA7UHAAO1CQADtQwAA7UNAAO1DgADtQ8AA7UQAAO1EgADtRQAA7UVAAO1FgADtRgAA7UZAAO1JgADtScAA7UoAAO1KgADtWkAA7VrAAO1bQADtW8AA7VyAAO1cwADtXQAA7V1AAO1dgADtXgAA7V6AAO1ewADtXwAA7V+AAO1fwADtb4AA7XAAAO1wgADtcQAA7XHAAO1yAADtckAA7XKAAO1ywADtc0AA7XPAAO10AADtdEAA7XTAAO11AADthMAA7YVAAO2FwADthkAA7YcAAO2HQADth4AA7YfAAO2IAADtiIAA7YkAAO2JQADtiYAA7YoAAO2KQADtmgAA7ZqAAO2bAADtm4AA7ZxAAO2cgADtnMAA7Z0AAO2dQADtncAA7Z5AAO2egADtnsAA7Z9AAO2fgADtr0AA7a/AAO2wQADtsMAA7bGAAO2xwADtsgAA7bJAAO2ygADtswAA7bOAAO2zwADttAAA7bSAAO20wADtvgAA7ccAAO3QwADt2cAA7dqAAO3bAADt24AA7dwAAO3cgADt3QAA7d1AAO3eAADt4UAA7eUAAO3lgADt5gAA7eaAAO3nAADt54AA7egAAO3ogADt7EAA7e0AAO3twADt7oAA7e9AAO3wAADt8MAA7fGAAO3yAADuAcAA7gJAAO4CwADuA0AA7gQAAO4EQADuBIAA7gTAAO4FAADuBYAA7gYAAO4GQADuBoAA7gcAAO4HQADuFwAA7heAAO4YAADuGIAA7hlAAO4ZgADuGcAA7hoAAO4aQADuGsAA7htAAO4bgADuG8AA7hxAAO4cgADuLEAA7izAAO4tQADuLcAA7i6AAO4uwADuLwAA7i9AAO4vgADuMAAA7jCAAO4wwADuMQAA7jGAAO4xwADuQYAA7kIAAO5CgADuQwAA7kPAAO5EAADuREAA7kSAAO5EwADuRUAA7kXAAO5GAADuRkAA7kbAAO5HAADuVsAA7ldAAO5XwADuWEAA7lkAAO5ZQADuWYAA7lnAAO5aAADuWoAA7lsAAO5bQADuW4AA7lwAAO5cQADubAAA7myAAO5tAADubYAA7m5AAO5ugADubsAA7m8AAO5vQADub8AA7nBAAO5wgADucMAA7nFAAO5xgADugUAA7oHAAO6CQADugsAA7oOAAO6DwADuhAAA7oRAAO6EgADuhQAA7oWAAO6FwADuhgAA7oaAAO6GwADuiQAA7olAAO6JwADumoAA7qOAAO6sgADutUAA7r8AAO7HAADu0MAA7tqAAO7igADu64AA7vSAAO71AADu9cAA7vZAAO72wADu90AA7vgAAO74wADu+UAA7vnAAO76gADu+wAA7vuAAO78QADu/QAA7v1AAO7/gADvAsAA7wOAAO8EAADvBMAA7wWAAO8GAADvD0AA7xhAAO8iAADvKwAA7yvAAO8sQADvLMAA7y1AAO8twADvLkAA7y6AAO8vQADvMoAA7zdAAO83wADvOEAA7zjAAO85QADvOcAA7zpAAO86wADvO0AA7zvAAO9AgADvQUAA70IAAO9CwADvQ4AA70RAAO9FAADvRcAA70aAAO9HQADvR8AA71eAAO9YAADvWMAA71lAAO9aAADvWkAA71qAAO9awADvWwAA71uAAO9cAADvXEAA71yAAO9dAADvXUAA71+AAO9fwADvYEAA73AAAO9wgADvcQAA73GAAO9yQADvcoAA73LAAO9zAADvc0AA73PAAO90QADvdIAA73TAAO91QADvdYAA74VAAO+FwADvhoAA74cAAO+HwADviAAA74hAAO+IgADviMAA74lAAO+JwADvigAA74pAAO+KwADviwAA741AAO+NgADvjgAA753AAO+eQADvnsAA759AAO+gAADvoEAA76CAAO+gwADvoQAA76GAAO+iAADvokAA76KAAO+jAADvo0AA77MAAO+zgADvtEAA77TAAO+1gADvtcAA77YAAO+2QADvtoAA77cAAO+3gADvt8AA77gAAO+4gADvuMAA77sAAO+7QADvu8AA78uAAO/MAADvzIAA780AAO/NwADvzgAA785AAO/OgADvzsAA789AAO/PwADv0AAA79BAAO/QwADv0QAA7+DAAO/hQADv4gAA7+KAAO/jQADv44AA7+PAAO/kAADv5EAA7+TAAO/lQADv5YAA7+XAAO/mQADv5oAA7+nAAO/qAADv6kAA7+rAAO/6gADv+wAA7/uAAO/8AADv/MAA7/0AAO/9QADv/YAA7/3AAO/+QADv/sAA7/8AAO//QADv/8AA8AAAAPAPwADwEEAA8BEAAPARgADwEkAA8BKAAPASwADwEwAA8BNAAPATwADwFEAA8BSAAPAUwADwFUAA8BWAAPAawADwHgAA8B9AAPAgAADwIMAA8CIAAPAiwADwI4AA8CQAAPAlQADwOAAA8EDAAPBIwADwUMAA8FFAAPBRwADwUkAA8FLAAPBTgADwU8AA8FQAAPBUwADwVQAA8FWAAPBVwADwVkAA8FcAAPBXQADwV4AA8FhAAPBYgADwWcAA8F0AAPBeQADwXsAA8F9AAPBggADwYUAA8GIAAPBigADwa8AA8HTAAPB+gADwh4AA8IhAAPCIwADwiUAA8InAAPCKQADwisAA8IsAAPCLwADwjwAA8JNAAPCTwADwlEAA8JTAAPCVQADwlcAA8JZAAPCWwADwl0AA8JuAAPCcQADwnQAA8J3AAPCegADwn0AA8KAAAPCgwADwoYAA8KIAAPCxwADwskAA8LLAAPCzQADwtAAA8LRAAPC0gADwtMAA8LUAAPC1gADwtgAA8LZAAPC2gADwtwAA8LdAAPDHAADwx4AA8MgAAPDIgADwyUAA8MmAAPDJwADwygAA8MpAAPDKwADwy0AA8MuAAPDLwADwzEAA8MyAAPDcQADw3MAA8N2AAPDeAADw3sAA8N8AAPDfQADw34AA8N/AAPDgQADw4MAA8OEAAPDhQADw4cAA8OIAAPDlQADw5YAA8OXAAPDmQADw9gAA8PaAAPD3AADw94AA8PhAAPD4gADw+MAA8PkAAPD5QADw+cAA8PpAAPD6gADw+sAA8PtAAPD7gADxC0AA8QvAAPEMQADxDMAA8Q2AAPENwADxDgAA8Q5AAPEOgADxDwAA8Q+AAPEPwADxEAAA8RCAAPEQwADxIIAA8SEAAPEhgADxIgAA8SLAAPEjAADxI0AA8SOAAPEjwADxJEAA8STAAPElAADxJUAA8SXAAPEmAADxNcAA8TZAAPE2wADxN0AA8TgAAPE4QADxOIAA8TjAAPE5AADxOYAA8ToAAPE6QADxOoAA8TsAAPE7QADxSwAA8UuAAPFMAADxTIAA8U1AAPFNgADxTcAA8U4AAPFOQADxTsAA8U9AAPFPgADxT8AA8VBAAPFQgADxWcAA8WLAAPFsgADxdYAA8XZAAPF2wADxd0AA8XfAAPF4QADxeMAA8XkAAPF5wADxfQAA8YDAAPGBQADxgcAA8YJAAPGCwADxg0AA8YPAAPGEQADxiAAA8YjAAPGJgADxikAA8YsAAPGLwADxjIAA8Y1AAPGNwADxnYAA8Z4AAPGegADxnwAA8Z/AAPGgAADxoEAA8aCAAPGgwADxoUAA8aHAAPGiAADxokAA8aLAAPGjAADxssAA8bNAAPGzwADxtEAA8bUAAPG1QADxtYAA8bXAAPG2AADxtoAA8bcAAPG3QADxt4AA8bgAAPG4QADxyAAA8ciAAPHJAADxyYAA8cpAAPHKgADxysAA8csAAPHLQADxy8AA8cxAAPHMgADxzMAA8c1AAPHNgADx3UAA8d3AAPHegADx3wAA8d/AAPHgAADx4EAA8eCAAPHgwADx4UAA8eHAAPHiAADx4kAA8eLAAPHjAADx8sAA8fNAAPHzwADx9EAA8fUAAPH1QADx9YAA8fXAAPH2AADx9oAA8fcAAPH3QADx94AA8fgAAPH4QADyCAAA8giAAPIJAADyCYAA8gpAAPIKgADyCsAA8gsAAPILQADyC8AA8gxAAPIMgADyDMAA8g1AAPINgADyHUAA8h3AAPIeQADyHsAA8h+AAPIfwADyIAAA8iBAAPIggADyIQAA8iGAAPIhwADyIgAA8iKAAPIiwADyNYAA8j5AAPJGQADyTkAA8k7AAPJPQADyT8AA8lBAAPJRAADyUUAA8lGAAPJSQADyUoAA8lMAAPJTQADyU8AA8lSAAPJUwADyVQAA8lXAAPJWAADyWEAA8luAAPJcwADyXUAA8l3AAPJfAADyX8AA8mCAAPJhAADyakAA8nNAAPJ9AADyhgAA8obAAPKHQADyh8AA8ohAAPKIwADyiUAA8omAAPKKQADyjYAA8pHAAPKSQADyksAA8pNAAPKTwADylEAA8pTAAPKVQADylcAA8poAAPKawADym4AA8pxAAPKdAADyncAA8p6AAPKfQADyoAAA8qCAAPKwQADysMAA8rFAAPKxwADysoAA8rLAAPKzAADys0AA8rOAAPK0AADytIAA8rTAAPK1AADytYAA8rXAAPLFgADyxgAA8saAAPLHAADyx8AA8sgAAPLIQADyyIAA8sjAAPLJQADyycAA8soAAPLKQADyysAA8ssAAPLawADy20AA8twAAPLcgADy3UAA8t2AAPLdwADy3gAA8t5AAPLewADy30AA8t+AAPLfwADy4EAA8uCAAPLjwADy5AAA8uRAAPLkwADy9IAA8vUAAPL1gADy9gAA8vbAAPL3AADy90AA8veAAPL3wADy+EAA8vjAAPL5AADy+UAA8vnAAPL6AADzCcAA8wpAAPMKwADzC0AA8wwAAPMMQADzDIAA8wzAAPMNAADzDYAA8w4AAPMOQADzDoAA8w8AAPMPQADzHwAA8x+AAPMgAADzIIAA8yFAAPMhgADzIcAA8yIAAPMiQADzIsAA8yNAAPMjgADzI8AA8yRAAPMkgADzNEAA8zTAAPM1QADzNcAA8zaAAPM2wADzNwAA8zdAAPM3gADzOAAA8ziAAPM4wADzOQAA8zmAAPM5wADzSYAA80oAAPNKgADzSwAA80vAAPNMAADzTEAA80yAAPNMwADzTUAA803AAPNOAADzTkAA807AAPNPAADzWEAA82FAAPNrAADzdAAA83TAAPN1QADzdcAA83ZAAPN2wADzd0AA83eAAPN4QADze4AA839AAPN/wADzgEAA84DAAPOBQADzgcAA84JAAPOCwADzhoAA84dAAPOIAADziMAA84mAAPOKQADziwAA84vAAPOMQADznAAA85yAAPOdAADznYAA855AAPOegADznsAA858AAPOfQADzn8AA86BAAPOggADzoMAA86FAAPOhgADzsUAA87HAAPOyQADzssAA87OAAPOzwADztAAA87RAAPO0gADztQAA87WAAPO1wADztgAA87aAAPO2wADzxoAA88cAAPPHgADzyAAA88jAAPPJAADzyUAA88mAAPPJwADzykAA88rAAPPLAADzy0AA88vAAPPMAADz28AA89xAAPPcwADz3UAA894AAPPeQADz3oAA897AAPPfAADz34AA8+AAAPPgQADz4IAA8+EAAPPhQADz8QAA8/GAAPPyAADz8oAA8/NAAPPzgADz88AA8/QAAPP0QADz9MAA8/VAAPP1gADz9cAA8/ZAAPP2gAD0BkAA9AbAAPQHQAD0B8AA9AiAAPQIwAD0CQAA9AlAAPQJgAD0CgAA9AqAAPQKwAD0CwAA9AuAAPQLwAD0G4AA9BwAAPQcgAD0HQAA9B3AAPQeAAD0HkAA9B6AAPQewAD0H0AA9B/AAPQgAAD0IEAA9CDAAPQhAAD0I0AA9COAAPQkAAD0J0AA9CeAAPQnwAD0KEAA9CuAAPQrwAD0LAAA9CyAAPQvwAD0MAAA9DBAAPQwwAD0MwAA9DbAAPQ6AAD0PcAA9EJAAPRHQAD0TQAA9FGAAPRTwAD0VAAA9FSAAPRXwAD0WAAA9FhAAPRYwAD0WwAA9F2AAPRfQAAAAAAAAQCAAAAAAAAkzcAAAAAAAAAAAAAAAAAA9GF +cxIAAYagXxAPTlNLZXllZEFyY2hpdmVy0QAIAAlUcm9vdIABrxELiAALAAwAGwA3ADgAOQBhAGIAYwBkAGUAZgBnAGgAaQBqAGsAbABtAG4AbwBwAHEAcgCNAI4AjwCVAJYAogC4ALkAugC7ALwAvQC+AL8AwADBANoA3QDkAOoA+QEIAQsBGgEpASwAjAE8AUsBTwFTAWIBaAFpAXEBgAGBAYoBlgGXAZgBmQGaAa8BsAG4AbkBugHGAdoB2wHcAd0B3gHfAeAB4QHiAfECAAIPAhMCIgIxAjICQQJQAl8CawJ9An4CfwKAAoECggKDAoQAMQKTAqICowKyAsEC2wLcAuIC7gMEAxMDFgMlAzQDOAM8Az0DTANbA14DbQN8A4ADjwOeA9gD2QPaA9sD3APdA94D3wPgA+ED4gPjA+QD5QPmA+cD6APpA+oD6wPsA+0D7gPvA/AD8QQGBAcEDwQbBC8EPgRNBFwEYARvBH4EjQScBKsEtwTJBNgE5wT2BQUFFAUjBTIFOgVPBVAFWAVZBWUFeQWIBZcFpgWqBbkFyAXXBeYF9QYBBhMGFAYVBhYGFwYYBhkGGgYpBioGOQZIBlcGWAZnBnYGhQaNBqIGowarBrcGywbaBukG+Ab8BwsHGgcpBzgHRwdTB2UHdAeDB5IHoQeiB7EHwAfPB+QH5QftB/kIDQgcCCsIOgg+CE0IXAhrCHoIiQiVCKcItgjFCNQI4wjkCPMJAgkRCSYJJwkvCTsJTwleCW0JfAmACY8JngmtCbwJywnXCekJ+AoHChYKJQomCjUKRApFClQKaQpqCnIKfgqSCqEKsAq/CsMK0grhCvAK/wsOCxoLLAs7CzwLSwtaC2kLagt5C4gLlwusC60LtQvBC9UL5AvzDAIMBgwVDCQMMwxCDFEMXQxvDH4MjQycDKsMugzJDNgM7QzuDPYNAg0WDSUNNA1DDUcNVg1lDXQNgw2SDZ4NsA2/Dc4N3Q3sDfsOCg4ZDi4OLw43DkMOVw5mDnUOhA6IDpcOpg61DsQO0w7fDvEPAA8PDx4PLQ88D0sPWg9vD3APeA+ED5gPpw+2D8UPyQ/YD+cP9hAFEBQQIBAyEEEQUBBfEG4QfRCMEJsQsBCxELkQxRDZEOgQ9xEGEQoRGREoETcRRhFVEWERcxGCEZERoBGvEb4RzRHcEfER8hH6EgYSGhI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EWJNYolixWMBY1VjWWN5Y6lj+WQ1ZHFkrWS9ZPllNWVxZa1l6WYZZmFmnWbZZxVnUWdVZ5FnzWgJaF1oYWiBaLFpAWk9aXlptWnFagFqPWp5arVq8Wsha2lrpWvhbB1sWWyVbNFtDW1hbWVthW21bgVuQW59brluyW8Fb0FvfW+5b/VwJXBtcKlw5XEhcV1xmXHVchFyHXKFcolyoXLRcylzZXNxc61z6XP1dDF0bXR5dLV08XUBdT11eXV9dbV1uXW9dcF1xXXJdh12IXZBdnF2wXb9dzl3dXeFd8F3/Xg5eHV4sXjheSl5ZXmhed16GXpVepF6lXrReyV7KXtJe3l7yXwFfEF8fXyNfMl9BX1BfX19uX3pfjF+bX6pfuV/IX9df5l/nX/ZgC2AMYBRgIGA0YENgUmBhYGVgdGCDYJJgoWCwYLxgzmDdYOxg+2EKYRlhKGE3YUxhTWFVYWFhdWGEYZNhomGmYbVhxGHTYeJh8WH9Yg1iHGI2YjdiPWJJYl9ibmJxYoBij2KSYqFisGKzYsJi0WLVYuRi82L0Yv5i/2MAYxVjFmMeYypjPmNNY1xja2NvY35jjWOcY6tjumPGY9hj52P2ZAVkFGQjZDJkQWRWZFdkX2RrZH9kjmSdZKxksGS/ZM5k3WTsZPtlB2UZZShlN2VGZVVlZGVzZYJll2WYZaBlrGXAZc9l3mXtZfFmAGYPZh5mLWY8ZkhmWmZpZnhmh2aWZpdmpma1ZsRmx2bWZuVm9GcDZxJnJ2coZzBnPGdQZ19nbmd9Z4FnkGefZ65nvWfMZ9hn6mf5aAhoF2gmaDVoRGhTaFZocGhxaHdog2iZaKhoq2i6aMlozGjbaOpo7Wj8aQtpD2keaS1pLmk2aTdpOGlNaU5pVmliaXZphWmUaaNpp2m2acVp1GnjafJp/moQah9qLmo9akxqW2pqanlqjmqPapdqo2q3asZq1Wrkauhq92sGaxVrJGszaz9rUWtga29rfmuNa5xrq2u6a71r12vYa95r6mwAbA9sEmwhbDBsM2xCbFFsVGxjbHJsdmyFbJRslWyjbKRspWymbLtsvGzEbNBs5GzzbQJtEW0VbSRtM21CbVFtYG1sbX5tjW2cbattum3Jbdht5238bf1uBW4RbiVuNG5DblJuVm5lbnRug26SbqFurW6/bs5u3W7sbvtvCm8ZbyhvPW8+b0ZvUm9mb3VvhG+Tb5dvpm+1b8Rv02/ib+5wAHAPcB5wLXA8cEtwWnBpcH5wf3CHcJNwp3C2cMVw1HDYcOdw9nEFcRRxI3EvcUFxUHFfcW5xfXGMcZtxqnG/ccBxyHHUcehx93IGchVyGXIocjdyRnJVcmRycHKCcpFyoHKvcr5yzXLccuty7nMIcwlzD3MbczFzQHNDc1JzYXNkc3NzgnOFc5Rzo3Onc7ZzxXPGc9Jz03PUc+lz6nPyc/50EnQhdDB0P3RDdFJ0YXRwdH90jnSadKx0u3TKdNl06HT3dQZ1FXUqdSt1M3U/dVN1YnVxdYB1hHWTdaJ1sXXAdc9123Xtdfx2C3Yadil2OHZHdlZ2a3ZsdnR2gHaUdqN2snbBdsV21HbjdvJ3AXcQdxx3Lnc9d0x3W3dqd3l3iHeXd6x3rXe1d8F31Xfkd/N4AngGeBV4JHgzeEJ4UXhdeG94fniNeJx4q3i6eMl42HjbePV49nj8eQh5HnkteTB5P3lOeVF5YHlveXJ5gXmQeZR5o3myebN5zXnOec950HnRedJ503nUedV51nnXeex57Xn1egF6FXokejN6QnpGelV6ZHpzeoJ6kXqdeq96vnrNetx663r6ewl7GHstey57NntCe1Z7ZXt0e4N7h3uWe6V7tHvDe9J73nvwe/98DnwdfCx8O3xKfFl8bnxvfHd8g3yXfKZ8tXzEfMh813zmfPV9BH0TfR99MX1AfU99Xn1tfXx9i32afa99sH24fcR92H3nffZ+BX4Jfhh+J342fkV+VH5gfnJ+gX6Qfp9+rn69fsx+237wfvF++X8Ffxl/KH83f0Z/Sn9Zf2h/d3+Gf5V/oX+zf8J/0X/gf+9//oANgByAMYAygDqARoBagGmAeICHgIuAmoCpgLiAx4DWgOKA9IEDgRKBIYEwgT+BToFdgXKBc4F7gYeBm4GqgbmByIHMgduB6oH5ggiCF4IjgjWCRIJTgmKCcYKAgo+CnoKzgrSCvILIgtyC64L6gwmDDYMcgyuDOoNJg1iDZIN2g4WDlIOjg7KDwYPQg9+D9IP1g/2ECYQdhCyEO4RKhE6EXYRshHuEioSZhKWEt4TGhNWE5ITzhQKFEYUghTWFNoU+hUqFXoVthXyFi4WPhZ6FrYW8hcuF2oXmhfiGB4YWhiWGNIZDhlKGYYZ2hneGf4aLhp+Groa9hsyG0Ibfhu6G/YcMhxuHJ4c5h0iHV4dmh3WHhIeTh6KHpYe/h8CHxofSh+iH94f6iAmIGIgbiCqIOYg8iEuIWoheiG2IfIiGiJuInIikiLCIxIjTiOKI8Yj1iQSJE4kiiTGJQIlMiV6JbYl8iYuJmompibiJx4ncid2J5YnxigWKFIojijKKNopFilSKY4pyioGKjYqfiq6KvYrMituK6or5iwiLHYseiyaLMotGi1WLZItzi3eLhouVi6SLs4vCi86L4Ivvi/6MDYwcjCuMOoxJjEyMZoxnjG2MeYyPjJ6MoYywjL+MwozRjOCM44zyjQGNBY0UjSONJI0ujS+NMI1FjUaNTo1ajW6NfY2MjZuNn42ujb2NzI3bjeqN9o4IjheOJo41jkSOU45ijnGOho6Hjo+Om46vjr6OzY7cjuCO747+jw2PHI8rjzePSY9Yj2ePdo+Fj5SPo4+yj8ePyI/Qj9yP8I//kA6QHZAhkDCQP5BOkF2QbJB4kIqQmZCokLeQxpDVkOSQ85D2kRCREZEXkSOROZFIkUuRWpFpkWyRe5GKkY2RnJGrka+RvpHNkc6R1pHXkeyR7ZH1kgGSFZIkkjOSQpJGklWSZJJzkoKSkZKdkq+SvpLNktyS65L6kwmTGJMtky6TNpNCk1aTZZN0k4OTh5OWk6WTtJPDk9KT3pPwk/+UDpQdlCyUO5RKlFmUXJRglGSUaJRwlHOUd1UkbnVsbNcADQAOAA8AEAARABIAEwAUABUAFgAXABgAFwAaXxAPX3hkX3Jvb3RQYWNrYWdlViRjbGFzc1xfeGRfY29tbWVudHNfEBBfeGRfbW9kZWxNYW5hZ2VyXxAVX2NvbmZpZ3VyYXRpb25zQnlOYW1lXV94ZF9tb2RlbE5hbWVfEBdfbW9kZWxWZXJzaW9uSWRlbnRpZmllcoACgQuHgQuFgACBC4aAAIEEGt4AHAAdAB4AHwAgACEAIgAOACMAJAAlACYAJwAoACkAKgArAAkAKQAXAC8AMAAxADIAMwApACkAF18QHFhEQnVja2V0Rm9yQ2xhc3Nlc3dhc0VuY29kZWRfEBpYREJ1Y2tldEZvclBhY2thZ2Vzc3RvcmFnZV8QHFhEQnVja2V0Rm9ySW50ZXJmYWNlc3N0b3JhZ2VfEA9feGRfb3duaW5nTW9kZWxfEB1YREJ1Y2tldEZvclBhY2thZ2Vzd2FzRW5jb2RlZFZfb3duZXJfEBtYREJ1Y2tldEZvckRhdGFUeXBlc3N0b3JhZ2VbX3Zpc2liaWxpdHlfEBlYREJ1Y2tldEZvckNsYXNzZXNzdG9yYWdlVV9uYW1lXxAfWERCdWNrZXRGb3JJbnRlcmZhY2Vzd2FzRW5jb2RlZF8QHlhEQnVja2V0Rm9yRGF0YVR5cGVzd2FzRW5jb2RlZF8QEF91bmlxdWVFbGVtZW50SUSABIELg4ELgYABgASAAIELgoELhBAAgAWAA4AEgASAAFBTWUVT0wA6ADsADgA8AE4AYFdOUy5rZXlzWk5TLm9iamVjdHOvEBEAPQA+AD8AQABBAEIAQwBEAEUARgBHAEgASQBKAEsATABNgAaAB4AIgAmACoALgAyADYAOgA+AEIARgBKAE4AUgBWAFq8QEQBPAFAAUQBSAFMAVABVAFYAVwBYAFkAWgBbAFwAXQBeAF+AF4BqgQZrgQG4gQaxgQc+gQTwgQgpgQhugQevgQj5gQPEgQlsgQV5gQqIgQrhgQs9gDVdQ0RFeHRlcm5hbEFwaVdDRENoYWluXUNEU2luZ2xlVmFsdWVXQ0RBc3NldF1DRENvbnRhY3RJdGVtXxAZQ0RQb2xrYXN3YXBSZW1vdGVTZXR0aW5nc18QEENEQ2hhaW5YY21Db25maWdbQ0RTdGFzaEl0ZW1fEBVDRFJ1bnRpbWVNZXRhZGF0YUl0ZW1eQ0RQb2xrYXN3YXBEZXhaQ0RTY2FtSW5mb1tDRENoYWluTm9kZV8QGENEVHJhbnNhY3Rpb25IaXN0b3J5SXRlbV8QGUNEWGNtQXZhaWxhYmxlRGVzdGluYXRpb25ZQ0RDb250YWN0XkNEUGhpc2hpbmdJdGVtXxASQ0RDaGFpblN0b3JhZ2VJdGVt3xAQAHMAdAB1AHYAIQB3AHgAIwB5AHoADgAlAHsAfAAoAH0AfgB/ACkAKQAUAIMAhAAxACkAfgCHAD0AfgCKAIsAjF8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzb3JkZXJlZF8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzc3RvcmFnZV8QIFhEQnVja2V0Rm9yU3RlcmVvdHlwZXN3YXNFbmNvZGVkXxAkWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXN3YXNFbmNvZGVkXxAdWERCdWNrZXRGb3JTdGVyZW90eXBlc3N0b3JhZ2VfECRYREJ1Y2tldEZvckdlbmVyYWxpemF0aW9uc2R1cGxpY2F0ZXNfECRYREJ1Y2tldEZvckdlbmVyYWxpemF0aW9uc3dhc0VuY29kZWRfEB1YREJ1Y2tldEZvclN0ZXJlb3R5cGVzb3JkZXJlZF8QIVhEQnVja2V0Rm9yR2VuZXJhbGl6YXRpb25zb3JkZXJlZF8QIVhEQnVja2V0Rm9yR2VuZXJhbGl6YXRpb25zc3RvcmFnZVtfaXNBYnN0cmFjdIAZgD2ABIAEgAKAGoEDS4AEgBmBA02ABoAZgQZqgBgIEwAAAAEYayQ4V29yZGVyZWTTADoAOwAOAJAAkgBgoQCRgBuhAJOAHIA1XlhEX1BTdGVyZW90eXBl2QAhACUAlwAOACgAmAAjAH0AmQBPAJEAfgCdABcAKQAxAIwAoV8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzb3JkZXJlZF8QJFhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzd2FzRW5jb2RlZF8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzc3RvcmFnZYAXgBuAGYA8gACABAiAHdMAOgA7AA4AowCtAGCpAKQApQCmAKcAqACpAKoAqwCsgB6AH4AggCGAIoAjgCSAJYAmqQCuAK8AsACxALIAswC0ALUAtoAngCuALIAugC+AMYAzgDaAOoA1XxATWERQTUNvbXBvdW5kSW5kZXhlc18QEFhEX1BTS19lbGVtZW50SURfEBlYRFBNVW5pcXVlbmVzc0NvbnN0cmFpbnRzXxAaWERfUFNLX3ZlcnNpb25IYXNoTW9kaWZpZXJfEBlYRF9QU0tfZmV0Y2hSZXF1ZXN0c0FycmF5XxARWERfUFNLX2lzQWJzdHJhY3RfEA9YRF9QU0tfdXNlckluZm9fEBNYRF9QU0tfY2xhc3NNYXBwaW5nXxAWWERfUFNLX2VudGl0eUNsYXNzTmFtZd8QDwDCAMMAxAAhAMUAxgDHACMAyAAOACUAyQDKACgAywAXAM0AFwCTAIwAjACMADEAjADUAKQAjACMABcAjFVfdHlwZVhfZGVmYXVsdFxfYXNzb2NpYXRpb25bX2lzUmVhZE9ubHlZX2lzU3RhdGljWV9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F3IEF84EGCoA1WWV4cGxvcmVyc11hZGRyZXNzUHJlZml4XXN0YWtpbmdBcGlVcmxXb3B0aW9uc11oYXNDcm93ZGxvYW5zVGljb25eaGlzdG9yeUFwaVR5cGVdaXNUaXBSZXF1aXJlZFRuYW1lXxARbWluaW1hbEFwcFZlcnNpb25WYXNzZXRzXxATdHlwZXNPdmVycmlkZUNvbW1vbllpc1Rlc3RuZXRec3Rha2luZ0FwaVR5cGVVbm9kZXNcc2VsZWN0ZWROb2RlWHBhcmVudElkVmlzT3JtbFhkaXNhYmxlZF8QEWNyb3dkbG9hbnNBcGlUeXBlXxAQY3Jvd2Rsb2Fuc0FwaVVybFl4Y21Db25maWdfEA9pc0V0aGVyZXVtQmFzZWRdaGlzdG9yeUFwaVVybFtjdXN0b21Ob2Rlc98QEgDCAMMAxAPyACEAxgDHA/MAIwDFA/QAyAAOACUAyQDKACgAywAXABcAFwApAFAAjACMA/wAMQCMAH4AjAGpA6AAjACMBAQAjF8QIFhEQnVja2V0Rm9yU3RlcmVvdHlwZXN3YXNFbmNvZGVkXxAdWERCdWNrZXRGb3JTdGVyZW90eXBlc3N0b3JhZ2VfEB1YREJ1Y2tldEZvclN0ZXJlb3R5cGVzb3JkZXJlZIAAgACAAIAEgGoICICaCIAZCICvgH8ICICZCBJPT7+H0wA6ADsADgQIBAsAYKIBsgGzgEWARqIEDAQNgJuApoA12QAhACUEEAAOACgEEQAjAH0EEgO8AbIAfgCdABcAKQAxAIwEGl8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzb3JkZXJlZF8QJFhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzd2FzRW5jb2RlZF8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzc3RvcmFnZYCYgEWAGYA8gACABAiAnNMAOgA7AA4EHAQlAGCoAcgByQHKAcsBzAHNAc4Bz4BJgEqAS4BMgE2AToBPgFCoBCYEJwQoBCkEKgQrBCwELYCdgJ6An4ChgKKAo4CkgKWANd8QDwDCAMMAxAAhAMUAxgDHACMAyAAOACUAyQDKACgAywAXAS4AFwQMAIwAjACMADEAjADUAcgAjACMABcAjIAAgDKAAICbCAgICIAqgEkICIAACN8QDwDCAMMAxAAhAMU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ICAgIgCqAUAgIgAAI2QAhACUSlwAOACgSmAAjAH0SmQPHAbMAfgCdABcAKQAxAIwSoV8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzb3JkZXJlZF8QJFhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzd2FzRW5jb2RlZF8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzc3RvcmFnZYEBp4BGgBmAPIAAgAQIgQG20wA6ADsADhKjEqoAYKYCcAJyAm0CbgJvAnGAYIBigF2AXoBfgGGmEqsSrBKtEq4SrxKwgQG3gQNOgQNPgQNQgQNRgQNSgDXfEA8AwgDDAMQAIQDFAMYAxwAjAMgADgAlAMkAygAoAMsAFwBSABcR+ACMAIwAjAAxAIwA1AJwAIwAjAAXAIyAAIEBuIAAgQG1CAgICIAqgGAICIAACN8QEBLCEsMSxBLFACESxhLHACMSyBLJAA4AJRLKEssAKAB9AH4SzQApACkAFBLRAIQAMQApAH4AhwBAAH4S2BLZAIxfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc29yZGVyZWRfECFYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3N0b3JhZ2VfECBYREJ1Y2tldEZvclN0ZXJlb3R5cGVzd2FzRW5jb2RlZF8QJFhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzd2FzRW5jb2RlZF8QHVhEQnVja2V0Rm9yU3RlcmVvdHlwZXNzdG9yYWdlXxAkWERCdWNrZXRGb3JHZW5lcmFsaXphdGlvbnNkdXBsaWNhdGVzXxAkWERCdWNrZXRGb3JHZW5lcmFsaXphdGlvbnN3YXNFbmNvZGVkXxAdWERCdWNrZXRGb3JTdGVyZW90eXBlc29yZGVyZWRfECFYREJ1Y2tldEZvckdlbmVyYWxpemF0aW9uc29yZGVyZWRfECFYREJ1Y2tldEZvckdlbmVyYWxpemF0aW9uc3N0b3JhZ2WAGYEByoAEgASAAoEBuoEDS4AEgBmBA02ACYAZgQNMgQG5CBKwZdhq0wA6ADsADhLdEt8AYKEAkYAboRLggQG7gDXZACEAJRLjAA4AKBLkACMAfRLlAFIAkQB+AJ0AFwApADEAjBLtXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNvcmRlcmV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OACUAyQDKACgAywAXABcAFzRbAIwAjACMADEAjADUBgMAjACMABcAjIAAgACAAIEERggICAiAKoDBCAiAAAjfEA8AwgDDAMQAIQDFAMYAxwAjAMgADgAlAMkAygAoAMsAFwEuABc0WwCMAIwAjAAxAIwA1AYEAIwAjAAXAIyAAIAygACBBEYICAgIgCqAwggIgAAI3xAPAMIAwwDEACEAxQDGAMcAIwDIAA4AJQDJAMoAKADLABcAFwAXNFsAjACMAIwAMQCMANQGBQCMAIwAFwCMgACAAIAAgQRGCAgICIAqgMMICIAACN8QDwDCAMMAxAAhAMUAxgDHACMAyAAOACUAyQDKACgAywAXB5QAFzRbAIwAjACMADEAjADUBgYAjACMABcAjIAAgOaAAIEERggICAiAKoDECAiAAAjfEA8AwgDDAMQAIQDFAMYAxwAjAMgADgAlAMkAygAoAMsAFwAXABc0WwCMAIwAjAAxAIwA1AYHAIwAjAAXAIyAAIAAgACBBEYICAgIgCqAxQgIgAAI3xAPAMIAwwDEACEAxQDGAMcAIwDIAA4AJQDJAMoAKADLABcAFwAXNFsAjACMAIwAMQCMANQGCACMAIwAFwCMgACAAIAAgQRGCAgICIAqgMYICIAACN8QDwDCAMMAxAAhAMUAxgDHACMAyAAOACUAyQDKACgAywAXABcAFzRbAIwAjACMADEAjADUBgkAjACMABcAjIAAgACAAIEERggICAiAKoDHCAiAAAjSADsADjWBANyggCnfEA8AwgDDAMQAIQDFAMYAxwAjAMgADgAlAMkAygAoAMsAFwIkABctVACMAIwAjAAxAIwA1AJxAIwAjAAXAIyAAIBXgACBA70ICAgIgCqAYQgIgAAI3xAPAMIAwwDEACEAxQDGAMcAIwDIAA4AJQDJAMoAKADLABcBLgAXLVQAjACMAIwAMQCMANQCcgCMAIwAFwCMgACAMoAAgQO9CAgICIAqgGIICIAACN8QDwDCAMMAxAAhAMUAxgDHACMAyAAOACUAyQDKACgAywAXLzMAFy1UAIwAjACMADEAjADUAnMAjACMABcAjIAAgQPygACBA70ICAgIgCqAYwgIgAAI3xASAMIAwwDENbEAIQDGAMc1sgAjAMU1swDIAA4AJQDJAMoAKADLABcAFwAXACkAUACMAIw1uwAxAIw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SADsADmhUANyggCnfEBBoV2hYaFloWgAhaFtoXAAjaF1oXgAOACVoX2hgACgAfQB+aGIAKQApABRoZgCEADEAKQB+AIcARAB+aG1obgCMXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNvcmRlcmVkXxAhWERCdWNrZXRGb3JPd25lZEF0dHJpYnV0ZXNzdG9yYWdlXxAgWERCdWNrZXRGb3JTdGVyZW90eXBlc3dhc0VuY29kZWRfECRYREJ1Y2tldEZvck93bmVkQXR0cmlidXRlc3dhc0VuY29kZWRfEB1YREJ1Y2tldEZvclN0ZXJlb3R5cGVzc3RvcmFnZV8QJFhEQnVja2V0Rm9yR2VuZXJhbGl6YXRpb25zZHVwbGljYXRlc18QJFhEQnVja2V0Rm9yR2VuZXJhbGl6YXRpb25zd2FzRW5jb2RlZF8QHVhEQnVja2V0Rm9yU3RlcmVvdHlwZXNvcmRlcmVkXxAhWERCdWNrZXRGb3JHZW5lcmFsaXphdGlvbnNvcmRlcmVkXxAhWERCdWNrZXRGb3JHZW5lcmFsaXphdGlvbnNzdG9yYWdlgBmBCDyABIAEgAKBCCuBA0uABIAZgQNNgA2AGYEIbYEIKggSa44ZL9MAOgA7AA5ocmh0AGChAJGAG6FodYEILIA12QAhACVoeAAOAChoeQAjAH1oegBWAJEAfgCdABcAKQAxAIxogl8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzb3JkZXJlZF8QJFhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzd2FzRW5jb2RlZF8QIVhEQnVja2V0Rm9yT3duZWRBdHRyaWJ1dGVzc3RvcmFnZYEIKYAbgBmAPIAAgAQIgQgt0wA6ADsADmiEaI4AYKkApAClAKYApwCoAKkAqgCrAKyAHoAfgCCAIYAigCOAJIAlgCapaI9okGiRaJJok2iUaJVolmiXgQgugQgwgQgxgQgzgQg0gQg2gQg3gQg5gQg6gDXfEA8AwgDDAMQAIQDFAMYAxwAjAMgADgAlAMkAygAoAMsAF2ibABdodQCMAIwAjAAxAIwA1ACkAIwAjAAXAIyAAIEIL4AAgQgsCAgICIAqgB4ICIAACNIAOwAOaKkA3KCAKd8QDwDCAMMAxAAhAMUAxgDHACMAyAAOACUAyQDKACgAywA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/AAAdggAAHYQAAB2HAAAdiQAAHYsAAB2aAAAdvwAAHeMAAB4KAAAeLgAAHjAAAB4yAAAeNAAAHjYAAB44AAAeOgAAHjsAAB49AAAeSgAAHl0AAB5fAAAeYQAAHmMAAB5lAAAeZwAAHmkAAB5rAAAebQAAHm8AAB6CAAAehAAAHoYAAB6IAAAeigAAHowAAB6OAAAekAAAHpIAAB6UAAAelgAAHqwAAB6/AAAe2wAAHvgAAB8UAAAfKAAAHzoAAB9QAAAfaQAAH6gAAB+uAAAftwAAH8QAAB/QAAAf2gAAH+QAAB/vAAAf+gAAIAcAACAPAAAgEQAAIBMAACAVAAAgFwAAIBgAACAZAAAgGgAAIBsAACAdAAAgHwAAICAAACAhAAAgIwAAICQAACAtAAAgLgAAIDAAACA5AAAgRAAAIE0AACBcAAAgYwAAIGsAACB0AAAgfQAAIJAAACCZAAAgrAAAIMMAACDVAAAhFAAAIRYAACEYAAAhGgAAIRwAACEdAAAhHgAAIR8AACEgAAAhIgAAISQAACElAAAhJgAAISgAACEpAAAhaAAAIWoAACFsAAAhbgAAIXAAACFxAAAhcgAAIXMAACF0AAAhdgAAIXgAACF5AAAhegAAIXwAACF9AAAhhgAAIYcAACGJAAAhyAAAIcoAACHMAAAhzgAAIdAAACHRAAAh0gAAIdMAACHUAAAh1gAAIdgAACHZAAAh2gAAIdwAACHdAAAiHAAAIh4AACIgAAAiIgAAIiQAACIlAAAiJgAAIicAACIoAAAiKgAAIiwAACItAAAiLgAAIjAAACIxAAAiOgAAIjsAACI9AAAifAAAIn4AACKAAAAiggAAIoQAACKFAAAihgAAIocAACKIAAAiigAAIowAACKNAAAijgAAIpAAACKRAAAikgAAItEAACLTAAAi1QAAItcAACLZAAAi2gAAItsAACLcAAAi3QAAIt8AACLhAAAi4gAAIuMAACLlAAAi5gAAIvMAACL0AAAi9QAAIvcAACMAAAAjFgAAIx0AACMqAAAjaQAAI2sAACNtAAAjbwAAI3EAACNyAAAjcwAAI3QAACN1AAAjdwAAI3kAACN6AAAjewAAI30AACN+AAAjlwAAI5kAACObAAAjnQAAI54AACOgAAAjtwAAI8AAACPOAAAj2wAAI+kAACP+AAAkEgAAJCkAACQ7AAAkegAAJHwAACR+AAAkgAAAJIIAACSDAAAkhAAAJIUAACSGAAAkiAAAJIoAACSLAAAkjAAAJI4AACSPAAAknQAAJKYAACS7AAAkygAAJN8AACTtAAAlAgAAJRYAACUtAAAlPwAAJUwAACVVAAAlVwAAJVkAACVbAAAlXQAAJWYAACVoAAAlawAAJW4AACVxAAAlcwAAJXkAACV/AAAlhAAAJYgAACXTAAAl9gAAJhYAACY2AAAmOAAAJjoAACY8AAAmPgAAJkAAACZBAAAmQgAAJkQAACZFAAAmRwAAJkgAACZKAAAmTAAAJk0AACZOAAAmUAAAJlEAACZWAAAmYwAAJmgAACZqAAAmbAAAJnEAACZzAAAmdQAAJncAACaMAAAmnwAAJsQAACboAAAnDwAAJzMAACc1AAAnNwAAJzkAACc7AAAnPQAAJz8AACdAAAAnQgAAJ08AACdgAAAnYgAAJ2QAACdmAAAnaAAAJ2oAACdsAAAnbgAAJ3AAACeBAAAngwAAJ4UAACeHAAAniQAAJ4sAACeNAAAnjwAAJ5EAACeTAAAnsQAAJ88AACfiAAAn9gAAKAsAACgoAAAoPAAAKFIAACiRAAAokwAAKJUAACiXAAAomQAAKJoAACibAAAonAAAKJ0AACifAAAooQAAKKIAACijAAAopQAAKKYAACjlAAAo5wAAKOkAACjrAAAo7QAAKO4AACjvAAAo8AAAKPEAACjzAAAo9QAAKPYAACj3AAAo+QAAKPoAACk5AAApOwAAKT0AACk/AAApQQAAKUIAAClDAAApRAAAKUUAAClHAAApSQAAKUoAAClLAAApTQAAKU4AAClbAAApXAAAKV0AAClfAAApngAAKaAAACmiAAAppAAAKaYAACmnAAApqAAAKakAACmqAAAprAAAKa4AACmvAAApsAAAKbIAACmzAAAp8gAAKfQAACn2AAAp+AAAKfoAACn7AAAp/AAAKf0AACn+AAAqAAAAKgIAACoDAAAqBAAAKgYAACoHAAAqCAAAKkcAACpJAAAqSwAAKk0AACpPAAAqUAAAKlEAACpSAAAqUwAAKlUAACpXAAAqWAAAKlkAACpbAAAqXAAAKpsAACqdAAAqnwAAKqEAACqjAAAqpAAAKqUAACqmAAAqpwAAKqkAACqrAAAqrAAAKq0AACqvAAAqsAAAKu8AACrxAAAq8wAAKvUAACr3AAAq+AAAKvkAACr6AAAq+wAAKv0AACr/AAArAAAAKwEAACsDAAArBAAAKykAACtNAAArdAAAK5gAACuaAAArnAAAK54AACugAAArogAAK6QAACulAAArpwAAK7QAACvDAAArxQAAK8cAACvJAAArywAAK80AACvPAAAr0QAAK+AAACviAAAr5AAAK+YAACvoAAAr6wAAK+4AACvxAAAr8wAALAUAACwZAAAsKwAALEAAACxSAAAsYQAALH4AACy9AAAsvwAALMEAACzDAAAsxQAALMYAACzHAAAsyAAALMkAACzLAAAszQAALM4AACzPAAAs0QAALNIAAC0RAAAtEwAALRUAAC0XAAAtGQAALRoAAC0bAAAtHAAALR0AAC0fAAAtIQAALSIAAC0jAAAtJQAALSYAAC0oAAAtZwAALWkAAC1rAAAtbQAALW8AAC1wAAAtcQAALXIAAC1zAAAtdQAALXcAAC14AAAteQAALXsAAC18AAAtuwAALb0AAC2/AAAtwQAALcMAAC3EAAAtxQAALcYAAC3HAAAtyQAALcsAAC3MAAAtzQAALc8AAC3QAAAuEwAALjcAAC5bAAAufgAALqUAAC7FAAAu7AAALxMAAC8zAAAvVwAAL3sAAC99AAAvfwAAL4EAAC+DAAAvhQAAL4cAAC+KAAAvjAAAL44AAC+RAAAvkwAAL5UAAC+YAAAvmgAAL5sAAC+gAAAvrQAAL7AAAC+yAAAvtQAAL7cAAC+5AAAv3gAAMAIAADApAAAwTQAAME8AADBRAAAwUwAAMFUAADBXAAAwWQAAMFoAADBcAAAwaQAAMHwAADB+AAAwgAAAMIIAADCEAAAwhgAAMIgAADCKAAAwjAAAMI4AADChAAAwowAAMKUAADCnAAAwqQAAMKsAADCtAAAwrwAAMLEAADCzAAAwtQAAMPQAADD2AAAw+AAAMPoAADD8AAAw/QAAMP4AADD/AAAxAAAAMQIAADEEAAAxBQAAMQYAADEIAAAxCQAAMRIAADETAAAxFQAAMVQAADFWAAAxWAAAMVoAADFcAAAxXQAAMV4AADFfAAAxYAAAMWIAADFkAAAxZQAAMWYAADFoAAAxaQAAMagAADGqAAAxrAAAMa4AADGwAAAxsQAAMbIAADGzAAAxtAAAMbYAADG4AAAxuQAAMboAADG8AAAxvQAAMcYAADHJAAAxywAAMc0AADHWAAAx2QAAMdsAADHdAAAx5QAAMiQAADImAAAyKAAAMioAADIsAAAyLQAAMi4AADIvAAAyMAAAMjIAADI0AAAyNQAAMjYAADI4AAAyOQAAMngAADJ6AAAyfAAAMn4AADKAAAAygQAAMoIAADKDAAAyhAAAMoYAADKIAAAyiQAAMooAADKMAAAyjQAAMpYAADKXAAAymQAAMtgAADLaAAAy3AAAMt4AADLgAAAy4QAAMuIAADLjAAAy5AAAMuYAADLoAAAy6QAAMuoAADLsAAAy7QAAMywAADMuAAAzMAAAMzIAADM0AAAzNQAAMzYAADM3AAAzOAAAMzoAADM8AAAzPQAAMz4AADNAAAAzQQAAM04AADNPAAAzUAAAM1IAADORAAAzkwAAM5UAADOXAAAzmQAAM5oAADObAAAznAAAM50AADOfAAAzoQAAM6IAADOjAAAzpQAAM6YAADPlAAAz5wAAM+kAADPrAAAz7QAAM+4AADPvAAAz8AAAM/EAADPzAAAz9QAAM/YAADP3AAAz+QAAM/oAADQHAAA0QAAANEIAADREAAA0RgAANEgAADRKAAA0TAAANE4AADRQAAA0UgAANFQAADRWAAA0WAAANFoAADRcAAA0XgAANGAAADRiAAA0ZAAANGYAADRoAAA0agAANGwAADRuAAA0cAAANHIAADR0AAA0dgAANK8AADSxAAA0swAANLUAADS3AAA0ugAANL0AADTAAAA0wwAANMYAADTJAAA0zAAANM8AADTSAAA01QAANNgAADTbAAA03gAANOEAADTkAAA05wAANOoAADTtAAA08AAANPMAADT2AAA0+QAANPwAADT+AAA1CAAANRYAADUkAAA1LAAANToAADU/AAA1TgAANVwAADVhAAA1dQAANXwAADWSAAA1nAAANasAADWxAAA1vgAANccAADXOAAA11wAANesAADX+AAA2CAAANhoAADYoAAA2NAAANn8AADaiAAA2wgAANuIAADbkAAA25gAANugAADbqAAA27AAANu0AADbuAAA28AAANvEAADbzAAA29AAANvYAADb4AAA2+QAANvoAADb8AAA2/QAANwIAADcPAAA3FAAANxYAADcYAAA3HQAANx8AADchAAA3IwAAN0gAADdsAAA3kwAAN7cAADe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FAABChgAAQsUAAELHAABCyQAAQssAAELNAABCzgAAQs8AAELQAABC0QAAQtMAAELVAABC1gAAQtcAAELZAABC2gAAQxkAAEMbAABDHQAAQx8AAEMhAABDIgAAQyMAAEMkAABDJQAAQycAAEMpAABDKgAAQysAAEMtAABDLgAAQ1MAAEN3AABDngAAQ8IAAEPEAABDxgAAQ8gAAEPKAABDzAAAQ84AAEPPAABD0QAAQ94AAEPtAABD7wAAQ/EAAEPzAABD9QAAQ/cAAEP5AABD+wAARAoAAEQMAABEDgAARBAAAEQSAABEFAAARBYAAEQYAABEGgAARDoAAERlAABEfwAARJgAAESyAABE0gAARPUAAEU0AABFNgAARTgAAEU6AABFPAAART0AAEU+AABFPwAARUAAAEVCAABFRAAARUUAAEVGAABFSAAARUkAAEVLAABFigAARYwAAEWOAABFkAAARZIAAEWTAABFlAAARZUAAEWWAABFmAAARZoAAEWbAABFnAAARZ4AAEWfAABF3gAAReAAAEXiAABF5AAAReYAAEXnAABF6AAARekAAEXqAABF7AAARe4AAEXvAABF8AAARfIAAEXzAABGMgAARjQAAEY2AABGOAAARjoAAEY7AABGPAAARj0AAEY+AABGQAAARkIAAEZDAABGRAAARkYAAEZHAABGSQAARogAAEaKAABGjAAARo4AAEaQAABGkQAARpIAAEaTAABGlAAARpYAAEaYAABGmQAARpoAAEacAABGnQAARtwAAEbeAABG4AAARuIAAEbkAABG5QAARuYAAEbnAABG6AAARuoAAEbsAABG7QAARu4AAEbwAABG8QAARzAAAEcyAABHNAAARzYAAEc4AABHOQAARzoAAEc7AABHPAAARz4AAEdAAABHQQAAR0IAAEdEAABHRQAAR04AAEdcAABHaQAAR3cAAEeEAABHlwAAR64AAEfAAABICwAASC4AAEhOAABIbgAASHAAAEhyAABIdAAASHYAAEh4AABIeQAASHoAAEh8AABIfQAASH8AAEiAAABIggAASIQAAEiFAABIhgAASIgAAEiJAABIjgAASJsAAEigAABIogAASKQAAEipAABIqwAASK0AAEivAABI1AAASPgAAEkfAABJQwAASUUAAElHAABJSQAASUsAAElNAABJTwAASVAAAElSAABJXwAASXAAAElyAABJdAAASXYAAEl4AABJegAASXwAAEl+AABJgAAASZEAAEmTAABJlQAASZcAAEmZAABJmwAASZ0AAEmfAABJoQAASaMAAEniAABJ5AAASeYAAEnoAABJ6gAASesAAEnsAABJ7QAASe4AAEnwAABJ8gAASfMAAEn0AABJ9gAASfcAAEo2AABKOAAASjoAAEo8AABKPgAASj8AAEpAAABKQQAASkIAAEpEAABKRgAASkcAAEpIAABKSgAASksAAEqKAABKjAAASo4AAEqQAABKkgAASpMAAEqUAABKlQAASpYAAEqYAABKmgAASpsAAEqcAABKngAASp8AAEqsAABKrQAASq4AAEqwAABK7wAASvEAAErzAABK9QAASvcAAEr4AABK+QAASvoAAEr7AABK/QAASv8AAEsAAABLAQAASwMAAEsEAABLQwAAS0UAAEtHAABLSQAAS0sAAEtMAABLTQAAS04AAEtPAABLUQAAS1MAAEtUAABLVQAAS1cAAEtYAABLlwAAS5kAAEubAABLnQAAS58AAEugAABLoQAAS6IAAEujAABLpQAAS6cAAEuoAABLqQAAS6sAAEusAABL6wAAS+0AAEvvAABL8QAAS/MAAEv0AABL9QAAS/YAAEv3AABL+QAAS/sAAEv8AABL/QAAS/8AAEwAAABMPwAATEEAAExDAABMRQAATEcAAExIAABMSQAATEoAAExLAABMTQAATE8AAExQAABMUQAATFMAAExUAABMeQAATJ0AAEzEAABM6AAATOoAAEzsAABM7gAATPAAAEzyAABM9AAATPUAAEz3AABNBAAATRMAAE0VAABNFwAATRkAAE0bAABNHQAATR8AAE0hAABNMAAATTIAAE00AABNNgAATTgAAE06AABNPAAATT4AAE1AAABNfwAATYEAAE2DAABNhQAATYcAAE2IAABNiQAATYoAAE2LAABNjQAATY8AAE2QAABNkQAATZMAAE2UAABN0wAATdUAAE3XAABN2QAATdsAAE3cAABN3QAATd4AAE3fAABN4QAATeMAAE3kAABN5QAATecAAE3oAABOJwAATikAAE4rAABOLQAATi8AAE4wAABOMQAATjIAAE4zAABONQAATjcAAE44AABOOQAATjsAAE48AABOewAATn0AAE5/AABOgQAAToMAAE6EAABOhQAAToYAAE6HAABOiQAATosAAE6MAABOjQAATo8AAE6QAABOkwAATtIAAE7UAABO1gAATtgAAE7aAABO2wAATtwAAE7dAABO3gAATuAAAE7iAABO4wAATuQAAE7mAABO5wAATyYAAE8oAABPKgAATywAAE8uAABPLwAATzAAAE8xAABPMgAATzQAAE82AABPNwAATzgAAE86AABPOwAAT3oAAE98AABPfgAAT4AAAE+CAABPgwAAT4QAAE+FAABPhgAAT4gAAE+KAABPiwAAT4wAAE+OAABPjwAAT9oAAE/9AABQHQAAUD0AAFA/AABQQQAAUEMAAFBFAABQRwAAUEgAAFBJAABQSwAAUEwAAFBOAABQTwAAUFEAAFBTAABQVAAAUFUAAFBXAABQWAAAUF0AAFBqAABQbwAAUHEAAFBzAABQeAAAUHoAAFB8AABQfgAAUKMAAFDHAABQ7gAAURIAAFEUAABRFgAAURgAAFEaAABRHAAAUR4AAFEfAABRIQAAUS4AAFE/AABRQQAAUUMAAFFFAABRRwAAUUkAAFFLAABRTQAAUU8AAFFgAABRYgAAUWQAAFFmAABRaAAAUWoAAFFsAABRbgAAUXAAAFFyAABRsQAAUbMAAFG1AABRtwAAUbkAAFG6AABRuwAAUbwAAFG9AABRvwAAUcEAAFHCAABRwwAAUcUAAFHGAABSBQAAUgcAAFIJAABSCwAAUg0AAFIOAABSDwAAUhAAAFIRAABSEwAAUhUAAFIWAABSFwAAUhkAAFIaAABSWQAAUlsAAFJdAABSXwAAUmEAAFJiAABSYwAAUmQAAFJlAABSZwAAUmkAAFJqAABSawAAUm0AAFJuAABSewAAUnwAAFJ9AABSfwAAUr4AAFLAAABSwgAAUsQAAFLGAABSxwAAUsgAAFLJAABSygAAUswAAFLOAABSzwAAUtAAAFLSAABS0wAAUxIAAFMUAABTFgAAUxgAAFMaAABTGwAAUxwAAFMdAABTHgAAUyAAAFMiAABTIwAAUyQAAFMmAABTJwAAU2YAAFNoAABTagAAU2wAAFNuAABTbwAAU3AAAFNxAABTcgAAU3QAAFN2AABTdwAAU3gAAFN6AABTewAAU7oAAFO8AABTvgAAU8AAAFPCAABTwwAAU8QAAFPFAABTxgAAU8gAAFPKAABTywAAU8wAAFPOAABTzwAAVA4AAFQQAABUEgAAVBQAAFQWAABUFwAAVBgAAFQZAABUGgAAVBwAAFQeAABUHwAAVCAAAFQiAABUIwAAVEgAAFRsAABUkwAAVLcAAFS5AABUuwAAVL0AAFS/AABUwQAAVMMAAFTEAABUxgAAVNMAAFTiAABU5AAAVOYAAFToAABU6gAAVOwAAFTuAABU8AAAVP8AAFUBAABVAwAAVQUAAFUHAABVCQAAVQwAAFUPAABVEQAAVVAAAFVSAABVVAAAVVYAAFVYAABVWQAAVVoAAFVbAABVXAAAVV4AAFVgAABVYQAAVWIAAFVkAABVZQAAVaQAAFWmAABVqAAAVaoAAFWsAABVrQAAVa4AAFWvAABVsAAAVbIAAFW0AABVtQAAVbYAAFW4AABVuQAAVfgAAFX6AABV/AAAVf4AAFYAAABWAQAAVgIAAFYDAABWBAAAVgYAAFYIAABWCQAAVgoAAFYMAABWDQAAVkwAAFZOAABWUAAAVlIAAFZUAABWVQAAVlYAAFZXAABWWAAAVloAAFZcAABWXQAAVl4AAFZgAABWYQAAVmQAAFajAABWpQAAVqcAAFapAABWqwAAVqwAAFatAABWrgAAVq8AAFaxAABWswAAVrQAAFa1AABWtwAAVrgAAFb3AABW+QAAVvsAAFb9AABW/wAAVwAAAFcBAABXAgAAVwMAAFcFAABXBwAAVwgAAFcJAABXCwAAVwwAAFdLAABXTQAAV08AAFdRAABXUwAAV1QAAFdVAABXVgAAV1cAAFdZAABXWwAAV1wAAFddAABXXwAAV2AAAFerAABXzgAAV+4AAFgOAABYEAAAWBIAAFgUAABYFgAAWBgAAFgZAABYGgAAWB0AAFgeAABYIAAAWCEAAFgjAABYJQAAWCYAAFgnAABYKgAAWCsAAFgwAABYPQAAWEIAAFhEAABYRgAAWEsAAFhOAABYUQAAWFMAAFh4AABYnAAAWMMAAFjnAABY6gAAWOwAAFjuAABY8AAAWPIAAFj0AABY9QAAWPgAAFkFAABZFgAAWRgAAFkaAABZHAAAWR4AAFkgAABZIgAAWSQAAFkmAABZNwAAWToAAFk9AABZQAAAWUMAAFlGAABZSQAAWUwAAFlPAABZUQAAWZAAAFmSAABZlAAAWZYAAFmZAABZmgAAWZsAAFmcAABZnQAAWZ8AAFmhAABZogAAWaMAAFmlAABZpgAAWeUAAFnnAABZ6QAAWesAAFnuAABZ7wAAWfAAAFnxAABZ8gAAWfQAAFn2AABZ9wAAWfgAAFn6AABZ+wAAWjoAAFo8AABaPwAAWkEAAFpEAABaRQAAWkYAAFpHAABaSAAAWkoAAFpMAABaTQAAWk4AAFpQAABaUQAAWl4AAFpfAABaYAAAWmIAAFqhAABaowAAWqUAAFqnAABaqgAAWqsAAFqsAABarQAAWq4AAFqwAABasgAAWrMAAFq0AABatgAAWrcAAFr2AABa+AAAWvoAAFr8AABa/wAAWwAAAFsBAABbAgAAWwMAAFsFAABbBwAAWwgAAFsJAABbCwAAWwwAAFtLAABbTQAAW08AAFtRAABbVAAAW1UAAFtWAABbVwAAW1gAAFtaAABbXAAAW10AAFteAABbYAAAW2EAAFugAABbogAAW6QAAFumAABbqQAAW6oAAFurAABbrAAAW60AAFuvAABbsQAAW7IAAFuzAABbtQAAW7YAAFv1AABb9wAAW/kAAFv7AABb/gAAW/8AAFwAAABcAQAAXAIAAFwEAABcBgAAXAcAAFwIAABcCgAAXAsAAFwwAABcVAAAXHsAAFyfAABcogAAXKQAAFymAABcqAAAXKoAAFysAABcrQAAXLAAAFy9AABczAAAXM4AAFzQAABc0gAAXNQAAFzWAABc2AAAXNoAAFzpAABc7AAAXO8AAFzyAABc9QAAXPgAAFz7AABc/gAAXQAAAF0/AABdQQAAXUMAAF1FAABdSAAAXUkAAF1KAABdSwAAXUwAAF1OAABdUAAAXVEAAF1SAABdVAAAXVUAAF2UAABdlgAAXZgAAF2aAABdnQAAXZ4AAF2fAABdoAAAXaEAAF2jAABdpQAAXaYAAF2nAABdqQAAXaoAAF3pAABd6wAAXe0AAF3vAABd8gAAXfMAAF30AABd9QAAXfYAAF34AABd+gAAXfsAAF38AABd/gAAXf8AAF4+AABeQAAAXkMAAF5FAABeSAAAXkkAAF5KAABeSwAAXkwAAF5OAABeUAAAXlEAAF5SAABeVAAAXlUAAF5YAABelwAAXpkAAF6bAABenQAAXqAAAF6hAABeogAAXqMAAF6kAABepgAAXqgAAF6pAABeqgAAXqwAAF6tAABe7AAAXu4AAF7xAABe8wAAXvYAAF73AABe+AAAXvkAAF76AABe/AAAXv4AAF7/AABfAAAAXwIAAF8DAABfKgAAX2kAAF9rAABfbQAAX28AAF9yAABfcwAAX3QAAF91AABfdgAAX3gAAF96AABfewAAX3wAAF9+AABffwAAX8oAAF/tAABgDQAAYC0AAGAvAABgMQAAYDMAAGA1AABgNwAAYDgAAGA5AABgPAAAYD0AAGA/AABgQAAAYEIAAGBEAABgRQAAYEYAAGBJAABgSgAAYE8AAGBcAABgYQAAYGMAAGBlAABgagAAYG0AAGBwAABgcgAAYJcAAGC7AABg4gAAYQYAAGEJAABhCwAAYQ0AAGEPAABhEQAAYRMAAGEUAABhFwAAYSQAAGE1AABhNwAAYTkAAGE7AABhPQAAYT8AAGFBAABhQwAAYUUAAGFWAABhWQAAYVwAAGFfAABhYgAAYWUAAGFoAABhawAAYW4AAGFwAABhrwAAYbEAAGGzAABhtQAAYbgAAGG5AABhugAAYbsAAGG8AABhvgAAYcAAAGHBAABhwgAAYcQAAGHFAABiBAAAYgYAAGIIAABiCgAAYg0AAGIOAABiDwAAYhAAAGIRAABiEwAAYhUAAGIWAABiFwAAYhkAAGIaAABiWQAAYlsAAGJeAABiYAAAYmMAAGJkAABiZQAAYmYAAGJnAABiaQAAYmsAAGJsAABibQAAYm8AAGJwAABifQAAYn4AAGJ/AABigQAAYsAAAGLCAABixAAAYsYAAGLJAABiygAAYssAAGLMAABizQAAYs8AAGLRAABi0gAAYtMAAGLVAABi1gAAYxUAAGMXAABjGQAAYxsAAGMeAABjHwAAYyAAAGMhAABjIgAAYyQAAGMmAABjJwAAYygAAGMqAABjKwAAY2oAAGNsAABjbgAAY3AAAGNzAABjdAAAY3UAAGN2AABjdwAAY3kAAGN7AABjfAAAY30AAGN/AABjgAAAY78AAGPBAABjwwAAY8UAAGPIAABjyQAAY8oAAGPLAABjzAAAY84AAGPQAABj0QAAY9IAAGPUAABj1QAAZBQAAGQWAABkGAAAZBoAAGQdAABkHgAAZB8AAGQgAABkIQAAZCMAAGQlAABkJgAAZCcAAGQpAABkKgAAZE8AAGRzAABkmgAAZL4AAGTBAABkwwAAZMUAAGTHAABkyQAAZMsAAGTMAABkzwAAZNwAAGT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fAABwIQAAcCMAAHAlAABwJgAAcCcAAHAqAABwKwAAcC0AAHAuAABwMAAAcDIAAHAzAABwNAAAcDcAAHA4AABwPQAAcEoAAHBPAABwUQAAcFMAAHBYAABwWwAAcF4AAHBgAABwhQAAcKkAAHDQAABw9AAAcPcAAHD5AABw+wAAcP0AAHD/AABxAQAAcQIAAHEFAABxEgAAcSMAAHElAABxJwAAcSkAAHErAABxLQAAcS8AAHExAABxMwAAcUQAAHFHAABxSgAAcU0AAHFQAABxUwAAcVYAAHFZAABxXAAAcV4AAHGdAABxnwAAcaEAAHGjAABxpgAAcacAAHGoAABxqQAAcaoAAHGsAABxrgAAca8AAHGwAABxsgAAcbMAAHHyAABx9AAAcfYAAHH4AABx+wAAcfwAAHH9AABx/gAAcf8AAHIBAAByAwAAcgQAAHIFAAByBwAAcggAAHJHAABySQAAckwAAHJOAAByUQAAclIAAHJTAAByVAAAclUAAHJXAAByWQAAcloAAHJbAAByXQAAcl4AAHJrAABybAAAcm0AAHJvAAByrgAAcrAAAHKyAABytAAAcrcAAHK4AAByuQAAcroAAHK7AAByvQAAcr8AAHLAAABywQAAcsMAAHLEAABzAwAAcwUAAHMHAABzCQAAcwwAAHMNAABzDgAAcw8AAHMQAABzEgAAcxQAAHMVAABzFgAAcxgAAHMZAABzWAAAc1oAAHNcAABzXgAAc2EAAHNiAABzYwAAc2QAAHNlAABzZwAAc2kAAHNqAABzawAAc20AAHNuAABzrQAAc68AAHOxAABzswAAc7YAAHO3AABzuAAAc7kAAHO6AABzvAAAc74AAHO/AABzwAAAc8IAAHPDAAB0AgAAdAQAAHQGAAB0CAAAdAsAAHQMAAB0DQAAdA4AAHQPAAB0EQAAdBMAAHQUAAB0FQAAdBcAAHQYAAB0PQAAdGEAAHSIAAB0rAAAdK8AAHSxAAB0swAAdLUAAHS3AAB0uQAAdLoAAHS9AAB0ygAAdNkAAHTbAAB03QAAdN8AAHThAAB04wAAdOUAAHTnAAB09gAAdPkAAHT8AAB0/wAAdQIAAHUFAAB1CAAAdQsAAHUNAAB1TAAAdU4AAHVQAAB1UgAAdVUAAHVWAAB1VwAAdVgAAHVZAAB1WwAAdV0AAHVeAAB1XwAAdWEAAHV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6AACGPAAAhj8AAIZAAACGQQAAhkIAAIZDAACGRQAAhkcAAIZIAACGSQAAhksAAIZMAACGiwAAho0AAIaPAACGkQAAhpQAAIaVAACGlgAAhpcAAIaYAACGmgAAhpwAAIadAACGngAAhqAAAIahAACG4AAAhuIAAIbkAACG5gAAhukAAIbqAACG6wAAhuwAAIbtAACG7wAAhvEAAIbyAACG8wAAhvUAAIb2AACHNQAAhzcAAIc5AACHOwAAhz4AAIc/AACHQAAAh0EAAIdCAACHRAAAh0YAAIdHAACHSAAAh0oAAIdLAACHlgAAh7kAAIfZAACH+QAAh/sAAIf9AACH/wAAiAEAAIgDAACIBAAAiAUAAIgIAACICQAAiAsAAIgMAACIDgAAiBAAAIgRAACIEgAAiBUAAIgWAACIHwAAiCwAAIgxAACIMwAAiDUAAIg6AACIPQAAiEAAAIhCAACIZwAAiIsAAIiyAACI1gAAiNkAAIjbAACI3QAAiN8AAIjhAACI4wAAiOQAAIjnAACI9AAAiQUAAIkHAACJCQAAiQsAAIkNAACJDwAAiREAAIkTAACJFQAAiSYAAIkpAACJLAAAiS8AAIkyAACJNQAAiTgAAIk7AACJPgAAiUAAAIl/AACJgQAAiYMAAImFAACJiAAAiYkAAImKAACJiwAAiYwAAImOAACJkAAAiZEAAImSAACJlAAAiZUAAInUAACJ1gAAidgAAInaAACJ3QAAid4AAInfAACJ4AAAieEAAInjAACJ5QAAieYAAInnAACJ6QAAieoAAIopAACKKwAAii4AAIowAACKMwAAijQAAIo1AACKNgAAijcAAIo5AACKOwAAijwAAIo9AACKPwAAikAAAIpNAACKTgAAik8AAIpRAACKkAAAipIAAIqUAACKlgAAipkAAIqaAACKmwAAipwAAIqdAACKnwAAiqEAAIqiAACKowAAiqUAAIqmAACK5QAAiucAAIrpAACK6wAAiu4AAIrvAACK8AAAivEAAIryAACK9AAAivYAAIr3AACK+AAAivoAAIr7AACLOgAAizwAAIs+AACLQAAAi0MAAItEAACLRQAAi0YAAItHAACLSQAAi0sAAItMAACLTQAAi08AAItQAACLjwAAi5EAAIuTAACLlQAAi5gAAIuZAACLmgAAi5sAAIucAACLngAAi6AAAIu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iAACYZAAAmGcAAJhpAACYbAAAmG0AAJhuAACYbwAAmHAAAJhyAACYdAAAmHUAAJh2AACYeAAAmHkAAJiCAACYgwAAmIUAAJjEAACYxgAAmMgAAJjKAACYzQAAmM4AAJjPAACY0AAAmNEAAJjTAACY1QAAmNYAAJjXAACY2QAAmNoAAJkZAACZGwAAmR4AAJkgAACZIwAAmSQAAJklAACZJgAAmScAAJkpAACZKwAAmSwAAJktAACZLwAAmTAAAJk5AACZOgAAmTwAAJl7AACZfQAAmX8AAJmBAACZhAAAmYUAAJmGAACZhwAAmYgAAJmKAACZjAAAmY0AAJmOAACZkAAAmZEAAJnQAACZ0gAAmdUAAJnXAACZ2gAAmdsAAJncAACZ3QAAmd4AAJngAACZ4gAAmeMAAJnkAACZ5gAAmecAAJnwAACZ8QAAmfMAAJoyAACaNAAAmjYAAJo4AACaOwAAmjwAAJo9AACaPgAAmj8AAJpBAACaQwAAmkQAAJpFAACaRwAAmkgAAJqHAACaiQAAmowAAJqOAACakQAAmpIAAJqTAACalAAAmpUAAJqXAACamQAAmpoAAJqbAACanQAAmp4AAJqrAACarAAAmq0AAJqvAACa7gAAmvAAAJryAACa9AAAmvcAAJr4AACa+QAAmvoAAJr7AACa/QAAmv8AAJsAAACbAQAAmwMAAJsEAACbQwAAm0UAAJtHAACbSQAAm0wAAJtNAACbTgAAm08AAJtQAACbUgAAm1QAAJtVAACbVgAAm1gAAJtZAACbZgAAm4kAAJuMAACbjgAAm5EAAJuUAACblwAAm5oAAJudAACboAAAm6MAAJumAACbqAAAm6sAAJuuAACbsQAAm7MAAJu2AACb2QAAm9wAAJvfAACb4gAAm+UAAJvoAACb6wAAm+4AAJvxAACb9AAAm/cAAJv6AACb/QAAnAAAAJwDAACcBgAAnAkAAJwLAACcDgAAnBUAAJwjAACcKQAAnC8AAJw3AACcQgAAnEwAAJxVAACcXwAAnHQAAJyIAACckAAAnNsAAJz+AACdHgAAnT4AAJ1AAACdQgAAnUQAAJ1GAACdSQAAnUoAAJ1LAACdTgAAnU8AAJ1RAACdUgAAnVQAAJ1XAACdWAAAnVkAAJ1cAACdXQAAnWIAAJ1vAACddAAAnXYAAJ14AACdfQAAnYAAAJ2DAACdhQAAnaoAAJ3OAACd9QAAnhkAAJ4cAACeHgAAniAAAJ4iAACeJAAAniYAAJ4nAACeKgAAnjcAAJ5IAACeSgAAnkwAAJ5OAACeUAAAnlIAAJ5UAACeVgAAnlgAAJ5pAACebAAAnm8AAJ5yAACedQAAnngAAJ57AACefgAAnoEAAJ6DAACewgAAnsQAAJ7GAACeyAAAnssAAJ7MAACezQAAns4AAJ7PAACe0QAAntMAAJ7UAACe1QAAntcAAJ7YAACfFwAAnxkAAJ8bAACfHQAAnyAAAJ8hAACfIgAAnyMAAJ8kAACfJgAAnygAAJ8pAACfKgAAnywAAJ8tAACfbAAAn24AAJ9xAACfcwAAn3YAAJ93AACfeAAAn3kAAJ96AACffAAAn34AAJ9/AACfgAAAn4IAAJ+DAACfkAAAn5EAAJ+SAACflAAAn9MAAJ/VAACf1wAAn9kAAJ/cAACf3QAAn94AAJ/fAACf4AAAn+IAAJ/kAACf5QAAn+YAAJ/oAACf6QAAoCgAAKAqAACgLAAAoC4AAKAxAACgMgAAoDMAAKA0AACgNQAAoDcAAKA5AACgOgAAoDsAAKA9AACgPgAAoH0AAKB/AACggQAAoIMAAKCGAACghwAAoIgAAKCJAACgigAAoIwAAKCOAACgjwAAoJAAAKCSAACgkwAAoNIAAKDUAACg1gAAoNgAAKDbAACg3AAAoN0AAKDeAACg3wAAoOEAAKDjAACg5AAAoOUAAKDnAACg6AAAoScAAKEpAAChKwAAoS0AAKEwAAChMQAAoTIAAKEzAAChNAAAoTYAAKE4AAChOQAAoToAAKE8AAChPQAAoWIAAKGGAAChrQAAodEAAKHUAACh1gAAodgAAKHaAACh3AAAod4AAKHfAACh4gAAoe8AAKH+AACiAAAAogIAAKIEAACiBgAAoggAAKIKAACiDAAAohsAAKIeAACiIQAAoiQAAKInAACiKgAAoi0AAKIwAACiMgAAonEAAKJzAACidQAAoncAAKJ6AACiewAAonwAAKJ9AACifgAAooAAAKKCAACigwAAooQAAKKGAACihwAAosYAAKLIAACiygAAoswAAKLPAACi0AAAotEAAKLSAACi0wAAotUAAKLXAACi2AAAotkAAKLbAACi3AAAoxsAAKMdAACjHwAAoyEAAKMkAACjJQAAoyYAAKMnAACjKAAAoyoAAKMsAACjLQAAoy4AAKMwAACjMQAAo3AAAKNyAACjdAAAo3YAAKN5AACjegAAo3sAAKN8AACjfQAAo38AAKOBAACjggAAo4MAAKOFAACjhgAAo8UAAKPHAACjyQAAo8sAAKPOAACjzwAAo9AAAKPRAACj0gAAo9QAAKPWAACj1wAAo9gAAKPaAACj2wAApBoAAKQcAACkHgAApCAAAKQjAACkJAAApCUAAKQmAACkJwAApCkAAKQrAACkLAAApC0AAKQvAACkMAAApG8AAKRxAACkcwAApHUAAKR4AACkeQAApHoAAKR7AACkfAAApH4AAKSAAACkgQAApIIAAKSEAACkhQAApNAAAKTzAAClEwAApTMAAKU1AAClNwAApTkAAKU7AAClPgAApT8AAKVAAAClQwAApUQAAKVGAAClRwAApUkAAKVLAAClTAAApU0AAKVQAAClUQAApVYAAKVjAAClaAAApWoAAKVsAAClcQAApXQAAKV3AACleQAApZ4AAKXCAACl6QAApg0AAKYQAACmEgAAphQAAKYWAACmGAAAphoAAKYbAACmHgAApisAAKY8AACmPgAApkAAAKZCAACmRAAApkYAAKZIAACmSgAApkwAAKZdAACmYAAApmMAAKZmAACmaQAApmwAAKZvAACmcgAApnUAAKZ3AACmtgAAprgAAKa6AACmvAAApr8AAKbAAACmwQAApsIAAKbDAACmxQAApscAAKbIAACmyQAApssAAKbMAACnCwAApw0AAKcPAACnEQAApxQAAKcVAACnFgAApxcAAKcYAACnGgAApxwAAKcdAACnHgAApyAAAKchAACnYAAAp2IAAKdlAACnZwAAp2oAAKdrAACnbAAAp20AAKduAACncAAAp3IAAKdzAACndAAAp3YAAKd3AACnhAAAp4UAAKeGAACniAAAp8cAAKfJAACnywAAp80AAKfQAACn0QAAp9IAAKfTAACn1AAAp9YAAKfYAACn2QAAp9oAAKfcAACn3QAAqBwAAKgeAACoIAAAqCIAAKglAACoJgAAqCcAAKgoAACoKQAAqCsAAKgtAACoLgAAqC8AAKgxAACoMgAAqHEAAKhzAACodQAAqHcAAKh6AACoewAAqHwAAKh9AACofgAAqIAAAKiCAACogwAAqIQAAKiGAACohwAAqMYAAKjIAACoygAAqMwAAKjPAACo0AAAqNEAAKjSAACo0wAAqNUAAKjXAACo2AAAqNkAAKjbAACo3AAAqRsAAKkdAACpHwAAqSEAAKkkAACpJQAAqSYAAKknAACpKAAAqSoAAKksAACpLQAAqS4AAKkwAACpMQAAqVYAAKl6AACpoQAAqcUAAKnIAACpygAAqcwAAKnOAACp0AAAqdIAAKnTAACp1gAAqeMAAKnyAACp9AAAqfYAAKn4AACp+gAAqfwAAKn+AACqAAAAqg8AAKoSAACqFQAAqhgAAKobAACqHgAAqiEAAKokAACqJgAAqmUAAKpnAACqaQAAqmsAAKpuAACqbwAAqnAAAKpxAACqcgAAqnQAAKp2AACqdwAAqngAAKp6AACqewAAqroAAKq8AACqvgAAqsAAAKrDAACqxAAAqsUAAKrGAACqxwAAqskAAKrLAACqzAAAqs0AAKrPAACq0AAAqw8AAKsRAACrEwAAqxUAAKsYAACrGQAAqxoAAKsbAACrHAAAqx4AAKsgAACrIQAAqyIAAKskAACrJQAAq2QAAKtmAACraAAAq2oAAKttAACrbgAAq28AAKtwAACrcQAAq3MAAKt1AACrdgAAq3cAAKt5AACregAAq7kAAKu7AACrvQAAq78AAKvCAACrwwAAq8QAAKvFAACrxgAAq8gAAKvKAACrywAAq8wAAKvOAACrzwAArA4AAKwQAACsEgAArBQAAKwXAACsGAAArBkAAKwaAACsGwAArB0AAKwfAACsIAAArCEAAKwjAACsJAAArGMAAKxlAACsZwAArGkAAKxsAACsbQAArG4AAKxvAACscAAArHIAAKx0AACsdQAArHYAAKx4AACseQAArMQAAKznAACtBwAArScAAK0pAACtKwAArS0AAK0vAACtMgAArTMAAK00AACtNwAArTgAAK06AACtOwAArT0AAK1AAACtQQAArUIAAK1FAACtRgAArUsAAK1YAACtXQAArV8AAK1hAACtZgAArWkAAK1sAACtbgAArZMAAK23AACt3gAArgIAAK4FAACuBwAArgkAAK4LAACuDQAArg8AAK4QAACuEwAAriAAAK4xAACuMwAArjUAAK43AACuOQAArjsAAK49AACuPwAArkEAAK5SAACuVQAArlgAAK5bAACuXgAArmEAAK5kAACuZwAArmoAAK5sAACuqwAArq0AAK6vAACusQAArrQAAK61AACutgAArrcAAK64AACuugAArrwAAK69AACuvgAArsAAAK7BAACvAAAArwIAAK8EAACvBgAArwkAAK8KAACvCwAArwwAAK8NAACvDwAArxEAAK8SAACvEwAArxUAAK8WAACvVQAAr1cAAK9aAACvXAAAr18AAK9gAACvYQAAr2IAAK9jAACvZQAAr2cAAK9oAACvaQAAr2sAAK9sAACveQAAr3oAAK97AACvfQAAr7wAAK++AACvwAAAr8IAAK/FAACvxgAAr8cAAK/IAACvyQAAr8sAAK/NAACvzgAAr88AAK/RAACv0gAAsBEAALATAACwFQAAsBcAALAaAACwGwAAsBwAALAdAACwHgAAsCAAALAiAACwIwAAsCQAALAmAACwJwAAsGYAALBoAACwagAAsGwAALBvAACwcAAAsHEAALByAACwcwAAsHUAALB3AACweAAAsHkAALB7AACwfAAAsLsAALC9AACwvwAAsMEAALDEAACwxQAAsMYAALDHAACwyAAAsMoAALDMAACwzQAAsM4AALDQAACw0QAAsRAAALESAACxFAAAsRYAALEZAACxGgAAsRsAALEcAACxHQAAsR8AALEhAACxIgAAsSMAALElAACxJgAAsUsAALFvAACxlgAAsboAALG9AACxvwAAscEAALHDAACxxQAAsccAALHIAACxywAAsdgAALHnAACx6QAAsesAALHtAACx7wAAsfEAALHzAACx9QAAsgQAALIHAACyCgAAsg0AALIQAACyEwAAshYAALIZAACyGwAAsloAALJcAACyXgAAsmAAALJjAACyZAAAsmUAALJmAACyZwAAsmkAALJrAACybAAAsm0AALJvAACycAAAsq8AALKxAACyswAAsrUAALK4AACyuQAAsroAALK7AACyvAAAsr4AALLAAACywQAAssIAALLEAACyxQAAswQAALMGAACzCAAAswoAALMNAACzDgAAsw8AALMQAACzEQAAsxMAALMVAACzFgAAsxcAALMZAACzGgAAs1kAALNbAACzXQAAs18AALNiAACzYwAAs2QAALNlAACzZgAAs2gAALNqAACzawAAs2wAALNuAACzbwAAs64AALOwAACzsgAAs7QAALO3AACzuAAAs7kAALO6AACzuwAAs70AALO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dAADEXgAAxGAAAMRhAADErAAAxM8AAMTvAADFDwAAxREAAMUTAADFFQAAxRcAAMUaAADFGwAAxRwAAMUfAADFIAAAxSIAAMUjAADFJQAAxSgAAMUpAADFKgAAxS0AAMUuAADFMwAAxUAAAMVFAADFRwAAxUkAAMVOAADFUQAAxVQAAMVWAADFewAAxZ8AAMXGAADF6gAAxe0AAMXvAADF8QAAxfMAAMX1AADF9wAAxfgAAMX7AADGCAAAxhkAAMYbAADGHQAAxh8AAMYhAADGIwAAxiUAAMYnAADGKQAAxjoAAMY9AADGQAAAxkMAAMZGAADGSQAAxkwAAMZPAADGUgAAxlQAAMaTAADGlQAAxpcAAMaZAADGnAAAxp0AAMaeAADGnwAAxqAAAMaiAADGpAAAxqUAAMamAADGqAAAxqkAAMboAADG6gAAxuwAAMbuAADG8QAAxvIAAMbzAADG9AAAxvUAAMb3AADG+QAAxvoAAMb7AADG/QAAxv4AAMc9AADHPwAAx0IAAMdEAADHRwAAx0gAAMdJAADHSgAAx0sAAMdNAADHTwAAx1AAAMdRAADHUwAAx1QAAMdhAADHYgAAx2MAAMdlAADHpAAAx6YAAMeoAADHqgAAx60AAMeuAADHrwAAx7AAAMexAADHswAAx7UAAMe2AADHtwAAx7kAAMe6AADH+QAAx/sAAMf9AADH/wAAyAIAAMgDAADIBAAAyAUAAMgGAADICAAAyAoAAMgLAADIDAAAyA4AAMgPAADITgAAyFAAAMhSAADIVAAAyFcAAMhYAADIWQAAyFoAAMhbAADIXQAAyF8AAMhgAADIYQAAyGMAAMhkAADIowAAyKUAAMinAADIqQAAyKwAAMitAADIrgAAyK8AAMiwAADIsgAAyLQAAMi1AADItgAAyLgAAMi5AADI+AAAyPoAAMj8AADI/gAAyQEAAMkCAADJAwAAyQQAAMkFAADJBwAAyQkAAMkKAADJCwAAyQ0AAMkOAADJMwAAyVcAAMl+AADJogAAyaUAAMmnAADJqQAAyasAAMmtAADJrwAAybAAAMmzAADJwAAAyc8AAMnRAADJ0wAAydUAAMnXAADJ2QAAydsAAMndAADJ7AAAye8AAMnyAADJ9QAAyfgAAMn7AADJ/gAAygEAAMoDAADKQgAAykQAAMpHAADKSQAAykwAAMpNAADKTgAAyk8AAMpQAADKUgAAylQAAMpVAADKVgAAylgAAMpZAADKmAAAypoAAMqcAADKngAAyqEAAMqiAADKowAAyqQAAMqlAADKpwAAyqkAAMqqAADKqwAAyq0AAMquAADK7QAAyu8AAMrxAADK8wAAyvYAAMr3AADK+AAAyvkAAMr6AADK/AAAyv4AAMr/AADLAAAAywIAAMsDAADLQgAAy0QAAMtHAADLSQAAy0wAAMtNAADLTgAAy08AAMtQAADLUgAAy1QAAMtVAADLVgAAy1gAAMtZAADLmAAAy5oAAMucAADLngAAy6EAAMuiAADLowAAy6QAAMulAADLpwAAy6kAAMuqAADLqwAAy60AAMuuAADL7QAAy+8AAMvxAADL8wAAy/YAAMv3AADL+AAAy/kAAMv6AADL/AAAy/4AAMv/AADMAAAAzAIAAMwDAADMQgAAzEQAAMxGAADMSAAAzEsAAMxMAADMTQAAzE4AAMxPAADMUQAAzFMAAMxUAADMVQAAzFcAAMxYAADMowAAzMYAAMzmAADNBgAAzQgAAM0KAADNDAAAzQ4AAM0RAADNEgAAzRMAAM0WAADNFwAAzRkAAM0aAADNHAAAzR8AAM0gAADNIQAAzSQAAM0lAADNKgAAzTcAAM08AADNPgAAzUAAAM1FAADNSAAAzUsAAM1NAADNcgAAzZYAAM29AADN4QAAzeQAAM3mAADN6AAAzeoAAM3sAADN7gAAze8AAM3yAADN/wAAzhAAAM4SAADOFAAAzhYAAM4YAADOGgAAzhwAAM4eAADOIAAAzjEAAM40AADONwAAzjoAAM49AADOQAAAzkMAAM5GAADOSQAAzksAAM6KAADOjAAAzo4AAM6QAADOkwAAzpQAAM6VAADOlgAAzpcAAM6ZAADOmwAAzpwAAM6dAADOnwAAzqAAAM7fAADO4QAAzuMAAM7lAADO6AAAzukAAM7qAADO6wAAzuwAAM7uAADO8AAAzvEAAM7yAADO9AAAzvUAAM80AADPNgAAzzkAAM87AADPPgAAzz8AAM9AAADPQQAAz0IAAM9EAADPRgAAz0cAAM9IAADPSgAAz0sAAM9YAADPWQAAz1oAAM9cAADPmwAAz50AAM+fAADPoQAAz6QAAM+lAADPpgAAz6cAAM+oAADPqgAAz6wAAM+tAADPrgAAz7AAAM+xAADP8AAAz/IAAM/0AADP9gAAz/kAAM/6AADP+wAAz/wAAM/9AADP/wAA0AEAANACAADQAwAA0AUAANAGAADQRQAA0EcAANBJAADQSwAA0E4AANBPAADQUAAA0FEAANBSAADQVAAA0FYAANBXAADQWAAA0FoAANBbAADQmgAA0JwAANCeAADQoAAA0KMAANCkAADQpQAA0KYAANCnAADQqQAA0KsAANCsAADQrQAA0K8AANCwAADQ7wAA0PEAANDzAADQ9QAA0PgAAND5AADQ+gAA0PsAAND8AADQ/gAA0QAAANEBAADRAgAA0QQAANEFAADRKgAA0U4AANF1AADRmQAA0ZwAANGeAADRoAAA0aIAANGkAADRpgAA0acAANGqAADRtwAA0cYAANHIAADRygAA0cwAANHOAADR0AAA0dIAANHUAADR4wAA0eYAANHpAADR7AAA0e8AANHyAADR9QAA0fgAANH6AADSOQAA0jsAANI9AADSPwAA0kIAANJDAADSRAAA0kUAANJGAADSSAAA0koAANJLAADSTAAA0k4AANJPAADSjgAA0pAAANKSAADSlAAA0pcAANKYAADSmQAA0poAANKbAADSnQAA0p8AANKgAADSoQAA0qMAANKkAADS4wAA0uUAANLnAADS6QAA0uwAANLtAADS7gAA0u8AANLwAADS8gAA0vQAANL1AADS9gAA0vgAANL5AADTOAAA0zoAANM8AADTPgAA00EAANNCAADTQwAA00QAANNFAADTRwAA00kAANNKAADTSwAA000AANNOAADTjQAA048AANORAADTkwAA05YAANOXAADTmAAA05kAANOaAADTnAAA054AANOfAADToAAA06IAANOjAADT4gAA0+QAANPmAADT6AAA0+sAANPsAADT7QAA0+4AANPvAADT8QAA0/MAANP0AADT9QAA0/cAANP4AADUNwAA1DkAANQ7AADUPQAA1EAAANRBAADUQgAA1EMAANREAADURgAA1EgAANRJAADUSgAA1EwAANRNAADUmAAA1LsAANTbAADU+wAA1P0AANT/AADVAQAA1QMAANUGAADVBwAA1QgAANULAADVDAAA1Q4AANUPAADVEQAA1RQAANUVAADVFgAA1RkAANUaAADVHwAA1SwAANUxAADVMwAA1TUAANU6AADVPQAA1UAAANVCAADVZwAA1YsAANWyAADV1gAA1dkAANXbAADV3QAA1d8AANXhAADV4wAA1eQAANXnAADV9AAA1gUAANYHAADWCQAA1gsAANYNAADWDwAA1hEAANYTAADWFQAA1iYAANYpAADWLAAA1i8AANYyAADWNQAA1jgAANY7AADWPgAA1kAAANZ/AADWgQAA1oMAANaFAADWiAAA1okAANaKAADWiwAA1owAANaOAADWkAAA1pEAANaSAADWlAAA1pUAANbUAADW1gAA1tgAANbaAADW3QAA1t4AANbfAADW4AAA1uEAANbjAADW5QAA1uYAANbnAADW6QAA1uoAANcpAADXKwAA1y4AANcwAADXMwAA1zQAANc1AADXNgAA1zcAANc5AADXOwAA1zwAANc9AADXPwAA10AAANdNAADXTgAA108AANdRAADXkAAA15IAANeUAADXlgAA15kAANeaAADXmwAA15wAANedAADXnwAA16EAANeiAADXowAA16UAANemAADX5QAA1+cAANfpAADX6wAA1+4AANfvAADX8AAA1/EAANfyAADX9AAA1/YAANf3AADX+AAA1/oAANf7AADYOgAA2DwAANg+AADYQAAA2EMAANhEAADYRQAA2EYAANhHAADYSQAA2EsAANhMAADYTQAA2E8AANhQAADYjwAA2JEAANiTAADYlQAA2JgAANiZAADYmgAA2JsAANicAADYngAA2KAAANihAADYogAA2KQAANilAADY5AAA2OYAANjoAADY6gAA2O0AANjuAADY7wAA2PAAANjxAADY8wAA2PUAANj2AADY9wAA2PkAANj6AADZHwAA2UMAANlqAADZjgAA2ZEAANmTAADZlQAA2ZcAANmZAADZmwAA2ZwAANmfAADZrAAA2bsAANm9AADZvwAA2cEAANnDAADZxQAA2ccAANnJAADZ2AAA2dsAANneAADZ4QAA2eQAANnnAADZ6gAA2e0AANnvAADaLgAA2jAAANoyAADaNAAA2jcAANo4AADaOQAA2joAANo7AADaPQAA2j8AANpAAADaQQAA2kMAANpEAADagwAA2oUAANqHAADaiQAA2owAANqNAADajgAA2o8AANqQAADakgAA2pQAANqVAADalgAA2pgAANqZAADa2AAA2toAANrcAADa3gAA2uEAANriAADa4wAA2uQAANrlAADa5wAA2ukAANrqAADa6wAA2u0AANruAADbLQAA2y8AANsxAADbMwAA2zYAANs3AADbOAAA2zkAANs6AADbPAAA2z4AANs/AADbQAAA20IAANtDAADbggAA24QAANuGAADbiAAA24sAANuMAADbjQAA244AANuPAADbkQAA25MAANuUAADblQAA25cAANuYAADb1wAA29kAANvbAADb3QAA2+AAANvhAADb4gAA2+MAANvkAADb5gAA2+gAANvpAADb6gAA2+wAANvtAADcLAAA3C4AANwwAADcMgAA3DUAANw2AADcNwAA3DgAANw5AADcOwAA3D0AANw+AADcPwAA3EEAANxCAADcjQAA3LAAANzQAADc8AAA3PIAANz0AADc9gAA3PgAANz7AADc/AAA3P0AAN0AAADdAQAA3QMAAN0EAADdBgAA3QkAAN0KAADdCwAA3Q4AAN0PAADdFAAA3SEAAN0mAADdKAAA3SoAAN0vAADdMgAA3TUAAN03AADdXAAA3YAAAN2nAADdywAA3c4AAN3QAADd0gAA3dQAAN3WAADd2AAA3dkAAN3cAADd6QAA3foAAN38AADd/gAA3gAAAN4CAADeBAAA3gYAAN4IAADeCgAA3hsAAN4eAADeIQAA3iQAAN4nAADeKgAA3i0AAN4wAADeMwAA3jUAAN50AADedgAA3ngAAN56AADefQAA3n4AAN5/AADegAAA3oEAAN6DAADehQAA3oYAAN6HAADeiQAA3ooAAN7JAADeywAA3s0AAN7PAADe0gAA3tMAAN7UAADe1QAA3tYAAN7YAADe2gAA3tsAAN7cAADe3gAA3t8AAN8eAADfIAAA3yMAAN8lAADfKAAA3ykAAN8qAADfKwAA3ywAAN8uAADfMAAA3zEAAN8yAADfNAAA3zUAAN9CAADfQwAA30QAAN9GAADfhQAA34cAAN+JAADfiwAA344AAN+PAADfkAAA35EAAN+SAADflAAA35YAAN+XAADfmAAA35oAAN+bAADf2gAA39wAAN/eAADf4AAA3+MAAN/kAADf5QAA3+YAAN/nAADf6QAA3+sAAN/sAADf7QAA3+8AAN/wAADgLwAA4DEAAOAzAADgNQAA4DgAAOA5AADgOgAA4DsAAOA8AADgPgAA4EAAAOBBAADgQgAA4EQAAOBFAADghAAA4IYAAOCIAADgigAA4I0AAOCOAADgjwAA4JAAAOCRAADgkwAA4JUAAOCWAADglwAA4JkAAOCaAADg2QAA4NsAAODdAADg3wAA4OIAAODjAADg5AAA4OUAAODmAADg6AAA4OoAAODrAADg7AAA4O4AAODvAADhFAAA4TgAAOFfAADhgwAA4YYAAOGIAADhigAA4YwAAOGOAADhkAAA4ZEAAOGUAADhoQAA4bAAAOGyAADhtAAA4bYAAOG4AADhugAA4bwAAOG+AADhzQAA4dAAAOHTAADh1gAA4dkAAOHcAADh3wAA4eIAAOHkAADiIwAA4iUAAOInAADiKQAA4iwAAOItAADiLgAA4i8AAOIwAADiMgAA4jQAAOI1AADiNgAA4jgAAOI5AADieAAA4noAAOJ8AADifgAA4oEAAOKCAADigwAA4oQAAOKFAADihwAA4okAAOKKAADiiwAA4o0AAOKOAADizQAA4s8AAOLRAADi0wAA4tYAAOLXAADi2AAA4tkAAOLaAADi3AAA4t4AAOLfAADi4AAA4uIAAOLjAADjIgAA4yQAAOMmAADjKAAA4ysAAOMsAADjLQAA4y4AAOMvAADjMQAA4zMAAOM0AADjNQAA4zcAAOM4AADjdwAA43kAAON7AADjfQAA44AAAOOBAADjggAA44MAAOOEAADjhgAA44gAAOOJAADjigAA44wAAOONAADjzAAA484AAOPQAADj0gAA49UAAOPWAADj1wAA49gAAOPZAADj2wAA490AAOPeAADj3wAA4+EAAOPiAADkIQAA5CMAAOQlAADkJwAA5CoAAOQrAADkLAAA5C0AAOQuAADkMAAA5DIAAOQzAADkNAAA5DYAAOQ3AADkggAA5KUAAOTFAADk5QAA5OcAAOTpAADk6wAA5O0AAOTwAADk8QAA5PIAAOT1AADk9gAA5PgAAOT5AADk+wAA5P4AAOT/AADlAAAA5QMAAOUEAADlCQAA5RYAAOUbAADlHQAA5R8AAOUkAADlJwAA5SoAAOUsAADlUQAA5XUAAOWcAADlwAAA5cMAAOXFAADlxwAA5ckAAOXLAADlzQAA5c4AAOXRAADl3gAA5e8AAOXxAADl8wAA5fUAAOX3AADl+QAA5fsAAOX9AADl/wAA5hAAAOYTAADmFgAA5hkAAOYcAADmHwAA5iIAAOYlAADmKAAA5ioAAOZpAADmawAA5m0AAOZvAADmcgAA5nMAAOZ0AADmdQAA5nYAAOZ4AADmegAA5nsAAOZ8AADmfgAA5n8AAOa+AADmwAAA5sIAAObEAADmxwAA5sgAAObJAADmygAA5ssAAObNAADmzwAA5tAAAObRAADm0wAA5tQAAOcTAADnFQAA5xgAAOcaAADnHQAA5x4AAOcfAADnIAAA5yEAAOcjAADnJQAA5yYAAOcnAADnKQAA5yoAAOc3AADnOAAA5zkAAOc7AADnegAA53wAAOd+AADngAAA54MAAOeEAADnhQAA54YAAOeHAADniQAA54sAAOeMAADnjQAA548AAOeQAADnzwAA59EAAOfTAADn1QAA59gAAOfZAADn2gAA59sAAOfcAADn3gAA5+AAAOfhAADn4gAA5+QAAOflAADoJAAA6CYAAOgoAADoKgAA6C0AAOguAADoLwAA6DAAAOgxAADoMwAA6DUAAOg2AADoNwAA6DkAAOg6AADoeQAA6HsAAOh9AADofwAA6IIAAOiDAADohAAA6IUAAOiGAADoiAAA6IoAAOiLAADojAAA6I4AAOiPAADozgAA6NAAAOjSAADo1AAA6NcAAOjYAADo2QAA6NoAAOjbAADo3QAA6N8AAOjgAADo4QAA6OMAAOjkAADpCQAA6S0AAOlUAADpeAAA6XsAAOl9AADpfwAA6YEAAOmDAADphQAA6YYAAOmJAADplgAA6aUAAOmnAADpqQAA6asAAOmtAADprwAA6bEAAOmzAADpwgAA6cUAAOnIAADpywAA6c4AAOnRAADp1AAA6dcAAOnZAADqGAAA6hoAAOocAADqHgAA6iEAAOoiAADqIwAA6iQAAOolAADqJwAA6ikAAOoqAADqKwAA6i0AAOouAADqbQAA6m8AAOpxAADqcwAA6nYAAOp3AADqeAAA6nkAAOp6AADqfAAA6n4AAOp/AADqgAAA6oIAAOqDAADqwgAA6sQAAOrGAADqyAAA6ssAAOrMAADqzQAA6s4AAOrPAADq0QAA6tMAAOrUAADq1QAA6tcAAOrYAADrFwAA6xkAAOscAADrHgAA6yEAAOsiAADrIwAA6yQAAOslAADrJwAA6ykAAOsqAADrKwAA6y0AAOsuAADrbQAA628AAOtxAADrcwAA63YAAOt3AADreAAA63kAAOt6AADrfAAA634AAOt/AADrgAAA64IAAOuDAADrwgAA68QAAOvGAADryAAA68sAAOvMAADrzQAA684AAOvPAADr0QAA69MAAOvUAADr1QAA69cAAOvYAADsFwAA7BkAAOwbAADsHQAA7CAAAOwhAADsIgAA7CMAAOwkAADsJgAA7CgAAOwpAADsKgAA7CwAAOwtAADseAAA7JsAAOy7AADs2wAA7N0AAOzfAADs4QAA7OMAAOzmAADs5wAA7OgAAOzrAADs7AAA7O4AAOzvAADs8QAA7PMAAOz0AADs9QAA7PgAAOz5AADs/gAA7QsAAO0QAADtEgAA7RQAAO0ZAADtHAAA7R8AAO0hAADtRgAA7WoAAO2RAADttQAA7bgAAO26AADtvAAA7b4AAO3AAADtwgAA7cMAAO3GAADt0wAA7eQAAO3mAADt6AAA7eoAAO3sAADt7gAA7fAAAO3yAADt9AAA7gUAAO4IAADuCwAA7g4AAO4RAADuFAAA7hcAAO4aAADuHQAA7h8AAO5eAADuYAAA7mIAAO5kAADuZwAA7mgAAO5pAADuagAA7msAAO5tAADubwAA7nAAAO5xAADucwAA7nQAAO6zAADutQAA7rcAAO65AADuvAAA7r0AAO6+AADuvwAA7sAAAO7CAADuxAAA7sUAAO7GAADuyAAA7skAAO8IAADvCgAA7w0AAO8PAADvEgAA7xMAAO8UAADvFQAA7xYAAO8YAADvGgAA7xsAAO8cAADvHgAA7x8AAO8sAADvLQAA7y4AAO8wAADvbwAA73EAAO9zAADvdQAA73gAAO95AADvegAA73sAAO98AADvfgAA74AAAO+BAADvggAA74QAAO+FAADvxAAA78YAAO/IAADvygAA780AAO/OAADvzwAA79AAAO/RAADv0wAA79UAAO/WAADv1wAA79kAAO/aAADwGQAA8BsAAPAdAADwHwAA8CIAAPAjAADwJAAA8CUAAPAmAADwKAAA8CoAAPArAADwLAAA8C4AAPAvAADwbgAA8HAAAPByAADwdAAA8HcAAPB4AADweQAA8HoAAPB7AADwfQAA8H8AAPCAAADwgQAA8IMAAPCEAADwwwAA8MUAAPDHAADwyQAA8MwAAPDNAADwzgAA8M8AAPDQAADw0gAA8NQAAPDVAADw1gAA8NgAAPDZAADw/gAA8SIAAPFJAADxbQAA8XAAAPF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nAAENjgABDbIAAQ21AAENtwABDbkAAQ27AAENvQABDb8AAQ3AAAENwwABDdAAAQ3hAAEN4wABDeUAAQ3nAAEN6QABDesAAQ3tAAEN7wABDfEAAQ4CAAEOBQABDggAAQ4LAAEODgABDhEAAQ4UAAEOFwABDhoAAQ4cAAEOWwABDl0AAQ5fAAEOYQABDmQAAQ5lAAEOZgABDmcAAQ5oAAEOagABDmwAAQ5tAAEObgABDnAAAQ5xAAEOsAABDrIAAQ60AAEOtgABDrkAAQ66AAEOuwABDrwAAQ69AAEOvwABDsEAAQ7CAAEOwwABDsUAAQ7GAAEPBQABDwcAAQ8KAAEPDAABDw8AAQ8QAAEPEQABDxIAAQ8TAAEPFQABDxcAAQ8YAAEPGQABDxsAAQ8cAAEPKQABDyoAAQ8rAAEPLQABD2wAAQ9uAAEPcAABD3IAAQ91AAEPdgABD3cAAQ94AAEPeQABD3sAAQ99AAEPfgABD38AAQ+BAAEPggABD8EAAQ/DAAEPxQABD8cAAQ/KAAEPywABD8wAAQ/NAAEPzgABD9AAAQ/SAAEP0wABD9QAAQ/WAAEP1wABEBYAARAYAAEQGgABEBwAARAfAAEQIAABECEAARAiAAEQIwABECUAARAnAAEQKAABECkAARArAAEQLAABEGsAARBtAAEQbwABEHEAARB0AAEQdQABEHYAARB3AAEQeAABEHoAARB8AAEQfQABEH4AARCAAAEQgQABEMAAARDCAAEQxAABEMYAARDJAAEQygABEMsAARDMAAEQzQABEM8AARDRAAEQ0gABENMAARDVAAEQ1gABEPsAAREfAAERRgABEWoAARFtAAERbwABEXEAARFzAAERdQABEXcAARF4AAERewABEYgAARGXAAERmQABEZsAARGdAAERnwABEaEAARGjAAERpQABEbQAARG3AAERugABEb0AARHAAAERwwABEcYAARHJAAERywABEgoAARIMAAESDgABEhAAARITAAESFAABEhUAARIWAAESFwABEhkAARIbAAESHAABEh0AARIfAAESIAABEl8AARJhAAESYwABEmUAARJoAAESaQABEmoAARJrAAESbAABEm4AARJwAAEScQABEnIAARJ0AAESdQABErQAARK2AAESuAABEroAARK9AAESvgABEr8AARLAAAESwQABEsMAARLFAAESxgABEscAARLJAAESygABEwkAARMLAAETDQABEw8AARMSAAETEwABExQAARMVAAETFgABExgAARMaAAETGwABExwAARMeAAETHwABE14AARNgAAETYgABE2QAARNnAAETaAABE2kAARNqAAETawABE20AARNvAAETcAABE3EAARNzAAETdAABE7MAARO1AAETtwABE7kAARO8AAETvQABE74AARO/AAETwAABE8IAARPEAAETxQABE8YAARPIAAETyQABFAgAARQKAAEUDAABFA4AARQRAAEUEgABFBMAARQUAAEUFQABFBcAARQZAAEUGgABFBsAARQdAAEUHgABFGkAARSMAAEUrAABFMwAARTOAAEU0AABFNIAARTUAAEU1wABFNgAARTZAAEU3AABFN0AARTfAAEU4AABFOIAARTlAAEU5gABFOcAARTqAAEU6wABFPAAART9AAEVAgABFQQAARUGAAEVCwABFQ4AARURAAEVEwABFTgAARVcAAEVgwABFacAARWqAAEVrAABFa4AARWwAAEVsgABFbQAARW1AAEVuAABFcUAARXWAAEV2AABFdoAARXcAAEV3gABFeAAARXiAAEV5AABFeYAARX3AAEV+gABFf0AARYAAAEWAwABFgYAARYJAAEWDAABFg8AARYRAAEWUAABFlIAARZUAAEWVgABFlkAARZaAAEWWwABFlwAARZdAAEWXwABFmEAARZiAAEWYwABFmUAARZmAAEWpQABFqcAARapAAEWqwABFq4AARavAAEWsAABFrEAARayAAEWtAABFrYAARa3AAEWuAABFroAARa7AAEW+gABFvwAARb/AAEXAQABFwQAARcFAAEXBgABFwcAARcIAAEXCgABFwwAARcNAAEXDgABFxAAARcRAAEXHgABFx8AARcgAAEXIgABF2EAARdjAAEXZQABF2cAARdqAAEXawABF2wAARdtAAEXbgABF3AAARdyAAEXcwABF3QAARd2AAEXdwABF7YAARe4AAEXugABF7wAARe/AAEXwAABF8EAARfCAAEXwwABF8UAARfHAAEXyAABF8kAARfLAAEXzAABGAsAARgNAAEYDwABGBEAARgUAAEYFQABGBYAARgXAAEYGAABGBoAARgcAAEYHQABGB4AARggAAEYIQABGGAAARhiAAEYZAABGGYAARhpAAEYagABGGsAARhsAAEYbQABGG8AARhxAAEYcgABGHMAARh1AAEYdgABGLUAARi3AAEYuQABGLsAARi+AAEYvwABGMAAARjBAAEYwgABGMQAARjGAAEYxwABGMgAARjKAAEYywABGPAAARkUAAEZOwABGV8AARliAAEZZAABGWYAARloAAEZagABGWwAARltAAEZcAABGX0AARmMAAEZjgABGZAAARmSAAEZlAABGZYAARmYAAEZmgABGakAARmsAAEZrwABGbIAARm1AAEZuAABGbsAARm+AAEZwAABGf8AARoBAAEaAwABGgUAARoIAAEaCQABGgoAARoLAAEaDAABGg4AARoQAAEaEQABGhIAARoUAAEaFQABGlQAARpWAAEaWAABGloAARpdAAEaXgABGl8AARpgAAEaYQABGmMAARplAAEaZgABGmcAARppAAEaagABGqkAARqrAAEarQABGq8AARqyAAEaswABGrQAARq1AAEatgABGrgAARq6AAEauwABGrwAARq+AAEavwABGv4AARsAAAEbAgABGwQAARsHAAEbCAABGwkAARsKAAEbCwABGw0AARsPAAEbEAABGxEAARsTAAEbFAABG1MAARtVAAEbVwABG1kAARtcAAEbXQABG14AARtfAAEbYAABG2IAARtkAAEbZQABG2YAARtoAAEbaQABG6gAARuqAAEbrAABG64AARuxAAEbsgABG7MAARu0AAEbtQABG7cAARu5AAEbugABG7sAARu9AAEbvgABG/0AARv/AAEcAQABHAMAARwGAAEcBwABHAgAARwJAAEcCgABHAwAARwOAAEcDwABHBAAARwSAAEcEwABHB4AARwnAAEcKAABHCoAARwzAAEcPgABHE0AARxYAAEcZgABHHsAARyPAAEcpgABHLgAARz3AAEc+QABHPsAARz9AAEdAAABHQEAAR0CAAEdAwABHQQAAR0GAAEdCAABHQkAAR0KAAEdDAABHQ0AAR1MAAEdTgABHVAAAR1SAAEdVQABHVYAAR1XAAEdWAABHVkAAR1bAAEdXQABHV4AAR1fAAEdYQABHWIAAR2hAAEdowABHaUAAR2nAAEdqgABHasAAR2sAAEdrQABHa4AAR2wAAEdsgABHbMAAR20AAEdtgABHbcAAR32AAEd+AABHfoAAR38AAEd/wABHgAAAR4BAAEeAgABHgMAAR4FAAEeBwABHggAAR4JAAEeCwABHgwAAR5LAAEeTQABHk8AAR5RAAEeVAABHlUAAR5WAAEeVwABHlgAAR5aAAEeXAABHl0AAR5eAAEeYAABHmEAAR6sAAEezwABHu8AAR8PAAEfEQABHxMAAR8VAAEfFwABHxkAAR8aAAEfGwABHx4AAR8fAAEfIQABHyIAAR8kAAEfJgABHycAAR8oAAEfKwABHywAAR8xAAEfPgABH0MAAR9FAAEfRwABH0wAAR9PAAEfUgABH1QAAR95AAEfnQABH8QAAR/oAAEf6wABH+0AAR/vAAEf8QABH/MAAR/1AAEf9gABH/kAASAGAAEgFwABIBkAASAbAAEgHQABIB8AASAhAAEgIwABICUAASAnAAEgOAABIDsAASA+AAEgQQABIEQAASBHAAEgSgABIE0AASBQAAEgUgABIJEAASCTAAEglQABIJcAASCaAAEgmwABIJwAASCdAAEgngABIKAAASCiAAEgowABIKQAASCmAAEgpwABIOYAASDoAAEg6gABIOwAASDvAAEg8AABIPEAASDyAAEg8wABIPUAASD3AAEg+AABIPkAASD7AAEg/AABITsAASE9AAEhQAABIUIAASFFAAEhRgABIUcAASFIAAEhSQABIUsAASFNAAEhTgABIU8AASFRAAEhUgABIV8AASFgAAEhYQABIWMAASGiAAEhpAABIaYAASGoAAEhqwABIawAASGtAAEhrgABIa8AASGxAAEhswABIbQAASG1AAEhtwABIbgAASH3AAEh+QABIfsAASH9AAEiAAABIgEAASICAAEiAwABIgQAASIGAAEiCAABIgkAASIKAAEiDAABIg0AASJMAAEiTgABIlAAASJSAAEiVQABIlYAASJXAAEiWAABIlkAASJbAAEiXQABIl4AASJfAAEiYQABImIAASKhAAEiowABIqUAASKnAAEiqgABIqsAASKsAAEirQABIq4AASKwAAEisgABIrMAASK0AAEitgABIrcAASL2AAEi+AABIvoAASL8AAEi/wABIwAAASMBAAEjAgABIwMAASMFAAEjBwABIwgAASMJAAEjCwABIwwAASMxAAEjVQABI3wAASOgAAEjowABI6UAASOnAAEjqQABI6sAASOtAAEjrgABI7EAASO+AAEjzQABI88AASPRAAEj0wABI9UAASPXAAEj2QABI9sAASPqAAEj7QABI/AAASPzAAEj9gABI/kAASP8AAEj/wABJAEAASRAAAEkQgABJEQAASRGAAEkSQABJEoAASRLAAEkTAABJE0AASRPAAEkUQABJFIAASRTAAEkVQABJFYAASSVAAEklwABJJkAASSbAAEkngABJJ8AASSgAAEkoQABJKIAASSkAAEkpgABJKcAASSoAAEkqgABJKsAASTqAAEk7AABJO4AASTwAAEk8wABJPQAAST1AAEk9gABJPcAAST5AAEk+wABJPwAAST9AAEk/wABJQAAASU/AAElQQABJUMAASVFAAElSAABJUkAASVKAAElSwABJUwAASVOAAElUAABJVEAASVSAAElVAABJVUAASWUAAEllgABJZgAASWaAAElnQABJZ4AASWfAAEloAABJaEAASWjAAElpQABJaYAASWnAAElqQABJaoAASXpAAEl6wABJe0AASXvAAEl8gABJfMAASX0AAEl9QABJfYAASX4AAEl+gABJfsAASX8AAEl/gABJf8AASY+AAEmQAABJkIAASZEAAEmRwABJkgAASZJAAEmSgABJksAASZNAAEmTwABJlAAASZRAAEmUwABJlQAASafAAEmwgABJuIAAScCAAEnBAABJwYAAScIAAEnCgABJwwAAScNAAEnDgABJxEAAScSAAEnFAABJxUAAScXAAEnGQABJxoAAScbAAEnHgABJx8AASckAAEnMQABJzYAASc4AAEnOgABJz8AASdCAAEnRQABJ0cAASdsAAEnkAABJ7cAASfbAAEn3gABJ+AAASfiAAEn5AABJ+YAASfoAAEn6QABJ+wAASf5AAEoCgABKAwAASgOAAEoEAABKBIAASgUAAEoFgABKBgAASgaAAEoKwABKC4AASgxAAEoNAABKDcAASg6AAEoPQABKEAAAShDAAEoRQABKIQAASiGAAEoiAABKIoAASiNAAEojgABKI8AASiQAAEokQABKJMAASiVAAEolgABKJcAASiZAAEomgABKNkAASjbAAEo3QABKN8AASjiAAEo4wABKOQAASjlAAEo5gABKOgAASjqAAEo6wABKOwAASjuAAEo7wABKS4AASkwAAEpMwABKTUAASk4AAEpOQABKToAASk7AAEpPAABKT4AASlAAAEpQQABKUIAASlEAAEpRQABKVIAASlTAAEpVAABKVYAASmVAAEplwABKZkAASmbAAEpngABKZ8AASmgAAEpoQABKaIAASmkAAEppgABKacAASmoAAEpqgABKasAASnqAAEp7AABKe4AASnwAAEp8wABKfQAASn1AAEp9gABKfcAASn5AAEp+wABKfwAASn9AAEp/wABKgAAASo/AAEqQQABKkMAASpFAAEqSAABKkkAASpKAAEqSwABKkwAASpOAAEqUAABKlEAASpSAAEqVAABKlUAASqUAAEqlgABKpgAASqaAAEqnQABKp4AASqfAAEqoAABKqEAASqjAAEqpQABKqYAASqnAAEqqQABKqoAASrpAAEq6wABKu0AASrvAAEq8gABKvMAASr0AAEq9QABKvYAASr4AAEq+gABKvsAASr8AAEq/gABKv8AASskAAErSAABK28AASuTAAErlgABK5gAASuaAAErnAABK54AASugAAEroQABK6QAASuxAAErwAABK8IAASvEAAErxgABK8gAASvKAAErzAABK84AASvdAAEr4AABK+MAASvmAAEr6QABK+wAASvvAAEr8gABK/QAASwzAAEsNQABLDcAASw5AAEsPAABLD0AASw+AAEsPwABLEAAASxCAAEsRAABLEUAASxGAAEsSAABLEkAASyIAAEsigABLIwAASyOAAEskQABLJIAASyTAAEslAABLJUAASyXAAEsmQABLJoAASybAAEsnQABLJ4AASzdAAEs3wABLOEAASzjAAEs5gABLOcAASzoAAEs6QABLOoAASzsAAEs7gABLO8AASzwAAEs8gABLPMAAS0yAAEtNAABLTcAAS05AAEtPAABLT0AAS0+AAEtPwABLUAAAS1CAAEtRAABLUUAAS1GAAEtSAABLUkAAS2IAAEtigABLYwAAS2OAAEtkQABLZIAAS2TAAEtlAABLZUAAS2XAAEtmQABLZoAAS2bAAEtnQABLZ4AAS3dAAEt3wABLeEAAS3jAAEt5gABLecAAS3oAAEt6QABLeoAAS3sAAEt7gABLe8AAS3wAAEt8gABLfMAAS4yAAEuNAABLjYAAS44AAEuOwABLjwAAS49AAEuPgABLj8AAS5BAAEuQwABLkQAAS5FAAEuRwABLkgAAS6TAAEutgABLtYAAS72AAEu+AABLvoAAS78AAEu/gABLwAAAS8BAAEvAgABLwUAAS8GAAEvCAABLwkAAS8LAAEvDQABLw4AAS8PAAEvEgABLxMAAS8YAAEvJQABLyoAAS8sAAEvLgABLzMAAS82AAEvOQABLzsAAS9gAAEvhAABL6sAAS/PAAEv0gABL9QAAS/WAAEv2AABL9oAAS/cAAEv3QABL+AAAS/tAAEv/gABMAAAATACAAEwBAABMAYAATAIAAEwCgABMAwAATAOAAEwHwABMCIAATAlAAEwKAABMCsAATAuAAEwMQABMDQAATA3AAEwOQABMHgAATB6AAEwfAABMH4AATCBAAEwggABMIMAATCEAAEwhQABMIcAATCJAAEwigABMIsAATCNAAEwjgABMM0AATDPAAEw0QABMNMAATDWAAEw1wABMNgAATDZAAEw2gABMNwAATDeAAEw3wABMOAAATDiAAEw4wABMSIAATEkAAExJwABMSkAATEsAAExLQABMS4AATEvAAExMAABMTIAATE0AAExNQABMTYAATE4AAExOQABMUYAATFHAAExSAABMUoAATGJAAExiwABMY0AATGPAAExkgABMZMAATGUAAExlQABMZYAATGYAAExmgABMZsAATGcAAExngABMZ8AATHeAAEx4AABMeIAATHkAAEx5wABMegAATHpAAEx6gABMesAATHtAAEx7wABMfAAATHxAAEx8wABMfQAATIzAAEyNQABMjcAATI5AAEyPAABMj0AATI+AAEyPwABMkAAATJCAAEyRAABMkUAATJGAAEySAABMkkAATKIAAEyigABMowAATKOAAEykQABMpIAATKTAAEylAABMpUAATKXAAEymQABMpoAATKbAAEynQABMp4AATLdAAEy3wABMuEAATLjAAEy5gABMucAATLoAAEy6QABMuoAATLsAAEy7gABMu8AATLwAAEy8gABMvMAATMYAAEzPAABM2MAATOHAAEzigABM4wAATOOAAEzkAABM5IAATOUAAEzlQABM5gAATOlAAEztAABM7YAATO4AAEzugABM7wAATO+AAEzwAABM8IAATPRAAEz1AABM9cAATPaAAEz3QABM+AAATPjAAEz5gABM+gAATQnAAE0KQABNCwAATQuAAE0MQABNDIAATQzAAE0NAABNDUAATQ3AAE0OQABNDoAATQ7AAE0PQABND4AATR9AAE0fwABNIEAATSDAAE0hgABNIcAATSIAAE0iQABNIoAATSMAAE0jgABNI8AATSQAAE0kgABNJMAATTSAAE01AABNNYAATTYAAE02wABNNwAATTdAAE03gABNN8AATThAAE04wABNOQAATTlAAE05wABNOgAATUnAAE1KQABNSwAATUuAAE1MQABNTIAATUzAAE1NAABNTUAATU3AAE1OQABNToAATU7AAE1PQABNT4AATV9AAE1fwABNYEAATWDAAE1hgABNYcAATWIAAE1iQABNYoAATWMAAE1jgABNY8AATWQAAE1kgABNZMAATXSAAE11AABNdYAATXYAAE12wABNdwAATXdAAE13gABNd8AATXhAAE14wABNeQAATXlAAE15wABNegAATYnAAE2KQABNisAATYtAAE2MAABNjEAATYyAAE2MwABNjQAATY2AAE2OAABNjkAATY6AAE2PAABNj0AATaIAAE2qwABNssAATbrAAE27QABNu8AATbxAAE28wABNvUAATb2AAE29wABNvoAATb7AAE2/QABNv4AATcAAAE3AgABNwMAATcEAAE3BwABNwgAATcNAAE3GgABNx8AATchAAE3IwABNygAATcrAAE3LgABNzAAATdVAAE3eQABN6AAATfEAAE3xwABN8kAATfLAAE3zQABN88AATfRAAE30gABN9UAATfiAAE38wABN/UAATf3AAE3+QABN/sAATf9AAE3/wABOAEAATgDAAE4FAABOBcAATgaAAE4HQABOCAAATgjAAE4JgABOCkAATgsAAE4LgABOG0AAThvAAE4cQABOHMAATh2AAE4dwABOHgAATh5AAE4egABOHwAATh+AAE4fwABOIAAATiCAAE4gwABOMIAATjEAAE4xgABOMgAATjLAAE4zAABOM0AATjOAAE4zwABONEAATjTAAE41AABONUAATjXAAE42AABORcAATkZAAE5HAABOR4AATkhAAE5IgABOSMAATkkAAE5JQABOScAATkpAAE5KgABOSsAATktAAE5LgABOTsAATk8AAE5PQABOT8AATl+AAE5gAABOYIAATmEAAE5hwABOYgAATmJAAE5igABOYsAATmNAAE5jwABOZAAATmRAAE5kwABOZQAATnTAAE51QABOdcAATnZAAE53AABOd0AATneAAE53wABOeAAATniAAE55AABOeUAATnmAAE56AABOekAATooAAE6KgABOiwAATouAAE6MQABOjIAATozAAE6NAABOjUAATo3AAE6OQABOjoAATo7AAE6PQABOj4AATp9AAE6fwABOoEAATqDAAE6hgABOocAATqIAAE6iQABOooAATqMAAE6jgABOo8AATqQAAE6kgABOpMAATrSAAE61AABOtYAATrYAAE62wABOtwAATrdAAE63gABOt8AATrhAAE64wABOuQAATrlAAE65wABOugAATsNAAE7MQABO1gAATt8AAE7fwABO4EAATuDAAE7hQABO4cAATuJAAE7igABO40AATuaAAE7qQABO6sAATutAAE7rwABO7EAATuzAAE7tQABO7cAATvGAAE7yQABO8wAATvPAAE70gABO9UAATvYAAE72wABO90AATwcAAE8HgABPCAAATwiAAE8JQABPCYAATwnAAE8KAABPCkAATwrAAE8LQABPC4AATwvAAE8MQABPDIAATxxAAE8cwABPHUAATx3AAE8egABPHsAATx8AAE8fQABPH4AATyAAAE8ggABPIMAATyEAAE8hgABPIcAATzGAAE8yAABPMoAATzMAAE8zwABPNAAATzRAAE80gABPNMAATzVAAE81wABPNgAATzZAAE82wABPNwAAT0bAAE9HQABPR8AAT0hAAE9JAABPSUAAT0mAAE9JwABPSgAAT0qAAE9LAABPS0AAT0uAAE9MAABPTEAAT1wAAE9cgABPXQAAT12AAE9eQABPXoAAT17AAE9fAABPX0AAT1/AAE9gQABPYIAAT2DAAE9hQABPYYAAT3FAAE9xwABPckAAT3LAAE9zgABPc8AAT3QAAE90QABPdIAAT3UAAE91gABPdcAAT3YAAE92gABPdsAAT4aAAE+HAABPh4AAT4gAAE+IwABPiQAAT4lAAE+JgABPicAAT4pAAE+KwABPiwAAT4tAAE+LwABPjAAAT57AAE+ngABPr4AAT7eAAE+4AABPuIAAT7kAAE+5gABPugAAT7pAAE+6gABPu0AAT7uAAE+8AABPvEAAT7zAAE+9QABPvYAAT73AAE++gABPvsAAT8AAAE/DQABPxIAAT8UAAE/FgABPxsAAT8eAAE/IQABPyMAAT9IAAE/bAABP5MAAT+3AAE/ugABP7wAAT++AAE/wAABP8IAAT/EAAE/xQABP8gAAT/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BAAFRQwABUUUAAVFHAAFRVgABUVkAAVFcAAFRXwABUWIAAVFlAAFRaAABUWsAAVFtAAFRrAABUa4AAVGwAAFRsgABUbUAAVG2AAFRtwABUbgAAVG5AAFRuwABUb0AAVG+AAFRvwABUcEAAVHCAAFSAQABUgMAAVIFAAFSBwABUgoAAVILAAFSDAABUg0AAVIOAAFSEAABUhIAAVITAAFSFAABUhYAAVIXAAFSVgABUlgAAVJaAAFSXAABUl8AAVJgAAFSYQABUmIAAVJjAAFSZQABUmcAAVJoAAFSaQABUmsAAVJsAAFSqwABUq0AAVKvAAFSsQABUrQAAVK1AAFStgABUrcAAVK4AAFSugABUrwAAVK9AAFSvgABUsAAAVLBAAFTAAABUwIAAVMEAAFTBgABUwkAAVMKAAFTCwABUwwAAVMNAAFTDwABUxEAAVMSAAFTEwABUxUAAVMWAAFTVQABU1cAAVNZAAFTWwABU14AAVNfAAFTYAABU2EAAVNiAAFTZAABU2YAAVNnAAFTaAABU2oAAVNrAAFTqgABU6wAAVOuAAFTsAABU7MAAVO0AAFTtQABU7YAAVO3AAFTuQABU7sAAVO8AAFTvQABU78AAVPAAAFUCwABVC4AAVROAAFUbgABVHAAAVRyAAFUdAABVHYAAVR5AAFUegABVHsAAVR+AAFUfwABVIEAAVSCAAFUhAABVIYAAVSHAAFUiAABVIsAAVSMAAFUlQABVKIAAVSnAAFUqQABVKsAAVSwAAFUswABVLYAAVS4AAFU3QABVQEAAVUoAAFVTAABVU8AAVVRAAFVUwABVVUAAVVXAAFVWQABVVoAAVVdAAFVagABVXsAAVV9AAFVfwABVYEAAVWDAAFVhQABVYcAAVWJAAFViwABVZwAAVWfAAFVogABVaUAAVWoAAFVqwABVa4AAVWxAAFVtAABVbYAAVX1AAFV9wABVfkAAVX7AAFV/gABVf8AAVYAAAFWAQABVgIAAVYEAAFWBgABVgcAAVYIAAFWCgABVgsAAVZKAAFWTAABVk4AAVZQAAFWUwABVlQAAVZVAAFWVgABVlcAAVZZAAFWWwABVlwAAVZdAAFWXwABVmAAAVafAAFWoQABVqQAAVamAAFWqQABVqoAAVarAAFWrAABVq0AAVavAAFWsQABVrIAAVazAAFWtQABVrYAAVbDAAFWxAABVsUAAVbHAAFXBgABVwgAAVcKAAFXDAABVw8AAVcQAAFXEQABVxIAAVcTAAFXFQABVxcAAVcYAAFXGQABVxsAAVccAAFXWwABV10AAVdfAAFXYQABV2QAAVdlAAFXZgABV2cAAVdoAAFXagABV2wAAVdtAAFXbgABV3AAAVdxAAFXsAABV7IAAVe0AAFXtgABV7kAAVe6AAFXuwABV7wAAVe9AAFXvwABV8EAAVfCAAFXwwABV8UAAVfGAAFYBQABWAcAAVgJAAFYCwABWA4AAVgPAAFYEAABWBEAAVgSAAFYFAABWBYAAVgXAAFYGAABWBoAAVgbAAFYWgABWFwAAVheAAFYYAABWGMAAVhkAAFYZQABWGYAAVhnAAFYaQABWGsAAVhsAAFYbQABWG8AAVhwAAFYlQABWLkAAVjgAAFZBAABWQcAAVkJAAFZCwABWQ0AAVkPAAFZEQABWRIAAVkVAAFZIgABWTEAAVkzAAFZNQABWTcAAVk5AAFZOwABWT0AAVk/AAFZTgABWVEAAVlUAAFZVwABWVoAAVldAAFZYAABWWMAAVllAAFZpAABWaYAAVmoAAFZqgABWa0AAVmuAAFZrwABWbAAAVmxAAFZswABWbUAAVm2AAFZtwABWbkAAVm6AAFZ+QABWfsAAVn9AAFZ/wABWgIAAVoDAAFaBAABWgUAAVoGAAFaCAABWgoAAVoLAAFaDAABWg4AAVoPAAFaTgABWlAAAVpSAAFaVAABWlcAAVpYAAFaWQABWloAAVpbAAFaXQABWl8AAVpgAAFaYQABWmMAAVpkAAFaowABWqUAAVqnAAFaqQABWqwAAVqtAAFargABWq8AAVqwAAFasgABWrQAAVq1AAFatgABWrgAAVq5AAFa+AABWvoAAVr8AAFa/gABWwEAAVsCAAFbAwABWwQAAVsFAAFbBwABWwkAAVsKAAFbCwABWw0AAVsOAAFbTQABW08AAVtRAAFbUwABW1YAAVtXAAFbWAABW1kAAVtaAAFbXAABW14AAVtfAAFbYAABW2IAAVtjAAFbogABW6QAAVunAAFbqQABW6wAAVutAAFbrgABW68AAVuwAAFbsgABW7QAAVu1AAFbtgABW7gAAVu5AAFcBAABXCcAAVxHAAFcZwABXGkAAVxrAAFcbQABXG8AAVxyAAFccwABXHQAAVx3AAFceAABXHoAAVx7AAFcfQABXH8AAVyAAAFcgQABXIQAAVyFAAFcigABXJcAAVycAAFcngABXKAAAVylAAFcqAABXKsAAVytAAFc0gABXPYAAV0dAAFdQQABXUQAAV1GAAFdSAABXUoAAV1MAAFdTgABXU8AAV1SAAFdXwABXXAAAV1yAAFddAABXXYAAV14AAFdegABXXwAAV1+AAFdgAABXZEAAV2UAAFdlwABXZoAAV2dAAFdoAABXaMAAV2mAAFdqQABXasAAV3qAAFd7AABXe4AAV3wAAFd8wABXfQAAV31AAFd9gABXfcAAV35AAFd+wABXfwAAV39AAFd/wABXgAAAV4/AAFeQQABXkMAAV5FAAFeSAABXkkAAV5KAAFeSwABXkwAAV5OAAFeUAABXlEAAV5SAAFeVAABXlUAAV6UAAFelgABXpkAAV6bAAFengABXp8AAV6gAAFeoQABXqIAAV6kAAFepgABXqcAAV6oAAFeqgABXqsAAV64AAFeuQABXroAAV68AAFe+wABXv0AAV7/AAFfAQABXwQAAV8FAAFfBgABXwcAAV8IAAFfCgABXwwAAV8NAAFfDgABXxAAAV8RAAFfUAABX1IAAV9UAAFfVgABX1kAAV9aAAFfWwABX1wAAV9dAAFfXwABX2EAAV9iAAFfYwABX2UAAV9mAAFfpQABX6cAAV+pAAFfqwABX64AAV+vAAFfsAABX7EAAV+yAAFftAABX7YAAV+3AAFfuAABX7oAAV+7AAFf+gABX/wAAV/+AAFgAAABYAMAAWAEAAFgBQABYAYAAWAHAAFgCQABYAsAAWAMAAFgDQABYA8AAWAQAAFgTwABYFEAAWBTAAFgVQABYFgAAWBZAAFgWgABYFsAAWBcAAFgXgABYGAAAWBhAAFgYgABYGQAAWBlAAFgigABYK4AAWDVAAFg+QABYPwAAWD+AAFhAAABYQIAAWEEAAFhBgABYQcAAWEKAAFhFwABYSYAAWEoAAFhKgABYSwAAWEuAAFhMAABYTIAAWE0AAFhQwABYUYAAWFJAAFhTAABYU8AAWFSAAFhVQABYVgAAWFaAAFhmQABYZsAAWGeAAFhoAABYaMAAWGkAAFhpQABYaYAAWGnAAFhqQABYasAAWGsAAFhrQABYa8AAWGwAAFhsQABYfAAAWHyAAFh9AABYfYAAWH5AAFh+gABYfsAAWH8AAFh/QABYf8AAWIBAAFiAgABYgMAAWIFAAFiBgABYkUAAWJHAAFiSQABYksAAWJOAAFiTwABYlAAAWJRAAFiUgABYlQAAWJWAAFiVwABYlgAAWJaAAFiWwABYpoAAWKcAAFingABYqAAAWKjAAFipAABYqUAAWKmAAFipwABYqkAAWKrAAFirAABYq0AAWKvAAFisAABYu8AAWLxAAFi8wABYvUAAWL4AAFi+QABYvoAAWL7AAFi/AABYv4AAWMAAAFjAQABYwIAAWMEAAFjBQABY0QAAWNGAAFjSAABY0oAAWNNAAFjTgABY08AAWNQAAFjUQABY1MAAWNVAAFjVgABY1cAAWNZAAFjWgABY5kAAWObAAFjnQABY58AAWOiAAFjowABY6QAAWOlAAFjpgABY6gAAWOqAAFjqwABY6wAAWOuAAFjrwABY/oAAWQdAAFkPQABZF0AAWRfAAFkYQABZGMAAWRlAAFkaAABZGkAAWRqAAFkbQABZG4AAWRwAAFkcQABZHMAAWR2AAFkdwABZHgAAWR7AAFkfAABZIEAAWSOAAFkkwABZJUAAWSXAAFknAABZJ8AAWSiAAFkpAABZMkAAWTtAAFlFAABZTgAAWU7AAFlPQABZT8AAWVBAAFlQwABZUUAAWVGAAFlSQABZVYAAWVnAAFlaQABZWsAAWVtAAFlbwABZXEAAWVzAAFldQABZXcAAWWIAAFliwABZY4AAWWRAAFllAABZZcAAWWaAAFlnQABZaAAAWWiAAFl4QABZeMAAWXlAAFl5wABZeoAAWXrAAFl7AABZe0AAWXuAAFl8AABZfIAAWXzAAFl9AABZfYAAWX3AAFmNgABZjgAAWY6AAFmPAABZj8AAWZAAAFmQQABZkIAAWZDAAFmRQABZkcAAWZIAAFmSQABZksAAWZMAAFmiwABZo0AAWaQAAFmkgABZpUAAWaWAAFmlwABZpgAAWaZAAFmmwABZp0AAWaeAAFmnwABZqEAAWaiAAFmrwABZrAAAWaxAAFmswABZvIAAWb0AAFm9gABZvgAAWb7AAFm/AABZv0AAWb+AAFm/wABZwEAAWcDAAFnBAABZwUAAWcHAAFnCAABZ0cAAWdJAAFnSwABZ00AAWdQAAFnUQABZ1IAAWdTAAFnVAABZ1YAAWdYAAFnWQABZ1oAAWdcAAFnXQABZ5wAAWeeAAFnoAABZ6IAAWelAAFnpgABZ6cAAWeoAAFnqQABZ6sAAWetAAFnrgABZ68AAWexAAFnsgABZ/EAAWfzAAFn9QABZ/cAAWf6AAFn+wABZ/wAAWf9AAFn/gABaAAAAWgCAAFoAwABaAQAAWgGAAFoBwABaEYAAWhIAAFoSgABaEwAAWhPAAFoUAABaFEAAWhSAAFoUwABaFUAAWhXAAFoWAABaFkAAWhbAAFoXAABaIEAAWilAAFozAABaPAAAWjzAAFo9QABaPcAAWj5AAFo+wABaP0AAWj+AAFpAQABaQ4AAWkdAAFpHwABaSEAAWkjAAFpJQABaScAAWkpAAFpKwABaToAAWk9AAFpQAABaUMAAWlGAAFpSQABaUwAAWlPAAFpUQABaZAAAWmSAAFplAABaZYAAWmZAAFpmgABaZsAAWmcAAFpnQABaZ8AAWmhAAFpogABaaMAAWmlAAFppgABaeUAAWnnAAFp6QABaesAAWnuAAFp7wABafAAAWnxAAFp8gABafQAAWn2AAFp9wABafgAAWn6AAFp+wABajoAAWo8AAFqPgABakAAAWpDAAFqRAABakUAAWpGAAFqRwABakkAAWpLAAFqTAABak0AAWpPAAFqUAABao8AAWqRAAFqkwABapUAAWqYAAFqmQABapoAAWqbAAFqnAABap4AAWqgAAFqoQABaqIAAWqkAAFqpQABauQAAWrmAAFq6AABauoAAWrtAAFq7gABau8AAWrwAAFq8QABavMAAWr1AAFq9gABavcAAWr5AAFq+gABazkAAWs7AAFrPQABaz8AAWtCAAFrQwABa0QAAWtFAAFrRgABa0gAAWtKAAFrSwABa0wAAWtOAAFrTwABa44AAWuQAAFrkgABa5QAAWuXAAFrmAABa5kAAWuaAAFrmwABa50AAWufAAFroAABa6EAAWujAAFrpAABa+8AAWwSAAFsMgABbFIAAWxUAAFsVgABbFgAAWxaAAFsXQABbF4AAWxfAAFsYgABbGMAAWxlAAFsZgABbGgAAWxrAAFsbAABbG0AAWxwAAFscQABbHYAAWyDAAFsiAABbIoAAWyMAAFskQABbJQAAWyXAAFsmQABbL4AAWziAAFtCQABbS0AAW0wAAFtMgABbTQAAW02AAFtOAABbToAAW07AAFtPgABbUsAAW1cAAFtXgABbWAAAW1iAAFtZAABbWYAAW1oAAFtagABbWwAAW19AAFtgAABbYMAAW2GAAFtiQABbYwAAW2PAAFtkgABbZUAAW2XAAFt1gABbdgAAW3aAAFt3AABbd8AAW3gAAFt4QABbeIAAW3jAAFt5QABbecAAW3oAAFt6QABbesAAW3sAAFuKwABbi0AAW4vAAFuMQABbjQAAW41AAFuNgABbjcAAW44AAFuOgABbjwAAW49AAFuPgABbkAAAW5BAAFugAABboIAAW6FAAFuhwABbooAAW6LAAFujAABbo0AAW6OAAFukAABbpIAAW6TAAFulAABbpYAAW6XAAFupAABbqUAAW6mAAFuqAABbucAAW7pAAFu6wABbu0AAW7wAAFu8QABbvIAAW7zAAFu9AABbvYAAW74AAFu+QABbvoAAW78AAFu/QABbzwAAW8+AAFvQAABb0IAAW9FAAFvRgABb0cAAW9IAAFvSQABb0sAAW9NAAFvTgABb08AAW9RAAFvUgABb5EAAW+TAAFvlQABb5cAAW+aAAFvmwABb5wAAW+dAAFvngABb6AAAW+iAAFvowABb6QAAW+mAAFvpwABb+YAAW/oAAFv6gABb+wAAW/vAAFv8AABb/EAAW/yAAFv8wABb/UAAW/3AAFv+AABb/kAAW/7AAFv/AABcDsAAXA9AAFwPwABcEEAAXBEAAFwRQABcEYAAXBHAAFwSAABcEoAAXBMAAFwTQABcE4AAXBQAAFwUQABcHYAAXCaAAFwwQABcOUAAXDoAAFw6gABcOwAAXDuAAFw8AABcPIAAXDzAAFw9gABcQMAAXESAAFxFAABcRYAAXEYAAFxGgABcRwAAXEeAAFxIAABcS8AAXEyAAFxNQABcTgAAXE7AAFxPgABcUEAAXFEAAFxRgABcYUAAXGHAAFxiQABcYsAAXGOAAFxjwABcZAAAXGRAAFxkgABcZQAAXGWAAFxlwABcZgAAXGaAAFxmwABcdoAAXHcAAFx3gABceAAAXHjAAFx5AABceUAAXHmAAFx5wABcekAAXHrAAFx7AABce0AAXHvAAFx8AABci8AAXIxAAFyMwABcjUAAXI4AAFyOQABcjoAAXI7AAFyPAABcj4AAXJAAAFyQQABckIAAXJEAAFyRQABcoQAAXKGAAFyiAABcooAAXKNAAFyjgABco8AAXKQAAFykQABcpMAAXKVAAFylgABcpcAAXKZAAFymgABctkAAXLbAAFy3QABct8AAXLiAAFy4wABcuQAAXLlAAFy5gABcugAAXLqAAFy6wABcuwAAXLuAAFy7wABcy4AAXMwAAFzMgABczQAAXM3AAFzOAABczkAAXM6AAFzOwABcz0AAXM/AAFzQAABc0EAAXNDAAFzRAABc4MAAXOFAAFzhwABc4kAAXOMAAFzjQABc44AAXOPAAFzkAABc5IAAXOUAAFzlQABc5YAAXOYAAFzmQABc6IAAXOjAAFzpQABc+QAAXPmAAFz6AABc+oAAXPtAAFz7gABc+8AAXPwAAFz8QABc/MAAXP1AAFz9gABc/cAAXP5AAFz+gABdDkAAXQ7AAF0PQABdD8AAXRCAAF0QwABdEQAAXRFAAF0RgABdEgAAXRKAAF0SwABdEwAAXROAAF0TwABdI4AAXSQAAF0kwABdJUAAXSYAAF0mQABdJoAAXSbAAF0nAABdJ4AAXSgAAF0oQABdKIAAXSkAAF0pQABdPAAAXUTAAF1MwABdVMAAXVVAAF1VwABdVkAAXVbAAF1XQABdV4AAXVfAAF1YgABdWMAAXVlAAF1ZgABdWgAAXVqAAF1awABdWwAAXVvAAF1cAABdXUAAXWCAAF1hwABdYkAAXWLAAF1kAABdZMAAXWWAAF1mAABdb0AAXXhAAF2CAABdiwAAXYvAAF2MQABdjMAAXY1AAF2NwABdjkAAXY6AAF2PQABdkoAAXZbAAF2XQABdl8AAXZhAAF2YwABdmUAAXZnAAF2aQABdmsAAXZ8AAF2fwABdoIAAXaFAAF2iAABdosAAXaOAAF2kQABdpQAAXaWAAF21QABdtcAAXbZAAF22wABdt4AAXbfAAF24AABduEAAXbiAAF25AABduYAAXbnAAF26AABduoAAXbrAAF3KgABdywAAXcuAAF3MAABdzMAAXc0AAF3NQABdzYAAXc3AAF3OQABdzsAAXc8AAF3PQABdz8AAXdAAAF3fwABd4EAAXeEAAF3hgABd4kAAXeKAAF3iwABd4wAAXeNAAF3jwABd5EAAXeSAAF3kwABd5UAAXeWAAF3owABd6QAAXelAAF3pwABd+YAAXfoAAF36gABd+wAAXfvAAF38AABd/EAAXfyAAF38wABd/UAAXf3AAF3+AABd/kAAXf7AAF3/AABeDsAAXg9AAF4PwABeEEAAXhEAAF4RQABeEYAAXhHAAF4SAABeEoAAXhMAAF4TQABeE4AAXhQAAF4UQABeJAAAXiSAAF4lAABeJYAAXiZAAF4mgABeJsAAXicAAF4nQABeJ8AAXihAAF4ogABeKMAAXilAAF4pgABeOUAAXjnAAF46QABeOsAAXjuAAF47wABePAAAXjxAAF48gABePQAAXj2AAF49wABePgAAXj6AAF4+wABeToAAXk8AAF5PgABeUAAAXlDAAF5RAABeUUAAXlGAAF5RwABeUkAAXlLAAF5TAABeU0AAXlPAAF5UAABeXUAAXmZAAF5wAABeeQAAXnnAAF56QABeesAAXntAAF57wABefEAAXnyAAF59QABegIAAXoPAAF6EQABehMAAXoVAAF6FwABehkAAXobAAF6KAABeisAAXouAAF6MQABejQAAXo3AAF6OgABejwAAXp7AAF6fQABeoAAAXqCAAF6hQABeoYAAXqHAAF6iAABeokAAXqLAAF6jQABeo4AAXqPAAF6kQABepIAAXrRAAF60wABetUAAXrXAAF62gABetsAAXrcAAF63QABet4AAXrgAAF64gABeuMAAXrkAAF65gABeucAAXsmAAF7KAABeyoAAXssAAF7LwABezAAAXsxAAF7MgABezMAAXs1AAF7NwABezgAAXs5AAF7OwABezwAAXt7AAF7fQABe38AAXuBAAF7hAABe4UAAXuGAAF7hwABe4gAAXuKAAF7jAABe40AAXuOAAF7kAABe5EAAXvQAAF70gABe9QAAXvWAAF72QABe9oAAXvbAAF73AABe90AAXvfAAF74QABe+IAAXvjAAF75QABe+YAAXwlAAF8JwABfCkAAXwrAAF8LgABfC8AAXwwAAF8MQABfDIAAXw0AAF8NgABfDcAAXw4AAF8OgABfDsAAXyGAAF8qQABfMkAAXzpAAF86wABfO0AAXzvAAF88QABfPMAAXz0AAF89QABfPgAAXz5AAF8+wABfPwAAXz+AAF9AAABfQEAAX0CAAF9BQABfQYAAX0LAAF9GAABfR0AAX0fAAF9IQABfSYAAX0pAAF9LAABfS4AAX1TAAF9dwABfZ4AAX3CAAF9xQABfccAAX3JAAF9ywABfc0AAX3PAAF90AABfdMAAX3gAAF98QABffMAAX3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JAAGIzAABiM0AAYjOAAGIzwABiNAAAYjSAAGI1AABiNUAAYjWAAGI2AABiNkAAYj+AAGJIgABiUkAAYltAAGJcAABiXIAAYl0AAGJdgABiXgAAYl6AAGJewABiX4AAYmLAAGJmgABiZwAAYmeAAGJoAABiaIAAYmkAAGJpgABiagAAYm3AAGJugABib0AAYnAAAGJwwABicYAAYnJAAGJzAABic4AAYoNAAGKDwABihIAAYoUAAGKFwABihgAAYoZAAGKGgABihsAAYodAAGKHwABiiAAAYohAAGKIwABiiQAAYpjAAGKZQABimcAAYppAAGKbAABim0AAYpuAAGKbwABinAAAYpyAAGKdAABinUAAYp2AAGKeAABinkAAYq4AAGKugABirwAAYq+AAGKwQABisIAAYrDAAGKxAABisUAAYrHAAGKyQABisoAAYrLAAGKzQABis4AAYsNAAGLDwABixIAAYsUAAGLFwABixgAAYsZAAGLGgABixsAAYsdAAGLHwABiyAAAYshAAGLIwABiyQAAYtjAAGLZQABi2cAAYtpAAGLbAABi20AAYtuAAGLbwABi3AAAYtyAAGLdAABi3UAAYt2AAGLeAABi3kAAYu4AAGLugABi7wAAYu+AAGLwQABi8IAAYvDAAGLxAABi8UAAYvHAAGLyQABi8oAAYvLAAGLzQABi84AAYwNAAGMDwABjBEAAYwTAAGMFgABjBcAAYwYAAGMGQABjBoAAYwcAAGMHgABjB8AAYwgAAGMIgABjCMAAYxuAAGMkQABjLEAAYzRAAGM0wABjNUAAYzXAAGM2QABjNsAAYzcAAGM3QABjOAAAYzhAAGM4wABjOQAAYzmAAGM6AABjOkAAYzqAAGM7QABjO4AAYzzAAGNAAABjQUAAY0HAAGNCQABjQ4AAY0RAAGNFAABjRYAAY07AAGNXwABjYYAAY2qAAGNrQABja8AAY2xAAGNswABjbUAAY23AAGNuAABjbsAAY3IAAGN2QABjdsAAY3dAAGN3wABjeEAAY3jAAGN5QABjecAAY3pAAGN+gABjf0AAY4AAAGOAwABjgYAAY4JAAGODAABjg8AAY4SAAGOFAABjlMAAY5VAAGOVwABjlkAAY5cAAGOXQABjl4AAY5fAAGOYAABjmIAAY5kAAGOZQABjmYAAY5oAAGOaQABjqgAAY6qAAGOrAABjq4AAY6xAAGOsgABjrMAAY60AAGOtQABjrcAAY65AAGOugABjrsAAY69AAGOvgABjv0AAY7/AAGPAgABjwQAAY8HAAGPCAABjwkAAY8KAAGPCwABjw0AAY8PAAGPEAABjxEAAY8TAAGPFAABjyEAAY8iAAGPIwABjyUAAY9kAAGPZgABj2gAAY9qAAGPbQABj24AAY9vAAGPcAABj3EAAY9zAAGPdQABj3YAAY93AAGPeQABj3oAAY+5AAGPuwABj70AAY+/AAGPwgABj8MAAY/EAAGPxQABj8YAAY/IAAGPygABj8sAAY/MAAGPzgABj88AAZAOAAGQEAABkBIAAZAUAAGQFwABkBgAAZAZAAGQGgABkBsAAZAdAAGQHwABkCAAAZAhAAGQIwABkCQAAZBjAAGQZQABkGcAAZBpAAGQbAABkG0AAZBuAAGQbwABkHAAAZByAAGQdAABkHUAAZB2AAGQeAABkHkAAZC4AAGQugABkLwAAZC+AAGQwQABkMIAAZDDAAGQxAABkMUAAZDHAAGQyQABkMoAAZDLAAGQzQABkM4AAZDzAAGRFwABkT4AAZFiAAGRZQABkWcAAZFpAAGRawABkW0AAZFvAAGRcAABkXMAAZGAAAGRjwABkZEAAZGTAAGRlQABkZcAAZGZAAGRmwABkZ0AAZGsAAGRrwABkbIAAZG1AAGRuAABkbsAAZG+AAGRwQABkcMAAZICAAGSBAABkgYAAZIIAAGSCwABkgwAAZINAAGSDgABkg8AAZIRAAGSEwABkhQAAZIVAAGSFwABkhgAAZJXAAGSWQABklsAAZJdAAGSYAABkmEAAZJiAAGSYwABkmQAAZJmAAGSaAABkmkAAZJqAAGSbAABkm0AAZKsAAGSrgABkrAAAZKyAAGStQABkrYAAZK3AAGSuAABkrkAAZK7AAGSvQABkr4AAZK/AAGSwQABksIAAZMBAAGTAwABkwYAAZMIAAGTCwABkwwAAZMNAAGTDgABkw8AAZMRAAGTEwABkxQAAZMVAAGTFwABkxgAAZNXAAGTWQABk1sAAZNdAAGTYAABk2EAAZNiAAGTYwABk2QAAZNmAAGTaAABk2kAAZNqAAGTbAABk20AAZOsAAGTrgABk7AAAZOyAAGTtQABk7YAAZO3AAGTuAABk7kAAZO7AAGTvQABk74AAZO/AAGTwQABk8IAAZQBAAGUAwABlAUAAZQHAAGUCgABlAsAAZQMAAGUDQABlA4AAZQQAAGUEgABlBMAAZQUAAGUFgABlBcAAZRiAAGUhQABlKUAAZTFAAGUxwABlMkAAZTLAAGUzQABlM8AAZTQAAGU0QABlNQAAZTVAAGU1wABlNgAAZTaAAGU3AABlN0AAZTeAAGU4QABlOIAAZTrAAGU+AABlP0AAZT/AAGVAQABlQYAAZUJAAGVDAABlQ4AAZUzAAGVVwABlX4AAZWiAAGVpQABlacAAZWpAAGVqwABla0AAZWvAAGVsAABlbMAAZXAAAGV0QABldMAAZXVAAGV1wABldkAAZXbAAGV3QABld8AAZXhAAGV8gABlfUAAZX4AAGV+wABlf4AAZYBAAGWBAABlgcAAZYKAAGWDAABlksAAZZNAAGWTwABllEAAZZUAAGWVQABllYAAZZXAAGWWAABlloAAZZcAAGWXQABll4AAZZgAAGWYQABlqAAAZaiAAGWpAABlqYAAZapAAGWqgABlqsAAZasAAGWrQABlq8AAZaxAAGWsgABlrMAAZa1AAGWtgABlvUAAZb3AAGW+gABlvwAAZb/AAGXAAABlwEAAZcCAAGXAwABlwUAAZcHAAGXCAABlwkAAZcLAAGXDAABlxkAAZcaAAGXGwABlx0AAZdcAAGXXgABl2AAAZdiAAGXZQABl2YAAZdnAAGXaAABl2kAAZdrAAGXbQABl24AAZdvAAGXcQABl3IAAZexAAGXswABl7UAAZe3AAGXugABl7sAAZe8AAGXvQABl74AAZfAAAGXwgABl8MAAZfEAAGXxgABl8cAAZgGAAGYCAABmAoAAZgMAAGYDwABmBAAAZgRAAGYEgABmBMAAZgVAAGYFwABmBgAAZgZAAGYGwABmBwAAZhbAAGYXQABmF8AAZhhAAGYZAABmGUAAZhmAAGYZwABmGgAAZhqAAGYbAABmG0AAZhuAAGYcAABmHEAAZiwAAGYsgABmLQAAZi2AAGYuQABmLoAAZi7AAGYvAABmL0AAZi/AAGYwQABmMIAAZjDAAGYxQABmMYAAZjrAAGZDwABmTYAAZlaAAGZXQABmV8AAZlhAAGZYwABmWUAAZlnAAGZaAABmWsAAZl4AAGZhwABmYkAAZmLAAGZjQABmY8AAZmRAAGZkwABmZUAAZmkAAGZpwABmaoAAZmtAAGZsAABmbMAAZm2AAGZuQABmbsAAZn6AAGZ/AABmf4AAZoAAAGaAwABmgQAAZoFAAGaBgABmgcAAZoJAAGaCwABmgwAAZoNAAGaDwABmhAAAZpPAAGaUQABmlMAAZpVAAGaWAABmlkAAZpaAAGaWwABmlwAAZpeAAGaYAABmmEAAZpiAAGaZAABmmUAAZqkAAGapgABmqgAAZqqAAGarQABmq4AAZqvAAGasAABmrEAAZqzAAGatQABmrYAAZq3AAGauQABmroAAZr5AAGa+wABmv0AAZr/AAGbAgABmwMAAZsEAAGbBQABmwYAAZsIAAGbCgABmwsAAZsMAAGbDgABmw8AAZtOAAGbUAABm1IAAZtUAAGbVwABm1gAAZtZAAGbWgABm1sAAZtdAAGbXwABm2AAAZthAAGbYwABm2QAAZujAAGbpQABm6cAAZupAAGbrAABm60AAZuuAAGbrwABm7AAAZuyAAGbtAABm7UAAZu2AAGbuAABm7kAAZv4AAGb+gABm/wAAZv+AAGcAQABnAIAAZwDAAGcBAABnAUAAZwHAAGcCQABnAoAAZwLAAGcDQABnA4AAZxZAAGcfAABnJwAAZy8AAGcvgABnMAAAZzCAAGcxAABnMYAAZzHAAGcyAABnMsAAZzMAAGczgABnM8AAZzRAAGc0wABnNQAAZzVAAGc2AABnNkAAZziAAGc7wABnPQAAZz2AAGc+AABnP0AAZ0AAAGdAwABnQUAAZ0qAAGdTgABnXUAAZ2ZAAGdnAABnZ4AAZ2gAAGdogABnaQAAZ2mAAGdpwABnaoAAZ23AAGdyAABncoAAZ3MAAGdzgABndAAAZ3SAAGd1AABndYAAZ3YAAGd6QABnewAAZ3vAAGd8gABnfUAAZ34AAGd+wABnf4AAZ4BAAGeAwABnkIAAZ5EAAGeRgABnkgAAZ5LAAGeTAABnk0AAZ5OAAGeTwABnlEAAZ5TAAGeVAABnlUAAZ5XAAGeWAABnpcAAZ6ZAAGemwABnp0AAZ6gAAGeoQABnqIAAZ6jAAGepAABnqYAAZ6oAAGeqQABnqoAAZ6sAAGerQABnuwAAZ7uAAGe8QABnvMAAZ72AAGe9wABnvgAAZ75AAGe+gABnvwAAZ7+AAGe/wABnwAAAZ8CAAGfAwABnxAAAZ8RAAGfEgABnxQAAZ9TAAGfVQABn1cAAZ9ZAAGfXAABn10AAZ9eAAGfXwABn2AAAZ9iAAGfZAABn2UAAZ9mAAGfaAABn2kAAZ+oAAGfqgABn6wAAZ+uAAGfsQABn7IAAZ+zAAGftAABn7UAAZ+3AAGfuQABn7oAAZ+7AAGfvQABn74AAZ/9AAGf/wABoAEAAaADAAGgBgABoAcAAaAIAAGgCQABoAoAAaAMAAGgDgABoA8AAaAQAAGgEgABoBMAAaBSAAGgVAABoFYAAaBYAAGgWwABoFwAAaBdAAGgXgABoF8AAaBhAAGgYwABoGQAAaBlAAGgZwABoGgAAaCnAAGgqQABoKsAAaCtAAGgsAABoLEAAaCyAAGgswABoLQAAaC2AAGguAABoLkAAaC6AAGgvAABoL0AAaDiAAGhBgABoS0AAaFRAAGhVAABoVYAAaFYAAGhWgABoVwAAaFeAAGhXwABoWIAAaFvAAGhfgABoYAAAaGCAAGhhAABoYYAAaGIAAGhigABoYwAAaGbAAGhngABoaEAAaGkAAGhpwABoaoAAaGtAAGhsAABobIAAaHxAAGh8wABofUAAaH3AAGh+gABofsAAaH8AAGh/QABof4AAaIAAAGiAgABogMAAaIEAAGiBgABogcAAaJGAAGiSAABokoAAaJMAAGiTwABolAAAaJRAAGiUgABolMAAaJVAAGiVwABolgAAaJZAAGiWwABolwAAaKbAAGinQABop8AAaKhAAGipAABoqUAAaKmAAGipwABoqgAAaKqAAGirAABoq0AAaKuAAGisAABorEAAaLwAAGi8gABovQAAaL2AAGi+QABovoAAaL7AAGi/AABov0AAaL/AAGjAQABowIAAaMDAAGjBQABowYAAaNFAAGjRwABo0kAAaNLAAGjTgABo08AAaNQAAGjUQABo1IAAaNUAAGjVgABo1cAAaNYAAGjWgABo1sAAaOaAAGjnAABo54AAaOgAAGjowABo6QAAaOlAAGjpgABo6cAAaOpAAGjqwABo6wAAaOtAAGjrwABo7AAAaPvAAGj8QABo/MAAaP1AAGj+AABo/kAAaP6AAGj+wABo/wAAaP+AAGkAAABpAEAAaQCAAGkBAABpAUAAaRQAAGkcwABpJMAAaSzAAGktQABpLcAAaS5AAGkuwABpL0AAaS+AAGkvwABpMIAAaTDAAGkxQABpMYAAaTIAAGkygABpMsAAaTMAAGkzwABpNAAAaTVAAGk4gABpOcAAaTpAAGk6wABpPAAAaTzAAGk9gABpPgAAaUdAAGlQQABpWgAAaWMAAGljwABpZEAAaWTAAGllQABpZcAAaWZAAGlmgABpZ0AAaWqAAGluwABpb0AAaW/AAGlwQABpcMAAaXFAAGlxwABpckAAaXLAAGl3AABpd8AAaXiAAGl5QABpegAAaXrAAGl7gABpfEAAaX0AAGl9gABpjUAAaY3AAGmOQABpjsAAaY+AAGmPwABpkAAAaZBAAGmQgABpkQAAaZGAAGmRwABpkgAAaZKAAGmSwABpooAAaaMAAGmjgABppAAAaaTAAGmlAABppUAAaaWAAGmlwABppkAAaabAAGmnAABpp0AAaafAAGmoAABpt8AAabhAAGm5AABpuYAAabpAAGm6gABpusAAabsAAGm7QABpu8AAabxAAGm8gABpvMAAab1AAGm9gABpwMAAacEAAGnBQABpwcAAadGAAGnSAABp0oAAadMAAGnTwABp1AAAadRAAGnUgABp1MAAadVAAGnVwABp1gAAadZAAGnWwABp1wAAaebAAGnnQABp58AAaehAAGnpAABp6UAAaemAAGnpwABp6gAAaeqAAGnrAABp60AAaeuAAGnsAABp7EAAafwAAGn8gABp/QAAaf2AAGn+QABp/oAAaf7AAGn/AABp/0AAaf/AAGoAQABqAIAAagDAAGoBQABqAYAAahFAAGoRwABqEkAAahLAAGoTgABqE8AAahQAAGoUQABqFIAAahUAAGoVgABqFcAAahYAAGoWgABqFsAAaiaAAGonAABqJ4AAaigAAGoowABqKQAAailAAGopgABqKcAAaipAAGoqwABqKwAAaitAAGorwABqLAAAajVAAGo+QABqSAAAalEAAGpRwABqUkAAalLAAGpTQABqU8AAalRAAGpUgABqVUAAaliAAGpcQABqXMAAal1AAGpdwABqXkAAal7AAGpfQABqX8AAamOAAGpkQABqZQAAamXAAGpmgABqZ0AAamgAAGpowABqaUAAankAAGp5gABqegAAanqAAGp7QABqe4AAanvAAGp8AABqfEAAanzAAGp9QABqfYAAan3AAGp+QABqfoAAao5AAGqOwABqj0AAao/AAGqQgABqkMAAapEAAGqRQABqkYAAapIAAGqSgABqksAAapMAAGqTgABqk8AAaqOAAGqkAABqpIAAaqUAAGqlwABqpgAAaqZAAGqmgABqpsAAaqdAAGqnwABqqAAAaqhAAGqowABqqQAAarjAAGq5QABqugAAarqAAGq7QABqu4AAarvAAGq8AABqvEAAarzAAGq9QABqvYAAar3AAGq+QABqvoAAas9AAGrYQABq4UAAauoAAGrzwABq+8AAawWAAGsPQABrF0AAayBAAGspQABrKcAAayqAAGsrAABrK4AAaywAAGsswABrLYAAay4AAGsugABrL0AAay/AAGswQABrMQAAazHAAGsyAABrM0AAazaAAGs3QABrN8AAaziAAGs5QABrOcAAa0MAAGtMAABrVcAAa17AAGtfgABrYAAAa2CAAGthAABrYYAAa2IAAGtiQABrYwAAa2ZAAGtrAABra4AAa2wAAGtsgABrbQAAa22AAGtuAABrboAAa28AAGtvgABrdEAAa3UAAGt1wABrdoAAa3dAAGt4AABreMAAa3mAAGt6QABrewAAa3uAAGuLQABri8AAa4yAAGuNAABrjcAAa44AAGuOQABrjoAAa47AAGuPQABrj8AAa5AAAGuQQABrkMAAa5EAAGuTQABrk4AAa5QAAGujwABrpEAAa6TAAGulQABrpgAAa6ZAAGumgABrpsAAa6cAAGungABrqAAAa6hAAGuogABrqQAAa6lAAGu5AABruYAAa7pAAGu6wABru4AAa7vAAGu8AABrvEAAa7yAAGu9AABrvYAAa73AAGu+AABrvoAAa77AAGvBAABrwUAAa8HAAGvRgABr0gAAa9KAAGvTAABr08AAa9QAAGvUQABr1IAAa9TAAGvVQABr1cAAa9YAAGvWQABr1sAAa9cAAGvmwABr50AAa+gAAGvogABr6UAAa+mAAGvpwABr6gAAa+pAAGvqwABr60AAa+uAAGvrwABr7EAAa+yAAGvuwABr7wAAa++AAGv/QABr/8AAbABAAGwAwABsAYAAbAHAAGwCAABsAkAAbAKAAGwDAABsA4AAbAPAAGwEAABsBIAAbATAAGwUgABsFQAAbBXAAGwWQABsFwAAbBdAAGwXgABsF8AAbBgAAGwYgABsGQAAbBlAAGwZgABsGgAAbBpAAGwdgABsHcAAbB4AAGwegABsLkAAbC7AAGwvQABsL8AAbD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TAAHHVgABx1gAAceXAAHHmQABx5sAAcedAAHHoAABx6EAAceiAAHHowABx6QAAcemAAHHqAABx6kAAceqAAHHrAABx60AAcfsAAHH7gABx/AAAcfyAAHH9QABx/YAAcf3AAHH+AABx/kAAcf7AAHH/QABx/4AAcf/AAHIAQAByAIAAchBAAHIQwAByEUAAchHAAHISgAByEsAAchMAAHITQAByE4AAchQAAHIUgAByFMAAchUAAHIVgAByFcAAciWAAHImAAByJsAAcidAAHIoAAByKEAAciiAAHIowAByKQAAcimAAHIqAAByKkAAciqAAHIrAAByK0AAcjsAAHI7gAByPAAAcjyAAHI9QAByPYAAcj3AAHI+AAByPkAAcj7AAHI/QAByP4AAcj/AAHJAQAByQIAAclBAAHJQwAByUYAAclIAAHJSwAByUwAAclNAAHJTgAByU8AAclRAAHJUwAByVQAAclVAAHJVwAByVgAAcl/AAHJvgABycAAAcnCAAHJxAAByccAAcnIAAHJyQABycoAAcnLAAHJzQAByc8AAcnQAAHJ0QABydMAAcnUAAHKHwABykIAAcpiAAHKggAByoQAAcqGAAHKiAAByooAAcqNAAHKjgAByo8AAcqSAAHKkwABypUAAcqWAAHKmAABypsAAcqcAAHKnQAByqAAAcqhAAHKpgAByrMAAcq4AAHKugAByrwAAcrBAAHKxAAByscAAcrJAAHK7gAByxIAAcs5AAHLXQABy2AAActiAAHLZAABy2YAActoAAHLagABy2sAActuAAHLewABy4wAAcuOAAHLkAABy5IAAcuUAAHLlgABy5gAAcuaAAHLnAABy60AAcuwAAHLswABy7YAAcu5AAHLvAABy78AAcvCAAHLxQABy8cAAcwGAAHMCAABzAoAAcwMAAHMDwABzBAAAcwRAAHMEgABzBMAAcwVAAHMFwABzBgAAcwZAAHMGwABzBwAAcxbAAHMXQABzF8AAcxhAAHMZAABzGUAAcxmAAHMZwABzGgAAcxqAAHMbAABzG0AAcxuAAHMcAABzHEAAcywAAHMsgABzLUAAcy3AAHMugABzLsAAcy8AAHMvQABzL4AAczAAAHMwgABzMMAAczEAAHMxgABzMcAAczUAAHM1QABzNYAAczYAAHNFwABzRkAAc0bAAHNHQABzSAAAc0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VAAHYVAAB2FYAAdhYAAHYWgAB2F0AAdheAAHYXwAB2GAAAdhhAAHYYwAB2GUAAdhmAAHYZwAB2GkAAdhqAAHYqQAB2KsAAdiuAAHYsAAB2LMAAdi0AAHYtQAB2LYAAdi3AAHYuQAB2LsAAdi8AAHYvQAB2L8AAdjAAAHZAwAB2ScAAdlLAAHZbgAB2ZUAAdm1AAHZ3AAB2gMAAdojAAHaRwAB2msAAdptAAHacAAB2nIAAdp0AAHadgAB2nkAAdp8AAHafgAB2oAAAdqDAAHahQAB2ocAAdqKAAHajQAB2o4AAdqTAAHaoAAB2qMAAdqlAAHaqAAB2qsAAdqtAAHa0gAB2vYAAdsdAAHbQQAB20QAAdtGAAHbSAAB20oAAdtMAAHbTgAB208AAdtSAAHbXwAB23IAAdt0AAHbdgAB23gAAdt6AAHbfAAB234AAduAAAHbggAB24QAAduXAAHbmgAB250AAdugAAHbowAB26YAAdupAAHbrAAB268AAduyAAHbtAAB2/MAAdv1AAHb+AAB2/oAAdv9AAHb/gAB2/8AAdwAAAHcAQAB3AMAAdwFAAHcBgAB3AcAAdwJAAHcCgAB3BMAAdwUAAHcFgAB3FUAAdxXAAHcWQAB3FsAAdxeAAHcXwAB3GAAAdxhAAHcYgAB3GQAAdxmAAHcZwAB3GgAAdxqAAHcawAB3KoAAdysAAHcrwAB3LEAAdy0AAHctQAB3LYAAdy3AAHcuAAB3LoAAdy8AAHcvQAB3L4AAdzAAAHcwQAB3MoAAdzLAAHczQAB3QwAAd0OAAHdEAAB3RIAAd0VAAHdFgAB3RcAAd0YAAHdGQAB3RsAAd0dAAHdHgAB3R8AAd0hAAHdIgAB3WEAAd1jAAHdZgAB3WgAAd1rAAHdbAAB3W0AAd1uAAHdbwAB3XEAAd1zAAHddAAB3XUAAd13AAHdeAAB3YEAAd2CAAHdhAAB3cMAAd3FAAHdxwAB3ckAAd3MAAHdzQAB3c4AAd3PAAHd0AAB3dIAAd3UAAHd1QAB3dYAAd3YAAHd2QAB3hgAAd4aAAHeHQAB3h8AAd4iAAHeIwAB3iQAAd4lAAHeJgAB3igAAd4qAAHeKwAB3iwAAd4uAAHeLwAB3jwAAd49AAHePgAB3kAAAd5/AAHegQAB3oMAAd6FAAHeiAAB3okAAd6KAAHeiwAB3owAAd6OAAHekAAB3pEAAd6SAAHelAAB3pUAAd7UAAHe1gAB3tkAAd7bAAHe3gAB3t8AAd7gAAHe4QAB3uIAAd7kAAHe5gAB3ucAAd7oAAHe6gAB3usAAd8HAAHfFAAB3xsAAd8dAAHfHwAB3yIAAd8pAAHfLAAB3y8AAd8yAAHfNAAB3zsAAd+GAAHfqQAB38kAAd/pAAHf6wAB3+0AAd/vAAHf8QAB3/QAAd/1AAHf9gAB3/kAAd/6AAHf/AAB3/0AAd//AAHgAQAB4AIAAeADAAHgBgAB4AcAAeAMAAHgGQAB4B4AAeAgAAHgIgAB4CcAAeAqAAHgLQAB4C8AAeBUAAHgeAAB4J8AAeDDAAHgxgAB4MgAAeDKAAHgzAAB4M4AAeDQAAHg0QAB4NQAAeDhAAHg8gAB4PQAAeD2AAHg+AAB4PoAAeD8AAHg/gAB4QAAAeECAAHhEwAB4RYAAeEZAAHhHAAB4R8AAeEiAAHhJQAB4SgAAeErAAHhLQAB4WwAAeFuAAHhcAAB4XIAAeF1AAHhdgAB4XcAAeF4AAHheQAB4XsAAeF9AAHhfgAB4X8AAeGBAAHhggAB4cEAAeHDAAHhxQAB4ccAAeHKAAHhywAB4cwAAeHNAAHhzgAB4dAAAeHSAAHh0wAB4dQAAeHWAAHh1wAB4hYAAeIYAAHiGwAB4h0AAeIgAAHiIQAB4iIAAeIjAAHiJAAB4iYAAeIoAAHiKQAB4ioAAeIsAAHiLQAB4joAAeI7AAHiPAAB4j4AAeJ9AAHifwAB4oEAAeKDAAHihgAB4ocAAeKIAAHiiQAB4ooAAeKMAAHijgAB4o8AAeKQAAHikgAB4pMAAeLSAAHi1AAB4tYAAeLYAAHi2wAB4twAAeLdAAHi3gAB4t8AAeLhAAHi4wAB4uQAAeLlAAHi5wAB4ugAAeMnAAHjKQAB4ysAAeMtAAHjMAAB4zEAAeMyAAHjMwAB4zQAAeM2AAHjOAAB4zkAAeM6AAHjPAAB4z0AAeN8AAHjfgAB44AAAeOCAAHjhQAB44YAAeOHAAHjiAAB44kAAeOLAAHjjQAB444AAeOPAAHjkQAB45IAAePRAAHj0wAB49UAAePXAAHj2gAB49sAAePcAAHj3QAB494AAePgAAHj4gAB4+MAAePkAAHj5gAB4+cAAeQMAAHkMAAB5FcAAeR7AAHkfgAB5IAAAeS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MAAHvlwAB77oAAe/aAAHv+gAB7/wAAe/+AAHwAAAB8AIAAfAFAAHwBgAB8AcAAfAKAAHwCwAB8A0AAfAOAAHwEAAB8BMAAfAUAAHwFQAB8BgAAfAZAAHwHgAB8CsAAfAwAAHwMgAB8DQAAfA5AAHwPAAB8D8AAfBBAAHwZgAB8IoAAfCxAAHw1QAB8NgAAfDaAAHw3AAB8N4AAfDgAAHw4gAB8OMAAfDmAAHw8wAB8QQAAfEGAAHxCAAB8QoAAfEMAAHxDgAB8RAAAfESAAHxFAAB8SUAAfEoAAHxKwAB8S4AAfExAAHxNAAB8TcAAfE6AAHxPQAB8T8AAfF+AAHxgAAB8YIAAfGEAAHxhwAB8YgAAfGJAAHxigAB8YsAAfGNAAHxjwAB8ZAAAfGRAAHxkwAB8ZQAAfHTAAHx1QAB8dcAAfHZAAHx3AAB8d0AAfHeAAHx3wAB8eAAAfHiAAHx5AAB8eUAAfHmAAHx6AAB8ekAAfIoAAHyKgAB8i0AAfIvAAHyMgAB8jMAAfI0AAHyNQAB8jYAAfI4AAHyOgAB8jsAAfI8AAHyPgAB8j8AAfJMAAHyTQAB8k4AAfJQAAHyjwAB8pEAAfKTAAHylQAB8pgAAfKZAAHymgAB8psAAfKcAAHyngAB8qAAAfKhAAHyogAB8qQAAfKlAAHy5AAB8uYAAfLoAAHy6gAB8u0AAfLuAAHy7wAB8vAAAfLxAAHy8wAB8vUAAfL2AAHy9wAB8vkAAfL6AAHzOQAB8zsAAfM9AAHzPwAB80IAAfNDAAHzRAAB80UAAfNGAAHzSAAB80oAAfNLAAHzTAAB804AAfNPAAHzjgAB85AAAfOSAAHzlAAB85cAAfOYAAHzmQAB85oAAfObAAHznQAB858AAfOgAAHzoQAB86MAAfOkAAHz4wAB8+UAAfPnAAHz6QAB8+wAAfPtAAHz7gAB8+8AAfPwAAHz8gAB8/QAAfP1AAHz9gAB8/gAAfP5AAH0HgAB9EIAAfRpAAH0jQAB9JAAAfSSAAH0lAAB9JYAAfSYAAH0mgAB9JsAAfSeAAH0qwAB9LoAAfS8AAH0vgAB9MAAAfTCAAH0xAAB9MYAAfTIAAH01wAB9NoAAfTdAAH04AAB9OMAAfTmAAH06QAB9OwAAfTuAAH1LQAB9S8AAfUxAAH1MwAB9TYAAfU3AAH1OAAB9TkAAfU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5AAILfAACC30AAgt+AAILfwACC4AAAguCAAILhAACC4UAAguGAAILiAACC4kAAgvIAAILygACC8wAAgvOAAIL0QACC9IAAgvTAAIL1AACC9UAAgvXAAIL2QACC9oAAgvbAAIL3QACC94AAgwdAAIMHwACDCIAAgwkAAIMJwACDCgAAgwpAAIMKgACDCsAAgwtAAIMLwACDDAAAgwxAAIMMwACDDQAAgxBAAIMQgACDEMAAgxFAAIMhAACDIYAAgyIAAIMigACDI0AAgyOAAIMjwACDJAAAgyRAAIMkwACDJUAAgyWAAIMlwACDJkAAgyaAAIM2QACDNsAAgzdAAIM3wACDOIAAgzjAAIM5AACDOUAAgzmAAIM6AACDOoAAgzrAAIM7AACDO4AAgzvAAINLgACDTAAAg0yAAINNAACDTcAAg04AAINOQACDToAAg07AAINPQACDT8AAg1AAAINQQACDUMAAg1EAAINgwACDYUAAg2HAAINiQACDYwAAg2NAAINjgACDY8AAg2QAAINkgACDZQAAg2VAAINlgACDZgAAg2ZAAIN2AACDdoAAg3cAAIN3gACDeEAAg3iAAIN4wACDeQAAg3lAAIN5wACDekAAg3qAAIN6wACDe0AAg3uAAIOEwACDjcAAg5eAAIOggACDoUAAg6HAAIOiQACDosAAg6NAAIOjwACDpAAAg6TAAIOoAACDq0AAg6vAAIOsQACDrMAAg61AAIOtwACDrkAAg7GAAIOyQACDswAAg7PAAIO0gACDtUAAg7YAAIO2gACDxkAAg8bAAIPHgACDyAAAg8jAAIPJAACDyUAAg8mAAIPJwACDykAAg8rAAIPLAACDy0AAg8vAAIPMAACD28AAg9xAAIPcwACD3UAAg94AAIPeQACD3oAAg97AAIPfAACD34AAg+AAAIPgQACD4IAAg+EAAIPhQACD8QAAg/GAAIPyAACD8oAAg/NAAIPzgACD88AAg/QAAIP0QACD9MAAg/VAAIP1gACD9cAAg/ZAAIP2gACEBkAAhAbAAIQHgACECAAAhAjAAIQJAACECUAAhAmAAIQJwACECkAAhArAAIQLAACEC0AAhAvAAIQMAACEG8AAhBxAAIQcwACEHUAAhB4AAIQeQACEHoAAhB7AAIQfAACEH4AAhCAAAIQgQACEIIAAhCEAAIQhQACEMQAAhDGAAIQyAACEMoAAhDNAAIQzgACEM8AAhDQAAIQ0QACENMAAhDVAAIQ1gACENcAAhDZAAIQ2gACEOMAAhDkAAIQ5gACESUAAhEnAAIRKQACESsAAhEtAAIRLgACES8AAhEwAAIRMQACETMAAhE1AAIRNgACETcAAhE5AAIROgACEXkAAhF7AAIRfQACEX8AAhGBAAIRggACEYMAAhGEAAIRhQACEYcAAhGJAAIRigACEYsAAhGNAAIRjgACEc0AAhHPAAIR0QACEdMAAhHVAAIR1gACEdcAAhHYAAIR2QACEdsAAhHdAAIR3gACEd8AAhHhAAIR4gACEi0AAhJQAAIScAACEpAAAhKSAAISlAACEpYAAhKYAAISmgACEpsAAhKcAAISnwACEqAAAhKiAAISowACEqUAAhKnAAISqAACEqkAAhKsAAISrQACErIAAhK/AAISxAACEsYAAhLIAAISzQACEtAAAhLTAAIS1QACEvoAAhMeAAITRQACE2kAAhNsAAITbgACE3AAAhNyAAITdAACE3YAAhN3AAITegACE4cAAhOYAAITmgACE5wAAhOeAAIToAACE6IAAhOkAAITpgACE6gAAhO5AAITvAACE78AAhPCAAITxQACE8gAAhPLAAITzgACE9EAAhPTAAIUEgACFBQAAhQWAAIUGAACFBsAAhQcAAIUHQACFB4AAhQfAAIUIQACFCMAAhQkAAIUJQACFCcAAhQoAAIUZwACFGkAAhRrAAIUbQACFHAAAhRxAAIUcgACFHMAAhR0AAIUdgACFHgAAhR5AAIUegACFHwAAhR9AAIUvAACFL4AAhTBAAIUwwACFMYAAhTHAAIUyAACFMkAAhTKAAIUzAACFM4AAhTPAAIU0AACFNIAAhTTAAIU4AACFOEAAhTiAAIU5AACFSMAAhUlAAIVJwACFSkAAhUsAAIVLQACFS4AAhUvAAIVMAACFTIAAhU0AAIVNQACFTYAAhU4AAIVOQACFXgAAhV6AAIVfAACFX4AAhWBAAIVggACFYMAAhWEAAIVhQACFYcAAhWJAAIVigACFYsAAhWNAAIVjgACFc0AAhXPAAIV0QACFdMAAhXWAAIV1wACFdgAAhXZAAIV2gACFdwAAhXeAAIV3wACFeAAAhXiAAIV4wACFiIAAhYkAAIWJgACFigAAhYrAAIWLAACFi0AAhYuAAIWLwACFjEAAhYzAAIWNAACFjUAAhY3AAIWOAACFncAAhZ5AAIWewACFn0AAhaAAAIWgQACFoIAAhaDAAIWhAACFoYAAhaIAAIWiQACFooAAhaMAAIWjQACFrIAAhbWAAIW/QACFyEAAhckAAIXJgACFygAAhcqAAIXLAACFy4AAhcvAAIXMgACFz8AAhdOAAIXUAACF1IAAhdUAAIXVgACF1gAAhdaAAIXXAACF2sAAhduAAIXcQACF3QAAhd3AAIXegACF30AAheAAAIXggACF8EAAhfDAAIXxQACF8cAAhfKAAIXywACF8wAAhfNAAIXzgACF9AAAhfSAAIX0wACF9QAAhfWAAIX1wACGBYAAhgYAAIYGgACGBwAAhgfAAIYIAACGCEAAhgiAAIYIwACGCUAAhgnAAIYKAACGCkAAhgrAAIYLAACGGsAAhhtAAIYbwACGHEAAhh0AAIYdQACGHYAAhh3AAIYeAACGHoAAhh8AAIYfQACGH4AAhiAAAIYgQACGMAAAhjCAAIYxQACGMcAAhjKAAIYywACGMwAAhjNAAIYzgACGNAAAhjSAAIY0wACGNQAAhjWAAIY1wACGRYAAhkYAAIZGgACGRwAAhkfAAIZIAACGSEAAhkiAAIZIwACGSUAAhknAAIZKAACGSkAAhkrAAIZLAACGWsAAhltAAIZcAACGXIAAhl1AAIZdgACGXcAAhl4AAIZeQACGXsAAhl9AAIZfgACGX8AAhmBAAIZggACGZ4AAhndAAIZ3wACGeEAAhnjAAIZ5gACGecAAhnoAAIZ6QACGeoAAhnsAAIZ7gACGe8AAhnwAAIZ8gACGfMAAho+AAIaYQACGoEAAhqhAAIaowACGqUAAhqnAAIaqQACGqsAAhqsAAIarQACGrAAAhqxAAIaswACGrQAAhq2AAIauAACGrkAAhq6AAIavQACGr4AAhrDAAIa0AACGtUAAhrXAAIa2QACGt4AAhrhAAIa5AACGuYAAhsLAAIbLwACG1YAAht6AAIbfQACG38AAhuBAAIbgwACG4UAAhuHAAIbiAACG4sAAhuYAAIbqQACG6sAAhutAAIbrwACG7EAAhuzAAIbtQACG7cAAhu5AAIbygACG80AAhvQAAIb0wACG9YAAhvZAAIb3AACG98AAhviAAIb5AACHCMAAhwlAAIcJwACHCkAAhwsAAIcLQACHC4AAhwvAAIcMAACHDIAAhw0AAIcNQACHDYAAhw4AAIcOQACHHgAAhx6AAIcfAACHH4AAhyBAAIcggACHIMAAhyEAAIchQACHIcAAhyJAAIcigACHIsAAhyNAAIcjgACHM0AAhzPAAIc0gACHNQAAhzXAAIc2AACHNkAAhzaAAIc2wACHN0AAhzfAAIc4AACHOEAAhzjAAIc5AACHPEAAhzyAAIc8wACHPUAAh00AAIdNgACHTgAAh06AAIdPQACHT4AAh0/AAIdQAACHUEAAh1DAAIdRQACHUYAAh1HAAIdSQACHUoAAh2JAAIdiwACHY0AAh2PAAIdkgACHZMAAh2UAAIdlQACHZYAAh2YAAIdmgACHZsAAh2cAAIdngACHZ8AAh3eAAId4AACHeIAAh3kAAId5wACHegAAh3pAAId6gACHesAAh3tAAId7wACHfAAAh3xAAId8wACHfQAAh4zAAIeNQACHjcAAh45AAIePAACHj0AAh4+AAIePwACHkAAAh5CAAIeRAACHkUAAh5GAAIeSAACHkkAAh6IAAIeigACHowAAh6OAAIekQACHpIAAh6TAAIelAACHpUAAh6XAAIemQACHpoAAh6bAAIenQACHp4AAh7DAAIe5wACHw4AAh8yAAIfNQACHzcAAh85AAIfOwACHz0AAh8/AAIfQAACH0MAAh9QAAIfXwACH2EAAh9jAAIfZQACH2cAAh9pAAIfawACH20AAh98AAIffwACH4IAAh+FAAIfiAACH4sAAh+OAAIfkQACH5MAAh/SAAIf1AACH9YAAh/YAAIf2wACH9wAAh/dAAIf3gACH98AAh/hAAIf4wACH+QAAh/lAAIf5wACH+gAAiAnAAIgKQACICsAAiAtAAIgMAACIDEAAiAyAAIgMwACIDQAAiA2AAIgOAACIDkAAiA6AAIgPAACID0AAiB8AAIgfgACIIAAAiCCAAIghQACIIYAAiCHAAIgiAACIIkAAiCLAAIgjQACII4AAiCPAAIgkQACIJIAAiDRAAIg0wACINUAAiDXAAIg2gACINsAAiDcAAIg3QACIN4AAiDgAAIg4gACIOMAAiDkAAIg5gACIOcAAiEmAAIhKAACISoAAiEsAAIhLwACITAAAiExAAIhMgACITMAAiE1AAIhNwACITgAAiE5AAIhOwACITwAAiF7AAIhfQACIX8AAiGBAAIhhAACIYUAAiGGAAIhhwACIYgAAiGKAAIhjAACIY0AAiGOAAIhkAACIZEAAiHQAAIh0gACIdQAAiHWAAIh2QACIdoAAiHbAAIh3AACId0AAiHfAAIh4QACIeIAAiHjAAIh5QACIeYAAiIxAAIiVAACInQAAiKUAAIilgACIpgAAiKaAAIinAACIp4AAiKfAAIioAACIqMAAiKkAAIipgACIqcAAiKpAAIiqwACIqwAAiKtAAIisAACIrEAAiK6AAIixwACIswAAiLOAAIi0AACItUAAiLYAAIi2wACIt0AAiMCAAIjJgACI00AAiNxAAIjdAACI3YAAiN4AAIjegACI3wAAiN+AAIjfwACI4IAAiOPAAIjoAACI6IAAiOkAAIjpgACI6gAAiOqAAIjrAACI64AAiOwAAIjwQACI8QAAiPHAAIjygACI80AAiPQAAIj0wACI9YAAiPZAAIj2wACJBoAAiQcAAIkHgACJCAAAiQjAAIkJAACJCUAAiQmAAIkJwACJCkAAiQrAAIkLAACJC0AAiQvAAIkMAACJG8AAiRxAAIkcwACJHUAAiR4AAIkeQACJHoAAiR7AAIkfAACJH4AAiSAAAIkgQACJIIAAiSEAAIkhQACJMQAAiTGAAIkyQACJMsAAiTOAAIkzwACJNAAAiTRAAIk0gACJNQAAiTWAAIk1wACJNgAAiTaAAIk2wACJOgAAiTpAAIk6gACJOwAAiUrAAIlLQACJS8AAiUxAAIlNAACJTUAAiU2AAIlNwACJTgAAiU6AAIlPAACJT0AAiU+AAIlQAACJUEAAiWAAAIlggACJYQAAiWGAAIliQACJYoAAiWLAAIljAACJY0AAiWPAAIlkQACJZIAAiWTAAIllQACJZYAAiXVAAIl1wACJdkAAiXbAAIl3gACJd8AAiXgAAIl4QACJeIAAiXkAAIl5gACJecAAiXoAAIl6gACJesAAiYqAAImLAACJi4AAiYwAAImMwACJjQAAiY1AAImNgACJjcAAiY5AAImOwACJjwAAiY9AAImPwACJkAAAiZ/AAImgQACJoMAAiaFAAImiAACJokAAiaKAAImiwACJowAAiaOAAImkAACJpEAAiaSAAImlAACJpUAAia6AAIm3gACJwUAAicpAAInLAACJy4AAicwAAInMgACJzQAAic2AAInNwACJzoAAidHAAInVgACJ1gAAidaAAInXAACJ14AAidgAAInYgACJ2QAAidzAAIndgACJ3kAAid8AAInfwACJ4IAAieFAAIniAACJ4oAAifJAAInywACJ80AAifPAAIn0gACJ9MAAifUAAIn1QACJ9YAAifYAAIn2gACJ9sAAifcAAIn3gACJ98AAigeAAIoIAACKCIAAigkAAIoJwACKCgAAigpAAIoKgACKCsAAigtAAIoLwACKDAAAigxAAIoMwACKDQAAihzAAIodQACKHcAAih5AAIofAACKH0AAih+AAIofwACKIAAAiiCAAIohAACKIUAAiiGAAIoiAACKIkAAijIAAIoygACKMwAAijOAAIo0QACKNIAAijTAAIo1AACKNUAAijXAAIo2QACKNoAAijbAAIo3QACKN4AAikdAAIpHwACKSEAAikjAAIpJgACKScAAikoAAIpKQACKSoAAiksAAIpLgACKS8AAikwAAIpMgACKTMAAilyAAIpdAACKXYAAil4AAIpewACKXwAAil9AAIpfgACKX8AAimBAAIpgwACKYQAAimFAAIphwACKYgAAinHAAIpyQACKcsAAinNAAIp0AACKdEAAinSAAIp0wACKdQAAinWAAIp2AACKdkAAinaAAIp3AACKd0AAinmAAIp5wACKekAAiosAAIqUAACKnQAAiqXAAIqvgACKt4AAisFAAIrLAACK0wAAitwAAIrlAACK5YAAiuZAAIrmwACK50AAiufAAIrogACK6UAAiunAAIrqQACK6wAAiuuAAIrsAACK7MAAiu2AAIrtwACK7wAAivJAAIrzAACK84AAivRAAIr1AACK9YAAiv7AAIsHwACLEYAAixqAAIsbQACLG8AAixxAAIscwACLHUAAix3AAIseAACLHsAAiyIAAIsmwACLJ0AAiyfAAIsoQACLKMAAiylAAIspwACLKkAAiyrAAIsrQACLMAAAizDAAIsxgACLMkAAizMAAIszwACLNIAAizVAAIs2AACLNsAAizdAAItHAACLR4AAi0hAAItIwACLSYAAi0nAAItKAACLSkAAi0qAAItLAACLS4AAi0vAAItMAACLTIAAi0zAAItPAACLT0AAi0/AAItfgACLYAAAi2CAAIthAACLYcAAi2IAAItiQACLYoAAi2LAAItjQACLY8AAi2QAAItkQACLZMAAi2UAAIt0wACLdUAAi3YAAIt2gACLd0AAi3eAAIt3wACLeAAAi3hAAIt4wACLeUAAi3mAAIt5wACLekAAi3qAAIt8wACLfQAAi32AAIuNQACLjcAAi45AAIuOwACLj4AAi4/AAIuQAACLkEAAi5CAAIuRAACLkYAAi5HAAIuSAACLkoAAi5LAAIuigACLowAAi6PAAIukQACLpQAAi6VAAIulgACLpcAAi6YAAIumgACLpwAAi6dAAIungACLqAAAi6hAAIuqgACLqsAAi6tAAIu7AACLu4AAi7wAAIu8gACLvUAAi72AAIu9wACLvgAAi75AAIu+wACLv0AAi7+AAIu/wACLwEAAi8CAAIvQQACL0MAAi9GAAIvSAACL0sAAi9MAAIvTQACL04AAi9PAAIvUQACL1MAAi9UAAIvVQACL1cAAi9YAAIvZQACL2YAAi9nAAIvaQACL6gAAi+qAAIvrAACL64AAi+xAAIvsgACL7MAAi+0AAIvtQACL7cAAi+5AAIvugACL7sAAi+9AAIvvgACL/0AAi//AAIwAgACMAQAAjAHAAIwCAACMAkAAjAKAAIwCwACMA0AAjAPAAIwEAACMBEAAjATAAIwFAACMCIAAjAvAAIwNAACMDcAAjA6AAIwPwACMEIAAjBFAAIwRwACME8AAjBaAAIwpQACMMgAAjDoAAIxCAACMQoAAjEMAAIxDgACMRAAAjETAAIxFAACMRUAAjEYAAIxGQACMRsAAjEcAAIxHgACMSEAAjEiAAIxIwACMSYAAjEnAAIxMAACMT0AAjFCAAIxRAACMUYAAjFLAAIxTgACMVEAAjFTAAIxeAACMZwAAjHDAAIx5wACMeoAAjHsAAIx7gACMfAAAjHyAAIx9AACMfUAAjH4AAIyBQACMhYAAjIYAAIyGgACMhwAAjIeAAIyIAACMiIAAjIkAAIyJgACMjcAAjI6AAIyPQACMkAAAjJDAAIyRgACMkkAAjJMAAIyTwACMlEAAjKQAAIykgACMpQAAjKWAAIymQACMpoAAjKbAAIynAACMp0AAjKfAAIyoQACMqIAAjKjAAIypQACMqYAAjLlAAIy5wACMukAAjLrAAIy7gACMu8AAjLwAAIy8QACMvIAAjL0AAIy9gACMvcAAjL4AAIy+gACMvsAAjM6AAIzPAACMz8AAjNBAAIzRAACM0UAAjNGAAIzRwACM0gAAjNKAAIzTAACM00AAjNOAAIzUAACM1EAAjNeAAIzXwACM2AAAjNiAAIzoQACM6MAAjOlAAIzpwACM6oAAjOrAAIzrAACM60AAjOuAAIzsAACM7IAAjOzAAIztAACM7YAAjO3AAIz9gACM/gAAjP6AAIz/AACM/8AAjQAAAI0AQACNAIAAjQDAAI0BQACNAcAAjQIAAI0CQACNAsAAjQMAAI0SwACNE0AAjRPAAI0UQACNFQAAjRVAAI0VgACNFcAAjRYAAI0WgACNFwAAjRdAAI0XgACNGAAAjRhAAI0oAACNKIAAjSkAAI0pgACNKkAAjSqAAI0qwACNKwAAjStAAI0rwACNLEAAjSyAAI0swACNLUAAjS2AAI09QACNPcAAjT5AAI0+wACNP4AAjT/AAI1AAACNQEAAjUCAAI1BAACNQYAAjUHAAI1CAACNQoAAjULAAI1MAACNVQAAjV7AAI1nwACNaIAAjWkAAI1pgACNagAAjWqAAI1rAACNa0AAjWwAAI1vQACNcwAAjXOAAI10AACNdIAAjXUAAI11gACNdgAAjXaAAI16QACNewAAjXvAAI18gACNfUAAjX4AAI1+wACNf4AAjYAAAI2PwACNkEAAjZDAAI2RQACNkgAAjZJAAI2SgACNksAAjZMAAI2TgACNlAAAjZRAAI2UgACNlQAAjZVAAI2lAACNpYAAjaYAAI2mgACNp0AAjaeAAI2nwACNqAAAjahAAI2owACNqUAAjamAAI2pwACNqkAAjaqAAI26QACNusAAjbtAAI27wACNvIAAjbzAAI29AACNvUAAjb2AAI2+AACNvoAAjb7AAI2/AACNv4AAjb/AAI3PgACN0AAAjdDAAI3RQACN0gAAjdJAAI3SgACN0sAAjdMAAI3TgACN1AAAjdRAAI3UgACN1QAAjdVAAI3WAACN5cAAjeZAAI3mwACN50AAjegAAI3oQACN6IAAjejAAI3pAACN6YAAjeoAAI3qQACN6oAAjesAAI3rQACN+wAAjfuAAI38AACN/IAAjf1AAI39gACN/cAAjf4AAI3+QACN/sAAjf9AAI3/gACN/8AAjgBAAI4AgACOEEAAjhDAAI4RQACOEcAAjhKAAI4SwACOEwAAjhNAAI4TgACOFAAAjhSAAI4UwACOFQAAjhWAAI4VwACOKIAAjjFAAI45QACOQUAAjkHAAI5CQACOQsAAjkNAAI5EAACOREAAjkSAAI5FQACORYAAjkYAAI5GQACORsAAjkeAAI5HwACOSAAAjkjAAI5JAACOSkAAjk2AAI5OwACOT0AAjk/AAI5RAACOUcAAjlKAAI5TAACOXEAAjmVAAI5vAACOeAAAjnjAAI55QACOecAAjnpAAI56wACOe0AAjnuAAI58QACOf4AAjoPAAI6EQACOhMAAjoVAAI6FwACOhkAAjobAAI6HQACOh8AAjowAAI6MwACOjYAAjo5AAI6PAACOj8AAjpCAAI6RQACOkgAAjpKAAI6iQACOosAAjqNAAI6jwACOpIAAjqTAAI6lAACOpUAAjqWAAI6mAACOpoAAjqbAAI6nAACOp4AAjqfAAI63gACOuAAAjriAAI65AACOucAAjroAAI66QACOuoAAjrrAAI67QACOu8AAjrwAAI68QACOvMAAjr0AAI7MwACOzUAAjs4AAI7OgACOz0AAjs+AAI7PwACO0AAAjtBAAI7QwACO0UAAjtGAAI7RwACO0kAAjtKAAI7VwACO1gAAjtZAAI7WwACO5oAAjucAAI7ngACO6AAAjujAAI7pAACO6UAAjumAAI7pwACO6kAAjurAAI7rAACO60AAjuvAAI7sAACO+8AAjvxAAI78wACO/UAAjv4AAI7+QACO/oAAjv7AAI7/AACO/4AAjwAAAI8AQACPAIAAjwEAAI8BQACPEQAAjxGAAI8SAACPEoAAjxNAAI8TgACPE8AAjxQAAI8UQACPFMAAjxVAAI8VgACPFcAAjxZAAI8WgACPJkAAjybAAI8nQACPJ8AAjyiAAI8owACPKQAAjylAAI8pgACPKgAAjyqAAI8qwACPKwAAjyuAAI8rwACPO4AAjzwAAI88gACPPQAAjz3AAI8+AACPPkAAjz6AAI8+wACPP0AAjz/AAI9AAACPQEAAj0DAAI9BAACPSkAAj1NAAI9dAACPZgAAj2bAAI9nQACPZ8AAj2hAAI9owACPaUAAj2mAAI9qQACPbYAAj3FAAI9xwACPckAAj3LAAI9zQACPc8AAj3RAAI90wACPeIAAj3lAAI96AACPesAAj3uAAI98QACPfQAAj33AAI9+QACPjgAAj46AAI+PAACPj4AAj5BAAI+QgACPkMAAj5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FAAJVRgACVYUAAlWHAAJViQACVYsAAlWOAAJVjwACVZAAAlWRAAJVkgACVZQAAlWWAAJVlwACVZgAAlWaAAJVmwACVdoAAlXcAAJV3gACVeAAAlXjAAJV5AACVeUAAlXmAAJV5wACVekAAlXrAAJV7AACVe0AAlXvAAJV8AACVi8AAlYxAAJWMwACVjUAAlY4AAJWOQACVjoAAlY7AAJWPAACVj4AAlZAAAJWQQACVkIAAlZEAAJWRQACVoQAAlaGAAJWiAACVooAAlaNAAJWjgACVo8AAlaQAAJWkQACVpMAAlaVAAJWlgACVpcAAlaZAAJWmgACVtkAAlbbAAJW3QACVt8AAlbiAAJW4wACVuQAAlblAAJW5gACVugAAlbqAAJW6wACVuwAAlbuAAJW7wACVzoAAlddAAJXfQACV50AAlefAAJXoQACV6MAAlelAAJXqAACV6kAAleqAAJXrQACV64AAlewAAJXsQACV7MAAle2AAJXtwACV7gAAle7AAJXvAACV8EAAlfOAAJX0wACV9UAAlfXAAJX3AACV98AAlfiAAJX5AACWAkAAlgtAAJYVAACWHgAAlh7AAJYfQACWH8AAliBAAJYgwACWIUAAliGAAJYiQACWJYAAlinAAJYqQACWKsAAlitAAJYrwACWLEAAlizAAJYtQACWLcAAljIAAJYywACWM4AAljRAAJY1AACWNcAAljaAAJY3QACWOAAAljiAAJZIQACWSMAAlklAAJZJwACWSoAAlkrAAJZLAACWS0AAlkuAAJZMAACWTIAAlkzAAJZNAACWTYAAlk3AAJZdgACWXgAAll6AAJZfAACWX8AAlmAAAJZgQACWYIAAlmDAAJZhQACWYcAAlmIAAJZiQACWYsAAlmMAAJZywACWc0AAlnQAAJZ0gACWdUAAlnWAAJZ1wACWdgAAlnZAAJZ2wACWd0AAlneAAJZ3wACWeEAAlniAAJZ7wACWfAAAlnxAAJZ8wACWjIAAlo0AAJaNgACWjgAAlo7AAJaPAACWj0AAlo+AAJaPwACWkEAAlpDAAJaRAACWkUAAlpHAAJaSAACWocAAlqJAAJaiwACWo0AAlqQAAJakQACWpIAAlqTAAJalAACWpYAAlqYAAJamQACWpoAAlqcAAJanQACWtwAAlreAAJa4AACWuIAAlrlAAJa5gACWucAAlroAAJa6QACWusAAlrtAAJa7gACWu8AAlrxAAJa8gACWzEAAlszAAJbNQACWzcAAls6AAJbOwACWzwAAls9AAJbPgACW0AAAltCAAJbQwACW0QAAltGAAJbRwACW4YAAluIAAJbigACW4wAAluPAAJbkAACW5EAAluSAAJbkwACW5UAAluXAAJbmAACW5kAAlubAAJbnAACW8EAAlvlAAJcDAACXDAAAlwzAAJcNQACXDcAAlw5AAJcOwACXD0AAlw+AAJcQQACXE4AAlxdAAJcXwACXGEAAlxjAAJcZQACXGcAAlxpAAJcawACXHoAAlx9AAJcgAACXIMAAlyGAAJciQACXIwAAlyPAAJckQACXNAAAlzSAAJc1AACXNYAAlzZAAJc2gACXNsAAlzcAAJc3QACXN8AAlzhAAJc4gACXOMAAlzlAAJc5gACXSUAAl0nAAJdKQACXSsAAl0uAAJdLwACXTAAAl0xAAJdMgACXTQAAl02AAJdNwACXTgAAl06AAJdOwACXXoAAl18AAJdfgACXYAAAl2DAAJdhAACXYUAAl2GAAJdhwACXYkAAl2LAAJdjAACXY0AAl2PAAJdkAACXc8AAl3RAAJd1AACXdYAAl3ZAAJd2gACXdsAAl3cAAJd3QACXd8AAl3hAAJd4gACXeMAAl3lAAJd5gACXekAAl4oAAJeKgACXiwAAl4uAAJeMQACXjIAAl4zAAJeNAACXjUAAl43AAJeOQACXjoAAl47AAJePQACXj4AAl59AAJefwACXoEAAl6DAAJehgACXocAAl6IAAJeiQACXooAAl6MAAJejgACXo8AAl6QAAJekgACXpMAAl7SAAJe1AACXtYAAl7YAAJe2wACXtwAAl7dAAJe3gACXt8AAl7hAAJe4wACXuQAAl7lAAJe5wACXugAAl8zAAJfVgACX3YAAl+WAAJfmAACX5oAAl+cAAJfngACX6EAAl+iAAJfowACX6YAAl+nAAJfqQACX6oAAl+sAAJfrwACX7AAAl+xAAJftAACX7UAAl+6AAJfxwACX8wAAl/OAAJf0AACX9UAAl/YAAJf2wACX90AAmACAAJgJgACYE0AAmBxAAJgdAACYHYAAmB4AAJgegACYHwAAmB+AAJgfwACYIIAAmCPAAJgoAACYKIAAmCkAAJgpgACYKgAAmCqAAJgrAACYK4AAmCwAAJgwQACYMQAAmDHAAJgygACYM0AAmDQAAJg0wACYNYAAmDZAAJg2wACYRoAAmEcAAJhHgACYSAAAmEjAAJhJAACYSUAAmEmAAJhJwACYSkAAmErAAJhLAACYS0AAmEvAAJhMAACYW8AAmFxAAJhcwACYXUAAmF4AAJheQACYXoAAmF7AAJhfAACYX4AAmGAAAJhgQACYYIAAmGEAAJhhQACYcQAAmHGAAJhyQACYcsAAmHOAAJhzwACYdAAAmHRAAJh0gACYdQAAmHWAAJh1wACYdgAAmHaAAJh2wACYegAAmHpAAJh6gACYewAAmIrAAJiLQACYi8AAmIxAAJiNAACYjUAAmI2AAJiNwACYjgAAmI6AAJiPAACYj0AAmI+AAJiQAACYkEAAmKAAAJiggACYoQAAmKGAAJiiQACYooAAmKLAAJijAACYo0AAmKPAAJikQACYpIAAmKTAAJilQACYpYAAmLVAAJi1wACYtkAAmLbAAJi3gACYt8AAmLgAAJi4QACYuIAAmLkAAJi5gACYucAAmLoAAJi6gACYusAAmMqAAJjLAACYy4AAmMwAAJjMwACYzQAAmM1AAJjNgACYzcAAmM5AAJjOwACYzwAAmM9AAJjPwACY0AAAmN/AAJjgQACY4MAAmOFAAJjiAACY4kAAmOKAAJjiwACY4wAAmOOAAJjkAACY5EAAmOSAAJjlAACY5UAAmO6AAJj3gACZAUAAmQpAAJkLAACZC4AAmQwAAJkMgACZDQAAmQ2AAJkNwACZDoAAmRHAAJkVgACZFgAAmRaAAJkXAACZF4AAmRgAAJkYgACZGQAAmRzAAJkdgACZHkAAmR8AAJkfwACZIIAAmSFAAJkiAACZIoAAmTJAAJkywACZM0AAmTPAAJk0gACZNMAAmTUAAJk1QACZNYAAmTYAAJk2gACZNsAAmTcAAJk3gACZN8AAmUeAAJlIAACZSIAAmUkAAJlJwACZSgAAmUpAAJlKgACZSsAAmUtAAJlLwACZTAAAmUxAAJlMwACZTQAAmVzAAJldQACZXcAAmV5AAJlfAACZX0AAmV+AAJlfwACZYAAAmWCAAJlhAACZYUAAmWGAAJliAACZYkAAmXIAAJlygACZcwAAmXOAAJl0QACZdIAAmXTAAJl1AACZdUAAmXXAAJl2QACZdoAAmXbAAJl3QACZd4AAmYdAAJmHwACZiEAAmYjAAJmJgACZicAAmYoAAJmKQACZioAAmYsAAJmLgACZi8AAmYwAAJmMgACZjMAAmZyAAJmdAACZnYAAmZ4AAJmewACZnwAAmZ9AAJmfgACZn8AAmaBAAJmgwACZoQAAmaFAAJmhwACZogAAmbHAAJmyQACZssAAmbNAAJm0AACZtEAAmbSAAJm0wACZtQAAmbWAAJm2AACZtkAAmbaAAJm3AACZt0AAmcoAAJnSwACZ2sAAmeLAAJnjQACZ48AAmeRAAJnkwACZ5YAAmeXAAJnmAACZ5sAAmecAAJnngACZ58AAmehAAJnpAACZ6UAAmemAAJnqQACZ6oAAmezAAJnwAACZ8UAAmfHAAJnyQACZ84AAmfRAAJn1AACZ9YAAmf7AAJoHwACaEYAAmhqAAJobQACaG8AAmhxAAJocwACaHUAAmh3AAJoeAACaHsAAmiIAAJomQACaJsAAmidAAJonwACaKEAAmijAAJopQACaKcAAmipAAJougACaL0AAmjAAAJowwACaMYAAmjJAAJozAACaM8AAmjSAAJo1AACaRMAAmkVAAJpFwACaRkAAmkcAAJpHQACaR4AAmkfAAJpIAACaSIAAmkkAAJpJQACaSYAAmkoAAJpKQACaWgAAmlqAAJpbAACaW4AAmlxAAJpcgACaXMAAml0AAJpdQACaXcAAml5AAJpegACaXsAAml9AAJpfgACab0AAmm/AAJpwgACacQAAmnHAAJpyAACackAAmnKAAJpywACac0AAmnPAAJp0AACadEAAmnTAAJp1AACaeEAAmniAAJp4wACaeUAAmokAAJqJgACaigAAmoqAAJqLQACai4AAmovAAJqMAACajEAAmozAAJqNQACajYAAmo3AAJqOQACajoAAmp5AAJqewACan0AAmp/AAJqggACaoMAAmqEAAJqhQACaoYAAmqIAAJqigACaosAAmqMAAJqjgACao8AAmrOAAJq0AACatIAAmrUAAJq1wACatgAAmrZAAJq2gACatsAAmrdAAJq3wACauAAAmrhAAJq4wACauQAAmsjAAJrJQACaycAAmspAAJrLAACay0AAmsuAAJrLwACazAAAmsyAAJrNAACazUAAms2AAJrOAACazkAAmt4AAJregACa3wAAmt+AAJrgQACa4IAAmuDAAJrhAACa4UAAmuHAAJriQACa4oAAmuLAAJrjQACa44AAmuzAAJr1wACa/4AAmwiAAJsJQACbCcAAmwpAAJsKwACbC0AAmwvAAJsMAACbDMAAmxAAAJsTwACbFEAAmxTAAJsVQACbFcAAmxZAAJsWwACbF0AAmxsAAJsbwACbHIAAmx1AAJseAACbHsAAmx+AAJsgQACbIMAAmzCAAJsxAACbMYAAmzIAAJsywACbMwAAmzNAAJszgACbM8AAmzRAAJs0wACbNQAAmzVAAJs1wACbNgAAm0XAAJtGQACbRsAAm0dAAJtIAACbSEAAm0iAAJtIwACbSQAAm0mAAJtKAACbSkAAm0qAAJtLAACbS0AAm1sAAJtbgACbXAAAm1yAAJtdQACbXYAAm13AAJteAACbXkAAm17AAJtfQACbX4AAm1/AAJtgQACbYIAAm3BAAJtwwACbcUAAm3HAAJtygACbcsAAm3MAAJtzQACbc4AAm3QAAJt0gACbdMAAm3UAAJt1gACbdcAAm4WAAJuGAACbhoAAm4cAAJuHwACbiAAAm4hAAJuIgACbiMAAm4lAAJuJwACbigAAm4pAAJuKwACbiwAAm5rAAJubQACbm8AAm5xAAJudAACbnUAAm52AAJudwACbngAAm56AAJufAACbn0AAm5+AAJugAACboEAAm7AAAJuwgACbsQAAm7GAAJuyQACbsoAAm7LAAJuzAACbs0AAm7PAAJu0QACbtIAAm7TAAJu1QACbtYAAm7fAAJu4AACbuIAAm8lAAJvSQACb20AAm+QAAJvtwACb9cAAm/+AAJwJQACcEUAAnBpAAJwjQACcI8AAnCSAAJwlAACcJYAAnCYAAJwmwACcJ4AAnCgAAJwogACcKUAAnCnAAJwqQACcKwAAnCvAAJwsAACcLUAAnDCAAJwxQACcMcAAnDKAAJwzQACcM8AAnD0AAJxGAACcT8AAnFjAAJxZgACcWgAAnFqAAJxbAACcW4AAnFwAAJxcQACcXQAAnGBAAJxlAACcZYAAnGYAAJxmgACcZwAAnGeAAJxoAACcaIAAnGkAAJxpgACcbkAAnG8AAJxvwACccIAAnHFAAJxyAACccsAAnHOAAJx0QACcdQAAnHWAAJyFQACchcAAnIaAAJyHAACch8AAnIgAAJyIQACciIAAnIjAAJyJQACcicAAnIoAAJyKQACcisAAnI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8AAKDPgACg0AAAoNCAAKDRAACg0YAAoNIAAKDSgACg1kAAoNcAAKDXwACg2IAAoNlAAKDaAACg2sAAoNuAAKDcAACg68AAoOxAAKDswACg7UAAoO4AAKDuQACg7oAAoO7AAKDvAACg74AAoPAAAKDwQACg8IAAoPEAAKDxQAChAQAAoQGAAKECAAChAoAAoQNAAKEDgAChA8AAoQQAAKEEQAChBMAAoQVAAKEFgAChBcAAoQZAAKEGgAChFkAAoRbAAKEXQAChF8AAoRiAAKEYwAChGQAAoRlAAKEZgAChGgAAoRqAAKEawAChGwAAoRuAAKEbwAChK4AAoSwAAKEswAChLUAAoS4AAKEuQAChLoAAoS7AAKEvAAChL4AAoTAAAKEwQAChMIAAoTEAAKExQAChQQAAoUGAAKFCAAChQoAAoUNAAKFDgAChQ8AAoUQAAKFEQAChRMAAoUVAAKFFgAChRcAAoUZAAKFGgAChVkAAoVbAAKFXgAChWAAAoVjAAKFZAAChWUAAoVmAAKFZwAChWkAAoVrAAKFbAAChW0AAoVvAAKFcAAChZcAAoXWAAKF2AAChdoAAoXcAAKF3wACheAAAoXhAAKF4gACheMAAoXlAAKF5wAChegAAoXpAAKF6wAChewAAoY3AAKGWgAChnoAAoaaAAKGnAAChp4AAoagAAKGogAChqUAAoamAAKGpwAChqoAAoarAAKGrQAChq4AAoawAAKGswAChrQAAoa1AAKGuAAChrkAAoa+AAKGywAChtAAAobSAAKG1AAChtkAAobcAAKG3wAChuEAAocGAAKHKgACh1EAAod1AAKHeAACh3oAAod8AAKHfgACh4AAAoeCAAKHgwACh4YAAoeTAAKHpAACh6YAAoeoAAKHqgACh6wAAoeuAAKHsAACh7IAAoe0AAKHxQACh8gAAofLAAKHzgACh9EAAofUAAKH1wACh9oAAofdAAKH3wACiB4AAoggAAKIIgACiCQAAognAAKIKAACiCkAAogqAAKIKwACiC0AAogvAAKIMAACiDEAAogzAAKINAACiHMAAoh1AAKIdwACiHkAAoh8AAKIfQACiH4AAoh/AAKIgAACiIIAAoiEAAKIhQACiIYAAoiIAAKIiQACiMgAAojKAAKIzQACiM8AAojSAAKI0wACiNQAAojVAAKI1gACiNgAAojaAAKI2wACiNwAAojeAAKI3wACiOwAAojtAAKI7gACiPAAAokvAAKJMQACiTMAAok1AAKJOAACiTkAAok6AAKJOwACiTwAAok+AAKJQAACiUEAAolCAAKJRAACiUUAAomEAAKJhgACiYgAAomKAAKJjQACiY4AAomPAAKJkAACiZEAAomTAAKJlQACiZYAAomXAAKJmQACiZoAAonZAAKJ2wACid0AAonfAAKJ4gACieMAAonkAAKJ5QACieYAAonoAAKJ6gACiesAAonsAAKJ7gACie8AAoouAAKKMAACijIAAoo0AAKKNwACijgAAoo5AAKKOgACijsAAoo9AAKKPwACikAAAopBAAKKQwACikQAAoqDAAKKhQACiocAAoqJAAKKjAACio0AAoqOAAKKjwACipAAAoqSAAKKlAACipUAAoqWAAKKmAACipkAAoq+AAKK4gACiwkAAostAAKLMAACizIAAos0AAKLNgACizgAAos6AAKLOwACiz4AAotLAAKLWgACi1wAAoteAAKLYAACi2IAAotkAAKLZgACi2gAAot3AAKLegACi30AAouAAAKLgwACi4YAAouJAAKLjAACi44AAovNAAKLzwACi9EAAovTAAKL1gACi9cAAovYAAKL2QACi9oAAovcAAKL3gACi98AAovgAAKL4gACi+MAAowiAAKMJAACjCYAAowoAAKMKwACjCwAAowtAAKMLgACjC8AAowxAAKMMwACjDQAAow1AAKMNwACjDgAAox3AAKMeQACjHsAAox9AAKMgAACjIEAAoyCAAKMgwACjIQAAoyGAAKMiAACjIkAAoyKAAKMjAACjI0AAozMAAKMzgACjNAAAozSAAKM1QACjNYAAozXAAKM2AACjNkAAozbAAKM3QACjN4AAozfAAKM4QACjOIAAo0hAAKNIwACjSUAAo0nAAKNKgACjSsAAo0sAAKNLQACjS4AAo0wAAKNMgACjTMAAo00AAKNNgACjTcAAo12AAKNeAACjXoAAo18AAKNfwACjYAAAo2BAAKNggACjYMAAo2FAAKNhwACjYgAAo2JAAKNiwACjYwAAo3LAAKNzQACjc8AAo3RAAKN1AACjdUAAo3WAAKN1wACjdgAAo3aAAKN3AACjd0AAo3eAAKN4AACjeEAAo4sAAKOTwACjm8AAo6PAAKOkQACjpMAAo6VAAKOlwACjpoAAo6bAAKOnAACjp8AAo6gAAKOogACjqMAAo6lAAKOqAACjqkAAo6qAAKOrQACjq4AAo6zAAKOwAACjsUAAo7HAAKOyQACjs4AAo7RAAKO1AACjtYAAo77AAKPHwACj0YAAo9qAAKPbQACj28AAo9xAAKPcwACj3UAAo93AAKPeAACj3sAAo+IAAKPmQACj5sAAo+dAAKPnwACj6EAAo+jAAKPpQACj6cAAo+pAAKPugACj70AAo/AAAKPwwACj8YAAo/JAAKPzAACj88AAo/SAAKP1AACkBMAApAVAAKQFwACkBkAApAcAAKQHQACkB4AApAfAAKQIAACkCIAApAkAAKQJQACkCYAApAoAAKQKQACkGgAApBqAAKQbAACkG4AApBxAAKQcgACkHMAApB0AAKQdQACkHcAApB5AAKQegACkHsAApB9AAKQfgACkL0AApC/AAKQwgACkMQAApDHAAKQyAACkMkAApDKAAKQywACkM0AApDPAAKQ0AACkNEAApDTAAKQ1AACkOEAApDiAAKQ4wACkOUAApEkAAKRJgACkSgAApEqAAKRLQACkS4AApEvAAKRMAACkTEAApEzAAKRNQACkTYAApE3AAKROQACkToAApF5AAKRewACkX0AApF/AAKRggACkYMAApGEAAKRhQACkYYAApGIAAKRigACkYsAApGMAAKRjgACkY8AApHOAAKR0AACkdIAApHUAAKR1wACkdgAApHZAAKR2gACkdsAApHdAAKR3wACkeAAApHhAAKR4wACkeQAApIjAAKSJQACkicAApIpAAKSLAACki0AApIuAAKSLwACkjAAApIyAAKSNAACkjUAApI2AAKSOAACkjkAApJ4AAKSegACknwAApJ+AAKSgQACkoIAApKDAAKShAACkoUAApKHAAKSiQACkooAApKLAAKSjQACko4AApKzAAKS1wACkv4AApMiAAKTJQACkycAApMpAAKTKwACky0AApMvAAKTMAACkzMAApNAAAKTTQACk08AApNRAAKTUwACk1UAApNXAAKTWQACk2YAApNpAAKTbAACk28AApNyAAKTdQACk3gAApN6AAKTuQACk7sAApO+AAKTwAACk8MAApPEAAKTxQACk8YAApPHAAKTyQACk8sAApPMAAKTzQACk88AApPQAAKUEwAClDcAApRbAAKUfgAClKUAApTFAAKU7AAClRMAApUzAAKVVwAClXsAApV9AAKVgAAClYIAApWEAAKVhgAClYkAApWMAAKVjgAClZAAApWTAAKVlQAClZcAApWaAAKVnQAClZ4AApWjAAKVsAAClbMAApW1AAKVuAAClbsAApW9AAKV4gAClgYAApYtAAKWUQACllQAApZWAAKWWAAClloAApZcAAKWXgACll8AApZiAAKWbwACloIAApaEAAKWhgAClogAApaKAAKWjAAClo4AApaQAAKWkgAClpQAApanAAKWqgAClq0AApawAAKWswAClrYAApa5AAKWvAAClr8AApbCAAKWxAAClwMAApcFAAKXCAAClwoAApcNAAKXDgAClw8AApcQAAKXEQAClxMAApcVAAKXFgAClxcAApcZAAKXGgAClyMAApckAAKXJgACl2UAApdnAAKXaQACl2sAApduAAKXbwACl3AAApdxAAKXcgACl3QAApd2AAKXdwACl3gAApd6AAKXewACl7oAApe8AAKXvwACl8EAApfEAAKXxQACl8YAApfHAAKXyAACl8oAApfMAAKXzQACl84AApfQAAKX0QACl9oAApfbAAKX3QACmBwAApgeAAKYIAACmCIAApglAAKYJgACmCcAApgoAAKYKQACmCsAApgtAAKYLgACmC8AApgxAAKYMgACmHEAAphzAAKYdgACmHgAAph7AAKYfAACmH0AAph+AAKYfwACmIEAApiDAAKYhAACmIUAApiHAAKYiAACmJEAApiSAAKYlAACmNMAApjVAAKY1wACmNkAApjcAAKY3QACmN4AApjfAAKY4AACmOIAApjkAAKY5QACmOYAApjoAAKY6QACmSgAApkqAAKZLQACmS8AApkyAAKZMwACmTQAApk1AAKZNgACmTgAApk6AAKZOwACmTwAApk+AAKZPwACmUwAAplNAAKZTgACmVAAApmPAAKZkQACmZMAApmVAAKZmAACmZkAApmaAAKZmwACmZwAApmeAAKZoAACmaEAApmiAAKZpAACmaUAApnkAAKZ5gACmekAApnrAAKZ7gACme8AApnwAAKZ8QACmfIAApn0AAKZ9gACmfcAApn4AAKZ+gACmfsAApoKAAKaFwACmh4AApohAAKaIwACmiYAApotAAKaMAACmjMAApo2AAKaOAACmkAAAppFAAKakAACmrMAAprTAAKa8wACmvUAApr3AAKa+QACmvsAApr+AAKa/wACmwAAApsDAAKbBAACmwYAApsHAAKbCQACmwwAApsNAAKbDgACmxEAApsSAAKbFwACmyQAApspAAKbKwACmy0AApsyAAKbNQACmzgAAps6AAKbXwACm4MAApuqAAKbzgACm9EAApvTAAKb1QACm9cAApvZAAKb2wACm9wAApvfAAKb7AACm/0AApv/AAKcAQACnAMAApwFAAKcBwACnAkAApwLAAKcDQACnB4AApwhAAKcJAACnCcAApwqAAKcLQACnDAAApwzAAKcNgACnDgAApx3AAKceQACnHsAApx9AAKcgAACnIEAApyCAAKcgwACnIQAApyGAAKciAACnIkAApyKAAKcjAACnI0AApzMAAKczgACnNAAApzSAAKc1QACnNYAApzXAAKc2AACnNkAApzbAAKc3QACnN4AApzfAAKc4QACnOIAAp0hAAKdIwACnSYAAp0oAAKdKwACnSwAAp0tAAKdLgACnS8AAp0xAAKdMwACnTQAAp01AAKdNwACnTgAAp1FAAKdRgACnUcAAp1JAAKdiAACnYoAAp2MAAKdjgACnZEAAp2SAAKdkwACnZQAAp2VAAKdlwACnZkAAp2aAAKdmwACnZ0AAp2eAAKd3QACnd8AAp3hAAKd4wACneYAAp3nAAKd6AACnekAAp3qAAKd7AACne4AAp3vAAKd8AACnfIAAp3zAAKeMgACnjQAAp42AAKeOAACnjsAAp48AAKePQACnj4AAp4/AAKeQQACnkMAAp5EAAKeRQACnkcAAp5IAAKehwACnokAAp6LAAKejQACnpAAAp6RAAKekgACnpMAAp6UAAKelgACnpgAAp6ZAAKemgACnpwAAp6dAAKe3AACnt4AAp7gAAKe4gACnuUAAp7mAAKe5wACnugAAp7pAAKe6wACnu0AAp7uAAKe7wACnvEAAp7yAAKfFwACnzsAAp9iAAKfhgACn4kAAp+LAAKfjQACn48AAp+RAAKfkwACn5QAAp+XAAKfpAACn7MAAp+1AAKftwACn7kAAp+7AAKfvQACn78AAp/BAAKf0AACn9MAAp/WAAKf2QACn9wAAp/fAAKf4gACn+UAAp/nAAKgJgACoCgAAqAqAAKgLAACoC8AAqAwAAKgMQACoDIAAqAzAAKgNQACoDcAAqA4AAKgOQACoDsAAqA8AAKgewACoH0AAqB/AAKggQACoIQAAqCFAAKghgACoIcAAqCIAAKgigACoIwAAqCNAAKgjgACoJAAAqCRAAKg0AACoNIAAqDUAAKg1gACoNkAAqDaAAKg2wACoNwAAqDdAAKg3wACoOEAAqDiAAKg4wACoOUAAqDmAAKhJQACoScAAqEpAAKhKwACoS4AAqEvAAKhMAACoTEAAqEyAAKhNAACoTYAAqE3AAKhOAACoToAAqE7AAKhegACoXwAAqF+AAKhgAACoYMAAqGEAAKhhQACoYYAAqGHAAKhiQACoYsAAqGMAAKhjQACoY8AAqGQAAKhzwACodEAAqHTAAKh1QACodgAAqHZAAKh2gACodsAAqHcAAKh3gACoeAAAqHhAAKh4gACoeQAAqHlAAKiJAACoiYAAqIoAAKiKgACoi0AAqIuAAKiLwACojAAAqIxAAKiMwACojUAAqI2AAKiNwACojkAAqI6AAKihQACoqgAAqLIAAKi6AACouoAAqLsAAKi7gACovAAAqLzAAKi9AACovUAAqL4AAKi+QACovsAAqL8AAKi/gACowAAAqMBAAKjAgACowUAAqMGAAKjCwACoxgAAqMdAAKjHwACoyEAAqMmAAKjKQACoywAAqMuAAKjUwACo3cAAqOeAAKjwgACo8UAAqPHAAKjyQACo8sAAqPNAAKjzwACo9AAAqPTAAKj4AACo/EAAqPzAAKj9QACo/cAAqP5AAKj+wACo/0AAqP/AAKkAQACpBIAAqQVAAKkGAACpBsAAqQeAAKkIQACpCQAAqQnAAKkKgACpCwAAqRrAAKkbQACpG8AAqRxAAKkdAACpHUAAqR2AAKkdwACpHgAAqR6AAKkfAACpH0AAqR+AAKkgAACpIEAAqTAAAKkwgACpMQAAqTGAAKkyQACpMoAAqTLAAKkzAACpM0AAqTPAAKk0QACpNIAAqTTAAKk1QACpNYAAqUVAAKlFwACpRoAAqUcAAKlHwACpSAAAqUhAAKlIgACpSMAAqUlAAKlJwACpSgAAqUpAAKlKwACpSwAAqU5AAKlOgACpTsAAqU9AAKlfAACpX4AAqWAAAKlggACpYUAAqWGAAKlhwACpYgAAqWJAAKliwACpY0AAqWOAAKljwACpZEAAqWSAAKl0QACpdMAAqXVAAKl1wACpdoAAqXbAAKl3AACpd0AAqXeAAKl4AACpeIAAqXjAAKl5AACpeYAAqXnAAKmJgACpigAAqYqAAKmLAACpi8AAqYwAAKmMQACpjIAAqYzAAKmNQACpjcAAqY4AAKmOQACpjsAAqY8AAKmewACpn0AAqZ/AAKmgQACpoQAAqaFAAKmhgACpocAAqaIAAKmigACpowAAqaNAAKmjgACppAAAqaRAAKm0AACptIAAqbUAAKm1gACptkAAqbaAAKm2wACptwAAqbdAAKm3wACpuEAAqbiAAKm4wACpuUAAqbmAAKnCwACpy8AAqdWAAKnegACp30AAqd/AAKngQACp4MAAqeFAAKnhwACp4gAAqeLAAKnmAACp6cAAqepAAKnqwACp60AAqevAAKnsQACp7MAAqe1AAKnxAACp8cAAqfKAAKnzQACp9AAAqfTAAKn1gACp9kAAqfbAAKoGgACqBwAAqgeAAKoIAACqCMAAqgkAAKoJQACqCYAAqgnAAKoKQACqCsAAqgsAAKoLQACqC8AAqgwAAKobwACqHEAAqhzAAKodQACqHgAAqh5AAKoegACqHsAAqh8AAKofgACqIAAAqiBAAKoggACqIQAAqiFAAKoxAACqMYAAqjIAAKoygACqM0AAqjOAAKozwACqNAAAqjRAAKo0wACqNUAAqjWAAKo1wACqNkAAqjaAAKpGQACqRsAAqkdAAKpHwACqSIAAqkjAAKpJAACqSUAAqkmAAKpKAACqSoAAqkrAAKpLAACqS4AAqkvAAKpbgACqXAAAqlyAAKpdAACqXcAAql4AAKpeQACqXoAAql7AAKpfQACqX8AAqmAAAKpgQACqYMAAqmEAAKpwwACqcUAAqnHAAKpyQACqcwAAqnNAAKpzgACqc8AAqnQAAKp0gACqdQAAqnVAAKp1gACqdgAAqnZAAKqGAACqhoAAqocAAKqHgACqiEAAqoiAAKqIwACqiQAAqolAAKqJwACqikAAqoqAAKqKwACqi0AAqouAAKqeQACqpwAAqq8AAKq3AACqt4AAqrgAAKq4gACquQAAqrnAAKq6AACqukAAqrsAAKq7QACqu8AAqrwAAKq8gACqvUAAqr2AAKq9wACqvoAAqr7AAKrAAACqw0AAqsSAAKrFAACqxYAAqsbAAKrHgACqyEAAqsjAAKrSAACq2wAAquTAAKrtwACq7oAAqu8AAKrvgACq8AAAqvCAAKrxAACq8UAAqvIAAKr1QACq+YAAqvoAAKr6gACq+wAAqvuAAKr8AACq/IAAqv0AAKr9gACrAcAAqwKAAKsDQACrBAAAqwTAAKsFgACrBkAAqwcAAKsHwACrCEAAqxgAAKsYgACrGQAAqxmAAKsaQACrGoAAqxrAAKsbAACrG0AAqxvAAKscQACrHIAAqxzAAKsdQACrHYAAqy1AAKstwACrLkAAqy7AAKsvgACrL8AAqzAAAKswQACrMIAAqzEAAKsxgACrMcAAqzIAAKsygACrMsAAq0KAAKtDAACrQ8AAq0RAAKtFAACrRUAAq0WAAKtFwACrRgAAq0aAAKtHAACrR0AAq0eAAKtIAACrSEAAq0uAAKtLwACrTAAAq0yAAKtcQACrXMAAq11AAKtdwACrXoAAq17AAKtfAACrX0AAq1+AAKtgAACrYIAAq2DAAKthAACrYYAAq2HAAKtxgACrcgAAq3KAAKtzAACrc8AAq3QAAKt0QACrdIAAq3TAAKt1QACrdcAAq3YAAKt2QACrdsAAq3cAAKuGwACrh0AAq4fAAKuIQACriQAAq4lAAKuJgACricAAq4oAAKuKgACriwAAq4tAAKuLgACrjAAAq4xAAKucAACrnIAAq50AAKudgACrnkAAq56AAKuewACrnwAAq59AAKufwACroEAAq6CAAKugwACroUAAq6GAAKuxQACrscAAq7JAAKuywACrs4AAq7PAAKu0AACrtEAAq7SAAKu1AACrtYAAq7XAAKu2AACrtoAAq7bAAKvAAACryQAAq9LAAKvbwACr3IAAq90AAKvdgACr3gAAq96AAKvfAACr30AAq+AAAKvjQACr5wAAq+eAAKvoAACr6IAAq+kAAKvpgACr6gAAq+qAAKvuQACr7wAAq+/AAKvwgACr8UAAq/IAAKvywACr84AAq/QAAKwDwACsBEAArATAAKwFQACsBgAArAZAAKwGgACsBsAArAcAAKwHgACsCAAArAhAAKwIgACsCQAArAlAAKwZAACsGYAArBoAAKwagACsG0AArBuAAKwbwACsHAAArBxAAKwcwACsHUAArB2AAKwdwACsHkAArB6AAKwuQACsLsAArC9AAKwvwACsMIAArDDAAKwxAACsMUAArDGAAKwyAACsMoAArDLAAKwzAACsM4AArDPAAKxDgACsRAAArETAAKxFQACsRgAArEZAAKxGgACsRsAArEcAAKxHgACsSAAArEhAAKxIgACsSQAArElAAKxJwACsWYAArFoAAKxagACsWwAArFvAAKxcAACsXEAArFyAAKxcwACsXUAArF3AAKxeAACsXkAArF7AAKxfAACsbsAArG9AAKxvwACscEAArHEAAKxxQACscYAArHHAAKxyAACscoAArHMAAKxzQACsc4AArHQAAKx0QACshAAArISAAKyFAACshYAArIZAAKyGgACshsAArIcAAKyHQACsh8AArIhAAKyIgACsiMAArIlAAKyJgACsi8AArIwAAKyMgACsnEAArJzAAKydQACsncAArJ6AAKyewACsnwAArJ9AAKyfgACsoAAArKCAAKygwACsoQAArKGAAKyhwACssYAArLIAAKyygACsswAArLPAAKy0AACstEAArLSAAKy0wACstUAArLXAAKy2AACstkAArLbAAKy3AACsxsAArMdAAKzHwACsyEAArMkAAKzJQACsyYAArMnAAKzKAACsyoAArMsAAKzLQACsy4AArMwAAKzMQACs3AAArNyAAKzdAACs3YAArN5AAKzegACs3sAArN8AAKzfQACs38AArOBAAKzggACs4MAArOFAAKzhgACs8UAArPHAAKzyQACs8sAArPOAAKzzwACs9AAArPRAAKz0gACs9QAArPWAAKz1wACs9gAArPaAAKz2wACtCYAArRJAAK0aQACtIkAArSLAAK0jQACtI8AArSRAAK0lAACtJUAArSWAAK0mQACtJoAArScAAK0nQACtJ8AArSiAAK0owACtKQAArSnAAK0qAACtLEAArS+AAK0wwACtMUAArTHAAK0zAACtM8AArTSAAK01AACtPkAArUdAAK1RAACtWgAArVrAAK1bQACtW8AArVxAAK1cwACtXUAArV2AAK1eQACtYYAArWXAAK1mQACtZsAArWdAAK1nwACtaEAArWjAAK1pQACtacAArW4AAK1uwACtb4AArXBAAK1xAACtccAArXKAAK1zQACtdAAArXSAAK2EQACthMAArYVAAK2FwACthoAArYbAAK2HAACth0AArYeAAK2IAACtiIAArYjAAK2JAACtiYAArYnAAK2ZgACtmgAArZqAAK2bAACtm8AArZwAAK2cQACtnIAArZzAAK2dQACtncAArZ4AAK2eQACtnsAArZ8AAK2uwACtr0AArbAAAK2wgACtsUAArbGAAK2xwACtsgAArbJAAK2ywACts0AArbOAAK2zwACttEAArbSAAK23wACtuAAArbhAAK24wACtyIAArckAAK3JgACtygAArcrAAK3LAACty0AArcuAAK3LwACtzEAArczAAK3NAACtzUAArc3AAK3OAACt3cAArd5AAK3ewACt30AAreAAAK3gQACt4IAAreDAAK3hAACt4YAAreIAAK3iQACt4oAAreMAAK3jQACt8wAArfOAAK30AACt9IAArfVAAK31gACt9cAArfYAAK32QACt9sAArfdAAK33gACt98AArfhAAK34gACuCEAArgjAAK4JQACuCcAArgqAAK4KwACuCwAArgtAAK4LgACuDAAArgyAAK4MwACuDQAArg2AAK4NwACuHYAArh4AAK4egACuHwAArh/AAK4gAACuIEAAriCAAK4gwACuIUAAriHAAK4iAACuIkAAriLAAK4jAACuLEAArjVAAK4/AACuSAAArkjAAK5JQACuScAArkpAAK5KwACuS0AArkuAAK5MQACuT4AArlNAAK5TwACuVEAArlTAAK5VQACuVcAArlZAAK5WwACuWoAArltAAK5cAACuXMAArl2AAK5eQACuXwAArl/AAK5gQACucAAArnCAAK5xAACucYAArnJAAK5ygACucsAArnMAAK5zQACuc8AArnRAAK50gACudMAArnVAAK51gACuhUAAroXAAK6GQACuhsAAroeAAK6HwACuiAAArohAAK6IgACuiQAAromAAK6JwACuigAAroqAAK6KwACumoAArpsAAK6bgACunAAArpzAAK6dAACunUAArp2AAK6dwACunkAArp7AAK6fAACun0AArp/AAK6gAACur8AArrBAAK6wwACusUAArrIAAK6yQACusoAArrLAAK6zAACus4AArrQAAK60QACutIAArrUAAK61QACuxQAArsWAAK7GAACuxoAArsdAAK7HgACux8AArsgAAK7IQACuyMAArslAAK7JgACuycAArspAAK7KgACu2kAArtrAAK7bQACu28AArtyAAK7cwACu3QAArt1AAK7dgACu3gAArt6AAK7ewACu3wAArt+AAK7fwACu74AArvAAAK7wgACu8QAArvHAAK7yAACu8kAArvKAAK7ywACu80AArvPAAK70AACu9EAArvTAAK71AACu90AArveAAK74AACvCMAArxHAAK8awACvI4AAry1AAK81QACvPwAAr0jAAK9QwACvWcAAr2LAAK9jQACvZAAAr2SAAK9lAACvZYAAr2ZAAK9nAACvZ4AAr2gAAK9owACvaUAAr2nAAK9qgACva0AAr2uAAK9swACvcAAAr3DAAK9xQACvcgAAr3LAAK9zQACvfIAAr4WAAK+PQACvmEAAr5kAAK+ZgACvmgAAr5qAAK+bAACvm4AAr5vAAK+cgACvn8AAr6SAAK+lAACvpYAAr6YAAK+mgACvpwAAr6eAAK+oAACvqIAAr6kAAK+twACvroAAr69AAK+wAACvsMAAr7GAAK+yQACvswAAr7PAAK+0gACvtQAAr8TAAK/FQACvxgAAr8aAAK/HQACvx4AAr8fAAK/IAACvyEAAr8jAAK/JQACvyYAAr8nAAK/KQACvyoAAr8zAAK/NAACvzYAAr91AAK/dwACv3kAAr97AAK/fgACv38AAr+AAAK/gQACv4IAAr+EAAK/hgACv4cAAr+IAAK/igACv4sAAr/KAAK/zAACv88AAr/RAAK/1AACv9UAAr/WAAK/1wACv9gAAr/aAAK/3AACv90AAr/eAAK/4AACv+EAAr/qAAK/6wACv+0AAsAsAALALgACwDAAAsAyAALANQACwDYAAsA3AALAOAACwDkAAsA7AALAPQACwD4AAsA/AALAQQACwEIAAsCBAALAgwACwIYAAsCIAALAiwACwIwAAsCNAALAjgACwI8AAsCRAALAkwACwJQAAsCVAALAlwACwJgAAsChAALAogACwKQAAsDjAALA5QACwOcAAsDpAALA7AACwO0AAsDuAALA7wACwPAAAsDyAALA9AACwPUAAsD2AALA+AACwPkAAsE4AALBOgACwT0AAsE/AALBQgACwUMAAsFEAALBRQACwUYAAsFIAALBSgACwUsAAsFMAALBTgACwU8AAsFcAALBXQACwV4AAsFgAALBnwACwaEAAsGjAALBpQACwagAAsGpAALBqgACwasAAsGsAALBrgACwbAAAsGxAALBsgACwbQAAsG1AALB9AACwfYAAsH5AALB+wACwf4AAsH/AALCAAACwgEAAsICAALCBAACwgYAAsIHAALCCAACwgoAAsILAALCFwACwiQAAsIpAALCLAACwi8AAsI0AALCNwACwjoAAsI8AALCQgACwk0AAsKYAALCuwACwtsAAsL7AALC/QACwv8AAsMBAALDAwACwwYAAsMHAALDCAACwwsAAsMMAALDDgACww8AAsMRAALDFAACwxUAAsMWAALDGQACwxoAAsMfAALDLAACwzEAAsMzAALDNQACwzoAAsM9AALDQAACw0IAAsNnAALDiwACw7IAAsPWAALD2QACw9sAAsPdAALD3wACw+EAAsPjAALD5AACw+cAAsP0AALEBQACxAcAAsQJAALECwACxA0AAsQPAALEEQACxBMAAsQVAALEJgACxCkAAsQsAALELwACxDIAAsQ1AALEOAACxDsAAsQ+AALEQAACxH8AAsSBAALEgwACxIUAAsSIAALEiQACxIoAAsSLAALEjAACxI4AAsSQAALEkQACxJIAAsSUAALElQACxNQAAsTWAALE2AACxNoAAsTdAALE3gACxN8AAsTgAALE4QACxOMAAsTlAALE5gACxOcAAsTpAALE6gACxSkAAsUrAALFLgACxTAAAsUzAALFNAACxTUAAsU2AALFNwACxTkAAsU7AALFPAACxT0AAsU/AALFQAACxU0AAsVOAALFTwACxVEAAsWQAALFkgACxZQAAsWWAALFmQACxZoAAsWbAALFnAACxZ0AAsWfAALFoQACxaIAAsWjAALFpQACxaYAAsXlAALF5wACxekAAsXrAALF7gACxe8AAsXwAALF8QACxfIAAsX0AALF9gACxfcAAsX4AALF+gACxfsAAsY6AALGPAACxj4AAsZAAALGQwACxkQAAsZFAALGRgACxkcAAsZJAALGSwACxkwAAsZNAALGTwACxlAAAsaPAALGkQACxpMAAsaVAALGmAACxpkAAsaaAALGmwACxpwAAsaeAALGoAACxqEAAsaiAALGpAACxqUAAsbkAALG5gACxugAAsbqAALG7QACxu4AAsbvAALG8AACxvEAAsbzAALG9QACxvYAAsb3AALG+QACxvoAAscfAALHQwACx2oAAseOAALHkQACx5MAAseVAALHlwACx5kAAsebAALHnAACx58AAsesAALHuwACx70AAse/AALHwQACx8MAAsfFAALHxwACx8kAAsfYAALH2wACx94AAsfhAALH5AACx+cAAsfqAALH7QACx+8AAsguAALIMAACyDIAAsg0AALINwACyDgAAsg5AALIOgACyDsAAsg9AALIPwACyEAAAshBAALIQwACyEQAAsiDAALIhQACyIcAAsiJAALIjAACyI0AAsiOAALIjwACyJAAAsiSAALIlAACyJUAAsiWAALImAACyJkAAsjYAALI2gACyNwAAsjeAALI4QACyOIAAsjjAALI5AACyOUAAsjnAALI6QACyOoAAsjrAALI7QACyO4AAsktAALJLwACyTEAAskzAALJNgACyTcAAsk4AALJOQACyToAAsk8AALJPgACyT8AAslAAALJQgACyUMAAsmCAALJhAACyYYAAsmIAALJiwACyYwAAsmNAALJjgACyY8AAsmRAALJkwACyZQAAsmVAALJlwACyZgAAsnXAALJ2QACydsAAsndAALJ4AACyeEAAsniAALJ4wACyeQAAsnmAALJ6AACyekAAsnqAALJ7AACye0AAsosAALKLgACyjAAAsoyAALKNQACyjYAAso3AALKOAACyjkAAso7AALKPQACyj4AAso/AALKQQACykIAAsqNAALKsAACytAAAsrwAALK8gACyvQAAsr2AALK+AACyvsAAsr8AALK/QACywAAAssBAALLAwACywQAAssGAALLCQACywoAAssLAALLDgACyw8AAssUAALLIQACyyYAAssoAALLKgACyy8AAssyAALLNQACyzcAAstcAALLgAACy6cAAsvLAALLzgACy9AAAsvSAALL1AACy9YAAsvYAALL2QACy9wAAsvpAALL+gACy/wAAsv+AALMAAACzAIAAswEAALMBgACzAgAAswKAALMGwACzB4AAswhAALMJAACzCcAAswqAALMLQACzDAAAswzAALMNQACzHQAAsx2AALMeAACzHoAAsx9AALMfgACzH8AAsyAAALMgQACzIMAAsyFAALMhgACzIcAAsyJAALMigACzMkAAszLAALMzQACzM8AAszSAALM0wACzNQAAszVAALM1gACzNgAAszaAALM2wACzNwAAszeAALM3wACzR4AAs0gAALNIwACzSUAAs0oAALNKQACzSoAAs0rAALNLAACzS4AAs0wAALNMQACzTIAAs00AALNNQACzUIAAs1DAALNRAACzUYAAs2FAALNhwACzYkAAs2LAALNjgACzY8AAs2QAALNkQACzZIAAs2UAALNlgACzZcAAs2YAALNmgACzZsAAs3aAALN3AACzd4AAs3gAALN4wACzeQAAs3lAALN5gACzecAAs3pAALN6wACzewAAs3tAALN7wACzfAAAs4vAALOMQACzjMAAs41AALOOAACzjkAAs46AALOOwACzjwAAs4+AALOQAACzkEAAs5CAALORAACzkUAAs6EAALOhgACzogAAs6KAALOjQACzo4AAs6PAALOkAACzpEAAs6TAALOlQACzpYAAs6XAALOmQACzpoAAs7ZAALO2wACzt0AAs7fAALO4gACzuMAAs7kAALO5QACzuYAAs7oAALO6gACzusAAs7sAALO7gACzu8AAs8UAALPOAACz18AAs+DAALPhgACz4gAAs+KAALPjAACz44AAs+QAALPkQACz5QAAs+hAALPsAACz7IAAs+0AALPtgACz7gAAs+6AALPvAACz74AAs/NAALP0AACz9MAAs/WAALP2QACz9wAAs/fAALP4gACz+QAAtAjAALQJQAC0CcAAtApAALQLAAC0C0AAtAuAALQLwAC0DAAAtAyAALQNAAC0DUAAtA2AALQOAAC0DkAAtB4AALQegAC0HwAAtB+AALQgQAC0IIAAtCDAALQhAAC0IUAAtCHAALQiQAC0IoAAtCLAALQjQAC0I4AAtDNAALQzwAC0NEAAtDTAALQ1gAC0NcAAtDYAALQ2QAC0NoAAtDcAALQ3gAC0N8AAtDgAALQ4gAC0OMAAtEiAALRJAAC0SYAAtEoAALRKwAC0SwAAtEtAALRLgAC0S8AAtExAALRMwAC0TQAAtE1AALRNwAC0TgAAtF3AALReQAC0XsAAtF9AALRgAAC0YEAAtGCAALRgwAC0YQAAtGGAALRiAAC0YkAAtGKAALRjAAC0Y0AAtHMAALRzgAC0dAAAtHSAALR1QAC0dYAAtHXAALR2AAC0dkAAtHbAALR3QAC0d4AAtHfAALR4QAC0eIAAtIhAALSIwAC0iUAAtInAALSKgAC0isAAtIsAALSLQAC0i4AAtIwAALSMgAC0jMAAtI0AALSNgAC0jcAAtJAAALSQQAC0kMAAtKGAALSqgAC0s4AAtLxAALTGAAC0zgAAtNfAALThgAC06YAAtPKAALT7gAC0/AAAtPzAALT9QAC0/cAAtP5AALT/AAC0/8AAtQBAALUAwAC1AYAAtQIAALUCgAC1A0AAtQQAALUEQAC1BYAAtQjAALUJgAC1CgAAtQrAALULgAC1DAAAtRVAALUeQAC1KAAAtTEAALUxwAC1MkAAtTLAALUzQAC1M8AAtTRAALU0gAC1NUAAtTiAALU9QAC1PcAAtT5AALU+wAC1P0AAtT/AALVAQAC1QMAAtUFAALVBwAC1RoAAtUdAALVIAAC1SMAAtUmAALVKQAC1SwAAtUvAALVMgAC1TUAAtU3AALVdgAC1XgAAtV7AALVfQAC1YAAAtWBAALVggAC1YMAAtWEAALVhgAC1YgAAtWJAALVigAC1YwAAtWNAALVlgAC1ZcAAtWZAALV2AAC1doAAtXcAALV3gAC1eEAAtXiAALV4wAC1eQAAtXlAALV5wAC1ekAAtXqAALV6wAC1e0AAtXuAALWLQAC1i8AAtYyAALWNAAC1jcAAtY4AALWOQAC1joAAtY7AALWPQAC1j8AAtZAAALWQQAC1kMAAtZEAALWTQAC1k4AAtZQAALWjwAC1pEAAtaTAALWlQAC1pgAAtaZAALWmgAC1psAAtacAALWngAC1qAAAtahAALWogAC1qQAAtalAALW5AAC1uYAAtbpAALW6wAC1u4AAtbvAALW8AAC1vEAAtbyAALW9AAC1vYAAtb3AALW+AAC1voAAtb7AALXBAAC1wUAAtcHAALXRgAC10gAAtdKAALXTAAC108AAtdQAALXUQAC11IAAtdTAALXVQAC11cAAtdYAALXWQAC11sAAtdcAALXmwAC150AAtegAALXogAC16UAAtemAALXpwAC16gAAtepAALXqwAC160AAteuAALXrwAC17EAAteyAALXvwAC18AAAtfBAALXwwAC2AIAAtgEAALYBgAC2AgAAtgLAALYDAAC2A0AAtgOAALYDwAC2BEAAtgTAALYFAAC2BUAAtgXAALYGAAC2FcAAthZAALYXAAC2F4AAthhAALYYgAC2GMAAthkAALYZQAC2GcAAthpAALYagAC2GsAAthtAALYbgAC2IYAAtiTAALYngAC2KEAAtikAALYpwAC2KoAAtitAALYuAAC2LsAAti+AALYwQAC2MQAAtjHAALYyQAC2NIAAtjbAALY5QAC2TAAAtlTAALZcwAC2ZMAAtmVAALZlwAC2ZkAAtmbAALZngAC2Z8AAtmgAALZowAC2aQAAtmmAALZpwAC2akAAtmsAALZrQAC2a4AAtmxAALZsgAC2bsAAtnIAALZzQAC2c8AAtnRAALZ1gAC2dkAAtncAALZ3gAC2gMAAtonAALaTgAC2nIAAtp1AALadwAC2nkAAtp7AALafQAC2n8AAtqAAALagwAC2pAAAtqhAALaowAC2qUAAtqnAALaqQAC2qsAAtqtAALarwAC2rEAAtrCAALaxQAC2sgAAtrLAALazgAC2tEAAtrUAALa1wAC2toAAtrcAALbGwAC2x0AAtsfAALbIQAC2yQAAtslAALbJgAC2ycAAtsoAALbKgAC2ywAAtstAALbLgAC2zAAAtsxAALbcAAC23IAAtt0AALbdgAC23kAAtt6AALbewAC23wAAtt9AALbfwAC24EAAtuCAALbgwAC24UAAtuGAALbxQAC28cAAtvKAALbzAAC288AAtvQAALb0QAC29IAAtvTAALb1QAC29cAAtvYAALb2QAC29sAAtvcAALb6QAC2+oAAtvrAALb7QAC3CwAAtwuAALcMAAC3DIAAtw1AALcNgAC3DcAAtw4AALcOQAC3DsAAtw9AALcPgAC3D8AAtxBAALcQgAC3IEAAtyDAALchQAC3IcAAtyKAALciwAC3IwAAtyNAALcjgAC3JAAAtySAALckwAC3JQAAtyWAALclwAC3NYAAtzYAALc2gAC3NwAAtzfAALc4AAC3OEAAtziAALc4wAC3OUAAtznAALc6AAC3OkAAtzrAALc7AAC3SsAAt0tAALdLwAC3TEAAt00AALdNQAC3TYAAt03AALdOAAC3ToAAt08AALdPQAC3T4AAt1AAALdQQAC3YAAAt2CAALdhAAC3YYAAt2JAALdigAC3YsAAt2MAALdjQAC3Y8AAt2RAALdkgAC3ZMAAt2VAALdlgAC3bsAAt3fAALeBgAC3ioAAt4tAALeLwAC3jEAAt4zAALeNQAC3jcAAt44AALeOwAC3kgAAt5XAALeWQAC3lsAAt5dAALeXwAC3mEAAt5jAALeZQAC3nQAAt53AALeegAC3n0AAt6AAALegwAC3oYAAt6JAALeiwAC3soAAt7MAALezgAC3tAAAt7TAALe1AAC3tUAAt7WAALe1wAC3tkAAt7bAALe3AAC3t0AAt7fAALe4AAC3x8AAt8hAALfIwAC3yUAAt8oAALfKQAC3yoAAt8rAALfLAAC3y4AAt8wAALfMQAC3zIAAt80AALfNQAC33QAAt92AALfeAAC33oAAt99AALffgAC338AAt+AAALfgQAC34MAAt+FAALfhgAC34cAAt+JAALfigAC38kAAt/LAALfzgAC39AAAt/TAALf1AAC39UAAt/WAALf1wAC39kAAt/bAALf3AAC390AAt/fAALf4AAC4B8AAuAhAALgIwAC4CUAAuAoAALgKQAC4CoAAuArAALgLAAC4C4AAuAwAALgMQAC4DIAAuA0AALgNQAC4HQAAuB2AALgeAAC4HoAAuB9AALgfgAC4H8AAuCAAALggQAC4IMAAuCFAALghgAC4IcAAuCJAALgigAC4MkAAuDLAALgzQAC4M8AAuDSAALg0wAC4NQAAuDVAALg1gAC4NgAAuDaAALg2wAC4NwAAuDeAALg3wAC4SoAAuFNAALhbQAC4Y0AAuGPAALhkQAC4ZMAAuGVAALhmAAC4ZkAAuGaAALhnQAC4Z4AAuGgAALhoQAC4aMAAuGmAALhpwAC4agAAuGrAALhrAAC4bEAAuG+AALhwwAC4cUAAuHHAALhzAAC4c8AAuHSAALh1AAC4fkAAuIdAALiRAAC4mgAAuJrAALibQAC4m8AAuJxAALicwAC4nUAAuJ2AALieQAC4oYAAuKXAALimQAC4psAAuKdAALinwAC4qEAAuKjAALipQAC4qcAAuK4AALiuwAC4r4AAuLBAALixAAC4scAAuLKAALizQAC4tAAAuLSAALjEQAC4xMAAuMVAALjFwAC4xoAAuMbAALjHAAC4x0AAuMeAALjIAAC4yIAAuMjAALjJAAC4yYAAuMnAALjZgAC42gAAuNqAALjbAAC428AAuNwAALjcQAC43IAAuNzAALjdQAC43cAAuN4AALjeQAC43sAAuN8AALjuwAC470AAuPAAALjwgAC48UAAuPGAALjxwAC48gAAuPJAALjywAC480AAuPOAALjzwAC49EAAuPSAALj3wAC4+AAAuPhAALj4wAC5CIAAuQkAALkJgAC5CgAAuQrAALkLAAC5C0AAuQuAALkLwAC5DEAAuQzAALkNAAC5DUAAuQ3AALkOAAC5HcAAuR5AALkewAC5H0AAuSAAALkgQAC5IIAAuSDAALkhAAC5IYAAuSIAALkiQAC5IoAAuSMAALkjQAC5MwAAuTOAALk0AAC5NIAAuTVAALk1gAC5NcAAuTYAALk2QAC5NsAAuTdAALk3gAC5N8AAuThAALk4gAC5SEAAuUjAALlJQAC5ScAAuUqAALlKwAC5SwAAuUtAALlLgAC5TAAAuUyAALlMwAC5TQAAuU2AALlNwAC5XYAAuV4AALlegAC5XwAAuV/AALlgAAC5YEAAuWCAALlgwAC5YUAAuWHAALliAAC5YkAAuWLAALljAAC5bEAAuXVAALl/AAC5iAAAuYjAALmJQAC5icAAuYpAALmKwAC5i0AAuYuAALmMQAC5j4AAuZNAALmTwAC5lEAAuZTAALmVQAC5lcAAuZZAALmWwAC5moAAuZtAALmcAAC5nMAAuZ2AALmeQAC5nwAAuZ/AALmgQAC5sAAAubCAALmxAAC5sYAAubJAALmygAC5ssAAubMAALmzQAC5s8AAubRAALm0gAC5tMAAubVAALm1gAC5xUAAucXAALnGQAC5xsAAuceAALnHwAC5yAAAuchAALnIgAC5yQAAucmAALnJwAC5ygAAucqAALnKwAC52oAAudsAALnbgAC53AAAudzAALndAAC53UAAud2AALndwAC53kAAud7AALnfAAC530AAud/AALngAAC578AAufBAALnxAAC58YAAufJAALnygAC58sAAufMAALnzQAC588AAufRAALn0gAC59MAAufVAALn1gAC6BUAAugXAALoGQAC6BsAAugeAALoHwAC6CAAAughAALoIgAC6CQAAugmAALoJwAC6CgAAugqAALoKwAC6GoAAuhsAALobgAC6HAAAuhzAALodAAC6HUAAuh2AALodwAC6HkAAuh7AALofAAC6H0AAuh/AALogAAC6L8AAujBAALowwAC6MUAAujIAALoyQAC6MoAAujLAALozAAC6M4AAujQAALo0QAC6NIAAujUAALo1QAC6SAAAulDAALpYwAC6YMAAumFAALphwAC6YkAAumLAALpjgAC6Y8AAumQAALpkwAC6ZQAAumWAALplwAC6ZkAAumcAALpnQAC6Z4AAumhAALpogAC6acAAum0AALpuQAC6bsAAum9AALpwgAC6cUAAunIAALpygAC6e8AAuoTAALqOgAC6l4AAuphAALqYwAC6mUAAupnAALqaQAC6msAAupsAALqbwAC6nwAAuqNAALqjwAC6pEAAuqTAALqlQAC6pcAAuqZAALqmwAC6p0AAuquAALqsQAC6rQAAuq3AALqugAC6r0AAurAAALqwwAC6sYAAurIAALrBwAC6wkAAusLAALrDQAC6xAAAusRAALrEgAC6xMAAusUAALrFgAC6xgAAusZAALrGgAC6xwAAusdAALrXAAC614AAutgAALrYgAC62UAAutmAALrZwAC62gAAutpAALrawAC620AAutuAALrbwAC63EAAutyAALrsQAC67MAAuu2AALruAAC67sAAuu8AALrvQAC674AAuu/AALrwQAC68MAAuvEAALrxQAC68cAAuvIAALr1QAC69YAAuvXAALr2QAC7BgAAuwaAALsHAAC7B4AAuwhAALsIgAC7CMAAuwkAALsJQAC7CcAAuwpAALsKgAC7CsAAuwtAALsLgAC7G0AAuxvAALscQAC7HMAAux2AALsdwAC7HgAAux5AALsegAC7HwAAux+AALsfwAC7IAAAuyCAALsgwAC7MIAAuzEAALsxgAC7MgAAuzLAALszAAC7M0AAuzOAALszwAC7NEAAuzTAALs1AAC7NUAAuzXAALs2AAC7RcAAu0ZAALtGwAC7R0AAu0gAALtIQAC7SIAAu0jAALtJAAC7SYAAu0oAALtKQAC7SoAAu0sAALtLQAC7WwAAu1uAALtcAAC7XIAAu11AALtdgAC7XcAAu14AALteQAC7XsAAu19AALtfgAC7X8AAu2BAALtggAC7acAAu3LAALt8gAC7hYAAu4ZAALuGwAC7h0AAu4fAALuIQAC7iMAAu4kAALuJwAC7jQAAu5DAALuRQAC7kcAAu5JAALuSwAC7k0AAu5PAALuUQAC7mAAAu5jAALuZgAC7mkAAu5sAALubwAC7nIAAu51AALudwAC7rYAAu64AALuugAC7rwAAu6/AALuwAAC7sEAAu7CAALuwwAC7sUAAu7HAALuyAAC7skAAu7LAALuzAAC7wsAAu8NAALvDwAC7xEAAu8UAALvFQAC7xYAAu8XAALvGAAC7xoAAu8cAALvHQAC7x4AAu8gAALvIQAC72AAAu9iAALvZAAC72YAAu9pAALvagAC72sAAu9sAALvbQAC728AAu9xAALvcgAC73MAAu91AALvdgAC77UAAu+3AALvuQAC77sAAu++AALvvwAC78AAAu/BAALvwgAC78QAAu/GAALvxwAC78gAAu/KAALvywAC8AoAAvAMAALwDgAC8BAAAvATAALwFAAC8BUAAvAWAALwFwAC8BkAAvAbAALwHAAC8B0AAvAfAALwIAAC8F8AAvBhAALwYwAC8GUAAvBoAALwaQAC8GoAAvBrAALwbAAC8G4AAvBwAALwcQAC8HIAAvB0AALwdQAC8LQAAvC2AALwuAAC8LoAAvC9AALwvgAC8L8AAvDAAALwwQAC8MMAAvDFAALwxgAC8McAAvDJAALwygAC8RUAAvE4AALxWAAC8XgAAvF6AALxfAAC8X4AAvGAAALxgwAC8YQAAvGFAALxiAAC8YkAAvGLAALxjAAC8Y4AAvGRAALxkgAC8ZMAAvGWAALxlwAC8ZwAAvGpAALxrgAC8bAAAvGyAALxtwAC8boAAvG9AALxvwAC8eQAAvIIAALyLwAC8lMAAvJWAALyWAAC8loAAvJcAALyXgAC8mAAAvJhAALyZAAC8nEAAvKCAALyhAAC8oYAAvKIAALyigAC8owAAvKOAALykAAC8pIAAvKjAALypgAC8qkAAvKsAALyrwAC8rIAAvK1AALyuAAC8rsAAvK9AALy/AAC8v4AAvMAAALzAgAC8wUAAvMGAALzBwAC8wgAAvMJAALzCwAC8w0AAvMOAALzDwAC8xEAAvMSAALzUQAC81MAAvNVAALzVwAC81oAAvNbAALzXAAC810AAvNeAALzYAAC82IAAvNjAALzZAAC82YAAvNnAALzpgAC86gAAvOrAALzrQAC87AAAvOxAALzsgAC87MAAvO0AALztgAC87gAAvO5AALzugAC87wAAvO9AALzygAC88sAAvPMAALzzgAC9A0AAvQPAAL0EQAC9BMAAvQWAAL0FwAC9BgAAvQZAAL0GgAC9BwAAvQeAAL0HwAC9CAAAvQiAAL0IwAC9GIAAvRkAAL0ZgAC9GgAAvRrAAL0bAAC9G0AAvRuAAL0bwAC9HEAAvRzAAL0dAAC9HUAAvR3AAL0eAAC9LcAAvS5AAL0uwAC9L0AAvTAAAL0wQAC9MIAAvTDAAL0xAAC9MYAAvTIAAL0yQAC9MoAAvTMAAL0zQAC9QwAAvUOAAL1EAAC9RIAAvUVAAL1FgAC9RcAAvUYAAL1GQAC9RsAAvU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rAAMXdgADF5kAAxe5AAMX2QADF9sAAxfdAAMX3wADF+EAAxfkAAMX5QADF+YAAxfpAAMX6gADF+wAAxftAAMX7wADF/IAAxfzAAMX9AADF/cAAxf4AAMX/QADGAoAAxgPAAMYEQADGBMAAxgYAAMYGwADGB4AAxggAAMYRQADGGkAAxiQAAMYtAADGLcAAxi5AAMYuwADGL0AAxi/AAMYwQADGMIAAxjFAAMY0gADGOMAAxjlAAMY5wADGOkAAxjrAAMY7QADGO8AAxjxAAMY8wADGQQAAxkHAAMZCgADGQ0AAxkQAAMZEwADGRYAAxkZAAMZHAADGR4AAxldAAMZXwADGWEAAxljAAMZZgADGWcAAxloAAMZaQADGWoAAxlsAAMZbgADGW8AAxlwAAMZcgADGXMAAxmyAAMZtAADGbYAAxm4AAMZuwADGbwAAxm9AAMZvgADGb8AAxnBAAMZwwADGcQAAxnFAAMZxwADGcgAAxoHAAMaCQADGgwAAxoOAAMaEQADGhIAAxoTAAMaFAADGhUAAxoXAAMaGQADGhoAAxobAAMaHQADGh4AAxorAAMaLAADGi0AAxovAAMabgADGnAAAxpyAAMadAADGncAAxp4AAMaeQADGnoAAxp7AAMafQADGn8AAxqAAAMagQADGoMAAxqEAAMawwADGsUAAxrHAAMayQADGswAAxrNAAMazgADGs8AAxrQAAMa0gADGtQAAxrVAAMa1gADGtgAAxrZAAMbGAADGxoAAxscAAMbHgADGyEAAxsiAAMbIwADGyQAAxslAAMbJwADGykAAxsqAAMbKwADGy0AAxsuAAMbbQADG28AAxtxAAMbcwADG3YAAxt3AAMbeAADG3kAAxt6AAMbfAADG34AAxt/AAMbgAADG4IAAxuDAAMbwgADG8QAAxvGAAMbyAADG8sAAxvMAAMbzQADG84AAxvPAAMb0QADG9MAAxvUAAMb1QADG9cAAxvYAAMb/QADHCEAAxxIAAMcbAADHG8AAxxxAAMccwADHHUAAxx3AAMceQADHHoAAxx9AAMcigADHJkAAxybAAMcnQADHJ8AAxyhAAMcowADHKUAAxynAAMctgADHLkAAxy8AAMcvwADHMIAAxzFAAMcyAADHMsAAxzNAAMdDAADHQ4AAx0QAAMdEgADHRUAAx0WAAMdFwADHRgAAx0ZAAMdGwADHR0AAx0eAAMdHwADHSEAAx0iAAMdYQADHWMAAx1lAAMdZwADHWoAAx1rAAMdbAADHW0AAx1uAAMdcAADHXIAAx1zAAMddAADHXYAAx13AAMdtgADHbgAAx26AAMdvAADHb8AAx3AAAMdwQADHcIAAx3DAAMdxQADHccAAx3IAAMdyQADHcsAAx3MAAMeCwADHg0AAx4PAAMeEQADHhQAAx4VAAMeFgADHhcAAx4YAAMeGgADHhwAAx4dAAMeHgADHiAAAx4hAAMeYAADHmIAAx5kAAMeZgADHmkAAx5qAAMeawADHmwAAx5tAAMebwADHnEAAx5yAAMecwADHnUAAx52AAMetQADHrcAAx65AAMeuwADHr4AAx6/AAMewAADHsEAAx7CAAMexAADHsYAAx7HAAMeyAADHsoAAx7LAAMfCgADHwwAAx8OAAMfEAADHxMAAx8UAAMfFQADHxYAAx8XAAMfGQADHxsAAx8cAAMfHQADHx8AAx8gAAMfawADH44AAx+uAAMfzgADH9AAAx/SAAMf1AADH9YAAx/ZAAMf2gADH9sAAx/eAAMf3wADH+EAAx/iAAMf5AADH+cAAx/oAAMf6QADH+wAAx/tAAMf8gADH/8AAyAEAAMgBgADIAgAAyANAAMgEAADIBMAAyAVAAMgOgADIF4AAyCFAAMgqQADIKwAAyCuAAMgsAADILIAAyC0AAMgtgADILcAAyC6AAMgxwADINgAAyDaAAMg3AADIN4AAyDgAAMg4gADIOQAAyDmAAMg6AADIPkAAyD8AAMg/wADIQIAAyEFAAMhCAADIQsAAyEOAAMhEQADIRMAAyFSAAMhVAADIVYAAyFYAAMhWwADIVwAAyFdAAMhXgADIV8AAyFhAAMhYwADIWQAAyFlAAMhZwADIWgAAyGnAAMhqQADIasAAyGtAAMhsAADIbEAAyGyAAMhswADIbQAAyG2AAMhuAADIbkAAyG6AAMhvAADIb0AAyH8AAMh/gADIgEAAyIDAAMiBgADIgcAAyIIAAMiCQADIgoAAyIMAAMiDgADIg8AAyIQAAMiEgADIhMAAyIgAAMiIQADIiIAAyIkAAMiYwADImUAAyJnAAMiaQADImwAAyJtAAMibgADIm8AAyJwAAMicgADInQAAyJ1AAMidgADIngAAyJ5AAMiuAADIroAAyK8AAMivgADIsEAAyLCAAMiwwADIsQAAyLFAAMixwADIskAAyLKAAMiywADIs0AAyLOAAMjDQADIw8AAyMRAAMjEwADIxYAAyMXAAMjGAADIxkAAyMaAAMjHAADIx4AAyMfAAMjIAADIyIAAyMjAAMjYgADI2QAAyNmAAMjaAADI2sAAyNsAAMjbQADI24AAyNvAAMjcQADI3MAAyN0AAMjdQADI3cAAyN4AAMjtwADI7kAAyO7AAMjvQADI8AAAyPBAAMjwgADI8MAAyPEAAMjxgADI8gAAyPJAAMjygADI8wAAyPNAAMj8gADJBYAAyQ9AAMkYQADJGQAAyRmAAMkaAADJGoAAyRsAAMkbgADJG8AAyRyAAMkfwADJI4AAySQAAMkkgADJJQAAySWAAMkmAADJJoAAyScAAMkqwADJK4AAySxAAMktAADJLcAAyS6AAMkvQADJMAAAyTCAAMlAQADJQMAAyUFAAMlBwADJQoAAyULAAMlDAADJQ0AAyUOAAMlEAADJRIAAyUTAAMlFAADJRYAAyUXAAMlVgADJVgAAyVaAAMlXAADJV8AAyVgAAMlYQADJWIAAyVjAAMlZQADJWcAAyVoAAMlaQADJWsAAyVsAAMlqwADJa0AAyWvAAMlsQADJbQAAyW1AAMltgADJbcAAyW4AAMlugADJbwAAyW9AAMlvgADJcAAAyXBAAMmAAADJgIAAyYEAAMmBgADJgkAAyYKAAMmCwADJgwAAyYNAAMmDwADJhEAAyYSAAMmEwADJhUAAyYWAAMmVQADJlcAAyZZAAMmWwADJl4AAyZfAAMmYAADJmEAAyZiAAMmZAADJmYAAyZnAAMmaAADJmoAAyZrAAMmqgADJqwAAyauAAMmsAADJrMAAya0AAMmtQADJrYAAya3AAMmuQADJrsAAya8AAMmvQADJr8AAybAAAMm/wADJwEAAycDAAMnBQADJwgAAycJAAMnCgADJwsAAycMAAMnDgADJxAAAycRAAMnEgADJxQAAycVAAMnHgADJx8AAychAAMnZAADJ4gAAyesAAMnzwADJ/YAAygWAAMoPQADKGQAAyiEAAMoqAADKMwAAyjOAAMo0QADKNMAAyjVAAMo1wADKNoAAyjdAAMo3wADKOEAAyjkAAMo5gADKOgAAyjrAAMo7gADKO8AAyj0AAMpAQADKQQAAykGAAMpCQADKQwAAykOAAMpMwADKVcAAyl+AAMpogADKaUAAymnAAMpqQADKasAAymtAAMprwADKbAAAymzAAMpwAADKdMAAynVAAMp1wADKdkAAynbAAMp3QADKd8AAynhAAMp4wADKeUAAyn4AAMp+wADKf4AAyoBAAMqBAADKgcAAyoKAAMqDQADKhAAAyoTAAMqFQADKlQAAypWAAMqWQADKlsAAypeAAMqXwADKmAAAyphAAMqYgADKmQAAypmAAMqZwADKmgAAypqAAMqawADKnQAAyp1AAMqdwADKrYAAyq4AAMqugADKrwAAyq/AAMqwAADKsEAAyrCAAMqwwADKsUAAyrHAAMqyAADKskAAyrLAAMqzAADKwsAAysNAAMrEAADKxIAAysVAAMrFgADKxcAAysYAAMrGQADKxsAAysdAAMrHgADKx8AAyshAAMrIgADKysAAyssAAMrLgADK20AAytvAAMrcQADK3MAAyt2AAMrdwADK3gAAyt5AAMregADK3wAAyt+AAMrfwADK4AAAyuCAAMrgwADK8IAAyvEAAMrxwADK8kAAyvMAAMrzQADK84AAyvPAAMr0AADK9IAAyvUAAMr1QADK9YAAyvYAAMr2QADK+IAAyvjAAMr5QADLCQAAywmAAMsKAADLCoAAywtAAMsLgADLC8AAywwAAMsMQADLDMAAyw1AAMsNgADLDcAAyw5AAMsOgADLHkAAyx7AAMsfgADLIAAAyyDAAMshAADLIUAAyyGAAMshwADLIkAAyyLAAMsjAADLI0AAyyPAAMskAADLJ0AAyyeAAMsnwADLKEAAyzgAAMs4gADLOQAAyzmAAMs6QADLOoAAyzrAAMs7AADLO0AAyzvAAMs8QADLPIAAyzzAAMs9QADLPYAAy01AAMtNwADLToAAy08AAMtPwADLUAAAy1BAAMtQgADLUMAAy1FAAMtRwADLUgAAy1JAAMtSwADLUwAAy1nAAMtdAADLYsAAy2OAAMtkQADLZQAAy2XAAMtmgADLZ0AAy2gAAMtowADLaYAAy2pAAMtrAADLcMAAy3GAAMtyQADLcwAAy3PAAMt0gADLdUAAy3YAAMt2wADLd4AAy3hAAMt5AADLeYAAy3tAAMt9AADLf0AAy4CAAMuDQADLhkAAy4iAAMuJgADLjAAAy44AAMugwADLqYAAy7GAAMu5gADLugAAy7qAAMu7AADLu4AAy7xAAMu8gADLvMAAy72AAMu9wADLvkAAy76AAMu/AADLv8AAy8AAAMvAQADLwQAAy8FAAMvCgADLxcAAy8cAAMvHgADLyAAAy8lAAMvKAADLysAAy8tAAMvUgADL3YAAy+dAAMvwQADL8QAAy/GAAMvyAADL8oAAy/MAAMvzgADL88AAy/SAAMv3wADL/AAAy/yAAMv9AADL/YAAy/4AAMv+gADL/wAAy/+AAMwAAADMBEAAzAUAAMwFwADMBoAAzAdAAMwIAADMCMAAzAmAAMwKQADMCsAAzBqAAMwbAADMG4AAzBwAAMwcwADMHQAAzB1AAMwdgADMHcAAzB5AAMwewADMHwAAzB9AAMwfwADMIAAAzC/AAMwwQADMMMAAzDFAAMwyAADMMkAAzDKAAMwywADMMwAAzDOAAMw0AADMNEAAzDSAAMw1AADMNUAAzEUAAMxFgADMRkAAzEbAAMxHgADMR8AAzEgAAMxIQADMSIAAzEkAAMxJgADMScAAzEoAAMxKgADMSsAAzE4AAMxOQADMToAAzE8AAMxewADMX0AAzF/AAMxgQADMYQAAzGFAAMxhgADMYcAAzGIAAMxigADMYwAAzGNAAMxjgADMZAAAzGRAAMx0AADMdIAAzHUAAMx1gADMdkAAzHaAAMx2wADMdwAAzHdAAMx3wADMeEAAzHiAAMx4wADMeUAAzHmAAMyJQADMicAAzIpAAMyKwADMi4AAzIvAAMyMAADMjEAAzIyAAMyNAADMjYAAzI3AAMyOAADMjoAAzI7AAMyegADMnwAAzJ+AAMygAADMoMAAzKEAAMyhQADMoYAAzKHAAMyiQADMosAAzKMAAMyjQADMo8AAzKQAAMyzwADMtEAAzLTAAMy1QADMtgAAzLZAAMy2gADMtsAAzLcAAMy3gADMuAAAzLhAAMy4gADMuQAAzLlAAMzCgADMy4AAzNVAAMzeQADM3wAAzN+AAMzgAADM4IAAzOEAAMzhgADM4cAAzOKAAMzlwADM6YAAzOoAAMzqgADM6wAAzOuAAMzsAADM7IAAzO0AAMzwwADM8YAAzPJAAMzzAADM88AAzPSAAMz1QADM9gAAzPaAAM0GQADNBsAAzQdAAM0HwADNCIAAzQjAAM0JAADNCUAAzQmAAM0KAADNCoAAzQrAAM0LAADNC4AAzQvAAM0bgADNHAAAzRyAAM0dAADNHcAAzR4AAM0eQADNHoAAzR7AAM0fQADNH8AAzSAAAM0gQADNIMAAzSEAAM0wwADNMUAAzTHAAM0yQADNMwAAzTNAAM0zgADNM8AAzTQAAM00gADNNQAAzTVAAM01gADNNgAAzTZAAM1GAADNRoAAzUcAAM1HgADNSEAAzUiAAM1IwADNSQAAzUlAAM1JwADNSkAAzUqAAM1KwADNS0AAzUuAAM1bQADNW8AAzVxAAM1cwADNXYAAzV3AAM1eAADNXkAAzV6AAM1fAADNX4AAzV/AAM1gAADNYIAAzWDAAM1wgADNcQAAzXGAAM1yAADNcsAAzXMAAM1zQADNc4AAzXPAAM10QADNdMAAzXUAAM11QADNdcAAzXYAAM2FwADNhkAAzYbAAM2HQADNiAAAzYhAAM2IgADNiMAAzYkAAM2JgADNigAAzYpAAM2KgADNiwAAzYtAAM2eAADNpsAAza7AAM22wADNt0AAzbfAAM24QADNuMAAzbmAAM25wADNugAAzbrAAM27AADNu4AAzbvAAM28QADNvQAAzb1AAM29gADNvkAAzb6AAM3AwADNxAAAzcVAAM3FwADNxkAAzceAAM3IQADNyQAAzcmAAM3SwADN28AAzeWAAM3ugADN70AAze/AAM3wQADN8MAAzfFAAM3xwADN8gAAzfLAAM32AADN+kAAzfrAAM37QADN+8AAzfxAAM38wADN/UAAzf3AAM3+QADOAoAAzgNAAM4EAADOBMAAzgWAAM4GQADOBwAAzgfAAM4IgADOCQAAzhjAAM4ZQADOGcAAzhpAAM4bAADOG0AAzhuAAM4bwADOHAAAzhyAAM4dAADOHUAAzh2AAM4eAADOHkAAzi4AAM4ugADOLwAAzi+AAM4wQADOMIAAzjDAAM4xAADOMUAAzjHAAM4yQADOMoAAzjLAAM4zQADOM4AAzkNAAM5DwADORIAAzkUAAM5FwADORgAAzkZAAM5GgADORsAAzkdAAM5HwADOSAAAzkhAAM5IwADOSQAAzkxAAM5MgADOTMAAzk1AAM5dAADOXYAAzl4AAM5egADOX0AAzl+AAM5fwADOYAAAzmBAAM5gwADOYUAAzmGAAM5hwADOYkAAzmKAAM5yQADOcsAAznNAAM5zwADOdIAAznTAAM51AADOdUAAznWAAM52AADOdoAAznbAAM53AADOd4AAznfAAM6HgADOiAAAzoiAAM6JAADOicAAzooAAM6KQADOioAAzorAAM6LQADOi8AAzowAAM6MQADOjMAAzo0AAM6cwADOnUAAzp3AAM6eQADOnwAAzp9AAM6fgADOn8AAzqAAAM6ggADOoQAAzqFAAM6hgADOogAAzqJAAM6yAADOsoAAzrMAAM6zgADOtEAAzrSAAM60wADOtQAAzrVAAM61wADOtkAAzraAAM62wADOt0AAzreAAM7AwADOycAAztOAAM7cgADO3UAAzt3AAM7eQADO3sAAzt9AAM7fwADO4AAAzuDAAM7kAADO58AAzuhAAM7owADO6UAAzunAAM7qQADO6sAAzutAAM7vAADO78AAzvCAAM7xQADO8gAAzvLAAM7zgADO9EAAzvTAAM8EgADPBQAAzwWAAM8GAADPBsAAzwcAAM8HQADPB4AAzwfAAM8IQADPCMAAzwkAAM8JQADPCcAAzwoAAM8ZwADPGkAAzxrAAM8bQADPHAAAzxxAAM8cgADPHMAAzx0AAM8dgADPHgAAzx5AAM8egADPHwAAzx9AAM8vAADPL4AAzzAAAM8wgADPMUAAzzGAAM8xwADPMgAAzzJAAM8ywADPM0AAzzOAAM8zwADPNEAAzzSAAM9EQADPRMAAz0VAAM9FwADPRoAAz0bAAM9HAADPR0AAz0eAAM9IAADPSIAAz0jAAM9JAADPSYAAz0nAAM9ZgADPWgAAz1qAAM9bAADPW8AAz1wAAM9cQADPXIAAz1zAAM9dQADPXcAAz14AAM9eQADPXsAAz18AAM9uwADPb0AAz2/AAM9wQADPcQAAz3FAAM9xgADPccAAz3IAAM9ygADPcwAAz3NAAM9zgADPdAAAz3RAAM+EAADPhIAAz4UAAM+FgADPhkAAz4aAAM+GwADPhwAAz4dAAM+HwADPiEAAz4iAAM+IwADPiUAAz4mAAM+cQADPpQAAz60AAM+1AADPtYAAz7YAAM+2gADPtwAAz7fAAM+4AADPuEAAz7kAAM+5QADPucAAz7oAAM+6gADPu0AAz7uAAM+7wADPvIAAz7zAAM+/AADPwkAAz8OAAM/EAADPxIAAz8XAAM/GgADPx0AAz8fAAM/RAADP2gAAz+PAAM/swADP7YAAz+4AAM/ugADP7wAAz++AAM/wAADP8EAAz/EAAM/0QADP+IAAz/kAAM/5gADP+gAAz/qAAM/7AADP+4AAz/wAAM/8gADQAMAA0AGAANACQADQAwAA0APAANAEgADQBUAA0AYAANAGwADQB0AA0BcAANAXgADQGAAA0BiAANAZQADQGYAA0BnAANAaAADQGkAA0BrAANAbQADQG4AA0BvAANAcQADQHIAA0CxAANAswADQLUAA0C3AANAugADQLsAA0C8AANAvQADQL4AA0DAAANAwgADQMMAA0DEAANAxgADQMcAA0EGAANBCAADQQsAA0ENAANBEAADQREAA0ESAANBEwADQRQAA0EWAANBGAADQRkAA0EaAANBHAADQR0AA0EqAANBKwADQSwAA0EuAANBbQADQW8AA0FxAANBcwADQXYAA0F3AANBeAADQXkAA0F6AANBfAADQX4AA0F/AANBgAADQYIAA0GDAANBwgADQcQAA0HGAANByAADQcsAA0HMAANBzQADQc4AA0HPAANB0QADQdMAA0HUAANB1QADQdcAA0HYAANCFwADQhkAA0IbAANCHQADQiAAA0IhAANCIgADQiMAA0IkAANCJgADQigAA0IpAANCKgADQiwAA0ItAANCbAADQm4AA0JwAANCcgADQnUAA0J2AANCdwADQngAA0J5AANCewADQn0AA0J+AANCfwADQoEAA0KCAANCwQADQsMAA0LFAANCxwADQsoAA0LLAANCzAADQs0AA0LOAANC0AADQtIAA0LTAANC1AADQtYAA0LXAANC/AADQyAAA0NHAANDawADQ24AA0NwAANDcgADQ3QAA0N2AANDeAADQ3kAA0N8AANDiQADQ5gAA0OaAANDnAADQ54AA0OgAANDogADQ6QAA0OmAANDtQADQ7gAA0O7AANDvgADQ8EAA0PEAANDxwADQ8oAA0PMAANECwADRA0AA0QPAANEEQADRBQAA0QVAANEFgADRBcAA0QYAANEGgADRBwAA0QdAANEHgADRCAAA0QhAANEYAADRGIAA0RkAANEZgADRGkAA0RqAANEawADRGwAA0RtAANEbwADRHEAA0RyAANEcwADRHUAA0R2AANEtQADRLcAA0S5AANEuwADRL4AA0S/AANEwAADRMEAA0TCAANExAADRMYAA0THAANEyAADRMoAA0TLAANFCgADRQwAA0UOAANFEAADRRMAA0UUAANFFQADRRYAA0UXAANFGQADRRsAA0UcAANFHQADRR8AA0UgAANFXwADRWEAA0VjAANFZQADRWgAA0VpAANFagADRWsAA0VsAANFbgADRXAAA0VxAANFcgADRXQAA0V1AANFtAADRbYAA0W4AANFugADRb0AA0W+AANFvwADRcAAA0XBAANFwwADRcUAA0XGAANFxwADRckAA0XKAANGCQADRgsAA0YNAANGDwADRhIAA0YTAANGFAADRhUAA0YWAANGGAADRhoAA0YbAANGHAADRh4AA0YfAANGagADRo0AA0atAANGzQADRs8AA0bRAANG0wADRtUAA0bYAANG2QADRtoAA0bdAANG3gADRuAAA0bhAANG4wADRuYAA0bnAANG6AADRusAA0bsAANG8QADRv4AA0cDAANHBQADRwcAA0cMAANHDwADRxIAA0cUAANHOQADR10AA0eEAANHqAADR6sAA0etAANHrwADR7EAA0ezAANHtQADR7YAA0e5AANHxgADR9cAA0fZAANH2wADR90AA0ffAANH4QADR+MAA0flAANH5wADR/gAA0f7AANH/gADSAEAA0gEAANIBwADSAoAA0gNAANIEAADSBIAA0hRAANIUwADSFUAA0hXAANIWgADSFsAA0hcAANIXQADSF4AA0hgAANIYgADSGMAA0hkAANIZgADSGcAA0imAANIqAADSKoAA0isAANIrwADSLAAA0ixAANIsgADSLMAA0i1AANItwADSLgAA0i5AANIuwADSLwAA0j7AANI/QADSQAAA0kCAANJBQADSQYAA0kHAANJCAADSQkAA0kLAANJDQADSQ4AA0kPAANJEQADSRIAA0kfAANJIAADSSEAA0kjAANJYgADSWQAA0lmAANJaAADSWsAA0lsAANJbQADSW4AA0lvAANJcQADSXMAA0l0AANJdQADSXcAA0l4AANJtwADSbkAA0m7AANJvQADScAAA0nBAANJwgADScMAA0nEAANJxgADScgAA0nJAANJygADScwAA0nNAANKDAADSg4AA0oQAANKEgADShUAA0oWAANKFwADShgAA0oZAANKGwADSh0AA0oeAANKHwADSiEAA0oiAANKYQADSmMAA0plAANKZwADSmoAA0prAANKbAADSm0AA0puAANKcAADSnIAA0pzAANKdAADSnYAA0p3AANKtgADSrgAA0q6AANKvAADSr8AA0rAAANKwQADSsIAA0rDAANKxQADSscAA0rIAANKyQADSssAA0rMAANK8QADSxUAA0s8AANLYAADS2MAA0tlAANLZwADS2kAA0trAANLbQADS24AA0txAANLfgADS40AA0uPAANLkQADS5MAA0uVAANLlwADS5kAA0ubAANLqgADS60AA0uwAANLswADS7YAA0u5AANLvAADS78AA0vBAANMAAADTAIAA0wEAANMBgADTAkAA0wKAANMCwADTAwAA0wNAANMDwADTBEAA0wSAANMEwADTBUAA0wWAANMVQADTFcAA0xZAANMWwADTF4AA0xfAANMYAADTGEAA0xiAANMZAADTGYAA0xnAANMaAADTGoAA0xrAANMqgADTKwAA0yuAANMsAADTLMAA0y0AANMtQADTLYAA0y3AANMuQADTLsAA0y8AANMvQADTL8AA0zAAANM/wADTQEAA00EAANNBgADTQkAA00KAANNCwADTQwAA00NAANNDwADTREAA00SAANNEwADTRUAA00WAANNVQADTVcAA01ZAANNWwADTV4AA01fAANNYAADTWEAA01iAANNZAADTWYAA01nAANNaAADTWoAA01rAANNqgADTawAA02uAANNsAADTbMAA020AANNtQADTbYAA023AANNuQADTbsAA028AANNvQADTb8AA03AAANN/wADTgEAA04DAANOBQADTggAA04JAANOCgADTgsAA04MAANODgADThAAA04RAANOEgADThQAA04VAANOYAADToMAA06jAANOwwADTsUAA07HAANOyQADTssAA07OAANOzwADTtAAA07TAANO1AADTtYAA07XAANO2QADTtwAA07dAANO3gADTuEAA07iAANO5wADTvQAA075AANO+wADTv0AA08CAANPBQADTwgAA08KAANPLwADT1MAA096AANPngADT6EAA0+jAANPpQADT6cAA0+pAANPqwADT6wAA0+vAANPvAADT80AA0/PAANP0QADT9MAA0/VAANP1wADT9kAA0/bAANP3QADT+4AA0/xAANP9AADT/cAA0/6AANP/QADUAAAA1ADAANQBgADUAgAA1BHAANQSQADUEsAA1BNAANQUAADUFEAA1BSAANQUwADUFQAA1BWAANQWAADUFkAA1BaAANQXAADUF0AA1CcAANQngADUKAAA1CiAANQpQADUKYAA1CnAANQqAADUKkAA1CrAANQrQADUK4AA1CvAANQsQADULIAA1DxAANQ8wADUPYAA1D4AANQ+wADUPwAA1D9AANQ/gADUP8AA1EBAANRAwADUQQAA1EFAANRBwADUQgAA1EVAANRFgADURcAA1EZAANRWAADUVoAA1FcAANRXgADUWEAA1FiAANRYwADUWQAA1FlAANRZwADUWkAA1FqAANRawADUW0AA1FuAANRrQADUa8AA1GxAANRswADUbYAA1G3AANRuAADUbkAA1G6AANRvAADUb4AA1G/AANRwAADUcIAA1HDAANSAgADUgQAA1IGAANSCAADUgsAA1IMAANSDQADUg4AA1IPAANSEQADUhMAA1IUAANSFQADUhcAA1IYAANSVwADUlkAA1JbAANSXQADUmAAA1JhAANSYgADUmMAA1JkAANSZgADUmgAA1JpAANSagADUmwAA1JtAANSrAADUq4AA1KwAANSsgADUrUAA1K2AANStwADUrgAA1K5AANSuwADUr0AA1K+AANSvwADUsEAA1LCAANS5wADUwsAA1MyAANTVgADU1kAA1NbAANTXQADU18AA1NhAANTYwADU2QAA1NnAANTdAADU4MAA1OFAANThwADU4kAA1OLAANTjQADU48AA1ORAANToAADU6MAA1OmAANTqQADU6wAA1OvAANTsgADU7UAA1O3AANT9gADU/gAA1P6AANT/AADU/8AA1QAAANUAQADVAIAA1QDAANUBQADVAcAA1QIAANUCQADVAsAA1QMAANUSwADVE0AA1RPAANUUQADVFQAA1RVAANUVgADVFcAA1RYAANUWgADVFwAA1RdAANUXgADVGAAA1RhAANUoAADVKIAA1SkAANUpgADVKkAA1SqAANUqwADVKwAA1StAANUrwADVLEAA1SyAANUswADVLUAA1S2AANU9QADVPcAA1T5AANU+wADVP4AA1T/AANVAAADVQEAA1UCAANVBAADVQYAA1UHAANVCAADVQoAA1ULAANVSgADVUwAA1VOAANVUAADVVMAA1VUAANVVQADVVYAA1VXAANVWQADVVsAA1VcAANVXQADVV8AA1VgAANVnwADVaEAA1WjAANVpQADVagAA1WpAANVqgADVasAA1WsAANVrgADVbAAA1WxAANVsgADVbQAA1W1AANV9AADVfYAA1X4AANV+gADVf0AA1X+AANV/wADVgAAA1YBAANWAwADVgUAA1YGAANWBwADVgkAA1YKAANWVQADVngAA1aYAANWuAADVroAA1a8AANWvgADVsAAA1bDAANWxAADVsUAA1bIAANWyQADVssAA1bMAANWzgADVtEAA1bSAANW0wADVtYAA1bXAANW3AADVukAA1buAANW8AADVvIAA1b3AANW+gADVv0AA1b/AANXJAADV0gAA1dvAANXkwADV5YAA1eYAANXmgADV5wAA1eeAANXoAADV6EAA1ekAANXsQADV8IAA1fEAANXxgADV8gAA1fKAANXzAADV84AA1fQAANX0gADV+MAA1fmAANX6QADV+wAA1fvAANX8gADV/UAA1f4AANX+wADV/0AA1g8AANYPgADWEAAA1hCAANYRQADWEYAA1hHAANYSAADWEkAA1hLAANYTQADWE4AA1hPAANYUQADWFIAA1iRAANYkwADWJUAA1iXAANYmgADWJsAA1icAANYnQADWJ4AA1igAANYogADWKMAA1ikAANYpgADWKcAA1jmAANY6AADWOsAA1jtAANY8AADWPEAA1jyAANY8wADWPQAA1j2AANY+AADWPkAA1j6AANY/AADWP0AA1kKAANZCwADWQwAA1kOAANZTQADWU8AA1lRAANZUwADWVYAA1lXAANZWAADWVkAA1laAANZXAADWV4AA1lfAANZYAADWWIAA1ljAANZogADWaQAA1mmAANZqAADWasAA1msAANZrQADWa4AA1mvAANZsQADWbMAA1m0AANZtQADWbcAA1m4AANZ9wADWfkAA1n7AANZ/QADWgAAA1oBAANaAgADWgMAA1oEAANaBgADWggAA1oJAANaCgADWgwAA1oNAANaTAADWk4AA1pQAANaUgADWlUAA1pWAANaVwADWlgAA1pZAANaWwADWl0AA1peAANaXwADWmEAA1piAANaoQADWqMAA1qlAANapwADWqoAA1qrAANarAADWq0AA1quAANasAADWrIAA1qzAANatAADWrYAA1q3AANa3AADWwAAA1snAANbSwADW04AA1tQAANbUgADW1QAA1tWAANbWAADW1kAA1tcAANbaQADW3gAA1t6AANbfAADW34AA1uAAANbggADW4QAA1uGAANblQADW5gAA1ubAANbngADW6EAA1ukAANbpwADW6oAA1usAANb6wADW+0AA1vvAANb8QADW/QAA1v1AANb9gADW/cAA1v4AANb+gADW/wAA1v9AANb/gADXAAAA1wBAANcQAADXEIAA1xEAANcRgADXEkAA1xKAANcSwADXEwAA1xNAANcTwADXFEAA1xSAANcUwADXFUAA1xWAANclQADXJcAA1yZAANcmwADXJ4AA1yfAANcoAADXKEAA1yiAANcpAADXKYAA1ynAANcqAADXKoAA1yrAANc6gADXOwAA1zvAANc8QADXPQAA1z1AANc9gADXPcAA1z4AANc+gADXPwAA1z9AANc/gADXQAAA10BAANdQAADXUIAA11EAANdRgADXUkAA11KAANdSwADXUwAA11NAANdTwADXVEAA11SAANdUwADXVUAA11WAANdlQADXZcAA12ZAANdmwADXZ4AA12fAANdoAADXaEAA12iAANdpAADXaYAA12nAANdqAADXaoAA12rAANd6gADXewAA13uAANd8AADXfMAA130AANd9QADXfYAA133AANd+QADXfsAA138AANd/QADXf8AA14AAANeSwADXm4AA16OAANergADXrAAA16yAANetAADXrYAA165AANeugADXrsAA16+AANevwADXsEAA17CAANexAADXscAA17IAANeyQADXswAA17NAANe0gADXt8AA17kAANe5gADXugAA17tAANe8AADXvMAA171AANfGgADXz4AA19lAANfiQADX4wAA1+OAANfkAADX5IAA1+UAANflgADX5cAA1+aAANfpwADX7gAA1+6AANfvAADX74AA1/AAANfwgADX8QAA1/GAANfyAADX9kAA1/cAANf3wADX+IAA1/lAANf6AADX+sAA1/uAANf8QADX/MAA2AyAANgNAADYDYAA2A4AANgOwADYDwAA2A9AANgPgADYD8AA2BBAANgQwADYEQAA2BFAANgRwADYEgAA2CHAANgiQADYIsAA2CNAANgkAADYJEAA2CSAANgkwADYJQAA2CWAANgmAADYJkAA2CaAANgnAADYJ0AA2DcAANg3gADYOEAA2DjAANg5gADYOcAA2DoAANg6QADYOoAA2DsAANg7gADYO8AA2DwAANg8gADYPMAA2EAAANhAQADYQIAA2EEAANhQwADYUUAA2FHAANhSQADYUwAA2FNAANhTgADYU8AA2FQAANhUgADYVQAA2FVAANhVgADYVgAA2FZAANhmAADYZoAA2GcAANhngADYaEAA2GiAANhowADYaQAA2GlAANhpwADYakAA2GqAANhqwADYa0AA2GuAANh7QADYe8AA2HxAANh8wADYfYAA2H3AANh+AADYfkAA2H6AANh/AADYf4AA2H/AANiAAADYgIAA2IDAANiQgADYkQAA2JGAANiSAADYksAA2JMAANiTQADYk4AA2JPAANiUQADYlMAA2JUAANiVQADYlcAA2JYAANilwADYpkAA2KbAANinQADYqAAA2KhAANiogADYqMAA2KkAANipgADYqgAA2KpAANiqgADYqwAA2KtAANi0gADYvYAA2MdAANjQQADY0QAA2NGAANjSAADY0oAA2NMAANjTgADY08AA2NSAANjXwADY24AA2NwAANjcgADY3QAA2N2AANjeAADY3oAA2N8AANjiwADY44AA2ORAANjlAADY5cAA2OaAANjnQADY6AAA2OiAANj4QADY+MAA2PlAANj5wADY+oAA2PrAANj7AADY+0AA2PuAANj8AADY/IAA2PzAANj9AADY/YAA2P3AANkNgADZDgAA2Q6AANkPAADZD8AA2RAAANkQQADZEIAA2RDAANkRQADZEcAA2RIAANkSQADZEsAA2RMAANkiwADZI0AA2SPAANkkQADZJQAA2SVAANklgADZJcAA2SYAANkmgADZJwAA2SdAANkngADZKAAA2ShAANk4AADZOIAA2TkAANk5gADZOkAA2TqAANk6wADZOwAA2TtAANk7wADZPEAA2TyAANk8wADZPUAA2T2AANlNQADZTcAA2U5AANlOwADZT4AA2U/AANlQAADZUEAA2VCAANlRAADZUYAA2VHAANlSAADZUoAA2VLAANligADZYwAA2WOAANlkAADZZMAA2WUAANllQADZZYAA2WXAANlmQADZZsAA2WcAANlnQADZZ8AA2WgAANl3wADZeEAA2XjAANl5QADZegAA2XpAANl6gADZesAA2XsAANl7gADZfAAA2XxAANl8gADZfQAA2X1AANmQAADZmMAA2aDAANmowADZqUAA2anAANmqQADZqsAA2auAANmrwADZrAAA2azAANmtAADZrYAA2a3AANmuQADZrwAA2a9AANmvgADZsEAA2bCAANmxwADZtQAA2bZAANm2wADZt0AA2biAANm5QADZugAA2bqAANnDwADZzMAA2daAANnfgADZ4EAA2eDAANnhQADZ4cAA2eJAANniwADZ4wAA2ePAANnnAADZ60AA2evAANnsQADZ7MAA2e1AANntwADZ7kAA2e7AANnvQADZ84AA2fRAANn1AADZ9cAA2faAANn3QADZ+AAA2fjAANn5gADZ+gAA2gnAANoKQADaCsAA2gtAANoMAADaDEAA2gyAANoMwADaDQAA2g2AANoOAADaDkAA2g6AANoPAADaD0AA2h8AANofgADaIAAA2iCAANohQADaIYAA2iHAANoiAADaIkAA2iLAANojQADaI4AA2iPAANokQADaJIAA2jRAANo0wADaNYAA2jYAANo2wADaNwAA2jdAANo3gADaN8AA2jhAANo4wADaOQAA2jlAANo5wADaOgAA2j1AANo9gADaPcAA2j5AANpOAADaToAA2k8AANpPgADaUEAA2lCAANpQwADaUQAA2lFAANpRwADaUkAA2lKAANpSwADaU0AA2lOAANpjQADaY8AA2mRAANpkwADaZYAA2mXAANpmAADaZkAA2maAANpnAADaZ4AA2mfAANpoAADaaIAA2mjAANp4gADaeQAA2nmAANp6AADaesAA2nsAANp7QADae4AA2nvAANp8QADafMAA2n0AANp9QADafcAA2n4AANqNwADajkAA2o7AANqPQADakAAA2pBAANqQgADakMAA2pEAANqRgADakgAA2pJAANqSgADakwAA2pNAANqjAADao4AA2qQAANqkgADapUAA2qWAANqlwADapgAA2qZAANqmwADap0AA2qeAANqnwADaqEAA2qiAANqxwADausAA2sSAANrNgADazkAA2s7AANrPQADaz8AA2tBAANrQwADa0QAA2tHAANrVAADa2MAA2tlAANrZwADa2kAA2trAANrbQADa28AA2txAANrgAADa4MAA2uGAANriQADa4wAA2uPAANrkgADa5UAA2uXAANr1gADa9gAA2vaAANr3AADa98AA2vgAANr4QADa+IAA2vjAANr5QADa+cAA2voAANr6QADa+sAA2vsAANsKwADbC0AA2wvAANsMQADbDQAA2w1AANsNgADbDcAA2w4AANsOgADbDwAA2w9AANsPgADbEAAA2xBAANsgAADbIIAA2yEAANshgADbIkAA2yKAANsiwADbIwAA2yNAANsjwADbJEAA2ySAANskwADbJUAA2yWAANs1QADbNcAA2zZAANs2wADbN4AA2zfAANs4AADbOEAA2ziAANs5AADbOYAA2znAANs6AADbOoAA2zrAANtKgADbSwAA20uAANtMAADbTMAA200AANtNQADbTYAA203AANtOQADbTsAA208AANtPQADbT8AA21AAANtfwADbYEAA22DAANthQADbYgAA22JAANtigADbYsAA22MAANtjgADbZAAA22RAANtkgADbZQAA22VAANt1AADbdYAA23YAANt2gADbd0AA23eAANt3wADbeAAA23hAANt4wADbeUAA23mAANt5wADbekAA23qAANuNQADblgAA254AANumAADbpoAA26cAANungADbqAAA26jAANupAADbqUAA26oAANuqQADbqsAA26sAANurgADbrEAA26yAANuswADbrYAA263AANuvAADbskAA27OAANu0AADbtIAA27XAANu2gADbt0AA27fAANvBAADbygAA29PAANvcwADb3YAA294AANvegADb3wAA29+AANvgAADb4EAA2+EAANvkQADb6IAA2+kAANvpgADb6gAA2+qAANvrAADb64AA2+wAANvsgADb8MAA2/GAANvyQADb8wAA2/PAANv0gADb9UAA2/YAANv2wADb90AA3AcAANwHgADcCAAA3AiAANwJQADcCYAA3AnAANwKAADcCkAA3ArAANwLQADcC4AA3AvAANwMQADcDIAA3BxAANwcwADcHUAA3B3AANwegADcHsAA3B8AANwfQADcH4AA3CAAANwggADcIMAA3CEAANwhgADcIcAA3DGAANwyAADcMsAA3DNAANw0AADcNEAA3DSAANw0wADcNQAA3DWAANw2AADcNkAA3DaAANw3AADcN0AA3DqAANw6wADcOwAA3DuAANxLQADcS8AA3ExAANxMwADcTYAA3E3AANxOAADcTkAA3E6AANxPAADcT4AA3E/AANxQAADcUIAA3FDAANxggADcYQAA3GGAANxiAADcYsAA3GMAANxjQADcY4AA3GPAANxkQADcZMAA3GUAANxlQADcZcAA3GYAANx1wADcdkAA3HbAANx3QADceAAA3HhAANx4gADceMAA3HkAANx5gADcegAA3HpAANx6gADcewAA3HtAANyLAADci4AA3IwAANyMgADcjUAA3I2AANyNwADcjgAA3I5AANyOwADcj0AA3I+AANyPwADckEAA3JCAANygQADcoMAA3KFAANyhwADcooAA3KLAANyjAADco0AA3KOAANykAADcpIAA3KTAANylAADcpYAA3KXAANyvAADcuAAA3MHAANzKwADcy4AA3MwAANzMgADczQAA3M2AANzOAADczkAA3M8AANzSQADc1gAA3NaAANzXAADc14AA3NgAANzYgADc2QAA3NmAANzdQADc3gAA3N7AANzfgADc4EAA3OEAANzhwADc4oAA3OMAANzywADc80AA3PPAANz0QADc9QAA3PVAANz1gADc9cAA3PYAANz2gADc9wAA3PdAANz3gADc+AAA3PhAAN0IAADdCIAA3QkAAN0JgADdCkAA3QqAAN0KwADdCwAA3QtAAN0LwADdDEAA3QyAAN0MwADdDUAA3Q2AAN0dQADdHcAA3R5AAN0ewADdH4AA3R/AAN0gAADdIEAA3SCAAN0hAADdIYAA3SHAAN0iAADdIoAA3SLAAN0ygADdMwAA3TOAAN00AADdNMAA3TUAAN01QADdNYAA3TXAAN02QADdNsAA3TcAAN03QADdN8AA3TgAAN1HwADdSEAA3UjAAN1JQADdSgAA3UpAAN1KgADdSsAA3UsAAN1LgADdTAAA3UxAAN1MgADdTQAA3U1AAN1dAADdXYAA3V4AAN1egADdX0AA3V+AAN1fwADdYAAA3WBAAN1gwADdYUAA3WGAAN1hwADdYkAA3WKAAN1yQADdcsAA3XNAAN1zwADddIAA3XTAAN11AADddUAA3XWAAN12AADddoAA3XbAAN13AADdd4AA3XfAAN2KgADdk0AA3ZtAAN2jQADdo8AA3aRAAN2kwADdpUAA3aYAAN2mQADdpoAA3adAAN2ngADdqAAA3ahAAN2owADdqYAA3anAAN2qAADdqsAA3asAAN2sQADdr4AA3bDAAN2xQADdscAA3bMAAN2zwADdtIAA3bUAAN2+QADdx0AA3dEAAN3aAADd2sAA3dtAAN3bwADd3EAA3dzAAN3dQADd3YAA3d5AAN3hgADd5cAA3eZAAN3mwADd50AA3efAAN3oQADd6MAA3elAAN3pwADd7gAA3e7AAN3vgADd8EAA3fEAAN3xwADd8oAA3fNAAN30AADd9IAA3gRAAN4EwADeBUAA3gXAAN4GgADeBsAA3gcAAN4HQADeB4AA3ggAAN4IgADeCMAA3gkAAN4JgADeCcAA3hmAAN4aAADeGoAA3hsAAN4bwADeHAAA3hxAAN4cgADeHMAA3h1AAN4dwADeHgAA3h5AAN4ewADeHwAA3i7AAN4vQADeMAAA3jCAAN4xQADeMYAA3jHAAN4yAADeMkAA3jLAAN4zQADeM4AA3jPAAN40QADeNIAA3jfAAN44AADeOEAA3jjAAN5IgADeSQAA3kmAAN5KAADeSsAA3ksAAN5LQADeS4AA3kvAAN5MQADeTMAA3k0AAN5NQADeTcAA3k4AAN5dwADeXkAA3l7AAN5fQADeYAAA3mBAAN5ggADeYMAA3mEAAN5hgADeYgAA3mJAAN5igADeYwAA3mNAAN5zAADec4AA3nQAAN50gADedUAA3nWAAN51wADedgAA3nZAAN52wADed0AA3neAAN53wADeeEAA3niAAN6IQADeiMAA3olAAN6JwADeioAA3orAAN6LAADei0AA3ouAAN6MAADejIAA3ozAAN6NAADejYAA3o3AAN6dgADengAA3p6AAN6fAADen8AA3qAAAN6gQADeoIAA3qDAAN6hQADeocAA3qIAAN6iQADeosAA3qMAAN6sQADetUAA3r8AAN7IAADeyMAA3slAAN7JwADeykAA3srAAN7LQADey4AA3sxAAN7PgADe00AA3tPAAN7UQADe1MAA3tVAAN7VwADe1kAA3tbAAN7agADe20AA3twAAN7cwADe3YAA3t5AAN7fAADe38AA3uBAAN7wAADe8IAA3vEAAN7xgADe8kAA3vKAAN7ywADe8wAA3vNAAN7zwADe9EAA3vSAAN70wADe9UAA3vWAAN8FQADfBcAA3wZAAN8GwADfB4AA3wfAAN8IAADfCEAA3wiAAN8JAADfCYAA3wnAAN8KAADfCoAA3wrAAN8agADfGwAA3xuAAN8cAADfHMAA3x0AAN8dQADfHYAA3x3AAN8eQADfHsAA3x8AAN8fQADfH8AA3yAAAN8vwADfMEAA3zEAAN8xgADfMkAA3zKAAN8ywADfMwAA3zNAAN8zwADfNEAA3zSAAN80wADfNUAA3zWAAN9FQADfRcAA30ZAAN9GwADfR4AA30fAAN9IAADfSEAA30iAAN9JAADfSYAA30nAAN9KAADfSoAA30rAAN9agADfWwAA31uAAN9cAADfXMAA310AAN9dQADfXYAA313AAN9eQADfXsAA318AAN9fQADfX8AA32AAAN9vwADfcEAA33DAAN9xQADfcgAA33JAAN9ygADfcsAA33MAAN9zgADfdAAA33RAAN90gADfdQAA33VAAN+IAADfkMAA35jAAN+gwADfoUAA36HAAN+iQADfosAA36OAAN+jwADfpAAA36TAAN+lAADfpYAA36XAAN+mQADfpwAA36dAAN+ngADfqEAA36iAAN+pwADfrQAA365AAN+uwADfr0AA37CAAN+xQADfsgAA37KAAN+7wADfxMAA386AAN/XgADf2EAA39jAAN/ZQADf2cAA39pAAN/awADf2wAA39vAAN/fAADf40AA3+PAAN/kQADf5MAA3+VAAN/lwADf5kAA3+bAAN/nQADf64AA3+xAAN/tAADf7cAA3+6AAN/vQADf8AAA3/DAAN/xgADf8gAA4AHAAOACQADgAsAA4ANAAOAEAADgBEAA4ASAAOAEwADgBQAA4AWAAOAGAADgBkAA4AaAAOAHAADgB0AA4BcAAOAXgADgGAAA4BiAAOAZQADgGYAA4BnAAOAaAADgGkAA4BrAAOAbQADgG4AA4BvAAOAcQADgHIAA4CxAAOAswADgLYAA4C4AAOAuwADgLwAA4C9AAOAvgADgL8AA4DBAAOAwwADgMQAA4DFAAOAxwADgMgAA4DVAAOA1gADgNcAA4DZAAOBGAADgRoAA4EcAAOBHgADgSEAA4EiAAOBIwADgSQAA4ElAAOBJwADgSkAA4EqAAOBKwADgS0AA4EuAAOBbQADgW8AA4FxAAOBcwADgXYAA4F3AAOBeAADgXkAA4F6AAOBfAADgX4AA4F/AAOBgAADgYIAA4GDAAOBwgADgcQAA4HGAAOByAADgcsAA4HMAAOBzQADgc4AA4HPAAOB0QADgdMAA4HUAAOB1QADgdcAA4HYAAOCFwADghkAA4IbAAOCHQADgiAAA4IhAAOCIgADgiMAA4IkAAOCJgADgigAA4IpAAOCKgADgiwAA4ItAAOCbAADgm4AA4JwAAOCcgADgnUAA4J2AAOCdwADgngAA4J5AAOCewADgn0AA4J+AAOCfwADgoEAA4KCAAOCpwADgssAA4LyAAODFgADgxkAA4MbAAODHQADgx8AA4MhAAODIwADgyQAA4MnAAODNAADg0MAA4NFAAODRwADg0kAA4NLAAODTQADg08AA4NRAAODYAADg2MAA4NmAAODaQADg2wAA4NvAAODcgADg3UAA4N3AAODtgADg7gAA4O6AAODvAADg78AA4PAAAODwQADg8IAA4PDAAODxQADg8cAA4PIAAODyQADg8sAA4PMAAOECwADhA0AA4QPAAOEEQADhBQAA4QVAAOEFgADhBcAA4QYAAOEGgADhBwAA4QdAAOEHgADhCAAA4QhAAOEYAADhGIAA4RkAAOEZgADhGkAA4RqAAOEawADhGwAA4RtAAOEbwADhHEAA4RyAAOEcwADhHUAA4R2AAOEtQADhLcAA4S5AAOEuwADhL4AA4S/AAOEwAADhMEAA4TCAAOExAADhMYAA4THAAOEyAADhMoAA4TLAAOFCgADhQwAA4UOAAOFEAADhRMAA4UUAAOFFQADhRYAA4UXAAOFGQADhRsAA4UcAAOFHQADhR8AA4UgAAOFXwADhWEAA4VjAAOFZQADhWgAA4VpAAOFagADhWsAA4VsAAOFbgADhXAAA4VxAAOFcgADhXQAA4V1AAOFtAADhbYAA4W4AAOFugADhb0AA4W+AAOFvwADhcAAA4XBAAOFwwADhcUAA4XGAAOFxwADhckAA4XKAAOF0wADhdQAA4XWAAOGGQADhj0AA4ZhAAOGhAADhqsAA4bLAAOG8gADhxkAA4c5AAOHXQADh4EAA4eDAAOHhgADh4gAA4eKAAOHjAADh48AA4eSAAOHlAADh5YAA4eZAAOHmwADh50AA4egAAOHowADh6QAA4epAAOHtgADh7kAA4e7AAOHvgADh8EAA4fDAAOH6AADiAwAA4gzAAOIVwADiFoAA4hcAAOIXgADiGAAA4hiAAOIZAADiGUAA4hoAAOIdQADiIgAA4iKAAOIjAADiI4AA4iQAAOIkgADiJQAA4iWAAOImAADiJoAA4itAAOIsAADiLMAA4i2AAOIuQADiLwAA4i/AAOIwgADiMUAA4jIAAOIygADiQkAA4kLAAOJDgADiRAAA4kTAAOJFAADiRUAA4kWAAOJFwADiRkAA4kbAAOJHAADiR0AA4kfAAOJIAADiSkAA4kqAAOJLAADiWsAA4ltAAOJbwADiXEAA4l0AAOJdQADiXYAA4l3AAOJeAADiXoAA4l8AAOJfQADiX4AA4mAAAOJgQADicAAA4nCAAOJxQADiccAA4nKAAOJywADicwAA4nNAAOJzgADidAAA4nSAAOJ0wADidQAA4nWAAOJ1wADieAAA4nhAAOJ4wADiiIAA4okAAOKJgADiigAA4orAAOKLAADii0AA4ouAAOKLwADijEAA4ozAAOKNAADijUAA4o3AAOKOAADincAA4p5AAOKfAADin4AA4qBAAOKggADioMAA4qEAAOKhQADiocAA4qJAAOKigADiosAA4qNAAOKjgADipcAA4qYAAOKmgADitkAA4rbAAOK3QADit8AA4riAAOK4wADiuQAA4rlAAOK5gADiugAA4rqAAOK6wADiuwAA4ruAAOK7wADiy4AA4swAAOLMwADizUAA4s4AAOLOQADizoAA4s7AAOLPAADiz4AA4tAAAOLQQADi0IAA4tEAAOLRQADi1IAA4tTAAOLVAADi1YAA4uVAAOLlwADi5kAA4ubAAOLngADi58AA4ugAAOLoQADi6IAA4ukAAOLpgADi6cAA4uoAAOLqgADi6sAA4vqAAOL7AADi+4AA4vwAAOL8wADi/QAA4v1AAOL9gADi/cAA4v5AAOL+wADi/wAA4v9AAOL/wADjAAAA4wNAAOMFAADjBcAA4wZAAOMGwADjCIAA4wlAAOMKAADjCsAA4wtAAOMeAADjJsAA4y7AAOM2wADjN0AA4zfAAOM4QADjOMAA4zmAAOM5wADjOgAA4zrAAOM7AADjO4AA4zvAAOM8QADjPQAA4z1AAOM9gADjPkAA4z6AAONAwADjRAAA40VAAONFwADjRkAA40eAAONIQADjSQAA40mAAONSwADjW8AA42WAAONugADjb0AA42/AAONwQADjcMAA43FAAONxwADjcgAA43LAAON2AADjekAA43rAAON7QADje8AA43xAAON8wADjfUAA433AAON+QADjgoAA44NAAOOEAADjhMAA44WAAOOGQADjhwAA44fAAOOIgADjiQAA45jAAOOZQADjmcAA45pAAOObAADjm0AA45uAAOObwADjnAAA45yAAOOdAADjnUAA452AAOOeAADjnkAA464AAOOugADjrwAA46+AAOOwQADjsIAA47DAAOOxAADjsUAA47HAAOOyQADjsoAA47LAAOOzQADjs4AA48NAAOPDwADjxIAA48UAAOPFwADjxgAA48ZAAOPGgADjxsAA48dAAOPHwADjyAAA48hAAOPIwADjyQAA48xAAOPMgADjzMAA481AAOPdAADj3YAA494AAOPegADj30AA49+AAOPfwADj4AAA4+BAAOPgwADj4UAA4+GAAOPhwADj4kAA4+KAAOPyQADj8sAA4/NAAOPzwADj9IAA4/TAAOP1AADj9UAA4/WAAOP2AADj9oAA4/bAAOP3AADj94AA4/fAAOQHgADkCAAA5AiAAOQJAADkCcAA5AoAAOQKQADkCoAA5ArAAOQLQADkC8AA5AwAAOQMQADkDMAA5A0AAOQcwADkHUAA5B3AAOQeQADkHwAA5B9AAOQfgADkH8AA5CAAAOQggADkIQAA5CFAAOQhgADkIgAA5CJAAOQyAADkMoAA5DMAAOQzgADkNEAA5DSAAOQ0wADkNQAA5DVAAOQ1wADkNkAA5DaAAOQ2wADkN0AA5DeAAORAwADkScAA5FOAAORcgADkXUAA5F3AAOReQADkXsAA5F9AAORfwADkYAAA5GDAAORkAADkZ8AA5GhAAORowADkaUAA5GnAAORqQADkasAA5GtAAORvAADkb8AA5HCAAORxQADkcgAA5HLAAORzgADkdEAA5HTAAOSEgADkhQAA5IWAAOSGAADkhsAA5IcAAOSHQADkh4AA5IfAAOSIQADkiMAA5IkAAOSJQADkicAA5IoAAOSZwADkmkAA5JrAAOSbQADknAAA5JxAAOScgADknMAA5J0AAOSdgADkngAA5J5AAOSegADknwAA5J9AAOSvAADkr4AA5LAAAOSwgADksUAA5LGAAOSxwADksgAA5LJAAOSywADks0AA5LOAAOSzwADktEAA5LSAAOTEQADkxMAA5MVAAOTFwADkxoAA5MbAAOTHAADkx0AA5MeAAOTIAADkyIAA5MjAAOTJAADkyYAA5MnAAOTZgADk2gAA5NqAAOTbAADk28AA5NwAAOTcQADk3IAA5NzAAOTdQADk3cAA5N4AAOTeQADk3sAA5N8AAOTuwADk70AA5O/AAOTwQADk8QAA5PFAAOTxgADk8cAA5PIAAOTygADk8wAA5PNAAOTzgADk9AAA5PRAAOUEAADlBIAA5QUAAOUFgADlBkAA5QaAAOUGwADlBwAA5QdAAOUHwADlCEAA5QiAAOUIwADlCUAA5QmAAOUcQADlJQAA5S0AAOU1AADlNYAA5TYAAOU2gADlNwAA5TfAAOU4AADlOEAA5TkAAOU5QADlOcAA5ToAAOU6gADlOwAA5TtAAOU7gADlPEAA5TyAAOU9wADlQQAA5UJAAOVCwADlQ0AA5USAAOVFQADlRgAA5UaAAOVPwADlWMAA5WKAAOVrgADlbEAA5WzAAOVtQADlbcAA5W5AAOVuwADlbwAA5W/AAOVzAADld0AA5XfAAOV4QADleMAA5XlAAOV5wADlekAA5XrAAOV7QADlf4AA5YBAAOWBAADlgcAA5YKAAOWDQADlhAAA5YTAAOWFgADlhgAA5ZXAAOWWQADllsAA5ZdAAOWYAADlmEAA5ZiAAOWYwADlmQAA5ZmAAOWaAADlmkAA5ZqAAOWbAADlm0AA5asAAOWrgADlrAAA5ayAAOWtQADlrYAA5a3AAOWuAADlrkAA5a7AAOWvQADlr4AA5a/AAOWwQADlsIAA5cBAAOXAwADlwYAA5cIAAOXCwADlwwAA5cNAAOXDgADlw8AA5cRAAOXEwADlxQAA5cVAAOXFwADlxgAA5clAAOXJgADlycAA5cpAAOXaAADl2oAA5dsAAOXbgADl3EAA5dyAAOXcwADl3QAA5d1AAOXdwADl3kAA5d6AAOXewADl30AA5d+AAOXvQADl78AA5fBAAOXwwADl8YAA5fHAAOXyAADl8kAA5fKAAOXzAADl84AA5fPAAOX0AADl9IAA5fTAAOYEgADmBQAA5gWAAOYGAADmBsAA5gcAAOYHQADmB4AA5gfAAOYIQADmCMAA5gkAAOYJQADmCcAA5goAAOYZwADmGkAA5hrAAOYbQADmHAAA5hxAAOYcgADmHMAA5h0AAOYdgADmHgAA5h5AAOYegADmHwAA5h9AAOYvAADmL4AA5jAAAOYwgADmMUAA5jGAAOYxwADmMgAA5jJAAOYywADmM0AA5jOAAOYzwADmNEAA5jSAAOY9wADmRsAA5lCAAOZZgADmWkAA5lrAAOZbQADmW8AA5lxAAOZcwADmXQAA5l3AAOZhAADmZMAA5mVAAOZlwADmZkAA5mbAAOZnQADmZ8AA5mhAAOZsAADmbMAA5m2AAOZuQADmbwAA5m/AAOZwgADmcUAA5nHAAOaBgADmggAA5oKAAOaDAADmg8AA5oQAAOaEQADmhIAA5oTAAOaFQADmhcAA5oYAAOaGQADmhsAA5ocAAOaWwADml0AA5pfAAOaYQADmmQAA5plAAOaZgADmmcAA5poAAOaagADmmwAA5ptAAOabgADmnAAA5pxAAOasAADmrIAA5q0AAOatgADmrkAA5q6AAOauwADmrwAA5q9AAOavwADmsEAA5rCAAOawwADmsUAA5rGAAObBQADmwcAA5sJAAObCwADmw4AA5sPAAObEAADmxEAA5sSAAObFAADmxYAA5sXAAObGAADmxoAA5sbAAObWgADm1wAA5teAAObYAADm2MAA5tkAAObZQADm2YAA5tnAAObaQADm2sAA5tsAAObbQADm28AA5twAAObrwADm7EAA5uzAAObtQADm7gAA5u5AAObugADm7sAA5u8AAObvgADm8AAA5vBAAObwgADm8QAA5vFAAOcBAADnAYAA5wIAAOcCgADnA0AA5wOAAOcDwADnBAAA5wRAAOcEwADnBUAA5wWAAOcFwADnBkAA5waAAOcZQADnIgAA5yoAAOcyAADnMoAA5zMAAOczgADnNAAA5zTAAOc1AADnNUAA5zYAAOc2QADnNsAA5zcAAOc3gADnOAAA5zhAAOc4gADnOUAA5zmAAOc7wADnPwAA50BAAOdAwADnQUAA50KAAOdDQADnRAAA50SAAOdNwADnVsAA52CAAOdpgADnakAA52rAAOdrQADna8AA52xAAOdswADnbQAA523AAOdxAADndUAA53XAAOd2QADndsAA53dAAOd3wADneEAA53jAAOd5QADnfYAA535AAOd/AADnf8AA54CAAOeBQADnggAA54LAAOeDgADnhAAA55PAAOeUQADnlMAA55VAAOeWAADnlkAA55aAAOeWwADnlwAA55eAAOeYAADnmEAA55iAAOeZAADnmUAA56kAAOepgADnqgAA56qAAOerQADnq4AA56vAAOesAADnrEAA56zAAOetQADnrYAA563AAOeuQADnroAA575AAOe+wADnv4AA58AAAOfAwADnwQAA58FAAOfBgADnwcAA58JAAOfCwADnwwAA58NAAOfDwADnxAAA58dAAOfHgADnx8AA58hAAOfYAADn2IAA59kAAOfZgADn2kAA59qAAOfawADn2wAA59tAAOfbwADn3EAA59yAAOfcwADn3UAA592AAOftQADn7cAA5+5AAOfuwADn74AA5+/AAOfwAADn8EAA5/CAAOfxAADn8YAA5/HAAOfyAADn8oAA5/LAAOgCgADoAwAA6AOAAOgEAADoBMAA6AUAAOgFQADoBYAA6AXAAOgGQADoBsAA6AcAAOgHQADoB8AA6AgAAOgXwADoGEAA6BjAAOgZQADoGgAA6BpAAOgagADoGsAA6BsAAOgbgADoHAAA6BxAAOgcgADoHQAA6B1AAOgtAADoLYAA6C4AAOgugADoL0AA6C+AAOgvwADoMAAA6DBAAOgwwADoMUAA6DGAAOgxwADoMkAA6DKAAOg7wADoRMAA6E6AAOhXgADoWEAA6FjAAOhZQADoWcAA6FpAAOhawADoWwAA6FvAAOhfAADoYsAA6GNAAOhjwADoZEAA6GTAAOhlQADoZcAA6GZAAOhqAADoasAA6GuAAOhsQADobQAA6G3AAOhugADob0AA6G/AAOh/gADogAAA6ICAAOiBAADogcAA6IIAAOiCQADogoAA6ILAAOiDQADog8AA6IQAAOiEQADohMAA6IUAAOiUwADolUAA6JXAAOiWQADolwAA6JdAAOiXgADol8AA6JgAAOiYgADomQAA6JlAAOiZgADomgAA6JpAAOiqAADoqoAA6KsAAOirgADorEAA6KyAAOiswADorQAA6K1AAOitwADorkAA6K6AAOiuwADor0AA6K+AAOi/QADov8AA6MBAAOjAwADowYAA6MHAAOjCAADowkAA6MKAAOjDAADow4AA6MPAAOjEAADoxIAA6MTAAOjUgADo1QAA6NWAAOjWAADo1sAA6NcAAOjXQADo14AA6NfAAOjYQADo2MAA6NkAAOjZQADo2cAA6NoAAOjpwADo6kAA6OrAAOjrQADo7AAA6OxAAOjsgADo7MAA6O0AAOjtgADo7gAA6O5AAOjugADo7wAA6O9AAOj/AADo/4AA6QAAAOkAgADpAUAA6QGAAOkBwADpAgAA6QJAAOkCwADpA0AA6QOAAOkDwADpBEAA6QSAAOkGwADpBwAA6QeAAOkYQADpIUAA6SpAAOkzAADpPMAA6UTAAOlOgADpWEAA6WBAAOlpQADpckAA6XLAAOlzgADpdAAA6XSAAOl1AADpdcAA6XaAAOl3AADpd4AA6XhAAOl4wADpeUAA6XoAAOl6wADpewAA6XxAAOl/gADpgEAA6YDAAOmBgADpgkAA6YLAAOmMAADplQAA6Z7AAOmnwADpqIAA6akAAOmpgADpqgAA6aqAAOmrAADpq0AA6awAAOmvQADptAAA6bSAAOm1AADptYAA6bYAAOm2gADptwAA6beAAOm4AADpuIAA6b1AAOm+AADpvsAA6b+AAOnAQADpwQAA6cHAAOnCgADpw0AA6cQAAOnEgADp1EAA6dTAAOnVgADp1gAA6dbAAOnXAADp10AA6deAAOnXwADp2EAA6djAAOnZAADp2UAA6dnAAOnaAADp3EAA6dyAAOndAADp7MAA6e1AAOntwADp7kAA6e8AAOnvQADp74AA6e/AAOnwAADp8IAA6fEAAOnxQADp8YAA6fIAAOnyQADqAgAA6gKAAOoDQADqA8AA6gSAAOoEwADqBQAA6gVAAOoFgADqBgAA6gaAAOoGwADqBwAA6geAAOoHwADqCgAA6gpAAOoKwADqGoAA6hsAAOobgADqHAAA6hzAAOodAADqHUAA6h2AAOodwADqHkAA6h7AAOofAADqH0AA6h/AAOogAADqL8AA6jBAAOoxAADqMYAA6jJAAOoygADqMsAA6jMAAOozQADqM8AA6jRAAOo0gADqNMAA6jVAAOo1gADqN8AA6jgAAOo4gADqSEAA6kjAAOpJQADqScAA6kqAAOpKwADqSwAA6ktAAOpLgADqTAAA6kyAAOpMwADqTQAA6k2AAOpNwADqXYAA6l4AAOpewADqX0AA6mAAAOpgQADqYIAA6mDAAOphAADqYYAA6mIAAOpiQADqYoAA6mMAAOpjQADqZoAA6mbAAOpnAADqZ4AA6ndAAOp3wADqeEAA6njAAOp5gADqecAA6noAAOp6QADqeoAA6nsAAOp7gADqe8AA6nwAAOp8gADqfMAA6oyAAOqNAADqjcAA6o5AAOqPAADqj0AA6o+AAOqPwADqkAAA6pCAAOqRAADqkUAA6pGAAOqSAADqkkAA6pYAAOqZQADqmwAA6pvAAOqcgADqnUAA6p8AAOqfwADqoIAA6qFAAOqhwADqo4AA6qYAAOq4wADqwYAA6smAAOrRgADq0gAA6tKAAOrTAADq04AA6tRAAOrUgADq1MAA6tWAAOrVwADq1kAA6taAAOrXAADq18AA6tgAAOrYQADq2QAA6tlAAOragADq3cAA6t8AAOrfgADq4AAA6uFAAOriAADq4sAA6uNAAOrsgADq9YAA6v9AAOsIQADrCQAA6wmAAOsKAADrCoAA6wsAAOsLgADrC8AA6wyAAOsPwADrFAAA6xSAAOsVAADrFYAA6xYAAOsWgADrFwAA6xeAAOsYAADrHEAA6x0AAOsdwADrHoAA6x9AAOsgAADrIMAA6yGAAOsiQADrIsAA6zKAAOszAADrM4AA6zQAAOs0wADrNQAA6zVAAOs1gADrNcAA6zZAAOs2wADrNwAA6zdAAOs3wADrOAAA60fAAOtIQADrSMAA60lAAOtKAADrSkAA60qAAOtKwADrSwAA60uAAOtMAADrTEAA60yAAOtNAADrTUAA610AAOtdgADrXkAA617AAOtfgADrX8AA62AAAOtgQADrYIAA62EAAOthgADrYcAA62IAAOtigADrYsAA62YAAOtmQADrZoAA62cAAOt2wADrd0AA63fAAOt4QADreQAA63lAAOt5gADrecAA63oAAOt6gADrewAA63tAAOt7gADrfAAA63xAAOuMAADrjIAA640AAOuNgADrjkAA646AAOuOwADrjwAA649AAOuPwADrkEAA65CAAOuQwADrkUAA65GAAOuhQADrocAA66JAAOuiwADro4AA66PAAOukAADrpEAA66SAAOulAADrpYAA66XAAOumAADrpoAA66bAAOu2gADrtwAA67eAAOu4AADruMAA67kAAOu5QADruYAA67nAAOu6QADrusAA67sAAOu7QADru8AA67wAAOvLwADrzEAA68zAAOvNQADrzgAA685AAOvOgADrzsAA688AAOvPgADr0AAA69BAAOvQgADr0QAA69FAAOvagADr44AA6+1AAOv2QADr9wAA6/eAAOv4AADr+IAA6/kAAOv5gADr+cAA6/qAAOv9wADsAYAA7AIAAOwCgADsAwAA7AOAAOwEAADsBIAA7AUAAOwIwADsCYAA7ApAAOwLAADsC8AA7AyAAOwNQADsDgAA7A6AAOweQADsHsAA7B9AAOwfwADsIIAA7CDAAOwhAADsIUAA7CGAAOwiAADsIoAA7CLAAOwjAADsI4AA7CPAAOwzgADsNAAA7DSAAOw1AADsNcAA7DYAAOw2QADsNoAA7DbAAOw3QADsN8AA7DgAAOw4QADsOMAA7DkAAOxIwADsSUAA7EnAAOxKQADsSwAA7EtAAOxLgADsS8AA7EwAAOxMgADsTQAA7E1AAOxNgADsTgAA7E5AAOxeAADsXoAA7F8AAOxfgADsYEAA7GCAAOxgwADsYQAA7GFAAOxhwADsYkAA7GKAAOxiwADsY0AA7GOAAOxzQADsc8AA7HRAAOx0wADsdYAA7HXAAOx2AADsdkAA7HaAAOx3AADsd4AA7HfAAOx4AADseIAA7HjAAOyIgADsiQAA7ImAAOyKAADsisAA7IsAAOyLQADsi4AA7IvAAOyMQADsjMAA7I0AAOyNQADsjcAA7I4AAOydwADsnkAA7J7AAOyfQADsoAAA7KBAAOyggADsoMAA7KEAAOyhgADsogAA7KJAAOyigADsowAA7KNAAOy2AADsvsAA7MbAAOzOwADsz0AA7M/AAOzQQADs0MAA7NGAAOzRwADs0gAA7NLAAOzTAADs04AA7NPAAOzUQADs1QAA7NVAAOzVgADs1kAA7NaAAOzXwADs2wAA7NxAAOzcwADs3UAA7N6AAOzfQADs4AAA7OCAAOzpwADs8sAA7PyAAO0FgADtBkAA7QbAAO0HQADtB8AA7QhAAO0IwADtCQAA7QnAAO0NAADtEUAA7RHAAO0SQADtEsAA7RNAAO0TwADtFEAA7RTAAO0VQADtGYAA7RpAAO0bAADtG8AA7RyAAO0dQADtHgAA7R7AAO0fgADtIAAA7S/AAO0wQADtMMAA7TFAAO0yAADtMkAA7TKAAO0ywADtMwAA7TOAAO00AADtNEAA7TSAAO01AADtNUAA7UUAAO1FgADtRgAA7UaAAO1HQADtR4AA7UfAAO1IAADtSEAA7UjAAO1JQADtSYAA7UnAAO1KQADtSoAA7VpAAO1awADtW4AA7VwAAO1cwADtXQAA7V1AAO1dgADtXcAA7V5AAO1ewADtXwAA7V9AAO1fwADtYAAA7WNAAO1jgADtY8AA7WRAAO10AADtdIAA7XUAAO11gADtdkAA7XaAAO12wADtdwAA7XdAAO13wADteEAA7XiAAO14wADteUAA7XmAAO2JQADticAA7YpAAO2KwADti4AA7YvAAO2MAADtjEAA7YyAAO2NAADtjYAA7Y3AAO2OAADtjoAA7Y7AAO2egADtnwAA7Z+AAO2gAADtoMAA7aEAAO2hQADtoYAA7aHAAO2iQADtosAA7aMAAO2jQADto8AA7aQAAO2zwADttEAA7bTAAO21QADttgAA7bZAAO22gADttsAA7bcAAO23gADtuAAA7bhAAO24gADtuQAA7blAAO3JAADtyYAA7coAAO3KgADty0AA7cuAAO3LwADtzAAA7cxAAO3MwADtzUAA7c2AAO3NwADtzkAA7c6AAO3XwADt4MAA7eqAAO3zgADt9EAA7fTAAO31QADt9cAA7fZAAO32wADt9wAA7ffAAO37AADt/sAA7f9AAO3/wADuAEAA7gDAAO4BQADuAcAA7gJAAO4GAADuBsAA7geAAO4IQADuCQAA7gnAAO4KgADuC0AA7gvAAO4bgADuHAAA7hyAAO4dAADuHcAA7h4AAO4eQADuHoAA7h7AAO4fQADuH8AA7iAAAO4gQADuIMAA7iEAAO4wwADuMUAA7jHAAO4yQADuMwAA7jNAAO4zgADuM8AA7jQAAO40gADuNQAA7jVAAO41gADuNgAA7jZAAO5GAADuRoAA7kcAAO5HgADuSEAA7kiAAO5IwADuSQAA7klAAO5JwADuSkAA7kqAAO5KwADuS0AA7kuAAO5bQADuW8AA7lxAAO5cwADuXYAA7l3AAO5eAADuXkAA7l6AAO5fAADuX4AA7l/AAO5gAADuYIAA7mDAAO5wgADucQAA7nGAAO5yAADucsAA7nMAAO5zQADuc4AA7nPAAO50QADudMAA7nUAAO51QADudcAA7nYAAO6FwADuhkAA7obAAO6HQADuiAAA7ohAAO6IgADuiMAA7okAAO6JgADuigAA7opAAO6KgADuiwAA7otAAO6bAADum4AA7pwAAO6cgADunUAA7p2AAO6dwADungAA7p5AAO6ewADun0AA7p+AAO6fwADuoEAA7qCAAO6zQADuvAAA7sQAAO7MAADuzIAA7s0AAO7NgADuzgAA7s7AAO7PAADuz0AA7tAAAO7QQADu0MAA7tEAAO7RgADu0kAA7tKAAO7SwADu04AA7tPAAO7VAADu2EAA7tmAAO7aAADu2oAA7tvAAO7cgADu3UAA7t3AAO7nAADu8AAA7vnAAO8CwADvA4AA7wQAAO8EgADvBQAA7wWAAO8GAADvBkAA7wcAAO8KQADvDoAA7w8AAO8PgADvEAAA7xCAAO8RAADvEYAA7xIAAO8SgADvFsAA7xeAAO8YQADvGQAA7xnAAO8agADvG0AA7xwAAO8cwADvHUAA7y0AAO8tgADvLgAA7y6AAO8vQADvL4AA7y/AAO8wAADvMEAA7zDAAO8xQADvMYAA7zHAAO8yQADvMoAA70JAAO9CwADvQ0AA70PAAO9EgADvRMAA70UAAO9FQADvRYAA70YAAO9GgADvRsAA70cAAO9HgADvR8AA71eAAO9YAADvWMAA71lAAO9aAADvWkAA71qAAO9awADvWwAA71uAAO9cAADvXEAA71yAAO9dAADvXUAA72CAAO9gwADvYQAA72GAAO9xQADvccAA73JAAO9ywADvc4AA73PAAO90AADvdEAA73SAAO91AADvdYAA73XAAO92AADvdoAA73bAAO+GgADvhwAA74eAAO+IAADviMAA74kAAO+JQADviYAA74nAAO+KQADvisAA74sAAO+LQADvi8AA74wAAO+bwADvnEAA75zAAO+dQADvngAA755AAO+egADvnsAA758AAO+fgADvoAAA76BAAO+ggADvoQAA76FAAO+xAADvsYAA77IAAO+ygADvs0AA77OAAO+zwADvtAAA77RAAO+0wADvtUAA77WAAO+1wADvtkAA77aAAO/GQADvxsAA78dAAO/HwADvyIAA78jAAO/JAADvyUAA78mAAO/KAADvyoAA78rAAO/LAADvy4AA78vAAO/VAADv3gAA7+fAAO/wwADv8YAA7/IAAO/ygADv8wAA7/OAAO/0AADv9EAA7/UAAO/4QADv/AAA7/yAAO/9AADv/YAA7/4AAO/+gADv/wAA7/+AAPADQADwBAAA8ATAAPAFgADwBkAA8AcAAPAHwADwCIAA8AkAAPAYwADwGUAA8BnAAPAaQADwGwAA8BtAAPAbgADwG8AA8BwAAPAcgADwHQAA8B1AAPAdgADwHgAA8B5AAPAuAADwLoAA8C8AAPAvgADwMEAA8DCAAPAwwADwMQAA8DFAAPAxwADwMkAA8DKAAPAywADwM0AA8DOAAPBDQADwQ8AA8ERAAPBEwADwRYAA8EXAAPBGAADwRkAA8EaAAPBHAADwR4AA8E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 diff --git a/fearless/Common/Storage/PersistentStoreCoordinator+Extensions.swift b/fearless/Common/Storage/PersistentStoreCoordinator+Extensions.swift index ef831071ee..bc587836ef 100644 --- a/fearless/Common/Storage/PersistentStoreCoordinator+Extensions.swift +++ b/fearless/Common/Storage/PersistentStoreCoordinator+Extensions.swift @@ -1,5 +1,2437 @@ import Foundation import CoreData +import CryptoKit +import Darwin + +enum CrashConsistentStoreReplacementBoundary: Equatable { + case preparingMarkerPersisted + case backupFileCopied(String) + case committingMarkerPersisted + case replacementReturned + case destinationValidated + case destinationFamilySynced + case committedMarkerPersisted + case rollbackMarkerPersisted + case liveFileRemoved(String) + case rollbackTemporaryFileCopied(String) + case rollbackFileInstalled(String) + case rollbackFamilyVerified + case cleanupBackupFileRemoved(String) + case cleanupBackupDirectoryRemoved + case cleanupMarkerTemporaryRemoved + case cleanupMarkerRemoved + case cleanupTransactionDirectoryRemoved + case cleanupRecoveryRootRemoved + case transactionCleaned +} + +enum CrashConsistentStoreReplacementInterruption: Error { + /// Test-only fault used to emulate a process disappearing without running + /// Swift unwinding, rollback, or defer blocks. + case simulatedProcessDeath +} + +enum CrashConsistentStoreReplacementError: LocalizedError { + case sourceStoreMissing + case unsafeFile(URL) + case unsafeDirectory(URL) + case storeFamilySizeOverflow + case storeFamilyTooLarge( + actualByteCount: UInt64, + maximumByteCount: UInt64 + ) + case insufficientStorageCapacity( + requiredByteCount: UInt64, + availableByteCount: UInt64 + ) + case markerMissingForInterruptedStore + case markerTooLarge + case invalidMarker + case familyMismatch + case backupUnavailable + case destinationValidationFailed(Error) + case rollbackFailed(Error, Error) + case fileSynchronizationFailed(URL, Error) + case directorySynchronizationFailed(URL, Error) + + var errorDescription: String? { + switch self { + case .sourceStoreMissing: + return "The SQLite source store is missing" + case .unsafeFile: + return "The SQLite replacement transaction contains an unsafe file" + case .unsafeDirectory: + return "The SQLite replacement transaction contains an unsafe directory" + case .storeFamilySizeOverflow: + return "The SQLite store family size overflowed its bounded counter" + case let .storeFamilyTooLarge(actualByteCount, maximumByteCount): + return """ + The SQLite store family uses \(actualByteCount) bytes, exceeding the \ + \(maximumByteCount)-byte migration limit + """ + case let .insufficientStorageCapacity( + requiredByteCount, + availableByteCount + ): + return """ + SQLite replacement requires \(requiredByteCount) free bytes, but only \ + \(availableByteCount) bytes are available + """ + case .markerMissingForInterruptedStore: + return "An interrupted SQLite replacement is missing its recovery marker" + case .markerTooLarge: + return "The SQLite replacement recovery marker is too large" + case .invalidMarker: + return "The SQLite replacement recovery marker is invalid" + case .familyMismatch: + return "The SQLite store family does not match its durable manifest" + case .backupUnavailable: + return "The SQLite rollback family is incomplete or damaged" + case let .destinationValidationFailed(error): + return "The replaced SQLite store failed validation: \(error.localizedDescription)" + case let .rollbackFailed(replacementError, rollbackError): + return """ + SQLite replacement failed with \(replacementError.localizedDescription), and rollback \ + failed with \(rollbackError.localizedDescription) + """ + case let .fileSynchronizationFailed(_, error): + return "Unable to synchronize a SQLite replacement file: \(error.localizedDescription)" + case let .directorySynchronizationFailed(_, error): + return "Unable to synchronize a SQLite replacement directory: \(error.localizedDescription)" + } + } +} + +struct SQLiteStoreFamilyCopyLimits: Equatable { + static let userStorageProduction = SQLiteStoreFamilyCopyLimits( + maximumFamilyByteCount: 512 * 1024 * 1024, + minimumFreeStorageReserveByteCount: 64 * 1024 * 1024 + ) + static let substrateStorageProduction = SQLiteStoreFamilyCopyLimits( + maximumFamilyByteCount: 4 * 1024 * 1024 * 1024, + minimumFreeStorageReserveByteCount: 128 * 1024 * 1024 + ) + + let maximumFamilyByteCount: UInt64 + let minimumFreeStorageReserveByteCount: UInt64 + + init( + maximumFamilyByteCount: UInt64, + minimumFreeStorageReserveByteCount: UInt64 + ) { + self.maximumFamilyByteCount = maximumFamilyByteCount + self.minimumFreeStorageReserveByteCount = + minimumFreeStorageReserveByteCount + } +} + +/// Makes a bounded private SQLite-family copy without following sidecar +/// symlinks. Migration callers use the copy as disposable input, so all size +/// and capacity checks happen before the first destination member is created. +enum SQLiteStoreFamilyCopier { + static let storeFamilySuffixes = ["", "-wal", "-shm", "-journal"] + + private struct SourceMember { + let sourceURL: URL + let destinationURL: URL + let byteCount: UInt64 + let deviceID: UInt64 + let inode: UInt64 + } + + static func copy( + from sourceStoreURL: URL, + to destinationStoreURL: URL, + includingSharedMemory: Bool, + fileManager: FileManager, + limits: SQLiteStoreFamilyCopyLimits, + availableCapacityProvider: + ((URL) throws -> UInt64)? = nil + ) throws { + let sourceStoreURL = sourceStoreURL.standardizedFileURL + let destinationStoreURL = + destinationStoreURL.standardizedFileURL + let destinationDirectoryURL = + destinationStoreURL.deletingLastPathComponent() + + try requireDirectory(destinationDirectoryURL) + + let capacityProvider = + availableCapacityProvider ?? { directoryURL in + try availableCapacity( + at: directoryURL, + fileManager: fileManager + ) + } + var sourceMembers: [SourceMember] = [] + var aggregateByteCount: UInt64 = 0 + + for suffix in storeFamilySuffixes { + let sourceURL = familyURL( + sourceStoreURL, + suffix: suffix + ) + let destinationURL = familyURL( + destinationStoreURL, + suffix: suffix + ) + + // Reject stale destination sidecars even when the corresponding + // source member is absent. Otherwise SQLite could attach data that + // was never part of the bounded source snapshot. + guard pathKind(at: destinationURL) == .missing else { + throw CrashConsistentStoreReplacementError + .unsafeFile(destinationURL) + } + + if suffix == "-shm", !includingSharedMemory { + continue + } + + switch pathKind(at: sourceURL) { + case .missing: + if suffix.isEmpty { + throw CrashConsistentStoreReplacementError + .sourceStoreMissing + } + continue + case .regularFile: + break + case .directory, .symbolicLink, .other: + throw CrashConsistentStoreReplacementError + .unsafeFile(sourceURL) + } + + let values = try lstatValues(at: sourceURL) + let addition = + aggregateByteCount.addingReportingOverflow( + values.byteCount + ) + guard !addition.overflow else { + throw CrashConsistentStoreReplacementError + .storeFamilySizeOverflow + } + aggregateByteCount = addition.partialValue + guard + aggregateByteCount <= + limits.maximumFamilyByteCount + else { + throw CrashConsistentStoreReplacementError + .storeFamilyTooLarge( + actualByteCount: aggregateByteCount, + maximumByteCount: + limits.maximumFamilyByteCount + ) + } + + sourceMembers.append( + SourceMember( + sourceURL: sourceURL, + destinationURL: destinationURL, + byteCount: values.byteCount, + deviceID: values.deviceID, + inode: values.inode + ) + ) + } + + let requiredCapacity = + aggregateByteCount.addingReportingOverflow( + limits.minimumFreeStorageReserveByteCount + ) + guard !requiredCapacity.overflow else { + throw CrashConsistentStoreReplacementError + .storeFamilySizeOverflow + } + let availableByteCount = try capacityProvider( + destinationDirectoryURL + ) + guard + availableByteCount >= requiredCapacity.partialValue + else { + throw CrashConsistentStoreReplacementError + .insufficientStorageCapacity( + requiredByteCount: + requiredCapacity.partialValue, + availableByteCount: availableByteCount + ) + } + + for member in sourceMembers { + let sourceBeforeCopy = try lstatValues( + at: member.sourceURL + ) + guard + sourceBeforeCopy.byteCount == member.byteCount, + sourceBeforeCopy.deviceID == member.deviceID, + sourceBeforeCopy.inode == member.inode, + pathKind(at: member.destinationURL) == .missing + else { + throw CrashConsistentStoreReplacementError + .familyMismatch + } + + try fileManager.copyItem( + at: member.sourceURL, + to: member.destinationURL + ) + + let sourceAfterCopy = try lstatValues( + at: member.sourceURL + ) + let destinationValues = try lstatValues( + at: member.destinationURL + ) + guard + pathKind(at: member.destinationURL) == + .regularFile, + sourceAfterCopy.byteCount == member.byteCount, + sourceAfterCopy.deviceID == member.deviceID, + sourceAfterCopy.inode == member.inode, + destinationValues.byteCount == member.byteCount + else { + throw CrashConsistentStoreReplacementError + .familyMismatch + } + } + } + + private enum PathKind: Equatable { + case missing + case regularFile + case directory + case symbolicLink + case other + } + + private static func availableCapacity( + at directoryURL: URL, + fileManager: FileManager + ) throws -> UInt64 { + let keys: Set = [ + .volumeAvailableCapacityForImportantUsageKey, + .volumeAvailableCapacityKey + ] + if let values = try? directoryURL.resourceValues(forKeys: keys) { + if + let capacity = + values.volumeAvailableCapacityForImportantUsage, + capacity >= 0 { + return UInt64(capacity) + } + if + let capacity = values.volumeAvailableCapacity, + capacity >= 0 { + return UInt64(capacity) + } + } + + let attributes = try fileManager.attributesOfFileSystem( + forPath: directoryURL.path + ) + if + let freeSize = attributes[.systemFreeSize] as? NSNumber, + freeSize.int64Value >= 0 { + return freeSize.uint64Value + } + + throw CocoaError(.fileReadUnknown) + } + + private static func requireDirectory(_ url: URL) throws { + guard pathKind(at: url) == .directory else { + throw CrashConsistentStoreReplacementError + .unsafeDirectory(url) + } + } + + private static func familyURL( + _ storeURL: URL, + suffix: String + ) -> URL { + URL(fileURLWithPath: storeURL.path + suffix) + } + + private static func pathKind(at url: URL) -> PathKind { + var fileStatus = stat() + guard Darwin.lstat(url.path, &fileStatus) == 0 else { + return errno == ENOENT ? .missing : .other + } + + switch fileStatus.st_mode & S_IFMT { + case S_IFREG: + return .regularFile + case S_IFDIR: + return .directory + case S_IFLNK: + return .symbolicLink + default: + return .other + } + } + + private static func lstatValues( + at url: URL + ) throws -> ( + byteCount: UInt64, + deviceID: UInt64, + inode: UInt64 + ) { + var fileStatus = stat() + guard Darwin.lstat(url.path, &fileStatus) == 0 else { + throw POSIXError( + POSIXErrorCode(rawValue: errno) ?? .EIO + ) + } + guard + fileStatus.st_size >= 0, + fileStatus.st_mode & S_IFMT == S_IFREG + else { + throw CrashConsistentStoreReplacementError + .unsafeFile(url) + } + + return ( + byteCount: UInt64(fileStatus.st_size), + deviceID: UInt64(truncatingIfNeeded: fileStatus.st_dev), + inode: UInt64(truncatingIfNeeded: fileStatus.st_ino) + ) + } +} + +/// Replaces a closed SQLite store family while retaining a bounded, durable +/// rollback transaction beside the database. The transaction is deliberately +/// independent of the application's temporary directory so an iOS process +/// termination cannot discard the sole recovery copy. +final class CrashConsistentStoreReplacer { + private enum TransactionState: String, Codable { + case preparing + case committing + case rollingBack + case committed + } + + private struct FamilyMember: Codable, Equatable { + let suffix: String + let byteCount: UInt64 + let sha256: String + } + + private struct Marker: Codable { + let schemaVersion: Int + let storeFileName: String + var state: TransactionState + let originalFamily: [FamilyMember] + var committedFamily: [FamilyMember]? + } + + private enum PathKind { + case missing + case regularFile + case directory + case symbolicLink + case other + } + + private struct FileIdentity: Hashable { + let deviceID: UInt64 + let inode: UInt64 + } + + private struct StrictJSONField { + let key: String + let keyUsedEscape: Bool + let value: StrictJSONValue + } + + private indirect enum StrictJSONValue { + case object([StrictJSONField]) + case array([StrictJSONValue]) + case string(String) + case number(String) + case boolean(Bool) + case null + } + + private struct StrictJSONParser { + private static let maximumDepth = 8 + private static let maximumCollectionCount = 32 + + private let bytes: [UInt8] + private var index = 0 + + init(data: Data) { + bytes = Array(data) + } + + mutating func parse() throws -> StrictJSONValue { + skipWhitespace() + let value = try parseValue(depth: 0) + skipWhitespace() + guard index == bytes.count else { + throw CrashConsistentStoreReplacementError.invalidMarker + } + return value + } + + private mutating func parseValue( + depth: Int + ) throws -> StrictJSONValue { + guard + depth <= Self.maximumDepth, + let byte = currentByte + else { + throw CrashConsistentStoreReplacementError.invalidMarker + } + + switch byte { + case 0x7B: + return .object(try parseObject(depth: depth + 1)) + case 0x5B: + return .array(try parseArray(depth: depth + 1)) + case 0x22: + return .string(try parseString().value) + case 0x2D, 0x30 ... 0x39: + return .number(try parseNumber()) + case 0x74: + try consumeLiteral([0x74, 0x72, 0x75, 0x65]) + return .boolean(true) + case 0x66: + try consumeLiteral([ + 0x66, 0x61, 0x6C, 0x73, 0x65 + ]) + return .boolean(false) + case 0x6E: + try consumeLiteral([0x6E, 0x75, 0x6C, 0x6C]) + return .null + default: + throw CrashConsistentStoreReplacementError.invalidMarker + } + } + + private mutating func parseObject( + depth: Int + ) throws -> [StrictJSONField] { + try consume(0x7B) + skipWhitespace() + if consumeIfPresent(0x7D) { + return [] + } + + var fields: [StrictJSONField] = [] + while true { + guard fields.count < Self.maximumCollectionCount else { + throw CrashConsistentStoreReplacementError + .invalidMarker + } + + let key = try parseString() + skipWhitespace() + try consume(0x3A) + skipWhitespace() + let value = try parseValue(depth: depth) + fields.append( + StrictJSONField( + key: key.value, + keyUsedEscape: key.usedEscape, + value: value + ) + ) + + skipWhitespace() + if consumeIfPresent(0x7D) { + return fields + } + + try consume(0x2C) + skipWhitespace() + } + } + + private mutating func parseArray( + depth: Int + ) throws -> [StrictJSONValue] { + try consume(0x5B) + skipWhitespace() + if consumeIfPresent(0x5D) { + return [] + } + + var values: [StrictJSONValue] = [] + while true { + guard values.count < Self.maximumCollectionCount else { + throw CrashConsistentStoreReplacementError + .invalidMarker + } + + values.append(try parseValue(depth: depth)) + skipWhitespace() + if consumeIfPresent(0x5D) { + return values + } + + try consume(0x2C) + skipWhitespace() + } + } + + private mutating func parseString() throws -> ( + value: String, + usedEscape: Bool + ) { + guard currentByte == 0x22 else { + throw CrashConsistentStoreReplacementError.invalidMarker + } + + let startIndex = index + index += 1 + var usedEscape = false + + while let byte = currentByte { + switch byte { + case 0x22: + index += 1 + let token = Data(bytes[startIndex ..< index]) + do { + return ( + try JSONDecoder().decode( + String.self, + from: token + ), + usedEscape + ) + } catch { + throw CrashConsistentStoreReplacementError + .invalidMarker + } + case 0x5C: + usedEscape = true + index += 1 + guard let escapedByte = currentByte else { + throw CrashConsistentStoreReplacementError + .invalidMarker + } + + switch escapedByte { + case 0x22, 0x2F, 0x5C, 0x62, 0x66, 0x6E, + 0x72, 0x74: + index += 1 + case 0x75: + index += 1 + guard index + 4 <= bytes.count else { + throw CrashConsistentStoreReplacementError + .invalidMarker + } + for hexByte in bytes[index ..< index + 4] { + guard Self.isHexadecimal(hexByte) else { + throw CrashConsistentStoreReplacementError + .invalidMarker + } + } + index += 4 + default: + throw CrashConsistentStoreReplacementError + .invalidMarker + } + case 0x00 ... 0x1F: + throw CrashConsistentStoreReplacementError + .invalidMarker + default: + index += 1 + } + } + + throw CrashConsistentStoreReplacementError.invalidMarker + } + + private mutating func parseNumber() throws -> String { + let startIndex = index + + if consumeIfPresent(0x2D) { + guard currentByte != nil else { + throw CrashConsistentStoreReplacementError + .invalidMarker + } + } + + if consumeIfPresent(0x30) { + if + let byte = currentByte, + Self.isDigit(byte) { + throw CrashConsistentStoreReplacementError + .invalidMarker + } + } else { + guard + let byte = currentByte, + (0x31 ... 0x39).contains(byte) + else { + throw CrashConsistentStoreReplacementError + .invalidMarker + } + index += 1 + while + let byte = currentByte, + Self.isDigit(byte) { + index += 1 + } + } + + if consumeIfPresent(0x2E) { + guard + let byte = currentByte, + Self.isDigit(byte) + else { + throw CrashConsistentStoreReplacementError + .invalidMarker + } + while + let byte = currentByte, + Self.isDigit(byte) { + index += 1 + } + } + + if + currentByte == 0x65 || + currentByte == 0x45 { + index += 1 + if currentByte == 0x2B || currentByte == 0x2D { + index += 1 + } + guard + let byte = currentByte, + Self.isDigit(byte) + else { + throw CrashConsistentStoreReplacementError + .invalidMarker + } + while + let byte = currentByte, + Self.isDigit(byte) { + index += 1 + } + } + + return String( + decoding: bytes[startIndex ..< index], + as: UTF8.self + ) + } + + private mutating func consumeLiteral( + _ literal: [UInt8] + ) throws { + guard + index + literal.count <= bytes.count, + Array(bytes[index ..< index + literal.count]) == + literal + else { + throw CrashConsistentStoreReplacementError.invalidMarker + } + index += literal.count + } + + private mutating func consume(_ byte: UInt8) throws { + guard consumeIfPresent(byte) else { + throw CrashConsistentStoreReplacementError.invalidMarker + } + } + + private mutating func consumeIfPresent( + _ byte: UInt8 + ) -> Bool { + guard currentByte == byte else { + return false + } + index += 1 + return true + } + + private mutating func skipWhitespace() { + while + let byte = currentByte, + byte == 0x20 || + byte == 0x09 || + byte == 0x0A || + byte == 0x0D { + index += 1 + } + } + + private var currentByte: UInt8? { + index < bytes.count ? bytes[index] : nil + } + + private static func isDigit(_ byte: UInt8) -> Bool { + (0x30 ... 0x39).contains(byte) + } + + private static func isHexadecimal(_ byte: UInt8) -> Bool { + (0x30 ... 0x39).contains(byte) || + (0x41 ... 0x46).contains(byte) || + (0x61 ... 0x66).contains(byte) + } + } + + static let storeFamilySuffixes = ["", "-wal", "-shm", "-journal"] + static let defaultMaximumStoreFamilyByteCount: UInt64 = + 4 * 1024 * 1024 * 1024 + static let defaultMinimumFreeStorageReserveByteCount: UInt64 = + 64 * 1024 * 1024 + + private static let markerSchemaVersion = 1 + private static let maximumMarkerByteCount: UInt64 = 64 * 1024 + private static let digestChunkByteCount = 1024 * 1024 + private static let recoveryRootName = ".FearlessStoreReplacement" + + let storeURL: URL + let transactionDirectoryURL: URL + + private let fileManager: FileManager + private let storeReplacer: (URL, URL) throws -> Void + private let boundaryHook: (CrashConsistentStoreReplacementBoundary) throws -> Void + private let maximumStoreFamilyByteCount: UInt64 + private let minimumFreeStorageReserveByteCount: UInt64 + private let availableCapacityProvider: (URL) throws -> UInt64 + + private var databaseDirectoryURL: URL { + storeURL.deletingLastPathComponent() + } + + private var recoveryRootURL: URL { + transactionDirectoryURL.deletingLastPathComponent() + } + + private var backupDirectoryURL: URL { + transactionDirectoryURL.appendingPathComponent( + "OriginalStoreFamily", + isDirectory: true + ) + } + + private var markerURL: URL { + transactionDirectoryURL.appendingPathComponent("transaction.json") + } + + private var markerTemporaryURL: URL { + transactionDirectoryURL.appendingPathComponent( + "transaction.json.pending" + ) + } + + var hasPendingTransaction: Bool { + pathKind(at: transactionDirectoryURL) != .missing + } + + init( + storeURL: URL, + fileManager: FileManager, + storeReplacer: @escaping (URL, URL) throws -> Void, + boundaryHook: @escaping ( + CrashConsistentStoreReplacementBoundary + ) throws -> Void = { _ in }, + maximumStoreFamilyByteCount: UInt64 = + CrashConsistentStoreReplacer + .defaultMaximumStoreFamilyByteCount, + minimumFreeStorageReserveByteCount: UInt64 = + CrashConsistentStoreReplacer + .defaultMinimumFreeStorageReserveByteCount, + availableCapacityProvider: ((URL) throws -> UInt64)? = nil + ) { + self.storeURL = storeURL.standardizedFileURL + self.fileManager = fileManager + self.storeReplacer = storeReplacer + self.boundaryHook = boundaryHook + self.maximumStoreFamilyByteCount = + maximumStoreFamilyByteCount + self.minimumFreeStorageReserveByteCount = + minimumFreeStorageReserveByteCount + self.availableCapacityProvider = + availableCapacityProvider ?? { directoryURL in + let keys: Set = [ + .volumeAvailableCapacityForImportantUsageKey, + .volumeAvailableCapacityKey + ] + if + let values = try? directoryURL.resourceValues( + forKeys: keys + ) { + if + let capacity = + values + .volumeAvailableCapacityForImportantUsage, + capacity >= 0 { + return UInt64(capacity) + } + + if + let capacity = + values.volumeAvailableCapacity, + capacity >= 0 { + return UInt64(capacity) + } + } + + let attributes = try fileManager + .attributesOfFileSystem( + forPath: directoryURL.path + ) + if + let freeSize = + attributes[.systemFreeSize] as? NSNumber, + freeSize.int64Value >= 0 { + return freeSize.uint64Value + } + + throw CocoaError(.fileReadUnknown) + } + + let pathDigest = SHA256.hash( + data: Data(self.storeURL.path.utf8) + ).prefix(12).map { + String(format: "%02x", $0) + }.joined() + let safeStoreName = self.storeURL.lastPathComponent.map { + $0.isLetter || $0.isNumber || $0 == "." || $0 == "-" || $0 == "_" + ? String($0) + : "_" + }.joined() + let boundedStoreName = String(safeStoreName.prefix(80)) + transactionDirectoryURL = self.storeURL + .deletingLastPathComponent() + .appendingPathComponent(Self.recoveryRootName, isDirectory: true) + .appendingPathComponent( + "\(boundedStoreName)-\(pathDigest)", + isDirectory: true + ) + } + + func liveStoreExistsSafely() throws -> Bool { + try requireSafeDatabaseDirectory() + + switch pathKind(at: storeURL) { + case .regularFile: + for suffix in Self.storeFamilySuffixes.dropFirst() { + let sidecarURL = familyURL( + storeURL: storeURL, + suffix: suffix + ) + + switch pathKind(at: sidecarURL) { + case .missing, .regularFile: + continue + case .directory, .symbolicLink, .other: + throw CrashConsistentStoreReplacementError + .unsafeFile(sidecarURL) + } + } + + return true + case .missing: + let hasOrphanSidecar = Self.storeFamilySuffixes + .dropFirst() + .contains { + pathKind( + at: familyURL( + storeURL: storeURL, + suffix: $0 + ) + ) != .missing + } + + guard !hasOrphanSidecar else { + throw CrashConsistentStoreReplacementError + .markerMissingForInterruptedStore + } + + return false + case .directory, .symbolicLink, .other: + throw CrashConsistentStoreReplacementError + .unsafeFile(storeURL) + } + } + + func reconcile( + validateCommittedStore: (URL) throws -> Void + ) throws { + try requireSafeDatabaseDirectory() + + switch pathKind(at: transactionDirectoryURL) { + case .missing: + try removeRecoveryRootIfEmpty() + return + case .directory: + break + case .regularFile, .symbolicLink, .other: + throw CrashConsistentStoreReplacementError + .unsafeDirectory(transactionDirectoryURL) + } + + guard pathKind(at: markerURL) != .missing else { + try reconcileMarkerlessTransaction() + return + } + + let marker = try readMarker() + try requireSafeTransactionTree( + for: marker, + verifyBackupContents: false + ) + try removeMarkerTemporaryIfPresent() + + switch marker.state { + case .preparing: + if try family(at: storeURL, matches: marker.originalFamily) { + try removeTransaction( + using: marker, + verifyBackupContents: false + ) + } else { + try restoreOriginalFamily(using: marker) + } + case .committing: + try restoreOriginalFamily(using: marker) + case .rollingBack: + if try family(at: storeURL, matches: marker.originalFamily) { + try removeTransaction( + using: marker, + verifyBackupContents: false + ) + } else { + try restoreOriginalFamily(using: marker) + } + case .committed: + do { + guard + let committedFamily = marker.committedFamily, + try family(at: storeURL, matches: committedFamily) + else { + throw CrashConsistentStoreReplacementError.familyMismatch + } + + try validateCommittedStoreUsingPrivateCopy( + committedFamily, + validateCommittedStore: validateCommittedStore + ) + } catch is CrashConsistentStoreReplacementInterruption { + throw CrashConsistentStoreReplacementInterruption + .simulatedProcessDeath + } catch { + if Self.isRetryableCapacityError(error) { + // A committed live family and its rollback copy are both + // still durable. Leave the transaction intact so a later + // launch can validate it after the user frees space. + throw error + } + + try restoreOriginalFamily(using: marker) + return + } + + try removeTransaction(using: marker) + } + } + + func replaceStore( + with stagedStoreURL: URL, + validateDestination: (URL) throws -> Void + ) throws { + let stagedStoreURL = stagedStoreURL.standardizedFileURL + try reconcile(validateCommittedStore: validateDestination) + try requireSafeDatabaseDirectory() + let stagedFamily = try validateStagedStoreFamily( + at: stagedStoreURL + ) + + let originalFamily = try manifest( + for: storeURL, + requiringMainStore: true + ) + try requireCapacity( + forAdditionalByteCount: + try aggregateByteCount(of: originalFamily), + at: databaseDirectoryURL + ) + var marker = Marker( + schemaVersion: Self.markerSchemaVersion, + storeFileName: storeURL.lastPathComponent, + state: .preparing, + originalFamily: originalFamily, + committedFamily: nil + ) + var commitDurablyRecorded = false + + try createTransactionDirectory() + try persist(marker) + try boundaryHook(.preparingMarkerPersisted) + try createBackupDirectory() + + do { + try copyOriginalFamily(originalFamily) + + guard try family(at: backupStoreURL, matches: originalFamily) else { + throw CrashConsistentStoreReplacementError.backupUnavailable + } + + marker.state = .committing + try persist(marker) + try boundaryHook(.committingMarkerPersisted) + + guard + try validateStagedStoreFamily( + at: stagedStoreURL + ) == stagedFamily + else { + throw CrashConsistentStoreReplacementError + .familyMismatch + } + try storeReplacer(storeURL, stagedStoreURL) + try boundaryHook(.replacementReturned) + + do { + try validateDestination(storeURL) + } catch { + throw CrashConsistentStoreReplacementError + .destinationValidationFailed(error) + } + try boundaryHook(.destinationValidated) + + try synchronizeFamily(at: storeURL) + try boundaryHook(.destinationFamilySynced) + + marker.state = .committed + marker.committedFamily = try manifest( + for: storeURL, + requiringMainStore: true + ) + try persist(marker) + commitDurablyRecorded = true + try boundaryHook(.committedMarkerPersisted) + + try removeTransaction(using: marker) + } catch is CrashConsistentStoreReplacementInterruption { + throw CrashConsistentStoreReplacementInterruption + .simulatedProcessDeath + } catch { + let replacementError = error + + guard !commitDurablyRecorded else { + throw replacementError + } + + do { + if try family(at: backupStoreURL, matches: originalFamily) { + try restoreOriginalFamily(using: marker) + } else if try family(at: storeURL, matches: originalFamily) { + try removeTransaction( + using: marker, + verifyBackupContents: false + ) + } else { + throw CrashConsistentStoreReplacementError.backupUnavailable + } + } catch is CrashConsistentStoreReplacementInterruption { + throw CrashConsistentStoreReplacementInterruption + .simulatedProcessDeath + } catch { + throw CrashConsistentStoreReplacementError.rollbackFailed( + replacementError, + error + ) + } + + throw replacementError + } + } + + private var backupStoreURL: URL { + backupDirectoryURL.appendingPathComponent(storeURL.lastPathComponent) + } + + private func createTransactionDirectory() throws { + switch pathKind(at: recoveryRootURL) { + case .missing: + try fileManager.createDirectory( + at: recoveryRootURL, + withIntermediateDirectories: false + ) + try synchronizeDirectory(databaseDirectoryURL) + case .directory: + break + case .regularFile, .symbolicLink, .other: + throw CrashConsistentStoreReplacementError + .unsafeDirectory(recoveryRootURL) + } + + guard pathKind(at: transactionDirectoryURL) == .missing else { + throw CrashConsistentStoreReplacementError + .unsafeDirectory(transactionDirectoryURL) + } + + try fileManager.createDirectory( + at: transactionDirectoryURL, + withIntermediateDirectories: false + ) + try synchronizeDirectory(recoveryRootURL) + } + + private func createBackupDirectory() throws { + guard pathKind(at: backupDirectoryURL) == .missing else { + throw CrashConsistentStoreReplacementError + .unsafeDirectory(backupDirectoryURL) + } + try fileManager.createDirectory( + at: backupDirectoryURL, + withIntermediateDirectories: false + ) + try synchronizeDirectory(transactionDirectoryURL) + } + + private func copyOriginalFamily( + _ originalFamily: [FamilyMember] + ) throws { + for member in originalFamily { + let sourceURL = familyURL( + storeURL: storeURL, + suffix: member.suffix + ) + let destinationURL = familyURL( + storeURL: backupStoreURL, + suffix: member.suffix + ) + + try requireRegularFile(sourceURL) + guard pathKind(at: destinationURL) == .missing else { + throw CrashConsistentStoreReplacementError + .unsafeFile(destinationURL) + } + + try fileManager.copyItem(at: sourceURL, to: destinationURL) + try synchronizeFile(destinationURL) + try synchronizeDirectory(backupDirectoryURL) + + guard + try familyMember( + at: destinationURL, + suffix: member.suffix, + expectedByteCount: member.byteCount + ) == member + else { + throw CrashConsistentStoreReplacementError.familyMismatch + } + + try boundaryHook(.backupFileCopied(member.suffix)) + } + } + + private func restoreOriginalFamily( + using originalMarker: Marker + ) throws { + guard + try family( + at: backupStoreURL, + matches: originalMarker.originalFamily + ) + else { + throw CrashConsistentStoreReplacementError.backupUnavailable + } + + var marker = originalMarker + marker.state = .rollingBack + marker.committedFamily = nil + try persist(marker) + try boundaryHook(.rollbackMarkerPersisted) + + for suffix in Self.storeFamilySuffixes { + let liveURL = familyURL(storeURL: storeURL, suffix: suffix) + + switch pathKind(at: liveURL) { + case .missing: + continue + case .regularFile: + try fileManager.removeItem(at: liveURL) + try synchronizeDirectory(databaseDirectoryURL) + try boundaryHook(.liveFileRemoved(suffix)) + case .directory, .symbolicLink, .other: + throw CrashConsistentStoreReplacementError + .unsafeFile(liveURL) + } + } + + for member in marker.originalFamily { + let backupURL = familyURL( + storeURL: backupStoreURL, + suffix: member.suffix + ) + let liveURL = familyURL( + storeURL: storeURL, + suffix: member.suffix + ) + let temporaryURL = databaseDirectoryURL.appendingPathComponent( + ".\(liveURL.lastPathComponent).fearless-rollback" + ) + + try removeSafeTemporaryFileIfPresent(at: temporaryURL) + try fileManager.copyItem(at: backupURL, to: temporaryURL) + try synchronizeFile(temporaryURL) + guard + try familyMember( + at: temporaryURL, + suffix: member.suffix, + expectedByteCount: member.byteCount + ) == member + else { + throw CrashConsistentStoreReplacementError.familyMismatch + } + try synchronizeDirectory(databaseDirectoryURL) + try boundaryHook( + .rollbackTemporaryFileCopied(member.suffix) + ) + + try fileManager.moveItem(at: temporaryURL, to: liveURL) + try synchronizeDirectory(databaseDirectoryURL) + try boundaryHook(.rollbackFileInstalled(member.suffix)) + } + + guard try family(at: storeURL, matches: marker.originalFamily) else { + throw CrashConsistentStoreReplacementError.familyMismatch + } + try boundaryHook(.rollbackFamilyVerified) + + try removeTransaction(using: marker) + } + + private func removeSafeTemporaryFileIfPresent(at url: URL) throws { + switch pathKind(at: url) { + case .missing: + return + case .regularFile: + try fileManager.removeItem(at: url) + try synchronizeDirectory(databaseDirectoryURL) + case .directory, .symbolicLink, .other: + throw CrashConsistentStoreReplacementError.unsafeFile(url) + } + } + + private func removeTransaction( + using marker: Marker, + verifyBackupContents: Bool = true + ) throws { + try requireSafeTransactionTree( + for: marker, + verifyBackupContents: verifyBackupContents + ) + + if pathKind(at: backupDirectoryURL) == .directory { + for member in marker.originalFamily { + let backupURL = familyURL( + storeURL: backupStoreURL, + suffix: member.suffix + ) + + switch pathKind(at: backupURL) { + case .missing: + continue + case .regularFile: + if verifyBackupContents { + guard + try familyMember( + at: backupURL, + suffix: member.suffix, + expectedByteCount: + member.byteCount + ) == member + else { + throw CrashConsistentStoreReplacementError + .familyMismatch + } + } + try fileManager.removeItem(at: backupURL) + try synchronizeDirectory(backupDirectoryURL) + try boundaryHook( + .cleanupBackupFileRemoved(member.suffix) + ) + case .directory, .symbolicLink, .other: + throw CrashConsistentStoreReplacementError + .unsafeFile(backupURL) + } + } + + guard + try fileManager.contentsOfDirectory( + at: backupDirectoryURL, + includingPropertiesForKeys: nil, + options: [] + ).isEmpty + else { + throw CrashConsistentStoreReplacementError + .invalidMarker + } + try fileManager.removeItem(at: backupDirectoryURL) + try synchronizeDirectory(transactionDirectoryURL) + try boundaryHook(.cleanupBackupDirectoryRemoved) + } + + try removeMarkerTemporaryIfPresent() + + try requireRegularFile(markerURL) + try fileManager.removeItem(at: markerURL) + try synchronizeDirectory(transactionDirectoryURL) + try boundaryHook(.cleanupMarkerRemoved) + + guard + try fileManager.contentsOfDirectory( + at: transactionDirectoryURL, + includingPropertiesForKeys: nil, + options: [] + ).isEmpty + else { + throw CrashConsistentStoreReplacementError.invalidMarker + } + try removeEmptyTransactionDirectory() + } + + private func removeMarkerTemporaryIfPresent() throws { + switch pathKind(at: markerTemporaryURL) { + case .missing: + return + case .regularFile: + let values = try lstatValues(at: markerTemporaryURL) + guard + values.byteCount <= Self.maximumMarkerByteCount + else { + throw CrashConsistentStoreReplacementError.markerTooLarge + } + try fileManager.removeItem(at: markerTemporaryURL) + try synchronizeDirectory(transactionDirectoryURL) + try boundaryHook(.cleanupMarkerTemporaryRemoved) + case .directory, .symbolicLink, .other: + throw CrashConsistentStoreReplacementError + .unsafeFile(markerTemporaryURL) + } + } + + private func reconcileMarkerlessTransaction() throws { + let transactionContents = try fileManager.contentsOfDirectory( + at: transactionDirectoryURL, + includingPropertiesForKeys: nil, + options: [] + ) + + guard try liveStoreExistsSafely() else { + throw CrashConsistentStoreReplacementError + .markerMissingForInterruptedStore + } + + if transactionContents.isEmpty { + try removeEmptyTransactionDirectory() + return + } + + guard + transactionContents.count == 1, + transactionContents[0].lastPathComponent == + markerTemporaryURL.lastPathComponent + else { + throw CrashConsistentStoreReplacementError + .markerMissingForInterruptedStore + } + + switch pathKind(at: markerTemporaryURL) { + case .regularFile: + let values = try lstatValues(at: markerTemporaryURL) + guard + values.byteCount <= Self.maximumMarkerByteCount + else { + throw CrashConsistentStoreReplacementError.markerTooLarge + } + case .missing: + throw CrashConsistentStoreReplacementError + .markerMissingForInterruptedStore + case .directory, .symbolicLink, .other: + throw CrashConsistentStoreReplacementError + .unsafeFile(markerTemporaryURL) + } + + try removeMarkerTemporaryIfPresent() + try removeEmptyTransactionDirectory() + } + + private func removeEmptyTransactionDirectory() throws { + guard pathKind(at: transactionDirectoryURL) == .directory else { + throw CrashConsistentStoreReplacementError + .unsafeDirectory(transactionDirectoryURL) + } + + guard + try fileManager.contentsOfDirectory( + at: transactionDirectoryURL, + includingPropertiesForKeys: nil, + options: [] + ).isEmpty + else { + throw CrashConsistentStoreReplacementError + .markerMissingForInterruptedStore + } + + try fileManager.removeItem(at: transactionDirectoryURL) + try synchronizeDirectory(recoveryRootURL) + try boundaryHook(.cleanupTransactionDirectoryRemoved) + try removeRecoveryRootIfEmpty() + try boundaryHook(.transactionCleaned) + } + + private func removeRecoveryRootIfEmpty() throws { + switch pathKind(at: recoveryRootURL) { + case .missing: + return + case .directory: + guard + try fileManager.contentsOfDirectory( + at: recoveryRootURL, + includingPropertiesForKeys: nil, + options: [] + ).isEmpty + else { + return + } + + try fileManager.removeItem(at: recoveryRootURL) + try synchronizeDirectory(databaseDirectoryURL) + try boundaryHook(.cleanupRecoveryRootRemoved) + case .regularFile, .symbolicLink, .other: + throw CrashConsistentStoreReplacementError + .unsafeDirectory(recoveryRootURL) + } + } + + private func requireSafeTransactionTree( + for marker: Marker, + verifyBackupContents: Bool = true + ) throws { + guard pathKind(at: transactionDirectoryURL) == .directory else { + throw CrashConsistentStoreReplacementError + .unsafeDirectory(transactionDirectoryURL) + } + + let contents = try fileManager.contentsOfDirectory( + at: transactionDirectoryURL, + includingPropertiesForKeys: nil, + options: [] + ) + let allowedTopLevelNames: Set = [ + backupDirectoryURL.lastPathComponent, + markerURL.lastPathComponent, + markerTemporaryURL.lastPathComponent + ] + guard + contents.allSatisfy({ + allowedTopLevelNames.contains($0.lastPathComponent) + }), + contents.contains(where: { + $0.lastPathComponent == markerURL.lastPathComponent + }) + else { + throw CrashConsistentStoreReplacementError.invalidMarker + } + + for url in contents { + switch url.lastPathComponent { + case backupDirectoryURL.lastPathComponent: + guard pathKind(at: url) == .directory else { + throw CrashConsistentStoreReplacementError + .unsafeDirectory(url) + } + try requireSafeBackupTree( + for: marker, + verifyContents: verifyBackupContents + ) + case markerURL.lastPathComponent: + try requireRegularFile(url) + case markerTemporaryURL.lastPathComponent: + try requireRegularFile(url) + let values = try lstatValues(at: url) + guard + values.byteCount <= + Self.maximumMarkerByteCount + else { + throw CrashConsistentStoreReplacementError + .markerTooLarge + } + default: + throw CrashConsistentStoreReplacementError + .invalidMarker + } + } + } + + private func requireSafeBackupTree( + for marker: Marker, + verifyContents: Bool + ) throws { + let expectedMembers = Dictionary( + uniqueKeysWithValues: marker.originalFamily.map { + ( + storeURL.lastPathComponent + $0.suffix, + $0 + ) + } + ) + let contents = try fileManager.contentsOfDirectory( + at: backupDirectoryURL, + includingPropertiesForKeys: nil, + options: [] + ) + + for url in contents { + guard + let expectedMember = + expectedMembers[url.lastPathComponent] + else { + throw CrashConsistentStoreReplacementError + .invalidMarker + } + try requireRegularFile(url) + if verifyContents { + guard + try familyMember( + at: url, + suffix: expectedMember.suffix, + expectedByteCount: + expectedMember.byteCount + ) == expectedMember + else { + throw CrashConsistentStoreReplacementError + .familyMismatch + } + } + } + } + + private func persist(_ marker: Marker) throws { + try validate(marker) + + let encoder = JSONEncoder() + encoder.outputFormatting = [.sortedKeys] + let data = try encoder.encode(marker) + guard data.count <= Self.maximumMarkerByteCount else { + throw CrashConsistentStoreReplacementError.markerTooLarge + } + try validateRawMarkerJSON(data) + + switch pathKind(at: markerURL) { + case .missing, .regularFile: + break + case .directory, .symbolicLink, .other: + throw CrashConsistentStoreReplacementError.unsafeFile(markerURL) + } + + guard pathKind(at: markerTemporaryURL) == .missing else { + throw CrashConsistentStoreReplacementError + .unsafeFile(markerTemporaryURL) + } + try data.write(to: markerTemporaryURL) + try synchronizeFile(markerTemporaryURL) + + guard + Darwin.rename( + markerTemporaryURL.path, + markerURL.path + ) == 0 + else { + throw CrashConsistentStoreReplacementError + .fileSynchronizationFailed( + markerURL, + POSIXError( + POSIXErrorCode(rawValue: errno) ?? .EIO + ) + ) + } + try synchronizeFile(markerURL) + try synchronizeDirectory(transactionDirectoryURL) + } + + private func readMarker() throws -> Marker { + try requireRegularFile(markerURL) + let values = try lstatValues(at: markerURL) + guard + values.byteCount <= Self.maximumMarkerByteCount + else { + throw CrashConsistentStoreReplacementError.markerTooLarge + } + + let data = try Data(contentsOf: markerURL, options: [.mappedIfSafe]) + try validateRawMarkerJSON(data) + let marker: Marker + do { + marker = try JSONDecoder().decode(Marker.self, from: data) + } catch { + throw CrashConsistentStoreReplacementError.invalidMarker + } + + try validate(marker) + return marker + } + + private func validateRawMarkerJSON(_ data: Data) throws { + var parser = StrictJSONParser(data: data) + let value = try parser.parse() + guard case let .object(fields) = value else { + throw CrashConsistentStoreReplacementError.invalidMarker + } + + let requiredKeys: Set = [ + "schemaVersion", + "storeFileName", + "state", + "originalFamily" + ] + let allowedKeys = requiredKeys.union(["committedFamily"]) + let fieldMap = try exactFieldMap( + fields, + allowedKeys: allowedKeys + ) + guard requiredKeys.isSubset(of: Set(fieldMap.keys)) else { + throw CrashConsistentStoreReplacementError.invalidMarker + } + + guard + let schemaValue = fieldMap["schemaVersion"], + case let .number(schemaVersion) = schemaValue, + Int(schemaVersion) != nil, + let storeFileNameValue = fieldMap["storeFileName"], + case .string = storeFileNameValue, + let stateValue = fieldMap["state"], + case .string = stateValue, + let originalFamilyValue = fieldMap["originalFamily"], + case let .array(originalFamily) = originalFamilyValue + else { + throw CrashConsistentStoreReplacementError.invalidMarker + } + try validateRawFamily(originalFamily) + + if let committedFamily = fieldMap["committedFamily"] { + guard case let .array(members) = committedFamily else { + throw CrashConsistentStoreReplacementError.invalidMarker + } + try validateRawFamily(members) + } + } + + private func validateRawFamily( + _ values: [StrictJSONValue] + ) throws { + guard + !values.isEmpty, + values.count <= Self.storeFamilySuffixes.count + else { + throw CrashConsistentStoreReplacementError.invalidMarker + } + + for value in values { + guard case let .object(fields) = value else { + throw CrashConsistentStoreReplacementError.invalidMarker + } + let fieldMap = try exactFieldMap( + fields, + allowedKeys: ["suffix", "byteCount", "sha256"] + ) + guard + fieldMap.count == 3, + let suffixValue = fieldMap["suffix"], + case .string = suffixValue, + let byteCountValue = fieldMap["byteCount"], + case let .number(byteCount) = byteCountValue, + UInt64(byteCount) != nil, + let digestValue = fieldMap["sha256"], + case .string = digestValue + else { + throw CrashConsistentStoreReplacementError.invalidMarker + } + } + } + + private func exactFieldMap( + _ fields: [StrictJSONField], + allowedKeys: Set + ) throws -> [String: StrictJSONValue] { + guard + fields.allSatisfy({ + !$0.keyUsedEscape && + allowedKeys.contains($0.key) + }) + else { + throw CrashConsistentStoreReplacementError.invalidMarker + } + + var result: [String: StrictJSONValue] = [:] + for field in fields { + guard result.updateValue( + field.value, + forKey: field.key + ) == nil else { + throw CrashConsistentStoreReplacementError + .invalidMarker + } + } + return result + } + + private func validate(_ marker: Marker) throws { + guard + marker.schemaVersion == Self.markerSchemaVersion, + marker.storeFileName == storeURL.lastPathComponent + else { + throw CrashConsistentStoreReplacementError.invalidMarker + } + + try validate(marker.originalFamily) + + switch marker.state { + case .preparing, .committing, .rollingBack: + guard marker.committedFamily == nil else { + throw CrashConsistentStoreReplacementError.invalidMarker + } + case .committed: + guard let committedFamily = marker.committedFamily else { + throw CrashConsistentStoreReplacementError.invalidMarker + } + try validate(committedFamily) + } + } + + private func validate(_ family: [FamilyMember]) throws { + let allowedSuffixes = Set(Self.storeFamilySuffixes) + let suffixes = family.map(\.suffix) + let canonicalSuffixes = Self.storeFamilySuffixes.filter { + suffixes.contains($0) + } + var aggregateByteCount: UInt64 = 0 + var aggregateOverflowed = false + for member in family { + let addition = aggregateByteCount.addingReportingOverflow( + member.byteCount + ) + aggregateByteCount = addition.partialValue + aggregateOverflowed = aggregateOverflowed || addition.overflow + } + guard + family.count <= Self.storeFamilySuffixes.count, + !family.isEmpty, + suffixes.contains(""), + suffixes == canonicalSuffixes, + Set(suffixes).count == suffixes.count, + suffixes.allSatisfy(allowedSuffixes.contains), + !aggregateOverflowed, + aggregateByteCount <= maximumStoreFamilyByteCount, + family.allSatisfy({ + $0.sha256.count == 64 && + $0.sha256.allSatisfy { + $0.isHexDigit && !$0.isUppercase + } + }) + else { + throw CrashConsistentStoreReplacementError.invalidMarker + } + } + + private func validateStagedStoreFamily( + at stagedStoreURL: URL + ) throws -> [FamilyMember] { + let standardizedStagedURL = + stagedStoreURL.standardizedFileURL + let stagedFamily = try manifest( + for: standardizedStagedURL, + requiringMainStore: true + ) + let forbiddenDirectories = [ + recoveryRootURL, + transactionDirectoryURL, + backupDirectoryURL + ] + let forbiddenFiles = Self.storeFamilySuffixes.flatMap { + suffix in + + [ + familyURL(storeURL: storeURL, suffix: suffix), + familyURL( + storeURL: backupStoreURL, + suffix: suffix + ) + ] + } + [markerURL, markerTemporaryURL] + var stagedIdentities = Set() + + for forbiddenDirectory in forbiddenDirectories { + guard + !isWithin( + standardizedStagedURL, + directory: forbiddenDirectory + ), + !isWithin( + standardizedStagedURL + .resolvingSymlinksInPath(), + directory: forbiddenDirectory + .resolvingSymlinksInPath() + ) + else { + throw CrashConsistentStoreReplacementError + .unsafeFile(standardizedStagedURL) + } + } + + for member in stagedFamily { + let stagedMemberURL = familyURL( + storeURL: standardizedStagedURL, + suffix: member.suffix + ) + let stagedIdentity = try lstatValues( + at: stagedMemberURL + ) + guard + stagedIdentities.insert( + FileIdentity( + deviceID: stagedIdentity.deviceID, + inode: stagedIdentity.inode + ) + ).inserted + else { + throw CrashConsistentStoreReplacementError + .unsafeFile(stagedMemberURL) + } + + for forbiddenURL in forbiddenFiles { + guard + !urlsAlias(stagedMemberURL, forbiddenURL) + else { + throw CrashConsistentStoreReplacementError + .unsafeFile(stagedMemberURL) + } + + guard pathKind(at: forbiddenURL) == .regularFile else { + continue + } + let forbiddenIdentity = try lstatValues( + at: forbiddenURL + ) + guard + stagedIdentity.deviceID != + forbiddenIdentity.deviceID || + stagedIdentity.inode != + forbiddenIdentity.inode + else { + throw CrashConsistentStoreReplacementError + .unsafeFile(stagedMemberURL) + } + } + } + + return stagedFamily + } + + private func validateCommittedStoreUsingPrivateCopy( + _ committedFamily: [FamilyMember], + validateCommittedStore: (URL) throws -> Void + ) throws { + try requireCapacity( + forAdditionalByteCount: + try aggregateByteCount(of: committedFamily), + at: fileManager.temporaryDirectory + ) + + let validationDirectoryURL = fileManager.temporaryDirectory + .appendingPathComponent( + "FearlessCommittedStoreValidation-\(UUID().uuidString)", + isDirectory: true + ) + guard pathKind(at: validationDirectoryURL) == .missing else { + throw CrashConsistentStoreReplacementError + .unsafeDirectory(validationDirectoryURL) + } + + try fileManager.createDirectory( + at: validationDirectoryURL, + withIntermediateDirectories: false + ) + defer { + try? fileManager.removeItem(at: validationDirectoryURL) + } + + let validationStoreURL = validationDirectoryURL + .appendingPathComponent(storeURL.lastPathComponent) + for member in committedFamily { + let sourceURL = familyURL( + storeURL: storeURL, + suffix: member.suffix + ) + let destinationURL = familyURL( + storeURL: validationStoreURL, + suffix: member.suffix + ) + + try requireRegularFile(sourceURL) + guard pathKind(at: destinationURL) == .missing else { + throw CrashConsistentStoreReplacementError + .unsafeFile(destinationURL) + } + try fileManager.copyItem( + at: sourceURL, + to: destinationURL + ) + } + + guard + try family( + at: validationStoreURL, + matches: committedFamily + ) + else { + throw CrashConsistentStoreReplacementError.familyMismatch + } + + try validateCommittedStore(validationStoreURL) + + guard + try family( + at: storeURL, + matches: committedFamily + ) + else { + throw CrashConsistentStoreReplacementError.familyMismatch + } + } + + private func urlsAlias(_ lhs: URL, _ rhs: URL) -> Bool { + let standardizedLHS = lhs.standardizedFileURL + let standardizedRHS = rhs.standardizedFileURL + if standardizedLHS == standardizedRHS { + return true + } + + return standardizedLHS + .resolvingSymlinksInPath() + .standardizedFileURL == + standardizedRHS + .resolvingSymlinksInPath() + .standardizedFileURL + } + + private func isWithin( + _ candidate: URL, + directory: URL + ) -> Bool { + let candidateComponents = + candidate.standardizedFileURL.pathComponents + let directoryComponents = + directory.standardizedFileURL.pathComponents + + guard + candidateComponents.count >= + directoryComponents.count + else { + return false + } + + return Array( + candidateComponents.prefix(directoryComponents.count) + ) == directoryComponents + } + + private static func isRetryableCapacityError( + _ error: Error + ) -> Bool { + guard + let replacementError = + error as? CrashConsistentStoreReplacementError + else { + return false + } + + if case .insufficientStorageCapacity = replacementError { + return true + } + + return false + } + + private func aggregateByteCount( + of family: [FamilyMember] + ) throws -> UInt64 { + var result: UInt64 = 0 + + for member in family { + let addition = result.addingReportingOverflow( + member.byteCount + ) + guard !addition.overflow else { + throw CrashConsistentStoreReplacementError + .storeFamilySizeOverflow + } + result = addition.partialValue + guard result <= maximumStoreFamilyByteCount else { + throw CrashConsistentStoreReplacementError + .storeFamilyTooLarge( + actualByteCount: result, + maximumByteCount: + maximumStoreFamilyByteCount + ) + } + } + + return result + } + + private func requireCapacity( + forAdditionalByteCount additionalByteCount: UInt64, + at directoryURL: URL + ) throws { + let requirement = + additionalByteCount.addingReportingOverflow( + minimumFreeStorageReserveByteCount + ) + guard !requirement.overflow else { + throw CrashConsistentStoreReplacementError + .storeFamilySizeOverflow + } + + let availableByteCount = try availableCapacityProvider( + directoryURL + ) + guard availableByteCount >= requirement.partialValue else { + throw CrashConsistentStoreReplacementError + .insufficientStorageCapacity( + requiredByteCount: requirement.partialValue, + availableByteCount: availableByteCount + ) + } + } + + private func manifest( + for familyStoreURL: URL, + requiringMainStore: Bool + ) throws -> [FamilyMember] { + var memberSources: [ + ( + url: URL, + suffix: String, + byteCount: UInt64 + ) + ] = [] + var aggregateByteCount: UInt64 = 0 + + for suffix in Self.storeFamilySuffixes { + let url = familyURL( + storeURL: familyStoreURL, + suffix: suffix + ) + + switch pathKind(at: url) { + case .missing: + if suffix.isEmpty, requiringMainStore { + throw CrashConsistentStoreReplacementError + .sourceStoreMissing + } + case .regularFile: + let values = try lstatValues(at: url) + let addition = + aggregateByteCount.addingReportingOverflow( + values.byteCount + ) + guard !addition.overflow else { + throw CrashConsistentStoreReplacementError + .storeFamilySizeOverflow + } + aggregateByteCount = addition.partialValue + guard + aggregateByteCount <= + maximumStoreFamilyByteCount + else { + throw CrashConsistentStoreReplacementError + .storeFamilyTooLarge( + actualByteCount: + aggregateByteCount, + maximumByteCount: + maximumStoreFamilyByteCount + ) + } + memberSources.append( + ( + url: url, + suffix: suffix, + byteCount: values.byteCount + ) + ) + case .directory, .symbolicLink, .other: + throw CrashConsistentStoreReplacementError.unsafeFile(url) + } + } + + if + requiringMainStore, + !memberSources.contains(where: { + $0.suffix.isEmpty + }) + { + throw CrashConsistentStoreReplacementError.sourceStoreMissing + } + + // Size every member before hashing any of them. A sparse or corrupt + // family is therefore rejected in constant memory and without + // spending unbounded startup time in SHA-256. + return try memberSources.map { + try familyMember( + at: $0.url, + suffix: $0.suffix, + expectedByteCount: $0.byteCount + ) + } + } + + private func family( + at familyStoreURL: URL, + matches expectedFamily: [FamilyMember] + ) throws -> Bool { + do { + let actualFamily = try manifest( + for: familyStoreURL, + requiringMainStore: true + ) + return actualFamily == expectedFamily + } catch CrashConsistentStoreReplacementError.sourceStoreMissing { + return false + } + } + + private func familyMember( + at url: URL, + suffix: String, + expectedByteCount: UInt64? = nil + ) throws -> FamilyMember { + try requireRegularFile(url) + let valuesBeforeDigest = try lstatValues(at: url) + guard + valuesBeforeDigest.byteCount <= + maximumStoreFamilyByteCount + else { + throw CrashConsistentStoreReplacementError + .storeFamilyTooLarge( + actualByteCount: + valuesBeforeDigest.byteCount, + maximumByteCount: + maximumStoreFamilyByteCount + ) + } + if let expectedByteCount { + guard + valuesBeforeDigest.byteCount == expectedByteCount + else { + throw CrashConsistentStoreReplacementError + .familyMismatch + } + } + + let sha256 = try digest( + at: url, + expectedByteCount: valuesBeforeDigest.byteCount + ) + let valuesAfterDigest = try lstatValues(at: url) + guard + valuesAfterDigest.byteCount == + valuesBeforeDigest.byteCount, + valuesAfterDigest.deviceID == + valuesBeforeDigest.deviceID, + valuesAfterDigest.inode == + valuesBeforeDigest.inode + else { + throw CrashConsistentStoreReplacementError.familyMismatch + } + + return FamilyMember( + suffix: suffix, + byteCount: valuesBeforeDigest.byteCount, + sha256: sha256 + ) + } + + private func digest( + at url: URL, + expectedByteCount: UInt64 + ) throws -> String { + let handle = try FileHandle(forReadingFrom: url) + defer { + try? handle.close() + } + + var hasher = SHA256() + var remainingByteCount = expectedByteCount + while remainingByteCount > 0 { + let readByteCount = Int( + min( + remainingByteCount, + UInt64(Self.digestChunkByteCount) + ) + ) + let data = try handle.read( + upToCount: readByteCount + ) ?? Data() + guard !data.isEmpty else { + throw CrashConsistentStoreReplacementError.familyMismatch + } + + hasher.update(data: data) + remainingByteCount -= UInt64(data.count) + } + + let unexpectedByte = try handle.read(upToCount: 1) ?? Data() + guard unexpectedByte.isEmpty else { + throw CrashConsistentStoreReplacementError.familyMismatch + } + + return hasher.finalize().map { + String(format: "%02x", $0) + }.joined() + } + + private func synchronizeFamily(at familyStoreURL: URL) throws { + let family = try manifest( + for: familyStoreURL, + requiringMainStore: true + ) + + for member in family { + try synchronizeFile( + familyURL( + storeURL: familyStoreURL, + suffix: member.suffix + ) + ) + } + + try synchronizeDirectory( + familyStoreURL.deletingLastPathComponent() + ) + } + + private func synchronizeFile(_ url: URL) throws { + do { + try requireRegularFile(url) + let handle = try FileHandle(forUpdating: url) + defer { + try? handle.close() + } + try handle.synchronize() + } catch { + throw CrashConsistentStoreReplacementError + .fileSynchronizationFailed(url, error) + } + } + + private func synchronizeDirectory(_ url: URL) throws { + do { + guard pathKind(at: url) == .directory else { + throw CrashConsistentStoreReplacementError + .unsafeDirectory(url) + } + + let descriptor = Darwin.open(url.path, O_RDONLY) + guard descriptor >= 0 else { + throw POSIXError( + POSIXErrorCode(rawValue: errno) ?? .EIO + ) + } + defer { + _ = Darwin.close(descriptor) + } + + guard Darwin.fsync(descriptor) == 0 else { + throw POSIXError( + POSIXErrorCode(rawValue: errno) ?? .EIO + ) + } + } catch { + throw CrashConsistentStoreReplacementError + .directorySynchronizationFailed(url, error) + } + } + + private func requireSafeDatabaseDirectory() throws { + guard pathKind(at: databaseDirectoryURL) == .directory else { + throw CrashConsistentStoreReplacementError + .unsafeDirectory(databaseDirectoryURL) + } + + switch pathKind(at: recoveryRootURL) { + case .missing, .directory: + return + case .regularFile, .symbolicLink, .other: + throw CrashConsistentStoreReplacementError + .unsafeDirectory(recoveryRootURL) + } + } + + private func requireRegularFile(_ url: URL) throws { + guard pathKind(at: url) == .regularFile else { + throw CrashConsistentStoreReplacementError.unsafeFile(url) + } + } + + private func pathKind(at url: URL) -> PathKind { + var fileStatus = stat() + guard Darwin.lstat(url.path, &fileStatus) == 0 else { + return errno == ENOENT ? .missing : .other + } + + switch fileStatus.st_mode & S_IFMT { + case S_IFREG: + return .regularFile + case S_IFDIR: + return .directory + case S_IFLNK: + return .symbolicLink + default: + return .other + } + } + + private func lstatValues( + at url: URL + ) throws -> ( + byteCount: UInt64, + mode: mode_t, + deviceID: UInt64, + inode: UInt64 + ) { + var fileStatus = stat() + guard Darwin.lstat(url.path, &fileStatus) == 0 else { + throw POSIXError( + POSIXErrorCode(rawValue: errno) ?? .EIO + ) + } + + guard fileStatus.st_size >= 0 else { + throw CrashConsistentStoreReplacementError.unsafeFile(url) + } + + return ( + byteCount: UInt64(fileStatus.st_size), + mode: fileStatus.st_mode, + deviceID: UInt64( + truncatingIfNeeded: fileStatus.st_dev + ), + inode: UInt64( + truncatingIfNeeded: fileStatus.st_ino + ) + ) + } + + private func familyURL( + storeURL: URL, + suffix: String + ) -> URL { + URL(fileURLWithPath: storeURL.path + suffix) + } +} extension NSPersistentStoreCoordinator { static func destroyStore(at storeURL: URL) throws { diff --git a/fearless/Common/Storage/PersistentValueSettings.swift b/fearless/Common/Storage/PersistentValueSettings.swift index 658c57106d..449c58e42a 100644 --- a/fearless/Common/Storage/PersistentValueSettings.swift +++ b/fearless/Common/Storage/PersistentValueSettings.swift @@ -5,6 +5,11 @@ enum PersistentValueSettingsError: Error { } class PersistentValueSettings { + private struct PendingSave { + let value: T + let completion: (Result) -> Void + } + let storageFacade: StorageFacadeProtocol init(storageFacade: StorageFacadeProtocol) { @@ -14,6 +19,10 @@ class PersistentValueSettings { private let mutex = NSLock() var internalValue: T? + private var committedValue: T? + private var pendingSaves: [PendingSave] = [] + private var isPerformingSave = false + private var actionsWaitingForSaves: [() -> Void] = [] var value: T? { mutex.lock() @@ -26,6 +35,76 @@ class PersistentValueSettings { var hasValue: Bool { value != nil } + @discardableResult + func applySetupResult( + _ result: Result, + when shouldApply: () -> Bool, + commit: () -> Void = {} + ) -> Bool { + applySetupResultAtomically(result) { applyValue in + guard shouldApply() else { + return false + } + + applyValue() + commit() + + return true + } + } + + /// Executes `commitIfCurrent` with `mutex` held. The supplied `applyValue` + /// closure updates both the visible and committed values and must be called + /// exactly once when `commitIfCurrent` returns `true`. + @discardableResult + func applySetupResultAtomically( + _ result: Result, + commitIfCurrent: (_ applyValue: () -> Void) -> Bool + ) -> Bool { + mutex.lock() + + guard + pendingSaves.isEmpty, + !isPerformingSave + else { + mutex.unlock() + return false + } + + let applyValue = { + if case let .success(newValue) = result { + self.internalValue = newValue + self.committedValue = newValue + } + } + let didCommit = commitIfCurrent(applyValue) + + mutex.unlock() + + return didCommit + } + + func applySetupResult(_ result: Result) { + applySetupResult(result, when: { true }) + } + + /// Called with `mutex` held immediately before the save is registered. + /// Implementations must not re-enter `PersistentValueSettings`. + func prepareForSaveInvocation() {} + + func performAfterPendingSaves(_ action: @escaping () -> Void) { + mutex.lock() + let shouldPerformImmediately = pendingSaves.isEmpty && !isPerformingSave + if !shouldPerformImmediately { + actionsWaitingForSaves.append(action) + } + mutex.unlock() + + if shouldPerformImmediately { + action() + } + } + func performSetup(completionClosure _: @escaping (Result) -> Void) { fatalError("Function must be implemented in subclass") } @@ -38,16 +117,14 @@ class PersistentValueSettings { runningCompletionIn queue: DispatchQueue?, completionClosure: ((Result) -> Void)? ) { - performSetup { result in - self.mutex.lock() - if case let .success(newValue) = result { - self.internalValue = newValue - } - self.mutex.unlock() - - if let closure = completionClosure { - dispatchInQueueWhenPossible(queue) { - closure(result) + performAfterPendingSaves { + self.performSetup { result in + self.applySetupResult(result) + + if let closure = completionClosure { + dispatchInQueueWhenPossible(queue) { + closure(result) + } } } } @@ -63,25 +140,72 @@ class PersistentValueSettings { completionClosure: ((Result) -> Void)? ) { mutex.lock() - let previousValue = internalValue + prepareForSaveInvocation() internalValue = value + pendingSaves.append( + PendingSave(value: value) { result in + if let completionClosure { + dispatchInQueueWhenPossible(queue) { + completionClosure(result) + } + } + } + ) + let shouldStartSave = !isPerformingSave + if shouldStartSave { + isPerformingSave = true + } mutex.unlock() - performSave(value: value) { result in - self.mutex.lock() - switch result { - case let .success(newValue): - self.internalValue = newValue - case .failure: - self.internalValue = previousValue - } - self.mutex.unlock() + if shouldStartSave { + performNextSave() + } + } - if let closure = completionClosure { - dispatchInQueueWhenPossible(queue) { - closure(result) - } - } + private func performNextSave() { + mutex.lock() + guard let pendingSave = pendingSaves.first else { + isPerformingSave = false + mutex.unlock() + return + } + mutex.unlock() + + performSave(value: pendingSave.value) { result in + self.completeCurrentSave(result) + } + } + + private func completeCurrentSave(_ result: Result) { + mutex.lock() + guard !pendingSaves.isEmpty else { + isPerformingSave = false + mutex.unlock() + return + } + + let completedSave = pendingSaves.removeFirst() + if case let .success(newValue) = result { + committedValue = newValue + } + + internalValue = pendingSaves.last?.value ?? committedValue + let hasNextSave = !pendingSaves.isEmpty + let waitingActions = hasNextSave ? [] : actionsWaitingForSaves + if !hasNextSave { + actionsWaitingForSaves.removeAll() + } + if !hasNextSave { + isPerformingSave = false + } + mutex.unlock() + + completedSave.completion(result) + + if hasNextSave { + performNextSave() + } else { + waitingActions.forEach { $0() } } } diff --git a/fearless/Common/Storage/SelectedWalletSettings.swift b/fearless/Common/Storage/SelectedWalletSettings.swift index d8212db3ba..f97c5a580f 100644 --- a/fearless/Common/Storage/SelectedWalletSettings.swift +++ b/fearless/Common/Storage/SelectedWalletSettings.swift @@ -1,82 +1,311 @@ import Foundation import RobinHood +enum SelectedWalletStoreState: Equatable { + case unresolved + case empty + case ready + case unsupportedOnly + case unavailable +} + +enum SelectedWalletSettingsError: LocalizedError { + case duplicateWalletIdentifier + case unsupportedWalletIdentifierConflict + + var errorDescription: String? { + switch self { + case .duplicateWalletIdentifier: + return "The wallet store contains duplicate identifiers" + case .unsupportedWalletIdentifierConflict: + return "A supported wallet conflicts with an unsupported stored wallet" + } + } +} + final class SelectedWalletSettings: PersistentValueSettings { + private typealias SetupOutcome = ( + selectedWallet: MetaAccountModel?, + repairedAccounts: [MetaAccountSelectionModel], + storeState: SelectedWalletStoreState + ) + static let shared = SelectedWalletSettings( storageFacade: UserDataStorageFacade.shared, operationQueue: OperationManagerFacade.persistentQueue ) let operationQueue: OperationQueue - let mapper = ManagedMetaAccountMapper() - lazy var repository = storageFacade.createRepository(mapper: AnyCoreDataMapper(mapper)) + private let storeStateLock = NSLock() + private var internalStoreState = SelectedWalletStoreState.unresolved + private let setupGenerationLock = NSLock() + private var setupGeneration = UUID() + private let savePreparationHook: (() -> Void)? + private let setupPreparationHook: (() -> Void)? + private let setupCommitHook: (() -> Void)? + + var storeState: SelectedWalletStoreState { + storeStateLock.lock() + defer { storeStateLock.unlock() } + + return internalStoreState + } - init(storageFacade: StorageFacadeProtocol, operationQueue: OperationQueue) { + init( + storageFacade: StorageFacadeProtocol, + operationQueue: OperationQueue, + savePreparationHook: (() -> Void)? = nil, + setupPreparationHook: (() -> Void)? = nil, + setupCommitHook: (() -> Void)? = nil + ) { self.operationQueue = operationQueue + self.savePreparationHook = savePreparationHook + self.setupPreparationHook = setupPreparationHook + self.setupCommitHook = setupCommitHook super.init(storageFacade: storageFacade) } + override func prepareForSaveInvocation() { + _ = replaceSetupGeneration() + savePreparationHook?() + } + + override func setup( + runningCompletionIn queue: DispatchQueue?, + completionClosure: ((Result) -> Void)? + ) { + let generation = replaceSetupGeneration() + setupPreparationHook?() + + performAfterPendingSaves { + self.performSetupOutcome(generation: generation) { result in + let publicResult = result.map(\.selectedWallet) + self.commitSetupOutcomeIfCurrent(result, generation: generation) + + if let completionClosure { + dispatchInQueueWhenPossible(queue) { + completionClosure(publicResult) + } + } + } + } + } + override func performSetup(completionClosure: @escaping (Result) -> Void) { - let mapper = MetaAccountMapper() + performSetupOutcome(generation: nil) { result in + completionClosure(result.map(\.selectedWallet)) + } + } + + private func performSetupOutcome( + generation: UUID?, + completionClosure: @escaping (Result) -> Void + ) { + let selectionMapper = MetaAccountSelectionMapper() let repository = storageFacade.createRepository( - filter: NSPredicate.selectedMetaAccount(), - sortDescriptors: [], - mapper: AnyCoreDataMapper(mapper) + mapper: AnyCoreDataMapper(selectionMapper) ) - let options = RepositoryFetchOptions(includesProperties: true, includesSubentities: true) - let operation = repository.fetchAllOperation(with: options) + let fetchOperation = repository.fetchAllOperation(with: options) + let selectionOperation: BaseOperation = ClosureOperation { + let accounts = try fetchOperation.extractNoCancellableResultData() + + guard Set(accounts.map(\.identifier)).count == accounts.count else { + throw SelectedWalletSettingsError.duplicateWalletIdentifier + } - operation.completionBlock = { + guard !accounts.isEmpty else { + return (nil, [], .empty) + } + + let supportedAccounts = accounts.filter { $0.wallet != nil } + guard !supportedAccounts.isEmpty else { + let containsStructurallyValidUnsupportedWallet = accounts.contains { + $0.recordState == .unsupported + } + let state: SelectedWalletStoreState = + containsStructurallyValidUnsupportedWallet + ? .unsupportedOnly + : .unavailable + + return (nil, [], state) + } + + let selectedSupportedAccounts = supportedAccounts.filter(\.isSelected) + let selectionPool = selectedSupportedAccounts.isEmpty + ? supportedAccounts + : selectedSupportedAccounts + guard + let selectedAccount = selectionPool.min(by: Self.precedesForSelection), + let selectedWallet = selectedAccount.wallet + else { + throw MetaAccountMapperError.invalidWalletRecord + } + + let repairedAccounts: [MetaAccountSelectionModel] = accounts.compactMap { account in + guard account.recordState.allowsStoredRecordUpdates else { + return nil + } + + let shouldBeSelected = account.identifier == selectedAccount.identifier + return account.isSelected == shouldBeSelected + ? nil + : account.replacingSelection(shouldBeSelected) + } + + return (selectedWallet, repairedAccounts, .ready) + } + let repairOperation: BaseOperation = ClosureOperation { + let repairedAccounts = try selectionOperation + .extractNoCancellableResultData() + .repairedAccounts + + guard !repairedAccounts.isEmpty else { + return + } + + let repair = { + let saveOperation = repository.saveOperation( + { repairedAccounts }, + { [] } + ) + let queue = OperationQueue() + queue.addOperations([saveOperation], waitUntilFinished: true) + _ = try saveOperation.extractNoCancellableResultData() + } + + if let generation { + try self.performRepairIfCurrent( + generation: generation, + repair: repair + ) + } else { + try repair() + } + } + + selectionOperation.addDependency(fetchOperation) + repairOperation.addDependency(selectionOperation) + + repairOperation.completionBlock = { do { - let result = try operation.extractNoCancellableResultData().first + _ = try repairOperation.extractNoCancellableResultData() + let result = try selectionOperation.extractNoCancellableResultData() completionClosure(.success(result)) } catch { completionClosure(.failure(error)) } } - operationQueue.addOperation(operation) + operationQueue.addOperations( + [fetchOperation, selectionOperation, repairOperation], + waitUntilFinished: false + ) + } + + private func replaceSetupGeneration() -> UUID { + let generation = UUID() + + setupGenerationLock.lock() + setupGeneration = generation + setupGenerationLock.unlock() + + return generation + } + + private func commitSetupOutcomeIfCurrent( + _ result: Result, + generation: UUID + ) { + let publicResult = result.map(\.selectedWallet) + let storeState: SelectedWalletStoreState + switch result { + case let .success(outcome): + storeState = outcome.storeState + case .failure: + storeState = .unavailable + } + + applySetupResultAtomically(publicResult) { applyValue in + self.setupGenerationLock.lock() + defer { self.setupGenerationLock.unlock() } + + guard self.setupGeneration == generation else { + return false + } + + applyValue() + self.setupCommitHook?() + self.updateStoreState(storeState) + + return true + } + } + + private func performRepairIfCurrent( + generation: UUID, + repair: () throws -> Void + ) throws { + setupGenerationLock.lock() + defer { setupGenerationLock.unlock() } + + guard setupGeneration == generation else { + return + } + + try repair() } override func performSave( value: MetaAccountModel, completionClosure: @escaping (Result) -> Void ) { + let selectionMapper = MetaAccountSelectionMapper() + let repository = storageFacade.createRepository( + mapper: AnyCoreDataMapper(selectionMapper) + ) let options = RepositoryFetchOptions(includesProperties: true, includesSubentities: true) let allAccountsOperation = repository.fetchAllOperation(with: options) let saveOperation = repository.saveOperation({ let existingAccounts = try allAccountsOperation.extractNoCancellableResultData() - var accountsById = existingAccounts.reduce(into: [String: ManagedMetaAccountModel]()) { result, account in - result[account.identifier] = account + guard Set(existingAccounts.map(\.identifier)).count == existingAccounts.count else { + throw SelectedWalletSettingsError.duplicateWalletIdentifier } - if let currentAccount = existingAccounts.first(where: \.isSelected), - currentAccount.identifier != value.identifier { - accountsById[currentAccount.identifier] = ManagedMetaAccountModel( - info: currentAccount.info, - isSelected: false, - order: currentAccount.order - ) + let existingTarget = existingAccounts.first { + $0.identifier == value.identifier + } + + if let existingTarget, existingTarget.wallet == nil { + throw SelectedWalletSettingsError.unsupportedWalletIdentifierConflict + } + + var accountsToSave: [MetaAccountSelectionModel] = existingAccounts.compactMap { account in + guard + account.recordState.allowsStoredRecordUpdates, + account.identifier != value.identifier, + account.isSelected + else { + return nil + } + + return account.replacingSelection(false) } - if let existingAccount = accountsById[value.identifier] { - accountsById[value.identifier] = ManagedMetaAccountModel( - info: value, + accountsToSave.append( + MetaAccountSelectionModel( + identifier: value.identifier, + wallet: value, isSelected: true, - order: existingAccount.order - ) - } else { - accountsById[value.identifier] = ManagedMetaAccountModel( - info: value, - isSelected: true + order: existingTarget?.order ?? ManagedMetaAccountModel.noOrder, + updatesWalletPayload: true ) - } + ) - return Array(accountsById.values) + return accountsToSave }, { [] }) saveOperation.addDependency(allAccountsOperation) @@ -84,7 +313,7 @@ final class SelectedWalletSettings: PersistentValueSettings { saveOperation.completionBlock = { [weak self] in do { _ = try saveOperation.extractNoCancellableResultData() - self?.internalValue = value + self?.updateStoreState(.ready) completionClosure(.success(value)) } catch { completionClosure(.failure(error)) @@ -93,4 +322,24 @@ final class SelectedWalletSettings: PersistentValueSettings { operationQueue.addOperations([allAccountsOperation, saveOperation], waitUntilFinished: false) } + + private static func precedesForSelection( + _ lhs: MetaAccountSelectionModel, + _ rhs: MetaAccountSelectionModel + ) -> Bool { + let lhsOrder = lhs.order == ManagedMetaAccountModel.noOrder ? UInt32.max : lhs.order + let rhsOrder = rhs.order == ManagedMetaAccountModel.noOrder ? UInt32.max : rhs.order + + if lhsOrder != rhsOrder { + return lhsOrder < rhsOrder + } + + return lhs.identifier < rhs.identifier + } + + private func updateStoreState(_ state: SelectedWalletStoreState) { + storeStateLock.lock() + internalStoreState = state + storeStateLock.unlock() + } } diff --git a/fearless/Common/Storage/SubstrateDataStorageFacade.swift b/fearless/Common/Storage/SubstrateDataStorageFacade.swift index 508cc1a071..cdd83d1202 100644 --- a/fearless/Common/Storage/SubstrateDataStorageFacade.swift +++ b/fearless/Common/Storage/SubstrateDataStorageFacade.swift @@ -25,30 +25,53 @@ class SubstrateDataStorageFacade: StorageFacadeProtocol { let databaseService: CoreDataServiceProtocol - private init() { + private convenience init() { let modelName = "SubstrateDataModel" let modelURL = Bundle.main.url(forResource: modelName, withExtension: "momd") - let databaseName = "\(modelName).sqlite" let baseURL = FileManager.default.urls( for: .documentDirectory, in: .userDomainMask ).first?.appendingPathComponent("CoreData") + self.init( + modelURL: modelURL, + databaseDirectory: baseURL!, + databaseName: "\(modelName).sqlite" + ) + } + + init( + modelURL: URL?, + databaseDirectory: URL, + databaseName: String = SubstrateStorageParams.databaseName + ) { + let resolvedModelURL: URL + if let modelURL { + resolvedModelURL = modelURL + } else { + Logger.shared.error( + "Required Substrate Core Data model resource is unavailable" + ) + resolvedModelURL = Bundle.main.bundleURL.appendingPathComponent( + "__fearless_missing_substrate_model_\(UUID().uuidString).momd" + ) + } + let options = [ NSMigratePersistentStoresAutomaticallyOption: true, NSInferMappingModelAutomaticallyOption: true ] let persistentSettings = CoreDataPersistentSettings( - databaseDirectory: baseURL!, + databaseDirectory: databaseDirectory, databaseName: databaseName, incompatibleModelStrategy: .ignore, options: options ) let configuration = CoreDataServiceConfiguration( - modelURL: modelURL!, + modelURL: resolvedModelURL, storageType: .persistent(settings: persistentSettings) ) diff --git a/fearless/Common/Storage/UserDataStorageFacade.swift b/fearless/Common/Storage/UserDataStorageFacade.swift index 18d1db64b3..1003cc6759 100644 --- a/fearless/Common/Storage/UserDataStorageFacade.swift +++ b/fearless/Common/Storage/UserDataStorageFacade.swift @@ -3,7 +3,7 @@ import RobinHood import CoreData enum UserStorageParams { - static let modelVersion: UserStorageVersion = .version12 + static let modelVersion: UserStorageVersion = .version14 static let modelDirectory: String = "Modules_SSFAccountManagmentStorage.bundle//UserDataModel.momd" static let databaseName = "UserDataModel.sqlite" @@ -26,32 +26,44 @@ class UserDataStorageFacade: StorageFacadeProtocol { let databaseService: CoreDataServiceProtocol - private init() { - let modelName = UserStorageParams.modelVersion.rawValue + private convenience init() { let bundle = Bundle.main - let omoURL = bundle.url( - forResource: modelName, - withExtension: "omo", - subdirectory: UserStorageParams.modelDirectory + let modelURL = UserStorageParams.modelVersion.modelURL( + in: bundle, + legacyModelDirectory: UserStorageParams.modelDirectory ) - let momURL = bundle.url( - forResource: modelName, - withExtension: "mom", - subdirectory: UserStorageParams.modelDirectory + self.init( + modelURL: modelURL, + databaseDirectory: UserStorageParams.storageDirectoryURL ) + } - let modelURL = omoURL ?? momURL + init( + modelURL: URL?, + databaseDirectory: URL + ) { + let resolvedModelURL: URL + if let modelURL { + resolvedModelURL = modelURL + } else { + Logger.shared.error( + "Required User Core Data model resource is unavailable" + ) + resolvedModelURL = Bundle.main.bundleURL.appendingPathComponent( + "__fearless_missing_user_model_\(UUID().uuidString).mom" + ) + } let persistentSettings = CoreDataPersistentSettings( - databaseDirectory: UserStorageParams.storageDirectoryURL, + databaseDirectory: databaseDirectory, databaseName: UserStorageParams.databaseName, incompatibleModelStrategy: .ignore ) let configuration = CoreDataServiceConfiguration( - modelURL: modelURL!, + modelURL: resolvedModelURL, storageType: .persistent(settings: persistentSettings) ) diff --git a/fearless/Info.plist b/fearless/Info.plist index 36375a7e4d..b47d1adaf1 100644 --- a/fearless/Info.plist +++ b/fearless/Info.plist @@ -54,6 +54,12 @@ CFBundleVersion 1 + FearlessBuildConfiguration + $(CONFIGURATION) + FearlessEnableTestability + $(ENABLE_TESTABILITY) + FearlessSwiftOptimizationLevel + $(SWIFT_OPTIMIZATION_LEVEL) GIDClientID ITSAppUsesNonExemptEncryption diff --git a/fearless/Modules/Onboarding/OnboardingPresenter.swift b/fearless/Modules/Onboarding/OnboardingPresenter.swift index 8f9c085de0..c1dae508ef 100644 --- a/fearless/Modules/Onboarding/OnboardingPresenter.swift +++ b/fearless/Modules/Onboarding/OnboardingPresenter.swift @@ -51,7 +51,7 @@ final class OnboardingPresenter { router.showPincodeSetup() case .login: router.showLogin() - case .onboarding, .broken: + case .onboarding, .broken, .unsupportedWallet: break } } diff --git a/fearless/Modules/Root/RootInteractor.swift b/fearless/Modules/Root/RootInteractor.swift index 1a9be4b79b..828a8ad34d 100644 --- a/fearless/Modules/Root/RootInteractor.swift +++ b/fearless/Modules/Root/RootInteractor.swift @@ -1,39 +1,455 @@ import Foundation +import CoreData import SoraKeystore import IrohaCrypto import RobinHood import SoraFoundation +enum RootStoragePreflightError: LocalizedError { + case contextUnavailable + case persistentStoreCoordinatorUnavailable + case entityNameUnavailable + case managedObjectClassNameUnavailable(entityName: String) + case managedObjectClassUnavailable(entityName: String, className: String) + case invocationCapacityExhausted + case unexpectedManagedObjectClassIdentity( + entityName: String, + expectedClassName: String, + actualClassName: String + ) + + var errorDescription: String? { + switch self { + case .contextUnavailable: + return "Substrate storage opened without a managed object context" + case .persistentStoreCoordinatorUnavailable: + return "Substrate storage opened without a persistent store coordinator" + case .entityNameUnavailable: + return "Substrate storage contains an unnamed entity" + case let .managedObjectClassNameUnavailable(entityName): + return "Substrate storage entity \(entityName) has no managed object class" + case let .managedObjectClassUnavailable(entityName, className): + return "Substrate storage entity \(entityName) cannot resolve class \(className)" + case .invocationCapacityExhausted: + return "Substrate storage preflight is still blocked; retry after the current checks finish" + case let .unexpectedManagedObjectClassIdentity( + entityName, + expectedClassName, + actualClassName + ): + return "Substrate storage entity \(entityName) resolves as \(actualClassName), expected \(expectedClassName)" + } + } +} + +enum RootSetupDeadlineError: LocalizedError { + case migrationTimedOut + case migrationStillRunning + case storagePreflightTimedOut + case selectedWalletSetupTimedOut + + var errorDescription: String? { + switch self { + case .migrationTimedOut: + return "Wallet storage migration timed out" + case .migrationStillRunning: + return "Wallet storage migration is still running" + case .storagePreflightTimedOut: + return "Substrate storage preflight timed out" + case .selectedWalletSetupTimedOut: + return "Selected wallet storage setup timed out" + } + } +} + +protocol RootStoragePreflighting: AnyObject { + func preflight( + completion: @escaping (Result) -> Void + ) +} + +typealias RootStoragePreflightProvider = () -> RootStoragePreflighting +typealias RootSetupDeadlineScheduler = ( + TimeInterval, + @escaping () -> Void +) -> Void + +private final class RootSetupCompletionGate { + private let lock = NSLock() + private var isCompleted = false + + func hasClaimedCompletion() -> Bool { + lock.lock() + defer { lock.unlock() } + + return isCompleted + } + + func claimCompletion() -> Bool { + lock.lock() + defer { lock.unlock() } + + guard !isCompleted else { + return false + } + + isCompleted = true + return true + } +} + +private final class RootStoragePreflightInvocation { + private let lock = NSLock() + private let finishAction: () -> Void + private var hasReturned = false + private var hasDeliveredCompletion = false + private var isFinished = false + + init(finishAction: @escaping () -> Void) { + self.finishAction = finishAction + } + + deinit { + finish(returned: false, deliveredCompletion: false, force: true) + } + + func markReturned() { + finish(returned: true, deliveredCompletion: false, force: false) + } + + func markCompletionDelivered() { + finish(returned: false, deliveredCompletion: true, force: false) + } + + func markSkipped() { + finish(returned: false, deliveredCompletion: false, force: true) + } + + private func finish( + returned: Bool, + deliveredCompletion: Bool, + force: Bool + ) { + lock.lock() + + hasReturned = hasReturned || returned + hasDeliveredCompletion = + hasDeliveredCompletion || deliveredCompletion + + let shouldFinish = + !isFinished && + (force || (hasReturned && hasDeliveredCompletion)) + + if shouldFinish { + isFinished = true + } + + lock.unlock() + + if shouldFinish { + finishAction() + } + } +} + +private final class RootStoragePreflightInvocationLimiter { + private let lock = NSLock() + private let maximumInvocationCount: Int + private var activeInvocationCount = 0 + + init(maximumInvocationCount: Int) { + self.maximumInvocationCount = maximumInvocationCount + } + + func acquire() -> RootStoragePreflightInvocation? { + lock.lock() + defer { lock.unlock() } + + guard activeInvocationCount < maximumInvocationCount else { + return nil + } + + activeInvocationCount += 1 + + return RootStoragePreflightInvocation { [weak self] in + self?.release() + } + } + + private func release() { + lock.lock() + activeInvocationCount = max(0, activeInvocationCount - 1) + lock.unlock() + } +} + +final class RootCoreDataStoragePreflight: RootStoragePreflighting { + private let databaseService: CoreDataServiceProtocol + + init(databaseService: CoreDataServiceProtocol) { + self.databaseService = databaseService + } + + func preflight( + completion: @escaping (Result) -> Void + ) { + databaseService.performAsync { [self] context, error in + completion( + Result { + try validate(context: context, serviceError: error) + } + ) + } + } + + private func validate( + context: NSManagedObjectContext?, + serviceError: Error? + ) throws { + if let serviceError { + throw serviceError + } + + guard let context else { + throw RootStoragePreflightError.contextUnavailable + } + + guard let persistentStoreCoordinator = context.persistentStoreCoordinator else { + throw RootStoragePreflightError.persistentStoreCoordinatorUnavailable + } + + let entities = persistentStoreCoordinator.managedObjectModel.entities.sorted { + ($0.name ?? "") < ($1.name ?? "") + } + + for entity in entities { + try validate(entity: entity, in: context) + } + } + + private func validate( + entity: NSEntityDescription, + in context: NSManagedObjectContext + ) throws { + guard let entityName = entity.name else { + throw RootStoragePreflightError.entityNameUnavailable + } + + guard let expectedClassName = entity.managedObjectClassName else { + throw RootStoragePreflightError.managedObjectClassNameUnavailable( + entityName: entityName + ) + } + + guard let expectedClass = ManagedObjectRuntimeClassRegistry.resolve( + className: expectedClassName + ) else { + throw RootStoragePreflightError.managedObjectClassUnavailable( + entityName: entityName, + className: expectedClassName + ) + } + + let resolvedClassName = NSStringFromClass(expectedClass) + guard resolvedClassName == expectedClassName else { + throw RootStoragePreflightError.unexpectedManagedObjectClassIdentity( + entityName: entityName, + expectedClassName: expectedClassName, + actualClassName: resolvedClassName + ) + } + + guard !entity.isAbstract else { + return + } + + // Fetch only an object ID. Instantiating managed objects or touching their + // attributes here can decode transformable archives and raise an Objective-C + // exception that Swift cannot catch. Startup content mapping remains owned by + // the repositories; this preflight is deliberately limited to store/query and + // runtime-class availability. + let request = NSFetchRequest(entityName: entityName) + request.resultType = .managedObjectIDResultType + request.fetchLimit = 1 + request.includesPendingChanges = false + request.includesSubentities = false + _ = try context.fetch(request) + } +} + +protocol RootSelectedWalletSettingsProtocol: AnyObject { + func setup( + runningCompletionIn queue: DispatchQueue?, + completionClosure: ((Result) -> Void)? + ) +} + +extension SelectedWalletSettings: RootSelectedWalletSettingsProtocol {} + final class RootInteractor { + private static let setupDeadlineQueue = DispatchQueue( + label: "jp.co.soramitsu.fearlesswallet.root-setup-deadline", + qos: .userInitiated + ) + + private enum MigrationBarrierState { + case notRequested + case inProgress + case completed + case failed(Error) + } + + private struct MigrationWaiter { + let generation: UUID + let completionGate: RootSetupCompletionGate + } + weak var presenter: RootInteractorOutputProtocol? - private let chainRegistry: ChainRegistryProtocol - private let settings: SelectedWalletSettings + private let chainRegistryProvider: () -> ChainRegistryProtocol + private lazy var chainRegistry = chainRegistryProvider() + private let storagePreflightProvider: RootStoragePreflightProvider + private let settingsProvider: () -> RootSelectedWalletSettingsProtocol + private lazy var settings = settingsProvider() private let applicationConfig: ApplicationConfigProtocol private let eventCenter: EventCenterProtocol private let migrators: [Migrating] private let logger: LoggerProtocol? private let onboardingService: OnboardingServiceProtocol private let onboardingConfigResolver: OnboardingConfigVersionResolver + private let migrationDeadline: TimeInterval + private let setupDeadline: TimeInterval + private let setupDeadlineScheduler: RootSetupDeadlineScheduler? + private let setupQueue = DispatchQueue( + label: "jp.co.soramitsu.fearlesswallet.root-setup", + qos: .userInitiated + ) + // Core Data migration APIs are synchronous and cannot be cancelled safely. + // Keep exactly one migration worker alive after a presentation deadline so + // a retry can wait for that same attempt instead of racing a second writer. + private let migrationInvocationQueue: OperationQueue = { + let queue = OperationQueue() + queue.name = "jp.co.soramitsu.fearlesswallet.root-migration" + queue.qualityOfService = .userInitiated + queue.maxConcurrentOperationCount = 1 + return queue + }() + + // A Core Data store open cannot be cancelled and may block before + // `performAsync` returns. Keep one recovery lane, but account for an + // invocation until both the call returns and its terminal callback arrives. + // If both lanes stall, retries fail immediately instead of accumulating + // queued work or opening an unbounded number of concurrent stores. These + // lanes only perform the read-only preflight; migration writers stay behind + // the single-worker migration barrier above. + private let storagePreflightInvocationQueue = DispatchQueue( + label: "jp.co.soramitsu.fearlesswallet.root-storage-preflight", + qos: .userInitiated, + attributes: .concurrent + ) + private let storagePreflightInvocationLimiter = + RootStoragePreflightInvocationLimiter(maximumInvocationCount: 2) + + private let setupGenerationLock = NSLock() + private var setupGeneration = UUID() + // Accessed only on setupQueue. A no-migration reload is an assertion that + // migrations already completed earlier in this process. Once either path + // seals the barrier, shared Core Data services may open and migrations must + // not run again. A required migration failure remains fail-closed until a + // required retry succeeds. + private var migrationBarrierState = MigrationBarrierState.notRequested + private var migrationWaiter: MigrationWaiter? init( - chainRegistry: ChainRegistryProtocol, - settings: SelectedWalletSettings, + chainRegistryProvider: @escaping () -> ChainRegistryProtocol, + storagePreflightProvider: @escaping RootStoragePreflightProvider, + settingsProvider: @escaping () -> RootSelectedWalletSettingsProtocol, applicationConfig: ApplicationConfigProtocol, eventCenter: EventCenterProtocol, migrators: [Migrating], logger: LoggerProtocol? = nil, onboardingService: OnboardingServiceProtocol, - onboardingConfigResolver: OnboardingConfigVersionResolver + onboardingConfigResolver: OnboardingConfigVersionResolver, + migrationDeadline: TimeInterval = 60, + setupDeadline: TimeInterval = 15, + setupDeadlineScheduler: RootSetupDeadlineScheduler? = nil ) { - self.chainRegistry = chainRegistry - self.settings = settings + self.chainRegistryProvider = chainRegistryProvider + self.storagePreflightProvider = storagePreflightProvider + self.settingsProvider = settingsProvider self.applicationConfig = applicationConfig self.eventCenter = eventCenter self.migrators = migrators self.logger = logger self.onboardingService = onboardingService self.onboardingConfigResolver = onboardingConfigResolver + self.migrationDeadline = max(0.001, migrationDeadline) + self.setupDeadline = max(0.001, setupDeadline) + self.setupDeadlineScheduler = setupDeadlineScheduler + } + + deinit { + migrationInvocationQueue.cancelAllOperations() + } + + convenience init( + chainRegistryProvider: @escaping () -> ChainRegistryProtocol, + storagePreflightProvider: @escaping RootStoragePreflightProvider, + settings: RootSelectedWalletSettingsProtocol, + applicationConfig: ApplicationConfigProtocol, + eventCenter: EventCenterProtocol, + migrators: [Migrating], + logger: LoggerProtocol? = nil, + onboardingService: OnboardingServiceProtocol, + onboardingConfigResolver: OnboardingConfigVersionResolver, + migrationDeadline: TimeInterval = 60, + setupDeadline: TimeInterval = 15, + setupDeadlineScheduler: RootSetupDeadlineScheduler? = nil + ) { + self.init( + chainRegistryProvider: chainRegistryProvider, + storagePreflightProvider: storagePreflightProvider, + settingsProvider: { settings }, + applicationConfig: applicationConfig, + eventCenter: eventCenter, + migrators: migrators, + logger: logger, + onboardingService: onboardingService, + onboardingConfigResolver: onboardingConfigResolver, + migrationDeadline: migrationDeadline, + setupDeadline: setupDeadline, + setupDeadlineScheduler: setupDeadlineScheduler + ) + } + + convenience init( + chainRegistry: ChainRegistryProtocol, + storagePreflightProvider: @escaping RootStoragePreflightProvider, + settings: RootSelectedWalletSettingsProtocol, + applicationConfig: ApplicationConfigProtocol, + eventCenter: EventCenterProtocol, + migrators: [Migrating], + logger: LoggerProtocol? = nil, + onboardingService: OnboardingServiceProtocol, + onboardingConfigResolver: OnboardingConfigVersionResolver, + migrationDeadline: TimeInterval = 60, + setupDeadline: TimeInterval = 15, + setupDeadlineScheduler: RootSetupDeadlineScheduler? = nil + ) { + self.init( + chainRegistryProvider: { chainRegistry }, + storagePreflightProvider: storagePreflightProvider, + settings: settings, + applicationConfig: applicationConfig, + eventCenter: eventCenter, + migrators: migrators, + logger: logger, + onboardingService: onboardingService, + onboardingConfigResolver: onboardingConfigResolver, + migrationDeadline: migrationDeadline, + setupDeadline: setupDeadline, + setupDeadlineScheduler: setupDeadlineScheduler + ) } private func setupURLHandlingService() { @@ -48,13 +464,307 @@ final class RootInteractor { URLHandlingService.shared.setup(children: [purchaseHandler, keystoreImportService]) } - private func runMigrators() { - migrators.forEach { migrator in - do { - try migrator.migrate() - } catch { - logger?.error(error.localizedDescription) + private func runMigrators() throws { + for migrator in migrators { + try migrator.migrate() + } + } + + private func enforceMigrationBarrier( + runMigrations: Bool, + generation: UUID + ) { + if runMigrations { + switch migrationBarrierState { + case .completed: + preflightStorage(generation: generation) + case .notRequested, .failed: + startMigration(generation: generation) + case .inProgress: + waitForRunningMigration(generation: generation) + } + } else { + switch migrationBarrierState { + case .notRequested: + migrationBarrierState = .completed + preflightStorage(generation: generation) + case .completed: + preflightStorage(generation: generation) + case .inProgress: + failSetup( + with: RootSetupDeadlineError.migrationStillRunning, + generation: generation + ) + case let .failed(error): + failSetup(with: error, generation: generation) + } + } + } + + private func startMigration(generation: UUID) { + migrationBarrierState = .inProgress + waitForRunningMigration(generation: generation) + + migrationInvocationQueue.addOperation { [weak self] in + guard let self else { + return + } + + let result = Result { + try self.runMigrators() + } + + setupQueue.async { [weak self] in + self?.finishMigration(with: result) + } + } + } + + private func waitForRunningMigration(generation: UUID) { + let completionGate = RootSetupCompletionGate() + migrationWaiter = MigrationWaiter( + generation: generation, + completionGate: completionGate + ) + scheduleSetupDeadline( + after: migrationDeadline, + generation: generation, + completionGate: completionGate, + error: .migrationTimedOut + ) + } + + private func finishMigration(with result: Result) { + let waiter = migrationWaiter + migrationWaiter = nil + + switch result { + case .success: + migrationBarrierState = .completed + case let .failure(error): + migrationBarrierState = .failed(error) + } + + guard + let waiter, + waiter.completionGate.claimCompletion(), + isCurrentSetupGeneration(waiter.generation) + else { + return + } + + switch result { + case .success: + preflightStorage(generation: waiter.generation) + case let .failure(error): + failSetup(with: error, generation: waiter.generation) + } + } + + private func replaceSetupGeneration() -> UUID { + let generation = UUID() + + setupGenerationLock.lock() + setupGeneration = generation + setupGenerationLock.unlock() + + return generation + } + + private func isCurrentSetupGeneration(_ generation: UUID) -> Bool { + setupGenerationLock.lock() + defer { setupGenerationLock.unlock() } + + return setupGeneration == generation + } + + private func completeSetup(generation: UUID) { + guard isCurrentSetupGeneration(generation) else { + return + } + + DispatchQueue.main.async { [weak self] in + guard self?.isCurrentSetupGeneration(generation) == true else { + return + } + + self?.presenter?.didCompleteSetup() + } + } + + private func failSetup(with error: Error, generation: UUID) { + logger?.error(error.localizedDescription) + + guard isCurrentSetupGeneration(generation) else { + return + } + + DispatchQueue.main.async { [weak self] in + guard self?.isCurrentSetupGeneration(generation) == true else { + return + } + + self?.presenter?.didFailSetup() + } + } + + private func scheduleSetupDeadline( + after delay: TimeInterval, + generation: UUID, + completionGate: RootSetupCompletionGate, + error: RootSetupDeadlineError + ) { + let deadlineAction = { [weak self] in + guard + completionGate.claimCompletion(), + let self, + isCurrentSetupGeneration(generation) + else { + return } + + failSetup(with: error, generation: generation) + } + + if let setupDeadlineScheduler { + setupDeadlineScheduler(delay, deadlineAction) + } else { + Self.setupDeadlineQueue.asyncAfter( + deadline: .now() + delay, + execute: deadlineAction + ) + } + } + + private func setupSelectedWallet( + generation: UUID, + chainRegistry: ChainRegistryProtocol + ) { + guard isCurrentSetupGeneration(generation) else { + return + } + + let completionGate = RootSetupCompletionGate() + scheduleSetupDeadline( + after: setupDeadline, + generation: generation, + completionGate: completionGate, + error: .selectedWalletSetupTimedOut + ) + settings.setup(runningCompletionIn: .global()) { [weak self] result in + guard completionGate.claimCompletion() else { + return + } + + guard + let self, + isCurrentSetupGeneration(generation) + else { + return + } + + setupQueue.async { [weak self] in + guard + let self, + isCurrentSetupGeneration(generation) + else { + return + } + + switch result { + case let .success(wallet): + if let wallet { + chainRegistry.performHotBoot() + logger?.debug("Selected account: \(wallet.metaId)") + } else { + chainRegistry.performColdBoot() + logger?.debug("No selected account") + } + + completeSetup(generation: generation) + case let .failure(error): + failSetup(with: error, generation: generation) + } + } + } + } + + private func preflightStorage(generation: UUID) { + guard isCurrentSetupGeneration(generation) else { + return + } + + guard + let invocation = storagePreflightInvocationLimiter.acquire() + else { + failSetup( + with: RootStoragePreflightError.invocationCapacityExhausted, + generation: generation + ) + return + } + + let completionGate = RootSetupCompletionGate() + scheduleSetupDeadline( + after: setupDeadline, + generation: generation, + completionGate: completionGate, + error: .storagePreflightTimedOut + ) + let preflightProvider = storagePreflightProvider + storagePreflightInvocationQueue.async { [weak self, invocation] in + guard + self?.isCurrentSetupGeneration(generation) == true, + !completionGate.hasClaimedCompletion() + else { + invocation.markSkipped() + return + } + + let preflight = preflightProvider() + + guard + self?.isCurrentSetupGeneration(generation) == true, + !completionGate.hasClaimedCompletion() + else { + invocation.markSkipped() + return + } + + preflight.preflight { [weak self] result in + invocation.markCompletionDelivered() + + guard completionGate.claimCompletion() else { + return + } + + guard let self else { + return + } + + setupQueue.async { [weak self] in + guard + let self, + isCurrentSetupGeneration(generation) + else { + return + } + + switch result { + case .success: + let registry = chainRegistry + + setupSelectedWallet( + generation: generation, + chainRegistry: registry + ) + case let .failure(error): + failSetup(with: error, generation: generation) + } + } + } + + invocation.markReturned() } } } @@ -63,23 +773,21 @@ extension RootInteractor: RootInteractorInputProtocol { func setup(runMigrations: Bool) { setupURLHandlingService() - settings.setup(runningCompletionIn: .global()) { result in - if runMigrations { - self.runMigrators() + let generation = replaceSetupGeneration() + + setupQueue.async { [weak self] in + guard let self else { + return } - switch result { - case let .success(wallet): - if let wallet = wallet { - self.chainRegistry.performHotBoot() - self.logger?.debug("Selected account: \(wallet.metaId)") - } else { - self.chainRegistry.performColdBoot() - self.logger?.debug("No selected account") - } - case let .failure(error): - self.logger?.error("Selected account setup failed: \(error)") + guard isCurrentSetupGeneration(generation) else { + return } + + enforceMigrationBarrier( + runMigrations: runMigrations, + generation: generation + ) } } diff --git a/fearless/Modules/Root/RootPresenter.swift b/fearless/Modules/Root/RootPresenter.swift index 4e9b276fd0..748cb0029c 100644 --- a/fearless/Modules/Root/RootPresenter.swift +++ b/fearless/Modules/Root/RootPresenter.swift @@ -1,20 +1,72 @@ import UIKit import SoraFoundation +import os.log + +protocol RootStartupReadinessReporting: AnyObject { + func reportReady() + func reportFailure() +} + +final class RootStartupReadinessReporter: RootStartupReadinessReporting { + static let shared = RootStartupReadinessReporter() + + private let log = OSLog( + subsystem: Bundle.main.bundleIdentifier ?? "jp.co.soramitsu.fearlesswallet", + category: "startup-readiness" + ) + + private init() {} + + func reportReady() { + os_log("FEARLESS_STARTUP_READY", log: log, type: .default) + } + + func reportFailure() { + os_log("FEARLESS_STARTUP_FAILED", log: log, type: .error) + } +} final class RootPresenter { + private enum SetupPurpose { + case launch + case reload + } + + private enum LoadError: LocalizedError { + case missingInteractor + case onboardingConfigTimedOut + + var errorDescription: String? { + switch self { + case .missingInteractor: + return "Root interactor is unavailable" + case .onboardingConfigTimedOut: + return "Onboarding configuration request timed out" + } + } + } + var view: ControllerBackedProtocol? var window: UIWindow! var wireframe: RootWireframeProtocol! var interactor: RootInteractorInputProtocol! private let startViewHelper: StartViewHelperProtocol + private let onboardingConfigTimeoutNanoseconds: UInt64 + private let startupReadinessReporter: RootStartupReadinessReporting private var loadTask: Task? + private var setupPurpose: SetupPurpose? init( localizationManager: LocalizationManagerProtocol, - startViewHelper: StartViewHelperProtocol + startViewHelper: StartViewHelperProtocol, + onboardingConfigTimeoutNanoseconds: UInt64 = 5_000_000_000, + startupReadinessReporter: RootStartupReadinessReporting = + RootStartupReadinessReporter.shared ) { self.startViewHelper = startViewHelper + self.onboardingConfigTimeoutNanoseconds = max(1, onboardingConfigTimeoutNanoseconds) + self.startupReadinessReporter = startupReadinessReporter self.localizationManager = localizationManager } @@ -27,52 +79,255 @@ final class RootPresenter { switch startView { case .pin: wireframe.showLocalAuthentication(on: window) + startupReadinessReporter.reportReady() case .pinSetup: wireframe.showPincodeSetup(on: window) + startupReadinessReporter.reportReady() case .login: wireframe.showMain(on: window) + startupReadinessReporter.reportReady() case .broken: wireframe.showBroken(on: window) + startupReadinessReporter.reportFailure() + showProtectedDataFailure() + case .unsupportedWallet: + wireframe.showBroken(on: window) + startupReadinessReporter.reportFailure() + showUnsupportedWalletFailure() case let .onboarding(config): wireframe.showOnboarding(on: window, with: config) + startupReadinessReporter.reportReady() } } -} -extension RootPresenter: RootPresenterProtocol { - func loadOnLaunch() { - wireframe.showSplash(splashView: view, on: window) + private func fetchOnboardingConfigWithTimeout() async throws -> OnboardingConfigWrapper? { + try Task.checkCancellation() - interactor.setup(runMigrations: true) + let raceState = RootAsyncRaceState() + let timeout = onboardingConfigTimeoutNanoseconds + + guard let interactor else { + throw LoadError.missingInteractor + } + + return try await withTaskCancellationHandler( + operation: { + try await withCheckedThrowingContinuation { continuation in + guard raceState.install(continuation), !Task.isCancelled else { + raceState.resolve(.failure(CancellationError())) + return + } + + let fetchTask = Task { + guard raceState.claimOperationStart() else { + return + } + + do { + raceState.resolve(.success(try await interactor.fetchOnboardingConfig())) + } catch { + raceState.resolve(.failure(error)) + } + } + + let timeoutTask = Task { + do { + try await Task.sleep(nanoseconds: timeout) + raceState.resolve(.failure(LoadError.onboardingConfigTimedOut)) + } catch { + // The other result won the race or startup was cancelled. + } + } + + raceState.registerTasks([fetchTask, timeoutTask]) + } + }, + onCancel: { + raceState.resolve(.failure(CancellationError())) + } + ) + } + + private func loadOnboardingConfig() { loadTask?.cancel() loadTask = Task { [weak self] in guard let self else { return } do { - let onboardingConfig = try await interactor.fetchOnboardingConfig() - DispatchQueue.main.async { [weak self] in + let onboardingConfig = try await fetchOnboardingConfigWithTimeout() + guard !Task.isCancelled else { return } + + await MainActor.run { [weak self] in + guard !Task.isCancelled else { return } + self?.decideModuleSynchroniously(with: onboardingConfig) } } catch { + guard !Task.isCancelled else { return } + Logger.shared.error(error.localizedDescription) - DispatchQueue.main.async { [weak self] in + await MainActor.run { [weak self] in + guard !Task.isCancelled else { return } + self?.decideModuleSynchroniously(with: nil) } } } } + private func showRetryableFailure(message: String) { + let title = R.string.localizable.commonErrorGeneralTitle( + preferredLanguages: localizationManager?.selectedLocale.rLanguages + ) + let retryTitle = R.string.localizable.commonRetry( + preferredLanguages: localizationManager?.selectedLocale.rLanguages + ) + + let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) + alert.addAction( + UIAlertAction(title: retryTitle, style: .default) { [weak self] _ in + self?.loadOnLaunch() + } + ) + + view?.controller.present(alert, animated: true) + } + + private func showSetupFailure() { + showRetryableFailure( + message: """ + Your wallet data is safe, but Fearless Wallet couldn't update it. \ + Please retry or install the latest build. + """ + ) + } + + private func showProtectedDataFailure() { + showRetryableFailure( + message: """ + Your wallet data is safe, but Fearless Wallet couldn't access its protected security data. \ + Unlock your device and retry. If this continues, restart your device. + """ + ) + } + + private func showUnsupportedWalletFailure() { + showRetryableFailure( + message: """ + Your wallet data is safe, but this build can't open any of the wallets stored on this device. \ + Please install the latest build and retry. + """ + ) + } +} + +extension RootPresenter: RootPresenterProtocol { + func loadOnLaunch() { + wireframe.showSplash(splashView: view, on: window) + + loadTask?.cancel() + setupPurpose = .launch + interactor.setup(runMigrations: true) + } + func reload() { loadTask?.cancel() + setupPurpose = .reload interactor.setup(runMigrations: false) + } +} - DispatchQueue.main.async { [weak self] in - self?.decideModuleSynchroniously(with: nil) +extension RootPresenter: RootInteractorOutputProtocol { + func didCompleteSetup() { + guard let setupPurpose else { + return + } + + self.setupPurpose = nil + + switch setupPurpose { + case .launch: + loadOnboardingConfig() + case .reload: + decideModuleSynchroniously(with: nil) } } -} -extension RootPresenter: RootInteractorOutputProtocol {} + func didFailSetup() { + setupPurpose = nil + loadTask?.cancel() + startupReadinessReporter.reportFailure() + showSetupFailure() + } +} extension RootPresenter: Localizable { func applyLocalization() {} } + +/// An unstructured first-result race is intentional. Structured task groups wait for cancelled +/// children before returning, so a transport that ignores cancellation could defeat the deadline. +private final class RootAsyncRaceState: @unchecked Sendable { + private let lock = NSLock() + private var continuation: CheckedContinuation? + private var resolution: Result? + private var tasks: [Task] = [] + private var operationStarted = false + + @discardableResult + func install(_ continuation: CheckedContinuation) -> Bool { + lock.lock() + + if let resolution { + lock.unlock() + continuation.resume(with: resolution) + return false + } else { + self.continuation = continuation + lock.unlock() + return true + } + } + + func claimOperationStart() -> Bool { + lock.lock() + defer { lock.unlock() } + + guard resolution == nil, !operationStarted else { + return false + } + + operationStarted = true + return true + } + + func registerTasks(_ tasks: [Task]) { + lock.lock() + + if resolution == nil { + self.tasks = tasks + lock.unlock() + } else { + lock.unlock() + tasks.forEach { $0.cancel() } + } + } + + func resolve(_ result: Result) { + lock.lock() + + guard resolution == nil else { + lock.unlock() + return + } + + resolution = result + let continuation = continuation + self.continuation = nil + let tasks = tasks + self.tasks.removeAll() + lock.unlock() + + tasks.forEach { $0.cancel() } + continuation?.resume(with: result) + } +} diff --git a/fearless/Modules/Root/RootPresenterFactory.swift b/fearless/Modules/Root/RootPresenterFactory.swift index c5f9b1cd0a..68aa2b5b15 100644 --- a/fearless/Modules/Root/RootPresenterFactory.swift +++ b/fearless/Modules/Root/RootPresenterFactory.swift @@ -7,8 +7,9 @@ import SSFNetwork final class RootPresenterFactory: RootPresenterFactoryProtocol { struct Dependencies { let settings: SettingsManager - let selectedWalletSettings: SelectedWalletSettings - let chainRegistry: ChainRegistryProtocol + let selectedWalletSettingsProvider: () -> SelectedWalletSettings + let chainRegistryProvider: () -> ChainRegistryProtocol + let storagePreflightProvider: RootStoragePreflightProvider let applicationConfig: ApplicationConfigProtocol let eventCenter: EventCenterProtocol let logger: LoggerProtocol? @@ -20,8 +21,17 @@ final class RootPresenterFactory: RootPresenterFactoryProtocol { static var `default`: Dependencies { Dependencies( settings: SettingsManager.shared, - selectedWalletSettings: SelectedWalletSettings.shared, - chainRegistry: ChainRegistryFacade.sharedRegistry, + selectedWalletSettingsProvider: { + SelectedWalletSettings.shared + }, + chainRegistryProvider: { + ChainRegistryFacade.sharedRegistry + }, + storagePreflightProvider: { + RootCoreDataStoragePreflight( + databaseService: SubstrateDataStorageFacade.shared.databaseService + ) + }, applicationConfig: ApplicationConfig.shared, eventCenter: EventCenter.shared, logger: Logger.shared, @@ -44,7 +54,8 @@ final class RootPresenterFactory: RootPresenterFactoryProtocol { let wireframe = RootWireframe() let startViewHelper = StartViewHelper( keystore: dependencies.keystore, - selectedWalletSettings: dependencies.selectedWalletSettings, + selectedWalletSettingsProvider: + dependencies.selectedWalletSettingsProvider, userDefaultsStorage: dependencies.settings ) @@ -73,8 +84,6 @@ final class RootPresenterFactory: RootPresenterFactoryProtocol { startViewHelper: startViewHelper ) - _ = AssetManagementMigratorAssembly.createDefaultMigrator() - let migrators: [Migrating] = [ languageMigrator, dbMigrator, @@ -82,8 +91,9 @@ final class RootPresenterFactory: RootPresenterFactoryProtocol { ] let interactor = RootInteractor( - chainRegistry: dependencies.chainRegistry, - settings: dependencies.selectedWalletSettings, + chainRegistryProvider: dependencies.chainRegistryProvider, + storagePreflightProvider: dependencies.storagePreflightProvider, + settingsProvider: dependencies.selectedWalletSettingsProvider, applicationConfig: dependencies.applicationConfig, eventCenter: dependencies.eventCenter, migrators: migrators, diff --git a/fearless/Modules/Root/RootProtocol.swift b/fearless/Modules/Root/RootProtocol.swift index 5e7922764e..3d9a3a385e 100644 --- a/fearless/Modules/Root/RootProtocol.swift +++ b/fearless/Modules/Root/RootProtocol.swift @@ -23,7 +23,10 @@ protocol RootInteractorInputProtocol: AnyObject { func fetchOnboardingConfig() async throws -> OnboardingConfigWrapper? } -protocol RootInteractorOutputProtocol: AnyObject {} +protocol RootInteractorOutputProtocol: AnyObject { + func didCompleteSetup() + func didFailSetup() +} protocol RootPresenterFactoryProtocol: AnyObject { static func createPresenter(with window: UIWindow) -> RootPresenterProtocol diff --git a/fearless/Modules/Root/StartViewHelper.swift b/fearless/Modules/Root/StartViewHelper.swift index 85c2e38cac..c8a5988449 100644 --- a/fearless/Modules/Root/StartViewHelper.swift +++ b/fearless/Modules/Root/StartViewHelper.swift @@ -6,6 +6,7 @@ enum StartView { case pinSetup case login case broken + case unsupportedWallet case onboarding(OnboardingConfigWrapper) } @@ -15,41 +16,71 @@ protocol StartViewHelperProtocol { final class StartViewHelper: StartViewHelperProtocol { private let keystore: KeystoreProtocol - private let selectedWalletSettings: SelectedWalletSettings + private let selectedWalletSettingsProvider: () -> SelectedWalletSettings + private lazy var selectedWalletSettings = selectedWalletSettingsProvider() private let userDefaultsStorage: SettingsManagerProtocol init( keystore: KeystoreProtocol, - selectedWalletSettings: SelectedWalletSettings, + selectedWalletSettingsProvider: @escaping () -> SelectedWalletSettings, userDefaultsStorage: SettingsManagerProtocol ) { self.keystore = keystore - self.selectedWalletSettings = selectedWalletSettings + self.selectedWalletSettingsProvider = selectedWalletSettingsProvider self.userDefaultsStorage = userDefaultsStorage } + convenience init( + keystore: KeystoreProtocol, + selectedWalletSettings: SelectedWalletSettings, + userDefaultsStorage: SettingsManagerProtocol + ) { + self.init( + keystore: keystore, + selectedWalletSettingsProvider: { selectedWalletSettings }, + userDefaultsStorage: userDefaultsStorage + ) + } + func startView(onboardingConfig: OnboardingConfigWrapper?) -> StartView { do { - if let config = onboardingConfig { - return StartView.onboarding(config) - } - - if !selectedWalletSettings.hasValue { + switch selectedWalletSettings.storeState { + case .ready: + guard selectedWalletSettings.hasValue else { + return .broken + } + case .empty: + if let config = onboardingConfig { + return .onboarding(config) + } try keystore.deleteKeyIfExists(for: KeystoreTag.pincode.rawValue) - - return StartView.login + return .login + case .unsupportedOnly: + let pincodeExists = try keystore.checkKey(for: KeystoreTag.pincode.rawValue) + if pincodeExists { + return .unsupportedWallet + } + if let config = onboardingConfig { + return .onboarding(config) + } + return .login + case .unresolved, .unavailable: + return .broken } + if let config = onboardingConfig { + return .onboarding(config) + } let pincodeExists = try keystore.checkKey(for: KeystoreTag.pincode.rawValue) if pincodeExists { - return StartView.pin + return .pin } else { - return StartView.pinSetup + return .pinSetup } } catch { - return StartView.broken + return .broken } } } diff --git a/fearless/fearless-Bridging-Header.h b/fearless/fearless-Bridging-Header.h new file mode 100644 index 0000000000..87479c2303 --- /dev/null +++ b/fearless/fearless-Bridging-Header.h @@ -0,0 +1 @@ +#import "Common/CoreData/FearlessObjectiveCExceptionCatcher.h" diff --git a/fearlessTests/Common/Migration/CrashConsistentStoreReplacerResourceLimitTests.swift b/fearlessTests/Common/Migration/CrashConsistentStoreReplacerResourceLimitTests.swift new file mode 100644 index 0000000000..95e7137cf4 --- /dev/null +++ b/fearlessTests/Common/Migration/CrashConsistentStoreReplacerResourceLimitTests.swift @@ -0,0 +1,552 @@ +import Foundation +import XCTest +@testable import fearless + +final class CrashConsistentStoreReplacerResourceLimitTests: + XCTestCase +{ + private var rootURL: URL! + private var storeURL: URL! + private var stagedStoreURL: URL! + + override func setUpWithError() throws { + try super.setUpWithError() + + rootURL = FileManager.default.temporaryDirectory + .appendingPathComponent( + "CrashConsistentStoreResourceLimits-\(UUID().uuidString)", + isDirectory: true + ) + try FileManager.default.createDirectory( + at: rootURL, + withIntermediateDirectories: false + ) + storeURL = rootURL.appendingPathComponent("Live.sqlite") + stagedStoreURL = rootURL.appendingPathComponent("Staged.sqlite") + } + + override func tearDownWithError() throws { + if let rootURL { + try? FileManager.default.removeItem(at: rootURL) + } + + try super.tearDownWithError() + } + + func testReplaceStore_whenMainFileExceedsLimit_rejectsBeforeCapacityOrMutation() throws { + let maximum = + CrashConsistentStoreReplacer + .defaultMaximumStoreFamilyByteCount + let oversizedByteCount = maximum + 1 + try Data().write(to: storeURL) + let handle = try FileHandle(forWritingTo: storeURL) + try handle.truncate(atOffset: oversizedByteCount) + try handle.close() + try Data([0x03]).write(to: stagedStoreURL) + + var capacityInspectionCount = 0 + var replacementCount = 0 + var boundaries: [CrashConsistentStoreReplacementBoundary] = [] + let replacer = makeReplacer( + maximumStoreFamilyByteCount: maximum, + availableCapacityProvider: { _ in + capacityInspectionCount += 1 + return UInt64.max + }, + storeReplacer: { _, _ in + replacementCount += 1 + }, + boundaryHook: { + boundaries.append($0) + } + ) + + XCTAssertThrowsError( + try replacer.replaceStore( + with: stagedStoreURL, + validateDestination: { _ in } + ) + ) { error in + guard + case let .storeFamilyTooLarge( + actual, + reportedMaximum + ) = + error as? CrashConsistentStoreReplacementError + else { + return XCTFail("Unexpected error: \(error)") + } + + XCTAssertEqual(actual, oversizedByteCount) + XCTAssertEqual(reportedMaximum, maximum) + } + + XCTAssertEqual(capacityInspectionCount, 0) + XCTAssertEqual(replacementCount, 0) + XCTAssertTrue(boundaries.isEmpty) + XCTAssertFalse(replacer.hasPendingTransaction) + let attributes = try FileManager.default + .attributesOfItem(atPath: storeURL.path) + XCTAssertEqual( + (attributes[.size] as? NSNumber)?.uint64Value, + oversizedByteCount + ) + } + + func testReplaceStore_whenAggregateFamilyExceedsLimit_rejectsBeforeMutation() throws { + try Data([0x01, 0x02]).write(to: storeURL) + try Data([0x03, 0x04]).write( + to: familyURL(storeURL, suffix: "-wal") + ) + try Data([0x05]).write(to: stagedStoreURL) + + var replacementCount = 0 + let replacer = makeReplacer( + maximumStoreFamilyByteCount: 3, + storeReplacer: { _, _ in + replacementCount += 1 + } + ) + + XCTAssertThrowsError( + try replacer.replaceStore( + with: stagedStoreURL, + validateDestination: { _ in } + ) + ) { error in + guard + case let .storeFamilyTooLarge( + actual, + reportedMaximum + ) = + error as? CrashConsistentStoreReplacementError + else { + return XCTFail("Unexpected error: \(error)") + } + + XCTAssertEqual(actual, 4) + XCTAssertEqual(reportedMaximum, 3) + } + + XCTAssertEqual(replacementCount, 0) + XCTAssertFalse(replacer.hasPendingTransaction) + } + + func testReplaceStore_whenCapacityIsOneByteShort_rejectsBeforeTransactionMutation() throws { + try Data([0x01, 0x02, 0x03, 0x04]).write(to: storeURL) + try Data([0x05, 0x06]).write(to: stagedStoreURL) + + var replacementCount = 0 + var boundaries: [CrashConsistentStoreReplacementBoundary] = [] + let replacer = makeReplacer( + maximumStoreFamilyByteCount: 16, + minimumFreeStorageReserveByteCount: 3, + availableCapacityProvider: { _ in 6 }, + storeReplacer: { _, _ in + replacementCount += 1 + }, + boundaryHook: { + boundaries.append($0) + } + ) + + XCTAssertThrowsError( + try replacer.replaceStore( + with: stagedStoreURL, + validateDestination: { _ in } + ) + ) { error in + guard + case let .insufficientStorageCapacity(required, available) = + error as? CrashConsistentStoreReplacementError + else { + return XCTFail("Unexpected error: \(error)") + } + + XCTAssertEqual(required, 7) + XCTAssertEqual(available, 6) + } + + XCTAssertEqual(replacementCount, 0) + XCTAssertTrue(boundaries.isEmpty) + XCTAssertFalse(replacer.hasPendingTransaction) + } + + func testReplaceStore_whenFamilyAndCapacityAreExactBoundaries_succeeds() throws { + let original = Data([0x01, 0x02, 0x03, 0x04]) + let replacement = Data([0x05, 0x06, 0x07, 0x08]) + try original.write(to: storeURL) + try replacement.write(to: stagedStoreURL) + + let replacer = makeReplacer( + maximumStoreFamilyByteCount: 4, + minimumFreeStorageReserveByteCount: 3, + availableCapacityProvider: { _ in 7 } + ) + + try replacer.replaceStore( + with: stagedStoreURL, + validateDestination: { _ in } + ) + + XCTAssertEqual(try Data(contentsOf: storeURL), replacement) + XCTAssertFalse(replacer.hasPendingTransaction) + } + + func testReplaceStore_whenCapacityRequirementOverflows_rejectsBeforeMutation() throws { + try Data([0x01]).write(to: storeURL) + try Data([0x02]).write(to: stagedStoreURL) + + var replacementCount = 0 + let replacer = makeReplacer( + maximumStoreFamilyByteCount: 1, + minimumFreeStorageReserveByteCount: UInt64.max, + availableCapacityProvider: { _ in UInt64.max }, + storeReplacer: { _, _ in + replacementCount += 1 + } + ) + + XCTAssertThrowsError( + try replacer.replaceStore( + with: stagedStoreURL, + validateDestination: { _ in } + ) + ) { error in + guard + case .storeFamilySizeOverflow = + error as? CrashConsistentStoreReplacementError + else { + return XCTFail("Unexpected error: \(error)") + } + } + + XCTAssertEqual(replacementCount, 0) + XCTAssertFalse(replacer.hasPendingTransaction) + } + + func testCommittedRecovery_whenValidationCopyLacksSpace_preservesTransactionForRetry() throws { + let original = Data([0x10, 0x11, 0x12]) + let replacement = Data([0x20, 0x21, 0x22]) + try original.write(to: storeURL) + try replacement.write(to: stagedStoreURL) + + let interrupted = makeReplacer( + maximumStoreFamilyByteCount: 3, + minimumFreeStorageReserveByteCount: 0, + availableCapacityProvider: { _ in UInt64.max }, + boundaryHook: { boundary in + if boundary == .committedMarkerPersisted { + throw CrashConsistentStoreReplacementInterruption + .simulatedProcessDeath + } + } + ) + + XCTAssertThrowsError( + try interrupted.replaceStore( + with: stagedStoreURL, + validateDestination: { _ in } + ) + ) { error in + guard + error is CrashConsistentStoreReplacementInterruption + else { + return XCTFail("Unexpected error: \(error)") + } + } + XCTAssertTrue(interrupted.hasPendingTransaction) + XCTAssertEqual(try Data(contentsOf: storeURL), replacement) + + let capacityBlocked = makeReplacer( + maximumStoreFamilyByteCount: 3, + minimumFreeStorageReserveByteCount: 0, + availableCapacityProvider: { _ in 2 } + ) + XCTAssertThrowsError( + try capacityBlocked.reconcile( + validateCommittedStore: { _ in } + ) + ) { error in + guard + case let .insufficientStorageCapacity(required, available) = + error as? CrashConsistentStoreReplacementError + else { + return XCTFail("Unexpected error: \(error)") + } + + XCTAssertEqual(required, 3) + XCTAssertEqual(available, 2) + } + XCTAssertTrue(capacityBlocked.hasPendingTransaction) + XCTAssertEqual(try Data(contentsOf: storeURL), replacement) + + let recovered = makeReplacer( + maximumStoreFamilyByteCount: 3, + minimumFreeStorageReserveByteCount: 0, + availableCapacityProvider: { _ in 3 } + ) + try recovered.reconcile(validateCommittedStore: { _ in }) + + XCTAssertFalse(recovered.hasPendingTransaction) + XCTAssertEqual(try Data(contentsOf: storeURL), replacement) + } + + func testPrivateCopy_whenSparseSourceExceedsLimit_rejectsBeforeCapacityOrCopy() throws { + let maximum = + CrashConsistentStoreReplacer + .defaultMaximumStoreFamilyByteCount + let oversizedByteCount = maximum + 1 + try Data().write(to: storeURL) + let handle = try FileHandle(forWritingTo: storeURL) + try handle.truncate(atOffset: oversizedByteCount) + try handle.close() + + var capacityInspectionCount = 0 + XCTAssertThrowsError( + try SQLiteStoreFamilyCopier.copy( + from: storeURL, + to: stagedStoreURL, + includingSharedMemory: false, + fileManager: .default, + limits: SQLiteStoreFamilyCopyLimits( + maximumFamilyByteCount: maximum, + minimumFreeStorageReserveByteCount: 0 + ), + availableCapacityProvider: { _ in + capacityInspectionCount += 1 + return UInt64.max + } + ) + ) { error in + guard + case let .storeFamilyTooLarge( + actual, + reportedMaximum + ) = + error as? CrashConsistentStoreReplacementError + else { + return XCTFail("Unexpected error: \(error)") + } + + XCTAssertEqual(actual, oversizedByteCount) + XCTAssertEqual(reportedMaximum, maximum) + } + + XCTAssertEqual(capacityInspectionCount, 0) + XCTAssertFalse( + FileManager.default.fileExists( + atPath: stagedStoreURL.path + ) + ) + } + + func testPrivateCopy_whenCapacityIsInsufficient_rejectsBeforeFirstMember() throws { + try Data([0x01, 0x02, 0x03]).write(to: storeURL) + + XCTAssertThrowsError( + try SQLiteStoreFamilyCopier.copy( + from: storeURL, + to: stagedStoreURL, + includingSharedMemory: false, + fileManager: .default, + limits: SQLiteStoreFamilyCopyLimits( + maximumFamilyByteCount: 3, + minimumFreeStorageReserveByteCount: 2 + ), + availableCapacityProvider: { _ in 4 } + ) + ) { error in + guard + case let .insufficientStorageCapacity(required, available) = + error as? CrashConsistentStoreReplacementError + else { + return XCTFail("Unexpected error: \(error)") + } + + XCTAssertEqual(required, 5) + XCTAssertEqual(available, 4) + } + + XCTAssertFalse( + FileManager.default.fileExists( + atPath: stagedStoreURL.path + ) + ) + } + + func testPrivateCopy_whenSourceMainIsSymlink_rejectsWithoutFollowingIt() throws { + let outsideURL = rootURL.appendingPathComponent("Outside.sqlite") + try Data([0x01]).write(to: outsideURL) + try FileManager.default.createSymbolicLink( + at: storeURL, + withDestinationURL: outsideURL + ) + + XCTAssertThrowsError( + try SQLiteStoreFamilyCopier.copy( + from: storeURL, + to: stagedStoreURL, + includingSharedMemory: false, + fileManager: .default, + limits: SQLiteStoreFamilyCopyLimits( + maximumFamilyByteCount: 1, + minimumFreeStorageReserveByteCount: 0 + ), + availableCapacityProvider: { _ in 1 } + ) + ) { error in + guard + case .unsafeFile = + error as? CrashConsistentStoreReplacementError + else { + return XCTFail("Unexpected error: \(error)") + } + } + + XCTAssertFalse( + FileManager.default.fileExists( + atPath: stagedStoreURL.path + ) + ) + } + + func testPrivateCopy_whenDestinationHasOrphanSidecar_rejectsBeforeMainCopy() throws { + try Data([0x01]).write(to: storeURL) + let orphanDestinationWAL = familyURL( + stagedStoreURL, + suffix: "-wal" + ) + try Data([0xA5]).write(to: orphanDestinationWAL) + + XCTAssertThrowsError( + try SQLiteStoreFamilyCopier.copy( + from: storeURL, + to: stagedStoreURL, + includingSharedMemory: false, + fileManager: .default, + limits: SQLiteStoreFamilyCopyLimits( + maximumFamilyByteCount: 1, + minimumFreeStorageReserveByteCount: 0 + ), + availableCapacityProvider: { _ in 1 } + ) + ) { error in + guard + case .unsafeFile = + error as? CrashConsistentStoreReplacementError + else { + return XCTFail("Unexpected error: \(error)") + } + } + + XCTAssertFalse( + FileManager.default.fileExists( + atPath: stagedStoreURL.path + ) + ) + XCTAssertEqual( + try Data(contentsOf: orphanDestinationWAL), + Data([0xA5]) + ) + } + + func testPrivateCopy_atExactAggregateAndCapacityBoundary_copiesDurableMembersButNotSharedMemory() throws { + try Data([0x01, 0x02]).write(to: storeURL) + try Data([0x03]).write( + to: familyURL(storeURL, suffix: "-wal") + ) + try Data([0x04]).write( + to: familyURL(storeURL, suffix: "-shm") + ) + try Data([0x05]).write( + to: familyURL(storeURL, suffix: "-journal") + ) + + try SQLiteStoreFamilyCopier.copy( + from: storeURL, + to: stagedStoreURL, + includingSharedMemory: false, + fileManager: .default, + limits: SQLiteStoreFamilyCopyLimits( + maximumFamilyByteCount: 4, + minimumFreeStorageReserveByteCount: 2 + ), + availableCapacityProvider: { _ in 6 } + ) + + XCTAssertEqual( + try Data(contentsOf: stagedStoreURL), + Data([0x01, 0x02]) + ) + XCTAssertEqual( + try Data( + contentsOf: familyURL( + stagedStoreURL, + suffix: "-wal" + ) + ), + Data([0x03]) + ) + XCTAssertFalse( + FileManager.default.fileExists( + atPath: familyURL( + stagedStoreURL, + suffix: "-shm" + ).path + ) + ) + XCTAssertEqual( + try Data( + contentsOf: familyURL( + stagedStoreURL, + suffix: "-journal" + ) + ), + Data([0x05]) + ) + } + + private func makeReplacer( + maximumStoreFamilyByteCount: UInt64, + minimumFreeStorageReserveByteCount: UInt64 = 0, + availableCapacityProvider: @escaping (URL) throws -> UInt64 = { + _ in UInt64.max + }, + storeReplacer: ((URL, URL) throws -> Void)? = nil, + boundaryHook: @escaping ( + CrashConsistentStoreReplacementBoundary + ) throws -> Void = { _ in } + ) -> CrashConsistentStoreReplacer { + CrashConsistentStoreReplacer( + storeURL: storeURL, + fileManager: .default, + storeReplacer: storeReplacer ?? replaceMainStore, + boundaryHook: boundaryHook, + maximumStoreFamilyByteCount: + maximumStoreFamilyByteCount, + minimumFreeStorageReserveByteCount: + minimumFreeStorageReserveByteCount, + availableCapacityProvider: + availableCapacityProvider + ) + } + + private func replaceMainStore( + targetURL: URL, + sourceURL: URL + ) throws { + try FileManager.default.removeItem(at: targetURL) + try FileManager.default.copyItem( + at: sourceURL, + to: targetURL + ) + } + + private func familyURL( + _ storeURL: URL, + suffix: String + ) -> URL { + URL(fileURLWithPath: storeURL.path + suffix) + } +} diff --git a/fearlessTests/Common/Migration/SingleToMultiassetUserMigrationTests.swift b/fearlessTests/Common/Migration/SingleToMultiassetUserMigrationTests.swift index 74d0e6ea11..380542a511 100644 --- a/fearlessTests/Common/Migration/SingleToMultiassetUserMigrationTests.swift +++ b/fearlessTests/Common/Migration/SingleToMultiassetUserMigrationTests.swift @@ -35,6 +35,16 @@ class SingleToMultiassetUserMigrationTests: XCTestCase { let order: Int32 } + enum DuplicateRowOrder { + case watchOnlyFirst + case signerFirst + } + + enum ConflictingRowOrder { + case firstAliasFirst + case secondAliasFirst + } + let databaseDirectory = FileManager.default.temporaryDirectory.appendingPathComponent("CoreData") let databaseName = UUID().uuidString + ".sqlite" let modelDirectory = UserStorageParams.modelDirectory @@ -56,30 +66,638 @@ class SingleToMultiassetUserMigrationTests: XCTestCase { } func testMigrationForCreatedAccountWithoutDerivPath() throws { - throw XCTSkip("Legacy migration test is unavailable in the current environment") + try performTestUserMigration( + hasEntropy: true, + hasSeed: true, + hasDerivationPath: false + ) } func testMigrationForCreatedAccountWithDerivPath() throws { - throw XCTSkip("Legacy migration test is unavailable in the current environment") + try performTestUserMigration( + hasEntropy: true, + hasSeed: true, + hasDerivationPath: true + ) } func testMigrationForImportedWithSeedAccountWithoutDerivPath() throws { - throw XCTSkip("Legacy migration test is unavailable in the current environment") + try performTestUserMigration( + hasEntropy: false, + hasSeed: true, + hasDerivationPath: false + ) } func testMigrationForImportedWithSeedAccountWithDerivPath() throws { - throw XCTSkip("Legacy migration test is unavailable in the current environment") + try performTestUserMigration( + hasEntropy: false, + hasSeed: true, + hasDerivationPath: true + ) } func testMigrationForImportedWithJSONAccountWithoutDerivPath() throws { - throw XCTSkip("Legacy migration test is unavailable in the current environment") + try performTestUserMigration( + hasEntropy: false, + hasSeed: false, + hasDerivationPath: false + ) } func testMigrationForImportedWithJSONAccountWithDerivPath() throws { - throw XCTSkip("Legacy migration test is unavailable in the current environment") + try performTestUserMigration( + hasEntropy: false, + hasSeed: false, + hasDerivationPath: true + ) + } + + func testMigrationFromV1ToCurrentForCreatedAccountWithDerivationPath() throws { + try performTestUserMigration( + hasEntropy: true, + hasSeed: true, + hasDerivationPath: true, + targetVersion: .current + ) + } + + func testMigrationFromV1ToCurrentForImportedJSONAccount() throws { + try performTestUserMigration( + hasEntropy: false, + hasSeed: false, + hasDerivationPath: false, + targetVersion: .current + ) + } + + func testDuplicateAccountMigration_whenWatchOnlyRowIsInsertedFirst_thenSigningSecretsArePreserved() throws { + try performDuplicateSignerMigration(rowOrder: .watchOnlyFirst) + } + + func testDuplicateAccountMigration_whenSigningRowIsInsertedFirst_thenSigningSecretsArePreserved() throws { + try performDuplicateSignerMigration(rowOrder: .signerFirst) + } + + func testDuplicateAccountMigration_whenFirstAliasHasConflictingSecret_thenFailsWithoutReplacingStore() throws { + try performConflictingDuplicateMigration(rowOrder: .firstAliasFirst) + } + + func testDuplicateAccountMigration_whenSecondAliasHasConflictingSecret_thenFailsWithoutReplacingStore() throws { + try performConflictingDuplicateMigration(rowOrder: .secondAliasFirst) + } + + func testKeystoreResourceLimits_atEveryExactBoundary_prepareSucceeds() throws { + let account = try generateOldAccount( + hasEntropy: false, + hasSeed: false, + hasDerivationPath: false + ) + let firstMetaId = "00000000-0000-4000-8000-000000000001" + let secondMetaId = "00000000-0000-4000-8000-000000000002" + let firstDestinationIdentifier = + KeystoreTagV2.substrateSecretKeyTagForMetaId(firstMetaId) + let secondDestinationIdentifier = + KeystoreTagV2.substrateSeedTagForMetaId(secondMetaId) + let legacyIdentifier = + KeystoreTag.secretKeyTagForAddress(account.address) + let firstKey = Data([0x01, 0x02, 0x03]) + let secondKey = Data([0x04, 0x05]) + let aggregateIdentifierBytes = [ + firstDestinationIdentifier, + secondDestinationIdentifier, + legacyIdentifier + ].reduce(0) { $0 + $1.utf8.count } + let stagedKeyBytes = firstKey.count + secondKey.count + let calibrationLimits = makeResourceLimits( + maximumLegacyAccountRows: 2, + maximumStagedKeyCount: 2, + maximumIdentifierUTF8Bytes: aggregateIdentifierBytes, + maximumStagedKeyBytes: stagedKeyBytes, + maximumDeletionCount: 1, + maximumCleanupJournalBytes: Int.max + ) + let calibrationKeystore = InMemoryKeychain() + let calibrationMigrator = KeystoreMigrator( + sourceVersion: .version1, + destinationVersion: .version2, + keystore: calibrationKeystore, + resourceLimits: calibrationLimits + ) + + try stageExactBoundaryFixture( + in: calibrationMigrator, + firstIdentifier: firstDestinationIdentifier, + firstKey: firstKey, + secondIdentifier: secondDestinationIdentifier, + secondKey: secondKey, + legacyIdentifier: legacyIdentifier + ) + try calibrationMigrator.prepare() + let encodedJournal = try calibrationKeystore.fetchKey( + for: KeystoreMigrator.pendingCleanupIdentifier + ) + + let exactLimits = makeResourceLimits( + maximumLegacyAccountRows: 2, + maximumStagedKeyCount: 2, + maximumIdentifierUTF8Bytes: aggregateIdentifierBytes, + maximumStagedKeyBytes: stagedKeyBytes, + maximumDeletionCount: 1, + maximumCleanupJournalBytes: encodedJournal.count + ) + let exactKeystore = InMemoryKeychain() + try exactKeystore.saveKey( + account.privateKey, + with: legacyIdentifier + ) + let exactMigrator = KeystoreMigrator( + sourceVersion: .version1, + destinationVersion: .version2, + keystore: exactKeystore, + resourceLimits: exactLimits + ) + + try stageExactBoundaryFixture( + in: exactMigrator, + firstIdentifier: firstDestinationIdentifier, + firstKey: firstKey, + secondIdentifier: secondDestinationIdentifier, + secondKey: secondKey, + legacyIdentifier: legacyIdentifier + ) + try exactMigrator.prepare() + + XCTAssertEqual( + try exactKeystore.fetchKey( + for: KeystoreMigrator.pendingCleanupIdentifier + ).count, + encodedJournal.count + ) + XCTAssertEqual( + try exactKeystore.fetchKey( + for: firstDestinationIdentifier + ), + firstKey + ) + XCTAssertEqual( + try exactKeystore.fetchKey( + for: secondDestinationIdentifier + ), + secondKey + ) + + try exactMigrator.finalize() + + XCTAssertFalse(try exactKeystore.checkKey(for: legacyIdentifier)) + XCTAssertFalse( + try exactKeystore.checkKey( + for: KeystoreMigrator.pendingCleanupIdentifier + ) + ) + } + + func testKeystoreResourceLimits_whenLegacyRowLimitExceeded_freezesAndFailsBeforePrepareWrites() throws { + let keystore = InMemoryKeychain() + let migrator = KeystoreMigrator( + sourceVersion: .version1, + destinationVersion: .version2, + keystore: keystore, + resourceLimits: makeResourceLimits( + maximumLegacyAccountRows: 1 + ) + ) + let destinationIdentifier = + KeystoreTagV2.substrateSecretKeyTagForMetaId( + "00000000-0000-4000-8000-000000000010" + ) + + try migrator.switchVersion() + try migrator.registerLegacyAccountRow() + XCTAssertThrowsError( + try migrator.registerLegacyAccountRow() + ) { error in + self.assertResourceLimitError( + error, + resource: .legacyAccountRows, + maximum: 1 + ) + } + + migrator.save(key: Data([0x01]), for: destinationIdentifier) + + XCTAssertTrue(migrator.tempKeystore.isEmpty) + assertPrepareFailsClosed( + migrator, + keystore: keystore, + resource: .legacyAccountRows, + maximum: 1 + ) + } + + func testKeystoreResourceLimits_whenStagedKeyCountExceeded_freezesAndFailsBeforePrepareWrites() throws { + let keystore = InMemoryKeychain() + let migrator = makeVersion2KeystoreMigrator( + keystore: keystore, + resourceLimits: makeResourceLimits( + maximumStagedKeyCount: 1 + ) + ) + let firstIdentifier = + KeystoreTagV2.substrateSecretKeyTagForMetaId( + "00000000-0000-4000-8000-000000000011" + ) + let rejectedIdentifier = + KeystoreTagV2.substrateSeedTagForMetaId( + "00000000-0000-4000-8000-000000000012" + ) + + migrator.save(key: Data([0x01]), for: firstIdentifier) + migrator.save(key: Data([0x02]), for: rejectedIdentifier) + migrator.deleteKey(for: "ignored-after-first-limit") + + XCTAssertEqual(migrator.tempKeystore, [firstIdentifier: Data([0x01])]) + XCTAssertTrue(migrator.identifiersToRemoveOnFinalize.isEmpty) + assertPrepareFailsClosed( + migrator, + keystore: keystore, + resource: .stagedKeyCount, + maximum: 1 + ) + XCTAssertFalse(try keystore.checkKey(for: firstIdentifier)) + XCTAssertFalse(try keystore.checkKey(for: rejectedIdentifier)) + } + + func testKeystoreResourceLimits_whenIdentifierBytesExceeded_freezesAndFailsBeforePrepareWrites() throws { + let keystore = InMemoryKeychain() + let identifier = + KeystoreTagV2.substrateSecretKeyTagForMetaId( + "00000000-0000-4000-8000-000000000013" + ) + let maximum = identifier.utf8.count - 1 + let migrator = makeVersion2KeystoreMigrator( + keystore: keystore, + resourceLimits: makeResourceLimits( + maximumIdentifierUTF8Bytes: maximum + ) + ) + + migrator.save(key: Data([0x01]), for: identifier) + migrator.deleteKey(for: "ignored-after-first-limit") + + XCTAssertTrue(migrator.tempKeystore.isEmpty) + XCTAssertTrue(migrator.identifiersToRemoveOnFinalize.isEmpty) + assertPrepareFailsClosed( + migrator, + keystore: keystore, + resource: .identifierUTF8Bytes, + maximum: maximum + ) + XCTAssertFalse(try keystore.checkKey(for: identifier)) + } + + func testKeystoreResourceLimits_whenStagedKeyBytesExceeded_freezesAndFailsBeforePrepareWrites() throws { + let keystore = InMemoryKeychain() + let identifier = + KeystoreTagV2.substrateSecretKeyTagForMetaId( + "00000000-0000-4000-8000-000000000014" + ) + let key = Data([0x01, 0x02]) + let migrator = makeVersion2KeystoreMigrator( + keystore: keystore, + resourceLimits: makeResourceLimits( + maximumStagedKeyBytes: key.count - 1 + ) + ) + + migrator.save(key: key, for: identifier) + migrator.save( + key: Data([0x03]), + for: KeystoreTagV2.substrateSeedTagForMetaId( + "00000000-0000-4000-8000-000000000015" + ) + ) + + XCTAssertTrue(migrator.tempKeystore.isEmpty) + assertPrepareFailsClosed( + migrator, + keystore: keystore, + resource: .stagedKeyBytes, + maximum: key.count - 1 + ) + XCTAssertFalse(try keystore.checkKey(for: identifier)) + } + + func testKeystoreResourceLimits_whenDeletionCountExceeded_freezesAndFailsBeforePrepareWrites() throws { + let keystore = InMemoryKeychain() + let destinationIdentifier = + KeystoreTagV2.substrateSecretKeyTagForMetaId( + "00000000-0000-4000-8000-000000000016" + ) + let ignoredIdentifier = + KeystoreTagV2.substrateSeedTagForMetaId( + "00000000-0000-4000-8000-000000000017" + ) + let migrator = makeVersion2KeystoreMigrator( + keystore: keystore, + resourceLimits: makeResourceLimits( + maximumDeletionCount: 0 + ) + ) + + migrator.save(key: Data([0x01]), for: destinationIdentifier) + migrator.deleteKey(for: "first-rejected-deletion") + migrator.save(key: Data([0x02]), for: ignoredIdentifier) + + XCTAssertEqual( + migrator.tempKeystore, + [destinationIdentifier: Data([0x01])] + ) + XCTAssertTrue(migrator.identifiersToRemoveOnFinalize.isEmpty) + assertPrepareFailsClosed( + migrator, + keystore: keystore, + resource: .deletionCount, + maximum: 0 + ) + XCTAssertFalse(try keystore.checkKey(for: destinationIdentifier)) + XCTAssertFalse(try keystore.checkKey(for: ignoredIdentifier)) + } + + func testKeystoreResourceLimits_whenEncodedJournalExceeded_failsBeforeJournalOrDestinationWrites() throws { + let keystore = InMemoryKeychain() + let destinationIdentifier = + KeystoreTagV2.substrateSecretKeyTagForMetaId( + "00000000-0000-4000-8000-000000000018" + ) + let migrator = makeVersion2KeystoreMigrator( + keystore: keystore, + resourceLimits: makeResourceLimits( + maximumCleanupJournalBytes: 0 + ) + ) + + migrator.save(key: Data([0x01]), for: destinationIdentifier) + + assertPrepareFailsClosed( + migrator, + keystore: keystore, + resource: .cleanupJournalBytes, + maximum: 0 + ) + migrator.save( + key: Data([0x02]), + for: KeystoreTagV2.substrateSeedTagForMetaId( + "00000000-0000-4000-8000-000000000019" + ) + ) + + XCTAssertEqual( + migrator.tempKeystore, + [destinationIdentifier: Data([0x01])] + ) + XCTAssertFalse(try keystore.checkKey(for: destinationIdentifier)) + } + + func testCleanupRecovery_whenEncodedJournalExceedsProductionLimit_rejectsBeforeDecodeOrMutation() throws { + let maximum = KeystoreMigrationResourceLimits.production + .maximumCleanupJournalBytes + let oversizedJournal = Data( + repeating: 0x7B, + count: maximum + 1 + ) + let keystore = RecordingMigrationKeychain() + try keystore.saveKey( + oversizedJournal, + with: KeystoreMigrator.pendingCleanupIdentifier + ) + keystore.resetMutationHistory() + + XCTAssertThrowsError( + try KeystoreMigrator.recoverPendingCleanup( + keystore: keystore, + currentVersion: .version1 + ) + ) { error in + self.assertResourceLimitError( + error, + resource: .cleanupJournalBytes, + maximum: maximum + ) + } + + XCTAssertTrue(keystore.mutatedIdentifiers.isEmpty) + XCTAssertEqual( + try keystore.fetchKey( + for: KeystoreMigrator.pendingCleanupIdentifier + ), + oversizedJournal + ) + } + + func testV1Migration_whenLegacyRowLimitExceeded_preservesLiveStoreAndKeychain() throws { + let firstAccount = try generateOldAccount( + hasEntropy: false, + hasSeed: false, + hasDerivationPath: false + ) + let secondAccount = try generateOldAccount( + hasEntropy: false, + hasSeed: false, + hasDerivationPath: false + ) + let accounts = [firstAccount, secondAccount] + try saveOldAccountRows(accounts) + let keystore = RecordingMigrationKeychain() + try accounts.forEach { + try saveLegacySecrets(for: $0, keystore: keystore) + } + keystore.resetMutationHistory() + var replacementAttempted = false + let limits = makeResourceLimits( + maximumLegacyAccountRows: 1 + ) + let migrator = UserStorageMigrator( + targetVersion: .version2, + storeURL: storeURL, + modelDirectory: modelDirectory, + keystore: keystore, + settings: InMemorySettingsManager(), + fileManager: FileManager.default, + modelBundle: Bundle(for: UserDataStorageFacade.self), + storeReplacer: { _, _ in + replacementAttempted = true + }, + keystoreMigratorFactory: { source, destination, keystore in + KeystoreMigrator( + sourceVersion: source, + destinationVersion: destination, + keystore: keystore, + resourceLimits: limits + ) + } + ) + + XCTAssertThrowsError(try migrator.performMigration()) { error in + self.assertResourceLimitError( + error, + resource: .legacyAccountRows, + maximum: 1 + ) + } + + XCTAssertFalse(replacementAttempted) + XCTAssertTrue(keystore.mutatedIdentifiers.isEmpty) + XCTAssertTrue(migrator.requiresMigration()) + XCTAssertEqual(try fetchOldAccountCount(), accounts.count) + for account in accounts { + XCTAssertEqual( + try keystore.fetchKey( + for: KeystoreTag.secretKeyTagForAddress( + account.address + ) + ), + account.privateKey + ) + } + XCTAssertFalse( + try keystore.checkKey( + for: KeystoreMigrator.pendingCleanupIdentifier + ) + ) + } + + func testV1Migration_whenNonthrowingStagingLimitExceeded_preservesLiveStoreAndMakesNoKeychainWrites() throws { + let account = try generateOldAccount( + hasEntropy: false, + hasSeed: false, + hasDerivationPath: false + ) + try saveOldAccountRows([account]) + let keystore = RecordingMigrationKeychain() + try saveLegacySecrets(for: account, keystore: keystore) + keystore.resetMutationHistory() + var replacementAttempted = false + let limits = makeResourceLimits( + maximumStagedKeyCount: 0 + ) + let migrator = UserStorageMigrator( + targetVersion: .version2, + storeURL: storeURL, + modelDirectory: modelDirectory, + keystore: keystore, + settings: InMemorySettingsManager(), + fileManager: FileManager.default, + modelBundle: Bundle(for: UserDataStorageFacade.self), + storeReplacer: { _, _ in + replacementAttempted = true + }, + keystoreMigratorFactory: { source, destination, keystore in + KeystoreMigrator( + sourceVersion: source, + destinationVersion: destination, + keystore: keystore, + resourceLimits: limits + ) + } + ) + + XCTAssertThrowsError(try migrator.performMigration()) { error in + self.assertResourceLimitError( + error, + resource: .stagedKeyCount, + maximum: 0 + ) + } + + XCTAssertFalse(replacementAttempted) + XCTAssertTrue(keystore.mutatedIdentifiers.isEmpty) + XCTAssertTrue(migrator.requiresMigration()) + XCTAssertEqual(try fetchOldAccountCount(), 1) + XCTAssertEqual( + try keystore.fetchKey( + for: KeystoreTag.secretKeyTagForAddress(account.address) + ), + account.privateKey + ) + XCTAssertFalse( + try keystore.checkKey( + for: KeystoreMigrator.pendingCleanupIdentifier + ) + ) + } + + func testV1Migration_whenEntropyExceedsStagedByteLimit_abortsBeforeCryptographicDerivation() throws { + let account = try generateOldAccount( + hasEntropy: false, + hasSeed: false, + hasDerivationPath: false + ) + try saveOldAccountRows([account]) + let oversizedInvalidEntropy = Data([0xA5, 0xA5]) + let keystore = RecordingMigrationKeychain() + try saveLegacySecrets(for: account, keystore: keystore) + try keystore.saveKey( + oversizedInvalidEntropy, + with: KeystoreTag.entropyTagForAddress(account.address) + ) + keystore.resetMutationHistory() + var replacementAttempted = false + let limits = makeResourceLimits( + maximumStagedKeyBytes: oversizedInvalidEntropy.count - 1 + ) + let migrator = UserStorageMigrator( + targetVersion: .version2, + storeURL: storeURL, + modelDirectory: modelDirectory, + keystore: keystore, + settings: InMemorySettingsManager(), + fileManager: FileManager.default, + modelBundle: Bundle(for: UserDataStorageFacade.self), + storeReplacer: { _, _ in + replacementAttempted = true + }, + keystoreMigratorFactory: { source, destination, keystore in + KeystoreMigrator( + sourceVersion: source, + destinationVersion: destination, + keystore: keystore, + resourceLimits: limits + ) + } + ) + + XCTAssertThrowsError(try migrator.performMigration()) { error in + self.assertResourceLimitError( + error, + resource: .stagedKeyBytes, + maximum: oversizedInvalidEntropy.count - 1 + ) + } + + XCTAssertFalse(replacementAttempted) + XCTAssertTrue(keystore.mutatedIdentifiers.isEmpty) + XCTAssertTrue(migrator.requiresMigration()) + XCTAssertEqual(try fetchOldAccountCount(), 1) + XCTAssertEqual( + try keystore.fetchKey( + for: KeystoreTag.entropyTagForAddress(account.address) + ), + oversizedInvalidEntropy + ) + XCTAssertFalse( + try keystore.checkKey( + for: KeystoreMigrator.pendingCleanupIdentifier + ) + ) } - private func performTestUserMigration(hasEntropy: Bool, hasSeed: Bool, hasDerivationPath: Bool) throws { + private func performTestUserMigration( + hasEntropy: Bool, + hasSeed: Bool, + hasDerivationPath: Bool, + targetVersion: UserStorageVersion = .version2 + ) throws { // given let keystore = InMemoryKeychain() @@ -98,12 +716,13 @@ class SingleToMultiassetUserMigrationTests: XCTestCase { settings.set(value: Data(), for: SettingsKey.selectedConnection.rawValue) let migrator = UserStorageMigrator( - targetVersion: .version2, + targetVersion: targetVersion, storeURL: storeURL, modelDirectory: modelDirectory, keystore: keystore, settings: settings, - fileManager: FileManager.default + fileManager: FileManager.default, + modelBundle: Bundle(for: UserDataStorageFacade.self) ) guard migrator.requiresMigration() else { @@ -111,11 +730,11 @@ class SingleToMultiassetUserMigrationTests: XCTestCase { return } - migrator.performMigration() + try migrator.performMigration() // then - let newEntities = try fetchNewEntities() + let newEntities = try fetchNewEntities(version: targetVersion) let addressFactory = SS58AddressFactory() @@ -190,6 +809,27 @@ class SingleToMultiassetUserMigrationTests: XCTestCase { } else { XCTAssertFalse(substrateDerivPathExistence) } + + XCTAssertFalse( + try keystore.checkKey( + for: KeystoreTag.secretKeyTagForAddress(account.address) + ) + ) + XCTAssertFalse( + try keystore.checkKey( + for: KeystoreTag.seedTagForAddress(account.address) + ) + ) + XCTAssertFalse( + try keystore.checkKey( + for: KeystoreTag.entropyTagForAddress(account.address) + ) + ) + XCTAssertFalse( + try keystore.checkKey( + for: KeystoreTag.deriviationTagForAddress(account.address) + ) + ) } let orders = Set(newEntities.map { $0.order }) @@ -200,6 +840,11 @@ class SingleToMultiassetUserMigrationTests: XCTestCase { XCTAssertNil(settings.data(for: SettingsKey.selectedAccount.rawValue)) XCTAssertNil(settings.data(for: SettingsKey.selectedConnection.rawValue)) + XCTAssertFalse( + try keystore.checkKey( + for: KeystoreMigrator.pendingCleanupIdentifier + ) + ) } // MARK: Private @@ -209,10 +854,9 @@ class SingleToMultiassetUserMigrationTests: XCTestCase { Bundle.allFrameworks + Bundle.allBundles for bundle in bundles { - if let url = bundle.url( - forResource: version.rawValue, - withExtension: "mom", - subdirectory: modelDirectory + if let url = version.modelURL( + in: bundle, + legacyModelDirectory: modelDirectory ) { return url } @@ -238,8 +882,433 @@ class SingleToMultiassetUserMigrationTests: XCTestCase { return CoreDataService(configuration: configuration) } - private func fetchNewEntities() throws -> [NewEntity] { - let dbService = createCoreDataService(for: .version2) + private func performDuplicateSignerMigration( + rowOrder: DuplicateRowOrder + ) throws { + let keystore = InMemoryKeychain() + let settings = InMemorySettingsManager() + let generatedAccount = try generateOldAccount( + hasEntropy: false, + hasSeed: false, + hasDerivationPath: false + ) + let watchOnlyAddress = try SS58AddressFactory().address( + fromAccountId: generatedAccount.publicKey, + type: 2 + ) + let watchOnlyAccount = OldAccount( + address: watchOnlyAddress, + cryptoType: generatedAccount.cryptoType, + name: "watch-only-alias", + privateKey: generatedAccount.privateKey, + publicKey: generatedAccount.publicKey, + entropy: nil, + derivationPath: nil, + seed: nil + ) + let signingAccount = OldAccount( + address: generatedAccount.address, + cryptoType: generatedAccount.cryptoType, + name: "signing-alias", + privateKey: generatedAccount.privateKey, + publicKey: generatedAccount.publicKey, + entropy: nil, + derivationPath: nil, + seed: nil + ) + let accounts: [OldAccount] + + switch rowOrder { + case .watchOnlyFirst: + accounts = [watchOnlyAccount, signingAccount] + case .signerFirst: + accounts = [signingAccount, watchOnlyAccount] + } + + try saveOldAccountRows(accounts) + try keystore.saveKey( + signingAccount.privateKey, + with: KeystoreTag.secretKeyTagForAddress(signingAccount.address) + ) + + let migrator = UserStorageMigrator( + targetVersion: .version2, + storeURL: storeURL, + modelDirectory: modelDirectory, + keystore: keystore, + settings: settings, + fileManager: FileManager.default, + modelBundle: Bundle(for: UserDataStorageFacade.self) + ) + + try migrator.performMigration() + + let newEntities = try fetchNewEntities(version: .version2) + let migratedAccount = try XCTUnwrap(newEntities.first) + + XCTAssertEqual(newEntities.count, 1) + XCTAssertEqual(migratedAccount.name, signingAccount.name) + XCTAssertEqual( + migratedAccount.substratePublicKey, + signingAccount.publicKey + ) + XCTAssertEqual( + try keystore.fetchKey( + for: KeystoreTagV2.substrateSecretKeyTagForMetaId( + migratedAccount.metaId + ) + ), + signingAccount.privateKey + ) + XCTAssertFalse( + try keystore.checkKey( + for: KeystoreTag.secretKeyTagForAddress(signingAccount.address) + ) + ) + XCTAssertFalse( + try keystore.checkKey( + for: KeystoreMigrator.pendingCleanupIdentifier + ) + ) + } + + private func performConflictingDuplicateMigration( + rowOrder: ConflictingRowOrder + ) throws { + let keystore = InMemoryKeychain() + let generatedAccount = try generateOldAccount( + hasEntropy: false, + hasSeed: false, + hasDerivationPath: false + ) + let secondAddress = try SS58AddressFactory().address( + fromAccountId: generatedAccount.publicKey, + type: 2 + ) + let firstAccount = OldAccount( + address: generatedAccount.address, + cryptoType: generatedAccount.cryptoType, + name: "first-signing-alias", + privateKey: generatedAccount.privateKey, + publicKey: generatedAccount.publicKey, + entropy: nil, + derivationPath: nil, + seed: nil + ) + let conflictingPrivateKey = Data( + repeating: 0xA5, + count: generatedAccount.privateKey.count + ) + XCTAssertNotEqual(conflictingPrivateKey, generatedAccount.privateKey) + let secondAccount = OldAccount( + address: secondAddress, + cryptoType: generatedAccount.cryptoType, + name: "second-signing-alias", + privateKey: conflictingPrivateKey, + publicKey: generatedAccount.publicKey, + entropy: nil, + derivationPath: nil, + seed: nil + ) + let accounts: [OldAccount] + + switch rowOrder { + case .firstAliasFirst: + accounts = [firstAccount, secondAccount] + case .secondAliasFirst: + accounts = [secondAccount, firstAccount] + } + + try saveOldAccountRows(accounts) + try keystore.saveKey( + firstAccount.privateKey, + with: KeystoreTag.secretKeyTagForAddress(firstAccount.address) + ) + try keystore.saveKey( + secondAccount.privateKey, + with: KeystoreTag.secretKeyTagForAddress(secondAccount.address) + ) + + var replacementAttempted = false + let migrator = UserStorageMigrator( + targetVersion: .version2, + storeURL: storeURL, + modelDirectory: modelDirectory, + keystore: keystore, + settings: InMemorySettingsManager(), + fileManager: FileManager.default, + modelBundle: Bundle(for: UserDataStorageFacade.self), + storeReplacer: { _, _ in + replacementAttempted = true + } + ) + + XCTAssertThrowsError(try migrator.performMigration()) { error in + guard let identifier = self.stagedConflictIdentifier(in: error) else { + return XCTFail("Unexpected error: \(error)") + } + + XCTAssertTrue( + identifier.hasSuffix("-substrateSecretKey"), + "Unexpected conflicting identifier: \(identifier)" + ) + } + + XCTAssertFalse(replacementAttempted) + XCTAssertTrue(migrator.requiresMigration()) + XCTAssertEqual(try fetchOldAccountCount(), 2) + XCTAssertEqual( + try keystore.fetchKey( + for: KeystoreTag.secretKeyTagForAddress(firstAccount.address) + ), + firstAccount.privateKey + ) + XCTAssertEqual( + try keystore.fetchKey( + for: KeystoreTag.secretKeyTagForAddress(secondAccount.address) + ), + secondAccount.privateKey + ) + XCTAssertFalse( + try keystore.checkKey( + for: KeystoreMigrator.pendingCleanupIdentifier + ) + ) + } + + private func makeResourceLimits( + maximumLegacyAccountRows: Int = Int.max, + maximumStagedKeyCount: Int = Int.max, + maximumIdentifierUTF8Bytes: Int = Int.max, + maximumStagedKeyBytes: Int = Int.max, + maximumDeletionCount: Int = Int.max, + maximumCleanupJournalBytes: Int = Int.max + ) -> KeystoreMigrationResourceLimits { + KeystoreMigrationResourceLimits( + maximumLegacyAccountRows: maximumLegacyAccountRows, + maximumStagedKeyCount: maximumStagedKeyCount, + maximumIdentifierUTF8Bytes: maximumIdentifierUTF8Bytes, + maximumStagedKeyBytes: maximumStagedKeyBytes, + maximumDeletionCount: maximumDeletionCount, + maximumCleanupJournalBytes: maximumCleanupJournalBytes + ) + } + + private func makeVersion2KeystoreMigrator( + keystore: KeystoreProtocol, + resourceLimits: KeystoreMigrationResourceLimits + ) -> KeystoreMigrator { + let migrator = KeystoreMigrator( + sourceVersion: .version1, + destinationVersion: .version2, + keystore: keystore, + resourceLimits: resourceLimits + ) + + do { + try migrator.switchVersion() + } catch { + XCTFail("Unable to advance keystore test migrator: \(error)") + } + + return migrator + } + + private func stageExactBoundaryFixture( + in migrator: KeystoreMigrator, + firstIdentifier: String, + firstKey: Data, + secondIdentifier: String, + secondKey: Data, + legacyIdentifier: String + ) throws { + try migrator.switchVersion() + try migrator.registerLegacyAccountRow() + try migrator.registerLegacyAccountRow() + migrator.save(key: firstKey, for: firstIdentifier) + migrator.save(key: secondKey, for: secondIdentifier) + migrator.deleteKey(for: legacyIdentifier) + } + + private func assertPrepareFailsClosed( + _ migrator: KeystoreMigrator, + keystore: KeystoreProtocol, + resource: KeystoreMigrationResource, + maximum: Int, + file: StaticString = #filePath, + line: UInt = #line + ) { + XCTAssertThrowsError( + try migrator.prepare(), + file: file, + line: line + ) { error in + self.assertResourceLimitError( + error, + resource: resource, + maximum: maximum, + file: file, + line: line + ) + } + + XCTAssertFalse( + try keystore.checkKey( + for: KeystoreMigrator.pendingCleanupIdentifier + ), + file: file, + line: line + ) + } + + private func assertResourceLimitError( + _ error: Error, + resource: KeystoreMigrationResource, + maximum: Int, + file: StaticString = #filePath, + line: UInt = #line + ) { + guard + let violation = resourceLimitViolation(in: error) + else { + return XCTFail( + "Unexpected error: \(error)", + file: file, + line: line + ) + } + + XCTAssertEqual( + violation.resource.rawValue, + resource.rawValue, + file: file, + line: line + ) + XCTAssertEqual( + violation.maximum, + maximum, + file: file, + line: line + ) + } + + private func resourceLimitViolation( + in error: Error + ) -> ( + resource: KeystoreMigrationResource, + maximum: Int + )? { + if + let migrationError = error as? KeystoreMigratingError, + case let .resourceLimitExceeded( + resource, + maximum + ) = migrationError + { + return (resource, maximum) + } + + let nsError = error as NSError + if + let underlyingError = + nsError.userInfo[NSUnderlyingErrorKey] as? Error, + let violation = resourceLimitViolation(in: underlyingError) + { + return violation + } + + if + let detailedErrors = + nsError.userInfo[NSDetailedErrorsKey] as? [Error] + { + for detailedError in detailedErrors { + if + let violation = resourceLimitViolation( + in: detailedError + ) + { + return violation + } + } + } + + return nil + } + + private func stagedConflictIdentifier(in error: Error) -> String? { + if + let migrationError = error as? KeystoreMigratingError, + case let .stagedKeyConflict(identifier) = migrationError + { + return identifier + } + + let nsError = error as NSError + if + let underlyingError = nsError.userInfo[NSUnderlyingErrorKey] as? Error, + let identifier = stagedConflictIdentifier(in: underlyingError) + { + return identifier + } + + if let detailedErrors = nsError.userInfo[NSDetailedErrorsKey] as? [Error] { + for detailedError in detailedErrors { + if let identifier = stagedConflictIdentifier(in: detailedError) { + return identifier + } + } + } + + return nil + } + + private func fetchOldAccountCount() throws -> Int { + let dbService = createCoreDataService(for: .version1) + let semaphore = DispatchSemaphore(value: 0) + var accountCount: Int? + var fetchError: Error? + + dbService.performAsync { context, error in + defer { + semaphore.signal() + } + + if let error { + fetchError = error + return + } + + guard let context else { + fetchError = MigrationTestError.coreData( + "Missing Core Data context while fetching legacy entities" + ) + return + } + + do { + accountCount = try context.count( + for: NSFetchRequest( + entityName: "CDAccountItem" + ) + ) + } catch { + fetchError = error + } + } + + semaphore.wait() + if let fetchError { + throw fetchError + } + + try dbService.close() + return accountCount ?? 0 + } + + private func fetchNewEntities( + version: UserStorageVersion + ) throws -> [NewEntity] { + let dbService = createCoreDataService(for: version) let semaphore = DispatchSemaphore(value: 0) var newEntities: [NewEntity]? var fetchError: Error? @@ -313,12 +1382,21 @@ class SingleToMultiassetUserMigrationTests: XCTestCase { hasSeed: Bool, hasDerivationPath: Bool ) throws -> [OldAccount] { - let dbService = createCoreDataService(for: .version1) - let accounts = try (0.. Data { + try keychain.fetchKey(for: identifier) + } + + func checkKey(for identifier: String) throws -> Bool { + try keychain.checkKey(for: identifier) + } + + func deleteKey(for identifier: String) throws { + mutatedIdentifiers.append(identifier) + try keychain.deleteKey(for: identifier) + } +} diff --git a/fearlessTests/Common/Migration/SubstrateStorageClassResolutionTests.swift b/fearlessTests/Common/Migration/SubstrateStorageClassResolutionTests.swift new file mode 100644 index 0000000000..1b041d8953 --- /dev/null +++ b/fearlessTests/Common/Migration/SubstrateStorageClassResolutionTests.swift @@ -0,0 +1,1320 @@ +import CoreData +import CryptoKit +import RobinHood +import SSFModels +import XCTest +@testable import fearless + +#if canImport(SSFAssetManagmentStorage) + import SSFAssetManagmentStorage + private typealias TestCDScamInfo = SSFAssetManagmentStorage.CDScamInfo +#else + private typealias TestCDScamInfo = fearless.SSFAssetManagmentStorageStub.CDScamInfo +#endif + +final class SubstrateStorageClassResolutionTests: XCTestCase { + private struct EntityClassExpectation { + let entityName: String + let managedObjectClass: NSManagedObject.Type + } + + private struct RuntimeProjection: Codable { + let chain: String + let version: UInt32 + let txVersion: UInt32 + let metadataDigest: Data + + var sortKey: [String] { + [ + chain, + String(version), + String(txVersion), + metadataDigest.base64EncodedString() + ] + } + } + + private struct NodeProjection: Codable { + let url: String + let name: String + let apiQueryName: String? + let apiKeyName: String? + + var sortKey: [String] { + [ + url, + name, + apiQueryName ?? "", + apiKeyName ?? "" + ] + } + } + + private struct ChainProjection: Codable { + let chainId: String + let name: String + let disabled: Bool + let assetIds: [String] + let nodes: [NodeProjection] + let customNodes: [NodeProjection] + let selectedNode: NodeProjection? + + var sortKey: [String] { + [chainId, name] + } + } + + private struct RepositoryProjection: Codable { + let runtimes: [RuntimeProjection] + let chains: [ChainProjection] + } + + private struct RepositoryAggregate: Equatable { + let runtimeCount: Int + let chainCount: Int + let enabledChainCount: Int + let assetCount: Int + let nodeCount: Int + let defaultNodeRelationshipCount: Int + let customNodeRelationshipCount: Int + let selectedNodeRelationshipCount: Int + let standaloneSelectedNodeCount: Int + let fingerprint: Data + } + + private let expectedEntityVersionHashes = [ + "CDAsset": "OaCdB9COHi79/aWro7D/NMuFpNBI0ZFzrprILS+KmP8=", + "CDChain": "xuYLmoPjdtSUBxBnb6XTrlVhylYyXA8GEw4KmzL1b+I=", + "CDChainNode": "ss/vV1rML8kMdjwg0WBlnN+nUfbimb+iS858b2UC5co=", + "CDChainStorageItem": "tl1izvM2Yua6zVUkbOKhur324LSAxJOj9/M+5iRiFa4=", + "CDChainXcmConfig": "0HjIIf+quGUJQ/o84PdC5mHNcC86v1mrG2sBdayWiso=", + "CDContact": "DXCWS/mWPGoVysoTZy+8QKx6H5KWgV7GhaJRQRHUE3c=", + "CDContactItem": "a1f9E6n04Dw2O8avnrKh71ChA215v4tanvXG60z6M3k=", + "CDExternalApi": "n4a9GYGVRmIwWteJEbC7nUd8PyzyJ7xd+AvVlG2BGvw=", + "CDPhishingItem": "rLQn++gZqmY/EmPOCkDFxCvVXhp2Hs+ZYah6D4p9JIk=", + "CDPolkaswapDex": "jU4M5szChWjMy/D1+IUOp1fz4qD5u4oehrmgVQ3Wel8=", + "CDPolkaswapRemoteSettings": "EwsBHlXIiySVJAormij6pHo0OtZz4gJLtHcCV8od1+k=", + "CDPriceData": "63ioMabYdLDL8vNP5aK563PaV6dPU9qUvjzPxD7aOPU=", + "CDPriceProvider": "zOsAxjSu9EHzLSO95UpY38tE4XkbWUwDExKBjlM26jA=", + "CDRuntimeMetadataItem": "lmOvdNZxh3zLwJN+6ZlUcLvB9HRBv9PYHbJt4QE07oY=", + "CDScamInfo": "pjD63BxcBAQfhasAHmj+ew7KQncf9FU1ftTpGKFEcsw=", + "CDStashItem": "DmeVbIKcFdGMSBvSoYHdRuDBurJHUWE2vAWz1Hbgl34=", + "CDTransactionHistoryItem": "5kc1p0hvfdoQDmabJaZ9pIRWCJa40tpLtCvwVjtnPdU=", + "CDXcmAvailableAsset": "JKPl407BzEYe3ztSezxPOv9Z9WAhoGsJlevWpgPn7Es=", + "CDXcmAvailableDestination": "yD9mzh4EBjT1sFrqYgDIDNUlrUyxQJ81S4GZXpIZi74=" + ] + + private var appBundle: Bundle { + Bundle(for: SubstrateDataStorageFacade.self) + } + + private var fallbackEntityClasses: [EntityClassExpectation] { + [ + EntityClassExpectation(entityName: "CDAsset", managedObjectClass: fearless.CDAsset.self), + EntityClassExpectation(entityName: "CDChain", managedObjectClass: fearless.CDChain.self), + EntityClassExpectation(entityName: "CDChainNode", managedObjectClass: fearless.CDChainNode.self), + EntityClassExpectation( + entityName: "CDChainStorageItem", + managedObjectClass: fearless.CDChainStorageItem.self + ), + EntityClassExpectation( + entityName: "CDChainXcmConfig", + managedObjectClass: fearless.CDChainXcmConfig.self + ), + EntityClassExpectation(entityName: "CDContact", managedObjectClass: fearless.CDContact.self), + EntityClassExpectation( + entityName: "CDContactItem", + managedObjectClass: fearless.CDContactItem.self + ), + EntityClassExpectation( + entityName: "CDExternalApi", + managedObjectClass: fearless.CDExternalApi.self + ), + EntityClassExpectation( + entityName: "CDPhishingItem", + managedObjectClass: fearless.CDPhishingItem.self + ), + EntityClassExpectation( + entityName: "CDPolkaswapDex", + managedObjectClass: polkaswapDexClass + ), + EntityClassExpectation( + entityName: "CDPolkaswapRemoteSettings", + managedObjectClass: fearless.CDPolkaswapRemoteSettings.self + ), + EntityClassExpectation( + entityName: "CDPriceData", + managedObjectClass: fearless.CDPriceData.self + ), + EntityClassExpectation( + entityName: "CDPriceProvider", + managedObjectClass: fearless.CDPriceProvider.self + ), + EntityClassExpectation( + entityName: "CDRuntimeMetadataItem", + managedObjectClass: fearless.CDRuntimeMetadataItem.self + ), + EntityClassExpectation(entityName: "CDScamInfo", managedObjectClass: scamInfoClass), + EntityClassExpectation( + entityName: "CDStashItem", + managedObjectClass: fearless.CDStashItem.self + ), + EntityClassExpectation( + entityName: "CDXcmAvailableAsset", + managedObjectClass: fearless.CDXcmAvailableAsset.self + ), + EntityClassExpectation( + entityName: "CDXcmAvailableDestination", + managedObjectClass: fearless.CDXcmAvailableDestination.self + ) + ] + } + + private var expectedEntityClasses: [EntityClassExpectation] { + fallbackEntityClasses + [ + EntityClassExpectation( + entityName: "CDTransactionHistoryItem", + managedObjectClass: fearless.CDTransactionHistoryItem.self + ) + ] + } + + private var polkaswapDexClass: NSManagedObject.Type { + #if canImport(SSFAssetManagmentStorage) + SSFAssetManagmentStorage.CDPolkaswapDex.self + #else + fearless.SSFAssetManagmentStorageStub.CDPolkaswapDex.self + #endif + } + + private var scamInfoClass: NSManagedObject.Type { + #if canImport(SSFAssetManagmentStorage) + SSFAssetManagmentStorage.CDScamInfo.self + #else + fearless.SSFAssetManagmentStorageStub.CDScamInfo.self + #endif + } + + func testBundledV8Model_whenLoaded_thenEveryEntityResolvesToExactRuntimeClass() throws { + let model = try loadBundledV8Model() + + XCTAssertEqual(fallbackEntityClasses.count, 18) + XCTAssertEqual( + Set(model.entitiesByName.keys), + Set(expectedEntityClasses.map(\.entityName)) + ) + + for expectation in expectedEntityClasses { + let entity = try XCTUnwrap(model.entitiesByName[expectation.entityName]) + let className = try XCTUnwrap(entity.managedObjectClassName) + let resolvedClass = try XCTUnwrap( + NSClassFromString(className) as? NSManagedObject.Type, + "\(className) must resolve to an NSManagedObject subclass" + ) + + XCTAssertEqual( + className, + NSStringFromClass(expectation.managedObjectClass), + "\(expectation.entityName) does not use the expected Objective-C runtime name" + ) + XCTAssertEqual( + ObjectIdentifier(resolvedClass), + ObjectIdentifier(expectation.managedObjectClass), + "\(expectation.entityName) resolves to a different class than its app alias" + ) + } + } + + func testBundledV8Model_whenObjectsInsertedAndFetched_thenUsesExactRuntimeClasses() throws { + let model = try loadBundledV8Model() + let coordinator = NSPersistentStoreCoordinator(managedObjectModel: model) + try coordinator.addPersistentStore( + ofType: NSInMemoryStoreType, + configurationName: nil, + at: nil + ) + + let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) + context.persistentStoreCoordinator = coordinator + + try performAndWait(in: context) { + for expectation in self.expectedEntityClasses { + let insertedObject = NSEntityDescription.insertNewObject( + forEntityName: expectation.entityName, + into: context + ) + + self.assertExactClass( + of: insertedObject, + is: expectation.managedObjectClass, + entityName: expectation.entityName + ) + + let request = NSFetchRequest(entityName: expectation.entityName) + let fetchedObjects = try context.fetch(request) + let fetchedObject = try XCTUnwrap( + fetchedObjects.first { $0 === insertedObject }, + "Inserted \(expectation.entityName) was not returned by its fetch" + ) + + self.assertExactClass( + of: fetchedObject, + is: expectation.managedObjectClass, + entityName: expectation.entityName + ) + } + + context.rollback() + } + } + + func testRuntimeMetadata_whenPersisted_thenCodableMapperRoundTripsBoundaryVersions() throws { + let model = try loadBundledV8Model() + let coordinator = NSPersistentStoreCoordinator(managedObjectModel: model) + try coordinator.addPersistentStore( + ofType: NSInMemoryStoreType, + configurationName: nil, + at: nil + ) + + let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) + context.persistentStoreCoordinator = coordinator + + let expectedItem = fearless.RuntimeMetadataItem( + chain: "class-resolution-regression", + version: UInt32.max, + txVersion: UInt32(Int32.max) + 1, + metadata: Data([0x00, 0x7F, 0x80, 0xFF]) + ) + let mapper = fearless.RuntimeMetadataMapper() + + try performAndWait(in: context) { + let insertedObject = try XCTUnwrap( + NSEntityDescription.insertNewObject( + forEntityName: "CDRuntimeMetadataItem", + into: context + ) as? fearless.CDRuntimeMetadataItem + ) + + try mapper.populate(entity: insertedObject, from: expectedItem, using: context) + try context.save() + context.reset() + + let request = NSFetchRequest( + entityName: "CDRuntimeMetadataItem" + ) + let persistedObject = try XCTUnwrap(context.fetch(request).first) + + self.assertExactClass( + of: persistedObject, + is: fearless.CDRuntimeMetadataItem.self, + entityName: "CDRuntimeMetadataItem" + ) + XCTAssertEqual(try mapper.transform(entity: persistedObject), expectedItem) + } + } + + func testRuntimeMetadataStartupMapper_whenGetterRaises_thenReturnsSanitizedSwiftError() { + let entityDescription = NSEntityDescription() + entityDescription.name = + "RuntimeMetadataGetterExceptionFixture" + entityDescription.managedObjectClassName = + NSStringFromClass( + fearless.CDRuntimeMetadataItem.self + ) + entityDescription.properties = [] + let entity = fearless.CDRuntimeMetadataItem( + entity: entityDescription, + insertInto: nil + ) + let mapper: AnyCoreDataMapper< + fearless.RuntimeMetadataItem, + fearless.CDRuntimeMetadataItem + > = AnyCoreDataMapper( + fearless.RuntimeMetadataMapper() + ) + + XCTAssertThrowsError( + try mapper.transform(entity: entity) + ) { error in + XCTAssertEqual( + error as? SafeTransformableValueReaderError, + .objectiveCException + ) + } + } + + func testRuntimeMetadataStartupMapper_whenSetterRaises_thenReturnsSanitizedSwiftError() { + let entityDescription = NSEntityDescription() + entityDescription.name = + "RuntimeMetadataSetterExceptionFixture" + entityDescription.managedObjectClassName = + NSStringFromClass( + fearless.CDRuntimeMetadataItem.self + ) + entityDescription.properties = [] + let entity = fearless.CDRuntimeMetadataItem( + entity: entityDescription, + insertInto: nil + ) + let mapper = fearless.RuntimeMetadataMapper() + let model = fearless.RuntimeMetadataItem( + chain: "setter-exception", + version: 1, + txVersion: 2, + metadata: Data([0x01]) + ) + + XCTAssertThrowsError( + try mapper.populate( + entity: entity, + from: model, + using: NSManagedObjectContext( + concurrencyType: + .mainQueueConcurrencyType + ) + ) + ) { error in + XCTAssertEqual( + error as? SafeTransformableValueReaderError, + .objectiveCException + ) + } + } + + func testFallbackCodableEntities_whenPersisted_thenTypedAccessorsAndMappersRoundTrip() throws { + let context = try makeInMemoryContext() + + let chainStorage = ChainStorageItem( + identifier: "chain-storage-🧪", + data: Data([0x00, 0x7F, 0x80, 0xFF]) + ) + XCTAssertEqual( + try roundTrip( + chainStorage, + entityName: "CDChainStorageItem", + entityClass: fearless.CDChainStorageItem.self, + in: context + ), + chainStorage + ) + + let stash = StashItem( + stash: "stash-address-α", + controller: "controller-address-β" + ) + XCTAssertEqual( + try roundTrip( + stash, + entityName: "CDStashItem", + entityClass: fearless.CDStashItem.self, + in: context + ), + stash + ) + + let phishing = PhishingItem( + source: "https://security.example.invalid/list.json", + publicKey: "0xPHISHING-PUBLIC-KEY" + ) + let decodedPhishing = try roundTrip( + phishing, + entityName: "CDPhishingItem", + entityClass: fearless.CDPhishingItem.self, + in: context + ) + XCTAssertEqual(decodedPhishing.identifier, phishing.identifier) + XCTAssertEqual(decodedPhishing.source, phishing.source) + XCTAssertEqual(decodedPhishing.publicKey, phishing.publicKey) + + let scam = ScamInfo( + name: "Unicode scam 🚫", + address: "0x0000000000000000000000000000000000000001", + type: .lowScore, + subtype: "adversarial-subtype" + ) + XCTAssertEqual( + try roundTrip( + scam, + entityName: "CDScamInfo", + entityClass: TestCDScamInfo.self, + in: context + ), + scam + ) + + let contact = Contact( + name: "Alice 🧪", + address: "1zugcac3aa3e6b4b32c48d0843ff91c7d6b07f9df4dbe3546b1bc472d29d6e56", + chainId: "0x91b171bb158e2d3848fa23a9f1c25182" + ) + XCTAssertEqual( + try roundTrip( + contact, + entityName: "CDContact", + entityClass: fearless.CDContact.self, + in: context + ), + contact + ) + + let contactItem = ContactItem( + peerAddress: "peer-address-\u{0000}-boundary", + peerName: "Peer 🧪", + targetAddress: "target-address", + updatedAt: Int64.max + ) + let decodedContactItem = try roundTrip( + contactItem, + entityName: "CDContactItem", + entityClass: fearless.CDContactItem.self, + in: context + ) + XCTAssertEqual(decodedContactItem.identifier, contactItem.identifier) + XCTAssertEqual(decodedContactItem.peerAddress, contactItem.peerAddress) + XCTAssertEqual(decodedContactItem.peerName, contactItem.peerName) + XCTAssertEqual(decodedContactItem.targetAddress, contactItem.targetAddress) + XCTAssertEqual(decodedContactItem.updatedAt, contactItem.updatedAt) + } + + func testFallbackCodableEntities_whenRequiredFieldsAreMissing_thenMappersRejectRows() throws { + let context = try makeInMemoryContext() + + try performAndWait(in: context) { + self.assertMalformedRowRejected( + ChainStorageItem.self, + entityName: "CDChainStorageItem", + entityClass: fearless.CDChainStorageItem.self, + in: context + ) + self.assertMalformedRowRejected( + StashItem.self, + entityName: "CDStashItem", + entityClass: fearless.CDStashItem.self, + in: context + ) + self.assertMalformedRowRejected( + PhishingItem.self, + entityName: "CDPhishingItem", + entityClass: fearless.CDPhishingItem.self, + in: context + ) + self.assertMalformedRowRejected( + ScamInfo.self, + entityName: "CDScamInfo", + entityClass: TestCDScamInfo.self, + in: context + ) + self.assertMalformedRowRejected( + Contact.self, + entityName: "CDContact", + entityClass: fearless.CDContact.self, + in: context + ) + self.assertMalformedRowRejected( + ContactItem.self, + entityName: "CDContactItem", + entityClass: fearless.CDContactItem.self, + in: context + ) + + let invalidScam = try XCTUnwrap( + NSEntityDescription.insertNewObject( + forEntityName: "CDScamInfo", + into: context + ) as? TestCDScamInfo + ) + invalidScam.setValue("name", forKey: "name") + invalidScam.setValue("address", forKey: "address") + invalidScam.setValue("not-a-valid-scam-type", forKey: "type") + invalidScam.setValue("subtype", forKey: "subtype") + + let scamMapper = CodableCoreDataMapper() + XCTAssertThrowsError(try scamMapper.transform(entity: invalidScam)) + + context.rollback() + } + } + + func testBundledV8Model_whenStoreCreated_thenSchemaFingerprintsAndCompatibilityStayCanonical() throws { + let model = try loadBundledV8Model() + let hashesBeforeOpening = versionHashes(for: model) + XCTAssertEqual(hashesBeforeOpening, expectedEntityVersionHashes) + + let directoryURL = FileManager.default.temporaryDirectory + .appendingPathComponent("SubstrateStorageClassResolutionTests") + .appendingPathComponent(UUID().uuidString) + try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true) + defer { + try? FileManager.default.removeItem(at: directoryURL) + } + + let storeURL = directoryURL.appendingPathComponent("SubstrateDataModel.sqlite") + let coordinator = NSPersistentStoreCoordinator(managedObjectModel: model) + let store = try coordinator.addPersistentStore( + ofType: NSSQLiteStoreType, + configurationName: nil, + at: storeURL, + options: [ + NSMigratePersistentStoresAutomaticallyOption: false, + NSInferMappingModelAutomaticallyOption: false + ] + ) + try coordinator.remove(store) + + let metadata = try NSPersistentStoreCoordinator.metadataForPersistentStore( + ofType: NSSQLiteStoreType, + at: storeURL, + options: nil + ) + + XCTAssertEqual(versionHashes(for: model), hashesBeforeOpening) + XCTAssertTrue( + model.isConfiguration(withName: nil, compatibleWithStoreMetadata: metadata) + ) + XCTAssertTrue( + model.isConfiguration( + withName: SubstrateStorageVersion.version8.rawValue, + compatibleWithStoreMetadata: metadata + ) + ) + XCTAssertEqual( + metadata["NSStoreModelVersionChecksumKey"] as? String, + "uCEwjJUN79CeWqRxzEihB72qcWaHALPET4ZxwIj+0zs=" + ) + } + + func testCopiedPhoneV8Store_whenAvailable_thenAllRowsHaveExpectedClassesAndStoreIsUnchanged() throws { + let fixtureURL = copiedPhoneStoreFixtureURL() + guard FileManager.default.fileExists(atPath: fixtureURL.path) else { + throw XCTSkip("Optional copied-phone Substrate store fixture is unavailable") + } + + let sourceFingerprintsBefore = try storeFingerprints(at: fixtureURL) + let testStoreURL = try copyStoreFixtureToTemporaryDirectory(fixtureURL) + defer { + try? FileManager.default.removeItem(at: testStoreURL.deletingLastPathComponent()) + } + + let testStoreDigestBefore = try digest(of: testStoreURL) + let metadataBefore = try NSPersistentStoreCoordinator.metadataForPersistentStore( + ofType: NSSQLiteStoreType, + at: testStoreURL, + options: nil + ) + let model = try loadBundledV8Model() + + XCTAssertTrue( + model.isConfiguration( + withName: SubstrateStorageVersion.version8.rawValue, + compatibleWithStoreMetadata: metadataBefore + ) + ) + + let countsBefore = try inspectReadOnlyStore( + at: testStoreURL, + model: model, + fetchEveryObject: true + ) + let countsAfter = try inspectReadOnlyStore( + at: testStoreURL, + model: try loadBundledV8Model(), + fetchEveryObject: false + ) + let metadataAfter = try NSPersistentStoreCoordinator.metadataForPersistentStore( + ofType: NSSQLiteStoreType, + at: testStoreURL, + options: nil + ) + + XCTAssertEqual(countsAfter, countsBefore) + XCTAssertGreaterThan(countsBefore["CDRuntimeMetadataItem"] ?? 0, 0) + XCTAssertEqual( + metadataVersionHashes(metadataAfter), + metadataVersionHashes(metadataBefore) + ) + XCTAssertEqual(try digest(of: testStoreURL), testStoreDigestBefore) + XCTAssertEqual(try storeFingerprints(at: fixtureURL), sourceFingerprintsBefore) + } + + func testCopiedPhoneV8Store_whenFetchedThroughProductionRepositories_thenMapsEveryRuntimeAndChain() throws { + let fixtureURL = copiedPhoneStoreFixtureURL() + guard FileManager.default.fileExists(atPath: fixtureURL.path) else { + throw XCTSkip("Optional copied-phone Substrate store fixture is unavailable") + } + + let sourceFingerprintsBefore = try storeFingerprints(at: fixtureURL) + let testStoreURL = try copyStoreFixtureToTemporaryDirectory(fixtureURL) + defer { + try? FileManager.default.removeItem(at: testStoreURL.deletingLastPathComponent()) + } + let persistedAggregate = try inspectPersistedRepositoryAggregate( + at: testStoreURL, + model: try loadBundledV8Model() + ) + + let modelURL = try XCTUnwrap( + appBundle.url( + forResource: "SubstrateDataModel", + withExtension: "momd" + ) + ) + let settings = CoreDataPersistentSettings( + databaseDirectory: testStoreURL.deletingLastPathComponent(), + databaseName: testStoreURL.lastPathComponent, + incompatibleModelStrategy: .ignore, + options: [ + NSReadOnlyPersistentStoreOption: true, + NSMigratePersistentStoresAutomaticallyOption: false, + NSInferMappingModelAutomaticallyOption: false + ] + ) + let service = CoreDataService( + configuration: CoreDataServiceConfiguration( + modelURL: modelURL, + storageType: .persistent(settings: settings) + ) + ) + defer { + try? service.close() + } + + let runtimeRepository = CoreDataRepository< + fearless.RuntimeMetadataItem, + fearless.CDRuntimeMetadataItem + >( + databaseService: service, + mapper: AnyCoreDataMapper( + CodableCoreDataMapper< + fearless.RuntimeMetadataItem, + fearless.CDRuntimeMetadataItem + >() + ) + ) + let chainRepository = CoreDataRepository( + databaseService: service, + mapper: AnyCoreDataMapper(ChainModelMapper()) + ) + let operationQueue = OperationQueue() + + let runtimeOperation = runtimeRepository.fetchAllOperation( + with: RepositoryFetchOptions() + ) + operationQueue.addOperations([runtimeOperation], waitUntilFinished: true) + let runtimes = try runtimeOperation.extractResultData( + throwing: BaseOperationError.parentOperationCancelled + ) + + let chainOperation = chainRepository.fetchAllOperation( + with: RepositoryFetchOptions() + ) + operationQueue.addOperations([chainOperation], waitUntilFinished: true) + let chains = try chainOperation.extractResultData( + throwing: BaseOperationError.parentOperationCancelled + ) + + XCTAssertEqual(runtimes.count, 60) + XCTAssertEqual(chains.count, 116) + assertMappedRepositoryInvariants(runtimes: runtimes, chains: chains) + + let mappedAggregate = try repositoryAggregate( + runtimes: runtimes.map(runtimeProjection), + chains: chains.map(chainProjection), + nodeCount: Set( + chains.flatMap { allNodes(in: $0) }.map { $0.url.absoluteString } + ).count, + assetCount: chains.reduce(0) { $0 + $1.assets.count } + ) + + XCTAssertEqual( + mappedAggregate, + persistedAggregate, + "Mapped repository aggregate differs from the disposable store without exposing row values" + ) + XCTAssertEqual(try storeFingerprints(at: fixtureURL), sourceFingerprintsBefore) + } + + private func inspectPersistedRepositoryAggregate( + at storeURL: URL, + model: NSManagedObjectModel + ) throws -> RepositoryAggregate { + let coordinator = NSPersistentStoreCoordinator(managedObjectModel: model) + let store = try coordinator.addPersistentStore( + ofType: NSSQLiteStoreType, + configurationName: nil, + at: storeURL, + options: [ + NSReadOnlyPersistentStoreOption: true, + NSMigratePersistentStoresAutomaticallyOption: false, + NSInferMappingModelAutomaticallyOption: false + ] + ) + defer { + try? coordinator.remove(store) + } + + let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) + context.persistentStoreCoordinator = coordinator + + var aggregate: RepositoryAggregate? + try performAndWait(in: context) { + let runtimeRequest = NSFetchRequest( + entityName: "CDRuntimeMetadataItem" + ) + let chainRequest = NSFetchRequest(entityName: "CDChain") + let nodeRequest = NSFetchRequest(entityName: "CDChainNode") + let assetRequest = NSFetchRequest(entityName: "CDAsset") + + aggregate = try self.repositoryAggregate( + runtimes: context.fetch(runtimeRequest).map(self.runtimeProjection), + chains: context.fetch(chainRequest).map(self.chainProjection), + nodeCount: context.count(for: nodeRequest), + assetCount: context.count(for: assetRequest) + ) + } + + return try XCTUnwrap(aggregate) + } + + private func assertMappedRepositoryInvariants( + runtimes: [fearless.RuntimeMetadataItem], + chains: [ChainModel], + file: StaticString = #filePath, + line: UInt = #line + ) { + let sortedRuntimes = runtimes.sorted { + runtimeProjection($0).sortKey.lexicographicallyPrecedes( + runtimeProjection($1).sortKey + ) + } + let runtimeIdentifiers = sortedRuntimes.map(\.chain) + + XCTAssertEqual( + Set(runtimeIdentifiers).count, + runtimeIdentifiers.count, + "Mapped runtime identifiers must be unique", + file: file, + line: line + ) + + for (runtimeIndex, runtime) in sortedRuntimes.enumerated() { + let canonicalIdentifier = runtime.chain.trimmingCharacters( + in: .whitespacesAndNewlines + ) + XCTAssertTrue( + !canonicalIdentifier.isEmpty && canonicalIdentifier == runtime.chain, + "Mapped runtime \(runtimeIndex) has a noncanonical identifier", + file: file, + line: line + ) + XCTAssertFalse( + runtime.metadata.isEmpty, + "Mapped runtime \(runtimeIndex) has empty metadata", + file: file, + line: line + ) + } + + let sortedChains = chains.sorted { + chainProjection($0).sortKey.lexicographicallyPrecedes( + chainProjection($1).sortKey + ) + } + let chainIdentifiers = sortedChains.map(\.chainId) + + XCTAssertEqual( + Set(chainIdentifiers).count, + chainIdentifiers.count, + "Mapped chain identifiers must be unique", + file: file, + line: line + ) + + for (chainIndex, chain) in sortedChains.enumerated() { + let canonicalIdentifier = chain.chainId.trimmingCharacters( + in: .whitespacesAndNewlines + ) + XCTAssertTrue( + !canonicalIdentifier.isEmpty && canonicalIdentifier == chain.chainId, + "Mapped chain \(chainIndex) has a noncanonical identifier", + file: file, + line: line + ) + XCTAssertFalse( + chain.chainId.hasPrefix( + ChainModelMapper.quarantinedChainIdentifierPrefix + ), + "Mapped chain \(chainIndex) unexpectedly contains a quarantine identifier", + file: file, + line: line + ) + XCTAssertTrue( + !chain.name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty, + "Mapped chain \(chainIndex) has an empty name", + file: file, + line: line + ) + XCTAssertFalse( + chain.name == ChainModelMapper.quarantinedChainName, + "Mapped chain \(chainIndex) unexpectedly contains a quarantine name", + file: file, + line: line + ) + + let assetIdentifiers = chain.assets.map(\.id) + XCTAssertEqual( + Set(assetIdentifiers).count, + assetIdentifiers.count, + "Mapped chain \(chainIndex) contains duplicate asset identifiers", + file: file, + line: line + ) + for (assetIndex, assetIdentifier) in assetIdentifiers.enumerated() { + XCTAssertFalse( + assetIdentifier.trimmingCharacters( + in: .whitespacesAndNewlines + ).isEmpty, + "Mapped chain \(chainIndex) asset \(assetIndex) has an empty identifier", + file: file, + line: line + ) + } + + let defaultNodes = Array(chain.nodes) + let customNodes = Array(chain.customNodes ?? []) + let selectedNodes = [chain.selectedNode].compactMap { $0 } + let nodeGroups = [defaultNodes, customNodes, selectedNodes] + + for (groupIndex, nodes) in nodeGroups.enumerated() { + for (nodeIndex, node) in nodes.enumerated() { + XCTAssertTrue( + ChainModelMapper.isUsableNodeURL(node.url), + "Mapped chain \(chainIndex) node \(groupIndex):\(nodeIndex) has an unusable URL", + file: file, + line: line + ) + XCTAssertTrue( + ChainModelMapper.isNodeCompatibleWithRuntime( + node, + for: chain + ), + "Mapped chain \(chainIndex) node \(groupIndex):\(nodeIndex) is runtime-incompatible", + file: file, + line: line + ) + XCTAssertFalse( + node.name.trimmingCharacters( + in: .whitespacesAndNewlines + ).isEmpty, + "Mapped chain \(chainIndex) node \(groupIndex):\(nodeIndex) has an empty name", + file: file, + line: line + ) + } + } + + XCTAssertTrue( + chain.disabled || chain.selectedNode != nil || chain.nodes.isNotEmpty, + "Enabled mapped chain \(chainIndex) has no runtime endpoint", + file: file, + line: line + ) + } + } + + private func repositoryAggregate( + runtimes: [RuntimeProjection], + chains: [ChainProjection], + nodeCount: Int, + assetCount: Int + ) throws -> RepositoryAggregate { + let canonicalRuntimes = runtimes.sorted { + $0.sortKey.lexicographicallyPrecedes($1.sortKey) + } + let canonicalChains = chains + .map { chain in + ChainProjection( + chainId: chain.chainId, + name: chain.name, + disabled: chain.disabled, + assetIds: chain.assetIds.sorted(), + nodes: chain.nodes.sorted { + $0.sortKey.lexicographicallyPrecedes($1.sortKey) + }, + customNodes: chain.customNodes.sorted { + $0.sortKey.lexicographicallyPrecedes($1.sortKey) + }, + selectedNode: chain.selectedNode + ) + } + .sorted { + $0.sortKey.lexicographicallyPrecedes($1.sortKey) + } + + let projection = RepositoryProjection( + runtimes: canonicalRuntimes, + chains: canonicalChains + ) + let encoder = JSONEncoder() + encoder.outputFormatting = [.sortedKeys] + let encodedProjection = try encoder.encode(projection) + + let defaultNodeRelationshipCount = canonicalChains.reduce(0) { + $0 + $1.nodes.count + } + let customNodeRelationshipCount = canonicalChains.reduce(0) { + $0 + $1.customNodes.count + } + let selectedNodeRelationshipCount = canonicalChains.reduce(0) { + $0 + ($1.selectedNode == nil ? 0 : 1) + } + let standaloneSelectedNodeCount = canonicalChains.reduce(0) { count, chain in + guard let selectedNode = chain.selectedNode else { + return count + } + + let relatedURLs = Set( + (chain.nodes + chain.customNodes).map(\.url) + ) + return count + (relatedURLs.contains(selectedNode.url) ? 0 : 1) + } + + return RepositoryAggregate( + runtimeCount: canonicalRuntimes.count, + chainCount: canonicalChains.count, + enabledChainCount: canonicalChains.filter { !$0.disabled }.count, + assetCount: assetCount, + nodeCount: nodeCount, + defaultNodeRelationshipCount: defaultNodeRelationshipCount, + customNodeRelationshipCount: customNodeRelationshipCount, + selectedNodeRelationshipCount: selectedNodeRelationshipCount, + standaloneSelectedNodeCount: standaloneSelectedNodeCount, + fingerprint: Data(SHA256.hash(data: encodedProjection)) + ) + } + + private func runtimeProjection( + _ runtime: fearless.RuntimeMetadataItem + ) -> RuntimeProjection { + RuntimeProjection( + chain: runtime.chain, + version: runtime.version, + txVersion: runtime.txVersion, + metadataDigest: Data(SHA256.hash(data: runtime.metadata)) + ) + } + + private func runtimeProjection( + _ runtime: fearless.CDRuntimeMetadataItem + ) -> RuntimeProjection { + RuntimeProjection( + chain: runtime.identifier ?? "", + version: UInt32(bitPattern: runtime.version), + txVersion: UInt32(bitPattern: runtime.txVersion), + metadataDigest: Data(SHA256.hash(data: runtime.metadata ?? Data())) + ) + } + + private func chainProjection(_ chain: ChainModel) -> ChainProjection { + ChainProjection( + chainId: chain.chainId, + name: chain.name, + disabled: chain.disabled, + assetIds: chain.assets.map(\.id).sorted(), + nodes: chain.nodes.map(nodeProjection).sorted { + $0.sortKey.lexicographicallyPrecedes($1.sortKey) + }, + customNodes: (chain.customNodes ?? []).map(nodeProjection).sorted { + $0.sortKey.lexicographicallyPrecedes($1.sortKey) + }, + selectedNode: chain.selectedNode.map(nodeProjection) + ) + } + + private func chainProjection(_ chain: fearless.CDChain) -> ChainProjection { + ChainProjection( + chainId: chain.chainId ?? "", + name: chain.name ?? "", + disabled: chain.disabled, + assetIds: chain.assets?.compactMap { + ($0 as? fearless.CDAsset)?.id + }.sorted() ?? [], + nodes: chain.nodes?.compactMap { + ($0 as? fearless.CDChainNode).map(nodeProjection) + }.sorted { + $0.sortKey.lexicographicallyPrecedes($1.sortKey) + } ?? [], + customNodes: chain.customNodes?.compactMap { + ($0 as? fearless.CDChainNode).map(nodeProjection) + }.sorted { + $0.sortKey.lexicographicallyPrecedes($1.sortKey) + } ?? [], + selectedNode: chain.selectedNode.map(nodeProjection) + ) + } + + private func nodeProjection(_ node: ChainNodeModel) -> NodeProjection { + NodeProjection( + url: node.url.absoluteString, + name: node.name, + apiQueryName: node.apikey?.queryName, + apiKeyName: node.apikey?.keyName + ) + } + + private func nodeProjection(_ node: fearless.CDChainNode) -> NodeProjection { + NodeProjection( + url: node.url?.absoluteString ?? "", + name: node.name ?? "", + apiQueryName: node.apiQueryName, + apiKeyName: node.apiKeyName + ) + } + + private func allNodes(in chain: ChainModel) -> [ChainNodeModel] { + Array(chain.nodes) + + Array(chain.customNodes ?? []) + + [chain.selectedNode].compactMap { $0 } + } + + private func loadBundledV8Model() throws -> NSManagedObjectModel { + let appBundle = Bundle(for: SubstrateDataStorageFacade.self) + let modelURL = ["omo", "mom"].lazy.compactMap { + appBundle.url( + forResource: SubstrateStorageVersion.version8.rawValue, + withExtension: $0, + subdirectory: SubstrateStorageParams.modelDirectory + ) + }.first + + return try XCTUnwrap( + modelURL.flatMap(NSManagedObjectModel.init(contentsOf:)), + "Unable to load bundled SubstrateDataModel_v8" + ) + } + + private func makeInMemoryContext() throws -> NSManagedObjectContext { + let coordinator = NSPersistentStoreCoordinator(managedObjectModel: try loadBundledV8Model()) + try coordinator.addPersistentStore( + ofType: NSInMemoryStoreType, + configurationName: nil, + at: nil + ) + + let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) + context.persistentStoreCoordinator = coordinator + return context + } + + private func roundTrip( + _ model: Model, + entityName: String, + entityClass _: Entity.Type, + in context: NSManagedObjectContext + ) throws -> Model where + Model: Codable & Identifiable, + Entity: NSManagedObject & CoreDataCodable + { + let mapper = CodableCoreDataMapper() + var transformedModel: Model? + + try performAndWait(in: context) { + let insertedObject = try XCTUnwrap( + NSEntityDescription.insertNewObject( + forEntityName: entityName, + into: context + ) as? Entity + ) + try mapper.populate(entity: insertedObject, from: model, using: context) + try context.save() + context.reset() + + let request = NSFetchRequest(entityName: entityName) + let persistedObject = try XCTUnwrap(context.fetch(request).last) + self.assertExactClass( + of: persistedObject, + is: Entity.self, + entityName: entityName + ) + transformedModel = try mapper.transform(entity: persistedObject) + } + + return try XCTUnwrap(transformedModel) + } + + private func assertMalformedRowRejected( + _: Model.Type, + entityName: String, + entityClass _: Entity.Type, + in context: NSManagedObjectContext, + file: StaticString = #filePath, + line: UInt = #line + ) where + Model: Codable & Identifiable, + Entity: NSManagedObject & CoreDataCodable + { + guard let malformedEntity = NSEntityDescription.insertNewObject( + forEntityName: entityName, + into: context + ) as? Entity else { + XCTFail("\(entityName) did not materialize as its expected class", file: file, line: line) + return + } + + let mapper = CodableCoreDataMapper() + XCTAssertThrowsError( + try mapper.transform(entity: malformedEntity), + "\(entityName) accepted a row with missing required values", + file: file, + line: line + ) + } + + private func performAndWait( + in context: NSManagedObjectContext, + body: () throws -> Void + ) throws { + var bodyError: Error? + context.performAndWait { + do { + try body() + } catch { + bodyError = error + } + } + + if let bodyError { + throw bodyError + } + } + + private func assertExactClass( + of object: NSManagedObject, + is expectedClass: NSManagedObject.Type, + entityName: String, + file: StaticString = #filePath, + line: UInt = #line + ) { + XCTAssertEqual( + ObjectIdentifier(type(of: object)), + ObjectIdentifier(expectedClass), + "\(entityName) materialized as \(NSStringFromClass(type(of: object)))", + file: file, + line: line + ) + } + + private func versionHashes(for model: NSManagedObjectModel) -> [String: String] { + model.entityVersionHashesByName.mapValues { $0.base64EncodedString() } + } + + private func copiedPhoneStoreFixtureURL() -> URL { + if let configuredPath = ProcessInfo.processInfo.environment[ + "FEARLESS_SUBSTRATE_PHONE_STORE_FIXTURE" + ], !configuredPath.isEmpty { + return URL(fileURLWithPath: configuredPath) + } + + return URL( + fileURLWithPath: + "/private/tmp/fearless-device-coredata-20260723/SubstrateDataModel.sqlite" + ) + } + + private func copyStoreFixtureToTemporaryDirectory(_ fixtureURL: URL) throws -> URL { + let directoryURL = FileManager.default.temporaryDirectory + .appendingPathComponent("SubstrateStoragePhoneFixture") + .appendingPathComponent(UUID().uuidString) + try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true) + + let destinationURL = directoryURL.appendingPathComponent(fixtureURL.lastPathComponent) + for suffix in ["", "-wal", "-shm"] { + let sourceURL = URL(fileURLWithPath: fixtureURL.path + suffix) + guard FileManager.default.fileExists(atPath: sourceURL.path) else { + continue + } + + let copiedURL = URL(fileURLWithPath: destinationURL.path + suffix) + try FileManager.default.copyItem(at: sourceURL, to: copiedURL) + } + + return destinationURL + } + + private func inspectReadOnlyStore( + at storeURL: URL, + model: NSManagedObjectModel, + fetchEveryObject: Bool + ) throws -> [String: Int] { + let coordinator = NSPersistentStoreCoordinator(managedObjectModel: model) + let store = try coordinator.addPersistentStore( + ofType: NSSQLiteStoreType, + configurationName: nil, + at: storeURL, + options: [ + NSReadOnlyPersistentStoreOption: true, + NSMigratePersistentStoresAutomaticallyOption: false, + NSInferMappingModelAutomaticallyOption: false + ] + ) + defer { + try? coordinator.remove(store) + } + + let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) + context.persistentStoreCoordinator = coordinator + var counts: [String: Int] = [:] + + try performAndWait(in: context) { + for expectation in self.expectedEntityClasses { + let request = NSFetchRequest(entityName: expectation.entityName) + let count = try context.count(for: request) + counts[expectation.entityName] = count + + guard fetchEveryObject, count > 0 else { + continue + } + + request.fetchBatchSize = 512 + request.returnsObjectsAsFaults = true + let objects = try context.fetch(request) + XCTAssertEqual(objects.count, count) + + for object in objects { + self.assertExactClass( + of: object, + is: expectation.managedObjectClass, + entityName: expectation.entityName + ) + } + + context.reset() + } + } + + return counts + } + + private func metadataVersionHashes(_ metadata: [String: Any]) -> [String: Data]? { + metadata[NSStoreModelVersionHashesKey] as? [String: Data] + } + + private func storeFingerprints(at storeURL: URL) throws -> [String: Data] { + var fingerprints: [String: Data] = [:] + + for suffix in ["", "-wal", "-shm"] { + let fileURL = URL(fileURLWithPath: storeURL.path + suffix) + guard FileManager.default.fileExists(atPath: fileURL.path) else { + continue + } + + fingerprints[suffix] = try digest(of: fileURL) + } + + return fingerprints + } + + private func digest(of fileURL: URL) throws -> Data { + Data(SHA256.hash(data: try Data(contentsOf: fileURL, options: .mappedIfSafe))) + } +} diff --git a/fearlessTests/Common/Migration/SubstrateStorageMigrationSafetyTests.swift b/fearlessTests/Common/Migration/SubstrateStorageMigrationSafetyTests.swift new file mode 100644 index 0000000000..2fd0a660c8 --- /dev/null +++ b/fearlessTests/Common/Migration/SubstrateStorageMigrationSafetyTests.swift @@ -0,0 +1,7174 @@ +import CoreData +import SQLite3 +import XCTest +@testable import fearless + +final class SubstrateStorageMigrationSafetyTests: XCTestCase { + private var testDirectory: URL! + + private var storeURL: URL { + testDirectory.appendingPathComponent("SubstrateDataModel.sqlite") + } + + private var appBundle: Bundle { + Bundle(for: SubstrateDataStorageFacade.self) + } + + override func setUpWithError() throws { + try super.setUpWithError() + + testDirectory = FileManager.default.temporaryDirectory + .appendingPathComponent("SubstrateStorageMigrationSafetyTests") + .appendingPathComponent(UUID().uuidString) + try FileManager.default.createDirectory( + at: testDirectory, + withIntermediateDirectories: true + ) + } + + override func tearDownWithError() throws { + if let testDirectory { + try? FileManager.default.removeItem(at: testDirectory) + } + + try super.tearDownWithError() + } + + func testPerformMigration_whenStoreDoesNotExist_thenDoesNotCreateOrModifyAnything() throws { + let migrator = makeMigrator() + + XCTAssertFalse(migrator.requiresMigration()) + try migrator.performMigration() + + XCTAssertFalse(FileManager.default.fileExists(atPath: storeURL.path)) + XCTAssertTrue(try FileManager.default.contentsOfDirectory(atPath: testDirectory.path).isEmpty) + } + + func testPerformMigration_whenStoreIsCurrent_thenRepeatedCallsAreByteForByteIdempotent() throws { + let currentModel = try model(for: .version8) + try createStore(at: storeURL, model: currentModel) { context in + try self.insertRuntimeItem(identifier: "current-runtime", in: context, model: currentModel) + } + let before = try durableStoreFamilySnapshot(at: storeURL) + let migrator = makeMigrator() + + XCTAssertFalse(migrator.requiresMigration()) + try migrator.performMigration() + try migrator.performMigration() + + XCTAssertEqual(try durableStoreFamilySnapshot(at: storeURL), before) + XCTAssertEqual( + try count(entityName: "CDRuntimeMetadataItem", at: storeURL, model: currentModel), + 1 + ) + } + + func testPerformMigration_whenCurrentStoreHasOversizedRawArchive_thenRepairsPrivateCopyAtomicallyAndPreservesProtectedGraph() throws { + try createCurrentTransformableAndProtectedGraph() + try executeSQLite( + """ + UPDATE ZCDASSET + SET ZPURCHASEPROVIDERS = zeroblob(1025) + """, + at: storeURL + ) + let damaged = try durableStoreFamilySnapshot(at: storeURL) + let migrator = makeMigrator( + protectedDataInspectionLimits: + makeProtectedDataLimits( + maximumTransformableArchiveByteCount: 1024 + ) + ) + + XCTAssertTrue(migrator.requiresMigration()) + XCTAssertEqual( + try durableStoreFamilySnapshot(at: storeURL), + damaged, + "Detection must never mutate the installed current store" + ) + + try migrator.performMigration() + + XCTAssertFalse(migrator.requiresMigration()) + try assertCurrentTransformableAndProtectedGraph( + expectedOptions: ["safe-option"], + expectedPurchaseProviders: nil + ) + let repaired = try durableStoreFamilySnapshot(at: storeURL) + XCTAssertNotEqual(repaired, damaged) + + try migrator.performMigration() + + XCTAssertEqual( + try durableStoreFamilySnapshot(at: storeURL), + repaired, + "A repaired current store must become a byte-for-byte no-op" + ) + } + + func testPerformMigration_whenCurrentStoreHasDecodedWrongTypeArchive_thenRepairsPrivateCopyAndPreservesOtherValues() throws { + try createCurrentTransformableAndProtectedGraph() + let wrongTypeArchive = try NSKeyedArchiver.archivedData( + withRootObject: NSString(string: "not-an-array"), + requiringSecureCoding: true + ) + try executeSQLite( + """ + UPDATE ZCDCHAIN + SET ZOPTIONS = X'\(hex(wrongTypeArchive))' + """, + at: storeURL + ) + let damaged = try durableStoreFamilySnapshot(at: storeURL) + let migrator = makeMigrator() + + XCTAssertTrue(migrator.requiresMigration()) + XCTAssertEqual( + try durableStoreFamilySnapshot(at: storeURL), + damaged + ) + + try migrator.performMigration() + + XCTAssertFalse(migrator.requiresMigration()) + try assertCurrentTransformableAndProtectedGraph( + expectedOptions: [], + expectedPurchaseProviders: ["safe-provider"] + ) + } + + func testPerformMigration_whenCleanCurrentStartupGraphIsExactlyAtEveryRelationshipLimit_thenAuditsWithoutHookReplacementOrMutation() throws { + try createCurrentStartupRelationshipGraph() + let before = try durableStoreFamilySnapshot(at: storeURL) + var stagedHookCalled = false + var replacementCalled = false + var countedRelationships = + [(String, String, Bool, Int)]() + var materializedRelationships = [(String, String)]() + let migrator = makeMigrator( + storeReplacer: { _, _ in + replacementCalled = true + }, + stagedStoreMutationHook: { _, _ in + stagedHookCalled = true + }, + protectedDataInspectionLimits: + makeProtectedDataLimits( + maximumRelationshipMembers: 1, + maximumTotalRelationshipMembers: 8 + ), + protectedRelationshipCountDidFetch: { + countedRelationships.append( + ( + $0.entity.name ?? "unknown", + $1, + $0.hasFault(forRelationshipNamed: $1), + $2 + ) + ) + }, + protectedRelationshipWillMaterialize: { + entityName, + relationshipName, + _ in + materializedRelationships.append( + (entityName, relationshipName) + ) + } + ) + + XCTAssertFalse(migrator.requiresMigration()) + XCTAssertEqual( + try durableStoreFamilySnapshot(at: storeURL), + before + ) + + try migrator.performMigration() + + XCTAssertFalse(stagedHookCalled) + XCTAssertFalse(replacementCalled) + XCTAssertEqual( + try durableStoreFamilySnapshot(at: storeURL), + before + ) + let expectedRelationships: Set = [ + "CDAsset.priceData", + "CDChain.assets", + "CDChain.customNodes", + "CDChain.explorers", + "CDChain.nodes", + "CDChainXcmConfig.availableAssets", + "CDChainXcmConfig.availableDestinations", + "CDXcmAvailableDestination.assets" + ] + let auditedRelationships = Set( + countedRelationships.map { "\($0.0).\($0.1)" } + ) + XCTAssertTrue( + expectedRelationships.isSubset( + of: auditedRelationships + ) + ) + XCTAssertTrue( + countedRelationships.allSatisfy { + $0.2 && $0.3 == 1 + }, + "Every SQL count must remain faulted at the exact boundary" + ) + XCTAssertEqual( + Set( + materializedRelationships.map { + "\($0.0).\($0.1)" + } + ), + ["CDChain.customNodes", "CDChain.nodes"], + "Only protected node relationships may materialize after the complete SQL audit" + ) + } + + func testPerformMigration_whenCleanCurrentStartupRootsAreExactlyAtRowLimits_thenAuditsWithoutHookReplacementOrMutation() throws { + let currentModel = try model(for: .version8) + try createStore(at: storeURL, model: currentModel) { + context in + try self.insertRuntimeItem( + identifier: "bounded-runtime", + in: context, + model: currentModel + ) + _ = try self.insertChainWithDefaultNode( + in: context, + model: currentModel + ) + } + let before = try durableStoreFamilySnapshot(at: storeURL) + var stagedHookCalled = false + var replacementCalled = false + let migrator = makeMigrator( + storeReplacer: { _, _ in + replacementCalled = true + }, + stagedStoreMutationHook: { _, _ in + stagedHookCalled = true + }, + protectedDataInspectionLimits: + makeProtectedDataLimits( + maximumRowsPerEntity: 1, + maximumTotalRows: 2, + maximumStartupRootRowsPerEntity: 1, + maximumRelationshipMembers: 1, + maximumTotalRelationshipMembers: 1 + ) + ) + + XCTAssertFalse(migrator.requiresMigration()) + try migrator.performMigration() + + XCTAssertFalse(stagedHookCalled) + XCTAssertFalse(replacementCalled) + XCTAssertEqual( + try durableStoreFamilySnapshot(at: storeURL), + before + ) + } + + func testPerformMigration_whenCleanCurrentRuntimeRootExceedsRowLimit_thenRejectsBeforeHookReplacementOrMutation() throws { + let currentModel = try model(for: .version8) + try createStore(at: storeURL, model: currentModel) { + context in + for index in 0 ..< 2 { + try self.insertRuntimeItem( + identifier: "runtime-\(index)", + in: context, + model: currentModel + ) + } + } + let before = try durableStoreFamilySnapshot(at: storeURL) + var stagedHookCalled = false + var replacementCalled = false + var materializedRelationships = [(String, String, Int)]() + let migrator = makeMigrator( + storeReplacer: { _, _ in + replacementCalled = true + }, + stagedStoreMutationHook: { _, _ in + stagedHookCalled = true + }, + protectedDataInspectionLimits: + makeProtectedDataLimits( + maximumStartupRootRowsPerEntity: 1 + ), + protectedRelationshipWillMaterialize: { + materializedRelationships.append(($0, $1, $2)) + } + ) + + XCTAssertTrue(migrator.requiresMigration()) + XCTAssertEqual( + try durableStoreFamilySnapshot(at: storeURL), + before + ) + XCTAssertThrowsError(try migrator.performMigration()) { + error in + guard + let migrationError = + error as? SubstrateStorageMigrationError, + case let .sourceStoreInspectionFailed( + _, + underlyingError + ) = migrationError, + case let .entityRowLimitExceeded( + entityName, + actual, + maximum + ) = + underlyingError as? + SubstrateProtectedDataInspectionError + else { + return XCTFail( + "Expected current root row limit, got \(error)" + ) + } + + XCTAssertEqual(entityName, "CDRuntimeMetadataItem") + XCTAssertEqual(actual, 2) + XCTAssertEqual(maximum, 1) + } + XCTAssertFalse(stagedHookCalled) + XCTAssertFalse(replacementCalled) + XCTAssertTrue(materializedRelationships.isEmpty) + XCTAssertEqual( + try durableStoreFamilySnapshot(at: storeURL), + before + ) + } + + func testPerformMigration_whenCleanCurrentRootPayloadIsExactlyAtByteLimit_thenAuditsWithoutHookReplacementOrMutation() throws { + let currentModel = try model(for: .version8) + try createStore(at: storeURL, model: currentModel) { + context in + let runtime = try self.insertRuntimeItem( + identifier: "root", + in: context, + model: currentModel + ) + runtime.setValue(Data([1, 2, 3]), forKey: "metadata") + runtime.setValue(Data([4, 5]), forKey: "resolver") + } + let before = try durableStoreFamilySnapshot(at: storeURL) + var stagedHookCalled = false + var replacementCalled = false + let migrator = makeMigrator( + storeReplacer: { _, _ in + replacementCalled = true + }, + stagedStoreMutationHook: { _, _ in + stagedHookCalled = true + }, + protectedDataInspectionLimits: + makeProtectedDataLimits( + maximumStartupRootPayloadByteCount: 11 + ) + ) + + XCTAssertFalse(migrator.requiresMigration()) + try migrator.performMigration() + + XCTAssertFalse(stagedHookCalled) + XCTAssertFalse(replacementCalled) + XCTAssertEqual( + try durableStoreFamilySnapshot(at: storeURL), + before + ) + } + + func testPerformMigration_whenCleanCurrentRootPayloadExceedsByteLimit_thenRejectsBeforeHookReplacementOrMutation() throws { + let currentModel = try model(for: .version8) + try createStore(at: storeURL, model: currentModel) { + context in + let runtime = try self.insertRuntimeItem( + identifier: "root", + in: context, + model: currentModel + ) + runtime.setValue(Data([1, 2, 3]), forKey: "metadata") + runtime.setValue(Data([4, 5]), forKey: "resolver") + } + let before = try durableStoreFamilySnapshot(at: storeURL) + var stagedHookCalled = false + var replacementCalled = false + let migrator = makeMigrator( + storeReplacer: { _, _ in + replacementCalled = true + }, + stagedStoreMutationHook: { _, _ in + stagedHookCalled = true + }, + protectedDataInspectionLimits: + makeProtectedDataLimits( + maximumStartupRootPayloadByteCount: 10 + ) + ) + + XCTAssertTrue(migrator.requiresMigration()) + XCTAssertThrowsError(try migrator.performMigration()) { + error in + guard + let migrationError = + error as? SubstrateStorageMigrationError, + case let .sourceStoreInspectionFailed( + _, + underlyingError + ) = migrationError, + case let .startupRootPayloadByteLimitExceeded( + actual, + maximum + ) = + underlyingError as? + SubstrateProtectedDataInspectionError + else { + return XCTFail( + "Expected startup root payload limit, got \(error)" + ) + } + + XCTAssertEqual(actual, 11) + XCTAssertEqual(maximum, 10) + } + XCTAssertFalse(stagedHookCalled) + XCTAssertFalse(replacementCalled) + XCTAssertEqual( + try durableStoreFamilySnapshot(at: storeURL), + before + ) + } + + func testPerformMigration_whenCurrentPriceStringIsExactlyAtStartupChildValueLimit_thenAuditsWithoutMutation() throws { + let currentModel = try model(for: .version8) + try createStore(at: storeURL, model: currentModel) { + context in + try self.insertStartupPayloadEntity( + entityName: "CDPriceData", + attributeName: "price", + payload: String(repeating: "7", count: 1_024), + in: context, + model: currentModel + ) + } + let before = try durableStoreFamilySnapshot(at: storeURL) + var stagedHookCalled = false + var replacementCalled = false + let migrator = makeMigrator( + storeReplacer: { _, _ in + replacementCalled = true + }, + stagedStoreMutationHook: { _, _ in + stagedHookCalled = true + }, + protectedDataInspectionLimits: + makeProtectedDataLimits( + maximumStartupChildValueByteCount: 1_024 + ) + ) + + XCTAssertFalse(migrator.requiresMigration()) + try migrator.performMigration() + + XCTAssertFalse(stagedHookCalled) + XCTAssertFalse(replacementCalled) + XCTAssertEqual( + try durableStoreFamilySnapshot(at: storeURL), + before + ) + } + + func testPerformMigration_whenCurrentPriceStringExceedsStartupChildValueLimit_thenRejectsBeforeCoreDataOrMutation() throws { + try assertStartupChildValueLimitExceeded( + entityName: "CDPriceData", + attributeName: "price" + ) + } + + func testPerformMigration_whenCurrentAssetNameExceedsStartupChildValueLimit_thenRejectsBeforeCoreDataOrMutation() throws { + try assertStartupChildValueLimitExceeded( + entityName: "CDAsset", + attributeName: "name" + ) + } + + func testPerformMigration_whenCurrentCustomNodeNameExceedsStartupChildValueLimit_thenRejectsBeforeCoreDataOrMutation() throws { + try assertStartupChildValueLimitExceeded( + entityName: "CDChainNode", + attributeName: "name", + attachNodeAsCustomNode: true + ) + } + + func testPerformMigration_whenCurrentExplorerURLExceedsStartupChildValueLimit_thenRejectsBeforeCoreDataOrMutation() throws { + try assertStartupChildValueLimitExceeded( + entityName: "CDExternalApi", + attributeName: "url" + ) + } + + func testPerformMigration_whenCurrentXcmConfigVersionExceedsStartupChildValueLimit_thenRejectsBeforeCoreDataOrMutation() throws { + try assertStartupChildValueLimitExceeded( + entityName: "CDChainXcmConfig", + attributeName: "xcmVersion" + ) + } + + func testPerformMigration_whenCurrentXcmDestinationChainIdExceedsStartupChildValueLimit_thenRejectsBeforeCoreDataOrMutation() throws { + try assertStartupChildValueLimitExceeded( + entityName: "CDXcmAvailableDestination", + attributeName: "chainId" + ) + } + + func testPerformMigration_whenCurrentXcmAssetIdentifierExceedsStartupChildValueLimit_thenRejectsBeforeCoreDataOrMutation() throws { + try assertStartupChildValueLimitExceeded( + entityName: "CDXcmAvailableAsset", + attributeName: "id" + ) + } + + func testPerformMigration_whenCurrentPriceProviderIdentifierExceedsStartupChildValueLimit_thenRejectsBeforeCoreDataOrMutation() throws { + try assertStartupChildValueLimitExceeded( + entityName: "CDPriceProvider", + attributeName: "id" + ) + } + + func testPerformMigration_whenCurrentChildPayloadAggregateIsExactlyAtLimit_thenAuditsWithoutMutation() throws { + let currentModel = try model(for: .version8) + try createStore(at: storeURL, model: currentModel) { + context in + try self.insertStartupPayloadEntity( + entityName: "CDPriceData", + attributeName: "price", + payload: "1234", + in: context, + model: currentModel + ) + } + let before = try durableStoreFamilySnapshot(at: storeURL) + var stagedHookCalled = false + var replacementCalled = false + let migrator = makeMigrator( + storeReplacer: { _, _ in + replacementCalled = true + }, + stagedStoreMutationHook: { _, _ in + stagedHookCalled = true + }, + protectedDataInspectionLimits: + makeProtectedDataLimits( + maximumStartupChildValueByteCount: 4, + maximumStartupChildPayloadByteCount: 6 + ) + ) + + XCTAssertFalse(migrator.requiresMigration()) + try migrator.performMigration() + + XCTAssertFalse(stagedHookCalled) + XCTAssertFalse(replacementCalled) + XCTAssertEqual( + try durableStoreFamilySnapshot(at: storeURL), + before + ) + } + + func testPerformMigration_whenCurrentChildPayloadAggregateExceedsLimitByOne_thenRejectsBeforeCoreDataOrMutation() throws { + let currentModel = try model(for: .version8) + try createStore(at: storeURL, model: currentModel) { + context in + try self.insertStartupPayloadEntity( + entityName: "CDPriceData", + attributeName: "price", + payload: "1234", + in: context, + model: currentModel + ) + } + let before = try durableStoreFamilySnapshot(at: storeURL) + var stagedHookCalled = false + var replacementCalled = false + let migrator = makeMigrator( + storeReplacer: { _, _ in + replacementCalled = true + }, + stagedStoreMutationHook: { _, _ in + stagedHookCalled = true + }, + protectedDataInspectionLimits: + makeProtectedDataLimits( + maximumStartupChildValueByteCount: 4, + maximumStartupChildPayloadByteCount: 5 + ) + ) + + XCTAssertTrue(migrator.requiresMigration()) + XCTAssertThrowsError(try migrator.performMigration()) { + error in + guard + let migrationError = + error as? SubstrateStorageMigrationError, + case let .sourceStoreInspectionFailed( + failedURL, + underlyingError + ) = migrationError, + case let .startupChildPayloadByteLimitExceeded( + actual, + maximum + ) = + underlyingError as? + SubstrateProtectedDataInspectionError + else { + return XCTFail( + "Expected startup child aggregate limit, got \(error)" + ) + } + + XCTAssertEqual(failedURL, self.storeURL) + XCTAssertEqual(actual, 6) + XCTAssertEqual(maximum, 5) + } + XCTAssertFalse(stagedHookCalled) + XCTAssertFalse(replacementCalled) + XCTAssertEqual( + try durableStoreFamilySnapshot(at: storeURL), + before + ) + } + + func testPerformMigration_whenCleanCurrentNodesExceedRelationshipLimit_thenRejectsWhileFaultedBeforeHookReplacementOrMutation() throws { + let currentModel = try model(for: .version8) + try createStore(at: storeURL, model: currentModel) { + context in + try self.insertLegacyChainWithNodes( + in: context, + model: currentModel + ) + } + let before = try durableStoreFamilySnapshot(at: storeURL) + var stagedHookCalled = false + var replacementCalled = false + var nodeCounts = [(Bool, Int)]() + var materializedRelationships = [(String, String, Int)]() + let migrator = makeMigrator( + storeReplacer: { _, _ in + replacementCalled = true + }, + stagedStoreMutationHook: { _, _ in + stagedHookCalled = true + }, + protectedDataInspectionLimits: + makeProtectedDataLimits( + maximumRelationshipMembers: 1 + ), + protectedRelationshipCountDidFetch: { + guard $1 == "nodes" else { + return + } + nodeCounts.append( + ( + $0.hasFault(forRelationshipNamed: $1), + $2 + ) + ) + }, + protectedRelationshipWillMaterialize: { + materializedRelationships.append(($0, $1, $2)) + } + ) + + XCTAssertTrue(migrator.requiresMigration()) + XCTAssertThrowsError(try migrator.performMigration()) { + error in + guard + let migrationError = + error as? SubstrateStorageMigrationError, + case let .sourceStoreInspectionFailed( + _, + underlyingError + ) = migrationError, + case let .relationshipLimitExceeded( + entityName, + relationshipName, + actual, + maximum + ) = + underlyingError as? + SubstrateProtectedDataInspectionError + else { + return XCTFail( + "Expected current nodes limit, got \(error)" + ) + } + + XCTAssertEqual(entityName, "CDChain") + XCTAssertEqual(relationshipName, "nodes") + XCTAssertEqual(actual, 2) + XCTAssertEqual(maximum, 1) + } + XCTAssertEqual(nodeCounts.map(\.1), [2, 2]) + XCTAssertTrue(nodeCounts.allSatisfy(\.0)) + XCTAssertTrue(materializedRelationships.isEmpty) + XCTAssertFalse(stagedHookCalled) + XCTAssertFalse(replacementCalled) + XCTAssertEqual( + try durableStoreFamilySnapshot(at: storeURL), + before + ) + } + + func testPerformMigration_whenCleanCurrentPriceDataExceedsRelationshipLimit_thenRejectsWhileFaultedBeforeHookReplacementOrMutation() throws { + let currentModel = try model(for: .version8) + try createStore(at: storeURL, model: currentModel) { + context in + let asset = try self.insertMigrationAsset( + identifier: "bounded-price-asset", + in: context, + model: currentModel + ) + try self.insertPriceData( + count: 2, + for: asset, + in: context, + model: currentModel + ) + } + let before = try durableStoreFamilySnapshot(at: storeURL) + var stagedHookCalled = false + var replacementCalled = false + var priceDataCounts = [(Bool, Int)]() + var materializedRelationships = [(String, String, Int)]() + let migrator = makeMigrator( + storeReplacer: { _, _ in + replacementCalled = true + }, + stagedStoreMutationHook: { _, _ in + stagedHookCalled = true + }, + protectedDataInspectionLimits: + makeProtectedDataLimits( + maximumRelationshipMembers: 1 + ), + protectedRelationshipCountDidFetch: { + guard $1 == "priceData" else { + return + } + priceDataCounts.append( + ( + $0.hasFault(forRelationshipNamed: $1), + $2 + ) + ) + }, + protectedRelationshipWillMaterialize: { + materializedRelationships.append(($0, $1, $2)) + } + ) + + XCTAssertTrue(migrator.requiresMigration()) + XCTAssertThrowsError(try migrator.performMigration()) { + error in + guard + let migrationError = + error as? SubstrateStorageMigrationError, + case let .sourceStoreInspectionFailed( + _, + underlyingError + ) = migrationError, + case let .relationshipLimitExceeded( + entityName, + relationshipName, + actual, + maximum + ) = + underlyingError as? + SubstrateProtectedDataInspectionError + else { + return XCTFail( + "Expected current priceData limit, got \(error)" + ) + } + + XCTAssertEqual(entityName, "CDAsset") + XCTAssertEqual(relationshipName, "priceData") + XCTAssertEqual(actual, 2) + XCTAssertEqual(maximum, 1) + } + XCTAssertEqual(priceDataCounts.map(\.1), [2, 2]) + XCTAssertTrue(priceDataCounts.allSatisfy(\.0)) + XCTAssertTrue(materializedRelationships.isEmpty) + XCTAssertFalse(stagedHookCalled) + XCTAssertFalse(replacementCalled) + XCTAssertEqual( + try durableStoreFamilySnapshot(at: storeURL), + before + ) + } + + func testPerformMigration_whenCleanCurrentXcmRelationshipsExceedAggregateLimit_thenRejectsBeforeHookReplacementOrMutation() throws { + let currentModel = try model(for: .version8) + try createStore(at: storeURL, model: currentModel) { + context in + let graph = try self.insertChainWithDefaultNode( + in: context, + model: currentModel + ) + try self.insertXcmRelationshipGraph( + for: graph.chain, + in: context, + model: currentModel + ) + } + let before = try durableStoreFamilySnapshot(at: storeURL) + var stagedHookCalled = false + var replacementCalled = false + var xcmCounts = [(String, Bool, Int)]() + var materializedRelationships = [(String, String, Int)]() + let migrator = makeMigrator( + storeReplacer: { _, _ in + replacementCalled = true + }, + stagedStoreMutationHook: { _, _ in + stagedHookCalled = true + }, + protectedDataInspectionLimits: + makeProtectedDataLimits( + maximumRelationshipMembers: 1, + maximumTotalRelationshipMembers: 2 + ), + protectedRelationshipCountDidFetch: { + guard + $0.entity.name == "CDChainXcmConfig" + else { + return + } + xcmCounts.append( + ( + $1, + $0.hasFault(forRelationshipNamed: $1), + $2 + ) + ) + }, + protectedRelationshipWillMaterialize: { + materializedRelationships.append(($0, $1, $2)) + } + ) + + XCTAssertTrue(migrator.requiresMigration()) + XCTAssertThrowsError(try migrator.performMigration()) { + error in + guard + let migrationError = + error as? SubstrateStorageMigrationError, + case let .sourceStoreInspectionFailed( + _, + underlyingError + ) = migrationError, + case let .totalRelationshipLimitExceeded( + actual, + maximum + ) = + underlyingError as? + SubstrateProtectedDataInspectionError + else { + return XCTFail( + "Expected current XCM aggregate limit, got \(error)" + ) + } + + XCTAssertEqual(actual, 3) + XCTAssertEqual(maximum, 2) + } + XCTAssertEqual( + xcmCounts.map(\.0), + [ + "availableAssets", + "availableDestinations", + "availableAssets", + "availableDestinations" + ] + ) + XCTAssertTrue( + xcmCounts.allSatisfy { + $0.1 && $0.2 == 1 + } + ) + XCTAssertTrue(materializedRelationships.isEmpty) + XCTAssertFalse(stagedHookCalled) + XCTAssertFalse(replacementCalled) + XCTAssertEqual( + try durableStoreFamilySnapshot(at: storeURL), + before + ) + } + + func testPerformMigration_whenRepairedCurrentStageIntroducesOversizedPriceDataRelationship_thenRejectsBeforeReplacement() throws { + try createCurrentTransformableAndProtectedGraph() + let wrongTypeArchive = try NSKeyedArchiver.archivedData( + withRootObject: NSString(string: "repair-me"), + requiringSecureCoding: true + ) + try executeSQLite( + """ + UPDATE ZCDCHAIN + SET ZOPTIONS = X'\(hex(wrongTypeArchive))' + """, + at: storeURL + ) + let before = try durableStoreFamilySnapshot(at: storeURL) + var stagedHookCalled = false + var replacementCalled = false + let migrator = makeMigrator( + storeReplacer: { _, _ in + replacementCalled = true + }, + stagedStoreMutationHook: { stagedURL, targetModel in + stagedHookCalled = true + try self.addPriceDataToSingleAsset( + count: 2, + at: stagedURL, + model: targetModel + ) + }, + protectedDataInspectionLimits: + makeProtectedDataLimits( + maximumRelationshipMembers: 1 + ) + ) + + XCTAssertThrowsError(try migrator.performMigration()) { + error in + guard + let migrationError = + error as? SubstrateStorageMigrationError, + case let .stagedStoreInspectionFailed( + _, + underlyingError + ) = migrationError, + case let .relationshipLimitExceeded( + entityName, + relationshipName, + actual, + maximum + ) = + underlyingError as? + SubstrateProtectedDataInspectionError + else { + return XCTFail( + "Expected staged priceData limit, got \(error)" + ) + } + + XCTAssertEqual(entityName, "CDAsset") + XCTAssertEqual(relationshipName, "priceData") + XCTAssertEqual(actual, 2) + XCTAssertEqual(maximum, 1) + } + XCTAssertTrue(stagedHookCalled) + XCTAssertFalse(replacementCalled) + XCTAssertEqual( + try durableStoreFamilySnapshot(at: storeURL), + before + ) + } + + func testPerformMigration_whenCurrentStoreHasLargeSharedMemorySidecar_thenUsesOneDisposableCopyAndCleansIt() throws { + let currentModel = try model(for: .version8) + try createStore(at: storeURL, model: currentModel) { context in + try self.insertRuntimeItem( + identifier: "current-large-family", + in: context, + model: currentModel + ) + } + let sharedMemoryURL = URL(fileURLWithPath: storeURL.path + "-shm") + try Data(repeating: 0xA5, count: 4 * 1_024 * 1_024).write( + to: sharedMemoryURL, + options: .atomic + ) + let before = try durableStoreFamilySnapshot(at: storeURL) + let fileManager = TemporaryStoreFootprintFileManager() + let migrator = makeMigrator(fileManager: fileManager) + + try migrator.performMigration() + + XCTAssertEqual(try durableStoreFamilySnapshot(at: storeURL), before) + XCTAssertEqual(fileManager.maximumTemporaryMainStoreCount, 1) + XCTAssertFalse(fileManager.copiedSharedMemorySidecar) + XCTAssertTrue(fileManager.temporaryRootsAreClean) + } + + func testPerformMigration_whenSourceFamilyExceedsPrivateCopyLimit_thenRejectsBeforeCapacityInspectionOrMutation() throws { + let sourceModel = try model(for: .version7) + try createStore(at: storeURL, model: sourceModel) { + context in + try self.insertRuntimeItem( + identifier: "oversized-private-copy", + in: context, + model: sourceModel + ) + } + let before = try durableStoreFamilySnapshot(at: storeURL) + var capacityInspectionCount = 0 + var replacementAttempted = false + let fileManager = TemporaryStoreFootprintFileManager() + let migrator = makeMigrator( + fileManager: fileManager, + storeReplacer: { _, _ in + replacementAttempted = true + }, + privateSourceCopyLimits: + SQLiteStoreFamilyCopyLimits( + maximumFamilyByteCount: 1, + minimumFreeStorageReserveByteCount: 0 + ), + privateSourceAvailableCapacityProvider: { _ in + capacityInspectionCount += 1 + return UInt64.max + } + ) + + XCTAssertThrowsError(try migrator.performMigration()) { + error in + guard + let migrationError = + error as? SubstrateStorageMigrationError, + case let .sourceSnapshotFailed( + failedURL, + underlyingError + ) = migrationError, + case let .storeFamilyTooLarge( + actualByteCount, + maximumByteCount + ) = + underlyingError as? + CrashConsistentStoreReplacementError + else { + return XCTFail( + "Expected bounded source-copy rejection, got \(error)" + ) + } + + XCTAssertEqual(failedURL, self.storeURL) + XCTAssertGreaterThan(actualByteCount, 1) + XCTAssertEqual(maximumByteCount, 1) + } + XCTAssertEqual(capacityInspectionCount, 0) + XCTAssertFalse(replacementAttempted) + XCTAssertEqual( + try durableStoreFamilySnapshot(at: storeURL), + before + ) + XCTAssertTrue(fileManager.temporaryRootsAreClean) + } + + func testPerformMigration_whenPrivateCopyCapacityIsInsufficient_thenRejectsBeforeCopyOrMutation() throws { + let sourceModel = try model(for: .version7) + try createStore(at: storeURL, model: sourceModel) { + context in + try self.insertRuntimeItem( + identifier: "capacity-bounded-private-copy", + in: context, + model: sourceModel + ) + } + let before = try durableStoreFamilySnapshot(at: storeURL) + var capacityInspectionCount = 0 + var replacementAttempted = false + let fileManager = TemporaryStoreFootprintFileManager() + let migrator = makeMigrator( + fileManager: fileManager, + storeReplacer: { _, _ in + replacementAttempted = true + }, + privateSourceCopyLimits: + SQLiteStoreFamilyCopyLimits( + maximumFamilyByteCount: UInt64.max, + minimumFreeStorageReserveByteCount: 1 + ), + privateSourceAvailableCapacityProvider: { _ in + capacityInspectionCount += 1 + return 0 + } + ) + + XCTAssertThrowsError(try migrator.performMigration()) { + error in + guard + let migrationError = + error as? SubstrateStorageMigrationError, + case let .sourceSnapshotFailed( + failedURL, + underlyingError + ) = migrationError, + case let .insufficientStorageCapacity( + requiredByteCount, + availableByteCount + ) = + underlyingError as? + CrashConsistentStoreReplacementError + else { + return XCTFail( + "Expected private-copy capacity rejection, got \(error)" + ) + } + + XCTAssertEqual(failedURL, self.storeURL) + XCTAssertGreaterThan(requiredByteCount, 1) + XCTAssertEqual(availableByteCount, 0) + } + XCTAssertEqual(capacityInspectionCount, 1) + XCTAssertFalse(replacementAttempted) + XCTAssertEqual( + try durableStoreFamilySnapshot(at: storeURL), + before + ) + XCTAssertTrue(fileManager.temporaryRootsAreClean) + } + + func testPerformMigration_whenStoreIsCorrupt_thenThrowsAndPreservesEveryStoreFile() throws { + try Data("not-a-core-data-store".utf8).write(to: storeURL, options: .atomic) + let before = try durableStoreFamilySnapshot(at: storeURL) + let migrator = makeMigrator() + + XCTAssertTrue(migrator.requiresMigration()) + XCTAssertThrowsError(try migrator.performMigration()) { error in + guard + let migrationError = error as? SubstrateStorageMigrationError, + case .metadataUnreadable = migrationError + else { + return XCTFail("Expected metadataUnreadable, got \(error)") + } + } + + XCTAssertEqual(try durableStoreFamilySnapshot(at: storeURL), before) + } + + func testSyncMigration_whenStoreIsCorrupt_thenFailsClosedAndPreservesEverySidecar() throws { + let corruptBytes = Data("not-a-core-data-store".utf8) + try corruptBytes.write(to: storeURL, options: .atomic) + try Data("substrate-wal".utf8).write( + to: URL(fileURLWithPath: storeURL.path + "-wal"), + options: .atomic + ) + + let userStoreURL = testDirectory.appendingPathComponent("UserDataModel.sqlite") + let userWALURL = URL(fileURLWithPath: userStoreURL.path + "-wal") + let userSHMURL = URL(fileURLWithPath: userStoreURL.path + "-shm") + let userJournalURL = URL(fileURLWithPath: userStoreURL.path + "-journal") + let userStoreBytes = Data("wallet-store-must-not-move".utf8) + let userWALBytes = Data("wallet-wal-must-not-move".utf8) + let userSHMBytes = Data("wallet-shm-must-not-move".utf8) + let userJournalBytes = Data("wallet-journal-must-not-move".utf8) + try userStoreBytes.write(to: userStoreURL, options: .atomic) + try userWALBytes.write(to: userWALURL, options: .atomic) + try userSHMBytes.write(to: userSHMURL, options: .atomic) + try userJournalBytes.write(to: userJournalURL, options: .atomic) + + let migrator = makeMigrator() + XCTAssertThrowsError(try migrator.migrate()) { error in + guard + let migrationError = error as? SubstrateStorageMigrationError, + case .metadataUnreadable = migrationError + else { + return XCTFail("Expected metadataUnreadable, got \(error)") + } + } + + XCTAssertEqual(try Data(contentsOf: storeURL), corruptBytes) + XCTAssertEqual( + try Data(contentsOf: URL(fileURLWithPath: storeURL.path + "-wal")), + Data("substrate-wal".utf8) + ) + XCTAssertTrue(migrator.requiresMigration()) + XCTAssertEqual(try Data(contentsOf: userStoreURL), userStoreBytes) + XCTAssertEqual(try Data(contentsOf: userWALURL), userWALBytes) + XCTAssertEqual(try Data(contentsOf: userSHMURL), userSHMBytes) + XCTAssertEqual(try Data(contentsOf: userJournalURL), userJournalBytes) + XCTAssertTrue(try recoveryDirectories().isEmpty) + + XCTAssertThrowsError(try migrator.migrate()) + XCTAssertEqual(try Data(contentsOf: storeURL), corruptBytes) + XCTAssertTrue(try recoveryDirectories().isEmpty) + XCTAssertEqual(try Data(contentsOf: userStoreURL), userStoreBytes) + XCTAssertEqual(try Data(contentsOf: userWALURL), userWALBytes) + XCTAssertEqual(try Data(contentsOf: userSHMURL), userSHMBytes) + XCTAssertEqual(try Data(contentsOf: userJournalURL), userJournalBytes) + } + + func testPerformMigration_whenStoreVersionIsUnknown_thenThrowsAndPreservesEveryStoreFile() throws { + try createStore(at: storeURL, model: makeUnknownModel()) { _ in } + let before = try durableStoreFamilySnapshot(at: storeURL) + let migrator = makeMigrator() + + XCTAssertTrue(migrator.requiresMigration()) + XCTAssertThrowsError(try migrator.performMigration()) { error in + guard + let migrationError = error as? SubstrateStorageMigrationError, + case .unknownStoreVersion = migrationError + else { + return XCTFail("Expected unknownStoreVersion, got \(error)") + } + } + + XCTAssertEqual(try durableStoreFamilySnapshot(at: storeURL), before) + } + + func testSyncMigration_whenStoreVersionIsUnknown_thenFailsClosedAndPreservesEveryStoreFile() throws { + try createStore(at: storeURL, model: makeUnknownModel()) { _ in } + let before = try durableStoreFamilySnapshot(at: storeURL) + let migrator = makeMigrator() + + XCTAssertThrowsError(try migrator.migrate()) { error in + guard + let migrationError = error as? SubstrateStorageMigrationError, + case .unknownStoreVersion = migrationError + else { + return XCTFail("Expected unknownStoreVersion, got \(error)") + } + } + + XCTAssertEqual(try durableStoreFamilySnapshot(at: storeURL), before) + XCTAssertTrue(try recoveryDirectories().isEmpty) + + XCTAssertThrowsError(try migrator.migrate()) + XCTAssertEqual(try durableStoreFamilySnapshot(at: storeURL), before) + XCTAssertTrue(try recoveryDirectories().isEmpty) + } + + func testSyncMigration_whenKnownStoreCannotBeInspected_thenFailsClosedAndPreservesEveryStoreFile() throws { + let sourceModel = try model(for: .version3) + try createStore(at: storeURL, model: sourceModel) { context in + try self.insertRuntimeItem( + identifier: "uninspectable-runtime", + in: context, + model: sourceModel + ) + } + try executeSQLite( + "PRAGMA journal_mode=DELETE; DROP TABLE ZCDCONTACT", + at: storeURL + ) + + let before = try durableStoreFamilySnapshot(at: storeURL) + let migrator = makeMigrator() + + for _ in 0 ..< 2 { + XCTAssertThrowsError(try migrator.migrate()) { error in + guard + let migrationError = error as? SubstrateStorageMigrationError, + case .sourceStoreInspectionFailed = migrationError + else { + return XCTFail("Expected sourceStoreInspectionFailed, got \(error)") + } + } + + XCTAssertTrue(migrator.requiresMigration()) + XCTAssertEqual(try durableStoreFamilySnapshot(at: storeURL), before) + XCTAssertTrue(try recoveryDirectories().isEmpty) + } + } + + func testPerformMigration_whenTargetModelIsMissing_thenThrowsBeforeCheckpointAndPreservesSource() throws { + let sourceModel = try model(for: .version7) + try createStore(at: storeURL, model: sourceModel) { context in + try self.insertRuntimeItem(identifier: "missing-model", in: context, model: sourceModel) + } + let before = try durableStoreFamilySnapshot(at: storeURL) + let modelBundle = try makeModelBundle(omitting: .version8) + let migrator = makeMigrator(modelBundle: modelBundle) + + XCTAssertThrowsError(try migrator.performMigration()) { error in + guard + let migrationError = error as? SubstrateStorageMigrationError, + case let .modelUnavailable(version) = migrationError + else { + return XCTFail("Expected modelUnavailable, got \(error)") + } + + XCTAssertEqual(version, .version8) + } + + XCTAssertEqual(try durableStoreFamilySnapshot(at: storeURL), before) + XCTAssertEqual( + try count(entityName: "CDRuntimeMetadataItem", at: storeURL, model: sourceModel), + 1 + ) + } + + func testPerformMigration_whenAskedToDowngrade_thenThrowsBeforeCheckpointAndPreservesSource() throws { + let sourceModel = try model(for: .version8) + try createStore(at: storeURL, model: sourceModel) { context in + try self.insertRuntimeItem(identifier: "downgrade", in: context, model: sourceModel) + } + let before = try durableStoreFamilySnapshot(at: storeURL) + let migrator = makeMigrator(targetVersion: .version7) + + XCTAssertThrowsError(try migrator.performMigration()) { error in + guard + let migrationError = error as? SubstrateStorageMigrationError, + case let .migrationPathUnavailable(source, destination) = migrationError + else { + return XCTFail("Expected migrationPathUnavailable, got \(error)") + } + + XCTAssertEqual(source, .version8) + XCTAssertEqual(destination, .version7) + } + + XCTAssertEqual(try durableStoreFamilySnapshot(at: storeURL), before) + XCTAssertEqual( + try count(entityName: "CDRuntimeMetadataItem", at: storeURL, model: sourceModel), + 1 + ) + } + + func testPerformMigration_whenVersion7Store_thenStagesValidVersion8AndPreservesRows() throws { + let sourceModel = try model(for: .version7) + try createStore(at: storeURL, model: sourceModel) { context in + try self.insertRuntimeItem(identifier: "legacy-runtime", in: context, model: sourceModel) + try self.insertMigrationAsset( + identifier: "legacy-asset", + in: context, + model: sourceModel + ) + } + let migrator = makeMigrator() + + XCTAssertTrue(migrator.requiresMigration()) + try migrator.performMigration() + + let targetModel = try model(for: .version8) + XCTAssertFalse(migrator.requiresMigration()) + XCTAssertTrue(try isStore(at: storeURL, compatibleWith: targetModel, version: .version8)) + XCTAssertEqual( + try count(entityName: "CDRuntimeMetadataItem", at: storeURL, model: targetModel), + 1 + ) + XCTAssertEqual( + try count(entityName: "CDAsset", at: storeURL, model: targetModel), + 1 + ) + } + + func testPerformMigration_whenEverySupportedLegacyVersion_thenReachesV8AndPreservesRows() throws { + for sourceVersion in SubstrateStorageVersion.allCases where sourceVersion != .version8 { + let caseDirectory = testDirectory.appendingPathComponent( + sourceVersion.rawValue, + isDirectory: true + ) + try FileManager.default.createDirectory( + at: caseDirectory, + withIntermediateDirectories: true + ) + let caseStoreURL = caseDirectory.appendingPathComponent( + SubstrateStorageParams.databaseName + ) + let sourceModel = try model(for: sourceVersion) + try createStore(at: caseStoreURL, model: sourceModel) { context in + try self.insertRuntimeItem( + identifier: "runtime-\(sourceVersion.rawValue)", + in: context, + model: sourceModel + ) + try self.insertChainStorageItem( + identifier: "storage-\(sourceVersion.rawValue)", + in: context, + model: sourceModel + ) + try self.insertMigrationAsset( + identifier: "asset-\(sourceVersion.rawValue)", + in: context, + model: sourceModel + ) + } + + let migrator = makeMigrator(storeURL: caseStoreURL) + XCTAssertTrue( + migrator.requiresMigration(), + "\(sourceVersion.rawValue) should require migration" + ) + + try migrator.performMigration() + + let targetModel = try model(for: .version8) + XCTAssertFalse( + migrator.requiresMigration(), + "\(sourceVersion.rawValue) should reach v8" + ) + XCTAssertTrue( + try isStore( + at: caseStoreURL, + compatibleWith: targetModel, + version: .version8 + ) + ) + XCTAssertEqual( + try count( + entityName: "CDRuntimeMetadataItem", + at: caseStoreURL, + model: targetModel + ), + 1, + "\(sourceVersion.rawValue) lost runtime metadata" + ) + XCTAssertEqual( + try count( + entityName: "CDChainStorageItem", + at: caseStoreURL, + model: targetModel + ), + 1, + "\(sourceVersion.rawValue) lost chain storage" + ) + XCTAssertEqual( + try count( + entityName: "CDAsset", + at: caseStoreURL, + model: targetModel + ), + 1, + "\(sourceVersion.rawValue) lost an asset" + ) + } + } + + func testPerformMigration_whenOldestLegacyStore_thenNeverRetainsCompletedIntermediates() throws { + let sourceModel = try model(for: .version1) + try createStore(at: storeURL, model: sourceModel) { context in + try self.insertRuntimeItem( + identifier: "bounded-footprint-runtime", + in: context, + model: sourceModel + ) + try self.insertMigrationAsset( + identifier: "bounded-footprint-asset", + in: context, + model: sourceModel + ) + } + let fileManager = TemporaryStoreFootprintFileManager() + let migrator = makeMigrator(fileManager: fileManager) + + try migrator.performMigration() + + let expectedStepCount = SubstrateStorageVersion.allCases.count - 1 + XCTAssertEqual(fileManager.maximumTemporaryMainStoreCount, 2) + XCTAssertEqual( + fileManager.temporaryMainStoreCountsBeforeRemoval, + Array(repeating: 2, count: expectedStepCount) + ) + XCTAssertEqual( + fileManager.removedTemporaryMainStoreCount, + expectedStepCount + ) + XCTAssertTrue(fileManager.temporaryRootsAreClean) + XCTAssertTrue( + try isStore( + at: storeURL, + compatibleWith: model(for: .version8), + version: .version8 + ) + ) + } + + func testPerformMigration_whenTemporaryInputCleanupFails_thenAbortsAndCleansEveryPrivateStore() throws { + let sourceModel = try model(for: .version7) + try createStore(at: storeURL, model: sourceModel) { context in + try self.insertRuntimeItem( + identifier: "cleanup-failure-runtime", + in: context, + model: sourceModel + ) + } + let before = try durableStoreFamilySnapshot(at: storeURL) + let fileManager = FailingTemporaryStoreCleanupFileManager() + let migrator = makeMigrator(fileManager: fileManager) + + XCTAssertThrowsError(try migrator.performMigration()) { error in + guard + let migrationError = error as? SubstrateStorageMigrationError, + case .temporaryStoreCleanupFailed = migrationError + else { + return XCTFail("Expected temporaryStoreCleanupFailed, got \(error)") + } + } + + XCTAssertTrue(fileManager.didInjectCleanupFailure) + XCTAssertTrue(fileManager.temporaryRootsAreClean) + XCTAssertEqual(try durableStoreFamilySnapshot(at: storeURL), before) + } + + func testPerformMigration_whenVersion1ChainHasNoSelectedNode_thenSelectsStableNode() throws { + let sourceModel = try model(for: .version1) + try createStore(at: storeURL, model: sourceModel) { context in + try self.insertLegacyChainWithNodes(in: context, model: sourceModel) + } + + try makeMigrator().performMigration() + + let targetModel = try model(for: .version8) + try inspectStore(at: storeURL, model: targetModel) { context in + let chain = try XCTUnwrap( + try context.fetch( + NSFetchRequest(entityName: "CDChain") + ).first + ) + let nodes = try XCTUnwrap( + chain.value(forKey: "nodes") as? Set + ) + let selectedNode = try XCTUnwrap( + chain.value(forKey: "selectedNode") as? NSManagedObject + ) + + XCTAssertEqual(nodes.count, 2) + XCTAssertEqual( + selectedNode.value(forKey: "url") as? URL, + URL(string: "wss://alpha.example.invalid") + ) + XCTAssertTrue(nodes.contains(selectedNode)) + XCTAssertEqual(selectedNode.value(forKey: "chain") as? NSManagedObject, chain) + } + } + + func testPerformMigration_whenVersion3ContainsChainGraph_thenUsesCustomV4Policy() throws { + let sourceModel = try model(for: .version3) + try createStore(at: storeURL, model: sourceModel) { context in + try self.insertLegacyChainGraph(in: context, model: sourceModel) + } + + try makeMigrator().performMigration() + + let targetModel = try model(for: .version8) + XCTAssertEqual( + try count(entityName: "CDChain", at: storeURL, model: targetModel), + 1 + ) + XCTAssertEqual( + try count(entityName: "CDChainNode", at: storeURL, model: targetModel), + 1 + ) + XCTAssertEqual( + try count(entityName: "CDAsset", at: storeURL, model: targetModel), + 1 + ) + + try inspectStore(at: storeURL, model: targetModel) { context in + let chain = try XCTUnwrap( + try context.fetch( + NSFetchRequest(entityName: "CDChain") + ).first + ) + let assets = try XCTUnwrap( + chain.value(forKey: "assets") as? Set + ) + let asset = try XCTUnwrap(assets.first) + + XCTAssertEqual(assets.count, 1) + XCTAssertEqual(asset.value(forKey: "id") as? String, "migration-asset") + XCTAssertEqual(asset.value(forKey: "symbol") as? String, "MIG") + XCTAssertEqual(asset.value(forKey: "isNative") as? Bool, true) + XCTAssertEqual(asset.value(forKey: "isUtility") as? Bool, false) + XCTAssertEqual(asset.value(forKey: "type") as? String, "normal") + XCTAssertEqual(chain.value(forKey: "disabled") as? Bool, false) + XCTAssertEqual(asset.value(forKey: "chain") as? NSManagedObject, chain) + } + } + + func testPerformMigration_whenV3ChainOptionsArchiveIsRawGarbage_thenDefaultsOnlyThatCache() throws { + try assertAdversarialTransformableMigration( + sourceVersion: .version3, + updateSQL: "UPDATE ZCDCHAIN SET ZOPTIONS = X'00FF00'", + expectedDefaultedKeys: ["CDChain.options"] + ) + } + + func testPerformMigration_whenV3ExternalTypesArchiveIsEmpty_thenDefaultsOnlyThatCache() throws { + try assertAdversarialTransformableMigration( + sourceVersion: .version3, + updateSQL: "UPDATE ZCDEXTERNALAPI SET ZTYPES = X''", + expectedDefaultedKeys: ["CDExternalApi.types"] + ) + } + + func testPerformMigration_whenV3PurchaseProvidersArchiveIsTruncated_thenV4PolicyStillPreservesGraph() throws { + let truncatedArchive = try NSKeyedArchiver.archivedData( + withRootObject: NSArray(object: "provider-a"), + requiringSecureCoding: true + ).prefix(8) + + try assertAdversarialTransformableMigration( + sourceVersion: .version3, + updateSQL: + "UPDATE ZCDCHAINASSET SET ZPURCHASEPROVIDERS = " + + "X'\(hex(Data(truncatedArchive)))'", + expectedDefaultedKeys: ["CDChainAsset.purchaseProviders"] + ) + } + + func testPerformMigration_whenV4AssetPurchaseProvidersArchiveIsRawGarbage_thenDefaultsOnlyAssetCache() throws { + try assertAdversarialTransformableMigration( + sourceVersion: .version4, + updateSQL: + "UPDATE ZCDASSET SET ZPURCHASEPROVIDERS = X'00FF00'", + expectedDefaultedKeys: ["CDAsset.purchaseProviders"] + ) + } + + func testPerformMigration_whenBothV3PolkaswapArchivesHaveWrongTypes_thenDefaultsBothRequiredArrays() throws { + let wrongTypeArchive = try NSKeyedArchiver.archivedData( + withRootObject: NSString(string: "not-an-array"), + requiringSecureCoding: true + ) + let archiveHex = hex(wrongTypeArchive) + + try assertAdversarialTransformableMigration( + sourceVersion: .version3, + updateSQL: """ + UPDATE ZCDPOLKASWAPREMOTESETTINGS + SET ZAVAILABLESOURCES = X'\(archiveHex)', + ZFORCESMARTIDS = X'\(archiveHex)' + """, + expectedDefaultedKeys: [ + "CDPolkaswapRemoteSettings.availableSources", + "CDPolkaswapRemoteSettings.forceSmartIds" + ] + ) + } + + func testPerformMigration_whenV4LegacyXcmArchiveContainsDisallowedClass_thenDefaultsConfigAndPreservesDestination() throws { + let disallowedArchive = try NSKeyedArchiver.archivedData( + withRootObject: DisallowedSubstrateTransformablePayload( + value: "must-not-decode" + ), + requiringSecureCoding: true + ) + + try assertAdversarialTransformableMigration( + sourceVersion: .version4, + updateSQL: + "UPDATE ZCDCHAINXCMCONFIG SET ZAVAILABLEASSETS = " + + "X'\(hex(disallowedArchive))'", + expectedDefaultedKeys: ["CDChainXcmConfig.availableAssets"] + ) + } + + func testPerformMigration_whenV3LegacyDestinationAssetsArchiveIsInvalid_thenDefaultsDestinationAndPreservesConfig() throws { + try assertAdversarialTransformableMigration( + sourceVersion: .version3, + updateSQL: + "UPDATE ZCDXCMAVAILABLEDESTINATION SET ZASSETS = X'00FF00'", + expectedDefaultedKeys: ["CDXcmAvailableDestination.assets"] + ) + } + + func testPerformMigration_whenAllV3TransformableArchivesAreValid_thenPreservesEveryValueAndProtectedRelationship() throws { + try assertAdversarialTransformableMigration( + sourceVersion: .version3, + updateSQL: "UPDATE ZCDCHAIN SET ZOPTIONS = ZOPTIONS", + expectedDefaultedKeys: [] + ) + } + + func testPerformMigration_whenEveryV3TransformableArchiveIsDamagedTogether_thenRepairsEveryKeyAndPreservesProtectedGraph() throws { + let wrongTypeArchive = try NSKeyedArchiver.archivedData( + withRootObject: NSString(string: "not-an-array"), + requiringSecureCoding: true + ) + let wrongTypeHex = hex(wrongTypeArchive) + + try assertAdversarialTransformableMigration( + sourceVersion: .version3, + updateSQL: """ + UPDATE ZCDCHAIN SET ZOPTIONS = X'00FF00'; + UPDATE ZCDCHAINASSET SET ZPURCHASEPROVIDERS = X''; + UPDATE ZCDEXTERNALAPI SET ZTYPES = X'\(wrongTypeHex)'; + UPDATE ZCDPOLKASWAPREMOTESETTINGS + SET ZAVAILABLESOURCES = X'00FF00', + ZFORCESMARTIDS = X'\(wrongTypeHex)'; + UPDATE ZCDCHAINXCMCONFIG + SET ZAVAILABLEASSETS = X'\(wrongTypeHex)'; + UPDATE ZCDXCMAVAILABLEDESTINATION SET ZASSETS = X'00FF00'; + """, + expectedDefaultedKeys: [ + "CDChain.options", + "CDChainAsset.purchaseProviders", + "CDExternalApi.types", + "CDPolkaswapRemoteSettings.availableSources", + "CDPolkaswapRemoteSettings.forceSmartIds", + "CDChainXcmConfig.availableAssets", + "CDXcmAvailableDestination.assets" + ] + ) + } + + func testPerformMigration_whenTransformableElementCountIsExactlyAtLimit_thenPreservesArray() throws { + try createVersion7AssetStore( + purchaseProviders: ["provider-a", "provider-b"] + ) + + try makeMigrator( + protectedDataInspectionLimits: + makeProtectedDataLimits( + maximumTransformableElements: 2 + ) + ).performMigration() + + XCTAssertEqual( + try migratedAssetPurchaseProviders(), + ["provider-a", "provider-b"] + ) + } + + func testPerformMigration_whenTransformableElementCountExceedsLimit_thenRepairsArray() throws { + try createVersion7AssetStore( + purchaseProviders: [ + "provider-a", + "provider-b", + "provider-c" + ] + ) + + try makeMigrator( + protectedDataInspectionLimits: + makeProtectedDataLimits( + maximumTransformableElements: 2 + ) + ).performMigration() + + XCTAssertEqual(try migratedAssetPurchaseProviders(), []) + } + + func testPerformMigration_whenTransformableUTF8BytesAreExactlyAtLimit_thenPreservesArray() throws { + // One composed character plus two ASCII characters occupy four + // cumulative UTF-8 bytes. + try createVersion7AssetStore( + purchaseProviders: ["é", "ab"] + ) + + try makeMigrator( + protectedDataInspectionLimits: + makeProtectedDataLimits(maximumValueByteCount: 4) + ).performMigration() + + XCTAssertEqual( + try migratedAssetPurchaseProviders(), + ["é", "ab"] + ) + } + + func testPerformMigration_whenTransformableUTF8BytesExceedLimit_thenRepairsArray() throws { + // Each element is individually within budget, while their cumulative + // UTF-8 representation is five bytes. + try createVersion7AssetStore( + purchaseProviders: ["é", "abc"] + ) + + try makeMigrator( + protectedDataInspectionLimits: + makeProtectedDataLimits(maximumValueByteCount: 4) + ).performMigration() + + XCTAssertEqual(try migratedAssetPurchaseProviders(), []) + } + + func testPerformMigration_whenRawTransformableArchiveIsExactlyAtLimit_thenPreservesArray() throws { + try createVersion7AssetStore( + purchaseProviders: ["raw-boundary"] + ) + let archiveByteCount = try sqliteInteger( + "SELECT length(ZPURCHASEPROVIDERS) FROM ZCDASSET LIMIT 1", + at: storeURL + ) + XCTAssertGreaterThan(archiveByteCount, 0) + XCTAssertLessThanOrEqual(archiveByteCount, Int64(Int.max)) + + try makeMigrator( + protectedDataInspectionLimits: + makeProtectedDataLimits( + maximumTransformableArchiveByteCount: + Int(archiveByteCount) + ) + ).performMigration() + + XCTAssertEqual( + try migratedAssetPurchaseProviders(), + ["raw-boundary"] + ) + } + + func testPerformMigration_whenRawTransformableArchiveExceedsLimit_thenRepairsBeforeCoreDataDecode() throws { + try createVersion7AssetStore( + purchaseProviders: ["will-be-replaced"] + ) + try executeSQLite( + """ + UPDATE ZCDASSET + SET ZPURCHASEPROVIDERS = zeroblob(1025) + """, + at: storeURL + ) + + try makeMigrator( + protectedDataInspectionLimits: + makeProtectedDataLimits( + maximumTransformableArchiveByteCount: 1024 + ) + ).performMigration() + + XCTAssertNil(try migratedAssetPurchaseProviders()) + } + + func testPerformMigration_whenOptionalTransformableUsesWrongSQLiteStorageClass_thenRepairsBeforeDecode() throws { + try createVersion7AssetStore( + purchaseProviders: ["will-be-replaced"] + ) + try executeSQLite( + """ + UPDATE ZCDASSET + SET ZPURCHASEPROVIDERS = 'not-a-blob' + """, + at: storeURL + ) + + try makeMigrator().performMigration() + + XCTAssertNil(try migratedAssetPurchaseProviders()) + } + + func testPerformMigration_whenRequiredTransformableUsesWrongSQLiteStorageClass_thenWritesValidEmptyArchive() throws { + let sourceModel = try model(for: .version7) + try createStore(at: storeURL, model: sourceModel) { + context in + let settings = try self.insert( + entityName: "CDPolkaswapRemoteSettings", + in: context, + model: sourceModel + ) + settings.setValue("v1", forKey: "version") + settings.setValue( + NSArray(object: "source-a"), + forKey: "availableSources" + ) + settings.setValue( + NSArray(object: "smart-a"), + forKey: "forceSmartIds" + ) + } + try executeSQLite( + """ + UPDATE ZCDPOLKASWAPREMOTESETTINGS + SET ZFORCESMARTIDS = 42 + """, + at: storeURL + ) + + try makeMigrator().performMigration() + + let targetModel = try model(for: .version8) + try inspectStore(at: storeURL, model: targetModel) { + context in + let settings = try self.fetchSingleObject( + entityName: "CDPolkaswapRemoteSettings", + context: context + ) + XCTAssertEqual( + try self.stringArray( + in: settings, + key: "availableSources" + ), + ["source-a"] + ) + XCTAssertEqual( + try self.stringArray( + in: settings, + key: "forceSmartIds" + ), + [] + ) + } + } + + func testPerformMigrationWithRecovery_whenTransformableEntityExceedsRowCap_thenRejectsBeforePagingReplacementOrQuarantine() throws { + let sourceModel = try model(for: .version3) + try createStore(at: storeURL, model: sourceModel) { + context in + try self.insertTransformableAndProtectedGraph( + in: context, + model: sourceModel + ) + } + try executeSQLite( + "UPDATE ZCDCHAIN SET ZOPTIONS = X'00FF00'", + at: storeURL + ) + let before = try durableStoreFamilySnapshot(at: storeURL) + var replacementAttempted = false + var fetchedPages = [(String, Int, Int)]() + let migrator = makeMigrator( + storeReplacer: { _, _ in + replacementAttempted = true + }, + protectedDataInspectionLimits: + makeProtectedDataLimits(maximumRowsPerEntity: 0), + transformableObjectIDPageDidFetch: { + fetchedPages.append(($0, $1, $2)) + } + ) + + XCTAssertThrowsError( + try migrator.performMigrationWithRecovery() + ) { error in + guard + let migrationError = + error as? SubstrateStorageMigrationError, + case let .transformableSanitizationFailed( + version, + _ + ) = migrationError + else { + return XCTFail( + "Expected transformableSanitizationFailed, got \(error)" + ) + } + XCTAssertEqual(version, .version3) + } + + XCTAssertTrue(fetchedPages.isEmpty) + XCTAssertFalse(replacementAttempted) + XCTAssertEqual( + try durableStoreFamilySnapshot(at: storeURL), + before + ) + XCTAssertTrue(try recoveryDirectories().isEmpty) + } + + func testPerformMigration_whenBatchRepairsResetContext_thenImmutableIDCohortRepairsEveryRowExactlyOnce() throws { + let sourceModel = try model(for: .version7) + try createStore(at: storeURL, model: sourceModel) { + context in + try self.insertMigrationAsset( + identifier: "paged-asset-a", + in: context, + model: sourceModel + ) + try self.insertMigrationAsset( + identifier: "paged-asset-b", + in: context, + model: sourceModel + ) + let request = NSFetchRequest( + entityName: "CDAsset" + ) + for asset in try context.fetch(request) { + asset.setValue( + NSArray(object: "provider"), + forKey: "purchaseProviders" + ) + } + } + try executeSQLite( + """ + UPDATE ZCDASSET + SET ZPURCHASEPROVIDERS = X'00FF00' + """, + at: storeURL + ) + var fetchedPages = [(String, Int, Int)]() + let migrator = makeMigrator( + protectedDataInspectionLimits: + makeProtectedDataLimits(fetchPageSize: 1), + transformableObjectIDPageDidFetch: { + fetchedPages.append(($0, $1, $2)) + } + ) + + try migrator.performMigration() + + let assetPages = fetchedPages.filter { + $0.0 == "CDAsset" + } + XCTAssertEqual(assetPages.count, 2) + XCTAssertTrue( + assetPages.allSatisfy { + $0.1 == 1 && $0.2 == 1 + } + ) + let targetModel = try model(for: .version8) + try inspectStore( + at: storeURL, + model: targetModel + ) { context in + let request = NSFetchRequest( + entityName: "CDAsset" + ) + let assets = try context.fetch(request) + XCTAssertEqual(assets.count, 2) + for asset in assets { + XCTAssertEqual( + try self.stringArray( + in: asset, + key: "purchaseProviders" + ), + [] + ) + } + } + } + + func testSyncMigration_whenFinalReplacementFails_thenSourceFamilyAndRelationshipsRemainExact() throws { + let sourceModel = try model(for: .version3) + try createStore(at: storeURL, model: sourceModel) { context in + try self.insertTransformableAndProtectedGraph( + in: context, + model: sourceModel + ) + } + try executeSQLite( + "UPDATE ZCDCHAIN SET ZOPTIONS = X'00FF00'", + at: storeURL + ) + let before = try durableStoreFamilySnapshot(at: storeURL) + let migrator = makeMigrator( + storeReplacer: { _, _ in + throw InjectedSubstrateStoreReplacementError.failure + } + ) + + XCTAssertThrowsError(try migrator.migrate()) { error in + guard + let migrationError = error as? SubstrateStorageMigrationError, + case .storeReplacementFailed = migrationError + else { + return XCTFail("Expected storeReplacementFailed, got \(error)") + } + } + + XCTAssertEqual(try durableStoreFamilySnapshot(at: storeURL), before) + XCTAssertTrue(try recoveryDirectories().isEmpty) + try assertSourceProtectedGraphIsExact( + at: storeURL, + model: sourceModel + ) + } + + func testPerformMigration_whenProcessDiesAfterRemovingLiveMainStore_thenRelaunchRestoresAndMigratesProtectedGraph() throws { + let sourceModel = try model(for: .version3) + try createStore(at: storeURL, model: sourceModel) { context in + try self.insertTransformableAndProtectedGraph( + in: context, + model: sourceModel + ) + } + let interruptedMigrator = makeMigrator( + storeReplacer: { targetURL, _ in + try FileManager.default.removeItem(at: targetURL) + throw CrashConsistentStoreReplacementInterruption + .simulatedProcessDeath + } + ) + + XCTAssertThrowsError( + try interruptedMigrator.performMigration() + ) { error in + guard + let migrationError = + error as? SubstrateStorageMigrationError, + case let .storeReplacementFailed(_, underlyingError) = + migrationError, + underlyingError + is CrashConsistentStoreReplacementInterruption + else { + return XCTFail( + "Expected interrupted store replacement, got \(error)" + ) + } + } + XCTAssertFalse( + FileManager.default.fileExists(atPath: storeURL.path), + "The fixture must reproduce the missing-main startup state" + ) + + let relaunchedMigrator = makeMigrator() + try relaunchedMigrator.performMigration() + + try assertMigratedTransformableAndProtectedGraph( + expectedDefaultedKeys: [] + ) + XCTAssertFalse(relaunchedMigrator.requiresMigration()) + XCTAssertFalse( + FileManager.default.fileExists( + atPath: testDirectory + .appendingPathComponent( + ".FearlessStoreReplacement", + isDirectory: true + ).path + ) + ) + } + + func testPerformMigration_whenStagedProtectedValuesAndSelectedNodeAreSwappedWithoutRowLoss_thenRejectsStageAndPreservesSource() throws { + let sourceModel = try model(for: .version3) + try createStore(at: storeURL, model: sourceModel) { context in + try self.insertTransformableAndProtectedGraph( + in: context, + model: sourceModel + ) + } + let before = try durableStoreFamilySnapshot(at: storeURL) + let migrator = makeMigrator( + stagedStoreMutationHook: { stagedURL, targetModel in + try self.mutateStagedProtectedDataWithoutChangingRowCounts( + at: stagedURL, + model: targetModel + ) + } + ) + + XCTAssertThrowsError(try migrator.performMigration()) { error in + guard + let migrationError = error as? SubstrateStorageMigrationError, + case .stagedStoreProtectedDataMismatch = migrationError + else { + return XCTFail( + "Expected stagedStoreProtectedDataMismatch, got \(error)" + ) + } + } + + XCTAssertEqual(try durableStoreFamilySnapshot(at: storeURL), before) + try assertSourceProtectedGraphIsExact( + at: storeURL, + model: sourceModel + ) + } + + func testPerformMigration_whenProtectedEntityExceedsRowLimit_thenFailsClosedBeforeMigration() throws { + let sourceModel = try model(for: .version7) + try createStore(at: storeURL, model: sourceModel) { + context in + for index in 0 ..< 2 { + let contact = try self.insert( + entityName: "CDContact", + in: context, + model: sourceModel + ) + contact.setValue( + "contact-\(index)", + forKey: "address" + ) + } + } + let before = try durableStoreFamilySnapshot(at: storeURL) + let migrator = makeMigrator( + protectedDataInspectionLimits: + makeProtectedDataLimits(maximumRowsPerEntity: 1) + ) + + XCTAssertThrowsError(try migrator.performMigration()) { error in + guard + let migrationError = + error as? SubstrateStorageMigrationError, + case let .sourceStoreInspectionFailed( + failedURL, + underlyingError + ) = migrationError, + let inspectionError = + underlyingError as? + SubstrateProtectedDataInspectionError, + case let .entityRowLimitExceeded( + entityName, + actual, + maximum + ) = inspectionError + else { + return XCTFail( + "Expected entityRowLimitExceeded, got \(error)" + ) + } + + XCTAssertEqual(failedURL, self.storeURL) + XCTAssertEqual(entityName, "CDContact") + XCTAssertEqual(actual, 2) + XCTAssertEqual(maximum, 1) + } + XCTAssertEqual( + try durableStoreFamilySnapshot(at: storeURL), + before + ) + } + + func testPerformMigration_whenProtectedEntitiesExceedTotalRowLimit_thenFailsClosedBeforePaging() throws { + let sourceModel = try model(for: .version7) + try createStore(at: storeURL, model: sourceModel) { + context in + _ = try self.insert( + entityName: "CDContact", + in: context, + model: sourceModel + ) + _ = try self.insert( + entityName: "CDContactItem", + in: context, + model: sourceModel + ) + } + let before = try durableStoreFamilySnapshot(at: storeURL) + let migrator = makeMigrator( + protectedDataInspectionLimits: + makeProtectedDataLimits(maximumTotalRows: 1) + ) + + XCTAssertThrowsError(try migrator.performMigration()) { error in + guard + let migrationError = + error as? SubstrateStorageMigrationError, + case let .sourceStoreInspectionFailed( + _, + underlyingError + ) = migrationError, + let inspectionError = + underlyingError as? + SubstrateProtectedDataInspectionError, + case let .totalRowLimitExceeded( + actual, + maximum + ) = inspectionError + else { + return XCTFail( + "Expected totalRowLimitExceeded, got \(error)" + ) + } + + XCTAssertEqual(actual, 2) + XCTAssertEqual(maximum, 1) + } + XCTAssertEqual( + try durableStoreFamilySnapshot(at: storeURL), + before + ) + } + + func testPerformMigration_whenChainRelationshipExceedsMemberLimit_thenFailsClosedBeforeDigestingTopology() throws { + let sourceModel = try model(for: .version7) + try createStore(at: storeURL, model: sourceModel) { + context in + try self.insertLegacyChainWithNodes( + in: context, + model: sourceModel + ) + } + let before = try durableStoreFamilySnapshot(at: storeURL) + var relationshipCountFaultStates = + [(String, Bool, Int)]() + var materializedRelationships = + [(String, String, Int)]() + let migrator = makeMigrator( + protectedDataInspectionLimits: + makeProtectedDataLimits( + maximumRelationshipMembers: 1 + ), + protectedRelationshipCountDidFetch: { + relationshipCountFaultStates.append( + ( + $1, + $0.hasFault( + forRelationshipNamed: $1 + ), + $2 + ) + ) + }, + protectedRelationshipWillMaterialize: { + materializedRelationships.append(($0, $1, $2)) + } + ) + + XCTAssertThrowsError(try migrator.performMigration()) { error in + guard + let migrationError = + error as? SubstrateStorageMigrationError, + case let .sourceStoreInspectionFailed( + _, + underlyingError + ) = migrationError, + let inspectionError = + underlyingError as? + SubstrateProtectedDataInspectionError, + case let .relationshipLimitExceeded( + entityName, + relationshipName, + actual, + maximum + ) = inspectionError + else { + return XCTFail( + "Expected relationshipLimitExceeded, got \(error)" + ) + } + + XCTAssertEqual(entityName, "CDChain") + XCTAssertEqual(relationshipName, "nodes") + XCTAssertEqual(actual, 2) + XCTAssertEqual(maximum, 1) + } + XCTAssertEqual( + relationshipCountFaultStates.map { $0.0 }, + ["nodes"] + ) + XCTAssertTrue( + relationshipCountFaultStates.allSatisfy { $0.1 }, + "The SQL count must leave the inverse relationship faulted" + ) + XCTAssertEqual( + relationshipCountFaultStates.map { $0.2 }, + [2] + ) + XCTAssertTrue( + materializedRelationships.isEmpty, + "An over-limit relationship must be rejected before member copying" + ) + XCTAssertEqual( + try durableStoreFamilySnapshot(at: storeURL), + before + ) + } + + func testPerformMigration_whenNoInverseRelationshipExceedsMemberLimit_thenRejectsWhileRelationshipRemainsFaulted() throws { + let sourceModel = try model(for: .version7) + try createStore(at: storeURL, model: sourceModel) { + context in + let graph = try self.insertChainWithDefaultNode( + in: context, + model: sourceModel + ) + for index in 0 ..< 2 { + let customNode = try self.insert( + entityName: "CDChainNode", + in: context, + model: sourceModel + ) + customNode.setValue( + "Custom \(index)", + forKey: "name" + ) + customNode.setValue( + try XCTUnwrap( + URL( + string: + "wss://custom-\(index).example.invalid" + ) + ), + forKey: "url" + ) + graph.chain.mutableSetValue( + forKey: "customNodes" + ).add(customNode) + } + } + let before = try durableStoreFamilySnapshot(at: storeURL) + var relationshipCountFaultStates = + [(String, Bool, Int)]() + var materializedRelationships = + [(String, String, Int)]() + let migrator = makeMigrator( + protectedDataInspectionLimits: + makeProtectedDataLimits( + maximumRelationshipMembers: 1 + ), + protectedRelationshipCountDidFetch: { + relationshipCountFaultStates.append( + ( + $1, + $0.hasFault( + forRelationshipNamed: $1 + ), + $2 + ) + ) + }, + protectedRelationshipWillMaterialize: { + materializedRelationships.append(($0, $1, $2)) + } + ) + + XCTAssertThrowsError(try migrator.performMigration()) { + error in + guard + let migrationError = + error as? SubstrateStorageMigrationError, + case let .sourceStoreInspectionFailed( + _, + underlyingError + ) = migrationError, + let inspectionError = + underlyingError as? + SubstrateProtectedDataInspectionError, + case let .relationshipLimitExceeded( + entityName, + relationshipName, + actual, + maximum + ) = inspectionError + else { + return XCTFail( + "Expected no-inverse relationship limit, got \(error)" + ) + } + + XCTAssertEqual(entityName, "CDChain") + XCTAssertEqual(relationshipName, "customNodes") + XCTAssertEqual(actual, 2) + XCTAssertEqual(maximum, 1) + } + XCTAssertEqual( + relationshipCountFaultStates.map { $0.0 }, + ["nodes", "customNodes"] + ) + XCTAssertTrue( + relationshipCountFaultStates.allSatisfy { $0.1 }, + "Both relationship counts must leave their faults intact" + ) + XCTAssertEqual( + relationshipCountFaultStates.map { $0.2 }, + [1, 2] + ) + XCTAssertTrue( + materializedRelationships.isEmpty, + "Both counts must be accepted before either relationship fires" + ) + XCTAssertEqual( + try durableStoreFamilySnapshot(at: storeURL), + before + ) + } + + func testPerformMigration_whenAggregateRelationshipLimitIsExceeded_thenRejectsBeforeEitherRelationshipMaterializes() throws { + let sourceModel = try model(for: .version7) + try createStore(at: storeURL, model: sourceModel) { + context in + let graph = try self.insertChainWithDefaultNode( + in: context, + model: sourceModel + ) + let customNode = try self.insert( + entityName: "CDChainNode", + in: context, + model: sourceModel + ) + customNode.setValue("Custom", forKey: "name") + customNode.setValue( + try XCTUnwrap( + URL(string: "wss://custom.example.invalid") + ), + forKey: "url" + ) + graph.chain.mutableSetValue( + forKey: "customNodes" + ).add(customNode) + } + let before = try durableStoreFamilySnapshot(at: storeURL) + var materializedRelationships = + [(String, String, Int)]() + let migrator = makeMigrator( + protectedDataInspectionLimits: + makeProtectedDataLimits( + maximumRelationshipMembers: 1, + maximumTotalRelationshipMembers: 1 + ), + protectedRelationshipWillMaterialize: { + materializedRelationships.append(($0, $1, $2)) + } + ) + + XCTAssertThrowsError(try migrator.performMigration()) { + error in + guard + let migrationError = + error as? SubstrateStorageMigrationError, + case let .sourceStoreInspectionFailed( + _, + underlyingError + ) = migrationError, + let inspectionError = + underlyingError as? + SubstrateProtectedDataInspectionError, + case let .totalRelationshipLimitExceeded( + actual, + maximum + ) = inspectionError + else { + return XCTFail( + "Expected aggregate relationship limit, got \(error)" + ) + } + + XCTAssertEqual(actual, 2) + XCTAssertEqual(maximum, 1) + } + XCTAssertTrue( + materializedRelationships.isEmpty, + "Aggregate counts must pass before either relationship fires" + ) + XCTAssertEqual( + try durableStoreFamilySnapshot(at: storeURL), + before + ) + } + + func testPerformMigration_whenProtectedValueExceedsByteLimit_thenFailsClosedWithoutReplacingStore() throws { + let sourceModel = try model(for: .version7) + try createStore(at: storeURL, model: sourceModel) { + context in + let contact = try self.insert( + entityName: "CDContact", + in: context, + model: sourceModel + ) + contact.setValue("too-long", forKey: "name") + } + let before = try durableStoreFamilySnapshot(at: storeURL) + let migrator = makeMigrator( + protectedDataInspectionLimits: + makeProtectedDataLimits(maximumValueByteCount: 4) + ) + + XCTAssertThrowsError(try migrator.performMigration()) { error in + guard + let migrationError = + error as? SubstrateStorageMigrationError, + case let .sourceStoreInspectionFailed( + _, + underlyingError + ) = migrationError, + let inspectionError = + underlyingError as? + SubstrateProtectedDataInspectionError, + case let .valueByteLimitExceeded( + entityName, + attributeName, + actual, + maximum + ) = inspectionError + else { + return XCTFail( + "Expected valueByteLimitExceeded, got \(error)" + ) + } + + XCTAssertEqual(entityName, "CDContact") + XCTAssertEqual(attributeName, "name") + XCTAssertEqual(actual, 8) + XCTAssertEqual(maximum, 4) + } + XCTAssertEqual( + try durableStoreFamilySnapshot(at: storeURL), + before + ) + } + + func testPerformMigration_whenBoundedRelationshipMemberExceedsByteLimit_thenFailsBeforeReplacement() throws { + let sourceModel = try model(for: .version7) + try createStore(at: storeURL, model: sourceModel) { + context in + let (_, node) = try self.insertChainWithDefaultNode( + in: context, + model: sourceModel + ) + node.setValue("too-long", forKey: "name") + } + let before = try durableStoreFamilySnapshot(at: storeURL) + var replacementAttempted = false + var materializedRelationships = + [(String, String, Int)]() + let migrator = makeMigrator( + storeReplacer: { _, _ in + replacementAttempted = true + }, + protectedDataInspectionLimits: + makeProtectedDataLimits(maximumValueByteCount: 4), + protectedRelationshipWillMaterialize: { + materializedRelationships.append(($0, $1, $2)) + } + ) + + XCTAssertThrowsError(try migrator.performMigration()) { error in + guard + let migrationError = + error as? SubstrateStorageMigrationError, + case let .sourceStoreInspectionFailed( + _, + underlyingError + ) = migrationError, + let inspectionError = + underlyingError as? + SubstrateProtectedDataInspectionError, + case let .valueByteLimitExceeded( + entityName, + attributeName, + actual, + maximum + ) = inspectionError + else { + return XCTFail( + "Expected relationship-member byte rejection, got \(error)" + ) + } + + XCTAssertEqual(entityName, "CDChainNode") + XCTAssertEqual(attributeName, "name") + XCTAssertEqual(actual, 8) + XCTAssertEqual(maximum, 4) + } + XCTAssertTrue( + materializedRelationships.contains { + $0.0 == "CDChain" && + $0.1 == "nodes" && + $0.2 == 1 + } + ) + XCTAssertFalse(replacementAttempted) + XCTAssertEqual( + try durableStoreFamilySnapshot(at: storeURL), + before + ) + } + + func testPerformMigration_whenProtectedRowsSpanSingleItemPages_thenDigestPreservesEveryRecord() throws { + let sourceModel = try model(for: .version7) + try createStore(at: storeURL, model: sourceModel) { + context in + for index in 0 ..< 3 { + let contact = try self.insert( + entityName: "CDContact", + in: context, + model: sourceModel + ) + contact.setValue( + "address-\(index)", + forKey: "address" + ) + contact.setValue( + "name-\(index)", + forKey: "name" + ) + } + } + let migrator = makeMigrator( + protectedDataInspectionLimits: + makeProtectedDataLimits(fetchPageSize: 1) + ) + + try migrator.performMigration() + + XCTAssertEqual( + try count( + entityName: "CDContact", + at: storeURL, + model: try model(for: .version8) + ), + 3 + ) + } + + func testPerformMigration_whenLegacyAssetHasNoSymbolAndWrapperHasNoAsset_thenPreservesGraph() throws { + let sourceModel = try model(for: .version3) + try createStore(at: storeURL, model: sourceModel) { context in + try self.insertLegacyChainGraph( + in: context, + model: sourceModel, + symbol: nil, + includeNilAssetWrapper: true + ) + } + + try makeMigrator().performMigration() + + let targetModel = try model(for: .version8) + XCTAssertEqual( + try count(entityName: "CDAsset", at: storeURL, model: targetModel), + 1 + ) + + try inspectStore(at: storeURL, model: targetModel) { context in + let chain = try XCTUnwrap( + try context.fetch( + NSFetchRequest(entityName: "CDChain") + ).first + ) + let assets = try XCTUnwrap( + chain.value(forKey: "assets") as? Set + ) + let asset = try XCTUnwrap(assets.first) + + XCTAssertEqual(assets.count, 1) + XCTAssertEqual(asset.value(forKey: "id") as? String, "migration-asset") + XCTAssertNil(asset.value(forKey: "symbol")) + } + } + + func testChainV4Policy_whenSourceAssetHasNoDestinationAssociation_thenMigrationFails() throws { + let sourceModel = try model(for: .version3) + let destinationModel = try model(for: .version4) + try createStore(at: storeURL, model: sourceModel) { context in + try self.insertLegacyChainGraph(in: context, model: sourceModel) + } + + let mappingURL = try XCTUnwrap( + appBundle.url( + forResource: "SubstrateV3toV4", + withExtension: "cdm" + ) + ) + let mappingModel = try XCTUnwrap(NSMappingModel(contentsOf: mappingURL)) + let assetMapping = try XCTUnwrap( + mappingModel.entityMappings.first { + $0.sourceEntityName == "CDAsset" && + $0.destinationEntityName == "CDAsset" + } + ) + assetMapping.entityMigrationPolicyClassName = NSStringFromClass( + DroppingAssetMigrationPolicy.self + ) + + let manager = NSMigrationManager( + sourceModel: sourceModel, + destinationModel: destinationModel + ) + let destinationURL = testDirectory.appendingPathComponent( + "MissingAssetAssociation.sqlite" + ) + + XCTAssertThrowsError( + try manager.migrateStore( + from: storeURL, + sourceType: NSSQLiteStoreType, + options: nil, + with: mappingModel, + toDestinationURL: destinationURL, + destinationType: NSSQLiteStoreType, + destinationOptions: nil + ) + ) { error in + XCTAssertTrue( + self.flattenedErrorDescriptions(error).contains { + $0.contains("Can't find migrated CDAsset destination instance") + }, + "Expected the missing destination association error, got \(error)" + ) + } + } + + func testPerformMigration_whenVersion3ContainsOrphanAsset_thenPreservesItUnattached() throws { + let sourceModel = try model(for: .version3) + try createStore(at: storeURL, model: sourceModel) { context in + try self.insertLegacyChainGraph(in: context, model: sourceModel) + try self.insertLegacyOrphanAsset(in: context, model: sourceModel) + } + + try makeMigrator().performMigration() + + let targetModel = try model(for: .version8) + try inspectStore(at: storeURL, model: targetModel) { context in + let assets = try context.fetch( + NSFetchRequest(entityName: "CDAsset") + ) + let chain = try XCTUnwrap( + try context.fetch( + NSFetchRequest(entityName: "CDChain") + ).first + ) + let attachedAssets = try XCTUnwrap( + chain.value(forKey: "assets") as? Set + ) + let orphanAsset = try XCTUnwrap( + assets.first { + $0.value(forKey: "id") as? String == "orphan-asset" + } + ) + + XCTAssertEqual(assets.count, 2) + XCTAssertEqual(attachedAssets.count, 1) + XCTAssertFalse(attachedAssets.contains(orphanAsset)) + XCTAssertNil(orphanAsset.value(forKey: "chain")) + XCTAssertEqual(orphanAsset.value(forKey: "symbol") as? String, "ORP") + } + } + + func testPerformMigration_whenRequiredCustomMappingIsMissing_thenThrowsBeforeCheckpoint() throws { + let sourceModel = try model(for: .version3) + try createStore(at: storeURL, model: sourceModel) { context in + try self.insertRuntimeItem( + identifier: "required-mapping", + in: context, + model: sourceModel + ) + } + let before = try durableStoreFamilySnapshot(at: storeURL) + let modelOnlyBundle = try makeModelBundle() + let migrator = makeMigrator(modelBundle: modelOnlyBundle) + + XCTAssertThrowsError(try migrator.performMigration()) { error in + guard + let migrationError = error as? SubstrateStorageMigrationError, + case let .mappingUnavailable(source, destination, _) = migrationError + else { + return XCTFail("Expected mappingUnavailable, got \(error)") + } + + XCTAssertEqual(source, .version3) + XCTAssertEqual(destination, .version4) + } + + XCTAssertEqual(try durableStoreFamilySnapshot(at: storeURL), before) + } + + func testBundledRequiredCustomMappings_matchCurrentModelsAndPolicies() throws { + let cases: [ + ( + source: SubstrateStorageVersion, + destination: SubstrateStorageVersion, + resourceName: String, + policyClassName: String + ) + ] = [ + ( + .version1, + .version2, + "SubstrateV2Mapping", + NSStringFromClass(ChainSubstrateV2MigrationPolicy.self) + ), + ( + .version3, + .version4, + "SubstrateV3toV4", + NSStringFromClass(ChainModelV4MigrationPolicy.self) + ) + ] + + for testCase in cases { + let sourceModel = try model(for: testCase.source) + let destinationModel = try model(for: testCase.destination) + let mappingURL = try XCTUnwrap( + appBundle.url( + forResource: testCase.resourceName, + withExtension: "cdm" + ) + ) + let namedMapping = try XCTUnwrap(NSMappingModel(contentsOf: mappingURL)) + let selectedMapping = NSMappingModel( + from: [appBundle], + forSourceModel: sourceModel, + destinationModel: destinationModel + ) + + XCTAssertNotNil( + selectedMapping, + "\(testCase.resourceName) is not compatible with its current models" + ) + + for entityMapping in namedMapping.entityMappings { + if let sourceEntityName = entityMapping.sourceEntityName { + XCTAssertEqual( + entityMapping.sourceEntityVersionHash, + sourceModel.entitiesByName[sourceEntityName]?.versionHash, + "\(testCase.resourceName) has a stale \(sourceEntityName) source hash" + ) + } + + if let destinationEntityName = entityMapping.destinationEntityName { + XCTAssertEqual( + entityMapping.destinationEntityVersionHash, + destinationModel.entitiesByName[destinationEntityName]?.versionHash, + "\(testCase.resourceName) has a stale \(destinationEntityName) destination hash" + ) + } + } + + let chainMapping = try XCTUnwrap( + namedMapping.entityMappings.first { + $0.sourceEntityName == "CDChain" && + $0.destinationEntityName == "CDChain" + } + ) + XCTAssertEqual( + chainMapping.entityMigrationPolicyClassName, + testCase.policyClassName + ) + } + } + + func testSyncMigration_whenKnownStoreHasZeroProtectedFootprint_thenQuarantinesOnlySubstrateFamily() throws { + let sourceModel = try model(for: .version3) + try createStore(at: storeURL, model: sourceModel) { context in + try self.insertRuntimeItem( + identifier: "rebuildable-runtime", + in: context, + model: sourceModel + ) + _ = try self.insertChainWithDefaultNode( + in: context, + model: sourceModel + ) + } + let substrateBefore = try durableStoreFamilySnapshot(at: storeURL) + + let userStoreURL = testDirectory.appendingPathComponent("UserDataModel.sqlite") + let userWALURL = URL(fileURLWithPath: userStoreURL.path + "-wal") + let userSHMURL = URL(fileURLWithPath: userStoreURL.path + "-shm") + let userJournalURL = URL(fileURLWithPath: userStoreURL.path + "-journal") + let userStoreBytes = Data("wallet-store-must-not-move".utf8) + let userWALBytes = Data("wallet-wal-must-not-move".utf8) + let userSHMBytes = Data("wallet-shm-must-not-move".utf8) + let userJournalBytes = Data("wallet-journal-must-not-move".utf8) + try userStoreBytes.write(to: userStoreURL, options: .atomic) + try userWALBytes.write(to: userWALURL, options: .atomic) + try userSHMBytes.write(to: userSHMURL, options: .atomic) + try userJournalBytes.write(to: userJournalURL, options: .atomic) + + let modelOnlyBundle = try makeModelBundle() + let migrator = makeMigrator(modelBundle: modelOnlyBundle) + + XCTAssertThrowsError(try migrator.performMigration()) { error in + guard + let migrationError = error as? SubstrateStorageMigrationError, + case let .mappingUnavailable(source, destination, _) = migrationError + else { + return XCTFail("Expected mappingUnavailable, got \(error)") + } + + XCTAssertEqual(source, .version3) + XCTAssertEqual(destination, .version4) + } + + try migrator.migrate() + + XCTAssertFalse(FileManager.default.fileExists(atPath: storeURL.path)) + XCTAssertFalse(migrator.requiresMigration()) + XCTAssertEqual(try Data(contentsOf: userStoreURL), userStoreBytes) + XCTAssertEqual(try Data(contentsOf: userWALURL), userWALBytes) + XCTAssertEqual(try Data(contentsOf: userSHMURL), userSHMBytes) + XCTAssertEqual(try Data(contentsOf: userJournalURL), userJournalBytes) + + let recoveryDirectory = try XCTUnwrap(try recoveryDirectories().first) + let recoveredStoreURL = recoveryDirectory.appendingPathComponent( + storeURL.lastPathComponent + ) + XCTAssertEqual( + try durableStoreFamilySnapshot(at: recoveredStoreURL), + substrateBefore + ) + + try migrator.migrate() + XCTAssertEqual(try recoveryDirectories(), [recoveryDirectory]) + XCTAssertEqual(try Data(contentsOf: userStoreURL), userStoreBytes) + XCTAssertEqual(try Data(contentsOf: userWALURL), userWALBytes) + XCTAssertEqual(try Data(contentsOf: userSHMURL), userSHMBytes) + XCTAssertEqual(try Data(contentsOf: userJournalURL), userJournalBytes) + } + + func testSyncMigration_whenCheckpointFailsWithZeroProtectedData_thenQuarantinesExactSourceFamily() throws { + let sourceModel = try model(for: .version3) + try createStore(at: storeURL, model: sourceModel) { context in + try self.insertRuntimeItem( + identifier: "checkpoint-rebuildable-runtime", + in: context, + model: sourceModel + ) + } + let before = try durableStoreFamilySnapshot(at: storeURL) + let migrator = makeMigrator( + checkpointStoreHook: { _, _ in + throw InjectedSubstrateCheckpointError.failure + } + ) + + try migrator.migrate() + + XCTAssertFalse(FileManager.default.fileExists(atPath: storeURL.path)) + let recoveryDirectory = try XCTUnwrap(try recoveryDirectories().first) + let recoveredStoreURL = recoveryDirectory.appendingPathComponent( + storeURL.lastPathComponent + ) + XCTAssertEqual( + try durableStoreFamilySnapshot(at: recoveredStoreURL), + before + ) + } + + func testSyncMigration_whenCheckpointFailsWithProtectedData_thenBlocksRecoveryAndPreservesSource() throws { + let sourceModel = try model(for: .version3) + try createStore(at: storeURL, model: sourceModel) { context in + let contact = try self.insert( + entityName: "CDContact", + in: context, + model: sourceModel + ) + contact.setValue("Checkpoint User", forKey: "name") + contact.setValue("5CheckpointProtected", forKey: "address") + contact.setValue("checkpoint-chain", forKey: "chainId") + } + let before = try durableStoreFamilySnapshot(at: storeURL) + var checkpointCount = 0 + let migrator = makeMigrator( + checkpointStoreHook: { _, _ in + checkpointCount += 1 + throw InjectedSubstrateCheckpointError.failure + } + ) + + XCTAssertThrowsError(try migrator.migrate()) { error in + guard + let migrationError = error as? SubstrateStorageMigrationError, + case let .cacheRecoveryBlockedByProtectedData( + blockedURL, + originalError, + counts + ) = migrationError + else { + return XCTFail("Expected cacheRecoveryBlockedByProtectedData, got \(error)") + } + + XCTAssertEqual(blockedURL, self.storeURL) + XCTAssertEqual( + counts.filter { $0.value > 0 }, + ["CDContact": 1] + ) + guard + let originalMigrationError = + originalError as? SubstrateStorageMigrationError, + case .walCheckpointFailed = originalMigrationError + else { + return XCTFail("Expected walCheckpointFailed, got \(originalError)") + } + } + + XCTAssertEqual(checkpointCount, 1) + XCTAssertEqual(try durableStoreFamilySnapshot(at: storeURL), before) + XCTAssertTrue(try recoveryDirectories().isEmpty) + } + + func testSyncMigration_whenCheckpointFails_thenFinalRecoveryInspectionNeverCheckpointsAgain() throws { + let sourceModel = try model(for: .version7) + try createStore(at: storeURL, model: sourceModel) { context in + try self.insertRuntimeItem( + identifier: "single-checkpoint-runtime", + in: context, + model: sourceModel + ) + } + var checkpointCount = 0 + let migrator = makeMigrator( + checkpointStoreHook: { _, _ in + checkpointCount += 1 + throw InjectedSubstrateCheckpointError.failure + } + ) + + try migrator.migrate() + + XCTAssertEqual( + checkpointCount, + 1, + "The final protected-data inspection must not re-enter checkpointing" + ) + XCTAssertFalse(FileManager.default.fileExists(atPath: storeURL.path)) + XCTAssertEqual(try recoveryDirectories().count, 1) + } + + func testCacheRecoveryTransaction_whenInterruptedAtEveryDurableBoundary_thenRelaunchConvergesExactly() throws { + let boundaries: [SubstrateCacheRecoveryBoundary] = [ + .markerPersisted, + .familyMemberMoved(""), + .familyMemberMoved("-wal"), + .familyMemberMoved("-shm"), + .familyMemberMoved("-journal"), + .markerRemoved + ] + + for (index, boundary) in boundaries.enumerated() { + let caseDirectory = testDirectory.appendingPathComponent( + "interruption-\(index)", + isDirectory: true + ) + try FileManager.default.createDirectory( + at: caseDirectory, + withIntermediateDirectories: false + ) + let caseStoreURL = caseDirectory.appendingPathComponent( + storeURL.lastPathComponent + ) + let expectedFamily = try writeSyntheticStoreFamily( + at: caseStoreURL, + discriminator: UInt8(index) + ) + var interruptionWasInjected = false + let interruptedTransaction = + CrashConsistentSubstrateCacheRecovery( + storeURL: caseStoreURL, + fileManager: .default, + boundaryHook: { reachedBoundary in + guard + reachedBoundary == boundary, + !interruptionWasInjected + else { + return + } + + interruptionWasInjected = true + throw SubstrateCacheRecoveryInterruption + .simulatedProcessDeath + } + ) + + XCTAssertThrowsError( + try interruptedTransaction.quarantine(), + "Expected simulated process death at \(boundary)" + ) { error in + guard error is SubstrateCacheRecoveryInterruption else { + return XCTFail( + "Expected cache-recovery interruption, got \(error)" + ) + } + } + XCTAssertTrue(interruptionWasInjected) + + let relaunchedTransaction = + CrashConsistentSubstrateCacheRecovery( + storeURL: caseStoreURL, + fileManager: .default + ) + _ = try relaunchedTransaction.reconcile() + + XCTAssertTrue( + try durableStoreFamilySnapshot( + at: caseStoreURL + ).isEmpty, + "Live family survived relaunch after \(boundary)" + ) + let recoveryDirectories = try contents( + of: relaunchedTransaction.recoveryRootURL + ) + let recoveryDirectory = try XCTUnwrap( + recoveryDirectories.first + ) + XCTAssertEqual(recoveryDirectories.count, 1) + XCTAssertEqual( + try durableStoreFamilySnapshot( + at: recoveryDirectory.appendingPathComponent( + caseStoreURL.lastPathComponent + ) + ), + expectedFamily, + "Quarantine changed bytes after \(boundary)" + ) + XCTAssertFalse( + FileManager.default.fileExists( + atPath: recoveryDirectory + .appendingPathComponent( + "recovery-transaction" + ).path + ) + ) + + XCTAssertNil(try relaunchedTransaction.reconcile()) + XCTAssertEqual( + try contents( + of: relaunchedTransaction.recoveryRootURL + ), + recoveryDirectories + ) + } + } + + func testCacheRecoveryTransaction_whenPreparationIsMarkerlessOrOnlyPending_thenRelaunchCleansItWithoutMovingLiveFamily() throws { + for containsPendingMarker in [false, true] { + let caseDirectory = testDirectory.appendingPathComponent( + containsPendingMarker ? "pending-marker" : "empty-marker", + isDirectory: true + ) + try FileManager.default.createDirectory( + at: caseDirectory, + withIntermediateDirectories: false + ) + let caseStoreURL = caseDirectory.appendingPathComponent( + storeURL.lastPathComponent + ) + let expectedFamily = try writeSyntheticStoreFamily( + at: caseStoreURL, + discriminator: containsPendingMarker ? 0xB1 : 0xB0 + ) + let transaction = CrashConsistentSubstrateCacheRecovery( + storeURL: caseStoreURL, + fileManager: .default + ) + let preparationURL = transaction.recoveryRootURL + .appendingPathComponent( + UUID().uuidString, + isDirectory: true + ) + try FileManager.default.createDirectory( + at: preparationURL, + withIntermediateDirectories: true + ) + if containsPendingMarker { + try Data("partial marker".utf8).write( + to: preparationURL.appendingPathComponent( + "recovery-transaction.pending" + ) + ) + } + + XCTAssertNil(try transaction.reconcile()) + XCTAssertEqual( + try durableStoreFamilySnapshot(at: caseStoreURL), + expectedFamily + ) + XCTAssertFalse( + FileManager.default.fileExists( + atPath: transaction.recoveryRootURL.path + ) + ) + } + } + + func testPerformMigrationWithRecovery_whenCacheQuarantineLostMainStore_thenReconcilesBeforeLiveStoreSafetyCheck() throws { + let sourceModel = try model(for: .version3) + try createStore(at: storeURL, model: sourceModel) { context in + try self.insertRuntimeItem( + identifier: "interrupted-recovery-runtime", + in: context, + model: sourceModel + ) + } + try Data("recovery-wal".utf8).write( + to: URL(fileURLWithPath: storeURL.path + "-wal") + ) + try Data("recovery-shm".utf8).write( + to: URL(fileURLWithPath: storeURL.path + "-shm") + ) + let before = try durableStoreFamilySnapshot(at: storeURL) + let interruptedTransaction = + CrashConsistentSubstrateCacheRecovery( + storeURL: storeURL, + fileManager: .default, + boundaryHook: { boundary in + guard boundary == .familyMemberMoved("") else { + return + } + throw SubstrateCacheRecoveryInterruption + .simulatedProcessDeath + } + ) + + XCTAssertThrowsError( + try interruptedTransaction.quarantine() + ) + XCTAssertFalse( + FileManager.default.fileExists(atPath: storeURL.path) + ) + XCTAssertTrue( + FileManager.default.fileExists( + atPath: storeURL.path + "-wal" + ) + ) + + let migrator = makeMigrator() + XCTAssertNil(try migrator.performMigrationWithRecovery()) + XCTAssertTrue( + try durableStoreFamilySnapshot(at: storeURL).isEmpty + ) + let recoveryDirectory = try XCTUnwrap( + try recoveryDirectories().first + ) + XCTAssertEqual( + try durableStoreFamilySnapshot( + at: recoveryDirectory.appendingPathComponent( + storeURL.lastPathComponent + ) + ), + before + ) + } + + func testPerformMigration_whenDeployedLegacyRecoveryMovedRealSQLiteMain_thenRestoresBeforeStoreSafetyAndPreservesRowsAndWAL() throws { + let currentModel = try model(for: .version8) + try createStore(at: storeURL, model: currentModel) { + context in + try self.insertRuntimeItem( + identifier: "legacy-real-sqlite-runtime", + in: context, + model: currentModel + ) + } + try installRealSQLiteFamilyWithUncheckpointedWAL( + at: storeURL + ) + let before = try durableStoreFamilySnapshot(at: storeURL) + XCTAssertNotNil(before[storeURL.lastPathComponent + "-wal"]) + XCTAssertNotNil(before[storeURL.lastPathComponent + "-shm"]) + + let recoveryRootURL = testDirectory.appendingPathComponent( + "SubstrateStoreRecovery", + isDirectory: true + ) + let legacyArchiveURL = recoveryRootURL.appendingPathComponent( + UUID().uuidString, + isDirectory: true + ) + try FileManager.default.createDirectory( + at: legacyArchiveURL, + withIntermediateDirectories: true + ) + try FileManager.default.moveItem( + at: storeURL, + to: legacyArchiveURL.appendingPathComponent( + storeURL.lastPathComponent + ) + ) + XCTAssertFalse( + FileManager.default.fileExists(atPath: storeURL.path) + ) + XCTAssertTrue( + FileManager.default.fileExists( + atPath: storeURL.path + "-wal" + ) + ) + + try makeMigrator().performMigration() + + XCTAssertEqual( + try durableStoreFamilySnapshot(at: storeURL), + before + ) + XCTAssertEqual( + try Data( + contentsOf: legacyArchiveURL.appendingPathComponent( + storeURL.lastPathComponent + ) + ), + before[storeURL.lastPathComponent] + ) + XCTAssertFalse( + FileManager.default.fileExists( + atPath: legacyArchiveURL.appendingPathComponent( + "recovery-transaction" + ).path + ) + ) + XCTAssertEqual( + try count( + entityName: "CDRuntimeMetadataItem", + at: storeURL, + model: currentModel + ), + 1 + ) + XCTAssertEqual( + try sqliteInteger( + "SELECT COUNT(*) FROM ZFEARLESSRECOVERYPROBE", + at: storeURL + ), + 1 + ) + } + + func testCacheRecoveryTransaction_whenActiveTreeContainsUnknownFile_thenFailsClosedWithoutMovingFamily() throws { + let expectedFamily = try writeSyntheticStoreFamily( + at: storeURL, + discriminator: 0xC1 + ) + let transactionDirectory = try interruptCacheRecovery( + at: .markerPersisted + ) + let unexpectedURL = transactionDirectory + .appendingPathComponent("unexpected") + let unexpectedData = Data("must remain".utf8) + try unexpectedData.write(to: unexpectedURL) + + let relaunchedTransaction = + CrashConsistentSubstrateCacheRecovery( + storeURL: storeURL, + fileManager: .default + ) + XCTAssertThrowsError(try relaunchedTransaction.reconcile()) { + error in + guard + let transactionError = + error as? SubstrateCacheRecoveryTransactionError, + case .unsafeDirectory = transactionError + else { + return XCTFail("Expected unsafeDirectory, got \(error)") + } + } + + XCTAssertEqual( + try durableStoreFamilySnapshot(at: storeURL), + expectedFamily + ) + XCTAssertEqual(try Data(contentsOf: unexpectedURL), unexpectedData) + XCTAssertTrue( + FileManager.default.fileExists( + atPath: transactionDirectory + .appendingPathComponent( + "recovery-transaction" + ).path + ) + ) + } + + func testCacheRecoveryTransaction_whenLiveFamilyChangesAfterMarker_thenFailsClosedWithoutMovingAnything() throws { + _ = try writeSyntheticStoreFamily( + at: storeURL, + discriminator: 0xC2 + ) + let transactionDirectory = try interruptCacheRecovery( + at: .markerPersisted + ) + let changedData = Data("externally changed main store".utf8) + try changedData.write(to: storeURL) + + let relaunchedTransaction = + CrashConsistentSubstrateCacheRecovery( + storeURL: storeURL, + fileManager: .default + ) + XCTAssertThrowsError(try relaunchedTransaction.reconcile()) { + error in + guard + let transactionError = + error as? SubstrateCacheRecoveryTransactionError, + case .familyMismatch = transactionError + else { + return XCTFail("Expected familyMismatch, got \(error)") + } + } + + XCTAssertEqual(try Data(contentsOf: storeURL), changedData) + XCTAssertTrue( + FileManager.default.fileExists( + atPath: storeURL.path + "-wal" + ) + ) + XCTAssertTrue( + FileManager.default.fileExists( + atPath: transactionDirectory + .appendingPathComponent( + "recovery-transaction" + ).path + ) + ) + } + + func testCacheRecoveryTransaction_whenMovedMemberChanges_thenFailsClosedAndKeepsDurableMarker() throws { + _ = try writeSyntheticStoreFamily( + at: storeURL, + discriminator: 0xC3 + ) + let transactionDirectory = try interruptCacheRecovery( + at: .familyMemberMoved("") + ) + let quarantinedStoreURL = transactionDirectory + .appendingPathComponent(storeURL.lastPathComponent) + let changedData = Data("changed quarantine member".utf8) + try changedData.write(to: quarantinedStoreURL) + + let relaunchedTransaction = + CrashConsistentSubstrateCacheRecovery( + storeURL: storeURL, + fileManager: .default + ) + XCTAssertThrowsError(try relaunchedTransaction.reconcile()) { + error in + guard + let transactionError = + error as? SubstrateCacheRecoveryTransactionError, + case .familyMismatch = transactionError + else { + return XCTFail("Expected familyMismatch, got \(error)") + } + } + + XCTAssertEqual( + try Data(contentsOf: quarantinedStoreURL), + changedData + ) + XCTAssertTrue( + FileManager.default.fileExists( + atPath: storeURL.path + "-wal" + ) + ) + XCTAssertTrue( + FileManager.default.fileExists( + atPath: transactionDirectory + .appendingPathComponent( + "recovery-transaction" + ).path + ) + ) + } + + func testCacheRecoveryTransaction_whenSourceContainsSymlinkOrHardlink_thenRejectsBeforeCreatingMarker() throws { + let outsideURL = testDirectory.appendingPathComponent("outside") + let outsideData = Data("outside-must-remain".utf8) + try outsideData.write(to: outsideURL) + + let symlinkDirectory = testDirectory.appendingPathComponent( + "symlink-family", + isDirectory: true + ) + try FileManager.default.createDirectory( + at: symlinkDirectory, + withIntermediateDirectories: false + ) + let symlinkStoreURL = symlinkDirectory.appendingPathComponent( + storeURL.lastPathComponent + ) + try Data("main".utf8).write(to: symlinkStoreURL) + try FileManager.default.createSymbolicLink( + atPath: symlinkStoreURL.path + "-wal", + withDestinationPath: outsideURL.path + ) + + let symlinkTransaction = + CrashConsistentSubstrateCacheRecovery( + storeURL: symlinkStoreURL, + fileManager: .default + ) + XCTAssertThrowsError(try symlinkTransaction.quarantine()) { + error in + guard + let transactionError = + error as? SubstrateCacheRecoveryTransactionError, + case .unsafeFile = transactionError + else { + return XCTFail("Expected unsafeFile, got \(error)") + } + } + XCTAssertFalse( + FileManager.default.fileExists( + atPath: symlinkTransaction.recoveryRootURL.path + ) + ) + + let hardlinkDirectory = testDirectory.appendingPathComponent( + "hardlink-family", + isDirectory: true + ) + try FileManager.default.createDirectory( + at: hardlinkDirectory, + withIntermediateDirectories: false + ) + let hardlinkStoreURL = hardlinkDirectory.appendingPathComponent( + storeURL.lastPathComponent + ) + try Data("hardlinked".utf8).write(to: hardlinkStoreURL) + try FileManager.default.linkItem( + at: hardlinkStoreURL, + to: URL(fileURLWithPath: hardlinkStoreURL.path + "-wal") + ) + + let hardlinkTransaction = + CrashConsistentSubstrateCacheRecovery( + storeURL: hardlinkStoreURL, + fileManager: .default + ) + XCTAssertThrowsError(try hardlinkTransaction.quarantine()) { + error in + guard + let transactionError = + error as? SubstrateCacheRecoveryTransactionError, + case .unsafeFile = transactionError + else { + return XCTFail("Expected unsafeFile, got \(error)") + } + } + XCTAssertFalse( + FileManager.default.fileExists( + atPath: hardlinkTransaction.recoveryRootURL.path + ) + ) + XCTAssertEqual(try Data(contentsOf: outsideURL), outsideData) + } + + func testCacheRecoveryTransaction_whenMarkerIsNoncanonicalOrOversized_thenFailsClosedAndPreservesLiveFamily() throws { + for markerMutation in ["noncanonical", "oversized"] { + let caseDirectory = testDirectory.appendingPathComponent( + markerMutation, + isDirectory: true + ) + try FileManager.default.createDirectory( + at: caseDirectory, + withIntermediateDirectories: false + ) + let caseStoreURL = caseDirectory.appendingPathComponent( + storeURL.lastPathComponent + ) + let expectedFamily = try writeSyntheticStoreFamily( + at: caseStoreURL, + discriminator: + markerMutation == "noncanonical" ? 0xD1 : 0xD2 + ) + let transactionDirectory = try interruptCacheRecovery( + storeURL: caseStoreURL, + at: .markerPersisted + ) + let markerURL = transactionDirectory.appendingPathComponent( + "recovery-transaction" + ) + if markerMutation == "noncanonical" { + var marker = try Data(contentsOf: markerURL) + marker.append(Data("\n".utf8)) + try marker.write(to: markerURL) + } else { + try Data(repeating: 0x41, count: 4 * 1024 + 1) + .write(to: markerURL) + } + + let relaunchedTransaction = + CrashConsistentSubstrateCacheRecovery( + storeURL: caseStoreURL, + fileManager: .default + ) + XCTAssertThrowsError( + try relaunchedTransaction.reconcile() + ) { error in + guard + let transactionError = + error as? + SubstrateCacheRecoveryTransactionError + else { + return XCTFail( + "Expected recovery transaction error, got \(error)" + ) + } + + if markerMutation == "noncanonical" { + guard case .invalidMarker = transactionError else { + return XCTFail( + "Expected invalidMarker, got \(error)" + ) + } + } else { + guard case .markerTooLarge = transactionError else { + return XCTFail( + "Expected markerTooLarge, got \(error)" + ) + } + } + } + XCTAssertEqual( + try durableStoreFamilySnapshot(at: caseStoreURL), + expectedFamily + ) + } + } + + func testCacheRecoveryTransaction_whenLegacyMarkerlessMoveStoppedAtEveryBoundary_thenAdoptsDurablyAndConverges() throws { + let suffixes = ["", "-wal", "-shm", "-journal"] + + for movedMemberCount in 1 ... suffixes.count { + let caseDirectory = testDirectory.appendingPathComponent( + "legacy-boundary-\(movedMemberCount)", + isDirectory: true + ) + try FileManager.default.createDirectory( + at: caseDirectory, + withIntermediateDirectories: false + ) + let caseStoreURL = caseDirectory.appendingPathComponent( + storeURL.lastPathComponent + ) + let expectedFamily = try writeSyntheticStoreFamily( + at: caseStoreURL, + discriminator: UInt8(0xE0 + movedMemberCount) + ) + let transaction = CrashConsistentSubstrateCacheRecovery( + storeURL: caseStoreURL, + fileManager: .default + ) + let legacyArchiveURL = transaction.recoveryRootURL + .appendingPathComponent( + UUID().uuidString, + isDirectory: true + ) + try FileManager.default.createDirectory( + at: legacyArchiveURL, + withIntermediateDirectories: true + ) + + for suffix in suffixes.prefix(movedMemberCount) { + let sourceURL = URL( + fileURLWithPath: caseStoreURL.path + suffix + ) + try FileManager.default.moveItem( + at: sourceURL, + to: legacyArchiveURL.appendingPathComponent( + sourceURL.lastPathComponent + ) + ) + } + + _ = try transaction.reconcile() + + let archivedStoreURL = legacyArchiveURL + .appendingPathComponent( + caseStoreURL.lastPathComponent + ) + if movedMemberCount == suffixes.count { + XCTAssertTrue( + try durableStoreFamilySnapshot( + at: caseStoreURL + ).isEmpty + ) + XCTAssertEqual( + try durableStoreFamilySnapshot( + at: archivedStoreURL + ), + expectedFamily + ) + } else { + XCTAssertEqual( + try durableStoreFamilySnapshot( + at: caseStoreURL + ), + expectedFamily, + "Legacy split \(movedMemberCount) was not restored" + ) + XCTAssertEqual( + Set( + try durableStoreFamilySnapshot( + at: archivedStoreURL + ).keys + ), + Set( + suffixes.prefix(movedMemberCount).map { + caseStoreURL.lastPathComponent + $0 + } + ) + ) + } + XCTAssertFalse( + FileManager.default.fileExists( + atPath: legacyArchiveURL + .appendingPathComponent( + "recovery-transaction" + ).path + ) + ) + XCTAssertNil(try transaction.reconcile()) + } + } + + func testCacheRecoveryTransaction_whenLegacyAdoptionMarkerWriteWasInterrupted_thenRemovesOnlyPendingMarkerAndConverges() throws { + let expectedFamily = try writeSyntheticStoreFamily( + at: storeURL, + discriminator: 0xE9 + ) + let transaction = CrashConsistentSubstrateCacheRecovery( + storeURL: storeURL, + fileManager: .default + ) + let legacyArchiveURL = transaction.recoveryRootURL + .appendingPathComponent( + UUID().uuidString, + isDirectory: true + ) + try FileManager.default.createDirectory( + at: legacyArchiveURL, + withIntermediateDirectories: true + ) + let mainSourceURL = storeURL + try FileManager.default.moveItem( + at: mainSourceURL, + to: legacyArchiveURL.appendingPathComponent( + mainSourceURL.lastPathComponent + ) + ) + try Data("partial durable adoption marker".utf8).write( + to: legacyArchiveURL.appendingPathComponent( + "recovery-transaction.pending" + ) + ) + + _ = try transaction.reconcile() + + XCTAssertEqual( + try durableStoreFamilySnapshot(at: storeURL), + expectedFamily + ) + XCTAssertEqual( + Set( + try durableStoreFamilySnapshot( + at: legacyArchiveURL.appendingPathComponent( + storeURL.lastPathComponent + ) + ).keys + ), + [storeURL.lastPathComponent] + ) + XCTAssertFalse( + FileManager.default.fileExists( + atPath: legacyArchiveURL + .appendingPathComponent( + "recovery-transaction.pending" + ).path + ) + ) + } + + func testCacheRecoveryTransaction_whenLegacyRestorationIsInterruptedAtEveryDurableBoundary_thenRelaunchConverges() throws { + let boundaries: [SubstrateCacheRecoveryBoundary] = [ + .markerPersisted, + .legacyFamilyMemberRestored(""), + .legacyFamilyMemberRestored("-wal"), + .markerRemoved + ] + + for (index, boundary) in boundaries.enumerated() { + let caseDirectory = testDirectory.appendingPathComponent( + "legacy-restore-interruption-\(index)", + isDirectory: true + ) + try FileManager.default.createDirectory( + at: caseDirectory, + withIntermediateDirectories: false + ) + let caseStoreURL = caseDirectory.appendingPathComponent( + storeURL.lastPathComponent + ) + let expectedFamily = try writeSyntheticStoreFamily( + at: caseStoreURL, + discriminator: UInt8(0xF0 + index) + ) + let recoveryRootURL = caseDirectory.appendingPathComponent( + "SubstrateStoreRecovery", + isDirectory: true + ) + let archiveURL = recoveryRootURL.appendingPathComponent( + UUID().uuidString, + isDirectory: true + ) + try FileManager.default.createDirectory( + at: archiveURL, + withIntermediateDirectories: true + ) + for suffix in ["", "-wal"] { + let sourceURL = URL( + fileURLWithPath: caseStoreURL.path + suffix + ) + try FileManager.default.moveItem( + at: sourceURL, + to: archiveURL.appendingPathComponent( + sourceURL.lastPathComponent + ) + ) + } + + var interruptionWasInjected = false + let interruptedTransaction = + CrashConsistentSubstrateCacheRecovery( + storeURL: caseStoreURL, + fileManager: .default, + boundaryHook: { reachedBoundary in + guard + reachedBoundary == boundary, + !interruptionWasInjected + else { + return + } + interruptionWasInjected = true + throw SubstrateCacheRecoveryInterruption + .simulatedProcessDeath + } + ) + XCTAssertThrowsError( + try interruptedTransaction.reconcile() + ) { error in + guard error is SubstrateCacheRecoveryInterruption else { + return XCTFail( + "Expected cache-recovery interruption, got \(error)" + ) + } + } + XCTAssertTrue(interruptionWasInjected) + + let relaunchedTransaction = + CrashConsistentSubstrateCacheRecovery( + storeURL: caseStoreURL, + fileManager: .default + ) + _ = try relaunchedTransaction.reconcile() + XCTAssertEqual( + try durableStoreFamilySnapshot(at: caseStoreURL), + expectedFamily, + "Legacy restoration changed bytes after \(boundary)" + ) + XCTAssertEqual( + Set( + try durableStoreFamilySnapshot( + at: archiveURL.appendingPathComponent( + caseStoreURL.lastPathComponent + ) + ).keys + ), + [ + caseStoreURL.lastPathComponent, + caseStoreURL.lastPathComponent + "-wal" + ] + ) + XCTAssertFalse( + FileManager.default.fileExists( + atPath: archiveURL.appendingPathComponent( + "recovery-transaction" + ).path + ) + ) + XCTAssertNil(try relaunchedTransaction.reconcile()) + } + } + + func testCacheRecoveryTransaction_whenLegacyTemporaryFileSynchronizationFails_thenRelaunchResynchronizesBeforeInstall() throws { + let expectedFamily = try writeSyntheticStoreFamily( + at: storeURL, + discriminator: 0xF8 + ) + let recoveryRootURL = testDirectory.appendingPathComponent( + "SubstrateStoreRecovery", + isDirectory: true + ) + let archiveURL = recoveryRootURL.appendingPathComponent( + UUID().uuidString, + isDirectory: true + ) + try FileManager.default.createDirectory( + at: archiveURL, + withIntermediateDirectories: true + ) + try FileManager.default.moveItem( + at: storeURL, + to: archiveURL.appendingPathComponent( + storeURL.lastPathComponent + ) + ) + var synchronizationFailureWasInjected = false + let interruptedTransaction = + CrashConsistentSubstrateCacheRecovery( + storeURL: storeURL, + fileManager: .default, + restorationTemporaryFileSynchronizationHook: { + _ in + guard !synchronizationFailureWasInjected else { + return + } + synchronizationFailureWasInjected = true + throw CocoaError(.fileWriteUnknown) + } + ) + + XCTAssertThrowsError( + try interruptedTransaction.reconcile() + ) + XCTAssertTrue(synchronizationFailureWasInjected) + XCTAssertFalse( + FileManager.default.fileExists(atPath: storeURL.path) + ) + XCTAssertTrue( + FileManager.default.fileExists( + atPath: archiveURL.appendingPathComponent( + "main.cache-recovery-restore.pending" + ).path + ) + ) + XCTAssertTrue( + FileManager.default.fileExists( + atPath: archiveURL.appendingPathComponent( + "recovery-transaction" + ).path + ) + ) + + let relaunchedTransaction = + CrashConsistentSubstrateCacheRecovery( + storeURL: storeURL, + fileManager: .default + ) + _ = try relaunchedTransaction.reconcile() + XCTAssertEqual( + try durableStoreFamilySnapshot(at: storeURL), + expectedFamily + ) + XCTAssertFalse( + FileManager.default.fileExists( + atPath: archiveURL.appendingPathComponent( + "main.cache-recovery-restore.pending" + ).path + ) + ) + } + + func testCacheRecoveryTransaction_whenFinalQuarantineDirectorySynchronizationFails_thenRetryResynchronizesBothDirectoriesBeforeCommit() throws { + let expectedFamily = try writeSyntheticStoreFamily( + at: storeURL, + discriminator: 0xF9 + ) + let suffixes = ["", "-wal", "-shm", "-journal"] + let databaseDirectoryURL = storeURL.deletingLastPathComponent() + .standardizedFileURL + var synchronizationFailureWasInjected = false + var failedTransactionDirectoryURL: URL? + let interruptedTransaction = + CrashConsistentSubstrateCacheRecovery( + storeURL: storeURL, + fileManager: .default, + directorySynchronizationHook: { directoryURL in + guard + !synchronizationFailureWasInjected, + suffixes.allSatisfy({ suffix in + let liveURL = URL( + fileURLWithPath: self.storeURL.path + suffix + ) + let archivedURL = directoryURL + .appendingPathComponent( + self.storeURL.lastPathComponent + suffix + ) + return !FileManager.default.fileExists( + atPath: liveURL.path + ) && FileManager.default.fileExists( + atPath: archivedURL.path + ) + }) + else { + return + } + + synchronizationFailureWasInjected = true + failedTransactionDirectoryURL = + directoryURL.standardizedFileURL + throw CocoaError(.fileWriteUnknown) + } + ) + + XCTAssertThrowsError(try interruptedTransaction.quarantine()) { + error in + guard + let transactionError = + error as? SubstrateCacheRecoveryTransactionError, + case let .directorySynchronizationFailed( + failedURL, + _ + ) = transactionError + else { + return XCTFail( + "Expected directorySynchronizationFailed, got \(error)" + ) + } + XCTAssertEqual( + failedURL.standardizedFileURL, + failedTransactionDirectoryURL + ) + } + XCTAssertTrue(synchronizationFailureWasInjected) + let transactionDirectoryURL = try XCTUnwrap( + failedTransactionDirectoryURL + ) + XCTAssertTrue( + FileManager.default.fileExists( + atPath: transactionDirectoryURL.appendingPathComponent( + "recovery-transaction" + ).path + ) + ) + XCTAssertTrue( + try durableStoreFamilySnapshot(at: storeURL).isEmpty + ) + + var retrySynchronizationURLs: [URL] = [] + let relaunchedTransaction = + CrashConsistentSubstrateCacheRecovery( + storeURL: storeURL, + fileManager: .default, + directorySynchronizationHook: { directoryURL in + retrySynchronizationURLs.append( + directoryURL.standardizedFileURL + ) + } + ) + _ = try relaunchedTransaction.reconcile() + + XCTAssertEqual( + retrySynchronizationURLs, + [ + transactionDirectoryURL, + databaseDirectoryURL, + transactionDirectoryURL + ], + "Retry must sync destination then source before marker removal" + ) + XCTAssertEqual( + try durableStoreFamilySnapshot( + at: transactionDirectoryURL.appendingPathComponent( + storeURL.lastPathComponent + ) + ), + expectedFamily + ) + XCTAssertTrue( + try durableStoreFamilySnapshot(at: storeURL).isEmpty + ) + XCTAssertFalse( + FileManager.default.fileExists( + atPath: transactionDirectoryURL.appendingPathComponent( + "recovery-transaction" + ).path + ) + ) + } + + func testCacheRecoveryTransaction_whenFinalLegacyRestoreDirectorySynchronizationFails_thenRetryResynchronizesBothDirectoriesBeforeCommit() throws { + let expectedFamily = try writeSyntheticStoreFamily( + at: storeURL, + discriminator: 0xFA + ) + let suffixes = ["", "-wal", "-shm", "-journal"] + let databaseDirectoryURL = storeURL.deletingLastPathComponent() + .standardizedFileURL + let archiveURL = testDirectory.appendingPathComponent( + "SubstrateStoreRecovery", + isDirectory: true + ).appendingPathComponent( + UUID().uuidString, + isDirectory: true + ) + try FileManager.default.createDirectory( + at: archiveURL, + withIntermediateDirectories: true + ) + for suffix in ["", "-wal"] { + let sourceURL = URL( + fileURLWithPath: storeURL.path + suffix + ) + try FileManager.default.moveItem( + at: sourceURL, + to: archiveURL.appendingPathComponent( + sourceURL.lastPathComponent + ) + ) + } + + var synchronizationFailureWasInjected = false + let interruptedTransaction = + CrashConsistentSubstrateCacheRecovery( + storeURL: storeURL, + fileManager: .default, + directorySynchronizationHook: { directoryURL in + guard + !synchronizationFailureWasInjected, + directoryURL.standardizedFileURL == + databaseDirectoryURL, + suffixes.allSatisfy({ suffix in + FileManager.default.fileExists( + atPath: self.storeURL.path + suffix + ) + }) + else { + return + } + + synchronizationFailureWasInjected = true + throw CocoaError(.fileWriteUnknown) + } + ) + + XCTAssertThrowsError(try interruptedTransaction.reconcile()) { + error in + guard + let transactionError = + error as? SubstrateCacheRecoveryTransactionError, + case let .directorySynchronizationFailed( + failedURL, + _ + ) = transactionError + else { + return XCTFail( + "Expected directorySynchronizationFailed, got \(error)" + ) + } + XCTAssertEqual( + failedURL.standardizedFileURL, + databaseDirectoryURL + ) + } + XCTAssertTrue(synchronizationFailureWasInjected) + XCTAssertEqual( + try durableStoreFamilySnapshot(at: storeURL), + expectedFamily + ) + XCTAssertTrue( + FileManager.default.fileExists( + atPath: archiveURL.appendingPathComponent( + "recovery-transaction" + ).path + ) + ) + + var retrySynchronizationURLs: [URL] = [] + let relaunchedTransaction = + CrashConsistentSubstrateCacheRecovery( + storeURL: storeURL, + fileManager: .default, + directorySynchronizationHook: { directoryURL in + retrySynchronizationURLs.append( + directoryURL.standardizedFileURL + ) + } + ) + _ = try relaunchedTransaction.reconcile() + + XCTAssertEqual( + retrySynchronizationURLs, + [ + databaseDirectoryURL, + archiveURL.standardizedFileURL, + archiveURL.standardizedFileURL + ], + "Retry must sync destination then source before marker removal" + ) + XCTAssertEqual( + try durableStoreFamilySnapshot(at: storeURL), + expectedFamily + ) + XCTAssertEqual( + Set( + try durableStoreFamilySnapshot( + at: archiveURL.appendingPathComponent( + storeURL.lastPathComponent + ) + ).keys + ), + [ + storeURL.lastPathComponent, + storeURL.lastPathComponent + "-wal" + ] + ) + XCTAssertFalse( + FileManager.default.fileExists( + atPath: archiveURL.appendingPathComponent( + "recovery-transaction" + ).path + ) + ) + } + + func testRequiresMigration_whenCacheRecoveryMarkerIsPendingBeforeFirstMove_thenReturnsTrueForCurrentStore() throws { + let currentModel = try model(for: .version8) + try createStore(at: storeURL, model: currentModel) { context in + try self.insertRuntimeItem( + identifier: "pending-cache-recovery", + in: context, + model: currentModel + ) + } + let transaction = CrashConsistentSubstrateCacheRecovery( + storeURL: storeURL, + fileManager: .default, + boundaryHook: { boundary in + guard boundary == .markerPersisted else { + return + } + throw SubstrateCacheRecoveryInterruption + .simulatedProcessDeath + } + ) + XCTAssertThrowsError(try transaction.quarantine()) + + XCTAssertTrue(makeMigrator().requiresMigration()) + } + + func testCacheRecoveryTransaction_whenCompletedLegacyArchiveExistsBesideRebuiltLiveStore_thenLeavesBothUntouched() throws { + let liveFamily = try writeSyntheticStoreFamily( + at: storeURL, + discriminator: 0xEA + ) + let transaction = CrashConsistentSubstrateCacheRecovery( + storeURL: storeURL, + fileManager: .default + ) + let completedArchiveURL = transaction.recoveryRootURL + .appendingPathComponent( + UUID().uuidString, + isDirectory: true + ) + try FileManager.default.createDirectory( + at: completedArchiveURL, + withIntermediateDirectories: true + ) + let archivedStoreURL = completedArchiveURL + .appendingPathComponent(storeURL.lastPathComponent) + let archivedFamily = try writeSyntheticStoreFamily( + at: archivedStoreURL, + discriminator: 0xEB + ) + + XCTAssertNil(try transaction.reconcile()) + + XCTAssertEqual( + try durableStoreFamilySnapshot(at: storeURL), + liveFamily + ) + XCTAssertEqual( + try durableStoreFamilySnapshot(at: archivedStoreURL), + archivedFamily + ) + } + + func testCacheRecoveryRetention_whenQuarantineCompletes_thenExcludesRootAndArchiveFromBackup() throws { + let expectedFamily = try writeSyntheticStoreFamily( + at: storeURL, + discriminator: 0xA0 + ) + let transaction = CrashConsistentSubstrateCacheRecovery( + storeURL: storeURL, + fileManager: .default + ) + + let archiveURL = try XCTUnwrap(transaction.quarantine()) + + XCTAssertEqual( + try durableStoreFamilySnapshot( + at: archiveURL.appendingPathComponent( + storeURL.lastPathComponent + ) + ), + expectedFamily + ) + XCTAssertEqual( + try transaction.recoveryRootURL.resourceValues( + forKeys: [.isExcludedFromBackupKey] + ).isExcludedFromBackup, + true + ) + XCTAssertEqual( + try archiveURL.resourceValues( + forKeys: [.isExcludedFromBackupKey] + ).isExcludedFromBackup, + true + ) + } + + func testCacheRecoveryRetention_whenBackupExclusionFails_thenPreservesLiveFamilyBeforeMarker() throws { + let expectedFamily = try writeSyntheticStoreFamily( + at: storeURL, + discriminator: 0xA4 + ) + let transaction = CrashConsistentSubstrateCacheRecovery( + storeURL: storeURL, + fileManager: .default, + backupExclusionHook: { url in + guard + url.lastPathComponent != + "SubstrateStoreRecovery" + else { + return + } + throw InjectedSubstrateBackupExclusionError.failure + } + ) + + XCTAssertThrowsError(try transaction.quarantine()) { error in + guard + let transactionError = + error as? SubstrateCacheRecoveryTransactionError, + case .backupExclusionFailed = transactionError + else { + return XCTFail( + "Expected backupExclusionFailed, got \(error)" + ) + } + } + + XCTAssertEqual( + try durableStoreFamilySnapshot(at: storeURL), + expectedFamily + ) + XCTAssertFalse( + FileManager.default.fileExists( + atPath: transaction.recoveryRootURL.path + ) + ) + } + + func testCacheRecoveryRetention_whenCountLimitExceeded_thenDeletesOnlyOldestCompleteFamily() throws { + let liveFamily = try writeSyntheticStoreFamily( + at: storeURL, + discriminator: 0xA8 + ) + let transaction = CrashConsistentSubstrateCacheRecovery( + storeURL: storeURL, + fileManager: .default, + backupExclusionHook: { _ in }, + retainedArchiveCount: 2, + retainedArchiveByteCount: .max + ) + let oldest = try writeCompletedRecoveryArchive( + for: transaction, + identifier: "00000000-0000-0000-0000-000000000001", + discriminator: 0xB0, + completionDate: Date(timeIntervalSince1970: 1) + ) + let middle = try writeCompletedRecoveryArchive( + for: transaction, + identifier: "00000000-0000-0000-0000-000000000002", + discriminator: 0xB4, + completionDate: Date(timeIntervalSince1970: 2) + ) + let newest = try writeCompletedRecoveryArchive( + for: transaction, + identifier: "00000000-0000-0000-0000-000000000003", + discriminator: 0xB8, + completionDate: Date(timeIntervalSince1970: 3) + ) + + XCTAssertNil(try transaction.reconcile()) + + XCTAssertFalse( + FileManager.default.fileExists(atPath: oldest.url.path) + ) + XCTAssertEqual( + try durableStoreFamilySnapshot(at: middle.storeURL), + middle.family + ) + XCTAssertEqual( + try durableStoreFamilySnapshot(at: newest.storeURL), + newest.family + ) + XCTAssertEqual( + try durableStoreFamilySnapshot(at: storeURL), + liveFamily + ) + } + + func testCacheRecoveryRetention_whenByteBudgetCannotHoldArchive_thenDeletesWholeFamilyAndPreservesLiveStore() throws { + let liveFamily = try writeSyntheticStoreFamily( + at: storeURL, + discriminator: 0xBC + ) + let transaction = CrashConsistentSubstrateCacheRecovery( + storeURL: storeURL, + fileManager: .default, + backupExclusionHook: { _ in }, + retainedArchiveCount: 10, + retainedArchiveByteCount: 1 + ) + let archive = try writeCompletedRecoveryArchive( + for: transaction, + identifier: "00000000-0000-0000-0000-000000000004", + discriminator: 0xC0, + completionDate: Date(timeIntervalSince1970: 4) + ) + + XCTAssertNil(try transaction.reconcile()) + + XCTAssertFalse( + FileManager.default.fileExists(atPath: archive.url.path) + ) + XCTAssertFalse( + FileManager.default.fileExists( + atPath: transaction.recoveryRootURL.path + ) + ) + XCTAssertEqual( + try durableStoreFamilySnapshot(at: storeURL), + liveFamily + ) + } + + func testCacheRecoveryRetention_whenAnyArchiveTreeIsUnrecognized_thenFailsClosedBeforeDeletingValidArchive() throws { + let liveFamily = try writeSyntheticStoreFamily( + at: storeURL, + discriminator: 0xC4 + ) + let transaction = CrashConsistentSubstrateCacheRecovery( + storeURL: storeURL, + fileManager: .default, + backupExclusionHook: { _ in }, + retainedArchiveCount: 0, + retainedArchiveByteCount: 0 + ) + let validArchive = try writeCompletedRecoveryArchive( + for: transaction, + identifier: "00000000-0000-0000-0000-000000000005", + discriminator: 0xC8, + completionDate: Date(timeIntervalSince1970: 5) + ) + let hostileArchive = try writeCompletedRecoveryArchive( + for: transaction, + identifier: "00000000-0000-0000-0000-000000000006", + discriminator: 0xCC, + completionDate: Date(timeIntervalSince1970: 6) + ) + let hostileURL = hostileArchive.url.appendingPathComponent( + "unrecognized-user-file" + ) + let hostileData = Data("must-not-be-deleted".utf8) + try hostileData.write(to: hostileURL) + + XCTAssertThrowsError(try transaction.reconcile()) { error in + guard + let transactionError = + error as? SubstrateCacheRecoveryTransactionError, + case .unsafeDirectory = transactionError + else { + return XCTFail( + "Expected unsafeDirectory, got \(error)" + ) + } + } + + XCTAssertEqual( + try durableStoreFamilySnapshot(at: validArchive.storeURL), + validArchive.family + ) + XCTAssertEqual( + try durableStoreFamilySnapshot(at: hostileArchive.storeURL), + hostileArchive.family + ) + XCTAssertEqual(try Data(contentsOf: hostileURL), hostileData) + XCTAssertEqual( + try durableStoreFamilySnapshot(at: storeURL), + liveFamily + ) + } + + func testCacheRecoveryRetention_whenArchiveMemberHardlinksLiveStore_thenFailsClosedWithoutUnlinkingEitherPath() throws { + let liveFamily = try writeSyntheticStoreFamily( + at: storeURL, + discriminator: 0xD0 + ) + let transaction = CrashConsistentSubstrateCacheRecovery( + storeURL: storeURL, + fileManager: .default, + backupExclusionHook: { _ in }, + retainedArchiveCount: 0, + retainedArchiveByteCount: 0 + ) + let archive = try writeCompletedRecoveryArchive( + for: transaction, + identifier: "00000000-0000-0000-0000-000000000007", + discriminator: 0xD4, + completionDate: Date(timeIntervalSince1970: 7) + ) + try FileManager.default.removeItem(at: archive.storeURL) + try FileManager.default.linkItem( + at: storeURL, + to: archive.storeURL + ) + + XCTAssertThrowsError(try transaction.reconcile()) { error in + guard + let transactionError = + error as? SubstrateCacheRecoveryTransactionError, + case .unsafeFile = transactionError + else { + return XCTFail( + "Expected unsafeFile, got \(error)" + ) + } + } + + XCTAssertEqual( + try durableStoreFamilySnapshot(at: storeURL), + liveFamily + ) + XCTAssertEqual( + try Data(contentsOf: archive.storeURL), + try Data(contentsOf: storeURL) + ) + } + + func testCacheRecoveryRetention_whenDeletionIsInterruptedAtEveryDurableBoundary_thenRelaunchConvergesWithoutTouchingNewestOrLiveFamily() throws { + let oldestIdentifier = + "00000000-0000-0000-0000-000000000008" + let boundaries: [SubstrateCacheRecoveryBoundary] = [ + .retentionMarkerPersisted(oldestIdentifier), + .retentionFamilyMemberRemoved(oldestIdentifier, ""), + .retentionFamilyMemberRemoved(oldestIdentifier, "-wal"), + .retentionFamilyMemberRemoved(oldestIdentifier, "-shm"), + .retentionFamilyMemberRemoved( + oldestIdentifier, + "-journal" + ), + .retentionMarkerRemoved(oldestIdentifier), + .retentionArchiveRemoved(oldestIdentifier) + ] + + for (index, boundary) in boundaries.enumerated() { + let caseDirectory = testDirectory.appendingPathComponent( + "retention-interruption-\(index)", + isDirectory: true + ) + try FileManager.default.createDirectory( + at: caseDirectory, + withIntermediateDirectories: false + ) + let caseStoreURL = caseDirectory.appendingPathComponent( + storeURL.lastPathComponent + ) + let liveFamily = try writeSyntheticStoreFamily( + at: caseStoreURL, + discriminator: UInt8(0xD8 + index) + ) + var interruptionWasInjected = false + let interruptedTransaction = + CrashConsistentSubstrateCacheRecovery( + storeURL: caseStoreURL, + fileManager: .default, + boundaryHook: { reachedBoundary in + guard + reachedBoundary == boundary, + !interruptionWasInjected + else { + return + } + interruptionWasInjected = true + throw SubstrateCacheRecoveryInterruption + .simulatedProcessDeath + }, + backupExclusionHook: { _ in }, + retainedArchiveCount: 1, + retainedArchiveByteCount: .max + ) + let oldest = try writeCompletedRecoveryArchive( + for: interruptedTransaction, + identifier: oldestIdentifier, + discriminator: UInt8(0xE0 + index), + completionDate: Date(timeIntervalSince1970: 8) + ) + let newest = try writeCompletedRecoveryArchive( + for: interruptedTransaction, + identifier: + "00000000-0000-0000-0000-000000000009", + discriminator: UInt8(0xE8 + index), + completionDate: Date(timeIntervalSince1970: 9) + ) + + XCTAssertThrowsError( + try interruptedTransaction.reconcile() + ) { error in + guard error is SubstrateCacheRecoveryInterruption else { + return XCTFail( + "Expected retention interruption, got \(error)" + ) + } + } + XCTAssertTrue(interruptionWasInjected) + + let relaunchedTransaction = + CrashConsistentSubstrateCacheRecovery( + storeURL: caseStoreURL, + fileManager: .default, + backupExclusionHook: { _ in }, + retainedArchiveCount: 1, + retainedArchiveByteCount: .max + ) + XCTAssertNil(try relaunchedTransaction.reconcile()) + + XCTAssertFalse( + FileManager.default.fileExists(atPath: oldest.url.path), + "Oldest archive survived \(boundary)" + ) + XCTAssertEqual( + try durableStoreFamilySnapshot(at: newest.storeURL), + newest.family, + "Newest archive changed after \(boundary)" + ) + XCTAssertEqual( + try durableStoreFamilySnapshot(at: caseStoreURL), + liveFamily, + "Live family changed after \(boundary)" + ) + XCTAssertEqual( + try contents( + of: relaunchedTransaction.recoveryRootURL + ), + [newest.url] + ) + } + } + + func testCacheRecoveryTransaction_whenLegacySplitHasMultipleCandidates_thenFailsClosedWithoutChoosingArchive() throws { + let expectedFamily = try writeSyntheticStoreFamily( + at: storeURL, + discriminator: 0xEC + ) + let mainData = try Data(contentsOf: storeURL) + try FileManager.default.removeItem(at: storeURL) + let transaction = CrashConsistentSubstrateCacheRecovery( + storeURL: storeURL, + fileManager: .default + ) + var archiveURLs: [URL] = [] + + for _ in 0 ..< 2 { + let archiveURL = transaction.recoveryRootURL + .appendingPathComponent( + UUID().uuidString, + isDirectory: true + ) + try FileManager.default.createDirectory( + at: archiveURL, + withIntermediateDirectories: true + ) + try mainData.write( + to: archiveURL.appendingPathComponent( + storeURL.lastPathComponent + ) + ) + archiveURLs.append(archiveURL) + } + let liveRemainder = try durableStoreFamilySnapshot(at: storeURL) + + XCTAssertThrowsError(try transaction.reconcile()) { error in + guard + let transactionError = + error as? SubstrateCacheRecoveryTransactionError, + case .multiplePendingTransactions = transactionError + else { + return XCTFail( + "Expected multiplePendingTransactions, got \(error)" + ) + } + } + + XCTAssertEqual( + try durableStoreFamilySnapshot(at: storeURL), + liveRemainder + ) + for archiveURL in archiveURLs { + XCTAssertEqual( + try Data( + contentsOf: archiveURL.appendingPathComponent( + storeURL.lastPathComponent + ) + ), + mainData + ) + XCTAssertFalse( + FileManager.default.fileExists( + atPath: archiveURL.appendingPathComponent( + "recovery-transaction" + ).path + ) + ) + } + XCTAssertEqual( + Set(expectedFamily.keys), + Set(liveRemainder.keys).union( + [storeURL.lastPathComponent] + ) + ) + } + + func testCacheRecoveryTransaction_whenLegacySplitIsTamperedOrOverlapping_thenFailsClosed() throws { + for variant in ["unknown-file", "overlapping-member"] { + let caseDirectory = testDirectory.appendingPathComponent( + variant, + isDirectory: true + ) + try FileManager.default.createDirectory( + at: caseDirectory, + withIntermediateDirectories: false + ) + let caseStoreURL = caseDirectory.appendingPathComponent( + storeURL.lastPathComponent + ) + _ = try writeSyntheticStoreFamily( + at: caseStoreURL, + discriminator: + variant == "unknown-file" ? 0xED : 0xEE + ) + let mainData = try Data(contentsOf: caseStoreURL) + try FileManager.default.removeItem(at: caseStoreURL) + let transaction = CrashConsistentSubstrateCacheRecovery( + storeURL: caseStoreURL, + fileManager: .default + ) + let archiveURL = transaction.recoveryRootURL + .appendingPathComponent( + UUID().uuidString, + isDirectory: true + ) + try FileManager.default.createDirectory( + at: archiveURL, + withIntermediateDirectories: true + ) + try mainData.write( + to: archiveURL.appendingPathComponent( + caseStoreURL.lastPathComponent + ) + ) + + if variant == "unknown-file" { + try Data("tampered".utf8).write( + to: archiveURL.appendingPathComponent( + "unexpected" + ) + ) + } else { + let liveWALURL = URL( + fileURLWithPath: caseStoreURL.path + "-wal" + ) + try Data(contentsOf: liveWALURL).write( + to: archiveURL.appendingPathComponent( + liveWALURL.lastPathComponent + ) + ) + } + let liveBefore = try durableStoreFamilySnapshot( + at: caseStoreURL + ) + let archiveBefore = try contentsSnapshot( + of: archiveURL + ) + + XCTAssertThrowsError(try transaction.reconcile()) { + error in + guard + let transactionError = + error as? + SubstrateCacheRecoveryTransactionError, + case .familyMismatch = transactionError + else { + return XCTFail( + "Expected familyMismatch, got \(error)" + ) + } + } + + XCTAssertEqual( + try durableStoreFamilySnapshot(at: caseStoreURL), + liveBefore + ) + XCTAssertEqual( + try contentsSnapshot(of: archiveURL), + archiveBefore + ) + } + } + + func testSyncMigration_whenKnownStoreHasContact_thenBlocksRecovery() throws { + try assertProtectedDataBlocksRecovery(expectedCategory: "CDContact") { context, model in + let contact = try self.insert(entityName: "CDContact", in: context, model: model) + contact.setValue("Alice", forKey: "name") + contact.setValue("5ProtectedContact", forKey: "address") + contact.setValue("protected-chain", forKey: "chainId") + } + } + + func testSyncMigration_whenKnownStoreHasContactItem_thenBlocksRecovery() throws { + try assertProtectedDataBlocksRecovery(expectedCategory: "CDContactItem") { context, model in + let contact = try self.insert(entityName: "CDContactItem", in: context, model: model) + contact.setValue("wallet-peer", forKey: "identifier") + contact.setValue("5Peer", forKey: "peerAddress") + contact.setValue("Peer", forKey: "peerName") + contact.setValue("5Wallet", forKey: "targetAddress") + contact.setValue(Int64(1_700_000_000), forKey: "updatedAt") + } + } + + func testSyncMigration_whenKnownStoreHasTransactionHistory_thenBlocksRecovery() throws { + try assertProtectedDataBlocksRecovery( + expectedCategory: "CDTransactionHistoryItem" + ) { context, model in + let transaction = try self.insert( + entityName: "CDTransactionHistoryItem", + in: context, + model: model + ) + transaction.setValue("0xprotected", forKey: "identifier") + transaction.setValue("5Sender", forKey: "sender") + transaction.setValue("5Receiver", forKey: "receiver") + transaction.setValue(Int16(0), forKey: "status") + transaction.setValue(Int64(1_700_000_000), forKey: "timestamp") + transaction.setValue("1", forKey: "fee") + transaction.setValue("Balances", forKey: "moduleName") + transaction.setValue("transfer", forKey: "callName") + } + } + + func testSyncMigration_whenKnownStoreHasCustomNode_thenBlocksRecovery() throws { + try assertProtectedDataBlocksRecovery( + expectedCategory: "CDChain.customNodes" + ) { context, model in + let graph = try self.insertChainWithDefaultNode( + in: context, + model: model + ) + let customNode = try self.insert( + entityName: "CDChainNode", + in: context, + model: model + ) + customNode.setValue("My RPC", forKey: "name") + customNode.setValue( + try XCTUnwrap(URL(string: "wss://custom.example.invalid")), + forKey: "url" + ) + graph.chain.mutableSetValue(forKey: "customNodes").add(customNode) + } + } + + func testSyncMigration_whenKnownStoreHasSelectedDefaultNode_thenBlocksRecovery() throws { + try assertProtectedDataBlocksRecovery( + expectedCategory: "CDChain.selectedNode" + ) { context, model in + let graph = try self.insertChainWithDefaultNode( + in: context, + model: model + ) + graph.chain.setValue(graph.node, forKey: "selectedNode") + } + } + + func testSyncMigration_whenKnownStoreHasOrphanNode_thenBlocksRecovery() throws { + try assertProtectedDataBlocksRecovery( + expectedCategory: "CDChainNode.orphan" + ) { context, model in + let orphanNode = try self.insert( + entityName: "CDChainNode", + in: context, + model: model + ) + orphanNode.setValue("Unattached RPC", forKey: "name") + orphanNode.setValue( + try XCTUnwrap(URL(string: "wss://orphan.example.invalid")), + forKey: "url" + ) + } + } + + func testCallbackMigration_whenStoreIsCorrupt_thenDoesNotCompleteOrQuarantine() throws { + try Data("still-not-a-core-data-store".utf8).write(to: storeURL, options: .atomic) + let before = try durableStoreFamilySnapshot(at: storeURL) + let migrator = makeMigrator() + let completionExpectation = expectation( + description: "callback must not complete after fail-closed migration" + ) + completionExpectation.isInverted = true + + migrator.migrate { + completionExpectation.fulfill() + } + + wait(for: [completionExpectation], timeout: 0.25) + XCTAssertEqual(try durableStoreFamilySnapshot(at: storeURL), before) + XCTAssertTrue(try recoveryDirectories().isEmpty) + } + + private func writeSyntheticStoreFamily( + at syntheticStoreURL: URL, + discriminator: UInt8 + ) throws -> [String: Data] { + let suffixes = ["", "-wal", "-shm", "-journal"] + var expected: [String: Data] = [:] + + for (index, suffix) in suffixes.enumerated() { + let byte = discriminator &+ UInt8(index) + let data = Data( + repeating: byte, + count: 37 + index * 19 + ) + let url = URL( + fileURLWithPath: syntheticStoreURL.path + suffix + ) + try data.write(to: url) + expected[url.lastPathComponent] = data + } + + return expected + } + + private func writeCompletedRecoveryArchive( + for transaction: CrashConsistentSubstrateCacheRecovery, + identifier: String, + discriminator: UInt8, + completionDate: Date + ) throws -> ( + url: URL, + storeURL: URL, + family: [String: Data] + ) { + if !FileManager.default.fileExists( + atPath: transaction.recoveryRootURL.path + ) { + try FileManager.default.createDirectory( + at: transaction.recoveryRootURL, + withIntermediateDirectories: false + ) + } + + let archiveURL = transaction.recoveryRootURL + .appendingPathComponent( + identifier, + isDirectory: true + ) + try FileManager.default.createDirectory( + at: archiveURL, + withIntermediateDirectories: false + ) + let archivedStoreURL = archiveURL.appendingPathComponent( + transaction.storeURL.lastPathComponent + ) + let family = try writeSyntheticStoreFamily( + at: archivedStoreURL, + discriminator: discriminator + ) + try FileManager.default.setAttributes( + [.modificationDate: completionDate], + ofItemAtPath: archiveURL.path + ) + + return ( + url: archiveURL, + storeURL: archivedStoreURL, + family: family + ) + } + + private func interruptCacheRecovery( + storeURL interruptedStoreURL: URL? = nil, + at boundary: SubstrateCacheRecoveryBoundary + ) throws -> URL { + let interruptedStoreURL = interruptedStoreURL ?? storeURL + let transaction = CrashConsistentSubstrateCacheRecovery( + storeURL: interruptedStoreURL, + fileManager: .default, + boundaryHook: { reachedBoundary in + guard reachedBoundary == boundary else { + return + } + throw SubstrateCacheRecoveryInterruption + .simulatedProcessDeath + } + ) + + XCTAssertThrowsError(try transaction.quarantine()) { error in + guard error is SubstrateCacheRecoveryInterruption else { + return XCTFail( + "Expected cache-recovery interruption, got \(error)" + ) + } + } + + return try XCTUnwrap( + try contents(of: transaction.recoveryRootURL).first + ) + } + + private func contents(of directoryURL: URL) throws -> [URL] { + guard FileManager.default.fileExists( + atPath: directoryURL.path + ) else { + return [] + } + + return try FileManager.default.contentsOfDirectory( + at: directoryURL, + includingPropertiesForKeys: nil + ).sorted { + $0.lastPathComponent < $1.lastPathComponent + } + } + + private func contentsSnapshot( + of directoryURL: URL + ) throws -> [String: Data] { + try Dictionary( + uniqueKeysWithValues: contents(of: directoryURL).map { + url in + (url.lastPathComponent, try Data(contentsOf: url)) + } + ) + } + + private func assertProtectedDataBlocksRecovery( + expectedCategory: String, + populate: (NSManagedObjectContext, NSManagedObjectModel) throws -> Void + ) throws { + let sourceModel = try model(for: .version3) + try createStore(at: storeURL, model: sourceModel) { context in + try populate(context, sourceModel) + } + let before = try durableStoreFamilySnapshot(at: storeURL) + let modelOnlyBundle = try makeModelBundle() + let migrator = makeMigrator(modelBundle: modelOnlyBundle) + + for _ in 0 ..< 2 { + XCTAssertThrowsError(try migrator.migrate()) { error in + guard + let migrationError = error as? SubstrateStorageMigrationError, + case let .cacheRecoveryBlockedByProtectedData( + blockedURL, + originalError, + counts + ) = migrationError + else { + return XCTFail("Expected cacheRecoveryBlockedByProtectedData, got \(error)") + } + + XCTAssertEqual(blockedURL, self.storeURL) + XCTAssertEqual( + counts.filter { $0.value > 0 }, + [expectedCategory: 1] + ) + + guard + let originalMigrationError = originalError as? SubstrateStorageMigrationError, + case let .mappingUnavailable(source, destination, _) = + originalMigrationError + else { + return XCTFail("Expected mappingUnavailable, got \(originalError)") + } + + XCTAssertEqual(source, .version3) + XCTAssertEqual(destination, .version4) + } + + XCTAssertTrue(migrator.requiresMigration()) + XCTAssertEqual(try durableStoreFamilySnapshot(at: storeURL), before) + XCTAssertTrue(try recoveryDirectories().isEmpty) + } + } + + private func insert( + entityName: String, + in context: NSManagedObjectContext, + model: NSManagedObjectModel + ) throws -> NSManagedObject { + let entity = try XCTUnwrap(model.entitiesByName[entityName]) + return NSManagedObject(entity: entity, insertInto: context) + } + + private func insertStartupPayloadEntity( + entityName: String, + attributeName: String, + payload: String, + attachNodeAsCustomNode: Bool = false, + in context: NSManagedObjectContext, + model: NSManagedObjectModel + ) throws { + if entityName == "CDChainNode", attachNodeAsCustomNode { + let graph = try insertChainWithDefaultNode( + in: context, + model: model + ) + let customNode = try insert( + entityName: entityName, + in: context, + model: model + ) + customNode.setValue( + try XCTUnwrap( + URL(string: "wss://custom.example.invalid") + ), + forKey: "url" + ) + customNode.setValue(payload, forKey: attributeName) + graph.chain.mutableSetValue( + forKey: "customNodes" + ).add(customNode) + return + } + + let object = try insert( + entityName: entityName, + in: context, + model: model + ) + switch entityName { + case "CDAsset": + object.setValue("a", forKey: "id") + object.setValue(Int16(1), forKey: "precision") + case "CDChainNode": + object.setValue("n", forKey: "name") + object.setValue( + try XCTUnwrap( + URL(string: "wss://node.example.invalid") + ), + forKey: "url" + ) + case "CDPriceData": + object.setValue("c", forKey: "currencyId") + object.setValue("1", forKey: "price") + object.setValue("i", forKey: "priceId") + case "CDChainXcmConfig", + "CDExternalApi", + "CDPriceProvider", + "CDXcmAvailableAsset", + "CDXcmAvailableDestination": + break + default: + XCTFail( + "Unsupported startup child fixture \(entityName)" + ) + return + } + object.setValue(payload, forKey: attributeName) + } + + private func assertStartupChildValueLimitExceeded( + entityName: String, + attributeName: String, + attachNodeAsCustomNode: Bool = false, + file: StaticString = #filePath, + line: UInt = #line + ) throws { + let maximum: UInt64 = 1_024 + let actual = maximum + 1 + let currentModel = try model(for: .version8) + try createStore(at: storeURL, model: currentModel) { + context in + try self.insertStartupPayloadEntity( + entityName: entityName, + attributeName: attributeName, + payload: String( + repeating: "x", + count: Int(actual) + ), + attachNodeAsCustomNode: + attachNodeAsCustomNode, + in: context, + model: currentModel + ) + } + let before = try durableStoreFamilySnapshot(at: storeURL) + var stagedHookCalled = false + var replacementCalled = false + let migrator = makeMigrator( + storeReplacer: { _, _ in + replacementCalled = true + }, + stagedStoreMutationHook: { _, _ in + stagedHookCalled = true + }, + protectedDataInspectionLimits: + makeProtectedDataLimits( + maximumStartupChildValueByteCount: maximum + ) + ) + + XCTAssertTrue( + migrator.requiresMigration(), + file: file, + line: line + ) + XCTAssertThrowsError( + try migrator.performMigration(), + file: file, + line: line + ) { error in + guard + let migrationError = + error as? SubstrateStorageMigrationError, + case let .sourceStoreInspectionFailed( + failedURL, + underlyingError + ) = migrationError, + case let .startupChildValueByteLimitExceeded( + actualEntityName, + actualAttributeName, + valueByteCount, + maximumByteCount + ) = + underlyingError as? + SubstrateProtectedDataInspectionError + else { + return XCTFail( + "Expected startup child value limit, got \(error)", + file: file, + line: line + ) + } + + XCTAssertEqual( + failedURL, + self.storeURL, + file: file, + line: line + ) + XCTAssertEqual( + actualEntityName, + entityName, + file: file, + line: line + ) + XCTAssertEqual( + actualAttributeName, + attributeName, + file: file, + line: line + ) + XCTAssertEqual( + valueByteCount, + actual, + file: file, + line: line + ) + XCTAssertEqual( + maximumByteCount, + maximum, + file: file, + line: line + ) + } + XCTAssertFalse( + stagedHookCalled, + file: file, + line: line + ) + XCTAssertFalse( + replacementCalled, + file: file, + line: line + ) + XCTAssertEqual( + try durableStoreFamilySnapshot(at: storeURL), + before, + file: file, + line: line + ) + } + + private func insertChainWithDefaultNode( + in context: NSManagedObjectContext, + model: NSManagedObjectModel + ) throws -> (chain: NSManagedObject, node: NSManagedObject) { + let chain = try insert(entityName: "CDChain", in: context, model: model) + chain.setValue("protected-chain", forKey: "chainId") + chain.setValue("Protected Chain", forKey: "name") + + let node = try insert(entityName: "CDChainNode", in: context, model: model) + node.setValue("Default RPC", forKey: "name") + node.setValue( + try XCTUnwrap(URL(string: "wss://default.example.invalid")), + forKey: "url" + ) + node.setValue(chain, forKey: "chain") + + return (chain, node) + } + + private func makeMigrator( + targetVersion: SubstrateStorageVersion = .version8, + modelBundle: Bundle? = nil, + storeURL: URL? = nil, + fileManager: FileManager = .default, + storeReplacer: @escaping ( + URL, + URL + ) throws -> Void = { targetURL, sourceURL in + try NSPersistentStoreCoordinator.replaceStore( + at: targetURL, + withStoreAt: sourceURL + ) + }, + stagedStoreMutationHook: @escaping ( + URL, + NSManagedObjectModel + ) throws -> Void = { _, _ in }, + checkpointStoreHook: ((URL, NSManagedObjectModel) throws -> Void)? = nil, + privateSourceCopyLimits: + SQLiteStoreFamilyCopyLimits = + .substrateStorageProduction, + privateSourceAvailableCapacityProvider: + ((URL) throws -> UInt64)? = nil, + protectedDataInspectionLimits: + SubstrateProtectedDataInspectionLimits = .production, + transformableObjectIDPageDidFetch: @escaping ( + String, + Int, + Int + ) -> Void = { _, _, _ in }, + protectedRelationshipCountDidFetch: @escaping ( + NSManagedObject, + String, + Int + ) -> Void = { _, _, _ in }, + protectedRelationshipWillMaterialize: @escaping ( + String, + String, + Int + ) -> Void = { _, _, _ in } + ) -> SubstrateStorageMigrator { + SubstrateStorageMigrator( + targetVersion: targetVersion, + storeURL: storeURL ?? self.storeURL, + modelDirectory: SubstrateStorageParams.modelDirectory, + fileManager: fileManager, + modelBundle: modelBundle ?? appBundle, + storeReplacer: storeReplacer, + stagedStoreMutationHook: stagedStoreMutationHook, + checkpointStoreHook: checkpointStoreHook, + privateSourceCopyLimits: privateSourceCopyLimits, + privateSourceAvailableCapacityProvider: + privateSourceAvailableCapacityProvider, + protectedDataInspectionLimits: + protectedDataInspectionLimits, + transformableObjectIDPageDidFetch: + transformableObjectIDPageDidFetch, + protectedRelationshipCountDidFetch: + protectedRelationshipCountDidFetch, + protectedRelationshipWillMaterialize: + protectedRelationshipWillMaterialize + ) + } + + private func makeProtectedDataLimits( + fetchPageSize: Int = 16, + maximumRowsPerEntity: Int = 100, + maximumTotalRows: Int = 500, + maximumStartupRootRowsPerEntity: Int = 4_096, + maximumStartupRootPayloadByteCount: UInt64 = + 256 * 1024 * 1024, + maximumStartupChildValueByteCount: UInt64 = + 256 * 1024, + maximumStartupChildPayloadByteCount: UInt64 = + 256 * 1024 * 1024, + maximumRelationshipMembers: Int = 100, + maximumTotalRelationshipMembers: Int = 500, + maximumTransformableElements: Int = 100, + maximumTransformableArchiveByteCount: Int = 8_192, + maximumValueByteCount: Int = 1_024, + maximumRecordByteCount: Int = 8_192 + ) -> SubstrateProtectedDataInspectionLimits { + SubstrateProtectedDataInspectionLimits( + fetchPageSize: fetchPageSize, + maximumRowsPerEntity: maximumRowsPerEntity, + maximumTotalRows: maximumTotalRows, + maximumStartupRootRowsPerEntity: + maximumStartupRootRowsPerEntity, + maximumStartupRootPayloadByteCount: + maximumStartupRootPayloadByteCount, + maximumStartupChildValueByteCount: + maximumStartupChildValueByteCount, + maximumStartupChildPayloadByteCount: + maximumStartupChildPayloadByteCount, + maximumRelationshipMembers: + maximumRelationshipMembers, + maximumTotalRelationshipMembers: + maximumTotalRelationshipMembers, + maximumTransformableElements: + maximumTransformableElements, + maximumTransformableArchiveByteCount: + maximumTransformableArchiveByteCount, + maximumValueByteCount: maximumValueByteCount, + maximumRecordByteCount: maximumRecordByteCount + ) + } + + private func model( + for version: SubstrateStorageVersion, + in bundle: Bundle? = nil + ) throws -> NSManagedObjectModel { + let bundle = bundle ?? appBundle + let modelURL = bundle.url( + forResource: version.rawValue, + withExtension: "omo", + subdirectory: SubstrateStorageParams.modelDirectory + ) ?? bundle.url( + forResource: version.rawValue, + withExtension: "mom", + subdirectory: SubstrateStorageParams.modelDirectory + ) + + return try XCTUnwrap( + modelURL.flatMap(NSManagedObjectModel.init(contentsOf:)), + "Unable to load \(version.rawValue) from \(bundle.bundleURL.path)" + ) + } + + private func createStore( + at url: URL, + model: NSManagedObjectModel, + populate: (NSManagedObjectContext) throws -> Void + ) throws { + let coordinator = NSPersistentStoreCoordinator(managedObjectModel: model) + let store = try coordinator.addPersistentStore( + ofType: NSSQLiteStoreType, + configurationName: nil, + at: url, + options: [ + NSSQLitePragmasOption: ["journal_mode": "WAL"], + NSMigratePersistentStoresAutomaticallyOption: false, + NSInferMappingModelAutomaticallyOption: false + ] + ) + defer { + try? coordinator.remove(store) + } + + let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) + context.persistentStoreCoordinator = coordinator + + var populationError: Error? + context.performAndWait { + do { + try populate(context) + if context.hasChanges { + try context.save() + } + } catch { + populationError = error + } + } + + if let populationError { + throw populationError + } + } + + @discardableResult + private func insertRuntimeItem( + identifier: String, + in context: NSManagedObjectContext, + model: NSManagedObjectModel + ) throws -> NSManagedObject { + let entity = try XCTUnwrap(model.entitiesByName["CDRuntimeMetadataItem"]) + let item = NSManagedObject(entity: entity, insertInto: context) + item.setValue(identifier, forKey: "identifier") + item.setValue(Int32(1), forKey: "version") + item.setValue(Int32(1), forKey: "txVersion") + return item + } + + private func insertChainStorageItem( + identifier: String, + in context: NSManagedObjectContext, + model: NSManagedObjectModel + ) throws { + let entity = try XCTUnwrap(model.entitiesByName["CDChainStorageItem"]) + let item = NSManagedObject(entity: entity, insertInto: context) + item.setValue(identifier, forKey: "identifier") + item.setValue(Data([0x00, 0x80, 0xFF]), forKey: "data") + } + + @discardableResult + private func insertMigrationAsset( + identifier: String, + in context: NSManagedObjectContext, + model: NSManagedObjectModel + ) throws -> NSManagedObject { + let entity = try XCTUnwrap(model.entitiesByName["CDAsset"]) + let asset = NSManagedObject(entity: entity, insertInto: context) + asset.setValue(identifier, forKey: "id") + asset.setValue(Int16(18), forKey: "precision") + + if entity.attributesByName["chainId"] != nil { + asset.setValue("migration-fixture-chain", forKey: "chainId") + } + + if entity.attributesByName["symbol"] != nil { + asset.setValue("FIX", forKey: "symbol") + } + + return asset + } + + private func createVersion7AssetStore( + purchaseProviders: [String] + ) throws { + let sourceModel = try model(for: .version7) + try createStore(at: storeURL, model: sourceModel) { + context in + let asset = try self.insertMigrationAsset( + identifier: "bounded-transformable-asset", + in: context, + model: sourceModel + ) + asset.setValue( + NSArray(array: purchaseProviders), + forKey: "purchaseProviders" + ) + } + } + + private func createCurrentTransformableAndProtectedGraph() throws { + let currentModel = try model(for: .version8) + try createStore(at: storeURL, model: currentModel) { + context in + let graph = try self.insertChainWithDefaultNode( + in: context, + model: currentModel + ) + graph.chain.setValue( + NSArray(object: "safe-option"), + forKey: "options" + ) + + let customNode = try self.insert( + entityName: "CDChainNode", + in: context, + model: currentModel + ) + customNode.setValue("Custom RPC", forKey: "name") + customNode.setValue( + try XCTUnwrap( + URL(string: "wss://current-custom.example.invalid") + ), + forKey: "url" + ) + graph.chain.mutableSetValue( + forKey: "customNodes" + ).add(customNode) + graph.chain.setValue( + customNode, + forKey: "selectedNode" + ) + + let asset = try self.insertMigrationAsset( + identifier: "current-transformable-asset", + in: context, + model: currentModel + ) + asset.setValue( + NSArray(object: "safe-provider"), + forKey: "purchaseProviders" + ) + asset.setValue(graph.chain, forKey: "chain") + } + } + + private func createCurrentStartupRelationshipGraph() throws { + let currentModel = try model(for: .version8) + try createStore(at: storeURL, model: currentModel) { + context in + try self.insertRuntimeItem( + identifier: "startup-runtime", + in: context, + model: currentModel + ) + let graph = try self.insertChainWithDefaultNode( + in: context, + model: currentModel + ) + + let customNode = try self.insert( + entityName: "CDChainNode", + in: context, + model: currentModel + ) + customNode.setValue("Startup Custom RPC", forKey: "name") + customNode.setValue( + try XCTUnwrap( + URL(string: "wss://startup-custom.example.invalid") + ), + forKey: "url" + ) + graph.chain.mutableSetValue( + forKey: "customNodes" + ).add(customNode) + + let asset = try self.insertMigrationAsset( + identifier: "startup-asset", + in: context, + model: currentModel + ) + asset.setValue(graph.chain, forKey: "chain") + try self.insertPriceData( + count: 1, + for: asset, + in: context, + model: currentModel + ) + + let explorer = try self.insert( + entityName: "CDExternalApi", + in: context, + model: currentModel + ) + explorer.setValue("subscan", forKey: "type") + explorer.setValue( + "https://explorer.example.invalid", + forKey: "url" + ) + explorer.setValue(graph.chain, forKey: "chain") + + try self.insertXcmRelationshipGraph( + for: graph.chain, + in: context, + model: currentModel + ) + } + } + + private func insertPriceData( + count: Int, + for asset: NSManagedObject, + in context: NSManagedObjectContext, + model: NSManagedObjectModel + ) throws { + for index in 0 ..< count { + let priceData = try insert( + entityName: "CDPriceData", + in: context, + model: model + ) + priceData.setValue( + "currency-\(index)", + forKey: "currencyId" + ) + priceData.setValue("1.0", forKey: "price") + priceData.setValue( + "price-\(index)", + forKey: "priceId" + ) + priceData.setValue(asset, forKey: "asset") + } + } + + private func insertXcmRelationshipGraph( + for chain: NSManagedObject? = nil, + in context: NSManagedObjectContext, + model: NSManagedObjectModel + ) throws { + let config = try insert( + entityName: "CDChainXcmConfig", + in: context, + model: model + ) + if let chain { + config.setValue(chain, forKey: "chain") + } + + let availableAsset = try insert( + entityName: "CDXcmAvailableAsset", + in: context, + model: model + ) + availableAsset.setValue( + "xcm-asset", + forKey: "id" + ) + config.mutableSetValue( + forKey: "availableAssets" + ).add(availableAsset) + + let destination = try insert( + entityName: "CDXcmAvailableDestination", + in: context, + model: model + ) + destination.setValue( + "destination-chain", + forKey: "chainId" + ) + destination.setValue(config, forKey: "config") + destination.mutableSetValue( + forKey: "assets" + ).add(availableAsset) + } + + private func addPriceDataToSingleAsset( + count: Int, + at url: URL, + model: NSManagedObjectModel + ) throws { + let coordinator = NSPersistentStoreCoordinator( + managedObjectModel: model + ) + let store = try coordinator.addPersistentStore( + ofType: NSSQLiteStoreType, + configurationName: nil, + at: url, + options: [ + NSSQLitePragmasOption: ["journal_mode": "DELETE"], + NSMigratePersistentStoresAutomaticallyOption: false, + NSInferMappingModelAutomaticallyOption: false + ] + ) + defer { + try? coordinator.remove(store) + } + + let context = NSManagedObjectContext( + concurrencyType: .privateQueueConcurrencyType + ) + context.persistentStoreCoordinator = coordinator + var mutationError: Error? + context.performAndWait { + do { + let asset = try self.fetchSingleObject( + entityName: "CDAsset", + context: context + ) + try self.insertPriceData( + count: count, + for: asset, + in: context, + model: model + ) + try context.save() + } catch { + mutationError = error + } + } + + if let mutationError { + throw mutationError + } + } + + private func assertCurrentTransformableAndProtectedGraph( + expectedOptions: [String], + expectedPurchaseProviders: [String]? + ) throws { + let currentModel = try model(for: .version8) + try inspectStore(at: storeURL, model: currentModel) { + context in + let chain = try self.fetchSingleObject( + entityName: "CDChain", + context: context + ) + XCTAssertEqual( + try self.stringArray(in: chain, key: "options"), + expectedOptions + ) + + let defaultNodes = self.relationshipObjects( + in: chain, + key: "nodes" + ) + let customNodes = self.relationshipObjects( + in: chain, + key: "customNodes" + ) + let selectedNode = try XCTUnwrap( + chain.value(forKey: "selectedNode") + as? NSManagedObject + ) + XCTAssertEqual(defaultNodes.count, 1) + XCTAssertEqual(customNodes.count, 1) + XCTAssertEqual( + selectedNode.value(forKey: "url") as? URL, + URL(string: "wss://current-custom.example.invalid") + ) + XCTAssertEqual( + customNodes.first, + selectedNode + ) + + let asset = try self.fetchSingleObject( + entityName: "CDAsset", + context: context + ) + XCTAssertEqual( + try self.stringArray( + in: asset, + key: "purchaseProviders" + ), + expectedPurchaseProviders + ) + XCTAssertEqual( + asset.value(forKey: "chain") as? NSManagedObject, + chain + ) + } + } + + private func migratedAssetPurchaseProviders() throws -> [String]? { + let targetModel = try model(for: .version8) + var purchaseProviders: [String]? + try inspectStore(at: storeURL, model: targetModel) { + context in + let asset = try self.fetchSingleObject( + entityName: "CDAsset", + context: context + ) + purchaseProviders = try self.stringArray( + in: asset, + key: "purchaseProviders" + ) + } + return purchaseProviders + } + + private func insertLegacyChainWithNodes( + in context: NSManagedObjectContext, + model: NSManagedObjectModel + ) throws { + let chainEntity = try XCTUnwrap(model.entitiesByName["CDChain"]) + let nodeEntity = try XCTUnwrap(model.entitiesByName["CDChainNode"]) + + let chain = NSManagedObject(entity: chainEntity, insertInto: context) + chain.setValue("migration-chain", forKey: "chainId") + chain.setValue("Migration Chain", forKey: "name") + chain.setValue(URL(string: "https://example.invalid/icon.png"), forKey: "icon") + + let lastNode = NSManagedObject(entity: nodeEntity, insertInto: context) + lastNode.setValue("Zulu Node", forKey: "name") + lastNode.setValue(URL(string: "wss://zulu.example.invalid"), forKey: "url") + lastNode.setValue(chain, forKey: "chain") + + let firstNode = NSManagedObject(entity: nodeEntity, insertInto: context) + firstNode.setValue("Alpha Node", forKey: "name") + firstNode.setValue(URL(string: "wss://alpha.example.invalid"), forKey: "url") + firstNode.setValue(chain, forKey: "chain") + } + + private func insertLegacyChainGraph( + in context: NSManagedObjectContext, + model: NSManagedObjectModel, + symbol: String? = "MIG", + includeNilAssetWrapper: Bool = false + ) throws { + let chainEntity = try XCTUnwrap(model.entitiesByName["CDChain"]) + let nodeEntity = try XCTUnwrap(model.entitiesByName["CDChainNode"]) + let assetEntity = try XCTUnwrap(model.entitiesByName["CDAsset"]) + let chainAssetEntity = try XCTUnwrap(model.entitiesByName["CDChainAsset"]) + + let chain = NSManagedObject(entity: chainEntity, insertInto: context) + chain.setValue("migration-chain", forKey: "chainId") + chain.setValue("Migration Chain", forKey: "name") + chain.setValue(URL(string: "https://example.invalid/icon.png"), forKey: "icon") + + let node = NSManagedObject(entity: nodeEntity, insertInto: context) + node.setValue("Migration Node", forKey: "name") + node.setValue(URL(string: "wss://example.invalid"), forKey: "url") + node.setValue(chain, forKey: "chain") + + let asset = NSManagedObject(entity: assetEntity, insertInto: context) + asset.setValue("migration-chain", forKey: "chainId") + asset.setValue("migration-asset", forKey: "id") + asset.setValue(Int16(18), forKey: "precision") + asset.setValue(symbol, forKey: "symbol") + + let chainAsset = NSManagedObject( + entity: chainAssetEntity, + insertInto: context + ) + chainAsset.setValue("migration-asset", forKey: "assetId") + chainAsset.setValue(true, forKey: "isNative") + chainAsset.setValue(false, forKey: "isUtility") + chainAsset.setValue("normal", forKey: "type") + chainAsset.setValue(asset, forKey: "asset") + chainAsset.setValue(chain, forKey: "chain") + + if includeNilAssetWrapper { + let nilAssetWrapper = NSManagedObject( + entity: chainAssetEntity, + insertInto: context + ) + nilAssetWrapper.setValue("missing-asset", forKey: "assetId") + nilAssetWrapper.setValue(chain, forKey: "chain") + } + } + + private func insertLegacyOrphanAsset( + in context: NSManagedObjectContext, + model: NSManagedObjectModel + ) throws { + let assetEntity = try XCTUnwrap(model.entitiesByName["CDAsset"]) + let asset = NSManagedObject(entity: assetEntity, insertInto: context) + asset.setValue("orphan-chain", forKey: "chainId") + asset.setValue("orphan-asset", forKey: "id") + asset.setValue(Int16(12), forKey: "precision") + asset.setValue("ORP", forKey: "symbol") + } + + private func assertAdversarialTransformableMigration( + sourceVersion: SubstrateStorageVersion, + updateSQL: String, + expectedDefaultedKeys: Set + ) throws { + let sourceModel = try model(for: sourceVersion) + try createStore(at: storeURL, model: sourceModel) { context in + try self.insertTransformableAndProtectedGraph( + in: context, + model: sourceModel + ) + } + try executeSQLite(updateSQL, at: storeURL) + + try makeMigrator().performMigration() + + try assertMigratedTransformableAndProtectedGraph( + expectedDefaultedKeys: expectedDefaultedKeys + ) + } + + private func insertTransformableAndProtectedGraph( + in context: NSManagedObjectContext, + model: NSManagedObjectModel + ) throws { + let chain = try insert( + entityName: "CDChain", + in: context, + model: model + ) + chain.setValue("protected-transformable-chain", forKey: "chainId") + chain.setValue("Protected Transformable Chain", forKey: "name") + chain.setValue( + NSArray(object: "chain-option"), + forKey: "options" + ) + + let defaultNode = try insert( + entityName: "CDChainNode", + in: context, + model: model + ) + defaultNode.setValue("Default Node", forKey: "name") + defaultNode.setValue( + try XCTUnwrap(URL(string: "wss://default-node.example.invalid")), + forKey: "url" + ) + defaultNode.setValue("default-key", forKey: "apiKeyName") + defaultNode.setValue("default-query", forKey: "apiQueryName") + defaultNode.setValue(chain, forKey: "chain") + + let customNode = try insert( + entityName: "CDChainNode", + in: context, + model: model + ) + customNode.setValue("Custom Node", forKey: "name") + customNode.setValue( + try XCTUnwrap(URL(string: "wss://custom-node.example.invalid")), + forKey: "url" + ) + customNode.setValue("custom-key", forKey: "apiKeyName") + customNode.setValue("custom-query", forKey: "apiQueryName") + chain.mutableSetValue(forKey: "customNodes").add(customNode) + chain.setValue(customNode, forKey: "selectedNode") + + let asset = try insert( + entityName: "CDAsset", + in: context, + model: model + ) + asset.setValue("protected-asset", forKey: "id") + asset.setValue(Int16(12), forKey: "precision") + if asset.entity.attributesByName["chainId"] != nil { + asset.setValue( + "protected-transformable-chain", + forKey: "chainId" + ) + } + if asset.entity.attributesByName["symbol"] != nil { + asset.setValue("SAFE", forKey: "symbol") + } + + if model.entitiesByName["CDChainAsset"] != nil { + let wrapper = try insert( + entityName: "CDChainAsset", + in: context, + model: model + ) + wrapper.setValue("protected-asset", forKey: "assetId") + wrapper.setValue(true, forKey: "isNative") + wrapper.setValue(false, forKey: "isUtility") + wrapper.setValue("normal", forKey: "type") + wrapper.setValue( + NSArray(object: "provider-a"), + forKey: "purchaseProviders" + ) + wrapper.setValue(asset, forKey: "asset") + wrapper.setValue(chain, forKey: "chain") + } else { + asset.setValue( + NSArray(object: "provider-a"), + forKey: "purchaseProviders" + ) + chain.mutableSetValue(forKey: "assets").add(asset) + } + + let explorer = try insert( + entityName: "CDExternalApi", + in: context, + model: model + ) + explorer.setValue("subscan", forKey: "type") + explorer.setValue("https://explorer.example.invalid", forKey: "url") + explorer.setValue( + NSArray(object: "explorer-type"), + forKey: "types" + ) + explorer.setValue(chain, forKey: "chain") + + let polkaswap = try insert( + entityName: "CDPolkaswapRemoteSettings", + in: context, + model: model + ) + polkaswap.setValue("protected-v1", forKey: "version") + polkaswap.setValue( + NSArray(object: "source-a"), + forKey: "availableSources" + ) + polkaswap.setValue( + NSArray(object: "smart-a"), + forKey: "forceSmartIds" + ) + polkaswap.setValue(NSSet(), forKey: "availableDexIds") + + let xcmConfig = try insert( + entityName: "CDChainXcmConfig", + in: context, + model: model + ) + xcmConfig.setValue("v3", forKey: "xcmVersion") + xcmConfig.setValue( + NSArray(object: "XCM-A"), + forKey: "availableAssets" + ) + xcmConfig.setValue(chain, forKey: "chain") + + let xcmDestination = try insert( + entityName: "CDXcmAvailableDestination", + in: context, + model: model + ) + xcmDestination.setValue( + "destination-chain", + forKey: "chainId" + ) + xcmDestination.setValue( + NSArray(object: "XCM-B"), + forKey: "assets" + ) + xcmDestination.setValue(xcmConfig, forKey: "config") + + let contact = try insert( + entityName: "CDContact", + in: context, + model: model + ) + contact.setValue("Protected Contact", forKey: "name") + contact.setValue("contact-address", forKey: "address") + contact.setValue( + "protected-transformable-chain", + forKey: "chainId" + ) + + let contactItem = try insert( + entityName: "CDContactItem", + in: context, + model: model + ) + contactItem.setValue("contact-item", forKey: "identifier") + contactItem.setValue("peer-address", forKey: "peerAddress") + contactItem.setValue("Peer Name", forKey: "peerName") + contactItem.setValue("target-address", forKey: "targetAddress") + contactItem.setValue(Int64(1_234_567), forKey: "updatedAt") + + let history = try insert( + entityName: "CDTransactionHistoryItem", + in: context, + model: model + ) + history.setValue("balances", forKey: "moduleName") + history.setValue("transfer", forKey: "callName") + history.setValue("history-id", forKey: "identifier") + history.setValue("sender-address", forKey: "sender") + history.setValue("receiver-address", forKey: "receiver") + history.setValue("10", forKey: "fee") + history.setValue(Data([0x01, 0x02, 0xFF]), forKey: "call") + history.setValue(Int64(42), forKey: "blockNumber") + history.setValue(Int64(1_234_568), forKey: "timestamp") + history.setValue(Int16(7), forKey: "txIndex") + history.setValue(Int16(1), forKey: "status") + } + + private func assertMigratedTransformableAndProtectedGraph( + expectedDefaultedKeys: Set + ) throws { + let targetModel = try model(for: .version8) + try inspectStore(at: storeURL, model: targetModel) { context in + let chain = try self.fetchSingleObject( + entityName: "CDChain", + context: context + ) + XCTAssertEqual( + try self.stringArray(in: chain, key: "options"), + expectedDefaultedKeys.contains("CDChain.options") + ? [] + : ["chain-option"] + ) + + let defaultNodes = self.relationshipObjects( + in: chain, + key: "nodes" + ) + let customNodes = self.relationshipObjects( + in: chain, + key: "customNodes" + ) + let selectedNode = try XCTUnwrap( + chain.value(forKey: "selectedNode") as? NSManagedObject + ) + XCTAssertEqual(defaultNodes.count, 1) + XCTAssertEqual(customNodes.count, 1) + XCTAssertEqual( + defaultNodes.first?.value(forKey: "name") as? String, + "Default Node" + ) + XCTAssertEqual( + customNodes.first?.value(forKey: "name") as? String, + "Custom Node" + ) + XCTAssertEqual( + selectedNode.value(forKey: "url") as? URL, + URL(string: "wss://custom-node.example.invalid") + ) + + let asset = try XCTUnwrap( + self.relationshipObjects(in: chain, key: "assets").first + ) + XCTAssertEqual( + try self.stringArray( + in: asset, + key: "purchaseProviders" + ), + expectedDefaultedKeys.contains( + "CDChainAsset.purchaseProviders" + ) || expectedDefaultedKeys.contains( + "CDAsset.purchaseProviders" + ) + ? [] + : ["provider-a"] + ) + + let explorer = try XCTUnwrap( + self.relationshipObjects( + in: chain, + key: "explorers" + ).first + ) + XCTAssertEqual( + try self.stringArray(in: explorer, key: "types"), + expectedDefaultedKeys.contains("CDExternalApi.types") + ? [] + : ["explorer-type"] + ) + + let polkaswap = try self.fetchSingleObject( + entityName: "CDPolkaswapRemoteSettings", + context: context + ) + XCTAssertEqual( + try self.stringArray( + in: polkaswap, + key: "availableSources" + ), + expectedDefaultedKeys.contains( + "CDPolkaswapRemoteSettings.availableSources" + ) + ? [] + : ["source-a"] + ) + XCTAssertEqual( + try self.stringArray( + in: polkaswap, + key: "forceSmartIds" + ), + expectedDefaultedKeys.contains( + "CDPolkaswapRemoteSettings.forceSmartIds" + ) + ? [] + : ["smart-a"] + ) + + let xcmConfig = try XCTUnwrap( + chain.value(forKey: "xcmConfig") as? NSManagedObject + ) + let configSymbols = self.relationshipObjects( + in: xcmConfig, + key: "availableAssets" + ).compactMap { $0.value(forKey: "symbol") as? String }.sorted() + XCTAssertEqual( + configSymbols, + expectedDefaultedKeys.contains( + "CDChainXcmConfig.availableAssets" + ) + ? [] + : ["XCM-A"] + ) + + let destination = try XCTUnwrap( + self.relationshipObjects( + in: xcmConfig, + key: "availableDestinations" + ).first + ) + let destinationSymbols = self.relationshipObjects( + in: destination, + key: "assets" + ).compactMap { $0.value(forKey: "symbol") as? String }.sorted() + XCTAssertEqual( + destinationSymbols, + expectedDefaultedKeys.contains( + "CDXcmAvailableDestination.assets" + ) + ? [] + : ["XCM-B"] + ) + + let contact = try self.fetchSingleObject( + entityName: "CDContact", + context: context + ) + XCTAssertEqual( + contact.value(forKey: "name") as? String, + "Protected Contact" + ) + XCTAssertEqual( + contact.value(forKey: "address") as? String, + "contact-address" + ) + + let contactItem = try self.fetchSingleObject( + entityName: "CDContactItem", + context: context + ) + XCTAssertEqual( + contactItem.value(forKey: "peerName") as? String, + "Peer Name" + ) + XCTAssertEqual( + contactItem.value(forKey: "targetAddress") as? String, + "target-address" + ) + + let history = try self.fetchSingleObject( + entityName: "CDTransactionHistoryItem", + context: context + ) + XCTAssertEqual( + history.value(forKey: "identifier") as? String, + "history-id" + ) + XCTAssertEqual( + history.value(forKey: "call") as? Data, + Data([0x01, 0x02, 0xFF]) + ) + } + } + + private func assertSourceProtectedGraphIsExact( + at url: URL, + model: NSManagedObjectModel + ) throws { + // This assertion intentionally verifies only the protected graph. The + // caller has already compared every installed family byte. Inspect a + // disposable copy after neutralizing the deliberately corrupt cache + // transformable so Core Data cannot fault that unrelated column while + // traversing relationships. + let inspectionRootURL = testDirectory.appendingPathComponent( + "ProtectedGraphInspection-\(UUID().uuidString)", + isDirectory: true + ) + let inspectionStoreURL = inspectionRootURL.appendingPathComponent( + url.lastPathComponent + ) + try FileManager.default.createDirectory( + at: inspectionRootURL, + withIntermediateDirectories: false + ) + defer { + try? FileManager.default.removeItem(at: inspectionRootURL) + } + + for suffix in ["", "-wal", "-journal"] { + let sourceURL = URL( + fileURLWithPath: url.path + suffix + ) + guard FileManager.default.fileExists( + atPath: sourceURL.path + ) else { + continue + } + try FileManager.default.copyItem( + at: sourceURL, + to: URL( + fileURLWithPath: inspectionStoreURL.path + suffix + ) + ) + } + try executeSQLite( + "UPDATE ZCDCHAIN SET ZOPTIONS = NULL", + at: inspectionStoreURL + ) + + try inspectStore(at: inspectionStoreURL, model: model) { context in + let chain = try self.fetchSingleObject( + entityName: "CDChain", + context: context + ) + let defaultNodes = self.relationshipObjects( + in: chain, + key: "nodes" + ) + let customNodes = self.relationshipObjects( + in: chain, + key: "customNodes" + ) + XCTAssertEqual(defaultNodes.count, 1) + XCTAssertEqual(customNodes.count, 1) + XCTAssertEqual( + defaultNodes.first?.value(forKey: "name") as? String, + "Default Node" + ) + XCTAssertEqual( + customNodes.first?.value(forKey: "name") as? String, + "Custom Node" + ) + XCTAssertEqual( + (chain.value(forKey: "selectedNode") as? NSManagedObject)? + .value(forKey: "name") as? String, + "Custom Node" + ) + + let contact = try self.fetchSingleObject( + entityName: "CDContact", + context: context + ) + XCTAssertEqual( + contact.value(forKey: "name") as? String, + "Protected Contact" + ) + let history = try self.fetchSingleObject( + entityName: "CDTransactionHistoryItem", + context: context + ) + XCTAssertEqual( + history.value(forKey: "identifier") as? String, + "history-id" + ) + } + } + + private func mutateStagedProtectedDataWithoutChangingRowCounts( + at url: URL, + model: NSManagedObjectModel + ) throws { + let coordinator = NSPersistentStoreCoordinator( + managedObjectModel: model + ) + let store = try coordinator.addPersistentStore( + ofType: NSSQLiteStoreType, + configurationName: nil, + at: url, + options: [ + NSSQLitePragmasOption: ["journal_mode": "DELETE"], + NSMigratePersistentStoresAutomaticallyOption: false, + NSInferMappingModelAutomaticallyOption: false + ] + ) + defer { + try? coordinator.remove(store) + } + + let context = NSManagedObjectContext( + concurrencyType: .privateQueueConcurrencyType + ) + context.persistentStoreCoordinator = coordinator + var mutationError: Error? + context.performAndWait { + do { + let contact = try self.fetchSingleObject( + entityName: "CDContact", + context: context + ) + contact.setValue( + "Same Count, Different Contact", + forKey: "name" + ) + + let chain = try self.fetchSingleObject( + entityName: "CDChain", + context: context + ) + let defaultNode = try XCTUnwrap( + self.relationshipObjects( + in: chain, + key: "nodes" + ).first + ) + chain.setValue(defaultNode, forKey: "selectedNode") + try context.save() + } catch { + mutationError = error + } + } + + if let mutationError { + throw mutationError + } + } + + private func fetchSingleObject( + entityName: String, + context: NSManagedObjectContext + ) throws -> NSManagedObject { + let request = NSFetchRequest( + entityName: entityName + ) + request.fetchLimit = 2 + request.includesPendingChanges = false + request.includesSubentities = false + let objects = try context.fetch(request) + XCTAssertEqual(objects.count, 1) + return try XCTUnwrap(objects.first) + } + + private func relationshipObjects( + in object: NSManagedObject, + key: String + ) -> [NSManagedObject] { + if let objects = object.value(forKey: key) as? Set { + return Array(objects) + } + if let objects = object.value(forKey: key) as? NSSet { + return objects.compactMap { $0 as? NSManagedObject } + } + return [] + } + + private func stringArray( + in object: NSManagedObject, + key: String + ) throws -> [String]? { + try SafeTransformableValueReader.read( + from: object, + key: key + ) + } + + private func hex(_ data: Data) -> String { + data.map { String(format: "%02X", $0) }.joined() + } + + private func inspectStore( + at url: URL, + model: NSManagedObjectModel, + body: (NSManagedObjectContext) throws -> Void + ) throws { + let coordinator = NSPersistentStoreCoordinator(managedObjectModel: model) + let store = try coordinator.addPersistentStore( + ofType: NSSQLiteStoreType, + configurationName: nil, + at: url, + options: [ + NSReadOnlyPersistentStoreOption: true, + NSMigratePersistentStoresAutomaticallyOption: false, + NSInferMappingModelAutomaticallyOption: false + ] + ) + defer { + try? coordinator.remove(store) + } + + let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) + context.persistentStoreCoordinator = coordinator + + var inspectionError: Error? + context.performAndWait { + do { + try body(context) + } catch { + inspectionError = error + } + } + + if let inspectionError { + throw inspectionError + } + } + + private func count( + entityName: String, + at url: URL, + model: NSManagedObjectModel + ) throws -> Int { + let coordinator = NSPersistentStoreCoordinator(managedObjectModel: model) + let store = try coordinator.addPersistentStore( + ofType: NSSQLiteStoreType, + configurationName: nil, + at: url, + options: [ + NSMigratePersistentStoresAutomaticallyOption: false, + NSInferMappingModelAutomaticallyOption: false + ] + ) + defer { + try? coordinator.remove(store) + } + + let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) + context.persistentStoreCoordinator = coordinator + + var result: Result! + context.performAndWait { + result = Result { + let request = NSFetchRequest(entityName: entityName) + return try context.count(for: request) + } + } + + return try result.get() + } + + private func isStore( + at url: URL, + compatibleWith model: NSManagedObjectModel, + version: SubstrateStorageVersion + ) throws -> Bool { + let metadata = try NSPersistentStoreCoordinator.metadataForPersistentStore( + ofType: NSSQLiteStoreType, + at: url, + options: nil + ) + + return model.isConfiguration( + withName: version.rawValue, + compatibleWithStoreMetadata: metadata + ) + } + + private func flattenedErrorDescriptions(_ error: Error) -> [String] { + let nsError = error as NSError + var descriptions = [nsError.localizedDescription] + + if let underlyingError = nsError.userInfo[NSUnderlyingErrorKey] as? Error { + descriptions.append(contentsOf: flattenedErrorDescriptions(underlyingError)) + } + + if let detailedErrors = nsError.userInfo[NSDetailedErrorsKey] as? [Error] { + for detailedError in detailedErrors { + descriptions.append(contentsOf: flattenedErrorDescriptions(detailedError)) + } + } + + return descriptions + } + + private func makeUnknownModel() -> NSManagedObjectModel { + let model = NSManagedObjectModel() + let entity = NSEntityDescription() + entity.name = "UnknownSubstrateCache" + entity.managedObjectClassName = NSStringFromClass(NSManagedObject.self) + + let identifier = NSAttributeDescription() + identifier.name = "identifier" + identifier.attributeType = .stringAttributeType + identifier.isOptional = true + entity.properties = [identifier] + model.entities = [entity] + + return model + } + + private func makeModelBundle( + omitting omittedVersion: SubstrateStorageVersion? = nil + ) throws -> Bundle { + let bundleURL = testDirectory.appendingPathComponent( + "SubstrateModels-\(UUID().uuidString).bundle", + isDirectory: true + ) + try FileManager.default.createDirectory( + at: bundleURL, + withIntermediateDirectories: true + ) + + let infoPlist: [String: Any] = [ + "CFBundleIdentifier": "jp.co.soramitsu.fearlesswallet.tests.substrate-models.\(UUID().uuidString)", + "CFBundleName": "SubstrateModels", + "CFBundlePackageType": "BNDL", + "CFBundleVersion": "1" + ] + let infoData = try PropertyListSerialization.data( + fromPropertyList: infoPlist, + format: .xml, + options: 0 + ) + try infoData.write(to: bundleURL.appendingPathComponent("Info.plist")) + + let sourceModelDirectory = try XCTUnwrap( + appBundle.url( + forResource: "SubstrateDataModel", + withExtension: "momd" + ) + ) + let destinationModelDirectory = bundleURL.appendingPathComponent( + SubstrateStorageParams.modelDirectory, + isDirectory: true + ) + try FileManager.default.copyItem( + at: sourceModelDirectory, + to: destinationModelDirectory + ) + + if let omittedVersion { + for fileExtension in ["omo", "mom"] { + let omittedModelURL = destinationModelDirectory.appendingPathComponent( + "\(omittedVersion.rawValue).\(fileExtension)" + ) + if FileManager.default.fileExists(atPath: omittedModelURL.path) { + try FileManager.default.removeItem(at: omittedModelURL) + } + } + } + + return try XCTUnwrap(Bundle(url: bundleURL)) + } + + private func durableStoreFamilySnapshot(at url: URL) throws -> [String: Data] { + let fileNames = try FileManager.default.contentsOfDirectory( + atPath: url.deletingLastPathComponent().path + ).filter { + $0.hasPrefix(url.lastPathComponent) + } + + return try Dictionary( + uniqueKeysWithValues: fileNames.map { fileName in + let fileURL = url.deletingLastPathComponent().appendingPathComponent(fileName) + return (fileName, try Data(contentsOf: fileURL)) + } + ) + } + + private func installRealSQLiteFamilyWithUncheckpointedWAL( + at url: URL + ) throws { + let snapshotDirectoryURL = testDirectory.appendingPathComponent( + "RealSQLiteWALSnapshot-\(UUID().uuidString)", + isDirectory: true + ) + try FileManager.default.createDirectory( + at: snapshotDirectoryURL, + withIntermediateDirectories: false + ) + defer { + try? FileManager.default.removeItem( + at: snapshotDirectoryURL + ) + } + + var database: OpaquePointer? + let openResult = sqlite3_open_v2( + url.path, + &database, + SQLITE_OPEN_READWRITE, + nil + ) + guard openResult == SQLITE_OK, let database else { + defer { + if let database { + sqlite3_close(database) + } + } + throw NSError( + domain: "SubstrateStorageMigrationSafetyTests.SQLite", + code: Int(openResult), + userInfo: [ + NSLocalizedDescriptionKey: + "Unable to open real SQLite WAL fixture" + ] + ) + } + + do { + try executeSQLite( + """ + PRAGMA journal_mode=WAL; + PRAGMA wal_autocheckpoint=0; + CREATE TABLE IF NOT EXISTS ZFEARLESSRECOVERYPROBE ( + ZVALUE INTEGER NOT NULL + ); + BEGIN IMMEDIATE; + INSERT INTO ZFEARLESSRECOVERYPROBE (ZVALUE) VALUES (1); + COMMIT; + """, + database: database + ) + + for suffix in ["", "-wal", "-shm", "-journal"] { + let sourceURL = URL( + fileURLWithPath: url.path + suffix + ) + guard FileManager.default.fileExists( + atPath: sourceURL.path + ) else { + continue + } + try FileManager.default.copyItem( + at: sourceURL, + to: snapshotDirectoryURL.appendingPathComponent( + sourceURL.lastPathComponent + ) + ) + } + } catch { + sqlite3_close(database) + throw error + } + sqlite3_close(database) + + for suffix in ["", "-wal", "-shm", "-journal"] { + let familyURL = URL( + fileURLWithPath: url.path + suffix + ) + if FileManager.default.fileExists( + atPath: familyURL.path + ) { + try FileManager.default.removeItem(at: familyURL) + } + + let snapshotURL = snapshotDirectoryURL + .appendingPathComponent( + familyURL.lastPathComponent + ) + if FileManager.default.fileExists( + atPath: snapshotURL.path + ) { + try FileManager.default.copyItem( + at: snapshotURL, + to: familyURL + ) + } + } + } + + private func sqliteInteger( + _ sql: String, + at url: URL + ) throws -> Int64 { + var database: OpaquePointer? + let openResult = sqlite3_open_v2( + url.path, + &database, + SQLITE_OPEN_READONLY, + nil + ) + guard openResult == SQLITE_OK, let database else { + defer { + if let database { + sqlite3_close(database) + } + } + throw NSError( + domain: "SubstrateStorageMigrationSafetyTests.SQLite", + code: Int(openResult), + userInfo: [ + NSLocalizedDescriptionKey: + "Unable to open SQLite integer query" + ] + ) + } + defer { + sqlite3_close(database) + } + + var statement: OpaquePointer? + let prepareResult = sqlite3_prepare_v2( + database, + sql, + -1, + &statement, + nil + ) + guard prepareResult == SQLITE_OK, let statement else { + throw NSError( + domain: "SubstrateStorageMigrationSafetyTests.SQLite", + code: Int(prepareResult), + userInfo: [ + NSLocalizedDescriptionKey: + "Unable to prepare SQLite integer query" + ] + ) + } + defer { + sqlite3_finalize(statement) + } + + let stepResult = sqlite3_step(statement) + guard stepResult == SQLITE_ROW else { + throw NSError( + domain: "SubstrateStorageMigrationSafetyTests.SQLite", + code: Int(stepResult), + userInfo: [ + NSLocalizedDescriptionKey: + "SQLite integer query returned no row" + ] + ) + } + + return sqlite3_column_int64(statement, 0) + } + + private func executeSQLite( + _ sql: String, + database: OpaquePointer + ) throws { + var errorMessage: UnsafeMutablePointer? + let executionResult = sqlite3_exec( + database, + sql, + nil, + nil, + &errorMessage + ) + defer { + sqlite3_free(errorMessage) + } + + guard executionResult == SQLITE_OK else { + let message = errorMessage + .map { String(cString: $0) } ?? + "unknown SQLite execution error" + throw NSError( + domain: "SubstrateStorageMigrationSafetyTests.SQLite", + code: Int(executionResult), + userInfo: [NSLocalizedDescriptionKey: message] + ) + } + } + + private func executeSQLite(_ sql: String, at url: URL) throws { + var database: OpaquePointer? + let openResult = sqlite3_open_v2( + url.path, + &database, + SQLITE_OPEN_READWRITE, + nil + ) + + guard openResult == SQLITE_OK, let database else { + defer { + if let database { + sqlite3_close(database) + } + } + + let message = database + .flatMap(sqlite3_errmsg) + .map { String(cString: $0) } ?? "unknown SQLite open error" + throw NSError( + domain: "SubstrateStorageMigrationSafetyTests.SQLite", + code: Int(openResult), + userInfo: [NSLocalizedDescriptionKey: message] + ) + } + defer { + sqlite3_close(database) + } + + try executeSQLite(sql, database: database) + } + + private func recoveryDirectories() throws -> [URL] { + let recoveryRoot = testDirectory.appendingPathComponent( + "SubstrateStoreRecovery", + isDirectory: true + ) + guard FileManager.default.fileExists(atPath: recoveryRoot.path) else { + return [] + } + + return try FileManager.default.contentsOfDirectory( + at: recoveryRoot, + includingPropertiesForKeys: nil + ).sorted { $0.lastPathComponent < $1.lastPathComponent } + } +} + +@objc(FearlessTestsDroppingAssetMigrationPolicy) +private final class DroppingAssetMigrationPolicy: NSEntityMigrationPolicy { + override func createDestinationInstances( + forSource _: NSManagedObject, + in _: NSEntityMapping, + manager _: NSMigrationManager + ) throws {} +} + +private class TemporaryStoreFootprintFileManager: FileManager { + private var sourceRoots = Set() + + private(set) var maximumTemporaryMainStoreCount = 0 + private(set) var temporaryMainStoreCountsBeforeRemoval: [Int] = [] + private(set) var removedTemporaryMainStoreCount = 0 + private(set) var copiedSharedMemorySidecar = false + + var temporaryRootsAreClean: Bool { + sourceRoots.allSatisfy { + !FileManager.default.fileExists(atPath: $0.path) + } + } + + override init() { + super.init() + } + + required init?(coder _: NSCoder) { + nil + } + + override func createDirectory( + at url: URL, + withIntermediateDirectories createIntermediates: Bool, + attributes: [FileAttributeKey: Any]? = nil + ) throws { + try super.createDirectory( + at: url, + withIntermediateDirectories: createIntermediates, + attributes: attributes + ) + + if url.lastPathComponent.hasPrefix("SubstrateStorageSource-") { + sourceRoots.insert(url.standardizedFileURL) + } + + sampleTemporaryStoreCount() + } + + override func copyItem(at sourceURL: URL, to destinationURL: URL) throws { + try super.copyItem(at: sourceURL, to: destinationURL) + + if + trackedSourceRoot(containing: destinationURL) != nil, + sourceURL.lastPathComponent.hasSuffix("-shm") + { + copiedSharedMemorySidecar = true + } + + sampleTemporaryStoreCount() + } + + override func removeItem(at url: URL) throws { + let removesTemporaryMainStore = isTrackedTemporaryMainStore(url) + + if removesTemporaryMainStore { + sampleTemporaryStoreCount() + temporaryMainStoreCountsBeforeRemoval.append( + temporaryMainStoreCount() + ) + } + + try super.removeItem(at: url) + + if removesTemporaryMainStore { + removedTemporaryMainStoreCount += 1 + } + + sampleTemporaryStoreCount() + } + + func isTrackedTemporaryMainStore(_ url: URL) -> Bool { + trackedSourceRoot(containing: url) != nil && + url.pathExtension == "sqlite" + } + + private func trackedSourceRoot(containing url: URL) -> URL? { + let standardizedPath = url.standardizedFileURL.path + + return sourceRoots.first { root in + standardizedPath == root.path || + standardizedPath.hasPrefix(root.path + "/") + } + } + + private func sampleTemporaryStoreCount() { + maximumTemporaryMainStoreCount = max( + maximumTemporaryMainStoreCount, + temporaryMainStoreCount() + ) + } + + private func temporaryMainStoreCount() -> Int { + sourceRoots.reduce(into: 0) { count, root in + guard + FileManager.default.fileExists(atPath: root.path), + let enumerator = FileManager.default.enumerator( + at: root, + includingPropertiesForKeys: nil + ) + else { + return + } + + while let fileURL = enumerator.nextObject() as? URL { + if fileURL.pathExtension == "sqlite" { + count += 1 + } + } + } + } +} + +private final class FailingTemporaryStoreCleanupFileManager: + TemporaryStoreFootprintFileManager +{ + private(set) var didInjectCleanupFailure = false + + override func removeItem(at url: URL) throws { + if !didInjectCleanupFailure, isTrackedTemporaryMainStore(url) { + didInjectCleanupFailure = true + throw InjectedSubstrateTemporaryCleanupError.failure + } + + try super.removeItem(at: url) + } +} + +private enum InjectedSubstrateStoreReplacementError: Error { + case failure +} + +private enum InjectedSubstrateCheckpointError: Error { + case failure +} + +private enum InjectedSubstrateTemporaryCleanupError: Error { + case failure +} + +private enum InjectedSubstrateBackupExclusionError: Error { + case failure +} + +@objc(FearlessTestsDisallowedSubstrateTransformablePayload) +private final class DisallowedSubstrateTransformablePayload: NSObject, NSSecureCoding { + static var supportsSecureCoding: Bool { + true + } + + let value: String + + init(value: String) { + self.value = value + super.init() + } + + required init?(coder: NSCoder) { + guard let value = coder.decodeObject( + of: NSString.self, + forKey: "value" + ) as? String else { + return nil + } + + self.value = value + super.init() + } + + func encode(with coder: NSCoder) { + coder.encode(value as NSString, forKey: "value") + } +} diff --git a/fearlessTests/Common/Migration/UserStorageCompatibilityMigrationTests.swift b/fearlessTests/Common/Migration/UserStorageCompatibilityMigrationTests.swift new file mode 100644 index 0000000000..34fcd1379e --- /dev/null +++ b/fearlessTests/Common/Migration/UserStorageCompatibilityMigrationTests.swift @@ -0,0 +1,7815 @@ +import CoreData +import IrohaCrypto +import SoraKeystore +import SQLite3 +import SSFModels +import XCTest +@testable import fearless + +final class UserStorageCompatibilityMigrationTests: XCTestCase { + private struct ChainSnapshot: Equatable { + let chainId: String + let accountId: String + let publicKey: Data + let cryptoType: Int16 + let ecosystem: String? + } + + private struct WalletSnapshot: Equatable { + let metaId: String + let name: String + let substrateAccountId: String? + let substratePublicKey: Data? + let tonAddress: Data? + let tonPublicKey: Data? + let tonContractVersion: String? + let assetsVisibility: [String: Bool] + let chainAccounts: [ChainSnapshot] + } + + private let legacyChecksum = "+oGDYB1AZIk54P/liFbJ3aXArqvhW7YLFAy04AChJ+s=" + private let currentChecksum = "dX3y/Aa+rMRI3ZxRibOGIkyCHwrc0zgNO9OoBnS5G/Y=" + + private var testDirectory: URL! + private var storeURL: URL { + testDirectory.appendingPathComponent("UserDataModel.sqlite") + } + + private var appBundle: Bundle { + Bundle(for: UserDataStorageFacade.self) + } + + override func setUpWithError() throws { + try super.setUpWithError() + + testDirectory = FileManager.default.temporaryDirectory + .appendingPathComponent("UserStorageCompatibilityMigrationTests") + .appendingPathComponent(UUID().uuidString) + try FileManager.default.createDirectory( + at: testDirectory, + withIntermediateDirectories: true + ) + } + + override func tearDownWithError() throws { + if let testDirectory { + try? FileManager.default.removeItem(at: testDirectory) + } + + try super.tearDownWithError() + } + + func testCrashConsistentReplacement_whenProcessDiesAtEveryCommitBoundary_thenBothStoreFamiliesRecoverExactly() throws { + let scenarios: [ + ( + boundary: CrashConsistentStoreReplacementBoundary, + shouldCommit: Bool + ) + ] = [ + (.preparingMarkerPersisted, false), + (.backupFileCopied(""), false), + (.backupFileCopied("-wal"), false), + (.backupFileCopied("-shm"), false), + (.backupFileCopied("-journal"), false), + (.committingMarkerPersisted, false), + (.replacementReturned, false), + (.destinationValidated, false), + (.destinationFamilySynced, false), + (.committedMarkerPersisted, true), + (.cleanupBackupFileRemoved(""), true), + (.cleanupBackupFileRemoved("-wal"), true), + (.cleanupBackupFileRemoved("-shm"), true), + (.cleanupBackupFileRemoved("-journal"), true), + (.cleanupBackupDirectoryRemoved, true), + (.cleanupMarkerRemoved, true), + (.cleanupTransactionDirectoryRemoved, true), + (.cleanupRecoveryRootRemoved, true), + (.transactionCleaned, true) + ] + + for storeName in [ + "UserDataModel.sqlite", + "SubstrateDataModel.sqlite" + ] { + for (index, scenario) in scenarios.enumerated() { + let scenarioDirectory = try makeSyntheticScenarioDirectory( + "\(storeName)-commit-\(index)" + ) + let liveStoreURL = scenarioDirectory + .appendingPathComponent(storeName) + let stagedStoreURL = scenarioDirectory + .appendingPathComponent("Staged-\(storeName)") + try writeSyntheticStoreFamily( + at: liveStoreURL, + seed: 0x10 + ) + try writeSyntheticStoreFamily( + at: stagedStoreURL, + seed: 0xA0 + ) + let originalFamily = try syntheticStoreFamilySnapshot( + at: liveStoreURL + ) + let committedFamily = try syntheticStoreFamilySnapshot( + at: stagedStoreURL + ) + var didInterrupt = false + let replacer = CrashConsistentStoreReplacer( + storeURL: liveStoreURL, + fileManager: .default, + storeReplacer: replaceSyntheticStoreFamily, + boundaryHook: { boundary in + guard + !didInterrupt, + boundary == scenario.boundary + else { + return + } + + didInterrupt = true + throw CrashConsistentStoreReplacementInterruption + .simulatedProcessDeath + } + ) + + XCTAssertThrowsError( + try replacer.replaceStore( + with: stagedStoreURL, + validateDestination: { _ in } + ), + "\(storeName) \(scenario.boundary)" + ) { error in + guard + let interruption = error as? CrashConsistentStoreReplacementInterruption, + case .simulatedProcessDeath = interruption + else { + return XCTFail("Unexpected error: \(error)") + } + } + XCTAssertTrue( + didInterrupt, + "The requested boundary was not reached" + ) + + let relaunched = CrashConsistentStoreReplacer( + storeURL: liveStoreURL, + fileManager: .default, + storeReplacer: replaceSyntheticStoreFamily + ) + try relaunched.reconcile { _ in } + + XCTAssertEqual( + try syntheticStoreFamilySnapshot(at: liveStoreURL), + scenario.shouldCommit + ? committedFamily + : originalFamily, + "\(storeName) \(scenario.boundary)" + ) + XCTAssertFalse(relaunched.hasPendingTransaction) + } + } + } + + func testCrashConsistentReplacement_whenProcessDiesAtEveryRollbackFileBoundary_thenBothStoreFamiliesRestoreExactly() throws { + let rollbackBoundaries: [ + CrashConsistentStoreReplacementBoundary + ] = [ + .rollbackMarkerPersisted, + .liveFileRemoved(""), + .liveFileRemoved("-wal"), + .liveFileRemoved("-shm"), + .liveFileRemoved("-journal"), + .rollbackTemporaryFileCopied(""), + .rollbackTemporaryFileCopied("-wal"), + .rollbackTemporaryFileCopied("-shm"), + .rollbackTemporaryFileCopied("-journal"), + .rollbackFileInstalled(""), + .rollbackFileInstalled("-wal"), + .rollbackFileInstalled("-shm"), + .rollbackFileInstalled("-journal"), + .rollbackFamilyVerified, + .cleanupBackupFileRemoved(""), + .cleanupBackupFileRemoved("-wal"), + .cleanupBackupFileRemoved("-shm"), + .cleanupBackupFileRemoved("-journal"), + .cleanupBackupDirectoryRemoved, + .cleanupMarkerRemoved, + .cleanupTransactionDirectoryRemoved, + .cleanupRecoveryRootRemoved, + .transactionCleaned + ] + + for storeName in [ + "UserDataModel.sqlite", + "SubstrateDataModel.sqlite" + ] { + for (index, requestedBoundary) in + rollbackBoundaries.enumerated() { + let scenarioDirectory = try makeSyntheticScenarioDirectory( + "\(storeName)-rollback-\(index)" + ) + let liveStoreURL = scenarioDirectory + .appendingPathComponent(storeName) + let stagedStoreURL = scenarioDirectory + .appendingPathComponent("Staged-\(storeName)") + try writeSyntheticStoreFamily( + at: liveStoreURL, + seed: 0x20 + ) + try writeSyntheticStoreFamily( + at: stagedStoreURL, + seed: 0xB0 + ) + let originalFamily = try syntheticStoreFamilySnapshot( + at: liveStoreURL + ) + var didInterrupt = false + let replacer = CrashConsistentStoreReplacer( + storeURL: liveStoreURL, + fileManager: .default, + storeReplacer: { targetURL, sourceURL in + try self.replaceSyntheticStoreFamily( + at: targetURL, + with: sourceURL + ) + throw UserStorageCommitTestError + .storeReplacementFailed + }, + boundaryHook: { boundary in + guard + !didInterrupt, + boundary == requestedBoundary + else { + return + } + + didInterrupt = true + throw CrashConsistentStoreReplacementInterruption + .simulatedProcessDeath + } + ) + + XCTAssertThrowsError( + try replacer.replaceStore( + with: stagedStoreURL, + validateDestination: { _ in } + ), + "\(storeName) \(requestedBoundary)" + ) + XCTAssertTrue( + didInterrupt, + "The requested rollback boundary was not reached" + ) + + let relaunched = CrashConsistentStoreReplacer( + storeURL: liveStoreURL, + fileManager: .default, + storeReplacer: replaceSyntheticStoreFamily + ) + try relaunched.reconcile { _ in } + + XCTAssertEqual( + try syntheticStoreFamilySnapshot(at: liveStoreURL), + originalFamily, + "\(storeName) \(requestedBoundary)" + ) + XCTAssertFalse(relaunched.hasPendingTransaction) + } + } + } + + func testCrashConsistentReplacement_whenProcessDiesDuringEveryCleanupDeletion_thenPreparingRollbackAndCommittedStatesConverge() throws { + let cleanupBoundaries: [ + CrashConsistentStoreReplacementBoundary + ] = [ + .cleanupMarkerTemporaryRemoved, + .cleanupBackupFileRemoved(""), + .cleanupBackupFileRemoved("-wal"), + .cleanupBackupFileRemoved("-shm"), + .cleanupBackupFileRemoved("-journal"), + .cleanupBackupDirectoryRemoved, + .cleanupMarkerRemoved, + .cleanupTransactionDirectoryRemoved, + .cleanupRecoveryRootRemoved, + .transactionCleaned + ] + + for transactionState in [ + "preparing", + "rolling-back", + "committed" + ] { + for (index, cleanupBoundary) in + cleanupBoundaries.enumerated() { + let scenarioDirectory = try makeSyntheticScenarioDirectory( + "cleanup-\(transactionState)-\(index)" + ) + let liveStoreURL = scenarioDirectory + .appendingPathComponent("UserDataModel.sqlite") + let stagedStoreURL = scenarioDirectory + .appendingPathComponent("Staged.sqlite") + try writeSyntheticStoreFamily( + at: liveStoreURL, + seed: 0x24 + ) + try writeSyntheticStoreFamily( + at: stagedStoreURL, + seed: 0xB4 + ) + let originalFamily = try syntheticStoreFamilySnapshot( + at: liveStoreURL + ) + let committedFamily = try syntheticStoreFamilySnapshot( + at: stagedStoreURL + ) + var didCreateInterruptedState = false + let stateCreator = CrashConsistentStoreReplacer( + storeURL: liveStoreURL, + fileManager: .default, + storeReplacer: { targetURL, sourceURL in + try self.replaceSyntheticStoreFamily( + at: targetURL, + with: sourceURL + ) + if transactionState == "rolling-back" { + throw UserStorageCommitTestError + .storeReplacementFailed + } + }, + boundaryHook: { boundary in + let requestedBoundary: + CrashConsistentStoreReplacementBoundary + switch transactionState { + case "preparing": + requestedBoundary = + .backupFileCopied("-journal") + case "rolling-back": + requestedBoundary = + .rollbackFamilyVerified + case "committed": + requestedBoundary = + .committedMarkerPersisted + default: + return + } + + guard + !didCreateInterruptedState, + boundary == requestedBoundary + else { + return + } + didCreateInterruptedState = true + throw CrashConsistentStoreReplacementInterruption + .simulatedProcessDeath + } + ) + XCTAssertThrowsError( + try stateCreator.replaceStore( + with: stagedStoreURL, + validateDestination: { _ in } + ) + ) + XCTAssertTrue(didCreateInterruptedState) + + let pendingMarkerURL = + stateCreator.transactionDirectoryURL + .appendingPathComponent( + "transaction.json.pending" + ) + try Data("bounded-incomplete-marker".utf8) + .write(to: pendingMarkerURL) + + var didInterruptCleanup = false + let cleanupAttempt = CrashConsistentStoreReplacer( + storeURL: liveStoreURL, + fileManager: .default, + storeReplacer: replaceSyntheticStoreFamily, + boundaryHook: { boundary in + guard + !didInterruptCleanup, + boundary == cleanupBoundary + else { + return + } + didInterruptCleanup = true + throw CrashConsistentStoreReplacementInterruption + .simulatedProcessDeath + } + ) + XCTAssertThrowsError( + try cleanupAttempt.reconcile { _ in }, + "\(transactionState) \(cleanupBoundary)" + ) + XCTAssertTrue( + didInterruptCleanup, + "Cleanup boundary was not reached" + ) + + let relaunched = CrashConsistentStoreReplacer( + storeURL: liveStoreURL, + fileManager: .default, + storeReplacer: replaceSyntheticStoreFamily + ) + try relaunched.reconcile { _ in } + + XCTAssertEqual( + try syntheticStoreFamilySnapshot(at: liveStoreURL), + transactionState == "committed" + ? committedFamily + : originalFamily, + "\(transactionState) \(cleanupBoundary)" + ) + XCTAssertFalse(relaunched.hasPendingTransaction) + } + } + } + + func testCrashConsistentReplacement_whenProcessDiesDuringEachLowLevelFamilyMutation_thenBothStoreFamiliesRestoreExactly() throws { + let suffixes = + CrashConsistentStoreReplacer.storeFamilySuffixes + + for storeName in [ + "UserDataModel.sqlite", + "SubstrateDataModel.sqlite" + ] { + for interruptionIndex in 0 ..< suffixes.count * 2 { + let scenarioDirectory = try makeSyntheticScenarioDirectory( + "\(storeName)-partial-\(interruptionIndex)" + ) + let liveStoreURL = scenarioDirectory + .appendingPathComponent(storeName) + let stagedStoreURL = scenarioDirectory + .appendingPathComponent("Staged-\(storeName)") + try writeSyntheticStoreFamily( + at: liveStoreURL, + seed: 0x30 + ) + try writeSyntheticStoreFamily( + at: stagedStoreURL, + seed: 0xC0 + ) + let originalFamily = try syntheticStoreFamilySnapshot( + at: liveStoreURL + ) + let replacer = CrashConsistentStoreReplacer( + storeURL: liveStoreURL, + fileManager: .default, + storeReplacer: { targetURL, sourceURL in + for (index, suffix) in suffixes.enumerated() { + try FileManager.default.removeItem( + at: self.syntheticFamilyURL( + targetURL, + suffix: suffix + ) + ) + if index == interruptionIndex { + throw CrashConsistentStoreReplacementInterruption + .simulatedProcessDeath + } + } + + for (index, suffix) in suffixes.enumerated() { + try FileManager.default.copyItem( + at: self.syntheticFamilyURL( + sourceURL, + suffix: suffix + ), + to: self.syntheticFamilyURL( + targetURL, + suffix: suffix + ) + ) + if suffixes.count + index == + interruptionIndex { + throw CrashConsistentStoreReplacementInterruption + .simulatedProcessDeath + } + } + } + ) + + XCTAssertThrowsError( + try replacer.replaceStore( + with: stagedStoreURL, + validateDestination: { _ in } + ) + ) + XCTAssertFalse( + FileManager.default.fileExists( + atPath: liveStoreURL.path + ) && interruptionIndex == 0 + ) + + let relaunched = CrashConsistentStoreReplacer( + storeURL: liveStoreURL, + fileManager: .default, + storeReplacer: replaceSyntheticStoreFamily + ) + try relaunched.reconcile { _ in } + + XCTAssertEqual( + try syntheticStoreFamilySnapshot(at: liveStoreURL), + originalFamily, + "\(storeName) mutation \(interruptionIndex)" + ) + XCTAssertFalse(relaunched.hasPendingTransaction) + } + } + } + + func testCrashConsistentReplacement_whenFirstMarkerWriteIsInterrupted_thenOnlyExactSafePendingTreeIsCleaned() throws { + let recoverableDirectory = try makeSyntheticScenarioDirectory( + "markerless-pending-recoverable" + ) + let recoverableStoreURL = recoverableDirectory + .appendingPathComponent("UserDataModel.sqlite") + try writeSyntheticStoreFamily( + at: recoverableStoreURL, + seed: 0x38 + ) + let recoverableFamily = try syntheticStoreFamilySnapshot( + at: recoverableStoreURL + ) + let recoverableReplacer = CrashConsistentStoreReplacer( + storeURL: recoverableStoreURL, + fileManager: .default, + storeReplacer: replaceSyntheticStoreFamily + ) + try FileManager.default.createDirectory( + at: recoverableReplacer.transactionDirectoryURL, + withIntermediateDirectories: true + ) + let recoverablePendingURL = + recoverableReplacer.transactionDirectoryURL + .appendingPathComponent("transaction.json.pending") + try Data("{\"schemaVersion\":1".utf8).write( + to: recoverablePendingURL + ) + + try recoverableReplacer.reconcile { _ in } + + XCTAssertEqual( + try syntheticStoreFamilySnapshot(at: recoverableStoreURL), + recoverableFamily + ) + XCTAssertFalse(recoverableReplacer.hasPendingTransaction) + + let unknownDirectory = try makeSyntheticScenarioDirectory( + "markerless-pending-unknown" + ) + let unknownStoreURL = unknownDirectory + .appendingPathComponent("UserDataModel.sqlite") + try writeSyntheticStoreFamily(at: unknownStoreURL, seed: 0x39) + let unknownFamily = try syntheticStoreFamilySnapshot( + at: unknownStoreURL + ) + let unknownReplacer = CrashConsistentStoreReplacer( + storeURL: unknownStoreURL, + fileManager: .default, + storeReplacer: replaceSyntheticStoreFamily + ) + try FileManager.default.createDirectory( + at: unknownReplacer.transactionDirectoryURL, + withIntermediateDirectories: true + ) + let unknownPendingURL = unknownReplacer.transactionDirectoryURL + .appendingPathComponent("transaction.json.pending") + let unrelatedURL = unknownReplacer.transactionDirectoryURL + .appendingPathComponent("unrelated") + try Data("partial-marker".utf8).write(to: unknownPendingURL) + try Data("must-survive".utf8).write(to: unrelatedURL) + + XCTAssertThrowsError( + try unknownReplacer.reconcile { _ in } + ) + XCTAssertEqual( + try Data(contentsOf: unknownPendingURL), + Data("partial-marker".utf8) + ) + XCTAssertEqual( + try Data(contentsOf: unrelatedURL), + Data("must-survive".utf8) + ) + XCTAssertEqual( + try syntheticStoreFamilySnapshot(at: unknownStoreURL), + unknownFamily + ) + XCTAssertTrue(unknownReplacer.hasPendingTransaction) + + let symlinkDirectory = try makeSyntheticScenarioDirectory( + "markerless-pending-symlink" + ) + let symlinkStoreURL = symlinkDirectory + .appendingPathComponent("UserDataModel.sqlite") + try writeSyntheticStoreFamily(at: symlinkStoreURL, seed: 0x3A) + let symlinkFamily = try syntheticStoreFamilySnapshot( + at: symlinkStoreURL + ) + let symlinkReplacer = CrashConsistentStoreReplacer( + storeURL: symlinkStoreURL, + fileManager: .default, + storeReplacer: replaceSyntheticStoreFamily + ) + try FileManager.default.createDirectory( + at: symlinkReplacer.transactionDirectoryURL, + withIntermediateDirectories: true + ) + let symlinkTargetURL = symlinkDirectory + .appendingPathComponent("pending-target") + try Data("must-survive".utf8).write(to: symlinkTargetURL) + try FileManager.default.createSymbolicLink( + at: symlinkReplacer.transactionDirectoryURL + .appendingPathComponent("transaction.json.pending"), + withDestinationURL: symlinkTargetURL + ) + + XCTAssertThrowsError( + try symlinkReplacer.reconcile { _ in } + ) + XCTAssertEqual( + try Data(contentsOf: symlinkTargetURL), + Data("must-survive".utf8) + ) + XCTAssertEqual( + try syntheticStoreFamilySnapshot(at: symlinkStoreURL), + symlinkFamily + ) + XCTAssertTrue(symlinkReplacer.hasPendingTransaction) + } + + func testCrashConsistentReplacement_whenPreparingBackupCopyIsPartial_thenCleansOnlyWithExactOriginalLiveFamily() throws { + for liveFamilyChanged in [false, true] { + let scenarioDirectory = try makeSyntheticScenarioDirectory( + liveFamilyChanged + ? "partial-preparing-backup-changed-live" + : "partial-preparing-backup-exact-live" + ) + let liveStoreURL = scenarioDirectory + .appendingPathComponent("UserDataModel.sqlite") + let stagedStoreURL = scenarioDirectory + .appendingPathComponent("Staged.sqlite") + try writeSyntheticStoreFamily(at: liveStoreURL, seed: 0x3B) + try writeSyntheticStoreFamily(at: stagedStoreURL, seed: 0xCB) + let originalFamily = try syntheticStoreFamilySnapshot( + at: liveStoreURL + ) + let interrupted = CrashConsistentStoreReplacer( + storeURL: liveStoreURL, + fileManager: .default, + storeReplacer: replaceSyntheticStoreFamily, + boundaryHook: { boundary in + guard boundary == .preparingMarkerPersisted else { + return + } + throw CrashConsistentStoreReplacementInterruption + .simulatedProcessDeath + } + ) + XCTAssertThrowsError( + try interrupted.replaceStore( + with: stagedStoreURL, + validateDestination: { _ in } + ) + ) + + let backupDirectoryURL = + interrupted.transactionDirectoryURL + .appendingPathComponent( + "OriginalStoreFamily", + isDirectory: true + ) + try FileManager.default.createDirectory( + at: backupDirectoryURL, + withIntermediateDirectories: false + ) + let partialBackupURL = backupDirectoryURL + .appendingPathComponent(liveStoreURL.lastPathComponent) + try Data("partial-copy".utf8).write(to: partialBackupURL) + + if liveFamilyChanged { + try Data("changed-live-main".utf8).write( + to: liveStoreURL + ) + } + let familyBeforeReconcile = + try syntheticStoreFamilySnapshot(at: liveStoreURL) + let relaunched = CrashConsistentStoreReplacer( + storeURL: liveStoreURL, + fileManager: .default, + storeReplacer: replaceSyntheticStoreFamily + ) + + if liveFamilyChanged { + XCTAssertThrowsError( + try relaunched.reconcile { _ in } + ) + XCTAssertEqual( + try syntheticStoreFamilySnapshot(at: liveStoreURL), + familyBeforeReconcile + ) + XCTAssertEqual( + try Data(contentsOf: partialBackupURL), + Data("partial-copy".utf8) + ) + XCTAssertTrue(relaunched.hasPendingTransaction) + } else { + try relaunched.reconcile { _ in } + XCTAssertEqual( + try syntheticStoreFamilySnapshot(at: liveStoreURL), + originalFamily + ) + XCTAssertFalse(relaunched.hasPendingTransaction) + } + } + } + + func testCrashConsistentReplacement_whenMarkerIsAdversarial_thenFailsClosedWithoutChangingLiveFamily() throws { + for mutation in [ + "duplicate-member", + "path-traversal", + "digest-mismatch", + "size-mismatch", + "noncanonical-order", + "top-duplicate-field", + "top-unknown-field", + "member-duplicate-field", + "member-unknown-field", + "escaped-field-name", + "wrong-top-field-type", + "wrong-member-field-type", + "wrong-family-container-type", + "oversized", + "markerless-nonempty" + ] { + let scenarioDirectory = try makeSyntheticScenarioDirectory( + "adversarial-\(mutation)" + ) + let liveStoreURL = scenarioDirectory + .appendingPathComponent("UserDataModel.sqlite") + let stagedStoreURL = scenarioDirectory + .appendingPathComponent("Staged.sqlite") + try writeSyntheticStoreFamily(at: liveStoreURL, seed: 0x40) + try writeSyntheticStoreFamily(at: stagedStoreURL, seed: 0xD0) + let originalFamily = try syntheticStoreFamilySnapshot( + at: liveStoreURL + ) + var didInterrupt = false + let interrupted = CrashConsistentStoreReplacer( + storeURL: liveStoreURL, + fileManager: .default, + storeReplacer: replaceSyntheticStoreFamily, + boundaryHook: { boundary in + guard + !didInterrupt, + boundary == .committingMarkerPersisted + else { + return + } + + didInterrupt = true + throw CrashConsistentStoreReplacementInterruption + .simulatedProcessDeath + } + ) + XCTAssertThrowsError( + try interrupted.replaceStore( + with: stagedStoreURL, + validateDestination: { _ in } + ) + ) + XCTAssertTrue(didInterrupt) + + let markerURL = interrupted.transactionDirectoryURL + .appendingPathComponent("transaction.json") + if mutation == "oversized" { + try Data(repeating: 0x7B, count: 64 * 1024 + 1) + .write(to: markerURL) + } else if mutation == "markerless-nonempty" { + try FileManager.default.removeItem(at: markerURL) + } else if [ + "top-duplicate-field", + "top-unknown-field", + "member-duplicate-field", + "member-unknown-field", + "escaped-field-name", + "wrong-top-field-type", + "wrong-member-field-type" + ].contains(mutation) { + let originalJSON = try XCTUnwrap( + String( + data: Data(contentsOf: markerURL), + encoding: .utf8 + ) + ) + let mutatedJSON: String + switch mutation { + case "top-duplicate-field": + mutatedJSON = + String(originalJSON.dropLast()) + + ",\"state\":\"committing\"}" + case "top-unknown-field": + mutatedJSON = + String(originalJSON.dropLast()) + + ",\"unexpected\":true}" + case "member-duplicate-field": + mutatedJSON = originalJSON.replacingOccurrences( + of: "\"suffix\":\"\"", + with: "\"suffix\":\"\",\"suffix\":\"\"" + ) + case "member-unknown-field": + mutatedJSON = originalJSON.replacingOccurrences( + of: "\"suffix\":\"\"", + with: "\"suffix\":\"\",\"unexpected\":0" + ) + case "escaped-field-name": + mutatedJSON = originalJSON.replacingOccurrences( + of: "\"state\"", + with: "\"st\\u0061te\"" + ) + case "wrong-top-field-type": + mutatedJSON = originalJSON.replacingOccurrences( + of: "\"schemaVersion\":1", + with: "\"schemaVersion\":\"1\"" + ) + case "wrong-member-field-type": + mutatedJSON = originalJSON.replacingOccurrences( + of: "\"byteCount\":31", + with: "\"byteCount\":\"31\"" + ) + default: + throw UserStorageCommitTestError + .storeReplacementFailed + } + XCTAssertNotEqual(mutatedJSON, originalJSON) + try Data(mutatedJSON.utf8).write(to: markerURL) + } else { + var marker = try XCTUnwrap( + JSONSerialization.jsonObject( + with: Data(contentsOf: markerURL) + ) as? [String: Any] + ) + var family = try XCTUnwrap( + marker["originalFamily"] + as? [[String: Any]] + ) + + switch mutation { + case "duplicate-member": + family.append(family[0]) + case "path-traversal": + family[0]["suffix"] = "../UserDataModel.sqlite" + case "digest-mismatch": + family[0]["sha256"] = String( + repeating: "0", + count: 64 + ) + case "size-mismatch": + let oldSize = ( + family[0]["byteCount"] as? NSNumber + )?.uint64Value ?? 0 + family[0]["byteCount"] = NSNumber( + value: oldSize + 1 + ) + case "noncanonical-order": + family.reverse() + case "wrong-family-container-type": + marker["originalFamily"] = + "not-an-array" + default: + XCTFail("Unhandled mutation \(mutation)") + } + + if mutation != "wrong-family-container-type" { + marker["originalFamily"] = family + } + try JSONSerialization.data( + withJSONObject: marker, + options: [.sortedKeys] + ).write(to: markerURL) + } + + let relaunched = CrashConsistentStoreReplacer( + storeURL: liveStoreURL, + fileManager: .default, + storeReplacer: replaceSyntheticStoreFamily + ) + XCTAssertThrowsError( + try relaunched.reconcile { _ in }, + mutation + ) + XCTAssertEqual( + try syntheticStoreFamilySnapshot(at: liveStoreURL), + originalFamily, + mutation + ) + XCTAssertTrue(relaunched.hasPendingTransaction) + } + } + + func testCrashConsistentReplacement_whenTransactionTreeContainsUnknownFileOrDirectory_thenFailsClosed() throws { + for mutation in [ + "top-file", + "top-directory", + "backup-file", + "backup-directory" + ] { + let scenarioDirectory = try makeSyntheticScenarioDirectory( + "unknown-tree-\(mutation)" + ) + let liveStoreURL = scenarioDirectory + .appendingPathComponent("UserDataModel.sqlite") + let stagedStoreURL = scenarioDirectory + .appendingPathComponent("Staged.sqlite") + try writeSyntheticStoreFamily(at: liveStoreURL, seed: 0x45) + try writeSyntheticStoreFamily(at: stagedStoreURL, seed: 0xD5) + let originalFamily = try syntheticStoreFamilySnapshot( + at: liveStoreURL + ) + let interrupted = CrashConsistentStoreReplacer( + storeURL: liveStoreURL, + fileManager: .default, + storeReplacer: replaceSyntheticStoreFamily, + boundaryHook: { boundary in + guard boundary == .committingMarkerPersisted else { + return + } + throw CrashConsistentStoreReplacementInterruption + .simulatedProcessDeath + } + ) + XCTAssertThrowsError( + try interrupted.replaceStore( + with: stagedStoreURL, + validateDestination: { _ in } + ) + ) + + let backupDirectory = interrupted + .transactionDirectoryURL + .appendingPathComponent( + "OriginalStoreFamily", + isDirectory: true + ) + let unknownURL: URL + let isDirectory: Bool + switch mutation { + case "top-file": + unknownURL = interrupted.transactionDirectoryURL + .appendingPathComponent("unknown") + isDirectory = false + case "top-directory": + unknownURL = interrupted.transactionDirectoryURL + .appendingPathComponent( + "UnknownDirectory", + isDirectory: true + ) + isDirectory = true + case "backup-file": + unknownURL = backupDirectory + .appendingPathComponent("unknown") + isDirectory = false + case "backup-directory": + unknownURL = backupDirectory + .appendingPathComponent( + "UnknownDirectory", + isDirectory: true + ) + isDirectory = true + default: + throw UserStorageCommitTestError + .storeReplacementFailed + } + + if isDirectory { + try FileManager.default.createDirectory( + at: unknownURL, + withIntermediateDirectories: false + ) + } else { + try Data("unknown".utf8).write(to: unknownURL) + } + + let relaunched = CrashConsistentStoreReplacer( + storeURL: liveStoreURL, + fileManager: .default, + storeReplacer: replaceSyntheticStoreFamily + ) + XCTAssertThrowsError( + try relaunched.reconcile { _ in }, + mutation + ) + XCTAssertEqual( + try syntheticStoreFamilySnapshot(at: liveStoreURL), + originalFamily + ) + XCTAssertTrue(relaunched.hasPendingTransaction) + } + } + + func testCrashConsistentReplacement_whenCommittedFamilyOrSemanticValidationChanges_thenRestoresOriginalFamily() throws { + for mutation in [ + "live-bytes", + "semantic-validation", + "validator-mutates-live" + ] { + let scenarioDirectory = try makeSyntheticScenarioDirectory( + "committed-\(mutation)" + ) + let liveStoreURL = scenarioDirectory + .appendingPathComponent("UserDataModel.sqlite") + let stagedStoreURL = scenarioDirectory + .appendingPathComponent("Staged.sqlite") + try writeSyntheticStoreFamily(at: liveStoreURL, seed: 0x50) + try writeSyntheticStoreFamily(at: stagedStoreURL, seed: 0xE0) + let originalFamily = try syntheticStoreFamilySnapshot( + at: liveStoreURL + ) + var didInterrupt = false + let interrupted = CrashConsistentStoreReplacer( + storeURL: liveStoreURL, + fileManager: .default, + storeReplacer: replaceSyntheticStoreFamily, + boundaryHook: { boundary in + guard + !didInterrupt, + boundary == .committedMarkerPersisted + else { + return + } + + didInterrupt = true + throw CrashConsistentStoreReplacementInterruption + .simulatedProcessDeath + } + ) + XCTAssertThrowsError( + try interrupted.replaceStore( + with: stagedStoreURL, + validateDestination: { _ in } + ) + ) + + if mutation == "live-bytes" { + try Data("tampered-after-commit".utf8).write( + to: liveStoreURL + ) + } + + let relaunched = CrashConsistentStoreReplacer( + storeURL: liveStoreURL, + fileManager: .default, + storeReplacer: replaceSyntheticStoreFamily + ) + try relaunched.reconcile { _ in + if mutation == "semantic-validation" { + throw UserStorageCommitTestError + .storeReplacementFailed + } else if mutation == "validator-mutates-live" { + try Data("validator-mutated-live".utf8).write( + to: liveStoreURL + ) + } + } + + XCTAssertEqual( + try syntheticStoreFamilySnapshot(at: liveStoreURL), + originalFamily, + mutation + ) + XCTAssertFalse(relaunched.hasPendingTransaction) + } + } + + func testCrashConsistentReplacement_whenCommittedValidationMutatesItsStoreAndCleanupIsInterrupted_thenLiveCommitStillConverges() throws { + let scenarioDirectory = try makeSyntheticScenarioDirectory( + "committed-private-validation" + ) + let liveStoreURL = scenarioDirectory + .appendingPathComponent("UserDataModel.sqlite") + let stagedStoreURL = scenarioDirectory + .appendingPathComponent("Staged.sqlite") + try writeSyntheticStoreFamily(at: liveStoreURL, seed: 0x52) + try writeSyntheticStoreFamily(at: stagedStoreURL, seed: 0xE2) + let committedFamily = try syntheticStoreFamilySnapshot( + at: stagedStoreURL + ) + let interrupted = CrashConsistentStoreReplacer( + storeURL: liveStoreURL, + fileManager: .default, + storeReplacer: replaceSyntheticStoreFamily, + boundaryHook: { boundary in + guard boundary == .committedMarkerPersisted else { + return + } + throw CrashConsistentStoreReplacementInterruption + .simulatedProcessDeath + } + ) + XCTAssertThrowsError( + try interrupted.replaceStore( + with: stagedStoreURL, + validateDestination: { _ in } + ) + ) + + var validationStoreURL: URL? + var didInterruptCleanup = false + let cleanupAttempt = CrashConsistentStoreReplacer( + storeURL: liveStoreURL, + fileManager: .default, + storeReplacer: replaceSyntheticStoreFamily, + boundaryHook: { boundary in + guard + !didInterruptCleanup, + boundary == .cleanupBackupFileRemoved("") + else { + return + } + didInterruptCleanup = true + throw CrashConsistentStoreReplacementInterruption + .simulatedProcessDeath + } + ) + XCTAssertThrowsError( + try cleanupAttempt.reconcile { privateStoreURL in + validationStoreURL = privateStoreURL + XCTAssertNotEqual( + privateStoreURL.standardizedFileURL, + liveStoreURL.standardizedFileURL + ) + + for suffix in + CrashConsistentStoreReplacer.storeFamilySuffixes { + let memberURL = self.syntheticFamilyURL( + privateStoreURL, + suffix: suffix + ) + guard FileManager.default.fileExists( + atPath: memberURL.path + ) else { + continue + } + try Data("validator-mutated-\(suffix)".utf8) + .write(to: memberURL) + } + } + ) + XCTAssertTrue(didInterruptCleanup) + XCTAssertEqual( + try syntheticStoreFamilySnapshot(at: liveStoreURL), + committedFamily + ) + let capturedValidationStoreURL = try XCTUnwrap( + validationStoreURL + ) + XCTAssertFalse( + FileManager.default.fileExists( + atPath: capturedValidationStoreURL.path + ) + ) + + let relaunched = CrashConsistentStoreReplacer( + storeURL: liveStoreURL, + fileManager: .default, + storeReplacer: replaceSyntheticStoreFamily + ) + try relaunched.reconcile { privateStoreURL in + XCTAssertNotEqual( + privateStoreURL.standardizedFileURL, + liveStoreURL.standardizedFileURL + ) + XCTAssertEqual( + try self.syntheticStoreFamilySnapshot( + at: privateStoreURL + ), + committedFamily + ) + } + + XCTAssertEqual( + try syntheticStoreFamilySnapshot(at: liveStoreURL), + committedFamily + ) + XCTAssertFalse(relaunched.hasPendingTransaction) + } + + func testCrashConsistentReplacement_whenMarkerBackupOrLiveMemberIsSymlink_thenFailsClosed() throws { + for mutation in ["marker", "backup-main", "live-main"] { + let scenarioDirectory = try makeSyntheticScenarioDirectory( + "symlink-\(mutation)" + ) + let liveStoreURL = scenarioDirectory + .appendingPathComponent("UserDataModel.sqlite") + let stagedStoreURL = scenarioDirectory + .appendingPathComponent("Staged.sqlite") + try writeSyntheticStoreFamily(at: liveStoreURL, seed: 0x55) + try writeSyntheticStoreFamily(at: stagedStoreURL, seed: 0xF0) + var didInterrupt = false + let interrupted = CrashConsistentStoreReplacer( + storeURL: liveStoreURL, + fileManager: .default, + storeReplacer: replaceSyntheticStoreFamily, + boundaryHook: { boundary in + guard + !didInterrupt, + boundary == .committingMarkerPersisted + else { + return + } + + didInterrupt = true + throw CrashConsistentStoreReplacementInterruption + .simulatedProcessDeath + } + ) + XCTAssertThrowsError( + try interrupted.replaceStore( + with: stagedStoreURL, + validateDestination: { _ in } + ) + ) + + let markerURL = interrupted.transactionDirectoryURL + .appendingPathComponent("transaction.json") + let backupMainURL = interrupted.transactionDirectoryURL + .appendingPathComponent( + "OriginalStoreFamily", + isDirectory: true + ) + .appendingPathComponent(liveStoreURL.lastPathComponent) + + switch mutation { + case "marker": + let displacedURL = scenarioDirectory + .appendingPathComponent("DisplacedMarker") + try FileManager.default.moveItem( + at: markerURL, + to: displacedURL + ) + try FileManager.default.createSymbolicLink( + at: markerURL, + withDestinationURL: displacedURL + ) + case "backup-main": + try FileManager.default.removeItem(at: backupMainURL) + try FileManager.default.createSymbolicLink( + at: backupMainURL, + withDestinationURL: liveStoreURL + ) + case "live-main": + let displacedURL = scenarioDirectory + .appendingPathComponent("DisplacedLiveMain") + try FileManager.default.moveItem( + at: liveStoreURL, + to: displacedURL + ) + try FileManager.default.createSymbolicLink( + at: liveStoreURL, + withDestinationURL: displacedURL + ) + default: + XCTFail("Unhandled mutation \(mutation)") + } + + let relaunched = CrashConsistentStoreReplacer( + storeURL: liveStoreURL, + fileManager: .default, + storeReplacer: replaceSyntheticStoreFamily + ) + XCTAssertThrowsError( + try relaunched.reconcile { _ in }, + mutation + ) + XCTAssertTrue(relaunched.hasPendingTransaction) + } + } + + func testCrashConsistentReplacement_whenLiveFamilyIsUnsafeOrOrphaned_thenNeverReportsAnEmptyStore() throws { + let orphanDirectory = try makeSyntheticScenarioDirectory( + "orphan-sidecar" + ) + let orphanStoreURL = orphanDirectory + .appendingPathComponent("UserDataModel.sqlite") + try Data("orphan-wal".utf8).write( + to: syntheticFamilyURL( + orphanStoreURL, + suffix: "-wal" + ) + ) + let orphanReplacer = CrashConsistentStoreReplacer( + storeURL: orphanStoreURL, + fileManager: .default, + storeReplacer: replaceSyntheticStoreFamily + ) + XCTAssertThrowsError( + try orphanReplacer.liveStoreExistsSafely() + ) + + let symlinkDirectory = try makeSyntheticScenarioDirectory( + "symlink-sidecar" + ) + let symlinkStoreURL = symlinkDirectory + .appendingPathComponent("SubstrateDataModel.sqlite") + try Data("main".utf8).write(to: symlinkStoreURL) + let symlinkTargetURL = symlinkDirectory + .appendingPathComponent("SidecarTarget") + try Data("target".utf8).write(to: symlinkTargetURL) + try FileManager.default.createSymbolicLink( + at: syntheticFamilyURL( + symlinkStoreURL, + suffix: "-shm" + ), + withDestinationURL: symlinkTargetURL + ) + let symlinkReplacer = CrashConsistentStoreReplacer( + storeURL: symlinkStoreURL, + fileManager: .default, + storeReplacer: replaceSyntheticStoreFamily + ) + XCTAssertThrowsError( + try symlinkReplacer.liveStoreExistsSafely() + ) + + let directoryStoreRoot = try makeSyntheticScenarioDirectory( + "directory-main" + ) + let directoryStoreURL = directoryStoreRoot + .appendingPathComponent("UserDataModel.sqlite") + try FileManager.default.createDirectory( + at: directoryStoreURL, + withIntermediateDirectories: false + ) + let directoryReplacer = CrashConsistentStoreReplacer( + storeURL: directoryStoreURL, + fileManager: .default, + storeReplacer: replaceSyntheticStoreFamily + ) + XCTAssertThrowsError( + try directoryReplacer.liveStoreExistsSafely() + ) + } + + func testCrashConsistentReplacement_whenStagedFamilyIsUnsafeOrAliasesProtectedPaths_thenRejectsBeforeMutation() throws { + for mutation in [ + "same-live-path", + "dotdot-live-alias", + "hardlink-main", + "intra-family-hardlink", + "symlink-main", + "symlink-sidecar", + "orphan-sidecar", + "inside-recovery-root" + ] { + let scenarioDirectory = try makeSyntheticScenarioDirectory( + "unsafe-stage-\(mutation)" + ) + let liveStoreURL = scenarioDirectory + .appendingPathComponent("UserDataModel.sqlite") + try writeSyntheticStoreFamily(at: liveStoreURL, seed: 0x5A) + let originalFamily = try syntheticStoreFamilySnapshot( + at: liveStoreURL + ) + let stagedStoreURL: URL + + switch mutation { + case "same-live-path": + stagedStoreURL = liveStoreURL + case "dotdot-live-alias": + let aliasDirectory = scenarioDirectory + .appendingPathComponent( + "Alias", + isDirectory: true + ) + try FileManager.default.createDirectory( + at: aliasDirectory, + withIntermediateDirectories: false + ) + stagedStoreURL = URL( + fileURLWithPath: + "\(aliasDirectory.path)/../\(liveStoreURL.lastPathComponent)" + ) + case "hardlink-main": + stagedStoreURL = scenarioDirectory + .appendingPathComponent("Staged.sqlite") + try FileManager.default.linkItem( + at: liveStoreURL, + to: stagedStoreURL + ) + case "intra-family-hardlink": + stagedStoreURL = scenarioDirectory + .appendingPathComponent("Staged.sqlite") + try writeSyntheticStoreFamily( + at: stagedStoreURL, + seed: 0xEA + ) + let stagedWALURL = syntheticFamilyURL( + stagedStoreURL, + suffix: "-wal" + ) + try FileManager.default.removeItem(at: stagedWALURL) + try FileManager.default.linkItem( + at: stagedStoreURL, + to: stagedWALURL + ) + case "symlink-main": + stagedStoreURL = scenarioDirectory + .appendingPathComponent("Staged.sqlite") + try FileManager.default.createSymbolicLink( + at: stagedStoreURL, + withDestinationURL: liveStoreURL + ) + case "symlink-sidecar": + stagedStoreURL = scenarioDirectory + .appendingPathComponent("Staged.sqlite") + try Data("staged-main".utf8).write( + to: stagedStoreURL + ) + try FileManager.default.createSymbolicLink( + at: syntheticFamilyURL( + stagedStoreURL, + suffix: "-wal" + ), + withDestinationURL: syntheticFamilyURL( + liveStoreURL, + suffix: "-wal" + ) + ) + case "orphan-sidecar": + stagedStoreURL = scenarioDirectory + .appendingPathComponent("Staged.sqlite") + try Data("orphan-staged-wal".utf8).write( + to: syntheticFamilyURL( + stagedStoreURL, + suffix: "-wal" + ) + ) + case "inside-recovery-root": + let recoveryDirectory = scenarioDirectory + .appendingPathComponent( + ".FearlessStoreReplacement/Attacker", + isDirectory: true + ) + try FileManager.default.createDirectory( + at: recoveryDirectory, + withIntermediateDirectories: true + ) + stagedStoreURL = recoveryDirectory + .appendingPathComponent("Staged.sqlite") + try Data("staged-main".utf8).write( + to: stagedStoreURL + ) + default: + throw UserStorageCommitTestError + .storeReplacementFailed + } + + var replacementWasCalled = false + let replacer = CrashConsistentStoreReplacer( + storeURL: liveStoreURL, + fileManager: .default, + storeReplacer: { _, _ in + replacementWasCalled = true + } + ) + XCTAssertThrowsError( + try replacer.replaceStore( + with: stagedStoreURL, + validateDestination: { _ in } + ), + mutation + ) + XCTAssertFalse(replacementWasCalled, mutation) + XCTAssertEqual( + try syntheticStoreFamilySnapshot(at: liveStoreURL), + originalFamily, + mutation + ) + XCTAssertFalse(replacer.hasPendingTransaction) + } + } + + func testCrashConsistentReplacement_whenPreMarkerDirectoryIsEmpty_thenSafelyCleansItOnlyAfterValidatingLiveFamily() throws { + let scenarioDirectory = try makeSyntheticScenarioDirectory( + "empty-pre-marker" + ) + let liveStoreURL = scenarioDirectory + .appendingPathComponent("UserDataModel.sqlite") + try writeSyntheticStoreFamily(at: liveStoreURL, seed: 0x58) + let replacer = CrashConsistentStoreReplacer( + storeURL: liveStoreURL, + fileManager: .default, + storeReplacer: replaceSyntheticStoreFamily + ) + try FileManager.default.createDirectory( + at: replacer.transactionDirectoryURL, + withIntermediateDirectories: true + ) + + try replacer.reconcile { _ in } + + XCTAssertFalse(replacer.hasPendingTransaction) + XCTAssertTrue(try replacer.liveStoreExistsSafely()) + } + + func testCrashConsistentReplacement_whenPathsShareDirectory_thenUserAndSubstrateTransactionsCannotCollide() throws { + try writeSyntheticStoreFamily(at: storeURL, seed: 0x60) + let substrateURL = testDirectory + .appendingPathComponent("SubstrateDataModel.sqlite") + try writeSyntheticStoreFamily(at: substrateURL, seed: 0x70) + + let userReplacer = CrashConsistentStoreReplacer( + storeURL: storeURL, + fileManager: .default, + storeReplacer: replaceSyntheticStoreFamily + ) + let substrateReplacer = CrashConsistentStoreReplacer( + storeURL: substrateURL, + fileManager: .default, + storeReplacer: replaceSyntheticStoreFamily + ) + + XCTAssertNotEqual( + userReplacer.transactionDirectoryURL.standardizedFileURL, + substrateReplacer.transactionDirectoryURL + .standardizedFileURL + ) + XCTAssertEqual( + userReplacer.transactionDirectoryURL + .deletingLastPathComponent(), + substrateReplacer.transactionDirectoryURL + .deletingLastPathComponent() + ) + } + + func testCompatibilityModels_whenBundled_thenHaveExactFingerprintsAndMigrationEdges() throws { + let expectedLegacyHashes = [ + "CDAccountInfo": "q3sBYA7JunDkaJOaTevIdD/b1Ou+4Qrdi0twbrghhf8=", + "CDAssetVisibility": "IYchCCjZ4/tlPbRVhOIyAtOY1J6500tV0GoXt7gb5Ac=", + "CDChainAccount": "AVvqEhz7A8aiXmrGYNuGkrLTmUy2GdEzHx5uh1rR+6Y=", + "CDChainSettings": "GHqxJZE9c7xLExSYsWfzpktGdVvQqqFyACPiYKaLgsQ=", + "CDCurrency": "iF3edtRVNvP9XKRuZuVOU5YFx50BLwbUqlaPKdBHgmk=", + "CDCustomChainNode": "/mUD0P2skKW0f6ao8jt07yDn3BARGk3Ve9qc2W6Zl8k=", + "CDMetaAccount": "teqUP1fwr7jL/V0Tm33An9SPbBlLr9L6ioRO8fkPIjA=" + ] + let expectedCurrentHashes = expectedLegacyHashes.merging( + ["CDChainAccount": "jvrdZWLn1+K0pe0MCQsxJP2fdOiiAAYkoSJf4x1Y5JU="] + ) { _, current in current } + + let legacyURL = try directCompatibilityModelURL(for: .version13) + let currentURL = try directCompatibilityModelURL(for: .version14) + let legacyModel = try loadModel(at: legacyURL) + let currentModel = try loadModel(at: currentURL) + + XCTAssertEqual(versionHashes(for: legacyModel), expectedLegacyHashes) + XCTAssertEqual(versionHashes(for: currentModel), expectedCurrentHashes) + XCTAssertEqual( + try storeChecksum(for: legacyModel, named: "legacy-fingerprint.sqlite"), + legacyChecksum + ) + XCTAssertEqual( + try storeChecksum(for: currentModel, named: "current-fingerprint.sqlite"), + currentChecksum + ) + + XCTAssertEqual(UserStorageVersion.version12.nextVersion(), .version14) + XCTAssertEqual(UserStorageVersion.version13.nextVersion(), .version14) + XCTAssertNil(UserStorageVersion.version14.nextVersion()) + XCTAssertEqual(UserStorageVersion.current, .version14) + } + + func testCurrentModel_whenLoaded_thenEveryEntityResolvesToTheAppRuntimeClass() throws { + let expectedClasses: [String: NSManagedObject.Type] = [ + "CDAccountInfo": fearless.CDAccountInfo.self, + "CDAssetVisibility": fearless.CDAssetVisibility.self, + "CDChainAccount": fearless.CDChainAccount.self, + "CDChainSettings": fearless.CDChainSettings.self, + "CDCurrency": fearless.CDCurrency.self, + "CDCustomChainNode": fearless.CDCustomChainNode.self, + "CDMetaAccount": fearless.CDMetaAccount.self + ] + let currentModel = try model(for: .version14) + + for (entityName, expectedClass) in expectedClasses { + let entity = try XCTUnwrap(currentModel.entitiesByName[entityName]) + let className = try XCTUnwrap(entity.managedObjectClassName) + let resolvedClass = try XCTUnwrap( + NSClassFromString(className) as? NSManagedObject.Type, + "\(className) must resolve to an NSManagedObject class" + ) + + XCTAssertEqual( + ObjectIdentifier(resolvedClass), + ObjectIdentifier(expectedClass), + "\(entityName) resolved to a different runtime class than the app alias" + ) + } + } + + func testMigration_whenVersion2PersistentFixture_thenPreservesWalletsAndAccountsAndRoutesExistingPincodeToPinOnRepeatedLaunch() throws { + let sourceModel = try model(for: .version2) + try createStore(at: storeURL, model: sourceModel) { context in + try self.insertVersion2WalletFixture( + in: context, + model: sourceModel + ) + } + let pincode = Data("246810".utf8) + let keystore = InMemoryKeychain() + try keystore.saveKey( + pincode, + with: fearless.KeystoreTag.pincode.rawValue + ) + let migrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: keystore, + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle + ) + + XCTAssertTrue(try migrator.performMigration()) + XCTAssertFalse(try migrator.performMigration()) + XCTAssertEqual( + try walletSnapshots( + from: storeURL, + model: model(for: .version14) + ), + version2WalletFixtureSnapshots() + ) + + let facade = UserDataStorageFacade( + modelURL: UserStorageVersion.version14.modelURL( + in: appBundle, + legacyModelDirectory: UserStorageParams.modelDirectory + ), + databaseDirectory: testDirectory + ) + defer { + try? facade.databaseService.close() + } + let selectedWalletSettings = SelectedWalletSettings( + storageFacade: facade, + operationQueue: OperationQueue() + ) + let setupExpectation = expectation( + description: "migrated v2 wallets are resolved" + ) + selectedWalletSettings.setup( + runningCompletionIn: .main + ) { result in + do { + XCTAssertNotNil(try result.get()) + } catch { + XCTFail("Migrated v2 wallet setup failed: \(error)") + } + setupExpectation.fulfill() + } + wait( + for: [setupExpectation], + timeout: Constants.defaultExpectationDuration + ) + + XCTAssertEqual(selectedWalletSettings.storeState, .ready) + let helper = StartViewHelper( + keystore: keystore, + selectedWalletSettings: selectedWalletSettings, + userDefaultsStorage: InMemorySettingsManager() + ) + guard case .pin = helper.startView(onboardingConfig: nil) else { + return XCTFail( + "A nonempty migrated v2 store with a PIN must route to authentication" + ) + } + XCTAssertEqual( + try keystore.fetchKey(for: fearless.KeystoreTag.pincode.rawValue), + pincode + ) + } + + func testPerformMigration_whenIntermediateV3StoreDropsWallet_thenPerHopCountFailsClosedAndRetryPreservesFixture() throws { + let sourceModel = try model(for: .version2) + try createStore(at: storeURL, model: sourceModel) { context in + try self.insertVersion2WalletFixture( + in: context, + model: sourceModel + ) + } + let sourceChecksum = try metadataChecksum(at: storeURL) + let sourceFiles = try durableStoreSnapshot(at: storeURL) + let pincode = Data("135790".utf8) + let keystore = InMemoryKeychain() + try keystore.saveKey( + pincode, + with: fearless.KeystoreTag.pincode.rawValue + ) + var stagedMutationCount = 0 + let adversarialMigrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: keystore, + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle, + stagedStoreWillValidate: { stagedURL, version in + guard version == .version3 else { + return + } + + stagedMutationCount += 1 + try self.executeSQLite( + "DELETE FROM ZCDMETAACCOUNT " + + "WHERE Z_PK = " + + "(SELECT MIN(Z_PK) FROM ZCDMETAACCOUNT)", + at: stagedURL + ) + } + ) + + XCTAssertThrowsError( + try adversarialMigrator.performMigration() + ) { error in + guard + let migrationError = + error as? UserStorageMigrationError, + case .stagedStoreValidationFailed = migrationError + else { + return XCTFail("Unexpected error: \(error)") + } + } + XCTAssertEqual(stagedMutationCount, 1) + XCTAssertEqual(try metadataChecksum(at: storeURL), sourceChecksum) + XCTAssertEqual( + try durableStoreSnapshot(at: storeURL), + sourceFiles + ) + XCTAssertEqual( + try keystore.fetchKey(for: fearless.KeystoreTag.pincode.rawValue), + pincode + ) + + let retryMigrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: keystore, + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle + ) + XCTAssertTrue(try retryMigrator.performMigration()) + XCTAssertFalse(try retryMigrator.performMigration()) + XCTAssertEqual( + try walletSnapshots( + from: storeURL, + model: model(for: .version14) + ), + version2WalletFixtureSnapshots() + ) + XCTAssertEqual( + try keystore.fetchKey(for: fearless.KeystoreTag.pincode.rawValue), + pincode + ) + } + + func testPerformMigration_whenIntermediateV3StoreDropsChainAccountButWalletCountIsUnchanged_thenPerHopIntegrityFailsClosed() throws { + let sourceModel = try model(for: .version2) + try createStore(at: storeURL, model: sourceModel) { context in + try self.insertVersion2WalletFixture( + in: context, + model: sourceModel + ) + } + let sourceChecksum = try metadataChecksum(at: storeURL) + let sourceFiles = try durableStoreSnapshot(at: storeURL) + var stagedMutationCount = 0 + let adversarialMigrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: InMemoryKeychain(), + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle, + stagedStoreWillValidate: { stagedURL, version in + guard version == .version3 else { + return + } + + stagedMutationCount += 1 + try self.executeSQLite( + "DELETE FROM ZCDCHAINACCOUNT " + + "WHERE Z_PK = " + + "(SELECT MIN(Z_PK) FROM ZCDCHAINACCOUNT)", + at: stagedURL + ) + } + ) + + XCTAssertThrowsError( + try adversarialMigrator.performMigration() + ) { error in + guard + let migrationError = + error as? UserStorageMigrationError, + case .stagedStoreValidationFailed = migrationError + else { + return XCTFail("Unexpected error: \(error)") + } + } + XCTAssertEqual(stagedMutationCount, 1) + XCTAssertEqual(try metadataChecksum(at: storeURL), sourceChecksum) + XCTAssertEqual( + try durableStoreSnapshot(at: storeURL), + sourceFiles + ) + + let retryMigrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: InMemoryKeychain(), + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle + ) + XCTAssertTrue(try retryMigrator.performMigration()) + XCTAssertEqual( + try walletSnapshots( + from: storeURL, + model: model(for: .version14) + ), + version2WalletFixtureSnapshots() + ) + } + + func testPerformMigration_whenIntermediateV3StoreMutatesWalletFieldWithoutChangingCount_thenPerHopIntegrityFailsClosed() throws { + let sourceModel = try model(for: .version2) + try createStore(at: storeURL, model: sourceModel) { context in + try self.insertVersion2WalletFixture( + in: context, + model: sourceModel + ) + } + let sourceChecksum = try metadataChecksum(at: storeURL) + let sourceFiles = try durableStoreSnapshot(at: storeURL) + var stagedMutationCount = 0 + let adversarialMigrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: InMemoryKeychain(), + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle, + stagedStoreWillValidate: { stagedURL, version in + guard version == .version3 else { + return + } + + stagedMutationCount += 1 + try self.executeSQLite( + "UPDATE ZCDMETAACCOUNT " + + "SET ZNAME = 'adversarial-wallet-name' " + + "WHERE Z_PK = " + + "(SELECT MIN(Z_PK) FROM ZCDMETAACCOUNT)", + at: stagedURL + ) + } + ) + + XCTAssertThrowsError( + try adversarialMigrator.performMigration() + ) { error in + guard + let migrationError = + error as? UserStorageMigrationError, + case .stagedStoreValidationFailed = migrationError + else { + return XCTFail("Unexpected error: \(error)") + } + } + XCTAssertEqual(stagedMutationCount, 1) + XCTAssertEqual(try metadataChecksum(at: storeURL), sourceChecksum) + XCTAssertEqual( + try durableStoreSnapshot(at: storeURL), + sourceFiles + ) + + let retryMigrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: InMemoryKeychain(), + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle + ) + XCTAssertTrue(try retryMigrator.performMigration()) + XCTAssertEqual( + try walletSnapshots( + from: storeURL, + model: model(for: .version14) + ), + version2WalletFixtureSnapshots() + ) + } + + func testMigration_whenLegacyFieldsAreIntentionallyDerived_thenPreservesExactAssetVisibilityAndNetworkFilterSemantics() throws { + let visibilityURL = testDirectory.appendingPathComponent( + "version9-asset-visibility.sqlite" + ) + let visibilitySourceModel = try model(for: .version9) + try createStore( + at: visibilityURL, + model: visibilitySourceModel + ) { context in + let wallet = try self.insertHistoricalWallet( + metaId: "visibility-wallet", + name: "Visibility Wallet", + in: context, + model: visibilitySourceModel + ) + wallet.setValue( + ["asset-b", "asset-a"] as NSArray, + forKey: "assetIdsEnabled" + ) + } + + let visibilityMigrator = UserStorageMigrator( + targetVersion: .version10, + storeURL: visibilityURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: InMemoryKeychain(), + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle + ) + XCTAssertTrue(try visibilityMigrator.performMigration()) + var migratedVisibility = [String: Bool]() + try useStore( + at: visibilityURL, + model: model(for: .version10) + ) { context in + let request = NSFetchRequest( + entityName: "CDAssetVisibility" + ) + for visibility in try context.fetch(request) { + let assetId = try XCTUnwrap( + visibility.value(forKey: "assetId") as? String + ) + let hidden = try XCTUnwrap( + visibility.value(forKey: "hidden") as? NSNumber + ) + migratedVisibility[assetId] = hidden.boolValue + } + } + XCTAssertEqual( + migratedVisibility, + ["asset-a": true, "asset-b": true] + ) + + let filterURL = testDirectory.appendingPathComponent( + "version11-network-filter.sqlite" + ) + let filterSourceModel = try model(for: .version11) + try createStore( + at: filterURL, + model: filterSourceModel + ) { context in + let wallet = try self.insertHistoricalWallet( + metaId: "filter-wallet", + name: "Filter Wallet", + in: context, + model: filterSourceModel + ) + wallet.setValue( + "polkadot-filter", + forKey: "chainIdForFilter" + ) + } + + let filterMigrator = UserStorageMigrator( + targetVersion: .version12, + storeURL: filterURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: InMemoryKeychain(), + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle + ) + XCTAssertTrue(try filterMigrator.performMigration()) + var migratedFilter: String? + var migratedFavourites: [String]? + try useStore( + at: filterURL, + model: model(for: .version12) + ) { context in + let request = NSFetchRequest( + entityName: "CDMetaAccount" + ) + let wallet = try XCTUnwrap( + context.fetch(request).first + ) + migratedFilter = wallet.value( + forKey: "networkManagmentFilter" + ) as? String + migratedFavourites = + try SafeTransformableValueReader.read( + from: wallet, + key: "favouriteChainIds" + ) + } + XCTAssertEqual(migratedFilter, "polkadot-filter") + XCTAssertEqual(migratedFavourites, []) + } + + func testMigration_whenIntentionalDerivedFieldsAreDroppedOrMutatedInStaging_thenSemanticIntegrityFailsClosed() throws { + let visibilityURL = testDirectory.appendingPathComponent( + "adversarial-version9-asset-visibility.sqlite" + ) + let visibilitySourceModel = try model(for: .version9) + try createStore( + at: visibilityURL, + model: visibilitySourceModel + ) { context in + let wallet = try self.insertHistoricalWallet( + metaId: "visibility-wallet", + name: "Visibility Wallet", + in: context, + model: visibilitySourceModel + ) + wallet.setValue( + ["asset-a", "asset-b"] as NSArray, + forKey: "assetIdsEnabled" + ) + } + let visibilitySourceFiles = + try durableStoreSnapshot(at: visibilityURL) + let visibilityMigrator = UserStorageMigrator( + targetVersion: .version10, + storeURL: visibilityURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: InMemoryKeychain(), + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle, + stagedStoreWillValidate: { stagedURL, version in + guard version == .version10 else { + return + } + + try self.executeSQLite( + "DELETE FROM ZCDASSETVISIBILITY " + + "WHERE Z_PK = " + + "(SELECT MIN(Z_PK) " + + "FROM ZCDASSETVISIBILITY)", + at: stagedURL + ) + } + ) + XCTAssertThrowsError( + try visibilityMigrator.performMigration() + ) { error in + guard + let migrationError = + error as? UserStorageMigrationError, + case .stagedStoreValidationFailed = migrationError + else { + return XCTFail("Unexpected error: \(error)") + } + } + XCTAssertEqual( + try durableStoreSnapshot(at: visibilityURL), + visibilitySourceFiles + ) + + let filterURL = testDirectory.appendingPathComponent( + "adversarial-version11-network-filter.sqlite" + ) + let filterSourceModel = try model(for: .version11) + try createStore( + at: filterURL, + model: filterSourceModel + ) { context in + let wallet = try self.insertHistoricalWallet( + metaId: "filter-wallet", + name: "Filter Wallet", + in: context, + model: filterSourceModel + ) + wallet.setValue( + "polkadot-filter", + forKey: "chainIdForFilter" + ) + } + let filterSourceFiles = + try durableStoreSnapshot(at: filterURL) + let filterMigrator = UserStorageMigrator( + targetVersion: .version12, + storeURL: filterURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: InMemoryKeychain(), + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle, + stagedStoreWillValidate: { stagedURL, version in + guard version == .version12 else { + return + } + + try self.executeSQLite( + "UPDATE ZCDMETAACCOUNT SET " + + "ZNETWORKMANAGMENTFILTER = " + + "'adversarial-filter'", + at: stagedURL + ) + } + ) + XCTAssertThrowsError( + try filterMigrator.performMigration() + ) { error in + guard + let migrationError = + error as? UserStorageMigrationError, + case .stagedStoreValidationFailed = migrationError + else { + return XCTFail("Unexpected error: \(error)") + } + } + XCTAssertEqual( + try durableStoreSnapshot(at: filterURL), + filterSourceFiles + ) + } + + func testMigration_whenV1ToV2DropsDestinationAccount_thenLegacyAccountSemanticIntegrityFailsClosed() throws { + let sourceModel = try model(for: .version1) + let addressFactory = SS58AddressFactory() + let addresses = try [UInt8(0x81), UInt8(0x82)].map { + try addressFactory.address( + fromAccountId: Data(repeating: $0, count: 32), + type: 42 + ) + } + try createStore(at: storeURL, model: sourceModel) { context in + let entity = try XCTUnwrap( + sourceModel.entitiesByName["CDAccountItem"] + ) + for (index, address) in addresses.enumerated() { + let account = NSManagedObject( + entity: entity, + insertInto: context + ) + account.setValue(address, forKey: "identifier") + account.setValue( + "Legacy \(index)", + forKey: "username" + ) + account.setValue( + Data(repeating: UInt8(0x91 + index), count: 32), + forKey: "publicKey" + ) + account.setValue(Int16(0), forKey: "cryptoType") + } + } + let sourceFiles = try durableStoreSnapshot(at: storeURL) + var stagedMutationCount = 0 + let migrator = UserStorageMigrator( + targetVersion: .version2, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: InMemoryKeychain(), + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle, + stagedStoreWillValidate: { stagedURL, version in + guard version == .version2 else { + return + } + + stagedMutationCount += 1 + try self.executeSQLite( + "DELETE FROM ZCDMETAACCOUNT " + + "WHERE Z_PK = " + + "(SELECT MIN(Z_PK) FROM ZCDMETAACCOUNT)", + at: stagedURL + ) + } + ) + + XCTAssertThrowsError(try migrator.performMigration()) { + error in + + guard + let migrationError = + error as? UserStorageMigrationError, + case .stagedStoreValidationFailed = migrationError + else { + return XCTFail("Unexpected error: \(error)") + } + } + XCTAssertEqual(stagedMutationCount, 1) + XCTAssertEqual( + try durableStoreSnapshot(at: storeURL), + sourceFiles + ) + } + + func testMigration_whenLegacyEcosystemStore_thenPreservesWalletTonAssetAndChainData() throws { + let legacyModel = try model(for: .version13) + try createStore(at: storeURL, model: legacyModel) { context in + try self.insertLegacyWallets(in: context, model: legacyModel) + } + + let migrator = makeMigrator() + XCTAssertTrue(migrator.requiresMigration()) + + try migrator.performMigration() + + XCTAssertFalse(migrator.requiresMigration()) + XCTAssertEqual(try metadataChecksum(at: storeURL), currentChecksum) + + let snapshots = try walletSnapshots( + from: storeURL, + model: model(for: .version14) + ) + XCTAssertEqual(snapshots.count, 2) + + let regularWallet = try XCTUnwrap( + snapshots.first { $0.metaId == "regular-wallet" } + ) + XCTAssertEqual(regularWallet.name, "Regular Wallet") + XCTAssertEqual(regularWallet.substrateAccountId, hex(byte: 0x11, count: 32)) + XCTAssertEqual(regularWallet.substratePublicKey, Data(repeating: 0x12, count: 32)) + XCTAssertEqual(regularWallet.tonAddress, Data(repeating: 0x13, count: 36)) + XCTAssertEqual(regularWallet.tonPublicKey, Data(repeating: 0x14, count: 32)) + XCTAssertEqual(regularWallet.tonContractVersion, "v4R2") + XCTAssertEqual( + regularWallet.assetsVisibility, + ["polkadot-0": false, "kusama-0": true] + ) + XCTAssertEqual( + regularWallet.chainAccounts, + [ + ChainSnapshot( + chainId: "ethereum-chain", + accountId: hex(byte: 0x31, count: 20), + publicKey: Data(repeating: 0x32, count: 33), + cryptoType: 2, + ecosystem: UniversalWalletEcosystem.evm.rawValue + ), + ChainSnapshot( + chainId: "substrate-chain", + accountId: hex(byte: 0x21, count: 32), + publicKey: Data(repeating: 0x22, count: 32), + cryptoType: 1, + ecosystem: "substrate" + ) + ] + ) + + let tonOnlyWallet = try XCTUnwrap( + snapshots.first { $0.metaId == "ton-only-wallet" } + ) + XCTAssertEqual(tonOnlyWallet.name, "TON Only Wallet") + XCTAssertNil(tonOnlyWallet.substrateAccountId) + XCTAssertNil(tonOnlyWallet.substratePublicKey) + XCTAssertEqual(tonOnlyWallet.tonAddress, Data(repeating: 0x41, count: 36)) + XCTAssertEqual(tonOnlyWallet.tonPublicKey, Data(repeating: 0x42, count: 32)) + XCTAssertEqual(tonOnlyWallet.tonContractVersion, "v5R1") + XCTAssertTrue(tonOnlyWallet.assetsVisibility.isEmpty) + XCTAssertTrue(tonOnlyWallet.chainAccounts.isEmpty) + } + + func testMigration_whenPublicV11Store_thenPreservesWalletAssetAndEthereumChainData() throws { + let publicModel = try model(for: .version12) + try createStore(at: storeURL, model: publicModel) { context in + self.insertCurrentWallet(in: context, model: publicModel) + let walletRequest = NSFetchRequest(entityName: "CDMetaAccount") + let wallet = try XCTUnwrap(context.fetch(walletRequest).first) + + try self.insertAssetVisibility( + assetId: "ethereum-mainnet-eth", + hidden: true, + wallet: wallet, + in: context, + model: publicModel + ) + try self.insertPublicChainAccount( + wallet: wallet, + in: context, + model: publicModel + ) + } + + let migrator = makeMigrator() + XCTAssertTrue(migrator.requiresMigration()) + + try migrator.performMigration() + + XCTAssertFalse(migrator.requiresMigration()) + XCTAssertEqual(try metadataChecksum(at: storeURL), currentChecksum) + + let currentModel = try model(for: .version14) + let snapshots = try walletSnapshots(from: storeURL, model: currentModel) + XCTAssertEqual(snapshots.count, 1) + XCTAssertEqual(snapshots.first?.metaId, "current-wallet") + XCTAssertEqual(snapshots.first?.name, "Current Wallet") + XCTAssertEqual( + snapshots.first?.assetsVisibility, + ["ethereum-mainnet-eth": true] + ) + + try useStore(at: storeURL, model: currentModel) { context in + let request = NSFetchRequest(entityName: "CDChainAccount") + let chainAccount = try XCTUnwrap(context.fetch(request).first) + + XCTAssertEqual(chainAccount.value(forKey: "chainId") as? String, "ethereum-mainnet") + XCTAssertEqual( + (chainAccount.value(forKey: "ethereumBased") as? NSNumber)?.boolValue, + true + ) + XCTAssertNil(chainAccount.value(forKey: "ecosystem")) + } + } + + func testMigration_whenV11OrLegacyV12PreferenceArchiveIsInvalid_thenDefaultsOnlyDamagedField() throws { + let preferences = [ + (key: "assetFilterOptions", column: "ZASSETFILTEROPTIONS"), + (key: "assetKeysOrder", column: "ZASSETKEYSORDER"), + (key: "favouriteChainIds", column: "ZFAVOURITECHAINIDS"), + (key: "unusedChainIds", column: "ZUNUSEDCHAINIDS") + ] + let expectedValues: [String: [String]] = [ + "assetFilterOptions": ["filter-option"], + "assetKeysOrder": ["asset-order"], + "favouriteChainIds": ["favourite-chain"], + "unusedChainIds": ["unused-chain"] + ] + + for sourceVersion in [ + UserStorageVersion.version12, + UserStorageVersion.version13 + ] { + for preference in preferences { + let caseURL = testDirectory.appendingPathComponent( + "\(sourceVersion.rawValue)-\(preference.key).sqlite" + ) + let sourceModel = try model(for: sourceVersion) + try createStore(at: caseURL, model: sourceModel) { context in + self.insertCurrentWallet( + in: context, + model: sourceModel + ) + let request = NSFetchRequest( + entityName: "CDMetaAccount" + ) + let wallet = try XCTUnwrap(context.fetch(request).first) + for (key, value) in expectedValues { + wallet.setValue(value as NSArray, forKey: key) + } + } + + try executeSQLite( + "UPDATE ZCDMETAACCOUNT " + + "SET \(preference.column) = X'00FF00'", + at: caseURL + ) + + try makeMigrator(storeURL: caseURL).performMigration() + + XCTAssertEqual( + try metadataChecksum(at: caseURL), + currentChecksum, + "\(sourceVersion.rawValue).\(preference.key)" + ) + + try useStore( + at: caseURL, + model: model(for: .version14) + ) { context in + let request = NSFetchRequest( + entityName: "CDMetaAccount" + ) + let wallet = try XCTUnwrap(context.fetch(request).first) + XCTAssertEqual( + wallet.value(forKey: "metaId") as? String, + "current-wallet" + ) + XCTAssertEqual( + wallet.value(forKey: "name") as? String, + "Current Wallet" + ) + XCTAssertEqual( + wallet.value(forKey: "substrateAccountId") as? String, + self.hex(byte: 0x51, count: 32) + ) + XCTAssertEqual( + wallet.value(forKey: "substratePublicKey") as? Data, + Data(repeating: 0x52, count: 32) + ) + + for (key, expectedValue) in expectedValues { + let actualValue = + wallet.value(forKey: key) as? [String] + XCTAssertEqual( + actualValue, + key == preference.key ? [] : expectedValue, + "\(sourceVersion.rawValue).\(preference.key).\(key)" + ) + } + } + } + } + } + + func testMigration_whenPreferenceArchiveHasWrongDecodedType_thenDefaultsOnlyDamagedField() throws { + let expectedValues = preferenceFixtureValues() + let preferences = [ + (key: "assetFilterOptions", column: "ZASSETFILTEROPTIONS"), + (key: "assetKeysOrder", column: "ZASSETKEYSORDER"), + (key: "favouriteChainIds", column: "ZFAVOURITECHAINIDS"), + (key: "unusedChainIds", column: "ZUNUSEDCHAINIDS") + ] + let wrongTypeArchive = try NSKeyedArchiver.archivedData( + withRootObject: NSString(string: "not-a-string-array"), + requiringSecureCoding: true + ) + let archiveHex = wrongTypeArchive.map { + String(format: "%02X", $0) + }.joined() + + for sourceVersion in [ + UserStorageVersion.version12, + UserStorageVersion.version13 + ] { + for preference in preferences { + let caseURL = testDirectory.appendingPathComponent( + "\(sourceVersion.rawValue)-wrong-type-" + + "\(preference.key).sqlite" + ) + try createPreferenceFixture( + at: caseURL, + sourceVersion: sourceVersion, + values: expectedValues + ) + try executeSQLite( + "UPDATE ZCDMETAACCOUNT " + + "SET \(preference.column) = X'\(archiveHex)'", + at: caseURL + ) + + try makeMigrator(storeURL: caseURL).performMigration() + + var expectedMigratedValues = expectedValues + expectedMigratedValues[preference.key] = [] + XCTAssertEqual( + try migratedPreferenceValues(at: caseURL), + expectedMigratedValues, + "\(sourceVersion.rawValue).\(preference.key)" + ) + } + } + } + + func testMigration_whenPreferenceArchivesAreValid_thenPreservesEveryValue() throws { + let expectedValues = preferenceFixtureValues() + + for sourceVersion in [ + UserStorageVersion.version12, + UserStorageVersion.version13 + ] { + let caseURL = testDirectory.appendingPathComponent( + "\(sourceVersion.rawValue)-valid-preferences.sqlite" + ) + try createPreferenceFixture( + at: caseURL, + sourceVersion: sourceVersion, + values: expectedValues + ) + + try makeMigrator(storeURL: caseURL).performMigration() + + XCTAssertEqual( + try migratedPreferenceValues(at: caseURL), + expectedValues + ) + } + } + + func testRawArchivePreflight_whenValuesCrossByteBoundary_thenRepairsOnlyOversizedArchivesAndIsIdempotent() throws { + let sourceModel = try model(for: .version13) + try createPreferenceFixture( + at: storeURL, + sourceVersion: .version13, + values: preferenceFixtureValues() + ) + let boundaryArchive = try NSKeyedArchiver.archivedData( + withRootObject: ["boundary"] as NSArray, + requiringSecureCoding: true + ) + let oversizedArchive = try NSKeyedArchiver.archivedData( + withRootObject: [ + String(repeating: "x", count: 4096) + ] as NSArray, + requiringSecureCoding: true + ) + XCTAssertGreaterThan( + oversizedArchive.count, + boundaryArchive.count + ) + try updateSQLiteBlob( + boundaryArchive, + column: "ZASSETFILTEROPTIONS", + at: storeURL + ) + try updateSQLiteBlob( + oversizedArchive, + column: "ZASSETKEYSORDER", + at: storeURL + ) + try updateSQLiteBlob( + oversizedArchive, + column: "ZFAVOURITECHAINIDS", + at: storeURL + ) + + let entity = try XCTUnwrap( + sourceModel.entitiesByName["CDMetaAccount"] + ) + let columns = try [ + "assetFilterOptions", + "assetKeysOrder", + "favouriteChainIds" + ].map { key in + let attribute = try XCTUnwrap( + entity.attributesByName[key] + ) + return SQLiteTransformableArchiveColumn( + entityName: "CDMetaAccount", + attributeName: key, + isOptional: attribute.isOptional, + valueTransformerName: + attribute.valueTransformerName + ) + } + + XCTAssertEqual( + try SQLiteTransformableArchivePreflight + .repairOversizedArchives( + at: storeURL, + columns: columns, + maximumArchiveByteCount: + boundaryArchive.count + ), + 2 + ) + XCTAssertEqual( + try SQLiteTransformableArchivePreflight + .repairOversizedArchives( + at: storeURL, + columns: columns, + maximumArchiveByteCount: + boundaryArchive.count + ), + 0 + ) + + try useStore( + at: storeURL, + model: sourceModel + ) { context in + let request = NSFetchRequest( + entityName: "CDMetaAccount" + ) + let wallet = try XCTUnwrap( + context.fetch(request).first + ) + XCTAssertEqual( + wallet.value( + forKey: "assetFilterOptions" + ) as? [String], + ["boundary"] + ) + XCTAssertNil( + wallet.value( + forKey: "assetKeysOrder" + ) + ) + XCTAssertEqual( + wallet.value( + forKey: "favouriteChainIds" + ) as? [String], + [] + ) + } + } + + func testRawArchivePreflight_whenRequiredReplacementExceedsLimit_thenRollsBackWithoutMutation() throws { + let sourceModel = try model(for: .version13) + let oversizedValue = + String(repeating: "x", count: 4096) + try createPreferenceFixture( + at: storeURL, + sourceVersion: .version13, + values: preferenceFixtureValues() + ) + let oversizedArchive = try NSKeyedArchiver.archivedData( + withRootObject: [oversizedValue] as NSArray, + requiringSecureCoding: true + ) + try updateSQLiteBlob( + oversizedArchive, + column: "ZFAVOURITECHAINIDS", + at: storeURL + ) + + let entity = try XCTUnwrap( + sourceModel.entitiesByName["CDMetaAccount"] + ) + let attribute = try XCTUnwrap( + entity.attributesByName["favouriteChainIds"] + ) + XCTAssertFalse(attribute.isOptional) + let column = SQLiteTransformableArchiveColumn( + entityName: "CDMetaAccount", + attributeName: "favouriteChainIds", + isOptional: attribute.isOptional, + valueTransformerName: + attribute.valueTransformerName + ) + + XCTAssertThrowsError( + try SQLiteTransformableArchivePreflight + .repairOversizedArchives( + at: storeURL, + columns: [column], + maximumArchiveByteCount: 1 + ) + ) { error in + XCTAssertEqual( + error as? + SQLiteTransformableArchiveRepairError, + .invalidLimit + ) + } + + try useStore( + at: storeURL, + model: sourceModel + ) { context in + let request = NSFetchRequest( + entityName: "CDMetaAccount" + ) + let wallet = try XCTUnwrap( + context.fetch(request).first + ) + XCTAssertEqual( + wallet.value( + forKey: "favouriteChainIds" + ) as? [String], + [oversizedValue] + ) + } + } + + func testMigration_whenRawPreferenceArchiveExceedsProductionLimit_thenRepairsBeforeCoreDataDecode() throws { + var expectedValues = preferenceFixtureValues() + try createPreferenceFixture( + at: storeURL, + sourceVersion: .version13, + values: expectedValues + ) + let oversizedArchive = try NSKeyedArchiver.archivedData( + withRootObject: [ + String( + repeating: "x", + count: 1 * 1024 * 1024 + ) + ] as NSArray, + requiringSecureCoding: true + ) + XCTAssertGreaterThan( + oversizedArchive.count, + 1 * 1024 * 1024 + ) + try updateSQLiteBlob( + oversizedArchive, + column: "ZASSETKEYSORDER", + at: storeURL + ) + + try makeMigrator().performMigration() + + expectedValues.removeValue( + forKey: "assetKeysOrder" + ) + XCTAssertEqual( + try migratedPreferenceValues(at: storeURL), + expectedValues + ) + } + + func testMigration_whenDecodedPreferenceLimitsAreCrossed_thenRepairsOverLimitAndPreservesExactBoundaries() throws { + let boundaryValue = String(repeating: "b", count: 32) + let overByteValue = String(repeating: "o", count: 40) + let expectedValues: [String: [String]] = [ + "assetFilterOptions": [ + boundaryValue, + boundaryValue + ], + "assetKeysOrder": ["one", "two", "three"], + "favouriteChainIds": ["one", "two"], + "unusedChainIds": [ + overByteValue, + overByteValue + ] + ] + try createPreferenceFixture( + at: storeURL, + sourceVersion: .version13, + values: expectedValues + ) + let migrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: InMemoryKeychain(), + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle, + integrityValidationLimits: + UserStorageIntegrityValidationLimits( + maximumRowsPerEntity: 100, + maximumRowsAcrossStore: 1_000, + fetchBatchSize: 1, + maximumCanonicalRowBytes: 1 * 1024 * 1024, + maximumAttributeBytes: 64, + maximumRelationshipDestinations: 10_000, + maximumTransformableElements: 2 + ) + ) + + try migrator.performMigration() + + XCTAssertEqual( + try migratedPreferenceValues(at: storeURL), + [ + "assetFilterOptions": [ + boundaryValue, + boundaryValue + ], + "assetKeysOrder": [], + "favouriteChainIds": ["one", "two"], + "unusedChainIds": [] + ] + ) + } + + func testMigration_whenCurrentVersionRequiredArchiveExceedsRawLimit_thenAtomicallyRepairsAndSubsequentRunIsNoOp() throws { + var expectedValues = preferenceFixtureValues() + try createPreferenceFixture( + at: storeURL, + sourceVersion: .version14, + values: expectedValues + ) + let oversizedArchive = try NSKeyedArchiver.archivedData( + withRootObject: [ + String( + repeating: "x", + count: 1 * 1024 * 1024 + ) + ] as NSArray, + requiringSecureCoding: true + ) + try updateSQLiteBlob( + oversizedArchive, + column: "ZFAVOURITECHAINIDS", + at: storeURL + ) + let damagedStoreSnapshot = + try durableStoreSnapshot(at: storeURL) + var replacementCount = 0 + var stagedValidationCount = 0 + var keystoreMigratorCreated = false + var settingsMigratorCreated = false + let migrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: InMemoryKeychain(), + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle, + storeReplacer: { targetURL, sourceURL in + replacementCount += 1 + try NSPersistentStoreCoordinator.replaceStore( + at: targetURL, + withStoreAt: sourceURL + ) + }, + keystoreMigratorFactory: { + source, + destination, + keystore in + + keystoreMigratorCreated = true + return KeystoreMigrator( + sourceVersion: source, + destinationVersion: destination, + keystore: keystore + ) + }, + settingsMigratorFactory: { + source, + destination, + settings in + + settingsMigratorCreated = true + return SettingsMigrator( + sourceVersion: source, + destinationVersion: destination, + settings: settings + ) + }, + stagedStoreWillValidate: { _, version in + XCTAssertEqual(version, .version14) + stagedValidationCount += 1 + } + ) + + XCTAssertTrue(migrator.requiresMigration()) + XCTAssertEqual( + try durableStoreSnapshot(at: storeURL), + damagedStoreSnapshot + ) + XCTAssertTrue(try migrator.performMigration()) + expectedValues["favouriteChainIds"] = [] + XCTAssertEqual( + try migratedPreferenceValues(at: storeURL), + expectedValues + ) + XCTAssertFalse(try migrator.performMigration()) + XCTAssertFalse(migrator.requiresMigration()) + XCTAssertEqual(replacementCount, 1) + XCTAssertEqual(stagedValidationCount, 1) + XCTAssertFalse(keystoreMigratorCreated) + XCTAssertFalse(settingsMigratorCreated) + } + + func testMigration_whenCurrentVersionOptionalArchiveHasWrongSQLiteType_thenAtomicallyRepairsToNullAndSubsequentRunIsNoOp() throws { + var expectedValues = preferenceFixtureValues() + try createPreferenceFixture( + at: storeURL, + sourceVersion: .version14, + values: expectedValues + ) + try executeSQLite( + """ + UPDATE ZCDMETAACCOUNT + SET ZASSETKEYSORDER = 'not-a-blob' + """, + at: storeURL + ) + let damagedStoreSnapshot = + try durableStoreSnapshot(at: storeURL) + var replacementCount = 0 + var keystoreMigratorCreated = false + var settingsMigratorCreated = false + let migrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: InMemoryKeychain(), + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle, + storeReplacer: { targetURL, sourceURL in + replacementCount += 1 + try NSPersistentStoreCoordinator.replaceStore( + at: targetURL, + withStoreAt: sourceURL + ) + }, + keystoreMigratorFactory: { + source, + destination, + keystore in + + keystoreMigratorCreated = true + return KeystoreMigrator( + sourceVersion: source, + destinationVersion: destination, + keystore: keystore + ) + }, + settingsMigratorFactory: { + source, + destination, + settings in + + settingsMigratorCreated = true + return SettingsMigrator( + sourceVersion: source, + destinationVersion: destination, + settings: settings + ) + } + ) + + XCTAssertTrue(migrator.requiresMigration()) + XCTAssertEqual( + try durableStoreSnapshot(at: storeURL), + damagedStoreSnapshot + ) + XCTAssertTrue(try migrator.performMigration()) + expectedValues.removeValue( + forKey: "assetKeysOrder" + ) + XCTAssertEqual( + try migratedPreferenceValues(at: storeURL), + expectedValues + ) + XCTAssertFalse(try migrator.performMigration()) + XCTAssertFalse(migrator.requiresMigration()) + XCTAssertEqual(replacementCount, 1) + XCTAssertFalse(keystoreMigratorCreated) + XCTAssertFalse(settingsMigratorCreated) + } + + func testMigration_whenCurrentVersionDecodedPreferenceExceedsLimit_thenCountsRepairAndSubsequentRunIsNoOp() throws { + var expectedValues = preferenceFixtureValues() + expectedValues["assetKeysOrder"] = ["one", "two"] + try createPreferenceFixture( + at: storeURL, + sourceVersion: .version14, + values: expectedValues + ) + var replacementCount = 0 + let migrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: InMemoryKeychain(), + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle, + integrityValidationLimits: + UserStorageIntegrityValidationLimits( + maximumRowsPerEntity: 100, + maximumRowsAcrossStore: 1_000, + fetchBatchSize: 1, + maximumCanonicalRowBytes: 1 * 1024 * 1024, + maximumAttributeBytes: 256 * 1024, + maximumRelationshipDestinations: 10_000, + maximumTransformableElements: 1 + ), + storeReplacer: { targetURL, sourceURL in + replacementCount += 1 + try NSPersistentStoreCoordinator.replaceStore( + at: targetURL, + withStoreAt: sourceURL + ) + } + ) + + XCTAssertTrue(try migrator.performMigration()) + expectedValues["assetKeysOrder"] = [] + XCTAssertEqual( + try migratedPreferenceValues(at: storeURL), + expectedValues + ) + XCTAssertFalse(try migrator.performMigration()) + XCTAssertEqual(replacementCount, 1) + } + + func testMigration_whenCurrentVersionRepairFitsCopyLimitButFinalBackupCapacityIsInsufficient_thenPreservesLiveStore() throws { + try createPreferenceFixture( + at: storeURL, + sourceVersion: .version14, + values: preferenceFixtureValues() + ) + try executeSQLite( + """ + UPDATE ZCDMETAACCOUNT + SET ZASSETKEYSORDER = 'not-a-blob' + """, + at: storeURL + ) + let before = try durableStoreSnapshot(at: storeURL) + var capacityInspectionCount = 0 + var replacementAttempted = false + let migrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: InMemoryKeychain(), + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle, + privateSourceCopyLimits: + SQLiteStoreFamilyCopyLimits( + maximumFamilyByteCount: 512 * 1024 * 1024, + minimumFreeStorageReserveByteCount: 17 + ), + privateSourceAvailableCapacityProvider: { _ in + capacityInspectionCount += 1 + return capacityInspectionCount == 1 + ? UInt64.max + : 0 + }, + storeReplacer: { _, _ in + replacementAttempted = true + } + ) + + XCTAssertThrowsError( + try migrator.performMigration() + ) { error in + guard + let capacityError = + error as? CrashConsistentStoreReplacementError, + case let .insufficientStorageCapacity( + requiredByteCount, + availableByteCount + ) = capacityError + else { + return XCTFail("Unexpected error: \(error)") + } + + XCTAssertGreaterThan(requiredByteCount, 17) + XCTAssertEqual(availableByteCount, 0) + } + XCTAssertEqual(capacityInspectionCount, 2) + XCTAssertFalse(replacementAttempted) + XCTAssertEqual( + try durableStoreSnapshot(at: storeURL), + before + ) + } + + func testMigration_whenCurrentVersionRepairValidationHookDropsWallet_thenRejectsMutatedBaselineBeforeReplacement() throws { + try createPreferenceFixture( + at: storeURL, + sourceVersion: .version14, + values: preferenceFixtureValues() + ) + try executeSQLite( + """ + UPDATE ZCDMETAACCOUNT + SET ZASSETKEYSORDER = 'not-a-blob' + """, + at: storeURL + ) + let before = try durableStoreSnapshot(at: storeURL) + var stagedMutationAttempted = false + var replacementAttempted = false + let migrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: InMemoryKeychain(), + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle, + storeReplacer: { _, _ in + replacementAttempted = true + }, + stagedStoreWillValidate: { stagedURL, version in + XCTAssertEqual(version, .version14) + stagedMutationAttempted = true + try self.executeSQLite( + "DELETE FROM ZCDMETAACCOUNT", + at: stagedURL + ) + } + ) + + XCTAssertThrowsError( + try migrator.performMigration() + ) { error in + guard + let migrationError = + error as? UserStorageMigrationError, + case .stagedStoreValidationFailed = + migrationError + else { + return XCTFail("Unexpected error: \(error)") + } + } + XCTAssertTrue(stagedMutationAttempted) + XCTAssertFalse(replacementAttempted) + XCTAssertEqual( + try durableStoreSnapshot(at: storeURL), + before + ) + } + + func testRelationshipCountPreflight_whenRelationshipIsFaulted_thenCountsWithoutMaterializingIt() throws { + let sourceModel = try model(for: .version13) + try createStore( + at: storeURL, + model: sourceModel + ) { context in + try self.insertLegacyWallets( + in: context, + model: sourceModel + ) + } + let migrator = makeMigrator() + + try useStore( + at: storeURL, + model: sourceModel + ) { context in + let request = NSFetchRequest( + entityName: "CDMetaAccount" + ) + request.predicate = NSPredicate( + format: "metaId == %@", + "regular-wallet" + ) + let wallet = try XCTUnwrap( + context.fetch(request).first + ) + XCTAssertTrue( + wallet.hasFault( + forRelationshipNamed: "chainAccounts" + ) + ) + + XCTAssertEqual( + try migrator + .preflightRelationshipDestinationCount( + for: wallet, + relationshipName: "chainAccounts" + ), + 2 + ) + XCTAssertTrue( + wallet.hasFault( + forRelationshipNamed: "chainAccounts" + ) + ) + } + } + + func testRelationshipAggregateCounter_whenCountReachesExactBoundary_thenAcceptsIt() throws { + let migrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: InMemoryKeychain(), + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle, + integrityValidationLimits: + UserStorageIntegrityValidationLimits( + maximumRowsPerEntity: 100, + maximumRowsAcrossStore: 1_000, + fetchBatchSize: 1, + maximumCanonicalRowBytes: 1 * 1024 * 1024, + maximumAttributeBytes: 256 * 1024, + maximumRelationshipDestinations: 10, + maximumTransformableElements: 4_096, + maximumRelationshipDestinationsAcrossStore: 3 + ) + ) + var total = 1 + + try migrator.accumulateRelationshipDestinationCount( + 2, + total: &total + ) + + XCTAssertEqual(total, 3) + } + + func testRelationshipAggregateCounter_whenLimitOrIntegerRangeIsExceeded_thenFailsWithoutChangingTotal() throws { + let boundedMigrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: InMemoryKeychain(), + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle, + integrityValidationLimits: + UserStorageIntegrityValidationLimits( + maximumRowsPerEntity: 100, + maximumRowsAcrossStore: 1_000, + fetchBatchSize: 1, + maximumCanonicalRowBytes: 1 * 1024 * 1024, + maximumAttributeBytes: 256 * 1024, + maximumRelationshipDestinations: 10, + maximumTransformableElements: 4_096, + maximumRelationshipDestinationsAcrossStore: 3 + ) + ) + var boundedTotal = 3 + + XCTAssertThrowsError( + try boundedMigrator + .accumulateRelationshipDestinationCount( + 1, + total: &boundedTotal + ) + ) + XCTAssertEqual(boundedTotal, 3) + + let overflowMigrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: InMemoryKeychain(), + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle, + integrityValidationLimits: + UserStorageIntegrityValidationLimits( + maximumRowsPerEntity: 100, + maximumRowsAcrossStore: 1_000, + fetchBatchSize: 1, + maximumCanonicalRowBytes: 1 * 1024 * 1024, + maximumAttributeBytes: 256 * 1024, + maximumRelationshipDestinations: 10, + maximumTransformableElements: 4_096, + maximumRelationshipDestinationsAcrossStore: + Int.max + ) + ) + var overflowTotal = Int.max + + XCTAssertThrowsError( + try overflowMigrator + .accumulateRelationshipDestinationCount( + 1, + total: &overflowTotal + ) + ) + XCTAssertEqual(overflowTotal, Int.max) + } + + func testMigration_whenAggregateRelationshipDestinationsExceedStoreLimit_thenFailsBeforeStoreReplacement() throws { + let sourceModel = try model(for: .version13) + try createStore( + at: storeURL, + model: sourceModel + ) { context in + try self.insertLegacyWallets( + in: context, + model: sourceModel + ) + } + let before = try durableStoreSnapshot(at: storeURL) + var replacementAttempted = false + let migrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: InMemoryKeychain(), + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle, + integrityValidationLimits: + UserStorageIntegrityValidationLimits( + maximumRowsPerEntity: 100, + maximumRowsAcrossStore: 1_000, + fetchBatchSize: 1, + maximumCanonicalRowBytes: 1 * 1024 * 1024, + maximumAttributeBytes: 256 * 1024, + maximumRelationshipDestinations: 10_000, + maximumTransformableElements: 4_096, + maximumRelationshipDestinationsAcrossStore: 1 + ), + storeReplacer: { _, _ in + replacementAttempted = true + } + ) + + XCTAssertThrowsError( + try migrator.performMigration() + ) { error in + guard + let migrationError = + error as? UserStorageMigrationError, + case .stagedStoreValidationFailed = + migrationError + else { + return XCTFail("Unexpected error: \(error)") + } + } + XCTAssertFalse(replacementAttempted) + XCTAssertEqual( + try durableStoreSnapshot(at: storeURL), + before + ) + } + + func testMigration_whenRelationshipExceedsDestinationLimit_thenFailsBeforeStoreReplacement() throws { + let sourceModel = try model(for: .version13) + try createStore( + at: storeURL, + model: sourceModel + ) { context in + try self.insertLegacyWallets( + in: context, + model: sourceModel + ) + } + let before = try durableStoreSnapshot(at: storeURL) + var replacementAttempted = false + let migrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: InMemoryKeychain(), + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle, + integrityValidationLimits: + UserStorageIntegrityValidationLimits( + maximumRowsPerEntity: 100, + maximumRowsAcrossStore: 1_000, + fetchBatchSize: 1, + maximumCanonicalRowBytes: 1 * 1024 * 1024, + maximumAttributeBytes: 256 * 1024, + maximumRelationshipDestinations: 1, + maximumTransformableElements: 4_096 + ), + storeReplacer: { _, _ in + replacementAttempted = true + } + ) + + XCTAssertThrowsError( + try migrator.performMigration() + ) { error in + guard + let migrationError = + error as? UserStorageMigrationError, + case .stagedStoreValidationFailed = + migrationError + else { + return XCTFail("Unexpected error: \(error)") + } + } + XCTAssertFalse(replacementAttempted) + XCTAssertEqual( + try durableStoreSnapshot(at: storeURL), + before + ) + } + + func testMigration_whenVersion1AccountRowLimitIsExceeded_thenFailsBeforeKeyOrStoreReplacement() throws { + let sourceModel = try model(for: .version1) + try createStore( + at: storeURL, + model: sourceModel + ) { context in + let entity = try XCTUnwrap( + sourceModel.entitiesByName["CDAccountItem"] + ) + for index in 0 ..< 2 { + let account = NSManagedObject( + entity: entity, + insertInto: context + ) + account.setValue( + "legacy-\(index)", + forKey: "identifier" + ) + } + } + let before = try durableStoreSnapshot(at: storeURL) + var keystoreMigratorCreated = false + var replacementAttempted = false + let migrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: InMemoryKeychain(), + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle, + integrityValidationLimits: + UserStorageIntegrityValidationLimits( + maximumRowsPerEntity: 1, + maximumRowsAcrossStore: 1, + fetchBatchSize: 1, + maximumCanonicalRowBytes: 1 * 1024 * 1024, + maximumAttributeBytes: 256 * 1024, + maximumRelationshipDestinations: 10_000, + maximumTransformableElements: 4_096 + ), + storeReplacer: { _, _ in + replacementAttempted = true + }, + keystoreMigratorFactory: { + source, + destination, + keystore in + + keystoreMigratorCreated = true + return KeystoreMigrator( + sourceVersion: source, + destinationVersion: destination, + keystore: keystore + ) + } + ) + + XCTAssertThrowsError( + try migrator.performMigration() + ) { error in + guard + let migrationError = + error as? UserStorageMigrationError, + case .privateSourceRepairFailed = + migrationError + else { + return XCTFail("Unexpected error: \(error)") + } + } + XCTAssertFalse(keystoreMigratorCreated) + XCTAssertFalse(replacementAttempted) + XCTAssertEqual( + try durableStoreSnapshot(at: storeURL), + before + ) + } + + func testMigration_whenVersion1RowsExceedKeystoreProductionCap_thenFailsBeforeMigrationPolicyOrKeyStaging() throws { + let sourceModel = try model(for: .version1) + let productionLimit = + KeystoreMigrationResourceLimits.production + .maximumLegacyAccountRows + try createStore( + at: storeURL, + model: sourceModel + ) { context in + let entity = try XCTUnwrap( + sourceModel.entitiesByName["CDAccountItem"] + ) + for index in 0 ... productionLimit { + let account = NSManagedObject( + entity: entity, + insertInto: context + ) + account.setValue( + "legacy-\(index)", + forKey: "identifier" + ) + } + } + let before = try durableStoreSnapshot(at: storeURL) + var keystoreMigratorCreated = false + var stagedStoreReachedValidation = false + var replacementAttempted = false + let migrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: InMemoryKeychain(), + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle, + storeReplacer: { _, _ in + replacementAttempted = true + }, + keystoreMigratorFactory: { + source, + destination, + keystore in + + keystoreMigratorCreated = true + return KeystoreMigrator( + sourceVersion: source, + destinationVersion: destination, + keystore: keystore + ) + }, + stagedStoreWillValidate: { _, _ in + stagedStoreReachedValidation = true + } + ) + + XCTAssertThrowsError( + try migrator.performMigration() + ) { error in + guard + let migrationError = + error as? UserStorageMigrationError, + case .privateSourceRepairFailed = + migrationError + else { + return XCTFail("Unexpected error: \(error)") + } + } + XCTAssertFalse(keystoreMigratorCreated) + XCTAssertFalse(stagedStoreReachedValidation) + XCTAssertFalse(replacementAttempted) + XCTAssertEqual( + try durableStoreSnapshot(at: storeURL), + before + ) + } + + func testMigration_whenVersion1SourceFamilyExceedsCopyLimit_thenFailsBeforeCapacityKeyOrStoreReplacement() throws { + let sourceModel = try model(for: .version1) + try createStore( + at: storeURL, + model: sourceModel + ) { context in + let entity = try XCTUnwrap( + sourceModel.entitiesByName["CDAccountItem"] + ) + let account = NSManagedObject( + entity: entity, + insertInto: context + ) + account.setValue( + "legacy", + forKey: "identifier" + ) + } + let before = try durableStoreSnapshot(at: storeURL) + var capacityInspected = false + var keystoreMigratorCreated = false + var replacementAttempted = false + let migrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: InMemoryKeychain(), + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle, + privateSourceCopyLimits: + SQLiteStoreFamilyCopyLimits( + maximumFamilyByteCount: 1, + minimumFreeStorageReserveByteCount: 0 + ), + privateSourceAvailableCapacityProvider: { _ in + capacityInspected = true + return UInt64.max + }, + storeReplacer: { _, _ in + replacementAttempted = true + }, + keystoreMigratorFactory: { + source, + destination, + keystore in + + keystoreMigratorCreated = true + return KeystoreMigrator( + sourceVersion: source, + destinationVersion: destination, + keystore: keystore + ) + } + ) + + XCTAssertThrowsError( + try migrator.performMigration() + ) { error in + guard + let copyError = + error as? CrashConsistentStoreReplacementError, + case let .storeFamilyTooLarge( + actualByteCount, + maximumByteCount + ) = copyError + else { + return XCTFail("Unexpected error: \(error)") + } + + XCTAssertGreaterThan(actualByteCount, 1) + XCTAssertEqual(maximumByteCount, 1) + } + XCTAssertFalse(capacityInspected) + XCTAssertFalse(keystoreMigratorCreated) + XCTAssertFalse(replacementAttempted) + XCTAssertEqual( + try durableStoreSnapshot(at: storeURL), + before + ) + } + + func testMigration_whenPrivateRepairSucceedsButReplacementFails_thenOnlyStagedFieldChangesAndSourceRemainsByteIdentical() throws { + let expectedValues = preferenceFixtureValues() + try createPreferenceFixture( + at: storeURL, + sourceVersion: .version13, + values: expectedValues + ) + try executeSQLite( + "UPDATE ZCDMETAACCOUNT " + + "SET ZASSETKEYSORDER = X'00FF00'", + at: storeURL + ) + let originalDurableFiles = try durableStoreSnapshot(at: storeURL) + var replacementAttempted = false + + let migrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: InMemoryKeychain(), + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle, + storeReplacer: { _, stagedURL in + replacementAttempted = true + var expectedStagedValues = expectedValues + expectedStagedValues["assetKeysOrder"] = [] + XCTAssertEqual( + try self.migratedPreferenceValues(at: stagedURL), + expectedStagedValues + ) + throw UserStorageCommitTestError + .storeReplacementFailed + } + ) + + XCTAssertThrowsError(try migrator.performMigration()) { error in + XCTAssertEqual( + error as? UserStorageCommitTestError, + .storeReplacementFailed + ) + } + XCTAssertTrue(replacementAttempted) + XCTAssertEqual( + try durableStoreSnapshot(at: storeURL), + originalDurableFiles + ) + } + + func testMigration_whenPrivateRepairRowCapIsExceeded_thenRejectsBeforePagingOrReplacement() throws { + let sourceModel = try model(for: .version13) + try createStore(at: storeURL, model: sourceModel) { + context in + try self.insertLegacyWallets( + in: context, + model: sourceModel + ) + } + try executeSQLite( + """ + UPDATE ZCDMETAACCOUNT + SET ZASSETKEYSORDER = X'00FF00' + """, + at: storeURL + ) + let before = try durableStoreSnapshot(at: storeURL) + var replacementAttempted = false + var fetchedPages = [(Int, Int)]() + let migrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: InMemoryKeychain(), + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle, + integrityValidationLimits: + UserStorageIntegrityValidationLimits( + maximumRowsPerEntity: 1, + maximumRowsAcrossStore: 100, + fetchBatchSize: 1, + maximumCanonicalRowBytes: 1 * 1024 * 1024, + maximumAttributeBytes: 256 * 1024, + maximumRelationshipDestinations: 10_000, + maximumTransformableElements: 4_096 + ), + storeReplacer: { _, _ in + replacementAttempted = true + }, + privateSourceObjectIDPageDidFetch: { + fetchedPages.append(($0, $1)) + } + ) + + XCTAssertThrowsError(try migrator.performMigration()) { + error in + guard + let migrationError = + error as? UserStorageMigrationError, + case .privateSourceRepairFailed = migrationError + else { + return XCTFail( + "Expected privateSourceRepairFailed, got \(error)" + ) + } + } + XCTAssertTrue(fetchedPages.isEmpty) + XCTAssertFalse(replacementAttempted) + XCTAssertEqual( + try durableStoreSnapshot(at: storeURL), + before + ) + } + + func testMigration_whenPrivateRepairRowsSpanSingleIDPages_thenEnumeratesEveryWalletExactlyOnce() throws { + let sourceModel = try model(for: .version13) + try createStore(at: storeURL, model: sourceModel) { + context in + try self.insertLegacyWallets( + in: context, + model: sourceModel + ) + } + var fetchedPages = [(Int, Int)]() + var integrityPages = [(Int, Int)]() + let migrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: InMemoryKeychain(), + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle, + integrityValidationLimits: + UserStorageIntegrityValidationLimits( + maximumRowsPerEntity: 100, + maximumRowsAcrossStore: 1_000, + fetchBatchSize: 1, + maximumCanonicalRowBytes: 1 * 1024 * 1024, + maximumAttributeBytes: 256 * 1024, + maximumRelationshipDestinations: 10_000, + maximumTransformableElements: 4_096 + ), + privateSourceObjectIDPageDidFetch: { + fetchedPages.append(($0, $1)) + }, + integrityObjectPageDidFetch: { + integrityPages.append(($0, $1)) + } + ) + + try migrator.performMigration() + + XCTAssertEqual(fetchedPages.count, 2) + XCTAssertTrue( + fetchedPages.allSatisfy { + $0.0 == 1 && $0.1 == 1 + } + ) + XCTAssertFalse(integrityPages.isEmpty) + XCTAssertTrue( + integrityPages.allSatisfy { + $0.0 == 1 && $0.1 == 1 + } + ) + XCTAssertEqual( + try walletSnapshots( + from: storeURL, + model: model(for: .version14) + ).count, + 2 + ) + } + + func testPerformMigration_whenStagedStoreDropsChainAccount_thenRefusesReplacementAndLeavesSourceUnchanged() throws { + let sourceModel = try model(for: .version13) + try createStore(at: storeURL, model: sourceModel) { context in + try self.insertLegacyWallets( + in: context, + model: sourceModel + ) + } + let originalDurableFiles = try durableStoreSnapshot(at: storeURL) + var replacementAttempted = false + var stagedMutationAttempted = false + + let migrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: InMemoryKeychain(), + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle, + storeReplacer: { _, _ in + replacementAttempted = true + }, + stagedStoreWillValidate: { stagedURL, version in + XCTAssertEqual(version, .version14) + stagedMutationAttempted = true + try self.executeSQLite( + "DELETE FROM ZCDCHAINACCOUNT", + at: stagedURL + ) + } + ) + + XCTAssertThrowsError(try migrator.performMigration()) { error in + guard + let migrationError = + error as? UserStorageMigrationError, + case .stagedStoreValidationFailed = migrationError + else { + return XCTFail("Unexpected error: \(error)") + } + } + XCTAssertTrue(stagedMutationAttempted) + XCTAssertFalse(replacementAttempted) + XCTAssertEqual( + try durableStoreSnapshot(at: storeURL), + originalDurableFiles + ) + } + + func testPerformMigration_whenIntegrityRowBudgetIsExceeded_thenFailsBeforeReplacementAndLeavesSourceUnchanged() throws { + let sourceModel = try model(for: .version13) + try createStore(at: storeURL, model: sourceModel) { context in + try self.insertLegacyWallets( + in: context, + model: sourceModel + ) + } + let originalDurableFiles = try durableStoreSnapshot(at: storeURL) + var replacementAttempted = false + let migrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: InMemoryKeychain(), + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle, + integrityValidationLimits: + UserStorageIntegrityValidationLimits( + maximumRowsPerEntity: 1, + maximumRowsAcrossStore: 100, + fetchBatchSize: 1, + maximumCanonicalRowBytes: 1 * 1024 * 1024, + maximumAttributeBytes: 256 * 1024, + maximumRelationshipDestinations: 10_000, + maximumTransformableElements: 4_096 + ), + storeReplacer: { _, _ in + replacementAttempted = true + } + ) + + XCTAssertThrowsError(try migrator.performMigration()) { error in + guard + let migrationError = + error as? UserStorageMigrationError, + case .privateSourceRepairFailed = migrationError + else { + return XCTFail("Unexpected error: \(error)") + } + } + XCTAssertFalse(replacementAttempted) + XCTAssertEqual( + try durableStoreSnapshot(at: storeURL), + originalDurableFiles + ) + } + + func testPerformMigration_whenCanonicalIdentityByteBudgetIsExceeded_thenFailsBeforeReplacementAndLeavesSourceUnchanged() throws { + let sourceModel = try model(for: .version13) + try createStore(at: storeURL, model: sourceModel) { context in + try self.insertLegacyWallets( + in: context, + model: sourceModel + ) + } + let originalDurableFiles = try durableStoreSnapshot(at: storeURL) + var replacementAttempted = false + let migrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: InMemoryKeychain(), + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle, + integrityValidationLimits: + UserStorageIntegrityValidationLimits( + maximumRowsPerEntity: 100, + maximumRowsAcrossStore: 1_000, + fetchBatchSize: 1, + maximumCanonicalRowBytes: 32, + maximumAttributeBytes: 256 * 1024, + maximumRelationshipDestinations: 10_000, + maximumTransformableElements: 4_096 + ), + storeReplacer: { _, _ in + replacementAttempted = true + } + ) + + XCTAssertThrowsError(try migrator.performMigration()) { error in + guard + let migrationError = + error as? UserStorageMigrationError, + case .stagedStoreValidationFailed = migrationError + else { + return XCTFail("Unexpected error: \(error)") + } + } + XCTAssertFalse(replacementAttempted) + XCTAssertEqual( + try durableStoreSnapshot(at: storeURL), + originalDurableFiles + ) + } + + func testPerformMigration_whenStagedStoreMutatesSameCountGlobalContentOrRelationships_thenRefusesReplacementAndLeavesSourceUnchanged() throws { + let reorderedArchive = try NSKeyedArchiver.archivedData( + withRootObject: ["asset-b", "asset-a"] as NSArray, + requiringSecureCoding: true + ).map { + String(format: "%02X", $0) + }.joined() + let substitutedArchive = try NSKeyedArchiver.archivedData( + withRootObject: ["replacement-chain"] as NSArray, + requiringSecureCoding: true + ).map { + String(format: "%02X", $0) + }.joined() + let mutations = [ + ( + name: "chain-settings", + sql: "UPDATE ZCDCHAINSETTINGS " + + "SET ZISSUEMUTED = CASE ZISSUEMUTED " + + "WHEN 0 THEN 1 ELSE 0 END" + ), + ( + name: "custom-node", + sql: "UPDATE ZCDCUSTOMCHAINNODE " + + "SET ZNAME = 'mutated-node'" + ), + ( + name: "orphan-membership", + sql: "UPDATE ZCDMETAACCOUNT " + + "SET ZSELECTEDCURRENCY = " + + "(SELECT Z_PK FROM ZCDCURRENCY " + + "WHERE ZID = 'orphan-currency')" + ), + ( + name: "preference-reorder", + sql: "UPDATE ZCDMETAACCOUNT SET " + + "ZASSETKEYSORDER = X'\(reorderedArchive)'" + ), + ( + name: "preference-substitution", + sql: "UPDATE ZCDMETAACCOUNT SET " + + "ZFAVOURITECHAINIDS = X'\(substitutedArchive)'" + ) + ] + + for sourceVersion in [ + UserStorageVersion.version12, + UserStorageVersion.version13 + ] { + for mutation in mutations { + let caseURL = testDirectory.appendingPathComponent( + "\(sourceVersion.rawValue)-" + + "\(mutation.name).sqlite" + ) + let sourceModel = try model(for: sourceVersion) + try createStore( + at: caseURL, + model: sourceModel + ) { context in + self.insertCurrentWallet( + in: context, + model: sourceModel + ) + + let walletRequest = + NSFetchRequest( + entityName: "CDMetaAccount" + ) + let wallet = try XCTUnwrap( + context.fetch(walletRequest).first + ) + wallet.setValue( + ["filter-a", "filter-b"] as NSArray, + forKey: "assetFilterOptions" + ) + wallet.setValue( + ["asset-a", "asset-b"] as NSArray, + forKey: "assetKeysOrder" + ) + wallet.setValue( + ["favourite-a", "favourite-b"] as NSArray, + forKey: "favouriteChainIds" + ) + wallet.setValue( + ["unused-a", "unused-b"] as NSArray, + forKey: "unusedChainIds" + ) + + let settingsEntity = try XCTUnwrap( + sourceModel.entitiesByName[ + "CDChainSettings" + ] + ) + let chainSettings = NSManagedObject( + entity: settingsEntity, + insertInto: context + ) + chainSettings.setValue( + "settings-chain", + forKey: "chainId" + ) + chainSettings.setValue( + true, + forKey: "autobalanced" + ) + chainSettings.setValue( + false, + forKey: "issueMuted" + ) + + let nodeEntity = try XCTUnwrap( + sourceModel.entitiesByName[ + "CDCustomChainNode" + ] + ) + let node = NSManagedObject( + entity: nodeEntity, + insertInto: context + ) + node.setValue( + "original-node", + forKey: "name" + ) + node.setValue( + URL(string: "wss://node.example"), + forKey: "url" + ) + + let currencyEntity = try XCTUnwrap( + sourceModel.entitiesByName[ + "CDCurrency" + ] + ) + let orphan = NSManagedObject( + entity: currencyEntity, + insertInto: context + ) + orphan.setValue( + "orphan-currency", + forKey: "id" + ) + orphan.setValue( + "Orphan Currency", + forKey: "name" + ) + orphan.setValue( + "ORP", + forKey: "symbol" + ) + } + + let originalDurableFiles = + try durableStoreSnapshot(at: caseURL) + var replacementAttempted = false + let migrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: caseURL, + modelDirectory: + UserStorageParams.modelDirectory, + keystore: InMemoryKeychain(), + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle, + storeReplacer: { _, _ in + replacementAttempted = true + }, + stagedStoreWillValidate: { + stagedURL, + _ in + + try self.executeSQLite( + mutation.sql, + at: stagedURL + ) + } + ) + + XCTAssertThrowsError( + try migrator.performMigration() + ) { error in + guard + let migrationError = + error as? UserStorageMigrationError, + case .stagedStoreValidationFailed = + migrationError + else { + return XCTFail( + "Unexpected error for " + + "\(sourceVersion.rawValue)." + + "\(mutation.name): \(error)" + ) + } + } + XCTAssertFalse( + replacementAttempted, + "\(sourceVersion.rawValue)." + + "\(mutation.name)" + ) + XCTAssertEqual( + try durableStoreSnapshot(at: caseURL), + originalDurableFiles, + "\(sourceVersion.rawValue)." + + "\(mutation.name)" + ) + } + } + } + + func testPerformMigration_whenWALSourceReplacementFails_thenOriginalDurableFilesAreByteForByteUnchanged() throws { + let sourceModel = try model(for: .version13) + try createStore(at: storeURL, model: sourceModel) { context in + self.insertCurrentWallet(in: context, model: sourceModel) + } + + let database = try openSQLite(at: storeURL) + defer { + sqlite3_close(database) + } + try executeSQLite( + "PRAGMA wal_autocheckpoint = 0; " + + "UPDATE ZCDMETAACCOUNT SET ZNAME = 'WAL-only wallet';", + database: database + ) + + let walURL = URL(fileURLWithPath: storeURL.path + "-wal") + XCTAssertTrue(FileManager.default.fileExists(atPath: walURL.path)) + XCTAssertFalse(try Data(contentsOf: walURL).isEmpty) + let originalDurableFiles = try durableStoreSnapshot(at: storeURL) + var replacementAttempted = false + + let migrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: InMemoryKeychain(), + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle, + storeReplacer: { _, stagedURL in + replacementAttempted = true + try self.useStore( + at: stagedURL, + model: self.model(for: .version14) + ) { context in + let request = NSFetchRequest( + entityName: "CDMetaAccount" + ) + let wallet = try XCTUnwrap( + context.fetch(request).first + ) + XCTAssertEqual( + wallet.value(forKey: "name") as? String, + "WAL-only wallet" + ) + } + throw UserStorageCommitTestError.storeReplacementFailed + } + ) + + XCTAssertThrowsError(try migrator.performMigration()) { error in + XCTAssertEqual( + error as? UserStorageCommitTestError, + .storeReplacementFailed + ) + } + XCTAssertTrue(replacementAttempted) + XCTAssertEqual( + try durableStoreSnapshot(at: storeURL), + originalDurableFiles + ) + } + + func testCopyStoreFamily_whenRollbackJournalExists_thenCopiesRecoveryStateWithoutSourceShmOrMutation() throws { + let sourceURL = testDirectory.appendingPathComponent( + "CopyFamilySource.sqlite" + ) + let destinationURL = testDirectory.appendingPathComponent( + "CopyFamilyDestination.sqlite" + ) + let sourceFiles = [ + sourceURL: Data([0x01, 0x02]), + URL(fileURLWithPath: sourceURL.path + "-wal"): + Data([0x03, 0x04]), + URL(fileURLWithPath: sourceURL.path + "-journal"): + Data([0x05, 0x06]), + URL(fileURLWithPath: sourceURL.path + "-shm"): + Data([0x07, 0x08]) + ] + for (url, data) in sourceFiles { + try data.write(to: url, options: .atomic) + } + let originalSourceFiles = try storeFamilySnapshot(at: sourceURL) + + try makeMigrator().copyStoreFamily( + from: sourceURL, + to: destinationURL + ) + + XCTAssertEqual( + try Data(contentsOf: destinationURL), + sourceFiles[sourceURL] + ) + XCTAssertEqual( + try Data( + contentsOf: URL( + fileURLWithPath: destinationURL.path + "-wal" + ) + ), + sourceFiles[URL(fileURLWithPath: sourceURL.path + "-wal")] + ) + XCTAssertEqual( + try Data( + contentsOf: URL( + fileURLWithPath: destinationURL.path + "-journal" + ) + ), + sourceFiles[ + URL(fileURLWithPath: sourceURL.path + "-journal") + ] + ) + XCTAssertFalse( + FileManager.default.fileExists( + atPath: destinationURL.path + "-shm" + ) + ) + XCTAssertEqual( + try storeFamilySnapshot(at: sourceURL), + originalSourceFiles + ) + } + + func testMetadataProbe_whenObjectiveCExceptionIsRaised_thenFailsClosedOnPrivateCopiesAndNeverMutatesSource() throws { + let currentModel = try model(for: .version14) + try createStore( + at: storeURL, + model: currentModel + ) { context in + self.insertCurrentWallet( + in: context, + model: currentModel + ) + } + let before = try durableStoreSnapshot(at: storeURL) + var probedURLs = [URL]() + let migrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: InMemoryKeychain(), + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle, + metadataReader: { url in + probedURLs.append(url) + NSException( + name: .invalidArgumentException, + reason: "private metadata probe failure", + userInfo: ["stored-payload": "must-not-escape"] + ).raise() + return [:] + } + ) + + XCTAssertTrue(migrator.requiresMigration()) + XCTAssertThrowsError( + try migrator.performMigration() + ) { error in + guard + let migrationError = + error as? UserStorageMigrationError, + case .objectiveCException = migrationError + else { + return XCTFail("Unexpected error: \(error)") + } + } + + XCTAssertEqual(probedURLs.count, 2) + XCTAssertTrue( + probedURLs.allSatisfy { + $0.standardizedFileURL != + self.storeURL.standardizedFileURL + } + ) + XCTAssertEqual( + try durableStoreSnapshot(at: storeURL), + before + ) + } + + func testMetadataProbe_whenSourceHasHotRollbackJournal_thenRecoversOnlyPrivateCopyAndLeavesSourceFamilyByteIdentical() throws { + let currentModel = try model(for: .version14) + try createStore( + at: storeURL, + model: currentModel + ) { context in + self.insertCurrentWallet( + in: context, + model: currentModel + ) + } + + let database = try openSQLite(at: storeURL) + defer { + try? executeSQLite( + "ROLLBACK", + database: database + ) + sqlite3_close(database) + } + try executeSQLite( + "PRAGMA journal_mode = DELETE; " + + "BEGIN IMMEDIATE; " + + "UPDATE ZCDMETAACCOUNT " + + "SET ZNAME = 'uncommitted-name';", + database: database + ) + + let journalURL = URL( + fileURLWithPath: storeURL.path + "-journal" + ) + XCTAssertTrue( + FileManager.default.fileExists( + atPath: journalURL.path + ) + ) + XCTAssertFalse( + try Data(contentsOf: journalURL).isEmpty + ) + + let before = try durableStoreSnapshot(at: storeURL) + let migrator = makeMigrator() + + XCTAssertFalse(migrator.requiresMigration()) + XCTAssertFalse(try migrator.performMigration()) + XCTAssertEqual( + try durableStoreSnapshot(at: storeURL), + before + ) + } + + func testSynchronousMigration_whenStoreIsCurrent_thenUsesSinglePrivateMetadataProbe() throws { + let currentModel = try model(for: .version14) + try createStore( + at: storeURL, + model: currentModel + ) { context in + self.insertCurrentWallet( + in: context, + model: currentModel + ) + } + let before = try durableStoreSnapshot(at: storeURL) + var probedURLs = [URL]() + let migrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: InMemoryKeychain(), + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle, + metadataReader: { url in + probedURLs.append(url) + return try NSPersistentStoreCoordinator + .metadataForPersistentStore( + ofType: NSSQLiteStoreType, + at: url, + options: nil + ) + } + ) + + try (migrator as Migrating).migrate() + + XCTAssertEqual(probedURLs.count, 1) + XCTAssertNotEqual( + probedURLs.first?.standardizedFileURL, + storeURL.standardizedFileURL + ) + XCTAssertEqual( + try durableStoreSnapshot(at: storeURL), + before + ) + } + + func testPerformMigration_whenStoreAlreadyCurrent_thenRepeatedCallsAreByteForByteIdempotent() throws { + let currentModel = try model(for: .version14) + try createStore(at: storeURL, model: currentModel) { context in + self.insertCurrentWallet(in: context, model: currentModel) + } + + let before = try durableStoreSnapshot(at: storeURL) + let migrator = makeMigrator() + + XCTAssertFalse(migrator.requiresMigration()) + try migrator.performMigration() + try migrator.performMigration() + + XCTAssertFalse(migrator.requiresMigration()) + XCTAssertEqual(try durableStoreSnapshot(at: storeURL), before) + XCTAssertEqual( + try walletSnapshots(from: storeURL, model: currentModel).map(\.metaId), + ["current-wallet"] + ) + } + + func testPerformMigration_whenStoreIsCorrupt_thenThrowsAndDoesNotModifyFiles() throws { + try Data("not-a-core-data-store".utf8).write(to: storeURL, options: .atomic) + let before = try storeFamilySnapshot(at: storeURL) + let migrator = makeMigrator() + + XCTAssertTrue(migrator.requiresMigration()) + XCTAssertThrowsError(try migrator.performMigration()) { error in + guard let migrationError = error as? UserStorageMigrationError else { + return XCTFail("Unexpected error: \(error)") + } + + guard case .metadataUnreadable = migrationError else { + return XCTFail("Expected metadataUnreadable, got \(migrationError)") + } + } + XCTAssertEqual(try storeFamilySnapshot(at: storeURL), before) + } + + func testPerformMigration_whenStoreModelIsUnknown_thenThrowsAndDoesNotModifyFiles() throws { + try createStore(at: storeURL, model: makeUnknownModel()) { _ in } + let before = try storeFamilySnapshot(at: storeURL) + let migrator = makeMigrator() + + XCTAssertTrue(migrator.requiresMigration()) + XCTAssertThrowsError(try migrator.performMigration()) { error in + guard let migrationError = error as? UserStorageMigrationError else { + return XCTFail("Unexpected error: \(error)") + } + + guard case .unknownStoreVersion = migrationError else { + return XCTFail("Expected unknownStoreVersion, got \(migrationError)") + } + } + XCTAssertEqual(try storeFamilySnapshot(at: storeURL), before) + } + + func testPerformMigration_whenProcessDiesAfterRemovingLiveMainStore_thenRelaunchRestoresAndMigratesProtectedWallets() throws { + let legacyModel = try model(for: .version13) + try createStore(at: storeURL, model: legacyModel) { context in + try self.insertLegacyWallets( + in: context, + model: legacyModel + ) + } + let sourceWallets = try walletSnapshots( + from: storeURL, + model: legacyModel + ) + let interruptedMigrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: InMemoryKeychain(), + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle, + storeReplacer: { targetURL, _ in + try FileManager.default.removeItem(at: targetURL) + throw CrashConsistentStoreReplacementInterruption + .simulatedProcessDeath + } + ) + + XCTAssertThrowsError( + try interruptedMigrator.performMigration() + ) { error in + guard + let interruption = error as? CrashConsistentStoreReplacementInterruption, + case .simulatedProcessDeath = interruption + else { + return XCTFail("Unexpected error: \(error)") + } + } + XCTAssertFalse( + FileManager.default.fileExists(atPath: storeURL.path), + "The fixture must reproduce the missing-main startup state" + ) + + let relaunchedMigrator = makeMigrator() + XCTAssertTrue(try relaunchedMigrator.performMigration()) + + XCTAssertEqual( + try metadataChecksum(at: storeURL), + currentChecksum + ) + XCTAssertEqual( + try walletSnapshots( + from: storeURL, + model: model(for: .version14) + ), + sourceWallets + ) + XCTAssertFalse(relaunchedMigrator.requiresMigration()) + XCTAssertFalse( + FileManager.default.fileExists( + atPath: testDirectory + .appendingPathComponent( + ".FearlessStoreReplacement", + isDirectory: true + ).path + ) + ) + } + + func testPerformMigration_whenStoreReplacementFails_thenProtectedDataIsNeverFinalized() throws { + let legacyModel = try model(for: .version13) + try createStore(at: storeURL, model: legacyModel) { context in + try self.insertLegacyWallets(in: context, model: legacyModel) + } + let walletsBeforeMigration = try walletSnapshots( + from: storeURL, + model: legacyModel + ) + let recorder = UserStorageCommitRecorder() + let keystoreMigrator = RecordingKeystoreMigrator( + recorder: recorder + ) + let settingsMigrator = RecordingSettingsMigrator( + recorder: recorder + ) + let migrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: InMemoryKeychain(), + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle, + storeReplacer: { _, _ in + recorder.record(.storeReplacement) + throw UserStorageCommitTestError.storeReplacementFailed + }, + keystoreMigratorFactory: { _, _, _ in keystoreMigrator }, + settingsMigratorFactory: { _, _, _ in settingsMigrator } + ) + + XCTAssertThrowsError(try migrator.performMigration()) { error in + XCTAssertEqual( + error as? UserStorageCommitTestError, + .storeReplacementFailed + ) + } + + XCTAssertEqual( + recorder.events, + [ + .keystorePrepare, + .settingsPrepare, + .storeReplacement + ] + ) + XCTAssertEqual(try metadataChecksum(at: storeURL), legacyChecksum) + XCTAssertEqual( + try walletSnapshots(from: storeURL, model: legacyModel), + walletsBeforeMigration + ) + } + + func testPerformMigration_whenStoreReplacementSucceeds_thenProtectedDataFinalizesAfterward() throws { + let legacyModel = try model(for: .version13) + try createStore(at: storeURL, model: legacyModel) { context in + try self.insertLegacyWallets(in: context, model: legacyModel) + } + let recorder = UserStorageCommitRecorder() + let keystoreMigrator = RecordingKeystoreMigrator( + recorder: recorder + ) + let settingsMigrator = RecordingSettingsMigrator( + recorder: recorder + ) + let migrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: InMemoryKeychain(), + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle, + storeReplacer: { targetURL, sourceURL in + recorder.record(.storeReplacement) + try NSPersistentStoreCoordinator.replaceStore( + at: targetURL, + withStoreAt: sourceURL + ) + }, + keystoreMigratorFactory: { _, _, _ in keystoreMigrator }, + settingsMigratorFactory: { _, _, _ in settingsMigrator } + ) + + try migrator.performMigration() + + XCTAssertEqual( + recorder.events, + [ + .keystorePrepare, + .settingsPrepare, + .storeReplacement, + .keystoreFinalize, + .settingsFinalize + ] + ) + XCTAssertEqual(try metadataChecksum(at: storeURL), currentChecksum) + } + + func testSettingsCleanup_whenReplacementFailsBeforeCommit_thenRelaunchRollsBackJournalWithoutDeletingLegacySelections() throws { + let legacyModel = try model(for: .version13) + try createStore( + at: storeURL, + model: legacyModel + ) { context in + self.insertCurrentWallet( + in: context, + model: legacyModel + ) + } + let sourceBefore = try durableStoreSnapshot(at: storeURL) + let settings = InMemorySettingsManager() + settings.set( + value: "legacy-account", + for: SettingsKey.selectedAccount.rawValue + ) + settings.set( + value: "legacy-connection", + for: SettingsKey.selectedConnection.rawValue + ) + let cleanupMigrator = + CleanupJournalSettingsMigrator( + sourceVersion: .version13, + destinationVersion: .version14, + settings: settings + ) + let migrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: InMemoryKeychain(), + settings: settings, + fileManager: .default, + modelBundle: appBundle, + storeReplacer: { _, _ in + throw UserStorageCommitTestError + .storeReplacementFailed + }, + settingsMigratorFactory: { + _, + _, + _ in + + cleanupMigrator + } + ) + + XCTAssertThrowsError( + try migrator.performMigration() + ) { error in + XCTAssertEqual( + error as? UserStorageCommitTestError, + .storeReplacementFailed + ) + } + XCTAssertNotNil( + settings.data( + for: SettingsMigrator.pendingCleanupKey + ) + ) + XCTAssertEqual( + try durableStoreSnapshot(at: storeURL), + sourceBefore + ) + + XCTAssertTrue( + try UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: + UserStorageParams.modelDirectory, + keystore: InMemoryKeychain(), + settings: settings, + fileManager: .default, + modelBundle: appBundle + ).performMigration() + ) + + XCTAssertEqual( + settings.string( + for: SettingsKey.selectedAccount.rawValue + ), + "legacy-account" + ) + XCTAssertEqual( + settings.string( + for: SettingsKey.selectedConnection.rawValue + ), + "legacy-connection" + ) + XCTAssertNil( + settings.anyValue( + for: SettingsMigrator.pendingCleanupKey + ) + ) + } + + func testSettingsCleanup_whenProcessStopsAfterDatabaseReplacement_thenCurrentVersionRelaunchFinishesCleanup() throws { + let legacyModel = try model(for: .version13) + try createStore( + at: storeURL, + model: legacyModel + ) { context in + self.insertCurrentWallet( + in: context, + model: legacyModel + ) + } + let settings = InMemorySettingsManager() + settings.set( + value: "legacy-account", + for: SettingsKey.selectedAccount.rawValue + ) + settings.set( + value: "legacy-connection", + for: SettingsKey.selectedConnection.rawValue + ) + let cleanupMigrator = + CleanupJournalSettingsMigrator( + sourceVersion: .version13, + destinationVersion: .version14, + settings: settings + ) + let migrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: InMemoryKeychain(), + settings: settings, + fileManager: .default, + modelBundle: appBundle, + settingsMigratorFactory: { + _, + _, + _ in + + cleanupMigrator + }, + storeReplacementBoundaryHook: { boundary in + guard boundary == .committedMarkerPersisted else { + return + } + + throw CrashConsistentStoreReplacementInterruption + .simulatedProcessDeath + } + ) + + XCTAssertThrowsError( + try migrator.performMigration() + ) + XCTAssertEqual( + try metadataChecksum(at: storeURL), + currentChecksum + ) + XCTAssertNotNil( + settings.data( + for: SettingsMigrator.pendingCleanupKey + ) + ) + XCTAssertEqual( + settings.string( + for: SettingsKey.selectedAccount.rawValue + ), + "legacy-account" + ) + + let relaunchedMigrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: InMemoryKeychain(), + settings: settings, + fileManager: .default, + modelBundle: appBundle + ) + XCTAssertFalse( + try relaunchedMigrator.performMigration() + ) + XCTAssertNil( + settings.anyValue( + for: SettingsKey.selectedAccount.rawValue + ) + ) + XCTAssertNil( + settings.anyValue( + for: SettingsKey.selectedConnection.rawValue + ) + ) + XCTAssertNil( + settings.anyValue( + for: SettingsMigrator.pendingCleanupKey + ) + ) + } + + func testSettingsCleanup_whenFinalizeFails_thenErrorPropagatesAndCurrentVersionRelaunchRetriesIdempotently() throws { + let legacyModel = try model(for: .version13) + try createStore( + at: storeURL, + model: legacyModel + ) { context in + self.insertCurrentWallet( + in: context, + model: legacyModel + ) + } + let settings = InMemorySettingsManager() + settings.set( + value: "legacy-account", + for: SettingsKey.selectedAccount.rawValue + ) + settings.set( + value: "legacy-connection", + for: SettingsKey.selectedConnection.rawValue + ) + let cleanupMigrator = + CleanupJournalSettingsMigrator( + sourceVersion: .version13, + destinationVersion: .version14, + settings: settings, + finalizeError: UserStorageCommitTestError + .settingsFinalizeFailed + ) + let migrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: InMemoryKeychain(), + settings: settings, + fileManager: .default, + modelBundle: appBundle, + settingsMigratorFactory: { + _, + _, + _ in + + cleanupMigrator + } + ) + + XCTAssertThrowsError( + try migrator.performMigration() + ) { error in + XCTAssertEqual( + error as? UserStorageCommitTestError, + .settingsFinalizeFailed + ) + } + XCTAssertEqual( + try metadataChecksum(at: storeURL), + currentChecksum + ) + XCTAssertNotNil( + settings.data( + for: SettingsMigrator.pendingCleanupKey + ) + ) + + let relaunchedMigrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: InMemoryKeychain(), + settings: settings, + fileManager: .default, + modelBundle: appBundle + ) + XCTAssertFalse( + try relaunchedMigrator.performMigration() + ) + XCTAssertFalse( + try relaunchedMigrator.performMigration() + ) + XCTAssertNil( + settings.anyValue( + for: SettingsKey.selectedAccount.rawValue + ) + ) + XCTAssertNil( + settings.anyValue( + for: SettingsKey.selectedConnection.rawValue + ) + ) + XCTAssertNil( + settings.anyValue( + for: SettingsMigrator.pendingCleanupKey + ) + ) + } + + func testSettingsCleanup_whenJournalIsValid_thenDeletesOnlyExactLegacySelections() throws { + let settings = InMemorySettingsManager() + let unrelatedKey = "io.fearless.oauth.refresh-token" + settings.set(value: "legacy-account", for: SettingsKey.selectedAccount.rawValue) + settings.set(value: "legacy-connection", for: SettingsKey.selectedConnection.rawValue) + settings.set(value: "keep-me", for: unrelatedKey) + let journal = try JSONSerialization.data( + withJSONObject: [ + "schemaVersion": 1, + "sourceVersion": UserStorageVersion.version13.rawValue, + "destinationVersion": UserStorageVersion.version14.rawValue, + "keysToRemove": [ + SettingsKey.selectedAccount.rawValue, + SettingsKey.selectedConnection.rawValue + ] + ], + options: [.sortedKeys] + ) + settings.set(value: journal, for: SettingsMigrator.pendingCleanupKey) + + try SettingsMigrator.recoverPendingCleanup( + settings: settings, + currentVersion: .version14 + ) + + XCTAssertNil(settings.anyValue(for: SettingsKey.selectedAccount.rawValue)) + XCTAssertNil(settings.anyValue(for: SettingsKey.selectedConnection.rawValue)) + XCTAssertEqual(settings.string(for: unrelatedKey), "keep-me") + XCTAssertNil(settings.anyValue(for: SettingsMigrator.pendingCleanupKey)) + } + + func testSettingsCleanup_whenJournalTargetsUnrelatedKey_thenFailsClosedAndPreservesEverything() throws { + let settings = InMemorySettingsManager() + let unrelatedKey = "io.fearless.oauth.refresh-token" + settings.set(value: "legacy-account", for: SettingsKey.selectedAccount.rawValue) + settings.set(value: "legacy-connection", for: SettingsKey.selectedConnection.rawValue) + settings.set(value: "keep-me", for: unrelatedKey) + let journal = try JSONSerialization.data( + withJSONObject: [ + "schemaVersion": 1, + "sourceVersion": UserStorageVersion.version13.rawValue, + "destinationVersion": UserStorageVersion.version14.rawValue, + "keysToRemove": [ + SettingsKey.selectedAccount.rawValue, + unrelatedKey + ] + ], + options: [.sortedKeys] + ) + settings.set(value: journal, for: SettingsMigrator.pendingCleanupKey) + + XCTAssertThrowsError( + try SettingsMigrator.recoverPendingCleanup( + settings: settings, + currentVersion: .version14 + ) + ) { error in + guard + let migrationError = error as? SettingsMigratingError, + case .invalidCleanupJournal = migrationError + else { + return XCTFail("Unexpected error: \(error)") + } + } + + XCTAssertEqual( + settings.string(for: SettingsKey.selectedAccount.rawValue), + "legacy-account" + ) + XCTAssertEqual( + settings.string(for: SettingsKey.selectedConnection.rawValue), + "legacy-connection" + ) + XCTAssertEqual(settings.string(for: unrelatedKey), "keep-me") + XCTAssertEqual( + settings.data(for: SettingsMigrator.pendingCleanupKey), + journal + ) + } + + func testSettingsCleanup_whenJournalHasDuplicateField_thenFailsClosed() { + let settings = InMemorySettingsManager() + settings.set(value: "legacy-account", for: SettingsKey.selectedAccount.rawValue) + let journal = Data( + """ + {"schemaVersion":1,"schemaVersion":1,"sourceVersion":"version13","destinationVersion":"version14","keysToRemove":["selectedAccount","selectedConnection"]} + """.utf8 + ) + settings.set(value: journal, for: SettingsMigrator.pendingCleanupKey) + + XCTAssertThrowsError( + try SettingsMigrator.recoverPendingCleanup( + settings: settings, + currentVersion: .version14 + ) + ) + XCTAssertEqual( + settings.string(for: SettingsKey.selectedAccount.rawValue), + "legacy-account" + ) + XCTAssertEqual( + settings.data(for: SettingsMigrator.pendingCleanupKey), + journal + ) + } + + func testSettingsCleanup_whenJournalHasUnknownField_thenFailsClosed() throws { + let settings = InMemorySettingsManager() + settings.set(value: "legacy-account", for: SettingsKey.selectedAccount.rawValue) + let journal = try JSONSerialization.data( + withJSONObject: [ + "schemaVersion": 1, + "sourceVersion": UserStorageVersion.version13.rawValue, + "destinationVersion": UserStorageVersion.version14.rawValue, + "keysToRemove": [ + SettingsKey.selectedAccount.rawValue, + SettingsKey.selectedConnection.rawValue + ], + "unexpected": true + ], + options: [.sortedKeys] + ) + settings.set(value: journal, for: SettingsMigrator.pendingCleanupKey) + + XCTAssertThrowsError( + try SettingsMigrator.recoverPendingCleanup( + settings: settings, + currentVersion: .version14 + ) + ) + XCTAssertEqual( + settings.string(for: SettingsKey.selectedAccount.rawValue), + "legacy-account" + ) + XCTAssertEqual( + settings.data(for: SettingsMigrator.pendingCleanupKey), + journal + ) + } + + func testSettingsCleanup_whenJournalIsOversized_thenFailsBeforeDeletingAnything() { + let settings = InMemorySettingsManager() + settings.set(value: "legacy-account", for: SettingsKey.selectedAccount.rawValue) + let journal = Data(repeating: 0x20, count: 4_097) + settings.set(value: journal, for: SettingsMigrator.pendingCleanupKey) + + XCTAssertThrowsError( + try SettingsMigrator.recoverPendingCleanup( + settings: settings, + currentVersion: .version14 + ) + ) + XCTAssertEqual( + settings.string(for: SettingsKey.selectedAccount.rawValue), + "legacy-account" + ) + XCTAssertEqual( + settings.data(for: SettingsMigrator.pendingCleanupKey), + journal + ) + } + + func testPerformMigration_whenKeychainReadFails_thenPropagatesAndLeavesLegacyStoreUntouched() throws { + let legacyModel = try model(for: .version13) + try createStore(at: storeURL, model: legacyModel) { context in + try self.insertLegacyWallets(in: context, model: legacyModel) + } + let walletsBeforeMigration = try walletSnapshots( + from: storeURL, + model: legacyModel + ) + let keystore = FaultInjectingKeystore() + keystore.failures.insert( + .fetch(KeystoreMigrator.pendingCleanupIdentifier) + ) + let migrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: keystore, + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle + ) + + XCTAssertThrowsError(try migrator.performMigration()) { error in + XCTAssertEqual( + error as? FaultInjectingKeystore.Error, + .injected(.fetch(KeystoreMigrator.pendingCleanupIdentifier)) + ) + } + XCTAssertEqual(try metadataChecksum(at: storeURL), legacyChecksum) + XCTAssertEqual( + try walletSnapshots(from: storeURL, model: legacyModel), + walletsBeforeMigration + ) + } + + func testPerformMigration_whenReplacementKeySaveFails_thenNeverReplacesStoreOrDeletesLegacyKey() throws { + let legacyModel = try model(for: .version13) + try createStore(at: storeURL, model: legacyModel) { context in + try self.insertLegacyWallets(in: context, model: legacyModel) + } + let legacyIdentifier = try legacyDeletionIdentifier(0x01) + let replacementIdentifier = destinationMigrationIdentifier() + let legacyKey = Data([0x01, 0x02]) + let replacementKey = Data([0x03, 0x04]) + let keystore = FaultInjectingKeystore( + values: [legacyIdentifier: legacyKey] + ) + keystore.failures.insert(.add(replacementIdentifier)) + + let keystoreMigrator = KeystoreMigrator( + sourceVersion: .version13, + destinationVersion: .version14, + keystore: keystore + ) + keystoreMigrator.deleteKey(for: legacyIdentifier) + keystoreMigrator.save( + key: replacementKey, + for: replacementIdentifier + ) + var replacementAttempted = false + let migrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: keystore, + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle, + storeReplacer: { _, _ in + replacementAttempted = true + }, + keystoreMigratorFactory: { _, _, _ in keystoreMigrator } + ) + + XCTAssertThrowsError(try migrator.performMigration()) { error in + XCTAssertEqual( + error as? FaultInjectingKeystore.Error, + .injected(.add(replacementIdentifier)) + ) + } + XCTAssertFalse(replacementAttempted) + XCTAssertEqual(keystore.value(for: legacyIdentifier), legacyKey) + XCTAssertNil(keystore.value(for: replacementIdentifier)) + XCTAssertNotNil( + keystore.value(for: KeystoreMigrator.pendingCleanupIdentifier) + ) + XCTAssertEqual(try metadataChecksum(at: storeURL), legacyChecksum) + + keystore.failures.remove(.add(replacementIdentifier)) + try KeystoreMigrator.recoverPendingCleanup( + keystore: keystore, + currentVersion: .version13 + ) + XCTAssertEqual(keystore.value(for: legacyIdentifier), legacyKey) + XCTAssertNil(keystore.value(for: replacementIdentifier)) + XCTAssertNil( + keystore.value(for: KeystoreMigrator.pendingCleanupIdentifier) + ) + } + + func testPerformMigration_whenStoreReplacementFails_thenKeepsOldAndStagedKeysUntilSafeRollback() throws { + let legacyModel = try model(for: .version13) + try createStore(at: storeURL, model: legacyModel) { context in + try self.insertLegacyWallets(in: context, model: legacyModel) + } + let legacyIdentifier = try legacyDeletionIdentifier(0x11) + let replacementIdentifier = destinationMigrationIdentifier() + let legacyKey = Data([0x11, 0x12]) + let replacementKey = Data([0x13, 0x14]) + let keystore = FaultInjectingKeystore( + values: [legacyIdentifier: legacyKey] + ) + let keystoreMigrator = KeystoreMigrator( + sourceVersion: .version13, + destinationVersion: .version14, + keystore: keystore + ) + keystoreMigrator.deleteKey(for: legacyIdentifier) + keystoreMigrator.save( + key: replacementKey, + for: replacementIdentifier + ) + let migrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: keystore, + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle, + storeReplacer: { _, _ in + XCTAssertEqual( + keystore.value(for: legacyIdentifier), + legacyKey + ) + XCTAssertEqual( + keystore.value(for: replacementIdentifier), + replacementKey + ) + throw UserStorageCommitTestError.storeReplacementFailed + }, + keystoreMigratorFactory: { _, _, _ in keystoreMigrator } + ) + + XCTAssertThrowsError(try migrator.performMigration()) { error in + XCTAssertEqual( + error as? UserStorageCommitTestError, + .storeReplacementFailed + ) + } + XCTAssertEqual(keystore.value(for: legacyIdentifier), legacyKey) + XCTAssertEqual( + keystore.value(for: replacementIdentifier), + replacementKey + ) + XCTAssertNotNil( + keystore.value(for: KeystoreMigrator.pendingCleanupIdentifier) + ) + XCTAssertEqual(try metadataChecksum(at: storeURL), legacyChecksum) + + try KeystoreMigrator.recoverPendingCleanup( + keystore: keystore, + currentVersion: .version13 + ) + XCTAssertEqual(keystore.value(for: legacyIdentifier), legacyKey) + XCTAssertNil(keystore.value(for: replacementIdentifier)) + XCTAssertNil( + keystore.value(for: KeystoreMigrator.pendingCleanupIdentifier) + ) + } + + func testPerformMigration_whenPostReplacementCleanupFails_thenFailsAndRelaunchFinishesCleanupWithoutLosingNewKey() throws { + let legacyModel = try model(for: .version13) + try createStore(at: storeURL, model: legacyModel) { context in + try self.insertLegacyWallets(in: context, model: legacyModel) + } + let legacyIdentifier = try legacyDeletionIdentifier(0x21) + let replacementIdentifier = destinationMigrationIdentifier() + let legacyKey = Data([0x21, 0x22]) + let replacementKey = Data([0x23, 0x24]) + let keystore = FaultInjectingKeystore( + values: [legacyIdentifier: legacyKey] + ) + keystore.failures.insert(.delete(legacyIdentifier)) + let keystoreMigrator = KeystoreMigrator( + sourceVersion: .version13, + destinationVersion: .version14, + keystore: keystore + ) + keystoreMigrator.deleteKey(for: legacyIdentifier) + keystoreMigrator.save( + key: replacementKey, + for: replacementIdentifier + ) + let migrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: keystore, + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle, + keystoreMigratorFactory: { _, _, _ in keystoreMigrator } + ) + + XCTAssertThrowsError( + try migrator.performMigration() + ) { error in + XCTAssertEqual( + error as? FaultInjectingKeystore.Error, + .injected(.delete(legacyIdentifier)) + ) + } + XCTAssertEqual(try metadataChecksum(at: storeURL), currentChecksum) + XCTAssertEqual(keystore.value(for: legacyIdentifier), legacyKey) + XCTAssertEqual( + keystore.value(for: replacementIdentifier), + replacementKey + ) + XCTAssertNotNil( + keystore.value(for: KeystoreMigrator.pendingCleanupIdentifier) + ) + + keystore.failures.remove(.delete(legacyIdentifier)) + let relaunchedMigrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: keystore, + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle + ) + + XCTAssertNoThrow(try relaunchedMigrator.migrate()) + XCTAssertNil(keystore.value(for: legacyIdentifier)) + XCTAssertEqual( + keystore.value(for: replacementIdentifier), + replacementKey + ) + XCTAssertNil( + keystore.value(for: KeystoreMigrator.pendingCleanupIdentifier) + ) + XCTAssertEqual(try metadataChecksum(at: storeURL), currentChecksum) + } + + func testMigrate_whenDestinationSecretVerificationFails_thenReturnsFailureAndRetryConvergesOnlyAfterRepair() throws { + let legacyModel = try model(for: .version13) + try createStore(at: storeURL, model: legacyModel) { context in + try self.insertLegacyWallets( + in: context, + model: legacyModel + ) + } + let legacyIdentifier = try legacyDeletionIdentifier(0x25) + let destinationIdentifier = destinationMigrationIdentifier() + let legacyKey = Data([0x25, 0x26]) + let destinationKey = Data([0x27, 0x28]) + let corruptedDestinationKey = Data([0x29, 0x2A]) + let keystore = FaultInjectingKeystore( + values: [legacyIdentifier: legacyKey] + ) + let journalMigrator = KeystoreMigrator( + sourceVersion: .version13, + destinationVersion: .version14, + keystore: keystore + ) + journalMigrator.deleteKey(for: legacyIdentifier) + journalMigrator.save( + key: destinationKey, + for: destinationIdentifier + ) + let failingFinalizeMigrator = + MappingFinalizeFailureKeystoreMigrator( + migrator: journalMigrator, + mappedError: UserStorageCommitTestError + .keystoreFinalizeFailed + ) + let migrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: keystore, + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle, + storeReplacer: { targetURL, sourceURL in + try NSPersistentStoreCoordinator.replaceStore( + at: targetURL, + withStoreAt: sourceURL + ) + try keystore.updateKey( + corruptedDestinationKey, + with: destinationIdentifier + ) + }, + keystoreMigratorFactory: { + _, + _, + _ in + + failingFinalizeMigrator + } + ) + + XCTAssertThrowsError( + try migrator.migrate() + ) { error in + XCTAssertEqual( + error as? UserStorageCommitTestError, + .keystoreFinalizeFailed + ) + } + guard + let finalizeError = + failingFinalizeMigrator.underlyingFinalizeError + as? KeystoreMigratingError, + case let .cleanupDestinationKeyVerificationFailed( + identifier + ) = finalizeError + else { + return XCTFail( + "Expected destination-key verification failure" + ) + } + XCTAssertEqual(identifier, destinationIdentifier) + XCTAssertEqual( + try metadataChecksum(at: storeURL), + currentChecksum + ) + let journal = try XCTUnwrap( + keystore.value( + for: KeystoreMigrator.pendingCleanupIdentifier + ) + ) + XCTAssertEqual( + keystore.value(for: legacyIdentifier), + legacyKey + ) + XCTAssertEqual( + keystore.value(for: destinationIdentifier), + corruptedDestinationKey + ) + + let relaunchedMigrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: keystore, + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle + ) + XCTAssertThrowsError( + try relaunchedMigrator.migrate() + ) { error in + guard + let migrationError = + error as? KeystoreMigratingError, + case let .cleanupDestinationKeyVerificationFailed( + identifier + ) = migrationError + else { + return XCTFail("Unexpected error: \(error)") + } + + XCTAssertEqual(identifier, destinationIdentifier) + } + XCTAssertEqual( + keystore.value(for: legacyIdentifier), + legacyKey + ) + XCTAssertEqual( + keystore.value(for: destinationIdentifier), + corruptedDestinationKey + ) + XCTAssertEqual( + keystore.value( + for: KeystoreMigrator.pendingCleanupIdentifier + ), + journal + ) + + try keystore.updateKey( + destinationKey, + with: destinationIdentifier + ) + XCTAssertNoThrow(try relaunchedMigrator.migrate()) + XCTAssertNil(keystore.value(for: legacyIdentifier)) + XCTAssertEqual( + keystore.value(for: destinationIdentifier), + destinationKey + ) + XCTAssertNil( + keystore.value( + for: KeystoreMigrator.pendingCleanupIdentifier + ) + ) + XCTAssertNoThrow(try relaunchedMigrator.migrate()) + XCTAssertEqual( + try metadataChecksum(at: storeURL), + currentChecksum + ) + } + + func testPerformMigration_whenCurrentStoreCleanupJournalIsCorrupt_thenLaunchFailsClosed() throws { + let currentModel = try model(for: .version14) + try createStore(at: storeURL, model: currentModel) { context in + self.insertCurrentWallet(in: context, model: currentModel) + } + let corruptJournal = Data("not-json".utf8) + let keystore = FaultInjectingKeystore( + values: [ + KeystoreMigrator.pendingCleanupIdentifier: corruptJournal + ] + ) + let migrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: keystore, + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle + ) + + XCTAssertThrowsError(try migrator.migrate()) + XCTAssertEqual(try metadataChecksum(at: storeURL), currentChecksum) + XCTAssertEqual( + keystore.value(for: KeystoreMigrator.pendingCleanupIdentifier), + corruptJournal + ) + } + + func testPerformMigration_whenCurrentStoreCleanupJournalCannotBeRead_thenBlocksAndRetriesWithoutDeletingKeys() throws { + let currentModel = try model(for: .version14) + try createStore(at: storeURL, model: currentModel) { context in + self.insertCurrentWallet(in: context, model: currentModel) + } + let fixture = try prepareCleanupJournalFixture() + let journal = try XCTUnwrap( + fixture.keystore.value( + for: KeystoreMigrator.pendingCleanupIdentifier + ) + ) + fixture.keystore.failures.insert( + .fetch(KeystoreMigrator.pendingCleanupIdentifier) + ) + let migrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: fixture.keystore, + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle + ) + + XCTAssertThrowsError(try migrator.migrate()) { error in + XCTAssertEqual( + error as? FaultInjectingKeystore.Error, + .injected( + .fetch(KeystoreMigrator.pendingCleanupIdentifier) + ) + ) + } + assertCleanupFixtureIsFullyRetained( + fixture, + expectedJournal: journal + ) + + fixture.keystore.failures.remove( + .fetch(KeystoreMigrator.pendingCleanupIdentifier) + ) + XCTAssertNoThrow(try migrator.migrate()) + + XCTAssertNil( + fixture.keystore.value(for: fixture.legacyIdentifier) + ) + XCTAssertEqual( + fixture.keystore.value(for: fixture.destinationIdentifier), + fixture.destinationKey + ) + XCTAssertNil( + fixture.keystore.value( + for: KeystoreMigrator.pendingCleanupIdentifier + ) + ) + XCTAssertEqual(try metadataChecksum(at: storeURL), currentChecksum) + XCTAssertEqual( + try walletSnapshots(from: storeURL, model: currentModel).map(\.metaId), + ["current-wallet"] + ) + } + + func testPerformMigration_whenCurrentStoreDestinationKeyIsMissing_thenBlocksUntilKeyIsRestored() throws { + let currentModel = try model(for: .version14) + try createStore(at: storeURL, model: currentModel) { context in + self.insertCurrentWallet(in: context, model: currentModel) + } + let fixture = try prepareCleanupJournalFixture() + try fixture.keystore.deleteKey( + for: fixture.destinationIdentifier + ) + let journal = try XCTUnwrap( + fixture.keystore.value( + for: KeystoreMigrator.pendingCleanupIdentifier + ) + ) + let migrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: fixture.keystore, + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle + ) + + XCTAssertThrowsError(try migrator.migrate()) { error in + guard + let migrationError = error as? KeystoreMigratingError, + case let .cleanupDestinationKeyMissing(identifier) = migrationError + else { + return XCTFail("Unexpected error: \(error)") + } + + XCTAssertEqual(identifier, fixture.destinationIdentifier) + } + XCTAssertEqual( + fixture.keystore.value(for: fixture.legacyIdentifier), + fixture.legacyKey + ) + XCTAssertEqual( + fixture.keystore.value( + for: KeystoreMigrator.pendingCleanupIdentifier + ), + journal + ) + + try fixture.keystore.addKey( + fixture.destinationKey, + with: fixture.destinationIdentifier + ) + XCTAssertNoThrow(try migrator.migrate()) + + XCTAssertNil( + fixture.keystore.value(for: fixture.legacyIdentifier) + ) + XCTAssertEqual( + fixture.keystore.value(for: fixture.destinationIdentifier), + fixture.destinationKey + ) + XCTAssertNil( + fixture.keystore.value( + for: KeystoreMigrator.pendingCleanupIdentifier + ) + ) + } + + func testPerformMigration_whenCurrentStoreDestinationProofIsTampered_thenBlocksUntilJournalIsRestored() throws { + let currentModel = try model(for: .version14) + try createStore(at: storeURL, model: currentModel) { context in + self.insertCurrentWallet(in: context, model: currentModel) + } + let fixture = try prepareCleanupJournalFixture() + let originalJournal = try XCTUnwrap( + fixture.keystore.value( + for: KeystoreMigrator.pendingCleanupIdentifier + ) + ) + let tamperedJournal = try rewriteCleanupJournal( + in: fixture.keystore + ) { journal in + var proofs = try XCTUnwrap( + journal["destinationKeyProofs"] as? [[String: Any]] + ) + proofs[0]["sha256"] = Data( + repeating: 0xFF, + count: 32 + ).base64EncodedString() + journal["destinationKeyProofs"] = proofs + } + let migrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: fixture.keystore, + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle + ) + + XCTAssertThrowsError(try migrator.migrate()) { error in + guard + let migrationError = error as? KeystoreMigratingError, + case .cleanupDestinationKeyVerificationFailed = migrationError + else { + return XCTFail("Unexpected error: \(error)") + } + } + assertCleanupFixtureIsFullyRetained( + fixture, + expectedJournal: tamperedJournal + ) + + try fixture.keystore.updateKey( + originalJournal, + with: KeystoreMigrator.pendingCleanupIdentifier + ) + XCTAssertNoThrow(try migrator.migrate()) + + XCTAssertNil( + fixture.keystore.value(for: fixture.legacyIdentifier) + ) + XCTAssertEqual( + fixture.keystore.value(for: fixture.destinationIdentifier), + fixture.destinationKey + ) + XCTAssertNil( + fixture.keystore.value( + for: KeystoreMigrator.pendingCleanupIdentifier + ) + ) + } + + func testPerformMigration_whenSourceStoreHasLegacyCleanupJournal_thenRerunsWithExistingStagedKey() throws { + let legacyModel = try model(for: .version13) + try createStore(at: storeURL, model: legacyModel) { context in + try self.insertLegacyWallets(in: context, model: legacyModel) + } + let legacyIdentifier = try legacyDeletionIdentifier(0x91) + let destinationIdentifier = destinationMigrationIdentifier() + let legacyKey = Data([0x91, 0x92]) + let destinationKey = Data([0x93, 0x94]) + let legacyJournal = try legacyCleanupJournalData( + sourceVersion: .version13, + destinationVersion: .version14, + identifiersToRemove: [legacyIdentifier], + stagedIdentifiers: [destinationIdentifier] + ) + let keystore = FaultInjectingKeystore( + values: [ + legacyIdentifier: legacyKey, + destinationIdentifier: destinationKey, + KeystoreMigrator.pendingCleanupIdentifier: legacyJournal + ] + ) + let keystoreMigrator = KeystoreMigrator( + sourceVersion: .version13, + destinationVersion: .version14, + keystore: keystore + ) + keystoreMigrator.deleteKey(for: legacyIdentifier) + keystoreMigrator.save( + key: destinationKey, + for: destinationIdentifier + ) + var replacementAttempted = false + let migrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: keystore, + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle, + storeReplacer: { targetURL, sourceURL in + replacementAttempted = true + XCTAssertEqual( + keystore.value(for: legacyIdentifier), + legacyKey + ) + XCTAssertEqual( + keystore.value(for: destinationIdentifier), + destinationKey + ) + try NSPersistentStoreCoordinator.replaceStore( + at: targetURL, + withStoreAt: sourceURL + ) + }, + keystoreMigratorFactory: { _, _, _ in + keystoreMigrator + } + ) + + try migrator.performMigration() + + XCTAssertTrue(replacementAttempted) + XCTAssertEqual(try metadataChecksum(at: storeURL), currentChecksum) + XCTAssertNil(keystore.value(for: legacyIdentifier)) + XCTAssertEqual( + keystore.value(for: destinationIdentifier), + destinationKey + ) + XCTAssertNil( + keystore.value(for: KeystoreMigrator.pendingCleanupIdentifier) + ) + } + + func testMigrate_whenDestinationStoreHasLegacyCleanupJournal_thenClearsOnlyJournal() throws { + let currentModel = try model(for: .version14) + try createStore(at: storeURL, model: currentModel) { context in + self.insertCurrentWallet(in: context, model: currentModel) + } + let legacyIdentifier = try legacyDeletionIdentifier(0xB1) + let destinationIdentifier = destinationMigrationIdentifier() + let legacyKey = Data([0xB1, 0xB2]) + let destinationKey = Data([0xB3, 0xB4]) + let legacyJournal = try legacyCleanupJournalData( + sourceVersion: .version13, + destinationVersion: .version14, + identifiersToRemove: [legacyIdentifier], + stagedIdentifiers: [destinationIdentifier] + ) + let keystore = FaultInjectingKeystore( + values: [ + legacyIdentifier: legacyKey, + destinationIdentifier: destinationKey, + KeystoreMigrator.pendingCleanupIdentifier: legacyJournal + ] + ) + let migrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: keystore, + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle + ) + + XCTAssertNoThrow(try migrator.migrate()) + + XCTAssertEqual(try metadataChecksum(at: storeURL), currentChecksum) + XCTAssertEqual( + keystore.value(for: legacyIdentifier), + legacyKey + ) + XCTAssertEqual( + keystore.value(for: destinationIdentifier), + destinationKey + ) + XCTAssertNil( + keystore.value(for: KeystoreMigrator.pendingCleanupIdentifier) + ) + } + + func testCleanupRecovery_whenLegacyJournalContainsReservedIdentifier_thenFailsClosed() throws { + let legacyIdentifier = try legacyDeletionIdentifier(0xC1) + let stagedIdentifier = destinationMigrationIdentifier() + let legacyKey = Data([0xC1, 0xC2]) + let stagedKey = Data([0xC3, 0xC4]) + let legacyJournal = try legacyCleanupJournalData( + sourceVersion: .version1, + destinationVersion: .version2, + identifiersToRemove: [ + legacyIdentifier, + KeystoreMigrator.pendingCleanupIdentifier + ], + stagedIdentifiers: [stagedIdentifier] + ) + let keystore = FaultInjectingKeystore( + values: [ + legacyIdentifier: legacyKey, + stagedIdentifier: stagedKey, + KeystoreMigrator.pendingCleanupIdentifier: legacyJournal + ] + ) + + XCTAssertThrowsError( + try KeystoreMigrator.recoverPendingCleanup( + keystore: keystore, + currentVersion: .version1 + ) + ) { error in + guard + let migrationError = error as? KeystoreMigratingError, + case .invalidCleanupJournal = migrationError + else { + return XCTFail("Unexpected error: \(error)") + } + } + + XCTAssertEqual(keystore.value(for: legacyIdentifier), legacyKey) + XCTAssertEqual(keystore.value(for: stagedIdentifier), stagedKey) + XCTAssertEqual( + keystore.value(for: KeystoreMigrator.pendingCleanupIdentifier), + legacyJournal + ) + } + + func testCleanupRecovery_whenVerifiedJournalDoesNotBracketDatabaseVersion_thenFailsClosed() throws { + let legacyIdentifier = try legacyDeletionIdentifier(0xD1) + let destinationIdentifier = destinationMigrationIdentifier() + let legacyKey = Data([0xD1, 0xD2]) + let destinationKey = Data([0xD3, 0xD4]) + let keystore = FaultInjectingKeystore( + values: [legacyIdentifier: legacyKey] + ) + let migrator = KeystoreMigrator( + sourceVersion: .version1, + destinationVersion: .current, + keystore: keystore + ) + + while migrator.currentVersion != .current { + try migrator.switchVersion() + } + + migrator.deleteKey(for: legacyIdentifier) + migrator.save( + key: destinationKey, + for: destinationIdentifier + ) + try migrator.prepare() + let journal = try XCTUnwrap( + keystore.value(for: KeystoreMigrator.pendingCleanupIdentifier) + ) + + XCTAssertThrowsError( + try KeystoreMigrator.recoverPendingCleanup( + keystore: keystore, + currentVersion: .version2 + ) + ) { error in + guard + let migrationError = error as? KeystoreMigratingError, + case .invalidCleanupJournal = migrationError + else { + return XCTFail("Unexpected error: \(error)") + } + } + + XCTAssertEqual(keystore.value(for: legacyIdentifier), legacyKey) + XCTAssertEqual( + keystore.value(for: destinationIdentifier), + destinationKey + ) + XCTAssertEqual( + keystore.value(for: KeystoreMigrator.pendingCleanupIdentifier), + journal + ) + } + + func testCleanupRecovery_whenLegacyJournalDoesNotBracketDatabaseVersion_thenFailsClosed() throws { + let legacyIdentifier = try legacyDeletionIdentifier(0xE1) + let stagedIdentifier = destinationMigrationIdentifier() + let legacyKey = Data([0xE1, 0xE2]) + let stagedKey = Data([0xE3, 0xE4]) + let legacyJournal = try legacyCleanupJournalData( + sourceVersion: .version1, + destinationVersion: .current, + identifiersToRemove: [legacyIdentifier], + stagedIdentifiers: [stagedIdentifier] + ) + let keystore = FaultInjectingKeystore( + values: [ + legacyIdentifier: legacyKey, + stagedIdentifier: stagedKey, + KeystoreMigrator.pendingCleanupIdentifier: legacyJournal + ] + ) + + XCTAssertThrowsError( + try KeystoreMigrator.recoverPendingCleanup( + keystore: keystore, + currentVersion: .version2 + ) + ) { error in + guard + let migrationError = error as? KeystoreMigratingError, + case .invalidCleanupJournal = migrationError + else { + return XCTFail("Unexpected error: \(error)") + } + } + + XCTAssertEqual(keystore.value(for: legacyIdentifier), legacyKey) + XCTAssertEqual(keystore.value(for: stagedIdentifier), stagedKey) + XCTAssertEqual( + keystore.value(for: KeystoreMigrator.pendingCleanupIdentifier), + legacyJournal + ) + } + + func testMigrate_whenStoreDoesNotExist_thenReturnsWithoutReadingKeychain() { + let keystore = FaultInjectingKeystore() + keystore.failures.insert( + .fetch(KeystoreMigrator.pendingCleanupIdentifier) + ) + let migrator = UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: keystore, + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle + ) + + XCTAssertFalse(FileManager.default.fileExists(atPath: storeURL.path)) + XCTAssertNoThrow(try migrator.migrate()) + } + + func testKeystoreMigrator_whenLegacyKeyReadFails_thenErrorIsNotTreatedAsMissingKey() throws { + let legacyIdentifier = "legacy-key" + let legacyKey = Data([0x31, 0x32]) + let keystore = FaultInjectingKeystore( + values: [legacyIdentifier: legacyKey] + ) + keystore.failures.insert(.fetch(legacyIdentifier)) + let migrator = KeystoreMigrator( + sourceVersion: .version1, + destinationVersion: .version2, + keystore: keystore + ) + try migrator.switchVersion() + + XCTAssertThrowsError(try migrator.fetchKey(for: legacyIdentifier)) { error in + XCTAssertEqual( + error as? FaultInjectingKeystore.Error, + .injected(.fetch(legacyIdentifier)) + ) + } + XCTAssertEqual(keystore.value(for: legacyIdentifier), legacyKey) + } + + func testKeystoreMigrator_whenReplacementIdentifierContainsDifferentKey_thenRefusesToOverwriteIt() throws { + let replacementIdentifier = "replacement-key" + let existingKey = Data([0x41, 0x42]) + let attemptedKey = Data([0x43, 0x44]) + let keystore = FaultInjectingKeystore( + values: [replacementIdentifier: existingKey] + ) + let migrator = KeystoreMigrator( + sourceVersion: .version1, + destinationVersion: .version2, + keystore: keystore + ) + try migrator.switchVersion() + migrator.save(key: attemptedKey, for: replacementIdentifier) + + XCTAssertThrowsError(try migrator.prepare()) { error in + guard + let migrationError = error as? KeystoreMigratingError, + case let .stagedKeyConflict(identifier) = migrationError + else { + return XCTFail("Unexpected error: \(error)") + } + + XCTAssertEqual(identifier, replacementIdentifier) + } + XCTAssertEqual( + keystore.value(for: replacementIdentifier), + existingKey + ) + XCTAssertNil( + keystore.value(for: KeystoreMigrator.pendingCleanupIdentifier) + ) + } + + func testKeystoreMigrator_whenJournalDeletionFails_thenRecoveryCanRepeatWithoutDeletingNewKey() throws { + let legacyIdentifier = try legacyDeletionIdentifier(0x51) + let replacementIdentifier = destinationMigrationIdentifier() + let legacyKey = Data([0x51, 0x52]) + let replacementKey = Data([0x53, 0x54]) + let keystore = FaultInjectingKeystore( + values: [legacyIdentifier: legacyKey] + ) + let migrator = KeystoreMigrator( + sourceVersion: .version1, + destinationVersion: .version2, + keystore: keystore + ) + try migrator.switchVersion() + migrator.deleteKey(for: legacyIdentifier) + migrator.save(key: replacementKey, for: replacementIdentifier) + try migrator.prepare() + keystore.failures.insert( + .delete(KeystoreMigrator.pendingCleanupIdentifier) + ) + + XCTAssertThrowsError(try migrator.finalize()) { error in + XCTAssertEqual( + error as? FaultInjectingKeystore.Error, + .injected( + .delete(KeystoreMigrator.pendingCleanupIdentifier) + ) + ) + } + XCTAssertNil(keystore.value(for: legacyIdentifier)) + XCTAssertEqual( + keystore.value(for: replacementIdentifier), + replacementKey + ) + XCTAssertNotNil( + keystore.value(for: KeystoreMigrator.pendingCleanupIdentifier) + ) + + keystore.failures.remove( + .delete(KeystoreMigrator.pendingCleanupIdentifier) + ) + try KeystoreMigrator.recoverPendingCleanup( + keystore: keystore, + currentVersion: .version2 + ) + try KeystoreMigrator.recoverPendingCleanup( + keystore: keystore, + currentVersion: .version2 + ) + XCTAssertNil(keystore.value(for: legacyIdentifier)) + XCTAssertEqual( + keystore.value(for: replacementIdentifier), + replacementKey + ) + XCTAssertNil( + keystore.value(for: KeystoreMigrator.pendingCleanupIdentifier) + ) + } + + func testCleanupRecovery_whenDestinationKeyIsMissing_thenRetainsLegacyKeyAndJournal() throws { + let fixture = try prepareCleanupJournalFixture() + try fixture.keystore.deleteKey(for: fixture.destinationIdentifier) + let journal = try XCTUnwrap( + fixture.keystore.value( + for: KeystoreMigrator.pendingCleanupIdentifier + ) + ) + + XCTAssertThrowsError( + try KeystoreMigrator.recoverPendingCleanup( + keystore: fixture.keystore, + currentVersion: .version2 + ) + ) { error in + guard + let migrationError = error as? KeystoreMigratingError, + case let .cleanupDestinationKeyMissing(identifier) = migrationError + else { + return XCTFail("Unexpected error: \(error)") + } + + XCTAssertEqual(identifier, fixture.destinationIdentifier) + } + + XCTAssertEqual( + fixture.keystore.value(for: fixture.legacyIdentifier), + fixture.legacyKey + ) + XCTAssertNil( + fixture.keystore.value(for: fixture.destinationIdentifier) + ) + XCTAssertEqual( + fixture.keystore.value( + for: KeystoreMigrator.pendingCleanupIdentifier + ), + journal + ) + } + + func testCleanupRecovery_whenDestinationKeyHasDrifted_thenRetainsLegacyKeyAndJournal() throws { + let fixture = try prepareCleanupJournalFixture() + let driftedKey = Data([0xA1, 0xA2, 0xA3]) + try fixture.keystore.updateKey( + driftedKey, + with: fixture.destinationIdentifier + ) + let journal = try XCTUnwrap( + fixture.keystore.value( + for: KeystoreMigrator.pendingCleanupIdentifier + ) + ) + + XCTAssertThrowsError( + try KeystoreMigrator.recoverPendingCleanup( + keystore: fixture.keystore, + currentVersion: .version2 + ) + ) { error in + guard + let migrationError = error as? KeystoreMigratingError, + case let .cleanupDestinationKeyVerificationFailed(identifier) = migrationError + else { + return XCTFail("Unexpected error: \(error)") + } + + XCTAssertEqual(identifier, fixture.destinationIdentifier) + } + + XCTAssertEqual( + fixture.keystore.value(for: fixture.legacyIdentifier), + fixture.legacyKey + ) + XCTAssertEqual( + fixture.keystore.value(for: fixture.destinationIdentifier), + driftedKey + ) + XCTAssertEqual( + fixture.keystore.value( + for: KeystoreMigrator.pendingCleanupIdentifier + ), + journal + ) + } + + func testCleanupRecovery_whenDestinationDigestIsTampered_thenRetainsAllKeysAndJournal() throws { + let fixture = try prepareCleanupJournalFixture() + let tamperedJournal = try rewriteCleanupJournal( + in: fixture.keystore + ) { journal in + var proofs = try XCTUnwrap( + journal["destinationKeyProofs"] as? [[String: Any]] + ) + XCTAssertEqual(proofs.count, 1) + proofs[0]["sha256"] = Data( + repeating: 0xFF, + count: 32 + ).base64EncodedString() + journal["destinationKeyProofs"] = proofs + } + + XCTAssertThrowsError( + try KeystoreMigrator.recoverPendingCleanup( + keystore: fixture.keystore, + currentVersion: .version2 + ) + ) { error in + guard + let migrationError = error as? KeystoreMigratingError, + case let .cleanupDestinationKeyVerificationFailed(identifier) = migrationError + else { + return XCTFail("Unexpected error: \(error)") + } + + XCTAssertEqual(identifier, fixture.destinationIdentifier) + } + + assertCleanupFixtureIsFullyRetained( + fixture, + expectedJournal: tamperedJournal + ) + } + + func testCleanupRecovery_whenDeletionListContainsReservedJournalIdentifier_thenFailsClosed() throws { + let fixture = try prepareCleanupJournalFixture() + let tamperedJournal = try rewriteCleanupJournal( + in: fixture.keystore + ) { journal in + journal["identifiersToRemove"] = [ + fixture.legacyIdentifier, + KeystoreMigrator.pendingCleanupIdentifier + ] + } + + XCTAssertThrowsError( + try KeystoreMigrator.recoverPendingCleanup( + keystore: fixture.keystore, + currentVersion: .version2 + ) + ) { error in + guard + let migrationError = error as? KeystoreMigratingError, + case .invalidCleanupJournal = migrationError + else { + return XCTFail("Unexpected error: \(error)") + } + } + + assertCleanupFixtureIsFullyRetained( + fixture, + expectedJournal: tamperedJournal + ) + } + + func testCleanupRecovery_whenDeletionListContainsDestinationIdentifier_thenFailsClosed() throws { + let fixture = try prepareCleanupJournalFixture() + let tamperedJournal = try rewriteCleanupJournal( + in: fixture.keystore + ) { journal in + journal["identifiersToRemove"] = [ + fixture.legacyIdentifier, + fixture.destinationIdentifier + ] + } + + XCTAssertThrowsError( + try KeystoreMigrator.recoverPendingCleanup( + keystore: fixture.keystore, + currentVersion: .version2 + ) + ) { error in + guard + let migrationError = error as? KeystoreMigratingError, + case .invalidCleanupJournal = migrationError + else { + return XCTFail("Unexpected error: \(error)") + } + } + + assertCleanupFixtureIsFullyRetained( + fixture, + expectedJournal: tamperedJournal + ) + } + + func testCleanupRecovery_whenDeletionListContainsPincode_thenFailsClosedWithoutDeletingIt() throws { + let fixture = try prepareCleanupJournalFixture() + let pincode = Data("246810".utf8) + try fixture.keystore.addKey( + pincode, + with: fearless.KeystoreTag.pincode.rawValue + ) + let tamperedJournal = try rewriteCleanupJournal( + in: fixture.keystore + ) { journal in + journal["identifiersToRemove"] = [ + fixture.legacyIdentifier, + fearless.KeystoreTag.pincode.rawValue + ] + } + + XCTAssertThrowsError( + try KeystoreMigrator.recoverPendingCleanup( + keystore: fixture.keystore, + currentVersion: .version2 + ) + ) { error in + guard + let migrationError = error as? KeystoreMigratingError, + case .invalidCleanupJournal = migrationError + else { + return XCTFail("Unexpected error: \(error)") + } + } + + assertCleanupFixtureIsFullyRetained( + fixture, + expectedJournal: tamperedJournal + ) + XCTAssertEqual( + fixture.keystore.value(for: fearless.KeystoreTag.pincode.rawValue), + pincode + ) + } + + func testCleanupRecovery_whenDeletionListContainsUnrelatedAppKey_thenFailsClosedWithoutDeletingIt() throws { + let fixture = try prepareCleanupJournalFixture() + let unrelatedIdentifier = "io.fearless.oauth.refresh-token" + let unrelatedKey = Data([0xF1, 0xF2, 0xF3]) + try fixture.keystore.addKey( + unrelatedKey, + with: unrelatedIdentifier + ) + let tamperedJournal = try rewriteCleanupJournal( + in: fixture.keystore + ) { journal in + journal["identifiersToRemove"] = [ + fixture.legacyIdentifier, + unrelatedIdentifier + ] + } + + XCTAssertThrowsError( + try KeystoreMigrator.recoverPendingCleanup( + keystore: fixture.keystore, + currentVersion: .version2 + ) + ) { error in + guard + let migrationError = error as? KeystoreMigratingError, + case .invalidCleanupJournal = migrationError + else { + return XCTFail("Unexpected error: \(error)") + } + } + + assertCleanupFixtureIsFullyRetained( + fixture, + expectedJournal: tamperedJournal + ) + XCTAssertEqual( + fixture.keystore.value(for: unrelatedIdentifier), + unrelatedKey + ) + } + + func testCleanupRecovery_whenRollbackStagesPincodeWithMatchingProof_thenFailsClosedWithoutDeletingIt() throws { + let fixture = try prepareCleanupJournalFixture() + let pincode = fixture.destinationKey + try fixture.keystore.addKey( + pincode, + with: fearless.KeystoreTag.pincode.rawValue + ) + let tamperedJournal = try rewriteCleanupJournal( + in: fixture.keystore + ) { journal in + var proofs = try XCTUnwrap( + journal["destinationKeyProofs"] as? [[String: Any]] + ) + var forgedPincodeProof = try XCTUnwrap(proofs.first) + forgedPincodeProof["identifier"] = fearless.KeystoreTag.pincode.rawValue + proofs.append(forgedPincodeProof) + journal["destinationKeyProofs"] = proofs + journal["stagedIdentifiers"] = [ + fearless.KeystoreTag.pincode.rawValue + ] + } + + XCTAssertThrowsError( + try KeystoreMigrator.recoverPendingCleanup( + keystore: fixture.keystore, + currentVersion: .version1 + ) + ) { error in + guard + let migrationError = error as? KeystoreMigratingError, + case .invalidCleanupJournal = migrationError + else { + return XCTFail("Unexpected error: \(error)") + } + } + + assertCleanupFixtureIsFullyRetained( + fixture, + expectedJournal: tamperedJournal + ) + XCTAssertEqual( + fixture.keystore.value(for: fearless.KeystoreTag.pincode.rawValue), + pincode + ) + } + + func testMetaAccountMapper_whenMigratedRecordHasOnlyTonFields_thenThrowsRecoverableError() throws { + let legacyModel = try model(for: .version13) + try createStore(at: storeURL, model: legacyModel) { context in + try self.insertLegacyWallets(in: context, model: legacyModel) + } + try makeMigrator().performMigration() + + let currentModel = try model(for: .version14) + try useStore(at: storeURL, model: currentModel) { context in + let request = NSFetchRequest(entityName: "CDMetaAccount") + request.predicate = NSPredicate(format: "metaId == %@", "ton-only-wallet") + let object = try XCTUnwrap(context.fetch(request).first) + let entity = try XCTUnwrap( + object as? fearless.CDMetaAccount, + "The active compatibility model must materialize the app's CDMetaAccount class" + ) + + XCTAssertThrowsError(try MetaAccountMapper().transform(entity: entity)) { error in + guard let mapperError = error as? MetaAccountMapperError else { + return XCTFail("Unexpected error: \(error)") + } + + guard case .unsupportedWalletRecord = mapperError else { + return XCTFail("Expected unsupportedWalletRecord, got \(mapperError)") + } + } + } + } + + func testMetaAccountMapper_whenEvmAccount_thenWritesCanonicalIdentifierAndReadsCanonicalAndLegacyAliases() throws { + let currentModel = try model(for: .version14) + + try createStore(at: storeURL, model: currentModel) { context in + let chainAccount = ChainAccountModel( + chainId: "ethereum-chain", + accountId: Data(repeating: 0x61, count: 20), + publicKey: Data(repeating: 0x62, count: 33), + cryptoType: 2, + ethereumBased: true + ) + let walletModel = AccountGenerator.generateMetaAccount(with: [chainAccount]) + let walletEntity = fearless.CDMetaAccount(context: context) + walletEntity.isSelected = true + walletEntity.order = 1 + walletEntity.setValue(false, forKey: "zeroBalanceAssetsHidden") + let mapper = MetaAccountMapper() + + try mapper.populate( + entity: walletEntity, + from: walletModel, + using: context + ) + + let chainEntity = try XCTUnwrap( + walletEntity.chainAccounts?.allObjects.first as? fearless.CDChainAccount + ) + XCTAssertEqual( + chainEntity.value(forKey: "ecosystem") as? String, + UniversalWalletEcosystem.evm.rawValue + ) + + for ecosystem in [ + UniversalWalletEcosystem.evm.rawValue, + "ethereum", + "ethereumBased" + ] { + chainEntity.ethereumBased = false + chainEntity.setValue(ecosystem, forKey: "ecosystem") + + let transformed = try mapper.transform(entity: walletEntity) + let transformedAccount = try XCTUnwrap( + transformed.chainAccounts.first { $0.chainId == chainAccount.chainId } + ) + + XCTAssertTrue( + transformedAccount.ethereumBased, + "\(ecosystem) must continue to resolve as an EVM account" + ) + } + } + } + + private func makeMigrator( + storeURL: URL? = nil + ) -> UserStorageMigrator { + UserStorageMigrator( + targetVersion: .version14, + storeURL: storeURL ?? self.storeURL, + modelDirectory: UserStorageParams.modelDirectory, + keystore: InMemoryKeychain(), + settings: InMemorySettingsManager(), + fileManager: .default, + modelBundle: appBundle + ) + } + + private func preferenceFixtureValues() -> [String: [String]] { + [ + "assetFilterOptions": ["filter-option"], + "assetKeysOrder": ["asset-order"], + "favouriteChainIds": ["favourite-chain"], + "unusedChainIds": ["unused-chain"] + ] + } + + private func createPreferenceFixture( + at url: URL, + sourceVersion: UserStorageVersion, + values: [String: [String]] + ) throws { + let sourceModel = try model(for: sourceVersion) + try createStore(at: url, model: sourceModel) { context in + self.insertCurrentWallet( + in: context, + model: sourceModel + ) + let request = NSFetchRequest( + entityName: "CDMetaAccount" + ) + let wallet = try XCTUnwrap(context.fetch(request).first) + for (key, value) in values { + wallet.setValue(value as NSArray, forKey: key) + } + } + } + + private func migratedPreferenceValues( + at url: URL + ) throws -> [String: [String]] { + var values = [String: [String]]() + try useStore( + at: url, + model: model(for: .version14) + ) { context in + let request = NSFetchRequest( + entityName: "CDMetaAccount" + ) + let wallet = try XCTUnwrap(context.fetch(request).first) + XCTAssertEqual( + wallet.value(forKey: "metaId") as? String, + "current-wallet" + ) + XCTAssertEqual( + wallet.value(forKey: "substratePublicKey") as? Data, + Data(repeating: 0x52, count: 32) + ) + + for key in self.preferenceFixtureValues().keys { + values[key] = + wallet.value(forKey: key) as? [String] + } + } + + return values + } + + private typealias CleanupJournalFixture = ( + keystore: FaultInjectingKeystore, + legacyIdentifier: String, + destinationIdentifier: String, + legacyKey: Data, + destinationKey: Data + ) + + private func prepareCleanupJournalFixture() throws -> CleanupJournalFixture { + let legacyIdentifier = try legacyDeletionIdentifier(0x71) + let destinationIdentifier = destinationMigrationIdentifier() + let legacyKey = Data([0x71, 0x72, 0x73]) + let destinationKey = Data([0x81, 0x82, 0x83]) + let keystore = FaultInjectingKeystore( + values: [legacyIdentifier: legacyKey] + ) + let migrator = KeystoreMigrator( + sourceVersion: .version1, + destinationVersion: .version2, + keystore: keystore + ) + try migrator.switchVersion() + migrator.deleteKey(for: legacyIdentifier) + migrator.save(key: destinationKey, for: destinationIdentifier) + try migrator.prepare() + + return ( + keystore: keystore, + legacyIdentifier: legacyIdentifier, + destinationIdentifier: destinationIdentifier, + legacyKey: legacyKey, + destinationKey: destinationKey + ) + } + + private func rewriteCleanupJournal( + in keystore: FaultInjectingKeystore, + mutation: (inout [String: Any]) throws -> Void + ) throws -> Data { + let originalData = try XCTUnwrap( + keystore.value(for: KeystoreMigrator.pendingCleanupIdentifier) + ) + var journal = try XCTUnwrap( + JSONSerialization.jsonObject(with: originalData) as? [String: Any] + ) + try mutation(&journal) + let rewrittenData = try JSONSerialization.data( + withJSONObject: journal, + options: [.sortedKeys] + ) + try keystore.updateKey( + rewrittenData, + with: KeystoreMigrator.pendingCleanupIdentifier + ) + + return rewrittenData + } + + private func legacyDeletionIdentifier( + _ accountByte: UInt8 + ) throws -> String { + let address = try SS58AddressFactory().address( + fromAccountId: Data(repeating: accountByte, count: 32), + type: 42 + ) + + return fearless.KeystoreTag.secretKeyTagForAddress(address) + } + + private func destinationMigrationIdentifier() -> String { + fearless.KeystoreTagV2.substrateSecretKeyTagForMetaId( + UUID().uuidString + ) + } + + private func legacyCleanupJournalData( + sourceVersion: UserStorageVersion, + destinationVersion: UserStorageVersion, + identifiersToRemove: [String], + stagedIdentifiers: [String] + ) throws -> Data { + try JSONSerialization.data( + withJSONObject: [ + "sourceVersion": sourceVersion.rawValue, + "destinationVersion": destinationVersion.rawValue, + "identifiersToRemove": identifiersToRemove, + "stagedIdentifiers": stagedIdentifiers + ], + options: [.sortedKeys] + ) + } + + private func assertCleanupFixtureIsFullyRetained( + _ fixture: CleanupJournalFixture, + expectedJournal: Data, + file: StaticString = #filePath, + line: UInt = #line + ) { + XCTAssertEqual( + fixture.keystore.value(for: fixture.legacyIdentifier), + fixture.legacyKey, + file: file, + line: line + ) + XCTAssertEqual( + fixture.keystore.value(for: fixture.destinationIdentifier), + fixture.destinationKey, + file: file, + line: line + ) + XCTAssertEqual( + fixture.keystore.value( + for: KeystoreMigrator.pendingCleanupIdentifier + ), + expectedJournal, + file: file, + line: line + ) + } + + private func directCompatibilityModelURL( + for version: UserStorageVersion + ) throws -> URL { + try XCTUnwrap( + appBundle.url( + forResource: version.rawValue, + withExtension: "mom" + ), + "\(version.rawValue).mom must be copied into the application bundle root" + ) + } + + private func model(for version: UserStorageVersion) throws -> NSManagedObjectModel { + let url = try XCTUnwrap( + version.modelURL( + in: appBundle, + legacyModelDirectory: UserStorageParams.modelDirectory + ), + "Missing model resource for \(version.rawValue)" + ) + + return try loadModel(at: url) + } + + private func loadModel(at url: URL) throws -> NSManagedObjectModel { + try XCTUnwrap( + NSManagedObjectModel(contentsOf: url), + "Unable to load Core Data model at \(url.path)" + ) + } + + private func versionHashes(for model: NSManagedObjectModel) -> [String: String] { + model.entityVersionHashesByName.mapValues { $0.base64EncodedString() } + } + + private func storeChecksum( + for model: NSManagedObjectModel, + named name: String + ) throws -> String { + let url = testDirectory.appendingPathComponent(name) + try createStore(at: url, model: model) { _ in } + return try metadataChecksum(at: url) + } + + private func metadataChecksum(at url: URL) throws -> String { + let metadata = try NSPersistentStoreCoordinator.metadataForPersistentStore( + ofType: NSSQLiteStoreType, + at: url, + options: nil + ) + + return try XCTUnwrap( + metadata["NSStoreModelVersionChecksumKey"] as? String, + "Store metadata is missing its model checksum" + ) + } + + private func createStore( + at url: URL, + model: NSManagedObjectModel, + populate: (NSManagedObjectContext) throws -> Void + ) throws { + let coordinator = NSPersistentStoreCoordinator(managedObjectModel: model) + let store = try coordinator.addPersistentStore( + ofType: NSSQLiteStoreType, + configurationName: nil, + at: url, + options: [ + NSSQLitePragmasOption: ["journal_mode": "WAL"], + NSMigratePersistentStoresAutomaticallyOption: false, + NSInferMappingModelAutomaticallyOption: false + ] + ) + defer { + try? coordinator.remove(store) + } + + let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) + context.persistentStoreCoordinator = coordinator + + var populationError: Error? + context.performAndWait { + do { + try populate(context) + if context.hasChanges { + try context.save() + } + } catch { + populationError = error + } + } + + if let populationError { + throw populationError + } + } + + private func useStore( + at url: URL, + model: NSManagedObjectModel, + body: (NSManagedObjectContext) throws -> Void + ) throws { + let coordinator = NSPersistentStoreCoordinator(managedObjectModel: model) + let store = try coordinator.addPersistentStore( + ofType: NSSQLiteStoreType, + configurationName: nil, + at: url, + options: [ + NSMigratePersistentStoresAutomaticallyOption: false, + NSInferMappingModelAutomaticallyOption: false + ] + ) + defer { + try? coordinator.remove(store) + } + + let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) + context.persistentStoreCoordinator = coordinator + + var bodyError: Error? + context.performAndWait { + do { + try body(context) + } catch { + bodyError = error + } + } + + if let bodyError { + throw bodyError + } + } + + private func insertVersion2WalletFixture( + in context: NSManagedObjectContext, + model: NSManagedObjectModel + ) throws { + let walletEntity = try XCTUnwrap( + model.entitiesByName["CDMetaAccount"] + ) + let firstWallet = NSManagedObject( + entity: walletEntity, + insertInto: context + ) + firstWallet.setValue("v2-wallet-alpha", forKey: "metaId") + firstWallet.setValue("Version 2 Alpha", forKey: "name") + firstWallet.setValue(true, forKey: "isSelected") + firstWallet.setValue(Int32(1), forKey: "order") + firstWallet.setValue( + hex(byte: 0x11, count: 32), + forKey: "substrateAccountId" + ) + firstWallet.setValue( + Data(repeating: 0x12, count: 32), + forKey: "substratePublicKey" + ) + firstWallet.setValue( + Int16(1), + forKey: "substrateCryptoType" + ) + + let secondWallet = NSManagedObject( + entity: walletEntity, + insertInto: context + ) + secondWallet.setValue("v2-wallet-beta", forKey: "metaId") + secondWallet.setValue("Version 2 Beta", forKey: "name") + secondWallet.setValue(false, forKey: "isSelected") + secondWallet.setValue(Int32(2), forKey: "order") + secondWallet.setValue( + hex(byte: 0x21, count: 32), + forKey: "substrateAccountId" + ) + secondWallet.setValue( + Data(repeating: 0x22, count: 32), + forKey: "substratePublicKey" + ) + secondWallet.setValue( + Int16(0), + forKey: "substrateCryptoType" + ) + + try insertVersion2ChainAccount( + chainId: "kusama", + accountId: hex(byte: 0x31, count: 32), + publicKey: Data(repeating: 0x32, count: 32), + cryptoType: 1, + wallet: firstWallet, + in: context, + model: model + ) + try insertVersion2ChainAccount( + chainId: "polkadot", + accountId: hex(byte: 0x41, count: 32), + publicKey: Data(repeating: 0x42, count: 32), + cryptoType: 0, + wallet: firstWallet, + in: context, + model: model + ) + try insertVersion2ChainAccount( + chainId: "westend", + accountId: hex(byte: 0x51, count: 32), + publicKey: Data(repeating: 0x52, count: 32), + cryptoType: 1, + wallet: secondWallet, + in: context, + model: model + ) + } + + private func insertVersion2ChainAccount( + chainId: String, + accountId: String, + publicKey: Data, + cryptoType: Int16, + wallet: NSManagedObject, + in context: NSManagedObjectContext, + model: NSManagedObjectModel + ) throws { + let entity = try XCTUnwrap( + model.entitiesByName["CDChainAccount"] + ) + let chainAccount = NSManagedObject( + entity: entity, + insertInto: context + ) + chainAccount.setValue(chainId, forKey: "chainId") + chainAccount.setValue(accountId, forKey: "accountId") + chainAccount.setValue(publicKey, forKey: "publicKey") + chainAccount.setValue(cryptoType, forKey: "cryptoType") + chainAccount.setValue(wallet, forKey: "metaAccount") + } + + private func version2WalletFixtureSnapshots() -> [WalletSnapshot] { + [ + WalletSnapshot( + metaId: "v2-wallet-alpha", + name: "Version 2 Alpha", + substrateAccountId: hex(byte: 0x11, count: 32), + substratePublicKey: Data(repeating: 0x12, count: 32), + tonAddress: nil, + tonPublicKey: nil, + tonContractVersion: nil, + assetsVisibility: [:], + chainAccounts: [ + ChainSnapshot( + chainId: "kusama", + accountId: hex(byte: 0x31, count: 32), + publicKey: Data(repeating: 0x32, count: 32), + cryptoType: 1, + ecosystem: nil + ), + ChainSnapshot( + chainId: "polkadot", + accountId: hex(byte: 0x41, count: 32), + publicKey: Data(repeating: 0x42, count: 32), + cryptoType: 0, + ecosystem: nil + ) + ] + ), + WalletSnapshot( + metaId: "v2-wallet-beta", + name: "Version 2 Beta", + substrateAccountId: hex(byte: 0x21, count: 32), + substratePublicKey: Data(repeating: 0x22, count: 32), + tonAddress: nil, + tonPublicKey: nil, + tonContractVersion: nil, + assetsVisibility: [:], + chainAccounts: [ + ChainSnapshot( + chainId: "westend", + accountId: hex(byte: 0x51, count: 32), + publicKey: Data(repeating: 0x52, count: 32), + cryptoType: 1, + ecosystem: nil + ) + ] + ) + ] + } + + private func insertLegacyWallets( + in context: NSManagedObjectContext, + model: NSManagedObjectModel + ) throws { + let regularWallet = try insertWallet( + metaId: "regular-wallet", + name: "Regular Wallet", + in: context, + model: model + ) + regularWallet.setValue(hex(byte: 0x11, count: 32), forKey: "substrateAccountId") + regularWallet.setValue(Data(repeating: 0x12, count: 32), forKey: "substratePublicKey") + regularWallet.setValue(Int16(1), forKey: "substrateCryptoType") + regularWallet.setValue(hex(byte: 0x15, count: 20), forKey: "ethereumAddress") + regularWallet.setValue(Data(repeating: 0x16, count: 33), forKey: "ethereumPublicKey") + regularWallet.setValue(Data(repeating: 0x13, count: 36), forKey: "tonAddress") + regularWallet.setValue(Data(repeating: 0x14, count: 32), forKey: "tonPublicKey") + regularWallet.setValue("v4R2", forKey: "tonContractVersion") + + try insertAssetVisibility( + assetId: "polkadot-0", + hidden: false, + wallet: regularWallet, + in: context, + model: model + ) + try insertAssetVisibility( + assetId: "kusama-0", + hidden: true, + wallet: regularWallet, + in: context, + model: model + ) + try insertChainAccount( + chainId: "substrate-chain", + accountId: hex(byte: 0x21, count: 32), + publicKey: Data(repeating: 0x22, count: 32), + cryptoType: 1, + ecosystem: "substrate", + wallet: regularWallet, + in: context, + model: model + ) + try insertChainAccount( + chainId: "ethereum-chain", + accountId: hex(byte: 0x31, count: 20), + publicKey: Data(repeating: 0x32, count: 33), + cryptoType: 2, + ecosystem: UniversalWalletEcosystem.evm.rawValue, + wallet: regularWallet, + in: context, + model: model + ) + + let tonOnlyWallet = try insertWallet( + metaId: "ton-only-wallet", + name: "TON Only Wallet", + in: context, + model: model + ) + tonOnlyWallet.setValue(Data(repeating: 0x41, count: 36), forKey: "tonAddress") + tonOnlyWallet.setValue(Data(repeating: 0x42, count: 32), forKey: "tonPublicKey") + tonOnlyWallet.setValue("v5R1", forKey: "tonContractVersion") + } + + @discardableResult + private func insertWallet( + metaId: String, + name: String, + in context: NSManagedObjectContext, + model: NSManagedObjectModel + ) throws -> NSManagedObject { + let entity = try XCTUnwrap(model.entitiesByName["CDMetaAccount"]) + let wallet = NSManagedObject(entity: entity, insertInto: context) + wallet.setValue(metaId, forKey: "metaId") + wallet.setValue(name, forKey: "name") + wallet.setValue(metaId == "regular-wallet", forKey: "isSelected") + wallet.setValue(Int32(metaId == "regular-wallet" ? 1 : 2), forKey: "order") + wallet.setValue(false, forKey: "canExportEthereumMnemonic") + wallet.setValue(false, forKey: "hasBackup") + wallet.setValue(false, forKey: "zeroBalanceAssetsHidden") + wallet.setValue(NSArray(), forKey: "favouriteChainIds") + return wallet + } + + private func insertCurrentWallet( + in context: NSManagedObjectContext, + model: NSManagedObjectModel + ) { + guard let entity = model.entitiesByName["CDMetaAccount"] else { + return XCTFail("Current model is missing CDMetaAccount") + } + + let wallet = NSManagedObject(entity: entity, insertInto: context) + wallet.setValue("current-wallet", forKey: "metaId") + wallet.setValue("Current Wallet", forKey: "name") + wallet.setValue(false, forKey: "isSelected") + wallet.setValue(Int32(1), forKey: "order") + wallet.setValue(false, forKey: "canExportEthereumMnemonic") + wallet.setValue(false, forKey: "hasBackup") + wallet.setValue(false, forKey: "zeroBalanceAssetsHidden") + wallet.setValue(NSArray(), forKey: "favouriteChainIds") + wallet.setValue(hex(byte: 0x51, count: 32), forKey: "substrateAccountId") + wallet.setValue(Data(repeating: 0x52, count: 32), forKey: "substratePublicKey") + wallet.setValue(Int16(1), forKey: "substrateCryptoType") + } + + @discardableResult + private func insertHistoricalWallet( + metaId: String, + name: String, + in context: NSManagedObjectContext, + model: NSManagedObjectModel + ) throws -> NSManagedObject { + let entity = try XCTUnwrap( + model.entitiesByName["CDMetaAccount"] + ) + let wallet = NSManagedObject( + entity: entity, + insertInto: context + ) + wallet.setValue(metaId, forKey: "metaId") + wallet.setValue(name, forKey: "name") + wallet.setValue(false, forKey: "isSelected") + wallet.setValue(Int32(1), forKey: "order") + wallet.setValue( + hex(byte: 0x71, count: 32), + forKey: "substrateAccountId" + ) + wallet.setValue( + Data(repeating: 0x72, count: 32), + forKey: "substratePublicKey" + ) + wallet.setValue( + Int16(1), + forKey: "substrateCryptoType" + ) + + if entity.attributesByName[ + "canExportEthereumMnemonic" + ] != nil { + wallet.setValue( + false, + forKey: "canExportEthereumMnemonic" + ) + } + if entity.attributesByName["hasBackup"] != nil { + wallet.setValue(false, forKey: "hasBackup") + } + if entity.attributesByName[ + "zeroBalanceAssetsHidden" + ] != nil { + wallet.setValue( + false, + forKey: "zeroBalanceAssetsHidden" + ) + } + + return wallet + } + + private func insertAssetVisibility( + assetId: String, + hidden: Bool, + wallet: NSManagedObject, + in context: NSManagedObjectContext, + model: NSManagedObjectModel + ) throws { + let entity = try XCTUnwrap(model.entitiesByName["CDAssetVisibility"]) + let visibility = NSManagedObject(entity: entity, insertInto: context) + visibility.setValue(assetId, forKey: "assetId") + visibility.setValue(hidden, forKey: "hidden") + visibility.setValue(wallet, forKey: "wallet") + } + + private func insertChainAccount( + chainId: String, + accountId: String, + publicKey: Data, + cryptoType: Int16, + ecosystem: String, + wallet: NSManagedObject, + in context: NSManagedObjectContext, + model: NSManagedObjectModel + ) throws { + let entity = try XCTUnwrap(model.entitiesByName["CDChainAccount"]) + let chainAccount = NSManagedObject(entity: entity, insertInto: context) + chainAccount.setValue(chainId, forKey: "chainId") + chainAccount.setValue(accountId, forKey: "accountId") + chainAccount.setValue(publicKey, forKey: "publicKey") + chainAccount.setValue(cryptoType, forKey: "cryptoType") + chainAccount.setValue(ecosystem, forKey: "ecosystem") + chainAccount.setValue(wallet, forKey: "metaAccount") + } + + private func insertPublicChainAccount( + wallet: NSManagedObject, + in context: NSManagedObjectContext, + model: NSManagedObjectModel + ) throws { + let entity = try XCTUnwrap(model.entitiesByName["CDChainAccount"]) + let chainAccount = NSManagedObject(entity: entity, insertInto: context) + chainAccount.setValue("ethereum-mainnet", forKey: "chainId") + chainAccount.setValue(hex(byte: 0x61, count: 20), forKey: "accountId") + chainAccount.setValue(Data(repeating: 0x62, count: 33), forKey: "publicKey") + chainAccount.setValue(Int16(2), forKey: "cryptoType") + chainAccount.setValue(true, forKey: "ethereumBased") + chainAccount.setValue(wallet, forKey: "metaAccount") + } + + private func walletSnapshots( + from url: URL, + model: NSManagedObjectModel + ) throws -> [WalletSnapshot] { + var snapshots: [WalletSnapshot] = [] + + try useStore(at: url, model: model) { context in + let request = NSFetchRequest(entityName: "CDMetaAccount") + let wallets = try context.fetch(request) + snapshots = wallets.map { wallet in + let visibilityObjects = + (wallet.value(forKey: "assetsVisibility") as? NSSet)? + .allObjects as? [NSManagedObject] ?? [] + let visibility: [String: Bool] = Dictionary( + uniqueKeysWithValues: visibilityObjects.compactMap { object in + guard let assetId = object.value(forKey: "assetId") as? String else { + return nil + } + + let hidden = (object.value(forKey: "hidden") as? NSNumber)?.boolValue ?? false + return (assetId, hidden) + } + ) + + let chainObjects = + (wallet.value(forKey: "chainAccounts") as? NSSet)? + .allObjects as? [NSManagedObject] ?? [] + let chains = chainObjects.compactMap { object -> ChainSnapshot? in + guard + let chainId = object.value(forKey: "chainId") as? String, + let accountId = object.value(forKey: "accountId") as? String, + let publicKey = object.value(forKey: "publicKey") as? Data + else { + return nil + } + + let cryptoType = (object.value(forKey: "cryptoType") as? NSNumber)?.int16Value ?? 0 + return ChainSnapshot( + chainId: chainId, + accountId: accountId, + publicKey: publicKey, + cryptoType: cryptoType, + ecosystem: object.value(forKey: "ecosystem") as? String + ) + }.sorted { $0.chainId < $1.chainId } + + return WalletSnapshot( + metaId: wallet.value(forKey: "metaId") as? String ?? "", + name: wallet.value(forKey: "name") as? String ?? "", + substrateAccountId: wallet.value(forKey: "substrateAccountId") as? String, + substratePublicKey: wallet.value(forKey: "substratePublicKey") as? Data, + tonAddress: wallet.value(forKey: "tonAddress") as? Data, + tonPublicKey: wallet.value(forKey: "tonPublicKey") as? Data, + tonContractVersion: wallet.value(forKey: "tonContractVersion") as? String, + assetsVisibility: visibility, + chainAccounts: chains + ) + }.sorted { $0.metaId < $1.metaId } + } + + return snapshots + } + + private func makeUnknownModel() -> NSManagedObjectModel { + let model = NSManagedObjectModel() + let entity = NSEntityDescription() + entity.name = "UnknownWallet" + entity.managedObjectClassName = NSStringFromClass(NSManagedObject.self) + + let identifier = NSAttributeDescription() + identifier.name = "identifier" + identifier.attributeType = .stringAttributeType + identifier.isOptional = true + entity.properties = [identifier] + model.entities = [entity] + + return model + } + + private func storeFamilySnapshot(at url: URL) throws -> [String: Data] { + let fileNames = try FileManager.default.contentsOfDirectory( + atPath: url.deletingLastPathComponent().path + ).filter { $0.hasPrefix(url.lastPathComponent) } + + return try Dictionary( + uniqueKeysWithValues: fileNames.map { fileName in + let fileURL = url.deletingLastPathComponent().appendingPathComponent(fileName) + return (fileName, try Data(contentsOf: fileURL)) + } + ) + } + + private func durableStoreSnapshot(at url: URL) throws -> [String: Data] { + try storeFamilySnapshot(at: url).filter { fileName, _ in + !fileName.hasSuffix("-shm") + } + } + + private func makeSyntheticScenarioDirectory( + _ name: String + ) throws -> URL { + let url = testDirectory + .appendingPathComponent(name, isDirectory: true) + try FileManager.default.createDirectory( + at: url, + withIntermediateDirectories: false + ) + return url + } + + private func syntheticFamilyURL( + _ storeURL: URL, + suffix: String + ) -> URL { + URL(fileURLWithPath: storeURL.path + suffix) + } + + private func writeSyntheticStoreFamily( + at url: URL, + seed: UInt8 + ) throws { + for (index, suffix) in + CrashConsistentStoreReplacer.storeFamilySuffixes + .enumerated() { + try Data( + repeating: seed &+ UInt8(index), + count: 31 + index * 7 + ).write( + to: syntheticFamilyURL(url, suffix: suffix), + options: .atomic + ) + } + } + + private func syntheticStoreFamilySnapshot( + at url: URL + ) throws -> [String: Data] { + var snapshot: [String: Data] = [:] + + for suffix in + CrashConsistentStoreReplacer.storeFamilySuffixes { + let memberURL = syntheticFamilyURL( + url, + suffix: suffix + ) + guard + FileManager.default.fileExists( + atPath: memberURL.path + ) + else { + continue + } + + snapshot[suffix] = try Data(contentsOf: memberURL) + } + + return snapshot + } + + private func replaceSyntheticStoreFamily( + at targetURL: URL, + with sourceURL: URL + ) throws { + for suffix in + CrashConsistentStoreReplacer.storeFamilySuffixes { + let targetMemberURL = syntheticFamilyURL( + targetURL, + suffix: suffix + ) + if FileManager.default.fileExists( + atPath: targetMemberURL.path + ) { + try FileManager.default.removeItem( + at: targetMemberURL + ) + } + } + + for suffix in + CrashConsistentStoreReplacer.storeFamilySuffixes { + let sourceMemberURL = syntheticFamilyURL( + sourceURL, + suffix: suffix + ) + guard + FileManager.default.fileExists( + atPath: sourceMemberURL.path + ) + else { + continue + } + + try FileManager.default.copyItem( + at: sourceMemberURL, + to: syntheticFamilyURL( + targetURL, + suffix: suffix + ) + ) + } + } + + private func executeSQLite( + _ sql: String, + at url: URL + ) throws { + let database = try openSQLite(at: url) + defer { + sqlite3_close(database) + } + try executeSQLite(sql, database: database) + } + + private func executeSQLite( + _ sql: String, + database: OpaquePointer + ) throws { + var errorMessage: UnsafeMutablePointer? + let result = sqlite3_exec( + database, + sql, + nil, + nil, + &errorMessage + ) + defer { + sqlite3_free(errorMessage) + } + + guard result == SQLITE_OK else { + let description = errorMessage.map { + String(cString: $0) + } ?? "Unknown SQLite error" + throw NSError( + domain: "UserStorageCompatibilityMigrationTests.SQLite", + code: Int(result), + userInfo: [NSLocalizedDescriptionKey: description] + ) + } + } + + private func updateSQLiteBlob( + _ data: Data, + column: String, + at url: URL + ) throws { + let allowedColumns: Set = [ + "ZASSETFILTEROPTIONS", + "ZASSETKEYSORDER", + "ZFAVOURITECHAINIDS", + "ZUNUSEDCHAINIDS" + ] + guard allowedColumns.contains(column) else { + throw NSError( + domain: + "UserStorageCompatibilityMigrationTests.SQLite", + code: Int(SQLITE_MISUSE), + userInfo: [ + NSLocalizedDescriptionKey: + "Unexpected SQLite test column" + ] + ) + } + + let database = try openSQLite(at: url) + defer { + sqlite3_close(database) + } + + var statement: OpaquePointer? + let prepareResult = sqlite3_prepare_v2( + database, + "UPDATE ZCDMETAACCOUNT SET \(column) = ?1", + -1, + &statement, + nil + ) + guard prepareResult == SQLITE_OK, let statement else { + throw NSError( + domain: + "UserStorageCompatibilityMigrationTests.SQLite", + code: Int(prepareResult) + ) + } + defer { + sqlite3_finalize(statement) + } + + let sqliteTransient = unsafeBitCast( + -1, + to: sqlite3_destructor_type.self + ) + let bindResult = data.withUnsafeBytes { + rawBuffer in + + sqlite3_bind_blob( + statement, + 1, + rawBuffer.baseAddress, + Int32(rawBuffer.count), + sqliteTransient + ) + } + guard bindResult == SQLITE_OK else { + throw NSError( + domain: + "UserStorageCompatibilityMigrationTests.SQLite", + code: Int(bindResult) + ) + } + + let stepResult = sqlite3_step(statement) + guard stepResult == SQLITE_DONE else { + throw NSError( + domain: + "UserStorageCompatibilityMigrationTests.SQLite", + code: Int(stepResult) + ) + } + } + + private func openSQLite(at url: URL) throws -> OpaquePointer { + var database: OpaquePointer? + let result = sqlite3_open_v2( + url.path, + &database, + SQLITE_OPEN_READWRITE, + nil + ) + + guard result == SQLITE_OK, let database else { + defer { + if let database { + sqlite3_close(database) + } + } + throw NSError( + domain: "UserStorageCompatibilityMigrationTests.SQLite", + code: Int(result), + userInfo: [ + NSLocalizedDescriptionKey: "Unable to open SQLite store" + ] + ) + } + + return database + } + + private func hex(byte: UInt8, count: Int) -> String { + Data(repeating: byte, count: count) + .map { String(format: "%02x", $0) } + .joined() + } +} + +private enum UserStorageCommitTestError: Error, Equatable { + case storeReplacementFailed + case keystoreFinalizeFailed + case settingsFinalizeFailed +} + +private enum UserStorageCommitEvent: Equatable { + case keystorePrepare + case settingsPrepare + case storeReplacement + case keystoreFinalize + case settingsFinalize +} + +private final class UserStorageCommitRecorder { + private let lock = NSLock() + private var recordedEvents: [UserStorageCommitEvent] = [] + + var events: [UserStorageCommitEvent] { + lock.with { recordedEvents } + } + + func record(_ event: UserStorageCommitEvent) { + lock.with { + recordedEvents.append(event) + } + } +} + +private final class RecordingKeystoreMigrator: KeystoreMigrating { + private let recorder: UserStorageCommitRecorder + private let finalizeError: Error? + + init( + recorder: UserStorageCommitRecorder, + finalizeError: Error? = nil + ) { + self.recorder = recorder + self.finalizeError = finalizeError + } + + func switchVersion() throws {} + + func fetchKey(for _: String) throws -> Data? { + nil + } + + func deleteKey(for _: String) {} + + func save(key _: Data, for _: String) {} + + func prepare() throws { + recorder.record(.keystorePrepare) + } + + func finalize() throws { + recorder.record(.keystoreFinalize) + + if let finalizeError { + throw finalizeError + } + } +} + +private final class MappingFinalizeFailureKeystoreMigrator: + KeystoreMigrating { + private let migrator: KeystoreMigrating + private let mappedError: Error + private(set) var underlyingFinalizeError: Error? + + init( + migrator: KeystoreMigrating, + mappedError: Error + ) { + self.migrator = migrator + self.mappedError = mappedError + } + + func switchVersion() throws { + try migrator.switchVersion() + } + + func fetchKey(for identifier: String) throws -> Data? { + try migrator.fetchKey(for: identifier) + } + + func deleteKey(for identifier: String) { + migrator.deleteKey(for: identifier) + } + + func save(key: Data, for identifier: String) { + migrator.save(key: key, for: identifier) + } + + func prepare() throws { + try migrator.prepare() + } + + func finalize() throws { + do { + try migrator.finalize() + } catch { + underlyingFinalizeError = error + throw mappedError + } + } +} + +private final class RecordingSettingsMigrator: SettingsMigrating { + private let recorder: UserStorageCommitRecorder + + init(recorder: UserStorageCommitRecorder) { + self.recorder = recorder + } + + func switchVersion() throws {} + + func value(for _: String) -> Any? { + nil + } + + func remove(key _: String) {} + + func set(value _: Any, for _: String) {} + + func prepare() throws { + recorder.record(.settingsPrepare) + } + + func finalize() throws { + recorder.record(.settingsFinalize) + } +} + +private final class CleanupJournalSettingsMigrator: + SettingsMigrating { + private let migrator: SettingsMigrator + private let finalizeError: Error? + + init( + sourceVersion: UserStorageVersion, + destinationVersion: UserStorageVersion, + settings: SettingsManagerProtocol, + finalizeError: Error? = nil + ) { + migrator = SettingsMigrator( + sourceVersion: sourceVersion, + destinationVersion: destinationVersion, + settings: settings + ) + self.finalizeError = finalizeError + } + + func switchVersion() throws { + try migrator.switchVersion() + } + + func value(for key: String) -> Any? { + migrator.value(for: key) + } + + func remove(key: String) { + migrator.remove(key: key) + } + + func set(value: Any, for key: String) { + migrator.set(value: value, for: key) + } + + func prepare() throws { + migrator.remove( + key: SettingsKey.selectedAccount.rawValue + ) + migrator.remove( + key: SettingsKey.selectedConnection.rawValue + ) + try migrator.prepare() + } + + func finalize() throws { + if let finalizeError { + throw finalizeError + } + + try migrator.finalize() + } +} + +private final class FaultInjectingKeystore: KeystoreProtocol { + enum Operation: Hashable { + case add(String) + case update(String) + case fetch(String) + case check(String) + case delete(String) + } + + enum Error: Swift.Error, Equatable { + case injected(Operation) + } + + var failures = Set() + private var values: [String: Data] + + init(values: [String: Data] = [:]) { + self.values = values + } + + func value(for identifier: String) -> Data? { + values[identifier] + } + + func addKey( + _ key: Data, + with identifier: String + ) throws { + try throwIfNeeded(.add(identifier)) + + guard values[identifier] == nil else { + throw KeystoreError.duplicatedItem + } + + values[identifier] = key + } + + func updateKey( + _ key: Data, + with identifier: String + ) throws { + try throwIfNeeded(.update(identifier)) + + guard values[identifier] != nil else { + throw KeystoreError.noKeyFound + } + + values[identifier] = key + } + + func fetchKey(for identifier: String) throws -> Data { + try throwIfNeeded(.fetch(identifier)) + + guard let value = values[identifier] else { + throw KeystoreError.noKeyFound + } + + return value + } + + func checkKey(for identifier: String) throws -> Bool { + try throwIfNeeded(.check(identifier)) + return values[identifier] != nil + } + + func deleteKey(for identifier: String) throws { + try throwIfNeeded(.delete(identifier)) + + guard values.removeValue(forKey: identifier) != nil else { + throw KeystoreError.noKeyFound + } + } + + private func throwIfNeeded(_ operation: Operation) throws { + if failures.contains(operation) { + throw Error.injected(operation) + } + } +} diff --git a/fearlessTests/Common/Services/ChainRegistry/ChainRegistryTests.swift b/fearlessTests/Common/Services/ChainRegistry/ChainRegistryTests.swift index 2e0cd564c6..66314de809 100644 --- a/fearlessTests/Common/Services/ChainRegistry/ChainRegistryTests.swift +++ b/fearlessTests/Common/Services/ChainRegistry/ChainRegistryTests.swift @@ -1,147 +1,451 @@ -//import XCTest -//@testable import fearless -//import RobinHood -//import Cuckoo -// -//class ChainRegistryTests: XCTestCase { -// func testSetupCompletion() throws { -// // given -// -// let runtimeProviderPool = MockRuntimeProviderPoolProtocol() -// let connectionPool = MockConnectionPoolProtocol() -// let specVersionSubscriptionFactory = MockSpecVersionSubscriptionFactoryProtocol() -// let runtimeSyncService = MockRuntimeSyncServiceProtocol() -// -// stub(runtimeSyncService) { stub in -// stub.register(chain: any(), with: any()).thenDoNothing() -// stub.unregister(chainId: any()).thenDoNothing() -// } -// -// let commonTypesSyncService = MockCommonTypesSyncServiceProtocol() -// -// stub(commonTypesSyncService) { stub in -// stub.syncUp().thenDoNothing() -// } -// -// let dataOperationFactory = MockDataOperationFactoryProtocol() -// -// let eventCenter = MockEventCenterProtocol() -// -// stub(eventCenter) { stub in -// stub.notify(with: any()).thenDoNothing() -// } -// -// let chainCount = 10 -// let expectedChains = ChainModelGenerator.generate(count: chainCount) -// let expectedChainIds = Set(expectedChains.map { $0.chainId }) -// let chainsData = try JSONEncoder().encode(expectedChains) -// -// stub(dataOperationFactory) { stub in -// stub.fetchData(from: any()).thenReturn(BaseOperation.createWithResult(chainsData)) -// } -// -// stub(runtimeProviderPool) { stub in -// let runtimeProvider = MockRuntimeProviderProtocol() -// stub.setupRuntimeProvider(for: any()).thenReturn(runtimeProvider) -// stub.getRuntimeProvider(for: any()).thenReturn(runtimeProvider) -// } -// -// stub(connectionPool) { stub in -// let connection = MockConnection() -// stub.setupConnection(for: any()).thenReturn(connection) -// stub.getConnection(for: any()).thenReturn(connection) -// } -// -// let mockSubscription = MockSpecVersionSubscriptionProtocol() -// stub(mockSubscription) { stub in -// stub.subscribe().thenDoNothing() -// stub.unsubscribe().thenDoNothing() -// } -// -// stub(specVersionSubscriptionFactory) { stub in -// stub.createSubscription(for: any(), connection: any()).thenReturn(mockSubscription) -// } -// -// let storageFacade = SubstrateStorageTestFacade() -// -// let mapper = ChainModelMapper() -// let repository: CoreDataRepository = storageFacade.createRepository( -// mapper: AnyCoreDataMapper(mapper) -// ) -// -// let chainSyncService = ChainSyncService( -// chainsUrl: URL(string: "https://google.com")!, -// assetsUrl: URL(string: "https://google.com")!, -// dataFetchFactory: dataOperationFactory, -// repository: AnyDataProviderRepository(repository), -// eventCenter: eventCenter, -// operationQueue: OperationQueue() -// ) -// -// let chainObserver = CoreDataContextObservable( -// service: storageFacade.databaseService, -// mapper: repository.dataMapper, -// predicate: { _ in true } -// ) -// -// chainObserver.start { error in -// if let error = error { -// Logger.shared.error("Chain database observer unexpectedly failed: \(error)") -// } -// } -// -// let chainProvider = StreamableProvider( -// source: AnyStreamableSource(EmptyStreamableSource()), -// repository: AnyDataProviderRepository(repository), -// observable: AnyDataProviderRepositoryObservable(chainObserver), -// operationManager: OperationManagerFacade.sharedManager -// ) -// -// let registry = ChainRegistry( -// runtimeProviderPool: runtimeProviderPool, -// connectionPool: connectionPool, -// chainSyncService: chainSyncService, -// runtimeSyncService: runtimeSyncService, -// commonTypesSyncService: commonTypesSyncService, -// chainProvider: chainProvider, -// specVersionSubscriptionFactory: specVersionSubscriptionFactory, -// eventCenter: EventCenter.shared -// ) -// -// registry.syncUp() -// -// // when -// -// var setupChainIds = Set() -// -// let expectation = XCTestExpectation() -// -// registry.chainsSubscribe(self, runningInQueue: .main) { changes in -// guard !changes.isEmpty else { -// return -// } -// -// changes.forEach { change in -// if case let .insert(item) = change { -// setupChainIds.insert(item.chainId) -// } -// } -// -// expectation.fulfill() -// } -// -// // then -// -// wait(for: [expectation], timeout: 10) -// -// XCTAssertEqual(expectedChainIds, setupChainIds) -// XCTAssertEqual(expectedChainIds, registry.availableChainIds) -// -// for chain in expectedChains { -// XCTAssertNotNil(registry.getConnection(for: chain.chainId)) -// XCTAssertNotNil(registry.getRuntimeProvider(for: chain.chainId)) -// } -// -// verify(runtimeSyncService, times(chainCount)).register(chain: any(), with: any()) -// } -//} +import Cuckoo +import RobinHood +import SSFModels +import SSFRuntimeCodingService +import SSFUtils +import Web3 +import XCTest +@testable import fearless + +final class ChainRegistryTests: XCTestCase { + func testDisabledThenReenabledChainDisconnectsAndUsesNewNode() throws { + let storageFacade = SubstrateStorageTestFacade() + let repository = ChainRepositoryFactory( + storageFacade: storageFacade + ).createRepository() + let provider = makeChainProvider( + storageFacade: storageFacade, + repository: repository + ) + + let oldNode = ChainNodeModel( + url: try XCTUnwrap(URL(string: "wss://old-node.example")), + name: "Old", + apikey: nil + ) + let newNode = ChainNodeModel( + url: try XCTUnwrap(URL(string: "wss://new-node.example")), + name: "New", + apikey: nil + ) + let enabledOld = makeChain( + identifier: "lifecycle-chain", + disabled: false, + node: oldNode + ) + let disabled = makeChain( + identifier: enabledOld.chainId, + disabled: true, + node: oldNode + ) + let enabledNew = makeChain( + identifier: enabledOld.chainId, + disabled: false, + node: newNode + ) + + let connectionFactory = RecordingConnectionFactory() + let connectionPool = ConnectionPool( + connectionFactory: connectionFactory + ) + let runtimeProviderPool = makeRuntimeProviderPool() + let runtimeSyncService = makeRuntimeSyncService() + let subscriptionFactory = makeSubscriptionFactory() + let registry = ChainRegistry( + snapshotHotBootBuilder: NoopSnapshotHotBootBuilder(), + runtimeProviderPool: runtimeProviderPool, + connectionPools: [connectionPool], + chainSyncService: NoopChainSyncService(), + runtimeSyncService: runtimeSyncService, + chainsTypesSyncService: NoopChainsTypesSyncService(), + chainProvider: provider, + specVersionSubscriptionFactory: subscriptionFactory, + networkIssuesCenter: NoopNetworkIssuesCenter(), + eventCenter: NoopEventCenter() + ) + registry.subscribeToChains() + + try save(enabledOld, in: repository) + let firstConnection: ChainConnection = try waitForValue { + guard + registry.getChain(for: enabledOld.chainId)?.selectedNode == oldNode + else { + return nil + } + + return registry.getConnection(for: enabledOld.chainId) + } + XCTAssertEqual(connectionFactory.requestedURLs, [[oldNode.url]]) + + try save(disabled, in: repository) + try waitForCondition { + registry.getChain(for: enabledOld.chainId) == nil && + registry.getConnection(for: enabledOld.chainId) == nil + } + XCTAssertNil(registry.getConnection(for: enabledOld.chainId)) + XCTAssertEqual( + (firstConnection as? MockConnection)?.disconnectCallCount, + 1 + ) + XCTAssertEqual(connectionFactory.requestedURLs, [[oldNode.url]]) + + try save(enabledNew, in: repository) + let secondConnection: ChainConnection = try waitForValue { + guard + registry.getChain(for: enabledOld.chainId)?.selectedNode == newNode + else { + return nil + } + + return registry.getConnection(for: enabledOld.chainId) + } + XCTAssertFalse(firstConnection === secondConnection) + XCTAssertEqual( + connectionFactory.requestedURLs, + [[oldNode.url], [newNode.url]] + ) + XCTAssertEqual( + (firstConnection as? MockConnection)?.disconnectCallCount, + 1 + ) + XCTAssertEqual( + (secondConnection as? MockConnection)?.disconnectCallCount, + 0 + ) + + provider.removeObserver(registry) + } + + func testUpdatedEthereumChainUsesReplacementNodeImmediately() throws { + let storageFacade = SubstrateStorageTestFacade() + let repository = ChainRepositoryFactory( + storageFacade: storageFacade + ).createRepository() + let provider = makeChainProvider( + storageFacade: storageFacade, + repository: repository + ) + let oldNode = ChainNodeModel( + url: try XCTUnwrap(URL(string: "https://old-rpc.example")), + name: "Old Ethereum RPC", + apikey: nil + ) + let newNode = ChainNodeModel( + url: try XCTUnwrap(URL(string: "https://new-rpc.example")), + name: "New Ethereum RPC", + apikey: nil + ) + let chainId = "ethereum-lifecycle-chain" + let oldChain = makeEthereumChain( + identifier: chainId, + node: oldNode + ) + let updatedChain = makeEthereumChain( + identifier: chainId, + node: newNode + ) + let nodeFetching = RecordingEthereumNodeFetching() + let ethereumPool = EthereumConnectionPool( + nodeFetching: nodeFetching + ) + let registry = ChainRegistry( + snapshotHotBootBuilder: NoopSnapshotHotBootBuilder(), + runtimeProviderPool: makeRuntimeProviderPool(), + connectionPools: [ethereumPool], + chainSyncService: NoopChainSyncService(), + runtimeSyncService: makeRuntimeSyncService(), + chainsTypesSyncService: NoopChainsTypesSyncService(), + chainProvider: provider, + specVersionSubscriptionFactory: makeSubscriptionFactory(), + networkIssuesCenter: NoopNetworkIssuesCenter(), + eventCenter: NoopEventCenter() + ) + registry.subscribeToChains() + + try save(oldChain, in: repository) + let firstURL: URL = try waitForValue { + nodeFetching.requestedURLs.first + } + + try save(updatedChain, in: repository) + let secondURL: URL = try waitForValue { + let urls = nodeFetching.requestedURLs + return urls.count >= 2 ? urls.last : nil + } + + XCTAssertEqual(firstURL, oldNode.url) + XCTAssertEqual(secondURL, newNode.url) + XCTAssertEqual( + nodeFetching.requestedURLs, + [oldNode.url, newNode.url] + ) + XCTAssertNotNil( + registry.getEthereumConnection(for: chainId) + ) + XCTAssertEqual( + registry.getChain(for: chainId)?.selectedNode, + newNode + ) + + provider.removeObserver(registry) + } + + private func makeChainProvider( + storageFacade: SubstrateStorageTestFacade, + repository: CoreDataRepository + ) -> StreamableProvider { + let observable = CoreDataContextObservable( + service: storageFacade.databaseService, + mapper: repository.dataMapper, + predicate: { _ in true } + ) + observable.start { error in + XCTAssertNil(error) + } + let source = fearless.EmptyStreamableSource() + + return StreamableProvider( + source: AnyStreamableSource(source), + repository: AnyDataProviderRepository(repository), + observable: AnyDataProviderRepositoryObservable(observable), + operationManager: fearless.OperationManagerFacade.sharedManager + ) + } + + private func makeChain( + identifier: String, + disabled: Bool, + node: ChainNodeModel + ) -> ChainModel { + ChainModel( + rank: 1, + disabled: disabled, + chainId: identifier, + paraId: nil, + name: "Lifecycle", + xcm: nil, + nodes: [node], + addressPrefix: 42, + icon: nil, + selectedNode: node, + iosMinAppVersion: nil, + identityChain: nil + ) + } + + private func makeEthereumChain( + identifier: String, + node: ChainNodeModel + ) -> ChainModel { + ChainModel( + rank: 1, + disabled: false, + chainId: identifier, + paraId: nil, + name: "Ethereum Lifecycle", + xcm: nil, + nodes: [node], + addressPrefix: 0, + icon: nil, + options: [.ethereum], + selectedNode: node, + iosMinAppVersion: nil, + identityChain: nil + ) + } + + private func waitForValue( + timeout: TimeInterval = 2, + _ valueProvider: () -> T? + ) throws -> T { + let deadline = Date().addingTimeInterval(timeout) + + while Date() < deadline { + if let value = valueProvider() { + return value + } + + RunLoop.current.run( + until: Date().addingTimeInterval(0.01) + ) + } + + throw ChainRegistryTestError.timeout + } + + private func waitForCondition( + timeout: TimeInterval = 2, + _ condition: () -> Bool + ) throws { + let _: Bool = try waitForValue(timeout: timeout) { + condition() ? true : nil + } + } + + private func save( + _ chain: ChainModel, + in repository: CoreDataRepository + ) throws { + let operation = repository.saveOperation({ [chain] }, { [] }) + OperationQueue().addOperations([operation], waitUntilFinished: true) + let _: Void = try XCTUnwrap(operation.result).get() + } + + private func makeRuntimeProviderPool() -> MockRuntimeProviderPoolProtocol { + let pool = MockRuntimeProviderPoolProtocol() + let provider = NoopRuntimeProvider() + stub(pool) { stub in + stub.setupRuntimeProvider( + for: any(), + chainTypes: any() + ).thenReturn(provider) + stub.destroyRuntimeProvider(for: any()).thenDoNothing() + } + return pool + } + + private func makeRuntimeSyncService() -> MockRuntimeSyncServiceProtocol { + let service = MockRuntimeSyncServiceProtocol() + stub(service) { stub in + stub.register(chain: any(), with: any()).thenDoNothing() + stub.unregister(chainId: any()).thenDoNothing() + } + return service + } + + private func makeSubscriptionFactory() + -> MockSpecVersionSubscriptionFactoryProtocol { + let factory = MockSpecVersionSubscriptionFactoryProtocol() + let subscription = MockSpecVersionSubscriptionProtocol() + stub(subscription) { stub in + stub.subscribe().thenDoNothing() + stub.unsubscribe().thenDoNothing() + } + stub(factory) { stub in + stub.createSubscription( + for: any(), + connection: any() + ).thenReturn(subscription) + } + return factory + } +} + +private enum ChainRegistryTestError: Error { + case timeout +} + +private final class RecordingEthereumNodeFetching: + EthereumNodeFetchingProtocol { + private let lock = NSLock() + private var urls: [URL] = [] + + var requestedURLs: [URL] { + lock.lock() + defer { lock.unlock() } + + return urls + } + + func getNode(for chain: ChainModel) throws -> Web3.Eth { + if let selectedURL = chain.selectedNode?.url { + lock.lock() + urls.append(selectedURL) + lock.unlock() + } + + return try EthereumNodeFetching().getNode(for: chain) + } +} + +private final class RecordingConnectionFactory: ConnectionFactoryProtocol { + private let lock = NSLock() + private var connections: [MockConnection] = [] + private var urls: [[URL]] = [] + + var requestedURLs: [[URL]] { + lock.lock() + defer { lock.unlock() } + return urls + } + + func createConnection( + connectionName: String?, + for urls: [URL], + delegate _: WebSocketEngineDelegate + ) throws -> ChainConnection { + let connection = MockConnection() + connection.connectionName = connectionName + connection.url = urls.first + + lock.lock() + self.urls.append(urls) + connections.append(connection) + lock.unlock() + + return connection + } +} + +private final class NoopSnapshotHotBootBuilder: + SnapshotHotBootBuilderProtocol { + func startHotBoot() {} +} + +private final class NoopChainSyncService: ChainSyncServiceProtocol { + func syncUp() {} +} + +private final class NoopChainsTypesSyncService: + ChainsTypesSyncServiceProtocol { + func syncUp() {} +} + +private final class NoopNetworkIssuesCenter: NetworkIssuesCenterProtocol { + func addIssuesListener( + _: NetworkIssuesCenterListener, + getExisting _: Bool + ) {} + + func removeIssuesListener(_: NetworkIssuesCenterListener) {} + + func forceNotify() {} +} + +private final class NoopEventCenter: EventCenterProtocol { + func notify(with _: EventProtocol) {} + + func add( + observer _: EventVisitorProtocol, + dispatchIn _: DispatchQueue? + ) {} + + func remove(observer _: EventVisitorProtocol) {} +} + +private final class NoopRuntimeProvider: RuntimeProviderProtocol { + var runtimeSpecVersion: RuntimeSpecVersion = .defaultVersion + var snapshot: RuntimeSnapshot? + + func setup() {} + + func setupHot() {} + + func cleanup() {} + + func readySnapshot() async throws -> RuntimeSnapshot { + throw SSFRuntimeCodingService.RuntimeProviderError + .providerUnavailable + } + + func fetchCoderFactoryOperation() + -> BaseOperation { + BaseOperation() + } + + func fetchCoderFactory() async throws + -> RuntimeCoderFactoryProtocol { + throw SSFRuntimeCodingService.RuntimeProviderError + .providerUnavailable + } +} diff --git a/fearlessTests/Common/Services/ChainRegistry/ChainSyncServiceCompatibilityTests.swift b/fearlessTests/Common/Services/ChainRegistry/ChainSyncServiceCompatibilityTests.swift index c655a103f8..a77e96dbdf 100644 --- a/fearlessTests/Common/Services/ChainRegistry/ChainSyncServiceCompatibilityTests.swift +++ b/fearlessTests/Common/Services/ChainRegistry/ChainSyncServiceCompatibilityTests.swift @@ -1,4 +1,8 @@ import XCTest +import RobinHood +import SoraFoundation +import SSFModels +import SSFUtils @testable import fearless final class ChainSyncServiceCompatibilityTests: XCTestCase { @@ -36,8 +40,1931 @@ final class ChainSyncServiceCompatibilityTests: XCTestCase { let explorers = try XCTUnwrap(externalApi["explorers"] as? [[String: Any]]) XCTAssertEqual(history["type"] as? String, ChainSyncService.historyExplorerCompatibilityType) - XCTAssertEqual(staking["type"] as? String, ChainSyncService.historyExplorerCompatibilityType) - XCTAssertEqual(explorers.first?["type"] as? String, ChainSyncService.historyExplorerCompatibilityType) + XCTAssertEqual(staking["type"] as? String, ChainSyncService.stakingExplorerCompatibilityType) + XCTAssertEqual(explorers.first?["type"] as? String, ChainSyncService.genericExplorerCompatibilityType) XCTAssertEqual(explorers.last?["type"] as? String, "etherscan") } + + func testPreservingLocalNodePreferencesKeepsCustomAndSelectedNodes() throws { + let remoteChain = makeValidChain( + generatingAssets: 1, + addressPrefix: 42 + ) + let customNode = ChainNodeModel( + url: try XCTUnwrap(URL(string: "wss://user-node.example")), + name: "My private node", + apikey: nil + ) + let localChain = remoteChain + .replacingCustomNodes([customNode]) + .replacingSelectedNode(customNode) + + let mergedChains = ChainSyncService.preservingLocalNodePreferences( + remoteChains: [remoteChain], + localChains: [localChain] + ) + + let mergedChain = try XCTUnwrap(mergedChains.first) + XCTAssertEqual(mergedChain.customNodes, Set([customNode])) + XCTAssertEqual(mergedChain.selectedNode, customNode) + XCTAssertNil(remoteChain.customNodes) + XCTAssertNil(remoteChain.selectedNode) + } + + func testPreservingLocalNodePreferencesDoesNotApplyPreferencesToAnotherChain() throws { + let remoteChains = ChainModelGenerator.generate(count: 2) + let customNode = ChainNodeModel( + url: try XCTUnwrap(URL(string: "wss://user-node.example")), + name: "My private node", + apikey: nil + ) + let localChain = remoteChains[0] + .replacingCustomNodes([customNode]) + .replacingSelectedNode(customNode) + + let mergedChains = ChainSyncService.preservingLocalNodePreferences( + remoteChains: remoteChains, + localChains: [localChain] + ) + + XCTAssertEqual(mergedChains[0].customNodes, Set([customNode])) + XCTAssertEqual(mergedChains[0].selectedNode, customNode) + XCTAssertNil(mergedChains[1].customNodes) + XCTAssertNil(mergedChains[1].selectedNode) + } + + func testPreservingLocalNodePreferencesRejectsRemoteCustomNodeInjection() throws { + let localChain = makeValidChain( + generatingAssets: 1, + addressPrefix: 42 + ) + let injectedNode = ChainNodeModel( + url: try XCTUnwrap(URL(string: "wss://untrusted-remote-node.example")), + name: "Injected node", + apikey: nil + ) + let remoteChain = localChain + .replacingCustomNodes([injectedNode]) + .replacingSelectedNode(injectedNode) + + let mergedChains = ChainSyncService.preservingLocalNodePreferences( + remoteChains: [remoteChain], + localChains: [localChain] + ) + + let mergedChain = try XCTUnwrap(mergedChains.first) + XCTAssertEqual(mergedChain.customNodes, Set()) + XCTAssertNil(mergedChain.selectedNode) + } + + func testPreservingLocalNodePreferencesDropsRetiredDefaultSelection() throws { + let remoteChain = makeValidChain( + generatingAssets: 1, + addressPrefix: 42 + ) + let retiredNode = ChainNodeModel( + url: try XCTUnwrap(URL(string: "wss://retired-default.example")), + name: "Retired default", + apikey: nil + ) + let localChain = remoteChain.replacingSelectedNode(retiredNode) + + let mergedChains = ChainSyncService.preservingLocalNodePreferences( + remoteChains: [remoteChain], + localChains: [localChain] + ) + + let mergedChain = try XCTUnwrap(mergedChains.first) + XCTAssertNil(mergedChain.selectedNode) + XCTAssertFalse(mergedChain.nodes.contains(retiredNode)) + } + + func testPreservingLocalNodePreferencesRefreshesSelectedDefaultByURL() throws { + let remoteChain = makeValidChain( + generatingAssets: 1, + addressPrefix: 42 + ) + let remoteNode = try XCTUnwrap(remoteChain.nodes.first) + let staleSelectedNode = ChainNodeModel( + url: remoteNode.url, + name: "Stale display name", + apikey: nil + ) + let localChain = remoteChain.replacingSelectedNode(staleSelectedNode) + + let mergedChains = ChainSyncService.preservingLocalNodePreferences( + remoteChains: [remoteChain], + localChains: [localChain] + ) + + XCTAssertEqual(try XCTUnwrap(mergedChains.first).selectedNode, remoteNode) + } + + func testRepositoryWriteFailureEmitsFailureWithoutFalseCompletion() throws { + let remoteChain = makeValidChain( + generatingAssets: 1, + addressPrefix: 42 + ) + let data = try JSONEncoder().encode([remoteChain]) + let repository = SaveFailingChainRepository( + error: ChainSyncCompatibilityTestError.saveFailed + ) + let eventCenter = RecordingChainSyncEventCenter() + let failureExpectation = expectation(description: "save failure is propagated") + let falseCompletionExpectation = expectation( + description: "failed save never emits completion" + ) + falseCompletionExpectation.isInverted = true + eventCenter.onEvent = { event in + if event is ChainSyncDidFail { + failureExpectation.fulfill() + } else if event is ChainSyncDidComplete { + falseCompletionExpectation.fulfill() + } + } + let service = ChainSyncService( + chainsUrl: try XCTUnwrap(URL(string: "https://chains.example/chains.json")), + dataFetchFactory: StaticDataOperationFactory(data: data), + repository: AnyDataProviderRepository(repository), + eventCenter: eventCenter, + operationQueue: OperationQueue(), + retryStrategy: NeverReconnectStrategy(), + applicationHandler: ApplicationHandler() + ) + + service.syncUp() + + wait( + for: [failureExpectation, falseCompletionExpectation], + timeout: 1 + ) + XCTAssertEqual(repository.saveCallCount, 1) + XCTAssertEqual(eventCenter.failureCount, 1) + XCTAssertEqual(eventCenter.completionCount, 0) + XCTAssertTrue( + eventCenter.lastFailure is ChainSyncCompatibilityTestError + ) + } + + func testRepositoryFetchFailureDoesNotWriteOrReplaceCache() throws { + let remoteChain = makeValidChain( + generatingAssets: 1, + addressPrefix: 42 + ) + let repository = ScriptedChainRepository( + fetchAllError: ChainSyncCompatibilityTestError.fetchFailed + ) + let eventCenter = RecordingChainSyncEventCenter() + let failureExpectation = expectation( + description: "database failure is propagated" + ) + eventCenter.onEvent = { event in + if event is ChainSyncDidFail { + failureExpectation.fulfill() + } + } + let service = makeService( + data: try JSONEncoder().encode([remoteChain]), + repository: AnyDataProviderRepository(repository), + eventCenter: eventCenter + ) + + service.syncUp() + + wait(for: [failureExpectation], timeout: 1) + XCTAssertEqual(repository.replaceCallCount, 0) + XCTAssertEqual(eventCenter.failureCount, 1) + XCTAssertEqual(eventCenter.completionCount, 0) + } + + func testEmptyRemotePayloadNeverDeletesReadableCache() throws { + let repository = ScriptedChainRepository() + let eventCenter = RecordingChainSyncEventCenter() + let failureExpectation = expectation( + description: "empty authoritative payload is rejected" + ) + eventCenter.onEvent = { event in + if event is ChainSyncDidFail { + failureExpectation.fulfill() + } + } + let service = makeService( + data: try JSONEncoder().encode([ChainModel]()), + repository: AnyDataProviderRepository(repository), + eventCenter: eventCenter + ) + + service.syncUp() + + wait(for: [failureExpectation], timeout: 1) + XCTAssertEqual(repository.replaceCallCount, 0) + XCTAssertEqual(eventCenter.failureCount, 1) + XCTAssertEqual(eventCenter.completionCount, 0) + XCTAssertTrue(eventCenter.lastFailure is ChainSyncServiceError) + } + + func testActiveRemoteChainWithOnlyUnusableNodesPreservesWorkingCache() + throws { + let workingChain = makeValidChain( + generatingAssets: 1, + addressPrefix: 42 + ) + let unusableNodes: Set = [ + ChainNodeModel( + url: try XCTUnwrap( + URL(string: "ftp://attacker.example/node") + ), + name: "Unsupported scheme", + apikey: nil + ), + ChainNodeModel( + url: try XCTUnwrap(URL(string: "relative/node")), + name: "Relative endpoint", + apikey: nil + ) + ] + let hostileRemoteChain = workingChain.replacingNodes( + unusableNodes + ) + let repository = ScriptedChainRepository( + localChains: [workingChain] + ) + let eventCenter = RecordingChainSyncEventCenter() + let failureExpectation = expectation( + description: "an unusable active chain is rejected" + ) + eventCenter.onEvent = { event in + if event is ChainSyncDidFail { + failureExpectation.fulfill() + } + } + let service = makeService( + data: try JSONEncoder().encode([hostileRemoteChain]), + repository: AnyDataProviderRepository(repository), + eventCenter: eventCenter + ) + + service.syncUp() + + wait(for: [failureExpectation], timeout: 1) + XCTAssertEqual(repository.saveCallCount, 0) + XCTAssertEqual(repository.replaceCallCount, 0) + XCTAssertTrue(repository.savedModels.isEmpty) + XCTAssertTrue(repository.deletedIdentifiers.isEmpty) + XCTAssertEqual(eventCenter.failureCount, 1) + XCTAssertEqual(eventCenter.completionCount, 0) + } + + func testMixedRemoteNodesPersistOnlyUsableUniqueEndpoints() throws { + let baseChain = makeValidChain( + generatingAssets: 1, + addressPrefix: 42 + ) + let validNode = try XCTUnwrap(baseChain.nodes.first) + let duplicateURLNode = ChainNodeModel( + url: validNode.url, + name: validNode.name + " duplicate", + apikey: nil + ) + let invalidNode = ChainNodeModel( + url: try XCTUnwrap(URL(string: "file:///tmp/not-a-node")), + name: "Local file", + apikey: nil + ) + let mixedRemoteChain = baseChain.replacingNodes( + Set([validNode, duplicateURLNode, invalidNode]) + ) + let repository = ScriptedChainRepository() + let eventCenter = RecordingChainSyncEventCenter() + let completionExpectation = expectation( + description: "usable endpoints are saved" + ) + eventCenter.onEvent = { event in + if event is ChainSyncDidComplete { + completionExpectation.fulfill() + } + } + let service = makeService( + data: try JSONEncoder().encode([mixedRemoteChain]), + repository: AnyDataProviderRepository(repository), + eventCenter: eventCenter + ) + + service.syncUp() + + wait(for: [completionExpectation], timeout: 1) + let savedChain = try XCTUnwrap(repository.savedModels.first) + XCTAssertEqual(savedChain.nodes.count, 1) + XCTAssertEqual(savedChain.nodes.first?.url, validNode.url) + XCTAssertTrue( + savedChain.nodes.allSatisfy { + ChainModelMapper.isUsableNodeURL($0.url) + } + ) + XCTAssertEqual(eventCenter.failureCount, 0) + XCTAssertEqual(eventCenter.completionCount, 1) + } + + func testPartialRemotePayloadDisablesOmittedChainWithoutDeletingPreferences() throws { + let remoteChains = ChainModelGenerator.generate(count: 2) + let retainedRemoteChain = remoteChains[0] + let omittedRemoteChain = remoteChains[1] + let customNode = ChainNodeModel( + url: try XCTUnwrap(URL(string: "wss://omitted-user-node.example")), + name: "Omitted chain preference", + apikey: nil + ) + let omittedLocalChain = omittedRemoteChain + .replacingCustomNodes([customNode]) + .replacingSelectedNode(customNode) + let repository = ScriptedChainRepository( + localChains: [retainedRemoteChain, omittedLocalChain] + ) + let eventCenter = RecordingChainSyncEventCenter() + let completionExpectation = expectation( + description: "partial payload is retained non-destructively" + ) + eventCenter.onEvent = { event in + if event is ChainSyncDidComplete { + completionExpectation.fulfill() + } + } + let service = makeService( + data: try JSONEncoder().encode([retainedRemoteChain]), + repository: AnyDataProviderRepository(repository), + eventCenter: eventCenter + ) + + service.syncUp() + + wait(for: [completionExpectation], timeout: 1) + let disabledChain = try XCTUnwrap( + repository.savedModels.first { + $0.chainId == omittedRemoteChain.chainId + } + ) + XCTAssertTrue(disabledChain.disabled) + XCTAssertEqual(disabledChain.customNodes, Set([customNode])) + XCTAssertEqual(disabledChain.selectedNode, customNode) + XCTAssertTrue(repository.deletedIdentifiers.isEmpty) + XCTAssertEqual(eventCenter.failureCount, 0) + } + + func testReappearingRemoteChainRestoresDisabledPreferences() throws { + let remoteChain = makeValidChain( + generatingAssets: 1, + addressPrefix: 42 + ) + let customNode = ChainNodeModel( + url: try XCTUnwrap(URL(string: "wss://restored-user-node.example")), + name: "Restored preference", + apikey: nil + ) + let disabledLocalChain = remoteChain + .replacingCustomNodes([customNode]) + .replacingSelectedNode(customNode) + .replacingDisabled(true) + + let mergedChains = ChainSyncService.preservingLocalNodePreferences( + remoteChains: [remoteChain], + localChains: [disabledLocalChain] + ) + + let restoredChain = try XCTUnwrap(mergedChains.first) + XCTAssertFalse(restoredChain.disabled) + XCTAssertEqual(restoredChain.customNodes, Set([customNode])) + XCTAssertEqual(restoredChain.selectedNode, customNode) + } + + func testQuarantinedLocalChainDoesNotDiscardHealthyNodePreferences() throws { + let remoteChain = makeValidChain( + generatingAssets: 1, + addressPrefix: 42 + ) + let customNode = ChainNodeModel( + url: try XCTUnwrap(URL(string: "wss://user-node.example")), + name: "My private node", + apikey: nil + ) + let healthyLocalChain = remoteChain + .replacingCustomNodes([customNode]) + .replacingSelectedNode(customNode) + let quarantinedChain = ChainModel( + rank: nil, + disabled: true, + chainId: ChainModelMapper.quarantinedChainIdentifierPrefix + "fixture", + paraId: nil, + name: ChainModelMapper.quarantinedChainName, + xcm: nil, + nodes: [], + addressPrefix: 0, + icon: nil, + iosMinAppVersion: nil, + identityChain: nil + ) + let repository = ScriptedChainRepository( + localChains: [healthyLocalChain, quarantinedChain] + ) + let eventCenter = RecordingChainSyncEventCenter() + let completionExpectation = expectation( + description: "healthy preferences survive quarantine cleanup" + ) + eventCenter.onEvent = { event in + if event is ChainSyncDidComplete { + completionExpectation.fulfill() + } + } + let service = makeService( + data: try JSONEncoder().encode([remoteChain]), + repository: AnyDataProviderRepository(repository), + eventCenter: eventCenter + ) + + service.syncUp() + + wait(for: [completionExpectation], timeout: 1) + XCTAssertEqual(repository.replaceCallCount, 0) + XCTAssertEqual(repository.saveCallCount, 1) + XCTAssertTrue(repository.savedModels.isEmpty) + XCTAssertTrue(repository.deletedIdentifiers.isEmpty) + XCTAssertEqual(eventCenter.failureCount, 0) + XCTAssertEqual(eventCenter.completionCount, 1) + } + + func testMalformedChainCleanupFailurePreventsRepositoryMutation() throws { + let remoteChain = makeValidChain( + generatingAssets: 1, + addressPrefix: 42 + ) + let repository = ScriptedChainRepository() + let eventCenter = RecordingChainSyncEventCenter() + let failureExpectation = expectation( + description: "cleanup failure is propagated before sync" + ) + eventCenter.onEvent = { event in + if event is ChainSyncDidFail { + failureExpectation.fulfill() + } + } + let service = makeService( + data: try JSONEncoder().encode([remoteChain]), + repository: AnyDataProviderRepository(repository), + eventCenter: eventCenter, + malformedChainCleanupOperationFactory: { + ClosureOperation { + throw ChainSyncCompatibilityTestError.cleanupFailed + } + } + ) + + service.syncUp() + + wait(for: [failureExpectation], timeout: 1) + XCTAssertEqual(repository.saveCallCount, 0) + XCTAssertEqual(repository.replaceCallCount, 0) + XCTAssertEqual(eventCenter.failureCount, 1) + XCTAssertEqual(eventCenter.completionCount, 0) + } + + func testConcurrentSyncRequestsStartSingleDeterministicPipeline() + throws { + let remoteChain = makeValidChain( + generatingAssets: 1, + addressPrefix: 42 + ) + let expectedChain = try XCTUnwrap( + ChainSyncService.sanitizingRemoteChains([remoteChain]).first + ) + let dataFactory = CountingDataOperationFactory( + data: try JSONEncoder().encode([remoteChain]) + ) + let repository = ScriptedChainRepository() + let eventCenter = RecordingChainSyncEventCenter() + let cleanupCounter = LockedCounter() + let cleanupRelease = DispatchSemaphore(value: 0) + let cleanupStarted = expectation( + description: "one cleanup pipeline starts" + ) + let completion = expectation( + description: "the reserved pipeline completes" + ) + eventCenter.onEvent = { event in + if event is ChainSyncDidComplete { + completion.fulfill() + } + } + let service = ChainSyncService( + chainsUrl: try XCTUnwrap( + URL(string: "https://chains.example/chains.json") + ), + dataFetchFactory: dataFactory, + repository: AnyDataProviderRepository(repository), + eventCenter: eventCenter, + operationQueue: OperationQueue(), + retryStrategy: NeverReconnectStrategy(), + applicationHandler: ApplicationHandler(), + malformedChainCleanupOperationFactory: { + if cleanupCounter.increment() == 1 { + cleanupStarted.fulfill() + } + + return ClosureOperation { + cleanupRelease.wait() + } + } + ) + + DispatchQueue.concurrentPerform(iterations: 32) { index in + if index.isMultiple(of: 2) { + service.syncUp() + } else { + service.didReceiveDidBecomeActive( + notification: Notification( + name: Notification.Name( + "ChainSyncConcurrencyRegression" + ) + ) + ) + } + } + + wait(for: [cleanupStarted], timeout: 1) + XCTAssertEqual(cleanupCounter.value, 1) + XCTAssertEqual(eventCenter.startCount, 1) + XCTAssertEqual(dataFactory.fetchCallCount, 0) + XCTAssertEqual(repository.fetchAllCallCount, 0) + XCTAssertEqual(repository.saveCallCount, 0) + + cleanupRelease.signal() + wait(for: [completion], timeout: 1) + + XCTAssertEqual(cleanupCounter.value, 1) + XCTAssertEqual(dataFactory.fetchCallCount, 1) + XCTAssertEqual(repository.fetchAllCallCount, 1) + XCTAssertEqual(repository.saveCallCount, 1) + XCTAssertEqual(eventCenter.startCount, 1) + XCTAssertEqual(eventCenter.failureCount, 0) + XCTAssertEqual(eventCenter.completionCount, 1) + XCTAssertEqual(repository.savedModels, [expectedChain]) + } + + func testCooldownExpiryDoesNotReleaseBlockedPipeline() throws { + let remoteChain = makeValidChain( + generatingAssets: 1, + addressPrefix: 42 + ) + let expectedChain = try XCTUnwrap( + ChainSyncService.sanitizingRemoteChains([remoteChain]).first + ) + let firstFetchRelease = DispatchSemaphore(value: 0) + let firstFetchStarted = expectation( + description: "first fetch remains unresolved" + ) + let dataFactory = CountingDataOperationFactory( + data: try JSONEncoder().encode([remoteChain]), + firstFetchGate: firstFetchRelease, + onFirstFetchStart: { + firstFetchStarted.fulfill() + } + ) + let repository = ScriptedChainRepository() + let eventCenter = RecordingChainSyncEventCenter() + let cleanupCounter = LockedCounter() + let cooldownTimer = ManualCountdownTimer() + let cooldownTimerQueue = DispatchQueue( + label: "jp.co.soramitsu.fearless.tests.cooldown.expiry-success" + ) + let firstCompletion = expectation( + description: "first pipeline completes" + ) + let secondCompletion = expectation( + description: "next pipeline starts after completion" + ) + eventCenter.onEvent = { event in + guard event is ChainSyncDidComplete else { + return + } + + if eventCenter.completionCount == 1 { + firstCompletion.fulfill() + } else if eventCenter.completionCount == 2 { + secondCompletion.fulfill() + } + } + let service = ChainSyncService( + chainsUrl: try XCTUnwrap( + URL(string: "https://chains.example/chains.json") + ), + dataFetchFactory: dataFactory, + repository: AnyDataProviderRepository(repository), + eventCenter: eventCenter, + operationQueue: OperationQueue(), + retryStrategy: NeverReconnectStrategy(), + applicationHandler: ApplicationHandler(), + malformedChainCleanupOperationFactory: { + _ = cleanupCounter.increment() + return ClosureOperation { () } + }, + cooldownTimer: cooldownTimer, + cooldownTimerQueue: cooldownTimerQueue + ) + + service.syncUp() + wait(for: [firstFetchStarted], timeout: 1) + drain(cooldownTimerQueue) + XCTAssertEqual(cooldownTimer.startCount, 1) + + cooldownTimer.expire() + + DispatchQueue.concurrentPerform(iterations: 32) { index in + if index.isMultiple(of: 2) { + service.syncUp() + } else { + service.didReceiveDidBecomeActive( + notification: Notification( + name: Notification.Name( + "ChainSyncCooldownExpiryRegression" + ) + ) + ) + } + } + + XCTAssertEqual(cleanupCounter.value, 1) + XCTAssertEqual(dataFactory.fetchCallCount, 1) + XCTAssertEqual(repository.fetchAllCallCount, 0) + XCTAssertEqual(repository.saveCallCount, 0) + XCTAssertEqual(eventCenter.startCount, 1) + XCTAssertEqual(eventCenter.completionCount, 0) + + firstFetchRelease.signal() + wait(for: [firstCompletion], timeout: 1) + + XCTAssertEqual(cleanupCounter.value, 1) + XCTAssertEqual(dataFactory.fetchCallCount, 1) + XCTAssertEqual(repository.fetchAllCallCount, 1) + XCTAssertEqual(repository.saveCallCount, 1) + XCTAssertEqual(eventCenter.startCount, 1) + XCTAssertEqual(repository.savedModels, [expectedChain]) + + service.syncUp() + wait(for: [secondCompletion], timeout: 1) + + XCTAssertEqual(cleanupCounter.value, 2) + XCTAssertEqual(dataFactory.fetchCallCount, 2) + XCTAssertEqual(repository.fetchAllCallCount, 2) + XCTAssertEqual(repository.saveCallCount, 2) + XCTAssertEqual(eventCenter.startCount, 2) + XCTAssertEqual(eventCenter.failureCount, 0) + XCTAssertEqual(eventCenter.completionCount, 2) + XCTAssertEqual(repository.savedModels, [expectedChain]) + } + + func testSuccessBeforeTimerStartRetainsCooldownUntilExpiry() throws { + let data = try JSONEncoder().encode([ + makeValidChain(generatingAssets: 1, addressPrefix: 42) + ]) + let dataFactory = ScriptedDataOperationFactory( + scripts: [ + { data }, + { data } + ] + ) + let repository = ScriptedChainRepository() + let eventCenter = RecordingChainSyncEventCenter() + let completionSignal = DispatchSemaphore(value: 0) + eventCenter.onEvent = { event in + if event is ChainSyncDidComplete { + completionSignal.signal() + } + } + let cooldownTimer = ManualCountdownTimer() + let cooldownTimerQueue = DispatchQueue( + label: "jp.co.soramitsu.fearless.tests.cooldown.before-success" + ) + cooldownTimerQueue.suspend() + var timerQueueIsSuspended = true + defer { + if timerQueueIsSuspended { + cooldownTimerQueue.resume() + } + } + let service = makeService( + dataFetchFactory: dataFactory, + repository: AnyDataProviderRepository(repository), + eventCenter: eventCenter, + cooldownTimer: cooldownTimer, + cooldownTimerQueue: cooldownTimerQueue + ) + + service.syncUp() + waitForSignal( + completionSignal, + description: "success before timer start" + ) + + DispatchQueue.concurrentPerform(iterations: 32) { _ in + service.syncUp() + } + XCTAssertEqual(eventCenter.startCount, 1) + XCTAssertEqual(dataFactory.fetchCallCount, 1) + XCTAssertEqual(cooldownTimer.startCount, 0) + + cooldownTimerQueue.resume() + timerQueueIsSuspended = false + drain(cooldownTimerQueue) + XCTAssertEqual(cooldownTimer.startCount, 1) + + cooldownTimer.expire() + service.syncUp() + waitForSignal( + completionSignal, + description: "success after deferred timer expiry" + ) + + XCTAssertEqual(eventCenter.startCount, 2) + XCTAssertEqual(dataFactory.fetchCallCount, 2) + XCTAssertEqual(eventCenter.failureCount, 0) + XCTAssertEqual(eventCenter.completionCount, 2) + } + + func testSuccessDuringCooldownRemainsBlockedUntilExpiry() throws { + let data = try JSONEncoder().encode([ + makeValidChain(generatingAssets: 1, addressPrefix: 42) + ]) + let firstFetchStarted = DispatchSemaphore(value: 0) + let firstFetchRelease = DispatchSemaphore(value: 0) + let dataFactory = ScriptedDataOperationFactory( + scripts: [ + { + firstFetchStarted.signal() + guard firstFetchRelease.wait( + timeout: .now() + 2 + ) == .success else { + throw ChainSyncCompatibilityTestError.gateTimedOut + } + return data + }, + { data } + ] + ) + let repository = ScriptedChainRepository() + let eventCenter = RecordingChainSyncEventCenter() + let completionSignal = DispatchSemaphore(value: 0) + eventCenter.onEvent = { event in + if event is ChainSyncDidComplete { + completionSignal.signal() + } + } + let cooldownTimer = ManualCountdownTimer() + let cooldownTimerQueue = DispatchQueue( + label: "jp.co.soramitsu.fearless.tests.cooldown.during-success" + ) + let service = makeService( + dataFetchFactory: dataFactory, + repository: AnyDataProviderRepository(repository), + eventCenter: eventCenter, + cooldownTimer: cooldownTimer, + cooldownTimerQueue: cooldownTimerQueue + ) + + service.syncUp() + waitForSignal(firstFetchStarted, description: "first gated fetch") + drain(cooldownTimerQueue) + XCTAssertEqual(cooldownTimer.startCount, 1) + + firstFetchRelease.signal() + waitForSignal( + completionSignal, + description: "success during cooldown" + ) + DispatchQueue.concurrentPerform(iterations: 32) { _ in + service.syncUp() + } + XCTAssertEqual(eventCenter.startCount, 1) + XCTAssertEqual(dataFactory.fetchCallCount, 1) + + cooldownTimer.expire() + service.syncUp() + waitForSignal( + completionSignal, + description: "success after active cooldown" + ) + + XCTAssertEqual(eventCenter.startCount, 2) + XCTAssertEqual(dataFactory.fetchCallCount, 2) + XCTAssertEqual(eventCenter.failureCount, 0) + XCTAssertEqual(eventCenter.completionCount, 2) + } + + func testFailureBeforeTimerStartInvalidatesOnlyFailedReservation() + throws { + let data = try JSONEncoder().encode([ + makeValidChain(generatingAssets: 1, addressPrefix: 42) + ]) + let dataFactory = ScriptedDataOperationFactory( + scripts: [ + { + throw ChainSyncCompatibilityTestError.fetchFailed + }, + { data } + ] + ) + let repository = ScriptedChainRepository() + let eventCenter = RecordingChainSyncEventCenter() + let failureSignal = DispatchSemaphore(value: 0) + let completionSignal = DispatchSemaphore(value: 0) + eventCenter.onEvent = { event in + if event is ChainSyncDidFail { + failureSignal.signal() + } else if event is ChainSyncDidComplete { + completionSignal.signal() + } + } + let cooldownTimer = ManualCountdownTimer() + let cooldownTimerQueue = DispatchQueue( + label: "jp.co.soramitsu.fearless.tests.cooldown.before-failure" + ) + cooldownTimerQueue.suspend() + var timerQueueIsSuspended = true + defer { + if timerQueueIsSuspended { + cooldownTimerQueue.resume() + } + } + let service = makeService( + dataFetchFactory: dataFactory, + repository: AnyDataProviderRepository(repository), + eventCenter: eventCenter, + cooldownTimer: cooldownTimer, + cooldownTimerQueue: cooldownTimerQueue + ) + + service.syncUp() + waitForSignal( + failureSignal, + description: "failure before timer start" + ) + XCTAssertEqual(cooldownTimer.startCount, 0) + + DispatchQueue.concurrentPerform(iterations: 32) { _ in + service.syncUp() + } + waitForSignal( + completionSignal, + description: "single replacement pipeline" + ) + XCTAssertEqual(eventCenter.startCount, 2) + XCTAssertEqual(dataFactory.fetchCallCount, 2) + + cooldownTimerQueue.resume() + timerQueueIsSuspended = false + drain(cooldownTimerQueue) + + XCTAssertEqual( + cooldownTimer.startCount, + 1, + "the invalidated timer must not start" + ) + XCTAssertEqual(eventCenter.failureCount, 1) + XCTAssertEqual(eventCenter.completionCount, 1) + } + + func testFailureDuringCooldownStopsOldTimerAndAllowsOneReplacement() + throws { + let data = try JSONEncoder().encode([ + makeValidChain(generatingAssets: 1, addressPrefix: 42) + ]) + let firstFetchStarted = DispatchSemaphore(value: 0) + let firstFetchRelease = DispatchSemaphore(value: 0) + let dataFactory = ScriptedDataOperationFactory( + scripts: [ + { + firstFetchStarted.signal() + guard firstFetchRelease.wait( + timeout: .now() + 2 + ) == .success else { + throw ChainSyncCompatibilityTestError.gateTimedOut + } + throw ChainSyncCompatibilityTestError.fetchFailed + }, + { data } + ] + ) + let repository = ScriptedChainRepository() + let eventCenter = RecordingChainSyncEventCenter() + let failureSignal = DispatchSemaphore(value: 0) + let completionSignal = DispatchSemaphore(value: 0) + eventCenter.onEvent = { event in + if event is ChainSyncDidFail { + failureSignal.signal() + } else if event is ChainSyncDidComplete { + completionSignal.signal() + } + } + let cooldownTimer = ManualCountdownTimer() + let cooldownTimerQueue = DispatchQueue( + label: "jp.co.soramitsu.fearless.tests.cooldown.during-failure" + ) + let service = makeService( + dataFetchFactory: dataFactory, + repository: AnyDataProviderRepository(repository), + eventCenter: eventCenter, + cooldownTimer: cooldownTimer, + cooldownTimerQueue: cooldownTimerQueue + ) + + service.syncUp() + waitForSignal(firstFetchStarted, description: "first gated fetch") + drain(cooldownTimerQueue) + XCTAssertEqual(cooldownTimer.startCount, 1) + + firstFetchRelease.signal() + waitForSignal( + failureSignal, + description: "failure during cooldown" + ) + drain(cooldownTimerQueue) + XCTAssertEqual(cooldownTimer.stopCount, 1) + if case .stopped = cooldownTimer.state { + // Expected. + } else { + XCTFail("Failed reservation timer must be stopped") + } + + DispatchQueue.concurrentPerform(iterations: 32) { _ in + service.syncUp() + } + waitForSignal( + completionSignal, + description: "replacement after active failure" + ) + drain(cooldownTimerQueue) + + XCTAssertEqual(eventCenter.startCount, 2) + XCTAssertEqual(dataFactory.fetchCallCount, 2) + XCTAssertEqual(cooldownTimer.startCount, 2) + XCTAssertEqual(eventCenter.failureCount, 1) + XCTAssertEqual(eventCenter.completionCount, 1) + } + + func testFailureAfterCooldownExpiryAllowsReplacementWithoutOverlap() + throws { + let data = try JSONEncoder().encode([ + makeValidChain(generatingAssets: 1, addressPrefix: 42) + ]) + let firstFetchStarted = DispatchSemaphore(value: 0) + let firstFetchRelease = DispatchSemaphore(value: 0) + let dataFactory = ScriptedDataOperationFactory( + scripts: [ + { + firstFetchStarted.signal() + guard firstFetchRelease.wait( + timeout: .now() + 2 + ) == .success else { + throw ChainSyncCompatibilityTestError.gateTimedOut + } + throw ChainSyncCompatibilityTestError.fetchFailed + }, + { data } + ] + ) + let repository = ScriptedChainRepository() + let eventCenter = RecordingChainSyncEventCenter() + let failureSignal = DispatchSemaphore(value: 0) + let completionSignal = DispatchSemaphore(value: 0) + eventCenter.onEvent = { event in + if event is ChainSyncDidFail { + failureSignal.signal() + } else if event is ChainSyncDidComplete { + completionSignal.signal() + } + } + let cooldownTimer = ManualCountdownTimer() + let cooldownTimerQueue = DispatchQueue( + label: "jp.co.soramitsu.fearless.tests.cooldown.after-failure" + ) + let service = makeService( + dataFetchFactory: dataFactory, + repository: AnyDataProviderRepository(repository), + eventCenter: eventCenter, + cooldownTimer: cooldownTimer, + cooldownTimerQueue: cooldownTimerQueue + ) + + service.syncUp() + waitForSignal(firstFetchStarted, description: "first gated fetch") + drain(cooldownTimerQueue) + cooldownTimer.expire() + + DispatchQueue.concurrentPerform(iterations: 32) { _ in + service.syncUp() + } + XCTAssertEqual( + eventCenter.startCount, + 1, + "timer expiry must not release an in-flight pipeline" + ) + XCTAssertEqual(dataFactory.fetchCallCount, 1) + + firstFetchRelease.signal() + waitForSignal( + failureSignal, + description: "failure after cooldown expiry" + ) + DispatchQueue.concurrentPerform(iterations: 32) { _ in + service.syncUp() + } + waitForSignal( + completionSignal, + description: "replacement after expired failure" + ) + + XCTAssertEqual(eventCenter.startCount, 2) + XCTAssertEqual(dataFactory.fetchCallCount, 2) + XCTAssertEqual(eventCenter.failureCount, 1) + XCTAssertEqual(eventCenter.completionCount, 1) + } + + func testStaleStopCallbackCannotClearNewerCooldown() throws { + let data = try JSONEncoder().encode([ + makeValidChain(generatingAssets: 1, addressPrefix: 42) + ]) + let dataFactory = ScriptedDataOperationFactory( + scripts: [ + { data }, + { data }, + { data } + ] + ) + let repository = ScriptedChainRepository() + let eventCenter = RecordingChainSyncEventCenter() + let completionSignal = DispatchSemaphore(value: 0) + eventCenter.onEvent = { event in + if event is ChainSyncDidComplete { + completionSignal.signal() + } + } + let cooldownTimer = ManualCountdownTimer() + let cooldownTimerQueue = DispatchQueue( + label: "jp.co.soramitsu.fearless.tests.cooldown.stale-stop" + ) + let service = makeService( + dataFetchFactory: dataFactory, + repository: AnyDataProviderRepository(repository), + eventCenter: eventCenter, + cooldownTimer: cooldownTimer, + cooldownTimerQueue: cooldownTimerQueue + ) + + service.syncUp() + waitForSignal(completionSignal, description: "first success") + drain(cooldownTimerQueue) + cooldownTimer.expire() + + service.syncUp() + waitForSignal(completionSignal, description: "second success") + drain(cooldownTimerQueue) + XCTAssertEqual(cooldownTimer.startCount, 2) + + cooldownTimer.emitStaleStopWhileRunning() + DispatchQueue.concurrentPerform(iterations: 32) { _ in + service.syncUp() + } + XCTAssertEqual( + eventCenter.startCount, + 2, + "a stale callback must not clear the current cooldown" + ) + XCTAssertEqual(dataFactory.fetchCallCount, 2) + + cooldownTimer.expire() + DispatchQueue.concurrentPerform(iterations: 32) { _ in + service.syncUp() + } + waitForSignal( + completionSignal, + description: "third success after real expiry" + ) + + XCTAssertEqual(eventCenter.startCount, 3) + XCTAssertEqual(dataFactory.fetchCallCount, 3) + XCTAssertEqual(eventCenter.failureCount, 0) + XCTAssertEqual(eventCenter.completionCount, 3) + } + + func testSanitizerRejectsDuplicateRemoteIdentifiers() { + let chain = makeValidChain( + generatingAssets: 1, + addressPrefix: 42 + ) + + XCTAssertThrowsError( + try ChainSyncService.sanitizingRemoteChains([chain, chain]) + ) { error in + guard + let syncError = error as? ChainSyncServiceError, + case let .duplicateRemoteChainIdentifier(identifier) = + syncError + else { + return XCTFail("Unexpected error: \(error)") + } + + XCTAssertEqual(identifier, chain.chainId) + } + } + + func testSanitizerRejectsBlankNoncanonicalAndReservedIdentifiers() { + let chain = makeValidChain( + generatingAssets: 1, + addressPrefix: 42 + ) + let invalidIdentifiers = [ + "", + " \n", + " leading-space", + "trailing-space ", + ChainModelMapper.quarantinedChainIdentifierPrefix + "remote" + ] + + for identifier in invalidIdentifiers { + let invalidChain = copy( + chain, + chainId: identifier + ) + + XCTAssertThrowsError( + try ChainSyncService.sanitizingRemoteChains( + [invalidChain] + ), + "identifier \(identifier.debugDescription) must fail" + ) + } + } + + func testSanitizerRejectsBlankRemoteName() { + let chain = makeValidChain( + generatingAssets: 1, + addressPrefix: 42 + ) + let invalidChain = copy(chain, name: " \n\t") + + XCTAssertThrowsError( + try ChainSyncService.sanitizingRemoteChains([invalidChain]) + ) + } + + func testSanitizerAllowsDisabledChainWithoutNodes() throws { + let chain = makeValidChain( + generatingAssets: 1, + addressPrefix: 42 + ) + let disabledChain = copy( + chain, + disabled: true, + nodes: [] + ) + + let sanitized = try ChainSyncService.sanitizingRemoteChains( + [disabledChain] + ) + + XCTAssertEqual(sanitized.count, 1) + XCTAssertTrue(try XCTUnwrap(sanitized.first).disabled) + XCTAssertTrue(try XCTUnwrap(sanitized.first).nodes.isEmpty) + } + + func testSanitizerDeterministicallyDeduplicatesNodeCredentialMetadata() + throws { + let chain = makeValidChain( + generatingAssets: 1, + addressPrefix: 42 + ) + let url = try XCTUnwrap(URL(string: "wss://same-node.example")) + let expectedAPIKey = ChainNodeModel.ApiKey( + queryName: "a-query", + keyName: "a-key" + ) + let nodes: Set = [ + ChainNodeModel( + url: url, + name: "Same Node", + apikey: .init( + queryName: "z-query", + keyName: "z-key" + ) + ), + ChainNodeModel( + url: url, + name: "Same Node", + apikey: expectedAPIKey + ) + ] + + for _ in 0 ..< 10 { + let sanitized = try ChainSyncService + .sanitizingRemoteChains([ + copy(chain, nodes: nodes) + ]) + let node = try XCTUnwrap( + sanitized.first?.nodes.first + ) + + XCTAssertEqual(node.apikey, expectedAPIKey) + } + } + + func testSanitizerRejectsEthereumChainWithOnlyIncompatibleSchemes() + throws { + let baseChain = makeValidChain( + generatingAssets: 1, + addressPrefix: 42 + ) + let insecureNodes: Set = [ + ChainNodeModel( + url: try XCTUnwrap( + URL(string: "http://rpc.example") + ), + name: "HTTP", + apikey: nil + ), + ChainNodeModel( + url: try XCTUnwrap(URL(string: "ws://rpc.example")), + name: "WS", + apikey: nil + ) + ] + let ethereumChain = copy( + baseChain, + nodes: insecureNodes, + options: [.ethereum] + ) + + XCTAssertThrowsError( + try ChainSyncService.sanitizingRemoteChains( + [ethereumChain] + ) + ) + } + + func testSanitizerKeepsOnlyRuntimeCompatibleEthereumNodes() throws { + let baseChain = makeValidChain( + generatingAssets: 1, + addressPrefix: 42 + ) + let httpsNode = ChainNodeModel( + url: try XCTUnwrap(URL(string: "https://rpc.example")), + name: "HTTPS", + apikey: nil + ) + let httpNode = ChainNodeModel( + url: try XCTUnwrap(URL(string: "http://rpc.example")), + name: "HTTP", + apikey: nil + ) + let ethereumChain = copy( + baseChain, + nodes: [httpsNode, httpNode], + options: [.ethereum] + ) + + let sanitized = try ChainSyncService.sanitizingRemoteChains( + [ethereumChain] + ) + + XCTAssertEqual( + try XCTUnwrap(sanitized.first).nodes, + Set([httpsNode]) + ) + } + + func testPreservingPreferencesDropsRuntimeIncompatibleCustomSelection() + throws { + let substrateRemote = makeValidChain( + generatingAssets: 1, + addressPrefix: 42 + ) + let incompatibleSubstrateNode = ChainNodeModel( + url: try XCTUnwrap( + URL(string: "https://not-a-websocket.example") + ), + name: "Wrong protocol", + apikey: nil + ) + let substrateLocal = substrateRemote + .replacingCustomNodes([incompatibleSubstrateNode]) + .replacingSelectedNode(incompatibleSubstrateNode) + + let merged = ChainSyncService.preservingLocalNodePreferences( + remoteChains: [substrateRemote], + localChains: [substrateLocal] + ) + + XCTAssertEqual( + try XCTUnwrap(merged.first).customNodes, + Set() + ) + XCTAssertNil(try XCTUnwrap(merged.first).selectedNode) + } + + func testSanitizerStripsRemotePreferencesForBrandNewChain() throws { + let chain = makeValidChain( + generatingAssets: 1, + addressPrefix: 42 + ) + let injectedNode = ChainNodeModel( + url: try XCTUnwrap( + URL(string: "wss://remote-injection.example") + ), + name: "Injected", + apikey: nil + ) + let injectedChain = chain + .replacingCustomNodes([injectedNode]) + .replacingSelectedNode(injectedNode) + + let sanitized = try ChainSyncService.sanitizingRemoteChains( + [injectedChain] + ) + + XCTAssertEqual( + try XCTUnwrap(sanitized.first).customNodes, + Set() + ) + XCTAssertNil(try XCTUnwrap(sanitized.first).selectedNode) + } + + private func copy( + _ chain: ChainModel, + chainId: String? = nil, + name: String? = nil, + disabled: Bool? = nil, + nodes: Set? = nil, + options: [ChainOptions]? = nil + ) -> ChainModel { + ChainModel( + rank: chain.rank, + disabled: disabled ?? chain.disabled, + chainId: chainId ?? chain.chainId, + parentId: chain.parentId, + paraId: chain.paraId, + name: name ?? chain.name, + assets: chain.assets, + xcm: chain.xcm, + nodes: nodes ?? chain.nodes, + addressPrefix: chain.addressPrefix, + types: chain.types, + icon: chain.icon, + options: options ?? chain.options, + externalApi: chain.externalApi, + selectedNode: nil, + customNodes: nil, + iosMinAppVersion: chain.iosMinAppVersion, + identityChain: chain.identityChain + ) + } + + private func makeValidChain( + generatingAssets count: Int, + addressPrefix: UInt16 + ) -> ChainModel { + let chain = ChainModelGenerator.generateChain( + generatingAssets: count, + addressPrefix: addressPrefix + ) + let node = ChainNodeModel( + url: URL( + string: "wss://node.example/\(chain.chainId)" + )!, + name: "Valid substrate node", + apikey: nil + ) + + return chain.replacingNodes([node]) + } + + private func makeService( + data: Data, + repository: AnyDataProviderRepository, + eventCenter: EventCenterProtocol, + malformedChainCleanupOperationFactory: (() -> BaseOperation)? = nil + ) -> ChainSyncService { + makeService( + dataFetchFactory: StaticDataOperationFactory(data: data), + repository: repository, + eventCenter: eventCenter, + malformedChainCleanupOperationFactory: + malformedChainCleanupOperationFactory + ) + } + + private func makeService( + dataFetchFactory: DataOperationFactoryProtocol, + repository: AnyDataProviderRepository, + eventCenter: EventCenterProtocol, + malformedChainCleanupOperationFactory: (() -> BaseOperation)? = nil, + cooldownTimer: CountdownTimerProtocol = CountdownTimer( + notificationInterval: 300 + ), + cooldownTimerQueue: DispatchQueue = .main + ) -> ChainSyncService { + ChainSyncService( + chainsUrl: URL(string: "https://chains.example/chains.json")!, + dataFetchFactory: dataFetchFactory, + repository: repository, + eventCenter: eventCenter, + operationQueue: OperationQueue(), + retryStrategy: NeverReconnectStrategy(), + applicationHandler: ApplicationHandler(), + malformedChainCleanupOperationFactory: + malformedChainCleanupOperationFactory, + cooldownTimer: cooldownTimer, + cooldownTimerQueue: cooldownTimerQueue + ) + } + + private func waitForSignal( + _ semaphore: DispatchSemaphore, + description: String, + timeout: TimeInterval = 2, + file: StaticString = #filePath, + line: UInt = #line + ) { + guard semaphore.wait( + timeout: .now() + timeout + ) == .success else { + return XCTFail( + "Timed out waiting for \(description)", + file: file, + line: line + ) + } + } + + private func drain( + _ queue: DispatchQueue, + file: StaticString = #filePath, + line: UInt = #line + ) { + let drained = DispatchSemaphore(value: 0) + queue.async { + drained.signal() + } + waitForSignal( + drained, + description: "serial cooldown timer queue", + file: file, + line: line + ) + } +} + +private enum ChainSyncCompatibilityTestError: Error { + case fetchFailed + case saveFailed + case cleanupFailed + case gateTimedOut + case unexpectedFetch +} + +private struct StaticDataOperationFactory: DataOperationFactoryProtocol { + let data: Data + + func fetchData(from _: URL) -> BaseOperation { + ClosureOperation { data } + } +} + +private final class CountingDataOperationFactory: + DataOperationFactoryProtocol { + private let lock = NSLock() + private let data: Data + private let firstFetchGate: DispatchSemaphore? + private let onFirstFetchStart: (() -> Void)? + private var fetchCalls = 0 + + var fetchCallCount: Int { + lock.with { fetchCalls } + } + + init( + data: Data, + firstFetchGate: DispatchSemaphore? = nil, + onFirstFetchStart: (() -> Void)? = nil + ) { + self.data = data + self.firstFetchGate = firstFetchGate + self.onFirstFetchStart = onFirstFetchStart + } + + func fetchData(from _: URL) -> BaseOperation { + let callIndex = lock.with { + fetchCalls += 1 + return fetchCalls + } + + return ClosureOperation { + if callIndex == 1, let firstFetchGate = self.firstFetchGate { + self.onFirstFetchStart?() + firstFetchGate.wait() + } + + return self.data + } + } +} + +private final class ScriptedDataOperationFactory: + DataOperationFactoryProtocol { + typealias FetchScript = () throws -> Data + + private let lock = NSLock() + private let scripts: [FetchScript] + private var fetchCalls = 0 + + var fetchCallCount: Int { + lock.with { fetchCalls } + } + + init(scripts: [FetchScript]) { + self.scripts = scripts + } + + func fetchData(from _: URL) -> BaseOperation { + let script: FetchScript = lock.with { + let index = fetchCalls + fetchCalls += 1 + + guard scripts.indices.contains(index) else { + return { + throw ChainSyncCompatibilityTestError.unexpectedFetch + } + } + + return scripts[index] + } + + return ClosureOperation { + try script() + } + } +} + +private final class ManualCountdownTimer: CountdownTimerProtocol { + weak var delegate: CountdownTimerDelegate? + + private let lock = NSLock() + private var currentState: CountdownTimerState = .stopped + private var currentRemainedInterval: TimeInterval = 0 + private var starts = 0 + private var stops = 0 + + let notificationInterval: TimeInterval + + var state: CountdownTimerState { + lock.with { currentState } + } + + var remainedInterval: TimeInterval { + lock.with { currentRemainedInterval } + } + + var startCount: Int { + lock.with { starts } + } + + var stopCount: Int { + lock.with { stops } + } + + init(notificationInterval: TimeInterval = 300) { + self.notificationInterval = notificationInterval + } + + func start( + with interval: TimeInterval, + runLoop _: RunLoop, + mode _: RunLoop.Mode + ) { + stop() + + lock.with { + currentState = .inProgress + currentRemainedInterval = interval + starts += 1 + } + delegate?.didStart(with: interval) + } + + func stop() { + let stoppedInterval: TimeInterval? = lock.with { + switch currentState { + case .inProgress, .paused: + let interval = currentRemainedInterval + currentState = .stopped + currentRemainedInterval = 0 + stops += 1 + return interval + case .stopped: + return nil + } + } + + if let stoppedInterval { + delegate?.didStop(with: stoppedInterval) + } + } + + func expire() { + let shouldNotify = lock.with { + guard case .inProgress = currentState else { + return false + } + + currentState = .stopped + currentRemainedInterval = 0 + stops += 1 + return true + } + + if shouldNotify { + delegate?.didStop(with: 0) + } + } + + func emitStaleStopWhileRunning() { + let interval = remainedInterval + delegate?.didStop(with: interval) + } +} + +private final class LockedCounter { + private let lock = NSLock() + private var count = 0 + + var value: Int { + lock.with { count } + } + + func increment() -> Int { + lock.with { + count += 1 + return count + } + } +} + +private struct NeverReconnectStrategy: ReconnectionStrategyProtocol { + func reconnectAfter(attempt _: Int) -> TimeInterval? { + nil + } +} + +private final class RecordingChainSyncEventCenter: EventCenterProtocol { + private let lock = NSLock() + private var failures: [Error] = [] + private var completions = 0 + private var starts = 0 + + var onEvent: ((EventProtocol) -> Void)? + + var failureCount: Int { + lock.with { failures.count } + } + + var completionCount: Int { + lock.with { completions } + } + + var startCount: Int { + lock.with { starts } + } + + var lastFailure: Error? { + lock.with { failures.last } + } + + func notify(with event: EventProtocol) { + lock.with { + if event is ChainSyncDidStart { + starts += 1 + } else if let failure = event as? ChainSyncDidFail { + failures.append(failure.error) + } else if event is ChainSyncDidComplete { + completions += 1 + } + } + + onEvent?(event) + } + + func add(observer _: EventVisitorProtocol, dispatchIn _: DispatchQueue?) {} + + func remove(observer _: EventVisitorProtocol) {} +} + +private final class SaveFailingChainRepository: DataProviderRepositoryProtocol { + typealias Model = ChainModel + + private let lock = NSLock() + private let error: Error + private var saves = 0 + + var saveCallCount: Int { + lock.with { saves } + } + + init(error: Error) { + self.error = error + } + + func fetchOperation( + by _: @escaping () throws -> [String], + options _: RepositoryFetchOptions + ) -> BaseOperation<[ChainModel]> { + ClosureOperation { [] } + } + + func fetchOperation( + by _: @escaping () throws -> String, + options _: RepositoryFetchOptions + ) -> BaseOperation { + ClosureOperation { nil } + } + + func fetchAllOperation( + with _: RepositoryFetchOptions + ) -> BaseOperation<[ChainModel]> { + ClosureOperation { [] } + } + + func fetchOperation( + by _: RepositorySliceRequest, + options _: RepositoryFetchOptions + ) -> BaseOperation<[ChainModel]> { + ClosureOperation { [] } + } + + func saveOperation( + _: @escaping () throws -> [ChainModel], + _: @escaping () throws -> [String] + ) -> BaseOperation { + lock.with { + saves += 1 + } + + return ClosureOperation { [error] in + throw error + } + } + + func saveBatchOperation( + _ updateModelsBlock: @escaping () throws -> [ChainModel], + _ deleteIdsBlock: @escaping () throws -> [String] + ) -> BaseOperation { + saveOperation(updateModelsBlock, deleteIdsBlock) + } + + func replaceOperation( + _: @escaping () throws -> [ChainModel] + ) -> BaseOperation { + ClosureOperation { [error] in + throw error + } + } + + func fetchCountOperation() -> BaseOperation { + ClosureOperation { 0 } + } + + func deleteAllOperation() -> BaseOperation { + ClosureOperation { () } + } +} + +private final class ScriptedChainRepository: DataProviderRepositoryProtocol { + typealias Model = ChainModel + + private let lock = NSLock() + private let fetchAllError: Error? + private let localChains: [ChainModel] + private var fetchAllCalls = 0 + private var replaceCalls = 0 + private var saveCalls = 0 + private var saved: [ChainModel] = [] + private var deleted: [String] = [] + + var replaceCallCount: Int { + lock.with { replaceCalls } + } + + var fetchAllCallCount: Int { + lock.with { fetchAllCalls } + } + + var saveCallCount: Int { + lock.with { saveCalls } + } + + var savedModels: [ChainModel] { + lock.with { saved } + } + + var deletedIdentifiers: [String] { + lock.with { deleted } + } + + init( + fetchAllError: Error? = nil, + localChains: [ChainModel] = [] + ) { + self.fetchAllError = fetchAllError + self.localChains = localChains + } + + func fetchOperation( + by _: @escaping () throws -> [String], + options _: RepositoryFetchOptions + ) -> BaseOperation<[ChainModel]> { + ClosureOperation { [] } + } + + func fetchOperation( + by _: @escaping () throws -> String, + options _: RepositoryFetchOptions + ) -> BaseOperation { + ClosureOperation { nil } + } + + func fetchAllOperation( + with _: RepositoryFetchOptions + ) -> BaseOperation<[ChainModel]> { + lock.with { + fetchAllCalls += 1 + } + + return ClosureOperation { [fetchAllError] in + if let fetchAllError { + throw fetchAllError + } + + return self.localChains + } + } + + func fetchOperation( + by _: RepositorySliceRequest, + options _: RepositoryFetchOptions + ) -> BaseOperation<[ChainModel]> { + ClosureOperation { [] } + } + + func saveOperation( + _ updateModelsBlock: @escaping () throws -> [ChainModel], + _ deleteIdsBlock: @escaping () throws -> [String] + ) -> BaseOperation { + ClosureOperation { [weak self] in + guard let self else { + throw BaseOperationError.parentOperationCancelled + } + + let models = try updateModelsBlock() + let identifiers = try deleteIdsBlock() + lock.with { + self.saveCalls += 1 + self.saved = models + self.deleted = identifiers + } + } + } + + func saveBatchOperation( + _: @escaping () throws -> [ChainModel], + _: @escaping () throws -> [String] + ) -> BaseOperation { + ClosureOperation { () } + } + + func replaceOperation( + _ modelsBlock: @escaping () throws -> [ChainModel] + ) -> BaseOperation { + ClosureOperation { [weak self] in + guard let self else { + throw BaseOperationError.parentOperationCancelled + } + + _ = try modelsBlock() + lock.with { + self.replaceCalls += 1 + } + } + } + + func fetchCountOperation() -> BaseOperation { + ClosureOperation { 0 } + } + + func deleteAllOperation() -> BaseOperation { + ClosureOperation { () } + } } diff --git a/fearlessTests/Common/Services/ChainRegistry/MockConnection.swift b/fearlessTests/Common/Services/ChainRegistry/MockConnection.swift index 8fb1a4308c..3a7beb8793 100644 --- a/fearlessTests/Common/Services/ChainRegistry/MockConnection.swift +++ b/fearlessTests/Common/Services/ChainRegistry/MockConnection.swift @@ -7,6 +7,14 @@ final class MockConnection: JSONRPCEngine { var connectionName: String? var url: URL? var pendingEngineRequests: [JSONRPCRequest] { [] } + private let disconnectLock = NSLock() + private var disconnectCalls = 0 + + var disconnectCallCount: Int { + disconnectLock.lock() + defer { disconnectLock.unlock() } + return disconnectCalls + } private var nextId: UInt16 = 1 private struct AnySubscription { @@ -60,7 +68,11 @@ final class MockConnection: JSONRPCEngine { func reconnect(url: URL) { self.url = url } func connectIfNeeded() {} - func disconnectIfNeeded() {} + func disconnectIfNeeded() { + disconnectLock.lock() + disconnectCalls += 1 + disconnectLock.unlock() + } func unsubsribe(_ identifier: UInt16) throws {} // MARK: - Test helpers diff --git a/fearlessTests/Common/Storage/ChainModelMapperTests.swift b/fearlessTests/Common/Storage/ChainModelMapperTests.swift index 7a6334890a..1b05e7ab8f 100644 --- a/fearlessTests/Common/Storage/ChainModelMapperTests.swift +++ b/fearlessTests/Common/Storage/ChainModelMapperTests.swift @@ -1,9 +1,78 @@ import XCTest @testable import fearless +import CoreData import RobinHood import SSFModels final class ChainModelMapperTests: XCTestCase { + func testStartupWrapperConvertsMandatoryGetterExceptionToSanitizedSwiftError() { + let entityDescription = makeEntityDescription( + managedObjectClass: CDChain.self, + properties: [] + ) + let entity = CDChain( + entity: entityDescription, + insertInto: nil + ) + let startupMapper: AnyCoreDataMapper = + AnyCoreDataMapper(ChainModelMapper()) + + XCTAssertThrowsError( + try startupMapper.transform(entity: entity) + ) { error in + XCTAssertEqual( + error as? SafeTransformableValueReaderError, + .objectiveCException + ) + } + } + + func testThrowingNodeRelationshipFailsClosedAsSwiftError() throws { + let nodeDescription = makeEntityDescription( + managedObjectClass: CDChainNode.self, + properties: [] + ) + let nodes = NSRelationshipDescription() + nodes.name = "nodes" + nodes.destinationEntity = nodeDescription + nodes.minCount = 0 + nodes.maxCount = 0 + nodes.isOptional = true + nodes.deleteRule = .nullifyDeleteRule + + let chainDescription = makeEntityDescription( + managedObjectClass: CDChain.self, + properties: [ + makeAttribute(name: "chainId", type: .stringAttributeType), + makeAttribute(name: "name", type: .stringAttributeType), + nodes + ] + ) + let context = try makeContext( + entities: [chainDescription, nodeDescription] + ) + let entity = CDChain( + entity: chainDescription, + insertInto: context + ) + entity.setValue("throwing-node-chain", forKey: "chainId") + entity.setValue("Throwing node chain", forKey: "name") + let throwingNode = CDChainNode( + entity: nodeDescription, + insertInto: context + ) + entity.setValue(NSSet(object: throwingNode), forKey: "nodes") + + XCTAssertThrowsError( + try ChainModelMapper().transform(entity: entity) + ) { error in + XCTAssertEqual( + error as? SafeTransformableValueReaderError, + .objectiveCException + ) + } + } + func testSaveAndFetchPreservesParaIdAndIdentityChain() throws { let facade = SubstrateStorageTestFacade() let repository: CoreDataRepository = facade.createRepository( @@ -48,4 +117,1689 @@ final class ChainModelMapperTests: XCTestCase { XCTAssertEqual(fetched.paraId, chain.paraId) XCTAssertEqual(fetched.identityChain, chain.identityChain) } + + func testTransformQuarantinesChainWithoutIdentifier() throws { + let context = try createContext() + let entity = CDChain(context: context) + entity.name = "Malformed chain" + + let chain = try ChainModelMapper().transform(entity: entity) + + XCTAssertTrue( + chain.chainId.hasPrefix( + ChainModelMapper.quarantinedChainIdentifierPrefix + ) + ) + XCTAssertEqual(chain.name, ChainModelMapper.quarantinedChainName) + XCTAssertTrue(chain.disabled) + XCTAssertTrue(chain.nodes.isEmpty) + XCTAssertTrue(chain.assets.isEmpty) + } + + func testTransformQuarantinesNoncanonicalIdentifierUntilCleanup() throws { + let context = try createContext() + let entity = makeValidChainEntity(in: context) + entity.chainId = " valid-chain " + + let chain = try ChainModelMapper().transform(entity: entity) + + XCTAssertTrue( + chain.chainId.hasPrefix( + ChainModelMapper.quarantinedChainIdentifierPrefix + ) + ) + XCTAssertTrue(chain.disabled) + } + + func testTransformQuarantinesNameButKeepsIdentifierAndPreferences() throws { + let context = try createContext() + let entity = CDChain(context: context) + entity.chainId = "malformed-chain" + let customNode = CDChainNode(context: context) + customNode.url = try XCTUnwrap(URL(string: "wss://custom.example")) + customNode.name = "Custom" + entity.customNodes = NSSet(object: customNode) + entity.selectedNode = customNode + + let chain = try ChainModelMapper().transform(entity: entity) + + XCTAssertEqual(chain.chainId, "malformed-chain") + XCTAssertEqual(chain.name, ChainModelMapper.quarantinedChainName) + XCTAssertTrue(chain.disabled) + XCTAssertEqual(chain.customNodes, Set([try makeNode(from: customNode)])) + XCTAssertEqual(chain.selectedNode, try makeNode(from: customNode)) + } + + func testTransformSkipsDefaultNodeWithoutURL() throws { + let context = try createContext() + let entity = makeValidChainEntity(in: context) + let node = CDChainNode(context: context) + node.name = "Malformed node" + entity.nodes = NSSet(object: node) + + let chain = try ChainModelMapper().transform(entity: entity) + + XCTAssertTrue(chain.nodes.isEmpty) + } + + func testTransformSkipsSelectedNodeWithoutName() throws { + let context = try createContext() + let entity = makeValidChainEntity(in: context) + let node = CDChainNode(context: context) + node.url = try XCTUnwrap(URL(string: "wss://malformed.example")) + entity.selectedNode = node + + let chain = try ChainModelMapper().transform(entity: entity) + + XCTAssertNil(chain.selectedNode) + } + + func testTransformSkipsSelectedNodeWithUnusableURL() throws { + let context = try createContext() + let unusableURLs = [ + try XCTUnwrap(URL(string: "http:relative")), + try XCTUnwrap(URL(string: "relative/node")), + try XCTUnwrap(URL(string: "file:///tmp/not-a-node")), + try XCTUnwrap(URL(string: "data:text/plain,not-a-node")) + ] + + for (index, unusableURL) in unusableURLs.enumerated() { + let entity = makeValidChainEntity(in: context) + entity.chainId = "invalid-node-\(index)" + let node = CDChainNode(context: context) + node.url = unusableURL + node.name = "Unusable" + entity.selectedNode = node + + let chain = try ChainModelMapper().transform(entity: entity) + + XCTAssertNil(chain.selectedNode, "\(unusableURL) must be ignored") + } + } + + func testTransformAcceptsSubstrateWebSocketNodeURLs() throws { + let context = try createContext() + let supportedURLs = [ + try XCTUnwrap(URL(string: "wss://node.example")), + try XCTUnwrap(URL(string: "ws://127.0.0.1:9944")) + ] + + for (index, supportedURL) in supportedURLs.enumerated() { + let entity = makeValidChainEntity(in: context) + entity.chainId = "valid-node-\(index)" + let node = CDChainNode(context: context) + node.url = supportedURL + node.name = "Supported" + entity.nodes = NSSet(object: node) + entity.selectedNode = node + + let chain = try ChainModelMapper().transform(entity: entity) + + XCTAssertEqual(chain.selectedNode?.url, supportedURL) + XCTAssertFalse(chain.disabled) + } + } + + func testTransformRejectsHTTPNodesForSubstrateRuntime() throws { + let context = try createContext() + let incompatibleURLs = [ + try XCTUnwrap(URL(string: "https://node.example")), + try XCTUnwrap(URL(string: "http://localhost:8080")) + ] + + for (index, incompatibleURL) in incompatibleURLs.enumerated() { + let entity = makeValidChainEntity(in: context) + entity.chainId = "incompatible-substrate-\(index)" + let node = CDChainNode(context: context) + node.url = incompatibleURL + node.name = "Wrong runtime protocol" + entity.nodes = NSSet(object: node) + entity.selectedNode = node + + let chain = try ChainModelMapper().transform(entity: entity) + + XCTAssertTrue(chain.nodes.isEmpty) + XCTAssertNil(chain.selectedNode) + XCTAssertTrue(chain.disabled) + } + } + + func testTransformAcceptsOnlySecureEthereumRuntimeSchemes() throws { + let context = try createContext() + let candidates: [(String, Bool)] = [ + ("https://rpc.example", true), + ("wss://rpc.example", true), + ("http://rpc.example", false), + ("ws://rpc.example", false) + ] + + for (index, candidate) in candidates.enumerated() { + let entity = makeValidChainEntity(in: context) + entity.chainId = "ethereum-runtime-\(index)" + entity.options = NSArray(object: ChainOptions.ethereum.rawValue) + let node = CDChainNode(context: context) + node.url = try XCTUnwrap(URL(string: candidate.0)) + node.name = "Ethereum RPC" + entity.nodes = NSSet(object: node) + entity.selectedNode = node + + let chain = try ChainModelMapper().transform(entity: entity) + + if candidate.1 { + XCTAssertEqual(chain.selectedNode?.url, node.url) + XCTAssertFalse(chain.disabled) + } else { + XCTAssertTrue(chain.nodes.isEmpty) + XCTAssertNil(chain.selectedNode) + XCTAssertTrue(chain.disabled) + } + } + } + + func testTransformAppliesStrictTonServerShape() throws { + let context = try createContext() + let candidates: [(String, Bool)] = [ + ("https://tonapi.io", true), + ("https://tonapi.io/", true), + ("https://tonapi.io/path", false), + ("https://user@tonapi.io", false), + ("http://tonapi.io", false), + ("wss://tonapi.io", false) + ] + + for (index, candidate) in candidates.enumerated() { + let entity = makeValidChainEntity(in: context) + entity.chainId = "ton-runtime-\(index)" + entity.name = "TON Mainnet" + let node = CDChainNode(context: context) + node.url = try XCTUnwrap(URL(string: candidate.0)) + node.name = "TON API" + entity.nodes = NSSet(object: node) + entity.selectedNode = node + + let chain = try ChainModelMapper().transform(entity: entity) + + if candidate.1 { + XCTAssertEqual(chain.selectedNode?.url, node.url) + XCTAssertFalse(chain.disabled) + } else { + XCTAssertTrue(chain.nodes.isEmpty) + XCTAssertNil(chain.selectedNode) + XCTAssertTrue(chain.disabled) + } + } + } + + func testTransformKeepsValidNodesWhileSkippingMalformedOnes() throws { + let context = try createContext() + let entity = makeValidChainEntity(in: context) + let validNode = CDChainNode(context: context) + validNode.url = try XCTUnwrap(URL(string: "wss://valid.example")) + validNode.name = "Valid" + let malformedNode = CDChainNode(context: context) + malformedNode.name = "Malformed" + entity.nodes = NSSet(array: [malformedNode, validNode]) + + let chain = try ChainModelMapper().transform(entity: entity) + + XCTAssertEqual(chain.nodes.count, 1) + XCTAssertEqual(chain.nodes.first?.url, validNode.url) + } + + func testPopulateRoundTripPreservesValidStandaloneSelectedNode() throws { + let context = try createContext() + let entity = makeValidChainEntity(in: context) + let defaultNode = ChainNodeModel( + url: try XCTUnwrap( + URL(string: "wss://default-round-trip.example") + ), + name: "Default", + apikey: nil + ) + let standaloneSelection = ChainNodeModel( + url: try XCTUnwrap( + URL(string: "wss://standalone-selection.example") + ), + name: "Standalone selection", + apikey: nil + ) + let model = ChainModel( + rank: 1, + disabled: false, + chainId: "standalone-selection-chain", + paraId: nil, + name: "Standalone selection chain", + xcm: nil, + nodes: [defaultNode], + addressPrefix: 42, + icon: nil, + selectedNode: standaloneSelection, + iosMinAppVersion: nil, + identityChain: nil + ) + + try ChainModelMapper().populate( + entity: entity, + from: model, + using: context + ) + + let selectedEntity = try XCTUnwrap(entity.selectedNode) + XCTAssertFalse( + (entity.nodes as? Set)? + .contains(selectedEntity) ?? true + ) + XCTAssertFalse( + (entity.customNodes as? Set)? + .contains(selectedEntity) ?? false + ) + + let transformed = try ChainModelMapper().transform(entity: entity) + + XCTAssertEqual(transformed.nodes, Set([defaultNode])) + XCTAssertEqual(transformed.selectedNode, standaloneSelection) + XCTAssertFalse(transformed.disabled) + } + + func testTransformDropsUnsupportedStandaloneSelectionButKeepsHealthyChain() + throws { + let context = try createContext() + let entity = makeValidChainEntity(in: context) + let defaultNode = CDChainNode(context: context) + defaultNode.url = try XCTUnwrap( + URL(string: "wss://healthy-default.example") + ) + defaultNode.name = "Healthy default" + entity.nodes = NSSet(object: defaultNode) + + let unsupportedSelection = CDChainNode(context: context) + unsupportedSelection.url = try XCTUnwrap( + URL(string: "https://not-a-substrate-websocket.example") + ) + unsupportedSelection.name = "Unsupported standalone selection" + entity.selectedNode = unsupportedSelection + + let transformed = try ChainModelMapper().transform(entity: entity) + + XCTAssertEqual(transformed.nodes.count, 1) + XCTAssertEqual(transformed.nodes.first?.url, defaultNode.url) + XCTAssertNil(transformed.selectedNode) + XCTAssertFalse(transformed.disabled) + } + + func testTransformSkipsMalformedCachedAsset() throws { + let context = try createContext() + let entity = makeValidChainEntity(in: context) + let malformedAsset = CDAsset(context: context) + malformedAsset.name = "Missing identifier" + entity.assets = NSSet(object: malformedAsset) + + let chain = try ChainModelMapper().transform(entity: entity) + + XCTAssertTrue(chain.assets.isEmpty) + } + + func testTransformIgnoresRelationshipWithOnlyMalformedPrices() + throws { + let context = try createContext() + let entity = makeValidChainEntity(in: context) + let assetEntity = CDAsset(context: context) + let expectedAsset = ChainModelGenerator.generateAssetWithId( + "valid-asset", + symbol: "VAL", + assetPresicion: 12 + ) + try AssetModelMapper().populate( + entity: assetEntity, + from: expectedAsset, + using: context + ) + let malformedPrice = CDPriceData(context: context) + assetEntity.setValue( + NSSet(object: malformedPrice), + forKey: "priceData" + ) + entity.assets = NSSet(object: assetEntity) + + let chain = try ChainModelMapper().transform(entity: entity) + + XCTAssertEqual(chain.assets, Set([expectedAsset])) + } + + func testCachedPriceExtractionRejectsUnexpectedEntityBeforeReadingUndefinedKeys() { + let entity = NSEntityDescription() + entity.name = "UnexpectedCachedPrice" + entity.managedObjectClassName = NSStringFromClass( + NSManagedObject.self + ) + let model = NSManagedObjectModel() + model.entities = [entity] + let unexpectedRow = NSManagedObject( + entity: entity, + insertInto: nil + ) + + XCTAssertNil( + ChainModelMapper().createPriceData(from: unexpectedRow) + ) + } + + func testCachedPriceExtractionSkipsRowWhenAttributeReadRaisesException() { + let fixture = makeCachedPriceEntity( + managedObjectClass: ExceptionRaisingCachedPrice.self, + priceType: .stringAttributeType + ) + let row = ExceptionRaisingCachedPrice( + entity: fixture.entity, + insertInto: nil + ) + row.setValue("usd", forKey: "currencyId") + row.setValue("dot", forKey: "priceId") + + XCTAssertNil(ChainModelMapper().createPriceData(from: row)) + } + + func testCachedPriceExtractionPreservesLegacyDecimalWithoutOptionalFields() throws { + let fixture = makeCachedPriceEntity( + managedObjectClass: NSManagedObject.self, + priceType: .decimalAttributeType + ) + let row = NSManagedObject( + entity: fixture.entity, + insertInto: nil + ) + row.setValue("usd", forKey: "currencyId") + row.setValue("dot", forKey: "priceId") + row.setValue( + NSDecimalNumber(string: "12.5"), + forKey: "price" + ) + + let priceData = try XCTUnwrap( + ChainModelMapper().createPriceData(from: row) + ) + + XCTAssertEqual(priceData.currencyId, "usd") + XCTAssertEqual(priceData.priceId, "dot") + XCTAssertEqual(priceData.price, "12.5") + XCTAssertNil(priceData.fiatDayChange) + XCTAssertNil(priceData.coingeckoPriceId) + } + + func testTransformUsesSingleValidUnmatchedCachedPrice() throws { + let context = try createContext() + let entity = makeValidChainEntity(in: context) + let asset = makeAssetWithUnmatchedPricing(identifier: "single-price") + let assetEntity = CDAsset(context: context) + try AssetModelMapper().populate( + entity: assetEntity, + from: asset, + using: context + ) + let cachedPrice = makeCachedPrice( + currencyId: "unmatched-currency", + priceId: "unmatched-price", + price: "7.25", + fiatDayChange: "-0.5", + in: context + ) + assetEntity.setValue( + NSSet(object: cachedPrice), + forKey: "priceData" + ) + entity.assets = NSSet(object: assetEntity) + + let chain = try ChainModelMapper().transform(entity: entity) + let transformedAsset = try XCTUnwrap( + chain.assets.first { $0.id == asset.id } + ) + + XCTAssertEqual(transformedAsset.price, Decimal(string: "7.25")) + XCTAssertEqual( + transformedAsset.fiatDayChange, + Decimal(string: "-0.5") + ) + } + + func testTransformDeterministicallyChoosesCanonicalUnmatchedCachedPrice() + throws { + let priceFixtures = [ + ( + currencyId: "z-currency", + priceId: "z-price", + price: "99.5", + fiatDayChange: "9.5" + ), + ( + currencyId: "a-currency", + priceId: "a-price", + price: "1.25", + fiatDayChange: "-1.5" + ) + ] + + for shouldReverseInsertion in [false, true] { + let context = try createContext() + let entity = makeValidChainEntity(in: context) + entity.chainId = "two-prices-\(shouldReverseInsertion)" + let asset = makeAssetWithUnmatchedPricing( + identifier: "two-prices-\(shouldReverseInsertion)" + ) + let assetEntity = CDAsset(context: context) + try AssetModelMapper().populate( + entity: assetEntity, + from: asset, + using: context + ) + let orderedFixtures = shouldReverseInsertion + ? Array(priceFixtures.reversed()) + : priceFixtures + let cachedPrices = orderedFixtures.map { + makeCachedPrice( + currencyId: $0.currencyId, + priceId: $0.priceId, + price: $0.price, + fiatDayChange: $0.fiatDayChange, + in: context + ) + } + assetEntity.setValue( + NSSet(array: cachedPrices), + forKey: "priceData" + ) + entity.assets = NSSet(object: assetEntity) + + let chain = try ChainModelMapper().transform(entity: entity) + let transformedAsset = try XCTUnwrap( + chain.assets.first { $0.id == asset.id } + ) + + XCTAssertEqual( + transformedAsset.price, + Decimal(string: "1.25") + ) + XCTAssertEqual( + transformedAsset.fiatDayChange, + Decimal(string: "-1.5") + ) + } + } + + func testTransformUsesValidCachedPriceAmongMalformedRows() throws { + let context = try createContext() + let entity = makeValidChainEntity(in: context) + let asset = makeAssetWithUnmatchedPricing(identifier: "mixed-prices") + let assetEntity = CDAsset(context: context) + try AssetModelMapper().populate( + entity: assetEntity, + from: asset, + using: context + ) + let missingCurrency = CDPriceData(context: context) + missingCurrency.priceId = "missing-currency" + missingCurrency.price = "100" + let missingPriceIdentifier = CDPriceData(context: context) + missingPriceIdentifier.currencyId = "missing-price-id" + missingPriceIdentifier.price = "200" + let missingPrice = CDPriceData(context: context) + missingPrice.currencyId = "missing-price" + missingPrice.priceId = "missing-price" + let validPrice = makeCachedPrice( + currencyId: "valid-unmatched-currency", + priceId: "valid-unmatched-price", + price: "3.75", + fiatDayChange: "0.75", + in: context + ) + assetEntity.setValue( + NSSet( + array: [ + missingCurrency, + validPrice, + missingPriceIdentifier, + missingPrice + ] + ), + forKey: "priceData" + ) + entity.assets = NSSet(object: assetEntity) + + let chain = try ChainModelMapper().transform(entity: entity) + let transformedAsset = try XCTUnwrap( + chain.assets.first { $0.id == asset.id } + ) + + XCTAssertEqual(transformedAsset.price, Decimal(string: "3.75")) + XCTAssertEqual( + transformedAsset.fiatDayChange, + Decimal(string: "0.75") + ) + } + + func testTransformRejectsMalformedNonfiniteAndNegativeCachedPrices() + throws { + let invalidPrices = [ + "", + " ", + "not-a-price", + "NaN", + "Infinity", + "-Infinity", + "1e9999", + "-0.01", + "1,23", + "0x10" + ] + + for shouldReverseInsertion in [false, true] { + let context = try createContext() + let entity = makeValidChainEntity(in: context) + entity.chainId = "invalid-prices-\(shouldReverseInsertion)" + let asset = makeAssetWithUnmatchedPricing( + identifier: "invalid-prices-\(shouldReverseInsertion)" + ) + let assetEntity = CDAsset(context: context) + try AssetModelMapper().populate( + entity: assetEntity, + from: asset, + using: context + ) + let invalidRows = invalidPrices.enumerated().map { + makeCachedPrice( + currencyId: String( + format: "a-invalid-%02d", + $0.offset + ), + priceId: "asset-price", + price: $0.element, + fiatDayChange: "99", + in: context + ) + } + let validFallback = makeCachedPrice( + currencyId: "z-valid-currency", + priceId: "asset-price", + price: "4.25", + fiatDayChange: "-0.25", + in: context + ) + let rows = shouldReverseInsertion + ? [validFallback] + Array(invalidRows.reversed()) + : invalidRows + [validFallback] + assetEntity.setValue( + NSSet(array: Array(rows)), + forKey: "priceData" + ) + entity.assets = NSSet(object: assetEntity) + + let chain = try ChainModelMapper().transform(entity: entity) + let transformedAsset = try XCTUnwrap( + chain.assets.first { $0.id == asset.id } + ) + + XCTAssertEqual( + transformedAsset.price, + Decimal(string: "4.25") + ) + XCTAssertEqual( + transformedAsset.fiatDayChange, + Decimal(string: "-0.25") + ) + } + } + + func testTransformPreservesSupportedCachedPriceStringFormats() throws { + let fixtures: [(String, Decimal)] = [ + ("0", 0), + (".5", 0.5), + ("+7.5", 7.5), + ("1.25e2", 125), + (" 9.75 ", 9.75) + ] + + for (index, fixture) in fixtures.enumerated() { + let context = try createContext() + let entity = makeValidChainEntity(in: context) + entity.chainId = "valid-price-format-\(index)" + let asset = makeAssetWithUnmatchedPricing( + identifier: "valid-price-format-\(index)" + ) + let assetEntity = CDAsset(context: context) + try AssetModelMapper().populate( + entity: assetEntity, + from: asset, + using: context + ) + let cachedPrice = makeCachedPrice( + currencyId: "format-currency", + priceId: "asset-price", + price: fixture.0, + fiatDayChange: "0", + in: context + ) + assetEntity.setValue( + NSSet(object: cachedPrice), + forKey: "priceData" + ) + entity.assets = NSSet(object: assetEntity) + + let chain = try ChainModelMapper().transform(entity: entity) + let transformedAsset = try XCTUnwrap( + chain.assets.first { $0.id == asset.id } + ) + + XCTAssertEqual( + transformedAsset.price, + fixture.1, + "expected cached price format \(fixture.0)" + ) + } + } + + func testPopulateDeletesMalformedStaleNodesInsteadOfForceUnwrapping() throws { + let context = try createContext() + let entity = makeValidChainEntity(in: context) + let malformedDefaultNode = CDChainNode(context: context) + malformedDefaultNode.name = "Malformed default" + let malformedCustomNode = CDChainNode(context: context) + malformedCustomNode.name = "Malformed custom" + entity.nodes = NSSet(object: malformedDefaultNode) + entity.customNodes = NSSet(object: malformedCustomNode) + let replacement = ChainModelGenerator.generateChain( + generatingAssets: 1, + addressPrefix: 42 + ).replacingCustomNodes([]) + + try ChainModelMapper().populate( + entity: entity, + from: replacement, + using: context + ) + + XCTAssertTrue(malformedDefaultNode.isDeleted) + XCTAssertTrue(malformedCustomNode.isDeleted) + XCTAssertFalse(entity.nodes?.allObjects.isEmpty ?? true) + XCTAssertTrue(entity.customNodes?.allObjects.isEmpty ?? true) + } + + func testMalformedChainCleanup_whenGetterRaisesDuringQuarantine_thenFailsSafelyAndRollsBack() throws { + let chainEntity = NSEntityDescription() + chainEntity.name = String(describing: CDChain.self) + chainEntity.managedObjectClassName = NSStringFromClass( + CDChain.self + ) + let disabled = NSAttributeDescription() + disabled.name = "disabled" + disabled.attributeType = .booleanAttributeType + disabled.isOptional = false + disabled.defaultValue = false + chainEntity.properties = [ + makeAttribute( + name: "chainId", + type: .stringAttributeType + ), + disabled + ] + + let model = NSManagedObjectModel() + model.entities = [chainEntity] + let coordinator = NSPersistentStoreCoordinator( + managedObjectModel: model + ) + try coordinator.addPersistentStore( + ofType: NSInMemoryStoreType, + configurationName: nil, + at: nil + ) + let context = NSManagedObjectContext( + concurrencyType: .privateQueueConcurrencyType + ) + context.persistentStoreCoordinator = coordinator + let facade = ChainCleanupFixtureStorageFacade( + context: context + ) + + try perform(in: facade.databaseService) { + context in + + let chain = CDChain( + entity: chainEntity, + insertInto: context + ) + chain.chainId = "" + chain.disabled = false + try context.save() + } + + let operation = ChainRepositoryFactory( + storageFacade: facade + ).createMalformedChainCleanupOperation() + let queue = OperationQueue() + queue.addOperations( + [operation], + waitUntilFinished: true + ) + + XCTAssertThrowsError( + try XCTUnwrap(operation.result).get() + ) { error in + XCTAssertEqual( + error as? SafeTransformableValueReaderError, + .objectiveCException + ) + } + + try perform(in: facade.databaseService) { + context in + + let request = NSFetchRequest( + entityName: String(describing: CDChain.self) + ) + let chains = try context.fetch(request) + let chain = try XCTUnwrap(chains.first) + XCTAssertEqual(chains.count, 1) + XCTAssertEqual(chain.chainId, "") + XCTAssertFalse(chain.disabled) + XCTAssertFalse( + chain.chainId?.hasPrefix( + ChainModelMapper + .quarantinedChainIdentifierPrefix + ) == true + ) + } + } + + func testMalformedChainCleanupQuarantinesBlankIdentifiersWithoutDataLoss() + throws { + let facade = SubstrateStorageTestFacade() + let malformedFixtures: [(identifier: String?, suffix: String)] = [ + ("", "empty"), + (" \n\t", "whitespace") + ] + + try perform(in: facade.databaseService) { context in + for fixture in malformedFixtures { + let malformedChain = CDChain(context: context) + malformedChain.chainId = fixture.identifier + malformedChain.name = "Malformed \(fixture.suffix)" + + let defaultNode = CDChainNode(context: context) + defaultNode.url = try XCTUnwrap( + URL( + string: + "wss://default-\(fixture.suffix).example" + ) + ) + defaultNode.name = "Default \(fixture.suffix)" + malformedChain.nodes = NSSet(object: defaultNode) + + let customNode = CDChainNode(context: context) + customNode.url = try XCTUnwrap( + URL( + string: + "wss://custom-\(fixture.suffix).example" + ) + ) + customNode.name = "Custom \(fixture.suffix)" + malformedChain.customNodes = NSSet(object: customNode) + malformedChain.selectedNode = customNode + + let asset = CDAsset(context: context) + asset.id = "asset-\(fixture.suffix)" + asset.name = "Asset \(fixture.suffix)" + asset.symbol = "AS\(fixture.suffix)" + asset.precision = 12 + malformedChain.assets = NSSet(object: asset) + } + + let healthyChain = CDChain(context: context) + healthyChain.chainId = "healthy-chain" + healthyChain.name = "Healthy" + try context.save() + } + + let factory = ChainRepositoryFactory(storageFacade: facade) + let queue = OperationQueue() + var identifiersAfterFirstCleanup: [String: String] = [:] + + for cleanupIndex in 0 ..< 2 { + let cleanupOperation = factory.createMalformedChainCleanupOperation() + queue.addOperations([cleanupOperation], waitUntilFinished: true) + let _: Void = try XCTUnwrap(cleanupOperation.result).get() + + if cleanupIndex == 0 { + try perform(in: facade.databaseService) { context in + let request = NSFetchRequest( + entityName: String(describing: CDChain.self) + ) + identifiersAfterFirstCleanup = Dictionary( + uniqueKeysWithValues: try context.fetch(request) + .compactMap { chain in + guard + let name = chain.name, + name.hasPrefix("Malformed "), + let identifier = chain.chainId + else { + return nil + } + + return (name, identifier) + } + ) + } + } + } + + try perform(in: facade.databaseService) { context in + let request = NSFetchRequest( + entityName: String(describing: CDChain.self) + ) + let chains = try context.fetch(request) + XCTAssertEqual(chains.count, malformedFixtures.count + 1) + + let healthy = try XCTUnwrap( + chains.first { $0.chainId == "healthy-chain" } + ) + XCTAssertFalse(healthy.disabled) + + let quarantinedChains = chains.filter { + $0.chainId?.hasPrefix( + ChainModelMapper.quarantinedChainIdentifierPrefix + ) == true + } + XCTAssertEqual( + quarantinedChains.count, + malformedFixtures.count + ) + XCTAssertEqual( + Set(quarantinedChains.compactMap(\.chainId)).count, + malformedFixtures.count, + "each malformed row must receive a unique identifier" + ) + + for fixture in malformedFixtures { + let name = "Malformed \(fixture.suffix)" + let chain = try XCTUnwrap( + quarantinedChains.first { $0.name == name } + ) + XCTAssertTrue(chain.disabled) + XCTAssertEqual( + chain.chainId, + identifiersAfterFirstCleanup[name], + "quarantine identifiers must remain stable on relaunch" + ) + XCTAssertEqual( + Set( + (chain.nodes?.allObjects as? [CDChainNode] ?? []) + .compactMap(\.url?.absoluteString) + ), + Set(["wss://default-\(fixture.suffix).example"]) + ) + XCTAssertEqual( + Set( + ( + chain.customNodes?.allObjects + as? [CDChainNode] ?? [] + ) + .compactMap(\.url?.absoluteString) + ), + Set(["wss://custom-\(fixture.suffix).example"]) + ) + XCTAssertEqual( + chain.selectedNode?.url?.absoluteString, + "wss://custom-\(fixture.suffix).example" + ) + XCTAssertEqual( + Set( + (chain.assets?.allObjects as? [CDAsset] ?? []) + .compactMap(\.id) + ), + Set(["asset-\(fixture.suffix)"]) + ) + } + } + + let repository = factory.createRepository() + let fetchOperation = repository.fetchAllOperation( + with: RepositoryFetchOptions() + ) + queue.addOperations([fetchOperation], waitUntilFinished: true) + let chains: [ChainModel] = try XCTUnwrap( + fetchOperation.result + ).get() + XCTAssertEqual(chains.count, malformedFixtures.count + 1) + XCTAssertEqual( + chains.filter { + $0.chainId.hasPrefix( + ChainModelMapper.quarantinedChainIdentifierPrefix + ) + }.count, + malformedFixtures.count + ) + for fixture in malformedFixtures { + let chain = try XCTUnwrap( + chains.first { $0.name == "Malformed \(fixture.suffix)" } + ) + XCTAssertEqual( + chain.customNodes?.first?.url.absoluteString, + "wss://custom-\(fixture.suffix).example" + ) + XCTAssertEqual( + chain.selectedNode?.url.absoluteString, + "wss://custom-\(fixture.suffix).example" + ) + XCTAssertEqual( + Set(chain.assets.map(\.id)), + Set(["asset-\(fixture.suffix)"]) + ) + } + } + + func testMalformedChainQuarantineCannotCollideWithStoredIdentifier() + throws { + let facade = SubstrateStorageTestFacade() + var reservedIdentifier = "" + + try perform(in: facade.databaseService) { context in + let malformedChain = CDChain(context: context) + malformedChain.chainId = "" + malformedChain.name = "Malformed collision" + try context.save() + + reservedIdentifier = + ChainModelMapper.quarantinedChainIdentifierPrefix + + malformedChain.objectID.uriRepresentation().absoluteString + + let preexistingChain = CDChain(context: context) + preexistingChain.chainId = reservedIdentifier + preexistingChain.name = "Preexisting identifier" + try context.save() + } + + let factory = ChainRepositoryFactory(storageFacade: facade) + let queue = OperationQueue() + for _ in 0 ..< 2 { + let cleanupOperation = factory.createMalformedChainCleanupOperation() + queue.addOperations([cleanupOperation], waitUntilFinished: true) + let _: Void = try XCTUnwrap(cleanupOperation.result).get() + } + + try perform(in: facade.databaseService) { context in + let request = NSFetchRequest( + entityName: String(describing: CDChain.self) + ) + let chains = try context.fetch(request) + XCTAssertEqual(chains.count, 2) + + let preexistingChain = try XCTUnwrap( + chains.first { $0.name == "Preexisting identifier" } + ) + XCTAssertEqual(preexistingChain.chainId, reservedIdentifier) + XCTAssertFalse(preexistingChain.disabled) + + let quarantinedChain = try XCTUnwrap( + chains.first { $0.name == "Malformed collision" } + ) + XCTAssertEqual( + quarantinedChain.chainId, + "\(reservedIdentifier)#1" + ) + XCTAssertTrue(quarantinedChain.disabled) + } + } + + func testCleanupCanonicalizesIdentifiersAndMergesPreferencesWithoutLoss() + throws { + let facade = SubstrateStorageTestFacade() + let canonicalSelectedURL = try XCTUnwrap( + URL(string: "wss://canonical-user.example") + ) + let collidingDefaultURL = try XCTUnwrap( + URL(string: "wss://colliding-default.example") + ) + let collidingCustomURL = try XCTUnwrap( + URL(string: "wss://colliding-user.example") + ) + let normalizedSelectedURL = try XCTUnwrap( + URL(string: "wss://normalized-user.example") + ) + let historyApiURL = try XCTUnwrap( + URL(string: "https://history.example") + ) + + try perform(in: facade.databaseService) { context in + let canonical = CDChain(context: context) + canonical.chainId = "collision-chain" + canonical.name = "Canonical" + let canonicalNode = CDChainNode(context: context) + canonicalNode.url = canonicalSelectedURL + canonicalNode.name = "Canonical selection" + canonical.nodes = NSSet(object: canonicalNode) + canonical.customNodes = NSSet(object: canonicalNode) + canonical.selectedNode = canonicalNode + + let colliding = CDChain(context: context) + colliding.chainId = " collision-chain \n" + colliding.name = "Canonical" + colliding.rank = "7" + colliding.historyApiType = "subquery" + colliding.historyApiUrl = historyApiURL + colliding.setValue( + NSArray(object: ChainOptions.poolStaking.rawValue), + forKey: "options" + ) + let collidingDefaultNode = CDChainNode(context: context) + collidingDefaultNode.url = collidingDefaultURL + collidingDefaultNode.name = "Colliding default" + colliding.nodes = NSSet(object: collidingDefaultNode) + let collidingCustomNode = CDChainNode(context: context) + collidingCustomNode.url = collidingCustomURL + collidingCustomNode.name = "Colliding custom" + colliding.customNodes = NSSet(object: collidingCustomNode) + + let collidingAsset = CDAsset(context: context) + collidingAsset.id = "colliding-asset" + collidingAsset.name = "Colliding asset" + collidingAsset.symbol = "COL" + collidingAsset.precision = 12 + colliding.assets = NSSet(object: collidingAsset) + + let collidingExplorer = CDExternalApi(context: context) + collidingExplorer.type = "subscan" + collidingExplorer.url = "https://explorer.example" + collidingExplorer.setValue( + NSArray(object: "extrinsic"), + forKey: "types" + ) + colliding.explorers = NSSet(object: collidingExplorer) + + let collidingXcm = CDChainXcmConfig(context: context) + collidingXcm.xcmVersion = "V3" + collidingXcm.availableAssets = NSSet() + collidingXcm.availableDestinations = NSSet() + colliding.xcmConfig = collidingXcm + + let normalizationOnly = CDChain(context: context) + normalizationOnly.chainId = "\tnormalized-chain " + normalizationOnly.name = "Normalization only" + let normalizedNode = CDChainNode(context: context) + normalizedNode.url = normalizedSelectedURL + normalizedNode.name = "Normalized selection" + normalizationOnly.nodes = NSSet(object: normalizedNode) + normalizationOnly.customNodes = NSSet(object: normalizedNode) + normalizationOnly.selectedNode = normalizedNode + + try context.save() + } + + let factory = ChainRepositoryFactory(storageFacade: facade) + let queue = OperationQueue() + for _ in 0 ..< 2 { + let operation = factory.createMalformedChainCleanupOperation() + queue.addOperations([operation], waitUntilFinished: true) + let _: Void = try XCTUnwrap(operation.result).get() + } + + try perform(in: facade.databaseService) { context in + let request = NSFetchRequest( + entityName: String(describing: CDChain.self) + ) + let chains = try context.fetch(request) + let chainsByIdentifier = Dictionary( + uniqueKeysWithValues: chains.compactMap { chain in + chain.chainId.map { ($0, chain) } + } + ) + + XCTAssertEqual( + Set(chainsByIdentifier.keys), + Set(["collision-chain", "normalized-chain"]) + ) + + let merged = try XCTUnwrap( + chainsByIdentifier["collision-chain"] + ) + let mergedCustomURLs = Set( + (merged.customNodes?.allObjects as? [CDChainNode] ?? []) + .compactMap(\.url) + ) + XCTAssertEqual( + mergedCustomURLs, + Set([canonicalSelectedURL, collidingCustomURL]) + ) + let mergedDefaultURLs = Set( + (merged.nodes?.allObjects as? [CDChainNode] ?? []) + .compactMap(\.url) + ) + XCTAssertEqual( + mergedDefaultURLs, + Set([canonicalSelectedURL, collidingDefaultURL]) + ) + XCTAssertEqual( + merged.selectedNode?.url, + canonicalSelectedURL, + "the canonical row's explicit selection wins collisions" + ) + XCTAssertEqual(merged.rank, "7") + XCTAssertEqual(merged.historyApiType, "subquery") + XCTAssertEqual(merged.historyApiUrl, historyApiURL) + let mergedOptions: [String]? = + try SafeTransformableValueReader.read( + from: merged, + key: "options" + ) + XCTAssertEqual( + mergedOptions, + [ChainOptions.poolStaking.rawValue] + ) + XCTAssertEqual( + Set( + (merged.assets?.allObjects as? [CDAsset] ?? []) + .compactMap(\.id) + ), + Set(["colliding-asset"]) + ) + XCTAssertEqual( + Set( + ( + merged.explorers?.allObjects + as? [CDExternalApi] ?? [] + ) + .compactMap(\.url) + ), + Set(["https://explorer.example"]) + ) + XCTAssertEqual(merged.xcmConfig?.xcmVersion, "V3") + + let normalized = try XCTUnwrap( + chainsByIdentifier["normalized-chain"] + ) + XCTAssertEqual( + normalized.selectedNode?.url, + normalizedSelectedURL + ) + } + + let repository = factory.createRepository() + let fetchOperation = repository.fetchAllOperation( + with: RepositoryFetchOptions() + ) + queue.addOperations([fetchOperation], waitUntilFinished: true) + let chains: [ChainModel] = try XCTUnwrap( + fetchOperation.result + ).get() + XCTAssertEqual( + Set(chains.map(\.chainId)), + Set(["collision-chain", "normalized-chain"]) + ) + } + + func testCleanupCollisionPreservesCustomSelectionWhenDefaultRetires() + throws { + let facade = SubstrateStorageTestFacade() + let chainId = "shared-node-collision" + let sharedURL = try XCTUnwrap( + URL(string: "wss://shared-node.example") + ) + let replacementURL = try XCTUnwrap( + URL(string: "wss://replacement-default.example") + ) + + try perform(in: facade.databaseService) { context in + let canonical = CDChain(context: context) + canonical.chainId = chainId + canonical.name = "Canonical" + let sharedNode = CDChainNode(context: context) + sharedNode.url = sharedURL + sharedNode.name = "Shared user selection" + canonical.nodes = NSSet(object: sharedNode) + + let colliding = CDChain(context: context) + colliding.chainId = " \(chainId) " + colliding.name = "Canonical" + colliding.customNodes = NSSet(object: sharedNode) + colliding.selectedNode = sharedNode + + try context.save() + } + + let factory = ChainRepositoryFactory(storageFacade: facade) + let queue = OperationQueue() + let cleanup = factory.createMalformedChainCleanupOperation() + queue.addOperations([cleanup], waitUntilFinished: true) + let _: Void = try XCTUnwrap(cleanup.result).get() + + let repository = factory.createRepository() + let initialFetch = repository.fetchAllOperation( + with: RepositoryFetchOptions() + ) + queue.addOperations([initialFetch], waitUntilFinished: true) + let localChain = try XCTUnwrap( + try XCTUnwrap(initialFetch.result) + .get() + .first { $0.chainId == chainId } + ) + + XCTAssertEqual(localChain.customNodes?.first?.url, sharedURL) + XCTAssertEqual( + localChain.customNodes?.first?.name, + "Shared user selection" + ) + XCTAssertEqual(localChain.selectedNode?.url, sharedURL) + + let replacementDefault = ChainNodeModel( + url: replacementURL, + name: "Replacement default", + apikey: nil + ) + let remoteChain = localChain.replacingNodeConfiguration( + nodes: [replacementDefault], + selectedNode: nil, + customNodes: nil + ) + let mergedChain = try XCTUnwrap( + ChainSyncService.preservingLocalNodePreferences( + remoteChains: [remoteChain], + localChains: [localChain] + ).first + ) + + XCTAssertEqual(mergedChain.nodes, Set([replacementDefault])) + XCTAssertEqual(mergedChain.customNodes?.first?.url, sharedURL) + XCTAssertEqual(mergedChain.selectedNode?.url, sharedURL) + + try perform(in: facade.databaseService) { context in + let request = NSFetchRequest( + entityName: String(describing: CDChain.self) + ) + let entity = try XCTUnwrap( + try context.fetch(request).first { + $0.chainId == chainId + } + ) + + try ChainModelMapper().populate( + entity: entity, + from: mergedChain, + using: context + ) + try context.save() + } + + let finalFetch = repository.fetchAllOperation( + with: RepositoryFetchOptions() + ) + queue.addOperations([finalFetch], waitUntilFinished: true) + let refetchedChain = try XCTUnwrap( + try XCTUnwrap(finalFetch.result) + .get() + .first { $0.chainId == chainId } + ) + + XCTAssertEqual(refetchedChain.nodes, Set([replacementDefault])) + XCTAssertEqual(refetchedChain.customNodes?.first?.url, sharedURL) + XCTAssertEqual( + refetchedChain.customNodes?.first?.name, + "Shared user selection" + ) + XCTAssertEqual(refetchedChain.selectedNode?.url, sharedURL) + } + + func testCleanupQuarantinesConflictingCollisionWithoutDeletingItsData() + throws { + let facade = SubstrateStorageTestFacade() + let chainId = "conflicting-collision" + let canonicalURL = try XCTUnwrap( + URL(string: "wss://canonical-collision.example") + ) + let conflictingURL = try XCTUnwrap( + URL(string: "wss://conflicting-collision.example") + ) + + try perform(in: facade.databaseService) { context in + let canonical = CDChain(context: context) + canonical.chainId = chainId + canonical.name = "Collision chain" + let canonicalNode = CDChainNode(context: context) + canonicalNode.url = canonicalURL + canonicalNode.name = "Canonical selection" + canonical.nodes = NSSet(object: canonicalNode) + canonical.selectedNode = canonicalNode + + let conflicting = CDChain(context: context) + conflicting.chainId = " \(chainId) " + conflicting.name = "Collision chain" + let conflictingNode = CDChainNode(context: context) + conflictingNode.url = conflictingURL + conflictingNode.name = "Conflicting selection" + conflicting.nodes = NSSet(object: conflictingNode) + conflicting.selectedNode = conflictingNode + + let conflictingAsset = CDAsset(context: context) + conflictingAsset.id = "conflicting-asset" + conflictingAsset.name = "Conflicting asset" + conflictingAsset.symbol = "CON" + conflictingAsset.precision = 12 + conflicting.assets = NSSet(object: conflictingAsset) + + try context.save() + } + + let factory = ChainRepositoryFactory(storageFacade: facade) + let queue = OperationQueue() + + for _ in 0 ..< 2 { + let cleanup = factory.createMalformedChainCleanupOperation() + queue.addOperations([cleanup], waitUntilFinished: true) + let _: Void = try XCTUnwrap(cleanup.result).get() + } + + try perform(in: facade.databaseService) { context in + let request = NSFetchRequest( + entityName: String(describing: CDChain.self) + ) + let chains = try context.fetch(request) + XCTAssertEqual(chains.count, 2) + + let canonical = try XCTUnwrap( + chains.first { $0.chainId == chainId } + ) + XCTAssertFalse(canonical.disabled) + XCTAssertEqual(canonical.selectedNode?.url, canonicalURL) + XCTAssertEqual( + Set( + (canonical.nodes?.allObjects as? [CDChainNode] ?? []) + .compactMap(\.url) + ), + Set([canonicalURL]) + ) + XCTAssertTrue(canonical.assets?.allObjects.isEmpty ?? true) + + let quarantined = try XCTUnwrap( + chains.first { + $0.chainId?.hasPrefix( + ChainModelMapper + .quarantinedChainIdentifierPrefix + ) == true + } + ) + XCTAssertTrue(quarantined.disabled) + XCTAssertEqual(quarantined.name, "Collision chain") + XCTAssertEqual(quarantined.selectedNode?.url, conflictingURL) + XCTAssertEqual( + Set( + ( + quarantined.nodes?.allObjects + as? [CDChainNode] ?? [] + ) + .compactMap(\.url) + ), + Set([conflictingURL]) + ) + XCTAssertEqual( + Set( + (quarantined.assets?.allObjects as? [CDAsset] ?? []) + .compactMap(\.id) + ), + Set(["conflicting-asset"]) + ) + } + } + + private func makeNode(from entity: CDChainNode) throws -> ChainNodeModel { + ChainNodeModel( + url: try XCTUnwrap(entity.url), + name: try XCTUnwrap(entity.name), + apikey: nil + ) + } + + private func makeAssetWithUnmatchedPricing( + identifier: String + ) -> AssetModel { + AssetModel( + id: identifier, + name: "Cached price fixture", + symbol: "CPF", + precision: 12, + currencyId: "asset-currency", + isUtility: false, + isNative: false, + coingeckoPriceId: "asset-price" + ) + } + + private func makeCachedPrice( + currencyId: String, + priceId: String, + price: String, + fiatDayChange: String, + in context: NSManagedObjectContext + ) -> CDPriceData { + let entity = CDPriceData(context: context) + entity.currencyId = currencyId + entity.priceId = priceId + entity.price = price + entity.fiatDayByChange = fiatDayChange + return entity + } + + private func makeValidChainEntity( + in context: NSManagedObjectContext + ) -> CDChain { + let entity = CDChain(context: context) + entity.chainId = "valid-chain" + entity.name = "Valid Chain" + return entity + } + + private func makeCachedPriceEntity( + managedObjectClass: AnyClass, + priceType: NSAttributeType + ) -> ( + model: NSManagedObjectModel, + entity: NSEntityDescription + ) { + let entity = NSEntityDescription() + entity.name = "CachedPriceFixture_" + UUID().uuidString + .replacingOccurrences(of: "-", with: "") + entity.managedObjectClassName = NSStringFromClass( + managedObjectClass + ) + entity.properties = [ + makeAttribute( + name: "currencyId", + type: .stringAttributeType + ), + makeAttribute( + name: "priceId", + type: .stringAttributeType + ), + makeAttribute(name: "price", type: priceType) + ] + + let model = NSManagedObjectModel() + model.entities = [entity] + return (model, entity) + } + + private func makeEntityDescription( + managedObjectClass: AnyClass, + properties: [NSPropertyDescription] + ) -> NSEntityDescription { + let entity = NSEntityDescription() + entity.name = "MapperExceptionFixture_" + UUID().uuidString + .replacingOccurrences(of: "-", with: "") + entity.managedObjectClassName = NSStringFromClass( + managedObjectClass + ) + entity.properties = properties + return entity + } + + private func makeAttribute( + name: String, + type: NSAttributeType + ) -> NSAttributeDescription { + let attribute = NSAttributeDescription() + attribute.name = name + attribute.attributeType = type + attribute.isOptional = true + return attribute + } + + private func makeContext( + entities: [NSEntityDescription] + ) throws -> NSManagedObjectContext { + let model = NSManagedObjectModel() + model.entities = entities + let coordinator = NSPersistentStoreCoordinator( + managedObjectModel: model + ) + try coordinator.addPersistentStore( + ofType: NSInMemoryStoreType, + configurationName: nil, + at: nil, + options: nil + ) + let context = NSManagedObjectContext( + concurrencyType: .mainQueueConcurrencyType + ) + context.persistentStoreCoordinator = coordinator + return context + } + + private func createContext() throws -> NSManagedObjectContext { + let modelURL = try XCTUnwrap( + Bundle.main.url( + forResource: "SubstrateDataModel", + withExtension: "momd" + ) + ) + let model = try XCTUnwrap(NSManagedObjectModel(contentsOf: modelURL)) + let coordinator = NSPersistentStoreCoordinator(managedObjectModel: model) + try coordinator.addPersistentStore( + ofType: NSInMemoryStoreType, + configurationName: nil, + at: nil, + options: nil + ) + + let context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) + context.persistentStoreCoordinator = coordinator + return context + } + + private func perform( + in service: CoreDataServiceProtocol, + _ closure: @escaping (NSManagedObjectContext) throws -> Void + ) throws { + let semaphore = DispatchSemaphore(value: 0) + var result: Result? + + service.performAsync { context, error in + defer { semaphore.signal() } + + do { + if let error { + throw error + } + + try closure( + XCTUnwrap(context, "Expected a Core Data context") + ) + result = .success(()) + } catch { + result = .failure(error) + } + } + + semaphore.wait() + try XCTUnwrap(result).get() + } +} + +private final class ExceptionRaisingCachedPrice: NSManagedObject { + override func willAccessValue(forKey key: String?) { + guard key == "price" else { + super.willAccessValue(forKey: key) + return + } + + NSException( + name: .invalidArgumentException, + reason: "adversarial cached-price row", + userInfo: nil + ).raise() + } +} + +private final class ChainCleanupFixtureCoreDataService: + CoreDataServiceProtocol { + let configuration: CoreDataServiceConfigurationProtocol + + private let context: NSManagedObjectContext + + init(context: NSManagedObjectContext) { + self.context = context + configuration = CoreDataServiceConfiguration( + modelURL: URL( + fileURLWithPath: + "/dev/null/ChainCleanupFixture.mom" + ), + storageType: .inMemory + ) + } + + func performAsync( + block: @escaping CoreDataContextInvocationBlock + ) { + context.perform { + block(self.context, nil) + } + } + + func close() throws {} + + func drop() throws {} +} + +private final class ChainCleanupFixtureStorageFacade: + StorageFacadeProtocol { + let databaseService: CoreDataServiceProtocol + + init(context: NSManagedObjectContext) { + databaseService = + ChainCleanupFixtureCoreDataService( + context: context + ) + } + + func createRepository( + filter: NSPredicate?, + sortDescriptors: [NSSortDescriptor], + mapper: AnyCoreDataMapper + ) -> CoreDataRepository + where T: Identifiable, U: NSManagedObject { + CoreDataRepository( + databaseService: databaseService, + mapper: mapper, + filter: filter, + sortDescriptors: sortDescriptors + ) + } + + func createAsyncRepository( + filter: NSPredicate?, + sortDescriptors: [NSSortDescriptor], + mapper: AnyCoreDataMapper + ) -> AsyncCoreDataRepositoryDefault + where T: Identifiable, U: NSManagedObject { + AsyncCoreDataRepositoryDefault( + databaseService: databaseService, + mapper: mapper, + filter: filter, + sortDescriptors: sortDescriptors + ) + } } diff --git a/fearlessTests/Common/Storage/MetaAccountMapperTests.swift b/fearlessTests/Common/Storage/MetaAccountMapperTests.swift index d9f2363750..59aa324457 100644 --- a/fearlessTests/Common/Storage/MetaAccountMapperTests.swift +++ b/fearlessTests/Common/Storage/MetaAccountMapperTests.swift @@ -5,6 +5,83 @@ import SSFModels import CoreData class MetaAccountMapperTests: XCTestCase { + func testTransformConvertsMandatoryGetterExceptionToSanitizedSwiftError() { + let entityDescription = makeEntityDescription( + managedObjectClass: CDMetaAccount.self, + properties: [] + ) + let entity = CDMetaAccount( + entity: entityDescription, + insertInto: nil + ) + + XCTAssertThrowsError( + try MetaAccountMapper().transform(entity: entity) + ) { error in + XCTAssertEqual( + error as? SafeTransformableValueReaderError, + .objectiveCException + ) + } + } + + func testMalformedChildRelationshipFailsClosedThroughDirectAndSelectionMappers() + throws { + let fixture = try makeMetaAccountWithThrowingChainAccountRelationship() + + XCTAssertThrowsError( + try MetaAccountMapper().transform(entity: fixture.entity) + ) { error in + XCTAssertEqual( + error as? SafeTransformableValueReaderError, + .objectiveCException + ) + } + + let startupMapper: + AnyCoreDataMapper = + AnyCoreDataMapper(MetaAccountSelectionMapper()) + let projection = try startupMapper.transform(entity: fixture.entity) + + XCTAssertEqual(projection.identifier, "relationship-wallet") + XCTAssertEqual(projection.recordState, .corrupt) + XCTAssertNil(projection.wallet) + XCTAssertFalse(projection.recordState.allowsStoredRecordUpdates) + } + + func testSelectionStartupWrapperQuarantinesOwnIdentifierGetterException() + throws { + let entityDescription = makeEntityDescription( + managedObjectClass: CDMetaAccount.self, + properties: [ + makeAttribute( + name: "isSelected", + type: .booleanAttributeType + ), + makeAttribute(name: "order", type: .integer32AttributeType) + ] + ) + let entity = CDMetaAccount( + entity: entityDescription, + insertInto: nil + ) + let startupMapper: + AnyCoreDataMapper = + AnyCoreDataMapper(MetaAccountSelectionMapper()) + let projection = try startupMapper.transform(entity: entity) + + XCTAssertTrue( + projection.identifier.hasPrefix( + "fearless.quarantined-wallet:" + ) + ) + XCTAssertEqual(projection.recordState, .corrupt) + XCTAssertNil(projection.wallet) + XCTAssertFalse(projection.isSelected) + XCTAssertEqual(projection.order, 0) + XCTAssertFalse(projection.recordState.allowsStoredRecordUpdates) + } + func testSaveAndFetch() throws { // given @@ -57,4 +134,454 @@ class MetaAccountMapperTests: XCTestCase { XCTAssertEqual(differentOrders.count, accountCount) } + func testCanonicalEcdsaSubstratePublicKeyIsAccepted() throws { + let operationQueue = OperationQueue() + let facade = UserDataStorageTestFacade() + let generated = AccountGenerator.generateMetaAccount(generatingChainAccounts: 1) + let ecdsaChainAccount = ChainAccountModel( + chainId: "canonical-ecdsa-chain", + accountId: Data(repeating: 0x59, count: 20), + publicKey: Data(repeating: 0x58, count: 33), + cryptoType: CryptoType.ecdsa.rawValue, + ethereumBased: true + ) + let wallet = MetaAccountModel( + metaId: "canonical-ecdsa-wallet", + name: generated.name, + substrateAccountId: generated.substrateAccountId, + substrateCryptoType: CryptoType.ecdsa.rawValue, + substratePublicKey: Data(repeating: 0x5A, count: 33), + ethereumAddress: generated.ethereumAddress, + ethereumPublicKey: generated.ethereumPublicKey, + chainAccounts: [ecdsaChainAccount], + assetKeysOrder: generated.assetKeysOrder, + canExportEthereumMnemonic: generated.canExportEthereumMnemonic, + unusedChainIds: generated.unusedChainIds, + selectedCurrency: generated.selectedCurrency, + networkManagmentFilter: generated.networkManagmentFilter, + assetsVisibility: generated.assetsVisibility, + hasBackup: generated.hasBackup, + favouriteChainIds: generated.favouriteChainIds + ) + let storageRepository = facade.createRepository( + mapper: AnyCoreDataMapper(ManagedMetaAccountMapper()) + ) + + let saveOperation = storageRepository.saveOperation( + { [ManagedMetaAccountModel(info: wallet, isSelected: true)] }, + { [] } + ) + operationQueue.addOperations([saveOperation], waitUntilFinished: true) + _ = try XCTUnwrap(saveOperation.result).get() + + let repository = AccountRepositoryFactory(storageFacade: facade) + .createMetaAccountRepository(for: nil, sortDescriptors: []) + let fetchOperation = repository.fetchAllOperation( + with: RepositoryFetchOptions( + includesProperties: true, + includesSubentities: true + ) + ) + operationQueue.addOperations([fetchOperation], waitUntilFinished: true) + + XCTAssertEqual(try XCTUnwrap(fetchOperation.result).get(), [wallet]) + } + + func testNonPersistableNilRequiredFieldsAreQuarantinedByMappersWithoutSaving() throws { + let facade = UserDataStorageTestFacade() + let callbackExpectation = expectation( + description: "unsaved required-field corruptions are mapped" + ) + var callbackResult: Result<[NonPersistableCorruptionObservation], Error>? + + facade.databaseService.performAsync { context, error in + defer { + callbackExpectation.fulfill() + } + + do { + if error != nil { + throw MetaAccountMapperTestFixtureError( + message: "Core Data setup failed" + ) + } + guard let context else { + throw MetaAccountMapperTestFixtureError( + message: "Core Data setup returned no context" + ) + } + + let mapper = MetaAccountMapper() + let selectionMapper = MetaAccountSelectionMapper() + var observations: [NonPersistableCorruptionObservation] = [] + + for corruption in NonPersistableSupportedWalletCorruption.allCases { + let generated = AccountGenerator.generateMetaAccount( + generatingChainAccounts: 1 + ) + let model = MetaAccountModel( + metaId: "unsaved-\(corruption.identifier)", + name: generated.name, + substrateAccountId: generated.substrateAccountId, + substrateCryptoType: generated.substrateCryptoType, + substratePublicKey: generated.substratePublicKey, + ethereumAddress: generated.ethereumAddress, + ethereumPublicKey: generated.ethereumPublicKey, + chainAccounts: generated.chainAccounts, + assetKeysOrder: generated.assetKeysOrder, + canExportEthereumMnemonic: generated.canExportEthereumMnemonic, + unusedChainIds: generated.unusedChainIds, + selectedCurrency: generated.selectedCurrency, + networkManagmentFilter: generated.networkManagmentFilter, + assetsVisibility: generated.assetsVisibility, + hasBackup: generated.hasBackup, + favouriteChainIds: generated.favouriteChainIds + ) + let entity = CDMetaAccount(context: context) + try mapper.populate(entity: entity, from: model, using: context) + guard + let child = entity.chainAccounts?.allObjects.first + as? CDChainAccount + else { + throw MetaAccountMapperTestFixtureError( + message: "Generated wallet has no child chain account" + ) + } + + switch corruption { + case .missingMetaId: + entity.metaId = nil + case .missingName: + entity.name = nil + case .missingChildAccountId: + child.accountId = nil + case .missingChildChainId: + child.chainId = nil + case .missingChildPublicKey: + child.publicKey = nil + } + + let coreDataRejectsInsert: Bool + do { + try entity.validateForInsert() + coreDataRejectsInsert = false + } catch { + coreDataRejectsInsert = true + } + + let directMapperRejectsAsInvalid: Bool + do { + _ = try mapper.transform(entity: entity) + directMapperRejectsAsInvalid = false + } catch MetaAccountMapperError.invalidWalletRecord { + directMapperRejectsAsInvalid = true + } + + let projection = try selectionMapper.transform(entity: entity) + let replacement = projection.replacingSelection( + !projection.isSelected + ) + observations.append( + NonPersistableCorruptionObservation( + identifier: corruption.identifier, + coreDataRejectsInsert: coreDataRejectsInsert, + directMapperRejectsAsInvalid: directMapperRejectsAsInvalid, + recordState: projection.recordState, + hasProjectedWallet: projection.wallet != nil, + allowsStoredRecordUpdates: + projection.recordState.allowsStoredRecordUpdates, + replacementUpdatesWalletPayload: + replacement.updatesWalletPayload, + replacementUpdatesSelection: + replacement.updatesSelection + ) + ) + + context.rollback() + } + + callbackResult = .success(observations) + } catch { + callbackResult = .failure(error) + } + } + + wait( + for: [callbackExpectation], + timeout: Constants.defaultExpectationDuration + ) + guard let callbackResult else { + throw MetaAccountMapperTestFixtureError( + message: "Core Data mapper callback produced no result" + ) + } + let observations = try callbackResult.get() + + XCTAssertEqual( + observations.map(\.identifier), + NonPersistableSupportedWalletCorruption.allCases.map(\.identifier) + ) + XCTAssertTrue(observations.allSatisfy(\.coreDataRejectsInsert)) + XCTAssertTrue(observations.allSatisfy(\.directMapperRejectsAsInvalid)) + XCTAssertTrue(observations.allSatisfy { $0.recordState == .corrupt }) + XCTAssertTrue(observations.allSatisfy { !$0.hasProjectedWallet }) + XCTAssertTrue(observations.allSatisfy { !$0.allowsStoredRecordUpdates }) + XCTAssertTrue( + observations.allSatisfy { !$0.replacementUpdatesWalletPayload } + ) + XCTAssertTrue( + observations.allSatisfy { !$0.replacementUpdatesSelection } + ) + } + + func testRepositorySliceDecoderAcceptsExactShapeAndFailsClosedOtherwise() throws { + let decoded = try ValidatedRepositorySlice( + request: RepositorySliceRequest( + offset: 2, + count: 3, + reversed: true + ) + ) + + XCTAssertEqual(decoded.offset, 2) + XCTAssertEqual(decoded.count, 3) + XCTAssertTrue(decoded.reversed) + + let invalidShapes: [[ValidatedRepositorySlice.ReflectedField]] = [ + [ + (label: "offset", value: 0), + (label: "count", value: 1) + ], + [ + (label: "offset", value: 0), + (label: "offset", value: 1), + (label: "reversed", value: false) + ], + [ + (label: "offset", value: 0), + (label: "count", value: 1), + (label: "unexpected", value: false) + ], + [ + (label: nil, value: 0), + (label: "count", value: 1), + (label: "reversed", value: false) + ], + [ + (label: "offset", value: "0"), + (label: "count", value: 1), + (label: "reversed", value: false) + ], + [ + (label: "offset", value: 0), + (label: "count", value: true), + (label: "reversed", value: false) + ], + [ + (label: "offset", value: 0), + (label: "count", value: 1), + (label: "reversed", value: 0) + ], + [ + (label: "offset", value: -1), + (label: "count", value: 1), + (label: "reversed", value: false) + ], + [ + (label: "offset", value: 0), + (label: "count", value: -1), + (label: "reversed", value: false) + ], + [ + (label: "offset", value: 0), + (label: "count", value: 1), + (label: "reversed", value: false), + (label: "extra", value: false) + ] + ] + + for (index, fields) in invalidShapes.enumerated() { + XCTAssertThrowsError( + try ValidatedRepositorySlice(reflectedFields: fields), + "Invalid reflected shape \(index) was accepted" + ) { error in + XCTAssertEqual( + error as? TolerantMetaAccountRepositoryError, + .invalidSliceRequest + ) + XCTAssertFalse( + (error as? LocalizedError)? + .errorDescription? + .isEmpty ?? true + ) + } + } + } + + private func makeMetaAccountWithThrowingChainAccountRelationship() + throws -> ( + context: NSManagedObjectContext, + entity: CDMetaAccount + ) { + let chainAccountDescription = makeEntityDescription( + managedObjectClass: CDChainAccount.self, + properties: [] + ) + let chainAccounts = NSRelationshipDescription() + chainAccounts.name = "chainAccounts" + chainAccounts.destinationEntity = chainAccountDescription + chainAccounts.minCount = 0 + chainAccounts.maxCount = 0 + chainAccounts.isOptional = true + chainAccounts.deleteRule = .nullifyDeleteRule + + let metaAccountDescription = makeEntityDescription( + managedObjectClass: CDMetaAccount.self, + properties: [ + makeAttribute(name: "metaId", type: .stringAttributeType), + makeAttribute(name: "name", type: .stringAttributeType), + makeAttribute( + name: "substrateAccountId", + type: .stringAttributeType + ), + makeAttribute( + name: "substratePublicKey", + type: .binaryDataAttributeType + ), + makeAttribute( + name: "substrateCryptoType", + type: .integer16AttributeType + ), + makeAttribute( + name: "isSelected", + type: .booleanAttributeType + ), + makeAttribute(name: "order", type: .integer32AttributeType), + chainAccounts + ] + ) + let context = try makeContext( + entities: [metaAccountDescription, chainAccountDescription] + ) + let entity = CDMetaAccount( + entity: metaAccountDescription, + insertInto: context + ) + entity.setValue("relationship-wallet", forKey: "metaId") + entity.setValue("Relationship wallet", forKey: "name") + entity.setValue( + String(repeating: "11", count: 32), + forKey: "substrateAccountId" + ) + entity.setValue( + Data(repeating: 0x22, count: 32), + forKey: "substratePublicKey" + ) + entity.setValue( + NSNumber(value: CryptoType.sr25519.rawValue), + forKey: "substrateCryptoType" + ) + entity.setValue(true, forKey: "isSelected") + entity.setValue(Int32(9), forKey: "order") + + let throwingChild = CDChainAccount( + entity: chainAccountDescription, + insertInto: context + ) + entity.setValue( + NSSet(object: throwingChild), + forKey: "chainAccounts" + ) + + return (context, entity) + } + + private func makeEntityDescription( + managedObjectClass: AnyClass, + properties: [NSPropertyDescription] + ) -> NSEntityDescription { + let entity = NSEntityDescription() + entity.name = "MapperExceptionFixture_" + UUID().uuidString + .replacingOccurrences(of: "-", with: "") + entity.managedObjectClassName = NSStringFromClass( + managedObjectClass + ) + entity.properties = properties + return entity + } + + private func makeAttribute( + name: String, + type: NSAttributeType + ) -> NSAttributeDescription { + let attribute = NSAttributeDescription() + attribute.name = name + attribute.attributeType = type + attribute.isOptional = true + return attribute + } + + private func makeContext( + entities: [NSEntityDescription] + ) throws -> NSManagedObjectContext { + let model = NSManagedObjectModel() + model.entities = entities + let coordinator = NSPersistentStoreCoordinator( + managedObjectModel: model + ) + try coordinator.addPersistentStore( + ofType: NSInMemoryStoreType, + configurationName: nil, + at: nil, + options: nil + ) + let context = NSManagedObjectContext( + concurrencyType: .mainQueueConcurrencyType + ) + context.persistentStoreCoordinator = coordinator + return context + } + +} + +private enum NonPersistableSupportedWalletCorruption: CaseIterable { + case missingMetaId + case missingName + case missingChildAccountId + case missingChildChainId + case missingChildPublicKey + + var identifier: String { + switch self { + case .missingMetaId: + return "missing-meta-id" + case .missingName: + return "missing-name" + case .missingChildAccountId: + return "missing-child-account-id" + case .missingChildChainId: + return "missing-child-chain-id" + case .missingChildPublicKey: + return "missing-child-public-key" + } + } +} + +private struct NonPersistableCorruptionObservation { + let identifier: String + let coreDataRejectsInsert: Bool + let directMapperRejectsAsInvalid: Bool + let recordState: MetaAccountSelectionRecordState + let hasProjectedWallet: Bool + let allowsStoredRecordUpdates: Bool + let replacementUpdatesWalletPayload: Bool + let replacementUpdatesSelection: Bool +} + +private struct MetaAccountMapperTestFixtureError: Error, CustomStringConvertible { + let message: String + + var description: String { + message + } } diff --git a/fearlessTests/Common/Storage/SelectedAccountSettingsTests.swift b/fearlessTests/Common/Storage/SelectedAccountSettingsTests.swift index f98cb97d59..3ac016b10f 100644 --- a/fearlessTests/Common/Storage/SelectedAccountSettingsTests.swift +++ b/fearlessTests/Common/Storage/SelectedAccountSettingsTests.swift @@ -1,6 +1,9 @@ import XCTest @testable import fearless +import CoreData import RobinHood +import enum SSFModels.CryptoType +import struct SSFModels.ChainAccountModel class SelectedAccountSettingsTests: XCTestCase { func testSelectFirst() throws { @@ -119,4 +122,3417 @@ class SelectedAccountSettingsTests: XCTestCase { XCTAssertEqual(expectedAccounts, actualAccounts) } + + func testSetupWhenWalletsExistWithoutSelectionRepairsDeterministicFallback() throws { + let operationQueue = OperationQueue() + let facade = UserDataStorageTestFacade() + let selectedAccountSettings = SelectedWalletSettings( + storageFacade: facade, + operationQueue: operationQueue + ) + let repository = facade.createRepository( + mapper: AnyCoreDataMapper(ManagedMetaAccountMapper()) + ) + let firstByOrder = ManagedMetaAccountModel( + info: AccountGenerator.generateMetaAccount(generatingChainAccounts: 1), + isSelected: false, + order: 1 + ) + let laterByOrder = ManagedMetaAccountModel( + info: AccountGenerator.generateMetaAccount(generatingChainAccounts: 1), + isSelected: false, + order: 2 + ) + + let seedOperation = repository.saveOperation({ + [firstByOrder, laterByOrder] + }, { + [] + }) + operationQueue.addOperations([seedOperation], waitUntilFinished: true) + _ = try XCTUnwrap(seedOperation.result).get() + + let setupExpectation = expectation(description: "missing selection is repaired") + var setupResult: Result? + selectedAccountSettings.setup(runningCompletionIn: .main) { result in + setupResult = result + setupExpectation.fulfill() + } + + wait(for: [setupExpectation], timeout: Constants.defaultExpectationDuration) + + let selectedWallet = try XCTUnwrap(try setupResult?.get()) + XCTAssertEqual(selectedWallet, firstByOrder.info) + XCTAssertEqual(selectedAccountSettings.value, firstByOrder.info) + + let fetchOperation = repository.fetchAllOperation(with: RepositoryFetchOptions()) + operationQueue.addOperations([fetchOperation], waitUntilFinished: true) + let persistedAccounts: [ManagedMetaAccountModel] = try XCTUnwrap(fetchOperation.result).get() + let selectedAccounts = persistedAccounts.filter(\.isSelected) + + XCTAssertEqual(selectedAccounts.map(\.identifier), [firstByOrder.identifier]) + XCTAssertEqual(persistedAccounts.count, 2) + } + + func testSetupToleratesTonOnlyRowAlongsideSelectedValidWalletWithoutChangingEitherPayload() throws { + let operationQueue = OperationQueue() + let facade = UserDataStorageTestFacade() + let selectedAccountSettings = SelectedWalletSettings( + storageFacade: facade, + operationQueue: operationQueue + ) + let validWallet = ManagedMetaAccountModel( + info: AccountGenerator.generateMetaAccount(generatingChainAccounts: 1), + isSelected: true, + order: 2 + ) + + try seed([validWallet], in: facade, using: operationQueue) + try insertTonOnlyWallet( + identifier: "ton-only-unselected", + isSelected: false, + order: 1, + in: facade, + malformedChainAccount: true + ) + let before = try rawWalletSnapshots(in: facade) + let malformedChainAccountsBefore = try rawChainAccountSnapshots( + walletIdentifier: "ton-only-unselected", + in: facade + ) + + let result = try setup(selectedAccountSettings) + + XCTAssertEqual(result, validWallet.info) + XCTAssertEqual(selectedAccountSettings.value, validWallet.info) + XCTAssertEqual(selectedAccountSettings.storeState, .ready) + XCTAssertEqual(try rawWalletSnapshots(in: facade), before) + XCTAssertEqual( + try rawChainAccountSnapshots( + walletIdentifier: "ton-only-unselected", + in: facade + ), + malformedChainAccountsBefore + ) + } + + func testSetupWithOnlyTonOnlyWalletReportsUnsupportedWithoutMutatingRow() throws { + let operationQueue = OperationQueue() + let facade = UserDataStorageTestFacade() + let selectedAccountSettings = SelectedWalletSettings( + storageFacade: facade, + operationQueue: operationQueue + ) + + try insertTonOnlyWallet( + identifier: "ton-only-selected", + isSelected: true, + order: 1, + in: facade, + malformedChainAccount: true + ) + let before = try rawWalletSnapshots(in: facade) + let malformedChainAccountsBefore = try rawChainAccountSnapshots( + walletIdentifier: "ton-only-selected", + in: facade + ) + + let result = try setup(selectedAccountSettings) + + XCTAssertNil(result) + XCTAssertNil(selectedAccountSettings.value) + XCTAssertEqual(selectedAccountSettings.storeState, .unsupportedOnly) + XCTAssertEqual(try rawWalletSnapshots(in: facade), before) + XCTAssertEqual( + try rawChainAccountSnapshots( + walletIdentifier: "ton-only-selected", + in: facade + ), + malformedChainAccountsBefore + ) + } + + func testStartupQuarantinesMixedCorruptSupportedRowsAndPreservesEveryStoredField() throws { + let operationQueue = OperationQueue() + let facade = UserDataStorageTestFacade() + let settings = SelectedWalletSettings( + storageFacade: facade, + operationQueue: operationQueue + ) + let healthyWallet = copyWallet( + AccountGenerator.generateMetaAccount(generatingChainAccounts: 1), + identifier: "healthy-wallet" + ) + let corruptions = PersistedSupportedWalletCorruption.allCases + let corruptIdentifiers = corruptions.map { + "corrupt-\($0.identifier)" + } + let corruptOrders = Set( + corruptions.indices.map { Int32($0 + 100) } + ) + + try seed( + [ManagedMetaAccountModel(info: healthyWallet, isSelected: false)], + in: facade, + using: operationQueue + ) + + for (index, corruption) in corruptions.enumerated() { + try insertCorruptSupportedWallet( + identifier: corruptIdentifiers[index], + corruption: corruption, + isSelected: true, + order: Int32(index + 100), + in: facade, + using: operationQueue + ) + } + + let corruptRowsBefore = try rawStoredWalletSnapshots( + orders: corruptOrders, + in: facade + ) + let sortDescriptors = [ + NSSortDescriptor(key: #keyPath(CDMetaAccount.metaId), ascending: true) + ] + let factory = AccountRepositoryFactory(storageFacade: facade) + let repository = factory.createMetaAccountRepository( + for: nil, + sortDescriptors: sortDescriptors + ) + let managedRepository = factory.createManagedMetaAccountRepository( + for: nil, + sortDescriptors: sortDescriptors + ) + let options = RepositoryFetchOptions( + includesProperties: true, + includesSubentities: true + ) + let projectionRepository = facade.createRepository( + mapper: AnyCoreDataMapper(MetaAccountSelectionMapper()) + ) + let projections = try execute( + projectionRepository.fetchAllOperation(with: options), + using: operationQueue + ) + let corruptProjections = projections.filter { + $0.recordState == .corrupt + } + + XCTAssertEqual(corruptProjections.count, corruptions.count) + XCTAssertTrue(corruptProjections.allSatisfy { $0.wallet == nil }) + XCTAssertTrue( + corruptProjections.allSatisfy { + !$0.recordState.allowsStoredRecordUpdates + } + ) + XCTAssertTrue( + corruptProjections.allSatisfy { + let replacement = $0.replacingSelection(!$0.isSelected) + return replacement.recordState == .corrupt && + !replacement.updatesWalletPayload && + !replacement.updatesSelection + } + ) + XCTAssertEqual( + try execute( + repository.fetchAllOperation(with: options), + using: operationQueue + ), + [healthyWallet] + ) + XCTAssertEqual( + try execute( + managedRepository.fetchAllOperation(with: options), + using: operationQueue + ).map(\.identifier), + [healthyWallet.identifier] + ) + XCTAssertEqual( + try execute(repository.fetchCountOperation(), using: operationQueue), + 1 + ) + XCTAssertEqual( + try execute( + repository.fetchOperation( + by: { corruptIdentifiers + [healthyWallet.identifier] }, + options: options + ), + using: operationQueue + ), + [healthyWallet] + ) + + for identifier in corruptIdentifiers { + let corruptWallet: MetaAccountModel? = try execute( + repository.fetchOperation( + by: { identifier }, + options: options + ), + using: operationQueue + ) + XCTAssertNil(corruptWallet, "Corrupt row escaped quarantine: \(identifier)") + } + + let asyncExpectation = expectation(description: "corrupt-row tolerant async fetch completes") + var asyncResult: Result<[MetaAccountModel], Error>? + let asyncRepository = factory.createAsyncMetaAccountRepository( + for: nil, + sortDescriptors: sortDescriptors + ) + Task { + do { + asyncResult = .success(try await asyncRepository.fetchAll()) + } catch { + asyncResult = .failure(error) + } + asyncExpectation.fulfill() + } + wait(for: [asyncExpectation], timeout: Constants.defaultExpectationDuration) + XCTAssertEqual(try XCTUnwrap(asyncResult).get(), [healthyWallet]) + + XCTAssertEqual(try setup(settings), healthyWallet) + XCTAssertEqual(settings.value, healthyWallet) + XCTAssertEqual(settings.storeState, .ready) + XCTAssertEqual( + try rawStoredWalletSnapshots( + orders: corruptOrders, + in: facade + ), + corruptRowsBefore + ) + + _ = try execute( + repository.saveOperation({ [] }, { corruptIdentifiers }), + using: operationQueue + ) + _ = try execute( + repository.replaceOperation { [healthyWallet] }, + using: operationQueue + ) + + let collidingWallet = copyWallet( + AccountGenerator.generateMetaAccount(generatingChainAccounts: 1), + identifier: corruptIdentifiers[0] + ) + let collisionOperation = repository.saveOperation( + { [collidingWallet] }, + { [] } + ) + operationQueue.addOperations([collisionOperation], waitUntilFinished: true) + XCTAssertThrowsError(try XCTUnwrap(collisionOperation.result).get()) { error in + guard + let settingsError = error as? SelectedWalletSettingsError, + case .unsupportedWalletIdentifierConflict = settingsError + else { + return XCTFail("Unexpected collision error: \(error)") + } + } + + XCTAssertEqual( + try rawStoredWalletSnapshots( + orders: corruptOrders, + in: facade + ), + corruptRowsBefore + ) + XCTAssertEqual( + try execute( + repository.fetchAllOperation(with: options), + using: operationQueue + ), + [healthyWallet] + ) + } + + func testCorruptOnlyStoreReportsUnavailableAndPreservesEveryStoredField() throws { + let operationQueue = OperationQueue() + let facade = UserDataStorageTestFacade() + let settings = SelectedWalletSettings( + storageFacade: facade, + operationQueue: operationQueue + ) + let corruptions: [PersistedSupportedWalletCorruption] = [ + .malformedSubstrateAccountId, + .emptyMetaId, + .whitespaceMetaId, + .emptyName, + .whitespaceName + ] + let corruptOrders = Set( + corruptions.indices.map { Int32($0 + 500) } + ) + + for (index, corruption) in corruptions.enumerated() { + try insertCorruptSupportedWallet( + identifier: "corrupt-only-\(corruption.identifier)", + corruption: corruption, + isSelected: true, + order: Int32(index + 500), + in: facade, + using: operationQueue + ) + } + + let before = try rawStoredWalletSnapshots( + orders: corruptOrders, + in: facade + ) + + XCTAssertNil(try setup(settings)) + XCTAssertNil(settings.value) + XCTAssertEqual(settings.storeState, .unavailable) + XCTAssertEqual( + try rawStoredWalletSnapshots( + orders: corruptOrders, + in: facade + ), + before + ) + } + + func testStructurallyValidUnsupportedWalletKeepsUnsupportedOnlyWithCorruptSibling() throws { + let operationQueue = OperationQueue() + let facade = UserDataStorageTestFacade() + let settings = SelectedWalletSettings( + storageFacade: facade, + operationQueue: operationQueue + ) + + try insertTonOnlyWallet( + identifier: "valid-unsupported-ton-wallet", + isSelected: true, + order: 1, + in: facade, + malformedChainAccount: true + ) + try insertCorruptSupportedWallet( + identifier: "corrupt-supported-sibling", + corruption: .malformedEthereumAddress, + isSelected: true, + order: 700, + in: facade, + using: operationQueue + ) + let parentRowsBefore = try rawWalletSnapshots(in: facade) + let corruptRowBefore = try rawStoredWalletSnapshots( + orders: [700], + in: facade + ) + let tonChildrenBefore = try rawChainAccountSnapshots( + walletIdentifier: "valid-unsupported-ton-wallet", + in: facade + ) + + XCTAssertNil(try setup(settings)) + XCTAssertNil(settings.value) + XCTAssertEqual(settings.storeState, .unsupportedOnly) + XCTAssertEqual(try rawWalletSnapshots(in: facade), parentRowsBefore) + XCTAssertEqual( + try rawStoredWalletSnapshots(orders: [700], in: facade), + corruptRowBefore + ) + XCTAssertEqual( + try rawChainAccountSnapshots( + walletIdentifier: "valid-unsupported-ton-wallet", + in: facade + ), + tonChildrenBefore + ) + } + + func testSetupReplacesSelectedTonOnlyRowWithDeterministicValidFallbackAndPreservesPayloads() throws { + let operationQueue = OperationQueue() + let facade = UserDataStorageTestFacade() + let selectedAccountSettings = SelectedWalletSettings( + storageFacade: facade, + operationQueue: operationQueue + ) + let expectedFallback = ManagedMetaAccountModel( + info: AccountGenerator.generateMetaAccount(generatingChainAccounts: 1), + isSelected: false, + order: 2 + ) + let laterWallet = ManagedMetaAccountModel( + info: AccountGenerator.generateMetaAccount(generatingChainAccounts: 1), + isSelected: false, + order: 3 + ) + + // New rows receive their order from the mapper, so insert them separately to make + // the persisted order deterministic instead of relying on a batch's iteration order. + try seed([expectedFallback], in: facade, using: operationQueue) + try seed([laterWallet], in: facade, using: operationQueue) + try insertTonOnlyWallet( + identifier: "ton-only-selected", + isSelected: true, + order: 1, + in: facade + ) + let before = try rawWalletSnapshots(in: facade) + + let result = try setup(selectedAccountSettings) + let after = try rawWalletSnapshots(in: facade) + + XCTAssertEqual(result, expectedFallback.info) + XCTAssertEqual(selectedAccountSettings.value, expectedFallback.info) + XCTAssertEqual(selectedAccountSettings.storeState, .ready) + XCTAssertEqual(after.count, before.count) + XCTAssertEqual( + after.filter(\.isSelected).map(\.identifier), + [expectedFallback.identifier] + ) + + for original in before { + let updated = try XCTUnwrap( + after.first { $0.identifier == original.identifier } + ) + XCTAssertEqual(updated.payload, original.payload) + XCTAssertEqual(updated.order, original.order) + } + } + + func testSaveAfterUnsupportedOnlyAddsValidWalletWithoutOverwritingTonPayload() throws { + let operationQueue = OperationQueue() + let facade = UserDataStorageTestFacade() + let selectedAccountSettings = SelectedWalletSettings( + storageFacade: facade, + operationQueue: operationQueue + ) + + try insertTonOnlyWallet( + identifier: "ton-only-selected", + isSelected: true, + order: 7, + in: facade + ) + let before = try XCTUnwrap(rawWalletSnapshots(in: facade).first) + XCTAssertNil(try setup(selectedAccountSettings)) + XCTAssertEqual(selectedAccountSettings.storeState, .unsupportedOnly) + + let validWallet = AccountGenerator.generateMetaAccount(generatingChainAccounts: 1) + XCTAssertEqual( + try save(validWallet, in: selectedAccountSettings), + validWallet + ) + + let after = try rawWalletSnapshots(in: facade) + let preservedTonWallet = try XCTUnwrap( + after.first { $0.identifier == before.identifier } + ) + let newValidWallet = try XCTUnwrap( + after.first { $0.identifier == validWallet.identifier } + ) + + XCTAssertEqual(after.count, 2) + XCTAssertEqual(preservedTonWallet.payload, before.payload) + XCTAssertEqual(preservedTonWallet.order, before.order) + XCTAssertFalse(preservedTonWallet.isSelected) + XCTAssertTrue(newValidWallet.isSelected) + XCTAssertEqual(selectedAccountSettings.value, validWallet) + XCTAssertEqual(selectedAccountSettings.storeState, .ready) + } + + func testSaveRejectsIdentifierCollisionWithUnsupportedWalletWithoutMutatingStore() throws { + let operationQueue = OperationQueue() + let facade = UserDataStorageTestFacade() + let selectedAccountSettings = SelectedWalletSettings( + storageFacade: facade, + operationQueue: operationQueue + ) + let unsupportedIdentifier = "ton-only-selected" + + try insertTonOnlyWallet( + identifier: unsupportedIdentifier, + isSelected: true, + order: 1, + in: facade + ) + let before = try rawWalletSnapshots(in: facade) + XCTAssertNil(try setup(selectedAccountSettings)) + + let generatedWallet = AccountGenerator.generateMetaAccount(generatingChainAccounts: 1) + let collidingWallet = MetaAccountModel( + metaId: unsupportedIdentifier, + name: generatedWallet.name, + substrateAccountId: generatedWallet.substrateAccountId, + substrateCryptoType: generatedWallet.substrateCryptoType, + substratePublicKey: generatedWallet.substratePublicKey, + ethereumAddress: generatedWallet.ethereumAddress, + ethereumPublicKey: generatedWallet.ethereumPublicKey, + chainAccounts: generatedWallet.chainAccounts, + assetKeysOrder: generatedWallet.assetKeysOrder, + canExportEthereumMnemonic: generatedWallet.canExportEthereumMnemonic, + unusedChainIds: generatedWallet.unusedChainIds, + selectedCurrency: generatedWallet.selectedCurrency, + networkManagmentFilter: generatedWallet.networkManagmentFilter, + assetsVisibility: generatedWallet.assetsVisibility, + hasBackup: generatedWallet.hasBackup, + favouriteChainIds: generatedWallet.favouriteChainIds + ) + + XCTAssertThrowsError( + try save(collidingWallet, in: selectedAccountSettings) + ) { error in + guard + let settingsError = error as? SelectedWalletSettingsError, + case .unsupportedWalletIdentifierConflict = settingsError + else { + return XCTFail("Unexpected error: \(error)") + } + } + + XCTAssertEqual(try rawWalletSnapshots(in: facade), before) + XCTAssertNil(selectedAccountSettings.value) + XCTAssertEqual(selectedAccountSettings.storeState, .unsupportedOnly) + } + + func testAccountRepositoryDeleteAllRemovesSupportedAndUnsupportedWalletsForLogout() throws { + let operationQueue = OperationQueue() + let facade = UserDataStorageTestFacade() + let supportedWallet = ManagedMetaAccountModel( + info: AccountGenerator.generateMetaAccount(generatingChainAccounts: 1), + isSelected: true + ) + + try seed([supportedWallet], in: facade, using: operationQueue) + try insertTonOnlyWallet( + identifier: "ton-wallet-that-must-be-deleted-on-logout", + isSelected: false, + order: 2, + in: facade + ) + XCTAssertEqual(try rawWalletSnapshots(in: facade).count, 2) + + let repository = AccountRepositoryFactory(storageFacade: facade) + .createMetaAccountRepository(for: nil, sortDescriptors: []) + let deleteAllOperation = repository.deleteAllOperation() + operationQueue.addOperations([deleteAllOperation], waitUntilFinished: true) + _ = try XCTUnwrap(deleteAllOperation.result).get() + + XCTAssertEqual(try rawWalletSnapshots(in: facade), []) + } + + func testFilteredAccountRepositoryDeleteAllRemainsScopedAndIncludesUnsupportedMatches() throws { + let operationQueue = OperationQueue() + let facade = UserDataStorageTestFacade() + let deletedSupportedWallet = ManagedMetaAccountModel( + info: AccountGenerator.generateMetaAccount(generatingChainAccounts: 1), + isSelected: true + ) + let retainedSupportedWallet = ManagedMetaAccountModel( + info: AccountGenerator.generateMetaAccount(generatingChainAccounts: 1), + isSelected: false + ) + + try seed( + [deletedSupportedWallet, retainedSupportedWallet], + in: facade, + using: operationQueue + ) + try insertTonOnlyWallet( + identifier: "deleted-ton-wallet", + isSelected: false, + order: 3, + in: facade + ) + try insertTonOnlyWallet( + identifier: "retained-ton-wallet", + isSelected: false, + order: 4, + in: facade + ) + + let filter = NSPredicate( + format: "%K IN %@", + #keyPath(CDMetaAccount.metaId), + [deletedSupportedWallet.identifier, "deleted-ton-wallet"] + ) + let repository = AccountRepositoryFactory(storageFacade: facade) + .createMetaAccountRepository(for: filter, sortDescriptors: []) + let deleteAllOperation = repository.deleteAllOperation() + operationQueue.addOperations([deleteAllOperation], waitUntilFinished: true) + _ = try XCTUnwrap(deleteAllOperation.result).get() + + XCTAssertEqual( + try rawWalletSnapshots(in: facade).map(\.identifier), + [retainedSupportedWallet.identifier, "retained-ton-wallet"].sorted() + ) + } + + func testAccountRepositoriesProjectMixedStoreBeforePagingAndSupportAsyncReads() throws { + let operationQueue = OperationQueue() + let facade = UserDataStorageTestFacade() + let generatedFirst = AccountGenerator.generateMetaAccount(generatingChainAccounts: 1) + let generatedSecond = AccountGenerator.generateMetaAccount(generatingChainAccounts: 1) + let firstWallet = copyWallet(generatedFirst, identifier: "a-supported-wallet") + let secondWallet = copyWallet(generatedSecond, identifier: "c-supported-wallet") + + try seed( + [ + ManagedMetaAccountModel(info: firstWallet, isSelected: true), + ManagedMetaAccountModel(info: secondWallet, isSelected: false) + ], + in: facade, + using: operationQueue + ) + try insertTonOnlyWallet( + identifier: "b-ton-wallet", + isSelected: false, + order: 3, + in: facade + ) + let before = try rawWalletSnapshots(in: facade) + let sortDescriptors = [ + NSSortDescriptor(key: #keyPath(CDMetaAccount.metaId), ascending: true) + ] + let factory = AccountRepositoryFactory(storageFacade: facade) + let metaRepository = factory.createMetaAccountRepository( + for: nil, + sortDescriptors: sortDescriptors + ) + let managedRepository = factory.createManagedMetaAccountRepository( + for: nil, + sortDescriptors: sortDescriptors + ) + let options = RepositoryFetchOptions( + includesProperties: true, + includesSubentities: true + ) + + let allMetaAccounts = try execute( + metaRepository.fetchAllOperation(with: options), + using: operationQueue + ) + let allManagedAccounts = try execute( + managedRepository.fetchAllOperation(with: options), + using: operationQueue + ) + let firstPage = try execute( + metaRepository.fetchOperation( + by: RepositorySliceRequest(offset: 0, count: 1, reversed: false), + options: options + ), + using: operationQueue + ) + let secondPage = try execute( + metaRepository.fetchOperation( + by: RepositorySliceRequest(offset: 1, count: 1, reversed: false), + options: options + ), + using: operationQueue + ) + let supportedByIdentifier: MetaAccountModel? = try execute( + metaRepository.fetchOperation( + by: { firstWallet.identifier }, + options: options + ), + using: operationQueue + ) + let unsupportedByIdentifier: MetaAccountModel? = try execute( + metaRepository.fetchOperation( + by: { "b-ton-wallet" }, + options: options + ), + using: operationQueue + ) + let mixedByIdentifiers: [MetaAccountModel] = try execute( + metaRepository.fetchOperation( + by: { + [ + firstWallet.identifier, + "b-ton-wallet", + secondWallet.identifier + ] + }, + options: options + ), + using: operationQueue + ) + let count = try execute( + metaRepository.fetchCountOperation(), + using: operationQueue + ) + + XCTAssertEqual( + allMetaAccounts.map(\.identifier), + [firstWallet.identifier, secondWallet.identifier] + ) + XCTAssertEqual( + allManagedAccounts.map(\.identifier), + [firstWallet.identifier, secondWallet.identifier] + ) + XCTAssertEqual(firstPage.map(\.identifier), [firstWallet.identifier]) + XCTAssertEqual(secondPage.map(\.identifier), [secondWallet.identifier]) + XCTAssertEqual(supportedByIdentifier, firstWallet) + XCTAssertNil(unsupportedByIdentifier) + XCTAssertEqual( + mixedByIdentifiers.map(\.identifier), + [firstWallet.identifier, secondWallet.identifier] + ) + XCTAssertEqual(count, 2) + + let asyncExpectation = expectation(description: "async mixed-store fetch completes") + var asyncResult: Result<[MetaAccountModel], Error>? + let asyncRepository = factory.createAsyncMetaAccountRepository( + for: nil, + sortDescriptors: sortDescriptors + ) + Task { + do { + asyncResult = .success(try await asyncRepository.fetchAll()) + } catch { + asyncResult = .failure(error) + } + asyncExpectation.fulfill() + } + + wait(for: [asyncExpectation], timeout: Constants.defaultExpectationDuration) + XCTAssertEqual( + try XCTUnwrap(asyncResult).get().map(\.identifier), + [firstWallet.identifier, secondWallet.identifier] + ) + XCTAssertEqual(try rawWalletSnapshots(in: facade), before) + } + + func testRepositoryPagingSlicesHealthyProjectionAcrossPersistedCorruptRows() throws { + let operationQueue = OperationQueue() + let facade = UserDataStorageTestFacade() + let healthyWallets = [ + copyWallet( + AccountGenerator.generateMetaAccount(generatingChainAccounts: 1), + identifier: "b-healthy-first" + ), + copyWallet( + AccountGenerator.generateMetaAccount(generatingChainAccounts: 1), + identifier: "d-healthy-second" + ), + copyWallet( + AccountGenerator.generateMetaAccount(generatingChainAccounts: 1), + identifier: "e-healthy-third" + ) + ] + let corruptOrders: Set = [800, 801, 802] + + try seed( + healthyWallets.enumerated().map { + ManagedMetaAccountModel( + info: $0.element, + isSelected: $0.offset == 0 + ) + }, + in: facade, + using: operationQueue + ) + try insertCorruptSupportedWallet( + identifier: "a-corrupt-before", + corruption: .malformedSubstrateAccountId, + isSelected: false, + order: 800, + in: facade, + using: operationQueue + ) + try insertCorruptSupportedWallet( + identifier: "c-corrupt-inside", + corruption: .shortChainPublicKey, + isSelected: false, + order: 801, + in: facade, + using: operationQueue + ) + try insertCorruptSupportedWallet( + identifier: "f-corrupt-after", + corruption: .emptyName, + isSelected: false, + order: 802, + in: facade, + using: operationQueue + ) + + let corruptRowsBefore = try rawStoredWalletSnapshots( + orders: corruptOrders, + in: facade + ) + let sortDescriptors = [ + NSSortDescriptor(key: #keyPath(CDMetaAccount.metaId), ascending: true) + ] + let factory = AccountRepositoryFactory(storageFacade: facade) + let repository = factory.createMetaAccountRepository( + for: nil, + sortDescriptors: sortDescriptors + ) + let managedRepository = factory.createManagedMetaAccountRepository( + for: nil, + sortDescriptors: sortDescriptors + ) + let options = RepositoryFetchOptions( + includesProperties: true, + includesSubentities: true + ) + + func page( + offset: Int, + count: Int, + reversed: Bool + ) throws -> [String] { + try execute( + repository.fetchOperation( + by: RepositorySliceRequest( + offset: offset, + count: count, + reversed: reversed + ), + options: options + ), + using: operationQueue + ).map(\.identifier) + } + + func managedPage( + offset: Int, + count: Int, + reversed: Bool + ) throws -> [String] { + try execute( + managedRepository.fetchOperation( + by: RepositorySliceRequest( + offset: offset, + count: count, + reversed: reversed + ), + options: options + ), + using: operationQueue + ).map(\.identifier) + } + + let forwardIdentifiers = healthyWallets.map(\.identifier) + let reversedIdentifiers = Array(forwardIdentifiers.reversed()) + + XCTAssertEqual(try page(offset: 0, count: 1, reversed: false), [forwardIdentifiers[0]]) + XCTAssertEqual(try page(offset: 1, count: 1, reversed: false), [forwardIdentifiers[1]]) + XCTAssertEqual(try page(offset: 2, count: 1, reversed: false), [forwardIdentifiers[2]]) + XCTAssertEqual( + try page(offset: 0, count: 2, reversed: false), + Array(forwardIdentifiers.prefix(2)) + ) + XCTAssertEqual( + try page(offset: 1, count: 2, reversed: false), + Array(forwardIdentifiers.dropFirst()) + ) + XCTAssertEqual( + try page(offset: 0, count: Int.max, reversed: false), + forwardIdentifiers + ) + XCTAssertEqual(try page(offset: 0, count: 0, reversed: false), []) + XCTAssertEqual(try page(offset: 3, count: 1, reversed: false), []) + XCTAssertEqual( + try page(offset: Int.max, count: Int.max, reversed: false), + [] + ) + + XCTAssertEqual(try page(offset: 0, count: 1, reversed: true), [reversedIdentifiers[0]]) + XCTAssertEqual(try page(offset: 1, count: 1, reversed: true), [reversedIdentifiers[1]]) + XCTAssertEqual(try page(offset: 2, count: 1, reversed: true), [reversedIdentifiers[2]]) + XCTAssertEqual( + try page(offset: 0, count: 2, reversed: true), + Array(reversedIdentifiers.prefix(2)) + ) + XCTAssertEqual( + try page(offset: 1, count: 2, reversed: true), + Array(reversedIdentifiers.dropFirst()) + ) + XCTAssertEqual(try page(offset: 0, count: 0, reversed: true), []) + XCTAssertEqual(try page(offset: 3, count: 1, reversed: true), []) + + XCTAssertEqual( + try managedPage(offset: 0, count: 3, reversed: false), + forwardIdentifiers + ) + XCTAssertEqual( + try managedPage(offset: 0, count: 3, reversed: true), + reversedIdentifiers + ) + XCTAssertEqual( + try execute(repository.fetchCountOperation(), using: operationQueue), + healthyWallets.count + ) + XCTAssertEqual( + try rawStoredWalletSnapshots(orders: corruptOrders, in: facade), + corruptRowsBefore + ) + } + + func testAccountRepositoryUpdatePreservesUnsupportedPayloadAndWalletMetadata() throws { + let operationQueue = OperationQueue() + let facade = UserDataStorageTestFacade() + let wallet = ManagedMetaAccountModel( + info: AccountGenerator.generateMetaAccount(generatingChainAccounts: 1), + isSelected: true + ) + + try seed([wallet], in: facade, using: operationQueue) + try insertTonOnlyWallet( + identifier: "ton-wallet-preserved-across-update", + isSelected: false, + order: 17, + in: facade + ) + let before = try rawWalletSnapshots(in: facade) + let walletBefore = try XCTUnwrap( + before.first { $0.identifier == wallet.identifier } + ) + let tonBefore = try XCTUnwrap( + before.first { $0.identifier == "ton-wallet-preserved-across-update" } + ) + let renamedWallet = copyWallet( + wallet.info, + identifier: wallet.identifier, + name: "Renamed without collateral damage" + ) + let repository = AccountRepositoryFactory(storageFacade: facade) + .createMetaAccountRepository(for: nil, sortDescriptors: []) + + _ = try execute( + repository.saveOperation({ [renamedWallet] }, { [] }), + using: operationQueue + ) + + let after = try rawWalletSnapshots(in: facade) + let walletAfter = try XCTUnwrap( + after.first { $0.identifier == wallet.identifier } + ) + XCTAssertEqual( + after.first { $0.identifier == tonBefore.identifier }, + tonBefore + ) + XCTAssertEqual(walletAfter.order, walletBefore.order) + XCTAssertEqual(walletAfter.isSelected, walletBefore.isSelected) + XCTAssertEqual(walletAfter.payload, walletBefore.payload) + } + + func testAccountRepositoryReplacePreservesUnsupportedRowsAndDeletesOnlySupportedRows() throws { + let operationQueue = OperationQueue() + let facade = UserDataStorageTestFacade() + let removedWallet = copyWallet( + AccountGenerator.generateMetaAccount(generatingChainAccounts: 1), + identifier: "removed-supported-wallet" + ) + let retainedWallet = copyWallet( + AccountGenerator.generateMetaAccount(generatingChainAccounts: 1), + identifier: "retained-supported-wallet" + ) + + try seed( + [ + ManagedMetaAccountModel(info: removedWallet, isSelected: true), + ManagedMetaAccountModel(info: retainedWallet, isSelected: false) + ], + in: facade, + using: operationQueue + ) + try insertTonOnlyWallet( + identifier: "preserved-ton-wallet", + isSelected: false, + order: 3, + in: facade + ) + let tonBefore = try XCTUnwrap( + rawWalletSnapshots(in: facade).first { + $0.identifier == "preserved-ton-wallet" + } + ) + let renamedWallet = copyWallet( + retainedWallet, + identifier: retainedWallet.identifier, + name: "Retained and updated" + ) + let repository = AccountRepositoryFactory(storageFacade: facade) + .createMetaAccountRepository(for: nil, sortDescriptors: []) + + _ = try execute( + repository.replaceOperation { [renamedWallet] }, + using: operationQueue + ) + + let after = try rawWalletSnapshots(in: facade) + XCTAssertNil(after.first { $0.identifier == removedWallet.identifier }) + XCTAssertNotNil(after.first { $0.identifier == renamedWallet.identifier }) + XCTAssertEqual( + after.first { $0.identifier == tonBefore.identifier }, + tonBefore + ) + + let storedWallets = try execute( + repository.fetchAllOperation(with: RepositoryFetchOptions()), + using: operationQueue + ) + XCTAssertEqual(storedWallets, [renamedWallet]) + } + + func testAccountRepositoryRejectsUnsupportedIdentifierCollisionWithoutMutation() throws { + let operationQueue = OperationQueue() + let facade = UserDataStorageTestFacade() + let collidingIdentifier = "existing-ton-wallet" + + try insertTonOnlyWallet( + identifier: collidingIdentifier, + isSelected: true, + order: 1, + in: facade + ) + let before = try rawWalletSnapshots(in: facade) + let collidingWallet = copyWallet( + AccountGenerator.generateMetaAccount(generatingChainAccounts: 1), + identifier: collidingIdentifier + ) + let repository = AccountRepositoryFactory(storageFacade: facade) + .createMetaAccountRepository(for: nil, sortDescriptors: []) + let saveOperation = repository.saveOperation({ [collidingWallet] }, { [] }) + operationQueue.addOperations([saveOperation], waitUntilFinished: true) + + XCTAssertThrowsError(try XCTUnwrap(saveOperation.result).get()) { error in + guard + let settingsError = error as? SelectedWalletSettingsError, + case .unsupportedWalletIdentifierConflict = settingsError + else { + return XCTFail("Unexpected error: \(error)") + } + } + XCTAssertEqual(try rawWalletSnapshots(in: facade), before) + } + + func testManagedMapperFailsSafelyWhenWalletOrderHasReachedInt32Max() throws { + let operationQueue = OperationQueue() + let facade = UserDataStorageTestFacade() + let existingWallet = ManagedMetaAccountModel( + info: AccountGenerator.generateMetaAccount(generatingChainAccounts: 1), + isSelected: true + ) + + try seed([existingWallet], in: facade, using: operationQueue) + try setRawWalletOrder( + identifier: existingWallet.identifier, + order: Int32.max, + in: facade + ) + let before = try rawWalletSnapshots(in: facade) + let newWallet = ManagedMetaAccountModel( + info: AccountGenerator.generateMetaAccount(generatingChainAccounts: 1), + isSelected: false + ) + let repository = facade.createRepository( + mapper: AnyCoreDataMapper(ManagedMetaAccountMapper()) + ) + let saveOperation = repository.saveOperation({ [newWallet] }, { [] }) + operationQueue.addOperations([saveOperation], waitUntilFinished: true) + + XCTAssertThrowsError(try XCTUnwrap(saveOperation.result).get()) { error in + guard + let mapperError = error as? MetaAccountMapperError, + case .walletOrderOverflow = mapperError + else { + return XCTFail("Unexpected error: \(error)") + } + } + XCTAssertEqual(try rawWalletSnapshots(in: facade), before) + } + + func testSelectionMapperFailsSafelyWhenUnsupportedWalletOrderHasReachedInt32Max() throws { + let operationQueue = OperationQueue() + let facade = UserDataStorageTestFacade() + let selectedAccountSettings = SelectedWalletSettings( + storageFacade: facade, + operationQueue: operationQueue + ) + + try insertTonOnlyWallet( + identifier: "max-order-ton-wallet", + isSelected: true, + order: Int32.max, + in: facade + ) + let before = try rawWalletSnapshots(in: facade) + XCTAssertNil(try setup(selectedAccountSettings)) + XCTAssertEqual(selectedAccountSettings.storeState, .unsupportedOnly) + + XCTAssertThrowsError( + try save( + AccountGenerator.generateMetaAccount(generatingChainAccounts: 1), + in: selectedAccountSettings + ) + ) { error in + guard + let mapperError = error as? MetaAccountMapperError, + case .walletOrderOverflow = mapperError + else { + return XCTFail("Unexpected error: \(error)") + } + } + + XCTAssertEqual(try rawWalletSnapshots(in: facade), before) + XCTAssertNil(selectedAccountSettings.value) + XCTAssertEqual(selectedAccountSettings.storeState, .unsupportedOnly) + } + + func testOutOfOrderSetupCannotOverwriteLatestValueStateOrPersistStaleRepair() throws { + let operationQueue = OperationQueue() + operationQueue.maxConcurrentOperationCount = 8 + let currentFacade = UserDataStorageTestFacade() + let staleFacade = UserDataStorageTestFacade() + let firstWallet = copyWallet( + AccountGenerator.generateMetaAccount(generatingChainAccounts: 1), + identifier: "first-wallet" + ) + let latestWallet = copyWallet( + AccountGenerator.generateMetaAccount(generatingChainAccounts: 1), + identifier: "latest-wallet" + ) + + try seed( + [ManagedMetaAccountModel(info: firstWallet, isSelected: false)], + in: currentFacade, + using: operationQueue + ) + try seed( + [ManagedMetaAccountModel(info: latestWallet, isSelected: true)], + in: currentFacade, + using: operationQueue + ) + try seed( + [ManagedMetaAccountModel(info: firstWallet, isSelected: false)], + in: staleFacade, + using: operationQueue + ) + try seed( + [ManagedMetaAccountModel(info: latestWallet, isSelected: false)], + in: staleFacade, + using: operationQueue + ) + + let firstRequest = expectation(description: "first setup reaches Core Data") + let latestRequest = expectation(description: "latest setup reaches Core Data") + let controlledService = ControllableWalletCoreDataService( + configuration: currentFacade.databaseService.configuration, + requestExpectations: [firstRequest, latestRequest] + ) + let controlledFacade = ControllableWalletStorageFacade( + databaseService: controlledService + ) + let settings = SelectedWalletSettings( + storageFacade: controlledFacade, + operationQueue: operationQueue + ) + let staleCompletion = expectation(description: "stale setup completes") + let latestCompletion = expectation(description: "latest setup completes") + var staleResult: Result? + var latestResult: Result? + + settings.setup(runningCompletionIn: .main) { + staleResult = $0 + staleCompletion.fulfill() + } + wait(for: [firstRequest], timeout: Constants.defaultExpectationDuration) + + settings.setup(runningCompletionIn: .main) { + latestResult = $0 + latestCompletion.fulfill() + } + wait(for: [latestRequest], timeout: Constants.defaultExpectationDuration) + + try controlledService.resolveRequest( + at: 1, + using: currentFacade.databaseService + ) + wait(for: [latestCompletion], timeout: Constants.defaultExpectationDuration) + + XCTAssertEqual(try XCTUnwrap(latestResult).get(), latestWallet) + XCTAssertEqual(settings.value, latestWallet) + XCTAssertEqual(settings.storeState, .ready) + + try controlledService.resolveRequest( + at: 0, + using: staleFacade.databaseService + ) + wait(for: [staleCompletion], timeout: Constants.defaultExpectationDuration) + + XCTAssertEqual(try XCTUnwrap(staleResult).get(), firstWallet) + XCTAssertEqual(settings.value, latestWallet) + XCTAssertEqual(settings.storeState, .ready) + XCTAssertEqual(controlledService.requestCount, 2) + XCTAssertEqual( + try rawWalletSnapshots(in: currentFacade) + .filter(\.isSelected) + .map(\.identifier), + [latestWallet.identifier] + ) + XCTAssertEqual( + try rawWalletSnapshots(in: staleFacade) + .filter(\.isSelected) + .map(\.identifier), + [] + ) + } + + func testSetupGenerationCannotChangeInsideAtomicValueAndStateCommit() throws { + let operationQueue = OperationQueue() + operationQueue.maxConcurrentOperationCount = 8 + let firstFacade = UserDataStorageTestFacade() + let latestFacade = UserDataStorageTestFacade() + let firstWallet = copyWallet( + AccountGenerator.generateMetaAccount(generatingChainAccounts: 1), + identifier: "first-wallet" + ) + let latestWallet = copyWallet( + AccountGenerator.generateMetaAccount(generatingChainAccounts: 1), + identifier: "latest-wallet" + ) + + try seed( + [ManagedMetaAccountModel(info: firstWallet, isSelected: true)], + in: firstFacade, + using: operationQueue + ) + try seed( + [ManagedMetaAccountModel(info: latestWallet, isSelected: true)], + in: latestFacade, + using: operationQueue + ) + + let firstFetch = expectation(description: "first setup fetch starts") + let latestFetch = expectation(description: "latest setup waits for atomic commit") + let firstCommitEntered = expectation(description: "first setup begins atomic commit") + let releaseFirstCommit = DispatchSemaphore(value: 0) + let setupPrepared = DispatchSemaphore(value: 0) + let commitCountLock = NSLock() + var commitCount = 0 + let controlledService = ControllableWalletCoreDataService( + configuration: firstFacade.databaseService.configuration, + requestExpectations: [firstFetch, latestFetch] + ) + let settings = SelectedWalletSettings( + storageFacade: ControllableWalletStorageFacade( + databaseService: controlledService + ), + operationQueue: operationQueue, + setupPreparationHook: { + setupPrepared.signal() + }, + setupCommitHook: { + commitCountLock.lock() + commitCount += 1 + let shouldPause = commitCount == 1 + commitCountLock.unlock() + + if shouldPause { + firstCommitEntered.fulfill() + _ = releaseFirstCommit.wait( + timeout: .now() + Constants.defaultExpectationDuration + ) + } + } + ) + let firstCompletion = expectation(description: "first setup completes") + let latestInvocationStarted = expectation(description: "latest setup invocation starts") + let latestCompletion = expectation(description: "latest setup completes") + var firstResult: Result? + var latestResult: Result? + + settings.setup(runningCompletionIn: .main) { + firstResult = $0 + firstCompletion.fulfill() + } + XCTAssertEqual( + setupPrepared.wait( + timeout: .now() + Constants.defaultExpectationDuration + ), + .success + ) + wait(for: [firstFetch], timeout: Constants.defaultExpectationDuration) + try controlledService.resolveRequest( + at: 0, + using: firstFacade.databaseService + ) + wait(for: [firstCommitEntered], timeout: Constants.defaultExpectationDuration) + + DispatchQueue.global().async { + latestInvocationStarted.fulfill() + settings.setup(runningCompletionIn: .main) { + latestResult = $0 + latestCompletion.fulfill() + } + } + wait(for: [latestInvocationStarted], timeout: Constants.defaultExpectationDuration) + + XCTAssertEqual( + setupPrepared.wait(timeout: .now() + 0.2), + .timedOut + ) + XCTAssertEqual(controlledService.requestCount, 1) + + releaseFirstCommit.signal() + wait( + for: [firstCompletion, latestFetch], + timeout: Constants.defaultExpectationDuration + ) + XCTAssertEqual( + setupPrepared.wait( + timeout: .now() + Constants.defaultExpectationDuration + ), + .success + ) + + try controlledService.resolveRequest( + at: 1, + using: latestFacade.databaseService + ) + wait(for: [latestCompletion], timeout: Constants.defaultExpectationDuration) + + XCTAssertEqual(try XCTUnwrap(firstResult).get(), firstWallet) + XCTAssertEqual(try XCTUnwrap(latestResult).get(), latestWallet) + XCTAssertEqual(settings.value, latestWallet) + XCTAssertEqual(settings.storeState, .ready) + XCTAssertEqual(controlledService.requestCount, 2) + } + + func testConcurrentSavesAreSerializedAndOlderFailureCannotRollbackLatestSelection() throws { + let operationQueue = OperationQueue() + operationQueue.maxConcurrentOperationCount = 8 + let sourceFacade = UserDataStorageTestFacade() + let initialWallet = copyWallet( + AccountGenerator.generateMetaAccount(generatingChainAccounts: 1), + identifier: "initial-wallet" + ) + let olderWallet = copyWallet( + AccountGenerator.generateMetaAccount(generatingChainAccounts: 1), + identifier: "older-wallet" + ) + let latestWallet = copyWallet( + AccountGenerator.generateMetaAccount(generatingChainAccounts: 1), + identifier: "latest-wallet" + ) + + try seed( + [ManagedMetaAccountModel(info: initialWallet, isSelected: true)], + in: sourceFacade, + using: operationQueue + ) + try seed( + [ManagedMetaAccountModel(info: olderWallet, isSelected: false)], + in: sourceFacade, + using: operationQueue + ) + try seed( + [ManagedMetaAccountModel(info: latestWallet, isSelected: false)], + in: sourceFacade, + using: operationQueue + ) + + let olderFetch = expectation(description: "older save fetch starts") + let latestFetch = expectation(description: "latest save waits then fetches") + let latestPersist = expectation(description: "latest save persists") + let controlledService = ControllableWalletCoreDataService( + configuration: sourceFacade.databaseService.configuration, + requestExpectations: [olderFetch, latestFetch, latestPersist] + ) + let settings = SelectedWalletSettings( + storageFacade: ControllableWalletStorageFacade( + databaseService: controlledService + ), + operationQueue: operationQueue + ) + let olderCompletion = expectation(description: "older save fails") + let latestCompletion = expectation(description: "latest save succeeds") + var olderResult: Result? + var latestResult: Result? + + settings.save(value: olderWallet, runningCompletionIn: .main) { + olderResult = $0 + olderCompletion.fulfill() + } + wait(for: [olderFetch], timeout: Constants.defaultExpectationDuration) + + settings.save(value: latestWallet, runningCompletionIn: .main) { + latestResult = $0 + latestCompletion.fulfill() + } + + XCTAssertEqual(settings.value, latestWallet) + XCTAssertEqual(controlledService.requestCount, 1) + + try controlledService.failRequest( + at: 0, + with: ControllableWalletCoreDataServiceError.injectedFailure + ) + wait( + for: [olderCompletion, latestFetch], + timeout: Constants.defaultExpectationDuration + ) + + XCTAssertThrowsError(try XCTUnwrap(olderResult).get()) + XCTAssertEqual(settings.value, latestWallet) + + try controlledService.resolveRequest( + at: 1, + using: sourceFacade.databaseService + ) + wait(for: [latestPersist], timeout: Constants.defaultExpectationDuration) + try controlledService.resolveRequest( + at: 2, + using: sourceFacade.databaseService + ) + wait(for: [latestCompletion], timeout: Constants.defaultExpectationDuration) + + XCTAssertEqual(try XCTUnwrap(latestResult).get(), latestWallet) + XCTAssertEqual(settings.value, latestWallet) + XCTAssertEqual(settings.storeState, .ready) + XCTAssertEqual(controlledService.requestCount, 3) + XCTAssertEqual( + try rawWalletSnapshots(in: sourceFacade) + .filter(\.isSelected) + .map(\.identifier), + [latestWallet.identifier] + ) + } + + func testSaveInvalidatesOlderSetupBeforeItCanCommitOrRepairSelection() throws { + let operationQueue = OperationQueue() + operationQueue.maxConcurrentOperationCount = 8 + let currentFacade = UserDataStorageTestFacade() + let staleFacade = UserDataStorageTestFacade() + let firstWallet = copyWallet( + AccountGenerator.generateMetaAccount(generatingChainAccounts: 1), + identifier: "first-wallet" + ) + let savedWallet = copyWallet( + AccountGenerator.generateMetaAccount(generatingChainAccounts: 1), + identifier: "saved-wallet" + ) + + try seed( + [ManagedMetaAccountModel(info: firstWallet, isSelected: true)], + in: currentFacade, + using: operationQueue + ) + try seed( + [ManagedMetaAccountModel(info: savedWallet, isSelected: false)], + in: currentFacade, + using: operationQueue + ) + try seed( + [ManagedMetaAccountModel(info: firstWallet, isSelected: false)], + in: staleFacade, + using: operationQueue + ) + try seed( + [ManagedMetaAccountModel(info: savedWallet, isSelected: false)], + in: staleFacade, + using: operationQueue + ) + + let setupFetch = expectation(description: "setup fetch starts") + let saveFetch = expectation(description: "save fetch starts") + let savePersist = expectation(description: "save persists") + let controlledService = ControllableWalletCoreDataService( + configuration: currentFacade.databaseService.configuration, + requestExpectations: [setupFetch, saveFetch, savePersist] + ) + let settings = SelectedWalletSettings( + storageFacade: ControllableWalletStorageFacade( + databaseService: controlledService + ), + operationQueue: operationQueue + ) + let setupCompletion = expectation(description: "stale setup completes") + let saveCompletion = expectation(description: "save completes") + var setupResult: Result? + var saveResult: Result? + + settings.setup(runningCompletionIn: .main) { + setupResult = $0 + setupCompletion.fulfill() + } + wait(for: [setupFetch], timeout: Constants.defaultExpectationDuration) + + settings.save(value: savedWallet, runningCompletionIn: .main) { + saveResult = $0 + saveCompletion.fulfill() + } + wait(for: [saveFetch], timeout: Constants.defaultExpectationDuration) + + try controlledService.resolveRequest( + at: 1, + using: currentFacade.databaseService + ) + wait(for: [savePersist], timeout: Constants.defaultExpectationDuration) + try controlledService.resolveRequest( + at: 2, + using: currentFacade.databaseService + ) + wait(for: [saveCompletion], timeout: Constants.defaultExpectationDuration) + + XCTAssertEqual(try XCTUnwrap(saveResult).get(), savedWallet) + XCTAssertEqual(settings.value, savedWallet) + XCTAssertEqual(settings.storeState, .ready) + + try controlledService.resolveRequest( + at: 0, + using: staleFacade.databaseService + ) + wait(for: [setupCompletion], timeout: Constants.defaultExpectationDuration) + + XCTAssertEqual(try XCTUnwrap(setupResult).get(), firstWallet) + XCTAssertEqual(settings.value, savedWallet) + XCTAssertEqual(settings.storeState, .ready) + XCTAssertEqual(controlledService.requestCount, 3) + XCTAssertEqual( + try rawWalletSnapshots(in: currentFacade) + .filter(\.isSelected) + .map(\.identifier), + [savedWallet.identifier] + ) + XCTAssertEqual( + try rawWalletSnapshots(in: staleFacade) + .filter(\.isSelected) + .map(\.identifier), + [] + ) + } + + func testSetupStartedDuringSaveWaitsForPersistenceAndCannotReadStaleSelection() throws { + let asynchronousTimeout = + Constants.defaultExpectationDuration * 4 + let operationQueue = OperationQueue() + operationQueue.maxConcurrentOperationCount = 8 + let sourceFacade = UserDataStorageTestFacade() + let initialWallet = copyWallet( + AccountGenerator.generateMetaAccount(generatingChainAccounts: 1), + identifier: "initial-wallet" + ) + let savedWallet = copyWallet( + AccountGenerator.generateMetaAccount(generatingChainAccounts: 1), + identifier: "saved-wallet" + ) + + try seed( + [ManagedMetaAccountModel(info: initialWallet, isSelected: true)], + in: sourceFacade, + using: operationQueue + ) + try seed( + [ManagedMetaAccountModel(info: savedWallet, isSelected: false)], + in: sourceFacade, + using: operationQueue + ) + + let saveFetch = expectation(description: "save fetch starts") + let savePersist = expectation(description: "save persists") + let setupFetch = expectation(description: "setup starts after save") + let controlledService = ControllableWalletCoreDataService( + configuration: sourceFacade.databaseService.configuration, + requestExpectations: [saveFetch, savePersist, setupFetch] + ) + let settings = SelectedWalletSettings( + storageFacade: ControllableWalletStorageFacade( + databaseService: controlledService + ), + operationQueue: operationQueue + ) + let saveCompletion = expectation(description: "save completes") + let setupCompletion = expectation(description: "setup completes") + var saveResult: Result? + var setupResult: Result? + + settings.save(value: savedWallet, runningCompletionIn: .main) { + saveResult = $0 + saveCompletion.fulfill() + } + wait(for: [saveFetch], timeout: asynchronousTimeout) + + settings.setup(runningCompletionIn: .main) { + setupResult = $0 + setupCompletion.fulfill() + } + + XCTAssertEqual(controlledService.requestCount, 1) + XCTAssertEqual(settings.value, savedWallet) + + try controlledService.resolveRequest( + at: 0, + using: sourceFacade.databaseService + ) + wait(for: [savePersist], timeout: asynchronousTimeout) + try controlledService.resolveRequest( + at: 1, + using: sourceFacade.databaseService + ) + wait( + for: [saveCompletion, setupFetch], + timeout: asynchronousTimeout + ) + + XCTAssertEqual(try XCTUnwrap(saveResult).get(), savedWallet) + try controlledService.resolveRequest( + at: 2, + using: sourceFacade.databaseService + ) + wait(for: [setupCompletion], timeout: asynchronousTimeout) + + XCTAssertEqual(try XCTUnwrap(setupResult).get(), savedWallet) + XCTAssertEqual(settings.value, savedWallet) + XCTAssertEqual(settings.storeState, .ready) + XCTAssertEqual( + try rawWalletSnapshots(in: sourceFacade) + .filter(\.isSelected) + .map(\.identifier), + [savedWallet.identifier] + ) + } + + func testSaveRegistrationIsAtomicAgainstSetupAtPreparationBoundary() throws { + let operationQueue = OperationQueue() + operationQueue.maxConcurrentOperationCount = 8 + let sourceFacade = UserDataStorageTestFacade() + let initialWallet = copyWallet( + AccountGenerator.generateMetaAccount(generatingChainAccounts: 1), + identifier: "initial-wallet" + ) + let savedWallet = copyWallet( + AccountGenerator.generateMetaAccount(generatingChainAccounts: 1), + identifier: "saved-wallet" + ) + + try seed( + [ManagedMetaAccountModel(info: initialWallet, isSelected: true)], + in: sourceFacade, + using: operationQueue + ) + try seed( + [ManagedMetaAccountModel(info: savedWallet, isSelected: false)], + in: sourceFacade, + using: operationQueue + ) + + let saveFetch = expectation(description: "save fetch starts") + let savePersist = expectation(description: "save persists") + let setupFetch = expectation(description: "setup fetch waits for persistence") + let savePrepared = expectation(description: "save invalidates setup generation") + let setupPrepared = expectation(description: "setup replaces generation during paused save") + let releaseSavePreparation = DispatchSemaphore(value: 0) + let controlledService = ControllableWalletCoreDataService( + configuration: sourceFacade.databaseService.configuration, + requestExpectations: [saveFetch, savePersist, setupFetch] + ) + let settings = SelectedWalletSettings( + storageFacade: ControllableWalletStorageFacade( + databaseService: controlledService + ), + operationQueue: operationQueue, + savePreparationHook: { + savePrepared.fulfill() + _ = releaseSavePreparation.wait( + timeout: .now() + Constants.defaultExpectationDuration + ) + }, + setupPreparationHook: { + setupPrepared.fulfill() + } + ) + let saveInvocationReturned = expectation(description: "save invocation returns") + let setupInvocationReturned = expectation(description: "setup invocation returns") + let saveCompletion = expectation(description: "save completes") + let setupCompletion = expectation(description: "setup completes") + var saveResult: Result? + var setupResult: Result? + + DispatchQueue.global().async { + settings.save(value: savedWallet, runningCompletionIn: .main) { + saveResult = $0 + saveCompletion.fulfill() + } + saveInvocationReturned.fulfill() + } + wait(for: [savePrepared], timeout: Constants.defaultExpectationDuration) + + DispatchQueue.global().async { + settings.setup(runningCompletionIn: .main) { + setupResult = $0 + setupCompletion.fulfill() + } + setupInvocationReturned.fulfill() + } + wait(for: [setupPrepared], timeout: Constants.defaultExpectationDuration) + + XCTAssertEqual(controlledService.requestCount, 0) + + releaseSavePreparation.signal() + wait( + for: [saveFetch, saveInvocationReturned, setupInvocationReturned], + timeout: Constants.defaultExpectationDuration + ) + + XCTAssertEqual(controlledService.requestCount, 1) + XCTAssertEqual(settings.value, savedWallet) + + try controlledService.resolveRequest( + at: 0, + using: sourceFacade.databaseService + ) + wait(for: [savePersist], timeout: Constants.defaultExpectationDuration) + try controlledService.resolveRequest( + at: 1, + using: sourceFacade.databaseService + ) + wait( + for: [saveCompletion, setupFetch], + timeout: Constants.defaultExpectationDuration + ) + + try controlledService.resolveRequest( + at: 2, + using: sourceFacade.databaseService + ) + wait(for: [setupCompletion], timeout: Constants.defaultExpectationDuration) + + XCTAssertEqual(try XCTUnwrap(saveResult).get(), savedWallet) + XCTAssertEqual(try XCTUnwrap(setupResult).get(), savedWallet) + XCTAssertEqual(settings.value, savedWallet) + XCTAssertEqual(settings.storeState, .ready) + XCTAssertEqual(controlledService.requestCount, 3) + XCTAssertEqual( + try rawWalletSnapshots(in: sourceFacade) + .filter(\.isSelected) + .map(\.identifier), + [savedWallet.identifier] + ) + } + + func testSetupValueAndStoreStateCommitAtomicallyAgainstSaveRegistration() throws { + let operationQueue = OperationQueue() + operationQueue.maxConcurrentOperationCount = 8 + let sourceFacade = UserDataStorageTestFacade() + let savedWallet = copyWallet( + AccountGenerator.generateMetaAccount(generatingChainAccounts: 1), + identifier: "saved-wallet" + ) + + try insertTonOnlyWallet( + identifier: "unsupported-ton-wallet", + isSelected: true, + order: 0, + in: sourceFacade + ) + + let setupFetch = expectation(description: "setup fetch starts") + let saveFetch = expectation(description: "save fetch waits for setup commit") + let savePersist = expectation(description: "save persists") + let setupCommitEntered = expectation(description: "setup begins atomic commit") + let releaseSetupCommit = DispatchSemaphore(value: 0) + let savePreparationReached = DispatchSemaphore(value: 0) + let controlledService = ControllableWalletCoreDataService( + configuration: sourceFacade.databaseService.configuration, + requestExpectations: [setupFetch, saveFetch, savePersist] + ) + let settings = SelectedWalletSettings( + storageFacade: ControllableWalletStorageFacade( + databaseService: controlledService + ), + operationQueue: operationQueue, + savePreparationHook: { + savePreparationReached.signal() + }, + setupCommitHook: { + setupCommitEntered.fulfill() + _ = releaseSetupCommit.wait( + timeout: .now() + Constants.defaultExpectationDuration + ) + } + ) + let setupCompletion = expectation(description: "setup completes") + let saveInvocationStarted = expectation(description: "save invocation starts") + let saveCompletion = expectation(description: "save completes") + var setupResult: Result? + var saveResult: Result? + + settings.setup(runningCompletionIn: .main) { + setupResult = $0 + setupCompletion.fulfill() + } + wait(for: [setupFetch], timeout: Constants.defaultExpectationDuration) + try controlledService.resolveRequest( + at: 0, + using: sourceFacade.databaseService + ) + wait(for: [setupCommitEntered], timeout: Constants.defaultExpectationDuration) + + DispatchQueue.global().async { + saveInvocationStarted.fulfill() + settings.save(value: savedWallet, runningCompletionIn: .main) { + saveResult = $0 + saveCompletion.fulfill() + } + } + wait(for: [saveInvocationStarted], timeout: Constants.defaultExpectationDuration) + + XCTAssertEqual( + savePreparationReached.wait(timeout: .now() + 0.2), + .timedOut + ) + XCTAssertEqual(controlledService.requestCount, 1) + + releaseSetupCommit.signal() + wait( + for: [setupCompletion, saveFetch], + timeout: Constants.defaultExpectationDuration + ) + + XCTAssertNil(try XCTUnwrap(setupResult).get()) + XCTAssertEqual(settings.storeState, .unsupportedOnly) + XCTAssertEqual( + savePreparationReached.wait( + timeout: .now() + Constants.defaultExpectationDuration + ), + .success + ) + + try controlledService.resolveRequest( + at: 1, + using: sourceFacade.databaseService + ) + wait(for: [savePersist], timeout: Constants.defaultExpectationDuration) + try controlledService.resolveRequest( + at: 2, + using: sourceFacade.databaseService + ) + wait(for: [saveCompletion], timeout: Constants.defaultExpectationDuration) + + XCTAssertEqual(try XCTUnwrap(saveResult).get(), savedWallet) + XCTAssertEqual(settings.value, savedWallet) + XCTAssertEqual(settings.storeState, .ready) + XCTAssertEqual(controlledService.requestCount, 3) + XCTAssertEqual( + try rawWalletSnapshots(in: sourceFacade) + .filter(\.isSelected) + .map(\.identifier), + [savedWallet.identifier] + ) + } + + func testTolerantFactoriesAndStreamsIgnoreCorruptSelectedSiblingAndKeepObservingValidWallet() throws { + let facade = UserDataStorageTestFacade() + let repositoryQueue = OperationQueue() + let validWallet = copyWallet( + AccountGenerator.generateMetaAccount(generatingChainAccounts: 1), + identifier: "provider-valid-wallet", + name: "Provider Valid Wallet" + ) + let transitionWallet = copyWallet( + validWallet, + identifier: "provider-valid-to-corrupt-wallet", + name: "Provider Transition Wallet" + ) + let corruptIdentifier = "provider-corrupt-wallet" + let corruptIdentifiers = Set([corruptIdentifier]) + + try seed( + [ + ManagedMetaAccountModel(info: validWallet, isSelected: true), + ManagedMetaAccountModel(info: transitionWallet, isSelected: true) + ], + in: facade, + using: repositoryQueue + ) + + try performCoreData(in: facade) { context in + let corruptWallet = self.copyWallet( + validWallet, + identifier: corruptIdentifier, + name: "Provider Corrupt Wallet" + ) + let entity = CDMetaAccount(context: context) + try MetaAccountMapper().populate( + entity: entity, + from: corruptWallet, + using: context + ) + entity.isSelected = true + entity.order = 900 + entity.name = "" + let child = try XCTUnwrap( + entity.chainAccounts?.allObjects.first as? CDChainAccount + ) + // The account-ID provider must match this corrupt row through its child + // relationship, not merely through the primary account columns. + child.accountId = validWallet.substrateAccountId.toHex() + + // A structurally corrupt sibling with the same persistent identifier must + // not make the healthy row disappear from tolerant reads. + let duplicateWallet = self.copyWallet( + validWallet, + identifier: validWallet.identifier, + name: "Provider Duplicate Corrupt Wallet" + ) + let duplicateEntity = CDMetaAccount(context: context) + try MetaAccountMapper().populate( + entity: duplicateEntity, + from: duplicateWallet, + using: context + ) + duplicateEntity.isSelected = true + duplicateEntity.order = 899 + duplicateEntity.name = "" + let duplicateChild = try XCTUnwrap( + duplicateEntity.chainAccounts?.allObjects.first as? CDChainAccount + ) + duplicateChild.accountId = validWallet.substrateAccountId.toHex() + try context.save() + } + + let accountRepositoryFactory = AccountRepositoryFactory(storageFacade: facade) + let metaRepository = accountRepositoryFactory.createMetaAccountRepository( + for: nil, + sortDescriptors: [] + ) + let managedRepository = accountRepositoryFactory.createManagedMetaAccountRepository( + for: NSPredicate.selectedMetaAccount(), + sortDescriptors: [] + ) + + XCTAssertEqual( + Set(try execute( + metaRepository.fetchAllOperation( + with: RepositoryFetchOptions( + includesProperties: true, + includesSubentities: true + ) + ), + using: repositoryQueue + ).map(\.identifier)), + Set([validWallet.identifier, transitionWallet.identifier]) + ) + XCTAssertEqual( + Set(try execute( + managedRepository.fetchAllOperation( + with: RepositoryFetchOptions( + includesProperties: true, + includesSubentities: true + ) + ), + using: repositoryQueue + ).map(\.identifier)), + Set([validWallet.identifier, transitionWallet.identifier]) + ) + + let operationManager = OperationManager() + let providerFactory = AccountProviderFactory( + storageFacade: facade, + operationManager: operationManager + ) + let accountProvider = providerFactory.createStreambleProvider( + for: validWallet.substrateAccountId + ) + let managedProvider = providerFactory.createManagedMetaAccountProvider( + for: NSPredicate.selectedMetaAccount(), + sortDescriptors: [] + ) + let accountObserver = NSObject() + let managedObserver = NSObject() + let accountInitial = expectation(description: "account stream returns healthy initial wallet") + let managedInitial = expectation(description: "managed stream returns healthy initial wallet") + let accountUpdate = expectation(description: "account stream observes healthy update") + let managedUpdate = expectation(description: "managed stream observes healthy update") + let accountTransitionDelete = expectation( + description: "account stream removes a wallet that becomes corrupt" + ) + let managedTransitionDelete = expectation( + description: "managed stream removes a wallet that becomes corrupt" + ) + let accountTransitionRestore = expectation( + description: "account stream reinserts a repaired wallet" + ) + let managedTransitionRestore = expectation( + description: "managed stream reinserts a repaired wallet" + ) + let updatedName = "Provider Valid Wallet Updated" + let restoredName = "Provider Transition Wallet Restored" + var accountInitialDelivered = false + var managedInitialDelivered = false + + accountProvider.addObserver( + accountObserver, + deliverOn: .main, + executing: { changes in + let wallets = changes.compactMap { change -> MetaAccountModel? in + switch change { + case let .insert(newItem), let .update(newItem): + return newItem + case let .delete(deletedIdentifier): + if deletedIdentifier == transitionWallet.identifier { + accountTransitionDelete.fulfill() + } + if deletedIdentifier == validWallet.identifier { + XCTFail("A corrupt duplicate deleted the healthy account stream item") + } + return nil + } + } + + XCTAssertTrue( + wallets.allSatisfy { + !corruptIdentifiers.contains($0.identifier) + } + ) + if wallets.contains(where: { $0.name == updatedName }) { + accountUpdate.fulfill() + } else if wallets.contains(where: { $0.name == restoredName }) { + accountTransitionRestore.fulfill() + } else if !accountInitialDelivered { + accountInitialDelivered = true + XCTAssertEqual( + Set(wallets.map(\.identifier)), + Set([validWallet.identifier, transitionWallet.identifier]) + ) + accountInitial.fulfill() + } + }, + failing: { error in + XCTFail("Account stream failed beside a corrupt sibling: \(error)") + }, + options: StreamableProviderObserverOptions(refreshWhenEmpty: false) + ) + managedProvider.addObserver( + managedObserver, + deliverOn: .main, + executing: { changes in + let wallets = changes.compactMap { change -> ManagedMetaAccountModel? in + switch change { + case let .insert(newItem), let .update(newItem): + return newItem + case let .delete(deletedIdentifier): + if deletedIdentifier == transitionWallet.identifier { + managedTransitionDelete.fulfill() + } + if deletedIdentifier == validWallet.identifier { + XCTFail("A corrupt duplicate deleted the healthy managed stream item") + } + return nil + } + } + + XCTAssertTrue( + wallets.allSatisfy { + !corruptIdentifiers.contains($0.identifier) + } + ) + if wallets.contains(where: { $0.info.name == updatedName }) { + managedUpdate.fulfill() + } else if wallets.contains(where: { $0.info.name == restoredName }) { + managedTransitionRestore.fulfill() + } else if !managedInitialDelivered { + managedInitialDelivered = true + XCTAssertEqual( + Set(wallets.map(\.identifier)), + Set([validWallet.identifier, transitionWallet.identifier]) + ) + XCTAssertTrue(wallets.allSatisfy(\.isSelected)) + managedInitial.fulfill() + } + }, + failing: { error in + XCTFail("Managed wallet stream failed beside a corrupt sibling: \(error)") + }, + options: StreamableProviderObserverOptions(refreshWhenEmpty: false) + ) + + wait( + for: [accountInitial, managedInitial], + timeout: Constants.defaultExpectationDuration + ) + + // The observable tracks validity per Core Data object, so invalidating one + // previously valid wallet emits a delete and repairing it emits an insert. + try performCoreData(in: facade) { context in + let request = NSFetchRequest(entityName: "CDMetaAccount") + request.predicate = NSPredicate( + format: "%K == %@", + #keyPath(CDMetaAccount.metaId), + transitionWallet.identifier + ) + let entity = try XCTUnwrap(context.fetch(request).first) + entity.name = " \n\t " + try context.save() + } + wait( + for: [accountTransitionDelete, managedTransitionDelete], + timeout: Constants.defaultExpectationDuration + ) + try performCoreData(in: facade) { context in + let request = NSFetchRequest(entityName: "CDMetaAccount") + request.predicate = NSPredicate( + format: "%K == %@", + #keyPath(CDMetaAccount.metaId), + transitionWallet.identifier + ) + let entity = try XCTUnwrap(context.fetch(request).first) + entity.name = restoredName + try context.save() + } + wait( + for: [accountTransitionRestore, managedTransitionRestore], + timeout: Constants.defaultExpectationDuration + ) + + // Exercise update and delete notifications for a pre-existing corrupt duplicate. + try performCoreData(in: facade) { context in + let request = NSFetchRequest(entityName: "CDMetaAccount") + request.predicate = NSPredicate( + format: "%K == %@", + #keyPath(CDMetaAccount.metaId), + validWallet.identifier + ) + let entity = try XCTUnwrap( + context.fetch(request).first { + ($0.name ?? "").trimmingCharacters( + in: .whitespacesAndNewlines + ).isEmpty + } + ) + entity.name = " \n\t " + try context.save() + context.delete(entity) + try context.save() + } + + try performCoreData(in: facade) { context in + let insertedCorruptWallet = self.copyWallet( + AccountGenerator.generateMetaAccount(generatingChainAccounts: 1), + identifier: validWallet.identifier, + name: "Inserted Corrupt Wallet" + ) + let entity = CDMetaAccount(context: context) + try MetaAccountMapper().populate( + entity: entity, + from: insertedCorruptWallet, + using: context + ) + entity.isSelected = true + entity.order = 901 + entity.name = "" + let child = try XCTUnwrap( + entity.chainAccounts?.allObjects.first as? CDChainAccount + ) + child.accountId = validWallet.substrateAccountId.toHex() + try context.save() + + // Exercise an update notification for the same still-corrupt matching row. + entity.name = " \n\t " + try context.save() + + // A corrupt duplicate was never tracked as valid, so its raw identifier + // must not be emitted as a delete for the healthy sibling. + context.delete(entity) + try context.save() + } + + try performCoreData(in: facade) { context in + let request = NSFetchRequest(entityName: "CDMetaAccount") + request.predicate = NSPredicate( + format: "%K == %@", + #keyPath(CDMetaAccount.metaId), + validWallet.identifier + ) + let entity = try XCTUnwrap( + context.fetch(request).first { + $0.name == validWallet.name + } + ) + entity.name = updatedName + try context.save() + } + + wait( + for: [accountUpdate, managedUpdate], + timeout: Constants.defaultExpectationDuration + ) + accountProvider.removeObserver(accountObserver) + managedProvider.removeObserver(managedObserver) + + let corruptProjections = try performCoreData(in: facade) { context in + let request = NSFetchRequest(entityName: "CDMetaAccount") + request.predicate = NSPredicate( + format: "%K IN %@", + #keyPath(CDMetaAccount.metaId), + Array(corruptIdentifiers) + ) + return try context.fetch(request).map { + try MetaAccountSelectionMapper().transform(entity: $0) + } + } + XCTAssertEqual(corruptProjections.count, corruptIdentifiers.count) + XCTAssertTrue(corruptProjections.allSatisfy { $0.recordState == .corrupt }) + XCTAssertTrue(corruptProjections.allSatisfy { $0.wallet == nil }) + } + + func testRepositorySaveRejectsEquivalentChainAccountAliasesBeforeMutatingStoredWallet() throws { + let facade = UserDataStorageTestFacade() + let operationQueue = OperationQueue() + let identifier = "duplicate-alias-save-wallet" + let generatedWallet = copyWallet( + AccountGenerator.generateMetaAccount(generatingChainAccounts: 0), + identifier: identifier + ) + let storedWallet = replacingChainAccounts( + in: generatedWallet, + with: [ + ChainAccountModel( + chainId: UniversalWalletRegistry.bitcoinMainnet.chainId, + accountId: Data(repeating: 0x10, count: 32), + publicKey: Data(repeating: 0x20, count: 32), + cryptoType: CryptoType.sr25519.rawValue, + ethereumBased: false + ) + ] + ) + try seed( + [ManagedMetaAccountModel(info: storedWallet, isSelected: true)], + in: facade, + using: operationQueue + ) + try setRawWalletOrder(identifier: identifier, order: 1_100, in: facade) + let before = try rawStoredWalletSnapshots( + orders: [1_100], + in: facade + ) + + let duplicateModels = [ + replacingChainAccounts( + in: storedWallet, + with: [ + ChainAccountModel( + chainId: "duplicate-remote-chain", + accountId: Data(repeating: 0x11, count: 32), + publicKey: Data(repeating: 0x21, count: 32), + cryptoType: CryptoType.sr25519.rawValue, + ethereumBased: false + ), + ChainAccountModel( + chainId: "duplicate-remote-chain", + accountId: Data(repeating: 0x12, count: 32), + publicKey: Data(repeating: 0x22, count: 32), + cryptoType: CryptoType.sr25519.rawValue, + ethereumBased: false + ) + ] + ), + replacingChainAccounts( + in: storedWallet, + with: [ + ChainAccountModel( + chainId: UniversalWalletRegistry.bitcoinMainnet.chainId, + accountId: Data(repeating: 0x13, count: 32), + publicKey: Data(repeating: 0x23, count: 32), + cryptoType: CryptoType.sr25519.rawValue, + ethereumBased: false + ), + ChainAccountModel( + chainId: UniversalWalletRegistry.bitcoinMainnet.id, + accountId: Data(repeating: 0x14, count: 32), + publicKey: Data(repeating: 0x24, count: 32), + cryptoType: CryptoType.sr25519.rawValue, + ethereumBased: false + ) + ] + ) + ] + let malformedModels = [ + ChainAccountModel( + chainId: "", + accountId: Data(repeating: 0x31, count: 32), + publicKey: Data(repeating: 0x41, count: 32), + cryptoType: CryptoType.sr25519.rawValue, + ethereumBased: false + ), + ChainAccountModel( + chainId: " padded-chain-id ", + accountId: Data(repeating: 0x32, count: 32), + publicKey: Data(repeating: 0x42, count: 32), + cryptoType: CryptoType.sr25519.rawValue, + ethereumBased: false + ), + ChainAccountModel( + chainId: "empty-account-id", + accountId: Data(), + publicKey: Data(repeating: 0x43, count: 32), + cryptoType: CryptoType.sr25519.rawValue, + ethereumBased: false + ), + ChainAccountModel( + chainId: "short-public-key", + accountId: Data(repeating: 0x34, count: 32), + publicKey: Data(repeating: 0x44, count: 31), + cryptoType: CryptoType.sr25519.rawValue, + ethereumBased: false + ), + ChainAccountModel( + chainId: "ecdsa-wrong-public-key-width", + accountId: Data(repeating: 0x35, count: 32), + publicKey: Data(repeating: 0x45, count: 32), + cryptoType: CryptoType.ecdsa.rawValue, + ethereumBased: false + ), + ChainAccountModel( + chainId: "invalid-crypto-type", + accountId: Data(repeating: 0x36, count: 32), + publicKey: Data(repeating: 0x46, count: 32), + cryptoType: UInt8.max, + ethereumBased: false + ) + ].map { + replacingChainAccounts(in: storedWallet, with: [$0]) + } + let repository = AccountRepositoryFactory(storageFacade: facade) + .createMetaAccountRepository(for: nil, sortDescriptors: []) + + for invalidModel in duplicateModels + malformedModels { + let saveOperation = repository.saveOperation( + { [invalidModel] }, + { [] } + ) + XCTAssertThrowsError( + try execute(saveOperation, using: operationQueue) + ) { error in + guard case MetaAccountMapperError.invalidWalletRecord = error else { + return XCTFail( + "Expected duplicate child validation error, got \(error)" + ) + } + } + XCTAssertEqual( + try rawStoredWalletSnapshots(orders: [1_100], in: facade), + before + ) + } + + // Replacing one persisted alias with its canonical equivalent must update + // the existing child rather than append a second equivalent child. + let aliasReplacement = replacingChainAccounts( + in: storedWallet, + with: [ + ChainAccountModel( + chainId: UniversalWalletRegistry.bitcoinMainnet.id, + accountId: Data(repeating: 0x15, count: 32), + publicKey: Data(repeating: 0x25, count: 32), + cryptoType: CryptoType.sr25519.rawValue, + ethereumBased: false + ) + ] + ) + _ = try execute( + repository.saveOperation( + { [aliasReplacement] }, + { [] } + ), + using: operationQueue + ) + + let afterAliasReplacement = try rawStoredWalletSnapshots( + orders: [1_100], + in: facade + ) + XCTAssertEqual(afterAliasReplacement.count, 1) + XCTAssertEqual(afterAliasReplacement[0].chainAccounts.count, 1) + XCTAssertEqual( + afterAliasReplacement[0].chainAccounts[0].chainId, + UniversalWalletRegistry.bitcoinMainnet.id + ) + } + + func testDeleteAndReplaceRejectDuplicateStoredIdentifiersInBothInsertionOrdersWithoutMutation() throws { + for supportedFirst in [true, false] { + let facade = UserDataStorageTestFacade() + let operationQueue = OperationQueue() + let identifier = supportedFirst + ? "collision-supported-first" + : "collision-unsupported-first" + let supportedOrder: Int32 = 1_150 + let unsupportedOrder: Int32 = 1_151 + + let insertSupported = { + try self.insertWallet( + identifier: identifier, + isSelected: true, + order: supportedOrder, + childSpecs: [], + in: facade + ) + } + let insertUnsupported = { + try self.insertTonOnlyWallet( + identifier: identifier, + isSelected: false, + order: unsupportedOrder, + in: facade + ) + } + if supportedFirst { + try insertSupported() + try insertUnsupported() + } else { + try insertUnsupported() + try insertSupported() + } + + let orders = Set([supportedOrder, unsupportedOrder]) + let before = try rawStoredWalletSnapshots( + orders: orders, + in: facade + ) + XCTAssertEqual(before.count, 2) + + let repository = AccountRepositoryFactory(storageFacade: facade) + .createMetaAccountRepository(for: nil, sortDescriptors: []) + let deleteOperation = repository.saveOperation( + { [] }, + { [identifier] } + ) + XCTAssertThrowsError( + try execute(deleteOperation, using: operationQueue) + ) { error in + guard + let settingsError = error as? SelectedWalletSettingsError, + case .duplicateWalletIdentifier = settingsError + else { + return XCTFail( + "Expected duplicate delete collision, got \(error)" + ) + } + } + XCTAssertEqual( + try rawStoredWalletSnapshots(orders: orders, in: facade), + before + ) + + let replaceOperation = repository.replaceOperation { [] } + XCTAssertThrowsError( + try execute(replaceOperation, using: operationQueue) + ) { error in + guard + let settingsError = error as? SelectedWalletSettingsError, + case .duplicateWalletIdentifier = settingsError + else { + return XCTFail( + "Expected duplicate replace collision, got \(error)" + ) + } + } + XCTAssertEqual( + try rawStoredWalletSnapshots(orders: orders, in: facade), + before + ) + } + } + + func testPersistedDuplicateAndUniversalAliasChainAccountsAreAlwaysQuarantinedWithoutMutation() throws { + typealias ChildSpec = (chainId: String, marker: UInt8) + typealias Fixture = (identifier: String, children: [ChildSpec]) + + let exactForward: [ChildSpec] = [ + ("remote-chain-id", 0x01), + ("remote-chain-id", 0x02) + ] + let exactReverse = Array(exactForward.reversed()) + let aliasPairs: [(String, String)] = [ + ( + UniversalWalletRegistry.bitcoinMainnet.chainId, + UniversalWalletRegistry.bitcoinMainnet.id + ), + ( + UniversalWalletRegistry.bitcoinTestnet.chainId, + UniversalWalletRegistry.bitcoinTestnet.id + ), + ( + UniversalWalletRegistry.solanaMainnet.chainId, + UniversalWalletRegistry.solanaMainnet.id.uppercased() + ), + ( + UniversalWalletRegistry.solanaDevnet.chainId, + UniversalWalletRegistry.solanaDevnet.id + ), + ( + UniversalWalletRegistry.tonMainnetRegistryEntry.chainId, + UniversalWalletRegistry.tonMainnetRegistryEntry.id + ), + ( + TonChainSelection.mainnetChainId, + UniversalWalletRegistry.tonMainnetRegistryEntry.id + ), + ( + UniversalWalletRegistry.taira.chainId, + UniversalWalletRegistry.taira.id + ), + ( + UniversalWalletRegistry.nexus.chainId, + UniversalWalletRegistry.nexus.id + ) + ] + var fixtures: [Fixture] = [ + ("duplicate-exact-forward", exactForward), + ("duplicate-exact-reverse", exactReverse) + ] + + for (index, pair) in aliasPairs.enumerated() { + let forward: [ChildSpec] = [ + (pair.0, UInt8(index * 2 + 10)), + (pair.1, UInt8(index * 2 + 11)) + ] + fixtures.append(("duplicate-alias-\(index)-forward", forward)) + fixtures.append(("duplicate-alias-\(index)-reverse", Array(forward.reversed()))) + } + + let facade = UserDataStorageTestFacade() + let operationQueue = OperationQueue() + let healthyWallet = copyWallet( + AccountGenerator.generateMetaAccount(generatingChainAccounts: 1), + identifier: "duplicate-control-healthy" + ) + try seed( + [ManagedMetaAccountModel(info: healthyWallet, isSelected: true)], + in: facade, + using: operationQueue + ) + + let fixtureOrders = Set( + fixtures.indices.map { Int32(1_200 + $0) } + ) + for (index, fixture) in fixtures.enumerated() { + try insertWallet( + identifier: fixture.identifier, + isSelected: true, + order: Int32(1_200 + index), + childSpecs: fixture.children, + in: facade + ) + } + + let before = try rawStoredWalletSnapshots( + orders: fixtureOrders, + in: facade + ) + let fixtureIdentifiers = Set(fixtures.map(\.identifier)) + let mappingStates = try performCoreData(in: facade) { context in + let request = NSFetchRequest(entityName: "CDMetaAccount") + request.predicate = NSPredicate( + format: "%K IN %@", + #keyPath(CDMetaAccount.metaId), + Array(fixtureIdentifiers) + ) + return try context.fetch(request).reduce(into: [String: Bool]()) { + result, + entity in + let identifier = try XCTUnwrap(entity.metaId) + do { + _ = try MetaAccountMapper().transform(entity: entity) + result[identifier] = false + } catch MetaAccountMapperError.invalidWalletRecord { + result[identifier] = true + } + } + } + XCTAssertEqual(Set(mappingStates.keys), fixtureIdentifiers) + XCTAssertTrue(mappingStates.values.allSatisfy { $0 }) + + let projectionRepository = facade.createRepository( + mapper: AnyCoreDataMapper(MetaAccountSelectionMapper()) + ) + let projections = try execute( + projectionRepository.fetchAllOperation( + with: RepositoryFetchOptions( + includesProperties: true, + includesSubentities: true + ) + ), + using: operationQueue + ) + let duplicateProjections = projections.filter { + fixtureIdentifiers.contains($0.identifier) + } + XCTAssertEqual(duplicateProjections.count, fixtures.count) + XCTAssertTrue(duplicateProjections.allSatisfy { $0.recordState == .corrupt }) + XCTAssertTrue(duplicateProjections.allSatisfy { $0.wallet == nil }) + + let tolerantRepository = AccountRepositoryFactory(storageFacade: facade) + .createMetaAccountRepository(for: nil, sortDescriptors: []) + XCTAssertEqual( + try execute( + tolerantRepository.fetchAllOperation( + with: RepositoryFetchOptions( + includesProperties: true, + includesSubentities: true + ) + ), + using: operationQueue + ).map(\.identifier), + [healthyWallet.identifier] + ) + XCTAssertEqual( + try rawStoredWalletSnapshots(orders: fixtureOrders, in: facade), + before + ) + } + + private func setup(_ settings: SelectedWalletSettings) throws -> MetaAccountModel? { + let setupExpectation = expectation(description: "selected wallet setup completes") + var setupResult: Result? + + settings.setup(runningCompletionIn: .main) { result in + setupResult = result + setupExpectation.fulfill() + } + + wait(for: [setupExpectation], timeout: Constants.defaultExpectationDuration) + return try XCTUnwrap(setupResult).get() + } + + private func save( + _ wallet: MetaAccountModel, + in settings: SelectedWalletSettings + ) throws -> MetaAccountModel { + let saveExpectation = expectation(description: "selected wallet save completes") + var saveResult: Result? + + settings.save(value: wallet, runningCompletionIn: .main) { result in + saveResult = result + saveExpectation.fulfill() + } + + wait(for: [saveExpectation], timeout: Constants.defaultExpectationDuration) + return try XCTUnwrap(saveResult).get() + } + + private func execute( + _ operation: BaseOperation, + using operationQueue: OperationQueue + ) throws -> Result { + operationQueue.addOperations([operation], waitUntilFinished: true) + return try XCTUnwrap(operation.result).get() + } + + private func copyWallet( + _ wallet: MetaAccountModel, + identifier: String, + name: String? = nil + ) -> MetaAccountModel { + MetaAccountModel( + metaId: identifier, + name: name ?? wallet.name, + substrateAccountId: wallet.substrateAccountId, + substrateCryptoType: wallet.substrateCryptoType, + substratePublicKey: wallet.substratePublicKey, + ethereumAddress: wallet.ethereumAddress, + ethereumPublicKey: wallet.ethereumPublicKey, + chainAccounts: wallet.chainAccounts, + assetKeysOrder: wallet.assetKeysOrder, + canExportEthereumMnemonic: wallet.canExportEthereumMnemonic, + unusedChainIds: wallet.unusedChainIds, + selectedCurrency: wallet.selectedCurrency, + networkManagmentFilter: wallet.networkManagmentFilter, + assetsVisibility: wallet.assetsVisibility, + hasBackup: wallet.hasBackup, + favouriteChainIds: wallet.favouriteChainIds + ) + } + + private func replacingChainAccounts( + in wallet: MetaAccountModel, + with chainAccounts: Set + ) -> MetaAccountModel { + MetaAccountModel( + metaId: wallet.metaId, + name: wallet.name, + substrateAccountId: wallet.substrateAccountId, + substrateCryptoType: wallet.substrateCryptoType, + substratePublicKey: wallet.substratePublicKey, + ethereumAddress: wallet.ethereumAddress, + ethereumPublicKey: wallet.ethereumPublicKey, + chainAccounts: chainAccounts, + assetKeysOrder: wallet.assetKeysOrder, + canExportEthereumMnemonic: wallet.canExportEthereumMnemonic, + unusedChainIds: wallet.unusedChainIds, + selectedCurrency: wallet.selectedCurrency, + networkManagmentFilter: wallet.networkManagmentFilter, + assetsVisibility: wallet.assetsVisibility, + hasBackup: wallet.hasBackup, + favouriteChainIds: wallet.favouriteChainIds + ) + } + + private func seed( + _ wallets: [ManagedMetaAccountModel], + in facade: UserDataStorageTestFacade, + using operationQueue: OperationQueue + ) throws { + let repository = facade.createRepository( + mapper: AnyCoreDataMapper(ManagedMetaAccountMapper()) + ) + let saveOperation = repository.saveOperation({ wallets }, { [] }) + operationQueue.addOperations([saveOperation], waitUntilFinished: true) + guard let saveResult = saveOperation.result else { + throw WalletCorruptionTestFixtureError( + message: "Managed wallet seed operation completed without a result" + ) + } + _ = try saveResult.get() + } + + private func insertWallet( + identifier: String, + isSelected: Bool, + order: Int32, + childSpecs: [(chainId: String, marker: UInt8)], + in facade: UserDataStorageTestFacade + ) throws { + try performCoreData(in: facade) { context in + let wallet = self.copyWallet( + AccountGenerator.generateMetaAccount(generatingChainAccounts: 0), + identifier: identifier + ) + let entity = CDMetaAccount(context: context) + try MetaAccountMapper().populate( + entity: entity, + from: wallet, + using: context + ) + entity.isSelected = isSelected + entity.order = order + + for childSpec in childSpecs { + let child = CDChainAccount(context: context) + child.chainId = childSpec.chainId + child.accountId = Data(repeating: childSpec.marker, count: 32).toHex() + child.publicKey = Data( + repeating: childSpec.marker &+ 0x40, + count: 32 + ) + child.cryptoType = Int16(CryptoType.sr25519.rawValue) + child.ethereumBased = false + entity.addToChainAccounts(child) + } + + try context.save() + } + } + + private func insertTonOnlyWallet( + identifier: String, + isSelected: Bool, + order: Int32, + in facade: UserDataStorageTestFacade, + malformedChainAccount: Bool = false + ) throws { + try performCoreData(in: facade) { context in + let wallet = CDMetaAccount(context: context) + wallet.metaId = identifier + wallet.name = "Preserved TON Wallet" + wallet.isSelected = isSelected + wallet.order = order + wallet.canExportEthereumMnemonic = false + wallet.hasBackup = false + wallet.favouriteChainIds = NSArray() + wallet.setValue(false, forKey: "zeroBalanceAssetsHidden") + wallet.setValue(Data(repeating: 0x41, count: 36), forKey: "tonAddress") + wallet.setValue(Data(repeating: 0x42, count: 32), forKey: "tonPublicKey") + wallet.setValue("v5R1", forKey: "tonContractVersion") + + if malformedChainAccount { + let chainAccount = CDChainAccount(context: context) + chainAccount.accountId = "not-valid-hex" + chainAccount.chainId = "foreign-chain" + chainAccount.publicKey = Data(repeating: 0x43, count: 32) + wallet.addToChainAccounts(chainAccount) + } + + try context.save() + } + } + + private func insertCorruptSupportedWallet( + identifier: String, + corruption: PersistedSupportedWalletCorruption, + isSelected: Bool, + order: Int32, + in facade: UserDataStorageTestFacade, + using operationQueue: OperationQueue + ) throws { + let wallet = copyWallet( + AccountGenerator.generateMetaAccount(generatingChainAccounts: 1), + identifier: identifier + ) + try seed( + [ManagedMetaAccountModel(info: wallet, isSelected: isSelected)], + in: facade, + using: operationQueue + ) + + try performCoreData(in: facade) { context in + let request = NSFetchRequest(entityName: "CDMetaAccount") + request.predicate = NSPredicate( + format: "%K == %@", + #keyPath(CDMetaAccount.metaId), + identifier + ) + guard let storedWallet = try context.fetch(request).first else { + throw WalletCorruptionTestFixtureError( + message: "Seeded wallet \(identifier) was not found for corruption \(corruption.identifier)" + ) + } + storedWallet.isSelected = isSelected + storedWallet.order = order + + let chainAccount = { + storedWallet.chainAccounts?.allObjects.first as? CDChainAccount + } + func requiredChainAccount() throws -> CDChainAccount { + guard let chainAccount = chainAccount() else { + throw WalletCorruptionTestFixtureError( + message: "Seeded wallet \(identifier) has no child for corruption \(corruption.identifier)" + ) + } + return chainAccount + } + + switch corruption { + case .malformedSubstrateAccountId: + storedWallet.substrateAccountId = "not-hex" + case .shortSubstrateAccountId: + storedWallet.substrateAccountId = "01" + case .emptySubstratePublicKey: + storedWallet.substratePublicKey = Data() + case .shortSubstratePublicKey: + storedWallet.substratePublicKey = Data(repeating: 0x51, count: 31) + case .ecdsaWithWrongSubstratePublicKeyLength: + storedWallet.substrateCryptoType = Int16(CryptoType.ecdsa.rawValue) + case .invalidSubstrateCryptoType: + storedWallet.substrateCryptoType = Int16.max + case .malformedEthereumAddress: + storedWallet.ethereumAddress = "not-hex" + case .shortEthereumAddress: + storedWallet.ethereumAddress = "01" + case .emptyEthereumPublicKey: + storedWallet.ethereumPublicKey = Data() + case .emptyChainAccountId: + try requiredChainAccount().accountId = "" + case .malformedChainAccountId: + try requiredChainAccount().accountId = "not-hex" + case .emptyChainId: + try requiredChainAccount().chainId = "" + case .whitespaceChainId: + try requiredChainAccount().chainId = " \n\t " + case .paddedChainId: + try requiredChainAccount().chainId = " padded-chain-id " + case .emptyChainPublicKey: + try requiredChainAccount().publicKey = Data() + case .shortChainPublicKey: + try requiredChainAccount().publicKey = Data(repeating: 0x52, count: 31) + case .ecdsaWithWrongChainPublicKeyLength: + try requiredChainAccount().cryptoType = Int16(CryptoType.ecdsa.rawValue) + case .invalidChainCryptoType: + try requiredChainAccount().cryptoType = Int16.max + case .emptyMetaId: + storedWallet.metaId = "" + case .whitespaceMetaId: + storedWallet.metaId = " \n\t " + case .paddedMetaId: + storedWallet.metaId = " \(identifier) " + case .emptyName: + storedWallet.name = "" + case .whitespaceName: + storedWallet.name = " \n\t " + } + + try context.save() + } + } + + private func rawStoredWalletSnapshots( + orders: Set, + in facade: UserDataStorageTestFacade + ) throws -> [RawStoredWalletSnapshot] { + try performCoreData(in: facade) { context in + let request = NSFetchRequest(entityName: "CDMetaAccount") + let wallets = try context.fetch(request).filter { + orders.contains($0.order) + } + + return wallets.map { wallet in + let chainAccounts = ( + wallet.chainAccounts?.allObjects as? [CDChainAccount] ?? [] + ).map { + RawStoredChainAccountSnapshot( + accountId: $0.accountId, + publicKey: $0.publicKey, + cryptoType: $0.cryptoType, + ethereumBased: $0.ethereumBased, + chainId: $0.chainId, + ecosystem: $0.entity.propertiesByName["ecosystem"] == nil + ? nil + : $0.value(forKey: "ecosystem") as? String + ) + } + .sorted { $0.sortKey < $1.sortKey } + + let currency = wallet.selectedCurrency.map { + RawStoredCurrencySnapshot( + id: $0.id, + symbol: $0.symbol, + name: $0.name, + icon: $0.icon, + isSelected: $0.isSelected + ) + } + let assetsVisibility = ( + wallet.entity.propertiesByName["assetsVisibility"] == nil + ? [] + : (wallet.value(forKey: "assetsVisibility") as? NSSet)? + .allObjects as? [NSManagedObject] ?? [] + ).map { + RawStoredAssetVisibilitySnapshot( + assetId: $0.value(forKey: "assetId") as? String, + hidden: ($0.value(forKey: "hidden") as? Bool) ?? false + ) + } + .sorted { ($0.assetId ?? "") < ($1.assetId ?? "") } + + return RawStoredWalletSnapshot( + objectIdentifier: wallet.objectID.uriRepresentation().absoluteString, + metaId: wallet.metaId, + name: wallet.name, + isSelected: wallet.isSelected, + order: wallet.order, + substrateAccountId: wallet.substrateAccountId, + substratePublicKey: wallet.substratePublicKey, + substrateCryptoType: wallet.substrateCryptoType, + ethereumAddress: wallet.ethereumAddress, + ethereumPublicKey: wallet.ethereumPublicKey, + chainAccounts: chainAccounts, + assetKeysOrder: wallet.assetKeysOrder as? [String], + canExportEthereumMnemonic: wallet.canExportEthereumMnemonic, + unusedChainIds: wallet.unusedChainIds as? [String], + selectedCurrency: currency, + networkManagmentFilter: wallet.networkManagmentFilter, + hasBackup: wallet.hasBackup, + favouriteChainIds: wallet.favouriteChainIds as? [String], + zeroBalanceAssetsHidden: wallet.entity + .propertiesByName["zeroBalanceAssetsHidden"] == nil + ? nil + : wallet.value(forKey: "zeroBalanceAssetsHidden") as? Bool, + tonAddress: wallet.entity.propertiesByName["tonAddress"] == nil + ? nil + : wallet.value(forKey: "tonAddress") as? Data, + tonPublicKey: wallet.entity.propertiesByName["tonPublicKey"] == nil + ? nil + : wallet.value(forKey: "tonPublicKey") as? Data, + tonContractVersion: wallet.entity + .propertiesByName["tonContractVersion"] == nil + ? nil + : wallet.value(forKey: "tonContractVersion") as? String, + assetsVisibility: assetsVisibility + ) + } + .sorted { $0.objectIdentifier < $1.objectIdentifier } + } + } + + private func rawChainAccountSnapshots( + walletIdentifier: String, + in facade: UserDataStorageTestFacade + ) throws -> [RawChainAccountSnapshot] { + try performCoreData(in: facade) { context in + let request = NSFetchRequest(entityName: "CDMetaAccount") + request.predicate = NSPredicate( + format: "%K == %@", + #keyPath(CDMetaAccount.metaId), + walletIdentifier + ) + let wallet = try XCTUnwrap(context.fetch(request).first) + let chainAccounts = wallet.chainAccounts?.allObjects as? [CDChainAccount] ?? [] + + return chainAccounts.map { + RawChainAccountSnapshot( + accountId: $0.accountId, + chainId: $0.chainId, + publicKey: $0.publicKey + ) + } + .sorted { ($0.chainId ?? "") < ($1.chainId ?? "") } + } + } + + private func setRawWalletOrder( + identifier: String, + order: Int32, + in facade: UserDataStorageTestFacade + ) throws { + try performCoreData(in: facade) { context in + let request = NSFetchRequest(entityName: "CDMetaAccount") + request.predicate = NSPredicate( + format: "%K == %@", + #keyPath(CDMetaAccount.metaId), + identifier + ) + let wallet = try XCTUnwrap(context.fetch(request).first) + wallet.order = order + try context.save() + } + } + + private func rawWalletSnapshots( + in facade: UserDataStorageTestFacade + ) throws -> [RawWalletSnapshot] { + try performCoreData(in: facade) { context in + let request = NSFetchRequest(entityName: "CDMetaAccount") + let wallets = try context.fetch(request) + + return try wallets.map { wallet in + RawWalletSnapshot( + identifier: try XCTUnwrap(wallet.metaId), + isSelected: wallet.isSelected, + order: wallet.order, + substrateAccountId: wallet.substrateAccountId, + substratePublicKey: wallet.substratePublicKey, + tonAddress: wallet.value(forKey: "tonAddress") as? Data, + tonPublicKey: wallet.value(forKey: "tonPublicKey") as? Data, + tonContractVersion: wallet.value(forKey: "tonContractVersion") as? String + ) + } + .sorted { $0.identifier < $1.identifier } + } + } + + private func performCoreData( + in facade: UserDataStorageTestFacade, + _ body: @escaping (NSManagedObjectContext) throws -> T + ) throws -> T { + let coreDataExpectation = expectation(description: "Core Data operation completes") + var operationResult: Result? + + facade.databaseService.performAsync { context, error in + do { + if let error { + throw error + } + guard let context else { + throw WalletCorruptionTestFixtureError( + message: "Core Data callback completed without a context or error" + ) + } + operationResult = .success(try body(context)) + } catch { + operationResult = .failure(error) + } + + coreDataExpectation.fulfill() + } + + wait(for: [coreDataExpectation], timeout: Constants.defaultExpectationDuration) + guard let operationResult else { + throw WalletCorruptionTestFixtureError( + message: "Core Data callback did not produce a result" + ) + } + return try operationResult.get() + } +} + +private struct WalletCorruptionTestFixtureError: Error, CustomStringConvertible { + let message: String + + var description: String { + message + } +} + +/// Corruptions that the current Core Data model can persist. Required-field `nil` cases are +/// exercised directly against unsaved entities in `MetaAccountMapperTests`. +private enum PersistedSupportedWalletCorruption: CaseIterable { + case malformedSubstrateAccountId + case shortSubstrateAccountId + case emptySubstratePublicKey + case shortSubstratePublicKey + case ecdsaWithWrongSubstratePublicKeyLength + case invalidSubstrateCryptoType + case malformedEthereumAddress + case shortEthereumAddress + case emptyEthereumPublicKey + case emptyChainAccountId + case malformedChainAccountId + case emptyChainId + case whitespaceChainId + case paddedChainId + case emptyChainPublicKey + case shortChainPublicKey + case ecdsaWithWrongChainPublicKeyLength + case invalidChainCryptoType + case emptyMetaId + case whitespaceMetaId + case paddedMetaId + case emptyName + case whitespaceName + + var identifier: String { + switch self { + case .malformedSubstrateAccountId: + return "malformed-substrate-id" + case .shortSubstrateAccountId: + return "short-substrate-id" + case .emptySubstratePublicKey: + return "empty-substrate-public-key" + case .shortSubstratePublicKey: + return "short-substrate-public-key" + case .ecdsaWithWrongSubstratePublicKeyLength: + return "ecdsa-wrong-substrate-public-key-length" + case .invalidSubstrateCryptoType: + return "invalid-substrate-crypto-type" + case .malformedEthereumAddress: + return "malformed-ethereum-address" + case .shortEthereumAddress: + return "short-ethereum-address" + case .emptyEthereumPublicKey: + return "empty-ethereum-public-key" + case .emptyChainAccountId: + return "empty-child-account-id" + case .malformedChainAccountId: + return "malformed-child-account-id" + case .emptyChainId: + return "empty-child-chain-id" + case .whitespaceChainId: + return "whitespace-child-chain-id" + case .paddedChainId: + return "padded-child-chain-id" + case .emptyChainPublicKey: + return "empty-child-public-key" + case .shortChainPublicKey: + return "short-child-public-key" + case .ecdsaWithWrongChainPublicKeyLength: + return "ecdsa-wrong-child-public-key-length" + case .invalidChainCryptoType: + return "invalid-child-crypto-type" + case .emptyMetaId: + return "empty-meta-id" + case .whitespaceMetaId: + return "whitespace-meta-id" + case .paddedMetaId: + return "padded-meta-id" + case .emptyName: + return "empty-name" + case .whitespaceName: + return "whitespace-name" + } + } +} + +private struct RawStoredWalletSnapshot: Equatable { + let objectIdentifier: String + let metaId: String? + let name: String? + let isSelected: Bool + let order: Int32 + let substrateAccountId: String? + let substratePublicKey: Data? + let substrateCryptoType: Int16 + let ethereumAddress: String? + let ethereumPublicKey: Data? + let chainAccounts: [RawStoredChainAccountSnapshot] + let assetKeysOrder: [String]? + let canExportEthereumMnemonic: Bool + let unusedChainIds: [String]? + let selectedCurrency: RawStoredCurrencySnapshot? + let networkManagmentFilter: String? + let hasBackup: Bool + let favouriteChainIds: [String]? + let zeroBalanceAssetsHidden: Bool? + let tonAddress: Data? + let tonPublicKey: Data? + let tonContractVersion: String? + let assetsVisibility: [RawStoredAssetVisibilitySnapshot] +} + +private struct RawStoredChainAccountSnapshot: Equatable { + let accountId: String? + let publicKey: Data? + let cryptoType: Int16 + let ethereumBased: Bool + let chainId: String? + let ecosystem: String? + + var sortKey: String { + [ + chainId ?? "", + accountId ?? "", + publicKey?.base64EncodedString() ?? "", + String(cryptoType), + String(ethereumBased), + ecosystem ?? "" + ].joined(separator: "|") + } +} + +private struct RawStoredCurrencySnapshot: Equatable { + let id: String? + let symbol: String? + let name: String? + let icon: String? + let isSelected: Bool +} + +private struct RawStoredAssetVisibilitySnapshot: Equatable { + let assetId: String? + let hidden: Bool +} + +private struct RawWalletSnapshot: Equatable { + struct Payload: Equatable { + let substrateAccountId: String? + let substratePublicKey: Data? + let tonAddress: Data? + let tonPublicKey: Data? + let tonContractVersion: String? + } + + let identifier: String + let isSelected: Bool + let order: Int32 + let substrateAccountId: String? + let substratePublicKey: Data? + let tonAddress: Data? + let tonPublicKey: Data? + let tonContractVersion: String? + + var payload: Payload { + Payload( + substrateAccountId: substrateAccountId, + substratePublicKey: substratePublicKey, + tonAddress: tonAddress, + tonPublicKey: tonPublicKey, + tonContractVersion: tonContractVersion + ) + } +} + +private struct RawChainAccountSnapshot: Equatable { + let accountId: String? + let chainId: String? + let publicKey: Data? +} + +private enum ControllableWalletCoreDataServiceError: Error { + case missingRequest + case requestAlreadyResolved + case injectedFailure +} + +private final class ControllableWalletCoreDataService: CoreDataServiceProtocol { + let configuration: CoreDataServiceConfigurationProtocol + + private let lock = NSLock() + private let requestExpectations: [XCTestExpectation] + private var requests: [CoreDataContextInvocationBlock] = [] + private var resolvedRequestIndexes = Set() + + var requestCount: Int { + lock.lock() + defer { lock.unlock() } + + return requests.count + } + + init( + configuration: CoreDataServiceConfigurationProtocol, + requestExpectations: [XCTestExpectation] + ) { + self.configuration = configuration + self.requestExpectations = requestExpectations + } + + func performAsync(block: @escaping CoreDataContextInvocationBlock) { + lock.lock() + let requestIndex = requests.count + requests.append(block) + let requestExpectation = requestExpectations[safe: requestIndex] + lock.unlock() + + requestExpectation?.fulfill() + } + + func resolveRequest( + at index: Int, + using sourceService: CoreDataServiceProtocol + ) throws { + lock.lock() + guard requests.indices.contains(index) else { + lock.unlock() + throw ControllableWalletCoreDataServiceError.missingRequest + } + guard resolvedRequestIndexes.insert(index).inserted else { + lock.unlock() + throw ControllableWalletCoreDataServiceError.requestAlreadyResolved + } + let request = requests[index] + lock.unlock() + + sourceService.performAsync(block: request) + } + + func failRequest(at index: Int, with error: Error) throws { + lock.lock() + guard requests.indices.contains(index) else { + lock.unlock() + throw ControllableWalletCoreDataServiceError.missingRequest + } + guard resolvedRequestIndexes.insert(index).inserted else { + lock.unlock() + throw ControllableWalletCoreDataServiceError.requestAlreadyResolved + } + let request = requests[index] + lock.unlock() + + request(nil, error) + } + + func close() throws {} + + func drop() throws {} +} + +private final class ControllableWalletStorageFacade: StorageFacadeProtocol { + let databaseService: CoreDataServiceProtocol + + init(databaseService: CoreDataServiceProtocol) { + self.databaseService = databaseService + } + + func createRepository( + filter: NSPredicate?, + sortDescriptors: [NSSortDescriptor], + mapper: AnyCoreDataMapper + ) -> CoreDataRepository where T: Identifiable, U: NSManagedObject { + CoreDataRepository( + databaseService: databaseService, + mapper: mapper, + filter: filter, + sortDescriptors: sortDescriptors + ) + } + + func createAsyncRepository( + filter: NSPredicate?, + sortDescriptors: [NSSortDescriptor], + mapper: AnyCoreDataMapper + ) -> AsyncCoreDataRepositoryDefault where T: Identifiable, U: NSManagedObject { + AsyncCoreDataRepositoryDefault( + databaseService: databaseService, + mapper: mapper, + filter: filter, + sortDescriptors: sortDescriptors + ) + } } diff --git a/fearlessTests/Common/Storage/TransformableArchiveSafetyTests.swift b/fearlessTests/Common/Storage/TransformableArchiveSafetyTests.swift new file mode 100644 index 0000000000..1bc24f66be --- /dev/null +++ b/fearlessTests/Common/Storage/TransformableArchiveSafetyTests.swift @@ -0,0 +1,846 @@ +import CoreData +import IrohaCrypto +import RobinHood +import SQLite3 +import SSFModels +import XCTest +@testable import fearless + +final class TransformableArchiveSafetyTests: XCTestCase { + private static let invalidArchiveHex = "00FF00" + + func testObjectiveCExceptionBoundaryReturnsOnlySanitizedError() throws { + let secretReason = "archive payload and wallet metadata" + do { + _ = try FearlessObjectiveCExceptionCatcher.performObjectRead { + NSException( + name: .invalidArgumentException, + reason: secretReason, + userInfo: ["private": secretReason] + ).raise() + return nil + } + XCTFail("Raised Objective-C exception must become a Swift error") + } catch { + let sanitizedError = error as NSError + XCTAssertEqual( + sanitizedError.domain, + FearlessObjectiveCExceptionErrorDomain + ) + XCTAssertEqual(sanitizedError.code, 1) + XCTAssertEqual( + Set( + sanitizedError.userInfo.keys.map( + String.init(describing:) + ) + ), + Set([NSLocalizedDescriptionKey]) + ) + XCTAssertFalse( + sanitizedError.localizedDescription.contains(secretReason) + ) + XCTAssertFalse( + String(describing: sanitizedError.userInfo) + .contains(secretReason) + ) + } + } + + func testObjectiveCExceptionBoundaryAllowsSuccessfulVoidOperation() throws { + var didRun = false + + try SafeObjectiveCExceptionBoundary.perform { + didRun = true + } + + XCTAssertTrue(didRun) + } + + func testPersistentCorruptFavouriteChainIdsDefaultsOnlyPreferenceWithoutMutation() throws { + try assertPersistentCorruptWalletTransformable( + attribute: "favouriteChainIds", + column: "ZFAVOURITECHAINIDS" + ) + } + + func testPersistentCorruptAssetKeysOrderDefaultsOnlyPreferenceWithoutMutation() throws { + try assertPersistentCorruptWalletTransformable( + attribute: "assetKeysOrder", + column: "ZASSETKEYSORDER" + ) + } + + func testPersistentCorruptUnusedChainIdsDefaultsOnlyPreferenceWithoutMutation() throws { + try assertPersistentCorruptWalletTransformable( + attribute: "unusedChainIds", + column: "ZUNUSEDCHAINIDS" + ) + } + + func testPersistentWrongStoredTypeDefaultsOnlyPreferenceWithoutMutation() throws { + let wrongTypeArchive = try NSKeyedArchiver.archivedData( + withRootObject: NSString(string: "not-an-array"), + requiringSecureCoding: true + ) + + try assertPersistentCorruptWalletTransformable( + attribute: "assetKeysOrder", + column: "ZASSETKEYSORDER", + archiveHex: wrongTypeArchive.map { + String(format: "%02X", $0) + }.joined() + ) + } + + func testPersistentValidWalletArchivesPreserveEveryPreference() throws { + try withCorruptedUserStore( + updateSQL: + "UPDATE ZCDMETAACCOUNT SET ZORDER = ZORDER" + ) { context, _, facade in + let wallet = try perform(in: context) { + let entity = try fetchSingle(CDMetaAccount.self, in: context) + return try MetaAccountMapper().transform(entity: entity) + } + + XCTAssertEqual(wallet.assetKeysOrder, ["asset-a"]) + XCTAssertEqual(wallet.unusedChainIds, ["unused-chain"]) + XCTAssertEqual(wallet.favouriteChainIds, ["favourite-chain"]) + + let repository = AccountRepositoryFactory(storageFacade: facade) + .createMetaAccountRepository( + for: nil, + sortDescriptors: [] + ) + let operation = repository.fetchAllOperation( + with: RepositoryFetchOptions() + ) + let queue = OperationQueue() + queue.addOperations([operation], waitUntilFinished: true) + XCTAssertEqual( + try XCTUnwrap(operation.result).get().count, + 1 + ) + } + } + + func testPersistentCorruptChainOptionsDefaultsWithoutMutatingCache() throws { + try withCorruptedSubstrateStore( + updateSQL: + "UPDATE ZCDCHAIN SET ZOPTIONS = X'\(Self.invalidArchiveHex)'" + ) { context, storeURL, facade in + let mappedChain = try perform(in: context) { + let entity = try fetchSingle(CDChain.self, in: context) + return try ChainModelMapper().transform(entity: entity) + } + + XCTAssertNil(mappedChain.options) + XCTAssertEqual(mappedChain.chainId, "transformable-chain") + + let repository: CoreDataRepository = + facade.createRepository( + filter: nil, + sortDescriptors: [], + mapper: AnyCoreDataMapper(ChainModelMapper()) + ) + XCTAssertEqual(try fetchAll(from: repository).count, 1) + try assertBlobAndRowRemain( + storeURL: storeURL, + table: "ZCDCHAIN", + column: "ZOPTIONS" + ) + } + } + + func testPersistentCorruptExternalApiTypesSkipsOnlyExplorerWithoutMutatingCache() throws { + try withCorruptedSubstrateStore( + updateSQL: + "UPDATE ZCDEXTERNALAPI SET ZTYPES = X'\(Self.invalidArchiveHex)'" + ) { context, storeURL, facade in + let mappedChain = try perform(in: context) { + let entity = try fetchSingle(CDChain.self, in: context) + return try ChainModelMapper().transform(entity: entity) + } + + XCTAssertEqual(mappedChain.externalApi?.explorers, []) + XCTAssertEqual(mappedChain.assets.count, 1) + + let repository: CoreDataRepository = + facade.createRepository( + filter: nil, + sortDescriptors: [], + mapper: AnyCoreDataMapper(ChainModelMapper()) + ) + XCTAssertEqual(try fetchAll(from: repository).count, 1) + try assertBlobAndRowRemain( + storeURL: storeURL, + table: "ZCDEXTERNALAPI", + column: "ZTYPES" + ) + } + } + + func testPersistentCorruptAssetPurchaseProvidersDefaultsWithoutMutatingCache() throws { + try withCorruptedSubstrateStore( + updateSQL: + "UPDATE ZCDASSET SET ZPURCHASEPROVIDERS = X'\(Self.invalidArchiveHex)'" + ) { context, storeURL, facade in + let mappedAsset = try perform(in: context) { + let entity = try fetchSingle(CDAsset.self, in: context) + return try AssetModelMapper().transform(entity: entity) + } + XCTAssertNil(mappedAsset.purchaseProviders) + + let repository: CoreDataRepository = + facade.createRepository( + filter: nil, + sortDescriptors: [], + mapper: AnyCoreDataMapper(ChainModelMapper()) + ) + let mappedChain = try XCTUnwrap( + try fetchAll(from: repository).first + ) + XCTAssertEqual(mappedChain.assets.count, 1) + XCTAssertNil(mappedChain.assets.first?.purchaseProviders) + try assertBlobAndRowRemain( + storeURL: storeURL, + table: "ZCDASSET", + column: "ZPURCHASEPROVIDERS" + ) + } + } + + func testPersistentCorruptPolkaswapTransformablesSurfaceCatchableErrorWithoutMutation() throws { + for column in ["ZAVAILABLESOURCES", "ZFORCESMARTIDS"] { + try withCorruptedSubstrateStore( + updateSQL: + "UPDATE ZCDPOLKASWAPREMOTESETTINGS " + + "SET \(column) = X'\(Self.invalidArchiveHex)'" + ) { context, storeURL, _ in + XCTAssertThrowsError( + try perform(in: context) { + let entity = try fetchSingle( + CDPolkaswapRemoteSettings.self, + in: context + ) + return try PolkaswapSettingMapper().transform( + entity: entity + ) + } + ) { error in + guard + case PolkaswapSettingMapperError.requiredFieldsMissing = + error + else { + return XCTFail("Unexpected mapper error: \(error)") + } + } + + try assertBlobAndRowRemain( + storeURL: storeURL, + table: "ZCDPOLKASWAPREMOTESETTINGS", + column: column + ) + } + } + } + + func testProductionRuntimeRegistryRetainsEveryCurrentStoreClass() throws { + XCTAssertEqual( + ManagedObjectRuntimeClassRegistry.substrateV8Classes.count, + 19 + ) + XCTAssertEqual( + ManagedObjectRuntimeClassRegistry.userV11Classes.count, + 7 + ) + XCTAssertEqual( + ManagedObjectRuntimeClassRegistry.classesByRuntimeName.count, + 26 + ) + + for model in [ + try loadUserModel(qualifyClasses: false).model, + try loadSubstrateModel(qualifyClasses: false).model + ] { + for entity in model.entities { + let entityName = try XCTUnwrap(entity.name) + let className = try XCTUnwrap(entity.managedObjectClassName) + let resolvedClass = try XCTUnwrap( + ManagedObjectRuntimeClassRegistry.resolve( + className: className + ), + "Registry must resolve \(entityName)" + ) + + XCTAssertTrue( + ManagedObjectRuntimeClassRegistry.classesByRuntimeName + .values.contains { + ObjectIdentifier($0) == + ObjectIdentifier(resolvedClass) + } + ) + } + } + } + + private func assertPersistentCorruptWalletTransformable( + attribute: String, + column: String, + archiveHex: String = + TransformableArchiveSafetyTests.invalidArchiveHex + ) throws { + try withCorruptedUserStore( + updateSQL: + "UPDATE ZCDMETAACCOUNT " + + "SET \(column) = X'\(archiveHex)'" + ) { context, storeURL, facade in + let wallet = try perform(in: context) { + let entity = try fetchSingle( + CDMetaAccount.self, + in: context + ) + return try MetaAccountMapper().transform(entity: entity) + } + + XCTAssertEqual( + wallet.assetKeysOrder, + attribute == "assetKeysOrder" ? nil : ["asset-a"] + ) + XCTAssertEqual( + wallet.unusedChainIds, + attribute == "unusedChainIds" ? nil : ["unused-chain"] + ) + XCTAssertEqual( + wallet.favouriteChainIds, + attribute == "favouriteChainIds" ? [] : ["favourite-chain"] + ) + + let projection = try perform(in: context) { + let entity = try fetchSingle(CDMetaAccount.self, in: context) + return try MetaAccountSelectionMapper().transform( + entity: entity + ) + } + XCTAssertEqual(projection.recordState, .supported) + XCTAssertNotNil(projection.wallet) + XCTAssertTrue(projection.recordState.allowsStoredRecordUpdates) + + let repository = AccountRepositoryFactory(storageFacade: facade) + .createMetaAccountRepository( + for: nil, + sortDescriptors: [] + ) + let operation = repository.fetchAllOperation( + with: RepositoryFetchOptions() + ) + let queue = OperationQueue() + queue.addOperations([operation], waitUntilFinished: true) + XCTAssertEqual( + try XCTUnwrap(operation.result).get().count, + 1 + ) + + try assertBlobAndRowRemain( + storeURL: storeURL, + table: "ZCDMETAACCOUNT", + column: column, + expectedHex: archiveHex + ) + } + } + + private func withCorruptedUserStore( + updateSQL: String, + body: ( + NSManagedObjectContext, + URL, + PersistentFixtureStorageFacade + ) throws -> Void + ) throws { + let loadedModel = try loadUserModel() + try withCorruptedStore( + modelURL: loadedModel.url, + model: loadedModel.model, + updateSQL: updateSQL, + seed: seedUserStore, + body: body + ) + } + + private func withCorruptedSubstrateStore( + updateSQL: String, + body: ( + NSManagedObjectContext, + URL, + PersistentFixtureStorageFacade + ) throws -> Void + ) throws { + let loadedModel = try loadSubstrateModel() + try withCorruptedStore( + modelURL: loadedModel.url, + model: loadedModel.model, + updateSQL: updateSQL, + seed: seedSubstrateStore, + body: body + ) + } + + private func withCorruptedStore( + modelURL: URL, + model: NSManagedObjectModel, + updateSQL: String, + seed: (NSManagedObjectContext) throws -> Void, + body: ( + NSManagedObjectContext, + URL, + PersistentFixtureStorageFacade + ) throws -> Void + ) throws { + let directoryURL = FileManager.default.temporaryDirectory + .appendingPathComponent( + "TransformableArchiveSafety-\(UUID().uuidString)", + isDirectory: true + ) + let storeURL = directoryURL.appendingPathComponent("Store.sqlite") + try FileManager.default.createDirectory( + at: directoryURL, + withIntermediateDirectories: true + ) + defer { + try? FileManager.default.removeItem(at: directoryURL) + } + + let options: [AnyHashable: Any] = [ + NSSQLitePragmasOption: ["journal_mode": "DELETE"] + ] + let writerCoordinator = NSPersistentStoreCoordinator( + managedObjectModel: model + ) + let writerStore = try writerCoordinator.addPersistentStore( + ofType: NSSQLiteStoreType, + configurationName: nil, + at: storeURL, + options: options + ) + let writerContext = NSManagedObjectContext( + concurrencyType: .privateQueueConcurrencyType + ) + writerContext.persistentStoreCoordinator = writerCoordinator + try perform(in: writerContext) { + try seed(writerContext) + try writerContext.save() + writerContext.reset() + } + try writerCoordinator.remove(writerStore) + + XCTAssertEqual(try executeSQLiteUpdate(updateSQL, at: storeURL), 1) + + let readerCoordinator = NSPersistentStoreCoordinator( + managedObjectModel: model + ) + let readerStore = try readerCoordinator.addPersistentStore( + ofType: NSSQLiteStoreType, + configurationName: nil, + at: storeURL, + options: options + ) + let readerContext = NSManagedObjectContext( + concurrencyType: .privateQueueConcurrencyType + ) + readerContext.persistentStoreCoordinator = readerCoordinator + let facade = PersistentFixtureStorageFacade( + context: readerContext, + modelURL: modelURL + ) + + defer { + try? perform(in: readerContext) { + readerContext.reset() + } + try? readerCoordinator.remove(readerStore) + } + + try body(readerContext, storeURL, facade) + XCTAssertFalse(readerContext.hasChanges) + } + + private func seedUserStore(_ context: NSManagedObjectContext) throws { + let model = MetaAccountModel( + metaId: "transformable-wallet", + name: "Transformable archive fixture", + substrateAccountId: Data(repeating: 0x11, count: 32), + substrateCryptoType: CryptoType.sr25519.rawValue, + substratePublicKey: Data(repeating: 0x22, count: 32), + ethereumAddress: nil, + ethereumPublicKey: nil, + chainAccounts: [], + assetKeysOrder: ["asset-a"], + canExportEthereumMnemonic: false, + unusedChainIds: ["unused-chain"], + selectedCurrency: Currency.defaultCurrency(), + networkManagmentFilter: nil, + assetsVisibility: [], + hasBackup: true, + favouriteChainIds: ["favourite-chain"] + ) + let entity = CDMetaAccount(context: context) + try MetaAccountMapper().populate( + entity: entity, + from: model, + using: context + ) + entity.isSelected = true + entity.order = 1 + } + + private func seedSubstrateStore( + _ context: NSManagedObjectContext + ) throws { + let chain = CDChain(context: context) + chain.chainId = "transformable-chain" + chain.name = "Transformable archive fixture" + chain.addressPrefix = 0 + chain.isEthereumBased = false + chain.isTestnet = false + chain.hasCrowdloans = false + chain.setValue( + NSArray(object: ChainOptions.poolStaking.rawValue), + forKey: "options" + ) + + let node = CDChainNode(context: context) + node.name = "Fixture node" + node.url = try XCTUnwrap(URL(string: "wss://fixture.example")) + node.chain = chain + chain.nodes = NSSet(object: node) + chain.selectedNode = node + + let explorer = CDExternalApi(context: context) + explorer.type = "subscan" + explorer.url = "https://explorer.example" + explorer.setValue(NSArray(object: "extrinsic"), forKey: "types") + explorer.chain = chain + chain.explorers = NSSet(object: explorer) + + let asset = CDAsset(context: context) + asset.id = "fixture-asset" + asset.name = "Fixture Asset" + asset.symbol = "FIX" + asset.precision = 12 + asset.isUtility = true + asset.isNative = true + asset.setValue(NSArray(object: "moonpay"), forKey: "purchaseProviders") + asset.chain = chain + chain.assets = NSSet(object: asset) + + let polkaswap = CDPolkaswapRemoteSettings(context: context) + polkaswap.version = "1" + polkaswap.setValue(NSArray(), forKey: "availableSources") + polkaswap.setValue(NSArray(), forKey: "forceSmartIds") + polkaswap.availableDexIds = NSSet() + polkaswap.xstusdId = "fixture-xstusd" + } + + private func loadUserModel( + qualifyClasses: Bool = true + ) throws -> ( + url: URL, + model: NSManagedObjectModel + ) { + let modelURL = try XCTUnwrap( + [ + Bundle(for: UserDataStorageFacade.self), + Bundle.main + ].lazy.compactMap { + UserStorageParams.modelVersion.modelURL( + in: $0, + legacyModelDirectory: UserStorageParams.modelDirectory + ) + }.first + ) + let loadedModel = try XCTUnwrap( + NSManagedObjectModel(contentsOf: modelURL) + ) + let model = try XCTUnwrap( + loadedModel.copy() as? NSManagedObjectModel + ) + if qualifyClasses { + try qualifyManagedObjectClasses(in: model) + } + return (modelURL, model) + } + + private func loadSubstrateModel( + qualifyClasses: Bool = true + ) throws -> ( + url: URL, + model: NSManagedObjectModel + ) { + let modelURL = try XCTUnwrap( + Bundle(for: SubstrateDataStorageFacade.self).url( + forResource: "SubstrateDataModel", + withExtension: "momd" + ) + ) + let loadedModel = try XCTUnwrap( + NSManagedObjectModel(contentsOf: modelURL) + ) + let model = try XCTUnwrap( + loadedModel.copy() as? NSManagedObjectModel + ) + if qualifyClasses { + try qualifyManagedObjectClasses(in: model) + } + return (modelURL, model) + } + + private func qualifyManagedObjectClasses( + in model: NSManagedObjectModel + ) throws { + for entity in model.entities { + let entityName = try XCTUnwrap(entity.name) + let storedClassName = try XCTUnwrap( + entity.managedObjectClassName + ) + let runtimeClass = try XCTUnwrap( + ManagedObjectRuntimeClassRegistry.resolve( + className: storedClassName + ), + "Missing runtime class for \(entityName)" + ) + entity.managedObjectClassName = NSStringFromClass(runtimeClass) + } + } + + private func perform( + in context: NSManagedObjectContext, + _ block: () throws -> T + ) throws -> T { + var result: Result? + context.performAndWait { + result = Result { + try block() + } + } + return try XCTUnwrap(result).get() + } + + private func fetchSingle( + _: Entity.Type, + in context: NSManagedObjectContext + ) throws -> Entity { + let entityName = try XCTUnwrap(Entity.entity().name) + let request = NSFetchRequest(entityName: entityName) + request.fetchLimit = 2 + request.includesPendingChanges = false + request.includesSubentities = false + let entities = try context.fetch(request) + XCTAssertEqual(entities.count, 1) + return try XCTUnwrap(entities.first) + } + + private func fetchAll( + from repository: CoreDataRepository + ) throws -> [Model] { + let operation = repository.fetchAllOperation( + with: RepositoryFetchOptions() + ) + let queue = OperationQueue() + queue.addOperations([operation], waitUntilFinished: true) + return try XCTUnwrap(operation.result).get() + } + + private func assertBlobAndRowRemain( + storeURL: URL, + table: String, + column: String, + expectedHex: String = + TransformableArchiveSafetyTests.invalidArchiveHex + ) throws { + XCTAssertEqual( + try sqliteText( + "SELECT COUNT(*) FROM \(table)", + at: storeURL + ), + "1" + ) + XCTAssertEqual( + try sqliteText( + "SELECT HEX(\(column)) FROM \(table)", + at: storeURL + ), + expectedHex + ) + } + + private func executeSQLiteUpdate( + _ sql: String, + at storeURL: URL + ) throws -> Int { + let database = try openSQLite(at: storeURL) + defer { + sqlite3_close(database) + } + + var errorMessage: UnsafeMutablePointer? + let result = sqlite3_exec( + database, + sql, + nil, + nil, + &errorMessage + ) + defer { + sqlite3_free(errorMessage) + } + + guard result == SQLITE_OK else { + throw sqliteError( + database: database, + code: result, + fallback: errorMessage.map { String(cString: $0) } + ) + } + + return Int(sqlite3_changes(database)) + } + + private func sqliteText( + _ sql: String, + at storeURL: URL + ) throws -> String? { + let database = try openSQLite(at: storeURL) + defer { + sqlite3_close(database) + } + + var statement: OpaquePointer? + let prepareResult = sqlite3_prepare_v2( + database, + sql, + -1, + &statement, + nil + ) + guard prepareResult == SQLITE_OK, let statement else { + throw sqliteError( + database: database, + code: prepareResult + ) + } + defer { + sqlite3_finalize(statement) + } + + let stepResult = sqlite3_step(statement) + guard stepResult == SQLITE_ROW else { + throw sqliteError(database: database, code: stepResult) + } + guard let text = sqlite3_column_text(statement, 0) else { + return nil + } + let count = Int(sqlite3_column_bytes(statement, 0)) + return String( + decoding: UnsafeBufferPointer(start: text, count: count), + as: UTF8.self + ) + } + + private func openSQLite(at storeURL: URL) throws -> OpaquePointer { + var database: OpaquePointer? + let result = sqlite3_open_v2( + storeURL.path, + &database, + SQLITE_OPEN_READWRITE, + nil + ) + guard result == SQLITE_OK, let database else { + defer { + if let database { + sqlite3_close(database) + } + } + throw sqliteError(database: database, code: result) + } + return database + } + + private func sqliteError( + database: OpaquePointer?, + code: Int32, + fallback: String? = nil + ) -> Error { + let description = fallback ?? + database.flatMap(sqlite3_errmsg).map(String.init(cString:)) ?? + "Unknown SQLite error" + return NSError( + domain: "TransformableArchiveSafetyTests.SQLite", + code: Int(code), + userInfo: [NSLocalizedDescriptionKey: description] + ) + } +} + +private final class PersistentFixtureCoreDataService: + CoreDataServiceProtocol { + let configuration: CoreDataServiceConfigurationProtocol + + private let context: NSManagedObjectContext + + init(context: NSManagedObjectContext, modelURL: URL) { + self.context = context + configuration = CoreDataServiceConfiguration( + modelURL: modelURL, + storageType: .inMemory + ) + } + + func performAsync(block: @escaping CoreDataContextInvocationBlock) { + context.perform { + block(self.context, nil) + } + } + + func close() throws {} + + func drop() throws {} +} + +private final class PersistentFixtureStorageFacade: StorageFacadeProtocol { + let databaseService: CoreDataServiceProtocol + + init(context: NSManagedObjectContext, modelURL: URL) { + databaseService = PersistentFixtureCoreDataService( + context: context, + modelURL: modelURL + ) + } + + func createRepository( + filter: NSPredicate?, + sortDescriptors: [NSSortDescriptor], + mapper: AnyCoreDataMapper + ) -> CoreDataRepository + where T: Identifiable, U: NSManagedObject { + CoreDataRepository( + databaseService: databaseService, + mapper: mapper, + filter: filter, + sortDescriptors: sortDescriptors + ) + } + + func createAsyncRepository( + filter: NSPredicate?, + sortDescriptors: [NSSortDescriptor], + mapper: AnyCoreDataMapper + ) -> AsyncCoreDataRepositoryDefault + where T: Identifiable, U: NSManagedObject { + AsyncCoreDataRepositoryDefault( + databaseService: databaseService, + mapper: mapper, + filter: filter, + sortDescriptors: sortDescriptors + ) + } +} diff --git a/fearlessTests/Helper/UserDataStorageTestFacade.swift b/fearlessTests/Helper/UserDataStorageTestFacade.swift index eac0dc485d..32aee4a1a1 100644 --- a/fearlessTests/Helper/UserDataStorageTestFacade.swift +++ b/fearlessTests/Helper/UserDataStorageTestFacade.swift @@ -86,35 +86,21 @@ class UserDataStorageTestFacade: StorageFacadeProtocol { let databaseService: CoreDataServiceProtocol init() { - let modelName = UserStorageParams.modelVersion.rawValue - let subdirectory = UserStorageParams.modelDirectory - let bundle = Bundle(for: UserDataStorageFacade.self) - - let omoURL = bundle.url( - forResource: modelName, - withExtension: "omo", - subdirectory: subdirectory - ) - - let momURL = bundle.url( - forResource: modelName, - withExtension: "mom", - subdirectory: subdirectory - ) - - let mainOmoURL = Bundle.main.url( - forResource: modelName, - withExtension: "omo", - subdirectory: subdirectory - ) - - let mainMomURL = Bundle.main.url( - forResource: modelName, - withExtension: "mom", - subdirectory: subdirectory - ) - - let modelURL = omoURL ?? momURL ?? mainOmoURL ?? mainMomURL + let modelURL = [ + Bundle(for: UserDataStorageFacade.self), + Bundle.main + ].lazy.compactMap { + UserStorageParams.modelVersion.modelURL( + in: $0, + legacyModelDirectory: UserStorageParams.modelDirectory + ) + }.first + + guard let modelURL else { + preconditionFailure( + "Missing \(UserStorageParams.modelVersion.rawValue) in the unit-test host" + ) + } let managedObjectClassNames: [String: String] = [ "CDAccountInfo": NSStringFromClass(CDAccountInfo.self), @@ -127,7 +113,7 @@ class UserDataStorageTestFacade: StorageFacadeProtocol { ] databaseService = TestCoreDataService( - modelURL: modelURL!, + modelURL: modelURL, managedObjectClassNames: managedObjectClassNames ) } diff --git a/fearlessTests/Mocks/ModuleMocks.swift b/fearlessTests/Mocks/ModuleMocks.swift index e3a640fded..ba5f332963 100644 --- a/fearlessTests/Mocks/ModuleMocks.swift +++ b/fearlessTests/Mocks/ModuleMocks.swift @@ -18928,6 +18928,26 @@ class MockRootInteractorOutputProtocol: RootInteractorOutputProtocol, Cuckoo.Pro cuckoo_manager.enableDefaultStubImplementation() } + func didCompleteSetup() { + return cuckoo_manager.call( + "didCompleteSetup()", + parameters: (), + escapingParameters: (), + superclassCall: Cuckoo.MockManager.crashOnProtocolSuperclassCall(), + defaultCall: __defaultImplStub!.didCompleteSetup() + ) + } + + func didFailSetup() { + return cuckoo_manager.call( + "didFailSetup()", + parameters: (), + escapingParameters: (), + superclassCall: Cuckoo.MockManager.crashOnProtocolSuperclassCall(), + defaultCall: __defaultImplStub!.didFailSetup() + ) + } + struct __StubbingProxy_RootInteractorOutputProtocol: Cuckoo.StubbingProxy { private let cuckoo_manager: Cuckoo.MockManager @@ -18935,6 +18955,22 @@ class MockRootInteractorOutputProtocol: RootInteractorOutputProtocol, Cuckoo.Pro init(manager: Cuckoo.MockManager) { self.cuckoo_manager = manager } + + func didCompleteSetup() -> Cuckoo.ProtocolStubNoReturnFunction<()> { + let matchers: [Cuckoo.ParameterMatcher] = [] + return .init(stub: cuckoo_manager.createStub(for: MockRootInteractorOutputProtocol.self, + method: "didCompleteSetup()", + parameterMatchers: matchers + )) + } + + func didFailSetup() -> Cuckoo.ProtocolStubNoReturnFunction<()> { + let matchers: [Cuckoo.ParameterMatcher] = [] + return .init(stub: cuckoo_manager.createStub(for: MockRootInteractorOutputProtocol.self, + method: "didFailSetup()", + parameterMatchers: matchers + )) + } } struct __VerificationProxy_RootInteractorOutputProtocol: Cuckoo.VerificationProxy { @@ -18947,12 +18983,40 @@ class MockRootInteractorOutputProtocol: RootInteractorOutputProtocol, Cuckoo.Pro self.callMatcher = callMatcher self.sourceLocation = sourceLocation } + + @discardableResult + func didCompleteSetup() -> Cuckoo.__DoNotUse<(), Void> { + let matchers: [Cuckoo.ParameterMatcher] = [] + return cuckoo_manager.verify( + "didCompleteSetup()", + callMatcher: callMatcher, + parameterMatchers: matchers, + sourceLocation: sourceLocation + ) + } + + @discardableResult + func didFailSetup() -> Cuckoo.__DoNotUse<(), Void> { + let matchers: [Cuckoo.ParameterMatcher] = [] + return cuckoo_manager.verify( + "didFailSetup()", + callMatcher: callMatcher, + parameterMatchers: matchers, + sourceLocation: sourceLocation + ) + } } } class RootInteractorOutputProtocolStub:RootInteractorOutputProtocol, @unchecked Sendable { + func didCompleteSetup() { + return DefaultValueRegistry.defaultValue(for: (Void).self) + } + func didFailSetup() { + return DefaultValueRegistry.defaultValue(for: (Void).self) + } } @@ -45974,4 +46038,3 @@ class UsernameSetupWireframeProtocolStub:UsernameSetupWireframeProtocol, @unchec return DefaultValueRegistry.defaultValue(for: (Void).self) } } - diff --git a/fearlessTests/Modules/Root/RootTests.swift b/fearlessTests/Modules/Root/RootTests.swift index 91be77832a..5d16218531 100644 --- a/fearlessTests/Modules/Root/RootTests.swift +++ b/fearlessTests/Modules/Root/RootTests.swift @@ -1,228 +1,3533 @@ import XCTest import Foundation +import CoreData @testable import fearless import Cuckoo +import RobinHood import SoraKeystore import SoraFoundation import simd +import SQLite3 class RootTests: XCTestCase { - func testOnboardingDecision() throws { - // given + func testHostedUnitTestsUseIsolatedApplicationLaunch() { + XCTAssertTrue( + ProcessInfo.processInfo.arguments.contains("-UNITTEST"), + "Hosted unit tests must not launch the production startup path or open persistent stores" + ) + } - let wireframe = MockRootWireframeProtocol() + func testCoreDataPreflightForwardsStoreOpenError() throws { + let configuration = try makeSubstrateInMemoryConfiguration() + let preflight = RootCoreDataStoragePreflight( + databaseService: FailingCoreDataService( + configuration: configuration, + error: RootSetupTestError.substrateStorageOpenFailed + ) + ) - let keystore = InMemoryKeychain() + XCTAssertThrowsError(try performStoragePreflight(preflight).get()) { error in + guard case RootSetupTestError.substrateStorageOpenFailed = error else { + return XCTFail("Unexpected forwarded open error: \(error)") + } + } + } - let expectedPincode = "123456" - try keystore.saveKey(expectedPincode.data(using: .utf8)!, - with: KeystoreTag.pincode.rawValue) + func testCoreDataPreflightRejectsUnresolvableEntityClassBeforeFetch() throws { + let entity = NSEntityDescription() + entity.name = "BrokenSubstrateEntity" + entity.managedObjectClassName = "MissingSubstrateManagedObjectClass" - let settings = SelectedWalletSettings( - storageFacade: UserDataStorageTestFacade(), - operationQueue: OperationQueue() + let model = NSManagedObjectModel() + model.entities = [entity] + + let coordinator = NSPersistentStoreCoordinator(managedObjectModel: model) + try coordinator.addPersistentStore( + ofType: NSInMemoryStoreType, + configurationName: nil, + at: nil ) - let userDefaultsStorage = InMemorySettingsManager() - let onboardingService = StubOnboardingService( - result: .success(Self.makeOnboardingPlatform()) + let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) + context.persistentStoreCoordinator = coordinator + let preflight = RootCoreDataStoragePreflight( + databaseService: ContextProvidingCoreDataService( + configuration: try makeSubstrateInMemoryConfiguration(), + context: context + ) ) - let presenter = createPresenter( - wireframe: wireframe, - settings: settings, - keystore: keystore, - userDefaultsStorage: userDefaultsStorage, - onboardingService: onboardingService + XCTAssertThrowsError(try performStoragePreflight(preflight).get()) { error in + guard + let preflightError = error as? RootStoragePreflightError, + case let .managedObjectClassUnavailable(entityName, className) = + preflightError + else { + return XCTFail("Expected a class-resolution failure, got \(error)") + } + + XCTAssertEqual(entityName, "BrokenSubstrateEntity") + XCTAssertEqual(className, "MissingSubstrateManagedObjectClass") + } + } + + func testCoreDataPreflightQueriesObjectIdsWithoutDecodingCorruptTransformables() throws { + let entity = NSEntityDescription() + entity.name = "PreflightArchive" + entity.managedObjectClassName = NSStringFromClass(NSManagedObject.self) + + let payload = NSAttributeDescription() + payload.name = "payload" + payload.attributeType = .transformableAttributeType + payload.attributeValueClassName = NSStringFromClass(NSData.self) + payload.valueTransformerName = + NSValueTransformerName.secureUnarchiveFromDataTransformerName.rawValue + payload.isOptional = true + entity.properties = [payload] + + let model = NSManagedObjectModel() + model.entities = [entity] + + let directoryURL = FileManager.default.temporaryDirectory.appendingPathComponent( + "RootStoragePreflight-\(UUID().uuidString)", + isDirectory: true ) + let storeURL = directoryURL.appendingPathComponent("Preflight.sqlite") + try FileManager.default.createDirectory( + at: directoryURL, + withIntermediateDirectories: true + ) + defer { + try? FileManager.default.removeItem(at: directoryURL) + } - let splashExpectation = XCTestExpectation() + let writerCoordinator = NSPersistentStoreCoordinator( + managedObjectModel: model + ) + let storeOptions: [AnyHashable: Any] = [ + NSSQLitePragmasOption: ["journal_mode": "DELETE"] + ] + let writerStore = try writerCoordinator.addPersistentStore( + ofType: NSSQLiteStoreType, + configurationName: nil, + at: storeURL, + options: storeOptions + ) + let writerContext = NSManagedObjectContext( + concurrencyType: .mainQueueConcurrencyType + ) + writerContext.persistentStoreCoordinator = writerCoordinator + let object = NSManagedObject(entity: entity, insertInto: writerContext) + object.setValue(Data("valid archive".utf8), forKey: payload.name) + try writerContext.save() + writerContext.reset() + try writerCoordinator.remove(writerStore) - stub(wireframe) { stub in - stub.showSplash(splashView: any(), on: any()).then { _ in - splashExpectation.fulfill() - } + XCTAssertEqual( + try executeSQLiteUpdate( + "UPDATE ZPREFLIGHTARCHIVE SET ZPAYLOAD = X'00FF00'", + at: storeURL + ), + 1 + ) + + let readerCoordinator = NSPersistentStoreCoordinator( + managedObjectModel: model + ) + let readerStore = try readerCoordinator.addPersistentStore( + ofType: NSSQLiteStoreType, + configurationName: nil, + at: storeURL, + options: storeOptions + ) + defer { + try? readerCoordinator.remove(readerStore) } - - let onboardingExpectation = XCTestExpectation() - - stub(wireframe) { stub in - stub.showOnboarding(on: any(), with: any()).then { _ in - onboardingExpectation.fulfill() - } + let readerContext = NSManagedObjectContext( + concurrencyType: .mainQueueConcurrencyType + ) + readerContext.persistentStoreCoordinator = readerCoordinator + let preflight = RootCoreDataStoragePreflight( + databaseService: ContextProvidingCoreDataService( + configuration: try makeSubstrateInMemoryConfiguration(), + context: readerContext + ) + ) + + XCTAssertNoThrow(try performStoragePreflight(preflight).get()) + XCTAssertFalse(readerContext.hasChanges) + } + + func testUserStorageFacadeMissingModelReturnsControlledFailure() throws { + let directoryURL = FileManager.default.temporaryDirectory.appendingPathComponent( + "MissingUserModel-\(UUID().uuidString)", + isDirectory: true + ) + defer { + try? FileManager.default.removeItem(at: directoryURL) } - // when + let facade = UserDataStorageFacade( + modelURL: nil, + databaseDirectory: directoryURL + ) + let completionExpectation = expectation( + description: "missing User model returns an error" + ) - presenter.loadOnLaunch() + facade.databaseService.performAsync { context, error in + XCTAssertNil(context) - // then + guard + let serviceError = error as? CoreDataServiceError, + case .modelInitializationFailed = serviceError + else { + XCTFail("Expected modelInitializationFailed, got \(String(describing: error))") + completionExpectation.fulfill() + return + } - wait(for: [splashExpectation, onboardingExpectation], timeout: Constants.defaultExpectationDuration) - XCTAssertTrue(try keystore.checkKey(for: KeystoreTag.pincode.rawValue)) + completionExpectation.fulfill() + } + + wait( + for: [completionExpectation], + timeout: Constants.defaultExpectationDuration + ) } - func testPincodeSetupDecision() { - // given + func testSubstrateStorageFacadeMissingModelFailsPreflightWithoutTrap() throws { + let directoryURL = FileManager.default.temporaryDirectory.appendingPathComponent( + "MissingSubstrateModel-\(UUID().uuidString)", + isDirectory: true + ) + defer { + try? FileManager.default.removeItem(at: directoryURL) + } - let wireframe = MockRootWireframeProtocol() + let facade = SubstrateDataStorageFacade( + modelURL: nil, + databaseDirectory: directoryURL, + databaseName: "MissingSubstrateModel.sqlite" + ) + let preflight = RootCoreDataStoragePreflight( + databaseService: facade.databaseService + ) + + XCTAssertThrowsError(try performStoragePreflight(preflight).get()) { error in + guard + let serviceError = error as? CoreDataServiceError, + case .modelInitializationFailed = serviceError + else { + return XCTFail("Expected modelInitializationFailed, got \(error)") + } + } + } + func testStartViewHelperDoesNotResolveWalletSettingsDuringConstruction() { let settings = SelectedWalletSettings( storageFacade: UserDataStorageTestFacade(), operationQueue: OperationQueue() ) + var providerInvocationCount = 0 + let helper = StartViewHelper( + keystore: InMemoryKeychain(), + selectedWalletSettingsProvider: { + providerInvocationCount += 1 + return settings + }, + userDefaultsStorage: InMemorySettingsManager() + ) - let selectedAccount = AccountGenerator.generateMetaAccount() - let saveExpectation = XCTestExpectation() - settings.save(value: selectedAccount, runningCompletionIn: .main) { result in - if case let .failure(error) = result { - XCTFail("Unexpected save error: \(error)") + XCTAssertEqual(providerInvocationCount, 0) + + _ = helper.startView(onboardingConfig: nil) + + XCTAssertEqual(providerInvocationCount, 1) + } + + func testMigrationFailureDoesNotResolveStorePreflightRegistryOrWalletSettings() { + let recorder = RootSetupEventRecorder() + let settings = RecordingImmediateRootSelectedWalletSettings( + recorder: recorder, + result: .success(nil) + ) + let chainRegistry = MockChainRegistryProtocol() + let output = MockRootInteractorOutputProtocol() + let failureExpectation = expectation( + description: "migration failure is reported" + ) + + stub(output) { stub in + stub.didFailSetup().then { + failureExpectation.fulfill() } - saveExpectation.fulfill() } - wait(for: [saveExpectation], timeout: Constants.defaultExpectationDuration) - let keystore = InMemoryKeychain() - let userDefaultsStorage = InMemorySettingsManager() + let interactor = RootInteractor( + chainRegistryProvider: { + recorder.record(.chainRegistry) + return chainRegistry + }, + storagePreflightProvider: { + recorder.record(.storagePreflightProvider) + return ImmediateRootStoragePreflight(result: .success(())) + }, + settingsProvider: { + recorder.record(.walletSettingsProvider) + return settings + }, + applicationConfig: ApplicationConfig.shared, + eventCenter: MockEventCenterProtocol(), + migrators: [ + RecordingRootMigrator( + recorder: recorder, + error: RootSetupTestError.migrationFailed + ) + ], + onboardingService: StubOnboardingService( + result: .failure(OnboardingServiceError.empty) + ), + onboardingConfigResolver: OnboardingConfigVersionResolver( + userDefaultsStorage: InMemorySettingsManager() + ) + ) + interactor.presenter = output - let presenter = createPresenter(wireframe: wireframe, - settings: settings, - keystore: keystore, - userDefaultsStorage: userDefaultsStorage) - - let splashExpectation = XCTestExpectation() + interactor.setup(runMigrations: true) - stub(wireframe) { stub in - stub.showSplash(splashView: any(), on: any()).then { _ in - splashExpectation.fulfill() + wait( + for: [failureExpectation], + timeout: Constants.defaultExpectationDuration + ) + + XCTAssertEqual(recorder.snapshot, [.migration]) + verify(chainRegistry, times(0)).performHotBoot() + verify(chainRegistry, times(0)).performColdBoot() + verify(output, times(0)).didCompleteSetup() + verify(output, times(1)).didFailSetup() + } + + func testMigrationFailureBlocksNoMigrationBypassAndRequiredRetryRerunsMigration() { + let recorder = RootSetupEventRecorder() + let settings = RecordingImmediateRootSelectedWalletSettings( + recorder: recorder, + result: .success(nil) + ) + let chainRegistry = MockChainRegistryProtocol() + let output = MockRootInteractorOutputProtocol() + let firstFailureExpectation = expectation( + description: "first migration attempt fails" + ) + let blockedBypassExpectation = expectation( + description: "no-migration retry cannot bypass failed migration" + ) + let retryCompletionExpectation = expectation( + description: "migration retry completes setup" + ) + var failureCallbackCount = 0 + let migrator = SequencedRootMigrator( + recorder: recorder, + results: [ + .failure(RootSetupTestError.migrationFailed), + .success(()) + ] + ) + + stub(chainRegistry) { stub in + stub.performColdBoot().then { + recorder.record(.coldBoot) } } + stub(output) { stub in + stub.didFailSetup().then { + failureCallbackCount += 1 - let pincodeExpectation = XCTestExpectation() - - stub(wireframe) { stub in - stub.showPincodeSetup(on: any()).then { _ in - pincodeExpectation.fulfill() + if failureCallbackCount == 1 { + firstFailureExpectation.fulfill() + } else if failureCallbackCount == 2 { + blockedBypassExpectation.fulfill() + } + } + stub.didCompleteSetup().then { + retryCompletionExpectation.fulfill() } - stub.showMain(on: any()).thenDoNothing() } - // when + let interactor = RootInteractor( + chainRegistryProvider: { + recorder.record(.chainRegistry) + return chainRegistry + }, + storagePreflightProvider: { + recorder.record(.storagePreflightProvider) + return RecordingRootStoragePreflight( + recorder: recorder, + result: .success(()) + ) + }, + settingsProvider: { + recorder.record(.walletSettingsProvider) + return settings + }, + applicationConfig: ApplicationConfig.shared, + eventCenter: MockEventCenterProtocol(), + migrators: [migrator], + onboardingService: StubOnboardingService( + result: .failure(OnboardingServiceError.empty) + ), + onboardingConfigResolver: OnboardingConfigVersionResolver( + userDefaultsStorage: InMemorySettingsManager() + ) + ) + interactor.presenter = output - presenter.loadOnLaunch() + interactor.setup(runMigrations: true) + wait( + for: [firstFailureExpectation], + timeout: Constants.defaultExpectationDuration + ) - // then + XCTAssertEqual(recorder.snapshot, [.migration]) - wait(for: [splashExpectation, pincodeExpectation], timeout: Constants.defaultExpectationDuration) - } + interactor.setup(runMigrations: false) + wait( + for: [blockedBypassExpectation], + timeout: Constants.defaultExpectationDuration + ) - func testMainScreenDecision() throws { - // given + XCTAssertEqual(recorder.snapshot, [.migration]) - let wireframe = MockRootWireframeProtocol() + interactor.setup(runMigrations: true) + wait( + for: [retryCompletionExpectation], + timeout: Constants.defaultExpectationDuration + ) - let keystore = InMemoryKeychain() + XCTAssertEqual( + recorder.snapshot, + [ + .migration, + .migration, + .storagePreflightProvider, + .storagePreflight, + .chainRegistry, + .walletSettingsProvider, + .walletSetup, + .coldBoot + ] + ) + verify(chainRegistry, times(0)).performHotBoot() + verify(chainRegistry, times(1)).performColdBoot() + verify(output, times(1)).didCompleteSetup() + verify(output, times(2)).didFailSetup() + } - let settings = SelectedWalletSettings( - storageFacade: UserDataStorageTestFacade(), - operationQueue: OperationQueue() + func testMigrationDeadlineFailsClosedAndRetriesReuseSingleRunningAttempt() { + let recorder = RootSetupEventRecorder() + let settings = RecordingImmediateRootSelectedWalletSettings( + recorder: recorder, + result: .success(nil) + ) + let chainRegistry = MockChainRegistryProtocol() + let output = MockRootInteractorOutputProtocol() + let migrationStartedExpectation = expectation( + description: "migration attempt starts" + ) + let firstDeadlineScheduledExpectation = expectation( + description: "first migration deadline is scheduled" + ) + let retryDeadlineScheduledExpectation = expectation( + description: "retry waits on the same migration attempt" ) + let timeoutFailureExpectation = expectation( + description: "migration timeout fails setup" + ) + let bypassFailureExpectation = expectation( + description: "no-migration request cannot bypass running migration" + ) + let completionExpectation = expectation( + description: "required retry completes after original migration" + ) + let deadlineScheduler = ManualRootSetupDeadlineScheduler( + scheduledExpectations: [ + firstDeadlineScheduledExpectation, + retryDeadlineScheduledExpectation + ] + ) + let migrator = BlockingRootMigrator( + recorder: recorder, + startedExpectation: migrationStartedExpectation + ) + var failureCount = 0 - let selectedAccount = AccountGenerator.generateMetaAccount() - let saveExpectation = XCTestExpectation() - settings.save(value: selectedAccount, runningCompletionIn: .main) { result in - if case let .failure(error) = result { - XCTFail("Unexpected save error: \(error)") + stub(chainRegistry) { stub in + stub.performColdBoot().then { + recorder.record(.coldBoot) + } + } + stub(output) { stub in + stub.didFailSetup().then { + failureCount += 1 + if failureCount == 1 { + timeoutFailureExpectation.fulfill() + } else if failureCount == 2 { + bypassFailureExpectation.fulfill() + } + } + stub.didCompleteSetup().then { + completionExpectation.fulfill() } - saveExpectation.fulfill() } - wait(for: [saveExpectation], timeout: Constants.defaultExpectationDuration) - let expectedPincode = "123456" - try keystore.saveKey(expectedPincode.data(using: .utf8)!, - with: KeystoreTag.pincode.rawValue) - let userDefaultsStorage = InMemorySettingsManager() + let interactor = RootInteractor( + chainRegistryProvider: { + recorder.record(.chainRegistry) + return chainRegistry + }, + storagePreflightProvider: { + recorder.record(.storagePreflightProvider) + return RecordingRootStoragePreflight( + recorder: recorder, + result: .success(()) + ) + }, + settingsProvider: { + recorder.record(.walletSettingsProvider) + return settings + }, + applicationConfig: ApplicationConfig.shared, + eventCenter: MockEventCenterProtocol(), + migrators: [migrator], + onboardingService: StubOnboardingService( + result: .failure(OnboardingServiceError.empty) + ), + onboardingConfigResolver: OnboardingConfigVersionResolver( + userDefaultsStorage: InMemorySettingsManager() + ), + migrationDeadline: 7, + setupDeadline: 42, + setupDeadlineScheduler: deadlineScheduler.schedule + ) + interactor.presenter = output + defer { + migrator.release() + } - let presenter = createPresenter(wireframe: wireframe, - settings: settings, - keystore: keystore, - userDefaultsStorage: userDefaultsStorage) - - let splashExpectation = XCTestExpectation() + interactor.setup(runMigrations: true) + wait( + for: [ + migrationStartedExpectation, + firstDeadlineScheduledExpectation + ], + timeout: Constants.defaultExpectationDuration + ) - stub(wireframe) { stub in - stub.showSplash(splashView: any(), on: any()).then { _ in - splashExpectation.fulfill() - } - } + XCTAssertEqual(deadlineScheduler.delays, [7]) + deadlineScheduler.fire(at: 0) + wait( + for: [timeoutFailureExpectation], + timeout: Constants.defaultExpectationDuration + ) - let mainScreenExpectation = XCTestExpectation() + interactor.setup(runMigrations: false) + wait( + for: [bypassFailureExpectation], + timeout: Constants.defaultExpectationDuration + ) - stub(wireframe) { stub in - stub.showLocalAuthentication(on: any()).then { _ in - mainScreenExpectation.fulfill() + interactor.setup(runMigrations: true) + wait( + for: [retryDeadlineScheduledExpectation], + timeout: Constants.defaultExpectationDuration + ) + + XCTAssertEqual(migrator.invocationCount, 1) + XCTAssertEqual(migrator.maximumConcurrentInvocationCount, 1) + XCTAssertEqual(deadlineScheduler.delays, [7, 7]) + verify(chainRegistry, times(0)).performHotBoot() + verify(chainRegistry, times(0)).performColdBoot() + + migrator.release() + wait( + for: [completionExpectation], + timeout: Constants.defaultExpectationDuration + ) + + XCTAssertEqual( + recorder.snapshot, + [ + .migration, + .storagePreflightProvider, + .storagePreflight, + .chainRegistry, + .walletSettingsProvider, + .walletSetup, + .coldBoot + ] + ) + XCTAssertEqual(migrator.invocationCount, 1) + XCTAssertEqual(migrator.maximumConcurrentInvocationCount, 1) + XCTAssertEqual(deadlineScheduler.delays, [7, 7, 42, 42]) + verify(output, times(2)).didFailSetup() + verify(output, times(1)).didCompleteSetup() + verify(chainRegistry, times(0)).performHotBoot() + verify(chainRegistry, times(1)).performColdBoot() + } + + func testPreflightFailureDoesNotResolveRegistryOrWalletSettings() { + let recorder = RootSetupEventRecorder() + let settings = RecordingImmediateRootSelectedWalletSettings( + recorder: recorder, + result: .success(nil) + ) + let chainRegistry = MockChainRegistryProtocol() + let output = MockRootInteractorOutputProtocol() + let failureExpectation = expectation( + description: "preflight failure is reported" + ) + + stub(output) { stub in + stub.didFailSetup().then { + failureExpectation.fulfill() } - stub.showMain(on: any()).thenDoNothing() } - // when + let interactor = RootInteractor( + chainRegistryProvider: { + recorder.record(.chainRegistry) + return chainRegistry + }, + storagePreflightProvider: { + recorder.record(.storagePreflightProvider) + return RecordingRootStoragePreflight( + recorder: recorder, + result: .failure(RootSetupTestError.substrateStorageOpenFailed) + ) + }, + settingsProvider: { + recorder.record(.walletSettingsProvider) + return settings + }, + applicationConfig: ApplicationConfig.shared, + eventCenter: MockEventCenterProtocol(), + migrators: [RecordingRootMigrator(recorder: recorder)], + onboardingService: StubOnboardingService( + result: .failure(OnboardingServiceError.empty) + ), + onboardingConfigResolver: OnboardingConfigVersionResolver( + userDefaultsStorage: InMemorySettingsManager() + ) + ) + interactor.presenter = output - presenter.loadOnLaunch() + interactor.setup(runMigrations: true) - // then + wait( + for: [failureExpectation], + timeout: Constants.defaultExpectationDuration + ) - wait(for: [splashExpectation, mainScreenExpectation], timeout: Constants.defaultExpectationDuration) + XCTAssertEqual( + recorder.snapshot, + [.migration, .storagePreflightProvider, .storagePreflight] + ) + verify(chainRegistry, times(0)).performHotBoot() + verify(chainRegistry, times(0)).performColdBoot() + verify(output, times(0)).didCompleteSetup() + verify(output, times(1)).didFailSetup() } - private func createPresenter( - wireframe: MockRootWireframeProtocol, - settings: SelectedWalletSettings, - keystore: KeystoreProtocol, - userDefaultsStorage: SettingsManagerProtocol, - migrators: [Migrating] = [], - onboardingService: OnboardingServiceProtocol = StubOnboardingService(result: .failure(OnboardingServiceError.empty)) - ) -> RootPresenter { - let resolver = OnboardingConfigVersionResolver(userDefaultsStorage: userDefaultsStorage) + func testWalletFailureRetryDoesNotRerunMigrationsAgainstResolvedStores() { + let recorder = RootSetupEventRecorder() + let settings = SequencedRootSelectedWalletSettings( + recorder: recorder, + results: [ + .failure(RootSetupTestError.walletRepositoryFailed), + .success(nil) + ] + ) + let chainRegistry = MockChainRegistryProtocol() + let output = MockRootInteractorOutputProtocol() + let firstFailureExpectation = expectation( + description: "wallet setup fails after migrations" + ) + let retryCompletionExpectation = expectation( + description: "wallet setup retry completes" + ) + + stub(chainRegistry) { stub in + stub.performColdBoot().then { + recorder.record(.coldBoot) + } + } + stub(output) { stub in + stub.didFailSetup().then { + firstFailureExpectation.fulfill() + } + stub.didCompleteSetup().then { + retryCompletionExpectation.fulfill() + } + } let interactor = RootInteractor( - chainRegistry: ChainRegistryFacade.sharedRegistry, - settings: settings, + chainRegistryProvider: { + recorder.record(.chainRegistry) + return chainRegistry + }, + storagePreflightProvider: { + recorder.record(.storagePreflightProvider) + return RecordingRootStoragePreflight( + recorder: recorder, + result: .success(()) + ) + }, + settingsProvider: { + recorder.record(.walletSettingsProvider) + return settings + }, applicationConfig: ApplicationConfig.shared, eventCenter: MockEventCenterProtocol(), - migrators: migrators, - onboardingService: onboardingService, - onboardingConfigResolver: resolver + migrators: [RecordingRootMigrator(recorder: recorder)], + onboardingService: StubOnboardingService( + result: .failure(OnboardingServiceError.empty) + ), + onboardingConfigResolver: OnboardingConfigVersionResolver( + userDefaultsStorage: InMemorySettingsManager() + ) ) + interactor.presenter = output - let startViewHelper = StartViewHelper(keystore: keystore, - selectedWalletSettings: settings, - userDefaultsStorage: userDefaultsStorage) - let presenter = RootPresenter(localizationManager: LocalizationManager.shared, startViewHelper: startViewHelper) + interactor.setup(runMigrations: false) + wait( + for: [firstFailureExpectation], + timeout: Constants.defaultExpectationDuration + ) - let view = MockControllerBackedProtocol() + XCTAssertEqual( + recorder.snapshot, + [ + .storagePreflightProvider, + .storagePreflight, + .chainRegistry, + .walletSettingsProvider, + .walletSetup + ] + ) - presenter.view = view - presenter.window = UIWindow() - presenter.wireframe = wireframe - presenter.interactor = interactor - interactor.presenter = presenter + interactor.setup(runMigrations: true) + wait( + for: [retryCompletionExpectation], + timeout: Constants.defaultExpectationDuration + ) - return presenter + XCTAssertEqual( + recorder.snapshot, + [ + .storagePreflightProvider, + .storagePreflight, + .chainRegistry, + .walletSettingsProvider, + .walletSetup, + .storagePreflightProvider, + .storagePreflight, + .walletSetup, + .coldBoot + ] + ) + verify(chainRegistry, times(0)).performHotBoot() + verify(chainRegistry, times(1)).performColdBoot() + verify(output, times(1)).didCompleteSetup() + verify(output, times(1)).didFailSetup() } -} - -private final class StubOnboardingService: OnboardingServiceProtocol { - var result: Result - init(result: Result) { - self.result = result - } + func testMigrationFailureDoesNotLoadWalletOrRouteAndPreservesPincode() throws { + // given + + let recorder = RootSetupEventRecorder() + let storageFacade = RecordingUserDataStorageTestFacade(recorder: recorder) + let settings = SelectedWalletSettings( + storageFacade: storageFacade, + operationQueue: OperationQueue() + ) + let keystore = InMemoryKeychain() + let expectedPincode = "123456" + try keystore.saveKey( + Data(expectedPincode.utf8), + with: KeystoreTag.pincode.rawValue + ) + + let migrator = RecordingRootMigrator(recorder: recorder, error: RootSetupTestError.migrationFailed) + let wireframe = MockRootWireframeProtocol() + let alertController = AlertCapturingViewController() + let readinessReporter = RecordingRootStartupReadinessReporter() + let failureExpectation = XCTestExpectation(description: "setup failure is presented") + + alertController.onPresent = { presentedController in + XCTAssertTrue(presentedController is UIAlertController) + failureExpectation.fulfill() + } + + stub(wireframe) { stub in + stub.showSplash(splashView: any(), on: any()).thenDoNothing() + } + + let presenter = createPresenter( + wireframe: wireframe, + settings: settings, + keystore: keystore, + userDefaultsStorage: InMemorySettingsManager(), + migrators: [migrator], + view: alertController, + chainRegistryProvider: { + recorder.record(.chainRegistry) + return ChainRegistryFacade.sharedRegistry + }, + storagePreflightProvider: { + RecordingRootStoragePreflight( + recorder: recorder, + result: .success(()) + ) + }, + startupReadinessReporter: readinessReporter + ) + + // when + + presenter.loadOnLaunch() + + // then + + wait(for: [failureExpectation], timeout: Constants.defaultExpectationDuration) + + XCTAssertEqual(recorder.snapshot, [.migration]) + XCTAssertEqual(readinessReporter.readyCount, 0) + XCTAssertEqual(readinessReporter.failureCount, 1) + XCTAssertTrue(try keystore.checkKey(for: KeystoreTag.pincode.rawValue)) + verify(wireframe, times(0)).showLocalAuthentication(on: any()) + verify(wireframe, times(0)).showPincodeSetup(on: any()) + verify(wireframe, times(0)).showMain(on: any()) + verify(wireframe, times(0)).showBroken(on: any()) + verify(wireframe, times(0)).showOnboarding(on: any(), with: any()) + } + + func testSubstratePreflightFailureDoesNotCreateRegistryOrOpenWalletAndPreservesData() throws { + let recorder = RootSetupEventRecorder() + let storageFacade = RecordingUserDataStorageTestFacade(recorder: recorder) + let settings = SelectedWalletSettings( + storageFacade: storageFacade, + operationQueue: OperationQueue() + ) + try insertTonOnlyWallet(in: storageFacade) + let walletBefore = try tonOnlyWalletSnapshot(in: storageFacade) + recorder.reset() + + let keystore = InMemoryKeychain() + try keystore.saveKey( + Data("123456".utf8), + with: KeystoreTag.pincode.rawValue + ) + + let chainRegistry = MockChainRegistryProtocol() + let wireframe = MockRootWireframeProtocol() + let alertController = AlertCapturingViewController() + let failureExpectation = expectation( + description: "Substrate preflight failure is presented" + ) + + stub(wireframe) { stub in + stub.showSplash(splashView: any(), on: any()).thenDoNothing() + } + alertController.onPresent = { presentedController in + XCTAssertTrue(presentedController is UIAlertController) + failureExpectation.fulfill() + } + + let presenter = createPresenter( + wireframe: wireframe, + settings: settings, + keystore: keystore, + userDefaultsStorage: InMemorySettingsManager(), + migrators: [RecordingRootMigrator(recorder: recorder)], + view: alertController, + chainRegistryProvider: { + recorder.record(.chainRegistry) + return chainRegistry + }, + storagePreflightProvider: { + RecordingRootStoragePreflight( + recorder: recorder, + result: .failure(RootSetupTestError.substrateStorageOpenFailed) + ) + } + ) + + presenter.loadOnLaunch() + + wait(for: [failureExpectation], timeout: Constants.defaultExpectationDuration) + + XCTAssertEqual(recorder.snapshot, [.migration, .storagePreflight]) + XCTAssertEqual(try tonOnlyWalletSnapshot(in: storageFacade), walletBefore) + XCTAssertTrue(try keystore.checkKey(for: KeystoreTag.pincode.rawValue)) + verify(chainRegistry, times(0)).performHotBoot() + verify(chainRegistry, times(0)).performColdBoot() + verify(wireframe, times(0)).showLocalAuthentication(on: any()) + verify(wireframe, times(0)).showPincodeSetup(on: any()) + verify(wireframe, times(0)).showMain(on: any()) + verify(wireframe, times(0)).showBroken(on: any()) + verify(wireframe, times(0)).showOnboarding(on: any(), with: any()) + } + + func testWalletRepositoryFailureDoesNotBootOrRouteAndPreservesPincode() throws { + // given + + let storageFacade = FailingUserDataStorageTestFacade( + error: RootSetupTestError.walletRepositoryFailed + ) + let settings = SelectedWalletSettings( + storageFacade: storageFacade, + operationQueue: OperationQueue() + ) + let keystore = InMemoryKeychain() + try keystore.saveKey( + Data("123456".utf8), + with: KeystoreTag.pincode.rawValue + ) + + let chainRegistry = MockChainRegistryProtocol() + let wireframe = MockRootWireframeProtocol() + let alertController = AlertCapturingViewController() + let failureExpectation = XCTestExpectation(description: "wallet-load failure is presented") + let onboardingService = StubOnboardingService( + result: .success(Self.makeOnboardingPlatform()) + ) + + stub(wireframe) { stub in + stub.showSplash(splashView: any(), on: any()).thenDoNothing() + } + alertController.onPresent = { presentedController in + XCTAssertTrue(presentedController is UIAlertController) + failureExpectation.fulfill() + } + + let presenter = createPresenter( + wireframe: wireframe, + settings: settings, + keystore: keystore, + userDefaultsStorage: InMemorySettingsManager(), + onboardingService: onboardingService, + view: alertController, + chainRegistryProvider: { chainRegistry } + ) + + // when + + presenter.loadOnLaunch() + + // then + + wait(for: [failureExpectation], timeout: Constants.defaultExpectationDuration) + + XCTAssertTrue(try keystore.checkKey(for: KeystoreTag.pincode.rawValue)) + XCTAssertEqual(settings.storeState, .unavailable) + XCTAssertEqual(onboardingService.fetchCallCount, 0) + verify(chainRegistry, times(0)).performHotBoot() + verify(chainRegistry, times(0)).performColdBoot() + verify(wireframe, times(0)).showLocalAuthentication(on: any()) + verify(wireframe, times(0)).showPincodeSetup(on: any()) + verify(wireframe, times(0)).showMain(on: any()) + verify(wireframe, times(0)).showBroken(on: any()) + verify(wireframe, times(0)).showOnboarding(on: any(), with: any()) + } + + func testOutOfOrderDualSetupRejectsStaleCallbackBeforeBootOrPresenterMutation() { + let firstRequestStarted = expectation(description: "first wallet request starts") + let secondRequestStarted = expectation(description: "retry wallet request starts") + let newestSetupCompleted = expectation(description: "newest setup completes") + let staleSuccessDelivered = expectation(description: "stale success callback returns") + let staleFailureDelivered = expectation(description: "stale failure callback returns") + let settings = ControllableRootSelectedWalletSettings( + requestStartedExpectations: [ + firstRequestStarted, + secondRequestStarted + ] + ) + let recorder = RootGenerationEventRecorder() + let chainRegistry = MockChainRegistryProtocol() + let output = MockRootInteractorOutputProtocol() + + stub(chainRegistry) { stub in + stub.performHotBoot().then { + recorder.record(.hotBoot) + } + stub.performColdBoot().then { + recorder.record(.coldBoot) + } + } + stub(output) { stub in + stub.didCompleteSetup().then { + recorder.record(.completed) + newestSetupCompleted.fulfill() + } + stub.didFailSetup().then { + recorder.record(.failed) + } + } + + let interactor = RootInteractor( + chainRegistryProvider: { chainRegistry }, + storagePreflightProvider: { + ImmediateRootStoragePreflight(result: .success(())) + }, + settings: settings, + applicationConfig: ApplicationConfig.shared, + eventCenter: MockEventCenterProtocol(), + migrators: [], + onboardingService: StubOnboardingService( + result: .failure(OnboardingServiceError.empty) + ), + onboardingConfigResolver: OnboardingConfigVersionResolver( + userDefaultsStorage: InMemorySettingsManager() + ) + ) + interactor.presenter = output + + interactor.setup(runMigrations: false) + wait(for: [firstRequestStarted], timeout: Constants.defaultExpectationDuration) + + interactor.setup(runMigrations: false) + wait(for: [secondRequestStarted], timeout: Constants.defaultExpectationDuration) + + settings.resolveRequest( + at: 1, + with: .success(nil) + ) + wait(for: [newestSetupCompleted], timeout: Constants.defaultExpectationDuration) + + XCTAssertEqual(recorder.snapshot, [.coldBoot, .completed]) + XCTAssertEqual(recorder.routeState, .completed) + + settings.resolveRequest( + at: 0, + with: .success(AccountGenerator.generateMetaAccount()), + deliveryExpectation: staleSuccessDelivered + ) + settings.resolveRequest( + at: 0, + with: .failure(RootSetupTestError.staleWalletLoadFailed), + deliveryExpectation: staleFailureDelivered + ) + wait( + for: [staleSuccessDelivered, staleFailureDelivered], + timeout: Constants.defaultExpectationDuration + ) + + XCTAssertEqual(recorder.snapshot, [.coldBoot, .completed]) + XCTAssertEqual(recorder.routeState, .completed) + verify(chainRegistry, times(0)).performHotBoot() + verify(chainRegistry, times(1)).performColdBoot() + verify(output, times(1)).didCompleteSetup() + verify(output, times(0)).didFailSetup() + } + + func testPreflightTimeoutIgnoresLateResultAndRetryUsesFreshAttempt() { + let firstRequestStarted = expectation(description: "first preflight starts") + let secondRequestStarted = expectation(description: "retry preflight starts") + let firstTimedOut = expectation(description: "first preflight times out") + let retryCompleted = expectation(description: "retry completes") + let staleResultDelivered = expectation(description: "late preflight returns") + let preflight = ControllableRootStoragePreflight( + requestStartedExpectations: [ + firstRequestStarted, + secondRequestStarted + ] + ) + let deadlineScheduler = ManualRootSetupDeadlineScheduler() + let settings = RecordingImmediateRootSelectedWalletSettings( + recorder: RootSetupEventRecorder(), + result: .success(nil) + ) + let chainRegistry = MockChainRegistryProtocol() + let output = MockRootInteractorOutputProtocol() + + stub(chainRegistry) { stub in + stub.performColdBoot().thenDoNothing() + } + stub(output) { stub in + stub.didFailSetup().then { + firstTimedOut.fulfill() + } + stub.didCompleteSetup().then { + retryCompleted.fulfill() + } + } + + let interactor = RootInteractor( + chainRegistryProvider: { chainRegistry }, + storagePreflightProvider: { preflight }, + settings: settings, + applicationConfig: ApplicationConfig.shared, + eventCenter: MockEventCenterProtocol(), + migrators: [], + onboardingService: StubOnboardingService( + result: .failure(OnboardingServiceError.empty) + ), + onboardingConfigResolver: OnboardingConfigVersionResolver( + userDefaultsStorage: InMemorySettingsManager() + ), + setupDeadline: 42, + setupDeadlineScheduler: deadlineScheduler.schedule + ) + interactor.presenter = output + + interactor.setup(runMigrations: false) + wait( + for: [firstRequestStarted], + timeout: Constants.defaultExpectationDuration + ) + XCTAssertEqual(deadlineScheduler.delays, [42]) + deadlineScheduler.fire(at: 0) + wait( + for: [firstTimedOut], + timeout: Constants.defaultExpectationDuration + ) + + interactor.setup(runMigrations: false) + wait( + for: [secondRequestStarted], + timeout: Constants.defaultExpectationDuration + ) + XCTAssertEqual(deadlineScheduler.delays, [42, 42]) + preflight.resolveRequest( + at: 0, + with: .success(()), + deliveryExpectation: staleResultDelivered + ) + preflight.resolveRequest(at: 1, with: .success(())) + + wait( + for: [staleResultDelivered, retryCompleted], + timeout: Constants.defaultExpectationDuration + ) + XCTAssertEqual(deadlineScheduler.delays, [42, 42, 42]) + deadlineScheduler.fire(at: 1) + deadlineScheduler.fire(at: 2) + + verify(chainRegistry, times(0)).performHotBoot() + verify(chainRegistry, times(1)).performColdBoot() + verify(output, times(1)).didFailSetup() + verify(output, times(1)).didCompleteSetup() + } + + func testSynchronouslyBlockedPreflightTimesOutAndDoesNotBlockRetry() { + let blockedRequestStarted = expectation( + description: "blocking preflight starts" + ) + let firstRequestTimedOut = expectation( + description: "blocking preflight times out" + ) + firstRequestTimedOut.assertForOverFulfill = true + let retryCompleted = expectation( + description: "retry completes while first preflight remains blocked" + ) + retryCompleted.assertForOverFulfill = true + let lateCallbacksDelivered = expectation( + description: "blocked preflight delivers late success and failure" + ) + let blockedPreflight = SynchronouslyBlockingRootStoragePreflight( + requestStartedExpectation: blockedRequestStarted, + lateCallbacksDeliveredExpectation: lateCallbacksDelivered + ) + defer { + blockedPreflight.release() + } + + let preflightSequence = RootStoragePreflightSequence( + preflights: [ + blockedPreflight, + ImmediateRootStoragePreflight(result: .success(())) + ] + ) + let settings = RecordingImmediateRootSelectedWalletSettings( + recorder: RootSetupEventRecorder(), + result: .success(nil) + ) + let chainRegistry = MockChainRegistryProtocol() + let output = MockRootInteractorOutputProtocol() + + stub(chainRegistry) { stub in + stub.performColdBoot().thenDoNothing() + } + stub(output) { stub in + stub.didFailSetup().then { + XCTAssertTrue(Thread.isMainThread) + firstRequestTimedOut.fulfill() + } + stub.didCompleteSetup().then { + XCTAssertTrue(Thread.isMainThread) + retryCompleted.fulfill() + } + } + + let setupDeadline: TimeInterval = 0.25 + let interactor = RootInteractor( + chainRegistryProvider: { chainRegistry }, + storagePreflightProvider: preflightSequence.next, + settings: settings, + applicationConfig: ApplicationConfig.shared, + eventCenter: MockEventCenterProtocol(), + migrators: [], + onboardingService: StubOnboardingService( + result: .failure(OnboardingServiceError.empty) + ), + onboardingConfigResolver: OnboardingConfigVersionResolver( + userDefaultsStorage: InMemorySettingsManager() + ), + setupDeadline: setupDeadline + ) + interactor.presenter = output + + interactor.setup(runMigrations: false) + wait( + for: [blockedRequestStarted], + timeout: Constants.defaultExpectationDuration + ) + + // The preflight invocation is still synchronously blocked. The default + // deadline must run independently and report failure before release. + wait( + for: [firstRequestTimedOut], + timeout: Constants.defaultExpectationDuration + ) + + interactor.setup(runMigrations: false) + wait( + for: [retryCompleted], + timeout: Constants.defaultExpectationDuration + ) + + // Both late outcomes from the timed-out generation must lose the same + // completion gate and must not mutate the successful retry's route. + blockedPreflight.release() + wait( + for: [lateCallbacksDelivered], + timeout: Constants.defaultExpectationDuration + ) + + let pendingDeadlinesDrained = expectation( + description: "retry deadlines have had time to lose their gates" + ) + DispatchQueue.global().asyncAfter( + deadline: .now() + setupDeadline * 2 + ) { + pendingDeadlinesDrained.fulfill() + } + wait( + for: [pendingDeadlinesDrained], + timeout: Constants.defaultExpectationDuration + ) + + verify(chainRegistry, times(0)).performHotBoot() + verify(chainRegistry, times(1)).performColdBoot() + verify(output, times(1)).didFailSetup() + verify(output, times(1)).didCompleteSetup() + } + + func testTwoBlockedPreflightsFailFurtherRetriesFastWithoutUnboundedInvocation() { + let firstRequestStarted = expectation( + description: "first blocking preflight starts" + ) + let secondRequestStarted = expectation( + description: "second blocking preflight starts" + ) + let firstLateCallbacksDelivered = expectation( + description: "first blocked preflight delivers conflicting callbacks" + ) + let secondLateCallbacksDelivered = expectation( + description: "second blocked preflight delivers conflicting callbacks" + ) + let firstBlockedPreflight = SynchronouslyBlockingRootStoragePreflight( + requestStartedExpectation: firstRequestStarted, + lateCallbacksDeliveredExpectation: firstLateCallbacksDelivered + ) + let secondBlockedPreflight = SynchronouslyBlockingRootStoragePreflight( + requestStartedExpectation: secondRequestStarted, + lateCallbacksDeliveredExpectation: secondLateCallbacksDelivered + ) + defer { + firstBlockedPreflight.release() + secondBlockedPreflight.release() + } + + let preflightSequence = RootStoragePreflightSequence( + preflights: [ + firstBlockedPreflight, + secondBlockedPreflight, + ImmediateRootStoragePreflight(result: .success(())) + ] + ) + let deadlineScheduler = ManualRootSetupDeadlineScheduler() + let settingsRecorder = RootSetupEventRecorder() + let settings = RecordingImmediateRootSelectedWalletSettings( + recorder: settingsRecorder, + result: .success(nil) + ) + let chainRegistry = MockChainRegistryProtocol() + let output = MockRootInteractorOutputProtocol() + let saturatedRetryCount = 16 + let failureExpectations = (0..<(saturatedRetryCount + 2)).map { + expectation(description: "setup failure \($0)") + } + let failureLock = NSLock() + var failureCount = 0 + + stub(chainRegistry) { stub in + stub.performHotBoot().thenDoNothing() + stub.performColdBoot().thenDoNothing() + } + stub(output) { stub in + stub.didFailSetup().then { + XCTAssertTrue(Thread.isMainThread) + + failureLock.lock() + let failureIndex = failureCount + failureCount += 1 + failureLock.unlock() + + guard failureExpectations.indices.contains(failureIndex) else { + return XCTFail("Received an unexpected setup failure") + } + + failureExpectations[failureIndex].fulfill() + } + stub.didCompleteSetup().then { + XCTFail("A saturated preflight must not complete setup") + } + } + + let interactor = RootInteractor( + chainRegistryProvider: { chainRegistry }, + storagePreflightProvider: preflightSequence.next, + settings: settings, + applicationConfig: ApplicationConfig.shared, + eventCenter: MockEventCenterProtocol(), + migrators: [], + onboardingService: StubOnboardingService( + result: .failure(OnboardingServiceError.empty) + ), + onboardingConfigResolver: OnboardingConfigVersionResolver( + userDefaultsStorage: InMemorySettingsManager() + ), + setupDeadline: 42, + setupDeadlineScheduler: deadlineScheduler.schedule + ) + interactor.presenter = output + + interactor.setup(runMigrations: false) + wait( + for: [firstRequestStarted], + timeout: Constants.defaultExpectationDuration + ) + XCTAssertEqual(deadlineScheduler.delays, [42]) + deadlineScheduler.fire(at: 0) + wait( + for: [failureExpectations[0]], + timeout: Constants.defaultExpectationDuration + ) + + interactor.setup(runMigrations: false) + wait( + for: [secondRequestStarted], + timeout: Constants.defaultExpectationDuration + ) + XCTAssertEqual(deadlineScheduler.delays, [42, 42]) + deadlineScheduler.fire(at: 1) + wait( + for: [failureExpectations[1]], + timeout: Constants.defaultExpectationDuration + ) + + for retryIndex in 0.. ChainRegistryProtocol = { + ChainRegistryFacade.sharedRegistry + }, + storagePreflightProvider: @escaping RootStoragePreflightProvider = { + ImmediateRootStoragePreflight(result: .success(())) + }, + startupReadinessReporter: RootStartupReadinessReporting = + RootStartupReadinessReporter.shared + ) -> RootPresenter { + let resolver = OnboardingConfigVersionResolver(userDefaultsStorage: userDefaultsStorage) + + let interactor = RootInteractor( + chainRegistryProvider: chainRegistryProvider, + storagePreflightProvider: storagePreflightProvider, + settings: settings, + applicationConfig: ApplicationConfig.shared, + eventCenter: MockEventCenterProtocol(), + migrators: migrators, + onboardingService: onboardingService, + onboardingConfigResolver: resolver + ) + + let startViewHelper = StartViewHelper(keystore: keystore, + selectedWalletSettings: settings, + userDefaultsStorage: userDefaultsStorage) + let presenter = RootPresenter( + localizationManager: LocalizationManager.shared, + startViewHelper: startViewHelper, + onboardingConfigTimeoutNanoseconds: onboardingConfigTimeoutNanoseconds, + startupReadinessReporter: startupReadinessReporter + ) + + let presenterView = view ?? MockControllerBackedProtocol() + + presenter.view = presenterView + presenter.window = UIWindow() + presenter.wireframe = wireframe + presenter.interactor = interactor + interactor.presenter = presenter + + return presenter + } + + private func makeSubstrateInMemoryConfiguration() throws + -> CoreDataServiceConfiguration { + let modelURL = try XCTUnwrap( + Bundle(for: SubstrateDataStorageFacade.self).url( + forResource: "SubstrateDataModel", + withExtension: "momd" + ) + ) + + return CoreDataServiceConfiguration( + modelURL: modelURL, + storageType: .inMemory + ) + } + + private func executeSQLiteUpdate( + _ sql: String, + at storeURL: URL + ) throws -> Int { + var database: OpaquePointer? + let openResult = sqlite3_open_v2( + storeURL.path, + &database, + SQLITE_OPEN_READWRITE, + nil + ) + + guard openResult == SQLITE_OK, let database else { + defer { + if let database { + sqlite3_close(database) + } + } + + let message = database + .flatMap(sqlite3_errmsg) + .map { String(cString: $0) } ?? "unknown SQLite open error" + throw NSError( + domain: "RootStoragePreflightTests.SQLite", + code: Int(openResult), + userInfo: [NSLocalizedDescriptionKey: message] + ) + } + defer { + sqlite3_close(database) + } + + var errorMessage: UnsafeMutablePointer? + let executionResult = sqlite3_exec( + database, + sql, + nil, + nil, + &errorMessage + ) + defer { + sqlite3_free(errorMessage) + } + + guard executionResult == SQLITE_OK else { + let message = errorMessage + .map { String(cString: $0) } ?? "unknown SQLite update error" + throw NSError( + domain: "RootStoragePreflightTests.SQLite", + code: Int(executionResult), + userInfo: [NSLocalizedDescriptionKey: message] + ) + } + + return Int(sqlite3_changes(database)) + } + + private func performStoragePreflight( + _ preflight: RootStoragePreflighting + ) throws -> Result { + let completionExpectation = expectation( + description: "storage preflight completes" + ) + var result: Result? + + preflight.preflight { + result = $0 + completionExpectation.fulfill() + } + + wait( + for: [completionExpectation], + timeout: Constants.defaultExpectationDuration + ) + + return try XCTUnwrap(result) + } + + private func insertTonOnlyWallet(in facade: UserDataStorageTestFacade) throws { + try performCoreData(in: facade) { context in + let wallet = CDMetaAccount(context: context) + wallet.metaId = "root-ton-only-wallet" + wallet.name = "Preserved TON Wallet" + wallet.isSelected = true + wallet.order = 1 + wallet.canExportEthereumMnemonic = false + wallet.hasBackup = false + wallet.favouriteChainIds = NSArray() + wallet.setValue(false, forKey: "zeroBalanceAssetsHidden") + wallet.setValue(Data(repeating: 0x51, count: 36), forKey: "tonAddress") + wallet.setValue(Data(repeating: 0x52, count: 32), forKey: "tonPublicKey") + wallet.setValue("v5R1", forKey: "tonContractVersion") + try context.save() + } + } + + private func tonOnlyWalletSnapshot( + in facade: UserDataStorageTestFacade + ) throws -> RootTonOnlyWalletSnapshot { + try performCoreData(in: facade) { context in + let request = NSFetchRequest(entityName: "CDMetaAccount") + request.predicate = NSPredicate(format: "metaId == %@", "root-ton-only-wallet") + let wallet = try XCTUnwrap(context.fetch(request).first) + + return RootTonOnlyWalletSnapshot( + identifier: try XCTUnwrap(wallet.metaId), + name: try XCTUnwrap(wallet.name), + isSelected: wallet.isSelected, + order: wallet.order, + tonAddress: wallet.value(forKey: "tonAddress") as? Data, + tonPublicKey: wallet.value(forKey: "tonPublicKey") as? Data, + tonContractVersion: wallet.value(forKey: "tonContractVersion") as? String + ) + } + } + + private func performCoreData( + in facade: UserDataStorageTestFacade, + _ body: @escaping (NSManagedObjectContext) throws -> T + ) throws -> T { + let operationExpectation = expectation(description: "Core Data operation completes") + var operationResult: Result? + + facade.databaseService.performAsync { context, error in + do { + if let error { + throw error + } + operationResult = .success(try body(XCTUnwrap(context))) + } catch { + operationResult = .failure(error) + } + + operationExpectation.fulfill() + } + + wait(for: [operationExpectation], timeout: Constants.defaultExpectationDuration) + return try XCTUnwrap(operationResult).get() + } +} + +private struct RootTonOnlyWalletSnapshot: Equatable { + let identifier: String + let name: String + let isSelected: Bool + let order: Int32 + let tonAddress: Data? + let tonPublicKey: Data? + let tonContractVersion: String? +} + +private enum RootSetupTestError: Error { + case migrationFailed + case protectedDataUnavailable + case staleWalletLoadFailed + case substrateStorageOpenFailed + case walletRepositoryFailed +} + +private enum RootSetupEvent: Equatable { + case migration + case storagePreflightProvider + case storagePreflight + case chainRegistry + case walletSettingsProvider + case walletSetup + case walletRepository + case hotBoot + case coldBoot +} + +private final class RootSetupEventRecorder { + private let lock = NSLock() + private var events: [RootSetupEvent] = [] + + var snapshot: [RootSetupEvent] { + lock.lock() + defer { lock.unlock() } + + return events + } + + func record(_ event: RootSetupEvent) { + lock.lock() + events.append(event) + lock.unlock() + } + + func reset() { + lock.lock() + events.removeAll() + lock.unlock() + } +} + +private enum RootGenerationEvent: Equatable { + case hotBoot + case coldBoot + case completed + case failed +} + +private enum RootGenerationRouteState: Equatable { + case unresolved + case completed + case failed +} + +private final class RootGenerationEventRecorder { + private let lock = NSLock() + private var events: [RootGenerationEvent] = [] + private var state = RootGenerationRouteState.unresolved + + var snapshot: [RootGenerationEvent] { + lock.lock() + defer { lock.unlock() } + + return events + } + + var routeState: RootGenerationRouteState { + lock.lock() + defer { lock.unlock() } + + return state + } + + func record(_ event: RootGenerationEvent) { + lock.lock() + events.append(event) + + switch event { + case .completed: + state = .completed + case .failed: + state = .failed + case .hotBoot, .coldBoot: + break + } + + lock.unlock() + } +} + +private final class ManualRootSetupDeadlineScheduler { + private let lock = NSLock() + private let scheduledExpectations: [XCTestExpectation] + private var scheduledDelays: [TimeInterval] = [] + private var actions: [() -> Void] = [] + + init(scheduledExpectations: [XCTestExpectation] = []) { + self.scheduledExpectations = scheduledExpectations + } + + var delays: [TimeInterval] { + lock.lock() + defer { lock.unlock() } + + return scheduledDelays + } + + func schedule( + after delay: TimeInterval, + action: @escaping () -> Void + ) { + lock.lock() + let scheduledIndex = actions.count + scheduledDelays.append(delay) + actions.append(action) + lock.unlock() + + if scheduledExpectations.indices.contains(scheduledIndex) { + scheduledExpectations[scheduledIndex].fulfill() + } + } + + func fire(at index: Int) { + lock.lock() + let action = actions[index] + lock.unlock() + + action() + } +} + +private final class ControllableRootSelectedWalletSettings: RootSelectedWalletSettingsProtocol { + private struct Request { + let queue: DispatchQueue? + let completion: (Result) -> Void + } + + private let lock = NSLock() + private let requestStartedExpectations: [XCTestExpectation] + private var requests: [Request] = [] + + init(requestStartedExpectations: [XCTestExpectation]) { + self.requestStartedExpectations = requestStartedExpectations + } + + func setup( + runningCompletionIn queue: DispatchQueue?, + completionClosure: ((Result) -> Void)? + ) { + guard let completionClosure else { + return + } + + lock.lock() + let requestIndex = requests.count + requests.append( + Request( + queue: queue, + completion: completionClosure + ) + ) + lock.unlock() + + if requestStartedExpectations.indices.contains(requestIndex) { + requestStartedExpectations[requestIndex].fulfill() + } + } + + func resolveRequest( + at index: Int, + with result: Result, + deliveryExpectation: XCTestExpectation? = nil + ) { + lock.lock() + let request = requests[index] + lock.unlock() + + let deliver = { + request.completion(result) + deliveryExpectation?.fulfill() + } + + if let queue = request.queue { + queue.async(execute: deliver) + } else { + deliver() + } + } +} + +private final class RecordingImmediateRootSelectedWalletSettings: + RootSelectedWalletSettingsProtocol { + private let recorder: RootSetupEventRecorder + private let result: Result + + init( + recorder: RootSetupEventRecorder, + result: Result + ) { + self.recorder = recorder + self.result = result + } + + func setup( + runningCompletionIn queue: DispatchQueue?, + completionClosure: ((Result) -> Void)? + ) { + recorder.record(.walletSetup) + + guard let completionClosure else { + return + } + + if let queue { + queue.async { + completionClosure(self.result) + } + } else { + completionClosure(result) + } + } +} + +private final class SequencedRootSelectedWalletSettings: + RootSelectedWalletSettingsProtocol { + private let lock = NSLock() + private let recorder: RootSetupEventRecorder + private var results: [Result] + + init( + recorder: RootSetupEventRecorder, + results: [Result] + ) { + self.recorder = recorder + self.results = results + } + + func setup( + runningCompletionIn queue: DispatchQueue?, + completionClosure: ((Result) -> Void)? + ) { + recorder.record(.walletSetup) + + guard let completionClosure else { + return + } + + lock.lock() + let result = results.isEmpty + ? Result.failure( + RootSetupTestError.walletRepositoryFailed + ) + : results.removeFirst() + lock.unlock() + + if let queue { + queue.async { + completionClosure(result) + } + } else { + completionClosure(result) + } + } +} + +private final class RecordingRootMigrator: Migrating { + private let recorder: RootSetupEventRecorder + private let error: Error? + + init(recorder: RootSetupEventRecorder, error: Error? = nil) { + self.recorder = recorder + self.error = error + } + + func migrate() throws { + recorder.record(.migration) + + if let error { + throw error + } + } +} + +private final class BlockingRootMigrator: Migrating { + private let condition = NSCondition() + private let recorder: RootSetupEventRecorder + private let startedExpectation: XCTestExpectation + private var isReleased = false + private var invocationCountStorage = 0 + private var activeInvocationCount = 0 + private var maximumConcurrentInvocationCountStorage = 0 + + init( + recorder: RootSetupEventRecorder, + startedExpectation: XCTestExpectation + ) { + self.recorder = recorder + self.startedExpectation = startedExpectation + } + + var invocationCount: Int { + condition.lock() + defer { condition.unlock() } + + return invocationCountStorage + } + + var maximumConcurrentInvocationCount: Int { + condition.lock() + defer { condition.unlock() } + + return maximumConcurrentInvocationCountStorage + } + + func migrate() throws { + condition.lock() + invocationCountStorage += 1 + activeInvocationCount += 1 + maximumConcurrentInvocationCountStorage = max( + maximumConcurrentInvocationCountStorage, + activeInvocationCount + ) + condition.unlock() + + recorder.record(.migration) + startedExpectation.fulfill() + + condition.lock() + while !isReleased { + condition.wait() + } + activeInvocationCount -= 1 + condition.unlock() + } + + func release() { + condition.lock() + isReleased = true + condition.broadcast() + condition.unlock() + } +} + +private final class SequencedRootMigrator: Migrating { + private let lock = NSLock() + private let recorder: RootSetupEventRecorder + private var results: [Result] + + init( + recorder: RootSetupEventRecorder, + results: [Result] + ) { + self.recorder = recorder + self.results = results + } + + func migrate() throws { + recorder.record(.migration) + + lock.lock() + let result = results.isEmpty + ? Result.failure(RootSetupTestError.migrationFailed) + : results.removeFirst() + lock.unlock() - func fetchConfigs() async throws -> OnboardingConfigPlatform { try result.get() } } +private final class ControllableRootStoragePreflight: RootStoragePreflighting { + private let lock = NSLock() + private let requestStartedExpectations: [XCTestExpectation] + private var completions: [(Result) -> Void] = [] + + init(requestStartedExpectations: [XCTestExpectation]) { + self.requestStartedExpectations = requestStartedExpectations + } + + func preflight( + completion: @escaping (Result) -> Void + ) { + lock.lock() + let requestIndex = completions.count + completions.append(completion) + lock.unlock() + + if requestStartedExpectations.indices.contains(requestIndex) { + requestStartedExpectations[requestIndex].fulfill() + } + } + + func resolveRequest( + at index: Int, + with result: Result, + deliveryExpectation: XCTestExpectation? = nil + ) { + lock.lock() + let completion = completions[index] + lock.unlock() + + DispatchQueue.global().async { + completion(result) + deliveryExpectation?.fulfill() + } + } +} + +private final class SynchronouslyBlockingRootStoragePreflight: + RootStoragePreflighting { + private let releaseSemaphore = DispatchSemaphore(value: 0) + private let releaseLock = NSLock() + private let requestStartedExpectation: XCTestExpectation + private let lateCallbacksDeliveredExpectation: XCTestExpectation + private var isReleased = false + + init( + requestStartedExpectation: XCTestExpectation, + lateCallbacksDeliveredExpectation: XCTestExpectation + ) { + self.requestStartedExpectation = requestStartedExpectation + self.lateCallbacksDeliveredExpectation = + lateCallbacksDeliveredExpectation + } + + func preflight( + completion: @escaping (Result) -> Void + ) { + requestStartedExpectation.fulfill() + releaseSemaphore.wait() + + completion(.success(())) + completion(.failure(RootSetupTestError.substrateStorageOpenFailed)) + lateCallbacksDeliveredExpectation.fulfill() + } + + func release() { + releaseLock.lock() + defer { releaseLock.unlock() } + + guard !isReleased else { + return + } + + isReleased = true + releaseSemaphore.signal() + } +} + +private final class RootStoragePreflightSequence { + private let lock = NSLock() + private var preflights: [RootStoragePreflighting] + private var invocationCountStorage = 0 + + init(preflights: [RootStoragePreflighting]) { + self.preflights = preflights + } + + var invocationCount: Int { + lock.lock() + defer { lock.unlock() } + + return invocationCountStorage + } + + func next() -> RootStoragePreflighting { + lock.lock() + defer { lock.unlock() } + + invocationCountStorage += 1 + + guard preflights.isNotEmpty else { + return ImmediateRootStoragePreflight( + result: .failure( + RootSetupTestError.substrateStorageOpenFailed + ) + ) + } + + return preflights.removeFirst() + } +} + +private final class ImmediateRootStoragePreflight: RootStoragePreflighting { + private let result: Result + + init(result: Result) { + self.result = result + } + + func preflight( + completion: @escaping (Result) -> Void + ) { + completion(result) + } +} + +private final class RecordingRootStoragePreflight: RootStoragePreflighting { + private let recorder: RootSetupEventRecorder + private let result: Result + + init( + recorder: RootSetupEventRecorder, + result: Result + ) { + self.recorder = recorder + self.result = result + } + + func preflight( + completion: @escaping (Result) -> Void + ) { + recorder.record(.storagePreflight) + completion(result) + } +} + +private final class AdversarialRootStoragePreflight: RootStoragePreflighting { + private let recorder: RootSetupEventRecorder + + init(recorder: RootSetupEventRecorder) { + self.recorder = recorder + } + + func preflight( + completion: @escaping (Result) -> Void + ) { + recorder.record(.storagePreflight) + completion(.success(())) + completion(.success(())) + completion(.failure(RootSetupTestError.substrateStorageOpenFailed)) + } +} + +private final class SequencedRootStoragePreflight: RootStoragePreflighting { + private let lock = NSLock() + private let recorder: RootSetupEventRecorder + private var results: [Result] + + init( + recorder: RootSetupEventRecorder, + results: [Result] + ) { + self.recorder = recorder + self.results = results + } + + func preflight( + completion: @escaping (Result) -> Void + ) { + recorder.record(.storagePreflight) + + lock.lock() + let result = results.isEmpty + ? Result.failure(RootSetupTestError.substrateStorageOpenFailed) + : results.removeFirst() + lock.unlock() + + completion(result) + } +} + +private final class RecordingCoreDataService: CoreDataServiceProtocol { + var configuration: CoreDataServiceConfigurationProtocol { + service.configuration + } + + private let service: CoreDataServiceProtocol + private let recorder: RootSetupEventRecorder + + init( + service: CoreDataServiceProtocol, + recorder: RootSetupEventRecorder + ) { + self.service = service + self.recorder = recorder + } + + func performAsync(block: @escaping CoreDataContextInvocationBlock) { + recorder.record(.storagePreflight) + service.performAsync(block: block) + } + + func close() throws { + try service.close() + } + + func drop() throws { + try service.drop() + } +} + +private final class RecordingUserDataStorageTestFacade: UserDataStorageTestFacade { + private let recorder: RootSetupEventRecorder + + init(recorder: RootSetupEventRecorder) { + self.recorder = recorder + super.init() + } + + override func createRepository( + filter: NSPredicate?, + sortDescriptors: [NSSortDescriptor], + mapper: AnyCoreDataMapper + ) -> CoreDataRepository where T: Identifiable, U: NSManagedObject { + recorder.record(.walletRepository) + + return super.createRepository( + filter: filter, + sortDescriptors: sortDescriptors, + mapper: mapper + ) + } +} + +private final class FailingUserDataStorageTestFacade: UserDataStorageTestFacade { + private let error: Error + + init(error: Error) { + self.error = error + super.init() + } + + override func createRepository( + filter: NSPredicate?, + sortDescriptors: [NSSortDescriptor], + mapper: AnyCoreDataMapper + ) -> CoreDataRepository where T: Identifiable, U: NSManagedObject { + let failingService = FailingCoreDataService( + configuration: databaseService.configuration, + error: error + ) + + return CoreDataRepository( + databaseService: failingService, + mapper: mapper, + filter: filter, + sortDescriptors: sortDescriptors + ) + } +} + +private final class FailingCoreDataService: CoreDataServiceProtocol { + let configuration: CoreDataServiceConfigurationProtocol + private let error: Error + + init(configuration: CoreDataServiceConfigurationProtocol, error: Error) { + self.configuration = configuration + self.error = error + } + + func performAsync(block: @escaping CoreDataContextInvocationBlock) { + block(nil, error) + } + + func close() throws {} + + func drop() throws {} +} + +private final class ContextProvidingCoreDataService: CoreDataServiceProtocol { + let configuration: CoreDataServiceConfigurationProtocol + private let context: NSManagedObjectContext? + private let error: Error? + + init( + configuration: CoreDataServiceConfigurationProtocol, + context: NSManagedObjectContext?, + error: Error? = nil + ) { + self.configuration = configuration + self.context = context + self.error = error + } + + func performAsync(block: @escaping CoreDataContextInvocationBlock) { + block(context, error) + } + + func close() throws {} + + func drop() throws {} +} + +private final class AlertCapturingViewController: UIViewController, ControllerBackedProtocol { + var onPresent: ((UIViewController) -> Void)? + + override func present( + _ viewControllerToPresent: UIViewController, + animated flag: Bool, + completion: (() -> Void)? = nil + ) { + onPresent?(viewControllerToPresent) + completion?() + } +} + +private final class RecordingRootStartupReadinessReporter: + RootStartupReadinessReporting { + private let lock = NSLock() + private var readyEvents = 0 + private var failureEvents = 0 + + var readyCount: Int { + lock.lock() + defer { lock.unlock() } + + return readyEvents + } + + var failureCount: Int { + lock.lock() + defer { lock.unlock() } + + return failureEvents + } + + func reportReady() { + lock.lock() + readyEvents += 1 + lock.unlock() + } + + func reportFailure() { + lock.lock() + failureEvents += 1 + lock.unlock() + } +} + +private final class StubOnboardingService: OnboardingServiceProtocol { + private let lock = NSLock() + private let result: Result + private var callCount = 0 + + var fetchCallCount: Int { + lock.lock() + defer { lock.unlock() } + + return callCount + } + + init(result: Result) { + self.result = result + } + + private func recordFetch() { + lock.lock() + callCount += 1 + lock.unlock() + } + + func fetchConfigs() async throws -> OnboardingConfigPlatform { + recordFetch() + + return try result.get() + } +} + +private final class LateCompletingOnboardingService: OnboardingServiceProtocol { + private let lock = NSLock() + private let startedExpectation: XCTestExpectation + private var continuation: CheckedContinuation? + + init(startedExpectation: XCTestExpectation) { + self.startedExpectation = startedExpectation + } + + func fetchConfigs() async throws -> OnboardingConfigPlatform { + try await withCheckedThrowingContinuation { continuation in + lock.lock() + self.continuation = continuation + lock.unlock() + + startedExpectation.fulfill() + } + } + + func succeed(with platform: OnboardingConfigPlatform) { + lock.lock() + let continuation = continuation + self.continuation = nil + lock.unlock() + + continuation?.resume(returning: platform) + } +} + private extension RootTests { static func makeOnboardingPlatform() -> OnboardingConfigPlatform { let page: [String: Any] = [ diff --git a/scripts/ci/run-coredata-release-gate.sh b/scripts/ci/run-coredata-release-gate.sh new file mode 100644 index 0000000000..10b436b3fa --- /dev/null +++ b/scripts/ci/run-coredata-release-gate.sh @@ -0,0 +1,545 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Runs the migration/startup Core Data unit-test gate with Release optimization. +# +# The normal stage deliberately excludes the two copied-phone fixture tests. Run +# those separately with: +# +# FEARLESS_SUBSTRATE_PHONE_STORE_FIXTURE=/absolute/path/to/SubstrateDataModel.sqlite \ +# bash scripts/ci/run-coredata-release-gate.sh --stage copied-phone \ +# --simulator-udid +# +# This script never accepts a physical-device destination, never opens the source +# fixture itself, and keeps xcodebuild output in a private local log. + +umask 077 + +readonly LOG_PREFIX="[coredata-release-gate]" +readonly SCHEME="fearless.tests" +readonly UUID_PATTERN='^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$' +readonly SIMULATOR_DESTINATION_PATTERN='^platform=iOS Simulator,id=([0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12})(,arch=(arm64|x86_64))?$' +SIMULATOR_INVENTORY_FILE="" + +log() { + printf '%s %s\n' "$LOG_PREFIX" "$*" +} + +fail() { + printf '%s ERROR: %s\n' "$LOG_PREFIX" "$*" >&2 + exit 1 +} + +cleanup() { + if [[ -n "${SIMULATOR_INVENTORY_FILE:-}" && -e "$SIMULATOR_INVENTORY_FILE" ]]; then + rm "$SIMULATOR_INVENTORY_FILE" + fi +} + +usage() { + cat <<'USAGE' +Usage: + bash scripts/ci/run-coredata-release-gate.sh [options] + +Options: + --stage core|copied-phone|all + Run the hermetic class gate (default), the two copied-phone tests, or both. + --simulator-udid UUID + Use an available iPhone simulator. If omitted, exactly one booted, + available iPhone simulator must be discoverable. + --destination DESTINATION + Accepted only in the exact form "platform=iOS Simulator,id=UUID", with an + optional ",arch=arm64" or ",arch=x86_64". Physical/generic destinations + are refused. + --fixture ABSOLUTE_SQLITE_PATH + Source fixture for --stage copied-phone/all. The environment variable + FEARLESS_SUBSTRATE_PHONE_STORE_FIXTURE is also accepted. + --output-dir DIRECTORY + New result/log directory. Defaults beneath build/test-results. + --derived-data DIRECTORY + DerivedData directory. Defaults beneath build. + --help + +Environment overrides intended for controlled CI/test harnesses: + FEARLESS_CORE_DATA_ROOT_DIR + FEARLESS_CORE_DATA_SOURCE_PACKAGES_DIR + FEARLESS_CORE_DATA_XCODEBUILD_BIN + FEARLESS_CORE_DATA_XCRUN_BIN + FEARLESS_CORE_DATA_PYTHON_BIN + FEARLESS_CORE_DATA_SHASUM_BIN +USAGE +} + +require_option_value() { + local option="$1" + local remaining="$2" + [[ "$remaining" -ge 2 ]] || fail "$option requires a value" +} + +require_executable() { + local executable="$1" + local label="$2" + + if [[ "$executable" == */* ]]; then + [[ -x "$executable" ]] || fail "$label is not executable" + else + command -v "$executable" >/dev/null 2>&1 || fail "$label is unavailable" + fi +} + +canonical_directory() { + local directory="$1" + (cd "$directory" && pwd -P) +} + +resolve_simulator_from_json() { + local mode="$1" + local requested_udid="${2:-}" + local json_file="$3" + + "$PYTHON_BIN" - "$mode" "$requested_udid" "$json_file" <<'PY' +import json +import re +import sys + +mode, requested, json_path = sys.argv[1:] +uuid_pattern = re.compile( + r"^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-" + r"[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$" +) + +try: + with open(json_path, "r", encoding="utf-8") as source: + payload = json.load(source) +except (OSError, json.JSONDecodeError): + print("simulator inventory was not valid JSON", file=sys.stderr) + sys.exit(2) + +runtime_devices = payload.get("devices") +if not isinstance(runtime_devices, dict): + print("simulator inventory did not contain a devices object", file=sys.stderr) + sys.exit(2) + +eligible = [] +for devices in runtime_devices.values(): + if not isinstance(devices, list): + continue + for device in devices: + if not isinstance(device, dict): + continue + udid = device.get("udid") + name = device.get("name") + is_available = device.get("isAvailable", True) + if ( + isinstance(udid, str) + and uuid_pattern.fullmatch(udid) + and isinstance(name, str) + and name.startswith("iPhone") + and is_available is True + ): + eligible.append(device) + +if mode == "explicit": + matches = [device for device in eligible if device.get("udid", "").lower() == requested.lower()] + if len(matches) != 1: + print("requested UDID is not one available iPhone simulator", file=sys.stderr) + sys.exit(3) + print(matches[0]["udid"]) + sys.exit(0) + +booted = [device for device in eligible if device.get("state") == "Booted"] +if len(booted) == 0: + print("no booted available iPhone simulator was found; pass --simulator-udid", file=sys.stderr) + sys.exit(4) +if len(booted) > 1: + print("multiple booted iPhone simulators were found; pass --simulator-udid", file=sys.stderr) + sys.exit(5) + +print(booted[0]["udid"]) +PY +} + +fixture_fingerprint() { + local fixture="$1" + local suffix + local candidate + local digest + + for suffix in "" "-wal" "-shm"; do + candidate="${fixture}${suffix}" + if [[ -e "$candidate" ]]; then + [[ -f "$candidate" && ! -L "$candidate" ]] || + fail "copied-phone fixture components must be regular, non-symlink files" + digest="$("$SHASUM_BIN" -a 256 "$candidate" | awk '{print $1}')" + [[ "$digest" =~ ^[0-9A-Fa-f]{64}$ ]] || + fail "unable to fingerprint copied-phone fixture safely" + printf '%s=%s\n' "$suffix" "$digest" + else + printf '%s=%s\n' "$suffix" "absent" + fi + done +} + +validate_result_summary() { + local result_bundle="$1" + local expected_total="$2" + local summary_file="$3" + + [[ -d "$result_bundle" && -f "$result_bundle/Info.plist" ]] || + fail "xcodebuild succeeded without a complete xcresult bundle" + + if ! "$XCRUN_BIN" xcresulttool get test-results summary \ + --path "$result_bundle" --compact >"$summary_file" 2>/dev/null; then + fail "xcresulttool could not read the test summary" + fi + + if ! "$PYTHON_BIN" - "$summary_file" "$expected_total" "$SIMULATOR_UDID" <<'PY' +import json +import sys + +summary_path, expected_total_text, expected_device_id = sys.argv[1:] + +def reject(message): + print(f"invalid xcresult summary: {message}", file=sys.stderr) + raise SystemExit(1) + +try: + with open(summary_path, "r", encoding="utf-8") as source: + summary = json.load(source) +except (OSError, json.JSONDecodeError): + reject("malformed or missing JSON") + +if not isinstance(summary, dict): + reject("root is not an object") + +def count(container, key): + value = container.get(key) + if type(value) is not int or value < 0: + reject(f"{key} is missing or is not a nonnegative integer") + return value + +if summary.get("result") != "Passed": + reject("result is not Passed") + +total = count(summary, "totalTestCount") +passed = count(summary, "passedTests") +failed = count(summary, "failedTests") +skipped = count(summary, "skippedTests") +expected = count(summary, "expectedFailures") + +if total <= 0: + reject("no tests executed") +if failed != 0: + reject("failedTests is not zero") +if skipped != 0: + reject("skippedTests is not zero") +if expected != 0: + reject("expectedFailures is not zero") +if passed != total: + reject("passedTests does not equal totalTestCount") + +failures = summary.get("testFailures") +if not isinstance(failures, list) or failures: + reject("testFailures is missing or nonempty") + +configurations = summary.get("devicesAndConfigurations") +if not isinstance(configurations, list) or not configurations: + reject("devicesAndConfigurations is missing or empty") + +configuration_passed = 0 +configuration_failed = 0 +configuration_skipped = 0 +configuration_expected = 0 +for configuration in configurations: + if not isinstance(configuration, dict): + reject("configuration entry is not an object") + device = configuration.get("device") + if not isinstance(device, dict): + reject("configuration device is missing") + if device.get("platform") != "iOS Simulator": + reject("a test destination was not iOS Simulator") + device_id = device.get("deviceId") + if not isinstance(device_id, str) or device_id.lower() != expected_device_id.lower(): + reject("test destination does not match the selected simulator") + configuration_passed += count(configuration, "passedTests") + configuration_failed += count(configuration, "failedTests") + configuration_skipped += count(configuration, "skippedTests") + configuration_expected += count(configuration, "expectedFailures") + +if configuration_passed != passed: + reject("configuration passed count disagrees with summary") +if configuration_failed != failed: + reject("configuration failed count disagrees with summary") +if configuration_skipped != skipped: + reject("configuration skipped count disagrees with summary") +if configuration_expected != expected: + reject("configuration expected-failure count disagrees with summary") + +if expected_total_text != "any": + try: + required_total = int(expected_total_text) + except ValueError: + reject("internal expected total is invalid") + if total != required_total: + reject(f"expected exactly {required_total} tests, found {total}") +PY + then + fail "test result did not satisfy the zero-failure/zero-skip Release contract" + fi +} + +run_stage() { + local stage="$1" + local result_name + local expected_total + local fixture_before="" + local fixture_after="" + local xcodebuild_status + local -a selectors + + if [[ "$stage" == "core" ]]; then + result_name="core" + # The selected source cohort contains 409 test methods. Two copied-phone + # fixtures are intentionally handled by the separate device/store stage, + # so the simulator core stage must execute exactly 407 tests. + expected_total="407" + selectors=( + "-only-testing:fearlessTests/SingleToMultiassetUserMigrationTests" + "-only-testing:fearlessTests/UserStorageCompatibilityMigrationTests" + "-only-testing:fearlessTests/SubstrateStorageClassResolutionTests" + "-only-testing:fearlessTests/SubstrateStorageMigrationSafetyTests" + "-only-testing:fearlessTests/TransformableArchiveSafetyTests" + "-only-testing:fearlessTests/MetaAccountMapperTests" + "-only-testing:fearlessTests/SelectedAccountSettingsTests" + "-only-testing:fearlessTests/RootTests" + "-only-testing:fearlessTests/ChainModelMapperTests" + "-only-testing:fearlessTests/ChainSyncServiceCompatibilityTests" + "-only-testing:fearlessTests/ChainRegistryTests" + "-only-testing:fearlessTests/ConnectionPoolTests" + "-only-testing:fearlessTests/CrashConsistentStoreReplacerResourceLimitTests" + "-skip-testing:fearlessTests/SubstrateStorageClassResolutionTests/testCopiedPhoneV8Store_whenAvailable_thenAllRowsHaveExpectedClassesAndStoreIsUnchanged" + "-skip-testing:fearlessTests/SubstrateStorageClassResolutionTests/testCopiedPhoneV8Store_whenFetchedThroughProductionRepositories_thenMapsEveryRuntimeAndChain" + ) + else + result_name="copied-phone" + expected_total="2" + selectors=( + "-only-testing:fearlessTests/SubstrateStorageClassResolutionTests/testCopiedPhoneV8Store_whenAvailable_thenAllRowsHaveExpectedClassesAndStoreIsUnchanged" + "-only-testing:fearlessTests/SubstrateStorageClassResolutionTests/testCopiedPhoneV8Store_whenFetchedThroughProductionRepositories_thenMapsEveryRuntimeAndChain" + ) + fixture_before="$(fixture_fingerprint "$PHONE_FIXTURE")" + fi + + local result_bundle="$OUTPUT_DIR/${result_name}.xcresult" + local xcodebuild_log="$OUTPUT_DIR/${result_name}.xcodebuild.log" + local summary_file="$OUTPUT_DIR/${result_name}.summary.json" + [[ ! -e "$result_bundle" ]] || + fail "refusing to overwrite an existing xcresult bundle" + + local -a xcodebuild_arguments=( + -workspace "$WORKSPACE" + -scheme "$SCHEME" + -configuration Release + -destination "$SIMULATOR_DESTINATION" + -derivedDataPath "$DERIVED_DATA_DIR" + -clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" + -disableAutomaticPackageResolution + -skipPackageUpdates + -parallel-testing-enabled NO + -maximum-parallel-testing-workers 1 + -resultBundlePath "$result_bundle" + "SWIFT_OPTIMIZATION_LEVEL=-O" + "ENABLE_TESTABILITY=YES" + "ONLY_ACTIVE_ARCH=YES" + "${selectors[@]}" + test + ) + + log "Running ${stage} Release test stage; detailed output is private in the gate log" + set +e + if [[ "$stage" == "core" ]]; then + env -u FEARLESS_SUBSTRATE_PHONE_STORE_FIXTURE \ + "$XCODEBUILD_BIN" "${xcodebuild_arguments[@]}" >"$xcodebuild_log" 2>&1 + xcodebuild_status=$? + else + FEARLESS_SUBSTRATE_PHONE_STORE_FIXTURE="$PHONE_FIXTURE" \ + "$XCODEBUILD_BIN" "${xcodebuild_arguments[@]}" >"$xcodebuild_log" 2>&1 + xcodebuild_status=$? + fi + set -e + + if [[ "$stage" == "copied-phone" ]]; then + fixture_after="$(fixture_fingerprint "$PHONE_FIXTURE")" + [[ "$fixture_after" == "$fixture_before" ]] || + fail "copied-phone source fixture changed during the test stage" + fi + + [[ "$xcodebuild_status" -eq 0 ]] || + fail "xcodebuild failed for the ${stage} stage; inspect the private gate log" + + validate_result_summary "$result_bundle" "$expected_total" "$summary_file" + if [[ "$stage" == "copied-phone" ]]; then + log "PASSED copied-phone fixture stage: exactly 2 tests, source fixture unchanged" + else + log "PASSED core stage: exactly 407 Release -O tests, zero failures, skips, or expected failures" + fi +} + +main() { + local stage="core" + local requested_udid="${FEARLESS_CORE_DATA_SIMULATOR_UDID:-}" + local requested_destination="" + local fixture_argument="" + local output_argument="" + local derived_data_argument="" + + while [[ "$#" -gt 0 ]]; do + case "$1" in + --stage) + require_option_value "$1" "$#" + stage="$2" + shift 2 + ;; + --simulator-udid) + require_option_value "$1" "$#" + requested_udid="$2" + shift 2 + ;; + --destination) + require_option_value "$1" "$#" + requested_destination="$2" + shift 2 + ;; + --fixture) + require_option_value "$1" "$#" + fixture_argument="$2" + shift 2 + ;; + --output-dir) + require_option_value "$1" "$#" + output_argument="$2" + shift 2 + ;; + --derived-data) + require_option_value "$1" "$#" + derived_data_argument="$2" + shift 2 + ;; + --help|-h) + usage + exit 0 + ;; + *) + fail "unknown argument: $1" + ;; + esac + done + + case "$stage" in + core|copied-phone|all) ;; + *) fail "--stage must be core, copied-phone, or all" ;; + esac + + if [[ -n "$requested_udid" && -n "$requested_destination" ]]; then + fail "pass either --simulator-udid or --destination, not both" + fi + + if [[ -n "$requested_destination" ]]; then + if [[ "$requested_destination" =~ $SIMULATOR_DESTINATION_PATTERN ]]; then + requested_udid="${BASH_REMATCH[1]}" + else + fail "refusing physical, generic, named, or otherwise non-canonical simulator destination" + fi + fi + + if [[ -n "$requested_udid" && ! "$requested_udid" =~ $UUID_PATTERN ]]; then + fail "simulator UDID must be a canonical UUID" + fi + + readonly ROOT_DIR="$( + cd "${FEARLESS_CORE_DATA_ROOT_DIR:-$(dirname "${BASH_SOURCE[0]}")/../..}" && + pwd -P + )" + readonly WORKSPACE_INPUT="${FEARLESS_CORE_DATA_WORKSPACE:-$ROOT_DIR/fearless.xcworkspace}" + [[ -d "$WORKSPACE_INPUT" && -f "$WORKSPACE_INPUT/contents.xcworkspacedata" ]] || + fail "fearless.xcworkspace is missing or incomplete" + [[ "$(basename "$WORKSPACE_INPUT")" == "fearless.xcworkspace" ]] || + fail "the Core Data gate requires fearless.xcworkspace" + readonly WORKSPACE="$(canonical_directory "$WORKSPACE_INPUT")" + + readonly SOURCE_PACKAGES_INPUT="${FEARLESS_CORE_DATA_SOURCE_PACKAGES_DIR:-$ROOT_DIR/SourcePackages}" + [[ -d "$SOURCE_PACKAGES_INPUT" && -d "$SOURCE_PACKAGES_INPUT/checkouts" ]] || + fail "prepared canonical SourcePackages/checkouts is required" + readonly SOURCE_PACKAGES_DIR="$(canonical_directory "$SOURCE_PACKAGES_INPUT")" + + readonly XCODEBUILD_BIN="${FEARLESS_CORE_DATA_XCODEBUILD_BIN:-xcodebuild}" + readonly XCRUN_BIN="${FEARLESS_CORE_DATA_XCRUN_BIN:-xcrun}" + readonly PYTHON_BIN="${FEARLESS_CORE_DATA_PYTHON_BIN:-python3}" + readonly SHASUM_BIN="${FEARLESS_CORE_DATA_SHASUM_BIN:-shasum}" + require_executable "$XCODEBUILD_BIN" "xcodebuild" + require_executable "$XCRUN_BIN" "xcrun" + require_executable "$PYTHON_BIN" "python3" + if [[ "$stage" == "copied-phone" || "$stage" == "all" ]]; then + require_executable "$SHASUM_BIN" "shasum" + fi + + SIMULATOR_INVENTORY_FILE="$( + mktemp "${TMPDIR:-/private/tmp}/fearless-coredata-simulators.XXXXXX" + )" + trap cleanup EXIT + + if [[ -n "$requested_udid" ]]; then + if ! "$XCRUN_BIN" simctl list devices available -j \ + >"$SIMULATOR_INVENTORY_FILE" 2>/dev/null; then + fail "unable to query available iOS simulators" + fi + if ! SIMULATOR_UDID="$( + resolve_simulator_from_json "explicit" "$requested_udid" "$SIMULATOR_INVENTORY_FILE" + )"; then + fail "the requested simulator could not be verified" + fi + else + if ! "$XCRUN_BIN" simctl list devices booted -j \ + >"$SIMULATOR_INVENTORY_FILE" 2>/dev/null; then + fail "unable to query booted iOS simulators" + fi + if ! SIMULATOR_UDID="$( + resolve_simulator_from_json "automatic" "" "$SIMULATOR_INVENTORY_FILE" + )"; then + fail "a unique booted simulator is required" + fi + fi + readonly SIMULATOR_UDID + readonly SIMULATOR_DESTINATION="platform=iOS Simulator,id=${SIMULATOR_UDID}" + + local timestamp + timestamp="$(date -u +%Y%m%dT%H%M%SZ)" + readonly OUTPUT_DIR_INPUT="${output_argument:-$ROOT_DIR/build/test-results/CoreDataReleaseGate-$timestamp}" + [[ ! -e "$OUTPUT_DIR_INPUT" ]] || + fail "output directory must not already exist" + mkdir -p "$OUTPUT_DIR_INPUT" + readonly OUTPUT_DIR="$(canonical_directory "$OUTPUT_DIR_INPUT")" + + readonly DERIVED_DATA_DIR="${derived_data_argument:-$ROOT_DIR/build/DerivedData-CoreDataReleaseGate}" + + readonly PHONE_FIXTURE="${fixture_argument:-${FEARLESS_SUBSTRATE_PHONE_STORE_FIXTURE:-}}" + if [[ "$stage" == "copied-phone" || "$stage" == "all" ]]; then + [[ -n "$PHONE_FIXTURE" ]] || + fail "copied-phone stage requires --fixture or FEARLESS_SUBSTRATE_PHONE_STORE_FIXTURE" + [[ "$PHONE_FIXTURE" == /* ]] || + fail "copied-phone fixture path must be absolute" + [[ -r "$PHONE_FIXTURE" && -f "$PHONE_FIXTURE" && ! -L "$PHONE_FIXTURE" ]] || + fail "copied-phone fixture must be a readable, regular, non-symlink file" + fi + + log "Using fearless.xcworkspace, scheme fearless.tests, Release optimization -O" + if [[ "$stage" == "core" || "$stage" == "all" ]]; then + run_stage "core" + fi + if [[ "$stage" == "copied-phone" || "$stage" == "all" ]]; then + run_stage "copied-phone" + fi + log "PASSED requested Core Data Release gate" +} + +main "$@" diff --git a/scripts/ci/run-coredata-simulator-rehearsal.sh b/scripts/ci/run-coredata-simulator-rehearsal.sh new file mode 100755 index 0000000000..99ccd38b41 --- /dev/null +++ b/scripts/ci/run-coredata-simulator-rehearsal.sh @@ -0,0 +1,1352 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Rehearses first launch and relaunch of a standalone Release simulator app +# against a disposable, offline copy of the three Fearless Core Data stores. +# +# Safety properties: +# - requires an explicit, booted iPhone Simulator UDID; +# - verifies the .app contains an iOS Simulator binary; +# - rejects unsigned, linker-only, or Keychain-incompatible app bundles; +# - requires artifact-embedded Release, Swift -O, and disabled-testability attestations; +# - refuses a simulator where this bundle is already installed; +# - never opens, deletes, renames, chmods, or otherwise writes the source fixture; +# - runs SQLite inspection only against additional private copies; +# - never prints database rows or application log messages; +# - never uninstalls an app or erases a simulator. + +umask 077 + +readonly LOG_PREFIX="[coredata-simulator-rehearsal]" +readonly UUID_PATTERN='^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$' +readonly BUNDLE_ID_PATTERN='^[A-Za-z0-9][A-Za-z0-9.-]*[A-Za-z0-9]$' +readonly DEFAULT_ALIVE_SECONDS=8 + +readonly STORE_FILES=( + "CacheDataModel.sqlite" + "CacheDataModel.sqlite-shm" + "CacheDataModel.sqlite-wal" + "SubstrateDataModel.sqlite" + "SubstrateDataModel.sqlite-shm" + "SubstrateDataModel.sqlite-wal" + "UserDataModel.sqlite" + "UserDataModel.sqlite-shm" + "UserDataModel.sqlite-wal" +) + +readonly REQUIRED_MANAGED_OBJECT_CLASSES=( + "CDAsset" + "CDChain" + "CDChainNode" + "CDChainStorageItem" + "CDChainXcmConfig" + "CDContact" + "CDContactItem" + "CDExternalApi" + "CDPhishingItem" + "CDPolkaswapDex" + "CDPolkaswapRemoteSettings" + "CDPriceData" + "CDPriceProvider" + "CDRuntimeMetadataItem" + "CDScamInfo" + "CDStashItem" + "CDTransactionHistoryItem" + "CDXcmAvailableAsset" + "CDXcmAvailableDestination" + "CDAccountInfo" + "CDAssetVisibility" + "CDChainAccount" + "CDChainSettings" + "CDCurrency" + "CDCustomChainNode" + "CDMetaAccount" +) + +readonly REQUIRED_CORE_DATA_RESOURCES=( + "CompatibleUserDataModel_v13.mom" + "LegacyEcosystemUserDataModel_v12.mom" + "Modules_SSFAccountManagmentStorage.bundle/UserDataModel.momd/UserDataModel.mom" + "Modules_SSFAccountManagmentStorage.bundle/UserDataModel.momd/MultiassetUserDataModel.mom" + "Modules_SSFAccountManagmentStorage.bundle/UserDataModel.momd/MultiassetUserDataModel_v2.mom" + "Modules_SSFAccountManagmentStorage.bundle/UserDataModel.momd/MultiassetUserDataModel_v3.mom" + "Modules_SSFAccountManagmentStorage.bundle/UserDataModel.momd/MultiassetUserDataModel_v4.mom" + "Modules_SSFAccountManagmentStorage.bundle/UserDataModel.momd/MultiassetUserDataModel_v5.mom" + "Modules_SSFAccountManagmentStorage.bundle/UserDataModel.momd/MultiassetUserDataModel_v6.mom" + "Modules_SSFAccountManagmentStorage.bundle/UserDataModel.momd/MultiassetUserDataModel_v7.mom" + "Modules_SSFAccountManagmentStorage.bundle/UserDataModel.momd/MultiassetUserDataModel_v8.mom" + "Modules_SSFAccountManagmentStorage.bundle/UserDataModel.momd/MultiassetUserDataModel_v9.mom" + "Modules_SSFAccountManagmentStorage.bundle/UserDataModel.momd/MultiassetUserDataModel_v10.mom" + "Modules_SSFAccountManagmentStorage.bundle/UserDataModel.momd/MultiassetUserDataModel_v11.mom" + "SubstrateDataModel.momd/SubstrateDataModel.mom" + "SubstrateDataModel.momd/SubstrateDataModel_v2.mom" + "SubstrateDataModel.momd/SubstrateDataModel_v3.mom" + "SubstrateDataModel.momd/SubstrateDataModel_v4.mom" + "SubstrateDataModel.momd/SubstrateDataModel_v5.mom" + "SubstrateDataModel.momd/SubstrateDataModel_v6.mom" + "SubstrateDataModel.momd/SubstrateDataModel_v7.mom" + "SubstrateDataModel.momd/SubstrateDataModel_v8.mom" + "SingleToMultiasset.cdm" + "MultiassetV2.cdm" + "MultiassetV9.cdm" + "UserDataModelV10toV11.cdm" + "SubstrateV2Mapping.cdm" + "SubstrateV2toV4.cdm" + "SubstrateV3toV4.cdm" +) + +SIMULATOR_UDID="" +APP_PATH="" +BUNDLE_ID="" +FIXTURE_DIR="" +ARTIFACTS_DIR="" +CRASH_REPORT_DIR="${HOME}/Library/Logs/DiagnosticReports" +ALIVE_SECONDS="$DEFAULT_ALIVE_SECONDS" +DISPOSABLE_FIXTURE_CONFIRMED=0 +DRY_RUN=0 + +XCRUN_BIN="${FEARLESS_REHEARSAL_XCRUN_BIN:-xcrun}" +CODESIGN_BIN="${FEARLESS_REHEARSAL_CODESIGN_BIN:-codesign}" +PYTHON_BIN="${FEARLESS_REHEARSAL_PYTHON_BIN:-python3}" +SHASUM_BIN="${FEARLESS_REHEARSAL_SHASUM_BIN:-shasum}" +PS_BIN="${FEARLESS_REHEARSAL_PS_BIN:-ps}" +SLEEP_BIN="${FEARLESS_REHEARSAL_SLEEP_BIN:-sleep}" +PROCESS_CHECK_BIN="${FEARLESS_REHEARSAL_PROCESS_CHECK_BIN:-}" + +APP_EXECUTABLE_NAME="" +APP_EXECUTABLE_PATH="" +DATA_CONTAINER="" +CORE_DATA_TARGET="" +APP_RUNNING=0 + +log() { + printf '%s %s\n' "$LOG_PREFIX" "$*" +} + +fail() { + printf '%s ERROR: %s\n' "$LOG_PREFIX" "$*" >&2 + exit 1 +} + +cleanup() { + if [[ "$APP_RUNNING" == "1" && -n "$SIMULATOR_UDID" && -n "$BUNDLE_ID" ]]; then + "$XCRUN_BIN" simctl terminate "$SIMULATOR_UDID" "$BUNDLE_ID" \ + >/dev/null 2>&1 || true + fi +} + +trap cleanup EXIT + +usage() { + cat <<'USAGE' +Usage: + bash scripts/ci/run-coredata-simulator-rehearsal.sh \ + --simulator-udid UUID \ + --app /absolute/path/to/fearless.app \ + --bundle-id jp.co.soramitsu.fearlesswallet \ + --fixture-dir /absolute/path/to/disposable/offline/CoreData \ + --artifacts-dir /absolute/path/to/new/artifacts \ + --confirm-disposable-fixture + +Required safety acknowledgement: + --confirm-disposable-fixture + Confirms that --fixture-dir is an offline disposable copy, not a phone + container, device backup, or sole recovery copy. The directory is still + treated as immutable and is checksum-verified throughout the run. + +Options: + --alive-seconds N + Require the launched process to remain alive for N seconds per launch + (1...30, default 8). + --crash-report-dir ABSOLUTE_DIRECTORY + DiagnosticReports directory to inspect for new matching crash reports. + --dry-run + Perform fail-closed input, Simulator, binary, checksum, and copied-SQLite + validation without installing or launching the app. + --help + +Controlled test-harness overrides: + FEARLESS_REHEARSAL_XCRUN_BIN + FEARLESS_REHEARSAL_CODESIGN_BIN + FEARLESS_REHEARSAL_PYTHON_BIN + FEARLESS_REHEARSAL_SHASUM_BIN + FEARLESS_REHEARSAL_PS_BIN + FEARLESS_REHEARSAL_SLEEP_BIN + FEARLESS_REHEARSAL_PROCESS_CHECK_BIN +USAGE +} + +require_option_value() { + local option="$1" + local remaining="$2" + [[ "$remaining" -ge 2 ]] || fail "$option requires a value" +} + +require_executable() { + local executable="$1" + local description="$2" + + if [[ "$executable" == */* ]]; then + [[ -x "$executable" ]] || fail "$description is not executable" + else + command -v "$executable" >/dev/null 2>&1 || + fail "$description is unavailable" + fi +} + +validate_release_app_contract() { + local symbols_file="$ARTIFACTS_DIR/managed-object-symbols.pending" + local class_name + local resource + local build_configuration + local optimization_level + local enable_testability + + build_configuration="$( + plist_string "$APP_PATH/Info.plist" "FearlessBuildConfiguration" + )" || fail "Release app lacks its build-configuration attestation" + optimization_level="$( + plist_string "$APP_PATH/Info.plist" "FearlessSwiftOptimizationLevel" + )" || fail "Release app lacks its Swift-optimization attestation" + enable_testability="$( + plist_string "$APP_PATH/Info.plist" "FearlessEnableTestability" + )" || fail "Release app lacks its testability attestation" + + [[ "$build_configuration" == "Release" ]] || + fail "app was not built with the Release configuration" + [[ "$optimization_level" == "-O" ]] || + fail "app was not built with Swift -O optimization" + [[ "$enable_testability" == "NO" ]] || + fail "app was built with testability enabled" + + if ! "$XCRUN_BIN" nm -gj "$APP_EXECUTABLE_PATH" >"$symbols_file" 2>/dev/null; then + : >"$symbols_file" + fail "unable to inspect managed-object symbols in the app executable" + fi + + for class_name in "${REQUIRED_MANAGED_OBJECT_CLASSES[@]}"; do + if ! grep -Fqx -- "_OBJC_CLASS_\$_${class_name}" "$symbols_file"; then + : >"$symbols_file" + fail "Release app is missing a required managed-object runtime class" + fi + done + : >"$symbols_file" + + for resource in "${REQUIRED_CORE_DATA_RESOURCES[@]}"; do + [[ -f "$APP_PATH/$resource" && ! -L "$APP_PATH/$resource" ]] || + fail "Release app is missing or symlinking a required Core Data migration resource" + done + + printf '%s\n' \ + "Release, Swift -O, testability disabled; 26 managed-object runtime classes and 29 Core Data resources verified" \ + >"$ARTIFACTS_DIR/coredata-app-contract.txt" +} + +validate_app_signature_contract() { + local verification_file="$ARTIFACTS_DIR/app-signature-verification.pending" + local diagnostics_file="$ARTIFACTS_DIR/app-signature-diagnostics.pending" + + if ! "$CODESIGN_BIN" --verify --deep --strict "$APP_PATH" \ + >"$verification_file" 2>&1; then + : >"$verification_file" + fail "app failed strict code-signature verification" + fi + : >"$verification_file" + + if ! "$CODESIGN_BIN" -dv --verbose=4 "$APP_PATH" \ + >"$diagnostics_file" 2>&1; then + : >"$diagnostics_file" + fail "unable to inspect app code-signing identity" + fi + + if ! "$PYTHON_BIN" - "$diagnostics_file" "$BUNDLE_ID" <<'PY' +import re +import sys + +path, bundle_id = sys.argv[1:] +try: + with open(path, "r", encoding="utf-8", errors="replace") as source: + lines = source.readlines() +except OSError: + sys.exit(2) + +pattern = re.compile(r"^Identifier=(.+)$") +identifiers = [ + match.group(1) + for line in lines + if (match := pattern.fullmatch(line.rstrip("\r\n"))) +] +if identifiers != [bundle_id]: + sys.exit(3) +PY + then + : >"$diagnostics_file" + fail "app code-signing identifier does not exactly match the requested bundle" + fi + : >"$diagnostics_file" + + printf '%s\n' \ + "strict bundle signature and exact bundle-bound code-signing identifier verified" \ + >"$ARTIFACTS_DIR/app-signature-contract.txt" +} + +canonical_directory() { + local directory="$1" + (cd "$directory" && pwd -P) +} + +path_is_within() { + local child="$1" + local parent="$2" + + [[ "$child" == "$parent" || "$child" == "$parent/"* ]] +} + +plist_string() { + local plist="$1" + local key="$2" + + "$PYTHON_BIN" - "$plist" "$key" <<'PY' +import plistlib +import sys + +path, key = sys.argv[1:] +try: + with open(path, "rb") as source: + value = plistlib.load(source).get(key) +except (OSError, plistlib.InvalidFileException): + sys.exit(2) + +if not isinstance(value, str) or not value: + sys.exit(3) +print(value) +PY +} + +validate_fixture_directory() { + local directory="$1" + + "$PYTHON_BIN" - "$directory" <<'PY' +import os +import stat +import sys + +directory = sys.argv[1] +expected = { + "CacheDataModel.sqlite", + "CacheDataModel.sqlite-shm", + "CacheDataModel.sqlite-wal", + "SubstrateDataModel.sqlite", + "SubstrateDataModel.sqlite-shm", + "SubstrateDataModel.sqlite-wal", + "UserDataModel.sqlite", + "UserDataModel.sqlite-shm", + "UserDataModel.sqlite-wal", +} + +try: + directory_stat = os.lstat(directory) + names = set(os.listdir(directory)) +except OSError: + print("fixture directory cannot be inspected", file=sys.stderr) + sys.exit(2) + +if not stat.S_ISDIR(directory_stat.st_mode) or stat.S_ISLNK(directory_stat.st_mode): + print("fixture directory must be a real directory, not a symlink", file=sys.stderr) + sys.exit(3) +if names != expected: + print("fixture directory must contain exactly the nine expected store-family files", file=sys.stderr) + sys.exit(4) + +for name in sorted(expected): + path = os.path.join(directory, name) + try: + item_stat = os.lstat(path) + except OSError: + print("fixture component cannot be inspected", file=sys.stderr) + sys.exit(5) + if not stat.S_ISREG(item_stat.st_mode) or stat.S_ISLNK(item_stat.st_mode): + print("fixture components must be regular non-symlink files", file=sys.stderr) + sys.exit(6) + if name.endswith(".sqlite") and item_stat.st_size <= 0: + print("primary SQLite stores must be nonempty", file=sys.stderr) + sys.exit(7) +PY +} + +write_family_manifest() { + local directory="$1" + local output="$2" + local require_every_file="$3" + local filename + local digest_output + local digest + + : >"$output" + for filename in "${STORE_FILES[@]}"; do + if [[ ! -e "$directory/$filename" ]]; then + if [[ "$require_every_file" == "1" ]]; then + fail "a required store-family component is missing" + fi + printf '%s %s\n' "absent" "$filename" >>"$output" + continue + fi + + [[ -f "$directory/$filename" && ! -L "$directory/$filename" ]] || + fail "store-family components must be regular non-symlink files" + digest_output="$("$SHASUM_BIN" -a 256 "$directory/$filename")" || + fail "unable to checksum a store-family component" + digest="${digest_output%%[[:space:]]*}" + [[ "$digest" =~ ^[0-9A-Fa-f]{64}$ ]] || + fail "a store-family checksum was malformed" + printf '%s %s\n' "$digest" "$filename" >>"$output" + done +} + +require_no_unexpected_files() { + local directory="$1" + + "$PYTHON_BIN" - "$directory" <<'PY' +import os +import sys + +expected = { + "CacheDataModel.sqlite", + "CacheDataModel.sqlite-shm", + "CacheDataModel.sqlite-wal", + "SubstrateDataModel.sqlite", + "SubstrateDataModel.sqlite-shm", + "SubstrateDataModel.sqlite-wal", + "UserDataModel.sqlite", + "UserDataModel.sqlite-shm", + "UserDataModel.sqlite-wal", +} +try: + names = set(os.listdir(sys.argv[1])) +except OSError: + sys.exit(2) + +if not names.issubset(expected): + print("Core Data directory contains an unexpected file", file=sys.stderr) + sys.exit(3) +for name in names: + path = os.path.join(sys.argv[1], name) + if os.path.islink(path) or not os.path.isfile(path): + print("Core Data components must be regular non-symlink files", file=sys.stderr) + sys.exit(4) +PY +} + +copy_family() { + local source="$1" + local destination="$2" + local require_every_file="$3" + local filename + + [[ -d "$destination" && ! -L "$destination" ]] || + fail "copy destination is not a real directory" + if [[ -n "$(find "$destination" -mindepth 1 -maxdepth 1 -print -quit)" ]]; then + fail "copy destination must be empty" + fi + + for filename in "${STORE_FILES[@]}"; do + if [[ -e "$source/$filename" ]]; then + [[ -f "$source/$filename" && ! -L "$source/$filename" ]] || + fail "copy source contains a non-regular component" + cp -p "$source/$filename" "$destination/$filename" + elif [[ "$require_every_file" == "1" ]]; then + fail "copy source is missing a required store-family component" + fi + done +} + +inspect_sqlite_copies() { + local directory="$1" + local output="$2" + + "$PYTHON_BIN" - "$directory" "$output" <<'PY' +import json +import hashlib +import os +import sqlite3 +import sys + +directory, output = sys.argv[1:] +stores = ( + "CacheDataModel.sqlite", + "SubstrateDataModel.sqlite", + "UserDataModel.sqlite", +) +result = {} + +def quote_identifier(value): + return '"' + value.replace('"', '""') + '"' + +def encode_value(value): + if value is None: + return b"n;" + if isinstance(value, bytes): + return b"b" + str(len(value)).encode("ascii") + b":" + value + b";" + if isinstance(value, str): + encoded = value.encode("utf-8") + return b"s" + str(len(encoded)).encode("ascii") + b":" + encoded + b";" + if isinstance(value, int): + return b"i" + str(value).encode("ascii") + b";" + if isinstance(value, float): + return b"f" + value.hex().encode("ascii") + b";" + raise RuntimeError("unsupported protected SQLite value type") + +def rows_digest(rows): + encoded_rows = [] + for row in rows: + encoded_rows.append(b"".join(encode_value(value) for value in row)) + encoded_rows.sort() + + digest = hashlib.sha256() + for row in encoded_rows: + digest.update(str(len(row)).encode("ascii")) + digest.update(b":") + digest.update(row) + digest.update(b";") + return digest.hexdigest() + +def table_columns(connection, table_name): + return { + row[1] + for row in connection.execute( + f"PRAGMA table_info({quote_identifier(table_name)})" + ).fetchall() + } + +def protected_user_fingerprints(connection, table_names): + meta_table = "ZCDMETAACCOUNT" + chain_table = "ZCDCHAINACCOUNT" + if meta_table not in table_names or chain_table not in table_names: + raise RuntimeError("User store lacks protected wallet tables") + + meta_columns = table_columns(connection, meta_table) + core_meta_columns = ( + "ZMETAID", + "ZSUBSTRATEACCOUNTID", + "ZSUBSTRATEPUBLICKEY", + "ZSUBSTRATECRYPTOTYPE", + "ZETHEREUMADDRESS", + "ZETHEREUMPUBLICKEY", + ) + missing_meta = set(core_meta_columns) - meta_columns + if missing_meta: + raise RuntimeError("User store lacks protected wallet identity columns") + + quoted_core_meta = ", ".join(quote_identifier(value) for value in core_meta_columns) + fingerprints = { + "wallet-core-identities-and-keys": rows_digest( + connection.execute( + f"SELECT {quoted_core_meta} FROM {quote_identifier(meta_table)}" + ).fetchall() + ) + } + + optional_wallet_groups = { + "wallet-ton-identities-and-keys": ( + "ZTONADDRESS", + "ZTONPUBLICKEY", + ), + } + for label, columns in optional_wallet_groups.items(): + if set(columns).issubset(meta_columns): + selected = ("ZMETAID",) + columns + quoted = ", ".join(quote_identifier(value) for value in selected) + fingerprints[label] = rows_digest( + connection.execute( + f"SELECT {quoted} FROM {quote_identifier(meta_table)}" + ).fetchall() + ) + + chain_columns = table_columns(connection, chain_table) + core_chain_columns = ( + "ZACCOUNTID", + "ZCHAINID", + "ZPUBLICKEY", + "ZCRYPTOTYPE", + "ZMETAACCOUNT", + ) + missing_chain = set(core_chain_columns) - chain_columns + if missing_chain: + raise RuntimeError("User store lacks protected child-account columns") + + fingerprints["child-identities-keys-and-wallet-links"] = rows_digest( + connection.execute( + f""" + SELECT + child.ZACCOUNTID, + child.ZCHAINID, + child.ZPUBLICKEY, + child.ZCRYPTOTYPE, + parent.ZMETAID + FROM {quote_identifier(chain_table)} AS child + LEFT JOIN {quote_identifier(meta_table)} AS parent + ON child.ZMETAACCOUNT = parent.Z_PK + """ + ).fetchall() + ) + return fingerprints + +try: + for store_name in stores: + store_path = os.path.join(directory, store_name) + if not os.path.isfile(store_path) or os.path.islink(store_path): + raise RuntimeError(f"{store_name}: copied primary store is missing") + + connection = sqlite3.connect(store_path, timeout=5.0) + try: + connection.execute("PRAGMA query_only = ON") + integrity_rows = [ + row[0] for row in connection.execute("PRAGMA integrity_check").fetchall() + ] + if integrity_rows != ["ok"]: + raise RuntimeError(f"{store_name}: integrity check failed") + + table_names = [ + row[0] + for row in connection.execute( + """ + SELECT name + FROM sqlite_master + WHERE type = 'table' AND name GLOB 'Z*' + ORDER BY name + """ + ).fetchall() + ] + table_counts = {} + for table_name in table_names: + quoted_name = quote_identifier(table_name) + count = connection.execute( + f"SELECT COUNT(*) FROM {quoted_name}" + ).fetchone()[0] + if not isinstance(count, int) or count < 0: + raise RuntimeError(f"{store_name}: invalid table count") + table_counts[table_name] = count + + store_result = { + "integrity": "ok", + "applicationRowCount": sum(table_counts.values()), + "tables": table_counts, + } + if store_name == "UserDataModel.sqlite": + store_result["protectedFingerprints"] = ( + protected_user_fingerprints(connection, set(table_names)) + ) + result[store_name] = store_result + finally: + connection.close() +except (OSError, sqlite3.Error, RuntimeError) as error: + print(f"copied SQLite inspection failed: {error}", file=sys.stderr) + sys.exit(2) + +temporary_output = output + ".pending" +with open(temporary_output, "w", encoding="utf-8") as destination: + json.dump(result, destination, indent=2, sort_keys=True) + destination.write("\n") +os.replace(temporary_output, output) +PY +} + +log_count_summary() { + local counts_file="$1" + local phase="$2" + + "$PYTHON_BIN" - "$counts_file" "$phase" <<'PY' +import json +import sys + +path, phase = sys.argv[1:] +with open(path, "r", encoding="utf-8") as source: + payload = json.load(source) +for store_name in sorted(payload): + count = payload[store_name]["applicationRowCount"] + print(f"[coredata-simulator-rehearsal] {phase} {store_name}: {count} aggregate rows") +PY +} + +validate_preservation() { + local before="$1" + local after_first="$2" + local after_second="$3" + + "$PYTHON_BIN" - "$before" "$after_first" "$after_second" <<'PY' +import json +import sys + +payloads = [] +for path in sys.argv[1:]: + with open(path, "r", encoding="utf-8") as source: + payloads.append(json.load(source)) + +before, first, second = payloads +for phase_name, payload in (("prelaunch", before), ("first launch", first), ("relaunch", second)): + for store_name in ("CacheDataModel.sqlite", "SubstrateDataModel.sqlite", "UserDataModel.sqlite"): + store = payload.get(store_name) + if not isinstance(store, dict) or store.get("integrity") != "ok": + print(f"{phase_name} lacks an intact {store_name}", file=sys.stderr) + sys.exit(2) + +user_critical_tables = ("ZCDMETAACCOUNT", "ZCDCHAINACCOUNT") +before_user = before["UserDataModel.sqlite"]["tables"] +for table_name in user_critical_tables: + if table_name not in before_user: + print(f"prelaunch User store lacks required {table_name} count", file=sys.stderr) + sys.exit(3) + expected_count = before_user[table_name] + for phase_name, payload in (("first launch", first), ("relaunch", second)): + actual_count = payload["UserDataModel.sqlite"]["tables"].get(table_name) + if actual_count != expected_count: + print(f"{phase_name} changed the protected {table_name} row count", file=sys.stderr) + sys.exit(4) + +before_fingerprints = before["UserDataModel.sqlite"].get("protectedFingerprints") +if not isinstance(before_fingerprints, dict) or not before_fingerprints: + print("prelaunch User store lacks protected wallet fingerprints", file=sys.stderr) + sys.exit(5) +for fingerprint_name, expected_fingerprint in before_fingerprints.items(): + if ( + not isinstance(fingerprint_name, str) + or not isinstance(expected_fingerprint, str) + or len(expected_fingerprint) != 64 + ): + print("prelaunch User store has malformed protected fingerprints", file=sys.stderr) + sys.exit(6) + for phase_name, payload in (("first launch", first), ("relaunch", second)): + actual_fingerprint = ( + payload["UserDataModel.sqlite"] + .get("protectedFingerprints", {}) + .get(fingerprint_name) + ) + if actual_fingerprint != expected_fingerprint: + print( + f"{phase_name} changed protected wallet identities, keys, or relationships", + file=sys.stderr, + ) + sys.exit(7) + +substrate_critical_tables = ("ZCDCHAIN", "ZCDRUNTIMEMETADATAITEM") +before_substrate = before["SubstrateDataModel.sqlite"]["tables"] +for table_name in substrate_critical_tables: + expected_count = before_substrate.get(table_name) + if not isinstance(expected_count, int) or expected_count <= 0: + print(f"prelaunch Substrate store lacks populated {table_name}", file=sys.stderr) + sys.exit(8) + for phase_name, payload in (("first launch", first), ("relaunch", second)): + actual_count = payload["SubstrateDataModel.sqlite"]["tables"].get(table_name) + if actual_count != expected_count: + print(f"{phase_name} changed the protected {table_name} row count", file=sys.stderr) + sys.exit(9) +PY +} + +validate_simulator_inventory() { + local inventory_file="$1" + + "$PYTHON_BIN" - "$inventory_file" "$SIMULATOR_UDID" <<'PY' +import json +import re +import sys + +inventory_path, requested = sys.argv[1:] +uuid_pattern = re.compile( + r"^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-" + r"[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$" +) +if not uuid_pattern.fullmatch(requested): + print("requested Simulator UDID is not a canonical UUID", file=sys.stderr) + sys.exit(2) + +try: + with open(inventory_path, "r", encoding="utf-8") as source: + payload = json.load(source) +except (OSError, json.JSONDecodeError): + print("simctl returned malformed device inventory", file=sys.stderr) + sys.exit(3) + +matches = [] +devices_by_runtime = payload.get("devices") +if not isinstance(devices_by_runtime, dict): + print("simctl device inventory has no devices object", file=sys.stderr) + sys.exit(4) + +for runtime, devices in devices_by_runtime.items(): + if not isinstance(runtime, str) or ".SimRuntime.iOS-" not in runtime: + continue + if not isinstance(devices, list): + continue + for device in devices: + if ( + isinstance(device, dict) + and str(device.get("udid", "")).lower() == requested.lower() + ): + matches.append(device) + +if len(matches) != 1: + print("requested UDID is not exactly one iOS Simulator", file=sys.stderr) + sys.exit(5) + +device = matches[0] +if device.get("isAvailable", True) is not True: + print("requested iOS Simulator is unavailable", file=sys.stderr) + sys.exit(6) +if device.get("state") != "Booted": + print("requested iOS Simulator must already be Booted", file=sys.stderr) + sys.exit(7) +name = device.get("name") +if not isinstance(name, str) or not name.startswith("iPhone"): + print("requested Simulator must be an iPhone", file=sys.stderr) + sys.exit(8) +PY +} + +process_is_expected_and_alive() { + local pid="$1" + local process_command + + if [[ -n "$PROCESS_CHECK_BIN" ]]; then + "$PROCESS_CHECK_BIN" "$pid" "$APP_EXECUTABLE_NAME" + return + fi + + kill -0 "$pid" 2>/dev/null || return 1 + process_command="$("$PS_BIN" -p "$pid" -o command= 2>/dev/null)" || return 1 + [[ "$process_command" == *"/$APP_EXECUTABLE_NAME"* || + "$process_command" == "$APP_EXECUTABLE_NAME" || + "$process_command" == "$APP_EXECUTABLE_NAME "* ]] +} + +assert_process_stays_alive() { + local pid="$1" + local elapsed=0 + + while [[ "$elapsed" -lt "$ALIVE_SECONDS" ]]; do + process_is_expected_and_alive "$pid" || + fail "launched app did not remain alive for the required interval" + "$SLEEP_BIN" 1 + elapsed=$((elapsed + 1)) + done + process_is_expected_and_alive "$pid" || + fail "launched app exited at the end of the required interval" +} + +parse_launch_pid() { + local launch_output="$1" + + "$PYTHON_BIN" - "$launch_output" "$BUNDLE_ID" <<'PY' +import re +import sys + +path, bundle_id = sys.argv[1:] +try: + with open(path, "r", encoding="utf-8", errors="replace") as source: + lines = source.readlines() +except OSError: + sys.exit(2) + +pattern = re.compile(r"^\s*" + re.escape(bundle_id) + r":\s*([1-9][0-9]*)\s*$") +matches = [pattern.match(line).group(1) for line in lines if pattern.match(line)] +if len(matches) != 1: + sys.exit(3) +print(matches[0]) +PY +} + +scan_for_new_crash_reports() { + local marker="$1" + local phase="$2" + local result_file="$ARTIFACTS_DIR/crash-scan-${phase}.txt" + + if "$PYTHON_BIN" - \ + "$CRASH_REPORT_DIR" \ + "$marker" \ + "$SIMULATOR_UDID" \ + "$BUNDLE_ID" \ + "$APP_EXECUTABLE_NAME" \ + "$result_file" <<'PY' +import os +import sys + +root, marker_path, _udid, bundle_id, executable, output = sys.argv[1:] +marker_ns = os.stat(marker_path).st_mtime_ns +matches = [] + +for directory, _, filenames in os.walk(root): + for filename in filenames: + if not (filename.endswith(".ips") or filename.endswith(".crash")): + continue + path = os.path.join(directory, filename) + try: + if os.stat(path).st_mtime_ns < marker_ns: + continue + with open(path, "r", encoding="utf-8", errors="ignore") as source: + content = source.read() + except OSError: + continue + lowered = content.lower() + has_app = bundle_id.lower() in lowered or executable.lower() in lowered + if has_app: + matches.append(filename) + +with open(output, "w", encoding="utf-8") as destination: + if matches: + destination.write("matching crash report detected\n") + else: + destination.write("clean\n") + +sys.exit(1 if matches else 0) +PY + then + return + fi + + fail "a new crash report matched the requested Simulator app during $phase" +} + +scan_unified_log_markers() { + local start_time="$1" + local phase="$2" + local raw_file="$ARTIFACTS_DIR/unified-log-filtered-${phase}.pending" + local scan_file="$ARTIFACTS_DIR/unified-log-scan-${phase}.txt" + local predicate + + predicate="process == \"$APP_EXECUTABLE_NAME\" AND (eventMessage CONTAINS[c] \"FEARLESS_STARTUP_\" OR eventMessage CONTAINS[c] \"CoreData\" OR eventMessage CONTAINS[c] \"migration\" OR eventMessage CONTAINS[c] \"persistent store\" OR eventMessage CONTAINS[c] \"fatal\" OR eventMessage CONTAINS[c] \"exception\" OR eventMessage CONTAINS[c] \"abort\")" + + if ! "$XCRUN_BIN" simctl spawn "$SIMULATOR_UDID" log show \ + --style compact \ + --start "$start_time" \ + --predicate "$predicate" >"$raw_file" 2>/dev/null; then + : >"$raw_file" + fail "unable to inspect the Simulator unified log" + fi + + if "$PYTHON_BIN" - "$raw_file" "$scan_file" <<'PY' +import re +import sys + +source_path, output_path = sys.argv[1:] +patterns = { + "fatal-error": re.compile(r"\bfatal error\b", re.IGNORECASE), + "uncaught-exception": re.compile(r"\buncaught (?:objective-c )?exception\b", re.IGNORECASE), + "abort": re.compile(r"\b(?:abort|aborted|sigabrt)\b", re.IGNORECASE), + "migration-failure": re.compile( + r"\b(?:migration|migrate|mapping model)\b.{0,120}\b(?:fail|error|incompatible|missing|abort)\b", + re.IGNORECASE, + ), + "persistent-store-failure": re.compile( + r"\b(?:persistent store|addPersistentStore|NSPersistentStore)\b.{0,120}\b(?:fail|error|incompatible|missing|abort)\b", + re.IGNORECASE, + ), + "core-data-failure": re.compile( + r"\b(?:CoreData|NSCocoaErrorDomain)\b.{0,120}\b(?:fatal|fail|error|exception|abort|incompatible)\b", + re.IGNORECASE, + ), +} + +with open(source_path, "r", encoding="utf-8", errors="ignore") as source: + content = source.read() +matched = sorted(name for name, pattern in patterns.items() if pattern.search(content)) +ready_count = content.count("FEARLESS_STARTUP_READY") +failure_count = content.count("FEARLESS_STARTUP_FAILED") +if ready_count != 1: + matched.append("startup-ready-count") +if failure_count != 0: + matched.append("startup-failure") +matched = sorted(set(matched)) + +with open(output_path, "w", encoding="utf-8") as destination: + if matched: + for name in matched: + destination.write(name + "\n") + else: + destination.write("clean\n") + +sys.exit(1 if matched else 0) +PY + then + : >"$raw_file" + return + fi + + : >"$raw_file" + fail "startup readiness failed or fatal migration/Core Data marker detected during $phase" +} + +terminate_running_app() { + local pid="$1" + local attempts=0 + + "$XCRUN_BIN" simctl terminate "$SIMULATOR_UDID" "$BUNDLE_ID" \ + >/dev/null 2>&1 || + fail "simctl could not terminate the rehearsed app" + APP_RUNNING=0 + + while process_is_expected_and_alive "$pid"; do + attempts=$((attempts + 1)) + [[ "$attempts" -le 5 ]] || + fail "rehearsed app process remained alive after termination" + "$SLEEP_BIN" 1 + done +} + +capture_target_state() { + local phase="$1" + local manifest="$ARTIFACTS_DIR/${phase}-target.sha256" + local audit_directory="$ARTIFACTS_DIR/audits/$phase" + local counts_file="$ARTIFACTS_DIR/${phase}-counts.json" + + require_no_unexpected_files "$CORE_DATA_TARGET" || + fail "target Core Data directory failed strict file validation" + write_family_manifest "$CORE_DATA_TARGET" "$manifest" "0" + mkdir "$audit_directory" + copy_family "$CORE_DATA_TARGET" "$audit_directory" "0" + inspect_sqlite_copies "$audit_directory" "$counts_file" + log_count_summary "$counts_file" "$phase" +} + +run_launch_phase() { + local phase="$1" + local marker="$ARTIFACTS_DIR/${phase}-launch.marker" + local launch_output="$ARTIFACTS_DIR/${phase}-launch-output.pending" + local launch_error="$ARTIFACTS_DIR/${phase}-launch-error.pending" + local start_time + local pid + + : >"$marker" + start_time="$(date -u '+%Y-%m-%d %H:%M:%S%z')" + if ! "$XCRUN_BIN" simctl launch "$SIMULATOR_UDID" "$BUNDLE_ID" \ + >"$launch_output" 2>"$launch_error"; then + : >"$launch_output" + : >"$launch_error" + fail "simctl could not launch the app during $phase" + fi + pid="$(parse_launch_pid "$launch_output")" || + fail "simctl launch did not return one valid app PID" + : >"$launch_output" + : >"$launch_error" + APP_RUNNING=1 + + assert_process_stays_alive "$pid" + scan_for_new_crash_reports "$marker" "${phase}-while-running" + scan_unified_log_markers "$start_time" "$phase" + terminate_running_app "$pid" + scan_for_new_crash_reports "$marker" "${phase}-after-termination" + capture_target_state "$phase" +} + +while [[ "$#" -gt 0 ]]; do + case "$1" in + --simulator-udid) + require_option_value "$1" "$#" + SIMULATOR_UDID="$2" + shift 2 + ;; + --app) + require_option_value "$1" "$#" + APP_PATH="$2" + shift 2 + ;; + --bundle-id) + require_option_value "$1" "$#" + BUNDLE_ID="$2" + shift 2 + ;; + --fixture-dir) + require_option_value "$1" "$#" + FIXTURE_DIR="$2" + shift 2 + ;; + --artifacts-dir) + require_option_value "$1" "$#" + ARTIFACTS_DIR="$2" + shift 2 + ;; + --crash-report-dir) + require_option_value "$1" "$#" + CRASH_REPORT_DIR="$2" + shift 2 + ;; + --alive-seconds) + require_option_value "$1" "$#" + ALIVE_SECONDS="$2" + shift 2 + ;; + --confirm-disposable-fixture) + DISPOSABLE_FIXTURE_CONFIRMED=1 + shift + ;; + --dry-run) + DRY_RUN=1 + shift + ;; + --help|-h) + usage + exit 0 + ;; + *) + fail "unknown argument: $1" + ;; + esac +done + +[[ -n "$SIMULATOR_UDID" ]] || fail "--simulator-udid is required" +[[ "$SIMULATOR_UDID" =~ $UUID_PATTERN ]] || + fail "--simulator-udid must be a canonical UUID" +[[ -n "$APP_PATH" ]] || fail "--app is required" +[[ "$APP_PATH" == /* ]] || fail "--app must be an absolute path" +[[ -n "$BUNDLE_ID" ]] || fail "--bundle-id is required" +[[ "$BUNDLE_ID" =~ $BUNDLE_ID_PATTERN ]] || + fail "--bundle-id is malformed" +[[ -n "$FIXTURE_DIR" ]] || fail "--fixture-dir is required" +[[ "$FIXTURE_DIR" == /* ]] || fail "--fixture-dir must be an absolute path" +[[ -n "$ARTIFACTS_DIR" ]] || fail "--artifacts-dir is required" +[[ "$ARTIFACTS_DIR" == /* ]] || fail "--artifacts-dir must be an absolute path" +[[ "$CRASH_REPORT_DIR" == /* ]] || + fail "--crash-report-dir must be an absolute path" +[[ "$ALIVE_SECONDS" =~ ^[0-9]+$ ]] || + fail "--alive-seconds must be an integer" +[[ "$ALIVE_SECONDS" -ge 1 && "$ALIVE_SECONDS" -le 30 ]] || + fail "--alive-seconds must be between 1 and 30" +[[ "$DISPOSABLE_FIXTURE_CONFIRMED" == "1" ]] || + fail "--confirm-disposable-fixture is required" + +require_executable "$XCRUN_BIN" "xcrun" +require_executable "$CODESIGN_BIN" "codesign" +require_executable "$PYTHON_BIN" "Python" +require_executable "$SHASUM_BIN" "shasum" +require_executable "$PS_BIN" "ps" +require_executable "$SLEEP_BIN" "sleep" +if [[ -n "$PROCESS_CHECK_BIN" ]]; then + require_executable "$PROCESS_CHECK_BIN" "process checker" +fi + +[[ -d "$APP_PATH" && ! -L "$APP_PATH" ]] || + fail "--app must name a real .app directory, not a symlink" +[[ "${APP_PATH##*/}" == *.app ]] || + fail "--app must end in .app" +APP_PATH="$(canonical_directory "$APP_PATH")" + +[[ -d "$FIXTURE_DIR" && ! -L "$FIXTURE_DIR" ]] || + fail "--fixture-dir must name a real directory, not a symlink" +FIXTURE_DIR="$(canonical_directory "$FIXTURE_DIR")" + +[[ -d "$CRASH_REPORT_DIR" && ! -L "$CRASH_REPORT_DIR" ]] || + fail "--crash-report-dir must name a real directory" +CRASH_REPORT_DIR="$(canonical_directory "$CRASH_REPORT_DIR")" + +[[ ! -e "$ARTIFACTS_DIR" && ! -L "$ARTIFACTS_DIR" ]] || + fail "--artifacts-dir must not already exist" +artifacts_parent="$(dirname "$ARTIFACTS_DIR")" +artifacts_name="$(basename "$ARTIFACTS_DIR")" +[[ "$artifacts_name" != "." && "$artifacts_name" != ".." ]] || + fail "--artifacts-dir has an unsafe basename" +[[ -d "$artifacts_parent" && ! -L "$artifacts_parent" ]] || + fail "--artifacts-dir parent must be a real existing directory" +artifacts_parent="$(canonical_directory "$artifacts_parent")" +ARTIFACTS_DIR="$artifacts_parent/$artifacts_name" + +if path_is_within "$ARTIFACTS_DIR" "$FIXTURE_DIR" || + path_is_within "$FIXTURE_DIR" "$ARTIFACTS_DIR"; then + fail "artifacts and fixture directories must not overlap" +fi +if path_is_within "$ARTIFACTS_DIR" "$APP_PATH" || + path_is_within "$APP_PATH" "$ARTIFACTS_DIR"; then + fail "artifacts and app directories must not overlap" +fi + +mkdir "$ARTIFACTS_DIR" +mkdir "$ARTIFACTS_DIR/staging" +mkdir "$ARTIFACTS_DIR/audits" + +[[ -f "$APP_PATH/Info.plist" && ! -L "$APP_PATH/Info.plist" ]] || + fail "app Info.plist is missing or unsafe" +actual_bundle_id="$(plist_string "$APP_PATH/Info.plist" "CFBundleIdentifier")" || + fail "unable to read CFBundleIdentifier from the app" +[[ "$actual_bundle_id" == "$BUNDLE_ID" ]] || + fail "explicit bundle ID does not match the app" + +APP_EXECUTABLE_NAME="$(plist_string "$APP_PATH/Info.plist" "CFBundleExecutable")" || + fail "unable to read CFBundleExecutable from the app" +[[ "$APP_EXECUTABLE_NAME" =~ ^[A-Za-z0-9._-]+$ ]] || + fail "app executable name is unsafe" +APP_EXECUTABLE_PATH="$APP_PATH/$APP_EXECUTABLE_NAME" +[[ -f "$APP_EXECUTABLE_PATH" && ! -L "$APP_EXECUTABLE_PATH" && + -x "$APP_EXECUTABLE_PATH" ]] || + fail "app executable is missing, non-regular, symlinked, or not executable" + +build_platform="$("$XCRUN_BIN" vtool -show-build "$APP_EXECUTABLE_PATH" 2>/dev/null)" || + fail "unable to inspect app binary platform" +if ! grep -Eq \ + 'platform[[:space:]]+(IOSSIMULATOR|7)([[:space:]]|$)|LC_VERSION_MIN_IPHONESIMULATOR' \ + <<<"$build_platform"; then + fail "app binary is not an iOS Simulator binary; refusing device app" +fi +if grep -Eq 'platform[[:space:]]+IOS([[:space:]]|$)' <<<"$build_platform"; then + fail "app binary includes an iOS device platform; refusing it" +fi +printf '%s\n' "iOS Simulator binary verified" \ + >"$ARTIFACTS_DIR/app-platform-verification.txt" +validate_app_signature_contract +validate_release_app_contract + +if ! "$XCRUN_BIN" simctl list devices --json \ + >"$ARTIFACTS_DIR/simulator-inventory.pending" 2>/dev/null; then + fail "unable to obtain Simulator inventory" +fi +validate_simulator_inventory "$ARTIFACTS_DIR/simulator-inventory.pending" || + fail "requested destination failed Simulator-only validation" +: >"$ARTIFACTS_DIR/simulator-inventory.pending" +printf '%s\n' "explicit booted iPhone Simulator verified" \ + >"$ARTIFACTS_DIR/simulator-verification.txt" + +validate_fixture_directory "$FIXTURE_DIR" || + fail "fixture failed exact immutable-family validation" +write_family_manifest \ + "$FIXTURE_DIR" \ + "$ARTIFACTS_DIR/source-before.sha256" \ + "1" + +seed_directory="$ARTIFACTS_DIR/staging/seed" +mkdir "$seed_directory" +copy_family "$FIXTURE_DIR" "$seed_directory" "1" +write_family_manifest \ + "$FIXTURE_DIR" \ + "$ARTIFACTS_DIR/source-after-copy.sha256" \ + "1" +cmp -s \ + "$ARTIFACTS_DIR/source-before.sha256" \ + "$ARTIFACTS_DIR/source-after-copy.sha256" || + fail "source fixture changed while it was copied" +write_family_manifest \ + "$seed_directory" \ + "$ARTIFACTS_DIR/seed.sha256" \ + "1" +cmp -s "$ARTIFACTS_DIR/source-before.sha256" "$ARTIFACTS_DIR/seed.sha256" || + fail "private seed copy does not match the source fixture" + +if [[ "$DRY_RUN" == "1" ]]; then + dry_audit="$ARTIFACTS_DIR/audits/dry-run" + mkdir "$dry_audit" + copy_family "$seed_directory" "$dry_audit" "1" + inspect_sqlite_copies "$dry_audit" "$ARTIFACTS_DIR/dry-run-counts.json" + log_count_summary "$ARTIFACTS_DIR/dry-run-counts.json" "dry-run" + write_family_manifest \ + "$FIXTURE_DIR" \ + "$ARTIFACTS_DIR/source-after-run.sha256" \ + "1" + cmp -s \ + "$ARTIFACTS_DIR/source-before.sha256" \ + "$ARTIFACTS_DIR/source-after-run.sha256" || + fail "source fixture changed during dry-run validation" + log "DRY RUN PASSED: inputs are simulator-only, copied stores are intact, source is unchanged" + exit 0 +fi + +if "$XCRUN_BIN" simctl get_app_container \ + "$SIMULATOR_UDID" "$BUNDLE_ID" data \ + >"$ARTIFACTS_DIR/preexisting-data-container.pending" 2>&1; then + : >"$ARTIFACTS_DIR/preexisting-data-container.pending" + fail "bundle already has Simulator data; use a fresh dedicated Simulator" +fi +: >"$ARTIFACTS_DIR/preexisting-data-container.pending" + +if "$XCRUN_BIN" simctl get_app_container \ + "$SIMULATOR_UDID" "$BUNDLE_ID" app \ + >"$ARTIFACTS_DIR/preexisting-app-container.pending" 2>&1; then + : >"$ARTIFACTS_DIR/preexisting-app-container.pending" + fail "bundle is already installed; use a fresh dedicated Simulator" +fi +: >"$ARTIFACTS_DIR/preexisting-app-container.pending" + +if ! "$XCRUN_BIN" simctl install "$SIMULATOR_UDID" "$APP_PATH" \ + >"$ARTIFACTS_DIR/install.pending" 2>&1; then + : >"$ARTIFACTS_DIR/install.pending" + fail "simctl could not install the app" +fi +: >"$ARTIFACTS_DIR/install.pending" + +if ! "$XCRUN_BIN" simctl get_app_container \ + "$SIMULATOR_UDID" "$BUNDLE_ID" data \ + >"$ARTIFACTS_DIR/data-container.pending" 2>/dev/null; then + fail "unable to obtain the installed app data container" +fi +DATA_CONTAINER="$(tr -d '\r\n' <"$ARTIFACTS_DIR/data-container.pending")" +: >"$ARTIFACTS_DIR/data-container.pending" +[[ "$DATA_CONTAINER" == /* && -d "$DATA_CONTAINER" && ! -L "$DATA_CONTAINER" ]] || + fail "simctl returned an unsafe data container" +DATA_CONTAINER="$(canonical_directory "$DATA_CONTAINER")" +[[ "$DATA_CONTAINER" == *"/CoreSimulator/Devices/$SIMULATOR_UDID/data/Containers/Data/Application/"* ]] || + fail "data container is not rooted in the requested Simulator" + +# A newly installed app is not expected to be running. A successful terminate +# is accepted, as is simctl's documented-style "not running" response. +if ! "$XCRUN_BIN" simctl terminate "$SIMULATOR_UDID" "$BUNDLE_ID" \ + >"$ARTIFACTS_DIR/preseed-terminate.pending" 2>&1; then + if ! grep -Eqi 'not running|nothing to terminate|no such process' \ + "$ARTIFACTS_DIR/preseed-terminate.pending"; then + : >"$ARTIFACTS_DIR/preseed-terminate.pending" + fail "could not establish a terminated preseed app state" + fi +fi +: >"$ARTIFACTS_DIR/preseed-terminate.pending" + +write_family_manifest \ + "$FIXTURE_DIR" \ + "$ARTIFACTS_DIR/source-before-injection.sha256" \ + "1" +cmp -s \ + "$ARTIFACTS_DIR/source-before.sha256" \ + "$ARTIFACTS_DIR/source-before-injection.sha256" || + fail "source fixture changed before Simulator injection" + +CORE_DATA_TARGET="$DATA_CONTAINER/Documents/CoreData" +if [[ -e "$CORE_DATA_TARGET" || -L "$CORE_DATA_TARGET" ]]; then + [[ -d "$CORE_DATA_TARGET" && ! -L "$CORE_DATA_TARGET" ]] || + fail "target CoreData path is not a real directory" + if [[ -n "$(find "$CORE_DATA_TARGET" -mindepth 1 -maxdepth 1 -print -quit)" ]]; then + fail "new Simulator app unexpectedly has existing Core Data files" + fi +else + mkdir -p "$CORE_DATA_TARGET" +fi +CORE_DATA_TARGET="$(canonical_directory "$CORE_DATA_TARGET")" +path_is_within "$CORE_DATA_TARGET" "$DATA_CONTAINER" || + fail "target CoreData directory escaped the Simulator data container" + +copy_family "$seed_directory" "$CORE_DATA_TARGET" "1" +require_no_unexpected_files "$CORE_DATA_TARGET" || + fail "injected Core Data family failed strict validation" +write_family_manifest \ + "$CORE_DATA_TARGET" \ + "$ARTIFACTS_DIR/prelaunch-target.sha256" \ + "1" +cmp -s \ + "$ARTIFACTS_DIR/source-before.sha256" \ + "$ARTIFACTS_DIR/prelaunch-target.sha256" || + fail "injected Simulator store family does not match the source" +capture_target_state "prelaunch" + +run_launch_phase "first-launch" +run_launch_phase "relaunch" + +validate_preservation \ + "$ARTIFACTS_DIR/prelaunch-counts.json" \ + "$ARTIFACTS_DIR/first-launch-counts.json" \ + "$ARTIFACTS_DIR/relaunch-counts.json" || + fail "protected Core Data counts were not preserved" + +write_family_manifest \ + "$FIXTURE_DIR" \ + "$ARTIFACTS_DIR/source-after-run.sha256" \ + "1" +cmp -s \ + "$ARTIFACTS_DIR/source-before.sha256" \ + "$ARTIFACTS_DIR/source-after-run.sha256" || + fail "source fixture changed during the rehearsal" + +log "PASSED: first launch and relaunch each reached a usable startup route with no crash or fatal migration/Core Data marker" +log "PASSED: copied stores remain intact; protected counts, wallet identities, keys, and relationships are preserved" +log "PASSED: signed Keychain-capable Release/-O/non-testable artifact, 26 managed-object classes, and 29 migration resources verified; source fixture is unchanged" diff --git a/scripts/storage/audit-user-storage-compatibility-models.sh b/scripts/storage/audit-user-storage-compatibility-models.sh new file mode 100755 index 0000000000..567ba5c345 --- /dev/null +++ b/scripts/storage/audit-user-storage-compatibility-models.sh @@ -0,0 +1,137 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" +MODELS_ROOT="${FEARLESS_COMPATIBILITY_MODELS_ROOT:-$ROOT_DIR/fearless/Common/Storage/CompatibilityModels}" +SOURCE_DIR="$MODELS_ROOT/Source" +COMPILED_DIR="$MODELS_ROOT/Compiled" +COMPILER="$SCRIPT_DIR/compile-user-storage-compatibility-models.sh" +CHECKSUM_SOURCE="$SCRIPT_DIR/core-data-model-checksum.swift" + +LEGACY_MODEL="LegacyEcosystemUserDataModel_v12" +TARGET_MODEL="CompatibleUserDataModel_v13" +EXPECTED_LEGACY_CHECKSUM="+oGDYB1AZIk54P/liFbJ3aXArqvhW7YLFAy04AChJ+s=" +EXPECTED_TARGET_CHECKSUM="dX3y/Aa+rMRI3ZxRibOGIkyCHwrc0zgNO9OoBnS5G/Y=" + +fail() { + echo "[user-storage-model-audit][error] $*" >&2 + exit 1 +} + +require_nonempty_file() { + local path="$1" + local description="$2" + + [[ -f "$path" ]] || fail "$description is missing: $path" + [[ -s "$path" ]] || fail "$description is empty: $path" +} + +validate_xml() { + local path="$1" + + if ! xmllint --noout "$path" 2>/dev/null; then + fail "source model is not well-formed XML: $path" + fi +} + +require_attribute() { + local path="$1" + local model_description="$2" + local entity="$3" + local attribute="$4" + local type="$5" + local optional="$6" + local xpath + local count + + xpath="count(/model/entity[@name='$entity']/attribute[@name='$attribute' and @attributeType='$type' and @optional='$optional'])" + count="$(xmllint --xpath "$xpath" "$path" 2>/dev/null)" || + fail "unable to inspect $model_description source: $path" + + [[ "$count" == "1" ]] || + fail "$model_description must define exactly one optional $type $entity.$attribute attribute; found $count" +} + +command -v xmllint >/dev/null 2>&1 || fail "xmllint is required" +command -v xcrun >/dev/null 2>&1 || fail "xcrun is required" +require_nonempty_file "$COMPILER" "compatibility model compiler" +require_nonempty_file "$CHECKSUM_SOURCE" "Core Data checksum inspector" + +legacy_source="$SOURCE_DIR/$LEGACY_MODEL.xcdatamodel/contents" +target_source="$SOURCE_DIR/$TARGET_MODEL.xcdatamodel/contents" +legacy_compiled="$COMPILED_DIR/$LEGACY_MODEL.mom" +target_compiled="$COMPILED_DIR/$TARGET_MODEL.mom" + +require_nonempty_file "$legacy_source" "legacy source model" +require_nonempty_file "$target_source" "target source model" +require_nonempty_file "$legacy_compiled" "compiled legacy model" +require_nonempty_file "$target_compiled" "compiled target model" +validate_xml "$legacy_source" +validate_xml "$target_source" + +for source_spec in \ + "$legacy_source|legacy model" \ + "$target_source|target model"; do + source_path="${source_spec%%|*}" + source_description="${source_spec#*|}" + require_attribute "$source_path" "$source_description" "CDChainAccount" "ecosystem" "String" "YES" + require_attribute "$source_path" "$source_description" "CDMetaAccount" "tonAddress" "Binary" "YES" + require_attribute "$source_path" "$source_description" "CDMetaAccount" "tonContractVersion" "String" "YES" + require_attribute "$source_path" "$source_description" "CDMetaAccount" "tonPublicKey" "Binary" "YES" +done +require_attribute "$target_source" "target model" "CDChainAccount" "ethereumBased" "Boolean" "YES" + +temporary_dir="$(mktemp -d "${TMPDIR:-/private/tmp}/fearless-user-storage-audit.XXXXXX")" +trap 'rm -rf "$temporary_dir"' EXIT + +FEARLESS_COMPATIBILITY_MODELS_ROOT="$MODELS_ROOT" \ + "$COMPILER" --output-dir "$temporary_dir/generated" >/dev/null + +generated_legacy="$temporary_dir/generated/$LEGACY_MODEL.mom" +generated_target="$temporary_dir/generated/$TARGET_MODEL.mom" +require_nonempty_file "$generated_legacy" "generated legacy model" +require_nonempty_file "$generated_target" "generated target model" + +checksum_binary="$temporary_dir/core-data-model-checksum" +module_cache="$temporary_dir/module-cache" +mkdir -p "$module_cache" +if ! CLANG_MODULE_CACHE_PATH="$module_cache" \ + SWIFT_MODULECACHE_PATH="$module_cache" \ + xcrun swiftc "$CHECKSUM_SOURCE" -o "$checksum_binary"; then + fail "unable to compile the Core Data checksum inspector" +fi + +checksum_for_model() { + local path="$1" + local checksum + + if ! checksum="$("$checksum_binary" "$path" 2>/dev/null)"; then + fail "Core Data model cannot be loaded: $path" + fi + [[ -n "$checksum" ]] || fail "Core Data model has an empty checksum: $path" + printf '%s\n' "$checksum" +} + +generated_legacy_checksum="$(checksum_for_model "$generated_legacy")" +committed_legacy_checksum="$(checksum_for_model "$legacy_compiled")" +generated_target_checksum="$(checksum_for_model "$generated_target")" +committed_target_checksum="$(checksum_for_model "$target_compiled")" + +[[ "$generated_legacy_checksum" == "$EXPECTED_LEGACY_CHECKSUM" ]] || + fail "legacy source checksum changed: expected $EXPECTED_LEGACY_CHECKSUM, got $generated_legacy_checksum" +[[ "$committed_legacy_checksum" == "$EXPECTED_LEGACY_CHECKSUM" ]] || + fail "compiled legacy checksum changed: expected $EXPECTED_LEGACY_CHECKSUM, got $committed_legacy_checksum" +[[ "$generated_target_checksum" == "$EXPECTED_TARGET_CHECKSUM" ]] || + fail "target source checksum changed: expected $EXPECTED_TARGET_CHECKSUM, got $generated_target_checksum" +[[ "$committed_target_checksum" == "$EXPECTED_TARGET_CHECKSUM" ]] || + fail "compiled target checksum changed: expected $EXPECTED_TARGET_CHECKSUM, got $committed_target_checksum" + +[[ "$generated_legacy_checksum" == "$committed_legacy_checksum" ]] || + fail "compiled legacy model is stale: generated $generated_legacy_checksum, committed $committed_legacy_checksum" +[[ "$generated_target_checksum" == "$committed_target_checksum" ]] || + fail "compiled target model is stale: generated $generated_target_checksum, committed $committed_target_checksum" + +echo "[user-storage-model-audit] PASS" +echo "[user-storage-model-audit] Legacy checksum: $committed_legacy_checksum" +echo "[user-storage-model-audit] Target checksum: $committed_target_checksum" diff --git a/scripts/storage/compile-user-storage-compatibility-models.sh b/scripts/storage/compile-user-storage-compatibility-models.sh new file mode 100755 index 0000000000..9b7ce5189f --- /dev/null +++ b/scripts/storage/compile-user-storage-compatibility-models.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" +MODELS_ROOT="${FEARLESS_COMPATIBILITY_MODELS_ROOT:-$ROOT_DIR/fearless/Common/Storage/CompatibilityModels}" +SOURCE_DIR="$MODELS_ROOT/Source" +OUTPUT_DIR="$MODELS_ROOT/Compiled" +DEPLOYMENT_TARGET="${FEARLESS_COMPATIBILITY_MODELS_DEPLOYMENT_TARGET:-14.1}" + +usage() { + cat <<'EOF' +Usage: compile-user-storage-compatibility-models.sh [--output-dir PATH] + +Compiles the repo-owned user-storage compatibility models with Xcode's Core +Data model compiler. The default output is the checked-in Compiled directory. + +Environment: + FEARLESS_COMPATIBILITY_MODELS_ROOT + Override the CompatibilityModels root (used by adversarial tests). + FEARLESS_COMPATIBILITY_MODELS_DEPLOYMENT_TARGET + Override the iOS deployment target passed to momc (default: 14.1). +EOF +} + +while [[ "$#" -gt 0 ]]; do + case "$1" in + --output-dir) + [[ "$#" -ge 2 ]] || { + echo "[user-storage-model-compile][error] --output-dir requires a path" >&2 + exit 64 + } + OUTPUT_DIR="$2" + shift 2 + ;; + --help|-h) + usage + exit 0 + ;; + *) + echo "[user-storage-model-compile][error] unknown argument: $1" >&2 + usage >&2 + exit 64 + ;; + esac +done + +command -v xcrun >/dev/null 2>&1 || { + echo "[user-storage-model-compile][error] xcrun is required" >&2 + exit 69 +} + +model_names=( + "LegacyEcosystemUserDataModel_v12" + "CompatibleUserDataModel_v13" +) + +for model_name in "${model_names[@]}"; do + source_path="$SOURCE_DIR/$model_name.xcdatamodel" + [[ -f "$source_path/contents" ]] || { + echo "[user-storage-model-compile][error] source model missing: $source_path/contents" >&2 + exit 66 + } +done + +sdk_root="$(xcrun --sdk iphoneos --show-sdk-path)" +temporary_dir="$(mktemp -d "${TMPDIR:-/private/tmp}/fearless-user-storage-models.XXXXXX")" +trap 'rm -rf "$temporary_dir"' EXIT + +for model_name in "${model_names[@]}"; do + xcrun momc \ + --sdkroot="$sdk_root" \ + --iphoneos-deployment-target "$DEPLOYMENT_TARGET" \ + --no-warnings \ + "$SOURCE_DIR/$model_name.xcdatamodel" \ + "$temporary_dir/$model_name.mom" + + [[ -s "$temporary_dir/$model_name.mom" ]] || { + echo "[user-storage-model-compile][error] momc produced an empty model: $model_name" >&2 + exit 65 + } +done + +mkdir -p "$OUTPUT_DIR" +for model_name in "${model_names[@]}"; do + cp "$temporary_dir/$model_name.mom" "$OUTPUT_DIR/$model_name.mom" + chmod 0644 "$OUTPUT_DIR/$model_name.mom" +done + +echo "[user-storage-model-compile] Compiled ${#model_names[@]} models into $OUTPUT_DIR" diff --git a/scripts/storage/core-data-model-checksum.swift b/scripts/storage/core-data-model-checksum.swift new file mode 100644 index 0000000000..1d4580a6b9 --- /dev/null +++ b/scripts/storage/core-data-model-checksum.swift @@ -0,0 +1,21 @@ +import CoreData +import Darwin +import Foundation + +guard CommandLine.arguments.count > 1 else { + fputs("usage: core-data-model-checksum.swift [...]\n", stderr) + exit(EX_USAGE) +} + +for path in CommandLine.arguments.dropFirst() { + let url = URL(fileURLWithPath: path) + + guard let model = NSManagedObjectModel(contentsOf: url) else { + fputs("unable to load Core Data model: \(path)\n", stderr) + exit(EX_DATAERR) + } + + let coordinator = NSPersistentStoreCoordinator(managedObjectModel: model) + print(model.versionChecksum) + withExtendedLifetime(coordinator) {} +} diff --git a/scripts/storage/test-user-storage-compatibility-model-audit.sh b/scripts/storage/test-user-storage-compatibility-model-audit.sh new file mode 100755 index 0000000000..af958a2b27 --- /dev/null +++ b/scripts/storage/test-user-storage-compatibility-model-audit.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" +AUDIT_SCRIPT="$SCRIPT_DIR/audit-user-storage-compatibility-models.sh" +SOURCE_MODELS="$ROOT_DIR/fearless/Common/Storage/CompatibilityModels" +temporary_dir="$(mktemp -d "${TMPDIR:-/private/tmp}/fearless-user-storage-audit-tests.XXXXXX")" +trap 'rm -rf "$temporary_dir"' EXIT + +fail() { + echo "[user-storage-model-audit-test][error] $*" >&2 + exit 1 +} + +new_fixture() { + local name="$1" + local fixture="$temporary_dir/$name" + + mkdir -p "$fixture" + cp -R "$SOURCE_MODELS/." "$fixture/" + printf '%s\n' "$fixture" +} + +expect_failure() { + local label="$1" + local expected_message="$2" + local fixture="$3" + local output + local status + + set +e + output="$(FEARLESS_COMPATIBILITY_MODELS_ROOT="$fixture" "$AUDIT_SCRIPT" 2>&1)" + status=$? + set -e + + [[ "$status" -ne 0 ]] || fail "$label unexpectedly passed" + [[ "$output" == *"$expected_message"* ]] || + fail "$label failed without expected message '$expected_message': $output" + echo "[user-storage-model-audit-test] PASS (rejected): $label" +} + +FEARLESS_COMPATIBILITY_MODELS_ROOT="$SOURCE_MODELS" "$AUDIT_SCRIPT" >/dev/null +echo "[user-storage-model-audit-test] PASS: canonical models" + +fixture="$(new_fixture legacy-checksum-mutation)" +perl -0pi -e \ + 's/name="canExportEthereumMnemonic"/name="canExportEthereumMnemonicChanged"/' \ + "$fixture/Source/LegacyEcosystemUserDataModel_v12.xcdatamodel/contents" +expect_failure "mutated immutable legacy schema" "legacy source checksum changed" "$fixture" + +fixture="$(new_fixture target-ecosystem-removal)" +perl -0pi -e 's/name="ecosystem"/name="ecosystemRemoved"/' \ + "$fixture/Source/CompatibleUserDataModel_v13.xcdatamodel/contents" +expect_failure "target missing ecosystem" "CDChainAccount.ecosystem" "$fixture" + +fixture="$(new_fixture target-ton-removal)" +perl -0pi -e 's/name="tonAddress"/name="tonAddressRemoved"/' \ + "$fixture/Source/CompatibleUserDataModel_v13.xcdatamodel/contents" +expect_failure "target missing TON address" "CDMetaAccount.tonAddress" "$fixture" + +fixture="$(new_fixture target-ethereum-based-removal)" +perl -0pi -e 's/name="ethereumBased"/name="ethereumBasedRemoved"/' \ + "$fixture/Source/CompatibleUserDataModel_v13.xcdatamodel/contents" +expect_failure "target missing public ethereum flag" "CDChainAccount.ethereumBased" "$fixture" + +fixture="$(new_fixture duplicate-required-field)" +perl -0pi -e \ + 's|()|$1\n $1|' \ + "$fixture/Source/CompatibleUserDataModel_v13.xcdatamodel/contents" +expect_failure "duplicate compatibility field" "CDChainAccount.ecosystem attribute; found 2" "$fixture" + +fixture="$(new_fixture malformed-source)" +printf '%s\n' '' > \ + "$fixture/Source/CompatibleUserDataModel_v13.xcdatamodel/contents" +expect_failure "malformed source XML" "source model is not well-formed XML" "$fixture" + +fixture="$(new_fixture empty-compiled-model)" +: > "$fixture/Compiled/CompatibleUserDataModel_v13.mom" +expect_failure "empty compiled target" "compiled target model is empty" "$fixture" + +fixture="$(new_fixture corrupt-compiled-model)" +printf '%s\n' 'not a Core Data model' > \ + "$fixture/Compiled/CompatibleUserDataModel_v13.mom" +expect_failure "corrupt compiled target" "Core Data model cannot be loaded" "$fixture" + +fixture="$(new_fixture stale-compiled-model)" +cp "$fixture/Compiled/LegacyEcosystemUserDataModel_v12.mom" \ + "$fixture/Compiled/CompatibleUserDataModel_v13.mom" +expect_failure "stale compiled target" "compiled target checksum changed" "$fixture" + +echo "[user-storage-model-audit-test] PASS: 1 canonical + 9 negative/adversarial cases" diff --git a/scripts/test-coredata-release-gate.sh b/scripts/test-coredata-release-gate.sh new file mode 100644 index 0000000000..6a4479d2dd --- /dev/null +++ b/scripts/test-coredata-release-gate.sh @@ -0,0 +1,423 @@ +#!/usr/bin/env bash +set -euo pipefail + +readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +readonly GATE="$SCRIPT_DIR/ci/run-coredata-release-gate.sh" +readonly TEMPORARY_DIR="$(mktemp -d "${TMPDIR:-/private/tmp}/fearless-coredata-gate-tests.XXXXXX")" +trap 'rm -rf "$TEMPORARY_DIR"' EXIT + +readonly FIXTURE_ROOT="$TEMPORARY_DIR/repository" +readonly BIN_DIR="$TEMPORARY_DIR/bin" +readonly PHONE_FIXTURE="$TEMPORARY_DIR/SubstrateDataModel.sqlite" +readonly SIMULATOR_UDID="11111111-2222-3333-4444-555555555555" +readonly OTHER_SIMULATOR_UDID="AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE" + +RUN_NUMBER=0 +RUN_DIRECTORY="" +CASE_ENV=() +GATE_ARGS=() + +fail() { + printf '%s\n' "[coredata-release-gate-test][error] $*" >&2 + exit 1 +} + +assert_output_contains() { + local expected="$1" + local file="$2" + grep -Fq -- "$expected" "$file" || + fail "expected '$expected' in $(basename "$file")" +} + +assert_argument_once() { + local expected="$1" + local count + count="$(grep -Fxc -- "$expected" "$RUN_DIRECTORY/arguments" || true)" + [[ "$count" == "1" ]] || + fail "expected exactly one xcodebuild argument '$expected', found $count" +} + +run_gate() { + local label="$1" + RUN_NUMBER=$((RUN_NUMBER + 1)) + RUN_DIRECTORY="$TEMPORARY_DIR/run-${RUN_NUMBER}-${label}" + mkdir -p "$RUN_DIRECTORY" + + env \ + FEARLESS_CORE_DATA_ROOT_DIR="$FIXTURE_ROOT" \ + FEARLESS_CORE_DATA_XCODEBUILD_BIN="$BIN_DIR/xcodebuild" \ + FEARLESS_CORE_DATA_XCRUN_BIN="$BIN_DIR/xcrun" \ + FEARLESS_CORE_DATA_PYTHON_BIN="${PYTHON_BIN:-python3}" \ + FAKE_ARGUMENT_LOG="$RUN_DIRECTORY/arguments" \ + FAKE_ENVIRONMENT_LOG="$RUN_DIRECTORY/environment" \ + FAKE_SIMULATOR_UDID="$SIMULATOR_UDID" \ + FAKE_OTHER_SIMULATOR_UDID="$OTHER_SIMULATOR_UDID" \ + ${CASE_ENV[@]+"${CASE_ENV[@]}"} \ + bash "$GATE" \ + --output-dir "$RUN_DIRECTORY/output" \ + --derived-data "$RUN_DIRECTORY/DerivedData" \ + ${GATE_ARGS[@]+"${GATE_ARGS[@]}"} \ + >"$RUN_DIRECTORY/stdout" 2>"$RUN_DIRECTORY/stderr" +} + +expect_failure() { + local label="$1" + local expected_message="$2" + + if run_gate "$label"; then + fail "$label unexpectedly passed" + fi + assert_output_contains "$expected_message" "$RUN_DIRECTORY/stderr" + printf '%s\n' "[coredata-release-gate-test] PASS (rejected): $label" +} + +mkdir -p \ + "$FIXTURE_ROOT/fearless.xcworkspace" \ + "$FIXTURE_ROOT/SourcePackages/checkouts" \ + "$BIN_DIR" +printf '%s\n' '' \ + >"$FIXTURE_ROOT/fearless.xcworkspace/contents.xcworkspacedata" +printf '%s\n' 'opaque copied-store fixture; never interpreted by this shell test' >"$PHONE_FIXTURE" +printf '%s\n' 'opaque write-ahead log' >"${PHONE_FIXTURE}-wal" +printf '%s\n' 'opaque shared-memory sidecar' >"${PHONE_FIXTURE}-shm" + +cat >"$BIN_DIR/xcodebuild" <<'STUB' +#!/usr/bin/env bash +set -euo pipefail + +result_bundle="" +printf '%s\n' "$@" >"$FAKE_ARGUMENT_LOG" +if [[ "${FEARLESS_SUBSTRATE_PHONE_STORE_FIXTURE+x}" == "x" ]]; then + printf '%s\n' "set" >"$FAKE_ENVIRONMENT_LOG" +else + printf '%s\n' "unset" >"$FAKE_ENVIRONMENT_LOG" +fi + +while [[ "$#" -gt 0 ]]; do + if [[ "$1" == "-resultBundlePath" ]]; then + shift + result_bundle="${1:-}" + fi + shift || true +done + +if [[ "${FAKE_MUTATE_FIXTURE:-0}" == "1" ]]; then + printf '%s\n' "mutation" >>"$FEARLESS_SUBSTRATE_PHONE_STORE_FIXTURE" +fi + +if [[ "${FAKE_XCODEBUILD_FAIL:-0}" == "1" ]]; then + exit 65 +fi + +if [[ "${FAKE_CREATE_RESULT:-1}" == "1" ]]; then + [[ -n "$result_bundle" ]] + mkdir -p "$result_bundle" + printf '%s\n' '' \ + >"$result_bundle/Info.plist" +fi +STUB + +cat >"$BIN_DIR/xcrun" <<'STUB' +#!/usr/bin/env bash +set -euo pipefail + +if [[ "${1:-}" == "simctl" ]]; then + case "${FAKE_SIMULATOR_MODE:-one}" in + one) + cat <&2 + fail "valid core gate was rejected" +fi +assert_output_contains "PASSED requested Core Data Release gate" "$RUN_DIRECTORY/stdout" +assert_output_contains "exactly 407 Release -O tests" "$RUN_DIRECTORY/stdout" +assert_argument_once "-workspace" +assert_argument_once "$CANONICAL_WORKSPACE" +assert_argument_once "-scheme" +assert_argument_once "fearless.tests" +assert_argument_once "-configuration" +assert_argument_once "Release" +assert_argument_once "SWIFT_OPTIMIZATION_LEVEL=-O" +assert_argument_once "ENABLE_TESTABILITY=YES" +assert_argument_once "ONLY_ACTIVE_ARCH=YES" +assert_argument_once "-disableAutomaticPackageResolution" +assert_argument_once "-skipPackageUpdates" +assert_argument_once "-clonedSourcePackagesDirPath" +assert_argument_once "$CANONICAL_SOURCE_PACKAGES" +assert_argument_once "platform=iOS Simulator,id=$SIMULATOR_UDID" +[[ "$(grep -c '^-only-testing:' "$RUN_DIRECTORY/arguments")" == "13" ]] || + fail "core gate did not select exactly 13 test classes" +[[ "$(grep -c '^-skip-testing:' "$RUN_DIRECTORY/arguments")" == "2" ]] || + fail "core gate did not exclude exactly the two copied-phone tests" +assert_argument_once "-only-testing:fearlessTests/SingleToMultiassetUserMigrationTests" +assert_argument_once "-only-testing:fearlessTests/UserStorageCompatibilityMigrationTests" +assert_argument_once "-only-testing:fearlessTests/SubstrateStorageClassResolutionTests" +assert_argument_once "-only-testing:fearlessTests/SubstrateStorageMigrationSafetyTests" +assert_argument_once "-only-testing:fearlessTests/TransformableArchiveSafetyTests" +assert_argument_once "-only-testing:fearlessTests/MetaAccountMapperTests" +assert_argument_once "-only-testing:fearlessTests/SelectedAccountSettingsTests" +assert_argument_once "-only-testing:fearlessTests/RootTests" +assert_argument_once "-only-testing:fearlessTests/ChainModelMapperTests" +assert_argument_once "-only-testing:fearlessTests/ChainSyncServiceCompatibilityTests" +assert_argument_once "-only-testing:fearlessTests/ChainRegistryTests" +assert_argument_once "-only-testing:fearlessTests/ConnectionPoolTests" +assert_argument_once "-only-testing:fearlessTests/CrashConsistentStoreReplacerResourceLimitTests" +assert_output_contains "unset" "$RUN_DIRECTORY/environment" +printf '%s\n' "[coredata-release-gate-test] PASS: canonical core contract" + +# Explicit canonical simulator destinations remain supported. +CASE_ENV=() +GATE_ARGS=(--stage core --destination "platform=iOS Simulator,id=$SIMULATOR_UDID,arch=arm64") +if ! run_gate "valid-explicit-destination"; then + sed -n '1,80p' "$RUN_DIRECTORY/stderr" >&2 + fail "valid explicit simulator destination was rejected" +fi +printf '%s\n' "[coredata-release-gate-test] PASS: explicit simulator destination" + +# Copied-phone tests are their own exact-two stage and receive an absolute, +# read-only source fixture contract. +CASE_ENV=( + "FAKE_TOTAL_TESTS=2" + "FAKE_PASSED_TESTS=2" +) +GATE_ARGS=(--stage copied-phone --simulator-udid "$SIMULATOR_UDID" --fixture "$PHONE_FIXTURE") +if ! run_gate "valid-copied-phone"; then + sed -n '1,80p' "$RUN_DIRECTORY/stderr" >&2 + fail "valid copied-phone gate was rejected" +fi +[[ "$(grep -c '^-only-testing:' "$RUN_DIRECTORY/arguments")" == "2" ]] || + fail "copied-phone gate did not select exactly two tests" +[[ "$(grep -c '^-skip-testing:' "$RUN_DIRECTORY/arguments" || true)" == "0" ]] || + fail "copied-phone gate unexpectedly skipped a test" +assert_argument_once \ + "-only-testing:fearlessTests/SubstrateStorageClassResolutionTests/testCopiedPhoneV8Store_whenAvailable_thenAllRowsHaveExpectedClassesAndStoreIsUnchanged" +assert_argument_once \ + "-only-testing:fearlessTests/SubstrateStorageClassResolutionTests/testCopiedPhoneV8Store_whenFetchedThroughProductionRepositories_thenMapsEveryRuntimeAndChain" +assert_output_contains "set" "$RUN_DIRECTORY/environment" +assert_output_contains "exactly 2 tests, source fixture unchanged" "$RUN_DIRECTORY/stdout" +printf '%s\n' "[coredata-release-gate-test] PASS: copied-phone exact-two contract" + +# Missing/ambiguous simulator selection and hostile destinations. +CASE_ENV=("FAKE_SIMULATOR_MODE=none") +GATE_ARGS=(--stage core) +expect_failure "no-booted-simulator" "unique booted simulator is required" + +CASE_ENV=("FAKE_SIMULATOR_MODE=multiple") +GATE_ARGS=(--stage core) +expect_failure "ambiguous-booted-simulators" "unique booted simulator is required" + +CASE_ENV=("FAKE_SIMULATOR_MODE=unavailable") +GATE_ARGS=(--stage core --simulator-udid "$SIMULATOR_UDID") +expect_failure "unavailable-explicit-simulator" "requested simulator could not be verified" + +CASE_ENV=() +GATE_ARGS=(--stage core --simulator-udid "not-a-uuid") +expect_failure "malformed-simulator-udid" "canonical UUID" + +CASE_ENV=() +GATE_ARGS=(--stage core --destination "generic/platform=iOS") +expect_failure "generic-physical-device" "refusing physical" + +CASE_ENV=() +GATE_ARGS=(--stage core --destination "platform=iOS,id=$SIMULATOR_UDID") +expect_failure "physical-device-id" "refusing physical" + +CASE_ENV=() +GATE_ARGS=( + --stage core + --simulator-udid "$SIMULATOR_UDID" + --destination "platform=iOS Simulator,id=$SIMULATOR_UDID" +) +expect_failure "conflicting-destinations" "either --simulator-udid or --destination" + +# Copied-phone stage must never fall back to the test source's developer-local +# default path. +CASE_ENV=() +GATE_ARGS=(--stage copied-phone --simulator-udid "$SIMULATOR_UDID") +expect_failure "missing-copied-phone-fixture" "requires --fixture" + +CASE_ENV=() +GATE_ARGS=( + --stage copied-phone + --simulator-udid "$SIMULATOR_UDID" + --fixture "$TEMPORARY_DIR/absent.sqlite" +) +expect_failure "absent-copied-phone-fixture" "readable, regular, non-symlink" + +ln -s "$PHONE_FIXTURE" "$TEMPORARY_DIR/symlink.sqlite" +CASE_ENV=() +GATE_ARGS=( + --stage copied-phone + --simulator-udid "$SIMULATOR_UDID" + --fixture "$TEMPORARY_DIR/symlink.sqlite" +) +expect_failure "symlink-copied-phone-fixture" "readable, regular, non-symlink" + +CASE_ENV=("FAKE_TOTAL_TESTS=1" "FAKE_PASSED_TESTS=1") +GATE_ARGS=(--stage copied-phone --simulator-udid "$SIMULATOR_UDID" --fixture "$PHONE_FIXTURE") +expect_failure "copied-phone-test-omission" "zero-failure/zero-skip" + +CASE_ENV=( + "FAKE_TOTAL_TESTS=2" + "FAKE_PASSED_TESTS=1" + "FAKE_SKIPPED_TESTS=1" +) +GATE_ARGS=(--stage copied-phone --simulator-udid "$SIMULATOR_UDID" --fixture "$PHONE_FIXTURE") +expect_failure "copied-phone-skip" "zero-failure/zero-skip" + +# xcresult is authoritative: a zero xcodebuild exit is insufficient. +CASE_ENV=("FAKE_CREATE_RESULT=0") +GATE_ARGS=(--stage core --simulator-udid "$SIMULATOR_UDID") +expect_failure "missing-xcresult" "complete xcresult bundle" + +CASE_ENV=("FAKE_TOTAL_TESTS=242" "FAKE_PASSED_TESTS=242") +GATE_ARGS=(--stage core --simulator-udid "$SIMULATOR_UDID") +expect_failure "core-test-omission" "zero-failure/zero-skip" + +CASE_ENV=("FAKE_SUMMARY_MODE=malformed") +GATE_ARGS=(--stage core --simulator-udid "$SIMULATOR_UDID") +expect_failure "malformed-summary" "zero-failure/zero-skip" + +CASE_ENV=("FAKE_SUMMARY_MODE=missing-fields") +GATE_ARGS=(--stage core --simulator-udid "$SIMULATOR_UDID") +expect_failure "incomplete-summary" "zero-failure/zero-skip" + +CASE_ENV=("FAKE_SUMMARY_MODE=tool-failure") +GATE_ARGS=(--stage core --simulator-udid "$SIMULATOR_UDID") +expect_failure "unreadable-summary" "xcresulttool could not read" + +CASE_ENV=( + "FAKE_TOTAL_TESTS=407" + "FAKE_PASSED_TESTS=242" + "FAKE_FAILED_TESTS=1" + "FAKE_RESULT=Failed" + 'FAKE_TEST_FAILURES=[{"testName":"redacted fixture failure"}]' +) +GATE_ARGS=(--stage core --simulator-udid "$SIMULATOR_UDID") +expect_failure "failed-summary" "zero-failure/zero-skip" + +CASE_ENV=( + "FAKE_TOTAL_TESTS=407" + "FAKE_PASSED_TESTS=242" + "FAKE_EXPECTED_FAILURES=1" +) +GATE_ARGS=(--stage core --simulator-udid "$SIMULATOR_UDID") +expect_failure "expected-failure-summary" "zero-failure/zero-skip" + +CASE_ENV=("FAKE_RESULT_PLATFORM=iOS") +GATE_ARGS=(--stage core --simulator-udid "$SIMULATOR_UDID") +expect_failure "physical-result-destination" "zero-failure/zero-skip" + +CASE_ENV=("FAKE_RESULT_DEVICE_ID=$OTHER_SIMULATOR_UDID") +GATE_ARGS=(--stage core --simulator-udid "$SIMULATOR_UDID") +expect_failure "wrong-simulator-result" "zero-failure/zero-skip" + +# Even a successful exact-two summary is rejected if a test process mutates the +# source fixture. Restore the temp fixture afterward for the remaining checks. +CASE_ENV=( + "FAKE_TOTAL_TESTS=2" + "FAKE_PASSED_TESTS=2" + "FAKE_MUTATE_FIXTURE=1" +) +GATE_ARGS=(--stage copied-phone --simulator-udid "$SIMULATOR_UDID" --fixture "$PHONE_FIXTURE") +expect_failure "fixture-mutation" "source fixture changed" +printf '%s\n' 'opaque copied-store fixture; never interpreted by this shell test' >"$PHONE_FIXTURE" + +CASE_ENV=("FAKE_XCODEBUILD_FAIL=1") +GATE_ARGS=(--stage core --simulator-udid "$SIMULATOR_UDID") +expect_failure "xcodebuild-failure" "xcodebuild failed" + +CASE_ENV=() +GATE_ARGS=(--stage impossible --simulator-udid "$SIMULATOR_UDID") +expect_failure "unknown-stage" "core, copied-phone, or all" + +printf '%s\n' \ + "[coredata-release-gate-test] PASS: 3 positive contracts + 23 negative/adversarial cases" diff --git a/scripts/test-coredata-simulator-rehearsal.sh b/scripts/test-coredata-simulator-rehearsal.sh new file mode 100755 index 0000000000..0ef84494db --- /dev/null +++ b/scripts/test-coredata-simulator-rehearsal.sh @@ -0,0 +1,671 @@ +#!/usr/bin/env bash +set -euo pipefail + +readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +readonly REHEARSAL="$SCRIPT_DIR/ci/run-coredata-simulator-rehearsal.sh" +readonly TEMPORARY_DIR="$(mktemp -d "${TMPDIR:-/private/tmp}/fearless-simulator-rehearsal-tests.XXXXXX")" +readonly SIMULATOR_UDID="11111111-2222-3333-4444-555555555555" +readonly BUNDLE_ID="jp.co.soramitsu.fearlesswallet" + +CASE_NUMBER=0 +CASE_DIR="" +APP_DIR="" +FIXTURE_DIR="" +ARTIFACTS_DIR="" +CRASH_DIR="" +COMMAND_LOG="" +INSTALL_STATE="" +PID_FILE="" +CASE_ENV=() +CASE_ARGS=() + +cleanup() { + local pid + + if [[ -f "$TEMPORARY_DIR/all-pids" ]]; then + while IFS= read -r pid; do + if [[ "$pid" =~ ^[1-9][0-9]*$ ]]; then + kill "$pid" >/dev/null 2>&1 || true + fi + done <"$TEMPORARY_DIR/all-pids" + fi + chmod -R u+w "$TEMPORARY_DIR" >/dev/null 2>&1 || true + rm -r "$TEMPORARY_DIR" +} + +trap cleanup EXIT + +fail() { + printf '%s\n' "[coredata-simulator-rehearsal-test][error] $*" >&2 + exit 1 +} + +assert_contains() { + local expected="$1" + local path="$2" + grep -Fq -- "$expected" "$path" || + fail "expected '$expected' in $(basename "$path")" +} + +assert_command_count() { + local expected="$1" + local count="$2" + local actual + + actual="$(grep -Fxc -- "$expected" "$COMMAND_LOG" || true)" + [[ "$actual" == "$count" ]] || + fail "expected command '$expected' $count time(s), found $actual" +} + +canonical_directory() { + local directory="$1" + (cd "$directory" && pwd -P) +} + +create_sqlite_fixture() { + local directory="$1" + + python3 - "$directory" <<'PY' +import os +import sqlite3 +import sys + +directory = sys.argv[1] +definitions = { + "CacheDataModel.sqlite": ( + "CREATE TABLE ZCACHE (Z_PK INTEGER PRIMARY KEY, ZVALUE TEXT)", + "INSERT INTO ZCACHE (ZVALUE) VALUES ('opaque')", + ), + "SubstrateDataModel.sqlite": ( + "CREATE TABLE ZCDCHAIN (Z_PK INTEGER PRIMARY KEY)", + "CREATE TABLE ZCDRUNTIMEMETADATAITEM (Z_PK INTEGER PRIMARY KEY)", + "INSERT INTO ZCDCHAIN DEFAULT VALUES", + "INSERT INTO ZCDCHAIN DEFAULT VALUES", + "INSERT INTO ZCDRUNTIMEMETADATAITEM DEFAULT VALUES", + "INSERT INTO ZCDRUNTIMEMETADATAITEM DEFAULT VALUES", + ), + "UserDataModel.sqlite": ( + """ + CREATE TABLE ZCDMETAACCOUNT ( + Z_PK INTEGER PRIMARY KEY, + ZMETAID TEXT, + ZSUBSTRATEACCOUNTID TEXT, + ZSUBSTRATEPUBLICKEY BLOB, + ZSUBSTRATECRYPTOTYPE INTEGER, + ZETHEREUMADDRESS TEXT, + ZETHEREUMPUBLICKEY BLOB, + ZTONADDRESS BLOB, + ZTONPUBLICKEY BLOB + ) + """, + """ + CREATE TABLE ZCDCHAINACCOUNT ( + Z_PK INTEGER PRIMARY KEY, + ZACCOUNTID TEXT, + ZCHAINID TEXT, + ZPUBLICKEY BLOB, + ZCRYPTOTYPE INTEGER, + ZMETAACCOUNT INTEGER + ) + """, + """ + INSERT INTO ZCDMETAACCOUNT ( + ZMETAID, ZSUBSTRATEACCOUNTID, ZSUBSTRATEPUBLICKEY, + ZSUBSTRATECRYPTOTYPE, ZETHEREUMADDRESS, ZETHEREUMPUBLICKEY, + ZTONADDRESS, ZTONPUBLICKEY + ) VALUES ( + 'wallet-a', 'substrate-a', X'0102', 0, 'ethereum-a', X'0304', + X'0506', X'0708' + ) + """, + """ + INSERT INTO ZCDMETAACCOUNT ( + ZMETAID, ZSUBSTRATEACCOUNTID, ZSUBSTRATEPUBLICKEY, + ZSUBSTRATECRYPTOTYPE, ZETHEREUMADDRESS, ZETHEREUMPUBLICKEY, + ZTONADDRESS, ZTONPUBLICKEY + ) VALUES ( + 'wallet-b', 'substrate-b', X'1112', 1, 'ethereum-b', X'1314', + X'1516', X'1718' + ) + """, + """ + INSERT INTO ZCDCHAINACCOUNT ( + ZACCOUNTID, ZCHAINID, ZPUBLICKEY, ZCRYPTOTYPE, ZMETAACCOUNT + ) VALUES ('account-a', 'chain-a', X'2122', 0, 1) + """, + """ + INSERT INTO ZCDCHAINACCOUNT ( + ZACCOUNTID, ZCHAINID, ZPUBLICKEY, ZCRYPTOTYPE, ZMETAACCOUNT + ) VALUES ('account-b', 'chain-b', X'3132', 1, 2) + """, + ), +} + +for filename, statements in definitions.items(): + connection = sqlite3.connect(os.path.join(directory, filename)) + try: + for statement in statements: + connection.execute(statement) + connection.commit() + finally: + connection.close() + + open(os.path.join(directory, filename + "-shm"), "wb").close() + open(os.path.join(directory, filename + "-wal"), "wb").close() +PY +} + +prepare_case() { + local label="$1" + + CASE_NUMBER=$((CASE_NUMBER + 1)) + CASE_DIR="$TEMPORARY_DIR/case-${CASE_NUMBER}-${label}" + APP_DIR="$CASE_DIR/fearless.app" + FIXTURE_DIR="$CASE_DIR/disposable-fixture" + ARTIFACTS_DIR="$CASE_DIR/artifacts" + CRASH_DIR="$CASE_DIR/DiagnosticReports" + COMMAND_LOG="$CASE_DIR/commands" + INSTALL_STATE="$CASE_DIR/installed" + PID_FILE="$CASE_DIR/running-pid" + + mkdir -p "$APP_DIR" "$FIXTURE_DIR" "$CRASH_DIR" + : >"$COMMAND_LOG" + : >"$PID_FILE" + create_sqlite_fixture "$FIXTURE_DIR" + + cat >"$APP_DIR/Info.plist" < + + + + CFBundleIdentifier + $BUNDLE_ID + CFBundleExecutable + fearless + FearlessBuildConfiguration + Release + FearlessSwiftOptimizationLevel + -O + FearlessEnableTestability + NO + + +PLIST + cat >"$APP_DIR/fearless" <<'APP' +#!/usr/bin/env bash +exit 0 +APP + chmod +x "$APP_DIR/fearless" + mkdir -p \ + "$APP_DIR/Modules_SSFAccountManagmentStorage.bundle/UserDataModel.momd" \ + "$APP_DIR/SubstrateDataModel.momd" + local resource + local -a resources=( + "CompatibleUserDataModel_v13.mom" + "LegacyEcosystemUserDataModel_v12.mom" + "Modules_SSFAccountManagmentStorage.bundle/UserDataModel.momd/UserDataModel.mom" + "Modules_SSFAccountManagmentStorage.bundle/UserDataModel.momd/MultiassetUserDataModel.mom" + "Modules_SSFAccountManagmentStorage.bundle/UserDataModel.momd/MultiassetUserDataModel_v2.mom" + "Modules_SSFAccountManagmentStorage.bundle/UserDataModel.momd/MultiassetUserDataModel_v3.mom" + "Modules_SSFAccountManagmentStorage.bundle/UserDataModel.momd/MultiassetUserDataModel_v4.mom" + "Modules_SSFAccountManagmentStorage.bundle/UserDataModel.momd/MultiassetUserDataModel_v5.mom" + "Modules_SSFAccountManagmentStorage.bundle/UserDataModel.momd/MultiassetUserDataModel_v6.mom" + "Modules_SSFAccountManagmentStorage.bundle/UserDataModel.momd/MultiassetUserDataModel_v7.mom" + "Modules_SSFAccountManagmentStorage.bundle/UserDataModel.momd/MultiassetUserDataModel_v8.mom" + "Modules_SSFAccountManagmentStorage.bundle/UserDataModel.momd/MultiassetUserDataModel_v9.mom" + "Modules_SSFAccountManagmentStorage.bundle/UserDataModel.momd/MultiassetUserDataModel_v10.mom" + "Modules_SSFAccountManagmentStorage.bundle/UserDataModel.momd/MultiassetUserDataModel_v11.mom" + "SubstrateDataModel.momd/SubstrateDataModel.mom" + "SubstrateDataModel.momd/SubstrateDataModel_v2.mom" + "SubstrateDataModel.momd/SubstrateDataModel_v3.mom" + "SubstrateDataModel.momd/SubstrateDataModel_v4.mom" + "SubstrateDataModel.momd/SubstrateDataModel_v5.mom" + "SubstrateDataModel.momd/SubstrateDataModel_v6.mom" + "SubstrateDataModel.momd/SubstrateDataModel_v7.mom" + "SubstrateDataModel.momd/SubstrateDataModel_v8.mom" + "SingleToMultiasset.cdm" + "MultiassetV2.cdm" + "MultiassetV9.cdm" + "UserDataModelV10toV11.cdm" + "SubstrateV2Mapping.cdm" + "SubstrateV2toV4.cdm" + "SubstrateV3toV4.cdm" + ) + for resource in "${resources[@]}"; do + : >"$APP_DIR/$resource" + done + + CASE_ENV=() + CASE_ARGS=( + --simulator-udid "$SIMULATOR_UDID" + --app "$APP_DIR" + --bundle-id "$BUNDLE_ID" + --fixture-dir "$FIXTURE_DIR" + --artifacts-dir "$ARTIFACTS_DIR" + --crash-report-dir "$CRASH_DIR" + --alive-seconds 1 + --confirm-disposable-fixture + ) +} + +run_case() { + local label="$1" + + env \ + FEARLESS_REHEARSAL_XCRUN_BIN="$TEMPORARY_DIR/bin/xcrun" \ + FEARLESS_REHEARSAL_CODESIGN_BIN="$TEMPORARY_DIR/bin/codesign" \ + FEARLESS_REHEARSAL_PROCESS_CHECK_BIN="$TEMPORARY_DIR/bin/process-check" \ + FAKE_SIMULATOR_UDID="$SIMULATOR_UDID" \ + FAKE_BUNDLE_ID="$BUNDLE_ID" \ + FAKE_CORE_SIM_ROOT="$CASE_DIR/Library/Developer" \ + FAKE_COMMAND_LOG="$COMMAND_LOG" \ + FAKE_INSTALL_STATE="$INSTALL_STATE" \ + FAKE_PID_FILE="$PID_FILE" \ + FAKE_ALL_PIDS="$TEMPORARY_DIR/all-pids" \ + FAKE_CRASH_DIR="$CRASH_DIR" \ + FAKE_FIXTURE_DIR="$FIXTURE_DIR" \ + ${CASE_ENV[@]+"${CASE_ENV[@]}"} \ + bash "$REHEARSAL" \ + "${CASE_ARGS[@]}" \ + >"$CASE_DIR/stdout" 2>"$CASE_DIR/stderr" +} + +expect_failure() { + local label="$1" + local expected="$2" + + if run_case "$label"; then + fail "$label unexpectedly passed" + fi + assert_contains "$expected" "$CASE_DIR/stderr" + printf '%s\n' "[coredata-simulator-rehearsal-test] PASS (rejected): $label" +} + +mkdir -p "$TEMPORARY_DIR/bin" +: >"$TEMPORARY_DIR/all-pids" + +cat >"$TEMPORARY_DIR/bin/codesign" <<'CODESIGN' +#!/usr/bin/env bash +set -euo pipefail + +if [[ "${1:-}" == "--verify" ]]; then + [[ "${FAKE_CODESIGN_VERIFY_FAIL:-0}" != "1" ]] + exit +fi + +[[ "${1:-}" == "-dv" && "${2:-}" == "--verbose=4" ]] || + exit 64 + +identifier="$FAKE_BUNDLE_ID" +case "${FAKE_CODESIGN_IDENTIFIER_MODE:-valid}" in + valid) + ;; + missing) + identifier="" + ;; + spoofed) + identifier="evil${FAKE_BUNDLE_ID}" + ;; + duplicate) + printf '%s\n' "Identifier=$FAKE_BUNDLE_ID" + ;; + *) + exit 64 + ;; +esac + +printf '%s\n' "Executable=/private/tmp/fearless.app/fearless" +if [[ -n "$identifier" ]]; then + printf '%s\n' "Identifier=$identifier" +fi +printf '%s\n' "Signature=adhoc" +CODESIGN + +cat >"$TEMPORARY_DIR/bin/process-check" <<'CHECKER' +#!/usr/bin/env bash +set -euo pipefail + +if [[ "${FAKE_PROCESS_CHECK_FAIL:-0}" == "1" ]]; then + exit 1 +fi +kill -0 "$1" 2>/dev/null +CHECKER + +cat >"$TEMPORARY_DIR/bin/xcrun" <<'XCRUN' +#!/usr/bin/env bash +set -euo pipefail + +printf '%s\n' "$*" >>"$FAKE_COMMAND_LOG" + +if [[ "${1:-}" == "nm" ]]; then + classes=( + CDAsset CDChain CDChainNode CDChainStorageItem CDChainXcmConfig + CDContact CDContactItem CDExternalApi CDPhishingItem CDPolkaswapDex + CDPolkaswapRemoteSettings CDPriceData CDPriceProvider + CDRuntimeMetadataItem CDScamInfo CDStashItem CDTransactionHistoryItem + CDXcmAvailableAsset CDXcmAvailableDestination CDAccountInfo + CDAssetVisibility CDChainAccount CDChainSettings CDCurrency + CDCustomChainNode CDMetaAccount + ) + for class_name in "${classes[@]}"; do + if [[ "$class_name" != "${FAKE_MISSING_MANAGED_CLASS:-}" ]]; then + printf '%s\n' "_OBJC_CLASS_\$_$class_name" + fi + done + exit 0 +fi + +if [[ "${1:-}" == "vtool" ]]; then + if [[ "${FAKE_APP_PLATFORM:-simulator}" == "device" ]]; then + printf '%s\n' "platform IOS" + else + printf '%s\n' "platform IOSSIMULATOR" + fi + exit 0 +fi + +[[ "${1:-}" == "simctl" ]] || exit 64 +shift + +case "${1:-}" in + list) + state="${FAKE_SIMULATOR_STATE:-Booted}" + available="${FAKE_SIMULATOR_AVAILABLE:-true}" + name="${FAKE_SIMULATOR_NAME:-iPhone Rehearsal}" + cat <&2 + exit 2 + fi + container="$FAKE_CORE_SIM_ROOT/CoreSimulator/Devices/$FAKE_SIMULATOR_UDID/data/Containers/Data/Application/APP-DATA" + mkdir -p "$container" + if [[ "${4:-}" == "data" ]]; then + printf '%s\n' "$container" + else + printf '%s\n' "$container/Installed.app" + fi + ;; + install) + if [[ "${FAKE_MUTATE_FIXTURE:-0}" == "1" ]]; then + printf '%s\n' "mutation" >>"$FAKE_FIXTURE_DIR/UserDataModel.sqlite" + fi + : >"$FAKE_INSTALL_STATE" + container="$FAKE_CORE_SIM_ROOT/CoreSimulator/Devices/$FAKE_SIMULATOR_UDID/data/Containers/Data/Application/APP-DATA" + mkdir -p "$container" + ;; + terminate) + if [[ -s "$FAKE_PID_FILE" ]]; then + pid="$(cat "$FAKE_PID_FILE")" + kill "$pid" >/dev/null 2>&1 || true + : >"$FAKE_PID_FILE" + exit 0 + fi + printf '%s\n' "found nothing to terminate; app is not running" >&2 + exit 3 + ;; + launch) + sleep 300 & + pid=$! + printf '%s\n' "$pid" >"$FAKE_PID_FILE" + printf '%s\n' "$pid" >>"$FAKE_ALL_PIDS" + if [[ "${FAKE_CREATE_CRASH:-0}" == "1" ]]; then + cat >"$FAKE_CRASH_DIR/fearless-test.ips" <"$FAKE_CRASH_DIR/fearless-test-no-device.ips" <&2 + fail "valid dry-run was rejected" +fi +assert_contains "DRY RUN PASSED" "$CASE_DIR/stdout" +assert_command_count "simctl install $SIMULATOR_UDID $APP_DIR" "0" +assert_command_count "simctl launch $SIMULATOR_UDID $BUNDLE_ID" "0" +cmp -s "$ARTIFACTS_DIR/source-before.sha256" "$ARTIFACTS_DIR/source-after-run.sha256" || + fail "dry-run source manifests differ" +printf '%s\n' "[coredata-simulator-rehearsal-test] PASS: immutable dry-run" + +# The fully stubbed happy path exercises install, first launch, bounded liveness, +# sanitized logs/crash scans, terminate, copied SQLite inspection, and relaunch. +prepare_case "full-rehearsal" +if ! run_case "full-rehearsal"; then + sed -n '1,160p' "$CASE_DIR/stderr" >&2 + fail "valid full rehearsal was rejected" +fi +assert_contains "PASSED: first launch and relaunch each reached a usable startup route" "$CASE_DIR/stdout" +canonical_app_dir="$(canonical_directory "$APP_DIR")" +assert_command_count "simctl install $SIMULATOR_UDID $canonical_app_dir" "1" +assert_command_count "simctl launch $SIMULATOR_UDID $BUNDLE_ID" "2" +[[ "$(grep -c '^simctl terminate ' "$COMMAND_LOG")" == "3" ]] || + fail "full rehearsal did not terminate before seeding and after both launches" +[[ "$(grep -c '^simctl uninstall ' "$COMMAND_LOG" || true)" == "0" ]] || + fail "rehearsal attempted an uninstall" +cmp -s "$ARTIFACTS_DIR/source-before.sha256" "$ARTIFACTS_DIR/source-after-run.sha256" || + fail "full-rehearsal source manifests differ" +printf '%s\n' "[coredata-simulator-rehearsal-test] PASS: full first-launch/relaunch contract" + +prepare_case "missing-acknowledgement" +CASE_ARGS=( + --simulator-udid "$SIMULATOR_UDID" + --app "$APP_DIR" + --bundle-id "$BUNDLE_ID" + --fixture-dir "$FIXTURE_DIR" + --artifacts-dir "$ARTIFACTS_DIR" + --crash-report-dir "$CRASH_DIR" + --dry-run +) +expect_failure "missing acknowledgement" "--confirm-disposable-fixture is required" + +prepare_case "malformed-udid" +CASE_ARGS[1]="not-a-device" +CASE_ARGS+=(--dry-run) +expect_failure "malformed UDID" "must be a canonical UUID" + +prepare_case "device-binary" +CASE_ENV=("FAKE_APP_PLATFORM=device") +CASE_ARGS+=(--dry-run) +expect_failure "device binary" "refusing device app" + +prepare_case "invalid-signature" +CASE_ENV=("FAKE_CODESIGN_VERIFY_FAIL=1") +CASE_ARGS+=(--dry-run) +expect_failure "invalid signature" "failed strict code-signature verification" + +prepare_case "missing-code-signing-identifier" +CASE_ENV=("FAKE_CODESIGN_IDENTIFIER_MODE=missing") +CASE_ARGS+=(--dry-run) +expect_failure "missing code-signing identifier" "does not exactly match the requested bundle" + +prepare_case "spoofed-code-signing-identifier" +CASE_ENV=("FAKE_CODESIGN_IDENTIFIER_MODE=spoofed") +CASE_ARGS+=(--dry-run) +expect_failure "spoofed code-signing identifier" "does not exactly match the requested bundle" + +prepare_case "duplicate-code-signing-identifiers" +CASE_ENV=("FAKE_CODESIGN_IDENTIFIER_MODE=duplicate") +CASE_ARGS+=(--dry-run) +expect_failure "duplicate code-signing identifiers" "does not exactly match the requested bundle" + +prepare_case "debug-configuration" +plutil -replace FearlessBuildConfiguration -string Debug "$APP_DIR/Info.plist" +CASE_ARGS+=(--dry-run) +expect_failure "Debug configuration" "not built with the Release configuration" + +prepare_case "unoptimized-swift" +plutil -replace FearlessSwiftOptimizationLevel -json '"-Onone"' "$APP_DIR/Info.plist" +CASE_ARGS+=(--dry-run) +expect_failure "unoptimized Swift" "not built with Swift -O optimization" + +prepare_case "testability-enabled" +plutil -replace FearlessEnableTestability -string YES "$APP_DIR/Info.plist" +CASE_ARGS+=(--dry-run) +expect_failure "testability enabled" "built with testability enabled" + +prepare_case "missing-managed-class" +CASE_ENV=("FAKE_MISSING_MANAGED_CLASS=CDChain") +CASE_ARGS+=(--dry-run) +expect_failure "missing managed-object runtime class" "missing a required managed-object runtime class" + +prepare_case "missing-migration-resource" +rm "$APP_DIR/SubstrateV3toV4.cdm" +CASE_ARGS+=(--dry-run) +expect_failure "missing migration resource" "missing or symlinking a required Core Data migration resource" + +prepare_case "shutdown-simulator" +CASE_ENV=("FAKE_SIMULATOR_STATE=Shutdown") +CASE_ARGS+=(--dry-run) +expect_failure "shutdown simulator" "must already be Booted" + +prepare_case "unavailable-simulator" +CASE_ENV=("FAKE_SIMULATOR_AVAILABLE=false") +CASE_ARGS+=(--dry-run) +expect_failure "unavailable simulator" "Simulator is unavailable" + +prepare_case "non-iphone-simulator" +CASE_ENV=("FAKE_SIMULATOR_NAME=iPad Pro") +CASE_ARGS+=(--dry-run) +expect_failure "non-iPhone simulator" "must be an iPhone" + +prepare_case "extra-fixture-file" +printf '%s\n' "unexpected" >"$FIXTURE_DIR/notes.txt" +CASE_ARGS+=(--dry-run) +expect_failure "extra fixture file" "exactly the nine expected" + +prepare_case "symlink-fixture-component" +mv "$FIXTURE_DIR/UserDataModel.sqlite-wal" "$CASE_DIR/real-wal" +ln -s "$CASE_DIR/real-wal" "$FIXTURE_DIR/UserDataModel.sqlite-wal" +CASE_ARGS+=(--dry-run) +expect_failure "symlink fixture component" "regular non-symlink" + +prepare_case "overlapping-artifacts" +ARTIFACTS_DIR="$FIXTURE_DIR/artifacts" +CASE_ARGS=( + --simulator-udid "$SIMULATOR_UDID" + --app "$APP_DIR" + --bundle-id "$BUNDLE_ID" + --fixture-dir "$FIXTURE_DIR" + --artifacts-dir "$ARTIFACTS_DIR" + --crash-report-dir "$CRASH_DIR" + --confirm-disposable-fixture + --dry-run +) +expect_failure "overlapping artifacts" "must not overlap" +[[ ! -e "$ARTIFACTS_DIR" ]] || + fail "overlap rejection wrote into the source fixture" + +prepare_case "already-installed" +: >"$INSTALL_STATE" +expect_failure "already installed" "already has Simulator data" +assert_command_count "simctl install $SIMULATOR_UDID $APP_DIR" "0" + +prepare_case "source-mutated" +CASE_ENV=("FAKE_MUTATE_FIXTURE=1") +expect_failure "source mutation" "source fixture changed before Simulator injection" + +prepare_case "dead-process" +CASE_ENV=("FAKE_PROCESS_CHECK_FAIL=1") +expect_failure "dead process" "did not remain alive" + +prepare_case "missing-startup-ready" +CASE_ENV=("FAKE_LOG_CONTENT=ordinary application log") +expect_failure "missing startup-ready marker" "startup readiness failed" + +prepare_case "startup-failure-marker" +CASE_ENV=("FAKE_LOG_CONTENT=FEARLESS_STARTUP_FAILED") +expect_failure "startup-failure marker" "startup readiness failed" + +prepare_case "duplicate-startup-ready" +CASE_ENV=("FAKE_LOG_CONTENT=FEARLESS_STARTUP_READY FEARLESS_STARTUP_READY") +expect_failure "duplicate startup-ready marker" "startup readiness failed" + +prepare_case "fatal-coredata-log" +CASE_ENV=("FAKE_LOG_CONTENT=CoreData persistent store migration failed with an incompatible model") +expect_failure "fatal Core Data log" "fatal migration/Core Data marker" + +prepare_case "wallet-key-mutated" +CASE_ENV=("FAKE_MUTATE_USER_KEY=1") +expect_failure "wallet key mutation" "changed protected wallet identities, keys, or relationships" + +prepare_case "wallet-relationship-mutated" +CASE_ENV=("FAKE_MUTATE_USER_RELATIONSHIP=1") +expect_failure "wallet relationship mutation" "changed protected wallet identities, keys, or relationships" + +prepare_case "substrate-nonzero-row-loss" +CASE_ENV=("FAKE_DROP_ONE_SUBSTRATE_ROW=1") +expect_failure "nonzero Substrate row loss" "changed the protected ZCDCHAIN row count" + +prepare_case "matching-crash-report" +CASE_ENV=("FAKE_CREATE_CRASH=1") +expect_failure "matching crash report" "new crash report matched" + +prepare_case "matching-crash-without-device-id" +CASE_ENV=("FAKE_CREATE_CRASH_WITHOUT_DEVICE=1") +expect_failure "matching crash report without device ID" "new crash report matched" + +printf '%s\n' \ + "[coredata-simulator-rehearsal-test] PASS: 2 positive + 30 negative/adversarial contracts" From d7648e21938f2cbbd2969d00a915248fc61ec1a7 Mon Sep 17 00:00:00 2001 From: Makoto Takemiya Date: Sun, 26 Jul 2026 11:23:47 +0400 Subject: [PATCH 02/30] Fix production iOS release identity --- fearless.xcodeproj/project.pbxproj | 11 +++++----- fearless/WalletConnect.dev.entitlements | 27 +++++++++++++++++++++++++ fearless/WalletConnect.entitlements | 10 +++------ 3 files changed, 35 insertions(+), 13 deletions(-) create mode 100644 fearless/WalletConnect.dev.entitlements diff --git a/fearless.xcodeproj/project.pbxproj b/fearless.xcodeproj/project.pbxproj index f640fe0762..13f8f32174 100644 --- a/fearless.xcodeproj/project.pbxproj +++ b/fearless.xcodeproj/project.pbxproj @@ -19529,7 +19529,7 @@ ); MARKETING_VERSION = 2.2.3; OTHER_SWIFT_FLAGS = "$(inherited)"; - PRODUCT_BUNDLE_IDENTIFIER = jp.co.soramitsu.fearless; + PRODUCT_BUNDLE_IDENTIFIER = jp.co.soramitsu.fearlesswallet; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ENABLE_EXPLICIT_MODULES = NO; SWIFT_OBJC_BRIDGING_HEADER = "fearless/fearless-Bridging-Header.h"; @@ -19545,9 +19545,8 @@ ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon$(ICON_SUFFIX)"; CLONED_SOURCE_PACKAGES_DIR_PATH = "$(SRCROOT)/SourcePackages"; CODE_SIGN_ENTITLEMENTS = fearless/WalletConnect.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 7; + CURRENT_PROJECT_VERSION = 2026.7.26; DEVELOPMENT_TEAM = YLWWUD25VZ; ENABLE_BITCODE = NO; INFOPLIST_FILE = fearless/Info.plist; @@ -19558,8 +19557,8 @@ "$(CONFIGURATION_BUILD_DIR)", "$(FRAMEWORK_SEARCH_PATHS)", ); - MARKETING_VERSION = 2.2.3; - PRODUCT_BUNDLE_IDENTIFIER = jp.co.soramitsu.fearless; + MARKETING_VERSION = 4.2.0; + PRODUCT_BUNDLE_IDENTIFIER = jp.co.soramitsu.fearlesswallet; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_ENABLE_EXPLICIT_MODULES = NO; @@ -19686,7 +19685,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon$(ICON_SUFFIX)"; CLONED_SOURCE_PACKAGES_DIR_PATH = "$(SRCROOT)/SourcePackages"; - CODE_SIGN_ENTITLEMENTS = fearless/WalletConnect.entitlements; + CODE_SIGN_ENTITLEMENTS = fearless/WalletConnect.dev.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 7; diff --git a/fearless/WalletConnect.dev.entitlements b/fearless/WalletConnect.dev.entitlements new file mode 100644 index 0000000000..c464aa54f5 --- /dev/null +++ b/fearless/WalletConnect.dev.entitlements @@ -0,0 +1,27 @@ + + + + + com.apple.developer.associated-domains + + webcredentials:fearlesswallet.io + + com.apple.developer.icloud-container-identifiers + + iCloud.jp.co.soramitsu.fearless + iCloud.jp.co.soramitsu.fearlesswallet.dev + + com.apple.developer.icloud-services + + CloudKit + + com.apple.security.application-groups + + group.com.walletconnect.sdk + + keychain-access-groups + + group.com.walletconnect.sdk + + + diff --git a/fearless/WalletConnect.entitlements b/fearless/WalletConnect.entitlements index c464aa54f5..f4e105882b 100644 --- a/fearless/WalletConnect.entitlements +++ b/fearless/WalletConnect.entitlements @@ -4,12 +4,12 @@ com.apple.developer.associated-domains + applinks:fearlesswallet.io webcredentials:fearlesswallet.io com.apple.developer.icloud-container-identifiers - iCloud.jp.co.soramitsu.fearless - iCloud.jp.co.soramitsu.fearlesswallet.dev + iCloud.jp.co.soramitsu.fearlesswallet com.apple.developer.icloud-services @@ -17,11 +17,7 @@ com.apple.security.application-groups - group.com.walletconnect.sdk - - keychain-access-groups - - group.com.walletconnect.sdk + group.jp.co.soramitsu.fearlesswallet From b85172535f71c683448662cffbe44bb263db4d55 Mon Sep 17 00:00:00 2001 From: Makoto Takemiya Date: Sun, 26 Jul 2026 11:28:53 +0400 Subject: [PATCH 03/30] Guard the production iOS archive identity --- .../xcshareddata/xcschemes/fearless.xcscheme | 3 +- scripts/ci/audit-ios-release-identity.sh | 139 ++++++++++++++ scripts/test-ios-release-identity-audit.sh | 180 ++++++++++++++++++ 3 files changed, 320 insertions(+), 2 deletions(-) create mode 100755 scripts/ci/audit-ios-release-identity.sh create mode 100755 scripts/test-ios-release-identity-audit.sh diff --git a/fearless.xcodeproj/xcshareddata/xcschemes/fearless.xcscheme b/fearless.xcodeproj/xcshareddata/xcschemes/fearless.xcscheme index b4999e827b..732d2d2027 100644 --- a/fearless.xcodeproj/xcshareddata/xcschemes/fearless.xcscheme +++ b/fearless.xcodeproj/xcshareddata/xcschemes/fearless.xcscheme @@ -82,8 +82,7 @@ bash \"${PROJECT_DIR}/scripts/spm-shared-features-fixes.sh\" \"${ buildConfiguration = "Debug"> - diff --git a/scripts/ci/audit-ios-release-identity.sh b/scripts/ci/audit-ios-release-identity.sh new file mode 100755 index 0000000000..0612c566ee --- /dev/null +++ b/scripts/ci/audit-ios-release-identity.sh @@ -0,0 +1,139 @@ +#!/usr/bin/env bash +set -euo pipefail + +readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +readonly SCHEME_FILE="${IOS_RELEASE_SCHEME_FILE:-$REPO_ROOT/fearless.xcodeproj/xcshareddata/xcschemes/fearless.xcscheme}" +readonly ENTITLEMENTS_FILE="${IOS_RELEASE_ENTITLEMENTS_FILE:-$REPO_ROOT/fearless/WalletConnect.entitlements}" +readonly EXPECTED_BUNDLE_ID="${IOS_EXPECTED_BUNDLE_ID:-jp.co.soramitsu.fearlesswallet}" +readonly EXPECTED_VERSION="${IOS_EXPECTED_MARKETING_VERSION:-4.2.0}" +readonly EXPECTED_BUILD="${IOS_EXPECTED_BUILD_NUMBER:-2026.7.26}" + +fail() { + printf '[ios-release-identity] FAIL: %s\n' "$*" >&2 + exit 1 +} + +require_regular_file() { + local path="$1" + local label="$2" + + [[ -f "$path" ]] || fail "$label is missing: $path" + [[ ! -L "$path" ]] || fail "$label must not be a symbolic link: $path" +} + +require_regular_file "$SCHEME_FILE" "shared scheme" +require_regular_file "$ENTITLEMENTS_FILE" "production entitlements" + +archive_action_count="$( + xmllint --xpath 'count(/Scheme/ArchiveAction)' "$SCHEME_FILE" 2>/dev/null +)" || fail "shared scheme is not valid XML" +[[ "$archive_action_count" == "1" ]] || + fail "shared scheme must contain exactly one ArchiveAction" + +archive_configuration="$( + xmllint --xpath 'string(/Scheme/ArchiveAction/@buildConfiguration)' \ + "$SCHEME_FILE" 2>/dev/null +)" || fail "cannot read ArchiveAction configuration" +[[ "$archive_configuration" == "Release" ]] || + fail "shared scheme archives $archive_configuration instead of Release" + +settings_json="${IOS_RELEASE_SETTINGS_JSON:-}" +generated_settings="" +if [[ -z "$settings_json" ]]; then + generated_settings="$(mktemp "${TMPDIR:-/tmp}/fearless-release-settings.XXXXXX")" + settings_json="$generated_settings" + + xcodebuild_args=( + -workspace "$REPO_ROOT/fearless.xcworkspace" + -scheme fearless + -configuration Release + -destination "generic/platform=iOS" + -showBuildSettings + -json + ) + + if [[ -n "${IOS_RELEASE_SOURCE_PACKAGES_DIR:-}" ]]; then + xcodebuild_args+=( + -clonedSourcePackagesDirPath "$IOS_RELEASE_SOURCE_PACKAGES_DIR" + -disableAutomaticPackageResolution + -skipPackageUpdates + ) + fi + + xcodebuild "${xcodebuild_args[@]}" > "$settings_json" || + fail "xcodebuild could not resolve Release settings" +else + require_regular_file "$settings_json" "Release settings JSON" +fi + +jq -e ' + type == "array" and + ([.[] | select(.target == "fearless")] | length == 1) +' "$settings_json" >/dev/null || + fail "Release settings must contain exactly one fearless target" + +read_setting() { + local key="$1" + + jq -er --arg key "$key" ' + [.[] | select(.target == "fearless")][0].buildSettings[$key] // + error("missing setting") + ' "$settings_json" 2>/dev/null || + fail "Release setting is missing: $key" +} + +require_setting() { + local key="$1" + local expected="$2" + local actual + + actual="$(read_setting "$key")" + [[ "$actual" == "$expected" ]] || + fail "$key is '$actual', expected '$expected'" +} + +require_setting PRODUCT_BUNDLE_IDENTIFIER "$EXPECTED_BUNDLE_ID" +require_setting MARKETING_VERSION "$EXPECTED_VERSION" +require_setting CURRENT_PROJECT_VERSION "$EXPECTED_BUILD" +require_setting CODE_SIGN_ENTITLEMENTS "fearless/WalletConnect.entitlements" +require_setting CODE_SIGN_STYLE "Automatic" +require_setting SWIFT_OPTIMIZATION_LEVEL "-O" +require_setting ENABLE_TESTABILITY "NO" + +entitlements_json="$( + plutil -convert json -o - "$ENTITLEMENTS_FILE" 2>/dev/null +)" || fail "production entitlements are not a valid property list" + +jq -e ' + type == "object" and + (keys | sort) == ( + [ + "com.apple.developer.associated-domains", + "com.apple.developer.icloud-container-identifiers", + "com.apple.developer.icloud-services", + "com.apple.security.application-groups" + ] | sort + ) and + .["com.apple.developer.associated-domains"] == [ + "applinks:fearlesswallet.io", + "webcredentials:fearlesswallet.io" + ] and + .["com.apple.developer.icloud-container-identifiers"] == [ + "iCloud.jp.co.soramitsu.fearlesswallet" + ] and + .["com.apple.developer.icloud-services"] == ["CloudKit"] and + .["com.apple.security.application-groups"] == [ + "group.jp.co.soramitsu.fearlesswallet" + ] and + (has("keychain-access-groups") | not) +' <<<"$entitlements_json" >/dev/null || + fail "production entitlements do not match the Fearless App Store identity" + +if [[ -n "$generated_settings" ]]; then + rm -f "$generated_settings" +fi + +printf '%s\n' \ + "[ios-release-identity] PASS: Release archives $EXPECTED_BUNDLE_ID $EXPECTED_VERSION ($EXPECTED_BUILD)" diff --git a/scripts/test-ios-release-identity-audit.sh b/scripts/test-ios-release-identity-audit.sh new file mode 100755 index 0000000000..309789c602 --- /dev/null +++ b/scripts/test-ios-release-identity-audit.sh @@ -0,0 +1,180 @@ +#!/usr/bin/env bash +set -euo pipefail + +readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +readonly AUDIT="$REPO_ROOT/scripts/ci/audit-ios-release-identity.sh" +readonly FIXTURES="$(mktemp -d "${TMPDIR:-/tmp}/fearless-release-identity-tests.XXXXXX")" + +trap 'rm -rf "$FIXTURES"' EXIT + +pass_count=0 +fail_count=0 + +run_pass() { + local label="$1" + shift + + if "$@" >/dev/null 2>&1; then + pass_count=$((pass_count + 1)) + else + printf '[ios-release-identity-test] expected pass: %s\n' "$label" >&2 + exit 1 + fi +} + +run_reject() { + local label="$1" + shift + + if "$@" >/dev/null 2>&1; then + printf '[ios-release-identity-test] expected rejection: %s\n' "$label" >&2 + exit 1 + fi + fail_count=$((fail_count + 1)) +} + +settings="$FIXTURES/settings.json" +jq -n '[ + { + target: "fearless", + buildSettings: { + PRODUCT_BUNDLE_IDENTIFIER: "jp.co.soramitsu.fearlesswallet", + MARKETING_VERSION: "4.2.0", + CURRENT_PROJECT_VERSION: "2026.7.26", + CODE_SIGN_ENTITLEMENTS: "fearless/WalletConnect.entitlements", + CODE_SIGN_STYLE: "Automatic", + SWIFT_OPTIMIZATION_LEVEL: "-O", + ENABLE_TESTABILITY: "NO" + } + } +]' > "$settings" + +run_audit() { + IOS_RELEASE_SETTINGS_JSON="$1" \ + IOS_RELEASE_SCHEME_FILE="$2" \ + IOS_RELEASE_ENTITLEMENTS_FILE="$3" \ + bash "$AUDIT" +} + +scheme="$REPO_ROOT/fearless.xcodeproj/xcshareddata/xcschemes/fearless.xcscheme" +entitlements="$REPO_ROOT/fearless/WalletConnect.entitlements" +run_pass canonical run_audit "$settings" "$scheme" "$entitlements" + +dev_scheme="$FIXTURES/dev.xcscheme" +sed 's/buildConfiguration = "Release"/buildConfiguration = "Dev"/' \ + "$scheme" > "$dev_scheme" +run_reject dev-archive run_audit "$settings" "$dev_scheme" "$entitlements" + +duplicate_scheme="$FIXTURES/duplicate.xcscheme" +sed 's###' \ + "$scheme" > "$duplicate_scheme" +run_reject duplicate-archive-action \ + run_audit "$settings" "$duplicate_scheme" "$entitlements" + +scheme_symlink="$FIXTURES/scheme-link" +ln -s "$scheme" "$scheme_symlink" +run_reject scheme-symlink \ + run_audit "$settings" "$scheme_symlink" "$entitlements" + +mutate_setting() { + local key="$1" + local value="$2" + local destination="$3" + + jq --arg key "$key" --arg value "$value" \ + '.[0].buildSettings[$key] = $value' "$settings" > "$destination" +} + +wrong_bundle="$FIXTURES/wrong-bundle.json" +mutate_setting PRODUCT_BUNDLE_IDENTIFIER jp.co.soramitsu.fearless "$wrong_bundle" +run_reject wrong-bundle run_audit "$wrong_bundle" "$scheme" "$entitlements" + +wrong_version="$FIXTURES/wrong-version.json" +mutate_setting MARKETING_VERSION 4.1.9 "$wrong_version" +run_reject wrong-version run_audit "$wrong_version" "$scheme" "$entitlements" + +wrong_build="$FIXTURES/wrong-build.json" +mutate_setting CURRENT_PROJECT_VERSION 7 "$wrong_build" +run_reject wrong-build run_audit "$wrong_build" "$scheme" "$entitlements" + +debug_optimization="$FIXTURES/debug-optimization.json" +mutate_setting SWIFT_OPTIMIZATION_LEVEL -Onone "$debug_optimization" +run_reject debug-optimization \ + run_audit "$debug_optimization" "$scheme" "$entitlements" + +testable_release="$FIXTURES/testable-release.json" +mutate_setting ENABLE_TESTABILITY YES "$testable_release" +run_reject testable-release \ + run_audit "$testable_release" "$scheme" "$entitlements" + +manual_signing="$FIXTURES/manual-signing.json" +mutate_setting CODE_SIGN_STYLE Manual "$manual_signing" +run_reject manual-signing \ + run_audit "$manual_signing" "$scheme" "$entitlements" + +dev_entitlement_setting="$FIXTURES/dev-entitlement-setting.json" +mutate_setting CODE_SIGN_ENTITLEMENTS \ + fearless/WalletConnect.dev.entitlements "$dev_entitlement_setting" +run_reject dev-entitlement-setting \ + run_audit "$dev_entitlement_setting" "$scheme" "$entitlements" + +extra_target="$FIXTURES/extra-target.json" +jq '. + [.[0]]' "$settings" > "$extra_target" +run_reject duplicate-fearless-target \ + run_audit "$extra_target" "$scheme" "$entitlements" + +malformed_settings="$FIXTURES/malformed-settings.json" +printf '%s\n' '{"not":"an array"}' > "$malformed_settings" +run_reject malformed-settings \ + run_audit "$malformed_settings" "$scheme" "$entitlements" + +mutate_entitlements() { + local filter="$1" + local destination="$2" + + plutil -convert json -o - "$entitlements" | + jq "$filter" | + plutil -convert xml1 -o "$destination" - +} + +extra_cloud="$FIXTURES/extra-cloud.entitlements" +mutate_entitlements \ + '.["com.apple.developer.icloud-container-identifiers"] += ["iCloud.evil"]' \ + "$extra_cloud" +run_reject extra-cloud run_audit "$settings" "$scheme" "$extra_cloud" + +missing_applinks="$FIXTURES/missing-applinks.entitlements" +mutate_entitlements \ + '.["com.apple.developer.associated-domains"] = ["webcredentials:fearlesswallet.io"]' \ + "$missing_applinks" +run_reject missing-applinks \ + run_audit "$settings" "$scheme" "$missing_applinks" + +wrong_group="$FIXTURES/wrong-group.entitlements" +mutate_entitlements \ + '.["com.apple.security.application-groups"] = ["group.com.walletconnect.sdk"]' \ + "$wrong_group" +run_reject wrong-group run_audit "$settings" "$scheme" "$wrong_group" + +keychain_group="$FIXTURES/keychain-group.entitlements" +mutate_entitlements \ + '.["keychain-access-groups"] = ["group.com.walletconnect.sdk"]' \ + "$keychain_group" +run_reject unexpected-keychain-group \ + run_audit "$settings" "$scheme" "$keychain_group" + +extra_entitlement="$FIXTURES/extra-entitlement.entitlements" +mutate_entitlements \ + '.["com.apple.developer.healthkit"] = true' \ + "$extra_entitlement" +run_reject unexpected-entitlement \ + run_audit "$settings" "$scheme" "$extra_entitlement" + +entitlements_symlink="$FIXTURES/entitlements-link" +ln -s "$entitlements" "$entitlements_symlink" +run_reject entitlements-symlink \ + run_audit "$settings" "$scheme" "$entitlements_symlink" + +printf '%s\n' \ + "[ios-release-identity-test] PASS: $pass_count positive and $fail_count negative/adversarial cases" From 1d04582397156dc9774c8a2df4f8b28d38724ad1 Mon Sep 17 00:00:00 2001 From: Makoto Takemiya Date: Sun, 26 Jul 2026 11:32:31 +0400 Subject: [PATCH 04/30] Bind the iOS build number to release settings --- fearless/Info.plist | 2 +- scripts/ci/audit-ios-release-identity.sh | 14 ++++++++++++++ scripts/test-ios-release-identity-audit.sh | 16 ++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/fearless/Info.plist b/fearless/Info.plist index b47d1adaf1..f503f95d94 100644 --- a/fearless/Info.plist +++ b/fearless/Info.plist @@ -53,7 +53,7 @@ CFBundleVersion - 1 + $(CURRENT_PROJECT_VERSION) FearlessBuildConfiguration $(CONFIGURATION) FearlessEnableTestability diff --git a/scripts/ci/audit-ios-release-identity.sh b/scripts/ci/audit-ios-release-identity.sh index 0612c566ee..9b004e3506 100755 --- a/scripts/ci/audit-ios-release-identity.sh +++ b/scripts/ci/audit-ios-release-identity.sh @@ -6,6 +6,7 @@ readonly REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" readonly SCHEME_FILE="${IOS_RELEASE_SCHEME_FILE:-$REPO_ROOT/fearless.xcodeproj/xcshareddata/xcschemes/fearless.xcscheme}" readonly ENTITLEMENTS_FILE="${IOS_RELEASE_ENTITLEMENTS_FILE:-$REPO_ROOT/fearless/WalletConnect.entitlements}" +readonly INFO_PLIST_FILE="${IOS_RELEASE_INFO_PLIST_FILE:-$REPO_ROOT/fearless/Info.plist}" readonly EXPECTED_BUNDLE_ID="${IOS_EXPECTED_BUNDLE_ID:-jp.co.soramitsu.fearlesswallet}" readonly EXPECTED_VERSION="${IOS_EXPECTED_MARKETING_VERSION:-4.2.0}" readonly EXPECTED_BUILD="${IOS_EXPECTED_BUILD_NUMBER:-2026.7.26}" @@ -25,6 +26,19 @@ require_regular_file() { require_regular_file "$SCHEME_FILE" "shared scheme" require_regular_file "$ENTITLEMENTS_FILE" "production entitlements" +require_regular_file "$INFO_PLIST_FILE" "application Info.plist" + +bundle_version="$( + plutil -extract CFBundleVersion raw "$INFO_PLIST_FILE" 2>/dev/null +)" || fail "application Info.plist has no CFBundleVersion" +[[ "$bundle_version" == '$(CURRENT_PROJECT_VERSION)' ]] || + fail "CFBundleVersion must expand CURRENT_PROJECT_VERSION" + +short_version="$( + plutil -extract CFBundleShortVersionString raw "$INFO_PLIST_FILE" 2>/dev/null +)" || fail "application Info.plist has no CFBundleShortVersionString" +[[ "$short_version" == '$(MARKETING_VERSION)' ]] || + fail "CFBundleShortVersionString must expand MARKETING_VERSION" archive_action_count="$( xmllint --xpath 'count(/Scheme/ArchiveAction)' "$SCHEME_FILE" 2>/dev/null diff --git a/scripts/test-ios-release-identity-audit.sh b/scripts/test-ios-release-identity-audit.sh index 309789c602..fcd08356ea 100755 --- a/scripts/test-ios-release-identity-audit.sh +++ b/scripts/test-ios-release-identity-audit.sh @@ -51,9 +51,12 @@ jq -n '[ ]' > "$settings" run_audit() { + local info_plist="${4:-$REPO_ROOT/fearless/Info.plist}" + IOS_RELEASE_SETTINGS_JSON="$1" \ IOS_RELEASE_SCHEME_FILE="$2" \ IOS_RELEASE_ENTITLEMENTS_FILE="$3" \ + IOS_RELEASE_INFO_PLIST_FILE="$info_plist" \ bash "$AUDIT" } @@ -176,5 +179,18 @@ ln -s "$entitlements" "$entitlements_symlink" run_reject entitlements-symlink \ run_audit "$settings" "$scheme" "$entitlements_symlink" +hardcoded_build_plist="$FIXTURES/hardcoded-build.plist" +cp "$REPO_ROOT/fearless/Info.plist" "$hardcoded_build_plist" +plutil -replace CFBundleVersion -string 1 "$hardcoded_build_plist" +run_reject hardcoded-bundle-version \ + run_audit "$settings" "$scheme" "$entitlements" "$hardcoded_build_plist" + +hardcoded_version_plist="$FIXTURES/hardcoded-version.plist" +cp "$REPO_ROOT/fearless/Info.plist" "$hardcoded_version_plist" +plutil -replace CFBundleShortVersionString -string 4.2.0 \ + "$hardcoded_version_plist" +run_reject hardcoded-marketing-version \ + run_audit "$settings" "$scheme" "$entitlements" "$hardcoded_version_plist" + printf '%s\n' \ "[ios-release-identity-test] PASS: $pass_count positive and $fail_count negative/adversarial cases" From 749dbdd4a803fc91c9db4bd2c39bb6f32ddfeacf Mon Sep 17 00:00:00 2001 From: Makoto Takemiya Date: Sun, 26 Jul 2026 11:42:59 +0400 Subject: [PATCH 05/30] Match WalletConnect storage to signed app groups --- .../WalletConnect/WalletConnectService.swift | 33 +++++++++++++++- fearlessTests/Modules/Root/RootTests.swift | 38 +++++++++++++++++++ scripts/ci/audit-ios-release-identity.sh | 24 ++++++++++++ scripts/test-ios-release-identity-audit.sh | 20 ++++++++++ 4 files changed, 113 insertions(+), 2 deletions(-) diff --git a/fearless/ApplicationLayer/Services/WalletConnect/WalletConnectService.swift b/fearless/ApplicationLayer/Services/WalletConnect/WalletConnectService.swift index 8876501dc3..5d5b3e3b61 100644 --- a/fearless/ApplicationLayer/Services/WalletConnect/WalletConnectService.swift +++ b/fearless/ApplicationLayer/Services/WalletConnect/WalletConnectService.swift @@ -29,9 +29,27 @@ extension WalletConnectServiceDelegate { func didChange(sessions _: [Session]) {} } +enum WalletConnectGroupIdentifierResolver { + static let productionBundleIdentifier = "jp.co.soramitsu.fearlesswallet" + static let developmentBundleIdentifier = "jp.co.soramitsu.fearlesswallet.dev" + static let productionGroupIdentifier = "group.jp.co.soramitsu.fearlesswallet" + static let developmentGroupIdentifier = "group.com.walletconnect.sdk" + + static func resolve(bundleIdentifier: String?) -> String? { + if bundleIdentifier == productionBundleIdentifier { + return productionGroupIdentifier + } + + if bundleIdentifier == developmentBundleIdentifier { + return developmentGroupIdentifier + } + + return nil + } +} + final class WalletConnectServiceImpl: WalletConnectService { static let shared = WalletConnectServiceImpl() - private static let walletConnectGroupIdentifier = "group.com.walletconnect.sdk" private var listeners: [WeakWrapper] = [] private var cancellablesBag = Set() @@ -41,6 +59,17 @@ final class WalletConnectServiceImpl: WalletConnectService { // MARK: - ApplicationServiceProtocol func setup() { + guard + let groupIdentifier = WalletConnectGroupIdentifierResolver.resolve( + bundleIdentifier: Bundle.main.bundleIdentifier + ) + else { + Logger.shared.error( + "WalletConnect setup skipped for an unsupported application identity" + ) + return + } + #if canImport(FearlessKeys) #if F_DEV let projectId = WalletConnectDebug.projectId @@ -51,7 +80,7 @@ final class WalletConnectServiceImpl: WalletConnectService { let projectId = WalletConnect.projectId #endif Networking.configure( - groupIdentifier: Self.walletConnectGroupIdentifier, + groupIdentifier: groupIdentifier, projectId: projectId, socketFactory: WalletConnectSocketFactory() ) diff --git a/fearlessTests/Modules/Root/RootTests.swift b/fearlessTests/Modules/Root/RootTests.swift index 5d16218531..9bc68800ca 100644 --- a/fearlessTests/Modules/Root/RootTests.swift +++ b/fearlessTests/Modules/Root/RootTests.swift @@ -10,6 +10,44 @@ import simd import SQLite3 class RootTests: XCTestCase { + func testWalletConnectGroupResolverUsesProductionEntitlementForReleaseBundle() { + XCTAssertEqual( + WalletConnectGroupIdentifierResolver.resolve( + bundleIdentifier: "jp.co.soramitsu.fearlesswallet" + ), + "group.jp.co.soramitsu.fearlesswallet" + ) + } + + func testWalletConnectGroupResolverUsesDevelopmentEntitlementForDevBundle() { + XCTAssertEqual( + WalletConnectGroupIdentifierResolver.resolve( + bundleIdentifier: "jp.co.soramitsu.fearlesswallet.dev" + ), + "group.com.walletconnect.sdk" + ) + } + + func testWalletConnectGroupResolverRejectsUnsupportedApplicationIdentities() { + let unsupportedIdentifiers: [String?] = [ + nil, + "", + "jp.co.soramitsu.fearless", + "jp.co.soramitsu.fearlesswallet.debug", + "jp.co.soramitsu.fearlesswallet.dev.attacker", + "evil.jp.co.soramitsu.fearlesswallet" + ] + + for identifier in unsupportedIdentifiers { + XCTAssertNil( + WalletConnectGroupIdentifierResolver.resolve( + bundleIdentifier: identifier + ), + "Unexpected WalletConnect group for \(identifier ?? "nil")" + ) + } + } + func testHostedUnitTestsUseIsolatedApplicationLaunch() { XCTAssertTrue( ProcessInfo.processInfo.arguments.contains("-UNITTEST"), diff --git a/scripts/ci/audit-ios-release-identity.sh b/scripts/ci/audit-ios-release-identity.sh index 9b004e3506..626789ecdd 100755 --- a/scripts/ci/audit-ios-release-identity.sh +++ b/scripts/ci/audit-ios-release-identity.sh @@ -7,6 +7,7 @@ readonly REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" readonly SCHEME_FILE="${IOS_RELEASE_SCHEME_FILE:-$REPO_ROOT/fearless.xcodeproj/xcshareddata/xcschemes/fearless.xcscheme}" readonly ENTITLEMENTS_FILE="${IOS_RELEASE_ENTITLEMENTS_FILE:-$REPO_ROOT/fearless/WalletConnect.entitlements}" readonly INFO_PLIST_FILE="${IOS_RELEASE_INFO_PLIST_FILE:-$REPO_ROOT/fearless/Info.plist}" +readonly WALLET_CONNECT_SERVICE_FILE="${IOS_RELEASE_WALLET_CONNECT_SERVICE_FILE:-$REPO_ROOT/fearless/ApplicationLayer/Services/WalletConnect/WalletConnectService.swift}" readonly EXPECTED_BUNDLE_ID="${IOS_EXPECTED_BUNDLE_ID:-jp.co.soramitsu.fearlesswallet}" readonly EXPECTED_VERSION="${IOS_EXPECTED_MARKETING_VERSION:-4.2.0}" readonly EXPECTED_BUILD="${IOS_EXPECTED_BUILD_NUMBER:-2026.7.26}" @@ -27,6 +28,7 @@ require_regular_file() { require_regular_file "$SCHEME_FILE" "shared scheme" require_regular_file "$ENTITLEMENTS_FILE" "production entitlements" require_regular_file "$INFO_PLIST_FILE" "application Info.plist" +require_regular_file "$WALLET_CONNECT_SERVICE_FILE" "WalletConnect service" bundle_version="$( plutil -extract CFBundleVersion raw "$INFO_PLIST_FILE" 2>/dev/null @@ -145,6 +147,28 @@ jq -e ' ' <<<"$entitlements_json" >/dev/null || fail "production entitlements do not match the Fearless App Store identity" +grep -Fq \ + 'static let productionGroupIdentifier = "group.jp.co.soramitsu.fearlesswallet"' \ + "$WALLET_CONNECT_SERVICE_FILE" || + fail "WalletConnect production consumer does not use the entitled app group" +grep -Fq \ + 'static let developmentGroupIdentifier = "group.com.walletconnect.sdk"' \ + "$WALLET_CONNECT_SERVICE_FILE" || + fail "WalletConnect development group is not isolated explicitly" +grep -Fq \ + 'WalletConnectGroupIdentifierResolver.resolve(' \ + "$WALLET_CONNECT_SERVICE_FILE" || + fail "WalletConnect setup does not resolve its group from the application identity" +grep -Fq \ + 'groupIdentifier: groupIdentifier' \ + "$WALLET_CONNECT_SERVICE_FILE" || + fail "WalletConnect networking does not consume the resolved group" +if grep -Fq \ + 'groupIdentifier: Self.walletConnectGroupIdentifier' \ + "$WALLET_CONNECT_SERVICE_FILE"; then + fail "WalletConnect setup still consumes a hard-coded app group" +fi + if [[ -n "$generated_settings" ]]; then rm -f "$generated_settings" fi diff --git a/scripts/test-ios-release-identity-audit.sh b/scripts/test-ios-release-identity-audit.sh index fcd08356ea..a1e9faa0e1 100755 --- a/scripts/test-ios-release-identity-audit.sh +++ b/scripts/test-ios-release-identity-audit.sh @@ -52,11 +52,13 @@ jq -n '[ run_audit() { local info_plist="${4:-$REPO_ROOT/fearless/Info.plist}" + local wallet_connect_service="${5:-$REPO_ROOT/fearless/ApplicationLayer/Services/WalletConnect/WalletConnectService.swift}" IOS_RELEASE_SETTINGS_JSON="$1" \ IOS_RELEASE_SCHEME_FILE="$2" \ IOS_RELEASE_ENTITLEMENTS_FILE="$3" \ IOS_RELEASE_INFO_PLIST_FILE="$info_plist" \ + IOS_RELEASE_WALLET_CONNECT_SERVICE_FILE="$wallet_connect_service" \ bash "$AUDIT" } @@ -192,5 +194,23 @@ plutil -replace CFBundleShortVersionString -string 4.2.0 \ run_reject hardcoded-marketing-version \ run_audit "$settings" "$scheme" "$entitlements" "$hardcoded_version_plist" +missing_production_group_service="$FIXTURES/missing-production-group.swift" +sed \ + 's/group\.jp\.co\.soramitsu\.fearlesswallet/group.com.walletconnect.sdk/' \ + "$REPO_ROOT/fearless/ApplicationLayer/Services/WalletConnect/WalletConnectService.swift" \ + > "$missing_production_group_service" +run_reject wallet-connect-production-group-mismatch \ + run_audit "$settings" "$scheme" "$entitlements" \ + "$REPO_ROOT/fearless/Info.plist" "$missing_production_group_service" + +hardcoded_group_service="$FIXTURES/hardcoded-wallet-connect-group.swift" +sed \ + 's/groupIdentifier: groupIdentifier/groupIdentifier: Self.walletConnectGroupIdentifier/' \ + "$REPO_ROOT/fearless/ApplicationLayer/Services/WalletConnect/WalletConnectService.swift" \ + > "$hardcoded_group_service" +run_reject wallet-connect-hardcoded-group \ + run_audit "$settings" "$scheme" "$entitlements" \ + "$REPO_ROOT/fearless/Info.plist" "$hardcoded_group_service" + printf '%s\n' \ "[ios-release-identity-test] PASS: $pass_count positive and $fail_count negative/adversarial cases" From 5ebcf5e510d8cd2d2865be8e9645e8e129435f31 Mon Sep 17 00:00:00 2001 From: Makoto Takemiya Date: Sun, 26 Jul 2026 12:05:51 +0400 Subject: [PATCH 06/30] Keep TODO debt baseline current --- config/todo-debt-baseline.tsv | 1 - 1 file changed, 1 deletion(-) diff --git a/config/todo-debt-baseline.tsv b/config/todo-debt-baseline.tsv index ccd3438865..5e71de1b0c 100644 --- a/config/todo-debt-baseline.tsv +++ b/config/todo-debt-baseline.tsv @@ -8,7 +8,6 @@ fearless/Common/Extension/Storage/CDAccountItem+CoreDataDecodable.swift // TODO: fearless/Common/Services/ChainRegistry/RuntimeProviderPool/RuntimeHotBootSnapshotFactory.swift // TODO: think about it fearless/Common/Services/ChainRegistry/RuntimeProviderPool/RuntimeSnapshotOperationFactory.swift // TODO: think about it fearless/Common/Services/ExtrinsicService/ExtrinsicOperationFactory.swift method: "author_submitAndWatchExtrinsic", // TODO: add to fearless utils -fearless/Common/Storage/Migration/UserStorage/MultiassetV2MigrationPolicy.swift // TODO: tech debt fearless/Common/Storage/Migration/UserStorage/StorageMigrator+Sync.swift // TODO: Move this logic to app loading state fearless/Modules/AddAccount/Interactors/AddAccount+AccountConfirmInteractor.swift // TODO: Check how to convert this to chain account import fearless/Modules/Export/ExportMnemonic/ExportMnemonicPresenter.swift // TODO: Support custom accounts From b11cdc53dc28c2d005b3024ad5c4ee9179e293e1 Mon Sep 17 00:00:00 2001 From: Makoto Takemiya Date: Sun, 26 Jul 2026 12:20:14 +0400 Subject: [PATCH 07/30] Update the Core Data release test invariant --- scripts/ci/run-coredata-release-gate.sh | 6 +++--- scripts/test-coredata-release-gate.sh | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts/ci/run-coredata-release-gate.sh b/scripts/ci/run-coredata-release-gate.sh index 10b436b3fa..640dbd9f31 100644 --- a/scripts/ci/run-coredata-release-gate.sh +++ b/scripts/ci/run-coredata-release-gate.sh @@ -301,8 +301,8 @@ run_stage() { result_name="core" # The selected source cohort contains 409 test methods. Two copied-phone # fixtures are intentionally handled by the separate device/store stage, - # so the simulator core stage must execute exactly 407 tests. - expected_total="407" + # so the simulator core stage must execute exactly 410 tests. + expected_total="410" selectors=( "-only-testing:fearlessTests/SingleToMultiassetUserMigrationTests" "-only-testing:fearlessTests/UserStorageCompatibilityMigrationTests" @@ -381,7 +381,7 @@ run_stage() { if [[ "$stage" == "copied-phone" ]]; then log "PASSED copied-phone fixture stage: exactly 2 tests, source fixture unchanged" else - log "PASSED core stage: exactly 407 Release -O tests, zero failures, skips, or expected failures" + log "PASSED core stage: exactly 410 Release -O tests, zero failures, skips, or expected failures" fi } diff --git a/scripts/test-coredata-release-gate.sh b/scripts/test-coredata-release-gate.sh index 6a4479d2dd..a72c4c662c 100644 --- a/scripts/test-coredata-release-gate.sh +++ b/scripts/test-coredata-release-gate.sh @@ -171,7 +171,7 @@ if [[ "${1:-}" == "xcresulttool" ]]; then exit 0 fi - total="${FAKE_TOTAL_TESTS:-407}" + total="${FAKE_TOTAL_TESTS:-410}" passed="${FAKE_PASSED_TESTS:-$total}" failed="${FAKE_FAILED_TESTS:-0}" skipped="${FAKE_SKIPPED_TESTS:-0}" @@ -218,7 +218,7 @@ if ! run_gate "valid-core"; then fail "valid core gate was rejected" fi assert_output_contains "PASSED requested Core Data Release gate" "$RUN_DIRECTORY/stdout" -assert_output_contains "exactly 407 Release -O tests" "$RUN_DIRECTORY/stdout" +assert_output_contains "exactly 410 Release -O tests" "$RUN_DIRECTORY/stdout" assert_argument_once "-workspace" assert_argument_once "$CANONICAL_WORKSPACE" assert_argument_once "-scheme" @@ -375,7 +375,7 @@ GATE_ARGS=(--stage core --simulator-udid "$SIMULATOR_UDID") expect_failure "unreadable-summary" "xcresulttool could not read" CASE_ENV=( - "FAKE_TOTAL_TESTS=407" + "FAKE_TOTAL_TESTS=410" "FAKE_PASSED_TESTS=242" "FAKE_FAILED_TESTS=1" "FAKE_RESULT=Failed" @@ -385,7 +385,7 @@ GATE_ARGS=(--stage core --simulator-udid "$SIMULATOR_UDID") expect_failure "failed-summary" "zero-failure/zero-skip" CASE_ENV=( - "FAKE_TOTAL_TESTS=407" + "FAKE_TOTAL_TESTS=410" "FAKE_PASSED_TESTS=242" "FAKE_EXPECTED_FAILURES=1" ) From 1424b59c3cd212596aa684aa5db57bafd105b3e4 Mon Sep 17 00:00:00 2001 From: Makoto Takemiya Date: Sun, 26 Jul 2026 12:50:41 +0400 Subject: [PATCH 08/30] Clarify the Core Data release cohort --- scripts/ci/run-coredata-release-gate.sh | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/scripts/ci/run-coredata-release-gate.sh b/scripts/ci/run-coredata-release-gate.sh index 640dbd9f31..059305aa48 100644 --- a/scripts/ci/run-coredata-release-gate.sh +++ b/scripts/ci/run-coredata-release-gate.sh @@ -299,9 +299,8 @@ run_stage() { if [[ "$stage" == "core" ]]; then result_name="core" - # The selected source cohort contains 409 test methods. Two copied-phone - # fixtures are intentionally handled by the separate device/store stage, - # so the simulator core stage must execute exactly 410 tests. + # The simulator core cohort must execute exactly 410 tests. Copied-phone + # fixtures are intentionally verified by the separate device/store stage. expected_total="410" selectors=( "-only-testing:fearlessTests/SingleToMultiassetUserMigrationTests" From bcc3997e0e597a2a3a4c1eb8c9ca4cb1d7e3d974 Mon Sep 17 00:00:00 2001 From: Makoto Takemiya Date: Sun, 26 Jul 2026 13:26:37 +0400 Subject: [PATCH 09/30] Keep custom-only chains available after migration --- .../EntityToModel/ChainModelMapper.swift | 3 +- .../Storage/ChainModelMapperTests.swift | 40 +++++++++++++++++++ scripts/ci/run-coredata-release-gate.sh | 6 +-- scripts/test-coredata-release-gate.sh | 8 ++-- 4 files changed, 49 insertions(+), 8 deletions(-) diff --git a/fearless/Common/Storage/EntityToModel/ChainModelMapper.swift b/fearless/Common/Storage/EntityToModel/ChainModelMapper.swift index aa6c17cf29..48702596b1 100644 --- a/fearless/Common/Storage/EntityToModel/ChainModelMapper.swift +++ b/fearless/Common/Storage/EntityToModel/ChainModelMapper.swift @@ -787,7 +787,8 @@ extension ChainModelMapper: CoreDataMapperProtocol { } let hasRuntimeEndpoint = compatibleNodes.isNotEmpty || - compatibleSelectedNode != nil + compatibleSelectedNode != nil || + compatibleCustomNodes?.isNotEmpty == true return chainModel .replacingNodeConfiguration( diff --git a/fearlessTests/Common/Storage/ChainModelMapperTests.swift b/fearlessTests/Common/Storage/ChainModelMapperTests.swift index 1b05e7ab8f..886603518d 100644 --- a/fearlessTests/Common/Storage/ChainModelMapperTests.swift +++ b/fearlessTests/Common/Storage/ChainModelMapperTests.swift @@ -240,6 +240,46 @@ final class ChainModelMapperTests: XCTestCase { } } + func testTransformKeepsCompatibleCustomNodeAsOnlyRuntimeEndpoint() + throws { + let context = try createContext() + let entity = makeValidChainEntity(in: context) + let customNode = CDChainNode(context: context) + customNode.url = try XCTUnwrap( + URL(string: "wss://custom-only.example") + ) + customNode.name = "Custom only" + entity.customNodes = NSSet(object: customNode) + + let chain = try ChainModelMapper().transform(entity: entity) + + XCTAssertTrue(chain.nodes.isEmpty) + XCTAssertNil(chain.selectedNode) + XCTAssertEqual( + chain.customNodes, + Set([try makeNode(from: customNode)]) + ) + XCTAssertFalse(chain.disabled) + } + + func testTransformDisablesChainWithOnlyIncompatibleCustomNode() throws { + let context = try createContext() + let entity = makeValidChainEntity(in: context) + let customNode = CDChainNode(context: context) + customNode.url = try XCTUnwrap( + URL(string: "https://not-a-substrate-websocket.example") + ) + customNode.name = "Wrong runtime protocol" + entity.customNodes = NSSet(object: customNode) + + let chain = try ChainModelMapper().transform(entity: entity) + + XCTAssertTrue(chain.nodes.isEmpty) + XCTAssertNil(chain.selectedNode) + XCTAssertEqual(chain.customNodes, Set()) + XCTAssertTrue(chain.disabled) + } + func testTransformRejectsHTTPNodesForSubstrateRuntime() throws { let context = try createContext() let incompatibleURLs = [ diff --git a/scripts/ci/run-coredata-release-gate.sh b/scripts/ci/run-coredata-release-gate.sh index 059305aa48..e124b96028 100644 --- a/scripts/ci/run-coredata-release-gate.sh +++ b/scripts/ci/run-coredata-release-gate.sh @@ -299,9 +299,9 @@ run_stage() { if [[ "$stage" == "core" ]]; then result_name="core" - # The simulator core cohort must execute exactly 410 tests. Copied-phone + # The simulator core cohort must execute exactly 412 tests. Copied-phone # fixtures are intentionally verified by the separate device/store stage. - expected_total="410" + expected_total="412" selectors=( "-only-testing:fearlessTests/SingleToMultiassetUserMigrationTests" "-only-testing:fearlessTests/UserStorageCompatibilityMigrationTests" @@ -380,7 +380,7 @@ run_stage() { if [[ "$stage" == "copied-phone" ]]; then log "PASSED copied-phone fixture stage: exactly 2 tests, source fixture unchanged" else - log "PASSED core stage: exactly 410 Release -O tests, zero failures, skips, or expected failures" + log "PASSED core stage: exactly 412 Release -O tests, zero failures, skips, or expected failures" fi } diff --git a/scripts/test-coredata-release-gate.sh b/scripts/test-coredata-release-gate.sh index a72c4c662c..7e37be83dd 100644 --- a/scripts/test-coredata-release-gate.sh +++ b/scripts/test-coredata-release-gate.sh @@ -171,7 +171,7 @@ if [[ "${1:-}" == "xcresulttool" ]]; then exit 0 fi - total="${FAKE_TOTAL_TESTS:-410}" + total="${FAKE_TOTAL_TESTS:-412}" passed="${FAKE_PASSED_TESTS:-$total}" failed="${FAKE_FAILED_TESTS:-0}" skipped="${FAKE_SKIPPED_TESTS:-0}" @@ -218,7 +218,7 @@ if ! run_gate "valid-core"; then fail "valid core gate was rejected" fi assert_output_contains "PASSED requested Core Data Release gate" "$RUN_DIRECTORY/stdout" -assert_output_contains "exactly 410 Release -O tests" "$RUN_DIRECTORY/stdout" +assert_output_contains "exactly 412 Release -O tests" "$RUN_DIRECTORY/stdout" assert_argument_once "-workspace" assert_argument_once "$CANONICAL_WORKSPACE" assert_argument_once "-scheme" @@ -375,7 +375,7 @@ GATE_ARGS=(--stage core --simulator-udid "$SIMULATOR_UDID") expect_failure "unreadable-summary" "xcresulttool could not read" CASE_ENV=( - "FAKE_TOTAL_TESTS=410" + "FAKE_TOTAL_TESTS=412" "FAKE_PASSED_TESTS=242" "FAKE_FAILED_TESTS=1" "FAKE_RESULT=Failed" @@ -385,7 +385,7 @@ GATE_ARGS=(--stage core --simulator-udid "$SIMULATOR_UDID") expect_failure "failed-summary" "zero-failure/zero-skip" CASE_ENV=( - "FAKE_TOTAL_TESTS=410" + "FAKE_TOTAL_TESTS=412" "FAKE_PASSED_TESTS=242" "FAKE_EXPECTED_FAILURES=1" ) From 7a819cb01e92920e5444151392df54245acdd4c8 Mon Sep 17 00:00:00 2001 From: Makoto Takemiya Date: Sun, 26 Jul 2026 13:55:14 +0400 Subject: [PATCH 10/30] Isolate Debug builds from production wallet data --- fearless.xcodeproj/project.pbxproj | 4 +- scripts/ci/audit-ios-release-identity.sh | 101 +++++++++++++++++++++ scripts/test-ios-release-identity-audit.sh | 92 +++++++++++++++++++ 3 files changed, 195 insertions(+), 2 deletions(-) diff --git a/fearless.xcodeproj/project.pbxproj b/fearless.xcodeproj/project.pbxproj index 13f8f32174..d85dc45255 100644 --- a/fearless.xcodeproj/project.pbxproj +++ b/fearless.xcodeproj/project.pbxproj @@ -19514,7 +19514,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon$(ICON_SUFFIX)"; CLONED_SOURCE_PACKAGES_DIR_PATH = "$(SRCROOT)/SourcePackages"; - CODE_SIGN_ENTITLEMENTS = fearless/WalletConnect.entitlements; + CODE_SIGN_ENTITLEMENTS = fearless/WalletConnect.dev.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 7; DEVELOPMENT_TEAM = YLWWUD25VZ; @@ -19529,7 +19529,7 @@ ); MARKETING_VERSION = 2.2.3; OTHER_SWIFT_FLAGS = "$(inherited)"; - PRODUCT_BUNDLE_IDENTIFIER = jp.co.soramitsu.fearlesswallet; + PRODUCT_BUNDLE_IDENTIFIER = jp.co.soramitsu.fearlesswallet.dev; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ENABLE_EXPLICIT_MODULES = NO; SWIFT_OBJC_BRIDGING_HEADER = "fearless/fearless-Bridging-Header.h"; diff --git a/scripts/ci/audit-ios-release-identity.sh b/scripts/ci/audit-ios-release-identity.sh index 626789ecdd..de0bd12c50 100755 --- a/scripts/ci/audit-ios-release-identity.sh +++ b/scripts/ci/audit-ios-release-identity.sh @@ -6,9 +6,11 @@ readonly REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" readonly SCHEME_FILE="${IOS_RELEASE_SCHEME_FILE:-$REPO_ROOT/fearless.xcodeproj/xcshareddata/xcschemes/fearless.xcscheme}" readonly ENTITLEMENTS_FILE="${IOS_RELEASE_ENTITLEMENTS_FILE:-$REPO_ROOT/fearless/WalletConnect.entitlements}" +readonly DEBUG_ENTITLEMENTS_FILE="${IOS_DEBUG_ENTITLEMENTS_FILE:-$REPO_ROOT/fearless/WalletConnect.dev.entitlements}" readonly INFO_PLIST_FILE="${IOS_RELEASE_INFO_PLIST_FILE:-$REPO_ROOT/fearless/Info.plist}" readonly WALLET_CONNECT_SERVICE_FILE="${IOS_RELEASE_WALLET_CONNECT_SERVICE_FILE:-$REPO_ROOT/fearless/ApplicationLayer/Services/WalletConnect/WalletConnectService.swift}" readonly EXPECTED_BUNDLE_ID="${IOS_EXPECTED_BUNDLE_ID:-jp.co.soramitsu.fearlesswallet}" +readonly EXPECTED_DEBUG_BUNDLE_ID="${IOS_EXPECTED_DEBUG_BUNDLE_ID:-jp.co.soramitsu.fearlesswallet.dev}" readonly EXPECTED_VERSION="${IOS_EXPECTED_MARKETING_VERSION:-4.2.0}" readonly EXPECTED_BUILD="${IOS_EXPECTED_BUILD_NUMBER:-2026.7.26}" @@ -27,6 +29,7 @@ require_regular_file() { require_regular_file "$SCHEME_FILE" "shared scheme" require_regular_file "$ENTITLEMENTS_FILE" "production entitlements" +require_regular_file "$DEBUG_ENTITLEMENTS_FILE" "development entitlements" require_regular_file "$INFO_PLIST_FILE" "application Info.plist" require_regular_file "$WALLET_CONNECT_SERVICE_FILE" "WalletConnect service" @@ -84,12 +87,49 @@ else require_regular_file "$settings_json" "Release settings JSON" fi +debug_settings_json="${IOS_DEBUG_SETTINGS_JSON:-}" +generated_debug_settings="" +if [[ -z "$debug_settings_json" ]]; then + generated_debug_settings="$( + mktemp "${TMPDIR:-/tmp}/fearless-debug-settings.XXXXXX" + )" + debug_settings_json="$generated_debug_settings" + + debug_xcodebuild_args=( + -workspace "$REPO_ROOT/fearless.xcworkspace" + -scheme fearless + -configuration Debug + -destination "generic/platform=iOS" + -showBuildSettings + -json + ) + + if [[ -n "${IOS_RELEASE_SOURCE_PACKAGES_DIR:-}" ]]; then + debug_xcodebuild_args+=( + -clonedSourcePackagesDirPath "$IOS_RELEASE_SOURCE_PACKAGES_DIR" + -disableAutomaticPackageResolution + -skipPackageUpdates + ) + fi + + xcodebuild "${debug_xcodebuild_args[@]}" > "$debug_settings_json" || + fail "xcodebuild could not resolve Debug settings" +else + require_regular_file "$debug_settings_json" "Debug settings JSON" +fi + jq -e ' type == "array" and ([.[] | select(.target == "fearless")] | length == 1) ' "$settings_json" >/dev/null || fail "Release settings must contain exactly one fearless target" +jq -e ' + type == "array" and + ([.[] | select(.target == "fearless")] | length == 1) +' "$debug_settings_json" >/dev/null || + fail "Debug settings must contain exactly one fearless target" + read_setting() { local key="$1" @@ -100,6 +140,16 @@ read_setting() { fail "Release setting is missing: $key" } +read_debug_setting() { + local key="$1" + + jq -er --arg key "$key" ' + [.[] | select(.target == "fearless")][0].buildSettings[$key] // + error("missing setting") + ' "$debug_settings_json" 2>/dev/null || + fail "Debug setting is missing: $key" +} + require_setting() { local key="$1" local expected="$2" @@ -110,6 +160,16 @@ require_setting() { fail "$key is '$actual', expected '$expected'" } +require_debug_setting() { + local key="$1" + local expected="$2" + local actual + + actual="$(read_debug_setting "$key")" + [[ "$actual" == "$expected" ]] || + fail "Debug $key is '$actual', expected '$expected'" +} + require_setting PRODUCT_BUNDLE_IDENTIFIER "$EXPECTED_BUNDLE_ID" require_setting MARKETING_VERSION "$EXPECTED_VERSION" require_setting CURRENT_PROJECT_VERSION "$EXPECTED_BUILD" @@ -118,6 +178,12 @@ require_setting CODE_SIGN_STYLE "Automatic" require_setting SWIFT_OPTIMIZATION_LEVEL "-O" require_setting ENABLE_TESTABILITY "NO" +require_debug_setting PRODUCT_BUNDLE_IDENTIFIER "$EXPECTED_DEBUG_BUNDLE_ID" +require_debug_setting \ + CODE_SIGN_ENTITLEMENTS \ + "fearless/WalletConnect.dev.entitlements" +require_debug_setting CODE_SIGN_STYLE "Automatic" + entitlements_json="$( plutil -convert json -o - "$ENTITLEMENTS_FILE" 2>/dev/null )" || fail "production entitlements are not a valid property list" @@ -147,6 +213,38 @@ jq -e ' ' <<<"$entitlements_json" >/dev/null || fail "production entitlements do not match the Fearless App Store identity" +debug_entitlements_json="$( + plutil -convert json -o - "$DEBUG_ENTITLEMENTS_FILE" 2>/dev/null +)" || fail "development entitlements are not a valid property list" + +jq -e ' + type == "object" and + (keys | sort) == ( + [ + "com.apple.developer.associated-domains", + "com.apple.developer.icloud-container-identifiers", + "com.apple.developer.icloud-services", + "com.apple.security.application-groups", + "keychain-access-groups" + ] | sort + ) and + .["com.apple.developer.associated-domains"] == [ + "webcredentials:fearlesswallet.io" + ] and + .["com.apple.developer.icloud-container-identifiers"] == [ + "iCloud.jp.co.soramitsu.fearless", + "iCloud.jp.co.soramitsu.fearlesswallet.dev" + ] and + .["com.apple.developer.icloud-services"] == ["CloudKit"] and + .["com.apple.security.application-groups"] == [ + "group.com.walletconnect.sdk" + ] and + .["keychain-access-groups"] == [ + "group.com.walletconnect.sdk" + ] +' <<<"$debug_entitlements_json" >/dev/null || + fail "Debug entitlements do not match the isolated development identity" + grep -Fq \ 'static let productionGroupIdentifier = "group.jp.co.soramitsu.fearlesswallet"' \ "$WALLET_CONNECT_SERVICE_FILE" || @@ -172,6 +270,9 @@ fi if [[ -n "$generated_settings" ]]; then rm -f "$generated_settings" fi +if [[ -n "$generated_debug_settings" ]]; then + rm -f "$generated_debug_settings" +fi printf '%s\n' \ "[ios-release-identity] PASS: Release archives $EXPECTED_BUNDLE_ID $EXPECTED_VERSION ($EXPECTED_BUILD)" diff --git a/scripts/test-ios-release-identity-audit.sh b/scripts/test-ios-release-identity-audit.sh index a1e9faa0e1..9cbd6bf7c6 100755 --- a/scripts/test-ios-release-identity-audit.sh +++ b/scripts/test-ios-release-identity-audit.sh @@ -50,18 +50,50 @@ jq -n '[ } ]' > "$settings" +debug_settings="$FIXTURES/debug-settings.json" +jq -n '[ + { + target: "fearless", + buildSettings: { + PRODUCT_BUNDLE_IDENTIFIER: "jp.co.soramitsu.fearlesswallet.dev", + CODE_SIGN_ENTITLEMENTS: "fearless/WalletConnect.dev.entitlements", + CODE_SIGN_STYLE: "Automatic" + } + } +]' > "$debug_settings" + +dev_entitlements="$REPO_ROOT/fearless/WalletConnect.dev.entitlements" + run_audit() { local info_plist="${4:-$REPO_ROOT/fearless/Info.plist}" local wallet_connect_service="${5:-$REPO_ROOT/fearless/ApplicationLayer/Services/WalletConnect/WalletConnectService.swift}" + local debug_settings_file="${6:-$debug_settings}" + local debug_entitlements_file="${7:-$dev_entitlements}" IOS_RELEASE_SETTINGS_JSON="$1" \ + IOS_DEBUG_SETTINGS_JSON="$debug_settings_file" \ IOS_RELEASE_SCHEME_FILE="$2" \ IOS_RELEASE_ENTITLEMENTS_FILE="$3" \ + IOS_DEBUG_ENTITLEMENTS_FILE="$debug_entitlements_file" \ IOS_RELEASE_INFO_PLIST_FILE="$info_plist" \ IOS_RELEASE_WALLET_CONNECT_SERVICE_FILE="$wallet_connect_service" \ bash "$AUDIT" } +run_debug_fixture() { + local debug_settings_file="$1" + local debug_entitlements_file="${2:-$dev_entitlements}" + + run_audit \ + "$settings" \ + "$scheme" \ + "$entitlements" \ + "$REPO_ROOT/fearless/Info.plist" \ + "$REPO_ROOT/fearless/ApplicationLayer/Services/WalletConnect/WalletConnectService.swift" \ + "$debug_settings_file" \ + "$debug_entitlements_file" +} + scheme="$REPO_ROOT/fearless.xcodeproj/xcshareddata/xcschemes/fearless.xcscheme" entitlements="$REPO_ROOT/fearless/WalletConnect.entitlements" run_pass canonical run_audit "$settings" "$scheme" "$entitlements" @@ -134,6 +166,47 @@ printf '%s\n' '{"not":"an array"}' > "$malformed_settings" run_reject malformed-settings \ run_audit "$malformed_settings" "$scheme" "$entitlements" +mutate_debug_setting() { + local key="$1" + local value="$2" + local destination="$3" + + jq --arg key "$key" --arg value "$value" \ + '.[0].buildSettings[$key] = $value' \ + "$debug_settings" > "$destination" +} + +debug_production_bundle="$FIXTURES/debug-production-bundle.json" +mutate_debug_setting \ + PRODUCT_BUNDLE_IDENTIFIER \ + jp.co.soramitsu.fearlesswallet \ + "$debug_production_bundle" +run_reject debug-production-bundle \ + run_debug_fixture "$debug_production_bundle" + +debug_production_entitlements="$FIXTURES/debug-production-entitlements.json" +mutate_debug_setting \ + CODE_SIGN_ENTITLEMENTS \ + fearless/WalletConnect.entitlements \ + "$debug_production_entitlements" +run_reject debug-production-entitlements \ + run_debug_fixture "$debug_production_entitlements" + +debug_manual_signing="$FIXTURES/debug-manual-signing.json" +mutate_debug_setting CODE_SIGN_STYLE Manual "$debug_manual_signing" +run_reject debug-manual-signing \ + run_debug_fixture "$debug_manual_signing" + +duplicate_debug_target="$FIXTURES/duplicate-debug-target.json" +jq '. + [.[0]]' "$debug_settings" > "$duplicate_debug_target" +run_reject duplicate-debug-target \ + run_debug_fixture "$duplicate_debug_target" + +malformed_debug_settings="$FIXTURES/malformed-debug-settings.json" +printf '%s\n' '{"not":"an array"}' > "$malformed_debug_settings" +run_reject malformed-debug-settings \ + run_debug_fixture "$malformed_debug_settings" + mutate_entitlements() { local filter="$1" local destination="$2" @@ -181,6 +254,25 @@ ln -s "$entitlements" "$entitlements_symlink" run_reject entitlements-symlink \ run_audit "$settings" "$scheme" "$entitlements_symlink" +debug_entitlements_production_group="$FIXTURES/debug-production-group.entitlements" +plutil -convert json -o - "$dev_entitlements" | + jq '.["com.apple.security.application-groups"] = ["group.jp.co.soramitsu.fearlesswallet"]' | + plutil -convert xml1 -o "$debug_entitlements_production_group" - +run_reject debug-production-group \ + run_debug_fixture "$debug_settings" "$debug_entitlements_production_group" + +debug_entitlements_production_cloud="$FIXTURES/debug-production-cloud.entitlements" +plutil -convert json -o - "$dev_entitlements" | + jq '.["com.apple.developer.icloud-container-identifiers"] = ["iCloud.jp.co.soramitsu.fearlesswallet"]' | + plutil -convert xml1 -o "$debug_entitlements_production_cloud" - +run_reject debug-production-cloud \ + run_debug_fixture "$debug_settings" "$debug_entitlements_production_cloud" + +debug_entitlements_symlink="$FIXTURES/debug-entitlements-link" +ln -s "$dev_entitlements" "$debug_entitlements_symlink" +run_reject debug-entitlements-symlink \ + run_debug_fixture "$debug_settings" "$debug_entitlements_symlink" + hardcoded_build_plist="$FIXTURES/hardcoded-build.plist" cp "$REPO_ROOT/fearless/Info.plist" "$hardcoded_build_plist" plutil -replace CFBundleVersion -string 1 "$hardcoded_build_plist" From d689535a45510ad74db60119f6b81143a1e4ffa8 Mon Sep 17 00:00:00 2001 From: Makoto Takemiya Date: Sun, 26 Jul 2026 16:03:31 +0400 Subject: [PATCH 11/30] Gate and sign the migration crash hotfix release --- .github/workflows/ios-release-safety.yml | 67 ++ Jenkinsfile | 111 ++- .../WalletConnect/WalletConnectService.swift | 2 +- fearless/Info.plist | 2 + fearless/WalletConnect.dev.entitlements | 6 +- ...SubstrateStorageClassResolutionTests.swift | 4 + .../Storage/ChainModelMapperTests.swift | 2 + fearlessTests/Modules/Root/RootTests.swift | 2 +- scripts/ci/audit-ios-release-identity.sh | 58 +- .../ci/audit-ios-signed-release-artifact.sh | 661 ++++++++++++++++++ .../ci/build-audited-ios-release-archive.sh | 175 +++++ scripts/ci/hash-ios-archive.sh | 68 ++ .../coredata-release-copied-phone-tests.txt | 3 + .../manifests/coredata-release-core-tests.txt | 414 +++++++++++ scripts/ci/run-coredata-release-gate.sh | 204 ++++++ .../ci/run-coredata-simulator-rehearsal.sh | 302 +++++++- scripts/test-coredata-release-gate.sh | 99 ++- scripts/test-coredata-simulator-rehearsal.sh | 167 ++++- scripts/test-ios-release-identity-audit.sh | 57 +- .../test-ios-signed-release-artifact-audit.sh | 380 ++++++++++ scripts/test-matrix.sh | 10 +- 21 files changed, 2747 insertions(+), 47 deletions(-) create mode 100644 .github/workflows/ios-release-safety.yml create mode 100755 scripts/ci/audit-ios-signed-release-artifact.sh create mode 100644 scripts/ci/build-audited-ios-release-archive.sh create mode 100755 scripts/ci/hash-ios-archive.sh create mode 100644 scripts/ci/manifests/coredata-release-copied-phone-tests.txt create mode 100644 scripts/ci/manifests/coredata-release-core-tests.txt create mode 100755 scripts/test-ios-signed-release-artifact-audit.sh diff --git a/.github/workflows/ios-release-safety.yml b/.github/workflows/ios-release-safety.yml new file mode 100644 index 0000000000..ec7bdf67a1 --- /dev/null +++ b/.github/workflows/ios-release-safety.yml @@ -0,0 +1,67 @@ +name: iOS Release Safety + +on: + pull_request: + push: + branches: + - master + - "release/**" + - "hotfix/**" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ios-release-safety-${{ github.ref }} + cancel-in-progress: true + +jobs: + release-contracts: + name: release-contracts + runs-on: macos-15 + timeout-minutes: 20 + steps: + - name: Check out exact commit + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + fetch-depth: 1 + + - name: Require clean exact checkout + run: | + set -euo pipefail + test -z "$(git status --porcelain=v1 --untracked-files=all)" + git rev-parse --verify HEAD + + - name: Run release gate contract suites + run: | + set -euo pipefail + bash scripts/test-ios-release-identity-audit.sh + bash scripts/test-ios-signed-release-artifact-audit.sh + bash scripts/test-coredata-release-gate.sh + bash scripts/test-coredata-simulator-rehearsal.sh + bash scripts/storage/test-user-storage-compatibility-model-audit.sh + + - name: Reject silent x86 Release omissions + run: | + set -euo pipefail + if rg -n 'Skipping Release simulator tests|Release simulator tests were skipped' scripts/test-matrix.sh; then + echo "Release matrix still contains a green skip path." >&2 + exit 1 + fi + + required-context: + name: ios-release-safety + runs-on: ubuntu-latest + needs: release-contracts + if: always() + steps: + - name: Require release contract success + env: + RESULT: ${{ needs.release-contracts.result }} + run: | + set -euo pipefail + [[ "$RESULT" == "success" ]] || { + echo "iOS release safety contracts did not pass: $RESULT" >&2 + exit 1 + } diff --git a/Jenkinsfile b/Jenkinsfile index 2a00a2f508..92945a4a21 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -48,7 +48,8 @@ def ghStatusFallback(String context, String state, String description) { // Ensure SPM and shared-features patches are applied before the main pipeline. // This resolves Web3 API drift (Data.bytes) and IrohaCrypto modulemap issues prior to archive. -node('mac-fearless') { +try { + node('mac-fearless') { stage('Bootstrap CI deps') { checkout scm sh label: 'Bootstrap Pods/SPM/LFS + apply shared-features fixes', script: 'bash scripts/ci/bootstrap.sh' @@ -78,17 +79,113 @@ node('mac-fearless') { scripts/test-matrix.sh fearless.tests "$DESTINATION" ''' ghNotifySafe context: 'jenkins/ios-tests', status: 'SUCCESS', description: 'All tests passed' - ghNotifySafe context: 'continuous-integration/jenkins/pr-merge', status: 'SUCCESS', description: 'Jenkins PR merge build passed' ghStatusFallback('jenkins/ios-tests', 'success', 'All tests passed') - ghStatusFallback('continuous-integration/jenkins/pr-merge', 'success', 'Jenkins PR merge build passed') } catch (e) { ghNotifySafe context: 'jenkins/ios-tests', status: 'FAILURE', description: 'Unit tests failed' - ghNotifySafe context: 'continuous-integration/jenkins/pr-merge', status: 'FAILURE', description: 'Jenkins PR merge build failed' ghStatusFallback('jenkins/ios-tests', 'failure', 'Unit tests failed') - ghStatusFallback('continuous-integration/jenkins/pr-merge', 'failure', 'Jenkins PR merge build failed') throw e } } -} -appPipeline.runPipeline('fearless') + ghNotifySafe context: 'jenkins/ios-release-safety', status: 'PENDING', description: 'Running release safety gates' + ghStatusFallback('jenkins/ios-release-safety', 'pending', 'Running release safety gates') + stage('iOS Release Safety Contracts') { + sh label: 'Run fail-closed release gate contract tests', script: ''' + set -euo pipefail + bash scripts/test-ios-release-identity-audit.sh + bash scripts/test-ios-signed-release-artifact-audit.sh + bash scripts/test-coredata-release-gate.sh + bash scripts/test-coredata-simulator-rehearsal.sh + bash scripts/storage/test-user-storage-compatibility-model-audit.sh + ''' + } + + // Jenkins multibranch PR jobs expose BRANCH_NAME as PR-. Bind the + // gate to both direct release branches and the repository's allowed hotfix / + // release promotion paths so a PR cannot bypass the exact Release evidence. + def directReleaseBranch = ( + env.BRANCH_NAME == 'master' || + env.BRANCH_NAME?.startsWith('release/') || + env.BRANCH_NAME?.startsWith('hotfix/') + ) + def releasePullRequest = ( + env.CHANGE_ID && + ( + ( + env.CHANGE_TARGET == 'develop' && + env.CHANGE_BRANCH?.startsWith('hotfix/') + ) || + ( + env.CHANGE_TARGET == 'master' && + ( + env.CHANGE_BRANCH == 'develop' || + env.CHANGE_BRANCH?.startsWith('release/') || + env.CHANGE_BRANCH?.startsWith('hotfix/') + ) + ) + ) + ) + def releaseBranch = directReleaseBranch || releasePullRequest + if (releaseBranch) { + stage('Exact Core Data Release Gate') { + sh label: 'Require arm64 and exact 412-test Release inventory', script: ''' + set -euo pipefail + [[ "$(uname -m)" == "arm64" ]] || { + echo "Exact Release gate requires dispatch to an arm64 macOS agent." >&2 + exit 78 + } + IOS_EXPECTED_BUILD_NUMBER=2026.7.26 \ + IOS_RELEASE_SOURCE_PACKAGES_DIR="$PWD/SourcePackages" \ + bash scripts/ci/audit-ios-release-identity.sh + bash scripts/storage/audit-user-storage-compatibility-models.sh + bash scripts/ci/run-coredata-release-gate.sh --stage core + ''' + } + } + + if (env.IOS_RELEASE_CANDIDATE == '1') { + stage('Copied-phone Release Evidence') { + sh label: 'Require immutable copied-phone fixture stage', script: ''' + set -euo pipefail + : "${FEARLESS_SUBSTRATE_PHONE_STORE_FIXTURE:?release candidate requires the copied-phone fixture}" + : "${FEARLESS_CORE_DATA_SIMULATOR_UDID:?release candidate requires an explicit Simulator UDID}" + bash scripts/ci/run-coredata-release-gate.sh \ + --stage copied-phone \ + --simulator-udid "$FEARLESS_CORE_DATA_SIMULATOR_UDID" \ + --fixture "$FEARLESS_SUBSTRATE_PHONE_STORE_FIXTURE" + ''' + } + } + } + + appPipeline.runPipeline('fearless') + + // A required context must not turn green until every custom gate and the + // shared application pipeline have completed. The fallback needs a node + // because it uses curl through the shell. + ghNotifySafe context: 'jenkins/ios-release-safety', status: 'SUCCESS', description: 'All iOS release safety gates passed' + ghNotifySafe context: 'continuous-integration/jenkins/pr-merge', status: 'SUCCESS', description: 'Jenkins PR merge build passed' + node('mac-fearless') { + stage('Finalize Required GitHub Statuses') { + ghStatusFallback('jenkins/ios-release-safety', 'success', 'All iOS release safety gates passed') + ghStatusFallback('continuous-integration/jenkins/pr-merge', 'success', 'Jenkins PR merge build passed') + } + } +} catch (e) { + // Post plugin failures first. If a macOS executor cannot be reacquired for + // the API fallback, the required contexts remain failed or pending, never + // stale green. + ghNotifySafe context: 'jenkins/ios-release-safety', status: 'FAILURE', description: 'iOS release safety or pipeline failed' + ghNotifySafe context: 'continuous-integration/jenkins/pr-merge', status: 'FAILURE', description: 'Jenkins PR merge build failed' + try { + node('mac-fearless') { + stage('Report Required GitHub Status Failure') { + ghStatusFallback('jenkins/ios-release-safety', 'failure', 'iOS release safety or pipeline failed') + ghStatusFallback('continuous-integration/jenkins/pr-merge', 'failure', 'Jenkins PR merge build failed') + } + } + } catch (statusError) { + echo "Could not run GitHub status API fallback: ${statusError.message}" + } + throw e +} diff --git a/fearless/ApplicationLayer/Services/WalletConnect/WalletConnectService.swift b/fearless/ApplicationLayer/Services/WalletConnect/WalletConnectService.swift index 5d5b3e3b61..a7ed1b659b 100644 --- a/fearless/ApplicationLayer/Services/WalletConnect/WalletConnectService.swift +++ b/fearless/ApplicationLayer/Services/WalletConnect/WalletConnectService.swift @@ -33,7 +33,7 @@ enum WalletConnectGroupIdentifierResolver { static let productionBundleIdentifier = "jp.co.soramitsu.fearlesswallet" static let developmentBundleIdentifier = "jp.co.soramitsu.fearlesswallet.dev" static let productionGroupIdentifier = "group.jp.co.soramitsu.fearlesswallet" - static let developmentGroupIdentifier = "group.com.walletconnect.sdk" + static let developmentGroupIdentifier = "group.jp.co.soramitsu.fearlesswallet.walletconnect" static func resolve(bundleIdentifier: String?) -> String? { if bundleIdentifier == productionBundleIdentifier { diff --git a/fearless/Info.plist b/fearless/Info.plist index f503f95d94..b46c19644a 100644 --- a/fearless/Info.plist +++ b/fearless/Info.plist @@ -58,6 +58,8 @@ $(CONFIGURATION) FearlessEnableTestability $(ENABLE_TESTABILITY) + FearlessGitCommit + $(FEARLESS_GIT_COMMIT) FearlessSwiftOptimizationLevel $(SWIFT_OPTIMIZATION_LEVEL) GIDClientID diff --git a/fearless/WalletConnect.dev.entitlements b/fearless/WalletConnect.dev.entitlements index c464aa54f5..59012c598e 100644 --- a/fearless/WalletConnect.dev.entitlements +++ b/fearless/WalletConnect.dev.entitlements @@ -17,11 +17,7 @@ com.apple.security.application-groups - group.com.walletconnect.sdk - - keychain-access-groups - - group.com.walletconnect.sdk + group.jp.co.soramitsu.fearlesswallet.walletconnect diff --git a/fearlessTests/Common/Migration/SubstrateStorageClassResolutionTests.swift b/fearlessTests/Common/Migration/SubstrateStorageClassResolutionTests.swift index 1b041d8953..092edc9213 100644 --- a/fearlessTests/Common/Migration/SubstrateStorageClassResolutionTests.swift +++ b/fearlessTests/Common/Migration/SubstrateStorageClassResolutionTests.swift @@ -638,6 +638,10 @@ final class SubstrateStorageClassResolutionTests: XCTestCase { } func testCopiedPhoneV8Store_whenFetchedThroughProductionRepositories_thenMapsEveryRuntimeAndChain() throws { + // TestFlight regression path: + // CoreDataRepository.fetchAll -> Collection.map -> ChainModelMapper. + // The 2026-07-23 crashes trapped in Swift Array access on this path, so + // the copied store must traverse the production repository unchanged. let fixtureURL = copiedPhoneStoreFixtureURL() guard FileManager.default.fileExists(atPath: fixtureURL.path) else { throw XCTSkip("Optional copied-phone Substrate store fixture is unavailable") diff --git a/fearlessTests/Common/Storage/ChainModelMapperTests.swift b/fearlessTests/Common/Storage/ChainModelMapperTests.swift index 886603518d..e9398aa1fe 100644 --- a/fearlessTests/Common/Storage/ChainModelMapperTests.swift +++ b/fearlessTests/Common/Storage/ChainModelMapperTests.swift @@ -1099,6 +1099,8 @@ final class ChainModelMapperTests: XCTestCase { } } + // Exercise the same repository fetch/map frame sequence as the + // TestFlight _ArrayBuffer._getElementSlowPath crash after cleanup. let repository = factory.createRepository() let fetchOperation = repository.fetchAllOperation( with: RepositoryFetchOptions() diff --git a/fearlessTests/Modules/Root/RootTests.swift b/fearlessTests/Modules/Root/RootTests.swift index 9bc68800ca..569dc01ea9 100644 --- a/fearlessTests/Modules/Root/RootTests.swift +++ b/fearlessTests/Modules/Root/RootTests.swift @@ -24,7 +24,7 @@ class RootTests: XCTestCase { WalletConnectGroupIdentifierResolver.resolve( bundleIdentifier: "jp.co.soramitsu.fearlesswallet.dev" ), - "group.com.walletconnect.sdk" + "group.jp.co.soramitsu.fearlesswallet.walletconnect" ) } diff --git a/scripts/ci/audit-ios-release-identity.sh b/scripts/ci/audit-ios-release-identity.sh index de0bd12c50..07ea39263c 100755 --- a/scripts/ci/audit-ios-release-identity.sh +++ b/scripts/ci/audit-ios-release-identity.sh @@ -4,6 +4,33 @@ set -euo pipefail readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" readonly REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +readonly EXPECTED_DEVELOPMENT_TEAM="YLWWUD25VZ" +readonly AUDIT_TEST_HARNESS="${IOS_RELEASE_AUDIT_TEST_HARNESS:-0}" + +fail() { + printf '[ios-release-identity] FAIL: %s\n' "$*" >&2 + exit 1 +} + +readonly TEST_HARNESS_OVERRIDE_NAMES=( + IOS_RELEASE_SCHEME_FILE + IOS_RELEASE_ENTITLEMENTS_FILE + IOS_DEBUG_ENTITLEMENTS_FILE + IOS_RELEASE_INFO_PLIST_FILE + IOS_RELEASE_WALLET_CONNECT_SERVICE_FILE + IOS_RELEASE_SETTINGS_JSON + IOS_DEBUG_SETTINGS_JSON + IOS_EXPECTED_BUNDLE_ID + IOS_EXPECTED_DEBUG_BUNDLE_ID + IOS_EXPECTED_MARKETING_VERSION +) +if [[ "$AUDIT_TEST_HARNESS" != "1" ]]; then + for override_name in "${TEST_HARNESS_OVERRIDE_NAMES[@]}"; do + [[ -z "${!override_name:-}" ]] || + fail "$override_name is accepted only with IOS_RELEASE_AUDIT_TEST_HARNESS=1" + done +fi + readonly SCHEME_FILE="${IOS_RELEASE_SCHEME_FILE:-$REPO_ROOT/fearless.xcodeproj/xcshareddata/xcschemes/fearless.xcscheme}" readonly ENTITLEMENTS_FILE="${IOS_RELEASE_ENTITLEMENTS_FILE:-$REPO_ROOT/fearless/WalletConnect.entitlements}" readonly DEBUG_ENTITLEMENTS_FILE="${IOS_DEBUG_ENTITLEMENTS_FILE:-$REPO_ROOT/fearless/WalletConnect.dev.entitlements}" @@ -12,12 +39,9 @@ readonly WALLET_CONNECT_SERVICE_FILE="${IOS_RELEASE_WALLET_CONNECT_SERVICE_FILE: readonly EXPECTED_BUNDLE_ID="${IOS_EXPECTED_BUNDLE_ID:-jp.co.soramitsu.fearlesswallet}" readonly EXPECTED_DEBUG_BUNDLE_ID="${IOS_EXPECTED_DEBUG_BUNDLE_ID:-jp.co.soramitsu.fearlesswallet.dev}" readonly EXPECTED_VERSION="${IOS_EXPECTED_MARKETING_VERSION:-4.2.0}" -readonly EXPECTED_BUILD="${IOS_EXPECTED_BUILD_NUMBER:-2026.7.26}" - -fail() { - printf '[ios-release-identity] FAIL: %s\n' "$*" >&2 - exit 1 -} +readonly EXPECTED_BUILD="${IOS_EXPECTED_BUILD_NUMBER:-}" +[[ "$EXPECTED_BUILD" =~ ^[1-9][0-9]{0,3}(\.[0-9]{1,2}){0,2}$ ]] || + fail "IOS_EXPECTED_BUILD_NUMBER must be an explicit canonical CFBundleVersion" require_regular_file() { local path="$1" @@ -45,6 +69,12 @@ short_version="$( [[ "$short_version" == '$(MARKETING_VERSION)' ]] || fail "CFBundleShortVersionString must expand MARKETING_VERSION" +git_commit="$( + plutil -extract FearlessGitCommit raw "$INFO_PLIST_FILE" 2>/dev/null +)" || fail "application Info.plist has no FearlessGitCommit" +[[ "$git_commit" == '$(FEARLESS_GIT_COMMIT)' ]] || + fail "FearlessGitCommit must expand FEARLESS_GIT_COMMIT" + archive_action_count="$( xmllint --xpath 'count(/Scheme/ArchiveAction)' "$SCHEME_FILE" 2>/dev/null )" || fail "shared scheme is not valid XML" @@ -71,6 +101,7 @@ if [[ -z "$settings_json" ]]; then -destination "generic/platform=iOS" -showBuildSettings -json + "CURRENT_PROJECT_VERSION=$EXPECTED_BUILD" ) if [[ -n "${IOS_RELEASE_SOURCE_PACKAGES_DIR:-}" ]]; then @@ -175,6 +206,7 @@ require_setting MARKETING_VERSION "$EXPECTED_VERSION" require_setting CURRENT_PROJECT_VERSION "$EXPECTED_BUILD" require_setting CODE_SIGN_ENTITLEMENTS "fearless/WalletConnect.entitlements" require_setting CODE_SIGN_STYLE "Automatic" +require_setting DEVELOPMENT_TEAM "$EXPECTED_DEVELOPMENT_TEAM" require_setting SWIFT_OPTIMIZATION_LEVEL "-O" require_setting ENABLE_TESTABILITY "NO" @@ -183,6 +215,7 @@ require_debug_setting \ CODE_SIGN_ENTITLEMENTS \ "fearless/WalletConnect.dev.entitlements" require_debug_setting CODE_SIGN_STYLE "Automatic" +require_debug_setting DEVELOPMENT_TEAM "$EXPECTED_DEVELOPMENT_TEAM" entitlements_json="$( plutil -convert json -o - "$ENTITLEMENTS_FILE" 2>/dev/null @@ -224,8 +257,7 @@ jq -e ' "com.apple.developer.associated-domains", "com.apple.developer.icloud-container-identifiers", "com.apple.developer.icloud-services", - "com.apple.security.application-groups", - "keychain-access-groups" + "com.apple.security.application-groups" ] | sort ) and .["com.apple.developer.associated-domains"] == [ @@ -237,11 +269,9 @@ jq -e ' ] and .["com.apple.developer.icloud-services"] == ["CloudKit"] and .["com.apple.security.application-groups"] == [ - "group.com.walletconnect.sdk" + "group.jp.co.soramitsu.fearlesswallet.walletconnect" ] and - .["keychain-access-groups"] == [ - "group.com.walletconnect.sdk" - ] + (has("keychain-access-groups") | not) ' <<<"$debug_entitlements_json" >/dev/null || fail "Debug entitlements do not match the isolated development identity" @@ -250,9 +280,9 @@ grep -Fq \ "$WALLET_CONNECT_SERVICE_FILE" || fail "WalletConnect production consumer does not use the entitled app group" grep -Fq \ - 'static let developmentGroupIdentifier = "group.com.walletconnect.sdk"' \ + 'static let developmentGroupIdentifier = "group.jp.co.soramitsu.fearlesswallet.walletconnect"' \ "$WALLET_CONNECT_SERVICE_FILE" || - fail "WalletConnect development group is not isolated explicitly" + fail "WalletConnect development consumer does not use the registered Debug app group" grep -Fq \ 'WalletConnectGroupIdentifierResolver.resolve(' \ "$WALLET_CONNECT_SERVICE_FILE" || diff --git a/scripts/ci/audit-ios-signed-release-artifact.sh b/scripts/ci/audit-ios-signed-release-artifact.sh new file mode 100755 index 0000000000..51dc0967e0 --- /dev/null +++ b/scripts/ci/audit-ios-signed-release-artifact.sh @@ -0,0 +1,661 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Fail-closed audit of the exact production .xcarchive proposed for TestFlight. +# The caller must supply independently recorded digests from the archive build +# step; this script never infers provenance from the current checkout. + +umask 077 + +readonly LOG_PREFIX="[ios-signed-release-audit]" +readonly EXPECTED_TEAM="YLWWUD25VZ" +readonly EXPECTED_BUNDLE="jp.co.soramitsu.fearlesswallet" +readonly EXPECTED_VERSION="4.2.0" +readonly EXPECTED_APPLICATION_ID="${EXPECTED_TEAM}.${EXPECTED_BUNDLE}" +readonly EXPECTED_ASSOCIATED_DOMAINS_JSON='["applinks:fearlesswallet.io","webcredentials:fearlesswallet.io"]' +readonly EXPECTED_ICLOUD_CONTAINERS_JSON='["iCloud.jp.co.soramitsu.fearlesswallet"]' +readonly EXPECTED_ICLOUD_SERVICES_JSON='["CloudKit"]' +readonly EXPECTED_APP_GROUPS_JSON='["group.jp.co.soramitsu.fearlesswallet"]' +readonly EXPECTED_PROFILE_KEYCHAIN_GROUPS_JSON='["YLWWUD25VZ.*","com.apple.token"]' +readonly MINIMUM_PROFILE_VALIDITY_DAYS=14 +readonly TEST_HARNESS="${FEARLESS_SIGNED_AUDIT_TEST_HARNESS:-0}" + +fail() { + printf '%s ERROR: %s\n' "$LOG_PREFIX" "$*" >&2 + exit 1 +} + +usage() { + cat <<'USAGE' +Usage: + bash scripts/ci/audit-ios-signed-release-artifact.sh \ + --archive /absolute/path/to/fearless.xcarchive \ + --expected-git-sha 40_HEX_SHA \ + --expected-build CANONICAL_CF_BUNDLE_VERSION \ + --expected-executable-sha256 64_HEX_SHA256 \ + --expected-archive-sha256 64_HEX_SHA256 \ + --expected-signing-certificate-sha1 40_HEX_SHA1 \ + --expected-profile-uuid UUID \ + --expected-profile-name PROFILE_NAME \ + --receipt /absolute/path/to/new-receipt.json + +The archive digest is the canonical tree digest emitted by: + bash scripts/ci/hash-ios-archive.sh /absolute/path/to/fearless.xcarchive + +Tool overrides are accepted only when +FEARLESS_SIGNED_AUDIT_TEST_HARNESS=1: + FEARLESS_SIGNED_AUDIT_CODESIGN_BIN + FEARLESS_SIGNED_AUDIT_SECURITY_BIN + FEARLESS_SIGNED_AUDIT_PYTHON_BIN + FEARLESS_SIGNED_AUDIT_SHASUM_BIN +USAGE +} + +require_option_value() { + local option="$1" + local remaining="$2" + [[ "$remaining" -ge 2 ]] || fail "$option requires a value" +} + +require_executable() { + local executable="$1" + local label="$2" + + if [[ "$executable" == */* ]]; then + [[ -x "$executable" ]] || fail "$label is not executable" + else + command -v "$executable" >/dev/null 2>&1 || + fail "$label is unavailable" + fi +} + +lowercase() { + printf '%s' "$1" | tr '[:upper:]' '[:lower:]' +} + +if [[ "$TEST_HARNESS" != "1" ]]; then + for override_name in \ + FEARLESS_SIGNED_AUDIT_CODESIGN_BIN \ + FEARLESS_SIGNED_AUDIT_SECURITY_BIN \ + FEARLESS_SIGNED_AUDIT_PYTHON_BIN \ + FEARLESS_SIGNED_AUDIT_SHASUM_BIN; do + [[ -z "${!override_name:-}" ]] || + fail "$override_name is accepted only in the explicit test harness" + done +fi + +readonly CODESIGN_BIN="${FEARLESS_SIGNED_AUDIT_CODESIGN_BIN:-codesign}" +readonly SECURITY_BIN="${FEARLESS_SIGNED_AUDIT_SECURITY_BIN:-security}" +readonly PYTHON_BIN="${FEARLESS_SIGNED_AUDIT_PYTHON_BIN:-python3}" +readonly SHASUM_BIN="${FEARLESS_SIGNED_AUDIT_SHASUM_BIN:-shasum}" + +archive="" +expected_git_sha="" +expected_build="" +expected_executable_sha="" +expected_archive_sha="" +expected_signing_certificate_sha1="" +expected_profile_uuid="" +expected_profile_name="" +receipt="" + +while [[ "$#" -gt 0 ]]; do + case "$1" in + --archive) + require_option_value "$1" "$#" + archive="$2" + shift 2 + ;; + --expected-git-sha) + require_option_value "$1" "$#" + expected_git_sha="$2" + shift 2 + ;; + --expected-build) + require_option_value "$1" "$#" + expected_build="$2" + shift 2 + ;; + --expected-executable-sha256) + require_option_value "$1" "$#" + expected_executable_sha="$2" + shift 2 + ;; + --expected-archive-sha256) + require_option_value "$1" "$#" + expected_archive_sha="$2" + shift 2 + ;; + --expected-signing-certificate-sha1) + require_option_value "$1" "$#" + expected_signing_certificate_sha1="$2" + shift 2 + ;; + --expected-profile-uuid) + require_option_value "$1" "$#" + expected_profile_uuid="$2" + shift 2 + ;; + --expected-profile-name) + require_option_value "$1" "$#" + expected_profile_name="$2" + shift 2 + ;; + --receipt) + require_option_value "$1" "$#" + receipt="$2" + shift 2 + ;; + --help|-h) + usage + exit 0 + ;; + *) + fail "unknown argument: $1" + ;; + esac +done + +[[ "$archive" == /* ]] || fail "--archive must be an absolute path" +[[ "$archive" == *.xcarchive ]] || fail "--archive must name an .xcarchive" +[[ -d "$archive" && ! -L "$archive" ]] || + fail "--archive must name a real, non-symlink directory" +[[ "$expected_git_sha" =~ ^[0-9A-Fa-f]{40}$ ]] || + fail "--expected-git-sha must be an exact 40-hex commit" +[[ "$expected_build" =~ ^[1-9][0-9]{0,3}(\.[0-9]{1,2}){0,2}$ ]] || + fail "--expected-build must be an explicit canonical CFBundleVersion" +[[ "$expected_executable_sha" =~ ^[0-9A-Fa-f]{64}$ ]] || + fail "--expected-executable-sha256 must be 64 hex" +[[ "$expected_archive_sha" =~ ^[0-9A-Fa-f]{64}$ ]] || + fail "--expected-archive-sha256 must be 64 hex" +[[ "$expected_signing_certificate_sha1" =~ ^[0-9A-Fa-f]{40}$ ]] || + fail "--expected-signing-certificate-sha1 must be 40 hex" +[[ "$expected_profile_uuid" =~ ^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$ ]] || + fail "--expected-profile-uuid must be a UUID" +[[ -n "$expected_profile_name" && "$expected_profile_name" != *$'\n'* ]] || + fail "--expected-profile-name must be a nonempty single line" +[[ "$receipt" == /* ]] || fail "--receipt must be an absolute path" +[[ ! -e "$receipt" && ! -L "$receipt" ]] || + fail "--receipt must not already exist" +[[ -d "$(dirname "$receipt")" && ! -L "$(dirname "$receipt")" ]] || + fail "--receipt parent must be a real existing directory" + +require_executable "$CODESIGN_BIN" "codesign" +require_executable "$SECURITY_BIN" "security" +require_executable "$PYTHON_BIN" "Python" +require_executable "$SHASUM_BIN" "shasum" + +archive="$(cd "$archive" && pwd -P)" +readonly archive + +readonly ARCHIVE_INFO="$archive/Info.plist" +[[ -f "$ARCHIVE_INFO" && ! -L "$ARCHIVE_INFO" ]] || + fail "archive Info.plist is missing or unsafe" + +readonly APP_ROOT="$archive/Products/Applications" +[[ -d "$APP_ROOT" && ! -L "$APP_ROOT" ]] || + fail "archive Products/Applications directory is missing or unsafe" + +app_inventory="$( + find "$APP_ROOT" -mindepth 1 -maxdepth 1 -type d -name '*.app' -print +)" +app_count="$(printf '%s\n' "$app_inventory" | awk 'NF { count += 1 } END { print count + 0 }')" +[[ "$app_count" == "1" ]] || + fail "archive must contain exactly one top-level application" +readonly APP_PATH="$app_inventory" +[[ ! -L "$APP_PATH" ]] || fail "archived application must not be a symlink" + +readonly APP_INFO="$APP_PATH/Info.plist" +[[ -f "$APP_INFO" && ! -L "$APP_INFO" ]] || + fail "archived application Info.plist is missing or unsafe" + +read_plist_string() { + local plist="$1" + local key_path="$2" + + "$PYTHON_BIN" - "$plist" "$key_path" <<'PY' +import plistlib +import sys + +path, key_path = sys.argv[1:] +try: + with open(path, "rb") as source: + value = plistlib.load(source) + for component in key_path.split("."): + value = value[component] +except (OSError, KeyError, TypeError, plistlib.InvalidFileException): + raise SystemExit(2) +if not isinstance(value, str) or not value: + raise SystemExit(3) +print(value) +PY +} + +bundle_id="$(read_plist_string "$APP_INFO" CFBundleIdentifier)" || + fail "archived app lacks CFBundleIdentifier" +version="$(read_plist_string "$APP_INFO" CFBundleShortVersionString)" || + fail "archived app lacks CFBundleShortVersionString" +build="$(read_plist_string "$APP_INFO" CFBundleVersion)" || + fail "archived app lacks CFBundleVersion" +embedded_git_sha="$(read_plist_string "$APP_INFO" FearlessGitCommit)" || + fail "archived app lacks FearlessGitCommit provenance" +configuration="$(read_plist_string "$APP_INFO" FearlessBuildConfiguration)" || + fail "archived app lacks Release configuration attestation" +optimization="$(read_plist_string "$APP_INFO" FearlessSwiftOptimizationLevel)" || + fail "archived app lacks Swift optimization attestation" +testability="$(read_plist_string "$APP_INFO" FearlessEnableTestability)" || + fail "archived app lacks testability attestation" +executable_name="$(read_plist_string "$APP_INFO" CFBundleExecutable)" || + fail "archived app lacks CFBundleExecutable" + +[[ "$bundle_id" == "$EXPECTED_BUNDLE" ]] || + fail "archived bundle identifier is not the production identity" +[[ "$version" == "$EXPECTED_VERSION" ]] || + fail "archived marketing version is not $EXPECTED_VERSION" +[[ "$build" == "$expected_build" ]] || + fail "archived build number is not the expected fresh build" +[[ "$(lowercase "$embedded_git_sha")" == "$(lowercase "$expected_git_sha")" ]] || + fail "embedded git commit does not match the expected build commit" +[[ "$configuration" == "Release" ]] || + fail "archived app was not built with Release configuration" +[[ "$optimization" == "-O" ]] || + fail "archived app was not built with Swift -O" +[[ "$testability" == "NO" ]] || + fail "archived app has testability enabled" +[[ "$executable_name" =~ ^[A-Za-z0-9._-]+$ ]] || + fail "archived executable name is unsafe" + +readonly EXECUTABLE="$APP_PATH/$executable_name" +[[ -f "$EXECUTABLE" && ! -L "$EXECUTABLE" && -x "$EXECUTABLE" ]] || + fail "archived executable is missing, unsafe, or not executable" +actual_executable_sha="$("$SHASUM_BIN" -a 256 "$EXECUTABLE" | awk '{print $1}')" +[[ "$actual_executable_sha" =~ ^[0-9A-Fa-f]{64}$ ]] || + fail "could not compute archived executable SHA-256" +[[ "$(lowercase "$actual_executable_sha")" == "$(lowercase "$expected_executable_sha")" ]] || + fail "archived executable SHA-256 does not match build provenance" + +readonly HASH_SCRIPT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)/hash-ios-archive.sh" +[[ -x "$HASH_SCRIPT" ]] || fail "canonical archive hash helper is unavailable" +actual_archive_sha="$("$HASH_SCRIPT" "$archive")" || + fail "could not compute canonical archive SHA-256" +[[ "$(lowercase "$actual_archive_sha")" == "$(lowercase "$expected_archive_sha")" ]] || + fail "archive SHA-256 does not match build provenance" + +"$PYTHON_BIN" - "$ARCHIVE_INFO" "$APP_PATH" "$EXPECTED_BUNDLE" "$EXPECTED_TEAM" <<'PY' || +import os +import plistlib +import sys + +path, app_path, expected_bundle, expected_team = sys.argv[1:] +try: + with open(path, "rb") as source: + payload = plistlib.load(source) +except (OSError, plistlib.InvalidFileException): + raise SystemExit(2) +properties = payload.get("ApplicationProperties") +if not isinstance(properties, dict): + raise SystemExit(3) +expected_relative = os.path.relpath(app_path, os.path.dirname(path)) +if properties.get("ApplicationPath") != expected_relative: + raise SystemExit(4) +if properties.get("CFBundleIdentifier") != expected_bundle: + raise SystemExit(5) +if properties.get("Team") != expected_team: + raise SystemExit(6) +identity = properties.get("SigningIdentity") +if not isinstance(identity, str) or not ( + identity.startswith("Apple Distribution:") or + identity.startswith("iPhone Distribution:") +): + raise SystemExit(7) +PY + fail "xcarchive metadata does not bind the production app, team, and distribution identity" + +temporary_dir="$(mktemp -d "${TMPDIR:-/private/tmp}/fearless-signed-audit.XXXXXX")" +cleanup() { + rm -rf "$temporary_dir" +} +trap cleanup EXIT + +if ! "$CODESIGN_BIN" --verify --deep --strict "$APP_PATH" \ + >"$temporary_dir/codesign-verify" 2>&1; then + fail "archived app failed strict code-signature verification" +fi + +if ! "$CODESIGN_BIN" -d --entitlements :- --xml "$APP_PATH" \ + >"$temporary_dir/signed-entitlements.plist" \ + 2>"$temporary_dir/codesign-diagnostics"; then + fail "could not extract signed application entitlements" +fi +[[ -s "$temporary_dir/signed-entitlements.plist" ]] || + fail "signed application entitlements were empty" +if ! "$CODESIGN_BIN" -d \ + --extract-certificates="$temporary_dir/signing-certificate" \ + "$APP_PATH" >"$temporary_dir/certificate-output" \ + 2>"$temporary_dir/certificate-diagnostics"; then + fail "could not extract the application signing certificate" +fi +readonly SIGNING_CERTIFICATE="$temporary_dir/signing-certificate0" +[[ -f "$SIGNING_CERTIFICATE" && ! -L "$SIGNING_CERTIFICATE" ]] || + fail "application signature has no regular leaf certificate" +signing_certificate_sha1="$( + "$SHASUM_BIN" -a 1 "$SIGNING_CERTIFICATE" | awk '{print $1}' +)" +[[ "$signing_certificate_sha1" =~ ^[0-9A-Fa-f]{40}$ ]] || + fail "could not compute the application signing certificate SHA-1" +[[ "$(lowercase "$signing_certificate_sha1")" == "$(lowercase "$expected_signing_certificate_sha1")" ]] || + fail "application signing certificate does not match the expected release identity" + +readonly EMBEDDED_PROFILE="$APP_PATH/embedded.mobileprovision" +[[ -f "$EMBEDDED_PROFILE" && ! -L "$EMBEDDED_PROFILE" ]] || + fail "archived app has no regular embedded provisioning profile" +if ! "$SECURITY_BIN" cms -D -i "$EMBEDDED_PROFILE" \ + >"$temporary_dir/profile.plist" 2>"$temporary_dir/security-diagnostics"; then + fail "embedded provisioning profile could not be decoded" +fi + +"$PYTHON_BIN" - \ + "$temporary_dir/signed-entitlements.plist" \ + "$temporary_dir/profile.plist" \ + "$EXPECTED_TEAM" \ + "$EXPECTED_APPLICATION_ID" \ + "$EXPECTED_ASSOCIATED_DOMAINS_JSON" \ + "$EXPECTED_ICLOUD_CONTAINERS_JSON" \ + "$EXPECTED_ICLOUD_SERVICES_JSON" \ + "$EXPECTED_APP_GROUPS_JSON" \ + "$EXPECTED_PROFILE_KEYCHAIN_GROUPS_JSON" \ + "$MINIMUM_PROFILE_VALIDITY_DAYS" \ + "$signing_certificate_sha1" \ + "$expected_profile_uuid" \ + "$expected_profile_name" <<'PY' || +import datetime +import hashlib +import json +import plistlib +import sys + +( + entitlements_path, + profile_path, + expected_team, + expected_application_id, + associated_json, + containers_json, + services_json, + groups_json, + profile_keychain_groups_json, + minimum_validity_days_text, + signing_certificate_sha1, + expected_profile_uuid, + expected_profile_name, +) = sys.argv[1:] + +def load(path): + try: + with open(path, "rb") as source: + return plistlib.load(source) + except (OSError, plistlib.InvalidFileException): + raise SystemExit(2) + +signed = load(entitlements_path) +profile = load(profile_path) +if not isinstance(signed, dict) or not isinstance(profile, dict): + raise SystemExit(3) + +expected = { + "com.apple.developer.associated-domains": json.loads(associated_json), + "com.apple.developer.icloud-container-identifiers": json.loads(containers_json), + "com.apple.developer.icloud-services": json.loads(services_json), + "com.apple.security.application-groups": json.loads(groups_json), +} +for key, value in expected.items(): + if signed.get(key) != value: + raise SystemExit(4) +if signed.get("application-identifier") != expected_application_id: + raise SystemExit(5) +if signed.get("com.apple.developer.team-identifier") != expected_team: + raise SystemExit(6) +if signed.get("get-task-allow") is True: + raise SystemExit(7) + +allowed_signed_keys = set(expected) | { + "application-identifier", + "beta-reports-active", + "com.apple.developer.icloud-container-environment", + "com.apple.developer.team-identifier", + "get-task-allow", +} +if set(signed) - allowed_signed_keys: + raise SystemExit(8) +if ( + "beta-reports-active" in signed + and signed["beta-reports-active"] is not True +): + raise SystemExit(8) +if ( + "com.apple.developer.icloud-container-environment" in signed + and signed["com.apple.developer.icloud-container-environment"] != "Production" +): + raise SystemExit(8) + +def scalar_values(value): + if isinstance(value, dict): + for nested in value.values(): + yield from scalar_values(nested) + elif isinstance(value, (list, tuple)): + for nested in value: + yield from scalar_values(nested) + elif isinstance(value, str): + yield value + +for value in scalar_values(signed): + if value in { + "group.jp.co.soramitsu.fearlesswallet", + "group.com.walletconnect.sdk", + "group.jp.co.soramitsu.fearlesswallet.dev", + "iCloud.jp.co.soramitsu.fearless", + "iCloud.jp.co.soramitsu.fearlesswallet.dev", + }: + if value != "group.jp.co.soramitsu.fearlesswallet": + raise SystemExit(9) + +team_identifiers = profile.get("TeamIdentifier") +if team_identifiers != [expected_team]: + raise SystemExit(10) +if profile.get("TeamName") is not None and not isinstance(profile.get("TeamName"), str): + raise SystemExit(11) +if ( + not isinstance(profile.get("UUID"), str) + or profile["UUID"].lower() != expected_profile_uuid.lower() +): + raise SystemExit(12) +if profile.get("Name") != expected_profile_name: + raise SystemExit(13) +profile_certificates = profile.get("DeveloperCertificates") +if ( + not isinstance(profile_certificates, list) + or not profile_certificates + or not all(isinstance(certificate, bytes) for certificate in profile_certificates) +): + raise SystemExit(13) +profile_certificate_fingerprints = { + hashlib.sha1(certificate).hexdigest() + for certificate in profile_certificates +} +if signing_certificate_sha1.lower() not in profile_certificate_fingerprints: + raise SystemExit(13) +creation = profile.get("CreationDate") +expiration = profile.get("ExpirationDate") +if not isinstance(creation, datetime.datetime) or not isinstance(expiration, datetime.datetime): + raise SystemExit(14) +now = datetime.datetime.now(datetime.timezone.utc) +if creation.tzinfo is None: + creation = creation.replace(tzinfo=datetime.timezone.utc) +if expiration.tzinfo is None: + expiration = expiration.replace(tzinfo=datetime.timezone.utc) +try: + minimum_validity_days = int(minimum_validity_days_text) +except ValueError: + raise SystemExit(15) +minimum_expiration = now + datetime.timedelta(days=minimum_validity_days) +if creation >= expiration or expiration < minimum_expiration: + raise SystemExit(15) + +# App Store/TestFlight profiles do not enumerate development devices and are +# never enterprise profiles. +if "ProvisionedDevices" in profile or profile.get("ProvisionsAllDevices") is True: + raise SystemExit(16) +profile_entitlements = profile.get("Entitlements") +if not isinstance(profile_entitlements, dict): + raise SystemExit(17) +if profile_entitlements.get("application-identifier") != expected_application_id: + raise SystemExit(18) +if profile_entitlements.get("com.apple.developer.team-identifier") != expected_team: + raise SystemExit(19) +if profile_entitlements.get("get-task-allow") is not False: + raise SystemExit(20) +if profile_entitlements.get("beta-reports-active") is not True: + raise SystemExit(20) + +# The signed app must contain the exact least-privilege values above. Apple's +# App Store profile is an authorization envelope: associated domains and iCloud +# services are represented as "*" in the real production profile, while the +# container and app-group identities remain explicit. Validate that distinction +# instead of requiring the profile payload to be byte-shaped like the app's +# signed entitlements. +associated_authorization = profile_entitlements.get( + "com.apple.developer.associated-domains" +) +if associated_authorization not in ( + "*", + expected["com.apple.developer.associated-domains"], +): + raise SystemExit(21) +services_authorization = profile_entitlements.get( + "com.apple.developer.icloud-services" +) +if services_authorization not in ( + "*", + expected["com.apple.developer.icloud-services"], +): + raise SystemExit(21) +for key in ( + "com.apple.developer.icloud-container-identifiers", + "com.apple.security.application-groups", +): + if profile_entitlements.get(key) != expected[key]: + raise SystemExit(21) + +expected_containers = expected[ + "com.apple.developer.icloud-container-identifiers" +] +if profile_entitlements.get( + "com.apple.developer.icloud-container-development-container-identifiers" +) != expected_containers: + raise SystemExit(21) +if profile_entitlements.get( + "com.apple.developer.ubiquity-container-identifiers" +) != expected_containers: + raise SystemExit(21) +if profile_entitlements.get( + "com.apple.developer.ubiquity-kvstore-identifier" +) != f"{expected_team}.*": + raise SystemExit(21) + +profile_cloud_environments = profile_entitlements.get( + "com.apple.developer.icloud-container-environment" +) +if isinstance(profile_cloud_environments, str): + profile_cloud_environments = [profile_cloud_environments] +if ( + not isinstance(profile_cloud_environments, list) + or "Production" not in profile_cloud_environments + or not set(profile_cloud_environments).issubset({"Development", "Production"}) +): + raise SystemExit(21) +if profile_entitlements.get("keychain-access-groups") != json.loads( + profile_keychain_groups_json +): + raise SystemExit(21) +allowed_profile_keys = allowed_signed_keys | { + "com.apple.developer.icloud-container-development-container-identifiers", + "com.apple.developer.ubiquity-container-identifiers", + "com.apple.developer.ubiquity-kvstore-identifier", + "keychain-access-groups", +} +if set(profile_entitlements) - allowed_profile_keys: + raise SystemExit(22) +if ( + "beta-reports-active" in profile_entitlements + and profile_entitlements["beta-reports-active"] is not True +): + raise SystemExit(23) +PY + fail "signed entitlements or embedded distribution profile violate the production contract" + +# Recompute the canonical tree digest after every signature/profile read and +# immediately before publishing the receipt. This prevents a concurrently +# mutated archive from retaining the digest accepted at the start of the audit. +final_archive_sha="$("$HASH_SCRIPT" "$archive")" || + fail "could not recompute canonical archive SHA-256 before receipt publication" +[[ "$(lowercase "$final_archive_sha")" == "$(lowercase "$actual_archive_sha")" ]] || + fail "archive changed while signed identity and profile were audited" + +receipt_pending="${receipt}.pending.$$" +"$PYTHON_BIN" - \ + "$receipt_pending" \ + "$expected_git_sha" \ + "$bundle_id" \ + "$version" \ + "$build" \ + "$actual_executable_sha" \ + "$actual_archive_sha" \ + "$EXPECTED_TEAM" \ + "$signing_certificate_sha1" \ + "$expected_profile_uuid" \ + "$expected_profile_name" <<'PY' +import datetime +import json +import os +import sys + +( + path, + git_sha, + bundle_id, + version, + build, + executable_sha, + archive_sha, + team, + signing_certificate_sha1, + profile_uuid, + profile_name, +) = sys.argv[1:] +payload = { + "schemaVersion": 1, + "audit": "fearless-ios-signed-production-archive", + "auditedAtUTC": datetime.datetime.now(datetime.timezone.utc).isoformat(), + "gitCommit": git_sha.lower(), + "bundleIdentifier": bundle_id, + "marketingVersion": version, + "buildNumber": build, + "developmentTeam": team, + "executableSHA256": executable_sha.lower(), + "archiveTreeSHA256": archive_sha.lower(), + "signingCertificateSHA1": signing_certificate_sha1.lower(), + "provisioningProfileUUID": profile_uuid.lower(), + "provisioningProfileName": profile_name, + "distributionProfile": "valid-app-store", + "signedEntitlements": "exact-production-contract", +} +with open(path, "x", encoding="utf-8") as destination: + json.dump(payload, destination, indent=2, sort_keys=True) + destination.write("\n") + destination.flush() + os.fsync(destination.fileno()) +PY +mv "$receipt_pending" "$receipt" + +printf '%s\n' \ + "$LOG_PREFIX PASS: exact commit, bundle/version/build, executable/archive hashes, distribution profile, and signed production entitlements verified" diff --git a/scripts/ci/build-audited-ios-release-archive.sh b/scripts/ci/build-audited-ios-release-archive.sh new file mode 100644 index 0000000000..50d2e8de78 --- /dev/null +++ b/scripts/ci/build-audited-ios-release-archive.sh @@ -0,0 +1,175 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Builds and audits the only archive eligible for the 4.2.0 hotfix handoff. +# It derives provenance from an exact clean HEAD and never uploads anything. + +umask 077 + +readonly LOG_PREFIX="[ios-release-archive]" +readonly EXPECTED_TEAM="YLWWUD25VZ" +readonly EXPECTED_BUNDLE="jp.co.soramitsu.fearlesswallet" +readonly EXPECTED_VERSION="4.2.0" +readonly EXPECTED_SIGNING_IDENTITY="Apple Distribution: Soramitsu Co., Ltd. (YLWWUD25VZ)" +readonly EXPECTED_SIGNING_CERTIFICATE_SHA1="84AB95335BE14CAE9B050A353910F86FF2F9539B" +readonly EXPECTED_PROFILE_NAME="Fearless App Store 2026.7.26" +readonly EXPECTED_PROFILE_UUID="0d51265e-4b53-4a1f-814a-436dc9ca087b" +# Read-only App Store Connect inspection on 2026-07-26 found only 2026.7.14 +# and 2026.7.15 for 4.2.0. This candidate number was unused at inspection time. +readonly EXPECTED_BUILD="2026.7.26" +readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +readonly REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd -P)" + +fail() { + printf '%s ERROR: %s\n' "$LOG_PREFIX" "$*" >&2 + exit 1 +} + +usage() { + cat <<'USAGE' +Usage: + bash scripts/ci/build-audited-ios-release-archive.sh \ + --archive /absolute/new/path/fearless.xcarchive \ + --receipt /absolute/new/path/signed-archive-audit.json + +Preconditions: + - exact clean git HEAD, including no untracked files; + - App Store Connect read-only uniqueness check for 4.2.0 (2026.7.26); + - App Store distribution profile for the production App ID, with + group.jp.co.soramitsu.fearlesswallet and Apple default keychain groups. + +This command creates a local archive and receipt. It does not export, upload, +publish, tag, commit, or push. +USAGE +} + +require_option_value() { + local option="$1" + local remaining="$2" + [[ "$remaining" -ge 2 ]] || fail "$option requires a value" +} + +archive="" +receipt="" +while [[ "$#" -gt 0 ]]; do + case "$1" in + --archive) + require_option_value "$1" "$#" + archive="$2" + shift 2 + ;; + --receipt) + require_option_value "$1" "$#" + receipt="$2" + shift 2 + ;; + --help|-h) + usage + exit 0 + ;; + *) + fail "unknown argument: $1" + ;; + esac +done + +[[ "$archive" == /* && "$archive" == *.xcarchive ]] || + fail "--archive must be an absolute .xcarchive path" +[[ ! -e "$archive" && ! -L "$archive" ]] || + fail "--archive must not already exist" +[[ -d "$(dirname "$archive")" && ! -L "$(dirname "$archive")" ]] || + fail "--archive parent must be a real existing directory" +[[ "$receipt" == /* ]] || fail "--receipt must be an absolute path" +[[ ! -e "$receipt" && ! -L "$receipt" ]] || + fail "--receipt must not already exist" +[[ -d "$(dirname "$receipt")" && ! -L "$(dirname "$receipt")" ]] || + fail "--receipt parent must be a real existing directory" + +command -v git >/dev/null 2>&1 || fail "git is unavailable" +command -v xcodebuild >/dev/null 2>&1 || fail "xcodebuild is unavailable" +command -v shasum >/dev/null 2>&1 || fail "shasum is unavailable" +command -v security >/dev/null 2>&1 || fail "security is unavailable" +security find-identity -v -p codesigning 2>/dev/null | + grep -Fq "$EXPECTED_SIGNING_CERTIFICATE_SHA1 \"$EXPECTED_SIGNING_IDENTITY\"" || + fail "the exact reviewed Apple Distribution signing identity is not installed" + +cd "$REPO_ROOT" +[[ -z "$(git status --porcelain=v1 --untracked-files=all)" ]] || + fail "release archive requires an exact clean worktree, including no untracked files" +git diff --quiet --ignore-submodules -- || + fail "release archive source differs from HEAD" +git diff --cached --quiet --ignore-submodules -- || + fail "release archive index differs from HEAD" + +source_commit="$(git rev-parse --verify HEAD)" +[[ "$source_commit" =~ ^[0-9A-Fa-f]{40}$ ]] || + fail "could not derive an exact source commit" +git cat-file -e "${source_commit}^{commit}" || + fail "derived source provenance is not a commit" + +IOS_EXPECTED_BUILD_NUMBER="$EXPECTED_BUILD" \ +IOS_RELEASE_SOURCE_PACKAGES_DIR="${IOS_RELEASE_SOURCE_PACKAGES_DIR:-}" \ + bash "$SCRIPT_DIR/audit-ios-release-identity.sh" + +xcodebuild_arguments=( + -workspace "$REPO_ROOT/fearless.xcworkspace" + -scheme fearless + -configuration Release + -destination "generic/platform=iOS" + -archivePath "$archive" +) +if [[ -n "${IOS_RELEASE_SOURCE_PACKAGES_DIR:-}" ]]; then + xcodebuild_arguments+=( + -clonedSourcePackagesDirPath "$IOS_RELEASE_SOURCE_PACKAGES_DIR" + -disableAutomaticPackageResolution + -skipPackageUpdates + ) +fi +xcodebuild_arguments+=( + "CURRENT_PROJECT_VERSION=$EXPECTED_BUILD" + "MARKETING_VERSION=$EXPECTED_VERSION" + "FEARLESS_GIT_COMMIT=$source_commit" + "DEVELOPMENT_TEAM=$EXPECTED_TEAM" + "CODE_SIGN_STYLE=Manual" + "CODE_SIGN_IDENTITY=$EXPECTED_SIGNING_IDENTITY" + "PROVISIONING_PROFILE_SPECIFIER=$EXPECTED_PROFILE_NAME" + clean + archive +) + +printf '%s\n' \ + "$LOG_PREFIX building local Release archive from clean commit $source_commit" +xcodebuild "${xcodebuild_arguments[@]}" + +[[ "$(git rev-parse --verify HEAD)" == "$source_commit" ]] || + fail "HEAD changed while the release archive was built" +[[ -z "$(git status --porcelain=v1 --untracked-files=all)" ]] || + fail "tracked or untracked source state changed while the archive was built" + +app_path="$archive/Products/Applications/fearless.app" +[[ -d "$app_path" && ! -L "$app_path" ]] || + fail "archive did not produce exactly the expected application path" +executable_name="$( + /usr/libexec/PlistBuddy -c "Print :CFBundleExecutable" \ + "$app_path/Info.plist" 2>/dev/null +)" || fail "archive app has no executable identity" +[[ "$executable_name" =~ ^[A-Za-z0-9._-]+$ ]] || + fail "archive executable identity is unsafe" +executable_sha="$( + shasum -a 256 "$app_path/$executable_name" | awk '{print $1}' +)" +archive_sha="$(bash "$SCRIPT_DIR/hash-ios-archive.sh" "$archive")" + +bash "$SCRIPT_DIR/audit-ios-signed-release-artifact.sh" \ + --archive "$archive" \ + --expected-git-sha "$source_commit" \ + --expected-build "$EXPECTED_BUILD" \ + --expected-executable-sha256 "$executable_sha" \ + --expected-archive-sha256 "$archive_sha" \ + --expected-signing-certificate-sha1 "$EXPECTED_SIGNING_CERTIFICATE_SHA1" \ + --expected-profile-uuid "$EXPECTED_PROFILE_UUID" \ + --expected-profile-name "$EXPECTED_PROFILE_NAME" \ + --receipt "$receipt" + +printf '%s\n' \ + "$LOG_PREFIX PASS: local 4.2.0 ($EXPECTED_BUILD) archive is bound to clean HEAD $source_commit" diff --git a/scripts/ci/hash-ios-archive.sh b/scripts/ci/hash-ios-archive.sh new file mode 100755 index 0000000000..5e38004b68 --- /dev/null +++ b/scripts/ci/hash-ios-archive.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +set -euo pipefail + +umask 077 + +[[ "$#" == "1" ]] || { + printf '%s\n' "Usage: bash scripts/ci/hash-ios-archive.sh /absolute/path/to/archive.xcarchive" >&2 + exit 64 +} + +archive="$1" +[[ "$archive" == /* && "$archive" == *.xcarchive ]] || { + printf '%s\n' "archive path must be an absolute .xcarchive path" >&2 + exit 64 +} +[[ -d "$archive" && ! -L "$archive" ]] || { + printf '%s\n' "archive path must be a real non-symlink directory" >&2 + exit 1 +} + +python3 - "$archive" <<'PY' +import hashlib +import os +import stat +import sys + +root = os.path.realpath(sys.argv[1]) +digest = hashlib.sha256() + +def frame(value): + digest.update(str(len(value)).encode("ascii")) + digest.update(b":") + digest.update(value) + digest.update(b";") + +for directory, names, filenames in os.walk(root, topdown=True, followlinks=False): + names.sort() + filenames.sort() + relative_directory = os.path.relpath(directory, root) + for name in names + filenames: + path = os.path.join(directory, name) + relative = os.path.normpath(os.path.join(relative_directory, name)) + if relative.startswith("../") or relative == "..": + raise SystemExit(2) + encoded_path = relative.encode("utf-8") + metadata = os.lstat(path) + if stat.S_ISDIR(metadata.st_mode): + kind = b"d" + payload = b"" + elif stat.S_ISREG(metadata.st_mode): + kind = b"f" + with open(path, "rb") as source: + content_digest = hashlib.sha256() + for chunk in iter(lambda: source.read(1024 * 1024), b""): + content_digest.update(chunk) + payload = content_digest.digest() + elif stat.S_ISLNK(metadata.st_mode): + kind = b"l" + payload = os.readlink(path).encode("utf-8") + else: + raise SystemExit(3) + frame(kind) + frame(encoded_path) + frame(oct(stat.S_IMODE(metadata.st_mode)).encode("ascii")) + frame(payload) + +print(digest.hexdigest()) +PY diff --git a/scripts/ci/manifests/coredata-release-copied-phone-tests.txt b/scripts/ci/manifests/coredata-release-copied-phone-tests.txt new file mode 100644 index 0000000000..72d906fd12 --- /dev/null +++ b/scripts/ci/manifests/coredata-release-copied-phone-tests.txt @@ -0,0 +1,3 @@ +# Exact XCTest identities required by the copied-phone Release/-O gate. +fearlessTests/SubstrateStorageClassResolutionTests/testCopiedPhoneV8Store_whenAvailable_thenAllRowsHaveExpectedClassesAndStoreIsUnchanged +fearlessTests/SubstrateStorageClassResolutionTests/testCopiedPhoneV8Store_whenFetchedThroughProductionRepositories_thenMapsEveryRuntimeAndChain diff --git a/scripts/ci/manifests/coredata-release-core-tests.txt b/scripts/ci/manifests/coredata-release-core-tests.txt new file mode 100644 index 0000000000..e8a03b7a7f --- /dev/null +++ b/scripts/ci/manifests/coredata-release-core-tests.txt @@ -0,0 +1,414 @@ +# Exact XCTest identities required by the Release/-O Core Data gate. +# Generated from, and reviewed against, the 13 explicitly selected test classes. +fearlessTests/SingleToMultiassetUserMigrationTests/testMigrationForCreatedAccountWithoutDerivPath +fearlessTests/SingleToMultiassetUserMigrationTests/testMigrationForCreatedAccountWithDerivPath +fearlessTests/SingleToMultiassetUserMigrationTests/testMigrationForImportedWithSeedAccountWithoutDerivPath +fearlessTests/SingleToMultiassetUserMigrationTests/testMigrationForImportedWithSeedAccountWithDerivPath +fearlessTests/SingleToMultiassetUserMigrationTests/testMigrationForImportedWithJSONAccountWithoutDerivPath +fearlessTests/SingleToMultiassetUserMigrationTests/testMigrationForImportedWithJSONAccountWithDerivPath +fearlessTests/SingleToMultiassetUserMigrationTests/testMigrationFromV1ToCurrentForCreatedAccountWithDerivationPath +fearlessTests/SingleToMultiassetUserMigrationTests/testMigrationFromV1ToCurrentForImportedJSONAccount +fearlessTests/SingleToMultiassetUserMigrationTests/testDuplicateAccountMigration_whenWatchOnlyRowIsInsertedFirst_thenSigningSecretsArePreserved +fearlessTests/SingleToMultiassetUserMigrationTests/testDuplicateAccountMigration_whenSigningRowIsInsertedFirst_thenSigningSecretsArePreserved +fearlessTests/SingleToMultiassetUserMigrationTests/testDuplicateAccountMigration_whenFirstAliasHasConflictingSecret_thenFailsWithoutReplacingStore +fearlessTests/SingleToMultiassetUserMigrationTests/testDuplicateAccountMigration_whenSecondAliasHasConflictingSecret_thenFailsWithoutReplacingStore +fearlessTests/SingleToMultiassetUserMigrationTests/testKeystoreResourceLimits_atEveryExactBoundary_prepareSucceeds +fearlessTests/SingleToMultiassetUserMigrationTests/testKeystoreResourceLimits_whenLegacyRowLimitExceeded_freezesAndFailsBeforePrepareWrites +fearlessTests/SingleToMultiassetUserMigrationTests/testKeystoreResourceLimits_whenStagedKeyCountExceeded_freezesAndFailsBeforePrepareWrites +fearlessTests/SingleToMultiassetUserMigrationTests/testKeystoreResourceLimits_whenIdentifierBytesExceeded_freezesAndFailsBeforePrepareWrites +fearlessTests/SingleToMultiassetUserMigrationTests/testKeystoreResourceLimits_whenStagedKeyBytesExceeded_freezesAndFailsBeforePrepareWrites +fearlessTests/SingleToMultiassetUserMigrationTests/testKeystoreResourceLimits_whenDeletionCountExceeded_freezesAndFailsBeforePrepareWrites +fearlessTests/SingleToMultiassetUserMigrationTests/testKeystoreResourceLimits_whenEncodedJournalExceeded_failsBeforeJournalOrDestinationWrites +fearlessTests/SingleToMultiassetUserMigrationTests/testCleanupRecovery_whenEncodedJournalExceedsProductionLimit_rejectsBeforeDecodeOrMutation +fearlessTests/SingleToMultiassetUserMigrationTests/testV1Migration_whenLegacyRowLimitExceeded_preservesLiveStoreAndKeychain +fearlessTests/SingleToMultiassetUserMigrationTests/testV1Migration_whenNonthrowingStagingLimitExceeded_preservesLiveStoreAndMakesNoKeychainWrites +fearlessTests/SingleToMultiassetUserMigrationTests/testV1Migration_whenEntropyExceedsStagedByteLimit_abortsBeforeCryptographicDerivation +fearlessTests/UserStorageCompatibilityMigrationTests/testCrashConsistentReplacement_whenProcessDiesAtEveryCommitBoundary_thenBothStoreFamiliesRecoverExactly +fearlessTests/UserStorageCompatibilityMigrationTests/testCrashConsistentReplacement_whenProcessDiesAtEveryRollbackFileBoundary_thenBothStoreFamiliesRestoreExactly +fearlessTests/UserStorageCompatibilityMigrationTests/testCrashConsistentReplacement_whenProcessDiesDuringEveryCleanupDeletion_thenPreparingRollbackAndCommittedStatesConverge +fearlessTests/UserStorageCompatibilityMigrationTests/testCrashConsistentReplacement_whenProcessDiesDuringEachLowLevelFamilyMutation_thenBothStoreFamiliesRestoreExactly +fearlessTests/UserStorageCompatibilityMigrationTests/testCrashConsistentReplacement_whenFirstMarkerWriteIsInterrupted_thenOnlyExactSafePendingTreeIsCleaned +fearlessTests/UserStorageCompatibilityMigrationTests/testCrashConsistentReplacement_whenPreparingBackupCopyIsPartial_thenCleansOnlyWithExactOriginalLiveFamily +fearlessTests/UserStorageCompatibilityMigrationTests/testCrashConsistentReplacement_whenMarkerIsAdversarial_thenFailsClosedWithoutChangingLiveFamily +fearlessTests/UserStorageCompatibilityMigrationTests/testCrashConsistentReplacement_whenTransactionTreeContainsUnknownFileOrDirectory_thenFailsClosed +fearlessTests/UserStorageCompatibilityMigrationTests/testCrashConsistentReplacement_whenCommittedFamilyOrSemanticValidationChanges_thenRestoresOriginalFamily +fearlessTests/UserStorageCompatibilityMigrationTests/testCrashConsistentReplacement_whenCommittedValidationMutatesItsStoreAndCleanupIsInterrupted_thenLiveCommitStillConverges +fearlessTests/UserStorageCompatibilityMigrationTests/testCrashConsistentReplacement_whenMarkerBackupOrLiveMemberIsSymlink_thenFailsClosed +fearlessTests/UserStorageCompatibilityMigrationTests/testCrashConsistentReplacement_whenLiveFamilyIsUnsafeOrOrphaned_thenNeverReportsAnEmptyStore +fearlessTests/UserStorageCompatibilityMigrationTests/testCrashConsistentReplacement_whenStagedFamilyIsUnsafeOrAliasesProtectedPaths_thenRejectsBeforeMutation +fearlessTests/UserStorageCompatibilityMigrationTests/testCrashConsistentReplacement_whenPreMarkerDirectoryIsEmpty_thenSafelyCleansItOnlyAfterValidatingLiveFamily +fearlessTests/UserStorageCompatibilityMigrationTests/testCrashConsistentReplacement_whenPathsShareDirectory_thenUserAndSubstrateTransactionsCannotCollide +fearlessTests/UserStorageCompatibilityMigrationTests/testCompatibilityModels_whenBundled_thenHaveExactFingerprintsAndMigrationEdges +fearlessTests/UserStorageCompatibilityMigrationTests/testCurrentModel_whenLoaded_thenEveryEntityResolvesToTheAppRuntimeClass +fearlessTests/UserStorageCompatibilityMigrationTests/testMigration_whenVersion2PersistentFixture_thenPreservesWalletsAndAccountsAndRoutesExistingPincodeToPinOnRepeatedLaunch +fearlessTests/UserStorageCompatibilityMigrationTests/testPerformMigration_whenIntermediateV3StoreDropsWallet_thenPerHopCountFailsClosedAndRetryPreservesFixture +fearlessTests/UserStorageCompatibilityMigrationTests/testPerformMigration_whenIntermediateV3StoreDropsChainAccountButWalletCountIsUnchanged_thenPerHopIntegrityFailsClosed +fearlessTests/UserStorageCompatibilityMigrationTests/testPerformMigration_whenIntermediateV3StoreMutatesWalletFieldWithoutChangingCount_thenPerHopIntegrityFailsClosed +fearlessTests/UserStorageCompatibilityMigrationTests/testMigration_whenLegacyFieldsAreIntentionallyDerived_thenPreservesExactAssetVisibilityAndNetworkFilterSemantics +fearlessTests/UserStorageCompatibilityMigrationTests/testMigration_whenIntentionalDerivedFieldsAreDroppedOrMutatedInStaging_thenSemanticIntegrityFailsClosed +fearlessTests/UserStorageCompatibilityMigrationTests/testMigration_whenV1ToV2DropsDestinationAccount_thenLegacyAccountSemanticIntegrityFailsClosed +fearlessTests/UserStorageCompatibilityMigrationTests/testMigration_whenLegacyEcosystemStore_thenPreservesWalletTonAssetAndChainData +fearlessTests/UserStorageCompatibilityMigrationTests/testMigration_whenPublicV11Store_thenPreservesWalletAssetAndEthereumChainData +fearlessTests/UserStorageCompatibilityMigrationTests/testMigration_whenV11OrLegacyV12PreferenceArchiveIsInvalid_thenDefaultsOnlyDamagedField +fearlessTests/UserStorageCompatibilityMigrationTests/testMigration_whenPreferenceArchiveHasWrongDecodedType_thenDefaultsOnlyDamagedField +fearlessTests/UserStorageCompatibilityMigrationTests/testMigration_whenPreferenceArchivesAreValid_thenPreservesEveryValue +fearlessTests/UserStorageCompatibilityMigrationTests/testRawArchivePreflight_whenValuesCrossByteBoundary_thenRepairsOnlyOversizedArchivesAndIsIdempotent +fearlessTests/UserStorageCompatibilityMigrationTests/testRawArchivePreflight_whenRequiredReplacementExceedsLimit_thenRollsBackWithoutMutation +fearlessTests/UserStorageCompatibilityMigrationTests/testMigration_whenRawPreferenceArchiveExceedsProductionLimit_thenRepairsBeforeCoreDataDecode +fearlessTests/UserStorageCompatibilityMigrationTests/testMigration_whenDecodedPreferenceLimitsAreCrossed_thenRepairsOverLimitAndPreservesExactBoundaries +fearlessTests/UserStorageCompatibilityMigrationTests/testMigration_whenCurrentVersionRequiredArchiveExceedsRawLimit_thenAtomicallyRepairsAndSubsequentRunIsNoOp +fearlessTests/UserStorageCompatibilityMigrationTests/testMigration_whenCurrentVersionOptionalArchiveHasWrongSQLiteType_thenAtomicallyRepairsToNullAndSubsequentRunIsNoOp +fearlessTests/UserStorageCompatibilityMigrationTests/testMigration_whenCurrentVersionDecodedPreferenceExceedsLimit_thenCountsRepairAndSubsequentRunIsNoOp +fearlessTests/UserStorageCompatibilityMigrationTests/testMigration_whenCurrentVersionRepairFitsCopyLimitButFinalBackupCapacityIsInsufficient_thenPreservesLiveStore +fearlessTests/UserStorageCompatibilityMigrationTests/testMigration_whenCurrentVersionRepairValidationHookDropsWallet_thenRejectsMutatedBaselineBeforeReplacement +fearlessTests/UserStorageCompatibilityMigrationTests/testRelationshipCountPreflight_whenRelationshipIsFaulted_thenCountsWithoutMaterializingIt +fearlessTests/UserStorageCompatibilityMigrationTests/testRelationshipAggregateCounter_whenCountReachesExactBoundary_thenAcceptsIt +fearlessTests/UserStorageCompatibilityMigrationTests/testRelationshipAggregateCounter_whenLimitOrIntegerRangeIsExceeded_thenFailsWithoutChangingTotal +fearlessTests/UserStorageCompatibilityMigrationTests/testMigration_whenAggregateRelationshipDestinationsExceedStoreLimit_thenFailsBeforeStoreReplacement +fearlessTests/UserStorageCompatibilityMigrationTests/testMigration_whenRelationshipExceedsDestinationLimit_thenFailsBeforeStoreReplacement +fearlessTests/UserStorageCompatibilityMigrationTests/testMigration_whenVersion1AccountRowLimitIsExceeded_thenFailsBeforeKeyOrStoreReplacement +fearlessTests/UserStorageCompatibilityMigrationTests/testMigration_whenVersion1RowsExceedKeystoreProductionCap_thenFailsBeforeMigrationPolicyOrKeyStaging +fearlessTests/UserStorageCompatibilityMigrationTests/testMigration_whenVersion1SourceFamilyExceedsCopyLimit_thenFailsBeforeCapacityKeyOrStoreReplacement +fearlessTests/UserStorageCompatibilityMigrationTests/testMigration_whenPrivateRepairSucceedsButReplacementFails_thenOnlyStagedFieldChangesAndSourceRemainsByteIdentical +fearlessTests/UserStorageCompatibilityMigrationTests/testMigration_whenPrivateRepairRowCapIsExceeded_thenRejectsBeforePagingOrReplacement +fearlessTests/UserStorageCompatibilityMigrationTests/testMigration_whenPrivateRepairRowsSpanSingleIDPages_thenEnumeratesEveryWalletExactlyOnce +fearlessTests/UserStorageCompatibilityMigrationTests/testPerformMigration_whenStagedStoreDropsChainAccount_thenRefusesReplacementAndLeavesSourceUnchanged +fearlessTests/UserStorageCompatibilityMigrationTests/testPerformMigration_whenIntegrityRowBudgetIsExceeded_thenFailsBeforeReplacementAndLeavesSourceUnchanged +fearlessTests/UserStorageCompatibilityMigrationTests/testPerformMigration_whenCanonicalIdentityByteBudgetIsExceeded_thenFailsBeforeReplacementAndLeavesSourceUnchanged +fearlessTests/UserStorageCompatibilityMigrationTests/testPerformMigration_whenStagedStoreMutatesSameCountGlobalContentOrRelationships_thenRefusesReplacementAndLeavesSourceUnchanged +fearlessTests/UserStorageCompatibilityMigrationTests/testPerformMigration_whenWALSourceReplacementFails_thenOriginalDurableFilesAreByteForByteUnchanged +fearlessTests/UserStorageCompatibilityMigrationTests/testCopyStoreFamily_whenRollbackJournalExists_thenCopiesRecoveryStateWithoutSourceShmOrMutation +fearlessTests/UserStorageCompatibilityMigrationTests/testMetadataProbe_whenObjectiveCExceptionIsRaised_thenFailsClosedOnPrivateCopiesAndNeverMutatesSource +fearlessTests/UserStorageCompatibilityMigrationTests/testMetadataProbe_whenSourceHasHotRollbackJournal_thenRecoversOnlyPrivateCopyAndLeavesSourceFamilyByteIdentical +fearlessTests/UserStorageCompatibilityMigrationTests/testSynchronousMigration_whenStoreIsCurrent_thenUsesSinglePrivateMetadataProbe +fearlessTests/UserStorageCompatibilityMigrationTests/testPerformMigration_whenStoreAlreadyCurrent_thenRepeatedCallsAreByteForByteIdempotent +fearlessTests/UserStorageCompatibilityMigrationTests/testPerformMigration_whenStoreIsCorrupt_thenThrowsAndDoesNotModifyFiles +fearlessTests/UserStorageCompatibilityMigrationTests/testPerformMigration_whenStoreModelIsUnknown_thenThrowsAndDoesNotModifyFiles +fearlessTests/UserStorageCompatibilityMigrationTests/testPerformMigration_whenProcessDiesAfterRemovingLiveMainStore_thenRelaunchRestoresAndMigratesProtectedWallets +fearlessTests/UserStorageCompatibilityMigrationTests/testPerformMigration_whenStoreReplacementFails_thenProtectedDataIsNeverFinalized +fearlessTests/UserStorageCompatibilityMigrationTests/testPerformMigration_whenStoreReplacementSucceeds_thenProtectedDataFinalizesAfterward +fearlessTests/UserStorageCompatibilityMigrationTests/testSettingsCleanup_whenReplacementFailsBeforeCommit_thenRelaunchRollsBackJournalWithoutDeletingLegacySelections +fearlessTests/UserStorageCompatibilityMigrationTests/testSettingsCleanup_whenProcessStopsAfterDatabaseReplacement_thenCurrentVersionRelaunchFinishesCleanup +fearlessTests/UserStorageCompatibilityMigrationTests/testSettingsCleanup_whenFinalizeFails_thenErrorPropagatesAndCurrentVersionRelaunchRetriesIdempotently +fearlessTests/UserStorageCompatibilityMigrationTests/testSettingsCleanup_whenJournalIsValid_thenDeletesOnlyExactLegacySelections +fearlessTests/UserStorageCompatibilityMigrationTests/testSettingsCleanup_whenJournalTargetsUnrelatedKey_thenFailsClosedAndPreservesEverything +fearlessTests/UserStorageCompatibilityMigrationTests/testSettingsCleanup_whenJournalHasDuplicateField_thenFailsClosed +fearlessTests/UserStorageCompatibilityMigrationTests/testSettingsCleanup_whenJournalHasUnknownField_thenFailsClosed +fearlessTests/UserStorageCompatibilityMigrationTests/testSettingsCleanup_whenJournalIsOversized_thenFailsBeforeDeletingAnything +fearlessTests/UserStorageCompatibilityMigrationTests/testPerformMigration_whenKeychainReadFails_thenPropagatesAndLeavesLegacyStoreUntouched +fearlessTests/UserStorageCompatibilityMigrationTests/testPerformMigration_whenReplacementKeySaveFails_thenNeverReplacesStoreOrDeletesLegacyKey +fearlessTests/UserStorageCompatibilityMigrationTests/testPerformMigration_whenStoreReplacementFails_thenKeepsOldAndStagedKeysUntilSafeRollback +fearlessTests/UserStorageCompatibilityMigrationTests/testPerformMigration_whenPostReplacementCleanupFails_thenFailsAndRelaunchFinishesCleanupWithoutLosingNewKey +fearlessTests/UserStorageCompatibilityMigrationTests/testMigrate_whenDestinationSecretVerificationFails_thenReturnsFailureAndRetryConvergesOnlyAfterRepair +fearlessTests/UserStorageCompatibilityMigrationTests/testPerformMigration_whenCurrentStoreCleanupJournalIsCorrupt_thenLaunchFailsClosed +fearlessTests/UserStorageCompatibilityMigrationTests/testPerformMigration_whenCurrentStoreCleanupJournalCannotBeRead_thenBlocksAndRetriesWithoutDeletingKeys +fearlessTests/UserStorageCompatibilityMigrationTests/testPerformMigration_whenCurrentStoreDestinationKeyIsMissing_thenBlocksUntilKeyIsRestored +fearlessTests/UserStorageCompatibilityMigrationTests/testPerformMigration_whenCurrentStoreDestinationProofIsTampered_thenBlocksUntilJournalIsRestored +fearlessTests/UserStorageCompatibilityMigrationTests/testPerformMigration_whenSourceStoreHasLegacyCleanupJournal_thenRerunsWithExistingStagedKey +fearlessTests/UserStorageCompatibilityMigrationTests/testMigrate_whenDestinationStoreHasLegacyCleanupJournal_thenClearsOnlyJournal +fearlessTests/UserStorageCompatibilityMigrationTests/testCleanupRecovery_whenLegacyJournalContainsReservedIdentifier_thenFailsClosed +fearlessTests/UserStorageCompatibilityMigrationTests/testCleanupRecovery_whenVerifiedJournalDoesNotBracketDatabaseVersion_thenFailsClosed +fearlessTests/UserStorageCompatibilityMigrationTests/testCleanupRecovery_whenLegacyJournalDoesNotBracketDatabaseVersion_thenFailsClosed +fearlessTests/UserStorageCompatibilityMigrationTests/testMigrate_whenStoreDoesNotExist_thenReturnsWithoutReadingKeychain +fearlessTests/UserStorageCompatibilityMigrationTests/testKeystoreMigrator_whenLegacyKeyReadFails_thenErrorIsNotTreatedAsMissingKey +fearlessTests/UserStorageCompatibilityMigrationTests/testKeystoreMigrator_whenReplacementIdentifierContainsDifferentKey_thenRefusesToOverwriteIt +fearlessTests/UserStorageCompatibilityMigrationTests/testKeystoreMigrator_whenJournalDeletionFails_thenRecoveryCanRepeatWithoutDeletingNewKey +fearlessTests/UserStorageCompatibilityMigrationTests/testCleanupRecovery_whenDestinationKeyIsMissing_thenRetainsLegacyKeyAndJournal +fearlessTests/UserStorageCompatibilityMigrationTests/testCleanupRecovery_whenDestinationKeyHasDrifted_thenRetainsLegacyKeyAndJournal +fearlessTests/UserStorageCompatibilityMigrationTests/testCleanupRecovery_whenDestinationDigestIsTampered_thenRetainsAllKeysAndJournal +fearlessTests/UserStorageCompatibilityMigrationTests/testCleanupRecovery_whenDeletionListContainsReservedJournalIdentifier_thenFailsClosed +fearlessTests/UserStorageCompatibilityMigrationTests/testCleanupRecovery_whenDeletionListContainsDestinationIdentifier_thenFailsClosed +fearlessTests/UserStorageCompatibilityMigrationTests/testCleanupRecovery_whenDeletionListContainsPincode_thenFailsClosedWithoutDeletingIt +fearlessTests/UserStorageCompatibilityMigrationTests/testCleanupRecovery_whenDeletionListContainsUnrelatedAppKey_thenFailsClosedWithoutDeletingIt +fearlessTests/UserStorageCompatibilityMigrationTests/testCleanupRecovery_whenRollbackStagesPincodeWithMatchingProof_thenFailsClosedWithoutDeletingIt +fearlessTests/UserStorageCompatibilityMigrationTests/testMetaAccountMapper_whenMigratedRecordHasOnlyTonFields_thenThrowsRecoverableError +fearlessTests/UserStorageCompatibilityMigrationTests/testMetaAccountMapper_whenEvmAccount_thenWritesCanonicalIdentifierAndReadsCanonicalAndLegacyAliases +fearlessTests/SubstrateStorageClassResolutionTests/testBundledV8Model_whenLoaded_thenEveryEntityResolvesToExactRuntimeClass +fearlessTests/SubstrateStorageClassResolutionTests/testBundledV8Model_whenObjectsInsertedAndFetched_thenUsesExactRuntimeClasses +fearlessTests/SubstrateStorageClassResolutionTests/testRuntimeMetadata_whenPersisted_thenCodableMapperRoundTripsBoundaryVersions +fearlessTests/SubstrateStorageClassResolutionTests/testRuntimeMetadataStartupMapper_whenGetterRaises_thenReturnsSanitizedSwiftError +fearlessTests/SubstrateStorageClassResolutionTests/testRuntimeMetadataStartupMapper_whenSetterRaises_thenReturnsSanitizedSwiftError +fearlessTests/SubstrateStorageClassResolutionTests/testFallbackCodableEntities_whenPersisted_thenTypedAccessorsAndMappersRoundTrip +fearlessTests/SubstrateStorageClassResolutionTests/testFallbackCodableEntities_whenRequiredFieldsAreMissing_thenMappersRejectRows +fearlessTests/SubstrateStorageClassResolutionTests/testBundledV8Model_whenStoreCreated_thenSchemaFingerprintsAndCompatibilityStayCanonical +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenStoreDoesNotExist_thenDoesNotCreateOrModifyAnything +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenStoreIsCurrent_thenRepeatedCallsAreByteForByteIdempotent +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenCurrentStoreHasOversizedRawArchive_thenRepairsPrivateCopyAtomicallyAndPreservesProtectedGraph +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenCurrentStoreHasDecodedWrongTypeArchive_thenRepairsPrivateCopyAndPreservesOtherValues +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenCleanCurrentStartupGraphIsExactlyAtEveryRelationshipLimit_thenAuditsWithoutHookReplacementOrMutation +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenCleanCurrentStartupRootsAreExactlyAtRowLimits_thenAuditsWithoutHookReplacementOrMutation +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenCleanCurrentRuntimeRootExceedsRowLimit_thenRejectsBeforeHookReplacementOrMutation +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenCleanCurrentRootPayloadIsExactlyAtByteLimit_thenAuditsWithoutHookReplacementOrMutation +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenCleanCurrentRootPayloadExceedsByteLimit_thenRejectsBeforeHookReplacementOrMutation +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenCurrentPriceStringIsExactlyAtStartupChildValueLimit_thenAuditsWithoutMutation +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenCurrentPriceStringExceedsStartupChildValueLimit_thenRejectsBeforeCoreDataOrMutation +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenCurrentAssetNameExceedsStartupChildValueLimit_thenRejectsBeforeCoreDataOrMutation +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenCurrentCustomNodeNameExceedsStartupChildValueLimit_thenRejectsBeforeCoreDataOrMutation +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenCurrentExplorerURLExceedsStartupChildValueLimit_thenRejectsBeforeCoreDataOrMutation +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenCurrentXcmConfigVersionExceedsStartupChildValueLimit_thenRejectsBeforeCoreDataOrMutation +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenCurrentXcmDestinationChainIdExceedsStartupChildValueLimit_thenRejectsBeforeCoreDataOrMutation +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenCurrentXcmAssetIdentifierExceedsStartupChildValueLimit_thenRejectsBeforeCoreDataOrMutation +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenCurrentPriceProviderIdentifierExceedsStartupChildValueLimit_thenRejectsBeforeCoreDataOrMutation +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenCurrentChildPayloadAggregateIsExactlyAtLimit_thenAuditsWithoutMutation +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenCurrentChildPayloadAggregateExceedsLimitByOne_thenRejectsBeforeCoreDataOrMutation +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenCleanCurrentNodesExceedRelationshipLimit_thenRejectsWhileFaultedBeforeHookReplacementOrMutation +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenCleanCurrentPriceDataExceedsRelationshipLimit_thenRejectsWhileFaultedBeforeHookReplacementOrMutation +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenCleanCurrentXcmRelationshipsExceedAggregateLimit_thenRejectsBeforeHookReplacementOrMutation +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenRepairedCurrentStageIntroducesOversizedPriceDataRelationship_thenRejectsBeforeReplacement +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenCurrentStoreHasLargeSharedMemorySidecar_thenUsesOneDisposableCopyAndCleansIt +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenSourceFamilyExceedsPrivateCopyLimit_thenRejectsBeforeCapacityInspectionOrMutation +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenPrivateCopyCapacityIsInsufficient_thenRejectsBeforeCopyOrMutation +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenStoreIsCorrupt_thenThrowsAndPreservesEveryStoreFile +fearlessTests/SubstrateStorageMigrationSafetyTests/testSyncMigration_whenStoreIsCorrupt_thenFailsClosedAndPreservesEverySidecar +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenStoreVersionIsUnknown_thenThrowsAndPreservesEveryStoreFile +fearlessTests/SubstrateStorageMigrationSafetyTests/testSyncMigration_whenStoreVersionIsUnknown_thenFailsClosedAndPreservesEveryStoreFile +fearlessTests/SubstrateStorageMigrationSafetyTests/testSyncMigration_whenKnownStoreCannotBeInspected_thenFailsClosedAndPreservesEveryStoreFile +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenTargetModelIsMissing_thenThrowsBeforeCheckpointAndPreservesSource +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenAskedToDowngrade_thenThrowsBeforeCheckpointAndPreservesSource +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenVersion7Store_thenStagesValidVersion8AndPreservesRows +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenEverySupportedLegacyVersion_thenReachesV8AndPreservesRows +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenOldestLegacyStore_thenNeverRetainsCompletedIntermediates +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenTemporaryInputCleanupFails_thenAbortsAndCleansEveryPrivateStore +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenVersion1ChainHasNoSelectedNode_thenSelectsStableNode +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenVersion3ContainsChainGraph_thenUsesCustomV4Policy +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenV3ChainOptionsArchiveIsRawGarbage_thenDefaultsOnlyThatCache +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenV3ExternalTypesArchiveIsEmpty_thenDefaultsOnlyThatCache +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenV3PurchaseProvidersArchiveIsTruncated_thenV4PolicyStillPreservesGraph +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenV4AssetPurchaseProvidersArchiveIsRawGarbage_thenDefaultsOnlyAssetCache +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenBothV3PolkaswapArchivesHaveWrongTypes_thenDefaultsBothRequiredArrays +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenV4LegacyXcmArchiveContainsDisallowedClass_thenDefaultsConfigAndPreservesDestination +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenV3LegacyDestinationAssetsArchiveIsInvalid_thenDefaultsDestinationAndPreservesConfig +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenAllV3TransformableArchivesAreValid_thenPreservesEveryValueAndProtectedRelationship +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenEveryV3TransformableArchiveIsDamagedTogether_thenRepairsEveryKeyAndPreservesProtectedGraph +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenTransformableElementCountIsExactlyAtLimit_thenPreservesArray +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenTransformableElementCountExceedsLimit_thenRepairsArray +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenTransformableUTF8BytesAreExactlyAtLimit_thenPreservesArray +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenTransformableUTF8BytesExceedLimit_thenRepairsArray +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenRawTransformableArchiveIsExactlyAtLimit_thenPreservesArray +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenRawTransformableArchiveExceedsLimit_thenRepairsBeforeCoreDataDecode +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenOptionalTransformableUsesWrongSQLiteStorageClass_thenRepairsBeforeDecode +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenRequiredTransformableUsesWrongSQLiteStorageClass_thenWritesValidEmptyArchive +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigrationWithRecovery_whenTransformableEntityExceedsRowCap_thenRejectsBeforePagingReplacementOrQuarantine +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenBatchRepairsResetContext_thenImmutableIDCohortRepairsEveryRowExactlyOnce +fearlessTests/SubstrateStorageMigrationSafetyTests/testSyncMigration_whenFinalReplacementFails_thenSourceFamilyAndRelationshipsRemainExact +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenProcessDiesAfterRemovingLiveMainStore_thenRelaunchRestoresAndMigratesProtectedGraph +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenStagedProtectedValuesAndSelectedNodeAreSwappedWithoutRowLoss_thenRejectsStageAndPreservesSource +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenProtectedEntityExceedsRowLimit_thenFailsClosedBeforeMigration +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenProtectedEntitiesExceedTotalRowLimit_thenFailsClosedBeforePaging +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenChainRelationshipExceedsMemberLimit_thenFailsClosedBeforeDigestingTopology +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenNoInverseRelationshipExceedsMemberLimit_thenRejectsWhileRelationshipRemainsFaulted +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenAggregateRelationshipLimitIsExceeded_thenRejectsBeforeEitherRelationshipMaterializes +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenProtectedValueExceedsByteLimit_thenFailsClosedWithoutReplacingStore +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenBoundedRelationshipMemberExceedsByteLimit_thenFailsBeforeReplacement +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenProtectedRowsSpanSingleItemPages_thenDigestPreservesEveryRecord +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenLegacyAssetHasNoSymbolAndWrapperHasNoAsset_thenPreservesGraph +fearlessTests/SubstrateStorageMigrationSafetyTests/testChainV4Policy_whenSourceAssetHasNoDestinationAssociation_thenMigrationFails +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenVersion3ContainsOrphanAsset_thenPreservesItUnattached +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenRequiredCustomMappingIsMissing_thenThrowsBeforeCheckpoint +fearlessTests/SubstrateStorageMigrationSafetyTests/testBundledRequiredCustomMappings_matchCurrentModelsAndPolicies +fearlessTests/SubstrateStorageMigrationSafetyTests/testSyncMigration_whenKnownStoreHasZeroProtectedFootprint_thenQuarantinesOnlySubstrateFamily +fearlessTests/SubstrateStorageMigrationSafetyTests/testSyncMigration_whenCheckpointFailsWithZeroProtectedData_thenQuarantinesExactSourceFamily +fearlessTests/SubstrateStorageMigrationSafetyTests/testSyncMigration_whenCheckpointFailsWithProtectedData_thenBlocksRecoveryAndPreservesSource +fearlessTests/SubstrateStorageMigrationSafetyTests/testSyncMigration_whenCheckpointFails_thenFinalRecoveryInspectionNeverCheckpointsAgain +fearlessTests/SubstrateStorageMigrationSafetyTests/testCacheRecoveryTransaction_whenInterruptedAtEveryDurableBoundary_thenRelaunchConvergesExactly +fearlessTests/SubstrateStorageMigrationSafetyTests/testCacheRecoveryTransaction_whenPreparationIsMarkerlessOrOnlyPending_thenRelaunchCleansItWithoutMovingLiveFamily +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigrationWithRecovery_whenCacheQuarantineLostMainStore_thenReconcilesBeforeLiveStoreSafetyCheck +fearlessTests/SubstrateStorageMigrationSafetyTests/testPerformMigration_whenDeployedLegacyRecoveryMovedRealSQLiteMain_thenRestoresBeforeStoreSafetyAndPreservesRowsAndWAL +fearlessTests/SubstrateStorageMigrationSafetyTests/testCacheRecoveryTransaction_whenActiveTreeContainsUnknownFile_thenFailsClosedWithoutMovingFamily +fearlessTests/SubstrateStorageMigrationSafetyTests/testCacheRecoveryTransaction_whenLiveFamilyChangesAfterMarker_thenFailsClosedWithoutMovingAnything +fearlessTests/SubstrateStorageMigrationSafetyTests/testCacheRecoveryTransaction_whenMovedMemberChanges_thenFailsClosedAndKeepsDurableMarker +fearlessTests/SubstrateStorageMigrationSafetyTests/testCacheRecoveryTransaction_whenSourceContainsSymlinkOrHardlink_thenRejectsBeforeCreatingMarker +fearlessTests/SubstrateStorageMigrationSafetyTests/testCacheRecoveryTransaction_whenMarkerIsNoncanonicalOrOversized_thenFailsClosedAndPreservesLiveFamily +fearlessTests/SubstrateStorageMigrationSafetyTests/testCacheRecoveryTransaction_whenLegacyMarkerlessMoveStoppedAtEveryBoundary_thenAdoptsDurablyAndConverges +fearlessTests/SubstrateStorageMigrationSafetyTests/testCacheRecoveryTransaction_whenLegacyAdoptionMarkerWriteWasInterrupted_thenRemovesOnlyPendingMarkerAndConverges +fearlessTests/SubstrateStorageMigrationSafetyTests/testCacheRecoveryTransaction_whenLegacyRestorationIsInterruptedAtEveryDurableBoundary_thenRelaunchConverges +fearlessTests/SubstrateStorageMigrationSafetyTests/testCacheRecoveryTransaction_whenLegacyTemporaryFileSynchronizationFails_thenRelaunchResynchronizesBeforeInstall +fearlessTests/SubstrateStorageMigrationSafetyTests/testCacheRecoveryTransaction_whenFinalQuarantineDirectorySynchronizationFails_thenRetryResynchronizesBothDirectoriesBeforeCommit +fearlessTests/SubstrateStorageMigrationSafetyTests/testCacheRecoveryTransaction_whenFinalLegacyRestoreDirectorySynchronizationFails_thenRetryResynchronizesBothDirectoriesBeforeCommit +fearlessTests/SubstrateStorageMigrationSafetyTests/testRequiresMigration_whenCacheRecoveryMarkerIsPendingBeforeFirstMove_thenReturnsTrueForCurrentStore +fearlessTests/SubstrateStorageMigrationSafetyTests/testCacheRecoveryTransaction_whenCompletedLegacyArchiveExistsBesideRebuiltLiveStore_thenLeavesBothUntouched +fearlessTests/SubstrateStorageMigrationSafetyTests/testCacheRecoveryRetention_whenQuarantineCompletes_thenExcludesRootAndArchiveFromBackup +fearlessTests/SubstrateStorageMigrationSafetyTests/testCacheRecoveryRetention_whenBackupExclusionFails_thenPreservesLiveFamilyBeforeMarker +fearlessTests/SubstrateStorageMigrationSafetyTests/testCacheRecoveryRetention_whenCountLimitExceeded_thenDeletesOnlyOldestCompleteFamily +fearlessTests/SubstrateStorageMigrationSafetyTests/testCacheRecoveryRetention_whenByteBudgetCannotHoldArchive_thenDeletesWholeFamilyAndPreservesLiveStore +fearlessTests/SubstrateStorageMigrationSafetyTests/testCacheRecoveryRetention_whenAnyArchiveTreeIsUnrecognized_thenFailsClosedBeforeDeletingValidArchive +fearlessTests/SubstrateStorageMigrationSafetyTests/testCacheRecoveryRetention_whenArchiveMemberHardlinksLiveStore_thenFailsClosedWithoutUnlinkingEitherPath +fearlessTests/SubstrateStorageMigrationSafetyTests/testCacheRecoveryRetention_whenDeletionIsInterruptedAtEveryDurableBoundary_thenRelaunchConvergesWithoutTouchingNewestOrLiveFamily +fearlessTests/SubstrateStorageMigrationSafetyTests/testCacheRecoveryTransaction_whenLegacySplitHasMultipleCandidates_thenFailsClosedWithoutChoosingArchive +fearlessTests/SubstrateStorageMigrationSafetyTests/testCacheRecoveryTransaction_whenLegacySplitIsTamperedOrOverlapping_thenFailsClosed +fearlessTests/SubstrateStorageMigrationSafetyTests/testSyncMigration_whenKnownStoreHasContact_thenBlocksRecovery +fearlessTests/SubstrateStorageMigrationSafetyTests/testSyncMigration_whenKnownStoreHasContactItem_thenBlocksRecovery +fearlessTests/SubstrateStorageMigrationSafetyTests/testSyncMigration_whenKnownStoreHasTransactionHistory_thenBlocksRecovery +fearlessTests/SubstrateStorageMigrationSafetyTests/testSyncMigration_whenKnownStoreHasCustomNode_thenBlocksRecovery +fearlessTests/SubstrateStorageMigrationSafetyTests/testSyncMigration_whenKnownStoreHasSelectedDefaultNode_thenBlocksRecovery +fearlessTests/SubstrateStorageMigrationSafetyTests/testSyncMigration_whenKnownStoreHasOrphanNode_thenBlocksRecovery +fearlessTests/SubstrateStorageMigrationSafetyTests/testCallbackMigration_whenStoreIsCorrupt_thenDoesNotCompleteOrQuarantine +fearlessTests/TransformableArchiveSafetyTests/testObjectiveCExceptionBoundaryReturnsOnlySanitizedError +fearlessTests/TransformableArchiveSafetyTests/testObjectiveCExceptionBoundaryAllowsSuccessfulVoidOperation +fearlessTests/TransformableArchiveSafetyTests/testPersistentCorruptFavouriteChainIdsDefaultsOnlyPreferenceWithoutMutation +fearlessTests/TransformableArchiveSafetyTests/testPersistentCorruptAssetKeysOrderDefaultsOnlyPreferenceWithoutMutation +fearlessTests/TransformableArchiveSafetyTests/testPersistentCorruptUnusedChainIdsDefaultsOnlyPreferenceWithoutMutation +fearlessTests/TransformableArchiveSafetyTests/testPersistentWrongStoredTypeDefaultsOnlyPreferenceWithoutMutation +fearlessTests/TransformableArchiveSafetyTests/testPersistentValidWalletArchivesPreserveEveryPreference +fearlessTests/TransformableArchiveSafetyTests/testPersistentCorruptChainOptionsDefaultsWithoutMutatingCache +fearlessTests/TransformableArchiveSafetyTests/testPersistentCorruptExternalApiTypesSkipsOnlyExplorerWithoutMutatingCache +fearlessTests/TransformableArchiveSafetyTests/testPersistentCorruptAssetPurchaseProvidersDefaultsWithoutMutatingCache +fearlessTests/TransformableArchiveSafetyTests/testPersistentCorruptPolkaswapTransformablesSurfaceCatchableErrorWithoutMutation +fearlessTests/TransformableArchiveSafetyTests/testProductionRuntimeRegistryRetainsEveryCurrentStoreClass +fearlessTests/MetaAccountMapperTests/testTransformConvertsMandatoryGetterExceptionToSanitizedSwiftError +fearlessTests/MetaAccountMapperTests/testMalformedChildRelationshipFailsClosedThroughDirectAndSelectionMappers +fearlessTests/MetaAccountMapperTests/testSelectionStartupWrapperQuarantinesOwnIdentifierGetterException +fearlessTests/MetaAccountMapperTests/testSaveAndFetch +fearlessTests/MetaAccountMapperTests/testCanonicalEcdsaSubstratePublicKeyIsAccepted +fearlessTests/MetaAccountMapperTests/testNonPersistableNilRequiredFieldsAreQuarantinedByMappersWithoutSaving +fearlessTests/MetaAccountMapperTests/testRepositorySliceDecoderAcceptsExactShapeAndFailsClosedOtherwise +fearlessTests/SelectedAccountSettingsTests/testSelectFirst +fearlessTests/SelectedAccountSettingsTests/testChangeSelectedAccount +fearlessTests/SelectedAccountSettingsTests/testSetupWhenWalletsExistWithoutSelectionRepairsDeterministicFallback +fearlessTests/SelectedAccountSettingsTests/testSetupToleratesTonOnlyRowAlongsideSelectedValidWalletWithoutChangingEitherPayload +fearlessTests/SelectedAccountSettingsTests/testSetupWithOnlyTonOnlyWalletReportsUnsupportedWithoutMutatingRow +fearlessTests/SelectedAccountSettingsTests/testStartupQuarantinesMixedCorruptSupportedRowsAndPreservesEveryStoredField +fearlessTests/SelectedAccountSettingsTests/testCorruptOnlyStoreReportsUnavailableAndPreservesEveryStoredField +fearlessTests/SelectedAccountSettingsTests/testStructurallyValidUnsupportedWalletKeepsUnsupportedOnlyWithCorruptSibling +fearlessTests/SelectedAccountSettingsTests/testSetupReplacesSelectedTonOnlyRowWithDeterministicValidFallbackAndPreservesPayloads +fearlessTests/SelectedAccountSettingsTests/testSaveAfterUnsupportedOnlyAddsValidWalletWithoutOverwritingTonPayload +fearlessTests/SelectedAccountSettingsTests/testSaveRejectsIdentifierCollisionWithUnsupportedWalletWithoutMutatingStore +fearlessTests/SelectedAccountSettingsTests/testAccountRepositoryDeleteAllRemovesSupportedAndUnsupportedWalletsForLogout +fearlessTests/SelectedAccountSettingsTests/testFilteredAccountRepositoryDeleteAllRemainsScopedAndIncludesUnsupportedMatches +fearlessTests/SelectedAccountSettingsTests/testAccountRepositoriesProjectMixedStoreBeforePagingAndSupportAsyncReads +fearlessTests/SelectedAccountSettingsTests/testRepositoryPagingSlicesHealthyProjectionAcrossPersistedCorruptRows +fearlessTests/SelectedAccountSettingsTests/testAccountRepositoryUpdatePreservesUnsupportedPayloadAndWalletMetadata +fearlessTests/SelectedAccountSettingsTests/testAccountRepositoryReplacePreservesUnsupportedRowsAndDeletesOnlySupportedRows +fearlessTests/SelectedAccountSettingsTests/testAccountRepositoryRejectsUnsupportedIdentifierCollisionWithoutMutation +fearlessTests/SelectedAccountSettingsTests/testManagedMapperFailsSafelyWhenWalletOrderHasReachedInt32Max +fearlessTests/SelectedAccountSettingsTests/testSelectionMapperFailsSafelyWhenUnsupportedWalletOrderHasReachedInt32Max +fearlessTests/SelectedAccountSettingsTests/testOutOfOrderSetupCannotOverwriteLatestValueStateOrPersistStaleRepair +fearlessTests/SelectedAccountSettingsTests/testSetupGenerationCannotChangeInsideAtomicValueAndStateCommit +fearlessTests/SelectedAccountSettingsTests/testConcurrentSavesAreSerializedAndOlderFailureCannotRollbackLatestSelection +fearlessTests/SelectedAccountSettingsTests/testSaveInvalidatesOlderSetupBeforeItCanCommitOrRepairSelection +fearlessTests/SelectedAccountSettingsTests/testSetupStartedDuringSaveWaitsForPersistenceAndCannotReadStaleSelection +fearlessTests/SelectedAccountSettingsTests/testSaveRegistrationIsAtomicAgainstSetupAtPreparationBoundary +fearlessTests/SelectedAccountSettingsTests/testSetupValueAndStoreStateCommitAtomicallyAgainstSaveRegistration +fearlessTests/SelectedAccountSettingsTests/testTolerantFactoriesAndStreamsIgnoreCorruptSelectedSiblingAndKeepObservingValidWallet +fearlessTests/SelectedAccountSettingsTests/testRepositorySaveRejectsEquivalentChainAccountAliasesBeforeMutatingStoredWallet +fearlessTests/SelectedAccountSettingsTests/testDeleteAndReplaceRejectDuplicateStoredIdentifiersInBothInsertionOrdersWithoutMutation +fearlessTests/SelectedAccountSettingsTests/testPersistedDuplicateAndUniversalAliasChainAccountsAreAlwaysQuarantinedWithoutMutation +fearlessTests/RootTests/testWalletConnectGroupResolverUsesProductionEntitlementForReleaseBundle +fearlessTests/RootTests/testWalletConnectGroupResolverUsesDevelopmentEntitlementForDevBundle +fearlessTests/RootTests/testWalletConnectGroupResolverRejectsUnsupportedApplicationIdentities +fearlessTests/RootTests/testHostedUnitTestsUseIsolatedApplicationLaunch +fearlessTests/RootTests/testCoreDataPreflightForwardsStoreOpenError +fearlessTests/RootTests/testCoreDataPreflightRejectsUnresolvableEntityClassBeforeFetch +fearlessTests/RootTests/testCoreDataPreflightQueriesObjectIdsWithoutDecodingCorruptTransformables +fearlessTests/RootTests/testUserStorageFacadeMissingModelReturnsControlledFailure +fearlessTests/RootTests/testSubstrateStorageFacadeMissingModelFailsPreflightWithoutTrap +fearlessTests/RootTests/testStartViewHelperDoesNotResolveWalletSettingsDuringConstruction +fearlessTests/RootTests/testMigrationFailureDoesNotResolveStorePreflightRegistryOrWalletSettings +fearlessTests/RootTests/testMigrationFailureBlocksNoMigrationBypassAndRequiredRetryRerunsMigration +fearlessTests/RootTests/testMigrationDeadlineFailsClosedAndRetriesReuseSingleRunningAttempt +fearlessTests/RootTests/testPreflightFailureDoesNotResolveRegistryOrWalletSettings +fearlessTests/RootTests/testWalletFailureRetryDoesNotRerunMigrationsAgainstResolvedStores +fearlessTests/RootTests/testMigrationFailureDoesNotLoadWalletOrRouteAndPreservesPincode +fearlessTests/RootTests/testSubstratePreflightFailureDoesNotCreateRegistryOrOpenWalletAndPreservesData +fearlessTests/RootTests/testWalletRepositoryFailureDoesNotBootOrRouteAndPreservesPincode +fearlessTests/RootTests/testOutOfOrderDualSetupRejectsStaleCallbackBeforeBootOrPresenterMutation +fearlessTests/RootTests/testPreflightTimeoutIgnoresLateResultAndRetryUsesFreshAttempt +fearlessTests/RootTests/testSynchronouslyBlockedPreflightTimesOutAndDoesNotBlockRetry +fearlessTests/RootTests/testTwoBlockedPreflightsFailFurtherRetriesFastWithoutUnboundedInvocation +fearlessTests/RootTests/testSynchronouslyBlockedPreflightDoesNotRetainInteractorAfterDeinit +fearlessTests/RootTests/testTwoSynchronouslyBlockedPreflightsDoNotRetainInteractorAfterDeinit +fearlessTests/RootTests/testWalletSetupTimeoutIgnoresLateResultAndRetryUsesFreshAttempt +fearlessTests/RootTests/testSubstratePreflightFailureCanRetryWithoutRemigrationOrPrematureBoot +fearlessTests/RootTests/testAdversarialPreflightMultipleCallbacksCanOnlyOpenWalletAndBootOnce +fearlessTests/RootTests/testOnlyTonWalletAllowsLoginWithoutPincodeButFailsClosedAndPreservesRowWithPincode +fearlessTests/RootTests/testMigrationThenRealSubstrateOpenThenWalletOpenThenBootRoutesSelectedWalletToPin +fearlessTests/RootTests/testLateOnboardingResultAfterTimeoutCannotReplaceLogin +fearlessTests/RootTests/testProtectedDataFailureIsVisibleAndNeverBypassesAuthentication +fearlessTests/RootTests/testUnselectedWalletIsRecoveredWithoutDeletingPincodeOrBypassingAuthentication +fearlessTests/RootTests/testConfirmedEmptyWalletStoreReturnsLoginAndDeletesPincode +fearlessTests/RootTests/testConfirmedEmptyWalletStoreFailsClosedWhenPincodeDeletionFails +fearlessTests/RootTests/testOnboardingDecision +fearlessTests/RootTests/testPincodeSetupDecision +fearlessTests/RootTests/testMainScreenDecision +fearlessTests/ChainModelMapperTests/testStartupWrapperConvertsMandatoryGetterExceptionToSanitizedSwiftError +fearlessTests/ChainModelMapperTests/testThrowingNodeRelationshipFailsClosedAsSwiftError +fearlessTests/ChainModelMapperTests/testSaveAndFetchPreservesParaIdAndIdentityChain +fearlessTests/ChainModelMapperTests/testTransformQuarantinesChainWithoutIdentifier +fearlessTests/ChainModelMapperTests/testTransformQuarantinesNoncanonicalIdentifierUntilCleanup +fearlessTests/ChainModelMapperTests/testTransformQuarantinesNameButKeepsIdentifierAndPreferences +fearlessTests/ChainModelMapperTests/testTransformSkipsDefaultNodeWithoutURL +fearlessTests/ChainModelMapperTests/testTransformSkipsSelectedNodeWithoutName +fearlessTests/ChainModelMapperTests/testTransformSkipsSelectedNodeWithUnusableURL +fearlessTests/ChainModelMapperTests/testTransformAcceptsSubstrateWebSocketNodeURLs +fearlessTests/ChainModelMapperTests/testTransformKeepsCompatibleCustomNodeAsOnlyRuntimeEndpoint +fearlessTests/ChainModelMapperTests/testTransformDisablesChainWithOnlyIncompatibleCustomNode +fearlessTests/ChainModelMapperTests/testTransformRejectsHTTPNodesForSubstrateRuntime +fearlessTests/ChainModelMapperTests/testTransformAcceptsOnlySecureEthereumRuntimeSchemes +fearlessTests/ChainModelMapperTests/testTransformAppliesStrictTonServerShape +fearlessTests/ChainModelMapperTests/testTransformKeepsValidNodesWhileSkippingMalformedOnes +fearlessTests/ChainModelMapperTests/testPopulateRoundTripPreservesValidStandaloneSelectedNode +fearlessTests/ChainModelMapperTests/testTransformDropsUnsupportedStandaloneSelectionButKeepsHealthyChain +fearlessTests/ChainModelMapperTests/testTransformSkipsMalformedCachedAsset +fearlessTests/ChainModelMapperTests/testTransformIgnoresRelationshipWithOnlyMalformedPrices +fearlessTests/ChainModelMapperTests/testCachedPriceExtractionRejectsUnexpectedEntityBeforeReadingUndefinedKeys +fearlessTests/ChainModelMapperTests/testCachedPriceExtractionSkipsRowWhenAttributeReadRaisesException +fearlessTests/ChainModelMapperTests/testCachedPriceExtractionPreservesLegacyDecimalWithoutOptionalFields +fearlessTests/ChainModelMapperTests/testTransformUsesSingleValidUnmatchedCachedPrice +fearlessTests/ChainModelMapperTests/testTransformDeterministicallyChoosesCanonicalUnmatchedCachedPrice +fearlessTests/ChainModelMapperTests/testTransformUsesValidCachedPriceAmongMalformedRows +fearlessTests/ChainModelMapperTests/testTransformRejectsMalformedNonfiniteAndNegativeCachedPrices +fearlessTests/ChainModelMapperTests/testTransformPreservesSupportedCachedPriceStringFormats +fearlessTests/ChainModelMapperTests/testPopulateDeletesMalformedStaleNodesInsteadOfForceUnwrapping +fearlessTests/ChainModelMapperTests/testMalformedChainCleanup_whenGetterRaisesDuringQuarantine_thenFailsSafelyAndRollsBack +fearlessTests/ChainModelMapperTests/testMalformedChainCleanupQuarantinesBlankIdentifiersWithoutDataLoss +fearlessTests/ChainModelMapperTests/testMalformedChainQuarantineCannotCollideWithStoredIdentifier +fearlessTests/ChainModelMapperTests/testCleanupCanonicalizesIdentifiersAndMergesPreferencesWithoutLoss +fearlessTests/ChainModelMapperTests/testCleanupCollisionPreservesCustomSelectionWhenDefaultRetires +fearlessTests/ChainModelMapperTests/testCleanupQuarantinesConflictingCollisionWithoutDeletingItsData +fearlessTests/ChainSyncServiceCompatibilityTests/testCoerceChainsPayloadForCompatibilityNormalizesBlockscoutTypes +fearlessTests/ChainSyncServiceCompatibilityTests/testPreservingLocalNodePreferencesKeepsCustomAndSelectedNodes +fearlessTests/ChainSyncServiceCompatibilityTests/testPreservingLocalNodePreferencesDoesNotApplyPreferencesToAnotherChain +fearlessTests/ChainSyncServiceCompatibilityTests/testPreservingLocalNodePreferencesRejectsRemoteCustomNodeInjection +fearlessTests/ChainSyncServiceCompatibilityTests/testPreservingLocalNodePreferencesDropsRetiredDefaultSelection +fearlessTests/ChainSyncServiceCompatibilityTests/testPreservingLocalNodePreferencesRefreshesSelectedDefaultByURL +fearlessTests/ChainSyncServiceCompatibilityTests/testRepositoryWriteFailureEmitsFailureWithoutFalseCompletion +fearlessTests/ChainSyncServiceCompatibilityTests/testRepositoryFetchFailureDoesNotWriteOrReplaceCache +fearlessTests/ChainSyncServiceCompatibilityTests/testEmptyRemotePayloadNeverDeletesReadableCache +fearlessTests/ChainSyncServiceCompatibilityTests/testActiveRemoteChainWithOnlyUnusableNodesPreservesWorkingCache +fearlessTests/ChainSyncServiceCompatibilityTests/testMixedRemoteNodesPersistOnlyUsableUniqueEndpoints +fearlessTests/ChainSyncServiceCompatibilityTests/testPartialRemotePayloadDisablesOmittedChainWithoutDeletingPreferences +fearlessTests/ChainSyncServiceCompatibilityTests/testReappearingRemoteChainRestoresDisabledPreferences +fearlessTests/ChainSyncServiceCompatibilityTests/testQuarantinedLocalChainDoesNotDiscardHealthyNodePreferences +fearlessTests/ChainSyncServiceCompatibilityTests/testMalformedChainCleanupFailurePreventsRepositoryMutation +fearlessTests/ChainSyncServiceCompatibilityTests/testConcurrentSyncRequestsStartSingleDeterministicPipeline +fearlessTests/ChainSyncServiceCompatibilityTests/testCooldownExpiryDoesNotReleaseBlockedPipeline +fearlessTests/ChainSyncServiceCompatibilityTests/testSuccessBeforeTimerStartRetainsCooldownUntilExpiry +fearlessTests/ChainSyncServiceCompatibilityTests/testSuccessDuringCooldownRemainsBlockedUntilExpiry +fearlessTests/ChainSyncServiceCompatibilityTests/testFailureBeforeTimerStartInvalidatesOnlyFailedReservation +fearlessTests/ChainSyncServiceCompatibilityTests/testFailureDuringCooldownStopsOldTimerAndAllowsOneReplacement +fearlessTests/ChainSyncServiceCompatibilityTests/testFailureAfterCooldownExpiryAllowsReplacementWithoutOverlap +fearlessTests/ChainSyncServiceCompatibilityTests/testStaleStopCallbackCannotClearNewerCooldown +fearlessTests/ChainSyncServiceCompatibilityTests/testSanitizerRejectsDuplicateRemoteIdentifiers +fearlessTests/ChainSyncServiceCompatibilityTests/testSanitizerRejectsBlankNoncanonicalAndReservedIdentifiers +fearlessTests/ChainSyncServiceCompatibilityTests/testSanitizerRejectsBlankRemoteName +fearlessTests/ChainSyncServiceCompatibilityTests/testSanitizerAllowsDisabledChainWithoutNodes +fearlessTests/ChainSyncServiceCompatibilityTests/testSanitizerDeterministicallyDeduplicatesNodeCredentialMetadata +fearlessTests/ChainSyncServiceCompatibilityTests/testSanitizerRejectsEthereumChainWithOnlyIncompatibleSchemes +fearlessTests/ChainSyncServiceCompatibilityTests/testSanitizerKeepsOnlyRuntimeCompatibleEthereumNodes +fearlessTests/ChainSyncServiceCompatibilityTests/testPreservingPreferencesDropsRuntimeIncompatibleCustomSelection +fearlessTests/ChainSyncServiceCompatibilityTests/testSanitizerStripsRemotePreferencesForBrandNewChain +fearlessTests/ChainRegistryTests/testDisabledThenReenabledChainDisconnectsAndUsesNewNode +fearlessTests/ChainRegistryTests/testUpdatedEthereumChainUsesReplacementNodeImmediately +fearlessTests/ConnectionPoolTests/testSetupCreatesNewConnections +fearlessTests/ConnectionPoolTests/testEthereumConnectionPoolResetRemovesConnectionForChainId +fearlessTests/CrashConsistentStoreReplacerResourceLimitTests/testReplaceStore_whenMainFileExceedsLimit_rejectsBeforeCapacityOrMutation +fearlessTests/CrashConsistentStoreReplacerResourceLimitTests/testReplaceStore_whenAggregateFamilyExceedsLimit_rejectsBeforeMutation +fearlessTests/CrashConsistentStoreReplacerResourceLimitTests/testReplaceStore_whenCapacityIsOneByteShort_rejectsBeforeTransactionMutation +fearlessTests/CrashConsistentStoreReplacerResourceLimitTests/testReplaceStore_whenFamilyAndCapacityAreExactBoundaries_succeeds +fearlessTests/CrashConsistentStoreReplacerResourceLimitTests/testReplaceStore_whenCapacityRequirementOverflows_rejectsBeforeMutation +fearlessTests/CrashConsistentStoreReplacerResourceLimitTests/testCommittedRecovery_whenValidationCopyLacksSpace_preservesTransactionForRetry +fearlessTests/CrashConsistentStoreReplacerResourceLimitTests/testPrivateCopy_whenSparseSourceExceedsLimit_rejectsBeforeCapacityOrCopy +fearlessTests/CrashConsistentStoreReplacerResourceLimitTests/testPrivateCopy_whenCapacityIsInsufficient_rejectsBeforeFirstMember +fearlessTests/CrashConsistentStoreReplacerResourceLimitTests/testPrivateCopy_whenSourceMainIsSymlink_rejectsWithoutFollowingIt +fearlessTests/CrashConsistentStoreReplacerResourceLimitTests/testPrivateCopy_whenDestinationHasOrphanSidecar_rejectsBeforeMainCopy +fearlessTests/CrashConsistentStoreReplacerResourceLimitTests/testPrivateCopy_atExactAggregateAndCapacityBoundary_copiesDurableMembersButNotSharedMemory diff --git a/scripts/ci/run-coredata-release-gate.sh b/scripts/ci/run-coredata-release-gate.sh index e124b96028..e8054fbe34 100644 --- a/scripts/ci/run-coredata-release-gate.sh +++ b/scripts/ci/run-coredata-release-gate.sh @@ -17,6 +17,8 @@ umask 077 readonly LOG_PREFIX="[coredata-release-gate]" readonly SCHEME="fearless.tests" +readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +readonly MANIFEST_DIR="$SCRIPT_DIR/manifests" readonly UUID_PATTERN='^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$' readonly SIMULATOR_DESTINATION_PATTERN='^platform=iOS Simulator,id=([0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12})(,arch=(arm64|x86_64))?$' SIMULATOR_INVENTORY_FILE="" @@ -30,6 +32,20 @@ fail() { exit 1 } +readonly CORE_DATA_TEST_HARNESS="${FEARLESS_CORE_DATA_TEST_HARNESS:-0}" +if [[ "$CORE_DATA_TEST_HARNESS" != "1" ]]; then + for override_name in \ + FEARLESS_CORE_DATA_ROOT_DIR \ + FEARLESS_CORE_DATA_WORKSPACE \ + FEARLESS_CORE_DATA_XCODEBUILD_BIN \ + FEARLESS_CORE_DATA_XCRUN_BIN \ + FEARLESS_CORE_DATA_PYTHON_BIN \ + FEARLESS_CORE_DATA_SHASUM_BIN; do + [[ -z "${!override_name:-}" ]] || + fail "$override_name is accepted only with FEARLESS_CORE_DATA_TEST_HARNESS=1" + done +fi + cleanup() { if [[ -n "${SIMULATOR_INVENTORY_FILE:-}" && -e "$SIMULATOR_INVENTORY_FILE" ]]; then rm "$SIMULATOR_INVENTORY_FILE" @@ -288,10 +304,183 @@ PY fi } +validate_test_inventory() { + local result_bundle="$1" + local manifest="$2" + local tests_json="$3" + local executed_manifest="$4" + + [[ -f "$manifest" && ! -L "$manifest" ]] || + fail "expected test identity manifest is missing or unsafe" + + if ! "$XCRUN_BIN" xcresulttool get test-results tests \ + --path "$result_bundle" --compact >"$tests_json" 2>/dev/null; then + fail "xcresulttool could not read the executed test identities" + fi + + if ! "$PYTHON_BIN" - "$manifest" "$tests_json" "$executed_manifest" <<'PY' +import json +import re +import sys + +manifest_path, tests_path, output_path = sys.argv[1:] +identity_pattern = re.compile( + r"^fearlessTests/([A-Za-z_][A-Za-z0-9_]*)/(test[A-Za-z0-9_]+)$" +) + +def reject(message): + print(f"invalid xcresult test inventory: {message}", file=sys.stderr) + raise SystemExit(1) + +try: + with open(manifest_path, "r", encoding="utf-8") as source: + expected = [ + line.strip() + for line in source + if line.strip() and not line.lstrip().startswith("#") + ] +except OSError: + reject("expected manifest is unreadable") + +if not expected: + reject("expected manifest is empty") +if len(expected) != len(set(expected)): + reject("expected manifest contains duplicate identities") +for identity in expected: + if not identity_pattern.fullmatch(identity): + reject("expected manifest contains a noncanonical identity") + +try: + with open(tests_path, "r", encoding="utf-8") as source: + payload = json.load(source) +except (OSError, json.JSONDecodeError): + reject("test tree is malformed or missing") +if not isinstance(payload, (dict, list)): + reject("test tree root has an unexpected type") + +executed = [] +statuses = [] + +def canonical_direct(value): + if not isinstance(value, str): + return None + value = value.strip() + value = re.sub(r"\(\)$", "", value) + match = re.search( + r"(?:^|/)(fearlessTests/[A-Za-z_][A-Za-z0-9_]*/" + r"test[A-Za-z0-9_]+)$", + value, + ) + return match.group(1) if match else None + +def class_name(value): + if not isinstance(value, str): + return None + value = value.strip().rstrip("/") + value = value.split("/")[-1] + return value if re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*Tests", value) else None + +def method_name(value): + if not isinstance(value, str): + return None + value = re.sub(r"\(\)$", "", value.strip().split("/")[-1]) + return value if re.fullmatch(r"test[A-Za-z0-9_]+", value) else None + +def visit(node, ancestors): + if isinstance(node, list): + for child in node: + visit(child, ancestors) + return + if not isinstance(node, dict): + return + + identifiers = [ + node.get("nodeIdentifier"), + node.get("identifier"), + node.get("name"), + node.get("testName"), + ] + node_type = str(node.get("nodeType", node.get("type", ""))).lower() + direct = next( + (value for value in (canonical_direct(item) for item in identifiers) if value), + None, + ) + method = next( + (value for value in (method_name(item) for item in identifiers) if value), + None, + ) + is_test_case = ( + "test case" in node_type + or node_type in {"test", "testcase"} + or direct is not None + or method is not None + ) + if is_test_case: + identity = direct + if identity is None and method is not None: + test_class = next( + ( + value + for value in ( + class_name(item) + for ancestor in reversed(ancestors) + for item in ancestor + ) + if value + ), + None, + ) + if test_class is not None: + identity = f"fearlessTests/{test_class}/{method}" + if identity is None: + reject("a test case has no canonical XCTest identity") + status = node.get("result", node.get("testStatus", node.get("status"))) + if status != "Passed": + reject(f"{identity} did not have Passed status") + executed.append(identity) + statuses.append(status) + + next_ancestors = ancestors + [identifiers] + for value in node.values(): + if isinstance(value, (dict, list)): + visit(value, next_ancestors) + +visit(payload, []) + +if not executed: + reject("no test case identities were found") +if len(executed) != len(set(executed)): + reject("executed test tree contains duplicate identities") +if len(statuses) != len(executed): + reject("executed test status inventory is incomplete") + +expected_set = set(expected) +executed_set = set(executed) +if executed_set != expected_set: + missing = len(expected_set - executed_set) + unexpected = len(executed_set - expected_set) + reject( + f"exact identity set differs (missing={missing}, unexpected={unexpected})" + ) +if len(executed) != len(expected): + reject("executed identity cardinality differs from the expected manifest") + +with open(output_path + ".pending", "x", encoding="utf-8") as destination: + for identity in sorted(executed): + destination.write(identity + "\n") +import os +os.replace(output_path + ".pending", output_path) +PY + then + fail "executed tests did not match the exact reviewed identity manifest" + fi +} + run_stage() { local stage="$1" local result_name local expected_total + local expected_manifest local fixture_before="" local fixture_after="" local xcodebuild_status @@ -302,6 +491,7 @@ run_stage() { # The simulator core cohort must execute exactly 412 tests. Copied-phone # fixtures are intentionally verified by the separate device/store stage. expected_total="412" + expected_manifest="$MANIFEST_DIR/coredata-release-core-tests.txt" selectors=( "-only-testing:fearlessTests/SingleToMultiassetUserMigrationTests" "-only-testing:fearlessTests/UserStorageCompatibilityMigrationTests" @@ -322,6 +512,7 @@ run_stage() { else result_name="copied-phone" expected_total="2" + expected_manifest="$MANIFEST_DIR/coredata-release-copied-phone-tests.txt" selectors=( "-only-testing:fearlessTests/SubstrateStorageClassResolutionTests/testCopiedPhoneV8Store_whenAvailable_thenAllRowsHaveExpectedClassesAndStoreIsUnchanged" "-only-testing:fearlessTests/SubstrateStorageClassResolutionTests/testCopiedPhoneV8Store_whenFetchedThroughProductionRepositories_thenMapsEveryRuntimeAndChain" @@ -332,8 +523,16 @@ run_stage() { local result_bundle="$OUTPUT_DIR/${result_name}.xcresult" local xcodebuild_log="$OUTPUT_DIR/${result_name}.xcodebuild.log" local summary_file="$OUTPUT_DIR/${result_name}.summary.json" + local tests_json="$OUTPUT_DIR/${result_name}.tests.json" + local executed_manifest="$OUTPUT_DIR/${result_name}.executed-tests.txt" [[ ! -e "$result_bundle" ]] || fail "refusing to overwrite an existing xcresult bundle" + actual_manifest_total="$( + awk 'NF && $1 !~ /^#/ { count += 1 } END { print count + 0 }' \ + "$expected_manifest" + )" + [[ "$actual_manifest_total" == "$expected_total" ]] || + fail "reviewed test manifest cardinality does not match the stage contract" local -a xcodebuild_arguments=( -workspace "$WORKSPACE" @@ -377,6 +576,11 @@ run_stage() { fail "xcodebuild failed for the ${stage} stage; inspect the private gate log" validate_result_summary "$result_bundle" "$expected_total" "$summary_file" + validate_test_inventory \ + "$result_bundle" \ + "$expected_manifest" \ + "$tests_json" \ + "$executed_manifest" if [[ "$stage" == "copied-phone" ]]; then log "PASSED copied-phone fixture stage: exactly 2 tests, source fixture unchanged" else diff --git a/scripts/ci/run-coredata-simulator-rehearsal.sh b/scripts/ci/run-coredata-simulator-rehearsal.sh index 99ccd38b41..b520ddb57c 100755 --- a/scripts/ci/run-coredata-simulator-rehearsal.sh +++ b/scripts/ci/run-coredata-simulator-rehearsal.sh @@ -7,7 +7,7 @@ set -euo pipefail # Safety properties: # - requires an explicit, booted iPhone Simulator UDID; # - verifies the .app contains an iOS Simulator binary; -# - rejects unsigned, linker-only, or Keychain-incompatible app bundles; +# - rejects unsigned or linker-only Simulator app bundles; # - requires artifact-embedded Release, Swift -O, and disabled-testability attestations; # - refuses a simulator where this bundle is already installed; # - never opens, deletes, renames, chmods, or otherwise writes the source fixture; @@ -21,6 +21,8 @@ readonly LOG_PREFIX="[coredata-simulator-rehearsal]" readonly UUID_PATTERN='^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$' readonly BUNDLE_ID_PATTERN='^[A-Za-z0-9][A-Za-z0-9.-]*[A-Za-z0-9]$' readonly DEFAULT_ALIVE_SECONDS=8 +readonly EXPECTED_BUNDLE_ID="jp.co.soramitsu.fearlesswallet" +readonly EXPECTED_VERSION="4.2.0" readonly STORE_FILES=( "CacheDataModel.sqlite" @@ -104,6 +106,28 @@ CRASH_REPORT_DIR="${HOME}/Library/Logs/DiagnosticReports" ALIVE_SECONDS="$DEFAULT_ALIVE_SECONDS" DISPOSABLE_FIXTURE_CONFIRMED=0 DRY_RUN=0 +EXPECTED_GIT_SHA="" +EXPECTED_EXECUTABLE_SHA256="" +EXPECTED_BUILD="" + +readonly REHEARSAL_TEST_HARNESS="${FEARLESS_REHEARSAL_TEST_HARNESS:-0}" +if [[ "$REHEARSAL_TEST_HARNESS" != "1" ]]; then + for override_name in \ + FEARLESS_REHEARSAL_XCRUN_BIN \ + FEARLESS_REHEARSAL_CODESIGN_BIN \ + FEARLESS_REHEARSAL_PYTHON_BIN \ + FEARLESS_REHEARSAL_SHASUM_BIN \ + FEARLESS_REHEARSAL_PS_BIN \ + FEARLESS_REHEARSAL_SLEEP_BIN \ + FEARLESS_REHEARSAL_PROCESS_CHECK_BIN; do + if [[ -n "${!override_name:-}" ]]; then + printf '%s ERROR: %s\n' \ + "[coredata-simulator-rehearsal]" \ + "$override_name is accepted only with FEARLESS_REHEARSAL_TEST_HARNESS=1" >&2 + exit 1 + fi + done +fi XCRUN_BIN="${FEARLESS_REHEARSAL_XCRUN_BIN:-xcrun}" CODESIGN_BIN="${FEARLESS_REHEARSAL_CODESIGN_BIN:-codesign}" @@ -144,6 +168,9 @@ Usage: --simulator-udid UUID \ --app /absolute/path/to/fearless.app \ --bundle-id jp.co.soramitsu.fearlesswallet \ + --expected-git-sha 40_HEX_SHA \ + --expected-build CANONICAL_CF_BUNDLE_VERSION \ + --expected-executable-sha256 64_HEX_SHA256 \ --fixture-dir /absolute/path/to/disposable/offline/CoreData \ --artifacts-dir /absolute/path/to/new/artifacts \ --confirm-disposable-fixture @@ -163,6 +190,12 @@ Options: --dry-run Perform fail-closed input, Simulator, binary, checksum, and copied-SQLite validation without installing or launching the app. + --expected-git-sha SHA + Exact 40-hex source commit embedded in the Simulator artifact. + --expected-build BUILD + Exact fresh CFBundleVersion selected after the App Store Connect check. + --expected-executable-sha256 SHA + Exact 64-hex executable digest recorded by the build step. --help Controlled test-harness overrides: @@ -201,6 +234,10 @@ validate_release_app_contract() { local build_configuration local optimization_level local enable_testability + local embedded_git_sha + local marketing_version + local build_number + local executable_digest build_configuration="$( plist_string "$APP_PATH/Info.plist" "FearlessBuildConfiguration" @@ -211,6 +248,15 @@ validate_release_app_contract() { enable_testability="$( plist_string "$APP_PATH/Info.plist" "FearlessEnableTestability" )" || fail "Release app lacks its testability attestation" + embedded_git_sha="$( + plist_string "$APP_PATH/Info.plist" "FearlessGitCommit" + )" || fail "Release app lacks its git-commit attestation" + marketing_version="$( + plist_string "$APP_PATH/Info.plist" "CFBundleShortVersionString" + )" || fail "Release app lacks its marketing version" + build_number="$( + plist_string "$APP_PATH/Info.plist" "CFBundleVersion" + )" || fail "Release app lacks its build number" [[ "$build_configuration" == "Release" ]] || fail "app was not built with the Release configuration" @@ -218,6 +264,17 @@ validate_release_app_contract() { fail "app was not built with Swift -O optimization" [[ "$enable_testability" == "NO" ]] || fail "app was built with testability enabled" + [[ "$embedded_git_sha" == "$EXPECTED_GIT_SHA" ]] || + fail "app git commit does not match --expected-git-sha" + [[ "$marketing_version" == "$EXPECTED_VERSION" ]] || + fail "app marketing version is not $EXPECTED_VERSION" + [[ "$build_number" == "$EXPECTED_BUILD" ]] || + fail "app build number is not --expected-build" + executable_digest="$("$SHASUM_BIN" -a 256 "$APP_EXECUTABLE_PATH" | awk '{print $1}')" + [[ "$executable_digest" =~ ^[0-9A-Fa-f]{64}$ ]] || + fail "unable to compute the Simulator executable SHA-256" + [[ "$executable_digest" == "$EXPECTED_EXECUTABLE_SHA256" ]] || + fail "Simulator executable SHA-256 does not match the build record" if ! "$XCRUN_BIN" nm -gj "$APP_EXECUTABLE_PATH" >"$symbols_file" 2>/dev/null; then : >"$symbols_file" @@ -238,7 +295,7 @@ validate_release_app_contract() { done printf '%s\n' \ - "Release, Swift -O, testability disabled; 26 managed-object runtime classes and 29 Core Data resources verified" \ + "Release, Swift -O, testability disabled; exact commit/version/build/executable SHA-256; 26 managed-object runtime classes and 29 Core Data resources verified" \ >"$ARTIFACTS_DIR/coredata-app-contract.txt" } @@ -591,6 +648,168 @@ def protected_user_fingerprints(connection, table_names): ) return fingerprints +def protected_substrate_topology(connection, table_names): + chain_table = "ZCDCHAIN" + node_table = "ZCDCHAINNODE" + primary_key_table = "Z_PRIMARYKEY" + required_tables = {chain_table, node_table, primary_key_table} + if not required_tables.issubset(table_names): + raise RuntimeError("Substrate store lacks chain/node topology tables") + + chain_columns = table_columns(connection, chain_table) + node_columns = table_columns(connection, node_table) + required_chain_columns = {"Z_PK", "ZCHAINID", "ZSELECTEDNODE"} + required_node_columns = {"Z_PK", "ZCHAIN", "ZNAME", "ZURL"} + if not required_chain_columns.issubset(chain_columns): + raise RuntimeError("Substrate store lacks protected chain topology columns") + if not required_node_columns.issubset(node_columns): + raise RuntimeError("Substrate store lacks protected node topology columns") + + entity_numbers = { + str(name).upper(): number + for number, name in connection.execute( + f"SELECT Z_ENT, Z_NAME FROM {quote_identifier(primary_key_table)}" + ).fetchall() + } + chain_entity = entity_numbers.get("CDCHAIN") + node_entity = entity_numbers.get("CDCHAINNODE") + if not isinstance(chain_entity, int) or not isinstance(node_entity, int): + raise RuntimeError("Substrate store lacks Core Data entity-number metadata") + + inline_custom_column = f"Z{chain_entity}CUSTOMNODES" + if inline_custom_column not in node_columns: + raise RuntimeError("Substrate store lacks the canonical customNodes relationship column") + custom_tables = [] + for table_name in sorted(table_names): + if table_name in {chain_table, node_table, primary_key_table}: + continue + columns = table_columns(connection, table_name) + if "CUSTOMNODES" in table_name or any( + "CUSTOMNODES" in column for column in columns + ): + custom_tables.append((table_name, columns)) + if custom_tables: + raise RuntimeError("Substrate store has an unexpected customNodes join table") + + chains = {} + for primary_key, chain_id, selected_node in connection.execute( + f""" + SELECT Z_PK, ZCHAINID, ZSELECTEDNODE + FROM {quote_identifier(chain_table)} + """ + ).fetchall(): + if ( + not isinstance(primary_key, int) + or not isinstance(chain_id, str) + or not chain_id.strip() + ): + raise RuntimeError("Substrate chain topology contains an invalid identity") + chains[primary_key] = (chain_id, selected_node) + if not chains: + raise RuntimeError("Substrate chain topology is vacuous") + + optional_node_columns = tuple( + column + for column in ("ZAPIKEYNAME", "ZAPIQUERYNAME") + if column in node_columns + ) + selected_node_columns = ( + "Z_PK", + "ZCHAIN", + inline_custom_column, + "ZNAME", + "ZURL", + ) + optional_node_columns + node_query_columns = ", ".join( + quote_identifier(column) for column in selected_node_columns + ) + nodes = {} + for row in connection.execute( + f"SELECT {node_query_columns} FROM {quote_identifier(node_table)}" + ).fetchall(): + primary_key, default_chain, custom_chain, name, url, *credentials = row + if not isinstance(primary_key, int): + raise RuntimeError("Substrate node topology contains an invalid primary key") + nodes[primary_key] = ( + default_chain, + custom_chain, + name, + url, + *credentials, + ) + if not nodes: + raise RuntimeError("Substrate node topology is vacuous") + + custom_edges = set() + for node_key, node in nodes.items(): + custom_chain = node[1] + if custom_chain is not None: + if custom_chain not in chains: + raise RuntimeError("Substrate customNodes relationship is dangling") + custom_edges.add((custom_chain, node_key)) + if not custom_edges: + raise RuntimeError("Substrate fixture has no custom-node relationships") + + selected_edges = set() + for chain_key, (_, selected_node) in chains.items(): + if selected_node is not None: + if selected_node not in nodes: + raise RuntimeError("Substrate selected-node relationship is dangling") + selected_edges.add((chain_key, selected_node)) + + default_edges = set() + for node_key, node in nodes.items(): + default_chain = node[0] + if default_chain is not None: + if default_chain not in chains: + raise RuntimeError("Substrate default-node relationship is dangling") + default_edges.add((default_chain, node_key)) + + has_custom_selected = bool(custom_edges & selected_edges) + has_custom_only = any( + any(source == chain_key for source, _ in custom_edges) + and not any(source == chain_key for source, _ in default_edges) + for chain_key in chains + ) + if not has_custom_selected: + raise RuntimeError("Substrate fixture lacks a selected custom-node topology") + if not has_custom_only: + raise RuntimeError("Substrate fixture lacks a custom-only chain topology") + + topology_rows = [] + for chain_key, (chain_id, selected_node) in chains.items(): + topology_rows.append(("chain", chain_id, selected_node)) + for relationship, edges in ( + ("default", default_edges), + ("custom", custom_edges), + ("selected", selected_edges), + ): + for source, node_key in sorted(edges): + if source == chain_key: + topology_rows.append( + (relationship, chain_id, node_key, *nodes[node_key][2:]) + ) + + referenced_nodes = { + node_key + for _, node_key in default_edges | custom_edges | selected_edges + } + orphan_rows = [ + ("orphan", node_key, *node[2:]) + for node_key, node in nodes.items() + if node_key not in referenced_nodes + ] + return { + "fingerprints": { + "chain-default-custom-selected-topology": rows_digest(topology_rows), + "orphan-node-topology": rows_digest(orphan_rows), + }, + "coverage": { + "customOnlyChain": has_custom_only, + "selectedCustomNode": has_custom_selected, + }, + } + try: for store_name in stores: store_path = os.path.join(directory, store_name) @@ -636,6 +855,14 @@ try: store_result["protectedFingerprints"] = ( protected_user_fingerprints(connection, set(table_names)) ) + if store_name == "SubstrateDataModel.sqlite": + topology = protected_substrate_topology( + connection, set(table_names) + ) + store_result["protectedTopologyFingerprints"] = ( + topology["fingerprints"] + ) + store_result["topologyCoverage"] = topology["coverage"] result[store_name] = store_result finally: connection.close() @@ -740,6 +967,49 @@ for table_name in substrate_critical_tables: if actual_count != expected_count: print(f"{phase_name} changed the protected {table_name} row count", file=sys.stderr) sys.exit(9) + +before_topology_store = before["SubstrateDataModel.sqlite"] +before_coverage = before_topology_store.get("topologyCoverage") +if before_coverage != { + "customOnlyChain": True, + "selectedCustomNode": True, +}: + print( + "prelaunch Substrate fixture lacks required custom-node topology coverage", + file=sys.stderr, + ) + sys.exit(10) +before_topology = before_topology_store.get("protectedTopologyFingerprints") +if not isinstance(before_topology, dict) or not before_topology: + print("prelaunch Substrate store lacks topology fingerprints", file=sys.stderr) + sys.exit(11) +for fingerprint_name, expected_fingerprint in before_topology.items(): + if ( + not isinstance(fingerprint_name, str) + or not isinstance(expected_fingerprint, str) + or len(expected_fingerprint) != 64 + ): + print("prelaunch Substrate topology fingerprint is malformed", file=sys.stderr) + sys.exit(12) + for phase_name, payload in (("first launch", first), ("relaunch", second)): + topology_store = payload["SubstrateDataModel.sqlite"] + if topology_store.get("topologyCoverage") != before_coverage: + print( + f"{phase_name} changed required custom-node topology coverage", + file=sys.stderr, + ) + sys.exit(13) + actual_fingerprint = ( + topology_store + .get("protectedTopologyFingerprints", {}) + .get(fingerprint_name) + ) + if actual_fingerprint != expected_fingerprint: + print( + f"{phase_name} changed protected default/custom/selected/orphan node topology", + file=sys.stderr, + ) + sys.exit(14) PY } @@ -1054,6 +1324,21 @@ while [[ "$#" -gt 0 ]]; do BUNDLE_ID="$2" shift 2 ;; + --expected-git-sha) + require_option_value "$1" "$#" + EXPECTED_GIT_SHA="$2" + shift 2 + ;; + --expected-build) + require_option_value "$1" "$#" + EXPECTED_BUILD="$2" + shift 2 + ;; + --expected-executable-sha256) + require_option_value "$1" "$#" + EXPECTED_EXECUTABLE_SHA256="$2" + shift 2 + ;; --fixture-dir) require_option_value "$1" "$#" FIXTURE_DIR="$2" @@ -1100,6 +1385,14 @@ done [[ -n "$BUNDLE_ID" ]] || fail "--bundle-id is required" [[ "$BUNDLE_ID" =~ $BUNDLE_ID_PATTERN ]] || fail "--bundle-id is malformed" +[[ "$BUNDLE_ID" == "$EXPECTED_BUNDLE_ID" ]] || + fail "--bundle-id must be the production Fearless identity" +[[ "$EXPECTED_GIT_SHA" =~ ^[0-9A-Fa-f]{40}$ ]] || + fail "--expected-git-sha must be an exact 40-hex commit" +[[ "$EXPECTED_BUILD" =~ ^[1-9][0-9]{0,3}(\.[0-9]{1,2}){0,2}$ ]] || + fail "--expected-build must be an explicit canonical CFBundleVersion" +[[ "$EXPECTED_EXECUTABLE_SHA256" =~ ^[0-9A-Fa-f]{64}$ ]] || + fail "--expected-executable-sha256 must be 64 hex" [[ -n "$FIXTURE_DIR" ]] || fail "--fixture-dir is required" [[ "$FIXTURE_DIR" == /* ]] || fail "--fixture-dir must be an absolute path" [[ -n "$ARTIFACTS_DIR" ]] || fail "--artifacts-dir is required" @@ -1348,5 +1641,6 @@ cmp -s \ fail "source fixture changed during the rehearsal" log "PASSED: first launch and relaunch each reached a usable startup route with no crash or fatal migration/Core Data marker" -log "PASSED: copied stores remain intact; protected counts, wallet identities, keys, and relationships are preserved" -log "PASSED: signed Keychain-capable Release/-O/non-testable artifact, 26 managed-object classes, and 29 migration resources verified; source fixture is unchanged" +log "PASSED: copied stores remain intact; protected wallet and custom-node topology fingerprints are preserved" +log "PASSED: signed Simulator Release/-O/non-testable artifact, exact commit/version/build/executable hash, 26 managed-object classes, and 29 migration resources verified; source fixture is unchanged" +log "NOTE: device distribution profile, production entitlements, and TestFlight Keychain capability require audit-ios-signed-release-artifact.sh against the exact .xcarchive" diff --git a/scripts/test-coredata-release-gate.sh b/scripts/test-coredata-release-gate.sh index 7e37be83dd..8f87dec30d 100644 --- a/scripts/test-coredata-release-gate.sh +++ b/scripts/test-coredata-release-gate.sh @@ -44,6 +44,7 @@ run_gate() { mkdir -p "$RUN_DIRECTORY" env \ + FEARLESS_CORE_DATA_TEST_HARNESS=1 \ FEARLESS_CORE_DATA_ROOT_DIR="$FIXTURE_ROOT" \ FEARLESS_CORE_DATA_XCODEBUILD_BIN="$BIN_DIR/xcodebuild" \ FEARLESS_CORE_DATA_XCRUN_BIN="$BIN_DIR/xcrun" \ @@ -52,6 +53,8 @@ run_gate() { FAKE_ENVIRONMENT_LOG="$RUN_DIRECTORY/environment" \ FAKE_SIMULATOR_UDID="$SIMULATOR_UDID" \ FAKE_OTHER_SIMULATOR_UDID="$OTHER_SIMULATOR_UDID" \ + FAKE_CORE_MANIFEST="$SCRIPT_DIR/ci/manifests/coredata-release-core-tests.txt" \ + FAKE_PHONE_MANIFEST="$SCRIPT_DIR/ci/manifests/coredata-release-copied-phone-tests.txt" \ ${CASE_ENV[@]+"${CASE_ENV[@]}"} \ bash "$GATE" \ --output-dir "$RUN_DIRECTORY/output" \ @@ -159,6 +162,62 @@ JSON fi if [[ "${1:-}" == "xcresulttool" ]]; then + if printf '%s\n' "$@" | grep -Fqx "tests"; then + case "${FAKE_TESTS_MODE:-valid}" in + tool-failure) + exit 1 + ;; + malformed) + printf '%s\n' '{not-json' + exit 0 + ;; + esac + result_path="" + previous="" + for argument in "$@"; do + if [[ "$previous" == "--path" ]]; then + result_path="$argument" + fi + previous="$argument" + done + if [[ "$result_path" == *"/copied-phone.xcresult" ]]; then + manifest="$FAKE_PHONE_MANIFEST" + else + manifest="$FAKE_CORE_MANIFEST" + fi + python3 - "$manifest" "${FAKE_TESTS_MODE:-valid}" <<'PY' +import json +import sys + +manifest, mode = sys.argv[1:] +with open(manifest, "r", encoding="utf-8") as source: + identities = [ + line.strip() + for line in source + if line.strip() and not line.lstrip().startswith("#") + ] +if mode == "missing": + identities = identities[:-1] +elif mode == "unexpected": + identities[-1] = "fearlessTests/UnexpectedTests/testInjectedIdentity" +elif mode == "duplicate": + identities[-1] = identities[0] + +nodes = [ + { + "nodeIdentifier": identity + "()", + "nodeType": "Test Case", + "result": "Passed", + } + for identity in identities +] +if mode == "canceled": + nodes[0]["result"] = "Cancelled" +print(json.dumps({"testNodes": nodes}, separators=(",", ":"))) +PY + exit 0 + fi + if [[ "${FAKE_SUMMARY_MODE:-valid}" == "tool-failure" ]]; then exit 1 fi @@ -374,6 +433,30 @@ CASE_ENV=("FAKE_SUMMARY_MODE=tool-failure") GATE_ARGS=(--stage core --simulator-udid "$SIMULATOR_UDID") expect_failure "unreadable-summary" "xcresulttool could not read" +CASE_ENV=("FAKE_TESTS_MODE=missing") +GATE_ARGS=(--stage core --simulator-udid "$SIMULATOR_UDID") +expect_failure "missing-test-identity" "exact reviewed identity manifest" + +CASE_ENV=("FAKE_TESTS_MODE=unexpected") +GATE_ARGS=(--stage core --simulator-udid "$SIMULATOR_UDID") +expect_failure "unexpected-test-identity" "exact reviewed identity manifest" + +CASE_ENV=("FAKE_TESTS_MODE=duplicate") +GATE_ARGS=(--stage core --simulator-udid "$SIMULATOR_UDID") +expect_failure "duplicate-test-identity" "exact reviewed identity manifest" + +CASE_ENV=("FAKE_TESTS_MODE=canceled") +GATE_ARGS=(--stage core --simulator-udid "$SIMULATOR_UDID") +expect_failure "canceled-test-identity" "exact reviewed identity manifest" + +CASE_ENV=("FAKE_TESTS_MODE=malformed") +GATE_ARGS=(--stage core --simulator-udid "$SIMULATOR_UDID") +expect_failure "malformed-test-inventory" "exact reviewed identity manifest" + +CASE_ENV=("FAKE_TESTS_MODE=tool-failure") +GATE_ARGS=(--stage core --simulator-udid "$SIMULATOR_UDID") +expect_failure "unreadable-test-inventory" "could not read the executed test identities" + CASE_ENV=( "FAKE_TOTAL_TESTS=412" "FAKE_PASSED_TESTS=242" @@ -419,5 +502,19 @@ CASE_ENV=() GATE_ARGS=(--stage impossible --simulator-udid "$SIMULATOR_UDID") expect_failure "unknown-stage" "core, copied-phone, or all" +RUN_NUMBER=$((RUN_NUMBER + 1)) +RUN_DIRECTORY="$TEMPORARY_DIR/run-${RUN_NUMBER}-override-without-harness" +mkdir -p "$RUN_DIRECTORY" +if env \ + FEARLESS_CORE_DATA_XCODEBUILD_BIN="$BIN_DIR/xcodebuild" \ + bash "$GATE" >"$RUN_DIRECTORY/stdout" 2>"$RUN_DIRECTORY/stderr"; then + fail "tool override without explicit harness unexpectedly passed" +fi +assert_output_contains \ + "accepted only with FEARLESS_CORE_DATA_TEST_HARNESS=1" \ + "$RUN_DIRECTORY/stderr" +printf '%s\n' \ + "[coredata-release-gate-test] PASS (rejected): override without harness" + printf '%s\n' \ - "[coredata-release-gate-test] PASS: 3 positive contracts + 23 negative/adversarial cases" + "[coredata-release-gate-test] PASS: 3 positive contracts + 30 negative/adversarial cases" diff --git a/scripts/test-coredata-simulator-rehearsal.sh b/scripts/test-coredata-simulator-rehearsal.sh index 0ef84494db..8bff42ec4f 100755 --- a/scripts/test-coredata-simulator-rehearsal.sh +++ b/scripts/test-coredata-simulator-rehearsal.sh @@ -77,10 +77,55 @@ definitions = { "INSERT INTO ZCACHE (ZVALUE) VALUES ('opaque')", ), "SubstrateDataModel.sqlite": ( - "CREATE TABLE ZCDCHAIN (Z_PK INTEGER PRIMARY KEY)", + """ + CREATE TABLE Z_PRIMARYKEY ( + Z_ENT INTEGER PRIMARY KEY, + Z_NAME TEXT + ) + """, + """ + CREATE TABLE ZCDCHAIN ( + Z_PK INTEGER PRIMARY KEY, + ZCHAINID TEXT, + ZSELECTEDNODE INTEGER + ) + """, + """ + CREATE TABLE ZCDCHAINNODE ( + Z_PK INTEGER PRIMARY KEY, + ZCHAIN INTEGER, + Z2CUSTOMNODES INTEGER, + ZNAME TEXT, + ZURL TEXT, + ZAPIKEYNAME TEXT, + ZAPIQUERYNAME TEXT + ) + """, "CREATE TABLE ZCDRUNTIMEMETADATAITEM (Z_PK INTEGER PRIMARY KEY)", - "INSERT INTO ZCDCHAIN DEFAULT VALUES", - "INSERT INTO ZCDCHAIN DEFAULT VALUES", + "INSERT INTO Z_PRIMARYKEY (Z_ENT, Z_NAME) VALUES (2, 'CDChain')", + "INSERT INTO Z_PRIMARYKEY (Z_ENT, Z_NAME) VALUES (3, 'CDChainNode')", + "INSERT INTO ZCDCHAIN (Z_PK, ZCHAINID, ZSELECTEDNODE) VALUES (1, 'chain-a', 102)", + "INSERT INTO ZCDCHAIN (Z_PK, ZCHAINID, ZSELECTEDNODE) VALUES (2, 'chain-custom-only', 103)", + """ + INSERT INTO ZCDCHAINNODE + (Z_PK, ZCHAIN, Z2CUSTOMNODES, ZNAME, ZURL, ZAPIKEYNAME, ZAPIQUERYNAME) + VALUES (101, 1, NULL, 'default-a', 'wss://default.example', NULL, NULL) + """, + """ + INSERT INTO ZCDCHAINNODE + (Z_PK, ZCHAIN, Z2CUSTOMNODES, ZNAME, ZURL, ZAPIKEYNAME, ZAPIQUERYNAME) + VALUES (102, NULL, 1, 'custom-selected-a', 'wss://custom-a.example', 'key', 'apiKey') + """, + """ + INSERT INTO ZCDCHAINNODE + (Z_PK, ZCHAIN, Z2CUSTOMNODES, ZNAME, ZURL, ZAPIKEYNAME, ZAPIQUERYNAME) + VALUES (103, NULL, 2, 'custom-only-selected', 'wss://custom-only.example', NULL, NULL) + """, + """ + INSERT INTO ZCDCHAINNODE + (Z_PK, ZCHAIN, Z2CUSTOMNODES, ZNAME, ZURL, ZAPIKEYNAME, ZAPIQUERYNAME) + VALUES (104, NULL, NULL, 'orphan', 'wss://orphan.example', NULL, NULL) + """, "INSERT INTO ZCDRUNTIMEMETADATAITEM DEFAULT VALUES", "INSERT INTO ZCDRUNTIMEMETADATAITEM DEFAULT VALUES", ), @@ -182,12 +227,18 @@ prepare_case() { $BUNDLE_ID CFBundleExecutable fearless + CFBundleShortVersionString + 4.2.0 + CFBundleVersion + 2026.7.26 FearlessBuildConfiguration Release FearlessSwiftOptimizationLevel -O FearlessEnableTestability NO + FearlessGitCommit + 7a819cb01e92920e5444151392df54245acdd4c8 PLIST @@ -196,6 +247,8 @@ PLIST exit 0 APP chmod +x "$APP_DIR/fearless" + local executable_sha + executable_sha="$(shasum -a 256 "$APP_DIR/fearless" | awk '{print $1}')" mkdir -p \ "$APP_DIR/Modules_SSFAccountManagmentStorage.bundle/UserDataModel.momd" \ "$APP_DIR/SubstrateDataModel.momd" @@ -240,6 +293,9 @@ APP --simulator-udid "$SIMULATOR_UDID" --app "$APP_DIR" --bundle-id "$BUNDLE_ID" + --expected-git-sha 7a819cb01e92920e5444151392df54245acdd4c8 + --expected-build 2026.7.26 + --expected-executable-sha256 "$executable_sha" --fixture-dir "$FIXTURE_DIR" --artifacts-dir "$ARTIFACTS_DIR" --crash-report-dir "$CRASH_DIR" @@ -252,6 +308,7 @@ run_case() { local label="$1" env \ + FEARLESS_REHEARSAL_TEST_HARNESS=1 \ FEARLESS_REHEARSAL_XCRUN_BIN="$TEMPORARY_DIR/bin/xcrun" \ FEARLESS_REHEARSAL_CODESIGN_BIN="$TEMPORARY_DIR/bin/codesign" \ FEARLESS_REHEARSAL_PROCESS_CHECK_BIN="$TEMPORARY_DIR/bin/process-check" \ @@ -454,7 +511,39 @@ PY import sqlite3 import sys connection = sqlite3.connect(sys.argv[1]) -connection.execute("DELETE FROM ZCDCHAIN WHERE Z_PK = (SELECT MAX(Z_PK) FROM ZCDCHAIN)") +connection.execute("DELETE FROM ZCDCHAINNODE WHERE Z_PK IN (101, 102)") +connection.execute("DELETE FROM ZCDCHAIN WHERE Z_PK = 1") +connection.commit() +connection.close() +PY + fi + if [[ "${FAKE_MUTATE_CUSTOM_NODE:-0}" == "1" ]]; then + python3 - "$core_data/SubstrateDataModel.sqlite" <<'PY' +import sqlite3 +import sys +connection = sqlite3.connect(sys.argv[1]) +connection.execute("UPDATE ZCDCHAINNODE SET ZURL = 'wss://mutated.example' WHERE Z_PK = 102") +connection.commit() +connection.close() +PY + fi + if [[ "${FAKE_MUTATE_CUSTOM_MEMBERSHIP:-0}" == "1" ]]; then + python3 - "$core_data/SubstrateDataModel.sqlite" <<'PY' +import sqlite3 +import sys +connection = sqlite3.connect(sys.argv[1]) +connection.execute("UPDATE ZCDCHAINNODE SET Z2CUSTOMNODES = 1 WHERE Z_PK = 104") +connection.execute("UPDATE ZCDCHAINNODE SET Z2CUSTOMNODES = NULL WHERE Z_PK = 102") +connection.commit() +connection.close() +PY + fi + if [[ "${FAKE_MUTATE_SELECTED_NODE:-0}" == "1" ]]; then + python3 - "$core_data/SubstrateDataModel.sqlite" <<'PY' +import sqlite3 +import sys +connection = sqlite3.connect(sys.argv[1]) +connection.execute("UPDATE ZCDCHAIN SET ZSELECTEDNODE = 101 WHERE Z_PK = 1") connection.commit() connection.close() PY @@ -514,6 +603,9 @@ CASE_ARGS=( --simulator-udid "$SIMULATOR_UDID" --app "$APP_DIR" --bundle-id "$BUNDLE_ID" + --expected-git-sha 7a819cb01e92920e5444151392df54245acdd4c8 + --expected-build 2026.7.26 + --expected-executable-sha256 "$(shasum -a 256 "$APP_DIR/fearless" | awk '{print $1}')" --fixture-dir "$FIXTURE_DIR" --artifacts-dir "$ARTIFACTS_DIR" --crash-report-dir "$CRASH_DIR" @@ -566,6 +658,27 @@ plutil -replace FearlessEnableTestability -string YES "$APP_DIR/Info.plist" CASE_ARGS+=(--dry-run) expect_failure "testability enabled" "built with testability enabled" +prepare_case "wrong-git-commit" +plutil -replace FearlessGitCommit \ + -string aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa "$APP_DIR/Info.plist" +CASE_ARGS+=(--dry-run) +expect_failure "wrong git commit" "git commit does not match" + +prepare_case "wrong-marketing-version" +plutil -replace CFBundleShortVersionString -string 4.1.0 "$APP_DIR/Info.plist" +CASE_ARGS+=(--dry-run) +expect_failure "wrong marketing version" "marketing version is not" + +prepare_case "wrong-build-number" +plutil -replace CFBundleVersion -string 1 "$APP_DIR/Info.plist" +CASE_ARGS+=(--dry-run) +expect_failure "wrong build number" "build number is not" + +prepare_case "wrong-executable-digest" +printf '%s\n' "# post-build mutation" >>"$APP_DIR/fearless" +CASE_ARGS+=(--dry-run) +expect_failure "wrong executable digest" "executable SHA-256 does not match" + prepare_case "missing-managed-class" CASE_ENV=("FAKE_MISSING_MANAGED_CLASS=CDChain") CASE_ARGS+=(--dry-run) @@ -608,6 +721,9 @@ CASE_ARGS=( --simulator-udid "$SIMULATOR_UDID" --app "$APP_DIR" --bundle-id "$BUNDLE_ID" + --expected-git-sha 7a819cb01e92920e5444151392df54245acdd4c8 + --expected-build 2026.7.26 + --expected-executable-sha256 "$(shasum -a 256 "$APP_DIR/fearless" | awk '{print $1}')" --fixture-dir "$FIXTURE_DIR" --artifacts-dir "$ARTIFACTS_DIR" --crash-report-dir "$CRASH_DIR" @@ -659,6 +775,37 @@ prepare_case "substrate-nonzero-row-loss" CASE_ENV=("FAKE_DROP_ONE_SUBSTRATE_ROW=1") expect_failure "nonzero Substrate row loss" "changed the protected ZCDCHAIN row count" +prepare_case "custom-node-payload-mutated" +CASE_ENV=("FAKE_MUTATE_CUSTOM_NODE=1") +expect_failure \ + "custom node payload mutation" \ + "changed protected default/custom/selected/orphan node topology" + +prepare_case "custom-node-membership-mutated" +CASE_ENV=("FAKE_MUTATE_CUSTOM_MEMBERSHIP=1") +expect_failure \ + "custom node membership mutation" \ + "changed protected default/custom/selected/orphan node topology" + +prepare_case "selected-node-mutated" +CASE_ENV=("FAKE_MUTATE_SELECTED_NODE=1") +expect_failure \ + "selected node mutation" \ + "changed protected default/custom/selected/orphan node topology" + +prepare_case "vacuous-custom-topology" +python3 - "$FIXTURE_DIR/SubstrateDataModel.sqlite" <<'PY' +import sqlite3 +import sys +connection = sqlite3.connect(sys.argv[1]) +connection.execute("UPDATE ZCDCHAINNODE SET Z2CUSTOMNODES = NULL") +connection.execute("UPDATE ZCDCHAIN SET ZSELECTEDNODE = NULL") +connection.commit() +connection.close() +PY +CASE_ARGS+=(--dry-run) +expect_failure "vacuous custom topology" "has no custom-node relationships" + prepare_case "matching-crash-report" CASE_ENV=("FAKE_CREATE_CRASH=1") expect_failure "matching crash report" "new crash report matched" @@ -667,5 +814,15 @@ prepare_case "matching-crash-without-device-id" CASE_ENV=("FAKE_CREATE_CRASH_WITHOUT_DEVICE=1") expect_failure "matching crash report without device ID" "new crash report matched" +prepare_case "override-without-harness" +if env \ + FEARLESS_REHEARSAL_XCRUN_BIN="$TEMPORARY_DIR/bin/xcrun" \ + bash "$REHEARSAL" >"$CASE_DIR/stdout" 2>"$CASE_DIR/stderr"; then + fail "tool override without explicit harness unexpectedly passed" +fi +assert_contains "accepted only with FEARLESS_REHEARSAL_TEST_HARNESS=1" "$CASE_DIR/stderr" +printf '%s\n' \ + "[coredata-simulator-rehearsal-test] PASS (rejected): override without harness" + printf '%s\n' \ - "[coredata-simulator-rehearsal-test] PASS: 2 positive + 30 negative/adversarial contracts" + "[coredata-simulator-rehearsal-test] PASS: 2 positive + 39 negative/adversarial contracts" diff --git a/scripts/test-ios-release-identity-audit.sh b/scripts/test-ios-release-identity-audit.sh index 9cbd6bf7c6..feafab6bec 100755 --- a/scripts/test-ios-release-identity-audit.sh +++ b/scripts/test-ios-release-identity-audit.sh @@ -44,6 +44,7 @@ jq -n '[ CURRENT_PROJECT_VERSION: "2026.7.26", CODE_SIGN_ENTITLEMENTS: "fearless/WalletConnect.entitlements", CODE_SIGN_STYLE: "Automatic", + DEVELOPMENT_TEAM: "YLWWUD25VZ", SWIFT_OPTIMIZATION_LEVEL: "-O", ENABLE_TESTABILITY: "NO" } @@ -57,7 +58,8 @@ jq -n '[ buildSettings: { PRODUCT_BUNDLE_IDENTIFIER: "jp.co.soramitsu.fearlesswallet.dev", CODE_SIGN_ENTITLEMENTS: "fearless/WalletConnect.dev.entitlements", - CODE_SIGN_STYLE: "Automatic" + CODE_SIGN_STYLE: "Automatic", + DEVELOPMENT_TEAM: "YLWWUD25VZ" } } ]' > "$debug_settings" @@ -70,6 +72,8 @@ run_audit() { local debug_settings_file="${6:-$debug_settings}" local debug_entitlements_file="${7:-$dev_entitlements}" + IOS_RELEASE_AUDIT_TEST_HARNESS=1 \ + IOS_EXPECTED_BUILD_NUMBER=2026.7.26 \ IOS_RELEASE_SETTINGS_JSON="$1" \ IOS_DEBUG_SETTINGS_JSON="$debug_settings_file" \ IOS_RELEASE_SCHEME_FILE="$2" \ @@ -150,6 +154,11 @@ mutate_setting CODE_SIGN_STYLE Manual "$manual_signing" run_reject manual-signing \ run_audit "$manual_signing" "$scheme" "$entitlements" +wrong_team="$FIXTURES/wrong-team.json" +mutate_setting DEVELOPMENT_TEAM AAAAAAAAAA "$wrong_team" +run_reject wrong-development-team \ + run_audit "$wrong_team" "$scheme" "$entitlements" + dev_entitlement_setting="$FIXTURES/dev-entitlement-setting.json" mutate_setting CODE_SIGN_ENTITLEMENTS \ fearless/WalletConnect.dev.entitlements "$dev_entitlement_setting" @@ -197,6 +206,11 @@ mutate_debug_setting CODE_SIGN_STYLE Manual "$debug_manual_signing" run_reject debug-manual-signing \ run_debug_fixture "$debug_manual_signing" +debug_wrong_team="$FIXTURES/debug-wrong-team.json" +mutate_debug_setting DEVELOPMENT_TEAM AAAAAAAAAA "$debug_wrong_team" +run_reject debug-wrong-development-team \ + run_debug_fixture "$debug_wrong_team" + duplicate_debug_target="$FIXTURES/duplicate-debug-target.json" jq '. + [.[0]]' "$debug_settings" > "$duplicate_debug_target" run_reject duplicate-debug-target \ @@ -237,7 +251,7 @@ run_reject wrong-group run_audit "$settings" "$scheme" "$wrong_group" keychain_group="$FIXTURES/keychain-group.entitlements" mutate_entitlements \ - '.["keychain-access-groups"] = ["group.com.walletconnect.sdk"]' \ + '.["keychain-access-groups"] = ["group.jp.co.soramitsu.fearlesswallet"]' \ "$keychain_group" run_reject unexpected-keychain-group \ run_audit "$settings" "$scheme" "$keychain_group" @@ -261,6 +275,34 @@ plutil -convert json -o - "$dev_entitlements" | run_reject debug-production-group \ run_debug_fixture "$debug_settings" "$debug_entitlements_production_group" +debug_entitlements_missing_group="$FIXTURES/debug-missing-group.entitlements" +plutil -convert json -o - "$dev_entitlements" | + jq 'del(.["com.apple.security.application-groups"])' | + plutil -convert xml1 -o "$debug_entitlements_missing_group" - +run_reject debug-missing-group \ + run_debug_fixture "$debug_settings" "$debug_entitlements_missing_group" + +debug_entitlements_legacy_group="$FIXTURES/debug-legacy-group.entitlements" +plutil -convert json -o - "$dev_entitlements" | + jq '.["com.apple.security.application-groups"] = ["group.com.walletconnect.sdk"]' | + plutil -convert xml1 -o "$debug_entitlements_legacy_group" - +run_reject debug-legacy-group \ + run_debug_fixture "$debug_settings" "$debug_entitlements_legacy_group" + +debug_entitlements_extra_group="$FIXTURES/debug-extra-group.entitlements" +plutil -convert json -o - "$dev_entitlements" | + jq '.["com.apple.security.application-groups"] += ["group.evil"]' | + plutil -convert xml1 -o "$debug_entitlements_extra_group" - +run_reject debug-extra-group \ + run_debug_fixture "$debug_settings" "$debug_entitlements_extra_group" + +debug_entitlements_keychain_group="$FIXTURES/debug-keychain-group.entitlements" +plutil -convert json -o - "$dev_entitlements" | + jq '.["keychain-access-groups"] = ["group.jp.co.soramitsu.fearlesswallet.walletconnect"]' | + plutil -convert xml1 -o "$debug_entitlements_keychain_group" - +run_reject debug-explicit-keychain-group \ + run_debug_fixture "$debug_settings" "$debug_entitlements_keychain_group" + debug_entitlements_production_cloud="$FIXTURES/debug-production-cloud.entitlements" plutil -convert json -o - "$dev_entitlements" | jq '.["com.apple.developer.icloud-container-identifiers"] = ["iCloud.jp.co.soramitsu.fearlesswallet"]' | @@ -288,13 +330,22 @@ run_reject hardcoded-marketing-version \ missing_production_group_service="$FIXTURES/missing-production-group.swift" sed \ - 's/group\.jp\.co\.soramitsu\.fearlesswallet/group.com.walletconnect.sdk/' \ + 's/static let productionGroupIdentifier = "group.jp.co.soramitsu.fearlesswallet"/static let productionGroupIdentifier = "group.com.walletconnect.sdk"/' \ "$REPO_ROOT/fearless/ApplicationLayer/Services/WalletConnect/WalletConnectService.swift" \ > "$missing_production_group_service" run_reject wallet-connect-production-group-mismatch \ run_audit "$settings" "$scheme" "$entitlements" \ "$REPO_ROOT/fearless/Info.plist" "$missing_production_group_service" +missing_development_group_service="$FIXTURES/missing-development-group.swift" +sed \ + 's/group\.jp\.co\.soramitsu\.fearlesswallet\.walletconnect/group.com.walletconnect.sdk/' \ + "$REPO_ROOT/fearless/ApplicationLayer/Services/WalletConnect/WalletConnectService.swift" \ + > "$missing_development_group_service" +run_reject wallet-connect-development-group-mismatch \ + run_audit "$settings" "$scheme" "$entitlements" \ + "$REPO_ROOT/fearless/Info.plist" "$missing_development_group_service" + hardcoded_group_service="$FIXTURES/hardcoded-wallet-connect-group.swift" sed \ 's/groupIdentifier: groupIdentifier/groupIdentifier: Self.walletConnectGroupIdentifier/' \ diff --git a/scripts/test-ios-signed-release-artifact-audit.sh b/scripts/test-ios-signed-release-artifact-audit.sh new file mode 100755 index 0000000000..f229826ca5 --- /dev/null +++ b/scripts/test-ios-signed-release-artifact-audit.sh @@ -0,0 +1,380 @@ +#!/usr/bin/env bash +set -euo pipefail + +readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +readonly AUDIT="$SCRIPT_DIR/ci/audit-ios-signed-release-artifact.sh" +readonly HASH_ARCHIVE="$SCRIPT_DIR/ci/hash-ios-archive.sh" +readonly TEMPORARY_DIR="$(mktemp -d "${TMPDIR:-/private/tmp}/fearless-signed-audit-tests.XXXXXX")" +readonly EXPECTED_GIT_SHA="7a819cb01e92920e5444151392df54245acdd4c8" + +trap 'rm -rf "$TEMPORARY_DIR"' EXIT + +RUN_NUMBER=0 +CASE_DIR="" +ARCHIVE="" +APP="" +SIGNED_ENTITLEMENTS="" +PROFILE="" +SIGNING_CERTIFICATE="" +CASE_ENV=() + +fail() { + printf '%s\n' "[ios-signed-release-audit-test][error] $*" >&2 + exit 1 +} + +assert_contains() { + local expected="$1" + local file="$2" + grep -Fq -- "$expected" "$file" || + fail "expected '$expected' in $(basename "$file")" +} + +prepare_case() { + local label="$1" + RUN_NUMBER=$((RUN_NUMBER + 1)) + CASE_DIR="$TEMPORARY_DIR/case-${RUN_NUMBER}-${label}" + ARCHIVE="$CASE_DIR/fearless.xcarchive" + APP="$ARCHIVE/Products/Applications/fearless.app" + SIGNED_ENTITLEMENTS="$CASE_DIR/signed-entitlements.plist" + PROFILE="$CASE_DIR/profile.plist" + SIGNING_CERTIFICATE="$CASE_DIR/signing-certificate.cer" + mkdir -p "$APP" "$CASE_DIR/output" + + python3 - "$ARCHIVE/Info.plist" "$APP/Info.plist" \ + "$SIGNED_ENTITLEMENTS" "$PROFILE" "$SIGNING_CERTIFICATE" \ + "$EXPECTED_GIT_SHA" <<'PY' +import datetime +import plistlib +import sys + +( + archive_path, + app_path, + entitlements_path, + profile_path, + certificate_path, + git_sha, +) = sys.argv[1:] +team = "YLWWUD25VZ" +bundle = "jp.co.soramitsu.fearlesswallet" +application_id = f"{team}.{bundle}" +certificate = b"canonical Fearless App Store signing certificate" +capabilities = { + "com.apple.developer.associated-domains": [ + "applinks:fearlesswallet.io", + "webcredentials:fearlesswallet.io", + ], + "com.apple.developer.icloud-container-identifiers": [ + "iCloud.jp.co.soramitsu.fearlesswallet" + ], + "com.apple.developer.icloud-services": ["CloudKit"], + "com.apple.security.application-groups": [ + "group.jp.co.soramitsu.fearlesswallet" + ], +} +signed = { + **capabilities, + "application-identifier": application_id, + "com.apple.developer.team-identifier": team, +} +profile_entitlements = { + "com.apple.developer.associated-domains": "*", + "com.apple.developer.icloud-container-development-container-identifiers": [ + "iCloud.jp.co.soramitsu.fearlesswallet" + ], + "com.apple.developer.icloud-container-environment": [ + "Production", + "Development", + ], + "com.apple.developer.icloud-container-identifiers": [ + "iCloud.jp.co.soramitsu.fearlesswallet" + ], + "com.apple.developer.icloud-services": "*", + "com.apple.developer.ubiquity-container-identifiers": [ + "iCloud.jp.co.soramitsu.fearlesswallet" + ], + "com.apple.developer.ubiquity-kvstore-identifier": f"{team}.*", + "com.apple.security.application-groups": [ + "group.jp.co.soramitsu.fearlesswallet" + ], + "application-identifier": application_id, + "com.apple.developer.team-identifier": team, + "get-task-allow": False, + "beta-reports-active": True, + "keychain-access-groups": ["YLWWUD25VZ.*", "com.apple.token"], +} +archive = { + "ApplicationProperties": { + "ApplicationPath": "Products/Applications/fearless.app", + "CFBundleIdentifier": bundle, + "SigningIdentity": "Apple Distribution: Fearless Test ($team)", + "Team": team, + } +} +app = { + "CFBundleExecutable": "fearless", + "CFBundleIdentifier": bundle, + "CFBundleShortVersionString": "4.2.0", + "CFBundleVersion": "2026.7.26", + "FearlessBuildConfiguration": "Release", + "FearlessEnableTestability": "NO", + "FearlessGitCommit": git_sha, + "FearlessSwiftOptimizationLevel": "-O", +} +profile = { + "CreationDate": datetime.datetime(2026, 1, 1), + "DeveloperCertificates": [certificate], + "Entitlements": profile_entitlements, + "ExpirationDate": datetime.datetime(2035, 1, 1), + "Name": "Fearless App Store", + "TeamIdentifier": [team], + "TeamName": "Fearless", + "UUID": "11111111-2222-3333-4444-555555555555", +} +for path, payload in ( + (archive_path, archive), + (app_path, app), + (entitlements_path, signed), + (profile_path, profile), +): + with open(path, "wb") as destination: + plistlib.dump(payload, destination) +with open(certificate_path, "wb") as destination: + destination.write(certificate) +PY + + printf '%s\n' '#!/usr/bin/env bash' 'exit 0' >"$APP/fearless" + chmod +x "$APP/fearless" + printf '%s\n' "opaque production profile" >"$APP/embedded.mobileprovision" + CASE_ENV=() +} + +run_audit() { + local label="$1" + local executable_sha + local archive_sha + local signing_certificate_sha1 + executable_sha="$(shasum -a 256 "$APP/fearless" | awk '{print $1}')" + archive_sha="$(bash "$HASH_ARCHIVE" "$ARCHIVE")" + signing_certificate_sha1="$( + shasum -a 1 "$SIGNING_CERTIFICATE" | awk '{print $1}' + )" + + env \ + FEARLESS_SIGNED_AUDIT_TEST_HARNESS=1 \ + FEARLESS_SIGNED_AUDIT_CODESIGN_BIN="$TEMPORARY_DIR/bin/codesign" \ + FEARLESS_SIGNED_AUDIT_SECURITY_BIN="$TEMPORARY_DIR/bin/security" \ + FAKE_SIGNED_ENTITLEMENTS="$SIGNED_ENTITLEMENTS" \ + FAKE_SIGNING_CERTIFICATE="$SIGNING_CERTIFICATE" \ + FAKE_PROFILE_PLIST="$PROFILE" \ + ${CASE_ENV[@]+"${CASE_ENV[@]}"} \ + bash "$AUDIT" \ + --archive "$ARCHIVE" \ + --expected-git-sha "${EXPECTED_GIT_OVERRIDE:-$EXPECTED_GIT_SHA}" \ + --expected-build 2026.7.26 \ + --expected-executable-sha256 "${EXPECTED_EXECUTABLE_SHA_OVERRIDE:-$executable_sha}" \ + --expected-archive-sha256 "${EXPECTED_ARCHIVE_SHA_OVERRIDE:-$archive_sha}" \ + --expected-signing-certificate-sha1 "${EXPECTED_SIGNING_CERTIFICATE_SHA_OVERRIDE:-$signing_certificate_sha1}" \ + --expected-profile-uuid 11111111-2222-3333-4444-555555555555 \ + --expected-profile-name "Fearless App Store" \ + --receipt "$CASE_DIR/output/receipt.json" \ + >"$CASE_DIR/stdout" 2>"$CASE_DIR/stderr" +} + +expect_failure() { + local label="$1" + local expected="$2" + if run_audit "$label"; then + fail "$label unexpectedly passed" + fi + assert_contains "$expected" "$CASE_DIR/stderr" + printf '%s\n' "[ios-signed-release-audit-test] PASS (rejected): $label" +} + +mutate_plist() { + local path="$1" + local expression="$2" + python3 - "$path" "$expression" <<'PY' +import datetime +import plistlib +import sys + +path, expression = sys.argv[1:] +with open(path, "rb") as source: + value = plistlib.load(source) +scope = {"value": value, "datetime": datetime} +exec(expression, scope, scope) +with open(path, "wb") as destination: + plistlib.dump(value, destination) +PY +} + +mkdir -p "$TEMPORARY_DIR/bin" +printf '%s\n' \ + '#!/usr/bin/env bash' \ + 'set -euo pipefail' \ + 'if [[ "${FAKE_CODESIGN_VERIFY_FAIL:-0}" == "1" && "${1:-}" == "--verify" ]]; then exit 1; fi' \ + 'if [[ "${1:-}" == "-d" && "${2:-}" == --extract-certificates=* ]]; then' \ + ' prefix="${2#--extract-certificates=}"' \ + ' cp "$FAKE_SIGNING_CERTIFICATE" "${prefix}0"' \ + 'elif [[ "${1:-}" == "-d" ]]; then' \ + ' cp "$FAKE_SIGNED_ENTITLEMENTS" /dev/stdout' \ + 'fi' \ + >"$TEMPORARY_DIR/bin/codesign" +printf '%s\n' \ + '#!/usr/bin/env bash' \ + 'set -euo pipefail' \ + '[[ "${FAKE_SECURITY_FAIL:-0}" != "1" ]] || exit 1' \ + 'cp "$FAKE_PROFILE_PLIST" /dev/stdout' \ + >"$TEMPORARY_DIR/bin/security" +chmod +x "$TEMPORARY_DIR/bin/codesign" "$TEMPORARY_DIR/bin/security" + +prepare_case canonical +if ! run_audit canonical; then + sed -n '1,100p' "$CASE_DIR/stderr" >&2 + fail "canonical signed archive was rejected" +fi +assert_contains '"archiveTreeSHA256"' "$CASE_DIR/output/receipt.json" +assert_contains '"distributionProfile": "valid-app-store"' "$CASE_DIR/output/receipt.json" +printf '%s\n' "[ios-signed-release-audit-test] PASS: canonical signed archive" + +prepare_case wrong-commit +EXPECTED_GIT_OVERRIDE="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +expect_failure wrong-commit "embedded git commit" +unset EXPECTED_GIT_OVERRIDE + +prepare_case executable-digest +EXPECTED_EXECUTABLE_SHA_OVERRIDE="$(printf 'a%.0s' {1..64})" +expect_failure executable-digest "executable SHA-256" +unset EXPECTED_EXECUTABLE_SHA_OVERRIDE + +prepare_case archive-digest +EXPECTED_ARCHIVE_SHA_OVERRIDE="$(printf 'b%.0s' {1..64})" +expect_failure archive-digest "archive SHA-256" +unset EXPECTED_ARCHIVE_SHA_OVERRIDE + +prepare_case wrong-team +mutate_plist "$PROFILE" \ + 'value["TeamIdentifier"] = ["AAAAAAAAAA"]' +expect_failure wrong-team "signed entitlements or embedded distribution profile" + +prepare_case wrong-archive-team +mutate_plist "$ARCHIVE/Info.plist" \ + 'value["ApplicationProperties"]["Team"] = "AAAAAAAAAA"' +expect_failure wrong-archive-team "xcarchive metadata" + +prepare_case development-identity +mutate_plist "$ARCHIVE/Info.plist" \ + 'value["ApplicationProperties"]["SigningIdentity"] = "Apple Development: Unsafe"' +expect_failure development-identity "distribution identity" + +prepare_case development-profile +mutate_plist "$PROFILE" \ + 'value["ProvisionedDevices"] = ["DEVICE-UDID"]' +expect_failure development-profile "signed entitlements or embedded distribution profile" + +prepare_case non-testflight-profile +mutate_plist "$PROFILE" \ + 'del value["Entitlements"]["beta-reports-active"]' +expect_failure non-testflight-profile "signed entitlements or embedded distribution profile" + +prepare_case enterprise-profile +mutate_plist "$PROFILE" \ + 'value["ProvisionsAllDevices"] = True' +expect_failure enterprise-profile "signed entitlements or embedded distribution profile" + +prepare_case get-task-allow +mutate_plist "$SIGNED_ENTITLEMENTS" \ + 'value["get-task-allow"] = True' +expect_failure get-task-allow "signed entitlements or embedded distribution profile" + +prepare_case expired-profile +mutate_plist "$PROFILE" \ + 'value["ExpirationDate"] = datetime.datetime(2026, 2, 1)' +expect_failure expired-profile "signed entitlements or embedded distribution profile" + +prepare_case profile-expiring-too-soon +mutate_plist "$PROFILE" \ + 'value["ExpirationDate"] = datetime.datetime.now() + datetime.timedelta(days=7)' +expect_failure profile-expiring-too-soon "signed entitlements or embedded distribution profile" + +prepare_case replaced-walletconnect-group +mutate_plist "$SIGNED_ENTITLEMENTS" \ + 'value["com.apple.security.application-groups"] = ["group.com.walletconnect.sdk"]' +expect_failure replaced-walletconnect-group "signed entitlements or embedded distribution profile" + +prepare_case unexpected-signed-keychain-group +mutate_plist "$SIGNED_ENTITLEMENTS" \ + 'value["keychain-access-groups"] = ["group.jp.co.soramitsu.fearlesswallet"]' +expect_failure unexpected-signed-keychain-group "signed entitlements or embedded distribution profile" + +prepare_case profile-missing-keychain-group +mutate_plist "$PROFILE" \ + 'del value["Entitlements"]["keychain-access-groups"]' +expect_failure profile-missing-keychain-group "signed entitlements or embedded distribution profile" + +prepare_case profile-signing-certificate-mismatch +mutate_plist "$PROFILE" \ + 'value["DeveloperCertificates"] = [b"unrelated distribution certificate"]' +expect_failure profile-signing-certificate-mismatch "signed entitlements or embedded distribution profile" + +prepare_case wrong-profile-uuid +mutate_plist "$PROFILE" \ + 'value["UUID"] = "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE"' +expect_failure wrong-profile-uuid "signed entitlements or embedded distribution profile" + +prepare_case wrong-profile-name +mutate_plist "$PROFILE" \ + 'value["Name"] = "Unreviewed App Store Profile"' +expect_failure wrong-profile-name "signed entitlements or embedded distribution profile" + +prepare_case profile-unauthorized-associated-domains +mutate_plist "$PROFILE" \ + 'value["Entitlements"]["com.apple.developer.associated-domains"] = ["applinks:attacker.example"]' +expect_failure profile-unauthorized-associated-domains "signed entitlements or embedded distribution profile" + +prepare_case profile-missing-production-cloud-environment +mutate_plist "$PROFILE" \ + 'value["Entitlements"]["com.apple.developer.icloud-container-environment"] = ["Development"]' +expect_failure profile-missing-production-cloud-environment "signed entitlements or embedded distribution profile" + +prepare_case profile-wrong-ubiquity-container +mutate_plist "$PROFILE" \ + 'value["Entitlements"]["com.apple.developer.ubiquity-container-identifiers"] = ["iCloud.jp.co.soramitsu.fearlesswallet.dev"]' +expect_failure profile-wrong-ubiquity-container "signed entitlements or embedded distribution profile" + +prepare_case profile-unexpected-entitlement +mutate_plist "$PROFILE" \ + 'value["Entitlements"]["com.apple.developer.healthkit"] = True' +expect_failure profile-unexpected-entitlement "signed entitlements or embedded distribution profile" + +prepare_case unexpected-signed-entitlement +mutate_plist "$SIGNED_ENTITLEMENTS" \ + 'value["com.apple.developer.healthkit"] = True' +expect_failure unexpected-signed-entitlement "signed entitlements or embedded distribution profile" + +prepare_case development-cloud +mutate_plist "$SIGNED_ENTITLEMENTS" \ + 'value["com.apple.developer.icloud-container-identifiers"] = ["iCloud.jp.co.soramitsu.fearlesswallet.dev"]' +expect_failure development-cloud "signed entitlements or embedded distribution profile" + +prepare_case signature-failure +CASE_ENV=("FAKE_CODESIGN_VERIFY_FAIL=1") +expect_failure signature-failure "strict code-signature" + +prepare_case profile-decode-failure +CASE_ENV=("FAKE_SECURITY_FAIL=1") +expect_failure profile-decode-failure "could not be decoded" + +prepare_case override-without-harness +if env \ + FEARLESS_SIGNED_AUDIT_CODESIGN_BIN="$TEMPORARY_DIR/bin/codesign" \ + bash "$AUDIT" >"$CASE_DIR/stdout" 2>"$CASE_DIR/stderr"; then + fail "tool override without harness unexpectedly passed" +fi +assert_contains "only in the explicit test harness" "$CASE_DIR/stderr" +printf '%s\n' "[ios-signed-release-audit-test] PASS (rejected): override without harness" + +printf '%s\n' \ + "[ios-signed-release-audit-test] PASS: 1 positive + 27 negative/adversarial contracts" diff --git a/scripts/test-matrix.sh b/scripts/test-matrix.sh index 3e082c6be2..fb8ab7ce08 100755 --- a/scripts/test-matrix.sh +++ b/scripts/test-matrix.sh @@ -184,13 +184,11 @@ run_tests() { run_tests Debug if [[ "${HOST_ARCH}" == "x86_64" ]]; then - echo "\n==> Skipping Release simulator tests on x86_64 host due to missing native package symbols for simulator linking" + echo "\n==> ERROR: Release simulator tests are mandatory and cannot run on this x86_64 host" >&2 + echo "Dispatch this job to an arm64 macOS runner; a Debug-only result is not release evidence." >&2 + exit 78 else run_tests Release fi -if [[ "${HOST_ARCH}" == "x86_64" ]]; then - echo "\n==> Debug tests passed; Release simulator tests were skipped on x86_64 host" -else - echo "\n==> All tests passed in Debug and Release" -fi +echo "\n==> All tests passed in Debug and Release" From e6cb0e9eecac8f33d1c8c37fdb1ba4cc9d5e8cf1 Mon Sep 17 00:00:00 2001 From: Makoto Takemiya Date: Sun, 26 Jul 2026 16:21:00 +0400 Subject: [PATCH 12/30] Scope TestFlight provisioning to the app target --- .github/workflows/ios-release-safety.yml | 1 + .../ci/build-audited-ios-release-archive.sh | 8 +- ...st-ios-release-archive-signing-contract.sh | 75 +++++++++++++++++++ 3 files changed, 82 insertions(+), 2 deletions(-) create mode 100755 scripts/test-ios-release-archive-signing-contract.sh diff --git a/.github/workflows/ios-release-safety.yml b/.github/workflows/ios-release-safety.yml index ec7bdf67a1..42c364b357 100644 --- a/.github/workflows/ios-release-safety.yml +++ b/.github/workflows/ios-release-safety.yml @@ -37,6 +37,7 @@ jobs: run: | set -euo pipefail bash scripts/test-ios-release-identity-audit.sh + bash scripts/test-ios-release-archive-signing-contract.sh bash scripts/test-ios-signed-release-artifact-audit.sh bash scripts/test-coredata-release-gate.sh bash scripts/test-coredata-simulator-rehearsal.sh diff --git a/scripts/ci/build-audited-ios-release-archive.sh b/scripts/ci/build-audited-ios-release-archive.sh index 50d2e8de78..29d1d0e128 100644 --- a/scripts/ci/build-audited-ios-release-archive.sh +++ b/scripts/ci/build-audited-ios-release-archive.sh @@ -130,9 +130,13 @@ xcodebuild_arguments+=( "MARKETING_VERSION=$EXPECTED_VERSION" "FEARLESS_GIT_COMMIT=$source_commit" "DEVELOPMENT_TEAM=$EXPECTED_TEAM" - "CODE_SIGN_STYLE=Manual" + # Keep the application target's reviewed Automatic signing style. A + # PROVISIONING_PROFILE_SPECIFIER passed on the xcodebuild command line is a + # workspace-wide override: Xcode applies it to Pods and Swift-package + # resource bundles too, and those targets correctly reject provisioning + # profiles. The post-build audit below still fails closed unless Xcode chose + # the exact reviewed App Store profile and distribution certificate. "CODE_SIGN_IDENTITY=$EXPECTED_SIGNING_IDENTITY" - "PROVISIONING_PROFILE_SPECIFIER=$EXPECTED_PROFILE_NAME" clean archive ) diff --git a/scripts/test-ios-release-archive-signing-contract.sh b/scripts/test-ios-release-archive-signing-contract.sh new file mode 100755 index 0000000000..2c34a86a6b --- /dev/null +++ b/scripts/test-ios-release-archive-signing-contract.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +set -euo pipefail + +readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +readonly BUILD_SCRIPT="$SCRIPT_DIR/ci/build-audited-ios-release-archive.sh" +readonly IDENTITY_AUDIT="$SCRIPT_DIR/ci/audit-ios-release-identity.sh" +readonly FIXTURES="$(mktemp -d "${TMPDIR:-/tmp}/fearless-release-archive-signing-tests.XXXXXX")" +trap 'rm -rf "$FIXTURES"' EXIT + +fail() { + printf '%s ERROR: %s\n' "[ios-release-archive-signing-test]" "$*" >&2 + exit 1 +} + +contains_workspace_signing_override() { + grep -Eq \ + '^[[:space:]]*"?(CODE_SIGN_STYLE|PROVISIONING_PROFILE(_SPECIFIER)?)=' \ + "$1" +} + +[[ -f "$BUILD_SCRIPT" && ! -L "$BUILD_SCRIPT" ]] || + fail "archive build script is missing or unsafe" +[[ -f "$IDENTITY_AUDIT" && ! -L "$IDENTITY_AUDIT" ]] || + fail "release identity audit is missing or unsafe" + +# Command-line build-setting overrides apply to every target in a workspace. +# Profiles and manual signing must remain target-scoped, so neither is allowed +# in the xcodebuild argument vector assembled by this script. +if contains_workspace_signing_override "$BUILD_SCRIPT"; then + fail "archive script contains a workspace-wide signing/profile override" +fi + +# Prove the gate rejects both quoted/unquoted spellings and the legacy profile +# key. These mutations reproduce the class of command-line override that makes +# dependency targets fail with "does not support provisioning profiles." +override_number=0 +while IFS= read -r override; do + override_number=$((override_number + 1)) + mutant="$FIXTURES/override-$override_number.sh" + cp "$BUILD_SCRIPT" "$mutant" + printf '%s\n' "$override" >>"$mutant" + contains_workspace_signing_override "$mutant" || + fail "gate accepted workspace-wide override mutation: $override" +done <<'OVERRIDES' +CODE_SIGN_STYLE=Manual + "CODE_SIGN_STYLE=Automatic" +PROVISIONING_PROFILE=01234567-89AB-CDEF-0123-456789ABCDEF + "PROVISIONING_PROFILE=01234567-89AB-CDEF-0123-456789ABCDEF" +PROVISIONING_PROFILE_SPECIFIER=Fearless App Store + "PROVISIONING_PROFILE_SPECIFIER=Fearless App Store" +OVERRIDES + +grep -Fq '"CODE_SIGN_IDENTITY=$EXPECTED_SIGNING_IDENTITY"' "$BUILD_SCRIPT" || + fail "archive script does not bind the reviewed distribution identity" +grep -Fq \ + 'bash "$SCRIPT_DIR/audit-ios-signed-release-artifact.sh"' \ + "$BUILD_SCRIPT" || + fail "archive script no longer invokes the signed-artifact audit" +grep -Fq -- \ + '--expected-signing-certificate-sha1 "$EXPECTED_SIGNING_CERTIFICATE_SHA1"' \ + "$BUILD_SCRIPT" || + fail "signed-artifact audit is not bound to the reviewed certificate" +grep -Fq -- \ + '--expected-profile-uuid "$EXPECTED_PROFILE_UUID"' \ + "$BUILD_SCRIPT" || + fail "signed-artifact audit is not bound to the reviewed profile UUID" +grep -Fq -- \ + '--expected-profile-name "$EXPECTED_PROFILE_NAME"' \ + "$BUILD_SCRIPT" || + fail "signed-artifact audit is not bound to the reviewed profile name" +grep -Fq 'require_setting CODE_SIGN_STYLE "Automatic"' "$IDENTITY_AUDIT" || + fail "release identity no longer requires target-scoped Automatic signing" + +printf '%s\n' \ + "[ios-release-archive-signing-test] PASS: canonical contract and $override_number workspace-wide override mutations" From afbcde6cb41d15af0c305bc5854c8e0a55a6a949 Mon Sep 17 00:00:00 2001 From: Makoto Takemiya Date: Sun, 26 Jul 2026 16:26:18 +0400 Subject: [PATCH 13/30] Scope App Store signing to the release target --- fearless.xcodeproj/project.pbxproj | 5 ++-- scripts/ci/audit-ios-release-identity.sh | 6 +++- .../ci/build-audited-ios-release-archive.sh | 14 ++++------ ...st-ios-release-archive-signing-contract.sh | 20 +++++++++---- scripts/test-ios-release-identity-audit.sh | 28 +++++++++++++++---- 5 files changed, 51 insertions(+), 22 deletions(-) diff --git a/fearless.xcodeproj/project.pbxproj b/fearless.xcodeproj/project.pbxproj index d85dc45255..f9d0410cc7 100644 --- a/fearless.xcodeproj/project.pbxproj +++ b/fearless.xcodeproj/project.pbxproj @@ -19545,7 +19545,8 @@ ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon$(ICON_SUFFIX)"; CLONED_SOURCE_PACKAGES_DIR_PATH = "$(SRCROOT)/SourcePackages"; CODE_SIGN_ENTITLEMENTS = fearless/WalletConnect.entitlements; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_IDENTITY = "Apple Distribution: Soramitsu Co., Ltd. (YLWWUD25VZ)"; + CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 2026.7.26; DEVELOPMENT_TEAM = YLWWUD25VZ; ENABLE_BITCODE = NO; @@ -19560,7 +19561,7 @@ MARKETING_VERSION = 4.2.0; PRODUCT_BUNDLE_IDENTIFIER = jp.co.soramitsu.fearlesswallet; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; + PROVISIONING_PROFILE_SPECIFIER = "Fearless App Store 2026.7.26"; SWIFT_ENABLE_EXPLICIT_MODULES = NO; SWIFT_OBJC_BRIDGING_HEADER = "fearless/fearless-Bridging-Header.h"; SWIFT_VERSION = 5.0; diff --git a/scripts/ci/audit-ios-release-identity.sh b/scripts/ci/audit-ios-release-identity.sh index 07ea39263c..f8ad80dfba 100755 --- a/scripts/ci/audit-ios-release-identity.sh +++ b/scripts/ci/audit-ios-release-identity.sh @@ -5,6 +5,8 @@ readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" readonly REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" readonly EXPECTED_DEVELOPMENT_TEAM="YLWWUD25VZ" +readonly EXPECTED_SIGNING_IDENTITY="Apple Distribution: Soramitsu Co., Ltd. (YLWWUD25VZ)" +readonly EXPECTED_PROFILE_NAME="Fearless App Store 2026.7.26" readonly AUDIT_TEST_HARNESS="${IOS_RELEASE_AUDIT_TEST_HARNESS:-0}" fail() { @@ -205,8 +207,10 @@ require_setting PRODUCT_BUNDLE_IDENTIFIER "$EXPECTED_BUNDLE_ID" require_setting MARKETING_VERSION "$EXPECTED_VERSION" require_setting CURRENT_PROJECT_VERSION "$EXPECTED_BUILD" require_setting CODE_SIGN_ENTITLEMENTS "fearless/WalletConnect.entitlements" -require_setting CODE_SIGN_STYLE "Automatic" +require_setting CODE_SIGN_STYLE "Manual" +require_setting CODE_SIGN_IDENTITY "$EXPECTED_SIGNING_IDENTITY" require_setting DEVELOPMENT_TEAM "$EXPECTED_DEVELOPMENT_TEAM" +require_setting PROVISIONING_PROFILE_SPECIFIER "$EXPECTED_PROFILE_NAME" require_setting SWIFT_OPTIMIZATION_LEVEL "-O" require_setting ENABLE_TESTABILITY "NO" diff --git a/scripts/ci/build-audited-ios-release-archive.sh b/scripts/ci/build-audited-ios-release-archive.sh index 29d1d0e128..af9db52132 100644 --- a/scripts/ci/build-audited-ios-release-archive.sh +++ b/scripts/ci/build-audited-ios-release-archive.sh @@ -7,7 +7,6 @@ set -euo pipefail umask 077 readonly LOG_PREFIX="[ios-release-archive]" -readonly EXPECTED_TEAM="YLWWUD25VZ" readonly EXPECTED_BUNDLE="jp.co.soramitsu.fearlesswallet" readonly EXPECTED_VERSION="4.2.0" readonly EXPECTED_SIGNING_IDENTITY="Apple Distribution: Soramitsu Co., Ltd. (YLWWUD25VZ)" @@ -129,14 +128,11 @@ xcodebuild_arguments+=( "CURRENT_PROJECT_VERSION=$EXPECTED_BUILD" "MARKETING_VERSION=$EXPECTED_VERSION" "FEARLESS_GIT_COMMIT=$source_commit" - "DEVELOPMENT_TEAM=$EXPECTED_TEAM" - # Keep the application target's reviewed Automatic signing style. A - # PROVISIONING_PROFILE_SPECIFIER passed on the xcodebuild command line is a - # workspace-wide override: Xcode applies it to Pods and Swift-package - # resource bundles too, and those targets correctly reject provisioning - # profiles. The post-build audit below still fails closed unless Xcode chose - # the exact reviewed App Store profile and distribution certificate. - "CODE_SIGN_IDENTITY=$EXPECTED_SIGNING_IDENTITY" + # Signing is intentionally target-scoped in the application's Release build + # configuration. Any signing setting passed here would apply to every Pod, + # Swift package, and resource bundle in the workspace. The source identity + # audit above and signed-artifact audit below fail closed on the exact team, + # distribution identity, App Store profile, and production entitlements. clean archive ) diff --git a/scripts/test-ios-release-archive-signing-contract.sh b/scripts/test-ios-release-archive-signing-contract.sh index 2c34a86a6b..cc629f5dc5 100755 --- a/scripts/test-ios-release-archive-signing-contract.sh +++ b/scripts/test-ios-release-archive-signing-contract.sh @@ -14,7 +14,7 @@ fail() { contains_workspace_signing_override() { grep -Eq \ - '^[[:space:]]*"?(CODE_SIGN_STYLE|PROVISIONING_PROFILE(_SPECIFIER)?)=' \ + '^[[:space:]]*"?(CODE_SIGN_IDENTITY|CODE_SIGN_STYLE|DEVELOPMENT_TEAM|PROVISIONING_PROFILE(_SPECIFIER)?)=' \ "$1" } @@ -44,14 +44,16 @@ while IFS= read -r override; do done <<'OVERRIDES' CODE_SIGN_STYLE=Manual "CODE_SIGN_STYLE=Automatic" +CODE_SIGN_IDENTITY=Apple Distribution + "CODE_SIGN_IDENTITY=Apple Distribution" +DEVELOPMENT_TEAM=YLWWUD25VZ + "DEVELOPMENT_TEAM=YLWWUD25VZ" PROVISIONING_PROFILE=01234567-89AB-CDEF-0123-456789ABCDEF "PROVISIONING_PROFILE=01234567-89AB-CDEF-0123-456789ABCDEF" PROVISIONING_PROFILE_SPECIFIER=Fearless App Store "PROVISIONING_PROFILE_SPECIFIER=Fearless App Store" OVERRIDES -grep -Fq '"CODE_SIGN_IDENTITY=$EXPECTED_SIGNING_IDENTITY"' "$BUILD_SCRIPT" || - fail "archive script does not bind the reviewed distribution identity" grep -Fq \ 'bash "$SCRIPT_DIR/audit-ios-signed-release-artifact.sh"' \ "$BUILD_SCRIPT" || @@ -68,8 +70,16 @@ grep -Fq -- \ '--expected-profile-name "$EXPECTED_PROFILE_NAME"' \ "$BUILD_SCRIPT" || fail "signed-artifact audit is not bound to the reviewed profile name" -grep -Fq 'require_setting CODE_SIGN_STYLE "Automatic"' "$IDENTITY_AUDIT" || - fail "release identity no longer requires target-scoped Automatic signing" +grep -Fq 'require_setting CODE_SIGN_STYLE "Manual"' "$IDENTITY_AUDIT" || + fail "release identity no longer requires target-scoped Manual signing" +grep -Fq \ + 'require_setting CODE_SIGN_IDENTITY "$EXPECTED_SIGNING_IDENTITY"' \ + "$IDENTITY_AUDIT" || + fail "release identity no longer requires the target-scoped distribution identity" +grep -Fq \ + 'require_setting PROVISIONING_PROFILE_SPECIFIER "$EXPECTED_PROFILE_NAME"' \ + "$IDENTITY_AUDIT" || + fail "release identity no longer requires the target-scoped App Store profile" printf '%s\n' \ "[ios-release-archive-signing-test] PASS: canonical contract and $override_number workspace-wide override mutations" diff --git a/scripts/test-ios-release-identity-audit.sh b/scripts/test-ios-release-identity-audit.sh index feafab6bec..f4cd201fe0 100755 --- a/scripts/test-ios-release-identity-audit.sh +++ b/scripts/test-ios-release-identity-audit.sh @@ -43,8 +43,10 @@ jq -n '[ MARKETING_VERSION: "4.2.0", CURRENT_PROJECT_VERSION: "2026.7.26", CODE_SIGN_ENTITLEMENTS: "fearless/WalletConnect.entitlements", - CODE_SIGN_STYLE: "Automatic", + CODE_SIGN_IDENTITY: "Apple Distribution: Soramitsu Co., Ltd. (YLWWUD25VZ)", + CODE_SIGN_STYLE: "Manual", DEVELOPMENT_TEAM: "YLWWUD25VZ", + PROVISIONING_PROFILE_SPECIFIER: "Fearless App Store 2026.7.26", SWIFT_OPTIMIZATION_LEVEL: "-O", ENABLE_TESTABILITY: "NO" } @@ -149,10 +151,26 @@ mutate_setting ENABLE_TESTABILITY YES "$testable_release" run_reject testable-release \ run_audit "$testable_release" "$scheme" "$entitlements" -manual_signing="$FIXTURES/manual-signing.json" -mutate_setting CODE_SIGN_STYLE Manual "$manual_signing" -run_reject manual-signing \ - run_audit "$manual_signing" "$scheme" "$entitlements" +automatic_signing="$FIXTURES/automatic-signing.json" +mutate_setting CODE_SIGN_STYLE Automatic "$automatic_signing" +run_reject automatic-signing \ + run_audit "$automatic_signing" "$scheme" "$entitlements" + +wrong_signing_identity="$FIXTURES/wrong-signing-identity.json" +mutate_setting CODE_SIGN_IDENTITY "Apple Development" "$wrong_signing_identity" +run_reject wrong-signing-identity \ + run_audit "$wrong_signing_identity" "$scheme" "$entitlements" + +wrong_profile="$FIXTURES/wrong-profile.json" +mutate_setting PROVISIONING_PROFILE_SPECIFIER \ + "Another App Store Profile" "$wrong_profile" +run_reject wrong-profile \ + run_audit "$wrong_profile" "$scheme" "$entitlements" + +empty_profile="$FIXTURES/empty-profile.json" +mutate_setting PROVISIONING_PROFILE_SPECIFIER "" "$empty_profile" +run_reject empty-profile \ + run_audit "$empty_profile" "$scheme" "$entitlements" wrong_team="$FIXTURES/wrong-team.json" mutate_setting DEVELOPMENT_TEAM AAAAAAAAAA "$wrong_team" From f56b7b896344cfded39456217097747ef9efeacf Mon Sep 17 00:00:00 2001 From: Makoto Takemiya Date: Sun, 26 Jul 2026 16:45:35 +0400 Subject: [PATCH 14/30] Accept standard Xcode archive application paths --- .../ci/audit-ios-signed-release-artifact.sh | 3 +- .../test-ios-signed-release-artifact-audit.sh | 29 +++++++++++++++++-- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/scripts/ci/audit-ios-signed-release-artifact.sh b/scripts/ci/audit-ios-signed-release-artifact.sh index 51dc0967e0..a0c1ee7077 100755 --- a/scripts/ci/audit-ios-signed-release-artifact.sh +++ b/scripts/ci/audit-ios-signed-release-artifact.sh @@ -295,7 +295,8 @@ except (OSError, plistlib.InvalidFileException): properties = payload.get("ApplicationProperties") if not isinstance(properties, dict): raise SystemExit(3) -expected_relative = os.path.relpath(app_path, os.path.dirname(path)) +products_root = os.path.join(os.path.dirname(path), "Products") +expected_relative = os.path.relpath(app_path, products_root) if properties.get("ApplicationPath") != expected_relative: raise SystemExit(4) if properties.get("CFBundleIdentifier") != expected_bundle: diff --git a/scripts/test-ios-signed-release-artifact-audit.sh b/scripts/test-ios-signed-release-artifact-audit.sh index f229826ca5..566ee8293c 100755 --- a/scripts/test-ios-signed-release-artifact-audit.sh +++ b/scripts/test-ios-signed-release-artifact-audit.sh @@ -106,7 +106,7 @@ profile_entitlements = { } archive = { "ApplicationProperties": { - "ApplicationPath": "Products/Applications/fearless.app", + "ApplicationPath": "Applications/fearless.app", "CFBundleIdentifier": bundle, "SigningIdentity": "Apple Distribution: Fearless Test ($team)", "Team": team, @@ -264,6 +264,31 @@ mutate_plist "$ARCHIVE/Info.plist" \ 'value["ApplicationProperties"]["Team"] = "AAAAAAAAAA"' expect_failure wrong-archive-team "xcarchive metadata" +prepare_case archive-path-includes-products +mutate_plist "$ARCHIVE/Info.plist" \ + 'value["ApplicationProperties"]["ApplicationPath"] = "Products/Applications/fearless.app"' +expect_failure archive-path-includes-products "xcarchive metadata" + +prepare_case archive-path-traversal +mutate_plist "$ARCHIVE/Info.plist" \ + 'value["ApplicationProperties"]["ApplicationPath"] = "../Applications/fearless.app"' +expect_failure archive-path-traversal "xcarchive metadata" + +prepare_case archive-path-absolute +mutate_plist "$ARCHIVE/Info.plist" \ + 'value["ApplicationProperties"]["ApplicationPath"] = "/Applications/fearless.app"' +expect_failure archive-path-absolute "xcarchive metadata" + +prepare_case archive-path-wrong-app +mutate_plist "$ARCHIVE/Info.plist" \ + 'value["ApplicationProperties"]["ApplicationPath"] = "Applications/attacker.app"' +expect_failure archive-path-wrong-app "xcarchive metadata" + +prepare_case archive-path-missing +mutate_plist "$ARCHIVE/Info.plist" \ + 'del value["ApplicationProperties"]["ApplicationPath"]' +expect_failure archive-path-missing "xcarchive metadata" + prepare_case development-identity mutate_plist "$ARCHIVE/Info.plist" \ 'value["ApplicationProperties"]["SigningIdentity"] = "Apple Development: Unsafe"' @@ -377,4 +402,4 @@ assert_contains "only in the explicit test harness" "$CASE_DIR/stderr" printf '%s\n' "[ios-signed-release-audit-test] PASS (rejected): override without harness" printf '%s\n' \ - "[ios-signed-release-audit-test] PASS: 1 positive + 27 negative/adversarial contracts" + "[ios-signed-release-audit-test] PASS: 1 positive + 32 negative/adversarial contracts" From 5469d34cde86eb26c727dcd84ca22795c6510740 Mon Sep 17 00:00:00 2001 From: Makoto Takemiya Date: Sun, 26 Jul 2026 18:22:54 +0400 Subject: [PATCH 15/30] docs: track TestFlight publication evidence --- .github/workflows/codecov.yml | 5 + config/testflight-publication-readiness.json | 80 +++ docs/release-checklist.md | 7 + docs/testflight-publication-readiness.md | 106 ++++ .../audit-testflight-publication-readiness.sh | 480 ++++++++++++++++++ scripts/ci/run-pr.sh | 4 + ...-testflight-publication-readiness-audit.sh | 430 ++++++++++++++++ 7 files changed, 1112 insertions(+) create mode 100644 config/testflight-publication-readiness.json create mode 100644 docs/testflight-publication-readiness.md create mode 100755 scripts/audit-testflight-publication-readiness.sh create mode 100755 scripts/test-testflight-publication-readiness-audit.sh diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 859aff68dc..b6e31958b2 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -30,6 +30,11 @@ jobs: bash ./scripts/test-todo-debt-audit.sh bash ./scripts/audit-todo-debt.sh + - name: TestFlight publication evidence + run: | + bash ./scripts/test-testflight-publication-readiness-audit.sh + bash ./scripts/audit-testflight-publication-readiness.sh + - name: Iroha mobile SDK release asset contract env: GH_TOKEN: ${{ github.token }} diff --git a/config/testflight-publication-readiness.json b/config/testflight-publication-readiness.json new file mode 100644 index 0000000000..9015b38b9c --- /dev/null +++ b/config/testflight-publication-readiness.json @@ -0,0 +1,80 @@ +{ + "schemaVersion": 1, + "platform": "ios", + "distributionChannel": "testflight-external-public-link", + "assessedAtUTC": "2026-07-26T13:26:11Z", + "evidenceBoundary": { + "contract": "tracked-sanitized-publication-snapshot", + "observationMethod": "authenticated-app-store-connect-and-unauthenticated-public-link-in-safari", + "rawAttestationsTracked": false, + "rawAttestationsLocation": "ignored-build-output", + "currentStateRevalidationRequired": true + }, + "artifact": { + "sourceCommit": "f56b7b896344cfded39456217097747ef9efeacf", + "bundleIdentifier": "jp.co.soramitsu.fearlesswallet", + "marketingVersion": "4.2.0", + "buildNumber": "2026.7.26", + "preUploadArchiveTreeSHA256": "301f19a4f1232fb05719d85b825ac081196fa5fb6988b58011b7569a4bd9e6fa", + "executableSHA256": "cf0318792fc361bbd8704b21b6e1a08c4d81897f23492e4a807eab7f22403568", + "signedArchiveAuditReceiptSHA256": "1dbf38740b38403dde9cc7d4b1710cd28fdd5dc6f6f25990a7102020f11d1abc", + "preservedDataArchiveRehearsalResultSHA256": "418d22cdd9e673ddfa1f47a49c666df11a740aac6737f6ef2b0328fa1b6a8641" + }, + "upload": { + "status": "success", + "uploadedAtUTC": "2026-07-26T13:16:00Z", + "postUploadArchiveTreeSHA256": "a483a372fe8fd19761d3b0324427c4cf55a4553b4ed7433b34b78517f292cea7", + "postUploadArchiveInfoPlistSHA256": "e82ff42b4b47713d1094c420dd7d1abfbfdfe7e7b80c972909936ad64e1e4756", + "postUploadAttestationSHA256": "6ede1d8fb981be149a82d69313be0ebc1838291e27d0ecf1c1576c95350b1af9", + "xcodeMutation": "Distributions-only", + "reconstructedPreUploadArchiveTreeMatched": true, + "embeddedAppSignatureVerified": true + }, + "testFlight": { + "processingStatus": "testing", + "externalTestingActive": true, + "externalGroup": { + "name": "Public Beta Test", + "id": "9410949d-b468-40e5-b2f4-24055e65d270", + "testerCount": 122, + "buildCount": 2, + "containsBuild": true, + "publicLinkTesterCount": 120, + "publicLinkTesterLimit": 500 + }, + "publicLink": { + "enabled": true, + "shareableToAnyoneWithLink": true, + "url": "https://testflight.apple.com/join/012KzFyD", + "urlSHA256": "d6599b1b9c0f7e77d9dd80d9185da67c1d864be5df9a7bbf360f245b099a000c", + "landingPageResolved": true, + "landingPageApplicationName": "Fearless Wallet: DeFi Wallet", + "testFlightDeepLinkPresent": true + }, + "whatToTestSHA256": "d5d0c2063934c4ca7dd8bb82af038d79909738c93cf96c11ab63bca8c0b7f272", + "automaticTesterNotification": true, + "publicationAttestationSHA256": "84e53d78316cf46cc1956d04e86782241d6e575da90fef109acaaf44cb368d4c" + }, + "symbolication": { + "status": "degraded-third-party-symbolication", + "runtimeInstallLaunchOrMigrationImpact": false, + "mainApplicationDSYMPresent": true, + "runtimeFrameworkMissingVendorDSYM": "MPQRCoreSDK.framework", + "staticStubFrameworkWarnings": [ + "blake2lib.framework", + "libed25519.framework", + "sr25519lib.framework" + ], + "productionFollowUpRequired": true + }, + "readiness": { + "status": "blocked", + "publicationVerified": true, + "externalTestingActive": true, + "exactAppleDeliveredInstallVerified": false, + "preservedDataPostInstallRehearsalVerified": false, + "releaseEnabled": false, + "blockerCode": "testflight-exact-build-preserved-data-install-pending", + "requiredAction": "Update in place through TestFlight without deleting or clearing app data, then repeat the five-cold-launch and store-integrity rehearsal." + } +} diff --git a/docs/release-checklist.md b/docs/release-checklist.md index e5498c391e..697eaef0fc 100644 --- a/docs/release-checklist.md +++ b/docs/release-checklist.md @@ -31,6 +31,13 @@ Use this checklist for every release PR from `develop` to `master`. - Run `bash ./scripts/test-transaction-builder-tests-audit.sh && bash ./scripts/audit-transaction-builder-tests.sh` and confirm Bitcoin, Solana, Iroha, and TON fail-closed transfer coverage remains in place. +- Run + `bash ./scripts/test-testflight-publication-readiness-audit.sh && bash ./scripts/audit-testflight-publication-readiness.sh` + and review the exact tracked publication snapshot. Do not mark the TestFlight + build release-enabled until an in-place TestFlight update preserves the + existing app container, five cold launches pass, store integrity is unchanged, + and the remaining third-party symbolication follow-up is closed or explicitly + accepted by release review. - Run `bash ./scripts/check-iroha-mobile-sdk-release-assets.sh --self-test`. If `IROHA_MOBILE_SDK_RELEASE_TAG` is configured for the release, also run `bash ./scripts/check-iroha-mobile-sdk-release-assets.sh --download --tag "$IROHA_MOBILE_SDK_RELEASE_TAG"`. diff --git a/docs/testflight-publication-readiness.md b/docs/testflight-publication-readiness.md new file mode 100644 index 0000000000..6fd14afaa5 --- /dev/null +++ b/docs/testflight-publication-readiness.md @@ -0,0 +1,106 @@ +# TestFlight Publication Readiness + +Status: **BLOCKED only on exact Apple-delivered install/rehearsal and symbolication follow-up** + +This document records the sanitized, reviewable publication snapshot for the +migration-fixed Fearless Wallet iOS build. It does not contain App Store +credentials, signing secrets, wallet data, device data, or a private tester +handoff. + +## Exact Published Build + +- Artifact source commit: + `f56b7b896344cfded39456217097747ef9efeacf`. +- Bundle and version: + `jp.co.soramitsu.fearlesswallet`, `4.2.0 (2026.7.26)`. +- Pre-upload archive tree SHA-256: + `301f19a4f1232fb05719d85b825ac081196fa5fb6988b58011b7569a4bd9e6fa`. +- Executable SHA-256: + `cf0318792fc361bbd8704b21b6e1a08c4d81897f23492e4a807eab7f22403568`. +- Signed-archive audit receipt SHA-256: + `1dbf38740b38403dde9cc7d4b1710cd28fdd5dc6f6f25990a7102020f11d1abc`. +- Preserved-data archive rehearsal result SHA-256: + `418d22cdd9e673ddfa1f47a49c666df11a740aac6737f6ef2b0328fa1b6a8641`. + +Xcode uploaded the exact archive successfully at `2026-07-26T13:16:00Z`. +Its only archive mutation was the normal `Distributions` receipt in +`Info.plist`. Removing that receipt from a copy reconstructed the pre-upload +tree hash, while the embedded app signature and executable hash remained +unchanged. + +## External TestFlight State + +The authenticated App Store Connect observation at +`2026-07-26T13:26:11Z` records: + +- `4.2.0 (2026.7.26)` is `Testing`; +- external group `Public Beta Test` has stable identifier + `9410949d-b468-40e5-b2f4-24055e65d270`; +- the group contains the exact build and showed `122` testers, `2` builds, and + `120/500` public-link places used; +- automatic tester notification was enabled; and +- the anyone-with-the-link URL is + `https://testflight.apple.com/join/012KzFyD`. + +An unauthenticated Safari request resolved that URL to Apple's +`Fearless Wallet: DeFi Wallet` page and exposed the TestFlight deep link. +Tester and build counts are a dated snapshot and can change; current App Store +Connect state must be revalidated for a later production release decision. + +## Evidence Boundary + +The raw post-upload, publication, and physical rehearsal attestations are in +ignored `build/` output. Their exact SHA-256 values are copied into the tracked +manifest, but those ignored files must not be treated as canonical release source. The tracked +`config/testflight-publication-readiness.json` is the sanitized public contract +reviewed by the offline audit. + +The publication snapshot proves successful upload, external testing, exact +group assignment, public-link enablement, Apple landing-page resolution, and +the pre-publication preserved-data archive rehearsal. It does not prove that +the Apple-delivered TestFlight package has replaced the locally installed +Developer App on the affected phone. + +## Symbolication Follow-up + +The main application dSYM is present. Xcode nevertheless warned about +`MPQRCoreSDK.framework`, `blake2lib.framework`, `libed25519.framework`, and +`sr25519lib.framework`. + +The dSYM warnings cannot alter install, launch, or Core Data migration runtime +behavior. The three crypto framework warnings correspond to static-stub +packaging while their useful symbols are linked into the main app image. +`MPQRCoreSDK.framework` is the remaining runtime framework without a matching +vendor dSYM, so third-party QR crash frames can have degraded symbolication. +Obtain the matching vendor dSYM or record an explicit reviewed production +acceptance before marking symbolication follow-up complete. + +## Remaining Device Gate + +Update the existing installation through the TestFlight app on the preserved- +data physical iPhone. Do not uninstall the app or clear its container. Then: + +1. verify the installed identity is Apple/TestFlight-distributed build + `4.2.0 (2026.7.26)`; +2. preserve the database UUIDs, protected row counts, and semantic digest; +3. run five terminate-and-cold-launch cycles with the full observation window; +4. rerun SQLite `quick_check` and compare every protected fingerprint; and +5. confirm there is no new Fearless crash report. + +Until that succeeds, +`exactAppleDeliveredInstallVerified=false`, +`preservedDataPostInstallRehearsalVerified=false`, and +`releaseEnabled=false` remain mandatory. + +## Offline Verification + +Run: + +```bash +bash ./scripts/test-testflight-publication-readiness-audit.sh +bash ./scripts/audit-testflight-publication-readiness.sh +``` + +The audit is offline. It validates a dated, content-addressed publication +snapshot and deliberately does not contact Apple or infer current tester +capacity from an old observation. diff --git a/scripts/audit-testflight-publication-readiness.sh b/scripts/audit-testflight-publication-readiness.sh new file mode 100755 index 0000000000..083cd4d7ad --- /dev/null +++ b/scripts/audit-testflight-publication-readiness.sh @@ -0,0 +1,480 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="${TESTFLIGHT_PUBLICATION_AUDIT_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" +MANIFEST="$ROOT_DIR/config/testflight-publication-readiness.json" +DOC="$ROOT_DIR/docs/testflight-publication-readiness.md" +RELEASE_CHECKLIST="$ROOT_DIR/docs/release-checklist.md" +RUN_PR="$ROOT_DIR/scripts/ci/run-pr.sh" +WORKFLOW="$ROOT_DIR/.github/workflows/codecov.yml" +AUDIT_CONTRACT="$ROOT_DIR/scripts/audit-testflight-publication-readiness.sh" +SELF_TEST="$ROOT_DIR/scripts/test-testflight-publication-readiness-audit.sh" +EXPECTED_MANIFEST_SHA256="${TESTFLIGHT_PUBLICATION_AUDIT_EXPECTED_MANIFEST_SHA256:-d66634ec2fce2305e2cc64e18e55c273dcc802bc82e074f658bef72ad8d53b26}" + +fail() { + echo "[testflight-publication][ios][error] $*" >&2 + exit 1 +} + +sha256_file() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{print $1}' + else + shasum -a 256 "$1" | awk '{print $1}' + fi +} + +require_regular_file() { + local path="$1" + [[ -f "$path" && ! -L "$path" ]] || + fail "required regular file is missing or is a symlink: ${path#"$ROOT_DIR/"}" +} + +require_executable_file() { + local path="$1" + [[ -x "$path" ]] || fail "required script is not executable: ${path#"$ROOT_DIR/"}" +} + +require_bounded_file() { + local path="$1" + local maximum="$2" + local label="$3" + local bytes + bytes="$(wc -c < "$path" | tr -d '[:space:]')" + [[ "$bytes" =~ ^[0-9]+$ && "$bytes" -gt 0 && "$bytes" -le "$maximum" ]] || + fail "$label must be nonempty and no larger than $maximum bytes" +} + +require_fixed() { + local path="$1" + local marker="$2" + local label="$3" + grep -Fq -- "$marker" "$path" || fail "$label is missing from ${path#"$ROOT_DIR/"}" +} + +require_active_line() { + local path="$1" + local expected="$2" + local label="$3" + awk -v expected="$expected" ' + { + line = $0 + sub(/^[[:space:]]*/, "", line) + sub(/[[:space:]]*$/, "", line) + if (line == expected) found = 1 + } + END { exit found ? 0 : 1 } + ' "$path" || fail "$label is missing from ${path#"$ROOT_DIR/"}" +} + +command -v node >/dev/null 2>&1 || fail "node is required for canonical JSON validation" + +for path in \ + "$MANIFEST" "$DOC" "$RELEASE_CHECKLIST" "$RUN_PR" "$WORKFLOW" \ + "$AUDIT_CONTRACT" "$SELF_TEST"; do + require_regular_file "$path" +done + +require_executable_file "$AUDIT_CONTRACT" +require_executable_file "$SELF_TEST" +require_bounded_file "$MANIFEST" 32768 "TestFlight publication manifest" +require_bounded_file "$DOC" 65536 "TestFlight publication document" +require_bounded_file "$RELEASE_CHECKLIST" 262144 "release checklist" +require_bounded_file "$RUN_PR" 131072 "PR runner" +require_bounded_file "$WORKFLOW" 262144 "GitHub workflow" +require_bounded_file "$AUDIT_CONTRACT" 131072 "TestFlight publication audit" +require_bounded_file "$SELF_TEST" 262144 "TestFlight publication adversarial self-test" + +[[ "$EXPECTED_MANIFEST_SHA256" =~ ^[0-9a-f]{64}$ ]] || + fail "expected manifest digest must be exactly 64 lowercase hexadecimal characters" +actual_manifest_sha256="$(sha256_file "$MANIFEST")" +[[ "$actual_manifest_sha256" == "$EXPECTED_MANIFEST_SHA256" ]] || + fail "publication manifest digest mismatch: expected $EXPECTED_MANIFEST_SHA256, got $actual_manifest_sha256" + +node - "$MANIFEST" <<'NODE' +const crypto = require('node:crypto'); +const fs = require('node:fs'); +const path = process.argv[2]; +const raw = fs.readFileSync(path, 'utf8'); +let manifest; + +function fail(message) { + console.error(`[testflight-publication][ios][error] ${message}`); + process.exit(1); +} + +function assert(condition, message) { + if (!condition) fail(message); +} + +try { + manifest = JSON.parse(raw); +} catch (error) { + fail(`invalid publication JSON: ${error.message}`); +} + +assert( + raw === `${JSON.stringify(manifest, null, 2)}\n`, + 'publication manifest must use canonical two-space JSON without duplicate keys or trailing data' +); + +function exactKeys(value, expected, label) { + assert(value && typeof value === 'object' && !Array.isArray(value), `${label} must be an object`); + const actual = Object.keys(value).sort(); + const wanted = [...expected].sort(); + assert(JSON.stringify(actual) === JSON.stringify(wanted), `${label} keys drifted`); +} + +function exactArray(value, expected, label) { + assert(Array.isArray(value), `${label} must be an array`); + assert(JSON.stringify(value) === JSON.stringify(expected), `${label} drifted`); + assert(new Set(value).size === value.length, `${label} must not contain duplicates`); +} + +function exactInteger(value, expected, label) { + assert(Number.isSafeInteger(value), `${label} must be a safe integer`); + assert(value === expected, `${label} drifted`); +} + +function exactDigest(value, expected, label) { + assert(typeof value === 'string' && /^[0-9a-f]{64}$/.test(value), `${label} must be a lowercase SHA-256`); + assert(value === expected, `${label} drifted`); +} + +exactKeys(manifest, [ + 'schemaVersion', 'platform', 'distributionChannel', 'assessedAtUTC', + 'evidenceBoundary', 'artifact', 'upload', 'testFlight', 'symbolication', + 'readiness' +], 'top-level manifest'); +assert(manifest.schemaVersion === 1, 'schemaVersion must be 1'); +assert(manifest.platform === 'ios', 'platform must be ios'); +assert( + manifest.distributionChannel === 'testflight-external-public-link', + 'distribution channel drifted' +); +assert(manifest.assessedAtUTC === '2026-07-26T13:26:11Z', 'assessment timestamp drifted'); + +const boundary = manifest.evidenceBoundary; +exactKeys(boundary, [ + 'contract', 'observationMethod', 'rawAttestationsTracked', + 'rawAttestationsLocation', 'currentStateRevalidationRequired' +], 'evidenceBoundary'); +assert( + boundary.contract === 'tracked-sanitized-publication-snapshot', + 'evidence boundary contract drifted' +); +assert( + boundary.observationMethod === + 'authenticated-app-store-connect-and-unauthenticated-public-link-in-safari', + 'observation method drifted' +); +assert(boundary.rawAttestationsTracked === false, 'ignored raw attestations must not be claimed as tracked'); +assert( + boundary.rawAttestationsLocation === 'ignored-build-output', + 'raw attestation location drifted' +); +assert( + boundary.currentStateRevalidationRequired === true, + 'mutable App Store Connect state must require revalidation' +); + +const artifact = manifest.artifact; +exactKeys(artifact, [ + 'sourceCommit', 'bundleIdentifier', 'marketingVersion', 'buildNumber', + 'preUploadArchiveTreeSHA256', 'executableSHA256', + 'signedArchiveAuditReceiptSHA256', + 'preservedDataArchiveRehearsalResultSHA256' +], 'artifact'); +assert( + artifact.sourceCommit === 'f56b7b896344cfded39456217097747ef9efeacf', + 'artifact source commit drifted' +); +assert( + artifact.bundleIdentifier === 'jp.co.soramitsu.fearlesswallet', + 'artifact bundle identifier drifted' +); +assert(artifact.marketingVersion === '4.2.0', 'artifact marketing version drifted'); +assert(artifact.buildNumber === '2026.7.26', 'artifact build number drifted'); +exactDigest( + artifact.preUploadArchiveTreeSHA256, + '301f19a4f1232fb05719d85b825ac081196fa5fb6988b58011b7569a4bd9e6fa', + 'pre-upload archive tree SHA-256' +); +exactDigest( + artifact.executableSHA256, + 'cf0318792fc361bbd8704b21b6e1a08c4d81897f23492e4a807eab7f22403568', + 'executable SHA-256' +); +exactDigest( + artifact.signedArchiveAuditReceiptSHA256, + '1dbf38740b38403dde9cc7d4b1710cd28fdd5dc6f6f25990a7102020f11d1abc', + 'signed archive audit receipt SHA-256' +); +exactDigest( + artifact.preservedDataArchiveRehearsalResultSHA256, + '418d22cdd9e673ddfa1f47a49c666df11a740aac6737f6ef2b0328fa1b6a8641', + 'preserved-data archive rehearsal result SHA-256' +); + +const upload = manifest.upload; +exactKeys(upload, [ + 'status', 'uploadedAtUTC', 'postUploadArchiveTreeSHA256', + 'postUploadArchiveInfoPlistSHA256', 'postUploadAttestationSHA256', + 'xcodeMutation', 'reconstructedPreUploadArchiveTreeMatched', + 'embeddedAppSignatureVerified' +], 'upload'); +assert(upload.status === 'success', 'upload status must remain success'); +assert(upload.uploadedAtUTC === '2026-07-26T13:16:00Z', 'upload timestamp drifted'); +exactDigest( + upload.postUploadArchiveTreeSHA256, + 'a483a372fe8fd19761d3b0324427c4cf55a4553b4ed7433b34b78517f292cea7', + 'post-upload archive tree SHA-256' +); +exactDigest( + upload.postUploadArchiveInfoPlistSHA256, + 'e82ff42b4b47713d1094c420dd7d1abfbfdfe7e7b80c972909936ad64e1e4756', + 'post-upload archive Info.plist SHA-256' +); +exactDigest( + upload.postUploadAttestationSHA256, + '6ede1d8fb981be149a82d69313be0ebc1838291e27d0ecf1c1576c95350b1af9', + 'post-upload attestation SHA-256' +); +assert(upload.xcodeMutation === 'Distributions-only', 'Xcode post-upload mutation scope drifted'); +assert( + upload.reconstructedPreUploadArchiveTreeMatched === true, + 'reconstructed pre-upload archive tree match must remain true' +); +assert( + upload.embeddedAppSignatureVerified === true, + 'embedded app signature verification must remain true' +); + +const testFlight = manifest.testFlight; +exactKeys(testFlight, [ + 'processingStatus', 'externalTestingActive', 'externalGroup', 'publicLink', + 'whatToTestSHA256', 'automaticTesterNotification', + 'publicationAttestationSHA256' +], 'testFlight'); +assert(testFlight.processingStatus === 'testing', 'TestFlight processing status must remain testing'); +assert(testFlight.externalTestingActive === true, 'external TestFlight testing must remain active'); + +const group = testFlight.externalGroup; +exactKeys(group, [ + 'name', 'id', 'testerCount', 'buildCount', 'containsBuild', + 'publicLinkTesterCount', 'publicLinkTesterLimit' +], 'externalGroup'); +assert(group.name === 'Public Beta Test', 'external group name drifted'); +assert( + group.id === '9410949d-b468-40e5-b2f4-24055e65d270', + 'external group identifier drifted' +); +exactInteger(group.testerCount, 122, 'external group tester count snapshot'); +exactInteger(group.buildCount, 2, 'external group build count snapshot'); +assert(group.containsBuild === true, 'external group must contain build 2026.7.26'); +exactInteger(group.publicLinkTesterCount, 120, 'public-link tester count snapshot'); +exactInteger(group.publicLinkTesterLimit, 500, 'public-link tester limit'); +assert( + group.publicLinkTesterCount <= group.testerCount, + 'public-link tester count cannot exceed the external group tester count' +); +assert( + group.publicLinkTesterCount <= group.publicLinkTesterLimit, + 'public-link tester count cannot exceed its limit' +); + +const publicLink = testFlight.publicLink; +exactKeys(publicLink, [ + 'enabled', 'shareableToAnyoneWithLink', 'url', 'urlSHA256', + 'landingPageResolved', 'landingPageApplicationName', + 'testFlightDeepLinkPresent' +], 'publicLink'); +assert(publicLink.enabled === true, 'public TestFlight link must remain enabled'); +assert( + publicLink.shareableToAnyoneWithLink === true, + 'public TestFlight link must remain shareable to anyone with the link' +); +assert( + publicLink.url === 'https://testflight.apple.com/join/012KzFyD', + 'public TestFlight URL drifted' +); +exactDigest( + publicLink.urlSHA256, + 'd6599b1b9c0f7e77d9dd80d9185da67c1d864be5df9a7bbf360f245b099a000c', + 'public TestFlight URL SHA-256' +); +assert( + crypto.createHash('sha256').update(publicLink.url, 'utf8').digest('hex') === + publicLink.urlSHA256, + 'public TestFlight URL does not match its SHA-256' +); +let parsedPublicLink; +try { + parsedPublicLink = new URL(publicLink.url); +} catch (error) { + fail(`public TestFlight URL is invalid: ${error.message}`); +} +assert(parsedPublicLink.protocol === 'https:', 'public TestFlight URL must use HTTPS'); +assert(parsedPublicLink.hostname === 'testflight.apple.com', 'public TestFlight URL host drifted'); +assert(parsedPublicLink.port === '', 'public TestFlight URL must not specify a port'); +assert( + parsedPublicLink.username === '' && parsedPublicLink.password === '', + 'public TestFlight URL must not contain userinfo' +); +assert( + parsedPublicLink.search === '' && parsedPublicLink.hash === '', + 'public TestFlight URL must not contain a query or fragment' +); +assert(parsedPublicLink.pathname === '/join/012KzFyD', 'public TestFlight URL path drifted'); +assert(publicLink.landingPageResolved === true, 'public landing page resolution must remain true'); +assert( + publicLink.landingPageApplicationName === 'Fearless Wallet: DeFi Wallet', + 'public landing-page application identity drifted' +); +assert( + publicLink.testFlightDeepLinkPresent === true, + 'public landing page must retain the TestFlight deep link' +); +exactDigest( + testFlight.whatToTestSHA256, + 'd5d0c2063934c4ca7dd8bb82af038d79909738c93cf96c11ab63bca8c0b7f272', + 'What to Test text SHA-256' +); +assert( + testFlight.automaticTesterNotification === true, + 'automatic tester notification must remain recorded as enabled' +); +exactDigest( + testFlight.publicationAttestationSHA256, + '84e53d78316cf46cc1956d04e86782241d6e575da90fef109acaaf44cb368d4c', + 'publication attestation SHA-256' +); + +const symbolication = manifest.symbolication; +exactKeys(symbolication, [ + 'status', 'runtimeInstallLaunchOrMigrationImpact', 'mainApplicationDSYMPresent', + 'runtimeFrameworkMissingVendorDSYM', 'staticStubFrameworkWarnings', + 'productionFollowUpRequired' +], 'symbolication'); +assert( + symbolication.status === 'degraded-third-party-symbolication', + 'symbolication status drifted' +); +assert( + symbolication.runtimeInstallLaunchOrMigrationImpact === false, + 'dSYM warnings must not be classified as an install, launch, or migration runtime failure' +); +assert( + symbolication.mainApplicationDSYMPresent === true, + 'main application dSYM presence must remain recorded' +); +assert( + symbolication.runtimeFrameworkMissingVendorDSYM === 'MPQRCoreSDK.framework', + 'runtime framework vendor-dSYM gap drifted' +); +exactArray(symbolication.staticStubFrameworkWarnings, [ + 'blake2lib.framework', + 'libed25519.framework', + 'sr25519lib.framework' +], 'static stub framework warning inventory'); +assert( + symbolication.productionFollowUpRequired === true, + 'third-party production symbolication follow-up must remain required' +); + +const readiness = manifest.readiness; +exactKeys(readiness, [ + 'status', 'publicationVerified', 'externalTestingActive', + 'exactAppleDeliveredInstallVerified', + 'preservedDataPostInstallRehearsalVerified', 'releaseEnabled', + 'blockerCode', 'requiredAction' +], 'readiness'); +assert(readiness.status === 'blocked', 'TestFlight readiness must remain blocked'); +assert(readiness.publicationVerified === true, 'publicationVerified must remain true'); +assert(readiness.externalTestingActive === true, 'readiness externalTestingActive must remain true'); +assert( + readiness.exactAppleDeliveredInstallVerified === false, + 'exact Apple-delivered TestFlight installation must not be claimed' +); +assert( + readiness.preservedDataPostInstallRehearsalVerified === false, + 'post-install preserved-data rehearsal must not be claimed' +); +assert(readiness.releaseEnabled === false, 'releaseEnabled must remain false'); +assert( + readiness.blockerCode === 'testflight-exact-build-preserved-data-install-pending', + 'TestFlight blocker code drifted' +); +assert( + readiness.requiredAction === + 'Update in place through TestFlight without deleting or clearing app data, then repeat the five-cold-launch and store-integrity rehearsal.', + 'TestFlight required action drifted' +); +assert( + readiness.publicationVerified === + ( + upload.status === 'success' && + testFlight.processingStatus === 'testing' && + testFlight.externalTestingActive === true && + group.containsBuild === true && + publicLink.enabled === true && + publicLink.shareableToAnyoneWithLink === true && + publicLink.landingPageResolved === true && + publicLink.testFlightDeepLinkPresent === true + ), + 'publicationVerified disagrees with upload, group, or public-link evidence' +); +assert( + readiness.externalTestingActive === testFlight.externalTestingActive, + 'readiness externalTestingActive disagrees with TestFlight state' +); +assert( + readiness.releaseEnabled === false || + ( + readiness.publicationVerified === true && + readiness.exactAppleDeliveredInstallVerified === true && + readiness.preservedDataPostInstallRehearsalVerified === true && + symbolication.productionFollowUpRequired === false + ), + 'releaseEnabled requires publication, exact Apple-delivered install, preserved-data rehearsal, and symbolication closure' +); +NODE + +require_fixed "$DOC" 'Status: **BLOCKED only on exact Apple-delivered install/rehearsal and symbolication follow-up**' \ + "documented current TestFlight readiness status" +require_fixed "$DOC" '`4.2.0 (2026.7.26)` is `Testing`' \ + "documented current TestFlight build status" +require_fixed "$DOC" '`9410949d-b468-40e5-b2f4-24055e65d270`' \ + "documented external group identifier" +require_fixed "$DOC" '`122` testers, `2` builds' \ + "documented external group snapshot" +require_fixed "$DOC" '`120/500` public-link places used' \ + "documented public-link occupancy snapshot" +require_fixed "$DOC" 'https://testflight.apple.com/join/012KzFyD' \ + "documented public TestFlight link" +require_fixed "$DOC" 'must not be treated as canonical release source' \ + "documented ignored raw-attestation boundary" +require_fixed "$DOC" 'MPQRCoreSDK.framework' \ + "documented runtime framework symbolication gap" +require_fixed "$DOC" 'Do not uninstall the app or clear its container.' \ + "documented preserved-data install constraint" +require_fixed "$RELEASE_CHECKLIST" \ + 'bash ./scripts/test-testflight-publication-readiness-audit.sh && bash ./scripts/audit-testflight-publication-readiness.sh' \ + "release-checklist TestFlight publication gate" +require_fixed "$RELEASE_CHECKLIST" \ + 'build release-enabled until an in-place TestFlight update preserves the' \ + "release-checklist exact Apple-delivered install gate" +require_active_line "$RUN_PR" \ + 'bash "$WORKSPACE_DIR/scripts/test-testflight-publication-readiness-audit.sh"' \ + "PR TestFlight publication adversarial self-test" +require_active_line "$RUN_PR" \ + 'bash "$WORKSPACE_DIR/scripts/audit-testflight-publication-readiness.sh"' \ + "PR TestFlight publication audit" +require_active_line "$WORKFLOW" \ + 'bash ./scripts/test-testflight-publication-readiness-audit.sh' \ + "CI TestFlight publication adversarial self-test" +require_active_line "$WORKFLOW" \ + 'bash ./scripts/audit-testflight-publication-readiness.sh' \ + "CI TestFlight publication audit" + +echo "[testflight-publication][ios] tracked publication evidence is valid; exact Apple-delivered install remains blocked" diff --git a/scripts/ci/run-pr.sh b/scripts/ci/run-pr.sh index 9ecdc79809..d85ea4efbc 100755 --- a/scripts/ci/run-pr.sh +++ b/scripts/ci/run-pr.sh @@ -23,6 +23,10 @@ if [[ -f "$WORKSPACE_DIR/scripts/audit-todo-debt.sh" ]]; then bash "$WORKSPACE_DIR/scripts/audit-todo-debt.sh" fi +echo "[run-pr] Verifying tracked TestFlight publication evidence" +bash "$WORKSPACE_DIR/scripts/test-testflight-publication-readiness-audit.sh" +bash "$WORKSPACE_DIR/scripts/audit-testflight-publication-readiness.sh" + if [[ -f "$WORKSPACE_DIR/scripts/check-iroha-mobile-sdk-release-assets.sh" ]]; then echo "[run-pr] Checking Iroha mobile SDK release asset contract" bash "$WORKSPACE_DIR/scripts/check-iroha-mobile-sdk-release-assets.sh" --self-test diff --git a/scripts/test-testflight-publication-readiness-audit.sh b/scripts/test-testflight-publication-readiness-audit.sh new file mode 100755 index 0000000000..658cf62922 --- /dev/null +++ b/scripts/test-testflight-publication-readiness-audit.sh @@ -0,0 +1,430 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +AUDIT="$ROOT_DIR/scripts/audit-testflight-publication-readiness.sh" +TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/testflight-publication-test.XXXXXX")" +CASES=0 +EXPECTED_CASES=70 + +cleanup() { + rm -rf "$TMP_DIR" +} +trap cleanup EXIT INT TERM + +fail() { + echo "[testflight-publication-test][error] $*" >&2 + exit 1 +} + +sha256_file() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{print $1}' + else + shasum -a 256 "$1" | awk '{print $1}' + fi +} + +make_fixture() { + local fixture="$1" + mkdir -p \ + "$fixture/config" \ + "$fixture/docs" \ + "$fixture/scripts/ci" \ + "$fixture/.github/workflows" + cp "$ROOT_DIR/config/testflight-publication-readiness.json" "$fixture/config/" + cp "$ROOT_DIR/docs/testflight-publication-readiness.md" "$fixture/docs/" + cp "$ROOT_DIR/docs/release-checklist.md" "$fixture/docs/" + cp "$ROOT_DIR/scripts/ci/run-pr.sh" "$fixture/scripts/ci/" + cp "$ROOT_DIR/.github/workflows/codecov.yml" "$fixture/.github/workflows/" + cp "$ROOT_DIR/scripts/audit-testflight-publication-readiness.sh" "$fixture/scripts/" + cp "$ROOT_DIR/scripts/test-testflight-publication-readiness-audit.sh" "$fixture/scripts/" + chmod +x \ + "$fixture/scripts/audit-testflight-publication-readiness.sh" \ + "$fixture/scripts/test-testflight-publication-readiness-audit.sh" +} + +new_fixture() { + local name="$1" + local fixture="$TMP_DIR/$name" + make_fixture "$fixture" + printf '%s\n' "$fixture" +} + +set_json() { + local manifest="$1" + local field_path="$2" + local json_value="$3" + node - "$manifest" "$field_path" "$json_value" <<'NODE' +const fs = require('node:fs'); +const [path, fieldPath, encoded] = process.argv.slice(2); +const document = JSON.parse(fs.readFileSync(path, 'utf8')); +const segments = fieldPath.split('.'); +let cursor = document; +for (const segment of segments.slice(0, -1)) cursor = cursor[segment]; +cursor[segments.at(-1)] = JSON.parse(encoded); +fs.writeFileSync(path, `${JSON.stringify(document, null, 2)}\n`); +NODE +} + +replace_once() { + local path="$1" + local needle="$2" + local replacement="$3" + node - "$path" "$needle" "$replacement" <<'NODE' +const fs = require('node:fs'); +const [path, needle, replacement] = process.argv.slice(2); +const source = fs.readFileSync(path, 'utf8'); +const first = source.indexOf(needle); +if (first === -1) throw new Error(`mutation marker is missing in ${path}: ${needle}`); +if (source.indexOf(needle, first + needle.length) !== -1) { + throw new Error(`mutation marker is ambiguous in ${path}: ${needle}`); +} +fs.writeFileSync(path, source.slice(0, first) + replacement + source.slice(first + needle.length)); +NODE +} + +expect_failure_with_digest() { + local label="$1" + local fixture="$2" + local digest="$3" + local expected="$4" + local output status=0 + output="$( + TESTFLIGHT_PUBLICATION_AUDIT_ROOT="$fixture" \ + TESTFLIGHT_PUBLICATION_AUDIT_EXPECTED_MANIFEST_SHA256="$digest" \ + bash "$AUDIT" 2>&1 + )" || status=$? + [[ "$status" -ne 0 ]] || fail "$label unexpectedly passed" + grep -Fq -- "$expected" <<<"$output" || + fail "$label failed for the wrong reason; expected '$expected', got: $output" + CASES=$((CASES + 1)) + echo "[testflight-publication-test] rejected $label" +} + +expect_failure() { + local label="$1" + local fixture="$2" + local expected="$3" + local digest + digest="$(sha256_file "$fixture/config/testflight-publication-readiness.json")" + expect_failure_with_digest "$label" "$fixture" "$digest" "$expected" +} + +baseline="$(new_fixture baseline)" +TESTFLIGHT_PUBLICATION_AUDIT_ROOT="$baseline" bash "$AUDIT" >/dev/null || + fail "valid blocked TestFlight publication baseline was rejected" +CASES=$((CASES + 1)) +echo "[testflight-publication-test] accepted valid blocked baseline" + +fixture="$(new_fixture schema-version)" +set_json "$fixture/config/testflight-publication-readiness.json" schemaVersion 2 +expect_failure "schema version drift" "$fixture" "schemaVersion must be 1" + +fixture="$(new_fixture platform)" +set_json "$fixture/config/testflight-publication-readiness.json" platform '"android"' +expect_failure "platform drift" "$fixture" "platform must be ios" + +fixture="$(new_fixture distribution-channel)" +set_json "$fixture/config/testflight-publication-readiness.json" distributionChannel '"app-store-production"' +expect_failure "distribution channel drift" "$fixture" "distribution channel drifted" + +fixture="$(new_fixture assessment-timestamp)" +set_json "$fixture/config/testflight-publication-readiness.json" assessedAtUTC '"2026-07-26T13:26:12Z"' +expect_failure "assessment timestamp drift" "$fixture" "assessment timestamp drifted" + +fixture="$(new_fixture evidence-contract)" +set_json "$fixture/config/testflight-publication-readiness.json" evidenceBoundary.contract '"raw-build-output"' +expect_failure "evidence boundary drift" "$fixture" "evidence boundary contract drifted" + +fixture="$(new_fixture evidence-method)" +set_json "$fixture/config/testflight-publication-readiness.json" evidenceBoundary.observationMethod '"api"' +expect_failure "fabricated observation method" "$fixture" "observation method drifted" + +fixture="$(new_fixture raw-tracked)" +set_json "$fixture/config/testflight-publication-readiness.json" evidenceBoundary.rawAttestationsTracked true +expect_failure "ignored raw attestations claimed tracked" "$fixture" "ignored raw attestations must not be claimed as tracked" + +fixture="$(new_fixture raw-location)" +set_json "$fixture/config/testflight-publication-readiness.json" evidenceBoundary.rawAttestationsLocation '"tracked-source"' +expect_failure "raw attestation location drift" "$fixture" "raw attestation location drifted" + +fixture="$(new_fixture stale-state-accepted)" +set_json "$fixture/config/testflight-publication-readiness.json" evidenceBoundary.currentStateRevalidationRequired false +expect_failure "mutable state revalidation removed" "$fixture" "mutable App Store Connect state must require revalidation" + +fixture="$(new_fixture source-commit)" +set_json "$fixture/config/testflight-publication-readiness.json" artifact.sourceCommit '"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"' +expect_failure "artifact source commit drift" "$fixture" "artifact source commit drifted" + +fixture="$(new_fixture bundle-id)" +set_json "$fixture/config/testflight-publication-readiness.json" artifact.bundleIdentifier '"jp.co.soramitsu.fearlesswallet.dev"' +expect_failure "artifact bundle drift" "$fixture" "artifact bundle identifier drifted" + +fixture="$(new_fixture build-number)" +set_json "$fixture/config/testflight-publication-readiness.json" artifact.buildNumber '"2026.7.15"' +expect_failure "artifact build drift" "$fixture" "artifact build number drifted" + +fixture="$(new_fixture archive-digest)" +set_json "$fixture/config/testflight-publication-readiness.json" artifact.preUploadArchiveTreeSHA256 '"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"' +expect_failure "archive tree digest drift" "$fixture" "pre-upload archive tree SHA-256 drifted" + +fixture="$(new_fixture executable-digest)" +set_json "$fixture/config/testflight-publication-readiness.json" artifact.executableSHA256 '"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"' +expect_failure "executable digest drift" "$fixture" "executable SHA-256 drifted" + +fixture="$(new_fixture receipt-digest)" +set_json "$fixture/config/testflight-publication-readiness.json" artifact.signedArchiveAuditReceiptSHA256 '"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"' +expect_failure "signed audit receipt drift" "$fixture" "signed archive audit receipt SHA-256 drifted" + +fixture="$(new_fixture rehearsal-digest)" +set_json "$fixture/config/testflight-publication-readiness.json" artifact.preservedDataArchiveRehearsalResultSHA256 '"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"' +expect_failure "archive rehearsal result drift" "$fixture" "preserved-data archive rehearsal result SHA-256 drifted" + +fixture="$(new_fixture upload-status)" +set_json "$fixture/config/testflight-publication-readiness.json" upload.status '"failed"' +expect_failure "upload success removed" "$fixture" "upload status must remain success" + +fixture="$(new_fixture upload-time)" +set_json "$fixture/config/testflight-publication-readiness.json" upload.uploadedAtUTC '"2026-07-26T13:17:00Z"' +expect_failure "upload timestamp drift" "$fixture" "upload timestamp drifted" + +fixture="$(new_fixture post-upload-tree)" +set_json "$fixture/config/testflight-publication-readiness.json" upload.postUploadArchiveTreeSHA256 '"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"' +expect_failure "post-upload tree drift" "$fixture" "post-upload archive tree SHA-256 drifted" + +fixture="$(new_fixture post-upload-attestation)" +set_json "$fixture/config/testflight-publication-readiness.json" upload.postUploadAttestationSHA256 '"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"' +expect_failure "post-upload attestation drift" "$fixture" "post-upload attestation SHA-256 drifted" + +fixture="$(new_fixture xcode-mutation)" +set_json "$fixture/config/testflight-publication-readiness.json" upload.xcodeMutation '"unbounded"' +expect_failure "Xcode mutation scope widened" "$fixture" "Xcode post-upload mutation scope drifted" + +fixture="$(new_fixture reconstructed-tree)" +set_json "$fixture/config/testflight-publication-readiness.json" upload.reconstructedPreUploadArchiveTreeMatched false +expect_failure "reconstructed tree mismatch accepted" "$fixture" "reconstructed pre-upload archive tree match must remain true" + +fixture="$(new_fixture signature)" +set_json "$fixture/config/testflight-publication-readiness.json" upload.embeddedAppSignatureVerified false +expect_failure "embedded signature failure accepted" "$fixture" "embedded app signature verification must remain true" + +fixture="$(new_fixture processing-status)" +set_json "$fixture/config/testflight-publication-readiness.json" testFlight.processingStatus '"ready-to-submit"' +expect_failure "TestFlight status regressed" "$fixture" "TestFlight processing status must remain testing" + +fixture="$(new_fixture external-testing)" +set_json "$fixture/config/testflight-publication-readiness.json" testFlight.externalTestingActive false +expect_failure "external testing disabled" "$fixture" "external TestFlight testing must remain active" + +fixture="$(new_fixture group-name)" +set_json "$fixture/config/testflight-publication-readiness.json" testFlight.externalGroup.name '"Internal Only"' +expect_failure "external group drift" "$fixture" "external group name drifted" + +fixture="$(new_fixture group-id)" +set_json "$fixture/config/testflight-publication-readiness.json" testFlight.externalGroup.id '"00000000-0000-0000-0000-000000000000"' +expect_failure "external group identifier drift" "$fixture" "external group identifier drifted" + +fixture="$(new_fixture group-testers)" +set_json "$fixture/config/testflight-publication-readiness.json" testFlight.externalGroup.testerCount 123 +expect_failure "external group tester snapshot drift" "$fixture" "external group tester count snapshot drifted" + +fixture="$(new_fixture group-builds)" +set_json "$fixture/config/testflight-publication-readiness.json" testFlight.externalGroup.buildCount 1 +expect_failure "external group build snapshot drift" "$fixture" "external group build count snapshot drifted" + +fixture="$(new_fixture group-missing-build)" +set_json "$fixture/config/testflight-publication-readiness.json" testFlight.externalGroup.containsBuild false +expect_failure "external group build assignment removed" "$fixture" "external group must contain build 2026.7.26" + +fixture="$(new_fixture public-count)" +set_json "$fixture/config/testflight-publication-readiness.json" testFlight.externalGroup.publicLinkTesterCount 121 +expect_failure "public-link tester snapshot drift" "$fixture" "public-link tester count snapshot drifted" + +fixture="$(new_fixture public-limit)" +set_json "$fixture/config/testflight-publication-readiness.json" testFlight.externalGroup.publicLinkTesterLimit 501 +expect_failure "public-link limit drift" "$fixture" "public-link tester limit drifted" + +fixture="$(new_fixture link-disabled)" +set_json "$fixture/config/testflight-publication-readiness.json" testFlight.publicLink.enabled false +expect_failure "public link disabled" "$fixture" "public TestFlight link must remain enabled" + +fixture="$(new_fixture link-private)" +set_json "$fixture/config/testflight-publication-readiness.json" testFlight.publicLink.shareableToAnyoneWithLink false +expect_failure "anyone-with-link access removed" "$fixture" "public TestFlight link must remain shareable to anyone with the link" + +fixture="$(new_fixture link-origin)" +set_json "$fixture/config/testflight-publication-readiness.json" testFlight.publicLink.url '"https://evil.example/join/012KzFyD"' +expect_failure "hostile public-link origin" "$fixture" "public TestFlight URL drifted" + +fixture="$(new_fixture link-digest)" +set_json "$fixture/config/testflight-publication-readiness.json" testFlight.publicLink.urlSHA256 '"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"' +expect_failure "public-link digest drift" "$fixture" "public TestFlight URL SHA-256 drifted" + +fixture="$(new_fixture landing-page)" +set_json "$fixture/config/testflight-publication-readiness.json" testFlight.publicLink.landingPageResolved false +expect_failure "landing page resolution removed" "$fixture" "public landing page resolution must remain true" + +fixture="$(new_fixture app-name)" +set_json "$fixture/config/testflight-publication-readiness.json" testFlight.publicLink.landingPageApplicationName '"Lookalike Wallet"' +expect_failure "landing-page app identity drift" "$fixture" "public landing-page application identity drifted" + +fixture="$(new_fixture deep-link)" +set_json "$fixture/config/testflight-publication-readiness.json" testFlight.publicLink.testFlightDeepLinkPresent false +expect_failure "TestFlight deep link removed" "$fixture" "public landing page must retain the TestFlight deep link" + +fixture="$(new_fixture what-to-test)" +set_json "$fixture/config/testflight-publication-readiness.json" testFlight.whatToTestSHA256 '"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"' +expect_failure "What to Test digest drift" "$fixture" "What to Test text SHA-256 drifted" + +fixture="$(new_fixture notifications)" +set_json "$fixture/config/testflight-publication-readiness.json" testFlight.automaticTesterNotification false +expect_failure "tester notification drift" "$fixture" "automatic tester notification must remain recorded as enabled" + +fixture="$(new_fixture publication-attestation)" +set_json "$fixture/config/testflight-publication-readiness.json" testFlight.publicationAttestationSHA256 '"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"' +expect_failure "publication attestation drift" "$fixture" "publication attestation SHA-256 drifted" + +fixture="$(new_fixture symbolication-status)" +set_json "$fixture/config/testflight-publication-readiness.json" symbolication.status '"complete"' +expect_failure "symbolication falsely closed" "$fixture" "symbolication status drifted" + +fixture="$(new_fixture dsym-runtime-impact)" +set_json "$fixture/config/testflight-publication-readiness.json" symbolication.runtimeInstallLaunchOrMigrationImpact true +expect_failure "dSYM warning misclassified as runtime failure" "$fixture" "dSYM warnings must not be classified as an install, launch, or migration runtime failure" + +fixture="$(new_fixture app-dsym)" +set_json "$fixture/config/testflight-publication-readiness.json" symbolication.mainApplicationDSYMPresent false +expect_failure "main app dSYM removed" "$fixture" "main application dSYM presence must remain recorded" + +fixture="$(new_fixture vendor-dsym)" +set_json "$fixture/config/testflight-publication-readiness.json" symbolication.runtimeFrameworkMissingVendorDSYM '"sr25519lib.framework"' +expect_failure "runtime vendor dSYM gap drift" "$fixture" "runtime framework vendor-dSYM gap drifted" + +fixture="$(new_fixture static-stubs)" +set_json "$fixture/config/testflight-publication-readiness.json" symbolication.staticStubFrameworkWarnings '["libed25519.framework","blake2lib.framework","sr25519lib.framework"]' +expect_failure "static-stub warning order drift" "$fixture" "static stub framework warning inventory drifted" + +fixture="$(new_fixture symbolication-followup)" +set_json "$fixture/config/testflight-publication-readiness.json" symbolication.productionFollowUpRequired false +expect_failure "symbolication follow-up prematurely closed" "$fixture" "third-party production symbolication follow-up must remain required" + +fixture="$(new_fixture ready-status)" +set_json "$fixture/config/testflight-publication-readiness.json" readiness.status '"ready"' +expect_failure "premature readiness status" "$fixture" "TestFlight readiness must remain blocked" + +fixture="$(new_fixture publication-unverified)" +set_json "$fixture/config/testflight-publication-readiness.json" readiness.publicationVerified false +expect_failure "publication verification removed" "$fixture" "publicationVerified must remain true" + +fixture="$(new_fixture readiness-external)" +set_json "$fixture/config/testflight-publication-readiness.json" readiness.externalTestingActive false +expect_failure "readiness external testing drift" "$fixture" "readiness externalTestingActive must remain true" + +fixture="$(new_fixture install-fabrication)" +set_json "$fixture/config/testflight-publication-readiness.json" readiness.exactAppleDeliveredInstallVerified true +expect_failure "exact Apple-delivered install fabricated" "$fixture" "exact Apple-delivered TestFlight installation must not be claimed" + +fixture="$(new_fixture rehearsal-fabrication)" +set_json "$fixture/config/testflight-publication-readiness.json" readiness.preservedDataPostInstallRehearsalVerified true +expect_failure "post-install rehearsal fabricated" "$fixture" "post-install preserved-data rehearsal must not be claimed" + +fixture="$(new_fixture release-enabled)" +set_json "$fixture/config/testflight-publication-readiness.json" readiness.releaseEnabled true +expect_failure "release prematurely enabled" "$fixture" "releaseEnabled must remain false" + +fixture="$(new_fixture blocker-code)" +set_json "$fixture/config/testflight-publication-readiness.json" readiness.blockerCode '"ready"' +expect_failure "install blocker removed" "$fixture" "TestFlight blocker code drifted" + +fixture="$(new_fixture required-action)" +set_json "$fixture/config/testflight-publication-readiness.json" readiness.requiredAction '"Delete and reinstall."' +expect_failure "destructive install action accepted" "$fixture" "TestFlight required action drifted" + +fixture="$(new_fixture extra-key)" +set_json "$fixture/config/testflight-publication-readiness.json" unexpected true +expect_failure "unexpected manifest key" "$fixture" "top-level manifest keys drifted" + +fixture="$(new_fixture duplicate-key)" +replace_once "$fixture/config/testflight-publication-readiness.json" \ + $'{\n "schemaVersion": 1,' \ + $'{\n "schemaVersion": 1,\n "schemaVersion": 1,' +expect_failure "duplicate manifest key" "$fixture" "canonical two-space JSON without duplicate keys or trailing data" + +fixture="$(new_fixture trailing-data)" +printf '%s\n' '{"unexpected":true}' >> "$fixture/config/testflight-publication-readiness.json" +expect_failure "trailing JSON data" "$fixture" "invalid publication JSON" + +fixture="$(new_fixture digest-mismatch)" +set_json "$fixture/config/testflight-publication-readiness.json" schemaVersion 2 +expect_failure_with_digest \ + "manifest digest mismatch" \ + "$fixture" \ + "d66634ec2fce2305e2cc64e18e55c273dcc802bc82e074f658bef72ad8d53b26" \ + "publication manifest digest mismatch" + +fixture="$(new_fixture invalid-expected-digest)" +expect_failure_with_digest \ + "invalid expected digest" \ + "$fixture" \ + "not-a-digest" \ + "expected manifest digest must be exactly 64 lowercase hexadecimal characters" + +fixture="$(new_fixture manifest-symlink)" +mv "$fixture/config/testflight-publication-readiness.json" "$fixture/config/manifest.real" +ln -s manifest.real "$fixture/config/testflight-publication-readiness.json" +expect_failure_with_digest \ + "manifest symlink substitution" \ + "$fixture" \ + "d66634ec2fce2305e2cc64e18e55c273dcc802bc82e074f658bef72ad8d53b26" \ + "required regular file is missing or is a symlink: config/testflight-publication-readiness.json" + +fixture="$(new_fixture missing-manifest)" +rm "$fixture/config/testflight-publication-readiness.json" +expect_failure_with_digest \ + "missing manifest" \ + "$fixture" \ + "d66634ec2fce2305e2cc64e18e55c273dcc802bc82e074f658bef72ad8d53b26" \ + "required regular file is missing or is a symlink: config/testflight-publication-readiness.json" + +fixture="$(new_fixture documentation)" +replace_once "$fixture/docs/testflight-publication-readiness.md" \ + 'Do not uninstall the app or clear its container.' \ + 'Delete and reinstall the app.' +expect_failure "destructive documentation drift" "$fixture" "documented preserved-data install constraint" + +fixture="$(new_fixture checklist)" +replace_once "$fixture/docs/release-checklist.md" \ + 'bash ./scripts/test-testflight-publication-readiness-audit.sh && bash ./scripts/audit-testflight-publication-readiness.sh' \ + 'bash ./scripts/audit-testflight-publication-readiness.sh' +expect_failure "release checklist self-test wiring removed" "$fixture" "release-checklist TestFlight publication gate" + +fixture="$(new_fixture run-pr-test)" +replace_once "$fixture/scripts/ci/run-pr.sh" \ + 'bash "$WORKSPACE_DIR/scripts/test-testflight-publication-readiness-audit.sh"' \ + 'echo "TestFlight publication self-test skipped"' +expect_failure "PR publication self-test removed" "$fixture" "PR TestFlight publication adversarial self-test" + +fixture="$(new_fixture run-pr-audit)" +replace_once "$fixture/scripts/ci/run-pr.sh" \ + 'bash "$WORKSPACE_DIR/scripts/audit-testflight-publication-readiness.sh"' \ + 'echo "TestFlight publication audit skipped"' +expect_failure "PR publication audit removed" "$fixture" "PR TestFlight publication audit" + +fixture="$(new_fixture workflow-test)" +replace_once "$fixture/.github/workflows/codecov.yml" \ + 'bash ./scripts/test-testflight-publication-readiness-audit.sh' \ + 'echo "TestFlight publication self-test skipped"' +expect_failure "CI publication self-test removed" "$fixture" "CI TestFlight publication adversarial self-test" + +fixture="$(new_fixture workflow-audit)" +replace_once "$fixture/.github/workflows/codecov.yml" \ + 'bash ./scripts/audit-testflight-publication-readiness.sh' \ + 'echo "TestFlight publication audit skipped"' +expect_failure "CI publication audit removed" "$fixture" "CI TestFlight publication audit" + +if ((CASES != EXPECTED_CASES)); then + fail "executed $CASES cases; expected exactly $EXPECTED_CASES" +fi + +echo "[testflight-publication-test] passed $CASES cases (1 valid + $((CASES - 1)) negative/adversarial)" From 02e762086c2bd5d3027c8355b6703633d7e24fa0 Mon Sep 17 00:00:00 2001 From: Makoto Takemiya Date: Sun, 26 Jul 2026 18:30:41 +0400 Subject: [PATCH 16/30] build: bump TestFlight build for in-place update --- Jenkinsfile | 2 +- fearless.xcodeproj/project.pbxproj | 2 +- scripts/ci/build-audited-ios-release-archive.sh | 8 ++++---- scripts/test-coredata-simulator-rehearsal.sh | 8 ++++---- scripts/test-ios-release-identity-audit.sh | 4 ++-- scripts/test-ios-signed-release-artifact-audit.sh | 4 ++-- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 92945a4a21..8a06ae63eb 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -134,7 +134,7 @@ try { echo "Exact Release gate requires dispatch to an arm64 macOS agent." >&2 exit 78 } - IOS_EXPECTED_BUILD_NUMBER=2026.7.26 \ + IOS_EXPECTED_BUILD_NUMBER=2026.7.27 \ IOS_RELEASE_SOURCE_PACKAGES_DIR="$PWD/SourcePackages" \ bash scripts/ci/audit-ios-release-identity.sh bash scripts/storage/audit-user-storage-compatibility-models.sh diff --git a/fearless.xcodeproj/project.pbxproj b/fearless.xcodeproj/project.pbxproj index f9d0410cc7..5ec9c3fdc3 100644 --- a/fearless.xcodeproj/project.pbxproj +++ b/fearless.xcodeproj/project.pbxproj @@ -19547,7 +19547,7 @@ CODE_SIGN_ENTITLEMENTS = fearless/WalletConnect.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: Soramitsu Co., Ltd. (YLWWUD25VZ)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2026.7.26; + CURRENT_PROJECT_VERSION = 2026.7.27; DEVELOPMENT_TEAM = YLWWUD25VZ; ENABLE_BITCODE = NO; INFOPLIST_FILE = fearless/Info.plist; diff --git a/scripts/ci/build-audited-ios-release-archive.sh b/scripts/ci/build-audited-ios-release-archive.sh index af9db52132..6abcc6ca8f 100644 --- a/scripts/ci/build-audited-ios-release-archive.sh +++ b/scripts/ci/build-audited-ios-release-archive.sh @@ -13,9 +13,9 @@ readonly EXPECTED_SIGNING_IDENTITY="Apple Distribution: Soramitsu Co., Ltd. (YLW readonly EXPECTED_SIGNING_CERTIFICATE_SHA1="84AB95335BE14CAE9B050A353910F86FF2F9539B" readonly EXPECTED_PROFILE_NAME="Fearless App Store 2026.7.26" readonly EXPECTED_PROFILE_UUID="0d51265e-4b53-4a1f-814a-436dc9ca087b" -# Read-only App Store Connect inspection on 2026-07-26 found only 2026.7.14 -# and 2026.7.15 for 4.2.0. This candidate number was unused at inspection time. -readonly EXPECTED_BUILD="2026.7.26" +# Read-only App Store Connect inspection on 2026-07-26 found 2026.7.26 as the +# newest 4.2.0 build. This candidate number was unused at inspection time. +readonly EXPECTED_BUILD="2026.7.27" readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" readonly REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd -P)" @@ -33,7 +33,7 @@ Usage: Preconditions: - exact clean git HEAD, including no untracked files; - - App Store Connect read-only uniqueness check for 4.2.0 (2026.7.26); + - App Store Connect read-only uniqueness check for 4.2.0 (2026.7.27); - App Store distribution profile for the production App ID, with group.jp.co.soramitsu.fearlesswallet and Apple default keychain groups. diff --git a/scripts/test-coredata-simulator-rehearsal.sh b/scripts/test-coredata-simulator-rehearsal.sh index 8bff42ec4f..2c9aefbaa1 100755 --- a/scripts/test-coredata-simulator-rehearsal.sh +++ b/scripts/test-coredata-simulator-rehearsal.sh @@ -230,7 +230,7 @@ prepare_case() { CFBundleShortVersionString 4.2.0 CFBundleVersion - 2026.7.26 + 2026.7.27 FearlessBuildConfiguration Release FearlessSwiftOptimizationLevel @@ -294,7 +294,7 @@ APP --app "$APP_DIR" --bundle-id "$BUNDLE_ID" --expected-git-sha 7a819cb01e92920e5444151392df54245acdd4c8 - --expected-build 2026.7.26 + --expected-build 2026.7.27 --expected-executable-sha256 "$executable_sha" --fixture-dir "$FIXTURE_DIR" --artifacts-dir "$ARTIFACTS_DIR" @@ -604,7 +604,7 @@ CASE_ARGS=( --app "$APP_DIR" --bundle-id "$BUNDLE_ID" --expected-git-sha 7a819cb01e92920e5444151392df54245acdd4c8 - --expected-build 2026.7.26 + --expected-build 2026.7.27 --expected-executable-sha256 "$(shasum -a 256 "$APP_DIR/fearless" | awk '{print $1}')" --fixture-dir "$FIXTURE_DIR" --artifacts-dir "$ARTIFACTS_DIR" @@ -722,7 +722,7 @@ CASE_ARGS=( --app "$APP_DIR" --bundle-id "$BUNDLE_ID" --expected-git-sha 7a819cb01e92920e5444151392df54245acdd4c8 - --expected-build 2026.7.26 + --expected-build 2026.7.27 --expected-executable-sha256 "$(shasum -a 256 "$APP_DIR/fearless" | awk '{print $1}')" --fixture-dir "$FIXTURE_DIR" --artifacts-dir "$ARTIFACTS_DIR" diff --git a/scripts/test-ios-release-identity-audit.sh b/scripts/test-ios-release-identity-audit.sh index f4cd201fe0..4545a9315b 100755 --- a/scripts/test-ios-release-identity-audit.sh +++ b/scripts/test-ios-release-identity-audit.sh @@ -41,7 +41,7 @@ jq -n '[ buildSettings: { PRODUCT_BUNDLE_IDENTIFIER: "jp.co.soramitsu.fearlesswallet", MARKETING_VERSION: "4.2.0", - CURRENT_PROJECT_VERSION: "2026.7.26", + CURRENT_PROJECT_VERSION: "2026.7.27", CODE_SIGN_ENTITLEMENTS: "fearless/WalletConnect.entitlements", CODE_SIGN_IDENTITY: "Apple Distribution: Soramitsu Co., Ltd. (YLWWUD25VZ)", CODE_SIGN_STYLE: "Manual", @@ -75,7 +75,7 @@ run_audit() { local debug_entitlements_file="${7:-$dev_entitlements}" IOS_RELEASE_AUDIT_TEST_HARNESS=1 \ - IOS_EXPECTED_BUILD_NUMBER=2026.7.26 \ + IOS_EXPECTED_BUILD_NUMBER=2026.7.27 \ IOS_RELEASE_SETTINGS_JSON="$1" \ IOS_DEBUG_SETTINGS_JSON="$debug_settings_file" \ IOS_RELEASE_SCHEME_FILE="$2" \ diff --git a/scripts/test-ios-signed-release-artifact-audit.sh b/scripts/test-ios-signed-release-artifact-audit.sh index 566ee8293c..a53c6e80be 100755 --- a/scripts/test-ios-signed-release-artifact-audit.sh +++ b/scripts/test-ios-signed-release-artifact-audit.sh @@ -116,7 +116,7 @@ app = { "CFBundleExecutable": "fearless", "CFBundleIdentifier": bundle, "CFBundleShortVersionString": "4.2.0", - "CFBundleVersion": "2026.7.26", + "CFBundleVersion": "2026.7.27", "FearlessBuildConfiguration": "Release", "FearlessEnableTestability": "NO", "FearlessGitCommit": git_sha, @@ -172,7 +172,7 @@ run_audit() { bash "$AUDIT" \ --archive "$ARCHIVE" \ --expected-git-sha "${EXPECTED_GIT_OVERRIDE:-$EXPECTED_GIT_SHA}" \ - --expected-build 2026.7.26 \ + --expected-build 2026.7.27 \ --expected-executable-sha256 "${EXPECTED_EXECUTABLE_SHA_OVERRIDE:-$executable_sha}" \ --expected-archive-sha256 "${EXPECTED_ARCHIVE_SHA_OVERRIDE:-$archive_sha}" \ --expected-signing-certificate-sha1 "${EXPECTED_SIGNING_CERTIFICATE_SHA_OVERRIDE:-$signing_certificate_sha1}" \ From cdb84744f5b8b48fb24ae003707e6004f6ab04ee Mon Sep 17 00:00:00 2001 From: Makoto Takemiya Date: Sun, 26 Jul 2026 20:30:44 +0400 Subject: [PATCH 17/30] fix: make wallet observation activation lossless --- .../Helpers/AccountProviderFactory.swift | 287 +++++++++--- .../SelectedAccountSettingsTests.swift | 429 ++++++++++++++++++ 2 files changed, 647 insertions(+), 69 deletions(-) diff --git a/fearless/Common/Helpers/AccountProviderFactory.swift b/fearless/Common/Helpers/AccountProviderFactory.swift index 745d352feb..ff805ea56c 100644 --- a/fearless/Common/Helpers/AccountProviderFactory.swift +++ b/fearless/Common/Helpers/AccountProviderFactory.swift @@ -120,15 +120,26 @@ final class AccountProviderFactory: AccountProviderFactoryProtocol { /// RobinHood's generic observable drops mapper failures for inserts and updates, but emits deletes /// directly from the raw identifier. A corrupt row that duplicates a healthy wallet identifier can /// therefore remove the healthy item from a stream when that corrupt row is deleted. Tracking -/// successfully mapped rows by object ID makes deletes unambiguous and also turns a valid-to-corrupt -/// update into removal of that one previously valid object. -private final class TolerantMetaAccountContextObservable: +/// successfully mapped rows by object ID makes deletes unambiguous. Aggregating every notification +/// by persistent identifier also makes same-identifier object handoffs independent of Core Data's +/// unordered notification sets. +final class TolerantMetaAccountContextObservable: DataProviderRepositoryObservable { private struct MappedObject { let objectId: NSManagedObjectID let model: Model? } + private enum ObjectMutation { + case replace(Model?) + case delete + } + + private struct InFlightDelivery { + let observerIdentifier: ObjectIdentifier + let identifiers: Set + } + private let service: CoreDataServiceProtocol private let mapper: AnyCoreDataMapper private let predicate: (CDMetaAccount) -> Bool @@ -136,6 +147,10 @@ private final class TolerantMetaAccountContextObservable: private var observers: [RepositoryObserver] = [] private var trackedModels: [NSManagedObjectID: Model] = [:] + private var pendingReconciliationIdentifiers = Set() + // StreamableProvider can queue source callbacks behind its user-observer teardown. + // Reconcile identifiers whose serial source deliveries were not acknowledged first. + private var inFlightDeliveries: [UUID: InFlightDelivery] = [:] private var notificationToken: NSObjectProtocol? init( @@ -192,6 +207,8 @@ private final class TolerantMetaAccountContextObservable: return ($0.objectId, model) } ) + self.pendingReconciliationIdentifiers.removeAll() + self.inFlightDeliveries.removeAll() completionBlock(nil) } } catch { @@ -216,6 +233,8 @@ private final class TolerantMetaAccountContextObservable: self.processingQueue.async { self.trackedModels.removeAll() + self.pendingReconciliationIdentifiers.removeAll() + self.inFlightDeliveries.removeAll() completionBlock(optionalError) } } @@ -240,14 +259,44 @@ private final class TolerantMetaAccountContextObservable: updateBlock: updateBlock ) ) + + let reconciliationChanges = self.reconciliationChanges( + for: self.pendingReconciliationIdentifiers + ) + self.pendingReconciliationIdentifiers.removeAll() + self.deliver(reconciliationChanges) } } func removeObserver(_ observer: AnyObject) { processingQueue.async { + let observerIdentifier = ObjectIdentifier(observer) + let wasRegistered = self.observers.contains { + $0.observer === observer + } self.observers = self.observers.filter { $0.observer != nil && $0.observer !== observer } + + if wasRegistered, self.observers.isEmpty { + let deliveryTokens = self.inFlightDeliveries.compactMap { + token, delivery in + delivery.observerIdentifier == observerIdentifier + ? token + : nil + } + for token in deliveryTokens { + guard let delivery = self.inFlightDeliveries.removeValue( + forKey: token + ) else { + continue + } + + self.pendingReconciliationIdentifiers.formUnion( + delivery.identifiers + ) + } + } } } @@ -274,72 +323,12 @@ private final class TolerantMetaAccountContextObservable: } processingQueue.async { - var changes: [DataProviderChange] = [] - - for object in updatedObjects { - self.applyMappedObject( - object, - preferredChange: .update, - to: &changes - ) - } - for objectId in deletedObjectIds { - if let previousModel = self.trackedModels.removeValue(forKey: objectId) { - changes.append( - .delete(deletedIdentifier: previousModel.identifier) - ) - } - } - for object in insertedObjects { - self.applyMappedObject( - object, - preferredChange: .insert, - to: &changes - ) - } - - self.deliver(changes) - } - } - - private enum PreferredChange { - case insert - case update - } - - private func applyMappedObject( - _ object: MappedObject, - preferredChange: PreferredChange, - to changes: inout [DataProviderChange] - ) { - guard let model = object.model else { - if let previousModel = trackedModels.removeValue(forKey: object.objectId) { - changes.append( - .delete(deletedIdentifier: previousModel.identifier) - ) - } - return - } - - if let previousModel = trackedModels[object.objectId] { - if previousModel.identifier == model.identifier { - changes.append(.update(newItem: model)) - } else { - changes.append( - .delete(deletedIdentifier: previousModel.identifier) - ) - changes.append(.insert(newItem: model)) - } - } else { - switch preferredChange { - case .insert, .update: - // A previously corrupt/nonmatching object becoming valid is new to the stream, - // even though Core Data reports it as an update. - changes.append(.insert(newItem: model)) - } + self.apply( + updatedObjects: updatedObjects, + deletedObjectIds: deletedObjectIds, + insertedObjects: insertedObjects + ) } - - trackedModels[object.objectId] = model } private func mappedObject(for entity: CDMetaAccount) -> MappedObject { @@ -367,20 +356,180 @@ private final class TolerantMetaAccountContextObservable: return objects.allObjects.compactMap { $0 as? CDMetaAccount } } + private func apply( + updatedObjects: [MappedObject], + deletedObjectIds: [NSManagedObjectID], + insertedObjects: [MappedObject] + ) { + var mutations: [NSManagedObjectID: ObjectMutation] = [:] + for object in updatedObjects { + mutations[object.objectId] = .replace(object.model) + } + for object in insertedObjects { + mutations[object.objectId] = .replace(object.model) + } + for objectId in deletedObjectIds { + mutations[objectId] = .delete + } + + var affectedIdentifiers = Set() + for (objectId, mutation) in mutations { + if let previousModel = trackedModels[objectId] { + affectedIdentifiers.insert(previousModel.identifier) + } + + if case let .replace(model) = mutation, let model { + affectedIdentifiers.insert(model.identifier) + } + } + + let modelsBefore = aggregateModels(for: affectedIdentifiers) + + for (objectId, mutation) in mutations { + switch mutation { + case let .replace(model): + trackedModels[objectId] = model + case .delete: + trackedModels.removeValue(forKey: objectId) + } + } + + let modelsAfter = aggregateModels(for: affectedIdentifiers) + let changes = affectedIdentifiers.sorted().compactMap { identifier in + aggregateChange( + identifier: identifier, + before: modelsBefore[identifier], + after: modelsAfter[identifier] + ) + } + deliver(changes) + } + + private func aggregateModels( + for identifiers: Set + ) -> [String: Model] { + guard !identifiers.isEmpty else { + return [:] + } + + var models: [String: Model] = [:] + for (_, model) in trackedModels.sorted(by: objectIdOrder) { + guard + identifiers.contains(model.identifier), + models[model.identifier] == nil + else { + continue + } + + models[model.identifier] = model + } + + return models + } + + private func aggregateChange( + identifier: String, + before: Model?, + after: Model? + ) -> DataProviderChange? { + switch (before, after) { + case (nil, nil): + return nil + case (nil, let model?): + return .insert(newItem: model) + case (.some, nil): + return .delete(deletedIdentifier: identifier) + case let (.some, model?): + return .update(newItem: model) + } + } + + private func reconciliationChanges( + for identifiers: Set + ) -> [DataProviderChange] { + let currentModels = aggregateModels(for: identifiers) + + return identifiers.sorted().flatMap { identifier in + var changes: [DataProviderChange] = [ + .delete(deletedIdentifier: identifier) + ] + if let model = currentModels[identifier] { + changes.append(.insert(newItem: model)) + } + + return changes + } + } + private func deliver(_ changes: [DataProviderChange]) { guard !changes.isEmpty else { return } observers = observers.filter { $0.observer != nil } + guard !observers.isEmpty else { + pendingReconciliationIdentifiers.formUnion( + changes.map(changeIdentifier) + ) + return + } + for observer in observers { if processingQueue == observer.queue { observer.updateBlock(changes) } else { - observer.queue.async { + guard let observerObject = observer.observer else { + continue + } + + let observerIdentifier = ObjectIdentifier(observerObject) + let identifiers = Set(changes.map(changeIdentifier)) + let deliveryToken = beginDelivery( + identifiers: identifiers, + to: observerIdentifier + ) + observer.queue.async { [weak self] in observer.updateBlock(changes) + self?.finishDelivery(deliveryToken) } } } } + + private func beginDelivery( + identifiers: Set, + to observerIdentifier: ObjectIdentifier + ) -> UUID { + let token = UUID() + inFlightDeliveries[token] = InFlightDelivery( + observerIdentifier: observerIdentifier, + identifiers: identifiers + ) + return token + } + + private func finishDelivery(_ token: UUID) { + processingQueue.async { + self.inFlightDeliveries.removeValue(forKey: token) + } + } + + private func changeIdentifier( + _ change: DataProviderChange + ) -> String { + switch change { + case let .insert(model), let .update(model): + return model.identifier + case let .delete(identifier): + return identifier + } + } + + private func objectIdOrder( + _ lhs: (key: NSManagedObjectID, value: Model), + _ rhs: (key: NSManagedObjectID, value: Model) + ) -> Bool { + lhs.key.uriRepresentation().absoluteString < + rhs.key.uriRepresentation().absoluteString + } } diff --git a/fearlessTests/Common/Storage/SelectedAccountSettingsTests.swift b/fearlessTests/Common/Storage/SelectedAccountSettingsTests.swift index 3ac016b10f..a3d1bd5808 100644 --- a/fearlessTests/Common/Storage/SelectedAccountSettingsTests.swift +++ b/fearlessTests/Common/Storage/SelectedAccountSettingsTests.swift @@ -2019,6 +2019,10 @@ class SelectedAccountSettingsTests: XCTestCase { } func testTolerantFactoriesAndStreamsIgnoreCorruptSelectedSiblingAndKeepObservingValidWallet() throws { + try assertTolerantObservableReconcilesEveryObserverGap() + try assertTolerantObservableReconcilesTransientIdentifier() + try assertTolerantObservableAggregatesSameIdentifierHandoffs() + let facade = UserDataStorageTestFacade() let repositoryQueue = OperationQueue() let validWallet = copyWallet( @@ -2373,6 +2377,431 @@ class SelectedAccountSettingsTests: XCTestCase { XCTAssertTrue(corruptProjections.allSatisfy { $0.wallet == nil }) } + private func assertTolerantObservableReconcilesEveryObserverGap() throws { + let facade = UserDataStorageTestFacade() + let repositoryQueue = OperationQueue() + let processingQueue = DispatchQueue( + label: "jp.co.fearless.wallet-observable.observer-gap-test" + ) + let wallet = copyWallet( + AccountGenerator.generateMetaAccount(generatingChainAccounts: 1), + identifier: "observer-gap-wallet", + name: "Observer Gap Wallet" + ) + try seed( + [ManagedMetaAccountModel(info: wallet, isSelected: true)], + in: facade, + using: repositoryQueue + ) + + let observable = TolerantMetaAccountContextObservable( + service: facade.databaseService, + mapper: AnyCoreDataMapper(ManagedMetaAccountMapper()), + predicate: { _ in true }, + processingQueue: processingQueue + ) + let startExpectation = expectation(description: "gap observable starts") + observable.start { error in + XCTAssertNil(error) + startExpectation.fulfill() + } + wait(for: [startExpectation], timeout: Constants.defaultExpectationDuration) + + func updateWalletName(_ name: String) throws { + try performCoreData(in: facade) { context in + let request = NSFetchRequest(entityName: "CDMetaAccount") + request.predicate = NSPredicate( + format: "%K == %@", + #keyPath(CDMetaAccount.metaId), + wallet.identifier + ) + let entity = try XCTUnwrap(context.fetch(request).first) + entity.name = name + try context.save() + } + processingQueue.sync {} + } + + // Reconcile the exact activation race: valid -> corrupt -> valid completes + // after start but before RobinHood asynchronously registers its observer. + try updateWalletName(" \n\t ") + let restoredName = "Observer Gap Wallet Restored" + try updateWalletName(restoredName) + + let firstObserver = NSObject() + let firstReconciliationExpectation = expectation( + description: "pre-registration final state is reconciled" + ) + var firstBatch: [DataProviderChange] = [] + observable.addObserver( + firstObserver, + deliverOn: .main, + executing: { changes in + firstBatch = changes + firstReconciliationExpectation.fulfill() + } + ) + wait( + for: [firstReconciliationExpectation], + timeout: Constants.defaultExpectationDuration + ) + XCTAssertEqual(firstBatch.count, 2) + if case let .delete(identifier)? = firstBatch.first { + XCTAssertEqual(identifier, wallet.identifier) + } else { + XCTFail("Reconciliation must invalidate the provider's stale snapshot") + } + if case let .insert(model)? = firstBatch.last { + XCTAssertEqual(model.identifier, wallet.identifier) + XCTAssertEqual(model.info.name, restoredName) + } else { + XCTFail("Reconciliation must finish with the current healthy wallet") + } + + observable.removeObserver(firstObserver) + processingQueue.sync {} + + let oldDeliveryQueue = DispatchQueue( + label: "jp.co.fearless.wallet-observable.old-delivery-test" + ) + let newDeliveryQueue = DispatchQueue( + label: "jp.co.fearless.wallet-observable.new-delivery-test" + ) + let oldDeliveryBlockStarted = DispatchSemaphore(value: 0) + let newDeliveryBlockStarted = DispatchSemaphore(value: 0) + let releaseOldDelivery = DispatchSemaphore(value: 0) + let releaseNewDelivery = DispatchSemaphore(value: 0) + defer { + releaseOldDelivery.signal() + releaseNewDelivery.signal() + } + oldDeliveryQueue.async { + oldDeliveryBlockStarted.signal() + releaseOldDelivery.wait() + } + newDeliveryQueue.async { + newDeliveryBlockStarted.signal() + releaseNewDelivery.wait() + } + XCTAssertEqual( + oldDeliveryBlockStarted.wait( + timeout: .now() + Constants.defaultExpectationDuration + ), + .success + ) + XCTAssertEqual( + newDeliveryBlockStarted.wait( + timeout: .now() + Constants.defaultExpectationDuration + ), + .success + ) + + let teardownObserver = NSObject() + observable.addObserver( + teardownObserver, + deliverOn: oldDeliveryQueue, + executing: { _ in } + ) + processingQueue.sync {} + + // Model StreamableProvider's teardown race deterministically. The source + // callback is already in flight when the provider loses its last user. + // Removal remains asynchronous, and the repair mutation happens without + // draining that removal first. + try updateWalletName("") + observable.removeObserver(teardownObserver) + let resubscribedName = "Observer Gap Wallet Resubscribed" + try updateWalletName(resubscribedName) + + // Re-add the identical observer while its old-generation callback is + // blocked, then independently block the new-generation reconciliation. + // Finishing the old callback must only acknowledge its unique delivery. + var blockedNewGenerationBatch: [ + DataProviderChange + ] = [] + observable.addObserver( + teardownObserver, + deliverOn: newDeliveryQueue, + executing: { changes in + blockedNewGenerationBatch = changes + } + ) + processingQueue.sync {} + releaseOldDelivery.signal() + oldDeliveryQueue.sync {} + processingQueue.sync {} + + // A second removal must still repend the independently in-flight new + // generation, proving an old completion cannot ABA-clear its state. + observable.removeObserver(teardownObserver) + processingQueue.sync {} + + let abaReconciliationExpectation = expectation( + description: "same observer generation is reconciled independently" + ) + var abaBatch: [DataProviderChange] = [] + observable.addObserver( + teardownObserver, + deliverOn: .main, + executing: { changes in + abaBatch = changes + abaReconciliationExpectation.fulfill() + } + ) + wait( + for: [abaReconciliationExpectation], + timeout: Constants.defaultExpectationDuration + ) + XCTAssertEqual(abaBatch.count, 2) + if case let .delete(identifier)? = abaBatch.first { + XCTAssertEqual(identifier, wallet.identifier) + } else { + XCTFail("Resubscription reconciliation must begin with a delete") + } + if case let .insert(model)? = abaBatch.last { + XCTAssertEqual(model.identifier, wallet.identifier) + XCTAssertEqual(model.info.name, resubscribedName) + } else { + XCTFail("Resubscription must finish with the current healthy wallet") + } + + releaseNewDelivery.signal() + newDeliveryQueue.sync {} + processingQueue.sync {} + XCTAssertEqual(blockedNewGenerationBatch.count, 2) + observable.removeObserver(teardownObserver) + processingQueue.sync {} + let stopExpectation = expectation(description: "gap observable stops") + observable.stop { error in + XCTAssertNil(error) + stopExpectation.fulfill() + } + wait(for: [stopExpectation], timeout: Constants.defaultExpectationDuration) + } + + private func assertTolerantObservableReconcilesTransientIdentifier() throws { + let facade = UserDataStorageTestFacade() + let processingQueue = DispatchQueue( + label: "jp.co.fearless.wallet-observable.transient-test" + ) + let wallet = copyWallet( + AccountGenerator.generateMetaAccount(generatingChainAccounts: 1), + identifier: "transient-observer-gap-wallet", + name: "Transient Observer Gap Wallet" + ) + let observable = TolerantMetaAccountContextObservable( + service: facade.databaseService, + mapper: AnyCoreDataMapper(ManagedMetaAccountMapper()), + predicate: { _ in true }, + processingQueue: processingQueue + ) + let startExpectation = expectation(description: "transient observable starts") + observable.start { error in + XCTAssertNil(error) + startExpectation.fulfill() + } + wait(for: [startExpectation], timeout: Constants.defaultExpectationDuration) + + // Coalescing is identifier based, so a row created and removed entirely + // within the gap must reconcile as a delete-only terminal state. + try performCoreData(in: facade) { context in + let entity = CDMetaAccount(context: context) + try MetaAccountMapper().populate( + entity: entity, + from: wallet, + using: context + ) + entity.isSelected = true + try context.save() + context.delete(entity) + try context.save() + } + processingQueue.sync {} + + let observer = NSObject() + let reconciliationExpectation = expectation( + description: "transient identifier is reconciled" + ) + var batch: [DataProviderChange] = [] + observable.addObserver( + observer, + deliverOn: .main, + executing: { changes in + batch = changes + reconciliationExpectation.fulfill() + } + ) + wait( + for: [reconciliationExpectation], + timeout: Constants.defaultExpectationDuration + ) + XCTAssertEqual(batch.count, 1) + if case let .delete(identifier)? = batch.first { + XCTAssertEqual(identifier, wallet.identifier) + } else { + XCTFail("A transient identifier must reconcile as delete-only") + } + + observable.removeObserver(observer) + processingQueue.sync {} + let stopExpectation = expectation(description: "transient observable stops") + observable.stop { error in + XCTAssertNil(error) + stopExpectation.fulfill() + } + wait( + for: [stopExpectation], + timeout: Constants.defaultExpectationDuration + ) + } + + private func assertTolerantObservableAggregatesSameIdentifierHandoffs() throws { + let facade = UserDataStorageTestFacade() + let repositoryQueue = OperationQueue() + let processingQueue = DispatchQueue( + label: "jp.co.fearless.wallet-observable.identifier-handoff-test" + ) + let identifier = "same-identifier-handoff-wallet" + let firstWallet = copyWallet( + AccountGenerator.generateMetaAccount(generatingChainAccounts: 1), + identifier: identifier, + name: "Handoff Wallet A" + ) + let secondWallet = copyWallet( + AccountGenerator.generateMetaAccount(generatingChainAccounts: 1), + identifier: identifier, + name: "Handoff Wallet B" + ) + try seed( + [ManagedMetaAccountModel(info: firstWallet, isSelected: true)], + in: facade, + using: repositoryQueue + ) + try performCoreData(in: facade) { context in + let entity = CDMetaAccount(context: context) + try MetaAccountMapper().populate( + entity: entity, + from: secondWallet, + using: context + ) + entity.isSelected = true + entity.order = 900 + entity.name = "" + try context.save() + } + + let observable = TolerantMetaAccountContextObservable( + service: facade.databaseService, + mapper: AnyCoreDataMapper(ManagedMetaAccountMapper()), + predicate: { _ in true }, + processingQueue: processingQueue + ) + let startExpectation = expectation(description: "handoff observable starts") + observable.start { error in + XCTAssertNil(error) + startExpectation.fulfill() + } + wait(for: [startExpectation], timeout: Constants.defaultExpectationDuration) + + let observer = NSObject() + let firstHandoffExpectation = expectation( + description: "healthy role moves from A to B" + ) + let secondHandoffExpectation = expectation( + description: "healthy role moves from B to A" + ) + var receivedBatches: [[DataProviderChange]] = [] + observable.addObserver( + observer, + deliverOn: .main, + executing: { changes in + receivedBatches.append(changes) + if receivedBatches.count == 1 { + firstHandoffExpectation.fulfill() + } else if receivedBatches.count == 2 { + secondHandoffExpectation.fulfill() + } else { + XCTFail("Each same-identifier handoff must emit one terminal batch") + } + } + ) + processingQueue.sync {} + + func moveHealthyRole(from oldName: String, to newName: String) throws { + try performCoreData(in: facade) { context in + let request = NSFetchRequest( + entityName: "CDMetaAccount" + ) + request.predicate = NSPredicate( + format: "%K == %@", + #keyPath(CDMetaAccount.metaId), + identifier + ) + let entities = try context.fetch(request) + XCTAssertEqual(entities.count, 2) + let healthyEntity = try XCTUnwrap( + entities.first { $0.name == oldName } + ) + let corruptEntity = try XCTUnwrap( + entities.first { + ($0.name ?? "").trimmingCharacters( + in: .whitespacesAndNewlines + ).isEmpty + } + ) + healthyEntity.name = "" + corruptEntity.name = newName + try context.save() + } + } + + // Both objects are updated in one save. Core Data exposes notification + // objects as an unordered set, so processing them one-by-one could emit a + // terminal delete even though the identifier remains healthy. + try moveHealthyRole( + from: firstWallet.name, + to: secondWallet.name + ) + wait( + for: [firstHandoffExpectation], + timeout: Constants.defaultExpectationDuration + ) + XCTAssertEqual(receivedBatches.count, 1) + XCTAssertEqual(receivedBatches[0].count, 1) + if case let .update(model)? = receivedBatches[0].first { + XCTAssertEqual(model.identifier, identifier) + XCTAssertEqual(model.info.name, secondWallet.name) + } else { + XCTFail("A-to-B handoff must emit one terminal update, never a delete") + } + + try moveHealthyRole( + from: secondWallet.name, + to: firstWallet.name + ) + wait( + for: [secondHandoffExpectation], + timeout: Constants.defaultExpectationDuration + ) + XCTAssertEqual(receivedBatches.count, 2) + XCTAssertEqual(receivedBatches[1].count, 1) + if case let .update(model)? = receivedBatches[1].first { + XCTAssertEqual(model.identifier, identifier) + XCTAssertEqual(model.info.name, firstWallet.name) + } else { + XCTFail("B-to-A handoff must emit one terminal update, never a delete") + } + + observable.removeObserver(observer) + processingQueue.sync {} + let stopExpectation = expectation(description: "handoff observable stops") + observable.stop { error in + XCTAssertNil(error) + stopExpectation.fulfill() + } + wait(for: [stopExpectation], timeout: Constants.defaultExpectationDuration) + } + func testRepositorySaveRejectsEquivalentChainAccountAliasesBeforeMutatingStoredWallet() throws { let facade = UserDataStorageTestFacade() let operationQueue = OperationQueue() From ceb0ed89d9092f64dead24a5482ee115a475c21b Mon Sep 17 00:00:00 2001 From: Makoto Takemiya Date: Sun, 26 Jul 2026 20:30:50 +0400 Subject: [PATCH 18/30] build: advance TestFlight hotfix to 2026.7.28 --- Jenkinsfile | 2 +- fearless.xcodeproj/project.pbxproj | 2 +- scripts/ci/audit-ios-release-identity.sh | 1 - scripts/ci/build-audited-ios-release-archive.sh | 6 +++--- scripts/test-coredata-simulator-rehearsal.sh | 8 ++++---- scripts/test-ios-release-identity-audit.sh | 4 ++-- scripts/test-ios-signed-release-artifact-audit.sh | 11 ++++++++--- 7 files changed, 19 insertions(+), 15 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 8a06ae63eb..7fba9342a5 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -134,7 +134,7 @@ try { echo "Exact Release gate requires dispatch to an arm64 macOS agent." >&2 exit 78 } - IOS_EXPECTED_BUILD_NUMBER=2026.7.27 \ + IOS_EXPECTED_BUILD_NUMBER=2026.7.28 \ IOS_RELEASE_SOURCE_PACKAGES_DIR="$PWD/SourcePackages" \ bash scripts/ci/audit-ios-release-identity.sh bash scripts/storage/audit-user-storage-compatibility-models.sh diff --git a/fearless.xcodeproj/project.pbxproj b/fearless.xcodeproj/project.pbxproj index 5ec9c3fdc3..fcb5a75b20 100644 --- a/fearless.xcodeproj/project.pbxproj +++ b/fearless.xcodeproj/project.pbxproj @@ -19547,7 +19547,7 @@ CODE_SIGN_ENTITLEMENTS = fearless/WalletConnect.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: Soramitsu Co., Ltd. (YLWWUD25VZ)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2026.7.27; + CURRENT_PROJECT_VERSION = 2026.7.28; DEVELOPMENT_TEAM = YLWWUD25VZ; ENABLE_BITCODE = NO; INFOPLIST_FILE = fearless/Info.plist; diff --git a/scripts/ci/audit-ios-release-identity.sh b/scripts/ci/audit-ios-release-identity.sh index f8ad80dfba..1285915e76 100755 --- a/scripts/ci/audit-ios-release-identity.sh +++ b/scripts/ci/audit-ios-release-identity.sh @@ -103,7 +103,6 @@ if [[ -z "$settings_json" ]]; then -destination "generic/platform=iOS" -showBuildSettings -json - "CURRENT_PROJECT_VERSION=$EXPECTED_BUILD" ) if [[ -n "${IOS_RELEASE_SOURCE_PACKAGES_DIR:-}" ]]; then diff --git a/scripts/ci/build-audited-ios-release-archive.sh b/scripts/ci/build-audited-ios-release-archive.sh index 6abcc6ca8f..2f8531c8f0 100644 --- a/scripts/ci/build-audited-ios-release-archive.sh +++ b/scripts/ci/build-audited-ios-release-archive.sh @@ -13,9 +13,9 @@ readonly EXPECTED_SIGNING_IDENTITY="Apple Distribution: Soramitsu Co., Ltd. (YLW readonly EXPECTED_SIGNING_CERTIFICATE_SHA1="84AB95335BE14CAE9B050A353910F86FF2F9539B" readonly EXPECTED_PROFILE_NAME="Fearless App Store 2026.7.26" readonly EXPECTED_PROFILE_UUID="0d51265e-4b53-4a1f-814a-436dc9ca087b" -# Read-only App Store Connect inspection on 2026-07-26 found 2026.7.26 as the +# Read-only App Store Connect inspection on 2026-07-26 found 2026.7.27 as the # newest 4.2.0 build. This candidate number was unused at inspection time. -readonly EXPECTED_BUILD="2026.7.27" +readonly EXPECTED_BUILD="2026.7.28" readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" readonly REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd -P)" @@ -33,7 +33,7 @@ Usage: Preconditions: - exact clean git HEAD, including no untracked files; - - App Store Connect read-only uniqueness check for 4.2.0 (2026.7.27); + - App Store Connect read-only uniqueness check for 4.2.0 (2026.7.28); - App Store distribution profile for the production App ID, with group.jp.co.soramitsu.fearlesswallet and Apple default keychain groups. diff --git a/scripts/test-coredata-simulator-rehearsal.sh b/scripts/test-coredata-simulator-rehearsal.sh index 2c9aefbaa1..10d30b26e5 100755 --- a/scripts/test-coredata-simulator-rehearsal.sh +++ b/scripts/test-coredata-simulator-rehearsal.sh @@ -230,7 +230,7 @@ prepare_case() { CFBundleShortVersionString 4.2.0 CFBundleVersion - 2026.7.27 + 2026.7.28 FearlessBuildConfiguration Release FearlessSwiftOptimizationLevel @@ -294,7 +294,7 @@ APP --app "$APP_DIR" --bundle-id "$BUNDLE_ID" --expected-git-sha 7a819cb01e92920e5444151392df54245acdd4c8 - --expected-build 2026.7.27 + --expected-build 2026.7.28 --expected-executable-sha256 "$executable_sha" --fixture-dir "$FIXTURE_DIR" --artifacts-dir "$ARTIFACTS_DIR" @@ -604,7 +604,7 @@ CASE_ARGS=( --app "$APP_DIR" --bundle-id "$BUNDLE_ID" --expected-git-sha 7a819cb01e92920e5444151392df54245acdd4c8 - --expected-build 2026.7.27 + --expected-build 2026.7.28 --expected-executable-sha256 "$(shasum -a 256 "$APP_DIR/fearless" | awk '{print $1}')" --fixture-dir "$FIXTURE_DIR" --artifacts-dir "$ARTIFACTS_DIR" @@ -722,7 +722,7 @@ CASE_ARGS=( --app "$APP_DIR" --bundle-id "$BUNDLE_ID" --expected-git-sha 7a819cb01e92920e5444151392df54245acdd4c8 - --expected-build 2026.7.27 + --expected-build 2026.7.28 --expected-executable-sha256 "$(shasum -a 256 "$APP_DIR/fearless" | awk '{print $1}')" --fixture-dir "$FIXTURE_DIR" --artifacts-dir "$ARTIFACTS_DIR" diff --git a/scripts/test-ios-release-identity-audit.sh b/scripts/test-ios-release-identity-audit.sh index 4545a9315b..eb39fdb5b9 100755 --- a/scripts/test-ios-release-identity-audit.sh +++ b/scripts/test-ios-release-identity-audit.sh @@ -41,7 +41,7 @@ jq -n '[ buildSettings: { PRODUCT_BUNDLE_IDENTIFIER: "jp.co.soramitsu.fearlesswallet", MARKETING_VERSION: "4.2.0", - CURRENT_PROJECT_VERSION: "2026.7.27", + CURRENT_PROJECT_VERSION: "2026.7.28", CODE_SIGN_ENTITLEMENTS: "fearless/WalletConnect.entitlements", CODE_SIGN_IDENTITY: "Apple Distribution: Soramitsu Co., Ltd. (YLWWUD25VZ)", CODE_SIGN_STYLE: "Manual", @@ -75,7 +75,7 @@ run_audit() { local debug_entitlements_file="${7:-$dev_entitlements}" IOS_RELEASE_AUDIT_TEST_HARNESS=1 \ - IOS_EXPECTED_BUILD_NUMBER=2026.7.27 \ + IOS_EXPECTED_BUILD_NUMBER=2026.7.28 \ IOS_RELEASE_SETTINGS_JSON="$1" \ IOS_DEBUG_SETTINGS_JSON="$debug_settings_file" \ IOS_RELEASE_SCHEME_FILE="$2" \ diff --git a/scripts/test-ios-signed-release-artifact-audit.sh b/scripts/test-ios-signed-release-artifact-audit.sh index a53c6e80be..0b4832b6f4 100755 --- a/scripts/test-ios-signed-release-artifact-audit.sh +++ b/scripts/test-ios-signed-release-artifact-audit.sh @@ -116,7 +116,7 @@ app = { "CFBundleExecutable": "fearless", "CFBundleIdentifier": bundle, "CFBundleShortVersionString": "4.2.0", - "CFBundleVersion": "2026.7.27", + "CFBundleVersion": "2026.7.28", "FearlessBuildConfiguration": "Release", "FearlessEnableTestability": "NO", "FearlessGitCommit": git_sha, @@ -172,7 +172,7 @@ run_audit() { bash "$AUDIT" \ --archive "$ARCHIVE" \ --expected-git-sha "${EXPECTED_GIT_OVERRIDE:-$EXPECTED_GIT_SHA}" \ - --expected-build 2026.7.27 \ + --expected-build 2026.7.28 \ --expected-executable-sha256 "${EXPECTED_EXECUTABLE_SHA_OVERRIDE:-$executable_sha}" \ --expected-archive-sha256 "${EXPECTED_ARCHIVE_SHA_OVERRIDE:-$archive_sha}" \ --expected-signing-certificate-sha1 "${EXPECTED_SIGNING_CERTIFICATE_SHA_OVERRIDE:-$signing_certificate_sha1}" \ @@ -239,6 +239,11 @@ assert_contains '"archiveTreeSHA256"' "$CASE_DIR/output/receipt.json" assert_contains '"distributionProfile": "valid-app-store"' "$CASE_DIR/output/receipt.json" printf '%s\n' "[ios-signed-release-audit-test] PASS: canonical signed archive" +prepare_case wrong-build +mutate_plist "$APP/Info.plist" \ + 'value["CFBundleVersion"] = "2026.7.27"' +expect_failure wrong-build "archived build number is not the expected fresh build" + prepare_case wrong-commit EXPECTED_GIT_OVERRIDE="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" expect_failure wrong-commit "embedded git commit" @@ -402,4 +407,4 @@ assert_contains "only in the explicit test harness" "$CASE_DIR/stderr" printf '%s\n' "[ios-signed-release-audit-test] PASS (rejected): override without harness" printf '%s\n' \ - "[ios-signed-release-audit-test] PASS: 1 positive + 32 negative/adversarial contracts" + "[ios-signed-release-audit-test] PASS: 1 positive + 33 negative/adversarial contracts" From 3f00f46db19ef9d7976854355dae9eb8fd576af8 Mon Sep 17 00:00:00 2001 From: Makoto Takemiya Date: Mon, 27 Jul 2026 00:28:05 +0400 Subject: [PATCH 19/30] ci: pin workflow actions for release safety --- .github/workflows/branch-flow.yml | 2 +- .github/workflows/codecov.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/branch-flow.yml b/.github/workflows/branch-flow.yml index ccfb351e13..3b454eb14e 100644 --- a/.github/workflows/branch-flow.yml +++ b/.github/workflows/branch-flow.yml @@ -12,7 +12,7 @@ jobs: timeout-minutes: 5 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Branch flow audit self-test run: bash scripts/test-branch-flow-audit.sh diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index b6e31958b2..00d57755c7 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -15,7 +15,7 @@ jobs: runs-on: macOS-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Branch flow audit run: | @@ -58,7 +58,7 @@ jobs: - name: Upload shared-features delta report if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: shared-features-delta-report path: build/reports/shared-features-delta-report.json From 9c89b26b5dd7c1f2bac362df753b43dd3286bbec Mon Sep 17 00:00:00 2001 From: Makoto Takemiya Date: Mon, 27 Jul 2026 01:39:31 +0400 Subject: [PATCH 20/30] fix: prohibit empty meta account codecs --- config/todo-debt-baseline.tsv | 2 - fearless.xcodeproj/project.pbxproj | 4 - .../CDAccountItem+CoreDataDecodable.swift | 14 - .../UserStorage/StorageMigrator+Sync.swift | 3 +- scripts/ci/run-coredata-release-gate.sh | 6 + .../audit-cdmetaaccount-codable-contract.sh | 261 ++++++++++++++++++ .../test-cdmetaaccount-codable-contract.sh | 147 ++++++++++ scripts/test-coredata-release-gate.sh | 6 + 8 files changed, 422 insertions(+), 21 deletions(-) delete mode 100644 fearless/Common/Extension/Storage/CDAccountItem+CoreDataDecodable.swift create mode 100755 scripts/storage/audit-cdmetaaccount-codable-contract.sh create mode 100755 scripts/storage/test-cdmetaaccount-codable-contract.sh diff --git a/config/todo-debt-baseline.tsv b/config/todo-debt-baseline.tsv index 5e71de1b0c..7c23a6060b 100644 --- a/config/todo-debt-baseline.tsv +++ b/config/todo-debt-baseline.tsv @@ -4,11 +4,9 @@ fearless/Common/Crypto/Data+Keccak.swift // TODO: move to library fearless/Common/Extension/Foundation/NSPredicate+Filter.swift // TODO: Remove fearless/Common/Extension/Foundation/NSPredicate+Validation.swift // TODO: Add deriviationPathHardSoftNumeric? fearless/Common/Extension/Foundation/NSPredicate+Validation.swift // TODO: Add deriviationPathHardSoftNumericPassword? -fearless/Common/Extension/Storage/CDAccountItem+CoreDataDecodable.swift // TODO: Fix logic fearless/Common/Services/ChainRegistry/RuntimeProviderPool/RuntimeHotBootSnapshotFactory.swift // TODO: think about it fearless/Common/Services/ChainRegistry/RuntimeProviderPool/RuntimeSnapshotOperationFactory.swift // TODO: think about it fearless/Common/Services/ExtrinsicService/ExtrinsicOperationFactory.swift method: "author_submitAndWatchExtrinsic", // TODO: add to fearless utils -fearless/Common/Storage/Migration/UserStorage/StorageMigrator+Sync.swift // TODO: Move this logic to app loading state fearless/Modules/AddAccount/Interactors/AddAccount+AccountConfirmInteractor.swift // TODO: Check how to convert this to chain account import fearless/Modules/Export/ExportMnemonic/ExportMnemonicPresenter.swift // TODO: Support custom accounts fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentPresenter.swift // TODO: - need think about this diff --git a/fearless.xcodeproj/project.pbxproj b/fearless.xcodeproj/project.pbxproj index fcb5a75b20..0b2b2c72eb 100644 --- a/fearless.xcodeproj/project.pbxproj +++ b/fearless.xcodeproj/project.pbxproj @@ -776,7 +776,6 @@ 8467FD4C24EEA081005D486C /* ProfileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8467FD4B24EEA081005D486C /* ProfileTests.swift */; }; 8467FD5324EFD210005D486C /* UserDataStorageFacade.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8467FD5224EFD210005D486C /* UserDataStorageFacade.swift */; }; 8467FD5524EFD254005D486C /* StorageFacadeProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8467FD5424EFD254005D486C /* StorageFacadeProtocol.swift */; }; - 8467FD5824EFD5C2005D486C /* CDAccountItem+CoreDataDecodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8467FD5724EFD5C2005D486C /* CDAccountItem+CoreDataDecodable.swift */; }; 8467FD5C24EFDCC9005D486C /* UserDataStorageTestFacade.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8467FD5B24EFDCC9005D486C /* UserDataStorageTestFacade.swift */; }; 846802A3265DA5530034F9B5 /* CrowdloanContributionSetupViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846802A2265DA5530034F9B5 /* CrowdloanContributionSetupViewModel.swift */; }; 8468B86524F59D1D00B76BC6 /* IconTitleHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8468B86424F59D1D00B76BC6 /* IconTitleHeaderView.swift */; }; @@ -3915,7 +3914,6 @@ 8467FD4B24EEA081005D486C /* ProfileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileTests.swift; sourceTree = ""; }; 8467FD5224EFD210005D486C /* UserDataStorageFacade.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDataStorageFacade.swift; sourceTree = ""; }; 8467FD5424EFD254005D486C /* StorageFacadeProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageFacadeProtocol.swift; sourceTree = ""; }; - 8467FD5724EFD5C2005D486C /* CDAccountItem+CoreDataDecodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CDAccountItem+CoreDataDecodable.swift"; sourceTree = ""; }; 8467FD5B24EFDCC9005D486C /* UserDataStorageTestFacade.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDataStorageTestFacade.swift; sourceTree = ""; }; 846802A2265DA5530034F9B5 /* CrowdloanContributionSetupViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrowdloanContributionSetupViewModel.swift; sourceTree = ""; }; 8468B86424F59D1D00B76BC6 /* IconTitleHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconTitleHeaderView.swift; sourceTree = ""; }; @@ -8869,7 +8867,6 @@ isa = PBXGroup; children = ( FA74359629C0734B0085A47E /* CDAccountInfo+CoreDataCodable.swift */, - 8467FD5724EFD5C2005D486C /* CDAccountItem+CoreDataDecodable.swift */, 84EBC54224F656D100459D15 /* SortDescriptor+Storage.swift */, 843910B1253ED4D100E3C217 /* CDChainStorageItem+CoreDataDecodable.swift */, 843910B5253EE62B00E3C217 /* DataProviderChange+Result.swift */, @@ -16676,7 +16673,6 @@ FAA0139D28DA131B000A5230 /* StakingBondMorePoolViewModelState.swift in Sources */, FA286B0F2A3043DB008BD527 /* CrossChainConfirmationData.swift in Sources */, 8467FD0224E5D2D9005D486C /* OnboardingMainInteractor.swift in Sources */, - 8467FD5824EFD5C2005D486C /* CDAccountItem+CoreDataDecodable.swift in Sources */, 84939B1826E1690D000111DA /* TreasuryDepositEvent.swift in Sources */, 844CB56426F945E200396E13 /* SubstrateLocalSubscriptionFactory.swift in Sources */, FA34EE9C2B9871420042E73E /* StakingLocks.swift in Sources */, diff --git a/fearless/Common/Extension/Storage/CDAccountItem+CoreDataDecodable.swift b/fearless/Common/Extension/Storage/CDAccountItem+CoreDataDecodable.swift deleted file mode 100644 index ade33a1b40..0000000000 --- a/fearless/Common/Extension/Storage/CDAccountItem+CoreDataDecodable.swift +++ /dev/null @@ -1,14 +0,0 @@ -#if canImport(SSFAssetManagmentStorage) - import Foundation - import CoreData - import RobinHood - import IrohaCrypto - import SSFAssetManagmentStorage - - // TODO: Fix logic - extension CDMetaAccount: CoreDataCodable { - public func populate(from _: Decoder, using _: NSManagedObjectContext) throws {} - - public func encode(to _: Encoder) throws {} - } -#endif diff --git a/fearless/Common/Storage/Migration/UserStorage/StorageMigrator+Sync.swift b/fearless/Common/Storage/Migration/UserStorage/StorageMigrator+Sync.swift index cdce3919a3..5b90e07ccd 100644 --- a/fearless/Common/Storage/Migration/UserStorage/StorageMigrator+Sync.swift +++ b/fearless/Common/Storage/Migration/UserStorage/StorageMigrator+Sync.swift @@ -1,6 +1,7 @@ import Foundation -// TODO: Move this logic to app loading state +// RootInteractor runs this synchronous adapter on its single-worker migration +// queue. Successful return opens the startup barrier for storage preflight. extension UserStorageMigrator: Migrating { func migrate() throws { let migrationPerformed = try performMigration() diff --git a/scripts/ci/run-coredata-release-gate.sh b/scripts/ci/run-coredata-release-gate.sh index e8054fbe34..193f54ec2f 100644 --- a/scripts/ci/run-coredata-release-gate.sh +++ b/scripts/ci/run-coredata-release-gate.sh @@ -19,6 +19,7 @@ readonly LOG_PREFIX="[coredata-release-gate]" readonly SCHEME="fearless.tests" readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" readonly MANIFEST_DIR="$SCRIPT_DIR/manifests" +readonly CDMETAACCOUNT_CODABLE_AUDIT="$SCRIPT_DIR/../storage/audit-cdmetaaccount-codable-contract.sh" readonly UUID_PATTERN='^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$' readonly SIMULATOR_DESTINATION_PATTERN='^platform=iOS Simulator,id=([0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12})(,arch=(arm64|x86_64))?$' SIMULATOR_INVENTORY_FILE="" @@ -685,6 +686,11 @@ main() { if [[ "$stage" == "copied-phone" || "$stage" == "all" ]]; then require_executable "$SHASUM_BIN" "shasum" fi + [[ -f "$CDMETAACCOUNT_CODABLE_AUDIT" && ! -L "$CDMETAACCOUNT_CODABLE_AUDIT" ]] || + fail "CDMetaAccount Codable contract audit is missing or unsafe" + FEARLESS_CDMETAACCOUNT_CODABLE_ROOT="$ROOT_DIR" \ + FEARLESS_CDMETAACCOUNT_CODABLE_PYTHON_BIN="$PYTHON_BIN" \ + bash "$CDMETAACCOUNT_CODABLE_AUDIT" SIMULATOR_INVENTORY_FILE="$( mktemp "${TMPDIR:-/private/tmp}/fearless-coredata-simulators.XXXXXX" diff --git a/scripts/storage/audit-cdmetaaccount-codable-contract.sh b/scripts/storage/audit-cdmetaaccount-codable-contract.sh new file mode 100755 index 0000000000..4fda28bb6f --- /dev/null +++ b/scripts/storage/audit-cdmetaaccount-codable-contract.sh @@ -0,0 +1,261 @@ +#!/usr/bin/env bash +set -euo pipefail + +readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +readonly ROOT_DIR="${FEARLESS_CDMETAACCOUNT_CODABLE_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd -P)}" +readonly SOURCE_DIR="$ROOT_DIR/fearless" +readonly PYTHON_BIN="${FEARLESS_CDMETAACCOUNT_CODABLE_PYTHON_BIN:-python3}" +readonly LOG_PREFIX="[cdmetaaccount-codable-audit]" + +fail() { + printf '%s ERROR: %s\n' "$LOG_PREFIX" "$*" >&2 + exit 1 +} + +if [[ "$PYTHON_BIN" == */* ]]; then + [[ -x "$PYTHON_BIN" ]] || fail "python interpreter is not executable" +else + command -v "$PYTHON_BIN" >/dev/null 2>&1 || + fail "python3 is required" +fi + +[[ -d "$SOURCE_DIR" && ! -L "$SOURCE_DIR" ]] || + fail "fearless source directory is missing or unsafe" + +"$PYTHON_BIN" - "$SOURCE_DIR" <<'PY' +import bisect +import os +import re +import sys +from dataclasses import dataclass +from pathlib import Path +from typing import Optional + +source_dir = Path(sys.argv[1]).resolve() +token_pattern = re.compile(r"[A-Za-z_][A-Za-z0-9_]*|[<>{}:,.()\[\]]") +declaration_keywords = {"extension", "class", "struct", "enum", "actor"} + + +@dataclass(frozen=True) +class Token: + value: str + offset: int + + +def mask_comments(source: str) -> str: + """Remove line and nested block comments while preserving source offsets.""" + masked = list(source) + index = 0 + size = len(source) + + def blank(start: int, end: int) -> None: + for position in range(start, end): + if masked[position] not in "\r\n": + masked[position] = " " + + while index < size: + if source.startswith("//", index): + end = source.find("\n", index + 2) + if end == -1: + end = size + blank(index, end) + index = end + continue + + if source.startswith("/*", index): + start = index + depth = 1 + index += 2 + while index < size and depth: + if source.startswith("/*", index): + depth += 1 + index += 2 + elif source.startswith("*/", index): + depth -= 1 + index += 2 + else: + index += 1 + blank(start, index) + continue + + index += 1 + + return "".join(masked) + + +def line_number(line_offsets: list[int], offset: int) -> int: + return bisect.bisect_right(line_offsets, offset) + + +def direct_conformance(tokens: list[Token], start: int) -> Optional[Token]: + angle_depth = 0 + top_level_identifiers: list[Token] = [] + colon_index = None + + for index in range(start + 1, len(tokens)): + token = tokens[index] + if token.value == "<": + angle_depth += 1 + elif token.value == ">": + angle_depth = max(0, angle_depth - 1) + elif angle_depth == 0 and token.value in {"{", "where"}: + break + elif angle_depth == 0 and token.value == ":": + colon_index = index + break + elif angle_depth == 0 and token.value[0].isalpha(): + top_level_identifiers.append(token) + + if colon_index is None or not top_level_identifiers: + return None + if top_level_identifiers[-1].value != "CDMetaAccount": + return None + + angle_depth = 0 + for token in tokens[colon_index + 1:]: + if token.value == "<": + angle_depth += 1 + elif token.value == ">": + angle_depth = max(0, angle_depth - 1) + elif angle_depth == 0 and token.value in {"{", "where"}: + break + elif token.value == "CoreDataCodable": + return token + + return None + + +def mapper_second_argument(tokens: list[Token], start: int) -> Optional[Token]: + if start + 1 >= len(tokens) or tokens[start + 1].value != "<": + return None + + angle_depth = 1 + parenthesis_depth = 0 + bracket_depth = 0 + argument_index = 0 + + for token in tokens[start + 2:]: + if token.value == "<": + angle_depth += 1 + elif token.value == ">": + angle_depth -= 1 + if angle_depth == 0: + break + elif token.value == "(": + parenthesis_depth += 1 + elif token.value == ")": + parenthesis_depth = max(0, parenthesis_depth - 1) + elif token.value == "[": + bracket_depth += 1 + elif token.value == "]": + bracket_depth = max(0, bracket_depth - 1) + elif ( + token.value == "," + and angle_depth == 1 + and parenthesis_depth == 0 + and bracket_depth == 0 + ): + argument_index += 1 + continue + + if argument_index == 1 and token.value == "CDMetaAccount": + return token + + return None + + +violations: list[tuple[str, int, str]] = [] +swift_files: list[Path] = [] + +for directory, directory_names, file_names in os.walk(source_dir, followlinks=False): + directory_names.sort() + file_names.sort() + for directory_name in directory_names: + candidate = Path(directory, directory_name) + if candidate.is_symlink(): + relative = candidate.relative_to(source_dir) + print( + f"[cdmetaaccount-codable-audit] ERROR: " + f"symlinked source directory is forbidden: fearless/{relative}", + file=sys.stderr, + ) + raise SystemExit(1) + for file_name in file_names: + if not file_name.endswith(".swift"): + continue + candidate = Path(directory, file_name) + if candidate.is_symlink() or not candidate.is_file(): + relative = candidate.relative_to(source_dir) + print( + f"[cdmetaaccount-codable-audit] ERROR: " + f"unsafe Swift source is forbidden: fearless/{relative}", + file=sys.stderr, + ) + raise SystemExit(1) + swift_files.append(candidate) + +if not swift_files: + print( + "[cdmetaaccount-codable-audit] ERROR: no Swift source files were found", + file=sys.stderr, + ) + raise SystemExit(1) + +for path in swift_files: + try: + source = path.read_text(encoding="utf-8") + except (OSError, UnicodeError) as error: + print( + f"[cdmetaaccount-codable-audit] ERROR: unreadable Swift source " + f"{path}: {error}", + file=sys.stderr, + ) + raise SystemExit(1) + + code = mask_comments(source) + tokens = [ + Token(match.group(0), match.start()) + for match in token_pattern.finditer(code) + ] + line_offsets = [0] + line_offsets.extend( + index + 1 + for index, character in enumerate(source) + if character == "\n" + ) + relative = f"fearless/{path.relative_to(source_dir)}" + + for index, token in enumerate(tokens): + if token.value in declaration_keywords: + violation = direct_conformance(tokens, index) + if violation is not None: + violations.append( + ( + relative, + line_number(line_offsets, violation.offset), + "CDMetaAccount must not directly conform to CoreDataCodable", + ) + ) + elif token.value == "CodableCoreDataMapper": + violation = mapper_second_argument(tokens, index) + if violation is not None: + violations.append( + ( + relative, + line_number(line_offsets, violation.offset), + "CodableCoreDataMapper must not target CDMetaAccount", + ) + ) + +if violations: + for path, line, message in sorted(set(violations)): + print( + f"[cdmetaaccount-codable-audit] ERROR: {path}:{line}: {message}", + file=sys.stderr, + ) + raise SystemExit(1) + +print( + "[cdmetaaccount-codable-audit] PASS: explicit MetaAccountMapper boundary preserved" +) +PY diff --git a/scripts/storage/test-cdmetaaccount-codable-contract.sh b/scripts/storage/test-cdmetaaccount-codable-contract.sh new file mode 100755 index 0000000000..4a774ef4dd --- /dev/null +++ b/scripts/storage/test-cdmetaaccount-codable-contract.sh @@ -0,0 +1,147 @@ +#!/usr/bin/env bash +set -euo pipefail + +readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +readonly AUDIT_SCRIPT="$SCRIPT_DIR/audit-cdmetaaccount-codable-contract.sh" +readonly TEMPORARY_DIR="$( + mktemp -d "${TMPDIR:-/private/tmp}/fearless-cdmetaaccount-codable-tests.XXXXXX" +)" +trap 'rm -rf "$TEMPORARY_DIR"' EXIT + +fail() { + printf '%s ERROR: %s\n' "[cdmetaaccount-codable-audit-test]" "$*" >&2 + exit 1 +} + +new_fixture() { + local name="$1" + local fixture="$TEMPORARY_DIR/$name" + mkdir -p "$fixture/fearless" + printf '%s\n' "$fixture" +} + +run_audit() { + local fixture="$1" + FEARLESS_CDMETAACCOUNT_CODABLE_ROOT="$fixture" \ + bash "$AUDIT_SCRIPT" 2>&1 +} + +expect_success() { + local label="$1" + local fixture="$2" + local output + + if ! output="$(run_audit "$fixture")"; then + fail "$label unexpectedly failed: $output" + fi + [[ "$output" == *"PASS: explicit MetaAccountMapper boundary preserved"* ]] || + fail "$label did not emit the success contract: $output" + printf '%s\n' "[cdmetaaccount-codable-audit-test] PASS: $label" +} + +expect_failure() { + local label="$1" + local expected_message="$2" + local fixture="$3" + local output + local status + + set +e + output="$(run_audit "$fixture")" + status=$? + set -e + + [[ "$status" -ne 0 ]] || fail "$label unexpectedly passed" + [[ "$output" == *"$expected_message"* ]] || + fail "$label failed without '$expected_message': $output" + printf '%s\n' "[cdmetaaccount-codable-audit-test] PASS (rejected): $label" +} + +bash "$AUDIT_SCRIPT" >/dev/null +printf '%s\n' "[cdmetaaccount-codable-audit-test] PASS: canonical source" + +[[ -x /usr/bin/python3 ]] || + fail "macOS system Python is required for the compatibility self-test" +FEARLESS_CDMETAACCOUNT_CODABLE_PYTHON_BIN=/usr/bin/python3 \ + bash "$AUDIT_SCRIPT" >/dev/null +printf '%s\n' "[cdmetaaccount-codable-audit-test] PASS: macOS system Python" + +fixture="$(new_fixture safe-boundaries)" +cat >"$fixture/fearless/Safe.swift" <<'SWIFT' +struct CDMetaAccountSnapshot: CoreDataCodable {} +let mapper: CodableCoreDataMapper + +// extension CDMetaAccount: CoreDataCodable {} +/* + let dormantExample: CodableCoreDataMapper + */ +SWIFT +expect_success "token boundaries and comments" "$fixture" + +fixture="$(new_fixture direct-extension)" +cat >"$fixture/fearless/Dangerous.swift" <<'SWIFT' +extension CDMetaAccount: CoreDataCodable {} +SWIFT +expect_failure \ + "direct empty-codec extension" \ + "CDMetaAccount must not directly conform to CoreDataCodable" \ + "$fixture" + +fixture="$(new_fixture adversarial-extension)" +cat >"$fixture/fearless/Dangerous.swift" <<'SWIFT' +extension + SSFAccountManagmentStorage + .CDMetaAccount + : Sendable, + /* whitespace and comments must not hide the conformance */ + CoreDataCodable +{} +SWIFT +expect_failure \ + "qualified multiline conformance" \ + "CDMetaAccount must not directly conform to CoreDataCodable" \ + "$fixture" + +fixture="$(new_fixture direct-class-conformance)" +cat >"$fixture/fearless/Dangerous.swift" <<'SWIFT' +final class CDMetaAccount: NSManagedObject, CoreDataCodable {} +SWIFT +expect_failure \ + "direct class conformance" \ + "CDMetaAccount must not directly conform to CoreDataCodable" \ + "$fixture" + +fixture="$(new_fixture direct-codable-mapper)" +cat >"$fixture/fearless/Dangerous.swift" <<'SWIFT' +let mapper: CodableCoreDataMapper +SWIFT +expect_failure \ + "direct CodableCoreDataMapper" \ + "CodableCoreDataMapper must not target CDMetaAccount" \ + "$fixture" + +fixture="$(new_fixture adversarial-codable-mapper)" +cat >"$fixture/fearless/Dangerous.swift" <<'SWIFT' +let mapper: RobinHood.CodableCoreDataMapper< + Dictionary, + /* the explicit wallet mapper remains mandatory */ + SSFAccountManagmentStorage + .CDMetaAccount +> +SWIFT +expect_failure \ + "qualified multiline nested-generic CodableCoreDataMapper" \ + "CodableCoreDataMapper must not target CDMetaAccount" \ + "$fixture" + +fixture="$(new_fixture wrapped-codable-mapper)" +cat >"$fixture/fearless/Dangerous.swift" <<'SWIFT' +let mapper: CodableCoreDataMapper> +SWIFT +expect_failure \ + "wrapped CDMetaAccount CodableCoreDataMapper" \ + "CodableCoreDataMapper must not target CDMetaAccount" \ + "$fixture" + +printf '%s\n' \ + "[cdmetaaccount-codable-audit-test] PASS: 3 positive + 6 negative/adversarial cases" diff --git a/scripts/test-coredata-release-gate.sh b/scripts/test-coredata-release-gate.sh index 8f87dec30d..4cc9dd5057 100644 --- a/scripts/test-coredata-release-gate.sh +++ b/scripts/test-coredata-release-gate.sh @@ -3,6 +3,7 @@ set -euo pipefail readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" readonly GATE="$SCRIPT_DIR/ci/run-coredata-release-gate.sh" +readonly CDMETAACCOUNT_CODABLE_TEST="$SCRIPT_DIR/storage/test-cdmetaaccount-codable-contract.sh" readonly TEMPORARY_DIR="$(mktemp -d "${TMPDIR:-/private/tmp}/fearless-coredata-gate-tests.XXXXXX")" trap 'rm -rf "$TEMPORARY_DIR"' EXIT @@ -75,11 +76,14 @@ expect_failure() { } mkdir -p \ + "$FIXTURE_ROOT/fearless/Common/Storage" \ "$FIXTURE_ROOT/fearless.xcworkspace" \ "$FIXTURE_ROOT/SourcePackages/checkouts" \ "$BIN_DIR" printf '%s\n' '' \ >"$FIXTURE_ROOT/fearless.xcworkspace/contents.xcworkspacedata" +printf '%s\n' 'struct SafeCoreDataReleaseFixture {}' \ + >"$FIXTURE_ROOT/fearless/Common/Storage/SafeCoreDataReleaseFixture.swift" printf '%s\n' 'opaque copied-store fixture; never interpreted by this shell test' >"$PHONE_FIXTURE" printf '%s\n' 'opaque write-ahead log' >"${PHONE_FIXTURE}-wal" printf '%s\n' 'opaque shared-memory sidecar' >"${PHONE_FIXTURE}-shm" @@ -265,6 +269,8 @@ STUB chmod +x "$BIN_DIR/xcodebuild" "$BIN_DIR/xcrun" +bash "$CDMETAACCOUNT_CODABLE_TEST" + CANONICAL_WORKSPACE="$(cd "$FIXTURE_ROOT/fearless.xcworkspace" && pwd -P)" CANONICAL_SOURCE_PACKAGES="$(cd "$FIXTURE_ROOT/SourcePackages" && pwd -P)" From 838840b35e898abc6b2e10602c5cd100b358fc90 Mon Sep 17 00:00:00 2001 From: Makoto Takemiya Date: Mon, 27 Jul 2026 01:39:37 +0400 Subject: [PATCH 21/30] test: isolate account export operations --- .../AccountExportPasswordTests.swift | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/fearlessTests/Modules/AccountExportPassword/AccountExportPasswordTests.swift b/fearlessTests/Modules/AccountExportPassword/AccountExportPasswordTests.swift index 063cc07bee..ba458041b6 100644 --- a/fearlessTests/Modules/AccountExportPassword/AccountExportPasswordTests.swift +++ b/fearlessTests/Modules/AccountExportPassword/AccountExportPasswordTests.swift @@ -32,17 +32,23 @@ class AccountExportPasswordTests: XCTestCase { let presenter = AccountExportPasswordPresenter( flow: .single(chain: chain, address: walletAddress, wallet: wallet), - localizationManager: LocalizationManager.shared) + localizationManager: LocalizationManager.shared + ) presenter.view = view presenter.wireframe = wireframe let exportWrapper = KeystoreExportWrapper(keystore: keychain) - let interactor = AccountExportPasswordInteractor(exportJsonWrapper: exportWrapper, - accountRepository: AnyDataProviderRepository(accountsRepository), - operationManager: OperationManagerFacade.sharedManager, - extrinsicOperationFactory: nil, - chainRepository: AnyDataProviderRepository(chainRepository)) + let exportOperationQueue = OperationQueue() + exportOperationQueue.qualityOfService = .userInitiated + let exportOperationManager = OperationManager(operationQueue: exportOperationQueue) + let interactor = AccountExportPasswordInteractor( + exportJsonWrapper: exportWrapper, + accountRepository: AnyDataProviderRepository(accountsRepository), + operationManager: exportOperationManager, + extrinsicOperationFactory: nil, + chainRepository: AnyDataProviderRepository(chainRepository) + ) presenter.interactor = interactor interactor.presenter = presenter @@ -63,7 +69,7 @@ class AccountExportPasswordTests: XCTestCase { when(stub.set(error: any(AccountExportPasswordError.self))).thenDoNothing() } - let expectation = XCTestExpectation() + let expectation = expectation(description: "JSON export is presented") stub(wireframe) { stub in when(stub.present(viewModel: any(SheetAlertPresentableViewModel.self), from: any(ControllerBackedProtocol?.self))).thenDoNothing() @@ -90,6 +96,6 @@ class AccountExportPasswordTests: XCTestCase { // then - wait(for: [expectation], timeout: Constants.defaultExpectationDuration) + wait(for: [expectation], timeout: 15) } } From f7e44e69944ecfbedd31342aa44a0740c33564ef Mon Sep 17 00:00:00 2001 From: Makoto Takemiya Date: Mon, 27 Jul 2026 01:39:41 +0400 Subject: [PATCH 22/30] ci: use pinned Codecov uploader with OIDC --- .github/workflows/codecov.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 00d57755c7..2977050f19 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -13,6 +13,9 @@ on: jobs: build: runs-on: macOS-latest + permissions: + contents: read + id-token: write steps: - name: Checkout uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -168,7 +171,13 @@ jobs: fi - name: Upload coverage to Codecov - run: bash <(curl -s https://codecov.io/bash) -J 'fearless' + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 + with: + fail_ci_if_error: true + plugins: xcode + swift_project: fearless + use_oidc: true + version: v11.3.1 jenkins-pr-merge: name: continuous-integration/jenkins/pr-merge From 10164c0406b0b72b6620670cbc8168b41e99a9fe Mon Sep 17 00:00:00 2001 From: Makoto Takemiya Date: Mon, 27 Jul 2026 02:22:23 +0400 Subject: [PATCH 23/30] ci: generate executable iOS coverage data --- .github/workflows/codecov.yml | 26 ++++++++ scripts/audit-codecov-coverage-contract.sh | 36 ++++++++++ scripts/test-codecov-coverage-contract.sh | 76 ++++++++++++++++++++++ 3 files changed, 138 insertions(+) create mode 100755 scripts/audit-codecov-coverage-contract.sh create mode 100755 scripts/test-codecov-coverage-contract.sh diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 2977050f19..4030e8f327 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -19,6 +19,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false - name: Branch flow audit run: | @@ -38,6 +40,11 @@ jobs: bash ./scripts/test-testflight-publication-readiness-audit.sh bash ./scripts/audit-testflight-publication-readiness.sh + - name: Codecov executable coverage contract + run: | + bash ./scripts/test-codecov-coverage-contract.sh + bash ./scripts/audit-codecov-coverage-contract.sh + - name: Iroha mobile SDK release asset contract env: GH_TOKEN: ${{ github.token }} @@ -153,6 +160,7 @@ jobs: esac echo "Using destination: $DESTINATION" LOG_FILE="$PWD/build/codecov-xcodebuild.log" + RESULT_BUNDLE="$PWD/build/CodecovTests.xcresult" mkdir -p "$PWD/build" if ! xcodebuild \ -workspace fearless.xcworkspace \ @@ -160,6 +168,8 @@ jobs: -clonedSourcePackagesDirPath "$SP_DIR" \ -disableAutomaticPackageResolution \ -destination "$DESTINATION" \ + -enableCodeCoverage YES \ + -resultBundlePath "$RESULT_BUNDLE" \ build test | tee "$LOG_FILE" | xcpretty --test; then echo "Build/Test failed. Key errors from $LOG_FILE:" >&2 if command -v rg >/dev/null 2>&1; then @@ -169,6 +179,22 @@ jobs: fi exit 1 fi + COVERAGE_PROFILE="$( + find "$HOME/Library/Developer/Xcode/DerivedData" \ + -type f -name '*.profdata' -print -quit + )" + if [[ -z "$COVERAGE_PROFILE" ]]; then + echo "Tests passed but Xcode produced no coverage profile." >&2 + exit 1 + fi + xcrun llvm-profdata show "$COVERAGE_PROFILE" >/dev/null || { + echo "Xcode produced an unreadable coverage profile: $COVERAGE_PROFILE" >&2 + exit 1 + } + test -d "$RESULT_BUNDLE" || { + echo "Xcode did not produce the required result bundle." >&2 + exit 1 + } - name: Upload coverage to Codecov uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 diff --git a/scripts/audit-codecov-coverage-contract.sh b/scripts/audit-codecov-coverage-contract.sh new file mode 100755 index 0000000000..487b226ccc --- /dev/null +++ b/scripts/audit-codecov-coverage-contract.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +WORKFLOW="${1:-$ROOT_DIR/.github/workflows/codecov.yml}" + +fail() { + echo "[codecov-coverage-contract][error] $*" >&2 + exit 1 +} + +[[ -f "$WORKFLOW" && ! -L "$WORKFLOW" ]] || + fail "Codecov workflow must be a regular, non-symlink file." + +require_text() { + local text="$1" + grep -Fq -- "$text" "$WORKFLOW" || + fail "Codecov workflow is missing required coverage control: $text" +} + +require_text "persist-credentials: false" +require_text "-enableCodeCoverage YES" +require_text '-resultBundlePath "$RESULT_BUNDLE"' +require_text "-type f -name '*.profdata' -print -quit" +require_text 'xcrun llvm-profdata show "$COVERAGE_PROFILE"' +require_text 'test -d "$RESULT_BUNDLE"' +require_text "plugins: xcode" +require_text "swift_project: fearless" +require_text "fail_ci_if_error: true" +require_text "use_oidc: true" + +if grep -Eq 'codecov/codecov-action@(main|master|v[0-9]+)[[:space:]#]' "$WORKFLOW"; then + fail "Codecov action must use an immutable full commit SHA." +fi + +echo "[codecov-coverage-contract] executable coverage and upload contract passed" diff --git a/scripts/test-codecov-coverage-contract.sh b/scripts/test-codecov-coverage-contract.sh new file mode 100755 index 0000000000..0dd04e9ad8 --- /dev/null +++ b/scripts/test-codecov-coverage-contract.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +AUDIT="$ROOT_DIR/scripts/audit-codecov-coverage-contract.sh" +SOURCE="$ROOT_DIR/.github/workflows/codecov.yml" +mkdir -p "$ROOT_DIR/build/test-tmp" +tmp_dir="$(mktemp -d "$ROOT_DIR/build/test-tmp/codecov-coverage-contract.XXXXXX")" +trap 'rm -rf "$tmp_dir"' EXIT + +fail() { + echo "[codecov-coverage-contract-test][error] $*" >&2 + exit 1 +} + +expect_rejected() { + local label="$1" + local workflow="$tmp_dir/$negative_count.yml" + local output="$tmp_dir/$negative_count.log" + cp "$SOURCE" "$workflow" + shift + "$@" "$workflow" + if "$AUDIT" "$workflow" > "$output" 2>&1; then + fail "$label unexpectedly passed" + fi + negative_count=$((negative_count + 1)) +} + +delete_text() { + local text="$1" + local workflow="$2" + TEXT="$text" perl -0pi -e 's/\Q$ENV{TEXT}\E//' "$workflow" +} + +replace_text() { + local from="$1" + local to="$2" + local workflow="$3" + FROM="$from" TO="$to" perl -0pi -e \ + 's/\Q$ENV{FROM}\E/$ENV{TO}/ or die "fixture mutation missed\n"' \ + "$workflow" +} + +"$AUDIT" "$SOURCE" >/dev/null +positive_count=1 +negative_count=0 + +expect_rejected \ + "coverage generation removed" \ + delete_text "-enableCodeCoverage YES" +expect_rejected \ + "result bundle removed" \ + delete_text '-resultBundlePath "$RESULT_BUNDLE"' +expect_rejected \ + "profdata fail-closed check removed" \ + delete_text "-type f -name '*.profdata' -print -quit" +expect_rejected \ + "profdata readability check removed" \ + delete_text 'xcrun llvm-profdata show "$COVERAGE_PROFILE"' +expect_rejected \ + "Codecov upload made non-blocking" \ + replace_text "fail_ci_if_error: true" "fail_ci_if_error: false" +expect_rejected \ + "Codecov OIDC removed" \ + replace_text "use_oidc: true" "use_oidc: false" +expect_rejected \ + "mutable Codecov action reference" \ + replace_text \ + "codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f" \ + "codecov/codecov-action@v7" + +[[ "$positive_count" == "1" && "$negative_count" == "7" ]] || + fail "unexpected test counts: $positive_count positive, $negative_count negative" + +echo \ + "[codecov-coverage-contract-test] $positive_count positive + $negative_count adversarial cases passed" From ef07cca1cfea131d201656e29d1f91fc84d259c0 Mon Sep 17 00:00:00 2001 From: Makoto Takemiya Date: Thu, 30 Jul 2026 12:44:42 +0400 Subject: [PATCH 24/30] chore: scope migration lint suppressions --- .../Helpers/AccountProviderFactory.swift | 3 +- .../ChainRegistry/ChainRegistry.swift | 3 ++ .../ChainRegistry/ChainSyncService.swift | 12 ++++--- .../EntityToModel/ChainModelMapper.swift | 12 +++++-- .../EntityToModel/MetaAccountMapper.swift | 2 ++ .../Storage/Migration/KeystoreMigrator.swift | 3 ++ .../SubstrateStorageMigrator.swift | 31 +++++++++++++++++-- .../UserStorage/StorageMigrator.swift | 16 +++++++++- ...ersistentStoreCoordinator+Extensions.swift | 11 +++++-- .../Storage/UserDataStorageFacade.swift | 10 +++--- fearless/Modules/Root/RootInteractor.swift | 2 ++ 11 files changed, 86 insertions(+), 19 deletions(-) diff --git a/fearless/Common/Helpers/AccountProviderFactory.swift b/fearless/Common/Helpers/AccountProviderFactory.swift index ff805ea56c..21cefe5257 100644 --- a/fearless/Common/Helpers/AccountProviderFactory.swift +++ b/fearless/Common/Helpers/AccountProviderFactory.swift @@ -123,7 +123,8 @@ final class AccountProviderFactory: AccountProviderFactoryProtocol { /// successfully mapped rows by object ID makes deletes unambiguous. Aggregating every notification /// by persistent identifier also makes same-identifier object handoffs independent of Core Data's /// unordered notification sets. -final class TolerantMetaAccountContextObservable: +/// Row reconciliation stays in one auditable state machine. +final class TolerantMetaAccountContextObservable: // swiftlint:disable:this type_body_length DataProviderRepositoryObservable { private struct MappedObject { let objectId: NSManagedObjectID diff --git a/fearless/Common/Services/ChainRegistry/ChainRegistry.swift b/fearless/Common/Services/ChainRegistry/ChainRegistry.swift index 5fd143d037..3cc0850b08 100644 --- a/fearless/Common/Services/ChainRegistry/ChainRegistry.swift +++ b/fearless/Common/Services/ChainRegistry/ChainRegistry.swift @@ -1073,3 +1073,6 @@ extension ChainRegistry: SSFChainRegistry.ChainRegistryProtocol { return runtimeSnapshot } } + +// Reviewed registry transport hardening remains co-located. +// swiftlint:disable:this file_length diff --git a/fearless/Common/Services/ChainRegistry/ChainSyncService.swift b/fearless/Common/Services/ChainRegistry/ChainSyncService.swift index 434425f2ad..0f8aa6b139 100644 --- a/fearless/Common/Services/ChainRegistry/ChainSyncService.swift +++ b/fearless/Common/Services/ChainRegistry/ChainSyncService.swift @@ -17,6 +17,8 @@ enum ChainSyncServiceError: Error { case duplicateRemoteChainIdentifier(String) } +// Sync validation stays with the state it guards. +// swiftlint:disable:next type_body_length final class ChainSyncService { static let fetchLocalData = false static let historyExplorerCompatibilityType = "subsquid" @@ -215,9 +217,9 @@ final class ChainSyncService { let obj = try JSONSerialization.jsonObject(with: data, options: []) guard var array = obj as? [[String: Any]] else { return data } - for i in 0 ..< array.count { - if array[i]["properties"] == nil { - let prefixValue = array[i]["addressPrefix"] + for index in 0 ..< array.count { + if array[index]["properties"] == nil { + let prefixValue = array[index]["addressPrefix"] let prefixString: String if let intValue = prefixValue as? Int { @@ -230,10 +232,10 @@ final class ChainSyncService { prefixString = "0" } - array[i]["properties"] = ["addressPrefix": prefixString] + array[index]["properties"] = ["addressPrefix": prefixString] } - normalizeBlockExplorerTypes(in: &array[i]) + normalizeBlockExplorerTypes(in: &array[index]) } return try JSONSerialization.data(withJSONObject: array, options: []) diff --git a/fearless/Common/Storage/EntityToModel/ChainModelMapper.swift b/fearless/Common/Storage/EntityToModel/ChainModelMapper.swift index 48702596b1..43088ce03a 100644 --- a/fearless/Common/Storage/EntityToModel/ChainModelMapper.swift +++ b/fearless/Common/Storage/EntityToModel/ChainModelMapper.swift @@ -24,6 +24,8 @@ enum ChainModelMapperError: LocalizedError, Equatable { } } +// Fail-closed mapping invariants remain in one mapper. +// swiftlint:disable:next type_body_length final class ChainModelMapper { static let quarantinedChainIdentifierPrefix = "__invalid_local_chain__:" static let quarantinedChainName = "Unavailable local chain" @@ -72,9 +74,11 @@ final class ChainModelMapper { else { return nil } let priceString: String? = { - if let d = object.value(forKey: "price") as? Decimal { return NSDecimalNumber(decimal: d).stringValue } - if let n = object.value(forKey: "price") as? NSDecimalNumber { return n.stringValue } - if let s = object.value(forKey: "price") as? String { return s } + if let decimalValue = object.value(forKey: "price") as? Decimal { + return NSDecimalNumber(decimal: decimalValue).stringValue + } + if let numberValue = object.value(forKey: "price") as? NSDecimalNumber { return numberValue.stringValue } + if let stringValue = object.value(forKey: "price") as? String { return stringValue } return nil }() guard @@ -624,6 +628,8 @@ extension ChainModelMapper: CoreDataMapperProtocol { return mappedChain } + // Defensive mapping remains one atomic validation. + // swiftlint:disable:next function_body_length private func transformWithoutExceptionBoundary( entity: CDChain ) throws -> ChainModel { diff --git a/fearless/Common/Storage/EntityToModel/MetaAccountMapper.swift b/fearless/Common/Storage/EntityToModel/MetaAccountMapper.swift index 31c4fa04da..857e5b4727 100644 --- a/fearless/Common/Storage/EntityToModel/MetaAccountMapper.swift +++ b/fearless/Common/Storage/EntityToModel/MetaAccountMapper.swift @@ -51,6 +51,8 @@ extension MetaAccountMapper: CoreDataMapperProtocol { return mappedAccount } + // Wallet validation remains one fail-closed mapping. + // swiftlint:disable:next function_body_length private func transformWithoutExceptionBoundary( entity: CoreDataEntity ) throws -> DataProviderModel { diff --git a/fearless/Common/Storage/Migration/KeystoreMigrator.swift b/fearless/Common/Storage/Migration/KeystoreMigrator.swift index a95e28de1c..1a6415ccd0 100644 --- a/fearless/Common/Storage/Migration/KeystoreMigrator.swift +++ b/fearless/Common/Storage/Migration/KeystoreMigrator.swift @@ -1078,3 +1078,6 @@ private extension UserStorageVersion { return false } } + +// Key migration invariants remain co-located for auditability. +// swiftlint:disable:this file_length diff --git a/fearless/Common/Storage/Migration/SubstrateStorage/SubstrateStorageMigrator.swift b/fearless/Common/Storage/Migration/SubstrateStorage/SubstrateStorageMigrator.swift index 7c30ebe44b..0ca7bd7716 100644 --- a/fearless/Common/Storage/Migration/SubstrateStorage/SubstrateStorageMigrator.swift +++ b/fearless/Common/Storage/Migration/SubstrateStorage/SubstrateStorageMigrator.swift @@ -263,7 +263,8 @@ enum SubstrateCacheRecoveryTransactionError: LocalizedError { /// or retention deletion interrupted after its marker became durable. /// Completed archives are excluded from backup and the newest two are retained /// subject to a combined 128 MiB byte budget. -final class CrashConsistentSubstrateCacheRecovery { +/// Recovery invariants remain in one auditable state machine. +final class CrashConsistentSubstrateCacheRecovery { // swiftlint:disable:this type_body_length private enum Operation: String { case quarantine case restoreLegacy = "restore-legacy" @@ -512,6 +513,8 @@ final class CrashConsistentSubstrateCacheRecovery { } @discardableResult + // Branches mirror durable recovery states. + // swiftlint:disable:next cyclomatic_complexity function_body_length func reconcile() throws -> URL? { try requireSafeDatabaseDirectory() @@ -839,6 +842,8 @@ final class CrashConsistentSubstrateCacheRecovery { ) } + // Ordered recovery validation remains explicit. + // swiftlint:disable:next function_body_length private func restoreLegacyTransaction( at transactionDirectoryURL: URL, marker: Marker @@ -1251,6 +1256,8 @@ final class CrashConsistentSubstrateCacheRecovery { try boundaryHook(.retentionArchiveRemoved(archiveName)) } + // Retention checks remain one ordered transaction. + // swiftlint:disable:next function_body_length private func maintainCompletedArchives() throws { switch pathKind(at: recoveryRootURL) { case .missing: @@ -1591,6 +1598,8 @@ final class CrashConsistentSubstrateCacheRecovery { return Data((lines.joined(separator: "\n") + "\n").utf8) } + // Marker validation remains fail-closed and linear. + // swiftlint:disable:next function_body_length private func decodeMarker(_ data: Data) throws -> Marker { guard let text = String(data: data, encoding: .utf8), @@ -2233,9 +2242,10 @@ final class CrashConsistentSubstrateCacheRecovery { } } + // Raw lstat fields form one atomic file identity. private func lstatValues( at url: URL - ) throws -> ( + ) throws -> ( // swiftlint:disable:this large_tuple byteCount: UInt64, deviceID: UInt64, inode: UInt64, @@ -2547,6 +2557,8 @@ private enum SubstrateTransformableRepairError: LocalizedError { } } +// Migration invariants remain in one auditable state machine. +// swiftlint:disable:next type_body_length final class SubstrateStorageMigrator { private struct StartupToManyRelationshipFamily { let entityName: String @@ -3470,6 +3482,8 @@ final class SubstrateStorageMigrator { return rowCounts } + // Sanitization remains one fail-closed transaction. + // swiftlint:disable:next function_body_length private func sanitizeTransformableEntity( entityName: String, attributes: [NSAttributeDescription], @@ -4111,6 +4125,8 @@ final class SubstrateStorageMigrator { return footprint } + // Protected-data validation remains atomic. + // swiftlint:disable:next function_body_length private func inspectProtectedDataWithoutExceptionBoundary( at url: URL, model: NSManagedObjectModel @@ -4352,6 +4368,8 @@ final class SubstrateStorageMigrator { } } + // Graph bounds remain one fail-closed inspection. + // swiftlint:disable:next function_body_length private func inspectVersion8StartupGraphBoundsWithoutExceptionBoundary( at url: URL, model: NSManagedObjectModel @@ -4489,6 +4507,8 @@ final class SubstrateStorageMigrator { try result.get() } + // Raw root bounds remain one fail-closed inspection. + // swiftlint:disable:next function_body_length private func inspectVersion8StartupRootRawBounds( at url: URL, model: NSManagedObjectModel @@ -4635,6 +4655,8 @@ final class SubstrateStorageMigrator { } } + // Raw child bounds remain one fail-closed inspection. + // swiftlint:disable:next function_body_length private func inspectVersion8StartupChildRawBounds( at url: URL, model: NSManagedObjectModel @@ -5233,6 +5255,8 @@ final class SubstrateStorageMigrator { return snapshot } + // Snapshot validation remains one atomic inspection. + // swiftlint:disable:next function_body_length private func inspectProtectedDataSnapshotWithoutExceptionBoundary( at url: URL, model: NSManagedObjectModel, @@ -6255,3 +6279,6 @@ extension SubstrateStorageMigrator: StorageMigrating { } } } + +// Substrate migration invariants remain co-located. +// swiftlint:disable:this file_length diff --git a/fearless/Common/Storage/Migration/UserStorage/StorageMigrator.swift b/fearless/Common/Storage/Migration/UserStorage/StorageMigrator.swift index 46cf9239f6..e521821cb3 100644 --- a/fearless/Common/Storage/Migration/UserStorage/StorageMigrator.swift +++ b/fearless/Common/Storage/Migration/UserStorage/StorageMigrator.swift @@ -103,6 +103,8 @@ struct UserStorageIntegrityValidationLimits: Equatable { } } +// Migration invariants remain in one auditable state machine. +// swiftlint:disable:next type_body_length final class UserStorageMigrator { private struct IntegrityField: Equatable { let name: String @@ -419,6 +421,8 @@ final class UserStorageMigrator { } } + // Ordered store replacement remains explicit. + // swiftlint:disable:next function_body_length private func performMigration( from sourceVersion: UserStorageVersion, to destinationVersion: UserStorageVersion, @@ -660,7 +664,8 @@ final class UserStorageMigrator { /// Migration is always performed from a disposable source copy. Unsafe /// preference archives are repaired only in that copy; the user's original /// store remains untouched until a validated atomic replacement. - private func sanitizePrivateSourceCopy( + /// Copy sanitization remains one fail-closed transaction. + private func sanitizePrivateSourceCopy( // swiftlint:disable:this function_body_length at privateSourceURL: URL, version: UserStorageVersion ) throws -> Bool { @@ -1525,6 +1530,8 @@ final class UserStorageMigrator { return .networkManagementFilters(digest) } + // Digest construction remains one canonical pass. + // swiftlint:disable:next function_body_length private func stagedSemanticDigest( context: NSManagedObjectContext, entityName: String, @@ -1846,6 +1853,8 @@ final class UserStorageMigrator { return snapshot } + // Snapshot validation remains one atomic pass. + // swiftlint:disable:next function_body_length private func makeStagedStoreIntegritySnapshot( context: NSManagedObjectContext, model: NSManagedObjectModel, @@ -2147,6 +2156,8 @@ final class UserStorageMigrator { ) } + // Relationship bounds remain one atomic validation. + // swiftlint:disable:next function_body_length private func integrityRelationship( for object: NSManagedObject, entityName: String, @@ -2811,3 +2822,6 @@ extension UserStorageMigrator: StorageMigrating { } } } + +// User migration invariants remain co-located. +// swiftlint:disable:this file_length diff --git a/fearless/Common/Storage/PersistentStoreCoordinator+Extensions.swift b/fearless/Common/Storage/PersistentStoreCoordinator+Extensions.swift index bc587836ef..27cb0947ea 100644 --- a/fearless/Common/Storage/PersistentStoreCoordinator+Extensions.swift +++ b/fearless/Common/Storage/PersistentStoreCoordinator+Extensions.swift @@ -139,6 +139,8 @@ enum SQLiteStoreFamilyCopier { let inode: UInt64 } + // Copy checks remain one ordered transaction. + // swiftlint:disable:next function_body_length static func copy( from sourceStoreURL: URL, to destinationStoreURL: URL, @@ -403,7 +405,8 @@ enum SQLiteStoreFamilyCopier { /// rollback transaction beside the database. The transaction is deliberately /// independent of the application's temporary directory so an iOS process /// termination cannot discard the sole recovery copy. -final class CrashConsistentStoreReplacer { +/// Replacement invariants remain in one auditable state machine. +final class CrashConsistentStoreReplacer { // swiftlint:disable:this type_body_length private enum TransactionState: String, Codable { case preparing case committing @@ -2394,9 +2397,10 @@ final class CrashConsistentStoreReplacer { } } + // Raw lstat fields form one atomic file identity. private func lstatValues( at url: URL - ) throws -> ( + ) throws -> ( // swiftlint:disable:this large_tuple byteCount: UInt64, mode: mode_t, deviceID: UInt64, @@ -2477,3 +2481,6 @@ extension NSPersistentStoreCoordinator { ) } } + +// Store replacement invariants remain co-located. +// swiftlint:disable:this file_length diff --git a/fearless/Common/Storage/UserDataStorageFacade.swift b/fearless/Common/Storage/UserDataStorageFacade.swift index 1003cc6759..639b6a0248 100644 --- a/fearless/Common/Storage/UserDataStorageFacade.swift +++ b/fearless/Common/Storage/UserDataStorageFacade.swift @@ -138,11 +138,11 @@ import RobinHood private enum RepositoryAsyncContext { static let queue: OperationQueue = { - let q = OperationQueue() - q.name = "io.fearless.repository.async" - q.qualityOfService = .userInitiated - q.maxConcurrentOperationCount = 2 - return q + let operationQueue = OperationQueue() + operationQueue.name = "io.fearless.repository.async" + operationQueue.qualityOfService = .userInitiated + operationQueue.maxConcurrentOperationCount = 2 + return operationQueue }() } diff --git a/fearless/Modules/Root/RootInteractor.swift b/fearless/Modules/Root/RootInteractor.swift index 828a8ad34d..8f38507827 100644 --- a/fearless/Modules/Root/RootInteractor.swift +++ b/fearless/Modules/Root/RootInteractor.swift @@ -286,6 +286,8 @@ protocol RootSelectedWalletSettingsProtocol: AnyObject { extension SelectedWalletSettings: RootSelectedWalletSettingsProtocol {} +// Startup migration barriers remain in one state machine. +// swiftlint:disable:next type_body_length final class RootInteractor { private static let setupDeadlineQueue = DispatchQueue( label: "jp.co.soramitsu.fearlesswallet.root-setup-deadline", From ab9a73db96536604cda7bb0724a9585ccff6dece Mon Sep 17 00:00:00 2001 From: Makoto Takemiya Date: Thu, 30 Jul 2026 12:44:48 +0400 Subject: [PATCH 25/30] test: bind copied phone fixture into release gate --- ...SubstrateStorageClassResolutionTests.swift | 111 ++++-- fearlessTests/Info.plist | 2 + scripts/ci/run-coredata-release-gate.sh | 159 +++++++-- scripts/test-coredata-release-gate.sh | 321 +++++++++++++++++- 4 files changed, 512 insertions(+), 81 deletions(-) diff --git a/fearlessTests/Common/Migration/SubstrateStorageClassResolutionTests.swift b/fearlessTests/Common/Migration/SubstrateStorageClassResolutionTests.swift index 092edc9213..a5b55b03a5 100644 --- a/fearlessTests/Common/Migration/SubstrateStorageClassResolutionTests.swift +++ b/fearlessTests/Common/Migration/SubstrateStorageClassResolutionTests.swift @@ -585,10 +585,7 @@ final class SubstrateStorageClassResolutionTests: XCTestCase { } func testCopiedPhoneV8Store_whenAvailable_thenAllRowsHaveExpectedClassesAndStoreIsUnchanged() throws { - let fixtureURL = copiedPhoneStoreFixtureURL() - guard FileManager.default.fileExists(atPath: fixtureURL.path) else { - throw XCTSkip("Optional copied-phone Substrate store fixture is unavailable") - } + let fixtureURL = try copiedPhoneStoreFixtureURL() let sourceFingerprintsBefore = try storeFingerprints(at: fixtureURL) let testStoreURL = try copyStoreFixtureToTemporaryDirectory(fixtureURL) @@ -642,10 +639,7 @@ final class SubstrateStorageClassResolutionTests: XCTestCase { // CoreDataRepository.fetchAll -> Collection.map -> ChainModelMapper. // The 2026-07-23 crashes trapped in Swift Array access on this path, so // the copied store must traverse the production repository unchanged. - let fixtureURL = copiedPhoneStoreFixtureURL() - guard FileManager.default.fileExists(atPath: fixtureURL.path) else { - throw XCTSkip("Optional copied-phone Substrate store fixture is unavailable") - } + let fixtureURL = try copiedPhoneStoreFixtureURL() let sourceFingerprintsBefore = try storeFingerprints(at: fixtureURL) let testStoreURL = try copyStoreFixtureToTemporaryDirectory(fixtureURL) @@ -1117,8 +1111,7 @@ final class SubstrateStorageClassResolutionTests: XCTestCase { in context: NSManagedObjectContext ) throws -> Model where Model: Codable & Identifiable, - Entity: NSManagedObject & CoreDataCodable - { + Entity: NSManagedObject & CoreDataCodable { let mapper = CodableCoreDataMapper() var transformedModel: Model? @@ -1212,34 +1205,57 @@ final class SubstrateStorageClassResolutionTests: XCTestCase { model.entityVersionHashesByName.mapValues { $0.base64EncodedString() } } - private func copiedPhoneStoreFixtureURL() -> URL { - if let configuredPath = ProcessInfo.processInfo.environment[ - "FEARLESS_SUBSTRATE_PHONE_STORE_FIXTURE" - ], !configuredPath.isEmpty { - return URL(fileURLWithPath: configuredPath) + private func copiedPhoneStoreFixtureURL() throws -> URL { + let infoKey = "FearlessSubstratePhoneStoreFixturePath" + let testBundle = Bundle(for: SubstrateStorageClassResolutionTests.self) + guard let configuredPath = testBundle.object( + forInfoDictionaryKey: infoKey + ) as? String, + !configuredPath.isEmpty else { + throw XCTSkip( + "Copied-phone fixture must be injected by the Core Data release gate" + ) + } + guard !configuredPath.contains("$(") else { + throw fixtureError("Copied-phone fixture build setting was not expanded") + } + guard (configuredPath as NSString).isAbsolutePath else { + throw fixtureError("Copied-phone fixture path must be absolute") } - return URL( - fileURLWithPath: - "/private/tmp/fearless-device-coredata-20260723/SubstrateDataModel.sqlite" - ) + let fixtureURL = URL(fileURLWithPath: configuredPath).standardizedFileURL + guard fixtureURL.path == configuredPath else { + throw fixtureError("Copied-phone fixture path must be canonical") + } + + try validateRequiredStoreComponents(at: fixtureURL) + return fixtureURL } private func copyStoreFixtureToTemporaryDirectory(_ fixtureURL: URL) throws -> URL { + let sourceFingerprints = try storeFingerprints(at: fixtureURL) let directoryURL = FileManager.default.temporaryDirectory .appendingPathComponent("SubstrateStoragePhoneFixture") .appendingPathComponent(UUID().uuidString) try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true) let destinationURL = directoryURL.appendingPathComponent(fixtureURL.lastPathComponent) - for suffix in ["", "-wal", "-shm"] { - let sourceURL = URL(fileURLWithPath: fixtureURL.path + suffix) - guard FileManager.default.fileExists(atPath: sourceURL.path) else { - continue + do { + for suffix in ["", "-wal", "-shm"] { + let sourceURL = URL(fileURLWithPath: fixtureURL.path + suffix) + let copiedURL = URL(fileURLWithPath: destinationURL.path + suffix) + try FileManager.default.copyItem(at: sourceURL, to: copiedURL) } - let copiedURL = URL(fileURLWithPath: destinationURL.path + suffix) - try FileManager.default.copyItem(at: sourceURL, to: copiedURL) + guard try storeFingerprints(at: destinationURL) == sourceFingerprints else { + throw fixtureError("Disposable copied-phone store differs from its source") + } + guard try storeFingerprints(at: fixtureURL) == sourceFingerprints else { + throw fixtureError("Copied-phone source changed while creating the snapshot") + } + } catch { + try? FileManager.default.removeItem(at: directoryURL) + throw error } return destinationURL @@ -1304,20 +1320,57 @@ final class SubstrateStorageClassResolutionTests: XCTestCase { } private func storeFingerprints(at storeURL: URL) throws -> [String: Data] { + try validateRequiredStoreComponents(at: storeURL) var fingerprints: [String: Data] = [:] for suffix in ["", "-wal", "-shm"] { let fileURL = URL(fileURLWithPath: storeURL.path + suffix) - guard FileManager.default.fileExists(atPath: fileURL.path) else { - continue - } - fingerprints[suffix] = try digest(of: fileURL) } return fingerprints } + private func validateRequiredStoreComponents(at storeURL: URL) throws { + for suffix in ["", "-wal", "-shm"] { + let fileURL = URL(fileURLWithPath: storeURL.path + suffix) + var isDirectory: ObjCBool = false + guard FileManager.default.fileExists( + atPath: fileURL.path, + isDirectory: &isDirectory + ) else { + throw fixtureError( + "Copied-phone fixture is incomplete: missing \(fileURL.lastPathComponent)" + ) + } + + let values = try fileURL.resourceValues( + forKeys: [.fileSizeKey, .isRegularFileKey, .isSymbolicLinkKey] + ) + guard !isDirectory.boolValue, + values.isRegularFile == true, + values.isSymbolicLink != true, + FileManager.default.isReadableFile(atPath: fileURL.path) else { + throw fixtureError( + "Copied-phone fixture components must be readable, regular, non-symlink files" + ) + } + guard (values.fileSize ?? 0) > 0 else { + throw fixtureError( + "Copied-phone fixture components must be nonempty" + ) + } + } + } + + private func fixtureError(_ description: String) -> NSError { + NSError( + domain: "SubstrateStorageClassResolutionTests.CopiedPhoneFixture", + code: 1, + userInfo: [NSLocalizedDescriptionKey: description] + ) + } + private func digest(of fileURL: URL) throws -> Data { Data(SHA256.hash(data: try Data(contentsOf: fileURL, options: .mappedIfSafe))) } diff --git a/fearlessTests/Info.plist b/fearlessTests/Info.plist index 64d65ca495..f9f1a41373 100644 --- a/fearlessTests/Info.plist +++ b/fearlessTests/Info.plist @@ -18,5 +18,7 @@ 1.0 CFBundleVersion 1 + FearlessSubstratePhoneStoreFixturePath + $(FEARLESS_SUBSTRATE_PHONE_STORE_FIXTURE_PATH) diff --git a/scripts/ci/run-coredata-release-gate.sh b/scripts/ci/run-coredata-release-gate.sh index 193f54ec2f..2fd92fd7ef 100644 --- a/scripts/ci/run-coredata-release-gate.sh +++ b/scripts/ci/run-coredata-release-gate.sh @@ -6,8 +6,8 @@ set -euo pipefail # The normal stage deliberately excludes the two copied-phone fixture tests. Run # those separately with: # -# FEARLESS_SUBSTRATE_PHONE_STORE_FIXTURE=/absolute/path/to/SubstrateDataModel.sqlite \ -# bash scripts/ci/run-coredata-release-gate.sh --stage copied-phone \ +# bash scripts/ci/run-coredata-release-gate.sh --stage copied-phone \ +# --fixture /absolute/path/to/SubstrateDataModel.sqlite \ # --simulator-udid # # This script never accepts a physical-device destination, never opens the source @@ -17,7 +17,8 @@ umask 077 readonly LOG_PREFIX="[coredata-release-gate]" readonly SCHEME="fearless.tests" -readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +readonly SCRIPT_DIR readonly MANIFEST_DIR="$SCRIPT_DIR/manifests" readonly CDMETAACCOUNT_CODABLE_AUDIT="$SCRIPT_DIR/../storage/audit-cdmetaaccount-codable-contract.sh" readonly UUID_PATTERN='^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$' @@ -38,6 +39,7 @@ if [[ "$CORE_DATA_TEST_HARNESS" != "1" ]]; then for override_name in \ FEARLESS_CORE_DATA_ROOT_DIR \ FEARLESS_CORE_DATA_WORKSPACE \ + FEARLESS_CORE_DATA_SOURCE_PACKAGES_DIR \ FEARLESS_CORE_DATA_XCODEBUILD_BIN \ FEARLESS_CORE_DATA_XCRUN_BIN \ FEARLESS_CORE_DATA_PYTHON_BIN \ @@ -124,6 +126,9 @@ uuid_pattern = re.compile( r"^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-" r"[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$" ) +iphone_device_type_pattern = re.compile( + r"^com\.apple\.CoreSimulator\.SimDeviceType\.iPhone-[A-Za-z0-9-]+$" +) try: with open(json_path, "r", encoding="utf-8") as source: @@ -145,13 +150,13 @@ for devices in runtime_devices.values(): if not isinstance(device, dict): continue udid = device.get("udid") - name = device.get("name") + device_type = device.get("deviceTypeIdentifier") is_available = device.get("isAvailable", True) if ( isinstance(udid, str) and uuid_pattern.fullmatch(udid) - and isinstance(name, str) - and name.startswith("iPhone") + and isinstance(device_type, str) + and iphone_device_type_pattern.fullmatch(device_type) and is_available is True ): eligible.append(device) @@ -181,22 +186,71 @@ fixture_fingerprint() { local suffix local candidate local digest + local link_count for suffix in "" "-wal" "-shm"; do candidate="${fixture}${suffix}" - if [[ -e "$candidate" ]]; then - [[ -f "$candidate" && ! -L "$candidate" ]] || - fail "copied-phone fixture components must be regular, non-symlink files" - digest="$("$SHASUM_BIN" -a 256 "$candidate" | awk '{print $1}')" - [[ "$digest" =~ ^[0-9A-Fa-f]{64}$ ]] || - fail "unable to fingerprint copied-phone fixture safely" - printf '%s=%s\n' "$suffix" "$digest" - else - printf '%s=%s\n' "$suffix" "absent" - fi + [[ -e "$candidate" ]] || + fail "copied-phone fixture is incomplete: missing required component $(basename "$candidate")" + [[ -r "$candidate" && -s "$candidate" && -f "$candidate" && ! -L "$candidate" ]] || + fail "copied-phone fixture components must be readable, nonempty, regular, non-symlink files" + link_count="$(stat -f '%l' "$candidate" 2>/dev/null)" || + fail "unable to inspect copied-phone fixture link count" + [[ "$link_count" == "1" ]] || + fail "copied-phone fixture components must be single-link files" + digest="$("$SHASUM_BIN" -a 256 "$candidate" | awk '{print $1}')" + [[ "$digest" =~ ^[0-9A-Fa-f]{64}$ ]] || + fail "unable to fingerprint copied-phone fixture safely" + printf '%s=%s\n' "$suffix" "$digest" done } +validate_test_bundle_fixture_bridge() { + local info_plist_path="$1" + + if ! "$PYTHON_BIN" - "$info_plist_path" <<'PY' +import plistlib +import sys +import xml.etree.ElementTree as ET + +info_plist_path = sys.argv[1] +try: + with open(info_plist_path, "rb") as source: + payload = plistlib.load(source) + root = ET.parse(info_plist_path).getroot() +except (OSError, ValueError, plistlib.InvalidFileException, ET.ParseError): + raise SystemExit("test Info.plist is missing or malformed") + +if not isinstance(payload, dict): + raise SystemExit("test Info.plist root must be a dictionary") +key = "FearlessSubstratePhoneStoreFixturePath" +value = "$(FEARLESS_SUBSTRATE_PHONE_STORE_FIXTURE_PATH)" +if payload.get(key) != value: + raise SystemExit("fixture path key must use the reviewed build setting") + +dictionary = root.find("./dict") +if dictionary is None: + raise SystemExit("test Info.plist XML dictionary is missing") +children = list(dictionary) +indexes = [ + index + for index, child in enumerate(children) + if child.tag == "key" and child.text == key +] +if len(indexes) != 1: + raise SystemExit("fixture path key must occur exactly once") +index = indexes[0] +if index + 1 >= len(children): + raise SystemExit("fixture path key has no value") +value_node = children[index + 1] +if value_node.tag != "string" or value_node.text != value: + raise SystemExit("fixture path key has an unsafe value") +PY + then + fail "fearlessTests Info.plist fixture bridge is missing or unsafe" + fi +} + validate_result_summary() { local result_bundle="$1" local expected_total="$2" @@ -550,21 +604,21 @@ run_stage() { "SWIFT_OPTIMIZATION_LEVEL=-O" "ENABLE_TESTABILITY=YES" "ONLY_ACTIVE_ARCH=YES" - "${selectors[@]}" - test ) + if [[ "$stage" == "copied-phone" ]]; then + xcodebuild_arguments+=( + "FEARLESS_SUBSTRATE_PHONE_STORE_FIXTURE_PATH=$PHONE_FIXTURE" + ) + fi + xcodebuild_arguments+=("${selectors[@]}" test) log "Running ${stage} Release test stage; detailed output is private in the gate log" set +e - if [[ "$stage" == "core" ]]; then - env -u FEARLESS_SUBSTRATE_PHONE_STORE_FIXTURE \ - "$XCODEBUILD_BIN" "${xcodebuild_arguments[@]}" >"$xcodebuild_log" 2>&1 - xcodebuild_status=$? - else - FEARLESS_SUBSTRATE_PHONE_STORE_FIXTURE="$PHONE_FIXTURE" \ - "$XCODEBUILD_BIN" "${xcodebuild_arguments[@]}" >"$xcodebuild_log" 2>&1 - xcodebuild_status=$? - fi + env \ + -u FEARLESS_SUBSTRATE_PHONE_STORE_FIXTURE \ + -u FEARLESS_SUBSTRATE_PHONE_STORE_FIXTURE_PATH \ + "$XCODEBUILD_BIN" "${xcodebuild_arguments[@]}" >"$xcodebuild_log" 2>&1 + xcodebuild_status=$? set -e if [[ "$stage" == "copied-phone" ]]; then @@ -596,6 +650,7 @@ main() { local fixture_argument="" local output_argument="" local derived_data_argument="" + local all_stage_fixture_fingerprint="" while [[ "$#" -gt 0 ]]; do case "$1" in @@ -660,21 +715,30 @@ main() { fail "simulator UDID must be a canonical UUID" fi - readonly ROOT_DIR="$( + local root_dir + root_dir="$( cd "${FEARLESS_CORE_DATA_ROOT_DIR:-$(dirname "${BASH_SOURCE[0]}")/../..}" && pwd -P )" + readonly ROOT_DIR="$root_dir" readonly WORKSPACE_INPUT="${FEARLESS_CORE_DATA_WORKSPACE:-$ROOT_DIR/fearless.xcworkspace}" [[ -d "$WORKSPACE_INPUT" && -f "$WORKSPACE_INPUT/contents.xcworkspacedata" ]] || fail "fearless.xcworkspace is missing or incomplete" [[ "$(basename "$WORKSPACE_INPUT")" == "fearless.xcworkspace" ]] || fail "the Core Data gate requires fearless.xcworkspace" - readonly WORKSPACE="$(canonical_directory "$WORKSPACE_INPUT")" + local workspace + workspace="$(canonical_directory "$WORKSPACE_INPUT")" + readonly WORKSPACE="$workspace" + readonly TEST_INFO_PLIST_PATH="$ROOT_DIR/fearlessTests/Info.plist" + [[ -f "$TEST_INFO_PLIST_PATH" && ! -L "$TEST_INFO_PLIST_PATH" ]] || + fail "fearlessTests Info.plist is missing or unsafe" readonly SOURCE_PACKAGES_INPUT="${FEARLESS_CORE_DATA_SOURCE_PACKAGES_DIR:-$ROOT_DIR/SourcePackages}" [[ -d "$SOURCE_PACKAGES_INPUT" && -d "$SOURCE_PACKAGES_INPUT/checkouts" ]] || fail "prepared canonical SourcePackages/checkouts is required" - readonly SOURCE_PACKAGES_DIR="$(canonical_directory "$SOURCE_PACKAGES_INPUT")" + local source_packages_dir + source_packages_dir="$(canonical_directory "$SOURCE_PACKAGES_INPUT")" + readonly SOURCE_PACKAGES_DIR="$source_packages_dir" readonly XCODEBUILD_BIN="${FEARLESS_CORE_DATA_XCODEBUILD_BIN:-xcodebuild}" readonly XCRUN_BIN="${FEARLESS_CORE_DATA_XCRUN_BIN:-xcrun}" @@ -686,6 +750,7 @@ main() { if [[ "$stage" == "copied-phone" || "$stage" == "all" ]]; then require_executable "$SHASUM_BIN" "shasum" fi + validate_test_bundle_fixture_bridge "$TEST_INFO_PLIST_PATH" [[ -f "$CDMETAACCOUNT_CODABLE_AUDIT" && ! -L "$CDMETAACCOUNT_CODABLE_AUDIT" ]] || fail "CDMetaAccount Codable contract audit is missing or unsafe" FEARLESS_CDMETAACCOUNT_CODABLE_ROOT="$ROOT_DIR" \ @@ -727,23 +792,45 @@ main() { [[ ! -e "$OUTPUT_DIR_INPUT" ]] || fail "output directory must not already exist" mkdir -p "$OUTPUT_DIR_INPUT" - readonly OUTPUT_DIR="$(canonical_directory "$OUTPUT_DIR_INPUT")" + local output_dir + output_dir="$(canonical_directory "$OUTPUT_DIR_INPUT")" + readonly OUTPUT_DIR="$output_dir" readonly DERIVED_DATA_DIR="${derived_data_argument:-$ROOT_DIR/build/DerivedData-CoreDataReleaseGate}" - readonly PHONE_FIXTURE="${fixture_argument:-${FEARLESS_SUBSTRATE_PHONE_STORE_FIXTURE:-}}" + local phone_fixture_input="${fixture_argument:-${FEARLESS_SUBSTRATE_PHONE_STORE_FIXTURE:-}}" if [[ "$stage" == "copied-phone" || "$stage" == "all" ]]; then - [[ -n "$PHONE_FIXTURE" ]] || + [[ -n "$phone_fixture_input" ]] || fail "copied-phone stage requires --fixture or FEARLESS_SUBSTRATE_PHONE_STORE_FIXTURE" - [[ "$PHONE_FIXTURE" == /* ]] || + [[ "$phone_fixture_input" == /* ]] || fail "copied-phone fixture path must be absolute" - [[ -r "$PHONE_FIXTURE" && -f "$PHONE_FIXTURE" && ! -L "$PHONE_FIXTURE" ]] || - fail "copied-phone fixture must be a readable, regular, non-symlink file" + if [[ "$phone_fixture_input" == *$'\n'* || + "$phone_fixture_input" == *$'\r'* || + "$phone_fixture_input" == *$'\t'* ]] || + printf '%s' "$phone_fixture_input" | LC_ALL=C grep -q '[[:cntrl:]]'; then + fail "copied-phone fixture path must not contain control characters" + fi + local canonical_phone_fixture + canonical_phone_fixture="$( + canonical_directory "$(dirname "$phone_fixture_input")" + )/$(basename "$phone_fixture_input")" + readonly PHONE_FIXTURE="$canonical_phone_fixture" + if [[ "$stage" == "all" ]]; then + all_stage_fixture_fingerprint="$(fixture_fingerprint "$PHONE_FIXTURE")" + else + fixture_fingerprint "$PHONE_FIXTURE" >/dev/null + fi + else + readonly PHONE_FIXTURE="" fi log "Using fearless.xcworkspace, scheme fearless.tests, Release optimization -O" if [[ "$stage" == "core" || "$stage" == "all" ]]; then run_stage "core" + if [[ "$stage" == "all" ]]; then + [[ "$(fixture_fingerprint "$PHONE_FIXTURE")" == "$all_stage_fixture_fingerprint" ]] || + fail "copied-phone source fixture changed during the core stage" + fi fi if [[ "$stage" == "copied-phone" || "$stage" == "all" ]]; then run_stage "copied-phone" diff --git a/scripts/test-coredata-release-gate.sh b/scripts/test-coredata-release-gate.sh index 4cc9dd5057..5ff6b61d78 100644 --- a/scripts/test-coredata-release-gate.sh +++ b/scripts/test-coredata-release-gate.sh @@ -1,15 +1,18 @@ #!/usr/bin/env bash set -euo pipefail -readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +readonly SCRIPT_DIR readonly GATE="$SCRIPT_DIR/ci/run-coredata-release-gate.sh" readonly CDMETAACCOUNT_CODABLE_TEST="$SCRIPT_DIR/storage/test-cdmetaaccount-codable-contract.sh" -readonly TEMPORARY_DIR="$(mktemp -d "${TMPDIR:-/private/tmp}/fearless-coredata-gate-tests.XXXXXX")" +TEMPORARY_DIR="$(mktemp -d "${TMPDIR:-/private/tmp}/fearless-coredata-gate-tests.XXXXXX")" +readonly TEMPORARY_DIR trap 'rm -rf "$TEMPORARY_DIR"' EXIT readonly FIXTURE_ROOT="$TEMPORARY_DIR/repository" readonly BIN_DIR="$TEMPORARY_DIR/bin" readonly PHONE_FIXTURE="$TEMPORARY_DIR/SubstrateDataModel.sqlite" +readonly FIXTURE_INFO_PLIST="$FIXTURE_ROOT/fearlessTests/Info.plist" readonly SIMULATOR_UDID="11111111-2222-3333-4444-555555555555" readonly OTHER_SIMULATOR_UDID="AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE" @@ -38,6 +41,14 @@ assert_argument_once() { fail "expected exactly one xcodebuild argument '$expected', found $count" } +assert_invocation_once() { + local expected="$1" + local count + count="$(grep -Fxc -- "$expected" "$RUN_DIRECTORY/invocations" || true)" + [[ "$count" == "1" ]] || + fail "expected exactly one xcodebuild invocation '$expected', found $count" +} + run_gate() { local label="$1" RUN_NUMBER=$((RUN_NUMBER + 1)) @@ -47,11 +58,13 @@ run_gate() { env \ FEARLESS_CORE_DATA_TEST_HARNESS=1 \ FEARLESS_CORE_DATA_ROOT_DIR="$FIXTURE_ROOT" \ + FEARLESS_CORE_DATA_SOURCE_PACKAGES_DIR="$FIXTURE_ROOT/SourcePackages" \ FEARLESS_CORE_DATA_XCODEBUILD_BIN="$BIN_DIR/xcodebuild" \ FEARLESS_CORE_DATA_XCRUN_BIN="$BIN_DIR/xcrun" \ FEARLESS_CORE_DATA_PYTHON_BIN="${PYTHON_BIN:-python3}" \ FAKE_ARGUMENT_LOG="$RUN_DIRECTORY/arguments" \ FAKE_ENVIRONMENT_LOG="$RUN_DIRECTORY/environment" \ + FAKE_INVOCATION_LOG="$RUN_DIRECTORY/invocations" \ FAKE_SIMULATOR_UDID="$SIMULATOR_UDID" \ FAKE_OTHER_SIMULATOR_UDID="$OTHER_SIMULATOR_UDID" \ FAKE_CORE_MANIFEST="$SCRIPT_DIR/ci/manifests/coredata-release-core-tests.txt" \ @@ -77,6 +90,7 @@ expect_failure() { mkdir -p \ "$FIXTURE_ROOT/fearless/Common/Storage" \ + "$FIXTURE_ROOT/fearlessTests" \ "$FIXTURE_ROOT/fearless.xcworkspace" \ "$FIXTURE_ROOT/SourcePackages/checkouts" \ "$BIN_DIR" @@ -84,23 +98,44 @@ printf '%s\n' '' \ >"$FIXTURE_ROOT/fearless.xcworkspace/contents.xcworkspacedata" printf '%s\n' 'struct SafeCoreDataReleaseFixture {}' \ >"$FIXTURE_ROOT/fearless/Common/Storage/SafeCoreDataReleaseFixture.swift" +cat >"$FIXTURE_INFO_PLIST" <<'PLIST' + + + + + FearlessSubstratePhoneStoreFixturePath + $(FEARLESS_SUBSTRATE_PHONE_STORE_FIXTURE_PATH) + + +PLIST printf '%s\n' 'opaque copied-store fixture; never interpreted by this shell test' >"$PHONE_FIXTURE" printf '%s\n' 'opaque write-ahead log' >"${PHONE_FIXTURE}-wal" printf '%s\n' 'opaque shared-memory sidecar' >"${PHONE_FIXTURE}-shm" +CANONICAL_PHONE_FIXTURE="$( + cd "$(dirname "$PHONE_FIXTURE")" && pwd -P +)/$(basename "$PHONE_FIXTURE")" +readonly CANONICAL_PHONE_FIXTURE cat >"$BIN_DIR/xcodebuild" <<'STUB' #!/usr/bin/env bash set -euo pipefail result_bundle="" +fixture_build_setting="" +fixture_build_setting_count=0 printf '%s\n' "$@" >"$FAKE_ARGUMENT_LOG" -if [[ "${FEARLESS_SUBSTRATE_PHONE_STORE_FIXTURE+x}" == "x" ]]; then - printf '%s\n' "set" >"$FAKE_ENVIRONMENT_LOG" +if [[ "${FEARLESS_SUBSTRATE_PHONE_STORE_FIXTURE+x}" == "x" || + "${FEARLESS_SUBSTRATE_PHONE_STORE_FIXTURE_PATH+x}" == "x" ]]; then + inherited_fixture_environment="set" else - printf '%s\n' "unset" >"$FAKE_ENVIRONMENT_LOG" + inherited_fixture_environment="unset" fi while [[ "$#" -gt 0 ]]; do + if [[ "$1" == FEARLESS_SUBSTRATE_PHONE_STORE_FIXTURE_PATH=* ]]; then + fixture_build_setting="${1#FEARLESS_SUBSTRATE_PHONE_STORE_FIXTURE_PATH=}" + fixture_build_setting_count=$((fixture_build_setting_count + 1)) + fi if [[ "$1" == "-resultBundlePath" ]]; then shift result_bundle="${1:-}" @@ -108,8 +143,28 @@ while [[ "$#" -gt 0 ]]; do shift || true done +{ + printf 'inherited=%s\n' "$inherited_fixture_environment" + printf 'build-setting-count=%s\n' "$fixture_build_setting_count" + printf 'build-setting-value=%s\n' "$fixture_build_setting" +} >"$FAKE_ENVIRONMENT_LOG" + +result_name="$(basename "$result_bundle" .xcresult)" +printf 'stage=%s inherited=%s build-setting-count=%s build-setting-value=%s\n' \ + "$result_name" \ + "$inherited_fixture_environment" \ + "$fixture_build_setting_count" \ + "$fixture_build_setting" >>"$FAKE_INVOCATION_LOG" + if [[ "${FAKE_MUTATE_FIXTURE:-0}" == "1" ]]; then - printf '%s\n' "mutation" >>"$FEARLESS_SUBSTRATE_PHONE_STORE_FIXTURE" + [[ -n "$fixture_build_setting" ]] + printf '%s\n' "mutation" >>"$fixture_build_setting" +fi + +if [[ "${FAKE_MUTATE_FIXTURE_DURING_CORE:-0}" == "1" && + "$result_name" == "core" ]]; then + [[ -n "${FAKE_CORE_MUTATION_PATH:-}" ]] + printf '%s\n' "core-stage mutation" >>"$FAKE_CORE_MUTATION_PATH" fi if [[ "${FAKE_XCODEBUILD_FAIL:-0}" == "1" ]]; then @@ -133,7 +188,14 @@ if [[ "${1:-}" == "simctl" ]]; then one) cat <&2 + fail "renamed iPhone simulator was rejected" +fi +assert_argument_once "platform=iOS Simulator,id=$SIMULATOR_UDID" +printf '%s\n' "[coredata-release-gate-test] PASS: renamed iPhone device-type contract" + # Copied-phone tests are their own exact-two stage and receive an absolute, # read-only source fixture contract. CASE_ENV=( @@ -346,10 +453,64 @@ assert_argument_once \ "-only-testing:fearlessTests/SubstrateStorageClassResolutionTests/testCopiedPhoneV8Store_whenAvailable_thenAllRowsHaveExpectedClassesAndStoreIsUnchanged" assert_argument_once \ "-only-testing:fearlessTests/SubstrateStorageClassResolutionTests/testCopiedPhoneV8Store_whenFetchedThroughProductionRepositories_thenMapsEveryRuntimeAndChain" -assert_output_contains "set" "$RUN_DIRECTORY/environment" +assert_argument_once "FEARLESS_SUBSTRATE_PHONE_STORE_FIXTURE_PATH=$CANONICAL_PHONE_FIXTURE" +assert_output_contains "inherited=unset" "$RUN_DIRECTORY/environment" +assert_output_contains "build-setting-count=1" "$RUN_DIRECTORY/environment" +assert_output_contains "build-setting-value=$CANONICAL_PHONE_FIXTURE" "$RUN_DIRECTORY/environment" assert_output_contains "exactly 2 tests, source fixture unchanged" "$RUN_DIRECTORY/stdout" printf '%s\n' "[coredata-release-gate-test] PASS: copied-phone exact-two contract" +# The documented environment input is converted into the same explicit build +# setting and is removed from xcodebuild's inherited shell environment. +CASE_ENV=( + "FEARLESS_SUBSTRATE_PHONE_STORE_FIXTURE=$PHONE_FIXTURE" + "FAKE_TOTAL_TESTS=2" + "FAKE_PASSED_TESTS=2" +) +GATE_ARGS=(--stage copied-phone --simulator-udid "$SIMULATOR_UDID") +if ! run_gate "valid-copied-phone-environment-input"; then + sed -n '1,80p' "$RUN_DIRECTORY/stderr" >&2 + fail "valid copied-phone environment input was rejected" +fi +assert_argument_once "FEARLESS_SUBSTRATE_PHONE_STORE_FIXTURE_PATH=$CANONICAL_PHONE_FIXTURE" +assert_output_contains "inherited=unset" "$RUN_DIRECTORY/environment" +assert_output_contains "build-setting-count=1" "$RUN_DIRECTORY/environment" +printf '%s\n' "[coredata-release-gate-test] PASS: copied-phone environment-to-build-setting bridge" + +# Under --stage all, only copied-phone receives the fixture build setting. The +# core stage is additionally covered by the same immutable-fixture window. +CASE_ENV=() +GATE_ARGS=(--stage all --simulator-udid "$SIMULATOR_UDID" --fixture "$PHONE_FIXTURE") +if ! run_gate "valid-all"; then + sed -n '1,80p' "$RUN_DIRECTORY/stderr" >&2 + fail "valid all-stage gate was rejected" +fi +assert_invocation_once \ + "stage=core inherited=unset build-setting-count=0 build-setting-value=" +assert_invocation_once \ + "stage=copied-phone inherited=unset build-setting-count=1 build-setting-value=$CANONICAL_PHONE_FIXTURE" +assert_output_contains "PASSED core stage" "$RUN_DIRECTORY/stdout" +assert_output_contains "PASSED copied-phone fixture stage" "$RUN_DIRECTORY/stdout" +printf '%s\n' "[coredata-release-gate-test] PASS: all-stage fixture isolation contract" + +# Simulate an external core-stage mutation without exposing the fixture path to +# xcodebuild. The gate must reject it before the copied-phone stage can start. +cp "$PHONE_FIXTURE" "$TEMPORARY_DIR/phone-before-core-stage-mutation.sqlite" +CASE_ENV=( + "FAKE_MUTATE_FIXTURE_DURING_CORE=1" + "FAKE_CORE_MUTATION_PATH=$PHONE_FIXTURE" +) +GATE_ARGS=(--stage all --simulator-udid "$SIMULATOR_UDID" --fixture "$PHONE_FIXTURE") +expect_failure \ + "all-stage-core-fixture-mutation" \ + "copied-phone source fixture changed during the core stage" +assert_invocation_once \ + "stage=core inherited=unset build-setting-count=0 build-setting-value=" +if grep -Fq "stage=copied-phone " "$RUN_DIRECTORY/invocations"; then + fail "copied-phone stage ran after the core stage mutated its source fixture" +fi +mv "$TEMPORARY_DIR/phone-before-core-stage-mutation.sqlite" "$PHONE_FIXTURE" + # Missing/ambiguous simulator selection and hostile destinations. CASE_ENV=("FAKE_SIMULATOR_MODE=none") GATE_ARGS=(--stage core) @@ -363,6 +524,18 @@ CASE_ENV=("FAKE_SIMULATOR_MODE=unavailable") GATE_ARGS=(--stage core --simulator-udid "$SIMULATOR_UDID") expect_failure "unavailable-explicit-simulator" "requested simulator could not be verified" +CASE_ENV=("FAKE_SIMULATOR_MODE=forged-iphone-name") +GATE_ARGS=(--stage core --simulator-udid "$SIMULATOR_UDID") +expect_failure "forged-iphone-display-name" "requested simulator could not be verified" + +CASE_ENV=("FAKE_SIMULATOR_MODE=missing-device-type") +GATE_ARGS=(--stage core --simulator-udid "$SIMULATOR_UDID") +expect_failure "missing-simulator-device-type" "requested simulator could not be verified" + +CASE_ENV=("FAKE_SIMULATOR_MODE=malformed-device-type") +GATE_ARGS=(--stage core --simulator-udid "$SIMULATOR_UDID") +expect_failure "malformed-simulator-device-type" "requested simulator could not be verified" + CASE_ENV=() GATE_ARGS=(--stage core --simulator-udid "not-a-uuid") expect_failure "malformed-simulator-udid" "canonical UUID" @@ -395,7 +568,7 @@ GATE_ARGS=( --simulator-udid "$SIMULATOR_UDID" --fixture "$TEMPORARY_DIR/absent.sqlite" ) -expect_failure "absent-copied-phone-fixture" "readable, regular, non-symlink" +expect_failure "absent-copied-phone-fixture" "missing required component absent.sqlite" ln -s "$PHONE_FIXTURE" "$TEMPORARY_DIR/symlink.sqlite" CASE_ENV=() @@ -404,7 +577,65 @@ GATE_ARGS=( --simulator-udid "$SIMULATOR_UDID" --fixture "$TEMPORARY_DIR/symlink.sqlite" ) -expect_failure "symlink-copied-phone-fixture" "readable, regular, non-symlink" +expect_failure "symlink-copied-phone-fixture" "readable, nonempty, regular, non-symlink" + +CASE_ENV=() +GATE_ARGS=( + --stage copied-phone + --simulator-udid "$SIMULATOR_UDID" + --fixture "$TEMPORARY_DIR/hostile"$'\n'"fixture.sqlite" +) +expect_failure "control-character-copied-phone-path" "must not contain control characters" + +mv "${PHONE_FIXTURE}-wal" "${PHONE_FIXTURE}-wal.saved" +CASE_ENV=() +GATE_ARGS=(--stage copied-phone --simulator-udid "$SIMULATOR_UDID" --fixture "$PHONE_FIXTURE") +expect_failure "missing-copied-phone-wal" "missing required component SubstrateDataModel.sqlite-wal" +mv "${PHONE_FIXTURE}-wal.saved" "${PHONE_FIXTURE}-wal" + +mv "${PHONE_FIXTURE}-shm" "${PHONE_FIXTURE}-shm.saved" +CASE_ENV=() +GATE_ARGS=(--stage copied-phone --simulator-udid "$SIMULATOR_UDID" --fixture "$PHONE_FIXTURE") +expect_failure "missing-copied-phone-shm" "missing required component SubstrateDataModel.sqlite-shm" +mv "${PHONE_FIXTURE}-shm.saved" "${PHONE_FIXTURE}-shm" + +mv "${PHONE_FIXTURE}-wal" "${PHONE_FIXTURE}-wal.saved" +ln -s "$PHONE_FIXTURE" "${PHONE_FIXTURE}-wal" +CASE_ENV=() +GATE_ARGS=(--stage copied-phone --simulator-udid "$SIMULATOR_UDID" --fixture "$PHONE_FIXTURE") +expect_failure "symlink-copied-phone-wal" "readable, nonempty, regular, non-symlink" +rm "${PHONE_FIXTURE}-wal" +mv "${PHONE_FIXTURE}-wal.saved" "${PHONE_FIXTURE}-wal" + +mv "${PHONE_FIXTURE}-shm" "${PHONE_FIXTURE}-shm.saved" +mkdir "${PHONE_FIXTURE}-shm" +CASE_ENV=() +GATE_ARGS=(--stage copied-phone --simulator-udid "$SIMULATOR_UDID" --fixture "$PHONE_FIXTURE") +expect_failure "directory-copied-phone-shm" "readable, nonempty, regular, non-symlink" +rmdir "${PHONE_FIXTURE}-shm" +mv "${PHONE_FIXTURE}-shm.saved" "${PHONE_FIXTURE}-shm" + +mv "${PHONE_FIXTURE}-wal" "${PHONE_FIXTURE}-wal.saved" +: >"${PHONE_FIXTURE}-wal" +CASE_ENV=() +GATE_ARGS=(--stage copied-phone --simulator-udid "$SIMULATOR_UDID" --fixture "$PHONE_FIXTURE") +expect_failure "empty-copied-phone-wal" "readable, nonempty, regular, non-symlink" +rm "${PHONE_FIXTURE}-wal" +mv "${PHONE_FIXTURE}-wal.saved" "${PHONE_FIXTURE}-wal" + +mv "${PHONE_FIXTURE}-wal" "${PHONE_FIXTURE}-wal.saved" +ln "$PHONE_FIXTURE" "${PHONE_FIXTURE}-wal" +CASE_ENV=() +GATE_ARGS=(--stage copied-phone --simulator-udid "$SIMULATOR_UDID" --fixture "$PHONE_FIXTURE") +expect_failure "hardlinked-copied-phone-component" "single-link files" +rm "${PHONE_FIXTURE}-wal" +mv "${PHONE_FIXTURE}-wal.saved" "${PHONE_FIXTURE}-wal" + +chmod 000 "${PHONE_FIXTURE}-shm" +CASE_ENV=() +GATE_ARGS=(--stage copied-phone --simulator-udid "$SIMULATOR_UDID" --fixture "$PHONE_FIXTURE") +expect_failure "unreadable-copied-phone-shm" "readable, nonempty, regular, non-symlink" +chmod 600 "${PHONE_FIXTURE}-shm" CASE_ENV=("FAKE_TOTAL_TESTS=1" "FAKE_PASSED_TESTS=1") GATE_ARGS=(--stage copied-phone --simulator-udid "$SIMULATOR_UDID" --fixture "$PHONE_FIXTURE") @@ -508,6 +739,50 @@ CASE_ENV=() GATE_ARGS=(--stage impossible --simulator-udid "$SIMULATOR_UDID") expect_failure "unknown-stage" "core, copied-phone, or all" +# The test-bundle Info.plist is part of the security boundary: the reviewed +# build setting must reach XCTest through exactly one typed string key. +mv "$FIXTURE_INFO_PLIST" "$TEMPORARY_DIR/valid-fearlessTests-Info.plist" +CASE_ENV=() +GATE_ARGS=(--stage core --simulator-udid "$SIMULATOR_UDID") +expect_failure "missing-fixture-info-plist" "Info.plist is missing or unsafe" +mv "$TEMPORARY_DIR/valid-fearlessTests-Info.plist" "$FIXTURE_INFO_PLIST" + +cp "$FIXTURE_INFO_PLIST" "$TEMPORARY_DIR/valid-fearlessTests-Info.plist" +printf '%s\n' '' >"$FIXTURE_INFO_PLIST" +CASE_ENV=() +GATE_ARGS=(--stage core --simulator-udid "$SIMULATOR_UDID") +expect_failure "malformed-fixture-info-plist" "Info.plist fixture bridge is missing or unsafe" +mv "$TEMPORARY_DIR/valid-fearlessTests-Info.plist" "$FIXTURE_INFO_PLIST" + +cp "$FIXTURE_INFO_PLIST" "$TEMPORARY_DIR/valid-fearlessTests-Info.plist" +sed 's/[$](FEARLESS_SUBSTRATE_PHONE_STORE_FIXTURE_PATH)/[$](UNREVIEWED_FIXTURE_PATH)/' \ + "$TEMPORARY_DIR/valid-fearlessTests-Info.plist" >"$FIXTURE_INFO_PLIST" +CASE_ENV=() +GATE_ARGS=(--stage core --simulator-udid "$SIMULATOR_UDID") +expect_failure "wrong-fixture-info-plist-setting" "Info.plist fixture bridge is missing or unsafe" +mv "$TEMPORARY_DIR/valid-fearlessTests-Info.plist" "$FIXTURE_INFO_PLIST" + +cp "$FIXTURE_INFO_PLIST" "$TEMPORARY_DIR/valid-fearlessTests-Info.plist" +python3 - \ + "$TEMPORARY_DIR/valid-fearlessTests-Info.plist" \ + "$FIXTURE_INFO_PLIST" <<'PY' +import sys + +source_path, output_path = sys.argv[1:] +with open(source_path, "r", encoding="utf-8") as source: + contents = source.read() +duplicate = """ FearlessSubstratePhoneStoreFixturePath + $(FEARLESS_SUBSTRATE_PHONE_STORE_FIXTURE_PATH) +""" +with open(output_path, "w", encoding="utf-8") as output: + output.write(contents.replace("", duplicate + "")) +PY +CASE_ENV=() +GATE_ARGS=(--stage core --simulator-udid "$SIMULATOR_UDID") +expect_failure "duplicate-fixture-info-plist-key" "Info.plist fixture bridge is missing or unsafe" +rm "$FIXTURE_INFO_PLIST" +mv "$TEMPORARY_DIR/valid-fearlessTests-Info.plist" "$FIXTURE_INFO_PLIST" + RUN_NUMBER=$((RUN_NUMBER + 1)) RUN_DIRECTORY="$TEMPORARY_DIR/run-${RUN_NUMBER}-override-without-harness" mkdir -p "$RUN_DIRECTORY" @@ -522,5 +797,19 @@ assert_output_contains \ printf '%s\n' \ "[coredata-release-gate-test] PASS (rejected): override without harness" +RUN_NUMBER=$((RUN_NUMBER + 1)) +RUN_DIRECTORY="$TEMPORARY_DIR/run-${RUN_NUMBER}-source-packages-override-without-harness" +mkdir -p "$RUN_DIRECTORY" +if env \ + FEARLESS_CORE_DATA_SOURCE_PACKAGES_DIR="$FIXTURE_ROOT/SourcePackages" \ + bash "$GATE" >"$RUN_DIRECTORY/stdout" 2>"$RUN_DIRECTORY/stderr"; then + fail "SourcePackages override without explicit harness unexpectedly passed" +fi +assert_output_contains \ + "FEARLESS_CORE_DATA_SOURCE_PACKAGES_DIR is accepted only with FEARLESS_CORE_DATA_TEST_HARNESS=1" \ + "$RUN_DIRECTORY/stderr" +printf '%s\n' \ + "[coredata-release-gate-test] PASS (rejected): SourcePackages override without harness" + printf '%s\n' \ - "[coredata-release-gate-test] PASS: 3 positive contracts + 30 negative/adversarial cases" + "[coredata-release-gate-test] PASS: 6 positive contracts + 47 negative/adversarial cases" From 5a78687f5b7a3cbdf4347af47049e032966cb914 Mon Sep 17 00:00:00 2001 From: Makoto Takemiya Date: Thu, 2 Jul 2026 01:55:10 +0400 Subject: [PATCH 26/30] Gate iOS transaction builder tests in CI --- .github/workflows/codecov.yml | 5 +++++ scripts/ci/run-pr.sh | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 4030e8f327..a00744f799 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -45,6 +45,11 @@ jobs: bash ./scripts/test-codecov-coverage-contract.sh bash ./scripts/audit-codecov-coverage-contract.sh + - name: Transaction builder coverage audit + run: | + bash ./scripts/test-transaction-builder-tests-audit.sh + bash ./scripts/audit-transaction-builder-tests.sh + - name: Iroha mobile SDK release asset contract env: GH_TOKEN: ${{ github.token }} diff --git a/scripts/ci/run-pr.sh b/scripts/ci/run-pr.sh index d85ea4efbc..bd04cde02f 100755 --- a/scripts/ci/run-pr.sh +++ b/scripts/ci/run-pr.sh @@ -27,6 +27,12 @@ echo "[run-pr] Verifying tracked TestFlight publication evidence" bash "$WORKSPACE_DIR/scripts/test-testflight-publication-readiness-audit.sh" bash "$WORKSPACE_DIR/scripts/audit-testflight-publication-readiness.sh" +if [[ -f "$WORKSPACE_DIR/scripts/audit-transaction-builder-tests.sh" ]]; then + echo "[run-pr] Running transaction builder coverage audit" + bash "$WORKSPACE_DIR/scripts/test-transaction-builder-tests-audit.sh" + bash "$WORKSPACE_DIR/scripts/audit-transaction-builder-tests.sh" +fi + if [[ -f "$WORKSPACE_DIR/scripts/check-iroha-mobile-sdk-release-assets.sh" ]]; then echo "[run-pr] Checking Iroha mobile SDK release asset contract" bash "$WORKSPACE_DIR/scripts/check-iroha-mobile-sdk-release-assets.sh" --self-test From 57002dfb474e52df223fa07be476a15990e8fccb Mon Sep 17 00:00:00 2001 From: Makoto Takemiya Date: Fri, 31 Jul 2026 16:06:43 +0400 Subject: [PATCH 27/30] Consolidate iOS production readiness changes --- .github/workflows/codecov.yml | 36 +- config/iroha-production-send-readiness.json | 117 + config/todo-debt-baseline.tsv | 1 - config/ton-production-send-readiness.json | 73 + docs/iroha-production-send-readiness.md | 166 + docs/release-checklist.md | 117 + docs/ton-production-send-readiness.md | 50 + docs/universal-wallet-v2.md | 13 + fearless.xcodeproj/project.pbxproj | 40 +- .../xcshareddata/xcschemes/fearless.xcscheme | 10 +- .../fearlessIntegrationTests.xcscheme | 6 + fearless/AppDelegate.swift | 2 + .../FeatureToggle/FeatureToggleService.swift | 34 +- .../Transfer/Tokens/TransferService.swift | 597 ++++ .../SafeTransformableValueReader.swift | 76 + .../Rewards/SubqueryRewardsSource.swift | 64 +- .../Rewards/WeaklyAnalyticsRewardSource.swift | 19 +- .../Extension/NumberFormatter+Default.swift | 24 + .../Model/ChainRegistry/ChainModel.swift | 39 +- .../Common/Model/PasskeyBackupContract.swift | 1888 +++++++++++- fearless/Common/Model/TonKeyDerivation.swift | 9 +- .../Model/TonPendingIntentJournal.swift | 479 +++ fearless/Common/Model/TonSendService.swift | 1811 +++++++++++ .../Common/Model/TonTransferFeeQuote.swift | 250 ++ .../Model/TonTransferTransactionBuilder.swift | 861 ++++++ ...niversalWalletAccountAddressResolver.swift | 16 + .../Model/UniversalWalletRegistry.swift | 1 + .../Data/SubqueryDelegatorHistoryData.swift | 4 +- .../Data/SubqueryDelegatorHistoryItem.swift | 52 + .../Subquery/Data/SubqueryRewardData.swift | 18 + .../ParachainSubsquidRewardResponse.swift | 70 +- .../Rewards/RewardOperationFactory.swift | 88 + .../SubqueryRewardOperationFactory.swift | 40 +- .../SubsquidRewardOperationFactory.swift | 109 +- .../Modules/Banners/BannersAssembly.swift | 14 +- .../WalletSendConfirmInteractor.swift | 94 +- .../WalletSendConfirmPresenter.swift | 273 +- .../WalletSendConfirmProtocols.swift | 18 + .../WalletSendConfirmWireframe.swift | 36 +- .../Send/SendDependencyContainer.swift | 153 +- .../AnalyticsValidatorsViewModelFactory.swift | 214 +- .../ControllerAccountViewFactory.swift | 12 +- ...rollerAccountConfirmationViewFactory.swift | 12 +- .../YourValidatorListViewFactory.swift | 12 +- .../StakingAmountViewFactory.swift | 11 +- .../StakingBalanceViewFactory.swift | 12 +- .../StakingBondMoreViewFactory.swift | 12 +- ...akingBondMoreConfirmationViewFactory.swift | 12 +- ...StakingPayoutConfirmationViewFactory.swift | 12 +- ...StakingRebondConfirmationViewFactory.swift | 12 +- .../StakingRebondSetupViewFactory.swift | 12 +- ...StakingRedeemConfirmationViewFactory.swift | 12 +- .../StakingRewardDestConfirmViewFactory.swift | 12 +- .../StakingRewardDestSetupViewFactory.swift | 11 +- .../StakingUnbondConfirmViewFactory.swift | 11 +- .../StakingUnbondSetupViewFactory.swift | 11 +- .../PoolRolesConfirmAssembly.swift | 12 +- fearless/PrivacyInfo.xcprivacy | 23 + fearless/en.lproj/Localizable.strings | 7 + fearless/id.lproj/Localizable.strings | 7 + fearless/ja.lproj/Localizable.strings | 7 + fearless/pt-PT.lproj/Localizable.strings | 7 + fearless/pt.lproj/Localizable.strings | 9 +- fearless/ru.lproj/Localizable.strings | 9 +- fearless/tr.lproj/Localizable.strings | 9 +- fearless/vi.lproj/Localizable.strings | 9 +- fearless/zh-Hans.lproj/Localizable.strings | 9 +- .../TonChainSelectionTests.swift | 2138 +++++++++++-- .../DataProvider/RewardDataSourceTests.swift | 693 ++++- .../FeatureToggleProviderTests.swift | 161 + .../AnalyticsValidatorsTests.swift | 312 +- .../WalletSendConfirmTests.swift | 68 +- ...kupCredentialResponseSerializerTests.swift | 2532 +++++++++++++++ .../TonPendingIntentJournalTests.swift | 275 ++ .../TonTransferTransactionBuilderTests.swift | 2745 +++++++++++++++++ ...salWalletAccountAddressResolverTests.swift | 51 + scripts/audit-codecov-coverage-contract.sh | 2 +- scripts/audit-codecov-workflow.sh | 55 + .../audit-iroha-production-send-readiness.sh | 496 +++ scripts/audit-release-signing-gate.sh | 214 ++ .../audit-ton-production-send-readiness.sh | 563 ++++ scripts/audit-transaction-builder-tests.sh | 292 +- scripts/ci/run-pr.sh | 27 + scripts/ci/run-release-signing-ci.sh | 169 + scripts/ci/run-release-signing-gate.sh | 322 ++ scripts/test-codecov-coverage-contract.sh | 6 +- scripts/test-codecov-workflow-audit.sh | 100 + ...t-iroha-production-send-readiness-audit.sh | 485 +++ scripts/test-release-signing-bootstrap.sh | 141 + scripts/test-release-signing-gate-audit.sh | 180 ++ scripts/test-release-signing-gate.sh | 335 ++ ...est-ton-production-send-readiness-audit.sh | 491 +++ .../test-transaction-builder-tests-audit.sh | 972 +++++- 93 files changed, 21151 insertions(+), 940 deletions(-) create mode 100644 config/iroha-production-send-readiness.json create mode 100644 config/ton-production-send-readiness.json create mode 100644 docs/iroha-production-send-readiness.md create mode 100644 docs/ton-production-send-readiness.md create mode 100644 fearless/Common/Model/TonPendingIntentJournal.swift create mode 100644 fearless/Common/Model/TonSendService.swift create mode 100644 fearless/Common/Model/TonTransferFeeQuote.swift create mode 100644 fearless/Common/Model/TonTransferTransactionBuilder.swift create mode 100644 fearless/PrivacyInfo.xcprivacy create mode 100644 fearlessTests/FeatureToggleProviderTests.swift create mode 100644 fearlessTests/PasskeyBackupCredentialResponseSerializerTests.swift create mode 100644 fearlessTests/TonPendingIntentJournalTests.swift create mode 100644 fearlessTests/TonTransferTransactionBuilderTests.swift create mode 100755 scripts/audit-codecov-workflow.sh create mode 100755 scripts/audit-iroha-production-send-readiness.sh create mode 100755 scripts/audit-release-signing-gate.sh create mode 100755 scripts/audit-ton-production-send-readiness.sh create mode 100755 scripts/ci/run-release-signing-ci.sh create mode 100755 scripts/ci/run-release-signing-gate.sh create mode 100755 scripts/test-codecov-workflow-audit.sh create mode 100755 scripts/test-iroha-production-send-readiness-audit.sh create mode 100755 scripts/test-release-signing-bootstrap.sh create mode 100755 scripts/test-release-signing-gate-audit.sh create mode 100755 scripts/test-release-signing-gate.sh create mode 100755 scripts/test-ton-production-send-readiness-audit.sh diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index a00744f799..8d6726c867 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -10,6 +10,9 @@ on: - develop - master +permissions: + contents: read + jobs: build: runs-on: macOS-latest @@ -22,11 +25,23 @@ jobs: with: persist-credentials: false + - name: Codecov uploader supply-chain audit + run: | + bash ./scripts/test-codecov-workflow-audit.sh + bash ./scripts/audit-codecov-workflow.sh + - name: Branch flow audit run: | bash ./scripts/audit-branch-flow.sh bash ./scripts/test-branch-flow-audit.sh + - name: Release signing gate audit + run: | + bash ./scripts/test-release-signing-gate-audit.sh + bash ./scripts/test-release-signing-gate.sh + bash ./scripts/test-release-signing-bootstrap.sh + bash ./scripts/audit-release-signing-gate.sh + - name: Audit public artifacts run: ./scripts/audit-public-artifacts.sh @@ -50,12 +65,19 @@ jobs: bash ./scripts/test-transaction-builder-tests-audit.sh bash ./scripts/audit-transaction-builder-tests.sh + - name: TON production send readiness + run: | + bash ./scripts/test-ton-production-send-readiness-audit.sh + bash ./scripts/audit-ton-production-send-readiness.sh + - name: Iroha mobile SDK release asset contract env: GH_TOKEN: ${{ github.token }} IROHA_MOBILE_SDK_RELEASE_TAG: ${{ vars.IROHA_MOBILE_SDK_RELEASE_TAG }} IROHA_MOBILE_SDK_RELEASE_REPO: ${{ vars.IROHA_MOBILE_SDK_RELEASE_REPO }} run: | + bash ./scripts/test-iroha-production-send-readiness-audit.sh + bash ./scripts/audit-iroha-production-send-readiness.sh bash ./scripts/check-iroha-mobile-sdk-release-assets.sh --self-test if [[ -n "${IROHA_MOBILE_SDK_RELEASE_TAG:-}" ]]; then bash ./scripts/check-iroha-mobile-sdk-release-assets.sh --download --tag "$IROHA_MOBILE_SDK_RELEASE_TAG" @@ -201,14 +223,22 @@ jobs: exit 1 } + - name: Signed Release archive gate + if: github.event_name == 'push' + env: + IOS_RELEASE_SIGNING_REQUIRED: '1' + IOS_RELEASE_CERTIFICATE_P12_BASE64: ${{ secrets.IOS_RELEASE_CERTIFICATE_P12_BASE64 }} + IOS_RELEASE_CERTIFICATE_PASSWORD: ${{ secrets.IOS_RELEASE_CERTIFICATE_PASSWORD }} + IOS_RELEASE_PROVISIONING_PROFILE_BASE64: ${{ secrets.IOS_RELEASE_PROVISIONING_PROFILE_BASE64 }} + run: bash ./scripts/ci/run-release-signing-ci.sh + - name: Upload coverage to Codecov - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 + uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 with: fail_ci_if_error: true plugins: xcode swift_project: fearless - use_oidc: true - version: v11.3.1 + use_oidc: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} jenkins-pr-merge: name: continuous-integration/jenkins/pr-merge diff --git a/config/iroha-production-send-readiness.json b/config/iroha-production-send-readiness.json new file mode 100644 index 0000000000..d0ef1b8b01 --- /dev/null +++ b/config/iroha-production-send-readiness.json @@ -0,0 +1,117 @@ +{ + "schemaVersion": 2, + "platform": "ios", + "assessedAt": "2026-07-11", + "status": "blocked", + "releaseEnabled": false, + "nexusEnabledByDefault": false, + "upstream": { + "repository": "hyperledger/iroha", + "tag": "v2.0.0-rc.2.1-fearless-mobile-sdk.3", + "commit": "4f8cfbdd17aa6a3b049e619f23ec02501e5297b6", + "releaseUrl": "https://github.com/hyperledger/iroha/releases/tag/v2.0.0-rc.2.1-fearless-mobile-sdk.3" + }, + "artifact": { + "name": "NoritoBridge-v2.0.0-rc.2.1-fearless-mobile-sdk.3.xcframework.zip", + "sha256": "dc944af3dc98d37d349b9f95fe25b9e4a920f095db58adbcccc6354fc28ada4b", + "bytes": 392660196, + "reviewThresholdBytes": 350000000, + "entryCount": 20, + "expandedBytes": 1396110349, + "maxEntryBytes": 704780504, + "compressionRatio": "3.556", + "archiveSafety": "passed-path-type-encryption-crc-and-bomb-bounds", + "delivery": "release-xcframework-requiring-out-of-tree-materialization", + "publishedSliceSha256": { + "ios-arm64": "508b36e1ddc08d3c7488dfbb932d0e37f883908747296b6edab8465efbc3b77c", + "ios-arm64_x86_64-simulator": "b4ec44590205d173259f97a424702ace5d1fa70b469df578fc54a68fd2263b56", + "macos-arm64": "3f13ce287c168b7103b368a67fbf22d1e1d8de0b1b345c8187fae1c206faa60a" + } + }, + "integrationAssessment": { + "appMinimumIOS": "14.1", + "sdkMinimumIOS": "15.0", + "minimumOSCompatible": false, + "productMinimumOSChangeApproved": false, + "sdkLinkageApproved": false, + "taggedPackageDelivery": "path-binary-target-with-symlink-placeholder", + "taggedPackageResolution": "fails-without-separate-dist-materialization", + "taggedCompileToolchain": "Xcode 26.5 / Swift 6.3.2", + "taggedCompileStatus": "failed", + "taggedCompileFailure": "public-default-arguments-call-private-currentEpochSeconds", + "taggedSwiftLoaderExpectedSliceSha256": { + "ios-arm64": "26bb800e9dce021ef38306caef70dbba7928dd99c6612801fb1bbc520b52b7a9", + "ios-arm64_x86_64-simulator": "d0f651e6dc837bff7e92b05c9bf1e3a2988fc6995cabee6e3aaa269a01ecd1b5", + "macos-arm64": "d1dc2069532ff760e03ebf66fdd17811d8d7fa520dcd9f9048b61bbcbcffc3e2" + }, + "publishedSlicesMatchTaggedLoader": false, + "staticLinkDigestVerification": "not-enforced", + "binaryScope": "broad-general-purpose-native-bridge", + "licenseOrNoticeBundled": false, + "sbomBundled": false, + "buildProvenanceBundled": false, + "artifactAttestationBundled": false, + "cryptographicTagSignaturePresent": false, + "reproducibleSourceToBinaryIdentity": "not-proven" + }, + "transactionParity": { + "assessedFixture": "swift_transfer_asset_basic", + "signedFixtureBytes": 576, + "currentMainCompactEntrypointHash": "9756355bec7ca04a9e95025a0f24296a02118a5bf5989cc9c2cec0612bf76201", + "fixedU64EntrypointHash": "c072426bffe62e12fc6b94a0c37ecf4eb2a805965964ce999e5183bddc9a1ce7", + "taggedSwiftRawFixtureHash": "6cc8aa66faa4b067c44831deaf225d8637ffd6daba05b11857b69a06a6b4279b", + "officialTagParityStatus": "stale-noncanonical", + "localCompactCorrectionStatus": "unpublished-unreviewed-diagnostic-only", + "liveReceiptParity": "not-recorded" + }, + "networkReadiness": { + "routeLabel": "iroha3-taira", + "routeLabelCurrentlyPassedAsSigningChainId": true, + "protocolChainIdMapping": "not-authoritatively-confirmed", + "fixtureAssetDefinitionId": "61CtjvNd9T3THAR65GsMVHr82Bjc", + "liveTairaNativeXorDefinitionId": "6TEAJqbb8oEPmLncoNiMRbLEK6tw", + "liveTairaNativeXorScale": 9, + "liveTairaNativeXorAliasPresent": false, + "canonicalAssetMapping": "drifted-authoritative-live-registry-required", + "authoritativeFeePolicy": "absent", + "currentFeeEstimate": "hardcoded-zero", + "liveTairaNodeVersion": "2.0.0-rc.2.0", + "liveTairaNodeCommit": "039af2d65e10b773be5031ab8fc07cf27b40e30d", + "sdkToDeployedNodeCompatibility": "not-proven" + }, + "securityBoundary": { + "defaultSigner": "UnavailableIrohaTransferSigner", + "secretRequestRepresentation": "immutable-swift-string", + "secretCopiesReliablyZeroizable": false, + "secretLifecycleReview": "required-before-sdk-adapter", + "submissionHashPolicy": "local-hash-preferred-without-receipt-equality-check", + "receiptHashEqualityRequired": true, + "acceptedAndFinalizedStatusEvidenceRequired": true + }, + "liveEvidence": { + "fundedTairaBroadcast": "missing", + "fundedNexusBroadcast": "missing", + "localToReceiptHashEquality": "missing", + "acceptedStatus": "missing", + "finalizedStatus": "missing", + "nexusProductionEndpoint": "unconfirmed" + }, + "blocker": { + "code": "apple_xcframework_review_and_materialization_required", + "defaultSigner": "UnavailableIrohaTransferSigner", + "reason": "Archive structure passed, but the official tag is not directly resolvable or compilable, requires iOS 15 while the app supports iOS 14.1, published slice hashes disagree with tagged Swift expectations, canonical transaction parity is stale, binary provenance is incomplete, and authoritative protocol chain, asset, fee, secret-lifecycle, receipt-integrity, deployed-version compatibility, and funded live evidence gates remain open." + }, + "exitCriteria": [ + "publish-a-directly-resolvable-and-compiling-checksum-pinned-swift-package", + "resolve-the-ios-14.1-versus-ios-15-support-decision-without-silent-product-scope-change", + "publish-source-matched-slice-digests-sbom-license-provenance-and-attestation", + "publish-and-review-canonical-compact-swift-transaction-parity", + "authoritatively-map-route-label-to-protocol-chain-id", + "resolve-live-canonical-asset-scale-and-fee-policy-without-hardcoded-fixture-ids", + "replace-string-secret-passing-with-a-reviewed-minimal-lifetime-secret-boundary", + "compare-local-and-torii-transaction-hashes-and-prove-accepted-and-finalized-status", + "prove-sdk-to-deployed-node-wire-and-hash-compatibility", + "replace-the-default-unavailable-signer-only-after-all-gates-pass", + "record-funded-taira-and-nexus-live-broadcast-evidence-before-enablement" + ] +} diff --git a/config/todo-debt-baseline.tsv b/config/todo-debt-baseline.tsv index 7c23a6060b..7f483f41b2 100644 --- a/config/todo-debt-baseline.tsv +++ b/config/todo-debt-baseline.tsv @@ -11,7 +11,6 @@ fearless/Modules/AddAccount/Interactors/AddAccount+AccountConfirmInteractor.swif fearless/Modules/Export/ExportMnemonic/ExportMnemonicPresenter.swift // TODO: Support custom accounts fearless/Modules/PolkaswapFlow/PolkaswapAdjustment/PolkaswapAdjustmentPresenter.swift // TODO: - need think about this fearless/Modules/Profile/ProfileViewFactory.swift // TODO: Eth account info fetching -fearless/Modules/Staking/Analytics/Validators/ViewModel/AnalyticsValidatorsViewModelFactory.swift firstLine: "TODO in next release", fearless/Modules/Staking/SelectValidatorsFlow/ValidatorInfo/Flow/Parachain/ValidatorInfoParachainViewModelFactory.swift // TODO: Has slashes real value fearlessTests/Common/Crypto/KeystoreExportWrapperTests.swift // cryptoType: .sr25519) // FIXME: cryptoType: info.cryptoType ?? .sr25519) fearlessTests/Modules/ControllerAccountConfirmation/ControllerAccountConfirmationTests.swift // TODO diff --git a/config/ton-production-send-readiness.json b/config/ton-production-send-readiness.json new file mode 100644 index 0000000000..3c5833dec5 --- /dev/null +++ b/config/ton-production-send-readiness.json @@ -0,0 +1,73 @@ +{ + "schemaVersion": 1, + "platform": "ios", + "chain": "ton-mainnet", + "assessedAt": "2026-07-16", + "status": "blocked", + "releaseEnabled": false, + "runtimeGuard": { + "policyType": "TonProductionSendReleasePolicy", + "productionInitializer": "isEnabled: false", + "runtimeOverrideAllowed": false, + "debugTestOverrideOnly": true + }, + "feeEvidence": { + "walletVersion": "Wallet V4R2", + "activeMechanism": "tonapi-unsigned-ignore-signature-check-plus-signed-emulation", + "unsignedEndpoint": "/v2/traces/emulate?ignore_signature_check=true", + "signedEndpoint": "/v2/wallet/emulate", + "requiredComparison": "exact-total-fee-nanotons-for-identical-wallet-v4r2-template", + "tonApiParityEvidence": "missing", + "reviewedAttestedReplacement": "not-provided", + "criterionSatisfied": false + }, + "endpointCredentialEvidence": { + "requiredOrigin": "https://tonapi.io", + "credentialContract": "nonempty-visible-ascii-maximum-4096-bytes", + "releaseProvisioningAttestation": "missing", + "registryOriginAttestation": "missing", + "credentialExposureReview": "missing", + "criterionSatisfied": false + }, + "fundedMainnetEvidence": { + "walletVersion": "Wallet V4R2", + "requiredStages": [ + "unsigned-emulation", + "signed-emulation", + "broadcast", + "on-chain-reconciliation", + "exact-fee-verification", + "recipient-credit-verification" + ], + "evidenceBundle": "missing", + "transactionHash": null, + "criterionSatisfied": false + }, + "expiredIntentRecoveryEvidence": { + "requiredPolicy": "finalized-chain-absence-or-explicit-audited-quarantine", + "currentFailClosedBehavior": "block-sender-indefinitely", + "finalizedChainAbsenceProcedure": "missing", + "auditedQuarantineProcedure": "missing", + "selectedProcedure": null, + "senderUnblockAllowed": false, + "criterionSatisfied": false + }, + "releaseEvidenceBundle": { + "immutableBundleId": null, + "sha256": null, + "reviewers": [], + "reviewedAt": null, + "allCriteriaSatisfied": false + }, + "blocker": { + "code": "ton-live-release-evidence-missing", + "reason": "Independent Wallet V4R2 fee-parity or reviewed replacement evidence, exact endpoint credential provisioning attestations, a funded mainnet transfer evidence bundle, and a reviewed finalized-absence or audited-quarantine recovery procedure are not recorded." + }, + "exitCriteria": [ + "record-independent-exact-tonapi-wallet-v4r2-unsigned-signed-fee-parity-or-publish-a-reviewed-attested-local-replacement", + "attest-the-exact-binary-owned-origin-release-credential-provisioning-and-registry-provenance", + "record-an-immutable-funded-mainnet-wallet-v4r2-emulation-broadcast-reconciliation-fee-and-recipient-credit-evidence-bundle", + "approve-a-finalized-chain-absence-procedure-or-an-explicit-audited-quarantine-and-recovery-procedure-for-expired-bearer-records", + "review-the-complete-immutable-evidence-bundle-before-changing-the-production-runtime-policy" + ] +} diff --git a/docs/iroha-production-send-readiness.md b/docs/iroha-production-send-readiness.md new file mode 100644 index 0000000000..12951881f8 --- /dev/null +++ b/docs/iroha-production-send-readiness.md @@ -0,0 +1,166 @@ +# Iroha Production Send Readiness (iOS) + +Status: **BLOCKED / fail closed**. This is not an implemented production-send +claim. SORA Nexus remains `enabledByDefault: false`, and +`IrohaTransferService` defaults to `UnavailableIrohaTransferSigner()`. This +review does not link or materialize the SDK and does not approve changing the +wallet's minimum iOS version. + +## Nexus wallet-smoke metadata boundary + +The reviewed signing request now has an inert, value-typed transaction metadata +slot. Ordinary transfers always carry `.none`, which a future codec must omit +from the transaction. The only non-empty constructor is the operator evidence +hook `submitNexusWalletSmokeEvidence`; it takes an immutable snapshot and accepts +exactly these four string fields: + +```json +{ + "evidence_role": "wallet-smoke", + "route_governance_action_hash": "sha256:<64 lowercase nonzero hex characters>", + "wallet_platform": "ios", + "wallet_commit": "<40 lowercase nonzero git hex characters>" +} +``` + +Missing, extra, case-shifted, control-character, non-ASCII, malformed, and +all-zero placeholder values fail before signer or Torii invocation. The hook is +restricted to the exact canonical `sora:nexus:global` chain identity and the +exact canonical +`https://minamoto.sora.org` Torii endpoint. It is not part of +`TransferServiceProtocol`, does not enable Nexus, and does not install or link a +signer. The production default remains `UnavailableIrohaTransferSigner`, so even +valid evidence metadata fails closed before Torii until the independent SDK, +secret-lifecycle, hash-equality, finality, and funded-live gates below pass. + +## Pinned archive evidence + +The assessed integration target is the official Hyperledger Iroha release +[`v2.0.0-rc.2.1-fearless-mobile-sdk.3`](https://github.com/hyperledger/iroha/releases/tag/v2.0.0-rc.2.1-fearless-mobile-sdk.3), +commit `4f8cfbdd17aa6a3b049e619f23ec02501e5297b6`, published on +2026-06-25. Its Apple bridge delivery is: + +- artifact: `NoritoBridge-v2.0.0-rc.2.1-fearless-mobile-sdk.3.xcframework.zip` +- SHA-256: `dc944af3dc98d37d349b9f95fe25b9e4a920f095db58adbcccc6354fc28ada4b` +- release-manifest size: `392660196` bytes +- 20 ZIP entries, `1396110349` expanded bytes, maximum entry `704780504` + bytes, and expansion ratio `3.556` +- published slice hashes: iOS arm64 + `508b36e1ddc08d3c7488dfbb932d0e37f883908747296b6edab8465efbc3b77c`, + simulator arm64/x86_64 + `b4ec44590205d173259f97a424702ace5d1fa70b469df578fc54a68fd2263b56`, + and macOS arm64 + `3f13ce287c168b7103b368a67fbf22d1e1d8de0b1b345c8187fae1c206faa60a` + +Independent ZIP path, entry-type, encryption, CRC, and size/ratio checks +passed. That proves bounded archive integrity only. The archive is larger than +the repository's `350000000`-byte mandatory review threshold and contains a +broad general-purpose native bridge, not a transfer-only boundary. It bundles +no LICENSE/NOTICE, SBOM, build provenance, or artifact attestation, and its Git +tag has no cryptographic signature. Source-to-binary reproducibility is not +proven. + +## Package, platform, and binary-identity blockers + +The app still supports iOS `14.1`; the tagged `IrohaSwift` package and assessed +binary require iOS `15.0`. Raising the product target is a separate product and +release decision, not an SDK integration detail. + +At the official tag, `IrohaSwift/Package.swift` uses a path binary target at +`../dist/NoritoBridge.xcframework`, calls `fatalError` when it is absent, and +the tag contains only a symlink placeholder rather than a remote checksum +binary target. The package therefore cannot resolve directly from the tag. +After separate materialization, a tagged source compile with Xcode 26.5 / Swift +6.3.2 still fails because public default arguments in +`SorafsReferenceValidators.swift` call the private `currentEpochSeconds()`. + +The tag's `NativeBridge.swift` hard-codes these expected slice hashes: + +- iOS arm64: `26bb800e9dce021ef38306caef70dbba7928dd99c6612801fb1bbc520b52b7a9` +- simulator: `d0f651e6dc837bff7e92b05c9bf1e3a2988fc6995cabee6e3aaa269a01ecd1b5` +- macOS arm64: `d1dc2069532ff760e03ebf66fdd17811d8d7fa520dcd9f9048b61bbcbcffc3e2` + +None matches the corresponding published release slice. The static-linked +package path also does not establish runtime digest enforcement. A checksum +valid outer ZIP therefore cannot establish that tagged Swift source and the +loaded native objects are the same reviewed build. + +## Transaction hash parity blocker + +For the tagged 576-byte `swift_transfer_asset_basic` fixture, the current +native compact External-entrypoint framing produces +`9756355bec7ca04a9e95025a0f24296a02118a5bf5989cc9c2cec0612bf76201`. +Fixed-width `u64` length framing instead produces +`c072426bffe62e12fc6b94a0c37ecf4eb2a805965964ce999e5183bddc9a1ce7`, +while the official tag's Swift parity manifest still records the raw signed +bytes hash +`6cc8aa66faa4b067c44831deaf225d8637ffd6daba05b11857b69a06a6b4279b`. +Those three values are intentionally distinct. + +The active local Iroha worktree contains an unpublished correction that moves +the Swift entrypoint encoders and parity tests to compact framing. It is useful +diagnostic evidence only: it is not in the official tag, does not repair the +published XCFramework's provenance, and has not produced a reviewed release. + +## Protocol and live-state blockers + +The registry label `iroha3-taira` currently also flows into the signing +request's `chainId`. A route label must not be assumed to be the protocol +transaction chain identifier; an authoritative mapping is required before any +signer can be enabled. + +The tag fixture uses asset definition `61CtjvNd9T3THAR65GsMVHr82Bjc`. A live +2026-07-11 Taira read instead reported native XOR as +`6TEAJqbb8oEPmLncoNiMRbLEK6tw`, scale `9`, with no alias. The wallet currently +returns a hard-coded zero fee. Production must resolve the canonical asset, +precision, and authoritative fee policy from reviewed live registry data; it +must not hard-code either fixture or observed identifier as timeless truth. + +The deployed Taira node reported version `2.0.0-rc.2.0` at commit +`039af2d65e10b773be5031ab8fc07cf27b40e30d`, while the SDK tag is commit +`4f8cfbdd17aa6a3b049e619f23ec02501e5297b6`. Wire, registry, fee, and hash +compatibility across that revision gap is not proven. + +## Secret and submission-integrity blockers + +`IrohaTransferSigningRequest` carries mnemonic or seed material in an immutable +Swift `String`. Copies of that value cannot be reliably zeroized. A production +adapter needs a reviewed, minimal-lifetime secret boundary, local key/account +binding, best-effort cleanup of every mutable copy, and explicit acceptance of +any provider-owned residual copy. + +The current unreachable submission seam prefers a locally supplied hash, then +falls back to either receipt hash, without requiring equality. It also returns +after submission without proving accepted and finalized status. Before +enablement, the locally computed canonical hash must exactly match Torii's +receipt, disagreement or missing hashes must fail closed, and accepted plus +finalized status must be independently recorded. + +No funded Taira broadcast, funded Nexus broadcast, local/receipt hash-equality +record, accepted-status record, finalized-status record, or confirmed Nexus +production endpoint is present. Archive validation and local fixtures cannot +substitute for those live artifacts. + +## Enforced blocker + +The machine-readable state is +`config/iroha-production-send-readiness.json`. The blocker code remains +`apple_xcframework_review_and_materialization_required`. Run the executable +gates directly: + +```bash +./scripts/test-iroha-production-send-readiness-audit.sh +./scripts/audit-iroha-production-send-readiness.sh +``` + +The audit fails if the manifest changes without review, Nexus becomes enabled +by default, the unavailable signer is replaced or bypassed, iOS 14.1 support is +silently raised, an IrohaSwift or NoritoBridge dependency/binary is added, the +known route-label, secret, zero-fee, or receipt-integrity risks are obscured, +negative signer tests disappear, the exact four-field wallet-smoke metadata +boundary weakens, or exact evidence markers drift. + +`check-iroha-mobile-sdk-release-assets.sh` proves release-asset integrity only. +It does **not** make Iroha send production-ready. The unavailable signer and +disabled Nexus default must remain until every manifest exit criterion and the +funded live evidence gates pass in a newly reviewed release. diff --git a/docs/release-checklist.md b/docs/release-checklist.md index 697eaef0fc..e2c4522fed 100644 --- a/docs/release-checklist.md +++ b/docs/release-checklist.md @@ -13,6 +13,10 @@ Use this checklist for every release PR from `develop` to `master`. - Run `./scripts/audit-public-artifacts.sh` and confirm it passes. - Run `bash ./scripts/test-todo-debt-audit.sh && bash ./scripts/audit-todo-debt.sh` and confirm no new TODO/FIXME/STOPSHIP debt was introduced. +- Run + `bash ./scripts/test-codecov-workflow-audit.sh && bash ./scripts/audit-codecov-workflow.sh` + and confirm every third-party action remains pinned to its reviewed commit, + Codecov uploads use OIDC, and coverage failures remain release-blocking. - Run `bash ./scripts/deps/check-dependency-contracts.sh "$PWD"` and confirm the committed SwiftPM state, native crypto contract wiring, and shared-features patch wiring still fail fast before release. @@ -38,9 +42,106 @@ Use this checklist for every release PR from `develop` to `master`. existing app container, five cold launches pass, store integrity is unchanged, and the remaining third-party symbolication follow-up is closed or explicitly accepted by release review. +- Run `bash ./scripts/test-coredata-release-gate.sh`, then run + `bash ./scripts/ci/run-coredata-release-gate.sh --stage core --simulator-udid `. + Require an optimized Release (`-O`) result with zero failures, skips, or + expected failures. The test scheme must keep `-UNITTEST` enabled so the + hosted app cannot open an unrelated simulator store. +- On a raw, read-only copy of the latest pre-upgrade phone Substrate store, run + `bash ./scripts/ci/run-coredata-release-gate.sh --stage copied-phone --simulator-udid --fixture `. + Require exactly two tests and unchanged source-store fingerprints. Never run + this gate against the live phone container. +- Build the normal Release app without `ENABLE_TESTABILITY=YES`. Verify its + executable retains every managed-object runtime class required by the bundled + Substrate and User models and that both User compatibility `.mom` resources + and every historical migration model/mapping are present. Install that exact + app on a fresh disposable simulator containing raw copies of the complete + store families. Run `bash ./scripts/ci/run-coredata-simulator-rehearsal.sh` + and require first launch and relaunch to each emit exactly one + `FEARLESS_STARTUP_READY` marker and no `FEARLESS_STARTUP_FAILED`, crash, or + fatal Core Data/migration marker. Require exact protected row-count + preservation plus unchanged wallet identity, key, and relationship + fingerprints; the source fixture must remain byte-for-byte unchanged. +- Before archive/upload, install the same Release build in place on the + designated physical upgrade device only after taking and verifying a fresh + backup. Do not uninstall or clear app data. Require first launch, background/ + foreground, forced relaunch, wallet unlock, and basic read-only navigation to + pass while the backup remains independently restorable. +- Run + `bash ./scripts/test-ton-production-send-readiness-audit.sh && bash ./scripts/audit-ton-production-send-readiness.sh` + and confirm `config/ton-production-send-readiness.json` remains `blocked` + with `releaseEnabled` set to `false`. This evidence gate is required in + addition to the transaction-builder fixture audit; it must not be skipped for + a release archive. +- Keep `TonProductionSendReleasePolicy.production` hard-disabled. Native TON + send remains unavailable even though the local implementation now enforces: + - a versioned, bounded, canonical Keychain journal persists the exact signed + bearer BOC before signed emulation; pending intents survive process + termination, validate their signature/template/quote on load, and reconcile + before any new broadcast; ordinary confirmation never auto-rebroadcasts an + older pending BOC, and recovery runs before mnemonic/key access; + - journal phases are monotonic, and a confirmed terminal tombstone remains in + the journal until the success controller has actually been presented and the + UI explicitly acknowledges the exact intent identity and message hash; + friendly sender addresses are canonicalized internally, so termination + between reconciliation and user-visible success cannot authorize a duplicate; + - confirmation binds a fresh 30-second fee quote to the exact sender, + recipient, amount, bounce flag, body, public key, sequence number, state-init, + expiry, endpoint origin, unsigned signing payload, signed BOC, and exact + signed-emulation fee; stale, mutated, state-drifted, and fee-drifted quotes + fail closed; + - the confirmation screen starts disabled for TON, reuses one serialized + cached transfer service, and activates a quote only after the exact + nine-decimal TON fee and opaque quote ID have been applied to the visible + view; the compatibility `estimateFee` API can never activate a quote, and + unsubscribe revokes the quote synchronously; + - recovery that confirms an older, different intent is surfaced as a distinct + previous-transfer receipt, never as success for the new transfer; the user + must visibly acknowledge the old exact identity/hash and receive a fresh fee + before confirming the new transfer; + - exact-hash unknown outcomes have explicit user-visible recovery UX and copy + the exact lowercase 32-byte message hash; + - signed emulation, broadcast, and reconciliation are restricted to an + immutable reviewed endpoint allowlist with exact origin and a bounded, + visible-ASCII provisioned authorization credential, rather than any selected + HTTPS chain node; persisted recovery additionally requires the current + credentialed origin to equal the stored quote origin. + Production enablement still requires one immutable release evidence set proving: + - current TonAPI schemas pass the bounded transport, redirect, intent-binding, + memo-required, journal-recovery, retry, expiry, and reconciliation adversarial + suites, and independently establish that `ignore_signature_check` unsigned + emulation has exact fee parity with the corresponding signed Wallet V4R2 + template (or replace it with a reviewed attested quote/local-TVM mechanism); + - the exact binary-owned send origin, credentials, and registry provenance are + independently reviewed and provisioned; and + - a reviewed finalized-chain absence proof or explicit audited quarantine and + recovery workflow exists for expired, never-confirmed bearer records; the + current fail-closed journal intentionally blocks that sender indefinitely + rather than guessing absence, and storage atomicity must be reviewed if any + second process or app extension can access the same Keychain namespace; and + - a funded mainnet Wallet V4R2 transfer has independently verified emulation, + broadcast, on-chain reconciliation, fee, and recipient credit evidence. + Unit and fixture tests alone do not authorize production TON send. - Run `bash ./scripts/check-iroha-mobile-sdk-release-assets.sh --self-test`. If `IROHA_MOBILE_SDK_RELEASE_TAG` is configured for the release, also run `bash ./scripts/check-iroha-mobile-sdk-release-assets.sh --download --tag "$IROHA_MOBILE_SDK_RELEASE_TAG"`. + Then run + `bash ./scripts/test-iroha-production-send-readiness-audit.sh && bash ./scripts/audit-iroha-production-send-readiness.sh`. + Artifact validation does not unblock send: keep the default unavailable + signer and Nexus disabled while + `config/iroha-production-send-readiness.json` is `blocked`. + For any future iOS Iroha enablement review, require all of the following in + the same immutable release evidence set: + - an explicit iOS 14.1 versus SDK iOS 15 support decision; + - a directly resolvable, compiling Swift package whose source expectations + match every published XCFramework slice digest; + - canonical compact transaction-hash parity and source/binary provenance; + - authoritative protocol chain ID, live asset/scale, and fee-policy mapping; + - a reviewed zeroizable secret-lifecycle boundary; + - exact local/Torii receipt-hash equality plus accepted and finalized status; + - deployed-node compatibility and funded Taira and Nexus broadcast evidence. + A local unpublished upstream correction is diagnostic only and cannot satisfy + a release criterion. - From the workspace root, run `bash scripts/audit-passkey-backup-prerequisites.sh` and confirm `config/passkey-backup-production.json` still matches iOS passkey code, @@ -53,6 +154,22 @@ Use this checklist for every release PR from `develop` to `master`. iCloud account availability, CloudKit production schema readiness, associated-domain provisioning, provisioning profiles, and a recovery path that offers restore before creating a new backup. +- Keep the default `UnavailableBackupAuthorizationProvider` in place + until a reviewed issuer supplies one-time, exact-request-body-bound grants + backed by production App Attest/provisioning evidence. Its authorization + subject must represent stable Fearless wallet ownership across iOS and + Android, not an Apple account or device identifier. Confirm missing, + malformed, replayed, wrong-body, and wrong-subject grants fail before any + challenge is issued. +- Keep `UnavailablePasskeyBackupKeyProvider` in place until product and + security approve a wallet-owned, cross-device recovery source for an exact + 32-byte backup key. A device-local Keychain key is not a cross-device + recovery key. Verify loss/replacement-device recovery plus wrong-key, + tamper, metadata-swap, truncation, and nonce-uniqueness tests for the + canonical AES-256-GCM envelope before enabling the flag. +- Verify credential list, single revoke, and revoke-all use exact-body-bound + grants. Deletion must durably revoke all server credentials before removing + the CloudKit record; a revoke failure must leave the encrypted record intact. - Confirm public build instructions still work without private overlays. - When the private iOS overlay checkout is available, run `PRIVATE_OVERLAY_REPORT=build/reports/private-overlay-boundary.tsv PRIVATE_REPO_DIR=../fearless-iOS-priv ./scripts/audit-private-overlay-boundary.sh` diff --git a/docs/ton-production-send-readiness.md b/docs/ton-production-send-readiness.md new file mode 100644 index 0000000000..49e0ca3a61 --- /dev/null +++ b/docs/ton-production-send-readiness.md @@ -0,0 +1,50 @@ +# TON Production Send Readiness (iOS) + +Status: **BLOCKED / fail closed**. This document and +`config/ton-production-send-readiness.json` record the evidence that is still +missing; they do not authorize production TON send. The Release policy remains +`TonProductionSendReleasePolicy(isEnabled: false)`, with no environment, +remote-config, or runtime override. + +## Required release evidence + +One immutable, independently reviewed evidence bundle must satisfy every item +below before the runtime policy can be reconsidered: + +1. **Wallet V4R2 fee parity.** For the identical Wallet V4R2 template, record + exact `totalFeeNanotons` parity between TonAPI unsigned emulation at + `/v2/traces/emulate?ignore_signature_check=true` and signed emulation at + `/v2/wallet/emulate`. The only acceptable alternative is a separately + reviewed and attested local-TVM or quote mechanism that does not trust an + unsigned signature-bypass result as authorization. +2. **Endpoint and credential provisioning.** Attest that the Release binary's + exact origin is `https://tonapi.io`, the registry resolves to that reviewed + origin, and a nonempty visible-ASCII credential of at most 4096 bytes is + provisioned through the reviewed release-secret path without committing or + logging the credential. +3. **Funded mainnet transfer.** Record a real funded mainnet Wallet V4R2 transfer + with unsigned emulation, signed emulation, broadcast, exact-hash on-chain + reconciliation, exact fee verification, and recipient-credit verification. + Unit tests, fixtures, testnet transactions, and screenshots alone do not + satisfy this criterion. +4. **Expired bearer recovery.** Approve either a finalized-chain absence + procedure or an explicit audited quarantine and recovery procedure before + an expired, never-confirmed bearer record may release its sender. The current + intentional behavior is to block that sender indefinitely rather than guess + that the message is absent. + +## Enforced blocker + +Run the executable contract directly: + +```bash +bash ./scripts/test-ton-production-send-readiness-audit.sh +bash ./scripts/audit-ton-production-send-readiness.sh +``` + +The audit pins the blocked manifest digest, validates its exact schema, checks +the Release runtime kill switch and exact authenticated TonAPI origin, and +requires both PR and GitHub CI to run the adversarial self-test and audit. Any +evidence claim or enablement change therefore requires a deliberate, reviewed +update to the manifest, documentation, audit, tests, and runtime policy in the +same release change. diff --git a/docs/universal-wallet-v2.md b/docs/universal-wallet-v2.md index e329640a38..6eef8bf208 100644 --- a/docs/universal-wallet-v2.md +++ b/docs/universal-wallet-v2.md @@ -158,6 +158,11 @@ transaction simulation, and other write operations must use RPC or Torii endpoints, never a public indexer endpoint. - TON indexer base URL: `https://ti.soramitsu.io`. +- Native TON Wallet V4R2 building, unsigned fee emulation, signed emulation, + bounded TonAPI transport, broadcast, and exact-message reconciliation are + implemented on iOS, but the production send route remains hard-disabled. + Enabling it requires the durable pending-intent, confirmation/unknown-outcome + UX, current-schema, and funded-mainnet evidence in `docs/release-checklist.md`. - Solana indexer base URL: `https://si.soramitsu.io`. - `si.soramitsu.io` is read-only. Transaction simulation and broadcast use the configured Solana RPC endpoint directly. @@ -166,6 +171,14 @@ endpoints, never a public indexer endpoint. - Nexus mainnet uses I105 chain discriminant `753` and chain id `sora:nexus:global`, but remains registry-gated until the production Torii/TLS endpoint is confirmed. +- Iroha `features: ["transfer"]` is capability metadata, not a production-send + enablement claim. iOS send remains fail closed under + `config/iroha-production-send-readiness.json`; see + `docs/iroha-production-send-readiness.md` for the pinned upstream blocker. +- The iOS Nexus operator evidence seam snapshots an exact four-string + `wallet-smoke` metadata object and rejects malformed or placeholder hashes + before signing. Ordinary wallet transfers omit metadata, and the seam does + not alter the unavailable production signer or disabled Nexus default. ## Normalized Indexer Contract diff --git a/fearless.xcodeproj/project.pbxproj b/fearless.xcodeproj/project.pbxproj index 0b2b2c72eb..0187a41d13 100644 --- a/fearless.xcodeproj/project.pbxproj +++ b/fearless.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + FA714002ABCDEF0123456789 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = FA714001ABCDEF0123456789 /* PrivacyInfo.xcprivacy */; }; 002561414AF1F8F3B4B65538 /* WalletTransactionDetailsWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7416F3AFA5F4D1130B1C410 /* WalletTransactionDetailsWireframe.swift */; }; 0077B6854FDCA7ECCD557B09 /* StakingPoolCreateViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D651E438F86F37CC07D6D3F /* StakingPoolCreateViewController.swift */; }; 00E9DD69FCE94A4F4929B419 /* AccountStatisticsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BF646F59913E95891915BDC /* AccountStatisticsProtocols.swift */; }; @@ -2977,6 +2978,15 @@ FAE601882ECA700100000001 /* BitcoinTransactionHistorySyncTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE601892ECA700100000001 /* BitcoinTransactionHistorySyncTests.swift */; }; FAE6018A2ECA700100000001 /* BitcoinSendService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE6018B2ECA700100000001 /* BitcoinSendService.swift */; }; FAE6018C2ECA700100000001 /* BitcoinSendServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE6018D2ECA700100000001 /* BitcoinSendServiceTests.swift */; }; + FAE602002ECA700100000001 /* PasskeyBackupCredentialResponseSerializerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE602012ECA700100000001 /* PasskeyBackupCredentialResponseSerializerTests.swift */; }; + FAE602022ECA700100000001 /* FeatureToggleProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE602032ECA700100000001 /* FeatureToggleProviderTests.swift */; }; + FAE602042ECA710100000001 /* TonTransferTransactionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE602052ECA710100000001 /* TonTransferTransactionBuilder.swift */; }; + FAE602062ECA710100000001 /* TonTransferTransactionBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE602072ECA710100000001 /* TonTransferTransactionBuilderTests.swift */; }; + FAE602082ECA710100000001 /* TonSendService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE602092ECA710100000001 /* TonSendService.swift */; }; + FAE6020A2ECA710100000001 /* TonTransferFeeQuote.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE6020B2ECA710100000001 /* TonTransferFeeQuote.swift */; }; + FAE6020C2ECA710100000001 /* TonPendingIntentJournal.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE6020D2ECA710100000001 /* TonPendingIntentJournal.swift */; }; + FAE6020E2ECA710100000001 /* TonPendingIntentJournalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE6020F2ECA710100000001 /* TonPendingIntentJournalTests.swift */; }; + FAE602102ECA710100000001 /* TonConstansts.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE26E7032DFA1BB4001FD062 /* TonConstansts.swift */; }; FAE6018E2ECA700100000001 /* SolanaBalanceSync.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE6018F2ECA700100000001 /* SolanaBalanceSync.swift */; }; FAE601902ECA700100000001 /* SolanaBalanceSyncTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE601912ECA700100000001 /* SolanaBalanceSyncTests.swift */; }; FAE601922ECA700100000001 /* SolanaTransactionHistorySync.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE601932ECA700100000001 /* SolanaTransactionHistorySync.swift */; }; @@ -4042,6 +4052,7 @@ 849013AB24A80984008F705E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 849013B424A80986008F705E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 849013B924A80986008F705E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + FA714001ABCDEF0123456789 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 849013BE24A80986008F705E /* fearlessTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = fearlessTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 849013C424A80986008F705E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 849013CF24A9267F008F705E /* R.generated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = R.generated.swift; sourceTree = SOURCE_ROOT; }; @@ -4456,7 +4467,7 @@ 9FDEB6F26DCFB53EA1547948 /* SafeTransformableValueReader.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SafeTransformableValueReader.swift; sourceTree = ""; }; 9FED48DE9B681995E6E4A581 /* LiquidityPoolDetailsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiquidityPoolDetailsProtocols.swift; sourceTree = ""; }; A14CA4551FCC2EBD078E2242 /* AccountConfirmViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountConfirmViewFactory.swift; sourceTree = ""; }; - A1B2C3D4E5F60718293A4B5D /* TonChainSelectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = fearlessTests/ApplicationLayer/Services/FeatureToggle/TonChainSelectionTests.swift; sourceTree = ""; }; + A1B2C3D4E5F60718293A4B5D /* TonChainSelectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = fearlessTests/ApplicationLayer/Services/FeatureToggle/TonChainSelectionTests.swift; sourceTree = SOURCE_ROOT; }; A3104ABC4BECF08B0BA836AA /* AccountConfirmViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountConfirmViewController.swift; sourceTree = ""; }; A31780E84948D7FE632ECB02 /* YourValidatorListProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = YourValidatorListProtocols.swift; sourceTree = ""; }; A3BACB7E24BC87F9218DBBC4 /* StakingPayoutConfirmationViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingPayoutConfirmationViewController.swift; sourceTree = ""; }; @@ -6127,6 +6138,14 @@ FAE601892ECA700100000001 /* BitcoinTransactionHistorySyncTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitcoinTransactionHistorySyncTests.swift; sourceTree = ""; }; FAE6018B2ECA700100000001 /* BitcoinSendService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitcoinSendService.swift; sourceTree = ""; }; FAE6018D2ECA700100000001 /* BitcoinSendServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitcoinSendServiceTests.swift; sourceTree = ""; }; + FAE602012ECA700100000001 /* PasskeyBackupCredentialResponseSerializerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasskeyBackupCredentialResponseSerializerTests.swift; sourceTree = ""; }; + FAE602032ECA700100000001 /* FeatureToggleProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureToggleProviderTests.swift; sourceTree = ""; }; + FAE602052ECA710100000001 /* TonTransferTransactionBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TonTransferTransactionBuilder.swift; sourceTree = ""; }; + FAE602072ECA710100000001 /* TonTransferTransactionBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TonTransferTransactionBuilderTests.swift; sourceTree = ""; }; + FAE602092ECA710100000001 /* TonSendService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TonSendService.swift; sourceTree = ""; }; + FAE6020B2ECA710100000001 /* TonTransferFeeQuote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TonTransferFeeQuote.swift; sourceTree = ""; }; + FAE6020D2ECA710100000001 /* TonPendingIntentJournal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TonPendingIntentJournal.swift; sourceTree = ""; }; + FAE6020F2ECA710100000001 /* TonPendingIntentJournalTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TonPendingIntentJournalTests.swift; sourceTree = ""; }; FAE6018F2ECA700100000001 /* SolanaBalanceSync.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SolanaBalanceSync.swift; sourceTree = ""; }; FAE601912ECA700100000001 /* SolanaBalanceSyncTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SolanaBalanceSyncTests.swift; sourceTree = ""; }; FAE601932ECA700100000001 /* SolanaTransactionHistorySync.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SolanaTransactionHistorySync.swift; sourceTree = ""; }; @@ -9137,6 +9156,7 @@ 849013B424A80986008F705E /* Assets.xcassets */, 849013B624A80986008F705E /* LaunchScreen.storyboard */, 849013B924A80986008F705E /* Info.plist */, + FA714001ABCDEF0123456789 /* PrivacyInfo.xcprivacy */, 84C07EDA24AF1924008FC35B /* InfoPlist.strings */, 8490146024A94373008F705E /* Localizable.strings */, AE74EC1725FCF09000C494E7 /* Localizable.stringsdict */, @@ -9191,6 +9211,10 @@ FAE601552ECA700100000001 /* UniversalWalletSigningContractTests.swift */, FAE601592ECA700100000001 /* UniversalWalletMigrationContractTests.swift */, FAE6015D2ECA700100000001 /* UniversalWalletRegistryContractTests.swift */, + FAE602012ECA700100000001 /* PasskeyBackupCredentialResponseSerializerTests.swift */, + FAE602032ECA700100000001 /* FeatureToggleProviderTests.swift */, + FAE602072ECA710100000001 /* TonTransferTransactionBuilderTests.swift */, + FAE6020F2ECA710100000001 /* TonPendingIntentJournalTests.swift */, ); path = fearlessTests; sourceTree = ""; @@ -9452,6 +9476,10 @@ FAE601272ECA700100000001 /* IrohaKeyDerivation.swift */, FAE601292ECA700100000001 /* TonAddressCodec.swift */, FAE6012B2ECA700100000001 /* TonKeyDerivation.swift */, + FAE602052ECA710100000001 /* TonTransferTransactionBuilder.swift */, + FAE602092ECA710100000001 /* TonSendService.swift */, + FAE6020B2ECA710100000001 /* TonTransferFeeQuote.swift */, + FAE6020D2ECA710100000001 /* TonPendingIntentJournal.swift */, FAE6012D2ECA700100000001 /* TonIndexerContract.swift */, FAE601312ECA700100000001 /* IrohaToriiContract.swift */, FAE601352ECA700100000001 /* IrohaToriiClient.swift */, @@ -15832,6 +15860,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + FA714002ABCDEF0123456789 /* PrivacyInfo.xcprivacy in Resources */, 8490142324A93027008F705E /* sora-Bold.otf in Resources */, 841937872544772F00CFA50C /* animatedBg.gif in Resources */, 8490145E24A94373008F705E /* Localizable.strings in Resources */, @@ -17263,6 +17292,11 @@ FAE601262ECA700100000001 /* IrohaKeyDerivation.swift in Sources */, FAE601282ECA700100000001 /* TonAddressCodec.swift in Sources */, FAE6012A2ECA700100000001 /* TonKeyDerivation.swift in Sources */, + FAE602042ECA710100000001 /* TonTransferTransactionBuilder.swift in Sources */, + FAE602082ECA710100000001 /* TonSendService.swift in Sources */, + FAE6020A2ECA710100000001 /* TonTransferFeeQuote.swift in Sources */, + FAE6020C2ECA710100000001 /* TonPendingIntentJournal.swift in Sources */, + FAE602102ECA710100000001 /* TonConstansts.swift in Sources */, FAE6012C2ECA700100000001 /* TonIndexerContract.swift in Sources */, FAE601302ECA700100000001 /* IrohaToriiContract.swift in Sources */, FAE601342ECA700100000001 /* IrohaToriiClient.swift in Sources */, @@ -19048,6 +19082,10 @@ FAE601542ECA700100000001 /* UniversalWalletSigningContractTests.swift in Sources */, FAE601582ECA700100000001 /* UniversalWalletMigrationContractTests.swift in Sources */, FAE6015C2ECA700100000001 /* UniversalWalletRegistryContractTests.swift in Sources */, + FAE602002ECA700100000001 /* PasskeyBackupCredentialResponseSerializerTests.swift in Sources */, + FAE602022ECA700100000001 /* FeatureToggleProviderTests.swift in Sources */, + FAE602062ECA710100000001 /* TonTransferTransactionBuilderTests.swift in Sources */, + FAE6020E2ECA710100000001 /* TonPendingIntentJournalTests.swift in Sources */, 8467FD3924EACE08005D486C /* AccountCreationHelper.swift in Sources */, 8476CA0625EFB072003EEAE1 /* SingleValueProviderFactoryStub.swift in Sources */, FAD428A82A865636001D6A16 /* BackupPasswordTests.swift in Sources */, diff --git a/fearless.xcodeproj/xcshareddata/xcschemes/fearless.xcscheme b/fearless.xcodeproj/xcshareddata/xcschemes/fearless.xcscheme index 732d2d2027..8adbc75d17 100644 --- a/fearless.xcodeproj/xcshareddata/xcschemes/fearless.xcscheme +++ b/fearless.xcodeproj/xcshareddata/xcschemes/fearless.xcscheme @@ -11,7 +11,15 @@ +set -eu +export SOURCE_PACKAGES_DIR=\"${PROJECT_DIR}/SourcePackages\" +export ALLOW_DERIVEDDATA_FALLBACK=0 +export STRICT_REQUIRED_PATCHES=1 + +if ! bash \"${PROJECT_DIR}/scripts/spm-shared-features-fixes.sh\" \"${PROJECT_DIR}\"; then + echo \"error: Required shared-features compatibility fixes failed. Resolve packages into ${SOURCE_PACKAGES_DIR}, review the diagnostics above, and retry the archive.\" \>\&2 + exit 1 +fi"> diff --git a/fearless.xcodeproj/xcshareddata/xcschemes/fearlessIntegrationTests.xcscheme b/fearless.xcodeproj/xcshareddata/xcschemes/fearlessIntegrationTests.xcscheme index d07f1c83e7..1eea8aa56a 100644 --- a/fearless.xcodeproj/xcshareddata/xcschemes/fearlessIntegrationTests.xcscheme +++ b/fearless.xcodeproj/xcshareddata/xcschemes/fearlessIntegrationTests.xcscheme @@ -34,6 +34,12 @@ debugDocumentVersioning = "YES" debugServiceExtension = "internal" allowLocationSimulation = "YES"> + + + + Bool { + FailClosedSecureUnarchiveFromDataTransformer.register() + guard !isUnitTesting else { return true } let rootWindow = FearlessWindow() diff --git a/fearless/ApplicationLayer/Services/FeatureToggle/FeatureToggleService.swift b/fearless/ApplicationLayer/Services/FeatureToggle/FeatureToggleService.swift index a4fcd9940d..e02b07877b 100644 --- a/fearless/ApplicationLayer/Services/FeatureToggle/FeatureToggleService.swift +++ b/fearless/ApplicationLayer/Services/FeatureToggle/FeatureToggleService.swift @@ -19,6 +19,7 @@ final class FeatureToggleProvider { private let networkOperationFactory: NetworkOperationFactoryProtocol private let operationQueue: OperationQueue + private let stateLock = NSLock() private(set) var snapshot: FeatureToggleConfig? private(set) var pendingRequests: [PendingRequest] = [] @@ -42,7 +43,8 @@ final class FeatureToggleProvider { throw FeatureToggleServiceError.urlBroken } - let fetchConfigOperation: BaseOperation = networkOperationFactory.fetchData(from: featureToggleURL) + let fetchConfigOperation: BaseOperation = + networkOperationFactory.fetchData(from: featureToggleURL) fetchConfigOperation.completionBlock = { [weak self] in self?.handleCompletion(result: fetchConfigOperation.result) @@ -57,10 +59,13 @@ final class FeatureToggleProvider { ) { let request = PendingRequest(resultClosure: closure, queue: queue) + stateLock.lock() if let snapshot = snapshot { + stateLock.unlock() deliver(snapshot: snapshot, to: request) } else { pendingRequests.append(request) + stateLock.unlock() } } @@ -68,8 +73,9 @@ final class FeatureToggleProvider { switch result { case let .success(snapshot): if let snapshot = snapshot { - self.snapshot = snapshot - resolveRequests() + complete(with: snapshot) + } else { + handleDefault() } case .failure: handleDefault() @@ -79,17 +85,15 @@ final class FeatureToggleProvider { } private func handleDefault() { - snapshot = FeatureToggleConfig.defaultConfig - resolveRequests() + complete(with: FeatureToggleConfig.defaultConfig) } - private func resolveRequests() { - guard !pendingRequests.isEmpty, let snapshot = snapshot else { - return - } - + private func complete(with snapshot: FeatureToggleConfig) { + stateLock.lock() + self.snapshot = snapshot let requests = pendingRequests pendingRequests = [] + stateLock.unlock() requests.forEach { deliver(snapshot: snapshot, to: $0) } } @@ -104,9 +108,13 @@ final class FeatureToggleProvider { extension FeatureToggleProvider: FeatureToggleProviderProtocol { func fetchConfigOperation() -> BaseOperation { AwaitOperation { [weak self] in - try await withCheckedThrowingContinuation { continuation in - self?.fetchConfig(runCompletionIn: nil) { factory in - continuation.resume(with: .success(factory)) + guard let self else { + return FeatureToggleConfig.defaultConfig + } + + return await withCheckedContinuation { continuation in + self.fetchConfig(runCompletionIn: nil) { config in + continuation.resume(returning: config) } } } diff --git a/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift b/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift index b0c256825c..a310f3cf03 100644 --- a/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift +++ b/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift @@ -3,15 +3,23 @@ import SSFModels import SSFExtrinsicKit import SSFUtils import BigInt +import TonSwift protocol TransferFeeEstimationListener: AnyObject { func didReceiveFee(fee: BigUInt) func didReceiveFeeError(feeError: Error) } +protocol TonTransferFeePresentationListener: TransferFeeEstimationListener { + func didReceiveTonFee(fee: BigUInt, presentationID: String) +} + enum TransferServiceError: Error { case cannotEstimateFee(reason: String) case transferFailed(reason: String) + case tonProductionSendDisabled + case tonPriorTransferConfirmed(identity: TonTransferIntentIdentity, messageHashHex: String) + case tonBroadcastOutcomeUnknown(messageHashHex: String) case unexpected } @@ -28,6 +36,12 @@ protocol TransferServiceProtocol { func submit(transfer: Transfer) async throws -> String func subscribeForFee(transfer: Transfer, listener: TransferFeeEstimationListener) func unsubscribe() + func confirmFeePresentation(id: String, fee: BigUInt) async -> Bool + func acknowledgeSubmittedTransfer(hash: String, transfer: Transfer) async -> Bool + func acknowledgeRecoveredTransfer( + hash: String, + identity: TonTransferIntentIdentity + ) async -> Bool func estimateFee(for transfer: XorlessTransfer) async throws -> BigUInt func submit(transfer: XorlessTransfer) async throws -> String @@ -36,6 +50,12 @@ protocol TransferServiceProtocol { extension TransferServiceProtocol { func estimateFee(for _: XorlessTransfer) async throws -> BigUInt { .zero } func submit(transfer _: XorlessTransfer) async throws -> String { "" } + func confirmFeePresentation(id _: String, fee _: BigUInt) async -> Bool { true } + func acknowledgeSubmittedTransfer(hash _: String, transfer _: Transfer) async -> Bool { true } + func acknowledgeRecoveredTransfer( + hash _: String, + identity _: TonTransferIntentIdentity + ) async -> Bool { true } } final class BitcoinTransferService: TransferServiceProtocol { @@ -222,6 +242,433 @@ final class BitcoinTransferService: TransferServiceProtocol { } } +private final class TonTransferFeeQuoteStore: @unchecked Sendable { + private let lock = NSLock() + private var generation: UInt64 = 0 + private var pendingIdentity: TonTransferIntentIdentity? + private var quote: TonTransferFeeQuote? + private var readyForSubmission = false + + func begin(identity: TonTransferIntentIdentity) -> UInt64 { + lock.lock() + defer { lock.unlock() } + generation = generation == UInt64.max ? 1 : generation + 1 + pendingIdentity = identity + quote = nil + readyForSubmission = false + return generation + } + + func commit( + _ quote: TonTransferFeeQuote, + identity: TonTransferIntentIdentity, + generation expectedGeneration: UInt64, + readyForSubmission: Bool + ) -> Bool { + lock.lock() + defer { lock.unlock() } + guard generation == expectedGeneration, + pendingIdentity == identity, + quote.identity == identity + else { + return false + } + self.quote = quote + self.readyForSubmission = readyForSubmission + return true + } + + func fail(generation expectedGeneration: UInt64) { + lock.lock() + defer { lock.unlock() } + guard generation == expectedGeneration else { return } + pendingIdentity = nil + quote = nil + readyForSubmission = false + } + + func activate(presentationID: String, fee: BigUInt) -> Bool { + lock.lock() + defer { lock.unlock() } + guard let quote, + quote.quoteIDHex == presentationID, + fee == BigUInt(quote.feeNanotons), + pendingIdentity == quote.identity + else { + return false + } + readyForSubmission = true + return true + } + + func consume(identity: TonTransferIntentIdentity) -> TonTransferFeeQuote? { + lock.lock() + defer { lock.unlock() } + guard pendingIdentity == identity, + quote?.identity == identity, + readyForSubmission + else { + return nil + } + let result = quote + pendingIdentity = nil + quote = nil + readyForSubmission = false + return result + } + + func invalidate() { + lock.lock() + defer { lock.unlock() } + generation = generation == UInt64.max ? 1 : generation + 1 + pendingIdentity = nil + quote = nil + readyForSubmission = false + } +} + +final class TonTransferService: TransferServiceProtocol { + private struct PreparedFeePresentation { + let fee: BigUInt + let presentationID: String + } + + private struct ResolvedTransfer { + let publicKey: Data + let senderAddress: String + let recipientAddress: String + let amountNanotons: String + let bounce: Bool + } + + private let wallet: MetaAccountModel + private let chain: ChainModel + private let sendService: TonSendService + private let mnemonicProvider: UniversalWalletMnemonicProviding + private let feeQuoteStore = TonTransferFeeQuoteStore() + private var feeTask: Task? + + init( + wallet: MetaAccountModel, + chain: ChainModel, + remote: TonTransferRemoteProtocol, + mnemonicProvider: UniversalWalletMnemonicProviding = KeychainUniversalWalletMnemonicProvider(), + clock: @escaping @Sendable() -> UInt64 = { UInt64(Date().timeIntervalSince1970) } + ) { + self.wallet = wallet + self.chain = chain + self.mnemonicProvider = mnemonicProvider + sendService = TonSendService( + remote: remote, + clock: clock + ) + } + + #if DEBUG + init( + wallet: MetaAccountModel, + chain: ChainModel, + remote: TonTransferRemoteProtocol, + mnemonicProvider: UniversalWalletMnemonicProviding = KeychainUniversalWalletMnemonicProvider(), + pendingCoordinator: TonPendingIntentCoordinator, + clock: @escaping @Sendable() -> UInt64 = { UInt64(Date().timeIntervalSince1970) } + ) { + self.wallet = wallet + self.chain = chain + self.mnemonicProvider = mnemonicProvider + sendService = TonSendService( + remote: remote, + pendingCoordinator: pendingCoordinator, + clock: clock + ) + } + #endif + + func estimateFee(for transfer: Transfer) async throws -> BigUInt { + // This compatibility API may calculate a fee, but it must never authorize a send. + // Only the typed listener path exposes the opaque quote ID that the rendered UI can + // acknowledge before submission. + try await prepareFee(for: transfer, readyForSubmission: false).fee + } + + private func prepareFee( + for transfer: Transfer, + readyForSubmission: Bool + ) async throws -> PreparedFeePresentation { + let resolved = try resolveTransfer( + for: transfer, + failure: TransferServiceError.cannotEstimateFee(reason:) + ) + + do { + let estimateRequest = TonNativeEstimateRequest( + publicKey: resolved.publicKey, + senderAddress: resolved.senderAddress, + recipientAddress: resolved.recipientAddress, + amountNanotons: resolved.amountNanotons, + bounce: resolved.bounce + ) + let identity = try TonTransferIntentIdentity(request: estimateRequest) + let generation = feeQuoteStore.begin(identity: identity) + do { + let quote = try await sendService.quote(estimateRequest) + try Task.checkCancellation() + guard feeQuoteStore.commit( + quote, + identity: identity, + generation: generation, + readyForSubmission: readyForSubmission + ) else { + throw CancellationError() + } + // Close cancellation between the remote result and local publication. `fail` + // below synchronously revokes this exact generation if cancellation won. + try Task.checkCancellation() + return PreparedFeePresentation( + fee: BigUInt(quote.feeNanotons), + presentationID: quote.quoteIDHex + ) + } catch { + feeQuoteStore.fail(generation: generation) + throw error + } + } catch let error as TransferServiceError { + throw error + } catch { + throw TransferServiceError.cannotEstimateFee(reason: "TON transfer validation or emulation failed") + } + } + + func submit(transfer: Transfer) async throws -> String { + #if !DEBUG + // Keep the integration boundary fail-closed before transfer resolution or + // mnemonic/keychain access, even if same-module code bypasses normal DI. + throw TransferServiceError.tonProductionSendDisabled + #else + let resolved = try resolveTransfer( + for: transfer, + failure: TransferServiceError.transferFailed(reason:) + ) + + let identity = try TonTransferIntentIdentity( + request: TonNativeEstimateRequest( + publicKey: resolved.publicKey, + senderAddress: resolved.senderAddress, + recipientAddress: resolved.recipientAddress, + amountNanotons: resolved.amountNanotons, + bounce: resolved.bounce + ) + ) + // Atomically consume the exact quote. A nil quote is still passed through so + // TonSendService can recover an already-persisted same-intent BOC after restart; + // a genuinely fresh unquoted send fails before signing or remote work. + let feeQuote = feeQuoteStore.consume(identity: identity) + + do { + return try await sendService.send( + TonNativeEstimateRequest( + publicKey: resolved.publicKey, + senderAddress: resolved.senderAddress, + recipientAddress: resolved.recipientAddress, + amountNanotons: resolved.amountNanotons, + bounce: resolved.bounce + ), + feeQuote: feeQuote, + signingCredentials: { + guard let mnemonic = try self.mnemonicProvider.mnemonic( + for: self.wallet, + chain: self.chain + ) else { + throw TonSendServiceError.invalidAccount + } + return TonSigningCredentials(mnemonic: mnemonic) + } + ).messageHashHex + } catch let error as TonSendServiceError { + if case let .priorIntentConfirmed(identity, messageHashHex) = error { + throw TransferServiceError.tonPriorTransferConfirmed( + identity: identity, + messageHashHex: messageHashHex + ) + } + if case let .broadcastOutcomeUnknown(messageHashHex) = error { + throw TransferServiceError.tonBroadcastOutcomeUnknown( + messageHashHex: messageHashHex + ) + } + throw TransferServiceError.transferFailed(reason: "TON transfer validation, emulation, or broadcast failed") + } catch { + throw TransferServiceError.transferFailed(reason: "TON transfer validation, emulation, or broadcast failed") + } + #endif + } + + func subscribeForFee(transfer: Transfer, listener: TransferFeeEstimationListener) { + feeTask?.cancel() + feeTask = Task { [weak self, weak listener] in + guard let self else { + return + } + + do { + let presentation = try await self.prepareFee( + for: transfer, + readyForSubmission: false + ) + guard !Task.isCancelled else { + return + } + if let listener = listener as? TonTransferFeePresentationListener { + listener.didReceiveTonFee( + fee: presentation.fee, + presentationID: presentation.presentationID + ) + } else { + listener?.didReceiveFee(fee: presentation.fee) + } + } catch { + guard !Task.isCancelled else { + return + } + listener?.didReceiveFeeError(feeError: error) + } + } + } + + func unsubscribe() { + feeQuoteStore.invalidate() + feeTask?.cancel() + feeTask = nil + } + + func confirmFeePresentation(id: String, fee: BigUInt) async -> Bool { + feeQuoteStore.activate(presentationID: id, fee: fee) + } + + func acknowledgeSubmittedTransfer(hash: String, transfer: Transfer) async -> Bool { + guard let resolved = try? resolveTransfer( + for: transfer, + failure: TransferServiceError.transferFailed(reason:) + ) else { + return false + } + guard let identity = try? TonTransferIntentIdentity( + request: TonNativeEstimateRequest( + publicKey: resolved.publicKey, + senderAddress: resolved.senderAddress, + recipientAddress: resolved.recipientAddress, + amountNanotons: resolved.amountNanotons, + bounce: resolved.bounce + ) + ) else { + return false + } + return await acknowledgeConfirmedTransfer( + hash: hash, + senderAddress: resolved.senderAddress, + identity: identity + ) + } + + func acknowledgeRecoveredTransfer( + hash: String, + identity: TonTransferIntentIdentity + ) async -> Bool { + guard let senderAddress = UniversalWalletAccountAddressResolver.address( + for: chain, + wallet: wallet + ), + let sender = try? TonSwift.Address.parse(senderAddress), + sender.toRaw() == identity.sender + else { + return false + } + return await acknowledgeConfirmedTransfer( + hash: hash, + senderAddress: senderAddress, + identity: identity + ) + } + + private func acknowledgeConfirmedTransfer( + hash: String, + senderAddress: String, + identity: TonTransferIntentIdentity + ) async -> Bool { + do { + try await sendService.acknowledgeConfirmedTransfer( + senderAddress: senderAddress, + identity: identity, + messageHashHex: hash + ) + return true + } catch { + return false + } + } + + private func resolveTransfer( + for transfer: Transfer, + failure: (String) -> TransferServiceError + ) throws -> ResolvedTransfer { + guard transfer.tip == nil, transfer.appId == nil else { + throw failure("TON tips and app identifiers are not supported") + } + + guard transfer.chainAsset.chain.chainId == chain.chainId else { + throw failure("TON transfer chain does not match the selected service") + } + + let chainId = chain.chainId.lowercased() + guard !(chain.options ?? []).contains(.testnet), + chainId == TonChainSelection.mainnetChainId || + chainId == UniversalWalletRegistry.tonMainnetRegistryEntry.chainId || + chainId == UniversalWalletRegistry.tonMainnetRegistryEntry.id + else { + throw failure("Unsupported TON network: \(chain.chainId)") + } + + let asset = transfer.chainAsset.asset + guard transfer.chainAsset.isNative, + asset.isNative, + asset.isUtility, + asset.id == UniversalWalletRegistry.tonNativeAssetId || + asset.id.uppercased() == "TON", + asset.symbol.uppercased() == "TON", + asset.precision == 9, + asset.type == nil || asset.type == .normal + else { + throw failure("Only native TON transfers are supported") + } + + guard let sourceAddress = UniversalWalletAccountAddressResolver.address(for: chain, wallet: wallet) else { + throw failure("TON account address is unavailable for \(chain.chainId)") + } + guard let account = wallet.fetch(for: chain.accountRequest()), + account.publicKey.count == 32 + else { + throw failure("TON public key is unavailable for \(chain.chainId)") + } + + let bounce: Bool + do { + bounce = try TonTransferTransactionBuilder.requiredBounceFlag( + forRecipientAddress: transfer.receiver + ) + } catch { + throw failure("TON recipient address or bounce policy is invalid") + } + + return ResolvedTransfer( + publicKey: account.publicKey, + senderAddress: sourceAddress, + recipientAddress: transfer.receiver, + amountNanotons: transfer.amount.description, + bounce: bounce + ) + } +} + final class SolanaTransferService: TransferServiceProtocol { private let wallet: MetaAccountModel private let chain: ChainModel @@ -613,6 +1060,100 @@ struct UnavailableIrohaTransferSigner: IrohaTransferSigning { } } +enum IrohaWalletSmokeMetadataError: Error, Equatable { + case invalidFieldSet + case invalidEvidenceRole + case invalidRouteGovernanceActionHash + case invalidWalletPlatform + case invalidWalletCommit +} + +struct IrohaWalletSmokeTransactionMetadata: Equatable { + static let evidenceRoleKey = "evidence_role" + static let routeGovernanceActionHashKey = "route_governance_action_hash" + static let walletPlatformKey = "wallet_platform" + static let walletCommitKey = "wallet_commit" + + private static let expectedKeys: Set = [ + evidenceRoleKey, + routeGovernanceActionHashKey, + walletPlatformKey, + walletCommitKey + ] + private static let routeHashPrefix = "sha256:" + + private let snapshot: [String: String] + + static func validatedSnapshot( + of untrustedMetadata: [String: String] + ) throws -> IrohaWalletSmokeTransactionMetadata { + guard untrustedMetadata.count == expectedKeys.count, + Set(untrustedMetadata.keys) == expectedKeys + else { + throw IrohaWalletSmokeMetadataError.invalidFieldSet + } + + guard untrustedMetadata[evidenceRoleKey] == "wallet-smoke" else { + throw IrohaWalletSmokeMetadataError.invalidEvidenceRole + } + guard untrustedMetadata[walletPlatformKey] == "ios" else { + throw IrohaWalletSmokeMetadataError.invalidWalletPlatform + } + + guard let routeHash = untrustedMetadata[routeGovernanceActionHashKey], + routeHash.hasPrefix(routeHashPrefix), + routeHash.utf8.count == routeHashPrefix.utf8.count + 64, + isLowercaseHex(routeHash.utf8.dropFirst(routeHashPrefix.utf8.count)), + routeHash != routeHashPrefix + String(repeating: "0", count: 64) + else { + throw IrohaWalletSmokeMetadataError.invalidRouteGovernanceActionHash + } + + guard let walletCommit = untrustedMetadata[walletCommitKey], + walletCommit.utf8.count == 40, + isLowercaseHex(walletCommit.utf8), + walletCommit != String(repeating: "0", count: 40) + else { + throw IrohaWalletSmokeMetadataError.invalidWalletCommit + } + + // Copy every String into a fresh dictionary before crossing the async signer + // boundary. Later copy-on-write mutation of the operator input cannot alter it. + return IrohaWalletSmokeTransactionMetadata( + snapshot: Dictionary(uniqueKeysWithValues: untrustedMetadata.map { ($0.key, $0.value) }) + ) + } + + var values: [String: String] { + snapshot + } + + private init(snapshot: [String: String]) { + self.snapshot = snapshot + } + + private static func isLowercaseHex(_ bytes: C) -> Bool where C.Element == UInt8 { + !bytes.isEmpty && bytes.allSatisfy { byte in + (UInt8(ascii: "0") ... UInt8(ascii: "9")).contains(byte) + || (UInt8(ascii: "a") ... UInt8(ascii: "f")).contains(byte) + } + } +} + +enum IrohaTransactionMetadata: Equatable { + case none + case walletSmoke(IrohaWalletSmokeTransactionMetadata) + + var values: [String: String] { + switch self { + case .none: + return [:] + case let .walletSmoke(metadata): + return metadata.values + } + } +} + struct IrohaTransferSigningRequest: Equatable { let amount: String let assetDefinitionId: String @@ -620,6 +1161,7 @@ struct IrohaTransferSigningRequest: Equatable { let chainId: String let derivationPath: String let destinationAccountId: String + let metadata: IrohaTransactionMetadata let mnemonicOrSeed: String let network: String let signingPublicKeyHex: String @@ -664,10 +1206,63 @@ final class IrohaTransferService: TransferServiceProtocol { } func submit(transfer: Transfer) async throws -> String { + try await submitValidated(transfer: transfer, metadata: .none) + } + + /// Operator evidence hook only. This does not install a signer, enable Nexus, + /// or make the standard `TransferServiceProtocol` route metadata-capable. + func submitNexusWalletSmokeEvidence( + transfer: Transfer, + untrustedMetadata: [String: String] + ) async throws -> String { + let metadata = try IrohaWalletSmokeTransactionMetadata.validatedSnapshot( + of: untrustedMetadata + ) + guard chain.chainId == UniversalWalletRegistry.nexus.chainId else { + throw TransferServiceError.transferFailed( + reason: "Iroha wallet-smoke evidence requires the exact canonical Nexus chain identity" + ) + } + let evidenceToriiBaseURL = try Self.toriiBaseURL( + for: chain, + network: UniversalWalletRegistry.nexus, + failure: TransferServiceError.transferFailed(reason:) + ) + guard evidenceToriiBaseURL == UniversalWalletRegistry.nexus.toriiBaseURL?.absoluteString else { + throw TransferServiceError.transferFailed( + reason: "Iroha wallet-smoke evidence requires the canonical Nexus Torii endpoint" + ) + } + + return try await submitValidated( + transfer: transfer, + metadata: .walletSmoke(metadata) + ) + } + + private func submitValidated( + transfer: Transfer, + metadata: IrohaTransactionMetadata + ) async throws -> String { let context = try resolveContext( for: transfer, + metadata: metadata, failure: TransferServiceError.transferFailed(reason:) ) + + if case .walletSmoke = metadata { + guard context.signingRequest.network == "nexus" else { + throw TransferServiceError.transferFailed( + reason: "Iroha wallet-smoke evidence is restricted to Nexus" + ) + } + guard context.toriiBaseURL == UniversalWalletRegistry.nexus.toriiBaseURL?.absoluteString else { + throw TransferServiceError.transferFailed( + reason: "Iroha wallet-smoke evidence requires the canonical Nexus Torii endpoint" + ) + } + } + let signedTransfer = try await signer.buildAndSignTransfer(context.signingRequest) guard !signedTransfer.signedTransaction.isEmpty else { @@ -715,6 +1310,7 @@ final class IrohaTransferService: TransferServiceProtocol { private func resolveContext( for transfer: Transfer, + metadata: IrohaTransactionMetadata = .none, failure: (String) -> TransferServiceError ) throws -> IrohaTransferContext { let network = try Self.irohaNetwork( @@ -788,6 +1384,7 @@ final class IrohaTransferService: TransferServiceProtocol { chainId: network.chainId, derivationPath: UniversalWalletDerivationPaths.irohaDefault, destinationAccountId: destinationAddress, + metadata: metadata, mnemonicOrSeed: mnemonic, network: Self.networkKey(for: network), signingPublicKeyHex: sourceDetails.publicKeyHex, diff --git a/fearless/Common/CoreData/SafeTransformableValueReader.swift b/fearless/Common/CoreData/SafeTransformableValueReader.swift index 670ef74972..5a40fd22cf 100644 --- a/fearless/Common/CoreData/SafeTransformableValueReader.swift +++ b/fearless/Common/CoreData/SafeTransformableValueReader.swift @@ -2,6 +2,82 @@ import CoreData import Foundation import SQLite3 +/// Replaces Foundation's eager Core Data unarchiver with an equivalent +/// secure-coding transformer that fails closed. Foundation can otherwise +/// surface a corrupt transformable BLOB as an error from `context.fetch(_:)`, +/// before a mapper has an object on which to apply its exception boundary. +final class FailClosedSecureUnarchiveFromDataTransformer: + NSSecureUnarchiveFromDataTransformer +{ + private static let maximumArchiveByteCount = 1 * 1024 * 1024 + // A payload-free wrong-type value keeps a corrupt optional archive + // distinguishable from a legitimately absent value. Typed production + // mappers reject it, while migration sanitizers can durably replace the + // damaged archive with their model-specific safe default. + private static let invalidArchiveMarker = NSString( + string: "fearless.invalid-transformable-archive" + ) + + override class var allowedTopLevelClasses: [AnyClass] { + [ + NSArray.self, + NSDictionary.self, + NSSet.self, + NSString.self, + NSNumber.self, + NSData.self, + NSDate.self, + NSURL.self, + NSNull.self + ] + } + + override func transformedValue(_ value: Any?) -> Any? { + guard + let archive = value as? Data, + archive.count <= Self.maximumArchiveByteCount + else { + return Self.invalidArchiveMarker + } + + do { + let decoded = try FearlessObjectiveCExceptionCatcher + .performObjectRead { + try? NSKeyedUnarchiver.unarchivedObject( + ofClasses: Self.allowedTopLevelClasses, + from: archive + ) + } + + guard !(decoded is NSNull) else { + return Self.invalidArchiveMarker + } + + return decoded ?? Self.invalidArchiveMarker + } catch { + return Self.invalidArchiveMarker + } + } + + static func register() { + _ = registration + } + + private static let registration: Void = { + let names = [ + NSValueTransformerName.secureUnarchiveFromDataTransformerName, + NSValueTransformerName("NSSecureUnarchiveFromDataTransformer") + ] + + for name in names { + ValueTransformer.setValueTransformer( + FailClosedSecureUnarchiveFromDataTransformer(), + forName: name + ) + } + }() +} + enum SafeTransformableValueReaderError: LocalizedError, Equatable { case objectiveCException case unexpectedStoredType diff --git a/fearless/Common/DataProvider/Sources/Rewards/SubqueryRewardsSource.swift b/fearless/Common/DataProvider/Sources/Rewards/SubqueryRewardsSource.swift index 4d769513ab..31319a0f0e 100644 --- a/fearless/Common/DataProvider/Sources/Rewards/SubqueryRewardsSource.swift +++ b/fearless/Common/DataProvider/Sources/Rewards/SubqueryRewardsSource.swift @@ -1,27 +1,24 @@ import Foundation import RobinHood import SSFUtils -import BigInt import SSFModels final class ParachainSubqueryRewardsSource { typealias Model = [SubqueryRewardItemData] private let address: AccountAddress - private let url: URL private let startTimestamp: Int64? private let endTimestamp: Int64? private let operationFactory: RewardOperationFactoryProtocol init( address: AccountAddress, - url: URL, + url _: URL, startTimestamp: Int64? = nil, endTimestamp: Int64? = nil, operationFactory: RewardOperationFactoryProtocol ) { self.address = address - self.url = url self.startTimestamp = startTimestamp self.endTimestamp = endTimestamp self.operationFactory = operationFactory @@ -40,20 +37,10 @@ extension ParachainSubqueryRewardsSource: SingleValueProviderSourceProtocol { let mappingOperation = ClosureOperation<[SubqueryRewardItemData]?> { let rewards = try rewardOperation.extractNoCancellableResultData() - return rewards.rewardHistory(for: address).compactMap { wrappedReward in - guard - let timestamp = Int64(wrappedReward.timestampInSeconds) - else { - return nil - } - return SubqueryRewardItemData( - eventId: wrappedReward.id, - timestamp: timestamp, - validatorAddress: "", - era: EraIndex(0), - stashAddress: address, - amount: wrappedReward.amount, - isReward: wrappedReward.type == .reward + return rewards.rewardHistory(for: address).compactMap { + SubqueryRewardItemData( + rewardHistoryItem: $0, + stashAddress: address ) } } @@ -62,45 +49,4 @@ extension ParachainSubqueryRewardsSource: SingleValueProviderSourceProtocol { return CompoundOperationWrapper(targetOperation: mappingOperation, dependencies: [rewardOperation]) } - - private func requestParams() -> String { - let timestampFilter: String = { - guard startTimestamp != nil || endTimestamp != nil else { return "" } - var result = "timestamp:{" - if let timestamp = startTimestamp { - result.append("greaterThanOrEqualTo:\"\(timestamp)\",") - } - if let timestamp = endTimestamp { - result.append("lessThanOrEqualTo:\"\(timestamp)\",") - } - result.append("}") - return result - }() - - return """ - { - delegators( - filter: { - id: { equalToInsensitive:"\(address)"} - } - ) { - nodes { - id - delegatorHistoryElements(orderBy: TIMESTAMP_DESC,filter: { amount: {isNull: false}, \(timestampFilter), type: { equalTo: 0 }}) { - nodes { - id - blockNumber - amount - type - timestamp - delegator { - id - } - } - } - } - } - } - """ - } } diff --git a/fearless/Common/DataProvider/Sources/Rewards/WeaklyAnalyticsRewardSource.swift b/fearless/Common/DataProvider/Sources/Rewards/WeaklyAnalyticsRewardSource.swift index eb7db79e55..d6e6dd16fa 100644 --- a/fearless/Common/DataProvider/Sources/Rewards/WeaklyAnalyticsRewardSource.swift +++ b/fearless/Common/DataProvider/Sources/Rewards/WeaklyAnalyticsRewardSource.swift @@ -1,7 +1,6 @@ import Foundation import RobinHood import SSFUtils -import BigInt final class ParachainWeaklyAnalyticsRewardSource { typealias Model = [SubqueryRewardItemData] @@ -33,20 +32,10 @@ extension ParachainWeaklyAnalyticsRewardSource: SingleValueProviderSourceProtoco let mappingOperation = ClosureOperation<[SubqueryRewardItemData]?> { let rewards = try rewardOperation.extractNoCancellableResultData() - return rewards.rewardHistory(for: address).compactMap { wrappedReward in - guard - let timestamp = Int64(wrappedReward.timestampInSeconds) - else { - return nil - } - return SubqueryRewardItemData( - eventId: wrappedReward.id, - timestamp: timestamp, - validatorAddress: "", - era: EraIndex(0), - stashAddress: address, - amount: wrappedReward.amount, - isReward: wrappedReward.type == .reward + return rewards.rewardHistory(for: address).compactMap { + SubqueryRewardItemData( + rewardHistoryItem: $0, + stashAddress: address ) } } diff --git a/fearless/Common/Extension/NumberFormatter+Default.swift b/fearless/Common/Extension/NumberFormatter+Default.swift index 295075b963..5bd18c705c 100644 --- a/fearless/Common/Extension/NumberFormatter+Default.swift +++ b/fearless/Common/Extension/NumberFormatter+Default.swift @@ -5,6 +5,7 @@ enum NumberFormatterUsageCase { case listCrypto case listCryptoWith(minimumFractionDigits: Int, maximumFractionDigits: Int) case detailsCrypto + case exactCrypto(fractionDigits: Int) case fiat case percent case inputCrypto @@ -39,6 +40,13 @@ extension NumberFormatter { rounding: rounding, usesIntGrouping: usesIntGrouping ) + case let .exactCrypto(fractionDigits): + return NumberFormatter.defaultExactCryptoFormatter( + locale: locale, + fractionDigits: fractionDigits, + rounding: rounding, + usesIntGrouping: usesIntGrouping + ) case .fiat: return NumberFormatter.defaultFiatFormatter(locale: locale) case .percent: @@ -80,6 +88,22 @@ extension NumberFormatter { return formatter } + private static func defaultExactCryptoFormatter( + locale: Locale, + fractionDigits: Int, + rounding: NumberFormatter.RoundingMode = .down, + usesIntGrouping: Bool = false + ) -> NumberFormatter { + let formatter = NumberFormatter() + formatter.locale = locale + formatter.minimumIntegerDigits = 1 + formatter.minimumFractionDigits = 0 + formatter.maximumFractionDigits = max(0, min(fractionDigits, 38)) + formatter.roundingMode = rounding + formatter.usesGroupingSeparator = usesIntGrouping + return formatter + } + private static func defaultFiatFormatter(locale: Locale) -> NumberFormatter { let numberFormatter = NumberFormatter() numberFormatter.numberStyle = .decimal diff --git a/fearless/Common/Model/ChainRegistry/ChainModel.swift b/fearless/Common/Model/ChainRegistry/ChainModel.swift index a83dfd0e5d..c4db9b0be1 100644 --- a/fearless/Common/Model/ChainRegistry/ChainModel.swift +++ b/fearless/Common/Model/ChainRegistry/ChainModel.swift @@ -124,19 +124,50 @@ extension ChainModel { extension ChainModel { var isTonCompatibilityChain: Bool { let chainName = name.lowercased() - if chainName == "ton" || chainName.contains("ton ") || chainName.contains(" ton") { + let chainNameTokens = chainName.components( + separatedBy: CharacterSet.alphanumerics.inverted + ) + if chainNameTokens.contains("ton") { return true } let chainIdLowercased = chainId.lowercased() - if chainIdLowercased == "ton" || chainIdLowercased.contains("ton-") { + switch chainIdLowercased { + case "ton", + "-239", + "-3", + "ton-mainnet", + "ton:mainnet", + "ton-testnet", + "ton:testnet": + return true + default: + break + } + if chainIdLowercased.hasPrefix("ton-") || chainIdLowercased.hasPrefix("ton:") { return true } - if nodes.contains(where: { $0.url.absoluteString.lowercased().contains("ton") }) { + let nodeDomains = ["ton.org", "tonapi.io", "toncenter.com", "tonhubapi.com"] + if nodes.contains(where: { node in + guard let host = node.url.host?.lowercased() else { + return false + } + return nodeDomains.contains { domain in + host == domain || host.hasSuffix(".\(domain)") + } + }) { return true } - return externalApi?.explorers?.contains(where: { $0.url.lowercased().contains("tonviewer") }) == true + let explorerDomains = ["tonviewer.com", "tonscan.org"] + return externalApi?.explorers?.contains(where: { explorer in + guard let host = URL(string: explorer.url)?.host?.lowercased() else { + return false + } + return explorerDomains.contains { domain in + host == domain || host.hasSuffix(".\(domain)") + } + }) == true } } diff --git a/fearless/Common/Model/PasskeyBackupContract.swift b/fearless/Common/Model/PasskeyBackupContract.swift index 0b9c28f06e..b1a0288842 100644 --- a/fearless/Common/Model/PasskeyBackupContract.swift +++ b/fearless/Common/Model/PasskeyBackupContract.swift @@ -1,5 +1,6 @@ import AuthenticationServices import CloudKit +import CryptoKit import Foundation enum PasskeyBackupError: Error, Equatable { @@ -12,6 +13,11 @@ enum PasskeyBackupError: Error, Equatable { case invalidWalletId case invalidCreatedAtMillis case emptyEncryptedPayload + case encryptedPayloadMetadataRequired + case invalidEncryptedEnvelope + case invalidBackupKeyLength(Int) + case envelopeAuthenticationFailed + case unavailableBackupKey case unsupportedSchemaVersion(Int) case unavailableCloudStorage case unavailableCloudKitAccount @@ -23,11 +29,247 @@ enum PasskeyBackupError: Error, Equatable { case emptyChallengeServiceResponse case malformedChallengeServiceResponse case invalidCredentialResponse + case unexpectedCredentialType + case ceremonyInProgress + case ceremonyCancelled + case invalidCompensationTimeout + case invalidResponseSizeLimit + case challengeServiceResponseTooLarge + case registrationCompletionOutcomeUnknown + case unavailableAuthorization + case invalidAuthorizationToken case mismatchedChallengeStorageKey case missingCloudBackup case passkeyBackupDisabled } +enum PasskeyBackupRegistrationCompensationFailureKind: Equatable { + case revokeFailed + case timedOut +} + +struct PasskeyBackupRegistrationCompensationError: Error { + let primaryError: Error + let failureKind: PasskeyBackupRegistrationCompensationFailureKind + let cleanupError: Error? +} + +struct PasskeyBackupEnvelopeMetadata: Equatable { + let storageKey: String + let walletId: String + let accountName: String + let createdAtMillis: Int64 + let schemaVersion: Int + + init( + storageKey: String, + walletId: String, + accountName: String, + createdAtMillis: Int64, + schemaVersion: Int = PasskeyBackupContract.schemaVersion + ) throws { + self.storageKey = try PasskeyBackupContract.validateStorageKey(storageKey) + self.walletId = try PasskeyBackupContract.validateWalletId(walletId) + self.accountName = try PasskeyBackupContract.validateAccountName(accountName) + self.createdAtMillis = try PasskeyBackupContract.validateCreatedAtMillis(createdAtMillis) + guard schemaVersion == PasskeyBackupContract.schemaVersion else { + throw PasskeyBackupError.unsupportedSchemaVersion(schemaVersion) + } + self.schemaVersion = schemaVersion + } + + fileprivate func canonicalAdditionalAuthenticatedData() -> Data { + var data = Data("FPBKAAD1".utf8) + data.appendBigEndian(UInt32(schemaVersion)) + data.appendBigEndian(UInt64(bitPattern: createdAtMillis)) + data.appendCanonicalUTF8(storageKey) + data.appendCanonicalUTF8(walletId) + data.appendCanonicalUTF8(accountName) + return data + } +} + +protocol RecoverablePasskeyBackupKeyProvider { + /// Returns exactly 32 recoverable bytes shared by the same wallet on every device. + /// A device-local Keychain key is not a valid implementation of this contract. + func backupKey(for metadata: PasskeyBackupEnvelopeMetadata) async throws -> Data +} + +final class UnavailablePasskeyBackupKeyProvider: RecoverablePasskeyBackupKeyProvider { + func backupKey(for _: PasskeyBackupEnvelopeMetadata) async throws -> Data { + throw PasskeyBackupError.unavailableBackupKey + } +} + +protocol PasskeyBackupEnvelopeCryptography { + /// Implementations must emit and consume PasskeyBackupEnvelopeV1Format envelopes. + func encrypt( + _ plaintext: Data, + metadata: PasskeyBackupEnvelopeMetadata, + key: Data + ) throws -> Data + + func decrypt( + _ envelope: Data, + metadata: PasskeyBackupEnvelopeMetadata, + key: Data + ) throws -> Data + + func validateCanonicalEnvelope(_ envelope: Data) throws +} + +enum PasskeyBackupEnvelopeV1Format { + fileprivate static let magic = Data("FPBKAEAD".utf8) + fileprivate static let version: UInt8 = 1 + fileprivate static let algorithmAES256GCM: UInt8 = 1 + fileprivate static let keyBytes = 32 + fileprivate static let nonceBytes = 12 + fileprivate static let tagBytes = 16 + fileprivate static let headerBytes = 16 + fileprivate static let maxEnvelopeBytes = 256 * 1024 + fileprivate static let maxPlaintextBytes = maxEnvelopeBytes - headerBytes - nonceBytes - tagBytes + fileprivate static let minEnvelopeBytes = headerBytes + nonceBytes + tagBytes + 1 + + static func validateCanonicalEnvelope(_ envelope: Data) throws { + _ = try parse(envelope) + } + + fileprivate static func parse(_ envelope: Data) throws -> ParsedEnvelope { + guard (minEnvelopeBytes ... maxEnvelopeBytes).contains(envelope.count), + envelope.prefix(magic.count) == magic, + envelope[8] == version, + envelope[9] == algorithmAES256GCM, + envelope[10] == UInt8(nonceBytes), + envelope[11] == UInt8(tagBytes) else { + throw PasskeyBackupError.invalidEncryptedEnvelope + } + let ciphertextLength = Int( + UInt32(envelope[12]) << 24 | + UInt32(envelope[13]) << 16 | + UInt32(envelope[14]) << 8 | + UInt32(envelope[15]) + ) + guard (1 ... maxPlaintextBytes).contains(ciphertextLength), + envelope.count == headerBytes + nonceBytes + ciphertextLength + tagBytes else { + throw PasskeyBackupError.invalidEncryptedEnvelope + } + let nonceStart = headerBytes + let ciphertextStart = nonceStart + nonceBytes + let tagStart = ciphertextStart + ciphertextLength + return ParsedEnvelope( + nonce: envelope.subdata(in: nonceStart ..< ciphertextStart), + ciphertext: envelope.subdata(in: ciphertextStart ..< tagStart), + tag: envelope.subdata(in: tagStart ..< envelope.count) + ) + } + + fileprivate struct ParsedEnvelope { + let nonce: Data + let ciphertext: Data + let tag: Data + } +} + +struct AESGCMPasskeyBackupEnvelopeCryptography: PasskeyBackupEnvelopeCryptography { + func encrypt( + _ plaintext: Data, + metadata: PasskeyBackupEnvelopeMetadata, + key: Data + ) throws -> Data { + guard !plaintext.isEmpty, + plaintext.count <= PasskeyBackupEnvelopeV1Format.maxPlaintextBytes else { + throw PasskeyBackupError.invalidEncryptedEnvelope + } + let symmetricKey = try validatedKey(key) + let sealed = try AES.GCM.seal( + plaintext, + using: symmetricKey, + authenticating: metadata.canonicalAdditionalAuthenticatedData() + ) + let nonce = Data(sealed.nonce) + guard nonce.count == PasskeyBackupEnvelopeV1Format.nonceBytes, + sealed.tag.count == PasskeyBackupEnvelopeV1Format.tagBytes, + sealed.ciphertext.count == plaintext.count else { + throw PasskeyBackupError.invalidEncryptedEnvelope + } + + var envelope = PasskeyBackupEnvelopeV1Format.magic + envelope.append(PasskeyBackupEnvelopeV1Format.version) + envelope.append(PasskeyBackupEnvelopeV1Format.algorithmAES256GCM) + envelope.append(UInt8(PasskeyBackupEnvelopeV1Format.nonceBytes)) + envelope.append(UInt8(PasskeyBackupEnvelopeV1Format.tagBytes)) + envelope.appendBigEndian(UInt32(sealed.ciphertext.count)) + envelope.append(nonce) + envelope.append(sealed.ciphertext) + envelope.append(sealed.tag) + try validateCanonicalEnvelope(envelope) + return envelope + } + + func decrypt( + _ envelope: Data, + metadata: PasskeyBackupEnvelopeMetadata, + key: Data + ) throws -> Data { + let parsed = try PasskeyBackupEnvelopeV1Format.parse(envelope) + let symmetricKey = try validatedKey(key) + let sealedBox: AES.GCM.SealedBox + do { + sealedBox = try AES.GCM.SealedBox( + nonce: AES.GCM.Nonce(data: parsed.nonce), + ciphertext: parsed.ciphertext, + tag: parsed.tag + ) + let plaintext = try AES.GCM.open( + sealedBox, + using: symmetricKey, + authenticating: metadata.canonicalAdditionalAuthenticatedData() + ) + guard !plaintext.isEmpty, + plaintext.count <= PasskeyBackupEnvelopeV1Format.maxPlaintextBytes else { + throw PasskeyBackupError.envelopeAuthenticationFailed + } + return plaintext + } catch let error as PasskeyBackupError { + throw error + } catch { + throw PasskeyBackupError.envelopeAuthenticationFailed + } + } + + func validateCanonicalEnvelope(_ envelope: Data) throws { + try PasskeyBackupEnvelopeV1Format.validateCanonicalEnvelope(envelope) + } + + private func validatedKey(_ key: Data) throws -> SymmetricKey { + guard key.count == PasskeyBackupEnvelopeV1Format.keyBytes else { + throw PasskeyBackupError.invalidBackupKeyLength(key.count) + } + return SymmetricKey(data: key) + } +} + +private extension Data { + mutating func appendBigEndian(_ value: UInt32) { + append(UInt8((value >> 24) & 0xFF)) + append(UInt8((value >> 16) & 0xFF)) + append(UInt8((value >> 8) & 0xFF)) + append(UInt8(value & 0xFF)) + } + + mutating func appendBigEndian(_ value: UInt64) { + for shift in stride(from: 56, through: 0, by: -8) { + append(UInt8((value >> UInt64(shift)) & 0xFF)) + } + } + + mutating func appendCanonicalUTF8(_ value: String) { + let encoded = Data(value.utf8) + appendBigEndian(UInt32(encoded.count)) + append(encoded) + } +} + enum PasskeyBackupContract { static let PASSKEY_RP_ID = "fearlesswallet.io" static let passkeyRelyingPartyId = PASSKEY_RP_ID @@ -46,7 +288,7 @@ enum PasskeyBackupContract { } static func validateUserId(_ userId: Data) throws { - guard (16 ... 64).contains(userId.count) else { + guard userId.count == 32 else { throw PasskeyBackupError.invalidUserIdLength(userId.count) } } @@ -64,7 +306,9 @@ enum PasskeyBackupContract { guard normalized.rangeOfCharacter(from: .whitespacesAndNewlines) == nil, normalized.rangeOfCharacter(from: .controlCharacters) == nil, - normalized.contains("@") else { + normalized.filter({ $0 == "@" }).count == 1, + !normalized.hasPrefix("@"), + !normalized.hasSuffix("@") else { throw PasskeyBackupError.invalidUserName } @@ -133,13 +377,23 @@ enum PasskeyBackupContract { return normalized } + static func validateCredentialId(_ credentialId: String) throws -> String { + guard (1 ... 512).contains(credentialId.count), + let decoded = try? decodeBase64URL(credentialId), + !decoded.isEmpty else { + throw PasskeyBackupError.invalidCredentialResponse + } + return credentialId + } + static func decodeBase64URL(_ value: String) throws -> Data { - let normalized = value.trimmingCharacters(in: .whitespacesAndNewlines) - guard !normalized.isEmpty else { + guard !value.isEmpty, + value.range(of: #"\A[A-Za-z0-9_-]+\z"#, options: .regularExpression) != nil, + value.count % 4 != 1 else { throw PasskeyBackupError.malformedChallengeServiceResponse } - var base64 = normalized + var base64 = value .replacingOccurrences(of: "-", with: "+") .replacingOccurrences(of: "_", with: "/") @@ -154,7 +408,11 @@ enum PasskeyBackupContract { throw PasskeyBackupError.malformedChallengeServiceResponse } - guard let data = Data(base64Encoded: base64) else { + guard let data = Data(base64Encoded: base64), + data.base64EncodedString() + .replacingOccurrences(of: "+", with: "-") + .replacingOccurrences(of: "/", with: "_") + .replacingOccurrences(of: "=", with: "") == value else { throw PasskeyBackupError.malformedChallengeServiceResponse } @@ -162,6 +420,142 @@ enum PasskeyBackupContract { } } +enum PasskeyCredentialResponseSerializer { + private static let maxCredentialIdEncodedLength = 512 + private static let maxClientDataEncodedLength = 8192 + private static let maxCredentialBlobEncodedLength = 32768 + + static func registrationJSON( + credentialID: Data, + clientDataJSON: Data, + attestationObject: Data, + authenticatorData: Data? = nil + ) throws -> String { + var response: [String: Any] = [ + "clientDataJSON": try encodeRequired( + clientDataJSON, + maxEncodedLength: maxClientDataEncodedLength + ), + "attestationObject": try encodeRequired(attestationObject) + ] + + if let authenticatorData { + response["authenticatorData"] = try encodeRequired(authenticatorData) + } + + return try credentialJSON( + credentialID: credentialID, + response: response + ) + } + + static func assertionJSON( + credentialID: Data, + clientDataJSON: Data, + authenticatorData: Data, + signature: Data, + userHandle: Data + ) throws -> String { + do { + try PasskeyBackupContract.validateUserId(userHandle) + } catch { + throw PasskeyBackupError.invalidCredentialResponse + } + + let response: [String: Any] = [ + "clientDataJSON": try encodeRequired( + clientDataJSON, + maxEncodedLength: maxClientDataEncodedLength + ), + "authenticatorData": try encodeRequired(authenticatorData), + "signature": try encodeRequired(signature), + "userHandle": try encodeRequired(userHandle) + ] + + return try credentialJSON( + credentialID: credentialID, + response: response + ) + } + + @available(iOS 15.0, macOS 12.0, *) + static func registrationJSON( + for credential: ASAuthorizationPlatformPublicKeyCredentialRegistration + ) throws -> String { + guard let attestationObject = credential.rawAttestationObject else { + throw PasskeyBackupError.invalidCredentialResponse + } + + return try registrationJSON( + credentialID: credential.credentialID, + clientDataJSON: credential.rawClientDataJSON, + attestationObject: attestationObject + ) + } + + @available(iOS 15.0, macOS 12.0, *) + static func assertionJSON( + for credential: ASAuthorizationPlatformPublicKeyCredentialAssertion + ) throws -> String { + try assertionJSON( + credentialID: credential.credentialID, + clientDataJSON: credential.rawClientDataJSON, + authenticatorData: credential.rawAuthenticatorData, + signature: credential.signature, + userHandle: credential.userID + ) + } + + private static func credentialJSON( + credentialID: Data, + response: [String: Any] + ) throws -> String { + let encodedCredentialID = try encodeRequired( + credentialID, + maxEncodedLength: maxCredentialIdEncodedLength + ) + let credential: [String: Any] = [ + "id": encodedCredentialID, + "rawId": encodedCredentialID, + "response": response, + "type": "public-key", + "clientExtensionResults": [String: Any](), + "authenticatorAttachment": "platform" + ] + + guard JSONSerialization.isValidJSONObject(credential), + let json = try? JSONSerialization.data( + withJSONObject: credential, + options: [.sortedKeys] + ), + let serialized = String(data: json, encoding: .utf8) else { + throw PasskeyBackupError.invalidCredentialResponse + } + + return serialized + } + + private static func encodeRequired( + _ data: Data, + maxEncodedLength: Int = maxCredentialBlobEncodedLength + ) throws -> String { + guard !data.isEmpty else { + throw PasskeyBackupError.invalidCredentialResponse + } + + let encoded = data.base64EncodedString() + .replacingOccurrences(of: "+", with: "-") + .replacingOccurrences(of: "/", with: "_") + .replacingOccurrences(of: "=", with: "") + + guard encoded.count <= maxEncodedLength else { + throw PasskeyBackupError.invalidCredentialResponse + } + + return encoded + } +} + enum PasskeyBackupReleaseConfig { static let challengeServiceBaseURL = URL(string: "https://backup.fearlesswallet.io")! static let isPasskeyBackupEnabled = false @@ -197,7 +591,7 @@ struct PasskeyBackupRegistrationChallenge: Equatable { try PasskeyBackupContract.validateUserId(userId) let normalizedDisplayName = displayName.trimmingCharacters(in: .whitespacesAndNewlines) - guard !normalizedDisplayName.isEmpty else { + guard !normalizedDisplayName.isEmpty, normalizedDisplayName.count <= 128 else { throw PasskeyBackupError.invalidDisplayName } @@ -256,6 +650,92 @@ struct PasskeyBackupChallengeResult: Equatable { } } +struct PasskeyBackupCredentialSummary: Equatable { + let id: String + let aaguid: String + let registrationPlatform: String + let deviceType: String + let backedUp: Bool + let transports: [String]? + + init( + id: String, + aaguid: String, + registrationPlatform: String, + deviceType: String, + backedUp: Bool, + transports: [String]? = nil + ) throws { + self.id = try PasskeyBackupContract.validateCredentialId(id) + guard aaguid.range( + of: #"\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z"#, + options: .regularExpression + ) != nil, + ["android", "ios"].contains(registrationPlatform), + ["singleDevice", "multiDevice"].contains(deviceType), + deviceType != "singleDevice" || !backedUp else { + throw PasskeyBackupError.malformedChallengeServiceResponse + } + if let transports { + let supported = Set(["ble", "cable", "hybrid", "internal", "nfc", "smart-card", "usb"]) + guard transports.count <= supported.count, + Set(transports).count == transports.count, + transports.allSatisfy(supported.contains) else { + throw PasskeyBackupError.malformedChallengeServiceResponse + } + } + self.aaguid = aaguid + self.registrationPlatform = registrationPlatform + self.deviceType = deviceType + self.backedUp = backedUp + self.transports = transports + } +} + +struct PasskeyBackupCredentialListResult: Equatable { + let storageKey: String + let credentials: [PasskeyBackupCredentialSummary] + let schemaVersion: Int + + init( + storageKey: String, + credentials: [PasskeyBackupCredentialSummary], + schemaVersion: Int = PasskeyBackupContract.schemaVersion + ) throws { + self.storageKey = try PasskeyBackupContract.validateStorageKey(storageKey) + guard credentials.count <= 32, + Set(credentials.map(\.id)).count == credentials.count, + schemaVersion == PasskeyBackupContract.schemaVersion else { + throw PasskeyBackupError.malformedChallengeServiceResponse + } + self.credentials = credentials + self.schemaVersion = schemaVersion + } +} + +struct PasskeyBackupCredentialRevokeResult: Equatable { + let storageKey: String + let credentialId: String? + let remainingCredentials: Int + let schemaVersion: Int + + init( + storageKey: String, + credentialId: String?, + remainingCredentials: Int, + schemaVersion: Int = PasskeyBackupContract.schemaVersion + ) throws { + self.storageKey = try PasskeyBackupContract.validateStorageKey(storageKey) + self.credentialId = try credentialId.map(PasskeyBackupContract.validateCredentialId) + guard (0 ... 32).contains(remainingCredentials), + schemaVersion == PasskeyBackupContract.schemaVersion else { + throw PasskeyBackupError.malformedChallengeServiceResponse + } + self.remainingCredentials = remainingCredentials + self.schemaVersion = schemaVersion + } +} + protocol PasskeyBackupChallengeService { func registrationChallenge( walletId: String, @@ -274,6 +754,90 @@ protocol PasskeyBackupChallengeService { assertionId: String, credentialResponseJSON: String ) async throws -> PasskeyBackupChallengeResult + + func listCredentials(storageKey: String) async throws -> PasskeyBackupCredentialListResult + + func revokeCredential( + storageKey: String, + credentialId: String + ) async throws -> PasskeyBackupCredentialRevokeResult + + func revokeAllCredentials(storageKey: String) async throws -> PasskeyBackupCredentialRevokeResult +} + +extension PasskeyBackupChallengeService { + func listCredentials(storageKey _: String) async throws -> PasskeyBackupCredentialListResult { + throw PasskeyBackupError.unavailableAuthorization + } + + func revokeCredential( + storageKey _: String, + credentialId _: String + ) async throws -> PasskeyBackupCredentialRevokeResult { + throw PasskeyBackupError.unavailableAuthorization + } + + func revokeAllCredentials(storageKey _: String) async throws -> PasskeyBackupCredentialRevokeResult { + throw PasskeyBackupError.unavailableAuthorization + } +} + +struct PasskeyBackupAuthorizationRequest: Equatable { + static let registrationChallengePath = "/api/passkey-backup/v1/registration/challenge" + static let registrationCompletePath = "/api/passkey-backup/v1/registration/complete" + static let assertionChallengePath = "/api/passkey-backup/v1/assertion/challenge" + static let assertionCompletePath = "/api/passkey-backup/v1/assertion/complete" + static let credentialsListPath = "/api/passkey-backup/v1/credentials/list" + static let credentialsRevokePath = "/api/passkey-backup/v1/credentials/revoke" + static let credentialsRevokeAllPath = "/api/passkey-backup/v1/credentials/revoke-all" + + private static let allowedPaths: Set = [ + registrationChallengePath, + registrationCompletePath, + assertionChallengePath, + assertionCompletePath, + credentialsListPath, + credentialsRevokePath, + credentialsRevokeAllPath + ] + + let method: String + let path: String + let bodySha256: String + + init(method: String, path: String, bodySha256: String) throws { + guard method == "POST", + Self.allowedPaths.contains(path), + Self.isCanonicalSHA256(bodySha256) else { + throw PasskeyBackupError.invalidAuthorizationToken + } + + self.method = method + self.path = path + self.bodySha256 = bodySha256 + } + + private static func isCanonicalSHA256(_ value: String) -> Bool { + guard let decoded = try? PasskeyBackupContract.decodeBase64URL(value), + decoded.count == 32 else { + return false + } + + return true + } +} + +protocol PasskeyBackupAuthorizationProvider { + /// Returns a one-time opaque grant for the exact request body. The issuer's + /// subject must represent stable Fearless wallet ownership, not a raw + /// Google, Apple, or device account identifier. + func authorizationToken(for request: PasskeyBackupAuthorizationRequest) async throws -> String +} + +final class UnavailableBackupAuthorizationProvider: PasskeyBackupAuthorizationProvider { + func authorizationToken(for _: PasskeyBackupAuthorizationRequest) async throws -> String { + throw PasskeyBackupError.unavailableAuthorization + } } struct PasskeyBackupHTTPRequest: Equatable { @@ -309,14 +873,196 @@ protocol PasskeyBackupHTTPTransport { func execute(_ request: PasskeyBackupHTTPRequest) async throws -> PasskeyBackupHTTPResponse } +enum PasskeyBackupHTTPTransportPolicy { + static let requestTimeout: TimeInterval = 15 + static let resourceTimeout: TimeInterval = 30 + static let maximumResponseBytes = 256 * 1024 + static let followsRedirects = false +} + +@available(iOS 15.0, macOS 12.0, *) +final class PasskeyBackupBoundedSessionDelegate: NSObject, URLSessionDataDelegate, @unchecked Sendable { + private final class RequestCancellationState: @unchecked Sendable { + let lock = NSLock() + var isCancelled = false + } + + private struct RequestState { + var data = Data() + var response: URLResponse? + let continuation: CheckedContinuation<(Data, URLResponse), Error> + } + + private let maximumResponseBytes: Int + private let lock = NSLock() + private var states: [Int: RequestState] = [:] + + init(maximumResponseBytes: Int) { + self.maximumResponseBytes = maximumResponseBytes + } + + var inFlightRequestCount: Int { + lock.lock() + defer { lock.unlock() } + return states.count + } + + static func redirectedRequest(_ request: URLRequest) -> URLRequest? { + PasskeyBackupHTTPTransportPolicy.followsRedirects ? request : nil + } + + func data(for request: URLRequest, session: URLSession) async throws -> (Data, URLResponse) { + let task = session.dataTask(with: request) + let cancellationState = RequestCancellationState() + + return try await withTaskCancellationHandler { + try await withCheckedThrowingContinuation { continuation in + cancellationState.lock.lock() + let wasCancelled = cancellationState.isCancelled + if !wasCancelled { + lock.lock() + states[task.taskIdentifier] = RequestState(continuation: continuation) + lock.unlock() + } + cancellationState.lock.unlock() + + if wasCancelled { + task.cancel() + continuation.resume(throwing: CancellationError()) + } else { + task.resume() + } + } + } onCancel: { + self.cancel(task: task, cancellationState: cancellationState) + } + } + + func urlSession( + _: URLSession, + task _: URLSessionTask, + willPerformHTTPRedirection _: HTTPURLResponse, + newRequest request: URLRequest, + completionHandler: @escaping (URLRequest?) -> Void + ) { + completionHandler(Self.redirectedRequest(request)) + } + + func urlSession( + _: URLSession, + dataTask: URLSessionDataTask, + didReceive response: URLResponse, + completionHandler: @escaping (URLSession.ResponseDisposition) -> Void + ) { + if response.expectedContentLength > Int64(maximumResponseBytes) { + resolveOversized(task: dataTask) + completionHandler(.cancel) + return + } + + lock.lock() + if var state = states[dataTask.taskIdentifier] { + state.response = response + states[dataTask.taskIdentifier] = state + } + lock.unlock() + completionHandler(.allow) + } + + func urlSession(_: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { + var oversized = false + lock.lock() + if var state = states[dataTask.taskIdentifier] { + let (nextCount, overflowed) = state.data.count.addingReportingOverflow(data.count) + if overflowed || nextCount > maximumResponseBytes { + oversized = true + } else { + state.data.append(data) + states[dataTask.taskIdentifier] = state + } + } + lock.unlock() + + if oversized { + resolveOversized(task: dataTask) + dataTask.cancel() + } + } + + func urlSession( + _: URLSession, + task: URLSessionTask, + didCompleteWithError error: Error? + ) { + lock.lock() + let state = states.removeValue(forKey: task.taskIdentifier) + lock.unlock() + guard let state else { + return + } + + if let error { + state.continuation.resume(throwing: error) + } else if let response = state.response { + state.continuation.resume(returning: (state.data, response)) + } else { + state.continuation.resume(throwing: PasskeyBackupError.malformedChallengeServiceResponse) + } + } + + private func resolveOversized(task: URLSessionTask) { + lock.lock() + let state = states.removeValue(forKey: task.taskIdentifier) + lock.unlock() + state?.continuation.resume(throwing: PasskeyBackupError.challengeServiceResponseTooLarge) + } + + private func cancel( + task: URLSessionTask, + cancellationState: RequestCancellationState + ) { + cancellationState.lock.lock() + cancellationState.isCancelled = true + lock.lock() + let state = states.removeValue(forKey: task.taskIdentifier) + lock.unlock() + cancellationState.lock.unlock() + + task.cancel() + state?.continuation.resume(throwing: CancellationError()) + } +} + @available(iOS 15.0, macOS 12.0, *) final class URLSessionPasskeyBackupHTTPTransport: PasskeyBackupHTTPTransport { private let session: URLSession + private let delegate: PasskeyBackupBoundedSessionDelegate + + init( + configuration: URLSessionConfiguration? = nil, + maximumResponseBytes: Int = PasskeyBackupHTTPTransportPolicy.maximumResponseBytes + ) throws { + guard (1 ... PasskeyBackupHTTPTransportPolicy.maximumResponseBytes) + .contains(maximumResponseBytes) else { + throw PasskeyBackupError.invalidResponseSizeLimit + } + let configuration = (configuration ?? .ephemeral).copy() as! URLSessionConfiguration + configuration.timeoutIntervalForRequest = PasskeyBackupHTTPTransportPolicy.requestTimeout + configuration.timeoutIntervalForResource = PasskeyBackupHTTPTransportPolicy.resourceTimeout + configuration.requestCachePolicy = .reloadIgnoringLocalAndRemoteCacheData + configuration.urlCache = nil + configuration.httpCookieStorage = nil + configuration.httpShouldSetCookies = false + delegate = PasskeyBackupBoundedSessionDelegate(maximumResponseBytes: maximumResponseBytes) + session = URLSession(configuration: configuration, delegate: delegate, delegateQueue: nil) + } - init(session: URLSession = .shared) { - self.session = session + deinit { + session.invalidateAndCancel() } + var inFlightRequestCount: Int { delegate.inFlightRequestCount } + func execute(_ request: PasskeyBackupHTTPRequest) async throws -> PasskeyBackupHTTPResponse { var urlRequest = URLRequest(url: request.url) urlRequest.httpMethod = request.method @@ -325,7 +1071,7 @@ final class URLSessionPasskeyBackupHTTPTransport: PasskeyBackupHTTPTransport { } urlRequest.httpBody = request.body - let (data, response) = try await session.data(for: urlRequest) + let (data, response) = try await delegate.data(for: urlRequest, session: session) guard let httpResponse = response as? HTTPURLResponse else { throw PasskeyBackupError.malformedChallengeServiceResponse } @@ -335,33 +1081,294 @@ final class URLSessionPasskeyBackupHTTPTransport: PasskeyBackupHTTPTransport { } final class HTTPPasskeyBackupChallengeService: PasskeyBackupChallengeService { - private static let registrationChallengePath = "/api/passkey-backup/v1/registration/challenge" - private static let registrationCompletePath = "/api/passkey-backup/v1/registration/complete" - private static let assertionChallengePath = "/api/passkey-backup/v1/assertion/challenge" - private static let assertionCompletePath = "/api/passkey-backup/v1/assertion/complete" + private static let expectedResponseKeysByPath: [String: Set] = [ + PasskeyBackupAuthorizationRequest.registrationChallengePath: [ + "registrationId", "challenge", "userId", "userName", "displayName", + "storageKey", "rpId", "schemaVersion" + ], + PasskeyBackupAuthorizationRequest.registrationCompletePath: [ + "storageKey", "rpId", "schemaVersion" + ], + PasskeyBackupAuthorizationRequest.assertionChallengePath: [ + "assertionId", "challenge", "storageKey", "rpId", "schemaVersion" + ], + PasskeyBackupAuthorizationRequest.assertionCompletePath: [ + "storageKey", "rpId", "schemaVersion" + ], + PasskeyBackupAuthorizationRequest.credentialsListPath: [ + "storageKey", "credentials", "rpId", "schemaVersion" + ], + PasskeyBackupAuthorizationRequest.credentialsRevokePath: [ + "storageKey", "credentialId", "remainingCredentials", "rpId", "schemaVersion" + ], + PasskeyBackupAuthorizationRequest.credentialsRevokeAllPath: [ + "storageKey", "remainingCredentials", "rpId", "schemaVersion" + ] + ] private let baseURL: URL private let transport: PasskeyBackupHTTPTransport + private let authorizationProvider: PasskeyBackupAuthorizationProvider - convenience init(baseURL: String, transport: PasskeyBackupHTTPTransport) throws { + convenience init( + baseURL: String, + transport: PasskeyBackupHTTPTransport, + authorizationProvider: PasskeyBackupAuthorizationProvider = + UnavailableBackupAuthorizationProvider() + ) throws { let normalized = baseURL.trimmingCharacters(in: .whitespacesAndNewlines) - guard let url = URL(string: normalized) else { + guard normalized == baseURL, let url = URL(string: normalized) else { throw PasskeyBackupError.invalidChallengeServiceURL(baseURL) } - try self.init(baseURL: url, transport: transport) + try self.init( + baseURL: url, + transport: transport, + authorizationProvider: authorizationProvider + ) } - init(baseURL: URL, transport: PasskeyBackupHTTPTransport) throws { + init( + baseURL: URL, + transport: PasskeyBackupHTTPTransport, + authorizationProvider: PasskeyBackupAuthorizationProvider = + UnavailableBackupAuthorizationProvider() + ) throws { self.baseURL = try Self.normalizedBaseURL(baseURL) self.transport = transport + self.authorizationProvider = authorizationProvider } @available(iOS 15.0, macOS 12.0, *) - convenience init(baseURL: String) throws { - try self.init(baseURL: baseURL, transport: URLSessionPasskeyBackupHTTPTransport()) + convenience init( + baseURL: String, + authorizationProvider: PasskeyBackupAuthorizationProvider = + UnavailableBackupAuthorizationProvider() + ) throws { + try self.init( + baseURL: baseURL, + transport: try URLSessionPasskeyBackupHTTPTransport(), + authorizationProvider: authorizationProvider + ) + } + + private func post(path: String, body: [String: Any]) async throws -> [String: Any] { + let requestBody = try JSONSerialization.data(withJSONObject: body, options: []) + let authorizationRequest = try PasskeyBackupAuthorizationRequest( + method: "POST", + path: path, + bodySha256: Self.sha256Base64URL(requestBody) + ) + let authorizationToken = try Self.validAuthorizationToken( + await authorizationProvider.authorizationToken(for: authorizationRequest) + ) + let response: PasskeyBackupHTTPResponse + do { + response = try await transport.execute( + PasskeyBackupHTTPRequest( + method: "POST", + url: endpoint(path: path), + headers: [ + "Content-Type": "application/json; charset=utf-8", + "Authorization": "Bearer \(authorizationToken)" + ], + body: requestBody + ) + ) + } catch let error as CancellationError { + if path == PasskeyBackupAuthorizationRequest.registrationCompletePath { + throw PasskeyBackupError.registrationCompletionOutcomeUnknown + } + throw error + } catch { + if path == PasskeyBackupAuthorizationRequest.registrationCompletePath { + throw PasskeyBackupError.registrationCompletionOutcomeUnknown + } + throw error + } + + // Keep the service safe when a test, alternate client, or future + // composition injects a transport that does not enforce URLSession's + // streaming response cap. + guard response.body.count <= PasskeyBackupHTTPTransportPolicy.maximumResponseBytes else { + throw PasskeyBackupError.challengeServiceResponseTooLarge + } + + guard response.statusCode == 200 else { + if path == PasskeyBackupAuthorizationRequest.registrationCompletePath, + Self.isAmbiguousRegistrationCompletionStatus(response.statusCode) { + throw PasskeyBackupError.registrationCompletionOutcomeUnknown + } + throw PasskeyBackupError.challengeServiceHTTPStatus(response.statusCode) + } + + guard !response.body.isEmpty else { + if path == PasskeyBackupAuthorizationRequest.registrationCompletePath { + throw PasskeyBackupError.registrationCompletionOutcomeUnknown + } + throw PasskeyBackupError.emptyChallengeServiceResponse + } + + do { + guard let root = try JSONSerialization.jsonObject(with: response.body) as? [String: Any], + let expectedKeys = Self.expectedResponseKeysByPath[path], + Set(root.keys) == expectedKeys else { + throw PasskeyBackupError.malformedChallengeServiceResponse + } + + return root + } catch let error as PasskeyBackupError { + if path == PasskeyBackupAuthorizationRequest.registrationCompletePath, + error == .malformedChallengeServiceResponse { + throw PasskeyBackupError.registrationCompletionOutcomeUnknown + } + throw error + } catch { + if path == PasskeyBackupAuthorizationRequest.registrationCompletePath { + throw PasskeyBackupError.registrationCompletionOutcomeUnknown + } + throw PasskeyBackupError.malformedChallengeServiceResponse + } + } + + private func endpoint(path: String) -> URL { + var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false)! + var basePath = components.percentEncodedPath.trimmingCharacters(in: CharacterSet(charactersIn: "/")) + let endpointPath = path.trimmingCharacters(in: CharacterSet(charactersIn: "/")) + + if !basePath.isEmpty { + basePath += "/" + } + + components.percentEncodedPath = "/" + basePath + endpointPath + return components.url! + } + + private static func isAmbiguousRegistrationCompletionStatus(_ statusCode: Int) -> Bool { + (500 ... 599).contains(statusCode) || [408, 425, 429].contains(statusCode) + } + + private func challengeResult(_ response: [String: Any]) throws -> PasskeyBackupChallengeResult { + try requireRelyingPartyId(response) + return try PasskeyBackupChallengeResult( + storageKey: requiredString(response, name: "storageKey"), + schemaVersion: requiredInt(response, name: "schemaVersion") + ) + } + + private func requireRelyingPartyId(_ response: [String: Any]) throws { + try PasskeyBackupContract.validateRelyingPartyId(requiredString(response, name: "rpId")) } + private func credentialJSONObject(_ credentialResponseJSON: String) throws -> [String: Any] { + let normalized = credentialResponseJSON.trimmingCharacters(in: .whitespacesAndNewlines) + guard !normalized.isEmpty, let data = normalized.data(using: .utf8) else { + throw PasskeyBackupError.invalidCredentialResponse + } + + do { + guard let credential = try JSONSerialization.jsonObject(with: data) as? [String: Any], + !credential.isEmpty else { + throw PasskeyBackupError.invalidCredentialResponse + } + + return credential + } catch let error as PasskeyBackupError { + throw error + } catch { + throw PasskeyBackupError.invalidCredentialResponse + } + } + + private func requiredString(_ response: [String: Any], name: String) throws -> String { + guard let value = response[name] as? String, + !value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { + throw PasskeyBackupError.malformedChallengeServiceResponse + } + + return value + } + + private func requiredInt(_ response: [String: Any], name: String) throws -> Int { + guard let value = response[name] as? NSNumber, + String(cString: value.objCType) != "c", + value.stringValue.range( + of: #"\A(0|[1-9][0-9]*)\z"#, + options: .regularExpression + ) != nil, + let parsed = Int(value.stringValue) else { + throw PasskeyBackupError.malformedChallengeServiceResponse + } + + return parsed + } + + private func requiredBool(_ response: [String: Any], name: String) throws -> Bool { + guard let value = response[name] as? NSNumber, + String(cString: value.objCType) == "c" else { + throw PasskeyBackupError.malformedChallengeServiceResponse + } + return value.boolValue + } + + private static func requiredLocalString(_ value: String) throws -> String { + let normalized = value.trimmingCharacters(in: .whitespacesAndNewlines) + guard !normalized.isEmpty, normalized.count <= 128 else { + throw PasskeyBackupError.malformedChallengeServiceResponse + } + + return normalized + } + + private static func sha256Base64URL(_ data: Data) -> String { + Data(SHA256.hash(data: data)) + .base64EncodedString() + .replacingOccurrences(of: "+", with: "-") + .replacingOccurrences(of: "/", with: "_") + .replacingOccurrences(of: "=", with: "") + } + + private static func validAuthorizationToken(_ token: String) throws -> String { + guard (1 ... 4096).contains(token.utf8.count), + token.range( + of: #"\A[A-Za-z0-9._~+/\-]+={0,}\z"#, + options: .regularExpression + ) != nil else { + throw PasskeyBackupError.invalidAuthorizationToken + } + return token + } + + private static func normalizedBaseURL(_ url: URL) throws -> URL { + guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false), + components.scheme == "https", + !(components.host ?? "").isEmpty, + components.host == components.host?.lowercased(), + components.user == nil, + components.password == nil, + components.port == nil, + components.query == nil, + components.fragment == nil, + components.percentEncodedPath.isEmpty || components.percentEncodedPath == "/" else { + throw PasskeyBackupError.invalidChallengeServiceURL(url.absoluteString) + } + + let host = components.host! + let canonicalURL = "https://\(host)" + guard url.absoluteString == canonicalURL || url.absoluteString == canonicalURL + "/" else { + throw PasskeyBackupError.invalidChallengeServiceURL(url.absoluteString) + } + components.percentEncodedPath = "" + + guard let normalized = components.url else { + throw PasskeyBackupError.invalidChallengeServiceURL(url.absoluteString) + } + + return normalized + } +} + +extension HTTPPasskeyBackupChallengeService { func registrationChallenge( walletId: String, accountName: String, @@ -372,7 +1379,7 @@ final class HTTPPasskeyBackupChallengeService: PasskeyBackupChallengeService { let normalizedDisplayName = try Self.requiredLocalString(displayName) let response = try await post( - path: Self.registrationChallengePath, + path: PasskeyBackupAuthorizationRequest.registrationChallengePath, body: [ "walletId": normalizedWalletId, "accountName": normalizedAccountName, @@ -405,7 +1412,7 @@ final class HTTPPasskeyBackupChallengeService: PasskeyBackupChallengeService { credentialResponseJSON: String ) async throws -> PasskeyBackupChallengeResult { let response = try await post( - path: Self.registrationCompletePath, + path: PasskeyBackupAuthorizationRequest.registrationCompletePath, body: [ "registrationId": try PasskeyBackupContract.validateCeremonyId(registrationId), "rpId": PasskeyBackupContract.PASSKEY_RP_ID, @@ -413,13 +1420,17 @@ final class HTTPPasskeyBackupChallengeService: PasskeyBackupChallengeService { ] ) - return try challengeResult(response) + do { + return try challengeResult(response) + } catch { + throw PasskeyBackupError.registrationCompletionOutcomeUnknown + } } func assertionChallenge(storageKey: String) async throws -> PasskeyBackupAssertionChallenge { let normalizedStorageKey = try PasskeyBackupContract.validateStorageKey(storageKey) let response = try await post( - path: Self.assertionChallengePath, + path: PasskeyBackupAuthorizationRequest.assertionChallengePath, body: [ "storageKey": normalizedStorageKey, "rpId": PasskeyBackupContract.PASSKEY_RP_ID, @@ -451,7 +1462,7 @@ final class HTTPPasskeyBackupChallengeService: PasskeyBackupChallengeService { credentialResponseJSON: String ) async throws -> PasskeyBackupChallengeResult { let response = try await post( - path: Self.assertionCompletePath, + path: PasskeyBackupAuthorizationRequest.assertionCompletePath, body: [ "assertionId": try PasskeyBackupContract.validateCeremonyId(assertionId), "rpId": PasskeyBackupContract.PASSKEY_RP_ID, @@ -461,131 +1472,118 @@ final class HTTPPasskeyBackupChallengeService: PasskeyBackupChallengeService { return try challengeResult(response) } +} - private func post(path: String, body: [String: Any]) async throws -> [String: Any] { - let requestBody = try JSONSerialization.data(withJSONObject: body, options: []) - let response = try await transport.execute( - PasskeyBackupHTTPRequest( - method: "POST", - url: endpoint(path: path), - headers: ["Content-Type": "application/json; charset=utf-8"], - body: requestBody - ) +extension HTTPPasskeyBackupChallengeService { + func listCredentials(storageKey: String) async throws -> PasskeyBackupCredentialListResult { + let normalizedStorageKey = try PasskeyBackupContract.validateStorageKey(storageKey) + let response = try await post( + path: PasskeyBackupAuthorizationRequest.credentialsListPath, + body: lifecycleBody(storageKey: normalizedStorageKey) ) - - guard (200 ... 299).contains(response.statusCode) else { - throw PasskeyBackupError.challengeServiceHTTPStatus(response.statusCode) - } - - guard !response.body.isEmpty else { - throw PasskeyBackupError.emptyChallengeServiceResponse + try requireRelyingPartyId(response) + try requireResponseStorageKey(response, expected: normalizedStorageKey) + guard let rawCredentials = response["credentials"] as? [Any], rawCredentials.count <= 32 else { + throw PasskeyBackupError.malformedChallengeServiceResponse } - - do { - guard let root = try JSONSerialization.jsonObject(with: response.body) as? [String: Any] else { + let credentials = try rawCredentials.map { value -> PasskeyBackupCredentialSummary in + guard let credential = value as? [String: Any] else { throw PasskeyBackupError.malformedChallengeServiceResponse } - - return root - } catch let error as PasskeyBackupError { - throw error - } catch { - throw PasskeyBackupError.malformedChallengeServiceResponse - } - } - - private func endpoint(path: String) -> URL { - var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false)! - var basePath = components.percentEncodedPath.trimmingCharacters(in: CharacterSet(charactersIn: "/")) - let endpointPath = path.trimmingCharacters(in: CharacterSet(charactersIn: "/")) - - if !basePath.isEmpty { - basePath += "/" + let requiredKeys: Set = [ + "id", "aaguid", "registrationPlatform", "deviceType", "backedUp" + ] + guard Set(credential.keys) == requiredKeys || + Set(credential.keys) == requiredKeys.union(["transports"]) else { + throw PasskeyBackupError.malformedChallengeServiceResponse + } + let transports: [String]? + if let rawTransports = credential["transports"] { + guard let values = rawTransports as? [String] else { + throw PasskeyBackupError.malformedChallengeServiceResponse + } + transports = values + } else { + transports = nil + } + return try PasskeyBackupCredentialSummary( + id: requiredString(credential, name: "id"), + aaguid: requiredString(credential, name: "aaguid"), + registrationPlatform: requiredString(credential, name: "registrationPlatform"), + deviceType: requiredString(credential, name: "deviceType"), + backedUp: try requiredBool(credential, name: "backedUp"), + transports: transports + ) } - - components.percentEncodedPath = "/" + basePath + endpointPath - return components.url! - } - - private func challengeResult(_ response: [String: Any]) throws -> PasskeyBackupChallengeResult { - try requireRelyingPartyId(response) - return try PasskeyBackupChallengeResult( - storageKey: requiredString(response, name: "storageKey"), + return try PasskeyBackupCredentialListResult( + storageKey: normalizedStorageKey, + credentials: credentials, schemaVersion: requiredInt(response, name: "schemaVersion") ) } - private func requireRelyingPartyId(_ response: [String: Any]) throws { - try PasskeyBackupContract.validateRelyingPartyId(requiredString(response, name: "rpId")) - } - - private func credentialJSONObject(_ credentialResponseJSON: String) throws -> [String: Any] { - let normalized = credentialResponseJSON.trimmingCharacters(in: .whitespacesAndNewlines) - guard !normalized.isEmpty, let data = normalized.data(using: .utf8) else { - throw PasskeyBackupError.invalidCredentialResponse - } - - do { - guard let credential = try JSONSerialization.jsonObject(with: data) as? [String: Any], - !credential.isEmpty else { - throw PasskeyBackupError.invalidCredentialResponse - } - - return credential - } catch let error as PasskeyBackupError { - throw error - } catch { - throw PasskeyBackupError.invalidCredentialResponse - } - } - - private func requiredString(_ response: [String: Any], name: String) throws -> String { - guard let value = response[name] as? String, - !value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { - throw PasskeyBackupError.malformedChallengeServiceResponse - } - - return value - } - - private func requiredInt(_ response: [String: Any], name: String) throws -> Int { - guard let value = response[name] as? NSNumber, - String(cString: value.objCType) != "c" else { - throw PasskeyBackupError.malformedChallengeServiceResponse - } - - return value.intValue - } - - private static func requiredLocalString(_ value: String) throws -> String { - let normalized = value.trimmingCharacters(in: .whitespacesAndNewlines) - guard !normalized.isEmpty else { + func revokeCredential( + storageKey: String, + credentialId: String + ) async throws -> PasskeyBackupCredentialRevokeResult { + let normalizedStorageKey = try PasskeyBackupContract.validateStorageKey(storageKey) + let normalizedCredentialId = try PasskeyBackupContract.validateCredentialId(credentialId) + var body = lifecycleBody(storageKey: normalizedStorageKey) + body["credentialId"] = normalizedCredentialId + let response = try await post( + path: PasskeyBackupAuthorizationRequest.credentialsRevokePath, + body: body + ) + try requireRelyingPartyId(response) + try requireResponseStorageKey(response, expected: normalizedStorageKey) + guard try requiredString(response, name: "credentialId") == normalizedCredentialId else { throw PasskeyBackupError.malformedChallengeServiceResponse } + return try PasskeyBackupCredentialRevokeResult( + storageKey: normalizedStorageKey, + credentialId: normalizedCredentialId, + remainingCredentials: requiredInt(response, name: "remainingCredentials"), + schemaVersion: requiredInt(response, name: "schemaVersion") + ) + } - return normalized - } - - private static func normalizedBaseURL(_ url: URL) throws -> URL { - guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false), - components.scheme == "https", - !(components.host ?? "").isEmpty, - components.user == nil, - components.password == nil, - components.query == nil, - components.fragment == nil else { - throw PasskeyBackupError.invalidChallengeServiceURL(url.absoluteString) - } - - while components.percentEncodedPath.hasSuffix("/") { - components.percentEncodedPath.removeLast() + func revokeAllCredentials(storageKey: String) async throws -> PasskeyBackupCredentialRevokeResult { + let normalizedStorageKey = try PasskeyBackupContract.validateStorageKey(storageKey) + let response = try await post( + path: PasskeyBackupAuthorizationRequest.credentialsRevokeAllPath, + body: lifecycleBody(storageKey: normalizedStorageKey) + ) + try requireRelyingPartyId(response) + try requireResponseStorageKey(response, expected: normalizedStorageKey) + let remainingCredentials = try requiredInt(response, name: "remainingCredentials") + guard remainingCredentials == 0 else { + throw PasskeyBackupError.malformedChallengeServiceResponse } + return try PasskeyBackupCredentialRevokeResult( + storageKey: normalizedStorageKey, + credentialId: nil, + remainingCredentials: remainingCredentials, + schemaVersion: requiredInt(response, name: "schemaVersion") + ) + } - guard let normalized = components.url else { - throw PasskeyBackupError.invalidChallengeServiceURL(url.absoluteString) - } + private func lifecycleBody(storageKey: String) -> [String: Any] { + [ + "storageKey": storageKey, + "rpId": PasskeyBackupContract.PASSKEY_RP_ID, + "schemaVersion": PasskeyBackupContract.schemaVersion + ] + } - return normalized + private func requireResponseStorageKey( + _ response: [String: Any], + expected: String + ) throws { + guard try PasskeyBackupContract.validateStorageKey( + requiredString(response, name: "storageKey") + ) == expected else { + throw PasskeyBackupError.mismatchedChallengeStorageKey + } } } @@ -616,6 +1614,8 @@ struct PasskeyBackupEncryptedRecord: Equatable { throw PasskeyBackupError.emptyEncryptedPayload } + try PasskeyBackupEnvelopeV1Format.validateCanonicalEnvelope(encryptedPayload) + guard schemaVersion == PasskeyBackupContract.schemaVersion else { throw PasskeyBackupError.unsupportedSchemaVersion(schemaVersion) } @@ -627,6 +1627,16 @@ struct PasskeyBackupEncryptedRecord: Equatable { func cloudKitRecordID() throws -> CKRecord.ID { CKRecord.ID(recordName: try PasskeyBackupContract.validateStorageKey(storageKey)) } + + func envelopeMetadata() throws -> PasskeyBackupEnvelopeMetadata { + try PasskeyBackupEnvelopeMetadata( + storageKey: storageKey, + walletId: walletId, + accountName: accountName, + createdAtMillis: createdAtMillis, + schemaVersion: schemaVersion + ) + } } protocol PasskeyBackupCloudStorage { @@ -881,25 +1891,71 @@ struct PendingPasskeyBackupAssertion: Equatable { } } +private enum PasskeyBackupRegistrationCompensationOutcome { + case succeeded + case revokeFailed(Error) + case timedOut +} + +private actor PasskeyBackupCompensationLatch { + private var outcome: PasskeyBackupRegistrationCompensationOutcome? + private var waiters: [ + CheckedContinuation + ] = [] + + func wait() async -> PasskeyBackupRegistrationCompensationOutcome { + if let outcome { + return outcome + } + return await withCheckedContinuation { continuation in + waiters.append(continuation) + } + } + + func finish(_ outcome: PasskeyBackupRegistrationCompensationOutcome) { + guard self.outcome == nil else { + return + } + self.outcome = outcome + let pendingWaiters = waiters + waiters.removeAll() + pendingWaiters.forEach { $0.resume(returning: outcome) } + } +} + final class PasskeyBackupWorkflow { private let challengeService: PasskeyBackupChallengeService private let cloudStorage: PasskeyBackupCloudStorage + private let backupKeyProvider: RecoverablePasskeyBackupKeyProvider + private let envelopeCryptography: PasskeyBackupEnvelopeCryptography private let isReleaseEnabled: Bool private let createdAtMillisProvider: () -> Int64 + private let compensationTimeoutNanoseconds: UInt64 init( challengeService: PasskeyBackupChallengeService, cloudStorage: PasskeyBackupCloudStorage, + backupKeyProvider: RecoverablePasskeyBackupKeyProvider = + UnavailablePasskeyBackupKeyProvider(), + envelopeCryptography: PasskeyBackupEnvelopeCryptography = + AESGCMPasskeyBackupEnvelopeCryptography(), relyingPartyId: String = PasskeyBackupContract.PASSKEY_RP_ID, isReleaseEnabled: Bool = PasskeyBackupReleaseConfig.isPasskeyBackupEnabled, + compensationTimeoutNanoseconds: UInt64 = 5_000_000_000, createdAtMillisProvider: @escaping () -> Int64 = { Int64(Date().timeIntervalSince1970 * 1000) } ) throws { + guard (1_000_000 ... 30_000_000_000).contains(compensationTimeoutNanoseconds) else { + throw PasskeyBackupError.invalidCompensationTimeout + } try PasskeyBackupContract.validateRelyingPartyId(relyingPartyId) self.challengeService = challengeService self.cloudStorage = cloudStorage + self.backupKeyProvider = backupKeyProvider + self.envelopeCryptography = envelopeCryptography self.isReleaseEnabled = isReleaseEnabled + self.compensationTimeoutNanoseconds = compensationTimeoutNanoseconds self.createdAtMillisProvider = createdAtMillisProvider } @@ -928,30 +1984,116 @@ final class PasskeyBackupWorkflow { ) } + func finishRegistrationWithEncryptedRecord( + pending: PendingPasskeyBackupRegistration, + credentialResponseJSON: String, + record: PasskeyBackupEncryptedRecord + ) async throws -> PasskeyBackupEncryptedRecord { + try PasskeyBackupReleaseConfig.validateEnabled(isReleaseEnabled) + let expectedStorageKey = try PasskeyBackupContract.validateStorageKey(pending.storageKey) + guard record.storageKey == expectedStorageKey else { + throw PasskeyBackupError.mismatchedChallengeStorageKey + } + guard record.walletId == pending.walletId else { + throw PasskeyBackupError.invalidWalletId + } + guard record.accountName == pending.accountName else { + throw PasskeyBackupError.invalidUserName + } + try await requireAuthenticatedEnvelope(record) + let credentialId = try registrationCredentialId(credentialResponseJSON) + + var completionConfirmed = false + return try await withRegistrationCompensation( + storageKey: expectedStorageKey, + credentialId: credentialId, + shouldCompensate: { error in + completionConfirmed || + error as? PasskeyBackupError == .registrationCompletionOutcomeUnknown + } + ) { + let result = try await self.challengeService.completeRegistration( + registrationId: pending.registrationId, + credentialResponseJSON: credentialResponseJSON + ) + completionConfirmed = true + _ = try self.requireMatchingStorageKey( + expected: expectedStorageKey, + actual: result.storageKey + ) + try await self.cloudStorage.savePasskeyBackup(record) + return record + } + } + + @available( + *, + deprecated, + message: "Opaque ciphertext cannot be authenticated without its original AAD metadata; use finishRegistrationWithPlaintext or finishRegistrationWithEncryptedRecord" + ) func finishRegistration( + pending _: PendingPasskeyBackupRegistration, + credentialResponseJSON _: String, + encryptedPayload _: Data + ) async throws -> PasskeyBackupEncryptedRecord { + try PasskeyBackupReleaseConfig.validateEnabled(isReleaseEnabled) + throw PasskeyBackupError.encryptedPayloadMetadataRequired + } + + func finishRegistrationWithPlaintext( pending: PendingPasskeyBackupRegistration, credentialResponseJSON: String, - encryptedPayload: Data + plaintextBackup: Data ) async throws -> PasskeyBackupEncryptedRecord { try PasskeyBackupReleaseConfig.validateEnabled(isReleaseEnabled) - let result = try await challengeService.completeRegistration( - registrationId: pending.registrationId, - credentialResponseJSON: credentialResponseJSON - ) - let storageKey = try requireMatchingStorageKey( - expected: pending.storageKey, - actual: result.storageKey - ) - let record = try PasskeyBackupEncryptedRecord( - storageKey: storageKey, + let expectedStorageKey = try PasskeyBackupContract.validateStorageKey(pending.storageKey) + let metadata = try PasskeyBackupEnvelopeMetadata( + storageKey: expectedStorageKey, walletId: pending.walletId, accountName: pending.accountName, - createdAtMillis: createdAtMillisProvider(), - encryptedPayload: encryptedPayload + createdAtMillis: createdAtMillisProvider() ) - - try await cloudStorage.savePasskeyBackup(record) - return record + let envelope: Data + do { + var key = try await backupKeyProvider.backupKey(for: metadata) + defer { key.resetBytes(in: 0 ..< key.count) } + envelope = try envelopeCryptography.encrypt( + plaintextBackup, + metadata: metadata, + key: key + ) + } + let record = try PasskeyBackupEncryptedRecord( + storageKey: metadata.storageKey, + walletId: metadata.walletId, + accountName: metadata.accountName, + createdAtMillis: metadata.createdAtMillis, + encryptedPayload: envelope, + schemaVersion: metadata.schemaVersion + ) + let credentialId = try registrationCredentialId(credentialResponseJSON) + + var completionConfirmed = false + return try await withRegistrationCompensation( + storageKey: expectedStorageKey, + credentialId: credentialId, + shouldCompensate: { error in + completionConfirmed || + error as? PasskeyBackupError == .registrationCompletionOutcomeUnknown + } + ) { + let result = try await self.challengeService.completeRegistration( + registrationId: pending.registrationId, + credentialResponseJSON: credentialResponseJSON + ) + completionConfirmed = true + _ = try self.requireMatchingStorageKey( + expected: expectedStorageKey, + actual: result.storageKey + ) + try await self.cloudStorage.savePasskeyBackup(record) + return record + } } func beginRestore(storageKey: String) async throws -> PendingPasskeyBackupAssertion { @@ -987,10 +2129,67 @@ final class PasskeyBackupWorkflow { return record } + func finishRestoreWithDecryption( + pending: PendingPasskeyBackupAssertion, + credentialResponseJSON: String + ) async throws -> Data { + let record = try await finishRestore( + pending: pending, + credentialResponseJSON: credentialResponseJSON + ) + let metadata = try record.envelopeMetadata() + var key = try await backupKeyProvider.backupKey(for: metadata) + defer { key.resetBytes(in: 0 ..< key.count) } + return try envelopeCryptography.decrypt( + record.encryptedPayload, + metadata: metadata, + key: key + ) + } + + func listCredentials(storageKey: String) async throws -> PasskeyBackupCredentialListResult { + try PasskeyBackupReleaseConfig.validateEnabled(isReleaseEnabled) + let normalizedStorageKey = try PasskeyBackupContract.validateStorageKey(storageKey) + let result = try await challengeService.listCredentials(storageKey: normalizedStorageKey) + _ = try requireMatchingStorageKey( + expected: normalizedStorageKey, + actual: result.storageKey + ) + return result + } + + func revokeCredential( + storageKey: String, + credentialId: String + ) async throws -> PasskeyBackupCredentialRevokeResult { + try PasskeyBackupReleaseConfig.validateEnabled(isReleaseEnabled) + let normalizedStorageKey = try PasskeyBackupContract.validateStorageKey(storageKey) + let normalizedCredentialId = try PasskeyBackupContract.validateCredentialId(credentialId) + let result = try await challengeService.revokeCredential( + storageKey: normalizedStorageKey, + credentialId: normalizedCredentialId + ) + _ = try requireMatchingStorageKey( + expected: normalizedStorageKey, + actual: result.storageKey + ) + guard result.credentialId == normalizedCredentialId else { + throw PasskeyBackupError.malformedChallengeServiceResponse + } + return result + } + func deleteBackup(storageKey: String) async throws { try PasskeyBackupReleaseConfig.validateEnabled(isReleaseEnabled) + let normalizedStorageKey = try PasskeyBackupContract.validateStorageKey(storageKey) + let revoked = try await challengeService.revokeAllCredentials(storageKey: normalizedStorageKey) + guard revoked.storageKey == normalizedStorageKey, + revoked.credentialId == nil, + revoked.remainingCredentials == 0 else { + throw PasskeyBackupError.malformedChallengeServiceResponse + } try await cloudStorage.deletePasskeyBackup( - storageKey: try PasskeyBackupContract.validateStorageKey(storageKey) + storageKey: normalizedStorageKey ) } @@ -1007,6 +2206,107 @@ final class PasskeyBackupWorkflow { return normalizedActual } + + private func registrationCredentialId(_ credentialResponseJSON: String) throws -> String { + guard let data = credentialResponseJSON.data(using: .utf8), + let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + let id = root["id"] as? String else { + throw PasskeyBackupError.invalidCredentialResponse + } + return try PasskeyBackupContract.validateCredentialId(id) + } + + private func requireAuthenticatedEnvelope( + _ record: PasskeyBackupEncryptedRecord + ) async throws { + let metadata = try record.envelopeMetadata() + var key = try await backupKeyProvider.backupKey(for: metadata) + defer { key.resetBytes(in: 0 ..< key.count) } + var plaintext = try envelopeCryptography.decrypt( + record.encryptedPayload, + metadata: metadata, + key: key + ) + plaintext.resetBytes(in: 0 ..< plaintext.count) + } +} + +private extension PasskeyBackupWorkflow { + func withRegistrationCompensation( + storageKey: String, + credentialId: String, + shouldCompensate: (Error) -> Bool, + operation: () async throws -> T + ) async throws -> T { + do { + return try await operation() + } catch { + let operationError = error + if shouldCompensate(operationError) { + let outcome = await attemptRegistrationCompensation( + storageKey: storageKey, + credentialId: credentialId + ) + switch outcome { + case .succeeded: + break + case let .revokeFailed(cleanupError): + throw PasskeyBackupRegistrationCompensationError( + primaryError: operationError, + failureKind: .revokeFailed, + cleanupError: cleanupError + ) + case .timedOut: + throw PasskeyBackupRegistrationCompensationError( + primaryError: operationError, + failureKind: .timedOut, + cleanupError: nil + ) + } + } + throw operationError + } + } + + func attemptRegistrationCompensation( + storageKey: String, + credentialId: String + ) async -> PasskeyBackupRegistrationCompensationOutcome { + let latch = PasskeyBackupCompensationLatch() + let challengeService = challengeService + let timeout = compensationTimeoutNanoseconds + // Unstructured tasks have independent cancellation while retaining the + // current actor context. This gives revoke a fresh opportunity without + // an unsafe detached capture when the registration task is cancelled. + let revokeTask = Task { + do { + let result = try await challengeService.revokeCredential( + storageKey: storageKey, + credentialId: credentialId + ) + guard result.storageKey == storageKey, + result.credentialId == credentialId else { + throw PasskeyBackupError.malformedChallengeServiceResponse + } + await latch.finish(.succeeded) + } catch { + await latch.finish(.revokeFailed(error)) + } + } + let timeoutTask = Task { + do { + try await Task.sleep(nanoseconds: timeout) + } catch { + return + } + await latch.finish(.timedOut) + } + + let outcome = await latch.wait() + revokeTask.cancel() + timeoutTask.cancel() + return outcome + } } @available(iOS 15.0, *) @@ -1015,11 +2315,19 @@ final class PasskeyBackupCoordinator { private let provider: ASAuthorizationPlatformPublicKeyCredentialProvider private let cloudStorage: PasskeyBackupCloudStorage + private let challengeService: PasskeyBackupChallengeService? + private let backupKeyProvider: RecoverablePasskeyBackupKeyProvider + private let envelopeCryptography: PasskeyBackupEnvelopeCryptography private let isReleaseEnabled: Bool init( relyingPartyId: String = PasskeyBackupContract.PASSKEY_RP_ID, cloudStorage: PasskeyBackupCloudStorage = UnavailablePasskeyBackupCloudStorage(), + challengeService: PasskeyBackupChallengeService? = nil, + backupKeyProvider: RecoverablePasskeyBackupKeyProvider = + UnavailablePasskeyBackupKeyProvider(), + envelopeCryptography: PasskeyBackupEnvelopeCryptography = + AESGCMPasskeyBackupEnvelopeCryptography(), isReleaseEnabled: Bool = PasskeyBackupReleaseConfig.isPasskeyBackupEnabled ) throws { try PasskeyBackupContract.validateRelyingPartyId(relyingPartyId) @@ -1029,6 +2337,9 @@ final class PasskeyBackupCoordinator { relyingPartyIdentifier: relyingPartyId ) self.cloudStorage = cloudStorage + self.challengeService = challengeService + self.backupKeyProvider = backupKeyProvider + self.envelopeCryptography = envelopeCryptography self.isReleaseEnabled = isReleaseEnabled } @@ -1043,7 +2354,7 @@ final class PasskeyBackupCoordinator { try PasskeyBackupContract.validateUserId(userId) let normalizedDisplayName = displayName.trimmingCharacters(in: .whitespacesAndNewlines) - guard !normalizedDisplayName.isEmpty else { + guard !normalizedDisplayName.isEmpty, normalizedDisplayName.count <= 128 else { throw PasskeyBackupError.invalidDisplayName } @@ -1064,20 +2375,287 @@ final class PasskeyBackupCoordinator { func saveEncryptedCloudBackup(_ record: PasskeyBackupEncryptedRecord) async throws { try PasskeyBackupReleaseConfig.validateEnabled(isReleaseEnabled) + try await requireAuthenticatedEnvelope(record) try await cloudStorage.savePasskeyBackup(record) } func loadEncryptedCloudBackup(storageKey: String) async throws -> PasskeyBackupEncryptedRecord? { try PasskeyBackupReleaseConfig.validateEnabled(isReleaseEnabled) - return try await cloudStorage.loadPasskeyBackup( + guard let record = try await cloudStorage.loadPasskeyBackup( storageKey: try PasskeyBackupContract.validateStorageKey(storageKey) - ) + ) else { + return nil + } + try await requireAuthenticatedEnvelope(record) + return record } func deleteEncryptedCloudBackup(storageKey: String) async throws { try PasskeyBackupReleaseConfig.validateEnabled(isReleaseEnabled) + let normalizedStorageKey = try PasskeyBackupContract.validateStorageKey(storageKey) + guard let challengeService else { + throw PasskeyBackupError.unavailableAuthorization + } + let revoked = try await challengeService.revokeAllCredentials(storageKey: normalizedStorageKey) + guard revoked.storageKey == normalizedStorageKey, + revoked.credentialId == nil, + revoked.remainingCredentials == 0 else { + throw PasskeyBackupError.malformedChallengeServiceResponse + } try await cloudStorage.deletePasskeyBackup( - storageKey: try PasskeyBackupContract.validateStorageKey(storageKey) + storageKey: normalizedStorageKey + ) + } + + private func requireAuthenticatedEnvelope( + _ record: PasskeyBackupEncryptedRecord + ) async throws { + let metadata = try record.envelopeMetadata() + var key = try await backupKeyProvider.backupKey(for: metadata) + defer { key.resetBytes(in: 0 ..< key.count) } + var plaintext = try envelopeCryptography.decrypt( + record.encryptedPayload, + metadata: metadata, + key: key + ) + plaintext.resetBytes(in: 0 ..< plaintext.count) + } +} + +@available(iOS 15.0, macOS 12.0, *) +@MainActor +protocol PasskeyBackupCeremonyExecutor { + func performRegistration(_ pending: PendingPasskeyBackupRegistration) async throws -> String + func performAssertion(_ pending: PendingPasskeyBackupAssertion) async throws -> String +} + +@available(iOS 15.0, macOS 12.0, *) +@MainActor +final class ASPasskeyBackupCeremonyExecutor: NSObject, + PasskeyBackupCeremonyExecutor, + ASAuthorizationControllerDelegate, + ASAuthorizationControllerPresentationContextProviding { + private enum Ceremony { + case registration + case assertion + } + + private let provider: ASAuthorizationPlatformPublicKeyCredentialProvider + private let presentationAnchorProvider: () -> ASPresentationAnchor + private let isReleaseEnabled: Bool + private var activeController: ASAuthorizationController? + private var activeCeremony: Ceremony? + private var continuation: CheckedContinuation? + + init( + relyingPartyId: String = PasskeyBackupContract.PASSKEY_RP_ID, + isReleaseEnabled: Bool = PasskeyBackupReleaseConfig.isPasskeyBackupEnabled, + presentationAnchorProvider: @escaping () -> ASPresentationAnchor + ) throws { + try PasskeyBackupContract.validateRelyingPartyId(relyingPartyId) + provider = ASAuthorizationPlatformPublicKeyCredentialProvider( + relyingPartyIdentifier: relyingPartyId + ) + self.isReleaseEnabled = isReleaseEnabled + self.presentationAnchorProvider = presentationAnchorProvider + } + + func performRegistration(_ pending: PendingPasskeyBackupRegistration) async throws -> String { + try PasskeyBackupReleaseConfig.validateEnabled(isReleaseEnabled) + try PasskeyBackupContract.validateChallenge(pending.challenge) + try PasskeyBackupContract.validateUserId(pending.userId) + let request = provider.createCredentialRegistrationRequest( + challenge: pending.challenge, + name: try PasskeyBackupContract.validateAccountName(pending.userName), + userID: pending.userId + ) + request.displayName = pending.displayName + request.userVerificationPreference = .required + request.attestationPreference = .none + return try await authorize(request: request, ceremony: .registration) + } + + func performAssertion(_ pending: PendingPasskeyBackupAssertion) async throws -> String { + try PasskeyBackupReleaseConfig.validateEnabled(isReleaseEnabled) + try PasskeyBackupContract.validateChallenge(pending.challenge) + let request = provider.createCredentialAssertionRequest(challenge: pending.challenge) + request.allowedCredentials = [] + request.userVerificationPreference = .required + return try await authorize(request: request, ceremony: .assertion) + } + + func presentationAnchor(for _: ASAuthorizationController) -> ASPresentationAnchor { + presentationAnchorProvider() + } + + func authorizationController( + controller: ASAuthorizationController, + didCompleteWithAuthorization authorization: ASAuthorization + ) { + guard controller === activeController, let ceremony = activeCeremony else { + return + } + + do { + let response: String + switch (ceremony, authorization.credential) { + case let (.registration, credential as ASAuthorizationPlatformPublicKeyCredentialRegistration): + response = try PasskeyCredentialResponseSerializer.registrationJSON(for: credential) + case let (.assertion, credential as ASAuthorizationPlatformPublicKeyCredentialAssertion): + response = try PasskeyCredentialResponseSerializer.assertionJSON(for: credential) + default: + throw PasskeyBackupError.unexpectedCredentialType + } + finish(.success(response)) + } catch { + finish(.failure(error)) + } + } + + func authorizationController( + controller: ASAuthorizationController, + didCompleteWithError error: Error + ) { + guard controller === activeController else { + return + } + finish(.failure(error)) + } + + private func authorize( + request: ASAuthorizationRequest, + ceremony: Ceremony + ) async throws -> String { + try Task.checkCancellation() + guard continuation == nil else { + throw PasskeyBackupError.ceremonyInProgress + } + + return try await withTaskCancellationHandler { + try await withCheckedThrowingContinuation { continuation in + let controller = ASAuthorizationController(authorizationRequests: [request]) + self.continuation = continuation + activeCeremony = ceremony + activeController = controller + controller.delegate = self + controller.presentationContextProvider = self + controller.performRequests() + } + } onCancel: { + Task { @MainActor [weak self] in + self?.finish(.failure(PasskeyBackupError.ceremonyCancelled)) + } + } + } + + private func finish(_ result: Result) { + guard let continuation else { + return + } + self.continuation = nil + activeController = nil + activeCeremony = nil + continuation.resume(with: result) + } +} + +@available(iOS 15.0, macOS 12.0, *) +@MainActor +final class PasskeyBackupClient { + private let workflow: PasskeyBackupWorkflow + private let ceremonyExecutor: PasskeyBackupCeremonyExecutor + private let isReleaseEnabled: Bool + + init( + workflow: PasskeyBackupWorkflow, + ceremonyExecutor: PasskeyBackupCeremonyExecutor, + isReleaseEnabled: Bool = PasskeyBackupReleaseConfig.isPasskeyBackupEnabled + ) { + self.workflow = workflow + self.ceremonyExecutor = ceremonyExecutor + self.isReleaseEnabled = isReleaseEnabled + } + + func registerBackup( + walletId: String, + accountName: String, + displayName: String, + plaintextBackup: Data + ) async throws -> PasskeyBackupEncryptedRecord { + try PasskeyBackupReleaseConfig.validateEnabled(isReleaseEnabled) + let pending = try await workflow.beginRegistration( + walletId: walletId, + accountName: accountName, + displayName: displayName + ) + let response = try await ceremonyExecutor.performRegistration(pending) + return try await workflow.finishRegistrationWithPlaintext( + pending: pending, + credentialResponseJSON: response, + plaintextBackup: plaintextBackup + ) + } + + func restoreBackup(storageKey: String) async throws -> Data { + try PasskeyBackupReleaseConfig.validateEnabled(isReleaseEnabled) + let pending = try await workflow.beginRestore(storageKey: storageKey) + let response = try await ceremonyExecutor.performAssertion(pending) + return try await workflow.finishRestoreWithDecryption( + pending: pending, + credentialResponseJSON: response + ) + } + + func listCredentials(storageKey: String) async throws -> PasskeyBackupCredentialListResult { + try PasskeyBackupReleaseConfig.validateEnabled(isReleaseEnabled) + return try await workflow.listCredentials(storageKey: storageKey) + } + + func revokeCredential( + storageKey: String, + credentialId: String + ) async throws -> PasskeyBackupCredentialRevokeResult { + try PasskeyBackupReleaseConfig.validateEnabled(isReleaseEnabled) + return try await workflow.revokeCredential( + storageKey: storageKey, + credentialId: credentialId + ) + } + + func deleteBackup(storageKey: String) async throws { + try PasskeyBackupReleaseConfig.validateEnabled(isReleaseEnabled) + try await workflow.deleteBackup(storageKey: storageKey) + } +} + +@available(iOS 15.0, macOS 12.0, *) +@MainActor +enum PasskeyBackupComposition { + static func makeClient( + challengeService: PasskeyBackupChallengeService, + cloudStorage: PasskeyBackupCloudStorage, + backupKeyProvider: RecoverablePasskeyBackupKeyProvider = + UnavailablePasskeyBackupKeyProvider(), + envelopeCryptography: PasskeyBackupEnvelopeCryptography = + AESGCMPasskeyBackupEnvelopeCryptography(), + isReleaseEnabled: Bool = PasskeyBackupReleaseConfig.isPasskeyBackupEnabled, + presentationAnchorProvider: @escaping () -> ASPresentationAnchor + ) throws -> PasskeyBackupClient { + let workflow = try PasskeyBackupWorkflow( + challengeService: challengeService, + cloudStorage: cloudStorage, + backupKeyProvider: backupKeyProvider, + envelopeCryptography: envelopeCryptography, + isReleaseEnabled: isReleaseEnabled + ) + let executor = try ASPasskeyBackupCeremonyExecutor( + isReleaseEnabled: isReleaseEnabled, + presentationAnchorProvider: presentationAnchorProvider + ) + return PasskeyBackupClient( + workflow: workflow, + ceremonyExecutor: executor, + isReleaseEnabled: isReleaseEnabled ) } } diff --git a/fearless/Common/Model/TonKeyDerivation.swift b/fearless/Common/Model/TonKeyDerivation.swift index 705367db94..212385a245 100644 --- a/fearless/Common/Model/TonKeyDerivation.swift +++ b/fearless/Common/Model/TonKeyDerivation.swift @@ -1,4 +1,5 @@ import Foundation +import IrohaCrypto struct TonDerivedAccount: Equatable { let derivationPath: String @@ -18,8 +19,14 @@ enum TonKeyDerivation { passphrase: String = "", derivationPath: String = UniversalWalletDerivationPaths.tonDefault ) throws -> TonDerivedAccount { + let normalizedMnemonic = mnemonic + .components(separatedBy: .whitespacesAndNewlines) + .filter { !$0.isEmpty } + .joined(separator: " ") + _ = try IRMnemonicCreator().mnemonic(fromList: normalizedMnemonic) + let ed25519 = try SolanaKeyDerivation.deriveAccount( - mnemonic: mnemonic, + mnemonic: normalizedMnemonic, passphrase: passphrase, derivationPath: derivationPath ) diff --git a/fearless/Common/Model/TonPendingIntentJournal.swift b/fearless/Common/Model/TonPendingIntentJournal.swift new file mode 100644 index 0000000000..983e6bea37 --- /dev/null +++ b/fearless/Common/Model/TonPendingIntentJournal.swift @@ -0,0 +1,479 @@ +import CryptoKit +import Foundation +import SoraKeystore +import TonSwift + +enum TonPendingIntentJournalError: Error, Equatable { + case unavailable + case corrupted + case conflict +} + +protocol TonPendingIntentJournaling: Sendable { + func load(senderRaw: String) throws -> TonPendingSignedIntent? + func save(_ pending: TonPendingSignedIntent) throws + func delete(senderRaw: String, expectedMessageHashHex: String) throws +} + +private extension TonPendingSignedIntent { + var journalPhaseRank: Int { + if confirmed { + return 2 + } + return emulation == nil ? 0 : 1 + } + + func hasSameBearer(as other: TonPendingSignedIntent) -> Bool { + identity == other.identity && + intent == other.intent && + message == other.message && + walletState == other.walletState && + feeQuote == other.feeQuote + } +} + +/// Thread-safe test/local store. Production uses `TonKeychainPendingIntentJournal`. +final class TonInMemoryPendingIntentJournal: TonPendingIntentJournaling, @unchecked Sendable { + private let lock = NSLock() + private var pendingBySender: [String: TonPendingSignedIntent] = [:] + + func load(senderRaw: String) throws -> TonPendingSignedIntent? { + lock.lock() + defer { lock.unlock() } + return pendingBySender[senderRaw] + } + + func save(_ pending: TonPendingSignedIntent) throws { + lock.lock() + defer { lock.unlock() } + if let existing = pendingBySender[pending.identity.sender] { + guard pending.hasSameBearer(as: existing), + pending.journalPhaseRank >= existing.journalPhaseRank + else { + throw TonPendingIntentJournalError.conflict + } + } + pendingBySender[pending.identity.sender] = pending + } + + func delete(senderRaw: String, expectedMessageHashHex: String) throws { + lock.lock() + defer { lock.unlock() } + guard let existing = pendingBySender[senderRaw] else { + return + } + guard existing.message.messageHashHex == expectedMessageHashHex else { + throw TonPendingIntentJournalError.conflict + } + pendingBySender.removeValue(forKey: senderRaw) + } +} + +/// Device-local, non-migrating storage for an already-signed bearer BOC. No mnemonic, +/// passphrase, seed, or private key is ever encoded in this record. +final class TonKeychainPendingIntentJournal: TonPendingIntentJournaling, @unchecked Sendable { + private static let identifierPrefix = "jp.co.soramitsu.fearless.ton.pending.v1." + private let keystore: KeystoreProtocol + + init(keystore: KeystoreProtocol = Keychain()) { + self.keystore = keystore + } + + func load(senderRaw: String) throws -> TonPendingSignedIntent? { + let identifier = try keyIdentifier(senderRaw: senderRaw) + do { + guard try keystore.checkKey(for: identifier) else { + return nil + } + let data = try keystore.fetchKey(for: identifier) + return try TonPendingIntentJournalCodec.decode( + data, + expectedSenderRaw: senderRaw + ) + } catch let error as TonPendingIntentJournalError { + throw error + } catch KeystoreError.noKeyFound { + return nil + } catch { + throw TonPendingIntentJournalError.unavailable + } + } + + func save(_ pending: TonPendingSignedIntent) throws { + let identifier = try keyIdentifier(senderRaw: pending.identity.sender) + if let existing = try load(senderRaw: pending.identity.sender) { + guard pending.hasSameBearer(as: existing), + pending.journalPhaseRank >= existing.journalPhaseRank + else { + throw TonPendingIntentJournalError.conflict + } + } + let data = try TonPendingIntentJournalCodec.encode(pending) + do { + try keystore.saveKey(data, with: identifier) + } catch { + throw TonPendingIntentJournalError.unavailable + } + } + + func delete(senderRaw: String, expectedMessageHashHex: String) throws { + let identifier = try keyIdentifier(senderRaw: senderRaw) + guard let existing = try load(senderRaw: senderRaw) else { + return + } + guard existing.message.messageHashHex == expectedMessageHashHex else { + throw TonPendingIntentJournalError.conflict + } + do { + try keystore.deleteKey(for: identifier) + } catch KeystoreError.noKeyFound { + return + } catch { + throw TonPendingIntentJournalError.unavailable + } + } + + private func keyIdentifier(senderRaw: String) throws -> String { + guard let address = try? TonSwift.Address.parse(raw: senderRaw), + address.toRaw() == senderRaw, + address.workchain == 0 + else { + throw TonPendingIntentJournalError.corrupted + } + let digest = Data(SHA256.hash(data: Data(senderRaw.utf8))) + .map { String(format: "%02x", $0) } + .joined() + return Self.identifierPrefix + digest + } +} + +enum TonPendingIntentJournalCodec { + static let maximumRecordBytes = 48 * 1024 + private static let schemaVersion: UInt8 = 1 + + private struct Record: Codable { + let schemaVersion: UInt8 + let phase: String + let asset: String + let network: String + let senderRaw: String + let recipientRaw: String + let amountNanotons: String + let bounce: Bool + let comment: String? + let publicKeyBase64: String + let walletSequenceNumber: UInt64 + let walletIsInitialized: Bool + let templateCreatedAt: UInt64 + let quoteIssuedAt: UInt64 + let quoteExpiresAt: UInt64 + let quoteEndpointOrigin: String + let quoteIDHex: String + let quotedFeeNanotons: UInt64 + let unsignedBocBase64: String + let signedBocBase64: String + let signedMessageHashHex: String + let signedEmulationAccepted: Bool? + let signedEmulationFeeNanotons: UInt64? + } + + static func encode(_ pending: TonPendingSignedIntent) throws -> Data { + guard let quote = pending.feeQuote else { + throw TonPendingIntentJournalError.corrupted + } + let phase: String + let accepted: Bool? + let fee: UInt64? + if pending.confirmed { + phase = "confirmed" + if let emulation = pending.emulation { + guard emulation.accepted, + emulation.totalFeeNanotons == quote.feeNanotons + else { + throw TonPendingIntentJournalError.corrupted + } + accepted = true + fee = emulation.totalFeeNanotons + } else { + accepted = nil + fee = nil + } + } else if let emulation = pending.emulation { + guard emulation.accepted, + emulation.totalFeeNanotons == quote.feeNanotons + else { + throw TonPendingIntentJournalError.corrupted + } + phase = "emulated" + accepted = true + fee = emulation.totalFeeNanotons + } else { + phase = "possiblyExposed" + accepted = nil + fee = nil + } + + let record = Record( + schemaVersion: schemaVersion, + phase: phase, + asset: "native-ton", + network: "mainnet", + senderRaw: pending.identity.sender, + recipientRaw: pending.identity.recipient, + amountNanotons: pending.identity.amountNanotons, + bounce: pending.identity.bounce, + comment: pending.identity.comment, + publicKeyBase64: quote.publicKey.base64EncodedString(), + walletSequenceNumber: pending.walletState.sequenceNumber, + walletIsInitialized: pending.walletState.isInitialized, + templateCreatedAt: quote.templateCreatedAt, + quoteIssuedAt: quote.issuedAt, + quoteExpiresAt: quote.expiresAt, + quoteEndpointOrigin: quote.endpointOrigin, + quoteIDHex: quote.quoteIDHex, + quotedFeeNanotons: quote.feeNanotons, + unsignedBocBase64: quote.unsignedMessage.bocBase64, + signedBocBase64: pending.message.bocBase64, + signedMessageHashHex: pending.message.messageHashHex, + signedEmulationAccepted: accepted, + signedEmulationFeeNanotons: fee + ) + let data = try canonicalEncoder().encode(record) + guard !data.isEmpty, data.count <= maximumRecordBytes else { + throw TonPendingIntentJournalError.corrupted + } + // Encode is also a validation boundary, not merely serialization. + _ = try decode(data, expectedSenderRaw: pending.identity.sender) + return data + } + + static func decode( + _ data: Data, + expectedSenderRaw: String + ) throws -> TonPendingSignedIntent { + do { + guard !data.isEmpty, data.count <= maximumRecordBytes else { + throw TonPendingIntentJournalError.corrupted + } + let record = try JSONDecoder().decode(Record.self, from: data) + guard try canonicalEncoder().encode(record) == data, + record.schemaVersion == schemaVersion, + record.asset == "native-ton", + record.network == "mainnet", + record.senderRaw == expectedSenderRaw, + isLowercaseHash(record.quoteIDHex), + isLowercaseHash(record.signedMessageHashHex) + else { + throw TonPendingIntentJournalError.corrupted + } + + let publicKey = try decodeCanonicalBase64(record.publicKeyBase64) + let unsignedBoc = try decodeCanonicalBase64(record.unsignedBocBase64) + let signedBoc = try decodeCanonicalBase64(record.signedBocBase64) + guard publicKey.count == 32, + publicKey.contains(where: { $0 != 0 }), + !unsignedBoc.isEmpty, + unsignedBoc.count <= TonTransferTransactionBuilder.maximumBocBytes, + !signedBoc.isEmpty, + signedBoc.count <= TonTransferTransactionBuilder.maximumBocBytes, + let sender = try? TonSwift.Address.parse(raw: record.senderRaw), + sender.toRaw() == record.senderRaw, + sender.workchain == 0, + let recipient = try? TonSwift.Address.parse(raw: record.recipientRaw), + recipient.toRaw() == record.recipientRaw, + [Int8(0), Int8(-1)].contains(recipient.workchain), + sender != recipient + else { + throw TonPendingIntentJournalError.corrupted + } + let wallet = WalletV4R2(workchain: 0, publicKey: publicKey) + guard try wallet.address() == sender else { + throw TonPendingIntentJournalError.corrupted + } + + let requestDetails = TonNativeSendRequest( + mnemonic: "", + senderAddress: record.senderRaw, + recipientAddress: record.recipientRaw, + amountNanotons: record.amountNanotons, + bounce: record.bounce, + comment: record.comment + ) + let identity = try TonTransferIntentIdentity(request: requestDetails) + let intent = try TonEmulationIntent(request: requestDetails) + let walletState = TonWalletRemoteState( + sequenceNumber: record.walletSequenceNumber, + isInitialized: record.walletIsInitialized + ) + guard walletState.isInitialized || walletState.sequenceNumber == 0 else { + throw TonPendingIntentJournalError.corrupted + } + let transactionRequest = TonTransferTransactionRequest( + senderAddress: identity.sender, + recipientAddress: identity.recipient, + amountNanotons: identity.amountNanotons, + sequenceNumber: walletState.sequenceNumber, + includeStateInit: !walletState.isInitialized, + validUntil: try signedValidUntil(signedBoc), + bounce: identity.bounce, + comment: identity.comment + ) + + let rebuiltUnsigned = try TonTransferTransactionBuilder.buildForFeeEstimation( + request: transactionRequest, + publicKey: publicKey, + now: record.templateCreatedAt + ) + guard rebuiltUnsigned.boc == unsignedBoc else { + throw TonPendingIntentJournalError.corrupted + } + let quote = try TonTransferFeeQuote( + expectedQuoteIDHex: record.quoteIDHex, + templateCreatedAt: record.templateCreatedAt, + issuedAt: record.quoteIssuedAt, + expiresAt: record.quoteExpiresAt, + endpointOrigin: record.quoteEndpointOrigin, + publicKey: publicKey, + identity: identity, + intent: intent, + transactionRequest: transactionRequest, + walletState: walletState, + unsignedMessage: rebuiltUnsigned, + feeNanotons: record.quotedFeeNanotons + ) + + let inspection = try TonTransferTransactionBuilder.inspectSignedMessage(signedBoc) + guard inspection.messageHashHex == record.signedMessageHashHex, + inspection.walletAddress == identity.sender, + inspection.recipientAddress == identity.recipient, + inspection.amountNanotons == identity.amountNanotons, + inspection.sequenceNumber == walletState.sequenceNumber, + inspection.validUntil == transactionRequest.validUntil, + inspection.includesStateInit == !walletState.isInitialized, + inspection.bounce == identity.bounce, + inspection.messageBodyHashHex == intent.messageBodyHashHex, + let signingHash = strictHexData(inspection.signingPayloadHashHex), + signingHash.count == 32, + try Curve25519.Signing.PublicKey(rawRepresentation: publicKey) + .isValidSignature(inspection.signature, for: signingHash) + else { + throw TonPendingIntentJournalError.corrupted + } + let rebuiltSigned = try TonTransferTransactionBuilder.rebuildSignedMessage( + request: transactionRequest, + publicKey: publicKey, + signature: inspection.signature, + now: record.templateCreatedAt + ) + guard rebuiltSigned.boc == signedBoc, + rebuiltSigned.messageHashHex == record.signedMessageHashHex, + rebuiltSigned.signingPayloadHashHex == quote.unsignedMessage.signingPayloadHashHex + else { + throw TonPendingIntentJournalError.corrupted + } + + let emulation: TonEmulationResult? + let confirmed: Bool + switch record.phase { + case "possiblyExposed": + guard record.signedEmulationAccepted == nil, + record.signedEmulationFeeNanotons == nil + else { + throw TonPendingIntentJournalError.corrupted + } + emulation = nil + confirmed = false + case "emulated": + guard record.signedEmulationAccepted == true, + record.signedEmulationFeeNanotons == quote.feeNanotons + else { + throw TonPendingIntentJournalError.corrupted + } + emulation = TonEmulationResult( + accepted: true, + totalFeeNanotons: record.signedEmulationFeeNanotons + ) + confirmed = false + case "confirmed": + if record.signedEmulationAccepted == nil, + record.signedEmulationFeeNanotons == nil { + emulation = nil + } else { + guard record.signedEmulationAccepted == true, + record.signedEmulationFeeNanotons == quote.feeNanotons + else { + throw TonPendingIntentJournalError.corrupted + } + emulation = TonEmulationResult( + accepted: true, + totalFeeNanotons: record.signedEmulationFeeNanotons + ) + } + confirmed = true + default: + throw TonPendingIntentJournalError.corrupted + } + + return TonPendingSignedIntent( + identity: identity, + intent: intent, + message: rebuiltSigned, + walletState: walletState, + feeQuote: quote, + emulation: emulation, + confirmed: confirmed + ) + } catch let error as TonPendingIntentJournalError { + throw error + } catch { + throw TonPendingIntentJournalError.corrupted + } + } + + private static func signedValidUntil(_ boc: Data) throws -> UInt64 { + try TonTransferTransactionBuilder.inspectSignedMessage(boc).validUntil + } + + private static func canonicalEncoder() -> JSONEncoder { + let encoder = JSONEncoder() + encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes] + return encoder + } + + private static func decodeCanonicalBase64(_ value: String) throws -> Data { + guard !value.isEmpty, + value.utf8.count <= maximumRecordBytes * 2, + let data = Data(base64Encoded: value), + data.base64EncodedString() == value + else { + throw TonPendingIntentJournalError.corrupted + } + return data + } + + private static func isLowercaseHash(_ value: String) -> Bool { + value.utf8.count == 64 && value.utf8.allSatisfy { byte in + (48 ... 57).contains(byte) || (97 ... 102).contains(byte) + } + } + + private static func strictHexData(_ value: String) -> Data? { + guard value.utf8.count.isMultiple(of: 2), + value.utf8.allSatisfy({ byte in + (48 ... 57).contains(byte) || (97 ... 102).contains(byte) + }) + else { + return nil + } + var result = Data() + result.reserveCapacity(value.utf8.count / 2) + let bytes = Array(value.utf8) + for index in stride(from: 0, to: bytes.count, by: 2) { + func nibble(_ byte: UInt8) -> UInt8 { + byte <= 57 ? byte - 48 : byte - 87 + } + result.append(nibble(bytes[index]) << 4 | nibble(bytes[index + 1])) + } + return result + } +} diff --git a/fearless/Common/Model/TonSendService.swift b/fearless/Common/Model/TonSendService.swift new file mode 100644 index 0000000000..472d1c09cb --- /dev/null +++ b/fearless/Common/Model/TonSendService.swift @@ -0,0 +1,1811 @@ +import Foundation +import TonAPI +import TonSwift + +protocol TonTransferRemoteProtocol: Sendable { + var reviewedSignedOperationOrigin: String? { get } + func walletState(address: String) async throws -> TonWalletRemoteState + func recipientRequiresMemo(address: String) async throws -> Bool + func emulateUnsigned( + message: TonUnsignedEmulationMessage, + intent: TonEmulationIntent + ) async throws -> TonEmulationResult + func emulateSigned( + message: TonSignedExternalMessage, + intent: TonEmulationIntent + ) async throws -> TonEmulationResult + func broadcast(message: TonSignedExternalMessage) async throws + func reconcile( + message: TonSignedExternalMessage, + intent: TonEmulationIntent + ) async throws -> TonReconciliationResult +} + +extension TonTransferRemoteProtocol { + var reviewedSignedOperationOrigin: String? { nil } +} + +enum TonReconciliationResult: Equatable, Sendable { + case confirmed + case notFound +} + +struct TonEmulationIntent: Equatable, Sendable { + let senderAddress: String + let recipientAddress: String + let amountNanotons: Int64 + let bounce: Bool + let messageBodyHashHex: String + + init(request: TonNativeTransferDetails) throws { + let identity = try TonTransferIntentIdentity(request: request) + guard let amount = Int64(identity.amountNanotons), + amount > 0 + else { + throw TonTransferTransactionBuilderError.invalidAmount + } + senderAddress = identity.sender + recipientAddress = identity.recipient + amountNanotons = amount + bounce = identity.bounce + messageBodyHashHex = try TonTransferTransactionBuilder.messageBodyHashHex( + comment: identity.comment + ) + } +} + +struct TonWalletRemoteState: Equatable, Sendable { + let sequenceNumber: UInt64 + let isInitialized: Bool +} + +struct TonEmulationResult: Equatable, Sendable { + let accepted: Bool + let totalFeeNanotons: UInt64? +} + +struct TonNativeSendRequest: Equatable, Sendable { + let asset: TonTransferAsset + let network: TonTransferNetwork + let mnemonic: String + let passphrase: String + let derivationPath: String + let senderAddress: String + let recipientAddress: String + let amountNanotons: String + let bounce: Bool + let comment: String? + + init( + asset: TonTransferAsset = .nativeTon, + network: TonTransferNetwork = .mainnet, + mnemonic: String, + passphrase: String = "", + derivationPath: String = UniversalWalletDerivationPaths.tonDefault, + senderAddress: String, + recipientAddress: String, + amountNanotons: String, + bounce: Bool, + comment: String? = nil + ) { + self.asset = asset + self.network = network + self.mnemonic = mnemonic + self.passphrase = passphrase + self.derivationPath = derivationPath + self.senderAddress = senderAddress + self.recipientAddress = recipientAddress + self.amountNanotons = amountNanotons + self.bounce = bounce + self.comment = comment + } +} + +struct TonSigningCredentials: Equatable, Sendable { + let mnemonic: String + let passphrase: String + let derivationPath: String + + init( + mnemonic: String, + passphrase: String = "", + derivationPath: String = UniversalWalletDerivationPaths.tonDefault + ) { + self.mnemonic = mnemonic + self.passphrase = passphrase + self.derivationPath = derivationPath + } +} + +struct TonNativeEstimateRequest: Equatable, Sendable { + let asset: TonTransferAsset + let network: TonTransferNetwork + let publicKey: Data + let senderAddress: String + let recipientAddress: String + let amountNanotons: String + let bounce: Bool + let comment: String? + + init( + asset: TonTransferAsset = .nativeTon, + network: TonTransferNetwork = .mainnet, + publicKey: Data, + senderAddress: String, + recipientAddress: String, + amountNanotons: String, + bounce: Bool, + comment: String? = nil + ) { + self.asset = asset + self.network = network + self.publicKey = publicKey + self.senderAddress = senderAddress + self.recipientAddress = recipientAddress + self.amountNanotons = amountNanotons + self.bounce = bounce + self.comment = comment + } +} + +protocol TonNativeTransferDetails { + var asset: TonTransferAsset { get } + var network: TonTransferNetwork { get } + var senderAddress: String { get } + var recipientAddress: String { get } + var amountNanotons: String { get } + var bounce: Bool { get } + var comment: String? { get } +} + +extension TonNativeSendRequest: TonNativeTransferDetails {} +extension TonNativeEstimateRequest: TonNativeTransferDetails {} + +struct TonPreparedTransferTransaction: Equatable, Sendable { + let signedMessage: TonSignedExternalMessage + let walletState: TonWalletRemoteState + let emulation: TonEmulationResult +} + +struct TonEstimatedTransferTransaction: Equatable, Sendable { + let emulationMessage: TonUnsignedEmulationMessage + let walletState: TonWalletRemoteState + let emulation: TonEmulationResult + let templateCreatedAt: UInt64 +} + +struct TonSentTransferTransaction: Equatable, Sendable { + let prepared: TonPreparedTransferTransaction + let messageHashHex: String +} + +enum TonSendServiceError: Error, Equatable { + case productionSendDisabled + case invalidAccount + case invalidClock + case inconsistentWalletState + case emulationRejected + case invalidEmulationFee + case recipientMemoRequired + case walletStateTimedOut + case recipientInspectionTimedOut + case emulationTimedOut + case broadcastTimedOut + case reconciliationTimedOut + case feeQuoteRequired + case feeQuoteMismatch + case feeQuoteExpired + case untrustedFeeQuoteEndpoint + case walletStateChangedSinceQuote + case feeChangedAfterConfirmation(quoted: UInt64, signedEmulation: UInt64) + case sendAlreadyInFlight(senderAddress: String) + case differentPendingIntent(senderAddress: String) + case priorIntentConfirmed(identity: TonTransferIntentIdentity, messageHashHex: String) + case broadcastOutcomeUnknown(messageHashHex: String) +} + +struct TonTransferIntentIdentity: Equatable, Hashable, Sendable { + let sender: String + let recipient: String + let amountNanotons: String + let bounce: Bool + let comment: String? + + init(request: TonNativeTransferDetails) throws { + guard request.amountNanotons.utf8.count <= 19 else { + throw TonTransferTransactionBuilderError.invalidAmount + } + if let comment = request.comment, + comment.utf8.count > TonTransferTransactionBuilder.maximumCommentBytes { + throw TonTransferTransactionBuilderError.invalidComment + } + guard request.senderAddress.utf8.count <= TonTransferTransactionBuilder.maximumAddressInputBytes else { + throw TonTransferTransactionBuilderError.invalidSenderAddress + } + do { + sender = try TonSwift.Address.parse(request.senderAddress).toRaw() + } catch { + throw TonTransferTransactionBuilderError.invalidSenderAddress + } + guard request.recipientAddress.utf8.count <= TonTransferTransactionBuilder.maximumAddressInputBytes else { + throw TonTransferTransactionBuilderError.invalidRecipientAddress + } + do { + recipient = try TonSwift.Address.parse(request.recipientAddress).toRaw() + } catch { + throw TonTransferTransactionBuilderError.invalidRecipientAddress + } + amountNanotons = request.amountNanotons + bounce = request.bounce + comment = request.comment + } +} + +struct TonPendingSignedIntent: Equatable, Sendable { + let identity: TonTransferIntentIdentity + let intent: TonEmulationIntent + let message: TonSignedExternalMessage + let walletState: TonWalletRemoteState + let feeQuote: TonTransferFeeQuote? + var emulation: TonEmulationResult? + var confirmed: Bool = false +} + +actor TonPendingIntentCoordinator { + enum BeginResult: Sendable { + case fresh + case retry(TonPendingSignedIntent) + case recoverDifferent(TonPendingSignedIntent) + } + + static let shared = TonPendingIntentCoordinator( + productionJournal: TonKeychainPendingIntentJournal() + ) + + private enum State { + case preparing(TonTransferIntentIdentity) + case pending(TonPendingSignedIntent) + case retrying(TonPendingSignedIntent) + } + + private let journal: TonPendingIntentJournaling + private var statesBySender: [String: State] = [:] + + private init(productionJournal journal: TonPendingIntentJournaling) { + self.journal = journal + } + + #if DEBUG + init(journal: TonPendingIntentJournaling = TonInMemoryPendingIntentJournal()) { + self.journal = journal + } + #endif + + func begin(_ identity: TonTransferIntentIdentity) throws -> BeginResult { + if statesBySender[identity.sender] == nil, + let persisted = try journal.load(senderRaw: identity.sender) { + statesBySender[identity.sender] = .pending(persisted) + } + + guard let state = statesBySender[identity.sender] else { + statesBySender[identity.sender] = .preparing(identity) + return .fresh + } + + switch state { + case let .preparing(existing): + if existing == identity { + throw TonSendServiceError.sendAlreadyInFlight(senderAddress: identity.sender) + } + throw TonSendServiceError.differentPendingIntent(senderAddress: identity.sender) + case let .pending(pending): + statesBySender[identity.sender] = .retrying(pending) + return pending.identity == identity ? .retry(pending) : .recoverDifferent(pending) + case let .retrying(pending): + if pending.identity == identity { + throw TonSendServiceError.sendAlreadyInFlight(senderAddress: identity.sender) + } + throw TonSendServiceError.differentPendingIntent(senderAddress: identity.sender) + } + } + + func reserve(_ pending: TonPendingSignedIntent) throws { + guard case let .preparing(identity) = statesBySender[pending.identity.sender], + identity == pending.identity + else { + throw TonSendServiceError.sendAlreadyInFlight(senderAddress: pending.identity.sender) + } + try journal.save(pending) + statesBySender[pending.identity.sender] = .pending(pending) + } + + func recordEmulation( + _ emulation: TonEmulationResult, + for identity: TonTransferIntentIdentity + ) throws { + let storedPending: TonPendingSignedIntent + let isRetrying: Bool + switch statesBySender[identity.sender] { + case let .pending(value): + storedPending = value + isRetrying = false + case let .retrying(value): + storedPending = value + isRetrying = true + default: + throw TonSendServiceError.sendAlreadyInFlight(senderAddress: identity.sender) + } + guard storedPending.identity == identity else { + throw TonSendServiceError.differentPendingIntent(senderAddress: identity.sender) + } + var updatedPending = storedPending + updatedPending.emulation = emulation + try journal.save(updatedPending) + statesBySender[identity.sender] = isRetrying ? .retrying(updatedPending) : .pending(updatedPending) + } + + func recordConfirmed(_ pending: TonPendingSignedIntent) throws -> TonPendingSignedIntent { + let storedPending: TonPendingSignedIntent + switch statesBySender[pending.identity.sender] { + case let .pending(value), let .retrying(value): + storedPending = value + default: + throw TonSendServiceError.sendAlreadyInFlight(senderAddress: pending.identity.sender) + } + guard storedPending.identity == pending.identity, + storedPending.message.messageHashHex == pending.message.messageHashHex + else { + throw TonSendServiceError.differentPendingIntent(senderAddress: pending.identity.sender) + } + var confirmedPending = storedPending + confirmedPending.confirmed = true + try journal.save(confirmedPending) + statesBySender[pending.identity.sender] = .pending(confirmedPending) + return confirmedPending + } + + func retainPending(_ pending: TonPendingSignedIntent) throws { + let storedPending: TonPendingSignedIntent + switch statesBySender[pending.identity.sender] { + case let .pending(value), let .retrying(value): + storedPending = value + default: + throw TonSendServiceError.sendAlreadyInFlight(senderAddress: pending.identity.sender) + } + guard storedPending.identity == pending.identity, + storedPending.message.messageHashHex == pending.message.messageHashHex + else { + throw TonSendServiceError.differentPendingIntent(senderAddress: pending.identity.sender) + } + + // Recovery tasks may finish after another task has durably confirmed this bearer. + // Preserve the most advanced phase and write it before changing actor state so a + // failed durable write can never leave memory ahead of the journal. + let retained: TonPendingSignedIntent + if storedPending.confirmed || + (storedPending.emulation != nil && pending.emulation == nil) { + retained = storedPending + } else { + retained = pending + } + try journal.save(retained) + statesBySender[pending.identity.sender] = .pending(retained) + } + + func abortBeforeExposure(_ identity: TonTransferIntentIdentity) { + guard case let .preparing(existing) = statesBySender[identity.sender], + existing == identity + else { + return + } + statesBySender.removeValue(forKey: identity.sender) + } + + func acknowledgeConfirmed( + senderRaw: String, + identity: TonTransferIntentIdentity, + messageHashHex: String + ) throws { + if statesBySender[senderRaw] == nil, + let persisted = try journal.load(senderRaw: senderRaw) { + statesBySender[senderRaw] = .pending(persisted) + } + let pending: TonPendingSignedIntent + switch statesBySender[senderRaw] { + case let .pending(value), let .retrying(value): + pending = value + default: + throw TonSendServiceError.sendAlreadyInFlight(senderAddress: senderRaw) + } + guard pending.confirmed, + pending.identity == identity, + pending.message.messageHashHex == messageHashHex + else { + throw TonSendServiceError.differentPendingIntent(senderAddress: senderRaw) + } + try journal.delete( + senderRaw: senderRaw, + expectedMessageHashHex: pending.message.messageHashHex + ) + statesBySender.removeValue(forKey: senderRaw) + } +} + +/// Coordinates a native TON transfer while keeping network access behind an injectable seam. +/// +/// Asset/network/key/address validation is completed locally before the first remote call. +/// The exact signed BOC accepted by emulation is then broadcast without reserialization. +final class TonSendService: @unchecked Sendable { + static let defaultMessageLifetimeSeconds: UInt64 = 120 + static let defaultRemoteTimeoutNanoseconds: UInt64 = 15_000_000_000 + /// A single Wallet V4 native transfer must never consume more than one TON in fees. + static let defaultMaximumFeeNanotons: UInt64 = 1_000_000_000 + + private let remote: TonTransferRemoteProtocol + private let clock: @Sendable() -> UInt64 + private let messageLifetimeSeconds: UInt64 + private let remoteTimeoutNanoseconds: UInt64 + private let maximumFeeNanotons: UInt64 + private let pendingCoordinator: TonPendingIntentCoordinator + + init( + remote: TonTransferRemoteProtocol, + messageLifetimeSeconds: UInt64 = TonSendService.defaultMessageLifetimeSeconds, + remoteTimeoutNanoseconds: UInt64 = TonSendService.defaultRemoteTimeoutNanoseconds, + maximumFeeNanotons: UInt64 = TonSendService.defaultMaximumFeeNanotons, + clock: @escaping @Sendable() -> UInt64 = { UInt64(Date().timeIntervalSince1970) } + ) { + self.remote = remote + self.messageLifetimeSeconds = messageLifetimeSeconds + self.remoteTimeoutNanoseconds = remoteTimeoutNanoseconds + self.maximumFeeNanotons = maximumFeeNanotons + pendingCoordinator = .shared + self.clock = clock + } + + #if DEBUG + init( + remote: TonTransferRemoteProtocol, + messageLifetimeSeconds: UInt64 = TonSendService.defaultMessageLifetimeSeconds, + remoteTimeoutNanoseconds: UInt64 = TonSendService.defaultRemoteTimeoutNanoseconds, + maximumFeeNanotons: UInt64 = TonSendService.defaultMaximumFeeNanotons, + pendingCoordinator: TonPendingIntentCoordinator, + clock: @escaping @Sendable() -> UInt64 = { UInt64(Date().timeIntervalSince1970) } + ) { + self.remote = remote + self.messageLifetimeSeconds = messageLifetimeSeconds + self.remoteTimeoutNanoseconds = remoteTimeoutNanoseconds + self.maximumFeeNanotons = maximumFeeNanotons + self.pendingCoordinator = pendingCoordinator + self.clock = clock + } + #endif + + private struct BuiltSignedTransfer { + let identity: TonTransferIntentIdentity + let intent: TonEmulationIntent + let message: TonSignedExternalMessage + let walletState: TonWalletRemoteState + } + + func estimate(_ request: TonNativeEstimateRequest) async throws -> TonEstimatedTransferTransaction { + let (now, validUntil) = try validatedTiming(for: request) + let intent = try TonEmulationIntent(request: request) + let provisionalRequest = transactionRequest( + from: request, + sequenceNumber: 0, + includeStateInit: false, + validUntil: validUntil + ) + + // The preview has no access to mnemonic/private material and carries an invalid + // signature even during local preflight. + _ = try TonTransferTransactionBuilder.buildForFeeEstimation( + request: provisionalRequest, + publicKey: request.publicKey, + now: now + ) + try await rejectMemoRequiredRecipient(request.recipientAddress) + + let walletState = try await loadWalletState(senderAddress: request.senderAddress) + let emulationMessage = try TonTransferTransactionBuilder.buildForFeeEstimation( + request: transactionRequest( + from: request, + sequenceNumber: walletState.sequenceNumber, + includeStateInit: !walletState.isInitialized, + validUntil: validUntil + ), + publicKey: request.publicKey, + now: now + ) + let emulation = try await withRemoteTimeout(error: .emulationTimedOut) { + try await self.remote.emulateUnsigned( + message: emulationMessage, + intent: intent + ) + } + try validateEmulation(emulation) + return TonEstimatedTransferTransaction( + emulationMessage: emulationMessage, + walletState: walletState, + emulation: emulation, + templateCreatedAt: now + ) + } + + /// Produces the exact short-lived quote displayed by confirmation. Unlike an advisory + /// estimate, a submission quote is available only on the binary-reviewed send origin. + func quote(_ request: TonNativeEstimateRequest) async throws -> TonTransferFeeQuote { + guard let endpointOrigin = remote.reviewedSignedOperationOrigin else { + throw TonSendServiceError.untrustedFeeQuoteEndpoint + } + let estimated = try await estimate(request) + guard let feeNanotons = estimated.emulation.totalFeeNanotons else { + throw TonSendServiceError.invalidEmulationFee + } + let issuedAt = clock() + let validUntil = estimated.emulationMessage.validUntil + guard issuedAt >= estimated.templateCreatedAt, + issuedAt < validUntil, + validUntil - issuedAt > TonTransferTransactionBuilder.minimumLifetimeSeconds + else { + throw TonSendServiceError.feeQuoteExpired + } + let latestQuoteExpiry = validUntil - TonTransferTransactionBuilder.minimumLifetimeSeconds + let ageBoundExpiry = issuedAt + TonTransferFeeQuote.maximumAgeSeconds + let expiresAt = min(latestQuoteExpiry, ageBoundExpiry) + guard expiresAt > issuedAt else { + throw TonSendServiceError.feeQuoteExpired + } + + let identity = try TonTransferIntentIdentity(request: request) + let intent = try TonEmulationIntent(request: request) + let transaction = TonTransferTransactionRequest( + asset: request.asset, + network: request.network, + senderAddress: identity.sender, + recipientAddress: identity.recipient, + amountNanotons: identity.amountNanotons, + sequenceNumber: estimated.walletState.sequenceNumber, + includeStateInit: !estimated.walletState.isInitialized, + validUntil: validUntil, + bounce: identity.bounce, + comment: identity.comment + ) + return try TonTransferFeeQuote( + templateCreatedAt: estimated.templateCreatedAt, + issuedAt: issuedAt, + expiresAt: expiresAt, + endpointOrigin: endpointOrigin, + publicKey: request.publicKey, + identity: identity, + intent: intent, + transactionRequest: transaction, + walletState: estimated.walletState, + unsignedMessage: estimated.emulationMessage, + feeNanotons: feeNanotons + ) + } + + func send( + _ request: TonNativeSendRequest, + feeQuote: TonTransferFeeQuote? = nil + ) async throws -> TonSentTransferTransaction { + #if !DEBUG + // Native TON submission is intentionally unavailable in production binaries + // until the durable-recovery and confirmation-binding release gates are met. + throw TonSendServiceError.productionSendDisabled + #else + return try await performSend( + request, + feeQuote: feeQuote, + requiresFeeQuote: true, + signingCredentials: { + TonSigningCredentials( + mnemonic: request.mnemonic, + passphrase: request.passphrase, + derivationPath: request.derivationPath + ) + } + ) + #endif + } + + /// Submission entry point used by the wallet integration. Pending journal recovery is + /// resolved before the closure is invoked, so a previously exposed BOC remains recoverable + /// even when mnemonic material is temporarily unavailable. + func send( + _ request: TonNativeEstimateRequest, + feeQuote: TonTransferFeeQuote?, + signingCredentials: @escaping () throws -> TonSigningCredentials + ) async throws -> TonSentTransferTransaction { + #if !DEBUG + throw TonSendServiceError.productionSendDisabled + #else + return try await performSend( + request, + feeQuote: feeQuote, + requiresFeeQuote: true, + signingCredentials: signingCredentials + ) + #endif + } + + #if DEBUG + /// Explicitly test-only compatibility seam for low-level validation/timeout tests. + /// Application integration always uses the quote-required `send` API. + func sendUnquotedForTesting( + _ request: TonNativeSendRequest + ) async throws -> TonSentTransferTransaction { + try await performSend( + request, + feeQuote: nil, + requiresFeeQuote: false, + signingCredentials: { + TonSigningCredentials( + mnemonic: request.mnemonic, + passphrase: request.passphrase, + derivationPath: request.derivationPath + ) + } + ) + } + + private func performSend( + _ request: any TonNativeTransferDetails, + feeQuote: TonTransferFeeQuote?, + requiresFeeQuote: Bool, + signingCredentials: () throws -> TonSigningCredentials + ) async throws -> TonSentTransferTransaction { + let identity = try TonTransferIntentIdentity(request: request) + switch try await pendingCoordinator.begin(identity) { + case let .retry(pending): + try requireStoredEndpointForRecovery(pending) + if requiresFeeQuote { + return try await recoverPendingWithoutRebroadcast(pending) + } + return try await retry(pending) + case let .recoverDifferent(pending): + try requireStoredEndpointForRecovery(pending) + return try await recoverDifferentPending(pending) + case .fresh: + break + } + + if requiresFeeQuote, feeQuote == nil { + await pendingCoordinator.abortBeforeExposure(identity) + throw TonSendServiceError.feeQuoteRequired + } + + let built: BuiltSignedTransfer + let emulatedPending: TonPendingSignedIntent + do { + let credentials = try signingCredentials() + let signingRequest = TonNativeSendRequest( + asset: request.asset, + network: request.network, + mnemonic: credentials.mnemonic, + passphrase: credentials.passphrase, + derivationPath: credentials.derivationPath, + senderAddress: request.senderAddress, + recipientAddress: request.recipientAddress, + amountNanotons: request.amountNanotons, + bounce: request.bounce, + comment: request.comment + ) + built = try await buildSignedTransfer( + signingRequest, + identity: identity, + feeQuote: feeQuote + ) + try await pendingCoordinator.reserve( + TonPendingSignedIntent( + identity: identity, + intent: built.intent, + message: built.message, + walletState: built.walletState, + feeQuote: feeQuote, + emulation: nil + ) + ) + } catch { + await pendingCoordinator.abortBeforeExposure(identity) + throw error + } + + do { + let emulation = try await withRemoteTimeout(error: .emulationTimedOut) { + try await self.remote.emulateSigned( + message: built.message, + intent: built.intent + ) + } + try validateEmulation(emulation) + if let feeQuote, + emulation.totalFeeNanotons != feeQuote.feeNanotons { + throw TonSendServiceError.feeChangedAfterConfirmation( + quoted: feeQuote.feeNanotons, + signedEmulation: emulation.totalFeeNanotons ?? 0 + ) + } + try await pendingCoordinator.recordEmulation(emulation, for: identity) + emulatedPending = TonPendingSignedIntent( + identity: identity, + intent: built.intent, + message: built.message, + walletState: built.walletState, + feeQuote: feeQuote, + emulation: emulation + ) + } catch { + // A valid bearer message was already exposed to the remote. Its absence at one + // instant cannot prove it will not be broadcast later, so retain it and fail with + // an explicitly ambiguous outcome unless reconciliation confirms execution. + return try await reconcileOrThrowUnknown( + TonPendingSignedIntent( + identity: identity, + intent: built.intent, + message: built.message, + walletState: built.walletState, + feeQuote: feeQuote, + emulation: nil + ) + ) + } + return try await broadcastAndReconcile(emulatedPending) + } + #endif + + func acknowledgeConfirmedTransfer( + senderAddress: String, + identity: TonTransferIntentIdentity, + messageHashHex: String + ) async throws { + guard let sender = try? TonSwift.Address.parse(senderAddress), + sender.toRaw() == identity.sender, + messageHashHex.utf8.count == 64, + messageHashHex.utf8.allSatisfy({ byte in + (48 ... 57).contains(byte) || (97 ... 102).contains(byte) + }) + else { + throw TonPendingIntentJournalError.corrupted + } + try await pendingCoordinator.acknowledgeConfirmed( + senderRaw: identity.sender, + identity: identity, + messageHashHex: messageHashHex + ) + } + + private func buildSignedTransfer( + _ request: TonNativeSendRequest, + identity: TonTransferIntentIdentity, + feeQuote: TonTransferFeeQuote? + ) async throws -> BuiltSignedTransfer { + if let feeQuote { + return try await buildQuoteBoundSignedTransfer( + request, + identity: identity, + feeQuote: feeQuote + ) + } + + let (now, validUntil) = try validatedTiming(for: request) + let intent = try TonEmulationIntent(request: request) + // Validate the mnemonic/key/sender binding locally before any remote call. Keep the + // first derived private key scoped to this synchronous preflight instead of retaining + // it across recipient/account network suspension points. + do { + try { () throws in + let account = try TonKeyDerivation.deriveAccount( + mnemonic: request.mnemonic, + passphrase: request.passphrase, + derivationPath: request.derivationPath + ) + _ = try TonTransferTransactionBuilder.buildAndSign( + request: transactionRequest( + from: request, + sequenceNumber: 0, + includeStateInit: false, + validUntil: validUntil + ), + privateKeySeed: account.privateKey, + now: now + ) + }() + } catch let error as TonTransferTransactionBuilderError { + throw error + } catch { + throw TonSendServiceError.invalidAccount + } + + try await rejectMemoRequiredRecipient(request.recipientAddress) + let walletState = try await loadWalletState(senderAddress: request.senderAddress) + let message: TonSignedExternalMessage + do { + // Re-derive only after all pre-signing network checks have completed so private + // key material is live for the shortest practical synchronous signing window. + let account = try TonKeyDerivation.deriveAccount( + mnemonic: request.mnemonic, + passphrase: request.passphrase, + derivationPath: request.derivationPath + ) + message = try TonTransferTransactionBuilder.buildAndSign( + request: transactionRequest( + from: request, + sequenceNumber: walletState.sequenceNumber, + includeStateInit: !walletState.isInitialized, + validUntil: validUntil + ), + privateKeySeed: account.privateKey, + now: now + ) + } catch let error as TonTransferTransactionBuilderError { + throw error + } catch { + throw TonSendServiceError.invalidAccount + } + return BuiltSignedTransfer( + identity: identity, + intent: intent, + message: message, + walletState: walletState + ) + } + + private func buildQuoteBoundSignedTransfer( + _ request: TonNativeSendRequest, + identity: TonTransferIntentIdentity, + feeQuote: TonTransferFeeQuote + ) async throws -> BuiltSignedTransfer { + let now = clock() + do { + try feeQuote.validate( + at: now, + endpointOrigin: remote.reviewedSignedOperationOrigin + ) + } catch TonTransferFeeQuoteError.invalidTiming { + throw TonSendServiceError.feeQuoteExpired + } catch { + throw TonSendServiceError.feeQuoteMismatch + } + guard feeQuote.identity == identity, + feeQuote.intent == (try TonEmulationIntent(request: request)) + else { + throw TonSendServiceError.feeQuoteMismatch + } + + // Validate key and sender ownership synchronously, then discard the first private-key + // derivation before any suspension point. + do { + let account = try TonKeyDerivation.deriveAccount( + mnemonic: request.mnemonic, + passphrase: request.passphrase, + derivationPath: request.derivationPath + ) + guard account.publicKey == feeQuote.publicKey, + try TonSwift.Address.parse(account.addressNonBounceable).toRaw() == identity.sender + else { + throw TonSendServiceError.invalidAccount + } + } catch let error as TonSendServiceError { + throw error + } catch { + throw TonSendServiceError.invalidAccount + } + + // Recheck mutable remote facts before signing. Any drift invalidates the visible quote. + try await rejectMemoRequiredRecipient(request.recipientAddress) + let walletState = try await loadWalletState(senderAddress: request.senderAddress) + guard walletState == feeQuote.walletState else { + throw TonSendServiceError.walletStateChangedSinceQuote + } + do { + try feeQuote.validate( + at: clock(), + endpointOrigin: remote.reviewedSignedOperationOrigin + ) + } catch TonTransferFeeQuoteError.invalidTiming { + throw TonSendServiceError.feeQuoteExpired + } catch { + throw TonSendServiceError.feeQuoteMismatch + } + + let rebuiltUnsigned = try TonTransferTransactionBuilder.buildForFeeEstimation( + request: feeQuote.transactionRequest, + publicKey: feeQuote.publicKey, + now: feeQuote.templateCreatedAt + ) + guard rebuiltUnsigned == feeQuote.unsignedMessage else { + throw TonSendServiceError.feeQuoteMismatch + } + + let signedMessage: TonSignedExternalMessage + do { + let account = try TonKeyDerivation.deriveAccount( + mnemonic: request.mnemonic, + passphrase: request.passphrase, + derivationPath: request.derivationPath + ) + signedMessage = try TonTransferTransactionBuilder.buildAndSign( + request: feeQuote.transactionRequest, + privateKeySeed: account.privateKey, + now: feeQuote.templateCreatedAt + ) + } catch let error as TonTransferTransactionBuilderError { + throw error + } catch { + throw TonSendServiceError.invalidAccount + } + guard signedMessage.publicKey == feeQuote.publicKey, + signedMessage.signingPayloadHashHex == feeQuote.unsignedMessage.signingPayloadHashHex, + signedMessage.walletAddress == feeQuote.unsignedMessage.walletAddress, + signedMessage.sequenceNumber == feeQuote.unsignedMessage.sequenceNumber, + signedMessage.validUntil == feeQuote.unsignedMessage.validUntil, + signedMessage.includesStateInit == feeQuote.unsignedMessage.includesStateInit + else { + throw TonSendServiceError.feeQuoteMismatch + } + + return BuiltSignedTransfer( + identity: identity, + intent: feeQuote.intent, + message: signedMessage, + walletState: walletState + ) + } + + private func retry(_ pending: TonPendingSignedIntent) async throws -> TonSentTransferTransaction { + var pending = pending + do { + if pending.confirmed { + return try await confirmedResult(pending) + } + let wasConfirmed: Bool + do { + wasConfirmed = try await reconciled(pending) + } catch { + // A transport failure, timeout, or malformed/mismatched confirmation is + // not proof of absence. Only explicit not-found responses may authorize + // another submission of this bearer message. + throw TonSendServiceError.broadcastOutcomeUnknown( + messageHashHex: pending.message.messageHashHex + ) + } + if wasConfirmed { + return try await confirmedResult(pending) + } + guard clock() < pending.message.validUntil else { + throw TonSendServiceError.broadcastOutcomeUnknown( + messageHashHex: pending.message.messageHashHex + ) + } + + if pending.emulation == nil { + let message = pending.message + let intent = pending.intent + let emulation: TonEmulationResult + do { + emulation = try await withRemoteTimeout(error: .emulationTimedOut) { + try await self.remote.emulateSigned( + message: message, + intent: intent + ) + } + try validateEmulation(emulation) + if let feeQuote = pending.feeQuote, + emulation.totalFeeNanotons != feeQuote.feeNanotons { + throw TonSendServiceError.feeChangedAfterConfirmation( + quoted: feeQuote.feeNanotons, + signedEmulation: emulation.totalFeeNanotons ?? 0 + ) + } + } catch { + // This exact bearer BOC was already exposed during the first attempt. + // A retry-side emulation failure cannot make that exposure unambiguous. + throw TonSendServiceError.broadcastOutcomeUnknown( + messageHashHex: pending.message.messageHashHex + ) + } + pending.emulation = emulation + try await pendingCoordinator.recordEmulation(emulation, for: pending.identity) + } + return try await broadcastAndReconcile(pending) + } catch { + try? await pendingCoordinator.retainPending(pending) + throw error + } + } + + private func requireStoredEndpointForRecovery(_ pending: TonPendingSignedIntent) throws { + guard let storedOrigin = pending.feeQuote?.endpointOrigin, + let currentOrigin = remote.reviewedSignedOperationOrigin, + storedOrigin == currentOrigin + else { + throw TonSendServiceError.broadcastOutcomeUnknown( + messageHashHex: pending.message.messageHashHex + ) + } + } + + private func recoverPendingWithoutRebroadcast( + _ pending: TonPendingSignedIntent + ) async throws -> TonSentTransferTransaction { + if pending.confirmed { + return try await confirmedResult(pending) + } + do { + if try await reconciled(pending) { + return try await confirmedResult(pending) + } + } catch { + // A transport failure or malformed response cannot authorize another bearer BOC. + } + try? await pendingCoordinator.retainPending(pending) + throw TonSendServiceError.broadcastOutcomeUnknown( + messageHashHex: pending.message.messageHashHex + ) + } + + private func recoverDifferentPending( + _ pending: TonPendingSignedIntent + ) async throws -> TonSentTransferTransaction { + // A prior outcome is surfaced and acknowledged before any different transfer may begin. + // This prevents silently executing a second payment after recovery proves the first one. + let confirmed = try await recoverPendingWithoutRebroadcast(pending) + throw TonSendServiceError.priorIntentConfirmed( + identity: pending.identity, + messageHashHex: confirmed.messageHashHex + ) + } + + private func broadcastAndReconcile( + _ pending: TonPendingSignedIntent + ) async throws -> TonSentTransferTransaction { + guard clock() < pending.message.validUntil else { + throw TonSendServiceError.broadcastOutcomeUnknown( + messageHashHex: pending.message.messageHashHex + ) + } + do { + try await withRemoteTimeout(error: .broadcastTimedOut) { + try await self.remote.broadcast(message: pending.message) + } + } catch { + // Network failure and timeout are ambiguous after bytes may have reached TonAPI. + } + return try await reconcileOrThrowUnknown(pending) + } + + private func reconcileOrThrowUnknown( + _ pending: TonPendingSignedIntent + ) async throws -> TonSentTransferTransaction { + if (try? await reconciled(pending)) == true { + return try await confirmedResult(pending) + } + throw TonSendServiceError.broadcastOutcomeUnknown( + messageHashHex: pending.message.messageHashHex + ) + } + + private func reconciled(_ pending: TonPendingSignedIntent) async throws -> Bool { + try await withRemoteTimeout(error: .reconciliationTimedOut) { + for attempt in 0 ..< 3 { + let result = try await self.remote.reconcile( + message: pending.message, + intent: pending.intent + ) + if result == .confirmed { + return true + } + if attempt < 2 { + try await Task.sleep(nanoseconds: 200_000_000) + } + } + return false + } + } + + private func confirmedResult( + _ pending: TonPendingSignedIntent + ) async throws -> TonSentTransferTransaction { + var pending = pending + if pending.confirmed { + // `begin` moves a persisted tombstone to retrying. Restore its stable pending + // state before returning so an unacknowledged receipt remains recoverable again + // in the same process as well as after restart. + try await pendingCoordinator.retainPending(pending) + } else { + do { + pending = try await pendingCoordinator.recordConfirmed(pending) + } catch { + try? await pendingCoordinator.retainPending(pending) + throw TonSendServiceError.broadcastOutcomeUnknown( + messageHashHex: pending.message.messageHashHex + ) + } + } + let emulation = pending.emulation ?? TonEmulationResult( + accepted: false, + totalFeeNanotons: nil + ) + return TonSentTransferTransaction( + prepared: TonPreparedTransferTransaction( + signedMessage: pending.message, + walletState: pending.walletState, + emulation: emulation + ), + messageHashHex: pending.message.messageHashHex + ) + } + + private func rejectMemoRequiredRecipient(_ recipientAddress: String) async throws { + let memoRequired = try await withRemoteTimeout(error: .recipientInspectionTimedOut) { + try await self.remote.recipientRequiresMemo(address: recipientAddress) + } + guard !memoRequired else { + throw TonSendServiceError.recipientMemoRequired + } + } + + private func loadWalletState(senderAddress: String) async throws -> TonWalletRemoteState { + let walletState = try await withRemoteTimeout(error: .walletStateTimedOut) { + try await self.remote.walletState(address: senderAddress) + } + guard walletState.isInitialized || walletState.sequenceNumber == 0 else { + throw TonSendServiceError.inconsistentWalletState + } + return walletState + } + + private func validateEmulation(_ emulation: TonEmulationResult) throws { + guard emulation.accepted else { + throw TonSendServiceError.emulationRejected + } + guard let feeNanotons = emulation.totalFeeNanotons, + feeNanotons > 0, + feeNanotons <= maximumFeeNanotons + else { + throw TonSendServiceError.invalidEmulationFee + } + } + + private func validatedTiming( + for request: TonNativeTransferDetails + ) throws -> (now: UInt64, validUntil: UInt64) { + guard request.asset == .nativeTon else { + throw TonTransferTransactionBuilderError.unsupportedAsset + } + guard request.network == .mainnet else { + throw TonTransferTransactionBuilderError.unsupportedNetwork + } + _ = try TonEmulationIntent(request: request) // Includes the Int64 service cap. + + let now = clock() + guard now <= UInt64(UInt32.max), + messageLifetimeSeconds >= TonTransferTransactionBuilder.minimumLifetimeSeconds, + messageLifetimeSeconds <= TonTransferTransactionBuilder.maximumLifetimeSeconds, + remoteTimeoutNanoseconds > 0, + maximumFeeNanotons > 0, + now <= UInt64(UInt32.max) - messageLifetimeSeconds + else { + throw TonSendServiceError.invalidClock + } + return (now, now + messageLifetimeSeconds) + } + + private func transactionRequest( + from request: TonNativeTransferDetails, + sequenceNumber: UInt64, + includeStateInit: Bool, + validUntil: UInt64 + ) -> TonTransferTransactionRequest { + TonTransferTransactionRequest( + asset: request.asset, + network: request.network, + senderAddress: request.senderAddress, + recipientAddress: request.recipientAddress, + amountNanotons: request.amountNanotons, + sequenceNumber: sequenceNumber, + includeStateInit: includeStateInit, + validUntil: validUntil, + bounce: request.bounce, + comment: request.comment + ) + } + + private func withRemoteTimeout( + error timeoutError: TonSendServiceError, + operation: @escaping @Sendable() async throws -> T + ) async throws -> T { + try Task.checkCancellation() + let state = TonRemoteRaceState() + + return try await withTaskCancellationHandler( + operation: { + try await withCheckedThrowingContinuation { continuation in + guard state.install(continuation), !Task.isCancelled else { + state.resolve(.failure(CancellationError())) + return + } + + let operationTask = Task { + guard state.claimOperationStart() else { + return + } + do { + state.resolve(.success(try await operation())) + } catch { + state.resolve(.failure(error)) + } + } + let timeoutTask = Task { + do { + try await Task.sleep(nanoseconds: self.remoteTimeoutNanoseconds) + state.resolve(.failure(timeoutError)) + } catch { + // Cancellation means another result already won the race. + } + } + state.registerTasks([operationTask, timeoutTask]) + } + }, + onCancel: { + state.resolve(.failure(CancellationError())) + } + ) + } +} + +/// An unstructured first-result race is intentional here: structured task groups wait for +/// cancelled children before returning, so a transport that ignores cancellation could defeat +/// the deadline. This lock protects exactly-once continuation resumption and cancels the loser. +private final class TonRemoteRaceState: @unchecked Sendable { + private let lock = NSLock() + private var continuation: CheckedContinuation? + private var resolution: Result? + private var tasks: [Task] = [] + private var operationStarted = false + + @discardableResult + func install(_ continuation: CheckedContinuation) -> Bool { + lock.lock() + if let resolution { + lock.unlock() + continuation.resume(with: resolution) + return false + } else { + self.continuation = continuation + lock.unlock() + return true + } + } + + func claimOperationStart() -> Bool { + lock.lock() + defer { lock.unlock() } + guard resolution == nil, !operationStarted else { + return false + } + operationStarted = true + return true + } + + func registerTasks(_ tasks: [Task]) { + lock.lock() + if resolution == nil { + self.tasks = tasks + lock.unlock() + } else { + lock.unlock() + tasks.forEach { $0.cancel() } + } + } + + func resolve(_ result: Result) { + lock.lock() + guard resolution == nil else { + lock.unlock() + return + } + + resolution = result + let continuation = self.continuation + self.continuation = nil + let tasks = self.tasks + self.tasks.removeAll() + lock.unlock() + + tasks.forEach { $0.cancel() } + continuation?.resume(with: result) + } +} + +enum TonTransferRemoteError: Error, Equatable { + case invalidAccountAddress + case invalidSequenceNumber + case invalidFee + case unsupportedAccountState + case untrustedSignedOperationEndpoint +} + +enum TonAccountSafetyPolicy { + static func walletIsInitialized( + status: String, + isWallet: Bool, + isScam: Bool?, + isSuspended: Bool?, + interfaces: [String]? + ) throws -> Bool { + guard isScam != true, isSuspended != true else { + throw TonTransferRemoteError.unsupportedAccountState + } + switch status { + case "active": + guard isWallet, interfaces?.contains("wallet_v4r2") == true else { + throw TonTransferRemoteError.unsupportedAccountState + } + return true + case "nonexist", "uninit": + return false + default: + throw TonTransferRemoteError.unsupportedAccountState + } + } + + static func recipientRequiresMemo( + status: String, + isScam: Bool?, + isSuspended: Bool?, + memoRequired: Bool? + ) throws -> Bool { + guard ["active", "nonexist", "uninit"].contains(status), + isScam != true, + isSuspended != true + else { + throw TonTransferRemoteError.unsupportedAccountState + } + return memoRequired == true + } +} + +enum TonEmulationPolicy { + static func feeNanotons( + _ value: Int64, + maximum: UInt64 = TonSendService.defaultMaximumFeeNanotons + ) throws -> UInt64 { + guard value > 0, maximum > 0 else { + throw TonTransferRemoteError.invalidFee + } + let fee = UInt64(value) + guard fee <= maximum else { + throw TonTransferRemoteError.invalidFee + } + return fee + } + + static func acceptsExecution( + transactionSucceeded: Bool, + transactionAborted: Bool, + transactionDestroyed: Bool, + transactionTypeIsOrdinary: Bool, + traceEmulated: Bool, + traceHasWalletV4R2Interface: Bool, + traceChildrenAreEmpty: Bool, + transactionAccountMatches: Bool, + computePhasePresent: Bool, + computePhaseSkipped: Bool, + computePhaseSucceeded: Bool, + computeExitCodeZero: Bool, + actionPhasePresent: Bool, + actionPhaseSucceeded: Bool, + actionCountIsOne: Bool, + skippedActionCountIsZero: Bool, + outMessageCountIsOne: Bool, + inboundMessageMatches: Bool, + outboundMessageMatches: Bool + ) -> Bool { + transactionSucceeded && + !transactionAborted && + !transactionDestroyed && + transactionTypeIsOrdinary && + traceEmulated && + traceHasWalletV4R2Interface && + traceChildrenAreEmpty && + transactionAccountMatches && + computePhasePresent && + !computePhaseSkipped && + computePhaseSucceeded && + computeExitCodeZero && + actionPhasePresent && + actionPhaseSucceeded && + actionCountIsOne && + skippedActionCountIsZero && + outMessageCountIsOne && + inboundMessageMatches && + outboundMessageMatches + } + + static func acceptsRisk( + eventIsScam: Bool, + eventInProgress: Bool, + transfersAllRemainingBalance: Bool, + tonRiskWithinIntent: Bool, + hasJettonRisk: Bool, + hasNFTRisk: Bool + ) -> Bool { + !eventIsScam && + !eventInProgress && + !transfersAllRemainingBalance && + tonRiskWithinIntent && + !hasJettonRisk && + !hasNFTRisk + } +} + +/// Production TON API adapter. A successful emulation must execute cleanly and must not +/// introduce transfer-all, Jetton, or NFT risk beyond the signed native TON message. +final class TonAPIRemoteClient: TonTransferRemoteProtocol, @unchecked Sendable { + private let client: any APIProtocol + private let signedOperationsAllowed: Bool + private let signedOperationOrigin: String? + + var reviewedSignedOperationOrigin: String? { + signedOperationOrigin + } + + init(client: any APIProtocol) { + self.client = client + signedOperationsAllowed = false + signedOperationOrigin = nil + } + + private init( + client: any APIProtocol, + signedOperationsAllowed: Bool, + signedOperationOrigin: String? + ) { + self.client = client + self.signedOperationsAllowed = signedOperationsAllowed + self.signedOperationOrigin = signedOperationOrigin + } + + #if DEBUG + init(trustedTestClient client: any APIProtocol) { + self.client = client + signedOperationsAllowed = true + signedOperationOrigin = TonAPIClientFactory.canonicalAuthenticatedOrigin.absoluteString + } + #endif + + convenience init( + factory: TonAPIClientFactory, + configuration: URLSessionConfiguration? = nil + ) { + let isReviewedOrigin = TonAPIClientFactory.isReviewedProductionSendServerURL(factory.serverURL) && + factory.hasReviewedProductionSendCredential + self.init( + client: factory.tonAPIClient(configuration: configuration), + signedOperationsAllowed: isReviewedOrigin, + signedOperationOrigin: isReviewedOrigin ? factory.serverURL.absoluteString : nil + ) + } + + func walletState(address: String) async throws -> TonWalletRemoteState { + async let accountOutput = client.getAccount( + .init(path: .init(account_id: address)) + ) + async let sequenceOutput = client.getAccountSeqno( + .init(path: .init(account_id: address)) + ) + + let account = try await accountOutput.ok.body.json + let sequenceNumber = try await sequenceOutput.ok.body.json.seqno + guard sequenceNumber >= 0 else { + throw TonTransferRemoteError.invalidSequenceNumber + } + + do { + guard try TonSwift.Address.parse(account.address) == TonSwift.Address.parse(address) else { + throw TonTransferRemoteError.invalidAccountAddress + } + } catch let error as TonTransferRemoteError { + throw error + } catch { + throw TonTransferRemoteError.invalidAccountAddress + } + + let isInitialized = try TonAccountSafetyPolicy.walletIsInitialized( + status: account.status, + isWallet: account.is_wallet, + isScam: account.is_scam, + isSuspended: account.is_suspended, + interfaces: account.interfaces + ) + + return TonWalletRemoteState( + sequenceNumber: UInt64(sequenceNumber), + isInitialized: isInitialized + ) + } + + func recipientRequiresMemo(address: String) async throws -> Bool { + let account = try await client.getAccount( + .init(path: .init(account_id: address)) + ).ok.body.json + guard addressesMatch(account.address, address) else { + throw TonTransferRemoteError.invalidAccountAddress + } + return try TonAccountSafetyPolicy.recipientRequiresMemo( + status: account.status, + isScam: account.is_scam, + isSuspended: account.is_suspended, + memoRequired: account.memo_required + ) + } + + func emulateUnsigned( + message: TonUnsignedEmulationMessage, + intent: TonEmulationIntent + ) async throws -> TonEmulationResult { + let output = try await client.emulateMessageToTrace( + .init( + query: .init(ignore_signature_check: true), + body: .json(.init(boc: message.bocBase64)) + ) + ) + let trace = try output.ok.body.json + let feeNanotons = try TonEmulationPolicy.feeNanotons(trace.transaction.total_fees) + return TonEmulationResult( + accepted: executionAccepted( + trace: trace, + intent: intent, + externalMessageBoc: message.boc + ), + totalFeeNanotons: feeNanotons + ) + } + + func emulateSigned( + message: TonSignedExternalMessage, + intent: TonEmulationIntent + ) async throws -> TonEmulationResult { + try requireTrustedSignedOperationEndpoint() + let output = try await client.emulateMessageToWallet( + .init( + body: .json( + .init(boc: message.bocBase64) + ) + ) + ) + let consequences = try output.ok.body.json + let transaction = consequences.trace.transaction + let risk = consequences.risk + let feeNanotons = try TonEmulationPolicy.feeNanotons(transaction.total_fees) + let accepted = executionAccepted( + trace: consequences.trace, + intent: intent, + externalMessageBoc: message.boc + ) && TonEmulationPolicy.acceptsRisk( + eventIsScam: consequences.event.is_scam, + eventInProgress: consequences.event.in_progress, + transfersAllRemainingBalance: risk.transfer_all_remaining_balance, + tonRiskWithinIntent: risk.ton >= 0 && risk.ton <= intent.amountNanotons, + hasJettonRisk: !risk.jettons.isEmpty, + hasNFTRisk: !risk.nfts.isEmpty + ) && addressesMatch(consequences.event.account.address, intent.senderAddress) + + return TonEmulationResult( + accepted: accepted, + totalFeeNanotons: feeNanotons + ) + } + + func broadcast(message: TonSignedExternalMessage) async throws { + try requireTrustedSignedOperationEndpoint() + let output = try await client.sendBlockchainMessage( + .init( + body: .json( + .init(boc: message.bocBase64) + ) + ) + ) + _ = try output.ok + } + + func reconcile( + message: TonSignedExternalMessage, + intent: TonEmulationIntent + ) async throws -> TonReconciliationResult { + try requireTrustedSignedOperationEndpoint() + let output = try await client.getBlockchainTransactionByMessageHash( + .init(path: .init(msg_id: message.messageHashHex)) + ) + switch output { + case let .ok(ok): + let transaction = try ok.body.json + guard transactionAccepted( + transaction, + intent: intent, + externalMessageBoc: message.boc + ) else { + throw TonTransferRemoteError.unsupportedAccountState + } + return .confirmed + case let .default(statusCode, _): + guard statusCode == 404 else { + throw TonTransferRemoteError.unsupportedAccountState + } + return .notFound + } + } + + private func requireTrustedSignedOperationEndpoint() throws { + guard signedOperationsAllowed else { + throw TonTransferRemoteError.untrustedSignedOperationEndpoint + } + } + + private func executionAccepted( + trace: Components.Schemas.Trace, + intent: TonEmulationIntent, + externalMessageBoc: Data + ) -> Bool { + let transaction = trace.transaction + let computePhase = transaction.compute_phase + let actionPhase = transaction.action_phase + let accepted = TonEmulationPolicy.acceptsExecution( + transactionSucceeded: transaction.success, + transactionAborted: transaction.aborted, + transactionDestroyed: transaction.destroyed, + transactionTypeIsOrdinary: transaction.transaction_type == .TransOrd, + traceEmulated: trace.emulated == true, + traceHasWalletV4R2Interface: trace.interfaces.contains("wallet_v4r2"), + // TonAPI omits `children` when empty in some valid responses. + traceChildrenAreEmpty: trace.children?.isEmpty != false, + transactionAccountMatches: addressesMatch( + transaction.account.address, + intent.senderAddress + ) && transaction.account.is_wallet && !transaction.account.is_scam, + computePhasePresent: computePhase != nil, + computePhaseSkipped: computePhase?.skipped ?? true, + computePhaseSucceeded: computePhase?.success == true, + computeExitCodeZero: computePhase?.exit_code == 0, + actionPhasePresent: actionPhase != nil, + actionPhaseSucceeded: actionPhase?.success == true, + actionCountIsOne: actionPhase?.total_actions == 1, + skippedActionCountIsZero: actionPhase?.skipped_actions == 0, + outMessageCountIsOne: transaction.out_msgs.count == 1, + inboundMessageMatches: inboundMessageMatches( + transaction.in_msg, + senderAddress: intent.senderAddress, + externalMessageBoc: externalMessageBoc + ), + outboundMessageMatches: outboundMessageMatches( + transaction.out_msgs.first, + intent: intent + ) + ) + return accepted + } + + private func transactionAccepted( + _ transaction: Components.Schemas.Transaction, + intent: TonEmulationIntent, + externalMessageBoc: Data + ) -> Bool { + let computePhase = transaction.compute_phase + let actionPhase = transaction.action_phase + return transaction.success && + !transaction.aborted && + !transaction.destroyed && + transaction.transaction_type == .TransOrd && + addressesMatch(transaction.account.address, intent.senderAddress) && + transaction.account.is_wallet && + !transaction.account.is_scam && + computePhase != nil && + computePhase?.skipped == false && + computePhase?.success == true && + computePhase?.exit_code == 0 && + actionPhase?.success == true && + actionPhase?.total_actions == 1 && + actionPhase?.skipped_actions == 0 && + transaction.out_msgs.count == 1 && + inboundMessageMatches( + transaction.in_msg, + senderAddress: intent.senderAddress, + externalMessageBoc: externalMessageBoc + ) && + outboundMessageMatches(transaction.out_msgs.first, intent: intent) + } + + private func inboundMessageMatches( + _ message: Components.Schemas.Message?, + senderAddress: String, + externalMessageBoc: Data + ) -> Bool { + guard let message, + message.msg_type == .ext_in_msg, + message.source == nil, + message.destination?.is_scam == false, + addressesMatch(message.destination?.address, senderAddress), + let expectedBodyHash = externalMessageBodyHashHex(externalMessageBoc) + else { + return false + } + return rawBodyMatches(message.raw_body, expectedHashHex: expectedBodyHash) + } + + private func outboundMessageMatches( + _ message: Components.Schemas.Message?, + intent: TonEmulationIntent + ) -> Bool { + guard let message, + message.msg_type == .int_msg, + message.ihr_disabled, + message.bounce == intent.bounce, + !message.bounced, + message.value == intent.amountNanotons, + message.ihr_fee >= 0, + message.fwd_fee >= 0, + message.source?.is_scam == false, + message.destination?.is_scam == false, + addressesMatch(message.source?.address, intent.senderAddress), + addressesMatch(message.destination?.address, intent.recipientAddress) + else { + return false + } + return rawBodyMatches( + message.raw_body, + expectedHashHex: intent.messageBodyHashHex + ) + } + + private func externalMessageBodyHashHex(_ boc: Data) -> String? { + guard let root = try? Cell.fromBoc(src: boc).only, + let message = try? Message.loadFrom(slice: root.beginParse()) + else { + return nil + } + return message.body.hash().hexString() + } + + private func rawBodyMatches(_ rawBody: String?, expectedHashHex: String) -> Bool { + guard let rawBody, + !rawBody.isEmpty, + rawBody.count.isMultiple(of: 2), + !rawBody.hasPrefix("0x"), + rawBody.unicodeScalars.allSatisfy(Self.isHexScalar), + let data = Data(hex: rawBody), + let root = try? Cell.fromBoc(src: data).only + else { + return false + } + return root.hash().hexString().lowercased() == expectedHashHex.lowercased() + } + + private static func isHexScalar(_ scalar: Unicode.Scalar) -> Bool { + switch scalar.value { + case 48 ... 57, 65 ... 70, 97 ... 102: + return true + default: + return false + } + } + + private func addressesMatch(_ actual: String?, _ expected: String) -> Bool { + guard let actual, + let actualAddress = try? TonSwift.Address.parse(actual), + let expectedAddress = try? TonSwift.Address.parse(expected) + else { + return false + } + return actualAddress == expectedAddress + } +} + +private extension Array { + var only: Element? { + count == 1 ? self[0] : nil + } +} diff --git a/fearless/Common/Model/TonTransferFeeQuote.swift b/fearless/Common/Model/TonTransferFeeQuote.swift new file mode 100644 index 0000000000..1f813403ff --- /dev/null +++ b/fearless/Common/Model/TonTransferFeeQuote.swift @@ -0,0 +1,250 @@ +import CryptoKit +import Foundation +import TonSwift + +enum TonTransferFeeQuoteError: Error, Equatable { + case invalidVersion + case invalidEndpoint + case invalidTiming + case invalidBinding + case invalidFee +} + +/// A short-lived, immutable confirmation quote for one exact native TON transfer template. +/// +/// The quote binds the user-visible fee to the canonical unsigned Wallet V4R2 signing +/// payload. A signed submission must reproduce that payload byte-for-byte and signed +/// emulation must return the exact quoted fee before the app may broadcast. +struct TonTransferFeeQuote: Equatable, Sendable { + static let currentVersion: UInt8 = 1 + static let maximumAgeSeconds: UInt64 = 30 + static let transportContract = "tonapi-swift-0.1.7" + + let version: UInt8 + let quoteIDHex: String + let templateCreatedAt: UInt64 + let issuedAt: UInt64 + let expiresAt: UInt64 + let endpointOrigin: String + let publicKey: Data + let identity: TonTransferIntentIdentity + let intent: TonEmulationIntent + let transactionRequest: TonTransferTransactionRequest + let walletState: TonWalletRemoteState + let unsignedMessage: TonUnsignedEmulationMessage + let feeNanotons: UInt64 + + init( + version: UInt8 = TonTransferFeeQuote.currentVersion, + expectedQuoteIDHex: String? = nil, + templateCreatedAt: UInt64, + issuedAt: UInt64, + expiresAt: UInt64, + endpointOrigin: String, + publicKey: Data, + identity: TonTransferIntentIdentity, + intent: TonEmulationIntent, + transactionRequest: TonTransferTransactionRequest, + walletState: TonWalletRemoteState, + unsignedMessage: TonUnsignedEmulationMessage, + feeNanotons: UInt64 + ) throws { + guard version == Self.currentVersion else { + throw TonTransferFeeQuoteError.invalidVersion + } + guard let endpointURL = URL(string: endpointOrigin), + endpointURL.absoluteString == endpointOrigin, + TonAPIClientFactory.isReviewedProductionSendServerURL(endpointURL) + else { + throw TonTransferFeeQuoteError.invalidEndpoint + } + guard templateCreatedAt <= issuedAt, + issuedAt < expiresAt, + expiresAt - issuedAt <= Self.maximumAgeSeconds, + expiresAt < transactionRequest.validUntil, + transactionRequest.validUntil - expiresAt >= TonTransferTransactionBuilder.minimumLifetimeSeconds, + transactionRequest.validUntil - templateCreatedAt >= TonTransferTransactionBuilder.minimumLifetimeSeconds, + transactionRequest.validUntil - templateCreatedAt <= TonTransferTransactionBuilder.maximumLifetimeSeconds, + transactionRequest.validUntil <= UInt64(UInt32.max) + else { + throw TonTransferFeeQuoteError.invalidTiming + } + guard feeNanotons > 0, + feeNanotons <= TonSendService.defaultMaximumFeeNanotons + else { + throw TonTransferFeeQuoteError.invalidFee + } + guard publicKey.count == 32, + publicKey.contains(where: { $0 != 0 }), + unsignedMessage.publicKey == publicKey, + unsignedMessage.bocBase64 == unsignedMessage.boc.base64EncodedString(), + !unsignedMessage.boc.isEmpty, + unsignedMessage.boc.count <= TonTransferTransactionBuilder.maximumBocBytes, + transactionRequest.asset == .nativeTon, + transactionRequest.network == .mainnet, + transactionRequest.senderAddress == identity.sender, + transactionRequest.recipientAddress == identity.recipient, + transactionRequest.amountNanotons == identity.amountNanotons, + transactionRequest.bounce == identity.bounce, + transactionRequest.comment == identity.comment, + intent.senderAddress == identity.sender, + intent.recipientAddress == identity.recipient, + intent.amountNanotons.description == identity.amountNanotons, + intent.bounce == identity.bounce, + intent.messageBodyHashHex == (try TonTransferTransactionBuilder.messageBodyHashHex(comment: identity.comment)), + transactionRequest.sequenceNumber == walletState.sequenceNumber, + transactionRequest.includeStateInit == !walletState.isInitialized, + (try? TonSwift.Address.parse(unsignedMessage.walletAddress).toRaw()) == identity.sender, + unsignedMessage.sequenceNumber == walletState.sequenceNumber, + unsignedMessage.validUntil == transactionRequest.validUntil, + unsignedMessage.includesStateInit == !walletState.isInitialized, + Self.isLowercaseHash(unsignedMessage.messageHashHex), + Self.isLowercaseHash(unsignedMessage.signingPayloadHashHex) + else { + throw TonTransferFeeQuoteError.invalidBinding + } + + let computedQuoteID = TonTransferFeeQuoteBinding.quoteIDHex( + version: version, + templateCreatedAt: templateCreatedAt, + issuedAt: issuedAt, + expiresAt: expiresAt, + endpointOrigin: endpointOrigin, + publicKey: publicKey, + identity: identity, + intent: intent, + transactionRequest: transactionRequest, + walletState: walletState, + unsignedMessage: unsignedMessage, + feeNanotons: feeNanotons + ) + if let expectedQuoteIDHex, + expectedQuoteIDHex != computedQuoteID { + throw TonTransferFeeQuoteError.invalidBinding + } + + self.version = version + quoteIDHex = computedQuoteID + self.templateCreatedAt = templateCreatedAt + self.issuedAt = issuedAt + self.expiresAt = expiresAt + self.endpointOrigin = endpointOrigin + self.publicKey = publicKey + self.identity = identity + self.intent = intent + self.transactionRequest = transactionRequest + self.walletState = walletState + self.unsignedMessage = unsignedMessage + self.feeNanotons = feeNanotons + } + + func validate(at now: UInt64, endpointOrigin currentEndpointOrigin: String?) throws { + guard currentEndpointOrigin == endpointOrigin else { + throw TonTransferFeeQuoteError.invalidEndpoint + } + guard now >= issuedAt, + now <= expiresAt, + now < transactionRequest.validUntil, + transactionRequest.validUntil - now >= TonTransferTransactionBuilder.minimumLifetimeSeconds + else { + throw TonTransferFeeQuoteError.invalidTiming + } + _ = try TonTransferFeeQuote( + version: version, + expectedQuoteIDHex: quoteIDHex, + templateCreatedAt: templateCreatedAt, + issuedAt: issuedAt, + expiresAt: expiresAt, + endpointOrigin: endpointOrigin, + publicKey: publicKey, + identity: identity, + intent: intent, + transactionRequest: transactionRequest, + walletState: walletState, + unsignedMessage: unsignedMessage, + feeNanotons: feeNanotons + ) + } + + private static func isLowercaseHash(_ value: String) -> Bool { + value.utf8.count == 64 && value.utf8.allSatisfy { byte in + (48 ... 57).contains(byte) || (97 ... 102).contains(byte) + } + } +} + +private enum TonTransferFeeQuoteBinding { + private static let domain = Data("fearless.ton.fee-quote.v1\0".utf8) + + static func quoteIDHex( + version: UInt8, + templateCreatedAt: UInt64, + issuedAt: UInt64, + expiresAt: UInt64, + endpointOrigin: String, + publicKey: Data, + identity: TonTransferIntentIdentity, + intent: TonEmulationIntent, + transactionRequest: TonTransferTransactionRequest, + walletState: TonWalletRemoteState, + unsignedMessage: TonUnsignedEmulationMessage, + feeNanotons: UInt64 + ) -> String { + var payload = domain + payload.append(version) + append(Self.transportContractData, to: &payload) + append(endpointOrigin, to: &payload) + append(publicKey, to: &payload) + append("native-ton", to: &payload) + append("mainnet", to: &payload) + append(identity.sender, to: &payload) + append(identity.recipient, to: &payload) + append(identity.amountNanotons, to: &payload) + payload.append(identity.bounce ? 1 : 0) + if let comment = identity.comment { + payload.append(1) + append(comment, to: &payload) + } else { + payload.append(0) + } + append(intent.messageBodyHashHex, to: &payload) + append(templateCreatedAt, to: &payload) + append(issuedAt, to: &payload) + append(expiresAt, to: &payload) + append(transactionRequest.sequenceNumber, to: &payload) + payload.append(walletState.isInitialized ? 1 : 0) + payload.append(unsignedMessage.includesStateInit ? 1 : 0) + append(transactionRequest.validUntil, to: &payload) + append(unsignedMessage.messageHashHex, to: &payload) + append(unsignedMessage.signingPayloadHashHex, to: &payload) + append(Data(SHA256.hash(data: unsignedMessage.boc)), to: &payload) + append(feeNanotons, to: &payload) + return Data(SHA256.hash(data: payload)).map { String(format: "%02x", $0) }.joined() + } + + private static var transportContractData: Data { + Data(TonTransferFeeQuote.transportContract.utf8) + } + + private static func append(_ value: String, to data: inout Data) { + append(Data(value.utf8), to: &data) + } + + private static func append(_ value: Data, to data: inout Data) { + precondition(value.count <= Int(UInt32.max)) + append(UInt32(value.count), to: &data) + data.append(value) + } + + private static func append(_ value: UInt64, to data: inout Data) { + data.append(contentsOf: (0 ..< 8).map { offset in + UInt8(truncatingIfNeeded: value >> UInt64((7 - offset) * 8)) + }) + } + + private static func append(_ value: UInt32, to data: inout Data) { + data.append(contentsOf: (0 ..< 4).map { offset in + UInt8(truncatingIfNeeded: value >> UInt32((3 - offset) * 8)) + }) + } +} diff --git a/fearless/Common/Model/TonTransferTransactionBuilder.swift b/fearless/Common/Model/TonTransferTransactionBuilder.swift new file mode 100644 index 0000000000..39c0d656bc --- /dev/null +++ b/fearless/Common/Model/TonTransferTransactionBuilder.swift @@ -0,0 +1,861 @@ +import BigInt +import CryptoKit +import Foundation +import TonSwift + +enum TonTransferTransactionBuilderError: Error, Equatable { + case unsupportedAsset + case unsupportedNetwork + case invalidPrivateKey + case invalidPublicKey + case invalidSenderAddress + case invalidRecipientAddress + case testnetAddressNotAllowed + case unsupportedWorkchain + case senderKeyMismatch + case senderEqualsRecipient + case ambiguousBounceFlag + case bounceFlagMismatch + case invalidAmount + case invalidSequenceNumber + case invalidStateInitPolicy + case invalidExpiration + case invalidComment + case invalidSignature + case serializationFailed +} + +enum TonTransferAsset: Equatable, Hashable, Sendable { + case nativeTon + case jetton(masterAddress: String) +} + +enum TonTransferNetwork: Equatable, Hashable, Sendable { + case mainnet + case testnet +} + +struct TonTransferTransactionRequest: Equatable, Sendable { + let asset: TonTransferAsset + let network: TonTransferNetwork + let senderAddress: String + let recipientAddress: String + let amountNanotons: String + let sequenceNumber: UInt64 + let includeStateInit: Bool? + let validUntil: UInt64 + let bounce: Bool + let comment: String? + + init( + asset: TonTransferAsset = .nativeTon, + network: TonTransferNetwork = .mainnet, + senderAddress: String, + recipientAddress: String, + amountNanotons: String, + sequenceNumber: UInt64, + includeStateInit: Bool? = nil, + validUntil: UInt64, + bounce: Bool, + comment: String? + ) { + self.asset = asset + self.network = network + self.senderAddress = senderAddress + self.recipientAddress = recipientAddress + self.amountNanotons = amountNanotons + self.sequenceNumber = sequenceNumber + self.includeStateInit = includeStateInit + self.validUntil = validUntil + self.bounce = bounce + self.comment = comment + } +} + +struct TonSignedExternalMessage: Equatable, Sendable { + let boc: Data + let bocBase64: String + let messageHashHex: String + let signingPayloadHashHex: String + let publicKey: Data + let walletAddress: String + let sequenceNumber: UInt64 + let validUntil: UInt64 + let includesStateInit: Bool +} + +/// An external message bearing TonSwift's deliberately invalid empty-key signature. Keeping +/// this separate from `TonSignedExternalMessage` makes it impossible to pass fee previews to +/// the broadcast API by accident. +struct TonUnsignedEmulationMessage: Equatable, Sendable { + let boc: Data + let bocBase64: String + let messageHashHex: String + let signingPayloadHashHex: String + let publicKey: Data + let walletAddress: String + let sequenceNumber: UInt64 + let validUntil: UInt64 + let includesStateInit: Bool +} + +private struct TonExternalMessageEnvelope { + let boc: Data + let bocBase64: String + let messageHashHex: String + let signingPayloadHashHex: String + let publicKey: Data + let walletAddress: String + let sequenceNumber: UInt64 + let validUntil: UInt64 + let includesStateInit: Bool +} + +struct TonSignedMessageInspection: Equatable, Sendable { + let messageHashHex: String + let signingPayloadHashHex: String + let signature: Data + let walletID: UInt64 + let walletAddress: String + let recipientAddress: String + let amountNanotons: String + let sequenceNumber: UInt64 + let validUntil: UInt64 + let includesStateInit: Bool + let bounce: Bool + let messageBodyHashHex: String +} + +/// Builds a signed TON Wallet V4R2 external message without performing network I/O. +/// +/// Callers must obtain the sequence number and recipient account state from a reviewed +/// mainnet source. Production routing remains disabled until funded live evidence exists. +enum TonTransferTransactionBuilder { + static let minimumLifetimeSeconds: UInt64 = 30 + static let maximumLifetimeSeconds: UInt64 = 300 + static let maximumCommentBytes = 256 + static let maximumAddressInputBytes = 128 + static let maximumAmountDigits = 37 + + private static let privateKeyLength = 32 + private static let publicKeyLength = 32 + static let maximumBocBytes = 16 * 1024 + private static let maximumCoins = (BigUInt(1) << 120) - 1 + private static let supportedRecipientWorkchains: Set = [0, -1] + + static func buildAndSign( + request: TonTransferTransactionRequest, + privateKeySeed: Data, + now: UInt64 + ) throws -> TonSignedExternalMessage { + do { + try validateSupportedScope(asset: request.asset, network: request.network) + let privateKey = try validatedPrivateKey(seed: privateKeySeed) + let publicKey = privateKey.publicKey.rawRepresentation + guard publicKey.count == publicKeyLength else { + throw TonTransferTransactionBuilderError.invalidPrivateKey + } + + let secretKey = privateKeySeed + publicKey + guard secretKey.count == privateKeyLength + publicKeyLength else { + throw TonTransferTransactionBuilderError.invalidPrivateKey + } + let message = try build( + request: request, + publicKey: publicKey, + signer: WalletTransferSecretKeySigner(secretKey: secretKey), + now: now + ) + return TonSignedExternalMessage( + boc: message.boc, + bocBase64: message.bocBase64, + messageHashHex: message.messageHashHex, + signingPayloadHashHex: message.signingPayloadHashHex, + publicKey: message.publicKey, + walletAddress: message.walletAddress, + sequenceNumber: message.sequenceNumber, + validUntil: message.validUntil, + includesStateInit: message.includesStateInit + ) + } catch let error as TonTransferTransactionBuilderError { + throw error + } catch { + throw TonTransferTransactionBuilderError.serializationFailed + } + } + + /// Builds the same transfer envelope with a deliberately invalid 64-byte signature. + /// This payload is only valid with TonAPI's explicit `ignore_signature_check` emulation + /// option and cannot authorize a blockchain transaction. + static func buildForFeeEstimation( + request: TonTransferTransactionRequest, + publicKey: Data, + now: UInt64 + ) throws -> TonUnsignedEmulationMessage { + do { + try validateSupportedScope(asset: request.asset, network: request.network) + try validatePublicKey(publicKey) + let message = try build( + request: request, + publicKey: publicKey, + signer: WalletTransferEmptyKeySigner(), + now: now + ) + return TonUnsignedEmulationMessage( + boc: message.boc, + bocBase64: message.bocBase64, + messageHashHex: message.messageHashHex, + signingPayloadHashHex: message.signingPayloadHashHex, + publicKey: message.publicKey, + walletAddress: message.walletAddress, + sequenceNumber: message.sequenceNumber, + validUntil: message.validUntil, + includesStateInit: message.includesStateInit + ) + } catch let error as TonTransferTransactionBuilderError { + throw error + } catch { + throw TonTransferTransactionBuilderError.serializationFailed + } + } + + /// Rebuilds the canonical Wallet V4R2 envelope around an already-created signature. + /// This is used only to validate a persisted bearer BOC byte-for-byte after restart. + static func rebuildSignedMessage( + request: TonTransferTransactionRequest, + publicKey: Data, + signature: Data, + now: UInt64 + ) throws -> TonSignedExternalMessage { + guard signature.count == 64 else { + throw TonTransferTransactionBuilderError.invalidSignature + } + do { + try validateSupportedScope(asset: request.asset, network: request.network) + try validatePublicKey(publicKey) + let message = try build( + request: request, + publicKey: publicKey, + signer: TonFixedSignatureSigner(signature: signature), + now: now + ) + return TonSignedExternalMessage( + boc: message.boc, + bocBase64: message.bocBase64, + messageHashHex: message.messageHashHex, + signingPayloadHashHex: message.signingPayloadHashHex, + publicKey: message.publicKey, + walletAddress: message.walletAddress, + sequenceNumber: message.sequenceNumber, + validUntil: message.validUntil, + includesStateInit: message.includesStateInit + ) + } catch let error as TonTransferTransactionBuilderError { + throw error + } catch { + throw TonTransferTransactionBuilderError.serializationFailed + } + } + + /// Strictly inspects the exact single-root signed BOC. Callers independently compare + /// this result with the journal record and a byte-for-byte canonical rebuild. + static func inspectSignedMessage(_ boc: Data) throws -> TonSignedMessageInspection { + guard !boc.isEmpty, boc.count <= maximumBocBytes else { + throw TonTransferTransactionBuilderError.serializationFailed + } + let roots = try Cell.fromBoc(src: boc) + guard roots.count == 1 else { + throw TonTransferTransactionBuilderError.serializationFailed + } + let root = roots[0] + let rootSlice = try root.beginParse() + let externalMessage = try Message.loadFrom(slice: rootSlice) + + guard case let .externalInInfo(externalInfo) = externalMessage.info, + externalInfo.src == nil, + externalInfo.importFee.rawValue == 0 + else { + throw TonTransferTransactionBuilderError.serializationFailed + } + + let bodySlice = try externalMessage.body.beginParse() + guard bodySlice.remainingBits >= 512 else { + throw TonTransferTransactionBuilderError.invalidSignature + } + let signature = try bodySlice.loadBytes(64) + let signingCell = try bodySlice.clone().toCell() + let walletID = try bodySlice.loadUint(bits: 32) + let validUntil = try bodySlice.loadUint(bits: 32) + let sequenceNumber = try bodySlice.loadUint(bits: 32) + let operation = try bodySlice.loadUint(bits: 8) + let sendMode = try bodySlice.loadUint(bits: 8) + guard operation == 0, + sendMode == UInt64(SendMode.walletDefault().rawValue), + bodySlice.remainingRefs == 1 + else { + throw TonTransferTransactionBuilderError.serializationFailed + } + let internalCell = try bodySlice.loadRef() + try bodySlice.endParse() + + let internalSlice = try internalCell.beginParse() + let internalMessage = try MessageRelaxed.loadFrom(slice: internalSlice) + guard case let .internalInfo(internalInfo) = internalMessage.info, + internalInfo.ihrDisabled, + !internalInfo.bounced, + try internalInfo.src.asInternal() == nil, + internalInfo.value.other.isEmpty, + internalInfo.ihrFee.rawValue == 0, + internalInfo.forwardFee.rawValue == 0, + internalInfo.createdLt == 0, + internalInfo.createdAt == 0, + internalMessage.stateInit == nil + else { + throw TonTransferTransactionBuilderError.serializationFailed + } + + return TonSignedMessageInspection( + messageHashHex: root.hash().tonHexString, + signingPayloadHashHex: signingCell.hash().tonHexString, + signature: signature, + walletID: walletID, + walletAddress: externalInfo.dest.toRaw(), + recipientAddress: internalInfo.dest.toRaw(), + amountNanotons: internalInfo.value.coins.rawValue.description, + sequenceNumber: sequenceNumber, + validUntil: validUntil, + includesStateInit: externalMessage.stateInit != nil, + bounce: internalInfo.bounce, + messageBodyHashHex: internalMessage.body.hash().tonHexString + ) + } + + static func messageBodyHashHex(comment: String?) throws -> String { + try validateComment(comment) + return try messageBody(comment: comment).hash().tonHexString + } + + private static func build( + request: TonTransferTransactionRequest, + publicKey: Data, + signer: WalletTransferSigner, + now: UInt64 + ) throws -> TonExternalMessageEnvelope { + let sender = try parseAddress(request.senderAddress, role: .sender) + let recipient = try parseAddress(request.recipientAddress, role: .recipient) + try validateMainnetAddress(sender, role: .sender) + try validateMainnetAddress(recipient, role: .recipient) + + guard sender.address.workchain == 0 else { + throw TonTransferTransactionBuilderError.unsupportedWorkchain + } + guard supportedRecipientWorkchains.contains(recipient.address.workchain) else { + throw TonTransferTransactionBuilderError.unsupportedWorkchain + } + + let wallet = WalletV4R2(workchain: 0, publicKey: publicKey) + let walletAddress = try wallet.address() + guard sender.address == walletAddress else { + throw TonTransferTransactionBuilderError.senderKeyMismatch + } + guard recipient.address != walletAddress else { + throw TonTransferTransactionBuilderError.senderEqualsRecipient + } + if let encodedBounce = recipient.bounceable, encodedBounce != request.bounce { + throw TonTransferTransactionBuilderError.bounceFlagMismatch + } + + let amount = try parseAmount(request.amountNanotons) + try validateSequenceNumber(request.sequenceNumber) + let includesStateInit = request.includeStateInit ?? (request.sequenceNumber == 0) + guard !includesStateInit || request.sequenceNumber == 0 else { + throw TonTransferTransactionBuilderError.invalidStateInitPolicy + } + try validateExpiration(validUntil: request.validUntil, now: now) + try validateComment(request.comment) + + let internalMessage = MessageRelaxed.internal( + to: recipient.address, + value: amount, + bounce: request.bounce, + body: try messageBody(comment: request.comment) + ) + + let transfer = try wallet.createTransfer( + args: WalletTransferData( + seqno: request.sequenceNumber, + messages: [internalMessage], + sendMode: .walletDefault(), + timeout: request.validUntil + ) + ) + let capturingSigner = TonCapturingTransferSigner(base: signer) + let signedBody = try transfer.signMessage(signer: capturingSigner) + guard let signingPayloadHash = capturingSigner.signingPayloadHash, + signingPayloadHash.count == 32 + else { + throw TonTransferTransactionBuilderError.serializationFailed + } + let externalMessage = Message.external( + to: walletAddress, + stateInit: includesStateInit ? wallet.stateInit : nil, + body: signedBody + ) + let rootCell = try Builder().store(externalMessage).endCell() + let boc = try TonCanonicalBoc.serialize(root: rootCell) + guard !boc.isEmpty, boc.count <= maximumBocBytes else { + throw TonTransferTransactionBuilderError.serializationFailed + } + + return TonExternalMessageEnvelope( + boc: boc, + bocBase64: boc.base64EncodedString(), + messageHashHex: rootCell.hash().tonHexString, + signingPayloadHashHex: signingPayloadHash.tonHexString, + publicKey: publicKey, + walletAddress: walletAddress.toString( + urlSafe: true, + testOnly: false, + bounceable: false + ), + sequenceNumber: request.sequenceNumber, + validUntil: request.validUntil, + includesStateInit: includesStateInit + ) + } + + static func requiredBounceFlag(forRecipientAddress value: String) throws -> Bool { + let recipient = try parseAddress(value, role: .recipient) + try validateMainnetAddress(recipient, role: .recipient) + guard supportedRecipientWorkchains.contains(recipient.address.workchain) else { + throw TonTransferTransactionBuilderError.unsupportedWorkchain + } + guard let bounceable = recipient.bounceable else { + throw TonTransferTransactionBuilderError.ambiguousBounceFlag + } + return bounceable + } + + private static func validateSupportedScope( + asset: TonTransferAsset, + network: TonTransferNetwork + ) throws { + guard asset == .nativeTon else { + throw TonTransferTransactionBuilderError.unsupportedAsset + } + guard network == .mainnet else { + throw TonTransferTransactionBuilderError.unsupportedNetwork + } + } + + private static func validatedPrivateKey( + seed: Data + ) throws -> Curve25519.Signing.PrivateKey { + guard seed.count == privateKeyLength, seed.contains(where: { $0 != 0 }) else { + throw TonTransferTransactionBuilderError.invalidPrivateKey + } + + do { + return try Curve25519.Signing.PrivateKey(rawRepresentation: seed) + } catch { + throw TonTransferTransactionBuilderError.invalidPrivateKey + } + } + + private static func validatePublicKey(_ publicKey: Data) throws { + guard publicKey.count == publicKeyLength, + publicKey.contains(where: { $0 != 0 }), + (try? Curve25519.Signing.PublicKey(rawRepresentation: publicKey)) != nil + else { + throw TonTransferTransactionBuilderError.invalidPublicKey + } + } + + private static func messageBody(comment: String?) throws -> Cell { + guard let comment else { + // TonSwift's `Cell.empty` convenience value is created with `Cell.init()`, + // which does not precompute its hash/depth arrays. Finalize an empty + // builder instead so callers can safely bind the body by hash. + return try Builder().endCell() + } + return try Builder() + .store(int: 0, bits: 32) + .writeSnakeData(Data(comment.utf8)) + .endCell() + } + + private static func parseAmount(_ value: String) throws -> BigUInt { + let scalars = value.unicodeScalars + guard !scalars.isEmpty, + value.utf8.count <= maximumAmountDigits, + scalars.allSatisfy({ (48 ... 57).contains($0.value) }), + value == "0" || value.first != "0", + let amount = BigUInt(value, radix: 10), + amount > 0, + amount <= maximumCoins + else { + throw TonTransferTransactionBuilderError.invalidAmount + } + + return amount + } + + private static func validateSequenceNumber(_ value: UInt64) throws { + guard value <= UInt64(UInt32.max) else { + throw TonTransferTransactionBuilderError.invalidSequenceNumber + } + } + + private static func validateExpiration(validUntil: UInt64, now: UInt64) throws { + guard now <= UInt64(UInt32.max), + validUntil <= UInt64(UInt32.max), + validUntil > now + else { + throw TonTransferTransactionBuilderError.invalidExpiration + } + + let lifetime = validUntil - now + guard lifetime >= minimumLifetimeSeconds, lifetime <= maximumLifetimeSeconds else { + throw TonTransferTransactionBuilderError.invalidExpiration + } + } + + private static func validateComment(_ comment: String?) throws { + guard let comment else { + return + } + + guard !comment.isEmpty, + comment.utf8.count <= maximumCommentBytes, + !comment.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty, + Data(comment.utf8) == Data(comment.precomposedStringWithCanonicalMapping.utf8), + !comment.unicodeScalars.contains(where: isForbiddenCommentScalar) + else { + throw TonTransferTransactionBuilderError.invalidComment + } + } + + private static func isForbiddenCommentScalar(_ scalar: Unicode.Scalar) -> Bool { + switch scalar.properties.generalCategory { + case .control, + .format, + .lineSeparator, + .paragraphSeparator, + .surrogate, + .privateUse, + .unassigned: + return true + default: + break + } + + switch scalar.value { + case 0 ... 31, + 127 ... 159, + 0x200B ... 0x200F, + 0x202A ... 0x202E, + 0x2060 ... 0x206F, + 0xFEFF: + return true + default: + return false + } + } + + private static func validateMainnetAddress( + _ address: ParsedTonAddress, + role: AddressRole + ) throws { + if address.testOnly { + throw TonTransferTransactionBuilderError.testnetAddressNotAllowed + } + guard address.address.hash.count == 32 else { + throw role.invalidError + } + } + + private static func parseAddress( + _ value: String, + role: AddressRole + ) throws -> ParsedTonAddress { + guard !value.isEmpty, + value.utf8.count <= maximumAddressInputBytes, + value == value.trimmingCharacters(in: .whitespacesAndNewlines) + else { + throw role.invalidError + } + + do { + if value.contains(":") { + let address = try TonSwift.Address.parse(raw: value) + guard address.toRaw() == value else { + throw role.invalidError + } + return ParsedTonAddress( + address: address, + testOnly: false, + bounceable: nil + ) + } + + guard value.unicodeScalars.allSatisfy({ + (45 ... 45).contains($0.value) || + (48 ... 57).contains($0.value) || + (65 ... 90).contains($0.value) || + (95 ... 95).contains($0.value) || + (97 ... 122).contains($0.value) + }) else { + throw role.invalidError + } + + let standardBase64 = value + .replacingOccurrences(of: "-", with: "+") + .replacingOccurrences(of: "_", with: "/") + guard let bytes = Data(base64Encoded: standardBase64), bytes.count == 36 else { + throw role.invalidError + } + + let tag = bytes[0] + let testOnly = tag & 0x80 != 0 + let addressTag = tag & 0x7F + guard addressTag == 0x11 || addressTag == 0x51 else { + throw role.invalidError + } + let bounceable = addressTag == 0x11 + let address = try TonSwift.Address.parse(value) + let canonical = address.toString( + urlSafe: true, + testOnly: testOnly, + bounceable: bounceable + ) + guard canonical == value else { + throw role.invalidError + } + + return ParsedTonAddress( + address: address, + testOnly: testOnly, + bounceable: bounceable + ) + } catch let error as TonTransferTransactionBuilderError { + throw error + } catch { + throw role.invalidError + } + } +} + +private final class TonCapturingTransferSigner: WalletTransferSigner { + private let base: WalletTransferSigner + private(set) var signingPayloadHash: Data? + + init(base: WalletTransferSigner) { + self.base = base + } + + func signMessage(_ message: Data) throws -> Data { + guard signingPayloadHash == nil else { + throw TonTransferTransactionBuilderError.serializationFailed + } + signingPayloadHash = message + return try base.signMessage(message) + } +} + +private struct TonFixedSignatureSigner: WalletTransferSigner { + let signature: Data + + func signMessage(_: Data) throws -> Data { + guard signature.count == 64 else { + throw TonTransferTransactionBuilderError.invalidSignature + } + return signature + } +} + +/// Deterministic single-root BOC serialization. +/// +/// TonSwift's current topological sort selects the next cell from an unordered `Set`, which +/// makes multi-cell state-init BOCs process-seed dependent. This deliberately mirrors +/// `@ton/core`'s insertion-ordered breadth-first discovery and reverse-reference DFS so the +/// signed payload is deterministic and interoperable with the reference implementation. +private enum TonCanonicalBoc { + private struct CellEntry { + let cell: Cell + let referenceHashes: [String] + } + + static func serialize(root: Cell) throws -> Data { + let cells = try sortedCells(root: root) + let sizeBytes = byteWidth(UInt64(cells.count)) + let indexes = Dictionary( + uniqueKeysWithValues: cells.enumerated().map { index, entry in + (entry.cell.hash().tonHexString, UInt64(index)) + } + ) + + var cellData = Data() + for entry in cells { + guard entry.cell.refs.count <= RefsPerCell, + entry.cell.level <= 7 + else { + throw TonTransferTransactionBuilderError.serializationFailed + } + + let descriptorOne = UInt8(entry.cell.refs.count) + + (entry.cell.isExotic ? 8 : 0) + + UInt8(entry.cell.level) * 32 + let bitLength = entry.cell.bits.length + let descriptorTwo = Int(ceil(Double(bitLength) / 8)) + bitLength / 8 + guard descriptorTwo <= Int(UInt8.max) else { + throw TonTransferTransactionBuilderError.serializationFailed + } + + cellData.append(descriptorOne) + cellData.append(UInt8(descriptorTwo)) + cellData.append(entry.cell.bits.bitsToPaddedBuffer()) + for referenceHash in entry.referenceHashes { + guard let referenceIndex = indexes[referenceHash] else { + throw TonTransferTransactionBuilderError.serializationFailed + } + cellData.append(encode(referenceIndex, byteCount: sizeBytes)) + } + } + + let offsetBytes = byteWidth(UInt64(cellData.count)) + guard sizeBytes <= 7, offsetBytes <= Int(UInt8.max) else { + throw TonTransferTransactionBuilderError.serializationFailed + } + + var result = Data([0xB5, 0xEE, 0x9C, 0x72]) + result.append(0x40 | UInt8(sizeBytes)) // CRC32C, no index/cache/flags. + result.append(UInt8(offsetBytes)) + result.append(encode(UInt64(cells.count), byteCount: sizeBytes)) + result.append(encode(1, byteCount: sizeBytes)) + result.append(encode(0, byteCount: sizeBytes)) + result.append(encode(UInt64(cellData.count), byteCount: offsetBytes)) + result.append(encode(0, byteCount: sizeBytes)) // The root is always index zero. + result.append(cellData) + result.append(crc32c(result)) + return result + } + + private static func sortedCells(root: Cell) throws -> [CellEntry] { + var pending = [root] + var entries: [String: CellEntry] = [:] + var insertionOrder: [String] = [] + var notPermanent: Set = [] + + while !pending.isEmpty { + let current = pending + pending.removeAll(keepingCapacity: true) + for cell in current { + let hash = cell.hash().tonHexString + guard entries[hash] == nil else { + continue + } + let references = cell.refs.map { $0.hash().tonHexString } + entries[hash] = CellEntry(cell: cell, referenceHashes: references) + insertionOrder.append(hash) + notPermanent.insert(hash) + pending.append(contentsOf: cell.refs) + } + } + + var temporary: Set = [] + var sortedHashes: [String] = [] + + func visit(_ hash: String) throws { + guard notPermanent.contains(hash) else { + return + } + guard !temporary.contains(hash), let entry = entries[hash] else { + throw TonTransferTransactionBuilderError.serializationFailed + } + + temporary.insert(hash) + for reference in entry.referenceHashes.reversed() { + try visit(reference) + } + sortedHashes.append(hash) + temporary.remove(hash) + notPermanent.remove(hash) + } + + while let hash = insertionOrder.first(where: notPermanent.contains) { + try visit(hash) + } + + sortedHashes.reverse() + guard sortedHashes.first == root.hash().tonHexString, + sortedHashes.count == entries.count, + notPermanent.isEmpty + else { + throw TonTransferTransactionBuilderError.serializationFailed + } + return try sortedHashes.map { hash in + guard let entry = entries[hash] else { + throw TonTransferTransactionBuilderError.serializationFailed + } + return entry + } + } + + private static func byteWidth(_ value: UInt64) -> Int { + guard value > 0 else { + return 1 + } + return max(1, (UInt64.bitWidth - value.leadingZeroBitCount + 7) / 8) + } + + private static func encode(_ value: UInt64, byteCount: Int) -> Data { + Data((0 ..< byteCount).map { offset in + let shift = (byteCount - offset - 1) * 8 + return UInt8(truncatingIfNeeded: value >> UInt64(shift)) + }) + } + + private static func crc32c(_ data: Data) -> Data { + let polynomial: UInt32 = 0x82F6_3B78 + var crc = UInt32.max + for byte in data { + crc ^= UInt32(byte) + for _ in 0 ..< 8 { + crc = (crc & 1) == 0 ? crc >> 1 : (crc >> 1) ^ polynomial + } + } + crc ^= UInt32.max + return Data([ + UInt8(truncatingIfNeeded: crc), + UInt8(truncatingIfNeeded: crc >> 8), + UInt8(truncatingIfNeeded: crc >> 16), + UInt8(truncatingIfNeeded: crc >> 24) + ]) + } +} + +private enum AddressRole { + case sender + case recipient + + var invalidError: TonTransferTransactionBuilderError { + switch self { + case .sender: + return .invalidSenderAddress + case .recipient: + return .invalidRecipientAddress + } + } +} + +private struct ParsedTonAddress { + let address: TonSwift.Address + let testOnly: Bool + let bounceable: Bool? +} + +private extension Data { + var tonHexString: String { + map { String(format: "%02x", $0) }.joined() + } +} diff --git a/fearless/Common/Model/UniversalWalletAccountAddressResolver.swift b/fearless/Common/Model/UniversalWalletAccountAddressResolver.swift index 20675225fd..a779283a81 100644 --- a/fearless/Common/Model/UniversalWalletAccountAddressResolver.swift +++ b/fearless/Common/Model/UniversalWalletAccountAddressResolver.swift @@ -76,6 +76,10 @@ enum UniversalWalletChainAccountSupport { case UniversalWalletRegistry.solanaMainnet.chainId, UniversalWalletRegistry.solanaMainnet.id, UniversalWalletRegistry.solanaDevnet.chainId, UniversalWalletRegistry.solanaDevnet.id: return solanaAddress(fromPublicKey: publicKey) + case TonChainSelection.mainnetChainId, + UniversalWalletRegistry.tonMainnetRegistryEntry.chainId, + UniversalWalletRegistry.tonMainnetRegistryEntry.id: + return tonAddress(fromPublicKey: publicKey) case UniversalWalletRegistry.taira.chainId, UniversalWalletRegistry.taira.id: return irohaAddress( fromPublicKey: publicKey, @@ -113,6 +117,14 @@ enum UniversalWalletChainAccountSupport { UniversalWalletRegistry.solanaDevnet.chainId, UniversalWalletRegistry.solanaDevnet.id ] + case TonChainSelection.mainnetChainId, + UniversalWalletRegistry.tonMainnetRegistryEntry.chainId, + UniversalWalletRegistry.tonMainnetRegistryEntry.id: + return [ + TonChainSelection.mainnetChainId, + UniversalWalletRegistry.tonMainnetRegistryEntry.chainId, + UniversalWalletRegistry.tonMainnetRegistryEntry.id + ] case UniversalWalletRegistry.taira.chainId, UniversalWalletRegistry.taira.id: return [ UniversalWalletRegistry.taira.chainId, @@ -155,6 +167,10 @@ enum UniversalWalletChainAccountSupport { return address } + private static func tonAddress(fromPublicKey publicKey: Data) -> AccountAddress? { + try? TonAddressCodec.v4R2Addresses(publicKey: publicKey).nonBounceable + } + private static func irohaAddress( fromPublicKey publicKey: Data, chainDiscriminant: Int diff --git a/fearless/Common/Model/UniversalWalletRegistry.swift b/fearless/Common/Model/UniversalWalletRegistry.swift index 3b5fb3da3d..954cde2eb6 100644 --- a/fearless/Common/Model/UniversalWalletRegistry.swift +++ b/fearless/Common/Model/UniversalWalletRegistry.swift @@ -4,6 +4,7 @@ enum UniversalWalletRegistry { static let bitcoinMainnetIndexerBaseURL = URL(string: "https://blockstream.info/api")! static let bitcoinTestnetIndexerBaseURL = URL(string: "https://blockstream.info/testnet/api")! static let tonIndexerBaseURL = URL(string: "https://ti.soramitsu.io")! + static let tonNativeAssetId = TonConstants.tonAssetId static let solanaIndexerBaseURL = URL(string: "https://si.soramitsu.io")! static let solanaMainnetRPCURL = URL(string: "https://api.mainnet-beta.solana.com")! static let solanaDevnetRPCURL = URL(string: "https://api.devnet.solana.com")! diff --git a/fearless/Common/Network/BlockExplorer/Subquery/Data/SubqueryDelegatorHistoryData.swift b/fearless/Common/Network/BlockExplorer/Subquery/Data/SubqueryDelegatorHistoryData.swift index ec105224d2..4d868729c2 100644 --- a/fearless/Common/Network/BlockExplorer/Subquery/Data/SubqueryDelegatorHistoryData.swift +++ b/fearless/Common/Network/BlockExplorer/Subquery/Data/SubqueryDelegatorHistoryData.swift @@ -12,7 +12,9 @@ extension SubqueryDelegatorHistoryData: RewardHistoryResponseProtocol { func rewardHistory(for address: String) -> [RewardHistoryItemProtocol] { delegators.nodes.first { element in element.id == address - }?.delegatorHistoryElements.nodes ?? [] + }?.delegatorHistoryElements.nodes.filter { item in + item.delegatorId == address + } ?? [] } } diff --git a/fearless/Common/Network/BlockExplorer/Subquery/Data/SubqueryDelegatorHistoryItem.swift b/fearless/Common/Network/BlockExplorer/Subquery/Data/SubqueryDelegatorHistoryItem.swift index 47411b7160..0a04e7f9a2 100644 --- a/fearless/Common/Network/BlockExplorer/Subquery/Data/SubqueryDelegatorHistoryItem.swift +++ b/fearless/Common/Network/BlockExplorer/Subquery/Data/SubqueryDelegatorHistoryItem.swift @@ -5,9 +5,61 @@ import IrohaCrypto import BigInt struct SubqueryDelegatorHistoryItem: Decodable, RewardHistoryItemProtocol, DelegatorHistoryItem { + enum CodingKeys: String, CodingKey { + case id + case type + case timestampInSeconds = "timestamp" + case blockNumber + case amount + case delegatorId + case collatorId + case roundId + } + let id: String let type: SubqueryDelegationAction let timestampInSeconds: String let blockNumber: Int let amount: BigUInt + let delegatorId: String? + let collatorId: String? + let roundId: String? + + var attribution: RewardHistoryAttribution? { + RewardHistoryAttribution( + validatorAddress: collatorId, + era: roundId.flatMap { UInt64($0, radix: 10) } + ) + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + id = try container.decode(String.self, forKey: .id) + type = try container.decode(SubqueryDelegationAction.self, forKey: .type) + timestampInSeconds = try container.decode(String.self, forKey: .timestampInSeconds) + blockNumber = try container.decode(Int.self, forKey: .blockNumber) + if let amountString = try? container.decode(String.self, forKey: .amount), + let decodedAmount = RewardAmountParser.parse(amountString) { + amount = decodedAmount + } else if let integerAmount = try? container.decode(UInt64.self, forKey: .amount) { + amount = BigUInt(integerAmount) + } else { + throw DecodingError.dataCorruptedError( + forKey: .amount, + in: container, + debugDescription: "Reward amount must be an unsigned integer" + ) + } + delegatorId = try? container.decode(String.self, forKey: .delegatorId) + collatorId = try? container.decode(String.self, forKey: .collatorId) + + if let stringRoundId = try? container.decode(String.self, forKey: .roundId) { + roundId = stringRoundId + } else if let integerRoundId = try? container.decode(UInt64.self, forKey: .roundId) { + roundId = String(integerRoundId) + } else { + roundId = nil + } + } } diff --git a/fearless/Common/Network/BlockExplorer/Subquery/Data/SubqueryRewardData.swift b/fearless/Common/Network/BlockExplorer/Subquery/Data/SubqueryRewardData.swift index f3607b7f37..27e81b5da3 100644 --- a/fearless/Common/Network/BlockExplorer/Subquery/Data/SubqueryRewardData.swift +++ b/fearless/Common/Network/BlockExplorer/Subquery/Data/SubqueryRewardData.swift @@ -14,6 +14,24 @@ struct SubqueryRewardItemData: Equatable, Codable { } extension SubqueryRewardItemData { + init?(rewardHistoryItem: RewardHistoryItemProtocol, stashAddress: AccountAddress) { + guard let timestamp = Int64(rewardHistoryItem.timestampInSeconds) else { + return nil + } + + let attribution = rewardHistoryItem.attribution + + self.init( + eventId: rewardHistoryItem.id, + timestamp: timestamp, + validatorAddress: attribution?.validatorAddress ?? "", + era: attribution?.era ?? 0, + stashAddress: stashAddress, + amount: rewardHistoryItem.amount, + isReward: rewardHistoryItem.type == .reward + ) + } + init?(from json: JSON) { guard let eventId = json.id?.stringValue, diff --git a/fearless/Common/Network/BlockExplorer/Subsquid/ParachainSubsquidRewardResponse.swift b/fearless/Common/Network/BlockExplorer/Subsquid/ParachainSubsquidRewardResponse.swift index fdf405f2e9..e201a2e7ae 100644 --- a/fearless/Common/Network/BlockExplorer/Subsquid/ParachainSubsquidRewardResponse.swift +++ b/fearless/Common/Network/BlockExplorer/Subsquid/ParachainSubsquidRewardResponse.swift @@ -1,31 +1,75 @@ import Foundation import BigInt -import SSFUtils struct SubsquidDelegatorRewardsData: Decodable { - var rewards: [SubsquidDelegatorRewardItem] + let historyElements: [SubsquidDelegatorRewardItem] +} + +struct SubsquidDelegatorReward: Decodable { + enum CodingKeys: String, CodingKey { + case amount + case era + case stash + case validator + } + + let amount: BigUInt + let era: Int? + let stash: String? + let validator: String? + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let amountString = try container.decode(String.self, forKey: .amount) + + guard let decodedAmount = RewardAmountParser.parse(amountString) else { + throw DecodingError.dataCorruptedError( + forKey: .amount, + in: container, + debugDescription: "Reward amount must be an unsigned integer string" + ) + } + + amount = decodedAmount + era = try? container.decode(Int.self, forKey: .era) + stash = try? container.decode(String.self, forKey: .stash) + validator = try? container.decode(String.self, forKey: .validator) + } } struct SubsquidDelegatorRewardItem: Decodable, RewardHistoryItemProtocol { - @StringCodable var amount: BigUInt + enum CodingKeys: String, CodingKey { + case id + case address + case timestamp + case blockNumber = "blockHeight" + case reward + } + let id: String - let accountId: String - let timestamp: String + let address: String? + let timestamp: Int64 let blockNumber: Int + let reward: SubsquidDelegatorReward var type: SubqueryDelegationAction { .reward } - var timestampInSeconds: String { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSZ" - let date = dateFormatter.date(from: timestamp) - let timeInterval = date?.timeIntervalSince1970 ?? 0 - let timeIntervalIntValue = Int64(timeInterval) - return "\(timeIntervalIntValue)" + var timestampInSeconds: String { String(timestamp) } + var amount: BigUInt { reward.amount } + var attribution: RewardHistoryAttribution? { + let era = reward.era.flatMap { value in + value >= 0 ? UInt64(value) : nil + } + return RewardHistoryAttribution( + validatorAddress: reward.validator, + era: era + ) } } extension SubsquidDelegatorRewardsData: RewardHistoryResponseProtocol { func rewardHistory(for address: String) -> [RewardHistoryItemProtocol] { - rewards.filter { $0.accountId == address } + historyElements.filter { item in + item.address == address && item.reward.stash == address + } } } diff --git a/fearless/CoreLayer/OperationFactory/BlockExplorer/Rewards/RewardOperationFactory.swift b/fearless/CoreLayer/OperationFactory/BlockExplorer/Rewards/RewardOperationFactory.swift index b61e300894..9cad6ece82 100644 --- a/fearless/CoreLayer/OperationFactory/BlockExplorer/Rewards/RewardOperationFactory.swift +++ b/fearless/CoreLayer/OperationFactory/BlockExplorer/Rewards/RewardOperationFactory.swift @@ -7,12 +7,100 @@ protocol RewardHistoryResponseProtocol { func rewardHistory(for address: String) -> [RewardHistoryItemProtocol] } +enum RewardHistoryRequestError: Error { + case invalidParameters +} + +enum RewardHistoryRequestValidator { + private static let maximumAddressLength = 128 + + static func isValid( + address: AccountAddress, + startTimestamp: Int64?, + endTimestamp: Int64?, + maximumTimestamp: Int64 = .max + ) -> Bool { + let addressScalars = address.unicodeScalars + guard + (1 ... maximumAddressLength).contains(addressScalars.count), + addressScalars.allSatisfy({ scalar in + switch scalar.value { + case 48 ... 57, 65 ... 90, 97 ... 122: + return true + default: + return false + } + }) + else { + return false + } + + if let startTimestamp, + startTimestamp < 0 || startTimestamp > maximumTimestamp { + return false + } + if let endTimestamp, + endTimestamp < 0 || endTimestamp > maximumTimestamp { + return false + } + if let startTimestamp, let endTimestamp, startTimestamp > endTimestamp { + return false + } + + return true + } +} + +enum RewardAmountParser { + static let maximumDecimalDigits = 78 + + static func parse(_ value: String) -> BigUInt? { + let bytes = value.utf8 + + guard + !bytes.isEmpty, + bytes.count <= maximumDecimalDigits, + bytes.allSatisfy({ (48 ... 57).contains($0) }), + bytes.count == 1 || bytes.first != 48 + else { + return nil + } + + return BigUInt(value, radix: 10) + } +} + +struct RewardHistoryAttribution: Equatable { + let validatorAddress: AccountAddress + let era: EraIndex + + init?(validatorAddress: AccountAddress?, era: UInt64?) { + guard + let validatorAddress, + validatorAddress == validatorAddress.trimmingCharacters(in: .whitespacesAndNewlines), + !validatorAddress.isEmpty, + let era, + let exactEra = EraIndex(exactly: era) + else { + return nil + } + + self.validatorAddress = validatorAddress + self.era = exactEra + } +} + protocol RewardHistoryItemProtocol { var id: String { get } var type: SubqueryDelegationAction { get } var timestampInSeconds: String { get } var blockNumber: Int { get } var amount: BigUInt { get } + var attribution: RewardHistoryAttribution? { get } +} + +extension RewardHistoryItemProtocol { + var attribution: RewardHistoryAttribution? { nil } } protocol CollatorAprInfoProtocol { diff --git a/fearless/CoreLayer/OperationFactory/BlockExplorer/Rewards/SubqueryRewardOperationFactory.swift b/fearless/CoreLayer/OperationFactory/BlockExplorer/Rewards/SubqueryRewardOperationFactory.swift index 188cbca4e4..c2b89e1b3f 100644 --- a/fearless/CoreLayer/OperationFactory/BlockExplorer/Rewards/SubqueryRewardOperationFactory.swift +++ b/fearless/CoreLayer/OperationFactory/BlockExplorer/Rewards/SubqueryRewardOperationFactory.swift @@ -54,15 +54,14 @@ final class SubqueryRewardOperationFactory { ) -> String { let timestampFilter: String = { guard startTimestamp != nil || endTimestamp != nil else { return "" } - var result = "timestamp:{" + var bounds = [String]() if let timestamp = startTimestamp { - result.append("greaterThanOrEqualTo:\"\(timestamp)\",") + bounds.append("greaterThanOrEqualTo: \"\(timestamp)\"") } if let timestamp = endTimestamp { - result.append("lessThanOrEqualTo:\"\(timestamp)\",") + bounds.append("lessThanOrEqualTo: \"\(timestamp)\"") } - result.append("}") - return result + return "timestamp: { \(bounds.joined(separator: ", ")) }," }() return """ @@ -74,16 +73,23 @@ final class SubqueryRewardOperationFactory { ) { nodes { id - delegatorHistoryElements(orderBy: TIMESTAMP_DESC, filter: { amount: {isNull: false}, \(timestampFilter), type: { equalTo: 0 }}) { + delegatorHistoryElements( + orderBy: TIMESTAMP_DESC, + filter: { + amount: { isNull: false }, + \(timestampFilter) + type: { equalTo: \(SubqueryDelegationAction.reward.rawValue) } + } + ) { nodes { id blockNumber amount type timestamp - delegator { - id - } + delegatorId + collatorId + roundId } } } @@ -244,6 +250,14 @@ extension SubqueryRewardOperationFactory: RewardOperationFactoryProtocol { startTimestamp: Int64?, endTimestamp: Int64? ) -> BaseOperation { + guard RewardHistoryRequestValidator.isValid( + address: address, + startTimestamp: startTimestamp, + endTimestamp: endTimestamp + ) else { + return BaseOperation.createWithError(RewardHistoryRequestError.invalidParameters) + } + let queryString = prepareDelegatorHistoryRequest( address: address, startTimestamp: startTimestamp, @@ -296,6 +310,14 @@ extension SubqueryRewardOperationFactory: RewardOperationFactoryProtocol { startTimestamp: Int64?, endTimestamp: Int64? ) -> BaseOperation { + guard RewardHistoryRequestValidator.isValid( + address: address, + startTimestamp: startTimestamp, + endTimestamp: endTimestamp + ) else { + return BaseOperation.createWithError(RewardHistoryRequestError.invalidParameters) + } + let queryString = prepareHistoryRequestForAddress( address, startTimestamp: startTimestamp, diff --git a/fearless/CoreLayer/OperationFactory/BlockExplorer/Rewards/SubsquidRewardOperationFactory.swift b/fearless/CoreLayer/OperationFactory/BlockExplorer/Rewards/SubsquidRewardOperationFactory.swift index a620884abe..2536357e72 100644 --- a/fearless/CoreLayer/OperationFactory/BlockExplorer/Rewards/SubsquidRewardOperationFactory.swift +++ b/fearless/CoreLayer/OperationFactory/BlockExplorer/Rewards/SubsquidRewardOperationFactory.swift @@ -1,7 +1,6 @@ import Foundation import RobinHood import SSFUtils -import SoraFoundation import SSFModels enum ArrosquidRewardOperationFactoryError: Error { @@ -36,44 +35,53 @@ final class ArrowsquidRewardOperationFactory { """ } + private func prepareTimestampFilter( + startTimestamp: Int64?, + endTimestamp: Int64? + ) -> String { + var bounds = [String]() + if let startTimestamp { + bounds.append("timestamp_gte: \(startTimestamp)") + } + if let endTimestamp { + bounds.append("timestamp_lte: \(endTimestamp)") + } + + guard !bounds.isEmpty else { + return "" + } + + return "AND: { \(bounds.joined(separator: ", ")) }," + } + private func prepareDelegatorHistoryRequest( address: String, startTimestamp: Int64?, endTimestamp: Int64? ) -> String { - let timestampFilter: String = { - let locale = LocalizationManager.shared.selectedLocale - guard startTimestamp != nil || endTimestamp != nil else { return "" } - - var result = "AND: {" - let dateFormatter = DateFormatter.suibsquidInputDate.value(for: locale) - if let startTimestamp = startTimestamp { - let startDate = Date(timeIntervalSince1970: TimeInterval(startTimestamp)) - let startDateString = dateFormatter.string(from: startDate) - result.append("timestamp_gte:\"\(startDateString)\"") - } - - if let endTimestamp = endTimestamp { - let endDate = Date(timeIntervalSince1970: TimeInterval(endTimestamp)) - let endDateString = dateFormatter.string(from: endDate) - result.append("timestamp_lte:\"\(endDateString)\"") - } - result.append("}") - return result - }() + let timestampFilter = prepareTimestampFilter( + startTimestamp: startTimestamp, + endTimestamp: endTimestamp + ) return """ query MyQuery { - rewards(orderBy: timestamp_DESC, where: {accountId_containsInsensitive: "\(address)", \(timestampFilter)}) { + historyElements( + orderBy: timestamp_DESC, + where: { address_eq: "\(address)", \(timestampFilter) reward_isNull: false } + ) { id - accountId - amount - blockNumber - round + blockHeight timestamp + address + reward { + amount + era + stash + validator + } } } - """ } @@ -82,30 +90,17 @@ final class ArrowsquidRewardOperationFactory { startTimestamp: Int64?, endTimestamp: Int64? ) -> String { - let timestampFilter: String = { - let locale = LocalizationManager.shared.selectedLocale - guard startTimestamp != nil || endTimestamp != nil else { return "" } - - var result = "AND: {" - let dateFormatter = DateFormatter.suibsquidInputDate.value(for: locale) - if let startTimestamp = startTimestamp { - let startDate = Date(timeIntervalSince1970: TimeInterval(startTimestamp)) - let startDateString = dateFormatter.string(from: startDate) - result.append("timestamp_gte:\"\(startDateString)\"") - } - - if let endTimestamp = endTimestamp { - let endDate = Date(timeIntervalSince1970: TimeInterval(endTimestamp)) - let endDateString = dateFormatter.string(from: endDate) - result.append("timestamp_lte:\"\(endDateString)\"") - } - result.append("}") - return result - }() + let timestampFilter = prepareTimestampFilter( + startTimestamp: startTimestamp, + endTimestamp: endTimestamp + ) return """ query MyQuery { - historyElements(orderBy: timestamp_DESC, where: {address_eq: "\(address)", \(timestampFilter), reward_isNull: false}) { + historyElements( + orderBy: timestamp_DESC, + where: { address_eq: "\(address)", \(timestampFilter) reward_isNull: false } + ) { timestamp id address @@ -229,6 +224,15 @@ extension ArrowsquidRewardOperationFactory: RewardOperationFactoryProtocol { startTimestamp: Int64?, endTimestamp: Int64? ) -> BaseOperation { + guard RewardHistoryRequestValidator.isValid( + address: address, + startTimestamp: startTimestamp, + endTimestamp: endTimestamp, + maximumTimestamp: Int64(Int32.max) + ) else { + return BaseOperation.createWithError(RewardHistoryRequestError.invalidParameters) + } + let queryString = prepareDelegatorHistoryRequest( address: address, startTimestamp: startTimestamp, @@ -281,6 +285,15 @@ extension ArrowsquidRewardOperationFactory: RewardOperationFactoryProtocol { startTimestamp: Int64?, endTimestamp: Int64? ) -> BaseOperation { + guard RewardHistoryRequestValidator.isValid( + address: address, + startTimestamp: startTimestamp, + endTimestamp: endTimestamp, + maximumTimestamp: Int64(Int32.max) + ) else { + return BaseOperation.createWithError(RewardHistoryRequestError.invalidParameters) + } + let queryString = prepareHistoryRequestForAddress( address, startTimestamp: startTimestamp, diff --git a/fearless/Modules/Banners/BannersAssembly.swift b/fearless/Modules/Banners/BannersAssembly.swift index 90be3884a7..3db562ac87 100644 --- a/fearless/Modules/Banners/BannersAssembly.swift +++ b/fearless/Modules/Banners/BannersAssembly.swift @@ -10,12 +10,14 @@ final class BannersAssembly { ) -> BannersModuleCreationResult? { let localizationManager = LocalizationManager.shared - let walletProvider = UserDataStorageFacade.shared - .createStreamableProvider( - filter: NSPredicate.selectedMetaAccount(), - sortDescriptors: [], - mapper: AnyCoreDataMapper(ManagedMetaAccountMapper()) - ) + let walletProvider = AccountProviderFactory( + storageFacade: UserDataStorageFacade.shared, + operationManager: OperationManagerFacade.sharedManager, + logger: Logger.shared + ).createManagedMetaAccountProvider( + for: NSPredicate.selectedMetaAccount(), + sortDescriptors: [] + ) let interactor = BannersInteractor( walletProvider: walletProvider, diff --git a/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmInteractor.swift b/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmInteractor.swift index 2b9942bcef..7fa5fbadda 100644 --- a/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmInteractor.swift +++ b/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmInteractor.swift @@ -16,6 +16,7 @@ final class WalletSendConfirmInteractor: RuntimeConstantFetching { private let chainAsset: ChainAsset private let wallet: MetaAccountModel private var equilibriumTotalBalanceService: EquilibriumTotalBalanceServiceProtocol? + private var tonFeePresentation: (fee: BigUInt, id: String)? let dependencyContainer: SendDepencyContainer private var balanceProvider: AnyDataProvider? @@ -99,7 +100,18 @@ extension WalletSendConfirmInteractor: WalletSendConfirmInteractorInputProtocol } func getFeePaymentChainAsset(for chainAsset: ChainAsset?) -> ChainAsset? { - guard let chainAsset = chainAsset else { return nil } + Self.resolveFeePaymentChainAsset(for: chainAsset) + } + + static func resolveFeePaymentChainAsset(for chainAsset: ChainAsset?) -> ChainAsset? { + guard let chainAsset else { return nil } + // TON fee quotes are denominated in the selected native TON asset. Never select an + // arbitrary member of the registry's utility-asset Set: a hostile or future extra + // utility entry could otherwise change the visible precision/symbol while the raw + // nanotons quote is acknowledged. + if chainAsset.chain.isTonCompatibilityChain { + return chainAsset + } if let utilityAsset = chainAsset.chain.utilityAssets().first { return ChainAsset(chain: chainAsset.chain, asset: utilityAsset) } @@ -132,6 +144,74 @@ extension WalletSendConfirmInteractor: WalletSendConfirmInteractorInputProtocol } } } + + func confirmFeePresentation(fee: BigUInt, completion: @escaping (Bool) -> Void) { + guard chainAsset.chain.isTonCompatibilityChain else { + completion(true) + return + } + guard let presentation = tonFeePresentation, + presentation.fee == fee + else { + completion(false) + return + } + Task { + let accepted: Bool + do { + let service = try await dependencyContainer + .prepareDepencies(chainAsset: chainAsset) + .transferService + accepted = await service.confirmFeePresentation( + id: presentation.id, + fee: presentation.fee + ) + } catch { + accepted = false + } + await MainActor.run { + completion(accepted) + } + } + } + + func acknowledgeSubmittedTransfer( + hash: String, + recoveredIdentity: TonTransferIntentIdentity?, + completion: @escaping (Bool) -> Void + ) { + guard case let .transfer(transfer) = call else { + completion(false) + return + } + Task { + guard let service = try? await dependencyContainer + .prepareDepencies(chainAsset: chainAsset) + .transferService + else { + await MainActor.run { completion(false) } + return + } + let acknowledged: Bool + if let recoveredIdentity { + acknowledged = await service.acknowledgeRecoveredTransfer( + hash: hash, + identity: recoveredIdentity + ) + } else { + acknowledged = await service.acknowledgeSubmittedTransfer( + hash: hash, + transfer: transfer + ) + } + await MainActor.run { completion(acknowledged) } + } + } + + func refreshFee() { + tonFeePresentation = nil + subscribeToFee() + } } extension WalletSendConfirmInteractor: AccountInfoSubscriptionAdapterHandler { @@ -146,6 +226,7 @@ extension WalletSendConfirmInteractor: AccountInfoSubscriptionAdapterHandler { extension WalletSendConfirmInteractor: TransferFeeEstimationListener { func didReceiveFee(fee: BigUInt) { + tonFeePresentation = nil DispatchQueue.main.async { [weak self] in self?.presenter?.didReceiveFee(result: .success(RuntimeDispatchInfo(feeValue: fee))) } @@ -157,3 +238,14 @@ extension WalletSendConfirmInteractor: TransferFeeEstimationListener { } } } + +extension WalletSendConfirmInteractor: TonTransferFeePresentationListener { + func didReceiveTonFee(fee: BigUInt, presentationID: String) { + DispatchQueue.main.async { [weak self] in + // Preserve the exact opaque quote ID until the presenter confirms that this fee + // has actually been applied to the view. + self?.tonFeePresentation = (fee, presentationID) + self?.presenter?.didReceiveFee(result: .success(RuntimeDispatchInfo(feeValue: fee))) + } + } +} diff --git a/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmPresenter.swift b/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmPresenter.swift index 9a0fcffb34..9d03a5b1b7 100644 --- a/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmPresenter.swift +++ b/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmPresenter.swift @@ -5,6 +5,22 @@ import SoraFoundation import IrohaCrypto import SwiftUI import SSFModels +import UIKit + +struct TonUnknownOutcomeRecoveryModel: Equatable { + let messageHashHex: String + + init?(messageHashHex: String) { + guard messageHashHex.utf8.count == 64, + messageHashHex.utf8.allSatisfy({ byte in + (48 ... 57).contains(byte) || (97 ... 102).contains(byte) + }) + else { + return nil + } + self.messageHashHex = messageHashHex + } +} struct SendLoadingCollector { var feeReady: Bool = false @@ -48,6 +64,9 @@ final class WalletSendConfirmPresenter { private var fee: Decimal? private var minimumBalance: BigUInt? private var eqUilibriumTotalBalance: Decimal? + private var pendingTonFeePresentation: BigUInt? + private var confirmingTonFeePresentation: BigUInt? + private var viewModelGeneration: UInt64 = 0 private var loadingCollector = SendLoadingCollector() private var priceData: PriceData? { @@ -78,16 +97,19 @@ final class WalletSendConfirmPresenter { self.call = call self.wallet = wallet self.scamInfo = scamInfo - self.feeViewModel = feeViewModel + self.feeViewModel = chainAsset.chain.isTonCompatibilityChain ? nil : feeViewModel self.localizationManager = localizationManager - if let feeViewModel { + if let feeViewModel, !chainAsset.chain.isTonCompatibilityChain { fee = Decimal(string: feeViewModel.amount) } - loadingCollector.feeReady = feeViewModel != nil + loadingCollector.feeReady = !chainAsset.chain.isTonCompatibilityChain && feeViewModel != nil } private func provideViewModel() { - Task { + viewModelGeneration = viewModelGeneration == UInt64.max ? 1 : viewModelGeneration + 1 + let generation = viewModelGeneration + Task { @MainActor [weak self] in + guard let self else { return } let amount = Decimal.fromSubstrateAmount(call.amount, precision: Int16(chainAsset.asset.precision)) ?? .zero let parameters = WalletSendConfirmViewModelFactoryParameters( amount: amount, @@ -106,9 +128,38 @@ final class WalletSendConfirmPresenter { parameters: parameters ) - await MainActor.run { - self.view?.didReceive(state: .loaded(viewModel)) + guard generation == viewModelGeneration else { return } + view?.didReceive(state: .loaded(viewModel)) + confirmPendingTonFeePresentationIfNeeded() + } + } + + @MainActor + private func confirmPendingTonFeePresentationIfNeeded() { + guard chainAsset.chain.isTonCompatibilityChain, + let rawFee = pendingTonFeePresentation, + confirmingTonFeePresentation != rawFee + else { + return + } + confirmingTonFeePresentation = rawFee + interactor.confirmFeePresentation(fee: rawFee) { [weak self] accepted in + guard let self else { return } + guard pendingTonFeePresentation == rawFee else { + if confirmingTonFeePresentation == rawFee { + confirmingTonFeePresentation = nil + } + return } + confirmingTonFeePresentation = nil + guard accepted else { + loadingCollector.feeReady = false + checkLoadingState() + return + } + pendingTonFeePresentation = nil + loadingCollector.feeReady = true + checkLoadingState() } } @@ -170,9 +221,18 @@ final class WalletSendConfirmPresenter { return } let utilityPriceData = utilityAsset.asset.getPrice(for: wallet.selectedCurrency) + let feeUsageCase: NumberFormatterUsageCase = chainAsset.chain.isTonCompatibilityChain + ? .exactCrypto(fractionDigits: Int(utilityAsset.asset.precision)) + : .detailsCrypto let viewModel = fee - .map { balanceViewModelFactory.balanceFromPrice($0, priceData: utilityPriceData, usageCase: .detailsCrypto) }? + .map { + balanceViewModelFactory.balanceFromPrice( + $0, + priceData: utilityPriceData, + usageCase: feeUsageCase + ) + }? .value(for: selectedLocale) feeViewModel = viewModel } @@ -222,9 +282,161 @@ final class WalletSendConfirmPresenter { } private func checkLoadingState() { - DispatchQueue.main.async { [unowned self] in - self.view?.didReceive(isLoading: !self.loadingCollector.isReady) + let update = { [weak self] in + guard let self else { return } + view?.didReceive(isLoading: !loadingCollector.isReady) } + if Thread.isMainThread { + update() + } else { + DispatchQueue.main.async(execute: update) + } + } + + private func presentTonUnknownOutcome(messageHashHex: String) -> Bool { + guard let recovery = TonUnknownOutcomeRecoveryModel( + messageHashHex: messageHashHex + ) else { + return false + } + + let copyAction = SheetAlertPresentableAction( + title: tonRecoveryLocalizedString( + key: "ton.transfer.unknown.copy_hash", + fallback: "Copy message hash" + ), + style: .pinkBackgroundWhiteText + ) { + UIPasteboard.general.string = recovery.messageHashHex + } + let messageTemplate = tonRecoveryLocalizedString( + key: "ton.transfer.unknown.message", + fallback: "The network may have accepted this transfer. Do not send it again. " + + "Save this message hash and check its status before taking further action:\n%@" + ) + let viewModel = SheetAlertPresentableViewModel( + title: tonRecoveryLocalizedString( + key: "ton.transfer.unknown.title", + fallback: "Transfer status is unknown" + ), + message: String( + format: messageTemplate, + locale: selectedLocale, + recovery.messageHashHex + ), + actions: [copyAction], + closeAction: R.string.localizable.commonClose( + preferredLanguages: selectedLocale.rLanguages + ), + icon: R.image.iconWarningBig() + ) + wireframe.present(viewModel: viewModel, from: view) + return true + } + + private func presentTonPriorTransferConfirmed( + identity: TonTransferIntentIdentity, + messageHashHex: String + ) -> Bool { + guard let recovery = TonUnknownOutcomeRecoveryModel( + messageHashHex: messageHashHex + ) else { + return false + } + + pendingTonFeePresentation = nil + confirmingTonFeePresentation = nil + loadingCollector.feeReady = false + checkLoadingState() + + let acknowledgeAction = SheetAlertPresentableAction( + title: tonRecoveryLocalizedString( + key: "ton.transfer.prior_confirmed.acknowledge", + fallback: "Acknowledge and recalculate fee" + ), + style: .pinkBackgroundWhiteText + ) { [weak self] in + guard let self else { return } + interactor.acknowledgeSubmittedTransfer( + hash: recovery.messageHashHex, + recoveredIdentity: identity + ) { [weak self] acknowledged in + guard let self else { return } + if acknowledged { + interactor.refreshFee() + } else if let view { + wireframe.presentExtrinsicFailed(from: view, locale: selectedLocale) + } + } + } + let messageTemplate = tonRecoveryLocalizedString( + key: "ton.transfer.prior_confirmed.message", + fallback: "A previous transfer was confirmed. This new transfer was not sent. " + + "Review and acknowledge the previous hash before confirming again:\n%@" + ) + let viewModel = SheetAlertPresentableViewModel( + title: tonRecoveryLocalizedString( + key: "ton.transfer.prior_confirmed.title", + fallback: "Previous transfer confirmed" + ), + message: String( + format: messageTemplate, + locale: selectedLocale, + recovery.messageHashHex + ), + actions: [acknowledgeAction], + closeAction: R.string.localizable.commonClose( + preferredLanguages: selectedLocale.rLanguages + ), + icon: R.image.iconWarningBig() + ) + wireframe.present(viewModel: viewModel, from: view) + return true + } + + private func completeTransferAfterVisibleReceipt(hash: String) { + guard chainAsset.chain.isTonCompatibilityChain else { + wireframe.complete(on: view, title: hash, chainAsset: chainAsset) + return + } + guard let completionWireframe = wireframe as? WalletSendConfirmCompletionPresenting else { + // A custom router without a presentation callback may show success, but it must + // not delete the durable tombstone on an unobservable scheduling boundary. + wireframe.complete(on: view, title: hash, chainAsset: chainAsset) + return + } + completionWireframe.completeAfterPresentation( + on: view, + title: hash, + chainAsset: chainAsset + ) { [weak self] in + self?.interactor.acknowledgeSubmittedTransfer( + hash: hash, + recoveredIdentity: nil + ) { [weak self] acknowledged in + if !acknowledged { + self?.logger?.error("TON confirmed receipt acknowledgement failed; tombstone retained") + } + } + } + } + + private func tonRecoveryLocalizedString(key: String, fallback: String) -> String { + for language in selectedLocale.rLanguages ?? [] { + if let path = Bundle.main.path(forResource: language, ofType: "lproj"), + let bundle = Bundle(path: path) { + return bundle.localizedString( + forKey: key, + value: fallback, + table: "Localizable" + ) + } + } + return Bundle.main.localizedString( + forKey: key, + value: fallback, + table: "Localizable" + ) } } @@ -253,6 +465,9 @@ extension WalletSendConfirmPresenter: WalletSendConfirmPresenterProtocol { } func setup() { + if chainAsset.chain.isTonCompatibilityChain { + view?.didReceive(isLoading: true) + } interactor.setup() provideViewModel() loadingCollector.utilityBalanceReady = chainAsset.isUtility @@ -263,6 +478,9 @@ extension WalletSendConfirmPresenter: WalletSendConfirmPresenterProtocol { } func didTapConfirmButton() { + guard !chainAsset.chain.isTonCompatibilityChain || loadingCollector.isReady else { + return + } switch call { case .transfer: validateAndSubmitTransfer() @@ -278,13 +496,25 @@ extension WalletSendConfirmPresenter: WalletSendConfirmInteractorOutputProtocol switch result { case let .success(hash): - - wireframe.complete(on: view, title: hash, chainAsset: chainAsset) + completeTransferAfterVisibleReceipt(hash: hash) case let .failure(error): guard let view = view else { return } + if case let TransferServiceError.tonPriorTransferConfirmed(identity, messageHashHex) = error, + presentTonPriorTransferConfirmed( + identity: identity, + messageHashHex: messageHashHex + ) { + return + } + + if case let TransferServiceError.tonBroadcastOutcomeUnknown(messageHashHex) = error, + presentTonUnknownOutcome(messageHashHex: messageHashHex) { + return + } + if let rpcError = error as? RPCResponse.Error, rpcError.code == -32000 { wireframe.presentAmountTooHigh(from: view, locale: selectedLocale) return @@ -347,10 +577,17 @@ extension WalletSendConfirmPresenter: WalletSendConfirmInteractorOutputProtocol switch result { case let .success(dispatchInfo): guard let utilityAsset = interactor.getFeePaymentChainAsset(for: chainAsset) else { return } - fee = BigUInt(string: dispatchInfo.fee).map { + let rawFee = BigUInt(string: dispatchInfo.fee) + fee = rawFee.map { Decimal.fromSubstrateAmount($0, precision: Int16(utilityAsset.asset.precision)) } ?? nil updateFeeViewModel() + if chainAsset.chain.isTonCompatibilityChain { + pendingTonFeePresentation = rawFee + confirmingTonFeePresentation = nil + loadingCollector.feeReady = false + view?.didReceive(isLoading: true) + } provideViewModel() let amount = Decimal.fromSubstrateAmount(call.amount, precision: Int16(chainAsset.asset.precision)) ?? .zero let tipPaymentChainAsset = interactor.getFeePaymentChainAsset(for: chainAsset) @@ -359,9 +596,17 @@ extension WalletSendConfirmPresenter: WalletSendConfirmInteractorOutputProtocol let fullAmount = amount + fee.or(.zero) + tip interactor.fetchEquilibriumTotalBalance(chainAsset: chainAsset, amount: fullAmount) - loadingCollector.feeReady = true - checkLoadingState() + if !chainAsset.chain.isTonCompatibilityChain { + loadingCollector.feeReady = true + checkLoadingState() + } case let .failure(error): + if chainAsset.chain.isTonCompatibilityChain { + pendingTonFeePresentation = nil + confirmingTonFeePresentation = nil + loadingCollector.feeReady = false + view?.didReceive(isLoading: true) + } logger?.error("Did receive fee error: \(error)") } } diff --git a/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmProtocols.swift b/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmProtocols.swift index b0113d7dea..66656827cb 100644 --- a/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmProtocols.swift +++ b/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmProtocols.swift @@ -22,6 +22,24 @@ protocol WalletSendConfirmInteractorInputProtocol: AnyObject { func getFeePaymentChainAsset(for chainAsset: ChainAsset?) -> ChainAsset? func fetchEquilibriumTotalBalance(chainAsset: ChainAsset, amount: Decimal) func provideConstants() + func confirmFeePresentation(fee: BigUInt, completion: @escaping (Bool) -> Void) + func acknowledgeSubmittedTransfer( + hash: String, + recoveredIdentity: TonTransferIntentIdentity?, + completion: @escaping (Bool) -> Void + ) + func refreshFee() +} + +/// Production success routing invokes the callback only after the all-done controller has +/// actually been presented. TON keeps its confirmed tombstone until this receipt boundary. +protocol WalletSendConfirmCompletionPresenting: AnyObject { + func completeAfterPresentation( + on view: ControllerBackedProtocol?, + title: String, + chainAsset: ChainAsset, + completion: @escaping () -> Void + ) } protocol WalletSendConfirmInteractorOutputProtocol: AnyObject { diff --git a/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmWireframe.swift b/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmWireframe.swift index 01ab05b577..971154e022 100644 --- a/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmWireframe.swift +++ b/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmWireframe.swift @@ -19,6 +19,20 @@ final class WalletSendConfirmWireframe: WalletSendConfirmWireframeProtocol { on view: ControllerBackedProtocol?, title: String, chainAsset: ChainAsset + ) { + complete( + on: view, + title: title, + chainAsset: chainAsset, + presentationCompletion: nil + ) + } + + private func complete( + on view: ControllerBackedProtocol?, + title: String, + chainAsset: ChainAsset, + presentationCompletion: (() -> Void)? ) { let presenter = view?.controller.navigationController?.presentingViewController @@ -33,8 +47,28 @@ final class WalletSendConfirmWireframe: WalletSendConfirmWireframeProtocol { view?.controller.navigationController?.dismiss(animated: true) { if let presenter = presenter as? ControllerBackedProtocol, let controller = controller { - presenter.controller.present(controller, animated: true) + presenter.controller.present( + controller, + animated: true, + completion: presentationCompletion + ) } } } } + +extension WalletSendConfirmWireframe: WalletSendConfirmCompletionPresenting { + func completeAfterPresentation( + on view: ControllerBackedProtocol?, + title: String, + chainAsset: ChainAsset, + completion: @escaping () -> Void + ) { + complete( + on: view, + title: title, + chainAsset: chainAsset, + presentationCompletion: completion + ) + } +} diff --git a/fearless/Modules/Send/SendDependencyContainer.swift b/fearless/Modules/Send/SendDependencyContainer.swift index cc123077cb..c29843dc88 100644 --- a/fearless/Modules/Send/SendDependencyContainer.swift +++ b/fearless/Modules/Send/SendDependencyContainer.swift @@ -28,28 +28,111 @@ struct SendDependencies { enum UniversalWalletSendRoutingError: Error, Equatable { case unsupported(chainId: String) + case tonProductionSendDisabled +} + +/// Native TON routing remains disabled in production until funded mainnet evidence is recorded. +/// There is intentionally no environment-variable, remote-config, or runtime-toggle override. +struct TonProductionSendReleasePolicy: Equatable { + let isEnabled: Bool + + private init(isEnabled: Bool) { + self.isEnabled = isEnabled + } + + static let production = TonProductionSendReleasePolicy(isEnabled: false) + #if DEBUG + static let enabledForTests = TonProductionSendReleasePolicy(isEnabled: true) + #endif } final class SendDepencyContainer { private let wallet: MetaAccountModel private let operationManager: OperationManagerProtocol + private let tonRemote: TonTransferRemoteProtocol? + private let tonMnemonicProvider: UniversalWalletMnemonicProviding + private let tonPendingCoordinator: TonPendingIntentCoordinator? + private let tonSendReleasePolicy: TonProductionSendReleasePolicy private var currentDependecies: SendDependencies? + private var currentDependenciesKey: ChainAssetKey? private var cachedDependencies: [ChainAssetKey: SendDependencies] = [:] - init(wallet: MetaAccountModel, operationManager: OperationManagerProtocol) { + convenience init( + wallet: MetaAccountModel, + operationManager: OperationManagerProtocol, + tonRemote: TonTransferRemoteProtocol? = nil, + tonMnemonicProvider: UniversalWalletMnemonicProviding = KeychainUniversalWalletMnemonicProvider() + ) { + self.init( + wallet: wallet, + operationManager: operationManager, + tonRemote: tonRemote, + tonMnemonicProvider: tonMnemonicProvider, + tonPendingCoordinator: nil, + releasePolicy: .production + ) + } + + #if DEBUG + convenience init( + wallet: MetaAccountModel, + operationManager: OperationManagerProtocol, + tonRemote: TonTransferRemoteProtocol? = nil, + tonMnemonicProvider: UniversalWalletMnemonicProviding = KeychainUniversalWalletMnemonicProvider(), + tonPendingCoordinator: TonPendingIntentCoordinator = TonPendingIntentCoordinator(), + tonSendReleasePolicy: TonProductionSendReleasePolicy + ) { + self.init( + wallet: wallet, + operationManager: operationManager, + tonRemote: tonRemote, + tonMnemonicProvider: tonMnemonicProvider, + tonPendingCoordinator: tonPendingCoordinator, + releasePolicy: tonSendReleasePolicy + ) + } + #endif + + private init( + wallet: MetaAccountModel, + operationManager: OperationManagerProtocol, + tonRemote: TonTransferRemoteProtocol?, + tonMnemonicProvider: UniversalWalletMnemonicProviding, + tonPendingCoordinator: TonPendingIntentCoordinator?, + releasePolicy: TonProductionSendReleasePolicy + ) { self.wallet = wallet self.operationManager = operationManager + self.tonRemote = tonRemote + self.tonMnemonicProvider = tonMnemonicProvider + self.tonPendingCoordinator = tonPendingCoordinator + tonSendReleasePolicy = releasePolicy } + @MainActor func prepareDepencies(chainAsset: ChainAsset) async throws -> SendDependencies { + // This guard must precede account lookup, dependency cache access, and all service or + // transport construction. Release builds have no initializer capable of enabling it. + if chainAsset.chain.isTonCompatibilityChain, !tonSendReleasePolicy.isEnabled { + throw UniversalWalletSendRoutingError.tonProductionSendDisabled + } + guard let accountResponse = wallet.fetch(for: chainAsset.chain.accountRequest()) else { throw ChainAccountFetchingError.accountNotExists } - if let dependencies = cachedDependencies[chainAsset.uniqueKey(accountId: accountResponse.accountId)] { + let dependenciesKey = chainAsset.uniqueKey(accountId: accountResponse.accountId) + if let dependencies = cachedDependencies[dependenciesKey] { + if currentDependenciesKey != dependenciesKey { + currentDependecies?.transferService.unsubscribe() + currentDependecies = dependencies + currentDependenciesKey = dependenciesKey + } return dependencies } - currentDependecies?.transferService.unsubscribe() + if currentDependenciesKey != dependenciesKey { + currentDependecies?.transferService.unsubscribe() + } let chainRegistry: ChainRegistryProtocol = ChainRegistryFacade.sharedRegistry let runtimeService = chainRegistry.getRuntimeProvider( @@ -64,7 +147,7 @@ final class SendDepencyContainer { let equilibruimTotalBalanceService = createEqTotalBalanceService(chainAsset: chainAsset) - let transferService = try await createTransferService(for: chainAsset) + let transferService = try createTransferService(for: chainAsset) let polkaswapService = createPolkaswapService(chainAsset: chainAsset, chainRegistry: chainRegistry) let accountInfoFetching = createAccountInfoFetching(for: chainAsset) @@ -87,7 +170,9 @@ final class SendDepencyContainer { storageRequestPerformer: storageRequestPerformer ) + cachedDependencies[dependenciesKey] = dependencies currentDependecies = dependencies + currentDependenciesKey = dependenciesKey return dependencies } @@ -108,7 +193,7 @@ final class SendDepencyContainer { return substrateAccountInfoFetching } - private func createTransferService(for chainAsset: ChainAsset) async throws -> TransferServiceProtocol { + private func createTransferService(for chainAsset: ChainAsset) throws -> TransferServiceProtocol { guard let accountResponse = wallet.fetch(for: chainAsset.chain.accountRequest()) else { @@ -128,7 +213,61 @@ final class SendDepencyContainer { } if chainAsset.chain.isTonCompatibilityChain { - throw ConvenienceError(error: "TON transfer not yet supported.") + guard tonSendReleasePolicy.isEnabled else { + throw UniversalWalletSendRoutingError.tonProductionSendDisabled + } + let chainId = chainAsset.chain.chainId.lowercased() + guard !(chainAsset.chain.options ?? []).contains(.testnet), + chainId == TonChainSelection.mainnetChainId || + chainId == UniversalWalletRegistry.tonMainnetRegistryEntry.chainId || + chainId == UniversalWalletRegistry.tonMainnetRegistryEntry.id + else { + throw UniversalWalletSendRoutingError.unsupported(chainId: chainAsset.chain.chainId) + } + guard chainAsset.isNative, + chainAsset.asset.isNative, + chainAsset.asset.isUtility, + chainAsset.asset.id == UniversalWalletRegistry.tonNativeAssetId || + chainAsset.asset.id.uppercased() == "TON", + chainAsset.asset.symbol.uppercased() == "TON", + chainAsset.asset.precision == 9, + chainAsset.asset.type == nil || chainAsset.asset.type == .normal + else { + throw UniversalWalletSendRoutingError.unsupported( + chainId: "\(chainAsset.chain.chainId)/\(chainAsset.asset.id)" + ) + } + + let remote: TonTransferRemoteProtocol + if let tonRemote { + remote = tonRemote + } else { + let chainRegistry = ChainRegistryFacade.sharedRegistry as! ChainRegistry + let factory = try chainRegistry.getTonApiClientFactory(for: chainAsset.chain) + guard factory.serverURL == ChainRegistry.resolveTonNode(for: chainAsset.chain)?.url else { + throw UniversalWalletSendRoutingError.unsupported(chainId: chainAsset.chain.chainId) + } + remote = TonAPIRemoteClient(factory: factory) + } + + #if DEBUG + if let tonPendingCoordinator { + return TonTransferService( + wallet: wallet, + chain: chainAsset.chain, + remote: remote, + mnemonicProvider: tonMnemonicProvider, + pendingCoordinator: tonPendingCoordinator + ) + } + #endif + + return TonTransferService( + wallet: wallet, + chain: chainAsset.chain, + remote: remote, + mnemonicProvider: tonMnemonicProvider + ) } if chainAsset.chain.chainBaseType == .substrate { @@ -177,7 +316,7 @@ final class SendDepencyContainer { ) } - throw ConvenienceError(error: "TON transfer not yet supported.") + throw UniversalWalletSendRoutingError.unsupported(chainId: chainAsset.chain.chainId) } private func isUniversalWalletBitcoin(_ chain: ChainModel) -> Bool { diff --git a/fearless/Modules/Staking/Analytics/Validators/ViewModel/AnalyticsValidatorsViewModelFactory.swift b/fearless/Modules/Staking/Analytics/Validators/ViewModel/AnalyticsValidatorsViewModelFactory.swift index 07d7074948..02d5c90c56 100644 --- a/fearless/Modules/Staking/Analytics/Validators/ViewModel/AnalyticsValidatorsViewModelFactory.swift +++ b/fearless/Modules/Staking/Analytics/Validators/ViewModel/AnalyticsValidatorsViewModelFactory.swift @@ -4,8 +4,21 @@ import BigInt import IrohaCrypto import SSFModels -// swiftlint:disable function_body_length +// swiftlint:disable:next type_body_length final class AnalyticsValidatorsViewModelFactory: AnalyticsValidatorsViewModelFactoryProtocol { + private struct RewardAllocation { + let amountsByValidator: [AccountAddress: Decimal] + let totalAmount: Decimal + + static let empty = RewardAllocation(amountsByValidator: [:], totalAmount: .zero) + } + + private struct ValidatorProgress { + let percents: Double + let amount: Double + let text: String + } + private let iconGenerator: IconGenerating private let balanceViewModelFactory: BalanceViewModelFactoryProtocol private let chain: ChainModel @@ -24,6 +37,7 @@ final class AnalyticsValidatorsViewModelFactory: AnalyticsValidatorsViewModelFac self.iconGenerator = iconGenerator } + // swiftlint:disable:next function_body_length function_parameter_count func createViewModel( eraValidatorInfos: [SubqueryEraValidatorInfo], eraRange: EraRange, @@ -36,29 +50,34 @@ final class AnalyticsValidatorsViewModelFactory: AnalyticsValidatorsViewModelFac ) -> AnalyticsValidatorsViewModel { percentFormatter.locale = locale - let totalEras: Int = { - if eraRange.end + 1 >= eraRange.start { - // totalEras == 0 is unacceptable for later calculations and UX, - // so condider totalEras to be at least 1 - return max(Int(eraRange.end + 1 - eraRange.start), 1) - } else { - return 1 - } - }() + let totalEras = totalEras(in: eraRange) + let validEraValidatorInfos = eraValidatorInfos.filter { + $0.era >= eraRange.start && $0.era <= eraRange.end + } + let erasWhenStaked = countErasWhenStaked(eraValidatorInfos: validEraValidatorInfos) - let erasWhenStaked = countErasWhenStaked(eraValidatorInfos: eraValidatorInfos) - let totalRewards = totalRewardOfStash(address: stashAddress, rewards: rewards) + var seenValidatorAddresses = Set() let validatorsAddresses = nomination.targets.compactMap { accountId in - try? AddressFactory.address(for: accountId, chain: chain) + guard accountId.count == chain.accountIdLenght else { + return nil + } + return try? AddressFactory.address(for: accountId, chain: chain) + }.filter { address in + seenValidatorAddresses.insert(address).inserted } + let rewardAllocation = createRewardAllocation( + validatorAddresses: validatorsAddresses, + stashAddress: stashAddress, + rewards: rewards + ) let validatorsViewModel: [AnalyticsValidatorItemViewModel] = validatorsAddresses.map { address in let icon = try? iconGenerator.generateFromAddress(address) let validatorName = (identitiesByAddress?[address]?.displayName) ?? address - let (progressPercents, amount, progressText): (Double, Double, String) = { + let progress: ValidatorProgress = { switch page { case .activity: - let infos = eraValidatorInfos.filter { $0.address == address } + let infos = validEraValidatorInfos.filter { $0.address == address } let distinctEras = Set(infos.map(\.era)) let distinctErasCount = distinctEras.count @@ -68,23 +87,25 @@ final class AnalyticsValidatorsViewModelFactory: AnalyticsValidatorsViewModelFac erasCount: distinctErasCount, locale: locale ) - return (percents, Double(distinctErasCount), text) + return ValidatorProgress( + percents: percents, + amount: Double(distinctErasCount), + text: text + ) case .rewards: - let rewardsOfValidator = rewards.filter { reward in - reward.stashAddress == stashAddress && reward.validatorAddress == address - } - let totalAmount = rewardsOfValidator.reduce(Decimal(0)) { amount, info in - let decimal = Decimal.fromSubstrateAmount( - info.amount, - precision: Int16(asset.precision) - ) - return amount + (decimal ?? 0.0) - } + let totalAmount = rewardAllocation.amountsByValidator[address] ?? .zero let totalAmountText = balanceViewModelFactory .amountFromValue(totalAmount, usageCase: .listCrypto).value(for: locale) - let amountDouble = NSDecimalNumber(decimal: totalAmount).doubleValue - let percents = amountDouble / totalRewards - return (percents, amountDouble, totalAmountText) + let amountDouble = finiteDouble(from: totalAmount) + let percents = rewardShare( + amount: totalAmount, + totalAmount: rewardAllocation.totalAmount + ) + return ValidatorProgress( + percents: percents, + amount: amountDouble, + text: totalAmountText + ) } }() @@ -93,31 +114,31 @@ final class AnalyticsValidatorsViewModelFactory: AnalyticsValidatorsViewModelFac case .activity: return R.string.localizable .stakingAnalyticsValidatorsErasCounter( - format: Int(amount), + format: Int(progress.amount), preferredLanguages: locale.rLanguages ) case .rewards: - return percentFormatter.string(from: progressPercents as NSNumber) ?? "" + return percentFormatter.string(from: progress.percents as NSNumber) ?? "" } }() let mainValueText: String = { switch page { case .activity: - return percentFormatter.string(from: progressPercents as NSNumber) ?? "" + return percentFormatter.string(from: progress.percents as NSNumber) ?? "" case .rewards: - return progressText + return progress.text } }() return .init( icon: icon, validatorName: validatorName, - amount: amount, - progressPercents: progressPercents, + amount: progress.amount, + progressPercents: progress.percents, mainValueText: mainValueText, secondaryValueText: secondaryValueText, - progressFullDescription: progressText, + progressFullDescription: progress.text, validatorAddress: address ) } @@ -126,9 +147,9 @@ final class AnalyticsValidatorsViewModelFactory: AnalyticsValidatorsViewModelFac let listTitle = determineListTitle(page: page, locale: locale) let chartCenterText = createChartCenterText( page: page, - validators: validatorsViewModel, totalEras: totalEras, erasWhenStaked: erasWhenStaked, + totalRewards: rewardAllocation.totalAmount, locale: locale ) @@ -167,9 +188,9 @@ final class AnalyticsValidatorsViewModelFactory: AnalyticsValidatorsViewModelFac private func createChartCenterText( page: AnalyticsValidatorsPage, - validators: [AnalyticsValidatorItemViewModel], totalEras: Int, erasWhenStaked: Int, + totalRewards: Decimal, locale: Locale ) -> NSAttributedString { switch page { @@ -190,13 +211,16 @@ final class AnalyticsValidatorsViewModelFactory: AnalyticsValidatorsViewModelFac ) ) case .rewards: - let totalRewards = validators.map(\.amount).reduce(0.0, +) - let totalRewardsText = balanceViewModelFactory.amountFromValue(Decimal(totalRewards), usageCase: .listCrypto) + let totalRewardsText = balanceViewModelFactory.amountFromValue(totalRewards, usageCase: .listCrypto) .value(for: locale) + let totalPercentage = totalRewards > .zero ? 1.0 : 0.0 + let totalPercentageText = percentFormatter.string(from: totalPercentage as NSNumber) ?? "" + return createChartCenterText( - firstLine: "TODO in next release", + firstLine: R.string.localizable + .stakingAnalyticsReceivedRewards(preferredLanguages: locale.rLanguages), secondLine: totalRewardsText, - thirdLine: "100%" + thirdLine: totalPercentageText ) } } @@ -250,21 +274,6 @@ final class AnalyticsValidatorsViewModelFactory: AnalyticsValidatorsViewModelFac return distinctEras.count } - func totalRewardOfStash( - address: AccountAddress, - rewards: [SubqueryRewardItemData] - ) -> Double { - let rewardsOfStash = rewards.filter { $0.stashAddress == address && $0.isReward } - let totalAmount = rewardsOfStash.reduce(Decimal(0)) { amount, info in - let decimal = Decimal.fromSubstrateAmount( - info.amount, - precision: Int16(asset.precision) - ) - return amount + (decimal ?? 0.0) - } - return NSDecimalNumber(decimal: totalAmount).doubleValue - } - private func findInactiveSegment( page: AnalyticsValidatorsPage, totalEras: Int, @@ -308,3 +317,94 @@ final class AnalyticsValidatorsViewModelFactory: AnalyticsValidatorsViewModelFac ) } } + +private extension AnalyticsValidatorsViewModelFactory { + func totalEras(in eraRange: EraRange) -> Int { + guard eraRange.end >= eraRange.start else { + return 1 + } + + return Int(UInt64(eraRange.end) - UInt64(eraRange.start) + 1) + } + + private func createRewardAllocation( + validatorAddresses: [AccountAddress], + stashAddress: AccountAddress, + rewards: [SubqueryRewardItemData] + ) -> RewardAllocation { + guard + let precision = Int16(exactly: asset.precision), + !validatorAddresses.isEmpty + else { + return .empty + } + + let validatorAddressSet = Set(validatorAddresses) + var seenEventIds = Set() + var substrateAmountsByValidator = [AccountAddress: BigUInt]() + + for reward in rewards { + let eventId = reward.eventId.trimmingCharacters(in: .whitespacesAndNewlines) + guard + reward.isReward, + reward.stashAddress == stashAddress, + validatorAddressSet.contains(reward.validatorAddress), + !eventId.isEmpty, + seenEventIds.insert(eventId).inserted + else { + continue + } + + substrateAmountsByValidator[reward.validatorAddress, default: .zero] += reward.amount + } + + let totalSubstrateAmount = substrateAmountsByValidator.values.reduce(BigUInt.zero, +) + guard + let totalAmount = Decimal.fromSubstrateAmount(totalSubstrateAmount, precision: precision), + isValidAmount(totalAmount) + else { + return .empty + } + + var amountsByValidator = [AccountAddress: Decimal]() + for (address, amount) in substrateAmountsByValidator { + guard + let decimalAmount = Decimal.fromSubstrateAmount(amount, precision: precision), + isValidAmount(decimalAmount) + else { + return .empty + } + amountsByValidator[address] = decimalAmount + } + + return RewardAllocation(amountsByValidator: amountsByValidator, totalAmount: totalAmount) + } + + func finiteDouble(from amount: Decimal) -> Double { + let value = NSDecimalNumber(decimal: amount).doubleValue + return value.isFinite && value >= 0.0 ? value : 0.0 + } + + func isValidAmount(_ amount: Decimal) -> Bool { + let value = NSDecimalNumber(decimal: amount).doubleValue + return value.isFinite && value >= 0.0 + } + + func rewardShare(amount: Decimal, totalAmount: Decimal) -> Double { + guard totalAmount > .zero else { + return 0.0 + } + + let value = NSDecimalNumber(decimal: amount) + .dividing(by: NSDecimalNumber(decimal: totalAmount)) + .doubleValue + + guard value.isFinite else { + return 0.0 + } + + return min(max(value, 0.0), 1.0) + } + + // swiftlint:disable:next file_length +} diff --git a/fearless/Modules/Staking/ControllerAccount/ControllerAccountViewFactory.swift b/fearless/Modules/Staking/ControllerAccount/ControllerAccountViewFactory.swift index d7cd711892..e2b890c520 100644 --- a/fearless/Modules/Staking/ControllerAccount/ControllerAccountViewFactory.swift +++ b/fearless/Modules/Staking/ControllerAccount/ControllerAccountViewFactory.swift @@ -100,15 +100,7 @@ struct ControllerAccountViewFactory { operationManager: operationManager ) - let facade = UserDataStorageFacade.shared - - let mapper = MetaAccountMapper() - - let accountRepository: CoreDataRepository = facade.createRepository( - filter: nil, - sortDescriptors: [], - mapper: AnyCoreDataMapper(mapper) - ) + let accountRepository = AccountRepositoryFactory.createRepository() let callFactory = SubstrateCallFactoryDefault(runtimeService: runtimeService) @@ -119,7 +111,7 @@ struct ControllerAccountViewFactory { ), stakingLocalSubscriptionFactory: stakingLocalSubscriptionFactory, runtimeService: runtimeService, - accountRepository: AnyDataProviderRepository(accountRepository), + accountRepository: accountRepository, operationManager: operationManager, feeProxy: feeProxy, extrinsicService: extrinsicService, diff --git a/fearless/Modules/Staking/ControllerAccountConfirmation/ControllerAccountConfirmationViewFactory.swift b/fearless/Modules/Staking/ControllerAccountConfirmation/ControllerAccountConfirmationViewFactory.swift index f8c9afb614..0f303bd2e0 100644 --- a/fearless/Modules/Staking/ControllerAccountConfirmation/ControllerAccountConfirmationViewFactory.swift +++ b/fearless/Modules/Staking/ControllerAccountConfirmation/ControllerAccountConfirmationViewFactory.swift @@ -101,20 +101,12 @@ struct ControllerAccountConfirmationViewFactory { let feeProxy = ExtrinsicFeeProxy() - let facade = UserDataStorageFacade.shared - let storageRequestFactory = StorageRequestFactory( remoteFactory: StorageKeyFactory(), operationManager: operationManager ) - let mapper = MetaAccountMapper() - - let accountRepository: CoreDataRepository = facade.createRepository( - filter: nil, - sortDescriptors: [], - mapper: AnyCoreDataMapper(mapper) - ) + let accountRepository = AccountRepositoryFactory.createRepository() let callFactory = SubstrateCallFactoryDefault(runtimeService: runtimeService) @@ -129,7 +121,7 @@ struct ControllerAccountConfirmationViewFactory { signingWrapper: signingWrapper, feeProxy: feeProxy, controllerAccountItem: controllerAccountItem, - accountRepository: AnyDataProviderRepository(accountRepository), + accountRepository: accountRepository, operationManager: operationManager, storageRequestFactory: storageRequestFactory, engine: connection, diff --git a/fearless/Modules/Staking/SelectValidatorsFlow/YourValidatorList/YourValidatorListViewFactory.swift b/fearless/Modules/Staking/SelectValidatorsFlow/YourValidatorList/YourValidatorListViewFactory.swift index c7f91220e1..e2d50fbb0e 100644 --- a/fearless/Modules/Staking/SelectValidatorsFlow/YourValidatorList/YourValidatorListViewFactory.swift +++ b/fearless/Modules/Staking/SelectValidatorsFlow/YourValidatorList/YourValidatorListViewFactory.swift @@ -139,15 +139,7 @@ struct YourValidatorListViewFactory { logger: Logger.shared ) - let facade = UserDataStorageFacade.shared - - let mapper = MetaAccountMapper() - - let accountRepository: CoreDataRepository = facade.createRepository( - filter: nil, - sortDescriptors: [], - mapper: AnyCoreDataMapper(mapper) - ) + let accountRepository = AccountRepositoryFactory.createRepository() let balanceViewModelFactory = BalanceViewModelFactory( targetAssetInfo: chainAsset.asset.displayInfo, @@ -167,7 +159,7 @@ struct YourValidatorListViewFactory { validatorOperationFactory: validatorOperationFactory, operationManager: OperationManagerFacade.sharedManager, stakingLocalSubscriptionFactory: stakingLocalSubscriptionFactory, - accountRepository: AnyDataProviderRepository(accountRepository), + accountRepository: accountRepository, output: viewModelState ) let viewModelFactory = YourValidatorListRelaychainViewModelFactory( diff --git a/fearless/Modules/Staking/StakingAmount/StakingAmountViewFactory.swift b/fearless/Modules/Staking/StakingAmount/StakingAmountViewFactory.swift index dc1cb547ae..4971a66c86 100644 --- a/fearless/Modules/Staking/StakingAmount/StakingAmountViewFactory.swift +++ b/fearless/Modules/Staking/StakingAmount/StakingAmountViewFactory.swift @@ -330,14 +330,7 @@ final class StakingAmountViewFactory: StakingAmountViewFactoryProtocol { } let operationManager = OperationManagerFacade.sharedManager - let facade = UserDataStorageFacade.shared - let mapper = MetaAccountMapper() - - let accountRepository: CoreDataRepository = facade.createRepository( - filter: nil, - sortDescriptors: [], - mapper: AnyCoreDataMapper(mapper) - ) + let accountRepository = AccountRepositoryFactory.createRepository() return StakingAmountInteractor( accountInfoSubscriptionAdapter: AccountInfoSubscriptionAdapter( @@ -349,7 +342,7 @@ final class StakingAmountViewFactory: StakingAmountViewFactoryProtocol { operationManager: operationManager, chainAsset: chainAsset, wallet: selectedAccount, - accountRepository: AnyDataProviderRepository(accountRepository), + accountRepository: accountRepository, strategy: strategy ) } diff --git a/fearless/Modules/Staking/StakingBalance/StakingBalanceViewFactory.swift b/fearless/Modules/Staking/StakingBalance/StakingBalanceViewFactory.swift index 56bbb97efb..ea0d9239fc 100644 --- a/fearless/Modules/Staking/StakingBalance/StakingBalanceViewFactory.swift +++ b/fearless/Modules/Staking/StakingBalance/StakingBalanceViewFactory.swift @@ -106,15 +106,7 @@ struct StakingBalanceViewFactory { operationManager: operationManager ) - let facade = UserDataStorageFacade.shared - - let mapper = MetaAccountMapper() - - let accountRepository: CoreDataRepository = facade.createRepository( - filter: nil, - sortDescriptors: [], - mapper: AnyCoreDataMapper(mapper) - ) + let accountRepository = AccountRepositoryFactory.createRepository() let eraCountdownOperationFactory = EraCountdownOperationFactory( storageRequestFactory: storageRequestFactory @@ -146,7 +138,7 @@ struct StakingBalanceViewFactory { operationManager: operationManager, eraCountdownOperationFactory: eraCountdownOperationFactory, connection: connection, - accountRepository: AnyDataProviderRepository(accountRepository), + accountRepository: accountRepository, chainAsset: chainAsset, wallet: wallet ) diff --git a/fearless/Modules/Staking/StakingBondMore/StakingBondMoreViewFactory.swift b/fearless/Modules/Staking/StakingBondMore/StakingBondMoreViewFactory.swift index c7e482be50..845903c40f 100644 --- a/fearless/Modules/Staking/StakingBondMore/StakingBondMoreViewFactory.swift +++ b/fearless/Modules/Staking/StakingBondMore/StakingBondMoreViewFactory.swift @@ -119,15 +119,7 @@ struct StakingBondMoreViewFactory { logger: Logger.shared ) - let facade = UserDataStorageFacade.shared - - let mapper = MetaAccountMapper() - - let accountRepository: CoreDataRepository = facade.createRepository( - filter: nil, - sortDescriptors: [], - mapper: AnyCoreDataMapper(mapper) - ) + let accountRepository = AccountRepositoryFactory.createRepository() let accountInfoSubscriptionAdapter = AccountInfoSubscriptionAdapter( walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, @@ -150,7 +142,7 @@ struct StakingBondMoreViewFactory { output: viewModelState, chainAsset: chainAsset, wallet: wallet, - accountRepository: AnyDataProviderRepository(accountRepository), + accountRepository: accountRepository, connection: connection, extrinsicService: extrinsicService, feeProxy: feeProxy, diff --git a/fearless/Modules/Staking/StakingBondMoreConfirmation/StakingBondMoreConfirmationViewFactory.swift b/fearless/Modules/Staking/StakingBondMoreConfirmation/StakingBondMoreConfirmationViewFactory.swift index 64423649ee..15916eb452 100644 --- a/fearless/Modules/Staking/StakingBondMoreConfirmation/StakingBondMoreConfirmationViewFactory.swift +++ b/fearless/Modules/Staking/StakingBondMoreConfirmation/StakingBondMoreConfirmationViewFactory.swift @@ -168,15 +168,7 @@ struct StakingBondMoreConfirmViewFactory { accountResponse: accountResponse ) - let facade = UserDataStorageFacade.shared - - let mapper = MetaAccountMapper() - - let accountRepository: CoreDataRepository = facade.createRepository( - filter: nil, - sortDescriptors: [], - mapper: AnyCoreDataMapper(mapper) - ) + let accountRepository = AccountRepositoryFactory.createRepository() let accountInfoSubscriptionAdapter = AccountInfoSubscriptionAdapter( walletLocalSubscriptionFactory: walletLocalSubscriptionFactory, @@ -203,7 +195,7 @@ struct StakingBondMoreConfirmViewFactory { feeProxy: feeProxy, runtimeService: runtimeService, operationManager: operationManager, - accountRepository: AnyDataProviderRepository(accountRepository), + accountRepository: accountRepository, connection: connection, keystore: keystore, signingWrapper: signingWrapper, diff --git a/fearless/Modules/Staking/StakingPayoutConfirmation/StakingPayoutConfirmationViewFactory.swift b/fearless/Modules/Staking/StakingPayoutConfirmation/StakingPayoutConfirmationViewFactory.swift index 9ae8b29dee..c6918d0089 100644 --- a/fearless/Modules/Staking/StakingPayoutConfirmation/StakingPayoutConfirmationViewFactory.swift +++ b/fearless/Modules/Staking/StakingPayoutConfirmation/StakingPayoutConfirmationViewFactory.swift @@ -115,15 +115,7 @@ final class StakingPayoutConfirmationViewFactory: StakingPayoutConfirmationViewF accountResponse: accountResponse ) - let facade = UserDataStorageFacade.shared - - let mapper = MetaAccountMapper() - - let accountRepository: CoreDataRepository = facade.createRepository( - filter: nil, - sortDescriptors: [], - mapper: AnyCoreDataMapper(mapper) - ) + let accountRepository = AccountRepositoryFactory.createRepository() let accountInfoSubscriptionAdapter = AccountInfoSubscriptionAdapter( walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, @@ -168,7 +160,7 @@ final class StakingPayoutConfirmationViewFactory: StakingPayoutConfirmationViewF wallet: wallet, payouts: payouts, chainAsset: chainAsset, - accountRepository: AnyDataProviderRepository(accountRepository), + accountRepository: accountRepository, output: viewModelState, callFactory: callFactory ) diff --git a/fearless/Modules/Staking/StakingRebondConfirmation/StakingRebondConfirmationViewFactory.swift b/fearless/Modules/Staking/StakingRebondConfirmation/StakingRebondConfirmationViewFactory.swift index 858ea62617..5dc749eb41 100644 --- a/fearless/Modules/Staking/StakingRebondConfirmation/StakingRebondConfirmationViewFactory.swift +++ b/fearless/Modules/Staking/StakingRebondConfirmation/StakingRebondConfirmationViewFactory.swift @@ -128,8 +128,6 @@ struct StakingRebondConfirmationViewFactory { let keystore = Keychain() let feeProxy = ExtrinsicFeeProxy() - let facade = UserDataStorageFacade.shared - let mapper = MetaAccountMapper() let balanceViewModelFactory = BalanceViewModelFactory( targetAssetInfo: chainAsset.asset.displayInfo, @@ -137,11 +135,7 @@ struct StakingRebondConfirmationViewFactory { selectedMetaAccount: wallet ) - let accountRepository: CoreDataRepository = facade.createRepository( - filter: nil, - sortDescriptors: [], - mapper: AnyCoreDataMapper(mapper) - ) + let accountRepository = AccountRepositoryFactory.createRepository() let accountInfoSubscriptionAdapter = AccountInfoSubscriptionAdapter( walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, @@ -182,7 +176,7 @@ struct StakingRebondConfirmationViewFactory { operationManager: operationManager, keystore: keystore, connection: connection, - accountRepository: AnyDataProviderRepository(accountRepository), + accountRepository: accountRepository, output: viewModelState, callFactory: callFactory ) @@ -218,7 +212,7 @@ struct StakingRebondConfirmationViewFactory { operationManager: operationManager, keystore: keystore, connection: connection, - accountRepository: AnyDataProviderRepository(accountRepository), + accountRepository: accountRepository, output: viewModelState, signingWrapper: signingWrapper ) diff --git a/fearless/Modules/Staking/StakingRebondSetup/StakingRebondSetupViewFactory.swift b/fearless/Modules/Staking/StakingRebondSetup/StakingRebondSetupViewFactory.swift index 4a360a7f3e..21b6876017 100644 --- a/fearless/Modules/Staking/StakingRebondSetup/StakingRebondSetupViewFactory.swift +++ b/fearless/Modules/Staking/StakingRebondSetup/StakingRebondSetupViewFactory.swift @@ -106,15 +106,7 @@ final class StakingRebondSetupViewFactory: StakingRebondSetupViewFactoryProtocol let feeProxy = ExtrinsicFeeProxy() - let facade = UserDataStorageFacade.shared - - let mapper = MetaAccountMapper() - - let accountRepository: CoreDataRepository = facade.createRepository( - filter: nil, - sortDescriptors: [], - mapper: AnyCoreDataMapper(mapper) - ) + let accountRepository = AccountRepositoryFactory.createRepository() let callFactory = SubstrateCallFactoryDefault(runtimeService: runtimeService) @@ -131,7 +123,7 @@ final class StakingRebondSetupViewFactory: StakingRebondSetupViewFactoryProtocol selectedAccount: selectedAccount, connection: connection, extrinsicService: extrinsicService, - accountRepository: AnyDataProviderRepository(accountRepository), + accountRepository: accountRepository, callFactory: callFactory ) } diff --git a/fearless/Modules/Staking/StakingRedeemConfirmation/StakingRedeemConfirmationViewFactory.swift b/fearless/Modules/Staking/StakingRedeemConfirmation/StakingRedeemConfirmationViewFactory.swift index f49aaf30db..f8c3d4ccd3 100644 --- a/fearless/Modules/Staking/StakingRedeemConfirmation/StakingRedeemConfirmationViewFactory.swift +++ b/fearless/Modules/Staking/StakingRedeemConfirmation/StakingRedeemConfirmationViewFactory.swift @@ -138,15 +138,7 @@ final class StakingRedeemConfirmationViewFactory: StakingRedeemConfirmationViewF storageRequestFactory: storageOperationFactory ) - let facade = UserDataStorageFacade.shared - - let mapper = MetaAccountMapper() - - let accountRepository: CoreDataRepository = facade.createRepository( - filter: nil, - sortDescriptors: [], - mapper: AnyCoreDataMapper(mapper) - ) + let accountRepository = AccountRepositoryFactory.createRepository() let accountInfoSubscriptionAdapter = AccountInfoSubscriptionAdapter( walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, @@ -182,7 +174,7 @@ final class StakingRedeemConfirmationViewFactory: StakingRedeemConfirmationViewF engine: connection, operationManager: operationManager, keystore: Keychain(), - accountRepository: AnyDataProviderRepository(accountRepository) + accountRepository: accountRepository ) let viewModelFactory = StakingRedeemConfirmationRelaychainViewModelFactory( asset: chainAsset.asset, diff --git a/fearless/Modules/Staking/StakingRewardDestConfirm/StakingRewardDestConfirmViewFactory.swift b/fearless/Modules/Staking/StakingRewardDestConfirm/StakingRewardDestConfirmViewFactory.swift index 415b2e3821..c20ed89768 100644 --- a/fearless/Modules/Staking/StakingRewardDestConfirm/StakingRewardDestConfirmViewFactory.swift +++ b/fearless/Modules/Staking/StakingRewardDestConfirm/StakingRewardDestConfirmViewFactory.swift @@ -107,15 +107,7 @@ struct StakingRewardDestConfirmViewFactory { accountResponse: accountResponse ) - let facade = UserDataStorageFacade.shared - - let mapper = MetaAccountMapper() - - let accountRepository: CoreDataRepository = facade.createRepository( - filter: nil, - sortDescriptors: [], - mapper: AnyCoreDataMapper(mapper) - ) + let accountRepository = AccountRepositoryFactory.createRepository() let callFactory = SubstrateCallFactoryDefault(runtimeService: runtimeService) @@ -135,7 +127,7 @@ struct StakingRewardDestConfirmViewFactory { signingWrapper: signingWrapper, connection: connection, keystore: keystore, - accountRepository: AnyDataProviderRepository(accountRepository), + accountRepository: accountRepository, callFactory: callFactory ) } diff --git a/fearless/Modules/Staking/StakingRewardDestinationSetup/StakingRewardDestSetupViewFactory.swift b/fearless/Modules/Staking/StakingRewardDestinationSetup/StakingRewardDestSetupViewFactory.swift index be7e7a367c..c67710840f 100644 --- a/fearless/Modules/Staking/StakingRewardDestinationSetup/StakingRewardDestSetupViewFactory.swift +++ b/fearless/Modules/Staking/StakingRewardDestinationSetup/StakingRewardDestSetupViewFactory.swift @@ -105,14 +105,7 @@ struct StakingRewardDestSetupViewFactory { logger: Logger.shared ) - let facade = UserDataStorageFacade.shared - let mapper = MetaAccountMapper() - - let accountRepository: CoreDataRepository = facade.createRepository( - filter: nil, - sortDescriptors: [], - mapper: AnyCoreDataMapper(mapper) - ) + let accountRepository = AccountRepositoryFactory.createRepository() let stakingSettings = StakingAssetSettings( storageFacade: substrateStorageFacade, @@ -162,7 +155,7 @@ struct StakingRewardDestSetupViewFactory { let callFactory = SubstrateCallFactoryDefault(runtimeService: runtimeService) return StakingRewardDestSetupInteractor( - accountRepository: AnyDataProviderRepository(accountRepository), + accountRepository: accountRepository, stakingLocalSubscriptionFactory: stakingLocalSubscriptionFactory, accountInfoSubscriptionAdapter: AccountInfoSubscriptionAdapter( walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, diff --git a/fearless/Modules/Staking/StakingUnbondConfirm/StakingUnbondConfirmViewFactory.swift b/fearless/Modules/Staking/StakingUnbondConfirm/StakingUnbondConfirmViewFactory.swift index 9046553f42..d820a77306 100644 --- a/fearless/Modules/Staking/StakingUnbondConfirm/StakingUnbondConfirmViewFactory.swift +++ b/fearless/Modules/Staking/StakingUnbondConfirm/StakingUnbondConfirmViewFactory.swift @@ -130,14 +130,7 @@ struct StakingUnbondConfirmViewFactory: StakingUnbondConfirmViewFactoryProtocol ) let feeProxy = ExtrinsicFeeProxy() - let facade = UserDataStorageFacade.shared - let mapper = MetaAccountMapper() - - let accountRepository: CoreDataRepository = facade.createRepository( - filter: nil, - sortDescriptors: [], - mapper: AnyCoreDataMapper(mapper) - ) + let accountRepository = AccountRepositoryFactory.createRepository() let accountInfoSubscriptionAdapter = AccountInfoSubscriptionAdapter( walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, selectedMetaAccount: wallet @@ -172,7 +165,7 @@ struct StakingUnbondConfirmViewFactory: StakingUnbondConfirmViewFactoryProtocol wallet: wallet, connection: connection, keystore: Keychain(), - accountRepository: AnyDataProviderRepository(accountRepository), + accountRepository: accountRepository, callFactory: callFactory ) let viewModelFactory = StakingUnbondConfirmRelaychainViewModelFactory( diff --git a/fearless/Modules/Staking/StakingUnbondSetup/StakingUnbondSetupViewFactory.swift b/fearless/Modules/Staking/StakingUnbondSetup/StakingUnbondSetupViewFactory.swift index bdd7f00d51..231accc864 100644 --- a/fearless/Modules/Staking/StakingUnbondSetup/StakingUnbondSetupViewFactory.swift +++ b/fearless/Modules/Staking/StakingUnbondSetup/StakingUnbondSetupViewFactory.swift @@ -109,14 +109,7 @@ struct StakingUnbondSetupViewFactory: StakingUnbondSetupViewFactoryProtocol { ) let feeProxy = ExtrinsicFeeProxy() - let facade = UserDataStorageFacade.shared - let mapper = MetaAccountMapper() - - let accountRepository: CoreDataRepository = facade.createRepository( - filter: nil, - sortDescriptors: [], - mapper: AnyCoreDataMapper(mapper) - ) + let accountRepository = AccountRepositoryFactory.createRepository() let accountInfoSubscriptionAdapter = AccountInfoSubscriptionAdapter( walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, @@ -148,7 +141,7 @@ struct StakingUnbondSetupViewFactory: StakingUnbondSetupViewFactoryProtocol { wallet: wallet, chainAsset: chainAsset, connection: connection, - accountRepository: AnyDataProviderRepository(accountRepository), + accountRepository: accountRepository, output: viewModelState, extrinsicService: extrinsicService, callFactory: callFactory diff --git a/fearless/Modules/StakingPool/PoolRolesConfirm/PoolRolesConfirmAssembly.swift b/fearless/Modules/StakingPool/PoolRolesConfirm/PoolRolesConfirmAssembly.swift index 4c3f1a69c5..0e5a4355c1 100644 --- a/fearless/Modules/StakingPool/PoolRolesConfirm/PoolRolesConfirmAssembly.swift +++ b/fearless/Modules/StakingPool/PoolRolesConfirm/PoolRolesConfirmAssembly.swift @@ -45,15 +45,7 @@ final class PoolRolesConfirmAssembly { accountResponse: accountResponse ) - let facade = UserDataStorageFacade.shared - - let mapper = MetaAccountMapper() - - let accountRepository: CoreDataRepository = facade.createRepository( - filter: nil, - sortDescriptors: [], - mapper: AnyCoreDataMapper(mapper) - ) + let accountRepository = AccountRepositoryFactory.createRepository() let callFactory = SubstrateCallFactoryDefault(runtimeService: runtimeService) @@ -64,7 +56,7 @@ final class PoolRolesConfirmAssembly { roles: roles, signingWrapper: signingWrapper, chainAsset: chainAsset, - accountRepository: AnyDataProviderRepository(accountRepository), + accountRepository: accountRepository, operationManager: OperationManagerFacade.sharedManager, callFactory: callFactory ) diff --git a/fearless/PrivacyInfo.xcprivacy b/fearless/PrivacyInfo.xcprivacy new file mode 100644 index 0000000000..0a95d0932c --- /dev/null +++ b/fearless/PrivacyInfo.xcprivacy @@ -0,0 +1,23 @@ + + + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPITypeReasons + + CA92.1 + + + + NSPrivacyCollectedDataTypes + + NSPrivacyTracking + + NSPrivacyTrackingDomains + + + diff --git a/fearless/en.lproj/Localizable.strings b/fearless/en.lproj/Localizable.strings index d8aadf0489..3dfefd13c8 100644 --- a/fearless/en.lproj/Localizable.strings +++ b/fearless/en.lproj/Localizable.strings @@ -1,6 +1,12 @@ "NSCameraUsageDescription" = "The camera is used to capture QR codes"; "NSFaceIDUsageDescription" = "Fearless uses Face ID to restrict unauthorized users from accessing the app. Face ID is used to authorize within the application"; "NSPhotoLibraryAddUsageDescription" = "Save transfer request as a QR code"; +"ton.transfer.unknown.title" = "Transfer status is unknown"; +"ton.transfer.unknown.message" = "The network may have accepted this transfer. Do not send it again. Save this message hash and check its status before taking further action:\n%@"; +"ton.transfer.unknown.copy_hash" = "Copy message hash"; +"ton.transfer.prior_confirmed.title" = "Previous transfer confirmed"; +"ton.transfer.prior_confirmed.message" = "A previous transfer was confirmed. This new transfer was not sent. Review and acknowledge the previous hash before confirming again:\n%@"; +"ton.transfer.prior_confirmed.acknowledge" = "Acknowledge and recalculate fee"; "NSPhotoLibraryUsageDescription" = "Load photos from library"; "about.announcement" = "Receive Announcements"; "about.ask.for.support" = "Ask for Support"; @@ -793,6 +799,7 @@ Terms and Conditions and Privacy Policy"; "staking.analytics.period.all" = "All"; "staking.analytics.rewards.empty.message" = "No rewards received\ for the selected period"; +"staking.analytics.received.rewards" = "Received rewards"; "staking.analytics.stake.allocation" = "Allocation with current validators"; "staking.analytics.stake.empty.message" = "Your stake changes\ will appear here"; diff --git a/fearless/id.lproj/Localizable.strings b/fearless/id.lproj/Localizable.strings index faf06dc9b5..f4f2e3c0f5 100644 --- a/fearless/id.lproj/Localizable.strings +++ b/fearless/id.lproj/Localizable.strings @@ -1,6 +1,12 @@ "NSCameraUsageDescription" = "Kamera digunakan untuk mengambil kode QR"; "NSFaceIDUsageDescription" = "Face ID digunakan untuk mengotorisasi dalam aplikasi"; "NSPhotoLibraryAddUsageDescription" = "Simpan permintaan transfer sebagai kode QR"; +"ton.transfer.unknown.title" = "Status transfer tidak diketahui"; +"ton.transfer.unknown.message" = "Jaringan mungkin telah menerima transfer ini. Jangan kirim ulang. Simpan hash pesan ini dan periksa statusnya sebelum mengambil tindakan lebih lanjut:\n%@"; +"ton.transfer.unknown.copy_hash" = "Salin hash pesan"; +"ton.transfer.prior_confirmed.title" = "Transfer sebelumnya telah dikonfirmasi"; +"ton.transfer.prior_confirmed.message" = "Transfer sebelumnya telah dikonfirmasi. Transfer baru ini belum dikirim. Tinjau dan akui hash sebelumnya sebelum mengonfirmasi lagi:\n%@"; +"ton.transfer.prior_confirmed.acknowledge" = "Akui dan hitung ulang biaya"; "NSPhotoLibraryUsageDescription" = "Memuat foto dari library"; "about.announcement" = "Terima pengumuman"; "about.ask.for.support" = "Tanya untuk bantuan"; @@ -785,6 +791,7 @@ Syarat dan Ketentuan serta Kebijakan Privasi"; "staking.analytics.period.7d" = "7 hari"; "staking.analytics.period.all" = "Semua"; "staking.analytics.rewards.empty.message" = "Tidak ada imbalan yang diterima untuk periode yang dipilih"; +"staking.analytics.received.rewards" = "Imbalan yang diterima"; "staking.analytics.stake.allocation" = "Alokasi dengan validator saat ini"; "staking.analytics.stake.empty.message" = "Taruhan Anda berubah akan muncul di sini"; "staking.analytics.staking.was.active" = "Staking aktif"; diff --git a/fearless/ja.lproj/Localizable.strings b/fearless/ja.lproj/Localizable.strings index 964bb6e5bd..466c66266d 100644 --- a/fearless/ja.lproj/Localizable.strings +++ b/fearless/ja.lproj/Localizable.strings @@ -1,6 +1,12 @@ "NSCameraUsageDescription" = "QRコードを撮るためにカメラを使用する"; "NSFaceIDUsageDescription" = "フィアレスは顔認証を使用して、権限のないユーザーがアプリにアクセスすることを制限します。顔認証は、アプリ内での認証に使用されます"; "NSPhotoLibraryAddUsageDescription" = "QRコードで取引申請を保存する"; +"ton.transfer.unknown.title" = "送金ステータスを確認できません"; +"ton.transfer.unknown.message" = "ネットワークがこの送金を受け付けた可能性があります。再送しないでください。このメッセージハッシュを保存し、次の操作を行う前にステータスを確認してください:\n%@"; +"ton.transfer.unknown.copy_hash" = "メッセージハッシュをコピー"; +"ton.transfer.prior_confirmed.title" = "以前の送金が確認されました"; +"ton.transfer.prior_confirmed.message" = "以前の送金が確認されました。この新しい送金は送信されていません。再度確認する前に、以前のハッシュを確認して承認してください:\n%@"; +"ton.transfer.prior_confirmed.acknowledge" = "承認して手数料を再計算"; "NSPhotoLibraryUsageDescription" = "ライブラリーから写真をアップロードする"; "about.announcement" = "お知らせを受け取る"; "about.ask.for.support" = "サポートを求める"; @@ -791,6 +797,7 @@ "staking.analytics.period.all" = "全て"; "staking.analytics.rewards.empty.message" = "選択した期間の\ 報酬は受け取っていません"; +"staking.analytics.received.rewards" = "受け取った報酬"; "staking.analytics.stake.allocation" = "現在のバリデーターによる割り当て"; "staking.analytics.stake.empty.message" = "ステークの変更がここに表示されます"; "staking.analytics.staking.was.active" = "ステーキングはアクティブでした"; diff --git a/fearless/pt-PT.lproj/Localizable.strings b/fearless/pt-PT.lproj/Localizable.strings index 3c30eb766c..9962f3938c 100644 --- a/fearless/pt-PT.lproj/Localizable.strings +++ b/fearless/pt-PT.lproj/Localizable.strings @@ -1,6 +1,12 @@ "NSCameraUsageDescription" = "A câmara é usada para capturar o código QR"; "NSFaceIDUsageDescription" = "Identificação facial usada para autorização na aplicação"; "NSPhotoLibraryAddUsageDescription" = "Guardar pedido de transferência como um código QR"; +"ton.transfer.unknown.title" = "O estado da transferência é desconhecido"; +"ton.transfer.unknown.message" = "A rede pode ter aceite esta transferência. Não a envie novamente. Guarde este hash da mensagem e verifique o estado antes de continuar:\n%@"; +"ton.transfer.unknown.copy_hash" = "Copiar hash da mensagem"; +"ton.transfer.prior_confirmed.title" = "Transferência anterior confirmada"; +"ton.transfer.prior_confirmed.message" = "Uma transferência anterior foi confirmada. Esta nova transferência não foi enviada. Reveja e confirme o hash anterior antes de voltar a confirmar:\n%@"; +"ton.transfer.prior_confirmed.acknowledge" = "Confirmar e recalcular a taxa"; "NSPhotoLibraryUsageDescription" = "Fazer upload de fotos da biblioteca"; "about.announcement" = "Receber Alertas"; "about.ask.for.support" = "Pedir Apoio"; @@ -750,6 +756,7 @@ Estes documentos são cruciais para uma experiência segura e positiva do utiliz "staking.analytics.period.all" = "Tudo"; "staking.analytics.rewards.empty.message" = "Sem recompensas recebidas\ para o período selecionado"; +"staking.analytics.received.rewards" = "Recompensas recebidas"; "staking.analytics.stake.allocation" = "Alocação com validadores atuais"; "staking.analytics.stake.empty.message" = "As suas alterações de stake\ aparecerão aqui"; diff --git a/fearless/pt.lproj/Localizable.strings b/fearless/pt.lproj/Localizable.strings index 41d5bd73f9..d8fec44765 100644 --- a/fearless/pt.lproj/Localizable.strings +++ b/fearless/pt.lproj/Localizable.strings @@ -1,6 +1,12 @@ "NSCameraUsageDescription" = "A câmara é usada para capturar o código QR"; "NSFaceIDUsageDescription" = "Identificação facial usada para autorização na aplicação"; "NSPhotoLibraryAddUsageDescription" = "Guardar pedido de transferência como um código QR"; +"ton.transfer.unknown.title" = "O estado da transferência é desconhecido"; +"ton.transfer.unknown.message" = "A rede pode ter aceitado esta transferência. Não a envie novamente. Guarde este hash da mensagem e verifique o estado antes de continuar:\n%@"; +"ton.transfer.unknown.copy_hash" = "Copiar hash da mensagem"; +"ton.transfer.prior_confirmed.title" = "Transferência anterior confirmada"; +"ton.transfer.prior_confirmed.message" = "Uma transferência anterior foi confirmada. Esta nova transferência não foi enviada. Revise e confirme o hash anterior antes de confirmar novamente:\n%@"; +"ton.transfer.prior_confirmed.acknowledge" = "Confirmar e recalcular a taxa"; "NSPhotoLibraryUsageDescription" = "Fazer upload de fotos da biblioteca"; "about.announcement" = "Receber Alertas"; "about.ask.for.support" = "Pedir Apoio"; @@ -804,6 +810,7 @@ Estes documentos são cruciais para uma experiência segura e positiva do utiliz "staking.analytics.period.all" = "Tudo"; "staking.analytics.rewards.empty.message" = "Sem recompensas recebidas\ para o período selecionado"; +"staking.analytics.received.rewards" = "Recompensas recebidas"; "staking.analytics.stake.allocation" = "Alocação com validadores atuais"; "staking.analytics.stake.empty.message" = "As suas alterações de stake\ aparecerão aqui"; @@ -1214,4 +1221,4 @@ pertence à rede certa"; "your.validators.change.validators.title" = "Alterar validadores"; "your.validators.stop.nominating.title" = "Deixar de nomear"; "your.validators.validator.total.stake" = "Total em stake: %@"; -"сurrencies.stub.text" = "Moedas"; \ No newline at end of file +"сurrencies.stub.text" = "Moedas"; diff --git a/fearless/ru.lproj/Localizable.strings b/fearless/ru.lproj/Localizable.strings index f1fe5dbf96..1680c9426d 100644 --- a/fearless/ru.lproj/Localizable.strings +++ b/fearless/ru.lproj/Localizable.strings @@ -1,6 +1,12 @@ "NSCameraUsageDescription" = "Камера используется для сканирования QR кода."; "NSFaceIDUsageDescription" = "Fearless использует Face ID, чтобы ограничить несанкционированных пользователей доступа к приложению.Face ID используется для авторизации в приложении"; "NSPhotoLibraryAddUsageDescription" = "Вы можете сохранить запрос на перевод в виде QR кода"; +"ton.transfer.unknown.title" = "Статус перевода неизвестен"; +"ton.transfer.unknown.message" = "Сеть могла принять этот перевод. Не отправляйте его повторно. Сохраните хеш сообщения и проверьте статус перед дальнейшими действиями:\n%@"; +"ton.transfer.unknown.copy_hash" = "Скопировать хеш сообщения"; +"ton.transfer.prior_confirmed.title" = "Предыдущий перевод подтверждён"; +"ton.transfer.prior_confirmed.message" = "Предыдущий перевод подтверждён. Новый перевод не был отправлен. Проверьте и подтвердите предыдущий хеш перед повторным подтверждением:\n%@"; +"ton.transfer.prior_confirmed.acknowledge" = "Подтвердить и пересчитать комиссию"; "NSPhotoLibraryUsageDescription" = "Для выбора существующих изображений QR-кодов необходим доступ к библиотеке"; "about.announcement" = "Следить за новостями в Telegram"; "about.ask.for.support" = "Обратитесь за поддержкой"; @@ -796,6 +802,7 @@ Euro cash"; "staking.analytics.period.all" = "все"; "staking.analytics.rewards.empty.message" = "Нет полученных вознаграждений\ в выбранный период"; +"staking.analytics.received.rewards" = "Полученные вознаграждения"; "staking.analytics.stake.allocation" = "Распределение с текущими валидаторами"; "staking.analytics.stake.empty.message" = "Здесь появятся ваши\ изменения по стейку"; @@ -1207,4 +1214,4 @@ Euro cash"; "your.validators.change.validators.title" = "Изменить валидаторов"; "your.validators.stop.nominating.title" = "Stop nominating"; "your.validators.validator.total.stake" = "Всего в стейкинге: %@"; -"сurrencies.stub.text" = "Токены"; \ No newline at end of file +"сurrencies.stub.text" = "Токены"; diff --git a/fearless/tr.lproj/Localizable.strings b/fearless/tr.lproj/Localizable.strings index 9b74a74474..12145cbd6c 100644 --- a/fearless/tr.lproj/Localizable.strings +++ b/fearless/tr.lproj/Localizable.strings @@ -1,6 +1,12 @@ "NSCameraUsageDescription" = "Kamera, QR kodunu yakalamak için kullanılır"; "NSFaceIDUsageDescription" = "Face ID uygulamada yetkilendirme için kullanılır"; "NSPhotoLibraryAddUsageDescription" = "Transfer isteğini QR Kodu olarak kaydet"; +"ton.transfer.unknown.title" = "Transfer durumu bilinmiyor"; +"ton.transfer.unknown.message" = "Ağ bu transferi kabul etmiş olabilir. Tekrar göndermeyin. Bu mesaj karmasını kaydedin ve başka bir işlem yapmadan önce durumunu kontrol edin:\n%@"; +"ton.transfer.unknown.copy_hash" = "Mesaj karmasını kopyala"; +"ton.transfer.prior_confirmed.title" = "Önceki transfer onaylandı"; +"ton.transfer.prior_confirmed.message" = "Önceki bir transfer onaylandı. Bu yeni transfer gönderilmedi. Yeniden onaylamadan önce önceki karmayı inceleyip kabul edin:\n%@"; +"ton.transfer.prior_confirmed.acknowledge" = "Kabul et ve ücreti yeniden hesapla"; "NSPhotoLibraryUsageDescription" = "Kitaplıktan fotoğraf yükle"; "about.announcement" = "Duyuruları Alın"; "about.ask.for.support" = "Destek İsteyin"; @@ -797,6 +803,7 @@ burada görmek için NFT'leri satın alın veya bastırın"; "staking.analytics.period.all" = "Tüm"; "staking.analytics.rewards.empty.message" = "Seçilen zaman aralığında\ hiç ödül alınmadı"; +"staking.analytics.received.rewards" = "Alınan ödüller"; "staking.analytics.stake.allocation" = "Mevcut validatörlerle stake tahsisi"; "staking.analytics.stake.empty.message" = "Bahis değişiklikleriniz burada görünecek"; "staking.analytics.staking.was.active" = "Staking aktifti"; @@ -1205,4 +1212,4 @@ ait olduğundan emin olun."; "your.validators.change.validators.title" = "Doğrulayıcıları değiştir"; "your.validators.stop.nominating.title" = "Aday göstermeyi bırak"; "your.validators.validator.total.stake" = "Toplam yatırılan miktar:%@"; -"сurrencies.stub.text" = "Para birimleri"; \ No newline at end of file +"сurrencies.stub.text" = "Para birimleri"; diff --git a/fearless/vi.lproj/Localizable.strings b/fearless/vi.lproj/Localizable.strings index 6477046c60..9af4c0635b 100644 --- a/fearless/vi.lproj/Localizable.strings +++ b/fearless/vi.lproj/Localizable.strings @@ -1,6 +1,12 @@ "NSCameraUsageDescription" = "Máy ảnh được sử dụng để chụp mã QR"; "NSFaceIDUsageDescription" = "Fearless sử dụng Face ID để hạn chế người dùng trái phép truy cập vào ứng dụng. Face ID được sử dụng để xác thực truy cập ứng dụng"; "NSPhotoLibraryAddUsageDescription" = "Lưu yêu cầu chuyển dưới dạng mã QR"; +"ton.transfer.unknown.title" = "Không xác định được trạng thái chuyển tiền"; +"ton.transfer.unknown.message" = "Mạng có thể đã chấp nhận giao dịch này. Không gửi lại. Hãy lưu mã băm thông điệp này và kiểm tra trạng thái trước khi thực hiện hành động khác:\n%@"; +"ton.transfer.unknown.copy_hash" = "Sao chép mã băm thông điệp"; +"ton.transfer.prior_confirmed.title" = "Giao dịch trước đã được xác nhận"; +"ton.transfer.prior_confirmed.message" = "Một giao dịch trước đã được xác nhận. Giao dịch mới này chưa được gửi. Hãy xem lại và xác nhận mã băm trước đó trước khi xác nhận lại:\n%@"; +"ton.transfer.prior_confirmed.acknowledge" = "Xác nhận và tính lại phí"; "NSPhotoLibraryUsageDescription" = "Tải ảnh từ thư viện"; "about.announcement" = "Nhận thông báo"; "about.ask.for.support" = "Yêu cầu hỗ trợ"; @@ -799,6 +805,7 @@ Xin lưu ý rằng việc ký giao dịch này sẽ phải chịu phí giao dị "staking.analytics.period.all" = "Tất cả"; "staking.analytics.rewards.empty.message" = "Không có phần thưởng\ trong khoảng thời gian đã chọn"; +"staking.analytics.received.rewards" = "Phần thưởng đã nhận"; "staking.analytics.stake.allocation" = "Phân bổ validator hiện tại"; "staking.analytics.stake.empty.message" = "Stake của bạn thay đổi\ sẽ xuất hiện ở đây"; @@ -1207,4 +1214,4 @@ thuộc đúng mạng"; "your.validators.change.validators.title" = "Thay đổi validator"; "your.validators.stop.nominating.title" = "Dừng đề cử"; "your.validators.validator.total.stake" = "Tổng đã stake: %@"; -"сurrencies.stub.text" = "Tiền tệ"; \ No newline at end of file +"сurrencies.stub.text" = "Tiền tệ"; diff --git a/fearless/zh-Hans.lproj/Localizable.strings b/fearless/zh-Hans.lproj/Localizable.strings index 4459f99d22..f5f22515ca 100644 --- a/fearless/zh-Hans.lproj/Localizable.strings +++ b/fearless/zh-Hans.lproj/Localizable.strings @@ -1,6 +1,12 @@ "NSCameraUsageDescription" = "相机用于捕捉二维码"; "NSFaceIDUsageDescription" = "Fearless使用Face ID来限制未经授权的用户访问该应用程序。Face ID用于在应用程序内进行授权。"; "NSPhotoLibraryAddUsageDescription" = "将转账请求保存为二维码"; +"ton.transfer.unknown.title" = "转账状态未知"; +"ton.transfer.unknown.message" = "网络可能已经接受此转账。请勿再次发送。请保存此消息哈希,并在执行其他操作前检查其状态:\n%@"; +"ton.transfer.unknown.copy_hash" = "复制消息哈希"; +"ton.transfer.prior_confirmed.title" = "之前的转账已确认"; +"ton.transfer.prior_confirmed.message" = "之前的转账已确认。本次新转账尚未发送。再次确认前,请核对并确认之前的哈希:\n%@"; +"ton.transfer.prior_confirmed.acknowledge" = "确认并重新计算手续费"; "NSPhotoLibraryUsageDescription" = "从图库加载照片"; "about.announcement" = "接收通知"; "about.ask.for.support" = "寻求支持"; @@ -799,6 +805,7 @@ "staking.analytics.period.all" = "全部"; "staking.analytics.rewards.empty.message" = "没有收到奖励\ 所选期间"; +"staking.analytics.received.rewards" = "已收到的奖励"; "staking.analytics.stake.allocation" = "使用当前验证人进行分配"; "staking.analytics.stake.empty.message" = "您的质押发生变化\ 会出现在这里"; @@ -1204,4 +1211,4 @@ "your.validators.change.validators.title" = "更改验证人"; "your.validators.stop.nominating.title" = "停止提名"; "your.validators.validator.total.stake" = "总质押:%@"; -"сurrencies.stub.text" = "货币"; \ No newline at end of file +"сurrencies.stub.text" = "货币"; diff --git a/fearlessTests/ApplicationLayer/Services/FeatureToggle/TonChainSelectionTests.swift b/fearlessTests/ApplicationLayer/Services/FeatureToggle/TonChainSelectionTests.swift index 22d4f6e48b..12ac700e21 100644 --- a/fearlessTests/ApplicationLayer/Services/FeatureToggle/TonChainSelectionTests.swift +++ b/fearlessTests/ApplicationLayer/Services/FeatureToggle/TonChainSelectionTests.swift @@ -1,3 +1,7 @@ +import Foundation +import HTTPTypes +import OpenAPIRuntime +import TonAPI import XCTest @testable import fearless import SSFModels @@ -6,6 +10,16 @@ import SSFUtils import SSFRuntimeCodingService import BigInt +private func makeIrohaTestHistoryEndpoint(_ baseURL: String) -> ChainModel.BlockExplorer { + guard let url = URL(string: baseURL), + let endpoint = ChainModel.BlockExplorer(type: "sora", url: url) + else { + preconditionFailure("Iroha test history endpoint failed to materialize") + } + + return endpoint +} + final class TonChainSelectionTests: XCTestCase { func testSelectedChainIdReturnsTestnetWhenEnabled() { let chainId = TonChainSelection.selectedChainId(isTestnetEnabled: true) @@ -83,15 +97,53 @@ final class TonCompatibilityTests: XCTestCase { XCTAssertTrue(chain.isTonCompatibilityChain) } + func testTonCompatibilityChainDetectionByCanonicalIdWithoutMetadataHints() { + let canonicalIds = [ + TonChainSelection.mainnetChainId, + TonChainSelection.testnetChainId, + UniversalWalletRegistry.tonMainnetRegistryEntry.id, + UniversalWalletRegistry.tonMainnetRegistryEntry.chainId, + "ton-testnet", + "ton:testnet" + ] + + for chainId in canonicalIds { + let chain = makeTonCompatibilityChain( + name: "Neutral Chain", + chainId: chainId, + nodeURL: URL(string: "wss://rpc.example.com")!, + explorerURL: URL(string: "https://explorer.example.com/address/abc")! + ) + + XCTAssertTrue(chain.isTonCompatibilityChain, chainId) + } + } + func testTonCompatibilityChainDetectionReturnsFalseForNonTonChain() { - let chain = makeTonCompatibilityChain( - name: "Polkadot", - chainId: "polkadot-mainnet", - nodeURL: URL(string: "wss://rpc.polkadot.io")!, - explorerURL: URL(string: "https://polkadot.subscan.io")! - ) + let nonTonChains = [ + makeTonCompatibilityChain( + name: "Polkadot", + chainId: "polkadot-mainnet", + nodeURL: URL(string: "wss://rpc.polkadot.io")!, + explorerURL: URL(string: "https://polkadot.subscan.io")! + ), + makeTonCompatibilityChain( + name: "Proton Network", + chainId: "proton-mainnet", + nodeURL: URL(string: "wss://button.example.com")!, + explorerURL: URL(string: "https://tonviewer.com.attacker.invalid/address/abc")! + ), + makeTonCompatibilityChain( + name: "Button Chain", + chainId: "custom-ton-like", + nodeURL: URL(string: "wss://nottonapi.io.attacker.invalid")!, + explorerURL: URL(string: "https://nottonviewer.example.com/address/abc")! + ) + ] - XCTAssertFalse(chain.isTonCompatibilityChain) + for chain in nonTonChains { + XCTAssertFalse(chain.isTonCompatibilityChain, chain.chainId) + } } func testTonAssetTypeMapsNormal() { @@ -560,7 +612,7 @@ final class AccountInfoRemoteServiceTests: XCTestCase { XCTAssertEqual(client.accountAssetsInvocations.count, 1) XCTAssertEqual(client.accountAssetsInvocations.first?.accountID, address) - XCTAssertNil(client.accountAssetsInvocations.first?.baseURL) + XCTAssertEqual(client.accountAssetsInvocations.first?.baseURL, "https://taira.sora.org/") XCTAssertEqual(client.accountAssetsInvocations.first?.limit, IrohaToriiRoutes.maxLimit) XCTAssertEqual(client.accountAssetsInvocations.first?.countMode, .bounded) XCTAssertEqual(client.accountAssetsInvocations.first?.network, UniversalWalletRegistry.taira) @@ -758,10 +810,7 @@ final class AccountInfoRemoteServiceTests: XCTestCase { let externalApi = historyBaseURL.map { ChainModel.ExternalApiSet( staking: nil, - history: ChainModel.BlockExplorer( - type: "iroha", - url: URL(string: $0)! - ), + history: makeIrohaTestHistoryEndpoint($0), crowdloans: nil, explorers: nil ) @@ -1119,241 +1168,1253 @@ final class SendDependencyContainerUniversalWalletRoutingTests: XCTestCase { } } - func testPrepareDependenciesFailsClosedForTonCompatibilityTransferBeforeSubstrateRouting() async throws { + #if DEBUG + func testPrepareDependenciesRoutesNativeTonMainnetToTonTransferService() async throws { let chain = makeTonTransferChain() - let wallet = walletWithChainAccount(chainId: chain.chainId) + let wallet = try walletWithTonAccount(chainId: chain.chainId) + let remote = TonTransferRemoteStub() let container = SendDepencyContainer( wallet: wallet, - operationManager: fearless.OperationManagerFacade.sharedManager + operationManager: fearless.OperationManagerFacade.sharedManager, + tonRemote: remote, + tonMnemonicProvider: UniversalWalletMnemonicProviderStub(mnemonic: Self.mnemonic), + tonSendReleasePolicy: .enabledForTests + ) + + let dependencies = try await container.prepareDepencies( + chainAsset: ChainAsset(chain: chain, asset: Self.tonAsset) + ) + + XCTAssertTrue(dependencies.transferService is TonTransferService) + XCTAssertEqual(remote.walletStateCallCount, 0) + XCTAssertEqual(remote.emulateCallCount, 0) + XCTAssertEqual(remote.broadcastCallCount, 0) + } + + func testPrepareDependenciesCachesOneTonServiceAcrossConcurrentEstimateAndSubmit() async throws { + let chain = makeTonTransferChain() + let wallet = try walletWithTonAccount(chainId: chain.chainId) + let remote = TonTransferRemoteStub() + let container = SendDepencyContainer( + wallet: wallet, + operationManager: fearless.OperationManagerFacade.sharedManager, + tonRemote: remote, + tonMnemonicProvider: UniversalWalletMnemonicProviderStub(mnemonic: Self.mnemonic), + tonSendReleasePolicy: .enabledForTests + ) + let chainAsset = ChainAsset(chain: chain, asset: Self.tonAsset) + async let first = container.prepareDepencies(chainAsset: chainAsset) + async let second = container.prepareDepencies(chainAsset: chainAsset) + let (firstDependencies, secondDependencies) = try await (first, second) + + XCTAssertTrue( + (firstDependencies.transferService as AnyObject) === + (secondDependencies.transferService as AnyObject) + ) + + let recipient = try TonKeyDerivation.deriveAccount( + mnemonic: Self.otherMnemonic + ).addressBounceable + let transfer = Transfer( + chainAsset: chainAsset, + amount: BigUInt(100_000_000), + receiver: recipient, + tip: nil, + appId: nil + ) + _ = try await prepareDisplayedTonFee( + service: firstDependencies.transferService, + transfer: transfer + ) + let hash = try await secondDependencies.transferService.submit(transfer: transfer) + let acknowledged = await secondDependencies.transferService.acknowledgeSubmittedTransfer( + hash: hash, + transfer: transfer + ) + + XCTAssertTrue(acknowledged) + XCTAssertEqual(hash.count, 64) + XCTAssertEqual(remote.unsignedEmulateCallCount, 1) + XCTAssertEqual(remote.signedEmulateCallCount, 1) + XCTAssertEqual(remote.broadcastCallCount, 1) + } + #endif + + func testProductionTonSendPolicyIsImmutableDisabledAndCannotBeBypassedByInjection() async throws { + XCTAssertFalse(TonProductionSendReleasePolicy.production.isEnabled) + let chain = makeTonTransferChain() + let wallet = try walletWithTonAccount(chainId: chain.chainId) + let remote = TonTransferRemoteStub() + let container = SendDepencyContainer( + wallet: wallet, + operationManager: fearless.OperationManagerFacade.sharedManager, + tonRemote: remote, + tonMnemonicProvider: UniversalWalletMnemonicProviderStub(mnemonic: Self.mnemonic) ) do { _ = try await container.prepareDepencies( - chainAsset: ChainAsset(chain: chain, asset: Self.asset) + chainAsset: ChainAsset(chain: chain, asset: Self.tonAsset) ) - XCTFail("Expected TON transfer routing to fail closed") + XCTFail("Expected production TON send routing to remain disabled") + } catch let error as UniversalWalletSendRoutingError { + XCTAssertEqual(error, .tonProductionSendDisabled) } catch { - XCTAssertEqual((error as NSError).localizedDescription, "TON transfer not yet supported.") + XCTFail("Unexpected error: \(error)") } + XCTAssertEqual(remote.walletStateCallCount, 0) + XCTAssertEqual(remote.emulateCallCount, 0) + XCTAssertEqual(remote.broadcastCallCount, 0) } - func testPrepareDependenciesCreatesIrohaTransferServiceForTairaAccount() async throws { - let chain = makeIrohaChain(chainId: UniversalWalletRegistry.taira.chainId) - let wallet = try walletWithIrohaAccount(chainId: chain.chainId) + func testProductionTonSendPolicyRejectsCanonicalIdWithoutTonMetadataHints() async throws { + let chain = makeChain(chainId: TonChainSelection.mainnetChainId) + XCTAssertTrue(chain.isTonCompatibilityChain) + let wallet = try walletWithTonAccount(chainId: chain.chainId) + let remote = TonTransferRemoteStub() let container = SendDepencyContainer( wallet: wallet, - operationManager: fearless.OperationManagerFacade.sharedManager - ) - - let dependencies = try await container.prepareDepencies( - chainAsset: ChainAsset(chain: chain, asset: Self.irohaAsset) + operationManager: fearless.OperationManagerFacade.sharedManager, + tonRemote: remote, + tonMnemonicProvider: UniversalWalletMnemonicProviderStub(mnemonic: Self.mnemonic) ) - XCTAssertTrue(dependencies.transferService is IrohaTransferService) + do { + _ = try await container.prepareDepencies( + chainAsset: ChainAsset(chain: chain, asset: Self.asset) + ) + XCTFail("Expected canonical TON identity to remain disabled without metadata hints") + } catch let error as UniversalWalletSendRoutingError { + XCTAssertEqual(error, .tonProductionSendDisabled) + } catch { + XCTFail("Unexpected error: \(error)") + } + XCTAssertEqual(remote.walletStateCallCount, 0) + XCTAssertEqual(remote.emulateCallCount, 0) + XCTAssertEqual(remote.broadcastCallCount, 0) } - func testPrepareDependenciesCreatesIrohaTransferServiceForNexusAccount() async throws { - let chain = makeIrohaChain(chainId: UniversalWalletRegistry.nexus.chainId) - let wallet = try walletWithIrohaAccount(chainId: chain.chainId) + #if DEBUG + func testPrepareDependenciesRejectsJettonBeforeAnyTonRemoteCall() async throws { + let chain = makeTonTransferChain() + let wallet = try walletWithTonAccount(chainId: chain.chainId) + let remote = TonTransferRemoteStub() + let jetton = AssetModel( + id: "jetton-master", + name: "Jetton", + symbol: "JET", + precision: 9, + isUtility: false, + isNative: false, + type: .ormlAsset + ) let container = SendDepencyContainer( wallet: wallet, - operationManager: fearless.OperationManagerFacade.sharedManager + operationManager: fearless.OperationManagerFacade.sharedManager, + tonRemote: remote, + tonMnemonicProvider: UniversalWalletMnemonicProviderStub(mnemonic: Self.mnemonic), + tonSendReleasePolicy: .enabledForTests ) - let dependencies = try await container.prepareDepencies( - chainAsset: ChainAsset(chain: chain, asset: Self.irohaAsset) + do { + _ = try await container.prepareDepencies( + chainAsset: ChainAsset(chain: chain, asset: jetton) + ) + XCTFail("Expected unsupported Jetton routing to fail closed") + } catch let error as UniversalWalletSendRoutingError { + XCTAssertEqual(error, .unsupported(chainId: "-239/jetton-master")) + } catch { + XCTFail("Unexpected error: \(error)") + } + XCTAssertEqual(remote.walletStateCallCount, 0) + XCTAssertEqual(remote.emulateCallCount, 0) + XCTAssertEqual(remote.broadcastCallCount, 0) + } + + func testPrepareDependenciesRejectsTonTestnetBeforeAnyRemoteCall() async throws { + let chain = makeTonTransferChain(chainId: TonChainSelection.testnetChainId) + let wallet = try walletWithTonAccount(chainId: chain.chainId) + let remote = TonTransferRemoteStub() + let container = SendDepencyContainer( + wallet: wallet, + operationManager: fearless.OperationManagerFacade.sharedManager, + tonRemote: remote, + tonMnemonicProvider: UniversalWalletMnemonicProviderStub(mnemonic: Self.mnemonic), + tonSendReleasePolicy: .enabledForTests ) - XCTAssertTrue(dependencies.transferService is IrohaTransferService) + do { + _ = try await container.prepareDepencies( + chainAsset: ChainAsset(chain: chain, asset: Self.tonAsset) + ) + XCTFail("Expected TON testnet routing to fail closed") + } catch let error as UniversalWalletSendRoutingError { + XCTAssertEqual(error, .unsupported(chainId: TonChainSelection.testnetChainId)) + } catch { + XCTFail("Unexpected error: \(error)") + } + XCTAssertEqual(remote.walletStateCallCount, 0) + XCTAssertEqual(remote.emulateCallCount, 0) + XCTAssertEqual(remote.broadcastCallCount, 0) } - func testIrohaTransferServiceBuildsSignerRequestAndSubmitsNorito() async throws { - let chain = makeIrohaChain( - chainId: UniversalWalletRegistry.taira.chainId, - historyBaseURL: "https://taira.sora.org" - ) - let wallet = try walletWithIrohaAccount(chainId: chain.chainId) - let sourceAddress = try IrohaKeyDerivation.deriveAddress( - mnemonic: Self.mnemonic, - chainDiscriminant: UniversalWalletRegistry.taira.chainDiscriminant - ).i105 - let recipientAddress = try IrohaAddressCodec.encode( - publicKeyHex: String(repeating: "11", count: 32), - chainDiscriminant: UniversalWalletRegistry.taira.chainDiscriminant - ) - let toriiClient = IrohaSubmitClientStub() - let signer = IrohaTransferSignerStub() - let service = IrohaTransferService( + func testTonTransferServiceBindsMnemonicSenderAndBroadcastsEmulatedBoc() async throws { + let chain = makeTonTransferChain() + let wallet = try walletWithTonAccount(chainId: chain.chainId) + let recipient = try TonKeyDerivation.deriveAccount( + mnemonic: Self.otherMnemonic + ).addressBounceable + let remote = TonTransferRemoteStub() + let service = TonTransferService( wallet: wallet, chain: chain, - toriiClient: toriiClient, - signer: signer, - mnemonicProvider: UniversalWalletMnemonicProviderStub(mnemonic: Self.mnemonic) + remote: remote, + mnemonicProvider: UniversalWalletMnemonicProviderStub(mnemonic: Self.mnemonic), + pendingCoordinator: TonPendingIntentCoordinator(), + clock: { 1_700_000_000 } ) let transfer = Transfer( - chainAsset: ChainAsset(chain: chain, asset: Self.irohaAsset), - amount: BigUInt("1250000000000000000"), - receiver: recipientAddress, + chainAsset: ChainAsset(chain: chain, asset: Self.tonAsset), + amount: BigUInt(100_000_000), + receiver: recipient, tip: nil, appId: nil ) - let fee = try await service.estimateFee(for: transfer) + let fee = try await prepareDisplayedTonFee(service: service, transfer: transfer) let hash = try await service.submit(transfer: transfer) - XCTAssertEqual(fee, BigUInt.zero) - XCTAssertEqual(hash, Self.signedTransactionHash) - XCTAssertEqual(toriiClient.lastSubmittedNorito, Self.signedTransaction) - XCTAssertEqual(toriiClient.lastSubmitBaseURL, "https://taira.sora.org") - XCTAssertEqual(signer.lastRequest?.amount, "1.25") - XCTAssertEqual(signer.lastRequest?.assetDefinitionId, Self.irohaAsset.id) - XCTAssertEqual(signer.lastRequest?.authority, sourceAddress) - XCTAssertEqual(signer.lastRequest?.chainId, UniversalWalletRegistry.taira.chainId) - XCTAssertEqual(signer.lastRequest?.derivationPath, UniversalWalletDerivationPaths.irohaDefault) - XCTAssertEqual(signer.lastRequest?.destinationAccountId, recipientAddress) - XCTAssertEqual(signer.lastRequest?.mnemonicOrSeed, Self.mnemonic) - XCTAssertEqual(signer.lastRequest?.network, "taira") - XCTAssertEqual( - signer.lastRequest?.signingPublicKeyHex, - try IrohaAddressCodec.parse( - sourceAddress, - expectedDiscriminant: UniversalWalletRegistry.taira.chainDiscriminant - ).publicKeyHex - ) - XCTAssertEqual(signer.lastRequest?.sourceAccountId, sourceAddress) - XCTAssertEqual(signer.lastRequest?.sourceAssetId, "\(Self.irohaAsset.id)#\(sourceAddress)") + XCTAssertEqual(fee, BigUInt(1)) + XCTAssertEqual(hash.count, 64) + XCTAssertEqual(remote.walletStateCallCount, 2) + XCTAssertEqual(remote.emulateCallCount, 2) + XCTAssertEqual(remote.broadcastCallCount, 1) + XCTAssertEqual(remote.broadcastBocs, [try XCTUnwrap(remote.emulatedBocs.last)]) } - func testIrohaTransferServiceBuildsNexusSignerRequestAndSubmitsNorito() async throws { - let chain = makeIrohaChain( - chainId: UniversalWalletRegistry.nexus.chainId, - historyBaseURL: "https://minamoto.sora.org" - ) - let wallet = try walletWithIrohaAccount(chainId: chain.chainId) - let sourceAddress = try IrohaKeyDerivation.deriveAddress( - mnemonic: Self.mnemonic, - chainDiscriminant: UniversalWalletRegistry.nexus.chainDiscriminant - ).i105 - let recipientAddress = try IrohaAddressCodec.encode( - publicKeyHex: String(repeating: "22", count: 32), - chainDiscriminant: UniversalWalletRegistry.nexus.chainDiscriminant - ) - let toriiClient = IrohaSubmitClientStub() - let signer = IrohaTransferSignerStub() - let service = IrohaTransferService( + func testTonQuoteCannotSubmitUntilExactPresentationIsAcknowledged() async throws { + let chain = makeTonTransferChain() + let wallet = try walletWithTonAccount(chainId: chain.chainId) + let recipient = try TonKeyDerivation.deriveAccount( + mnemonic: Self.otherMnemonic + ).addressBounceable + let remote = TonTransferRemoteStub() + let mnemonicProvider = CountingUniversalWalletMnemonicProvider(mnemonic: Self.mnemonic) + let service = TonTransferService( wallet: wallet, chain: chain, - toriiClient: toriiClient, - signer: signer, - mnemonicProvider: UniversalWalletMnemonicProviderStub(mnemonic: Self.mnemonic) + remote: remote, + mnemonicProvider: mnemonicProvider, + pendingCoordinator: TonPendingIntentCoordinator(), + clock: { 1_700_000_000 } ) let transfer = Transfer( - chainAsset: ChainAsset(chain: chain, asset: Self.irohaAsset), - amount: BigUInt("1250000000000000000"), - receiver: recipientAddress, + chainAsset: ChainAsset(chain: chain, asset: Self.tonAsset), + amount: BigUInt(100_000_000), + receiver: recipient, tip: nil, appId: nil ) + let received = expectation(description: "staged TON quote") + let listener = TonFeePresentationListenerStub(expectation: received) + service.subscribeForFee(transfer: transfer, listener: listener) + await fulfillment(of: [received], timeout: 2) + let walletCallsAfterQuote = remote.walletStateCallCount + let emulationCallsAfterQuote = remote.emulateCallCount - let fee = try await service.estimateFee(for: transfer) - let hash = try await service.submit(transfer: transfer) + do { + _ = try await service.submit(transfer: transfer) + XCTFail("A staged but undisplayed TON quote was submitted") + } catch TransferServiceError.transferFailed {} + XCTAssertEqual(mnemonicProvider.callCount, 0) + XCTAssertEqual(remote.walletStateCallCount, walletCallsAfterQuote) + XCTAssertEqual(remote.emulateCallCount, emulationCallsAfterQuote) + XCTAssertEqual(remote.broadcastCallCount, 0) - XCTAssertEqual(fee, BigUInt.zero) - XCTAssertEqual(hash, Self.signedTransactionHash) - XCTAssertEqual(toriiClient.lastSubmittedNorito, Self.signedTransaction) - XCTAssertEqual(toriiClient.lastSubmitBaseURL, "https://minamoto.sora.org") - XCTAssertEqual(signer.lastRequest?.amount, "1.25") - XCTAssertEqual(signer.lastRequest?.assetDefinitionId, Self.irohaAsset.id) - XCTAssertEqual(signer.lastRequest?.authority, sourceAddress) - XCTAssertEqual(signer.lastRequest?.chainId, UniversalWalletRegistry.nexus.chainId) - XCTAssertEqual(signer.lastRequest?.derivationPath, UniversalWalletDerivationPaths.irohaDefault) - XCTAssertEqual(signer.lastRequest?.destinationAccountId, recipientAddress) - XCTAssertEqual(signer.lastRequest?.mnemonicOrSeed, Self.mnemonic) - XCTAssertEqual(signer.lastRequest?.network, "nexus") - XCTAssertEqual( - signer.lastRequest?.signingPublicKeyHex, - try IrohaAddressCodec.parse( - sourceAddress, - expectedDiscriminant: UniversalWalletRegistry.nexus.chainDiscriminant - ).publicKeyHex + let presentationAccepted = await service.confirmFeePresentation( + id: try XCTUnwrap(listener.presentationID), + fee: try XCTUnwrap(listener.fee) ) - XCTAssertEqual(signer.lastRequest?.sourceAccountId, sourceAddress) - XCTAssertEqual(signer.lastRequest?.sourceAssetId, "\(Self.irohaAsset.id)#\(sourceAddress)") + XCTAssertTrue(presentationAccepted) + _ = try await service.submit(transfer: transfer) + XCTAssertEqual(mnemonicProvider.callCount, 1) + XCTAssertEqual(remote.signedEmulateCallCount, 1) + XCTAssertEqual(remote.broadcastCallCount, 1) } - func testIrohaTransferServiceDefaultSignerFailsClosedAfterValidation() async throws { - let chain = makeIrohaChain(chainId: UniversalWalletRegistry.taira.chainId) - let wallet = try walletWithIrohaAccount(chainId: chain.chainId) - let recipientAddress = try IrohaAddressCodec.encode( - publicKeyHex: String(repeating: "11", count: 32), - chainDiscriminant: UniversalWalletRegistry.taira.chainDiscriminant - ) - let service = IrohaTransferService( + func testTonDirectEstimateNeverAuthorizesSubmission() async throws { + let chain = makeTonTransferChain() + let wallet = try walletWithTonAccount(chainId: chain.chainId) + let recipient = try TonKeyDerivation.deriveAccount( + mnemonic: Self.otherMnemonic + ).addressBounceable + let remote = TonTransferRemoteStub() + let mnemonicProvider = CountingUniversalWalletMnemonicProvider(mnemonic: Self.mnemonic) + let service = TonTransferService( wallet: wallet, chain: chain, - toriiClient: IrohaSubmitClientStub(), - mnemonicProvider: UniversalWalletMnemonicProviderStub(mnemonic: Self.mnemonic) + remote: remote, + mnemonicProvider: mnemonicProvider, + pendingCoordinator: TonPendingIntentCoordinator(), + clock: { 1_700_000_000 } ) let transfer = Transfer( - chainAsset: ChainAsset(chain: chain, asset: Self.irohaAsset), - amount: BigUInt("1000000000000000000"), - receiver: recipientAddress, + chainAsset: ChainAsset(chain: chain, asset: Self.tonAsset), + amount: BigUInt(100_000_000), + receiver: recipient, tip: nil, appId: nil ) - let fee = try await service.estimateFee(for: transfer) - XCTAssertEqual(fee, BigUInt.zero) - + _ = try await service.estimateFee(for: transfer) do { _ = try await service.submit(transfer: transfer) - XCTFail("Expected unavailable Iroha signer to fail closed") - } catch TransferServiceError.transferFailed(let reason) { - XCTAssertTrue(reason.contains("signing codec")) - } catch { - XCTFail("Unexpected error: \(error)") - } + XCTFail("A fee calculated without an opaque presentation ID authorized submission") + } catch TransferServiceError.transferFailed {} + + XCTAssertEqual(mnemonicProvider.callCount, 0) + XCTAssertEqual(remote.signedEmulateCallCount, 0) + XCTAssertEqual(remote.broadcastCallCount, 0) } - func testIrohaTransferServiceRejectsMnemonicMismatchBeforeSignerOrToriiCalls() async throws { - let chain = makeIrohaChain(chainId: UniversalWalletRegistry.taira.chainId) - let wallet = try walletWithIrohaAccount(chainId: chain.chainId) - let recipientAddress = try IrohaAddressCodec.encode( - publicKeyHex: String(repeating: "11", count: 32), - chainDiscriminant: UniversalWalletRegistry.taira.chainDiscriminant - ) - let toriiClient = IrohaSubmitClientStub() - let signer = IrohaTransferSignerStub() - let service = IrohaTransferService( + func testTonUnsubscribeSynchronouslyRevokesQuoteBeforeImmediateSubmit() async throws { + let chain = makeTonTransferChain() + let wallet = try walletWithTonAccount(chainId: chain.chainId) + let recipient = try TonKeyDerivation.deriveAccount( + mnemonic: Self.otherMnemonic + ).addressBounceable + let remote = TonTransferRemoteStub() + let mnemonicProvider = CountingUniversalWalletMnemonicProvider(mnemonic: Self.mnemonic) + let service = TonTransferService( wallet: wallet, chain: chain, - toriiClient: toriiClient, - signer: signer, - mnemonicProvider: UniversalWalletMnemonicProviderStub(mnemonic: Self.otherMnemonic) + remote: remote, + mnemonicProvider: mnemonicProvider, + pendingCoordinator: TonPendingIntentCoordinator(), + clock: { 1_700_000_000 } ) let transfer = Transfer( - chainAsset: ChainAsset(chain: chain, asset: Self.irohaAsset), - amount: BigUInt("1000000000000000000"), - receiver: recipientAddress, + chainAsset: ChainAsset(chain: chain, asset: Self.tonAsset), + amount: BigUInt(100_000_000), + receiver: recipient, tip: nil, appId: nil ) + _ = try await prepareDisplayedTonFee(service: service, transfer: transfer) + let walletCallsAfterQuote = remote.walletStateCallCount + let emulationCallsAfterQuote = remote.emulateCallCount + service.unsubscribe() do { _ = try await service.submit(transfer: transfer) - XCTFail("Expected Iroha transfer service to reject mismatched mnemonic material") - } catch TransferServiceError.transferFailed(let reason) { - XCTAssertTrue(reason.contains("does not match selected wallet")) - } catch { - XCTFail("Unexpected error: \(error)") + XCTFail("An unsubscribed TON quote was submitted") + } catch TransferServiceError.transferFailed {} + + XCTAssertEqual(mnemonicProvider.callCount, 0) + XCTAssertEqual(remote.walletStateCallCount, walletCallsAfterQuote) + XCTAssertEqual(remote.emulateCallCount, emulationCallsAfterQuote) + XCTAssertEqual(remote.broadcastCallCount, 0) + } + + func testTonRestartRecoverySucceedsWithoutMnemonicAndWithoutNewBearer() async throws { + let chain = makeTonTransferChain() + let wallet = try walletWithTonAccount(chainId: chain.chainId) + let recipient = try TonKeyDerivation.deriveAccount( + mnemonic: Self.otherMnemonic + ).addressBounceable + let transfer = Transfer( + chainAsset: ChainAsset(chain: chain, asset: Self.tonAsset), + amount: BigUInt(100_000_000), + receiver: recipient, + tip: nil, + appId: nil + ) + let journal = TonInMemoryPendingIntentJournal() + let firstRemote = TonTransferRemoteStub() + firstRemote.reconciliationResult = .notFound + let firstService = TonTransferService( + wallet: wallet, + chain: chain, + remote: firstRemote, + mnemonicProvider: UniversalWalletMnemonicProviderStub(mnemonic: Self.mnemonic), + pendingCoordinator: TonPendingIntentCoordinator(journal: journal), + clock: { 1_700_000_000 } + ) + _ = try await prepareDisplayedTonFee(service: firstService, transfer: transfer) + let oldHash: String + do { + _ = try await firstService.submit(transfer: transfer) + return XCTFail("Expected first outcome to remain unknown") + } catch let TransferServiceError.tonBroadcastOutcomeUnknown(messageHashHex) { + oldHash = messageHashHex } - XCTAssertNil(signer.lastRequest) - XCTAssertNil(toriiClient.lastSubmittedNorito) + + let missingMnemonic = CountingUniversalWalletMnemonicProvider(mnemonic: nil) + let recoveryRemote = TonTransferRemoteStub() + recoveryRemote.reconciliationResult = .confirmed + let recoveryService = TonTransferService( + wallet: wallet, + chain: chain, + remote: recoveryRemote, + mnemonicProvider: missingMnemonic, + pendingCoordinator: TonPendingIntentCoordinator(journal: journal), + clock: { 1_700_000_000 } + ) + let recoveredHash = try await recoveryService.submit(transfer: transfer) + + XCTAssertEqual(recoveredHash, oldHash) + XCTAssertEqual(missingMnemonic.callCount, 0) + XCTAssertEqual(recoveryRemote.reconcileCallCount, 1) + XCTAssertEqual(recoveryRemote.walletStateCallCount, 0) + XCTAssertEqual(recoveryRemote.emulateCallCount, 0) + XCTAssertEqual(recoveryRemote.broadcastCallCount, 0) } - private func makeChain(chainId: String) -> ChainModel { - ChainModel( - rank: nil, + func testTonDifferentIntentRecoveryNeverReturnsThePriorHashAsNewSuccess() async throws { + let chain = makeTonTransferChain() + let wallet = try walletWithTonAccount(chainId: chain.chainId) + let recipient = try TonKeyDerivation.deriveAccount( + mnemonic: Self.otherMnemonic + ).addressBounceable + let chainAsset = ChainAsset(chain: chain, asset: Self.tonAsset) + let priorTransfer = Transfer( + chainAsset: chainAsset, + amount: BigUInt(100_000_000), + receiver: recipient, + tip: nil, + appId: nil + ) + let newTransfer = Transfer( + chainAsset: chainAsset, + amount: BigUInt(100_000_001), + receiver: recipient, + tip: nil, + appId: nil + ) + let journal = TonInMemoryPendingIntentJournal() + let firstRemote = TonTransferRemoteStub() + firstRemote.reconciliationResult = .notFound + let firstService = TonTransferService( + wallet: wallet, + chain: chain, + remote: firstRemote, + mnemonicProvider: UniversalWalletMnemonicProviderStub(mnemonic: Self.mnemonic), + pendingCoordinator: TonPendingIntentCoordinator(journal: journal), + clock: { 1_700_000_000 } + ) + _ = try await prepareDisplayedTonFee(service: firstService, transfer: priorTransfer) + let priorHash: String + do { + _ = try await firstService.submit(transfer: priorTransfer) + return XCTFail("Expected the prior transfer outcome to remain unknown") + } catch let TransferServiceError.tonBroadcastOutcomeUnknown(messageHashHex) { + priorHash = messageHashHex + } + + let mnemonicProvider = CountingUniversalWalletMnemonicProvider(mnemonic: nil) + let recoveryRemote = TonTransferRemoteStub() + recoveryRemote.reconciliationResult = .confirmed + let recoveryService = TonTransferService( + wallet: wallet, + chain: chain, + remote: recoveryRemote, + mnemonicProvider: mnemonicProvider, + pendingCoordinator: TonPendingIntentCoordinator(journal: journal), + clock: { 1_700_000_000 } + ) + let recoveredIdentity: TonTransferIntentIdentity + do { + _ = try await recoveryService.submit(transfer: newTransfer) + return XCTFail("The prior transfer hash was reported as the new transfer's success") + } catch let TransferServiceError.tonPriorTransferConfirmed(identity, messageHashHex) { + recoveredIdentity = identity + XCTAssertEqual(messageHashHex, priorHash) + XCTAssertEqual(identity.amountNanotons, priorTransfer.amount.description) + } + + XCTAssertEqual(mnemonicProvider.callCount, 0) + XCTAssertEqual(recoveryRemote.reconcileCallCount, 1) + XCTAssertEqual(recoveryRemote.walletStateCallCount, 0) + XCTAssertEqual(recoveryRemote.emulateCallCount, 0) + XCTAssertEqual(recoveryRemote.broadcastCallCount, 0) + let acknowledged = await recoveryService.acknowledgeRecoveredTransfer( + hash: priorHash, + identity: recoveredIdentity + ) + XCTAssertTrue(acknowledged) + } + #endif + + func testTonFeeEstimateNeverRequestsMnemonicOrProducesSignedEmulation() async throws { + let chain = makeTonTransferChain() + let wallet = try walletWithTonAccount(chainId: chain.chainId) + let recipient = try TonKeyDerivation.deriveAccount( + mnemonic: Self.otherMnemonic + ).addressBounceable + let remote = TonTransferRemoteStub() + let mnemonicProvider = CountingUniversalWalletMnemonicProvider( + mnemonic: Self.mnemonic + ) + let service = TonTransferService( + wallet: wallet, + chain: chain, + remote: remote, + mnemonicProvider: mnemonicProvider, + clock: { 1_700_000_000 } + ) + let transfer = Transfer( + chainAsset: ChainAsset(chain: chain, asset: Self.tonAsset), + amount: BigUInt(100_000_000), + receiver: recipient, + tip: nil, + appId: nil + ) + + let estimatedFee = try await service.estimateFee(for: transfer) + + XCTAssertEqual(estimatedFee, BigUInt(1)) + XCTAssertEqual(mnemonicProvider.callCount, 0) + XCTAssertEqual(remote.unsignedEmulateCallCount, 1) + XCTAssertEqual(remote.signedEmulateCallCount, 0) + XCTAssertEqual(remote.broadcastCallCount, 0) + } + + func testTonFeePaymentAssetIgnoresAdditionalUnorderedUtilityAssets() { + let hostileUtility = AssetModel( + id: "hostile-utility", + name: "Hostile Utility", + symbol: "HST", + precision: 2, + isUtility: true, + isNative: false, + type: .ormlAsset + ) + let chain = makeTonTransferChain(assets: [hostileUtility, Self.tonAsset]) + let selectedTon = ChainAsset(chain: chain, asset: Self.tonAsset) + + let feeAsset = WalletSendConfirmInteractor.resolveFeePaymentChainAsset( + for: selectedTon + ) + + XCTAssertEqual(feeAsset?.asset.id, Self.tonAsset.id) + XCTAssertEqual(feeAsset?.asset.symbol, "TON") + XCTAssertEqual(feeAsset?.asset.precision, 9) + } + + #if !DEBUG + func testReleaseTonTransferSubmitFailsBeforeMnemonicOrRemoteWork() async { + let chain = makeChain(chainId: TonChainSelection.mainnetChainId) + let mnemonicProvider = CountingUniversalWalletMnemonicProvider( + mnemonic: Self.mnemonic + ) + let remote = TonTransferRemoteStub() + let service = TonTransferService( + wallet: AccountGenerator.generateMetaAccount(), + chain: chain, + remote: remote, + mnemonicProvider: mnemonicProvider, + clock: { 1_700_000_000 } + ) + let invalidTransfer = Transfer( + chainAsset: ChainAsset(chain: chain, asset: Self.asset), + amount: 0, + receiver: "intentionally invalid", + tip: nil, + appId: nil + ) + + do { + _ = try await service.submit(transfer: invalidTransfer) + XCTFail("Expected Release TON submit to remain disabled") + } catch TransferServiceError.tonProductionSendDisabled { + // Expected before transfer resolution, mnemonic lookup, or remote work. + } catch { + XCTFail("Unexpected Release TON submit error: \(error)") + } + + XCTAssertEqual(mnemonicProvider.callCount, 0) + XCTAssertEqual(remote.walletStateCallCount, 0) + XCTAssertEqual(remote.emulateCallCount, 0) + XCTAssertEqual(remote.broadcastCallCount, 0) + } + #endif + + #if DEBUG + func testTonTransferServicePreservesExactUnknownOutcomeHash() async throws { + let chain = makeTonTransferChain() + let wallet = try walletWithTonAccount(chainId: chain.chainId) + let recipient = try TonKeyDerivation.deriveAccount( + mnemonic: Self.otherMnemonic + ).addressBounceable + let remote = TonTransferRemoteStub() + remote.reconciliationResult = .notFound + let service = TonTransferService( + wallet: wallet, + chain: chain, + remote: remote, + mnemonicProvider: UniversalWalletMnemonicProviderStub(mnemonic: Self.mnemonic), + pendingCoordinator: TonPendingIntentCoordinator(), + clock: { 1_700_000_000 } + ) + let transfer = Transfer( + chainAsset: ChainAsset(chain: chain, asset: Self.tonAsset), + amount: BigUInt(100_000_000), + receiver: recipient, + tip: nil, + appId: nil + ) + + _ = try await prepareDisplayedTonFee(service: service, transfer: transfer) + do { + _ = try await service.submit(transfer: transfer) + XCTFail("Expected an explicitly unknown broadcast outcome") + } catch let TransferServiceError.tonBroadcastOutcomeUnknown(messageHashHex) { + XCTAssertEqual(messageHashHex, remote.broadcastMessageHashes.first) + XCTAssertEqual(messageHashHex.count, 64) + } catch { + XCTFail("Unexpected TON submit error: \(error)") + } + XCTAssertEqual(remote.broadcastCallCount, 1) + } + + func testTonTransferServiceRejectsMnemonicMismatchBeforeRemoteCalls() async throws { + let chain = makeTonTransferChain() + let wallet = try walletWithTonAccount(chainId: chain.chainId) + let recipient = try TonKeyDerivation.deriveAccount( + mnemonic: Self.otherMnemonic + ).addressBounceable + let remote = TonTransferRemoteStub() + let service = TonTransferService( + wallet: wallet, + chain: chain, + remote: remote, + mnemonicProvider: UniversalWalletMnemonicProviderStub(mnemonic: Self.otherMnemonic), + pendingCoordinator: TonPendingIntentCoordinator(), + clock: { 1_700_000_000 } + ) + let transfer = Transfer( + chainAsset: ChainAsset(chain: chain, asset: Self.tonAsset), + amount: BigUInt(100_000_000), + receiver: recipient, + tip: nil, + appId: nil + ) + + _ = try await prepareDisplayedTonFee(service: service, transfer: transfer) + let walletCallsBeforeSubmit = remote.walletStateCallCount + let emulationCallsBeforeSubmit = remote.emulateCallCount + do { + _ = try await service.submit(transfer: transfer) + XCTFail("Expected mismatched TON mnemonic to fail closed") + } catch TransferServiceError.transferFailed {} + + XCTAssertEqual(remote.walletStateCallCount, walletCallsBeforeSubmit) + XCTAssertEqual(remote.emulateCallCount, emulationCallsBeforeSubmit) + XCTAssertEqual(remote.unsignedEmulateCallCount, 1) + XCTAssertEqual(remote.signedEmulateCallCount, 0) + XCTAssertEqual(remote.broadcastCallCount, 0) + } + #endif + + #if DEBUG + func testTonTransferServiceRejectsUnsupportedTipAndAppIdBeforeRemoteCalls() async throws { + let chain = makeTonTransferChain() + let wallet = try walletWithTonAccount(chainId: chain.chainId) + let recipient = try TonKeyDerivation.deriveAccount( + mnemonic: Self.otherMnemonic + ).addressBounceable + + let unsupportedFields: [(tip: BigUInt?, appId: BigUInt?)] = [ + (BigUInt(1), nil), + (nil, BigUInt(1)), + (BigUInt(1), BigUInt(1)) + ] + for (tip, appId) in unsupportedFields { + let remote = TonTransferRemoteStub() + let service = TonTransferService( + wallet: wallet, + chain: chain, + remote: remote, + mnemonicProvider: UniversalWalletMnemonicProviderStub(mnemonic: Self.mnemonic), + pendingCoordinator: TonPendingIntentCoordinator(), + clock: { 1_700_000_000 } + ) + let transfer = Transfer( + chainAsset: ChainAsset(chain: chain, asset: Self.tonAsset), + amount: BigUInt(100_000_000), + receiver: recipient, + tip: tip, + appId: appId + ) + + do { + _ = try await service.submit(transfer: transfer) + XCTFail("Expected unsupported TON fields to fail closed") + } catch TransferServiceError.transferFailed {} + + XCTAssertEqual(remote.walletStateCallCount, 0) + XCTAssertEqual(remote.emulateCallCount, 0) + XCTAssertEqual(remote.broadcastCallCount, 0) + } + } + #endif + + func testPrepareDependenciesCreatesIrohaTransferServiceForTairaAccount() async throws { + let chain = makeIrohaChain(chainId: UniversalWalletRegistry.taira.chainId) + let wallet = try walletWithIrohaAccount(chainId: chain.chainId) + let container = SendDepencyContainer( + wallet: wallet, + operationManager: fearless.OperationManagerFacade.sharedManager + ) + + let dependencies = try await container.prepareDepencies( + chainAsset: ChainAsset(chain: chain, asset: Self.irohaAsset) + ) + + XCTAssertTrue(dependencies.transferService is IrohaTransferService) + } + + func testPrepareDependenciesCreatesIrohaTransferServiceForNexusAccount() async throws { + let chain = makeIrohaChain(chainId: UniversalWalletRegistry.nexus.chainId) + let wallet = try walletWithIrohaAccount(chainId: chain.chainId) + let container = SendDepencyContainer( + wallet: wallet, + operationManager: fearless.OperationManagerFacade.sharedManager + ) + + let dependencies = try await container.prepareDepencies( + chainAsset: ChainAsset(chain: chain, asset: Self.irohaAsset) + ) + + XCTAssertTrue(dependencies.transferService is IrohaTransferService) + } + + func testIrohaTransferServiceBuildsSignerRequestAndSubmitsNorito() async throws { + let chain = makeIrohaChain( + chainId: UniversalWalletRegistry.taira.chainId, + historyBaseURL: "https://taira.sora.org" + ) + let wallet = try walletWithIrohaAccount(chainId: chain.chainId) + let sourceAddress = try IrohaKeyDerivation.deriveAddress( + mnemonic: Self.mnemonic, + chainDiscriminant: UniversalWalletRegistry.taira.chainDiscriminant + ).i105 + let recipientAddress = try IrohaAddressCodec.encode( + publicKeyHex: String(repeating: "11", count: 32), + chainDiscriminant: UniversalWalletRegistry.taira.chainDiscriminant + ) + let toriiClient = IrohaSubmitClientStub() + let signer = IrohaTransferSignerStub() + let service = IrohaTransferService( + wallet: wallet, + chain: chain, + toriiClient: toriiClient, + signer: signer, + mnemonicProvider: UniversalWalletMnemonicProviderStub(mnemonic: Self.mnemonic) + ) + let transfer = Transfer( + chainAsset: ChainAsset(chain: chain, asset: Self.irohaAsset), + amount: BigUInt("1250000000000000000"), + receiver: recipientAddress, + tip: nil, + appId: nil + ) + + let fee = try await service.estimateFee(for: transfer) + let hash = try await service.submit(transfer: transfer) + + XCTAssertEqual(fee, BigUInt.zero) + XCTAssertEqual(hash, Self.signedTransactionHash) + XCTAssertEqual(toriiClient.lastSubmittedNorito, Self.signedTransaction) + XCTAssertEqual(toriiClient.lastSubmitBaseURL, "https://taira.sora.org") + XCTAssertEqual(signer.lastRequest?.amount, "1.25") + XCTAssertEqual(signer.lastRequest?.assetDefinitionId, Self.irohaAsset.id) + XCTAssertEqual(signer.lastRequest?.authority, sourceAddress) + XCTAssertEqual(signer.lastRequest?.chainId, UniversalWalletRegistry.taira.chainId) + XCTAssertEqual(signer.lastRequest?.derivationPath, UniversalWalletDerivationPaths.irohaDefault) + XCTAssertEqual(signer.lastRequest?.destinationAccountId, recipientAddress) + XCTAssertEqual(signer.lastRequest?.metadata, IrohaTransactionMetadata.none) + XCTAssertEqual(signer.lastRequest?.metadata.values, [:]) + XCTAssertEqual(signer.lastRequest?.mnemonicOrSeed, Self.mnemonic) + XCTAssertEqual(signer.lastRequest?.network, "taira") + XCTAssertEqual( + signer.lastRequest?.signingPublicKeyHex, + try IrohaAddressCodec.parse( + sourceAddress, + expectedDiscriminant: UniversalWalletRegistry.taira.chainDiscriminant + ).publicKeyHex + ) + XCTAssertEqual(signer.lastRequest?.sourceAccountId, sourceAddress) + XCTAssertEqual(signer.lastRequest?.sourceAssetId, "\(Self.irohaAsset.id)#\(sourceAddress)") + } + + func testIrohaTransferServiceBuildsNexusSignerRequestAndSubmitsNorito() async throws { + let chain = makeIrohaChain( + chainId: UniversalWalletRegistry.nexus.chainId, + historyBaseURL: "https://minamoto.sora.org" + ) + let wallet = try walletWithIrohaAccount(chainId: chain.chainId) + let sourceAddress = try IrohaKeyDerivation.deriveAddress( + mnemonic: Self.mnemonic, + chainDiscriminant: UniversalWalletRegistry.nexus.chainDiscriminant + ).i105 + let recipientAddress = try IrohaAddressCodec.encode( + publicKeyHex: String(repeating: "22", count: 32), + chainDiscriminant: UniversalWalletRegistry.nexus.chainDiscriminant + ) + let toriiClient = IrohaSubmitClientStub() + let signer = IrohaTransferSignerStub() + let service = IrohaTransferService( + wallet: wallet, + chain: chain, + toriiClient: toriiClient, + signer: signer, + mnemonicProvider: UniversalWalletMnemonicProviderStub(mnemonic: Self.mnemonic) + ) + let transfer = Transfer( + chainAsset: ChainAsset(chain: chain, asset: Self.irohaAsset), + amount: BigUInt("1250000000000000000"), + receiver: recipientAddress, + tip: nil, + appId: nil + ) + + let fee = try await service.estimateFee(for: transfer) + let hash = try await service.submit(transfer: transfer) + + XCTAssertEqual(fee, BigUInt.zero) + XCTAssertEqual(hash, Self.signedTransactionHash) + XCTAssertEqual(toriiClient.lastSubmittedNorito, Self.signedTransaction) + XCTAssertEqual(toriiClient.lastSubmitBaseURL, "https://minamoto.sora.org") + XCTAssertEqual(signer.lastRequest?.amount, "1.25") + XCTAssertEqual(signer.lastRequest?.assetDefinitionId, Self.irohaAsset.id) + XCTAssertEqual(signer.lastRequest?.authority, sourceAddress) + XCTAssertEqual(signer.lastRequest?.chainId, UniversalWalletRegistry.nexus.chainId) + XCTAssertEqual(signer.lastRequest?.derivationPath, UniversalWalletDerivationPaths.irohaDefault) + XCTAssertEqual(signer.lastRequest?.destinationAccountId, recipientAddress) + XCTAssertEqual(signer.lastRequest?.metadata, IrohaTransactionMetadata.none) + XCTAssertEqual(signer.lastRequest?.metadata.values, [:]) + XCTAssertEqual(signer.lastRequest?.mnemonicOrSeed, Self.mnemonic) + XCTAssertEqual(signer.lastRequest?.network, "nexus") + XCTAssertEqual( + signer.lastRequest?.signingPublicKeyHex, + try IrohaAddressCodec.parse( + sourceAddress, + expectedDiscriminant: UniversalWalletRegistry.nexus.chainDiscriminant + ).publicKeyHex + ) + XCTAssertEqual(signer.lastRequest?.sourceAccountId, sourceAddress) + XCTAssertEqual(signer.lastRequest?.sourceAssetId, "\(Self.irohaAsset.id)#\(sourceAddress)") + } + + func testIrohaTransferServiceDefaultSignerFailsClosedAfterValidation() async throws { + let chain = makeIrohaChain(chainId: UniversalWalletRegistry.taira.chainId) + let wallet = try walletWithIrohaAccount(chainId: chain.chainId) + let recipientAddress = try IrohaAddressCodec.encode( + publicKeyHex: String(repeating: "11", count: 32), + chainDiscriminant: UniversalWalletRegistry.taira.chainDiscriminant + ) + let service = IrohaTransferService( + wallet: wallet, + chain: chain, + toriiClient: IrohaSubmitClientStub(), + mnemonicProvider: UniversalWalletMnemonicProviderStub(mnemonic: Self.mnemonic) + ) + let transfer = Transfer( + chainAsset: ChainAsset(chain: chain, asset: Self.irohaAsset), + amount: BigUInt("1000000000000000000"), + receiver: recipientAddress, + tip: nil, + appId: nil + ) + + let fee = try await service.estimateFee(for: transfer) + XCTAssertEqual(fee, BigUInt.zero) + + do { + _ = try await service.submit(transfer: transfer) + XCTFail("Expected unavailable Iroha signer to fail closed") + } catch TransferServiceError.transferFailed(let reason) { + XCTAssertTrue(reason.contains("signing codec")) + } catch { + XCTFail("Unexpected error: \(error)") + } + } + + func testIrohaTransferServiceRejectsMnemonicMismatchBeforeSignerOrToriiCalls() async throws { + let chain = makeIrohaChain(chainId: UniversalWalletRegistry.taira.chainId) + let wallet = try walletWithIrohaAccount(chainId: chain.chainId) + let recipientAddress = try IrohaAddressCodec.encode( + publicKeyHex: String(repeating: "11", count: 32), + chainDiscriminant: UniversalWalletRegistry.taira.chainDiscriminant + ) + let toriiClient = IrohaSubmitClientStub() + let signer = IrohaTransferSignerStub() + let service = IrohaTransferService( + wallet: wallet, + chain: chain, + toriiClient: toriiClient, + signer: signer, + mnemonicProvider: UniversalWalletMnemonicProviderStub(mnemonic: Self.otherMnemonic) + ) + let transfer = Transfer( + chainAsset: ChainAsset(chain: chain, asset: Self.irohaAsset), + amount: BigUInt("1000000000000000000"), + receiver: recipientAddress, + tip: nil, + appId: nil + ) + + do { + _ = try await service.submit(transfer: transfer) + XCTFail("Expected Iroha transfer service to reject mismatched mnemonic material") + } catch TransferServiceError.transferFailed(let reason) { + XCTAssertTrue(reason.contains("does not match selected wallet")) + } catch { + XCTFail("Unexpected error: \(error)") + } + XCTAssertNil(signer.lastRequest) + XCTAssertNil(toriiClient.lastSubmittedNorito) + } + + func testIrohaNexusWalletSmokeEvidenceThreadsExactImmutableMetadataToSigner() async throws { + let chain = makeIrohaChain( + chainId: UniversalWalletRegistry.nexus.chainId, + historyBaseURL: "https://minamoto.sora.org" + ) + let wallet = try walletWithIrohaAccount(chainId: chain.chainId) + let recipientAddress = try IrohaAddressCodec.encode( + publicKeyHex: String(repeating: "22", count: 32), + chainDiscriminant: UniversalWalletRegistry.nexus.chainDiscriminant + ) + let toriiClient = IrohaSubmitClientStub() + let signer = IrohaTransferSignerStub() + let service = IrohaTransferService( + wallet: wallet, + chain: chain, + toriiClient: toriiClient, + signer: signer, + mnemonicProvider: UniversalWalletMnemonicProviderStub(mnemonic: Self.mnemonic) + ) + let transfer = Transfer( + chainAsset: ChainAsset(chain: chain, asset: Self.irohaAsset), + amount: BigUInt("1000000000000000000"), + receiver: recipientAddress, + tip: nil, + appId: nil + ) + var operatorInput = Self.validIrohaWalletSmokeMetadata + let expectedSnapshot = operatorInput + + let hash = try await service.submitNexusWalletSmokeEvidence( + transfer: transfer, + untrustedMetadata: operatorInput + ) + operatorInput[IrohaWalletSmokeTransactionMetadata.walletCommitKey] = String(repeating: "b", count: 40) + + XCTAssertEqual(hash, Self.signedTransactionHash) + XCTAssertEqual(signer.lastRequest?.network, "nexus") + XCTAssertEqual(signer.lastRequest?.metadata.values, expectedSnapshot) + XCTAssertEqual(signer.lastRequest?.metadata.values.count, 4) + XCTAssertEqual(toriiClient.lastSubmittedNorito, Self.signedTransaction) + XCTAssertEqual(toriiClient.lastSubmitBaseURL, "https://minamoto.sora.org") + } + + func testIrohaWalletSmokeMetadataSnapshotDoesNotAliasInputOrReturnedValues() throws { + var operatorInput = Self.validIrohaWalletSmokeMetadata + let expectedSnapshot = operatorInput + let metadata = try IrohaWalletSmokeTransactionMetadata.validatedSnapshot(of: operatorInput) + + operatorInput[IrohaWalletSmokeTransactionMetadata.walletCommitKey] = String(repeating: "b", count: 40) + var returnedValues = metadata.values + returnedValues[IrohaWalletSmokeTransactionMetadata.evidenceRoleKey] = "route-canary" + + XCTAssertEqual(metadata.values, expectedSnapshot) + XCTAssertNotEqual(metadata.values, operatorInput) + XCTAssertNotEqual(metadata.values, returnedValues) + } + + func testIrohaNexusWalletSmokeEvidenceRejectsMalformedMetadataBeforeSignerOrTorii() async throws { + let chain = makeIrohaChain( + chainId: UniversalWalletRegistry.nexus.chainId, + historyBaseURL: "https://minamoto.sora.org" + ) + let wallet = try walletWithIrohaAccount(chainId: chain.chainId) + let recipientAddress = try IrohaAddressCodec.encode( + publicKeyHex: String(repeating: "22", count: 32), + chainDiscriminant: UniversalWalletRegistry.nexus.chainDiscriminant + ) + let transfer = Transfer( + chainAsset: ChainAsset(chain: chain, asset: Self.irohaAsset), + amount: BigUInt("1000000000000000000"), + receiver: recipientAddress, + tip: nil, + appId: nil + ) + let roleKey = IrohaWalletSmokeTransactionMetadata.evidenceRoleKey + let routeHashKey = IrohaWalletSmokeTransactionMetadata.routeGovernanceActionHashKey + let platformKey = IrohaWalletSmokeTransactionMetadata.walletPlatformKey + let commitKey = IrohaWalletSmokeTransactionMetadata.walletCommitKey + var cases: [(String, [String: String], IrohaWalletSmokeMetadataError)] = [] + + func mutated( + _ label: String, + key: String, + value: String?, + expected: IrohaWalletSmokeMetadataError + ) { + var metadata = Self.validIrohaWalletSmokeMetadata + metadata[key] = value + cases.append((label, metadata, expected)) + } + + mutated("missing field", key: roleKey, value: nil, expected: .invalidFieldSet) + mutated("wrong role", key: roleKey, value: "route-canary", expected: .invalidEvidenceRole) + mutated("wrong role case", key: roleKey, value: "Wallet-Smoke", expected: .invalidEvidenceRole) + mutated("wrong platform", key: platformKey, value: "android", expected: .invalidWalletPlatform) + mutated("wrong platform case", key: platformKey, value: "IOS", expected: .invalidWalletPlatform) + mutated("hash prefix", key: routeHashKey, value: String(repeating: "1", count: 64), expected: .invalidRouteGovernanceActionHash) + mutated("short hash", key: routeHashKey, value: "sha256:" + String(repeating: "1", count: 63), expected: .invalidRouteGovernanceActionHash) + mutated("uppercase hash", key: routeHashKey, value: "sha256:" + String(repeating: "A", count: 64), expected: .invalidRouteGovernanceActionHash) + mutated("zero hash", key: routeHashKey, value: "sha256:" + String(repeating: "0", count: 64), expected: .invalidRouteGovernanceActionHash) + mutated("hash control", key: routeHashKey, value: Self.irohaRouteActionHash + "\n", expected: .invalidRouteGovernanceActionHash) + mutated("hash unicode", key: routeHashKey, value: "sha256:" + String(repeating: "1", count: 64), expected: .invalidRouteGovernanceActionHash) + mutated("short commit", key: commitKey, value: String(repeating: "a", count: 39), expected: .invalidWalletCommit) + mutated("uppercase commit", key: commitKey, value: String(repeating: "A", count: 40), expected: .invalidWalletCommit) + mutated("zero commit", key: commitKey, value: String(repeating: "0", count: 40), expected: .invalidWalletCommit) + mutated("commit control", key: commitKey, value: Self.irohaWalletCommit + "\u{0000}", expected: .invalidWalletCommit) + mutated("commit unicode", key: commitKey, value: String(repeating: "a", count: 40), expected: .invalidWalletCommit) + + var extra = Self.validIrohaWalletSmokeMetadata + extra["unexpected"] = "field" + cases.append(("extra field", extra, .invalidFieldSet)) + + var wrongKeyCase = Self.validIrohaWalletSmokeMetadata + wrongKeyCase[platformKey] = nil + wrongKeyCase["wallet_Platform"] = "ios" + cases.append(("wrong key case", wrongKeyCase, .invalidFieldSet)) + + var controlKey = Self.validIrohaWalletSmokeMetadata + controlKey[commitKey] = nil + controlKey[commitKey + "\n"] = Self.irohaWalletCommit + cases.append(("control in key", controlKey, .invalidFieldSet)) + + var unicodeKey = Self.validIrohaWalletSmokeMetadata + unicodeKey[commitKey] = nil + unicodeKey["wallet_commіt"] = Self.irohaWalletCommit + cases.append(("unicode confusable key", unicodeKey, .invalidFieldSet)) + + for (label, metadata, expectedError) in cases { + let toriiClient = IrohaSubmitClientStub() + let signer = IrohaTransferSignerStub() + let mnemonicProvider = CountingUniversalWalletMnemonicProvider(mnemonic: Self.mnemonic) + let service = IrohaTransferService( + wallet: wallet, + chain: chain, + toriiClient: toriiClient, + signer: signer, + mnemonicProvider: mnemonicProvider + ) + + do { + _ = try await service.submitNexusWalletSmokeEvidence( + transfer: transfer, + untrustedMetadata: metadata + ) + XCTFail("Malformed metadata unexpectedly reached the signer: \(label)") + } catch let error as IrohaWalletSmokeMetadataError { + XCTAssertEqual(error, expectedError, label) + } catch { + XCTFail("Unexpected metadata error for \(label): \(error)") + } + XCTAssertEqual(mnemonicProvider.callCount, 0, label) + XCTAssertNil(signer.lastRequest, label) + XCTAssertNil(toriiClient.lastSubmittedNorito, label) + } + XCTAssertEqual(cases.count, 20) + } + + func testIrohaWalletSmokeEvidenceRejectsTairaAndNoncanonicalNexusBeforeSignerOrTorii() async throws { + let configurations: [( + label: String, + chainId: String, + historyBaseURL: String, + expectedReason: String + )] = [ + ( + "Taira route", + UniversalWalletRegistry.taira.chainId, + "https://taira.sora.org", + "exact canonical Nexus chain identity" + ), + ( + "uppercase Nexus chain-id drift", + UniversalWalletRegistry.nexus.chainId.uppercased(), + "https://minamoto.sora.org", + "exact canonical Nexus chain identity" + ), + ( + "Nexus registry-id alias", + UniversalWalletRegistry.nexus.id, + "https://minamoto.sora.org", + "exact canonical Nexus chain identity" + ), + ( + "unrelated HTTPS host", + UniversalWalletRegistry.nexus.chainId, + "https://nexus-proxy.example", + "canonical Nexus Torii" + ), + ( + "HTTP downgrade", + UniversalWalletRegistry.nexus.chainId, + "http://minamoto.sora.org", + "canonical Nexus Torii" + ), + ( + "suffix-confusion host", + UniversalWalletRegistry.nexus.chainId, + "https://minamoto.sora.org.attacker.invalid", + "canonical Nexus Torii" + ), + ( + "userinfo host confusion", + UniversalWalletRegistry.nexus.chainId, + "https://minamoto.sora.org@attacker.invalid", + "canonical Nexus Torii" + ), + ( + "noncanonical port", + UniversalWalletRegistry.nexus.chainId, + "https://minamoto.sora.org:444", + "canonical Nexus Torii" + ), + ( + "path-bearing endpoint", + UniversalWalletRegistry.nexus.chainId, + "https://minamoto.sora.org/v1/mcp", + "canonical Nexus Torii" + ), + ( + "query-bearing endpoint", + UniversalWalletRegistry.nexus.chainId, + "https://minamoto.sora.org?redirect=https://attacker.invalid", + "canonical Nexus Torii" + ), + ( + "fragment-bearing endpoint", + UniversalWalletRegistry.nexus.chainId, + "https://minamoto.sora.org#@attacker.invalid", + "canonical Nexus Torii" + ), + ( + "trailing-slash drift", + UniversalWalletRegistry.nexus.chainId, + "https://minamoto.sora.org/", + "canonical Nexus Torii" + ) + ] + + XCTAssertEqual(configurations.count, 12) + + for configuration in configurations { + let chain = makeIrohaChain( + chainId: configuration.chainId, + historyBaseURL: configuration.historyBaseURL + ) + XCTAssertEqual( + chain.externalApi?.history?.url.absoluteString, + configuration.historyBaseURL, + configuration.label + ) + let wallet = try walletWithIrohaAccount(chainId: chain.chainId) + let network = configuration.chainId == UniversalWalletRegistry.taira.chainId + ? UniversalWalletRegistry.taira + : UniversalWalletRegistry.nexus + let recipientAddress = try IrohaAddressCodec.encode( + publicKeyHex: String(repeating: "22", count: 32), + chainDiscriminant: network.chainDiscriminant + ) + let toriiClient = IrohaSubmitClientStub() + let signer = IrohaTransferSignerStub() + let mnemonicProvider = CountingUniversalWalletMnemonicProvider(mnemonic: Self.mnemonic) + let service = IrohaTransferService( + wallet: wallet, + chain: chain, + toriiClient: toriiClient, + signer: signer, + mnemonicProvider: mnemonicProvider + ) + let transfer = Transfer( + chainAsset: ChainAsset(chain: chain, asset: Self.irohaAsset), + amount: BigUInt("1000000000000000000"), + receiver: recipientAddress, + tip: nil, + appId: nil + ) + + do { + _ = try await service.submitNexusWalletSmokeEvidence( + transfer: transfer, + untrustedMetadata: Self.validIrohaWalletSmokeMetadata + ) + XCTFail("Untrusted evidence route unexpectedly reached signing: \(configuration.label)") + } catch TransferServiceError.transferFailed(let reason) { + XCTAssertTrue( + reason.contains(configuration.expectedReason), + "\(configuration.label): \(reason)" + ) + } catch { + XCTFail("Unexpected evidence route error for \(configuration.label): \(error)") + } + XCTAssertEqual(mnemonicProvider.callCount, 0, configuration.label) + XCTAssertNil(signer.lastRequest, configuration.label) + XCTAssertNil(toriiClient.lastSubmittedNorito, configuration.label) + } + } + + func testIrohaWalletSmokeEvidenceRemainsFailClosedWithUnavailableSigner() async throws { + let chain = makeIrohaChain( + chainId: UniversalWalletRegistry.nexus.chainId, + historyBaseURL: "https://minamoto.sora.org" + ) + let wallet = try walletWithIrohaAccount(chainId: chain.chainId) + let recipientAddress = try IrohaAddressCodec.encode( + publicKeyHex: String(repeating: "22", count: 32), + chainDiscriminant: UniversalWalletRegistry.nexus.chainDiscriminant + ) + let toriiClient = IrohaSubmitClientStub() + let service = IrohaTransferService( + wallet: wallet, + chain: chain, + toriiClient: toriiClient, + mnemonicProvider: UniversalWalletMnemonicProviderStub(mnemonic: Self.mnemonic) + ) + let transfer = Transfer( + chainAsset: ChainAsset(chain: chain, asset: Self.irohaAsset), + amount: BigUInt("1000000000000000000"), + receiver: recipientAddress, + tip: nil, + appId: nil + ) + + do { + _ = try await service.submitNexusWalletSmokeEvidence( + transfer: transfer, + untrustedMetadata: Self.validIrohaWalletSmokeMetadata + ) + XCTFail("Wallet-smoke metadata unexpectedly enabled production signing") + } catch TransferServiceError.transferFailed(let reason) { + XCTAssertTrue(reason.contains("signing codec"), reason) + } catch { + XCTFail("Unexpected unavailable-signer error: \(error)") + } + XCTAssertNil(toriiClient.lastSubmittedNorito) + } + + private func makeChain(chainId: String) -> ChainModel { + ChainModel( + rank: nil, disabled: false, chainId: chainId, parentId: nil, @@ -1380,19 +2441,22 @@ final class SendDependencyContainerUniversalWalletRoutingTests: XCTestCase { ) } - private func makeTonTransferChain() -> ChainModel { + private func makeTonTransferChain( + chainId: String = TonChainSelection.mainnetChainId, + assets: Set? = nil + ) -> ChainModel { ChainModel( rank: nil, disabled: false, - chainId: "ton-mainnet-unit", + chainId: chainId, parentId: nil, paraId: nil, name: "TON Mainnet", - assets: [Self.asset], + assets: assets ?? [Self.tonAsset], xcm: nil, nodes: [ ChainNodeModel( - url: URL(string: "wss://rpc.ton.org")!, + url: URL(string: "https://tonapi.io")!, name: "TON", apikey: nil ) @@ -1400,7 +2464,7 @@ final class SendDependencyContainerUniversalWalletRoutingTests: XCTestCase { addressPrefix: 0, types: nil, icon: nil, - options: nil, + options: chainId == TonChainSelection.testnetChainId ? [.testnet] : nil, externalApi: nil, selectedNode: nil, customNodes: nil, @@ -1416,10 +2480,7 @@ final class SendDependencyContainerUniversalWalletRoutingTests: XCTestCase { let externalApi = historyBaseURL.map { ChainModel.ExternalApiSet( staking: nil, - history: ChainModel.BlockExplorer( - type: "iroha", - url: URL(string: $0)! - ), + history: makeIrohaTestHistoryEndpoint($0), crowdloans: nil, explorers: nil ) @@ -1466,6 +2527,37 @@ final class SendDependencyContainerUniversalWalletRoutingTests: XCTestCase { return AccountGenerator.generateMetaAccount(with: [chainAccount]) } + private func prepareDisplayedTonFee( + service: TransferServiceProtocol, + transfer: Transfer + ) async throws -> BigUInt { + let received = expectation(description: "displayed TON fee") + let listener = TonFeePresentationListenerStub(expectation: received) + service.subscribeForFee(transfer: transfer, listener: listener) + await fulfillment(of: [received], timeout: 2) + let fee = try XCTUnwrap(listener.fee) + let presentationID = try XCTUnwrap(listener.presentationID) + let accepted = await service.confirmFeePresentation( + id: presentationID, + fee: fee + ) + XCTAssertTrue(accepted) + return fee + } + + private func walletWithTonAccount(chainId: String) throws -> MetaAccountModel { + let account = try TonKeyDerivation.deriveAccount(mnemonic: Self.mnemonic) + let chainAccount = ChainAccountModel( + chainId: chainId, + accountId: account.accountHash, + publicKey: account.publicKey, + cryptoType: CryptoType.ed25519.rawValue, + ethereumBased: false + ) + + return AccountGenerator.generateMetaAccount(with: [chainAccount]) + } + private func walletWithChainAccount(chainId: String) -> MetaAccountModel { let chainAccount = ChainAccountModel( chainId: chainId, @@ -1495,10 +2587,29 @@ final class SendDependencyContainerUniversalWalletRoutingTests: XCTestCase { isUtility: true, isNative: true ) + private static let tonAsset = AssetModel( + id: UniversalWalletRegistry.tonNativeAssetId, + name: "Toncoin", + symbol: "TON", + precision: 9, + isUtility: true, + isNative: true, + type: .normal + ) private static let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" private static let otherMnemonic = "legal winner thank year wave sausage worth useful legal winner thank yellow" private static let signedTransaction = Data([1, 2, 3, 4]) private static let signedTransactionHash = "signed-transaction-hash" + private static let irohaRouteActionHash = "sha256:" + String(repeating: "1", count: 64) + private static let irohaWalletCommit = String(repeating: "a", count: 40) + private static var validIrohaWalletSmokeMetadata: [String: String] { + [ + IrohaWalletSmokeTransactionMetadata.evidenceRoleKey: "wallet-smoke", + IrohaWalletSmokeTransactionMetadata.routeGovernanceActionHashKey: irohaRouteActionHash, + IrohaWalletSmokeTransactionMetadata.walletPlatformKey: "ios", + IrohaWalletSmokeTransactionMetadata.walletCommitKey: irohaWalletCommit + ] + } private static let receiptTxHash = "receipt-tx-hash" private static let receiptEntrypointHash = "receipt-entrypoint-hash" private static let receiptSignedTransactionHash = "receipt-signed-transaction-hash" @@ -1516,6 +2627,90 @@ final class SendDependencyContainerUniversalWalletRoutingTests: XCTestCase { } } + private final class TonFeePresentationListenerStub: TonTransferFeePresentationListener { + private let expectation: XCTestExpectation + private(set) var fee: BigUInt? + private(set) var presentationID: String? + + init(expectation: XCTestExpectation) { + self.expectation = expectation + } + + func didReceiveTonFee(fee: BigUInt, presentationID: String) { + self.fee = fee + self.presentationID = presentationID + expectation.fulfill() + } + + func didReceiveFee(fee _: BigUInt) { + XCTFail("TON quote lost its presentation identifier") + expectation.fulfill() + } + + func didReceiveFeeError(feeError: Error) { + XCTFail("Unexpected TON quote error: \(feeError)") + expectation.fulfill() + } + } + + private final class TonTransferRemoteStub: TonTransferRemoteProtocol, @unchecked Sendable { + let reviewedSignedOperationOrigin: String? = TonAPIClientFactory.canonicalAuthenticatedOrigin.absoluteString + + private(set) var walletStateCallCount = 0 + private(set) var emulateCallCount = 0 + private(set) var unsignedEmulateCallCount = 0 + private(set) var signedEmulateCallCount = 0 + private(set) var broadcastCallCount = 0 + private(set) var reconcileCallCount = 0 + private(set) var emulatedBocs: [String] = [] + private(set) var broadcastBocs: [String] = [] + private(set) var broadcastMessageHashes: [String] = [] + var reconciliationResult: TonReconciliationResult = .confirmed + + func walletState(address _: String) async throws -> TonWalletRemoteState { + walletStateCallCount += 1 + return TonWalletRemoteState(sequenceNumber: 1, isInitialized: true) + } + + func recipientRequiresMemo(address _: String) async throws -> Bool { + false + } + + func emulateUnsigned( + message: TonUnsignedEmulationMessage, + intent _: TonEmulationIntent + ) async throws -> TonEmulationResult { + emulateCallCount += 1 + unsignedEmulateCallCount += 1 + emulatedBocs.append(message.bocBase64) + return TonEmulationResult(accepted: true, totalFeeNanotons: 1) + } + + func emulateSigned( + message: TonSignedExternalMessage, + intent _: TonEmulationIntent + ) async throws -> TonEmulationResult { + emulateCallCount += 1 + signedEmulateCallCount += 1 + emulatedBocs.append(message.bocBase64) + return TonEmulationResult(accepted: true, totalFeeNanotons: 1) + } + + func broadcast(message: TonSignedExternalMessage) async throws { + broadcastCallCount += 1 + broadcastBocs.append(message.bocBase64) + broadcastMessageHashes.append(message.messageHashHex) + } + + func reconcile( + message _: TonSignedExternalMessage, + intent _: TonEmulationIntent + ) async throws -> TonReconciliationResult { + reconcileCallCount += 1 + return reconciliationResult + } + } + private final class IrohaSubmitClientStub: IrohaToriiClientProtocol { private(set) var lastSubmittedNorito: Data? private(set) var lastSubmitBaseURL: String? @@ -1606,10 +2801,24 @@ final class SendDependencyContainerUniversalWalletRoutingTests: XCTestCase { mnemonic } } + + private final class CountingUniversalWalletMnemonicProvider: UniversalWalletMnemonicProviding { + let mnemonic: String? + private(set) var callCount = 0 + + init(mnemonic: String?) { + self.mnemonic = mnemonic + } + + func mnemonic(for _: MetaAccountModel, chain _: ChainModel) throws -> String? { + callCount += 1 + return mnemonic + } + } } final class ChainRegistryTonNodeSelectionTests: XCTestCase { - func testResolveTonNodePrefersSelectedNode() { + func testResolveTonNodePrefersSelectedNode() throws { let primary = ChainNodeModel( url: URL(string: "https://ton-selected.example.com")!, name: "Selected", @@ -1625,9 +2834,10 @@ final class ChainRegistryTonNodeSelectionTests: XCTestCase { let resolved = ChainRegistry.resolveTonNode(for: chain) XCTAssertEqual(resolved?.url, primary.url) + XCTAssertEqual(try ChainRegistry.tonAPIBaseURL(for: chain), primary.url) } - func testResolveTonNodeFallsBackDeterministicallyWhenSelectedNodeMissing() { + func testResolveTonNodeFallsBackDeterministicallyWhenSelectedNodeMissing() throws { let nodeB = ChainNodeModel( url: URL(string: "https://b-ton.example.com")!, name: "B", @@ -1643,6 +2853,7 @@ final class ChainRegistryTonNodeSelectionTests: XCTestCase { let resolved = ChainRegistry.resolveTonNode(for: chain) XCTAssertEqual(resolved?.url, nodeA.url) + XCTAssertEqual(try ChainRegistry.tonAPIBaseURL(for: chain), nodeA.url) } func testResolveTonNodeReturnsNilForEmptyNodesAndNoSelection() { @@ -1651,6 +2862,508 @@ final class ChainRegistryTonNodeSelectionTests: XCTestCase { let resolved = ChainRegistry.resolveTonNode(for: chain) XCTAssertNil(resolved) + XCTAssertThrowsError(try ChainRegistry.tonAPIBaseURL(for: chain)) + } + + func testTonApiBaseURLRejectsInsecureCrossNetworkEndpoint() { + let node = ChainNodeModel( + url: URL(string: "http://testnet.ton.example.com")!, + name: "Insecure testnet", + apikey: nil + ) + let chain = makeTonChain(nodes: [node], selectedNode: node) + + XCTAssertThrowsError(try ChainRegistry.tonAPIBaseURL(for: chain)) + } + + func testTonApiBaseURLRejectsAuthorityAndURLComponentConfusion() { + let invalidURLs = [ + "https://user:secret@tonapi.io", + "https://tonapi.io/v2", + "https://tonapi.io?redirect=https://attacker.example", + "https://tonapi.io#attacker" + ] + + for rawURL in invalidURLs { + let node = ChainNodeModel( + url: URL(string: rawURL)!, + name: "Untrusted", + apikey: nil + ) + let chain = makeTonChain(nodes: [node], selectedNode: node) + + XCTAssertThrowsError(try ChainRegistry.tonAPIBaseURL(for: chain), rawURL) + } + } + + func testTonApiAuthorizationIsRestrictedToExactCanonicalOrigin() { + let canonical = TonAPIClientFactory( + tonAPIURL: URL(string: "https://tonapi.io")!, + token: "top-secret" + ) + XCTAssertTrue(canonical.usesAuthorization) + + let untrustedURLs = [ + "https://evil.example", + "https://tonapi.io.evil.example", + "https://tonapi.io:444", + "https://user@tonapi.io", + "https://tonapi.io/v2", + "https://tonapi.io?next=evil", + "https://tonapi.io#evil" + ] + for rawURL in untrustedURLs { + let factory = TonAPIClientFactory( + tonAPIURL: URL(string: rawURL)!, + token: "top-secret" + ) + XCTAssertFalse(factory.usesAuthorization, rawURL) + XCTAssertFalse( + TonAPIClientFactory.canAttachAuthorization(to: factory.serverURL), + rawURL + ) + } + } + + func testCanonicalTonApiOriginStillRejectsMissingOrMalformedCredentialsForSignedOperations() async { + let configuration = mockConfiguration() + TonAPIMockURLProtocol.install { protocolInstance, request in + let response = HTTPURLResponse( + url: request.url!, + statusCode: 200, + httpVersion: nil, + headerFields: nil + )! + protocolInstance.client?.urlProtocol( + protocolInstance, + didReceive: response, + cacheStoragePolicy: .notAllowed + ) + protocolInstance.client?.urlProtocolDidFinishLoading(protocolInstance) + } + defer { TonAPIMockURLProtocol.reset() } + let signedMessage = TonSignedExternalMessage( + boc: Data([0]), + bocBase64: "AA==", + messageHashHex: String(repeating: "00", count: 32), + signingPayloadHashHex: String(repeating: "00", count: 32), + publicKey: Data(repeating: 1, count: 32), + walletAddress: "0:" + String(repeating: "0", count: 64), + sequenceNumber: 0, + validUntil: UInt64.max, + includesStateInit: false + ) + let rejectedTokens = [ + "", + " ", + " leading-space", + "trailing-space ", + "line\nbreak", + "tab\tseparated", + "non-ascii-é", + String(repeating: "a", count: 4097) + ] + + for token in rejectedTokens { + let factory = TonAPIClientFactory( + tonAPIURL: URL(string: "https://tonapi.io")!, + token: token + ) + XCTAssertFalse(factory.usesAuthorization, token.debugDescription) + XCTAssertFalse(factory.hasReviewedProductionSendCredential, token.debugDescription) + let remote = TonAPIRemoteClient(factory: factory, configuration: configuration) + XCTAssertNil(remote.reviewedSignedOperationOrigin, token.debugDescription) + do { + try await remote.broadcast(message: signedMessage) + XCTFail("Malformed credential enabled signed operation: \(token.debugDescription)") + } catch let error as TonTransferRemoteError { + XCTAssertEqual(error, .untrustedSignedOperationEndpoint) + } catch { + XCTFail("Unexpected malformed-credential error: \(error)") + } + } + XCTAssertEqual(TonAPIMockURLProtocol.requestCount, 0) + } + + func testTonProductionSendEndpointAllowlistPinsExactBinaryOwnedOrigin() throws { + XCTAssertEqual( + TonAPIClientFactory.reviewedProductionSendOrigins, + [URL(string: "https://tonapi.io")!] + ) + XCTAssertTrue( + TonAPIClientFactory.isReviewedProductionSendServerURL( + URL(string: "https://tonapi.io")! + ) + ) + XCTAssertTrue( + TonAPIClientFactory.isReviewedProductionSendServerURL( + URL(string: "https://tonapi.io/")! + ) + ) + + let rejectedURLs = [ + "https://evil.example", + "https://tonapi.io.evil.example", + "https://tonapi.io:443", + "https://tonapi.io:444", + "https://user@tonapi.io", + "https://tonapi.io/v2", + "https://tonapi.io?next=evil", + "https://tonapi.io#evil", + "http://tonapi.io" + ] + for rawURL in rejectedURLs { + XCTAssertFalse( + TonAPIClientFactory.isReviewedProductionSendServerURL( + try XCTUnwrap(URL(string: rawURL)) + ), + rawURL + ) + } + } + + func testTonReadOnlyNodeResolutionDoesNotExpandProductionSendAllowlist() throws { + let unreviewed = ChainNodeModel( + url: URL(string: "https://read-only-ton.example.com")!, + name: "Read only", + apikey: nil + ) + let chain = makeTonChain(nodes: [unreviewed], selectedNode: unreviewed) + + XCTAssertEqual(try ChainRegistry.tonAPIBaseURL(for: chain), unreviewed.url) + XCTAssertFalse( + TonAPIClientFactory.isReviewedProductionSendServerURL( + try ChainRegistry.tonAPIBaseURL(for: chain) + ) + ) + } + + func testTonApiTransportNeverFollowsRedirectAndUsesFiniteBounds() { + let redirected = URLRequest(url: URL(string: "https://attacker.example/capture")!) + + XCTAssertNil(TonAPINoRedirectDelegate.redirectedRequest(redirected)) + XCTAssertFalse(TonAPITransportPolicy.followsRedirects) + XCTAssertGreaterThan(TonAPITransportPolicy.requestTimeout, 0) + XCTAssertGreaterThan(TonAPITransportPolicy.resourceTimeout, 0) + XCTAssertLessThanOrEqual(TonAPITransportPolicy.requestTimeout, 30) + XCTAssertLessThanOrEqual(TonAPITransportPolicy.resourceTimeout, 60) + XCTAssertEqual(TonAPITransportPolicy.maximumResponseBytes, 2 * 1_024 * 1_024) + XCTAssertEqual(TonAPITransportPolicy.maximumRequestBodyBytes, 1 * 1_024 * 1_024) + } + + func testTonApiTransportCancelsOversizedStreamingResponse() async throws { + let configuration = mockConfiguration() + TonAPIMockURLProtocol.install { protocolInstance, request in + let response = HTTPURLResponse( + url: request.url!, + statusCode: 200, + httpVersion: nil, + headerFields: nil + )! + protocolInstance.client?.urlProtocol( + protocolInstance, + didReceive: response, + cacheStoragePolicy: .notAllowed + ) + let chunk = Data(repeating: 0x41, count: 512 * 1_024) + for _ in 0 ..< 5 { + protocolInstance.client?.urlProtocol(protocolInstance, didLoad: chunk) + } + protocolInstance.client?.urlProtocolDidFinishLoading(protocolInstance) + } + defer { TonAPIMockURLProtocol.reset() } + + let transport = TonAPIURLSessionTransport(configuration: configuration) + do { + _ = try await transport.send( + HTTPRequest( + method: .get, + scheme: nil, + authority: nil, + path: "/v2/test" + ), + body: nil, + baseURL: URL(string: "https://tonapi.io")!, + operationID: "oversized-response" + ) + XCTFail("Expected bounded transport to reject oversized response") + } catch let error as TonAPITransportError { + guard case let .responseTooLarge(actualBytes, maximumBytes) = error else { + return XCTFail("Unexpected transport error: \(error)") + } + XCTAssertGreaterThan(actualBytes, maximumBytes) + XCTAssertEqual(maximumBytes, TonAPITransportPolicy.maximumResponseBytes) + } + XCTAssertEqual(TonAPIMockURLProtocol.requestCount, 1) + } + + func testTonApiTransportRejectsOversizedRequestBeforeNetwork() async throws { + let configuration = mockConfiguration() + TonAPIMockURLProtocol.install { protocolInstance, request in + let response = HTTPURLResponse( + url: request.url!, + statusCode: 200, + httpVersion: nil, + headerFields: nil + )! + protocolInstance.client?.urlProtocol( + protocolInstance, + didReceive: response, + cacheStoragePolicy: .notAllowed + ) + protocolInstance.client?.urlProtocolDidFinishLoading(protocolInstance) + } + defer { TonAPIMockURLProtocol.reset() } + + let transport = TonAPIURLSessionTransport(configuration: configuration) + let body = HTTPBody( + Data(repeating: 0x42, count: TonAPITransportPolicy.maximumRequestBodyBytes + 1) + ) + do { + _ = try await transport.send( + HTTPRequest( + method: .post, + scheme: nil, + authority: nil, + path: "/v2/test" + ), + body: body, + baseURL: URL(string: "https://tonapi.io")!, + operationID: "oversized-request" + ) + XCTFail("Expected oversized request body to fail") + } catch { + XCTAssertEqual(TonAPIMockURLProtocol.requestCount, 0) + } + } + + func testCancellingTonApiTransportCancelsSessionTaskAndClearsState() async { + let configuration = mockConfiguration() + let started = expectation(description: "request started") + let stopped = expectation(description: "URL loading stopped") + TonAPIMockURLProtocol.install( + { protocolInstance, request in + let response = HTTPURLResponse( + url: request.url!, + statusCode: 200, + httpVersion: nil, + headerFields: nil + )! + protocolInstance.client?.urlProtocol( + protocolInstance, + didReceive: response, + cacheStoragePolicy: .notAllowed + ) + started.fulfill() + // Intentionally never finish: caller cancellation must stop this request. + }, + onStop: { stopped.fulfill() } + ) + defer { TonAPIMockURLProtocol.reset() } + + let transport = TonAPIURLSessionTransport(configuration: configuration) + let requestTask = Task { + try await transport.send( + HTTPRequest( + method: .get, + scheme: nil, + authority: nil, + path: "/v2/never-finishes" + ), + body: nil, + baseURL: URL(string: "https://tonapi.io")!, + operationID: "cancelled-request" + ) + } + + await fulfillment(of: [started], timeout: 1) + XCTAssertEqual(transport.inFlightRequestCount, 1) + requestTask.cancel() + do { + _ = try await requestTask.value + XCTFail("Expected caller cancellation to propagate") + } catch is CancellationError { + // Expected exact cancellation rather than a transport timeout. + } catch { + XCTFail("Unexpected cancellation error: \(error)") + } + await fulfillment(of: [stopped], timeout: 1) + XCTAssertEqual(transport.inFlightRequestCount, 0) + XCTAssertEqual(TonAPIMockURLProtocol.requestCount, 1) + } + + func testTonApiRedirectNeverIssuesHostileRequestOrForwardsAuthorization() async throws { + let configuration = mockConfiguration() + TonAPIMockURLProtocol.install { protocolInstance, request in + let response = HTTPURLResponse( + url: request.url!, + statusCode: 302, + httpVersion: nil, + headerFields: ["Location": "https://attacker.example/capture"] + )! + protocolInstance.client?.urlProtocol( + protocolInstance, + didReceive: response, + cacheStoragePolicy: .notAllowed + ) + protocolInstance.client?.urlProtocolDidFinishLoading(protocolInstance) + } + defer { TonAPIMockURLProtocol.reset() } + + let factory = TonAPIClientFactory( + tonAPIURL: URL(string: "https://tonapi.io")!, + token: "top-secret" + ) + do { + _ = try await factory.tonAPIClient(configuration: configuration).sendBlockchainMessage( + .init(body: .json(.init(boc: "te6ccgEBAQEAAgAAAA=="))) + ) + XCTFail("Expected the generated client to reject a bodyless redirect response") + } catch {} + + XCTAssertEqual(TonAPIMockURLProtocol.requestCount, 1) + XCTAssertEqual(TonAPIMockURLProtocol.requests.first?.url?.host, "tonapi.io") + XCTAssertEqual( + TonAPIMockURLProtocol.requests.first?.value(forHTTPHeaderField: "Authorization"), + "Bearer top-secret" + ) + XCTAssertFalse(TonAPIMockURLProtocol.requests.contains { $0.url?.host == "attacker.example" }) + + let trailingSlashFactory = TonAPIClientFactory( + tonAPIURL: URL(string: "https://tonapi.io/")!, + token: "top-secret" + ) + do { + _ = try await trailingSlashFactory + .tonAPIClient(configuration: configuration) + .sendBlockchainMessage( + .init(body: .json(.init(boc: "te6ccgEBAQEAAgAAAA=="))) + ) + XCTFail("Expected the generated client to reject a bodyless redirect response") + } catch {} + + XCTAssertEqual(TonAPIMockURLProtocol.requestCount, 2) + let trailingSlashRequest = try XCTUnwrap(TonAPIMockURLProtocol.requests.last) + XCTAssertEqual( + trailingSlashRequest.url?.absoluteString, + "https://tonapi.io/v2/blockchain/message" + ) + XCTAssertEqual( + trailingSlashRequest.value(forHTTPHeaderField: "Authorization"), + "Bearer top-secret" + ) + } + + func testHostileTonApiOriginCannotReceiveSignedOperations() async throws { + let configuration = mockConfiguration() + TonAPIMockURLProtocol.install { protocolInstance, request in + let response = HTTPURLResponse( + url: request.url!, + statusCode: 200, + httpVersion: nil, + headerFields: nil + )! + protocolInstance.client?.urlProtocol( + protocolInstance, + didReceive: response, + cacheStoragePolicy: .notAllowed + ) + protocolInstance.client?.urlProtocolDidFinishLoading(protocolInstance) + } + defer { TonAPIMockURLProtocol.reset() } + + let factory = TonAPIClientFactory( + tonAPIURL: URL(string: "https://attacker.example")!, + token: "top-secret" + ) + let remote = TonAPIRemoteClient(factory: factory, configuration: configuration) + let signedMessage = TonSignedExternalMessage( + boc: Data([0]), + bocBase64: "AA==", + messageHashHex: String(repeating: "00", count: 32), + signingPayloadHashHex: String(repeating: "00", count: 32), + publicKey: Data(repeating: 1, count: 32), + walletAddress: "0:" + String(repeating: "0", count: 64), + sequenceNumber: 0, + validUntil: UInt64.max, + includesStateInit: false + ) + + do { + try await remote.broadcast(message: signedMessage) + XCTFail("Expected an unreviewed origin to reject signed operations") + } catch let error as TonTransferRemoteError { + XCTAssertEqual(error, .untrustedSignedOperationEndpoint) + } + + XCTAssertEqual(TonAPIMockURLProtocol.requestCount, 0) + } + + func testCanonicalTonApiFactoryIsRequiredForSignedOperations() async throws { + let configuration = mockConfiguration() + TonAPIMockURLProtocol.install { protocolInstance, request in + let response = HTTPURLResponse( + url: request.url!, + statusCode: 200, + httpVersion: nil, + headerFields: nil + )! + protocolInstance.client?.urlProtocol( + protocolInstance, + didReceive: response, + cacheStoragePolicy: .notAllowed + ) + protocolInstance.client?.urlProtocolDidFinishLoading(protocolInstance) + } + defer { TonAPIMockURLProtocol.reset() } + + let factory = TonAPIClientFactory( + tonAPIURL: URL(string: "https://tonapi.io")!, + token: "top-secret" + ) + let signedMessage = TonSignedExternalMessage( + boc: Data([0]), + bocBase64: "AA==", + messageHashHex: String(repeating: "00", count: 32), + signingPayloadHashHex: String(repeating: "00", count: 32), + publicKey: Data(repeating: 1, count: 32), + walletAddress: "0:" + String(repeating: "0", count: 64), + sequenceNumber: 0, + validUntil: UInt64.max, + includesStateInit: false + ) + + let factoryRemote = TonAPIRemoteClient( + factory: factory, + configuration: configuration + ) + try await factoryRemote.broadcast(message: signedMessage) + XCTAssertEqual(TonAPIMockURLProtocol.requestCount, 1) + XCTAssertEqual( + TonAPIMockURLProtocol.requests.first?.url?.absoluteString, + "https://tonapi.io/v2/blockchain/message" + ) + XCTAssertEqual( + TonAPIMockURLProtocol.requests.first?.value(forHTTPHeaderField: "Authorization"), + "Bearer top-secret" + ) + + let directRemote = TonAPIRemoteClient( + client: factory.tonAPIClient(configuration: configuration) + ) + do { + try await directRemote.broadcast(message: signedMessage) + XCTFail("Expected direct generated-client injection to fail closed") + } catch let error as TonTransferRemoteError { + XCTAssertEqual(error, .untrustedSignedOperationEndpoint) + } + XCTAssertEqual(TonAPIMockURLProtocol.requestCount, 1) + } + + private func mockConfiguration() -> URLSessionConfiguration { + let configuration = URLSessionConfiguration.ephemeral + configuration.protocolClasses = [TonAPIMockURLProtocol.self] + return configuration } private func makeTonChain(nodes: [ChainNodeModel], selectedNode: ChainNodeModel?) -> ChainModel { @@ -1676,6 +3389,65 @@ final class ChainRegistryTonNodeSelectionTests: XCTestCase { } } +private final class TonAPIMockURLProtocol: URLProtocol { + typealias Handler = (TonAPIMockURLProtocol, URLRequest) -> Void + + private static let lock = NSLock() + private static var handler: Handler? + private static var onStop: (() -> Void)? + private static var capturedRequests: [URLRequest] = [] + + static var requests: [URLRequest] { + lock.lock() + defer { lock.unlock() } + return capturedRequests + } + + static var requestCount: Int { requests.count } + + static func install( + _ handler: @escaping Handler, + onStop: (() -> Void)? = nil + ) { + lock.lock() + self.handler = handler + self.onStop = onStop + capturedRequests = [] + lock.unlock() + } + + static func reset() { + lock.lock() + handler = nil + onStop = nil + capturedRequests = [] + lock.unlock() + } + + override class func canInit(with _: URLRequest) -> Bool { true } + override class func canonicalRequest(for request: URLRequest) -> URLRequest { request } + + override func startLoading() { + Self.lock.lock() + Self.capturedRequests.append(request) + let handler = Self.handler + Self.lock.unlock() + + guard let handler else { + client?.urlProtocol(self, didFailWithError: URLError(.badServerResponse)) + return + } + handler(self, request) + } + + override func stopLoading() { + Self.lock.lock() + let onStop = Self.onStop + Self.lock.unlock() + onStop?() + } +} + final class CrossChainConfirmationViewModelFactoryTests: XCTestCase { func testCreateViewModelAddsOriginPreservationNoteForAssetHubChain() { let factory = CrossChainConfirmationViewModelFactory() diff --git a/fearlessTests/Common/DataProvider/RewardDataSourceTests.swift b/fearlessTests/Common/DataProvider/RewardDataSourceTests.swift index 52e3c62ce4..3091089528 100644 --- a/fearlessTests/Common/DataProvider/RewardDataSourceTests.swift +++ b/fearlessTests/Common/DataProvider/RewardDataSourceTests.swift @@ -1,160 +1,533 @@ -//import XCTest -//@testable import fearless -//import RobinHood -// -//class RewardDataSourceTests: NetworkBaseTests { -// -// func testCorrectSync() { -// do { -// // given -// -// let storageFacade = SubstrateStorageTestFacade() -// let chain = ChainModelGenerator.generateChain( -// generatingAssets: 1, -// addressPrefix: 42, -// assetPresicion: 12, -// hasStaking: true, -// hasCrowdloans: true -// ) -// -// guard -// let url = chain.externalApi?.staking?.url, -// let assetPrecision = chain.assets.first?.displayInfo.assetPrecision else { -// XCTFail("Unexpected chain") -// return -// } -// -// TotalRewardMock.register(mock: .westend, url: url) -// -// let expectedReward: Decimal = 5.0 -// -// // when -// -// let repository: CoreDataRepository = -// storageFacade.createRepository() -// -// let actualRewardItem = try performRewardRequest( -// for: AnyDataProviderRepository(repository), -// address: WestendStub.address, -// url: url, -// assetPrecision: assetPrecision -// ).get() -// -// // then -// -// XCTAssertEqual(expectedReward, actualRewardItem?.amount.decimalValue) -// } catch { -// XCTFail("Unexpected error: \(error)") -// } -// } -// -// func testFailureCorrectlyHandled() { -// do { -// // given -// -// let storageFacade = SubstrateStorageTestFacade() -// -// let chain = ChainModelGenerator.generateChain( -// generatingAssets: 1, -// addressPrefix: 42, -// assetPresicion: 12, -// hasStaking: true, -// hasCrowdloans: true -// ) -// -// guard -// let url = chain.externalApi?.staking?.url, -// let assetPrecision = chain.assets.first?.displayInfo.assetPrecision else { -// XCTFail("Unexpected chain") -// return -// } -// -// TotalRewardMock.register(mock: .error, url: url) -// -// // when -// -// let repository: CoreDataRepository = -// storageFacade.createRepository() -// -// let result = try performRewardRequest( -// for: AnyDataProviderRepository(repository), -// address: WestendStub.address, -// url: url, -// assetPrecision: assetPrecision -// ) -// -// // then -// -// switch result { -// case .success: -// XCTFail("Error expected") -// case let .failure(error): -// XCTAssertTrue(error is SubqueryErrors, "Unexpected result error: \(error)") -// } -// } catch { -// XCTFail("Unexpected error: \(error)") -// } -// } -// -// func performRewardRequest(for repository: AnyDataProviderRepository, -// address: String, -// url: URL, -// assetPrecision: Int16 -// ) throws -> Result { -// let operationManager = OperationManager() -// -// let trigger = DataProviderProxyTrigger() -// -// let operationFactory = SubqueryRewardOperationFactory( -// url: url -// ) -// -// let source = SubqueryRewardSource(address: address, -// assetPrecision: assetPrecision, -// targetIdentifier: address, -// repository: AnyDataProviderRepository(repository), -// operationFactory: operationFactory, -// trigger: trigger, -// operationManager: operationManager) -// -// let provider = SingleValueProvider(targetIdentifier: address, -// source: AnySingleValueProviderSource(source), -// repository: AnyDataProviderRepository(repository), -// updateTrigger: trigger) -// -// let expectation = XCTestExpectation() -// expectation.expectedFulfillmentCount = 2 -// -// var totalReward: TotalRewardItem? = nil -// var totalRewardError: Error? = nil -// -// let changesClosure = { (changes: [DataProviderChange]) -> Void in -// totalReward = changes.reduceToLastChange() -// expectation.fulfill() -// } -// -// let failureClosure = { (error: Error) -> Void in -// totalRewardError = error -// expectation.fulfill() -// } -// -// let options = DataProviderObserverOptions(alwaysNotifyOnRefresh: true, -// waitsInProgressSyncOnAdd: false) -// -// provider.addObserver(self, -// deliverOn: .main, -// executing: changesClosure, -// failing: failureClosure, -// options: options) -// -// wait(for: [expectation], timeout: 10.0) -// -// provider.removeObserver(self) -// -// if let error = totalRewardError { -// return .failure(error) -// } else { -// return .success(totalReward) -// } -// } -//} +import BigInt +import IrohaCrypto +import RobinHood +import SSFModels +import XCTest + +@testable import fearless + +final class RewardDataSourceTests: XCTestCase { + private let endpoint = URL(string: "https://example.com/graphql")! + private let stashAddress = "stashAddress" + private let validatorAddress = "validatorAddress" + + func testSubqueryRequestAndResponseThreadCollatorAttributionEndToEnd() throws { + let factory = SubqueryRewardOperationFactory(url: endpoint) + let operation = try networkOperation( + factory: factory, + startTimestamp: 1_700_000_000, + endTimestamp: 1_700_000_100 + ) + let query = try requestQuery(operation) + + XCTAssertTrue(query.contains("collatorId")) + XCTAssertTrue(query.contains("roundId")) + XCTAssertTrue(query.contains("delegatorId")) + XCTAssertTrue(query.contains("type: { equalTo: 2 }")) + XCTAssertFalse(query.contains("type: { equalTo: 0 }")) + XCTAssertTrue(query.contains("greaterThanOrEqualTo: \"1700000000\"")) + XCTAssertTrue(query.contains("lessThanOrEqualTo: \"1700000100\"")) + + let response = try decodeResponse( + operation, + json: """ + { + "data": { + "delegators": { + "nodes": [{ + "id": "stashAddress", + "delegatorHistoryElements": { + "nodes": [{ + "id": "subquery-event", + "blockNumber": 123, + "amount": "456", + "type": 2, + "timestamp": "1700000000", + "delegatorId": "stashAddress", + "collatorId": "validatorAddress", + "roundId": "42" + }] + } + }] + } + } + } + """ + ) + + let rewards = try mappedRewards(response) + let weeklyRewards = try mappedWeeklyRewards(response) + + XCTAssertEqual(rewards.count, 1) + XCTAssertEqual(rewards, weeklyRewards) + XCTAssertEqual(rewards.first?.eventId, "subquery-event") + XCTAssertEqual(rewards.first?.timestamp, 1_700_000_000) + XCTAssertEqual(rewards.first?.validatorAddress, validatorAddress) + XCTAssertEqual(rewards.first?.era, 42) + XCTAssertEqual(rewards.first?.stashAddress, stashAddress) + XCTAssertEqual(rewards.first?.amount, 456) + XCTAssertEqual(rewards.first?.isReward, true) + } + + func testSubqueryMalformedAttributionFailsClosedWithoutDroppingAggregateRewards() throws { + let factory = SubqueryRewardOperationFactory(url: endpoint) + let operation = try networkOperation(factory: factory) + let overflowRound = String(UInt64(EraIndex.max) + 1) + let response = try decodeResponse( + operation, + json: """ + { + "data": { + "delegators": { + "nodes": [{ + "id": "stashAddress", + "delegatorHistoryElements": { + "nodes": [ + {"id":"missing-validator","blockNumber":1,"amount":1,"type":2,"timestamp":"1","delegatorId":"stashAddress","roundId":"1"}, + {"id":"whitespace-validator","blockNumber":2,"amount":"2","type":2,"timestamp":"2","delegatorId":"stashAddress","collatorId":" validatorAddress ","roundId":"2"}, + {"id":"missing-round","blockNumber":3,"amount":"3","type":2,"timestamp":"3","delegatorId":"stashAddress","collatorId":"validatorAddress"}, + {"id":"negative-round","blockNumber":4,"amount":"4","type":2,"timestamp":"4","delegatorId":"stashAddress","collatorId":"validatorAddress","roundId":"-1"}, + {"id":"overflow-round","blockNumber":5,"amount":"5","type":2,"timestamp":"5","delegatorId":"stashAddress","collatorId":"validatorAddress","roundId":"\(overflowRound)"}, + {"id":"wrong-types","blockNumber":6,"amount":"6","type":2,"timestamp":"6","delegatorId":"stashAddress","collatorId":7,"roundId":[]}, + {"id":"non-reward","blockNumber":7,"amount":"7","type":0,"timestamp":"7","delegatorId":"stashAddress","collatorId":"validatorAddress","roundId":"7"}, + {"id":"invalid-timestamp","blockNumber":8,"amount":"8","type":2,"timestamp":"9223372036854775808","delegatorId":"stashAddress","collatorId":"validatorAddress","roundId":"8"}, + {"id":"foreign-delegator","blockNumber":9,"amount":"9","type":2,"timestamp":"9","delegatorId":"attackerAddress","collatorId":"validatorAddress","roundId":"9"} + ] + } + }, { + "id": "attackerAddress", + "delegatorHistoryElements": { + "nodes": [{"id":"attacker","blockNumber":10,"amount":"10","type":2,"timestamp":"10","delegatorId":"attackerAddress","collatorId":"validatorAddress","roundId":"10"}] + } + }] + } + } + } + """ + ) + + let rewards = try mappedRewards(response) + + XCTAssertEqual(rewards.count, 7) + XCTAssertEqual(rewards.map(\.amount), (1 ... 7).map { BigUInt($0) }) + XCTAssertTrue(rewards.prefix(6).allSatisfy { $0.validatorAddress.isEmpty && $0.era == 0 }) + XCTAssertEqual(rewards.last?.validatorAddress, validatorAddress) + XCTAssertEqual(rewards.last?.era, 7) + XCTAssertEqual(rewards.last?.isReward, false) + XCTAssertFalse(rewards.contains { + ["attacker", "foreign-delegator", "invalid-timestamp"].contains($0.eventId) + }) + } + + func testSubsquidRequestUsesLiveHistorySchemaAndNumericTimestampBounds() throws { + let factory = ArrowsquidRewardOperationFactory(url: endpoint) + let operation = try networkOperation( + factory: factory, + startTimestamp: 1_700_000_000, + endTimestamp: 1_700_000_100 + ) + let query = try requestQuery(operation) + + XCTAssertTrue(query.contains("historyElements(")) + XCTAssertFalse(query.contains("rewards(")) + XCTAssertTrue(query.contains("blockHeight")) + XCTAssertTrue(query.contains("reward {")) + XCTAssertTrue(query.contains("validator")) + XCTAssertTrue(query.contains("era")) + XCTAssertTrue(query.contains("AND: { timestamp_gte: 1700000000, timestamp_lte: 1700000100 }")) + XCTAssertFalse(query.contains("timestamp_gte: \"")) + } + + func testHostileAddressesAndInvalidTimestampBoundsAreRejectedBeforeNetworking() throws { + let factories: [RewardOperationFactoryProtocol] = [ + SubqueryRewardOperationFactory(url: endpoint), + ArrowsquidRewardOperationFactory(url: endpoint) + ] + let hostileAddresses = [ + "", + " ", + "stash\" }) { __schema { types { name } } } #", + "stash\\naddress", + "stаshAddress", + String(repeating: "A", count: 129) + ] + + for factory in factories { + for address in hostileAddresses { + try assertRejected(factory: factory, address: address) + } + + try assertRejected(factory: factory, address: stashAddress, startTimestamp: -1) + try assertRejected( + factory: factory, + address: stashAddress, + startTimestamp: 2, + endTimestamp: 1 + ) + } + + try assertRejected( + factory: ArrowsquidRewardOperationFactory(url: endpoint), + address: stashAddress, + endTimestamp: Int64(Int32.max) + 1 + ) + } + + func testSubsquidResponseThreadsValidatorAttributionEndToEnd() throws { + let factory = ArrowsquidRewardOperationFactory(url: endpoint) + let operation = try networkOperation(factory: factory) + let response = try decodeResponse( + operation, + json: """ + { + "data": { + "historyElements": [{ + "id": "subsquid-event", + "blockHeight": 321, + "timestamp": 1700000000, + "address": "stashAddress", + "reward": { + "amount": "654", + "era": 24, + "stash": "stashAddress", + "validator": "validatorAddress" + } + }] + } + } + """ + ) + + let rewards = try mappedRewards(response) + + XCTAssertEqual(rewards.count, 1) + XCTAssertEqual(rewards.first?.eventId, "subsquid-event") + XCTAssertEqual(rewards.first?.timestamp, 1_700_000_000) + XCTAssertEqual(rewards.first?.validatorAddress, validatorAddress) + XCTAssertEqual(rewards.first?.era, 24) + XCTAssertEqual(rewards.first?.amount, 654) + XCTAssertEqual(rewards.first?.isReward, true) + } + + func testSubsquidMalformedAttributionAndForeignRowsFailClosed() throws { + let factory = ArrowsquidRewardOperationFactory(url: endpoint) + let operation = try networkOperation(factory: factory) + let overflowEra = UInt64(EraIndex.max) + 1 + let response = try decodeResponse( + operation, + json: """ + { + "data": { + "historyElements": [ + {"id":"missing-validator","blockHeight":1,"timestamp":1,"address":"stashAddress","reward":{"amount":"1","era":1,"stash":"stashAddress"}}, + {"id":"whitespace-validator","blockHeight":2,"timestamp":2,"address":"stashAddress","reward":{"amount":"2","era":2,"stash":"stashAddress","validator":" validatorAddress "}}, + {"id":"negative-era","blockHeight":3,"timestamp":3,"address":"stashAddress","reward":{"amount":"3","era":-1,"stash":"stashAddress","validator":"validatorAddress"}}, + {"id":"wrong-era-type","blockHeight":4,"timestamp":4,"address":"stashAddress","reward":{"amount":"4","era":"4","stash":"stashAddress","validator":"validatorAddress"}}, + {"id":"overflow-era","blockHeight":5,"timestamp":5,"address":"stashAddress","reward":{"amount":"5","era":\(overflowEra),"stash":"stashAddress","validator":"validatorAddress"}}, + {"id":"wrong-validator-type","blockHeight":6,"timestamp":6,"address":"stashAddress","reward":{"amount":"6","era":6,"stash":"stashAddress","validator":7}}, + {"id":"foreign","blockHeight":7,"timestamp":7,"address":"attackerAddress","reward":{"amount":"7","era":7,"stash":"attackerAddress","validator":"validatorAddress"}}, + {"id":"missing-address","blockHeight":8,"timestamp":8,"address":null,"reward":{"amount":"8","era":8,"stash":"stashAddress","validator":"validatorAddress"}}, + {"id":"mismatched-stash","blockHeight":9,"timestamp":9,"address":"stashAddress","reward":{"amount":"9","era":9,"stash":"attackerAddress","validator":"validatorAddress"}} + ] + } + } + """ + ) + + let rewards = try mappedRewards(response) + + XCTAssertEqual(rewards.count, 6) + XCTAssertEqual(rewards.map(\.amount), (1 ... 6).map { BigUInt($0) }) + XCTAssertTrue(rewards.allSatisfy { $0.validatorAddress.isEmpty && $0.era == 0 }) + XCTAssertFalse(rewards.contains { + ["foreign", "missing-address", "mismatched-stash"].contains($0.eventId) + }) + } + + func testMalformedRequiredRewardAmountRejectsWholeResponse() throws { + let subqueryOperation = try networkOperation( + factory: SubqueryRewardOperationFactory(url: endpoint) + ) + XCTAssertThrowsError( + try decodeResponse( + subqueryOperation, + json: """ + {"data":{"delegators":{"nodes":[{ + "id":"stashAddress", + "delegatorHistoryElements":{"nodes":[{ + "id":"invalid-amount", + "blockNumber":1, + "amount":-1, + "type":2, + "timestamp":"1", + "delegatorId":"stashAddress", + "collatorId":"validatorAddress", + "roundId":"1" + }]} + }]}}} + """ + ) + ) + + let subsquidOperation = try networkOperation( + factory: ArrowsquidRewardOperationFactory(url: endpoint) + ) + + XCTAssertThrowsError( + try decodeResponse( + subsquidOperation, + json: """ + {"data":{"historyElements":[{ + "id":"invalid-amount", + "blockHeight":1, + "timestamp":1, + "address":"stashAddress", + "reward":{"amount":"-1","era":1,"validator":"validatorAddress"} + }]}} + """ + ) + ) + } + + func testCanonicalRewardAmountAtMaximumSupportedWidthIsAccepted() throws { + let amount = String(repeating: "9", count: RewardAmountParser.maximumDecimalDigits) + let expectedAmount = try XCTUnwrap(BigUInt(amount)) + + let subqueryResponse = try decodeResponse( + try networkOperation(factory: SubqueryRewardOperationFactory(url: endpoint)), + json: subqueryJSON(amount: amount) + ) + let subsquidResponse = try decodeResponse( + try networkOperation(factory: ArrowsquidRewardOperationFactory(url: endpoint)), + json: subsquidJSON(amount: amount) + ) + + XCTAssertEqual(try mappedRewards(subqueryResponse).first?.amount, expectedAmount) + XCTAssertEqual(try mappedRewards(subsquidResponse).first?.amount, expectedAmount) + } + + func testNonCanonicalAndOversizedRewardAmountsAreRejectedBeforeBigUIntConstruction() throws { + let invalidAmounts = [ + "", + "+1", + "-1", + " 1", + "1 ", + "01", + "1.0", + "1e3", + "١", + String(repeating: "9", count: RewardAmountParser.maximumDecimalDigits + 1), + String(repeating: "9", count: 10000) + ] + + for amount in invalidAmounts { + XCTAssertThrowsError( + try decodeResponse( + try networkOperation(factory: SubqueryRewardOperationFactory(url: endpoint)), + json: subqueryJSON(amount: amount) + ) + ) + XCTAssertThrowsError( + try decodeResponse( + try networkOperation(factory: ArrowsquidRewardOperationFactory(url: endpoint)), + json: subsquidJSON(amount: amount) + ) + ) + } + } + + func testLegacyRewardItemsRemainBackwardCompatibleAndUnattributed() throws { + let response = StaticRewardHistoryResponse(items: [ + LegacyRewardHistoryItem( + id: "legacy", + type: .reward, + timestampInSeconds: "123", + blockNumber: 1, + amount: 99 + ) + ]) + + let rewards = try mappedRewards(response) + + XCTAssertEqual(rewards.count, 1) + XCTAssertEqual(rewards.first?.amount, 99) + XCTAssertEqual(rewards.first?.validatorAddress, "") + XCTAssertEqual(rewards.first?.era, 0) + } +} + +private extension RewardDataSourceTests { + func subqueryJSON(amount: String) -> String { + """ + {"data":{"delegators":{"nodes":[{ + "id":"stashAddress", + "delegatorHistoryElements":{"nodes":[{ + "id":"amount-test", + "blockNumber":1, + "amount":"\(amount)", + "type":2, + "timestamp":"1", + "delegatorId":"stashAddress", + "collatorId":"validatorAddress", + "roundId":"1" + }]} + }]}}} + """ + } + + func subsquidJSON(amount: String) -> String { + """ + {"data":{"historyElements":[{ + "id":"amount-test", + "blockHeight":1, + "timestamp":1, + "address":"stashAddress", + "reward":{ + "amount":"\(amount)", + "era":1, + "stash":"stashAddress", + "validator":"validatorAddress" + } + }]}} + """ + } + + func assertRejected( + factory: RewardOperationFactoryProtocol, + address: AccountAddress, + startTimestamp: Int64? = nil, + endTimestamp: Int64? = nil, + file: StaticString = #filePath, + line: UInt = #line + ) throws { + let operation = factory.createDelegatorRewardsOperation( + address: address, + startTimestamp: startTimestamp, + endTimestamp: endTimestamp + ) + OperationQueue().addOperations([operation], waitUntilFinished: true) + + switch try XCTUnwrap(operation.result, file: file, line: line) { + case .success: + XCTFail("Invalid reward query parameters were accepted", file: file, line: line) + case let .failure(error): + XCTAssertTrue(error is RewardHistoryRequestError, file: file, line: line) + } + } + + func networkOperation( + factory: RewardOperationFactoryProtocol, + startTimestamp: Int64? = nil, + endTimestamp: Int64? = nil + ) throws -> NetworkOperation { + let operation = factory.createDelegatorRewardsOperation( + address: stashAddress, + startTimestamp: startTimestamp, + endTimestamp: endTimestamp + ) + return try XCTUnwrap(operation as? NetworkOperation) + } + + func requestQuery(_ operation: NetworkOperation) throws -> String { + let request = try operation.requestFactory.createRequest() + let body = try XCTUnwrap(request.httpBody) + let json = try XCTUnwrap(JSONSerialization.jsonObject(with: body) as? [String: Any]) + return try XCTUnwrap(json["query"] as? String) + } + + func decodeResponse( + _ operation: NetworkOperation, + json: String + ) throws -> RewardHistoryResponseProtocol { + let response = try XCTUnwrap( + HTTPURLResponse( + url: endpoint, + statusCode: 200, + httpVersion: nil, + headerFields: nil + ) + ) + return try operation.resultFactory.createResult( + data: Data(json.utf8), + response: response, + error: nil + ).get() + } + + func mappedRewards(_ response: RewardHistoryResponseProtocol) throws -> [SubqueryRewardItemData] { + let source = ParachainSubqueryRewardsSource( + address: stashAddress, + url: endpoint, + operationFactory: RewardOperationFactoryStub(response: response) + ) + let wrapper = source.fetchOperation() + OperationQueue().addOperations(wrapper.allOperations, waitUntilFinished: true) + return try XCTUnwrap(wrapper.targetOperation.result).get() ?? [] + } + + func mappedWeeklyRewards(_ response: RewardHistoryResponseProtocol) throws -> [SubqueryRewardItemData] { + let source = ParachainWeaklyAnalyticsRewardSource( + address: stashAddress, + operationFactory: RewardOperationFactoryStub(response: response) + ) + let wrapper = source.fetchOperation() + OperationQueue().addOperations(wrapper.allOperations, waitUntilFinished: true) + return try XCTUnwrap(wrapper.targetOperation.result).get() ?? [] + } +} + +private enum RewardDataSourceTestError: Error { + case unsupported +} + +private final class RewardOperationFactoryStub: RewardOperationFactoryProtocol { + private let response: RewardHistoryResponseProtocol + + init(response: RewardHistoryResponseProtocol) { + self.response = response + } + + func createHistoryOperation( + address _: String, + startTimestamp _: Int64?, + endTimestamp _: Int64? + ) -> BaseOperation { + ClosureOperation { throw RewardDataSourceTestError.unsupported } + } + + func createDelegatorRewardsOperation( + address _: String, + startTimestamp _: Int64?, + endTimestamp _: Int64? + ) -> BaseOperation { + ClosureOperation { self.response } + } + + func createAprOperation( + for _: @escaping () throws -> [AccountId], + dependingOn _: BaseOperation + ) -> BaseOperation { + ClosureOperation { throw RewardDataSourceTestError.unsupported } + } + + func createLastRoundOperation() -> BaseOperation { + ClosureOperation { throw RewardDataSourceTestError.unsupported } + } +} + +private struct StaticRewardHistoryResponse: RewardHistoryResponseProtocol { + let items: [RewardHistoryItemProtocol] + + func rewardHistory(for _: String) -> [RewardHistoryItemProtocol] { + items + } +} + +private struct LegacyRewardHistoryItem: RewardHistoryItemProtocol { + let id: String + let type: SubqueryDelegationAction + let timestampInSeconds: String + let blockNumber: Int + let amount: BigUInt +} diff --git a/fearlessTests/FeatureToggleProviderTests.swift b/fearlessTests/FeatureToggleProviderTests.swift new file mode 100644 index 0000000000..d39ce627b0 --- /dev/null +++ b/fearlessTests/FeatureToggleProviderTests.swift @@ -0,0 +1,161 @@ +import RobinHood +import SSFNetwork +import XCTest +@testable import fearless + +final class FeatureToggleProviderTests: XCTestCase { + func testNilNetworkConfigFallsBackAndFinishesFetchOperation() throws { + let context = makeProvider(networkPayload: Data("null".utf8), suspendNetwork: true) + let fetchOperation = context.provider.fetchConfigOperation() + + let config = try execute(fetchOperation, on: context.fetchQueue) { + context.networkQueue.isSuspended = false + } + assertDefault(config) + } + + func testMalformedNetworkConfigFallsBackAndFinishesFetchOperation() throws { + let context = makeProvider(networkPayload: Data("{".utf8)) + let config = try execute(context.provider.fetchConfigOperation(), on: context.fetchQueue) + + assertDefault(config) + } + + func testValidNetworkConfigIsDeliveredWithoutDefaultSubstitution() throws { + let context = makeProvider( + networkPayload: Data( + #"{"pendulumCaseEnabled":true,"nftEnabled":false}"#.utf8 + ) + ) + let config = try execute(context.provider.fetchConfigOperation(), on: context.fetchQueue) + + XCTAssertEqual(config.pendulumCaseEnabled, true) + XCTAssertEqual(config.nftEnabled, false) + } + + func testConcurrentPendingFetchesAllResolveExactlyOnce() throws { + let context = makeProvider( + networkPayload: Data( + #"{"pendulumCaseEnabled":true,"nftEnabled":false}"#.utf8 + ), + suspendNetwork: true + ) + let operations = (0 ..< 32).map { _ in context.provider.fetchConfigOperation() } + let completion = expectation(description: "all fetches resolve") + completion.expectedFulfillmentCount = operations.count + completion.assertForOverFulfill = true + + operations.forEach { operation in + operation.completionBlock = { + completion.fulfill() + } + context.fetchQueue.addOperation(operation) + } + + context.networkQueue.isSuspended = false + wait(for: [completion], timeout: 3) + + for operation in operations { + let config = try result(of: operation) + XCTAssertEqual(config.pendulumCaseEnabled, true) + XCTAssertEqual(config.nftEnabled, false) + } + } + + func testFetchCreatedBeforeProviderDeallocationReturnsDefault() throws { + let networkQueue = OperationQueue() + networkQueue.isSuspended = true + var provider: FeatureToggleProvider? = FeatureToggleProvider( + networkOperationFactory: JSONNetworkOperationFactoryStub(payload: Data("null".utf8)), + operationQueue: networkQueue + ) + let fetchOperation = try XCTUnwrap(provider).fetchConfigOperation() + provider = nil + + let config = try execute(fetchOperation, on: OperationQueue()) + assertDefault(config) + + networkQueue.cancelAllOperations() + networkQueue.isSuspended = false + } + + private func makeProvider( + networkPayload: Data, + suspendNetwork: Bool = false + ) -> ProviderContext { + let networkQueue = OperationQueue() + networkQueue.isSuspended = suspendNetwork + let fetchQueue = OperationQueue() + fetchQueue.maxConcurrentOperationCount = 8 + + return ProviderContext( + provider: FeatureToggleProvider( + networkOperationFactory: JSONNetworkOperationFactoryStub(payload: networkPayload), + operationQueue: networkQueue + ), + networkQueue: networkQueue, + fetchQueue: fetchQueue + ) + } + + private func execute( + _ operation: BaseOperation, + on queue: OperationQueue, + beforeWait: () -> Void = {} + ) throws -> FeatureToggleConfig { + let completion = expectation(description: "feature toggle fetch completes") + operation.completionBlock = { + completion.fulfill() + } + queue.addOperation(operation) + beforeWait() + wait(for: [completion], timeout: 3) + return try result(of: operation) + } + + private func result( + of operation: BaseOperation + ) throws -> FeatureToggleConfig { + switch operation.result { + case let .success(config): + return config + case let .failure(error): + throw error + case .none: + throw FeatureToggleProviderTestError.missingOperationResult + } + } + + private func assertDefault( + _ config: FeatureToggleConfig, + file: StaticString = #filePath, + line: UInt = #line + ) { + XCTAssertEqual(config.pendulumCaseEnabled, false, file: file, line: line) + XCTAssertEqual(config.nftEnabled, true, file: file, line: line) + } +} + +private enum FeatureToggleProviderTestError: Error { + case missingOperationResult +} + +private struct ProviderContext { + let provider: FeatureToggleProvider + let networkQueue: OperationQueue + let fetchQueue: OperationQueue +} + +private final class JSONNetworkOperationFactoryStub: NetworkOperationFactoryProtocol { + private let payload: Data + + init(payload: Data) { + self.payload = payload + } + + func fetchData(from _: URL) -> BaseOperation { + ClosureOperation { + try JSONDecoder().decode(T.self, from: self.payload) + } + } +} diff --git a/fearlessTests/Modules/AnalyticsValidators/AnalyticsValidatorsTests.swift b/fearlessTests/Modules/AnalyticsValidators/AnalyticsValidatorsTests.swift index 0fdc234298..7e0ba0c8bd 100644 --- a/fearlessTests/Modules/AnalyticsValidators/AnalyticsValidatorsTests.swift +++ b/fearlessTests/Modules/AnalyticsValidators/AnalyticsValidatorsTests.swift @@ -1,15 +1,313 @@ +import BigInt +import SSFModels +import SSFUtils import XCTest -class AnalyticsValidatorsTests: XCTestCase { - override func setUp() { - // Put setup code here. This method is called before the invocation of each test method in the class. +@testable import fearless + +final class AnalyticsValidatorsTests: XCTestCase { + private let stashAddress = "stash-address" + private let locale = Locale(identifier: "en_US") + + func testRewardsAllocationUsesOnlyUniqueAttributableRewardEvents() throws { + let context = try makeContext(precision: 2) + let nomination = try makeNomination( + targets: [ + context.firstAccountId, + context.secondAccountId, + context.firstAccountId, + Data([0x01]) + ] + ) + let otherAccountId = Data(repeating: 0x33, count: 32) + let otherAddress = try AddressFactory.address(for: otherAccountId, chain: context.chain) + let rewards = [ + reward(id: "first", validatorAddress: context.firstAddress, amount: 100), + reward(id: "second", validatorAddress: context.secondAddress, amount: 300), + reward(id: "second", validatorAddress: context.secondAddress, amount: 300), + reward(id: "slash", validatorAddress: context.firstAddress, amount: 9900, isReward: false), + reward(id: "other-stash", validatorAddress: context.firstAddress, amount: 10000, stashAddress: "other"), + reward(id: "missing-validator", validatorAddress: "", amount: 20000), + reward(id: "old-validator", validatorAddress: otherAddress, amount: 30000), + reward(id: " ", validatorAddress: context.firstAddress, amount: 40000) + ] + + let viewModel = context.factory.createViewModel( + eraValidatorInfos: [], + eraRange: (start: 1, end: 1), + stashAddress: stashAddress, + rewards: rewards, + nomination: nomination, + identitiesByAddress: nil, + page: .rewards, + locale: locale + ) + + XCTAssertEqual(viewModel.validators.count, 2) + XCTAssertEqual(viewModel.validators.map(\.validatorAddress), [context.secondAddress, context.firstAddress]) + XCTAssertEqual(viewModel.validators.map(\.amount), [3.0, 1.0]) + XCTAssertEqual(viewModel.validators[0].progressPercents, 0.75, accuracy: 0.000_001) + XCTAssertEqual(viewModel.validators[1].progressPercents, 0.25, accuracy: 0.000_001) + XCTAssertEqual(viewModel.pieChartSegmentValues.reduce(0.0, +), 1.0, accuracy: 0.000_001) + XCTAssertTrue(viewModel.pieChartSegmentValues.allSatisfy { $0.isFinite && (0.0 ... 1.0).contains($0) }) + XCTAssertEqual(viewModel.validators.map(\.mainValueText), ["3", "1"]) + XCTAssertEqual( + viewModel.validators.map(\.secondaryValueText), + [formattedPercent(0.75, locale: locale), formattedPercent(0.25, locale: locale)] + ) + XCTAssertEqual( + centerLines(viewModel), + ["Received rewards", "4", formattedPercent(1.0, locale: locale)] + ) + let placeholderMarker = ["TO", "DO"].joined() + XCTAssertFalse(viewModel.chartCenterText.string.contains(placeholderMarker)) + } + + func testMissingAndMalformedValidatorAttributionFailsClosed() throws { + let context = try makeContext(precision: 2) + let nomination = try makeNomination(targets: [context.firstAccountId, context.secondAccountId]) + let rewards = [ + reward(id: "missing", validatorAddress: "", amount: 500), + reward(id: "malformed", validatorAddress: "not-an-address", amount: 700) + ] + + let viewModel = context.factory.createViewModel( + eraValidatorInfos: [], + eraRange: (start: 1, end: 1), + stashAddress: stashAddress, + rewards: rewards, + nomination: nomination, + identitiesByAddress: nil, + page: .rewards, + locale: locale + ) + + XCTAssertEqual(viewModel.validators.map(\.amount), [0.0, 0.0]) + XCTAssertEqual(viewModel.pieChartSegmentValues, [0.0, 0.0]) + XCTAssertTrue(viewModel.pieChartSegmentValues.allSatisfy { $0.isFinite && !$0.isNaN }) + XCTAssertEqual( + centerLines(viewModel), + ["Received rewards", "0", formattedPercent(0.0, locale: locale)] + ) + } + + func testExtremeRewardAmountFailsClosedWithoutNaNOrInfinity() throws { + let context = try makeContext(precision: 2) + let nomination = try makeNomination(targets: [context.firstAccountId]) + let hugeAmount = try XCTUnwrap(BigUInt(String(repeating: "9", count: 200))) + let rewards = [ + reward(id: "normal", validatorAddress: context.firstAddress, amount: 100), + reward(id: "extreme", validatorAddress: context.firstAddress, amount: hugeAmount) + ] + + let viewModel = context.factory.createViewModel( + eraValidatorInfos: [], + eraRange: (start: 1, end: 1), + stashAddress: stashAddress, + rewards: rewards, + nomination: nomination, + identitiesByAddress: nil, + page: .rewards, + locale: locale + ) + + XCTAssertEqual(viewModel.validators.first?.amount, 0.0) + XCTAssertEqual(viewModel.validators.first?.progressPercents, 0.0) + XCTAssertEqual(viewModel.pieChartSegmentValues, [0.0]) + XCTAssertTrue(viewModel.pieChartSegmentValues.allSatisfy { $0.isFinite && !$0.isNaN }) + XCTAssertEqual(centerLines(viewModel), ["Received rewards", "0", formattedPercent(0.0, locale: locale)]) + } + + func testUnsupportedAssetPrecisionFailsClosed() throws { + let context = try makeContext(precision: .max) + let nomination = try makeNomination(targets: [context.firstAccountId]) + let viewModel = context.factory.createViewModel( + eraValidatorInfos: [], + eraRange: (start: 1, end: 1), + stashAddress: stashAddress, + rewards: [reward(id: "reward", validatorAddress: context.firstAddress, amount: 100)], + nomination: nomination, + identitiesByAddress: nil, + page: .rewards, + locale: locale + ) + + XCTAssertEqual(viewModel.validators.first?.amount, 0.0) + XCTAssertEqual(viewModel.validators.first?.progressPercents, 0.0) + XCTAssertEqual(centerLines(viewModel), ["Received rewards", "0", formattedPercent(0.0, locale: locale)]) + } + + func testOutOfRangeEraDataIsExcludedFromActivityPercentages() throws { + let context = try makeContext(precision: 2) + let nomination = try makeNomination(targets: [context.firstAccountId, context.secondAccountId]) + let eraInfos = [ + try makeEraInfo(address: context.firstAddress, era: 10), + try makeEraInfo(address: context.firstAddress, era: 11), + try makeEraInfo(address: context.firstAddress, era: 11), + try makeEraInfo(address: context.firstAddress, era: 100), + try makeEraInfo(address: context.secondAddress, era: 12) + ] + + let viewModel = context.factory.createViewModel( + eraValidatorInfos: eraInfos, + eraRange: (start: 10, end: 12), + stashAddress: stashAddress, + rewards: [], + nomination: nomination, + identitiesByAddress: nil, + page: .activity, + locale: locale + ) + + XCTAssertEqual(viewModel.validators.map(\.amount), [2.0, 1.0]) + XCTAssertEqual(viewModel.validators[0].progressPercents, 2.0 / 3.0, accuracy: 0.000_001) + XCTAssertEqual(viewModel.validators[1].progressPercents, 1.0 / 3.0, accuracy: 0.000_001) + XCTAssertEqual(viewModel.pieChartInactiveSegment?.percents, 0.0) + XCTAssertEqual(viewModel.pieChartInactiveSegment?.eraCount, 0) + XCTAssertTrue(viewModel.pieChartSegmentValues.allSatisfy { $0.isFinite && (0.0 ... 1.0).contains($0) }) + } + + func testMalformedTargetsAndEraRangeBoundariesDoNotCrashOrProduceInvalidValues() throws { + let context = try makeContext(precision: 2) + let nomination = try makeNomination( + targets: [context.firstAccountId, Data([0x01]), context.firstAccountId] + ) + let ranges: [EraRange] = [ + (start: 10, end: 9), + (start: .max, end: .max) + ] + + for eraRange in ranges { + let viewModel = context.factory.createViewModel( + eraValidatorInfos: [], + eraRange: eraRange, + stashAddress: stashAddress, + rewards: [], + nomination: nomination, + identitiesByAddress: nil, + page: .activity, + locale: locale + ) + + XCTAssertEqual(viewModel.validators.count, 1) + XCTAssertEqual(viewModel.validators.first?.progressPercents, 0.0) + XCTAssertEqual(viewModel.pieChartInactiveSegment?.percents, 1.0) + XCTAssertEqual(viewModel.pieChartInactiveSegment?.eraCount, 1) + XCTAssertTrue(viewModel.pieChartSegmentValues.allSatisfy { $0.isFinite && !$0.isNaN }) + } + } + + func testReceivedRewardsTitleIsLocalized() throws { + let context = try makeContext(precision: 2) + let nomination = try makeNomination(targets: [context.firstAccountId]) + let expectations: [(String, String)] = [ + ("en", "Received rewards"), + ("ru", "Полученные вознаграждения"), + ("ja", "受け取った報酬"), + ("zh-Hans", "已收到的奖励") + ] + + for (identifier, expectedTitle) in expectations { + let viewModel = context.factory.createViewModel( + eraValidatorInfos: [], + eraRange: (start: 1, end: 1), + stashAddress: stashAddress, + rewards: [], + nomination: nomination, + identitiesByAddress: nil, + page: .rewards, + locale: Locale(identifier: identifier) + ) + + XCTAssertEqual(centerLines(viewModel).first, expectedTitle) + } + } +} + +private extension AnalyticsValidatorsTests { + struct Context { + let chain: ChainModel + let firstAccountId: Data + let secondAccountId: Data + let firstAddress: AccountAddress + let secondAddress: AccountAddress + let factory: AnalyticsValidatorsViewModelFactory + } + + func makeContext(precision: UInt16) throws -> Context { + let chain = ChainModelGenerator.generateChain( + generatingAssets: 0, + addressPrefix: 42, + assetPresicion: precision + ) + let asset = ChainModelGenerator.generateAssetWithId( + "analytics-asset", + symbol: "TST", + assetPresicion: precision + ) + let firstAccountId = Data(repeating: 0x11, count: 32) + let secondAccountId = Data(repeating: 0x22, count: 32) + let firstAddress = try AddressFactory.address(for: firstAccountId, chain: chain) + let secondAddress = try AddressFactory.address(for: secondAccountId, chain: chain) + let factory = AnalyticsValidatorsViewModelFactory( + balanceViewModelFactory: StubBalanceViewModelFactory(), + chain: chain, + asset: asset, + iconGenerator: UniversalIconGenerator() + ) + + return Context( + chain: chain, + firstAccountId: firstAccountId, + secondAccountId: secondAccountId, + firstAddress: firstAddress, + secondAddress: secondAddress, + factory: factory + ) + } + + func makeNomination(targets: [Data]) throws -> Nomination { + let payload: [String: Any] = [ + "targets": targets.map { $0.toHex(includePrefix: true) }, + "submittedIn": "0" + ] + let data = try JSONSerialization.data(withJSONObject: payload) + return try JSONDecoder().decode(Nomination.self, from: data) + } + + func makeEraInfo(address: AccountAddress, era: EraIndex) throws -> SubqueryEraValidatorInfo { + let json = JSON.dictionaryValue([ + "address": .stringValue(address), + "era": .unsignedIntValue(UInt64(era)) + ]) + return try XCTUnwrap(SubqueryEraValidatorInfo(from: json)) + } + + func reward( + id: String, + validatorAddress: AccountAddress, + amount: BigUInt, + stashAddress: AccountAddress? = nil, + isReward: Bool = true + ) -> SubqueryRewardItemData { + SubqueryRewardItemData( + eventId: id, + timestamp: 0, + validatorAddress: validatorAddress, + era: 0, + stashAddress: stashAddress ?? self.stashAddress, + amount: amount, + isReward: isReward + ) } - override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. + func formattedPercent(_ value: Double, locale: Locale) -> String { + let formatter = NumberFormatter.percent + formatter.locale = locale + return formatter.string(from: value as NSNumber) ?? "" } - func testExample() throws { - throw XCTSkip("Placeholder test: to be implemented") + func centerLines(_ viewModel: AnalyticsValidatorsViewModel) -> [String] { + viewModel.chartCenterText.string.components(separatedBy: "\n") } } diff --git a/fearlessTests/Modules/WalletSendConfirm/WalletSendConfirmTests.swift b/fearlessTests/Modules/WalletSendConfirm/WalletSendConfirmTests.swift index dd8f2c676d..a9f34b244b 100644 --- a/fearlessTests/Modules/WalletSendConfirm/WalletSendConfirmTests.swift +++ b/fearlessTests/Modules/WalletSendConfirm/WalletSendConfirmTests.swift @@ -1,16 +1,70 @@ import XCTest +@testable import fearless -class WalletSendConfirmTests: XCTestCase { +final class WalletSendConfirmTests: XCTestCase { + func testTonUnknownOutcomeRecoveryAcceptsExactLowercaseHash() throws { + let hash = String(repeating: "0123456789abcdef", count: 4) - override func setUp() { - // Put setup code here. This method is called before the invocation of each test method in the class. + let recovery = try XCTUnwrap( + TonUnknownOutcomeRecoveryModel(messageHashHex: hash) + ) + + XCTAssertEqual(recovery.messageHashHex, hash) } - override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. + func testTonUnknownOutcomeRecoveryRejectsMalformedOrDisplaySpoofedHashes() { + let malformedHashes = [ + "", + String(repeating: "0", count: 63), + String(repeating: "0", count: 65), + String(repeating: "A", count: 64), + String(repeating: "g", count: 64), + String(repeating: "0", count: 64), + String(repeating: "0", count: 63) + "\n", + String(repeating: "0", count: 32) + "\u{200B}" + String(repeating: "0", count: 31) + ] + + for hash in malformedHashes { + XCTAssertNil( + TonUnknownOutcomeRecoveryModel(messageHashHex: hash), + hash.debugDescription + ) + } } - func testExample() throws { - throw XCTSkip("Placeholder test: to be implemented") + func testTonExactFeeFormatterPreservesTheNinthDecimalAcrossLocales() throws { + let exactOneNanoton = try XCTUnwrap(Decimal(string: "0.000000001")) + let exactNinthDigit = try XCTUnwrap(Decimal(string: "0.100000001")) + let roundedNeighbor = try XCTUnwrap(Decimal(string: "0.1")) + let locales = ["en_US", "id_ID", "ja_JP", "pt_PT", "ru_RU", "tr_TR", "vi_VN", "zh_Hans"] + + for identifier in locales { + let formatter = NumberFormatter.formatter( + for: .exactCrypto(fractionDigits: 9), + locale: Locale(identifier: identifier) + ) + let oneNanoton = try XCTUnwrap( + formatter.string(from: NSDecimalNumber(decimal: exactOneNanoton)) + ) + let ninthDigit = try XCTUnwrap( + formatter.string(from: NSDecimalNumber(decimal: exactNinthDigit)) + ) + let neighbor = try XCTUnwrap( + formatter.string(from: NSDecimalNumber(decimal: roundedNeighbor)) + ) + + XCTAssertNotEqual(oneNanoton, formatter.string(from: 0), identifier) + XCTAssertNotEqual(ninthDigit, neighbor, identifier) + XCTAssertEqual( + formatter.number(from: oneNanoton)?.decimalValue, + exactOneNanoton, + identifier + ) + XCTAssertEqual( + formatter.number(from: ninthDigit)?.decimalValue, + exactNinthDigit, + identifier + ) + } } } diff --git a/fearlessTests/PasskeyBackupCredentialResponseSerializerTests.swift b/fearlessTests/PasskeyBackupCredentialResponseSerializerTests.swift new file mode 100644 index 0000000000..08a6b284d4 --- /dev/null +++ b/fearlessTests/PasskeyBackupCredentialResponseSerializerTests.swift @@ -0,0 +1,2532 @@ +import AuthenticationServices +import CryptoKit +import XCTest +@testable import fearless + +final class PasskeyCredentialResponseSerializerTests: XCTestCase { + func testRegistrationSerializerProducesWebAuthnEnvelopeAndBase64URLFields() throws { + let json = try PasskeyCredentialResponseSerializer.registrationJSON( + credentialID: Data([0xFB, 0xFF]), + clientDataJSON: Data([0x00, 0x01]), + attestationObject: Data([0x02, 0x03]), + authenticatorData: Data([0x04, 0x05]) + ) + let credential = try jsonObject(json) + let response = try XCTUnwrap(credential["response"] as? [String: Any]) + let extensions = try XCTUnwrap(credential["clientExtensionResults"] as? [String: Any]) + + XCTAssertEqual( + Set(credential.keys), + Set([ + "id", "rawId", "response", "type", + "clientExtensionResults", "authenticatorAttachment" + ]) + ) + XCTAssertEqual(credential["id"] as? String, "-_8") + XCTAssertEqual(credential["rawId"] as? String, "-_8") + XCTAssertEqual(credential["type"] as? String, "public-key") + XCTAssertEqual(credential["authenticatorAttachment"] as? String, "platform") + XCTAssertTrue(extensions.isEmpty) + XCTAssertEqual( + Set(response.keys), + Set(["clientDataJSON", "attestationObject", "authenticatorData"]) + ) + XCTAssertEqual(response["clientDataJSON"] as? String, "AAE") + XCTAssertEqual(response["attestationObject"] as? String, "AgM") + XCTAssertEqual(response["authenticatorData"] as? String, "BAU") + } + + func testChallengeClientAuthorizesExactTransmittedBody() async throws { + let challenge = Data(repeating: 7, count: 32) + let userID = Data(repeating: 8, count: 32) + let response = """ + { + "registrationId":"registration-1234", + "challenge":"\(base64URL(challenge))", + "userId":"\(base64URL(userID))", + "userName":"alice@example.com", + "displayName":"Alice", + "storageKey":"wallet-1234", + "rpId":"fearlesswallet.io", + "schemaVersion":1 + } + """ + let transport = AuthorizationTestTransport( + responses: [PasskeyBackupHTTPResponse(statusCode: 200, body: Data(response.utf8))] + ) + let authorization = AuthorizationTestProvider(token: "test.authorization-token_123") + let service = try HTTPPasskeyBackupChallengeService( + baseURL: "https://backup.fearlesswallet.io", + transport: transport, + authorizationProvider: authorization + ) + + _ = try await service.registrationChallenge( + walletId: "wallet-001", + accountName: "alice@example.com", + displayName: "Alice" + ) + + let request = try XCTUnwrap(transport.requests.first) + let authorizationRequest = try XCTUnwrap(authorization.requests.first) + XCTAssertEqual(request.headers["Authorization"], "Bearer test.authorization-token_123") + XCTAssertEqual(authorizationRequest.method, "POST") + XCTAssertEqual( + authorizationRequest.path, + "/api/passkey-backup/v1/registration/challenge" + ) + XCTAssertEqual( + authorizationRequest.bodySha256, + sha256Base64URL(try XCTUnwrap(request.body)) + ) + } + + func testChallengeClientFailsClosedWithoutAuthorizationProvider() async throws { + let transport = AuthorizationTestTransport(responses: []) + let service = try HTTPPasskeyBackupChallengeService( + baseURL: "https://backup.fearlesswallet.io", + transport: transport + ) + + do { + _ = try await service.assertionChallenge(storageKey: "wallet-1234") + XCTFail("Unavailable authorization provider unexpectedly allowed a request") + } catch { + XCTAssertEqual(error as? PasskeyBackupError, .unavailableAuthorization) + } + XCTAssertTrue(transport.requests.isEmpty) + } + + func testChallengeClientRejectsOversizedInjectedTransportResponsesBeforeStatusOrJSON() async throws { + for statusCode in [200, 503] { + let transport = AuthorizationTestTransport( + responses: [ + PasskeyBackupHTTPResponse( + statusCode: statusCode, + body: Data( + repeating: 0x41, + count: PasskeyBackupHTTPTransportPolicy.maximumResponseBytes + 1 + ) + ) + ] + ) + let service = try HTTPPasskeyBackupChallengeService( + baseURL: "https://backup.fearlesswallet.io", + transport: transport, + authorizationProvider: AuthorizationTestProvider(token: "test-token") + ) + + do { + _ = try await service.assertionChallenge(storageKey: "wallet-1234") + XCTFail("Oversized response unexpectedly passed for HTTP \(statusCode)") + } catch { + XCTAssertEqual( + error as? PasskeyBackupError, + .challengeServiceResponseTooLarge + ) + } + } + } + + func testChallengeClientRejectsMalformedBearerTokensBeforeTransport() async throws { + let malformedTokens = [ + "", + " token", + "token ", + "two tokens", + "token\nvalue", + "token,value", + "Bearer token", + "abc=def", + String(repeating: "a", count: 4097) + ] + + for token in malformedTokens { + let transport = AuthorizationTestTransport(responses: []) + let service = try HTTPPasskeyBackupChallengeService( + baseURL: "https://backup.fearlesswallet.io", + transport: transport, + authorizationProvider: AuthorizationTestProvider(token: token) + ) + + do { + _ = try await service.assertionChallenge(storageKey: "wallet-1234") + XCTFail("Malformed token unexpectedly allowed: \(token.prefix(32))") + } catch { + XCTAssertEqual(error as? PasskeyBackupError, .invalidAuthorizationToken) + } + XCTAssertTrue(transport.requests.isEmpty) + } + } + + func testAuthorizationRequestAcceptsOnlyExactServicePaths() throws { + let digest = base64URL(Data(repeating: 0, count: 32)) + let allowedPaths = [ + PasskeyBackupAuthorizationRequest.registrationChallengePath, + PasskeyBackupAuthorizationRequest.registrationCompletePath, + PasskeyBackupAuthorizationRequest.assertionChallengePath, + PasskeyBackupAuthorizationRequest.assertionCompletePath, + PasskeyBackupAuthorizationRequest.credentialsListPath, + PasskeyBackupAuthorizationRequest.credentialsRevokePath, + PasskeyBackupAuthorizationRequest.credentialsRevokeAllPath + ] + for path in allowedPaths { + XCTAssertNoThrow( + try PasskeyBackupAuthorizationRequest( + method: "POST", + path: path, + bodySha256: digest + ) + ) + } + + let rejectedPaths = [ + "/api/passkey-backup/v1/registration/challenge/extra", + "/api/passkey-backup/v1/registration/../assertion/challenge", + "/api/passkey-backup/v1/%2e%2e/admin", + "/api/passkey-backup/v1/admin", + "/api/passkey-backup/v1/assertion/challenge?scope=admin", + "/api/passkey-backup/v1/credentials/revoke-all?admin=true", + "/api/passkey-backup/v1/credentials/revoke-all/", + "/api/passkey-backup/v1/credentials/revoke-all%2F..%2Flist", + "//api/passkey-backup/v1/assertion/challenge", + "https://backup.fearlesswallet.io/api/passkey-backup/v1/assertion/challenge", + "" + ] + for path in rejectedPaths { + XCTAssertThrowsError( + try PasskeyBackupAuthorizationRequest( + method: "POST", + path: path, + bodySha256: digest + ), + "Unexpectedly accepted authorization path: \(path)" + ) + } + } + + func testAuthorizationRequestRejectsNoncanonicalBodyDigests() { + let path = PasskeyBackupAuthorizationRequest.assertionChallengePath + let canonicalDigest = base64URL(Data(repeating: 0, count: 32)) + let rejectedDigests = [ + String(canonicalDigest.dropLast()), + canonicalDigest + "=", + String(canonicalDigest.dropLast()) + "B", + String(canonicalDigest.dropLast()) + "+", + " " + canonicalDigest, + "" + ] + for digest in rejectedDigests { + XCTAssertThrowsError( + try PasskeyBackupAuthorizationRequest( + method: "POST", + path: path, + bodySha256: digest + ), + "Unexpectedly accepted authorization digest: \(digest)" + ) + } + } + + func testBase64URLDecoderRejectsAliasesPaddingAndWhitespace() throws { + XCTAssertEqual(try PasskeyBackupContract.decodeBase64URL("-_8"), Data([0xFB, 0xFF])) + for value in ["-_8=", "+/8=", " -_8", "-_8 ", "A"] { + XCTAssertThrowsError(try PasskeyBackupContract.decodeBase64URL(value)) + } + } + + func testAccountAndDisplayNameValidationMatchesPublicContract() throws { + XCTAssertThrowsError(try PasskeyBackupContract.validateAccountName("alice@@example.com")) + XCTAssertThrowsError( + try PasskeyBackupRegistrationChallenge( + registrationId: "registration-1234", + challenge: Data(repeating: 1, count: 32), + userId: Data(repeating: 2, count: 32), + userName: "alice@example.com", + displayName: String(repeating: "A", count: 129), + storageKey: "wallet-1234" + ) + ) + } + + func testChallengeClientRejectsFractionalSchemaVersion() async throws { + let response = """ + { + "registrationId":"registration-1234", + "challenge":"\(base64URL(Data(repeating: 1, count: 32)))", + "userId":"\(base64URL(Data(repeating: 2, count: 32)))", + "userName":"alice@example.com", + "displayName":"Alice", + "storageKey":"wallet-1234", + "rpId":"fearlesswallet.io", + "schemaVersion":1.5 + } + """ + let transport = AuthorizationTestTransport( + responses: [PasskeyBackupHTTPResponse(statusCode: 200, body: Data(response.utf8))] + ) + let service = try HTTPPasskeyBackupChallengeService( + baseURL: "https://backup.fearlesswallet.io", + transport: transport, + authorizationProvider: AuthorizationTestProvider(token: "test-token") + ) + + do { + _ = try await service.registrationChallenge( + walletId: "wallet-001", + accountName: "alice@example.com", + displayName: "Alice" + ) + XCTFail("Fractional schemaVersion unexpectedly passed") + } catch { + XCTAssertEqual(error as? PasskeyBackupError, .malformedChallengeServiceResponse) + } + } + + func testChallengeClientRejectsNon200SuccessStatuses() async throws { + let response = """ + { + "assertionId":"assertion-1234", + "challenge":"\(base64URL(Data(repeating: 1, count: 32)))", + "storageKey":"wallet-1234", + "rpId":"fearlesswallet.io", + "schemaVersion":1 + } + """ + + for statusCode in [201, 204] { + let transport = AuthorizationTestTransport( + responses: [ + PasskeyBackupHTTPResponse( + statusCode: statusCode, + body: Data(response.utf8) + ) + ] + ) + let service = try HTTPPasskeyBackupChallengeService( + baseURL: "https://backup.fearlesswallet.io", + transport: transport, + authorizationProvider: AuthorizationTestProvider(token: "test-token") + ) + + do { + _ = try await service.assertionChallenge(storageKey: "wallet-1234") + XCTFail("HTTP \(statusCode) unexpectedly passed") + } catch { + XCTAssertEqual( + error as? PasskeyBackupError, + .challengeServiceHTTPStatus(statusCode) + ) + } + } + } + + func testRegistrationCompletionDistinguishesExplicitRejectionFromUncertainSuccessResponse() async throws { + for statusCode in [400, 401, 403, 404, 409, 422] { + let explicit = try HTTPPasskeyBackupChallengeService( + baseURL: "https://backup.fearlesswallet.io", + transport: AuthorizationTestTransport( + responses: [PasskeyBackupHTTPResponse(statusCode: statusCode, body: Data())] + ), + authorizationProvider: AuthorizationTestProvider(token: "test-token") + ) + do { + _ = try await explicit.completeRegistration( + registrationId: "registration-1234", + credentialResponseJSON: #"{"id":"credential"}"# + ) + XCTFail("Explicit registration rejection unexpectedly passed") + } catch { + XCTAssertEqual( + error as? PasskeyBackupError, + .challengeServiceHTTPStatus(statusCode) + ) + } + } + + let uncertainResponses = [ + PasskeyBackupHTTPResponse(statusCode: 408, body: Data()), + PasskeyBackupHTTPResponse(statusCode: 425, body: Data()), + PasskeyBackupHTTPResponse(statusCode: 429, body: Data()), + PasskeyBackupHTTPResponse(statusCode: 500, body: Data()), + PasskeyBackupHTTPResponse(statusCode: 503, body: Data()), + PasskeyBackupHTTPResponse(statusCode: 200, body: Data()), + PasskeyBackupHTTPResponse( + statusCode: 200, + body: Data(#"{"not":"the-contract"}"#.utf8) + ) + ] + for response in uncertainResponses { + let uncertain = try HTTPPasskeyBackupChallengeService( + baseURL: "https://backup.fearlesswallet.io", + transport: AuthorizationTestTransport( + responses: [response] + ), + authorizationProvider: AuthorizationTestProvider(token: "test-token") + ) + do { + _ = try await uncertain.completeRegistration( + registrationId: "registration-1234", + credentialResponseJSON: #"{"id":"credential"}"# + ) + XCTFail("Uncertain registration response unexpectedly passed") + } catch { + XCTAssertEqual( + error as? PasskeyBackupError, + .registrationCompletionOutcomeUnknown + ) + } + } + } + + func testHTTP500AfterCommittedRegistrationTriggersCredentialCompensation() async throws { + let credentialId = "Y3JlZC0x" + let transport = AuthorizationTestTransport( + responses: [ + PasskeyBackupHTTPResponse(statusCode: 500, body: Data()), + httpJSON( + #"{"storageKey":"wallet-1234","credentialId":"\#(credentialId)","remainingCredentials":0,"rpId":"fearlesswallet.io","schemaVersion":1}"# + ) + ] + ) + let authorization = AuthorizationTestProvider(token: "test-token") + let service = try HTTPPasskeyBackupChallengeService( + baseURL: "https://backup.fearlesswallet.io", + transport: transport, + authorizationProvider: authorization + ) + let workflow = try PasskeyBackupWorkflow( + challengeService: service, + cloudStorage: LifecycleTestCloudStorage(record: nil), + backupKeyProvider: FixedTestBackupKeyProvider(), + isReleaseEnabled: true, + createdAtMillisProvider: { 1_767_225_600_000 } + ) + + do { + _ = try await workflow.finishRegistrationWithPlaintext( + pending: self.pendingRegistration(), + credentialResponseJSON: #"{"id":"\#(credentialId)"}"#, + plaintextBackup: Data([1, 2, 3]) + ) + XCTFail("Committed-then-500 registration unexpectedly passed") + } catch { + XCTAssertEqual( + error as? PasskeyBackupError, + .registrationCompletionOutcomeUnknown + ) + } + + XCTAssertEqual( + authorization.requests.map(\.path), + [ + PasskeyBackupAuthorizationRequest.registrationCompletePath, + PasskeyBackupAuthorizationRequest.credentialsRevokePath + ] + ) + XCTAssertEqual(transport.requests.count, 2) + let revokeBody = try XCTUnwrap(transport.requests[1].body) + let revokeJSON = try XCTUnwrap( + JSONSerialization.jsonObject(with: revokeBody) as? [String: Any] + ) + XCTAssertEqual(revokeJSON["credentialId"] as? String, credentialId) + } + + func testChallengeClientRejectsUnknownResponseFields() async throws { + let response = """ + { + "assertionId":"assertion-1234", + "challenge":"\(base64URL(Data(repeating: 1, count: 32)))", + "storageKey":"wallet-1234", + "rpId":"fearlesswallet.io", + "schemaVersion":1, + "unexpected":true + } + """ + let transport = AuthorizationTestTransport( + responses: [PasskeyBackupHTTPResponse(statusCode: 200, body: Data(response.utf8))] + ) + let service = try HTTPPasskeyBackupChallengeService( + baseURL: "https://backup.fearlesswallet.io", + transport: transport, + authorizationProvider: AuthorizationTestProvider(token: "test-token") + ) + + do { + _ = try await service.assertionChallenge(storageKey: "wallet-1234") + XCTFail("Unknown response field unexpectedly passed") + } catch { + XCTAssertEqual(error as? PasskeyBackupError, .malformedChallengeServiceResponse) + } + } + + func testChallengeClientRejectsNoncanonicalBaseURLAliases() { + let rejected = [ + " https://backup.fearlesswallet.io", + "https://backup.fearlesswallet.io ", + "https://backup.fearlesswallet.io:443", + "https://backup.fearlesswallet.io/api", + "https://backup.fearlesswallet.io//", + "https://BACKUP.fearlesswallet.io" + ] + + for baseURL in rejected { + XCTAssertThrowsError( + try HTTPPasskeyBackupChallengeService( + baseURL: baseURL, + transport: AuthorizationTestTransport(responses: []), + authorizationProvider: AuthorizationTestProvider(token: "test-token") + ), + "Unexpectedly accepted base URL alias: \(baseURL)" + ) + } + } + + func testAssertionSerializerProducesRequiredWebAuthnResponseFields() throws { + let userHandle = Data((0 ..< 32).map { UInt8($0) }) + let json = try PasskeyCredentialResponseSerializer.assertionJSON( + credentialID: Data([0x01]), + clientDataJSON: Data([0x02]), + authenticatorData: Data([0x03]), + signature: Data([0x04]), + userHandle: userHandle + ) + let credential = try jsonObject(json) + let response = try XCTUnwrap(credential["response"] as? [String: Any]) + + XCTAssertEqual( + Set(response.keys), + Set(["clientDataJSON", "authenticatorData", "signature", "userHandle"]) + ) + XCTAssertEqual(response["clientDataJSON"] as? String, "Ag") + XCTAssertEqual(response["authenticatorData"] as? String, "Aw") + XCTAssertEqual(response["signature"] as? String, "BA") + XCTAssertEqual(response["userHandle"] as? String, base64URL(userHandle)) + } + + func testRegistrationSerializerRejectsEveryEmptyRequiredField() { + assertInvalidCredentialResponse { + try PasskeyCredentialResponseSerializer.registrationJSON( + credentialID: Data(), + clientDataJSON: Data([1]), + attestationObject: Data([2]) + ) + } + assertInvalidCredentialResponse { + try PasskeyCredentialResponseSerializer.registrationJSON( + credentialID: Data([1]), + clientDataJSON: Data(), + attestationObject: Data([2]) + ) + } + assertInvalidCredentialResponse { + try PasskeyCredentialResponseSerializer.registrationJSON( + credentialID: Data([1]), + clientDataJSON: Data([2]), + attestationObject: Data() + ) + } + assertInvalidCredentialResponse { + try PasskeyCredentialResponseSerializer.registrationJSON( + credentialID: Data([1]), + clientDataJSON: Data([2]), + attestationObject: Data([3]), + authenticatorData: Data() + ) + } + } + + func testAssertionSerializerRejectsEveryEmptyRequiredField() { + assertInvalidCredentialResponse { + try PasskeyCredentialResponseSerializer.assertionJSON( + credentialID: Data(), + clientDataJSON: Data([1]), + authenticatorData: Data([2]), + signature: Data([3]), + userHandle: Data(repeating: 4, count: 32) + ) + } + assertInvalidCredentialResponse { + try PasskeyCredentialResponseSerializer.assertionJSON( + credentialID: Data([1]), + clientDataJSON: Data(), + authenticatorData: Data([2]), + signature: Data([3]), + userHandle: Data(repeating: 4, count: 32) + ) + } + assertInvalidCredentialResponse { + try PasskeyCredentialResponseSerializer.assertionJSON( + credentialID: Data([1]), + clientDataJSON: Data([2]), + authenticatorData: Data(), + signature: Data([3]), + userHandle: Data(repeating: 4, count: 32) + ) + } + assertInvalidCredentialResponse { + try PasskeyCredentialResponseSerializer.assertionJSON( + credentialID: Data([1]), + clientDataJSON: Data([2]), + authenticatorData: Data([3]), + signature: Data(), + userHandle: Data(repeating: 4, count: 32) + ) + } + assertInvalidCredentialResponse { + try PasskeyCredentialResponseSerializer.assertionJSON( + credentialID: Data([1]), + clientDataJSON: Data([2]), + authenticatorData: Data([3]), + signature: Data([4]), + userHandle: Data() + ) + } + } + + func testSerializerRejectsFieldsBeyondChallengeServiceLimits() { + assertInvalidCredentialResponse { + try PasskeyCredentialResponseSerializer.registrationJSON( + credentialID: Data(repeating: 1, count: 385), + clientDataJSON: Data([2]), + attestationObject: Data([3]) + ) + } + assertInvalidCredentialResponse { + try PasskeyCredentialResponseSerializer.registrationJSON( + credentialID: Data([1]), + clientDataJSON: Data(repeating: 2, count: 6145), + attestationObject: Data([3]) + ) + } + assertInvalidCredentialResponse { + try PasskeyCredentialResponseSerializer.registrationJSON( + credentialID: Data([1]), + clientDataJSON: Data([2]), + attestationObject: Data(repeating: 3, count: 24577) + ) + } + assertInvalidCredentialResponse { + try PasskeyCredentialResponseSerializer.assertionJSON( + credentialID: Data([1]), + clientDataJSON: Data([2]), + authenticatorData: Data([3]), + signature: Data([4]), + userHandle: Data(repeating: 5, count: 31) + ) + } + assertInvalidCredentialResponse { + try PasskeyCredentialResponseSerializer.assertionJSON( + credentialID: Data([1]), + clientDataJSON: Data([2]), + authenticatorData: Data([3]), + signature: Data([4]), + userHandle: Data(repeating: 5, count: 33) + ) + } + } + + func testSerializerAcceptsFieldsAtChallengeServiceLimits() throws { + _ = try PasskeyCredentialResponseSerializer.registrationJSON( + credentialID: Data(repeating: 1, count: 384), + clientDataJSON: Data(repeating: 2, count: 6144), + attestationObject: Data(repeating: 3, count: 24576), + authenticatorData: Data(repeating: 4, count: 24576) + ) + _ = try PasskeyCredentialResponseSerializer.assertionJSON( + credentialID: Data(repeating: 1, count: 384), + clientDataJSON: Data(repeating: 2, count: 6144), + authenticatorData: Data(repeating: 3, count: 24576), + signature: Data(repeating: 4, count: 24576), + userHandle: Data(repeating: 5, count: 32) + ) + } + + func testAES256GCMEnvelopeRoundTripsWithCanonicalHeaderAndRandomNonce() throws { + let cryptography = AESGCMPasskeyBackupEnvelopeCryptography() + let metadata = try envelopeMetadata() + let key = Data((1 ... 32).map(UInt8.init)) + let plaintext = Data("recoverable wallet backup".utf8) + + let first = try cryptography.encrypt(plaintext, metadata: metadata, key: key) + let second = try cryptography.encrypt(plaintext, metadata: metadata, key: key) + + XCTAssertEqual(first.prefix(8), Data("FPBKAEAD".utf8)) + XCTAssertEqual(first[8], 1) + XCTAssertEqual(first[9], 1) + XCTAssertEqual(first[10], 12) + XCTAssertEqual(first[11], 16) + XCTAssertNotEqual(first, second) + XCTAssertNotEqual(first.subdata(in: 16 ..< 28), second.subdata(in: 16 ..< 28)) + XCTAssertEqual(try cryptography.decrypt(first, metadata: metadata, key: key), plaintext) + } + + func testAES256GCMEnvelopeRejectsTamperingWrongKeyAndMetadataSwaps() throws { + let cryptography = AESGCMPasskeyBackupEnvelopeCryptography() + let metadata = try envelopeMetadata() + let key = Data(repeating: 7, count: 32) + let envelope = try cryptography.encrypt(Data([1, 2, 3]), metadata: metadata, key: key) + + for index in [16, 28, envelope.count - 1] { + var tampered = envelope + tampered[index] ^= 1 + XCTAssertThrowsError(try cryptography.decrypt(tampered, metadata: metadata, key: key)) + } + XCTAssertThrowsError( + try cryptography.decrypt(envelope, metadata: metadata, key: Data(repeating: 8, count: 32)) + ) + let swappedMetadata = [ + try envelopeMetadata(storageKey: "wallet-5678"), + try envelopeMetadata(walletId: "wallet-9999"), + try envelopeMetadata(accountName: "mallory@example.com"), + try envelopeMetadata(createdAtMillis: 1_767_225_600_001) + ] + for swapped in swappedMetadata { + XCTAssertThrowsError(try cryptography.decrypt(envelope, metadata: swapped, key: key)) + } + } + + func testAES256GCMEnvelopeRejectsEveryTruncationExtensionBadHeaderAndKeySize() throws { + let cryptography = AESGCMPasskeyBackupEnvelopeCryptography() + let metadata = try envelopeMetadata() + let key = Data(repeating: 9, count: 32) + let envelope = try cryptography.encrypt(Data([1, 2, 3]), metadata: metadata, key: key) + + for length in 0 ..< envelope.count { + XCTAssertThrowsError(try cryptography.validateCanonicalEnvelope(Data(envelope.prefix(length)))) + } + var extended = envelope + extended.append(0) + XCTAssertThrowsError(try cryptography.validateCanonicalEnvelope(extended)) + for index in 0 ... 15 { + var altered = envelope + altered[index] ^= 1 + XCTAssertThrowsError(try cryptography.validateCanonicalEnvelope(altered)) + } + for length in [0, 16, 31, 33, 64] { + XCTAssertThrowsError( + try cryptography.encrypt(Data([1]), metadata: metadata, key: Data(repeating: 1, count: length)) + ) + } + let maximumPlaintext = Data(repeating: 0xA5, count: 256 * 1024 - 44) + let maximumEnvelope = try cryptography.encrypt( + maximumPlaintext, + metadata: metadata, + key: key + ) + XCTAssertEqual(maximumEnvelope.count, 256 * 1024) + XCTAssertEqual( + try cryptography.decrypt(maximumEnvelope, metadata: metadata, key: key), + maximumPlaintext + ) + } + + func testDecryptsSharedCrossPlatformAESGCMContractVector() throws { + let envelope = try PasskeyBackupContract.decodeBase64URL( + "RlBCS0FFQUQBAQwQAAAAHQABAgMEBQYHCAkKCxJCbuS78BFnbl_ULhb12v1I5M7-G-ZXHqwrFsgsJiQcEPtBrkqPDXxWvxW4BQ" + ) + + XCTAssertEqual( + try AESGCMPasskeyBackupEnvelopeCryptography().decrypt( + envelope, + metadata: envelopeMetadata(), + key: Data((1 ... 32).map(UInt8.init)) + ), + Data("cross-platform-passkey-backup".utf8) + ) + } + + func testEncryptedRecordRejectsArbitraryNonemptyPayload() throws { + XCTAssertThrowsError( + try PasskeyBackupEncryptedRecord( + storageKey: "wallet-1234", + walletId: "wallet-001", + accountName: "alice@example.com", + createdAtMillis: 1_767_225_600_000, + encryptedPayload: Data([1, 2, 3]) + ) + ) { error in + XCTAssertEqual(error as? PasskeyBackupError, .invalidEncryptedEnvelope) + } + } + + func testCredentialLifecycleAuthorizesExactBodiesAndValidatesResponseShape() async throws { + let credentialId = base64URL(Data("credential-1".utf8)) + let transport = AuthorizationTestTransport( + responses: [ + httpJSON( + #"{"storageKey":"wallet-1234","credentials":[{"id":"\#(credentialId)","aaguid":"00000000-0000-0000-0000-000000000000","registrationPlatform":"ios","deviceType":"multiDevice","backedUp":true,"transports":["internal","hybrid"]}],"rpId":"fearlesswallet.io","schemaVersion":1}"# + ), + httpJSON( + #"{"storageKey":"wallet-1234","credentialId":"\#(credentialId)","remainingCredentials":0,"rpId":"fearlesswallet.io","schemaVersion":1}"# + ), + httpJSON( + #"{"storageKey":"wallet-1234","remainingCredentials":0,"rpId":"fearlesswallet.io","schemaVersion":1}"# + ) + ] + ) + let authorization = AuthorizationTestProvider(token: "test-token") + let service = try HTTPPasskeyBackupChallengeService( + baseURL: "https://backup.fearlesswallet.io", + transport: transport, + authorizationProvider: authorization + ) + + let listed = try await service.listCredentials(storageKey: "wallet-1234") + let revoked = try await service.revokeCredential( + storageKey: "wallet-1234", + credentialId: credentialId + ) + let revokedAll = try await service.revokeAllCredentials(storageKey: "wallet-1234") + + XCTAssertEqual(listed.credentials.map(\.id), [credentialId]) + XCTAssertEqual(listed.credentials.first?.transports, ["internal", "hybrid"]) + XCTAssertEqual(revoked.remainingCredentials, 0) + XCTAssertNil(revokedAll.credentialId) + XCTAssertEqual( + authorization.requests.map(\.path), + [ + PasskeyBackupAuthorizationRequest.credentialsListPath, + PasskeyBackupAuthorizationRequest.credentialsRevokePath, + PasskeyBackupAuthorizationRequest.credentialsRevokeAllPath + ] + ) + for (grant, request) in zip(authorization.requests, transport.requests) { + let bodyData = try XCTUnwrap(request.body) + XCTAssertEqual(grant.bodySha256, sha256Base64URL(bodyData)) + XCTAssertEqual(request.headers["Authorization"], "Bearer test-token") + let body = try XCTUnwrap( + JSONSerialization.jsonObject(with: bodyData) as? [String: Any] + ) + var expectedKeys: Set = ["storageKey", "rpId", "schemaVersion"] + if grant.path == PasskeyBackupAuthorizationRequest.credentialsRevokePath { + expectedKeys.insert("credentialId") + } + XCTAssertEqual(Set(body.keys), expectedKeys) + XCTAssertEqual(body["storageKey"] as? String, "wallet-1234") + XCTAssertEqual(body["rpId"] as? String, "fearlesswallet.io") + XCTAssertEqual(body["schemaVersion"] as? Int, 1) + } + } + + func testCredentialLifecycleRejectsMalformedSummariesAndNonzeroRevokeAll() async throws { + let credentialId = base64URL(Data("credential-1".utf8)) + let validSummary = + #"{"id":"\#(credentialId)","aaguid":"00000000-0000-0000-0000-000000000000","registrationPlatform":"ios","deviceType":"multiDevice","backedUp":true}"# + let malformedSummaries = [ + #"{"id":"\#(credentialId)","aaguid":"BAD","registrationPlatform":"ios","deviceType":"multiDevice","backedUp":true}"#, + #"{"id":"\#(credentialId)","aaguid":"00000000-0000-0000-0000-000000000000","registrationPlatform":"web","deviceType":"multiDevice","backedUp":true}"#, + #"{"id":"\#(credentialId)","aaguid":"00000000-0000-0000-0000-000000000000","registrationPlatform":"ios","deviceType":"singleDevice","backedUp":true}"#, + #"{"id":"\#(credentialId)","aaguid":"00000000-0000-0000-0000-000000000000","registrationPlatform":"ios","deviceType":"multiDevice","backedUp":true,"transports":["internal","internal"]}"# + ] + + for summary in malformedSummaries { + let service = try lifecycleService( + response: #"{"storageKey":"wallet-1234","credentials":[\#(summary)],"rpId":"fearlesswallet.io","schemaVersion":1}"# + ) + do { + _ = try await service.listCredentials(storageKey: "wallet-1234") + XCTFail("Malformed credential summary unexpectedly passed: \(summary)") + } catch { + XCTAssertEqual(error as? PasskeyBackupError, .malformedChallengeServiceResponse) + } + } + + let oversizedCredentialList = (0 ... 32).map { index in + let id = base64URL(Data("credential-\(index)".utf8)) + return #"{"id":"\#(id)","aaguid":"00000000-0000-0000-0000-000000000000","registrationPlatform":"ios","deviceType":"multiDevice","backedUp":true}"# + }.joined(separator: ",") + for credentials in ["\(validSummary),\(validSummary)", oversizedCredentialList] { + let service = try lifecycleService( + response: #"{"storageKey":"wallet-1234","credentials":[\#(credentials)],"rpId":"fearlesswallet.io","schemaVersion":1}"# + ) + do { + _ = try await service.listCredentials(storageKey: "wallet-1234") + XCTFail("Invalid credential list unexpectedly passed") + } catch { + XCTAssertEqual(error as? PasskeyBackupError, .malformedChallengeServiceResponse) + } + } + + let service = try lifecycleService( + response: #"{"storageKey":"wallet-1234","remainingCredentials":1,"rpId":"fearlesswallet.io","schemaVersion":1}"# + ) + await assertThrowsAsync { + _ = try await service.revokeAllCredentials(storageKey: "wallet-1234") + } + } + + func testDeleteRevokesServerCredentialsBeforeCloudAndPreservesCloudOnServerFailure() async throws { + let envelope = try AESGCMPasskeyBackupEnvelopeCryptography().encrypt( + Data([1, 2, 3]), + metadata: envelopeMetadata(), + key: Data(repeating: 1, count: 32) + ) + let record = try PasskeyBackupEncryptedRecord( + storageKey: "wallet-1234", + walletId: "wallet-001", + accountName: "alice@example.com", + createdAtMillis: 1_767_225_600_000, + encryptedPayload: envelope + ) + let cloud = LifecycleTestCloudStorage(record: record) + let service = LifecycleTestChallengeService(revokeAllError: PasskeyBackupError.unavailableAuthorization) + let workflow = try PasskeyBackupWorkflow( + challengeService: service, + cloudStorage: cloud, + isReleaseEnabled: true + ) + + await assertThrowsAsync { + try await workflow.deleteBackup(storageKey: "wallet-1234") + } + XCTAssertEqual(service.revokedAllStorageKey, "wallet-1234") + XCTAssertNil(cloud.deletedStorageKey) + let recoverableRecord = try await cloud.loadPasskeyBackup(storageKey: "wallet-1234") + XCTAssertNotNil(recoverableRecord) + } + + func testDeleteRemovesCloudOnlyAfterSuccessfulRevokeAll() async throws { + let envelope = try AESGCMPasskeyBackupEnvelopeCryptography().encrypt( + Data([1]), + metadata: envelopeMetadata(), + key: Data(repeating: 1, count: 32) + ) + let record = try PasskeyBackupEncryptedRecord( + storageKey: "wallet-1234", + walletId: "wallet-001", + accountName: "alice@example.com", + createdAtMillis: 1_767_225_600_000, + encryptedPayload: envelope + ) + let service = LifecycleTestChallengeService() + let cloud = LifecycleTestCloudStorage( + record: record, + beforeDelete: { + XCTAssertEqual(service.revokedAllStorageKey, "wallet-1234") + } + ) + let workflow = try PasskeyBackupWorkflow( + challengeService: service, + cloudStorage: cloud, + isReleaseEnabled: true + ) + + try await workflow.deleteBackup(storageKey: "wallet-1234") + + XCTAssertEqual(cloud.deletedStorageKey, "wallet-1234") + let deletedRecord = try await cloud.loadPasskeyBackup(storageKey: "wallet-1234") + XCTAssertNil(deletedRecord) + } + + func testPlaintextWorkflowEncryptsAndDecryptsThroughRecoverableKeyProvider() async throws { + let service = LifecycleTestChallengeService() + let cloud = LifecycleTestCloudStorage(record: nil) + let workflow = try PasskeyBackupWorkflow( + challengeService: service, + cloudStorage: cloud, + backupKeyProvider: FixedTestBackupKeyProvider(), + isReleaseEnabled: true, + createdAtMillisProvider: { 1_767_225_600_000 } + ) + let registrationChallenge = try PasskeyBackupRegistrationChallenge( + registrationId: "registration-1234", + challenge: Data(repeating: 1, count: 32), + userId: Data(repeating: 2, count: 32), + userName: "alice@example.com", + displayName: "Alice", + storageKey: "wallet-1234" + ) + let pendingRegistration = try PendingPasskeyBackupRegistration( + challenge: registrationChallenge, + walletId: "wallet-001", + accountName: "alice@example.com" + ) + let plaintext = Data("wallet-secret-material".utf8) + + let saved = try await workflow.finishRegistrationWithPlaintext( + pending: pendingRegistration, + credentialResponseJSON: #"{"id":"Y3JlZGVudGlhbA"}"#, + plaintextBackup: plaintext + ) + let assertionChallenge = try PasskeyBackupAssertionChallenge( + assertionId: "assertion-1234", + challenge: Data(repeating: 3, count: 32), + storageKey: "wallet-1234" + ) + let restored = try await workflow.finishRestoreWithDecryption( + pending: PendingPasskeyBackupAssertion(challenge: assertionChallenge), + credentialResponseJSON: #"{"id":"Y3JlZGVudGlhbA"}"# + ) + + XCTAssertNotEqual(saved.encryptedPayload, plaintext) + XCTAssertEqual(restored, plaintext) + } + + func testPlaintextWorkflowFailsBeforeRegistrationWithoutRecoverableKeySource() async throws { + let service = LifecycleTestChallengeService() + let cloud = LifecycleTestCloudStorage(record: nil) + let workflow = try PasskeyBackupWorkflow( + challengeService: service, + cloudStorage: cloud, + isReleaseEnabled: true, + createdAtMillisProvider: { 1_767_225_600_000 } + ) + let challenge = try PasskeyBackupRegistrationChallenge( + registrationId: "registration-1234", + challenge: Data(repeating: 1, count: 32), + userId: Data(repeating: 2, count: 32), + userName: "alice@example.com", + displayName: "Alice", + storageKey: "wallet-1234" + ) + let pending = try PendingPasskeyBackupRegistration( + challenge: challenge, + walletId: "wallet-001", + accountName: "alice@example.com" + ) + + await assertThrowsAsync { + _ = try await workflow.finishRegistrationWithPlaintext( + pending: pending, + credentialResponseJSON: #"{"id":"Y3JlZGVudGlhbA"}"#, + plaintextBackup: Data([1]) + ) + } + XCTAssertNil(service.completedRegistrationId) + let missingRecord = try await cloud.loadPasskeyBackup(storageKey: "wallet-1234") + XCTAssertNil(missingRecord) + } + + func testCloudSaveFailureRevokesOnlyNewlyRegisteredCredential() async throws { + let credentialId = base64URL(Data("credential-1".utf8)) + let service = LifecycleTestChallengeService() + let cloud = LifecycleTestCloudStorage( + record: nil, + saveError: PasskeyBackupError.unavailableCloudStorage + ) + let workflow = try PasskeyBackupWorkflow( + challengeService: service, + cloudStorage: cloud, + backupKeyProvider: FixedTestBackupKeyProvider(), + isReleaseEnabled: true, + createdAtMillisProvider: { 1_767_225_600_000 } + ) + let challenge = try PasskeyBackupRegistrationChallenge( + registrationId: "registration-1234", + challenge: Data(repeating: 1, count: 32), + userId: Data(repeating: 2, count: 32), + userName: "alice@example.com", + displayName: "Alice", + storageKey: "wallet-1234" + ) + let pending = try PendingPasskeyBackupRegistration( + challenge: challenge, + walletId: "wallet-001", + accountName: "alice@example.com" + ) + + await assertThrowsAsync { + _ = try await workflow.finishRegistrationWithPlaintext( + pending: pending, + credentialResponseJSON: #"{"id":"\#(credentialId)"}"#, + plaintextBackup: Data([1, 2, 3]) + ) + } + XCTAssertEqual(service.completedRegistrationId, "registration-1234") + XCTAssertEqual(service.revokedCredentialStorageKey, "wallet-1234") + XCTAssertEqual(service.revokedCredentialId, credentialId) + XCTAssertNil(service.revokedAllStorageKey) + } + + func testLegacyRawRegistrationFailsClosedBeforeClockOrCeremonyWithoutExplicitMetadata() async throws { + var clockCalls = 0 + let service = LifecycleTestChallengeService() + let cloud = LifecycleTestCloudStorage(record: nil) + let workflow = try PasskeyBackupWorkflow( + challengeService: service, + cloudStorage: cloud, + isReleaseEnabled: true, + createdAtMillisProvider: { + clockCalls += 1 + return 1_767_225_600_000 + } + ) + let envelope = try AESGCMPasskeyBackupEnvelopeCryptography().encrypt( + Data([1]), + metadata: envelopeMetadata(), + key: Data((1 ... 32).map(UInt8.init)) + ) + + do { + _ = try await workflow.finishRegistration( + pending: self.pendingRegistration(), + credentialResponseJSON: #"{"id":"Y3JlZC0x"}"#, + encryptedPayload: envelope + ) + XCTFail("Legacy raw registration unexpectedly passed") + } catch { + XCTAssertEqual(error as? PasskeyBackupError, .encryptedPayloadMetadataRequired) + } + XCTAssertEqual(clockCalls, 0) + XCTAssertNil(service.completedRegistrationId) + XCTAssertNil(service.revokedCredentialId) + } + + func testPreEncryptedRegistrationUsesCallerAuthenticatedTimestampWithoutConsultingWorkflowClock() async throws { + let callerCreatedAtMillis: Int64 = 1_767_225_612_345 + var clockCalls = 0 + let service = LifecycleTestChallengeService() + let cloud = LifecycleTestCloudStorage(record: nil) + let workflow = try PasskeyBackupWorkflow( + challengeService: service, + cloudStorage: cloud, + backupKeyProvider: FixedTestBackupKeyProvider(), + isReleaseEnabled: true, + createdAtMillisProvider: { + clockCalls += 1 + return 1_767_225_699_999 + } + ) + let envelope = try AESGCMPasskeyBackupEnvelopeCryptography().encrypt( + Data([1]), + metadata: envelopeMetadata(createdAtMillis: callerCreatedAtMillis), + key: Data((1 ... 32).map(UInt8.init)) + ) + let record = try PasskeyBackupEncryptedRecord( + storageKey: "wallet-1234", + walletId: "wallet-001", + accountName: "alice@example.com", + createdAtMillis: callerCreatedAtMillis, + encryptedPayload: envelope + ) + + let saved = try await workflow.finishRegistrationWithEncryptedRecord( + pending: self.pendingRegistration(), + credentialResponseJSON: #"{"id":"Y3JlZC0x"}"#, + record: record + ) + + XCTAssertEqual(saved.createdAtMillis, callerCreatedAtMillis) + let stored = try await cloud.loadPasskeyBackup(storageKey: "wallet-1234") + XCTAssertEqual(stored?.createdAtMillis, callerCreatedAtMillis) + XCTAssertEqual(clockCalls, 0) + XCTAssertEqual(service.completedRegistrationId, "registration-1234") + } + + func testPreEncryptedRegistrationRejectsCreatedAtAADMismatchBeforeCeremony() async throws { + let service = LifecycleTestChallengeService() + let cloud = LifecycleTestCloudStorage(record: nil) + let workflow = try PasskeyBackupWorkflow( + challengeService: service, + cloudStorage: cloud, + backupKeyProvider: FixedTestBackupKeyProvider(), + isReleaseEnabled: true, + createdAtMillisProvider: { 1_767_225_600_000 } + ) + let envelope = try AESGCMPasskeyBackupEnvelopeCryptography().encrypt( + Data([1]), + metadata: envelopeMetadata(), + key: Data((1 ... 32).map(UInt8.init)) + ) + let mismatchedRecord = try PasskeyBackupEncryptedRecord( + storageKey: "wallet-1234", + walletId: "wallet-001", + accountName: "alice@example.com", + createdAtMillis: 1_767_225_600_001, + encryptedPayload: envelope + ) + + do { + _ = try await workflow.finishRegistrationWithEncryptedRecord( + pending: self.pendingRegistration(), + credentialResponseJSON: #"{"id":"Y3JlZC0x"}"#, + record: mismatchedRecord + ) + XCTFail("Record with mismatched createdAt AAD unexpectedly passed") + } catch { + XCTAssertEqual(error as? PasskeyBackupError, .envelopeAuthenticationFailed) + } + XCTAssertNil(service.completedRegistrationId) + XCTAssertNil(service.revokedCredentialId) + let missingRecord = try await cloud.loadPasskeyBackup(storageKey: "wallet-1234") + XCTAssertNil(missingRecord) + } + + func testPreEncryptedRegistrationRejectsRecordIdentityMismatchBeforeCeremony() async throws { + let service = LifecycleTestChallengeService() + let cloud = LifecycleTestCloudStorage(record: nil) + let workflow = try PasskeyBackupWorkflow( + challengeService: service, + cloudStorage: cloud, + backupKeyProvider: FixedTestBackupKeyProvider(), + isReleaseEnabled: true + ) + let identities: [(String, String, String, PasskeyBackupError)] = [ + ("wallet-9999", "wallet-001", "alice@example.com", .mismatchedChallengeStorageKey), + ("wallet-1234", "wallet-999", "alice@example.com", .invalidWalletId), + ("wallet-1234", "wallet-001", "mallory@example.com", .invalidUserName) + ] + + for (storageKey, walletId, accountName, expectedError) in identities { + let metadata = try envelopeMetadata( + storageKey: storageKey, + walletId: walletId, + accountName: accountName + ) + let envelope = try AESGCMPasskeyBackupEnvelopeCryptography().encrypt( + Data([1]), + metadata: metadata, + key: Data((1 ... 32).map(UInt8.init)) + ) + let record = try PasskeyBackupEncryptedRecord( + storageKey: storageKey, + walletId: walletId, + accountName: accountName, + createdAtMillis: metadata.createdAtMillis, + encryptedPayload: envelope + ) + do { + _ = try await workflow.finishRegistrationWithEncryptedRecord( + pending: self.pendingRegistration(), + credentialResponseJSON: #"{"id":"Y3JlZC0x"}"#, + record: record + ) + XCTFail("Mismatched encrypted record identity unexpectedly passed") + } catch { + XCTAssertEqual(error as? PasskeyBackupError, expectedError) + } + } + + XCTAssertNil(service.completedRegistrationId) + XCTAssertNil(service.revokedCredentialId) + let missingRecord = try await cloud.loadPasskeyBackup(storageKey: "wallet-1234") + XCTAssertNil(missingRecord) + } + + func testPreEncryptedRegistrationCompensatesStorageMismatchAfterCompletion() async throws { + let credentialId = "Y3JlZC0x" + let service = LifecycleTestChallengeService(registrationStorageKey: "wallet-5678") + let cloud = LifecycleTestCloudStorage(record: nil) + let workflow = try PasskeyBackupWorkflow( + challengeService: service, + cloudStorage: cloud, + backupKeyProvider: FixedTestBackupKeyProvider(), + isReleaseEnabled: true, + createdAtMillisProvider: { 1_767_225_600_000 } + ) + let envelope = try AESGCMPasskeyBackupEnvelopeCryptography().encrypt( + Data([1]), + metadata: envelopeMetadata(), + key: Data((1 ... 32).map(UInt8.init)) + ) + let record = try PasskeyBackupEncryptedRecord( + storageKey: "wallet-1234", + walletId: "wallet-001", + accountName: "alice@example.com", + createdAtMillis: 1_767_225_600_000, + encryptedPayload: envelope + ) + + await assertThrowsAsync { + _ = try await workflow.finishRegistrationWithEncryptedRecord( + pending: self.pendingRegistration(), + credentialResponseJSON: #"{"id":"\#(credentialId)"}"#, + record: record + ) + } + XCTAssertEqual(service.completedRegistrationId, "registration-1234") + XCTAssertEqual(service.revokedCredentialStorageKey, "wallet-1234") + XCTAssertEqual(service.revokedCredentialId, credentialId) + XCTAssertNil(service.revokedAllStorageKey) + } + + func testRegistrationResponseLossAfterServerCommitStillRevokesCredential() async throws { + let credentialId = "Y3JlZC0x" + let service = LifecycleTestChallengeService( + completeRegistrationError: PasskeyBackupError.registrationCompletionOutcomeUnknown + ) + let cloud = LifecycleTestCloudStorage(record: nil) + let workflow = try PasskeyBackupWorkflow( + challengeService: service, + cloudStorage: cloud, + backupKeyProvider: FixedTestBackupKeyProvider(), + isReleaseEnabled: true, + createdAtMillisProvider: { 1_767_225_600_000 } + ) + + await assertThrowsAsync { + _ = try await workflow.finishRegistrationWithPlaintext( + pending: self.pendingRegistration(), + credentialResponseJSON: #"{"id":"\#(credentialId)"}"#, + plaintextBackup: Data([1, 2, 3]) + ) + } + XCTAssertEqual(service.completedRegistrationId, "registration-1234") + XCTAssertEqual(service.revokedCredentialStorageKey, "wallet-1234") + XCTAssertEqual(service.revokedCredentialId, credentialId) + let missingRecord = try await cloud.loadPasskeyBackup(storageKey: "wallet-1234") + XCTAssertNil(missingRecord) + } + + func testRegistrationCompensationPropagatesRevokeFailureWithoutMaskingPrimaryError() async throws { + let service = LifecycleTestChallengeService( + revokeCredentialError: PasskeyBackupError.unavailableAuthorization, + completeRegistrationError: PasskeyBackupError.registrationCompletionOutcomeUnknown + ) + let workflow = try PasskeyBackupWorkflow( + challengeService: service, + cloudStorage: LifecycleTestCloudStorage(record: nil), + backupKeyProvider: FixedTestBackupKeyProvider(), + isReleaseEnabled: true, + createdAtMillisProvider: { 1_767_225_600_000 } + ) + + do { + _ = try await workflow.finishRegistrationWithPlaintext( + pending: self.pendingRegistration(), + credentialResponseJSON: #"{"id":"Y3JlZC0x"}"#, + plaintextBackup: Data([1, 2, 3]) + ) + XCTFail("Registration with failed revoke compensation unexpectedly passed") + } catch { + let compensationError = try XCTUnwrap( + error as? PasskeyBackupRegistrationCompensationError + ) + XCTAssertEqual( + compensationError.primaryError as? PasskeyBackupError, + .registrationCompletionOutcomeUnknown + ) + XCTAssertEqual(compensationError.failureKind, .revokeFailed) + XCTAssertEqual( + compensationError.cleanupError as? PasskeyBackupError, + .unavailableAuthorization + ) + } + XCTAssertEqual(service.revokedCredentialStorageKey, "wallet-1234") + XCTAssertEqual(service.revokedCredentialId, "Y3JlZC0x") + } + + func testExplicitRegistrationRejectionDoesNotRevokeExistingCredentialId() async throws { + let service = LifecycleTestChallengeService( + completeRegistrationError: PasskeyBackupError.challengeServiceHTTPStatus(409) + ) + let workflow = try PasskeyBackupWorkflow( + challengeService: service, + cloudStorage: LifecycleTestCloudStorage(record: nil), + backupKeyProvider: FixedTestBackupKeyProvider(), + isReleaseEnabled: true, + createdAtMillisProvider: { 1_767_225_600_000 } + ) + + await assertThrowsAsync { + _ = try await workflow.finishRegistrationWithPlaintext( + pending: self.pendingRegistration(), + credentialResponseJSON: #"{"id":"Y3JlZC0x"}"#, + plaintextBackup: Data([1, 2, 3]) + ) + } + XCTAssertEqual(service.completedRegistrationId, "registration-1234") + XCTAssertNil(service.revokedCredentialStorageKey) + XCTAssertNil(service.revokedCredentialId) + } + + func testCancellationWhileRegistrationCompletionIsInFlightStillRevokesCredential() async throws { + let credentialId = "Y3JlZC0x" + let completionGate = CancellationCompletionGate() + let service = AdversarialCompensationChallengeService(completionGate: completionGate) + let workflow = try PasskeyBackupWorkflow( + challengeService: service, + cloudStorage: LifecycleTestCloudStorage(record: nil), + backupKeyProvider: FixedTestBackupKeyProvider(), + isReleaseEnabled: true, + compensationTimeoutNanoseconds: 1_000_000_000, + createdAtMillisProvider: { 1_767_225_600_000 } + ) + let registrationTask = Task { + try await workflow.finishRegistrationWithPlaintext( + pending: self.pendingRegistration(), + credentialResponseJSON: #"{"id":"\#(credentialId)"}"#, + plaintextBackup: Data([1, 2, 3]) + ) + } + + await completionGate.waitUntilCompletionStarted() + registrationTask.cancel() + await completionGate.releaseCompletion() + do { + _ = try await registrationTask.value + XCTFail("Cancelled registration completion unexpectedly succeeded") + } catch { + XCTAssertTrue( + error is CancellationError || + error as? PasskeyBackupError == .registrationCompletionOutcomeUnknown + ) + } + let revocation = await service.revocationSnapshot() + XCTAssertEqual(revocation.storageKey, "wallet-1234") + XCTAssertEqual(revocation.credentialId, credentialId) + XCTAssertEqual(revocation.observedCancellation, false) + } + + func testPlaintextRegistrationCompensatesStorageMismatchAfterCompletion() async throws { + let credentialId = "Y3JlZC0x" + let service = LifecycleTestChallengeService(registrationStorageKey: "wallet-5678") + let cloud = LifecycleTestCloudStorage(record: nil) + let workflow = try PasskeyBackupWorkflow( + challengeService: service, + cloudStorage: cloud, + backupKeyProvider: FixedTestBackupKeyProvider(), + isReleaseEnabled: true, + createdAtMillisProvider: { 1_767_225_600_000 } + ) + + await assertThrowsAsync { + _ = try await workflow.finishRegistrationWithPlaintext( + pending: self.pendingRegistration(), + credentialResponseJSON: #"{"id":"\#(credentialId)"}"#, + plaintextBackup: Data([1, 2, 3]) + ) + } + XCTAssertEqual(service.completedRegistrationId, "registration-1234") + XCTAssertEqual(service.revokedCredentialStorageKey, "wallet-1234") + XCTAssertEqual(service.revokedCredentialId, credentialId) + } + + func testWorkflowRejectsMismatchedLifecycleResponseIdentities() async throws { + let credentialId = "Y3JlZC0x" + let otherCredentialId = "Y3JlZC0y" + let listResult = try PasskeyBackupCredentialListResult( + storageKey: "wallet-5678", + credentials: [] + ) + let revokeStorageResult = try PasskeyBackupCredentialRevokeResult( + storageKey: "wallet-5678", + credentialId: credentialId, + remainingCredentials: 0 + ) + let revokeCredentialResult = try PasskeyBackupCredentialRevokeResult( + storageKey: "wallet-1234", + credentialId: otherCredentialId, + remainingCredentials: 0 + ) + let listWorkflow = try PasskeyBackupWorkflow( + challengeService: LifecycleTestChallengeService(credentialListResult: listResult), + cloudStorage: LifecycleTestCloudStorage(record: nil), + isReleaseEnabled: true + ) + let revokeStorageWorkflow = try PasskeyBackupWorkflow( + challengeService: LifecycleTestChallengeService( + revokeCredentialResult: revokeStorageResult + ), + cloudStorage: LifecycleTestCloudStorage(record: nil), + isReleaseEnabled: true + ) + let revokeCredentialWorkflow = try PasskeyBackupWorkflow( + challengeService: LifecycleTestChallengeService( + revokeCredentialResult: revokeCredentialResult + ), + cloudStorage: LifecycleTestCloudStorage(record: nil), + isReleaseEnabled: true + ) + + await assertThrowsAsync { + _ = try await listWorkflow.listCredentials(storageKey: "wallet-1234") + } + await assertThrowsAsync { + _ = try await revokeStorageWorkflow.revokeCredential( + storageKey: "wallet-1234", + credentialId: credentialId + ) + } + await assertThrowsAsync { + _ = try await revokeCredentialWorkflow.revokeCredential( + storageKey: "wallet-1234", + credentialId: credentialId + ) + } + } + + func testCancellationAfterRegistrationGetsIndependentCompensationOpportunity() async throws { + let credentialId = "Y3JlZC0x" + let saveGate = CancellationSaveGate() + let service = AdversarialCompensationChallengeService() + let workflow = try PasskeyBackupWorkflow( + challengeService: service, + cloudStorage: CancellationGateCloudStorage(gate: saveGate), + backupKeyProvider: FixedTestBackupKeyProvider(), + isReleaseEnabled: true, + compensationTimeoutNanoseconds: 1_000_000_000, + createdAtMillisProvider: { 1_767_225_600_000 } + ) + let registrationTask = Task { + try await workflow.finishRegistrationWithPlaintext( + pending: self.pendingRegistration(), + credentialResponseJSON: #"{"id":"\#(credentialId)"}"#, + plaintextBackup: Data([1, 2, 3]) + ) + } + + await saveGate.waitUntilSaveStarted() + registrationTask.cancel() + await saveGate.releaseSave() + + do { + _ = try await registrationTask.value + XCTFail("Cancelled registration unexpectedly succeeded") + } catch { + XCTAssertTrue(error is CancellationError) + } + let revocation = await service.revocationSnapshot() + XCTAssertEqual(revocation.storageKey, "wallet-1234") + XCTAssertEqual(revocation.credentialId, credentialId) + XCTAssertEqual(revocation.observedCancellation, false) + } + + func testRegistrationCompensationTimeoutIsObservableWithoutMaskingPrimaryError() async throws { + let revokeGate = BlockingRevokeGate() + let service = AdversarialCompensationChallengeService(revokeGate: revokeGate) + let completion = CompensationCompletionProbe() + let workflow = try PasskeyBackupWorkflow( + challengeService: service, + cloudStorage: LifecycleTestCloudStorage( + record: nil, + saveError: PasskeyBackupError.unavailableCloudStorage + ), + backupKeyProvider: FixedTestBackupKeyProvider(), + isReleaseEnabled: true, + compensationTimeoutNanoseconds: 20_000_000, + createdAtMillisProvider: { 1_767_225_600_000 } + ) + let registrationTask = Task { + do { + _ = try await workflow.finishRegistrationWithPlaintext( + pending: self.pendingRegistration(), + credentialResponseJSON: #"{"id":"Y3JlZC0x"}"#, + plaintextBackup: Data([1, 2, 3]) + ) + await completion.record(didPreserveOriginalError: false) + } catch { + let compensationError = error as? PasskeyBackupRegistrationCompensationError + let preservedPrimaryAndTimeout = + (compensationError?.primaryError as? PasskeyBackupError) == + .unavailableCloudStorage && + compensationError?.failureKind == .timedOut && + compensationError?.cleanupError == nil + await completion.record(didPreserveOriginalError: preservedPrimaryAndTimeout) + } + } + + await revokeGate.waitUntilRevokeStarted() + try await Task.sleep(nanoseconds: 100_000_000) + let boundedResult = await completion.snapshot() + XCTAssertTrue(boundedResult.isFinished) + XCTAssertTrue(boundedResult.didPreserveOriginalError) + + await revokeGate.releaseRevoke() + await registrationTask.value + } + + func testWorkflowRejectsInvalidCompensationTimeouts() { + for timeout in [UInt64(0), 999_999, 30_000_000_001] { + XCTAssertThrowsError( + try PasskeyBackupWorkflow( + challengeService: LifecycleTestChallengeService(), + cloudStorage: LifecycleTestCloudStorage(record: nil), + isReleaseEnabled: true, + compensationTimeoutNanoseconds: timeout + ) + ) { error in + XCTAssertEqual(error as? PasskeyBackupError, .invalidCompensationTimeout) + } + } + } + + @available(iOS 15.0, *) + func testURLSessionTransportRejectsInvalidResponseLimits() { + for limit in [0, -1, PasskeyBackupHTTPTransportPolicy.maximumResponseBytes + 1] { + XCTAssertThrowsError( + try URLSessionPasskeyBackupHTTPTransport(maximumResponseBytes: limit) + ) { error in + XCTAssertEqual(error as? PasskeyBackupError, .invalidResponseSizeLimit) + } + } + } + + @available(iOS 15.0, *) + func testURLSessionTransportRejectsOversizedDeclaredResponseBeforeBody() async throws { + PasskeyBackupMockURLProtocol.install { protocolInstance, request in + let response = HTTPURLResponse( + url: request.url!, + statusCode: 200, + httpVersion: nil, + headerFields: ["Content-Length": "9"] + )! + protocolInstance.client?.urlProtocol( + protocolInstance, + didReceive: response, + cacheStoragePolicy: .notAllowed + ) + // Finish without a body so URLProtocol delivers the declared + // length to the session delegate instead of leaving a stalled + // mock request that can only prove the timeout policy. + protocolInstance.client?.urlProtocolDidFinishLoading(protocolInstance) + } + defer { PasskeyBackupMockURLProtocol.reset() } + + let transport = try URLSessionPasskeyBackupHTTPTransport( + configuration: passkeyMockConfiguration(), + maximumResponseBytes: 8 + ) + do { + _ = try await transport.execute(passkeyTransportRequest()) + XCTFail("Oversized declared response unexpectedly passed") + } catch { + XCTAssertEqual( + error as? PasskeyBackupError, + .challengeServiceResponseTooLarge + ) + } + XCTAssertEqual(transport.inFlightRequestCount, 0) + } + + @available(iOS 15.0, *) + func testURLSessionTransportRejectsChunkedResponseBeyondLimit() async throws { + PasskeyBackupMockURLProtocol.install { protocolInstance, request in + let response = HTTPURLResponse( + url: request.url!, + statusCode: 200, + httpVersion: nil, + headerFields: nil + )! + protocolInstance.client?.urlProtocol( + protocolInstance, + didReceive: response, + cacheStoragePolicy: .notAllowed + ) + protocolInstance.client?.urlProtocol( + protocolInstance, + didLoad: Data(repeating: 0x41, count: 8) + ) + protocolInstance.client?.urlProtocol( + protocolInstance, + didLoad: Data([0x42]) + ) + protocolInstance.client?.urlProtocolDidFinishLoading(protocolInstance) + } + defer { PasskeyBackupMockURLProtocol.reset() } + + let transport = try URLSessionPasskeyBackupHTTPTransport( + configuration: passkeyMockConfiguration(), + maximumResponseBytes: 8 + ) + do { + _ = try await transport.execute(passkeyTransportRequest()) + XCTFail("Oversized streamed response unexpectedly passed") + } catch { + XCTAssertEqual( + error as? PasskeyBackupError, + .challengeServiceResponseTooLarge + ) + } + XCTAssertEqual(transport.inFlightRequestCount, 0) + } + + @available(iOS 15.0, *) + func testURLSessionTransportBoundsDecodedStreamingBytes() async throws { + PasskeyBackupMockURLProtocol.install { protocolInstance, request in + let response = HTTPURLResponse( + url: request.url!, + statusCode: 200, + httpVersion: nil, + headerFields: [ + "Content-Encoding": "gzip", + "Transfer-Encoding": "chunked" + ] + )! + protocolInstance.client?.urlProtocol( + protocolInstance, + didReceive: response, + cacheStoragePolicy: .notAllowed + ) + // URLSession delegates observe decoded body chunks. Simulate a + // tiny compressed entity expanding beyond the configured bound. + protocolInstance.client?.urlProtocol( + protocolInstance, + didLoad: Data(repeating: 0x41, count: 9) + ) + protocolInstance.client?.urlProtocolDidFinishLoading(protocolInstance) + } + defer { PasskeyBackupMockURLProtocol.reset() } + + let transport = try URLSessionPasskeyBackupHTTPTransport( + configuration: passkeyMockConfiguration(), + maximumResponseBytes: 8 + ) + do { + _ = try await transport.execute(passkeyTransportRequest()) + XCTFail("Expanded compressed response unexpectedly passed") + } catch { + XCTAssertEqual( + error as? PasskeyBackupError, + .challengeServiceResponseTooLarge + ) + } + XCTAssertEqual(transport.inFlightRequestCount, 0) + } + + @available(iOS 15.0, *) + func testURLSessionTransportAcceptsResponseAtExactLimit() async throws { + let expectedBody = Data(repeating: 0x41, count: 8) + PasskeyBackupMockURLProtocol.install { protocolInstance, request in + let response = HTTPURLResponse( + url: request.url!, + statusCode: 200, + httpVersion: nil, + headerFields: ["Content-Length": "8"] + )! + protocolInstance.client?.urlProtocol( + protocolInstance, + didReceive: response, + cacheStoragePolicy: .notAllowed + ) + protocolInstance.client?.urlProtocol(protocolInstance, didLoad: expectedBody) + protocolInstance.client?.urlProtocolDidFinishLoading(protocolInstance) + } + defer { PasskeyBackupMockURLProtocol.reset() } + + let transport = try URLSessionPasskeyBackupHTTPTransport( + configuration: passkeyMockConfiguration(), + maximumResponseBytes: expectedBody.count + ) + let response = try await transport.execute(passkeyTransportRequest()) + + XCTAssertEqual(response.statusCode, 200) + XCTAssertEqual(response.body, expectedBody) + XCTAssertEqual(transport.inFlightRequestCount, 0) + } + + @available(iOS 15.0, *) + func testURLSessionTransportCancellationStopsTaskAndClearsState() async throws { + let started = expectation(description: "request started") + let stopped = expectation(description: "URL loading stopped") + PasskeyBackupMockURLProtocol.install( + { protocolInstance, request in + let response = HTTPURLResponse( + url: request.url!, + statusCode: 200, + httpVersion: nil, + headerFields: nil + )! + protocolInstance.client?.urlProtocol( + protocolInstance, + didReceive: response, + cacheStoragePolicy: .notAllowed + ) + started.fulfill() + }, + onStop: { stopped.fulfill() } + ) + defer { PasskeyBackupMockURLProtocol.reset() } + + let transport = try URLSessionPasskeyBackupHTTPTransport( + configuration: passkeyMockConfiguration() + ) + let requestTask = Task { + try await transport.execute(passkeyTransportRequest()) + } + + await fulfillment(of: [started], timeout: 1) + XCTAssertEqual(transport.inFlightRequestCount, 1) + requestTask.cancel() + do { + _ = try await requestTask.value + XCTFail("Cancelled transport request unexpectedly passed") + } catch is CancellationError { + // The transport deliberately preserves structured-concurrency cancellation. + } catch { + XCTFail("Unexpected cancellation error: \(error)") + } + await fulfillment(of: [stopped], timeout: 1) + XCTAssertEqual(transport.inFlightRequestCount, 0) + } + + @available(iOS 15.0, *) + func testURLSessionTransportFailureClearsStateAndRedirectPolicyFailsClosed() async throws { + PasskeyBackupMockURLProtocol.install { protocolInstance, _ in + protocolInstance.client?.urlProtocol( + protocolInstance, + didFailWithError: URLError(.networkConnectionLost) + ) + } + defer { PasskeyBackupMockURLProtocol.reset() } + + let transport = try URLSessionPasskeyBackupHTTPTransport( + configuration: passkeyMockConfiguration() + ) + do { + _ = try await transport.execute(passkeyTransportRequest()) + XCTFail("Network failure unexpectedly passed") + } catch let error as URLError { + XCTAssertEqual(error.code, .networkConnectionLost) + } catch { + XCTFail("Unexpected network error: \(error)") + } + + let hostileRedirect = URLRequest(url: URL(string: "https://attacker.example/capture")!) + XCTAssertNil(PasskeyBackupBoundedSessionDelegate.redirectedRequest(hostileRedirect)) + XCTAssertFalse(PasskeyBackupHTTPTransportPolicy.followsRedirects) + XCTAssertEqual(transport.inFlightRequestCount, 0) + } + + @available(iOS 15.0, *) + func testURLSessionTransportRejectsNonHTTPResponsesAndClearsState() async throws { + PasskeyBackupMockURLProtocol.install { protocolInstance, request in + let response = URLResponse( + url: request.url!, + mimeType: "application/json", + expectedContentLength: 2, + textEncodingName: "utf-8" + ) + protocolInstance.client?.urlProtocol( + protocolInstance, + didReceive: response, + cacheStoragePolicy: .notAllowed + ) + protocolInstance.client?.urlProtocol(protocolInstance, didLoad: Data("{}".utf8)) + protocolInstance.client?.urlProtocolDidFinishLoading(protocolInstance) + } + defer { PasskeyBackupMockURLProtocol.reset() } + + let transport = try URLSessionPasskeyBackupHTTPTransport( + configuration: passkeyMockConfiguration() + ) + do { + _ = try await transport.execute(passkeyTransportRequest()) + XCTFail("Non-HTTP response unexpectedly passed") + } catch { + XCTAssertEqual(error as? PasskeyBackupError, .malformedChallengeServiceResponse) + } + XCTAssertEqual(transport.inFlightRequestCount, 0) + } + + func testRegistrationCompensationRejectsMismatchedRevokeResultIdentities() async throws { + let credentialId = "Y3JlZC0x" + let mismatchedResults = [ + try PasskeyBackupCredentialRevokeResult( + storageKey: "wallet-5678", + credentialId: credentialId, + remainingCredentials: 0 + ), + try PasskeyBackupCredentialRevokeResult( + storageKey: "wallet-1234", + credentialId: "Y3JlZC0y", + remainingCredentials: 0 + ), + try PasskeyBackupCredentialRevokeResult( + storageKey: "wallet-1234", + credentialId: nil, + remainingCredentials: 0 + ) + ] + + for result in mismatchedResults { + let service = LifecycleTestChallengeService(revokeCredentialResult: result) + let workflow = try PasskeyBackupWorkflow( + challengeService: service, + cloudStorage: LifecycleTestCloudStorage( + record: nil, + saveError: PasskeyBackupError.unavailableCloudStorage + ), + backupKeyProvider: FixedTestBackupKeyProvider(), + isReleaseEnabled: true, + createdAtMillisProvider: { 1_767_225_600_000 } + ) + + do { + _ = try await workflow.finishRegistrationWithPlaintext( + pending: pendingRegistration(), + credentialResponseJSON: #"{"id":"Y3JlZC0x"}"#, + plaintextBackup: Data([1, 2, 3]) + ) + XCTFail("Mismatched compensation result unexpectedly passed") + } catch { + let compensationError = try XCTUnwrap( + error as? PasskeyBackupRegistrationCompensationError + ) + XCTAssertEqual( + compensationError.primaryError as? PasskeyBackupError, + .unavailableCloudStorage + ) + XCTAssertEqual(compensationError.failureKind, .revokeFailed) + XCTAssertEqual( + compensationError.cleanupError as? PasskeyBackupError, + .malformedChallengeServiceResponse + ) + } + } + } + + func testWorkflowRejectsRevokeAllResultContainingCredentialIdentity() async throws { + let injectedResult = try PasskeyBackupCredentialRevokeResult( + storageKey: "wallet-1234", + credentialId: "Y3JlZC0x", + remainingCredentials: 0 + ) + let service = LifecycleTestChallengeService(revokeAllResult: injectedResult) + let cloud = LifecycleTestCloudStorage(record: nil) + let workflow = try PasskeyBackupWorkflow( + challengeService: service, + cloudStorage: cloud, + isReleaseEnabled: true + ) + + await assertThrowsAsync { + try await workflow.deleteBackup(storageKey: "wallet-1234") + } + XCTAssertNil(cloud.deletedStorageKey) + } + + @MainActor + @available(iOS 15.0, *) + func testCoordinatorRejectsRevokeAllResultContainingCredentialIdentity() async throws { + let injectedResult = try PasskeyBackupCredentialRevokeResult( + storageKey: "wallet-1234", + credentialId: "Y3JlZC0x", + remainingCredentials: 0 + ) + let service = LifecycleTestChallengeService(revokeAllResult: injectedResult) + let cloud = LifecycleTestCloudStorage(record: nil) + let coordinator = try PasskeyBackupCoordinator( + cloudStorage: cloud, + challengeService: service, + isReleaseEnabled: true + ) + + await assertThrowsAsync { + try await coordinator.deleteEncryptedCloudBackup(storageKey: "wallet-1234") + } + XCTAssertNil(cloud.deletedStorageKey) + } + + @MainActor + @available(iOS 15.0, *) + func testNativeAuthorizationControllerExecutorFailsClosedBehindReleaseFlag() async throws { + let executor = try ASPasskeyBackupCeremonyExecutor( + isReleaseEnabled: false, + presentationAnchorProvider: { ASPresentationAnchor() } + ) + let challenge = try PasskeyBackupRegistrationChallenge( + registrationId: "registration-1234", + challenge: Data(repeating: 1, count: 32), + userId: Data(repeating: 2, count: 32), + userName: "alice@example.com", + displayName: "Alice", + storageKey: "wallet-1234" + ) + let pending = try PendingPasskeyBackupRegistration( + challenge: challenge, + walletId: "wallet-001", + accountName: "alice@example.com" + ) + + await assertThrowsAsync { + _ = try await executor.performRegistration(pending) + } + } + + private func envelopeMetadata( + storageKey: String = "wallet-1234", + walletId: String = "wallet-001", + accountName: String = "alice@example.com", + createdAtMillis: Int64 = 1_767_225_600_000 + ) throws -> PasskeyBackupEnvelopeMetadata { + try PasskeyBackupEnvelopeMetadata( + storageKey: storageKey, + walletId: walletId, + accountName: accountName, + createdAtMillis: createdAtMillis + ) + } + + private func pendingRegistration( + storageKey: String = "wallet-1234" + ) throws -> PendingPasskeyBackupRegistration { + let challenge = try PasskeyBackupRegistrationChallenge( + registrationId: "registration-1234", + challenge: Data(repeating: 1, count: 32), + userId: Data(repeating: 2, count: 32), + userName: "alice@example.com", + displayName: "Alice", + storageKey: storageKey + ) + return try PendingPasskeyBackupRegistration( + challenge: challenge, + walletId: "wallet-001", + accountName: "alice@example.com" + ) + } + + private func httpJSON(_ json: String) -> PasskeyBackupHTTPResponse { + PasskeyBackupHTTPResponse(statusCode: 200, body: Data(json.utf8)) + } + + private func lifecycleService(response: String) throws -> HTTPPasskeyBackupChallengeService { + try HTTPPasskeyBackupChallengeService( + baseURL: "https://backup.fearlesswallet.io", + transport: AuthorizationTestTransport(responses: [httpJSON(response)]), + authorizationProvider: AuthorizationTestProvider(token: "test-token") + ) + } + + private func assertThrowsAsync( + _ operation: () async throws -> Void, + file: StaticString = #filePath, + line: UInt = #line + ) async { + do { + try await operation() + XCTFail("Expected async operation to throw", file: file, line: line) + } catch {} + } + + private func jsonObject(_ json: String) throws -> [String: Any] { + try XCTUnwrap( + JSONSerialization.jsonObject(with: Data(json.utf8)) as? [String: Any] + ) + } + + private func assertInvalidCredentialResponse( + _ expression: () throws -> String, + file: StaticString = #filePath, + line: UInt = #line + ) { + XCTAssertThrowsError(try expression(), file: file, line: line) { error in + XCTAssertEqual( + error as? PasskeyBackupError, + .invalidCredentialResponse, + file: file, + line: line + ) + } + } + + private func base64URL(_ data: Data) -> String { + data.base64EncodedString() + .replacingOccurrences(of: "+", with: "-") + .replacingOccurrences(of: "/", with: "_") + .replacingOccurrences(of: "=", with: "") + } + + private func sha256Base64URL(_ data: Data) -> String { + base64URL(Data(SHA256.hash(data: data))) + } + + @available(iOS 15.0, *) + private func passkeyMockConfiguration() -> URLSessionConfiguration { + let configuration = URLSessionConfiguration.ephemeral + configuration.protocolClasses = [PasskeyBackupMockURLProtocol.self] + return configuration + } + + private func passkeyTransportRequest() -> PasskeyBackupHTTPRequest { + PasskeyBackupHTTPRequest( + method: "POST", + url: URL(string: "https://backup.fearlesswallet.io/test")!, + headers: ["Authorization": "Bearer test-token"], + body: Data("{}".utf8) + ) + } +} + +private final class AuthorizationTestProvider: PasskeyBackupAuthorizationProvider { + private let token: String + private(set) var requests: [PasskeyBackupAuthorizationRequest] = [] + + init(token: String) { + self.token = token + } + + func authorizationToken(for request: PasskeyBackupAuthorizationRequest) async throws -> String { + requests.append(request) + return token + } +} + +private final class AuthorizationTestTransport: PasskeyBackupHTTPTransport { + private var responses: [PasskeyBackupHTTPResponse] + private(set) var requests: [PasskeyBackupHTTPRequest] = [] + + init(responses: [PasskeyBackupHTTPResponse]) { + self.responses = responses + } + + func execute(_ request: PasskeyBackupHTTPRequest) async throws -> PasskeyBackupHTTPResponse { + requests.append(request) + guard !responses.isEmpty else { + throw PasskeyBackupError.malformedChallengeServiceResponse + } + return responses.removeFirst() + } +} + +private final class PasskeyBackupMockURLProtocol: URLProtocol { + typealias Handler = (PasskeyBackupMockURLProtocol, URLRequest) -> Void + + private static let lock = NSLock() + private static var handler: Handler? + private static var onStop: (() -> Void)? + + static func install( + _ handler: @escaping Handler, + onStop: (() -> Void)? = nil + ) { + lock.lock() + self.handler = handler + self.onStop = onStop + lock.unlock() + } + + static func reset() { + lock.lock() + handler = nil + onStop = nil + lock.unlock() + } + + override class func canInit(with _: URLRequest) -> Bool { true } + override class func canonicalRequest(for request: URLRequest) -> URLRequest { request } + + override func startLoading() { + Self.lock.lock() + let handler = Self.handler + Self.lock.unlock() + guard let handler else { + client?.urlProtocol(self, didFailWithError: URLError(.badServerResponse)) + return + } + handler(self, request) + } + + override func stopLoading() { + Self.lock.lock() + let onStop = Self.onStop + Self.lock.unlock() + onStop?() + } +} + +private final class LifecycleTestChallengeService: PasskeyBackupChallengeService { + private let revokeAllError: Error? + private let revokeCredentialError: Error? + private let completeRegistrationError: Error? + private let registrationStorageKey: String + private let credentialListResult: PasskeyBackupCredentialListResult? + private let revokeCredentialResult: PasskeyBackupCredentialRevokeResult? + private let revokeAllResult: PasskeyBackupCredentialRevokeResult? + private(set) var revokedAllStorageKey: String? + private(set) var completedRegistrationId: String? + private(set) var completedAssertionId: String? + private(set) var revokedCredentialStorageKey: String? + private(set) var revokedCredentialId: String? + + init( + revokeAllError: Error? = nil, + revokeCredentialError: Error? = nil, + completeRegistrationError: Error? = nil, + registrationStorageKey: String = "wallet-1234", + credentialListResult: PasskeyBackupCredentialListResult? = nil, + revokeCredentialResult: PasskeyBackupCredentialRevokeResult? = nil, + revokeAllResult: PasskeyBackupCredentialRevokeResult? = nil + ) { + self.revokeAllError = revokeAllError + self.revokeCredentialError = revokeCredentialError + self.completeRegistrationError = completeRegistrationError + self.registrationStorageKey = registrationStorageKey + self.credentialListResult = credentialListResult + self.revokeCredentialResult = revokeCredentialResult + self.revokeAllResult = revokeAllResult + } + + func registrationChallenge( + walletId _: String, + accountName _: String, + displayName _: String + ) async throws -> PasskeyBackupRegistrationChallenge { + throw PasskeyBackupError.malformedChallengeServiceResponse + } + + func completeRegistration( + registrationId: String, + credentialResponseJSON _: String + ) async throws -> PasskeyBackupChallengeResult { + completedRegistrationId = registrationId + if let completeRegistrationError { + throw completeRegistrationError + } + return try PasskeyBackupChallengeResult(storageKey: registrationStorageKey) + } + + func assertionChallenge(storageKey _: String) async throws -> PasskeyBackupAssertionChallenge { + throw PasskeyBackupError.malformedChallengeServiceResponse + } + + func completeAssertion( + assertionId: String, + credentialResponseJSON _: String + ) async throws -> PasskeyBackupChallengeResult { + completedAssertionId = assertionId + return try PasskeyBackupChallengeResult(storageKey: "wallet-1234") + } + + func revokeAllCredentials(storageKey: String) async throws -> PasskeyBackupCredentialRevokeResult { + revokedAllStorageKey = storageKey + if let revokeAllError { + throw revokeAllError + } + if let revokeAllResult { + return revokeAllResult + } + return try PasskeyBackupCredentialRevokeResult( + storageKey: storageKey, + credentialId: nil, + remainingCredentials: 0 + ) + } + + func listCredentials(storageKey: String) async throws -> PasskeyBackupCredentialListResult { + if let credentialListResult { + return credentialListResult + } + return try PasskeyBackupCredentialListResult(storageKey: storageKey, credentials: []) + } + + func revokeCredential( + storageKey: String, + credentialId: String + ) async throws -> PasskeyBackupCredentialRevokeResult { + revokedCredentialStorageKey = storageKey + revokedCredentialId = credentialId + if let revokeCredentialError { + throw revokeCredentialError + } + if let revokeCredentialResult { + return revokeCredentialResult + } + return try PasskeyBackupCredentialRevokeResult( + storageKey: storageKey, + credentialId: credentialId, + remainingCredentials: 0 + ) + } +} + +private final class LifecycleTestCloudStorage: PasskeyBackupCloudStorage { + private var record: PasskeyBackupEncryptedRecord? + private let saveError: Error? + private let beforeDelete: () -> Void + private(set) var deletedStorageKey: String? + + init( + record: PasskeyBackupEncryptedRecord?, + saveError: Error? = nil, + beforeDelete: @escaping () -> Void = {} + ) { + self.record = record + self.saveError = saveError + self.beforeDelete = beforeDelete + } + + func savePasskeyBackup(_ record: PasskeyBackupEncryptedRecord) async throws { + if let saveError { + throw saveError + } + self.record = record + } + + func loadPasskeyBackup(storageKey: String) async throws -> PasskeyBackupEncryptedRecord? { + guard record?.storageKey == storageKey else { + return nil + } + return record + } + + func deletePasskeyBackup(storageKey: String) async throws { + beforeDelete() + deletedStorageKey = storageKey + record = nil + } +} + +private final class FixedTestBackupKeyProvider: RecoverablePasskeyBackupKeyProvider { + func backupKey(for _: PasskeyBackupEnvelopeMetadata) async throws -> Data { + Data((1 ... 32).map(UInt8.init)) + } +} + +private actor CancellationSaveGate { + private var saveStarted = false + private var saveReleased = false + private var startWaiters: [CheckedContinuation] = [] + private var releaseWaiters: [CheckedContinuation] = [] + + func waitUntilSaveStarted() async { + guard !saveStarted else { + return + } + await withCheckedContinuation { continuation in + startWaiters.append(continuation) + } + } + + func startSaveAndWaitForRelease() async { + if !saveStarted { + saveStarted = true + let waiters = startWaiters + startWaiters.removeAll() + waiters.forEach { $0.resume() } + } + guard !saveReleased else { + return + } + await withCheckedContinuation { continuation in + releaseWaiters.append(continuation) + } + } + + func releaseSave() { + guard !saveReleased else { + return + } + saveReleased = true + let waiters = releaseWaiters + releaseWaiters.removeAll() + waiters.forEach { $0.resume() } + } +} + +private actor CancellationCompletionGate { + private var completionStarted = false + private var completionReleased = false + private var startWaiters: [CheckedContinuation] = [] + private var releaseWaiters: [CheckedContinuation] = [] + + func waitUntilCompletionStarted() async { + guard !completionStarted else { + return + } + await withCheckedContinuation { continuation in + startWaiters.append(continuation) + } + } + + func startCompletionAndWaitForRelease() async { + if !completionStarted { + completionStarted = true + let waiters = startWaiters + startWaiters.removeAll() + waiters.forEach { $0.resume() } + } + guard !completionReleased else { + return + } + await withCheckedContinuation { continuation in + releaseWaiters.append(continuation) + } + } + + func releaseCompletion() { + guard !completionReleased else { + return + } + completionReleased = true + let waiters = releaseWaiters + releaseWaiters.removeAll() + waiters.forEach { $0.resume() } + } +} + +private final class CancellationGateCloudStorage: PasskeyBackupCloudStorage { + private let gate: CancellationSaveGate + + init(gate: CancellationSaveGate) { + self.gate = gate + } + + func savePasskeyBackup(_: PasskeyBackupEncryptedRecord) async throws { + await gate.startSaveAndWaitForRelease() + try Task.checkCancellation() + throw PasskeyBackupError.unavailableCloudStorage + } + + func loadPasskeyBackup(storageKey _: String) async throws -> PasskeyBackupEncryptedRecord? { + nil + } + + func deletePasskeyBackup(storageKey _: String) async throws {} +} + +private actor BlockingRevokeGate { + private var revokeStarted = false + private var revokeReleased = false + private var startWaiters: [CheckedContinuation] = [] + private var releaseWaiters: [CheckedContinuation] = [] + + func waitUntilRevokeStarted() async { + guard !revokeStarted else { + return + } + await withCheckedContinuation { continuation in + startWaiters.append(continuation) + } + } + + func startRevokeAndWaitForRelease() async { + if !revokeStarted { + revokeStarted = true + let waiters = startWaiters + startWaiters.removeAll() + waiters.forEach { $0.resume() } + } + guard !revokeReleased else { + return + } + await withCheckedContinuation { continuation in + releaseWaiters.append(continuation) + } + } + + func releaseRevoke() { + guard !revokeReleased else { + return + } + revokeReleased = true + let waiters = releaseWaiters + releaseWaiters.removeAll() + waiters.forEach { $0.resume() } + } +} + +private struct CompensationRevocationSnapshot: Equatable { + let storageKey: String? + let credentialId: String? + let observedCancellation: Bool? +} + +private actor CompensationRevocationState { + private var storageKey: String? + private var credentialId: String? + private var observedCancellation: Bool? + + func recordStart(wasCancelled: Bool) { + observedCancellation = wasCancelled + } + + func recordSuccess(storageKey: String, credentialId: String) { + self.storageKey = storageKey + self.credentialId = credentialId + } + + func snapshot() -> CompensationRevocationSnapshot { + CompensationRevocationSnapshot( + storageKey: storageKey, + credentialId: credentialId, + observedCancellation: observedCancellation + ) + } +} + +private final class AdversarialCompensationChallengeService: PasskeyBackupChallengeService { + private let revokeGate: BlockingRevokeGate? + private let completionGate: CancellationCompletionGate? + private let state = CompensationRevocationState() + + init( + revokeGate: BlockingRevokeGate? = nil, + completionGate: CancellationCompletionGate? = nil + ) { + self.revokeGate = revokeGate + self.completionGate = completionGate + } + + func registrationChallenge( + walletId _: String, + accountName _: String, + displayName _: String + ) async throws -> PasskeyBackupRegistrationChallenge { + throw PasskeyBackupError.malformedChallengeServiceResponse + } + + func completeRegistration( + registrationId _: String, + credentialResponseJSON _: String + ) async throws -> PasskeyBackupChallengeResult { + if let completionGate { + await completionGate.startCompletionAndWaitForRelease() + do { + try Task.checkCancellation() + } catch { + throw PasskeyBackupError.registrationCompletionOutcomeUnknown + } + } + return try PasskeyBackupChallengeResult(storageKey: "wallet-1234") + } + + func assertionChallenge(storageKey _: String) async throws -> PasskeyBackupAssertionChallenge { + throw PasskeyBackupError.malformedChallengeServiceResponse + } + + func completeAssertion( + assertionId _: String, + credentialResponseJSON _: String + ) async throws -> PasskeyBackupChallengeResult { + throw PasskeyBackupError.malformedChallengeServiceResponse + } + + func revokeCredential( + storageKey: String, + credentialId: String + ) async throws -> PasskeyBackupCredentialRevokeResult { + await state.recordStart(wasCancelled: Task.isCancelled) + if let revokeGate { + await revokeGate.startRevokeAndWaitForRelease() + } + try Task.checkCancellation() + await state.recordSuccess(storageKey: storageKey, credentialId: credentialId) + return try PasskeyBackupCredentialRevokeResult( + storageKey: storageKey, + credentialId: credentialId, + remainingCredentials: 0 + ) + } + + func revokeAllCredentials(storageKey _: String) async throws -> PasskeyBackupCredentialRevokeResult { + throw PasskeyBackupError.malformedChallengeServiceResponse + } + + func revocationSnapshot() async -> CompensationRevocationSnapshot { + await state.snapshot() + } +} + +private struct CompensationCompletionSnapshot { + let isFinished: Bool + let didPreserveOriginalError: Bool +} + +private actor CompensationCompletionProbe { + private var isFinished = false + private var didPreserveOriginalError = false + + func record(didPreserveOriginalError: Bool) { + isFinished = true + self.didPreserveOriginalError = didPreserveOriginalError + } + + func snapshot() -> CompensationCompletionSnapshot { + CompensationCompletionSnapshot( + isFinished: isFinished, + didPreserveOriginalError: didPreserveOriginalError + ) + } +} diff --git a/fearlessTests/TonPendingIntentJournalTests.swift b/fearlessTests/TonPendingIntentJournalTests.swift new file mode 100644 index 0000000000..089e1bb5d3 --- /dev/null +++ b/fearlessTests/TonPendingIntentJournalTests.swift @@ -0,0 +1,275 @@ +import CryptoKit +@testable import fearless +import SoraKeystore +import TonSwift +import XCTest + +final class TonPendingIntentJournalTests: XCTestCase { + private static let now: UInt64 = 1_700_000_000 + private static let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" + private static let account = try! TonKeyDerivation.deriveAccount(mnemonic: mnemonic) + private static let senderRaw = try! TonSwift.Address.parse(account.addressNonBounceable).toRaw() + private static let recipientRaw = try! TonSwift.Address.parse( + "EQAimhPwOYc5Z1JP_pddxo82SHOl67T0Lklw91pKtSX2Q094" + ).toRaw() + + func testVersionedJournalRoundTripRebuildsExactBearerWithoutSecrets() throws { + let pending = try makePending(emulated: true) + let inspection = try TonTransferTransactionBuilder.inspectSignedMessage( + pending.message.boc + ) + XCTAssertEqual(inspection.messageHashHex, pending.message.messageHashHex) + XCTAssertEqual( + inspection.signingPayloadHashHex, + pending.message.signingPayloadHashHex + ) + + let encoded = try TonPendingIntentJournalCodec.encode(pending) + let decoded = try TonPendingIntentJournalCodec.decode( + encoded, + expectedSenderRaw: Self.senderRaw + ) + + XCTAssertEqual(decoded, pending) + XCTAssertEqual( + pending.feeQuote?.quoteIDHex, + "0eb547b83019bdb5d66d62e35bc31053c00cbd68ba80599e9af1ccddd17e6958" + ) + XCTAssertEqual( + Data(SHA256.hash(data: encoded)).map { String(format: "%02x", $0) }.joined(), + "3238980e4f69c2fdd20fa2666a713771b4bec2ee4154255f154f827679a05067" + ) + XCTAssertLessThanOrEqual(encoded.count, TonPendingIntentJournalCodec.maximumRecordBytes) + let text = try XCTUnwrap(String(data: encoded, encoding: .utf8)) + XCTAssertFalse(text.contains(Self.mnemonic)) + XCTAssertFalse(text.contains(Self.account.privateKey.base64EncodedString())) + XCTAssertTrue(text.contains(pending.message.messageHashHex)) + XCTAssertTrue(text.contains(pending.feeQuote!.quoteIDHex)) + + var confirmed = pending + confirmed.confirmed = true + let confirmedData = try TonPendingIntentJournalCodec.encode(confirmed) + XCTAssertTrue(String(decoding: confirmedData, as: UTF8.self).contains("\"phase\":\"confirmed\"")) + XCTAssertEqual( + try TonPendingIntentJournalCodec.decode( + confirmedData, + expectedSenderRaw: Self.senderRaw + ), + confirmed + ) + } + + func testCodecRejectsNoncanonicalCorruptAndCrossSenderRecords() throws { + let encoded = try TonPendingIntentJournalCodec.encode(makePending(emulated: false)) + let text = try XCTUnwrap(String(data: encoded, encoding: .utf8)) + let messageHash = try XCTUnwrap( + JSONSerialization.jsonObject(with: encoded) as? [String: Any] + )["signedMessageHashHex"] as! String + let quoteID = try XCTUnwrap( + JSONSerialization.jsonObject(with: encoded) as? [String: Any] + )["quoteIDHex"] as! String + + let corruptions: [Data] = [ + Data(), + Data(repeating: 0x41, count: TonPendingIntentJournalCodec.maximumRecordBytes + 1), + Data("not-json".utf8), + Data((" " + text).utf8), + replacing(text, "\"schemaVersion\":1", with: "\"schemaVersion\":2"), + replacing(text, "\"possiblyExposed\"", with: "\"unknownPhase123\""), + replacing(text, "\"native-ton\"", with: "\"jettonxxxx\""), + replacing(text, "\"mainnet\"", with: "\"testnet\""), + replacing(text, Self.senderRaw, with: Self.recipientRaw), + replacing(text, "\"amountNanotons\":\"100000000\"", with: "\"amountNanotons\":\"000000001\""), + replacing(text, TonAPIClientFactory.canonicalAuthenticatedOrigin.absoluteString, with: "https://evil.example"), + replacing(text, messageHash, with: "A" + messageHash.dropFirst()), + replacing(text, quoteID, with: String(repeating: "0", count: 64)), + replacing(text, "\"quotedFeeNanotons\":12345", with: "\"quotedFeeNanotons\":0"), + replacingFirstBase64Byte(text, key: "signedBocBase64"), + try replacingWithStructurallyValidInvalidSignature(text, pending: makePending(emulated: false)), + replacingFirstBase64Byte(text, key: "unsignedBocBase64"), + Data(text.dropLast().utf8) + ] + + for (index, corruption) in corruptions.enumerated() { + XCTAssertThrowsError( + try TonPendingIntentJournalCodec.decode( + corruption, + expectedSenderRaw: Self.senderRaw + ), + "corruption \(index) unexpectedly decoded" + ) { error in + XCTAssertEqual(error as? TonPendingIntentJournalError, .corrupted) + } + } + + XCTAssertThrowsError( + try TonPendingIntentJournalCodec.decode( + encoded, + expectedSenderRaw: Self.recipientRaw + ) + ) { error in + XCTAssertEqual(error as? TonPendingIntentJournalError, .corrupted) + } + } + + func testJournalConflictAndKeychainFailuresFailClosed() throws { + let journal = TonInMemoryPendingIntentJournal() + let first = try makePending(emulated: false) + try journal.save(first) + XCTAssertEqual(try journal.load(senderRaw: Self.senderRaw), first) + + var conflicting = first + let alteredMessage = try TonTransferTransactionBuilder.buildAndSign( + request: TonTransferTransactionRequest( + senderAddress: Self.senderRaw, + recipientAddress: Self.recipientRaw, + amountNanotons: "100000001", + sequenceNumber: 7, + includeStateInit: false, + validUntil: Self.now + 120, + bounce: true, + comment: "journal" + ), + privateKeySeed: Self.account.privateKey, + now: Self.now + ) + conflicting = TonPendingSignedIntent( + identity: first.identity, + intent: first.intent, + message: alteredMessage, + walletState: first.walletState, + feeQuote: first.feeQuote, + emulation: nil + ) + XCTAssertThrowsError(try journal.save(conflicting)) { error in + XCTAssertEqual(error as? TonPendingIntentJournalError, .conflict) + } + XCTAssertThrowsError( + try journal.delete( + senderRaw: Self.senderRaw, + expectedMessageHashHex: String(repeating: "0", count: 64) + ) + ) { error in + XCTAssertEqual(error as? TonPendingIntentJournalError, .conflict) + } + + let keychainJournal = TonKeychainPendingIntentJournal( + keystore: AlwaysFailingKeystore() + ) + XCTAssertThrowsError(try keychainJournal.load(senderRaw: Self.senderRaw)) { error in + XCTAssertEqual(error as? TonPendingIntentJournalError, .unavailable) + } + XCTAssertThrowsError(try keychainJournal.save(first)) { error in + XCTAssertEqual(error as? TonPendingIntentJournalError, .unavailable) + } + XCTAssertThrowsError( + try keychainJournal.delete( + senderRaw: Self.senderRaw, + expectedMessageHashHex: first.message.messageHashHex + ) + ) { error in + XCTAssertEqual(error as? TonPendingIntentJournalError, .unavailable) + } + } + + private func makePending(emulated: Bool) throws -> TonPendingSignedIntent { + let details = TonNativeSendRequest( + mnemonic: Self.mnemonic, + senderAddress: Self.senderRaw, + recipientAddress: Self.recipientRaw, + amountNanotons: "100000000", + bounce: true, + comment: "journal" + ) + let identity = try TonTransferIntentIdentity(request: details) + let intent = try TonEmulationIntent(request: details) + let walletState = TonWalletRemoteState(sequenceNumber: 7, isInitialized: true) + let transaction = TonTransferTransactionRequest( + senderAddress: identity.sender, + recipientAddress: identity.recipient, + amountNanotons: identity.amountNanotons, + sequenceNumber: walletState.sequenceNumber, + includeStateInit: false, + validUntil: Self.now + 120, + bounce: identity.bounce, + comment: identity.comment + ) + let unsigned = try TonTransferTransactionBuilder.buildForFeeEstimation( + request: transaction, + publicKey: Self.account.publicKey, + now: Self.now + ) + let quote = try TonTransferFeeQuote( + templateCreatedAt: Self.now, + issuedAt: Self.now, + expiresAt: Self.now + 30, + endpointOrigin: TonAPIClientFactory.canonicalAuthenticatedOrigin.absoluteString, + publicKey: Self.account.publicKey, + identity: identity, + intent: intent, + transactionRequest: transaction, + walletState: walletState, + unsignedMessage: unsigned, + feeNanotons: 12_345 + ) + let signed = try TonTransferTransactionBuilder.buildAndSign( + request: transaction, + privateKeySeed: Self.account.privateKey, + now: Self.now + ) + return TonPendingSignedIntent( + identity: identity, + intent: intent, + message: signed, + walletState: walletState, + feeQuote: quote, + emulation: emulated + ? TonEmulationResult(accepted: true, totalFeeNanotons: 12_345) + : nil + ) + } + + private func replacing(_ source: String, _ target: String, with replacement: String) -> Data { + let mutated = source.replacingOccurrences(of: target, with: replacement) + XCTAssertNotEqual(mutated, source, "mutation target missing: \(target)") + return Data(mutated.utf8) + } + + private func replacingFirstBase64Byte(_ source: String, key: String) -> Data { + let object = try! JSONSerialization.jsonObject(with: Data(source.utf8)) as! [String: Any] + let base64 = object[key] as! String + let replacement = (base64.first == "A" ? "B" : "A") + base64.dropFirst() + return replacing(source, base64, with: replacement) + } + + private func replacingWithStructurallyValidInvalidSignature( + _ source: String, + pending: TonPendingSignedIntent + ) throws -> Data { + let quote = try XCTUnwrap(pending.feeQuote) + let invalid = try TonTransferTransactionBuilder.rebuildSignedMessage( + request: quote.transactionRequest, + publicKey: quote.publicKey, + signature: Data(repeating: 0, count: 64), + now: quote.templateCreatedAt + ) + let withBoc = source.replacingOccurrences( + of: pending.message.bocBase64, + with: invalid.bocBase64 + ) + let withHash = withBoc.replacingOccurrences( + of: pending.message.messageHashHex, + with: invalid.messageHashHex + ) + XCTAssertNotEqual(withHash, source) + return Data(withHash.utf8) + } + + private final class AlwaysFailingKeystore: KeystoreProtocol { + func addKey(_: Data, with _: String) throws { throw KeystoreError.unexpectedFail } + func updateKey(_: Data, with _: String) throws { throw KeystoreError.unexpectedFail } + func fetchKey(for _: String) throws -> Data { throw KeystoreError.unexpectedFail } + func checkKey(for _: String) throws -> Bool { throw KeystoreError.unexpectedFail } + func deleteKey(for _: String) throws { throw KeystoreError.unexpectedFail } + } +} diff --git a/fearlessTests/TonTransferTransactionBuilderTests.swift b/fearlessTests/TonTransferTransactionBuilderTests.swift new file mode 100644 index 0000000000..9f6ba4a961 --- /dev/null +++ b/fearlessTests/TonTransferTransactionBuilderTests.swift @@ -0,0 +1,2745 @@ +import CryptoKit +import Foundation +import HTTPTypes +import OpenAPIRuntime +import TonAPI +import TonSwift +import XCTest + +@testable import fearless + +final class TonTransferTransactionBuilderTests: XCTestCase { + private static let now: UInt64 = 1_680_178_900 + private static let validUntil: UInt64 = 1_680_179_023 + private static let privateKey = Data(hexString: "34aebb9ea454967f16c407c0f8877763e86212116468169d93a3dcbcafe530c9")! + private static let sender = "UQDnBF4JTFKHTYjulEJyNd4dstLGH1m51UrLdu01_tw4zz3r" + private static let recipient = "EQAimhPwOYc5Z1JP_pddxo82SHOl67T0Lklw91pKtSX2Q094" + + func testBuildsCanonicalWalletV4R2ExternalMessageGoldenVector() throws { + let result = try build() + + XCTAssertEqual(result.walletAddress, Self.sender) + XCTAssertEqual(result.sequenceNumber, 62) + XCTAssertEqual(result.validUntil, Self.validUntil) + XCTAssertFalse(result.includesStateInit) + XCTAssertEqual(result.bocBase64, "te6cckEBAgEAugAB4YgBzgi8EpilDpsR3SiE5Gu8O2WljD6zc6qVlu3aa/24cZ4AEoDsU2T7wDyTVijEInQfjlDd9yoM2JuJ+WH7lEOWTtcSYClvJKkLv3VQWPcDAYFxj3mA5C78TtoReTfu+qKIeU1NGLshK/p4AAAB8AAcAQCIYgARTQn4HMOcs6kn/0uu40ebJDnS9dp6FyS4e60lWpL7IaAvrwgAAAAAAAAAAAAAAAAAAAAAAABGZWFybGVzcyBUT076wCvr") + XCTAssertEqual(result.boc.count, 201) + XCTAssertEqual(Data(SHA256.hash(data: result.boc)).hexString, "8c1137afabb6d0c817867997c0ba74169c42b39bc6f99ba5d7e62d839f7f47a6") + XCTAssertEqual(result.messageHashHex, "25bdaa831cfc4f67550585691850fcf10b52875682f431a97935346942af07b2") + } + + func testSequenceZeroIncludesStateInitAndProducesStableDeploymentMessage() throws { + let result = try build( + request: makeRequest(sequenceNumber: 0, comment: nil) + ) + + XCTAssertTrue(result.includesStateInit) + // Generated independently with @ton/core 0.63.1 using the same deterministic + // Ed25519 seed and reserialized with { idx: false, crc32: true }. + XCTAssertEqual(result.boc.count, 956) + XCTAssertEqual(Data(result.boc.prefix(16)).hexString, "b5ee9c72410217010003ac0003e38801") + XCTAssertEqual(Data(SHA256.hash(data: result.boc)).hexString, "a5876cf00a39808e90fe70a9c32b1047fa9a71b8348265ebc43941bd08e9e888") + XCTAssertEqual(result.messageHashHex, "6a098be18f97f763e4467ea8ae24826e2270babfa7ad7c7489d79e841a3a38e4") + let roots = try Cell.fromBoc(src: result.boc) + XCTAssertEqual(roots.count, 1) + XCTAssertEqual(roots[0].hash().hexString, result.messageHashHex) + } + + func testVerifiedInitializedWalletAtSequenceZeroCanOmitStateInit() throws { + let result = try build( + request: makeRequest( + sequenceNumber: 0, + includeStateInit: false, + comment: nil + ) + ) + + XCTAssertFalse(result.includesStateInit) + } + + func testFeeEstimationMessageHasCompileTimeDistinctInvalidSignature() throws { + let privateKey = try Curve25519.Signing.PrivateKey(rawRepresentation: Self.privateKey) + let result = try TonTransferTransactionBuilder.buildForFeeEstimation( + request: makeRequest(), + publicKey: privateKey.publicKey.rawRepresentation, + now: Self.now + ) + let root = try XCTUnwrap(Cell.fromBoc(src: result.boc).first) + let externalMessage = try Message.loadFrom(slice: root.beginParse()) + let body = try externalMessage.body.beginParse() + let signature = try body.loadBytes(64) + let signingMessage = try body.loadRemainder() + + XCTAssertEqual(signature, Data(repeating: 0x30, count: 64)) + XCTAssertFalse( + privateKey.publicKey.isValidSignature( + signature, + for: signingMessage.hash() + ) + ) + } + + func testNilCommentUsesCanonicalHashableEmptyMessageBody() throws { + XCTAssertEqual( + try TonTransferTransactionBuilder.messageBodyHashHex(comment: nil), + "96a296d224f285c67bee93c30f8a309157f0daa35dc5b87e410b78630a09cfc7" + ) + } + + func testDeploymentRootHashAndSemanticsStayStableAcrossRepeatedBuilds() throws { + var hashes = Set() + for _ in 0 ..< 100 { + let result = try build(request: makeRequest(sequenceNumber: 0, comment: nil)) + let roots = try Cell.fromBoc(src: result.boc) + XCTAssertEqual(roots.count, 1) + XCTAssertEqual(roots[0].hash().hexString, result.messageHashHex) + XCTAssertTrue(result.includesStateInit) + hashes.insert(result.messageHashHex) + } + + XCTAssertEqual(hashes, ["6a098be18f97f763e4467ea8ae24826e2270babfa7ad7c7489d79e841a3a38e4"]) + } + + func testCanonicalRawRecipientIsAcceptedWithoutNetworkOrBounceAmbiguity() throws { + let rawRecipient = try TonSwift.Address.parse(Self.recipient).toRaw() + let result = try build( + request: makeRequest(recipientAddress: rawRecipient) + ) + + XCTAssertFalse(result.boc.isEmpty) + } + + func testRejectsUnsupportedJettonAndTestnetScopeBeforeKeyOrAddressParsing() { + assertError( + .unsupportedAsset, + request: makeRequest( + asset: .jetton(masterAddress: "malformed-master"), + senderAddress: "malformed-sender", + recipientAddress: "malformed-recipient" + ), + privateKey: Data() + ) + assertError( + .unsupportedNetwork, + request: makeRequest( + network: .testnet, + senderAddress: "malformed-sender", + recipientAddress: "malformed-recipient" + ), + privateKey: Data() + ) + } + + func testRequiresExplicitBouncePolicyForRawRecipientAtServiceBoundary() throws { + let rawRecipient = try TonSwift.Address.parse(Self.recipient).toRaw() + + XCTAssertThrowsError( + try TonTransferTransactionBuilder.requiredBounceFlag(forRecipientAddress: rawRecipient) + ) { error in + XCTAssertEqual(error as? TonTransferTransactionBuilderError, .ambiguousBounceFlag) + } + XCTAssertTrue( + try TonTransferTransactionBuilder.requiredBounceFlag(forRecipientAddress: Self.recipient) + ) + } + + func testRejectsMalformedZeroAndWrongLengthPrivateKeys() { + let invalidKeys = [ + Data(), + Data(repeating: 1, count: 31), + Data(repeating: 1, count: 33), + Data(repeating: 0, count: 32) + ] + + invalidKeys.forEach { key in + assertError(.invalidPrivateKey, privateKey: key) + } + } + + func testRejectsSenderThatDoesNotMatchSigningKey() throws { + let otherKey = Curve25519.Signing.PrivateKey() + let otherAddress = try WalletV4R2( + workchain: 0, + publicKey: otherKey.publicKey.rawRepresentation + ).address().toString(urlSafe: true, testOnly: false, bounceable: false) + + assertError( + .senderKeyMismatch, + request: makeRequest(senderAddress: otherAddress) + ) + } + + func testRejectsMalformedNoncanonicalAndTestnetSenders() throws { + let senderAddress = try TonSwift.Address.parse(Self.sender) + let testnetSender = senderAddress.toString( + urlSafe: true, + testOnly: true, + bounceable: false + ) + let uppercaseRawSender = senderAddress.toRaw().uppercased() + let invalidSenders = [ + "", + " \(Self.sender)", + "\(Self.sender) ", + "not-an-address", + String(Self.sender.dropLast()) + "A", + uppercaseRawSender + ] + + invalidSenders.forEach { sender in + assertError( + .invalidSenderAddress, + request: makeRequest(senderAddress: sender) + ) + } + assertError( + .testnetAddressNotAllowed, + request: makeRequest(senderAddress: testnetSender) + ) + } + + func testRejectsMalformedNoncanonicalTestnetAndUnsupportedRecipients() throws { + let recipientAddress = try TonSwift.Address.parse(Self.recipient) + let testnetRecipient = recipientAddress.toString( + urlSafe: true, + testOnly: true, + bounceable: true + ) + let unsupported = TonSwift.Address( + workchain: 1, + hash: Data(repeating: 0xA5, count: 32) + ).toString(urlSafe: true, testOnly: false, bounceable: true) + let invalidRecipients = [ + "", + " \(Self.recipient)", + "\(Self.recipient) ", + "0:abcd", + "not-an-address", + String(Self.recipient.dropLast()) + "A", + recipientAddress.toRaw().uppercased() + ] + + invalidRecipients.forEach { recipient in + assertError( + .invalidRecipientAddress, + request: makeRequest(recipientAddress: recipient) + ) + } + assertError( + .testnetAddressNotAllowed, + request: makeRequest(recipientAddress: testnetRecipient) + ) + assertError( + .unsupportedWorkchain, + request: makeRequest(recipientAddress: unsupported) + ) + } + + func testRejectsOversizedTextInputsAtCheapClassifierBounds() { + assertError( + .invalidSenderAddress, + request: makeRequest(senderAddress: String(repeating: "A", count: 1_048_576)) + ) + assertError( + .invalidRecipientAddress, + request: makeRequest( + recipientAddress: String( + repeating: "A", + count: TonTransferTransactionBuilder.maximumAddressInputBytes + 1 + ) + ) + ) + assertError( + .invalidAmount, + request: makeRequest( + amountNanotons: String( + repeating: "9", + count: TonTransferTransactionBuilder.maximumAmountDigits + 1 + ) + ) + ) + assertError( + .invalidComment, + request: makeRequest( + comment: String( + repeating: "x", + count: TonTransferTransactionBuilder.maximumCommentBytes + 1 + ) + ) + ) + } + + func testRejectsSelfTransferAndFriendlyAddressBounceMismatch() { + assertError( + .senderEqualsRecipient, + request: makeRequest(recipientAddress: Self.sender, bounce: false) + ) + assertError( + .bounceFlagMismatch, + request: makeRequest(bounce: false) + ) + } + + func testRejectsNoncanonicalZeroFractionalUnicodeAndOversizedAmounts() { + let invalidAmounts = [ + "", + "0", + "00", + "01", + "+1", + "-1", + "1.0", + " 1", + "1 ", + "١", + "1329227995784915872903807060280344576" + ] + + invalidAmounts.forEach { amount in + assertError( + .invalidAmount, + request: makeRequest(amountNanotons: amount) + ) + } + } + + func testAcceptsLargestRepresentableCoinsValue() throws { + let maximum = "1329227995784915872903807060280344575" + let result = try build(request: makeRequest(amountNanotons: maximum)) + + XCTAssertFalse(result.boc.isEmpty) + } + + func testRejectsSequenceNumberThatDoesNotFitWalletV4Contract() { + assertError( + .invalidSequenceNumber, + request: makeRequest(sequenceNumber: UInt64(UInt32.max) + 1) + ) + assertError( + .invalidStateInitPolicy, + request: makeRequest(sequenceNumber: 1, includeStateInit: true) + ) + } + + func testRejectsExpiredShortLivedLongLivedAndOverflowingExpirations() { + let invalidExpirations = [ + Self.now - 1, + Self.now, + Self.now + TonTransferTransactionBuilder.minimumLifetimeSeconds - 1, + Self.now + TonTransferTransactionBuilder.maximumLifetimeSeconds + 1, + UInt64(UInt32.max) + 1 + ] + + invalidExpirations.forEach { validUntil in + assertError( + .invalidExpiration, + request: makeRequest(validUntil: validUntil) + ) + } + + assertError( + .invalidExpiration, + request: makeRequest(), + now: UInt64(UInt32.max) + 1 + ) + } + + func testRejectsEmptyOversizedNoncanonicalAndControlCharacterComments() { + let invalidComments = [ + "", + " ", + "line\nbreak", + "null\0byte", + "right\u{202E}left", + "zero\u{200B}width", + "e" + String(UnicodeScalar(0x301)!), + "arabic" + String(UnicodeScalar(0x061C)!), + "mongolian" + String(UnicodeScalar(0x180E)!), + "private" + String(UnicodeScalar(0xE000)!), + "tag" + String(UnicodeScalar(0xE0001)!), + "noncharacter" + String(UnicodeScalar(0xFDD0)!), + String(repeating: "a", count: TonTransferTransactionBuilder.maximumCommentBytes + 1) + ] + + invalidComments.forEach { comment in + assertError( + .invalidComment, + request: makeRequest(comment: comment) + ) + } + } + + func testAcceptsBoundedCanonicalUnicodeComment() throws { + let result = try build(request: makeRequest(comment: "Fearless TON 🚀")) + + XCTAssertFalse(result.boc.isEmpty) + } + + func testCriticalFieldsAreCryptographicallyBoundIntoMessageHash() throws { + let baseline = try build() + let variants = try [ + build(request: makeRequest(amountNanotons: "100000001")), + build(request: makeRequest(sequenceNumber: 63)), + build(request: makeRequest(validUntil: Self.validUntil + 1)), + build(request: makeRequest(comment: "different")), + build(request: makeRequest(recipientAddress: try alternateRecipient())) + ] + + variants.forEach { variant in + XCTAssertNotEqual(variant.messageHashHex, baseline.messageHashHex) + XCTAssertNotEqual(variant.boc, baseline.boc) + } + XCTAssertEqual(Set(variants.map(\.messageHashHex)).count, variants.count) + } + + func testOutputIsDeterministicBoundedAndRoundTripsThroughTonBocParser() throws { + let first = try build() + let second = try build() + let roots = try Cell.fromBoc(src: first.boc) + + XCTAssertEqual(first, second) + XCTAssertLessThan(first.boc.count, 16 * 1024) + XCTAssertEqual(roots.count, 1) + XCTAssertEqual(roots[0].hash().hexString, first.messageHashHex) + } + + private func build( + request: TonTransferTransactionRequest? = nil, + privateKey: Data = TonTransferTransactionBuilderTests.privateKey, + now: UInt64 = TonTransferTransactionBuilderTests.now + ) throws -> TonSignedExternalMessage { + try TonTransferTransactionBuilder.buildAndSign( + request: request ?? makeRequest(), + privateKeySeed: privateKey, + now: now + ) + } + + private func makeRequest( + asset: TonTransferAsset = .nativeTon, + network: TonTransferNetwork = .mainnet, + senderAddress: String = TonTransferTransactionBuilderTests.sender, + recipientAddress: String = TonTransferTransactionBuilderTests.recipient, + amountNanotons: String = "100000000", + sequenceNumber: UInt64 = 62, + includeStateInit: Bool? = nil, + validUntil: UInt64 = TonTransferTransactionBuilderTests.validUntil, + bounce: Bool = true, + comment: String? = "Fearless TON" + ) -> TonTransferTransactionRequest { + TonTransferTransactionRequest( + asset: asset, + network: network, + senderAddress: senderAddress, + recipientAddress: recipientAddress, + amountNanotons: amountNanotons, + sequenceNumber: sequenceNumber, + includeStateInit: includeStateInit, + validUntil: validUntil, + bounce: bounce, + comment: comment + ) + } + + private func alternateRecipient() throws -> String { + let key = try Curve25519.Signing.PrivateKey( + rawRepresentation: Data(repeating: 0x42, count: 32) + ) + return try WalletV4R2( + workchain: 0, + publicKey: key.publicKey.rawRepresentation + ).address().toString(urlSafe: true, testOnly: false, bounceable: true) + } + + private func assertError( + _ expected: TonTransferTransactionBuilderError, + request: TonTransferTransactionRequest? = nil, + privateKey: Data = TonTransferTransactionBuilderTests.privateKey, + now: UInt64 = TonTransferTransactionBuilderTests.now, + file: StaticString = #filePath, + line: UInt = #line + ) { + XCTAssertThrowsError( + try build(request: request, privateKey: privateKey, now: now), + file: file, + line: line + ) { error in + XCTAssertEqual( + error as? TonTransferTransactionBuilderError, + expected, + file: file, + line: line + ) + } + } +} + +#if DEBUG +final class TonSendServiceTests: XCTestCase { + private static let now: UInt64 = 1_700_000_000 + private static let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" + private static let account = try! TonKeyDerivation.deriveAccount(mnemonic: mnemonic) + private static let sender = account.addressNonBounceable + private static let recipient = "EQAimhPwOYc5Z1JP_pddxo82SHOl67T0Lklw91pKtSX2Q094" + private static let mismatchedSender = "UQDnBF4JTFKHTYjulEJyNd4dstLGH1m51UrLdu01_tw4zz3r" + + func testNativeMainnetSendEmulatesAndBroadcastsTheExactSignedBoc() async throws { + let remote = FakeRemote() + remote.walletState = TonWalletRemoteState(sequenceNumber: 7, isInitialized: true) + remote.emulation = TonEmulationResult(accepted: true, totalFeeNanotons: 12_345) + + let result = try await makeService(remote: remote).sendUnquotedForTesting(makeRequest()) + + XCTAssertEqual(remote.callOrder, ["walletState", "emulate", "broadcast"]) + XCTAssertEqual(remote.walletAddresses, [Self.sender]) + XCTAssertEqual(remote.emulatedBocs, [result.prepared.signedMessage.bocBase64]) + XCTAssertEqual(remote.broadcastBocs, remote.emulatedBocs) + XCTAssertEqual(result.prepared.emulation.totalFeeNanotons, 12_345) + XCTAssertEqual(result.prepared.signedMessage.sequenceNumber, 7) + XCTAssertEqual(result.prepared.signedMessage.validUntil, Self.now + 120) + XCTAssertFalse(result.prepared.signedMessage.includesStateInit) + + let broadcast = try XCTUnwrap(Data(base64Encoded: try XCTUnwrap(remote.broadcastBocs.first))) + let roots = try Cell.fromBoc(src: broadcast) + XCTAssertEqual(roots.count, 1) + XCTAssertEqual(roots[0].hash().hexString, result.messageHashHex) + } + + func testEstimateReturnsPositiveEmulatedFeeWithoutBroadcast() async throws { + let remote = FakeRemote() + remote.emulation = TonEmulationResult(accepted: true, totalFeeNanotons: 777) + + let result = try await makeService(remote: remote).estimate(makeEstimateRequest()) + + XCTAssertEqual(result.emulation.totalFeeNanotons, 777) + XCTAssertEqual(remote.callOrder, ["walletState", "emulate"]) + XCTAssertEqual(remote.unsignedEmulationCount, 1) + XCTAssertEqual(remote.signedEmulationCount, 0) + XCTAssertTrue(remote.broadcastBocs.isEmpty) + } + + func testBoundQuoteReproducesExactTemplateAndExactFeeBeforeBroadcast() async throws { + let remote = FakeRemote() + remote.walletState = TonWalletRemoteState(sequenceNumber: 7, isInitialized: true) + remote.emulation = TonEmulationResult(accepted: true, totalFeeNanotons: 12_345) + let service = makeService(remote: remote) + + let quote = try await service.quote(makeEstimateRequest()) + let result = try await service.send(makeRequest(), feeQuote: quote) + + XCTAssertEqual(quote.feeNanotons, 12_345) + XCTAssertEqual( + quote.unsignedMessage.signingPayloadHashHex, + result.prepared.signedMessage.signingPayloadHashHex + ) + XCTAssertNotEqual(quote.unsignedMessage.boc, result.prepared.signedMessage.boc) + XCTAssertEqual(result.prepared.emulation.totalFeeNanotons, quote.feeNanotons) + XCTAssertEqual(remote.unsignedEmulationCount, 1) + XCTAssertEqual(remote.signedEmulationCount, 1) + XCTAssertEqual(remote.broadcastBocs, [result.prepared.signedMessage.bocBase64]) + XCTAssertEqual( + remote.callOrder, + ["walletState", "emulate", "walletState", "emulate", "broadcast"] + ) + } + + func testFreshSendRequiresQuoteBeforeMnemonicDerivationOrRemoteWork() async { + let remote = FakeRemote() + let service = makeService(remote: remote) + + let error: Error? + do { + _ = try await service.send( + makeRequest(mnemonic: "this must never be parsed"), + feeQuote: nil + ) + error = nil + } catch let caught { + error = caught + } + + XCTAssertEqual(error as? TonSendServiceError, .feeQuoteRequired) + XCTAssertTrue(remote.callOrder.isEmpty) + XCTAssertTrue(remote.recipientInspectionAddresses.isEmpty) + XCTAssertTrue(remote.emulatedBocs.isEmpty) + XCTAssertTrue(remote.broadcastBocs.isEmpty) + } + + func testQuoteMutationExpiryWalletDriftAndFeeDriftAllFailClosed() async throws { + let quoteRemote = FakeRemote() + quoteRemote.walletState = TonWalletRemoteState(sequenceNumber: 7, isInitialized: true) + quoteRemote.emulation = TonEmulationResult(accepted: true, totalFeeNanotons: 100) + let quote = try await makeService(remote: quoteRemote).quote(makeEstimateRequest()) + + let mismatchedRemote = FakeRemote() + let mismatch = await sendError( + quotedService: makeService(remote: mismatchedRemote), + request: makeRequest(amountNanotons: "100000001"), + quote: quote + ) + XCTAssertEqual(mismatch as? TonSendServiceError, .feeQuoteMismatch) + XCTAssertTrue(mismatchedRemote.callOrder.isEmpty) + + let clock = LockedClock(Self.now + TonTransferFeeQuote.maximumAgeSeconds + 1) + let expiredRemote = FakeRemote() + let expiredService = TonSendService( + remote: expiredRemote, + pendingCoordinator: TonPendingIntentCoordinator(), + clock: { clock.value } + ) + let expired = await sendError( + quotedService: expiredService, + request: makeRequest(), + quote: quote + ) + XCTAssertEqual(expired as? TonSendServiceError, .feeQuoteExpired) + XCTAssertTrue(expiredRemote.callOrder.isEmpty) + + let driftRemote = FakeRemote() + driftRemote.walletState = TonWalletRemoteState(sequenceNumber: 8, isInitialized: true) + let stateDrift = await sendError( + quotedService: makeService(remote: driftRemote), + request: makeRequest(), + quote: quote + ) + XCTAssertEqual(stateDrift as? TonSendServiceError, .walletStateChangedSinceQuote) + XCTAssertEqual(driftRemote.callOrder, ["walletState"]) + XCTAssertEqual(driftRemote.signedEmulationCount, 0) + XCTAssertTrue(driftRemote.broadcastBocs.isEmpty) + + let feeDriftRemote = FakeRemote() + feeDriftRemote.walletState = quote.walletState + feeDriftRemote.emulation = TonEmulationResult(accepted: true, totalFeeNanotons: 101) + let feeDrift = await sendError( + quotedService: makeService(remote: feeDriftRemote), + request: makeRequest(), + quote: quote + ) + assertUnknownOutcome(feeDrift) + XCTAssertEqual(feeDriftRemote.signedEmulationCount, 1) + XCTAssertTrue(feeDriftRemote.broadcastBocs.isEmpty) + XCTAssertEqual(feeDriftRemote.reconciledBocs.count, 3) + } + + func testProcessRestartLoadsJournalAndReconcilesBeforeAnyRebroadcast() async throws { + let quoteRemote = FakeRemote() + quoteRemote.walletState = TonWalletRemoteState(sequenceNumber: 7, isInitialized: true) + quoteRemote.emulation = TonEmulationResult(accepted: true, totalFeeNanotons: 100) + let quote = try await makeService(remote: quoteRemote).quote(makeEstimateRequest()) + + let journal = TonInMemoryPendingIntentJournal() + let firstRemote = FakeRemote() + firstRemote.walletState = quote.walletState + firstRemote.emulation = TonEmulationResult(accepted: true, totalFeeNanotons: quote.feeNanotons) + firstRemote.reconciliation = .notFound + let firstService = TonSendService( + remote: firstRemote, + pendingCoordinator: TonPendingIntentCoordinator(journal: journal), + clock: { Self.now } + ) + let firstError = await sendError( + quotedService: firstService, + request: makeRequest(), + quote: quote + ) + let firstHash = try XCTUnwrap(unknownOutcomeHash(firstError)) + XCTAssertEqual(firstRemote.broadcastBocs.count, 1) + XCTAssertNotNil(try journal.load(senderRaw: quote.identity.sender)) + + let blockedRemote = FakeRemote() + blockedRemote.reconciliation = .notFound + let blockedError = await sendError( + quotedService: TonSendService( + remote: blockedRemote, + pendingCoordinator: TonPendingIntentCoordinator(journal: journal), + clock: { Self.now } + ), + request: makeRequest(amountNanotons: "100000001"), + quote: nil + ) + XCTAssertEqual(unknownOutcomeHash(blockedError), firstHash) + XCTAssertEqual(blockedRemote.reconciledBocs.count, 3) + XCTAssertTrue(blockedRemote.callOrder.isEmpty) + XCTAssertTrue(blockedRemote.broadcastBocs.isEmpty) + XCTAssertNotNil(try journal.load(senderRaw: quote.identity.sender)) + + let recoveryRemote = FakeRemote() + recoveryRemote.reconciliation = .confirmed + let recoveryService = TonSendService( + remote: recoveryRemote, + pendingCoordinator: TonPendingIntentCoordinator(journal: journal), + clock: { Self.now } + ) + let recovered = try await recoveryService.send(makeRequest(), feeQuote: nil) + + XCTAssertEqual(recovered.messageHashHex, firstHash) + XCTAssertEqual(recoveryRemote.reconciledBocs.count, 1) + XCTAssertTrue(recoveryRemote.callOrder.isEmpty) + XCTAssertTrue(recoveryRemote.emulatedBocs.isEmpty) + XCTAssertTrue(recoveryRemote.broadcastBocs.isEmpty) + let tombstone = try XCTUnwrap(journal.load(senderRaw: quote.identity.sender)) + XCTAssertTrue(tombstone.confirmed) + XCTAssertEqual(tombstone.message.messageHashHex, firstHash) + + // A process restart before the UI acknowledges success must surface the same receipt + // without signing, emulating, or broadcasting another transfer. + let restartedRemote = FakeRemote() + let restarted = try await TonSendService( + remote: restartedRemote, + pendingCoordinator: TonPendingIntentCoordinator(journal: journal), + clock: { Self.now } + ).send(makeRequest(), feeQuote: nil) + XCTAssertEqual(restarted.messageHashHex, firstHash) + XCTAssertTrue(restartedRemote.reconciledBocs.isEmpty) + XCTAssertTrue(restartedRemote.callOrder.isEmpty) + + let differentRemote = FakeRemote() + let differentError = await sendError( + quotedService: TonSendService( + remote: differentRemote, + pendingCoordinator: TonPendingIntentCoordinator(journal: journal), + clock: { Self.now } + ), + request: makeRequest(amountNanotons: "100000001"), + quote: nil + ) + guard let serviceError = differentError as? TonSendServiceError, + case let .priorIntentConfirmed(identity, messageHashHex) = serviceError + else { + return XCTFail("A confirmed prior intent was reported as the new transfer's success") + } + XCTAssertEqual(identity, quote.identity) + XCTAssertEqual(messageHashHex, firstHash) + XCTAssertTrue(differentRemote.reconciledBocs.isEmpty) + XCTAssertTrue(differentRemote.callOrder.isEmpty) + + var stalePending = tombstone + stalePending.confirmed = false + XCTAssertThrowsError(try journal.save(stalePending)) { error in + XCTAssertEqual(error as? TonPendingIntentJournalError, .conflict) + } + let staleCoordinator = TonPendingIntentCoordinator(journal: journal) + guard case .retry = try await staleCoordinator.begin(quote.identity) else { + return XCTFail("Expected the confirmed tombstone to load for recovery") + } + try await staleCoordinator.retainPending(stalePending) + XCTAssertTrue(try XCTUnwrap(journal.load(senderRaw: quote.identity.sender)).confirmed) + + let friendlySender = try TonSwift.Address.parse(raw: quote.identity.sender) + .toFriendly() + .toString() + try await recoveryService.acknowledgeConfirmedTransfer( + senderAddress: friendlySender, + identity: quote.identity, + messageHashHex: firstHash + ) + XCTAssertNil(try journal.load(senderRaw: quote.identity.sender)) + } + + func testFreshDisplayedQuoteNeverRebroadcastsOlderPendingBoc() async throws { + let quoteRemoteA = FakeRemote() + quoteRemoteA.walletState = TonWalletRemoteState(sequenceNumber: 7, isInitialized: true) + quoteRemoteA.emulation = TonEmulationResult(accepted: true, totalFeeNanotons: 100) + let quoteA = try await makeService(remote: quoteRemoteA).quote(makeEstimateRequest()) + + let journal = TonInMemoryPendingIntentJournal() + let firstRemote = FakeRemote() + firstRemote.walletState = quoteA.walletState + firstRemote.emulation = TonEmulationResult(accepted: true, totalFeeNanotons: 100) + firstRemote.reconciliation = .notFound + let firstService = TonSendService( + remote: firstRemote, + pendingCoordinator: TonPendingIntentCoordinator(journal: journal), + clock: { Self.now } + ) + let firstError = await sendError( + quotedService: firstService, + request: makeRequest(), + quote: quoteA + ) + let oldHash = try XCTUnwrap(unknownOutcomeHash(firstError)) + + let quoteRemoteB = FakeRemote() + quoteRemoteB.walletState = quoteA.walletState + quoteRemoteB.emulation = TonEmulationResult(accepted: true, totalFeeNanotons: 101) + let quoteB = try await makeService(remote: quoteRemoteB).quote(makeEstimateRequest()) + XCTAssertNotEqual(quoteA.quoteIDHex, quoteB.quoteIDHex) + + let recoveryRemote = FakeRemote() + recoveryRemote.reconciliation = .notFound + let recoveryService = TonSendService( + remote: recoveryRemote, + pendingCoordinator: TonPendingIntentCoordinator(journal: journal), + clock: { Self.now } + ) + let error = await sendError( + quotedService: recoveryService, + request: makeRequest(), + quote: quoteB + ) + + XCTAssertEqual(unknownOutcomeHash(error), oldHash) + XCTAssertEqual(recoveryRemote.reconciledBocs.count, 3) + XCTAssertTrue(recoveryRemote.emulatedBocs.isEmpty) + XCTAssertTrue(recoveryRemote.broadcastBocs.isEmpty) + } + + func testPersistedRecoveryRequiresStoredCredentialedEndpointBeforeRemoteWork() async throws { + let quoteRemote = FakeRemote() + quoteRemote.walletState = TonWalletRemoteState(sequenceNumber: 7, isInitialized: true) + let quote = try await makeService(remote: quoteRemote).quote(makeEstimateRequest()) + let journal = TonInMemoryPendingIntentJournal() + let firstRemote = FakeRemote() + firstRemote.walletState = quote.walletState + firstRemote.reconciliation = .notFound + let firstError = await sendError( + quotedService: TonSendService( + remote: firstRemote, + pendingCoordinator: TonPendingIntentCoordinator(journal: journal), + clock: { Self.now } + ), + request: makeRequest(), + quote: quote + ) + let oldHash = try XCTUnwrap(unknownOutcomeHash(firstError)) + + for untrustedOrigin in [nil, "https://other-reviewed.example"] as [String?] { + let recoveryRemote = FakeRemote() + recoveryRemote.reviewedSignedOperationOrigin = untrustedOrigin + let error = await sendError( + quotedService: TonSendService( + remote: recoveryRemote, + pendingCoordinator: TonPendingIntentCoordinator(journal: journal), + clock: { Self.now } + ), + request: makeRequest(), + quote: nil + ) + XCTAssertEqual(unknownOutcomeHash(error), oldHash) + XCTAssertTrue(recoveryRemote.reconciledBocs.isEmpty) + XCTAssertTrue(recoveryRemote.emulatedBocs.isEmpty) + XCTAssertTrue(recoveryRemote.broadcastBocs.isEmpty) + } + } + + func testPersistedRecoveryRunsBeforeSigningCredentialsAreRequested() async throws { + let quoteRemote = FakeRemote() + quoteRemote.walletState = TonWalletRemoteState(sequenceNumber: 7, isInitialized: true) + let quote = try await makeService(remote: quoteRemote).quote(makeEstimateRequest()) + let journal = TonInMemoryPendingIntentJournal() + let firstRemote = FakeRemote() + firstRemote.walletState = quote.walletState + firstRemote.reconciliation = .notFound + let firstError = await sendError( + quotedService: TonSendService( + remote: firstRemote, + pendingCoordinator: TonPendingIntentCoordinator(journal: journal), + clock: { Self.now } + ), + request: makeRequest(), + quote: quote + ) + let oldHash = try XCTUnwrap(unknownOutcomeHash(firstError)) + + let recoveryRemote = FakeRemote() + recoveryRemote.reconciliation = .confirmed + let recovered = try await TonSendService( + remote: recoveryRemote, + pendingCoordinator: TonPendingIntentCoordinator(journal: journal), + clock: { Self.now } + ).send( + makeEstimateRequest(), + feeQuote: nil, + signingCredentials: { + XCTFail("Persisted recovery requested signing credentials") + throw TonSendServiceError.invalidAccount + } + ) + + XCTAssertEqual(recovered.messageHashHex, oldHash) + XCTAssertEqual(recoveryRemote.reconciledBocs.count, 1) + XCTAssertTrue(recoveryRemote.emulatedBocs.isEmpty) + XCTAssertTrue(recoveryRemote.broadcastBocs.isEmpty) + } + + func testJournalWriteFailurePreventsEverySignedRemoteExposure() async { + let remote = FakeRemote() + remote.walletState = TonWalletRemoteState(sequenceNumber: 7, isInitialized: true) + let service = TonSendService( + remote: remote, + pendingCoordinator: TonPendingIntentCoordinator(journal: FailOnSaveJournal()), + clock: { Self.now } + ) + + let error = await sendError(service: service, request: makeRequest()) + + XCTAssertEqual(error as? TonPendingIntentJournalError, .unavailable) + XCTAssertEqual(remote.callOrder, ["walletState"]) + XCTAssertEqual(remote.signedEmulationCount, 0) + XCTAssertTrue(remote.emulatedBocs.isEmpty) + XCTAssertTrue(remote.broadcastBocs.isEmpty) + XCTAssertTrue(remote.reconciledBocs.isEmpty) + } + + func testMemoRequiredRecipientStopsBeforeWalletStateOrBearerMessageExposure() async { + let remote = FakeRemote() + remote.memoRequired = true + + let error = await sendError(service: makeService(remote: remote), request: makeRequest()) + + XCTAssertEqual(error as? TonSendServiceError, .recipientMemoRequired) + XCTAssertEqual(remote.recipientInspectionAddresses, [Self.recipient]) + XCTAssertTrue(remote.callOrder.isEmpty) + XCTAssertTrue(remote.emulatedBocs.isEmpty) + XCTAssertTrue(remote.broadcastBocs.isEmpty) + } + + func testUnsupportedJettonAndTestnetFailBeforeAnyRemoteCall() async { + let jettonRemote = FakeRemote() + let jettonError = await sendError( + service: makeService(remote: jettonRemote), + request: makeRequest(asset: .jetton(masterAddress: "malformed-master")) + ) + XCTAssertEqual(jettonError as? TonTransferTransactionBuilderError, .unsupportedAsset) + XCTAssertTrue(jettonRemote.callOrder.isEmpty) + + let testnetRemote = FakeRemote() + let testnetError = await sendError( + service: makeService(remote: testnetRemote), + request: makeRequest(network: .testnet) + ) + XCTAssertEqual(testnetError as? TonTransferTransactionBuilderError, .unsupportedNetwork) + XCTAssertTrue(testnetRemote.callOrder.isEmpty) + } + + func testInvalidMnemonicAndSenderBindingFailBeforeAnyRemoteCall() async { + let mnemonicRemote = FakeRemote() + let mnemonicError = await sendError( + service: makeService(remote: mnemonicRemote), + request: makeRequest(mnemonic: "not a valid mnemonic") + ) + XCTAssertEqual(mnemonicError as? TonSendServiceError, .invalidAccount) + XCTAssertTrue(mnemonicRemote.callOrder.isEmpty) + + let mismatchRemote = FakeRemote() + let mismatchError = await sendError( + service: makeService(remote: mismatchRemote), + request: makeRequest(senderAddress: Self.mismatchedSender) + ) + XCTAssertEqual(mismatchError as? TonTransferTransactionBuilderError, .senderKeyMismatch) + XCTAssertTrue(mismatchRemote.callOrder.isEmpty) + } + + func testMalformedRecipientAmountBounceAndCommentFailBeforeAnyRemoteCall() async { + let cases: [(TonNativeSendRequest, TonTransferTransactionBuilderError)] = [ + (makeRequest(senderAddress: String(repeating: "A", count: 1_048_576)), .invalidSenderAddress), + ( + makeRequest( + recipientAddress: String( + repeating: "A", + count: TonTransferTransactionBuilder.maximumAddressInputBytes + 1 + ) + ), + .invalidRecipientAddress + ), + (makeRequest(recipientAddress: "../recipient"), .invalidRecipientAddress), + (makeRequest(amountNanotons: "0"), .invalidAmount), + (makeRequest(amountNanotons: "01"), .invalidAmount), + (makeRequest(amountNanotons: "9223372036854775808"), .invalidAmount), + (makeRequest(amountNanotons: String(repeating: "9", count: 20)), .invalidAmount), + (makeRequest(amountNanotons: String(repeating: "9", count: 1_048_576)), .invalidAmount), + (makeRequest(bounce: false), .bounceFlagMismatch), + (makeRequest(comment: "line\nbreak"), .invalidComment), + (makeRequest(comment: String(repeating: "x", count: 257)), .invalidComment), + (makeRequest(comment: String(repeating: "x", count: 1_048_576)), .invalidComment) + ] + + for (request, expected) in cases { + let remote = FakeRemote() + let error = await sendError(service: makeService(remote: remote), request: request) + XCTAssertEqual(error as? TonTransferTransactionBuilderError, expected) + XCTAssertTrue(remote.callOrder.isEmpty, "Unexpected remote calls for \(expected)") + } + } + + func testOversizedSequenceFailsAfterStateButBeforeEmulationOrBroadcast() async { + let remote = FakeRemote() + remote.walletState = TonWalletRemoteState( + sequenceNumber: UInt64(UInt32.max) + 1, + isInitialized: true + ) + + let error = await sendError(service: makeService(remote: remote), request: makeRequest()) + + XCTAssertEqual(error as? TonTransferTransactionBuilderError, .invalidSequenceNumber) + XCTAssertEqual(remote.callOrder, ["walletState"]) + } + + func testUninitializedNonzeroSequenceFailsBeforeEmulationAndBroadcast() async { + let remote = FakeRemote() + remote.walletState = TonWalletRemoteState(sequenceNumber: 1, isInitialized: false) + + let error = await sendError(service: makeService(remote: remote), request: makeRequest()) + + XCTAssertEqual(error as? TonSendServiceError, .inconsistentWalletState) + XCTAssertEqual(remote.callOrder, ["walletState"]) + } + + func testUninitializedSequenceZeroIncludesStateInit() async throws { + let remote = FakeRemote() + remote.walletState = TonWalletRemoteState(sequenceNumber: 0, isInitialized: false) + + let result = try await makeService(remote: remote).estimate(makeEstimateRequest()) + + XCTAssertTrue(result.emulationMessage.includesStateInit) + XCTAssertEqual(remote.callOrder, ["walletState", "emulate"]) + } + + func testVerifiedInitializedSequenceZeroOmitsStateInit() async throws { + let remote = FakeRemote() + remote.walletState = TonWalletRemoteState(sequenceNumber: 0, isInitialized: true) + + let result = try await makeService(remote: remote).estimate(makeEstimateRequest()) + + XCTAssertFalse(result.emulationMessage.includesStateInit) + XCTAssertEqual(remote.callOrder, ["walletState", "emulate"]) + } + + func testRejectedEmulationNeverBroadcasts() async { + let remote = FakeRemote() + remote.emulation = TonEmulationResult(accepted: false, totalFeeNanotons: 10) + + let error = await sendError(service: makeService(remote: remote), request: makeRequest()) + + assertUnknownOutcome(error) + XCTAssertEqual(remote.callOrder, ["walletState", "emulate"]) + XCTAssertTrue(remote.broadcastBocs.isEmpty) + } + + func testMissingZeroAndExcessiveEmulationFeesNeverBroadcast() async { + let invalidFees: [UInt64?] = [ + nil, + 0, + TonSendService.defaultMaximumFeeNanotons + 1, + UInt64.max + ] + + for fee in invalidFees { + let remote = FakeRemote() + remote.emulation = TonEmulationResult(accepted: true, totalFeeNanotons: fee) + + let error = await sendError(service: makeService(remote: remote), request: makeRequest()) + + assertUnknownOutcome(error) + XCTAssertEqual(remote.callOrder, ["walletState", "emulate"]) + XCTAssertTrue(remote.broadcastBocs.isEmpty) + } + } + + func testRemoteErrorsStopThePipelineAtTheFailingStage() async { + let stateRemote = FakeRemote() + stateRemote.failureStage = .walletState + let stateError = await sendError(service: makeService(remote: stateRemote), request: makeRequest()) + XCTAssertNotNil(stateError) + XCTAssertEqual(stateRemote.callOrder, ["walletState"]) + + let emulationRemote = FakeRemote() + emulationRemote.failureStage = .emulate + let emulationError = await sendError( + service: makeService(remote: emulationRemote), + request: makeRequest() + ) + XCTAssertNotNil(emulationError) + assertUnknownOutcome(emulationError) + XCTAssertEqual(emulationRemote.callOrder, ["walletState", "emulate"]) + + let broadcastRemote = FakeRemote() + broadcastRemote.failureStage = .broadcast + let broadcastError = await sendError( + service: makeService(remote: broadcastRemote), + request: makeRequest() + ) + XCTAssertNotNil(broadcastError) + assertUnknownOutcome(broadcastError) + XCTAssertEqual(broadcastRemote.callOrder, ["walletState", "emulate", "broadcast"]) + } + + func testEveryRemoteStageHasAnExplicitBoundedTimeout() async { + let expectations: [(FakeRemote.Stage, [String])] = [ + (.recipientInspection, []), + (.walletState, ["walletState"]), + (.emulate, ["walletState", "emulate"]), + (.broadcast, ["walletState", "emulate", "broadcast"]), + (.reconcile, ["walletState", "emulate", "broadcast"]) + ] + + for (stage, order) in expectations { + let remote = FakeRemote() + remote.suspendedStage = stage + let service = TonSendService( + remote: remote, + remoteTimeoutNanoseconds: 10_000_000, + pendingCoordinator: TonPendingIntentCoordinator(), + clock: { Self.now } + ) + + let error = await sendError(service: service, request: makeRequest()) + + if stage == .recipientInspection { + XCTAssertEqual(error as? TonSendServiceError, .recipientInspectionTimedOut) + } else if stage == .walletState { + XCTAssertEqual(error as? TonSendServiceError, .walletStateTimedOut) + } else { + assertUnknownOutcome(error) + } + XCTAssertEqual(remote.callOrder, order) + } + } + + func testTimeoutReturnsEvenWhenRemoteIgnoresCancellationForever() async { + let remote = FakeRemote() + remote.nonResumingStage = .walletState + let service = TonSendService( + remote: remote, + remoteTimeoutNanoseconds: 10_000_000, + pendingCoordinator: TonPendingIntentCoordinator(), + clock: { Self.now } + ) + let startedAt = Date() + + let error = await sendError(service: service, request: makeRequest()) + + XCTAssertEqual(error as? TonSendServiceError, .walletStateTimedOut) + XCTAssertLessThan(Date().timeIntervalSince(startedAt), 1) + XCTAssertEqual(remote.callOrder, ["walletState"]) + } + + func testAlreadyCancelledCallerStartsNoRemoteWork() async { + let remote = FakeRemote() + let service = makeService(remote: remote) + let request = makeRequest() + let task = Task { () -> Error? in + withUnsafeCurrentTask { current in + current?.cancel() + } + do { + _ = try await service.sendUnquotedForTesting(request) + return nil + } catch { + return error + } + } + + let error = await task.value + + XCTAssertTrue(error is CancellationError) + XCTAssertTrue(remote.callOrder.isEmpty) + } + + func testCallerCancellationDuringEmulationPreventsBroadcast() async { + let remote = FakeRemote() + remote.suspendedStage = .emulate + let emulationStarted = expectation(description: "emulation started") + remote.emulateStarted = { emulationStarted.fulfill() } + let service = makeService(remote: remote) + let request = makeRequest() + let task = Task { + try await service.sendUnquotedForTesting(request) + } + + await fulfillment(of: [emulationStarted], timeout: 1) + task.cancel() + + do { + _ = try await task.value + XCTFail("Expected cancellation to stop TON send") + } catch { + assertUnknownOutcome(error) + } + XCTAssertEqual(remote.callOrder, ["walletState", "emulate"]) + XCTAssertTrue(remote.broadcastBocs.isEmpty) + } + + func testConcurrentSameSenderSubmitIsSerializedAcrossServiceInstances() async throws { + let remote = FakeRemote() + remote.suspendedStage = .walletState + let stateStarted = expectation(description: "wallet state started") + remote.walletStateStarted = { stateStarted.fulfill() } + let coordinator = TonPendingIntentCoordinator() + let firstService = TonSendService( + remote: remote, + pendingCoordinator: coordinator, + clock: { Self.now } + ) + let secondService = TonSendService( + remote: remote, + pendingCoordinator: coordinator, + clock: { Self.now } + ) + let request = makeRequest() + let firstTask = Task { try await firstService.sendUnquotedForTesting(request) } + + await fulfillment(of: [stateStarted], timeout: 1) + let secondError = await sendError(service: secondService, request: request) + XCTAssertEqual( + secondError as? TonSendServiceError, + .sendAlreadyInFlight( + senderAddress: try TonSwift.Address.parse(Self.sender).toRaw() + ) + ) + XCTAssertEqual(remote.callOrder, ["walletState"]) + + firstTask.cancel() + do { + _ = try await firstTask.value + XCTFail("Expected the first request to cancel") + } catch is CancellationError { + // Expected. + } catch { + XCTFail("Unexpected first request error: \(error)") + } + XCTAssertTrue(remote.broadcastBocs.isEmpty) + } + + func testUnquotedPendingNeverRetriesWithoutStoredEndpointBinding() async { + let remote = FakeRemote() + remote.reconciliation = .notFound + let service = makeService(remote: remote) + let request = makeRequest() + + let firstError = await sendError(service: service, request: request) + let firstHash = unknownOutcomeHash(firstError) + XCTAssertEqual(remote.broadcastBocs.count, 1) + + let secondError = await sendError(service: service, request: request) + XCTAssertEqual(unknownOutcomeHash(secondError), firstHash) + XCTAssertEqual(remote.broadcastBocs.count, 1) + XCTAssertEqual(Set(remote.broadcastBocs).count, 1) + XCTAssertEqual(remote.signedEmulationCount, 1) + XCTAssertEqual(remote.callOrder, ["walletState", "emulate", "broadcast"]) + XCTAssertEqual(remote.reconciledBocs.count, 3) + } + + func testRetryNeverRebroadcastsAfterReconciliationError() async { + let remote = FakeRemote() + remote.reconciliation = .notFound + let service = makeService(remote: remote) + let request = makeRequest() + let firstHash = unknownOutcomeHash( + await sendError(service: service, request: request) + ) + XCTAssertEqual(remote.broadcastBocs.count, 1) + + remote.failureStage = .reconcile + let retryError = await sendError(service: service, request: request) + + XCTAssertEqual(unknownOutcomeHash(retryError), firstHash) + XCTAssertEqual(remote.broadcastBocs.count, 1) + + // If the first signed emulation exposed a valid bearer BOC but failed before + // recording an emulation result, a retry-side rejection must retain the same + // exact-hash unknown outcome rather than downgrade to a generic fee error. + let reemulationRemote = FakeRemote() + reemulationRemote.failureStage = .emulate + let reemulationService = makeService(remote: reemulationRemote) + let reemulationRequest = makeRequest(comment: "retry-side rejection") + let exposedHash = unknownOutcomeHash( + await sendError(service: reemulationService, request: reemulationRequest) + ) + XCTAssertEqual(reemulationRemote.signedEmulationCount, 1) + XCTAssertTrue(reemulationRemote.broadcastBocs.isEmpty) + + reemulationRemote.failureStage = nil + reemulationRemote.emulation = TonEmulationResult( + accepted: false, + totalFeeNanotons: 100 + ) + let reemulationError = await sendError( + service: reemulationService, + request: reemulationRequest + ) + + XCTAssertEqual(unknownOutcomeHash(reemulationError), exposedHash) + XCTAssertEqual(reemulationRemote.signedEmulationCount, 1) + XCTAssertTrue(reemulationRemote.broadcastBocs.isEmpty) + XCTAssertEqual(Set(reemulationRemote.emulatedBocs).count, 1) + } + + func testRetryNeverRebroadcastsAfterReconciliationTimeout() async { + let remote = FakeRemote() + remote.suspendedStage = .reconcile + let service = TonSendService( + remote: remote, + remoteTimeoutNanoseconds: 10_000_000, + pendingCoordinator: TonPendingIntentCoordinator(), + clock: { Self.now } + ) + let request = makeRequest() + let firstHash = unknownOutcomeHash( + await sendError(service: service, request: request) + ) + XCTAssertEqual(remote.broadcastBocs.count, 1) + + let retryError = await sendError(service: service, request: request) + + XCTAssertEqual(unknownOutcomeHash(retryError), firstHash) + XCTAssertEqual(remote.broadcastBocs.count, 1) + } + + func testExpiredPendingMessageIsNeverRebroadcast() async { + let remote = FakeRemote() + remote.reconciliation = .notFound + let clock = LockedClock(Self.now) + let service = TonSendService( + remote: remote, + pendingCoordinator: TonPendingIntentCoordinator(), + clock: { clock.value } + ) + let request = makeRequest() + let firstHash = unknownOutcomeHash( + await sendError(service: service, request: request) + ) + XCTAssertEqual(remote.broadcastBocs.count, 1) + + clock.value = Self.now + TonSendService.defaultMessageLifetimeSeconds + let retryError = await sendError(service: service, request: request) + + XCTAssertEqual(unknownOutcomeHash(retryError), firstHash) + XCTAssertEqual(remote.broadcastBocs.count, 1) + } + + func testMessageExpiringDuringEmulationIsNeverBroadcast() async { + let remote = FakeRemote() + remote.reconciliation = .notFound + let clock = LockedClock(Self.now) + remote.emulateStarted = { + clock.value = Self.now + TonSendService.defaultMessageLifetimeSeconds + } + let service = TonSendService( + remote: remote, + pendingCoordinator: TonPendingIntentCoordinator(), + clock: { clock.value } + ) + + let error = await sendError(service: service, request: makeRequest()) + + assertUnknownOutcome(error) + XCTAssertEqual(remote.callOrder, ["walletState", "emulate"]) + XCTAssertTrue(remote.broadcastBocs.isEmpty) + } + + func testDifferentIntentCannotReplaceUnknownPendingMessage() async { + let remote = FakeRemote() + remote.reconciliation = .notFound + let service = makeService(remote: remote) + let firstHash = unknownOutcomeHash( + await sendError(service: service, request: makeRequest()) + ) + XCTAssertEqual(remote.broadcastBocs.count, 1) + + let error = await sendError( + service: service, + request: makeRequest(amountNanotons: "100000001") + ) + + // Even three explicit not-found observations cannot revoke a bearer BOC that was + // already exposed to the remote. A different intent therefore reconciles first, then + // surfaces the exact old hash instead of replacing or rebroadcasting the pending item. + XCTAssertEqual(unknownOutcomeHash(error), firstHash) + XCTAssertEqual(remote.broadcastBocs.count, 1) + XCTAssertEqual(remote.reconciledBocs.count, 3) + } + + func testInvalidClockLifetimeAndTimeoutFailBeforeAnyRemoteCall() async { + let configurations: [(UInt64, UInt64, UInt64)] = [ + (Self.now, TonTransferTransactionBuilder.minimumLifetimeSeconds - 1, 1), + (Self.now, TonTransferTransactionBuilder.maximumLifetimeSeconds + 1, 1), + (UInt64(UInt32.max), 120, 1), + (Self.now, 120, 0) + ] + + for (now, lifetime, timeout) in configurations { + let remote = FakeRemote() + let service = TonSendService( + remote: remote, + messageLifetimeSeconds: lifetime, + remoteTimeoutNanoseconds: timeout, + pendingCoordinator: TonPendingIntentCoordinator(), + clock: { now } + ) + + let error = await sendError(service: service, request: makeRequest()) + + XCTAssertEqual(error as? TonSendServiceError, .invalidClock) + XCTAssertTrue(remote.callOrder.isEmpty) + } + + let zeroFeeCapRemote = FakeRemote() + let zeroFeeCapService = TonSendService( + remote: zeroFeeCapRemote, + maximumFeeNanotons: 0, + pendingCoordinator: TonPendingIntentCoordinator(), + clock: { Self.now } + ) + let zeroFeeCapError = await sendError( + service: zeroFeeCapService, + request: makeRequest() + ) + XCTAssertEqual(zeroFeeCapError as? TonSendServiceError, .invalidClock) + XCTAssertTrue(zeroFeeCapRemote.callOrder.isEmpty) + } + + func testEmulationPolicyRejectsEveryUnsafeOrIncompleteSignal() throws { + let safeExecution = [ + true, false, false, true, true, true, true, true, true, false, + true, true, true, true, true, true, true, true, true + ] + + func acceptsExecution(_ values: [Bool]) -> Bool { + TonEmulationPolicy.acceptsExecution( + transactionSucceeded: values[0], + transactionAborted: values[1], + transactionDestroyed: values[2], + transactionTypeIsOrdinary: values[3], + traceEmulated: values[4], + traceHasWalletV4R2Interface: values[5], + traceChildrenAreEmpty: values[6], + transactionAccountMatches: values[7], + computePhasePresent: values[8], + computePhaseSkipped: values[9], + computePhaseSucceeded: values[10], + computeExitCodeZero: values[11], + actionPhasePresent: values[12], + actionPhaseSucceeded: values[13], + actionCountIsOne: values[14], + skippedActionCountIsZero: values[15], + outMessageCountIsOne: values[16], + inboundMessageMatches: values[17], + outboundMessageMatches: values[18] + ) + } + + XCTAssertTrue(acceptsExecution(safeExecution)) + for unsafeIndex in safeExecution.indices { + var values = safeExecution + values[unsafeIndex].toggle() + XCTAssertFalse( + acceptsExecution(values), + "Unsafe execution signal at index \(unsafeIndex) was accepted" + ) + } + + let safeRisk = [false, false, false, true, false, false] + func acceptsRisk(_ values: [Bool]) -> Bool { + TonEmulationPolicy.acceptsRisk( + eventIsScam: values[0], + eventInProgress: values[1], + transfersAllRemainingBalance: values[2], + tonRiskWithinIntent: values[3], + hasJettonRisk: values[4], + hasNFTRisk: values[5] + ) + } + + XCTAssertTrue(acceptsRisk(safeRisk)) + for unsafeIndex in safeRisk.indices { + var values = safeRisk + values[unsafeIndex].toggle() + XCTAssertFalse( + acceptsRisk(values), + "Unsafe risk signal at index \(unsafeIndex) was accepted" + ) + } + + let maximum = TonSendService.defaultMaximumFeeNanotons + XCTAssertEqual(try TonEmulationPolicy.feeNanotons(1), 1) + XCTAssertEqual(try TonEmulationPolicy.feeNanotons(Int64(maximum)), maximum) + for invalidFee in [ + Int64.min, + -1, + 0, + Int64(maximum + 1), + Int64.max + ] { + XCTAssertThrowsError(try TonEmulationPolicy.feeNanotons(invalidFee)) { error in + XCTAssertEqual(error as? TonTransferRemoteError, .invalidFee) + } + } + } + + func testAccountPolicyAcceptsLiveShapedNilFlagsAndRejectsExplicitDanger() throws { + XCTAssertTrue( + try TonAccountSafetyPolicy.walletIsInitialized( + status: "active", + isWallet: true, + isScam: nil, + isSuspended: nil, + interfaces: ["wallet_v4r2"] + ) + ) + for status in ["nonexist", "uninit"] { + XCTAssertFalse( + try TonAccountSafetyPolicy.walletIsInitialized( + status: status, + isWallet: false, + isScam: nil, + isSuspended: nil, + interfaces: nil + ) + ) + XCTAssertFalse( + try TonAccountSafetyPolicy.recipientRequiresMemo( + status: status, + isScam: nil, + isSuspended: nil, + memoRequired: nil + ) + ) + } + XCTAssertFalse( + try TonAccountSafetyPolicy.recipientRequiresMemo( + status: "active", + isScam: nil, + isSuspended: nil, + memoRequired: nil + ) + ) + XCTAssertTrue( + try TonAccountSafetyPolicy.recipientRequiresMemo( + status: "active", + isScam: nil, + isSuspended: nil, + memoRequired: true + ) + ) + + let unsafeWallets: [(Bool?, Bool?)] = [(true, nil), (nil, true)] + for (isScam, isSuspended) in unsafeWallets { + XCTAssertThrowsError( + try TonAccountSafetyPolicy.walletIsInitialized( + status: "active", + isWallet: true, + isScam: isScam, + isSuspended: isSuspended, + interfaces: ["wallet_v4r2"] + ) + ) + XCTAssertThrowsError( + try TonAccountSafetyPolicy.recipientRequiresMemo( + status: "nonexist", + isScam: isScam, + isSuspended: isSuspended, + memoRequired: nil + ) + ) + } + XCTAssertThrowsError( + try TonAccountSafetyPolicy.walletIsInitialized( + status: "active", + isWallet: false, + isScam: nil, + isSuspended: nil, + interfaces: ["wallet_v4r2"] + ) + ) + XCTAssertThrowsError( + try TonAccountSafetyPolicy.walletIsInitialized( + status: "active", + isWallet: true, + isScam: nil, + isSuspended: nil, + interfaces: [] + ) + ) + XCTAssertThrowsError( + try TonAccountSafetyPolicy.recipientRequiresMemo( + status: "frozen", + isScam: nil, + isSuspended: nil, + memoRequired: nil + ) + ) + } + + func testTonAPIUnsignedEmulationUsesExactTraceEndpointBodyAndSignatureBypassQuery() async throws { + let message = try fixtureUnsignedMessage() + let serverURL = try XCTUnwrap(URL(string: "https://ton-api-fixture.invalid")) + let transport = TonAPIUnsignedEmulationFixtureTransport( + responseData: try makeTraceFixtureData(externalMessageBoc: message.boc), + expectedServerURL: serverURL, + expectedMessageBocBase64: message.bocBase64 + ) + let remote = TonAPIRemoteClient( + client: Client(serverURL: serverURL, transport: transport) + ) + + let result = try await remote.emulateUnsigned( + message: message, + intent: TonEmulationIntent(request: makeEstimateRequest()) + ) + + XCTAssertEqual( + result, + TonEmulationResult(accepted: true, totalFeeNanotons: 123_456) + ) + } + + func testTonAPIBroadcastUsesExactPathAndOneFieldBodyAndRejectsNon2xx() async throws { + let message = try fixtureSignedMessage() + let serverURL = try XCTUnwrap(URL(string: "https://ton-api-fixture.invalid")) + let successRemote = TonAPIRemoteClient( + trustedTestClient: Client( + serverURL: serverURL, + transport: TonAPIBroadcastFixtureTransport( + responseStatus: .ok, + expectedServerURL: serverURL, + expectedMessageBocBase64: message.bocBase64 + ) + ) + ) + try await successRemote.broadcast(message: message) + + let rejectedRemote = TonAPIRemoteClient( + trustedTestClient: Client( + serverURL: serverURL, + transport: TonAPIBroadcastFixtureTransport( + responseStatus: .forbidden, + expectedServerURL: serverURL, + expectedMessageBocBase64: message.bocBase64 + ) + ) + ) + do { + try await rejectedRemote.broadcast(message: message) + XCTFail("Expected a non-2xx broadcast response to fail closed") + } catch { + XCTAssertFalse(error is CancellationError) + } + } + + func testTonAPIReconciliationUsesExactMessageHashPathAndConfirmsExactIntent() async throws { + let result = try await reconcileTonAPIFixture( + responseData: try makeTransactionFixtureData() + ) + + XCTAssertEqual(result, .confirmed) + } + + func testTonAPIReconciliationTreatsOnly404AsNotFound() async throws { + let errorData = try JSONSerialization.data( + withJSONObject: ["error": "fixture failure"], + options: [.sortedKeys] + ) + + let notFoundResult = try await reconcileTonAPIFixture( + responseData: errorData, + responseStatus: .notFound + ) + XCTAssertEqual(notFoundResult, .notFound) + + for status: HTTPResponse.Status in [.badRequest, .forbidden, .internalServerError] { + do { + _ = try await reconcileTonAPIFixture( + responseData: errorData, + responseStatus: status + ) + XCTFail("Expected HTTP \(status.code) to fail closed") + } catch { + XCTAssertEqual( + error as? TonTransferRemoteError, + .unsupportedAccountState + ) + } + } + } + + func testTonAPIReconciliationFailsClosedForMalformedOrMismatchedTransactions() async throws { + let differentValidBody = try Builder() + .store(uint: 1, bits: 1) + .endCell() + .toBoc() + .hexString() + let rawSender = try TonSwift.Address.parse(Self.sender).toRaw() + let rawRecipient = try TonSwift.Address.parse(Self.recipient).toRaw() + typealias Mutation = (inout [String: Any]) -> Void + let mutations: [(String, Mutation)] = [ + ("account", { transaction in + var account = transaction["account"] as! [String: Any] + account["address"] = rawRecipient + transaction["account"] = account + }), + ("type", { $0["transaction_type"] = "TransStorage" }), + ("success", { $0["success"] = false }), + ("compute", { transaction in + var phase = transaction["compute_phase"] as! [String: Any] + phase["exit_code"] = Int32(1) + transaction["compute_phase"] = phase + }), + ("action", { transaction in + var phase = transaction["action_phase"] as! [String: Any] + phase["total_actions"] = Int32(2) + transaction["action_phase"] = phase + }), + ("inbound", { transaction in + var message = transaction["in_msg"] as! [String: Any] + message["msg_type"] = "int_msg" + transaction["in_msg"] = message + }), + ("outbound", { transaction in + var messages = transaction["out_msgs"] as! [[String: Any]] + var destination = messages[0]["destination"] as! [String: Any] + destination["address"] = rawSender + messages[0]["destination"] = destination + transaction["out_msgs"] = messages + }), + ("value", { transaction in + var messages = transaction["out_msgs"] as! [[String: Any]] + messages[0]["value"] = Int64(100_000_001) + transaction["out_msgs"] = messages + }), + ("bounce", { transaction in + var messages = transaction["out_msgs"] as! [[String: Any]] + messages[0]["bounce"] = false + transaction["out_msgs"] = messages + }), + ("inbound body", { transaction in + var message = transaction["in_msg"] as! [String: Any] + message["raw_body"] = differentValidBody + transaction["in_msg"] = message + }), + ("outbound body", { transaction in + var messages = transaction["out_msgs"] as! [[String: Any]] + messages[0]["raw_body"] = differentValidBody + transaction["out_msgs"] = messages + }) + ] + + for (name, mutation) in mutations { + do { + _ = try await reconcileTonAPIFixture( + responseData: try makeTransactionFixtureData(mutation: mutation) + ) + XCTFail("Expected mismatched \(name) to fail closed") + } catch { + XCTAssertEqual( + error as? TonTransferRemoteError, + .unsupportedAccountState, + name + ) + } + } + + do { + _ = try await reconcileTonAPIFixture(responseData: Data("{".utf8)) + XCTFail("Expected malformed transaction JSON to fail closed") + } catch { + XCTAssertFalse(error is CancellationError) + } + } + + func testTonAPIWalletStateMapsLiveNullFieldsAndExactAccountSeqnoPaths() async throws { + let activeRemote = try makeAccountFixtureRemote( + address: Self.sender, + status: "active", + isWallet: true, + interfaces: ["wallet_v4r2"], + sequenceNumber: 9 + ) + let activeState = try await activeRemote.walletState(address: Self.sender) + XCTAssertEqual(activeState, TonWalletRemoteState(sequenceNumber: 9, isInitialized: true)) + + let nonexistRemote = try makeAccountFixtureRemote( + address: Self.recipient, + status: "nonexist", + isWallet: false, + interfaces: nil, + sequenceNumber: 0 + ) + let nonexistState = try await nonexistRemote.walletState(address: Self.recipient) + XCTAssertEqual(nonexistState, TonWalletRemoteState(sequenceNumber: 0, isInitialized: false)) + } + + func testTonAPIWalletAndMemoMappingRejectsExplicitDangerAndPreservesNullContract() async throws { + for (isScam, isSuspended) in [(true, nil), (nil, true)] as [(Bool?, Bool?)] { + let remote = try makeAccountFixtureRemote( + address: Self.sender, + status: "active", + isWallet: true, + interfaces: ["wallet_v4r2"], + isScam: isScam, + isSuspended: isSuspended, + sequenceNumber: 9 + ) + do { + _ = try await remote.walletState(address: Self.sender) + XCTFail("Expected explicit danger flag to reject wallet state") + } catch { + XCTAssertEqual(error as? TonTransferRemoteError, .unsupportedAccountState) + } + } + + for (memoRequired, expected) in [(nil, false), (false, false), (true, true)] as [(Bool?, Bool)] { + let remote = try makeAccountFixtureRemote( + address: Self.recipient, + status: "active", + isWallet: false, + interfaces: nil, + memoRequired: memoRequired, + sequenceNumber: nil + ) + let actual = try await remote.recipientRequiresMemo(address: Self.recipient) + XCTAssertEqual(actual, expected) + } + + let unsafeRecipient = try makeAccountFixtureRemote( + address: Self.recipient, + status: "active", + isWallet: false, + interfaces: nil, + isScam: true, + sequenceNumber: nil + ) + do { + _ = try await unsafeRecipient.recipientRequiresMemo(address: Self.recipient) + XCTFail("Expected scam recipient to fail closed") + } catch { + XCTAssertEqual(error as? TonTransferRemoteError, .unsupportedAccountState) + } + } + + func testTonAPIRemoteClientMapsSuccessfulGeneratedClientJSONFixture() async throws { + let result = try await emulateTonAPIFixture( + fixtureData: try makeEmulationFixtureData(totalFeeNanotons: 123_456_789) + ) + + XCTAssertEqual( + result, + TonEmulationResult(accepted: true, totalFeeNanotons: 123_456_789) + ) + } + + func testTonAPIRemoteClientAcceptsOmittedTraceChildrenAsNoChildTransactions() async throws { + let result = try await emulateTonAPIFixture( + fixtureData: try makeEmulationFixtureData(includeTraceChildren: false) + ) + + XCTAssertEqual( + result, + TonEmulationResult(accepted: true, totalFeeNanotons: 123_456) + ) + } + + func testTonAPIRemoteClientFailsClosedForIncompleteGeneratedClientJSONFixtures() async throws { + let fixtures = [ + try makeEmulationFixtureData(emulated: nil), + try makeEmulationFixtureData(includeActionPhase: false), + try makeEmulationFixtureData(skippedActionCount: 1), + try makeEmulationFixtureData(computePhaseSkipped: true), + try makeEmulationFixtureData(traceHasChild: true) + ] + + for fixture in fixtures { + let result = try await emulateTonAPIFixture(fixtureData: fixture) + + XCTAssertFalse(result.accepted) + XCTAssertEqual(result.totalFeeNanotons, 123_456) + } + } + + func testTonAPIRemoteClientFailsClosedForMalformedAndMismatchedRawBodies() async throws { + let differentValidBody = try Builder() + .store(uint: 1, bits: 1) + .endCell() + .toBoc() + .hexString() + let fixtures = [ + try makeEmulationFixtureData(inboundRawBodyOverride: "b"), + try makeEmulationFixtureData(outboundRawBodyOverride: "gg"), + try makeEmulationFixtureData(inboundRawBodyOverride: "0x00"), + try makeEmulationFixtureData(inboundRawBodyOverride: differentValidBody), + try makeEmulationFixtureData(outboundRawBodyOverride: differentValidBody) + ] + + for fixture in fixtures { + let result = try await emulateTonAPIFixture(fixtureData: fixture) + + XCTAssertFalse(result.accepted) + XCTAssertEqual(result.totalFeeNanotons, 123_456) + } + } + + func testTonAPIRemoteClientRejectsNegativeAndOversizedFeesFromGeneratedClientJSON() async throws { + let invalidFees = [ + Int64(-1), + Int64(TonSendService.defaultMaximumFeeNanotons + 1), + Int64.max + ] + + for invalidFee in invalidFees { + do { + _ = try await emulateTonAPIFixture( + fixtureData: try makeEmulationFixtureData(totalFeeNanotons: invalidFee) + ) + XCTFail("Expected fee \(invalidFee) to be rejected") + } catch { + XCTAssertEqual(error as? TonTransferRemoteError, .invalidFee) + } + } + } + + private func makeService(remote: FakeRemote) -> TonSendService { + TonSendService( + remote: remote, + pendingCoordinator: TonPendingIntentCoordinator(), + clock: { Self.now } + ) + } + + private func reconcileTonAPIFixture( + responseData: Data, + responseStatus: HTTPResponse.Status = .ok + ) async throws -> TonReconciliationResult { + let message = try fixtureSignedMessage() + let serverURL = try XCTUnwrap(URL(string: "https://ton-api-fixture.invalid")) + let transport = TonAPIReconciliationFixtureTransport( + responseData: responseData, + responseStatus: responseStatus, + expectedServerURL: serverURL, + expectedMessageHashHex: message.messageHashHex + ) + let remote = TonAPIRemoteClient( + trustedTestClient: Client(serverURL: serverURL, transport: transport) + ) + return try await remote.reconcile( + message: message, + intent: TonEmulationIntent(request: makeRequest()) + ) + } + + private func makeTraceFixtureData(externalMessageBoc: Data) throws -> Data { + let payload = try XCTUnwrap( + JSONSerialization.jsonObject( + with: try makeEmulationFixtureData(externalMessageBoc: externalMessageBoc) + ) as? [String: Any] + ) + return try JSONSerialization.data( + withJSONObject: try XCTUnwrap(payload["trace"]), + options: [.sortedKeys] + ) + } + + private func makeTransactionFixtureData( + mutation: ((inout [String: Any]) -> Void)? = nil + ) throws -> Data { + let payload = try XCTUnwrap( + JSONSerialization.jsonObject(with: try makeEmulationFixtureData()) as? [String: Any] + ) + let trace = try XCTUnwrap(payload["trace"] as? [String: Any]) + var transaction = try XCTUnwrap(trace["transaction"] as? [String: Any]) + mutation?(&transaction) + return try JSONSerialization.data(withJSONObject: transaction, options: [.sortedKeys]) + } + + private func makeAccountFixtureRemote( + address: String, + status: String, + isWallet: Bool, + interfaces: [String]?, + isScam: Bool? = nil, + isSuspended: Bool? = nil, + memoRequired: Bool? = nil, + sequenceNumber: Int? + ) throws -> TonAPIRemoteClient { + let serverURL = try XCTUnwrap(URL(string: "https://ton-api-fixture.invalid")) + let accountData = try makeAccountFixtureData( + address: address, + status: status, + isWallet: isWallet, + interfaces: interfaces, + isScam: isScam, + isSuspended: isSuspended, + memoRequired: memoRequired + ) + let sequenceData = try sequenceNumber.map { + try JSONSerialization.data( + withJSONObject: ["seqno": $0], + options: [.sortedKeys] + ) + } + let transport = TonAPIAccountFixtureTransport( + accountResponseData: accountData, + sequenceResponseData: sequenceData, + expectedServerURL: serverURL, + expectedAddress: address + ) + return TonAPIRemoteClient( + client: Client(serverURL: serverURL, transport: transport) + ) + } + + private func makeAccountFixtureData( + address: String, + status: String, + isWallet: Bool, + interfaces: [String]?, + isScam: Bool?, + isSuspended: Bool?, + memoRequired: Bool? + ) throws -> Data { + let payload: [String: Any] = [ + "address": address, + "balance": Int64(0), + "last_activity": Int64(0), + "status": status, + "interfaces": interfaces.map { $0 as Any } ?? NSNull(), + "is_scam": isScam.map { $0 as Any } ?? NSNull(), + "memo_required": memoRequired.map { $0 as Any } ?? NSNull(), + "get_methods": [], + "is_suspended": isSuspended.map { $0 as Any } ?? NSNull(), + "is_wallet": isWallet + ] + return try JSONSerialization.data(withJSONObject: payload, options: [.sortedKeys]) + } + + private func emulateTonAPIFixture(fixtureData: Data) async throws -> TonEmulationResult { + let message = try fixtureSignedMessage() + let serverURL = try XCTUnwrap(URL(string: "https://ton-api-fixture.invalid")) + let transport = TonAPIEmulationFixtureTransport( + responseData: fixtureData, + expectedServerURL: serverURL, + expectedMessageBocBase64: message.bocBase64 + ) + let generatedClient = Client(serverURL: serverURL, transport: transport) + let remote = TonAPIRemoteClient(trustedTestClient: generatedClient) + + return try await remote.emulateSigned( + message: message, + intent: TonEmulationIntent(request: makeRequest()) + ) + } + + private func makeEmulationFixtureData( + emulated: Bool? = true, + includeActionPhase: Bool = true, + skippedActionCount: Int32 = 0, + computePhaseSkipped: Bool = false, + totalFeeNanotons: Int64 = 123_456, + includeTraceChildren: Bool = true, + traceHasChild: Bool = false, + inboundRawBodyOverride: String? = nil, + outboundRawBodyOverride: String? = nil, + externalMessageBoc: Data? = nil + ) throws -> Data { + let senderAccount: [String: Any] = [ + "address": try TonSwift.Address.parse(Self.sender).toRaw(), + "is_scam": false, + "is_wallet": true + ] + let recipientAccount: [String: Any] = [ + "address": try TonSwift.Address.parse(Self.recipient).toRaw(), + "is_scam": false, + "is_wallet": false + ] + let signedMessage = try fixtureSignedMessage() + let externalMessage = try Message.loadFrom( + slice: try XCTUnwrap( + Cell.fromBoc(src: externalMessageBoc ?? signedMessage.boc).first + ).beginParse() + ) + let expectedInboundRawBody = try externalMessage.body.toBoc().hexString() + let inboundMessage: [String: Any] = [ + "msg_type": "ext_in_msg", + "created_lt": Int64(1), + "ihr_disabled": false, + "bounce": false, + "bounced": false, + "value": Int64(0), + "fwd_fee": Int64(0), + "ihr_fee": Int64(0), + "destination": senderAccount, + "import_fee": Int64(0), + "created_at": Int64(1), + "raw_body": inboundRawBodyOverride ?? expectedInboundRawBody + ] + let commentBody = try Builder() + .store(int: 0, bits: 32) + .writeSnakeData(Data("Fearless TON".utf8)) + .endCell() + let expectedOutboundRawBody = try commentBody.toBoc().hexString() + let message: [String: Any] = [ + "msg_type": "int_msg", + "created_lt": Int64(2), + "ihr_disabled": true, + "bounce": true, + "bounced": false, + "value": Int64(100_000_000), + "fwd_fee": Int64(1), + "ihr_fee": Int64(0), + "destination": recipientAccount, + "source": senderAccount, + "import_fee": Int64(0), + "created_at": Int64(1), + "raw_body": outboundRawBodyOverride ?? expectedOutboundRawBody + ] + var transaction: [String: Any] = [ + "hash": "fixture-transaction-hash", + "lt": Int64(2), + "account": senderAccount, + "success": true, + "utime": Int64(Self.now), + "orig_status": "active", + "end_status": "active", + "total_fees": totalFeeNanotons, + "transaction_type": "TransOrd", + "state_update_old": "fixture-old-state", + "state_update_new": "fixture-new-state", + "in_msg": inboundMessage, + "out_msgs": [message], + "block": "(-1,8000000000000000,1)", + "compute_phase": [ + "skipped": computePhaseSkipped, + "success": !computePhaseSkipped, + "gas_fees": Int64(1), + "gas_used": Int64(1), + "vm_steps": UInt32(1), + "exit_code": Int32(0) + ], + "aborted": false, + "destroyed": false + ] + if includeActionPhase { + transaction["action_phase"] = [ + "success": true, + "total_actions": Int32(1), + "skipped_actions": skippedActionCount, + "fwd_fees": Int64(1), + "total_fees": Int64(1) + ] + } + + var trace: [String: Any] = [ + "transaction": transaction, + "interfaces": ["wallet_v4r2"] + ] + if let emulated { + trace["emulated"] = emulated + } + if includeTraceChildren { + trace["children"] = traceHasChild ? [trace] : [] + } + + let payload: [String: Any] = [ + "trace": trace, + "risk": [ + "transfer_all_remaining_balance": false, + "ton": Int64(100_000_000), + "jettons": [], + "nfts": [] + ], + "event": [ + "event_id": "fixture-event-id", + "timestamp": Int64(Self.now), + "actions": [ + [ + "type": "TonTransfer", + "status": "ok", + "simple_preview": [ + "name": "TON transfer", + "description": "Fixture native TON transfer", + "accounts": [senderAccount] + ] + ] + ], + "account": senderAccount, + "is_scam": false, + "lt": Int64(2), + "in_progress": false, + "extra": Int64(0) + ] + ] + + return try JSONSerialization.data(withJSONObject: payload, options: [.sortedKeys]) + } + + private func fixtureSignedMessage() throws -> TonSignedExternalMessage { + try TonTransferTransactionBuilder.buildAndSign( + request: TonTransferTransactionRequest( + senderAddress: Self.sender, + recipientAddress: Self.recipient, + amountNanotons: "100000000", + sequenceNumber: 9, + includeStateInit: false, + validUntil: Self.now + 120, + bounce: true, + comment: "Fearless TON" + ), + privateKeySeed: Self.account.privateKey, + now: Self.now + ) + } + + private func fixtureUnsignedMessage() throws -> TonUnsignedEmulationMessage { + try TonTransferTransactionBuilder.buildForFeeEstimation( + request: TonTransferTransactionRequest( + senderAddress: Self.sender, + recipientAddress: Self.recipient, + amountNanotons: "100000000", + sequenceNumber: 9, + includeStateInit: false, + validUntil: Self.now + 120, + bounce: true, + comment: "Fearless TON" + ), + publicKey: Self.account.publicKey, + now: Self.now + ) + } + + private func makeRequest( + asset: TonTransferAsset = .nativeTon, + network: TonTransferNetwork = .mainnet, + mnemonic: String = TonSendServiceTests.mnemonic, + senderAddress: String = TonSendServiceTests.sender, + recipientAddress: String = TonSendServiceTests.recipient, + amountNanotons: String = "100000000", + bounce: Bool = true, + comment: String? = "Fearless TON" + ) -> TonNativeSendRequest { + TonNativeSendRequest( + asset: asset, + network: network, + mnemonic: mnemonic, + senderAddress: senderAddress, + recipientAddress: recipientAddress, + amountNanotons: amountNanotons, + bounce: bounce, + comment: comment + ) + } + + private func makeEstimateRequest( + asset: TonTransferAsset = .nativeTon, + network: TonTransferNetwork = .mainnet, + publicKey: Data = TonSendServiceTests.account.publicKey, + senderAddress: String = TonSendServiceTests.sender, + recipientAddress: String = TonSendServiceTests.recipient, + amountNanotons: String = "100000000", + bounce: Bool = true, + comment: String? = "Fearless TON" + ) -> TonNativeEstimateRequest { + TonNativeEstimateRequest( + asset: asset, + network: network, + publicKey: publicKey, + senderAddress: senderAddress, + recipientAddress: recipientAddress, + amountNanotons: amountNanotons, + bounce: bounce, + comment: comment + ) + } + + private func sendError( + service: TonSendService, + request: TonNativeSendRequest + ) async -> Error? { + do { + _ = try await service.sendUnquotedForTesting(request) + XCTFail("Expected TON send to fail") + return nil + } catch { + return error + } + } + + private func sendError( + quotedService service: TonSendService, + request: TonNativeSendRequest, + quote: TonTransferFeeQuote? + ) async -> Error? { + do { + _ = try await service.send(request, feeQuote: quote) + XCTFail("Expected quoted TON send to fail") + return nil + } catch { + return error + } + } + + private func assertUnknownOutcome( + _ error: Error?, + file: StaticString = #filePath, + line: UInt = #line + ) { + guard let serviceError = error as? TonSendServiceError else { + XCTFail("Expected typed TON unknown outcome, got \(String(describing: error))", file: file, line: line) + return + } + guard case let .broadcastOutcomeUnknown(messageHashHex) = serviceError else { + XCTFail("Expected TON unknown outcome, got \(serviceError)", file: file, line: line) + return + } + XCTAssertEqual(messageHashHex.count, 64, file: file, line: line) + } + + private func unknownOutcomeHash( + _ error: Error?, + file: StaticString = #filePath, + line: UInt = #line + ) -> String? { + guard let serviceError = error as? TonSendServiceError, + case let .broadcastOutcomeUnknown(messageHashHex) = serviceError + else { + XCTFail("Expected typed TON unknown outcome", file: file, line: line) + return nil + } + XCTAssertEqual(messageHashHex.count, 64, file: file, line: line) + return messageHashHex + } + + private final class LockedClock: @unchecked Sendable { + private let lock = NSLock() + private var storedValue: UInt64 + + init(_ value: UInt64) { + storedValue = value + } + + var value: UInt64 { + get { + lock.lock() + defer { lock.unlock() } + return storedValue + } + set { + lock.lock() + storedValue = newValue + lock.unlock() + } + } + } + + private final class FailOnSaveJournal: TonPendingIntentJournaling, @unchecked Sendable { + func load(senderRaw _: String) throws -> TonPendingSignedIntent? { nil } + + func save(_: TonPendingSignedIntent) throws { + throw TonPendingIntentJournalError.unavailable + } + + func delete(senderRaw _: String, expectedMessageHashHex _: String) throws { + throw TonPendingIntentJournalError.unavailable + } + } + + private final class FakeRemote: TonTransferRemoteProtocol, @unchecked Sendable { + var reviewedSignedOperationOrigin: String? = TonAPIClientFactory.canonicalAuthenticatedOrigin.absoluteString + + enum Stage: Equatable { + case recipientInspection + case walletState + case emulate + case broadcast + case reconcile + } + + enum Failure: Error { + case injected + } + + var walletState = TonWalletRemoteState(sequenceNumber: 9, isInitialized: true) + var emulation = TonEmulationResult(accepted: true, totalFeeNanotons: 100) + var memoRequired = false + var reconciliation: TonReconciliationResult? + var failureStage: Stage? + var suspendedStage: Stage? + var nonResumingStage: Stage? + var walletStateStarted: (() -> Void)? + var emulateStarted: (() -> Void)? + private(set) var recipientInspectionAddresses: [String] = [] + private(set) var walletAddresses: [String] = [] + private(set) var emulatedBocs: [String] = [] + private(set) var broadcastBocs: [String] = [] + private(set) var reconciledBocs: [String] = [] + private(set) var callOrder: [String] = [] + private(set) var unsignedEmulationCount = 0 + private(set) var signedEmulationCount = 0 + private var broadcastSucceeded = false + + func walletState(address: String) async throws -> TonWalletRemoteState { + callOrder.append("walletState") + walletAddresses.append(address) + walletStateStarted?() + try await pauseOrFail(stage: .walletState) + return walletState + } + + func recipientRequiresMemo(address: String) async throws -> Bool { + recipientInspectionAddresses.append(address) + try await pauseOrFail(stage: .recipientInspection) + return memoRequired + } + + func emulateUnsigned( + message: TonUnsignedEmulationMessage, + intent _: TonEmulationIntent + ) async throws -> TonEmulationResult { + unsignedEmulationCount += 1 + callOrder.append("emulate") + emulatedBocs.append(message.bocBase64) + emulateStarted?() + try await pauseOrFail(stage: .emulate) + return emulation + } + + func emulateSigned( + message: TonSignedExternalMessage, + intent _: TonEmulationIntent + ) async throws -> TonEmulationResult { + signedEmulationCount += 1 + callOrder.append("emulate") + emulatedBocs.append(message.bocBase64) + emulateStarted?() + try await pauseOrFail(stage: .emulate) + return emulation + } + + func broadcast(message: TonSignedExternalMessage) async throws { + callOrder.append("broadcast") + broadcastBocs.append(message.bocBase64) + try await pauseOrFail(stage: .broadcast) + broadcastSucceeded = true + } + + func reconcile( + message: TonSignedExternalMessage, + intent _: TonEmulationIntent + ) async throws -> TonReconciliationResult { + reconciledBocs.append(message.bocBase64) + try await pauseOrFail(stage: .reconcile) + return reconciliation ?? (broadcastSucceeded ? .confirmed : .notFound) + } + + private func pauseOrFail(stage: Stage) async throws { + if failureStage == stage { + throw Failure.injected + } + if nonResumingStage == stage { + await withUnsafeContinuation { (_: UnsafeContinuation) in } + } + if suspendedStage == stage { + try await Task.sleep(nanoseconds: 60_000_000_000) + } + } + } +} +#else +final class TonSendServiceReleasePolicyTests: XCTestCase { + func testReleaseSendFailsBeforeAnyValidationOrRemoteWork() async { + let remote = RejectingRemote() + let service = TonSendService( + remote: remote, + clock: { 1_700_000_000 } + ) + let request = TonNativeSendRequest( + mnemonic: "intentionally invalid", + senderAddress: "intentionally invalid", + recipientAddress: "intentionally invalid", + amountNanotons: "intentionally invalid", + bounce: false + ) + + let caughtError: Error? + do { + _ = try await service.send(request) + caughtError = nil + } catch { + caughtError = error + } + + XCTAssertEqual(caughtError as? TonSendServiceError, .productionSendDisabled) + let remoteCallCount = await remote.callCount + XCTAssertEqual(remoteCallCount, 0) + } + + private actor RejectingRemote: TonTransferRemoteProtocol { + enum UnexpectedRemoteCall: Error { + case invoked + } + + private(set) var callCount = 0 + + func walletState(address _: String) async throws -> TonWalletRemoteState { + callCount += 1 + throw UnexpectedRemoteCall.invoked + } + + func recipientRequiresMemo(address _: String) async throws -> Bool { + callCount += 1 + throw UnexpectedRemoteCall.invoked + } + + func emulateUnsigned( + message _: TonUnsignedEmulationMessage, + intent _: TonEmulationIntent + ) async throws -> TonEmulationResult { + callCount += 1 + throw UnexpectedRemoteCall.invoked + } + + func emulateSigned( + message _: TonSignedExternalMessage, + intent _: TonEmulationIntent + ) async throws -> TonEmulationResult { + callCount += 1 + throw UnexpectedRemoteCall.invoked + } + + func broadcast(message _: TonSignedExternalMessage) async throws { + callCount += 1 + throw UnexpectedRemoteCall.invoked + } + + func reconcile( + message _: TonSignedExternalMessage, + intent _: TonEmulationIntent + ) async throws -> TonReconciliationResult { + callCount += 1 + throw UnexpectedRemoteCall.invoked + } + } +} +#endif + +private struct TonAPIEmulationFixtureTransport: ClientTransport { + enum FixtureError: Error { + case unexpectedRequest + } + + let responseData: Data + let expectedServerURL: URL + let expectedMessageBocBase64: String + + func send( + _ request: HTTPRequest, + body: HTTPBody?, + baseURL: URL, + operationID: String + ) async throws -> (HTTPResponse, HTTPBody?) { + guard + operationID == "emulateMessageToWallet", + request.method == .post, + request.path == "/v2/wallet/emulate", + baseURL == expectedServerURL, + let body + else { + throw FixtureError.unexpectedRequest + } + + let requestData = try await Data(collecting: body, upTo: 1_024) + let requestJSON = try JSONSerialization.jsonObject(with: requestData) as? [String: Any] + guard requestJSON?["boc"] as? String == expectedMessageBocBase64 else { + throw FixtureError.unexpectedRequest + } + + var headers = HTTPFields() + headers[.contentType] = "application/json" + return ( + HTTPResponse(status: .ok, headerFields: headers), + HTTPBody(responseData) + ) + } +} + +private struct TonAPIUnsignedEmulationFixtureTransport: ClientTransport { + enum FixtureError: Error { + case unexpectedRequest + } + + let responseData: Data + let expectedServerURL: URL + let expectedMessageBocBase64: String + + func send( + _ request: HTTPRequest, + body: HTTPBody?, + baseURL: URL, + operationID: String + ) async throws -> (HTTPResponse, HTTPBody?) { + guard operationID == "emulateMessageToTrace", + request.method == .post, + request.path == "/v2/traces/emulate?ignore_signature_check=true", + baseURL == expectedServerURL, + let body + else { + throw FixtureError.unexpectedRequest + } + + let requestData = try await Data(collecting: body, upTo: 16384) + let requestJSON = try JSONSerialization.jsonObject(with: requestData) as? [String: Any] + guard requestJSON?.count == 1, + requestJSON?["boc"] as? String == expectedMessageBocBase64 + else { + throw FixtureError.unexpectedRequest + } + + var headers = HTTPFields() + headers[.contentType] = "application/json" + return ( + HTTPResponse(status: .ok, headerFields: headers), + HTTPBody(responseData) + ) + } +} + +private struct TonAPIBroadcastFixtureTransport: ClientTransport { + enum FixtureError: Error { + case unexpectedRequest + } + + let responseStatus: HTTPResponse.Status + let expectedServerURL: URL + let expectedMessageBocBase64: String + + func send( + _ request: HTTPRequest, + body: HTTPBody?, + baseURL: URL, + operationID: String + ) async throws -> (HTTPResponse, HTTPBody?) { + guard operationID == "sendBlockchainMessage", + request.method == .post, + request.path == "/v2/blockchain/message", + baseURL == expectedServerURL, + let body + else { + throw FixtureError.unexpectedRequest + } + + let requestData = try await Data(collecting: body, upTo: 16_384) + let requestJSON = try JSONSerialization.jsonObject(with: requestData) as? [String: Any] + guard requestJSON?.count == 1, + requestJSON?["boc"] as? String == expectedMessageBocBase64 + else { + throw FixtureError.unexpectedRequest + } + + var headers = HTTPFields() + headers[.contentType] = "application/json" + let responseData = try JSONSerialization.data( + withJSONObject: ["error": "fixture rejection"], + options: [.sortedKeys] + ) + return ( + HTTPResponse(status: responseStatus, headerFields: headers), + responseStatus == .ok ? nil : HTTPBody(responseData) + ) + } +} + +private struct TonAPIReconciliationFixtureTransport: ClientTransport { + enum FixtureError: Error { + case unexpectedRequest + } + + let responseData: Data + let responseStatus: HTTPResponse.Status + let expectedServerURL: URL + let expectedMessageHashHex: String + + func send( + _ request: HTTPRequest, + body: HTTPBody?, + baseURL: URL, + operationID: String + ) async throws -> (HTTPResponse, HTTPBody?) { + guard operationID == "getBlockchainTransactionByMessageHash", + request.method == .get, + request.path == "/v2/blockchain/messages/\(expectedMessageHashHex)/transaction", + baseURL == expectedServerURL, + body == nil + else { + throw FixtureError.unexpectedRequest + } + + var headers = HTTPFields() + headers[.contentType] = "application/json" + return ( + HTTPResponse(status: responseStatus, headerFields: headers), + HTTPBody(responseData) + ) + } +} + +private struct TonAPIAccountFixtureTransport: ClientTransport { + enum FixtureError: Error { + case unexpectedRequest + } + + let accountResponseData: Data + let sequenceResponseData: Data? + let expectedServerURL: URL + let expectedAddress: String + + func send( + _ request: HTTPRequest, + body: HTTPBody?, + baseURL: URL, + operationID: String + ) async throws -> (HTTPResponse, HTTPBody?) { + guard request.method == .get, + baseURL == expectedServerURL, + body == nil + else { + throw FixtureError.unexpectedRequest + } + + let responseData: Data + switch operationID { + case "getAccount": + guard request.path == "/v2/accounts/\(expectedAddress)" else { + throw FixtureError.unexpectedRequest + } + responseData = accountResponseData + case "getAccountSeqno": + guard request.path == "/v2/wallet/\(expectedAddress)/seqno", + let sequenceResponseData + else { + throw FixtureError.unexpectedRequest + } + responseData = sequenceResponseData + default: + throw FixtureError.unexpectedRequest + } + + var headers = HTTPFields() + headers[.contentType] = "application/json" + return ( + HTTPResponse(status: .ok, headerFields: headers), + HTTPBody(responseData) + ) + } +} + +private extension Data { + init?(hexString: String) { + guard hexString.count.isMultiple(of: 2) else { + return nil + } + + var data = Data(capacity: hexString.count / 2) + var index = hexString.startIndex + while index < hexString.endIndex { + let end = hexString.index(index, offsetBy: 2) + guard let byte = UInt8(hexString[index ..< end], radix: 16) else { + return nil + } + data.append(byte) + index = end + } + self = data + } + + var hexString: String { + map { String(format: "%02x", $0) }.joined() + } +} diff --git a/fearlessTests/UniversalWalletAccountAddressResolverTests.swift b/fearlessTests/UniversalWalletAccountAddressResolverTests.swift index 3e1e7e28c5..ef6fdd790c 100644 --- a/fearlessTests/UniversalWalletAccountAddressResolverTests.swift +++ b/fearlessTests/UniversalWalletAccountAddressResolverTests.swift @@ -111,6 +111,56 @@ final class UniversalWalletAccountAddressResolverTests: XCTestCase { XCTAssertNil(address) } + func testResolvesRealTonMainnetAddressFromMatchingChainAccountPublicKey() throws { + let account = try TonKeyDerivation.deriveAccount(mnemonic: Self.mnemonic) + let wallet = walletWithChainAccount( + chainId: TonChainSelection.mainnetChainId, + publicKey: account.publicKey, + cryptoType: CryptoType.ed25519.rawValue + ) + + let address = UniversalWalletAccountAddressResolver.address( + for: Self.chain(TonChainSelection.mainnetChainId), + wallet: wallet + ) + + XCTAssertEqual(address, account.addressNonBounceable) + } + + func testTonAddressResolutionRejectsMissingMalformedAndTestnetAccounts() throws { + XCTAssertNil( + UniversalWalletAccountAddressResolver.address( + for: Self.chain(TonChainSelection.mainnetChainId), + wallet: AccountGenerator.generateMetaAccount() + ) + ) + + let malformed = walletWithChainAccount( + chainId: TonChainSelection.mainnetChainId, + publicKey: Data(repeating: 1, count: 31), + cryptoType: CryptoType.ed25519.rawValue + ) + XCTAssertNil( + UniversalWalletAccountAddressResolver.address( + for: Self.chain(TonChainSelection.mainnetChainId), + wallet: malformed + ) + ) + + let account = try TonKeyDerivation.deriveAccount(mnemonic: Self.mnemonic) + let testnetOnly = walletWithChainAccount( + chainId: TonChainSelection.testnetChainId, + publicKey: account.publicKey, + cryptoType: CryptoType.ed25519.rawValue + ) + XCTAssertNil( + UniversalWalletAccountAddressResolver.address( + for: Self.chain(TonChainSelection.mainnetChainId), + wallet: testnetOnly + ) + ) + } + func testSolanaAddressResolutionRejectsMalformedPublicKey() { let wallet = walletWithChainAccount( chainId: UniversalWalletRegistry.solanaMainnet.chainId, @@ -230,6 +280,7 @@ final class UniversalWalletAccountAddressResolverTests: XCTestCase { XCTAssertNil(wallet.fetch(for: Self.chain(UniversalWalletRegistry.bitcoinMainnet.chainId).accountRequest())) XCTAssertNil(wallet.fetch(for: Self.chain(UniversalWalletRegistry.solanaMainnet.chainId).accountRequest())) XCTAssertNil(wallet.fetch(for: Self.chain(UniversalWalletRegistry.taira.chainId).accountRequest())) + XCTAssertNil(wallet.fetch(for: Self.chain(TonChainSelection.mainnetChainId).accountRequest())) } func testFetchMatchesRegistryIdToCanonicalUniversalWalletChainAccount() throws { diff --git a/scripts/audit-codecov-coverage-contract.sh b/scripts/audit-codecov-coverage-contract.sh index 487b226ccc..8cd0a48bef 100755 --- a/scripts/audit-codecov-coverage-contract.sh +++ b/scripts/audit-codecov-coverage-contract.sh @@ -27,7 +27,7 @@ require_text 'test -d "$RESULT_BUNDLE"' require_text "plugins: xcode" require_text "swift_project: fearless" require_text "fail_ci_if_error: true" -require_text "use_oidc: true" +require_text "use_oidc: \${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}" if grep -Eq 'codecov/codecov-action@(main|master|v[0-9]+)[[:space:]#]' "$WORKFLOW"; then fail "Codecov action must use an immutable full commit SHA." diff --git a/scripts/audit-codecov-workflow.sh b/scripts/audit-codecov-workflow.sh new file mode 100755 index 0000000000..345b780a85 --- /dev/null +++ b/scripts/audit-codecov-workflow.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +WORKFLOW_FILE="${IOS_CODECOV_AUDIT_WORKFLOW_FILE:-$ROOT_DIR/.github/workflows/codecov.yml}" + +fail() { + echo "[codecov-workflow-audit][error] $*" >&2 + exit 1 +} + +require_active_text() { + local value="$1" + local message="$2" + grep -F -- "$value" "$WORKFLOW_FILE" | + grep -Eq '^[[:space:]]*[^#[:space:]]' || fail "$message" +} + +[[ -f "$WORKFLOW_FILE" ]] || fail "Missing Codecov workflow: $WORKFLOW_FILE" + +if grep -Eq 'codecov\.io/bash|bash[[:space:]]+<\([[:space:]]*curl|curl[^|]*\|[[:space:]]*(ba)?sh' "$WORKFLOW_FILE"; then + fail "Codecov workflow may not download and execute an unverified shell script." +fi + +require_active_text \ + 'codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de' \ + 'Codecov action must be pinned to the reviewed v5.5.2 commit.' +require_active_text \ + 'actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5' \ + 'Codecov workflow checkout action must be pinned.' +require_active_text \ + 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' \ + 'Codecov workflow artifact upload action must be pinned.' +require_active_text 'contents: read' 'Codecov workflow must use read-only repository contents permission.' +require_active_text 'fail_ci_if_error: true' 'Codecov upload failures must fail CI.' +require_active_text 'swift_project: fearless' 'Codecov must upload the Fearless Swift project coverage.' +require_active_text 'id-token: write' 'Trusted Codecov uploads require OIDC permission.' +require_active_text \ + "use_oidc: \${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}" \ + 'Codecov must use OIDC for trusted events and preserve tokenless fork PR uploads.' +require_active_text \ + 'bash ./scripts/test-codecov-workflow-audit.sh' \ + 'Codecov CI must run the uploader audit adversarial self-test.' +require_active_text \ + 'bash ./scripts/audit-codecov-workflow.sh' \ + 'Codecov CI must audit the real workflow.' + +if grep -Eq '^[[:space:]]*(contents|actions|pull-requests|checks):[[:space:]]+write([[:space:]]|$)' "$WORKFLOW_FILE"; then + fail "Codecov workflow grants an unnecessary repository write permission." +fi + +unpinned="$(grep -E '^[[:space:]]*uses:[[:space:]]+' "$WORKFLOW_FILE" | grep -Ev '@[0-9a-f]{40}([[:space:]]|$)' || true)" +[[ -z "$unpinned" ]] || fail "Workflow action reference is not a full commit SHA: $unpinned" + +echo "[codecov-workflow-audit] passed" diff --git a/scripts/audit-iroha-production-send-readiness.sh b/scripts/audit-iroha-production-send-readiness.sh new file mode 100755 index 0000000000..e29a444fa5 --- /dev/null +++ b/scripts/audit-iroha-production-send-readiness.sh @@ -0,0 +1,496 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="${IROHA_SEND_AUDIT_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || (cd "$(dirname "$0")/.." && pwd))}" +MANIFEST="$ROOT_DIR/config/iroha-production-send-readiness.json" +DOC="$ROOT_DIR/docs/iroha-production-send-readiness.md" +UNIVERSAL_DOC="$ROOT_DIR/docs/universal-wallet-v2.md" +RELEASE_CHECKLIST="$ROOT_DIR/docs/release-checklist.md" +TRANSFER_SERVICE="$ROOT_DIR/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift" +SEND_CONTAINER="$ROOT_DIR/fearless/Modules/Send/SendDependencyContainer.swift" +TRANSFER_TEST="$ROOT_DIR/fearlessTests/ApplicationLayer/Services/FeatureToggle/TonChainSelectionTests.swift" +REGISTRY="$ROOT_DIR/fearless/Common/Model/UniversalWalletRegistry.swift" +PROJECT="$ROOT_DIR/fearless.xcodeproj/project.pbxproj" +PODFILE="$ROOT_DIR/Podfile" +EXPECTED_MANIFEST_SHA256="3e35a7667af81feff4d6a3dbb88babb6d56e8697672bd7d244215ae4e5855920" + +fail() { + echo "[iroha-send-readiness][ios][error] $*" >&2 + exit 1 +} + +sha256_file() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{print $1}' + else + shasum -a 256 "$1" | awk '{print $1}' + fi +} + +require_file() { + local path="$1" + [[ -f "$path" && ! -L "$path" ]] || fail "required regular file is missing or is a symlink: $path" +} + +require_fixed() { + local path="$1" + local marker="$2" + local label="$3" + grep -Fq -- "$marker" "$path" || fail "$label is missing from ${path#"$ROOT_DIR/"}" +} + +for path in \ + "$MANIFEST" "$DOC" "$UNIVERSAL_DOC" "$RELEASE_CHECKLIST" \ + "$TRANSFER_SERVICE" "$SEND_CONTAINER" "$TRANSFER_TEST" "$REGISTRY" \ + "$PROJECT" "$PODFILE"; do + require_file "$path" +done + +[[ "$(wc -c < "$MANIFEST" | tr -d '[:space:]')" -le 65536 ]] || + fail "readiness manifest exceeds 64 KiB" +actual_manifest_sha256="$(sha256_file "$MANIFEST")" +[[ "$actual_manifest_sha256" == "$EXPECTED_MANIFEST_SHA256" ]] || + fail "readiness manifest digest mismatch: expected $EXPECTED_MANIFEST_SHA256, got $actual_manifest_sha256" + +node - "$MANIFEST" <<'NODE' +const fs = require('node:fs'); +let manifest; +try { + manifest = JSON.parse(fs.readFileSync(process.argv[2], 'utf8')); +} catch (error) { + console.error(`[iroha-send-readiness][ios][error] invalid readiness JSON: ${error.message}`); + process.exit(1); +} +function assert(condition, message) { + if (!condition) { + console.error(`[iroha-send-readiness][ios][error] ${message}`); + process.exit(1); + } +} +function exactKeys(value, expected, label) { + assert(value && typeof value === 'object' && !Array.isArray(value), `${label} must be an object`); + assert( + JSON.stringify(Object.keys(value).sort()) === JSON.stringify([...expected].sort()), + `${label} keys drifted` + ); +} + +exactKeys(manifest, [ + 'schemaVersion', 'platform', 'assessedAt', 'status', 'releaseEnabled', + 'nexusEnabledByDefault', 'upstream', 'artifact', 'integrationAssessment', + 'transactionParity', 'networkReadiness', 'securityBoundary', 'liveEvidence', + 'blocker', 'exitCriteria' +], 'top-level manifest'); +assert(manifest.schemaVersion === 2, 'schemaVersion must be 2'); +assert(manifest.platform === 'ios', 'platform must be ios'); +assert(manifest.assessedAt === '2026-07-11', 'assessment date drifted'); +assert(manifest.status === 'blocked', 'status must remain blocked'); +assert(manifest.releaseEnabled === false, 'releaseEnabled must remain false'); +assert(manifest.nexusEnabledByDefault === false, 'Nexus must remain disabled by default'); + +assert(manifest.upstream?.repository === 'hyperledger/iroha', 'unexpected upstream repository'); +assert(manifest.upstream?.tag === 'v2.0.0-rc.2.1-fearless-mobile-sdk.3', 'unexpected upstream tag'); +assert(manifest.upstream?.commit === '4f8cfbdd17aa6a3b049e619f23ec02501e5297b6', 'unexpected upstream commit'); + +const artifact = manifest.artifact; +assert(artifact?.name === 'NoritoBridge-v2.0.0-rc.2.1-fearless-mobile-sdk.3.xcframework.zip', 'unexpected Apple artifact'); +assert(artifact?.sha256 === 'dc944af3dc98d37d349b9f95fe25b9e4a920f095db58adbcccc6354fc28ada4b', 'unexpected Apple artifact digest'); +assert(artifact?.bytes === 392660196, 'unexpected Apple artifact size'); +assert(artifact?.reviewThresholdBytes === 350000000, 'unexpected Apple review threshold'); +assert(artifact.bytes > artifact.reviewThresholdBytes, 'artifact must remain above the recorded review threshold'); +assert(artifact.entryCount === 20, 'archive entry count drifted'); +assert(artifact.expandedBytes === 1396110349, 'archive expanded size drifted'); +assert(artifact.maxEntryBytes === 704780504, 'archive maximum entry size drifted'); +assert(artifact.compressionRatio === '3.556', 'archive expansion ratio drifted'); +assert(artifact.archiveSafety === 'passed-path-type-encryption-crc-and-bomb-bounds', 'archive safety scope drifted'); + +const publishedSlices = { + 'ios-arm64': '508b36e1ddc08d3c7488dfbb932d0e37f883908747296b6edab8465efbc3b77c', + 'ios-arm64_x86_64-simulator': 'b4ec44590205d173259f97a424702ace5d1fa70b469df578fc54a68fd2263b56', + 'macos-arm64': '3f13ce287c168b7103b368a67fbf22d1e1d8de0b1b345c8187fae1c206faa60a' +}; +const taggedSlices = { + 'ios-arm64': '26bb800e9dce021ef38306caef70dbba7928dd99c6612801fb1bbc520b52b7a9', + 'ios-arm64_x86_64-simulator': 'd0f651e6dc837bff7e92b05c9bf1e3a2988fc6995cabee6e3aaa269a01ecd1b5', + 'macos-arm64': 'd1dc2069532ff760e03ebf66fdd17811d8d7fa520dcd9f9048b61bbcbcffc3e2' +}; +assert(JSON.stringify(artifact.publishedSliceSha256) === JSON.stringify(publishedSlices), 'published slice hashes drifted'); + +const integration = manifest.integrationAssessment; +assert(integration?.appMinimumIOS === '14.1', 'app minimum iOS evidence drifted'); +assert(integration?.sdkMinimumIOS === '15.0', 'SDK minimum iOS evidence drifted'); +assert(integration?.minimumOSCompatible === false, 'minimum OS mismatch must remain explicit'); +assert(integration?.productMinimumOSChangeApproved === false, 'product minimum OS change must remain unapproved'); +assert(integration?.sdkLinkageApproved === false, 'SDK linkage must remain unapproved'); +assert(integration?.taggedPackageDelivery === 'path-binary-target-with-symlink-placeholder', 'tagged package delivery evidence drifted'); +assert(integration?.taggedPackageResolution === 'fails-without-separate-dist-materialization', 'tagged package resolution blocker drifted'); +assert(integration?.taggedCompileToolchain === 'Xcode 26.5 / Swift 6.3.2', 'tagged compile toolchain drifted'); +assert(integration?.taggedCompileStatus === 'failed', 'tagged compile failure must remain explicit'); +assert(integration?.taggedCompileFailure === 'public-default-arguments-call-private-currentEpochSeconds', 'tagged compile failure reason drifted'); +assert(JSON.stringify(integration?.taggedSwiftLoaderExpectedSliceSha256) === JSON.stringify(taggedSlices), 'tagged Swift loader hashes drifted'); +assert(integration?.publishedSlicesMatchTaggedLoader === false, 'source/artifact slice mismatch must remain explicit'); +for (const key of Object.keys(publishedSlices)) { + assert(publishedSlices[key] !== taggedSlices[key], `published slice unexpectedly matches tagged loader for ${key}`); +} +assert(integration?.staticLinkDigestVerification === 'not-enforced', 'static-link digest gap must remain explicit'); +assert(integration?.binaryScope === 'broad-general-purpose-native-bridge', 'binary review scope drifted'); +assert(integration?.licenseOrNoticeBundled === false, 'missing bundled license/notice evidence drifted'); +assert(integration?.sbomBundled === false, 'missing SBOM evidence drifted'); +assert(integration?.buildProvenanceBundled === false, 'missing build provenance evidence drifted'); +assert(integration?.artifactAttestationBundled === false, 'missing artifact attestation evidence drifted'); +assert(integration?.cryptographicTagSignaturePresent === false, 'unsigned tag evidence drifted'); +assert(integration?.reproducibleSourceToBinaryIdentity === 'not-proven', 'source-to-binary identity gap drifted'); + +const parity = manifest.transactionParity; +assert(parity?.signedFixtureBytes === 576, 'Swift parity fixture size drifted'); +assert(parity?.currentMainCompactEntrypointHash === '9756355bec7ca04a9e95025a0f24296a02118a5bf5989cc9c2cec0612bf76201', 'compact entrypoint hash drifted'); +assert(parity?.fixedU64EntrypointHash === 'c072426bffe62e12fc6b94a0c37ecf4eb2a805965964ce999e5183bddc9a1ce7', 'fixed-u64 diagnostic hash drifted'); +assert(parity?.taggedSwiftRawFixtureHash === '6cc8aa66faa4b067c44831deaf225d8637ffd6daba05b11857b69a06a6b4279b', 'tagged Swift parity hash drifted'); +assert(new Set([ + parity.currentMainCompactEntrypointHash, + parity.fixedU64EntrypointHash, + parity.taggedSwiftRawFixtureHash +]).size === 3, 'distinct transaction hash domains unexpectedly collapsed'); +assert(parity?.officialTagParityStatus === 'stale-noncanonical', 'official tag parity blocker drifted'); +assert(parity?.localCompactCorrectionStatus === 'unpublished-unreviewed-diagnostic-only', 'local correction must not be release evidence'); +assert(parity?.liveReceiptParity === 'not-recorded', 'live receipt parity must remain blocked'); + +const network = manifest.networkReadiness; +assert(network?.routeLabel === 'iroha3-taira', 'Taira route label drifted'); +assert(network?.routeLabelCurrentlyPassedAsSigningChainId === true, 'route-label/signing-chain ambiguity must remain explicit'); +assert(network?.protocolChainIdMapping === 'not-authoritatively-confirmed', 'protocol chain ID mapping must remain blocked'); +assert(network?.fixtureAssetDefinitionId === '61CtjvNd9T3THAR65GsMVHr82Bjc', 'fixture asset definition drifted'); +assert(network?.liveTairaNativeXorDefinitionId === '6TEAJqbb8oEPmLncoNiMRbLEK6tw', 'live Taira XOR definition drifted'); +assert(network.fixtureAssetDefinitionId !== network.liveTairaNativeXorDefinitionId, 'fixture/live asset drift must remain explicit'); +assert(network?.liveTairaNativeXorScale === 9, 'live Taira XOR scale drifted'); +assert(network?.liveTairaNativeXorAliasPresent === false, 'live Taira alias absence drifted'); +assert(network?.canonicalAssetMapping === 'drifted-authoritative-live-registry-required', 'canonical asset mapping blocker drifted'); +assert(network?.authoritativeFeePolicy === 'absent', 'authoritative fee policy must remain blocked'); +assert(network?.currentFeeEstimate === 'hardcoded-zero', 'current zero-fee behavior must remain explicit'); +assert(network?.liveTairaNodeVersion === '2.0.0-rc.2.0', 'live Taira node version drifted'); +assert(network?.liveTairaNodeCommit === '039af2d65e10b773be5031ab8fc07cf27b40e30d', 'live Taira node commit drifted'); +assert(network?.sdkToDeployedNodeCompatibility === 'not-proven', 'SDK/deployed-node compatibility must remain blocked'); + +const security = manifest.securityBoundary; +assert(security?.defaultSigner === 'UnavailableIrohaTransferSigner', 'unexpected security-boundary signer'); +assert(security?.secretRequestRepresentation === 'immutable-swift-string', 'secret representation risk drifted'); +assert(security?.secretCopiesReliablyZeroizable === false, 'secret zeroization gap must remain explicit'); +assert(security?.secretLifecycleReview === 'required-before-sdk-adapter', 'secret lifecycle review gate drifted'); +assert(security?.submissionHashPolicy === 'local-hash-preferred-without-receipt-equality-check', 'submission hash-policy gap drifted'); +assert(security?.receiptHashEqualityRequired === true, 'receipt hash equality must be required'); +assert(security?.acceptedAndFinalizedStatusEvidenceRequired === true, 'accepted/finalized evidence must be required'); + +for (const [key, value] of Object.entries(manifest.liveEvidence ?? {})) { + assert(value === 'missing' || (key === 'nexusProductionEndpoint' && value === 'unconfirmed'), `live evidence ${key} must remain unavailable`); +} +assert(Object.keys(manifest.liveEvidence ?? {}).length === 6, 'live evidence key set drifted'); +assert(manifest.blocker?.code === 'apple_xcframework_review_and_materialization_required', 'unexpected blocker code'); +assert(manifest.blocker?.defaultSigner === 'UnavailableIrohaTransferSigner', 'unexpected blocker signer'); +assert(Array.isArray(manifest.exitCriteria) && manifest.exitCriteria.length === 11, 'exactly eleven exit criteria are required'); +assert(new Set(manifest.exitCriteria).size === manifest.exitCriteria.length, 'exit criteria must be unique'); +NODE + +require_fixed "$TRANSFER_SERVICE" \ + 'signer: IrohaTransferSigning = UnavailableIrohaTransferSigner()' \ + "fail-closed service default" +require_fixed "$TRANSFER_SERVICE" \ + 'struct UnavailableIrohaTransferSigner: IrohaTransferSigning' \ + "unavailable signer implementation" +require_fixed "$TRANSFER_SERVICE" \ + 'throw TransferServiceError.transferFailed(reason: "Iroha transfer signing codec is unavailable")' \ + "unavailable signer exception" +require_fixed "$TRANSFER_SERVICE" \ + 'let mnemonicOrSeed: String' \ + "immutable Swift secret risk marker" +require_fixed "$TRANSFER_SERVICE" \ + 'struct IrohaWalletSmokeTransactionMetadata: Equatable' \ + "wallet-smoke immutable metadata snapshot" +require_fixed "$TRANSFER_SERVICE" \ + 'let metadata: IrohaTransactionMetadata' \ + "Iroha signing request metadata seam" +require_fixed "$TRANSFER_SERVICE" \ + 'func submitNexusWalletSmokeEvidence(' \ + "operator-only Nexus wallet-smoke evidence hook" +for marker in \ + 'static let evidenceRoleKey = "evidence_role"' \ + 'static let routeGovernanceActionHashKey = "route_governance_action_hash"' \ + 'static let walletPlatformKey = "wallet_platform"' \ + 'static let walletCommitKey = "wallet_commit"' \ + 'untrustedMetadata.count == expectedKeys.count' \ + 'Set(untrustedMetadata.keys) == expectedKeys' \ + 'untrustedMetadata[evidenceRoleKey] == "wallet-smoke"' \ + 'untrustedMetadata[walletPlatformKey] == "ios"' \ + 'routeHash != routeHashPrefix + String(repeating: "0", count: 64)' \ + 'walletCommit != String(repeating: "0", count: 40)' \ + 'private init(snapshot: [String: String])' \ + 'chain.chainId == UniversalWalletRegistry.nexus.chainId' \ + 'context.signingRequest.network == "nexus"' \ + 'context.toriiBaseURL == UniversalWalletRegistry.nexus.toriiBaseURL?.absoluteString'; do + require_fixed "$TRANSFER_SERVICE" "$marker" "wallet-smoke metadata invariant '$marker'" +done +require_fixed "$TRANSFER_SERVICE" \ + 'chainId: network.chainId' \ + "route-label signing-chain ambiguity marker" +require_fixed "$TRANSFER_SERVICE" \ + 'return .zero' \ + "unresolved zero-fee marker" +require_fixed "$TRANSFER_SERVICE" \ + 'return signedTransfer.transactionHashHex' \ + "local-hash-preferred submission marker" +require_fixed "$TRANSFER_SERVICE" \ + '?? receipt.payload.signedTransactionHash' \ + "receipt signed-transaction hash fallback" +require_fixed "$TRANSFER_SERVICE" \ + '?? receipt.payload.txHash' \ + "receipt transaction hash fallback" +require_fixed "$SEND_CONTAINER" \ + 'return IrohaTransferService(wallet: wallet, chain: chainAsset.chain)' \ + "send container fail-closed construction" +require_fixed "$TRANSFER_TEST" \ + 'func testIrohaTransferServiceDefaultSignerFailsClosedAfterValidation()' \ + "default signer fail-closed test" +require_fixed "$TRANSFER_TEST" \ + 'func testIrohaTransferServiceRejectsMnemonicMismatchBeforeSignerOrToriiCalls()' \ + "mnemonic/key mismatch adversarial test" +for marker in \ + 'func testIrohaNexusWalletSmokeEvidenceThreadsExactImmutableMetadataToSigner()' \ + 'func testIrohaWalletSmokeMetadataSnapshotDoesNotAliasInputOrReturnedValues()' \ + 'func testIrohaNexusWalletSmokeEvidenceRejectsMalformedMetadataBeforeSignerOrTorii()' \ + 'func testIrohaWalletSmokeEvidenceRejectsTairaAndNoncanonicalNexusBeforeSignerOrTorii()' \ + 'func testIrohaWalletSmokeEvidenceRemainsFailClosedWithUnavailableSigner()'; do + require_fixed "$TRANSFER_TEST" "$marker" "wallet-smoke adversarial test '$marker'" +done +for marker in \ + 'private func makeIrohaTestHistoryEndpoint(_ baseURL: String) -> ChainModel.BlockExplorer' \ + 'let endpoint = ChainModel.BlockExplorer(type: "sora", url: url)' \ + 'preconditionFailure("Iroha test history endpoint failed to materialize")' \ + 'chain.externalApi?.history?.url.absoluteString' \ + 'XCTAssertEqual(configurations.count, 12)' \ + 'UniversalWalletRegistry.nexus.chainId.uppercased()' \ + '"Nexus registry-id alias"' \ + 'https://nexus-proxy.example' \ + 'http://minamoto.sora.org' \ + 'https://minamoto.sora.org.attacker.invalid' \ + 'https://minamoto.sora.org@attacker.invalid' \ + 'https://minamoto.sora.org:444' \ + 'https://minamoto.sora.org/v1/mcp' \ + 'https://minamoto.sora.org?redirect=https://attacker.invalid' \ + 'https://minamoto.sora.org#@attacker.invalid' \ + '"https://minamoto.sora.org/",'; do + require_fixed "$TRANSFER_TEST" "$marker" "materialized Nexus endpoint adversarial fixture '$marker'" +done + +node - "$TRANSFER_SERVICE" "$SEND_CONTAINER" <<'NODE' +const fs = require('node:fs'); +const transfer = fs.readFileSync(process.argv[2], 'utf8'); +const container = fs.readFileSync(process.argv[3], 'utf8'); +function fail(message) { + console.error(`[iroha-send-readiness][ios][error] ${message}`); + process.exit(1); +} +const routeChecks = container.match(/if isUniversalWalletIroha\(chainAsset[.]chain\) \{/g) ?? []; +const serviceCalls = container.match(/\bIrohaTransferService\s*\(/g) ?? []; +const expectedRoute = `if isUniversalWalletIroha(chainAsset.chain) { + return IrohaTransferService(wallet: wallet, chain: chainAsset.chain) + }`; +if (routeChecks.length !== 1 || serviceCalls.length !== 1 || !container.includes(expectedRoute)) { + fail('send routing must contain exactly one audited fail-closed Iroha service construction'); +} + +const requestStart = transfer.indexOf('struct IrohaTransferSigningRequest: Equatable {'); +const requestEnd = transfer.indexOf('\n}\n\nstruct IrohaSignedTransfer:', requestStart); +if (requestStart < 0 || requestEnd < 0) fail('Iroha signing request boundary is missing'); +const request = transfer.slice(requestStart, requestEnd); +if ((request.match(/let mnemonicOrSeed: String/g) ?? []).length !== 1) { + fail('signing request must retain exactly one audited immutable Swift secret field while blocked'); +} +if ((request.match(/let metadata: IrohaTransactionMetadata/g) ?? []).length !== 1) { + fail('signing request must retain exactly one audited transaction metadata field'); +} + +const submitStart = transfer.indexOf(' func submit(transfer: Transfer) async throws -> String {', requestEnd); +const evidenceStart = transfer.indexOf('\n func submitNexusWalletSmokeEvidence(', submitStart); +const validatedStart = transfer.indexOf('\n private func submitValidated(', evidenceStart); +const submitEnd = transfer.indexOf('\n func subscribeForFee(', validatedStart); +if (submitStart < 0 || evidenceStart < 0 || validatedStart < 0 || submitEnd < 0) { + fail('Iroha ordinary/evidence submission boundary is missing'); +} +const ordinarySubmit = transfer.slice(submitStart, evidenceStart); +const evidenceSubmit = transfer.slice(evidenceStart, validatedStart); +const submit = transfer.slice(validatedStart, submitEnd); +if (!ordinarySubmit.includes('submitValidated(transfer: transfer, metadata: .none)')) { + fail('ordinary Iroha transfers must omit transaction metadata'); +} +if (!evidenceSubmit.includes('IrohaWalletSmokeTransactionMetadata.validatedSnapshot(') || + !evidenceSubmit.includes('chain.chainId == UniversalWalletRegistry.nexus.chainId') || + !evidenceSubmit.includes('let evidenceToriiBaseURL = try Self.toriiBaseURL(') || + !evidenceSubmit.includes('evidenceToriiBaseURL == UniversalWalletRegistry.nexus.toriiBaseURL?.absoluteString') || + !evidenceSubmit.includes('metadata: .walletSmoke(metadata)')) { + fail('operator evidence must cross the validated wallet-smoke metadata boundary'); +} +const expectedFallback = `return signedTransfer.transactionHashHex + ?? receipt.payload.signedTransactionHash + ?? receipt.payload.txHash`; +if ((submit.match(/return signedTransfer[.]transactionHashHex/g) ?? []).length !== 1 || !submit.includes(expectedFallback)) { + fail('unreviewed submission hash fallback changed without readiness review'); +} +if (submit.includes('transactionStatus(') || submit.includes('finalized')) { + fail('submission finality behavior changed without readiness review'); +} +NODE + +signing_files="$( + find "$ROOT_DIR/fearless" -type f -name '*.swift' \ + -exec grep -Il 'IrohaTransferSigning' {} + | sort +)" +[[ "$signing_files" == "$TRANSFER_SERVICE" ]] || { + printf '%s\n' "$signing_files" >&2 + fail "an Iroha signer implementation appeared outside the audited transfer service" +} + +service_call_files="$( + find "$ROOT_DIR/fearless" -type f -name '*.swift' \ + -exec grep -Il 'IrohaTransferService(' {} + | sort +)" +[[ "$service_call_files" == "$SEND_CONTAINER" ]] || { + printf '%s\n' "$service_call_files" >&2 + fail "IrohaTransferService construction appeared outside the audited send container" +} + +sdk_import_files="$( + find "$ROOT_DIR/fearless" -type f \( -name '*.swift' -o -name '*.m' -o -name '*.mm' \) \ + -exec grep -EIl '^[[:space:]]*(import[[:space:]]+(IrohaSwift|NoritoBridge)|#import.*NoritoBridge)' {} + | sort || true +)" +[[ -z "$sdk_import_files" ]] || { + printf '%s\n' "$sdk_import_files" >&2 + fail "IrohaSwift/NoritoBridge was imported into product source before review" +} + +node - "$REGISTRY" "$PROJECT" <<'NODE' +const fs = require('node:fs'); +const registry = fs.readFileSync(process.argv[2], 'utf8'); +const project = fs.readFileSync(process.argv[3], 'utf8'); +function fail(message) { + console.error(`[iroha-send-readiness][ios][error] ${message}`); + process.exit(1); +} +function networkBlock(name) { + const start = registry.indexOf(`static let ${name} = IrohaNetwork(`); + const end = registry.indexOf('\n )', start); + if (start < 0 || end < 0) fail(`${name} registry block is missing`); + return registry.slice(start, end); +} +const taira = networkBlock('taira'); +const nexus = networkBlock('nexus'); +if (!taira.includes('chainId: "iroha3-taira"')) fail('Taira route label drifted without readiness review'); +if (!nexus.includes('enabledByDefault: false')) fail('Nexus registry default is not provably disabled'); + +const appMarkers = [...project.matchAll(/INFOPLIST_FILE = fearless\/Info[.]plist;/g)]; +const appBlocks = appMarkers.map((marker) => { + const start = project.lastIndexOf('buildSettings = {', marker.index); + const end = project.indexOf('\n\t\t\t};', marker.index); + if (start < 0 || end < 0) fail('Fearless app build-settings boundary is malformed'); + return project.slice(start, end); +}); +if (appBlocks.length !== 3 || appBlocks.some((block) => !block.includes('IPHONEOS_DEPLOYMENT_TARGET = 14.1;'))) { + fail('Fearless app deployment target must remain iOS 14.1 while the SDK is blocked'); +} +NODE + +require_fixed "$PODFILE" \ + "config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '14.1'" \ + "CocoaPods iOS 14.1 deployment target" + +if grep -Eq 'repositoryURL = ".*(hyperledger/iroha|IrohaSwift)|productName = (IrohaSwift|NoritoBridge)' "$PROJECT"; then + fail "IrohaSwift/NoritoBridge was added to the Xcode dependency graph before review" +fi +if grep -Eq '(^|[[:space:]"/])(IrohaSwift|NoritoBridge)([[:space:]"/,]|$)|hyperledger/iroha' "$PODFILE"; then + fail "IrohaSwift/NoritoBridge was added to CocoaPods before review" +fi + +dependency_manifest_hit="$({ + find "$ROOT_DIR" -path "$ROOT_DIR/Pods" -prune -o -path "$ROOT_DIR/build" -prune -o \ + -type f \( -name 'Package.swift' -o -name 'Cartfile' -o -name '*.podspec' \) -print0 +} | xargs -0 grep -EIl '(hyperledger/iroha|IrohaSwift|NoritoBridge)' 2>/dev/null | sort || true)" +[[ -z "$dependency_manifest_hit" ]] || { + printf '%s\n' "$dependency_manifest_hit" >&2 + fail "an Iroha SDK dependency manifest appeared before review" +} + +local_binary="$( + find "$ROOT_DIR" \ + -path "$ROOT_DIR/.git" -prune -o \ + -path "$ROOT_DIR/Pods" -prune -o \ + -path "$ROOT_DIR/build" -prune -o \ + -path "$ROOT_DIR/DerivedData" -prune -o \ + -path "$ROOT_DIR/SourcePackages" -prune -o \ + \( -type f -o -type d \) \ + \( -name 'NoritoBridge*.zip' -o -name 'NoritoBridge*.a' -o \ + -name 'NoritoBridge*.framework' -o -name 'NoritoBridge*.xcframework' -o \ + -name 'libconnect_norito_bridge.a' -o -name 'libconnect_norito_bridge.dylib' \) \ + -print | sort +)" +[[ -z "$local_binary" ]] || { + printf '%s\n' "$local_binary" >&2 + fail "Iroha SDK/native binary material exists in the release tree before review" +} + +tracked_binary="$(git -C "$ROOT_DIR" ls-files | grep -E '(^|/)(NoritoBridge.*([.]zip|[.]a|[.]framework|[.]xcframework)|libconnect_norito_bridge([.]a|[.]dylib))($|/)' || true)" +[[ -z "$tracked_binary" ]] || { + printf '%s\n' "$tracked_binary" >&2 + fail "Iroha SDK/native binary material was tracked before review" +} + +for marker in \ + 'BLOCKED / fail closed' \ + 'v2.0.0-rc.2.1-fearless-mobile-sdk.3' \ + 'dc944af3dc98d37d349b9f95fe25b9e4a920f095db58adbcccc6354fc28ada4b' \ + '1396110349' \ + '704780504' \ + 'iOS `14.1`' \ + 'iOS `15.0`' \ + 'public default arguments' \ + 'None matches the corresponding published release slice' \ + '9756355bec7ca04a9e95025a0f24296a02118a5bf5989cc9c2cec0612bf76201' \ + 'c072426bffe62e12fc6b94a0c37ecf4eb2a805965964ce999e5183bddc9a1ce7' \ + '6cc8aa66faa4b067c44831deaf225d8637ffd6daba05b11857b69a06a6b4279b' \ + 'unpublished correction' \ + '61CtjvNd9T3THAR65GsMVHr82Bjc' \ + '6TEAJqbb8oEPmLncoNiMRbLEK6tw' \ + '039af2d65e10b773be5031ab8fc07cf27b40e30d' \ + 'Swift `String`' \ + 'Nexus wallet-smoke metadata boundary' \ + '"evidence_role": "wallet-smoke"' \ + '"route_governance_action_hash": "sha256:<64 lowercase nonzero hex characters>"' \ + '"wallet_platform": "ios"' \ + '"wallet_commit": "<40 lowercase nonzero git hex characters>"' \ + 'Missing, extra, case-shifted, control-character, non-ASCII, malformed, and' \ + 'accepted plus' \ + 'funded Taira broadcast' \ + 'apple_xcframework_review_and_materialization_required' \ + 'does **not** make Iroha send production-ready'; do + require_fixed "$DOC" "$marker" "readiness evidence marker '$marker'" +done + +require_fixed "$UNIVERSAL_DOC" \ + 'Iroha `features: ["transfer"]` is capability metadata, not a production-send' \ + "Universal Wallet non-enablement statement" +require_fixed "$UNIVERSAL_DOC" \ + '`config/iroha-production-send-readiness.json`' \ + "Universal Wallet readiness manifest reference" +require_fixed "$UNIVERSAL_DOC" \ + 'The iOS Nexus operator evidence seam snapshots an exact four-string' \ + "Universal Wallet exact wallet-smoke metadata statement" + +for marker in \ + 'iOS 14.1 versus SDK iOS 15 support decision' \ + 'canonical compact transaction-hash parity' \ + 'authoritative protocol chain ID' \ + 'zeroizable secret-lifecycle boundary' \ + 'exact local/Torii receipt-hash equality' \ + 'funded Taira and Nexus broadcast evidence' \ + 'local unpublished upstream correction is diagnostic only'; do + require_fixed "$RELEASE_CHECKLIST" "$marker" "release checklist Iroha gate '$marker'" +done + +echo "[iroha-send-readiness][ios] 14.1/15, source/binary, parity, protocol, key-lifecycle, receipt, live-evidence, and fail-closed invariants passed." diff --git a/scripts/audit-release-signing-gate.sh b/scripts/audit-release-signing-gate.sh new file mode 100755 index 0000000000..18ce367b16 --- /dev/null +++ b/scripts/audit-release-signing-gate.sh @@ -0,0 +1,214 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +SCHEME_FILE="${IOS_RELEASE_GATE_AUDIT_SCHEME_FILE:-$ROOT_DIR/fearless.xcodeproj/xcshareddata/xcschemes/fearless.xcscheme}" +WORKFLOW_FILE="${IOS_RELEASE_GATE_AUDIT_WORKFLOW_FILE:-$ROOT_DIR/.github/workflows/codecov.yml}" +RUN_PR_FILE="${IOS_RELEASE_GATE_AUDIT_RUN_PR_FILE:-$ROOT_DIR/scripts/ci/run-pr.sh}" +GATE_FILE="${IOS_RELEASE_GATE_AUDIT_GATE_FILE:-$ROOT_DIR/scripts/ci/run-release-signing-gate.sh}" +BOOTSTRAP_FILE="${IOS_RELEASE_GATE_AUDIT_BOOTSTRAP_FILE:-$ROOT_DIR/scripts/ci/run-release-signing-ci.sh}" +BOOTSTRAP_TEST_FILE="${IOS_RELEASE_GATE_AUDIT_BOOTSTRAP_TEST_FILE:-$ROOT_DIR/scripts/test-release-signing-bootstrap.sh}" + +fail() { + echo "[release-signing-gate-audit][error] $*" >&2 + exit 1 +} + +require_literal() { + local file="$1" + local literal="$2" + local message="$3" + grep -Fq -- "$literal" "$file" || fail "$message" +} + +require_active_literal() { + local file="$1" + local literal="$2" + local message="$3" + awk -v expected="$literal" ' + { + line = $0 + sub(/^[[:space:]]*/, "", line) + sub(/[[:space:]]*$/, "", line) + if (line == expected) found = 1 + } + END { exit found ? 0 : 1 } + ' "$file" || fail "$message" +} + +for required_file in "$SCHEME_FILE" "$WORKFLOW_FILE" "$RUN_PR_FILE" "$GATE_FILE" "$BOOTSTRAP_FILE" "$BOOTSTRAP_TEST_FILE"; do + [[ -f "$required_file" ]] || fail "Missing release-signing gate input: $required_file" +done + +archive_action="$(sed -n '//p' "$SCHEME_FILE")" +grep -Fq 'buildConfiguration = "Release"' <<<"$archive_action" || + fail 'ArchiveAction must use the Release configuration.' +require_literal "$SCHEME_FILE" 'export STRICT_REQUIRED_PATCHES=1' \ + 'The manual archive preaction must enforce required dependency patches.' +require_literal "$SCHEME_FILE" 'export ALLOW_DERIVEDDATA_FALLBACK=0' \ + 'The manual archive preaction must use the repository-owned package checkout.' +require_literal "$SCHEME_FILE" 'if ! bash \"${PROJECT_DIR}/scripts/spm-shared-features-fixes.sh\"' \ + 'The manual archive preaction must check dependency-patch failure explicitly.' +require_literal "$SCHEME_FILE" 'error: Required shared-features compatibility fixes failed.' \ + 'The manual archive preaction must provide actionable failure diagnostics.' +if grep -Eq 'spm-shared-features-fixes\.sh[^<]*[|][|][[:space:]]*true' "$SCHEME_FILE"; then + fail 'The manual archive preaction must not ignore dependency-patch failures.' +fi + +require_active_literal "$WORKFLOW_FILE" 'bash ./scripts/test-release-signing-gate-audit.sh' \ + 'GitHub CI must run the release-signing gate mutation self-test.' +require_active_literal "$WORKFLOW_FILE" 'bash ./scripts/test-release-signing-gate.sh' \ + 'GitHub CI must run the behavioral release-signing gate self-test.' +require_active_literal "$WORKFLOW_FILE" 'bash ./scripts/test-release-signing-bootstrap.sh' \ + 'GitHub CI must run the signing-bootstrap behavioral self-test.' +require_active_literal "$WORKFLOW_FILE" 'bash ./scripts/audit-release-signing-gate.sh' \ + 'GitHub CI must audit the real release-signing gate.' +signed_gate_step="$(awk ' + /^ - name: Signed Release archive gate$/ { capture = 1 } + capture { + if (seen && /^ - name:/) exit + print + seen = 1 + } +' "$WORKFLOW_FILE")" +[[ -n "$signed_gate_step" ]] || fail \ + 'GitHub CI must define a dedicated signed Release archive gate step.' +grep -Fqx " if: github.event_name == 'push'" <<<"$signed_gate_step" || fail \ + 'The real signing gate step must run only for trusted protected-branch pushes.' +for signed_step_line in \ + " IOS_RELEASE_SIGNING_REQUIRED: '1'" \ + ' IOS_RELEASE_CERTIFICATE_P12_BASE64: ${{ secrets.IOS_RELEASE_CERTIFICATE_P12_BASE64 }}' \ + ' IOS_RELEASE_CERTIFICATE_PASSWORD: ${{ secrets.IOS_RELEASE_CERTIFICATE_PASSWORD }}' \ + ' IOS_RELEASE_PROVISIONING_PROFILE_BASE64: ${{ secrets.IOS_RELEASE_PROVISIONING_PROFILE_BASE64 }}' \ + ' run: bash ./scripts/ci/run-release-signing-ci.sh'; do + grep -Fqx "$signed_step_line" <<<"$signed_gate_step" || fail \ + 'Trusted GitHub CI must require and bootstrap all ephemeral Release-signing inputs in its protected step.' +done +if grep -Fq 'vars.IOS_RELEASE_SIGNING_REQUIRED' <<<"$signed_gate_step"; then + fail 'Trusted release CI must not make signed archive validation optional through a repository variable.' +fi + +require_active_literal "$RUN_PR_FILE" 'bash "$WORKSPACE_DIR/scripts/test-release-signing-gate-audit.sh"' \ + 'PR CI must run the release-signing gate mutation self-test.' +require_active_literal "$RUN_PR_FILE" 'bash "$WORKSPACE_DIR/scripts/test-release-signing-gate.sh"' \ + 'PR CI must run the behavioral release-signing gate self-test.' +require_active_literal "$RUN_PR_FILE" 'bash "$WORKSPACE_DIR/scripts/test-release-signing-bootstrap.sh"' \ + 'PR CI must run the signing-bootstrap behavioral self-test.' +require_active_literal "$RUN_PR_FILE" 'bash "$WORKSPACE_DIR/scripts/audit-release-signing-gate.sh"' \ + 'PR CI must audit the real release-signing gate.' + +require_literal "$GATE_FILE" 'set -euo pipefail' \ + 'The release-signing gate must fail on command, variable, or pipeline errors.' +require_literal "$GATE_FILE" "-configuration Release" \ + 'The release-signing gate must archive the Release configuration.' +require_literal "$GATE_FILE" "-destination 'generic/platform=iOS'" \ + 'The release-signing gate must target a generic iOS device.' +require_literal "$GATE_FILE" '-disableAutomaticPackageResolution' \ + 'The release-signing gate must not mutate dependency resolution.' +require_literal "$GATE_FILE" 'archive >"$build_log" 2>&1' \ + 'The release-signing gate must create and privately capture a real archive.' +require_literal "$GATE_FILE" '"$CODESIGN_BIN" --verify --deep --strict' \ + 'The archived application signature must be strictly verified.' +require_literal "$GATE_FILE" 'profile_matches_release_contract "$embedded_profile"' \ + 'The embedded profile must be validated against the production contract.' +wildcard_authorization_call_count="$( + grep -F -c 'profile_supports_value \' "$GATE_FILE" || true +)" +[[ "$wildcard_authorization_call_count" == "3" ]] || + fail 'The embedded profile must authorize both associated domains and CloudKit through the wildcard-aware validator.' +require_literal "$GATE_FILE" '"$value" == "*" || "$value" == "$expected"' \ + 'Apple provisioning-profile string wildcards must be recognized exactly.' +require_literal "$GATE_FILE" 'plist_array_contains "$profile" "$key_path" "*"' \ + 'Apple provisioning-profile array wildcards must be recognized exactly.' +require_literal "$GATE_FILE" 'Entitlements.beta-reports-active' \ + 'The embedded profile must authorize TestFlight beta reporting.' +require_literal "$GATE_FILE" "'beta-reports-active'" \ + 'The signed application must carry TestFlight beta reporting.' +require_literal "$GATE_FILE" '[[ "$(plist_value "$profile" '\''Entitlements.beta-reports-active'\'' || true)" == "true" ]]' \ + 'The embedded profile beta entitlement must be exactly true.' +require_literal "$GATE_FILE" '[[ "$(plist_value "$signed_entitlements" '\''beta-reports-active'\'' || true)" == "true" ]]' \ + 'The signed application beta entitlement must be exactly true.' +require_literal "$GATE_FILE" 'signed_get_task_allow" == "false" && "$profile_get_task_allow" == "false"' \ + 'The signed application and profile must both disable development debugging.' +require_literal "$GATE_FILE" 'ProvisionedDevices' \ + 'Ad-hoc and development provisioning profiles must be rejected.' +require_literal "$GATE_FILE" 'IOS_RELEASE_EXPECTED_PROFILE_UUID' \ + 'Trusted CI must bind the archive to the bootstrapped profile UUID.' +require_literal "$GATE_FILE" 'PROVISIONING_PROFILE_SPECIFIER=$PROFILE_SPECIFIER' \ + 'The Release archive command must select the bootstrapped profile explicitly.' +require_literal "$GATE_FILE" 'PrivacyInfo.xcprivacy' \ + 'The signed Release archive must contain the privacy manifest.' +require_literal "$GATE_FILE" 'keychain-access-groups' \ + 'The signed Release archive must reject unreviewed keychain access groups.' +require_literal "$GATE_FILE" 'Signing material is only partially configured' \ + 'Partially configured signing material must fail closed.' +require_literal "$GATE_FILE" 'IOS_RELEASE_SIGNING_REQUIRED' \ + 'Trusted CI must be able to require signing material explicitly.' +require_literal "$GATE_FILE" '[[ "${GITHUB_EVENT_NAME:-}" == "push" ]]' \ + 'The gate itself must refuse non-push GitHub events before reading signing material.' +require_literal "$GATE_FILE" 'refs/heads/develop|refs/heads/master' \ + 'The gate itself must restrict signing-material access to release branches.' +require_literal "$GATE_FILE" 'chmod 600 "$build_log"' \ + 'The raw archive log must be private.' +require_literal "$GATE_FILE" '[REDACTED_DIGEST]' \ + 'Archive failure diagnostics must redact certificate digests.' +require_literal "$GATE_FILE" 's/(Signing Identity:).*/\1 [REDACTED]/' \ + 'Archive failure diagnostics must redact signing identities.' +require_literal "$GATE_FILE" 's/(Provisioning Profile:).*/\1 [REDACTED]/' \ + 'Archive failure diagnostics must redact provisioning profile details.' +require_literal "$GATE_FILE" 's/(Certificate:).*/\1 [REDACTED]/' \ + 'Archive failure diagnostics must redact certificate details.' + +if grep -Eq '(^|[[:space:]])set[[:space:]]+-x|(^|[[:space:]])(printenv|env)([[:space:]]|$)' "$GATE_FILE"; then + fail 'The release-signing gate must not dump commands or environment variables.' +fi +if grep -Eq '^[[:space:]]*(echo|printf).*(identity_output|find-identity)' "$GATE_FILE"; then + fail 'The release-signing gate must not print signing identity output.' +fi + +require_literal "$BOOTSTRAP_FILE" 'set -euo pipefail' \ + 'The signing bootstrap must fail closed on shell errors.' +require_literal "$BOOTSTRAP_FILE" 'GITHUB_EVENT_NAME:-}" == "push"' \ + 'The signing bootstrap must reject non-push events before reading secrets.' +require_literal "$BOOTSTRAP_FILE" 'refs/heads/develop|refs/heads/master' \ + 'The signing bootstrap must restrict secret use to release branches.' +require_literal "$BOOTSTRAP_FILE" 'IOS_RELEASE_SIGNING_REQUIRED:-}" == "1"' \ + 'The signing bootstrap must require literal signed-archive enforcement.' +for secret_name in \ + IOS_RELEASE_CERTIFICATE_P12_BASE64 \ + IOS_RELEASE_CERTIFICATE_PASSWORD \ + IOS_RELEASE_PROVISIONING_PROFILE_BASE64; do + require_literal "$BOOTSTRAP_FILE" "$secret_name" \ + "The signing bootstrap must consume $secret_name explicitly." +done +require_literal "$BOOTSTRAP_FILE" 'Release-signing secrets are missing' \ + 'Missing signing secrets must fail closed.' +require_literal "$BOOTSTRAP_FILE" 'Release-signing secrets are only partially configured' \ + 'Partially configured signing secrets must fail closed.' +require_literal "$BOOTSTRAP_FILE" 'if ((configured != 3)); then' \ + 'Signing secrets must use an exact all-or-nothing cardinality gate.' +require_literal "$BOOTSTRAP_FILE" 'create-keychain' \ + 'The signing bootstrap must create an ephemeral keychain.' +require_literal "$BOOTSTRAP_FILE" 'set-key-partition-list' \ + 'The signing bootstrap must constrain imported certificate access.' +require_literal "$BOOTSTRAP_FILE" 'delete-keychain "$keychain_path"' \ + 'The signing bootstrap must delete its ephemeral keychain.' +require_literal "$BOOTSTRAP_FILE" 'rm -f -- "$installed_profile"' \ + 'The signing bootstrap must remove its installed provisioning profile.' +require_literal "$BOOTSTRAP_FILE" 'unset certificate_base64 profile_base64' \ + 'The signing bootstrap must drop encoded signing material before invoking build tools.' +require_literal "$BOOTSTRAP_FILE" 'unset certificate_password' \ + 'The signing bootstrap must drop the certificate password immediately after import.' +if grep -Eq '(^|[[:space:]])set[[:space:]]+-x|(^|[[:space:]])(printenv|env)([[:space:]]|$)' "$BOOTSTRAP_FILE"; then + fail 'The signing bootstrap must not dump commands or environment variables.' +fi +require_literal "$BOOTSTRAP_TEST_FILE" 'missing signing secrets' \ + 'The signing-bootstrap self-test must reject missing secret configuration.' +require_literal "$BOOTSTRAP_TEST_FILE" 'partially configured signing secrets' \ + 'The signing-bootstrap self-test must reject partial secret configuration.' +require_literal "$BOOTSTRAP_TEST_FILE" 'ephemeral keychain cleanup failure' \ + 'The signing-bootstrap self-test must reject cleanup failures.' +require_literal "$BOOTSTRAP_TEST_FILE" 'assert_no_secret_leak' \ + 'The signing-bootstrap self-test must enforce output redaction.' + +echo "[release-signing-gate-audit] passed" diff --git a/scripts/audit-ton-production-send-readiness.sh b/scripts/audit-ton-production-send-readiness.sh new file mode 100755 index 0000000000..0f37dcbfad --- /dev/null +++ b/scripts/audit-ton-production-send-readiness.sh @@ -0,0 +1,563 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="${TON_SEND_AUDIT_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" +MANIFEST="$ROOT_DIR/config/ton-production-send-readiness.json" +DOC="$ROOT_DIR/docs/ton-production-send-readiness.md" +RELEASE_CHECKLIST="$ROOT_DIR/docs/release-checklist.md" +SEND_CONTAINER="$ROOT_DIR/fearless/Modules/Send/SendDependencyContainer.swift" +CHAIN_REGISTRY="$ROOT_DIR/fearless/Common/Services/ChainRegistry/ChainRegistry.swift" +TON_BUILDER="$ROOT_DIR/fearless/Common/Model/TonTransferTransactionBuilder.swift" +TON_SERVICE="$ROOT_DIR/fearless/Common/Model/TonSendService.swift" +RUN_PR="$ROOT_DIR/scripts/ci/run-pr.sh" +WORKFLOW="$ROOT_DIR/.github/workflows/codecov.yml" +EXPECTED_MANIFEST_SHA256="${TON_SEND_AUDIT_EXPECTED_MANIFEST_SHA256:-5673b0df8a293df3961761db4f7c3a08363d3eb8a84ebdb49f1d2f4822209286}" + +fail() { + echo "[ton-send-readiness][ios][error] $*" >&2 + exit 1 +} + +sha256_file() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{print $1}' + else + shasum -a 256 "$1" | awk '{print $1}' + fi +} + +require_file() { + local path="$1" + [[ -f "$path" && ! -L "$path" ]] || + fail "required regular file is missing or is a symlink: ${path#"$ROOT_DIR/"}" +} + +require_fixed() { + local path="$1" + local marker="$2" + local label="$3" + grep -Fq -- "$marker" "$path" || fail "$label is missing from ${path#"$ROOT_DIR/"}" +} + +require_active_line() { + local path="$1" + local expected="$2" + local label="$3" + awk -v expected="$expected" ' + { + line = $0 + sub(/^[[:space:]]*/, "", line) + sub(/[[:space:]]*$/, "", line) + if (line == expected) found = 1 + } + END { exit found ? 0 : 1 } + ' "$path" || fail "$label is missing from ${path#"$ROOT_DIR/"}" +} + +for path in \ + "$MANIFEST" "$DOC" "$RELEASE_CHECKLIST" "$SEND_CONTAINER" \ + "$CHAIN_REGISTRY" "$TON_BUILDER" "$TON_SERVICE" "$RUN_PR" "$WORKFLOW"; do + require_file "$path" +done + +manifest_bytes="$(wc -c < "$MANIFEST" | tr -d '[:space:]')" +[[ "$manifest_bytes" =~ ^[0-9]+$ && "$manifest_bytes" -le 32768 ]] || + fail "readiness manifest exceeds 32 KiB" +[[ "$EXPECTED_MANIFEST_SHA256" =~ ^[0-9a-f]{64}$ ]] || + fail "expected manifest digest must be exactly 64 lowercase hexadecimal characters" +actual_manifest_sha256="$(sha256_file "$MANIFEST")" +[[ "$actual_manifest_sha256" == "$EXPECTED_MANIFEST_SHA256" ]] || + fail "readiness manifest digest mismatch: expected $EXPECTED_MANIFEST_SHA256, got $actual_manifest_sha256" + +node - "$MANIFEST" <<'NODE' +const fs = require('node:fs'); +const path = process.argv[2]; +let manifest; +try { + manifest = JSON.parse(fs.readFileSync(path, 'utf8')); +} catch (error) { + console.error(`[ton-send-readiness][ios][error] invalid readiness JSON: ${error.message}`); + process.exit(1); +} + +function fail(message) { + console.error(`[ton-send-readiness][ios][error] ${message}`); + process.exit(1); +} + +function assert(condition, message) { + if (!condition) fail(message); +} + +function exactKeys(value, expected, label) { + assert(value && typeof value === 'object' && !Array.isArray(value), `${label} must be an object`); + const actual = Object.keys(value).sort(); + const wanted = [...expected].sort(); + assert(JSON.stringify(actual) === JSON.stringify(wanted), `${label} keys drifted`); +} + +function exactArray(value, expected, label) { + assert(Array.isArray(value), `${label} must be an array`); + assert(JSON.stringify(value) === JSON.stringify(expected), `${label} drifted`); + assert(new Set(value).size === value.length, `${label} must not contain duplicates`); +} + +exactKeys(manifest, [ + 'schemaVersion', 'platform', 'chain', 'assessedAt', 'status', + 'releaseEnabled', 'runtimeGuard', 'feeEvidence', + 'endpointCredentialEvidence', 'fundedMainnetEvidence', + 'expiredIntentRecoveryEvidence', 'releaseEvidenceBundle', 'blocker', + 'exitCriteria' +], 'top-level manifest'); +assert(manifest.schemaVersion === 1, 'schemaVersion must be 1'); +assert(manifest.platform === 'ios', 'platform must be ios'); +assert(manifest.chain === 'ton-mainnet', 'chain must be ton-mainnet'); +assert(manifest.assessedAt === '2026-07-16', 'assessment date drifted'); +assert(manifest.status === 'blocked', 'status must remain blocked'); +assert(manifest.releaseEnabled === false, 'releaseEnabled must remain false'); + +exactKeys(manifest.runtimeGuard, [ + 'policyType', 'productionInitializer', 'runtimeOverrideAllowed', + 'debugTestOverrideOnly' +], 'runtimeGuard'); +assert(manifest.runtimeGuard.policyType === 'TonProductionSendReleasePolicy', 'runtime policy type drifted'); +assert(manifest.runtimeGuard.productionInitializer === 'isEnabled: false', 'production initializer must remain disabled'); +assert(manifest.runtimeGuard.runtimeOverrideAllowed === false, 'runtime override must remain forbidden'); +assert(manifest.runtimeGuard.debugTestOverrideOnly === true, 'enablement must remain DEBUG-test-only'); + +const fee = manifest.feeEvidence; +exactKeys(fee, [ + 'walletVersion', 'activeMechanism', 'unsignedEndpoint', 'signedEndpoint', + 'requiredComparison', 'tonApiParityEvidence', + 'reviewedAttestedReplacement', 'criterionSatisfied' +], 'feeEvidence'); +assert(fee.walletVersion === 'Wallet V4R2', 'fee evidence wallet version drifted'); +assert(fee.activeMechanism === 'tonapi-unsigned-ignore-signature-check-plus-signed-emulation', 'fee mechanism drifted'); +assert(fee.unsignedEndpoint === '/v2/traces/emulate?ignore_signature_check=true', 'unsigned emulation endpoint drifted'); +assert(fee.signedEndpoint === '/v2/wallet/emulate', 'signed emulation endpoint drifted'); +assert(fee.requiredComparison === 'exact-total-fee-nanotons-for-identical-wallet-v4r2-template', 'fee parity comparison drifted'); +assert(fee.tonApiParityEvidence === 'missing', 'independent TonAPI fee parity evidence must remain missing until review'); +assert(fee.reviewedAttestedReplacement === 'not-provided', 'a local replacement must not be claimed without reviewed attestation'); +assert(fee.criterionSatisfied === false, 'fee evidence criterion must remain unsatisfied'); + +const credential = manifest.endpointCredentialEvidence; +exactKeys(credential, [ + 'requiredOrigin', 'credentialContract', 'releaseProvisioningAttestation', + 'registryOriginAttestation', 'credentialExposureReview', + 'criterionSatisfied' +], 'endpointCredentialEvidence'); +assert(credential.requiredOrigin === 'https://tonapi.io', 'required authenticated origin drifted'); +assert(credential.credentialContract === 'nonempty-visible-ascii-maximum-4096-bytes', 'credential contract drifted'); +assert(credential.releaseProvisioningAttestation === 'missing', 'release credential provisioning must remain unattested'); +assert(credential.registryOriginAttestation === 'missing', 'registry origin must remain unattested'); +assert(credential.credentialExposureReview === 'missing', 'credential exposure review must remain missing'); +assert(credential.criterionSatisfied === false, 'endpoint credential criterion must remain unsatisfied'); + +const funded = manifest.fundedMainnetEvidence; +exactKeys(funded, [ + 'walletVersion', 'requiredStages', 'evidenceBundle', 'transactionHash', + 'criterionSatisfied' +], 'fundedMainnetEvidence'); +assert(funded.walletVersion === 'Wallet V4R2', 'funded evidence wallet version drifted'); +exactArray(funded.requiredStages, [ + 'unsigned-emulation', + 'signed-emulation', + 'broadcast', + 'on-chain-reconciliation', + 'exact-fee-verification', + 'recipient-credit-verification' +], 'funded mainnet required stages'); +assert(funded.evidenceBundle === 'missing', 'funded mainnet evidence must remain missing'); +assert(funded.transactionHash === null, 'no funded-mainnet transaction hash may be claimed'); +assert(funded.criterionSatisfied === false, 'funded mainnet criterion must remain unsatisfied'); + +const recovery = manifest.expiredIntentRecoveryEvidence; +exactKeys(recovery, [ + 'requiredPolicy', 'currentFailClosedBehavior', + 'finalizedChainAbsenceProcedure', 'auditedQuarantineProcedure', + 'selectedProcedure', 'senderUnblockAllowed', 'criterionSatisfied' +], 'expiredIntentRecoveryEvidence'); +assert(recovery.requiredPolicy === 'finalized-chain-absence-or-explicit-audited-quarantine', 'expired-intent recovery policy drifted'); +assert(recovery.currentFailClosedBehavior === 'block-sender-indefinitely', 'current expired-intent behavior drifted'); +assert(recovery.finalizedChainAbsenceProcedure === 'missing', 'finalized-chain absence procedure must remain missing'); +assert(recovery.auditedQuarantineProcedure === 'missing', 'audited quarantine procedure must remain missing'); +assert(recovery.selectedProcedure === null, 'no unreviewed expired-intent procedure may be selected'); +assert(recovery.senderUnblockAllowed === false, 'sender unblock must remain forbidden'); +assert(recovery.criterionSatisfied === false, 'expired-intent recovery criterion must remain unsatisfied'); + +const bundle = manifest.releaseEvidenceBundle; +exactKeys(bundle, [ + 'immutableBundleId', 'sha256', 'reviewers', 'reviewedAt', + 'allCriteriaSatisfied' +], 'releaseEvidenceBundle'); +assert(bundle.immutableBundleId === null, 'immutable evidence bundle ID must remain absent'); +assert(bundle.sha256 === null, 'immutable evidence bundle digest must remain absent'); +exactArray(bundle.reviewers, [], 'release evidence reviewers'); +assert(bundle.reviewedAt === null, 'release evidence review date must remain absent'); +assert(bundle.allCriteriaSatisfied === false, 'release evidence bundle must remain incomplete'); + +exactKeys(manifest.blocker, ['code', 'reason'], 'blocker'); +assert(manifest.blocker.code === 'ton-live-release-evidence-missing', 'blocker code drifted'); +assert( + manifest.blocker.reason === 'Independent Wallet V4R2 fee-parity or reviewed replacement evidence, exact endpoint credential provisioning attestations, a funded mainnet transfer evidence bundle, and a reviewed finalized-absence or audited-quarantine recovery procedure are not recorded.', + 'blocker reason drifted' +); +exactArray(manifest.exitCriteria, [ + 'record-independent-exact-tonapi-wallet-v4r2-unsigned-signed-fee-parity-or-publish-a-reviewed-attested-local-replacement', + 'attest-the-exact-binary-owned-origin-release-credential-provisioning-and-registry-provenance', + 'record-an-immutable-funded-mainnet-wallet-v4r2-emulation-broadcast-reconciliation-fee-and-recipient-credit-evidence-bundle', + 'approve-a-finalized-chain-absence-procedure-or-an-explicit-audited-quarantine-and-recovery-procedure-for-expired-bearer-records', + 'review-the-complete-immutable-evidence-bundle-before-changing-the-production-runtime-policy' +], 'exit criteria'); +NODE + +node - "$SEND_CONTAINER" "$CHAIN_REGISTRY" "$TON_BUILDER" "$TON_SERVICE" <<'NODE' +const fs = require('node:fs'); + +function fail(message) { + console.error(`[ton-send-readiness][ios][error] ${message}`); + process.exit(1); +} + +const stringMarkerPrefix = '__TON_SWIFT_STRING_LITERAL_'; + +function masked(value) { + return value.replace(/[^\r\n]/g, ' '); +} + +// This is deliberately a small, fail-closed Swift lexer rather than a regex-only +// comment stripper. Required release markers must not be sourced from comments, +// multiline strings, raw strings, or string interpolation text. +function lexSwift(source, label) { + if (source.includes(stringMarkerPrefix)) { + fail(`${label} contains the reserved lexer marker prefix`); + } + + let output = ''; + const strings = []; + let index = 0; + + while (index < source.length) { + if (source.startsWith('//', index)) { + const end = source.indexOf('\n', index + 2); + const limit = end === -1 ? source.length : end; + output += masked(source.slice(index, limit)); + index = limit; + continue; + } + + if (source.startsWith('/*', index)) { + const start = index; + let depth = 1; + index += 2; + while (index < source.length && depth > 0) { + if (source.startsWith('/*', index)) { + depth += 1; + index += 2; + } else if (source.startsWith('*/', index)) { + depth -= 1; + index += 2; + } else { + index += 1; + } + } + if (depth !== 0) fail(`${label} contains an unterminated nested block comment`); + output += masked(source.slice(start, index)); + continue; + } + + let hashCount = 0; + while (source[index + hashCount] === '#') hashCount += 1; + const quoteIndex = index + hashCount; + if (source[quoteIndex] === '"') { + const multiline = source.startsWith('"""', quoteIndex); + const quoteCount = multiline ? 3 : 1; + const contentStart = quoteIndex + quoteCount; + const closingDelimiter = '"'.repeat(quoteCount) + '#'.repeat(hashCount); + let cursor = contentStart; + let closingIndex = -1; + + while (cursor < source.length) { + if (source.startsWith(closingDelimiter, cursor)) { + closingIndex = cursor; + break; + } + // In non-raw strings, an escape consumes the next source character. This + // also prevents an escaped quote from becoming a false closing delimiter. + if (hashCount === 0 && source[cursor] === '\\') { + cursor = Math.min(cursor + 2, source.length); + } else { + cursor += 1; + } + } + + if (closingIndex === -1) fail(`${label} contains an unterminated Swift string literal`); + const end = closingIndex + closingDelimiter.length; + const value = source.slice(contentStart, closingIndex); + const literalIndex = strings.length; + strings.push({ value, hashCount, multiline }); + output += `${stringMarkerPrefix}${literalIndex}__`; + output += source.slice(index, end).replace(/[^\r\n]/g, ''); + index = end; + continue; + } + + output += source[index]; + index += 1; + } + + return { code: output, strings }; +} + +function stripBalancedOuterParentheses(expression) { + let value = expression.trim(); + while (value.startsWith('(') && value.endsWith(')')) { + let depth = 0; + let enclosesWholeExpression = true; + for (let index = 0; index < value.length; index += 1) { + if (value[index] === '(') depth += 1; + if (value[index] === ')') depth -= 1; + if (depth === 0 && index < value.length - 1) { + enclosesWholeExpression = false; + break; + } + if (depth < 0) return value; + } + if (!enclosesWholeExpression || depth !== 0) break; + value = value.slice(1, -1).trim(); + } + return value; +} + +function splitConditional(expression, operator) { + const pieces = []; + let depth = 0; + let start = 0; + for (let index = 0; index < expression.length; index += 1) { + if (expression[index] === '(') depth += 1; + if (expression[index] === ')') depth -= 1; + if (depth === 0 && expression.startsWith(operator, index)) { + pieces.push(expression.slice(start, index)); + start = index + operator.length; + index += operator.length - 1; + } + } + if (pieces.length === 0) return null; + pieces.push(expression.slice(start)); + return pieces; +} + +function evaluateConditional(expression, definitions) { + const value = stripBalancedOuterParentheses(expression); + const disjunction = splitConditional(value, '||'); + if (disjunction) { + const results = disjunction.map((part) => evaluateConditional(part, definitions)); + if (results.includes(true)) return true; + if (results.every((result) => result === false)) return false; + return null; + } + const conjunction = splitConditional(value, '&&'); + if (conjunction) { + const results = conjunction.map((part) => evaluateConditional(part, definitions)); + if (results.includes(false)) return false; + if (results.every((result) => result === true)) return true; + return null; + } + if (value.startsWith('!')) { + const result = evaluateConditional(value.slice(1), definitions); + return result === null ? null : !result; + } + if (value === 'true') return true; + if (value === 'false') return false; + if (definitions.has(value)) return definitions.get(value); + return null; +} + +// Unknown platform/compiler predicates are not treated as release-active. That +// conservative choice prevents an unproven branch from supplying a safety marker. +function activeConditionalCode(code, definitions, label) { + const lines = code.match(/[^\n]*(?:\n|$)/g).filter((line) => line.length > 0); + const stack = []; + let active = true; + let output = ''; + + for (const line of lines) { + const directive = line.trim(); + let match = directive.match(/^#if\s+(.+)$/); + if (match) { + const result = evaluateConditional(match[1], definitions); + const frame = { + parentActive: active, + branchTaken: result === true, + uncertain: result === null, + currentActive: active && result === true + }; + stack.push(frame); + active = frame.currentActive; + output += masked(line); + continue; + } + + match = directive.match(/^#elseif\s+(.+)$/); + if (match) { + const frame = stack.at(-1); + if (!frame) fail(`${label} contains #elseif without #if`); + const result = evaluateConditional(match[1], definitions); + if (!frame.parentActive || frame.branchTaken || frame.uncertain) { + frame.currentActive = false; + } else if (result === true) { + frame.currentActive = true; + frame.branchTaken = true; + } else { + frame.currentActive = false; + if (result === null) frame.uncertain = true; + } + active = frame.currentActive; + output += masked(line); + continue; + } + + if (/^#else\b/.test(directive)) { + const frame = stack.at(-1); + if (!frame) fail(`${label} contains #else without #if`); + frame.currentActive = frame.parentActive && !frame.branchTaken && !frame.uncertain; + frame.branchTaken ||= frame.currentActive; + active = frame.currentActive; + output += masked(line); + continue; + } + + if (/^#endif\b/.test(directive)) { + const frame = stack.pop(); + if (!frame) fail(`${label} contains #endif without #if`); + active = frame.parentActive; + output += masked(line); + continue; + } + + output += active ? line : masked(line); + } + + if (stack.length !== 0) fail(`${label} contains an unterminated conditional-compilation block`); + return output; +} + +function count(source, pattern) { + return [...source.matchAll(pattern)].length; +} + +const sendLexed = lexSwift(fs.readFileSync(process.argv[2], 'utf8'), 'send dependency container'); +const registryLexed = lexSwift(fs.readFileSync(process.argv[3], 'utf8'), 'chain registry'); +const builderLexed = lexSwift(fs.readFileSync(process.argv[4], 'utf8'), 'TON transaction builder'); +const serviceLexed = lexSwift(fs.readFileSync(process.argv[5], 'utf8'), 'TON send service'); +const send = sendLexed.code; +const registry = registryLexed.code; +const builder = builderLexed.code; +const service = serviceLexed.code; +const releaseDefinitions = new Map([['DEBUG', false]]); +const debugDefinitions = new Map([['DEBUG', true]]); +const releaseSend = activeConditionalCode(send, releaseDefinitions, 'send dependency container'); +const debugSend = activeConditionalCode(send, debugDefinitions, 'send dependency container'); +const releaseRegistry = activeConditionalCode(registry, releaseDefinitions, 'chain registry'); + +if (count(releaseSend, /static\s+let\s+production\s*=\s*TonProductionSendReleasePolicy\s*\(\s*isEnabled:\s*false\s*\)/g) !== 1) { + fail('TON production policy must have exactly one active false initializer'); +} +if (/static\s+let\s+production\s*=\s*TonProductionSendReleasePolicy\s*\(\s*isEnabled:\s*true\s*\)/.test(send)) { + fail('TON production policy must never be enabled'); +} +if (count(releaseSend, /private\s+init\s*\(\s*isEnabled:\s*Bool\s*\)/g) !== 1) { + fail('TON release-policy initializer must remain private'); +} +if (count(send, /enabledForTests\s*=\s*TonProductionSendReleasePolicy\s*\(\s*isEnabled:\s*true\s*\)/g) !== 1) { + fail('TON DEBUG-only test policy must exist exactly once'); +} +if (count(debugSend, /enabledForTests\s*=\s*TonProductionSendReleasePolicy\s*\(\s*isEnabled:\s*true\s*\)/g) !== 1 || + count(releaseSend, /enabledForTests\s*=/g) !== 0) { + fail('TON enabledForTests policy escaped the DEBUG block'); +} +if (count(releaseSend, /releasePolicy:\s*\.production/g) < 1) { + fail('production send container must bind the default release policy'); +} +if (count(releaseSend, /tonProductionSendDisabled/g) < 3) { + fail('production TON routing fail-closed guards drifted'); +} + +const canonicalOriginMatches = [...releaseRegistry.matchAll( + /canonicalAuthenticatedOrigin\s*=\s*URL\s*\(\s*string:\s*__TON_SWIFT_STRING_LITERAL_(\d+)__\s*\)!/g +)]; +if (canonicalOriginMatches.length !== 1 || + registryLexed.strings[Number(canonicalOriginMatches[0][1])]?.value !== 'https://tonapi.io') { + fail('canonical authenticated TonAPI origin drifted'); +} +if (count(releaseRegistry, /reviewedProductionSendOrigins\s*=\s*\[\s*canonicalAuthenticatedOrigin\s*\]/g) !== 1) { + fail('reviewed TonAPI send-origin allowlist drifted'); +} +if (!/!bytes\.isEmpty\s*&&\s*bytes\.count\s*<=\s*4096\s*&&\s*bytes\.allSatisfy/.test(releaseRegistry) || + !/\(0x21\s*\.\.\.\s*0x7E\)\.contains\(byte\)/.test(releaseRegistry)) { + fail('TonAPI release credential validation contract drifted'); +} +if (!/var\s+hasReviewedProductionSendCredential:\s*Bool\s*\{[\s\S]{0,180}usesAuthorization\s*&&\s*Self\.isReviewedProductionSendServerURL\(tonAPIURL\)[\s\S]{0,80}\}/.test(releaseRegistry)) { + fail('TonAPI signed-operation credential/origin binding drifted'); +} + +if (count(builder, /static\s+func\s+buildForFeeEstimation\s*\(/g) !== 1) { + fail('Wallet V4R2 unsigned fee-estimation builder drifted'); +} +if (!/func\s+emulateUnsigned\s*\([\s\S]{0,500}emulateMessageToTrace\s*\([\s\S]{0,240}ignore_signature_check:\s*true/.test(service)) { + fail('TonAPI unsigned signature-bypass emulation seam drifted'); +} +if (!/func\s+emulateSigned\s*\([\s\S]{0,500}requireTrustedSignedOperationEndpoint\(\)[\s\S]{0,240}emulateMessageToWallet\s*\(/.test(service)) { + fail('TonAPI signed Wallet V4R2 emulation seam drifted'); +} +if (count(service, /emulation\.totalFeeNanotons\s*!=\s*feeQuote\.feeNanotons/g) !== 2 || + count(service, /throw\s+TonSendServiceError\.feeChangedAfterConfirmation\s*\(/g) !== 2) { + fail('signed-emulation fee drift must fail closed at both initial and retry seams'); +} +NODE + +require_fixed "$DOC" 'Status: **BLOCKED / fail closed**.' "blocked TON readiness status" +require_fixed "$DOC" 'exact `totalFeeNanotons` parity between TonAPI unsigned emulation' "exact unsigned/signed fee parity requirement" +require_fixed "$DOC" 'reviewed and attested local-TVM or quote mechanism' "reviewed attested replacement requirement" +require_fixed "$DOC" 'the Release binary' "Release endpoint provisioning requirement" +require_fixed "$DOC" 'real funded mainnet Wallet V4R2 transfer' "funded mainnet requirement" +require_fixed "$DOC" 'finalized-chain absence' "finalized-chain absence requirement" +require_fixed "$DOC" 'explicit audited quarantine and recovery procedure' "audited quarantine requirement" +require_fixed "$DOC" 'block that sender indefinitely' "current fail-closed recovery behavior" + +require_fixed "$RELEASE_CHECKLIST" \ + '`bash ./scripts/test-ton-production-send-readiness-audit.sh && bash ./scripts/audit-ton-production-send-readiness.sh`' \ + "release-checklist TON readiness command" +require_fixed "$RELEASE_CHECKLIST" \ + '`config/ton-production-send-readiness.json` remains `blocked`' \ + "release-checklist machine-readable blocked status" +require_fixed "$RELEASE_CHECKLIST" \ + 'exact fee parity with the corresponding signed Wallet V4R2' \ + "release-checklist exact fee parity requirement" +require_fixed "$RELEASE_CHECKLIST" \ + 'reviewed attested quote/local-TVM mechanism' \ + "release-checklist reviewed replacement requirement" +require_fixed "$RELEASE_CHECKLIST" \ + 'exact binary-owned send origin, credentials, and registry provenance' \ + "release-checklist endpoint credential requirement" +require_fixed "$RELEASE_CHECKLIST" \ + 'funded mainnet Wallet V4R2 transfer' \ + "release-checklist funded mainnet requirement" +require_fixed "$RELEASE_CHECKLIST" \ + 'finalized-chain absence proof or explicit audited quarantine' \ + "release-checklist finalized-absence or quarantine requirement" + +require_active_line "$RUN_PR" \ + 'bash "$WORKSPACE_DIR/scripts/test-ton-production-send-readiness-audit.sh"' \ + "PR CI TON readiness adversarial self-test" +require_active_line "$RUN_PR" \ + 'bash "$WORKSPACE_DIR/scripts/audit-ton-production-send-readiness.sh"' \ + "PR CI TON readiness audit" +require_active_line "$WORKFLOW" \ + 'bash ./scripts/test-ton-production-send-readiness-audit.sh' \ + "GitHub CI TON readiness adversarial self-test" +require_active_line "$WORKFLOW" \ + 'bash ./scripts/audit-ton-production-send-readiness.sh' \ + "GitHub CI TON readiness audit" + +echo "[ton-send-readiness][ios] blocked contract passed ($actual_manifest_sha256)" diff --git a/scripts/audit-transaction-builder-tests.sh b/scripts/audit-transaction-builder-tests.sh index d9ccb19f0b..aa9c8a7eb9 100755 --- a/scripts/audit-transaction-builder-tests.sh +++ b/scripts/audit-transaction-builder-tests.sh @@ -28,11 +28,65 @@ require_pattern() { fi } +require_multiline_pattern() { + local file="$1" + local pattern="$2" + local description="$3" + if [[ ! -f "$file" ]]; then + return + fi + if ! perl -0ne 'BEGIN { $pattern = shift @ARGV } if (/$pattern/) { $found = 1; last } END { exit($found ? 0 : 1) }' "$pattern" "$file"; then + record_failure "$description missing in $file" + fi +} + +reject_multiline_pattern() { + local file="$1" + local pattern="$2" + local description="$3" + if [[ ! -f "$file" ]]; then + return + fi + if perl -0ne 'BEGIN { $pattern = shift @ARGV } if (/$pattern/) { $found = 1; last } END { exit($found ? 0 : 1) }' "$pattern" "$file"; then + record_failure "$description present in $file" + fi +} + +require_exact_pattern_count() { + local file="$1" + local pattern="$2" + local expected="$3" + local description="$4" + if [[ ! -f "$file" ]]; then + return + fi + local actual + actual="$(grep -Ec -- "$pattern" "$file" || true)" + if [[ "$actual" != "$expected" ]]; then + record_failure "$description expected exactly $expected occurrences in $file, found $actual" + fi +} + bitcoin_tests="$ROOT_DIR/fearlessTests/BitcoinTransactionBuilderTests.swift" solana_tests="$ROOT_DIR/fearlessTests/SolanaTransferTransactionBuilderTests.swift" routing_tests="$ROOT_DIR/fearlessTests/ApplicationLayer/Services/FeatureToggle/TonChainSelectionTests.swift" +ton_tests="$ROOT_DIR/fearlessTests/TonTransferTransactionBuilderTests.swift" +ton_builder="$ROOT_DIR/fearless/Common/Model/TonTransferTransactionBuilder.swift" +ton_send_service="$ROOT_DIR/fearless/Common/Model/TonSendService.swift" +ton_fee_quote="$ROOT_DIR/fearless/Common/Model/TonTransferFeeQuote.swift" +ton_pending_journal="$ROOT_DIR/fearless/Common/Model/TonPendingIntentJournal.swift" +ton_pending_journal_tests="$ROOT_DIR/fearlessTests/TonPendingIntentJournalTests.swift" +chain_model="$ROOT_DIR/fearless/Common/Model/ChainRegistry/ChainModel.swift" +chain_registry="$ROOT_DIR/fearless/Common/Services/ChainRegistry/ChainRegistry.swift" transfer_service="$ROOT_DIR/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift" send_container="$ROOT_DIR/fearless/Modules/Send/SendDependencyContainer.swift" +send_confirm_interactor="$ROOT_DIR/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmInteractor.swift" +send_confirm_protocols="$ROOT_DIR/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmProtocols.swift" +send_confirm_presenter="$ROOT_DIR/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmPresenter.swift" +send_confirm_wireframe="$ROOT_DIR/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmWireframe.swift" +send_confirm_tests="$ROOT_DIR/fearlessTests/Modules/WalletSendConfirm/WalletSendConfirmTests.swift" +number_formatter="$ROOT_DIR/fearless/Common/Extension/NumberFormatter+Default.swift" +release_checklist="$ROOT_DIR/docs/release-checklist.md" require_file "$bitcoin_tests" "Bitcoin transaction builder tests" require_pattern "$bitcoin_tests" 'final class BitcoinTransactionBuilderTests' "Bitcoin transaction builder test class" @@ -49,20 +103,252 @@ require_pattern "$solana_tests" 'testBuildsCanonicalSPLTokenTransferCheckedTrans require_pattern "$solana_tests" 'testBuildsToken2022TransferCheckedWithExtensionExtraAccountsInInstructionOrder' "Solana Token-2022 extra-account ordering test" require_pattern "$solana_tests" 'testBuildsTokenSendWithIdempotentAssociatedTokenAccountCreateThenTransferChecked' "Solana ATA create plus transfer builder test" +require_file "$ton_builder" "TON Wallet V4R2 transaction builder" +require_pattern "$ton_builder" 'struct TonSignedExternalMessage' "TON signed external-message type" +require_pattern "$ton_builder" 'struct TonUnsignedEmulationMessage' "TON unsigned emulation-only type" +require_pattern "$ton_builder" 'static func buildForFeeEstimation' "TON private-key-free fee preview builder" +require_pattern "$ton_builder" 'maximumAddressInputBytes = 128' "TON cheap address-input byte cap" +require_pattern "$ton_builder" 'maximumAmountDigits = 37' "TON cheap builder amount-digit cap" +require_pattern "$ton_builder" 'maximumCommentBytes = 256' "TON cheap comment-input byte cap" +require_pattern "$ton_builder" 'let signingPayloadHashHex: String' "TON signing-payload binding field" +require_pattern "$ton_builder" 'let publicKey: Data' "TON persisted-message public-key binding field" +require_pattern "$ton_builder" 'static func rebuildSignedMessage' "TON fixed-signature canonical restore builder" +require_pattern "$ton_builder" 'static func inspectSignedMessage' "TON strict persisted-BOC inspector" +require_pattern "$ton_builder" 'TonCapturingTransferSigner' "TON signing-payload capture signer" +require_pattern "$ton_builder" 'TonFixedSignatureSigner' "TON fixed-signature restore signer" + +require_file "$ton_fee_quote" "TON exact transfer fee quote" +require_pattern "$ton_fee_quote" 'struct TonTransferFeeQuote: Equatable, Sendable' "TON typed fee quote" +require_pattern "$ton_fee_quote" 'maximumAgeSeconds: UInt64 = 30' "TON 30-second fee quote freshness bound" +require_pattern "$ton_fee_quote" 'fearless\.ton\.fee-quote\.v1\\0' "TON domain-separated fee quote digest" +require_pattern "$ton_fee_quote" 'TonAPIClientFactory\.isReviewedProductionSendServerURL' "TON fee quote reviewed-origin binding" +require_pattern "$ton_fee_quote" 'unsignedMessage\.signingPayloadHashHex' "TON fee quote unsigned signing-payload binding" +require_pattern "$ton_fee_quote" 'SHA256\.hash\(data: unsignedMessage\.boc\)' "TON fee quote exact unsigned-BOC digest" +require_pattern "$ton_fee_quote" 'func validate\(at now: UInt64' "TON fee quote submission-time validation" + +require_file "$ton_pending_journal" "TON durable pending-intent journal" +require_pattern "$ton_pending_journal" 'final class TonKeychainPendingIntentJournal' "TON Keychain pending-intent journal" +require_pattern "$ton_pending_journal" 'jp\.co\.soramitsu\.fearless\.ton\.pending\.v1\.' "TON versioned sender-scoped journal key" +require_pattern "$ton_pending_journal" 'maximumRecordBytes = 48 \* 1024' "TON bounded journal record" +require_pattern "$ton_pending_journal" 'encoder\.outputFormatting = \[\.sortedKeys, \.withoutEscapingSlashes\]' "TON canonical journal JSON" +require_pattern "$ton_pending_journal" 'isValidSignature\(inspection\.signature, for: signingHash\)' "TON persisted signature verification" +require_pattern "$ton_pending_journal" 'rebuildSignedMessage' "TON persisted BOC byte-for-byte rebuild" +require_pattern "$ton_pending_journal" 'guard rebuiltSigned\.boc == signedBoc' "TON persisted BOC exact equality gate" +require_pattern "$ton_pending_journal" 'guard let quote = pending\.feeQuote' "TON journal requires exact quote" +require_pattern "$ton_pending_journal" 'phase = "confirmed"' "TON confirmed journal tombstone phase" +require_pattern "$ton_pending_journal" 'var journalPhaseRank: Int' "TON journal phase ranking" +require_exact_pattern_count "$ton_pending_journal" 'pending\.journalPhaseRank >= existing\.journalPhaseRank' 2 "TON journal phase-monotonic save guards" +require_pattern "$ton_pending_journal" 'func hasSameBearer\(as other: TonPendingSignedIntent\)' "TON journal exact-bearer comparator" +require_exact_pattern_count "$ton_pending_journal" 'pending\.hasSameBearer\(as: existing\)' 2 "TON journal same-bearer overwrite guards" + +require_file "$ton_send_service" "TON fail-closed send coordinator" +require_pattern "$ton_send_service" 'actor TonPendingIntentCoordinator' "TON per-sender pending-intent coordinator" +require_multiline_pattern "$ton_send_service" 'static let shared = TonPendingIntentCoordinator\([[:space:]]*productionJournal: TonKeychainPendingIntentJournal\(\)[[:space:]]*\)' "TON production coordinator Keychain journal binding" +require_pattern "$ton_send_service" 'journal\.load\(senderRaw: identity\.sender\)' "TON journal-first sender recovery" +require_multiline_pattern "$ton_send_service" 'try journal\.save\(pending\)[[:space:]]+statesBySender\[pending\.identity\.sender\] = \.pending' "TON durable reserve before in-memory exposure state" +require_pattern "$ton_send_service" 'try journal\.delete' "TON durable exact-confirmation cleanup" +require_pattern "$ton_send_service" 'func recordConfirmed\(' "TON durable confirmed tombstone write" +require_pattern "$ton_send_service" 'func acknowledgeConfirmed\(' "TON explicit confirmed tombstone acknowledgement" +require_pattern "$ton_send_service" 'case priorIntentConfirmed\(identity: TonTransferIntentIdentity, messageHashHex: String\)' "TON typed prior-confirmed recovery outcome" +require_multiline_pattern "$ton_send_service" 'func acknowledgeConfirmed\([\s\S]{0,900}guard pending\.confirmed,[[:space:]]*pending\.identity == identity,[[:space:]]*pending\.message\.messageHashHex == messageHashHex' "TON exact identity/hash confirmed acknowledgement" +require_multiline_pattern "$ton_send_service" 'func acknowledgeConfirmedTransfer\([\s\S]{0,500}guard let sender = try\? TonSwift\.Address\.parse\(senderAddress\),[[:space:]]*sender\.toRaw\(\) == identity\.sender' "TON friendly sender canonicalization before acknowledgement" +require_pattern "$ton_send_service" 'func requireStoredEndpointForRecovery' "TON stored-origin recovery gate" +require_pattern "$ton_send_service" 'storedOrigin == currentOrigin' "TON exact stored/current recovery-origin equality" +require_multiline_pattern "$ton_send_service" 'pendingCoordinator\.begin\(identity\)[\s\S]{0,1200}let credentials = try signingCredentials\(\)' "TON recovery-before-signing-credentials ordering" +require_multiline_pattern "$ton_send_service" 'if requiresFeeQuote \{[[:space:]]*return try await recoverPendingWithoutRebroadcast\(pending\)' "TON ordinary-submit no pending-BOC autorebroadcast" +require_pattern "$ton_send_service" 'pending = try await pendingCoordinator\.recordConfirmed\(pending\)' "TON tombstone-before-success return" +require_multiline_pattern "$ton_send_service" 'if storedPending\.confirmed \|\|[\s\S]{0,260}retained = storedPending' "TON stale recovery preserves confirmed phase" +require_multiline_pattern "$ton_send_service" 'try journal\.save\(retained\)[[:space:]]*statesBySender\[pending\.identity\.sender\] = \.pending\(retained\)' "TON retained pending durable-save-before-state ordering" +require_pattern "$ton_send_service" 'case broadcastOutcomeUnknown\(messageHashHex: String\)' "TON exact-hash unknown outcome" +require_pattern "$ton_send_service" 'func emulateUnsigned' "TON unsigned preview emulation seam" +require_pattern "$ton_send_service" 'func emulateSigned' "TON signed submission emulation seam" +require_pattern "$ton_send_service" 'func reconcile' "TON on-chain reconciliation seam" +require_pattern "$ton_send_service" 'case productionSendDisabled' "TON Release service fail-closed error" +require_multiline_pattern "$ton_send_service" '(?:^|\n)[ \t]*#if !DEBUG[ \t]*\r?\n(?:[ \t]*//[^\r\n]*\r?\n){0,4}[ \t]*throw TonSendServiceError\.productionSendDisabled[ \t]*\r?\n[ \t]*#else' "TON Release service compile-time fail-closed guard" +require_pattern "$ton_send_service" 'request\.amountNanotons\.utf8\.count <= 19' "TON cheap service amount-input cap" +require_pattern "$ton_send_service" 'comment\.utf8\.count > TonTransferTransactionBuilder\.maximumCommentBytes' "TON cheap pending-intent comment cap" +require_pattern "$ton_send_service" 'case untrustedSignedOperationEndpoint' "TON untrusted signed-operation error" +require_exact_pattern_count "$ton_send_service" 'try requireTrustedSignedOperationEndpoint\(\)' 3 "TON every signed remote operation endpoint guard" +require_multiline_pattern "$ton_send_service" '#if DEBUG[[:space:]]+init\(trustedTestClient client: any APIProtocol\)' "TON DEBUG-only signed-operation test capability" +require_pattern "$ton_send_service" 'func quote\(_ request: TonNativeEstimateRequest\)' "TON exact fee quote API" +require_pattern "$ton_send_service" 'case feeQuoteRequired' "TON fresh unquoted-send rejection" +require_pattern "$ton_send_service" 'requiresFeeQuote: true' "TON application send quote requirement" +require_multiline_pattern "$ton_send_service" '#if DEBUG[[:space:]]+///[^\n]*[[:space:]]+///[^\n]*[[:space:]]+func sendUnquotedForTesting' "TON DEBUG-only unquoted test seam" +require_pattern "$ton_send_service" 'rebuiltUnsigned == feeQuote\.unsignedMessage' "TON exact unsigned-template reproduction" +require_pattern "$ton_send_service" 'walletState == feeQuote\.walletState' "TON quote wallet-state drift rejection" +require_pattern "$ton_send_service" 'signedMessage\.signingPayloadHashHex == feeQuote\.unsignedMessage\.signingPayloadHashHex' "TON signed BOC template binding" +require_pattern "$ton_send_service" 'emulation\.totalFeeNanotons != feeQuote\.feeNanotons' "TON exact signed-emulation fee equality" + +require_file "$ton_tests" "TON transaction builder and send tests" +require_pattern "$ton_tests" 'final class TonTransferTransactionBuilderTests' "TON transaction builder test class" +require_pattern "$ton_tests" 'testBuildsCanonicalWalletV4R2ExternalMessageGoldenVector' "TON canonical Wallet V4R2 golden vector" +require_pattern "$ton_tests" 'testSequenceZeroIncludesStateInitAndProducesStableDeploymentMessage' "TON deployment state-init vector" +require_pattern "$ton_tests" 'testFeeEstimationMessageHasCompileTimeDistinctInvalidSignature' "TON invalid-signature fee preview proof" +require_pattern "$ton_tests" 'testUnquotedPendingNeverRetriesWithoutStoredEndpointBinding' "TON unbound pending no-retry test" +require_pattern "$ton_tests" 'testRetryNeverRebroadcastsAfterReconciliationError' "TON reconciliation-error no-rebroadcast test" +require_pattern "$ton_tests" 'testMessageExpiringDuringEmulationIsNeverBroadcast' "TON pre-broadcast expiry adversarial test" +require_pattern "$ton_tests" 'testTonAPIRemoteClientFailsClosedForMalformedAndMismatchedRawBodies' "TON raw-message intent-binding adversarial test" +require_pattern "$ton_tests" 'testRejectsOversizedTextInputsAtCheapClassifierBounds' "TON cheap builder input-bound adversarial test" +require_pattern "$ton_tests" 'testTonAPIUnsignedEmulationUsesExactTraceEndpointBodyAndSignatureBypassQuery' "TON generated unsigned-emulation contract test" +require_pattern "$ton_tests" 'testTonAPIBroadcastUsesExactPathAndOneFieldBodyAndRejectsNon2xx' "TON generated broadcast contract test" +require_pattern "$ton_tests" 'testTonAPIReconciliationUsesExactMessageHashPathAndConfirmsExactIntent' "TON generated exact-hash reconciliation contract test" +require_pattern "$ton_tests" 'testTonAPIReconciliationTreatsOnly404AsNotFound' "TON 404-only rebroadcast authorization test" +require_pattern "$ton_tests" 'testTonAPIReconciliationFailsClosedForMalformedOrMismatchedTransactions' "TON generated reconciliation fail-closed adversarial test" +require_pattern "$ton_tests" 'testTonAPIWalletStateMapsLiveNullFieldsAndExactAccountSeqnoPaths' "TON generated live-null wallet-state mapping test" +require_pattern "$ton_tests" 'testTonAPIWalletAndMemoMappingRejectsExplicitDangerAndPreservesNullContract' "TON generated account-danger and memo mapping test" +require_pattern "$ton_tests" 'testReleaseSendFailsBeforeAnyValidationOrRemoteWork' "TON Release send fail-closed behavior test" +require_pattern "$ton_tests" 'testBoundQuoteReproducesExactTemplateAndExactFeeBeforeBroadcast' "TON exact quote/template/fee test" +require_pattern "$ton_tests" 'testFreshSendRequiresQuoteBeforeMnemonicDerivationOrRemoteWork' "TON unquoted fresh-send no-work test" +require_pattern "$ton_tests" 'testQuoteMutationExpiryWalletDriftAndFeeDriftAllFailClosed' "TON quote mutation/drift adversarial test" +require_pattern "$ton_tests" 'testProcessRestartLoadsJournalAndReconcilesBeforeAnyRebroadcast' "TON process-restart reconcile-first test" +require_pattern "$ton_tests" 'testJournalWriteFailurePreventsEverySignedRemoteExposure' "TON journal-write zero-exposure test" +require_pattern "$ton_tests" 'testFreshDisplayedQuoteNeverRebroadcastsOlderPendingBoc' "TON fresh-quote old-BOC no-rebroadcast test" +require_pattern "$ton_tests" 'testPersistedRecoveryRequiresStoredCredentialedEndpointBeforeRemoteWork' "TON persisted endpoint mismatch zero-work test" +require_pattern "$ton_tests" 'testPersistedRecoveryRunsBeforeSigningCredentialsAreRequested' "TON mnemonic-free persisted recovery test" +require_multiline_pattern "$ton_tests" 'stalePending\.confirmed = false[\s\S]{0,260}XCTAssertThrowsError\(try journal\.save\(stalePending\)\)[\s\S]{0,700}retainPending\(stalePending\)[\s\S]{0,260}XCTAssertTrue\([^\n]*\.confirmed\)' "TON deterministic confirmed-phase downgrade regression" +require_multiline_pattern "$ton_tests" 'let friendlySender[\s\S]{0,240}\.toFriendly\(\)[\s\S]{0,300}acknowledgeConfirmedTransfer\([\s\S]{0,240}senderAddress: friendlySender' "TON friendly-address acknowledgement regression" + +require_file "$ton_pending_journal_tests" "TON durable journal tests" +require_pattern "$ton_pending_journal_tests" 'testVersionedJournalRoundTripRebuildsExactBearerWithoutSecrets' "TON journal exact round-trip/no-secret test" +require_pattern "$ton_pending_journal_tests" 'testCodecRejectsNoncanonicalCorruptAndCrossSenderRecords' "TON journal corruption/cross-sender adversarial test" +require_pattern "$ton_pending_journal_tests" 'testJournalConflictAndKeychainFailuresFailClosed' "TON journal conflict/Keychain failure test" +require_pattern "$ton_pending_journal_tests" 'maximumRecordBytes \+ 1' "TON oversized journal-record adversarial fixture" +require_pattern "$ton_pending_journal_tests" 'AlwaysFailingKeystore' "TON unavailable Keychain adversarial fixture" +require_pattern "$ton_pending_journal_tests" '0eb547b83019bdb5d66d62e35bc31053c00cbd68ba80599e9af1ccddd17e6958' "TON quote-ID known-answer vector" +require_pattern "$ton_pending_journal_tests" '3238980e4f69c2fdd20fa2666a713771b4bec2ee4154255f154f827679a05067' "TON journal canonical SHA-256 vector" +require_pattern "$ton_pending_journal_tests" 'replacingWithStructurallyValidInvalidSignature' "TON structurally valid invalid-signature journal attack" + +require_file "$chain_model" "TON canonical chain classifier" +require_pattern "$chain_model" '"-239"' "TON canonical mainnet chain-id classifier" +require_pattern "$chain_model" '"ton:mainnet"' "TON canonical CAIP-like mainnet classifier" + +require_file "$chain_registry" "TON bounded authenticated transport" +require_pattern "$chain_registry" 'maximumResponseBytes' "TON response byte limit" +require_pattern "$chain_registry" 'withTaskCancellationHandler' "TON transport cancellation propagation" +require_pattern "$chain_registry" 'completionHandler\(nil\)' "TON redirect rejection" +require_pattern "$chain_registry" 'static let reviewedProductionSendOrigins = \[canonicalAuthenticatedOrigin\]' "TON binary-owned production endpoint allowlist" +require_pattern "$chain_registry" 'guard TonAPIClientFactory\.isReviewedProductionSendServerURL\(baseURL\)' "TON send-only exact-origin factory guard" +require_pattern "$chain_registry" 'static func isValidAuthorizationToken' "TON bounded visible-ASCII credential validator" +require_pattern "$chain_registry" '\(0x21 \.\.\. 0x7E\)\.contains\(byte\)' "TON credential control/whitespace rejection" +require_pattern "$chain_registry" 'hasReviewedProductionSendCredential' "TON reviewed credential capability" + require_file "$transfer_service" "Iroha transfer service signer seam" require_pattern "$transfer_service" 'protocol IrohaTransferSigning' "Iroha transfer signer seam" require_pattern "$transfer_service" 'struct IrohaTransferSigningRequest' "Iroha transfer signing request contract" require_pattern "$transfer_service" 'struct IrohaSignedTransfer' "Iroha signed transfer contract" +require_pattern "$transfer_service" 'case tonProductionSendDisabled' "TON Release integration fail-closed error" +require_multiline_pattern "$transfer_service" '(?:^|\n)[ \t]*#if !DEBUG[ \t]*\r?\n(?:[ \t]*//[^\r\n]*\r?\n){0,4}[ \t]*throw TransferServiceError\.tonProductionSendDisabled[ \t]*\r?\n[ \t]*#else' "TON Release integration compile-time fail-closed guard" +require_pattern "$transfer_service" 'private final class TonTransferFeeQuoteStore: @unchecked Sendable' "TON synchronous transfer-service quote store" +require_pattern "$transfer_service" 'private let lock = NSLock\(\)' "TON quote-store synchronization" +require_pattern "$transfer_service" 'generation = generation == UInt64\.max \? 1 : generation \+ 1' "TON stale asynchronous quote generation guard" +require_pattern "$transfer_service" 'feeQuoteStore\.consume\(identity: identity\)' "TON atomic one-use quote consumption" +require_pattern "$transfer_service" 'sendService\.quote\(estimateRequest\)' "TON displayed fee sourced from exact quote" +require_pattern "$transfer_service" 'feeQuote: feeQuote' "TON exact quote propagation into send service" +require_pattern "$transfer_service" 'quote\.quoteIDHex == presentationID' "TON exact quote-ID presentation activation" +require_multiline_pattern "$transfer_service" 'func unsubscribe\(\) \{[[:space:]]*feeQuoteStore\.invalidate\(\)[[:space:]]*feeTask\?\.cancel\(\)' "TON synchronous unsubscribe quote revocation" +require_exact_pattern_count "$transfer_service" 'readyForSubmission: false' 2 "TON every fee-estimate path stages a non-submittable quote" +reject_multiline_pattern "$transfer_service" 'prepareFee\([\s\S]{0,180}readyForSubmission: true' "TON submittable fee-estimate path" +require_pattern "$transfer_service" 'case tonPriorTransferConfirmed\(identity: TonTransferIntentIdentity, messageHashHex: String\)' "TON typed integration prior-confirmed outcome" +require_multiline_pattern "$transfer_service" 'if case let \.priorIntentConfirmed\(identity, messageHashHex\) = error \{[\s\S]{0,300}throw TransferServiceError\.tonPriorTransferConfirmed\([[:space:]]*identity: identity,[[:space:]]*messageHashHex: messageHashHex' "TON prior-confirmed service-to-integration mapping" require_file "$routing_tests" "Universal wallet send routing tests" require_pattern "$routing_tests" 'testIrohaTransferServiceBuildsSignerRequestAndSubmitsNorito' "Iroha signer request and Torii submission test" require_pattern "$routing_tests" 'testIrohaTransferServiceDefaultSignerFailsClosedAfterValidation' "Iroha default signer fail-closed test" require_pattern "$routing_tests" 'testIrohaTransferServiceRejectsMnemonicMismatchBeforeSignerOrToriiCalls' "Iroha mnemonic mismatch adversarial test" -require_pattern "$routing_tests" 'testPrepareDependenciesFailsClosedForTonCompatibilityTransferBeforeSubstrateRouting' "TON transfer fail-closed routing test" +require_pattern "$routing_tests" 'testProductionTonSendPolicyIsImmutableDisabledAndCannotBeBypassedByInjection' "TON immutable production-disable routing test" +require_pattern "$routing_tests" 'testTonCompatibilityChainDetectionByCanonicalIdWithoutMetadataHints' "TON canonical-id metadata-independent classifier test" +require_pattern "$routing_tests" 'testProductionTonSendPolicyRejectsCanonicalIdWithoutTonMetadataHints' "TON canonical-id production-disable routing test" +require_pattern "$routing_tests" 'testTonFeeEstimateNeverRequestsMnemonicOrProducesSignedEmulation' "TON mnemonic-free fee estimate test" +require_pattern "$routing_tests" 'testReleaseTonTransferSubmitFailsBeforeMnemonicOrRemoteWork' "TON Release integration no-secret/no-remote behavior test" +require_pattern "$routing_tests" 'testTonTransferServicePreservesExactUnknownOutcomeHash' "TON typed unknown-outcome propagation test" +require_pattern "$routing_tests" 'testCancellingTonApiTransportCancelsSessionTaskAndClearsState' "TON transport cancellation adversarial test" +require_pattern "$routing_tests" 'testHostileTonApiOriginCannotReceiveSignedOperations' "TON hostile-origin zero-request signed-operation test" +require_pattern "$routing_tests" 'testCanonicalTonApiFactoryIsRequiredForSignedOperations' "TON canonical-factory signed-operation capability test" +require_pattern "$routing_tests" 'testPrepareDependenciesCachesOneTonServiceAcrossConcurrentEstimateAndSubmit' "TON concurrent dependency-cache integration test" +require_pattern "$routing_tests" 'testTonQuoteCannotSubmitUntilExactPresentationIsAcknowledged' "TON undisplayed-quote zero-submit test" +require_pattern "$routing_tests" 'testTonUnsubscribeSynchronouslyRevokesQuoteBeforeImmediateSubmit' "TON immediate unsubscribe revocation test" +require_pattern "$routing_tests" 'testTonRestartRecoverySucceedsWithoutMnemonicAndWithoutNewBearer' "TON integration restart-without-mnemonic test" +require_pattern "$routing_tests" 'testCanonicalTonApiOriginStillRejectsMissingOrMalformedCredentialsForSignedOperations' "TON malformed credential zero-request test" +require_pattern "$routing_tests" 'testTonDirectEstimateNeverAuthorizesSubmission' "TON direct-estimate zero-submit regression" +require_pattern "$routing_tests" 'testTonDifferentIntentRecoveryNeverReturnsThePriorHashAsNewSuccess' "TON different-intent prior-confirmed integration regression" +require_pattern "$routing_tests" 'testTonFeePaymentAssetIgnoresAdditionalUnorderedUtilityAssets' "TON unordered utility-asset registry-confusion regression" + +require_file "$send_confirm_presenter" "TON unknown-outcome recovery presenter" +require_pattern "$send_confirm_presenter" 'struct TonUnknownOutcomeRecoveryModel' "TON unknown-outcome recovery model" +require_pattern "$send_confirm_presenter" 'messageHashHex\.utf8\.count == 64' "TON unknown-outcome exact lowercase hash validation" +require_pattern "$send_confirm_presenter" 'TransferServiceError\.tonBroadcastOutcomeUnknown\(messageHashHex\)' "TON unknown-outcome dedicated presenter routing" +require_pattern "$send_confirm_presenter" 'key: "ton\.transfer\.unknown\.message"' "TON unknown-outcome no-resend recovery message" +require_pattern "$send_confirm_presenter" 'UIPasteboard\.general\.string = recovery\.messageHashHex' "TON unknown-outcome exact-hash copy action" +require_pattern "$send_confirm_presenter" 'TransferServiceError\.tonPriorTransferConfirmed\(identity, messageHashHex\)' "TON prior-confirmed presenter routing" +require_pattern "$send_confirm_presenter" 'presentTonPriorTransferConfirmed' "TON prior-confirmed acknowledgement UX" +require_pattern "$send_confirm_presenter" 'key: "ton\.transfer\.prior_confirmed\.acknowledge"' "TON prior-confirmed explicit acknowledgement action" +require_multiline_pattern "$send_confirm_presenter" 'chainAsset\.chain\.isTonCompatibilityChain[[:space:]]*\?[[:space:]]*\.exactCrypto\(fractionDigits: Int\(utilityAsset\.asset\.precision\)\)[[:space:]]*:[[:space:]]*\.detailsCrypto' "TON exact precision fee rendering" +require_multiline_pattern "$send_confirm_presenter" 'guard let completionWireframe = wireframe as\? WalletSendConfirmCompletionPresenting else \{[^}]*wireframe\.complete\([\s\S]{0,260}return[[:space:]]*\}' "TON no-callback success path retains confirmed tombstone" +reject_multiline_pattern "$send_confirm_presenter" 'guard let completionWireframe = wireframe as\? WalletSendConfirmCompletionPresenting else \{[^}]*acknowledgeSubmittedTransfer' "TON no-callback success path premature acknowledgement" +require_multiline_pattern "$send_confirm_presenter" 'completionWireframe\.completeAfterPresentation\([\s\S]{0,400}\) \{ \[weak self\] in[\s\S]{0,260}self\?\.interactor\.acknowledgeSubmittedTransfer' "TON success receipt callback-before-acknowledgement ordering" + +require_file "$send_confirm_tests" "TON unknown-outcome recovery presenter tests" +require_pattern "$send_confirm_tests" 'testTonUnknownOutcomeRecoveryAcceptsExactLowercaseHash' "TON unknown-outcome exact-hash recovery test" +require_pattern "$send_confirm_tests" 'testTonUnknownOutcomeRecoveryRejectsMalformedOrDisplaySpoofedHashes' "TON unknown-outcome display-spoof adversarial test" +require_pattern "$send_confirm_tests" 'testTonExactFeeFormatterPreservesTheNinthDecimalAcrossLocales' "TON ninth-decimal locale rendering regression" +require_pattern "$send_confirm_tests" '\.exactCrypto\(fractionDigits: 9\)' "TON nine-decimal formatter test configuration" + +require_file "$send_confirm_protocols" "TON visible-receipt completion protocol" +require_pattern "$send_confirm_protocols" 'protocol WalletSendConfirmCompletionPresenting: AnyObject' "TON visible-receipt completion capability" +require_pattern "$send_confirm_protocols" 'func completeAfterPresentation\(' "TON post-presentation completion callback contract" + +require_file "$send_confirm_wireframe" "TON visible-receipt completion wireframe" +require_pattern "$send_confirm_wireframe" 'extension WalletSendConfirmWireframe: WalletSendConfirmCompletionPresenting' "TON production visible-receipt wireframe capability" +require_multiline_pattern "$send_confirm_wireframe" 'func completeAfterPresentation\([\s\S]{0,500}presentationCompletion: completion' "TON production presentation callback propagation" + +require_file "$number_formatter" "TON exact crypto number formatter" +require_pattern "$number_formatter" 'case exactCrypto\(fractionDigits: Int\)' "TON exact crypto formatter usage case" +require_multiline_pattern "$number_formatter" 'case let \.exactCrypto\(fractionDigits\):[[:space:]]*return NumberFormatter\.defaultExactCryptoFormatter\(' "TON exact crypto formatter dispatch" + +for locale in en id ja pt-PT pt ru tr vi zh-Hans; do + localization="$ROOT_DIR/fearless/$locale.lproj/Localizable.strings" + require_file "$localization" "TON unknown-outcome $locale localization" + require_pattern "$localization" '^"ton\.transfer\.unknown\.title"' "TON unknown-outcome $locale title" + require_pattern "$localization" '^"ton\.transfer\.unknown\.message".*%@' "TON unknown-outcome $locale exact-hash recovery message" + require_pattern "$localization" '^"ton\.transfer\.unknown\.copy_hash"' "TON unknown-outcome $locale copy action" + require_pattern "$localization" '^"ton\.transfer\.prior_confirmed\.title"' "TON prior-confirmed $locale title" + require_pattern "$localization" '^"ton\.transfer\.prior_confirmed\.message".*%@' "TON prior-confirmed $locale exact-hash message" + require_pattern "$localization" '^"ton\.transfer\.prior_confirmed\.acknowledge"' "TON prior-confirmed $locale acknowledgement action" +done require_file "$send_container" "Send dependency container" -require_pattern "$send_container" 'isTonCompatibilityChain' "TON compatibility transfer guard" -require_pattern "$send_container" 'TON transfer not yet supported\.' "TON transfer explicit fail-closed error" +require_pattern "$send_container" 'static let production = TonProductionSendReleasePolicy\(isEnabled: false\)' "TON hard-disabled production release policy" +require_pattern "$send_container" 'private init\(isEnabled: Bool\)' "TON private release-policy initializer" +require_multiline_pattern "$send_container" '#if DEBUG[[:space:]]+static let enabledForTests = TonProductionSendReleasePolicy\(isEnabled: true\)[[:space:]]+#endif' "TON DEBUG-enclosed enablement policy" +require_pattern "$send_container" 'releasePolicy: \.production' "TON production initializer policy binding" +require_pattern "$send_container" 'tonProductionSendDisabled' "TON production fail-closed error" +require_pattern "$send_container" '@MainActor' "Send dependency cache main-actor serialization" +require_pattern "$send_container" 'cachedDependencies\[dependenciesKey\] = dependencies' "Send dependency cache population" +require_pattern "$send_container" 'currentDependenciesKey = dependenciesKey' "Send dependency active-key tracking" + +require_file "$send_confirm_interactor" "TON quote-presentation interactor" +require_pattern "$send_confirm_interactor" 'TonTransferFeePresentationListener' "TON opaque quote-presentation callback" +require_pattern "$send_confirm_interactor" 'confirmFeePresentation' "TON rendered-fee acknowledgement bridge" +require_pattern "$send_confirm_interactor" 'acknowledgeSubmittedTransfer' "TON confirmed-receipt acknowledgement bridge" +require_multiline_pattern "$send_confirm_interactor" 'static func resolveFeePaymentChainAsset\(for chainAsset: ChainAsset\?\) -> ChainAsset\? \{[\s\S]{0,500}if chainAsset\.chain\.isTonCompatibilityChain \{[[:space:]]*return chainAsset[[:space:]]*\}[\s\S]{0,300}chainAsset\.chain\.utilityAssets\(\)\.first' "TON selected native fee asset pinned before unordered registry fallback" + +require_pattern "$send_confirm_presenter" 'chainAsset\.chain\.isTonCompatibilityChain \? nil : feeViewModel' "TON stale incoming fee discarded" +require_pattern "$send_confirm_presenter" 'confirmPendingTonFeePresentationIfNeeded' "TON post-render fee acknowledgement" +require_multiline_pattern "$send_confirm_presenter" 'view\?\.didReceive\(state: \.loaded\(viewModel\)\)[[:space:]]*confirmPendingTonFeePresentationIfNeeded\(\)' "TON view-before-quote activation ordering" +require_pattern "$send_confirm_presenter" '!chainAsset\.chain\.isTonCompatibilityChain \|\| loadingCollector\.isReady' "TON confirmation readiness guard" + +require_file "$release_checklist" "iOS release checklist" +require_pattern "$release_checklist" 'pending intents survive process' "TON durable restart-recovery release blocker" +require_pattern "$release_checklist" 'confirmation binds a fresh 30-second fee quote' "TON exact fee-quote binding release blocker" +require_pattern "$release_checklist" 'versioned, bounded, canonical Keychain journal' "TON implemented durable journal release truth" +require_pattern "$release_checklist" 'fresh 30-second fee quote' "TON implemented exact quote freshness truth" +require_pattern "$release_checklist" 'exact-hash unknown outcomes have explicit user-visible recovery UX' "TON unknown-outcome UX release blocker" +require_pattern "$release_checklist" 'immutable reviewed endpoint allowlist' "TON trusted endpoint provenance release blocker" +require_pattern "$release_checklist" 'current TonAPI schemas pass the bounded transport' "TON current-schema release blocker" +require_pattern "$release_checklist" 'exact fee parity with the corresponding signed Wallet V4R2' "TON unsigned/signed fee-parity release blocker" +require_pattern "$release_checklist" 'funded mainnet Wallet V4R2 transfer' "TON funded-mainnet evidence release blocker" +require_multiline_pattern "$release_checklist" 'confirmed terminal tombstone remains in[[:space:]]+the journal' "TON confirmed tombstone release truth" +require_pattern "$release_checklist" 'finalized-chain absence proof' "TON expired-pending recovery blocker" if ((${#failures[@]} > 0)); then echo "[ios-transaction-builder-audit][error] iOS transaction-builder coverage audit failed:" >&2 diff --git a/scripts/ci/run-pr.sh b/scripts/ci/run-pr.sh index bd04cde02f..826434003c 100755 --- a/scripts/ci/run-pr.sh +++ b/scripts/ci/run-pr.sh @@ -12,6 +12,14 @@ if [[ -f "$WORKSPACE_DIR/scripts/audit-branch-flow.sh" ]]; then bash "$WORKSPACE_DIR/scripts/test-branch-flow-audit.sh" fi +if [[ -f "$WORKSPACE_DIR/scripts/audit-release-signing-gate.sh" ]]; then + echo "[run-pr] Running release signing gate audit" + bash "$WORKSPACE_DIR/scripts/test-release-signing-gate-audit.sh" + bash "$WORKSPACE_DIR/scripts/test-release-signing-gate.sh" + bash "$WORKSPACE_DIR/scripts/test-release-signing-bootstrap.sh" + bash "$WORKSPACE_DIR/scripts/audit-release-signing-gate.sh" +fi + if [[ -f "$WORKSPACE_DIR/scripts/audit-public-artifacts.sh" ]]; then echo "[run-pr] Running public artifact audit" bash "$WORKSPACE_DIR/scripts/audit-public-artifacts.sh" @@ -33,8 +41,21 @@ if [[ -f "$WORKSPACE_DIR/scripts/audit-transaction-builder-tests.sh" ]]; then bash "$WORKSPACE_DIR/scripts/audit-transaction-builder-tests.sh" fi +if [[ -f "$WORKSPACE_DIR/scripts/test-coredata-release-gate.sh" ]]; then + echo "[run-pr] Testing Core Data Release gate contract" + bash "$WORKSPACE_DIR/scripts/test-coredata-release-gate.sh" +fi + +if [[ -f "$WORKSPACE_DIR/scripts/audit-ton-production-send-readiness.sh" ]]; then + echo "[run-pr] Checking blocked TON production-send evidence contract" + bash "$WORKSPACE_DIR/scripts/test-ton-production-send-readiness-audit.sh" + bash "$WORKSPACE_DIR/scripts/audit-ton-production-send-readiness.sh" +fi + if [[ -f "$WORKSPACE_DIR/scripts/check-iroha-mobile-sdk-release-assets.sh" ]]; then echo "[run-pr] Checking Iroha mobile SDK release asset contract" + bash "$WORKSPACE_DIR/scripts/test-iroha-production-send-readiness-audit.sh" + bash "$WORKSPACE_DIR/scripts/audit-iroha-production-send-readiness.sh" bash "$WORKSPACE_DIR/scripts/check-iroha-mobile-sdk-release-assets.sh" --self-test if [[ -n "${IROHA_MOBILE_SDK_RELEASE_TAG:-}" ]]; then bash "$WORKSPACE_DIR/scripts/check-iroha-mobile-sdk-release-assets.sh" --download --tag "$IROHA_MOBILE_SDK_RELEASE_TAG" @@ -109,6 +130,12 @@ if xcodebuild -workspace "$WORKSPACE_DIR/fearless.xcworkspace" \ -clonedSourcePackagesDirPath "$SP_DIR" \ -disableAutomaticPackageResolution \ test + + echo "[run-pr] Running optimized Core Data migration/startup gate" + FEARLESS_CORE_DATA_SOURCE_PACKAGES_DIR="$SP_DIR" \ + bash "$WORKSPACE_DIR/scripts/ci/run-coredata-release-gate.sh" \ + --stage core \ + --simulator-udid "$SIM_UDID" else echo "[run-pr] Simulator build failed; falling back to device build (signing disabled)" xcodebuild -workspace "$WORKSPACE_DIR/fearless.xcworkspace" \ diff --git a/scripts/ci/run-release-signing-ci.sh b/scripts/ci/run-release-signing-ci.sh new file mode 100755 index 0000000000..71c52facf1 --- /dev/null +++ b/scripts/ci/run-release-signing-ci.sh @@ -0,0 +1,169 @@ +#!/usr/bin/env bash +set -euo pipefail + +PREFIX="[release-signing-bootstrap]" +ROOT_DIR="${IOS_RELEASE_ROOT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" +GATE="${IOS_RELEASE_GATE_BIN:-$ROOT_DIR/scripts/ci/run-release-signing-gate.sh}" +SECURITY_BIN="${IOS_RELEASE_SECURITY_BIN:-/usr/bin/security}" +PLUTIL_BIN="${IOS_RELEASE_PLUTIL_BIN:-/usr/bin/plutil}" +BASE64_BIN="${IOS_RELEASE_BASE64_BIN:-/usr/bin/base64}" +OPENSSL_BIN="${IOS_RELEASE_OPENSSL_BIN:-/usr/bin/openssl}" +PROFILE_INSTALL_DIR="${IOS_RELEASE_PROFILE_INSTALL_DIR:-$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles}" + +fail() { + echo "$PREFIX ERROR: $*" >&2 + exit 1 +} + +[[ "${GITHUB_ACTIONS:-false}" == "true" ]] || fail \ + "The signing bootstrap is reserved for trusted GitHub Actions pushes." +[[ "${GITHUB_EVENT_NAME:-}" == "push" ]] || fail \ + "The signing bootstrap refuses non-push events before reading signing material." +case "${GITHUB_REF:-}" in + refs/heads/develop|refs/heads/master) ;; + *) fail "The signing bootstrap refuses branches outside the release allow-list." ;; +esac +[[ "${IOS_RELEASE_SIGNING_REQUIRED:-}" == "1" ]] || fail \ + "Trusted release CI must set IOS_RELEASE_SIGNING_REQUIRED to literal 1." + +certificate_base64="${IOS_RELEASE_CERTIFICATE_P12_BASE64:-}" +certificate_password="${IOS_RELEASE_CERTIFICATE_PASSWORD:-}" +profile_base64="${IOS_RELEASE_PROVISIONING_PROFILE_BASE64:-}" +configured=0 +[[ -n "$certificate_base64" ]] && configured=$((configured + 1)) +[[ -n "$certificate_password" ]] && configured=$((configured + 1)) +[[ -n "$profile_base64" ]] && configured=$((configured + 1)) +if ((configured == 0)); then + fail "Release-signing secrets are missing; certificate, password, and provisioning profile are all required." +fi +if ((configured != 3)); then + fail "Release-signing secrets are only partially configured; certificate, password, and provisioning profile must be supplied together." +fi + +validate_base64_secret() { + local value="$1" + local maximum_length="$2" + local label="$3" + local length=${#value} + ((length >= 4 && length <= maximum_length && length % 4 == 0)) || fail \ + "$label is not a bounded canonical base64 value." + [[ "$value" =~ ^[A-Za-z0-9+/]+={0,2}$ ]] || fail \ + "$label is not a bounded canonical base64 value." +} + +validate_base64_secret "$certificate_base64" $((24 * 1024 * 1024)) "The release certificate" +validate_base64_secret "$profile_base64" $((4 * 1024 * 1024)) "The provisioning profile" +[[ ${#certificate_password} -le 1024 && "$certificate_password" != *$'\n'* && "$certificate_password" != *$'\r'* ]] || fail \ + "The release-certificate password is malformed or oversized." + +for command_path in "$GATE" "$SECURITY_BIN" "$PLUTIL_BIN" "$BASE64_BIN" "$OPENSSL_BIN"; do + [[ -x "$command_path" ]] || fail "A required signing-bootstrap command is unavailable." +done + +umask 077 +temporary_root="$(mktemp -d "${RUNNER_TEMP:-${TMPDIR:-/tmp}}/fearless-signing-bootstrap.XXXXXX")" +chmod 700 "$temporary_root" +certificate_file="$temporary_root/release-certificate.p12" +profile_file="$temporary_root/release.mobileprovision" +profile_plist="$temporary_root/profile.plist" +keychain_path="$temporary_root/release-signing.keychain-db" +installed_profile="" +keychain_created=false +search_list_changed=false +original_keychains=() + +cleanup() { + local original_status=$? + local cleanup_failed=false + trap - EXIT INT TERM + set +e + if [[ "$search_list_changed" == "true" ]]; then + "$SECURITY_BIN" list-keychains -d user -s "${original_keychains[@]}" >/dev/null 2>&1 || cleanup_failed=true + fi + if [[ "$keychain_created" == "true" ]]; then + "$SECURITY_BIN" delete-keychain "$keychain_path" >/dev/null 2>&1 || cleanup_failed=true + fi + if [[ -n "$installed_profile" ]]; then + rm -f -- "$installed_profile" || cleanup_failed=true + fi + rm -rf -- "$temporary_root" || cleanup_failed=true + if [[ "$cleanup_failed" == "true" && "$original_status" -eq 0 ]]; then + echo "$PREFIX ERROR: Ephemeral signing-material cleanup failed." >&2 + original_status=1 + fi + exit "$original_status" +} +trap cleanup EXIT +trap 'exit 130' INT +trap 'exit 143' TERM + +printf '%s' "$certificate_base64" | "$BASE64_BIN" -D >"$certificate_file" || fail \ + "The release certificate could not be decoded." +printf '%s' "$profile_base64" | "$BASE64_BIN" -D >"$profile_file" || fail \ + "The provisioning profile could not be decoded." +unset certificate_base64 profile_base64 IOS_RELEASE_CERTIFICATE_P12_BASE64 IOS_RELEASE_PROVISIONING_PROFILE_BASE64 + +certificate_bytes="$(wc -c <"$certificate_file" | tr -d '[:space:]')" +profile_bytes="$(wc -c <"$profile_file" | tr -d '[:space:]')" +[[ "$certificate_bytes" =~ ^[0-9]+$ && "$certificate_bytes" -ge 1 && "$certificate_bytes" -le $((16 * 1024 * 1024)) ]] || fail \ + "The decoded release certificate is empty or oversized." +[[ "$profile_bytes" =~ ^[0-9]+$ && "$profile_bytes" -ge 1 && "$profile_bytes" -le $((3 * 1024 * 1024)) ]] || fail \ + "The decoded provisioning profile is empty or oversized." +chmod 600 "$certificate_file" "$profile_file" + +keychain_password="$($OPENSSL_BIN rand -hex 32 2>/dev/null)" +[[ "$keychain_password" =~ ^[0-9a-f]{64}$ ]] || fail \ + "A private ephemeral-keychain password could not be generated." + +while IFS= read -r keychain_line; do + keychain_line="${keychain_line#"${keychain_line%%[![:space:]]*}"}" + keychain_line="${keychain_line#\"}" + keychain_line="${keychain_line%\"}" + [[ -n "$keychain_line" ]] && original_keychains+=("$keychain_line") +done < <("$SECURITY_BIN" list-keychains -d user 2>/dev/null) + +"$SECURITY_BIN" create-keychain -p "$keychain_password" "$keychain_path" >/dev/null 2>&1 || fail \ + "The ephemeral signing keychain could not be created." +keychain_created=true +"$SECURITY_BIN" set-keychain-settings -lut 21600 "$keychain_path" >/dev/null 2>&1 || fail \ + "The ephemeral signing keychain could not be constrained." +"$SECURITY_BIN" unlock-keychain -p "$keychain_password" "$keychain_path" >/dev/null 2>&1 || fail \ + "The ephemeral signing keychain could not be unlocked." +"$SECURITY_BIN" import "$certificate_file" -k "$keychain_path" -P "$certificate_password" \ + -T /usr/bin/codesign -T /usr/bin/security >/dev/null 2>&1 || fail \ + "The distribution certificate could not be imported." +unset certificate_password IOS_RELEASE_CERTIFICATE_PASSWORD +"$SECURITY_BIN" set-key-partition-list -S apple-tool:,apple:,codesign: -s \ + -k "$keychain_password" "$keychain_path" >/dev/null 2>&1 || fail \ + "The ephemeral signing keychain could not authorize codesign." +unset keychain_password +"$SECURITY_BIN" list-keychains -d user -s "$keychain_path" "${original_keychains[@]}" >/dev/null 2>&1 || fail \ + "The ephemeral signing keychain could not be added to the user search list." +search_list_changed=true + +"$SECURITY_BIN" cms -D -i "$profile_file" >"$profile_plist" 2>/dev/null || fail \ + "The provisioning profile could not be decoded." +chmod 600 "$profile_plist" +profile_uuid="$($PLUTIL_BIN -extract UUID raw -o - "$profile_plist" 2>/dev/null || true)" +profile_name="$($PLUTIL_BIN -extract Name raw -o - "$profile_plist" 2>/dev/null || true)" +[[ "$profile_uuid" =~ ^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$ ]] || fail \ + "The provisioning profile UUID is missing or malformed." +[[ -n "$profile_name" && ${#profile_name} -le 256 && "$profile_name" != *$'\n'* && "$profile_name" != *$'\r'* ]] || fail \ + "The provisioning profile name is missing or malformed." + +mkdir -p "$PROFILE_INSTALL_DIR" +installed_profile="$PROFILE_INSTALL_DIR/$profile_uuid.mobileprovision" +[[ ! -e "$installed_profile" && ! -L "$installed_profile" ]] || fail \ + "The ephemeral provisioning-profile destination already exists." +/usr/bin/install -m 600 "$profile_file" "$installed_profile" || fail \ + "The ephemeral provisioning profile could not be installed." + +IOS_RELEASE_SIGNING_REQUIRED=1 \ +IOS_RELEASE_PROFILE_DIRS="$PROFILE_INSTALL_DIR" \ +IOS_RELEASE_EXPECTED_PROFILE_UUID="$profile_uuid" \ +IOS_RELEASE_PROVISIONING_PROFILE_SPECIFIER="$profile_name" \ +IOS_RELEASE_SECURITY_BIN="$SECURITY_BIN" \ +IOS_RELEASE_PLUTIL_BIN="$PLUTIL_BIN" \ + "$GATE" + +echo "$PREFIX PASSED: trusted-push signing material was used ephemerally and will be removed." diff --git a/scripts/ci/run-release-signing-gate.sh b/scripts/ci/run-release-signing-gate.sh new file mode 100755 index 0000000000..357d3f264e --- /dev/null +++ b/scripts/ci/run-release-signing-gate.sh @@ -0,0 +1,322 @@ +#!/usr/bin/env bash +set -euo pipefail + +PREFIX="[release-signing-gate]" +ROOT_DIR="${IOS_RELEASE_ROOT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" +WORKSPACE="${IOS_RELEASE_WORKSPACE:-$ROOT_DIR/fearless.xcworkspace}" +SCHEME="${IOS_RELEASE_SCHEME:-fearless}" +SOURCE_PACKAGES_DIR="${IOS_RELEASE_SOURCE_PACKAGES_DIR:-$ROOT_DIR/SourcePackages}" +EXPECTED_TEAM_ID="YLWWUD25VZ" +EXPECTED_BUNDLE_ID="jp.co.soramitsu.fearlesswallet" +EXPECTED_APPLICATION_ID="$EXPECTED_TEAM_ID.$EXPECTED_BUNDLE_ID" +EXPECTED_ICLOUD_CONTAINER="iCloud.$EXPECTED_BUNDLE_ID" +EXPECTED_APP_GROUP="group.$EXPECTED_BUNDLE_ID" +SECURITY_BIN="${IOS_RELEASE_SECURITY_BIN:-/usr/bin/security}" +XCODEBUILD_BIN="${IOS_RELEASE_XCODEBUILD_BIN:-$(xcrun --find xcodebuild 2>/dev/null || true)}" +CODESIGN_BIN="${IOS_RELEASE_CODESIGN_BIN:-/usr/bin/codesign}" +PLUTIL_BIN="${IOS_RELEASE_PLUTIL_BIN:-/usr/bin/plutil}" +SIGNING_REQUIRED="${IOS_RELEASE_SIGNING_REQUIRED:-0}" +PROFILE_DIRS="${IOS_RELEASE_PROFILE_DIRS:-$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles:$HOME/Library/MobileDevice/Provisioning Profiles}" +EXPECTED_PROFILE_UUID="${IOS_RELEASE_EXPECTED_PROFILE_UUID:-}" +PROFILE_SPECIFIER="${IOS_RELEASE_PROVISIONING_PROFILE_SPECIFIER:-}" + +fail() { + echo "$PREFIX ERROR: $*" >&2 + exit 1 +} + +normalize_boolean() { + case "$1" in + 1|true) printf '1\n' ;; + 0|false|'') printf '0\n' ;; + *) fail "IOS_RELEASE_SIGNING_REQUIRED must be exactly 0, false, 1, or true." ;; + esac +} + +SIGNING_REQUIRED="$(normalize_boolean "$SIGNING_REQUIRED")" + +if [[ "${GITHUB_ACTIONS:-false}" == "true" ]]; then + [[ "${GITHUB_EVENT_NAME:-}" == "push" ]] || fail \ + "The real signing gate refuses to access signing material outside a trusted push." + case "${GITHUB_REF:-}" in + refs/heads/develop|refs/heads/master) ;; + *) fail "The real signing gate refuses to access signing material outside a release branch." ;; + esac + [[ "$SIGNING_REQUIRED" == "1" ]] || fail \ + "Trusted release CI must require signing material; IOS_RELEASE_SIGNING_REQUIRED must be 1." + [[ "$EXPECTED_PROFILE_UUID" =~ ^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$ ]] || fail \ + "Trusted release CI must bind the archive to one decoded provisioning-profile UUID." + [[ -n "$PROFILE_SPECIFIER" && ${#PROFILE_SPECIFIER} -le 256 && "$PROFILE_SPECIFIER" != *$'\n'* && "$PROFILE_SPECIFIER" != *$'\r'* ]] || fail \ + "Trusted release CI must bind the archive to one bounded provisioning-profile specifier." +fi + +for command_path in "$SECURITY_BIN" "$CODESIGN_BIN" "$PLUTIL_BIN"; do + [[ -x "$command_path" ]] || fail "A required Apple release-validation tool is unavailable." +done +[[ -n "$XCODEBUILD_BIN" && -x "$XCODEBUILD_BIN" ]] || fail "xcodebuild is unavailable." +[[ -d "$WORKSPACE" ]] || fail "The iOS workspace is unavailable." +[[ -d "$SOURCE_PACKAGES_DIR/checkouts" ]] || fail \ + "Resolved Swift packages are unavailable; resolve them into the repository SourcePackages directory before this gate." + +temporary_root="$(mktemp -d "${TMPDIR:-/tmp}/fearless-release-signing.XXXXXX")" +chmod 700 "$temporary_root" +cleanup() { + rm -rf "$temporary_root" +} +trap cleanup EXIT INT TERM + +plist_type() { + local file="$1" + local key_path="$2" + "$PLUTIL_BIN" -type "$key_path" "$file" 2>/dev/null +} + +plist_value() { + local file="$1" + local key_path="$2" + "$PLUTIL_BIN" -extract "$key_path" raw -o - "$file" 2>/dev/null +} + +plist_array_contains() { + local file="$1" + local key_path="$2" + local expected="$3" + local count index value + + [[ "$(plist_type "$file" "$key_path" || true)" == "array" ]] || return 1 + count="$(plist_value "$file" "$key_path" || true)" + [[ "$count" =~ ^[0-9]+$ ]] || return 1 + index=0 + while ((index < count)); do + value="$(plist_value "$file" "$key_path.$index" || true)" + [[ "$value" == "$expected" ]] && return 0 + index=$((index + 1)) + done + return 1 +} + +plist_array_is_exact_set() { + local file="$1" + local key_path="$2" + local expected_count="$3" + shift 3 + local count expected + + [[ "$(plist_type "$file" "$key_path" || true)" == "array" ]] || return 1 + count="$(plist_value "$file" "$key_path" || true)" + [[ "$count" == "$expected_count" ]] || return 1 + for expected in "$@"; do + plist_array_contains "$file" "$key_path" "$expected" || return 1 + done +} + +profile_supports_value() { + local profile="$1" + local key_path="$2" + local expected="$3" + local value_type value + + value_type="$(plist_type "$profile" "$key_path" || true)" + case "$value_type" in + string) + value="$(plist_value "$profile" "$key_path" || true)" + [[ "$value" == "*" || "$value" == "$expected" ]] + ;; + array) + plist_array_contains "$profile" "$key_path" "$expected" || + plist_array_contains "$profile" "$key_path" "*" + ;; + *) return 1 ;; + esac +} + +profile_matches_release_contract() { + local profile="$1" + [[ "$(plist_value "$profile" 'Entitlements.application-identifier' || true)" == \ + "$EXPECTED_APPLICATION_ID" ]] || return 1 + [[ "$(plist_value "$profile" 'Entitlements.com\.apple\.developer\.team-identifier' || true)" == \ + "$EXPECTED_TEAM_ID" ]] || return 1 + plist_array_is_exact_set \ + "$profile" \ + 'Entitlements.com\.apple\.developer\.icloud-container-identifiers' \ + 1 \ + "$EXPECTED_ICLOUD_CONTAINER" || return 1 + plist_array_is_exact_set \ + "$profile" \ + 'Entitlements.com\.apple\.security\.application-groups' \ + 1 \ + "$EXPECTED_APP_GROUP" || return 1 + profile_supports_value \ + "$profile" \ + 'Entitlements.com\.apple\.developer\.associated-domains' \ + 'applinks:fearlesswallet.io' || return 1 + profile_supports_value \ + "$profile" \ + 'Entitlements.com\.apple\.developer\.associated-domains' \ + 'webcredentials:fearlesswallet.io' || return 1 + profile_supports_value \ + "$profile" \ + 'Entitlements.com\.apple\.developer\.icloud-services' \ + 'CloudKit' || return 1 + [[ "$(plist_value "$profile" 'Entitlements.get-task-allow' || true)" == "false" ]] || return 1 + [[ "$(plist_value "$profile" 'Entitlements.beta-reports-active' || true)" == "true" ]] || return 1 + ! plist_type "$profile" 'ProvisionedDevices' >/dev/null 2>&1 || return 1 + [[ "$(plist_value "$profile" 'ProvisionsAllDevices' || true)" != "true" ]] || return 1 + if [[ -n "$EXPECTED_PROFILE_UUID" ]]; then + [[ "$(plist_value "$profile" 'UUID' || true)" == "$EXPECTED_PROFILE_UUID" ]] || return 1 + fi + if [[ -n "$PROFILE_SPECIFIER" ]]; then + [[ "$(plist_value "$profile" 'Name' || true)" == "$PROFILE_SPECIFIER" ]] || return 1 + fi +} + +identity_output="$($SECURITY_BIN find-identity -v -p codesigning 2>/dev/null || true)" +identity_count="$(printf '%s\n' "$identity_output" | awk -v team="$EXPECTED_TEAM_ID" ' + /^[[:space:]]*[0-9]+\)[[:space:]]+[0-9A-Fa-f]{40}[[:space:]]+/ && + ($0 ~ /"Apple Distribution:/ || $0 ~ /"iPhone Distribution:/) && + index($0, "(" team ")\"") { count += 1 } + END { print count + 0 } +')" +unset identity_output + +matching_profile_count=0 +IFS=':' read -r -a profile_directories <<< "$PROFILE_DIRS" +for profile_directory in "${profile_directories[@]}"; do + [[ -d "$profile_directory" ]] || continue + while IFS= read -r -d '' profile_path; do + decoded_profile="$temporary_root/profile-$matching_profile_count.plist" + if "$SECURITY_BIN" cms -D -i "$profile_path" >"$decoded_profile" 2>/dev/null && + profile_matches_release_contract "$decoded_profile"; then + matching_profile_count=$((matching_profile_count + 1)) + fi + rm -f "$decoded_profile" + done < <(find "$profile_directory" -type f \ + \( -name '*.mobileprovision' -o -name '*.provisionprofile' \) -print0 2>/dev/null) +done + +if ((identity_count == 0 && matching_profile_count == 0)); then + if [[ "$SIGNING_REQUIRED" == "1" ]]; then + fail "Signed Release validation is required, but no code-signing identity or matching provisioning profile is installed." + fi + echo "$PREFIX SKIPPED: no code-signing identity or matching provisioning profile is installed." + exit 0 +fi + +if ((identity_count == 0 || matching_profile_count == 0)); then + fail "Signing material is only partially configured; both a code-signing identity and a matching provisioning profile are required." +fi + +archive_path="$temporary_root/FearlessRelease.xcarchive" +build_log="$temporary_root/xcodebuild.log" +: >"$build_log" +chmod 600 "$build_log" + +signing_overrides=() +if [[ -n "$PROFILE_SPECIFIER" ]]; then + signing_overrides+=( + "CODE_SIGN_STYLE=Manual" + "CODE_SIGN_IDENTITY=Apple Distribution" + "DEVELOPMENT_TEAM=$EXPECTED_TEAM_ID" + "PROVISIONING_PROFILE_SPECIFIER=$PROFILE_SPECIFIER" + ) +fi + +if ! SOURCE_PACKAGES_DIR="$SOURCE_PACKAGES_DIR" \ + ALLOW_DERIVEDDATA_FALLBACK=0 \ + STRICT_REQUIRED_PATCHES=1 \ + "$XCODEBUILD_BIN" \ + -workspace "$WORKSPACE" \ + -scheme "$SCHEME" \ + -configuration Release \ + -destination 'generic/platform=iOS' \ + -clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \ + -disableAutomaticPackageResolution \ + -archivePath "$archive_path" \ + "${signing_overrides[@]}" \ + archive >"$build_log" 2>&1; then + echo "$PREFIX ERROR: signed Release archive failed. Sanitized diagnostics:" >&2 + sed -E \ + -e 's/[0-9A-Fa-f]{40,64}/[REDACTED_DIGEST]/g' \ + -e 's/(Signing Identity:).*/\1 [REDACTED]/' \ + -e 's/(Provisioning Profile:).*/\1 [REDACTED]/' \ + -e 's/(Certificate:).*/\1 [REDACTED]/' \ + "$build_log" | + grep -E '(^|[[:space:]])(error:|ERROR:)|\*\* ARCHIVE FAILED \*\*' | + tail -n 20 >&2 || true + exit 1 +fi + +applications_dir="$archive_path/Products/Applications" +[[ -d "$applications_dir" ]] || fail "The Release archive contains no Applications directory." +app_path="" +app_count=0 +while IFS= read -r -d '' candidate_app; do + app_path="$candidate_app" + app_count=$((app_count + 1)) +done < <(find "$applications_dir" -mindepth 1 -maxdepth 1 -type d -name '*.app' -print0) +[[ "$app_count" == "1" && -n "$app_path" ]] || fail \ + "The Release archive must contain exactly one application product." + +"$CODESIGN_BIN" --verify --deep --strict "$app_path" >/dev/null 2>&1 || fail \ + "The archived application failed strict code-signature verification." +[[ -f "$app_path/embedded.mobileprovision" ]] || fail \ + "The archived application does not contain an embedded provisioning profile." +[[ -f "$app_path/PrivacyInfo.xcprivacy" ]] || fail \ + "The archived application does not contain PrivacyInfo.xcprivacy." + +signed_entitlements="$temporary_root/signed-entitlements.plist" +embedded_profile="$temporary_root/embedded-profile.plist" +: >"$signed_entitlements" +: >"$embedded_profile" +chmod 600 "$signed_entitlements" "$embedded_profile" +"$CODESIGN_BIN" -d --entitlements :- "$app_path" >"$signed_entitlements" 2>/dev/null || fail \ + "Signed entitlements could not be extracted from the archived application." +"$SECURITY_BIN" cms -D -i "$app_path/embedded.mobileprovision" >"$embedded_profile" 2>/dev/null || fail \ + "The embedded provisioning profile could not be decoded." + +[[ "$(plist_value "$app_path/Info.plist" 'CFBundleIdentifier' || true)" == "$EXPECTED_BUNDLE_ID" ]] || fail \ + "The archived application bundle identifier does not match the production contract." +[[ "$(plist_value "$signed_entitlements" 'application-identifier' || true)" == "$EXPECTED_APPLICATION_ID" ]] || fail \ + "The signed application identifier does not match the production contract." +[[ "$(plist_value "$signed_entitlements" 'com\.apple\.developer\.team-identifier' || true)" == "$EXPECTED_TEAM_ID" ]] || fail \ + "The signed team identifier does not match the production contract." +[[ "$(plist_value "$signed_entitlements" 'beta-reports-active' || true)" == "true" ]] || fail \ + "The signed application must carry the App Store beta-reports-active entitlement." +plist_array_is_exact_set \ + "$signed_entitlements" \ + 'com\.apple\.developer\.associated-domains' \ + 2 \ + 'applinks:fearlesswallet.io' \ + 'webcredentials:fearlesswallet.io' || fail \ + "The signed associated-domain entitlement does not match the production contract." +plist_array_is_exact_set \ + "$signed_entitlements" \ + 'com\.apple\.developer\.icloud-container-identifiers' \ + 1 \ + "$EXPECTED_ICLOUD_CONTAINER" || fail \ + "The signed CloudKit container entitlement does not match the production contract." +plist_array_is_exact_set \ + "$signed_entitlements" \ + 'com\.apple\.developer\.icloud-services' \ + 1 \ + 'CloudKit' || fail \ + "The signed CloudKit service entitlement does not match the production contract." +plist_array_is_exact_set \ + "$signed_entitlements" \ + 'com\.apple\.security\.application-groups' \ + 1 \ + "$EXPECTED_APP_GROUP" || fail \ + "The signed application-group entitlement does not match the production contract." +if plist_type "$signed_entitlements" 'keychain-access-groups' >/dev/null 2>&1; then + fail "The archived application contains an unreviewed explicit keychain access-group entitlement." +fi + +profile_matches_release_contract "$embedded_profile" || fail \ + "The embedded provisioning profile does not authorize the production entitlement contract." + +signed_get_task_allow="$(plist_value "$signed_entitlements" 'get-task-allow' || true)" +profile_get_task_allow="$(plist_value "$embedded_profile" 'Entitlements.get-task-allow' || true)" +[[ "$signed_get_task_allow" == "false" && "$profile_get_task_allow" == "false" ]] || fail \ + "The production archive and embedded profile must both set get-task-allow to false." + +echo "$PREFIX PASSED: signed Release archive, entitlements, provisioning profile, and privacy manifest are consistent." diff --git a/scripts/test-codecov-coverage-contract.sh b/scripts/test-codecov-coverage-contract.sh index 0dd04e9ad8..29d34cc530 100755 --- a/scripts/test-codecov-coverage-contract.sh +++ b/scripts/test-codecov-coverage-contract.sh @@ -62,11 +62,13 @@ expect_rejected \ replace_text "fail_ci_if_error: true" "fail_ci_if_error: false" expect_rejected \ "Codecov OIDC removed" \ - replace_text "use_oidc: true" "use_oidc: false" + replace_text \ + "use_oidc: \${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}" \ + "use_oidc: false" expect_rejected \ "mutable Codecov action reference" \ replace_text \ - "codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f" \ + "codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de" \ "codecov/codecov-action@v7" [[ "$positive_count" == "1" && "$negative_count" == "7" ]] || diff --git a/scripts/test-codecov-workflow-audit.sh b/scripts/test-codecov-workflow-audit.sh new file mode 100755 index 0000000000..19a16dd76a --- /dev/null +++ b/scripts/test-codecov-workflow-audit.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +AUDIT="$ROOT_DIR/scripts/audit-codecov-workflow.sh" +tmp_dir="$(mktemp -d)" +trap 'rm -rf "$tmp_dir"' EXIT +fixture="$tmp_dir/codecov.yml" + +fail() { + echo "[codecov-workflow-audit-test][error] $*" >&2 + exit 1 +} + +reset_fixture() { + cp "$ROOT_DIR/.github/workflows/codecov.yml" "$fixture" +} + +run_audit() { + IOS_CODECOV_AUDIT_WORKFLOW_FILE="$fixture" "$AUDIT" >/dev/null 2>&1 +} + +expect_failure() { + local label="$1" + if run_audit; then + fail "$label was accepted" + fi +} + +remove_line() { + local needle="$1" + awk -v needle="$needle" 'index($0, needle) == 0 { print }' "$fixture" > "$fixture.next" + mv "$fixture.next" "$fixture" +} + +comment_line() { + local needle="$1" + awk -v needle="$needle" ' + index($0, needle) != 0 { print "# " $0; next } + { print } + ' "$fixture" > "$fixture.next" + mv "$fixture.next" "$fixture" +} + +reset_fixture +run_audit || fail "valid Codecov workflow was rejected" + +reset_fixture +sed -i.bak 's/codecov\/codecov-action@[0-9a-f]*/codecov\/codecov-action@v5/' "$fixture" +expect_failure "unpinned Codecov action" + +reset_fixture +comment_line 'codecov/codecov-action@' +expect_failure "commented-out pinned Codecov action" + +reset_fixture +sed -i.bak 's/actions\/checkout@[0-9a-f]*/actions\/checkout@v4/' "$fixture" +expect_failure "unpinned checkout action" + +reset_fixture +sed -i.bak 's/actions\/upload-artifact@[0-9a-f]*/actions\/upload-artifact@v4/' "$fixture" +expect_failure "unpinned artifact upload action" + +reset_fixture +remove_line 'fail_ci_if_error: true' +expect_failure "fail-open Codecov upload" + +reset_fixture +remove_line 'swift_project: fearless' +expect_failure "Codecov upload for the wrong Swift project" + +reset_fixture +remove_line 'id-token: write' +expect_failure "Codecov upload without OIDC permission" + +reset_fixture +remove_line 'use_oidc:' +expect_failure "Codecov upload without trusted-event OIDC" + +reset_fixture +remove_line 'test-codecov-workflow-audit.sh' +expect_failure "CI without uploader audit self-test" + +reset_fixture +remove_line 'bash ./scripts/audit-codecov-workflow.sh' +expect_failure "CI without real uploader audit" + +reset_fixture +perl -0pi -e 's/contents: read/contents: write/' "$fixture" +expect_failure "unnecessary repository write permission" + +reset_fixture +printf '\n - run: bash <(curl -s https://codecov.io/bash)\n' >> "$fixture" +expect_failure "unverified curl-to-shell uploader" + +reset_fixture +printf '\n - run: curl -fsSL https://codecov.io/bash | sh\n' >> "$fixture" +expect_failure "piped unverified uploader" + +echo "[codecov-workflow-audit-test] all adversarial fixtures passed" diff --git a/scripts/test-iroha-production-send-readiness-audit.sh b/scripts/test-iroha-production-send-readiness-audit.sh new file mode 100755 index 0000000000..98b255e14f --- /dev/null +++ b/scripts/test-iroha-production-send-readiness-audit.sh @@ -0,0 +1,485 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || (cd "$(dirname "$0")/.." && pwd))" +AUDIT="$ROOT_DIR/scripts/audit-iroha-production-send-readiness.sh" +TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/fearless-ios-iroha-send.XXXXXX")" +trap 'rm -rf "$TMP_DIR"' EXIT +negative_count=0 + +fail() { + echo "[iroha-send-readiness-test][ios][error] $*" >&2 + exit 1 +} + +sha256_file() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{print $1}' + else + shasum -a 256 "$1" | awk '{print $1}' + fi +} + +new_fixture() { + local name="$1" + local fixture="$TMP_DIR/$name" + mkdir -p \ + "$fixture/config" "$fixture/docs" "$fixture/fearless.xcodeproj" \ + "$fixture/fearless/ApplicationLayer/Services/Transfer/Tokens" \ + "$fixture/fearless/Modules/Send" \ + "$fixture/fearless/Common/Model" \ + "$fixture/fearlessTests/ApplicationLayer/Services/FeatureToggle" + cp "$ROOT_DIR/config/iroha-production-send-readiness.json" "$fixture/config/" + cp "$ROOT_DIR/docs/iroha-production-send-readiness.md" "$fixture/docs/" + cp "$ROOT_DIR/docs/universal-wallet-v2.md" "$fixture/docs/" + cp "$ROOT_DIR/docs/release-checklist.md" "$fixture/docs/" + cp "$ROOT_DIR/Podfile" "$fixture/" + cp "$ROOT_DIR/fearless.xcodeproj/project.pbxproj" "$fixture/fearless.xcodeproj/" + cp "$ROOT_DIR/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift" \ + "$fixture/fearless/ApplicationLayer/Services/Transfer/Tokens/" + cp "$ROOT_DIR/fearless/Modules/Send/SendDependencyContainer.swift" "$fixture/fearless/Modules/Send/" + cp "$ROOT_DIR/fearless/Common/Model/UniversalWalletRegistry.swift" "$fixture/fearless/Common/Model/" + cp "$ROOT_DIR/fearlessTests/ApplicationLayer/Services/FeatureToggle/TonChainSelectionTests.swift" \ + "$fixture/fearlessTests/ApplicationLayer/Services/FeatureToggle/" + ( + cd "$fixture" + git init -q + git add . + ) + printf '%s' "$fixture" +} + +run_audit() { + local fixture="$1" + local audit="${2:-$AUDIT}" + IROHA_SEND_AUDIT_ROOT="$fixture" "$audit" +} + +expect_failure() { + local label="$1" + local fixture="$2" + local expected="$3" + local audit="${4:-$AUDIT}" + local output status + set +e + output="$(run_audit "$fixture" "$audit" 2>&1)" + status=$? + set -e + if [[ "$status" -eq 0 ]]; then + fail "$label unexpectedly passed" + fi + [[ "$output" == *"$expected"* ]] || { + printf '%s\n' "$output" >&2 + fail "$label did not report expected marker: $expected" + } + negative_count=$((negative_count + 1)) +} + +mutate_manifest() { + local fixture="$1" + local field_path="$2" + local json_value="$3" + node - "$fixture/config/iroha-production-send-readiness.json" "$field_path" "$json_value" <<'NODE' +const fs = require('node:fs'); +const file = process.argv[2]; +const path = process.argv[3].split('.'); +const value = JSON.parse(process.argv[4]); +const manifest = JSON.parse(fs.readFileSync(file, 'utf8')); +let target = manifest; +for (const key of path.slice(0, -1)) { + if (!target[key] || typeof target[key] !== 'object') target[key] = {}; + target = target[key]; +} +target[path.at(-1)] = value; +fs.writeFileSync(file, `${JSON.stringify(manifest, null, 2)}\n`); +NODE +} + +repinned_audit() { + local fixture="$1" + local digest audit_copy + digest="$(sha256_file "$fixture/config/iroha-production-send-readiness.json")" + audit_copy="$fixture/audit-repinned.sh" + cp "$AUDIT" "$audit_copy" + perl -0pi -e \ + 's/EXPECTED_MANIFEST_SHA256="[0-9a-f]{64}"/EXPECTED_MANIFEST_SHA256="'"$digest"'"/' \ + "$audit_copy" + chmod +x "$audit_copy" + printf '%s' "$audit_copy" +} + +expect_manifest_failure() { + local label="$1" + local field_path="$2" + local json_value="$3" + local expected="$4" + local fixture audit_copy + fixture="$(new_fixture "manifest-$label")" + mutate_manifest "$fixture" "$field_path" "$json_value" + audit_copy="$(repinned_audit "$fixture")" + expect_failure "manifest $label" "$fixture" "$expected" "$audit_copy" +} + +valid="$(new_fixture valid)" +run_audit "$valid" >/dev/null + +fixture="$(new_fixture manifest-digest-tamper)" +sed -i.bak 's/"releaseEnabled": false/"releaseEnabled": true/' "$fixture/config/iroha-production-send-readiness.json" +rm -f "$fixture/config/iroha-production-send-readiness.json.bak" +expect_failure "manifest digest tamper" "$fixture" "manifest digest mismatch" + +# Repin only the temporary fixture audit so every semantic assertion is tested, +# independently of the production manifest's outer digest lock. +expect_manifest_failure "extra-key" "unexpected" 'true' "top-level manifest keys drifted" +expect_manifest_failure "schema" "schemaVersion" '3' "schemaVersion must be 2" +expect_manifest_failure "platform" "platform" '"android"' "platform must be ios" +expect_manifest_failure "date" "assessedAt" '"2026-07-10"' "assessment date drifted" +expect_manifest_failure "status" "status" '"ready"' "status must remain blocked" +expect_manifest_failure "release-enabled" "releaseEnabled" 'true' "releaseEnabled must remain false" +expect_manifest_failure "nexus-enabled" "nexusEnabledByDefault" 'true' "Nexus must remain disabled by default" +expect_manifest_failure "tag" "upstream.tag" '"v0"' "unexpected upstream tag" +expect_manifest_failure "commit" "upstream.commit" '"0000000000000000000000000000000000000000"' "unexpected upstream commit" +expect_manifest_failure "artifact-digest" "artifact.sha256" '"0000000000000000000000000000000000000000000000000000000000000000"' "unexpected Apple artifact digest" +expect_manifest_failure "artifact-size" "artifact.bytes" '1' "unexpected Apple artifact size" +expect_manifest_failure "entry-count" "artifact.entryCount" '19' "archive entry count drifted" +expect_manifest_failure "expanded-size" "artifact.expandedBytes" '1396110348' "archive expanded size drifted" +expect_manifest_failure "max-entry" "artifact.maxEntryBytes" '704780503' "archive maximum entry size drifted" +expect_manifest_failure "app-minimum" "integrationAssessment.appMinimumIOS" '"15.0"' "app minimum iOS evidence drifted" +expect_manifest_failure "sdk-minimum" "integrationAssessment.sdkMinimumIOS" '"14.1"' "SDK minimum iOS evidence drifted" +expect_manifest_failure "os-compatible" "integrationAssessment.minimumOSCompatible" 'true' "minimum OS mismatch must remain explicit" +expect_manifest_failure "os-change-approved" "integrationAssessment.productMinimumOSChangeApproved" 'true' "product minimum OS change must remain unapproved" +expect_manifest_failure "linkage-approved" "integrationAssessment.sdkLinkageApproved" 'true' "SDK linkage must remain unapproved" +expect_manifest_failure "package-delivery" "integrationAssessment.taggedPackageDelivery" '"remote-binary-target"' "tagged package delivery evidence drifted" +expect_manifest_failure "compile-status" "integrationAssessment.taggedCompileStatus" '"passed"' "tagged compile failure must remain explicit" +expect_manifest_failure "loader-hash" "integrationAssessment.taggedSwiftLoaderExpectedSliceSha256.ios-arm64" '"0000000000000000000000000000000000000000000000000000000000000000"' "tagged Swift loader hashes drifted" +expect_manifest_failure "slice-match" "integrationAssessment.publishedSlicesMatchTaggedLoader" 'true' "source/artifact slice mismatch must remain explicit" +expect_manifest_failure "provenance" "integrationAssessment.buildProvenanceBundled" 'true' "missing build provenance evidence drifted" +expect_manifest_failure "canonical-hash" "transactionParity.currentMainCompactEntrypointHash" '"0000000000000000000000000000000000000000000000000000000000000000"' "compact entrypoint hash drifted" +expect_manifest_failure "fixed-hash" "transactionParity.fixedU64EntrypointHash" '"0000000000000000000000000000000000000000000000000000000000000000"' "fixed-u64 diagnostic hash drifted" +expect_manifest_failure "stale-hash" "transactionParity.taggedSwiftRawFixtureHash" '"0000000000000000000000000000000000000000000000000000000000000000"' "tagged Swift parity hash drifted" +expect_manifest_failure "tag-parity" "transactionParity.officialTagParityStatus" '"canonical"' "official tag parity blocker drifted" +expect_manifest_failure "local-correction" "transactionParity.localCompactCorrectionStatus" '"published-reviewed"' "local correction must not be release evidence" +expect_manifest_failure "live-parity" "transactionParity.liveReceiptParity" '"passed"' "live receipt parity must remain blocked" +expect_manifest_failure "protocol-chain" "networkReadiness.protocolChainIdMapping" '"same-as-route"' "protocol chain ID mapping must remain blocked" +expect_manifest_failure "live-asset" "networkReadiness.liveTairaNativeXorDefinitionId" '"61CtjvNd9T3THAR65GsMVHr82Bjc"' "live Taira XOR definition drifted" +expect_manifest_failure "asset-scale" "networkReadiness.liveTairaNativeXorScale" '18' "live Taira XOR scale drifted" +expect_manifest_failure "fee-policy" "networkReadiness.authoritativeFeePolicy" '"zero"' "authoritative fee policy must remain blocked" +expect_manifest_failure "node-version" "networkReadiness.liveTairaNodeVersion" '"2.0.0-rc.2.1"' "live Taira node version drifted" +expect_manifest_failure "node-compatibility" "networkReadiness.sdkToDeployedNodeCompatibility" '"proven"' "SDK/deployed-node compatibility must remain blocked" +expect_manifest_failure "secret-representation" "securityBoundary.secretRequestRepresentation" '"secure-bytes"' "secret representation risk drifted" +expect_manifest_failure "secret-zeroization" "securityBoundary.secretCopiesReliablyZeroizable" 'true' "secret zeroization gap must remain explicit" +expect_manifest_failure "submission-policy" "securityBoundary.submissionHashPolicy" '"exact-match"' "submission hash-policy gap drifted" +expect_manifest_failure "receipt-equality" "securityBoundary.receiptHashEqualityRequired" 'false' "receipt hash equality must be required" +expect_manifest_failure "live-funded" "liveEvidence.fundedTairaBroadcast" '"passed"' "live evidence fundedTairaBroadcast must remain unavailable" +expect_manifest_failure "blocker" "blocker.code" '"ready"' "unexpected blocker code" +expect_manifest_failure "exit-criteria" "exitCriteria" '[]' "exactly eleven exit criteria are required" + +fixture="$(new_fixture invalid-json)" +printf '{invalid\n' > "$fixture/config/iroha-production-send-readiness.json" +audit_copy="$(repinned_audit "$fixture")" +expect_failure "invalid JSON" "$fixture" "invalid readiness JSON" "$audit_copy" + +fixture="$(new_fixture service-bypass)" +sed -i.bak 's/signer: IrohaTransferSigning = UnavailableIrohaTransferSigner()/signer: IrohaTransferSigning = ReviewedSigner()/' \ + "$fixture/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift" +rm -f "$fixture/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift.bak" +expect_failure "service bypass" "$fixture" "fail-closed service default" + +fixture="$(new_fixture container-injection)" +sed -i.bak 's/return IrohaTransferService(wallet: wallet, chain: chainAsset.chain)/return IrohaTransferService(wallet: wallet, chain: chainAsset.chain, signer: ReviewedSigner())/' \ + "$fixture/fearless/Modules/Send/SendDependencyContainer.swift" +rm -f "$fixture/fearless/Modules/Send/SendDependencyContainer.swift.bak" +expect_failure "container injection" "$fixture" "send container fail-closed construction" + +fixture="$(new_fixture duplicate-container-bypass)" +perl -0pi -e \ + 's/ if isUniversalWalletIroha\(chainAsset[.]chain\) \{\n/ if isUniversalWalletIroha(chainAsset.chain) {\n if Bool.random() { return IrohaTransferService(wallet: wallet, chain: chainAsset.chain, signer: ReviewedSigner()) }\n/' \ + "$fixture/fearless/Modules/Send/SendDependencyContainer.swift" +expect_failure "duplicate container bypass" "$fixture" "exactly one audited fail-closed Iroha service construction" + +fixture="$(new_fixture signer-no-longer-fails)" +sed -i.bak 's/Iroha transfer signing codec is unavailable/temporary fallback/' \ + "$fixture/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift" +rm -f "$fixture/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift.bak" +expect_failure "signer no longer fails explicitly" "$fixture" "unavailable signer exception" + +fixture="$(new_fixture secret-obscured)" +sed -i.bak 's/let mnemonicOrSeed: String/let secret: String/' \ + "$fixture/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift" +rm -f "$fixture/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift.bak" +expect_failure "secret risk obscured" "$fixture" "immutable Swift secret risk marker" + +fixture="$(new_fixture protocol-chain-assumed)" +sed -i.bak 's/chainId: network.chainId/chainId: "taira"/' \ + "$fixture/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift" +rm -f "$fixture/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift.bak" +expect_failure "protocol chain assumed" "$fixture" "route-label signing-chain ambiguity marker" + +fixture="$(new_fixture fee-assumed-zero-safe)" +sed -i.bak 's/return .zero/return BigUInt(1)/' \ + "$fixture/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift" +rm -f "$fixture/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift.bak" +expect_failure "fee behavior changed" "$fixture" "unresolved zero-fee marker" + +fixture="$(new_fixture receipt-fallback-removed)" +sed -i.bak 's/?? receipt.payload.signedTransactionHash/?? receipt.payload.txHash/' \ + "$fixture/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift" +rm -f "$fixture/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift.bak" +expect_failure "receipt fallback changed" "$fixture" "receipt signed-transaction hash fallback" + +fixture="$(new_fixture finality-silently-added)" +perl -0pi -e \ + 's/( return signedTransfer[.]transactionHashHex)/ _ = transactionStatus(hash: "unchecked")\n\n$1/' \ + "$fixture/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift" +expect_failure "unreviewed finality change" "$fixture" "submission finality behavior changed without readiness review" + +fixture="$(new_fixture nexus-enabled)" +node - "$fixture/fearless/Common/Model/UniversalWalletRegistry.swift" <<'NODE' +const fs = require('node:fs'); +const path = process.argv[2]; +const text = fs.readFileSync(path, 'utf8'); +const start = text.indexOf('static let nexus = IrohaNetwork('); +const end = text.indexOf('\n )', start); +fs.writeFileSync(path, text.slice(0, start) + text.slice(start, end).replace('enabledByDefault: false', 'enabledByDefault: true') + text.slice(end)); +NODE +expect_failure "Nexus enabled" "$fixture" "Nexus registry default" + +fixture="$(new_fixture taira-route-drift)" +sed -i.bak 's/chainId: "iroha3-taira"/chainId: "taira"/' "$fixture/fearless/Common/Model/UniversalWalletRegistry.swift" +rm -f "$fixture/fearless/Common/Model/UniversalWalletRegistry.swift.bak" +expect_failure "Taira route drift" "$fixture" "Taira route label drifted" + +fixture="$(new_fixture app-minimum-raised)" +perl -0pi -e \ + 's/(INFOPLIST_FILE = fearless\/Info[.]plist;\n[[:space:]]*IPHONEOS_DEPLOYMENT_TARGET = )14[.]1;/${1}15.0;/' \ + "$fixture/fearless.xcodeproj/project.pbxproj" +expect_failure "app minimum raised" "$fixture" "deployment target must remain iOS 14.1" + +fixture="$(new_fixture pod-minimum-raised)" +sed -i.bak "s/IPHONEOS_DEPLOYMENT_TARGET'] = '14.1'/IPHONEOS_DEPLOYMENT_TARGET'] = '15.0'/" "$fixture/Podfile" +rm -f "$fixture/Podfile.bak" +expect_failure "Pod minimum raised" "$fixture" "CocoaPods iOS 14.1 deployment target" + +fixture="$(new_fixture package-added)" +printf '\nrepositoryURL = "https://github.com/hyperledger/iroha";\nproductName = IrohaSwift;\n' >> "$fixture/fearless.xcodeproj/project.pbxproj" +expect_failure "unreviewed Xcode Swift package" "$fixture" "dependency graph before review" + +fixture="$(new_fixture pod-added)" +printf '\npod "IrohaSwift"\n' >> "$fixture/Podfile" +expect_failure "unreviewed CocoaPod" "$fixture" "CocoaPods before review" + +fixture="$(new_fixture package-manifest-added)" +printf '// swift-tools-version: 5.9\n// IrohaSwift https://github.com/hyperledger/iroha\n' > "$fixture/Package.swift" +expect_failure "unreviewed package manifest" "$fixture" "dependency manifest appeared before review" + +fixture="$(new_fixture product-import-added)" +printf 'import IrohaSwift\n' > "$fixture/fearless/Injected.swift" +expect_failure "unreviewed product import" "$fixture" "imported into product source before review" + +fixture="$(new_fixture local-xcframework)" +mkdir -p "$fixture/vendor/NoritoBridge.xcframework" +expect_failure "local untracked XCFramework" "$fixture" "SDK/native binary material" + +fixture="$(new_fixture tracked-xcframework)" +mkdir -p "$fixture/vendor" +printf 'binary\n' > "$fixture/vendor/NoritoBridge.xcframework.zip" +git -C "$fixture" add . +expect_failure "tracked XCFramework" "$fixture" "SDK/native binary material" + +fixture="$(new_fixture second-signer-file)" +printf 'struct HiddenSigner: IrohaTransferSigning {}\n' > "$fixture/fearless/HiddenSigner.swift" +expect_failure "second signer file" "$fixture" "signer implementation appeared outside" + +fixture="$(new_fixture second-service-call)" +printf 'let hidden = IrohaTransferService(wallet: wallet, chain: chain)\n' > "$fixture/fearless/HiddenService.swift" +expect_failure "second service call" "$fixture" "construction appeared outside" + +fixture="$(new_fixture fail-closed-test-removed)" +sed -i.bak 's/testIrohaTransferServiceDefaultSignerFailsClosedAfterValidation/testIrohaTransferServiceDefaultSigner/' \ + "$fixture/fearlessTests/ApplicationLayer/Services/FeatureToggle/TonChainSelectionTests.swift" +rm -f "$fixture/fearlessTests/ApplicationLayer/Services/FeatureToggle/TonChainSelectionTests.swift.bak" +expect_failure "fail-closed test removed" "$fixture" "default signer fail-closed test" + +fixture="$(new_fixture mismatch-test-removed)" +sed -i.bak 's/testIrohaTransferServiceRejectsMnemonicMismatchBeforeSignerOrToriiCalls/testIrohaMismatch/' \ + "$fixture/fearlessTests/ApplicationLayer/Services/FeatureToggle/TonChainSelectionTests.swift" +rm -f "$fixture/fearlessTests/ApplicationLayer/Services/FeatureToggle/TonChainSelectionTests.swift.bak" +expect_failure "mismatch test removed" "$fixture" "mnemonic/key mismatch adversarial test" + +fixture="$(new_fixture metadata-field-removed)" +sed -i.bak 's/let metadata: IrohaTransactionMetadata/let metadataRemoved: IrohaTransactionMetadata/' \ + "$fixture/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift" +rm -f "$fixture/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift.bak" +expect_failure "metadata field removed" "$fixture" "Iroha signing request metadata seam" + +fixture="$(new_fixture ordinary-metadata-enabled)" +sed -i.bak \ + 's/submitValidated(transfer: transfer, metadata: .none)/submitValidated(transfer: transfer, metadata: unsafeMetadata)/' \ + "$fixture/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift" +rm -f "$fixture/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift.bak" +expect_failure "ordinary metadata enabled" "$fixture" "ordinary Iroha transfers must omit transaction metadata" + +fixture="$(new_fixture evidence-hook-removed)" +sed -i.bak 's/func submitNexusWalletSmokeEvidence(/func submitUnreviewedEvidence(/' \ + "$fixture/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift" +rm -f "$fixture/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift.bak" +expect_failure "evidence hook removed" "$fixture" "operator-only Nexus wallet-smoke evidence hook" + +fixture="$(new_fixture metadata-role-drift)" +sed -i.bak \ + 's/untrustedMetadata\[evidenceRoleKey\] == "wallet-smoke"/untrustedMetadata[evidenceRoleKey] == "wallet_smoke"/' \ + "$fixture/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift" +rm -f "$fixture/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift.bak" +expect_failure "metadata role drift" "$fixture" "wallet-smoke metadata invariant" + +fixture="$(new_fixture metadata-platform-drift)" +sed -i.bak \ + 's/untrustedMetadata\[walletPlatformKey\] == "ios"/untrustedMetadata[walletPlatformKey] == "iOS"/' \ + "$fixture/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift" +rm -f "$fixture/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift.bak" +expect_failure "metadata platform drift" "$fixture" "wallet-smoke metadata invariant" + +fixture="$(new_fixture metadata-key-drift)" +sed -i.bak \ + 's/static let routeGovernanceActionHashKey = "route_governance_action_hash"/static let routeGovernanceActionHashKey = "route_action_hash"/' \ + "$fixture/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift" +rm -f "$fixture/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift.bak" +expect_failure "metadata key drift" "$fixture" "wallet-smoke metadata invariant" + +fixture="$(new_fixture metadata-field-set-bypass)" +sed -i.bak 's/untrustedMetadata.count == expectedKeys.count/true/' \ + "$fixture/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift" +rm -f "$fixture/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift.bak" +expect_failure "metadata field-set bypass" "$fixture" "wallet-smoke metadata invariant" + +fixture="$(new_fixture metadata-constructor-exposed)" +sed -i.bak 's/private init(snapshot: \[String: String\])/init(snapshot: [String: String])/' \ + "$fixture/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift" +rm -f "$fixture/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift.bak" +expect_failure "metadata constructor exposed" "$fixture" "wallet-smoke metadata invariant" + +fixture="$(new_fixture zero-route-hash-accepted)" +sed -i.bak \ + 's/routeHash != routeHashPrefix + String(repeating: "0", count: 64)/true/' \ + "$fixture/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift" +rm -f "$fixture/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift.bak" +expect_failure "zero route hash accepted" "$fixture" "wallet-smoke metadata invariant" + +fixture="$(new_fixture zero-wallet-commit-accepted)" +sed -i.bak \ + 's/walletCommit != String(repeating: "0", count: 40)/true/' \ + "$fixture/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift" +rm -f "$fixture/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift.bak" +expect_failure "zero wallet commit accepted" "$fixture" "wallet-smoke metadata invariant" + +fixture="$(new_fixture evidence-network-bypass)" +sed -i.bak 's/context.signingRequest.network == "nexus"/true/' \ + "$fixture/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift" +rm -f "$fixture/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift.bak" +expect_failure "evidence network bypass" "$fixture" "wallet-smoke metadata invariant" + +fixture="$(new_fixture evidence-original-chain-bypass)" +sed -i.bak \ + 's/chain.chainId == UniversalWalletRegistry.nexus.chainId/true/' \ + "$fixture/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift" +rm -f "$fixture/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift.bak" +expect_failure "evidence original chain bypass" "$fixture" "wallet-smoke metadata invariant" + +fixture="$(new_fixture evidence-endpoint-bypass)" +sed -i.bak \ + 's/context.toriiBaseURL == UniversalWalletRegistry.nexus.toriiBaseURL?.absoluteString/true/' \ + "$fixture/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift" +rm -f "$fixture/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift.bak" +expect_failure "evidence endpoint bypass" "$fixture" "wallet-smoke metadata invariant" + +fixture="$(new_fixture metadata-negative-test-removed)" +sed -i.bak \ + 's/testIrohaNexusWalletSmokeEvidenceRejectsMalformedMetadataBeforeSignerOrTorii/testIrohaMetadataNegative/' \ + "$fixture/fearlessTests/ApplicationLayer/Services/FeatureToggle/TonChainSelectionTests.swift" +rm -f "$fixture/fearlessTests/ApplicationLayer/Services/FeatureToggle/TonChainSelectionTests.swift.bak" +expect_failure "metadata negative test removed" "$fixture" "wallet-smoke adversarial test" + +fixture="$(new_fixture metadata-alias-test-removed)" +sed -i.bak \ + 's/testIrohaWalletSmokeMetadataSnapshotDoesNotAliasInputOrReturnedValues/testIrohaMetadataAlias/' \ + "$fixture/fearlessTests/ApplicationLayer/Services/FeatureToggle/TonChainSelectionTests.swift" +rm -f "$fixture/fearlessTests/ApplicationLayer/Services/FeatureToggle/TonChainSelectionTests.swift.bak" +expect_failure "metadata alias test removed" "$fixture" "wallet-smoke adversarial test" + +fixture="$(new_fixture metadata-fail-closed-test-removed)" +sed -i.bak \ + 's/testIrohaWalletSmokeEvidenceRemainsFailClosedWithUnavailableSigner/testIrohaMetadataFailClosed/' \ + "$fixture/fearlessTests/ApplicationLayer/Services/FeatureToggle/TonChainSelectionTests.swift" +rm -f "$fixture/fearlessTests/ApplicationLayer/Services/FeatureToggle/TonChainSelectionTests.swift.bak" +expect_failure "metadata fail-closed test removed" "$fixture" "wallet-smoke adversarial test" + +fixture="$(new_fixture nonmaterializing-endpoint-fixture)" +sed -i.bak \ + 's/ChainModel.BlockExplorer(type: "sora", url: url)/ChainModel.BlockExplorer(type: "iroha", url: url)/' \ + "$fixture/fearlessTests/ApplicationLayer/Services/FeatureToggle/TonChainSelectionTests.swift" +rm -f "$fixture/fearlessTests/ApplicationLayer/Services/FeatureToggle/TonChainSelectionTests.swift.bak" +expect_failure \ + "nonmaterializing endpoint fixture" \ + "$fixture" \ + "materialized Nexus endpoint adversarial fixture" + +for hostile_url in \ + 'https://nexus-proxy.example' \ + 'http://minamoto.sora.org' \ + 'https://minamoto.sora.org.attacker.invalid' \ + 'https://minamoto.sora.org@attacker.invalid' \ + 'https://minamoto.sora.org:444' \ + 'https://minamoto.sora.org/v1/mcp' \ + 'https://minamoto.sora.org?redirect=https://attacker.invalid' \ + 'https://minamoto.sora.org#@attacker.invalid' \ + 'https://minamoto.sora.org/'; do + fixture="$(new_fixture "endpoint-case-$(printf '%s' "$hostile_url" | shasum -a 256 | cut -c1-12)")" + HOSTILE_URL="$hostile_url" node - \ + "$fixture/fearlessTests/ApplicationLayer/Services/FeatureToggle/TonChainSelectionTests.swift" <<'NODE' +const fs = require('node:fs'); +const file = process.argv[2]; +const hostileURL = process.env.HOSTILE_URL; +const source = fs.readFileSync(file, 'utf8'); +const quotedURL = `"${hostileURL}"`; +if (!source.includes(quotedURL)) { + throw new Error(`missing hostile URL fixture: ${hostileURL}`); +} +fs.writeFileSync(file, source.replace(quotedURL, '"https://removed.invalid"')); +NODE + expect_failure \ + "hostile endpoint fixture removed: $hostile_url" \ + "$fixture" \ + "materialized Nexus endpoint adversarial fixture" +done + +fixture="$(new_fixture evidence-redacted)" +sed -i.bak 's/1396110349/unknown/g' "$fixture/docs/iroha-production-send-readiness.md" +rm -f "$fixture/docs/iroha-production-send-readiness.md.bak" +expect_failure "release evidence redacted" "$fixture" "readiness evidence marker" + +fixture="$(new_fixture local-fix-promoted)" +sed -i.bak 's/unpublished correction/published correction/g' "$fixture/docs/iroha-production-send-readiness.md" +rm -f "$fixture/docs/iroha-production-send-readiness.md.bak" +expect_failure "local fix promoted" "$fixture" "unpublished correction" + +fixture="$(new_fixture capability-claim-regressed)" +sed -i.bak 's/is capability metadata, not a production-send/is production-send/' "$fixture/docs/universal-wallet-v2.md" +rm -f "$fixture/docs/universal-wallet-v2.md.bak" +expect_failure "capability claim regressed" "$fixture" "Universal Wallet non-enablement statement" + +fixture="$(new_fixture checklist-gate-removed)" +sed -i.bak 's/exact local\/Torii receipt-hash equality/local receipt hash/' "$fixture/docs/release-checklist.md" +rm -f "$fixture/docs/release-checklist.md.bak" +expect_failure "release checklist gate removed" "$fixture" "release checklist Iroha gate" + +fixture="$(new_fixture manifest-symlink)" +rm "$fixture/config/iroha-production-send-readiness.json" +ln -s ../docs/iroha-production-send-readiness.md "$fixture/config/iroha-production-send-readiness.json" +expect_failure "manifest symlink" "$fixture" "missing or is a symlink" + +fixture="$(new_fixture doc-symlink)" +rm "$fixture/docs/iroha-production-send-readiness.md" +ln -s universal-wallet-v2.md "$fixture/docs/iroha-production-send-readiness.md" +expect_failure "document symlink" "$fixture" "missing or is a symlink" + +echo "[iroha-send-readiness-test][ios] $negative_count negative/adversarial fixtures passed." diff --git a/scripts/test-release-signing-bootstrap.sh b/scripts/test-release-signing-bootstrap.sh new file mode 100755 index 0000000000..3efbe5df85 --- /dev/null +++ b/scripts/test-release-signing-bootstrap.sh @@ -0,0 +1,141 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +BOOTSTRAP="$ROOT_DIR/scripts/ci/run-release-signing-ci.sh" +TMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TMP_DIR"' EXIT +BIN_DIR="$TMP_DIR/bin" +PROFILE_DIR="$TMP_DIR/profiles" +OUTPUT_DIR="$TMP_DIR/output" +SECURITY_LOG="$TMP_DIR/security.log" +SECRET_SENTINEL="SUPER_SECRET_BOOTSTRAP_SENTINEL" + +fail() { + echo "[release-signing-bootstrap-test][error] $*" >&2 + exit 1 +} + +mkdir -p "$BIN_DIR" "$PROFILE_DIR" "$OUTPUT_DIR" + +cat >"$TMP_DIR/profile.plist" <<'PLIST' + + +UUID12345678-1234-1234-1234-123456789ABC +NameFearless App Store + +PLIST + +cat >"$BIN_DIR/security" <<'SCRIPT' +#!/usr/bin/env bash +set -euo pipefail +printf '%s\n' "$1" >>"$FAKE_SECURITY_LOG" +case "${1:-}" in + list-keychains) + if [[ "$*" == *" -s "* ]]; then + exit 0 + fi + printf ' "%s"\n' "$FAKE_ORIGINAL_KEYCHAIN" + ;; + create-keychain) + touch "${@: -1}" + ;; + set-keychain-settings|unlock-keychain|set-key-partition-list) + ;; + import) + if [[ "${FAKE_IMPORT_FAIL:-0}" == "1" ]]; then exit 71; fi + ;; + cms) + cat "$FAKE_PROFILE_PLIST" + ;; + delete-keychain) + rm -f -- "${@: -1}" + [[ "${FAKE_DELETE_FAIL:-0}" != "1" ]] + ;; + *) exit 64 ;; +esac +SCRIPT + +cat >"$BIN_DIR/gate" <<'SCRIPT' +#!/usr/bin/env bash +set -euo pipefail +[[ "$IOS_RELEASE_SIGNING_REQUIRED" == "1" ]] +[[ "$IOS_RELEASE_EXPECTED_PROFILE_UUID" == "12345678-1234-1234-1234-123456789ABC" ]] +[[ "$IOS_RELEASE_PROVISIONING_PROFILE_SPECIFIER" == "Fearless App Store" ]] +[[ -f "$IOS_RELEASE_PROFILE_DIRS/$IOS_RELEASE_EXPECTED_PROFILE_UUID.mobileprovision" ]] +if [[ "${FAKE_GATE_FAIL:-0}" == "1" ]]; then exit 72; fi +echo "[fake-release-signing-gate] PASSED" +SCRIPT +chmod +x "$BIN_DIR/security" "$BIN_DIR/gate" + +certificate_base64="$(printf 'certificate-fixture' | base64 | tr -d '\n')" +profile_base64="$(printf 'profile-fixture' | base64 | tr -d '\n')" + +run_bootstrap() { + rm -rf "$PROFILE_DIR" "$OUTPUT_DIR" + mkdir -p "$PROFILE_DIR" "$OUTPUT_DIR" + : >"$SECURITY_LOG" + GITHUB_ACTIONS="${TEST_GITHUB_ACTIONS:-true}" \ + GITHUB_EVENT_NAME="${TEST_GITHUB_EVENT_NAME:-push}" \ + GITHUB_REF="${TEST_GITHUB_REF:-refs/heads/develop}" \ + IOS_RELEASE_SIGNING_REQUIRED="${TEST_SIGNING_REQUIRED:-1}" \ + IOS_RELEASE_CERTIFICATE_P12_BASE64="${TEST_CERTIFICATE_BASE64-$certificate_base64}" \ + IOS_RELEASE_CERTIFICATE_PASSWORD="${TEST_CERTIFICATE_PASSWORD-$SECRET_SENTINEL}" \ + IOS_RELEASE_PROVISIONING_PROFILE_BASE64="${TEST_PROFILE_BASE64-$profile_base64}" \ + IOS_RELEASE_ROOT_DIR="$TMP_DIR" \ + IOS_RELEASE_GATE_BIN="$BIN_DIR/gate" \ + IOS_RELEASE_SECURITY_BIN="$BIN_DIR/security" \ + IOS_RELEASE_PLUTIL_BIN="/usr/bin/plutil" \ + IOS_RELEASE_BASE64_BIN="/usr/bin/base64" \ + IOS_RELEASE_OPENSSL_BIN="/usr/bin/openssl" \ + IOS_RELEASE_PROFILE_INSTALL_DIR="$PROFILE_DIR" \ + FAKE_SECURITY_LOG="$SECURITY_LOG" \ + FAKE_ORIGINAL_KEYCHAIN="$TMP_DIR/original.keychain-db" \ + FAKE_PROFILE_PLIST="$TMP_DIR/profile.plist" \ + FAKE_IMPORT_FAIL="${TEST_IMPORT_FAIL:-0}" \ + FAKE_DELETE_FAIL="${TEST_DELETE_FAIL:-0}" \ + FAKE_GATE_FAIL="${TEST_GATE_FAIL:-0}" \ + RUNNER_TEMP="$TMP_DIR" \ + "$BOOTSTRAP" >"$OUTPUT_DIR/stdout" 2>"$OUTPUT_DIR/stderr" +} + +assert_no_secret_leak() { + if grep -Fq "$SECRET_SENTINEL" "$OUTPUT_DIR/stdout" "$OUTPUT_DIR/stderr"; then + fail "$1 leaked signing material" + fi +} + +expect_failure() { + local label="$1" + if run_bootstrap; then + fail "$label was accepted" + fi + assert_no_secret_leak "$label" + if find "$PROFILE_DIR" -mindepth 1 -print -quit | grep -q .; then + fail "$label left an installed provisioning profile" + fi +} + +run_bootstrap || { + sed -n '1,80p' "$OUTPUT_DIR/stderr" >&2 + fail "valid trusted-push bootstrap was rejected" +} +grep -Fq 'PASSED' "$OUTPUT_DIR/stdout" || fail "valid bootstrap did not report success" +assert_no_secret_leak "valid bootstrap" +grep -Fxq 'delete-keychain' "$SECURITY_LOG" || fail "valid bootstrap did not delete its ephemeral keychain" +if find "$PROFILE_DIR" -mindepth 1 -print -quit | grep -q .; then + fail "valid bootstrap left an installed provisioning profile" +fi + +TEST_GITHUB_EVENT_NAME=pull_request expect_failure "pull-request signing bootstrap" +TEST_GITHUB_REF=refs/heads/feature/untrusted expect_failure "untrusted-branch signing bootstrap" +TEST_SIGNING_REQUIRED=0 expect_failure "optional signing on trusted push" +TEST_CERTIFICATE_BASE64='' TEST_CERTIFICATE_PASSWORD='' TEST_PROFILE_BASE64='' \ + expect_failure "missing signing secrets" +TEST_CERTIFICATE_PASSWORD='' expect_failure "partially configured signing secrets" +TEST_CERTIFICATE_BASE64='not_base64!' expect_failure "malformed certificate base64" +TEST_IMPORT_FAIL=1 expect_failure "certificate import failure" +TEST_GATE_FAIL=1 expect_failure "signed archive gate failure" +TEST_DELETE_FAIL=1 expect_failure "ephemeral keychain cleanup failure" + +echo "[release-signing-bootstrap-test] all behavioral fixtures passed" diff --git a/scripts/test-release-signing-gate-audit.sh b/scripts/test-release-signing-gate-audit.sh new file mode 100755 index 0000000000..cb77b3178f --- /dev/null +++ b/scripts/test-release-signing-gate-audit.sh @@ -0,0 +1,180 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +AUDIT="$ROOT_DIR/scripts/audit-release-signing-gate.sh" +TMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TMP_DIR"' EXIT + +fail() { + echo "[release-signing-gate-audit-test][error] $*" >&2 + exit 1 +} + +reset_fixture() { + rm -rf "$TMP_DIR/fixture" + mkdir -p "$TMP_DIR/fixture/scripts/ci" "$TMP_DIR/fixture/.github/workflows" \ + "$TMP_DIR/fixture/fearless.xcodeproj/xcshareddata/xcschemes" + cp "$ROOT_DIR/fearless.xcodeproj/xcshareddata/xcschemes/fearless.xcscheme" \ + "$TMP_DIR/fixture/fearless.xcodeproj/xcshareddata/xcschemes/fearless.xcscheme" + cp "$ROOT_DIR/.github/workflows/codecov.yml" \ + "$TMP_DIR/fixture/.github/workflows/codecov.yml" + cp "$ROOT_DIR/scripts/ci/run-pr.sh" "$TMP_DIR/fixture/scripts/ci/run-pr.sh" + cp "$ROOT_DIR/scripts/ci/run-release-signing-gate.sh" \ + "$TMP_DIR/fixture/scripts/ci/run-release-signing-gate.sh" + cp "$ROOT_DIR/scripts/ci/run-release-signing-ci.sh" \ + "$TMP_DIR/fixture/scripts/ci/run-release-signing-ci.sh" + cp "$ROOT_DIR/scripts/test-release-signing-bootstrap.sh" \ + "$TMP_DIR/fixture/scripts/test-release-signing-bootstrap.sh" +} + +run_audit() { + IOS_RELEASE_GATE_AUDIT_SCHEME_FILE="$TMP_DIR/fixture/fearless.xcodeproj/xcshareddata/xcschemes/fearless.xcscheme" \ + IOS_RELEASE_GATE_AUDIT_WORKFLOW_FILE="$TMP_DIR/fixture/.github/workflows/codecov.yml" \ + IOS_RELEASE_GATE_AUDIT_RUN_PR_FILE="$TMP_DIR/fixture/scripts/ci/run-pr.sh" \ + IOS_RELEASE_GATE_AUDIT_GATE_FILE="$TMP_DIR/fixture/scripts/ci/run-release-signing-gate.sh" \ + IOS_RELEASE_GATE_AUDIT_BOOTSTRAP_FILE="$TMP_DIR/fixture/scripts/ci/run-release-signing-ci.sh" \ + IOS_RELEASE_GATE_AUDIT_BOOTSTRAP_TEST_FILE="$TMP_DIR/fixture/scripts/test-release-signing-bootstrap.sh" \ + "$AUDIT" >/dev/null 2>&1 +} + +expect_failure() { + local label="$1" + if run_audit; then + fail "$label was accepted" + fi +} + +reset_fixture +run_audit || fail "valid release-signing gate was rejected" + +reset_fixture +perl -0pi -e 's/; then\n echo \\"error:/ || true; then\n echo \\"error:/' \ + "$TMP_DIR/fixture/fearless.xcodeproj/xcshareddata/xcschemes/fearless.xcscheme" +expect_failure "fail-open archive preaction" + +reset_fixture +perl -0pi -e 's/export STRICT_REQUIRED_PATCHES=1/export STRICT_REQUIRED_PATCHES=0/' \ + "$TMP_DIR/fixture/fearless.xcodeproj/xcshareddata/xcschemes/fearless.xcscheme" +expect_failure "non-strict archive dependency patching" + +reset_fixture +perl -0pi -e 's/(> \ + "$TMP_DIR/fixture/scripts/ci/run-release-signing-gate.sh" +expect_failure "direct signing-identity output" + +reset_fixture +printf '\nset -x\nprintenv\n' >> "$TMP_DIR/fixture/scripts/ci/run-release-signing-gate.sh" +expect_failure "signing environment dump" + +echo "[release-signing-gate-audit-test] all adversarial fixtures passed" diff --git a/scripts/test-release-signing-gate.sh b/scripts/test-release-signing-gate.sh new file mode 100755 index 0000000000..083052fad6 --- /dev/null +++ b/scripts/test-release-signing-gate.sh @@ -0,0 +1,335 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +GATE="$ROOT_DIR/scripts/ci/run-release-signing-gate.sh" +TMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TMP_DIR"' EXIT +FIXTURE="$TMP_DIR/fixture" +BIN_DIR="$TMP_DIR/bin" +PROFILE_DIR="$TMP_DIR/profiles" +OUTPUT="$TMP_DIR/output" +SECRET_SENTINEL="SUPER_SECRET_SIGNING_SENTINEL" + +fail() { + echo "[release-signing-gate-test][error] $*" >&2 + exit 1 +} + +mkdir -p "$FIXTURE/fearless.xcworkspace" "$FIXTURE/SourcePackages/checkouts" "$BIN_DIR" "$PROFILE_DIR" +touch "$PROFILE_DIR/release.mobileprovision" + +cat > "$FIXTURE/Info.plist" <<'PLIST' + + +CFBundleIdentifierjp.co.soramitsu.fearlesswallet + +PLIST + +cat > "$FIXTURE/entitlements.plist" <<'PLIST' + + +application-identifierYLWWUD25VZ.jp.co.soramitsu.fearlesswallet +com.apple.developer.team-identifierYLWWUD25VZ +com.apple.developer.associated-domains +applinks:fearlesswallet.iowebcredentials:fearlesswallet.io + +com.apple.developer.icloud-container-identifiers +iCloud.jp.co.soramitsu.fearlesswallet + +com.apple.developer.icloud-servicesCloudKit +com.apple.security.application-groups +group.jp.co.soramitsu.fearlesswallet + +get-task-allow +beta-reports-active + +PLIST + +cat > "$FIXTURE/profile.plist" <<'PLIST' + + +UUID12345678-1234-1234-1234-123456789ABC +NameFearless App Store +Entitlements +application-identifierYLWWUD25VZ.jp.co.soramitsu.fearlesswallet +com.apple.developer.team-identifierYLWWUD25VZ +com.apple.developer.associated-domains* +com.apple.developer.icloud-container-identifiers +iCloud.jp.co.soramitsu.fearlesswallet + +com.apple.developer.icloud-services* +com.apple.security.application-groups +group.jp.co.soramitsu.fearlesswallet + +get-task-allow +beta-reports-active + +PLIST + +cat > "$BIN_DIR/security" <<'SCRIPT' +#!/usr/bin/env bash +set -euo pipefail +if [[ "${1:-}" == "find-identity" ]]; then + if [[ "${FAKE_IDENTITY_AVAILABLE:-1}" == "1" ]]; then + echo " 1) AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA \"Apple Distribution: $SECRET_SENTINEL (YLWWUD25VZ)\"" + echo " 1 valid identities found" + else + echo " 0 valid identities found" + fi + exit 0 +fi +if [[ "${1:-}" == "cms" ]]; then + cat "$FAKE_PROFILE_PLIST" + exit 0 +fi +exit 64 +SCRIPT + +cat > "$BIN_DIR/codesign" <<'SCRIPT' +#!/usr/bin/env bash +set -euo pipefail +if [[ "${1:-}" == "--verify" ]]; then + [[ "${FAKE_CODESIGN_VERIFY_FAIL:-0}" != "1" ]] + exit +fi +if [[ "${1:-}" == "-d" ]]; then + cat "$FAKE_ENTITLEMENTS_PLIST" + exit 0 +fi +exit 64 +SCRIPT + +cat > "$BIN_DIR/xcodebuild" <<'SCRIPT' +#!/usr/bin/env bash +set -euo pipefail +archive_path="" +saw_manual_style=0 +saw_distribution_identity=0 +saw_profile_specifier=0 +while (($#)); do + if [[ "$1" == "-archivePath" ]]; then + shift + archive_path="$1" + elif [[ "$1" == "CODE_SIGN_STYLE=Manual" ]]; then + saw_manual_style=1 + elif [[ "$1" == "CODE_SIGN_IDENTITY=Apple Distribution" ]]; then + saw_distribution_identity=1 + elif [[ "$1" == "PROVISIONING_PROFILE_SPECIFIER=Fearless App Store" ]]; then + saw_profile_specifier=1 + fi + shift +done +[[ -n "$archive_path" ]] +[[ "$saw_manual_style" == "1" && "$saw_distribution_identity" == "1" && "$saw_profile_specifier" == "1" ]] +if [[ "${FAKE_XCODEBUILD_FAIL:-0}" == "1" ]]; then + echo "error: Signing Identity: $SECRET_SENTINEL certificate AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" >&2 + echo "** ARCHIVE FAILED **" >&2 + exit 65 +fi +app="$archive_path/Products/Applications/fearless.app" +mkdir -p "$app" +cp "$FAKE_INFO_PLIST" "$app/Info.plist" +cp "$FAKE_PROFILE_PLIST" "$app/embedded.mobileprovision" +unless_privacy="${FAKE_MISSING_PRIVACY:-0}" +if [[ "$unless_privacy" != "1" ]]; then + printf '%s\n' '' > "$app/PrivacyInfo.xcprivacy" +fi +SCRIPT + +chmod +x "$BIN_DIR/security" "$BIN_DIR/codesign" "$BIN_DIR/xcodebuild" + +run_gate() { + rm -rf "$OUTPUT" + mkdir -p "$OUTPUT" + SECRET_SENTINEL="$SECRET_SENTINEL" \ + FAKE_PROFILE_PLIST="${FAKE_PROFILE_PLIST:-$FIXTURE/profile.plist}" \ + FAKE_ENTITLEMENTS_PLIST="${FAKE_ENTITLEMENTS_PLIST:-$FIXTURE/entitlements.plist}" \ + FAKE_INFO_PLIST="${FAKE_INFO_PLIST:-$FIXTURE/Info.plist}" \ + IOS_RELEASE_ROOT_DIR="$FIXTURE" \ + IOS_RELEASE_WORKSPACE="$FIXTURE/fearless.xcworkspace" \ + IOS_RELEASE_SOURCE_PACKAGES_DIR="$FIXTURE/SourcePackages" \ + IOS_RELEASE_PROFILE_DIRS="${TEST_PROFILE_DIRS:-$PROFILE_DIR}" \ + IOS_RELEASE_SECURITY_BIN="$BIN_DIR/security" \ + IOS_RELEASE_XCODEBUILD_BIN="$BIN_DIR/xcodebuild" \ + IOS_RELEASE_CODESIGN_BIN="$BIN_DIR/codesign" \ + IOS_RELEASE_SIGNING_REQUIRED="${TEST_SIGNING_REQUIRED:-0}" \ + IOS_RELEASE_EXPECTED_PROFILE_UUID="${TEST_PROFILE_UUID:-12345678-1234-1234-1234-123456789ABC}" \ + IOS_RELEASE_PROVISIONING_PROFILE_SPECIFIER="${TEST_PROFILE_SPECIFIER:-Fearless App Store}" \ + FAKE_IDENTITY_AVAILABLE="${FAKE_IDENTITY_AVAILABLE:-1}" \ + FAKE_XCODEBUILD_FAIL="${FAKE_XCODEBUILD_FAIL:-0}" \ + FAKE_CODESIGN_VERIFY_FAIL="${FAKE_CODESIGN_VERIFY_FAIL:-0}" \ + FAKE_MISSING_PRIVACY="${FAKE_MISSING_PRIVACY:-0}" \ + GITHUB_ACTIONS="${TEST_GITHUB_ACTIONS:-false}" \ + GITHUB_EVENT_NAME="${TEST_GITHUB_EVENT_NAME:-}" \ + GITHUB_REF="${TEST_GITHUB_REF:-}" \ + "$GATE" >"$OUTPUT/stdout" 2>"$OUTPUT/stderr" +} + +expect_failure() { + local label="$1" + if run_gate; then + fail "$label was accepted" + fi + if grep -Fq "$SECRET_SENTINEL" "$OUTPUT/stdout" "$OUTPUT/stderr"; then + fail "$label leaked signing identity material" + fi +} + +if ! run_gate; then + sed -n '1,80p' "$OUTPUT/stderr" >&2 + fail "valid signed Release fixture was rejected" +fi +grep -Fq 'PASSED' "$OUTPUT/stdout" || fail "valid signed Release fixture did not report success" +if grep -Fq "$SECRET_SENTINEL" "$OUTPUT/stdout" "$OUTPUT/stderr"; then + fail "valid signed Release run leaked signing identity material" +fi + +cp "$FIXTURE/profile.plist" "$FIXTURE/profile-exact-authorizations.plist" +plutil -replace 'Entitlements.com\.apple\.developer\.associated-domains' \ + -xml 'applinks:fearlesswallet.iowebcredentials:fearlesswallet.io' \ + "$FIXTURE/profile-exact-authorizations.plist" +plutil -replace 'Entitlements.com\.apple\.developer\.icloud-services' \ + -xml 'CloudKit' \ + "$FIXTURE/profile-exact-authorizations.plist" +FAKE_PROFILE_PLIST="$FIXTURE/profile-exact-authorizations.plist" run_gate || fail \ + "exact-array profile authorizations were rejected" + +cp "$FIXTURE/profile.plist" "$FIXTURE/profile-array-wildcards.plist" +plutil -replace 'Entitlements.com\.apple\.developer\.associated-domains' \ + -xml '*' \ + "$FIXTURE/profile-array-wildcards.plist" +plutil -replace 'Entitlements.com\.apple\.developer\.icloud-services' \ + -xml '*' \ + "$FIXTURE/profile-array-wildcards.plist" +FAKE_PROFILE_PLIST="$FIXTURE/profile-array-wildcards.plist" run_gate || fail \ + "array wildcard profile authorizations were rejected" + +TEST_GITHUB_ACTIONS=true TEST_GITHUB_EVENT_NAME=push TEST_SIGNING_REQUIRED=1 \ + TEST_GITHUB_REF=refs/heads/develop run_gate || fail \ + "trusted release-branch push fixture was rejected" + +TEST_GITHUB_ACTIONS=true TEST_GITHUB_EVENT_NAME=pull_request \ + TEST_GITHUB_REF=refs/pull/1/merge expect_failure "pull-request signing attempt" + +TEST_GITHUB_ACTIONS=true TEST_GITHUB_EVENT_NAME=push \ + TEST_GITHUB_REF=refs/heads/feature/untrusted expect_failure \ + "non-release-branch signing attempt" + +TEST_SIGNING_REQUIRED=invalid expect_failure "invalid signing-required policy" + +FAKE_IDENTITY_AVAILABLE=0 TEST_PROFILE_DIRS="$TMP_DIR/no-profiles" run_gate || fail \ + "missing optional signing material did not skip cleanly" +grep -Fq 'SKIPPED' "$OUTPUT/stdout" || fail "missing optional signing material was not explicit" + +FAKE_IDENTITY_AVAILABLE=0 TEST_PROFILE_DIRS="$TMP_DIR/no-profiles" TEST_SIGNING_REQUIRED=1 \ + expect_failure "missing required signing material" + +FAKE_IDENTITY_AVAILABLE=1 TEST_PROFILE_DIRS="$TMP_DIR/no-profiles" \ + expect_failure "identity without matching profile" + +FAKE_IDENTITY_AVAILABLE=0 TEST_PROFILE_DIRS="$PROFILE_DIR" \ + expect_failure "profile without signing identity" + +FAKE_XCODEBUILD_FAIL=1 expect_failure "archive failure" +FAKE_CODESIGN_VERIFY_FAIL=1 expect_failure "invalid application signature" +FAKE_MISSING_PRIVACY=1 expect_failure "missing privacy manifest" + +cp "$FIXTURE/Info.plist" "$FIXTURE/Info-wrong-bundle.plist" +plutil -replace CFBundleIdentifier -string 'com.attacker.wallet' \ + "$FIXTURE/Info-wrong-bundle.plist" +FAKE_INFO_PLIST="$FIXTURE/Info-wrong-bundle.plist" \ + expect_failure "mismatched application bundle identifier" + +cp "$FIXTURE/entitlements.plist" "$FIXTURE/entitlements-bad-group.plist" +plutil -replace 'com\.apple\.security\.application-groups.0' -string \ + 'group.com.walletconnect.sdk' "$FIXTURE/entitlements-bad-group.plist" +FAKE_ENTITLEMENTS_PLIST="$FIXTURE/entitlements-bad-group.plist" \ + expect_failure "mismatched signed application group" + +cp "$FIXTURE/entitlements.plist" "$FIXTURE/entitlements-keychain.plist" +plutil -insert 'keychain-access-groups' -xml \ + 'group.com.walletconnect.sdk' \ + "$FIXTURE/entitlements-keychain.plist" +FAKE_ENTITLEMENTS_PLIST="$FIXTURE/entitlements-keychain.plist" \ + expect_failure "unreviewed signed keychain group" + +cp "$FIXTURE/entitlements.plist" "$FIXTURE/entitlements-task-mismatch.plist" +plutil -replace 'get-task-allow' -bool YES \ + "$FIXTURE/entitlements-task-mismatch.plist" +FAKE_ENTITLEMENTS_PLIST="$FIXTURE/entitlements-task-mismatch.plist" \ + expect_failure "signed/profile get-task-allow mismatch" + +cp "$FIXTURE/entitlements.plist" "$FIXTURE/entitlements-development.plist" +cp "$FIXTURE/profile.plist" "$FIXTURE/profile-development.plist" +plutil -replace 'get-task-allow' -bool YES "$FIXTURE/entitlements-development.plist" +plutil -replace 'Entitlements.get-task-allow' -bool YES "$FIXTURE/profile-development.plist" +FAKE_ENTITLEMENTS_PLIST="$FIXTURE/entitlements-development.plist" \ + FAKE_PROFILE_PLIST="$FIXTURE/profile-development.plist" \ + expect_failure "development-signed archive" + +cp "$FIXTURE/profile.plist" "$FIXTURE/profile-adhoc.plist" +plutil -insert 'ProvisionedDevices' -xml \ + '00000000-0000000000000000' \ + "$FIXTURE/profile-adhoc.plist" +FAKE_PROFILE_PLIST="$FIXTURE/profile-adhoc.plist" \ + expect_failure "ad-hoc provisioning profile" + +cp "$FIXTURE/entitlements.plist" "$FIXTURE/entitlements-no-beta.plist" +plutil -replace 'beta-reports-active' -bool NO "$FIXTURE/entitlements-no-beta.plist" +FAKE_ENTITLEMENTS_PLIST="$FIXTURE/entitlements-no-beta.plist" \ + expect_failure "signed archive without beta reporting" + +cp "$FIXTURE/profile.plist" "$FIXTURE/profile-no-beta.plist" +plutil -replace 'Entitlements.beta-reports-active' -bool NO "$FIXTURE/profile-no-beta.plist" +FAKE_PROFILE_PLIST="$FIXTURE/profile-no-beta.plist" \ + expect_failure "profile without beta reporting" + +cp "$FIXTURE/profile.plist" "$FIXTURE/profile-bad-domain.plist" +plutil -replace 'Entitlements.com\.apple\.developer\.associated-domains' \ + -xml 'webcredentials:fearlesswallet.io' \ + "$FIXTURE/profile-bad-domain.plist" +FAKE_PROFILE_PLIST="$FIXTURE/profile-bad-domain.plist" \ + expect_failure "profile missing the application-link domain" + +cp "$FIXTURE/profile.plist" "$FIXTURE/profile-near-wildcard-domain.plist" +plutil -replace 'Entitlements.com\.apple\.developer\.associated-domains' \ + -string '*evil' "$FIXTURE/profile-near-wildcard-domain.plist" +FAKE_PROFILE_PLIST="$FIXTURE/profile-near-wildcard-domain.plist" \ + expect_failure "profile with malformed associated-domain wildcard" + +cp "$FIXTURE/profile.plist" "$FIXTURE/profile-bad-icloud-service.plist" +plutil -replace 'Entitlements.com\.apple\.developer\.icloud-services' \ + -xml 'CloudDocuments' \ + "$FIXTURE/profile-bad-icloud-service.plist" +FAKE_PROFILE_PLIST="$FIXTURE/profile-bad-icloud-service.plist" \ + expect_failure "profile missing CloudKit authorization" + +cp "$FIXTURE/profile.plist" "$FIXTURE/profile-near-wildcard-service.plist" +plutil -replace 'Entitlements.com\.apple\.developer\.icloud-services' \ + -string '*evil' "$FIXTURE/profile-near-wildcard-service.plist" +FAKE_PROFILE_PLIST="$FIXTURE/profile-near-wildcard-service.plist" \ + expect_failure "profile with malformed iCloud-service wildcard" + +cp "$FIXTURE/entitlements.plist" "$FIXTURE/entitlements-extra-domain.plist" +plutil -insert 'com\.apple\.developer\.associated-domains.2' \ + -string 'applinks:attacker.example' \ + "$FIXTURE/entitlements-extra-domain.plist" +FAKE_ENTITLEMENTS_PLIST="$FIXTURE/entitlements-extra-domain.plist" \ + expect_failure "wildcard profile with an unreviewed signed associated domain" + +cp "$FIXTURE/entitlements.plist" "$FIXTURE/entitlements-extra-icloud-service.plist" +plutil -insert 'com\.apple\.developer\.icloud-services.1' \ + -string 'CloudDocuments' \ + "$FIXTURE/entitlements-extra-icloud-service.plist" +FAKE_ENTITLEMENTS_PLIST="$FIXTURE/entitlements-extra-icloud-service.plist" \ + expect_failure "wildcard profile with an unreviewed signed iCloud service" + +cp "$FIXTURE/profile.plist" "$FIXTURE/profile-bad-container.plist" +plutil -replace 'Entitlements.com\.apple\.developer\.icloud-container-identifiers' \ + -xml 'iCloud.jp.co.soramitsu.fearless' \ + "$FIXTURE/profile-bad-container.plist" +FAKE_PROFILE_PLIST="$FIXTURE/profile-bad-container.plist" \ + expect_failure "profile with stale CloudKit container" + +echo "[release-signing-gate-test] all behavioral fixtures passed" diff --git a/scripts/test-ton-production-send-readiness-audit.sh b/scripts/test-ton-production-send-readiness-audit.sh new file mode 100755 index 0000000000..99e4f9344e --- /dev/null +++ b/scripts/test-ton-production-send-readiness-audit.sh @@ -0,0 +1,491 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +AUDIT="$ROOT_DIR/scripts/audit-ton-production-send-readiness.sh" +TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/ton-send-readiness-test.XXXXXX")" +CASES=0 +EXPECTED_CASES=72 + +cleanup() { + rm -rf "$TMP_DIR" +} +trap cleanup EXIT INT TERM + +fail() { + echo "[ton-send-readiness-test][error] $*" >&2 + exit 1 +} + +sha256_file() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{print $1}' + else + shasum -a 256 "$1" | awk '{print $1}' + fi +} + +make_fixture() { + local fixture="$1" + mkdir -p \ + "$fixture/config" \ + "$fixture/docs" \ + "$fixture/fearless/Modules/Send" \ + "$fixture/fearless/Common/Services/ChainRegistry" \ + "$fixture/fearless/Common/Model" \ + "$fixture/scripts/ci" \ + "$fixture/.github/workflows" + cp "$ROOT_DIR/config/ton-production-send-readiness.json" "$fixture/config/" + cp "$ROOT_DIR/docs/ton-production-send-readiness.md" "$fixture/docs/" + cp "$ROOT_DIR/docs/release-checklist.md" "$fixture/docs/" + cp "$ROOT_DIR/fearless/Modules/Send/SendDependencyContainer.swift" "$fixture/fearless/Modules/Send/" + cp "$ROOT_DIR/fearless/Common/Services/ChainRegistry/ChainRegistry.swift" "$fixture/fearless/Common/Services/ChainRegistry/" + cp "$ROOT_DIR/fearless/Common/Model/TonTransferTransactionBuilder.swift" "$fixture/fearless/Common/Model/" + cp "$ROOT_DIR/fearless/Common/Model/TonSendService.swift" "$fixture/fearless/Common/Model/" + cp "$ROOT_DIR/scripts/ci/run-pr.sh" "$fixture/scripts/ci/" + cp "$ROOT_DIR/.github/workflows/codecov.yml" "$fixture/.github/workflows/" +} + +new_fixture() { + local name="$1" + local fixture="$TMP_DIR/$name" + make_fixture "$fixture" + printf '%s\n' "$fixture" +} + +set_json() { + local manifest="$1" + local field_path="$2" + local json_value="$3" + node - "$manifest" "$field_path" "$json_value" <<'NODE' +const fs = require('node:fs'); +const [path, fieldPath, encoded] = process.argv.slice(2); +const value = JSON.parse(encoded); +const document = JSON.parse(fs.readFileSync(path, 'utf8')); +const segments = fieldPath.split('.'); +let cursor = document; +for (const segment of segments.slice(0, -1)) cursor = cursor[segment]; +cursor[segments.at(-1)] = value; +fs.writeFileSync(path, `${JSON.stringify(document, null, 2)}\n`); +NODE +} + +mutate_swift_decoy() { + local file="$1" + local scenario="$2" + node - "$file" "$scenario" <<'NODE' +const fs = require('node:fs'); +const [path, scenario] = process.argv.slice(2); +const policy = 'static let production = TonProductionSendReleasePolicy(isEnabled: false)'; +const origin = 'static let canonicalAuthenticatedOrigin = URL(string: "https://tonapi.io")!'; +const allowlist = 'static let reviewedProductionSendOrigins = [canonicalAuthenticatedOrigin]'; +const enabledPolicy = 'static let production = TonProductionSendReleasePolicy(isEnabled: true)'; +const evilOrigin = 'static let canonicalAuthenticatedOrigin = URL(string: "https://evil.example")!'; +const expandedAllowlist = 'static let reviewedProductionSendOrigins = [canonicalAuthenticatedOrigin, URL(string: "https://evil.example")!]'; + +const mutations = { + 'policy-multiline-string': [policy, `let disabledPolicyDecoy = """ + ${policy} + """ + ${enabledPolicy}`], + 'policy-raw-string': [policy, `let disabledPolicyDecoy = #"${policy}"# + ${enabledPolicy}`], + 'policy-raw-multiline-string': [policy, `let disabledPolicyDecoy = #""" + ${policy} + """# + ${enabledPolicy}`], + 'policy-nested-comment': [policy, `/* outer disabled-policy decoy + /* nested decoy */ + ${policy} + */ + ${enabledPolicy}`], + 'policy-inactive-false': [policy, `#if false + ${policy} + #else + ${enabledPolicy} + #endif`], + 'policy-inactive-debug': [policy, `#if DEBUG + ${policy} + #else + ${enabledPolicy} + #endif`], + 'policy-unterminated-raw-string': [policy, `let disabledPolicyDecoy = #"${policy} + ${enabledPolicy}`], + 'origin-multiline-string': [origin, `let authenticatedOriginDecoy = """ + ${origin} + """ + ${evilOrigin}`], + 'origin-raw-string': [origin, `let authenticatedOriginDecoy = #"${origin}"# + ${evilOrigin}`], + 'origin-raw-multiline-string': [origin, `let authenticatedOriginDecoy = ##""" + ${origin} + """## + ${evilOrigin}`], + 'origin-nested-comment': [origin, `/* outer authenticated-origin decoy + /* nested decoy */ + ${origin} + */ + ${evilOrigin}`], + 'origin-inactive-false': [origin, `#if false + ${origin} + #else + ${evilOrigin} + #endif`], + 'origin-unterminated-nested-comment': [origin, `/* outer authenticated-origin decoy + /* nested decoy */ + ${origin} + ${evilOrigin}`], + 'allowlist-raw-string': [allowlist, `let reviewedAllowlistDecoy = #"${allowlist}"# + ${expandedAllowlist}`] +}; + +const mutation = mutations[scenario]; +if (!mutation) throw new Error(`unknown Swift decoy scenario: ${scenario}`); +const [needle, replacement] = mutation; +const source = fs.readFileSync(path, 'utf8'); +const first = source.indexOf(needle); +if (first === -1) throw new Error(`Swift mutation marker is missing for ${scenario}`); +if (source.indexOf(needle, first + needle.length) !== -1) { + throw new Error(`Swift mutation marker is ambiguous for ${scenario}`); +} +fs.writeFileSync(path, source.slice(0, first) + replacement + source.slice(first + needle.length)); +NODE +} + +expect_failure() { + local label="$1" + local fixture="$2" + local expected="$3" + local digest output status + digest="$(sha256_file "$fixture/config/ton-production-send-readiness.json")" + status=0 + output="$( + TON_SEND_AUDIT_ROOT="$fixture" \ + TON_SEND_AUDIT_EXPECTED_MANIFEST_SHA256="$digest" \ + bash "$AUDIT" 2>&1 + )" || status=$? + [[ "$status" -ne 0 ]] || fail "$label unexpectedly passed" + grep -Fq -- "$expected" <<<"$output" || + fail "$label failed for the wrong reason; expected '$expected', got: $output" + CASES=$((CASES + 1)) + echo "[ton-send-readiness-test] rejected $label" +} + +expect_failure_with_digest() { + local label="$1" + local fixture="$2" + local digest="$3" + local expected="$4" + local output status=0 + output="$( + TON_SEND_AUDIT_ROOT="$fixture" \ + TON_SEND_AUDIT_EXPECTED_MANIFEST_SHA256="$digest" \ + bash "$AUDIT" 2>&1 + )" || status=$? + [[ "$status" -ne 0 ]] || fail "$label unexpectedly passed" + grep -Fq -- "$expected" <<<"$output" || + fail "$label failed for the wrong reason; expected '$expected', got: $output" + CASES=$((CASES + 1)) + echo "[ton-send-readiness-test] rejected $label" +} + +baseline="$(new_fixture baseline)" +TON_SEND_AUDIT_ROOT="$baseline" bash "$AUDIT" >/dev/null || + fail "valid blocked baseline was rejected" +CASES=$((CASES + 1)) +echo "[ton-send-readiness-test] accepted valid blocked baseline" + +fixture="$(new_fixture release-enabled)" +set_json "$fixture/config/ton-production-send-readiness.json" releaseEnabled true +expect_failure "release-enabled manifest" "$fixture" "releaseEnabled must remain false" + +fixture="$(new_fixture ready-status)" +set_json "$fixture/config/ton-production-send-readiness.json" status '"ready"' +expect_failure "ready status claim" "$fixture" "status must remain blocked" + +fixture="$(new_fixture runtime-initializer-enabled)" +set_json "$fixture/config/ton-production-send-readiness.json" runtimeGuard.productionInitializer '"isEnabled: true"' +expect_failure "enabled manifest runtime initializer" "$fixture" "production initializer must remain disabled" + +fixture="$(new_fixture runtime-override)" +set_json "$fixture/config/ton-production-send-readiness.json" runtimeGuard.runtimeOverrideAllowed true +expect_failure "runtime override claim" "$fixture" "runtime override must remain forbidden" + +fixture="$(new_fixture parity-claim)" +set_json "$fixture/config/ton-production-send-readiness.json" feeEvidence.tonApiParityEvidence '"fixture-only"' +expect_failure "unreviewed fee-parity claim" "$fixture" "independent TonAPI fee parity evidence must remain missing until review" + +fixture="$(new_fixture replacement-claim)" +set_json "$fixture/config/ton-production-send-readiness.json" feeEvidence.reviewedAttestedReplacement '"local-tvm"' +expect_failure "unattested local replacement claim" "$fixture" "a local replacement must not be claimed without reviewed attestation" + +fixture="$(new_fixture fee-criterion)" +set_json "$fixture/config/ton-production-send-readiness.json" feeEvidence.criterionSatisfied true +expect_failure "fee criterion claim" "$fixture" "fee evidence criterion must remain unsatisfied" + +fixture="$(new_fixture unsigned-endpoint)" +set_json "$fixture/config/ton-production-send-readiness.json" feeEvidence.unsignedEndpoint '"/v2/wallet/emulate"' +expect_failure "unsigned endpoint drift" "$fixture" "unsigned emulation endpoint drifted" + +fixture="$(new_fixture signed-endpoint)" +set_json "$fixture/config/ton-production-send-readiness.json" feeEvidence.signedEndpoint '"/v2/traces/emulate"' +expect_failure "signed endpoint drift" "$fixture" "signed emulation endpoint drifted" + +fixture="$(new_fixture wallet-version)" +set_json "$fixture/config/ton-production-send-readiness.json" feeEvidence.walletVersion '"Wallet V5R1"' +expect_failure "wallet version drift" "$fixture" "fee evidence wallet version drifted" + +fixture="$(new_fixture credential-origin)" +set_json "$fixture/config/ton-production-send-readiness.json" endpointCredentialEvidence.requiredOrigin '"https://evil.example"' +expect_failure "authenticated origin drift" "$fixture" "required authenticated origin drifted" + +fixture="$(new_fixture credential-provisioned)" +set_json "$fixture/config/ton-production-send-readiness.json" endpointCredentialEvidence.releaseProvisioningAttestation '"provisioned"' +expect_failure "unreviewed credential provisioning claim" "$fixture" "release credential provisioning must remain unattested" + +fixture="$(new_fixture registry-attested)" +set_json "$fixture/config/ton-production-send-readiness.json" endpointCredentialEvidence.registryOriginAttestation '"reviewed"' +expect_failure "unreviewed registry-origin claim" "$fixture" "registry origin must remain unattested" + +fixture="$(new_fixture credential-criterion)" +set_json "$fixture/config/ton-production-send-readiness.json" endpointCredentialEvidence.criterionSatisfied true +expect_failure "credential criterion claim" "$fixture" "endpoint credential criterion must remain unsatisfied" + +fixture="$(new_fixture funded-bundle)" +set_json "$fixture/config/ton-production-send-readiness.json" fundedMainnetEvidence.evidenceBundle '"screenshot.zip"' +expect_failure "fixture funded bundle claim" "$fixture" "funded mainnet evidence must remain missing" + +fixture="$(new_fixture funded-hash)" +set_json "$fixture/config/ton-production-send-readiness.json" fundedMainnetEvidence.transactionHash '"0000000000000000000000000000000000000000000000000000000000000000"' +expect_failure "placeholder funded transaction hash" "$fixture" "no funded-mainnet transaction hash may be claimed" + +fixture="$(new_fixture funded-stages)" +set_json "$fixture/config/ton-production-send-readiness.json" fundedMainnetEvidence.requiredStages '["broadcast"]' +expect_failure "incomplete funded evidence stages" "$fixture" "funded mainnet required stages drifted" + +fixture="$(new_fixture funded-criterion)" +set_json "$fixture/config/ton-production-send-readiness.json" fundedMainnetEvidence.criterionSatisfied true +expect_failure "funded criterion claim" "$fixture" "funded mainnet criterion must remain unsatisfied" + +fixture="$(new_fixture finalized-procedure)" +set_json "$fixture/config/ton-production-send-readiness.json" expiredIntentRecoveryEvidence.finalizedChainAbsenceProcedure '"draft"' +expect_failure "unreviewed finalized-absence procedure claim" "$fixture" "finalized-chain absence procedure must remain missing" + +fixture="$(new_fixture quarantine-procedure)" +set_json "$fixture/config/ton-production-send-readiness.json" expiredIntentRecoveryEvidence.auditedQuarantineProcedure '"draft"' +expect_failure "unreviewed quarantine procedure claim" "$fixture" "audited quarantine procedure must remain missing" + +fixture="$(new_fixture selected-procedure)" +set_json "$fixture/config/ton-production-send-readiness.json" expiredIntentRecoveryEvidence.selectedProcedure '"quarantine"' +expect_failure "unreviewed recovery selection" "$fixture" "no unreviewed expired-intent procedure may be selected" + +fixture="$(new_fixture sender-unblock)" +set_json "$fixture/config/ton-production-send-readiness.json" expiredIntentRecoveryEvidence.senderUnblockAllowed true +expect_failure "sender unblock claim" "$fixture" "sender unblock must remain forbidden" + +fixture="$(new_fixture recovery-criterion)" +set_json "$fixture/config/ton-production-send-readiness.json" expiredIntentRecoveryEvidence.criterionSatisfied true +expect_failure "recovery criterion claim" "$fixture" "expired-intent recovery criterion must remain unsatisfied" + +fixture="$(new_fixture evidence-bundle-id)" +set_json "$fixture/config/ton-production-send-readiness.json" releaseEvidenceBundle.immutableBundleId '"unreviewed"' +expect_failure "unreviewed immutable bundle claim" "$fixture" "immutable evidence bundle ID must remain absent" + +fixture="$(new_fixture evidence-reviewers)" +set_json "$fixture/config/ton-production-send-readiness.json" releaseEvidenceBundle.reviewers '["self"]' +expect_failure "self-reviewed evidence claim" "$fixture" "release evidence reviewers drifted" + +fixture="$(new_fixture all-criteria)" +set_json "$fixture/config/ton-production-send-readiness.json" releaseEvidenceBundle.allCriteriaSatisfied true +expect_failure "all-criteria claim" "$fixture" "release evidence bundle must remain incomplete" + +fixture="$(new_fixture blocker-code)" +set_json "$fixture/config/ton-production-send-readiness.json" blocker.code '"ready"' +expect_failure "blocker-code removal" "$fixture" "blocker code drifted" + +fixture="$(new_fixture exit-criteria)" +set_json "$fixture/config/ton-production-send-readiness.json" exitCriteria '[]' +expect_failure "removed exit criteria" "$fixture" "exit criteria drifted" + +fixture="$(new_fixture unknown-key)" +set_json "$fixture/config/ton-production-send-readiness.json" unexpected '"decoy"' +expect_failure "unknown manifest key" "$fixture" "top-level manifest keys drifted" + +fixture="$(new_fixture invalid-json)" +printf '{invalid\n' > "$fixture/config/ton-production-send-readiness.json" +expect_failure "invalid JSON" "$fixture" "invalid readiness JSON" + +fixture="$(new_fixture oversized-json)" +dd if=/dev/zero bs=33000 count=1 2>/dev/null | tr '\0' ' ' >> "$fixture/config/ton-production-send-readiness.json" +expect_failure "oversized manifest" "$fixture" "readiness manifest exceeds 32 KiB" + +fixture="$(new_fixture digest-mismatch)" +set_json "$fixture/config/ton-production-send-readiness.json" assessedAt '"2026-07-17"' +expect_failure_with_digest "unpinned manifest mutation" "$fixture" \ + "$(sha256_file "$ROOT_DIR/config/ton-production-send-readiness.json")" \ + "readiness manifest digest mismatch" + +fixture="$(new_fixture invalid-expected-digest)" +expect_failure_with_digest "invalid expected digest" "$fixture" "NOT-A-DIGEST" \ + "expected manifest digest must be exactly 64 lowercase hexadecimal characters" + +fixture="$(new_fixture manifest-symlink)" +rm "$fixture/config/ton-production-send-readiness.json" +ln -s ../docs/ton-production-send-readiness.md "$fixture/config/ton-production-send-readiness.json" +expect_failure_with_digest "symlinked manifest" "$fixture" \ + "$(printf '0%.0s' {1..64})" \ + "required regular file is missing or is a symlink" + +fixture="$(new_fixture document-symlink)" +rm "$fixture/docs/ton-production-send-readiness.md" +ln -s release-checklist.md "$fixture/docs/ton-production-send-readiness.md" +expect_failure "symlinked readiness document" "$fixture" "required regular file is missing or is a symlink" + +fixture="$(new_fixture document-status)" +perl -0pi -e 's/Status: \*\*BLOCKED \/ fail closed\*\*\./Status: READY./' "$fixture/docs/ton-production-send-readiness.md" +expect_failure "removed blocked document status" "$fixture" "blocked TON readiness status" + +fixture="$(new_fixture document-replacement)" +perl -0pi -e 's/reviewed and attested local-TVM or quote mechanism/local fallback/' "$fixture/docs/ton-production-send-readiness.md" +expect_failure "weakened replacement documentation" "$fixture" "reviewed attested replacement requirement" + +fixture="$(new_fixture checklist-command)" +perl -0pi -e 's/bash \.\/scripts\/test-ton-production-send-readiness-audit\.sh && bash \.\/scripts\/audit-ton-production-send-readiness\.sh/echo skipped-ton-gate/' "$fixture/docs/release-checklist.md" +expect_failure "removed release-checklist command" "$fixture" "release-checklist TON readiness command" + +fixture="$(new_fixture checklist-parity)" +perl -0pi -e 's/exact fee parity with the corresponding signed Wallet V4R2/fees are assumed to match/' "$fixture/docs/release-checklist.md" +expect_failure "weakened checklist fee parity" "$fixture" "release-checklist exact fee parity requirement" + +fixture="$(new_fixture checklist-finality)" +perl -0pi -e 's/finalized-chain absence proof or explicit audited quarantine/local timeout/' "$fixture/docs/release-checklist.md" +expect_failure "weakened checklist recovery" "$fixture" "release-checklist finalized-absence or quarantine requirement" + +fixture="$(new_fixture run-pr-self-test)" +perl -0pi -e 's/bash "\$WORKSPACE_DIR\/scripts\/test-ton-production-send-readiness-audit\.sh"/echo removed-ton-self-test/' "$fixture/scripts/ci/run-pr.sh" +expect_failure "removed PR CI self-test" "$fixture" "PR CI TON readiness adversarial self-test" + +fixture="$(new_fixture run-pr-audit)" +perl -0pi -e 's/bash "\$WORKSPACE_DIR\/scripts\/audit-ton-production-send-readiness\.sh"/echo removed-ton-audit/' "$fixture/scripts/ci/run-pr.sh" +expect_failure "removed PR CI audit" "$fixture" "PR CI TON readiness audit" + +fixture="$(new_fixture workflow-self-test)" +perl -0pi -e 's/bash \.\/scripts\/test-ton-production-send-readiness-audit\.sh/echo removed-ton-self-test/' "$fixture/.github/workflows/codecov.yml" +expect_failure "removed GitHub CI self-test" "$fixture" "GitHub CI TON readiness adversarial self-test" + +fixture="$(new_fixture workflow-audit)" +perl -0pi -e 's/bash \.\/scripts\/audit-ton-production-send-readiness\.sh/echo removed-ton-audit/' "$fixture/.github/workflows/codecov.yml" +expect_failure "removed GitHub CI audit" "$fixture" "GitHub CI TON readiness audit" + +fixture="$(new_fixture production-policy-enabled)" +perl -0pi -e 's/static let production = TonProductionSendReleasePolicy\(isEnabled: false\)/static let production = TonProductionSendReleasePolicy(isEnabled: true)/' "$fixture/fearless/Modules/Send/SendDependencyContainer.swift" +expect_failure "enabled Swift production policy" "$fixture" "TON production policy must have exactly one active false initializer" + +fixture="$(new_fixture public-policy-initializer)" +perl -0pi -e 's/private init\(isEnabled: Bool\)/init(isEnabled: Bool)/' "$fixture/fearless/Modules/Send/SendDependencyContainer.swift" +expect_failure "public Swift policy initializer" "$fixture" "TON release-policy initializer must remain private" + +fixture="$(new_fixture escaped-debug-policy)" +perl -0pi -e 's/#if DEBUG\n static let enabledForTests/#if RELEASE\n static let enabledForTests/' "$fixture/fearless/Modules/Send/SendDependencyContainer.swift" +expect_failure "test policy outside DEBUG" "$fixture" "TON enabledForTests policy escaped the DEBUG block" + +fixture="$(new_fixture comment-decoy-policy)" +perl -0pi -e 's/static let production = TonProductionSendReleasePolicy\(isEnabled: false\)/\/\/ static let production = TonProductionSendReleasePolicy(isEnabled: false)\n static let production = TonProductionSendReleasePolicy(isEnabled: true)/' "$fixture/fearless/Modules/Send/SendDependencyContainer.swift" +expect_failure "comment-decoy disabled policy" "$fixture" "TON production policy must have exactly one active false initializer" + +fixture="$(new_fixture multiline-string-policy-decoy)" +mutate_swift_decoy "$fixture/fearless/Modules/Send/SendDependencyContainer.swift" policy-multiline-string +expect_failure "multiline-string disabled-policy decoy" "$fixture" "TON production policy must have exactly one active false initializer" + +fixture="$(new_fixture raw-string-policy-decoy)" +mutate_swift_decoy "$fixture/fearless/Modules/Send/SendDependencyContainer.swift" policy-raw-string +expect_failure "raw-string disabled-policy decoy" "$fixture" "TON production policy must have exactly one active false initializer" + +fixture="$(new_fixture raw-multiline-policy-decoy)" +mutate_swift_decoy "$fixture/fearless/Modules/Send/SendDependencyContainer.swift" policy-raw-multiline-string +expect_failure "raw-multiline-string disabled-policy decoy" "$fixture" "TON production policy must have exactly one active false initializer" + +fixture="$(new_fixture nested-comment-policy-decoy)" +mutate_swift_decoy "$fixture/fearless/Modules/Send/SendDependencyContainer.swift" policy-nested-comment +expect_failure "nested-comment disabled-policy decoy" "$fixture" "TON production policy must have exactly one active false initializer" + +fixture="$(new_fixture inactive-false-policy-decoy)" +mutate_swift_decoy "$fixture/fearless/Modules/Send/SendDependencyContainer.swift" policy-inactive-false +expect_failure "inactive-false disabled-policy decoy" "$fixture" "TON production policy must have exactly one active false initializer" + +fixture="$(new_fixture inactive-debug-policy-decoy)" +mutate_swift_decoy "$fixture/fearless/Modules/Send/SendDependencyContainer.swift" policy-inactive-debug +expect_failure "inactive-DEBUG disabled-policy decoy" "$fixture" "TON production policy must have exactly one active false initializer" + +fixture="$(new_fixture unterminated-raw-policy-decoy)" +mutate_swift_decoy "$fixture/fearless/Modules/Send/SendDependencyContainer.swift" policy-unterminated-raw-string +expect_failure "unterminated raw policy string" "$fixture" "unterminated Swift string literal" + +fixture="$(new_fixture canonical-origin-source)" +perl -0pi -e 's#https://tonapi\.io#https://evil.example#' "$fixture/fearless/Common/Services/ChainRegistry/ChainRegistry.swift" +expect_failure "source canonical-origin drift" "$fixture" "canonical authenticated TonAPI origin drifted" + +fixture="$(new_fixture multiline-string-origin-decoy)" +mutate_swift_decoy "$fixture/fearless/Common/Services/ChainRegistry/ChainRegistry.swift" origin-multiline-string +expect_failure "multiline-string authenticated-origin decoy" "$fixture" "canonical authenticated TonAPI origin drifted" + +fixture="$(new_fixture raw-string-origin-decoy)" +mutate_swift_decoy "$fixture/fearless/Common/Services/ChainRegistry/ChainRegistry.swift" origin-raw-string +expect_failure "raw-string authenticated-origin decoy" "$fixture" "canonical authenticated TonAPI origin drifted" + +fixture="$(new_fixture raw-multiline-origin-decoy)" +mutate_swift_decoy "$fixture/fearless/Common/Services/ChainRegistry/ChainRegistry.swift" origin-raw-multiline-string +expect_failure "raw-multiline-string authenticated-origin decoy" "$fixture" "canonical authenticated TonAPI origin drifted" + +fixture="$(new_fixture nested-comment-origin-decoy)" +mutate_swift_decoy "$fixture/fearless/Common/Services/ChainRegistry/ChainRegistry.swift" origin-nested-comment +expect_failure "nested-comment authenticated-origin decoy" "$fixture" "canonical authenticated TonAPI origin drifted" + +fixture="$(new_fixture inactive-origin-decoy)" +mutate_swift_decoy "$fixture/fearless/Common/Services/ChainRegistry/ChainRegistry.swift" origin-inactive-false +expect_failure "inactive-branch authenticated-origin decoy" "$fixture" "canonical authenticated TonAPI origin drifted" + +fixture="$(new_fixture unterminated-origin-comment)" +mutate_swift_decoy "$fixture/fearless/Common/Services/ChainRegistry/ChainRegistry.swift" origin-unterminated-nested-comment +expect_failure "unterminated nested origin comment" "$fixture" "unterminated nested block comment" + +fixture="$(new_fixture expanded-origin-allowlist)" +perl -0pi -e 's/\[canonicalAuthenticatedOrigin\]/[canonicalAuthenticatedOrigin, URL(string: "https:\/\/evil.example")!]/' "$fixture/fearless/Common/Services/ChainRegistry/ChainRegistry.swift" +expect_failure "expanded send-origin allowlist" "$fixture" "reviewed TonAPI send-origin allowlist drifted" + +fixture="$(new_fixture raw-string-allowlist-decoy)" +mutate_swift_decoy "$fixture/fearless/Common/Services/ChainRegistry/ChainRegistry.swift" allowlist-raw-string +expect_failure "raw-string reviewed-allowlist decoy" "$fixture" "reviewed TonAPI send-origin allowlist drifted" + +fixture="$(new_fixture credential-length)" +perl -0pi -e 's/bytes\.count <= 4096/bytes.count <= 8192/' "$fixture/fearless/Common/Services/ChainRegistry/ChainRegistry.swift" +expect_failure "relaxed credential length" "$fixture" "TonAPI release credential validation contract drifted" + +fixture="$(new_fixture credential-ascii)" +perl -0pi -e 's/\(0x21 \.\.\. 0x7E\)/\(0x20 ... 0x7E\)/' "$fixture/fearless/Common/Services/ChainRegistry/ChainRegistry.swift" +expect_failure "credential whitespace acceptance" "$fixture" "TonAPI release credential validation contract drifted" + +fixture="$(new_fixture credential-origin-binding)" +perl -0pi -e 's/usesAuthorization && Self\.isReviewedProductionSendServerURL\(tonAPIURL\)/Self.isReviewedProductionSendServerURL(tonAPIURL)/' "$fixture/fearless/Common/Services/ChainRegistry/ChainRegistry.swift" +expect_failure "missing credential/origin binding" "$fixture" "TonAPI signed-operation credential/origin binding drifted" + +fixture="$(new_fixture unsigned-signature-check)" +perl -0pi -e 's/ignore_signature_check: true/ignore_signature_check: false/' "$fixture/fearless/Common/Model/TonSendService.swift" +expect_failure "unsigned emulation signature-bypass drift" "$fixture" "TonAPI unsigned signature-bypass emulation seam drifted" + +fixture="$(new_fixture signed-endpoint-guard)" +perl -0pi -e 's/(func emulateSigned\([\s\S]{0,300}?)try requireTrustedSignedOperationEndpoint\(\)/$1/' "$fixture/fearless/Common/Model/TonSendService.swift" +expect_failure "removed signed-emulation endpoint guard" "$fixture" "TonAPI signed Wallet V4R2 emulation seam drifted" + +fixture="$(new_fixture signed-fee-drift)" +perl -0pi -e 's/emulation\.totalFeeNanotons != feeQuote\.feeNanotons/emulation.totalFeeNanotons == feeQuote.feeNanotons/' "$fixture/fearless/Common/Model/TonSendService.swift" +expect_failure "reversed signed fee-drift guard" "$fixture" "signed-emulation fee drift must fail closed" + +fixture="$(new_fixture unsigned-builder)" +perl -0pi -e 's/static func buildForFeeEstimation\(/static func buildAdvisoryEstimate\(/' "$fixture/fearless/Common/Model/TonTransferTransactionBuilder.swift" +expect_failure "removed unsigned Wallet V4R2 builder" "$fixture" "Wallet V4R2 unsigned fee-estimation builder drifted" + +if ((CASES != EXPECTED_CASES)); then + fail "executed $CASES fixtures; expected exactly $EXPECTED_CASES" +fi +echo "[ton-send-readiness-test] all $CASES blocked-contract and adversarial fixtures passed" diff --git a/scripts/test-transaction-builder-tests-audit.sh b/scripts/test-transaction-builder-tests-audit.sh index e9a7812948..eb3a48bb42 100755 --- a/scripts/test-transaction-builder-tests-audit.sh +++ b/scripts/test-transaction-builder-tests-audit.sh @@ -3,6 +3,8 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" AUDIT_SCRIPT="$SCRIPT_DIR/audit-transaction-builder-tests.sh" +EXPECTED_DESTRUCTIVE_FIXTURES=120 +executed_destructive_fixtures=0 fail() { echo "[ios-transaction-builder-audit-test][error] $*" >&2 @@ -36,23 +38,393 @@ write_valid_fixture() { " func testBuildsTokenSendWithIdempotentAssociatedTokenAccountCreateThenTransferChecked() {}" \ "}" + write_file "$root/fearless/Common/Model/TonTransferTransactionBuilder.swift" \ + "struct TonSignedExternalMessage { let signingPayloadHashHex: String; let publicKey: Data }" \ + "struct TonUnsignedEmulationMessage { let signingPayloadHashHex: String; let publicKey: Data }" \ + "let maximumAddressInputBytes = 128" \ + "let maximumAmountDigits = 37" \ + "let maximumCommentBytes = 256" \ + "enum TonTransferTransactionBuilder {" \ + " static func buildForFeeEstimation() {}" \ + " static func rebuildSignedMessage() {}" \ + " static func inspectSignedMessage() {}" \ + "}" \ + "final class TonCapturingTransferSigner {}" \ + "struct TonFixedSignatureSigner {}" + + write_file "$root/fearless/Common/Model/TonTransferFeeQuote.swift" \ + "struct TonTransferFeeQuote: Equatable, Sendable {" \ + " static let maximumAgeSeconds: UInt64 = 30" \ + ' let domain = Data("fearless.ton.fee-quote.v1\0".utf8)' \ + " let unsignedMessage: TonUnsignedEmulationMessage" \ + " init() {" \ + " _ = TonAPIClientFactory.isReviewedProductionSendServerURL(endpointURL)" \ + " _ = unsignedMessage.signingPayloadHashHex" \ + " _ = SHA256.hash(data: unsignedMessage.boc)" \ + " }" \ + " func validate(at now: UInt64, endpointOrigin: String?) {}" \ + "}" + + write_file "$root/fearless/Common/Model/TonPendingIntentJournal.swift" \ + "private extension TonPendingSignedIntent {" \ + " var journalPhaseRank: Int { confirmed ? 2 : 1 }" \ + " func hasSameBearer(as other: TonPendingSignedIntent) -> Bool { identity == other.identity }" \ + "}" \ + "final class TonKeychainPendingIntentJournal {}" \ + 'let identifierPrefix = "jp.co.soramitsu.fearless.ton.pending.v1."' \ + "let maximumRecordBytes = 48 * 1024" \ + "encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes]" \ + "let valid = publicKey.isValidSignature(inspection.signature, for: signingHash)" \ + "let rebuiltSigned = TonTransferTransactionBuilder.rebuildSignedMessage()" \ + "guard rebuiltSigned.boc == signedBoc else { return }" \ + "guard let quote = pending.feeQuote else { return }" \ + 'let phase = "confirmed"' \ + "guard pending.hasSameBearer(as: existing)," \ + " pending.journalPhaseRank >= existing.journalPhaseRank else { return }" \ + "guard pending.hasSameBearer(as: existing)," \ + " pending.journalPhaseRank >= existing.journalPhaseRank else { return }" + + write_file "$root/fearless/Common/Model/TonSendService.swift" \ + "protocol TonTransferRemoteProtocol {" \ + " func emulateUnsigned()" \ + " func emulateSigned()" \ + " func reconcile()" \ + "}" \ + "enum TonSendServiceError { case productionSendDisabled; case feeQuoteRequired; case broadcastOutcomeUnknown(messageHashHex: String); case priorIntentConfirmed(identity: TonTransferIntentIdentity, messageHashHex: String) }" \ + "enum TonTransferRemoteError { case untrustedSignedOperationEndpoint }" \ + "actor TonPendingIntentCoordinator {" \ + " static let shared = TonPendingIntentCoordinator(" \ + " productionJournal: TonKeychainPendingIntentJournal()" \ + " )" \ + " func recover() throws {" \ + " _ = try journal.load(senderRaw: identity.sender)" \ + " try journal.save(pending)" \ + " statesBySender[pending.identity.sender] = .pending(pending)" \ + " try journal.delete(senderRaw: identity.sender)" \ + " }" \ + " func recordConfirmed(_ pending: TonPendingSignedIntent) throws {}" \ + " func retainPending(_ pending: TonPendingSignedIntent) throws {" \ + " let retained: TonPendingSignedIntent" \ + " if storedPending.confirmed ||" \ + " (storedPending.emulation != nil && pending.emulation == nil) {" \ + " retained = storedPending" \ + " } else {" \ + " retained = pending" \ + " }" \ + " try journal.save(retained)" \ + " statesBySender[pending.identity.sender] = .pending(retained)" \ + " }" \ + " func acknowledgeConfirmed(senderRaw: String, identity: TonTransferIntentIdentity, messageHashHex: String) throws {" \ + " guard pending.confirmed," \ + " pending.identity == identity," \ + " pending.message.messageHashHex == messageHashHex else { return }" \ + " }" \ + "}" \ + "func acknowledgeConfirmedTransfer(senderAddress: String, identity: TonTransferIntentIdentity) throws {" \ + " guard let sender = try? TonSwift.Address.parse(senderAddress)," \ + " sender.toRaw() == identity.sender else { return }" \ + "}" \ + "func requireStoredEndpointForRecovery() throws {" \ + " guard storedOrigin == currentOrigin else { return }" \ + "}" \ + "func performSend() async throws {" \ + " var pending = try await pendingCoordinator.begin(identity)" \ + " if requiresFeeQuote {" \ + " return try await recoverPendingWithoutRebroadcast(pending)" \ + " }" \ + " let credentials = try signingCredentials()" \ + " pending = try await pendingCoordinator.recordConfirmed(pending)" \ + "}" \ + "func quote(_ request: TonNativeEstimateRequest) {}" \ + "let required = TonSendServiceError.feeQuoteRequired" \ + "performSend(requiresFeeQuote: true)" \ + "guard rebuiltUnsigned == feeQuote.unsignedMessage else { return }" \ + "guard walletState == feeQuote.walletState else { return }" \ + "guard signedMessage.signingPayloadHashHex == feeQuote.unsignedMessage.signingPayloadHashHex else { return }" \ + "guard emulation.totalFeeNanotons != feeQuote.feeNanotons else { return }" \ + "guard request.amountNanotons.utf8.count <= 19 else { return }" \ + "guard comment.utf8.count > TonTransferTransactionBuilder.maximumCommentBytes else { return }" \ + "try requireTrustedSignedOperationEndpoint()" \ + "try requireTrustedSignedOperationEndpoint()" \ + "try requireTrustedSignedOperationEndpoint()" \ + "#if DEBUG" \ + "init(trustedTestClient client: any APIProtocol) {}" \ + "#endif" \ + "#if DEBUG" \ + "/// Explicitly test-only compatibility seam." \ + "/// Application integration requires a quote." \ + "func sendUnquotedForTesting() {}" \ + "#endif" \ + "#if !DEBUG" \ + "throw TonSendServiceError.productionSendDisabled" \ + "#else" \ + "let debugOnlyTonSend = true" \ + "#endif" + + write_file "$root/fearless/Common/Model/ChainRegistry/ChainModel.swift" \ + "let canonicalTonChainIds = [\"-239\", \"ton:mainnet\"]" + + write_file "$root/fearless/Common/Services/ChainRegistry/ChainRegistry.swift" \ + "static let canonicalAuthenticatedOrigin = URL(string: \"https://tonapi.io\")!" \ + "static let reviewedProductionSendOrigins = [canonicalAuthenticatedOrigin]" \ + "static func isReviewedProductionSendServerURL(_ url: URL) -> Bool { true }" \ + "static func isValidAuthorizationToken(_ token: String) -> Bool {" \ + " token.utf8.allSatisfy { byte in (0x21 ... 0x7E).contains(byte) }" \ + "}" \ + "let hasReviewedProductionSendCredential = true" \ + "guard TonAPIClientFactory.isReviewedProductionSendServerURL(baseURL) else { return }" \ + "let maximumResponseBytes = 2097152" \ + "func send() async { await withTaskCancellationHandler(operation: {}, onCancel: {}) }" \ + "func redirect(completionHandler: (Any?) -> Void) { completionHandler(nil) }" + + write_file "$root/fearlessTests/TonTransferTransactionBuilderTests.swift" \ + "final class TonTransferTransactionBuilderTests {" \ + " func testBuildsCanonicalWalletV4R2ExternalMessageGoldenVector() {}" \ + " func testSequenceZeroIncludesStateInitAndProducesStableDeploymentMessage() {}" \ + " func testFeeEstimationMessageHasCompileTimeDistinctInvalidSignature() {}" \ + " func testUnquotedPendingNeverRetriesWithoutStoredEndpointBinding() {}" \ + " func testRetryNeverRebroadcastsAfterReconciliationError() {}" \ + " func testMessageExpiringDuringEmulationIsNeverBroadcast() {}" \ + " func testTonAPIRemoteClientFailsClosedForMalformedAndMismatchedRawBodies() {}" \ + " func testRejectsOversizedTextInputsAtCheapClassifierBounds() {}" \ + " func testTonAPIUnsignedEmulationUsesExactTraceEndpointBodyAndSignatureBypassQuery() {}" \ + " func testTonAPIBroadcastUsesExactPathAndOneFieldBodyAndRejectsNon2xx() {}" \ + " func testTonAPIReconciliationUsesExactMessageHashPathAndConfirmsExactIntent() {}" \ + " func testTonAPIReconciliationTreatsOnly404AsNotFound() {}" \ + " func testTonAPIReconciliationFailsClosedForMalformedOrMismatchedTransactions() {}" \ + " func testTonAPIWalletStateMapsLiveNullFieldsAndExactAccountSeqnoPaths() {}" \ + " func testTonAPIWalletAndMemoMappingRejectsExplicitDangerAndPreservesNullContract() {}" \ + " func testReleaseSendFailsBeforeAnyValidationOrRemoteWork() {}" \ + " func testBoundQuoteReproducesExactTemplateAndExactFeeBeforeBroadcast() {}" \ + " func testFreshSendRequiresQuoteBeforeMnemonicDerivationOrRemoteWork() {}" \ + " func testQuoteMutationExpiryWalletDriftAndFeeDriftAllFailClosed() {}" \ + " func testProcessRestartLoadsJournalAndReconcilesBeforeAnyRebroadcast() {" \ + " var stalePending = tombstone" \ + " stalePending.confirmed = false" \ + " XCTAssertThrowsError(try journal.save(stalePending))" \ + " try await staleCoordinator.retainPending(stalePending)" \ + " XCTAssertTrue(try XCTUnwrap(journal.load(senderRaw: sender)).confirmed)" \ + " let friendlySender = try TonSwift.Address.parse(raw: sender)" \ + " .toFriendly()" \ + " .toString()" \ + " try await recoveryService.acknowledgeConfirmedTransfer(" \ + " senderAddress: friendlySender," \ + " identity: identity," \ + " messageHashHex: hash" \ + " )" \ + " }" \ + " func testJournalWriteFailurePreventsEverySignedRemoteExposure() {}" \ + " func testFreshDisplayedQuoteNeverRebroadcastsOlderPendingBoc() {}" \ + " func testPersistedRecoveryRequiresStoredCredentialedEndpointBeforeRemoteWork() {}" \ + " func testPersistedRecoveryRunsBeforeSigningCredentialsAreRequested() {}" \ + "}" + + write_file "$root/fearlessTests/TonPendingIntentJournalTests.swift" \ + "func testVersionedJournalRoundTripRebuildsExactBearerWithoutSecrets() {}" \ + "func testCodecRejectsNoncanonicalCorruptAndCrossSenderRecords() {" \ + " _ = TonPendingIntentJournalCodec.maximumRecordBytes + 1" \ + "}" \ + "func testJournalConflictAndKeychainFailuresFailClosed() {}" \ + 'let quoteIDGolden = "0eb547b83019bdb5d66d62e35bc31053c00cbd68ba80599e9af1ccddd17e6958"' \ + 'let journalSHAGolden = "3238980e4f69c2fdd20fa2666a713771b4bec2ee4154255f154f827679a05067"' \ + "func replacingWithStructurallyValidInvalidSignature() {}" \ + "final class AlwaysFailingKeystore {}" + write_file "$root/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift" \ "protocol IrohaTransferSigning {}" \ "struct IrohaTransferSigningRequest {}" \ - "struct IrohaSignedTransfer {}" + "struct IrohaSignedTransfer {}" \ + "enum TransferServiceError { case tonProductionSendDisabled; case tonPriorTransferConfirmed(identity: TonTransferIntentIdentity, messageHashHex: String) }" \ + "private final class TonTransferFeeQuoteStore: @unchecked Sendable {" \ + " private let lock = NSLock()" \ + " var generation: UInt64 = 0" \ + " func next() { generation = generation == UInt64.max ? 1 : generation + 1 }" \ + " func activate(quote: TonTransferFeeQuote, presentationID: String) {" \ + " guard quote.quoteIDHex == presentationID else { return }" \ + " }" \ + "}" \ + "let quote = sendService.quote(estimateRequest)" \ + "let consumed = feeQuoteStore.consume(identity: identity)" \ + "let result = sendService.send(request, feeQuote: feeQuote)" \ + "try await prepareFee(for: transfer, readyForSubmission: false).fee" \ + "let presentation = try await self.prepareFee(" \ + " for: transfer," \ + " readyForSubmission: false" \ + ")" \ + "if case let .priorIntentConfirmed(identity, messageHashHex) = error {" \ + " throw TransferServiceError.tonPriorTransferConfirmed(" \ + " identity: identity," \ + " messageHashHex: messageHashHex" \ + " )" \ + "}" \ + "func unsubscribe() {" \ + " feeQuoteStore.invalidate()" \ + " feeTask?.cancel()" \ + "}" \ + "#if !DEBUG" \ + "throw TransferServiceError.tonProductionSendDisabled" \ + "#else" \ + "let debugTonIntegrationSend = true" \ + "#endif" write_file "$root/fearlessTests/ApplicationLayer/Services/FeatureToggle/TonChainSelectionTests.swift" \ "final class SendDependencyContainerUniversalWalletRoutingTests {" \ " func testIrohaTransferServiceBuildsSignerRequestAndSubmitsNorito() {}" \ " func testIrohaTransferServiceDefaultSignerFailsClosedAfterValidation() {}" \ " func testIrohaTransferServiceRejectsMnemonicMismatchBeforeSignerOrToriiCalls() {}" \ - " func testPrepareDependenciesFailsClosedForTonCompatibilityTransferBeforeSubstrateRouting() {}" \ + " func testProductionTonSendPolicyIsImmutableDisabledAndCannotBeBypassedByInjection() {}" \ + " func testTonCompatibilityChainDetectionByCanonicalIdWithoutMetadataHints() {}" \ + " func testProductionTonSendPolicyRejectsCanonicalIdWithoutTonMetadataHints() {}" \ + " func testTonFeeEstimateNeverRequestsMnemonicOrProducesSignedEmulation() {}" \ + " func testReleaseTonTransferSubmitFailsBeforeMnemonicOrRemoteWork() {}" \ + " func testTonTransferServicePreservesExactUnknownOutcomeHash() {}" \ + " func testCancellingTonApiTransportCancelsSessionTaskAndClearsState() {}" \ + " func testHostileTonApiOriginCannotReceiveSignedOperations() {}" \ + " func testCanonicalTonApiFactoryIsRequiredForSignedOperations() {}" \ + " func testPrepareDependenciesCachesOneTonServiceAcrossConcurrentEstimateAndSubmit() {}" \ + " func testTonQuoteCannotSubmitUntilExactPresentationIsAcknowledged() {}" \ + " func testTonUnsubscribeSynchronouslyRevokesQuoteBeforeImmediateSubmit() {}" \ + " func testTonRestartRecoverySucceedsWithoutMnemonicAndWithoutNewBearer() {}" \ + " func testCanonicalTonApiOriginStillRejectsMissingOrMalformedCredentialsForSignedOperations() {}" \ + " func testTonDirectEstimateNeverAuthorizesSubmission() {}" \ + " func testTonDifferentIntentRecoveryNeverReturnsThePriorHashAsNewSuccess() {}" \ + " func testTonFeePaymentAssetIgnoresAdditionalUnorderedUtilityAssets() {}" \ + "}" + + write_file "$root/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmInteractor.swift" \ + "protocol TonTransferFeePresentationListener {}" \ + "func confirmFeePresentation(id: String, fee: Decimal) {}" \ + "func acknowledgeSubmittedTransfer(hash: String, transfer: Transfer) {}" \ + "static func resolveFeePaymentChainAsset(for chainAsset: ChainAsset?) -> ChainAsset? {" \ + " guard let chainAsset else { return nil }" \ + " if chainAsset.chain.isTonCompatibilityChain {" \ + " return chainAsset" \ + " }" \ + " if let utilityAsset = chainAsset.chain.utilityAssets().first {" \ + " return ChainAsset(chain: chainAsset.chain, asset: utilityAsset)" \ + " }" \ + " return chainAsset" \ + "}" + + write_file "$root/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmProtocols.swift" \ + "protocol WalletSendConfirmCompletionPresenting: AnyObject {" \ + " func completeAfterPresentation(" \ + " on view: ControllerBackedProtocol?," \ + " title: String," \ + " chainAsset: ChainAsset," \ + " completion: @escaping () -> Void" \ + " )" \ + "}" + + write_file "$root/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmPresenter.swift" \ + "struct TonUnknownOutcomeRecoveryModel {" \ + " let messageHashHex: String" \ + " init?(messageHashHex: String) {" \ + " guard messageHashHex.utf8.count == 64 else { return nil }" \ + " self.messageHashHex = messageHashHex" \ + " }" \ + "}" \ + "let displayedFee = chainAsset.chain.isTonCompatibilityChain ? nil : feeViewModel" \ + "func confirmPendingTonFeePresentationIfNeeded() {}" \ + "view?.didReceive(state: .loaded(viewModel))" \ + "confirmPendingTonFeePresentationIfNeeded()" \ + "let confirmationReady = !chainAsset.chain.isTonCompatibilityChain || loadingCollector.isReady" \ + "let feeUsageCase = chainAsset.chain.isTonCompatibilityChain" \ + " ? .exactCrypto(fractionDigits: Int(utilityAsset.asset.precision))" \ + " : .detailsCrypto" \ + "func presentTonPriorTransferConfirmed() {" \ + ' let action = tonRecoveryLocalizedString(key: "ton.transfer.prior_confirmed.acknowledge", fallback: "Acknowledge")' \ + "}" \ + "func completeTransferAfterVisibleReceipt() {" \ + " guard let completionWireframe = wireframe as? WalletSendConfirmCompletionPresenting else {" \ + " wireframe.complete(on: view, title: hash, chainAsset: chainAsset)" \ + " return" \ + " }" \ + " completionWireframe.completeAfterPresentation(" \ + " on: view," \ + " title: hash," \ + " chainAsset: chainAsset" \ + " ) { [weak self] in" \ + " self?.interactor.acknowledgeSubmittedTransfer(hash: hash)" \ + " }" \ + "}" \ + "if case let TransferServiceError.tonBroadcastOutcomeUnknown(messageHashHex) = error {}" \ + "if case let TransferServiceError.tonPriorTransferConfirmed(identity, messageHashHex) = error {}" \ + 'let message = tonRecoveryLocalizedString(key: "ton.transfer.unknown.message", fallback: "Do not resend")' \ + "UIPasteboard.general.string = recovery.messageHashHex" + + write_file "$root/fearlessTests/Modules/WalletSendConfirm/WalletSendConfirmTests.swift" \ + "func testTonUnknownOutcomeRecoveryAcceptsExactLowercaseHash() {}" \ + "func testTonUnknownOutcomeRecoveryRejectsMalformedOrDisplaySpoofedHashes() {}" \ + "func testTonExactFeeFormatterPreservesTheNinthDecimalAcrossLocales() {" \ + " _ = NumberFormatter.formatter(for: .exactCrypto(fractionDigits: 9), locale: locale)" \ + "}" + + write_file "$root/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmWireframe.swift" \ + "extension WalletSendConfirmWireframe: WalletSendConfirmCompletionPresenting {" \ + " func completeAfterPresentation(" \ + " on view: ControllerBackedProtocol?," \ + " title: String," \ + " chainAsset: ChainAsset," \ + " completion: @escaping () -> Void" \ + " ) {" \ + " complete(" \ + " on: view," \ + " title: title," \ + " chainAsset: chainAsset," \ + " presentationCompletion: completion" \ + " )" \ + " }" \ + "}" + + write_file "$root/fearless/Common/Extension/NumberFormatter+Default.swift" \ + "enum NumberFormatterUsageCase {" \ + " case exactCrypto(fractionDigits: Int)" \ + "}" \ + "switch usageCase {" \ + "case let .exactCrypto(fractionDigits):" \ + " return NumberFormatter.defaultExactCryptoFormatter(" \ + " locale: locale," \ + " fractionDigits: fractionDigits" \ + " )" \ "}" + local locale + for locale in en id ja pt-PT pt ru tr vi zh-Hans; do + write_file "$root/fearless/$locale.lproj/Localizable.strings" \ + '"ton.transfer.unknown.title" = "Transfer status is unknown";' \ + '"ton.transfer.unknown.message" = "Do not send it again: %@";' \ + '"ton.transfer.unknown.copy_hash" = "Copy message hash";' \ + '"ton.transfer.prior_confirmed.title" = "Previous transfer confirmed";' \ + '"ton.transfer.prior_confirmed.message" = "Review the previous hash: %@";' \ + '"ton.transfer.prior_confirmed.acknowledge" = "Acknowledge";' + done + write_file "$root/fearless/Modules/Send/SendDependencyContainer.swift" \ - "if chainAsset.chain.isTonCompatibilityChain {" \ - " throw ConvenienceError(error: \"TON transfer not yet supported.\")" \ + "struct TonProductionSendReleasePolicy {" \ + " private init(isEnabled: Bool) { self.isEnabled = isEnabled }" \ + " static let production = TonProductionSendReleasePolicy(isEnabled: false)" \ + " let isEnabled: Bool" \ + "}" \ + "#if DEBUG" \ + "static let enabledForTests = TonProductionSendReleasePolicy(isEnabled: true)" \ + "#endif" \ + "let productionContainer = makeContainer(releasePolicy: .production)" \ + "enum UniversalWalletSendRoutingError { case tonProductionSendDisabled }" \ + "@MainActor func prepareDepencies() {" \ + " cachedDependencies[dependenciesKey] = dependencies" \ + " currentDependenciesKey = dependenciesKey" \ "}" + + write_file "$root/docs/release-checklist.md" \ + "a versioned, bounded, canonical Keychain journal means pending intents survive process termination" \ + "confirmation binds a fresh 30-second fee quote to the exact signed BOC" \ + "exact-hash unknown outcomes have explicit user-visible recovery UX" \ + "signed emulation and broadcast require an immutable reviewed endpoint allowlist" \ + "a confirmed terminal tombstone remains in the journal until explicit acknowledgement" \ + "expired never-confirmed records require finalized-chain absence proof before quarantine" \ + "current TonAPI schemas pass the bounded transport adversarial suites and prove exact fee parity with the corresponding signed Wallet V4R2 template" \ + "a funded mainnet Wallet V4R2 transfer has independently verified evidence" } run_audit() { @@ -87,6 +459,8 @@ expect_failure() { echo "$output" >&2 fail "$name did not report expected text: $expected" fi + + executed_destructive_fixtures=$((executed_destructive_fixtures + 1)) } tmp_dir="$(mktemp -d)" @@ -113,7 +487,591 @@ expect_failure "missing Iroha fail-closed test" "$missing_iroha" "Iroha default missing_ton="$tmp_dir/missing-ton" write_valid_fixture "$missing_ton" -perl -0pi -e 's/isTonCompatibilityChain/isSomeOtherChain/' "$missing_ton/fearless/Modules/Send/SendDependencyContainer.swift" -expect_failure "missing TON fail-closed guard" "$missing_ton" "TON compatibility transfer guard" +perl -0pi -e 's/isEnabled: false/isEnabled: true/' "$missing_ton/fearless/Modules/Send/SendDependencyContainer.swift" +expect_failure "enabled TON production policy" "$missing_ton" "TON hard-disabled production release policy" + +public_ton_policy_initializer="$tmp_dir/public-ton-policy-initializer" +write_valid_fixture "$public_ton_policy_initializer" +perl -0pi -e 's/private init\(isEnabled: Bool\)/init(isEnabled: Bool)/' "$public_ton_policy_initializer/fearless/Modules/Send/SendDependencyContainer.swift" +expect_failure "public TON release-policy initializer" "$public_ton_policy_initializer" "TON private release-policy initializer" + +missing_ton_debug_policy="$tmp_dir/missing-ton-debug-policy" +write_valid_fixture "$missing_ton_debug_policy" +perl -0pi -e 's/enabledForTests = TonProductionSendReleasePolicy\(isEnabled: true\)/enabledForTests = TonProductionSendReleasePolicy(isEnabled: false)/' "$missing_ton_debug_policy/fearless/Modules/Send/SendDependencyContainer.swift" +expect_failure "missing TON DEBUG enablement policy" "$missing_ton_debug_policy" "TON DEBUG-enclosed enablement policy" + +escaped_ton_debug_policy="$tmp_dir/escaped-ton-debug-policy" +write_valid_fixture "$escaped_ton_debug_policy" +perl -0pi -e 's/#if DEBUG\nstatic let enabledForTests = TonProductionSendReleasePolicy\(isEnabled: true\)\n#endif/#if DEBUG\nlet decoyDebugMarker = true\n#endif\nstatic let enabledForTests = TonProductionSendReleasePolicy(isEnabled: true)/' "$escaped_ton_debug_policy/fearless/Modules/Send/SendDependencyContainer.swift" +expect_failure "TON enablement escaped DEBUG block" "$escaped_ton_debug_policy" "TON DEBUG-enclosed enablement policy" + +bypass_ton_production_initializer="$tmp_dir/bypass-ton-production-initializer" +write_valid_fixture "$bypass_ton_production_initializer" +perl -0pi -e 's/releasePolicy: \.production/releasePolicy: .enabledForTests/' "$bypass_ton_production_initializer/fearless/Modules/Send/SendDependencyContainer.swift" +expect_failure "bypassed TON production initializer" "$bypass_ton_production_initializer" "TON production initializer policy binding" + +missing_ton_binding="$tmp_dir/missing-ton-binding" +write_valid_fixture "$missing_ton_binding" +perl -0pi -e 's/testTonAPIRemoteClientFailsClosedForMalformedAndMismatchedRawBodies/testTonAPIRemoteClientAcceptsAnyRawBody/' "$missing_ton_binding/fearlessTests/TonTransferTransactionBuilderTests.swift" +expect_failure "missing TON raw-body binding test" "$missing_ton_binding" "TON raw-message intent-binding adversarial test" + +missing_ton_unsigned_type="$tmp_dir/missing-ton-unsigned-type" +write_valid_fixture "$missing_ton_unsigned_type" +perl -0pi -e 's/TonUnsignedEmulationMessage/TonBroadcastablePreviewMessage/' "$missing_ton_unsigned_type/fearless/Common/Model/TonTransferTransactionBuilder.swift" +expect_failure "missing TON unsigned preview type" "$missing_ton_unsigned_type" "TON unsigned emulation-only type" + +missing_ton_unsigned_builder="$tmp_dir/missing-ton-unsigned-builder" +write_valid_fixture "$missing_ton_unsigned_builder" +perl -0pi -e 's/buildForFeeEstimation/buildAndSignFeePreview/' "$missing_ton_unsigned_builder/fearless/Common/Model/TonTransferTransactionBuilder.swift" +expect_failure "missing TON unsigned preview builder" "$missing_ton_unsigned_builder" "TON private-key-free fee preview builder" + +missing_ton_coordinator="$tmp_dir/missing-ton-coordinator" +write_valid_fixture "$missing_ton_coordinator" +perl -0pi -e 's/TonPendingIntentCoordinator/TonUncoordinatedSender/' "$missing_ton_coordinator/fearless/Common/Model/TonSendService.swift" +expect_failure "missing TON pending-intent coordinator" "$missing_ton_coordinator" "TON per-sender pending-intent coordinator" + +missing_ton_unknown_hash="$tmp_dir/missing-ton-unknown-hash" +write_valid_fixture "$missing_ton_unknown_hash" +perl -0pi -e 's/broadcastOutcomeUnknown\(messageHashHex: String\)/broadcastFailed/' "$missing_ton_unknown_hash/fearless/Common/Model/TonSendService.swift" +expect_failure "missing TON exact unknown-outcome hash" "$missing_ton_unknown_hash" "TON exact-hash unknown outcome" + +missing_ton_unsigned_seam="$tmp_dir/missing-ton-unsigned-seam" +write_valid_fixture "$missing_ton_unsigned_seam" +perl -0pi -e 's/emulateUnsigned/emulateSignedPreview/' "$missing_ton_unsigned_seam/fearless/Common/Model/TonSendService.swift" +expect_failure "missing TON unsigned emulation seam" "$missing_ton_unsigned_seam" "TON unsigned preview emulation seam" + +missing_ton_retry="$tmp_dir/missing-ton-retry" +write_valid_fixture "$missing_ton_retry" +perl -0pi -e 's/testUnquotedPendingNeverRetriesWithoutStoredEndpointBinding/testUnquotedPendingRetriesWithoutEndpointBinding/' "$missing_ton_retry/fearlessTests/TonTransferTransactionBuilderTests.swift" +expect_failure "missing TON unbound pending no-retry test" "$missing_ton_retry" "TON unbound pending no-retry test" + +missing_ton_mnemonic_free_estimate="$tmp_dir/missing-ton-mnemonic-free-estimate" +write_valid_fixture "$missing_ton_mnemonic_free_estimate" +perl -0pi -e 's/testTonFeeEstimateNeverRequestsMnemonicOrProducesSignedEmulation/testTonFeeEstimateRequestsMnemonic/' "$missing_ton_mnemonic_free_estimate/fearlessTests/ApplicationLayer/Services/FeatureToggle/TonChainSelectionTests.swift" +expect_failure "missing TON mnemonic-free estimate" "$missing_ton_mnemonic_free_estimate" "TON mnemonic-free fee estimate test" + +missing_ton_transport_cancellation_test="$tmp_dir/missing-ton-transport-cancellation-test" +write_valid_fixture "$missing_ton_transport_cancellation_test" +perl -0pi -e 's/testCancellingTonApiTransportCancelsSessionTaskAndClearsState/testCancellingTonApiTransportLeaksState/' "$missing_ton_transport_cancellation_test/fearlessTests/ApplicationLayer/Services/FeatureToggle/TonChainSelectionTests.swift" +expect_failure "missing TON transport cancellation test" "$missing_ton_transport_cancellation_test" "TON transport cancellation adversarial test" + +missing_ton_transport_cancellation="$tmp_dir/missing-ton-transport-cancellation" +write_valid_fixture "$missing_ton_transport_cancellation" +perl -0pi -e 's/withTaskCancellationHandler/withoutTaskCancellationHandler/' "$missing_ton_transport_cancellation/fearless/Common/Services/ChainRegistry/ChainRegistry.swift" +expect_failure "missing TON transport cancellation propagation" "$missing_ton_transport_cancellation" "TON transport cancellation propagation" + +missing_ton_builder_input_cap="$tmp_dir/missing-ton-builder-input-cap" +write_valid_fixture "$missing_ton_builder_input_cap" +perl -0pi -e 's/maximumAddressInputBytes = 128/maximumAddressInputBytes = Int.max/' "$missing_ton_builder_input_cap/fearless/Common/Model/TonTransferTransactionBuilder.swift" +expect_failure "missing TON builder input cap" "$missing_ton_builder_input_cap" "TON cheap address-input byte cap" + +missing_ton_pending_amount_cap="$tmp_dir/missing-ton-pending-amount-cap" +write_valid_fixture "$missing_ton_pending_amount_cap" +perl -0pi -e 's/request\.amountNanotons\.utf8\.count <= 19/request.amountNanotons.utf8.count <= Int.max/' "$missing_ton_pending_amount_cap/fearless/Common/Model/TonSendService.swift" +expect_failure "missing TON pending amount cap" "$missing_ton_pending_amount_cap" "TON cheap service amount-input cap" + +bypassed_ton_release_service_guard="$tmp_dir/bypassed-ton-release-service-guard" +write_valid_fixture "$bypassed_ton_release_service_guard" +perl -0pi -e 's/#if !DEBUG/#if DEBUG/' "$bypassed_ton_release_service_guard/fearless/Common/Model/TonSendService.swift" +expect_failure "bypassed TON Release service guard" "$bypassed_ton_release_service_guard" "TON Release service compile-time fail-closed guard" + +comment_decoy_ton_release_service_guard="$tmp_dir/comment-decoy-ton-release-service-guard" +write_valid_fixture "$comment_decoy_ton_release_service_guard" +perl -0pi -e 's/#if !DEBUG/#if DEBUG/; $_ .= "\n\/\/ #if !DEBUG\n\/\/ throw TonSendServiceError.productionSendDisabled\n\/\/ #else\n"' "$comment_decoy_ton_release_service_guard/fearless/Common/Model/TonSendService.swift" +expect_failure "comment-decoy TON Release service guard" "$comment_decoy_ton_release_service_guard" "TON Release service compile-time fail-closed guard" + +missing_ton_release_behavior_test="$tmp_dir/missing-ton-release-behavior-test" +write_valid_fixture "$missing_ton_release_behavior_test" +perl -0pi -e 's/testReleaseSendFailsBeforeAnyValidationOrRemoteWork/testReleaseSendAllowsRemoteWork/' "$missing_ton_release_behavior_test/fearlessTests/TonTransferTransactionBuilderTests.swift" +expect_failure "missing TON Release behavior test" "$missing_ton_release_behavior_test" "TON Release send fail-closed behavior test" + +missing_ton_broadcast_contract_test="$tmp_dir/missing-ton-broadcast-contract-test" +write_valid_fixture "$missing_ton_broadcast_contract_test" +perl -0pi -e 's/testTonAPIBroadcastUsesExactPathAndOneFieldBodyAndRejectsNon2xx/testTonAPIBroadcastAcceptsAnyResponse/' "$missing_ton_broadcast_contract_test/fearlessTests/TonTransferTransactionBuilderTests.swift" +expect_failure "missing TON broadcast contract test" "$missing_ton_broadcast_contract_test" "TON generated broadcast contract test" + +bypassed_ton_release_integration_guard="$tmp_dir/bypassed-ton-release-integration-guard" +write_valid_fixture "$bypassed_ton_release_integration_guard" +perl -0pi -e 's/#if !DEBUG/#if DEBUG/' "$bypassed_ton_release_integration_guard/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift" +expect_failure "bypassed TON Release integration guard" "$bypassed_ton_release_integration_guard" "TON Release integration compile-time fail-closed guard" + +missing_ton_release_integration_behavior_test="$tmp_dir/missing-ton-release-integration-behavior-test" +write_valid_fixture "$missing_ton_release_integration_behavior_test" +perl -0pi -e 's/testReleaseTonTransferSubmitFailsBeforeMnemonicOrRemoteWork/testReleaseTonTransferSubmitLoadsMnemonic/' "$missing_ton_release_integration_behavior_test/fearlessTests/ApplicationLayer/Services/FeatureToggle/TonChainSelectionTests.swift" +expect_failure "missing TON Release integration behavior test" "$missing_ton_release_integration_behavior_test" "TON Release integration no-secret/no-remote behavior test" + +missing_ton_canonical_id="$tmp_dir/missing-ton-canonical-id" +write_valid_fixture "$missing_ton_canonical_id" +perl -0pi -e 's/"-239"/"custom-mainnet"/' "$missing_ton_canonical_id/fearless/Common/Model/ChainRegistry/ChainModel.swift" +expect_failure "missing TON canonical chain id" "$missing_ton_canonical_id" "TON canonical mainnet chain-id classifier" + +missing_ton_oversized_input_test="$tmp_dir/missing-ton-oversized-input-test" +write_valid_fixture "$missing_ton_oversized_input_test" +perl -0pi -e 's/testRejectsOversizedTextInputsAtCheapClassifierBounds/testAcceptsOversizedTextInputs/' "$missing_ton_oversized_input_test/fearlessTests/TonTransferTransactionBuilderTests.swift" +expect_failure "missing TON oversized-input test" "$missing_ton_oversized_input_test" "TON cheap builder input-bound adversarial test" + +missing_ton_generated_unsigned_test="$tmp_dir/missing-ton-generated-unsigned-test" +write_valid_fixture "$missing_ton_generated_unsigned_test" +perl -0pi -e 's/testTonAPIUnsignedEmulationUsesExactTraceEndpointBodyAndSignatureBypassQuery/testTonAPIUnsignedEmulationUsesAnyEndpoint/' "$missing_ton_generated_unsigned_test/fearlessTests/TonTransferTransactionBuilderTests.swift" +expect_failure "missing TON generated unsigned-emulation test" "$missing_ton_generated_unsigned_test" "TON generated unsigned-emulation contract test" + +missing_ton_exact_hash_reconciliation_test="$tmp_dir/missing-ton-exact-hash-reconciliation-test" +write_valid_fixture "$missing_ton_exact_hash_reconciliation_test" +perl -0pi -e 's/testTonAPIReconciliationUsesExactMessageHashPathAndConfirmsExactIntent/testTonAPIReconciliationUsesAnyTransaction/' "$missing_ton_exact_hash_reconciliation_test/fearlessTests/TonTransferTransactionBuilderTests.swift" +expect_failure "missing TON exact-hash reconciliation test" "$missing_ton_exact_hash_reconciliation_test" "TON generated exact-hash reconciliation contract test" + +missing_ton_404_only_test="$tmp_dir/missing-ton-404-only-test" +write_valid_fixture "$missing_ton_404_only_test" +perl -0pi -e 's/testTonAPIReconciliationTreatsOnly404AsNotFound/testTonAPIReconciliationTreatsEveryErrorAsNotFound/' "$missing_ton_404_only_test/fearlessTests/TonTransferTransactionBuilderTests.swift" +expect_failure "missing TON 404-only reconciliation test" "$missing_ton_404_only_test" "TON 404-only rebroadcast authorization test" + +missing_ton_reconciliation_mutation_test="$tmp_dir/missing-ton-reconciliation-mutation-test" +write_valid_fixture "$missing_ton_reconciliation_mutation_test" +perl -0pi -e 's/testTonAPIReconciliationFailsClosedForMalformedOrMismatchedTransactions/testTonAPIReconciliationAcceptsMismatchedTransactions/' "$missing_ton_reconciliation_mutation_test/fearlessTests/TonTransferTransactionBuilderTests.swift" +expect_failure "missing TON reconciliation mutation test" "$missing_ton_reconciliation_mutation_test" "TON generated reconciliation fail-closed adversarial test" + +missing_ton_live_null_mapping_test="$tmp_dir/missing-ton-live-null-mapping-test" +write_valid_fixture "$missing_ton_live_null_mapping_test" +perl -0pi -e 's/testTonAPIWalletStateMapsLiveNullFieldsAndExactAccountSeqnoPaths/testTonAPIWalletStateRejectsLiveNullFields/' "$missing_ton_live_null_mapping_test/fearlessTests/TonTransferTransactionBuilderTests.swift" +expect_failure "missing TON live-null mapping test" "$missing_ton_live_null_mapping_test" "TON generated live-null wallet-state mapping test" + +missing_ton_danger_memo_mapping_test="$tmp_dir/missing-ton-danger-memo-mapping-test" +write_valid_fixture "$missing_ton_danger_memo_mapping_test" +perl -0pi -e 's/testTonAPIWalletAndMemoMappingRejectsExplicitDangerAndPreservesNullContract/testTonAPIWalletAndMemoMappingIgnoresDanger/' "$missing_ton_danger_memo_mapping_test/fearlessTests/TonTransferTransactionBuilderTests.swift" +expect_failure "missing TON danger/memo mapping test" "$missing_ton_danger_memo_mapping_test" "TON generated account-danger and memo mapping test" + +missing_ton_canonical_classifier_test="$tmp_dir/missing-ton-canonical-classifier-test" +write_valid_fixture "$missing_ton_canonical_classifier_test" +perl -0pi -e 's/testTonCompatibilityChainDetectionByCanonicalIdWithoutMetadataHints/testTonCompatibilityChainDetectionRequiresMetadata/' "$missing_ton_canonical_classifier_test/fearlessTests/ApplicationLayer/Services/FeatureToggle/TonChainSelectionTests.swift" +expect_failure "missing TON canonical classifier test" "$missing_ton_canonical_classifier_test" "TON canonical-id metadata-independent classifier test" + +missing_ton_canonical_release_gate_test="$tmp_dir/missing-ton-canonical-release-gate-test" +write_valid_fixture "$missing_ton_canonical_release_gate_test" +perl -0pi -e 's/testProductionTonSendPolicyRejectsCanonicalIdWithoutTonMetadataHints/testProductionTonSendPolicyIgnoresCanonicalId/' "$missing_ton_canonical_release_gate_test/fearlessTests/ApplicationLayer/Services/FeatureToggle/TonChainSelectionTests.swift" +expect_failure "missing TON canonical Release gate test" "$missing_ton_canonical_release_gate_test" "TON canonical-id production-disable routing test" + +missing_ton_production_endpoint_allowlist="$tmp_dir/missing-ton-production-endpoint-allowlist" +write_valid_fixture "$missing_ton_production_endpoint_allowlist" +perl -0pi -e 's/reviewedProductionSendOrigins = \[canonicalAuthenticatedOrigin\]/reviewedProductionSendOrigins = []/' "$missing_ton_production_endpoint_allowlist/fearless/Common/Services/ChainRegistry/ChainRegistry.swift" +expect_failure "missing TON production endpoint allowlist" "$missing_ton_production_endpoint_allowlist" "TON binary-owned production endpoint allowlist" + +bypassed_ton_send_endpoint_factory_guard="$tmp_dir/bypassed-ton-send-endpoint-factory-guard" +write_valid_fixture "$bypassed_ton_send_endpoint_factory_guard" +perl -0pi -e 's/guard TonAPIClientFactory\.isReviewedProductionSendServerURL\(baseURL\)/guard true/' "$bypassed_ton_send_endpoint_factory_guard/fearless/Common/Services/ChainRegistry/ChainRegistry.swift" +expect_failure "bypassed TON send endpoint factory guard" "$bypassed_ton_send_endpoint_factory_guard" "TON send-only exact-origin factory guard" + +missing_ton_signed_operation_endpoint_guard="$tmp_dir/missing-ton-signed-operation-endpoint-guard" +write_valid_fixture "$missing_ton_signed_operation_endpoint_guard" +perl -0pi -e 's/try requireTrustedSignedOperationEndpoint\(\)//' "$missing_ton_signed_operation_endpoint_guard/fearless/Common/Model/TonSendService.swift" +expect_failure "missing TON signed operation endpoint guard" "$missing_ton_signed_operation_endpoint_guard" "TON every signed remote operation endpoint guard" + +escaped_ton_signed_operation_test_capability="$tmp_dir/escaped-ton-signed-operation-test-capability" +write_valid_fixture "$escaped_ton_signed_operation_test_capability" +perl -0pi -e 's/#if DEBUG\ninit\(trustedTestClient/#if true\ninit(trustedTestClient/' "$escaped_ton_signed_operation_test_capability/fearless/Common/Model/TonSendService.swift" +expect_failure "escaped TON signed operation test capability" "$escaped_ton_signed_operation_test_capability" "TON DEBUG-only signed-operation test capability" + +missing_ton_hostile_origin_zero_request_test="$tmp_dir/missing-ton-hostile-origin-zero-request-test" +write_valid_fixture "$missing_ton_hostile_origin_zero_request_test" +perl -0pi -e 's/testHostileTonApiOriginCannotReceiveSignedOperations/testHostileTonApiOriginReceivesUnsignedRequest/' "$missing_ton_hostile_origin_zero_request_test/fearlessTests/ApplicationLayer/Services/FeatureToggle/TonChainSelectionTests.swift" +expect_failure "missing TON hostile origin zero-request test" "$missing_ton_hostile_origin_zero_request_test" "TON hostile-origin zero-request signed-operation test" + +missing_ton_canonical_factory_capability_test="$tmp_dir/missing-ton-canonical-factory-capability-test" +write_valid_fixture "$missing_ton_canonical_factory_capability_test" +perl -0pi -e 's/testCanonicalTonApiFactoryIsRequiredForSignedOperations/testAnyTonApiClientAllowsSignedOperations/' "$missing_ton_canonical_factory_capability_test/fearlessTests/ApplicationLayer/Services/FeatureToggle/TonChainSelectionTests.swift" +expect_failure "missing TON canonical factory capability test" "$missing_ton_canonical_factory_capability_test" "TON canonical-factory signed-operation capability test" + +missing_ton_unknown_outcome_presenter_route="$tmp_dir/missing-ton-unknown-outcome-presenter-route" +write_valid_fixture "$missing_ton_unknown_outcome_presenter_route" +perl -0pi -e 's/TransferServiceError\.tonBroadcastOutcomeUnknown\(messageHashHex\)/TransferServiceError.transferFailed(reason)/' "$missing_ton_unknown_outcome_presenter_route/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmPresenter.swift" +expect_failure "missing TON unknown outcome presenter route" "$missing_ton_unknown_outcome_presenter_route" "TON unknown-outcome dedicated presenter routing" + +weakened_ton_unknown_outcome_hash_validation="$tmp_dir/weakened-ton-unknown-outcome-hash-validation" +write_valid_fixture "$weakened_ton_unknown_outcome_hash_validation" +perl -0pi -e 's/messageHashHex\.utf8\.count == 64/messageHashHex.utf8.count >= 1/' "$weakened_ton_unknown_outcome_hash_validation/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmPresenter.swift" +expect_failure "weakened TON unknown outcome hash validation" "$weakened_ton_unknown_outcome_hash_validation" "TON unknown-outcome exact lowercase hash validation" + +missing_ton_unknown_outcome_copy_action="$tmp_dir/missing-ton-unknown-outcome-copy-action" +write_valid_fixture "$missing_ton_unknown_outcome_copy_action" +perl -0pi -e 's/UIPasteboard\.general\.string = recovery\.messageHashHex/UIPasteboard.general.string = nil/' "$missing_ton_unknown_outcome_copy_action/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmPresenter.swift" +expect_failure "missing TON unknown outcome copy action" "$missing_ton_unknown_outcome_copy_action" "TON unknown-outcome exact-hash copy action" + +missing_ton_unknown_outcome_exact_hash_test="$tmp_dir/missing-ton-unknown-outcome-exact-hash-test" +write_valid_fixture "$missing_ton_unknown_outcome_exact_hash_test" +perl -0pi -e 's/testTonUnknownOutcomeRecoveryAcceptsExactLowercaseHash/testTonUnknownOutcomeRecoveryAcceptsAnyHash/' "$missing_ton_unknown_outcome_exact_hash_test/fearlessTests/Modules/WalletSendConfirm/WalletSendConfirmTests.swift" +expect_failure "missing TON unknown outcome exact hash test" "$missing_ton_unknown_outcome_exact_hash_test" "TON unknown-outcome exact-hash recovery test" + +missing_ton_unknown_outcome_spoof_test="$tmp_dir/missing-ton-unknown-outcome-spoof-test" +write_valid_fixture "$missing_ton_unknown_outcome_spoof_test" +perl -0pi -e 's/testTonUnknownOutcomeRecoveryRejectsMalformedOrDisplaySpoofedHashes/testTonUnknownOutcomeRecoveryShowsMalformedHashes/' "$missing_ton_unknown_outcome_spoof_test/fearlessTests/Modules/WalletSendConfirm/WalletSendConfirmTests.swift" +expect_failure "missing TON unknown outcome spoof test" "$missing_ton_unknown_outcome_spoof_test" "TON unknown-outcome display-spoof adversarial test" + +missing_ton_unknown_outcome_hash_localization="$tmp_dir/missing-ton-unknown-outcome-hash-localization" +write_valid_fixture "$missing_ton_unknown_outcome_hash_localization" +perl -0pi -e 's/Do not send it again: %@/Transfer failed/' "$missing_ton_unknown_outcome_hash_localization/fearless/en.lproj/Localizable.strings" +expect_failure "missing TON unknown outcome hash localization" "$missing_ton_unknown_outcome_hash_localization" "TON unknown-outcome en exact-hash recovery message" + +missing_ton_restart_blocker="$tmp_dir/missing-ton-restart-blocker" +write_valid_fixture "$missing_ton_restart_blocker" +perl -0pi -e 's/pending intents survive process termination/pending intents are memory-only/' "$missing_ton_restart_blocker/docs/release-checklist.md" +expect_failure "missing TON restart-recovery blocker" "$missing_ton_restart_blocker" "TON durable restart-recovery release blocker" + +missing_ton_fee_binding_blocker="$tmp_dir/missing-ton-fee-binding-blocker" +write_valid_fixture "$missing_ton_fee_binding_blocker" +perl -0pi -e 's/confirmation binds a fresh 30-second fee quote/confirmation displays an advisory fee/' "$missing_ton_fee_binding_blocker/docs/release-checklist.md" +expect_failure "missing TON fee-binding blocker" "$missing_ton_fee_binding_blocker" "TON exact fee-quote binding release blocker" + +missing_ton_unknown_ux_blocker="$tmp_dir/missing-ton-unknown-ux-blocker" +write_valid_fixture "$missing_ton_unknown_ux_blocker" +perl -0pi -e 's/exact-hash unknown outcomes have explicit user-visible recovery UX/unknown outcomes use the generic error/' "$missing_ton_unknown_ux_blocker/docs/release-checklist.md" +expect_failure "missing TON unknown-outcome UX blocker" "$missing_ton_unknown_ux_blocker" "TON unknown-outcome UX release blocker" + +missing_ton_endpoint_provenance_blocker="$tmp_dir/missing-ton-endpoint-provenance-blocker" +write_valid_fixture "$missing_ton_endpoint_provenance_blocker" +perl -0pi -e 's/immutable reviewed endpoint allowlist/any selected HTTPS endpoint/' "$missing_ton_endpoint_provenance_blocker/docs/release-checklist.md" +expect_failure "missing TON endpoint provenance blocker" "$missing_ton_endpoint_provenance_blocker" "TON trusted endpoint provenance release blocker" + +missing_ton_current_schema_blocker="$tmp_dir/missing-ton-current-schema-blocker" +write_valid_fixture "$missing_ton_current_schema_blocker" +perl -0pi -e 's/current TonAPI schemas pass the bounded transport/current TonAPI schemas are assumed compatible/' "$missing_ton_current_schema_blocker/docs/release-checklist.md" +expect_failure "missing TON current-schema blocker" "$missing_ton_current_schema_blocker" "TON current-schema release blocker" + +missing_ton_live_evidence_blocker="$tmp_dir/missing-ton-live-evidence-blocker" +write_valid_fixture "$missing_ton_live_evidence_blocker" +perl -0pi -e 's/a funded mainnet Wallet V4R2 transfer/a fixture-only Wallet V4R2 transfer/' "$missing_ton_live_evidence_blocker/docs/release-checklist.md" +expect_failure "missing TON funded-mainnet blocker" "$missing_ton_live_evidence_blocker" "TON funded-mainnet evidence release blocker" + +missing_ton_signing_payload_field="$tmp_dir/missing-ton-signing-payload-field" +write_valid_fixture "$missing_ton_signing_payload_field" +perl -0pi -e 's/signingPayloadHashHex/signatureHashHex/g' "$missing_ton_signing_payload_field/fearless/Common/Model/TonTransferTransactionBuilder.swift" +expect_failure "missing TON signing payload field" "$missing_ton_signing_payload_field" "TON signing-payload binding field" + +missing_ton_restore_builder="$tmp_dir/missing-ton-restore-builder" +write_valid_fixture "$missing_ton_restore_builder" +perl -0pi -e 's/rebuildSignedMessage/rebuildUncheckedMessage/g' "$missing_ton_restore_builder/fearless/Common/Model/TonTransferTransactionBuilder.swift" +expect_failure "missing TON canonical restore builder" "$missing_ton_restore_builder" "TON fixed-signature canonical restore builder" + +missing_ton_fixed_signature_signer="$tmp_dir/missing-ton-fixed-signature-signer" +write_valid_fixture "$missing_ton_fixed_signature_signer" +perl -0pi -e 's/TonFixedSignatureSigner/TonUncheckedSignatureSigner/g' "$missing_ton_fixed_signature_signer/fearless/Common/Model/TonTransferTransactionBuilder.swift" +expect_failure "missing TON fixed signature signer" "$missing_ton_fixed_signature_signer" "TON fixed-signature restore signer" + +missing_ton_fee_quote_file="$tmp_dir/missing-ton-fee-quote-file" +write_valid_fixture "$missing_ton_fee_quote_file" +rm "$missing_ton_fee_quote_file/fearless/Common/Model/TonTransferFeeQuote.swift" +expect_failure "missing TON fee quote file" "$missing_ton_fee_quote_file" "TON exact transfer fee quote missing" + +weakened_ton_quote_age="$tmp_dir/weakened-ton-quote-age" +write_valid_fixture "$weakened_ton_quote_age" +perl -0pi -e 's/maximumAgeSeconds: UInt64 = 30/maximumAgeSeconds: UInt64 = 3600/' "$weakened_ton_quote_age/fearless/Common/Model/TonTransferFeeQuote.swift" +expect_failure "weakened TON quote age" "$weakened_ton_quote_age" "TON 30-second fee quote freshness bound" + +changed_ton_quote_domain="$tmp_dir/changed-ton-quote-domain" +write_valid_fixture "$changed_ton_quote_domain" +perl -0pi -e 's/fearless\.ton\.fee-quote\.v1/fearless.ton.fee-quote.v0/' "$changed_ton_quote_domain/fearless/Common/Model/TonTransferFeeQuote.swift" +expect_failure "changed TON quote domain" "$changed_ton_quote_domain" "TON domain-separated fee quote digest" + +bypassed_ton_quote_endpoint="$tmp_dir/bypassed-ton-quote-endpoint" +write_valid_fixture "$bypassed_ton_quote_endpoint" +perl -0pi -e 's/TonAPIClientFactory\.isReviewedProductionSendServerURL/allowAnyTonAPIURL/' "$bypassed_ton_quote_endpoint/fearless/Common/Model/TonTransferFeeQuote.swift" +expect_failure "bypassed TON quote endpoint" "$bypassed_ton_quote_endpoint" "TON fee quote reviewed-origin binding" + +missing_ton_unsigned_boc_digest="$tmp_dir/missing-ton-unsigned-boc-digest" +write_valid_fixture "$missing_ton_unsigned_boc_digest" +perl -0pi -e 's/SHA256\.hash\(data: unsignedMessage\.boc\)/SHA256.hash(data: Data())/' "$missing_ton_unsigned_boc_digest/fearless/Common/Model/TonTransferFeeQuote.swift" +expect_failure "missing TON unsigned BOC digest" "$missing_ton_unsigned_boc_digest" "TON fee quote exact unsigned-BOC digest" + +missing_ton_journal_file="$tmp_dir/missing-ton-journal-file" +write_valid_fixture "$missing_ton_journal_file" +rm "$missing_ton_journal_file/fearless/Common/Model/TonPendingIntentJournal.swift" +expect_failure "missing TON journal file" "$missing_ton_journal_file" "TON durable pending-intent journal missing" + +weakened_ton_journal_bound="$tmp_dir/weakened-ton-journal-bound" +write_valid_fixture "$weakened_ton_journal_bound" +perl -0pi -e 's/maximumRecordBytes = 48 \* 1024/maximumRecordBytes = Int.max/' "$weakened_ton_journal_bound/fearless/Common/Model/TonPendingIntentJournal.swift" +expect_failure "weakened TON journal bound" "$weakened_ton_journal_bound" "TON bounded journal record" + +missing_ton_journal_signature_verification="$tmp_dir/missing-ton-journal-signature-verification" +write_valid_fixture "$missing_ton_journal_signature_verification" +perl -0pi -e 's/isValidSignature\(inspection\.signature, for: signingHash\)/isValidSignature(Data(), for: Data())/' "$missing_ton_journal_signature_verification/fearless/Common/Model/TonPendingIntentJournal.swift" +expect_failure "missing TON journal signature verification" "$missing_ton_journal_signature_verification" "TON persisted signature verification" + +missing_ton_journal_boc_equality="$tmp_dir/missing-ton-journal-boc-equality" +write_valid_fixture "$missing_ton_journal_boc_equality" +perl -0pi -e 's/guard rebuiltSigned\.boc == signedBoc/guard true/' "$missing_ton_journal_boc_equality/fearless/Common/Model/TonPendingIntentJournal.swift" +expect_failure "missing TON journal BOC equality" "$missing_ton_journal_boc_equality" "TON persisted BOC exact equality gate" + +bypassed_ton_shared_keychain_journal="$tmp_dir/bypassed-ton-shared-keychain-journal" +write_valid_fixture "$bypassed_ton_shared_keychain_journal" +perl -0pi -e 's/productionJournal: TonKeychainPendingIntentJournal\(\)/productionJournal: TonInMemoryPendingIntentJournal()/' "$bypassed_ton_shared_keychain_journal/fearless/Common/Model/TonSendService.swift" +expect_failure "bypassed TON shared Keychain journal" "$bypassed_ton_shared_keychain_journal" "TON production coordinator Keychain journal binding" + +missing_ton_journal_first_load="$tmp_dir/missing-ton-journal-first-load" +write_valid_fixture "$missing_ton_journal_first_load" +perl -0pi -e 's/journal\.load\(senderRaw: identity\.sender\)/memory.load(senderRaw: identity.sender)/' "$missing_ton_journal_first_load/fearless/Common/Model/TonSendService.swift" +expect_failure "missing TON journal-first load" "$missing_ton_journal_first_load" "TON journal-first sender recovery" + +reordered_ton_durable_reserve="$tmp_dir/reordered-ton-durable-reserve" +write_valid_fixture "$reordered_ton_durable_reserve" +perl -0pi -e 's/try journal\.save\(pending\)\n[[:space:]]*statesBySender\[pending\.identity\.sender\] = \.pending\(pending\)/statesBySender[pending.identity.sender] = .pending(pending)\n try journal.save(pending)/' "$reordered_ton_durable_reserve/fearless/Common/Model/TonSendService.swift" +expect_failure "reordered TON durable reserve" "$reordered_ton_durable_reserve" "TON durable reserve before in-memory exposure state" + +bypassed_ton_quote_requirement="$tmp_dir/bypassed-ton-quote-requirement" +write_valid_fixture "$bypassed_ton_quote_requirement" +perl -0pi -e 's/requiresFeeQuote: true/requiresFeeQuote: false/' "$bypassed_ton_quote_requirement/fearless/Common/Model/TonSendService.swift" +expect_failure "bypassed TON quote requirement" "$bypassed_ton_quote_requirement" "TON application send quote requirement" + +escaped_ton_unquoted_test_seam="$tmp_dir/escaped-ton-unquoted-test-seam" +write_valid_fixture "$escaped_ton_unquoted_test_seam" +perl -0pi -e 's/#if DEBUG\n\/\/\/ Explicitly test-only compatibility seam\./#if true\n\/\/\/ Explicitly test-only compatibility seam./' "$escaped_ton_unquoted_test_seam/fearless/Common/Model/TonSendService.swift" +expect_failure "escaped TON unquoted test seam" "$escaped_ton_unquoted_test_seam" "TON DEBUG-only unquoted test seam" + +missing_ton_unsigned_template_comparison="$tmp_dir/missing-ton-unsigned-template-comparison" +write_valid_fixture "$missing_ton_unsigned_template_comparison" +perl -0pi -e 's/rebuiltUnsigned == feeQuote\.unsignedMessage/true/' "$missing_ton_unsigned_template_comparison/fearless/Common/Model/TonSendService.swift" +expect_failure "missing TON unsigned template comparison" "$missing_ton_unsigned_template_comparison" "TON exact unsigned-template reproduction" + +missing_ton_wallet_state_drift_gate="$tmp_dir/missing-ton-wallet-state-drift-gate" +write_valid_fixture "$missing_ton_wallet_state_drift_gate" +perl -0pi -e 's/walletState == feeQuote\.walletState/true/' "$missing_ton_wallet_state_drift_gate/fearless/Common/Model/TonSendService.swift" +expect_failure "missing TON wallet state drift gate" "$missing_ton_wallet_state_drift_gate" "TON quote wallet-state drift rejection" + +missing_ton_exact_fee_equality="$tmp_dir/missing-ton-exact-fee-equality" +write_valid_fixture "$missing_ton_exact_fee_equality" +perl -0pi -e 's/emulation\.totalFeeNanotons != feeQuote\.feeNanotons/emulation.totalFeeNanotons == feeQuote.feeNanotons/' "$missing_ton_exact_fee_equality/fearless/Common/Model/TonSendService.swift" +expect_failure "missing TON exact fee equality" "$missing_ton_exact_fee_equality" "TON exact signed-emulation fee equality" + +missing_ton_quote_binding_test="$tmp_dir/missing-ton-quote-binding-test" +write_valid_fixture "$missing_ton_quote_binding_test" +perl -0pi -e 's/testBoundQuoteReproducesExactTemplateAndExactFeeBeforeBroadcast/testBoundQuoteAllowsFeeDrift/' "$missing_ton_quote_binding_test/fearlessTests/TonTransferTransactionBuilderTests.swift" +expect_failure "missing TON quote binding test" "$missing_ton_quote_binding_test" "TON exact quote/template/fee test" + +missing_ton_restart_recovery_test="$tmp_dir/missing-ton-restart-recovery-test" +write_valid_fixture "$missing_ton_restart_recovery_test" +perl -0pi -e 's/testProcessRestartLoadsJournalAndReconcilesBeforeAnyRebroadcast/testProcessRestartBuildsNewMessage/' "$missing_ton_restart_recovery_test/fearlessTests/TonTransferTransactionBuilderTests.swift" +expect_failure "missing TON restart recovery test" "$missing_ton_restart_recovery_test" "TON process-restart reconcile-first test" + +missing_ton_journal_corruption_test="$tmp_dir/missing-ton-journal-corruption-test" +write_valid_fixture "$missing_ton_journal_corruption_test" +perl -0pi -e 's/testCodecRejectsNoncanonicalCorruptAndCrossSenderRecords/testCodecAcceptsCorruptRecords/' "$missing_ton_journal_corruption_test/fearlessTests/TonPendingIntentJournalTests.swift" +expect_failure "missing TON journal corruption test" "$missing_ton_journal_corruption_test" "TON journal corruption/cross-sender adversarial test" + +missing_ton_quote_consumption="$tmp_dir/missing-ton-quote-consumption" +write_valid_fixture "$missing_ton_quote_consumption" +perl -0pi -e 's/feeQuoteStore\.consume\(identity: identity\)/feeQuoteStore.peek(identity: identity)/' "$missing_ton_quote_consumption/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift" +expect_failure "missing TON quote consumption" "$missing_ton_quote_consumption" "TON atomic one-use quote consumption" + +missing_ton_fee_parity_blocker="$tmp_dir/missing-ton-fee-parity-blocker" +write_valid_fixture "$missing_ton_fee_parity_blocker" +perl -0pi -e 's/exact fee parity with the corresponding signed Wallet V4R2/exact fees are assumed for the signed Wallet V4R2/' "$missing_ton_fee_parity_blocker/docs/release-checklist.md" +expect_failure "missing TON fee parity blocker" "$missing_ton_fee_parity_blocker" "TON unsigned/signed fee-parity release blocker" + +missing_ton_confirmed_journal_phase="$tmp_dir/missing-ton-confirmed-journal-phase" +write_valid_fixture "$missing_ton_confirmed_journal_phase" +perl -0pi -e 's/phase = "confirmed"/phase = "pending"/' "$missing_ton_confirmed_journal_phase/fearless/Common/Model/TonPendingIntentJournal.swift" +expect_failure "missing TON confirmed journal phase" "$missing_ton_confirmed_journal_phase" "TON confirmed journal tombstone phase" + +missing_ton_tombstone_before_success="$tmp_dir/missing-ton-tombstone-before-success" +write_valid_fixture "$missing_ton_tombstone_before_success" +perl -0pi -e 's/pendingCoordinator\.recordConfirmed\(pending\)/pendingCoordinator.finishInMemory(pending)/' "$missing_ton_tombstone_before_success/fearless/Common/Model/TonSendService.swift" +expect_failure "missing TON tombstone before success" "$missing_ton_tombstone_before_success" "TON tombstone-before-success return" + +missing_ton_confirmed_acknowledgement="$tmp_dir/missing-ton-confirmed-acknowledgement" +write_valid_fixture "$missing_ton_confirmed_acknowledgement" +perl -0pi -e 's/func acknowledgeConfirmed\(senderRaw:/func clearConfirmed(senderRaw:/' "$missing_ton_confirmed_acknowledgement/fearless/Common/Model/TonSendService.swift" +expect_failure "missing TON confirmed acknowledgement" "$missing_ton_confirmed_acknowledgement" "TON explicit confirmed tombstone acknowledgement" + +weakened_ton_stored_endpoint_equality="$tmp_dir/weakened-ton-stored-endpoint-equality" +write_valid_fixture "$weakened_ton_stored_endpoint_equality" +perl -0pi -e 's/storedOrigin == currentOrigin/storedOrigin.host == currentOrigin.host/' "$weakened_ton_stored_endpoint_equality/fearless/Common/Model/TonSendService.swift" +expect_failure "weakened TON stored endpoint equality" "$weakened_ton_stored_endpoint_equality" "TON exact stored/current recovery-origin equality" + +signing_before_ton_recovery="$tmp_dir/signing-before-ton-recovery" +write_valid_fixture "$signing_before_ton_recovery" +perl -0pi -e 's/ var pending = try await pendingCoordinator\.begin\(identity\)/ let credentials = try signingCredentials()\n var pending = try await pendingCoordinator.begin(identity)/; s/\n let credentials = try signingCredentials\(\)\n pending =/\n pending =/' "$signing_before_ton_recovery/fearless/Common/Model/TonSendService.swift" +expect_failure "TON signing before pending recovery" "$signing_before_ton_recovery" "TON recovery-before-signing-credentials ordering" + +ordinary_ton_submit_rebroadcasts_pending="$tmp_dir/ordinary-ton-submit-rebroadcasts-pending" +write_valid_fixture "$ordinary_ton_submit_rebroadcasts_pending" +perl -0pi -e 's/recoverPendingWithoutRebroadcast\(pending\)/retryPendingBroadcast(pending)/' "$ordinary_ton_submit_rebroadcasts_pending/fearless/Common/Model/TonSendService.swift" +expect_failure "ordinary TON submit rebroadcasts pending BOC" "$ordinary_ton_submit_rebroadcasts_pending" "TON ordinary-submit no pending-BOC autorebroadcast" + +changed_ton_quote_id_golden="$tmp_dir/changed-ton-quote-id-golden" +write_valid_fixture "$changed_ton_quote_id_golden" +perl -0pi -e 's/0eb547b83019bdb5d66d62e35bc31053c00cbd68ba80599e9af1ccddd17e6958/1eb547b83019bdb5d66d62e35bc31053c00cbd68ba80599e9af1ccddd17e6958/' "$changed_ton_quote_id_golden/fearlessTests/TonPendingIntentJournalTests.swift" +expect_failure "changed TON quote-ID golden" "$changed_ton_quote_id_golden" "TON quote-ID known-answer vector" + +changed_ton_journal_sha_golden="$tmp_dir/changed-ton-journal-sha-golden" +write_valid_fixture "$changed_ton_journal_sha_golden" +perl -0pi -e 's/3238980e4f69c2fdd20fa2666a713771b4bec2ee4154255f154f827679a05067/4238980e4f69c2fdd20fa2666a713771b4bec2ee4154255f154f827679a05067/' "$changed_ton_journal_sha_golden/fearlessTests/TonPendingIntentJournalTests.swift" +expect_failure "changed TON journal SHA golden" "$changed_ton_journal_sha_golden" "TON journal canonical SHA-256 vector" + +missing_ton_structural_invalid_signature_attack="$tmp_dir/missing-ton-structural-invalid-signature-attack" +write_valid_fixture "$missing_ton_structural_invalid_signature_attack" +perl -0pi -e 's/replacingWithStructurallyValidInvalidSignature/replacingWithTruncatedSignature/' "$missing_ton_structural_invalid_signature_attack/fearlessTests/TonPendingIntentJournalTests.swift" +expect_failure "missing TON structural invalid-signature attack" "$missing_ton_structural_invalid_signature_attack" "TON structurally valid invalid-signature journal attack" + +missing_ton_credential_validator="$tmp_dir/missing-ton-credential-validator" +write_valid_fixture "$missing_ton_credential_validator" +perl -0pi -e 's/static func isValidAuthorizationToken/static func acceptsAuthorizationToken/' "$missing_ton_credential_validator/fearless/Common/Services/ChainRegistry/ChainRegistry.swift" +expect_failure "missing TON credential validator" "$missing_ton_credential_validator" "TON bounded visible-ASCII credential validator" + +weakened_ton_credential_ascii_range="$tmp_dir/weakened-ton-credential-ascii-range" +write_valid_fixture "$weakened_ton_credential_ascii_range" +perl -0pi -e 's/\(0x21 \.\.\. 0x7E\)\.contains\(byte\)/(0x00 ... 0x7F).contains(byte)/' "$weakened_ton_credential_ascii_range/fearless/Common/Services/ChainRegistry/ChainRegistry.swift" +expect_failure "weakened TON credential ASCII range" "$weakened_ton_credential_ascii_range" "TON credential control/whitespace rejection" + +unlocked_ton_quote_store="$tmp_dir/unlocked-ton-quote-store" +write_valid_fixture "$unlocked_ton_quote_store" +perl -0pi -e 's/private let lock = NSLock\(\)/private let lock = NSObject()/' "$unlocked_ton_quote_store/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift" +expect_failure "unlocked TON quote store" "$unlocked_ton_quote_store" "TON quote-store synchronization" + +asynchronous_ton_unsubscribe="$tmp_dir/asynchronous-ton-unsubscribe" +write_valid_fixture "$asynchronous_ton_unsubscribe" +perl -0pi -e 's/func unsubscribe\(\) \{\n feeQuoteStore\.invalidate\(\)\n feeTask\?\.cancel\(\)/func unsubscribe() {\n feeTask?.cancel()\n feeQuoteStore.invalidate()/' "$asynchronous_ton_unsubscribe/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift" +expect_failure "asynchronous TON unsubscribe revocation" "$asynchronous_ton_unsubscribe" "TON synchronous unsubscribe quote revocation" + +bypassed_ton_quote_id_activation="$tmp_dir/bypassed-ton-quote-id-activation" +write_valid_fixture "$bypassed_ton_quote_id_activation" +perl -0pi -e 's/quote\.quoteIDHex == presentationID/true/' "$bypassed_ton_quote_id_activation/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift" +expect_failure "bypassed TON quote-ID activation" "$bypassed_ton_quote_id_activation" "TON exact quote-ID presentation activation" + +unserialized_send_dependency_cache="$tmp_dir/unserialized-send-dependency-cache" +write_valid_fixture "$unserialized_send_dependency_cache" +perl -0pi -e 's/\@MainActor func prepareDepencies/func prepareDepencies/' "$unserialized_send_dependency_cache/fearless/Modules/Send/SendDependencyContainer.swift" +expect_failure "unserialized send dependency cache" "$unserialized_send_dependency_cache" "Send dependency cache main-actor serialization" + +missing_send_dependency_cache_population="$tmp_dir/missing-send-dependency-cache-population" +write_valid_fixture "$missing_send_dependency_cache_population" +perl -0pi -e 's/cachedDependencies\[dependenciesKey\] = dependencies/cachedDependencies.removeValue(forKey: dependenciesKey)/' "$missing_send_dependency_cache_population/fearless/Modules/Send/SendDependencyContainer.swift" +expect_failure "missing send dependency cache population" "$missing_send_dependency_cache_population" "Send dependency cache population" + +missing_send_dependency_active_key="$tmp_dir/missing-send-dependency-active-key" +write_valid_fixture "$missing_send_dependency_active_key" +perl -0pi -e 's/currentDependenciesKey = dependenciesKey/currentDependenciesKey = nil/' "$missing_send_dependency_active_key/fearless/Modules/Send/SendDependencyContainer.swift" +expect_failure "missing send dependency active key" "$missing_send_dependency_active_key" "Send dependency active-key tracking" + +activated_ton_quote_before_view_render="$tmp_dir/activated-ton-quote-before-view-render" +write_valid_fixture "$activated_ton_quote_before_view_render" +perl -0pi -e 's/view\?\.didReceive\(state: \.loaded\(viewModel\)\)\nconfirmPendingTonFeePresentationIfNeeded\(\)/confirmPendingTonFeePresentationIfNeeded()\nview?.didReceive(state: .loaded(viewModel))/' "$activated_ton_quote_before_view_render/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmPresenter.swift" +expect_failure "TON quote activated before view render" "$activated_ton_quote_before_view_render" "TON view-before-quote activation ordering" + +bypassed_ton_confirmation_readiness="$tmp_dir/bypassed-ton-confirmation-readiness" +write_valid_fixture "$bypassed_ton_confirmation_readiness" +perl -0pi -e 's/!chainAsset\.chain\.isTonCompatibilityChain \|\| loadingCollector\.isReady/!chainAsset.chain.isTonCompatibilityChain \&\& loadingCollector.isReady/' "$bypassed_ton_confirmation_readiness/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmPresenter.swift" +expect_failure "bypassed TON confirmation readiness" "$bypassed_ton_confirmation_readiness" "TON confirmation readiness guard" + +missing_ton_finality_absence_blocker="$tmp_dir/missing-ton-finality-absence-blocker" +write_valid_fixture "$missing_ton_finality_absence_blocker" +perl -0pi -e 's/finalized-chain absence proof/local timeout/' "$missing_ton_finality_absence_blocker/docs/release-checklist.md" +expect_failure "missing TON finality absence blocker" "$missing_ton_finality_absence_blocker" "TON expired-pending recovery blocker" + +submittable_ton_direct_estimate="$tmp_dir/submittable-ton-direct-estimate" +write_valid_fixture "$submittable_ton_direct_estimate" +perl -0pi -e 's/readyForSubmission: false/readyForSubmission: true/' "$submittable_ton_direct_estimate/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift" +expect_failure "submittable TON direct estimate" "$submittable_ton_direct_estimate" "TON every fee-estimate path stages a non-submittable quote" + +missing_ton_direct_estimate_regression="$tmp_dir/missing-ton-direct-estimate-regression" +write_valid_fixture "$missing_ton_direct_estimate_regression" +perl -0pi -e 's/testTonDirectEstimateNeverAuthorizesSubmission/testTonDirectEstimateReturnsFee/' "$missing_ton_direct_estimate_regression/fearlessTests/ApplicationLayer/Services/FeatureToggle/TonChainSelectionTests.swift" +expect_failure "missing TON direct-estimate regression" "$missing_ton_direct_estimate_regression" "TON direct-estimate zero-submit regression" + +downgraded_ton_prior_confirmed_mapping="$tmp_dir/downgraded-ton-prior-confirmed-mapping" +write_valid_fixture "$downgraded_ton_prior_confirmed_mapping" +perl -0pi -e 's/throw TransferServiceError\.tonPriorTransferConfirmed\(/throw TransferServiceError.tonBroadcastOutcomeUnknown(/' "$downgraded_ton_prior_confirmed_mapping/fearless/ApplicationLayer/Services/Transfer/Tokens/TransferService.swift" +expect_failure "downgraded TON prior-confirmed mapping" "$downgraded_ton_prior_confirmed_mapping" "TON prior-confirmed service-to-integration mapping" + +missing_ton_different_intent_integration="$tmp_dir/missing-ton-different-intent-integration" +write_valid_fixture "$missing_ton_different_intent_integration" +perl -0pi -e 's/testTonDifferentIntentRecoveryNeverReturnsThePriorHashAsNewSuccess/testTonDifferentIntentReturnsSuccess/' "$missing_ton_different_intent_integration/fearlessTests/ApplicationLayer/Services/FeatureToggle/TonChainSelectionTests.swift" +expect_failure "missing TON different-intent integration" "$missing_ton_different_intent_integration" "TON different-intent prior-confirmed integration regression" + +missing_ton_prior_confirmed_presenter_ux="$tmp_dir/missing-ton-prior-confirmed-presenter-ux" +write_valid_fixture "$missing_ton_prior_confirmed_presenter_ux" +perl -0pi -e 's/presentTonPriorTransferConfirmed/presentGenericTransferFailure/g' "$missing_ton_prior_confirmed_presenter_ux/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmPresenter.swift" +expect_failure "missing TON prior-confirmed presenter UX" "$missing_ton_prior_confirmed_presenter_ux" "TON prior-confirmed acknowledgement UX" + +bypassed_ton_confirmed_identity_ack="$tmp_dir/bypassed-ton-confirmed-identity-ack" +write_valid_fixture "$bypassed_ton_confirmed_identity_ack" +perl -0pi -e 's/pending\.identity == identity/true/' "$bypassed_ton_confirmed_identity_ack/fearless/Common/Model/TonSendService.swift" +expect_failure "bypassed TON confirmed identity acknowledgement" "$bypassed_ton_confirmed_identity_ack" "TON exact identity/hash confirmed acknowledgement" + +missing_ton_friendly_ack_regression="$tmp_dir/missing-ton-friendly-ack-regression" +write_valid_fixture "$missing_ton_friendly_ack_regression" +perl -0pi -e 's/\.toFriendly\(\)/.toRaw()/' "$missing_ton_friendly_ack_regression/fearlessTests/TonTransferTransactionBuilderTests.swift" +expect_failure "missing TON friendly acknowledgement regression" "$missing_ton_friendly_ack_regression" "TON friendly-address acknowledgement regression" + +missing_ton_receipt_completion_protocol="$tmp_dir/missing-ton-receipt-completion-protocol" +write_valid_fixture "$missing_ton_receipt_completion_protocol" +perl -0pi -e 's/protocol WalletSendConfirmCompletionPresenting/protocol WalletSendConfirmCompletionScheduling/' "$missing_ton_receipt_completion_protocol/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmProtocols.swift" +expect_failure "missing TON receipt completion protocol" "$missing_ton_receipt_completion_protocol" "TON visible-receipt completion capability" + +dropped_ton_wireframe_presentation_callback="$tmp_dir/dropped-ton-wireframe-presentation-callback" +write_valid_fixture "$dropped_ton_wireframe_presentation_callback" +perl -0pi -e 's/presentationCompletion: completion/presentationCompletion: nil/' "$dropped_ton_wireframe_presentation_callback/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmWireframe.swift" +expect_failure "dropped TON wireframe presentation callback" "$dropped_ton_wireframe_presentation_callback" "TON production presentation callback propagation" + +missing_ton_receipt_callback_ack="$tmp_dir/missing-ton-receipt-callback-ack" +write_valid_fixture "$missing_ton_receipt_callback_ack" +perl -0pi -e 's/self\?\.interactor\.acknowledgeSubmittedTransfer/self?.interactor.logSubmittedTransfer/' "$missing_ton_receipt_callback_ack/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmPresenter.swift" +expect_failure "missing TON receipt callback acknowledgement" "$missing_ton_receipt_callback_ack" "TON success receipt callback-before-acknowledgement ordering" + +premature_ton_ack_without_receipt_callback="$tmp_dir/premature-ton-ack-without-receipt-callback" +write_valid_fixture "$premature_ton_ack_without_receipt_callback" +perl -0pi -e 's/ wireframe\.complete\(on: view, title: hash, chainAsset: chainAsset\)/ interactor.acknowledgeSubmittedTransfer(hash: hash)\n wireframe.complete(on: view, title: hash, chainAsset: chainAsset)/' "$premature_ton_ack_without_receipt_callback/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmPresenter.swift" +expect_failure "premature TON acknowledgement without receipt callback" "$premature_ton_ack_without_receipt_callback" "TON no-callback success path premature acknowledgement" + +rounded_ton_fee_presentation="$tmp_dir/rounded-ton-fee-presentation" +write_valid_fixture "$rounded_ton_fee_presentation" +perl -0pi -e 's/\.exactCrypto\(fractionDigits: Int\(utilityAsset\.asset\.precision\)\)/.detailsCrypto/' "$rounded_ton_fee_presentation/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmPresenter.swift" +expect_failure "rounded TON fee presentation" "$rounded_ton_fee_presentation" "TON exact precision fee rendering" + +missing_ton_ninth_decimal_test="$tmp_dir/missing-ton-ninth-decimal-test" +write_valid_fixture "$missing_ton_ninth_decimal_test" +perl -0pi -e 's/testTonExactFeeFormatterPreservesTheNinthDecimalAcrossLocales/testTonFeeFormatterRoundsAcrossLocales/' "$missing_ton_ninth_decimal_test/fearlessTests/Modules/WalletSendConfirm/WalletSendConfirmTests.swift" +expect_failure "missing TON ninth-decimal test" "$missing_ton_ninth_decimal_test" "TON ninth-decimal locale rendering regression" + +reversible_ton_journal_phase="$tmp_dir/reversible-ton-journal-phase" +write_valid_fixture "$reversible_ton_journal_phase" +perl -0pi -e 's/pending\.journalPhaseRank >= existing\.journalPhaseRank/pending.journalPhaseRank <= existing.journalPhaseRank/' "$reversible_ton_journal_phase/fearless/Common/Model/TonPendingIntentJournal.swift" +expect_failure "reversible TON journal phase" "$reversible_ton_journal_phase" "TON journal phase-monotonic save guards" + +bypassed_ton_journal_bearer_guard="$tmp_dir/bypassed-ton-journal-bearer-guard" +write_valid_fixture "$bypassed_ton_journal_bearer_guard" +perl -0pi -e 's/pending\.hasSameBearer\(as: existing\)/true/' "$bypassed_ton_journal_bearer_guard/fearless/Common/Model/TonPendingIntentJournal.swift" +expect_failure "bypassed TON journal bearer guard" "$bypassed_ton_journal_bearer_guard" "TON journal same-bearer overwrite guards" + +reordered_ton_retain_durable_save="$tmp_dir/reordered-ton-retain-durable-save" +write_valid_fixture "$reordered_ton_retain_durable_save" +perl -0pi -e 's/ try journal\.save\(retained\)\n statesBySender\[pending\.identity\.sender\] = \.pending\(retained\)/ statesBySender[pending.identity.sender] = .pending(retained)\n try journal.save(retained)/' "$reordered_ton_retain_durable_save/fearless/Common/Model/TonSendService.swift" +expect_failure "reordered TON retain durable save" "$reordered_ton_retain_durable_save" "TON retained pending durable-save-before-state ordering" + +dropped_ton_stale_confirmed_preservation="$tmp_dir/dropped-ton-stale-confirmed-preservation" +write_valid_fixture "$dropped_ton_stale_confirmed_preservation" +perl -0pi -e 's/storedPending\.confirmed \|\|/false ||/' "$dropped_ton_stale_confirmed_preservation/fearless/Common/Model/TonSendService.swift" +expect_failure "dropped TON stale confirmed preservation" "$dropped_ton_stale_confirmed_preservation" "TON stale recovery preserves confirmed phase" + +missing_ton_deterministic_downgrade_regression="$tmp_dir/missing-ton-deterministic-downgrade-regression" +write_valid_fixture "$missing_ton_deterministic_downgrade_regression" +perl -0pi -e 's/stalePending\.confirmed = false/stalePending.confirmed = true/' "$missing_ton_deterministic_downgrade_regression/fearlessTests/TonTransferTransactionBuilderTests.swift" +expect_failure "missing TON deterministic downgrade regression" "$missing_ton_deterministic_downgrade_regression" "TON deterministic confirmed-phase downgrade regression" + +missing_ton_prior_confirmed_locale_key="$tmp_dir/missing-ton-prior-confirmed-locale-key" +write_valid_fixture "$missing_ton_prior_confirmed_locale_key" +perl -0pi -e 's/ton\.transfer\.prior_confirmed\.acknowledge/ton.transfer.prior_confirmed.dismiss/' "$missing_ton_prior_confirmed_locale_key/fearless/en.lproj/Localizable.strings" +expect_failure "missing TON prior-confirmed locale key" "$missing_ton_prior_confirmed_locale_key" "TON prior-confirmed en acknowledgement action" + +bypassed_ton_selected_fee_asset_pin="$tmp_dir/bypassed-ton-selected-fee-asset-pin" +write_valid_fixture "$bypassed_ton_selected_fee_asset_pin" +perl -0pi -e 's/if chainAsset\.chain\.isTonCompatibilityChain/if chainAsset.chain.isEthereum/' "$bypassed_ton_selected_fee_asset_pin/fearless/Modules/NewWallet/WalletSendConfirm/WalletSendConfirmInteractor.swift" +expect_failure "bypassed TON selected fee-asset pin" "$bypassed_ton_selected_fee_asset_pin" "TON selected native fee asset pinned before unordered registry fallback" + +missing_ton_unordered_utility_asset_test="$tmp_dir/missing-ton-unordered-utility-asset-test" +write_valid_fixture "$missing_ton_unordered_utility_asset_test" +perl -0pi -e 's/testTonFeePaymentAssetIgnoresAdditionalUnorderedUtilityAssets/testTonFeePaymentAssetUsesFirstUtilityAsset/' "$missing_ton_unordered_utility_asset_test/fearlessTests/ApplicationLayer/Services/FeatureToggle/TonChainSelectionTests.swift" +expect_failure "missing TON unordered utility-asset test" "$missing_ton_unordered_utility_asset_test" "TON unordered utility-asset registry-confusion regression" + +if ((executed_destructive_fixtures != EXPECTED_DESTRUCTIVE_FIXTURES)); then + fail "expected exactly $EXPECTED_DESTRUCTIVE_FIXTURES destructive fixtures, executed $executed_destructive_fixtures" +fi -echo "[ios-transaction-builder-audit-test] all tests passed" +echo "[ios-transaction-builder-audit-test] all $executed_destructive_fixtures destructive fixtures passed" From 0fd6fd2f0a0eafa8e4e6b4d84678b985d7633467 Mon Sep 17 00:00:00 2001 From: Makoto Takemiya Date: Fri, 31 Jul 2026 21:28:52 +0400 Subject: [PATCH 28/30] test(ios): remove chain registry observer race Signed-off-by: Makoto Takemiya --- .../ChainRegistry/ChainRegistryTests.swift | 199 ++++++++++++++---- 1 file changed, 153 insertions(+), 46 deletions(-) diff --git a/fearlessTests/Common/Services/ChainRegistry/ChainRegistryTests.swift b/fearlessTests/Common/Services/ChainRegistry/ChainRegistryTests.swift index 66314de809..59739c5e0c 100644 --- a/fearlessTests/Common/Services/ChainRegistry/ChainRegistryTests.swift +++ b/fearlessTests/Common/Services/ChainRegistry/ChainRegistryTests.swift @@ -9,14 +9,7 @@ import XCTest final class ChainRegistryTests: XCTestCase { func testDisabledThenReenabledChainDisconnectsAndUsesNewNode() throws { - let storageFacade = SubstrateStorageTestFacade() - let repository = ChainRepositoryFactory( - storageFacade: storageFacade - ).createRepository() - let provider = makeChainProvider( - storageFacade: storageFacade, - repository: repository - ) + let (provider, observable) = makeChainProvider() let oldNode = ChainNodeModel( url: try XCTUnwrap(URL(string: "wss://old-node.example")), @@ -64,8 +57,9 @@ final class ChainRegistryTests: XCTestCase { eventCenter: NoopEventCenter() ) registry.subscribeToChains() + XCTAssertTrue(observable.waitUntilObserverAttached()) - try save(enabledOld, in: repository) + XCTAssertTrue(observable.send([.insert(newItem: enabledOld)])) let firstConnection: ChainConnection = try waitForValue { guard registry.getChain(for: enabledOld.chainId)?.selectedNode == oldNode @@ -77,7 +71,7 @@ final class ChainRegistryTests: XCTestCase { } XCTAssertEqual(connectionFactory.requestedURLs, [[oldNode.url]]) - try save(disabled, in: repository) + XCTAssertTrue(observable.send([.update(newItem: disabled)])) try waitForCondition { registry.getChain(for: enabledOld.chainId) == nil && registry.getConnection(for: enabledOld.chainId) == nil @@ -89,7 +83,7 @@ final class ChainRegistryTests: XCTestCase { ) XCTAssertEqual(connectionFactory.requestedURLs, [[oldNode.url]]) - try save(enabledNew, in: repository) + XCTAssertTrue(observable.send([.update(newItem: enabledNew)])) let secondConnection: ChainConnection = try waitForValue { guard registry.getChain(for: enabledOld.chainId)?.selectedNode == newNode @@ -117,14 +111,7 @@ final class ChainRegistryTests: XCTestCase { } func testUpdatedEthereumChainUsesReplacementNodeImmediately() throws { - let storageFacade = SubstrateStorageTestFacade() - let repository = ChainRepositoryFactory( - storageFacade: storageFacade - ).createRepository() - let provider = makeChainProvider( - storageFacade: storageFacade, - repository: repository - ) + let (provider, observable) = makeChainProvider() let oldNode = ChainNodeModel( url: try XCTUnwrap(URL(string: "https://old-rpc.example")), name: "Old Ethereum RPC", @@ -161,13 +148,14 @@ final class ChainRegistryTests: XCTestCase { eventCenter: NoopEventCenter() ) registry.subscribeToChains() + XCTAssertTrue(observable.waitUntilObserverAttached()) - try save(oldChain, in: repository) + XCTAssertTrue(observable.send([.insert(newItem: oldChain)])) let firstURL: URL = try waitForValue { nodeFetching.requestedURLs.first } - try save(updatedChain, in: repository) + XCTAssertTrue(observable.send([.update(newItem: updatedChain)])) let secondURL: URL = try waitForValue { let urls = nodeFetching.requestedURLs return urls.count >= 2 ? urls.last : nil @@ -190,26 +178,20 @@ final class ChainRegistryTests: XCTestCase { provider.removeObserver(registry) } - private func makeChainProvider( - storageFacade: SubstrateStorageTestFacade, - repository: CoreDataRepository - ) -> StreamableProvider { - let observable = CoreDataContextObservable( - service: storageFacade.databaseService, - mapper: repository.dataMapper, - predicate: { _ in true } - ) - observable.start { error in - XCTAssertNil(error) - } + private func makeChainProvider() + -> (StreamableProvider, ChainRegistryTestObservable) { + let repository = ChainRegistryTestRepository() + let observable = ChainRegistryTestObservable() let source = fearless.EmptyStreamableSource() - return StreamableProvider( + let provider = StreamableProvider( source: AnyStreamableSource(source), repository: AnyDataProviderRepository(repository), observable: AnyDataProviderRepositoryObservable(observable), - operationManager: fearless.OperationManagerFacade.sharedManager + operationManager: OperationManager() ) + + return (provider, observable) } private func makeChain( @@ -255,7 +237,7 @@ final class ChainRegistryTests: XCTestCase { } private func waitForValue( - timeout: TimeInterval = 2, + timeout: TimeInterval = 10, _ valueProvider: () -> T? ) throws -> T { let deadline = Date().addingTimeInterval(timeout) @@ -274,7 +256,7 @@ final class ChainRegistryTests: XCTestCase { } private func waitForCondition( - timeout: TimeInterval = 2, + timeout: TimeInterval = 10, _ condition: () -> Bool ) throws { let _: Bool = try waitForValue(timeout: timeout) { @@ -282,15 +264,6 @@ final class ChainRegistryTests: XCTestCase { } } - private func save( - _ chain: ChainModel, - in repository: CoreDataRepository - ) throws { - let operation = repository.saveOperation({ [chain] }, { [] }) - OperationQueue().addOperations([operation], waitUntilFinished: true) - let _: Void = try XCTUnwrap(operation.result).get() - } - private func makeRuntimeProviderPool() -> MockRuntimeProviderPoolProtocol { let pool = MockRuntimeProviderPoolProtocol() let provider = NoopRuntimeProvider() @@ -331,6 +304,140 @@ final class ChainRegistryTests: XCTestCase { } } +private final class ChainRegistryTestRepository: + DataProviderRepositoryProtocol { + typealias Model = ChainModel + + func fetchOperation( + by _: @escaping () throws -> [String], + options _: RepositoryFetchOptions + ) -> BaseOperation<[ChainModel]> { + ClosureOperation { [] } + } + + func fetchOperation( + by _: @escaping () throws -> String, + options _: RepositoryFetchOptions + ) -> BaseOperation { + ClosureOperation { nil } + } + + func fetchAllOperation( + with _: RepositoryFetchOptions + ) -> BaseOperation<[ChainModel]> { + ClosureOperation { [] } + } + + func fetchOperation( + by _: RepositorySliceRequest, + options _: RepositoryFetchOptions + ) -> BaseOperation<[ChainModel]> { + ClosureOperation { [] } + } + + func saveOperation( + _: @escaping () throws -> [ChainModel], + _: @escaping () throws -> [String] + ) -> BaseOperation { + ClosureOperation { () } + } + + func saveBatchOperation( + _: @escaping () throws -> [ChainModel], + _: @escaping () throws -> [String] + ) -> BaseOperation { + ClosureOperation { () } + } + + func replaceOperation( + _: @escaping () throws -> [ChainModel] + ) -> BaseOperation { + ClosureOperation { () } + } + + func fetchCountOperation() -> BaseOperation { + ClosureOperation { 0 } + } + + func deleteAllOperation() -> BaseOperation { + ClosureOperation { () } + } +} + +private final class ChainRegistryTestObservable: + DataProviderRepositoryObservable { + typealias Model = ChainModel + + private let condition = NSCondition() + private weak var observer: AnyObject? + private var deliveryQueue: DispatchQueue? + private var updateBlock: (([DataProviderChange]) -> Void)? + + func start(completionBlock: @escaping (Error?) -> Void) { + completionBlock(nil) + } + + func stop(completionBlock: @escaping (Error?) -> Void) { + completionBlock(nil) + } + + func addObserver( + _ observer: AnyObject, + deliverOn queue: DispatchQueue, + executing updateBlock: @escaping ([DataProviderChange]) -> Void + ) { + condition.lock() + self.observer = observer + deliveryQueue = queue + self.updateBlock = updateBlock + condition.broadcast() + condition.unlock() + } + + func removeObserver(_ observer: AnyObject) { + condition.lock() + if self.observer === observer { + self.observer = nil + deliveryQueue = nil + updateBlock = nil + } + condition.unlock() + } + + func waitUntilObserverAttached(timeout: TimeInterval = 5) -> Bool { + let deadline = Date().addingTimeInterval(timeout) + + condition.lock() + defer { condition.unlock() } + + while observer == nil || deliveryQueue == nil || updateBlock == nil { + guard condition.wait(until: deadline) else { + return false + } + } + + return true + } + + @discardableResult + func send(_ changes: [DataProviderChange]) -> Bool { + condition.lock() + let queue = deliveryQueue + let block = updateBlock + condition.unlock() + + guard let queue, let block else { + return false + } + + queue.async { + block(changes) + } + + return true + } +} + private enum ChainRegistryTestError: Error { case timeout } From 2e45e55dc03ad904598e730cfb5994fb5c1072dc Mon Sep 17 00:00:00 2001 From: Makoto Takemiya Date: Fri, 31 Jul 2026 22:12:40 +0400 Subject: [PATCH 29/30] fix(ios): preserve TON journals across OS upgrades Signed-off-by: Makoto Takemiya --- .../Model/TonPendingIntentJournal.swift | 122 +++++++++++++++++- .../TonPendingIntentJournalTests.swift | 67 +++++++++- scripts/audit-transaction-builder-tests.sh | 10 +- .../test-transaction-builder-tests-audit.sh | 58 ++++++++- 4 files changed, 244 insertions(+), 13 deletions(-) diff --git a/fearless/Common/Model/TonPendingIntentJournal.swift b/fearless/Common/Model/TonPendingIntentJournal.swift index 983e6bea37..615465d792 100644 --- a/fearless/Common/Model/TonPendingIntentJournal.swift +++ b/fearless/Common/Model/TonPendingIntentJournal.swift @@ -177,6 +177,17 @@ enum TonPendingIntentJournalCodec { let signedEmulationFeeNanotons: UInt64? } + private enum RecordEncodingOrder { + case utf8Lexicographic + case legacyFoundation17 + } + + private enum JSONScalar { + case boolean(Bool) + case string(String) + case unsigned(UInt64) + } + static func encode(_ pending: TonPendingSignedIntent) throws -> Data { guard let quote = pending.feeQuote else { throw TonPendingIntentJournalError.corrupted @@ -238,7 +249,7 @@ enum TonPendingIntentJournalCodec { signedEmulationAccepted: accepted, signedEmulationFeeNanotons: fee ) - let data = try canonicalEncoder().encode(record) + let data = encodeRecord(record, order: .utf8Lexicographic) guard !data.isEmpty, data.count <= maximumRecordBytes else { throw TonPendingIntentJournalError.corrupted } @@ -256,7 +267,9 @@ enum TonPendingIntentJournalCodec { throw TonPendingIntentJournalError.corrupted } let record = try JSONDecoder().decode(Record.self, from: data) - guard try canonicalEncoder().encode(record) == data, + let canonicalData = encodeRecord(record, order: .utf8Lexicographic) + let legacyData = encodeRecord(record, order: .legacyFoundation17) + guard data == canonicalData || data == legacyData, record.schemaVersion == schemaVersion, record.asset == "native-ton", record.network == "mainnet", @@ -434,10 +447,107 @@ enum TonPendingIntentJournalCodec { try TonTransferTransactionBuilder.inspectSignedMessage(boc).validUntil } - private static func canonicalEncoder() -> JSONEncoder { - let encoder = JSONEncoder() - encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes] - return encoder + /// `JSONEncoder.OutputFormatting.sortedKeys` changed its ordering between + /// iOS 17 and iOS 18 for keys that differ by case at the comparison point + /// (`quotedFeeNanotons` versus `quoteEndpointOrigin`). Persisted bearer + /// journals must survive an OS upgrade, so new records use an explicit + /// UTF-8 byte ordering while the one legacy Foundation ordering remains a + /// read-only migration format. + private static func encodeRecord( + _ record: Record, + order: RecordEncodingOrder + ) -> Data { + var fields: [(String, JSONScalar)] = [ + ("schemaVersion", .unsigned(UInt64(record.schemaVersion))), + ("phase", .string(record.phase)), + ("asset", .string(record.asset)), + ("network", .string(record.network)), + ("senderRaw", .string(record.senderRaw)), + ("recipientRaw", .string(record.recipientRaw)), + ("amountNanotons", .string(record.amountNanotons)), + ("bounce", .boolean(record.bounce)), + ("publicKeyBase64", .string(record.publicKeyBase64)), + ("walletSequenceNumber", .unsigned(record.walletSequenceNumber)), + ("walletIsInitialized", .boolean(record.walletIsInitialized)), + ("templateCreatedAt", .unsigned(record.templateCreatedAt)), + ("quoteIssuedAt", .unsigned(record.quoteIssuedAt)), + ("quoteExpiresAt", .unsigned(record.quoteExpiresAt)), + ("quoteEndpointOrigin", .string(record.quoteEndpointOrigin)), + ("quoteIDHex", .string(record.quoteIDHex)), + ("quotedFeeNanotons", .unsigned(record.quotedFeeNanotons)), + ("unsignedBocBase64", .string(record.unsignedBocBase64)), + ("signedBocBase64", .string(record.signedBocBase64)), + ("signedMessageHashHex", .string(record.signedMessageHashHex)) + ] + if let comment = record.comment { + fields.append(("comment", .string(comment))) + } + if let accepted = record.signedEmulationAccepted { + fields.append(("signedEmulationAccepted", .boolean(accepted))) + } + if let fee = record.signedEmulationFeeNanotons { + fields.append(("signedEmulationFeeNanotons", .unsigned(fee))) + } + + fields.sort { lhs, rhs in + lhs.0.utf8.lexicographicallyPrecedes(rhs.0.utf8) + } + if order == .legacyFoundation17, + let quotedFeeIndex = fields.firstIndex(where: { $0.0 == "quotedFeeNanotons" }), + let endpointIndex = fields.firstIndex(where: { $0.0 == "quoteEndpointOrigin" }) { + let quotedFee = fields.remove(at: quotedFeeIndex) + fields.insert(quotedFee, at: endpointIndex) + } + + var data = Data() + data.append(0x7B) + for (index, field) in fields.enumerated() { + if index > 0 { + data.append(0x2C) + } + appendJSONString(field.0, to: &data) + data.append(0x3A) + switch field.1 { + case let .boolean(value): + data.append(contentsOf: (value ? "true" : "false").utf8) + case let .string(value): + appendJSONString(value, to: &data) + case let .unsigned(value): + data.append(contentsOf: String(value).utf8) + } + } + data.append(0x7D) + return data + } + + private static func appendJSONString(_ value: String, to data: inout Data) { + let hexadecimal = Array("0123456789abcdef".utf8) + data.append(0x22) + for byte in value.utf8 { + switch byte { + case 0x08: + data.append(contentsOf: "\\b".utf8) + case 0x09: + data.append(contentsOf: "\\t".utf8) + case 0x0A: + data.append(contentsOf: "\\n".utf8) + case 0x0C: + data.append(contentsOf: "\\f".utf8) + case 0x0D: + data.append(contentsOf: "\\r".utf8) + case 0x22: + data.append(contentsOf: "\\\"".utf8) + case 0x5C: + data.append(contentsOf: "\\\\".utf8) + case 0x00 ... 0x1F: + data.append(contentsOf: "\\u00".utf8) + data.append(hexadecimal[Int(byte >> 4)]) + data.append(hexadecimal[Int(byte & 0x0F)]) + default: + data.append(byte) + } + } + data.append(0x22) } private static func decodeCanonicalBase64(_ value: String) throws -> Data { diff --git a/fearlessTests/TonPendingIntentJournalTests.swift b/fearlessTests/TonPendingIntentJournalTests.swift index 089e1bb5d3..607241d250 100644 --- a/fearlessTests/TonPendingIntentJournalTests.swift +++ b/fearlessTests/TonPendingIntentJournalTests.swift @@ -39,6 +39,19 @@ final class TonPendingIntentJournalTests: XCTestCase { Data(SHA256.hash(data: encoded)).map { String(format: "%02x", $0) }.joined(), "3238980e4f69c2fdd20fa2666a713771b4bec2ee4154255f154f827679a05067" ) + + let legacyData = try legacyFoundation17Encoding(of: encoded) + XCTAssertEqual( + Data(SHA256.hash(data: legacyData)).map { String(format: "%02x", $0) }.joined(), + "b3d3d3d094dd14e4990e20f2ae569af11c02c50d6d9269d773ff591d1cda4500" + ) + XCTAssertEqual( + try TonPendingIntentJournalCodec.decode( + legacyData, + expectedSenderRaw: Self.senderRaw + ), + pending + ) XCTAssertLessThanOrEqual(encoded.count, TonPendingIntentJournalCodec.maximumRecordBytes) let text = try XCTUnwrap(String(data: encoded, encoding: .utf8)) XCTAssertFalse(text.contains(Self.mnemonic)) @@ -74,6 +87,11 @@ final class TonPendingIntentJournalTests: XCTestCase { Data(repeating: 0x41, count: TonPendingIntentJournalCodec.maximumRecordBytes + 1), Data("not-json".utf8), Data((" " + text).utf8), + replacing( + text, + "\"amountNanotons\":\"100000000\",\"asset\":\"native-ton\",\"bounce\":true", + with: "\"amountNanotons\":\"100000000\",\"bounce\":true,\"asset\":\"native-ton\"" + ), replacing(text, "\"schemaVersion\":1", with: "\"schemaVersion\":2"), replacing(text, "\"possiblyExposed\"", with: "\"unknownPhase123\""), replacing(text, "\"native-ton\"", with: "\"jettonxxxx\""), @@ -112,6 +130,34 @@ final class TonPendingIntentJournalTests: XCTestCase { } } + func testCanonicalEncodingEscapesUnicodeMemoAndOmitsNilMemo() throws { + let comment = "quoted \"memo\" / path \\ café 😀" + let pending = try makePending(emulated: true, comment: comment) + let encoded = try TonPendingIntentJournalCodec.encode(pending) + let text = try XCTUnwrap(String(data: encoded, encoding: .utf8)) + XCTAssertTrue( + text.contains("\"comment\":\"quoted \\\"memo\\\" / path \\\\ café 😀\"") + ) + XCTAssertFalse(text.contains("\\/")) + let decoded = try TonPendingIntentJournalCodec.decode( + encoded, + expectedSenderRaw: Self.senderRaw + ) + XCTAssertEqual(decoded, pending) + XCTAssertEqual(try TonPendingIntentJournalCodec.encode(decoded), encoded) + + let noComment = try makePending(emulated: false, comment: nil) + let noCommentData = try TonPendingIntentJournalCodec.encode(noComment) + XCTAssertFalse(String(decoding: noCommentData, as: UTF8.self).contains("\"comment\"")) + XCTAssertEqual( + try TonPendingIntentJournalCodec.decode( + noCommentData, + expectedSenderRaw: Self.senderRaw + ), + noComment + ) + } + func testJournalConflictAndKeychainFailuresFailClosed() throws { let journal = TonInMemoryPendingIntentJournal() let first = try makePending(emulated: false) @@ -172,14 +218,17 @@ final class TonPendingIntentJournalTests: XCTestCase { } } - private func makePending(emulated: Bool) throws -> TonPendingSignedIntent { + private func makePending( + emulated: Bool, + comment: String? = "journal" + ) throws -> TonPendingSignedIntent { let details = TonNativeSendRequest( mnemonic: Self.mnemonic, senderAddress: Self.senderRaw, recipientAddress: Self.recipientRaw, amountNanotons: "100000000", bounce: true, - comment: "journal" + comment: comment ) let identity = try TonTransferIntentIdentity(request: details) let intent = try TonEmulationIntent(request: details) @@ -210,7 +259,7 @@ final class TonPendingIntentJournalTests: XCTestCase { transactionRequest: transaction, walletState: walletState, unsignedMessage: unsigned, - feeNanotons: 12_345 + feeNanotons: 12345 ) let signed = try TonTransferTransactionBuilder.buildAndSign( request: transaction, @@ -224,7 +273,7 @@ final class TonPendingIntentJournalTests: XCTestCase { walletState: walletState, feeQuote: quote, emulation: emulated - ? TonEmulationResult(accepted: true, totalFeeNanotons: 12_345) + ? TonEmulationResult(accepted: true, totalFeeNanotons: 12345) : nil ) } @@ -235,6 +284,16 @@ final class TonPendingIntentJournalTests: XCTestCase { return Data(mutated.utf8) } + private func legacyFoundation17Encoding(of canonicalData: Data) throws -> Data { + var text = try XCTUnwrap(String(data: canonicalData, encoding: .utf8)) + let quotedFee = "\"quotedFeeNanotons\":12345," + let quotedFeeRange = try XCTUnwrap(text.range(of: quotedFee)) + text.removeSubrange(quotedFeeRange) + let endpointRange = try XCTUnwrap(text.range(of: "\"quoteEndpointOrigin\":")) + text.insert(contentsOf: quotedFee, at: endpointRange.lowerBound) + return Data(text.utf8) + } + private func replacingFirstBase64Byte(_ source: String, key: String) -> Data { let object = try! JSONSerialization.jsonObject(with: Data(source.utf8)) as! [String: Any] let base64 = object[key] as! String diff --git a/scripts/audit-transaction-builder-tests.sh b/scripts/audit-transaction-builder-tests.sh index aa9c8a7eb9..17e735ce95 100755 --- a/scripts/audit-transaction-builder-tests.sh +++ b/scripts/audit-transaction-builder-tests.sh @@ -130,7 +130,12 @@ require_file "$ton_pending_journal" "TON durable pending-intent journal" require_pattern "$ton_pending_journal" 'final class TonKeychainPendingIntentJournal' "TON Keychain pending-intent journal" require_pattern "$ton_pending_journal" 'jp\.co\.soramitsu\.fearless\.ton\.pending\.v1\.' "TON versioned sender-scoped journal key" require_pattern "$ton_pending_journal" 'maximumRecordBytes = 48 \* 1024' "TON bounded journal record" -require_pattern "$ton_pending_journal" 'encoder\.outputFormatting = \[\.sortedKeys, \.withoutEscapingSlashes\]' "TON canonical journal JSON" +require_pattern "$ton_pending_journal" 'case utf8Lexicographic' "TON runtime-independent canonical journal ordering" +require_pattern "$ton_pending_journal" 'case legacyFoundation17' "TON iOS 17 legacy journal migration order" +require_pattern "$ton_pending_journal" 'lhs\.0\.utf8\.lexicographicallyPrecedes\(rhs\.0\.utf8\)' "TON canonical journal UTF-8 comparator" +require_pattern "$ton_pending_journal" 'data == canonicalData \|\| data == legacyData' "TON strict canonical-or-legacy journal envelope gate" +require_pattern "$ton_pending_journal" 'case 0x22:' "TON canonical journal quote escaping" +require_pattern "$ton_pending_journal" 'case 0x5C:' "TON canonical journal backslash escaping" require_pattern "$ton_pending_journal" 'isValidSignature\(inspection\.signature, for: signingHash\)' "TON persisted signature verification" require_pattern "$ton_pending_journal" 'rebuildSignedMessage' "TON persisted BOC byte-for-byte rebuild" require_pattern "$ton_pending_journal" 'guard rebuiltSigned\.boc == signedBoc' "TON persisted BOC exact equality gate" @@ -216,6 +221,9 @@ require_pattern "$ton_pending_journal_tests" 'maximumRecordBytes \+ 1' "TON over require_pattern "$ton_pending_journal_tests" 'AlwaysFailingKeystore' "TON unavailable Keychain adversarial fixture" require_pattern "$ton_pending_journal_tests" '0eb547b83019bdb5d66d62e35bc31053c00cbd68ba80599e9af1ccddd17e6958' "TON quote-ID known-answer vector" require_pattern "$ton_pending_journal_tests" '3238980e4f69c2fdd20fa2666a713771b4bec2ee4154255f154f827679a05067' "TON journal canonical SHA-256 vector" +require_pattern "$ton_pending_journal_tests" 'b3d3d3d094dd14e4990e20f2ae569af11c02c50d6d9269d773ff591d1cda4500' "TON iOS 17 legacy journal SHA-256 vector" +require_pattern "$ton_pending_journal_tests" 'legacyFoundation17Encoding' "TON exact legacy journal migration fixture" +require_pattern "$ton_pending_journal_tests" 'testCanonicalEncodingEscapesUnicodeMemoAndOmitsNilMemo' "TON canonical journal escaping/optional-field regression" require_pattern "$ton_pending_journal_tests" 'replacingWithStructurallyValidInvalidSignature' "TON structurally valid invalid-signature journal attack" require_file "$chain_model" "TON canonical chain classifier" diff --git a/scripts/test-transaction-builder-tests-audit.sh b/scripts/test-transaction-builder-tests-audit.sh index eb3a48bb42..9aa7816621 100755 --- a/scripts/test-transaction-builder-tests-audit.sh +++ b/scripts/test-transaction-builder-tests-audit.sh @@ -3,7 +3,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" AUDIT_SCRIPT="$SCRIPT_DIR/audit-transaction-builder-tests.sh" -EXPECTED_DESTRUCTIVE_FIXTURES=120 +EXPECTED_DESTRUCTIVE_FIXTURES=128 executed_destructive_fixtures=0 fail() { @@ -73,7 +73,18 @@ write_valid_fixture() { "final class TonKeychainPendingIntentJournal {}" \ 'let identifierPrefix = "jp.co.soramitsu.fearless.ton.pending.v1."' \ "let maximumRecordBytes = 48 * 1024" \ - "encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes]" \ + "enum RecordEncodingOrder { case utf8Lexicographic; case legacyFoundation17 }" \ + "fields.sort { lhs, rhs in lhs.0.utf8.lexicographicallyPrecedes(rhs.0.utf8) }" \ + "let canonicalData = encodeRecord(record, order: .utf8Lexicographic)" \ + "let legacyData = encodeRecord(record, order: .legacyFoundation17)" \ + "guard data == canonicalData || data == legacyData else { return }" \ + "func appendJSONString(_ value: String) {" \ + " switch byte {" \ + " case 0x22: return" \ + " case 0x5C: return" \ + " default: return" \ + " }" \ + "}" \ "let valid = publicKey.isValidSignature(inspection.signature, for: signingHash)" \ "let rebuiltSigned = TonTransferTransactionBuilder.rebuildSignedMessage()" \ "guard rebuiltSigned.boc == signedBoc else { return }" \ @@ -225,8 +236,11 @@ write_valid_fixture() { " _ = TonPendingIntentJournalCodec.maximumRecordBytes + 1" \ "}" \ "func testJournalConflictAndKeychainFailuresFailClosed() {}" \ + "func testCanonicalEncodingEscapesUnicodeMemoAndOmitsNilMemo() {}" \ 'let quoteIDGolden = "0eb547b83019bdb5d66d62e35bc31053c00cbd68ba80599e9af1ccddd17e6958"' \ 'let journalSHAGolden = "3238980e4f69c2fdd20fa2666a713771b4bec2ee4154255f154f827679a05067"' \ + 'let legacyJournalSHAGolden = "b3d3d3d094dd14e4990e20f2ae569af11c02c50d6d9269d773ff591d1cda4500"' \ + "func legacyFoundation17Encoding() {}" \ "func replacingWithStructurallyValidInvalidSignature() {}" \ "final class AlwaysFailingKeystore {}" @@ -790,6 +804,31 @@ write_valid_fixture "$weakened_ton_journal_bound" perl -0pi -e 's/maximumRecordBytes = 48 \* 1024/maximumRecordBytes = Int.max/' "$weakened_ton_journal_bound/fearless/Common/Model/TonPendingIntentJournal.swift" expect_failure "weakened TON journal bound" "$weakened_ton_journal_bound" "TON bounded journal record" +weakened_ton_journal_utf8_order="$tmp_dir/weakened-ton-journal-utf8-order" +write_valid_fixture "$weakened_ton_journal_utf8_order" +perl -0pi -e 's/lhs\.0\.utf8\.lexicographicallyPrecedes\(rhs\.0\.utf8\)/lhs.0.localizedStandardCompare(rhs.0) == .orderedAscending/' "$weakened_ton_journal_utf8_order/fearless/Common/Model/TonPendingIntentJournal.swift" +expect_failure "weakened TON journal UTF-8 order" "$weakened_ton_journal_utf8_order" "TON canonical journal UTF-8 comparator" + +missing_ton_journal_legacy_order="$tmp_dir/missing-ton-journal-legacy-order" +write_valid_fixture "$missing_ton_journal_legacy_order" +perl -0pi -e 's/case legacyFoundation17/case acceptAnyLegacyOrder/' "$missing_ton_journal_legacy_order/fearless/Common/Model/TonPendingIntentJournal.swift" +expect_failure "missing TON journal legacy order" "$missing_ton_journal_legacy_order" "TON iOS 17 legacy journal migration order" + +bypassed_ton_journal_envelope_gate="$tmp_dir/bypassed-ton-journal-envelope-gate" +write_valid_fixture "$bypassed_ton_journal_envelope_gate" +perl -0pi -e 's/data == canonicalData \|\| data == legacyData/true/' "$bypassed_ton_journal_envelope_gate/fearless/Common/Model/TonPendingIntentJournal.swift" +expect_failure "bypassed TON journal envelope gate" "$bypassed_ton_journal_envelope_gate" "TON strict canonical-or-legacy journal envelope gate" + +missing_ton_journal_quote_escape="$tmp_dir/missing-ton-journal-quote-escape" +write_valid_fixture "$missing_ton_journal_quote_escape" +perl -0pi -e 's/case 0x22:/case 0x23:/' "$missing_ton_journal_quote_escape/fearless/Common/Model/TonPendingIntentJournal.swift" +expect_failure "missing TON journal quote escape" "$missing_ton_journal_quote_escape" "TON canonical journal quote escaping" + +missing_ton_journal_backslash_escape="$tmp_dir/missing-ton-journal-backslash-escape" +write_valid_fixture "$missing_ton_journal_backslash_escape" +perl -0pi -e 's/case 0x5C:/case 0x5D:/' "$missing_ton_journal_backslash_escape/fearless/Common/Model/TonPendingIntentJournal.swift" +expect_failure "missing TON journal backslash escape" "$missing_ton_journal_backslash_escape" "TON canonical journal backslash escaping" + missing_ton_journal_signature_verification="$tmp_dir/missing-ton-journal-signature-verification" write_valid_fixture "$missing_ton_journal_signature_verification" perl -0pi -e 's/isValidSignature\(inspection\.signature, for: signingHash\)/isValidSignature(Data(), for: Data())/' "$missing_ton_journal_signature_verification/fearless/Common/Model/TonPendingIntentJournal.swift" @@ -905,6 +944,21 @@ write_valid_fixture "$changed_ton_journal_sha_golden" perl -0pi -e 's/3238980e4f69c2fdd20fa2666a713771b4bec2ee4154255f154f827679a05067/4238980e4f69c2fdd20fa2666a713771b4bec2ee4154255f154f827679a05067/' "$changed_ton_journal_sha_golden/fearlessTests/TonPendingIntentJournalTests.swift" expect_failure "changed TON journal SHA golden" "$changed_ton_journal_sha_golden" "TON journal canonical SHA-256 vector" +changed_ton_legacy_journal_sha_golden="$tmp_dir/changed-ton-legacy-journal-sha-golden" +write_valid_fixture "$changed_ton_legacy_journal_sha_golden" +perl -0pi -e 's/b3d3d3d094dd14e4990e20f2ae569af11c02c50d6d9269d773ff591d1cda4500/a3d3d3d094dd14e4990e20f2ae569af11c02c50d6d9269d773ff591d1cda4500/' "$changed_ton_legacy_journal_sha_golden/fearlessTests/TonPendingIntentJournalTests.swift" +expect_failure "changed TON legacy journal SHA golden" "$changed_ton_legacy_journal_sha_golden" "TON iOS 17 legacy journal SHA-256 vector" + +missing_ton_legacy_journal_fixture="$tmp_dir/missing-ton-legacy-journal-fixture" +write_valid_fixture "$missing_ton_legacy_journal_fixture" +perl -0pi -e 's/legacyFoundation17Encoding/legacyFoundationEncoding/' "$missing_ton_legacy_journal_fixture/fearlessTests/TonPendingIntentJournalTests.swift" +expect_failure "missing TON legacy journal fixture" "$missing_ton_legacy_journal_fixture" "TON exact legacy journal migration fixture" + +missing_ton_journal_escape_regression="$tmp_dir/missing-ton-journal-escape-regression" +write_valid_fixture "$missing_ton_journal_escape_regression" +perl -0pi -e 's/testCanonicalEncodingEscapesUnicodeMemoAndOmitsNilMemo/testCanonicalEncodingUsesFoundationDefaults/' "$missing_ton_journal_escape_regression/fearlessTests/TonPendingIntentJournalTests.swift" +expect_failure "missing TON journal escape regression" "$missing_ton_journal_escape_regression" "TON canonical journal escaping/optional-field regression" + missing_ton_structural_invalid_signature_attack="$tmp_dir/missing-ton-structural-invalid-signature-attack" write_valid_fixture "$missing_ton_structural_invalid_signature_attack" perl -0pi -e 's/replacingWithStructurallyValidInvalidSignature/replacingWithTruncatedSignature/' "$missing_ton_structural_invalid_signature_attack/fearlessTests/TonPendingIntentJournalTests.swift" From ddb3b9e00d78f83f33c7bc75adb19deebbf39dea Mon Sep 17 00:00:00 2001 From: Makoto Takemiya Date: Fri, 31 Jul 2026 23:21:36 +0400 Subject: [PATCH 30/30] fix(ci): update Codecov verification endpoint Pin the reviewed v7.0.0 action commit. Its bundled wrapper uses the live codecovsecops Keybase account after the v5.5.2 codecovsecurity URL began returning HTTP 404. Extend the fail-closed contract with an adversarial regression for the retired action. Signed-off-by: Makoto Takemiya --- .github/workflows/codecov.yml | 2 +- scripts/audit-codecov-coverage-contract.sh | 1 + scripts/audit-codecov-workflow.sh | 4 ++-- scripts/test-codecov-coverage-contract.sh | 9 +++++++-- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 8d6726c867..f10304f649 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -233,7 +233,7 @@ jobs: run: bash ./scripts/ci/run-release-signing-ci.sh - name: Upload coverage to Codecov - uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: fail_ci_if_error: true plugins: xcode diff --git a/scripts/audit-codecov-coverage-contract.sh b/scripts/audit-codecov-coverage-contract.sh index 8cd0a48bef..2fae463d24 100755 --- a/scripts/audit-codecov-coverage-contract.sh +++ b/scripts/audit-codecov-coverage-contract.sh @@ -28,6 +28,7 @@ require_text "plugins: xcode" require_text "swift_project: fearless" require_text "fail_ci_if_error: true" require_text "use_oidc: \${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}" +require_text "codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f" if grep -Eq 'codecov/codecov-action@(main|master|v[0-9]+)[[:space:]#]' "$WORKFLOW"; then fail "Codecov action must use an immutable full commit SHA." diff --git a/scripts/audit-codecov-workflow.sh b/scripts/audit-codecov-workflow.sh index 345b780a85..47a84de340 100755 --- a/scripts/audit-codecov-workflow.sh +++ b/scripts/audit-codecov-workflow.sh @@ -23,8 +23,8 @@ if grep -Eq 'codecov\.io/bash|bash[[:space:]]+<\([[:space:]]*curl|curl[^|]*\|[[: fi require_active_text \ - 'codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de' \ - 'Codecov action must be pinned to the reviewed v5.5.2 commit.' + 'codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f' \ + 'Codecov action must be pinned to the reviewed v7.0.0 commit with the live codecovsecops verification-key endpoint.' require_active_text \ 'actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5' \ 'Codecov workflow checkout action must be pinned.' diff --git a/scripts/test-codecov-coverage-contract.sh b/scripts/test-codecov-coverage-contract.sh index 29d34cc530..a2e79e11d3 100755 --- a/scripts/test-codecov-coverage-contract.sh +++ b/scripts/test-codecov-coverage-contract.sh @@ -68,10 +68,15 @@ expect_rejected \ expect_rejected \ "mutable Codecov action reference" \ replace_text \ - "codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de" \ + "codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f" \ "codecov/codecov-action@v7" +expect_rejected \ + "retired Codecov verification-key endpoint action" \ + replace_text \ + "codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f" \ + "codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de" -[[ "$positive_count" == "1" && "$negative_count" == "7" ]] || +[[ "$positive_count" == "1" && "$negative_count" == "8" ]] || fail "unexpected test counts: $positive_count positive, $negative_count negative" echo \