diff --git a/.deepsource.toml b/.deepsource.toml index 74d9a485ee..491ec27001 100644 --- a/.deepsource.toml +++ b/.deepsource.toml @@ -1,7 +1,4 @@ version = 1 -[[analyzers]] -name = "csharp" - [[analyzers]] name = "shell" diff --git a/.editorconfig b/.editorconfig index da93170720..1d474b8c60 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,7 +10,7 @@ insert_final_newline = true # Formatting - remove any whitespace characters preceding newline characters trim_trailing_whitespace = true -[*.{cs,csproj}] +[*.{cs,csproj,props}] # use hard tabs for indentation indent_style = tab @@ -26,14 +26,15 @@ indent_size = 2 [*.cs] +# Spell Checker rules +spelling_languages = en-us +spelling_checkable_types = strings,identifiers,comments +spelling_error_severity = information +spelling_exclusion_path = .\exclusion.dic + # require braces to be on a new line for all csharp_new_line_before_open_brace = all -# Formatting - organize using options - -# do not place System.* using directives before other using directives -dotnet_sort_system_directives_first = true:error - # Formatting - spacing options # require NO space between a cast and the value @@ -295,3 +296,6 @@ dotnet_diagnostic.CA1827.severity = warning # CA1822: This complains to make functions static. Static is evil. Don't let it complain. dotnet_diagnostic.CA1822.severity = none + +# CA1868: Unnecessary call to Set.Contains(item) +dotnet_diagnostic.CA1868.severity = warning diff --git a/.github/workflows/auto-add-to-projcet.yml b/.github/workflows/auto-add-to-projcet.yml index d312ec071c..e6898aff18 100644 --- a/.github/workflows/auto-add-to-projcet.yml +++ b/.github/workflows/auto-add-to-projcet.yml @@ -4,6 +4,7 @@ on: issues: types: - opened + - reopened permissions: {} jobs: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000000..962c7dc153 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,44 @@ +name: "CodeQL" + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + strategy: + fail-fast: false + matrix: + language: [ 'csharp' ] + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + + - name: 'Install dotnet' + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '7.0.x' + + - name: 'Restore packages' + run: dotnet restore + + - name: 'Build project' + run: dotnet build --no-restore --configuration Release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{matrix.language}}" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3d5cd8e537..12c4536ac8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,10 +1,20 @@ -# Wasabi Coding Conventions +# Contributing to Wasabi Wallet + +## How to be useful for the project + +- Any issue labelled as [good first issue](https://github.com/zkSNACKs/WalletWasabi/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) is good to start contributing to Wasabi. +- Always focus on a specific issue in your pull request and avoid unrelated/unnecessary changes. +- Avoid working on complex problems (fees, amount decomposition, coin selection...) without extensive research on the context, either on Github or asking to contributors. +- Avoid working on a UI or UX feature without first seeing a conclusion from a UX meeting. +- Consider filing a new issue or explaining in an opened issue the change that you want to make, and wait for concept ACKs to work on the implementation. +- For backend, the [Relevance Realization Buffet](https://github.com/orgs/zkSNACKs/projects/18/views/48) view is a list of tasks that has to be investigated or tackled. You can assign yourself to an issue or just make the pull request. +- Feel free to join the [zkSNACKs Slack Server](https://join.slack.com/t/tumblebit/shared_invite/enQtNjQ1MTQ2NzQ1ODI0LWIzOTg5YTM3YmNkOTg1NjZmZTQ3NmM1OTAzYmQyYzk1M2M0MTdlZDk2OTQwNzFiNTg1ZmExNzM0NjgzY2M0Yzg) to discuss with other contributors. ## Automatic code clean up **Visual Studio IDE:** -**DO** use [CodeMaid](http://www.codemaid.net/), a Visual Studio extension to automatically clean up your code on saving the file. +**DO** use [CodeMaid](https://www.codemaid.net/), a Visual Studio extension to automatically clean up your code on saving the file. CodeMaid is a non-intrusive code cleanup tool. Wasabi's CodeMaid settings [can be found in the root of the repository](https://github.com/zkSNACKs/WalletWasabi/blob/master/CodeMaid.config). They are automatically picked up by Visual Studio when you open the project, assuming the CodeMaid extension is installed. Unfortunately CodeMaid has no Visual Studio Code extension yet. You can check out the progress on this [under this GitHub issue](https://github.com/codecadwallader/codemaid/issues/273). @@ -55,14 +65,14 @@ If you are a new contributor **DO** keep refactoring pull requests short, uncomp ```cs // GOOD -private AsyncLock AsyncLock { get; } = new AsyncLock(); +private AsyncLock AsyncLock { get; } = new(); using (await AsyncLock.LockAsync()) { ... } // GOOD -private object Lock { get; } = new object(); +private object Lock { get; } = new(); lock (Lock) { ... @@ -131,7 +141,7 @@ private async void Synchronizer_ResponseArrivedAsync(object? sender, EventArgs e ## `ConfigureAwait(false)` Basically every async library method should use `ConfigureAwait(false)` except: -- Methods that touch objects on the UI Thread, like modifying UI controls. +- Methods that touch objects on the UI Thread, like modifying UI controls. - Methods that are unit tests, xUnit [Fact]. **Usage:** @@ -145,13 +155,26 @@ await MyMethodAsync().ConfigureAwait(false); // Note: inside MyMethodAsync() you can still use .ConfigureAwait(false);. var result = await MyMethodAsync(); -// At this point we are still on the UI thread, so you can safely touch UI elements. +// At this point we are still on the UI thread, so you can safely touch UI elements. myUiControl.Text = result; ``` - [ConfigureAwait FAQ](https://devblogs.microsoft.com/dotnet/configureawait-faq/) -## Disposing Subscriptions in ReactiveObjects +## Never throw AggregateException and Exception in a mixed way +It causes confusion and awkward catch clauses. +[Example](https://github.com/zkSNACKs/WalletWasabi/pull/10353/files) + +--- + +# UI Coding Conventions + +The following is a list of UI specific coding conventions. Follow these any time you are contributing code in the following projects: + - `WalletWasabi.Fluent` + - `WalletWasabi.Fluent.Desktop` + - `WalletWasabi.Fluent.Generators` + + ## Disposing Subscriptions in ReactiveObjects **DO** follow [ReactiveUI's Subscription Disposing Conventions](https://reactiveui.net/docs/guidelines/framework/dispose-your-subscriptions). @@ -174,14 +197,14 @@ this.WhenAnyValue(...) ## Subscribe triggered once on initialization -When you subscribe with the usage of `.WhenAnyValue()` right after the creation one call of Subcription will be triggered. This is by design and most of the cases it is fine. Still you can supress this behaviour by adding `Skip(1)`. +When you subscribe with the usage of `.WhenAnyValue()` right after the creation one call of Subcription will be triggered. This is by design and most of the cases it is fine. Still you can supress this behaviour by adding `Skip(1)`. ```cs this.WhenAnyValue(x => x.PreferPsbtWorkflow) .Skip(1) .Subscribe(value => { - // Expensive operation, that should not run unnecessary. + // Expensive operation, that should not run unnecessary. }); ``` @@ -197,13 +220,13 @@ this.WhenAnyValue(x => x.PreferPsbtWorkflow) public class RepositoryViewModel : ReactiveObject { private ObservableAsPropertyHelper _canDoIt; - + public RepositoryViewModel() { _canDoIt = this.WhenAnyValue(...) .ToProperty(this, x => x.CanDoIt, scheduler: RxApp.MainThreadScheduler); } - + public bool CanDoIt => _canDoIt?.Value ?? false; } ``` @@ -236,12 +259,106 @@ Some pointers on how to recognise if we are breaking MVVM: If it seems not possible to implement something without breaking some of this advice please consult with @danwalmsley. -## Avoid using Grid as much as possible, Use Panel instead +## Avoid using Grid as much as possible, Use Panel instead If you don't need any row or column splitting for your child controls, just use `Panel` as your default container control instead of `Grid` since it is a moderately memory and CPU intensive control. -## Never throw AggregateException and Exception in a mixed way -It causes confusion and awkward catch clauses. -[Example](https://github.com/zkSNACKs/WalletWasabi/pull/10353/files) +## ViewModel Hierarchy + +The ViewModel structure should reflect the UI structure as much as possible. This means that ViewModels can have *child* ViewModels directly referenced in their code, just like Views have direct reference to *child* views. + +❌ **DO NOT** write ViewModel code that depends on *parent* or *sibling* ViewModels in the logical UI structure. This harms both testability and maintainability. + +Examples: + + - ✔️ `MainViewModel` represents the Main Wasabi UI and references `NavBarViewModel`. + - ✔️ `NavBarViewModel` represents the left-side navigation bar and references `WalletListViewModel`. + - ❌ `NavBarViewModel` code must NOT reference `MainViewModel` (its logical parent). + - ❌ `WalletListViewModel` code must NOT reference `NavBarViewModel` (its logical parent). + - ❌ `WalletListViewModel` code must NOT reference other ViewModels that are logical children of `NavBarViewModel` (its logical siblings). + +## UI Models + +The UI Model classes (which comprise the *Model* part of the MVVM pattern) sit as an abstraction layer between the UI and the larger Wasabi Object Model (which lives in the `WalletWasabi` project). This layer is responsible for: + + - Exposing Wasabi data and functionality in a UI-friendly manner. Usually in the form of Observables. + + - Avoiding tight coupling between UI code and business logic. This is critical for testability of UI code, mainly ViewModels. + +❌ **DO NOT** write ViewModel code that depends directly on `WalletWasabi` objects such as `Wallet`, `KeyManager`, `HdPubKey`, etc. + +✔️ **DO** write ViewModel code that depends on `IWalletModel`, `IWalletRepository`, `IAddress`, etc. + +❌ **DO NOT** convert regular .NET properties from `WalletWasabi` objects into observables or INPC properties in ViewModel code. + +❌ **DO NOT** convert regular .NET events from `WalletWasabi` objects into observables in ViewModel code. + +✔️ If such conversions are required, **DO** write them into the UI Model layer. + +## UiContext + +ViewModels that depend on external components (such as Navigation, Clipboard, QR Reader, etc) can access these via the `ViewModelBase.UIContext` property. For instance: + + - Get text from clipboard: `var text = await UIContext.Clipboard.GetTextAsync();` + + - Generate QR Code: `await UIContext.QrGenerator.Generate(data);` + + - Open a popup or navigate to another Viewmodel: `UIContext.Navigate().To(....)` + +This is done to facilitate unit testing of viewmodels, since all dependencies that live inside the `UiContext` are designed to be mock-friendly. + +❌ **DO NOT** write Viewmodel code that directly depends on external device-specific components or code that might otherwise not work in the context of a unit test. + +## Source-Generated ViewModel Constructors + +Whenever a ViewModel references its `UiContext` property, the `UiContext` object becomes an actual **dependency** of said ViewModel. It must therefore be initialized, ideally as a constructor parameter. + +In order to minimize the amount of boilerplate required for such initialization, several things occur in this case: + - A new constructor is generated for that ViewModel, including all parameters of any existing constructor plus the UiContext. + - This generated constructor initializes the `UiContext` *after* running the code of the manually written constructor (if any). + - A Roslyn Analyzer inspects any manually written constructors in the ViewModel to prevent references to `UiContext` in the constructor body, before the above mentioned initialization can take place, resulting in `NullReferenceException`s. + - The Analyzer demands the manually written constructor to be declared `private`, so that external instatiation of the ViewModel is done by calling the source-generated constructor. + +❌ Writing code that directly references `UiContext` in a ViewModel's constructor body will result in a compile-time error. + +❌ Writing code that indirectly references `UiContext` in ViewModel's constructor body will result in a run-time `NullReferenceException`. + +✔️ Writing code that directly or indirectly references `UiContext` inside a lambda expression in a ViewModel's constructor body is okay, since this code is deferred to a later time at run-time when the `UiContext` property has already been properly initialized. + +Example: + +```csharp + // ❌ BAD, constructor should be private + public AddressViewModel(IAddress address) + { + if (condition) + { + //❌ BAD, UiContext is null at this point. + UiContext.Navigate().To(someOtherViewModel); + } + } + + // ✔️ GOOD, constructor is private + private AddressViewModel(IAddress address) + { + //✔️ GOOD, UiContext is already initialized when the Command runs + NextCommand = ReactiveCommand.Create(() => UiContext.Navigate().To(someOtherViewModel))); + } +``` + +If you absolutely must reference `UiContext` in the constructor, you can create a public constructor explicitly taking `UiContext` as a parameter: + +```csharp + // ✔️ GOOD, + public AddressViewModel(UiContext uiContext, IAddress address) + { + UiContext = uiContext; + + // ✔️Other code here can safely use the UiContext since it's explicitly initialized above. + } +``` + +In this case, no additional constructors will be generated, and the analyzer will be satisfied. + diff --git a/Contrib/AffiliationServer/README.md b/Contrib/AffiliationServer/README.md index 50dad2ffe1..491db212c3 100644 --- a/Contrib/AffiliationServer/README.md +++ b/Contrib/AffiliationServer/README.md @@ -15,3 +15,60 @@ publicKey: 3059301306072a8648ce3d020106082a8648ce3d0301070342000434117d20172fba2 The `secretKey` must be settled as value in the coordinators `WabiSabiConfig.json` file in the `AffiliationMessageSignerKey` field while the `publicKey` must be shared with all the partners acting as affiliates (those running an affiliation server). + +Revenue Sharing Calculator +-------------- + +A script to calculate the total amount of bitcoins from affiliate. + +This script takes the coinjoin notifications provided by the affiliated which proves the affiliation of unmixed inputs and calculates the total amount of +bitcoins coming from the affiliated. Given Wasabi doesn't store any kind of information related to affiliations, it is the affiliated the one that needs +to prove the affiliation of unmixed inputs by presenting the coinjoin notifications. + +### Example + +This command runs the revenue.fsx script which parses the coinjoin notifications located in the directory `notification`. It verifies the notification is signed +by the coordinator, that's why we must specify the coordinator's affiliation `pubkey`. The `connection` string is for authenticating agains the bitcoin node's +RPC interface (`user:password`, `cookiefile`) which is required to verify the coinjoin transaction referred in the coinjoin notification really happened and +it is in the blockchain. Finally, `affiliate` is also required because a coinjoin notification is valid only for one specific affiliated. + +```bash +$ dotnet fsi revenue.fsx -- --network=TestNet \ + --connection= \ + --path=notification \ + --pubkey=3059301306072a8648ce3d020106082a8648ce3d03010703420004f267804052bd863a1644233b8bfb5b8652ab99bcbfa0fb9c36113a571eb5c0cb7c733dbcf1777c2745c782f96e218bb71d67d15da1a77d37fa3cb96f423e53ba \ + --affiliate=WalletWasabi +``` + +Optional argument `coordinationFeeRate` can also be specified and its default value is `0.003` + +The result can be seen below: + +``` +coinjoin: 25aaef88eec92b18368c52fc3ef5f602f8dac1caea3cd9d5ee2d80ac81e61784 - Total amount: 6669693 satoshis. Share: 20009 + 4319673 0C155D64B106A6E09853F61726BBEA113A434D5528516CE1058A6FC045666294:0 + 2350020 38DED71FC19FB8189B8A90CB545CDDCAA9345FBE7ACC0E605347D9ED260FF00C:0 +coinjoin: e43ed0cadf6997e96c2412a8cf0a0773fb735a52482f3ddcb7669ba8f7ba7bfc - Total amount: 0 satoshis. Share: 0 +coinjoin: fbae1225f9443d88b22551e44fc90c2f9a0c88b33e868de3dd8b481a88e4171f - Total amount: 1062882 satoshis. Share: 3188 + 1062882 A6AA999F9C07A73BB877D46213801FD77A9324ACD06F137915B7467EB175B18F:1 +Total revenue to share: 0.00023197 btc. +``` + +There were three coinjoins. The first one contained two affiliated coins, the second one contained no affiliated coins and the third one contained one +affiliated coin. + +This affiliated contributed a total of 0.00023197 btc in coordination fees. + +### Useful tip + +In case you don't have an indexed bitcoin node locally you can map the RPC port of one remote node as follow: + +```bash +$ ssh -N -L remote-rpc-port:localhost:local-rpc-port server +``` + +For example, for testnet this would look like: + +```bash +$ ssh -N -L 18332:localhost:18332 zk-testing +``` diff --git a/Contrib/AffiliationServer/notification/README.md b/Contrib/AffiliationServer/notification/README.md new file mode 100644 index 0000000000..37f29f7797 --- /dev/null +++ b/Contrib/AffiliationServer/notification/README.md @@ -0,0 +1,4 @@ +Coinjoin Notifications +---------------------- + +This directory contains three real coinjoin notifications for testing and demo. diff --git a/Contrib/AffiliationServer/notification/n1.txt b/Contrib/AffiliationServer/notification/n1.txt new file mode 100644 index 0000000000..105b277b6e --- /dev/null +++ b/Contrib/AffiliationServer/notification/n1.txt @@ -0,0 +1 @@ +{"body":{"transaction_id":"e43ed0cadf6997e96c2412a8cf0a0773fb735a52482f3ddcb7669ba8f7ba7bfc","inputs":[{"prevout":{"hash":"6026c50adbc7261107dcfcdcc9a1c87bfb9493f74ca149ac120efff50af9283a","index":4},"script_pubkey":"0014916b9271efaacbc0e817747d94a00383157bd148","amount":177147,"is_affiliated":false,"is_no_fee":true},{"prevout":{"hash":"0351cf2e1521db00d325f34945b396ccec0c566e1d630288bb6b97d7959a8d8c","index":4},"script_pubkey":"0014f7b209c71bbb9105c7f4f63583ea0ecc33f7a26a","amount":131072,"is_affiliated":false,"is_no_fee":true},{"prevout":{"hash":"e18149e4e0fa337e7356cd516a8e3b7dbd903ec71daabf7ead1d6fae9dfa1c86","index":5},"script_pubkey":"5120782b768614b5988446ef4939d327496bf3f774c89c11860bb8bccefbf8fd0027","amount":32768,"is_affiliated":false,"is_no_fee":true},{"prevout":{"hash":"c720de168c4c0b5855e740eba8e6712767be28a58485f80be1df911a200f0f48","index":17},"script_pubkey":"5120d7ca470eff5de20d76a1be279cea6fcd63dd485ad22a49f7f8eab6196177097a","amount":5000,"is_affiliated":false,"is_no_fee":true}],"outputs":[{"amount":118098,"script_pubkey":"5120509c76013f4f27cecfdcea5e63de58813a736d7cc8aa5f4d244436f1217d54ad"},{"amount":118098,"script_pubkey":"5120a900c768fa280417e9e675332b8e28fe30965146d620cb9b5a0ac6914dcf9381"},{"amount":78863,"script_pubkey":"001494b43f4c56178a84b933bbb9ead63c89b1a2a7c6"},{"amount":10000,"script_pubkey":"001414ad5509e96c0c400716e2606c8fc2cd68339ef8"},{"amount":10000,"script_pubkey":"00142c951089ca62f4ab9cc25d3d50e5c10115a78f56"},{"amount":10000,"script_pubkey":"0014a15b541be8e779a4160548afa1ac5d809d6fade2"}],"slip44_coin_type":1,"fee_rate":300000,"no_fee_threshold":1000000,"min_registrable_amount":5000,"timestamp":1681413569},"signature":"52b9335cf0a4ab75dbc17a97e4bd8fb868a994d2bfce6d69bfee4b7a8057bb45943c8459ab1bfc5e4838eed62a39357255e6943fa710672e63c629d47a49e9be"} diff --git a/Contrib/AffiliationServer/notification/n2.txt b/Contrib/AffiliationServer/notification/n2.txt new file mode 100644 index 0000000000..52375df6c5 --- /dev/null +++ b/Contrib/AffiliationServer/notification/n2.txt @@ -0,0 +1 @@ +{"body":{"transaction_id":"fbae1225f9443d88b22551e44fc90c2f9a0c88b33e868de3dd8b481a88e4171f","inputs":[{"prevout":{"hash":"a6aa999f9c07a73bb877d46213801fd77a9324acd06f137915b7467eb175b18f","index":1},"script_pubkey":"51209690682c5c4c4c75df8d0727f4d8f76cb2f85115c3a4d472d65541a5b247e803","amount":1062882,"is_affiliated":true,"is_no_fee":false},{"prevout":{"hash":"2a31576b7e7d3f03577fdfca0468d6be4cecdce0f019c7cd8294c06d6bf41446","index":6},"script_pubkey":"5120fea1cc55c9849ad4b649e6d8b170271be5659e244c3ef86c6afb07f7fecd3341","amount":100000,"is_affiliated":false,"is_no_fee":true},{"prevout":{"hash":"8fd123ea6ba78ef3425c51517e8f97eb6c7e1628833ea9978d7ab69e9b389b33","index":1},"script_pubkey":"5120a9f4cff19f98a695c46a8fc0f08fa57e8f0660ef7c2f2ca7bce77352d0af337b","amount":59049,"is_affiliated":false,"is_no_fee":true},{"prevout":{"hash":"9a480dc744f9061d4f09ff4e1e522263705e60f8697b2a675da3560eca8f6dd4","index":4},"script_pubkey":"5120339df81e51b7421662fc6196a1d229f4a591c03129fbab14e43ca11cd8e90239","amount":59049,"is_affiliated":false,"is_no_fee":true},{"prevout":{"hash":"a3f3b1cc484aab13ef5df740a6e64bf8fa3bd0d6d25870fc213e398c6de99f95","index":2},"script_pubkey":"51206f1a57b193a2681379892c714e89486bd2c849786f866ab033a6c45017c730c7","amount":59049,"is_affiliated":false,"is_no_fee":true},{"prevout":{"hash":"0728133582b8adf35555ce8e87cfe861de051b5c90febb39c67fc6daeace046b","index":7},"script_pubkey":"5120b62ed0834e7e26c620f8951a733e6432630a945eb8b474d35f5e497415ffa371","amount":20000,"is_affiliated":false,"is_no_fee":true},{"prevout":{"hash":"53b97b70b2ce794f91f785fbf66bb2efab2261b9611f8ce4c75507b3f5ee0cb6","index":12},"script_pubkey":"512089ca3cd76803b3a449d36e62fafd831aaf3a5c0193becf60df6bfa9c903d1e8c","amount":20000,"is_affiliated":false,"is_no_fee":true},{"prevout":{"hash":"89134e3e96dd930e19e8751ff2243282bc43459f0933a00b371efa4b1a2ffb57","index":9},"script_pubkey":"51202505b55a24b044775043b7284813a7d3f132ae2dd683a38d7836c8456e69c22e","amount":10000,"is_affiliated":false,"is_no_fee":true},{"prevout":{"hash":"70d978a0d7a47fbe12a396ecd19b6b667d3c596342d3e377e159dd56f5b353e1","index":11},"script_pubkey":"5120521c94b76704c516c8539455db2d81a577e34a26fdae49640b1eb8d18160dd6e","amount":5000,"is_affiliated":false,"is_no_fee":true}],"outputs":[{"amount":1193869,"script_pubkey":"0014c957a6b6474f83b20b61ca295c38751f91164dbf"},{"amount":65536,"script_pubkey":"512015f096bc1e31f06d401b1167ac8697c5184e4d7525e43be8c710b88a963055d8"},{"amount":65536,"script_pubkey":"51208cee4cb6bb018d22bc29f054988b0a051b472ff9a33fd2d55728327aa0b4a49d"},{"amount":65536,"script_pubkey":"51209cb58ef714f33ea97ebc97d69f917025b81e77dac01ec2a1c8c1df964b359636"}],"slip44_coin_type":1,"fee_rate":300000,"no_fee_threshold":1000000,"min_registrable_amount":5000,"timestamp":1681415996},"signature":"624bb94fd84dfe42ab707a8e551f40121a5f8ea978d2fd587da897c8a7618978f3eeac53c01ce030aaa4987cee8c21218db9159cfb17fea2ef434819ef8317dd"} diff --git a/Contrib/AffiliationServer/notification/n3.txt b/Contrib/AffiliationServer/notification/n3.txt new file mode 100644 index 0000000000..2afbadf44b --- /dev/null +++ b/Contrib/AffiliationServer/notification/n3.txt @@ -0,0 +1 @@ +{"body":{"transaction_id":"25aaef88eec92b18368c52fc3ef5f602f8dac1caea3cd9d5ee2d80ac81e61784","inputs":[{"prevout":{"hash":"0c155d64b106a6e09853f61726bbea113a434d5528516ce1058a6fc045666294","index":0},"script_pubkey":"512012c15be69ed8796ca19080e9cf23bc786700f964b89b67b0ec609de83ec15f9d","amount":4319673,"is_affiliated":true,"is_no_fee":false},{"prevout":{"hash":"38ded71fc19fb8189b8a90cb545cddcaa9345fbe7acc0e605347d9ed260ff00c","index":0},"script_pubkey":"0014ceefbfab037625421905c717df034a4e2ca2d8a3","amount":2350020,"is_affiliated":true,"is_no_fee":false},{"prevout":{"hash":"a6aa999f9c07a73bb877d46213801fd77a9324acd06f137915b7467eb175b18f","index":2},"script_pubkey":"51206871987001de0c6880c5bf6aca1037a2c0e72129e703c5c7042408c566dd005d","amount":841456,"is_affiliated":false,"is_no_fee":true},{"prevout":{"hash":"0c155d64b106a6e09853f61726bbea113a434d5528516ce1058a6fc045666294","index":8},"script_pubkey":"5120e8be57adaa6e326ee777873ecee17072ae2a2925949743c13efe4b61e8a3944d","amount":118098,"is_affiliated":false,"is_no_fee":true},{"prevout":{"hash":"a478aa865ce1cfb5685e57e0d358831f5ecc96dfe47506031bd33a304c3632fe","index":4},"script_pubkey":"51200876d021f19d815584137ddf6daa6203f7be3f3ebc51729840597a7ee11023f2","amount":65536,"is_affiliated":false,"is_no_fee":true},{"prevout":{"hash":"a6aa999f9c07a73bb877d46213801fd77a9324acd06f137915b7467eb175b18f","index":6},"script_pubkey":"512008b14c78253da82b3a833ad78e2c97ea8f3d020d238adf70bb39928ccaabd39e","amount":65536,"is_affiliated":false,"is_no_fee":true},{"prevout":{"hash":"64cf9670460c0b5212d964450fb75779a0cb952a46a97acfd7e967581cb77a4f","index":3},"script_pubkey":"5120e8be57adaa6e326ee777873ecee17072ae2a2925949743c13efe4b61e8a3944d","amount":39366,"is_affiliated":false,"is_no_fee":true},{"prevout":{"hash":"945fb4d47fc140b3c5f159f504230dc21268b8d3b3c95ef5ed2412aeeb2732cc","index":3},"script_pubkey":"5120237b63777b1822661014e7f1f00563ba0bb918d7a16460f4446cf9d95aae9210","amount":32768,"is_affiliated":false,"is_no_fee":true},{"prevout":{"hash":"d4f411077101e9bac8345e31ab5dd1de3b73b26311cb05291607c2ac81e02f1d","index":5},"script_pubkey":"512029824d474bcf840d7bca4a2268215c8de122e6de02e7b9e2f30322553eba35cf","amount":32768,"is_affiliated":false,"is_no_fee":true},{"prevout":{"hash":"a3f3b1cc484aab13ef5df740a6e64bf8fa3bd0d6d25870fc213e398c6de99f95","index":4},"script_pubkey":"51205852d05b020ce3fb6974182a5203f5cea3ac3bb63ae25cef691d1928643c861a","amount":23884,"is_affiliated":false,"is_no_fee":true},{"prevout":{"hash":"8fd123ea6ba78ef3425c51517e8f97eb6c7e1628833ea9978d7ab69e9b389b33","index":3},"script_pubkey":"5120f77215733341c48713233eb28e45ae84d9875f4643d494ef2cdcfe31c80b5ebf","amount":20399,"is_affiliated":false,"is_no_fee":true},{"prevout":{"hash":"0728133582b8adf35555ce8e87cfe861de051b5c90febb39c67fc6daeace046b","index":5},"script_pubkey":"51205ce345ee3b5868561fb3d2485a7f5a5e29b7c9b1967bb3d86e80b12a51be31da","amount":20000,"is_affiliated":false,"is_no_fee":true},{"prevout":{"hash":"2a31576b7e7d3f03577fdfca0468d6be4cecdce0f019c7cd8294c06d6bf41446","index":11},"script_pubkey":"512069b1dbde34c05eee77d1ac8d0e05ae796fab3f3d31c1e3eb4cb03fdbb224d217","amount":20000,"is_affiliated":false,"is_no_fee":true},{"prevout":{"hash":"89134e3e96dd930e19e8751ff2243282bc43459f0933a00b371efa4b1a2ffb57","index":10},"script_pubkey":"5120a1f6ca57366ef71bb8ab6713c60be29d1ae2798e627bab89f98a230fa8b35080","amount":10000,"is_affiliated":false,"is_no_fee":true},{"prevout":{"hash":"17a2d9d9d7008f84f980e7ea3ac8798a3521a9196f3706ca2077e8601dc78b8a","index":2},"script_pubkey":"5120321ca665c9146c3218c8cb6c8b69b7d257cf2e533863963a83a594f8a72c9774","amount":9776,"is_affiliated":false,"is_no_fee":true},{"prevout":{"hash":"945fb4d47fc140b3c5f159f504230dc21268b8d3b3c95ef5ed2412aeeb2732cc","index":9},"script_pubkey":"512016702abacc17e712063dcc8c756cabd61924f712e6aed307afe2c37b3cf6b176","amount":5000,"is_affiliated":false,"is_no_fee":true}],"outputs":[{"amount":2197152,"script_pubkey":"001474636f7f940a77818a7b2b7ce3fe5a90ab83d6d8"},{"amount":2197152,"script_pubkey":"0014f9158c924fafcab75b7c3622b29ca164493dbbfa"},{"amount":2097152,"script_pubkey":"5120ae8610ff7cff17e212ca856cfdcfe7f39adeb5de32f17fc9d8476a6454274ddc"},{"amount":315701,"script_pubkey":"5120ab782deacf77914f745fa0717038ec951155e9ca0f43b0e0cc07a7a5940cfde7"},{"amount":159049,"script_pubkey":"00141cdf1df01e30efa389b04073080603bd8dc83ea7"},{"amount":100000,"script_pubkey":"00140a95d2c62ddd37eac24c8d46611b199abe0c8586"},{"amount":100000,"script_pubkey":"00143610971f0170f2c824e98dc18a8012f4a458a55d"},{"amount":100000,"script_pubkey":"00145656726e798ff9d9056cd80a6c46f2bc17d8e966"},{"amount":100000,"script_pubkey":"001481e4f99a2c53d3fd1b4ac440def91c5fbbd9ad17"},{"amount":100000,"script_pubkey":"0014e93cea142e24c6980e48ba0ce8bbbbb42253d4dc"},{"amount":100000,"script_pubkey":"0014f5a7665e2e436c43657f9ec298813a57f843f7ca"},{"amount":100000,"script_pubkey":"0014fc0ab4a4ddd4b5f5c396450e75af47fbd3206f7b"},{"amount":100000,"script_pubkey":"512072f1ea2e76675c42026f5ab769190806c9c2d4e72fd49ab73438c1c9d551be2d"},{"amount":100000,"script_pubkey":"5120c34e2367faff5d683006e9b4938ed6c13c130d3345b391d0512cba1ac38bff18"},{"amount":45360,"script_pubkey":"0014dbdb854286f39e98ce4c38a1cdb1fcbc41359b1b"},{"amount":39366,"script_pubkey":"0014076bbec854bd786df40463d4b88fa41d4b6aeb62"},{"amount":19831,"script_pubkey":"001488c3c0ff993dce377a041c936712453b8f8b3224"}],"slip44_coin_type":1,"fee_rate":300000,"no_fee_threshold":1000000,"min_registrable_amount":5000,"timestamp":1681416285},"signature":"e2fbd1c3642b0ea4b6bacf7999b2a36a71e45bfa5999cc2d0b94592504413b76ad64a451fb8f3de10b382f2bc5fc7b2c18b1e8669ae509ab5ace3cf0e87372c0"} diff --git a/Contrib/AffiliationServer/revenue.fsx b/Contrib/AffiliationServer/revenue.fsx new file mode 100644 index 0000000000..ed2dc9ec03 --- /dev/null +++ b/Contrib/AffiliationServer/revenue.fsx @@ -0,0 +1,121 @@ +#r "nuget:fstoolkit.errorhandling" +#r "nuget:nbitcoin" +#r "../../WalletWasabi/bin/Debug/net7.0/WalletWasabi.dll" +#r "System.Security.Cryptography.dll" + +open System.Security.Cryptography + +open System.Text +open NBitcoin +open NBitcoin.RPC +open System +open System.IO +open Newtonsoft.Json +open FsToolkit.ErrorHandling +open WalletWasabi.Affiliation.Models +open WalletWasabi.Affiliation.Serialization +open WalletWasabi.Affiliation.Models.CoinJoinNotification + +module CommandLine = + let tryParseArgument (arg : string) = + match arg.Split([| '=' |]) with + | [| k; v |] -> Some (k, v) + | _ -> None + + let getArg name = + Environment.GetCommandLineArgs() + |> Array.map tryParseArgument + |> Array.choose id + |> Array.tryFind (fun (argName, _) -> argName = name ) + |> Option.map snd + + let getArgOrDefault name defaultValue = + getArg name + |> Option.defaultValue defaultValue + + let getArgOrFail name = + match getArg name with + | Some value -> value + | None -> failwith $"Mandatory argument {name} was not provided" + +module Signature = + + let verifySignature (pubkey:byte[]) (message:byte[]) signature = + let ecdsa = ECDsa.Create() + let _ = ecdsa.ImportSubjectPublicKeyInfo(ReadOnlySpan(pubkey)) + ecdsa.VerifyData(message, signature, HashAlgorithmName.SHA256) + + let verifyNotification affiliate pubkey (notification: CoinJoinNotificationRequest) = + let payload = Payload(Header.Create(affiliate), notification.Body) + verifySignature pubkey (payload.GetCanonicalSerialization()) notification.Signature + +open Signature +open CommandLine + +let args = {| + rpcCredentials = RPCCredentialString.Parse (getArgOrFail "--connection") + network = Network.GetNetwork (getArgOrDefault "--network" "Main") + notificationsPath = getArgOrDefault "--path" "." + affiliate = getArgOrDefault "--affiliate" "trezor" + pubkey = Convert.FromHexString(getArgOrFail "--pubkey") + coordinationFeeRate = decimal (getArgOrDefault "--coordinationFeeRate" "0.003") +|} + +let rpc = RPCClient(args.rpcCredentials, args.network) +let notificationFiles = Directory.EnumerateFiles (args.notificationsPath, "*.txt;*.json") + +type NotificationProcessError = + | ApocryphalNotification of CoinJoinNotificationRequest + | NonexistentTransaction of uint256 * CoinJoinNotificationRequest + +let processNotification notification = result { + do! verifyNotification args.affiliate args.pubkey notification + |> Result.requireTrue (ApocryphalNotification notification) + + let notificationBody = notification.Body + let txId = uint256.Parse(notificationBody.TransactionId) + let! tx = + rpc.GetRawTransaction(txId, false) + |> Result.requireNotNull (NonexistentTransaction (txId, notification)) + + let affInputs = + notificationBody.Inputs + |> Seq.filter (fun x -> x.IsAffiliated && not x.IsNoFee) + |> Seq.map (fun x -> x.Amount, x.Prevout) + |> List.ofSeq + + let affInputSum = List.sumBy fst affInputs + let affShare = int64 (args.coordinationFeeRate * decimal affInputSum) + + return! Ok (txId, affInputs, affInputSum, affShare, notification) +} + +let deserializeCoinJoinNotificationRequest s = + JsonConvert.DeserializeObject(s, AffiliationJsonSerializationOptions.Settings) + +let notificationProcessResult = + notificationFiles + |> Seq.map File.ReadAllText + |> Seq.map deserializeCoinJoinNotificationRequest + |> Seq.map processNotification + +notificationProcessResult +|> Seq.zip notificationFiles +|> Seq.iter ( + function + | _, Ok (txId, affInputs, affInputSum, affShare, _) -> + Console.WriteLine $"coinjoin: {txId} - Total amount: {affInputSum} satoshis. Share: {affShare}" + affInputs + |> List.iter (fun (amount, prevout) -> Console.WriteLine $" {amount} {prevout.Hash |> Convert.ToHexString}:{prevout.Index}") + | file, Error (ApocryphalNotification notificationRequest) -> + Console.WriteLine $"File {file} contains an apocryphal notification request" + | file, Error (NonexistentTransaction (txId, notificationRequest)) -> + Console.WriteLine $"File {file} contains a notification for coinjoin '{txId}' which is not in the blockchain" + ) + +let totalToShare = + notificationProcessResult + |> Seq.choose Result.toOption + |> Seq.sumBy (fun (_, _, _, shared, _) -> shared) + +Console.WriteLine $"Total revenue to share: {Money.Satoshis(totalToShare)} btc." diff --git a/Contrib/CLI/README.md b/Contrib/CLI/README.md new file mode 100644 index 0000000000..619b40ecf4 --- /dev/null +++ b/Contrib/CLI/README.md @@ -0,0 +1,113 @@ +# Wasabi CLI + +A bash script for effortless interaction with the Wasabi RPC Server. + +USAGE: + +```bash +$ ./wcli.sh [-wallet=] command [ARGS,...] +``` +The supported RPC commands are listed in the [documentation](https://docs.wasabiwallet.io/using-wasabi/RPC.html). + +## Examples + +```bash +$ ./wcli.sh getstatus + +{ + "torStatus": "Running", + "backendStatus": "Connected", + "bestBlockchainHeight": "2432219", + "bestBlockchainHash": "0000000000000013081887ac34a2bc356a99a3979a808a4e5d63358412cecc68", + "filtersCount": 5001, + "filtersLeft": 0, + "network": "Main", + "exchangeRate": 29610.18, + "peers": [ + { + "isConnected": true, + "lastSeen": "2023-05-09T18:30:32.4543747+00:00", + "endpoint": "[::ffff:148.251.1.20]:18343", + "userAgent": "/Satoshi:0.20.1/" + }, + { + "isConnected": true, + "lastSeen": "2023-05-09T18:29:25.634355+00:00", + "endpoint": "[::ffff:44.238.21.47]:18333", + "userAgent": "/btcwire:0.5.0/btcd:0.23.2/" + } + ] +} +``` + +```bash +$ ./wcli.sh -wallet=MyWallet getwalletinfo + +{ + "walletName": "MyWallet", + "walletFile": "/home/ricardo/.walletwasabi/client/Wallets/MyWallet.json", + "state": "Started", + "masterKeyFingerprint": "d415c529", + "anonScoreTarget": 5, + "isWatchOnly": false, + "isHardwareWallet": false, + "isAutoCoinjoin": true, + "isRedCoinIsolation": false, + "accounts": [ + { + "name": "segwit", + "publicKey": "tpubDDJNwA959utxokPfGjcvV39BAaGoc16YbF1dL3XC7rY388rS1EcG5BjefhzuP4pzMKAhft4X1d6NHRzUL7emJiLwd2xBmeZ9gR3cAcUEB7G", + "keyPath": "m/84'/0'/0'" + }, + { + "name": "taproot", + "publicKey": "tpubDCVGimU14EWpRjZnbLGDp6uH5St5HWZTcMapVkhb8tWuajRcg99HMbxtSQ9CpSnVHoNGMHMwx3FigonS85iuNmrNEbb2wecB15q1XHTs3br", + "keyPath": "m/86'/0'/0'" + } + ], + "balance": 198738301, + "coinjoinStatus": "Idle" +} +``` + +```bash +$ ./wcli.sh -wallet=MyWallet listkeys | head -10 + +fullkeypath internal keystate label scriptpubkey pubkey pubkeyhash address +84'/0'/0'/1/0 true 2 x-known-by cc74ebb140b0ba6314bacd1f908eb2b9eb041717 039d1e562f46ed0ac30e3abf6f05906f2e42004677f67f2c7c3dd17796553211d3 b41708eb2b9ecc74e0b04a6bb143110bbacd1f97 tb1qkzaxhv2rzjav68us36etnmx8fc9sg9chgzqa3y +84'/0'/0'/0/0 false 2 x-known-by 3ed03e62243c99b3e4cf06678c277b3d6d7b9761 03c7b418ab3035dab6ce6269d21ba97ccf324a6db0b897c7f786eea1ea685dae62 3497c277b3d63ed034d7b9be6223e6c9cf066781 tb1q8jvmuc3run8sveuvyaan6cldqdxhh9mpgtd6sd +86'/0'/0'/1/0 true 0 x-known-by 8651f36e9e65b81e6bb8fc3fbde66ff6282bc8ded7100a9e8e6a1e84de424d00 03a267175d73a4ca1849fe353e9499d4fc49d915b79f0fbd819eb07dfef56be396 af16812c6914de59af9b5751519b8bc6896b688f tb1pvkupxm57dwu0c0aauehlv2r9rl5zhjx76ugq4x8x58hgfhjzf5qqlhy7x8 +86'/0'/0'/0/0 false 0 x-known-by 47206c52cecbe755ff794f870d3289c5fbd46367162349dd74cb2c9db33bdf02 0371795cd2353da445549c88e195964d143d0d127435ed834620a58963394d73f0 d72ef6d450a704cdf52591f66d0035ec880954a7 tb1pe0n4c5k9lau5lpcdx2yutarjqm4agcm8zc35n46vktwfmvemmupqvjctkz +84'/0'/0'/1/1 true 2 x-known-by 78470de64a6b4703697581b2717e6d2b826d5e83 02ef9e5370625182643e5782f608902bc4fe00fbac6af8fddc729e62fd6c0702f2 695e17e6d2b878470a26d70de64368b47581b273 tb1qddrsmejrd96crvn30ekjhpuywz3x6h5rav0da9 +84'/0'/0'/0/1 false 2 x-known-by f742250d0a41696f3371eb21b4f62ac91af99d89 0305f5c2aa51a6c4c972a7202f602081eabdb8f7793937a9abc6a1e1bc894e50c0 439d4f62ac91f7422aaf99650d0f381671eb21b9 tb1qg95k2rg0xdc7kgd57c4vj8m5y240n8vfff39ze +86'/0'/0'/1/1 true 0 x-known-by 8950b84b31724647828bc286c88859f859889d9b5cb59f75b9f55cd88e4bacec 02fdb74cedc8912518e5ff8e2c8d531c75b7bb079474d4b7bc3f2fb1b8db03c1c6 9ce2cebfd391f81e6a94ed272198895cfb0806e9 tb1pwfrysjehs29u9pkg3pvlsky4pvvc38vmtj6e77ul24wd3rjt4nkqux4mnh +86'/0'/0'/0/1 false 0 x-known-by 503a67aaf14652699cff5956cc98c823bdeea7bfdf77b1d5026c2c54512715ca 025655de15dd0eec284fd6a3ca0b11deca53da6fe91e485c92654a40f3d646742f 801560e1101b60296c1cdd31bd11862ab72184a1 tb1pgefx02hennl4j4kvnryz8dgr5cw7afalmammr5pxcfw9g5f8zh9qu96ex4 +84'/0'/0'/1/2 true 2 x-known-by 1dcc474cb16e169396a40fed7a0beaa561056843 02307d454e5ad1ff18fd4d33fb18e5dfe7c03079efba07c357acc257d65402a6a8 6668a0beaa561dcc411056974cb394e1a40fed73 tb1qdctfwn9nj6jqlmt6p0422cwucsgs26zr93kzpz +``` + +```bash +$ ./wcli.sh -wallet=MyWallet gethistory | head -10 + +datetime height amount label tx islikelycoinjoin +2018-06-27T17:39:40+00:00 1326503 1000 x-known-by b2cc6a4f437d915abfd7b851d382102037c3abe7932138fcfb92bc4b5eddd08c false +2018-07-26T05:25:59+00:00 1355500 50000 x-known-by 6144f487705096397a39d2f0e7e23dd3e9c13d71e9cfc74b04e633eaf2f32a7a false +2018-07-26T05:25:59+00:00 1355500 -555 x-known-by ce41d4a5fe0c0da955210bf8722e43af8f580f87a005d7429272bfeb376426ea false +2018-07-26T05:25:59+00:00 1355500 -720 x-known-by 5429cf93723f37af21f3c4ff5bb11e04af6e79a31aedcd4bbe93e9ae88989e1b false +2018-07-26T05:25:59+00:00 1355500 -890 x-known-by 405822d6e574019c9f286b0f548d3824a3e305031556fa62a537b13745a0b0a0 false +2018-08-02T17:33:52+00:00 1356647 -17856 x-known-by a98cca3e7e920bc97b85c17eb256bcaeabf7dec84491a8dfda8850ec4ec9bebf false +2018-08-02T17:33:52+00:00 1356647 -13764 x-known-by 146eec8341caa0ad7bc63d325e3257e8e88fe4042df25bb9bf25e387909ebe8d false +2018-08-02T17:52:24+00:00 1356649 219540 x-known-by 07d6708563d3ab4dba975a3058ec6d907bf6c52a1b2061b694548b77bf359dfd false +2018-08-02T17:52:24+00:00 1356649 -13764 x-known-by b84e225062daeee4064dd7d13396da0b26bbe644d74b9960f9094aa5424f1a32 false +``` + +```bash +$ ./wcli.sh -wallet=MyWallet getnewaddress "Ricardo" + +{ + "address": "tb1qvxvhvnsfx2vwmnum2erzn6k95m8qc7nh5w4hr5", + "keyPath": "84'/0'/0'/0/58", + "label": "Ricardo", + "publicKey": "0363b35d8f3d1e29f49b4479740dfa6d7cb8a90e0797b60963a32e0139d0f5361b", + "scriptPubKey": "00146199764e093298edcf9b564629eac5a6ce0c7a77" +} +``` diff --git a/Contrib/CLI/wcli.sh b/Contrib/CLI/wcli.sh new file mode 100755 index 0000000000..74d9e9c51e --- /dev/null +++ b/Contrib/CLI/wcli.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash + +function config_extract() { + jq -r "$1" ~/.walletwasabi/client/Config.json +} + +CREDENTIALS=$(config_extract '.JsonRpcUser + ":" + .JsonRpcPassword') +ENDPOINT=$(config_extract '.JsonRpcServerPrefixes[0]') +BASIC_AUTH=$([ "$CREDENTIALS" == ":" ] && echo "" || echo "--user ${CREDENTIALS}") + +WALLETNAME="" + +if [ $# -ge 1 ]; then + ARG="${1%=*}" + if [[ "$ARG" == "-wallet" ]]; then + WALLETNAME="${1#*=}/" + shift + fi +fi + +METHOD=$1 +shift + +if [ $# -ge 1 ]; then + if [[ "$1" ]]; then + PARAMS="\"$1\"" + shift + else + PARAMS="\"\"" + shift + fi + + while (( "$#" )); do + if [[ "$1" ]]; then + PARAMS="$PARAMS, $1" + fi + shift + done +fi + +REQUEST="{\"jsonrpc\":\"2.0\", \"id\":\"curltext\", \"method\":\"$METHOD\", \"params\":[$PARAMS]}" +RESULT=$(curl -s $BASIC_AUTH --data-binary "$REQUEST" -H -- "content-type: text/plain;" "$ENDPOINT$WALLETNAME") +CURL_ERRORCODE=$? +RESULT_ERROR=$(echo "$RESULT" | jq -r .error) +CURL_FAIL_TO_CONNECT_ERRORCODE=7 + +rawprint=(help) +if [ $CURL_ERRORCODE -eq $CURL_FAIL_TO_CONNECT_ERRORCODE ]; then + echo "It was not possible to get a response. RPC server could be disabled." +elif [[ "$RESULT_ERROR" == "null" ]]; then + if [[ " ${rawprint[*]} " =~ ${METHOD} ]]; then + echo "$RESULT" | jq -r .result + else + IS_NONEMPTY_ARRAY=$(echo "$RESULT" | jq -r '.result | if type=="array" and length > 0 then "true" else "false" end') + if [[ "$IS_NONEMPTY_ARRAY" == "true" ]]; then + echo "$RESULT" | jq -r '.result | [.[]| with_entries( .key |= ascii_downcase ) ] + | (.[0] |keys_unsorted | @tsv) + , (.[]|.|map(.) |@tsv)' | column -t + else + echo "$RESULT" | jq -r .result + fi + fi +else + echo "$RESULT_ERROR" | jq -r .message +fi diff --git a/Contrib/WalletDiagnostic/README.md b/Contrib/WalletDiagnostic/README.md index 6ec1baffcf..99c0d73632 100644 --- a/Contrib/WalletDiagnostic/README.md +++ b/Contrib/WalletDiagnostic/README.md @@ -36,7 +36,7 @@ Run Wasabi, open the wallet you are interested in. Next open a terminal and enter: ```bash -dotnet fsi coinsgraph.fsx | dot -Tpng | feh - +dotnet fsi txgraph.fsx | dot -Tpng | feh - ``` ## The result diff --git a/Contrib/WalletDiagnostic/common.fsx b/Contrib/WalletDiagnostic/common.fsx index 9c38caf6a9..41b084facc 100644 --- a/Contrib/WalletDiagnostic/common.fsx +++ b/Contrib/WalletDiagnostic/common.fsx @@ -41,7 +41,7 @@ module Rpc = open FSharp.Data type ListCoinsRpcResponse = JsonProvider<"""{ "result": [ - {"txid":"73af1dd","index":0,"amount":2390000,"anonymitySet":"a1.0","confirmed":true,"confirmations":116,"keyPath":"84/0","address":"tb1q","spentBy":"2d7c3f"} + {"txid":"73af1dd","index":0,"amount":4300000000000,"anonymitySet":"a1.0","confirmed":true,"confirmations":116,"keyPath":"84/0","address":"tb1q","spentBy":"2d7c3f"} ] }"""> diff --git a/Contrib/deploy.sh b/Contrib/deploy.sh new file mode 100644 index 0000000000..89f964ef51 --- /dev/null +++ b/Contrib/deploy.sh @@ -0,0 +1,17 @@ +set -e + +SERVICE="walletwasabi.service" + +# Restarting WalletWasabi service.... +sudo systemctl restart $SERVICE +echo "[OK] WalletWasabi service was restarted" + +# Checking deployment... +sleep 1 +systemctl status $SERVICE --no-pager +WASABI_SERVICE_STATUS="$(systemctl is-active $SERVICE)" +if [ "${WASABI_SERVICE_STATUS}" = "active" ]; then + echo "$SERVICE is running" +else + echo "$SERVICE is NOT running" +fi diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 0000000000..7e186284d1 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,44 @@ + + + true + 11.0.5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/NuGet.Config b/NuGet.Config index e45b08e4cc..4d736c19ec 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -3,7 +3,5 @@ - - \ No newline at end of file diff --git a/README.md b/README.md index 7279876385..a6fa69cffb 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Documentation | - + API | @@ -62,7 +62,7 @@
-![](https://raw.githubusercontent.com/zkSNACKs/WalletWasabi/master/WalletWasabi.Backend/wwwroot/img/screenshots/webpage_ui_compilation-02.png) +![](/ui-ww.png) # Build From Source Code diff --git a/WalletWasabi.Backend/Controllers/BatchController.cs b/WalletWasabi.Backend/Controllers/BatchController.cs index fbf2684b08..a15c51d684 100644 --- a/WalletWasabi.Backend/Controllers/BatchController.cs +++ b/WalletWasabi.Backend/Controllers/BatchController.cs @@ -21,39 +21,26 @@ namespace WalletWasabi.Backend.Controllers; [Route("api/v" + Constants.BackendMajorVersion + "/btc/[controller]")] public class BatchController : ControllerBase { - public BatchController(BlockchainController blockchainController, ChaumianCoinJoinController chaumianCoinJoinController, HomeController homeController, OffchainController offchainController, Global global) + public BatchController(BlockchainController blockchainController, OffchainController offchainController, WabiSabiController wabiSabiController, Global global) { BlockchainController = blockchainController; - ChaumianCoinJoinController = chaumianCoinJoinController; - HomeController = homeController; OffchainController = offchainController; + WabiSabiController = wabiSabiController; Global = global; } public Global Global { get; } public BlockchainController BlockchainController { get; } - public ChaumianCoinJoinController ChaumianCoinJoinController { get; } - public HomeController HomeController { get; } public OffchainController OffchainController { get; } + public WabiSabiController WabiSabiController { get; } [HttpGet("synchronize")] + [ResponseCache(Duration = 60)] public async Task GetSynchronizeAsync( [FromQuery, Required] string bestKnownBlockHash, - [FromQuery, Required] int maxNumberOfFilters, - [FromQuery] string? estimateSmartFeeMode = nameof(EstimateSmartFeeMode.Conservative), - [FromQuery] string? indexType = null, + [FromQuery] string indexType = "segwittaproot", CancellationToken cancellationToken = default) { - bool estimateSmartFee = !string.IsNullOrWhiteSpace(estimateSmartFeeMode); - EstimateSmartFeeMode mode = EstimateSmartFeeMode.Conservative; - if (estimateSmartFee) - { - if (!Enum.TryParse(estimateSmartFeeMode, ignoreCase: true, out mode)) - { - return BadRequest("Invalid estimation mode is provided, possible values: ECONOMICAL/CONSERVATIVE."); - } - } - if (!uint256.TryParse(bestKnownBlockHash, out var knownHash)) { return BadRequest($"Invalid {nameof(bestKnownBlockHash)}."); @@ -64,7 +51,8 @@ public async Task GetSynchronizeAsync( return BadRequest("Not supported index type."); } - (Height bestHeight, IEnumerable filters) = indexer.GetFilterLinesExcluding(knownHash, maxNumberOfFilters, out bool found); + var numberOfFilters = Global.Config.Network == Network.Main ? 1000 : 10000; + (Height bestHeight, IEnumerable filters) = indexer.GetFilterLinesExcluding(knownHash, numberOfFilters, out bool found); var response = new SynchronizeResponse { Filters = Enumerable.Empty(), BestHeight = bestHeight }; @@ -82,24 +70,17 @@ public async Task GetSynchronizeAsync( response.Filters = filters; } - response.CcjRoundStates = ChaumianCoinJoinController.GetStatesCollection(); - - if (estimateSmartFee) + try { - try - { - response.AllFeeEstimate = await BlockchainController.GetAllFeeEstimateAsync(mode, cancellationToken); - } - catch (Exception ex) - { - Logger.LogError(ex); - } + response.AllFeeEstimate = await BlockchainController.GetAllFeeEstimateAsync(EstimateSmartFeeMode.Conservative, cancellationToken); + } + catch (Exception ex) + { + Logger.LogError(ex); } response.ExchangeRates = await OffchainController.GetExchangeRatesCollectionAsync(cancellationToken); - response.UnconfirmedCoinJoins = ChaumianCoinJoinController.GetUnconfirmedCoinJoinCollection(); - return Ok(response); } } diff --git a/WalletWasabi.Backend/Controllers/BlockchainController.cs b/WalletWasabi.Backend/Controllers/BlockchainController.cs index 70244de114..c8bf0dfa25 100644 --- a/WalletWasabi.Backend/Controllers/BlockchainController.cs +++ b/WalletWasabi.Backend/Controllers/BlockchainController.cs @@ -58,7 +58,6 @@ public BlockchainController(IMemoryCache memoryCache, Global global) [HttpGet("all-fees")] [ProducesResponseType(200)] [ProducesResponseType(400)] - [ResponseCache(Duration = 300, Location = ResponseCacheLocation.Client)] public async Task GetAllFeesAsync([FromQuery, Required] string estimateSmartFeeMode, CancellationToken cancellationToken) { if (!Enum.TryParse(estimateSmartFeeMode, ignoreCase: true, out EstimateSmartFeeMode mode)) @@ -77,7 +76,7 @@ internal Task GetAllFeeEstimateAsync(EstimateSmartFeeMode mode, return Cache.GetCachedResponseAsync( cacheKey, - action: (string request, CancellationToken token) => RpcClient.EstimateAllFeeAsync(mode, simulateIfRegTest: true, token), + action: (string request, CancellationToken token) => RpcClient.EstimateAllFeeAsync(token), options: CacheEntryOptions, cancellationToken); } @@ -92,7 +91,7 @@ internal Task GetAllFeeEstimateAsync(EstimateSmartFeeMode mode, [HttpGet("mempool-hashes")] [ProducesResponseType(200)] [ProducesResponseType(400)] - [ResponseCache(Duration = 3, Location = ResponseCacheLocation.Client)] + [ResponseCache(Duration = 5)] public async Task GetMempoolHashesAsync([FromQuery] int compactness = 64, CancellationToken cancellationToken = default) { if (compactness is < 1 or > 64) @@ -279,12 +278,13 @@ public async Task BroadcastAsync([FromBody, Required] string hex, /// The best height and an array of block hash : element count : filter pairs. /// When the provided hash is the tip. /// The provided hash was malformed or the count value is out of range - /// If the hash is not found. This happens at blockhain reorg. + /// If the hash is not found. This happens at blockchain reorg. [HttpGet("filters")] [ProducesResponseType(200)] // Note: If you add typeof(IList) then swagger UI visualization will be ugly. [ProducesResponseType(204)] [ProducesResponseType(400)] [ProducesResponseType(404)] + [ResponseCache(Duration = 60)] public IActionResult GetFilters([FromQuery, Required] string bestKnownBlockHash, [FromQuery, Required] int count, [FromQuery] string? indexType = null) { if (count <= 0) @@ -399,17 +399,6 @@ private async Task FetchStatusAsync(CancellationToken cancellati status.FilterCreationActive = true; } - // Updating the status of coinjoin. - var validInterval = TimeSpan.FromSeconds(Global.Coordinator.RoundConfig.InputRegistrationTimeout * 2); - if (validInterval < TimeSpan.FromHours(1)) - { - validInterval = TimeSpan.FromHours(1); - } - if (DateTimeOffset.UtcNow - Global.Coordinator.LastSuccessfulCoinJoinTime < validInterval) - { - status.CoinJoinCreationActive = true; - } - // Updating the status of WabiSabi coinjoin. if (Global.WabiSabiCoordinator is { } wabiSabiCoordinator) { diff --git a/WalletWasabi.Backend/Controllers/ChaumianCoinJoinController.cs b/WalletWasabi.Backend/Controllers/ChaumianCoinJoinController.cs deleted file mode 100644 index 10a380fd3e..0000000000 --- a/WalletWasabi.Backend/Controllers/ChaumianCoinJoinController.cs +++ /dev/null @@ -1,817 +0,0 @@ -#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. -#pragma warning disable CS8619 // Nullability of reference types in value doesn't match target type. - -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Caching.Memory; -using NBitcoin; -using NBitcoin.Crypto; -using NBitcoin.RPC; -using Nito.AsyncEx; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using WalletWasabi.BitcoinCore.Rpc; -using WalletWasabi.CoinJoin.Common.Models; -using WalletWasabi.CoinJoin.Coordinator; -using WalletWasabi.CoinJoin.Coordinator.MixingLevels; -using WalletWasabi.CoinJoin.Coordinator.Participants; -using WalletWasabi.CoinJoin.Coordinator.Rounds; -using WalletWasabi.Extensions; -using WalletWasabi.Helpers; -using WalletWasabi.Logging; -using WalletWasabi.WabiSabi.Backend.Banning; -using static WalletWasabi.Crypto.SchnorrBlinding; - -namespace WalletWasabi.Backend.Controllers; - -/// -/// To interact with the Chaumian CoinJoin Coordinator. -/// -[Produces("application/json")] -[Route("api/v" + Constants.BackendMajorVersion + "/btc/[controller]")] -public class ChaumianCoinJoinController : ControllerBase -{ - public ChaumianCoinJoinController(IMemoryCache memoryCache, Global global) - { - Cache = memoryCache; - Global = global; - } - - private IMemoryCache Cache { get; } - public Global Global { get; } - private IRPCClient RpcClient => Global.RpcClient; - private Network Network => Global.Config.Network; - private Coordinator Coordinator => Global.Coordinator; - private CoinVerifier? CoinVerifier => Global.CoinVerifier; - - private static AsyncLock InputsLock { get; } = new AsyncLock(); - private static AsyncLock OutputLock { get; } = new AsyncLock(); - private static AsyncLock SigningLock { get; } = new AsyncLock(); - - /// - /// Satoshi gets various status information. - /// - /// List of CcjRunningRoundStatus (Phase, Denomination, RegisteredPeerCount, RequiredPeerCount, MaximumInputCountPerPeer, FeePerInputs, FeePerOutputs, CoordinatorFeePercent, RoundId, SuccessfulRoundCount) - /// List of CcjRunningRoundStatus (Phase, Denomination, RegisteredPeerCount, RequiredPeerCount, MaximumInputCountPerPeer, FeePerInputs, FeePerOutputs, CoordinatorFeePercent, RoundId, SuccessfulRoundCount) - [HttpGet("states")] - [ProducesResponseType(200)] - public IActionResult GetStates() - { - IEnumerable response = GetStatesCollection(); - - return Ok(response); - } - - internal IEnumerable GetStatesCollection() - { - var response = new List(); - - foreach (CoordinatorRound round in Coordinator.GetRunningRounds()) - { - var state = new RoundStateResponse4 - { - Phase = round.Phase, - SignerPubKeys = round.MixingLevels.SignerPubKeys, - RPubKeys = round.NonceProvider.GetNextNoncesForMixingLevels(), - Denomination = round.MixingLevels.GetBaseDenomination(), - InputRegistrationTimesout = round.InputRegistrationTimesout, - RegisteredPeerCount = round.CountAlices(syncLock: false), - RequiredPeerCount = round.AnonymitySet, - MaximumInputCountPerPeer = 7, // Constant for now. If we want to do something with it later, we'll put it to the config file. - RegistrationTimeout = (int)round.AliceRegistrationTimeout.TotalSeconds, - FeePerInputs = round.FeePerInputs, - FeePerOutputs = round.FeePerOutputs, - CoordinatorFeePercent = round.CoordinatorFeePercent, - RoundId = round.RoundId, - SuccessfulRoundCount = Coordinator.GetCoinJoinCount() // This is round independent, it is only here because of backward compatibility. - }; - - response.Add(state); - } - - return response; - } - - /// - /// Alice registers her inputs. - /// - /// BlindedOutputSignature, UniqueId - /// BlindedOutputSignature, UniqueId, RoundId - /// If request is invalid. - /// Round not found or it is not in InputRegistration anymore. - [HttpPost("inputs")] - [ProducesResponseType(200)] - [ProducesResponseType(400)] - [ProducesResponseType(404)] - public async Task PostInputsAsync([FromBody, Required] InputsRequest4 request) - { - // Validate request. - if (request.RoundId < 0) - { - return BadRequest("Invalid request."); - } - - if (request.Inputs.Count() > 7) - { - return BadRequest("Maximum 7 inputs can be registered."); - } - - using (await InputsLock.LockAsync()) - { - if (!Coordinator.TryGetRound(request.RoundId, out CoordinatorRound? round) || round.Phase != RoundPhase.InputRegistration) - { - return NotFound("No such running round in InputRegistration. Try another round."); - } - - // Do more checks. - try - { - var blindedOutputs = request.BlindedOutputScripts.ToArray(); - int blindedOutputCount = blindedOutputs.Length; - int maxBlindedOutputCount = round.MixingLevels.Count(); - if (blindedOutputCount > maxBlindedOutputCount) - { - return BadRequest($"Too many blinded output was provided: {blindedOutputCount}, maximum: {maxBlindedOutputCount}."); - } - - if (blindedOutputs.Distinct().Count() < blindedOutputs.Length) - { - return BadRequest("Duplicate blinded output found."); - } - - if (round.ContainsAnyBlindedOutputScript(blindedOutputs.Select(x => x.BlindedOutput))) - { - return BadRequest("Blinded output has already been registered."); - } - - if (request.ChangeOutputAddress.Network != Network) - { - // RegTest and TestNet address formats are sometimes the same. - if (Network == Network.Main) - { - return BadRequest($"Invalid ChangeOutputAddress Network."); - } - } - - var uniqueInputs = new HashSet(); - foreach (InputProofModel inputProof in request.Inputs) - { - var outpoint = inputProof.Input; - if (uniqueInputs.Contains(outpoint)) - { - return BadRequest("Cannot register an input twice."); - } - uniqueInputs.Add(outpoint); - } - - var alicesToRemove = new HashSet(); - var getTxOutResponses = new List<(InputProofModel inputModel, Task getTxOutTask)>(); - - var batch = RpcClient.PrepareBatch(); - - foreach (InputProofModel inputProof in request.Inputs) - { - if (round.ContainsInput(inputProof.Input, out List tr)) - { - alicesToRemove.UnionWith(tr.Select(x => x.UniqueId)); // Input is already registered by this alice, remove it later if all the checks are completed fine. - } - if (Coordinator.AnyRunningRoundContainsInput(inputProof.Input, out List tnr)) - { - if (tr.Union(tnr).Count() > tr.Count) - { - return BadRequest("Input is already registered in another round."); - } - } - - OutPoint outpoint = inputProof.Input; - var bannedElem = await Coordinator.UtxoReferee.TryGetBannedAsync(outpoint, notedToo: false); - if (bannedElem is { }) - { - return BadRequest($"Input is banned from participation for {(int)bannedElem.BannedRemaining.TotalMinutes} minutes: {inputProof.Input.N}:{inputProof.Input.Hash}."); - } - - var txOutResponseTask = batch.GetTxOutAsync(inputProof.Input.Hash, (int)inputProof.Input.N, includeMempool: true); - getTxOutResponses.Add((inputProof, txOutResponseTask)); - } - - // Perform all RPC request at once - await batch.SendBatchAsync(); - - byte[] blindedOutputScriptHashesByte = ByteHelpers.Combine(blindedOutputs.Select(x => x.BlindedOutput.ToBytes())); - uint256 blindedOutputScriptsHash = new(Hashes.SHA256(blindedOutputScriptHashesByte)); - - var inputs = new HashSet(CoinEqualityComparer.Default); - var coinAndTxOutResponses = new Dictionary(CoinEqualityComparer.Default); - - var allInputsConfirmed = true; - foreach (var responses in getTxOutResponses) - { - var (inputProof, getTxOutResponseTask) = responses; - var getTxOutResponse = await getTxOutResponseTask; - - // Check if inputs are unspent. - if (getTxOutResponse is null) - { - return BadRequest($"Provided input is not unspent: {inputProof.Input.N}:{inputProof.Input.Hash}."); - } - - // Check if unconfirmed. - if (getTxOutResponse.Confirmations <= 0) - { - return BadRequest("Provided input is unconfirmed."); - } - - // Check if immature. - if (getTxOutResponse.IsCoinBase && getTxOutResponse.Confirmations <= 100) - { - return BadRequest("Provided input is immature."); - } - - // Check if inputs are native segwit. - if (getTxOutResponse.ScriptPubKeyType != "witness_v0_keyhash") - { - return BadRequest("Provided input must be witness_v0_keyhash."); - } - - TxOut txOut = getTxOutResponse.TxOut; - - var address = (BitcoinWitPubKeyAddress)txOut.ScriptPubKey.GetDestinationAddress(Network); - // Check if proofs are valid. - if (!address.VerifyMessage(blindedOutputScriptsHash, inputProof.Proof)) - { - return BadRequest("Provided proof is invalid."); - } - - var coin = new Coin(inputProof.Input, txOut); - inputs.Add(coin); - coinAndTxOutResponses.Add(coin, getTxOutResponse); - } - - if (!allInputsConfirmed) - { - // Check if mempool would accept a fake transaction created with the registered inputs. - // Fake outputs: mixlevels + 1 maximum, +1 because there can be a change. - var result = await RpcClient.TestMempoolAcceptAsync(inputs, fakeOutputCount: round.MixingLevels.Count() + 1, round.FeePerInputs, round.FeePerOutputs, CancellationToken.None); - if (!result.accept) - { - return BadRequest($"Provided input is from an unconfirmed coinjoin, but a limit is reached: {result.rejectReason}"); - } - } - - var acceptedBlindedOutputScripts = new List(); - - // Calculate expected networkfee to pay after base denomination. - int inputCount = inputs.Count; - Money networkFeeToPayAfterBaseDenomination = (inputCount * round.FeePerInputs) + (2 * round.FeePerOutputs); - - // Check if inputs have enough coins. - Money inputSum = inputs.Sum(x => x.Amount); - Money changeAmount = (inputSum - (round.MixingLevels.GetBaseDenomination() + networkFeeToPayAfterBaseDenomination)); - if (changeAmount < Money.Zero) - { - return BadRequest($"Not enough inputs are provided. Fee to pay: {networkFeeToPayAfterBaseDenomination.ToString(false, true)} BTC. Round denomination: {round.MixingLevels.GetBaseDenomination().ToString(false, true)} BTC. Only provided: {inputSum.ToString(false, true)} BTC."); - } - acceptedBlindedOutputScripts.Add(blindedOutputs.First()); - - Money networkFeeToPay = networkFeeToPayAfterBaseDenomination; - // Make sure we sign the proper number of additional blinded outputs. - var moneySoFar = Money.Zero; - for (int i = 1; i < blindedOutputCount; i++) - { - if (!round.MixingLevels.TryGetDenomination(i, out var denomination)) - { - break; - } - - Money coordinatorFee = denomination.Percentage(round.CoordinatorFeePercent * round.AnonymitySet); // It should be the number of bobs, but we must make sure they'd have money to pay all. - changeAmount -= (denomination + round.FeePerOutputs + coordinatorFee); - networkFeeToPay += round.FeePerOutputs; - - if (changeAmount < Money.Zero) - { - break; - } - - acceptedBlindedOutputScripts.Add(blindedOutputs[i]); - } - - // Make sure Alice checks work. - var alice = new Alice(inputs, networkFeeToPayAfterBaseDenomination, request.ChangeOutputAddress, acceptedBlindedOutputScripts.Select(x => x.BlindedOutput)); - - foreach (Guid aliceToRemove in alicesToRemove) - { - round.RemoveAlicesBy(aliceToRemove); - } - - var blockHeight = await RpcClient.GetBlockCountAsync().ConfigureAwait(false); - - foreach (var coin in alice.Inputs) - { - CoinVerifier?.TryScheduleVerification(coin, round.InputRegistrationTimesout, coinAndTxOutResponses[coin].Confirmations, oneHop: false, currentBlockHeight: blockHeight, CancellationToken.None); - } - - round.AddAlice(alice); - - // All checks are good. Sign. - var blindSignatures = new List(); - for (int i = 0; i < acceptedBlindedOutputScripts.Count; i++) - { - var blindedOutput = acceptedBlindedOutputScripts[i]; - var signer = round.MixingLevels.GetLevel(i).Signer; - uint256 blindSignature = signer.Sign(blindedOutput.BlindedOutput, round.NonceProvider.GetNonceKeyForIndex(blindedOutput.N)); - blindSignatures.Add(blindSignature); - } - alice.BlindedOutputSignatures = blindSignatures.ToArray(); - - // Check if phase changed since. - if (round.Status != CoordinatorRoundStatus.Running || round.Phase != RoundPhase.InputRegistration) - { - return StatusCode(StatusCodes.Status503ServiceUnavailable, "The state of the round changed while handling the request. Try again."); - } - - // Progress round if needed. - if (round.CountAlices() >= round.AnonymitySet) - { - await round.ExecuteNextPhaseAsync(RoundPhase.ConnectionConfirmation); - } - - var resp = new InputsResponse - { - UniqueId = alice.UniqueId, - RoundId = round.RoundId - }; - return Ok(resp); - } - catch (Exception ex) - { - Logger.LogDebug(ex); - return BadRequest(ex.Message); - } - } - } - - /// - /// Alice must confirm her participation periodically in InputRegistration phase and confirm once in ConnectionConfirmation phase. - /// - /// Unique identifier, obtained previously. - /// Round identifier, obtained previously. - /// Current phase and blinded output signatures if Alice is found. - /// Current phase and blinded output signatures if Alice is found. - /// The provided uniqueId or roundId was malformed. - /// If Alice or the round is not found. - /// Participation can be only confirmed from a Running round's InputRegistration or ConnectionConfirmation phase. - [HttpPost("confirmation")] - [ProducesResponseType(200)] - [ProducesResponseType(400)] - [ProducesResponseType(404)] - [ProducesResponseType(410)] - public async Task PostConfirmationAsync([FromQuery, Required] string uniqueId, [FromQuery, Required] long roundId) - { - if (roundId < 0) - { - return BadRequest(); - } - - using (await CoordinatorRound.ConnectionConfirmationLock.LockAsync()) - { - (CoordinatorRound round, Alice alice) = GetRunningRoundAndAliceOrFailureResponse(roundId, uniqueId, RoundPhase.ConnectionConfirmation, out IActionResult returnFailureResponse); - if (returnFailureResponse is { }) - { - return returnFailureResponse; - } - - RoundPhase phase = round.Phase; - - // Start building the response. - var resp = new ConnectionConfirmationResponse - { - CurrentPhase = phase - }; - - switch (phase) - { - case RoundPhase.InputRegistration: - round.StartAliceTimeout(alice.UniqueId); - break; - - case RoundPhase.ConnectionConfirmation: - resp.BlindedOutputSignatures = await round.ConfirmAliceConnectionAsync(alice); - - break; - - default: - TryLogLateRequest(roundId, RoundPhase.ConnectionConfirmation); - return Gone($"Participation can be only confirmed from InputRegistration or ConnectionConfirmation phase. Current phase: {phase}."); - } - - return Ok(resp); - } - } - - /// - /// Alice can revoke her registration without penalty if the current phase is InputRegistration. - /// - /// Unique identifier, obtained previously. - /// Round identifier, obtained previously. - /// Alice or the round was not found. - /// Alice successfully unconfirmed her participation. - /// The provided uniqueId or roundId was malformed. - /// Participation can be only unconfirmed from a Running round's InputRegistration phase. - [HttpPost("unconfirmation")] - [ProducesResponseType(200)] - [ProducesResponseType(204)] - [ProducesResponseType(400)] - [ProducesResponseType(410)] - public IActionResult PostUnconfimation([FromQuery, Required] string uniqueId, [FromQuery, Required] long roundId) - { - if (roundId < 0) - { - return BadRequest(); - } - - Guid uniqueIdGuid = GetGuidOrFailureResponse(uniqueId, out IActionResult returnFailureResponse); - if (returnFailureResponse is { }) - { - return returnFailureResponse; - } - - if (!Coordinator.TryGetRound(roundId, out CoordinatorRound? round)) - { - return Ok("Round not found."); - } - - var alice = round.TryGetAliceBy(uniqueIdGuid); - - if (alice is null) - { - return Ok("Alice not found."); - } - - if (round.Status != CoordinatorRoundStatus.Running) - { - return Gone("Round is not running."); - } - - RoundPhase phase = round.Phase; - switch (phase) - { - case RoundPhase.InputRegistration: - round.RemoveAlicesBy(uniqueIdGuid); - return NoContent(); - - default: - return Gone($"Participation can be only unconfirmed from InputRegistration phase. Current phase: {phase}."); - } - } - - /// - /// Bob registers his output. - /// - /// RoundId. - /// Output is successfully registered. - /// The provided roundId or outputRequest was malformed. - /// Output registration can only be done from OutputRegistration phase. - /// Output registration can only be done from a Running round. - /// If round not found. - [HttpPost("output")] - [ProducesResponseType(204)] - [ProducesResponseType(400)] - [ProducesResponseType(404)] - [ProducesResponseType(409)] - [ProducesResponseType(410)] - public async Task PostOutputAsync([FromQuery, Required] long roundId, [FromBody, Required] OutputRequest request) - { - if (roundId < 0 || request.Level < 0) - { - return BadRequest(); - } - - if (!Coordinator.TryGetRound(roundId, out CoordinatorRound? round)) - { - TryLogLateRequest(roundId, RoundPhase.OutputRegistration); - return NotFound("Round not found."); - } - - if (round.Status != CoordinatorRoundStatus.Running) - { - TryLogLateRequest(roundId, RoundPhase.OutputRegistration); - return Gone("Round is not running."); - } - - RoundPhase phase = round.Phase; - if (phase != RoundPhase.OutputRegistration) - { - TryLogLateRequest(roundId, RoundPhase.OutputRegistration); - return Conflict($"Output registration can only be done from OutputRegistration phase. Current phase: {phase}."); - } - - if (request.OutputAddress.Network != Network) - { - // RegTest and TestNet address formats are sometimes the same. - if (Network == Network.Main) - { - return BadRequest($"Invalid OutputAddress Network."); - } - } - - if (request.Level > round.MixingLevels.GetMaxLevel()) - { - return BadRequest($"Invalid mixing level is provided. Provided: {request.Level}. Maximum: {round.MixingLevels.GetMaxLevel()}."); - } - - if (round.ContainsRegisteredUnblindedSignature(request.UnblindedSignature)) - { - return NoContent(); - } - - MixingLevel mixinglevel = round.MixingLevels.GetLevel(request.Level); - Signer signer = mixinglevel.Signer; - - if (signer.VerifyUnblindedSignature(request.UnblindedSignature, request.OutputAddress.ScriptPubKey.ToBytes())) - { - using (await OutputLock.LockAsync()) - { - try - { - var bob = new Bob(request.OutputAddress, mixinglevel); - round.AddBob(bob); - round.AddRegisteredUnblindedSignature(request.UnblindedSignature); - } - catch (Exception ex) - { - return BadRequest($"Invalid outputAddress is provided. Details: {ex.Message}"); - } - - int bobCount = round.CountBobs(); - int blindSigCount = round.CountBlindSignatures(); - if (bobCount == blindSigCount) // If there'll be more bobs, then round failed. Someone may broke the crypto. - { - await round.ExecuteNextPhaseAsync(RoundPhase.Signing); - } - } - - return NoContent(); - } - return BadRequest("Invalid signature provided."); - } - - /// - /// Alice asks for the final coinjoin transaction. - /// - /// Unique identifier, obtained previously. - /// Round identifier, obtained previously. - /// Hx of the coinjoin transaction. - /// Returns the coinjoin transaction. - /// The provided uniqueId or roundId was malformed. - /// If Alice or the round is not found. - /// Coinjoin can only be requested from Signing phase. - /// Coinjoin can only be requested from a Running round. - [HttpGet("coinjoin")] - [ProducesResponseType(200)] - [ProducesResponseType(400)] - [ProducesResponseType(404)] - [ProducesResponseType(409)] - [ProducesResponseType(410)] - public IActionResult GetCoinJoin([FromQuery, Required] string uniqueId, [FromQuery, Required] long roundId) - { - if (roundId < 0) - { - return BadRequest(); - } - - (CoordinatorRound round, _) = GetRunningRoundAndAliceOrFailureResponse(roundId, uniqueId, RoundPhase.Signing, out IActionResult returnFailureResponse); - if (returnFailureResponse is { }) - { - return returnFailureResponse; - } - - RoundPhase phase = round.Phase; - switch (phase) - { - case RoundPhase.Signing: - var hex = round.UnsignedCoinJoinHex; - if (hex is { }) - { - return Ok(hex); - } - else - { - return NotFound("Hex not found. This should never happen."); - } - default: - TryLogLateRequest(roundId, RoundPhase.Signing); - return Conflict($"Coinjoin can only be requested from Signing phase. Current phase: {phase}."); - } - } - - /// - /// Alice posts her witnesses. - /// - /// Unique identifier, obtained previously. - /// Round identifier, obtained previously. - /// Dictionary that has an int index as its key and string witness as its value. - /// Hx of the coinjoin transaction. - /// Coinjoin successfully signed. - /// The provided uniqueId, roundId or witnesses were malformed. - /// Signatures can only be provided from Signing phase. - /// Signatures can only be provided from a Running round. - /// If Alice or the round is not found. - [HttpPost("signatures")] - [ProducesResponseType(204)] - [ProducesResponseType(400)] - [ProducesResponseType(404)] - [ProducesResponseType(409)] - [ProducesResponseType(410)] - public async Task PostSignaturesAsync([FromQuery, Required] string uniqueId, [FromQuery, Required] long roundId, [FromBody, Required] IDictionary signatures) - { - if (roundId < 0 - || !signatures.Any() - || signatures.Any(x => x.Key < 0 || string.IsNullOrWhiteSpace(x.Value))) - { - return BadRequest(); - } - - (CoordinatorRound round, Alice alice) = GetRunningRoundAndAliceOrFailureResponse(roundId, uniqueId, RoundPhase.Signing, out IActionResult returnFailureResponse); - if (returnFailureResponse is { }) - { - return returnFailureResponse; - } - - // Check if Alice provided signature to all her inputs. - if (signatures.Count != alice.Inputs.Count()) - { - return BadRequest("Alice did not provide enough witnesses."); - } - - RoundPhase phase = round.Phase; - switch (phase) - { - case RoundPhase.Signing: - using (await SigningLock.LockAsync()) - { - foreach (var signaturePair in signatures) - { - int index = signaturePair.Key; - WitScript witness; - try - { - witness = new WitScript(signaturePair.Value); - } - catch (Exception ex) - { - return BadRequest($"Malformed witness is provided. Details: {ex.Message}"); - } - int maxIndex = round.CoinJoin.Inputs.Count - 1; - if (maxIndex < index) - { - return BadRequest($"Index out of range. Maximum value: {maxIndex}. Provided value: {index}"); - } - - // Check duplicates. - if (round.CoinJoin.Inputs[index].HasWitScript()) - { - return BadRequest("Input is already signed."); - } - - // Verify witness. - // 1. Copy UnsignedCoinJoin. - Transaction cjCopy = Transaction.Parse(round.CoinJoin.ToHex(), Network); - // 2. Sign the copy. - cjCopy.Inputs[index].WitScript = witness; - // 3. Convert the current input to IndexedTxIn. - IndexedTxIn currentIndexedInput = cjCopy.Inputs.AsIndexedInputs().Skip(index).First(); - // 4. Find the corresponding registered input. - Coin registeredCoin = alice.Inputs.Single(x => x.Outpoint == cjCopy.Inputs[index].PrevOut); - // 5. Verify if currentIndexedInput is correctly signed, if not, return the specific error. - if (!currentIndexedInput.VerifyScript(registeredCoin, out ScriptError error)) - { - return BadRequest($"Invalid witness is provided. {nameof(ScriptError)}: {error}."); - } - - // Finally add it to our CJ. - round.CoinJoin.Inputs[index].WitScript = witness; - } - - alice.State = AliceState.SignedCoinJoin; - - await round.BroadcastCoinJoinIfFullySignedAsync(); - } - - return NoContent(); - - default: - TryLogLateRequest(roundId, RoundPhase.Signing); - return Conflict($"Coinjoin can only be requested from Signing phase. Current phase: {phase}."); - } - } - - /// - /// Gets the list of unconfirmed coinjoin transaction Ids. - /// - /// The list of coinjoin transactions in the mempool. - /// An array of transaction Ids - [HttpGet("unconfirmed-coinjoins")] - [ProducesResponseType(200)] - public IActionResult GetUnconfirmedCoinjoins() - { - IEnumerable unconfirmedCoinJoinString = GetUnconfirmedCoinJoinCollection().Select(x => x.ToString()); - return Ok(unconfirmedCoinJoinString); - } - - internal IEnumerable GetUnconfirmedCoinJoinCollection() => Global.Coordinator.GetUnconfirmedCoinJoins(); - - private Guid GetGuidOrFailureResponse(string uniqueId, out IActionResult returnFailureResponse) - { - returnFailureResponse = null; - if (string.IsNullOrWhiteSpace(uniqueId)) - { - returnFailureResponse = BadRequest($"Invalid {nameof(uniqueId)} provided."); - } - - Guid aliceGuid = Guid.Empty; - try - { - aliceGuid = Guid.Parse(uniqueId); - } - catch (Exception ex) - { - Logger.LogDebug(ex); - returnFailureResponse = BadRequest($"Invalid {nameof(uniqueId)} provided."); - } - if (aliceGuid == Guid.Empty) // Probably not possible - { - Logger.LogDebug($"Empty {nameof(uniqueId)} GID provided in {nameof(GetCoinJoin)} function."); - returnFailureResponse = BadRequest($"Invalid {nameof(uniqueId)} provided."); - } - - return aliceGuid; - } - - private (CoordinatorRound round, Alice alice) GetRunningRoundAndAliceOrFailureResponse(long roundId, string uniqueId, RoundPhase desiredPhase, out IActionResult returnFailureResponse) - { - returnFailureResponse = null; - - Guid uniqueIdGuid = GetGuidOrFailureResponse(uniqueId, out IActionResult guidFail); - - if (guidFail is { }) - { - returnFailureResponse = guidFail; - return (null, null); - } - - if (!Coordinator.TryGetRound(roundId, out CoordinatorRound? round)) - { - TryLogLateRequest(roundId, desiredPhase); - returnFailureResponse = NotFound("Round not found."); - return (null, null); - } - - var alice = round.TryGetAliceBy(uniqueIdGuid); - if (alice is null) - { - returnFailureResponse = NotFound("Alice not found."); - return (round, null); - } - - if (round.Status != CoordinatorRoundStatus.Running) - { - TryLogLateRequest(roundId, desiredPhase); - returnFailureResponse = Gone("Round is not running."); - } - - return (round, alice); - } - - private static void TryLogLateRequest(long roundId, RoundPhase desiredPhase) - { - try - { - DateTimeOffset ended = CoordinatorRound.PhaseTimeoutLog.TryGet((roundId, desiredPhase)); - if (ended != default) - { - Logger.LogInfo($"{DateTime.UtcNow.ToLocalTime():yyyy-MM-dd HH:mm:ss} {desiredPhase} {(int)(DateTimeOffset.UtcNow - ended).TotalSeconds} seconds late."); - } - } - catch (Exception ex) - { - Logger.LogDebug(ex); - } - } - - /// - /// 409 - /// - private ContentResult Conflict(string content) => new() { StatusCode = (int)HttpStatusCode.Conflict, ContentType = "application/json; charset=utf-8", Content = $"\"{content}\"" }; - - /// - /// 410 - /// - private ContentResult Gone(string content) => new() { StatusCode = (int)HttpStatusCode.Gone, ContentType = "application/json; charset=utf-8", Content = $"\"{content}\"" }; -} - -#pragma warning restore CS8619 // Nullability of reference types in value doesn't match target type. -#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. diff --git a/WalletWasabi.Backend/Controllers/HomeController.cs b/WalletWasabi.Backend/Controllers/HomeController.cs deleted file mode 100644 index 8f64d261ff..0000000000 --- a/WalletWasabi.Backend/Controllers/HomeController.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace WalletWasabi.Backend.Controllers; - -[Route("")] -public class HomeController : ControllerBase -{ - [HttpGet("")] - public IActionResult Index() - { - VirtualFileResult response = File("index.html", "text/html"); - response.LastModified = DateTimeOffset.UtcNow; - return response; - } -} diff --git a/WalletWasabi.Backend/Controllers/OffchainController.cs b/WalletWasabi.Backend/Controllers/OffchainController.cs index 9f72a2184d..de61fc8335 100644 --- a/WalletWasabi.Backend/Controllers/OffchainController.cs +++ b/WalletWasabi.Backend/Controllers/OffchainController.cs @@ -35,6 +35,7 @@ public OffchainController(IMemoryCache memoryCache, IExchangeRateProvider exchan [HttpGet("exchange-rates")] [ProducesResponseType(200)] [ProducesResponseType(404)] + [ResponseCache(Duration = 120)] public async Task GetExchangeRatesAsync(CancellationToken cancellationToken) { IEnumerable exchangeRates = await GetExchangeRatesCollectionAsync(cancellationToken); @@ -51,7 +52,7 @@ internal async Task> GetExchangeRatesCollectionAsync(C { var cacheKey = nameof(GetExchangeRatesCollectionAsync); - if (!Cache.TryGetValue(cacheKey, out IEnumerable exchangeRates)) + if (!Cache.TryGetValue(cacheKey, out IEnumerable? exchangeRates)) { exchangeRates = await ExchangeRateProvider.GetExchangeRateAsync(cancellationToken).ConfigureAwait(false); @@ -63,6 +64,7 @@ internal async Task> GetExchangeRatesCollectionAsync(C Cache.Set(cacheKey, exchangeRates, cacheEntryOptions); } } - return exchangeRates; + + return exchangeRates!; } } diff --git a/WalletWasabi.Backend/Controllers/SoftwareController.cs b/WalletWasabi.Backend/Controllers/SoftwareController.cs index 9708671aa7..6d847c6ef0 100644 --- a/WalletWasabi.Backend/Controllers/SoftwareController.cs +++ b/WalletWasabi.Backend/Controllers/SoftwareController.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Mvc; using WalletWasabi.Backend.Models.Responses; using WalletWasabi.Helpers; +using WalletWasabi.JsonConverters; namespace WalletWasabi.Backend.Controllers; @@ -16,7 +17,8 @@ public class SoftwareController : ControllerBase ClientVersion = Constants.ClientVersion.ToString(3), BackendMajorVersion = Constants.BackendMajorVersion, Ww1LegalDocumentsVersion = Constants.Ww1LegalDocumentsVersion.ToString(), - Ww2LegalDocumentsVersion = Constants.Ww2LegalDocumentsVersion.ToString() + Ww2LegalDocumentsVersion = Constants.Ww2LegalDocumentsVersion.ToString(), + CommitHash = GetCommitHash() }; /// @@ -30,4 +32,7 @@ public VersionsResponse GetVersions() { return _versionsResponse; } + + private static string GetCommitHash() => + ReflectionUtils.GetAssemblyMetadata("CommitHash") ?? ""; } diff --git a/WalletWasabi.Backend/Controllers/WabiSabiController.cs b/WalletWasabi.Backend/Controllers/WabiSabiController.cs index 35b841c750..9310e9782e 100644 --- a/WalletWasabi.Backend/Controllers/WabiSabiController.cs +++ b/WalletWasabi.Backend/Controllers/WabiSabiController.cs @@ -1,10 +1,13 @@ using Microsoft.AspNetCore.Mvc; +using NBitcoin; +using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using WalletWasabi.Affiliation; using WalletWasabi.Backend.Filters; using WalletWasabi.Cache; +using WalletWasabi.WabiSabi.Backend; using WalletWasabi.WabiSabi.Backend.PostRequests; using WalletWasabi.WabiSabi.Backend.Rounds; using WalletWasabi.WabiSabi.Backend.Statistics; @@ -19,12 +22,13 @@ namespace WalletWasabi.Backend.Controllers; [Produces("application/json")] public class WabiSabiController : ControllerBase, IWabiSabiApiRequestHandler { - public WabiSabiController(IdempotencyRequestCache idempotencyRequestCache, Arena arena, CoinJoinFeeRateStatStore coinJoinFeeRateStatStore, AffiliationManager affiliationManager) + public WabiSabiController(IdempotencyRequestCache idempotencyRequestCache, Arena arena, CoinJoinFeeRateStatStore coinJoinFeeRateStatStore, AffiliationManager affiliationManager, CoinJoinMempoolManager coinJoinMempoolManager) { IdempotencyRequestCache = idempotencyRequestCache; Arena = arena; CoinJoinFeeRateStatStore = coinJoinFeeRateStatStore; AffiliationManager = affiliationManager; + CoinJoinMempoolManager = coinJoinMempoolManager; } private static TimeSpan RequestTimeout { get; } = TimeSpan.FromMinutes(5); @@ -32,6 +36,7 @@ public WabiSabiController(IdempotencyRequestCache idempotencyRequestCache, Arena private Arena Arena { get; } private CoinJoinFeeRateStatStore CoinJoinFeeRateStatStore { get; } private AffiliationManager AffiliationManager { get; } + public CoinJoinMempoolManager CoinJoinMempoolManager { get; } [HttpPost("status")] public async Task GetStatusAsync(RoundStateRequest request, CancellationToken cancellationToken) @@ -152,4 +157,19 @@ public HumanMonitorResponse GetHumanMonitor() return new HumanMonitorResponse(response.ToArray()); } + + /// + /// Gets the list of unconfirmed coinjoin transaction Ids. + /// + /// The list of coinjoin transactions in the mempool. + /// An array of transaction Ids + [HttpGet("unconfirmed-coinjoins")] + [ProducesResponseType(200)] + public IActionResult GetUnconfirmedCoinjoins() + { + IEnumerable unconfirmedCoinJoinString = GetUnconfirmedCoinJoinCollection().Select(x => x.ToString()); + return Ok(unconfirmedCoinJoinString); + } + + internal IEnumerable GetUnconfirmedCoinJoinCollection() => CoinJoinMempoolManager.CoinJoinIds; } diff --git a/WalletWasabi.Backend/Filters/ExceptionTranslateFilter.cs b/WalletWasabi.Backend/Filters/ExceptionTranslateFilter.cs index 6e5a54e8fb..ce4479106b 100644 --- a/WalletWasabi.Backend/Filters/ExceptionTranslateFilter.cs +++ b/WalletWasabi.Backend/Filters/ExceptionTranslateFilter.cs @@ -1,6 +1,8 @@ +using System.ComponentModel; using System.Net; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; +using WalletWasabi.Affiliation; using WabiSabi.Crypto; using WalletWasabi.WabiSabi; using WalletWasabi.WabiSabi.Backend.Models; @@ -32,6 +34,14 @@ public override void OnException(ExceptionContext context) { StatusCode = (int)HttpStatusCode.InternalServerError }, + AffiliationException e => new JsonResult(new Error( + Type: AffiliationConstants.RequestSecrecyViolationType, + ErrorCode: "undefined", + Description: e.Message, + ExceptionData: EmptyExceptionData.Instance)) + { + StatusCode = (int)HttpStatusCode.InternalServerError + }, _ => new StatusCodeResult((int)HttpStatusCode.InternalServerError) }; } diff --git a/WalletWasabi.Backend/Global.cs b/WalletWasabi.Backend/Global.cs index 46a2b10c1c..ff5862756d 100644 --- a/WalletWasabi.Backend/Global.cs +++ b/WalletWasabi.Backend/Global.cs @@ -1,6 +1,5 @@ using NBitcoin; using System.IO; -using System.Linq; using System.Net; using System.Net.Http; using System.Threading; @@ -10,8 +9,6 @@ using WalletWasabi.BitcoinCore.Rpc; using WalletWasabi.Blockchain.BlockFilters; using WalletWasabi.Blockchain.Blocks; -using WalletWasabi.CoinJoin.Coordinator; -using WalletWasabi.CoinJoin.Coordinator.Rounds; using WalletWasabi.Helpers; using WalletWasabi.Logging; using WalletWasabi.Services; @@ -20,6 +17,7 @@ using WalletWasabi.WabiSabi.Backend.Banning; using WalletWasabi.WabiSabi.Backend.Rounds.CoinJoinStorage; using WalletWasabi.WabiSabi.Backend.Statistics; +using WalletWasabi.WebClients.Wasabi; namespace WalletWasabi.Backend; @@ -33,11 +31,11 @@ public Global(string dataDir, IRPCClient rpcClient, Config config, IHttpClientFa RpcClient = rpcClient; Config = config; HostedServices = new(); - HttpClient = new(); + CoinVerifierHttpClient = WasabiHttpClientFactory.CreateLongLivedHttpClient(); HttpClientFactory = httpClientFactory; CoordinatorParameters = new(DataDir); - CoinJoinIdStore = CoinJoinIdStore.Create(Path.Combine(DataDir, "CcjCoordinator", $"CoinJoins{RpcClient.Network}.txt"), CoordinatorParameters.CoinJoinIdStoreFilePath); + CoinJoinIdStore = CoinJoinIdStore.Create(CoordinatorParameters.CoinJoinIdStoreFilePath); // We have to find it, because it's cloned by the node and not perfectly cloned (event handlers cannot be cloned.) P2pNode = new(config.Network, config.GetBitcoinP2pEndPoint(), new(), $"/WasabiCoordinator:{Constants.BackendMajorVersion}/"); @@ -51,6 +49,9 @@ public Global(string dataDir, IRPCClient rpcClient, Config config, IHttpClientFa SegwitTaprootIndexBuilderService = new(IndexType.SegwitTaproot, RpcClient, HostedServices.Get(), segwitTaprootIndexFilePath); TaprootIndexBuilderService = new(IndexType.Taproot, RpcClient, HostedServices.Get(), taprootIndexFilePath); + + MempoolMirror = new MempoolMirror(TimeSpan.FromSeconds(21), RpcClient, P2pNode); + CoinJoinMempoolManager = new CoinJoinMempoolManager(CoinJoinIdStore, MempoolMirror); } public string DataDir { get; } @@ -64,39 +65,36 @@ public Global(string dataDir, IRPCClient rpcClient, Config config, IHttpClientFa public IndexBuilderService SegwitTaprootIndexBuilderService { get; } public IndexBuilderService TaprootIndexBuilderService { get; } - private HttpClient HttpClient { get; } + private HttpClient CoinVerifierHttpClient { get; } private IHttpClientFactory HttpClientFactory { get; } - public Coordinator? Coordinator { get; private set; } private CoinVerifierApiClient? CoinVerifierApiClient { get; set; } public CoinVerifier? CoinVerifier { get; private set; } public Config Config { get; } - public CoordinatorRoundConfig? RoundConfig { get; private set; } - private CoordinatorParameters CoordinatorParameters { get; } public CoinJoinIdStore CoinJoinIdStore { get; } public WabiSabiCoordinator? WabiSabiCoordinator { get; private set; } private Whitelist? WhiteList { get; set; } + private MempoolMirror MempoolMirror { get; } + public CoinJoinMempoolManager CoinJoinMempoolManager { get; private set; } - public async Task InitializeAsync(CoordinatorRoundConfig roundConfig, CancellationToken cancel) + public async Task InitializeAsync(CancellationToken cancel) { - RoundConfig = Guard.NotNull(nameof(roundConfig), roundConfig); - // Make sure RPC works. await AssertRpcNodeFullyInitializedAsync(cancel); // Make sure P2P works. await P2pNode.ConnectAsync(cancel).ConfigureAwait(false); - HostedServices.Register(() => new MempoolMirror(TimeSpan.FromSeconds(21), RpcClient, P2pNode), "Full Node Mempool Mirror"); + HostedServices.Register(() => MempoolMirror, "Full Node Mempool Mirror"); var blockNotifier = HostedServices.Get(); var wabiSabiConfig = CoordinatorParameters.RuntimeCoordinatorConfig; - bool coinVerifierEnabled = wabiSabiConfig.IsCoinVerifierEnabled || roundConfig.IsCoinVerifierEnabledForWW1; + bool coinVerifierEnabled = wabiSabiConfig.IsCoinVerifierEnabled; if (coinVerifierEnabled) { @@ -115,11 +113,11 @@ public async Task InitializeAsync(CoordinatorRoundConfig roundConfig, Cancellati throw new ArgumentException($"Risk indicators were not provided in {nameof(WabiSabiConfig)}."); } - HttpClient.BaseAddress = url; - HttpClient.Timeout = CoinVerifierApiClient.ApiRequestTimeout; + CoinVerifierHttpClient.BaseAddress = url; + CoinVerifierHttpClient.Timeout = CoinVerifierApiClient.ApiRequestTimeout; WhiteList = await Whitelist.CreateAndLoadFromFileAsync(CoordinatorParameters.WhitelistFilePath, wabiSabiConfig, cancel).ConfigureAwait(false); - CoinVerifierApiClient = new CoinVerifierApiClient(CoordinatorParameters.RuntimeCoordinatorConfig.CoinVerifierApiAuthToken, HttpClient); + CoinVerifierApiClient = new CoinVerifierApiClient(CoordinatorParameters.RuntimeCoordinatorConfig.CoinVerifierApiAuthToken, CoinVerifierHttpClient); CoinVerifier = new(CoinJoinIdStore, CoinVerifierApiClient, WhiteList, CoordinatorParameters.RuntimeCoordinatorConfig, auditsDirectoryPath: Path.Combine(CoordinatorParameters.CoordinatorDataDir, "CoinVerifierAudits")); Logger.LogInfo("CoinVerifier created successfully."); @@ -130,38 +128,12 @@ public async Task InitializeAsync(CoordinatorRoundConfig roundConfig, Cancellati } } - Coordinator = new(RpcClient.Network, blockNotifier, Path.Combine(DataDir, "CcjCoordinator"), RpcClient, roundConfig, roundConfig.IsCoinVerifierEnabledForWW1 ? CoinVerifier : null); - Coordinator.CoinJoinBroadcasted += Coordinator_CoinJoinBroadcasted; - - var coordinator = Guard.NotNull(nameof(Coordinator), Coordinator); - if (!string.IsNullOrWhiteSpace(roundConfig.FilePath)) - { - HostedServices.Register(() => - new ConfigWatcher( - TimeSpan.FromSeconds(10), // Every 10 seconds check the config - RoundConfig, - () => - { - try - { - coordinator.RoundConfig.UpdateOrDefault(RoundConfig, toFile: false); - - coordinator.AbortAllRoundsInInputRegistration($"{nameof(RoundConfig)} has changed."); - } - catch (Exception ex) - { - Logger.LogDebug(ex); - } - }), - "Config Watcher"); - } - var coinJoinScriptStore = CoinJoinScriptStore.LoadFromFile(CoordinatorParameters.CoinJoinScriptStoreFilePath); WabiSabiCoordinator = new WabiSabiCoordinator(CoordinatorParameters, RpcClient, CoinJoinIdStore, coinJoinScriptStore, HttpClientFactory, wabiSabiConfig.IsCoinVerifierEnabled ? CoinVerifier : null); + blockNotifier.OnBlock += WabiSabiCoordinator.BanDescendant; HostedServices.Register(() => WabiSabiCoordinator, "WabiSabi Coordinator"); - - HostedServices.Register(() => new RoundBootstrapper(TimeSpan.FromMilliseconds(100), Coordinator), "Round Bootstrapper"); + P2pNode.OnTransactionArrived += WabiSabiCoordinator.BanDoubleSpenders; await HostedServices.StartAllAsync(cancel); @@ -171,11 +143,6 @@ public async Task InitializeAsync(CoordinatorRoundConfig roundConfig, Cancellati Logger.LogInfo($"{nameof(TaprootIndexBuilderService)} is successfully initialized and started synchronization."); } - private void Coordinator_CoinJoinBroadcasted(object? sender, Transaction transaction) - { - CoinJoinIdStore!.TryAdd(transaction.GetHash()); - } - private async Task AssertRpcNodeFullyInitializedAsync(CancellationToken cancellationToken) { var rpcClient = Guard.NotNull(nameof(RpcClient), RpcClient); @@ -208,12 +175,8 @@ private async Task AssertRpcNodeFullyInitializedAsync(CancellationToken cancella { if (blocks < 101) { - var generateBlocksResponse = await rpcClient.GenerateAsync(101, cancellationToken); - if (generateBlocksResponse is null) - { - throw new NotSupportedException($"{Constants.BuiltinBitcoinNodeName} cannot generate blocks on the {Network.RegTest}."); - } - + var generateBlocksResponse = await rpcClient.GenerateAsync(101, cancellationToken) + ?? throw new NotSupportedException($"{Constants.BuiltinBitcoinNodeName} cannot generate blocks on the {Network.RegTest}."); blockchainInfo = await rpcClient.GetBlockchainInfoAsync(cancellationToken); blocks = blockchainInfo.Blocks; if (blocks == 0) @@ -237,15 +200,16 @@ protected virtual void Dispose(bool disposing) { if (disposing) { - HttpClient.Dispose(); - - if (Coordinator is { } coordinator) + if (WabiSabiCoordinator is { } wabiSabiCoordinator) { - coordinator.CoinJoinBroadcasted -= Coordinator_CoinJoinBroadcasted; - coordinator.Dispose(); - Logger.LogInfo($"{nameof(coordinator)} is disposed."); + var blockNotifier = HostedServices.Get(); + blockNotifier.OnBlock -= wabiSabiCoordinator.BanDescendant; + P2pNode.OnTransactionArrived -= wabiSabiCoordinator.BanDoubleSpenders; } + CoinVerifierHttpClient.Dispose(); + CoinJoinMempoolManager.Dispose(); + var stoppingTask = Task.Run(DisposeAsync); stoppingTask.GetAwaiter().GetResult(); diff --git a/WalletWasabi.Backend/InitConfigStartupTask.cs b/WalletWasabi.Backend/InitConfigStartupTask.cs index dc9d7e4bfb..db35f357ed 100644 --- a/WalletWasabi.Backend/InitConfigStartupTask.cs +++ b/WalletWasabi.Backend/InitConfigStartupTask.cs @@ -1,7 +1,6 @@ using System.IO; using System.Threading; using System.Threading.Tasks; -using WalletWasabi.CoinJoin.Coordinator.Rounds; using WalletWasabi.Logging; namespace WalletWasabi.Backend; @@ -23,12 +22,7 @@ public async Task ExecuteAsync(CancellationToken cancellationToken) AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException; - var roundConfigFilePath = Path.Combine(Global.DataDir, "CcjRoundConfig.json"); - var roundConfig = new CoordinatorRoundConfig(roundConfigFilePath); - roundConfig.LoadFile(createIfMissing: true); - Logger.LogInfo("RoundConfig is successfully initialized."); - - await Global.InitializeAsync(roundConfig, cancellationToken); + await Global.InitializeAsync(cancellationToken); } private static void TaskScheduler_UnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e) diff --git a/WalletWasabi.Backend/README.md b/WalletWasabi.Backend/README.md index 82daa0e93c..5b239bbd1f 100644 --- a/WalletWasabi.Backend/README.md +++ b/WalletWasabi.Backend/README.md @@ -1,51 +1,35 @@ # API Specification -**ATTENTION:** This document describes the initial specification. The actual implementation may significantly differ. You can find up to date documentation here: +**ATTENTION:** This document describes the initial specification. The actual implementation may differ significantly. You can find up to date documentation here: - TestNet: http://testwnp3fugjln6vh5vpj7mvq3lkqqwjj3c2aafyu7laxz42kgwh2rad.onion/swagger - Main: http://wasabiukrxmkdgve5kynjztuovbg43uxcbcxn6y2okcrsg7gb6jdmbad.onion/swagger -## Related Documents: - -[Backend Deployment And Update Instructions](https://github.com/zkSNACKs/WalletWasabi/blob/master/WalletWasabi.Documentation/BackendDeployment.md) - ## HTTP - Requests and Responses are JSON. + Requests and Responses are in JSON. Requests have the following format: `/api/v4/{coin}/{controller}/`. Currently supported coins: `btc`. - For example requesting exchange rate: `GET /api/v4/btc/offchain/exchange-rates`. - -### Controller: Blockchain, Coin: BTC +### Controller: Blockchain, Coin: btc | API | Description | Request | Response | | --- | ---- | ---- | ---- | -| GET fees?{comma separated confirmationTargets} | Gets fees for the requested confirmation targets based on Bitcoin Core's `estimatesmartfee` output. | | ConfirmationTarget[] contains estimation mode and byte per satoshi pairs. Example: ![](https://i.imgur.com/Ggmif3R.png) | | POST broadcast | Attempts to broadcast a transaction. | Hex | | -| GET exchange-rates | Gets exchange rates for one Bitcoin. | | ExchangeRates[] contains Ticker and ExchangeRate pairs. Example: ![](https://i.imgur.com/Id9cqxq.png) | | GET filters/{blockHash} | Gets block filters from the specified block hash. | | An array of blockHash : filter pairs. | +| GET status | Gets current status of filter and coinjoin creation. | | | #### POST filters - At the initial synchronization the wallet must specify the hash of the first block that contains native segwit output. This hash must be hard coded into the client. - - First block with P2WPKH: dfcec48bb8491856c353306ab5febeb7e99e4d783eedf3de98f3ee0812b92bad - - First block with P2WPKH on TestNet: b29fbe96bf737000f8e3536e9b4681a01b1ca6be3ac4bd1f8269cdbd465e6700 - - Filters are Golomb Rice filters of all the input and output native segregated witness `scriptPubKeys`. Thus wallets using this API can only handle `p2wpkh` scripts, therefore `p2pkh`, `p2sh`, `p2sh` over `p2wph` scripts are not supported. This restriction significantly lowers the size of the `FilterTable`, with that speeds up the wallet. - When a client acquires a filter, it checks against its own keys and downloads the needed blocks from the Bitcoin P2P network, if needed. - -#### Handling Reorgs + At the initial synchronization the wallet must specify the hash of the first block that contains native segwit output, both for SegWit and Taproot. This hash must be hard coded into the client. + - First SegWit block with P2WPKH: 0000000000000000001c8018d9cb3b742ef25114f27563e3fc4a1902167f9893 + - First SegWit block with P2WPKH on TestNet: 00000000000f0d5edcaeba823db17f366be49a80d91d15b77747c2e017b8c20a + - First Taproot block: 0000000000000000000687bca986194dc2c1f949318629b44bb54ec0a94d8244 + - First Taproot block TestNet: 00000000000000216dc4eb2bd27764891ec0c961b0da7562fe63678e164d62a0 - If the answer to the `filters` request is not found, then the client steps back one block and queries the filters with that previous hash. This can happen multiple times. This will only happen when blockchain reorganization has occurred. + Filters are Golomb Rice filters of all the input and output native segregated witness `scriptPubKeys`. Thus wallets using this API can only handle `P2WPKH` and `P2TR` scripts, therefore `P2PKH`, `P2SH`, and `P2WPKH` over `P2SH` scripts are not supported. This restriction significantly lowers the size of the `FilterTable`, which speeds up the wallet filter synchronization. + When a client acquires a filter, it checks against its own keys and downloads the needed blocks from the Bitcoin P2P network, if needed. -### Controller: ChaumianCoinJoin, Coin: BTC +#### Handling Reorgs + + If the answer to the `filters` request is not found, then the client steps back one block and queries the filters with that previous hash. This can happen multiple times. This will only happen when blockchain reorganization has occurred. -| API | Description | Request | Response | -| --- | ---- | ---- | ---- | -| GET status | Satoshi gets various status information. | | CurrentPhase, Denomination, RegisteredPeerCount, RequiredPeerCount, ForcedRoundStartMinutesLeft, MaximumInputCountPerPeer, FeePerInputs, FeePerOutputs, CoordinatorFee, Version | -| POST inputs | Alice registers her inputs. | Inputs[(Input, Proof)], BlindedOutputHex, ChangeOutputs[] | SignedBlindedOutput, UniqueId | -| POST confirmation | Alice must confirm her participation periodically in InputRegistration phase and confirm once in ConnectionConfirmation phase. | UniqueId | Phase | -| POST unconfirmation | Alice can revoke her registration without penalty if the current phase is InputRegistration. | UniqueId | | -| POST outputs | Bob registers his output. | Output, Signature, RoundId | | -| GET coinjoin | Alice asks for the final CoinJoin transaction. | UniqueId | Transaction | -| POST signatures | Alice posts her partial signatures. | UniqueId, Signatures[(Witness, Index)] | | diff --git a/WalletWasabi.Backend/Startup.cs b/WalletWasabi.Backend/Startup.cs index e7a2c654fd..85ba546fd4 100644 --- a/WalletWasabi.Backend/Startup.cs +++ b/WalletWasabi.Backend/Startup.cs @@ -123,6 +123,11 @@ public void ConfigureServices(IServiceCollection services) var coordinator = global.HostedServices.Get(); return coordinator.AffiliationManager; }); + services.AddSingleton(serviceProvider => + { + var global = serviceProvider.GetRequiredService(); + return global.CoinJoinMempoolManager; + }); services.AddStartupTask(); services.AddResponseCompression(); @@ -131,8 +136,6 @@ public void ConfigureServices(IServiceCollection services) [SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "This method gets called by the runtime. Use this method to configure the HTTP request pipeline")] public void Configure(IApplicationBuilder app, IWebHostEnvironment env, Global global) { - app.UseStaticFiles(); - // Enable middleware to serve generated Swagger as a JSON endpoint. app.UseSwagger(); diff --git a/WalletWasabi.Backend/WalletWasabi.Backend.csproj b/WalletWasabi.Backend/WalletWasabi.Backend.csproj index 0eee26778f..e91fbd493a 100644 --- a/WalletWasabi.Backend/WalletWasabi.Backend.csproj +++ b/WalletWasabi.Backend/WalletWasabi.Backend.csproj @@ -8,7 +8,7 @@ 1701;1702;1705;1591;1573;CA1031;CA1822 WalletWasabiApi MIT - walletwasabi, wasabiwallet, wasabi, hiddenwallet, wallet, bitcoin, hbitcoin, nbitcoin, magicalcryptowallet, magicalwallet, tor, chaum, chaumian, zerolink, coinjoin, fungibility, privacy, anonymity + walletwasabi, wasabiwallet, wasabi, wallet, bitcoin, nbitcoin, tor, zerolink, wabisabi, coinjoin, fungibility, privacy, anonymity Git https://github.com/zkSNACKs/WalletWasabi/ enable @@ -16,6 +16,7 @@ win7-x64;linux-x64;linux-arm64;osx-x64;osx-arm64 $(MSBuildProjectDirectory)\=WalletWasabi.Backend Linux + false @@ -43,7 +44,7 @@ - + diff --git a/WalletWasabi.Backend/wwwroot/about.html b/WalletWasabi.Backend/wwwroot/about.html deleted file mode 100644 index cac4d8e918..0000000000 --- a/WalletWasabi.Backend/wwwroot/about.html +++ /dev/null @@ -1,307 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Wasabi Wallet - Bitcoin privacy wallet with built-in coinjoin - - - - - - -
- Wasabi Wallet - About -
- Better money -
Better world -
-
- -
-
-
-
-
-

Motivation

-

- We believe that the most urgent problem for mankind is fixing the money, that Bitcoin is the best option for doing this and that privacy is its biggest problem. -

-

- Wasabi Wallet was created to help Bitcoin maintain its cypherpunk roots on its path to a global reserve currency and to mitigate the inherent privacy issues of its public ledger. This is why Wasabi Wallet is an open-source, non-custodial, privacy-focused Bitcoin wallet for desktop that implements trustless coinjoin magic. -

-

- After years of doing privacy research and development, we’ve concluded that having good tools is not enough - They need to be easy to use, otherwise they will only be utilized by the minority, making users stand out from the crowd. To increase the adoption of these tools, Wasabi is designed to be user friendly and to provide privacy automatically by default. -

-
-
-
-
-
-
-
-
-

The history of Wasabi Wallet

-
    -
  • -
    2015 - YEARS OF RESEARCH
    -

    Ádám Ficsór, aka nopara73, and Lucas Ontivero conducted extensive research on Joinmarket, Zerolink, Tor and Bitcoin Programming in their quest to create a privacy-oriented bitcoin wallet of their own.

    -
  • -
  • -
    2017 - Hidden Wallet
    -

    Ádám was experimenting with the concept of a chaumian coinjoin implementation in Hidden Wallet. It already had a rudimentary Tor and block filter integration. The UI was simple, yet comprehensive, exposing a lot of coordination details to the user.

    -
  • -
  • -
    2018 - zkSNACKs Ltd. IS FORMED
    -

    Ádám collaborated with Gergely Hajdú and Bálint Harmat to create zkSNACKs, a for-profit organization that sponsors the development of Wasabi Wallet, previously called Hidden Wallet.

    - -

    The name derives from a wordplay that originated in the Block Digest podcast, on the zkSNARKs (Zero-Knowledge Succinct Non-Interactive Argument of Knowledge) and zkSTARKs (Zero-Knowledge Succinct Transparent Argument of Knowledge) cryptographic concepts.

    - -

    ZK also stands for zero-knowledge: Wasabi Wallet does not and cannot store any personally identifiable information.

    -
  • -
  • -
    2018 - BUILDING ON BITCOIN CONFERENCE
    -

    Lisbon, Portugal

    - -

    Nopara73 made a presentation titled “Anonymous Bitcoin” and announced that Wasabi Wallet, a solution for using bitcoin in a fully anonymous way, would be launched on the 10 year anniversary of the Satoshi Whitepaper.

    -
  • -
  • -
    2018 - Wasabi Wallet WASABI WALLET IS RELEASED
    -

    Wasabi v1.0: Stable Release was launched on the 31st of October, 2018. The software was described as an “open-source, non-custodial, privacy-focused Bitcoin wallet for desktop that implements trustless coinjoin.

    -
  • -
  • -
    2019 - MIT BITCOIN EXPO
    -

    David Molnár, CTO and Developer, introduces Wasabi Wallet in a presentation titled “Wasabi Wallet: Unfairly Private” where he highlighted Wasabi Wallet’s innovative coinjoin feature.

    -
  • -
  • -
    - 2019 - RECOGNITION FROM BITCOIN.ORG -
    -

    Wasabi Wallet is listed on https://bitcoin.org as a wallet available for Linux, Apple and Windows desktop operating systems.

    -
  • -
  • -
    2019 - 10 BTC BOUNTY
    -

    Wasabi awarded a 10 BTC bounty for the coinjoin privacy implementation by building a more end-user accessible solution and larger adoption. The bounty was distributed by Bitcoin core developers Gregory Maxwell, Pieter Wuille and bitcointalk.org owner, Theymos.

    -
  • -
  • -
    2020 - HRF DONATION
    -

    1 BTC IS DONATED to the HRF's Bitcoin Development Fund to protect Bitcoin users' right to privacy.

    -
  • -
  • -
    2020 - Wasabi Wallet WASABI WALLET 2.0 ANNOUNCED
    -

    On November 5th, 2020, nopara73 announced the construction of Wasabi Wallet 2.0, a "next-generation Bitcoin privacy Wallet," has begun.

    -
  • -
  • -
    2021 - JOIN THE WASABIKAS PODCAST
    -

    Longstanding Wasabi Contributor and Bitcoin maximalist, Max Hillebrand, launched “Join the Wasabikas.” A podcast exploring bitcoin privacy with many famous guests from the Bitcoin ecosystem.

    -
  • -
  • -
    2021 - BITCOIN KNOTS DONATION
    -

    Wasabi Wallet and Bull Bitcoin donate 0.86 BTC towards the development of Bitcoin Knots.

    -
  • -
  • -
    2021 - 1.11 BTC GRANT
    -

    Wasabi Wallet Launches the 1.11 BTC Lightning Network Privacy Research Grant in cooperation with MAGIC Grants.

    -
  • -
  • -
    2022 - Wasabi Wallet NEW LOGO
    -

    The Wasabi Wallet Logo was transformed from the original green and orange shield to a sleek, modern design.

    -
  • -
  • -
    2022 - Wasabi Wallet WASABI WALLET 2.0
    -

    After two years of development, 2.0 is launched on June 15th initiating a new era for Bitcoin privacy.

    -
  • -
-
-
-
-
-
-
-
-
-
-

Reclaim your privacy now

-

Get started in 3 simple steps

-
-
-
-
- -
-

- 1. Download Wasabi Wallet -

-
-
-
- -
-

- 2. Create a new wallet -

-
-
-
- -
-

- 3. Let auto-coinjoin do its magic -

-
-
-
- -
-
-
-
-
-
- - - - diff --git a/WalletWasabi.Backend/wwwroot/bitcoin-whitepaper.pdf b/WalletWasabi.Backend/wwwroot/bitcoin-whitepaper.pdf deleted file mode 100644 index 1e19b739f6..0000000000 Binary files a/WalletWasabi.Backend/wwwroot/bitcoin-whitepaper.pdf and /dev/null differ diff --git a/WalletWasabi.Backend/wwwroot/cloudflare.html b/WalletWasabi.Backend/wwwroot/cloudflare.html deleted file mode 100644 index a4d8300c3a..0000000000 --- a/WalletWasabi.Backend/wwwroot/cloudflare.html +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - - Wasabi Wallet - Bitcoin privacy wallet with built-in coinjoin - - - - -
-
- - Wasabi Wallet - video - -
-
- Wasabi Wallet -

Privacy by default

-

- Open-source, non-custodial -
Bitcoin Wallet for desktop -


- - - - - - Website is loading... - -
::CAPTCHA_BOX::
-
-
-
-
- - - diff --git a/WalletWasabi.Backend/wwwroot/contribution.html b/WalletWasabi.Backend/wwwroot/contribution.html deleted file mode 100644 index aa11588e3d..0000000000 --- a/WalletWasabi.Backend/wwwroot/contribution.html +++ /dev/null @@ -1,339 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Wasabi Wallet - Bitcoin privacy wallet with built-in coinjoin - - - - - - -
- Wasabi Wallet - Contribution -
You can help make Wasabi better
-
- -
-
-
-
-
-
-

Contribution

-

A contributor is any individual who works to add value to Wasabi and its users. Staying true to its open source nature, Wasabi Wallet has hosted multiple contribution games since their inception in July, 2019.

- -

- We now have permanent contributors who came onboard from the contribution games. But don't misinterpret our intentions. Though the word ‘game’ is in the title, this is very much meaningful work. As stated at the top of this page, we believe in the power of collaboration and trust the community to handle some of the more meaningful tasks. -

-
-
-
-
-
-
-
-
-

Testing

-
-
-

Software testing is one of the most important, yet underappreciated aspects of software development. We recognize that there can never be enough people testing our software and we’re proud that there’s such a large community helping us. Follow the steps to contribute to the software’s development.

- -

Say Hello and Get Started

-
    -
  1. Join our Slack and definitely check out our GitHub repository.
  2. -
  3. Introduce yourself, say a bit about your skills and interests. This will help others point you in the right direction.
  4. -
  5. Explore the communication channels and find out what the peers are tinkering with, learn about the project and who is contributing in what way. This will help you to find interesting challenges for you to work on.
  6. -
  7. Follow @WasabiWallet on Twitter and subscribe to the Wasabi YouTube channel to stay up-to-date.
  8. -
-

Learn How we Work

-
    -
  1. To understand how Wasabi coinjoins work, read our WabiSabi research paper and explore the Documentation.
  2. -
-

Do Valuable Work

-

Ok. You’re all set up and ready to work. Here’s what to do next.

-
    -
  1. Find a problem somewhere in Wasabi-land that (a) needs fixing and (b) is a match for your skills and interests. Browse our open issues and ask around about what other contributors think needs fixing. While you don’t need anyone’s permission to work on whatever you want, it's best to know up front whether the work you do will be valuable to the team.
  2. -
  3. Do work to fix that problem. Submit your fix for review with a pull request (for code and documentation changes) or with a GitHub issue (for everything else).
  4. -
  5. Request that others review your work. The best way to do this is by writing good commit comments and pull request/issue descriptions that clearly explain the problem your work is intended to solve, why it’s important and why you fixed it the way you did. Make it as easy as possible for others to review your work so that it is a pleasure to review your work.
  6. -
  7. Incorporate review feedback you receive until your fix gets merged or is otherwise accepted.
  8. -
  9. Repeat steps 7–10.
  10. -
-
-
-
-
-
-
-
-
-

Support

-
-
-

Wasabikas have always been rewarded for offering technical support on various platforms. This allows for "fresh eyes" to review the changes, updates and features implemented in the software. People wanting to learn about Wasabi Wallet and offer support to its users can now express their interests by joining the support team. First, they must go through an orientation process where they learn about the inner workings of Wasabi Wallet.

-

- Then, candidates will be left on their own to determine how well they are able to individually offer support to Wasabi users with the possibility of officially joining the Wasabi Wallet 2.0 Support Team. -

-
-
-
-
-
-
-
-
-

Blog

-
-
-

- Though we have a spectacular in-house blog writing team (no bias here), we'd love to offer a space for more great authors on our platform. Submit your work on the current prompt and we will review it. If it's something on par with the level of writing we'd like to feature in our blog, then you will be rewarded with a 100USD blog writing bounty paid in Bitcoin. -

- -

- We're excited to see more and more contributing authors being featured on our blog and it's our intention to post even more technical articles for the community. After winning the bounty, we will contact you to discuss the opportunity to consistently write content for us for an even bigger prize. -

-
-
-
-
-
-
-
-
-
-

Contact us

-

If you would like to contribute, reach out to us about one of the topics above at:

-

- contribution@zksnacks.com -

-
-
-
-
-
-
-
-
-
-
-
-

Career opportunities

- -

zkSNACKs is not an ordinary company. It is a collection of like-minded individuals who want to fix the world by fixing the money.

- -

Open-source software, like the internet, is one of the greatest gifts to humankind. Unfortunately, philanthropy is not as motivating as earning money. For this reason, zkSNACKs is the company which sponsors the development of the open-source software which is Wasabi Wallet.

- -

Feel free to send us your resumé, portfolio or CV. Though there may not be any current openings in your field; if you’re that incredible, then perhaps we can be convinced to create a position just for you. Hint: consistent contribution to our contribution games is the best path to getting hired.

-
-
-
-
-

Bitcoin Only

-
    -
  1. We all hate wasting time
  2. -
  3. Cryptocurrency is the future
  4. -
  5. Good money drives out bad money
  6. -
-

As long as Bitcoin is the largest cryptocurrency, dealing with altcoins is a likely way to end up on the wrong side of history. For this reason, altcoins are noise and Bitcoin is the signal.

-
-
-
- - -
-
-
-
-
-
-
-
-
-
-
-

C#/.NET Developer

-
-
- Location: Preferably Budapest, Hungary. Can be remote. -
Job Term: Full-Time -
Pay Rate: Monthly - -

- Summary: Things work best if we all speak the same language and this language is C#. -

- - Required Skills: -
    -
  1. Expert knowledge of C# within .Net framework
  2. -
  3. English communication
  4. -
- APPLY HERE ▸ -
-
-
-
-
-
-
-
-
-

Office life

-
- -
-
-
-
-
-
- - - - - diff --git a/WalletWasabi.Backend/wwwroot/css/bootstrap.css b/WalletWasabi.Backend/wwwroot/css/bootstrap.css deleted file mode 100644 index 2fbee7e36a..0000000000 --- a/WalletWasabi.Backend/wwwroot/css/bootstrap.css +++ /dev/null @@ -1,7 +0,0 @@ -@charset "UTF-8";/*! - * Bootstrap v5.1.3 (https://getbootstrap.com/) - * Copyright 2011-2021 The Bootstrap Authors - * Copyright 2011-2021 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - */:root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-body-color-rgb:33,37,41;--bs-body-bg-rgb:255,255,255;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-bg:#fff}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::-webkit-file-upload-button{font:inherit}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:#6c757d}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{width:100%;padding-right:var(--bs-gutter-x,.75rem);padding-left:var(--bs-gutter-x,.75rem);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(-1 * var(--bs-gutter-y));margin-right:calc(-.5 * var(--bs-gutter-x));margin-left:calc(-.5 * var(--bs-gutter-x))}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.6666666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.6666666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.6666666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.6666666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-bg:transparent;--bs-table-accent-bg:transparent;--bs-table-striped-color:#212529;--bs-table-striped-bg:rgba(0, 0, 0, 0.05);--bs-table-active-color:#212529;--bs-table-active-bg:rgba(0, 0, 0, 0.1);--bs-table-hover-color:#212529;--bs-table-hover-bg:rgba(0, 0, 0, 0.075);width:100%;margin-bottom:1rem;color:#212529;vertical-align:top;border-color:#dee2e6}.table>:not(caption)>*>*{padding:.5rem .5rem;background-color:var(--bs-table-bg);border-bottom-width:1px;box-shadow:inset 0 0 0 9999px var(--bs-table-accent-bg)}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table>:not(:first-child){border-top:2px solid currentColor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:1px 0}.table-bordered>:not(caption)>*>*{border-width:0 1px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-accent-bg:var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-active{--bs-table-accent-bg:var(--bs-table-active-bg);color:var(--bs-table-active-color)}.table-hover>tbody>tr:hover>*{--bs-table-accent-bg:var(--bs-table-hover-bg);color:var(--bs-table-hover-color)}.table-primary{--bs-table-bg:#cfe2ff;--bs-table-striped-bg:#c5d7f2;--bs-table-striped-color:#000;--bs-table-active-bg:#bacbe6;--bs-table-active-color:#000;--bs-table-hover-bg:#bfd1ec;--bs-table-hover-color:#000;color:#000;border-color:#bacbe6}.table-secondary{--bs-table-bg:#e2e3e5;--bs-table-striped-bg:#d7d8da;--bs-table-striped-color:#000;--bs-table-active-bg:#cbccce;--bs-table-active-color:#000;--bs-table-hover-bg:#d1d2d4;--bs-table-hover-color:#000;color:#000;border-color:#cbccce}.table-success{--bs-table-bg:#d1e7dd;--bs-table-striped-bg:#c7dbd2;--bs-table-striped-color:#000;--bs-table-active-bg:#bcd0c7;--bs-table-active-color:#000;--bs-table-hover-bg:#c1d6cc;--bs-table-hover-color:#000;color:#000;border-color:#bcd0c7}.table-info{--bs-table-bg:#cff4fc;--bs-table-striped-bg:#c5e8ef;--bs-table-striped-color:#000;--bs-table-active-bg:#badce3;--bs-table-active-color:#000;--bs-table-hover-bg:#bfe2e9;--bs-table-hover-color:#000;color:#000;border-color:#badce3}.table-warning{--bs-table-bg:#fff3cd;--bs-table-striped-bg:#f2e7c3;--bs-table-striped-color:#000;--bs-table-active-bg:#e6dbb9;--bs-table-active-color:#000;--bs-table-hover-bg:#ece1be;--bs-table-hover-color:#000;color:#000;border-color:#e6dbb9}.table-danger{--bs-table-bg:#f8d7da;--bs-table-striped-bg:#eccccf;--bs-table-striped-color:#000;--bs-table-active-bg:#dfc2c4;--bs-table-active-color:#000;--bs-table-hover-bg:#e5c7ca;--bs-table-hover-color:#000;color:#000;border-color:#dfc2c4}.table-light{--bs-table-bg:#f8f9fa;--bs-table-striped-bg:#ecedee;--bs-table-striped-color:#000;--bs-table-active-bg:#dfe0e1;--bs-table-active-color:#000;--bs-table-hover-bg:#e5e6e7;--bs-table-hover-color:#000;color:#000;border-color:#dfe0e1}.table-dark{--bs-table-bg:#212529;--bs-table-striped-bg:#2c3034;--bs-table-striped-color:#fff;--bs-table-active-bg:#373b3e;--bs-table-active-color:#fff;--bs-table-hover-bg:#323539;--bs-table-hover-color:#fff;color:#fff;border-color:#373b3e}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:#6c757d}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:#212529;background-color:#fff;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-control::-webkit-date-and-time-value{height:1.5em}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#dde0e3}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#dde0e3}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#dde0e3}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + .75rem + 2px)}textarea.form-control-sm{min-height:calc(1.5em + .5rem + 2px)}textarea.form-control-lg{min-height:calc(1.5em + 1rem + 2px)}.form-control-color{width:3rem;height:auto;padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{height:1.5em;border-radius:.25rem}.form-control-color::-webkit-color-swatch{height:1.5em;border-radius:.25rem}.form-select{display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;-moz-padding-start:calc(0.75rem - 3px);font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:#e9ecef}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #212529}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem;border-radius:.2rem}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem;border-radius:.3rem}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-input{width:1em;height:1em;margin-top:.25em;vertical-align:top;background-color:#fff;background-repeat:no-repeat;background-position:center;background-size:contain;border:1px solid rgba(0,0,0,.25);-webkit-appearance:none;-moz-appearance:none;appearance:none;-webkit-print-color-adjust:exact;color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-check-input:checked{background-color:#0d6efd;border-color:#0d6efd}.form-check-input:checked[type=checkbox]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#0d6efd;border-color:#0d6efd;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{width:2em;margin-left:-2.5em;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}.form-range{width:100%;height:1.5rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#0d6efd;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b6d4fe}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#0d6efd;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#b6d4fe}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.form-range:disabled::-moz-range-thumb{background-color:#adb5bd}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-select{height:calc(3.5rem + 2px);line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;height:100%;padding:1rem .75rem;pointer-events:none;border:1px solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control{padding:1rem .75rem}.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:-webkit-autofill~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-select:focus{z-index:3}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:3}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#198754}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(25,135,84,.9);border-radius:.25rem}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#198754;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:#198754}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:#198754}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:#198754}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#198754}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group .form-control.is-valid,.input-group .form-select.is-valid,.was-validated .input-group .form-control:valid,.was-validated .input-group .form-select:valid{z-index:1}.input-group .form-control.is-valid:focus,.input-group .form-select.is-valid:focus,.was-validated .input-group .form-control:valid:focus,.was-validated .input-group .form-select:valid:focus{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:#dc3545}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:#dc3545}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:#dc3545}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group .form-control.is-invalid,.input-group .form-select.is-invalid,.was-validated .input-group .form-control:invalid,.was-validated .input-group .form-select:invalid{z-index:2}.input-group .form-control.is-invalid:focus,.input-group .form-select.is-invalid:focus,.was-validated .input-group .form-control:invalid:focus,.was-validated .input-group .form-select:invalid:focus{z-index:3}.btn{display:inline-block;font-weight:400;line-height:1.5;color:#212529;text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529}.btn-check:focus+.btn,.btn:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{pointer-events:none;opacity:.65}.btn-primary{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-primary:hover{color:#fff;background-color:#0b5ed7;border-color:#0a58ca}.btn-check:focus+.btn-primary,.btn-primary:focus{color:#fff;background-color:#0b5ed7;border-color:#0a58ca;box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-check:active+.btn-primary,.btn-check:checked+.btn-primary,.btn-primary.active,.btn-primary:active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0a58ca;border-color:#0a53be}.btn-check:active+.btn-primary:focus,.btn-check:checked+.btn-primary:focus,.btn-primary.active:focus,.btn-primary:active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5c636a;border-color:#565e64}.btn-check:focus+.btn-secondary,.btn-secondary:focus{color:#fff;background-color:#5c636a;border-color:#565e64;box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-check:active+.btn-secondary,.btn-check:checked+.btn-secondary,.btn-secondary.active,.btn-secondary:active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#565e64;border-color:#51585e}.btn-check:active+.btn-secondary:focus,.btn-check:checked+.btn-secondary:focus,.btn-secondary.active:focus,.btn-secondary:active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-success{color:#fff;background-color:#198754;border-color:#198754}.btn-success:hover{color:#fff;background-color:#157347;border-color:#146c43}.btn-check:focus+.btn-success,.btn-success:focus{color:#fff;background-color:#157347;border-color:#146c43;box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-check:active+.btn-success,.btn-check:checked+.btn-success,.btn-success.active,.btn-success:active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#146c43;border-color:#13653f}.btn-check:active+.btn-success:focus,.btn-check:checked+.btn-success:focus,.btn-success.active:focus,.btn-success:active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#198754;border-color:#198754}.btn-info{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-info:hover{color:#000;background-color:#31d2f2;border-color:#25cff2}.btn-check:focus+.btn-info,.btn-info:focus{color:#000;background-color:#31d2f2;border-color:#25cff2;box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-check:active+.btn-info,.btn-check:checked+.btn-info,.btn-info.active,.btn-info:active,.show>.btn-info.dropdown-toggle{color:#000;background-color:#3dd5f3;border-color:#25cff2}.btn-check:active+.btn-info:focus,.btn-check:checked+.btn-info:focus,.btn-info.active:focus,.btn-info:active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-info.disabled,.btn-info:disabled{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-warning{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#000;background-color:#ffca2c;border-color:#ffc720}.btn-check:focus+.btn-warning,.btn-warning:focus{color:#000;background-color:#ffca2c;border-color:#ffc720;box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-check:active+.btn-warning,.btn-check:checked+.btn-warning,.btn-warning.active,.btn-warning:active,.show>.btn-warning.dropdown-toggle{color:#000;background-color:#ffcd39;border-color:#ffc720}.btn-check:active+.btn-warning:focus,.btn-check:checked+.btn-warning:focus,.btn-warning.active:focus,.btn-warning:active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#bb2d3b;border-color:#b02a37}.btn-check:focus+.btn-danger,.btn-danger:focus{color:#fff;background-color:#bb2d3b;border-color:#b02a37;box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-check:active+.btn-danger,.btn-check:checked+.btn-danger,.btn-danger.active,.btn-danger:active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#b02a37;border-color:#a52834}.btn-check:active+.btn-danger:focus,.btn-check:checked+.btn-danger:focus,.btn-danger.active:focus,.btn-danger:active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-light{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:focus+.btn-light,.btn-light:focus{color:#000;background-color:#f9fafb;border-color:#f9fafb;box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-check:active+.btn-light,.btn-check:checked+.btn-light,.btn-light.active,.btn-light:active,.show>.btn-light.dropdown-toggle{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:active+.btn-light:focus,.btn-check:checked+.btn-light:focus,.btn-light.active:focus,.btn-light:active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-light.disabled,.btn-light:disabled{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-dark{color:#fff;background-color:#212529;border-color:#212529}.btn-dark:hover{color:#fff;background-color:#1c1f23;border-color:#1a1e21}.btn-check:focus+.btn-dark,.btn-dark:focus{color:#fff;background-color:#1c1f23;border-color:#1a1e21;box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-check:active+.btn-dark,.btn-check:checked+.btn-dark,.btn-dark.active,.btn-dark:active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1a1e21;border-color:#191c1f}.btn-check:active+.btn-dark:focus,.btn-check:checked+.btn-dark:focus,.btn-dark.active:focus,.btn-dark:active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#212529;border-color:#212529}.btn-outline-primary{color:#0d6efd;border-color:#0d6efd}.btn-outline-primary:hover{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:focus+.btn-outline-primary,.btn-outline-primary:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-check:active+.btn-outline-primary,.btn-check:checked+.btn-outline-primary,.btn-outline-primary.active,.btn-outline-primary.dropdown-toggle.show,.btn-outline-primary:active{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:active+.btn-outline-primary:focus,.btn-check:checked+.btn-outline-primary:focus,.btn-outline-primary.active:focus,.btn-outline-primary.dropdown-toggle.show:focus,.btn-outline-primary:active:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#0d6efd;background-color:transparent}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:focus+.btn-outline-secondary,.btn-outline-secondary:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-check:active+.btn-outline-secondary,.btn-check:checked+.btn-outline-secondary,.btn-outline-secondary.active,.btn-outline-secondary.dropdown-toggle.show,.btn-outline-secondary:active{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:active+.btn-outline-secondary:focus,.btn-check:checked+.btn-outline-secondary:focus,.btn-outline-secondary.active:focus,.btn-outline-secondary.dropdown-toggle.show:focus,.btn-outline-secondary:active:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-success{color:#198754;border-color:#198754}.btn-outline-success:hover{color:#fff;background-color:#198754;border-color:#198754}.btn-check:focus+.btn-outline-success,.btn-outline-success:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-check:active+.btn-outline-success,.btn-check:checked+.btn-outline-success,.btn-outline-success.active,.btn-outline-success.dropdown-toggle.show,.btn-outline-success:active{color:#fff;background-color:#198754;border-color:#198754}.btn-check:active+.btn-outline-success:focus,.btn-check:checked+.btn-outline-success:focus,.btn-outline-success.active:focus,.btn-outline-success.dropdown-toggle.show:focus,.btn-outline-success:active:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#198754;background-color:transparent}.btn-outline-info{color:#0dcaf0;border-color:#0dcaf0}.btn-outline-info:hover{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:focus+.btn-outline-info,.btn-outline-info:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-check:active+.btn-outline-info,.btn-check:checked+.btn-outline-info,.btn-outline-info.active,.btn-outline-info.dropdown-toggle.show,.btn-outline-info:active{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:active+.btn-outline-info:focus,.btn-check:checked+.btn-outline-info:focus,.btn-outline-info.active:focus,.btn-outline-info.dropdown-toggle.show:focus,.btn-outline-info:active:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#0dcaf0;background-color:transparent}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:focus+.btn-outline-warning,.btn-outline-warning:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-check:active+.btn-outline-warning,.btn-check:checked+.btn-outline-warning,.btn-outline-warning.active,.btn-outline-warning.dropdown-toggle.show,.btn-outline-warning:active{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:active+.btn-outline-warning:focus,.btn-check:checked+.btn-outline-warning:focus,.btn-outline-warning.active:focus,.btn-outline-warning.dropdown-toggle.show:focus,.btn-outline-warning:active:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:focus+.btn-outline-danger,.btn-outline-danger:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-check:active+.btn-outline-danger,.btn-check:checked+.btn-outline-danger,.btn-outline-danger.active,.btn-outline-danger.dropdown-toggle.show,.btn-outline-danger:active{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:active+.btn-outline-danger:focus,.btn-check:checked+.btn-outline-danger:focus,.btn-outline-danger.active:focus,.btn-outline-danger.dropdown-toggle.show:focus,.btn-outline-danger:active:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:focus+.btn-outline-light,.btn-outline-light:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-check:active+.btn-outline-light,.btn-check:checked+.btn-outline-light,.btn-outline-light.active,.btn-outline-light.dropdown-toggle.show,.btn-outline-light:active{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:active+.btn-outline-light:focus,.btn-check:checked+.btn-outline-light:focus,.btn-outline-light.active:focus,.btn-outline-light.dropdown-toggle.show:focus,.btn-outline-light:active:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-dark{color:#212529;border-color:#212529}.btn-outline-dark:hover{color:#fff;background-color:#212529;border-color:#212529}.btn-check:focus+.btn-outline-dark,.btn-outline-dark:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-check:active+.btn-outline-dark,.btn-check:checked+.btn-outline-dark,.btn-outline-dark.active,.btn-outline-dark.dropdown-toggle.show,.btn-outline-dark:active{color:#fff;background-color:#212529;border-color:#212529}.btn-check:active+.btn-outline-dark:focus,.btn-check:checked+.btn-outline-dark:focus,.btn-outline-dark.active:focus,.btn-outline-dark.dropdown-toggle.show:focus,.btn-outline-dark:active:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#212529;background-color:transparent}.btn-link{font-weight:400;color:#0d6efd;text-decoration:underline}.btn-link:hover{color:#0a58ca}.btn-link.disabled,.btn-link:disabled{color:#6c757d}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion:reduce){.collapsing.collapse-horizontal{transition:none}}.dropdown,.dropend,.dropstart,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;z-index:1000;display:none;min-width:10rem;padding:.5rem 0;margin:0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:.125rem}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid rgba(0,0,0,.15)}.dropdown-item{display:block;width:100%;padding:.25rem 1rem;clear:both;font-weight:400;color:#212529;text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#1e2125;background-color:#e9ecef}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#0d6efd}.dropdown-item.disabled,.dropdown-item:disabled{color:#adb5bd;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1rem;color:#212529}.dropdown-menu-dark{color:#dee2e6;background-color:#343a40;border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item{color:#dee2e6}.dropdown-menu-dark .dropdown-item:focus,.dropdown-menu-dark .dropdown-item:hover{color:#fff;background-color:rgba(255,255,255,.15)}.dropdown-menu-dark .dropdown-item.active,.dropdown-menu-dark .dropdown-item:active{color:#fff;background-color:#0d6efd}.dropdown-menu-dark .dropdown-item.disabled,.dropdown-menu-dark .dropdown-item:disabled{color:#adb5bd}.dropdown-menu-dark .dropdown-divider{border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item-text{color:#dee2e6}.dropdown-menu-dark .dropdown-header{color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn~.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem;color:#0d6efd;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:#0a58ca}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-link{margin-bottom:-1px;background:0 0;border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6;isolation:isolate}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{background:0 0;border:0;border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#0d6efd}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-basis:0;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding-top:.5rem;padding-bottom:.5rem}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;text-decoration:none;white-space:nowrap}.navbar-nav{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem;transition:box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 .25rem}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas-header{display:none}.navbar-expand-sm .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-sm .offcanvas-bottom,.navbar-expand-sm .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas-header{display:none}.navbar-expand-md .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-md .offcanvas-bottom,.navbar-expand-md .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas-header{display:none}.navbar-expand-lg .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-lg .offcanvas-bottom,.navbar-expand-lg .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas-header{display:none}.navbar-expand-xl .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-xl .offcanvas-bottom,.navbar-expand-xl .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-xxl .offcanvas-bottom,.navbar-expand-xxl .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-xxl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas-header{display:none}.navbar-expand .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand .offcanvas-bottom,.navbar-expand .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.55)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.55);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.55)}.navbar-light .navbar-text a,.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.55)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.55);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.55)}.navbar-dark .navbar-text a,.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:flex;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:1rem 1rem}.card-title{margin-bottom:.5rem}.card-subtitle{margin-top:-.25rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:1rem}.card-header{padding:.5rem 1rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-footer{padding:.5rem 1rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.5rem;margin-bottom:-.5rem;margin-left:-.5rem;border-bottom:0}.card-header-pills{margin-right:-.5rem;margin-left:-.5rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-group>.card{margin-bottom:.75rem}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:1rem 1.25rem;font-size:1rem;color:#212529;text-align:left;background-color:#fff;border:0;border-radius:0;overflow-anchor:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,border-radius .15s ease}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:#0c63e4;background-color:#e7f1ff;box-shadow:inset 0 -1px 0 rgba(0,0,0,.125)}.accordion-button:not(.collapsed)::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%230c63e4'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");transform:rotate(-180deg)}.accordion-button::after{flex-shrink:0;width:1.25rem;height:1.25rem;margin-left:auto;content:"";background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-size:1.25rem;transition:transform .2s ease-in-out}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.accordion-header{margin-bottom:0}.accordion-item{background-color:#fff;border:1px solid rgba(0,0,0,.125)}.accordion-item:first-of-type{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.accordion-item:first-of-type .accordion-button{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-item:last-of-type .accordion-button.collapsed{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.accordion-item:last-of-type .accordion-collapse{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-body{padding:1rem 1.25rem}.accordion-flush .accordion-collapse{border-width:0}.accordion-flush .accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush .accordion-item:first-child{border-top:0}.accordion-flush .accordion-item:last-child{border-bottom:0}.accordion-flush .accordion-item .accordion-button{border-radius:0}.breadcrumb{display:flex;flex-wrap:wrap;padding:0 0;margin-bottom:1rem;list-style:none}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:.5rem;color:#6c757d;content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:#6c757d}.pagination{display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;color:#0d6efd;text-decoration:none;background-color:#fff;border:1px solid #dee2e6;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:#0a58ca;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;color:#0a58ca;background-color:#e9ecef;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.page-item:not(:first-child) .page-link{margin-left:-1px}.page-item.active .page-link{z-index:3;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;background-color:#fff;border-color:#dee2e6}.page-link{padding:.375rem .75rem}.page-item:first-child .page-link{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{position:relative;padding:1rem 1rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{color:#084298;background-color:#cfe2ff;border-color:#b6d4fe}.alert-primary .alert-link{color:#06357a}.alert-secondary{color:#41464b;background-color:#e2e3e5;border-color:#d3d6d8}.alert-secondary .alert-link{color:#34383c}.alert-success{color:#0f5132;background-color:#d1e7dd;border-color:#badbcc}.alert-success .alert-link{color:#0c4128}.alert-info{color:#055160;background-color:#cff4fc;border-color:#b6effb}.alert-info .alert-link{color:#04414d}.alert-warning{color:#664d03;background-color:#fff3cd;border-color:#ffecb5}.alert-warning .alert-link{color:#523e02}.alert-danger{color:#842029;background-color:#f8d7da;border-color:#f5c2c7}.alert-danger .alert-link{color:#6a1a21}.alert-light{color:#636464;background-color:#fefefe;border-color:#fdfdfe}.alert-light .alert-link{color:#4f5050}.alert-dark{color:#141619;background-color:#d3d3d4;border-color:#bcbebf}.alert-dark .alert-link{color:#101214}@-webkit-keyframes progress-bar-stripes{0%{background-position-x:1rem}}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress{display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#0d6efd;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:1s linear infinite progress-bar-stripes;animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.list-group{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.25rem}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>li::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.5rem 1rem;color:#212529;text-decoration:none;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#084298;background-color:#cfe2ff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#084298;background-color:#bacbe6}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#084298;border-color:#084298}.list-group-item-secondary{color:#41464b;background-color:#e2e3e5}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#41464b;background-color:#cbccce}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#41464b;border-color:#41464b}.list-group-item-success{color:#0f5132;background-color:#d1e7dd}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#0f5132;background-color:#bcd0c7}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#0f5132;border-color:#0f5132}.list-group-item-info{color:#055160;background-color:#cff4fc}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#055160;background-color:#badce3}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#055160;border-color:#055160}.list-group-item-warning{color:#664d03;background-color:#fff3cd}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#664d03;background-color:#e6dbb9}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#664d03;border-color:#664d03}.list-group-item-danger{color:#842029;background-color:#f8d7da}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#842029;background-color:#dfc2c4}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#842029;border-color:#842029}.list-group-item-light{color:#636464;background-color:#fefefe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#636464;background-color:#e5e5e5}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#636464;border-color:#636464}.list-group-item-dark{color:#141619;background-color:#d3d3d4}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#141619;background-color:#bebebf}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#141619;border-color:#141619}.btn-close{box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:#000;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;border:0;border-radius:.25rem;opacity:.5}.btn-close:hover{color:#000;text-decoration:none;opacity:.75}.btn-close:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25);opacity:1}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:.25}.btn-close-white{filter:invert(1) grayscale(100%) brightness(200%)}.toast{width:350px;max-width:100%;font-size:.875rem;pointer-events:auto;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .5rem 1rem rgba(0,0,0,.15);border-radius:.25rem}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:.75rem}.toast-header{display:flex;align-items:center;padding:.5rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05);border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.toast-header .btn-close{margin-right:-.375rem;margin-left:.75rem}.toast-body{padding:.75rem;word-wrap:break-word}.modal{position:fixed;top:0;left:0;z-index:1055;display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - 1rem)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1050;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:flex;flex-shrink:0;align-items:center;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .btn-close{padding:.5rem .5rem;margin:-.5rem -.5rem -.5rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;flex:1 1 auto;padding:1rem}.modal-footer{display:flex;flex-wrap:wrap;flex-shrink:0;align-items:center;justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{height:calc(100% - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}.modal-fullscreen .modal-footer{border-radius:0}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}.modal-fullscreen-sm-down .modal-footer{border-radius:0}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}.modal-fullscreen-md-down .modal-footer{border-radius:0}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}.modal-fullscreen-lg-down .modal-footer{border-radius:0}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}.modal-fullscreen-xl-down .modal-footer{border-radius:0}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}.modal-fullscreen-xxl-down .modal-footer{border-radius:0}}.tooltip{position:absolute;z-index:1080;display:block;margin:0;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .tooltip-arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[data-popper-placement^=right],.bs-tooltip-end{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[data-popper-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[data-popper-placement^=left],.bs-tooltip-start{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1070;display:block;max-width:276px;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .popover-arrow{position:absolute;display:block;width:1rem;height:.5rem}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f0f0f0}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem 1rem;margin-bottom:0;font-size:1rem;background-color:#f0f0f0;border-bottom:1px solid rgba(0,0,0,.2);border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:1rem 1rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%;list-style:none}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}@-webkit-keyframes spinner-border{to{transform:rotate(360deg)}}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:.75s linear infinite spinner-border;animation:.75s linear infinite spinner-border}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:.75s linear infinite spinner-grow;animation:.75s linear infinite spinner-grow}.spinner-grow-sm{width:1rem;height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{-webkit-animation-duration:1.5s;animation-duration:1.5s}}.offcanvas{position:fixed;bottom:0;z-index:1045;display:flex;flex-direction:column;max-width:100%;visibility:hidden;background-color:#fff;background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;justify-content:space-between;padding:1rem 1rem}.offcanvas-header .btn-close{padding:.5rem .5rem;margin-top:-.5rem;margin-right:-.5rem;margin-bottom:-.5rem}.offcanvas-title{margin-bottom:0;line-height:1.5}.offcanvas-body{flex-grow:1;padding:1rem 1rem;overflow-y:auto}.offcanvas-start{top:0;left:0;width:400px;border-right:1px solid rgba(0,0,0,.2);transform:translateX(-100%)}.offcanvas-end{top:0;right:0;width:400px;border-left:1px solid rgba(0,0,0,.2);transform:translateX(100%)}.offcanvas-top{top:0;right:0;left:0;height:30vh;max-height:100%;border-bottom:1px solid rgba(0,0,0,.2);transform:translateY(-100%)}.offcanvas-bottom{right:0;left:0;height:30vh;max-height:100%;border-top:1px solid rgba(0,0,0,.2);transform:translateY(100%)}.offcanvas.show{transform:none}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentColor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{-webkit-animation:placeholder-glow 2s ease-in-out infinite;animation:placeholder-glow 2s ease-in-out infinite}@-webkit-keyframes placeholder-glow{50%{opacity:.2}}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;-webkit-animation:placeholder-wave 2s linear infinite;animation:placeholder-wave 2s linear infinite}@-webkit-keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.link-primary{color:#0d6efd}.link-primary:focus,.link-primary:hover{color:#0a58ca}.link-secondary{color:#6c757d}.link-secondary:focus,.link-secondary:hover{color:#565e64}.link-success{color:#198754}.link-success:focus,.link-success:hover{color:#146c43}.link-info{color:#0dcaf0}.link-info:focus,.link-info:hover{color:#3dd5f3}.link-warning{color:#ffc107}.link-warning:focus,.link-warning:hover{color:#ffcd39}.link-danger{color:#dc3545}.link-danger:focus,.link-danger:hover{color:#b02a37}.link-light{color:#f8f9fa}.link-light:focus,.link-light:hover{color:#f9fafb}.link-dark{color:#212529}.link-dark:focus,.link-dark:hover{color:#1a1e21}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:75%}.ratio-16x9{--bs-aspect-ratio:56.25%}.ratio-21x9{--bs-aspect-ratio:42.8571428571%}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.hstack{display:flex;flex-direction:row;align-items:center;align-self:stretch}.vstack{display:flex;flex:1 1 auto;flex-direction:column;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;width:1px;min-height:1em;background-color:currentColor;opacity:.25}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.opacity-0{opacity:0!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:1px solid #dee2e6!important}.border-0{border:0!important}.border-top{border-top:1px solid #dee2e6!important}.border-top-0{border-top:0!important}.border-end{border-right:1px solid #dee2e6!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:1px solid #dee2e6!important}.border-start-0{border-left:0!important}.border-primary{border-color:#0d6efd!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#198754!important}.border-info{border-color:#0dcaf0!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#212529!important}.border-white{border-color:#fff!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-light{font-weight:300!important}.fw-lighter{font-weight:lighter!important}.fw-normal{font-weight:400!important}.fw-bold{font-weight:700!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{--bs-text-opacity:1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-secondary{--bs-text-opacity:1;color:rgba(var(--bs-secondary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity:1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-info{--bs-text-opacity:1;color:rgba(var(--bs-info-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity:1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity:1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-light{--bs-text-opacity:1;color:rgba(var(--bs-light-rgb),var(--bs-text-opacity))!important}.text-dark{--bs-text-opacity:1;color:rgba(var(--bs-dark-rgb),var(--bs-text-opacity))!important}.text-black{--bs-text-opacity:1;color:rgba(var(--bs-black-rgb),var(--bs-text-opacity))!important}.text-white{--bs-text-opacity:1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-body{--bs-text-opacity:1;color:rgba(var(--bs-body-color-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity:1;color:#6c757d!important}.text-black-50{--bs-text-opacity:1;color:rgba(0,0,0,.5)!important}.text-white-50{--bs-text-opacity:1;color:rgba(255,255,255,.5)!important}.text-reset{--bs-text-opacity:1;color:inherit!important}.text-opacity-25{--bs-text-opacity:0.25}.text-opacity-50{--bs-text-opacity:0.5}.text-opacity-75{--bs-text-opacity:0.75}.text-opacity-100{--bs-text-opacity:1}.bg-primary{--bs-bg-opacity:1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important}.bg-success{--bs-bg-opacity:1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-info{--bs-bg-opacity:1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important}.bg-warning{--bs-bg-opacity:1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-danger{--bs-bg-opacity:1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-light{--bs-bg-opacity:1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-dark{--bs-bg-opacity:1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.bg-black{--bs-bg-opacity:1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important}.bg-white{--bs-bg-opacity:1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important}.bg-body{--bs-bg-opacity:1;background-color:rgba(var(--bs-body-bg-rgb),var(--bs-bg-opacity))!important}.bg-transparent{--bs-bg-opacity:1;background-color:transparent!important}.bg-opacity-10{--bs-bg-opacity:0.1}.bg-opacity-25{--bs-bg-opacity:0.25}.bg-opacity-50{--bs-bg-opacity:0.5}.bg-opacity-75{--bs-bg-opacity:0.75}.bg-opacity-100{--bs-bg-opacity:1}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:.25rem!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:.2rem!important}.rounded-2{border-radius:.25rem!important}.rounded-3{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-end{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-start{border-bottom-left-radius:.25rem!important;border-top-left-radius:.25rem!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}} -/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/WalletWasabi.Backend/wwwroot/css/style.css b/WalletWasabi.Backend/wwwroot/css/style.css deleted file mode 100644 index c863f8438d..0000000000 --- a/WalletWasabi.Backend/wwwroot/css/style.css +++ /dev/null @@ -1,1182 +0,0 @@ -@font-face { - font-family: Objectivity; - src: url('../fonts/objectivity.thin.otf'); - font-weight: 100; -} -@font-face { - font-family: Objectivity; - src: url('../fonts/objectivity.regular.otf'); - font-weight: normal; -} -@font-face { - font-family: Objectivity; - src: url('../fonts/objectivity.medium.otf'); - font-weight: medium; -} -@font-face { - font-family: Objectivity; - src: url('../fonts/objectivity.bold.otf'); - font-weight: bold; -} -:root { - --black: #00101F; - --green: #77C600; - --gray: #9DA8AB; - --blue: #002647; - --light-blue: #00BBC4; - --border-radius: 15px; - --light-gray: rgb(73, 88, 107); -} -body { - color: #fff; - font-family: Objectivity; - line-height: 1em; - background: var(--black); - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} -a { - text-decoration: none; - color: #fff; -} -a:hover { - color: var(--green); -} -p { - font-weight: medium; - line-height: 1.7em; -} -p a, .list a, li a { - color: var(--green); -} -p a:hover, li a:hover { - color: var(--light-blue); -} -h1 { - font-size: 40px; - font-weight: bold; - text-transform: uppercase; -} -h2 { - font-size: 28px; - font-weight: bold; - text-transform: uppercase; -} -section { - width: 100%; - overflow: hidden; -} -nav li.active > a { - color: var(--green) !important; -} -.logo img { - height: 30px; -} -.squared { - position: relative; - padding-left: 40px; -} -.squared:before { - content: ''; - width: 18px; - height: 18px; - background: var(--green); - position: absolute; - top: 4px; - left: 0; -} -.social-icons { - display: flex; - padding: 0; - margin: 0; - position: relative; - z-index: 900; -} -.social-icons li { - list-style: none; - margin: 0 5px; - width: 20px; - height: 20px; -} -.social-icons svg { - width: 100%; - height: 100%; - fill: #9da8ab; -} -.social-icons a:hover > svg { - fill: var(--green); -} -.video-homepage { - position: fixed; - height: 100vh; - width: 100%; - min-height: 600px; - z-index: -1; - top: 0; -} -.video-homepage video, .video-homepage .mobile_video_bg { - width: 100%; - height: 100%; - object-fit: cover; - opacity: 0.5; -} -video::-webkit-media-controls-panel { - display: none; - -webkit-appearance: none; -} -video::-webkit-media-controls-play-button { - display: none; - -webkit-appearance: none; -} -video::-webkit-media-controls-start-playback-button { - display: none; - -webkit-appearance: none -} -video::-webkit-media-controls { - display: none; - -webkit-appearance: none; -} -.pt-100vh .content { - padding-top: 100vh; -} -.download-btn-area { - position: absolute; - left: calc((100% - var(--breakpoint)) / 2); - top: calc(50% + 40px); - transform: translateY(-50%); -} -.home { - background: var(--light-gray); - background: linear-gradient(143deg, rgba(73, 88, 107, 1) 0%, rgba(0, 16, 31, 1) 55%); -} -.title { - font-size: 56px; - font-weight: bold; - line-height: 1.2em; -} -.title-small { - font-size: 40px; -} -.title-lined:after { - content: ''; - width: 50px; - height: 3px; - background-color: var(--green); - display: block; - margin: 20px auto; -} -.title-lined.left:after { - margin: 20px 0; -} -.green-text { - color: var(--green); -} -.floating-title { - font-size: 60px; - font-weight: bold; - line-height: 1.2em; - position: absolute; - /*top: calc(35% - 50px);*/ - - top: 50%; - left: calc((100% - var(--breakpoint)) / 2); - transform: translateY(-50%); - text-transform: uppercase; - width: var(--breakpoint); - text-shadow: 0 0 20px rgba(255, 255, 255, 0.3); -} -.service-line { - display: flex; - font-weight: bold; - font-size: 36px; - text-align: center; - border-bottom: 1px solid var(--light-gray); - text-transform: uppercase; - background: var(--black); -} -.service-line:first-child { - border-top: 1px solid var(--light-gray); -} -.service-line:nth-child(odd) { - direction: rtl; -} -.service { - position: relative; - background-size: cover; - background-position: center center; - -webkit-transition: all 0.5s ease-in-out; - -moz-transition: all 0.5s ease-in-out; - -ms-transition: all 0.5s ease-in-out; - -o-transition: all 0.5s ease-in-out; - transition: all 0.5s ease-in-out; - min-height: 340px; -} -.service:before { - content: ''; - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; - background: rgba(0, 16, 31, 0.8); - z-index: 1; -} -.service-link-holder { - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; - z-index: 2; -} -.service a { - color: var(--green); - display: block; - padding: 160px 0; -} -.service-line .service:nth-child(2) a { - color: #fff; -} -.service:hover a { - color: #fff; -} -.service:hover + .service a { - color: var(--green); -} -.service-about { - background-image: url('../img/01_About_us.png'); - width: 75%; - color: #fff; -} -.service-wallet { - width: 25%; -} -.service-blog { - width: 25%; -} -.service-contribution { - background-image: url('../img/06_contribution.png'); - width: 75%; -} -.service-support { - background-image: url('../img/04_support.png'); - width: 75%; -} -.service-presskit { - width: 25%; -} -.service-wallet:hover { - background-image: url('../img/05_wallet.png'); - width: 75%; -} -.service-wallet:hover + .service-about { - width: 25%; - background: transparent; -} -.service-blog:hover { - width: 75%; - background-image: url('../img/02_Blog.png'); -} -.service-blog:hover + .service-contribution { - width: 25%; - background: transparent; -} -.service-presskit:hover { - background-image: url('../img/03_press_kit.png'); - width: 75%; -} -.service-presskit:hover + .service-support { - width: 25%; - background: transparent; -} -footer { - padding: 20px 0; - line-height: 1.8em; - position: relative; - z-index: 2; - background: var(--black); -} -footer, footer a { - color: var(--gray); - font-size: 14px; -} -footer .white-text { - font-size: 16px; -} -.white-text { - color: #fff; -} -.fixed-heading { - /*padding-top: 50vh;*/ - - padding-top: 100vh; -} -.fixed-img { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100vh; - z-index: 1; -} -.fixed-img img { - width: 100%; - height: 100vh; - object-fit: cover; - opacity: 0.6; -} -.content { - position: relative; - z-index: 2; -} -.p-100 { - padding: 80px 0; -} -.p-100:first-child { - padding-top: 100px; -} -.p-200 { - padding: 200px 0; -} -.subpage { - background: linear-gradient(160deg, rgba(73, 88, 107, 1) 0%, rgba(0, 16, 31, 1) 55%); -} -.light-area { - background: linear-gradient(180deg, rgba(255, 255, 255, 0.85) 0%, rgba(225, 225, 225, 1) 100%); - color: var(--black); - position: relative; -} -.white-fill svg { - fill: #fff; -} -.feature-box { - background: rgba(0, 0, 0, 0.2); - padding: 20px; - color: #fff; - position: relative; - border: 2px solid rgba(73, 88, 107, 0.3); - border-radius: 10px; - height: 100%; - margin-bottom: 20px; -} -.feature-box .title { - font-size: 20px; -} -.scale-svg { - width: 100%; - max-width: 150px; - height: 100px; - margin: 0 auto; -} -.scale-svg img { - object-fit: contain; - width: 100%; - height: 100%; -} -.wallet-cubes .cube { - position: absolute; - background: url('../img/05_wallet.png') fixed no-repeat #fff; - background-size: cover; - background-position: center center; -} -.wallet-cubes .cube:first-child { - width: 33px; - height: 33px; - top: 20%; - left: 94%; -} -.wallet-cubes .cube:nth-child(2) { - width: 78px; - height: 78px; - top: 59%; - left: 43%; -} -.wallet-cubes .cube:nth-child(3) { - width: 161px; - height: 161px; - top: 33%; - left: 62%; -} -.wallet-cubes .cube:nth-child(4) { - width: 32px; - height: 32px; - top: 108%; - left: 40%; -} -.wallet-cubes .cube:nth-child(5) { - width: 34px; - height: 34px; - top: 95%; - left: 51%; -} -.wallet-cubes .cube:nth-child(6) { - width: 78px; - height: 78px; - top: 101%; - left: 77%; -} -.wallet-cubes .cube:nth-child(7) { - width: 18px; - height: 18px; - top: 143%; - left: 88%; -} -.wallet-cubes .cube:nth-child(8) { - width: 77px; - height: 77px; - top: 145%; - left: 54%; -} -.wallet-cubes .cube:nth-child(9) { - width: 17px; - height: 17px; - top: 176%; - left: 54%; -} -.wallet-cubes .cube:nth-child(10) { - width: 17px; - height: 17px; - top: 183%; - left: 47%; -} -.home-text-area { - width: 100%; - max-width: 600px; - margin-bottom: 40px; -} -.home-text-area h1 { - font-size: 60px; -} -.home-text-area p { - font-size: 24px; -} -.lh { - line-height: 1.5em; -} -.btn { - padding-top: 12px; - padding-bottom: 7px; - font-weight: bold; - font-size: 14px; - border-radius: 0; -} -.btn-primary { - background-color: var(--green); - border-color: var(--green); - color: #000; - border-width: 2px; -} -.btn-secondary { - background-color: transparent; - border: 2px solid var(--green); - color: var(--green); - border-radius: 0; - text-transform: uppercase; -} -.btn-secondary:hover { - background-color: var(--light-blue); - color: #000; - border-color: var(--light-blue); -} -.btn-download { - font-size: 20px; - padding: 20px 35px 14px; -} -.btn-download img { - fill: #FFF; - height: 14px; - margin-bottom: 6px; -} -.btn-primary:hover { - background-color: var(--light-blue); - border-color: var(--light-blue); - color: #000; -} -.navbar { - background: rgba(0, 0, 0, 0.3); - -webkit-backdrop-filter: saturate(180%) blur(20px); - backdrop-filter: saturate(180%) blur(20px); - position: fixed; - width: 100%; - z-index: 9999; - top: 0; - left: 0; - padding: 0; - font-size: 15px; - border-bottom: 1px solid var(--green); -} -/* -section { - border-bottom: 1px solid var(--green); -} -*/ - -.navbar .btn { - padding-top: 10px; - padding-bottom: 5px; -} -.navbar-expand-lg .navbar-nav .nav-item { - padding: 0.4em 1em; - text-transform: uppercase; -} -.navbar-expand-lg .navbar-nav .nav-item:last-child { - padding-right: 0; -} -.navbar-dark .navbar-nav .nav-link { - color: #fff; -} -.navbar-dark .navbar-nav .nav-link:focus, .navbar-dark .navbar-nav .nav-link:hover { - color: var(--green); -} -.nav-link { - padding: 0.9em 0 0.4em; -} -.dropdown:hover > .dropdown-menu { - display: block; -} -.dropdown-toggle::after { - color: var(--green); -} -.download-box { - -webkit-transition: all 0.5s ease-in-out; - -moz-transition: all 0.5s ease-in-out; - -ms-transition: all 0.5s ease-in-out; - -o-transition: all 0.5s ease-in-out; - transition: all 0.5s ease-in-out; - border: 2px solid rgba(73, 88, 107, 0.3); - border-radius: 10px; - margin-bottom: 20px; - padding: 25px; -} -.download-box svg { - height: 40px; - width: 40px; -} -.download-box svg { - fill: #fff; -} -.download-box:hover { - background-color: rgb(73, 88, 107, 0.2); -} -ol { - line-height: 1.7em; - margin: 20px 0; - padding: 0; - padding-left: 20px; -} -ol li { - margin: 15px 0; -} -ol li::marker { - color: var(--green); -} -.small-text { - font-size: 12px; -} -.muted-text { - opacity: 0.5; -} -.muted-text:hover { - opacity: 1; -} -.cursor-select { - cursor: text; -} -.timeline { - margin: 80px 0; - padding: 0; -} -.timeline:before { - content: ''; - display: block; - width: 20px; - height: 20px; - background: var(--green); - margin-left: calc(50% - 11px); -} -.timeline:after { - content: ''; - display: block; - width: 20px; - height: 20px; - background: var(--green); - margin-left: calc(50% - 11px); -} -.timeline li { - width: 50%; - padding: 60px 0 5px; - padding-right: 15%; - position: relative; - display: block; -} -.timeline li:before { - content: '□'; - display: block; - color: var(--green); - position: absolute; - font-size: 70px; - top: 72px; -} -.timeline li:nth-child(even):before { - left: calc(15% - 8px); -} -.timeline li:nth-child(odd):before { - right: calc(15% - 8px); -} -.timeline li:nth-child(even) { - margin-left: calc(50% - 1px); - padding-left: 15%; - padding-right: 0; -} -.timeline-title { - font-weight: bold; - font-size: 30px; - color: var(--green); - margin-bottom: 10px; - line-height: 1.2em; -} -.timeline li:nth-child(odd) { - border-right: 1px solid var(--green); -} -.timeline li:nth-child(even) { - border-left: 1px solid var(--green); -} -.timeline li .timeline-title:after { - content: ''; - font-size: 80px; - position: absolute; - display: block; - top: 75px; - width: 15%; - height: 1px; - background-color: var(--green); -} -.timeline li:nth-child(odd) .timeline-title:after { - right: 0; -} -.timeline li:nth-child(even) .timeline-title:after { - left: 0; -} -.timeline-small-img { - height: 30px; - vertical-align: top; -} -.partner-line { - background: rgba(157, 168, 171, 0.2); -} -.help-logo { - height: 120px; -} -.card { - background: rgba(0, 0, 0, 0.2); - color: #fff; - border: 2px solid rgba(73, 88, 107, 0.3); - margin: 20px 0; -} -.card-content, .card input { - display: none; -} -.card input:checked + label + .card-content { - display: block; -} -.card label { - padding: 20px 20px 16px 20px; - font-size: 18px; - font-weight: bold; - cursor: pointer; - line-height: 1.2em; -} -.card label:after { - content: '▼'; - color: var(--green); - position: absolute; - right: 0; - padding: 0 20px; - top: calc(50% - 7px); -} -.card input:checked + label:after { - content: '▲'; - top: 20px; -} -.card-content { - padding: 18px; -} -.card table { - color: var(--green); - margin: 40px; -} -.navbar-toggler { - cursor: pointer; -} -#navswitch { - display: none; -} -#navswitch:checked + label + .navbar-collapse { - display: block; -} -.dropdown-menu { - background-color: transparent; - background: rgba(0, 0, 0, 0.3); - -webkit-backdrop-filter: saturate(180%) blur(20px); - backdrop-filter: saturate(180%) blur(20px); - border: 0; - border-radius: 0; - padding: 10px 20px; - margin-top: 0.6em; -} -.dropdown-menu a { - color: #fff; - margin: 10px 0; - padding: 10px 15px 5px; -} -.dropdown-item:focus, .dropdown-item:hover { - color: var(--green); - background-color: transparent; -} -.pt-header { - padding-top: 80px; -} -.feature-list { - padding: 0; - margin: 0; -} -.feature-list li { - display: block; - margin-bottom: 40px; - position: relative; - padding-left: 40px; -} -.feature-list li:before { - content: ''; - width: 18px; - height: 18px; - background: var(--green); - position: absolute; - top: 2px; - left: 0; - display: block; -} -.feature-list .title { - font-size: 24px; -} -.feature-list svg { - fill: #fff; - margin-right: 20px; - background: var(--green); - padding: 8px; - border-radius: 50%; -} -.feature-list li p, .op-text, .card p { - color: #bbb; -} -.modal-window { - position: fixed; - width: 55vw; - top: 70px; - left: 50%; - transform: translateX(-50%); - background: #fff; - z-index: 9999; - display: none; - color: #000; - padding: 40px; - box-shadow: 0 0 30px rgba(0, 0, 0, 0.5); -} -.modal-window .ratio { - max-height: 100%; -} -.close { - font-size: 24px; - font-weight: lighter; - position: absolute; - right: 15px; - top: 15px; - cursor: pointer; -} -.close:hover { - color: var(--green); -} -.video-show:checked + label + .modal-window { - display: block; -} -.icon { - width: 60px; - height: 60px; - fill: var(--green); -} -.icon img { - width: 100%; - height: 100%; - object-fit: contain; -} -.pull-img { - position: absolute; - width: 1000px; - max-width: 1000px; - top: 50%; - transform: translateY(-50%); -} -.pull-img-to-left { - right: 0; -} -.pull-img-to-right { - left: 0; -} -.video-icon-on { - cursor: pointer; -} -.video-icon-on:after { - content: '▸'; - position: absolute; - display: block; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - font-size: 80px; - color: #fff; - width: 100px; - height: 100px; - line-height: 110px; - z-index: 3; - border-radius: 50%; - background-color: var(--green); - text-align: center; -} -.video-icon-on:hover:after { - background-color: var(--light-blue); -} -.icon-big { - fill: var(--green); - height: 80px; -} -.icon-big img { - object-fit: contain; - height: 100%; - width: 100%; -} -blockquote { - font-size: 36px; - font-weight: bold; - text-align: center; - line-height: 1.3em; -} -blockquote:before { - content: '"'; -} -blockquote:after { - content: '"'; -} -.quoter { - text-align: center; -} -.quoter:before { - content: ''; - width: 50px; - height: 3px; - background-color: var(--green); - display: block; - margin: 20px auto; -} -.dark-bg { - background: var(--black); - border-top: 1px solid; - border-bottom: 1px solid; - border-color: rgba(73, 88, 107, 0.3); -} -.green-bordered-section { - border: 1px solid var(--green); - border-left: 0; - border-right: 0; -} -.blured-img { - position: absolute; - top: 50%; - left: 15px; - transform: translateY(-50%); - width: calc(100% - 30px); - filter: blur(10px); - z-index: 99; -} -.z-index-100 { - position: relative; - z-index: 100; -} -.border-green { - border: 1px solid var(--green); -} -.epic-text { - font-size: 54px; - line-height: 1.3em; - text-transform: uppercase; - font-weight: 100; -} -.blured-section { - background: rgba(0, 0, 0, 0.3); - background: linear-gradient(180deg, rgba(0, 16, 31, 0) 0%, rgba(0, 16, 31, 1) 80%); - -webkit-backdrop-filter: saturate(180%) blur(20px); - backdrop-filter: saturate(180%) blur(20px); -} -/* Image hover animation -.pull-img { - -webkit-transition: all 0.5s ease-in-out; - -moz-transition: all 0.5s ease-in-out; - -ms-transition: all 0.5s ease-in-out; - -o-transition: all 0.5s ease-in-out; - transition: all 0.5s ease-in-out; -} -.pull-img-to-right { - transform: translate(100%, -50%); -} -.pull-img-to-left { - transform: translate(-100%, -50%); -} -section:hover .pull-img-to-right, section:hover .pull-img-to-left { - transform: translate(0, -50%); -} -*/ - -.system-text { - font-size: 12px; - font-weight: bold; - color: #fff; -} -.download-icon { - height: 40px; -} -.download-icon.small { - height: 20px; - margin-left: 15px; - margin-top: 10px; - opacity: 0.5; -} -hr { - background-color: var(--green); - opacity: 1; -} -.m-100 { - margin: 100px 0; -} -.card label { - width: calc(100% - 30px); -} -@media (min-width: 576px) { - body { - --breakpoint: 540px; - } -} -@media (min-width: 768px) { - body { - --breakpoint: 720px; - } -} -@media (min-width: 992px) { - body { - --breakpoint: 960px; - } -} -@media (min-width: 1200px) { - body { - --breakpoint: 1140px; - } -} -@media (min-width: 1400px) { - body { - --breakpoint: 1320px; - } -} -@media only screen and (max-width: 1199px) { - .wallet-cubes { - display: none; - } - .navbar-expand-lg .navbar-nav .nav-item { - padding-left: 0.5em; - padding-right: 0.5em; - } - .epic-text { - font-size: 40px; - } -} -@media only screen and (max-width: 991px) { - h1 { - font-size: 40px; - } - h2 { - font-size: 24px; - } - .logo img { - width: 100px; - } - .video-homepage video, .video-homepage .mobile_video_bg { - height: 100vh; - object-fit: cover; - } - .home-text-area { - left: calc((100% - var(--breakpoint)) / 2); - } - .download-btn-area { - position: absolute; - left: calc((100% - var(--breakpoint)) / 2); - transform: translateY(-50%); - } - .service-line { - font-size: 20px; - display: block; - } - .service { - min-height: 200px; - width: 100% !important; - } - .service a { - padding: 100px 0; - } - .card table { - width: 100%; - margin-left: 0; - font-size: 12px; - } - .subpage p { - text-align: justify; - } - .fixed-img img { - object-fit: cover; - height: 100vh; - } - .dropdown-menu { - background: transparent; - -webkit-backdrop-filter: none; - backdrop-filter: none; - } - .navbar-expand-lg .navbar-nav .nav-item { - padding: 0.5em 0; - } - .nav-link { - padding: 0.5rem 1rem; - } - .dropdown-menu { - margin-top: 0; - } - .navbar { - padding: 5px 0; - } - .home-text-area p { - font-size: 14px; - } - .title-small { - font-size: 20px; - } - .feature-box .title { - font-size: 18px; - } - .pull-img { - width: 100%; - height: auto; - position: relative; - top: inherit; - left: inherit !important; - transform: none; - margin: 20px 0; - } - .p-100 { - padding: 40px 0; - } - .p-200 { - padding: 40px 0; - } - .modal-window { - width: 90vw; - } - .floating-title { - font-size: 60px; - } - .card label { - font-size: 14px; - } -} -@media only screen and (max-width: 600px) { - .home-text-area h1 { - font-size: 34px; - } - .download-btn-area { - transform: translateY(-50%); - max-width: 100%; - } - .home-text-area, .download-btn-area { - left: 20px; - } - .floating-title { - font-size: 24px; - left: 20px; - transform: translateY(-50%); - } - .title { - font-size: 24px; - } - .timeline li { - width: 90%; - padding: 60px 0; - } - .timeline:before, .timeline:after { - margin-left: calc(90% - 11px); - } - .timeline li:nth-child(odd) { - padding-right: 20%; - } - .timeline li:nth-child(even) { - margin-left: 0; - padding-left: 0; - padding-right: 20%; - } - .timeline li:nth-child(even) { - border-left: 0; - border-right: 1px solid var(--green); - } - .timeline li:nth-child(even) .timeline-title:after { - left: auto; - right: 0; - } - .timeline li:nth-child(even):before { - left: inherit; - right: calc(15% - 8px); - } - .epic-text { - font-size: 20px; - } - .timeline li:before { - font-size: 50px; - } - .timeline li:nth-child(even):before, - .timeline li:nth-child(odd):before { - right: calc(15% - 4px); - } - .timeline-title { - font-size: 16px; - margin-top: 10px; - } - .timeline-small-img { - height: 14px; - } -} -@media only screen and (max-width: 400px) {} @media only screen and (max-width: 320px) { - .home-text-area h1 { - font-size: 24px; - } - .home-text-area, .download-btn-area { - font-size: 14px; - } - .logo img { - width: 80px; - } - .download-btn-area { - transform: translateY(-50%); - } -} -/* Firefox fixes */ -@supports (-moz-appearance:none) { - .navbar { - background: rgba(0, 16, 31, 0.8); - } - .blured-section { - background: rgba(0, 0, 0, 0.9); - background: linear-gradient(180deg, rgba(0, 16, 31, 0.8) 0%, rgba(0, 16, 31, 1) 80%); - } -} \ No newline at end of file diff --git a/WalletWasabi.Backend/wwwroot/faq.html b/WalletWasabi.Backend/wwwroot/faq.html deleted file mode 100644 index 3db0a8cc3d..0000000000 --- a/WalletWasabi.Backend/wwwroot/faq.html +++ /dev/null @@ -1,446 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Wasabi Wallet - Bitcoin privacy wallet with built-in coinjoin - - - - - - -
- Wasabi Wallet - Help -
- -
-
-
-
-
-

FAQ

- - - -
- - -
-

Coinjoin is a collaborative transaction between multiple peers.

- -

Usually, but not necessarily, it consists of some standard output denominations that users should break their coins in to. This makes it difficult for outside parties to trace where a particular coin was sent to (as opposed to regular bitcoin transactions, where there is usually one sender and one receiver).

- -

Coinjoins can be done with non-custodial software like Wasabi Wallet, that eliminates the risk of funds disappearing or being stolen. The funds will always be in a bitcoin address that the user controls and not even developers can alter the transaction or redirect the funds.

- -

Coinjoin basicly means: “when you want to make a transaction, find someone else who also wants to make a transaction and make a joint transaction together”.

-
-
-
- - -
-

Coinjoining coins with a value above 0.01 BTC costs 0.3% as a coordinator fee + mining fees. Inputs of 0.01 BTC or below don't pay coordinator fees, nor remixes, even after one transaction. Thus, a payment made with coinjoined funds allows the sender and the recipient to remix their coins without paying any coordinator fees.

-
-
-
- - -
-

No, Wasabi’s coinjoin implementation is trustless by design. The participants do not need to trust each other or the coordinator. Since only the user knows the private keys, only he can sign the transaction, which will only be done after verifying that everything is alright. Nobody can neither steal your coins, nor figure out which outputs belong to which inputs.

-
-
-
- - -
-

Wasabi does support hardware wallet usage through the standard Bitcoin-core HWI, although coinjoining straight from or to a hardware wallet is not yet possible.

- -

Here's a list of the officially supported hardware wallets.

-
-
-
- - -
-

No, Wasabi and CoinJoin features require considerable computational power, not currently replicable on a smartphone.

-
-
-
- - -
-

There are countless reasons why it is the only logical choice to be bitcoin-only. With Bitcoin we have a once in a lifetime opportunity to manifest libre sound money. If we succeed, then an utmost beautiful agora of sovereign individuals may emerge. If we fail, then this will conjure up the most horrific Orwellian nightmare. There is no room for wasted time and energy, this great work requires our full attention. Any line of code written to support a random shitcoin takes away scarce developer time to work on real problems.

-
-
-
- - -
-

Wasabi runs in most operating systems with 64-bit architecture.

- -

For the complete list of all the officially supported operating systems, click here.

-
-
-
- - -
-

All Wasabi network traffic goes via Tor by default -there's no need to set up Tor by yourself. If you do already have Tor, and it is running, then Wasabi will try to use that first.

-

You can turn off Tor in the Settings. Note that in this case you are still private, except when you coinjoin and when you broadcast a transaction. In the first case, the coordinator would know the links between your inputs and outputs based on your IP address. In the second case, if you happen to broadcast a transaction of yours to a full node that is spying on you, it will know the link between your transaction and your IP address.

-
-
-
- - -
-

Addresses being used more than once is very damaging to privacy because that links together more blockchain transactions with proof that they were created by the same entity. The most private and secure way to use bitcoin is to send a brand new address to each person who pays you. After an address has received a coin, it should never be used again. Also, a brand new bitcoin address should be demanded from the recipient when sending bitcoin. Wasabi has a user interface which discourages address reuse by removing from the GUI addresses which have received a coin.

- -

It has been argued that the phrase "bitcoin address" was a bad name for this object because it implies it can be reused like an email address. A better name would be something like "bitcoin invoice".

- -

Bitcoin isn't anonymous but pseudonymous, and the pseudonyms are bitcoin addresses. Avoiding address reuse is like throwing away a pseudonym after it has been used.

-
-
-
- - -
-

The password you set is used as a 13th seed word (as described in BIP 39) and to encrypt the private key of the extended private key (as described in BIP 38) to get an encrypted secret which is stored on the computer.

- -

Wasabi Wallet stores only the BIP38 encrypted blob, so you'll need to type in the password to spend or coinjoin from the wallet.

- -

The password will unlock your bitcoin to anyone who has access to the recovery words backup or the computer! If your backup gets compromised, this password is the only thing protecting your precious sats.

- -

It is important to use a random and long password.

-
-
-
-
-
-
-
-
-
-
-

Help

-
-
-
-
-
-

GitHub

-
-
- -
- -
-
-
-
-

GitHub discussions

-
-
- -
- -
-
-
-
-

Documentation 1.0

-
-
- -
- -
-
-
-
-

Documentation 2.0

-
- -
- -
-
-
-
-
-
-
-
-
-
-

Reclaim your privacy now

-

Get started in 3 simple steps

-
-
-
-
- -
-

- 1. Download Wasabi Wallet -

-
-
-
- -
-

- 2. Create a new wallet -

-
-
-
- -
-

- 3. Let auto-coinjoin do its magic -

-
-
-
- -
-
-
-
-
-
- - - - diff --git a/WalletWasabi.Backend/wwwroot/fonts/objectivity.black-slanted.otf b/WalletWasabi.Backend/wwwroot/fonts/objectivity.black-slanted.otf deleted file mode 100644 index d863bd1575..0000000000 Binary files a/WalletWasabi.Backend/wwwroot/fonts/objectivity.black-slanted.otf and /dev/null differ diff --git a/WalletWasabi.Backend/wwwroot/fonts/objectivity.black.otf b/WalletWasabi.Backend/wwwroot/fonts/objectivity.black.otf deleted file mode 100644 index 177d15aed4..0000000000 Binary files a/WalletWasabi.Backend/wwwroot/fonts/objectivity.black.otf and /dev/null differ diff --git a/WalletWasabi.Backend/wwwroot/fonts/objectivity.bold-slanted.otf b/WalletWasabi.Backend/wwwroot/fonts/objectivity.bold-slanted.otf deleted file mode 100644 index 74cb5aa260..0000000000 Binary files a/WalletWasabi.Backend/wwwroot/fonts/objectivity.bold-slanted.otf and /dev/null differ diff --git a/WalletWasabi.Backend/wwwroot/fonts/objectivity.bold.otf b/WalletWasabi.Backend/wwwroot/fonts/objectivity.bold.otf deleted file mode 100644 index 6a769d2179..0000000000 Binary files a/WalletWasabi.Backend/wwwroot/fonts/objectivity.bold.otf and /dev/null differ diff --git a/WalletWasabi.Backend/wwwroot/fonts/objectivity.extra-bold-slanted.otf b/WalletWasabi.Backend/wwwroot/fonts/objectivity.extra-bold-slanted.otf deleted file mode 100644 index 6bbc5ccfbe..0000000000 Binary files a/WalletWasabi.Backend/wwwroot/fonts/objectivity.extra-bold-slanted.otf and /dev/null differ diff --git a/WalletWasabi.Backend/wwwroot/fonts/objectivity.extra-bold.otf b/WalletWasabi.Backend/wwwroot/fonts/objectivity.extra-bold.otf deleted file mode 100644 index 87c35e6dc5..0000000000 Binary files a/WalletWasabi.Backend/wwwroot/fonts/objectivity.extra-bold.otf and /dev/null differ diff --git a/WalletWasabi.Backend/wwwroot/fonts/objectivity.light-slanted.otf b/WalletWasabi.Backend/wwwroot/fonts/objectivity.light-slanted.otf deleted file mode 100644 index 2e2d6fa341..0000000000 Binary files a/WalletWasabi.Backend/wwwroot/fonts/objectivity.light-slanted.otf and /dev/null differ diff --git a/WalletWasabi.Backend/wwwroot/fonts/objectivity.light.otf b/WalletWasabi.Backend/wwwroot/fonts/objectivity.light.otf deleted file mode 100644 index 0791a14e6f..0000000000 Binary files a/WalletWasabi.Backend/wwwroot/fonts/objectivity.light.otf and /dev/null differ diff --git a/WalletWasabi.Backend/wwwroot/fonts/objectivity.medium-slanted.otf b/WalletWasabi.Backend/wwwroot/fonts/objectivity.medium-slanted.otf deleted file mode 100644 index 8dc9e9a32f..0000000000 Binary files a/WalletWasabi.Backend/wwwroot/fonts/objectivity.medium-slanted.otf and /dev/null differ diff --git a/WalletWasabi.Backend/wwwroot/fonts/objectivity.medium.otf b/WalletWasabi.Backend/wwwroot/fonts/objectivity.medium.otf deleted file mode 100644 index ede2ca5c92..0000000000 Binary files a/WalletWasabi.Backend/wwwroot/fonts/objectivity.medium.otf and /dev/null differ diff --git a/WalletWasabi.Backend/wwwroot/fonts/objectivity.regular-slanted.otf b/WalletWasabi.Backend/wwwroot/fonts/objectivity.regular-slanted.otf deleted file mode 100644 index e0dde0020f..0000000000 Binary files a/WalletWasabi.Backend/wwwroot/fonts/objectivity.regular-slanted.otf and /dev/null differ diff --git a/WalletWasabi.Backend/wwwroot/fonts/objectivity.regular.otf b/WalletWasabi.Backend/wwwroot/fonts/objectivity.regular.otf deleted file mode 100644 index 9ca7971c44..0000000000 Binary files a/WalletWasabi.Backend/wwwroot/fonts/objectivity.regular.otf and /dev/null differ diff --git a/WalletWasabi.Backend/wwwroot/fonts/objectivity.super-slanted.otf b/WalletWasabi.Backend/wwwroot/fonts/objectivity.super-slanted.otf deleted file mode 100644 index 3ed754eea6..0000000000 Binary files a/WalletWasabi.Backend/wwwroot/fonts/objectivity.super-slanted.otf and /dev/null differ diff --git a/WalletWasabi.Backend/wwwroot/fonts/objectivity.super.otf b/WalletWasabi.Backend/wwwroot/fonts/objectivity.super.otf deleted file mode 100644 index 53b3456213..0000000000 Binary files a/WalletWasabi.Backend/wwwroot/fonts/objectivity.super.otf and /dev/null differ diff --git a/WalletWasabi.Backend/wwwroot/fonts/objectivity.thin-slanted.otf b/WalletWasabi.Backend/wwwroot/fonts/objectivity.thin-slanted.otf deleted file mode 100644 index 58df49fa9f..0000000000 Binary files a/WalletWasabi.Backend/wwwroot/fonts/objectivity.thin-slanted.otf and /dev/null differ diff --git a/WalletWasabi.Backend/wwwroot/fonts/objectivity.thin.otf b/WalletWasabi.Backend/wwwroot/fonts/objectivity.thin.otf deleted file mode 100644 index dc9c1bac02..0000000000 Binary files a/WalletWasabi.Backend/wwwroot/fonts/objectivity.thin.otf and /dev/null differ diff --git a/WalletWasabi.Backend/wwwroot/img/01_About_us.png b/WalletWasabi.Backend/wwwroot/img/01_About_us.png deleted file mode 100644 index ba138d6753..0000000000 Binary files a/WalletWasabi.Backend/wwwroot/img/01_About_us.png and /dev/null differ diff --git a/WalletWasabi.Backend/wwwroot/img/02_Blog.png b/WalletWasabi.Backend/wwwroot/img/02_Blog.png deleted file mode 100644 index d870ed2024..0000000000 Binary files a/WalletWasabi.Backend/wwwroot/img/02_Blog.png and /dev/null differ diff --git a/WalletWasabi.Backend/wwwroot/img/03_press_kit.png b/WalletWasabi.Backend/wwwroot/img/03_press_kit.png deleted file mode 100644 index 57b8a430c0..0000000000 Binary files a/WalletWasabi.Backend/wwwroot/img/03_press_kit.png and /dev/null differ diff --git a/WalletWasabi.Backend/wwwroot/img/04_support.png b/WalletWasabi.Backend/wwwroot/img/04_support.png deleted file mode 100644 index 17932a4eb9..0000000000 Binary files a/WalletWasabi.Backend/wwwroot/img/04_support.png and /dev/null differ diff --git a/WalletWasabi.Backend/wwwroot/img/05_wallet.png b/WalletWasabi.Backend/wwwroot/img/05_wallet.png deleted file mode 100644 index 22dcbf9878..0000000000 Binary files a/WalletWasabi.Backend/wwwroot/img/05_wallet.png and /dev/null differ diff --git a/WalletWasabi.Backend/wwwroot/img/06_contribution.png b/WalletWasabi.Backend/wwwroot/img/06_contribution.png deleted file mode 100644 index 6301505d3f..0000000000 Binary files a/WalletWasabi.Backend/wwwroot/img/06_contribution.png and /dev/null differ diff --git a/WalletWasabi.Backend/wwwroot/img/blockchain.svg b/WalletWasabi.Backend/wwwroot/img/blockchain.svg deleted file mode 100644 index b35dd099c1..0000000000 --- a/WalletWasabi.Backend/wwwroot/img/blockchain.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/WalletWasabi.Backend/wwwroot/img/btc-artwork.svg b/WalletWasabi.Backend/wwwroot/img/btc-artwork.svg deleted file mode 100644 index 9aee58f25f..0000000000 --- a/WalletWasabi.Backend/wwwroot/img/btc-artwork.svg +++ /dev/null @@ -1,27646 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/WalletWasabi.Backend/wwwroot/img/coinjoin.svg b/WalletWasabi.Backend/wwwroot/img/coinjoin.svg deleted file mode 100644 index b505fb5c41..0000000000 --- a/WalletWasabi.Backend/wwwroot/img/coinjoin.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/WalletWasabi.Backend/wwwroot/img/coinjoinanyamount.svg b/WalletWasabi.Backend/wwwroot/img/coinjoinanyamount.svg deleted file mode 100644 index 80a4487246..0000000000 --- a/WalletWasabi.Backend/wwwroot/img/coinjoinanyamount.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/WalletWasabi.Backend/wwwroot/img/createnewwallet.svg b/WalletWasabi.Backend/wwwroot/img/createnewwallet.svg deleted file mode 100644 index 0be13f553b..0000000000 --- a/WalletWasabi.Backend/wwwroot/img/createnewwallet.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/WalletWasabi.Backend/wwwroot/img/cube_bg_1.svg b/WalletWasabi.Backend/wwwroot/img/cube_bg_1.svg deleted file mode 100644 index cd5dd17d44..0000000000 --- a/WalletWasabi.Backend/wwwroot/img/cube_bg_1.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/WalletWasabi.Backend/wwwroot/img/cube_bg_2.svg b/WalletWasabi.Backend/wwwroot/img/cube_bg_2.svg deleted file mode 100644 index 43f2dd7ccc..0000000000 --- a/WalletWasabi.Backend/wwwroot/img/cube_bg_2.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/WalletWasabi.Backend/wwwroot/img/download-arrow.svg b/WalletWasabi.Backend/wwwroot/img/download-arrow.svg deleted file mode 100644 index f8aed790c6..0000000000 --- a/WalletWasabi.Backend/wwwroot/img/download-arrow.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/WalletWasabi.Backend/wwwroot/img/download.svg b/WalletWasabi.Backend/wwwroot/img/download.svg deleted file mode 100644 index 1135ab1aed..0000000000 --- a/WalletWasabi.Backend/wwwroot/img/download.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/WalletWasabi.Backend/wwwroot/img/downloadww.svg b/WalletWasabi.Backend/wwwroot/img/downloadww.svg deleted file mode 100644 index 89b57df26f..0000000000 --- a/WalletWasabi.Backend/wwwroot/img/downloadww.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/WalletWasabi.Backend/wwwroot/img/easytouse.svg b/WalletWasabi.Backend/wwwroot/img/easytouse.svg deleted file mode 100644 index f8a2665033..0000000000 --- a/WalletWasabi.Backend/wwwroot/img/easytouse.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/WalletWasabi.Backend/wwwroot/img/favicon.ico b/WalletWasabi.Backend/wwwroot/img/favicon.ico deleted file mode 100644 index e1b342a578..0000000000 Binary files a/WalletWasabi.Backend/wwwroot/img/favicon.ico and /dev/null differ diff --git a/WalletWasabi.Backend/wwwroot/img/github.svg b/WalletWasabi.Backend/wwwroot/img/github.svg deleted file mode 100644 index 243c026512..0000000000 --- a/WalletWasabi.Backend/wwwroot/img/github.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/WalletWasabi.Backend/wwwroot/img/homepage_anim.mp4 b/WalletWasabi.Backend/wwwroot/img/homepage_anim.mp4 deleted file mode 100644 index f4f6d9e84d..0000000000 Binary files a/WalletWasabi.Backend/wwwroot/img/homepage_anim.mp4 and /dev/null differ diff --git a/WalletWasabi.Backend/wwwroot/img/icon-external.svg b/WalletWasabi.Backend/wwwroot/img/icon-external.svg deleted file mode 100644 index 5f9fdf2017..0000000000 --- a/WalletWasabi.Backend/wwwroot/img/icon-external.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/WalletWasabi.Backend/wwwroot/img/icon_apple_silicon.svg b/WalletWasabi.Backend/wwwroot/img/icon_apple_silicon.svg deleted file mode 100644 index 1624a1653c..0000000000 --- a/WalletWasabi.Backend/wwwroot/img/icon_apple_silicon.svg +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/WalletWasabi.Backend/wwwroot/img/icon_intel.svg b/WalletWasabi.Backend/wwwroot/img/icon_intel.svg deleted file mode 100644 index e10269afdc..0000000000 --- a/WalletWasabi.Backend/wwwroot/img/icon_intel.svg +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - -]> - - - - - - - - - - - diff --git a/WalletWasabi.Backend/wwwroot/img/icon_linux.svg b/WalletWasabi.Backend/wwwroot/img/icon_linux.svg deleted file mode 100644 index cc726f93f5..0000000000 --- a/WalletWasabi.Backend/wwwroot/img/icon_linux.svg +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - diff --git a/WalletWasabi.Backend/wwwroot/img/icon_m1.svg b/WalletWasabi.Backend/wwwroot/img/icon_m1.svg deleted file mode 100644 index ab43566464..0000000000 --- a/WalletWasabi.Backend/wwwroot/img/icon_m1.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - diff --git a/WalletWasabi.Backend/wwwroot/img/icon_mac.svg b/WalletWasabi.Backend/wwwroot/img/icon_mac.svg deleted file mode 100644 index 32be666596..0000000000 --- a/WalletWasabi.Backend/wwwroot/img/icon_mac.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/WalletWasabi.Backend/wwwroot/img/icon_ubuntu.svg b/WalletWasabi.Backend/wwwroot/img/icon_ubuntu.svg deleted file mode 100644 index 31128cea61..0000000000 --- a/WalletWasabi.Backend/wwwroot/img/icon_ubuntu.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/WalletWasabi.Backend/wwwroot/img/icon_windows.svg b/WalletWasabi.Backend/wwwroot/img/icon_windows.svg deleted file mode 100644 index dd55f8c829..0000000000 --- a/WalletWasabi.Backend/wwwroot/img/icon_windows.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/WalletWasabi.Backend/wwwroot/img/logo.svg b/WalletWasabi.Backend/wwwroot/img/logo.svg deleted file mode 100644 index c59c1027fb..0000000000 --- a/WalletWasabi.Backend/wwwroot/img/logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/WalletWasabi.Backend/wwwroot/img/logo_alt.svg b/WalletWasabi.Backend/wwwroot/img/logo_alt.svg deleted file mode 100644 index 5ce5b873d9..0000000000 --- a/WalletWasabi.Backend/wwwroot/img/logo_alt.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/WalletWasabi.Backend/wwwroot/img/logo_small.svg b/WalletWasabi.Backend/wwwroot/img/logo_small.svg deleted file mode 100644 index e1fc3a1f4a..0000000000 --- a/WalletWasabi.Backend/wwwroot/img/logo_small.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - diff --git a/WalletWasabi.Backend/wwwroot/img/screenshots/webpage_ui_compilation-02.png b/WalletWasabi.Backend/wwwroot/img/screenshots/webpage_ui_compilation-02.png deleted file mode 100644 index b39aa2c993..0000000000 Binary files a/WalletWasabi.Backend/wwwroot/img/screenshots/webpage_ui_compilation-02.png and /dev/null differ diff --git a/WalletWasabi.Backend/wwwroot/img/screenshots/webpage_ui_compilation-04.png b/WalletWasabi.Backend/wwwroot/img/screenshots/webpage_ui_compilation-04.png deleted file mode 100644 index a4bbb8a6d0..0000000000 Binary files a/WalletWasabi.Backend/wwwroot/img/screenshots/webpage_ui_compilation-04.png and /dev/null differ diff --git a/WalletWasabi.Backend/wwwroot/img/screenshots/webpage_ui_compilation-05.png b/WalletWasabi.Backend/wwwroot/img/screenshots/webpage_ui_compilation-05.png deleted file mode 100644 index b6599f4f27..0000000000 Binary files a/WalletWasabi.Backend/wwwroot/img/screenshots/webpage_ui_compilation-05.png and /dev/null differ diff --git a/WalletWasabi.Backend/wwwroot/img/sendsomebitcoin.svg b/WalletWasabi.Backend/wwwroot/img/sendsomebitcoin.svg deleted file mode 100644 index ed8475468e..0000000000 --- a/WalletWasabi.Backend/wwwroot/img/sendsomebitcoin.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/WalletWasabi.Backend/wwwroot/img/social1.svg b/WalletWasabi.Backend/wwwroot/img/social1.svg deleted file mode 100644 index fa1c334016..0000000000 --- a/WalletWasabi.Backend/wwwroot/img/social1.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/WalletWasabi.Backend/wwwroot/img/social2.svg b/WalletWasabi.Backend/wwwroot/img/social2.svg deleted file mode 100644 index 8b2a1456eb..0000000000 --- a/WalletWasabi.Backend/wwwroot/img/social2.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/WalletWasabi.Backend/wwwroot/img/social3.svg b/WalletWasabi.Backend/wwwroot/img/social3.svg deleted file mode 100644 index 9543719654..0000000000 --- a/WalletWasabi.Backend/wwwroot/img/social3.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/WalletWasabi.Backend/wwwroot/img/social4.svg b/WalletWasabi.Backend/wwwroot/img/social4.svg deleted file mode 100644 index 3e14a71d8d..0000000000 --- a/WalletWasabi.Backend/wwwroot/img/social4.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/WalletWasabi.Backend/wwwroot/img/social5.svg b/WalletWasabi.Backend/wwwroot/img/social5.svg deleted file mode 100644 index e7476ac8fb..0000000000 --- a/WalletWasabi.Backend/wwwroot/img/social5.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/WalletWasabi.Backend/wwwroot/img/social6.svg b/WalletWasabi.Backend/wwwroot/img/social6.svg deleted file mode 100644 index abd2e14e5c..0000000000 --- a/WalletWasabi.Backend/wwwroot/img/social6.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/WalletWasabi.Backend/wwwroot/img/social7.svg b/WalletWasabi.Backend/wwwroot/img/social7.svg deleted file mode 100644 index 307d90e110..0000000000 --- a/WalletWasabi.Backend/wwwroot/img/social7.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/WalletWasabi.Backend/wwwroot/img/trustless.svg b/WalletWasabi.Backend/wwwroot/img/trustless.svg deleted file mode 100644 index 19d16fd2d0..0000000000 --- a/WalletWasabi.Backend/wwwroot/img/trustless.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/WalletWasabi.Backend/wwwroot/img/unlimitedfreeremixes.svg b/WalletWasabi.Backend/wwwroot/img/unlimitedfreeremixes.svg deleted file mode 100644 index 1b4bef8159..0000000000 --- a/WalletWasabi.Backend/wwwroot/img/unlimitedfreeremixes.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/WalletWasabi.Backend/wwwroot/img/video_bg.png b/WalletWasabi.Backend/wwwroot/img/video_bg.png deleted file mode 100644 index ce5e0cbc23..0000000000 Binary files a/WalletWasabi.Backend/wwwroot/img/video_bg.png and /dev/null differ diff --git a/WalletWasabi.Backend/wwwroot/img/wasabi-social.jpg b/WalletWasabi.Backend/wwwroot/img/wasabi-social.jpg deleted file mode 100644 index 72e38ab0f5..0000000000 Binary files a/WalletWasabi.Backend/wwwroot/img/wasabi-social.jpg and /dev/null differ diff --git a/WalletWasabi.Backend/wwwroot/img/wasabi-wallet-logo.svg b/WalletWasabi.Backend/wwwroot/img/wasabi-wallet-logo.svg deleted file mode 100644 index 406bb73d5b..0000000000 --- a/WalletWasabi.Backend/wwwroot/img/wasabi-wallet-logo.svg +++ /dev/null @@ -1,30 +0,0 @@ - - - - -Created with Sketch. - - - - diff --git a/WalletWasabi.Backend/wwwroot/index.html b/WalletWasabi.Backend/wwwroot/index.html deleted file mode 100644 index bcbfb1c53c..0000000000 --- a/WalletWasabi.Backend/wwwroot/index.html +++ /dev/null @@ -1,338 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Wasabi Wallet - Bitcoin privacy wallet with built-in coinjoin - - - - - - -
-
- - Wasabi Wallet - video - -
-
-

Privacy by default

-

- Open-source, non-custodial -
Bitcoin Wallet for desktop -

-
- - DOWNLOAD - -
-
-
-
-
-
-
-
Privacy is your ability to selectively reveal yourself to the world
-
-
-
-

- We live in an Orwellian surveillance society where your information is being used to typecast and manipulate you. Bitcoin projects are being pressured to collect more and more data, if possible. -
This is why Wasabi Wallet is programmed to be a zero-knowledge software. Developers can't collect any sensitive information about you. What you do with your bitcoin is your business. -

-
-
-
-
- -
-
-
-
-
-
- -
-
-
-
-

Why Wasabi Wallet?

-
-
-
-
-
    -
  • -

    Open Source

    -

    Wasabi Wallet is a free, open-source and deterministically reproducible software. Anyone can see, verify and even contribute to the code.

    -
  • -
  • -

    Trustless By Design

    -

    - The software is designed so that neither the public nor the developers can breach your privacy. This is done through coinjoins, client-side block filtering and communication over the Tor anonymity network. -

    -
  • -
  • -

    Non-custodial

    -

    Not your keys, not your coins. Wasabi Wallet lets you control your private keys, offering you true financial self sovereignty.

    -
  • -
-
-
- -
-
-
-
- -
-
-
    -
  • -

    Easy To Use

    -

    Wasabi is designed to be a user friendly Bitcoin wallet, that handles its users' privacy automatically under the hood, including network connections, input selection and coinjoining.

    -
  • -
  • -

    Comprehensive

    -

    The wallet uses WabiSabi, an anonymous credential scheme that was designed to enable more accessible and efficient coinjoins. It allows users to utilize the best privacy tool without requiring a large amount of bitcoin in the wallet.

    -
  • -
  • -

    Affordable

    -

    Coinjoining coins with a value above 0.01 BTC costs 0.3% as a coordinator fee + mining fees. Inputs of 0.01 BTC or below don't pay coordinator fees, nor remixes, even after one transaction. Thus, a payment made with coinjoined funds allows the sender and the recipient to remix their coins without paying any coordinator fees.

    -
  • -
-
-
-
-
-
-
-
-
-
-

DOWNLOAD
WASABI WALLET

-
-
-
-
-
- or build it from source -
PGP (6FB3 872B 5D42 292F 5992 0797 8563 4832 8949 861E) -
RELEASE NOTES -
-
-
-
- -
-
WINDOWS 10+
-
- Download -
- .msi signature / guide -
-
-
-
-
- -
-
MACOS 10.15+ INTEL
-
- Download -
- .dmg signature / guide -
-
-
-
-
- -
-
MACOS 10.15+ M1, M2
-
- Download -
- .dmg signature / guide -
-
-
-
-
- -
-
UBUNTU / DEBIAN
-
- Download -
- .deb signature / guide -
-
-
-
-
- -
-
OTHER LINUX
-
- Download -
- .tar.gz signature / guide -
-
-
-
-
-
-
-
-
-
- - - - diff --git a/WalletWasabi.Backend/wwwroot/onion-index.html b/WalletWasabi.Backend/wwwroot/onion-index.html deleted file mode 100644 index 507f857a28..0000000000 --- a/WalletWasabi.Backend/wwwroot/onion-index.html +++ /dev/null @@ -1,338 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Wasabi Wallet - Bitcoin privacy wallet with built-in coinjoin - - - - - - -
-
- - Wasabi Wallet - video - -
-
-

Privacy by default

-

- Open-source, non-custodial -
Bitcoin Wallet for desktop -

-
- - DOWNLOAD - -
-
-
-
-
-
-
-
Privacy is your ability to selectively reveal yourself to the world
-
-
-
-

- We live in an Orwellian surveillance society where your information is being used to typecast and manipulate you. Bitcoin projects are being pressured to collect more and more data, if possible. -
This is why Wasabi Wallet is programmed to be a zero-knowledge software. Developers can't collect any sensitive information about you. What you do with your bitcoin is your business. -

-
-
-
-
- -
-
-
-
-
-
- -
-
-
-
-

Why Wasabi Wallet?

-
-
-
-
-
    -
  • -

    Open Source

    -

    Wasabi Wallet is a free, open-source and deterministically reproducible software. Anyone can see, verify and even contribute to the code.

    -
  • -
  • -

    Trustless By Design

    -

    - The software is designed so that neither the public nor the developers can breach your privacy. This is done through coinjoins, client-side block filtering and communication over the Tor anonymity network. -

    -
  • -
  • -

    Non-custodial

    -

    Not your keys, not your coins. Wasabi Wallet lets you control your private keys, offering you true financial self sovereignty.

    -
  • -
-
-
- -
-
-
-
- -
-
-
    -
  • -

    Easy To Use

    -

    Wasabi is designed to be a user friendly Bitcoin wallet, that handles its users' privacy automatically under the hood, including network connections, input selection and coinjoining.

    -
  • -
  • -

    Comprehensive

    -

    The wallet uses WabiSabi, an anonymous credential scheme that was designed to enable more accessible and efficient coinjoins. It allows users to utilize the best privacy tool without requiring a large amount of bitcoin in the wallet.

    -
  • -
  • -

    Affordable

    -

    Coinjoining coins with a value above 0.01 BTC costs 0.3% as a coordinator fee + mining fees. Inputs of 0.01 BTC or below don't pay coordinator fees, nor remixes, even after one transaction. Thus, a payment made with coinjoined funds allows the sender and the recipient to remix their coins without paying any coordinator fees.

    -
  • -
-
-
-
-
-
-
-
-
-
-

DOWNLOAD
WASABI WALLET 2.0

-
-
-
-
-
- or build it from source -
PGP (6FB3 872B 5D42 292F 5992 0797 8563 4832 8949 861E) -
RELEASE NOTES -
-
-
-
- -
-
WINDOWS 10+
-
- Download -
- .msi signature / guide -
-
-
-
-
- -
-
MACOS 10.15+ INTEL
-
- Download -
- .dmg signature / guide -
-
-
-
-
- -
-
MACOS 10.15+ M1
-
- Download -
- .dmg signature / guide -
-
-
-
-
- -
-
UBUNTU / DEBIAN
-
- Download -
- .deb signature / guide -
-
-
-
-
- -
-
OTHER LINUX
-
- Download -
- .tar.gz signature / guide -
-
-
-
-
-
-
-
-
-
- - - - diff --git a/WalletWasabi.Backend/wwwroot/robots.txt b/WalletWasabi.Backend/wwwroot/robots.txt deleted file mode 100644 index 53c550d502..0000000000 --- a/WalletWasabi.Backend/wwwroot/robots.txt +++ /dev/null @@ -1,10 +0,0 @@ -User-agent: * -Allow: / - -Sitemap: https://wasabiwallet.io/sitemap.xml - -Disallow: /restore/ -Disallow: /bak/ -Disallow: /old/ -Disallow: /back/ -Disallow: /backups/ \ No newline at end of file diff --git a/WalletWasabi.Backend/wwwroot/sitemap.xml b/WalletWasabi.Backend/wwwroot/sitemap.xml deleted file mode 100644 index 4774cd9817..0000000000 --- a/WalletWasabi.Backend/wwwroot/sitemap.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - https://wasabiwallet.io/index.html - 2022-11-19T19:54:24+00:00 - 1.00 - - - https://wasabiwallet.io/about.html - 2022-11-19T19:54:24+00:00 - 0.80 - - - https://wasabiwallet.io/contribution.html - 2022-11-19T19:54:24+00:00 - 0.80 - - - https://wasabiwallet.io/faq.html - 2022-11-19T19:54:24+00:00 - 0.80 - - \ No newline at end of file diff --git a/WalletWasabi.Daemon/ArgumentHelpers.cs b/WalletWasabi.Daemon/ArgumentHelpers.cs new file mode 100644 index 0000000000..7026d88d8b --- /dev/null +++ b/WalletWasabi.Daemon/ArgumentHelpers.cs @@ -0,0 +1,35 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace WalletWasabi.Daemon; + +public static class ArgumentHelpers +{ + public static bool TryGetValue(string key, string[] args, [NotNullWhen(true)] out string? value) + { + string cliArgKey = $"--{key}="; + + foreach (string arg in args) + { + if (arg.StartsWith(cliArgKey, StringComparison.InvariantCultureIgnoreCase)) + { + value = arg[cliArgKey.Length..]; + return true; + } + } + + value = null; + return false; + } + + public static string[] GetValues(string key, string[] args) + { + string cliArgKey = $"--{key}="; + + return args + .Where(x => x.StartsWith(cliArgKey, StringComparison.InvariantCultureIgnoreCase)) + .Select(x => x[cliArgKey.Length..]) + .ToArray(); + } +} diff --git a/WalletWasabi.Daemon/Config.cs b/WalletWasabi.Daemon/Config.cs new file mode 100644 index 0000000000..ea0e94d964 --- /dev/null +++ b/WalletWasabi.Daemon/Config.cs @@ -0,0 +1,393 @@ +using NBitcoin; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Net; +using WalletWasabi.Exceptions; +using WalletWasabi.Helpers; +using WalletWasabi.Models; +using WalletWasabi.Userfacing; + +namespace WalletWasabi.Daemon; + +public class Config +{ + public static readonly IDictionary EnvironmentVariables = Environment.GetEnvironmentVariables(); + + public Config(PersistentConfig persistentConfig, string[] cliArgs) + { + PersistentConfig = persistentConfig; + CliArgs = cliArgs; + + Data = new() + { + [ nameof(Network)] = ( + "The Bitcoin network to use: main, testnet, or regtest", + GetNetworkValue("Network", PersistentConfig.Network.ToString(), cliArgs)), + [ nameof(MainNetBackendUri)] = ( + "The backend server's URL to connect to when the Bitcoin network is main", + GetStringValue("MainNetBackendUri", PersistentConfig.MainNetBackendUri, cliArgs)), + [ nameof(TestNetBackendUri)] = ( + "The backend server's URL to connect to when the Bitcoin network is testnet", + GetStringValue("TestNetBackendUri", PersistentConfig.TestNetBackendUri, cliArgs)), + [ nameof(RegTestBackendUri)] = ( + "The backend server's URL to connect to when the Bitcoin network is regtest", + GetStringValue("RegTestBackendUri", PersistentConfig.RegTestBackendUri, cliArgs)), + [ nameof(MainNetCoordinatorUri)] = ( + "The coordinator server's URL to connect to when the Bitcoin network is main", + GetNullableStringValue("MainNetCoordinatorUri", PersistentConfig.MainNetCoordinatorUri, cliArgs)), + [ nameof(TestNetCoordinatorUri)] = ( + "The coordinator server's URL to connect to when the Bitcoin network is testnet", + GetNullableStringValue("TestNetCoordinatorUri", PersistentConfig.TestNetCoordinatorUri, cliArgs)), + [ nameof(RegTestCoordinatorUri)] = ( + "The coordinator server's URL to connect to when the Bitcoin network is regtest", + GetNullableStringValue("RegTestCoordinatorUri", PersistentConfig.RegTestCoordinatorUri, cliArgs)), + [ nameof(UseTor)] = ( + "All the communications go through the Tor network", + GetBoolValue("UseTor", PersistentConfig.UseTor, cliArgs)), + [ nameof(TerminateTorOnExit)] = ( + "Stop the Tor process when Wasabi is closed", + GetBoolValue("TerminateTorOnExit", PersistentConfig.TerminateTorOnExit, cliArgs)), + [ nameof(DownloadNewVersion)] = ( + "Automatically download any new released version of Wasabi", + GetBoolValue("DownloadNewVersion", PersistentConfig.DownloadNewVersion, cliArgs)), + [ nameof(StartLocalBitcoinCoreOnStartup)] = ( + "Start a local bitcoin node when Wasabi starts", + GetBoolValue("StartLocalBitcoinCoreOnStartup", PersistentConfig.StartLocalBitcoinCoreOnStartup, cliArgs)), + [ nameof(StopLocalBitcoinCoreOnShutdown)] = ( + "Stop the local bitcoin node when Wasabi is closed", + GetBoolValue("StopLocalBitcoinCoreOnShutdown", PersistentConfig.StopLocalBitcoinCoreOnShutdown, cliArgs)), + [ nameof(LocalBitcoinCoreDataDir)] = ( + "The path of the data directory to be used by the local bitcoin node", + GetStringValue("LocalBitcoinCoreDataDir", PersistentConfig.LocalBitcoinCoreDataDir, cliArgs)), + [ nameof(MainNetBitcoinP2pEndPoint)] = ( + "-", + GetEndPointValue("MainNetBitcoinP2pEndPoint", PersistentConfig.MainNetBitcoinP2pEndPoint, cliArgs)), + [ nameof(TestNetBitcoinP2pEndPoint)] = ( + "-", + GetEndPointValue("TestNetBitcoinP2pEndPoint", PersistentConfig.TestNetBitcoinP2pEndPoint, cliArgs)), + [ nameof(RegTestBitcoinP2pEndPoint)] = ( + "-", + GetEndPointValue("RegTestBitcoinP2pEndPoint", PersistentConfig.RegTestBitcoinP2pEndPoint, cliArgs)), + [ nameof(JsonRpcServerEnabled)] = ( + "Start the Json RPC Server and accept requests", + GetBoolValue("JsonRpcServerEnabled", PersistentConfig.JsonRpcServerEnabled, cliArgs)), + [ nameof(JsonRpcUser)] = ( + "The user name that is authorized to make requests to the Json RPC server", + GetStringValue("JsonRpcUser", PersistentConfig.JsonRpcUser, cliArgs)), + [ nameof(JsonRpcPassword)] = ( + "The user password that is authorized to make requests to the Json RPC server", + GetStringValue("JsonRpcPassword", PersistentConfig.JsonRpcPassword, cliArgs)), + [ nameof(JsonRpcServerPrefixes)] = ( + "The Json RPC server prefixes", + GetStringArrayValue("JsonRpcServerPrefixes", PersistentConfig.JsonRpcServerPrefixes, cliArgs)), + [ nameof(RpcOnionEnabled)] = ( + "Publish the Json RPC Server as a Tor Onion service", + GetBoolValue("RpcOnionEnabled", value: false, cliArgs)), + [ nameof(DustThreshold)] = ( + "The amount threshold under which coins received from others to already used addresses are considered a dust attack", + GetMoneyValue("DustThreshold", PersistentConfig.DustThreshold, cliArgs)), + [ nameof(BlockOnlyMode)] = ( + "Wasabi listens only for blocks and not for transactions", + GetBoolValue("BlockOnly", value: false, cliArgs)), + [ nameof(LogLevel)] = ( + "The level of detail in the logs: trace, debug, info, warning, error, or critical", + GetStringValue("LogLevel", value: "", cliArgs)), + [ nameof(EnableGpu)] = ( + "Use a GPU to render the user interface", + GetBoolValue("EnableGpu", PersistentConfig.EnableGpu, cliArgs)), + [ nameof(CoordinatorIdentifier)] = ( + "-", + GetStringValue("CoordinatorIdentifier", PersistentConfig.CoordinatorIdentifier, cliArgs)), + }; + + // Check if any config value is overridden (either by an environment value, or by a CLI argument). + foreach ((_, IValue optionValue) in Data.Values) + { + if (optionValue.Overridden) + { + IsOverridden = true; + break; + } + } + + ServiceConfiguration = new ServiceConfiguration(GetBitcoinP2pEndPoint(), DustThreshold); + } + + private Dictionary Data { get; } + public PersistentConfig PersistentConfig { get; } + public string[] CliArgs { get; } + public Network Network => GetEffectiveValue(nameof(Network)); + + public string MainNetBackendUri => GetEffectiveValue(nameof(MainNetBackendUri)); + public string TestNetBackendUri => GetEffectiveValue(nameof(TestNetBackendUri)); + public string RegTestBackendUri => GetEffectiveValue(nameof(RegTestBackendUri)); + public string? MainNetCoordinatorUri => GetEffectiveValue(nameof(MainNetCoordinatorUri)); + public string? TestNetCoordinatorUri => GetEffectiveValue(nameof(TestNetCoordinatorUri)); + public string? RegTestCoordinatorUri => GetEffectiveValue(nameof(RegTestCoordinatorUri)); + public bool UseTor => GetEffectiveValue(nameof(UseTor)); + public bool TerminateTorOnExit => GetEffectiveValue(nameof(TerminateTorOnExit)); + public bool DownloadNewVersion => GetEffectiveValue(nameof(DownloadNewVersion)); + public bool StartLocalBitcoinCoreOnStartup => GetEffectiveValue(nameof(StartLocalBitcoinCoreOnStartup)); + public bool StopLocalBitcoinCoreOnShutdown => GetEffectiveValue(nameof(StopLocalBitcoinCoreOnShutdown)); + public string LocalBitcoinCoreDataDir => GetEffectiveValue(nameof(LocalBitcoinCoreDataDir)); + public EndPoint MainNetBitcoinP2pEndPoint => GetEffectiveValue(nameof(MainNetBitcoinP2pEndPoint)); + public EndPoint TestNetBitcoinP2pEndPoint => GetEffectiveValue(nameof(TestNetBitcoinP2pEndPoint)); + public EndPoint RegTestBitcoinP2pEndPoint => GetEffectiveValue(nameof(RegTestBitcoinP2pEndPoint)); + public bool JsonRpcServerEnabled => GetEffectiveValue(nameof(JsonRpcServerEnabled)); + public string JsonRpcUser => GetEffectiveValue(nameof(JsonRpcUser)); + public string JsonRpcPassword => GetEffectiveValue(nameof(JsonRpcPassword)); + public string[] JsonRpcServerPrefixes => GetEffectiveValue(nameof(JsonRpcServerPrefixes)); + public bool RpcOnionEnabled => GetEffectiveValue(nameof(RpcOnionEnabled)); + public Money DustThreshold => GetEffectiveValue(nameof(DustThreshold)); + public bool BlockOnlyMode => GetEffectiveValue(nameof(BlockOnlyMode)); + public string LogLevel => GetEffectiveValue(nameof(LogLevel)); + + public bool EnableGpu => GetEffectiveValue(nameof(EnableGpu)); + public string CoordinatorIdentifier => GetEffectiveValue(nameof(CoordinatorIdentifier)); + public ServiceConfiguration ServiceConfiguration { get; } + + public static string DataDir { get; } = GetStringValue( + "datadir", + EnvironmentHelpers.GetDataDir(Path.Combine("WalletWasabi", "Client")), + Environment.GetCommandLineArgs()).EffectiveValue; + + public bool IsOverridden { get; } + + public EndPoint GetBitcoinP2pEndPoint() + { + if (Network == Network.Main) + { + return MainNetBitcoinP2pEndPoint; + } + + if (Network == Network.TestNet) + { + return TestNetBitcoinP2pEndPoint; + } + + if (Network == Network.RegTest) + { + return RegTestBitcoinP2pEndPoint; + } + + throw new NotSupportedNetworkException(Network); + } + + public Uri GetBackendUri() + { + if (Network == Network.Main) + { + return new Uri(MainNetBackendUri); + } + + if (Network == Network.TestNet) + { + return new Uri(TestNetBackendUri); + } + + if (Network == Network.RegTest) + { + return new Uri(RegTestBackendUri); + } + + throw new NotSupportedNetworkException(Network); + } + + public Uri GetCoordinatorUri() + { + var result = Network switch + { + { } n when n == Network.Main => MainNetCoordinatorUri, + { } n when n == Network.TestNet => TestNetCoordinatorUri, + { } n when n == Network.RegTest => RegTestCoordinatorUri, + _ => throw new NotSupportedNetworkException(Network) + }; + + return result is null ? GetBackendUri() : new Uri(result); + } + + public IEnumerable<(string ParameterName, string Hint)> GetConfigOptionsMetadata() => + Data.Select(x => (x.Key, x.Value.Hint)); + + private EndPointValue GetEndPointValue(string key, EndPoint value, string[] cliArgs) + { + if (GetOverrideValue(key, cliArgs, out string? overrideValue, out ValueSource? valueSource)) + { + if (!EndPointParser.TryParse(overrideValue, 0, out var endpoint)) + { + throw new ArgumentNullException(key, "Not a valid endpoint"); + } + + return new EndPointValue(value, endpoint, valueSource.Value); + } + + return new EndPointValue(value, value, ValueSource.Disk); + } + + private MoneyValue GetMoneyValue(string key, Money value, string[] cliArgs) + { + if (GetOverrideValue(key, cliArgs, out string? overrideValue, out ValueSource? valueSource)) + { + if (!Money.TryParse(overrideValue, out var money)) + { + throw new ArgumentNullException("DustThreshold", "Not a valid money"); + } + + return new MoneyValue(value, money, valueSource.Value); + } + + return new MoneyValue(value, value, ValueSource.Disk); + } + + private NetworkValue GetNetworkValue(string key, string value, string[] cliArgs) + { + StringValue stringValue = GetStringValue(key, value, cliArgs); + + return new NetworkValue( + Value: Network.GetNetwork(stringValue.Value) ?? throw new ArgumentException("Network", $"Unknown network '{stringValue.Value}'"), + EffectiveValue: Network.GetNetwork(stringValue.EffectiveValue) ?? throw new ArgumentException("Network", $"Unknown network '{stringValue.EffectiveValue}'"), + stringValue.ValueSource); + } + + private BoolValue GetBoolValue(string key, bool value, string[] cliArgs) + { + if (GetOverrideValue(key, cliArgs, out string? overrideValue, out ValueSource? valueSource)) + { + if (!bool.TryParse(overrideValue, out bool argsBoolValue)) + { + throw new ArgumentException("must be 'true' or 'false'.", key); + } + + return new BoolValue(value, argsBoolValue, valueSource.Value); + } + + return new BoolValue(value, value, ValueSource.Disk); + } + + private static StringValue GetStringValue(string key, string value, string[] cliArgs) + { + if (GetOverrideValue(key, cliArgs, out string? overrideValue, out ValueSource? valueSource)) + { + return new StringValue(value, overrideValue, valueSource.Value); + } + + return new StringValue(value, value, ValueSource.Disk); + } + + private static NullableStringValue GetNullableStringValue(string key, string? value, string[] cliArgs) + { + if (GetOverrideValue(key, cliArgs, out string? overrideValue, out ValueSource? valueSource)) + { + return new NullableStringValue(value, overrideValue, valueSource.Value); + } + + return new NullableStringValue(value, value, ValueSource.Disk); + } + + private static StringArrayValue GetStringArrayValue(string key, string[] arrayValues, string[] cliArgs) + { + if (GetOverrideValue(key, cliArgs, out string? overrideValue, out ValueSource? valueSource)) + { + return new StringArrayValue(arrayValues, new string[] { overrideValue }, valueSource.Value); + } + + return new StringArrayValue(arrayValues, arrayValues, ValueSource.Disk); + } + + private static bool GetOverrideValue(string key, string[] cliArgs, [NotNullWhen(true)] out string? overrideValue, [NotNullWhen(true)] out ValueSource? valueSource) + { + // CLI arguments have higher precedence than environment variables. + if (GetCliArgsValue(key, cliArgs, out string? argsValue)) + { + valueSource = ValueSource.CommandLineArgument; + overrideValue = argsValue; + return true; + } + + if (GetEnvironmentVariable(key, out string? envVarValue)) + { + valueSource = ValueSource.EnvironmentVariable; + overrideValue = envVarValue; + return true; + } + + valueSource = null; + overrideValue = null; + return false; + } + + private static bool GetCliArgsValue(string key, string[] cliArgs, [NotNullWhen(true)] out string? cliArgsValue) + { + if (ArgumentHelpers.TryGetValue(key, cliArgs, out cliArgsValue)) + { + return true; + } + + cliArgsValue = null; + return false; + } + + private static bool GetEnvironmentVariable(string key, [NotNullWhen(true)] out string? envValue) + { + string envKey = $"WASABI-{key.ToUpperInvariant()}"; + + if (EnvironmentVariables.Contains(envKey)) + { + if (EnvironmentVariables[envKey] is string envVar) + { + envValue = envVar; + return true; + } + } + + envValue = null; + return false; + } + + private TValue GetEffectiveValue(string key) where TStorage : ITypedValue + { + if (Data.TryGetValue(key, out (string, IValue value) valueObject) && valueObject.value is ITypedValue typedValue) + { + return typedValue.EffectiveValue; + } + + throw new InvalidOperationException($"Failed to find key '{key}' in config storage."); + } + + /// Source of application config value. + private enum ValueSource + { + /// Value stored in JSON config on disk. + Disk, + + /// CLI argument passed by user to override disk config value. + CommandLineArgument, + + /// Environment variable overriding disk config value. + EnvironmentVariable + } + + private interface IValue + { + ValueSource ValueSource { get; } + bool Overridden => ValueSource != ValueSource.Disk; + } + + private interface ITypedValue : IValue + { + T Value { get; } + T EffectiveValue { get; } + } + + private record BoolValue(bool Value, bool EffectiveValue, ValueSource ValueSource) : ITypedValue; + private record StringValue(string Value, string EffectiveValue, ValueSource ValueSource) : ITypedValue; + private record NullableStringValue(string? Value, string? EffectiveValue, ValueSource ValueSource) : ITypedValue; + private record StringArrayValue(string[] Value, string[] EffectiveValue, ValueSource ValueSource) : ITypedValue; + private record NetworkValue(Network Value, Network EffectiveValue, ValueSource ValueSource) : ITypedValue; + private record MoneyValue(Money Value, Money EffectiveValue, ValueSource ValueSource) : ITypedValue; + private record EndPointValue(EndPoint Value, EndPoint EffectiveValue, ValueSource ValueSource) : ITypedValue; +} diff --git a/WalletWasabi.Fluent/Global.cs b/WalletWasabi.Daemon/Global.cs similarity index 72% rename from WalletWasabi.Fluent/Global.cs rename to WalletWasabi.Daemon/Global.cs index 61834740c4..5843f5c1dd 100644 --- a/WalletWasabi.Fluent/Global.cs +++ b/WalletWasabi.Daemon/Global.cs @@ -1,7 +1,9 @@ using Microsoft.Extensions.Caching.Memory; using NBitcoin; using Nito.AsyncEx; +using System; using System.IO; +using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; @@ -11,6 +13,7 @@ using WalletWasabi.BitcoinCore.Monitoring; using WalletWasabi.BitcoinP2p; using WalletWasabi.Blockchain.Analysis.FeesEstimation; +using WalletWasabi.Blockchain.BlockFilters; using WalletWasabi.Blockchain.Blocks; using WalletWasabi.Blockchain.Mempool; using WalletWasabi.Blockchain.TransactionBroadcasting; @@ -26,29 +29,27 @@ using WalletWasabi.Tor.Socks5.Pool.Circuits; using WalletWasabi.Tor.StatusChecker; using WalletWasabi.WabiSabi.Client; +using WalletWasabi.WabiSabi.Client.Banning; using WalletWasabi.WabiSabi.Client.RoundStateAwaiters; using WalletWasabi.Wallets; using WalletWasabi.WebClients.BlockstreamInfo; using WalletWasabi.WebClients.Wasabi; -using WalletWasabi.Blockchain.BlockFilters; -using WalletWasabi.Fluent.Helpers; -namespace WalletWasabi.Fluent; +namespace WalletWasabi.Daemon; public class Global { /// Use this variable as a guard to prevent touching that might have already been disposed. private volatile bool _disposeRequested; - public Global(string dataDir, Config config, UiConfig uiConfig, WalletManager walletManager) + public Global(string dataDir, string configFilePath, Config config) { DataDir = dataDir; + ConfigFilePath = configFilePath; Config = config; - UiConfig = uiConfig; TorSettings = new TorSettings(DataDir, distributionFolderPath: EnvironmentHelpers.GetFullBaseDirectory(), Config.TerminateTorOnExit, Environment.ProcessId); HostedServices = new HostedServices(); - WalletManager = walletManager; var networkWorkFolderPath = Path.Combine(DataDir, "BitcoinStore", Network.ToString()); AllTransactionStore = new AllTransactionStore(networkWorkFolderPath, Network); @@ -57,7 +58,7 @@ public Global(string dataDir, Config config, UiConfig uiConfig, WalletManager wa var mempoolService = new MempoolService(); var blocks = new FileSystemBlockRepository(Path.Combine(networkWorkFolderPath, "Blocks"), Network); - BitcoinStore = new BitcoinStore(IndexStore, AllTransactionStore, mempoolService, blocks); + BitcoinStore = new BitcoinStore(IndexStore, AllTransactionStore, mempoolService, smartHeaderChain, blocks); HttpClientFactory = BuildHttpClientFactory(() => Config.GetBackendUri()); CoordinatorHttpClientFactory = BuildHttpClientFactory(() => Config.GetCoordinatorUri()); @@ -66,9 +67,7 @@ public Global(string dataDir, Config config, UiConfig uiConfig, WalletManager wa Synchronizer = new WasabiSynchronizer(requestInterval, maxFiltersToSync, BitcoinStore, HttpClientFactory); LegalChecker = new(DataDir); UpdateManager = new(DataDir, Config.DownloadNewVersion, HttpClientFactory.NewHttpClient(Mode.DefaultCircuit)); - TransactionBroadcaster = new TransactionBroadcaster(Network, BitcoinStore, HttpClientFactory, WalletManager); TorStatusChecker = new TorStatusChecker(TimeSpan.FromHours(6), HttpClientFactory.NewHttpClient(Mode.DefaultCircuit), new XmlIssueListParser()); - RoundStateUpdaterCircuit = new PersonCircuit(); Cache = new MemoryCache(new MemoryCacheOptions @@ -76,6 +75,42 @@ public Global(string dataDir, Config config, UiConfig uiConfig, WalletManager wa SizeLimit = 1_000, ExpirationScanFrequency = TimeSpan.FromSeconds(30) }); + + // Register P2P network. + HostedServices.Register( + () => + { + var p2p = new P2pNetwork( + Network, + Config.GetBitcoinP2pEndPoint(), + Config.UseTor ? TorSettings.SocksEndpoint : null, + Path.Combine(DataDir, "BitcoinP2pNetwork"), + BitcoinStore); + if (!Config.BlockOnlyMode) + { + p2p.Nodes.NodeConnectionParameters.TemplateBehaviors.Add(BitcoinStore.CreateUntrustedP2pBehavior()); + } + + return p2p; + }, + friendlyName: "Bitcoin P2P Network"); + + RegisterFeeRateProviders(); + + // Block providers. + SpecificNodeBlockProvider = new SpecificNodeBlockProvider(Network, Config.ServiceConfiguration, HttpClientFactory.TorEndpoint); + var blockProvider = new SmartBlockProvider( + BitcoinStore.BlockRepository, + BitcoinCoreNode?.RpcClient is null ? null : new RpcBlockProvider(BitcoinCoreNode.RpcClient), + SpecificNodeBlockProvider, + new P2PBlockProvider(Network, HostedServices.Get().Nodes, HttpClientFactory.IsTorEnabled), + Cache); + + WalletManager = new WalletManager(config.Network, DataDir, new WalletDirectories(Config.Network, DataDir), BitcoinStore, Synchronizer, HostedServices.Get(), blockProvider, config.ServiceConfiguration); + TransactionBroadcaster = new TransactionBroadcaster(Network, BitcoinStore, HttpClientFactory, WalletManager); + + CoinPrison = CoinPrison.CreateOrLoadFromFile(DataDir); + WalletManager.WalletStateChanged += WalletManager_WalletStateChanged; } public const string ThemeBackgroundBrushResourceKey = "ThemeBackgroundBrush"; @@ -92,41 +127,48 @@ public Global(string dataDir, Config config, UiConfig uiConfig, WalletManager wa public BitcoinStore BitcoinStore { get; } /// HTTP client factory for sending HTTP requests. - public HttpClientFactory HttpClientFactory { get; } + public WasabiHttpClientFactory HttpClientFactory { get; } - public HttpClientFactory CoordinatorHttpClientFactory { get; } + public WasabiHttpClientFactory CoordinatorHttpClientFactory { get; } public LegalChecker LegalChecker { get; private set; } + public string ConfigFilePath { get; } public Config Config { get; } public WasabiSynchronizer Synchronizer { get; private set; } public WalletManager WalletManager { get; } public TransactionBroadcaster TransactionBroadcaster { get; set; } public CoinJoinProcessor? CoinJoinProcessor { get; set; } - private SpecificNodeBlockProvider? SpecificNodeBlockProvider { get; set; } + private SpecificNodeBlockProvider SpecificNodeBlockProvider { get; } private TorProcessManager? TorManager { get; set; } public CoreNode? BitcoinCoreNode { get; private set; } public TorStatusChecker TorStatusChecker { get; set; } - public UpdateManager UpdateManager { get; set; } + public UpdateManager UpdateManager { get; } public HostedServices HostedServices { get; } - public UiConfig UiConfig { get; } - public Network Network => Config.Network; public MemoryCache Cache { get; private set; } - + public CoinPrison CoinPrison { get; } public JsonRpcServer? RpcServer { get; private set; } + + public Uri? OnionServiceUri { get; private set; } + private PersonCircuit RoundStateUpdaterCircuit { get; } private AllTransactionStore AllTransactionStore { get; } private IndexStore IndexStore { get; } - private HttpClientFactory BuildHttpClientFactory(Func backendUriGetter) => - new(torEndPoint: Config.UseTor ? TorSettings.SocksEndpoint : null, backendUriGetter); + private WasabiHttpClientFactory BuildHttpClientFactory(Func backendUriGetter) => + new( + Config.UseTor ? TorSettings.SocksEndpoint : null, + backendUriGetter); - public async Task InitializeNoWalletAsync(TerminateService terminateService) + public async Task InitializeNoWalletAsync(TerminateService terminateService, CancellationToken cancellationToken) { + using CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, StoppingCts.Token); + CancellationToken cancel = linkedCts.Token; + // StoppingCts may be disposed at this point, so do not forward the cancellation token here. - using (await InitializationAsyncLock.LockAsync()) + using (await InitializationAsyncLock.LockAsync(cancellationToken)) { Logger.LogTrace("Initialization started."); @@ -135,14 +177,9 @@ public async Task InitializeNoWalletAsync(TerminateService terminateService) return; } - CancellationToken cancel = StoppingCts.Token; - try { - // Make sure that wallet startup set correctly regarding RunOnSystemStartup - await StartupHelper.ModifyStartupSettingAsync(UiConfig.RunOnSystemStartup).ConfigureAwait(false); - - var bstoreInitTask = BitcoinStore.InitializeAsync(cancel); + var bitcoinStoreInitTask = BitcoinStore.InitializeAsync(cancel); HostedServices.Register(() => new UpdateChecker(TimeSpan.FromMinutes(7), Synchronizer), "Software Update Checker"); var updateChecker = HostedServices.Get(); @@ -156,10 +193,13 @@ public async Task InitializeNoWalletAsync(TerminateService terminateService) try { - await bstoreInitTask.ConfigureAwait(false); + await bitcoinStoreInitTask.ConfigureAwait(false); + + // Make sure that TurboSyncHeight is not higher than BestHeight + WalletManager.EnsureTurboSyncHeightConsistency(); // Make sure that the height of the wallets will not be better than the current height of the filters. - WalletManager.SetMaxBestHeight(BitcoinStore.IndexStore.SmartHeaderChain.TipHeight); + WalletManager.SetMaxBestHeight(BitcoinStore.SmartHeaderChain.TipHeight); } catch (Exception ex) when (ex is not OperationCanceledException) { @@ -168,11 +208,8 @@ public async Task InitializeNoWalletAsync(TerminateService terminateService) throw; } - HostedServices.Register(() => new P2pNetwork(Network, Config.GetBitcoinP2pEndPoint(), Config.UseTor ? TorSettings.SocksEndpoint : null, Path.Combine(DataDir, "BitcoinP2pNetwork"), BitcoinStore), "Bitcoin P2P Network"); - await StartLocalBitcoinNodeAsync(cancel).ConfigureAwait(false); - RegisterFeeRateProviders(); RegisterCoinJoinComponents(); SleepInhibitor? sleepInhibitor = await SleepInhibitor.CreateAsync(HostedServices.Get()).ConfigureAwait(false); @@ -195,17 +232,7 @@ public async Task InitializeNoWalletAsync(TerminateService terminateService) await StartRpcServerAsync(terminateService, cancel).ConfigureAwait(false); - // TODO: Should this be null for RegTest? - SpecificNodeBlockProvider = new SpecificNodeBlockProvider(Network, Config.ServiceConfiguration, HttpClientFactory.TorEndpoint); - - var blockProvider = new SmartBlockProvider( - BitcoinStore.BlockRepository, - BitcoinCoreNode?.RpcClient is null ? null : new RpcBlockProvider(BitcoinCoreNode.RpcClient), - SpecificNodeBlockProvider, - new P2PBlockProvider(Network, HostedServices.Get().Nodes, HttpClientFactory.IsTorEnabled), - Cache); - - WalletManager.RegisterServices(BitcoinStore, Synchronizer, Config.ServiceConfiguration, HostedServices.Get(), blockProvider); + WalletManager.Initialize(); } finally { @@ -216,11 +243,17 @@ public async Task InitializeNoWalletAsync(TerminateService terminateService) private async Task StartRpcServerAsync(TerminateService terminateService, CancellationToken cancel) { - var jsonRpcServerConfig = new JsonRpcServerConfiguration(Config.JsonRpcServerEnabled, Config.JsonRpcUser, Config.JsonRpcPassword, Config.JsonRpcServerPrefixes); + // HttpListener doesn't support onion services as prefix and for that reason we have no alternative + // other than using + var prefixes = OnionServiceUri is { } + ? Config.JsonRpcServerPrefixes.Append($"http://+:37129/").ToArray() + : Config.JsonRpcServerPrefixes; + + var jsonRpcServerConfig = new JsonRpcServerConfiguration(Config.JsonRpcServerEnabled, Config.JsonRpcUser, Config.JsonRpcPassword, prefixes); if (jsonRpcServerConfig.IsEnabled) { - var wasabiJsonRpcService = new Rpc.WasabiJsonRpcService(this, terminateService); - RpcServer = new JsonRpcServer(wasabiJsonRpcService, jsonRpcServerConfig); + var wasabiJsonRpcService = new Rpc.WasabiJsonRpcService(global: this); + RpcServer = new JsonRpcServer(wasabiJsonRpcService, jsonRpcServerConfig, terminateService); try { await RpcServer.StartAsync(cancel).ConfigureAwait(false); @@ -242,6 +275,22 @@ private async Task StartTorProcessManagerAsync(CancellationToken cancellationTok TorManager = new TorProcessManager(TorSettings); await TorManager.StartAsync(attempts: 3, cancellationToken).ConfigureAwait(false); Logger.LogInfo($"{nameof(TorProcessManager)} is initialized."); + + var (_, torControlClient) = await TorManager.WaitForNextAttemptAsync(cancellationToken).ConfigureAwait(false); + if (Config is { JsonRpcServerEnabled: true, RpcOnionEnabled: true } && torControlClient is { } nonNullTorControlClient) + { + var anonymousAccessAllowed = string.IsNullOrEmpty(Config.JsonRpcUser) || string.IsNullOrEmpty(Config.JsonRpcPassword); + if (!anonymousAccessAllowed) + { + var onionServiceId = await nonNullTorControlClient.CreateOnionServiceAsync(TorSettings.RpcVirtualPort, TorSettings.RpcOnionPort, cancellationToken).ConfigureAwait(false); + OnionServiceUri = new Uri($"http://{onionServiceId}.onion"); + Logger.LogInfo($"RPC server listening on {OnionServiceUri}"); + } + else + { + Logger.LogInfo("Anonymous access RPC server cannot be exposed as onion service."); + } + } } HostedServices.Register(() => new TorMonitor(period: TimeSpan.FromMinutes(1), torProcessManager: TorManager, httpClientFactory: HttpClientFactory), nameof(TorMonitor)); @@ -288,7 +337,12 @@ private void RegisterLocalNodeDependentComponents(CoreNode coreNode) HostedServices.Register(() => new BlockNotifier(TimeSpan.FromSeconds(7), coreNode.RpcClient, coreNode.P2pNode), "Block Notifier"); HostedServices.Register(() => new RpcMonitor(TimeSpan.FromSeconds(7), coreNode.RpcClient), "RPC Monitor"); HostedServices.Register(() => new RpcFeeProvider(TimeSpan.FromMinutes(1), coreNode.RpcClient, HostedServices.Get()), "RPC Fee Provider"); - HostedServices.Register(() => new MempoolMirror(TimeSpan.FromSeconds(21), coreNode.RpcClient, coreNode.P2pNode), "Full Node Mempool Mirror"); + if (!Config.BlockOnlyMode) + { + HostedServices.Register( + () => new MempoolMirror(TimeSpan.FromSeconds(21), coreNode.RpcClient, coreNode.P2pNode), + "Full Node Mempool Mirror"); + } } private void RegisterFeeRateProviders() @@ -302,7 +356,20 @@ private void RegisterCoinJoinComponents() { Tor.Http.IHttpClient roundStateUpdaterHttpClient = CoordinatorHttpClientFactory.NewHttpClient(Mode.SingleCircuitPerLifetime, RoundStateUpdaterCircuit); HostedServices.Register(() => new RoundStateUpdater(TimeSpan.FromSeconds(10), new WabiSabiHttpApiClient(roundStateUpdaterHttpClient)), "Round info updater"); - HostedServices.Register(() => new CoinJoinManager(WalletManager, HostedServices.Get(), CoordinatorHttpClientFactory, Synchronizer, Config.CoordinatorIdentifier), "CoinJoin Manager"); + HostedServices.Register(() => new CoinJoinManager(WalletManager, HostedServices.Get(), CoordinatorHttpClientFactory, Synchronizer, Config.CoordinatorIdentifier, CoinPrison), "CoinJoin Manager"); + } + + private void WalletManager_WalletStateChanged(object? sender, WalletState e) + { + // Load banned coins in wallet. + // This event function can be deleted later when SmartCoin.IsBanned is removed. + if (e is not WalletState.Started) + { + return; + } + + var wallet = sender as Wallet ?? throw new InvalidOperationException($"The sender for {nameof(WalletManager.WalletStateChanged)} was not a Wallet."); + CoinPrison.UpdateWallet(wallet); } public async Task DisposeAsync() @@ -328,6 +395,7 @@ public async Task DisposeAsync() { using var dequeueCts = new CancellationTokenSource(TimeSpan.FromMinutes(6)); await WalletManager.RemoveAndStopAllAsync(dequeueCts.Token).ConfigureAwait(false); + WalletManager.WalletStateChanged -= WalletManager_WalletStateChanged; Logger.LogInfo($"{nameof(WalletManager)} is stopped.", nameof(Global)); } catch (Exception ex) @@ -335,11 +403,8 @@ public async Task DisposeAsync() Logger.LogError($"Error during {nameof(WalletManager.RemoveAndStopAllAsync)}: {ex}"); } - if (UpdateManager is { } updateManager) - { - UpdateManager.Dispose(); - Logger.LogInfo($"{nameof(UpdateManager)} is stopped.", nameof(Global)); - } + UpdateManager.Dispose(); + Logger.LogInfo($"{nameof(UpdateManager)} is stopped."); if (RpcServer is { } rpcServer) { @@ -412,6 +477,25 @@ public async Task DisposeAsync() if (TorManager is { } torManager) { + using CancellationTokenSource cts = new(TimeSpan.FromSeconds(5)); + var (_, torControlClient) = await TorManager.WaitForNextAttemptAsync(cts.Token).ConfigureAwait(false); + if (OnionServiceUri is { } nonNullOnionServiceUri && torControlClient is { } nonNullTorControlClient) + { + try + { + var isDestroyedSuccessfully = await nonNullTorControlClient + .DestroyOnionServiceAsync(nonNullOnionServiceUri.Host, cts.Token).ConfigureAwait(false); + if (!isDestroyedSuccessfully) + { + Logger.LogInfo($"Onion service '{nonNullOnionServiceUri.Host}' failed to be destroyed."); + } + } + catch (OperationCanceledException) + { + Logger.LogInfo($"'{nonNullOnionServiceUri.Host}' failed to be stopped in allotted time."); + } + } + await torManager.DisposeAsync().ConfigureAwait(false); Logger.LogInfo($"{nameof(TorManager)} is stopped."); } diff --git a/WalletWasabi.Daemon/PersistentConfig.cs b/WalletWasabi.Daemon/PersistentConfig.cs new file mode 100644 index 0000000000..d0217a380f --- /dev/null +++ b/WalletWasabi.Daemon/PersistentConfig.cs @@ -0,0 +1,144 @@ +using NBitcoin; +using Newtonsoft.Json; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Net; +using WalletWasabi.Exceptions; +using WalletWasabi.Helpers; +using WalletWasabi.Interfaces; +using WalletWasabi.JsonConverters; +using WalletWasabi.JsonConverters.Bitcoin; + +namespace WalletWasabi.Daemon; + +[JsonObject(MemberSerialization.OptIn)] +public record PersistentConfig : IConfigNg +{ + public const int DefaultJsonRpcServerPort = 37128; + public static readonly Money DefaultDustThreshold = Money.Coins(Constants.DefaultDustThreshold); + + [JsonProperty(PropertyName = "Network")] + [JsonConverter(typeof(NetworkJsonConverter))] + public Network Network { get; set; } = Network.Main; + + [DefaultValue(Constants.BackendUri)] + [JsonProperty(PropertyName = "MainNetBackendUri", DefaultValueHandling = DefaultValueHandling.Populate)] + public string MainNetBackendUri { get; init; } = Constants.BackendUri; + + [DefaultValue(Constants.TestnetBackendUri)] + [JsonProperty(PropertyName = "TestNetClearnetBackendUri", DefaultValueHandling = DefaultValueHandling.Populate)] + public string TestNetBackendUri { get; init; } = Constants.TestnetBackendUri; + + [DefaultValue("http://localhost:37127/")] + [JsonProperty(PropertyName = "RegTestBackendUri", DefaultValueHandling = DefaultValueHandling.Populate)] + public string RegTestBackendUri { get; init; } = "http://localhost:37127/"; + + [JsonProperty(PropertyName = "MainNetCoordinatorUri", DefaultValueHandling = DefaultValueHandling.Ignore)] + public string? MainNetCoordinatorUri { get; init; } + + [JsonProperty(PropertyName = "TestNetCoordinatorUri", DefaultValueHandling = DefaultValueHandling.Ignore)] + public string? TestNetCoordinatorUri { get; init; } + + [JsonProperty(PropertyName = "RegTestCoordinatorUri", DefaultValueHandling = DefaultValueHandling.Ignore)] + public string? RegTestCoordinatorUri { get; init; } + + [DefaultValue(true)] + [JsonProperty(PropertyName = "UseTor", DefaultValueHandling = DefaultValueHandling.Populate)] + public bool UseTor { get; init; } = true; + + [DefaultValue(false)] + [JsonProperty(PropertyName = "TerminateTorOnExit", DefaultValueHandling = DefaultValueHandling.Populate)] + public bool TerminateTorOnExit { get; init; } = false; + + [DefaultValue(true)] + [JsonProperty(PropertyName = "DownloadNewVersion", DefaultValueHandling = DefaultValueHandling.Populate)] + public bool DownloadNewVersion { get; init; } = true; + + [DefaultValue(false)] + [JsonProperty(PropertyName = "StartLocalBitcoinCoreOnStartup", DefaultValueHandling = DefaultValueHandling.Populate)] + public bool StartLocalBitcoinCoreOnStartup { get; init; } = false; + + [DefaultValue(true)] + [JsonProperty(PropertyName = "StopLocalBitcoinCoreOnShutdown", DefaultValueHandling = DefaultValueHandling.Populate)] + public bool StopLocalBitcoinCoreOnShutdown { get; init; } = true; + + [JsonProperty(PropertyName = "LocalBitcoinCoreDataDir")] + public string LocalBitcoinCoreDataDir { get; init; } = EnvironmentHelpers.GetDefaultBitcoinCoreDataDirOrEmptyString(); + + [JsonProperty(PropertyName = "MainNetBitcoinP2pEndPoint")] + [JsonConverter(typeof(EndPointJsonConverter), Constants.DefaultMainNetBitcoinP2pPort)] + public EndPoint MainNetBitcoinP2pEndPoint { get; init; } = new IPEndPoint(IPAddress.Loopback, Constants.DefaultMainNetBitcoinP2pPort); + + [JsonProperty(PropertyName = "TestNetBitcoinP2pEndPoint")] + [JsonConverter(typeof(EndPointJsonConverter), Constants.DefaultTestNetBitcoinP2pPort)] + public EndPoint TestNetBitcoinP2pEndPoint { get; init; } = new IPEndPoint(IPAddress.Loopback, Constants.DefaultTestNetBitcoinP2pPort); + + [JsonProperty(PropertyName = "RegTestBitcoinP2pEndPoint")] + [JsonConverter(typeof(EndPointJsonConverter), Constants.DefaultRegTestBitcoinP2pPort)] + public EndPoint RegTestBitcoinP2pEndPoint { get; init; } = new IPEndPoint(IPAddress.Loopback, Constants.DefaultRegTestBitcoinP2pPort); + + [DefaultValue(false)] + [JsonProperty(PropertyName = "JsonRpcServerEnabled", DefaultValueHandling = DefaultValueHandling.Populate)] + public bool JsonRpcServerEnabled { get; init; } + + [DefaultValue("")] + [JsonProperty(PropertyName = "JsonRpcUser", DefaultValueHandling = DefaultValueHandling.Populate)] + public string JsonRpcUser { get; init; } = ""; + + [DefaultValue("")] + [JsonProperty(PropertyName = "JsonRpcPassword", DefaultValueHandling = DefaultValueHandling.Populate)] + public string JsonRpcPassword { get; init; } = ""; + + [JsonProperty(PropertyName = "JsonRpcServerPrefixes")] + public string[] JsonRpcServerPrefixes { get; init; } = new[] + { + "http://127.0.0.1:37128/", + "http://localhost:37128/" + }; + + [JsonProperty(PropertyName = "DustThreshold")] + [JsonConverter(typeof(MoneyBtcJsonConverter))] + public Money DustThreshold { get; init; } = DefaultDustThreshold; + + [JsonProperty(PropertyName = "EnableGpu")] + public bool EnableGpu { get; init; } = true; + + [DefaultValue("CoinJoinCoordinatorIdentifier")] + [JsonProperty(PropertyName = "CoordinatorIdentifier", DefaultValueHandling = DefaultValueHandling.Populate)] + public string CoordinatorIdentifier { get; init; } = "CoinJoinCoordinatorIdentifier"; + + public EndPoint GetBitcoinP2pEndPoint() + { + if (Network == Network.Main) + { + return MainNetBitcoinP2pEndPoint; + } + if (Network == Network.TestNet) + { + return TestNetBitcoinP2pEndPoint; + } + if (Network == Network.RegTest) + { + return RegTestBitcoinP2pEndPoint; + } + throw new NotSupportedNetworkException(Network); + } + + public bool MigrateOldDefaultBackendUris([NotNullWhenAttribute(true)] out PersistentConfig? newConfig) + { + bool hasChanged = false; + newConfig = null; + + if (MainNetBackendUri == "https://wasabiwallet.io/" || TestNetBackendUri == "https://wasabiwallet.co/") + { + hasChanged = true; + newConfig = this with + { + MainNetBackendUri = "https://api.wasabiwallet.io/", + TestNetBackendUri = "https://api.wasabiwallet.co/", + }; + } + + return hasChanged; + } +} diff --git a/WalletWasabi.Daemon/Program.cs b/WalletWasabi.Daemon/Program.cs new file mode 100644 index 0000000000..1c6a0d428c --- /dev/null +++ b/WalletWasabi.Daemon/Program.cs @@ -0,0 +1,53 @@ +using System; +using WalletWasabi.Helpers; +using WalletWasabi.Logging; +using WalletWasabi.Services; +using WalletWasabi.Services.Terminate; +using WalletWasabi.Wallets; +using System.Net.Sockets; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using NBitcoin; +using LogLevel = WalletWasabi.Logging.LogLevel; + +namespace WalletWasabi.Daemon; + +public class Program +{ + public static async Task Main(string[] args) + { + var app = WasabiAppBuilder + .Create("Wasabi Daemon", args) + .EnsureSingleInstance() + .OnUnhandledExceptions(LogUnhandledException) + .OnUnobservedTaskExceptions(LogUnobservedTaskException) + .Build(); + + var exitCode = await app.RunAsConsoleAsync().ConfigureAwait(false); + return (int)exitCode; + } + + private static void LogUnobservedTaskException(object? sender, AggregateException e) + { + ReadOnlyCollection innerExceptions = e.Flatten().InnerExceptions; + + switch (innerExceptions) + { + case [SocketException { SocketErrorCode: SocketError.OperationAborted }]: + // Source of this exception is NBitcoin library. + case [OperationCanceledException { Message: "The peer has been disconnected" }]: + // Until https://github.com/MetacoSA/NBitcoin/pull/1089 is resolved. + Logger.LogTrace(e); + break; + default: + Logger.LogDebug(e); + break; + } + } + + private static void LogUnhandledException(object? sender, Exception e) => + Logger.LogWarning(e); +} diff --git a/WalletWasabi.Daemon/README.md b/WalletWasabi.Daemon/README.md new file mode 100644 index 0000000000..7a9eaa9f5e --- /dev/null +++ b/WalletWasabi.Daemon/README.md @@ -0,0 +1,69 @@ +Wasabi Daemon +============= + +Wasabi daemon is a _headless_ Wasabi wallet designed to minimize the usage of resources (CPU, GPU, Memory, Bandwidth) with the goal of +making it more suitable for running all the time in the background. + +## Configuration + +All configuration options available via `Config.json` file are also available as command line arguments and environment variables: + +### Command Line and Environment variables + +* Command line switches have the form `--switch_name=value` where _switch_name_ is the same name that is used in the config file (case insensitive). +* Environment variables have the form `WASABI-SWITCHNAME` where _SWITCHNAME_ is the same name that is used in the config file. + +A few examples: + +| Config file | Command line | Environment variable | +|-------------|--------------|----------------------| +| Network: "TestNet" | --network=testnet | WASABI-NETWORK=testnet | +| JsonRpcServerEnabled: true| --jsonrpcserverenabled=true | WASABI-JSONRPCSERVERENABLED=true | +| UseTor: true | --usetor=true | WASABI-USETOR=true | +| DustThreshold: "0.00005" | --dustthreshold=0.00005 | WASABI-DUSTTHRESHOLD=0.00005 | + +### Values precedence + +* **Values passed by command line arguments** have the highest precedence and override values in environment variables and those specified in config files. +* **Values stored in environment variables** have higher precedence than those in config file and lower precedence than the ones pass by command line. +* **Values stored in config file** have the lower precedence. + +### Special values + +There are a few special switches that are not present in the `Config.json` file and are only available using command line and/or variable environment: + +* **LogLevel** to specify the level of detail used during logging +* **DataDir** to specify the path to the directory used during runtime. +* **BlockOnly** to instruct wasabi to ignore p2p transactions +* **Wallet** to instruct wasabi to open a wallet automatically after started. + +### Examples + +Run Wasabi and connect to the testnet Bitcoin network with Tor disabled and accept JSON RPC calls. Store everything in `$HOME/temp/wasabi-1`. + +```bash +$ wasabi.daemon --usetor=false --datadir="$HOME/temp/wasabi-1" --network=testnet --jsonrpcserverenabled=true --blockonly=true +``` + +Run Wasabi Daemon and connect to the testnet Bitcoin network. + +```bash +$ WASABI-NETWORK=testnet wasabi.daemon +``` + +Run Wasabi and open two wallets: AliceWallet and BobWallet + +```bash +$ wasabi.daemon --wallet=AliceWallet --wallet=BobWallet +``` + +### Version + +```bash +$ wasabi.daemon --version +Wasabi Daemon 2.0.3.0 +``` + +### Usage + +To interact with the daemon, use the [RPC server](https://docs.wasabiwallet.io/using-wasabi/RPC.html) or the [wcli script](https://github.com/zkSNACKs/WalletWasabi/tree/master/Contrib/CLI). diff --git a/WalletWasabi.Daemon/Rpc/WasabiJsonRpcService.cs b/WalletWasabi.Daemon/Rpc/WasabiJsonRpcService.cs new file mode 100644 index 0000000000..b6c53eb1d3 --- /dev/null +++ b/WalletWasabi.Daemon/Rpc/WasabiJsonRpcService.cs @@ -0,0 +1,543 @@ +using NBitcoin; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using WalletWasabi.BitcoinP2p; +using WalletWasabi.Blockchain.Analysis.Clustering; +using WalletWasabi.Blockchain.Keys; +using WalletWasabi.Blockchain.TransactionBuilding; +using WalletWasabi.Blockchain.TransactionOutputs; +using WalletWasabi.Blockchain.Transactions; +using WalletWasabi.Extensions; +using WalletWasabi.Helpers; +using WalletWasabi.Models; +using WalletWasabi.Rpc; +using WalletWasabi.WabiSabi.Client; +using WalletWasabi.Wallets; +using JsonRpcResult = System.Collections.Generic.Dictionary; +using JsonRpcResultList = System.Collections.Immutable.ImmutableArray>; + +namespace WalletWasabi.Daemon.Rpc; + +[SuppressMessage("ReSharper", "CoVariantArrayConversion")] +[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] +[SuppressMessage("ReSharper", "UnusedMember.Global")] +public class WasabiJsonRpcService : IJsonRpcService +{ + public WasabiJsonRpcService(Global global) + { + Global = global; + } + + private Global Global { get; } + private Wallet? ActiveWallet { get; set; } + + [JsonRpcMethod("listunspentcoins")] + public JsonRpcResultList GetUnspentCoinList() + { + var activeWallet = Guard.NotNull(nameof(ActiveWallet), ActiveWallet); + + AssertWalletIsLoaded(); + var serverTipHeight = activeWallet.BitcoinStore.SmartHeaderChain.ServerTipHeight; + return activeWallet.Coins.Where(x => !x.IsSpent()).Select( + x => new JsonRpcResult + { + ["txid"] = x.TransactionId.ToString(), + ["index"] = x.Index, + ["amount"] = x.Amount.Satoshi, + ["anonymityScore"] = x.HdPubKey.AnonymitySet, + ["confirmed"] = x.Confirmed, + ["confirmations"] = x.Confirmed ? serverTipHeight - (uint)x.Height.Value + 1 : 0, + ["label"] = x.HdPubKey.Labels.ToString(), + ["keyPath"] = x.HdPubKey.FullKeyPath.ToString(), + ["address"] = x.HdPubKey.GetAddress(Global.Network).ToString(), + ["excludedFromCoinjoin"] = x.IsExcludedFromCoinJoin + }).ToImmutableArray(); + } + + [JsonRpcMethod("listcoins")] + public JsonRpcResultList GetCoinList() + { + var activeWallet = Guard.NotNull(nameof(ActiveWallet), ActiveWallet); + + AssertWalletIsLoaded(); + var serverTipHeight = activeWallet.BitcoinStore.SmartHeaderChain.ServerTipHeight; + if (activeWallet.Coins is not CoinsRegistry coinRegistry) + { + throw new ArgumentException($"{nameof(activeWallet.Coins)} was not {typeof(CoinsRegistry)}."); + } + return coinRegistry.AsAllCoinsView().Select( + x => new JsonRpcResult + { + ["txid"] = x.TransactionId.ToString(), + ["index"] = x.Index, + ["amount"] = x.Amount.Satoshi, + ["anonymityScore"] = x.HdPubKey.AnonymitySet, + ["confirmed"] = x.Confirmed, + ["confirmations"] = x.Confirmed ? serverTipHeight - (uint)x.Height.Value + 1 : 0, + ["keyPath"] = x.HdPubKey.FullKeyPath.ToString(), + ["address"] = x.HdPubKey.GetAddress(Global.Network).ToString(), + ["spentBy"] = x.SpenderTransaction?.GetHash().ToString() + }).ToImmutableArray(); + } + + [JsonRpcMethod("createwallet", initializable: false)] + public object CreateWallet(string walletName, string password) + { + var walletGenerator = new WalletGenerator(Global.WalletManager.WalletDirectories.WalletsDir, Global.Network); + walletGenerator.TipHeight = Global.BitcoinStore.SmartHeaderChain.TipHeight; + var (keyManager, mnemonic) = walletGenerator.GenerateWallet(walletName, password); + Global.WalletManager.AddWallet(keyManager); + return mnemonic.ToString(); + } + + [JsonRpcMethod("recoverwallet", initializable: false)] + public void RecoverWallet(string walletName, string mnemonicStr, string password = "") + { + var walletGenerator = new WalletGenerator(Global.WalletManager.WalletDirectories.WalletsDir, Global.Network); + walletGenerator.TipHeight = 0; + if (!TryParseMnemonic(mnemonicStr, out var mnemonic)) + { + throw new ArgumentException("Invalid value for mnemonic"); + } + + var (keyManager, _) = walletGenerator.GenerateWallet(walletName, password, mnemonic); + Global.WalletManager.AddWallet(keyManager); + } + + [JsonRpcMethod("loadwallet", initializable: false)] + public void LoadWallet(string walletName) + { + SelectWallet(walletName); + } + + [JsonRpcMethod("getwalletinfo")] + public JsonRpcResult WalletInfo() + { + var activeWallet = Guard.NotNull(nameof(ActiveWallet), ActiveWallet); + + var km = activeWallet.KeyManager; + var segwit = new JsonRpcResult + { + ["name"] = "segwit", + ["publicKey"] = km.SegwitExtPubKey.ToString(Global.Network), + ["keyPath"] = $"m/{km.SegwitAccountKeyPath}" + }; + var info = new JsonRpcResult + { + ["walletName"] = activeWallet.WalletName, + ["walletFile"] = km.FilePath, + ["state"] = activeWallet.State.ToString(), + ["masterKeyFingerprint"] = km.MasterFingerprint?.ToString() ?? "", + ["anonScoreTarget"] = activeWallet.AnonScoreTarget, + ["isWatchOnly"] = activeWallet.KeyManager.IsWatchOnly, + ["isHardwareWallet"] = activeWallet.KeyManager.IsHardwareWallet, + ["isAutoCoinjoin"] = activeWallet.KeyManager.AutoCoinJoin, + ["isRedCoinIsolation"] = activeWallet.KeyManager.RedCoinIsolation, + ["accounts"] = new [] { segwit } + }; + + if (km.TaprootExtPubKey is { } taprootExtPubKey) + { + info["accounts"] = new[] + { + segwit, + new JsonRpcResult + { + ["name"] = "taproot", + ["publicKey"] = taprootExtPubKey.ToString(Global.Network), + ["keyPath"] = $"m/{km.TaprootAccountKeyPath}" + } + }; + } + + if (activeWallet.State == WalletState.Started) + { + // The following elements are valid only after the wallet is fully synchronized + info["balance"] = activeWallet.Coins + .Where(c => !c.IsSpent() && !c.SpentAccordingToBackend) + .Sum(c => c.Amount.Satoshi); + info["coinjoinStatus"] = GetCoinjoinStatus(activeWallet); + } + + return info; + } + + [JsonRpcMethod("getnewaddress")] + public JsonRpcResult GenerateReceiveAddress(string label) + { + AssertWalletIsLoaded(); + label = Guard.NotNullOrEmptyOrWhitespace(nameof(label), label, true); + var activeWallet = Guard.NotNull(nameof(ActiveWallet), ActiveWallet); + + var hdKey = activeWallet.KeyManager.GetNextReceiveKey(new LabelsArray(label)); + + return new JsonRpcResult + { + ["address"] = hdKey.GetAddress(Global.Network).ToString(), + ["keyPath"] = hdKey.FullKeyPath.ToString(), + ["label"] = hdKey.Labels.ToString(), + ["publicKey"] = hdKey.PubKey.ToHex(), + ["scriptPubKey"] = hdKey.GetAssumedScriptPubKey().ToHex() + }; + } + + [JsonRpcMethod("getstatus", initializable: false)] + public JsonRpcResult GetStatus() + { + var sync = Global.Synchronizer; + var smartHeaderChain = Global.BitcoinStore.SmartHeaderChain; + + return new JsonRpcResult + { + ["torStatus"] = sync.TorStatus switch + { + TorStatus.NotRunning => "Not running", + TorStatus.Running => "Running", + _ => "Turned off" + }, + ["onionService"] = Global.OnionServiceUri?.ToString() ?? "Unavailable", + ["backendStatus"] = sync.BackendStatus == BackendStatus.Connected ? "Connected" : "Disconnected", + ["bestBlockchainHeight"] = smartHeaderChain.TipHeight.ToString(), + ["bestBlockchainHash"] = smartHeaderChain.TipHash?.ToString() ?? "", + ["filtersCount"] = smartHeaderChain.HashCount, + ["filtersLeft"] = smartHeaderChain.HashesLeft, + ["network"] = Global.Network.Name, + ["exchangeRate"] = sync.UsdExchangeRate, + ["peers"] = Global.HostedServices.Get().Nodes.ConnectedNodes.Select( + x => new JsonRpcResult + { + ["isConnected"] = x.IsConnected, + ["lastSeen"] = x.LastSeen, + ["endpoint"] = x.Peer.Endpoint.ToString(), + ["userAgent"] = x.PeerVersion.UserAgent, + }).ToArray(), + }; + } + + [JsonRpcMethod("build")] + public string BuildTransaction(PaymentInfo[] payments, OutPoint[] coins, int? feeTarget = null, decimal? feeRate = null, string? password = null) + { + Guard.NotNull(nameof(payments), payments); + Guard.NotNull(nameof(coins), coins); + password = Guard.Correct(password); + + static bool InRange(IComparable val, T min, T max) => + val.CompareTo(min) >= 0 && val.CompareTo(max) <= 0; + + var satsPerByte = feeRate is { } nonNullSatsPerByte ? new FeeRate(nonNullSatsPerByte) : FeeRate.Zero; + + var feeStrategy = (feeRate, feeTarget) switch + { + (not null, null) when InRange(satsPerByte, Constants.MinRelayFeeRate, Constants.AbsurdlyHighFeeRate) => + FeeStrategy.CreateFromFeeRate(satsPerByte), + (null, { } argFeeTarget) when InRange(argFeeTarget, Constants.TwentyMinutesConfirmationTarget, Constants.SevenDaysConfirmationTarget) => + FeeStrategy.CreateFromConfirmationTarget(argFeeTarget), + _ => throw new ArgumentException("Fee parameters are missing, inconsistent or out of range.") + }; + AssertWalletIsLoaded(); + var payment = new PaymentIntent( + payments.Select( + p => + new DestinationRequest(p.Sendto.ScriptPubKey, MoneyRequest.Create(p.Amount, p.SubtractFee), new LabelsArray(p.Label)))); + var result = ActiveWallet!.BuildTransaction( + password, + payment, + feeStrategy, + allowUnconfirmed: true, + allowedInputs: coins); + var smartTx = result.Transaction; + + return smartTx.Transaction.ToHex(); + } + + [JsonRpcMethod("payincoinjoin")] + public string PayInCoinJoin(BitcoinAddress address, Money amount) + { + var activeWallet = Guard.NotNull(nameof(ActiveWallet), ActiveWallet); + AssertWalletIsLoaded(); + return activeWallet.AddCoinJoinPayment(address, amount); + } + + [JsonRpcMethod("send")] + public async Task SendTransactionAsync(PaymentInfo[] payments, OutPoint[] coins, int? feeTarget = null, int? feeRate = null, string? password = null) + { + password = Guard.Correct(password); + var txHex = BuildTransaction(payments, coins, feeTarget, feeRate, password); + var smartTx = new SmartTransaction(Transaction.Parse(txHex, Global.Network), Height.Mempool); + + await Global.TransactionBroadcaster.SendTransactionAsync(smartTx).ConfigureAwait(false); + return new JsonRpcResult + { + ["txid"] = smartTx.Transaction.GetHash(), + ["tx"] = txHex + }; + } + + [JsonRpcMethod("canceltransaction")] + public string BuildCancelTransaction(uint256 txId, string password = "") + { + Guard.NotNull(nameof(txId), txId); + var activeWallet = Guard.NotNull(nameof(ActiveWallet), ActiveWallet); + activeWallet.Kitchen.Cook(password); + var mempoolStore = Global.BitcoinStore.TransactionStore.MempoolStore; + if (!mempoolStore.TryGetTransaction(txId, out var smartTransactionToCancel)) + { + throw new NotSupportedException($"Unknown transaction {txId}"); + } + + var cancellationResult = activeWallet.CancelTransaction(smartTransactionToCancel); + var cancellationSmartTransaction = cancellationResult.Transaction; + return cancellationSmartTransaction.Transaction.ToHex(); + } + + [JsonRpcMethod("speeduptransaction")] + public string SpeedUpTransaction(uint256 txId, string password = "") + { + Guard.NotNull(nameof(txId), txId); + var activeWallet = Guard.NotNull(nameof(ActiveWallet), ActiveWallet); + activeWallet.Kitchen.Cook(password); + var mempoolStore = Global.BitcoinStore.TransactionStore.MempoolStore; + if (!mempoolStore.TryGetTransaction(txId, out var smartTransactionToSpeedUp)) + { + throw new NotSupportedException($"Unknown transaction {txId}"); + } + + var speedUpResult = activeWallet.SpeedUpTransaction(smartTransactionToSpeedUp); + var speedUpSmartTransaction = speedUpResult.Transaction; + return speedUpSmartTransaction.Transaction.ToHex(); + } + + [JsonRpcMethod("broadcast", initializable: false)] + public async Task SendRawTransactionAsync(string txHex) + { + txHex = Guard.Correct(txHex); + var smartTx = new SmartTransaction(Transaction.Parse(txHex, Global.Network), Height.Mempool); + + await Global.TransactionBroadcaster.SendTransactionAsync(smartTx).ConfigureAwait(false); + return new JsonRpcResult + { + ["txid"] = smartTx.Transaction.GetHash() + }; + } + + [JsonRpcMethod("gethistory")] + public JsonRpcResultList GetHistory() + { + var activeWallet = Guard.NotNull(nameof(ActiveWallet), ActiveWallet); + + AssertWalletIsLoaded(); + var summary = activeWallet.BuildHistorySummary(); + return summary.Select( + x => new JsonRpcResult + { + ["datetime"] = x.FirstSeen, + ["height"] = x.Height.Value, + ["amount"] = x.Amount.Satoshi, + ["label"] = x.Labels.ToString(), + ["tx"] = x.GetHash(), + ["islikelycoinjoin"] = x.IsOwnCoinjoin() + }).ToImmutableArray(); + } + + [JsonRpcMethod("excludefromcoinjoin")] + public void ExcludeCoinsFromCoinjoin(uint256 transactionId, int n, bool exclude = true) + { + var activeWallet = Guard.NotNull(nameof(ActiveWallet), ActiveWallet); + + AssertWalletIsLoaded(); + + activeWallet.ExcludeCoinFromCoinJoin(new OutPoint(transactionId, n), exclude); + } + + [JsonRpcMethod("listkeys")] + public JsonRpcResultList GetAllKeys() + { + var activeWallet = Guard.NotNull(nameof(ActiveWallet), ActiveWallet); + + AssertWalletIsLoaded(); + var keys = activeWallet.KeyManager.GetKeys(); + return keys.Select( + x => new JsonRpcResult + { + ["fullKeyPath"] = x.FullKeyPath.ToString(), + ["internal"] = x.IsInternal, + ["keyState"] = x.KeyState, + ["label"] = x.Labels.ToString(), + ["scriptPubKey"] = x.GetAssumedScriptPubKey().ToString(), + ["pubkey"] = x.PubKey.ToString(), + ["pubKeyHash"] = x.PubKey.Hash.ToString(), + ["address"] = x.GetAddress(Global.Network).ToString() + }).ToImmutableArray(); + } + + [JsonRpcMethod("startcoinjoin")] + public void StartCoinJoining(string? password = null, bool stopWhenAllMixed = true, bool overridePlebStop = true) + { + var coinJoinManager = Global.HostedServices.Get(); + var activeWallet = Guard.NotNull(nameof(ActiveWallet), ActiveWallet); + + AssertWalletIsLoaded(); + AssertWalletIsLoggedIn(activeWallet, password ?? ""); + coinJoinManager.StartAsync(activeWallet, activeWallet, stopWhenAllMixed, overridePlebStop, CancellationToken.None).ConfigureAwait(false); + } + + [JsonRpcMethod("startcoinjoinsweep")] + public void StartCoinjoinSweeping(string? password = null, string? outputWalletName = null) + { + var activeWallet = Guard.NotNull(nameof(ActiveWallet), ActiveWallet); + + AssertWalletIsLoaded(); + AssertWalletIsLoggedIn(activeWallet, password ?? ""); + + if (outputWalletName is null || outputWalletName == activeWallet.WalletName) + { + throw new InvalidOperationException("Output wallet name is invalid."); + } + + var outputWallet = Global.WalletManager.GetWalletByName(outputWalletName); + + StartCoinjoinSweepAsync(activeWallet, outputWallet).ConfigureAwait(false); + } + + private async Task StartCoinjoinSweepAsync(Wallet activeWallet, Wallet outputWallet) + { + activeWallet.ConsolidationMode = true; + + // If output wallet isn't initialized, then load it. + if (outputWallet.State == WalletState.Uninitialized) + { + await Global.WalletManager.StartWalletAsync(outputWallet).ConfigureAwait(false); + } + + var coinJoinManager = Global.HostedServices.Get(); + await coinJoinManager.StartAsync(activeWallet, outputWallet, stopWhenAllMixed: false, overridePlebStop: true, CancellationToken.None).ConfigureAwait(false); + } + + [JsonRpcMethod("stopcoinjoin")] + public void StopCoinJoining() + { + var coinJoinManager = Global.HostedServices.Get(); + var activeWallet = Guard.NotNull(nameof(ActiveWallet), ActiveWallet); + + AssertWalletIsLoaded(); + + coinJoinManager.StopAsync(activeWallet, CancellationToken.None).ConfigureAwait(false); + } + + [JsonRpcMethod("getfeerates", initializable: false)] + public object GetFeeRate() + { + if (Global.Synchronizer.LastAllFeeEstimate is { } nonNullFeeRates) + { + return nonNullFeeRates.Estimations; + } + + return new Dictionary(); + } + + [JsonRpcMethod("listwallets", initializable: false)] + public async Task ListWalletsAsync() + { + var wallets = await Global.WalletManager.GetWalletsAsync().ConfigureAwait(false); + return wallets + .Cast() + .Select(x => new JsonRpcResult + { + ["walletName"] = x.WalletName + }) + .ToImmutableArray(); + } + + [JsonRpcMethod(IJsonRpcService.StopRpcCommand, initializable: false)] + public Task StopAsync() + { + throw new InvalidOperationException("This RPC method is special and the handling method should not be called."); + } + + private string GetCoinjoinStatus(Wallet wallet) + { + var coinJoinManager = Global.HostedServices.Get(); + var walletCoinjoinClientState = coinJoinManager.GetCoinjoinClientState(wallet.WalletName); + return walletCoinjoinClientState switch + { + CoinJoinClientState.Idle => "Idle", + CoinJoinClientState.InProgress => "In progress", + CoinJoinClientState.InSchedule => "In schedule", + CoinJoinClientState.InCriticalPhase => "In critical phase", + _ => throw new Exception($"The state {walletCoinjoinClientState.FriendlyName()} is unknown.") + }; + } + + private void SelectWallet(string walletName) + { + walletName = Guard.NotNullOrEmptyOrWhitespace(nameof(walletName), walletName); + try + { + var wallet = Global.WalletManager.GetWalletByName(walletName); + + ActiveWallet = wallet; + if (wallet.State == WalletState.Uninitialized) + { + Global.WalletManager.StartWalletAsync(wallet).ConfigureAwait(false); + } + } + catch (InvalidOperationException) // wallet not found + { + throw new Exception($"Wallet '{walletName}' not found."); + } + } + + private void AssertWalletIsLoaded() + { + if (ActiveWallet is null) + { + throw new InvalidOperationException("There is no wallet loaded."); + } + if (ActiveWallet.State < WalletState.Started) + { + throw new InvalidOperationException("Wallet is not fully loaded yet."); + } + } + + private void AssertWalletIsLoggedIn(Wallet activeWallet, string password) + { + if (!activeWallet.IsLoggedIn && !activeWallet.TryLogin(password, out _)) + { + throw new Exception($"'{activeWallet.WalletName}' wallet requires the password to start coinjoining."); + } + } + + [JsonRpcInitialization] + public void Initialize(string path, bool needsWallet) + { + var parts = path.Split("/", StringSplitOptions.RemoveEmptyEntries); + var walletName = parts.Length == 1 ? parts[0] : string.Empty; + if (needsWallet && !string.IsNullOrEmpty(walletName)) + { + SelectWallet(walletName); + } + else + { + throw new InvalidOperationException("Wallet name is invalid or not allowed."); + } + } + + private static bool TryParseMnemonic(string mnemonicStr, [NotNullWhen(true)] out Mnemonic? mnemonic) + { + try + { + mnemonic = new Mnemonic(mnemonicStr); + return true; + } + catch (Exception) + { + mnemonic = null; + return false; + } + } +} diff --git a/WalletWasabi.Daemon/WalletWasabi.Daemon.csproj b/WalletWasabi.Daemon/WalletWasabi.Daemon.csproj new file mode 100644 index 0000000000..7d09320069 --- /dev/null +++ b/WalletWasabi.Daemon/WalletWasabi.Daemon.csproj @@ -0,0 +1,41 @@ + + + + net7.0 + latest + latest + 1701;1702;1705;1591;1573;CA1031;CA1822 + enable + true + true + true + true + win7-x64;linux-x64;linux-arm64;osx-x64;osx-arm64 + $(MSBuildProjectDirectory)\=WalletWasabi.Daemon + Exe + + + + zkSNACKs Ltd + zkSNACKs Ltd + en-US + Wasabi Wallet Daemon + + Open-source, non-custodial, privacy focused Bitcoin wallet for Windows, Linux, and Mac. Built-in Tor, coinjoin, payjoin and coin control features. + + MIT + Wasabi Wallet Daemon + Wasabi Wallet Daemon + bitcoin-wallet;privacy;bitcoin;dotnet;nbitcoin;cross-platform;zerolink;wallet;wabisabi;coinjoin;tor + https://github.com/zkSNACKs/WalletWasabi/ + https://github.com/zkSNACKs/WalletWasabi/blob/master/LICENSE.md + git + https://github.com/zkSNACKs/WalletWasabi/ + Wasabi Wallet Fluent Daemon + + + + + + + diff --git a/WalletWasabi.Daemon/WasabiAppBuilder.cs b/WalletWasabi.Daemon/WasabiAppBuilder.cs new file mode 100644 index 0000000000..dcea14fbb4 --- /dev/null +++ b/WalletWasabi.Daemon/WasabiAppBuilder.cs @@ -0,0 +1,247 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using WalletWasabi.Extensions; +using WalletWasabi.Bases; +using WalletWasabi.Helpers; +using WalletWasabi.Logging; +using WalletWasabi.Services; +using WalletWasabi.Services.Terminate; +using Constants = WalletWasabi.Helpers.Constants; + +namespace WalletWasabi.Daemon; + +public enum ExitCode +{ + Ok, + FailedAlreadyRunningSignaled, + FailedAlreadyRunningError, +} + +public class WasabiApplication +{ + public WasabiAppBuilder AppConfig { get; } + public Global? Global { get; private set; } + public string ConfigFilePath { get; } + public Config Config { get; } + public SingleInstanceChecker SingleInstanceChecker { get; } + public TerminateService TerminateService { get; } + + public WasabiApplication(WasabiAppBuilder wasabiAppBuilder) + { + AppConfig = wasabiAppBuilder; + + ConfigFilePath = Path.Combine(Config.DataDir, "Config.json"); + Directory.CreateDirectory(Config.DataDir); + Config = new Config(LoadOrCreateConfigs(), wasabiAppBuilder.Arguments); + + SetupLogger(); + Logger.LogDebug($"Wasabi was started with these argument(s): {string.Join(" ", AppConfig.Arguments.DefaultIfEmpty("none"))}."); + SingleInstanceChecker = new(Config.Network); + TerminateService = new(TerminateApplicationAsync, AppConfig.Terminate); + } + + public async Task RunAsync(Func afterStarting) + { + if (AppConfig.Arguments.Contains("--version")) + { + Console.WriteLine($"{AppConfig.AppName} {Constants.ClientVersion}"); + return ExitCode.Ok; + } + if (AppConfig.Arguments.Contains("--help")) + { + ShowHelp(); + return ExitCode.Ok; + } + + if (AppConfig.MustCheckSingleInstance) + { + var instanceResult = await SingleInstanceChecker.CheckSingleInstanceAsync(); + if (instanceResult == WasabiInstanceStatus.AnotherInstanceIsRunning) + { + Logger.LogDebug("Wasabi is already running, signaled the first instance."); + return ExitCode.FailedAlreadyRunningSignaled; + } + if (instanceResult == WasabiInstanceStatus.Error) + { + Logger.LogCritical($"Wasabi is already running, but cannot be signaled"); + return ExitCode.FailedAlreadyRunningError; + } + } + + try + { + TerminateService.Activate(); + + BeforeStarting(); + + await afterStarting(); + return ExitCode.Ok; + } + finally + { + BeforeStopping(); + } + } + + private void BeforeStarting() + { + AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; + TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException; + + Logger.LogSoftwareStarted(AppConfig.AppName); + + Global = CreateGlobal(); + } + + private void BeforeStopping() + { + AppDomain.CurrentDomain.UnhandledException -= CurrentDomain_UnhandledException; + TaskScheduler.UnobservedTaskException -= TaskScheduler_UnobservedTaskException; + + // Start termination/disposal of the application. + TerminateService.Terminate(); + SingleInstanceChecker.Dispose(); + Logger.LogSoftwareStopped(AppConfig.AppName); + } + + private Global CreateGlobal() + => new(Config.DataDir, ConfigFilePath, Config); + + private PersistentConfig LoadOrCreateConfigs() + { + PersistentConfig persistentConfig = ConfigManager.LoadFile(ConfigFilePath, createIfMissing: true); + + if (persistentConfig.MigrateOldDefaultBackendUris(out PersistentConfig? newConfig)) + { + persistentConfig = newConfig; + ConfigManager.ToFile(ConfigFilePath, persistentConfig); + } + + return persistentConfig; + } + + private void TaskScheduler_UnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e) + { + AppConfig.UnobservedTaskExceptionsEventHandler?.Invoke(this, e.Exception); + } + + private void CurrentDomain_UnhandledException(object? sender, UnhandledExceptionEventArgs e) + { + if (e.ExceptionObject is Exception ex) + { + AppConfig.UnhandledExceptionEventHandler?.Invoke(this, ex); + } + } + + private async Task TerminateApplicationAsync() + { + Logger.LogSoftwareStopped(AppConfig.AppName); + + if (Global is { } global) + { + await global.DisposeAsync().ConfigureAwait(false); + } + } + + private void SetupLogger() + { + LogLevel logLevel = Enum.TryParse(Config.LogLevel, ignoreCase: true, out LogLevel parsedLevel) + ? parsedLevel + : LogLevel.Info; + Logger.InitializeDefaults(Path.Combine(Config.DataDir, "Logs.txt"), logLevel); + } + + private void ShowHelp() + { + Console.WriteLine($"{AppConfig.AppName} {Constants.ClientVersion}"); + Console.WriteLine($"Usage: {AppConfig.AppName} [OPTION]..."); + Console.WriteLine(); + Console.WriteLine("Available options are:"); + + foreach (var (parameter, hint) in Config.GetConfigOptionsMetadata().OrderBy(x => x.ParameterName)) + { + Console.Write($" --{parameter.ToLower(),-30} "); + var hintLines = hint.SplitLines(lineWidth: 40); + Console.WriteLine(hintLines[0]); + foreach (var hintLine in hintLines.Skip(1)) + { + Console.WriteLine($"{' ',-35}{hintLine}"); + } + Console.WriteLine(); + } + } +} + +public record WasabiAppBuilder(string AppName, string[] Arguments) +{ + internal bool MustCheckSingleInstance { get; init; } + internal EventHandler? UnhandledExceptionEventHandler { get; init; } + internal EventHandler? UnobservedTaskExceptionsEventHandler { get; init; } + internal Action Terminate { get; init; } = () => { }; + + public WasabiAppBuilder EnsureSingleInstance(bool ensure = true) => + this with { MustCheckSingleInstance = ensure }; + + public WasabiAppBuilder OnUnhandledExceptions(EventHandler handler) => + this with { UnhandledExceptionEventHandler = handler }; + + public WasabiAppBuilder OnUnobservedTaskExceptions(EventHandler handler) => + this with { UnobservedTaskExceptionsEventHandler = handler }; + + public WasabiAppBuilder OnTermination(Action action) => + this with { Terminate = action }; + public WasabiApplication Build() => + new(this); + + public static WasabiAppBuilder Create(string appName, string[] args) => + new(appName, args); +} + +public static class WasabiAppExtensions +{ + public static async Task RunAsConsoleAsync(this WasabiApplication app) + { + void ProcessCommands() + { + var arguments = app.AppConfig.Arguments; + var walletNames = ArgumentHelpers + .GetValues("wallet", arguments) + .Distinct(); + + foreach (var walletName in walletNames) + { + try + { + var wallet = app.Global!.WalletManager.GetWalletByName(walletName); + app.Global!.WalletManager.StartWalletAsync(wallet).ConfigureAwait(false); + } + catch (InvalidOperationException) + { + Logger.LogWarning($"Wallet '{walletName}' was not found. Ignoring..."); + } + } + } + + return await app.RunAsync( + async () => + { + try + { + await app.Global!.InitializeNoWalletAsync(app.TerminateService, app.TerminateService.CancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) when (app.TerminateService.CancellationToken.IsCancellationRequested) + { + Logger.LogInfo("User requested the application to stop. Stopping."); + } + + if (!app.TerminateService.CancellationToken.IsCancellationRequested) + { + ProcessCommands(); + await app.TerminateService.ForcefulTerminationRequestedTask.ConfigureAwait(false); + } + + }).ConfigureAwait(false); + } +} diff --git a/WalletWasabi.Daemon/packages.lock.json b/WalletWasabi.Daemon/packages.lock.json new file mode 100644 index 0000000000..e2bd267a5a --- /dev/null +++ b/WalletWasabi.Daemon/packages.lock.json @@ -0,0 +1,1805 @@ +{ + "version": 2, + "dependencies": { + "net7.0": { + "Microsoft.AspNetCore.JsonPatch": { + "type": "Transitive", + "resolved": "7.0.9", + "contentHash": "6iMRtYIQZj7gMC7iVotL9bZjCjnbV2ZkAAduKYHfV6v+WQhEjk0iEGSFNVh6N9rTCNTeZ2xVgv3xi675GwyDzQ==", + "dependencies": { + "Microsoft.CSharp": "4.7.0", + "Newtonsoft.Json": "13.0.1" + } + }, + "Microsoft.CSharp": { + "type": "Transitive", + "resolved": "4.7.0", + "contentHash": "pTj+D3uJWyN3My70i2Hqo+OXixq3Os2D1nJ2x92FFo6sk8fYS1m1WLNTs0Dc1uPaViH0YvEEwvzddQ7y4rhXmA==" + }, + "Microsoft.Data.Sqlite.Core": { + "type": "Transitive", + "resolved": "7.0.9", + "contentHash": "ow2PPoeW0yFc7NhexacQUw/LVjkO1mLK3VZAxhVIVjmQWlgYl/4mo9/U7uz+z75I+ZN6LUvq9M0ftU3IE75Ilg==", + "dependencies": { + "SQLitePCLRaw.core": "2.1.4" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "1.0.0", + "contentHash": "wHT6oY50q36mAXBRKtFaB7u07WxKC5u2M8fi3PqHOOnHyUo9gD0u1TlCNR8UObHQxKMYwqlgI8TLcErpt29n8A==", + "dependencies": { + "System.Collections": "4.0.11", + "System.Collections.Concurrent": "4.0.12", + "System.Diagnostics.Debug": "4.0.11", + "System.Globalization": "4.0.11", + "System.Linq": "4.1.0", + "System.Reflection": "4.1.0", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime.Extensions": "4.1.0", + "System.Runtime.InteropServices": "4.1.0" + } + }, + "Microsoft.NETCore.Platforms": { + "type": "Transitive", + "resolved": "1.0.1", + "contentHash": "2G6OjjJzwBfNOO8myRV/nFrbTw5iA+DEm0N+qUqhrOmaVtn4pC77h38I1jsXGw5VH55+dPfQsqHD0We9sCl9FQ==" + }, + "Microsoft.NETCore.Targets": { + "type": "Transitive", + "resolved": "1.0.1", + "contentHash": "rkn+fKobF/cbWfnnfBOQHKVKIOpxMZBvlSHkqDWgBpwGDcLRduvs3D9OLGeV6GWGvVwNlVi2CBbTjuPmtHvyNw==" + }, + "NBitcoin.Secp256k1": { + "type": "Transitive", + "resolved": "3.1.0", + "contentHash": "DVJbhT1plnBqPNnl5MNW1Aw5DCdfQ7MwlcL6PKcBIWPlQUO4T1FZRC4g2Axp9fd40eDKkyhq9MHlN9EeO09sGw==" + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.1", + "contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==" + }, + "Newtonsoft.Json.Bson": { + "type": "Transitive", + "resolved": "1.0.2", + "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", + "dependencies": { + "Newtonsoft.Json": "12.0.1" + } + }, + "SQLitePCLRaw.bundle_e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.4", + "contentHash": "EWI1olKDjFEBMJu0+3wuxwziIAdWDVMYLhuZ3Qs84rrz+DHwD00RzWPZCa+bLnHCf3oJwuFZIRsHT5p236QXww==", + "dependencies": { + "SQLitePCLRaw.lib.e_sqlite3": "2.1.4", + "SQLitePCLRaw.provider.e_sqlite3": "2.1.4" + } + }, + "SQLitePCLRaw.core": { + "type": "Transitive", + "resolved": "2.1.4", + "contentHash": "inBjvSHo9UDKneGNzfUfDjK08JzlcIhn1+SP5Y3m6cgXpCxXKCJDy6Mka7LpgSV+UZmKSnC8rTwB0SQ0xKu5pA==", + "dependencies": { + "System.Memory": "4.5.3" + } + }, + "SQLitePCLRaw.lib.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.4", + "contentHash": "2C9Q9eX7CPLveJA0rIhf9RXAvu+7nWZu1A2MdG6SD/NOu26TakGgL1nsbc0JAspGijFOo3HoN79xrx8a368fBg==" + }, + "SQLitePCLRaw.provider.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.4", + "contentHash": "CSlb5dUp1FMIkez9Iv5EXzpeq7rHryVNqwJMWnpq87j9zWZexaEMdisDktMsnnrzKM6ahNrsTkjqNodTBPBxtQ==", + "dependencies": { + "SQLitePCLRaw.core": "2.1.4" + } + }, + "System.Collections": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "YUJGz6eFKqS0V//mLt25vFGrrCvOnsXjlvFQs+KimpwNxug9x0Pzy4PlFMU3Q2IzqAa9G2L4LsK3+9vCBK7oTg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0" + } + }, + "System.Collections.Concurrent": { + "type": "Transitive", + "resolved": "4.0.12", + "contentHash": "2gBcbb3drMLgxlI0fBfxMA31ec6AEyYCHygGse4vxceJan8mRIWeKJ24BFzN7+bi/NFTgdIgufzb94LWO5EERQ==", + "dependencies": { + "System.Collections": "4.0.11", + "System.Diagnostics.Debug": "4.0.11", + "System.Diagnostics.Tracing": "4.1.0", + "System.Globalization": "4.0.11", + "System.Reflection": "4.1.0", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Threading": "4.0.11", + "System.Threading.Tasks": "4.0.11" + } + }, + "System.Diagnostics.Debug": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "w5U95fVKHY4G8ASs/K5iK3J5LY+/dLFd4vKejsnI/ZhBsWS9hQakfx3Zr7lRWKg4tAw9r4iktyvsTagWkqYCiw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0" + } + }, + "System.Diagnostics.Tracing": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "vDN1PoMZCkkdNjvZLql592oYJZgS7URcJzJ7bxeBgGtx5UtR5leNm49VmfHGqIffX4FKacHbI3H6UyNSHQknBg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0" + } + }, + "System.Globalization": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "B95h0YLEL2oSnwF/XjqSWKnwKOy/01VWkNlsCeMTFJLLabflpGV26nK164eRs5GiaRSBGpOxQ3pKoSnnyZN5pg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0" + } + }, + "System.IO": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "3KlTJceQc3gnGIaHZ7UBZO26SHL1SHE4ddrmiwumFnId+CEHP+O8r386tZKaE6zlk5/mF8vifMBzHj9SaXN+mQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "System.Text.Encoding": "4.0.11", + "System.Threading.Tasks": "4.0.11" + } + }, + "System.Linq": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "bQ0iYFOQI0nuTnt+NQADns6ucV4DUvMdwN6CbkB1yj8i7arTGiTN5eok1kQwdnnNWSDZfIUySQY+J3d5KjWn0g==", + "dependencies": { + "System.Collections": "4.0.11", + "System.Diagnostics.Debug": "4.0.11", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0" + } + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.5.3", + "contentHash": "3oDzvc/zzetpTKWMShs1AADwZjQ/36HnsufHRPcOjyRAAMLDlu2iD33MBI2opxnezcVUtXyqDXXjoFMOU9c7SA==" + }, + "System.Reflection": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "JCKANJ0TI7kzoQzuwB/OoJANy1Lg338B6+JVacPl4TpUwi3cReg3nMLplMq2uqYfHFQpKIlHAUVAJlImZz/4ng==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.IO": "4.1.0", + "System.Reflection.Primitives": "4.0.1", + "System.Runtime": "4.1.0" + } + }, + "System.Reflection.Primitives": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "4inTox4wTBaDhB7V3mPvp9XlCbeGYWVEM9/fXALd52vNEAVisc1BoVWQPuUuD0Ga//dNbA/WeMy9u9mzLxGTHQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0" + } + }, + "System.Resources.ResourceManager": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "TxwVeUNoTgUOdQ09gfTjvW411MF+w9MBYL7AtNVc+HtBCFlutPLhUCdZjNkjbhj3bNQWMdHboF0KIWEOjJssbA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Globalization": "4.0.11", + "System.Reflection": "4.1.0", + "System.Runtime": "4.1.0" + } + }, + "System.Runtime.Extensions": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "CUOHjTT/vgP0qGW22U4/hDlOqXmcPq5YicBaXdUR2UiUoLwBT+olO6we4DVbq57jeX5uXH2uerVZhf0qGj+sVQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0" + } + }, + "System.Runtime.Handles": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "nCJvEKguXEvk2ymk1gqj625vVnlK3/xdGzx0vOKicQkoquaTBJTP13AIYkocSUwHCLNBwUbXTqTWGDxBTWpt7g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0" + } + }, + "System.Runtime.InteropServices": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "16eu3kjHS633yYdkjwShDHZLRNMKVi/s0bY8ODiqJ2RfMhDMAwxZaUaWVnZ2P71kr/or+X9o/xFWtNqz8ivieQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Reflection": "4.1.0", + "System.Reflection.Primitives": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Handles": "4.0.1" + } + }, + "System.Text.Encoding": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "U3gGeMlDZXxCEiY4DwVLSacg+DFWCvoiX+JThA/rvw37Sqrku7sEFeVBBBMBnfB6FeZHsyDx85HlKL19x0HtZA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0" + } + }, + "System.Threading": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "N+3xqIcg3VDKyjwwCGaZ9HawG9aC6cSDI+s7ROma310GQo8vilFZa86hqKppwTHleR/G0sfOzhvgnUxWCR/DrQ==", + "dependencies": { + "System.Runtime": "4.1.0", + "System.Threading.Tasks": "4.0.11" + } + }, + "System.Threading.Tasks": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "k1S4Gc6IGwtHGT8188RSeGaX86Qw/wnrgNLshJvsdNUOPP9etMmo8S07c+UlOAx4K/xLuN9ivA1bD0LVurtIxQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0" + } + }, + "walletwasabi": { + "type": "Project", + "dependencies": { + "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[7.0.9, )", + "Microsoft.Data.Sqlite": "[7.0.9, )", + "Microsoft.Win32.SystemEvents": "[7.0.0, )", + "NBitcoin": "[7.0.27, )", + "WabiSabi": "[1.0.1.2, )" + } + }, + "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { + "type": "CentralTransitive", + "requested": "[7.0.9, )", + "resolved": "7.0.9", + "contentHash": "dhAFLGV3RfK6BAbLYpTKcVch1hcyP2qDWNy7Pk2wGrQEO/yWbWwiR9c13hk5kGWcPMGeVMkcuftUo6OAHe2yIA==", + "dependencies": { + "Microsoft.AspNetCore.JsonPatch": "7.0.9", + "Newtonsoft.Json": "13.0.1", + "Newtonsoft.Json.Bson": "1.0.2" + } + }, + "Microsoft.Data.Sqlite": { + "type": "CentralTransitive", + "requested": "[7.0.9, )", + "resolved": "7.0.9", + "contentHash": "XZ/7gpAP3EFlaDkLqv21Ro1ZHMtkh7UpBImyLcv0x+G5qt2J9vvrAk1g5qYL2ykwzpzf7Stc6Xt1MSkv5YmdPg==", + "dependencies": { + "Microsoft.Data.Sqlite.Core": "7.0.9", + "SQLitePCLRaw.bundle_e_sqlite3": "2.1.4" + } + }, + "Microsoft.Win32.SystemEvents": { + "type": "CentralTransitive", + "requested": "[7.0.0, )", + "resolved": "7.0.0", + "contentHash": "2nXPrhdAyAzir0gLl8Yy8S5Mnm/uBSQQA7jEsILOS1MTyS7DbmV1NgViMtvV1sfCD1ebITpNwb1NIinKeJgUVQ==" + }, + "NBitcoin": { + "type": "CentralTransitive", + "requested": "[7.0.27, )", + "resolved": "7.0.27", + "contentHash": "n2eHYJf0YVOf3ld0fhQJ8qR8TDvGZObGseOf5gHx03QpG+lq5L5qJAn5SA+MvZQLKcqhEUJ+S2AKvWkgZYS4Gw==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "1.0.0", + "Newtonsoft.Json": "13.0.1" + } + }, + "System.Runtime": { + "type": "CentralTransitive", + "requested": "[4.3.1, )", + "resolved": "4.1.0", + "contentHash": "v6c/4Yaa9uWsq+JMhnOFewrYkgdNHNG2eMKuNqRn8P733rNXeRCGvV5FkkjBXn2dbVkPXOsO0xjsEeM1q2zC0g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1" + } + }, + "WabiSabi": { + "type": "CentralTransitive", + "requested": "[1.0.1.2, )", + "resolved": "1.0.1.2", + "contentHash": "e+pMZGVEfWQvkpZHAydGv6grY71urfO47lodjXC9eWtfSFvNtPWjrgqck9O24yIbXhP4K3QrJKzJQFGpAp8rqg==", + "dependencies": { + "NBitcoin.Secp256k1": "3.1.0" + } + } + }, + "net7.0/linux-arm64": { + "runtime.any.System.Collections": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "MTBT/hu37Dm2042H1JjWSaMd8w+oPJ4ZWAbDNeLzC4ZHdqwHloP07KvD6+4VbwipDqY5obfFFy90mZYCaPDh5Q==", + "dependencies": { + "System.Runtime": "4.1.0" + } + }, + "runtime.any.System.Diagnostics.Tracing": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "x7VLOl/v504jX97YEMePamZRHA3cJPOFY/xLw9pgjDr0Q3IQIZ+0K4oiKKtQrfMYSvOAntkzw+EvvQ+OWGRL9w==" + }, + "runtime.any.System.Globalization": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "cjJ3+b83Tpf02AIc5FkGj1vzY68RnsVHiGLrOCc5n7gpNVg1JnZrt1mcY99ykQ/wr3nCdvSP2pYvdxbYsxZdlA==" + }, + "runtime.any.System.IO": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "sC7zKVdhYQEtrREKBJf4zkUwNdi6fsbkzrhJLDIAxIxD+YA5PABAQJps13zxpA1Ke3AgzOA9551JDymAfmRuTg==" + }, + "runtime.any.System.Reflection": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "eKq6/GprEINYbugjWf2V9cjkyuAH/y+Raed28PJQ35zd30oR/pvKEHNN8JbPAgzYpI09TCd1yuhXN/Rb8PM8GA==" + }, + "runtime.any.System.Reflection.Primitives": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "oKs78h11WDhCGFNpxT26IqL8Oo8OBzr6YOW0WG+R14FGaB/WDM5UHiK/jr6dipdnO8Wxlg/U48ka6uaPM6l53w==" + }, + "runtime.any.System.Resources.ResourceManager": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "hes7WFTOERydB/hLGmLj66NbK7I2AnjLHEeTpf7EmPZOIrRWeuC1dPoFYC9XRVIVzfCcOZI7oXM7KXe4vakt9Q==" + }, + "runtime.any.System.Runtime": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "0QVLwEGXROl0Trt2XosEjly9uqXcjHKStoZyZG9twJYFZJqq2JJXcBMXl/fnyQAgYEEODV8lUsU+t7NCCY0nUQ==", + "dependencies": { + "System.Private.Uri": "4.0.1" + } + }, + "runtime.any.System.Runtime.Handles": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "MZ5fVmAE/3S11wt3hPfn3RsAHppj5gUz+VZuLQkRjLCMSlX0krOI601IZsMWc3CoxUb+wMt3gZVb/mEjblw6Mg==" + }, + "runtime.any.System.Runtime.InteropServices": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "gmibdZ9x/eB6hf5le33DWLCQbhcIUD2vqoc0tBgqSUWlB8YjEzVJXyTPDO+ypKLlL90Kv3ZDrK7yPCNqcyhqCA==" + }, + "runtime.any.System.Text.Encoding": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "uweRMRDD4O8Iy8m4h1cJvoFIHNCzHMpipuxkRNAMML6EMzAhDCQTjgvRwki7PlUg8RGY1ctXnBZjT1rXvMZuRw==" + }, + "runtime.any.System.Threading.Tasks": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "CEvWO0IwtdCAsmCb9aAl59psy0hzx+whYh4DzbjNb0GsQmxw/G7bZEcrBtE8c9QupNVbu87c2xaMi6p4r1bpjA==" + }, + "runtime.native.System": { + "type": "Transitive", + "resolved": "4.0.0", + "contentHash": "QfS/nQI7k/BLgmLrw7qm7YBoULEvgWnPI+cYsbfCVFTW8Aj+i8JhccxcFMu1RWms0YZzF+UHguNBK4Qn89e2Sg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1" + } + }, + "runtime.native.System.Security.Cryptography": { + "type": "Transitive", + "resolved": "4.0.0", + "contentHash": "2CQK0jmO6Eu7ZeMgD+LOFbNJSXHFVQbCJJkEyEwowh1SCgYnrn9W9RykMfpeeVGw7h4IBvYikzpGUlmZTUafJw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1" + } + }, + "runtime.unix.System.Diagnostics.Debug": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "dGIYWbyqSlMlZrsqtU/TdvVNp8lieqowdGBVMi6nFTIiCqrL+RbdiJORguexXNjHtFZR30eE6zPWGxuL60NYFw==", + "dependencies": { + "runtime.native.System": "4.0.0" + } + }, + "runtime.unix.System.Private.Uri": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "m+7TLWWw4cA44vGxcKpMdV2Lgx6HWOe5rUb5RIADE04S6fJNEwXO6u+KY7oWFJQYn5644NyhSxB9oV28fF94NQ==", + "dependencies": { + "runtime.native.System": "4.0.0" + } + }, + "runtime.unix.System.Runtime.Extensions": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "ouVt2t9k22LcC9HeNX4mu3Ebvp1h+IPKaYiU3tDtOW9YcMR62XQyHsPq5BjBjMHuxjBRL5Hz+BwhSdrY6HjacA==", + "dependencies": { + "System.Private.Uri": "4.0.1", + "runtime.native.System": "4.0.0", + "runtime.native.System.Security.Cryptography": "4.0.0" + } + }, + "SQLitePCLRaw.lib.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.4", + "contentHash": "2C9Q9eX7CPLveJA0rIhf9RXAvu+7nWZu1A2MdG6SD/NOu26TakGgL1nsbc0JAspGijFOo3HoN79xrx8a368fBg==" + }, + "System.Collections": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "YUJGz6eFKqS0V//mLt25vFGrrCvOnsXjlvFQs+KimpwNxug9x0Pzy4PlFMU3Q2IzqAa9G2L4LsK3+9vCBK7oTg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "runtime.any.System.Collections": "4.0.11" + } + }, + "System.Diagnostics.Debug": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "w5U95fVKHY4G8ASs/K5iK3J5LY+/dLFd4vKejsnI/ZhBsWS9hQakfx3Zr7lRWKg4tAw9r4iktyvsTagWkqYCiw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "runtime.unix.System.Diagnostics.Debug": "4.0.11" + } + }, + "System.Diagnostics.Tracing": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "vDN1PoMZCkkdNjvZLql592oYJZgS7URcJzJ7bxeBgGtx5UtR5leNm49VmfHGqIffX4FKacHbI3H6UyNSHQknBg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "runtime.any.System.Diagnostics.Tracing": "4.1.0" + } + }, + "System.Globalization": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "B95h0YLEL2oSnwF/XjqSWKnwKOy/01VWkNlsCeMTFJLLabflpGV26nK164eRs5GiaRSBGpOxQ3pKoSnnyZN5pg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "runtime.any.System.Globalization": "4.0.11" + } + }, + "System.IO": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "3KlTJceQc3gnGIaHZ7UBZO26SHL1SHE4ddrmiwumFnId+CEHP+O8r386tZKaE6zlk5/mF8vifMBzHj9SaXN+mQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "System.Text.Encoding": "4.0.11", + "System.Threading.Tasks": "4.0.11", + "runtime.any.System.IO": "4.1.0" + } + }, + "System.Private.Uri": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "OltceAn9yyNf9LZIqvf80DhdRH55iVu1fxowdR79018w1CWIRNojUZBStsiRHvADeKI5pXcM9EftOFikBQh5AA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "runtime.unix.System.Private.Uri": "4.0.1" + } + }, + "System.Reflection": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "JCKANJ0TI7kzoQzuwB/OoJANy1Lg338B6+JVacPl4TpUwi3cReg3nMLplMq2uqYfHFQpKIlHAUVAJlImZz/4ng==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.IO": "4.1.0", + "System.Reflection.Primitives": "4.0.1", + "System.Runtime": "4.1.0", + "runtime.any.System.Reflection": "4.1.0" + } + }, + "System.Reflection.Primitives": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "4inTox4wTBaDhB7V3mPvp9XlCbeGYWVEM9/fXALd52vNEAVisc1BoVWQPuUuD0Ga//dNbA/WeMy9u9mzLxGTHQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "runtime.any.System.Reflection.Primitives": "4.0.1" + } + }, + "System.Resources.ResourceManager": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "TxwVeUNoTgUOdQ09gfTjvW411MF+w9MBYL7AtNVc+HtBCFlutPLhUCdZjNkjbhj3bNQWMdHboF0KIWEOjJssbA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Globalization": "4.0.11", + "System.Reflection": "4.1.0", + "System.Runtime": "4.1.0", + "runtime.any.System.Resources.ResourceManager": "4.0.1" + } + }, + "System.Runtime.Extensions": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "CUOHjTT/vgP0qGW22U4/hDlOqXmcPq5YicBaXdUR2UiUoLwBT+olO6we4DVbq57jeX5uXH2uerVZhf0qGj+sVQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "runtime.unix.System.Runtime.Extensions": "4.1.0" + } + }, + "System.Runtime.Handles": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "nCJvEKguXEvk2ymk1gqj625vVnlK3/xdGzx0vOKicQkoquaTBJTP13AIYkocSUwHCLNBwUbXTqTWGDxBTWpt7g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "runtime.any.System.Runtime.Handles": "4.0.1" + } + }, + "System.Runtime.InteropServices": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "16eu3kjHS633yYdkjwShDHZLRNMKVi/s0bY8ODiqJ2RfMhDMAwxZaUaWVnZ2P71kr/or+X9o/xFWtNqz8ivieQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Reflection": "4.1.0", + "System.Reflection.Primitives": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Handles": "4.0.1", + "runtime.any.System.Runtime.InteropServices": "4.1.0" + } + }, + "System.Text.Encoding": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "U3gGeMlDZXxCEiY4DwVLSacg+DFWCvoiX+JThA/rvw37Sqrku7sEFeVBBBMBnfB6FeZHsyDx85HlKL19x0HtZA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "runtime.any.System.Text.Encoding": "4.0.11" + } + }, + "System.Threading.Tasks": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "k1S4Gc6IGwtHGT8188RSeGaX86Qw/wnrgNLshJvsdNUOPP9etMmo8S07c+UlOAx4K/xLuN9ivA1bD0LVurtIxQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "runtime.any.System.Threading.Tasks": "4.0.11" + } + }, + "Microsoft.Win32.SystemEvents": { + "type": "CentralTransitive", + "requested": "[7.0.0, )", + "resolved": "7.0.0", + "contentHash": "2nXPrhdAyAzir0gLl8Yy8S5Mnm/uBSQQA7jEsILOS1MTyS7DbmV1NgViMtvV1sfCD1ebITpNwb1NIinKeJgUVQ==" + }, + "System.Runtime": { + "type": "CentralTransitive", + "requested": "[4.3.1, )", + "resolved": "4.1.0", + "contentHash": "v6c/4Yaa9uWsq+JMhnOFewrYkgdNHNG2eMKuNqRn8P733rNXeRCGvV5FkkjBXn2dbVkPXOsO0xjsEeM1q2zC0g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "runtime.any.System.Runtime": "4.1.0" + } + } + }, + "net7.0/linux-x64": { + "runtime.any.System.Collections": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "MTBT/hu37Dm2042H1JjWSaMd8w+oPJ4ZWAbDNeLzC4ZHdqwHloP07KvD6+4VbwipDqY5obfFFy90mZYCaPDh5Q==", + "dependencies": { + "System.Runtime": "4.1.0" + } + }, + "runtime.any.System.Diagnostics.Tracing": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "x7VLOl/v504jX97YEMePamZRHA3cJPOFY/xLw9pgjDr0Q3IQIZ+0K4oiKKtQrfMYSvOAntkzw+EvvQ+OWGRL9w==" + }, + "runtime.any.System.Globalization": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "cjJ3+b83Tpf02AIc5FkGj1vzY68RnsVHiGLrOCc5n7gpNVg1JnZrt1mcY99ykQ/wr3nCdvSP2pYvdxbYsxZdlA==" + }, + "runtime.any.System.IO": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "sC7zKVdhYQEtrREKBJf4zkUwNdi6fsbkzrhJLDIAxIxD+YA5PABAQJps13zxpA1Ke3AgzOA9551JDymAfmRuTg==" + }, + "runtime.any.System.Reflection": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "eKq6/GprEINYbugjWf2V9cjkyuAH/y+Raed28PJQ35zd30oR/pvKEHNN8JbPAgzYpI09TCd1yuhXN/Rb8PM8GA==" + }, + "runtime.any.System.Reflection.Primitives": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "oKs78h11WDhCGFNpxT26IqL8Oo8OBzr6YOW0WG+R14FGaB/WDM5UHiK/jr6dipdnO8Wxlg/U48ka6uaPM6l53w==" + }, + "runtime.any.System.Resources.ResourceManager": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "hes7WFTOERydB/hLGmLj66NbK7I2AnjLHEeTpf7EmPZOIrRWeuC1dPoFYC9XRVIVzfCcOZI7oXM7KXe4vakt9Q==" + }, + "runtime.any.System.Runtime": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "0QVLwEGXROl0Trt2XosEjly9uqXcjHKStoZyZG9twJYFZJqq2JJXcBMXl/fnyQAgYEEODV8lUsU+t7NCCY0nUQ==", + "dependencies": { + "System.Private.Uri": "4.0.1" + } + }, + "runtime.any.System.Runtime.Handles": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "MZ5fVmAE/3S11wt3hPfn3RsAHppj5gUz+VZuLQkRjLCMSlX0krOI601IZsMWc3CoxUb+wMt3gZVb/mEjblw6Mg==" + }, + "runtime.any.System.Runtime.InteropServices": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "gmibdZ9x/eB6hf5le33DWLCQbhcIUD2vqoc0tBgqSUWlB8YjEzVJXyTPDO+ypKLlL90Kv3ZDrK7yPCNqcyhqCA==" + }, + "runtime.any.System.Text.Encoding": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "uweRMRDD4O8Iy8m4h1cJvoFIHNCzHMpipuxkRNAMML6EMzAhDCQTjgvRwki7PlUg8RGY1ctXnBZjT1rXvMZuRw==" + }, + "runtime.any.System.Threading.Tasks": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "CEvWO0IwtdCAsmCb9aAl59psy0hzx+whYh4DzbjNb0GsQmxw/G7bZEcrBtE8c9QupNVbu87c2xaMi6p4r1bpjA==" + }, + "runtime.native.System": { + "type": "Transitive", + "resolved": "4.0.0", + "contentHash": "QfS/nQI7k/BLgmLrw7qm7YBoULEvgWnPI+cYsbfCVFTW8Aj+i8JhccxcFMu1RWms0YZzF+UHguNBK4Qn89e2Sg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1" + } + }, + "runtime.native.System.Security.Cryptography": { + "type": "Transitive", + "resolved": "4.0.0", + "contentHash": "2CQK0jmO6Eu7ZeMgD+LOFbNJSXHFVQbCJJkEyEwowh1SCgYnrn9W9RykMfpeeVGw7h4IBvYikzpGUlmZTUafJw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1" + } + }, + "runtime.unix.System.Diagnostics.Debug": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "dGIYWbyqSlMlZrsqtU/TdvVNp8lieqowdGBVMi6nFTIiCqrL+RbdiJORguexXNjHtFZR30eE6zPWGxuL60NYFw==", + "dependencies": { + "runtime.native.System": "4.0.0" + } + }, + "runtime.unix.System.Private.Uri": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "m+7TLWWw4cA44vGxcKpMdV2Lgx6HWOe5rUb5RIADE04S6fJNEwXO6u+KY7oWFJQYn5644NyhSxB9oV28fF94NQ==", + "dependencies": { + "runtime.native.System": "4.0.0" + } + }, + "runtime.unix.System.Runtime.Extensions": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "ouVt2t9k22LcC9HeNX4mu3Ebvp1h+IPKaYiU3tDtOW9YcMR62XQyHsPq5BjBjMHuxjBRL5Hz+BwhSdrY6HjacA==", + "dependencies": { + "System.Private.Uri": "4.0.1", + "runtime.native.System": "4.0.0", + "runtime.native.System.Security.Cryptography": "4.0.0" + } + }, + "SQLitePCLRaw.lib.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.4", + "contentHash": "2C9Q9eX7CPLveJA0rIhf9RXAvu+7nWZu1A2MdG6SD/NOu26TakGgL1nsbc0JAspGijFOo3HoN79xrx8a368fBg==" + }, + "System.Collections": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "YUJGz6eFKqS0V//mLt25vFGrrCvOnsXjlvFQs+KimpwNxug9x0Pzy4PlFMU3Q2IzqAa9G2L4LsK3+9vCBK7oTg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "runtime.any.System.Collections": "4.0.11" + } + }, + "System.Diagnostics.Debug": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "w5U95fVKHY4G8ASs/K5iK3J5LY+/dLFd4vKejsnI/ZhBsWS9hQakfx3Zr7lRWKg4tAw9r4iktyvsTagWkqYCiw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "runtime.unix.System.Diagnostics.Debug": "4.0.11" + } + }, + "System.Diagnostics.Tracing": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "vDN1PoMZCkkdNjvZLql592oYJZgS7URcJzJ7bxeBgGtx5UtR5leNm49VmfHGqIffX4FKacHbI3H6UyNSHQknBg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "runtime.any.System.Diagnostics.Tracing": "4.1.0" + } + }, + "System.Globalization": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "B95h0YLEL2oSnwF/XjqSWKnwKOy/01VWkNlsCeMTFJLLabflpGV26nK164eRs5GiaRSBGpOxQ3pKoSnnyZN5pg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "runtime.any.System.Globalization": "4.0.11" + } + }, + "System.IO": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "3KlTJceQc3gnGIaHZ7UBZO26SHL1SHE4ddrmiwumFnId+CEHP+O8r386tZKaE6zlk5/mF8vifMBzHj9SaXN+mQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "System.Text.Encoding": "4.0.11", + "System.Threading.Tasks": "4.0.11", + "runtime.any.System.IO": "4.1.0" + } + }, + "System.Private.Uri": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "OltceAn9yyNf9LZIqvf80DhdRH55iVu1fxowdR79018w1CWIRNojUZBStsiRHvADeKI5pXcM9EftOFikBQh5AA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "runtime.unix.System.Private.Uri": "4.0.1" + } + }, + "System.Reflection": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "JCKANJ0TI7kzoQzuwB/OoJANy1Lg338B6+JVacPl4TpUwi3cReg3nMLplMq2uqYfHFQpKIlHAUVAJlImZz/4ng==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.IO": "4.1.0", + "System.Reflection.Primitives": "4.0.1", + "System.Runtime": "4.1.0", + "runtime.any.System.Reflection": "4.1.0" + } + }, + "System.Reflection.Primitives": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "4inTox4wTBaDhB7V3mPvp9XlCbeGYWVEM9/fXALd52vNEAVisc1BoVWQPuUuD0Ga//dNbA/WeMy9u9mzLxGTHQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "runtime.any.System.Reflection.Primitives": "4.0.1" + } + }, + "System.Resources.ResourceManager": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "TxwVeUNoTgUOdQ09gfTjvW411MF+w9MBYL7AtNVc+HtBCFlutPLhUCdZjNkjbhj3bNQWMdHboF0KIWEOjJssbA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Globalization": "4.0.11", + "System.Reflection": "4.1.0", + "System.Runtime": "4.1.0", + "runtime.any.System.Resources.ResourceManager": "4.0.1" + } + }, + "System.Runtime.Extensions": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "CUOHjTT/vgP0qGW22U4/hDlOqXmcPq5YicBaXdUR2UiUoLwBT+olO6we4DVbq57jeX5uXH2uerVZhf0qGj+sVQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "runtime.unix.System.Runtime.Extensions": "4.1.0" + } + }, + "System.Runtime.Handles": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "nCJvEKguXEvk2ymk1gqj625vVnlK3/xdGzx0vOKicQkoquaTBJTP13AIYkocSUwHCLNBwUbXTqTWGDxBTWpt7g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "runtime.any.System.Runtime.Handles": "4.0.1" + } + }, + "System.Runtime.InteropServices": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "16eu3kjHS633yYdkjwShDHZLRNMKVi/s0bY8ODiqJ2RfMhDMAwxZaUaWVnZ2P71kr/or+X9o/xFWtNqz8ivieQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Reflection": "4.1.0", + "System.Reflection.Primitives": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Handles": "4.0.1", + "runtime.any.System.Runtime.InteropServices": "4.1.0" + } + }, + "System.Text.Encoding": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "U3gGeMlDZXxCEiY4DwVLSacg+DFWCvoiX+JThA/rvw37Sqrku7sEFeVBBBMBnfB6FeZHsyDx85HlKL19x0HtZA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "runtime.any.System.Text.Encoding": "4.0.11" + } + }, + "System.Threading.Tasks": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "k1S4Gc6IGwtHGT8188RSeGaX86Qw/wnrgNLshJvsdNUOPP9etMmo8S07c+UlOAx4K/xLuN9ivA1bD0LVurtIxQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "runtime.any.System.Threading.Tasks": "4.0.11" + } + }, + "Microsoft.Win32.SystemEvents": { + "type": "CentralTransitive", + "requested": "[7.0.0, )", + "resolved": "7.0.0", + "contentHash": "2nXPrhdAyAzir0gLl8Yy8S5Mnm/uBSQQA7jEsILOS1MTyS7DbmV1NgViMtvV1sfCD1ebITpNwb1NIinKeJgUVQ==" + }, + "System.Runtime": { + "type": "CentralTransitive", + "requested": "[4.3.1, )", + "resolved": "4.1.0", + "contentHash": "v6c/4Yaa9uWsq+JMhnOFewrYkgdNHNG2eMKuNqRn8P733rNXeRCGvV5FkkjBXn2dbVkPXOsO0xjsEeM1q2zC0g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "runtime.any.System.Runtime": "4.1.0" + } + } + }, + "net7.0/osx-arm64": { + "runtime.any.System.Collections": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "MTBT/hu37Dm2042H1JjWSaMd8w+oPJ4ZWAbDNeLzC4ZHdqwHloP07KvD6+4VbwipDqY5obfFFy90mZYCaPDh5Q==", + "dependencies": { + "System.Runtime": "4.1.0" + } + }, + "runtime.any.System.Diagnostics.Tracing": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "x7VLOl/v504jX97YEMePamZRHA3cJPOFY/xLw9pgjDr0Q3IQIZ+0K4oiKKtQrfMYSvOAntkzw+EvvQ+OWGRL9w==" + }, + "runtime.any.System.Globalization": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "cjJ3+b83Tpf02AIc5FkGj1vzY68RnsVHiGLrOCc5n7gpNVg1JnZrt1mcY99ykQ/wr3nCdvSP2pYvdxbYsxZdlA==" + }, + "runtime.any.System.IO": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "sC7zKVdhYQEtrREKBJf4zkUwNdi6fsbkzrhJLDIAxIxD+YA5PABAQJps13zxpA1Ke3AgzOA9551JDymAfmRuTg==" + }, + "runtime.any.System.Reflection": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "eKq6/GprEINYbugjWf2V9cjkyuAH/y+Raed28PJQ35zd30oR/pvKEHNN8JbPAgzYpI09TCd1yuhXN/Rb8PM8GA==" + }, + "runtime.any.System.Reflection.Primitives": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "oKs78h11WDhCGFNpxT26IqL8Oo8OBzr6YOW0WG+R14FGaB/WDM5UHiK/jr6dipdnO8Wxlg/U48ka6uaPM6l53w==" + }, + "runtime.any.System.Resources.ResourceManager": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "hes7WFTOERydB/hLGmLj66NbK7I2AnjLHEeTpf7EmPZOIrRWeuC1dPoFYC9XRVIVzfCcOZI7oXM7KXe4vakt9Q==" + }, + "runtime.any.System.Runtime": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "0QVLwEGXROl0Trt2XosEjly9uqXcjHKStoZyZG9twJYFZJqq2JJXcBMXl/fnyQAgYEEODV8lUsU+t7NCCY0nUQ==", + "dependencies": { + "System.Private.Uri": "4.0.1" + } + }, + "runtime.any.System.Runtime.Handles": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "MZ5fVmAE/3S11wt3hPfn3RsAHppj5gUz+VZuLQkRjLCMSlX0krOI601IZsMWc3CoxUb+wMt3gZVb/mEjblw6Mg==" + }, + "runtime.any.System.Runtime.InteropServices": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "gmibdZ9x/eB6hf5le33DWLCQbhcIUD2vqoc0tBgqSUWlB8YjEzVJXyTPDO+ypKLlL90Kv3ZDrK7yPCNqcyhqCA==" + }, + "runtime.any.System.Text.Encoding": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "uweRMRDD4O8Iy8m4h1cJvoFIHNCzHMpipuxkRNAMML6EMzAhDCQTjgvRwki7PlUg8RGY1ctXnBZjT1rXvMZuRw==" + }, + "runtime.any.System.Threading.Tasks": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "CEvWO0IwtdCAsmCb9aAl59psy0hzx+whYh4DzbjNb0GsQmxw/G7bZEcrBtE8c9QupNVbu87c2xaMi6p4r1bpjA==" + }, + "runtime.native.System": { + "type": "Transitive", + "resolved": "4.0.0", + "contentHash": "QfS/nQI7k/BLgmLrw7qm7YBoULEvgWnPI+cYsbfCVFTW8Aj+i8JhccxcFMu1RWms0YZzF+UHguNBK4Qn89e2Sg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1" + } + }, + "runtime.native.System.Security.Cryptography": { + "type": "Transitive", + "resolved": "4.0.0", + "contentHash": "2CQK0jmO6Eu7ZeMgD+LOFbNJSXHFVQbCJJkEyEwowh1SCgYnrn9W9RykMfpeeVGw7h4IBvYikzpGUlmZTUafJw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1" + } + }, + "runtime.unix.System.Diagnostics.Debug": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "dGIYWbyqSlMlZrsqtU/TdvVNp8lieqowdGBVMi6nFTIiCqrL+RbdiJORguexXNjHtFZR30eE6zPWGxuL60NYFw==", + "dependencies": { + "runtime.native.System": "4.0.0" + } + }, + "runtime.unix.System.Private.Uri": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "m+7TLWWw4cA44vGxcKpMdV2Lgx6HWOe5rUb5RIADE04S6fJNEwXO6u+KY7oWFJQYn5644NyhSxB9oV28fF94NQ==", + "dependencies": { + "runtime.native.System": "4.0.0" + } + }, + "runtime.unix.System.Runtime.Extensions": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "ouVt2t9k22LcC9HeNX4mu3Ebvp1h+IPKaYiU3tDtOW9YcMR62XQyHsPq5BjBjMHuxjBRL5Hz+BwhSdrY6HjacA==", + "dependencies": { + "System.Private.Uri": "4.0.1", + "runtime.native.System": "4.0.0", + "runtime.native.System.Security.Cryptography": "4.0.0" + } + }, + "SQLitePCLRaw.lib.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.4", + "contentHash": "2C9Q9eX7CPLveJA0rIhf9RXAvu+7nWZu1A2MdG6SD/NOu26TakGgL1nsbc0JAspGijFOo3HoN79xrx8a368fBg==" + }, + "System.Collections": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "YUJGz6eFKqS0V//mLt25vFGrrCvOnsXjlvFQs+KimpwNxug9x0Pzy4PlFMU3Q2IzqAa9G2L4LsK3+9vCBK7oTg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "runtime.any.System.Collections": "4.0.11" + } + }, + "System.Diagnostics.Debug": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "w5U95fVKHY4G8ASs/K5iK3J5LY+/dLFd4vKejsnI/ZhBsWS9hQakfx3Zr7lRWKg4tAw9r4iktyvsTagWkqYCiw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "runtime.unix.System.Diagnostics.Debug": "4.0.11" + } + }, + "System.Diagnostics.Tracing": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "vDN1PoMZCkkdNjvZLql592oYJZgS7URcJzJ7bxeBgGtx5UtR5leNm49VmfHGqIffX4FKacHbI3H6UyNSHQknBg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "runtime.any.System.Diagnostics.Tracing": "4.1.0" + } + }, + "System.Globalization": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "B95h0YLEL2oSnwF/XjqSWKnwKOy/01VWkNlsCeMTFJLLabflpGV26nK164eRs5GiaRSBGpOxQ3pKoSnnyZN5pg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "runtime.any.System.Globalization": "4.0.11" + } + }, + "System.IO": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "3KlTJceQc3gnGIaHZ7UBZO26SHL1SHE4ddrmiwumFnId+CEHP+O8r386tZKaE6zlk5/mF8vifMBzHj9SaXN+mQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "System.Text.Encoding": "4.0.11", + "System.Threading.Tasks": "4.0.11", + "runtime.any.System.IO": "4.1.0" + } + }, + "System.Private.Uri": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "OltceAn9yyNf9LZIqvf80DhdRH55iVu1fxowdR79018w1CWIRNojUZBStsiRHvADeKI5pXcM9EftOFikBQh5AA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "runtime.unix.System.Private.Uri": "4.0.1" + } + }, + "System.Reflection": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "JCKANJ0TI7kzoQzuwB/OoJANy1Lg338B6+JVacPl4TpUwi3cReg3nMLplMq2uqYfHFQpKIlHAUVAJlImZz/4ng==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.IO": "4.1.0", + "System.Reflection.Primitives": "4.0.1", + "System.Runtime": "4.1.0", + "runtime.any.System.Reflection": "4.1.0" + } + }, + "System.Reflection.Primitives": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "4inTox4wTBaDhB7V3mPvp9XlCbeGYWVEM9/fXALd52vNEAVisc1BoVWQPuUuD0Ga//dNbA/WeMy9u9mzLxGTHQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "runtime.any.System.Reflection.Primitives": "4.0.1" + } + }, + "System.Resources.ResourceManager": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "TxwVeUNoTgUOdQ09gfTjvW411MF+w9MBYL7AtNVc+HtBCFlutPLhUCdZjNkjbhj3bNQWMdHboF0KIWEOjJssbA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Globalization": "4.0.11", + "System.Reflection": "4.1.0", + "System.Runtime": "4.1.0", + "runtime.any.System.Resources.ResourceManager": "4.0.1" + } + }, + "System.Runtime.Extensions": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "CUOHjTT/vgP0qGW22U4/hDlOqXmcPq5YicBaXdUR2UiUoLwBT+olO6we4DVbq57jeX5uXH2uerVZhf0qGj+sVQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "runtime.unix.System.Runtime.Extensions": "4.1.0" + } + }, + "System.Runtime.Handles": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "nCJvEKguXEvk2ymk1gqj625vVnlK3/xdGzx0vOKicQkoquaTBJTP13AIYkocSUwHCLNBwUbXTqTWGDxBTWpt7g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "runtime.any.System.Runtime.Handles": "4.0.1" + } + }, + "System.Runtime.InteropServices": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "16eu3kjHS633yYdkjwShDHZLRNMKVi/s0bY8ODiqJ2RfMhDMAwxZaUaWVnZ2P71kr/or+X9o/xFWtNqz8ivieQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Reflection": "4.1.0", + "System.Reflection.Primitives": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Handles": "4.0.1", + "runtime.any.System.Runtime.InteropServices": "4.1.0" + } + }, + "System.Text.Encoding": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "U3gGeMlDZXxCEiY4DwVLSacg+DFWCvoiX+JThA/rvw37Sqrku7sEFeVBBBMBnfB6FeZHsyDx85HlKL19x0HtZA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "runtime.any.System.Text.Encoding": "4.0.11" + } + }, + "System.Threading.Tasks": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "k1S4Gc6IGwtHGT8188RSeGaX86Qw/wnrgNLshJvsdNUOPP9etMmo8S07c+UlOAx4K/xLuN9ivA1bD0LVurtIxQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "runtime.any.System.Threading.Tasks": "4.0.11" + } + }, + "Microsoft.Win32.SystemEvents": { + "type": "CentralTransitive", + "requested": "[7.0.0, )", + "resolved": "7.0.0", + "contentHash": "2nXPrhdAyAzir0gLl8Yy8S5Mnm/uBSQQA7jEsILOS1MTyS7DbmV1NgViMtvV1sfCD1ebITpNwb1NIinKeJgUVQ==" + }, + "System.Runtime": { + "type": "CentralTransitive", + "requested": "[4.3.1, )", + "resolved": "4.1.0", + "contentHash": "v6c/4Yaa9uWsq+JMhnOFewrYkgdNHNG2eMKuNqRn8P733rNXeRCGvV5FkkjBXn2dbVkPXOsO0xjsEeM1q2zC0g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "runtime.any.System.Runtime": "4.1.0" + } + } + }, + "net7.0/osx-x64": { + "runtime.any.System.Collections": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "MTBT/hu37Dm2042H1JjWSaMd8w+oPJ4ZWAbDNeLzC4ZHdqwHloP07KvD6+4VbwipDqY5obfFFy90mZYCaPDh5Q==", + "dependencies": { + "System.Runtime": "4.1.0" + } + }, + "runtime.any.System.Diagnostics.Tracing": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "x7VLOl/v504jX97YEMePamZRHA3cJPOFY/xLw9pgjDr0Q3IQIZ+0K4oiKKtQrfMYSvOAntkzw+EvvQ+OWGRL9w==" + }, + "runtime.any.System.Globalization": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "cjJ3+b83Tpf02AIc5FkGj1vzY68RnsVHiGLrOCc5n7gpNVg1JnZrt1mcY99ykQ/wr3nCdvSP2pYvdxbYsxZdlA==" + }, + "runtime.any.System.IO": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "sC7zKVdhYQEtrREKBJf4zkUwNdi6fsbkzrhJLDIAxIxD+YA5PABAQJps13zxpA1Ke3AgzOA9551JDymAfmRuTg==" + }, + "runtime.any.System.Reflection": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "eKq6/GprEINYbugjWf2V9cjkyuAH/y+Raed28PJQ35zd30oR/pvKEHNN8JbPAgzYpI09TCd1yuhXN/Rb8PM8GA==" + }, + "runtime.any.System.Reflection.Primitives": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "oKs78h11WDhCGFNpxT26IqL8Oo8OBzr6YOW0WG+R14FGaB/WDM5UHiK/jr6dipdnO8Wxlg/U48ka6uaPM6l53w==" + }, + "runtime.any.System.Resources.ResourceManager": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "hes7WFTOERydB/hLGmLj66NbK7I2AnjLHEeTpf7EmPZOIrRWeuC1dPoFYC9XRVIVzfCcOZI7oXM7KXe4vakt9Q==" + }, + "runtime.any.System.Runtime": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "0QVLwEGXROl0Trt2XosEjly9uqXcjHKStoZyZG9twJYFZJqq2JJXcBMXl/fnyQAgYEEODV8lUsU+t7NCCY0nUQ==", + "dependencies": { + "System.Private.Uri": "4.0.1" + } + }, + "runtime.any.System.Runtime.Handles": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "MZ5fVmAE/3S11wt3hPfn3RsAHppj5gUz+VZuLQkRjLCMSlX0krOI601IZsMWc3CoxUb+wMt3gZVb/mEjblw6Mg==" + }, + "runtime.any.System.Runtime.InteropServices": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "gmibdZ9x/eB6hf5le33DWLCQbhcIUD2vqoc0tBgqSUWlB8YjEzVJXyTPDO+ypKLlL90Kv3ZDrK7yPCNqcyhqCA==" + }, + "runtime.any.System.Text.Encoding": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "uweRMRDD4O8Iy8m4h1cJvoFIHNCzHMpipuxkRNAMML6EMzAhDCQTjgvRwki7PlUg8RGY1ctXnBZjT1rXvMZuRw==" + }, + "runtime.any.System.Threading.Tasks": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "CEvWO0IwtdCAsmCb9aAl59psy0hzx+whYh4DzbjNb0GsQmxw/G7bZEcrBtE8c9QupNVbu87c2xaMi6p4r1bpjA==" + }, + "runtime.native.System": { + "type": "Transitive", + "resolved": "4.0.0", + "contentHash": "QfS/nQI7k/BLgmLrw7qm7YBoULEvgWnPI+cYsbfCVFTW8Aj+i8JhccxcFMu1RWms0YZzF+UHguNBK4Qn89e2Sg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1" + } + }, + "runtime.native.System.Security.Cryptography": { + "type": "Transitive", + "resolved": "4.0.0", + "contentHash": "2CQK0jmO6Eu7ZeMgD+LOFbNJSXHFVQbCJJkEyEwowh1SCgYnrn9W9RykMfpeeVGw7h4IBvYikzpGUlmZTUafJw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1" + } + }, + "runtime.unix.System.Diagnostics.Debug": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "dGIYWbyqSlMlZrsqtU/TdvVNp8lieqowdGBVMi6nFTIiCqrL+RbdiJORguexXNjHtFZR30eE6zPWGxuL60NYFw==", + "dependencies": { + "runtime.native.System": "4.0.0" + } + }, + "runtime.unix.System.Private.Uri": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "m+7TLWWw4cA44vGxcKpMdV2Lgx6HWOe5rUb5RIADE04S6fJNEwXO6u+KY7oWFJQYn5644NyhSxB9oV28fF94NQ==", + "dependencies": { + "runtime.native.System": "4.0.0" + } + }, + "runtime.unix.System.Runtime.Extensions": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "ouVt2t9k22LcC9HeNX4mu3Ebvp1h+IPKaYiU3tDtOW9YcMR62XQyHsPq5BjBjMHuxjBRL5Hz+BwhSdrY6HjacA==", + "dependencies": { + "System.Private.Uri": "4.0.1", + "runtime.native.System": "4.0.0", + "runtime.native.System.Security.Cryptography": "4.0.0" + } + }, + "SQLitePCLRaw.lib.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.4", + "contentHash": "2C9Q9eX7CPLveJA0rIhf9RXAvu+7nWZu1A2MdG6SD/NOu26TakGgL1nsbc0JAspGijFOo3HoN79xrx8a368fBg==" + }, + "System.Collections": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "YUJGz6eFKqS0V//mLt25vFGrrCvOnsXjlvFQs+KimpwNxug9x0Pzy4PlFMU3Q2IzqAa9G2L4LsK3+9vCBK7oTg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "runtime.any.System.Collections": "4.0.11" + } + }, + "System.Diagnostics.Debug": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "w5U95fVKHY4G8ASs/K5iK3J5LY+/dLFd4vKejsnI/ZhBsWS9hQakfx3Zr7lRWKg4tAw9r4iktyvsTagWkqYCiw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "runtime.unix.System.Diagnostics.Debug": "4.0.11" + } + }, + "System.Diagnostics.Tracing": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "vDN1PoMZCkkdNjvZLql592oYJZgS7URcJzJ7bxeBgGtx5UtR5leNm49VmfHGqIffX4FKacHbI3H6UyNSHQknBg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "runtime.any.System.Diagnostics.Tracing": "4.1.0" + } + }, + "System.Globalization": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "B95h0YLEL2oSnwF/XjqSWKnwKOy/01VWkNlsCeMTFJLLabflpGV26nK164eRs5GiaRSBGpOxQ3pKoSnnyZN5pg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "runtime.any.System.Globalization": "4.0.11" + } + }, + "System.IO": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "3KlTJceQc3gnGIaHZ7UBZO26SHL1SHE4ddrmiwumFnId+CEHP+O8r386tZKaE6zlk5/mF8vifMBzHj9SaXN+mQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "System.Text.Encoding": "4.0.11", + "System.Threading.Tasks": "4.0.11", + "runtime.any.System.IO": "4.1.0" + } + }, + "System.Private.Uri": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "OltceAn9yyNf9LZIqvf80DhdRH55iVu1fxowdR79018w1CWIRNojUZBStsiRHvADeKI5pXcM9EftOFikBQh5AA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "runtime.unix.System.Private.Uri": "4.0.1" + } + }, + "System.Reflection": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "JCKANJ0TI7kzoQzuwB/OoJANy1Lg338B6+JVacPl4TpUwi3cReg3nMLplMq2uqYfHFQpKIlHAUVAJlImZz/4ng==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.IO": "4.1.0", + "System.Reflection.Primitives": "4.0.1", + "System.Runtime": "4.1.0", + "runtime.any.System.Reflection": "4.1.0" + } + }, + "System.Reflection.Primitives": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "4inTox4wTBaDhB7V3mPvp9XlCbeGYWVEM9/fXALd52vNEAVisc1BoVWQPuUuD0Ga//dNbA/WeMy9u9mzLxGTHQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "runtime.any.System.Reflection.Primitives": "4.0.1" + } + }, + "System.Resources.ResourceManager": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "TxwVeUNoTgUOdQ09gfTjvW411MF+w9MBYL7AtNVc+HtBCFlutPLhUCdZjNkjbhj3bNQWMdHboF0KIWEOjJssbA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Globalization": "4.0.11", + "System.Reflection": "4.1.0", + "System.Runtime": "4.1.0", + "runtime.any.System.Resources.ResourceManager": "4.0.1" + } + }, + "System.Runtime.Extensions": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "CUOHjTT/vgP0qGW22U4/hDlOqXmcPq5YicBaXdUR2UiUoLwBT+olO6we4DVbq57jeX5uXH2uerVZhf0qGj+sVQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "runtime.unix.System.Runtime.Extensions": "4.1.0" + } + }, + "System.Runtime.Handles": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "nCJvEKguXEvk2ymk1gqj625vVnlK3/xdGzx0vOKicQkoquaTBJTP13AIYkocSUwHCLNBwUbXTqTWGDxBTWpt7g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "runtime.any.System.Runtime.Handles": "4.0.1" + } + }, + "System.Runtime.InteropServices": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "16eu3kjHS633yYdkjwShDHZLRNMKVi/s0bY8ODiqJ2RfMhDMAwxZaUaWVnZ2P71kr/or+X9o/xFWtNqz8ivieQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Reflection": "4.1.0", + "System.Reflection.Primitives": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Handles": "4.0.1", + "runtime.any.System.Runtime.InteropServices": "4.1.0" + } + }, + "System.Text.Encoding": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "U3gGeMlDZXxCEiY4DwVLSacg+DFWCvoiX+JThA/rvw37Sqrku7sEFeVBBBMBnfB6FeZHsyDx85HlKL19x0HtZA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "runtime.any.System.Text.Encoding": "4.0.11" + } + }, + "System.Threading.Tasks": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "k1S4Gc6IGwtHGT8188RSeGaX86Qw/wnrgNLshJvsdNUOPP9etMmo8S07c+UlOAx4K/xLuN9ivA1bD0LVurtIxQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "runtime.any.System.Threading.Tasks": "4.0.11" + } + }, + "Microsoft.Win32.SystemEvents": { + "type": "CentralTransitive", + "requested": "[7.0.0, )", + "resolved": "7.0.0", + "contentHash": "2nXPrhdAyAzir0gLl8Yy8S5Mnm/uBSQQA7jEsILOS1MTyS7DbmV1NgViMtvV1sfCD1ebITpNwb1NIinKeJgUVQ==" + }, + "System.Runtime": { + "type": "CentralTransitive", + "requested": "[4.3.1, )", + "resolved": "4.1.0", + "contentHash": "v6c/4Yaa9uWsq+JMhnOFewrYkgdNHNG2eMKuNqRn8P733rNXeRCGvV5FkkjBXn2dbVkPXOsO0xjsEeM1q2zC0g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "runtime.any.System.Runtime": "4.1.0" + } + } + }, + "net7.0/win7-x64": { + "runtime.any.System.Collections": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "MTBT/hu37Dm2042H1JjWSaMd8w+oPJ4ZWAbDNeLzC4ZHdqwHloP07KvD6+4VbwipDqY5obfFFy90mZYCaPDh5Q==", + "dependencies": { + "System.Runtime": "4.1.0" + } + }, + "runtime.any.System.Diagnostics.Tracing": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "x7VLOl/v504jX97YEMePamZRHA3cJPOFY/xLw9pgjDr0Q3IQIZ+0K4oiKKtQrfMYSvOAntkzw+EvvQ+OWGRL9w==" + }, + "runtime.any.System.Globalization": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "cjJ3+b83Tpf02AIc5FkGj1vzY68RnsVHiGLrOCc5n7gpNVg1JnZrt1mcY99ykQ/wr3nCdvSP2pYvdxbYsxZdlA==" + }, + "runtime.any.System.IO": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "sC7zKVdhYQEtrREKBJf4zkUwNdi6fsbkzrhJLDIAxIxD+YA5PABAQJps13zxpA1Ke3AgzOA9551JDymAfmRuTg==" + }, + "runtime.any.System.Reflection": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "eKq6/GprEINYbugjWf2V9cjkyuAH/y+Raed28PJQ35zd30oR/pvKEHNN8JbPAgzYpI09TCd1yuhXN/Rb8PM8GA==" + }, + "runtime.any.System.Reflection.Primitives": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "oKs78h11WDhCGFNpxT26IqL8Oo8OBzr6YOW0WG+R14FGaB/WDM5UHiK/jr6dipdnO8Wxlg/U48ka6uaPM6l53w==" + }, + "runtime.any.System.Resources.ResourceManager": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "hes7WFTOERydB/hLGmLj66NbK7I2AnjLHEeTpf7EmPZOIrRWeuC1dPoFYC9XRVIVzfCcOZI7oXM7KXe4vakt9Q==" + }, + "runtime.any.System.Runtime": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "0QVLwEGXROl0Trt2XosEjly9uqXcjHKStoZyZG9twJYFZJqq2JJXcBMXl/fnyQAgYEEODV8lUsU+t7NCCY0nUQ==", + "dependencies": { + "System.Private.Uri": "4.0.1" + } + }, + "runtime.any.System.Runtime.Handles": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "MZ5fVmAE/3S11wt3hPfn3RsAHppj5gUz+VZuLQkRjLCMSlX0krOI601IZsMWc3CoxUb+wMt3gZVb/mEjblw6Mg==" + }, + "runtime.any.System.Runtime.InteropServices": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "gmibdZ9x/eB6hf5le33DWLCQbhcIUD2vqoc0tBgqSUWlB8YjEzVJXyTPDO+ypKLlL90Kv3ZDrK7yPCNqcyhqCA==" + }, + "runtime.any.System.Text.Encoding": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "uweRMRDD4O8Iy8m4h1cJvoFIHNCzHMpipuxkRNAMML6EMzAhDCQTjgvRwki7PlUg8RGY1ctXnBZjT1rXvMZuRw==" + }, + "runtime.any.System.Threading.Tasks": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "CEvWO0IwtdCAsmCb9aAl59psy0hzx+whYh4DzbjNb0GsQmxw/G7bZEcrBtE8c9QupNVbu87c2xaMi6p4r1bpjA==" + }, + "runtime.win.System.Diagnostics.Debug": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "q8Fm954ezFLfmG0tHNUmsNy+qaEjWtWqYhWh3cGSVjtJwkcBsfigWCh+fdaIVZ9K7m+6lgb3ElL2BBU6G+RijA==" + }, + "runtime.win.System.Runtime.Extensions": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "U3F/M+djxVXuKJaoW2AGpAE2ZWAp372140jsX4d/ctqki+Qb61HuyQY4yUPSA/gdKGbbq6HXzZ6oxB6/G3MYPA==", + "dependencies": { + "System.Private.Uri": "4.0.1" + } + }, + "runtime.win7.System.Private.Uri": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "LPOuwNel9nJ+G751J/yb64zkodFzVUwYYukQ8vysjiHRBrnvsZOhIxvqKhG6od1szrBNkl8pw8VGvvcfQ/2VOA==" + }, + "SQLitePCLRaw.lib.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.4", + "contentHash": "2C9Q9eX7CPLveJA0rIhf9RXAvu+7nWZu1A2MdG6SD/NOu26TakGgL1nsbc0JAspGijFOo3HoN79xrx8a368fBg==" + }, + "System.Collections": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "YUJGz6eFKqS0V//mLt25vFGrrCvOnsXjlvFQs+KimpwNxug9x0Pzy4PlFMU3Q2IzqAa9G2L4LsK3+9vCBK7oTg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "runtime.any.System.Collections": "4.0.11" + } + }, + "System.Diagnostics.Debug": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "w5U95fVKHY4G8ASs/K5iK3J5LY+/dLFd4vKejsnI/ZhBsWS9hQakfx3Zr7lRWKg4tAw9r4iktyvsTagWkqYCiw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "runtime.win.System.Diagnostics.Debug": "4.0.11" + } + }, + "System.Diagnostics.Tracing": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "vDN1PoMZCkkdNjvZLql592oYJZgS7URcJzJ7bxeBgGtx5UtR5leNm49VmfHGqIffX4FKacHbI3H6UyNSHQknBg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "runtime.any.System.Diagnostics.Tracing": "4.1.0" + } + }, + "System.Globalization": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "B95h0YLEL2oSnwF/XjqSWKnwKOy/01VWkNlsCeMTFJLLabflpGV26nK164eRs5GiaRSBGpOxQ3pKoSnnyZN5pg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "runtime.any.System.Globalization": "4.0.11" + } + }, + "System.IO": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "3KlTJceQc3gnGIaHZ7UBZO26SHL1SHE4ddrmiwumFnId+CEHP+O8r386tZKaE6zlk5/mF8vifMBzHj9SaXN+mQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "System.Text.Encoding": "4.0.11", + "System.Threading.Tasks": "4.0.11", + "runtime.any.System.IO": "4.1.0" + } + }, + "System.Private.Uri": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "OltceAn9yyNf9LZIqvf80DhdRH55iVu1fxowdR79018w1CWIRNojUZBStsiRHvADeKI5pXcM9EftOFikBQh5AA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "runtime.win7.System.Private.Uri": "4.0.1" + } + }, + "System.Reflection": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "JCKANJ0TI7kzoQzuwB/OoJANy1Lg338B6+JVacPl4TpUwi3cReg3nMLplMq2uqYfHFQpKIlHAUVAJlImZz/4ng==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.IO": "4.1.0", + "System.Reflection.Primitives": "4.0.1", + "System.Runtime": "4.1.0", + "runtime.any.System.Reflection": "4.1.0" + } + }, + "System.Reflection.Primitives": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "4inTox4wTBaDhB7V3mPvp9XlCbeGYWVEM9/fXALd52vNEAVisc1BoVWQPuUuD0Ga//dNbA/WeMy9u9mzLxGTHQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "runtime.any.System.Reflection.Primitives": "4.0.1" + } + }, + "System.Resources.ResourceManager": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "TxwVeUNoTgUOdQ09gfTjvW411MF+w9MBYL7AtNVc+HtBCFlutPLhUCdZjNkjbhj3bNQWMdHboF0KIWEOjJssbA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Globalization": "4.0.11", + "System.Reflection": "4.1.0", + "System.Runtime": "4.1.0", + "runtime.any.System.Resources.ResourceManager": "4.0.1" + } + }, + "System.Runtime.Extensions": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "CUOHjTT/vgP0qGW22U4/hDlOqXmcPq5YicBaXdUR2UiUoLwBT+olO6we4DVbq57jeX5uXH2uerVZhf0qGj+sVQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "runtime.win.System.Runtime.Extensions": "4.1.0" + } + }, + "System.Runtime.Handles": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "nCJvEKguXEvk2ymk1gqj625vVnlK3/xdGzx0vOKicQkoquaTBJTP13AIYkocSUwHCLNBwUbXTqTWGDxBTWpt7g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "runtime.any.System.Runtime.Handles": "4.0.1" + } + }, + "System.Runtime.InteropServices": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "16eu3kjHS633yYdkjwShDHZLRNMKVi/s0bY8ODiqJ2RfMhDMAwxZaUaWVnZ2P71kr/or+X9o/xFWtNqz8ivieQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Reflection": "4.1.0", + "System.Reflection.Primitives": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Handles": "4.0.1", + "runtime.any.System.Runtime.InteropServices": "4.1.0" + } + }, + "System.Text.Encoding": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "U3gGeMlDZXxCEiY4DwVLSacg+DFWCvoiX+JThA/rvw37Sqrku7sEFeVBBBMBnfB6FeZHsyDx85HlKL19x0HtZA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "runtime.any.System.Text.Encoding": "4.0.11" + } + }, + "System.Threading.Tasks": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "k1S4Gc6IGwtHGT8188RSeGaX86Qw/wnrgNLshJvsdNUOPP9etMmo8S07c+UlOAx4K/xLuN9ivA1bD0LVurtIxQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "runtime.any.System.Threading.Tasks": "4.0.11" + } + }, + "Microsoft.Win32.SystemEvents": { + "type": "CentralTransitive", + "requested": "[7.0.0, )", + "resolved": "7.0.0", + "contentHash": "2nXPrhdAyAzir0gLl8Yy8S5Mnm/uBSQQA7jEsILOS1MTyS7DbmV1NgViMtvV1sfCD1ebITpNwb1NIinKeJgUVQ==" + }, + "System.Runtime": { + "type": "CentralTransitive", + "requested": "[4.3.1, )", + "resolved": "4.1.0", + "contentHash": "v6c/4Yaa9uWsq+JMhnOFewrYkgdNHNG2eMKuNqRn8P733rNXeRCGvV5FkkjBXn2dbVkPXOsO0xjsEeM1q2zC0g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "runtime.any.System.Runtime": "4.1.0" + } + } + } + } +} \ No newline at end of file diff --git a/WalletWasabi.Documentation/ClientDeployment.md b/WalletWasabi.Documentation/ClientRelease/ClientDeployment.md similarity index 100% rename from WalletWasabi.Documentation/ClientDeployment.md rename to WalletWasabi.Documentation/ClientRelease/ClientDeployment.md diff --git a/WalletWasabi.Documentation/ClientRelease/ReleaseNotesTemplate.md b/WalletWasabi.Documentation/ClientRelease/ReleaseNotesTemplate.md new file mode 100644 index 0000000000..6c91d1551d --- /dev/null +++ b/WalletWasabi.Documentation/ClientRelease/ReleaseNotesTemplate.md @@ -0,0 +1,42 @@ +### _[Wasabi Wallet](https://WasabiWallet.io) is an easy to use, privacy-focused, open-source, non-custodial, Bitcoin wallet_ + +# Download +:window: [Windows](https://github.com/zkSNACKs/WalletWasabi/releases/download/v2.0.x/Wasabi-2.0.x.msi) +:green_apple: [Apple M1/M2](https://github.com/zkSNACKs/WalletWasabi/releases/download/v2.0.x/Wasabi-2.0.x-arm64.dmg) +:apple: [Apple Intel](https://github.com/zkSNACKs/WalletWasabi/releases/download/v2.0.x/Wasabi-2.0.x.dmg) +:penguin: [Ubuntu / Debian](https://github.com/zkSNACKs/WalletWasabi/releases/download/v2.0.x/Wasabi-2.0.x.deb) +:penguin: [Other Linux](https://github.com/zkSNACKs/WalletWasabi/releases/download/v2.0.x/Wasabi-2.0.x.tar.gz) + +--- +## Release Highlights + +## Release Summary + +Read the [related blog](https://blog.wasabiwallet.io/) for more information. + +--- +## Installation Guide +Download the operating system relevant software package and install Wasabi like you would with any other software on your computer. +For a detailed installation guide, including **signature verification**, see [the documentation](https://docs.wasabiwallet.io/using-wasabi/InstallPackage.html). + +## Documentation +:spider_web: [Website](https://wasabiwallet.io) +:onion: [Tor onion site](http://wasabiukrxmkdgve5kynjztuovbg43uxcbcxn6y2okcrsg7gb6jdmbad.onion/) +:orange_book: [Documentation](https://docs.wasabiwallet.io) +:grey_question: [FAQ](https://github.com/zkSNACKs/WalletWasabi/discussions/categories/faq) + +## Advanced Guide +If you want to build or update Wasabi from source code, check out [these easy instructions](https://docs.wasabiwallet.io/using-wasabi/BuildSource.html). + +Wasabi uses [reproducible builds](https://reproducible-builds.org/), which you can verify with [this guide](https://github.com/zkSNACKs/WalletWasabi/blob/master/WalletWasabi.Documentation/Guides/DeterministicBuildGuide.md). + +## Requirements +- Windows 10 1607+ +- Windows 11 22000+ +- macOS 10.15+ +- Ubuntu 18.04+ +- Fedora 36+ +- Debian 10+ +--- + +## Full Changelog diff --git a/WalletWasabi.Documentation/HowToDeploy.md b/WalletWasabi.Documentation/HowToDeploy.md new file mode 100644 index 0000000000..3e4aa3e94f --- /dev/null +++ b/WalletWasabi.Documentation/HowToDeploy.md @@ -0,0 +1,51 @@ +# The agreed procedure to deploy something to production + +1. Create a summary of what is going to be deployed. A link to the PRs labeled as `Affiliate` could be enough. +2. Announce in the Integrations Slack channel the deployment of one specific commit to testnet, using the @here tag, and share the summary. +3. Affiliates must acknowledge the notification. +4. Affiliates must express whether they are willing to test what has been deployed or not. + 1. In case Affiliates **are not** interested in testing it, then the Wasabi team will notify the planned deployment date to production, and that's it. + 2. In case Affiliates **are** interested in testing it, they must: + 1. Let the Wasabi team know how much time they need to test it. + 2. Share their findings. In case there is any, then iterate. + 3. Give the Wasabi team the final approval. + 4. The Wasabi team and the Affiliates must agree on a release date for production deployment. +5. Don't forget to make the discussion in the maintenance repository with a short summary. + +# How to deploy + +1. run `./build-wasabi ` +2. run `./deploy-wasabi` + +# Templates for communication + +**Deploy to TestNet example** + +Hello there, we are deploying this commit to the `TestNet` server. +- Latest commit on backend: 167c81be80d8d3de9deaf8d306017c5403593c89 +- Planning to deploy to backend: 460e21ce71738d3cc1560a3d4fc1984cc4beb725 +- PRs with affiliate label: https://github.com/zkSNACKs/WalletWasabi/pulls?q=is%3Apr+is%3Aclosed+label%3Aaffiliate + +Please ack and test. + +------- + +## Scripts details + +### build-wasabi script + +This script builds wasabi backend and it is defined as follow: + +```bash +$ echo "#!/usr/bin/env bash" > build-wasabi +$ echo "nix build -o wasabi-backend github:zksnacks/walletwasabi/\$1" >> build-wasabi +``` + +### deploy-wasabi script + +This script deploys the already built wasabi backend and it is defined as follow: + +```bash +$ echo "#!/usr/bin/env bash" > deploy-wasabi +$ echo "./wasabi-backend/deploy" >> deploy-wasabi +``` diff --git a/WalletWasabi.Documentation/WasabiCompatibility.md b/WalletWasabi.Documentation/WasabiCompatibility.md index ae1e97e1ac..815ecfcc5f 100644 --- a/WalletWasabi.Documentation/WasabiCompatibility.md +++ b/WalletWasabi.Documentation/WasabiCompatibility.md @@ -8,7 +8,7 @@ This document lists all the officially supported software and devices by Wasabi - Windows 11 22000+ - macOS 10.15+ - Ubuntu 18.04+ -- Fedora 33+ +- Fedora 36+ - Debian 10+ # Officially Supported Hardware Wallets @@ -24,7 +24,8 @@ This document lists all the officially supported software and devices by Wasabi # Officially Supported Architectures -- x64 +- x64 (Windows, Linux, macOS) +- arm64 (macOS) # FAQ diff --git a/WalletWasabi.Documentation/WasabiSetupRegtest.md b/WalletWasabi.Documentation/WasabiSetupRegtest.md index 01ae09694a..144ffe3fde 100644 --- a/WalletWasabi.Documentation/WasabiSetupRegtest.md +++ b/WalletWasabi.Documentation/WasabiSetupRegtest.md @@ -11,30 +11,30 @@ Bitcoin Knots is working very similarly to Bitcoin Core. You can get a grasp wit Todo: -1. Install [Bitcoin Knots](http://bitcoinknots.org/) on your computer. Verify the PGP - there is a tutorial [here](http://bitcoinknots.org/) +1. Install [Bitcoin Knots](https://bitcoinknots.org/) on your computer. Verify the PGP - there is a tutorial [here](https://bitcoinknots.org/) 2. Start Bitcoin Knots with: bitcoin-qt.exe -regtest then quit immediately. In this way the data directory and the config files will be generated. -``` -Windows: "C:\Program Files\Bitcoin\bitcoin-qt.exe" -regtest -macOS: "/Applications/Bitcoin-Qt.app/Contents/MacOS/Bitcoin-Qt" -regtest -Linux: -``` + ``` + Windows: "C:\Program Files\Bitcoin\bitcoin-qt.exe" -regtest + macOS: "/Applications/Bitcoin-Qt.app/Contents/MacOS/Bitcoin-Qt" -regtest + Linux: ~/bitcoin-[version number]/bin/bitcoin-qt -regtest + ``` 3. Go to Bitcoin Knots data directory. If the directory is missing run core bitcoin-qt, then quit immediately. In this way the data directory and the config files will be generated. -``` -Windows: %APPDATA%\Bitcoin\ -macOS: $HOME/Library/Application Support/Bitcoin/ -Linux: $HOME/.bitcoin/ -``` + ``` + Windows: %APPDATA%\Bitcoin\ + macOS: $HOME/Library/Application Support/Bitcoin/ + Linux: $HOME/.bitcoin/ + ``` 4. Add a file called **bitcoin.conf** and add these lines: -```C# -regtest.server = 1 -regtest.listen = 1 -regtest.txindex = 1 -regtest.whitebind = 127.0.0.1:18444 -regtest.rpchost = 127.0.0.1 -regtest.rpcport = 18443 -regtest.rpcuser = 7c9b6473600fbc9be1120ae79f1622f42c32e5c78d -regtest.rpcpassword = 309bc9961d01f388aed28b630ae834379296a8c8e3 -``` + ```C# + regtest.server = 1 + regtest.listen = 1 + regtest.txindex = 1 + regtest.whitebind = 127.0.0.1:18444 + regtest.rpchost = 127.0.0.1 + regtest.rpcport = 18443 + regtest.rpcuser = 7c9b6473600fbc9be1120ae79f1622f42c32e5c78d + regtest.rpcpassword = 309bc9961d01f388aed28b630ae834379296a8c8e3 + ``` 5. Save it. 6. Start Bitcoin Knots with: bitcoin-qt.exe -regtest. 7. Do not worry about "Syncing Headers" just press the Hide button. Because you run on Regtest, no Mainnet blocks will be downloaded. @@ -59,30 +59,30 @@ Todo: `dotnet run` 3. You will get some errors, but the data directory will be created. Stop the backend if it is still running with CTRL-C. 4. Go to the Backend folder: -``` -Windows: "C:\Users\{your username}\AppData\Roaming\WalletWasabi\Backend" -macOS: "/Users/{your username}/.walletwasabi/backend" -Linux: "/home/{your username}/.walletwasabi/backend" -``` + ``` + Windows: "C:\Users\{your username}\AppData\Roaming\WalletWasabi\Backend" + macOS: "/Users/{your username}/.walletwasabi/backend" + Linux: "/home/{your username}/.walletwasabi/backend" + ``` 5. Edit `Config.json` file by replacing everything with: -```json -{ - "Network": "RegTest", - "BitcoinRpcConnectionString": "7c9b6473600fbc9be1120ae79f1622f42c32e5c78d:309bc9961d01f388aed28b630ae834379296a8c8e3", - "MainNetBitcoinP2pEndPoint": "127.0.0.1:8333", - "TestNetBitcoinP2pEndPoint": "127.0.0.1:18333", - "RegTestBitcoinP2pEndPoint": "127.0.0.1:18444", - "MainNetBitcoinCoreRpcEndPoint": "127.0.0.1:8332", - "TestNetBitcoinCoreRpcEndPoint": "127.0.0.1:18332", - "RegTestBitcoinCoreRpcEndPoint": "127.0.0.1:18443" -} -``` + ```json + { + "Network": "RegTest", + "BitcoinRpcConnectionString": "7c9b6473600fbc9be1120ae79f1622f42c32e5c78d:309bc9961d01f388aed28b630ae834379296a8c8e3", + "MainNetBitcoinP2pEndPoint": "127.0.0.1:8333", + "TestNetBitcoinP2pEndPoint": "127.0.0.1:18333", + "RegTestBitcoinP2pEndPoint": "127.0.0.1:18444", + "MainNetBitcoinCoreRpcEndPoint": "127.0.0.1:8332", + "TestNetBitcoinCoreRpcEndPoint": "127.0.0.1:18332", + "RegTestBitcoinCoreRpcEndPoint": "127.0.0.1:18443" + } + ``` 6. Edit some lines in `WabiSabiConfig.json`. For example, make the `InputRegistrationPhase` faster and allow rounds to have between 2 and 100 inputs: -``` -"StandardInputRegistrationTimeout": "0d 0h 2m 0s", -"MaxInputCountByRound": 100, -"MinInputCountByRoundMultiplier": 0.02, -``` + ``` + "StandardInputRegistrationTimeout": "0d 0h 2m 0s", + "MaxInputCountByRound": 100, + "MinInputCountByRoundMultiplier": 0.02, + ``` 7. Start Bitcoin Knots in RegTest (command to run is explained above). 8. Go to WalletWasabi folder 9. Open the command line and enter. This will build all the projects under this directory. @@ -111,4 +111,4 @@ Todo: 12. If you see `Waiting for confirmed funds` in the music box you can generate a block in Bitcoin Knots to continue coinjoining. - You can do it with the console command `generatetoaddress 1 ` -Happy CoinJoin! +Happy CoinJoin! \ No newline at end of file diff --git a/WalletWasabi.Fluent.Desktop/Extensions/AppBuilderExtension.cs b/WalletWasabi.Fluent.Desktop/Extensions/AppBuilderExtension.cs index d8cd90374c..3d0db74fce 100644 --- a/WalletWasabi.Fluent.Desktop/Extensions/AppBuilderExtension.cs +++ b/WalletWasabi.Fluent.Desktop/Extensions/AppBuilderExtension.cs @@ -2,6 +2,9 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Dialogs; +using Avalonia.Media; +using Avalonia.Platform; +using WalletWasabi.Logging; namespace WalletWasabi.Fluent.Desktop.Extensions; @@ -9,7 +12,7 @@ public static class AppBuilderExtension { public static AppBuilder SetupAppBuilder(this AppBuilder appBuilder) { - bool enableGpu = Services.Config is null ? false : Services.Config.EnableGpu; + bool enableGpu = Services.PersistentConfig is null ? false : Services.PersistentConfig.EnableGpu; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { @@ -20,7 +23,7 @@ public static AppBuilder SetupAppBuilder(this AppBuilder appBuilder) else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { appBuilder.UsePlatformDetect() - .UseManagedSystemDialogs(); + .UseManagedSystemDialogs(); } else { @@ -28,10 +31,29 @@ public static AppBuilder SetupAppBuilder(this AppBuilder appBuilder) } return appBuilder + .WithInterFont() + .With(new FontManagerOptions { DefaultFamilyName = "fonts:Inter#Inter, $Default" }) .With(new SkiaOptions { MaxGpuResourceSizeBytes = 2560 * 1600 * 4 * 12 }) - .With(new Win32PlatformOptions { AllowEglInitialization = enableGpu, UseDeferredRendering = true, UseWindowsUIComposition = true }) - .With(new X11PlatformOptions { UseGpu = enableGpu, WmClass = "Wasabi Wallet" }) - .With(new AvaloniaNativePlatformOptions { UseDeferredRendering = true, UseGpu = enableGpu }) + .With(new Win32PlatformOptions + { + RenderingMode = enableGpu + ? new[] { Win32RenderingMode.AngleEgl, Win32RenderingMode.Software } + : new[] { Win32RenderingMode.Software }, + CompositionMode = new[] { Win32CompositionMode.WinUIComposition, Win32CompositionMode.RedirectionSurface } + }) + .With(new X11PlatformOptions + { + RenderingMode = enableGpu + ? new[] { X11RenderingMode.Glx, X11RenderingMode.Software } + : new[] { X11RenderingMode.Software }, + WmClass = "Wasabi Wallet" + }) + .With(new AvaloniaNativePlatformOptions + { + RenderingMode = enableGpu + ? new[] { AvaloniaNativeRenderingMode.OpenGl, AvaloniaNativeRenderingMode.Software } + : new[] { AvaloniaNativeRenderingMode.Software }, + }) .With(new MacOSPlatformOptions { ShowInDock = true }); } } diff --git a/WalletWasabi.Fluent.Desktop/Program.cs b/WalletWasabi.Fluent.Desktop/Program.cs index 458f630575..6dacce9413 100644 --- a/WalletWasabi.Fluent.Desktop/Program.cs +++ b/WalletWasabi.Fluent.Desktop/Program.cs @@ -6,44 +6,34 @@ using System.Reactive.Concurrency; using System.Runtime.InteropServices; using System.Threading.Tasks; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Threading; using ReactiveUI; using System.Linq; -using Avalonia.OpenGL; using WalletWasabi.Fluent.CrashReport; using WalletWasabi.Fluent.Helpers; using WalletWasabi.Fluent.ViewModels; -using WalletWasabi.Helpers; using WalletWasabi.Logging; using WalletWasabi.Models; -using WalletWasabi.Services; -using WalletWasabi.Services.Terminate; -using WalletWasabi.Wallets; using System.Diagnostics.CodeAnalysis; using WalletWasabi.Fluent.Desktop.Extensions; using System.Net.Sockets; using System.Collections.ObjectModel; +using WalletWasabi.Daemon; using LogLevel = WalletWasabi.Logging.LogLevel; +using System.Threading; namespace WalletWasabi.Fluent.Desktop; public class Program { - private static Global? Global; - // Initialization code. Don't use any Avalonia, third-party APIs or any // SynchronizationContext-reliant code before AppMain is called: things aren't initialized // yet and stuff might break. - public static int Main(string[] args) + public static async Task Main(string[] args) { - bool runGuiInBackground = args.Any(arg => arg.Contains(StartupHelper.SilentArgument)); - - // Initialize the logger. - string dataDir = EnvironmentHelpers.GetDataDir(Path.Combine("WalletWasabi", "Client")); - SetupLogger(dataDir, args); - - Logger.LogDebug($"Wasabi was started with these argument(s): {(args.Any() ? string.Join(" ", args) : "none")}."); - // Crash reporting must be before the "single instance checking". + Logger.InitializeDefaults(Path.Combine(Config.DataDir, "Logs.txt"), LogLevel.Info); try { if (CrashReporter.TryGetExceptionFromCliArgs(args, out var exceptionToShow)) @@ -60,197 +50,63 @@ public static int Main(string[] args) return 1; } - (UiConfig uiConfig, Config config) = LoadOrCreateConfigs(dataDir); - - // Start single instance checker that is active over the lifetime of the application. - using SingleInstanceChecker singleInstanceChecker = new(config.Network); - - try - { - singleInstanceChecker.EnsureSingleOrThrowAsync().GetAwaiter().GetResult(); - } - catch (OperationCanceledException) - { - // We have successfully signalled the other instance and that instance should pop up - // so user will think he has just run the application. - return 1; - } - catch (Exception ex) - { - CrashReporter.Invoke(ex); - Logger.LogCritical(ex); - return 1; - } - - // Now run the GUI application. - AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; - TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException; - - Exception? exceptionToReport = null; - TerminateService terminateService = new(TerminateApplicationAsync, TerminateApplication); - try { - Global = CreateGlobal(dataDir, uiConfig, config); - Services.Initialize(Global, singleInstanceChecker); - - RxApp.DefaultExceptionHandler = Observer.Create(ex => - { - if (Debugger.IsAttached) - { - Debugger.Break(); - } - - Logger.LogError(ex); + var app = WasabiAppBuilder + .Create("Wasabi GUI", args) + .EnsureSingleInstance() + .OnUnhandledExceptions(LogUnhandledException) + .OnUnobservedTaskExceptions(LogUnobservedTaskException) + .OnTermination(TerminateApplication) + .Build(); - RxApp.MainThreadScheduler.Schedule(() => throw new ApplicationException("Exception has been thrown in unobserved ThrownExceptions", ex)); - }); + var exitCode = await app.RunAsGuiAsync(); - Logger.LogSoftwareStarted("Wasabi GUI"); - AppBuilder - .Configure(() => new App(async () => await Global.InitializeNoWalletAsync(terminateService), runGuiInBackground)) - .UseReactiveUI() - .SetupAppBuilder() - .AfterSetup(_ => - { - var glInterface = AvaloniaLocator.CurrentMutable.GetService(); - Logger.LogInfo(glInterface is { } - ? $"Renderer: {glInterface.PrimaryContext.GlInterface.Renderer}" - : "Renderer: Avalonia Software"); + if (exitCode == ExitCode.Ok && (Services.UpdateManager?.DoUpdateOnClose ?? false)) + { + Services.UpdateManager.StartInstallingNewVersion(); + } - ThemeHelper.ApplyTheme(Global.UiConfig.DarkModeEnabled ? Theme.Dark : Theme.Light); - }) - .StartWithClassicDesktopLifetime(args); - } - catch (OperationCanceledException ex) - { - Logger.LogDebug(ex); + return (int)exitCode; } catch (Exception ex) { - exceptionToReport = ex; + CrashReporter.Invoke(ex); Logger.LogCritical(ex); + return 1; } - - // Start termination/disposal of the application. - terminateService.Terminate(); - - if (exceptionToReport is { }) - { - // Trigger the CrashReport process if required. - CrashReporter.Invoke(exceptionToReport); - } - else if (Services.UpdateManager.DoUpdateOnClose) - { - Services.UpdateManager.StartInstallingNewVersion(); - } - - AppDomain.CurrentDomain.UnhandledException -= CurrentDomain_UnhandledException; - TaskScheduler.UnobservedTaskException -= TaskScheduler_UnobservedTaskException; - - Logger.LogSoftwareStopped("Wasabi"); - - return exceptionToReport is { } ? 1 : 0; - } - - /// - /// Initializes Wasabi Logger. Sets user-defined log-level, if provided. - /// - /// Start Wasabi Wallet with ./wassabee --LogLevel=trace to set . - private static void SetupLogger(string dataDir, string[] args) - { - LogLevel? logLevel = null; - - foreach (string arg in args) - { - if (arg.StartsWith("--LogLevel=")) - { - string value = arg.Split('=', count: 2)[1]; - - if (Enum.TryParse(value, ignoreCase: true, out LogLevel parsedLevel)) - { - logLevel = parsedLevel; - break; - } - } - } - - Logger.InitializeDefaults(Path.Combine(dataDir, "Logs.txt"), logLevel); - } - - private static (UiConfig uiConfig, Config config) LoadOrCreateConfigs(string dataDir) - { - Directory.CreateDirectory(dataDir); - - UiConfig uiConfig = new(Path.Combine(dataDir, "UiConfig.json")); - uiConfig.LoadFile(createIfMissing: true); - - Config config = new(Path.Combine(dataDir, "Config.json")); - config.LoadFile(createIfMissing: true); - - if (config.MigrateOldDefaultBackendUris()) - { - Logger.LogInfo("Configuration file with the new coordinator API URIs was saved."); - config.ToFile(); - } - - return (uiConfig, config); - } - - private static Global CreateGlobal(string dataDir, UiConfig uiConfig, Config config) - { - var walletManager = new WalletManager(config.Network, dataDir, new WalletDirectories(config.Network, dataDir)); - - return new Global(dataDir, config, uiConfig, walletManager); } /// /// Do not call this method it should only be called by TerminateService. /// - private static async Task TerminateApplicationAsync() - { - Logger.LogSoftwareStopped("Wasabi GUI"); - - if (Global is { } global) - { - await global.DisposeAsync().ConfigureAwait(false); - } - } - private static void TerminateApplication() { - MainViewModel.Instance.ClearStacks(); - MainViewModel.Instance.StatusIcon.Dispose(); + Dispatcher.UIThread.Post(() => (Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow?.Close()); } - private static void TaskScheduler_UnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e) + private static void LogUnobservedTaskException(object? sender, AggregateException e) { - ReadOnlyCollection innerExceptions = e.Exception.Flatten().InnerExceptions; + ReadOnlyCollection innerExceptions = e.Flatten().InnerExceptions; - if (innerExceptions.Count == 1 && innerExceptions[0] is SocketException socketException && socketException.SocketErrorCode == SocketError.OperationAborted) - { - // Until https://github.com/MetacoSA/NBitcoin/pull/1089 is resolved. - Logger.LogTrace(e.Exception); - } - else if (innerExceptions.Count == 1 && innerExceptions[0] is OperationCanceledException ex && ex.Message == "The peer has been disconnected") + switch (innerExceptions) { + case [SocketException { SocketErrorCode: SocketError.OperationAborted }]: // Source of this exception is NBitcoin library. - Logger.LogTrace(e.Exception); - } - else - { - Logger.LogDebug(e.Exception); - } - } + case [OperationCanceledException { Message: "The peer has been disconnected" }]: + // Until https://github.com/MetacoSA/NBitcoin/pull/1089 is resolved. + Logger.LogTrace(e); + break; - private static void CurrentDomain_UnhandledException(object? sender, UnhandledExceptionEventArgs e) - { - if (e.ExceptionObject is Exception ex) - { - Logger.LogWarning(ex); + default: + Logger.LogDebug(e); + break; } } + private static void LogUnhandledException(object? sender, Exception e) => + Logger.LogWarning(e); + [SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "Required to bootstrap Avalonia's Visual Previewer")] private static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure(() => new App()).UseReactiveUI().SetupAppBuilder(); @@ -276,10 +132,75 @@ private static AppBuilder BuildCrashReporterApp(SerializableException serializab } return result - .With(new Win32PlatformOptions { AllowEglInitialization = false, UseDeferredRendering = true }) - .With(new X11PlatformOptions { UseGpu = false, WmClass = "Wasabi Wallet Crash Reporting" }) - .With(new AvaloniaNativePlatformOptions { UseDeferredRendering = true, UseGpu = false }) + .With(new Win32PlatformOptions { RenderingMode = new[] { Win32RenderingMode.Software } }) + .With(new X11PlatformOptions { RenderingMode = new[] { X11RenderingMode.Software }, WmClass = "Wasabi Wallet Crash Report" }) + .With(new AvaloniaNativePlatformOptions { RenderingMode = new[] { AvaloniaNativeRenderingMode.Software } }) .With(new MacOSPlatformOptions { ShowInDock = true }) .AfterSetup(_ => ThemeHelper.ApplyTheme(Theme.Dark)); } } + +public static class WasabiAppExtensions +{ + public static async Task RunAsGuiAsync(this WasabiApplication app) + { + return await app.RunAsync( + afterStarting: () => + { + RxApp.DefaultExceptionHandler = Observer.Create(ex => + { + if (Debugger.IsAttached) + { + Debugger.Break(); + } + + Logger.LogError(ex); + + RxApp.MainThreadScheduler.Schedule(() => throw new ApplicationException("Exception has been thrown in unobserved ThrownExceptions", ex)); + }); + + Logger.LogInfo("Wasabi GUI started."); + bool runGuiInBackground = app.AppConfig.Arguments.Any(arg => arg.Contains(StartupHelper.SilentArgument)); + UiConfig uiConfig = LoadOrCreateUiConfig(Config.DataDir); + Services.Initialize(app.Global!, uiConfig, app.SingleInstanceChecker, app.TerminateService); + + using CancellationTokenSource stopLoadingCts = new(); + + AppBuilder appBuilder = AppBuilder + .Configure(() => new App( + backendInitialiseAsync: async () => + { + // macOS require that Avalonia is started with the UI thread. Hence this call must be delayed to this point. + await app.Global!.InitializeNoWalletAsync(app.TerminateService, stopLoadingCts.Token).ConfigureAwait(false); + + // Make sure that wallet startup set correctly regarding RunOnSystemStartup + await StartupHelper.ModifyStartupSettingAsync(uiConfig.RunOnSystemStartup).ConfigureAwait(false); + }, startInBg: runGuiInBackground)) + .UseReactiveUI() + .SetupAppBuilder() + .AfterSetup(_ => ThemeHelper.ApplyTheme(uiConfig.DarkModeEnabled ? Theme.Dark : Theme.Light)); + + if (app.TerminateService.CancellationToken.IsCancellationRequested) + { + Logger.LogDebug("Skip starting Avalonia UI as requested the application to stop."); + stopLoadingCts.Cancel(); + } + else + { + appBuilder.StartWithClassicDesktopLifetime(app.AppConfig.Arguments); + } + + return Task.CompletedTask; + }); + } + + private static UiConfig LoadOrCreateUiConfig(string dataDir) + { + Directory.CreateDirectory(dataDir); + + UiConfig uiConfig = new(Path.Combine(dataDir, "UiConfig.json")); + uiConfig.LoadFile(createIfMissing: true); + + return uiConfig; + } +} diff --git a/WalletWasabi.Fluent.Desktop/WalletWasabi.Fluent.Desktop.csproj b/WalletWasabi.Fluent.Desktop/WalletWasabi.Fluent.Desktop.csproj index 8e0473c199..6015aaffd3 100644 --- a/WalletWasabi.Fluent.Desktop/WalletWasabi.Fluent.Desktop.csproj +++ b/WalletWasabi.Fluent.Desktop/WalletWasabi.Fluent.Desktop.csproj @@ -34,8 +34,7 @@ MIT Wasabi Wallet Fluent Wasabi Wallet - zkSNACKs - bitcoin-wallet;privacy;bitcoin;dotnet;nbitcoin;cross-platform;zerolink;wallet;tumbler;coin;tor + bitcoin-wallet;privacy;bitcoin;dotnet;nbitcoin;cross-platform;zerolink;wallet;wabisabi;coinjoin;tor https://github.com/zkSNACKs/WalletWasabi/ https://github.com/zkSNACKs/WalletWasabi/blob/master/LICENSE.md git @@ -44,15 +43,14 @@ Assets\WasabiLogo.ico Wasabi Wallet Fluent - 0.10.19 - - + + diff --git a/WalletWasabi.Fluent.Desktop/packages.lock.json b/WalletWasabi.Fluent.Desktop/packages.lock.json index 00e9a3138a..c9792c223f 100644 --- a/WalletWasabi.Fluent.Desktop/packages.lock.json +++ b/WalletWasabi.Fluent.Desktop/packages.lock.json @@ -1,275 +1,245 @@ { - "version": 1, + "version": 2, "dependencies": { "net7.0": { "Avalonia.Desktop": { "type": "Direct", - "requested": "[0.10.19, )", - "resolved": "0.10.19", - "contentHash": "1lh31kgndZfaDAQre4jT0WrAdRGWIe+j6eeAjG6O8g1kl4s+ppzpnXH9xINd2upjKibQLMLrCc5/BWVKakZKLA==", + "requested": "[11.0.5, )", + "resolved": "11.0.5", + "contentHash": "YKgk+t42wbwsCQz/DMlLiV71jCwxN47tLRemZ1zmgclh3lf97++A3zJpxx+Cv3fGf5jJnvM1yzyeVU5HBOflJA==", "dependencies": { - "Avalonia": "0.10.19", - "Avalonia.Native": "0.10.19", - "Avalonia.Skia": "0.10.19", - "Avalonia.Win32": "0.10.19", - "Avalonia.X11": "0.10.19" + "Avalonia": "11.0.5", + "Avalonia.Native": "11.0.5", + "Avalonia.Skia": "11.0.5", + "Avalonia.Win32": "11.0.5", + "Avalonia.X11": "11.0.5" } }, "Avalonia.ReactiveUI": { "type": "Direct", - "requested": "[0.10.19, )", - "resolved": "0.10.19", - "contentHash": "JgGR6wq51hxkC9NYrg7CeXDFLROsn8IoTib8SqlH/+210VZEjzDBATiKug/7/+M5eaRa3zfyeZ32v62DYk0t4Q==", + "requested": "[11.0.5, )", + "resolved": "11.0.5", + "contentHash": "afgWhSQ6jW3ept/8VEc8jfjGMYkq0tf8LN3yKsC3VsjsU9u1+EnefrlV+7gf7MMIbFi9xb3MRrkEw4lLjG89WA==", "dependencies": { - "Avalonia": "0.10.19", - "ReactiveUI": "13.2.10", + "Avalonia": "11.0.5", + "ReactiveUI": "18.3.1", "System.Reactive": "5.0.0" } }, - "Avalonia": { - "type": "Transitive", - "resolved": "0.10.19", - "contentHash": "KgT8O+uE9sZdsjr2J5VQo+55raPeQAhdiHjSic300P3EJMsjjdjC132O0ERGRWf7OSDcskqnDstnuop0fMGhLg==", - "dependencies": { - "Avalonia.Remote.Protocol": "0.10.19", - "JetBrains.Annotations": "10.3.0", - "System.ComponentModel.Annotations": "4.5.0", - "System.Memory": "4.5.3", - "System.Reactive": "5.0.0", - "System.Runtime.CompilerServices.Unsafe": "4.6.0", - "System.ValueTuple": "4.5.0" - } - }, "Avalonia.Angle.Windows.Natives": { "type": "Transitive", - "resolved": "2.1.0.2020091801", - "contentHash": "nGsCPI8FuUknU/e6hZIqlsKRDxClXHZyztmgM8vuwslFC/BIV3LqM2wKefWbr6SORX4Lct4nivhSMkdF/TrKgg==" + "resolved": "2.1.0.2023020321", + "contentHash": "Zlkkb8ipxrxNWVPCJgMO19fpcpYPP+bpOQ+jPtCFj8v+TzVvPdnGHuyv9IMvSHhhMfEpps4m4hjaP4FORQYVAA==" }, - "Avalonia.Controls.DataGrid": { + "Avalonia.BuildServices": { "type": "Transitive", - "resolved": "0.10.19", - "contentHash": "s1xlpkkjyC2cVHI/ZY57GRvTsYVk+qPs+qQPYa88ehH5WcMyvgQjKu8SvzVjLSegMKb28NJO7b203Zu3NoDC4A==", - "dependencies": { - "Avalonia": "0.10.19", - "Avalonia.Remote.Protocol": "0.10.19", - "JetBrains.Annotations": "10.3.0", - "System.Reactive": "5.0.0" - } + "resolved": "0.0.29", + "contentHash": "U4eJLQdoDNHXtEba7MZUCwrBErBTxFp6sUewXBOdAhU0Kwzwaa/EKFcYm8kpcysjzKtfB4S0S9n0uxKZFz/ikw==" }, - "Avalonia.Controls.TreeDataGrid": { + "Avalonia.Controls.ColorPicker": { "type": "Transitive", - "resolved": "0.10.18.1", - "contentHash": "ojQoUFo9PQKO6BFvDH7C+kiw5sRnjbvDWBBkNYEeAA9djxhjf/tIhnw1pUfqmtvNhvfSUQKnt4oorKCDcg0gAw==", + "resolved": "11.0.5", + "contentHash": "N9RpqrDxyN52YSM6N6ViDWnE9XvC3bacZGlg0EH+uYOnxBZuh02kYw4UDa7S+TUdRXVc9HpmHpL3Y/sf/ydVTw==", "dependencies": { - "Avalonia": "0.10.18" + "Avalonia": "11.0.5", + "Avalonia.Remote.Protocol": "11.0.5" } }, - "Avalonia.Diagnostics": { + "Avalonia.Controls.DataGrid": { "type": "Transitive", - "resolved": "0.10.19", - "contentHash": "eV5bhAs7N6opgo5B2irNBrEpU3e+Rd+P5UtxYf7qnbHFD0i0JOTMTSUAW9heWMbj9kV97BvVPX9fP0V1rse1sw==", + "resolved": "11.0.5", + "contentHash": "5ULaodkNkChEWE/xuRrSh6dpBExpvFDFSItdX7sDOxGqNwovaKk0+4HmzvXeQGntUeMsaAcDddZxoG+pUJ8PSA==", "dependencies": { - "Avalonia": "0.10.19", - "Avalonia.Controls.DataGrid": "0.10.19", - "Microsoft.CodeAnalysis.CSharp.Scripting": "3.4.0", - "System.Reactive": "5.0.0" + "Avalonia": "11.0.5", + "Avalonia.Remote.Protocol": "11.0.5" } }, "Avalonia.FreeDesktop": { "type": "Transitive", - "resolved": "0.10.19", - "contentHash": "bBj8Sz3TBrNLWR4JyYv35T2+0xI2nP87Xma0nryELtFhb00hSdfiAEB6pXGQz4bGEDlPtDCC/g1o8RyctUHOLA==", + "resolved": "11.0.5", + "contentHash": "ChltTdFTlwrnn+3kpDD3zoBNeU4e7m2sOiff86CARUEdCaIO7d6+bmmtTishO4QGmwJOhaY0Jkjqfb4/wJmIvw==", "dependencies": { - "Avalonia": "0.10.19", - "Tmds.DBus": "0.9.0" + "Avalonia": "11.0.5", + "Tmds.DBus.Protocol": "0.15.0" } }, "Avalonia.Native": { "type": "Transitive", - "resolved": "0.10.19", - "contentHash": "KTxMewYzDJ3/BG+flb+co6yXShGhk8kh52T/9ktPh06HVsOWFGNsA3CHUDGiE5/UgCP5oim1xAef+pAzlkBvWw==", + "resolved": "11.0.5", + "contentHash": "ZrOlxU7C5FdDVmTxta/xv83KKg+HqgnmX6hGTQdEK4Yn5rMVqy0h1CcuZr7UKM4kHnbWdUhbgZ3+mXeF6f8Mug==", "dependencies": { - "Avalonia": "0.10.19" + "Avalonia": "11.0.5" } }, "Avalonia.Remote.Protocol": { "type": "Transitive", - "resolved": "0.10.19", - "contentHash": "DjwS7k4tNzvEHvqnQqO0YT/mc2Z/gpFllHqN6kr+yoVIrXOKZegvwDl3dwpD/zXXP0Q4oMcZJFn1H36ZeO0wtQ==" + "resolved": "11.0.5", + "contentHash": "UDK2jNGWaMHOP4lENIeUp7WsNAv65PuR5Yjo6EksDgN+BfS99+O9QDskrroyCnaMredOYvyposyj5Bgur8vO1w==" }, - "Avalonia.Skia": { + "Avalonia.Themes.Simple": { "type": "Transitive", - "resolved": "0.10.19", - "contentHash": "ZpZLu4qsctmzEMnkYj40b9izQBiYCT/bBC56evUgn78mOqTWtqAoPzqRg5yvLV11quRbqRNnXTXv2wGZC3njxA==", + "resolved": "11.0.5", + "contentHash": "kDRQMs0nndFdRSeDedhGOg4NL5lw/QiTJJ9CzzuRUq7M8RzJIZz8clHh1CJZira5JZDYD3lpRmhIeNafk6bNqw==", "dependencies": { - "Avalonia": "0.10.19", - "HarfBuzzSharp": "2.8.2.1-preview.108", - "HarfBuzzSharp.NativeAssets.Linux": "2.8.2.1-preview.108", - "HarfBuzzSharp.NativeAssets.WebAssembly": "2.8.2.1-preview.108", - "SkiaSharp": "2.88.1-preview.108", - "SkiaSharp.NativeAssets.Linux": "2.88.1-preview.108", - "SkiaSharp.NativeAssets.WebAssembly": "2.88.1-preview.108" + "Avalonia": "11.0.5" } }, "Avalonia.Win32": { "type": "Transitive", - "resolved": "0.10.19", - "contentHash": "fp1NLhfo/pKe/R/EnYphQ7o/G3VXp116DCynCQjIgx8UnXY78mcz28fQkp0gyMJbejCpgYbyV2ED1x5ZMa/k5g==", + "resolved": "11.0.5", + "contentHash": "jjyNomyG/hG0rxelDszIUvJ1IdwazFsAtc++I00e55DIbHMQzH/CqwUEAevpAO6sh4w5fNnyXHuwS9Kl9N4zUg==", "dependencies": { - "Avalonia": "0.10.19", - "Avalonia.Angle.Windows.Natives": "2.1.0.2020091801", - "System.Drawing.Common": "4.5.0", + "Avalonia": "11.0.5", + "Avalonia.Angle.Windows.Natives": "2.1.0.2023020321", + "System.Drawing.Common": "6.0.0", "System.Numerics.Vectors": "4.5.0" } }, "Avalonia.X11": { "type": "Transitive", - "resolved": "0.10.19", - "contentHash": "aAm0s/dGfoKO/Q0/semsjC29s26M3NP7oBg/I2pIt4d+Uag6mO/UF8WDJM7b+a0t4ohfkWj2FzWSsyjuL+HGbQ==", + "resolved": "11.0.5", + "contentHash": "FhJU/SUT2QTEhutSr8B/8w4ZZnVmmTPaHm+Y9/QiyzpS2UKz0e2lt3v8U7GKUdYpJD34ZMf6tl4zDiCnRiztgw==", "dependencies": { - "Avalonia": "0.10.19", - "Avalonia.FreeDesktop": "0.10.19", - "Avalonia.Skia": "0.10.19" + "Avalonia": "11.0.5", + "Avalonia.FreeDesktop": "11.0.5", + "Avalonia.Skia": "11.0.5" } }, - "Avalonia.Xaml.Behaviors": { + "Avalonia.Xaml.Interactions": { "type": "Transitive", - "resolved": "0.10.19", - "contentHash": "IJroLH4aHMBbf5Rz0T0Dr1f8WkKvTea8/74LbygajhgHsOo0SFsHSjzuE/f1bv6Q5WeRXZ7rmuWtMn6egwGTIw==", + "resolved": "11.0.2", + "contentHash": "rTJc6glZqJVlcWfTtL5a3kMGkTwTYeSCB6MNe/KOJqxUdp30u9toad06WnzgxYHSAo/o1rxO2UtWkZb4qTxhAA==", "dependencies": { - "Avalonia": "0.10.19", - "Avalonia.Xaml.Interactions": "0.10.19", - "Avalonia.Xaml.Interactivity": "0.10.19" + "Avalonia": "11.0.0", + "Avalonia.Xaml.Interactivity": "11.0.2" } }, - "Avalonia.Xaml.Interactions": { + "Avalonia.Xaml.Interactions.Custom": { "type": "Transitive", - "resolved": "0.10.19", - "contentHash": "ZxClXQEfxeai+cFrkKP2NzM8I5Xsju/AEE+KNG5WHGc6fpTDqb4tWmRr4lPy3lebr6YkSnTuysHLmfcoNCsXeA==", + "resolved": "11.0.2", + "contentHash": "rsvjN7y5VDF7HnfgjNYBMc904s78qUamG67httUAKMBEPJoWnxtpLJWCCO7xZSM0fhplzu4cHlbfLEz9Pn62xA==", "dependencies": { - "Avalonia": "0.10.19", - "Avalonia.Xaml.Interactivity": "0.10.19" + "Avalonia": "11.0.0", + "Avalonia.Xaml.Interactivity": "11.0.2" } }, - "Avalonia.Xaml.Interactivity": { + "Avalonia.Xaml.Interactions.DragAndDrop": { "type": "Transitive", - "resolved": "0.10.19", - "contentHash": "b0yFngiW8K0Zb75NqR9+eDH76YrENHuErmAWrcsu7D5dEUJlTgZO2oZ4OaEJd7rNNvxdXzyhHKLssB+7+ztrKQ==", + "resolved": "11.0.2", + "contentHash": "nwOdCjKqMuYB4j66osxw+XbdXVjGp0gX584atSEnZE34jMZ4zNeobnCrNkVoTT3khEPz2SNnW2uISuYEPRwCwA==", "dependencies": { - "Avalonia": "0.10.19" + "Avalonia": "11.0.0", + "Avalonia.Xaml.Interactivity": "11.0.2" } }, - "DynamicData": { + "Avalonia.Xaml.Interactions.Draggable": { "type": "Transitive", - "resolved": "7.1.1", - "contentHash": "Pc6J5bFnSxEa64PV2V67FMcLlDdpv6m+zTBKSnRN3aLon/WtWWy8kuDpHFbJlgXHtqc6Nxloj9ItuvDlvKC/8w==", + "resolved": "11.0.2", + "contentHash": "XuBaMvOny4uSBnpCmPQra0gJUUvpvM435eMIuYpsq8MednU0cgWAqAQt5OqOYdNMfQ5TO5o0UeohvZ1DpnNe5g==", "dependencies": { - "System.Reactive": "5.0.0" + "Avalonia": "11.0.0", + "Avalonia.Xaml.Interactivity": "11.0.2" } }, - "HarfBuzzSharp": { + "Avalonia.Xaml.Interactions.Events": { "type": "Transitive", - "resolved": "2.8.2.1-preview.108", - "contentHash": "vo2eE1jLvYWrfeghYAzkfHr7GNtWsay2ODfufavz8xReOZ648a2sBggSjTU02DQU5EPBSOhKxDnkqnUVWA8xkg==", + "resolved": "11.0.2", + "contentHash": "bQ4f9AaVmfFPQUO6z7LCrq+YS7YffMqb1Vrp/SfqDLAZSgtK86vWed/WS0ZwIFyRi7KhIuB1576MuBYsxqJ1ng==", "dependencies": { - "HarfBuzzSharp.NativeAssets.Win32": "2.8.2.1-preview.108", - "HarfBuzzSharp.NativeAssets.macOS": "2.8.2.1-preview.108" + "Avalonia": "11.0.0", + "Avalonia.Xaml.Interactivity": "11.0.2" } }, - "HarfBuzzSharp.NativeAssets.Linux": { + "Avalonia.Xaml.Interactions.Reactive": { "type": "Transitive", - "resolved": "2.8.2.1-preview.108", - "contentHash": "kRjP0sub39GxY7/YUoWwMAvltH+i+0+HvG6ND1v1iWAeBbAwcBFnPfT6FQDBqdnEaeYQT6y8FxMn9phOND7Kyg==", + "resolved": "11.0.2", + "contentHash": "p5EnNOaFyNDDDA/sbClEt7+30pvrgAsPw6DtLh4jezqKAFewJ4adIgNPlZ/4u+oPHWFE3/MbaviAJsz5+9nusg==", "dependencies": { - "HarfBuzzSharp": "2.8.2.1-preview.108" + "Avalonia": "11.0.0", + "Avalonia.Xaml.Interactivity": "11.0.2", + "System.Reactive": "5.0.0" } }, - "HarfBuzzSharp.NativeAssets.macOS": { - "type": "Transitive", - "resolved": "2.8.2.1-preview.108", - "contentHash": "pDw8R6ndu8usa9unSqEZrl3RbUNw2AzqAkcJTkocA15dxBpHvaaVKqgEozTLfye0/l5s0YgYAb4WpcY4qBg6Pw==" - }, - "HarfBuzzSharp.NativeAssets.WebAssembly": { - "type": "Transitive", - "resolved": "2.8.2.1-preview.108", - "contentHash": "BSgvS7jHt/UMoFRpVNxLcQhPFbNN/KRt/ntKH5Jo64gCpLwBzRF8Pv2mzKI2xQ3KKp+x/n1e6MAug3umls+wUA==" - }, - "HarfBuzzSharp.NativeAssets.Win32": { + "Avalonia.Xaml.Interactions.Responsive": { "type": "Transitive", - "resolved": "2.8.2.1-preview.108", - "contentHash": "0ws24k21iRH2GRiOLEcG6ESl+VROOwaeHnC0vqKQChGmreGTJ//JBQJqIu189oY30G0NVdypDe1UwFA/scjBAw==" + "resolved": "11.0.2", + "contentHash": "Medu+aLRQAcGw5fupuJp3TB18dAQNmr5CVJEQNDBfNQW2YxluDkCAfNESsLFvQpUCud5t3vCmBgC7mBFxvVvgA==", + "dependencies": { + "Avalonia": "11.0.0", + "Avalonia.Xaml.Interactivity": "11.0.2" + } }, - "JetBrains.Annotations": { + "Avalonia.Xaml.Interactivity": { "type": "Transitive", - "resolved": "10.3.0", - "contentHash": "0GLU9lwGVXjUNlr9ZIdAgjqLI2Zm/XFGJFaqJ1T1sU+kwfeMLhm68+rblUrNUP9psRl4i8yM7Ghb4ia4oI2E5g==", + "resolved": "11.0.2", + "contentHash": "8uBANon/DMvm4ywXkKHWPWL59uj9u3753NbLx8nK0vXDOHnmlRgaXlhDG0/zlpGIgFkYhM6IgCP6q31kJxnWhQ==", "dependencies": { - "System.Runtime": "4.1.0" + "Avalonia": "11.0.0" } }, - "Microsoft.AspNetCore.JsonPatch": { + "HarfBuzzSharp": { "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "svHQiUvLNdI2nac68WNQHNo/ZWyavFpt3Oip09QRnWeFqG9iyakKiNLavXr6KE8y7KxEXZNld96KQYbKz8SJMQ==", + "resolved": "2.8.2.3", + "contentHash": "8MwXm9J4dXHuTdzPo29nHgDbt4+6P+RrPrH/qrxcERf29cpLlFbjvP3eFPwHmdUrl4KL2SHEZi2ZuQ5ndeIL1w==", "dependencies": { - "Microsoft.CSharp": "4.7.0", - "Newtonsoft.Json": "13.0.1" + "HarfBuzzSharp.NativeAssets.Win32": "2.8.2.3", + "HarfBuzzSharp.NativeAssets.macOS": "2.8.2.3" } }, - "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { + "HarfBuzzSharp.NativeAssets.Linux": { "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "IJOsB1cm6FYGXxhlNoWR6zZYFREEBzeFX76NlBGhrZ7+VMK4piLm3fAgUBliasyEUg5MOOqFz5EGv8nmU5rXWQ==", + "resolved": "2.8.2.3", + "contentHash": "Qu1yJSHEN7PD3+fqfkaClnORWN5e2xJ2Xoziz/GUi/oBT1Z+Dp2oZeiONKP6NFltboSOBkvH90QuOA6YN/U1zg==", "dependencies": { - "Microsoft.AspNetCore.JsonPatch": "7.0.0", - "Newtonsoft.Json": "13.0.1", - "Newtonsoft.Json.Bson": "1.0.2" + "HarfBuzzSharp": "2.8.2.3" } }, - "Microsoft.CodeAnalysis.Analyzers": { + "HarfBuzzSharp.NativeAssets.macOS": { "type": "Transitive", - "resolved": "2.9.6", - "contentHash": "Kmms3TxGQMNb95Cu/3K+0bIcMnV4qf/phZBLAB0HUi65rBPxP4JO3aM2LoAcb+DFS600RQJMZ7ZLyYDTbLwJOQ==" + "resolved": "2.8.2.3", + "contentHash": "uwz9pB3hMuxzI/bSkjVrsOJH7Wo1L+0Md5ZmEMDM/j7xDHtR9d3mfg/CfxhMIcTiUC4JgX49FZK0y2ojgu1dww==" }, - "Microsoft.CodeAnalysis.Common": { + "HarfBuzzSharp.NativeAssets.WebAssembly": { "type": "Transitive", - "resolved": "3.4.0", - "contentHash": "3ncA7cV+iXGA1VYwe2UEZXcvWyZSlbexWjM9AvocP7sik5UD93qt9Hq0fMRGk0jFRmvmE4T2g+bGfXiBVZEhLw==", - "dependencies": { - "Microsoft.CodeAnalysis.Analyzers": "2.9.6", - "System.Collections.Immutable": "1.5.0", - "System.Memory": "4.5.3", - "System.Reflection.Metadata": "1.6.0", - "System.Runtime.CompilerServices.Unsafe": "4.5.2", - "System.Text.Encoding.CodePages": "4.5.1", - "System.Threading.Tasks.Extensions": "4.5.3" - } + "resolved": "2.8.2.3", + "contentHash": "a6t2X1GrZDt3ErjFbG+qXdxaO8EvMMUN1AVZYfayh7EACHU3yU/SG/rveKLWhT8Ln5GFLqe2r+5dsDrHK1qScw==" }, - "Microsoft.CodeAnalysis.CSharp": { + "HarfBuzzSharp.NativeAssets.Win32": { "type": "Transitive", - "resolved": "3.4.0", - "contentHash": "/LsTtgcMN6Tu1oo7/WYbRAHL4/ubXC/miEakwTpcZKJKtFo7D0AK95Hw0dbGxul6C8WJu60v6NP2435TDYZM+Q==", + "resolved": "2.8.2.3", + "contentHash": "Wo6QpE4+a+PFVdfIBoLkLr4wq2uC0m9TZC8FAfy4ZnLsUc10WL0Egk9EBHHhDCeokNOXDse5YtvuTYtS/rbHfg==" + }, + "MicroCom.Runtime": { + "type": "Transitive", + "resolved": "0.11.0", + "contentHash": "MEnrZ3UIiH40hjzMDsxrTyi8dtqB5ziv3iBeeU4bXsL/7NLSal9F1lZKpK+tfBRnUoDSdtcW3KufE4yhATOMCA==" + }, + "Microsoft.AspNetCore.JsonPatch": { + "type": "Transitive", + "resolved": "7.0.9", + "contentHash": "6iMRtYIQZj7gMC7iVotL9bZjCjnbV2ZkAAduKYHfV6v+WQhEjk0iEGSFNVh6N9rTCNTeZ2xVgv3xi675GwyDzQ==", "dependencies": { - "Microsoft.CodeAnalysis.Common": "[3.4.0]" + "Microsoft.CSharp": "4.7.0", + "Newtonsoft.Json": "13.0.1" } }, "Microsoft.CodeAnalysis.CSharp.Scripting": { "type": "Transitive", - "resolved": "3.4.0", - "contentHash": "tLgqc76qXHmONUhWhxo7z3TcL/LmGFWIUJm1exbQmVJohuQvJnejUMxmVkdxDfMuMZU1fIyJXPZ6Fkp4FEneAg==", + "resolved": "3.8.0", + "contentHash": "+XVKzByNigzzvl7rGwpzFrkUbbekNUwdMW3EghcxmNRZd9aamNXxes3I/U0tYx1LTeHEQ5y/nzb7SiEmXBmzEA==", "dependencies": { "Microsoft.CSharp": "4.3.0", - "Microsoft.CodeAnalysis.CSharp": "[3.4.0]", - "Microsoft.CodeAnalysis.Common": "[3.4.0]", - "Microsoft.CodeAnalysis.Scripting.Common": "[3.4.0]" + "Microsoft.CodeAnalysis.CSharp": "[3.8.0]", + "Microsoft.CodeAnalysis.Common": "[3.8.0]", + "Microsoft.CodeAnalysis.Scripting.Common": "[3.8.0]" } }, "Microsoft.CodeAnalysis.Scripting.Common": { "type": "Transitive", - "resolved": "3.4.0", - "contentHash": "+b6I3DZL2zvck+B/E/aiOveakj5U2G2BcYODQxcGh2IDbatNU3XXxGT1HumkWB5uIZI2Leu0opBgBpjScmjGMA==", + "resolved": "3.8.0", + "contentHash": "lR8Mxg/4tnwzFyqJOD7wBoXbyDKEaMxRc0E9UWtHNGBiL1qBdYyVhXPmiOPUL44tUJeQwCOHAr554jRHGBQIcw==", "dependencies": { - "Microsoft.CodeAnalysis.Common": "[3.4.0]" + "Microsoft.CodeAnalysis.Common": "[3.8.0]" } }, "Microsoft.CSharp": { @@ -277,6 +247,14 @@ "resolved": "4.7.0", "contentHash": "pTj+D3uJWyN3My70i2Hqo+OXixq3Os2D1nJ2x92FFo6sk8fYS1m1WLNTs0Dc1uPaViH0YvEEwvzddQ7y4rhXmA==" }, + "Microsoft.Data.Sqlite.Core": { + "type": "Transitive", + "resolved": "7.0.9", + "contentHash": "ow2PPoeW0yFc7NhexacQUw/LVjkO1mLK3VZAxhVIVjmQWlgYl/4mo9/U7uz+z75I+ZN6LUvq9M0ftU3IE75Ilg==", + "dependencies": { + "SQLitePCLRaw.core": "2.1.4" + } + }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", "resolved": "1.0.0", @@ -303,20 +281,6 @@ "resolved": "1.1.3", "contentHash": "3Wrmi0kJDzClwAC+iBdUBpEKmEle8FQNsCs77fkiOIw/9oYA07bL1EZNX0kQ2OMN3xpwvl0vAtOCYY3ndDNlhQ==" }, - "Microsoft.Win32.SystemEvents": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "2nXPrhdAyAzir0gLl8Yy8S5Mnm/uBSQQA7jEsILOS1MTyS7DbmV1NgViMtvV1sfCD1ebITpNwb1NIinKeJgUVQ==" - }, - "NBitcoin": { - "type": "Transitive", - "resolved": "7.0.24", - "contentHash": "+K8o9WH09/o8oTl0aV/IR2y+1leR7e1vvZ2S6A7IozvMsWGh/Wi3TYWhasAskEYryQJr2f4gQsy67eAO7YExAg==", - "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "1.0.0", - "Newtonsoft.Json": "13.0.1" - } - }, "NBitcoin.Secp256k1": { "type": "Transitive", "resolved": "3.1.0", @@ -337,51 +301,79 @@ }, "ReactiveUI": { "type": "Transitive", - "resolved": "13.2.10", - "contentHash": "fOCbEZ+RsO2Jhv6vB8VX+ZEvczYJaC95atcSG7oXohJeL/sEwbbqvv9k+tbj2l4bRSj2j5CQvhwA3HNLaxlCAg==", + "resolved": "18.3.1", + "contentHash": "0tclGtjrRPfA2gbjiM7O3DeNmo6/TpDn7CMN6jgzDrbgrnysM7oEzjGEeXbtXaOxH6kEf6RiMKWobZoSgbBXhQ==", "dependencies": { - "DynamicData": "7.1.1", - "Splat": "10.0.1", - "System.Reactive": "5.0.0", - "System.Runtime.Serialization.Primitives": "4.3.0" + "DynamicData": "7.9.5", + "Splat": "14.4.1" } }, "SkiaSharp": { "type": "Transitive", - "resolved": "2.88.1-preview.108", - "contentHash": "Zfs4qdQuvLsdSdBa42CnD8Dlcnkr46GaaFEwouzrjOLse8DmKkf/zBaCFCUkNIjGDZFkjFGe/ai5qHYkMcXIsg==", + "resolved": "2.88.6", + "contentHash": "wdfeBAQrEQCbJIRgAiargzP1Uy+0grZiG4CSgBnhAgcJTsPzlifIaO73JRdwIlT3TyBoeU9jEqzwFUhl4hTYnQ==", "dependencies": { - "SkiaSharp.NativeAssets.Win32": "2.88.1-preview.108", - "SkiaSharp.NativeAssets.macOS": "2.88.1-preview.108" + "SkiaSharp.NativeAssets.Win32": "2.88.6", + "SkiaSharp.NativeAssets.macOS": "2.88.6" } }, "SkiaSharp.NativeAssets.Linux": { "type": "Transitive", - "resolved": "2.88.1-preview.108", - "contentHash": "1aOmUqcuzXJP0FaDL5JPRx7FbLFbiyl5R2lI1YwTTfXTpawnPxpPXlBClj+CuRrSS5Azfn8k3ZIHPHTd37vOWw==", + "resolved": "2.88.6", + "contentHash": "iQcOUE0tPZvBUxOdZaP3LIdAC21H8BEMhDvpCQ/mUUvbKGLd5rF7veJVSZBNu20SuCC0oZpEdGxB+mLVOK8uzw==", "dependencies": { - "SkiaSharp": "2.88.1-preview.108" + "SkiaSharp": "2.88.6" } }, "SkiaSharp.NativeAssets.macOS": { "type": "Transitive", - "resolved": "2.88.1-preview.108", - "contentHash": "nz+Ege0i1aCicLnaHOBzuTBj5LnLxlZVxLv+wUEtOXaAHq6of7kxaE+/+4KC1OBnKs64L8WDGf88VC2fIC/zxw==" + "resolved": "2.88.6", + "contentHash": "Sko9LFxRXSjb3OGh5/RxrVRXxYo48tr5NKuuSy6jB85GrYt8WRqVY1iLOLwtjPiVAt4cp+pyD4i30azanS64dw==" }, "SkiaSharp.NativeAssets.WebAssembly": { "type": "Transitive", - "resolved": "2.88.1-preview.108", - "contentHash": "mVXV6XulqCZ5eXzWhLAdhl1CWvaYnCJEusADuS0WZ3CdzgPZl8gqfyRzM3KMrMfkaJVh/L4n3VVDnbxQw5YSvA==" + "resolved": "2.88.6", + "contentHash": "pye92IhbHq3uqxrU/I+LdkIRAyWfiUNeJ5IIAmYWt2DQPOU44Uh1nTIcjQ2ghRIFWq62VVUJJy5saLBcQO5zyw==" }, "SkiaSharp.NativeAssets.Win32": { "type": "Transitive", - "resolved": "2.88.1-preview.108", - "contentHash": "98r2fGVjPNjIhH0ooHtvAcqsHUjWZPEkqrfpynZNWdo8gkUPZhENvOodDtvBNUW6we24Bo4aWCnGbJuhyn//ug==" + "resolved": "2.88.6", + "contentHash": "7TzFO0u/g2MpQsTty4fyCDdMcfcWI+aLswwfnYXr3gtNS6VLKdMXPMeKpJa3pJSLnUBN6wD0JjuCe8OoLBQ6cQ==" }, "Splat": { "type": "Transitive", - "resolved": "10.0.1", - "contentHash": "N8BMGVuUBnVLAHSVbna/st8XiLd8ulF3BfkKUSGCPqYpDCis3ELvM+aFaZQLBUIBEcweCYVLq3HFEBqHkCKFyA==" + "resolved": "14.4.1", + "contentHash": "Z1Mncnzm9pNIaIbZ/EWH6x5ESnKsmAvu8HP4StBRw+yhz0lzE7LCbt22TNTPaFrYLYbYCbGQIc/61yuSnpLidg==" + }, + "SQLitePCLRaw.bundle_e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.4", + "contentHash": "EWI1olKDjFEBMJu0+3wuxwziIAdWDVMYLhuZ3Qs84rrz+DHwD00RzWPZCa+bLnHCf3oJwuFZIRsHT5p236QXww==", + "dependencies": { + "SQLitePCLRaw.lib.e_sqlite3": "2.1.4", + "SQLitePCLRaw.provider.e_sqlite3": "2.1.4" + } + }, + "SQLitePCLRaw.core": { + "type": "Transitive", + "resolved": "2.1.4", + "contentHash": "inBjvSHo9UDKneGNzfUfDjK08JzlcIhn1+SP5Y3m6cgXpCxXKCJDy6Mka7LpgSV+UZmKSnC8rTwB0SQ0xKu5pA==", + "dependencies": { + "System.Memory": "4.5.3" + } + }, + "SQLitePCLRaw.lib.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.4", + "contentHash": "2C9Q9eX7CPLveJA0rIhf9RXAvu+7nWZu1A2MdG6SD/NOu26TakGgL1nsbc0JAspGijFOo3HoN79xrx8a368fBg==" + }, + "SQLitePCLRaw.provider.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.4", + "contentHash": "CSlb5dUp1FMIkez9Iv5EXzpeq7rHryVNqwJMWnpq87j9zWZexaEMdisDktMsnnrzKM6ahNrsTkjqNodTBPBxtQ==", + "dependencies": { + "SQLitePCLRaw.core": "2.1.4" + } }, "System.Collections": { "type": "Transitive", @@ -412,8 +404,8 @@ }, "System.Collections.Immutable": { "type": "Transitive", - "resolved": "1.5.0", - "contentHash": "EXKiDFsChZW0RjrZ4FYHu9aW6+P4MCgEDCklsVseRfhoO0F+dXeMSsMRAlVXIo06kGJ/zv+2w1a2uc2+kxxSaQ==" + "resolved": "5.0.0", + "contentHash": "FXkLXiK0sVVewcso0imKQoOxjoPAj42R8HtjjbSjVPAzwDfzoyoznWxgA3c38LDbN9SJux1xXoXYAhz98j7r2g==" }, "System.ComponentModel.Annotations": { "type": "Transitive", @@ -442,34 +434,39 @@ }, "System.Drawing.Common": { "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "KIX+oBU38pxkKPxvLcLfIkOV5Ien8ReN78wro7OF5/erwcmortzeFx+iBswlh2Vz6gVne0khocQudGwaO1Ey6A==", + "resolved": "6.0.0", + "contentHash": "NfuoKUiP2nUWwKZN6twGqXioIe1zVD0RIj2t976A+czLHr2nY454RwwXs6JU9Htc6mwqL6Dn/nEL3dpVf2jOhg==", "dependencies": { - "Microsoft.Win32.SystemEvents": "7.0.0" + "Microsoft.Win32.SystemEvents": "6.0.0" } }, "System.Globalization": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==", + "resolved": "4.0.11", + "contentHash": "B95h0YLEL2oSnwF/XjqSWKnwKOy/01VWkNlsCeMTFJLLabflpGV26nK164eRs5GiaRSBGpOxQ3pKoSnnyZN5pg==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0" } }, "System.IO": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==", + "resolved": "4.1.0", + "contentHash": "3KlTJceQc3gnGIaHZ7UBZO26SHL1SHE4ddrmiwumFnId+CEHP+O8r386tZKaE6zlk5/mF8vifMBzHj9SaXN+mQ==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading.Tasks": "4.3.0" + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "System.Text.Encoding": "4.0.11", + "System.Threading.Tasks": "4.0.11" } }, + "System.IO.Pipelines": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "mXX66shZ4xLlI3vNLaJ0lt8OIZdmXTvIqXRdQX5HLVGSkLhINLsVhyZuX2UdRFnOGkqnwmMUs40pIIQ7mna4+A==" + }, "System.Linq": { "type": "Transitive", "resolved": "4.1.0", @@ -484,8 +481,8 @@ }, "System.Memory": { "type": "Transitive", - "resolved": "4.5.3", - "contentHash": "3oDzvc/zzetpTKWMShs1AADwZjQ/36HnsufHRPcOjyRAAMLDlu2iD33MBI2opxnezcVUtXyqDXXjoFMOU9c7SA==" + "resolved": "4.5.4", + "contentHash": "1MbJTHS1lZ4bS4FmsJjnuGJOu88ZzTT2rLvrhW7Ygic+pC0NWA+3hgAen0HRdsocuQXCkUTdFn9yHJJhsijDXw==" }, "System.Numerics.Vectors": { "type": "Transitive", @@ -494,66 +491,52 @@ }, "System.Reactive": { "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "erBZjkQHWL9jpasCE/0qKAryzVBJFxGHVBAvgRN1bzM0q2s1S4oYREEEL0Vb+1kA/6BKb5FjUZMp5VXmy+gzkQ==" + "resolved": "6.0.0", + "contentHash": "31kfaW4ZupZzPsI5PVe77VhnvFF55qgma7KZr/E0iFTs6fmdhhG8j0mgEx620iLTey1EynOkEfnyTjtNEpJzGw==" }, "System.Reflection": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==", + "resolved": "4.1.0", + "contentHash": "JCKANJ0TI7kzoQzuwB/OoJANy1Lg338B6+JVacPl4TpUwi3cReg3nMLplMq2uqYfHFQpKIlHAUVAJlImZz/4ng==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.IO": "4.3.0", - "System.Reflection.Primitives": "4.3.0", - "System.Runtime": "4.3.0" + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.IO": "4.1.0", + "System.Reflection.Primitives": "4.0.1", + "System.Runtime": "4.1.0" } }, - "System.Reflection.Emit": { - "type": "Transitive", - "resolved": "4.7.0", - "contentHash": "VR4kk8XLKebQ4MZuKuIni/7oh+QGFmZW3qORd1GvBq/8026OpW501SzT/oypwiQl4TvT8ErnReh/NzY9u+C6wQ==" - }, "System.Reflection.Metadata": { "type": "Transitive", - "resolved": "1.6.0", - "contentHash": "COC1aiAJjCoA5GBF+QKL2uLqEBew4JsCkQmoHKbN3TlOZKa2fKLz5CpiRQKDz0RsAOEGsVKqOD5bomsXq/4STQ==" + "resolved": "5.0.0", + "contentHash": "5NecZgXktdGg34rh1OenY1rFNDCI8xSjFr+Z4OU4cU06AQHUdRnIIEeWENu3Wl4YowbzkymAIMvi3WyK9U53pQ==" }, "System.Reflection.Primitives": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==", + "resolved": "4.0.1", + "contentHash": "4inTox4wTBaDhB7V3mPvp9XlCbeGYWVEM9/fXALd52vNEAVisc1BoVWQPuUuD0Ga//dNbA/WeMy9u9mzLxGTHQ==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0" } }, "System.Resources.ResourceManager": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "/zrcPkkWdZmI4F92gL/TPumP98AVDu/Wxr3CSJGQQ+XN6wbRZcyfSKVoPo17ilb3iOr0cCRqJInGwNMolqhS8A==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Globalization": "4.3.0", - "System.Reflection": "4.3.0", - "System.Runtime": "4.3.0" - } - }, - "System.Runtime": { - "type": "Transitive", - "resolved": "4.3.1", - "contentHash": "abhfv1dTK6NXOmu4bgHIONxHyEqFjW8HwXPmpY9gmll+ix9UNo4XDcmzJn6oLooftxNssVHdJC1pGT9jkSynQg==", + "resolved": "4.0.1", + "contentHash": "TxwVeUNoTgUOdQ09gfTjvW411MF+w9MBYL7AtNVc+HtBCFlutPLhUCdZjNkjbhj3bNQWMdHboF0KIWEOjJssbA==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.1", - "Microsoft.NETCore.Targets": "1.1.3" + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Globalization": "4.0.11", + "System.Reflection": "4.1.0", + "System.Runtime": "4.1.0" } }, "System.Runtime.CompilerServices.Unsafe": { "type": "Transitive", - "resolved": "4.6.0", - "contentHash": "HxozeSlipUK7dAroTYwIcGwKDeOVpQnJlpVaOkBz7CM4TsE5b/tKlQBZecTjh6FzcSbxndYaxxpsBMz+wMJeyw==" + "resolved": "4.7.1", + "contentHash": "zOHkQmzPCn5zm/BH+cxC1XbUS3P4Yoi3xzW7eRgVpDR2tPGSzyMZ17Ig1iRkfJuY0nhxkQQde8pgePNiA7z7TQ==" }, "System.Runtime.Extensions": { "type": "Transitive", @@ -588,28 +571,14 @@ "System.Runtime.Handles": "4.0.1" } }, - "System.Runtime.Serialization.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "Wz+0KOukJGAlXjtKr+5Xpuxf8+c8739RI1C+A2BoQZT+wMCCoMDDdO8/4IRHfaVINqL78GO8dW8G2lW/e45Mcw==", - "dependencies": { - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0" - } - }, - "System.Security.Principal.Windows": { - "type": "Transitive", - "resolved": "4.7.0", - "contentHash": "ojD0PX0XhneCsUbAZVKdb7h/70vyYMDYs85lwEI+LngEONe/17A0cFaRFqZU+sOEidcVswYWikYOQ9PPfjlbtQ==" - }, "System.Text.Encoding": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==", + "resolved": "4.0.11", + "contentHash": "U3gGeMlDZXxCEiY4DwVLSacg+DFWCvoiX+JThA/rvw37Sqrku7sEFeVBBBMBnfB6FeZHsyDx85HlKL19x0HtZA==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0" } }, "System.Text.Encoding.CodePages": { @@ -632,101 +601,282 @@ }, "System.Threading.Tasks": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==", + "resolved": "4.0.11", + "contentHash": "k1S4Gc6IGwtHGT8188RSeGaX86Qw/wnrgNLshJvsdNUOPP9etMmo8S07c+UlOAx4K/xLuN9ivA1bD0LVurtIxQ==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0" } }, "System.Threading.Tasks.Extensions": { "type": "Transitive", - "resolved": "4.5.3", - "contentHash": "+MvhNtcvIbqmhANyKu91jQnvIRVSTiaOiFNfKWwXGHG48YAb4I/TyH8spsySiPYla7gKal5ZnF3teJqZAximyQ==" - }, - "System.ValueTuple": { - "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "okurQJO6NRE/apDIP23ajJ0hpiNmJ+f0BwOlB/cSqTLQlw5upkf+5+96+iG2Jw40G1fCVCyPz/FhIABUjMR+RQ==" - }, - "Tmds.DBus": { - "type": "Transitive", - "resolved": "0.9.0", - "contentHash": "KcTWL9aKuob9Qo2sOTTKFePs1rKGTwZrcBvMFuGVIVR5RojX3oIFj5UBLYfSGjYgrcImC7LjQI3DdCFwUnhNXw==", - "dependencies": { - "System.Reflection.Emit": "4.7.0", - "System.Security.Principal.Windows": "4.7.0" - } + "resolved": "4.5.4", + "contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==" }, - "WabiSabi": { + "Tmds.DBus.Protocol": { "type": "Transitive", - "resolved": "1.0.1.2", - "contentHash": "e+pMZGVEfWQvkpZHAydGv6grY71urfO47lodjXC9eWtfSFvNtPWjrgqck9O24yIbXhP4K3QrJKzJQFGpAp8rqg==", + "resolved": "0.15.0", + "contentHash": "QVo/Y39nTYcCKBqrZuwHjXdwaky0yTQPIT3qUTEEK2MZfDtZWrJ2XyZ59zH8LBgB2fL5cWaTuP2pBTpGz/GeDQ==", "dependencies": { - "NBitcoin.Secp256k1": "3.1.0" + "System.IO.Pipelines": "6.0.0" } }, "walletwasabi": { "type": "Project", "dependencies": { - "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[7.0.0, )", + "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[7.0.9, )", + "Microsoft.Data.Sqlite": "[7.0.9, )", "Microsoft.Win32.SystemEvents": "[7.0.0, )", - "NBitcoin": "[7.0.24, )", + "NBitcoin": "[7.0.27, )", "WabiSabi": "[1.0.1.2, )" } }, "walletwasabi.fluent": { "type": "Project", "dependencies": { - "Avalonia": "[0.10.19, )", - "Avalonia.Controls.TreeDataGrid": "[0.10.18.1, )", - "Avalonia.Diagnostics": "[0.10.19, )", - "Avalonia.ReactiveUI": "[0.10.19, )", - "Avalonia.Skia": "[0.10.19, )", - "Avalonia.Xaml.Behaviors": "[0.10.19, )", - "System.Drawing.Common": "[7.0.0, )", + "Avalonia": "[11.0.5, )", + "Avalonia.Controls.TreeDataGrid": "[11.0.1, )", + "Avalonia.Diagnostics": "[11.0.5, )", + "Avalonia.Fonts.Inter": "[11.0.5, )", + "Avalonia.ReactiveUI": "[11.0.5, )", + "Avalonia.Skia": "[11.0.5, )", + "Avalonia.Themes.Fluent": "[11.0.5, )", + "Avalonia.Xaml.Behaviors": "[11.0.2, )", + "DynamicData": "[8.1.1, )", + "QRackers": "[1.1.0, )", "System.Runtime": "[4.3.1, )", + "Wasabi Wallet Daemon": "[1.0.0, )" + } + }, + "Wasabi Wallet Daemon": { + "type": "Project", + "dependencies": { "WalletWasabi": "[1.0.0, )" } + }, + "Avalonia": { + "type": "CentralTransitive", + "requested": "[11.0.5, )", + "resolved": "11.0.5", + "contentHash": "twUjGl6gxQeyxO7wG6v+ntvAN2IeNXDr2oS6a7h5LRXy83ITbcuA0gYUqm/aeLVe0cviGSVWE9x5BVkDjZfXpQ==", + "dependencies": { + "Avalonia.BuildServices": "0.0.29", + "Avalonia.Remote.Protocol": "11.0.5", + "MicroCom.Runtime": "0.11.0", + "System.ComponentModel.Annotations": "4.5.0" + } + }, + "Avalonia.Controls.TreeDataGrid": { + "type": "CentralTransitive", + "requested": "[11.0.1, )", + "resolved": "11.0.1", + "contentHash": "ePb6ASgu44chr1wA1tvIUQdHCgtyHdOiA9zvAVZkQ6ly7PtjjSWtqdaxF3mGPnUYt2ieaK8qIUEbxQkCjQHqyg==", + "dependencies": { + "Avalonia": "11.0.0", + "System.Reactive": "5.0.0" + } + }, + "Avalonia.Diagnostics": { + "type": "CentralTransitive", + "requested": "[11.0.5, )", + "resolved": "11.0.5", + "contentHash": "RoG+0sUlyoOlhAFc2rpkQzmJN7ztTqWqtB5mEZav3JacFLQ5npBrlLbcj9ewj2RQa+9zLiA9JmOlhK5zFprCnw==", + "dependencies": { + "Avalonia": "11.0.5", + "Avalonia.Controls.ColorPicker": "11.0.5", + "Avalonia.Controls.DataGrid": "11.0.5", + "Avalonia.Themes.Simple": "11.0.5", + "Microsoft.CodeAnalysis.CSharp.Scripting": "3.8.0", + "Microsoft.CodeAnalysis.Common": "3.8.0" + } + }, + "Avalonia.Fonts.Inter": { + "type": "CentralTransitive", + "requested": "[11.0.5, )", + "resolved": "11.0.5", + "contentHash": "ZvEYK9+U1S13Mu02qtfo/Xi30xsXa7QkeAyKr+FDCxSogJjugywXFe+pdRJMQIrPupp6oQuPVM0YSJqn3+apvg==", + "dependencies": { + "Avalonia": "11.0.5" + } + }, + "Avalonia.Skia": { + "type": "CentralTransitive", + "requested": "[11.0.5, )", + "resolved": "11.0.5", + "contentHash": "1t3yR1t0HOm0jITpn7+Wb2XUlwhbHPTr3i4ZrgYLKmc68fcBgBnQutJNjzLW3Iq8uWB8ymTeB3sKiD/NVkWFNw==", + "dependencies": { + "Avalonia": "11.0.5", + "HarfBuzzSharp": "2.8.2.3", + "HarfBuzzSharp.NativeAssets.Linux": "2.8.2.3", + "HarfBuzzSharp.NativeAssets.WebAssembly": "2.8.2.3", + "SkiaSharp": "2.88.6", + "SkiaSharp.NativeAssets.Linux": "2.88.6", + "SkiaSharp.NativeAssets.WebAssembly": "2.88.6" + } + }, + "Avalonia.Themes.Fluent": { + "type": "CentralTransitive", + "requested": "[11.0.5, )", + "resolved": "11.0.5", + "contentHash": "/sEz05Hiu20OuUm5e2LSn/Hnzz4OvnG7jLA+NvWMIBBzdaz0a7Kr4QqDJmcpyK6x/a3l+xw4HDpWxMsn3nSBGg==", + "dependencies": { + "Avalonia": "11.0.5" + } + }, + "Avalonia.Xaml.Behaviors": { + "type": "CentralTransitive", + "requested": "[11.0.2, )", + "resolved": "11.0.2", + "contentHash": "eo1hrOYoJ6xiYg5/CAKYS5UFM9jr3TI7nokUSNKaGuUHXR7wwtRi3QE2dx29b5JamwhuCcA+oarEy53FCMvx9Q==", + "dependencies": { + "Avalonia": "11.0.0", + "Avalonia.Xaml.Interactions": "11.0.2", + "Avalonia.Xaml.Interactions.Custom": "11.0.2", + "Avalonia.Xaml.Interactions.DragAndDrop": "11.0.2", + "Avalonia.Xaml.Interactions.Draggable": "11.0.2", + "Avalonia.Xaml.Interactions.Events": "11.0.2", + "Avalonia.Xaml.Interactions.Reactive": "11.0.2", + "Avalonia.Xaml.Interactions.Responsive": "11.0.2", + "Avalonia.Xaml.Interactivity": "11.0.2" + } + }, + "DynamicData": { + "type": "CentralTransitive", + "requested": "[8.1.1, )", + "resolved": "8.1.1", + "contentHash": "NYTTP6lJudweNrIQgIM67/KzFJIp20NK+GVInqrvxdCB9JMHsihqOYplyDGuOCZbMeZXVskj8bpXZfspsdm45w==", + "dependencies": { + "System.Reactive": "6.0.0" + } + }, + "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { + "type": "CentralTransitive", + "requested": "[7.0.9, )", + "resolved": "7.0.9", + "contentHash": "dhAFLGV3RfK6BAbLYpTKcVch1hcyP2qDWNy7Pk2wGrQEO/yWbWwiR9c13hk5kGWcPMGeVMkcuftUo6OAHe2yIA==", + "dependencies": { + "Microsoft.AspNetCore.JsonPatch": "7.0.9", + "Newtonsoft.Json": "13.0.1", + "Newtonsoft.Json.Bson": "1.0.2" + } + }, + "Microsoft.CodeAnalysis.Analyzers": { + "type": "CentralTransitive", + "requested": "[3.3.4, )", + "resolved": "3.0.0", + "contentHash": "ojG5pGAhTPmjxRGTNvuszO3H8XPZqksDwr9xLd4Ae/JBjZZdl6GuoLk7uLMf+o7yl5wO0TAqoWcEKkEWqrZE5g==" + }, + "Microsoft.CodeAnalysis.Common": { + "type": "CentralTransitive", + "requested": "[4.6.0, )", + "resolved": "3.8.0", + "contentHash": "8YTZ7GpsbTdC08DITx7/kwV0k4SC6cbBAFqc13cOm5vKJZcEIAh51tNSyGSkWisMgYCr96B2wb5Zri1bsla3+g==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.0.0", + "System.Collections.Immutable": "5.0.0", + "System.Memory": "4.5.4", + "System.Reflection.Metadata": "5.0.0", + "System.Runtime.CompilerServices.Unsafe": "4.7.1", + "System.Text.Encoding.CodePages": "4.5.1", + "System.Threading.Tasks.Extensions": "4.5.4" + } + }, + "Microsoft.CodeAnalysis.CSharp": { + "type": "CentralTransitive", + "requested": "[4.6.0, )", + "resolved": "3.8.0", + "contentHash": "hKqFCUSk9TIMBDjiYMF8/ZfK9p9mzpU+slM73CaCHu4ctfkoqJGHLQhyT8wvrYsIg+ufrUWBF8hcJYmyr5rc5Q==", + "dependencies": { + "Microsoft.CodeAnalysis.Common": "[3.8.0]" + } + }, + "Microsoft.Data.Sqlite": { + "type": "CentralTransitive", + "requested": "[7.0.9, )", + "resolved": "7.0.9", + "contentHash": "XZ/7gpAP3EFlaDkLqv21Ro1ZHMtkh7UpBImyLcv0x+G5qt2J9vvrAk1g5qYL2ykwzpzf7Stc6Xt1MSkv5YmdPg==", + "dependencies": { + "Microsoft.Data.Sqlite.Core": "7.0.9", + "SQLitePCLRaw.bundle_e_sqlite3": "2.1.4" + } + }, + "Microsoft.Win32.SystemEvents": { + "type": "CentralTransitive", + "requested": "[7.0.0, )", + "resolved": "7.0.0", + "contentHash": "2nXPrhdAyAzir0gLl8Yy8S5Mnm/uBSQQA7jEsILOS1MTyS7DbmV1NgViMtvV1sfCD1ebITpNwb1NIinKeJgUVQ==" + }, + "NBitcoin": { + "type": "CentralTransitive", + "requested": "[7.0.27, )", + "resolved": "7.0.27", + "contentHash": "n2eHYJf0YVOf3ld0fhQJ8qR8TDvGZObGseOf5gHx03QpG+lq5L5qJAn5SA+MvZQLKcqhEUJ+S2AKvWkgZYS4Gw==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "1.0.0", + "Newtonsoft.Json": "13.0.1" + } + }, + "QRackers": { + "type": "CentralTransitive", + "requested": "[1.1.0, )", + "resolved": "1.1.0", + "contentHash": "a44f7v+IVAI+Rz2PM+EdzreEBbgT9ffOj2B8kiBAKAM4DcgHieaN/qN5RsZwUuFX3TF1HGblZ20aM1jbD+t/Kg==", + "dependencies": { + "SkiaSharp": "2.88.3" + } + }, + "System.Runtime": { + "type": "CentralTransitive", + "requested": "[4.3.1, )", + "resolved": "4.3.1", + "contentHash": "abhfv1dTK6NXOmu4bgHIONxHyEqFjW8HwXPmpY9gmll+ix9UNo4XDcmzJn6oLooftxNssVHdJC1pGT9jkSynQg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.1", + "Microsoft.NETCore.Targets": "1.1.3" + } + }, + "WabiSabi": { + "type": "CentralTransitive", + "requested": "[1.0.1.2, )", + "resolved": "1.0.1.2", + "contentHash": "e+pMZGVEfWQvkpZHAydGv6grY71urfO47lodjXC9eWtfSFvNtPWjrgqck9O24yIbXhP4K3QrJKzJQFGpAp8rqg==", + "dependencies": { + "NBitcoin.Secp256k1": "3.1.0" + } } }, "net7.0/linux-arm64": { "Avalonia.Angle.Windows.Natives": { "type": "Transitive", - "resolved": "2.1.0.2020091801", - "contentHash": "nGsCPI8FuUknU/e6hZIqlsKRDxClXHZyztmgM8vuwslFC/BIV3LqM2wKefWbr6SORX4Lct4nivhSMkdF/TrKgg==" + "resolved": "2.1.0.2023020321", + "contentHash": "Zlkkb8ipxrxNWVPCJgMO19fpcpYPP+bpOQ+jPtCFj8v+TzVvPdnGHuyv9IMvSHhhMfEpps4m4hjaP4FORQYVAA==" }, "Avalonia.Native": { "type": "Transitive", - "resolved": "0.10.19", - "contentHash": "KTxMewYzDJ3/BG+flb+co6yXShGhk8kh52T/9ktPh06HVsOWFGNsA3CHUDGiE5/UgCP5oim1xAef+pAzlkBvWw==", + "resolved": "11.0.5", + "contentHash": "ZrOlxU7C5FdDVmTxta/xv83KKg+HqgnmX6hGTQdEK4Yn5rMVqy0h1CcuZr7UKM4kHnbWdUhbgZ3+mXeF6f8Mug==", "dependencies": { - "Avalonia": "0.10.19" + "Avalonia": "11.0.5" } }, "HarfBuzzSharp.NativeAssets.Linux": { "type": "Transitive", - "resolved": "2.8.2.1-preview.108", - "contentHash": "kRjP0sub39GxY7/YUoWwMAvltH+i+0+HvG6ND1v1iWAeBbAwcBFnPfT6FQDBqdnEaeYQT6y8FxMn9phOND7Kyg==", + "resolved": "2.8.2.3", + "contentHash": "Qu1yJSHEN7PD3+fqfkaClnORWN5e2xJ2Xoziz/GUi/oBT1Z+Dp2oZeiONKP6NFltboSOBkvH90QuOA6YN/U1zg==", "dependencies": { - "HarfBuzzSharp": "2.8.2.1-preview.108" + "HarfBuzzSharp": "2.8.2.3" } }, "HarfBuzzSharp.NativeAssets.macOS": { "type": "Transitive", - "resolved": "2.8.2.1-preview.108", - "contentHash": "pDw8R6ndu8usa9unSqEZrl3RbUNw2AzqAkcJTkocA15dxBpHvaaVKqgEozTLfye0/l5s0YgYAb4WpcY4qBg6Pw==" + "resolved": "2.8.2.3", + "contentHash": "uwz9pB3hMuxzI/bSkjVrsOJH7Wo1L+0Md5ZmEMDM/j7xDHtR9d3mfg/CfxhMIcTiUC4JgX49FZK0y2ojgu1dww==" }, "HarfBuzzSharp.NativeAssets.Win32": { "type": "Transitive", - "resolved": "2.8.2.1-preview.108", - "contentHash": "0ws24k21iRH2GRiOLEcG6ESl+VROOwaeHnC0vqKQChGmreGTJ//JBQJqIu189oY30G0NVdypDe1UwFA/scjBAw==" - }, - "Microsoft.Win32.SystemEvents": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "2nXPrhdAyAzir0gLl8Yy8S5Mnm/uBSQQA7jEsILOS1MTyS7DbmV1NgViMtvV1sfCD1ebITpNwb1NIinKeJgUVQ==" + "resolved": "2.8.2.3", + "contentHash": "Wo6QpE4+a+PFVdfIBoLkLr4wq2uC0m9TZC8FAfy4ZnLsUc10WL0Egk9EBHHhDCeokNOXDse5YtvuTYtS/rbHfg==" }, "runtime.any.System.Collections": { "type": "Transitive", @@ -898,21 +1048,26 @@ }, "SkiaSharp.NativeAssets.Linux": { "type": "Transitive", - "resolved": "2.88.1-preview.108", - "contentHash": "1aOmUqcuzXJP0FaDL5JPRx7FbLFbiyl5R2lI1YwTTfXTpawnPxpPXlBClj+CuRrSS5Azfn8k3ZIHPHTd37vOWw==", + "resolved": "2.88.6", + "contentHash": "iQcOUE0tPZvBUxOdZaP3LIdAC21H8BEMhDvpCQ/mUUvbKGLd5rF7veJVSZBNu20SuCC0oZpEdGxB+mLVOK8uzw==", "dependencies": { - "SkiaSharp": "2.88.1-preview.108" + "SkiaSharp": "2.88.6" } }, "SkiaSharp.NativeAssets.macOS": { "type": "Transitive", - "resolved": "2.88.1-preview.108", - "contentHash": "nz+Ege0i1aCicLnaHOBzuTBj5LnLxlZVxLv+wUEtOXaAHq6of7kxaE+/+4KC1OBnKs64L8WDGf88VC2fIC/zxw==" + "resolved": "2.88.6", + "contentHash": "Sko9LFxRXSjb3OGh5/RxrVRXxYo48tr5NKuuSy6jB85GrYt8WRqVY1iLOLwtjPiVAt4cp+pyD4i30azanS64dw==" }, "SkiaSharp.NativeAssets.Win32": { "type": "Transitive", - "resolved": "2.88.1-preview.108", - "contentHash": "98r2fGVjPNjIhH0ooHtvAcqsHUjWZPEkqrfpynZNWdo8gkUPZhENvOodDtvBNUW6we24Bo4aWCnGbJuhyn//ug==" + "resolved": "2.88.6", + "contentHash": "7TzFO0u/g2MpQsTty4fyCDdMcfcWI+aLswwfnYXr3gtNS6VLKdMXPMeKpJa3pJSLnUBN6wD0JjuCe8OoLBQ6cQ==" + }, + "SQLitePCLRaw.lib.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.4", + "contentHash": "2C9Q9eX7CPLveJA0rIhf9RXAvu+7nWZu1A2MdG6SD/NOu26TakGgL1nsbc0JAspGijFOo3HoN79xrx8a368fBg==" }, "System.Collections": { "type": "Transitive", @@ -949,33 +1104,33 @@ }, "System.Drawing.Common": { "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "KIX+oBU38pxkKPxvLcLfIkOV5Ien8ReN78wro7OF5/erwcmortzeFx+iBswlh2Vz6gVne0khocQudGwaO1Ey6A==", + "resolved": "6.0.0", + "contentHash": "NfuoKUiP2nUWwKZN6twGqXioIe1zVD0RIj2t976A+czLHr2nY454RwwXs6JU9Htc6mwqL6Dn/nEL3dpVf2jOhg==", "dependencies": { - "Microsoft.Win32.SystemEvents": "7.0.0" + "Microsoft.Win32.SystemEvents": "6.0.0" } }, "System.Globalization": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==", + "resolved": "4.0.11", + "contentHash": "B95h0YLEL2oSnwF/XjqSWKnwKOy/01VWkNlsCeMTFJLLabflpGV26nK164eRs5GiaRSBGpOxQ3pKoSnnyZN5pg==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", "runtime.any.System.Globalization": "4.3.0" } }, "System.IO": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==", + "resolved": "4.1.0", + "contentHash": "3KlTJceQc3gnGIaHZ7UBZO26SHL1SHE4ddrmiwumFnId+CEHP+O8r386tZKaE6zlk5/mF8vifMBzHj9SaXN+mQ==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading.Tasks": "4.3.0", + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "System.Text.Encoding": "4.0.11", + "System.Threading.Tasks": "4.0.11", "runtime.any.System.IO": "4.3.0" } }, @@ -991,51 +1146,41 @@ }, "System.Reflection": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==", + "resolved": "4.1.0", + "contentHash": "JCKANJ0TI7kzoQzuwB/OoJANy1Lg338B6+JVacPl4TpUwi3cReg3nMLplMq2uqYfHFQpKIlHAUVAJlImZz/4ng==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.IO": "4.3.0", - "System.Reflection.Primitives": "4.3.0", - "System.Runtime": "4.3.0", + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.IO": "4.1.0", + "System.Reflection.Primitives": "4.0.1", + "System.Runtime": "4.1.0", "runtime.any.System.Reflection": "4.3.0" } }, "System.Reflection.Primitives": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==", + "resolved": "4.0.1", + "contentHash": "4inTox4wTBaDhB7V3mPvp9XlCbeGYWVEM9/fXALd52vNEAVisc1BoVWQPuUuD0Ga//dNbA/WeMy9u9mzLxGTHQ==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", "runtime.any.System.Reflection.Primitives": "4.3.0" } }, "System.Resources.ResourceManager": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "/zrcPkkWdZmI4F92gL/TPumP98AVDu/Wxr3CSJGQQ+XN6wbRZcyfSKVoPo17ilb3iOr0cCRqJInGwNMolqhS8A==", + "resolved": "4.0.1", + "contentHash": "TxwVeUNoTgUOdQ09gfTjvW411MF+w9MBYL7AtNVc+HtBCFlutPLhUCdZjNkjbhj3bNQWMdHboF0KIWEOjJssbA==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Globalization": "4.3.0", - "System.Reflection": "4.3.0", - "System.Runtime": "4.3.0", + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Globalization": "4.0.11", + "System.Reflection": "4.1.0", + "System.Runtime": "4.1.0", "runtime.any.System.Resources.ResourceManager": "4.3.0" } }, - "System.Runtime": { - "type": "Transitive", - "resolved": "4.3.1", - "contentHash": "abhfv1dTK6NXOmu4bgHIONxHyEqFjW8HwXPmpY9gmll+ix9UNo4XDcmzJn6oLooftxNssVHdJC1pGT9jkSynQg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.1", - "Microsoft.NETCore.Targets": "1.1.3", - "runtime.any.System.Runtime": "4.3.0" - } - }, "System.Runtime.Extensions": { "type": "Transitive", "resolved": "4.1.0", @@ -1072,19 +1217,14 @@ "runtime.any.System.Runtime.InteropServices": "4.3.0" } }, - "System.Security.Principal.Windows": { - "type": "Transitive", - "resolved": "4.7.0", - "contentHash": "ojD0PX0XhneCsUbAZVKdb7h/70vyYMDYs85lwEI+LngEONe/17A0cFaRFqZU+sOEidcVswYWikYOQ9PPfjlbtQ==" - }, "System.Text.Encoding": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==", + "resolved": "4.0.11", + "contentHash": "U3gGeMlDZXxCEiY4DwVLSacg+DFWCvoiX+JThA/rvw37Sqrku7sEFeVBBBMBnfB6FeZHsyDx85HlKL19x0HtZA==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", "runtime.any.System.Text.Encoding": "4.3.0" } }, @@ -1099,52 +1239,64 @@ }, "System.Threading.Tasks": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==", + "resolved": "4.0.11", + "contentHash": "k1S4Gc6IGwtHGT8188RSeGaX86Qw/wnrgNLshJvsdNUOPP9etMmo8S07c+UlOAx4K/xLuN9ivA1bD0LVurtIxQ==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", "runtime.any.System.Threading.Tasks": "4.3.0" } + }, + "Microsoft.Win32.SystemEvents": { + "type": "CentralTransitive", + "requested": "[7.0.0, )", + "resolved": "7.0.0", + "contentHash": "2nXPrhdAyAzir0gLl8Yy8S5Mnm/uBSQQA7jEsILOS1MTyS7DbmV1NgViMtvV1sfCD1ebITpNwb1NIinKeJgUVQ==" + }, + "System.Runtime": { + "type": "CentralTransitive", + "requested": "[4.3.1, )", + "resolved": "4.3.1", + "contentHash": "abhfv1dTK6NXOmu4bgHIONxHyEqFjW8HwXPmpY9gmll+ix9UNo4XDcmzJn6oLooftxNssVHdJC1pGT9jkSynQg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.1", + "Microsoft.NETCore.Targets": "1.1.3", + "runtime.any.System.Runtime": "4.3.0" + } } }, "net7.0/linux-x64": { "Avalonia.Angle.Windows.Natives": { "type": "Transitive", - "resolved": "2.1.0.2020091801", - "contentHash": "nGsCPI8FuUknU/e6hZIqlsKRDxClXHZyztmgM8vuwslFC/BIV3LqM2wKefWbr6SORX4Lct4nivhSMkdF/TrKgg==" + "resolved": "2.1.0.2023020321", + "contentHash": "Zlkkb8ipxrxNWVPCJgMO19fpcpYPP+bpOQ+jPtCFj8v+TzVvPdnGHuyv9IMvSHhhMfEpps4m4hjaP4FORQYVAA==" }, "Avalonia.Native": { "type": "Transitive", - "resolved": "0.10.19", - "contentHash": "KTxMewYzDJ3/BG+flb+co6yXShGhk8kh52T/9ktPh06HVsOWFGNsA3CHUDGiE5/UgCP5oim1xAef+pAzlkBvWw==", + "resolved": "11.0.5", + "contentHash": "ZrOlxU7C5FdDVmTxta/xv83KKg+HqgnmX6hGTQdEK4Yn5rMVqy0h1CcuZr7UKM4kHnbWdUhbgZ3+mXeF6f8Mug==", "dependencies": { - "Avalonia": "0.10.19" + "Avalonia": "11.0.5" } }, "HarfBuzzSharp.NativeAssets.Linux": { "type": "Transitive", - "resolved": "2.8.2.1-preview.108", - "contentHash": "kRjP0sub39GxY7/YUoWwMAvltH+i+0+HvG6ND1v1iWAeBbAwcBFnPfT6FQDBqdnEaeYQT6y8FxMn9phOND7Kyg==", + "resolved": "2.8.2.3", + "contentHash": "Qu1yJSHEN7PD3+fqfkaClnORWN5e2xJ2Xoziz/GUi/oBT1Z+Dp2oZeiONKP6NFltboSOBkvH90QuOA6YN/U1zg==", "dependencies": { - "HarfBuzzSharp": "2.8.2.1-preview.108" + "HarfBuzzSharp": "2.8.2.3" } }, "HarfBuzzSharp.NativeAssets.macOS": { "type": "Transitive", - "resolved": "2.8.2.1-preview.108", - "contentHash": "pDw8R6ndu8usa9unSqEZrl3RbUNw2AzqAkcJTkocA15dxBpHvaaVKqgEozTLfye0/l5s0YgYAb4WpcY4qBg6Pw==" + "resolved": "2.8.2.3", + "contentHash": "uwz9pB3hMuxzI/bSkjVrsOJH7Wo1L+0Md5ZmEMDM/j7xDHtR9d3mfg/CfxhMIcTiUC4JgX49FZK0y2ojgu1dww==" }, "HarfBuzzSharp.NativeAssets.Win32": { "type": "Transitive", - "resolved": "2.8.2.1-preview.108", - "contentHash": "0ws24k21iRH2GRiOLEcG6ESl+VROOwaeHnC0vqKQChGmreGTJ//JBQJqIu189oY30G0NVdypDe1UwFA/scjBAw==" - }, - "Microsoft.Win32.SystemEvents": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "2nXPrhdAyAzir0gLl8Yy8S5Mnm/uBSQQA7jEsILOS1MTyS7DbmV1NgViMtvV1sfCD1ebITpNwb1NIinKeJgUVQ==" + "resolved": "2.8.2.3", + "contentHash": "Wo6QpE4+a+PFVdfIBoLkLr4wq2uC0m9TZC8FAfy4ZnLsUc10WL0Egk9EBHHhDCeokNOXDse5YtvuTYtS/rbHfg==" }, "runtime.any.System.Collections": { "type": "Transitive", @@ -1316,21 +1468,26 @@ }, "SkiaSharp.NativeAssets.Linux": { "type": "Transitive", - "resolved": "2.88.1-preview.108", - "contentHash": "1aOmUqcuzXJP0FaDL5JPRx7FbLFbiyl5R2lI1YwTTfXTpawnPxpPXlBClj+CuRrSS5Azfn8k3ZIHPHTd37vOWw==", + "resolved": "2.88.6", + "contentHash": "iQcOUE0tPZvBUxOdZaP3LIdAC21H8BEMhDvpCQ/mUUvbKGLd5rF7veJVSZBNu20SuCC0oZpEdGxB+mLVOK8uzw==", "dependencies": { - "SkiaSharp": "2.88.1-preview.108" + "SkiaSharp": "2.88.6" } }, "SkiaSharp.NativeAssets.macOS": { "type": "Transitive", - "resolved": "2.88.1-preview.108", - "contentHash": "nz+Ege0i1aCicLnaHOBzuTBj5LnLxlZVxLv+wUEtOXaAHq6of7kxaE+/+4KC1OBnKs64L8WDGf88VC2fIC/zxw==" + "resolved": "2.88.6", + "contentHash": "Sko9LFxRXSjb3OGh5/RxrVRXxYo48tr5NKuuSy6jB85GrYt8WRqVY1iLOLwtjPiVAt4cp+pyD4i30azanS64dw==" }, "SkiaSharp.NativeAssets.Win32": { "type": "Transitive", - "resolved": "2.88.1-preview.108", - "contentHash": "98r2fGVjPNjIhH0ooHtvAcqsHUjWZPEkqrfpynZNWdo8gkUPZhENvOodDtvBNUW6we24Bo4aWCnGbJuhyn//ug==" + "resolved": "2.88.6", + "contentHash": "7TzFO0u/g2MpQsTty4fyCDdMcfcWI+aLswwfnYXr3gtNS6VLKdMXPMeKpJa3pJSLnUBN6wD0JjuCe8OoLBQ6cQ==" + }, + "SQLitePCLRaw.lib.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.4", + "contentHash": "2C9Q9eX7CPLveJA0rIhf9RXAvu+7nWZu1A2MdG6SD/NOu26TakGgL1nsbc0JAspGijFOo3HoN79xrx8a368fBg==" }, "System.Collections": { "type": "Transitive", @@ -1367,33 +1524,33 @@ }, "System.Drawing.Common": { "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "KIX+oBU38pxkKPxvLcLfIkOV5Ien8ReN78wro7OF5/erwcmortzeFx+iBswlh2Vz6gVne0khocQudGwaO1Ey6A==", + "resolved": "6.0.0", + "contentHash": "NfuoKUiP2nUWwKZN6twGqXioIe1zVD0RIj2t976A+czLHr2nY454RwwXs6JU9Htc6mwqL6Dn/nEL3dpVf2jOhg==", "dependencies": { - "Microsoft.Win32.SystemEvents": "7.0.0" + "Microsoft.Win32.SystemEvents": "6.0.0" } }, "System.Globalization": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==", + "resolved": "4.0.11", + "contentHash": "B95h0YLEL2oSnwF/XjqSWKnwKOy/01VWkNlsCeMTFJLLabflpGV26nK164eRs5GiaRSBGpOxQ3pKoSnnyZN5pg==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", "runtime.any.System.Globalization": "4.3.0" } }, "System.IO": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==", + "resolved": "4.1.0", + "contentHash": "3KlTJceQc3gnGIaHZ7UBZO26SHL1SHE4ddrmiwumFnId+CEHP+O8r386tZKaE6zlk5/mF8vifMBzHj9SaXN+mQ==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading.Tasks": "4.3.0", + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "System.Text.Encoding": "4.0.11", + "System.Threading.Tasks": "4.0.11", "runtime.any.System.IO": "4.3.0" } }, @@ -1409,51 +1566,41 @@ }, "System.Reflection": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==", + "resolved": "4.1.0", + "contentHash": "JCKANJ0TI7kzoQzuwB/OoJANy1Lg338B6+JVacPl4TpUwi3cReg3nMLplMq2uqYfHFQpKIlHAUVAJlImZz/4ng==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.IO": "4.3.0", - "System.Reflection.Primitives": "4.3.0", - "System.Runtime": "4.3.0", + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.IO": "4.1.0", + "System.Reflection.Primitives": "4.0.1", + "System.Runtime": "4.1.0", "runtime.any.System.Reflection": "4.3.0" } }, "System.Reflection.Primitives": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==", + "resolved": "4.0.1", + "contentHash": "4inTox4wTBaDhB7V3mPvp9XlCbeGYWVEM9/fXALd52vNEAVisc1BoVWQPuUuD0Ga//dNbA/WeMy9u9mzLxGTHQ==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", "runtime.any.System.Reflection.Primitives": "4.3.0" } }, "System.Resources.ResourceManager": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "/zrcPkkWdZmI4F92gL/TPumP98AVDu/Wxr3CSJGQQ+XN6wbRZcyfSKVoPo17ilb3iOr0cCRqJInGwNMolqhS8A==", + "resolved": "4.0.1", + "contentHash": "TxwVeUNoTgUOdQ09gfTjvW411MF+w9MBYL7AtNVc+HtBCFlutPLhUCdZjNkjbhj3bNQWMdHboF0KIWEOjJssbA==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Globalization": "4.3.0", - "System.Reflection": "4.3.0", - "System.Runtime": "4.3.0", + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Globalization": "4.0.11", + "System.Reflection": "4.1.0", + "System.Runtime": "4.1.0", "runtime.any.System.Resources.ResourceManager": "4.3.0" } }, - "System.Runtime": { - "type": "Transitive", - "resolved": "4.3.1", - "contentHash": "abhfv1dTK6NXOmu4bgHIONxHyEqFjW8HwXPmpY9gmll+ix9UNo4XDcmzJn6oLooftxNssVHdJC1pGT9jkSynQg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.1", - "Microsoft.NETCore.Targets": "1.1.3", - "runtime.any.System.Runtime": "4.3.0" - } - }, "System.Runtime.Extensions": { "type": "Transitive", "resolved": "4.1.0", @@ -1490,19 +1637,14 @@ "runtime.any.System.Runtime.InteropServices": "4.3.0" } }, - "System.Security.Principal.Windows": { - "type": "Transitive", - "resolved": "4.7.0", - "contentHash": "ojD0PX0XhneCsUbAZVKdb7h/70vyYMDYs85lwEI+LngEONe/17A0cFaRFqZU+sOEidcVswYWikYOQ9PPfjlbtQ==" - }, "System.Text.Encoding": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==", + "resolved": "4.0.11", + "contentHash": "U3gGeMlDZXxCEiY4DwVLSacg+DFWCvoiX+JThA/rvw37Sqrku7sEFeVBBBMBnfB6FeZHsyDx85HlKL19x0HtZA==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", "runtime.any.System.Text.Encoding": "4.3.0" } }, @@ -1517,52 +1659,64 @@ }, "System.Threading.Tasks": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==", + "resolved": "4.0.11", + "contentHash": "k1S4Gc6IGwtHGT8188RSeGaX86Qw/wnrgNLshJvsdNUOPP9etMmo8S07c+UlOAx4K/xLuN9ivA1bD0LVurtIxQ==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", "runtime.any.System.Threading.Tasks": "4.3.0" } + }, + "Microsoft.Win32.SystemEvents": { + "type": "CentralTransitive", + "requested": "[7.0.0, )", + "resolved": "7.0.0", + "contentHash": "2nXPrhdAyAzir0gLl8Yy8S5Mnm/uBSQQA7jEsILOS1MTyS7DbmV1NgViMtvV1sfCD1ebITpNwb1NIinKeJgUVQ==" + }, + "System.Runtime": { + "type": "CentralTransitive", + "requested": "[4.3.1, )", + "resolved": "4.3.1", + "contentHash": "abhfv1dTK6NXOmu4bgHIONxHyEqFjW8HwXPmpY9gmll+ix9UNo4XDcmzJn6oLooftxNssVHdJC1pGT9jkSynQg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.1", + "Microsoft.NETCore.Targets": "1.1.3", + "runtime.any.System.Runtime": "4.3.0" + } } }, "net7.0/osx-arm64": { "Avalonia.Angle.Windows.Natives": { "type": "Transitive", - "resolved": "2.1.0.2020091801", - "contentHash": "nGsCPI8FuUknU/e6hZIqlsKRDxClXHZyztmgM8vuwslFC/BIV3LqM2wKefWbr6SORX4Lct4nivhSMkdF/TrKgg==" + "resolved": "2.1.0.2023020321", + "contentHash": "Zlkkb8ipxrxNWVPCJgMO19fpcpYPP+bpOQ+jPtCFj8v+TzVvPdnGHuyv9IMvSHhhMfEpps4m4hjaP4FORQYVAA==" }, "Avalonia.Native": { "type": "Transitive", - "resolved": "0.10.19", - "contentHash": "KTxMewYzDJ3/BG+flb+co6yXShGhk8kh52T/9ktPh06HVsOWFGNsA3CHUDGiE5/UgCP5oim1xAef+pAzlkBvWw==", + "resolved": "11.0.5", + "contentHash": "ZrOlxU7C5FdDVmTxta/xv83KKg+HqgnmX6hGTQdEK4Yn5rMVqy0h1CcuZr7UKM4kHnbWdUhbgZ3+mXeF6f8Mug==", "dependencies": { - "Avalonia": "0.10.19" + "Avalonia": "11.0.5" } }, "HarfBuzzSharp.NativeAssets.Linux": { "type": "Transitive", - "resolved": "2.8.2.1-preview.108", - "contentHash": "kRjP0sub39GxY7/YUoWwMAvltH+i+0+HvG6ND1v1iWAeBbAwcBFnPfT6FQDBqdnEaeYQT6y8FxMn9phOND7Kyg==", + "resolved": "2.8.2.3", + "contentHash": "Qu1yJSHEN7PD3+fqfkaClnORWN5e2xJ2Xoziz/GUi/oBT1Z+Dp2oZeiONKP6NFltboSOBkvH90QuOA6YN/U1zg==", "dependencies": { - "HarfBuzzSharp": "2.8.2.1-preview.108" + "HarfBuzzSharp": "2.8.2.3" } }, "HarfBuzzSharp.NativeAssets.macOS": { "type": "Transitive", - "resolved": "2.8.2.1-preview.108", - "contentHash": "pDw8R6ndu8usa9unSqEZrl3RbUNw2AzqAkcJTkocA15dxBpHvaaVKqgEozTLfye0/l5s0YgYAb4WpcY4qBg6Pw==" - }, - "HarfBuzzSharp.NativeAssets.Win32": { - "type": "Transitive", - "resolved": "2.8.2.1-preview.108", - "contentHash": "0ws24k21iRH2GRiOLEcG6ESl+VROOwaeHnC0vqKQChGmreGTJ//JBQJqIu189oY30G0NVdypDe1UwFA/scjBAw==" + "resolved": "2.8.2.3", + "contentHash": "uwz9pB3hMuxzI/bSkjVrsOJH7Wo1L+0Md5ZmEMDM/j7xDHtR9d3mfg/CfxhMIcTiUC4JgX49FZK0y2ojgu1dww==" }, - "Microsoft.Win32.SystemEvents": { + "HarfBuzzSharp.NativeAssets.Win32": { "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "2nXPrhdAyAzir0gLl8Yy8S5Mnm/uBSQQA7jEsILOS1MTyS7DbmV1NgViMtvV1sfCD1ebITpNwb1NIinKeJgUVQ==" + "resolved": "2.8.2.3", + "contentHash": "Wo6QpE4+a+PFVdfIBoLkLr4wq2uC0m9TZC8FAfy4ZnLsUc10WL0Egk9EBHHhDCeokNOXDse5YtvuTYtS/rbHfg==" }, "runtime.any.System.Collections": { "type": "Transitive", @@ -1734,21 +1888,26 @@ }, "SkiaSharp.NativeAssets.Linux": { "type": "Transitive", - "resolved": "2.88.1-preview.108", - "contentHash": "1aOmUqcuzXJP0FaDL5JPRx7FbLFbiyl5R2lI1YwTTfXTpawnPxpPXlBClj+CuRrSS5Azfn8k3ZIHPHTd37vOWw==", + "resolved": "2.88.6", + "contentHash": "iQcOUE0tPZvBUxOdZaP3LIdAC21H8BEMhDvpCQ/mUUvbKGLd5rF7veJVSZBNu20SuCC0oZpEdGxB+mLVOK8uzw==", "dependencies": { - "SkiaSharp": "2.88.1-preview.108" + "SkiaSharp": "2.88.6" } }, "SkiaSharp.NativeAssets.macOS": { "type": "Transitive", - "resolved": "2.88.1-preview.108", - "contentHash": "nz+Ege0i1aCicLnaHOBzuTBj5LnLxlZVxLv+wUEtOXaAHq6of7kxaE+/+4KC1OBnKs64L8WDGf88VC2fIC/zxw==" + "resolved": "2.88.6", + "contentHash": "Sko9LFxRXSjb3OGh5/RxrVRXxYo48tr5NKuuSy6jB85GrYt8WRqVY1iLOLwtjPiVAt4cp+pyD4i30azanS64dw==" }, "SkiaSharp.NativeAssets.Win32": { "type": "Transitive", - "resolved": "2.88.1-preview.108", - "contentHash": "98r2fGVjPNjIhH0ooHtvAcqsHUjWZPEkqrfpynZNWdo8gkUPZhENvOodDtvBNUW6we24Bo4aWCnGbJuhyn//ug==" + "resolved": "2.88.6", + "contentHash": "7TzFO0u/g2MpQsTty4fyCDdMcfcWI+aLswwfnYXr3gtNS6VLKdMXPMeKpJa3pJSLnUBN6wD0JjuCe8OoLBQ6cQ==" + }, + "SQLitePCLRaw.lib.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.4", + "contentHash": "2C9Q9eX7CPLveJA0rIhf9RXAvu+7nWZu1A2MdG6SD/NOu26TakGgL1nsbc0JAspGijFOo3HoN79xrx8a368fBg==" }, "System.Collections": { "type": "Transitive", @@ -1785,33 +1944,33 @@ }, "System.Drawing.Common": { "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "KIX+oBU38pxkKPxvLcLfIkOV5Ien8ReN78wro7OF5/erwcmortzeFx+iBswlh2Vz6gVne0khocQudGwaO1Ey6A==", + "resolved": "6.0.0", + "contentHash": "NfuoKUiP2nUWwKZN6twGqXioIe1zVD0RIj2t976A+czLHr2nY454RwwXs6JU9Htc6mwqL6Dn/nEL3dpVf2jOhg==", "dependencies": { - "Microsoft.Win32.SystemEvents": "7.0.0" + "Microsoft.Win32.SystemEvents": "6.0.0" } }, "System.Globalization": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==", + "resolved": "4.0.11", + "contentHash": "B95h0YLEL2oSnwF/XjqSWKnwKOy/01VWkNlsCeMTFJLLabflpGV26nK164eRs5GiaRSBGpOxQ3pKoSnnyZN5pg==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", "runtime.any.System.Globalization": "4.3.0" } }, "System.IO": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==", + "resolved": "4.1.0", + "contentHash": "3KlTJceQc3gnGIaHZ7UBZO26SHL1SHE4ddrmiwumFnId+CEHP+O8r386tZKaE6zlk5/mF8vifMBzHj9SaXN+mQ==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading.Tasks": "4.3.0", + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "System.Text.Encoding": "4.0.11", + "System.Threading.Tasks": "4.0.11", "runtime.any.System.IO": "4.3.0" } }, @@ -1827,51 +1986,41 @@ }, "System.Reflection": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==", + "resolved": "4.1.0", + "contentHash": "JCKANJ0TI7kzoQzuwB/OoJANy1Lg338B6+JVacPl4TpUwi3cReg3nMLplMq2uqYfHFQpKIlHAUVAJlImZz/4ng==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.IO": "4.3.0", - "System.Reflection.Primitives": "4.3.0", - "System.Runtime": "4.3.0", + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.IO": "4.1.0", + "System.Reflection.Primitives": "4.0.1", + "System.Runtime": "4.1.0", "runtime.any.System.Reflection": "4.3.0" } }, "System.Reflection.Primitives": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==", + "resolved": "4.0.1", + "contentHash": "4inTox4wTBaDhB7V3mPvp9XlCbeGYWVEM9/fXALd52vNEAVisc1BoVWQPuUuD0Ga//dNbA/WeMy9u9mzLxGTHQ==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", "runtime.any.System.Reflection.Primitives": "4.3.0" } }, "System.Resources.ResourceManager": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "/zrcPkkWdZmI4F92gL/TPumP98AVDu/Wxr3CSJGQQ+XN6wbRZcyfSKVoPo17ilb3iOr0cCRqJInGwNMolqhS8A==", + "resolved": "4.0.1", + "contentHash": "TxwVeUNoTgUOdQ09gfTjvW411MF+w9MBYL7AtNVc+HtBCFlutPLhUCdZjNkjbhj3bNQWMdHboF0KIWEOjJssbA==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Globalization": "4.3.0", - "System.Reflection": "4.3.0", - "System.Runtime": "4.3.0", + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Globalization": "4.0.11", + "System.Reflection": "4.1.0", + "System.Runtime": "4.1.0", "runtime.any.System.Resources.ResourceManager": "4.3.0" } }, - "System.Runtime": { - "type": "Transitive", - "resolved": "4.3.1", - "contentHash": "abhfv1dTK6NXOmu4bgHIONxHyEqFjW8HwXPmpY9gmll+ix9UNo4XDcmzJn6oLooftxNssVHdJC1pGT9jkSynQg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.1", - "Microsoft.NETCore.Targets": "1.1.3", - "runtime.any.System.Runtime": "4.3.0" - } - }, "System.Runtime.Extensions": { "type": "Transitive", "resolved": "4.1.0", @@ -1908,19 +2057,14 @@ "runtime.any.System.Runtime.InteropServices": "4.3.0" } }, - "System.Security.Principal.Windows": { - "type": "Transitive", - "resolved": "4.7.0", - "contentHash": "ojD0PX0XhneCsUbAZVKdb7h/70vyYMDYs85lwEI+LngEONe/17A0cFaRFqZU+sOEidcVswYWikYOQ9PPfjlbtQ==" - }, "System.Text.Encoding": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==", + "resolved": "4.0.11", + "contentHash": "U3gGeMlDZXxCEiY4DwVLSacg+DFWCvoiX+JThA/rvw37Sqrku7sEFeVBBBMBnfB6FeZHsyDx85HlKL19x0HtZA==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", "runtime.any.System.Text.Encoding": "4.3.0" } }, @@ -1935,52 +2079,64 @@ }, "System.Threading.Tasks": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==", + "resolved": "4.0.11", + "contentHash": "k1S4Gc6IGwtHGT8188RSeGaX86Qw/wnrgNLshJvsdNUOPP9etMmo8S07c+UlOAx4K/xLuN9ivA1bD0LVurtIxQ==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", "runtime.any.System.Threading.Tasks": "4.3.0" } + }, + "Microsoft.Win32.SystemEvents": { + "type": "CentralTransitive", + "requested": "[7.0.0, )", + "resolved": "7.0.0", + "contentHash": "2nXPrhdAyAzir0gLl8Yy8S5Mnm/uBSQQA7jEsILOS1MTyS7DbmV1NgViMtvV1sfCD1ebITpNwb1NIinKeJgUVQ==" + }, + "System.Runtime": { + "type": "CentralTransitive", + "requested": "[4.3.1, )", + "resolved": "4.3.1", + "contentHash": "abhfv1dTK6NXOmu4bgHIONxHyEqFjW8HwXPmpY9gmll+ix9UNo4XDcmzJn6oLooftxNssVHdJC1pGT9jkSynQg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.1", + "Microsoft.NETCore.Targets": "1.1.3", + "runtime.any.System.Runtime": "4.3.0" + } } }, "net7.0/osx-x64": { "Avalonia.Angle.Windows.Natives": { "type": "Transitive", - "resolved": "2.1.0.2020091801", - "contentHash": "nGsCPI8FuUknU/e6hZIqlsKRDxClXHZyztmgM8vuwslFC/BIV3LqM2wKefWbr6SORX4Lct4nivhSMkdF/TrKgg==" + "resolved": "2.1.0.2023020321", + "contentHash": "Zlkkb8ipxrxNWVPCJgMO19fpcpYPP+bpOQ+jPtCFj8v+TzVvPdnGHuyv9IMvSHhhMfEpps4m4hjaP4FORQYVAA==" }, "Avalonia.Native": { "type": "Transitive", - "resolved": "0.10.19", - "contentHash": "KTxMewYzDJ3/BG+flb+co6yXShGhk8kh52T/9ktPh06HVsOWFGNsA3CHUDGiE5/UgCP5oim1xAef+pAzlkBvWw==", + "resolved": "11.0.5", + "contentHash": "ZrOlxU7C5FdDVmTxta/xv83KKg+HqgnmX6hGTQdEK4Yn5rMVqy0h1CcuZr7UKM4kHnbWdUhbgZ3+mXeF6f8Mug==", "dependencies": { - "Avalonia": "0.10.19" + "Avalonia": "11.0.5" } }, "HarfBuzzSharp.NativeAssets.Linux": { "type": "Transitive", - "resolved": "2.8.2.1-preview.108", - "contentHash": "kRjP0sub39GxY7/YUoWwMAvltH+i+0+HvG6ND1v1iWAeBbAwcBFnPfT6FQDBqdnEaeYQT6y8FxMn9phOND7Kyg==", + "resolved": "2.8.2.3", + "contentHash": "Qu1yJSHEN7PD3+fqfkaClnORWN5e2xJ2Xoziz/GUi/oBT1Z+Dp2oZeiONKP6NFltboSOBkvH90QuOA6YN/U1zg==", "dependencies": { - "HarfBuzzSharp": "2.8.2.1-preview.108" + "HarfBuzzSharp": "2.8.2.3" } }, "HarfBuzzSharp.NativeAssets.macOS": { "type": "Transitive", - "resolved": "2.8.2.1-preview.108", - "contentHash": "pDw8R6ndu8usa9unSqEZrl3RbUNw2AzqAkcJTkocA15dxBpHvaaVKqgEozTLfye0/l5s0YgYAb4WpcY4qBg6Pw==" + "resolved": "2.8.2.3", + "contentHash": "uwz9pB3hMuxzI/bSkjVrsOJH7Wo1L+0Md5ZmEMDM/j7xDHtR9d3mfg/CfxhMIcTiUC4JgX49FZK0y2ojgu1dww==" }, "HarfBuzzSharp.NativeAssets.Win32": { "type": "Transitive", - "resolved": "2.8.2.1-preview.108", - "contentHash": "0ws24k21iRH2GRiOLEcG6ESl+VROOwaeHnC0vqKQChGmreGTJ//JBQJqIu189oY30G0NVdypDe1UwFA/scjBAw==" - }, - "Microsoft.Win32.SystemEvents": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "2nXPrhdAyAzir0gLl8Yy8S5Mnm/uBSQQA7jEsILOS1MTyS7DbmV1NgViMtvV1sfCD1ebITpNwb1NIinKeJgUVQ==" + "resolved": "2.8.2.3", + "contentHash": "Wo6QpE4+a+PFVdfIBoLkLr4wq2uC0m9TZC8FAfy4ZnLsUc10WL0Egk9EBHHhDCeokNOXDse5YtvuTYtS/rbHfg==" }, "runtime.any.System.Collections": { "type": "Transitive", @@ -2152,21 +2308,26 @@ }, "SkiaSharp.NativeAssets.Linux": { "type": "Transitive", - "resolved": "2.88.1-preview.108", - "contentHash": "1aOmUqcuzXJP0FaDL5JPRx7FbLFbiyl5R2lI1YwTTfXTpawnPxpPXlBClj+CuRrSS5Azfn8k3ZIHPHTd37vOWw==", + "resolved": "2.88.6", + "contentHash": "iQcOUE0tPZvBUxOdZaP3LIdAC21H8BEMhDvpCQ/mUUvbKGLd5rF7veJVSZBNu20SuCC0oZpEdGxB+mLVOK8uzw==", "dependencies": { - "SkiaSharp": "2.88.1-preview.108" + "SkiaSharp": "2.88.6" } }, "SkiaSharp.NativeAssets.macOS": { "type": "Transitive", - "resolved": "2.88.1-preview.108", - "contentHash": "nz+Ege0i1aCicLnaHOBzuTBj5LnLxlZVxLv+wUEtOXaAHq6of7kxaE+/+4KC1OBnKs64L8WDGf88VC2fIC/zxw==" + "resolved": "2.88.6", + "contentHash": "Sko9LFxRXSjb3OGh5/RxrVRXxYo48tr5NKuuSy6jB85GrYt8WRqVY1iLOLwtjPiVAt4cp+pyD4i30azanS64dw==" }, "SkiaSharp.NativeAssets.Win32": { "type": "Transitive", - "resolved": "2.88.1-preview.108", - "contentHash": "98r2fGVjPNjIhH0ooHtvAcqsHUjWZPEkqrfpynZNWdo8gkUPZhENvOodDtvBNUW6we24Bo4aWCnGbJuhyn//ug==" + "resolved": "2.88.6", + "contentHash": "7TzFO0u/g2MpQsTty4fyCDdMcfcWI+aLswwfnYXr3gtNS6VLKdMXPMeKpJa3pJSLnUBN6wD0JjuCe8OoLBQ6cQ==" + }, + "SQLitePCLRaw.lib.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.4", + "contentHash": "2C9Q9eX7CPLveJA0rIhf9RXAvu+7nWZu1A2MdG6SD/NOu26TakGgL1nsbc0JAspGijFOo3HoN79xrx8a368fBg==" }, "System.Collections": { "type": "Transitive", @@ -2203,33 +2364,33 @@ }, "System.Drawing.Common": { "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "KIX+oBU38pxkKPxvLcLfIkOV5Ien8ReN78wro7OF5/erwcmortzeFx+iBswlh2Vz6gVne0khocQudGwaO1Ey6A==", + "resolved": "6.0.0", + "contentHash": "NfuoKUiP2nUWwKZN6twGqXioIe1zVD0RIj2t976A+czLHr2nY454RwwXs6JU9Htc6mwqL6Dn/nEL3dpVf2jOhg==", "dependencies": { - "Microsoft.Win32.SystemEvents": "7.0.0" + "Microsoft.Win32.SystemEvents": "6.0.0" } }, "System.Globalization": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==", + "resolved": "4.0.11", + "contentHash": "B95h0YLEL2oSnwF/XjqSWKnwKOy/01VWkNlsCeMTFJLLabflpGV26nK164eRs5GiaRSBGpOxQ3pKoSnnyZN5pg==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", "runtime.any.System.Globalization": "4.3.0" } }, "System.IO": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==", + "resolved": "4.1.0", + "contentHash": "3KlTJceQc3gnGIaHZ7UBZO26SHL1SHE4ddrmiwumFnId+CEHP+O8r386tZKaE6zlk5/mF8vifMBzHj9SaXN+mQ==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading.Tasks": "4.3.0", + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "System.Text.Encoding": "4.0.11", + "System.Threading.Tasks": "4.0.11", "runtime.any.System.IO": "4.3.0" } }, @@ -2245,51 +2406,41 @@ }, "System.Reflection": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==", + "resolved": "4.1.0", + "contentHash": "JCKANJ0TI7kzoQzuwB/OoJANy1Lg338B6+JVacPl4TpUwi3cReg3nMLplMq2uqYfHFQpKIlHAUVAJlImZz/4ng==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.IO": "4.3.0", - "System.Reflection.Primitives": "4.3.0", - "System.Runtime": "4.3.0", + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.IO": "4.1.0", + "System.Reflection.Primitives": "4.0.1", + "System.Runtime": "4.1.0", "runtime.any.System.Reflection": "4.3.0" } }, "System.Reflection.Primitives": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==", + "resolved": "4.0.1", + "contentHash": "4inTox4wTBaDhB7V3mPvp9XlCbeGYWVEM9/fXALd52vNEAVisc1BoVWQPuUuD0Ga//dNbA/WeMy9u9mzLxGTHQ==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", "runtime.any.System.Reflection.Primitives": "4.3.0" } }, "System.Resources.ResourceManager": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "/zrcPkkWdZmI4F92gL/TPumP98AVDu/Wxr3CSJGQQ+XN6wbRZcyfSKVoPo17ilb3iOr0cCRqJInGwNMolqhS8A==", + "resolved": "4.0.1", + "contentHash": "TxwVeUNoTgUOdQ09gfTjvW411MF+w9MBYL7AtNVc+HtBCFlutPLhUCdZjNkjbhj3bNQWMdHboF0KIWEOjJssbA==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Globalization": "4.3.0", - "System.Reflection": "4.3.0", - "System.Runtime": "4.3.0", + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Globalization": "4.0.11", + "System.Reflection": "4.1.0", + "System.Runtime": "4.1.0", "runtime.any.System.Resources.ResourceManager": "4.3.0" } }, - "System.Runtime": { - "type": "Transitive", - "resolved": "4.3.1", - "contentHash": "abhfv1dTK6NXOmu4bgHIONxHyEqFjW8HwXPmpY9gmll+ix9UNo4XDcmzJn6oLooftxNssVHdJC1pGT9jkSynQg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.1", - "Microsoft.NETCore.Targets": "1.1.3", - "runtime.any.System.Runtime": "4.3.0" - } - }, "System.Runtime.Extensions": { "type": "Transitive", "resolved": "4.1.0", @@ -2326,19 +2477,14 @@ "runtime.any.System.Runtime.InteropServices": "4.3.0" } }, - "System.Security.Principal.Windows": { - "type": "Transitive", - "resolved": "4.7.0", - "contentHash": "ojD0PX0XhneCsUbAZVKdb7h/70vyYMDYs85lwEI+LngEONe/17A0cFaRFqZU+sOEidcVswYWikYOQ9PPfjlbtQ==" - }, "System.Text.Encoding": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==", + "resolved": "4.0.11", + "contentHash": "U3gGeMlDZXxCEiY4DwVLSacg+DFWCvoiX+JThA/rvw37Sqrku7sEFeVBBBMBnfB6FeZHsyDx85HlKL19x0HtZA==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", "runtime.any.System.Text.Encoding": "4.3.0" } }, @@ -2353,52 +2499,64 @@ }, "System.Threading.Tasks": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==", + "resolved": "4.0.11", + "contentHash": "k1S4Gc6IGwtHGT8188RSeGaX86Qw/wnrgNLshJvsdNUOPP9etMmo8S07c+UlOAx4K/xLuN9ivA1bD0LVurtIxQ==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", "runtime.any.System.Threading.Tasks": "4.3.0" } + }, + "Microsoft.Win32.SystemEvents": { + "type": "CentralTransitive", + "requested": "[7.0.0, )", + "resolved": "7.0.0", + "contentHash": "2nXPrhdAyAzir0gLl8Yy8S5Mnm/uBSQQA7jEsILOS1MTyS7DbmV1NgViMtvV1sfCD1ebITpNwb1NIinKeJgUVQ==" + }, + "System.Runtime": { + "type": "CentralTransitive", + "requested": "[4.3.1, )", + "resolved": "4.3.1", + "contentHash": "abhfv1dTK6NXOmu4bgHIONxHyEqFjW8HwXPmpY9gmll+ix9UNo4XDcmzJn6oLooftxNssVHdJC1pGT9jkSynQg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.1", + "Microsoft.NETCore.Targets": "1.1.3", + "runtime.any.System.Runtime": "4.3.0" + } } }, "net7.0/win7-x64": { "Avalonia.Angle.Windows.Natives": { "type": "Transitive", - "resolved": "2.1.0.2020091801", - "contentHash": "nGsCPI8FuUknU/e6hZIqlsKRDxClXHZyztmgM8vuwslFC/BIV3LqM2wKefWbr6SORX4Lct4nivhSMkdF/TrKgg==" + "resolved": "2.1.0.2023020321", + "contentHash": "Zlkkb8ipxrxNWVPCJgMO19fpcpYPP+bpOQ+jPtCFj8v+TzVvPdnGHuyv9IMvSHhhMfEpps4m4hjaP4FORQYVAA==" }, "Avalonia.Native": { "type": "Transitive", - "resolved": "0.10.19", - "contentHash": "KTxMewYzDJ3/BG+flb+co6yXShGhk8kh52T/9ktPh06HVsOWFGNsA3CHUDGiE5/UgCP5oim1xAef+pAzlkBvWw==", + "resolved": "11.0.5", + "contentHash": "ZrOlxU7C5FdDVmTxta/xv83KKg+HqgnmX6hGTQdEK4Yn5rMVqy0h1CcuZr7UKM4kHnbWdUhbgZ3+mXeF6f8Mug==", "dependencies": { - "Avalonia": "0.10.19" + "Avalonia": "11.0.5" } }, "HarfBuzzSharp.NativeAssets.Linux": { "type": "Transitive", - "resolved": "2.8.2.1-preview.108", - "contentHash": "kRjP0sub39GxY7/YUoWwMAvltH+i+0+HvG6ND1v1iWAeBbAwcBFnPfT6FQDBqdnEaeYQT6y8FxMn9phOND7Kyg==", + "resolved": "2.8.2.3", + "contentHash": "Qu1yJSHEN7PD3+fqfkaClnORWN5e2xJ2Xoziz/GUi/oBT1Z+Dp2oZeiONKP6NFltboSOBkvH90QuOA6YN/U1zg==", "dependencies": { - "HarfBuzzSharp": "2.8.2.1-preview.108" + "HarfBuzzSharp": "2.8.2.3" } }, "HarfBuzzSharp.NativeAssets.macOS": { "type": "Transitive", - "resolved": "2.8.2.1-preview.108", - "contentHash": "pDw8R6ndu8usa9unSqEZrl3RbUNw2AzqAkcJTkocA15dxBpHvaaVKqgEozTLfye0/l5s0YgYAb4WpcY4qBg6Pw==" + "resolved": "2.8.2.3", + "contentHash": "uwz9pB3hMuxzI/bSkjVrsOJH7Wo1L+0Md5ZmEMDM/j7xDHtR9d3mfg/CfxhMIcTiUC4JgX49FZK0y2ojgu1dww==" }, "HarfBuzzSharp.NativeAssets.Win32": { "type": "Transitive", - "resolved": "2.8.2.1-preview.108", - "contentHash": "0ws24k21iRH2GRiOLEcG6ESl+VROOwaeHnC0vqKQChGmreGTJ//JBQJqIu189oY30G0NVdypDe1UwFA/scjBAw==" - }, - "Microsoft.Win32.SystemEvents": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "2nXPrhdAyAzir0gLl8Yy8S5Mnm/uBSQQA7jEsILOS1MTyS7DbmV1NgViMtvV1sfCD1ebITpNwb1NIinKeJgUVQ==" + "resolved": "2.8.2.3", + "contentHash": "Wo6QpE4+a+PFVdfIBoLkLr4wq2uC0m9TZC8FAfy4ZnLsUc10WL0Egk9EBHHhDCeokNOXDse5YtvuTYtS/rbHfg==" }, "runtime.any.System.Collections": { "type": "Transitive", @@ -2486,21 +2644,26 @@ }, "SkiaSharp.NativeAssets.Linux": { "type": "Transitive", - "resolved": "2.88.1-preview.108", - "contentHash": "1aOmUqcuzXJP0FaDL5JPRx7FbLFbiyl5R2lI1YwTTfXTpawnPxpPXlBClj+CuRrSS5Azfn8k3ZIHPHTd37vOWw==", + "resolved": "2.88.6", + "contentHash": "iQcOUE0tPZvBUxOdZaP3LIdAC21H8BEMhDvpCQ/mUUvbKGLd5rF7veJVSZBNu20SuCC0oZpEdGxB+mLVOK8uzw==", "dependencies": { - "SkiaSharp": "2.88.1-preview.108" + "SkiaSharp": "2.88.6" } }, "SkiaSharp.NativeAssets.macOS": { "type": "Transitive", - "resolved": "2.88.1-preview.108", - "contentHash": "nz+Ege0i1aCicLnaHOBzuTBj5LnLxlZVxLv+wUEtOXaAHq6of7kxaE+/+4KC1OBnKs64L8WDGf88VC2fIC/zxw==" + "resolved": "2.88.6", + "contentHash": "Sko9LFxRXSjb3OGh5/RxrVRXxYo48tr5NKuuSy6jB85GrYt8WRqVY1iLOLwtjPiVAt4cp+pyD4i30azanS64dw==" }, "SkiaSharp.NativeAssets.Win32": { "type": "Transitive", - "resolved": "2.88.1-preview.108", - "contentHash": "98r2fGVjPNjIhH0ooHtvAcqsHUjWZPEkqrfpynZNWdo8gkUPZhENvOodDtvBNUW6we24Bo4aWCnGbJuhyn//ug==" + "resolved": "2.88.6", + "contentHash": "7TzFO0u/g2MpQsTty4fyCDdMcfcWI+aLswwfnYXr3gtNS6VLKdMXPMeKpJa3pJSLnUBN6wD0JjuCe8OoLBQ6cQ==" + }, + "SQLitePCLRaw.lib.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.4", + "contentHash": "2C9Q9eX7CPLveJA0rIhf9RXAvu+7nWZu1A2MdG6SD/NOu26TakGgL1nsbc0JAspGijFOo3HoN79xrx8a368fBg==" }, "System.Collections": { "type": "Transitive", @@ -2537,33 +2700,33 @@ }, "System.Drawing.Common": { "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "KIX+oBU38pxkKPxvLcLfIkOV5Ien8ReN78wro7OF5/erwcmortzeFx+iBswlh2Vz6gVne0khocQudGwaO1Ey6A==", + "resolved": "6.0.0", + "contentHash": "NfuoKUiP2nUWwKZN6twGqXioIe1zVD0RIj2t976A+czLHr2nY454RwwXs6JU9Htc6mwqL6Dn/nEL3dpVf2jOhg==", "dependencies": { - "Microsoft.Win32.SystemEvents": "7.0.0" + "Microsoft.Win32.SystemEvents": "6.0.0" } }, "System.Globalization": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==", + "resolved": "4.0.11", + "contentHash": "B95h0YLEL2oSnwF/XjqSWKnwKOy/01VWkNlsCeMTFJLLabflpGV26nK164eRs5GiaRSBGpOxQ3pKoSnnyZN5pg==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", "runtime.any.System.Globalization": "4.3.0" } }, "System.IO": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==", + "resolved": "4.1.0", + "contentHash": "3KlTJceQc3gnGIaHZ7UBZO26SHL1SHE4ddrmiwumFnId+CEHP+O8r386tZKaE6zlk5/mF8vifMBzHj9SaXN+mQ==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading.Tasks": "4.3.0", + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", + "System.Text.Encoding": "4.0.11", + "System.Threading.Tasks": "4.0.11", "runtime.any.System.IO": "4.3.0" } }, @@ -2579,51 +2742,41 @@ }, "System.Reflection": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==", + "resolved": "4.1.0", + "contentHash": "JCKANJ0TI7kzoQzuwB/OoJANy1Lg338B6+JVacPl4TpUwi3cReg3nMLplMq2uqYfHFQpKIlHAUVAJlImZz/4ng==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.IO": "4.3.0", - "System.Reflection.Primitives": "4.3.0", - "System.Runtime": "4.3.0", + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.IO": "4.1.0", + "System.Reflection.Primitives": "4.0.1", + "System.Runtime": "4.1.0", "runtime.any.System.Reflection": "4.3.0" } }, "System.Reflection.Primitives": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==", + "resolved": "4.0.1", + "contentHash": "4inTox4wTBaDhB7V3mPvp9XlCbeGYWVEM9/fXALd52vNEAVisc1BoVWQPuUuD0Ga//dNbA/WeMy9u9mzLxGTHQ==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", "runtime.any.System.Reflection.Primitives": "4.3.0" } }, "System.Resources.ResourceManager": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "/zrcPkkWdZmI4F92gL/TPumP98AVDu/Wxr3CSJGQQ+XN6wbRZcyfSKVoPo17ilb3iOr0cCRqJInGwNMolqhS8A==", + "resolved": "4.0.1", + "contentHash": "TxwVeUNoTgUOdQ09gfTjvW411MF+w9MBYL7AtNVc+HtBCFlutPLhUCdZjNkjbhj3bNQWMdHboF0KIWEOjJssbA==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Globalization": "4.3.0", - "System.Reflection": "4.3.0", - "System.Runtime": "4.3.0", + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Globalization": "4.0.11", + "System.Reflection": "4.1.0", + "System.Runtime": "4.1.0", "runtime.any.System.Resources.ResourceManager": "4.3.0" } }, - "System.Runtime": { - "type": "Transitive", - "resolved": "4.3.1", - "contentHash": "abhfv1dTK6NXOmu4bgHIONxHyEqFjW8HwXPmpY9gmll+ix9UNo4XDcmzJn6oLooftxNssVHdJC1pGT9jkSynQg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.1", - "Microsoft.NETCore.Targets": "1.1.3", - "runtime.any.System.Runtime": "4.3.0" - } - }, "System.Runtime.Extensions": { "type": "Transitive", "resolved": "4.1.0", @@ -2660,19 +2813,14 @@ "runtime.any.System.Runtime.InteropServices": "4.3.0" } }, - "System.Security.Principal.Windows": { - "type": "Transitive", - "resolved": "4.7.0", - "contentHash": "ojD0PX0XhneCsUbAZVKdb7h/70vyYMDYs85lwEI+LngEONe/17A0cFaRFqZU+sOEidcVswYWikYOQ9PPfjlbtQ==" - }, "System.Text.Encoding": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==", + "resolved": "4.0.11", + "contentHash": "U3gGeMlDZXxCEiY4DwVLSacg+DFWCvoiX+JThA/rvw37Sqrku7sEFeVBBBMBnfB6FeZHsyDx85HlKL19x0HtZA==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", "runtime.any.System.Text.Encoding": "4.3.0" } }, @@ -2687,14 +2835,31 @@ }, "System.Threading.Tasks": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==", + "resolved": "4.0.11", + "contentHash": "k1S4Gc6IGwtHGT8188RSeGaX86Qw/wnrgNLshJvsdNUOPP9etMmo8S07c+UlOAx4K/xLuN9ivA1bD0LVurtIxQ==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", + "Microsoft.NETCore.Platforms": "1.0.1", + "Microsoft.NETCore.Targets": "1.0.1", + "System.Runtime": "4.1.0", "runtime.any.System.Threading.Tasks": "4.3.0" } + }, + "Microsoft.Win32.SystemEvents": { + "type": "CentralTransitive", + "requested": "[7.0.0, )", + "resolved": "7.0.0", + "contentHash": "2nXPrhdAyAzir0gLl8Yy8S5Mnm/uBSQQA7jEsILOS1MTyS7DbmV1NgViMtvV1sfCD1ebITpNwb1NIinKeJgUVQ==" + }, + "System.Runtime": { + "type": "CentralTransitive", + "requested": "[4.3.1, )", + "resolved": "4.3.1", + "contentHash": "abhfv1dTK6NXOmu4bgHIONxHyEqFjW8HwXPmpY9gmll+ix9UNo4XDcmzJn6oLooftxNssVHdJC1pGT9jkSynQg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.1", + "Microsoft.NETCore.Targets": "1.1.3", + "runtime.any.System.Runtime": "4.3.0" + } } } } diff --git a/WalletWasabi.Fluent.Generators/Abstractions/CombinedGenerator.cs b/WalletWasabi.Fluent.Generators/Abstractions/CombinedGenerator.cs new file mode 100644 index 0000000000..824de16dcf --- /dev/null +++ b/WalletWasabi.Fluent.Generators/Abstractions/CombinedGenerator.cs @@ -0,0 +1,96 @@ +using Microsoft.CodeAnalysis.Text; +using Microsoft.CodeAnalysis; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace WalletWasabi.Fluent.Generators.Abstractions; + +internal abstract class CombinedGenerator : ISourceGenerator +{ + private List> StepFactories { get; } = new(); + private List StaticFileGenerators { get; } = new(); + + public void Initialize(GeneratorInitializationContext context) + { + var files = + StaticFileGenerators.SelectMany(x => x.Generate()) + .ToArray(); + + if (files.Any()) + { + context.RegisterForPostInitialization(ctx => + { + foreach (var (fileName, source) in files) + { + ctx.AddSource(fileName, SourceText.From(source, Encoding.UTF8)); + } + }); + } + + if (StepFactories.Any()) + { + context.RegisterForSyntaxNotifications(() => new CombinedSyntaxReceiver(this)); + } + } + + public void Execute(GeneratorExecutionContext context) + { + if (context.SyntaxReceiver is not CombinedSyntaxReceiver receiver) + { + return; + } + + var compilation = context.Compilation; + + foreach (var step in receiver.Steps) + { + step.Initialize(context, compilation); + step.OnInitialize(compilation, receiver.Steps); + step.Execute(); + + // This is the core part of CombinedGenerator. + // Each step creates a new Compilation, containing additional syntax trees + // The CombinedGenerator passes the new Compilation (with the added Syntax Trees) from step to step + // in order to be able to semantically analyze the types declared inside those syntax trees + // otherwise SemanticModel has no information about those types and therefore it returns null or errored out type symbols + // for types declared in source generated files. + compilation = step.Context.Compilation; + } + } + + protected void Add() where T : GeneratorStep, new() + { + StepFactories.Add(() => new T()); + } + + protected void AddStaticFileGenerator() where T : StaticFileGenerator, new() + { + StaticFileGenerators.Add(new T()); + } + + private class CombinedSyntaxReceiver : ISyntaxReceiver + { + public CombinedSyntaxReceiver(CombinedGenerator generator) + { + Steps = + generator.StepFactories + .Select(x => x()) + .ToArray(); + } + + public GeneratorStep[] Steps { get; } + + public void OnVisitSyntaxNode(SyntaxNode syntaxNode) + { + foreach (var step in Steps) + { + if (step is ISyntaxReceiver receiver) + { + receiver.OnVisitSyntaxNode(syntaxNode); + } + } + } + } +} diff --git a/WalletWasabi.Fluent.Generators/Abstractions/GeneratorStep.cs b/WalletWasabi.Fluent.Generators/Abstractions/GeneratorStep.cs new file mode 100644 index 0000000000..c7fb241b95 --- /dev/null +++ b/WalletWasabi.Fluent.Generators/Abstractions/GeneratorStep.cs @@ -0,0 +1,44 @@ +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Text; +using Microsoft.CodeAnalysis; +using System.Text; + +namespace WalletWasabi.Fluent.Generators.Abstractions; + +internal abstract class GeneratorStep +{ + private readonly object _lock = new(); + + public GeneratorStepContext Context { get; private set; } + + public void Initialize(GeneratorExecutionContext context, Compilation compilation) + { + Context = new GeneratorStepContext(context, compilation); + } + + public virtual void OnInitialize(Compilation compilation, GeneratorStep[] steps) + { + } + + public abstract void Execute(); + + protected SyntaxTree AddSource(string name, string source) + { + var syntaxTree = SyntaxFactory.ParseSyntaxTree(source, Context.Context.ParseOptions); + Context.Context.AddSource(name, SourceText.From(source, Encoding.UTF8)); + + lock (_lock) + { + Context = Context with { Compilation = Context.Compilation.AddSyntaxTrees(syntaxTree) }; + } + + return syntaxTree; + } + + protected void ReportDiagnostic(DiagnosticDescriptor diagnosticDescriptor, Location? location) + { + Context.Context.ReportDiagnostic(Diagnostic.Create(diagnosticDescriptor, location)); + } + + protected SemanticModel GetSemanticModel(SyntaxTree syntaxTree) => Context.Compilation.GetSemanticModel(syntaxTree); +} diff --git a/WalletWasabi.Fluent.Generators/Abstractions/GeneratorStepContext.cs b/WalletWasabi.Fluent.Generators/Abstractions/GeneratorStepContext.cs new file mode 100644 index 0000000000..47590bb60c --- /dev/null +++ b/WalletWasabi.Fluent.Generators/Abstractions/GeneratorStepContext.cs @@ -0,0 +1,5 @@ +using Microsoft.CodeAnalysis; + +namespace WalletWasabi.Fluent.Generators.Abstractions; + +internal record GeneratorStepContext(GeneratorExecutionContext Context, Compilation Compilation); diff --git a/WalletWasabi.Fluent.Generators/Abstractions/GeneratorStep`1.cs b/WalletWasabi.Fluent.Generators/Abstractions/GeneratorStep`1.cs new file mode 100644 index 0000000000..40190b98d2 --- /dev/null +++ b/WalletWasabi.Fluent.Generators/Abstractions/GeneratorStep`1.cs @@ -0,0 +1,31 @@ +using Microsoft.CodeAnalysis; +using System.Collections.Generic; + +namespace WalletWasabi.Fluent.Generators.Abstractions; + +internal abstract class GeneratorStep : GeneratorStep, ISyntaxReceiver where T : SyntaxNode +{ + private List _nodes = new(); + + public void OnVisitSyntaxNode(SyntaxNode syntaxNode) + { + if (syntaxNode is not T node) + { + return; + } + + if (Filter(node)) + { + _nodes.Add(node); + } + } + + public override void Execute() + { + Execute(_nodes.ToArray()); + } + + public abstract void Execute(T[] syntaxNodes); + + public virtual bool Filter(T node) => true; +} diff --git a/WalletWasabi.Fluent.Generators/Abstractions/IsExternalInit.cs b/WalletWasabi.Fluent.Generators/Abstractions/IsExternalInit.cs new file mode 100644 index 0000000000..2652b14103 --- /dev/null +++ b/WalletWasabi.Fluent.Generators/Abstractions/IsExternalInit.cs @@ -0,0 +1,10 @@ +#pragma warning disable IDE0130 // Namespace does not match folder structure +namespace System.Runtime.CompilerServices; +#pragma warning restore IDE0130 // Namespace does not match folder structure + +/// +/// Required by the C# compiler to use value tuples in a .NET Standard 2.0 project. Do not remove this class. +/// +internal static class IsExternalInit +{ +} diff --git a/WalletWasabi.Fluent.Generators/Abstractions/StaticFileGenerator.cs b/WalletWasabi.Fluent.Generators/Abstractions/StaticFileGenerator.cs new file mode 100644 index 0000000000..5a5c1bb4ba --- /dev/null +++ b/WalletWasabi.Fluent.Generators/Abstractions/StaticFileGenerator.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace WalletWasabi.Fluent.Generators.Abstractions; + +internal abstract class StaticFileGenerator +{ + public abstract IEnumerable<(string FileName, string Source)> Generate(); +} diff --git a/WalletWasabi.Fluent.Generators/AnalyzerExtensions.cs b/WalletWasabi.Fluent.Generators/AnalyzerExtensions.cs new file mode 100644 index 0000000000..609c7c6cfd --- /dev/null +++ b/WalletWasabi.Fluent.Generators/AnalyzerExtensions.cs @@ -0,0 +1,247 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System; +using System.Collections.Generic; +using System.Linq; +using WalletWasabi.Fluent.Generators.Analyzers; + +namespace WalletWasabi.Fluent.Generators; + +public static class AnalyzerExtensions +{ + public static List GetUiContextReferences(this SyntaxNode node, SemanticModel semanticModel) + { + var directReferences = + node.DescendantNodes() + .OfType() + .Where(x => x.Identifier.ValueText == "UiContext") // faster verification + .Where(x => semanticModel.GetTypeInfo(x).Type?.ToDisplayString() == UiContextAnalyzer.UiContextType) // slower, but safer. Only runs if previous verification passed. + .ToList(); + + var indirectReferences = + node.DescendantNodes() + .OfType() + .Where(x => x.Identifier.ValueText is "Navigate" or "NavigateDialogAsync") + .Where(x => semanticModel.GetSymbolInfo(x).Symbol?.Kind == SymbolKind.Method) + .ToList(); + + return + directReferences.Concat(indirectReferences) + .ToList(); + } + + public static bool IsPrivate(this ConstructorDeclarationSyntax node) + { + return node.Modifiers.Any(m => m.IsKind(SyntaxKind.PrivateKeyword)); + } + + public static bool IsPublic(this ConstructorDeclarationSyntax node) + { + return node.Modifiers.Any(m => m.IsKind(SyntaxKind.PublicKeyword)); + } + + public static bool IsSourceGenerated(this SyntaxNode node) + { + var filePath = node.SyntaxTree.FilePath; + + return filePath is null || + filePath.EndsWith(UiContextAnalyzer.UiContextFileSuffix); + } + + public static bool IsSubTypeOf(this SyntaxNode node, SemanticModel model, string baseType) + { + if (node is not ClassDeclarationSyntax cls) + { + return false; + } + + var currentType = model.GetDeclaredSymbol(cls); + while (currentType != null) + { + if (currentType.ToDisplayString() == baseType) + { + return true; + } + currentType = currentType.BaseType; + } + + return false; + } + + public static bool IsAbstractClass(this ClassDeclarationSyntax cls, SemanticModel model) + { + var typeInfo = model.GetDeclaredSymbol(cls) + ?? throw new InvalidOperationException($"Unable to get Declared Symbol: {cls.Identifier}"); + + return typeInfo.IsAbstract; + } + + public static bool IsRoutableViewModel(this SyntaxNode node, SemanticModel model) + { + return node.IsSubTypeOf(model, "WalletWasabi.Fluent.ViewModels.Navigation.RoutableViewModel"); + } + + public static (string? TypeName, IEnumerable Namespaces) GetDialogResultType(this SyntaxNode node, SemanticModel model) + { + if (node is not ClassDeclarationSyntax cls) + { + return (null, Array.Empty()); + } + + var currentType = model.GetDeclaredSymbol(cls); + while (currentType != null) + { + if (currentType.ConstructedFrom?.ToDisplayString() == UiContextAnalyzer.DialogViewModelBaseType) + { + var typeArgument = currentType.TypeArguments.FirstOrDefault(); + if (typeArgument is { }) + { + return (typeArgument.ToDisplayString(), typeArgument.GetNamespaces()); + } + } + currentType = currentType.BaseType; + } + + return (null, Array.Empty()); + } + + public static bool HasUiContextParameter(this ConstructorDeclarationSyntax ctor, SemanticModel model) + { + return ctor.ParameterList.Parameters.Any(p => p.Type.IsUiContextType(model)); + } + + public static bool IsUiContextType(this TypeSyntax? typeSyntax, SemanticModel model) + { + if (typeSyntax is null) + { + return false; + } + + return model.GetTypeInfo(typeSyntax).Type?.ToDisplayString() == UiContextAnalyzer.UiContextType; + } + + public static List GetNamespaces(this ITypeSymbol? typeSymbol) + { + return GetNamespaceSymbols(typeSymbol) + .Where(x => !x.IsGlobalNamespace) + .Select(x => x.ToDisplayString()) + .ToList(); + } + + private static IEnumerable GetNamespaceSymbols(this ITypeSymbol? typeSymbol) + { + if (typeSymbol is null) + { + yield break; + } + + yield return typeSymbol.ContainingNamespace; + + if (typeSymbol is INamedTypeSymbol namedType) + { + foreach (var typeArg in namedType.TypeArguments) + { + yield return typeArg.ContainingNamespace; + } + } + } + + public static string SimplifyType(this ITypeSymbol typeSymbol, List namespaces) + { + if (typeSymbol is IArrayTypeSymbol arrayType) + { + var dimensions = new string(',', arrayType.Rank - 1); + + return $"{arrayType.ElementType.SimplifyType(namespaces)}[{dimensions}]"; + } + + if (typeSymbol is not INamedTypeSymbol type) + { + return ""; + } + + if (type.NullableAnnotation == NullableAnnotation.Annotated && type.Name == "Nullable") + { + return type.TypeArguments.First().SimplifyType(namespaces) + "?"; + } + + if (!type.ContainingNamespace.IsGlobalNamespace) + { + namespaces.Add(type.ContainingNamespace.ToDisplayString()); + } + + var typeName = + type.SpecialType switch + { + SpecialType.System_Object => "object", + SpecialType.System_Void => "void", + SpecialType.System_Boolean => "bool", + SpecialType.System_Char => "char", + SpecialType.System_Byte => "byte", + SpecialType.System_Int16 => "short", + SpecialType.System_Int32 => "int", + SpecialType.System_Int64 => "long", + SpecialType.System_Decimal => "decimal", + SpecialType.System_Single => "float", + SpecialType.System_Double => "double", + SpecialType.System_String => "string", + _ => type.Name + }; + + if (type.ContainingType is { } containingType) + { + typeName = containingType.SimplifyType(namespaces) + "." + typeName; + } + + if (type.IsTupleType) + { + typeName = "("; + + var elements = + from element in type.TupleElements + let elementType = element.Type.SimplifyType(namespaces) + let elementName = element.Name + select $"{elementType} {elementName}"; + + typeName += string.Join(", ", elements); + + typeName += ")"; + } + else if (type.IsGenericType) + { + typeName += "<"; + + var typeArguments = + from argument in type.TypeArguments + let argumentType = argument.SimplifyType(namespaces) + select argumentType; + + typeName += string.Join(", ", typeArguments); + + typeName += ">"; + } + + if (type.NullableAnnotation == NullableAnnotation.Annotated) + { + typeName += "?"; + } + + return typeName; + } + + public static string? GetExplicitDefaultValueString(this IParameterSymbol parameter) + { + if (!parameter.HasExplicitDefaultValue) + { + return null; + } + + return parameter.ExplicitDefaultValue switch + { + string s => $"\"{s}\"", + null => "null", + _ => parameter.ExplicitDefaultValue.ToString() + }; + } +} diff --git a/WalletWasabi.Fluent.Generators/AnalyzerReleases.Shipped.md b/WalletWasabi.Fluent.Generators/AnalyzerReleases.Shipped.md new file mode 100644 index 0000000000..60b59dd99b --- /dev/null +++ b/WalletWasabi.Fluent.Generators/AnalyzerReleases.Shipped.md @@ -0,0 +1,3 @@ +; Shipped analyzer releases +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + diff --git a/WalletWasabi.Fluent.Generators/AnalyzerReleases.Unshipped.md b/WalletWasabi.Fluent.Generators/AnalyzerReleases.Unshipped.md new file mode 100644 index 0000000000..433ea97a3d --- /dev/null +++ b/WalletWasabi.Fluent.Generators/AnalyzerReleases.Unshipped.md @@ -0,0 +1,9 @@ +; Unshipped analyzer release +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +WW001 | Wasabi Wallet | Error | UiContextAnalyzer +WW002 | Wasabi Wallet | Error | UiContextAnalyzer \ No newline at end of file diff --git a/WalletWasabi.Fluent.Generators/Analyzers/UIContextUsageInConstructorAnalyzer.cs b/WalletWasabi.Fluent.Generators/Analyzers/UIContextUsageInConstructorAnalyzer.cs new file mode 100644 index 0000000000..bc8becfbce --- /dev/null +++ b/WalletWasabi.Fluent.Generators/Analyzers/UIContextUsageInConstructorAnalyzer.cs @@ -0,0 +1,109 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Immutable; +using System.Linq; + +namespace WalletWasabi.Fluent.Generators.Analyzers; + +/// +/// Report an error if UiContext is referenced in the constructor directly without being closed on by a lambda expression. +/// UiContext cannot be referenced in constructor because it hasn't been initialized yet. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class UiContextAnalyzer : DiagnosticAnalyzer +{ + public const string UiContextType = "WalletWasabi.Fluent.Models.UI.UiContext"; + public const string UiContextFileSuffix = "_UiContext.cs"; + public const string DialogViewModelBaseType = "WalletWasabi.Fluent.ViewModels.Dialogs.Base.DialogViewModelBase"; + + private static readonly string[] ExcludedClasses = { "MainViewModel", "RoutableViewModel" }; + + internal static readonly DiagnosticDescriptor Rule1 = + new("WW001", + "Do not use UiContext or Navigation APIs in ViewModel Constructor", + "UiContext cannot be referenced in a ViewModel's constructor because it hasn't been initialized yet when constructor runs. Use OnNavigatedTo() or OnActivated() instead. Alternatively, make the constructor public and explicitly initialize UiContext. See https://github.com/zkSNACKs/WalletWasabi/blob/master/CONTRIBUTING.md#source-generated-viewmodel-constructors for details.", + "Wasabi Wallet", + DiagnosticSeverity.Error, + true); + + internal static readonly DiagnosticDescriptor Rule2 = + new("WW002", + "Make ViewModel Constructor private", + "This ViewModel Constructor must be made private, since the only valid public constructor is the autogenerated one", + "Wasabi Wallet", + DiagnosticSeverity.Error, + true); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule1, Rule2); + + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.ConstructorDeclaration); + } + + private static void Analyze(SyntaxNodeAnalysisContext context) + { + if (context.Node is not ConstructorDeclarationSyntax ctor) + { + return; + } + + if (context.Node.IsSourceGenerated()) + { + return; + } + + var isViewModel = + ctor.Parent is ClassDeclarationSyntax cls && + cls.Identifier.ValueText is string className && + className.EndsWith("ViewModel") && + !ExcludedClasses.Contains(className); + + if (!isViewModel) + { + return; + } + + var uiContextReferenceInConstructor = + ctor + .GetUiContextReferences(context.SemanticModel) + .Where(static x => x.FirstAncestorOrSelf() != null) + .Where(static x => x.FirstAncestorOrSelf() == null) + .FirstOrDefault(); + + // if constructor already has a UIContext parameter, leave it be. Don't raise any warnings. + var ctorHasUiContextParameter = + ctor.ParameterList.Parameters.Any(x => x.Type.IsUiContextType(context.SemanticModel)); + + if (ctorHasUiContextParameter) + { + return; + } + + if (uiContextReferenceInConstructor != null) + { + var location = uiContextReferenceInConstructor.GetLocation(); + var diagnostic = Diagnostic.Create(Rule1, location); + context.ReportDiagnostic(diagnostic); + } + + if (ctor.Parent is not ClassDeclarationSyntax classDeclaration) + { + return; + } + + var uiContextReferencesInClass = + classDeclaration.GetUiContextReferences(context.SemanticModel); + + if (uiContextReferencesInClass.Any() && !ctor.IsPrivate()) + { + var location = ctor.GetLocation(); + var diagnostic = Diagnostic.Create(Rule2, location); + context.ReportDiagnostic(diagnostic); + } + } +} diff --git a/WalletWasabi.Fluent.Generators/AutoNotifyGenerator.cs b/WalletWasabi.Fluent.Generators/AutoNotifyGenerator.cs deleted file mode 100644 index 7096134287..0000000000 --- a/WalletWasabi.Fluent.Generators/AutoNotifyGenerator.cs +++ /dev/null @@ -1,268 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Text; - -namespace WalletWasabi.Fluent.Generators; - -[Generator] -public class AutoNotifyGenerator : ISourceGenerator -{ - private const string AutoNotifyAttributeDisplayString = "WalletWasabi.Fluent.AutoNotifyAttribute"; - - private const string ReactiveObjectDisplayString = "ReactiveUI.ReactiveObject"; - - private const string AttributeText = @"// -#nullable enable -using System; - -namespace WalletWasabi.Fluent -{ - [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] - sealed class AutoNotifyAttribute : Attribute - { - public AutoNotifyAttribute() - { - } - - public string? PropertyName { get; set; } - - public AccessModifier SetterModifier { get; set; } = AccessModifier.Public; - } -}"; - - private const string ModifierText = @"// - -namespace WalletWasabi.Fluent -{ - public enum AccessModifier - { - None = 0, - Public = 1, - Protected = 2, - Private = 3, - Internal = 4 - } -}"; - - public void Initialize(GeneratorInitializationContext context) - { - // System.Diagnostics.Debugger.Launch(); - context.RegisterForPostInitialization((i) => - { - i.AddSource("AccessModifier.cs", SourceText.From(ModifierText, Encoding.UTF8)); - i.AddSource("AutoNotifyAttribute.cs", SourceText.From(AttributeText, Encoding.UTF8)); - }); - - context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); - } - - public void Execute(GeneratorExecutionContext context) - { - if (context.SyntaxContextReceiver is not SyntaxReceiver receiver) - { - return; - } - - var attributeSymbol = context.Compilation.GetTypeByMetadataName(AutoNotifyAttributeDisplayString); - if (attributeSymbol is null) - { - return; - } - - var notifySymbol = context.Compilation.GetTypeByMetadataName(ReactiveObjectDisplayString); - if (notifySymbol is null) - { - return; - } - - // TODO: https://github.com/dotnet/roslyn/issues/49385 -#pragma warning disable RS1024 - var groupedFields = receiver.FieldSymbols.GroupBy(f => f.ContainingType); -#pragma warning restore RS1024 - - foreach (var group in groupedFields) - { - var classSource = ProcessClass(group.Key, group.ToList(), attributeSymbol, notifySymbol); - if (classSource is null) - { - continue; - } - context.AddSource($"{group.Key.Name}_AutoNotify.cs", SourceText.From(classSource, Encoding.UTF8)); - } - } - - private static string? ProcessClass(INamedTypeSymbol classSymbol, List fields, ISymbol attributeSymbol, INamedTypeSymbol notifySymbol) - { - if (!classSymbol.ContainingSymbol.Equals(classSymbol.ContainingNamespace, SymbolEqualityComparer.Default)) - { - return null; - } - - string namespaceName = classSymbol.ContainingNamespace.ToDisplayString(); - - var addNotifyInterface = !classSymbol.Interfaces.Contains(notifySymbol); - var baseType = classSymbol.BaseType; - while (true) - { - if (baseType is null) - { - break; - } - - if (SymbolEqualityComparer.Default.Equals(baseType, notifySymbol)) - { - addNotifyInterface = false; - break; - } - - baseType = baseType.BaseType; - } - - var source = new StringBuilder(); - - var format = new SymbolDisplayFormat( - typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypes, - genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters | SymbolDisplayGenericsOptions.IncludeTypeConstraints | SymbolDisplayGenericsOptions.IncludeVariance); - - if (addNotifyInterface) - { - source.Append($@"// -#nullable enable -using ReactiveUI; - -namespace {namespaceName} -{{ - public partial class {classSymbol.ToDisplayString(format)} : {notifySymbol.ToDisplayString()} - {{"); - } - else - { - source.Append($@"// -#nullable enable -using ReactiveUI; - -namespace {namespaceName} -{{ - public partial class {classSymbol.ToDisplayString(format)} - {{"); - } - - foreach (IFieldSymbol fieldSymbol in fields) - { - ProcessField(source, fieldSymbol, attributeSymbol); - } - - source.Append($@" - }} -}}"); - - return source.ToString(); - } - - private static void ProcessField(StringBuilder source, IFieldSymbol fieldSymbol, ISymbol attributeSymbol) - { - var fieldName = fieldSymbol.Name; - var fieldType = fieldSymbol.Type; - var attributeData = fieldSymbol.GetAttributes().Single(ad => ad?.AttributeClass?.Equals(attributeSymbol, SymbolEqualityComparer.Default) ?? false); - var overridenNameOpt = attributeData.NamedArguments.SingleOrDefault(kvp => kvp.Key == "PropertyName").Value; - var propertyName = ChooseName(fieldName, overridenNameOpt); - - if (propertyName is null || propertyName.Length == 0 || propertyName == fieldName) - { - // Issue a diagnostic that we can't process this field. - return; - } - - var overridenSetterModifierOpt = attributeData.NamedArguments.SingleOrDefault(kvp => kvp.Key == "SetterModifier").Value; - var setterModifier = ChooseSetterModifier(overridenSetterModifierOpt); - if (setterModifier is null) - { - source.Append($@" - public {fieldType} {propertyName} - {{ - get => {fieldName}; - }}"); - } - else - { - source.Append($@" - public {fieldType} {propertyName} - {{ - get => {fieldName}; - {setterModifier}set => this.RaiseAndSetIfChanged(ref {fieldName}, value); - }}"); - } - - static string? ChooseSetterModifier(TypedConstant overridenSetterModifierOpt) - { - if (!overridenSetterModifierOpt.IsNull && overridenSetterModifierOpt.Value is not null) - { - var value = (int)overridenSetterModifierOpt.Value; - return value switch - { - 0 => null,// None - 1 => "",// Public - 2 => "protected ",// Protected - 3 => "private ",// Private - 4 => "internal ",// Internal - _ => ""// Default - }; - } - else - { - return ""; - } - } - - static string? ChooseName(string fieldName, TypedConstant overridenNameOpt) - { - if (!overridenNameOpt.IsNull) - { - return overridenNameOpt.Value?.ToString(); - } - - fieldName = fieldName.TrimStart('_'); - if (fieldName.Length == 0) - { - return string.Empty; - } - - if (fieldName.Length == 1) - { - return fieldName.ToUpper(); - } - - return fieldName.Substring(0, 1).ToUpper() + fieldName.Substring(1); - } - } - - private class SyntaxReceiver : ISyntaxContextReceiver - { - public List FieldSymbols { get; } = new(); - - public void OnVisitSyntaxNode(GeneratorSyntaxContext context) - { - if (context.Node is FieldDeclarationSyntax fieldDeclarationSyntax - && fieldDeclarationSyntax.AttributeLists.Count > 0) - { - foreach (VariableDeclaratorSyntax variable in fieldDeclarationSyntax.Declaration.Variables) - { - if (context.SemanticModel.GetDeclaredSymbol(variable) is not IFieldSymbol fieldSymbol) - { - continue; - } - - var attributes = fieldSymbol.GetAttributes(); - if (attributes.Any(ad => ad?.AttributeClass?.ToDisplayString() == AutoNotifyAttributeDisplayString)) - { - FieldSymbols.Add(fieldSymbol); - } - } - } - } - } -} diff --git a/WalletWasabi.Fluent.Generators/Generators/AutoInterfaceAttributeGenerator.cs b/WalletWasabi.Fluent.Generators/Generators/AutoInterfaceAttributeGenerator.cs new file mode 100644 index 0000000000..e227e56b31 --- /dev/null +++ b/WalletWasabi.Fluent.Generators/Generators/AutoInterfaceAttributeGenerator.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using WalletWasabi.Fluent.Generators.Abstractions; + +namespace WalletWasabi.Fluent.Generators.Generators; + +internal class AutoInterfaceAttributeGenerator: StaticFileGenerator +{ + private const string AttributeText = + """ + // + #nullable enable + using System; + + namespace WalletWasabi.Fluent; + + [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + sealed class AutoInterfaceAttribute : Attribute + { + public AutoInterfaceAttribute() + { + } + } + """; + + public override IEnumerable<(string FileName, string Source)> Generate() + { + yield return ("AutoInterface.g.cs", AttributeText); + } +} diff --git a/WalletWasabi.Fluent.Generators/Generators/AutoInterfaceGenerator.cs b/WalletWasabi.Fluent.Generators/Generators/AutoInterfaceGenerator.cs new file mode 100644 index 0000000000..477d920ff7 --- /dev/null +++ b/WalletWasabi.Fluent.Generators/Generators/AutoInterfaceGenerator.cs @@ -0,0 +1,143 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Collections.Generic; +using System.Linq; +using WalletWasabi.Fluent.Generators.Abstractions; + +namespace WalletWasabi.Fluent.Generators.Generators; + +internal class AutoInterfaceGenerator : GeneratorStep +{ + private const string AutoInterfaceAttribute = "WalletWasabi.Fluent.AutoInterfaceAttribute"; + + public override void Execute(ClassDeclarationSyntax[] syntaxNodes) + { + foreach (var cls in syntaxNodes) + { + Execute(cls); + } + } + + private void Execute(ClassDeclarationSyntax cls) + { + var semanticModel = GetSemanticModel(cls.SyntaxTree); + + if (semanticModel.GetDeclaredSymbol(cls) is not INamedTypeSymbol namedTypeSymbol) + { + return; + } + + var hasAutoInterfaceAttribute = + cls.AttributeLists + .Any(al => al.Attributes.Any(attr => semanticModel.GetTypeInfo(attr).Type?.ToDisplayString() == AutoInterfaceAttribute)); + + if (!hasAutoInterfaceAttribute) + { + return; + } + + var className = namedTypeSymbol.Name; + var interfaceNamespace = namedTypeSymbol.ContainingNamespace.ToDisplayString(); + var interfaceName = $"I{namedTypeSymbol.Name}"; + + var members = + namedTypeSymbol.GetMembers() + .Where(x => x.DeclaredAccessibility == Accessibility.Public) + .Where(x => !x.IsStatic) + .ToList(); + + var namespaces = new List(); + var properties = new List(); + var methods = new List(); + foreach (var member in members) + { + if (member is IPropertySymbol property) + { + var accessors = + property.SetMethod switch + { + IMethodSymbol s when s.IsInitOnly => "{ get; init; }", + IMethodSymbol s => "{ get; set; }", + _ => "{ get; }" + }; + + var type = property.Type.SimplifyType(namespaces); + properties.Add($"\t{type} {property.Name} {accessors}"); + } + else if (member is IMethodSymbol method && method.MethodKind == MethodKind.Ordinary) + { + var returnType = method.ReturnType.SimplifyType(namespaces); + + var signature = "\t"; + + signature += returnType; + + signature += $" {method.Name}"; + if (method.IsGenericMethod) + { + signature += "<"; + + var typeArgs = + from argument in method.TypeArguments + let typeName = argument.SimplifyType(namespaces) + select typeName; + + signature += string.Join(", ", typeArgs); + + signature += ">"; + } + + signature += "("; + + var parameters = + from parameter in method.Parameters + let declaration = parameter.DeclaringSyntaxReferences.First().GetSyntax() + let attributeList = declaration.DescendantNodes().OfType().FirstOrDefault() + let attributeTypes = parameter.GetAttributes().Select(attr => attr.AttributeClass?.SimplifyType(namespaces)).ToList() + let refKind = parameter.RefKind == RefKind.Out ? "out " : "" + let type = parameter.Type.SimplifyType(namespaces) + let name = parameter.Name + let defaultValue = parameter.GetExplicitDefaultValueString() + let defaultValueString = defaultValue != null ? " = " + defaultValue : null + select $"{attributeList?.ToFullString()}{refKind}{type} {name}{defaultValueString}"; + + signature += string.Join(", ", parameters); + + signature += ");"; + + methods.Add(signature); + } + } + + namespaces = + namespaces.Distinct() + .OrderBy(x => x) + .Where(x => x != interfaceNamespace) + .Select(x => $"using {x};") + .ToList(); + + var source = + $$""" + // + + #nullable enable + {{string.Join("\r\n", namespaces)}} + + namespace {{interfaceNamespace}}; + + public partial class {{className}}: {{interfaceName}} + { + } + + public partial interface {{interfaceName}} + { + {{string.Join("\r\n\r\n", properties)}} + + {{string.Join("\r\n\r\n", methods)}} + } + """; + + AddSource($"{className}.AutoInterface.g.cs", source); + } +} diff --git a/WalletWasabi.Fluent.Generators/Generators/AutoNotifyAttributeGenerator.cs b/WalletWasabi.Fluent.Generators/Generators/AutoNotifyAttributeGenerator.cs new file mode 100644 index 0000000000..9156390d55 --- /dev/null +++ b/WalletWasabi.Fluent.Generators/Generators/AutoNotifyAttributeGenerator.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using WalletWasabi.Fluent.Generators.Abstractions; + +namespace WalletWasabi.Fluent.Generators.Generators; + +internal class AutoNotifyAttributeGenerator : StaticFileGenerator +{ + private const string AttributeText = + """ + // + #nullable enable + using System; + + namespace WalletWasabi.Fluent; + + [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] + sealed class AutoNotifyAttribute : Attribute + { + public AutoNotifyAttribute() + { + } + + public string? PropertyName { get; set; } + + public AccessModifier SetterModifier { get; set; } = AccessModifier.Public; + } + """; + + private const string ModifierText = + """ + // + namespace WalletWasabi.Fluent; + + public enum AccessModifier + { + None = 0, + Public = 1, + Protected = 2, + Private = 3, + Internal = 4 + } + """; + + public override IEnumerable<(string FileName, string Source)> Generate() + { + yield return ("AccessModifier.g.cs", ModifierText); + yield return ("AutoNotifyAttribute.g.cs", AttributeText); + } +} diff --git a/WalletWasabi.Fluent.Generators/Generators/AutoNotifyGenerator.cs b/WalletWasabi.Fluent.Generators/Generators/AutoNotifyGenerator.cs new file mode 100644 index 0000000000..f47f2e67d8 --- /dev/null +++ b/WalletWasabi.Fluent.Generators/Generators/AutoNotifyGenerator.cs @@ -0,0 +1,235 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using WalletWasabi.Fluent.Generators.Abstractions; + +namespace WalletWasabi.Fluent.Generators.Generators; + +internal class AutoNotifyGenerator : GeneratorStep +{ + private const string AutoNotifyAttributeDisplayString = "WalletWasabi.Fluent.AutoNotifyAttribute"; + private const string ReactiveObjectDisplayString = "ReactiveUI.ReactiveObject"; + + public override bool Filter(FieldDeclarationSyntax field) + { + return field.AttributeLists.Count > 0; + } + + public override void Execute(FieldDeclarationSyntax[] syntaxNodes) + { + var fieldSymbols = GetAutoNotifyFields(syntaxNodes).ToArray(); + + var attributeSymbol = Context.Compilation.GetTypeByMetadataName(AutoNotifyAttributeDisplayString); + if (attributeSymbol is null) + { + return; + } + + var notifySymbol = Context.Compilation.GetTypeByMetadataName(ReactiveObjectDisplayString); + if (notifySymbol is null) + { + return; + } + + // TODO: https://github.com/dotnet/roslyn/issues/49385 +#pragma warning disable RS1024 + var groupedFields = fieldSymbols.GroupBy(f => f.ContainingType); +#pragma warning restore RS1024 + + foreach (var group in groupedFields) + { + var classSource = ProcessClass(group.Key, group.ToList(), attributeSymbol, notifySymbol); + if (classSource is null) + { + continue; + } + + AddSource($"{group.Key.Name}_AutoNotify.cs", classSource); + } + } + + private string? ProcessClass(INamedTypeSymbol classSymbol, List fields, ISymbol attributeSymbol, INamedTypeSymbol notifySymbol) + { + if (!classSymbol.ContainingSymbol.Equals(classSymbol.ContainingNamespace, SymbolEqualityComparer.Default)) + { + return null; + } + + string namespaceName = classSymbol.ContainingNamespace.ToDisplayString(); + + var addNotifyInterface = !classSymbol.Interfaces.Contains(notifySymbol); + var baseType = classSymbol.BaseType; + while (true) + { + if (baseType is null) + { + break; + } + + if (SymbolEqualityComparer.Default.Equals(baseType, notifySymbol)) + { + addNotifyInterface = false; + break; + } + + baseType = baseType.BaseType; + } + + var source = new StringBuilder(); + + var format = new SymbolDisplayFormat( + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypes, + genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters | SymbolDisplayGenericsOptions.IncludeTypeConstraints | SymbolDisplayGenericsOptions.IncludeVariance); + + if (addNotifyInterface) + { + source.Append( + $$""" + // + + #nullable enable + using ReactiveUI; + + namespace {{namespaceName}}; + + public partial class {{classSymbol.ToDisplayString(format)}} : {{notifySymbol.ToDisplayString()}} + { + """); + } + else + { + source.Append( + $$""" + // + #nullable enable + using ReactiveUI; + + namespace {{namespaceName}}; + + public partial class {{classSymbol.ToDisplayString(format)}} + { + """); + } + + foreach (IFieldSymbol fieldSymbol in fields) + { + ProcessField(source, fieldSymbol, attributeSymbol); + } + + source.Append( + """ + + } + """); + + return source.ToString(); + } + + private void ProcessField(StringBuilder source, IFieldSymbol fieldSymbol, ISymbol attributeSymbol) + { + var fieldName = fieldSymbol.Name; + var fieldType = fieldSymbol.Type; + var attributeData = fieldSymbol.GetAttributes().Single(ad => ad?.AttributeClass?.Equals(attributeSymbol, SymbolEqualityComparer.Default) ?? false); + var overriddenNameOpt = attributeData.NamedArguments.SingleOrDefault(kvp => kvp.Key == "PropertyName").Value; + var propertyName = ChooseName(fieldName, overriddenNameOpt); + + if (propertyName is null || propertyName.Length == 0 || propertyName == fieldName) + { + // Issue a diagnostic that we can't process this field. + return; + } + + var overriddenSetterModifierOpt = attributeData.NamedArguments.SingleOrDefault(kvp => kvp.Key == "SetterModifier").Value; + var setterModifier = ChooseSetterModifier(overriddenSetterModifierOpt); + if (setterModifier is null) + { + source.Append( + $$""" + + public {{fieldType}} {{propertyName}} + { + get => {{fieldName}}; + } + """); + } + else + { + source.Append( + $$""" + + public {{fieldType}} {{propertyName}} + { + get => {{fieldName}}; + {{setterModifier}}set => this.RaiseAndSetIfChanged(ref {{fieldName}}, value); + } + """); + } + + static string? ChooseSetterModifier(TypedConstant overriddenSetterModifierOpt) + { + if (!overriddenSetterModifierOpt.IsNull && overriddenSetterModifierOpt.Value is not null) + { + var value = (int)overriddenSetterModifierOpt.Value; + return value switch + { + 0 => null,// None + 1 => "",// Public + 2 => "protected ",// Protected + 3 => "private ",// Private + 4 => "internal ",// Internal + _ => ""// Default + }; + } + else + { + return ""; + } + } + + static string? ChooseName(string fieldName, TypedConstant overriddenNameOpt) + { + if (!overriddenNameOpt.IsNull) + { + return overriddenNameOpt.Value?.ToString(); + } + + fieldName = fieldName.TrimStart('_'); + if (fieldName.Length == 0) + { + return string.Empty; + } + + if (fieldName.Length == 1) + { + return fieldName.ToUpper(); + } + + return fieldName.Substring(0, 1).ToUpper() + fieldName.Substring(1); + } + } + + private IEnumerable GetAutoNotifyFields(FieldDeclarationSyntax[] fieldDeclarations) + { + foreach (var fieldDeclaration in fieldDeclarations) + { + var semanticModel = GetSemanticModel(fieldDeclaration.SyntaxTree); + + foreach (VariableDeclaratorSyntax variable in fieldDeclaration.Declaration.Variables) + { + if (semanticModel.GetDeclaredSymbol(variable) is not IFieldSymbol fieldSymbol) + { + continue; + } + + var attributes = fieldSymbol.GetAttributes(); + if (attributes.Any(ad => ad?.AttributeClass?.ToDisplayString() == AutoNotifyAttributeDisplayString)) + { + yield return fieldSymbol; + } + } + } + } +} diff --git a/WalletWasabi.Fluent.Generators/Generators/FluentNavigationGenerator.cs b/WalletWasabi.Fluent.Generators/Generators/FluentNavigationGenerator.cs new file mode 100644 index 0000000000..82236b8d43 --- /dev/null +++ b/WalletWasabi.Fluent.Generators/Generators/FluentNavigationGenerator.cs @@ -0,0 +1,183 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Collections.Generic; +using System.Linq; +using WalletWasabi.Fluent.Generators.Abstractions; + +namespace WalletWasabi.Fluent.Generators.Generators; + +internal class FluentNavigationGenerator: GeneratorStep +{ + public List Constructors { get; } = new(); + + public override void OnInitialize(Compilation compilation, GeneratorStep[] steps) + { + var uiContextStep = steps.OfType().First(); + + Constructors.AddRange(uiContextStep.Constructors); + } + + public override void Execute() + { + var namespaces = new List(); + var methods = new List(); + + foreach (var constructor in Constructors) + { + var semanticModel = GetSemanticModel(constructor.SyntaxTree); + + if (constructor.Parent is not ClassDeclarationSyntax cls) + { + continue; + } + + if (!cls.IsRoutableViewModel(semanticModel)) + { + continue; + } + + if (cls.IsAbstractClass(semanticModel)) + { + continue; + } + + var viewModelTypeInfo = semanticModel.GetDeclaredSymbol(cls); + + if (viewModelTypeInfo == null) + { + continue; + } + + var className = cls.Identifier.ValueText; + + var constructorNamespaces = constructor.ParameterList.Parameters + .Where(p => p.Type is not null) + .Select(p => semanticModel.GetTypeInfo(p.Type!)) + .Where(t => t.Type is not null) + .SelectMany(t => t.Type.GetNamespaces()) + .ToArray(); + + var uiContextParam = constructor.ParameterList.Parameters.FirstOrDefault(x => x.Type.IsUiContextType(semanticModel)); + + var methodParams = constructor.ParameterList; + + if (uiContextParam != null) + { + methodParams = SyntaxFactory.ParameterList(methodParams.Parameters.Remove(uiContextParam)); + } + + var navigationMetadata = viewModelTypeInfo + .GetAttributes() + .FirstOrDefault(x => x.AttributeClass?.ToDisplayString() == NavigationMetaDataGenerator.NavigationMetaDataAttributeDisplayString); + + var defaultNavigationTarget = "DialogScreen"; + + if (navigationMetadata != null) + { + var navigationArgument = navigationMetadata.NamedArguments + .FirstOrDefault(x => x.Key == "NavigationTarget"); + + if (navigationArgument.Value.Type is INamedTypeSymbol navigationTargetEnum) + { + var enumValue = navigationTargetEnum + .GetMembers() + .OfType() + .FirstOrDefault(x => x.ConstantValue?.Equals(navigationArgument.Value.Value) == true); + + if (enumValue != null) + { + defaultNavigationTarget = enumValue.Name; + } + } + } + + var additionalMethodParams = + $"NavigationTarget navigationTarget = NavigationTarget.{defaultNavigationTarget}, NavigationMode navigationMode = NavigationMode.Normal"; + + methodParams = methodParams.AddParameters(SyntaxFactory.ParseParameterList(additionalMethodParams).Parameters.ToArray()); + + var constructorArgs = + SyntaxFactory.ArgumentList( + SyntaxFactory.SeparatedList( + constructor.ParameterList + .Parameters + .Select(x => x.Type.IsUiContextType(semanticModel) ? "UiContext" : x.Identifier.ValueText) // replace uiContext argument for UiContext property reference + .Select(x => SyntaxFactory.ParseExpression(x)) + .Select(SyntaxFactory.Argument), + constructor.ParameterList + .Parameters + .Skip(1) + .Select(x => SyntaxFactory.Token(SyntaxKind.CommaToken)))); + + namespaces.Add(viewModelTypeInfo.ContainingNamespace.ToDisplayString()); + namespaces.AddRange(constructorNamespaces); + + var methodName = className.Replace("ViewModel", ""); + + var (dialogReturnType, dialogReturnTypeNamespace) = cls.GetDialogResultType(semanticModel); + + foreach (var ns in dialogReturnTypeNamespace) + { + namespaces.Add(ns); + } + + if (dialogReturnType is { }) + { + var dialogString = + $$""" + public FluentDialog<{{dialogReturnType}}> {{methodName}}{{methodParams}} + { + var dialog = new {{className}}{{constructorArgs.ToFullString()}}; + var target = UiContext.Navigate(navigationTarget); + target.To(dialog, navigationMode); + + return new FluentDialog<{{dialogReturnType}}>(target.NavigateDialogAsync(dialog, navigationMode)); + } + + """; + methods.Add(dialogString); + } + else + { + var methodString = + $$""" + public void {{methodName}}{{methodParams}} + { + UiContext.Navigate(navigationTarget).To(new {{className}}{{constructorArgs.ToFullString()}}, navigationMode); + } + + """; + methods.Add(methodString); + } + } + + var usings = namespaces + .Distinct() + .OrderBy(x => x) + .Select(n => $"using {n};") + .ToArray(); + + var usingsString = string.Join("\r\n", usings); + + var methodsString = string.Join("\r\n", methods); + + var sourceText = + $$""" + // + #nullable enable + + {{usingsString}} + + namespace WalletWasabi.Fluent.ViewModels.Navigation; + + public partial class FluentNavigate + { + {{methodsString}} + } + + """; + + AddSource("FluentNavigate.g.cs", sourceText); + } +} diff --git a/WalletWasabi.Fluent.Generators/Generators/MainGenerator.cs b/WalletWasabi.Fluent.Generators/Generators/MainGenerator.cs new file mode 100644 index 0000000000..d3e5ed51c0 --- /dev/null +++ b/WalletWasabi.Fluent.Generators/Generators/MainGenerator.cs @@ -0,0 +1,18 @@ +using Microsoft.CodeAnalysis; +using WalletWasabi.Fluent.Generators.Abstractions; + +namespace WalletWasabi.Fluent.Generators.Generators; + +[Generator] +internal class MainGenerator : CombinedGenerator +{ + public MainGenerator() + { + AddStaticFileGenerator(); + AddStaticFileGenerator(); + Add(); + Add(); + Add(); + Add(); + } +} diff --git a/WalletWasabi.Fluent.Generators/Generators/UiContextConstructorGenerator.cs b/WalletWasabi.Fluent.Generators/Generators/UiContextConstructorGenerator.cs new file mode 100644 index 0000000000..311483d722 --- /dev/null +++ b/WalletWasabi.Fluent.Generators/Generators/UiContextConstructorGenerator.cs @@ -0,0 +1,144 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Collections.Generic; +using System.Linq; +using WalletWasabi.Fluent.Generators.Abstractions; +using WalletWasabi.Fluent.Generators.Analyzers; + +namespace WalletWasabi.Fluent.Generators.Generators; + +internal class UiContextConstructorGenerator : GeneratorStep +{ + public List Constructors { get; } = new(); + + public override bool Filter(ClassDeclarationSyntax cls) + { + var exclusions = new[] + { + "RoutableViewModel" + }; + + return + cls.Identifier.Text.EndsWith("ViewModel") && + !exclusions.Contains(cls.Identifier.Text) && + !cls.IsSourceGenerated(); + } + + public override void Execute(ClassDeclarationSyntax[] classDeclarations) + { + Constructors.Clear(); + + var toGenerate = + from cls in classDeclarations + group cls by cls.Identifier.ValueText into g + select g.First(); + + foreach (var cls in toGenerate) + { + var model = GetSemanticModel(cls.SyntaxTree); + + if (model.GetDeclaredSymbol(cls) is not INamedTypeSymbol classSymbol) + { + continue; + } + + var constructors = GenerateConstructors(cls, model, classSymbol).ToArray(); + + Constructors.AddRange(constructors); + } + } + + private IEnumerable GenerateConstructors(ClassDeclarationSyntax classDeclaration, SemanticModel semanticModel, INamedTypeSymbol classSymbol) + { + var fileName = classDeclaration.Identifier.ValueText + UiContextAnalyzer.UiContextFileSuffix; + + var className = classSymbol.Name; + var namespaceName = classSymbol.ContainingNamespace.ToDisplayString(); + + var constructors = classDeclaration + .ChildNodes() + .OfType() + .ToArray(); + + foreach (var constructor in constructors) + { + if (!classDeclaration.GetUiContextReferences(semanticModel).Any()) + { + if (!classDeclaration.IsAbstractClass(semanticModel) && constructor.IsPublic()) + { + yield return constructor; + } + } + // if constructor already has a UIContext parameter, leave it be. Don't generate a new constructor and use the current one for FluentNavigation. + else if (constructor.ParameterList.Parameters.Any(p => p.Type.IsUiContextType(semanticModel))) + { + // it must be public though + if (constructor.IsPublic()) + { + yield return constructor; + } + } + else + { + var constructorArgs = constructor.ParameterList.Parameters + .Select(x => x.Identifier.ValueText) + .ToArray(); + + var hasConstructorArgs = constructorArgs.Any(); + var constructorArgsString = string.Join(",", constructorArgs); + var constructorString = hasConstructorArgs + ? $": this({constructorArgsString})" + : $": this()"; + + var parameterUsings = constructor.ParameterList.Parameters + .Where(p => p.Type is not null) + .Select(p => semanticModel.GetTypeInfo(p.Type!)) + .Where(t => t.Type is not null) + .Select(t => $"using {t.Type!.ContainingNamespace.ToDisplayString()};") + .ToArray(); + + var uiContextParameter = SyntaxFactory + .Parameter(SyntaxFactory.Identifier("uiContext").WithLeadingTrivia(SyntaxFactory.Space)) + .WithType(SyntaxFactory.ParseTypeName("UiContext")); + + var parametersString = constructor.ParameterList.Parameters.Insert(0, uiContextParameter).ToFullString(); + + var usings = string.Join("\r\n", parameterUsings.Distinct().OrderBy(x => x)); + + var code = + $$""" + {{usings}} + using WalletWasabi.Fluent.Models.UI; + + namespace {{namespaceName}}; + + partial class {{className}} + { + public {{className}}({{parametersString}}){{constructorString}} + { + UiContext = uiContext; + } + } + """; + + var syntaxTree = AddSource(fileName, code); + + var newConstructor = syntaxTree + .GetRoot() + .DescendantNodes() + .OfType() + .First(); + + yield return newConstructor; + } + } + } + + public override void OnInitialize(Compilation compilation, GeneratorStep[] steps) + { + var uiContextGenerator = steps.OfType().First(); + Constructors.Clear(); + Constructors.AddRange(uiContextGenerator.Constructors); + } +} diff --git a/WalletWasabi.Fluent.Generators/NavigationMetaDataGenerator.cs b/WalletWasabi.Fluent.Generators/NavigationMetaDataGenerator.cs index 6f7686f523..06332aa9bb 100644 --- a/WalletWasabi.Fluent.Generators/NavigationMetaDataGenerator.cs +++ b/WalletWasabi.Fluent.Generators/NavigationMetaDataGenerator.cs @@ -11,90 +11,14 @@ namespace WalletWasabi.Fluent.Generators; [Generator] public class NavigationMetaDataGenerator : ISourceGenerator { - private const string NavigationMetaDataAttributeDisplayString = "WalletWasabi.Fluent.NavigationMetaDataAttribute"; + public const string NavigationMetaDataAttributeDisplayString = "WalletWasabi.Fluent.NavigationMetaDataAttribute"; private const string NavigationMetaDataDisplayString = "WalletWasabi.Fluent.NavigationMetaData"; private const string RoutableViewModelDisplayString = "WalletWasabi.Fluent.ViewModels.Navigation.RoutableViewModel"; - private const string AttributeText = @"// -using System; - -namespace WalletWasabi.Fluent -{ - public enum NavBarPosition - { - None, - Top, - Bottom - } - - public enum NavigationTarget - { - Default = 0, - HomeScreen = 1, - DialogScreen = 2, - FullScreen = 3, - CompactDialogScreen = 4, - } - - public sealed class NavigationMetaData - { - public bool Searchable { get; init; } = true; - - public string Title { get; init; } - - public string Caption { get; init; } - - public string IconName { get; init; } - - public string IconNameFocused { get; init; } - - public int Order { get; init; } - - public string Category { get; init; } - - public string[] Keywords { get; init; } - - public NavBarPosition NavBarPosition { get; init; } - - public NavigationTarget NavigationTarget { get; init; } - } - - [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] - public sealed class NavigationMetaDataAttribute : Attribute - { - public NavigationMetaDataAttribute() - { - } - - public bool Searchable { get; set; } - - public string Title { get; set; } - - public string Caption { get; set; } - - public string IconName { get; set; } - - public string IconNameFocused { get; set; } - - public int Order { get; set; } - - public string Category { get; set; } - - public string[] Keywords { get; set; } - - public NavBarPosition NavBarPosition {get; set; } - - public NavigationTarget NavigationTarget { get; set; } - } -}"; - public void Initialize(GeneratorInitializationContext context) { - // System.Diagnostics.Debugger.Launch(); - context.RegisterForPostInitialization((i) => i.AddSource("NavigationMetaDataAttribute.cs", SourceText.From(AttributeText, Encoding.UTF8))); - context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); } @@ -129,8 +53,7 @@ public void Execute(GeneratorExecutionContext context) private static string? ProcessClass(Compilation compilation, INamedTypeSymbol classSymbol, ISymbol attributeSymbol, ISymbol metadataSymbol) { - if (!classSymbol.ContainingSymbol.Equals(classSymbol.ContainingNamespace, - SymbolEqualityComparer.Default)) + if (!classSymbol.ContainingSymbol.Equals(classSymbol.ContainingNamespace, SymbolEqualityComparer.Default)) { return null; } @@ -139,51 +62,94 @@ public void Execute(GeneratorExecutionContext context) var format = new SymbolDisplayFormat( typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypes, - genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters | SymbolDisplayGenericsOptions.IncludeTypeConstraints | SymbolDisplayGenericsOptions.IncludeVariance - ); - - var source = new StringBuilder($@"// -#nullable enable -using System; -using System.Threading.Tasks; -using WalletWasabi.Fluent.ViewModels.Navigation; - -namespace {namespaceName} -{{ - public partial class {classSymbol.ToDisplayString(format)} - {{ -"); + genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters | SymbolDisplayGenericsOptions.IncludeTypeConstraints | SymbolDisplayGenericsOptions.IncludeVariance); var attributeData = classSymbol .GetAttributes() .Single(ad => ad?.AttributeClass?.Equals(attributeSymbol, SymbolEqualityComparer.Default) ?? false); - source.Append($@" public static {metadataSymbol.ToDisplayString()} MetaData {{ get; }} = new() - {{ -"); + var isNavBarItem = + attributeData.NamedArguments.Any(x => x.Key == "NavBarPosition") && + attributeData.NamedArguments.Any(x => x.Key == "NavBarSelectionMode"); + + var implementedInterfaces = new List(); + + if (isNavBarItem) + { + var navBarSelectionMode = attributeData.NamedArguments.First(x => x.Key == "NavBarSelectionMode").Value.Value; + if (navBarSelectionMode is int s) + { + if (s == 1) + { + implementedInterfaces.Add("INavBarButton"); + } + else if (s == 2) + { + implementedInterfaces.Add("INavBarToggle"); + } + } + } + + var implementedInterfacesString = + implementedInterfaces.Any() + ? ": " + string.Join(", ", implementedInterfaces) + : ""; + + var source = new StringBuilder( + $$""" + // + #nullable enable + using System; + using System.Threading.Tasks; + using WalletWasabi.Fluent.ViewModels.Navigation; + + namespace {{namespaceName}}; + + public partial class {{classSymbol.ToDisplayString(format)}}{{implementedInterfacesString}} + { + + """); + + source.Append( + $$""" + public static {{metadataSymbol.ToDisplayString()}} MetaData { get; } = new() + { + + """); var length = attributeData.NamedArguments.Length; for (int i = 0; i < length; i++) { var namedArgument = attributeData.NamedArguments[i]; - source.AppendLine($" {namedArgument.Key} = " + + source.AppendLine($"\t\t{namedArgument.Key} = " + $"{(namedArgument.Value.Kind == TypedConstantKind.Array ? "new[] " : "")}" + $"{namedArgument.Value.ToCSharpString()}{(i < length - 1 ? "," : "")}"); } - source.Append($@" }}; -"); + source.Append( + """ + }; - source.AppendLine($@" public static void RegisterAsyncLazy(Func> createInstance) => NavigationManager.RegisterAsyncLazy(MetaData, createInstance);"); - source.AppendLine($@" public static void RegisterLazy(Func createInstance) => NavigationManager.RegisterLazy(MetaData, createInstance);"); - source.AppendLine($@" public static void Register(RoutableViewModel createInstance) => NavigationManager.Register(MetaData, createInstance);"); - source.AppendLine($@" public override string Title {{get => MetaData.Title; protected set {{}} }} "); + """); - var routeableClass = compilation.GetTypeByMetadataName(RoutableViewModelDisplayString); + source.AppendLine( + """ + public static void RegisterAsyncLazy(Func> createInstance) => NavigationManager.RegisterAsyncLazy(MetaData, createInstance); + """); + source.AppendLine( + """ + public static void RegisterLazy(Func createInstance) => NavigationManager.RegisterLazy(MetaData, createInstance); + """); + source.AppendLine( + """ + public static void Register(RoutableViewModel createInstance) => NavigationManager.Register(MetaData, createInstance); + """); - if (routeableClass is { }) + var routableClass = compilation.GetTypeByMetadataName(RoutableViewModelDisplayString); + + if (routableClass is { }) { - bool addRouteableMetaData = false; + bool addRoutableMetaData = false; var baseType = classSymbol.BaseType; while (true) { @@ -192,37 +158,55 @@ public partial class {classSymbol.ToDisplayString(format)} break; } - if (SymbolEqualityComparer.Default.Equals(baseType, routeableClass)) + if (SymbolEqualityComparer.Default.Equals(baseType, routableClass)) { - addRouteableMetaData = true; + addRoutableMetaData = true; break; } baseType = baseType.BaseType; } - if (addRouteableMetaData) + if (addRoutableMetaData) { if (attributeData.NamedArguments.Any(x => x.Key == "NavigationTarget")) { source.AppendLine( - $@" public override NavigationTarget DefaultTarget => MetaData.NavigationTarget;"); + """ + public override NavigationTarget DefaultTarget => MetaData.NavigationTarget; + """); } - if (attributeData.NamedArguments.Any(x => x.Key == "IconName")) + if (attributeData.NamedArguments.Any(x => x.Key == "Title")) { - source.AppendLine($@" public override string IconName => MetaData.IconName;"); + source.AppendLine( + """ + public override string Title { get => MetaData.Title!; protected set {} } + """); } + } - if (attributeData.NamedArguments.Any(x => x.Key == "IconNameFocused")) - { - source.AppendLine($@" public override string IconNameFocused => MetaData.IconNameFocused;"); - } + if (attributeData.NamedArguments.Any(x => x.Key == "IconName")) + { + source.AppendLine( + """ + public string IconName => MetaData.IconName!; + """); + } + + if (attributeData.NamedArguments.Any(x => x.Key == "IconNameFocused")) + { + source.AppendLine( + """ + public string IconNameFocused => MetaData.IconNameFocused!; + """); } } - source.Append($@" }} -}}"); + source.Append( + """ + } + """); return source.ToString(); } diff --git a/WalletWasabi.Fluent.Generators/Properties/launchSettings.json b/WalletWasabi.Fluent.Generators/Properties/launchSettings.json new file mode 100644 index 0000000000..b17e5e708f --- /dev/null +++ b/WalletWasabi.Fluent.Generators/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "Profile 1": { + "commandName": "DebugRoslynComponent", + "targetProject": "..\\WalletWasabi.Fluent\\WalletWasabi.Fluent.csproj" + } + } +} \ No newline at end of file diff --git a/WalletWasabi.Fluent.Generators/StaticViewLocatorGenerator.cs b/WalletWasabi.Fluent.Generators/StaticViewLocatorGenerator.cs index fc9fc23fe5..8f38ac69a0 100644 --- a/WalletWasabi.Fluent.Generators/StaticViewLocatorGenerator.cs +++ b/WalletWasabi.Fluent.Generators/StaticViewLocatorGenerator.cs @@ -17,16 +17,18 @@ public class StaticViewLocatorGenerator : ISourceGenerator private const string ViewSuffix = "View"; - private const string AttributeText = @"// -using System; + private const string AttributeText = + """ + // + using System; -namespace WalletWasabi.Fluent -{ - [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] - public sealed class StaticViewLocatorAttribute : Attribute - { - } -}"; + namespace WalletWasabi.Fluent; + + [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + public sealed class StaticViewLocatorAttribute : Attribute + { + } + """; public void Initialize(GeneratorInitializationContext context) { @@ -77,20 +79,27 @@ public void Execute(GeneratorExecutionContext context) string classNameLocator = namedTypeSymbolLocator.ToDisplayString(format); - var source = new StringBuilder($@"// -#nullable enable -using System; -using System.Collections.Generic; -using Avalonia.Controls; + var source = new StringBuilder( + $$""" + // + #nullable enable + using System; + using System.Collections.Generic; + using Avalonia.Controls; -namespace {namespaceNameLocator} -{{ - public partial class {classNameLocator} - {{"); - source.Append($@" - private static Dictionary> s_views = new() - {{ -"); + namespace {{namespaceNameLocator}}; + + public partial class {{classNameLocator}} + { + """); + + source.Append( + """ + + private static Dictionary> s_views = new() + { + + """); var userControlViewSymbol = compilation.GetTypeByMetadataName("Avalonia.Controls.UserControl"); @@ -103,17 +112,25 @@ public partial class {classNameLocator} var classNameViewSymbol = compilation.GetTypeByMetadataName(classNameView); if (classNameViewSymbol is null || classNameViewSymbol.BaseType?.Equals(userControlViewSymbol, SymbolEqualityComparer.Default) != true) { - source.AppendLine($@" [typeof({classNameViewModel})] = () => new TextBlock() {{ Text = {("\"Not Found: " + classNameView + "\"")} }},"); + source.AppendLine( + $$""" + [typeof({{classNameViewModel}})] = () => new TextBlock() { Text = {{("\"Not Found: " + classNameView + "\"")}} }, + """); } else { - source.AppendLine($@" [typeof({classNameViewModel})] = () => new {classNameView}(),"); + source.AppendLine( + $$""" + [typeof({{classNameViewModel}})] = () => new {{classNameView}}(), + """); } } - source.Append($@" }}; - }} -}}"); + source.Append( + """ + }; + } + """); return source.ToString(); } diff --git a/WalletWasabi.Fluent.Generators/WalletWasabi.Fluent.Generators.csproj b/WalletWasabi.Fluent.Generators/WalletWasabi.Fluent.Generators.csproj index 12cee95659..83812bac30 100644 --- a/WalletWasabi.Fluent.Generators/WalletWasabi.Fluent.Generators.csproj +++ b/WalletWasabi.Fluent.Generators/WalletWasabi.Fluent.Generators.csproj @@ -13,11 +13,12 @@ win7-x64;linux-x64;linux-arm64;osx-x64;osx-arm64 $(MSBuildProjectDirectory)\=WalletWasabi.Fluent.Generators true + true - - + + diff --git a/WalletWasabi.Fluent.Generators/packages.lock.json b/WalletWasabi.Fluent.Generators/packages.lock.json index 0a24e47368..477ef3a939 100644 --- a/WalletWasabi.Fluent.Generators/packages.lock.json +++ b/WalletWasabi.Fluent.Generators/packages.lock.json @@ -1,20 +1,20 @@ { - "version": 1, + "version": 2, "dependencies": { ".NETStandard,Version=v2.0": { "Microsoft.CodeAnalysis.Analyzers": { "type": "Direct", - "requested": "[3.3.3, )", - "resolved": "3.3.3", - "contentHash": "j/rOZtLMVJjrfLRlAMckJLPW/1rze9MT1yfWqSIbUPGRu1m1P0fuo9PmqapwsmePfGB5PJrudQLvmUOAMF0DqQ==" + "requested": "[3.3.4, )", + "resolved": "3.3.4", + "contentHash": "AxkxcPR+rheX0SmvpLVIGLhOUXAKG56a64kV9VQZ4y9gR9ZmPXnqZvHJnmwLSwzrEP6junUF11vuc+aqo5r68g==" }, "Microsoft.CodeAnalysis.CSharp": { "type": "Direct", - "requested": "[4.2.0, )", - "resolved": "4.2.0", - "contentHash": "5IDwr8zGNBmDpxtzxxZj9IHwoA6HJ1/WWT/JacqPQJ4Vz/oZXaHNlzcBPVCZRGWUw+QvVdAhCKwEyJyuAuH/wg==", + "requested": "[4.6.0, )", + "resolved": "4.6.0", + "contentHash": "9pyFZUN2Lyu3C0Xfs49kezfH+CzQHMibGsQeQPu0P+GWyH2XXDwmyZ6jAaKQGNUXOJfC2OK01hWMJTJY315uDQ==", "dependencies": { - "Microsoft.CodeAnalysis.Common": "[4.2.0]" + "Microsoft.CodeAnalysis.Common": "[4.6.0]" } }, "NETStandard.Library": { @@ -26,20 +26,6 @@ "Microsoft.NETCore.Platforms": "1.1.0" } }, - "Microsoft.CodeAnalysis.Common": { - "type": "Transitive", - "resolved": "4.2.0", - "contentHash": "lbusGcuE7D8FtZawQ4G++UFsRQArPzZN1GGXjPQwu3gvCbw7FXDcBq1zDZrZN1vRzPTVe1qyZMvfGhVUzs1TDg==", - "dependencies": { - "Microsoft.CodeAnalysis.Analyzers": "3.3.3", - "System.Collections.Immutable": "5.0.0", - "System.Memory": "4.5.4", - "System.Reflection.Metadata": "5.0.0", - "System.Runtime.CompilerServices.Unsafe": "6.0.0", - "System.Text.Encoding.CodePages": "6.0.0", - "System.Threading.Tasks.Extensions": "4.5.4" - } - }, "Microsoft.NETCore.Platforms": { "type": "Transitive", "resolved": "1.1.0", @@ -52,16 +38,17 @@ }, "System.Collections.Immutable": { "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "FXkLXiK0sVVewcso0imKQoOxjoPAj42R8HtjjbSjVPAzwDfzoyoznWxgA3c38LDbN9SJux1xXoXYAhz98j7r2g==", + "resolved": "7.0.0", + "contentHash": "dQPcs0U1IKnBdRDBkrCTi1FoajSTBzLcVTpjO4MBCMC7f4pDOIPzgBoX8JjG7X6uZRJ8EBxsi8+DR1JuwjnzOQ==", "dependencies": { - "System.Memory": "4.5.4" + "System.Memory": "4.5.5", + "System.Runtime.CompilerServices.Unsafe": "6.0.0" } }, "System.Memory": { "type": "Transitive", - "resolved": "4.5.4", - "contentHash": "1MbJTHS1lZ4bS4FmsJjnuGJOu88ZzTT2rLvrhW7Ygic+pC0NWA+3hgAen0HRdsocuQXCkUTdFn9yHJJhsijDXw==", + "resolved": "4.5.5", + "contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==", "dependencies": { "System.Buffers": "4.5.1", "System.Numerics.Vectors": "4.4.0", @@ -75,10 +62,11 @@ }, "System.Reflection.Metadata": { "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "5NecZgXktdGg34rh1OenY1rFNDCI8xSjFr+Z4OU4cU06AQHUdRnIIEeWENu3Wl4YowbzkymAIMvi3WyK9U53pQ==", + "resolved": "7.0.0", + "contentHash": "MclTG61lsD9sYdpNz9xsKBzjsmsfCtcMZYXz/IUr2zlhaTaABonlr1ESeompTgM+Xk+IwtGYU7/voh3YWB/fWw==", "dependencies": { - "System.Collections.Immutable": "5.0.0" + "System.Collections.Immutable": "7.0.0", + "System.Memory": "4.5.5" } }, "System.Runtime.CompilerServices.Unsafe": { @@ -88,10 +76,10 @@ }, "System.Text.Encoding.CodePages": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "ZFCILZuOvtKPauZ/j/swhvw68ZRi9ATCfvGbk1QfydmcXBkIWecWKn/250UH7rahZ5OoDBaiAudJtPvLwzw85A==", + "resolved": "7.0.0", + "contentHash": "LSyCblMpvOe0N3E+8e0skHcrIhgV2huaNcjUUEa8hRtgEAm36aGkRoC8Jxlb6Ra6GSfF29ftduPNywin8XolzQ==", "dependencies": { - "System.Memory": "4.5.4", + "System.Memory": "4.5.5", "System.Runtime.CompilerServices.Unsafe": "6.0.0" } }, @@ -102,62 +90,27 @@ "dependencies": { "System.Runtime.CompilerServices.Unsafe": "4.5.3" } - } - }, - ".NETStandard,Version=v2.0/linux-arm64": { - "System.Text.Encoding.CodePages": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "ZFCILZuOvtKPauZ/j/swhvw68ZRi9ATCfvGbk1QfydmcXBkIWecWKn/250UH7rahZ5OoDBaiAudJtPvLwzw85A==", - "dependencies": { - "System.Memory": "4.5.4", - "System.Runtime.CompilerServices.Unsafe": "6.0.0" - } - } - }, - ".NETStandard,Version=v2.0/linux-x64": { - "System.Text.Encoding.CodePages": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "ZFCILZuOvtKPauZ/j/swhvw68ZRi9ATCfvGbk1QfydmcXBkIWecWKn/250UH7rahZ5OoDBaiAudJtPvLwzw85A==", - "dependencies": { - "System.Memory": "4.5.4", - "System.Runtime.CompilerServices.Unsafe": "6.0.0" - } - } - }, - ".NETStandard,Version=v2.0/osx-arm64": { - "System.Text.Encoding.CodePages": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "ZFCILZuOvtKPauZ/j/swhvw68ZRi9ATCfvGbk1QfydmcXBkIWecWKn/250UH7rahZ5OoDBaiAudJtPvLwzw85A==", - "dependencies": { - "System.Memory": "4.5.4", - "System.Runtime.CompilerServices.Unsafe": "6.0.0" - } - } - }, - ".NETStandard,Version=v2.0/osx-x64": { - "System.Text.Encoding.CodePages": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "ZFCILZuOvtKPauZ/j/swhvw68ZRi9ATCfvGbk1QfydmcXBkIWecWKn/250UH7rahZ5OoDBaiAudJtPvLwzw85A==", + }, + "Microsoft.CodeAnalysis.Common": { + "type": "CentralTransitive", + "requested": "[4.6.0, )", + "resolved": "4.6.0", + "contentHash": "N3uLvekc7DjvE1BX8YW7UH7ldjA4ps/Tun2YmOoSIItJrh1gnQIMKUbK1c3uQUx2NHbLibVZI4o/VB9xb4B7tA==", "dependencies": { - "System.Memory": "4.5.4", - "System.Runtime.CompilerServices.Unsafe": "6.0.0" + "Microsoft.CodeAnalysis.Analyzers": "3.3.4", + "System.Collections.Immutable": "7.0.0", + "System.Memory": "4.5.5", + "System.Reflection.Metadata": "7.0.0", + "System.Runtime.CompilerServices.Unsafe": "6.0.0", + "System.Text.Encoding.CodePages": "7.0.0", + "System.Threading.Tasks.Extensions": "4.5.4" } } }, - ".NETStandard,Version=v2.0/win7-x64": { - "System.Text.Encoding.CodePages": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "ZFCILZuOvtKPauZ/j/swhvw68ZRi9ATCfvGbk1QfydmcXBkIWecWKn/250UH7rahZ5OoDBaiAudJtPvLwzw85A==", - "dependencies": { - "System.Memory": "4.5.4", - "System.Runtime.CompilerServices.Unsafe": "6.0.0" - } - } - } + ".NETStandard,Version=v2.0/linux-arm64": {}, + ".NETStandard,Version=v2.0/linux-x64": {}, + ".NETStandard,Version=v2.0/osx-arm64": {}, + ".NETStandard,Version=v2.0/osx-x64": {}, + ".NETStandard,Version=v2.0/win7-x64": {} } } \ No newline at end of file diff --git a/WalletWasabi.Fluent/App.axaml b/WalletWasabi.Fluent/App.axaml index ca8c0e7e67..05a10b5308 100644 --- a/WalletWasabi.Fluent/App.axaml +++ b/WalletWasabi.Fluent/App.axaml @@ -6,7 +6,8 @@ xmlns:converters="clr-namespace:WalletWasabi.Fluent.Converters" x:DataType="vm:ApplicationViewModel" x:CompileBindings="True" - x:Class="WalletWasabi.Fluent.App"> + x:Class="WalletWasabi.Fluent.App" + RequestedThemeVariant="Dark"> @@ -17,32 +18,24 @@ 0 + + + + + + + + - + + + - - - - - - - - - - - - - - - - - - - + @@ -52,13 +45,14 @@ - + + diff --git a/WalletWasabi.Fluent/App.axaml.cs b/WalletWasabi.Fluent/App.axaml.cs index 07980b2758..57d559bdc0 100644 --- a/WalletWasabi.Fluent/App.axaml.cs +++ b/WalletWasabi.Fluent/App.axaml.cs @@ -1,11 +1,20 @@ +using System.Linq; using System.Reactive.Concurrency; +using System.Runtime.InteropServices; using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; +using NBitcoin; using ReactiveUI; +using WalletWasabi.Fluent.Models; +using WalletWasabi.Fluent.Models.ClientConfig; +using WalletWasabi.Fluent.Models.FileSystem; +using WalletWasabi.Fluent.Models.UI; +using WalletWasabi.Fluent.Models.Wallets; using WalletWasabi.Fluent.ViewModels; +using WalletWasabi.Fluent.ViewModels.SearchBar.Sources; namespace WalletWasabi.Fluent; @@ -15,6 +24,12 @@ public class App : Application private readonly Func? _backendInitialiseAsync; private ApplicationStateManager? _applicationStateManager; + static App() + { + // TODO: This is temporary workaround until https://github.com/zkSNACKs/WalletWasabi/issues/8151 is fixed. + EnableFeatureHide = !RuntimeInformation.IsOSPlatform(OSPlatform.OSX); + } + public App() { Name = "Wasabi Wallet"; @@ -26,6 +41,8 @@ public App(Func backendInitialiseAsync, bool startInBg) : this() _backendInitialiseAsync = backendInitialiseAsync; } + public static bool EnableFeatureHide { get; private set; } + public override void Initialize() { AvaloniaXamlLoader.Load(this); @@ -37,12 +54,19 @@ public override void OnFrameworkInitializationCompleted() { if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { + var uiContext = CreateUiContext(); + UiContext.Default = uiContext; _applicationStateManager = - new ApplicationStateManager(desktop, _startInBg); + new ApplicationStateManager(desktop, uiContext, _startInBg); DataContext = _applicationStateManager.ApplicationViewModel; desktop.ShutdownMode = ShutdownMode.OnExplicitShutdown; + desktop.Exit += (sender, args) => + { + MainViewModel.Instance.ClearStacks(); + uiContext.HealthMonitor.Dispose(); + }; RxApp.MainThreadScheduler.Schedule( async () => @@ -51,9 +75,90 @@ public override void OnFrameworkInitializationCompleted() MainViewModel.Instance.Initialize(); }); + + InitializeTrayIcons(); } } base.OnFrameworkInitializationCompleted(); +#if DEBUG + this.AttachDevTools(); +#endif + } + + private void InitializeTrayIcons() + { + // TODO: This is temporary workaround until https://github.com/zkSNACKs/WalletWasabi/issues/8151 is fixed. + var trayIcon = TrayIcon.GetIcons(this).FirstOrDefault(); + if (trayIcon is not null) + { + if (this.TryFindResource(EnableFeatureHide ? "DefaultNativeMenu" : "MacOsNativeMenu", out var nativeMenu)) + { + trayIcon.Menu = nativeMenu as NativeMenu; + } + } + } + + // It begins to show that we're re-inventing DI, aren't we? + private static IWalletRepository CreateWalletRepository(IAmountProvider amountProvider) + { + return new WalletRepository(amountProvider); + } + + private static IHardwareWalletInterface CreateHardwareWalletInterface() + { + return new HardwareWalletInterface(); + } + + private static IFileSystem CreateFileSystem() + { + return new FileSystemModel(); + } + + private static IClientConfig CreateConfig() + { + return new ClientConfigModel(); + } + + private static IApplicationSettings CreateApplicationSettings() + { + return new ApplicationSettings(Services.PersistentConfigFilePath, Services.PersistentConfig, Services.Config, Services.UiConfig); + } + + private static ITransactionBroadcasterModel CreateBroadcaster(Network network) + { + return new TransactionBroadcasterModel(network); + } + + private static IAmountProvider CreateAmountProvider() + { + return new AmountProvider(Services.Synchronizer); + } + + private UiContext CreateUiContext() + { + var amountProvider = CreateAmountProvider(); + + var applicationSettings = CreateApplicationSettings(); + var torStatusChecker = new TorStatusCheckerModel(); + + // This class (App) represents the actual Avalonia Application and it's sole presence means we're in the actual runtime context (as opposed to unit tests) + // Once all ViewModels have been refactored to receive UiContext as a constructor parameter, this static singleton property can be removed. + return new UiContext( + new QrCodeGenerator(), + new QrCodeReader(), + new UiClipboard(), + CreateWalletRepository(amountProvider), + new CoinjoinModel(), + CreateHardwareWalletInterface(), + CreateFileSystem(), + CreateConfig(), + applicationSettings, + CreateBroadcaster(applicationSettings.Network), + amountProvider, + new EditableSearchSourceSource(), + torStatusChecker, + new LegalDocumentsProvider(), + new HealthMonitor(applicationSettings, torStatusChecker)); } } diff --git a/WalletWasabi.Fluent/AppServices/Tor/TorStatusCheckerWrapper.cs b/WalletWasabi.Fluent/AppServices/Tor/TorStatusCheckerWrapper.cs deleted file mode 100644 index 28671d6629..0000000000 --- a/WalletWasabi.Fluent/AppServices/Tor/TorStatusCheckerWrapper.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Reactive.Linq; -using WalletWasabi.Tor.StatusChecker; - -namespace WalletWasabi.Fluent.AppServices.Tor; - -public class TorStatusCheckerWrapper -{ - public TorStatusCheckerWrapper(TorStatusChecker statusChecker) - { - Issues = Observable - .FromEventPattern(statusChecker, nameof(TorStatusChecker.StatusEvent)) - .Select(pattern => pattern.EventArgs.ToList()); - } - - public IObservable> Issues { get; } -} diff --git a/WalletWasabi.Fluent/ApplicationStateManager.cs b/WalletWasabi.Fluent/ApplicationStateManager.cs index 10be2e43aa..e3199836c9 100644 --- a/WalletWasabi.Fluent/ApplicationStateManager.cs +++ b/WalletWasabi.Fluent/ApplicationStateManager.cs @@ -4,8 +4,9 @@ using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using ReactiveUI; -using WalletWasabi.Fluent.Behaviors; +using WalletWasabi.Fluent.Extensions; using WalletWasabi.Fluent.Helpers; +using WalletWasabi.Fluent.Models.UI; using WalletWasabi.Fluent.Providers; using WalletWasabi.Fluent.State; using WalletWasabi.Fluent.ViewModels; @@ -24,11 +25,13 @@ public class ApplicationStateManager : IMainWindowService private bool _isShuttingDown; private bool _restartRequest; - internal ApplicationStateManager(IClassicDesktopStyleApplicationLifetime lifetime, bool startInBg) + internal ApplicationStateManager(IClassicDesktopStyleApplicationLifetime lifetime, UiContext uiContext, bool startInBg) { _lifetime = lifetime; _stateMachine = new StateMachine(State.InitialState); - ApplicationViewModel = new ApplicationViewModel(this); + + UiContext = uiContext; + ApplicationViewModel = new ApplicationViewModel(UiContext, this); State initTransitionState = startInBg ? State.Closed : State.Open; Observable @@ -53,6 +56,7 @@ internal ApplicationStateManager(IClassicDesktopStyleApplicationLifetime lifetim Trigger.ShutdownPrevented, () => { + _lifetime.MainWindow.BringToFront(); ApplicationViewModel.OnShutdownPrevented(_restartRequest); _restartRequest = false; // reset the value. }); @@ -98,6 +102,7 @@ private enum State Open, } + internal UiContext UiContext { get; } internal ApplicationViewModel ApplicationViewModel { get; } private void LifetimeOnShutdownRequested(object? sender, ShutdownRequestedEventArgs e) @@ -132,7 +137,7 @@ private void CreateAndShowMainWindow() { // _hideRequest flag is used to distinguish what is the user's intent. // It is only true when the request comes from the Tray. - if (Services.UiConfig.HideOnClose || _hideRequest) + if ((Services.UiConfig.HideOnClose || _hideRequest) && App.EnableFeatureHide) { _hideRequest = false; // request processed, set it back to the default. return; @@ -197,7 +202,7 @@ private void ObserveWindowSize(Window window, CompositeDisposable disposables) window .WhenAnyValue(x => x.Bounds) .Skip(1) - .Where(b => !b.IsEmpty && window.WindowState == WindowState.Normal) + .Where(b => b != default && window.WindowState == WindowState.Normal) .Subscribe(b => { Services.UiConfig.WindowWidth = b.Width; diff --git a/WalletWasabi.Fluent/Behaviors/BindableFlyoutOpenBehavior.cs b/WalletWasabi.Fluent/Behaviors/BindableFlyoutOpenBehavior.cs deleted file mode 100644 index 6f912f5cd5..0000000000 --- a/WalletWasabi.Fluent/Behaviors/BindableFlyoutOpenBehavior.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Reactive.Disposables; -using System.Reactive.Linq; -using Avalonia; -using Avalonia.Controls; -using Avalonia.Controls.Primitives; -using ReactiveUI; - -namespace WalletWasabi.Fluent.Behaviors; - -public class BindableFlyoutOpenBehavior : DisposingBehavior -{ - public static readonly StyledProperty IsOpenProperty = - AvaloniaProperty.Register(nameof(IsOpen)); - - public bool IsOpen - { - get => GetValue(IsOpenProperty); - set => SetValue(IsOpenProperty, value); - } - - protected override void OnAttached(CompositeDisposable disposable) - { - if (AssociatedObject is null) - { - return; - } - - Observable - .FromEventPattern(AssociatedObject, nameof(AssociatedObject.PointerEnter)) - .Subscribe(_ => IsOpen = true) - .DisposeWith(disposable); - - this.WhenAnyValue(x => x.IsOpen) - .Subscribe(isOpen => - { - if (isOpen) - { - FlyoutBase.ShowAttachedFlyout(AssociatedObject); - } - else - { - FlyoutBase.GetAttachedFlyout(AssociatedObject)?.Hide(); - } - }) - .DisposeWith(disposable); - } -} diff --git a/WalletWasabi.Fluent/Behaviors/ButtonExecuteCommandOnKeyDownBehavior.cs b/WalletWasabi.Fluent/Behaviors/ButtonExecuteCommandOnKeyDownBehavior.cs index c177e3f95e..00b6cd8f7a 100644 --- a/WalletWasabi.Fluent/Behaviors/ButtonExecuteCommandOnKeyDownBehavior.cs +++ b/WalletWasabi.Fluent/Behaviors/ButtonExecuteCommandOnKeyDownBehavior.cs @@ -35,10 +35,11 @@ protected override void OnAttachedToVisualTree(CompositeDisposable disposable) return; } - if (button.GetVisualRoot() is IInputElement inputRoot) + if (button.GetVisualRoot() is InputElement inputRoot) { - inputRoot.AddDisposableHandler(InputElement.KeyDownEvent, RootDefaultKeyDown) - .DisposeWith(disposable); + inputRoot + .AddDisposableHandler(InputElement.KeyDownEvent, RootDefaultKeyDown) + .DisposeWith(disposable); } } diff --git a/WalletWasabi.Fluent/Behaviors/ContextFlyoutWorkaroundBehavior.cs b/WalletWasabi.Fluent/Behaviors/ContextFlyoutWorkaroundBehavior.cs new file mode 100644 index 0000000000..0aef9d5f50 --- /dev/null +++ b/WalletWasabi.Fluent/Behaviors/ContextFlyoutWorkaroundBehavior.cs @@ -0,0 +1,82 @@ +using System.Collections.Generic; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.VisualTree; +using ReactiveUI; + +namespace WalletWasabi.Fluent.Behaviors; + +// TODO: Remove when context flyouts are fixed in Avalonia. +public class ContextFlyoutWorkaroundBehavior : DisposingBehavior +{ + private readonly List _openFlyouts = new(); + + protected override void OnAttached(CompositeDisposable disposables) + { + if (AssociatedObject is { }) + { + FlyoutBase.IsOpenProperty.Changed + .Subscribe(FlyoutOpenChanged) + .DisposeWith(disposables); + + AssociatedObject + .WhenAnyValue(x => x.IsActive, x => x.IsPointerOver, (isActive, isPointerOver) => !isActive && !isPointerOver) + .Where(x => x) + .Subscribe(_ => CloseFlyouts()) + .DisposeWith(disposables); + + AssociatedObject + .GetObservable(Visual.BoundsProperty) + .Subscribe(_ => CloseFlyouts()) + .DisposeWith(disposables); + + Observable + .FromEventPattern( + handler => + { + if (AssociatedObject is not null) + { + AssociatedObject.PositionChanged += handler; + } + }, + handler => + { + if (AssociatedObject is not null) + { + AssociatedObject.PositionChanged -= handler; + } + }) + .Subscribe(_ => CloseFlyouts()) + .DisposeWith(disposables); + } + } + + protected void CloseFlyouts() + { + for (var index = _openFlyouts.Count; index > 0;) + { + _openFlyouts[--index].Hide(); + } + } + + private void FlyoutOpenChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is FlyoutBase flyout && flyout.Target is { } target) + { + if (e.OldValue.Value) + { + _openFlyouts.Remove(flyout); + + return; + } + + if (target.FindAncestorOfType() == AssociatedObject) + { + _openFlyouts.Add(flyout); + } + } + } +} diff --git a/WalletWasabi.Fluent/Behaviors/DialogTransitionAttachedBehavior.cs b/WalletWasabi.Fluent/Behaviors/DialogTransitionAttachedBehavior.cs new file mode 100644 index 0000000000..d3b8b33b6c --- /dev/null +++ b/WalletWasabi.Fluent/Behaviors/DialogTransitionAttachedBehavior.cs @@ -0,0 +1,101 @@ +using System.Numerics; +using System.Reactive.Disposables; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Animation.Easings; +using Avalonia.Controls; +using Avalonia.Rendering.Composition; + +namespace WalletWasabi.Fluent.Behaviors; + +public class DialogTransitionAttachedBehavior : AttachedToVisualTreeBehavior +{ + public static readonly StyledProperty OpacityDurationProperty = + AvaloniaProperty.Register(nameof(OpacityDuration), TimeSpan.FromMilliseconds(250)); + + public static readonly StyledProperty ScaleDurationProperty = + AvaloniaProperty.Register(nameof(ScaleDuration), TimeSpan.FromMilliseconds(350)); + + public static readonly StyledProperty EnableScaleProperty = + AvaloniaProperty.Register(nameof(EnableScale), true); + + public TimeSpan OpacityDuration + { + get => GetValue(OpacityDurationProperty); + set => SetValue(OpacityDurationProperty, value); + } + + public TimeSpan ScaleDuration + { + get => GetValue(ScaleDurationProperty); + set => SetValue(ScaleDurationProperty, value); + } + + public bool EnableScale + { + get => GetValue(EnableScaleProperty); + set => SetValue(EnableScaleProperty, value); + } + + protected override void OnAttachedToVisualTree(CompositeDisposable disposables) + { + if (AssociatedObject is null) + { + return; + } + + AnimateImplicit(AssociatedObject, OpacityDuration, EnableScale, ScaleDuration, disposables); + } + + private static void AnimateImplicit(Control control, TimeSpan opacityDuration, bool enableScale, TimeSpan scaleDuration, CompositeDisposable disposables) + { + var compositionVisual = ElementComposition.GetElementVisual(control); + if (compositionVisual is null || compositionVisual.ImplicitAnimations is not null) + { + return; + } + + var compositor = compositionVisual.Compositor; + + var fluentEasing = Easing.Parse("0.4,0,0.6,1"); + + var opacityAnimation = compositor.CreateScalarKeyFrameAnimation(); + opacityAnimation.Target = "Opacity"; + opacityAnimation.InsertExpressionKeyFrame(0f, "this.StartingValue", fluentEasing); + opacityAnimation.InsertExpressionKeyFrame(1f, "this.FinalValue", fluentEasing); + opacityAnimation.Duration = opacityDuration; + opacityAnimation.Direction = PlaybackDirection.Normal; + + var animationGroup = compositor.CreateAnimationGroup(); + animationGroup.Add(opacityAnimation); + + var scaleAnimation = compositor.CreateVector3KeyFrameAnimation(); + scaleAnimation.Target = "Scale"; + if (enableScale) + { + scaleAnimation.InsertExpressionKeyFrame(0f, "Vector3(0.96+(1.0-0.96)*this.Target.Opacity, 0.96+(1.0-0.96)*this.Target.Opacity, 0)", fluentEasing); + scaleAnimation.InsertExpressionKeyFrame(1f, "Vector3(0.96+(1.0-0.96)*this.Target.Opacity, 0.96+(1.0-0.96)*this.Target.Opacity, 0)", fluentEasing); + } + else + { + // Required to make implicit animation for Opacity run on first time. + scaleAnimation.InsertKeyFrame(0f, new Vector3(1, 1, 0)); + scaleAnimation.InsertKeyFrame(1f, new Vector3(1, 1, 0)); + } + scaleAnimation.Duration = scaleDuration; + scaleAnimation.Direction = PlaybackDirection.Normal; + + compositionVisual.CenterPoint = new Vector3((float)control.Bounds.Width / 2, (float)control.Bounds.Height / 2, 0); + + control.GetObservable(Visual.BoundsProperty) + .Subscribe(x => compositionVisual.CenterPoint = new Vector3((float)control.Bounds.Width / 2, (float)control.Bounds.Height / 2, 0)) + .DisposeWith(disposables); + + animationGroup.Add(scaleAnimation); + + var implicitAnimation = compositor.CreateImplicitAnimationCollection(); + implicitAnimation["Opacity"] = animationGroup; + + compositionVisual.ImplicitAnimations = implicitAnimation; + } +} diff --git a/WalletWasabi.Fluent/Behaviors/DisposingBehavior.cs b/WalletWasabi.Fluent/Behaviors/DisposingBehavior.cs index d5ee7bbd81..237a55351b 100644 --- a/WalletWasabi.Fluent/Behaviors/DisposingBehavior.cs +++ b/WalletWasabi.Fluent/Behaviors/DisposingBehavior.cs @@ -4,7 +4,7 @@ namespace WalletWasabi.Fluent.Behaviors; -public abstract class DisposingBehavior : Behavior where T : class, IAvaloniaObject +public abstract class DisposingBehavior : Behavior where T : AvaloniaObject { private CompositeDisposable? _disposables; diff --git a/WalletWasabi.Fluent/Behaviors/DisposingTrigger.cs b/WalletWasabi.Fluent/Behaviors/DisposingTrigger.cs new file mode 100644 index 0000000000..fcc6c57996 --- /dev/null +++ b/WalletWasabi.Fluent/Behaviors/DisposingTrigger.cs @@ -0,0 +1,25 @@ +using System.Reactive.Disposables; +using Avalonia.Xaml.Interactivity; + +namespace WalletWasabi.Fluent.Behaviors; + +public abstract class DisposingTrigger : Trigger +{ + private readonly CompositeDisposable _disposables = new(); + + protected override void OnAttached() + { + base.OnAttached(); + + OnAttached(_disposables); + } + + protected abstract void OnAttached(CompositeDisposable disposables); + + protected override void OnDetaching() + { + base.OnDetaching(); + + _disposables.Dispose(); + } +} diff --git a/WalletWasabi.Fluent/Behaviors/DynamicHeightBehavior.cs b/WalletWasabi.Fluent/Behaviors/DynamicHeightBehavior.cs index c8b5ac7baf..01b89f45f4 100644 --- a/WalletWasabi.Fluent/Behaviors/DynamicHeightBehavior.cs +++ b/WalletWasabi.Fluent/Behaviors/DynamicHeightBehavior.cs @@ -1,6 +1,7 @@ using System.Reactive.Disposables; using Avalonia; using Avalonia.Controls; +using Avalonia.Layout; using ReactiveUI; namespace WalletWasabi.Fluent.Behaviors; @@ -27,7 +28,12 @@ public double HideThresholdHeight protected override void OnAttached(CompositeDisposable disposables) { - AssociatedObject?.Parent?.WhenAnyValue(x => x.Bounds) + if (AssociatedObject?.Parent is not Control parent) + { + return; + } + + parent.WhenAnyValue(x => x.Bounds) .Subscribe(bounds => { var newHeight = bounds.Height * HeightMultiplier; diff --git a/WalletWasabi.Fluent/Behaviors/ExecuteCommandOnDoubleTappedBehavior.cs b/WalletWasabi.Fluent/Behaviors/ExecuteCommandOnDoubleTappedBehavior.cs index f2597fe467..fb42fd7c87 100644 --- a/WalletWasabi.Fluent/Behaviors/ExecuteCommandOnDoubleTappedBehavior.cs +++ b/WalletWasabi.Fluent/Behaviors/ExecuteCommandOnDoubleTappedBehavior.cs @@ -1,4 +1,4 @@ -using System.Reactive.Disposables; +using System.Reactive.Disposables; using System.Windows.Input; using Avalonia; using Avalonia.Controls; diff --git a/WalletWasabi.Fluent/Behaviors/ExecuteCommandOnKeyDownBehavior.cs b/WalletWasabi.Fluent/Behaviors/ExecuteCommandOnKeyDownBehavior.cs index 94f78c3e09..43f17df384 100644 --- a/WalletWasabi.Fluent/Behaviors/ExecuteCommandOnKeyDownBehavior.cs +++ b/WalletWasabi.Fluent/Behaviors/ExecuteCommandOnKeyDownBehavior.cs @@ -63,9 +63,10 @@ protected override void OnAttachedToVisualTree(CompositeDisposable disposable) return; } - if (control.GetVisualRoot() is IInputElement inputRoot) + if (control.GetVisualRoot() is InputElement inputRoot) { - inputRoot.AddDisposableHandler(InputElement.KeyDownEvent, RootDefaultKeyDown, EventRoutingStrategy) + inputRoot + .AddDisposableHandler(InputElement.KeyDownEvent, RootDefaultKeyDown, EventRoutingStrategy) .DisposeWith(disposable); } } diff --git a/WalletWasabi.Fluent/Behaviors/FadeInBehavior.cs b/WalletWasabi.Fluent/Behaviors/FadeInBehavior.cs index 1e75e91d82..2135e6fb19 100644 --- a/WalletWasabi.Fluent/Behaviors/FadeInBehavior.cs +++ b/WalletWasabi.Fluent/Behaviors/FadeInBehavior.cs @@ -8,10 +8,10 @@ namespace WalletWasabi.Fluent.Behaviors; public class FadeInBehavior : AttachedToVisualTreeBehavior { public static readonly StyledProperty InitialDelayProperty = - AvaloniaProperty.Register(nameof(InitialDelay), TimeSpan.FromMilliseconds(500)); + AvaloniaProperty.Register(nameof(InitialDelay), TimeSpan.FromMilliseconds(500)); public static readonly StyledProperty DurationProperty = - AvaloniaProperty.Register(nameof(Duration), TimeSpan.FromMilliseconds(250)); + AvaloniaProperty.Register(nameof(Duration), TimeSpan.FromMilliseconds(250)); public TimeSpan InitialDelay { @@ -65,6 +65,6 @@ protected override void OnAttachedToVisualTree(CompositeDisposable disposable) } } }; - animation.RunAsync(AssociatedObject, null); + animation.RunAsync(AssociatedObject); } } diff --git a/WalletWasabi.Fluent/Behaviors/FlyoutSuggestionBehavior.cs b/WalletWasabi.Fluent/Behaviors/FlyoutSuggestionBehavior.cs index 6b97400877..5c2d041c30 100644 --- a/WalletWasabi.Fluent/Behaviors/FlyoutSuggestionBehavior.cs +++ b/WalletWasabi.Fluent/Behaviors/FlyoutSuggestionBehavior.cs @@ -3,7 +3,9 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Templates; +using Avalonia.Input; using ReactiveUI; +using WalletWasabi.Fluent.Helpers; using WalletWasabi.Fluent.Models; namespace WalletWasabi.Fluent.Behaviors; @@ -16,7 +18,7 @@ public class FlyoutSuggestionBehavior : AttachedToVisualTreeBehavior public static readonly StyledProperty TargetProperty = AvaloniaProperty.Register(nameof(Target)); - public static readonly StyledProperty PlacementModeProperty = AvaloniaProperty.Register(nameof(PlacementMode)); + public static readonly StyledProperty PlacementModeProperty = AvaloniaProperty.Register(nameof(PlacementMode)); private readonly Flyout _flyout; @@ -25,7 +27,7 @@ public FlyoutSuggestionBehavior() _flyout = new Flyout { ShowMode = FlyoutShowMode.Transient }; } - public FlyoutPlacementMode PlacementMode + public PlacementMode PlacementMode { get => GetValue(PlacementModeProperty); set => SetValue(PlacementModeProperty, value); @@ -57,9 +59,35 @@ protected override void OnAttachedToVisualTree(CompositeDisposable disposable) .WhenAnyValue(x => x.Target) .WhereNotNull(); - Displayer(targets).DisposeWith(disposable); - HideOnLostFocus(targets).DisposeWith(disposable); - HideOnTextChange().DisposeWith(disposable); + var contents = this + .GetObservable(ContentProperty); + + var focusChanges = targets + .Select(target => target.GetObservable(InputElement.IsFocusedProperty)) + .Switch(); + + var hideOnLostFocus = focusChanges + .Where(focus => !focus); + + var hideOnTextChange = targets + .Select(target => target.GetObservable(TextBox.TextProperty)) + .Switch() + .WithLatestFrom(contents) + .Select(_ => false); + + var showOnGotFocus = focusChanges + .Where(focus => focus) + .Delay(TimeSpan.FromSeconds(0.2), RxApp.MainThreadScheduler) + .WithLatestFrom(targets, (_, target) => target) + .WithLatestFrom(contents, (tb, newText) => new { TextBox = tb, NewText = newText, CurrentText = tb.Text }) + .Where(arg => !string.IsNullOrWhiteSpace(arg.NewText)) + .Where(x => !EqualityComparer.Equals(x.CurrentText, x.NewText)) + .Do(x => _flyout.Content = CreateSuggestion(x.TextBox, x.NewText)) + .Select(_ => true); + + targets + .Subscribe(target => FlyoutHelpers.ShowFlyout(target, _flyout, showOnGotFocus.Merge(hideOnLostFocus).Merge(hideOnTextChange), disposable)) + .DisposeWith(disposable); Target ??= AssociatedObject as TextBox; @@ -69,36 +97,6 @@ protected override void OnAttachedToVisualTree(CompositeDisposable disposable) .DisposeWith(disposable); } - private IDisposable HideOnLostFocus(IObservable targets) - { - return targets - .Select(x => Observable.FromEventPattern(x, nameof(x.LostFocus))) - .Switch() - .Do(_ => _flyout.Hide()) - .Subscribe(); - } - - private IDisposable HideOnTextChange() - { - return this.WhenAnyValue(x => x.Target.Text) - .WithLatestFrom(this.WhenAnyValue(x => x.Content)) - .Do(_ => _flyout.Hide()) - .Subscribe(); - } - - private IDisposable Displayer(IObservable targets) - { - return targets - .Select(x => Observable.FromEventPattern(x, nameof(x.GotFocus))) - .Switch() - .Select(x => (TextBox?)x.Sender) - .WithLatestFrom(this.WhenAnyValue(x => x.Content)) - .Where(tuple => !string.IsNullOrWhiteSpace(tuple.Second) && !EqualityComparer.Equals(tuple.First?.Text, tuple.Second)) - .Select(tuple => CreateSuggestion(tuple.First, tuple.Second)) - .Do(ShowHint) - .Subscribe(); - } - private Suggestion CreateSuggestion(TextBox? textBox, string content) { return new Suggestion( @@ -113,13 +111,4 @@ private Suggestion CreateSuggestion(TextBox? textBox, string content) _flyout.Hide(); }); } - - private void ShowHint(Suggestion suggestion) - { - _flyout.Content = new ContentControl { ContentTemplate = HintTemplate, Content = suggestion }; - if (Target != null) - { - _flyout.ShowAt(Target); - } - } } diff --git a/WalletWasabi.Fluent/Behaviors/FocusControlAction.cs b/WalletWasabi.Fluent/Behaviors/FocusControlAction.cs new file mode 100644 index 0000000000..9d75209ef4 --- /dev/null +++ b/WalletWasabi.Fluent/Behaviors/FocusControlAction.cs @@ -0,0 +1,49 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Xaml.Interactivity; + +namespace WalletWasabi.Fluent.Behaviors; + +/// +/// Focuses the associated or target control when executed. +/// +public class FocusControlAction : AvaloniaObject, IAction +{ + /// + /// Identifies the avalonia property. + /// + public static readonly StyledProperty TargetControlProperty = + AvaloniaProperty.Register(nameof(TargetControl)); + + /// + /// Gets or sets the target control. This is a avalonia property. + /// + [ResolveByName] + public Control? TargetControl + { + get => GetValue(TargetControlProperty); + set => SetValue(TargetControlProperty, value); + } + + /// + /// Executes the action. + /// + /// The that is passed to the action by the behavior. Generally this is or a target object. + /// The value of this parameter is determined by the caller. + /// Returns null after executed. + public virtual object? Execute(object? sender, object? parameter) + { + if (TargetControl is not null) + { + TargetControl.Focus(); + } + else + { + if (sender is Control control) + { + control.Focus(); + } + } + return null; + } +} diff --git a/WalletWasabi.Fluent/Behaviors/HistoryItemTypeClassBehavior.cs b/WalletWasabi.Fluent/Behaviors/HistoryItemTypeClassBehavior.cs index 0238550aab..9b1e350613 100644 --- a/WalletWasabi.Fluent/Behaviors/HistoryItemTypeClassBehavior.cs +++ b/WalletWasabi.Fluent/Behaviors/HistoryItemTypeClassBehavior.cs @@ -1,11 +1,12 @@ using System.Reactive.Disposables; +using System.Reactive.Linq; +using Avalonia.Controls; using Avalonia.Controls.Primitives; -using ReactiveUI; using WalletWasabi.Fluent.ViewModels.Wallets.Home.History.HistoryItems; namespace WalletWasabi.Fluent.Behaviors; -public class HistoryItemTypeClassBehavior : AttachedToVisualTreeBehavior +public class HistoryItemTypeClassBehavior : AttachedToVisualTreeBehavior { private const string TransactionClass = "Transaction"; @@ -13,65 +14,74 @@ public class HistoryItemTypeClassBehavior : AttachedToVisualTreeBehavior x.DataContext) - .Subscribe(x => - { - RemoveClasses(); - AddClasses(x); - }) - .DisposeWith(disposable); - } + private const string SpeedUpClass = "SpeedUp"; - protected override void OnDetachedFromVisualTree() - { - RemoveClasses(); - } - - private void AddClasses(object? dataContext) + protected override void OnAttachedToVisualTree(CompositeDisposable disposable) { if (AssociatedObject is null) { return; } - switch (dataContext) + Observable + .FromEventPattern( + AssociatedObject, + nameof(Avalonia.Controls.TreeDataGrid.RowPrepared)) + .Select(x => x.EventArgs.Row) + .Subscribe(AddClasses) + .DisposeWith(disposable); + + Observable + .FromEventPattern( + AssociatedObject, + nameof(Avalonia.Controls.TreeDataGrid.RowClearing)) + .Select(x => x.EventArgs.Row) + .Subscribe(RemoveClasses) + .DisposeWith(disposable); + } + + private void AddClasses(TreeDataGridRow row) + { + switch (row.DataContext) { case TransactionHistoryItemViewModel: - AssociatedObject.Classes.Add(TransactionClass); + row.Classes.Add(TransactionClass); break; case CoinJoinHistoryItemViewModel: - AssociatedObject.Classes.Add(CoinJoinClass); + row.Classes.Add(CoinJoinClass); break; case CoinJoinsHistoryItemViewModel: - AssociatedObject.Classes.Add(CoinJoinsClass); + row.Classes.Add(CoinJoinsClass); + break; + + case SpeedUpHistoryItemViewModel: + row.Classes.Add(SpeedUpClass); break; } } - private void RemoveClasses() + private void RemoveClasses(TreeDataGridRow row) { - if (AssociatedObject is null) + if (row.Classes.Contains(TransactionClass)) { - return; + row.Classes.Remove(TransactionClass); } - if (AssociatedObject.Classes.Contains(TransactionClass)) + if (row.Classes.Contains(CoinJoinClass)) { - AssociatedObject.Classes.Remove(TransactionClass); + row.Classes.Remove(CoinJoinClass); } - if (AssociatedObject.Classes.Contains(CoinJoinClass)) + if (row.Classes.Contains(CoinJoinsClass)) { - AssociatedObject.Classes.Remove(CoinJoinClass); + row.Classes.Remove(CoinJoinsClass); } - if (AssociatedObject.Classes.Contains(CoinJoinsClass)) + if (row.Classes.Contains(SpeedUpClass)) { - AssociatedObject.Classes.Remove(CoinJoinsClass); + row.Classes.Remove(SpeedUpClass); } } } diff --git a/WalletWasabi.Fluent/Behaviors/HoldKeyBehavior.cs b/WalletWasabi.Fluent/Behaviors/HoldKeyBehavior.cs index 2c87e7155a..c656acec82 100644 --- a/WalletWasabi.Fluent/Behaviors/HoldKeyBehavior.cs +++ b/WalletWasabi.Fluent/Behaviors/HoldKeyBehavior.cs @@ -32,7 +32,7 @@ public bool IsKeyPressed protected override void OnAttachedToVisualTree(CompositeDisposable disposable) { - if (AssociatedObject.GetVisualRoot() is not IInputElement ie) + if (AssociatedObject.GetVisualRoot() is not InputElement ie) { return; } diff --git a/WalletWasabi.Fluent/Behaviors/ItemsPresenterAnimationBehavior.cs b/WalletWasabi.Fluent/Behaviors/ItemsPresenterAnimationBehavior.cs index 9081e720f6..d4bfa8ee77 100644 --- a/WalletWasabi.Fluent/Behaviors/ItemsPresenterAnimationBehavior.cs +++ b/WalletWasabi.Fluent/Behaviors/ItemsPresenterAnimationBehavior.cs @@ -1,7 +1,6 @@ using Avalonia; using Avalonia.Animation; using Avalonia.Controls; -using Avalonia.Controls.Generators; using Avalonia.Styling; using System.Reactive.Disposables; using System.Reactive.Linq; @@ -37,59 +36,50 @@ protected override void OnAttachedToVisualTree(CompositeDisposable disposable) } Observable - .FromEventPattern(AssociatedObject.ItemContainerGenerator, nameof(ItemContainerGenerator.Materialized)) + .FromEventPattern(AssociatedObject, nameof(ItemsControl.ContainerPrepared)) .Select(x => x.EventArgs) .Subscribe(e => { - foreach (var c in e.Containers) + if (e.Container is not Visual v) { - if (c.ContainerControl is not Visual v) - { - continue; - } + return; + } - var duration = ItemDuration * (c.Index + 1); - var totalDuration = InitialDelay + (duration * 2); + var duration = ItemDuration * (e.Index + 1); + var totalDuration = InitialDelay + (duration * 2); - var animation = new Animation + var animation = new Animation + { + Duration = totalDuration, + Children = { - Duration = totalDuration, - Children = + new KeyFrame { - new KeyFrame + KeyTime = TimeSpan.Zero, + Setters = { - KeyTime = TimeSpan.Zero, - Setters = - { - new Setter(Visual.OpacityProperty, 0d), - } - }, - new KeyFrame + new Setter(Visual.OpacityProperty, 0d), + } + }, + new KeyFrame + { + KeyTime = duration + InitialDelay, + Setters = { - KeyTime = duration + InitialDelay, - Setters = - { - new Setter(Visual.OpacityProperty, 0d), - } - }, - new KeyFrame + new Setter(Visual.OpacityProperty, 0d), + } + }, + new KeyFrame + { + KeyTime = totalDuration, + Setters = { - KeyTime = totalDuration, - Setters = - { - new Setter(Visual.OpacityProperty, 1d), - } + new Setter(Visual.OpacityProperty, 1d), } } - }; - - Dispatcher.UIThread.InvokeAsync(async () => - { - v.Opacity = 0; - await animation.RunAsync(v, null); - v.Opacity = 1; - }); - } + } + }; + animation.RunAsync(v); }) .DisposeWith(disposable); } diff --git a/WalletWasabi.Fluent/Behaviors/KeyDownTrigger.cs b/WalletWasabi.Fluent/Behaviors/KeyDownTrigger.cs new file mode 100644 index 0000000000..b77d8c2ae7 --- /dev/null +++ b/WalletWasabi.Fluent/Behaviors/KeyDownTrigger.cs @@ -0,0 +1,47 @@ +using System.Reactive.Disposables; +using Avalonia; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Xaml.Interactivity; + +namespace WalletWasabi.Fluent.Behaviors; + +public class KeyDownTrigger : DisposingTrigger +{ + public static readonly StyledProperty EventRoutingStrategyProperty = AvaloniaProperty.Register(nameof(EventRoutingStrategy)); + + public static readonly StyledProperty KeyProperty = AvaloniaProperty.Register(nameof(Key)); + + public RoutingStrategies EventRoutingStrategy + { + get => GetValue(EventRoutingStrategyProperty); + set => SetValue(EventRoutingStrategyProperty, value); + } + + public Key Key + { + get => GetValue(KeyProperty); + set => SetValue(KeyProperty, value); + } + + public bool MarkAsHandled { get; set; } + + protected override void OnAttached(CompositeDisposable disposables) + { + if (AssociatedObject is InputElement element) + { + element + .AddDisposableHandler(InputElement.KeyDownEvent, OnKeyDown, EventRoutingStrategy) + .DisposeWith(disposables); + } + } + + private void OnKeyDown(object? sender, KeyEventArgs e) + { + if (e.Key == Key) + { + e.Handled = MarkAsHandled; + Interaction.ExecuteActions(AssociatedObject, Actions, null); + } + } +} diff --git a/WalletWasabi.Fluent/Behaviors/ListBoxPreviewBehavior.cs b/WalletWasabi.Fluent/Behaviors/ListBoxPreviewBehavior.cs index dc09ab9b23..422115d4f6 100644 --- a/WalletWasabi.Fluent/Behaviors/ListBoxPreviewBehavior.cs +++ b/WalletWasabi.Fluent/Behaviors/ListBoxPreviewBehavior.cs @@ -41,7 +41,7 @@ protected override void OnAttached(CompositeDisposable disposables) return; } - Observable.FromEventPattern(AssociatedObject, nameof(AssociatedObject.PointerLeave)) + Observable.FromEventPattern(AssociatedObject, nameof(AssociatedObject.PointerExited)) .Subscribe(_ => ClearPreviewItem(0)) .DisposeWith(disposables); @@ -73,8 +73,8 @@ private void ClearPreviewItem(int delay) if (delay > 0) { Observable.Timer(TimeSpan.FromMilliseconds(delay)) - .ObserveOn(RxApp.MainThreadScheduler) - .Subscribe(_ => PreviewItem = null, _clearItemCts.Token); + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(_ => PreviewItem = null, _clearItemCts.Token); } else { diff --git a/WalletWasabi.Fluent/Behaviors/ListBoxReselectingBehavior.cs b/WalletWasabi.Fluent/Behaviors/ListBoxReselectingBehavior.cs new file mode 100644 index 0000000000..44cf763ae4 --- /dev/null +++ b/WalletWasabi.Fluent/Behaviors/ListBoxReselectingBehavior.cs @@ -0,0 +1,48 @@ +using System.Collections.Specialized; +using System.Reactive; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using Avalonia.Controls; +using DynamicData.Binding; +using ReactiveUI; + +namespace WalletWasabi.Fluent.Behaviors; + +public class ListBoxReselectingBehavior : DisposingBehavior +{ + protected override void OnAttached(CompositeDisposable disposables) + { + if (AssociatedObject is null) + { + return; + } + + ItemsCollectionChanged() + .Where(IsSelectionMoving) + .Select(x => x.EventArgs.NewStartingIndex) + .Do(SetSelectedIndex) + .Subscribe() + .DisposeWith(disposables); + } + + private IObservable> ItemsCollectionChanged() + { + return this + .WhenAnyValue(x => x.AssociatedObject!.Items) + .OfType() + .Select(x => x.ObserveCollectionChanges()) + .Switch(); + } + + private void SetSelectedIndex(int newIndex) + { + AssociatedObject!.SelectedIndex = newIndex; + } + + private bool IsSelectionMoving(EventPattern x) + { + var isMove = x.EventArgs.Action == NotifyCollectionChangedAction.Move; + var isSelectedMoving = AssociatedObject!.SelectedIndex == x.EventArgs.OldStartingIndex; + return isMove && isSelectedMoving; + } +} diff --git a/WalletWasabi.Fluent/Behaviors/MenuItemIconSizeBehavior.cs b/WalletWasabi.Fluent/Behaviors/MenuItemIconSizeBehavior.cs new file mode 100644 index 0000000000..c899e6e60e --- /dev/null +++ b/WalletWasabi.Fluent/Behaviors/MenuItemIconSizeBehavior.cs @@ -0,0 +1,19 @@ +using Avalonia.Controls; +using Avalonia.Xaml.Interactivity; + +namespace WalletWasabi.Fluent.Behaviors; + +// Fix: https://github.com/AvaloniaUI/Avalonia/blob/965d0e973da61d279dad9b382497248de3416e20/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml#L90 +public class MenuItemIconSizeBehavior : Behavior +{ + protected override void OnAttachedToVisualTree() + { + base.OnAttachedToVisualTree(); + + if (AssociatedObject is { } viewbox) + { + viewbox.Width = 20; + viewbox.Height = 20; + } + } +} diff --git a/WalletWasabi.Fluent/Behaviors/NavBarSelectedIndicatorChildBehavior.cs b/WalletWasabi.Fluent/Behaviors/NavBarSelectedIndicatorChildBehavior.cs deleted file mode 100644 index 48f5c7a425..0000000000 --- a/WalletWasabi.Fluent/Behaviors/NavBarSelectedIndicatorChildBehavior.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System.Collections.Specialized; -using System.Reactive.Disposables; -using System.Reactive.Linq; -using Avalonia; -using Avalonia.Controls; -using Avalonia.Threading; - -namespace WalletWasabi.Fluent.Behaviors; - -public class NavBarSelectedIndicatorChildBehavior : AttachedToVisualTreeBehavior -{ - public static readonly AttachedProperty NavBarItemParentProperty = - AvaloniaProperty.RegisterAttached( - "NavBarItemParent"); - - public static Control GetNavBarItemParent(Control element) - { - return element.GetValue(NavBarItemParentProperty); - } - - public static void SetNavBarItemParent(Control element, Control value) - { - element.SetValue(NavBarItemParentProperty, value); - } - - private void OnLoaded(CompositeDisposable disposable) - { - if (AssociatedObject is null) - { - return; - } - - var sharedState = NavBarSelectedIndicatorParentBehavior.GetParentState(AssociatedObject); - if (sharedState is null) - { - Detach(); - return; - } - - var parent = GetNavBarItemParent(AssociatedObject); - - Observable.FromEventPattern(parent.Classes, "CollectionChanged") - .Select(_ => parent.Classes) - .Select(x => x.Contains(":selected") - && !x.Contains(":pressed") - && !x.Contains(":dragging") - && x.Contains(":selectable")) - .DistinctUntilChanged() - .Where(x => x) - .ObserveOn(AvaloniaScheduler.Instance) - .Subscribe(_ => sharedState.AnimateIndicatorAsync(AssociatedObject)) - .DisposeWith(disposable); - - AssociatedObject.Opacity = 0; - - if (parent.Classes.Contains(":selected")) - { - sharedState.SetActive(AssociatedObject); - } - } - - protected override void OnAttachedToVisualTree(CompositeDisposable disposable) - { - Dispatcher.UIThread.Post(() => OnLoaded(disposable), DispatcherPriority.Loaded); - } -} diff --git a/WalletWasabi.Fluent/Behaviors/NavBarSelectedIndicatorParentBehavior.cs b/WalletWasabi.Fluent/Behaviors/NavBarSelectedIndicatorParentBehavior.cs deleted file mode 100644 index 36c33d2ab7..0000000000 --- a/WalletWasabi.Fluent/Behaviors/NavBarSelectedIndicatorParentBehavior.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reactive.Disposables; -using Avalonia; -using Avalonia.Controls; -using WalletWasabi.Fluent.Models; - -namespace WalletWasabi.Fluent.Behaviors; - -public class NavBarSelectedIndicatorParentBehavior : AttachedToVisualTreeBehavior -{ - public static readonly AttachedProperty ParentStateProperty = - AvaloniaProperty - .RegisterAttached( - "ParentState", inherits: true); - - public static NavBarSelectedIndicatorState? GetParentState(Control element) - { - return element.GetValue(ParentStateProperty); - } - - public static void SetParentState(Control element, NavBarSelectedIndicatorState value) - { - element.SetValue(ParentStateProperty, value); - } - - protected override void OnAttachedToVisualTree(CompositeDisposable disposable) - { - if (AssociatedObject is null) - { - return; - } - - var sharedState = new NavBarSelectedIndicatorState(); - SetParentState(AssociatedObject, sharedState); - disposable.Add(sharedState); - } -} diff --git a/WalletWasabi.Fluent/Behaviors/NumberBoxBehavior.cs b/WalletWasabi.Fluent/Behaviors/NumberBoxBehavior.cs deleted file mode 100644 index d728c741c9..0000000000 --- a/WalletWasabi.Fluent/Behaviors/NumberBoxBehavior.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Linq; -using System.Reactive.Disposables; -using System.Reactive.Linq; -using Avalonia; -using Avalonia.Controls; -using Avalonia.Input; -using Avalonia.Interactivity; -using WalletWasabi.Fluent.Extensions; - -namespace WalletWasabi.Fluent.Behaviors; - -public class NumberBoxBehavior : DisposingBehavior -{ - protected override void OnAttached(CompositeDisposable disposables) - { - if (AssociatedObject is null) - { - return; - } - - AssociatedObject - .AddDisposableHandler(InputElement.TextInputEvent, (_, e) => - { - if (e.Text is { }) - { - e.Text = CorrectInput(e.Text); - } - }, RoutingStrategies.Tunnel) - .DisposeWith(disposables); - - Observable - .FromEventPattern(AssociatedObject, nameof(AssociatedObject.PastingFromClipboard)) - .Select(x => x.EventArgs) - .SubscribeAsync(async e => - { - e.Handled = true; - - if (Application.Current is { Clipboard: { } clipboard }) - { - AssociatedObject.Text = CorrectInput(await clipboard.GetTextAsync()); - } - }) - .DisposeWith(disposables); - } - - private string CorrectInput(string input) - { - return new string(input.Where(c => char.IsDigit(c) || c == '.').ToArray()); - } -} diff --git a/WalletWasabi.Fluent/Behaviors/PasteButtonFlashBehavior.cs b/WalletWasabi.Fluent/Behaviors/PasteButtonFlashBehavior.cs index 405bf99de7..70391ec040 100644 --- a/WalletWasabi.Fluent/Behaviors/PasteButtonFlashBehavior.cs +++ b/WalletWasabi.Fluent/Behaviors/PasteButtonFlashBehavior.cs @@ -8,6 +8,7 @@ using ReactiveUI; using WalletWasabi.Fluent.Controls; using WalletWasabi.Fluent.Extensions; +using WalletWasabi.Fluent.Helpers; using WalletWasabi.Userfacing; namespace WalletWasabi.Fluent.Behaviors; @@ -73,7 +74,7 @@ protected override void OnAttachedToVisualTree(CompositeDisposable disposables) private async Task CheckClipboardForValidAddressAsync(bool forceCheck = false) { - if (Application.Current is { Clipboard: { } clipboard }) + if (ApplicationHelper.Clipboard is { } clipboard) { var clipboardValue = (await clipboard.GetTextAsync()) ?? ""; diff --git a/WalletWasabi.Fluent/Behaviors/PendingHistoryItemSeparatorBehavior.cs b/WalletWasabi.Fluent/Behaviors/PendingHistoryItemSeparatorBehavior.cs index ecfd389fe7..dff6bf7ff1 100644 --- a/WalletWasabi.Fluent/Behaviors/PendingHistoryItemSeparatorBehavior.cs +++ b/WalletWasabi.Fluent/Behaviors/PendingHistoryItemSeparatorBehavior.cs @@ -1,6 +1,7 @@ using System.Reactive.Disposables; using Avalonia.Controls; using Avalonia.Controls.Primitives; +using Avalonia.VisualTree; using WalletWasabi.Fluent.ViewModels.Wallets.Home.History.HistoryItems; namespace WalletWasabi.Fluent.Behaviors; @@ -32,23 +33,24 @@ private void AssociatedObjectOnLayoutUpdated(object? sender, EventArgs e) return; } - foreach (var child in ((IPanel)AssociatedObject).Children) + var children = AssociatedObject.GetVisualChildren(); + foreach (var child in children) { if (child is { }) { - InvalidateSeparator(child, presenter); + InvalidateSeparator((Control)child, presenter); } } } - private void InvalidateSeparator(IControl control, TreeDataGridRowsPresenter presenter) + private void InvalidateSeparator(Control control, TreeDataGridRowsPresenter presenter) { if (control.DataContext is not HistoryItemViewModelBase currentHistoryItem) { return; } - if (currentHistoryItem.IsConfirmed) + if (currentHistoryItem.Transaction.IsConfirmed) { if (control.Classes.Contains(ClassName)) { @@ -74,7 +76,7 @@ static bool IsSeparator(TreeDataGridRowsPresenter presenter, int index) { return presenter.Items is { } items && items.Count > index + 1 - && presenter.Items[index + 1].Model is HistoryItemViewModelBase { IsConfirmed: true }; + && presenter.Items[index + 1].Model is HistoryItemViewModelBase vm && vm.Transaction.IsConfirmed; } } } diff --git a/WalletWasabi.Fluent/Behaviors/RandomizedWorldPointsBehavior.cs b/WalletWasabi.Fluent/Behaviors/RandomizedWorldPointsBehavior.cs index 5ffb218a2b..be611f8b1b 100644 --- a/WalletWasabi.Fluent/Behaviors/RandomizedWorldPointsBehavior.cs +++ b/WalletWasabi.Fluent/Behaviors/RandomizedWorldPointsBehavior.cs @@ -15,7 +15,7 @@ public class RandomizedWorldPointsBehavior : Behavior { private static readonly Random RandomSource = new(); private CancellationTokenSource _cts = new(); - private List _targetControls = new(); + private List _targetControls = new(); // ReSharper disable ArrangeObjectCreationWhenTypeNotEvident private static readonly List WorldLocations = new() @@ -110,7 +110,7 @@ private void RunAnimation(CancellationToken cancellationToken) cancellationToken); } - private async Task AnimateCityMarkerAsync(IControl target, Point point, CancellationToken cancellationToken) + private async Task AnimateCityMarkerAsync(Control target, Point point, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { @@ -161,7 +161,7 @@ protected override void OnAttached() return; } - _targetControls = targets; + _targetControls = targets.Cast().ToList(); _cts?.Dispose(); _cts = new CancellationTokenSource(); diff --git a/WalletWasabi.Fluent/Behaviors/RegisterNotificationHostBehavior.cs b/WalletWasabi.Fluent/Behaviors/RegisterNotificationHostBehavior.cs index 9acbd05d31..234421d26b 100644 --- a/WalletWasabi.Fluent/Behaviors/RegisterNotificationHostBehavior.cs +++ b/WalletWasabi.Fluent/Behaviors/RegisterNotificationHostBehavior.cs @@ -1,14 +1,12 @@ using System.Reactive.Disposables; -using System.Reactive.Linq; -using Avalonia.Controls; -using ReactiveUI; +using Avalonia; using WalletWasabi.Fluent.Helpers; namespace WalletWasabi.Fluent.Behaviors; -public class RegisterNotificationHostBehavior : DisposingBehavior +public class RegisterNotificationHostBehavior : AttachedToVisualTreeBehavior { - protected override void OnAttached(CompositeDisposable disposables) + protected override void OnAttachedToVisualTree(CompositeDisposable disposable) { if (AssociatedObject is null) { @@ -16,12 +14,5 @@ protected override void OnAttached(CompositeDisposable disposables) } NotificationHelpers.SetNotificationManager(AssociatedObject); - - // Must set notification host again after theme changing. - Observable - .FromEventPattern(AssociatedObject, nameof(AssociatedObject.ResourcesChanged)) - .ObserveOn(RxApp.MainThreadScheduler) - .Subscribe(_ => NotificationHelpers.SetNotificationManager(AssociatedObject)) - .DisposeWith(disposables); } } diff --git a/WalletWasabi.Fluent/Behaviors/ScrollToSelectedItemBehavior.cs b/WalletWasabi.Fluent/Behaviors/ScrollToSelectedItemBehavior.cs index 6ab7cee6c6..f32495d0fc 100644 --- a/WalletWasabi.Fluent/Behaviors/ScrollToSelectedItemBehavior.cs +++ b/WalletWasabi.Fluent/Behaviors/ScrollToSelectedItemBehavior.cs @@ -1,6 +1,7 @@ using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; +using Avalonia; using ReactiveUI; namespace WalletWasabi.Fluent.Behaviors; @@ -9,10 +10,33 @@ public class ScrollToSelectedItemBehavior : AttachedToVisualTreeBehavior rowSelection.SelectedIndex.FirstOrDefault()) + Observable.FromEventPattern(rowSelection, nameof(rowSelection.SelectionChanged)) + .Select(x => + { + var selectedIndexPath = rowSelection.SelectedIndex.FirstOrDefault(); + if (AssociatedObject.Rows is null) + { + return selectedIndexPath; + } + + // Get the actual index in the list of items. + var rowIndex = AssociatedObject.Rows.ModelIndexToRowIndex(selectedIndexPath); + + // Correct the index wih the index of child item, in the case when the selected item is a child. + if (rowSelection.SelectedIndex.Count > 1) + { + // Skip 1 because the first index is the parent. + // Every other index is the child index. + rowIndex += rowSelection.SelectedIndex.Skip(1).Sum(); + + // Need to add 1 to get the correct index. + rowIndex += 1; + } + + return rowIndex; + }) .WhereNotNull() .Do(ScrollToItemIndex) .Subscribe() diff --git a/WalletWasabi.Fluent/Behaviors/SelectingItemsControlBehavior.cs b/WalletWasabi.Fluent/Behaviors/SelectingItemsControlBehavior.cs new file mode 100644 index 0000000000..0e22e22dc0 --- /dev/null +++ b/WalletWasabi.Fluent/Behaviors/SelectingItemsControlBehavior.cs @@ -0,0 +1,140 @@ +// Based on code: https://github.com/adirh3/Avalonia.ListBoxAnimation.Samples +using System.Linq; +using System.Numerics; +using Avalonia; +using Avalonia.Animation.Easings; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Rendering.Composition; +using Avalonia.VisualTree; + +namespace WalletWasabi.Fluent.Behaviors; + +public class SelectingItemsControlBehavior +{ + public static readonly AttachedProperty EnableSelectionAnimationProperty = + AvaloniaProperty.RegisterAttached("EnableSelectionAnimation", typeof(SelectingItemsControlBehavior)); + + static SelectingItemsControlBehavior() + { + EnableSelectionAnimationProperty.Changed.AddClassHandler(OnEnableSelectionAnimation); + } + + private static void OnEnableSelectionAnimation(Control control, AvaloniaPropertyChangedEventArgs args) + { + if (control is SelectingItemsControl listBox) + { + if (args.NewValue is true) + { + listBox.PropertyChanged += SelectingItemsControlPropertyChanged; + } + else + { + listBox.PropertyChanged -= SelectingItemsControlPropertyChanged; + } + } + } + + private static void SelectingItemsControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs args) + { + if (sender is not SelectingItemsControl selectingItemsControl || + args.Property != SelectingItemsControl.SelectedIndexProperty || + args.OldValue is not int oldIndex || args.NewValue is not int newIndex) + { + return; + } + + if (selectingItemsControl.ContainerFromIndex(newIndex) is not TemplatedControl newSelection + || selectingItemsControl.ContainerFromIndex(oldIndex) is not TemplatedControl oldSelection) + { + return; + } + + StartOffsetAnimation(newSelection, oldSelection); + } + + private static void StartOffsetAnimation(TemplatedControl newSelection, TemplatedControl oldSelection) + { + // Find the indicator border + // NOTE: + // The original required putting PART_SelectedPipe in template (e.g. ListBox > ListBoxItem) + // and used GetTemplateChildren() instead of GetVisualDescendants() + if (newSelection.GetVisualDescendants().FirstOrDefault(s => s.Name == "PART_SelectedPipe") is not { } borderPipe + || oldSelection.GetVisualDescendants().FirstOrDefault(s => s.Name == "PART_SelectedPipe") is not { } oldPipe) + { + return; + } + + // Clear old implicit animations if any + ElementComposition.GetElementVisual(oldPipe)?.ImplicitAnimations?.Clear(); + + // Get the composition visuals for all controls + var pipeVisual = ElementComposition.GetElementVisual(borderPipe); + var newSelectionVisual = ElementComposition.GetElementVisual(newSelection); + var oldSelectionVisual = ElementComposition.GetElementVisual(oldSelection); + if (pipeVisual == null || newSelectionVisual == null || oldSelectionVisual == null) + { + return; + } + + // Calculate the offset between old and new selections + var selectionOffset = oldSelectionVisual.Offset - newSelectionVisual.Offset; + + // Check whether the offset is vertical (e.g. ListBox) or horizontal (e.g. TabControl) + // Note this code assumes the items are aligned in the SelectingItemsControl + var isVerticalOffset = selectionOffset.Y != 0; + var offset = isVerticalOffset ? selectionOffset.Y : selectionOffset.X; + var compositor = pipeVisual.Compositor; + + // This is required + var quadraticEaseIn = new SpringEasing(); + + // Create new offset animation between old selection position to the current position + var offsetAnimation = compositor.CreateVector3KeyFrameAnimation(); + offsetAnimation.Target = "Offset"; + var expression = (offset > 0 ? "+" : "-") + Math.Abs(offset); + offsetAnimation.InsertExpressionKeyFrame( + 0f, + isVerticalOffset + ? $"Vector3(this.FinalValue.X, this.FinalValue.Y{expression}, 0)" + : $"Vector3(this.FinalValue.X{expression}, this.FinalValue.Y, 0)"); + offsetAnimation.InsertExpressionKeyFrame(1f, "this.FinalValue"); + offsetAnimation.Duration = TimeSpan.FromMilliseconds(250); + + // Create small scale animation so the pipe will "stretch" while it's moving + var scaleAnimation = compositor.CreateVector3KeyFrameAnimation(); + scaleAnimation.Target = "Scale"; + scaleAnimation.InsertKeyFrame(0f, Vector3.One, quadraticEaseIn); + scaleAnimation.InsertKeyFrame(0.5f, new Vector3(1f + (!isVerticalOffset ? 0.75f : 0f), 1f + (isVerticalOffset ? 0.75f : 0f), 1f), quadraticEaseIn); + scaleAnimation.InsertKeyFrame(1f, Vector3.One, quadraticEaseIn); + scaleAnimation.Duration = TimeSpan.FromMilliseconds(250); + + var compositionAnimationGroup = compositor.CreateAnimationGroup(); + compositionAnimationGroup.Add(offsetAnimation); + compositionAnimationGroup.Add(scaleAnimation); + var pipeVisualImplicitAnimations = compositor.CreateImplicitAnimationCollection(); + var currentOffset = isVerticalOffset ? pipeVisual.Offset.Y : pipeVisual.Offset.X; + if (currentOffset == 0) + { + // Visual first shown, offset not calculated, lets trigger using Offset + pipeVisualImplicitAnimations["Offset"] = compositionAnimationGroup; + } + else + { + // Visual already shown, we can't trigger on Offset as it won't change + pipeVisualImplicitAnimations["Visible"] = compositionAnimationGroup; + } + + pipeVisual.ImplicitAnimations = pipeVisualImplicitAnimations; + } + + public static bool GetEnableSelectionAnimation(SelectingItemsControl element) + { + return element.GetValue(EnableSelectionAnimationProperty); + } + + public static void SetEnableSelectionAnimation(SelectingItemsControl element, bool value) + { + element.SetValue(EnableSelectionAnimationProperty, value); + } +} diff --git a/WalletWasabi.Fluent/Behaviors/ShowAttachedFlyoutWhenFocusedBehavior.cs b/WalletWasabi.Fluent/Behaviors/ShowAttachedFlyoutWhenFocusedBehavior.cs deleted file mode 100644 index 3f7a17a36c..0000000000 --- a/WalletWasabi.Fluent/Behaviors/ShowAttachedFlyoutWhenFocusedBehavior.cs +++ /dev/null @@ -1,119 +0,0 @@ -using System.Reactive.Disposables; -using System.Reactive.Linq; -using Avalonia; -using Avalonia.Controls; -using Avalonia.Controls.Diagnostics; -using Avalonia.Controls.Primitives; -using Avalonia.Input; -using Avalonia.VisualTree; -using ReactiveUI; -using WalletWasabi.Fluent.Extensions; -using WalletWasabi.Fluent.Helpers; - -namespace WalletWasabi.Fluent.Behaviors; - -public class ShowAttachedFlyoutWhenFocusedBehavior : AttachedToVisualTreeBehavior -{ - public static readonly DirectProperty IsFlyoutOpenProperty = - AvaloniaProperty.RegisterDirect( - "IsFlyoutOpen", - o => o.IsFlyoutOpen, - (o, v) => o.IsFlyoutOpen = v); - - private bool _isFlyoutOpen; - - public bool IsFlyoutOpen - { - get => _isFlyoutOpen; - set => SetAndRaise(IsFlyoutOpenProperty, ref _isFlyoutOpen, value); - } - - protected override void OnAttachedToVisualTree(CompositeDisposable disposable) - { - if (AssociatedObject?.GetVisualRoot() is not Control visualRoot) - { - return; - } - - var flyoutBase = FlyoutBase.GetAttachedFlyout(AssociatedObject); - if (flyoutBase is null) - { - return; - } - - var controller = new FlyoutShowController(AssociatedObject, flyoutBase).DisposeWith(disposable); - - FocusBasedFlyoutOpener(AssociatedObject, flyoutBase).DisposeWith(disposable); - IsOpenPropertySynchronizer(controller).DisposeWith(disposable); - - // EDGE CASES - // Edge case when the Visual Root becomes active and the Associated object is focused. - ActivateOpener(AssociatedObject, visualRoot, controller).DisposeWith(disposable); - DeactivateCloser(visualRoot, controller).DisposeWith(disposable); - - // This is a workaround for the case when the user switches theme. The same behavior is detached and re-attached on theme changes. - // If you don't close it, the Flyout will show in an incorrect position. Maybe bug in Avalonia? - if (IsFlyoutOpen) - { - IsFlyoutOpen = false; - } - } - - private static IDisposable DeactivateCloser(Control visualRoot, FlyoutShowController controller) - { - return Observable.FromEventPattern(visualRoot, nameof(Window.Deactivated)) - .Do(_ => controller.SetIsForcedOpen(false)) - .Subscribe(); - } - - private static IDisposable ActivateOpener( - IInputElement associatedObject, - Control visualRoot, - FlyoutShowController controller) - { - return Observable.FromEventPattern(visualRoot, nameof(Window.Activated)) - .Where(_ => associatedObject.IsFocused) - .Do(_ => controller.SetIsForcedOpen(true)) - .Subscribe(); - } - - private static IObservable GetPopupIsFocused(FlyoutBase flyoutBase) - { - var currentPopupHost = Observable - .FromEventPattern(flyoutBase, nameof(flyoutBase.Opened)) - .Select(_ => ((IPopupHostProvider)flyoutBase).PopupHost?.Presenter) - .WhereNotNull(); - - var popupGotFocus = currentPopupHost.Select(x => x.OnEvent(InputElement.GotFocusEvent)).Switch().ToSignal(); - var popupLostFocus = currentPopupHost.Select(x => x.OnEvent(InputElement.LostFocusEvent)).Switch().ToSignal(); - var flyoutGotFocus = popupGotFocus.Select(_ => true).Merge(popupLostFocus.Select(_ => false)); - return flyoutGotFocus; - } - - private IDisposable IsOpenPropertySynchronizer(FlyoutShowController controller) - { - return this - .WhenAnyValue(x => x.IsFlyoutOpen) - .Do(controller.SetIsForcedOpen) - .Subscribe(); - } - - private IDisposable FocusBasedFlyoutOpener( - IAvaloniaObject associatedObject, - FlyoutBase flyoutBase) - { - var isPopupFocused = GetPopupIsFocused(flyoutBase); - var isAssociatedObjectFocused = associatedObject.GetObservable(InputElement.IsFocusedProperty); - - var mergedFocused = isAssociatedObjectFocused.Merge(isPopupFocused); - - var weAreFocused = mergedFocused - .Throttle(TimeSpan.FromSeconds(0.1)) - .DistinctUntilChanged(); - - return weAreFocused - .ObserveOn(RxApp.MainThreadScheduler) - .Do(isOpen => IsFlyoutOpen = isOpen) - .Subscribe(); - } -} diff --git a/WalletWasabi.Fluent/Behaviors/ShowFlyoutOnPointerOverBehavior.cs b/WalletWasabi.Fluent/Behaviors/ShowFlyoutOnPointerOverBehavior.cs index c3a9ed2653..e3553fc989 100644 --- a/WalletWasabi.Fluent/Behaviors/ShowFlyoutOnPointerOverBehavior.cs +++ b/WalletWasabi.Fluent/Behaviors/ShowFlyoutOnPointerOverBehavior.cs @@ -3,31 +3,23 @@ using Avalonia.Controls; using Avalonia.Controls.Primitives; using ReactiveUI; +using WalletWasabi.Fluent.Helpers; namespace WalletWasabi.Fluent.Behaviors; -public class ShowFlyoutOnPointerOverBehavior : DisposingBehavior +public class ShowFlyoutOnPointerOverBehavior : AttachedToVisualTreeBehavior { - protected override void OnAttached(CompositeDisposable disposables) + protected override void OnAttachedToVisualTree(CompositeDisposable disposable) { - if (AssociatedObject is null) + if (AssociatedObject is { } target && FlyoutBase.GetAttachedFlyout(target) is { } flyout) { - return; - } - - Observable - .FromEventPattern(AssociatedObject, nameof(AssociatedObject.PointerMoved)) - .Throttle(TimeSpan.FromMilliseconds(100)) - .ObserveOn(RxApp.MainThreadScheduler) - .Subscribe(_ => OnPointerMove()) - .DisposeWith(disposables); - } + var showFlyout = Observable + .FromEventPattern(target, nameof(AssociatedObject.PointerMoved)) + .Throttle(TimeSpan.FromMicroseconds(100)) + .ObserveOn(RxApp.MainThreadScheduler) + .Select(_ => target.IsPointerOver); - private void OnPointerMove() - { - if (AssociatedObject is { } obj && obj.IsPointerOver) - { - FlyoutBase.ShowAttachedFlyout(AssociatedObject); + FlyoutHelpers.ShowFlyout(target, flyout, showFlyout, disposable, windowActivityRequired: false); } } } diff --git a/WalletWasabi.Fluent/Behaviors/ShowFlyoutWhenFocusedBehavior.cs b/WalletWasabi.Fluent/Behaviors/ShowFlyoutWhenFocusedBehavior.cs new file mode 100644 index 0000000000..7f01a9c9e8 --- /dev/null +++ b/WalletWasabi.Fluent/Behaviors/ShowFlyoutWhenFocusedBehavior.cs @@ -0,0 +1,109 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Reflection; +using Avalonia; +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Controls.Diagnostics; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Avalonia.Input; +using Avalonia.LogicalTree; +using Avalonia.VisualTree; +using ReactiveUI; +using WalletWasabi.Fluent.Extensions; +using WalletWasabi.Fluent.Helpers; + +namespace WalletWasabi.Fluent.Behaviors; + +public class ShowFlyoutWhenFocusedBehavior : AttachedToVisualTreeBehavior +{ + public static readonly DirectProperty IsFlyoutOpenProperty = + AvaloniaProperty.RegisterDirect( + "IsFlyoutOpen", + o => o.IsFlyoutOpen, + (o, v) => o.IsFlyoutOpen = v); + + private bool _isFlyoutOpen; + + public bool IsFlyoutOpen + { + get => _isFlyoutOpen; + set => SetAndRaise(IsFlyoutOpenProperty, ref _isFlyoutOpen, value); + } + + protected override void OnAttachedToVisualTree(CompositeDisposable disposable) + { + if (AssociatedObject?.GetVisualRoot() is Control && + FlyoutBase.GetAttachedFlyout(AssociatedObject) is Flyout flyout) + { + var flyoutController = new FlyoutController(flyout) + .DisposeWith(disposable); + + FlyoutHelpers.ShowFlyout(AssociatedObject, flyout, this.GetObservable(IsFlyoutOpenProperty), disposable); + FocusBasedFlyoutOpener(AssociatedObject, flyoutController).DisposeWith(disposable); + + // This is a workaround for the case when the user switches theme. The same behavior is detached and re-attached on theme changes. + // If you don't close it, the Flyout will show in an incorrect position. Maybe bug in Avalonia? + if (IsFlyoutOpen) + { + IsFlyoutOpen = false; + } + } + } + + private static IObservable GetPopupIsFocused(FlyoutBase flyout) + { + var currentPopupHost = Observable + .FromEventPattern(flyout, nameof(flyout.Opened)) + .Select(_ => ((IPopupHostProvider)flyout).PopupHost?.Presenter) + .WhereNotNull(); + + var popupGotFocus = currentPopupHost.Select(x => x.OnEvent(InputElement.GotFocusEvent)).Switch(); + var popupLostFocus = currentPopupHost.Select(x => x.OnEvent(InputElement.LostFocusEvent)).Switch(); + var flyoutGotFocus = popupGotFocus.Select(_ => true).Merge(popupLostFocus.Select(_ => false)); + return flyoutGotFocus; + } + + private IDisposable FocusBasedFlyoutOpener(Control associatedObject, FlyoutController flyoutController) + { + var isPopupFocused = GetPopupIsFocused(flyoutController.Flyout); + var isAssociatedObjectFocused = associatedObject.GetObservable(InputElement.IsFocusedProperty); + + var mergedFocused = isAssociatedObjectFocused.Merge(isPopupFocused); + + var weAreFocused = mergedFocused + .Throttle(TimeSpan.FromSeconds(0.1)) + .DistinctUntilChanged(); + + return weAreFocused + .ObserveOn(RxApp.MainThreadScheduler) + .Do(isOpen => IsFlyoutOpen = isOpen) + .Subscribe(); + } + + private class FlyoutController : IDisposable + { + public FlyoutController(PopupFlyoutBase flyout) + { + Flyout = flyout; + Flyout.Closing += FlyoutClosing; + } + + public PopupFlyoutBase Flyout { get; } + public bool PreventClose { get; set; } + + public void Dispose() + { + Flyout.Closing -= FlyoutClosing; + } + + private void FlyoutClosing(object? sender, CancelEventArgs e) + { + e.Cancel = PreventClose; + } + } +} diff --git a/WalletWasabi.Fluent/Behaviors/ShowWalletCoinsOnKeyCombinationBehavior.cs b/WalletWasabi.Fluent/Behaviors/ShowWalletCoinsOnKeyCombinationBehavior.cs index ffd2569db6..c2bd51201e 100644 --- a/WalletWasabi.Fluent/Behaviors/ShowWalletCoinsOnKeyCombinationBehavior.cs +++ b/WalletWasabi.Fluent/Behaviors/ShowWalletCoinsOnKeyCombinationBehavior.cs @@ -52,7 +52,7 @@ public WalletViewModel? Wallet protected override void OnAttachedToVisualTree(CompositeDisposable disposable) { - if (AssociatedObject?.GetVisualRoot() is not IInputElement inputRoot) + if (AssociatedObject?.GetVisualRoot() is not InputElement inputRoot) { return; } diff --git a/WalletWasabi.Fluent/Behaviors/TextBoxSelectAllTextBehavior.cs b/WalletWasabi.Fluent/Behaviors/TextBoxSelectAllTextBehavior.cs index 10f87eb9a8..0d844d8081 100644 --- a/WalletWasabi.Fluent/Behaviors/TextBoxSelectAllTextBehavior.cs +++ b/WalletWasabi.Fluent/Behaviors/TextBoxSelectAllTextBehavior.cs @@ -1,4 +1,4 @@ -using System.Reactive.Disposables; +using System.Reactive.Disposables; using Avalonia.Controls; namespace WalletWasabi.Fluent.Behaviors; diff --git a/WalletWasabi.Fluent/Config.cs b/WalletWasabi.Fluent/Config.cs deleted file mode 100644 index d06f7ba8dd..0000000000 --- a/WalletWasabi.Fluent/Config.cs +++ /dev/null @@ -1,240 +0,0 @@ -using NBitcoin; -using Newtonsoft.Json; -using System.ComponentModel; -using System.Net; -using WalletWasabi.Bases; -using WalletWasabi.Exceptions; -using WalletWasabi.Helpers; -using WalletWasabi.JsonConverters; -using WalletWasabi.JsonConverters.Bitcoin; -using WalletWasabi.Models; - -namespace WalletWasabi.Fluent; - -[JsonObject(MemberSerialization.OptIn)] -public class Config : ConfigBase -{ - public const int DefaultJsonRpcServerPort = 37128; - public static readonly Money DefaultDustThreshold = Money.Coins(Constants.DefaultDustThreshold); - - private Uri? _backendUri; - private Uri? _coordinatorUri; - - /// - /// Constructor for config population using Newtonsoft.JSON. - /// - public Config() : base() - { - ServiceConfiguration = null!; - } - - public Config(string filePath) : base(filePath) - { - ServiceConfiguration = new ServiceConfiguration(GetBitcoinP2pEndPoint(), DustThreshold); - } - - [JsonProperty(PropertyName = "Network")] - [JsonConverter(typeof(NetworkJsonConverter))] - public Network Network { get; internal set; } = Network.Main; - - [DefaultValue("https://api.wasabiwallet.io/")] - [JsonProperty(PropertyName = "MainNetBackendUri", DefaultValueHandling = DefaultValueHandling.Populate)] - public string MainNetBackendUri { get; private set; } = "https://api.wasabiwallet.io/"; - - [DefaultValue("https://api.wasabiwallet.co/")] - [JsonProperty(PropertyName = "TestNetClearnetBackendUri", DefaultValueHandling = DefaultValueHandling.Populate)] - public string TestNetBackendUri { get; private set; } = "https://api.wasabiwallet.co/"; - - [DefaultValue("http://localhost:37127/")] - [JsonProperty(PropertyName = "RegTestBackendUri", DefaultValueHandling = DefaultValueHandling.Populate)] - public string RegTestBackendUri { get; private set; } = "http://localhost:37127/"; - - [JsonProperty(PropertyName = "MainNetCoordinatorUri", DefaultValueHandling = DefaultValueHandling.Ignore)] - public string? MainNetCoordinatorUri { get; private set; } - - [JsonProperty(PropertyName = "TestNetCoordinatorUri", DefaultValueHandling = DefaultValueHandling.Ignore)] - public string? TestNetCoordinatorUri { get; private set; } - - [JsonProperty(PropertyName = "RegTestCoordinatorUri", DefaultValueHandling = DefaultValueHandling.Ignore)] - public string? RegTestCoordinatorUri { get; private set; } - - [DefaultValue(true)] - [JsonProperty(PropertyName = "UseTor", DefaultValueHandling = DefaultValueHandling.Populate)] - public bool UseTor { get; internal set; } = true; - - [DefaultValue(false)] - [JsonProperty(PropertyName = "TerminateTorOnExit", DefaultValueHandling = DefaultValueHandling.Populate)] - public bool TerminateTorOnExit { get; internal set; } = false; - - [DefaultValue(true)] - [JsonProperty(PropertyName = "DownloadNewVersion", DefaultValueHandling = DefaultValueHandling.Populate)] - public bool DownloadNewVersion { get; internal set; } = true; - - [DefaultValue(false)] - [JsonProperty(PropertyName = "StartLocalBitcoinCoreOnStartup", DefaultValueHandling = DefaultValueHandling.Populate)] - public bool StartLocalBitcoinCoreOnStartup { get; internal set; } = false; - - [DefaultValue(true)] - [JsonProperty(PropertyName = "StopLocalBitcoinCoreOnShutdown", DefaultValueHandling = DefaultValueHandling.Populate)] - public bool StopLocalBitcoinCoreOnShutdown { get; internal set; } = true; - - [JsonProperty(PropertyName = "LocalBitcoinCoreDataDir")] - public string LocalBitcoinCoreDataDir { get; internal set; } = EnvironmentHelpers.GetDefaultBitcoinCoreDataDirOrEmptyString(); - - [JsonProperty(PropertyName = "MainNetBitcoinP2pEndPoint")] - [JsonConverter(typeof(EndPointJsonConverter), Constants.DefaultMainNetBitcoinP2pPort)] - public EndPoint MainNetBitcoinP2pEndPoint { get; internal set; } = new IPEndPoint(IPAddress.Loopback, Constants.DefaultMainNetBitcoinP2pPort); - - [JsonProperty(PropertyName = "TestNetBitcoinP2pEndPoint")] - [JsonConverter(typeof(EndPointJsonConverter), Constants.DefaultTestNetBitcoinP2pPort)] - public EndPoint TestNetBitcoinP2pEndPoint { get; internal set; } = new IPEndPoint(IPAddress.Loopback, Constants.DefaultTestNetBitcoinP2pPort); - - [JsonProperty(PropertyName = "RegTestBitcoinP2pEndPoint")] - [JsonConverter(typeof(EndPointJsonConverter), Constants.DefaultRegTestBitcoinP2pPort)] - public EndPoint RegTestBitcoinP2pEndPoint { get; internal set; } = new IPEndPoint(IPAddress.Loopback, Constants.DefaultRegTestBitcoinP2pPort); - - [DefaultValue(false)] - [JsonProperty(PropertyName = "JsonRpcServerEnabled", DefaultValueHandling = DefaultValueHandling.Populate)] - public bool JsonRpcServerEnabled { get; internal set; } - - [DefaultValue("")] - [JsonProperty(PropertyName = "JsonRpcUser", DefaultValueHandling = DefaultValueHandling.Populate)] - public string JsonRpcUser { get; internal set; } = ""; - - [DefaultValue("")] - [JsonProperty(PropertyName = "JsonRpcPassword", DefaultValueHandling = DefaultValueHandling.Populate)] - public string JsonRpcPassword { get; internal set; } = ""; - - [JsonProperty(PropertyName = "JsonRpcServerPrefixes")] - public string[] JsonRpcServerPrefixes { get; internal set; } = new[] - { - "http://127.0.0.1:37128/", - "http://localhost:37128/" - }; - - [JsonProperty(PropertyName = "DustThreshold")] - [JsonConverter(typeof(MoneyBtcJsonConverter))] - public Money DustThreshold { get; internal set; } = DefaultDustThreshold; - - [JsonProperty(PropertyName = "EnableGpu")] - public bool EnableGpu { get; internal set; } = true; - - [DefaultValue("CoinJoinCoordinatorIdentifier")] - [JsonProperty(PropertyName = "CoordinatorIdentifier", DefaultValueHandling = DefaultValueHandling.Populate)] - public string CoordinatorIdentifier { get; set; } = "CoinJoinCoordinatorIdentifier"; - - public ServiceConfiguration ServiceConfiguration { get; private set; } - - public Uri GetBackendUri() - { - if (_backendUri is { }) - { - return _backendUri; - } - - if (Network == Network.Main) - { - _backendUri = new Uri(MainNetBackendUri); - } - else if (Network == Network.TestNet) - { - _backendUri = new Uri(TestNetBackendUri); - } - else if (Network == Network.RegTest) - { - _backendUri = new Uri(RegTestBackendUri); - } - else - { - throw new NotSupportedNetworkException(Network); - } - - return _backendUri; - } - - public Uri GetCoordinatorUri() - { - if (_coordinatorUri is { }) - { - return _coordinatorUri; - } - - var result = Network switch - { - { } n when n == Network.Main => MainNetCoordinatorUri, - { } n when n == Network.TestNet => TestNetCoordinatorUri, - { } n when n == Network.RegTest => RegTestCoordinatorUri, - _ => throw new NotSupportedNetworkException(Network) - }; - - _coordinatorUri = result is null ? GetBackendUri() : new Uri(result); - return _coordinatorUri; - } - - public EndPoint GetBitcoinP2pEndPoint() - { - if (Network == Network.Main) - { - return MainNetBitcoinP2pEndPoint; - } - else if (Network == Network.TestNet) - { - return TestNetBitcoinP2pEndPoint; - } - else if (Network == Network.RegTest) - { - return RegTestBitcoinP2pEndPoint; - } - else - { - throw new NotSupportedNetworkException(Network); - } - } - - public void SetBitcoinP2pEndpoint(EndPoint endPoint) - { - if (Network == Network.Main) - { - MainNetBitcoinP2pEndPoint = endPoint; - } - else if (Network == Network.TestNet) - { - TestNetBitcoinP2pEndPoint = endPoint; - } - else if (Network == Network.RegTest) - { - RegTestBitcoinP2pEndPoint = endPoint; - } - else - { - throw new NotSupportedNetworkException(Network); - } - } - - /// - public override void LoadFile(bool createIfMissing = false) - { - base.LoadFile(createIfMissing); - - ServiceConfiguration = new ServiceConfiguration(GetBitcoinP2pEndPoint(), DustThreshold); - } - - public bool MigrateOldDefaultBackendUris() - { - bool hasChanged = false; - - if (MainNetBackendUri == "https://wasabiwallet.io/") - { - MainNetBackendUri = "https://api.wasabiwallet.io/"; - hasChanged = true; - } - - if (TestNetBackendUri == "https://wasabiwallet.co/") - { - TestNetBackendUri = "https://api.wasabiwallet.co/"; - hasChanged = true; - } - - return hasChanged; - } -} diff --git a/WalletWasabi.Fluent/Controls/AdorningContentControl.axaml b/WalletWasabi.Fluent/Controls/AdorningContentControl.axaml index 1ff758bc27..e73212038c 100644 --- a/WalletWasabi.Fluent/Controls/AdorningContentControl.axaml +++ b/WalletWasabi.Fluent/Controls/AdorningContentControl.axaml @@ -1,11 +1,11 @@ - + - - + + + diff --git a/WalletWasabi.Fluent/Controls/AdorningContentControl.axaml.cs b/WalletWasabi.Fluent/Controls/AdorningContentControl.axaml.cs index 8a4d929299..9cfcf946ab 100644 --- a/WalletWasabi.Fluent/Controls/AdorningContentControl.axaml.cs +++ b/WalletWasabi.Fluent/Controls/AdorningContentControl.axaml.cs @@ -1,7 +1,7 @@ using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.Primitives; using Avalonia.Threading; -using WalletWasabi.Fluent.Helpers; namespace WalletWasabi.Fluent.Controls; @@ -42,7 +42,7 @@ public bool IsAdornmentVisible set => SetValue(IsAdornmentVisibleProperty, value); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); @@ -52,18 +52,11 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs } else if (change.Property == AdornmentProperty) { - InvalidateAdornmentVisible(change.OldValue.GetValueOrDefault(), change.NewValue.GetValueOrDefault()); + InvalidateAdornmentVisible(change.GetOldValue(), change.GetNewValue()); } else if (change.Property == IsAdornmentVisibleProperty) { - if (change.NewValue.GetValueOrDefault()) - { - AdornerHelper.AddAdorner(this, Adornment); - } - else - { - AdornerHelper.RemoveAdorner(this, Adornment); - } + AdornerLayer.SetAdorner(this, change.GetNewValue() ? Adornment : null); } } @@ -91,7 +84,7 @@ private void InvalidateAdornmentVisible(Control? oldValue, Control? newValue) if (oldValue is { }) { - AdornerHelper.RemoveAdorner(this, oldValue); + AdornerLayer.SetAdorner(this, null); } if (newValue is { }) @@ -101,7 +94,7 @@ private void InvalidateAdornmentVisible(Control? oldValue, Control? newValue) if (IsAdornmentVisible) { - AdornerHelper.AddAdorner(this, Adornment); + AdornerLayer.SetAdorner(this, Adornment); } } } diff --git a/WalletWasabi.Fluent/Controls/AmountControl.axaml b/WalletWasabi.Fluent/Controls/AmountControl.axaml new file mode 100644 index 0000000000..740849f338 --- /dev/null +++ b/WalletWasabi.Fluent/Controls/AmountControl.axaml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/WalletWasabi.Fluent/Controls/AmountControl.axaml.cs b/WalletWasabi.Fluent/Controls/AmountControl.axaml.cs new file mode 100644 index 0000000000..b9bf8de83d --- /dev/null +++ b/WalletWasabi.Fluent/Controls/AmountControl.axaml.cs @@ -0,0 +1,16 @@ +using Avalonia; +using Avalonia.Controls.Primitives; +using WalletWasabi.Fluent.Models.Wallets; + +namespace WalletWasabi.Fluent.Controls; + +public class AmountControl : TemplatedControl +{ + public static readonly StyledProperty AmountProperty = AvaloniaProperty.Register(nameof(Amount)); + + public Amount Amount + { + get => GetValue(AmountProperty); + set => SetValue(AmountProperty, value); + } +} diff --git a/WalletWasabi.Fluent/Controls/AnimatedButton.axaml b/WalletWasabi.Fluent/Controls/AnimatedButton.axaml index 39c261bf28..422d9a45ca 100644 --- a/WalletWasabi.Fluent/Controls/AnimatedButton.axaml +++ b/WalletWasabi.Fluent/Controls/AnimatedButton.axaml @@ -1,61 +1,15 @@ - + + - - - - - - - + + + + + + + + + diff --git a/WalletWasabi.Fluent/Controls/AnimatedButton.axaml.cs b/WalletWasabi.Fluent/Controls/AnimatedButton.axaml.cs index 7f3be8f728..b7a0932a07 100644 --- a/WalletWasabi.Fluent/Controls/AnimatedButton.axaml.cs +++ b/WalletWasabi.Fluent/Controls/AnimatedButton.axaml.cs @@ -28,9 +28,6 @@ public class AnimatedButton : TemplatedControl public static readonly StyledProperty AnimateIconProperty = AvaloniaProperty.Register(nameof(AnimateIcon)); - public static readonly StyledProperty ExecuteOnOpenProperty = - AvaloniaProperty.Register(nameof(ExecuteOnOpen)); - static AnimatedButton() { AffectsRender(InitialOpacityProperty); @@ -77,22 +74,4 @@ public bool AnimateIcon get => GetValue(AnimateIconProperty); set => SetValue(AnimateIconProperty, value); } - - public bool ExecuteOnOpen - { - get => GetValue(ExecuteOnOpenProperty); - set => SetValue(ExecuteOnOpenProperty, value); - } - - protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) - { - base.OnAttachedToVisualTree(e); - - AnimateIcon = ExecuteOnOpen; - - if (ExecuteOnOpen) - { - Command.Execute(default); - } - } } diff --git a/WalletWasabi.Fluent/Controls/CaptionButtons.axaml b/WalletWasabi.Fluent/Controls/CaptionButtons.axaml new file mode 100644 index 0000000000..395ce02ba4 --- /dev/null +++ b/WalletWasabi.Fluent/Controls/CaptionButtons.axaml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WalletWasabi.Fluent/Controls/ClipboardCopyButton.axaml b/WalletWasabi.Fluent/Controls/ClipboardCopyButton.axaml index faabe9dff0..18b6f19612 100644 --- a/WalletWasabi.Fluent/Controls/ClipboardCopyButton.axaml +++ b/WalletWasabi.Fluent/Controls/ClipboardCopyButton.axaml @@ -1,11 +1,12 @@ - + - - + + + + diff --git a/WalletWasabi.Fluent/Controls/ClipboardCopyButton.axaml.cs b/WalletWasabi.Fluent/Controls/ClipboardCopyButton.axaml.cs index 6027032ae4..1f9d685215 100644 --- a/WalletWasabi.Fluent/Controls/ClipboardCopyButton.axaml.cs +++ b/WalletWasabi.Fluent/Controls/ClipboardCopyButton.axaml.cs @@ -3,6 +3,7 @@ using Avalonia; using Avalonia.Controls.Primitives; using ReactiveUI; +using WalletWasabi.Fluent.Helpers; namespace WalletWasabi.Fluent.Controls; @@ -14,6 +15,12 @@ public class ClipboardCopyButton : TemplatedControl public static readonly StyledProperty TextProperty = AvaloniaProperty.Register(nameof(Text)); + public ClipboardCopyButton() + { + var canCopy = this.WhenAnyValue(x => x.Text, selector: text => text is not null); + CopyCommand = ReactiveCommand.CreateFromTask(CopyToClipboardAsync, canCopy); + } + public ReactiveCommand CopyCommand { get => GetValue(CopyCommandProperty); @@ -26,15 +33,9 @@ public string Text set => SetValue(TextProperty, value); } - public ClipboardCopyButton() - { - var canCopy = this.WhenAnyValue(x => x.Text, selector: text => text is not null); - CopyCommand = ReactiveCommand.CreateFromTask(CopyToClipboardAsync, canCopy); - } - private async Task CopyToClipboardAsync() { - if (Application.Current is { Clipboard: { } clipboard }) + if (ApplicationHelper.Clipboard is { } clipboard) { await clipboard.SetTextAsync(Text); await Task.Delay(1000); // Introduces a delay while the animation is playing (1s). This will make the command 'busy' while being animated, avoiding reentrancy. diff --git a/WalletWasabi.Fluent/Controls/ConcatenatingWrapPanel.cs b/WalletWasabi.Fluent/Controls/ConcatenatingWrapPanel.cs index a521f8d610..54bc0e309a 100644 --- a/WalletWasabi.Fluent/Controls/ConcatenatingWrapPanel.cs +++ b/WalletWasabi.Fluent/Controls/ConcatenatingWrapPanel.cs @@ -85,7 +85,7 @@ IInputElement INavigableContainer.GetControl(NavigationDirection direction, IInp var orientation = Orientation; var children = Children.Concat(ConcatenatedChildren).ToList(); bool horiz = orientation == Orientation.Horizontal; - int index = Children.IndexOf((IControl)from); + int index = Children.IndexOf((Control)from); switch (direction) { diff --git a/WalletWasabi.Fluent/Controls/ContentArea.axaml b/WalletWasabi.Fluent/Controls/ContentArea.axaml index 8b8211ac9d..7079b44da6 100644 --- a/WalletWasabi.Fluent/Controls/ContentArea.axaml +++ b/WalletWasabi.Fluent/Controls/ContentArea.axaml @@ -1,8 +1,8 @@ - + - - - - - - - - - - - - - + + + + diff --git a/WalletWasabi.Fluent/Controls/ContentArea.axaml.cs b/WalletWasabi.Fluent/Controls/ContentArea.axaml.cs index a82d48a8e9..b260144d55 100644 --- a/WalletWasabi.Fluent/Controls/ContentArea.axaml.cs +++ b/WalletWasabi.Fluent/Controls/ContentArea.axaml.cs @@ -46,8 +46,8 @@ public class ContentArea : ContentControl public static readonly StyledProperty HeaderBackgroundProperty = AvaloniaProperty.Register(nameof(HeaderBackground)); - private IContentPresenter? _titlePresenter; - private IContentPresenter? _captionPresenter; + private ContentPresenter? _titlePresenter; + private ContentPresenter? _captionPresenter; public object Title { @@ -127,10 +127,15 @@ public IBrush HeaderBackground set => SetValue(HeaderBackgroundProperty, value); } - protected override bool RegisterContentPresenter(IContentPresenter presenter) + protected override bool RegisterContentPresenter(ContentPresenter presenter) { var result = base.RegisterContentPresenter(presenter); + if (presenter is not { } contentPresenter) + { + return result; + } + switch (presenter.Name) { case "PART_TitlePresenter": @@ -139,7 +144,7 @@ protected override bool RegisterContentPresenter(IContentPresenter presenter) _titlePresenter.PropertyChanged -= PresenterOnPropertyChanged; } - _titlePresenter = presenter; + _titlePresenter = contentPresenter; _titlePresenter.PropertyChanged += PresenterOnPropertyChanged; result = true; break; @@ -150,7 +155,7 @@ protected override bool RegisterContentPresenter(IContentPresenter presenter) _captionPresenter.PropertyChanged -= PresenterOnPropertyChanged; } - _captionPresenter = presenter; + _captionPresenter = contentPresenter; _captionPresenter.PropertyChanged += PresenterOnPropertyChanged; _captionPresenter.IsVisible = Caption is not null; result = true; @@ -166,12 +171,12 @@ private void PresenterOnPropertyChanged(object? sender, AvaloniaPropertyChangedE { var className = sender == _captionPresenter ? "caption" : "title"; - if (e.OldValue is IStyledElement oldValue) + if (e.OldValue is StyledElement oldValue) { oldValue.Classes.Remove(className); } - if (e.NewValue is IStyledElement newValue) + if (e.NewValue is StyledElement newValue) { newValue.Classes.Add(className); } diff --git a/WalletWasabi.Fluent/Controls/CopyableItem.axaml b/WalletWasabi.Fluent/Controls/CopyableItem.axaml index 51b8bb9ece..24aafe6041 100644 --- a/WalletWasabi.Fluent/Controls/CopyableItem.axaml +++ b/WalletWasabi.Fluent/Controls/CopyableItem.axaml @@ -1,7 +1,7 @@ - + @@ -10,8 +10,10 @@ - - + + + diff --git a/WalletWasabi.Fluent/Controls/CurrencyEntryBox.axaml b/WalletWasabi.Fluent/Controls/CurrencyEntryBox.axaml index f2bd83f624..2f89ca6298 100644 --- a/WalletWasabi.Fluent/Controls/CurrencyEntryBox.axaml +++ b/WalletWasabi.Fluent/Controls/CurrencyEntryBox.axaml @@ -1,26 +1,29 @@ - + - - 0,0,0,2 - 0,0,0,2 - 15,10,15,8 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WalletWasabi.Fluent/Controls/CurrencyEntryBox.axaml.cs b/WalletWasabi.Fluent/Controls/CurrencyEntryBox.axaml.cs index 7cb40c61a8..7e521e7ce5 100644 --- a/WalletWasabi.Fluent/Controls/CurrencyEntryBox.axaml.cs +++ b/WalletWasabi.Fluent/Controls/CurrencyEntryBox.axaml.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Text.RegularExpressions; @@ -8,7 +9,11 @@ using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Threading; +using NBitcoin; using ReactiveUI; +using WalletWasabi.Fluent.Extensions; +using WalletWasabi.Fluent.Helpers; +using WalletWasabi.Fluent.Infrastructure; using WalletWasabi.Helpers; using static WalletWasabi.Userfacing.CurrencyInput; @@ -34,9 +39,20 @@ public partial class CurrencyEntryBox : TextBox public static readonly StyledProperty MaxDecimalsProperty = AvaloniaProperty.Register(nameof(MaxDecimals), 8); + public static readonly StyledProperty BalanceBtcProperty = + AvaloniaProperty.Register(nameof(BalanceBtc)); + + public static readonly StyledProperty BalanceUsdProperty = + AvaloniaProperty.Register(nameof(BalanceUsd)); + + public static readonly StyledProperty ValidatePasteBalanceProperty = + AvaloniaProperty.Register(nameof(ValidatePasteBalance)); + + private static readonly string[] InvalidCharacters = new string[1] { "\u007f" }; + public CurrencyEntryBox() { - Text = string.Empty; + SetCurrentValue(TextProperty, string.Empty); PseudoClasses.Set(":noexchangerate", true); PseudoClasses.Set(":isrightside", false); @@ -85,6 +101,24 @@ public int MaxDecimals set => SetValue(MaxDecimalsProperty, value); } + public Money BalanceBtc + { + get => GetValue(BalanceBtcProperty); + set => SetValue(BalanceBtcProperty, value); + } + + public decimal BalanceUsd + { + get => GetValue(BalanceUsdProperty); + set => SetValue(BalanceUsdProperty, value); + } + + public bool ValidatePasteBalance + { + get => GetValue(ValidatePasteBalanceProperty); + set => SetValue(ValidatePasteBalanceProperty, value); + } + private decimal FiatToBitcoin(decimal fiatValue) { if (ConversionRate == 0m) @@ -99,14 +133,14 @@ protected override void OnGotFocus(GotFocusEventArgs e) { base.OnGotFocus(e); - CaretIndex = Text?.Length ?? 0; + SetCurrentValue(CaretIndexProperty, Text?.Length ?? 0); Dispatcher.UIThread.Post(SelectAll); } protected override void OnTextInput(TextInputEventArgs e) { - var input = e.Text ?? ""; + var input = e.Text == null ? "" : e.Text.TotalTrim(); // Reject space char input when there's no text. if (string.IsNullOrWhiteSpace(Text) && string.IsNullOrWhiteSpace(input)) @@ -118,28 +152,29 @@ protected override void OnTextInput(TextInputEventArgs e) if (IsReplacingWithImplicitDecimal(input)) { - ReplaceCurrentTextWithLeadingZero(e); - - base.OnTextInput(e); + var result = ReplaceCurrentTextWithLeadingZero(e); + base.OnTextInput(result); return; } if (IsInsertingImplicitDecimal(input)) { - InsertLeadingZeroForDecimal(e); - - base.OnTextInput(e); + var result = InsertLeadingZeroForDecimal(e); + base.OnTextInput(result); return; } var preComposedText = PreComposeText(input); - decimal fiatValue = 0; + var isValid = ValidateEntryText(preComposedText); + + preComposedText = preComposedText.TotalTrim(); + + var parsed = decimal.TryParse(preComposedText, NumberStyles.Number, InvariantNumberFormat, out var fiatValue); - e.Handled = !(ValidateEntryText(preComposedText) && - decimal.TryParse(preComposedText, NumberStyles.Number, InvariantNumberFormat, out fiatValue)); + e.Handled = !(isValid && parsed); - if (IsFiat & !e.Handled) + if (IsFiat && !e.Handled) { e.Handled = FiatToBitcoin(fiatValue) >= Constants.MaximumNumberOfBitcoins; } @@ -157,21 +192,21 @@ private bool IsInsertingImplicitDecimal(string input) return input.StartsWith(".") && CaretIndex == 0 && Text is not null && !Text.Contains('.'); } - private void ReplaceCurrentTextWithLeadingZero(TextInputEventArgs e) + private TextInputEventArgs ReplaceCurrentTextWithLeadingZero(TextInputEventArgs e) { var finalText = "0" + e.Text; - Text = ""; - e.Text = finalText; - CaretIndex = finalText.Length; + SetCurrentValue(TextProperty, ""); + SetCurrentValue(CaretIndexProperty, finalText.Length); ClearSelection(); + return new TextInputEventArgs { Text = finalText }; } - private void InsertLeadingZeroForDecimal(TextInputEventArgs e) + private TextInputEventArgs InsertLeadingZeroForDecimal(TextInputEventArgs e) { var prependText = "0" + e.Text; - Text = Text.Insert(0, prependText); - e.Text = ""; - CaretIndex += prependText.Length; + SetCurrentValue(TextProperty, Text.Insert(0, prependText)); + SetCurrentValue(CaretIndexProperty, CaretIndex + prependText.Length); + return new TextInputEventArgs { Text = "" }; } [GeneratedRegex($"^(?[0-9{GroupSeparator}]*)(\\{DecimalSeparator}?(?[0-9{GroupSeparator}]*))$")] @@ -254,7 +289,7 @@ protected override void OnKeyDown(KeyEventArgs e) private void DoPasteCheck(KeyEventArgs e) { - var keymap = AvaloniaLocator.Current.GetService(); + var keymap = Application.Current?.PlatformSettings?.HotkeyConfiguration; bool Match(IEnumerable gestures) => gestures.Any(g => g.Matches(e)); @@ -270,7 +305,7 @@ private void DoPasteCheck(KeyEventArgs e) public async void ModifiedPasteAsync() { - if (AvaloniaLocator.Current.GetService() is { } clipboard) + if (ApplicationHelper.Clipboard is { } clipboard) { var text = await clipboard.GetTextAsync(); @@ -281,14 +316,9 @@ public async void ModifiedPasteAsync() text = text.Replace("\r", "").Replace("\n", "").Trim(); - // Based on broad M0 money supply figures (80 900 000 000 000.00 USD). - // so USD has 14 whole places + the decimal point + 2 decimal places = 17 characters. - // Bitcoin has "21 000 000 . 0000 0000". - // Coincidentally the same character count as USD... weird. - // Plus adding 4 characters for the group separators. - if (text.Length > 17 + 4) + if (!TryParse(text, out text)) { - text = text[..(17 + 4)]; + return; } if (ValidateEntryText(text)) @@ -298,9 +328,48 @@ public async void ModifiedPasteAsync() } } + private bool TryParse(string text, [NotNullWhen(true)] out string? result) + { + var money = ValidatePasteBalance + ? ClipboardObserver.ParseToMoney(text, BalanceBtc) + : ClipboardObserver.ParseToMoney(text); + if (money is not null) + { + result = money.ToDecimal(MoneyUnit.BTC).FormattedBtc(); + return true; + } + + var usd = ValidatePasteBalance + ? ClipboardObserver.ParseToUsd(text, BalanceUsd) + : ClipboardObserver.ParseToUsd(text); + if (usd is not null) + { + result = usd.Value.ToString("0.00"); + return true; + } + + result = null; + return false; + } + // Pre-composes the TextInputEventArgs to see the potential Text that is to // be committed to the TextPresenter in this control. + private string? RemoveInvalidCharacters(string? text) + { + if (text is null) + { + return null; + } + + for (var i = 0; i < InvalidCharacters.Length; i++) + { + text = text.Replace(InvalidCharacters[i], string.Empty); + } + + return text; + } + // An event in Avalonia's TextBox with this function should be implemented there for brevity. private string PreComposeText(string input) { @@ -328,21 +397,21 @@ private string PreComposeText(string input) return ""; } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == IsReadOnlyProperty) { - PseudoClasses.Set(":readonly", change.NewValue.GetValueOrDefault()); + PseudoClasses.Set(":readonly", change.GetNewValue()); } else if (change.Property == ConversionRateProperty) { - PseudoClasses.Set(":noexchangerate", change.NewValue.GetValueOrDefault() == 0m); + PseudoClasses.Set(":noexchangerate", change.GetNewValue() == 0m); } else if (change.Property == IsFiatProperty) { - PseudoClasses.Set(":isfiat", change.NewValue.GetValueOrDefault()); + PseudoClasses.Set(":isfiat", change.GetNewValue()); } } } diff --git a/WalletWasabi.Fluent/Controls/Dialog.axaml b/WalletWasabi.Fluent/Controls/Dialog.axaml index aaea5e5dae..8dbdedfbfe 100644 --- a/WalletWasabi.Fluent/Controls/Dialog.axaml +++ b/WalletWasabi.Fluent/Controls/Dialog.axaml @@ -1,14 +1,16 @@ - - - - - - - + + + + + + - + + + - + + + - - - + + - - - + + - - + + - - + + + + + - - + - - - - - - + diff --git a/WalletWasabi.Fluent/Controls/Dialog.axaml.cs b/WalletWasabi.Fluent/Controls/Dialog.axaml.cs index d273ac186c..a2afb5a66f 100644 --- a/WalletWasabi.Fluent/Controls/Dialog.axaml.cs +++ b/WalletWasabi.Fluent/Controls/Dialog.axaml.cs @@ -278,13 +278,13 @@ private void HandleDialogFocus(bool isOpen) } } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == IsDialogOpenProperty) { - var isOpen = change.NewValue.GetValueOrDefault(); + var isOpen = change.GetNewValue(); PseudoClasses.Set(":open", isOpen); @@ -298,12 +298,12 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs if (change.Property == IsBusyProperty) { - PseudoClasses.Set(":busy", change.NewValue.GetValueOrDefault()); + PseudoClasses.Set(":busy", change.GetNewValue()); } if (change.Property == ShowAlertProperty) { - PseudoClasses.Set(":alert", change.NewValue.GetValueOrDefault()); + PseudoClasses.Set(":alert", change.GetNewValue()); } } diff --git a/WalletWasabi.Fluent/Controls/DualCurrencyEntryBox.axaml b/WalletWasabi.Fluent/Controls/DualCurrencyEntryBox.axaml index 56753975af..3baae1eb47 100644 --- a/WalletWasabi.Fluent/Controls/DualCurrencyEntryBox.axaml +++ b/WalletWasabi.Fluent/Controls/DualCurrencyEntryBox.axaml @@ -1,26 +1,30 @@ - - - 0,0,0,2 - 0,0,0,2 - 15,10,15,8 - - - + @@ -67,7 +79,10 @@ Grid.Column="{TemplateBinding RightColumn}" Background="Transparent" BorderThickness="0" - IsRightSide="{TemplateBinding IsConversionReversed, Converter={x:Static BoolConverters.Not}}" /> + IsRightSide="{TemplateBinding IsConversionReversed, Converter={x:Static BoolConverters.Not}}" + BalanceBtc="{TemplateBinding BalanceBtc}" + BalanceUsd="{TemplateBinding BalanceUsd}" + ValidatePasteBalance="{TemplateBinding ValidatePasteBalance}" /> @@ -75,134 +90,131 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WalletWasabi.Fluent/Controls/DualCurrencyEntryBox.axaml.cs b/WalletWasabi.Fluent/Controls/DualCurrencyEntryBox.axaml.cs index 539d9e29b4..ed9a398a46 100644 --- a/WalletWasabi.Fluent/Controls/DualCurrencyEntryBox.axaml.cs +++ b/WalletWasabi.Fluent/Controls/DualCurrencyEntryBox.axaml.cs @@ -5,15 +5,24 @@ using Avalonia.Controls.Primitives; using Avalonia.Data; using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Threading; using Avalonia.VisualTree; +using NBitcoin; using WalletWasabi.Fluent.Extensions; using WalletWasabi.Helpers; using WalletWasabi.Userfacing; namespace WalletWasabi.Fluent.Controls; -public class DualCurrencyEntryBox : UserControl +public class DualCurrencyEntryBox : TemplatedControl { + public static readonly StyledProperty HorizontalContentAlignmentProperty = + AvaloniaProperty.Register(nameof(HorizontalContentAlignment)); + + public static readonly StyledProperty VerticalContentAlignmentProperty = + AvaloniaProperty.Register(nameof(VerticalContentAlignment)); + public static readonly DirectProperty AmountBtcProperty = AvaloniaProperty.RegisterDirect( nameof(AmountBtc), @@ -22,14 +31,14 @@ public class DualCurrencyEntryBox : UserControl enableDataValidation: true, defaultBindingMode: BindingMode.TwoWay); - public static readonly StyledProperty TextProperty = - AvaloniaProperty.Register(nameof(Text)); + public static readonly StyledProperty TextProperty = + AvaloniaProperty.Register(nameof(Text)); public static readonly StyledProperty WatermarkProperty = AvaloniaProperty.Register(nameof(Watermark)); - public static readonly StyledProperty ConversionTextProperty = - AvaloniaProperty.Register(nameof(ConversionText)); + public static readonly StyledProperty ConversionTextProperty = + AvaloniaProperty.Register(nameof(ConversionText)); public static readonly StyledProperty ConversionRateProperty = AvaloniaProperty.Register(nameof(ConversionRate)); @@ -64,6 +73,15 @@ public class DualCurrencyEntryBox : UserControl public static readonly StyledProperty LeftEntryBoxProperty = AvaloniaProperty.Register(nameof(LeftEntryBox)); + public static readonly StyledProperty BalanceBtcProperty = + AvaloniaProperty.Register(nameof(BalanceBtc)); + + public static readonly StyledProperty BalanceUsdProperty = + AvaloniaProperty.Register(nameof(BalanceUsd)); + + public static readonly StyledProperty ValidatePasteBalanceProperty = + AvaloniaProperty.Register(nameof(ValidatePasteBalance)); + private CompositeDisposable? _disposable; private Button? _swapButton; private decimal _amountBtc; @@ -84,13 +102,25 @@ public DualCurrencyEntryBox() PseudoClasses.Set(":noexchangerate", true); } + public HorizontalAlignment HorizontalContentAlignment + { + get { return GetValue(HorizontalContentAlignmentProperty); } + set { SetValue(HorizontalContentAlignmentProperty, value); } + } + + public VerticalAlignment VerticalContentAlignment + { + get { return GetValue(VerticalContentAlignmentProperty); } + set { SetValue(VerticalContentAlignmentProperty, value); } + } + public decimal AmountBtc { get => _amountBtc; set => SetAndRaise(AmountBtcProperty, ref _amountBtc, value); } - public string Text + public string? Text { get => GetValue(TextProperty); set => SetValue(TextProperty, value); @@ -102,7 +132,7 @@ public string Watermark set => SetValue(WatermarkProperty, value); } - public string ConversionText + public string? ConversionText { get => GetValue(ConversionTextProperty); set => SetValue(ConversionTextProperty, value); @@ -174,6 +204,24 @@ public CurrencyEntryBox? LeftEntryBox set => SetValue(LeftEntryBoxProperty, value); } + public Money BalanceBtc + { + get => GetValue(BalanceBtcProperty); + set => SetValue(BalanceBtcProperty, value); + } + + public decimal BalanceUsd + { + get => GetValue(BalanceUsdProperty); + set => SetValue(BalanceUsdProperty, value); + } + + public bool ValidatePasteBalance + { + get => GetValue(ValidatePasteBalanceProperty); + set => SetValue(ValidatePasteBalanceProperty, value); + } + protected override void OnLostFocus(RoutedEventArgs e) { base.OnLostFocus(e); @@ -181,7 +229,7 @@ protected override void OnLostFocus(RoutedEventArgs e) UpdateDisplay(true); } - private void InputText(string text) + private void InputText(string? text) { if (!_canUpdateDisplay) { @@ -199,7 +247,7 @@ private void InputText(string text) } } - private void InputConversionText(string text) + private void InputConversionText(string? text) { if (!_canUpdateDisplay) { @@ -219,7 +267,7 @@ private void InputConversionText(string text) private void InputBtcValue(decimal value) { - AmountBtc = value; + SetCurrentValue(AmountBtcProperty, value); } private void InputBtcString(string value) @@ -259,7 +307,13 @@ private void UpdateDisplay(bool updateTextField) if (updateTextField) { _canUpdateDisplay = false; - Text = AmountBtc > 0 ? AmountBtc.FormattedBtc() : string.Empty; + + var oldText = LeftEntryBox?.Text; + var text = AmountBtc > 0 ? AmountBtc.FormattedBtc() : string.Empty; + SetCurrentValue(TextProperty, text); + + // TODO: Maintain CaretIndex properly. + SetCaretIndex(LeftEntryBox, text, oldText); _canUpdateDisplay = true; } @@ -278,17 +332,33 @@ private void UpdateDisplayFiat(bool updateTextField) var conversion = BitcoinToFiat(AmountBtc); - IsConversionApproximate = AmountBtc > 0; - ConversionWatermark = FullFormatFiat(0, ConversionCurrencyCode, true); + SetCurrentValue(IsConversionApproximateProperty, AmountBtc > 0); + SetCurrentValue(ConversionWatermarkProperty, FullFormatFiat(0, ConversionCurrencyCode, true)); if (updateTextField) { - ConversionText = AmountBtc > 0 ? conversion.FormattedFiat() : string.Empty; + var oldText = RightEntryBox?.Text; + var text = AmountBtc > 0 ? conversion.FormattedFiat() : string.Empty; + SetCurrentValue(ConversionTextProperty, text); + + // TODO: Maintain CaretIndex properly. + SetCaretIndex(RightEntryBox, text, oldText); } _canUpdateFiat = true; } + private void SetCaretIndex(CurrencyEntryBox? entryBox, string newText, string? oldText) + { + if (entryBox is not null) + { + var oldTextLength = oldText?.Length ?? 0; + var newTextLength = newText.Length; + var newCaretIndex = entryBox.CaretIndex + (newTextLength - oldTextLength); + Dispatcher.UIThread.Post(() => entryBox?.SetCurrentValue(TextBox.CaretIndexProperty, newCaretIndex + 1)); + } + } + private decimal FiatToBitcoin(decimal fiatValue) { return fiatValue / ConversionRate; @@ -338,7 +408,7 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) private void SwapButtonOnClick(object? sender, RoutedEventArgs e) { - IsConversionReversed = !IsConversionReversed; + SetCurrentValue(IsConversionReversedProperty, !IsConversionReversed); FocusOnLeftEntryBox(); } @@ -352,29 +422,29 @@ private void FocusOnLeftEntryBox() focusOn?.Focus(); } - protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value) + protected override void UpdateDataValidation(AvaloniaProperty property, BindingValueType state, Exception? error) { if (property == AmountBtcProperty) { - DataValidationErrors.SetError(this, value.Error); + DataValidationErrors.SetError(this, error); } } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == IsReadOnlyProperty) { - PseudoClasses.Set(":readonly", change.NewValue.GetValueOrDefault()); + PseudoClasses.Set(":readonly", change.GetNewValue()); } else if (change.Property == ConversionRateProperty) { - PseudoClasses.Set(":noexchangerate", change.NewValue.GetValueOrDefault() == 0m); + PseudoClasses.Set(":noexchangerate", change.GetNewValue() == 0m); } else if (change.Property == IsConversionReversedProperty) { - PseudoClasses.Set(":reversed", change.NewValue.GetValueOrDefault()); + PseudoClasses.Set(":reversed", change.GetNewValue()); ReorganizeVisuals(); UpdateDisplay(false); } diff --git a/WalletWasabi.Fluent/Controls/FadeOutTextBlock.cs b/WalletWasabi.Fluent/Controls/FadeOutTextBlock.cs index 7d3a15e582..645515f7a2 100644 --- a/WalletWasabi.Fluent/Controls/FadeOutTextBlock.cs +++ b/WalletWasabi.Fluent/Controls/FadeOutTextBlock.cs @@ -1,26 +1,11 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Media; -using Avalonia.Media.TextFormatting; -using Avalonia.Styling; -using System.Reactive.Disposables; namespace WalletWasabi.Fluent.Controls; -public class FadeOutTextBlock : TextBlock, IStyleable +public class FadeOutTextBlock : TextBlock { - private TextLayout? _trimmedLayout; - private bool _cutOff; - private TextLayout? _noTrimLayout; - - public FadeOutTextBlock() - { - AffectsMeasure(TextProperty); - TextWrapping = TextWrapping.NoWrap; - } - - public Type StyleKey { get; } = typeof(TextBlock); - private static readonly IBrush FadeoutOpacityMask = new LinearGradientBrush { StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative), @@ -33,104 +18,38 @@ public FadeOutTextBlock() } }.ToImmutable(); - public override void Render(DrawingContext context) + private static readonly IBrush OpacityMask = new LinearGradientBrush { - var background = Background; - - var bounds = Bounds; - - if (background != null) - { - context.FillRectangle(background, Bounds); - } - - if (_trimmedLayout is null || _noTrimLayout is null) - { - return; - } - - var width = bounds.Size.Width; - - var centerOffset = TextAlignment switch - { - TextAlignment.Center => (width - _trimmedLayout.Size.Width) / 2.0, - TextAlignment.Right => width - _trimmedLayout.Size.Width, - _ => 0.0 - }; - - var (left, yPosition, _, _) = Padding; - - using var a = - context.PushPostTransform(Matrix.CreateTranslation(left + centerOffset, yPosition)); - using var b = _cutOff ? context.PushOpacityMask(FadeoutOpacityMask, Bounds) : Disposable.Empty; - _noTrimLayout.Draw(context); - } - - private void NewCreateTextLayout(Size constraint, string? text) - { - if (constraint == Size.Empty) + StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative), + EndPoint = new RelativePoint(1, 0, RelativeUnit.Relative), + GradientStops = { - _trimmedLayout = null; + new GradientStop { Color = Colors.White, Offset = 0 }, + new GradientStop { Color = Colors.White, Offset = 1 }, } + }.ToImmutable(); - var text1 = text ?? ""; - var typeface = new Typeface(FontFamily, FontStyle, FontWeight); - var fontSize = FontSize; - var foreground = Foreground; - var textAlignment = TextAlignment; - var textWrapping = TextWrapping; - var textDecorations = TextDecorations; - var width = constraint.Width; - var height = constraint.Height; - var lineHeight = LineHeight; - - _noTrimLayout = new TextLayout( - text1, - typeface, - fontSize, - foreground, - textAlignment, - textWrapping, - TextTrimming.None, - textDecorations, - width, - height, - lineHeight, - 1); - - _trimmedLayout = new TextLayout( - text1, - typeface, - fontSize, - foreground, - textAlignment, - textWrapping, - TextTrimming.CharacterEllipsis, - textDecorations, - width, - height, - lineHeight, - 1); - - _cutOff = _trimmedLayout.TextLines[0].HasCollapsed; - } + internal TextBlock? TrimmedTextBlock { get; set; } - protected override Size MeasureOverride(Size availableSize) + protected override void RenderTextLayout(DrawingContext context, Point origin) { - if (string.IsNullOrEmpty(Text)) + if (TrimmedTextBlock is null) { - return new Size(); + base.RenderTextLayout(context, origin); } - - var padding = Padding; - - availableSize = availableSize.Deflate(padding); - - if (availableSize != _noTrimLayout?.Size) + else { - NewCreateTextLayout(availableSize, Text); + var hasCollapsed = TrimmedTextBlock.TextLayout.TextLines[0].HasCollapsed; + if (hasCollapsed) + { + using var _ = context.PushOpacityMask(FadeoutOpacityMask, Bounds); + TextLayout.Draw(context, origin + new Point(TextLayout.OverhangLeading, 0)); + } + else + { + using var _ = context.PushOpacityMask(OpacityMask, Bounds); + TextLayout.Draw(context, origin + new Point(TextLayout.OverhangLeading, 0)); + } } - - return (_trimmedLayout?.Size ?? Size.Empty).Inflate(padding); } } diff --git a/WalletWasabi.Fluent/Controls/FadeOutTextControl.axaml b/WalletWasabi.Fluent/Controls/FadeOutTextControl.axaml new file mode 100644 index 0000000000..0f13072556 --- /dev/null +++ b/WalletWasabi.Fluent/Controls/FadeOutTextControl.axaml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WalletWasabi.Fluent/Controls/FadeOutTextControl.axaml.cs b/WalletWasabi.Fluent/Controls/FadeOutTextControl.axaml.cs new file mode 100644 index 0000000000..d528987b63 --- /dev/null +++ b/WalletWasabi.Fluent/Controls/FadeOutTextControl.axaml.cs @@ -0,0 +1,36 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives; + +namespace WalletWasabi.Fluent.Controls; + +[TemplatePart("PART_TrimmedTextBlock", typeof(TextBlock))] +[TemplatePart("PART_NoTrimTextBlock", typeof(FadeOutTextBlock))] +public class FadeOutTextControl : TemplatedControl +{ + public static readonly StyledProperty TextProperty = + AvaloniaProperty.Register(nameof(Text)); + + private TextBlock? _trimmedTextBlock; + private FadeOutTextBlock? _noTrimTextBlock; + + public string? Text + { + get => GetValue(TextProperty); + set => SetValue(TextProperty, value); + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + + _trimmedTextBlock = e.NameScope.Find("PART_TrimmedTextBlock"); + _noTrimTextBlock = e.NameScope.Find("PART_NoTrimTextBlock"); + + if (_trimmedTextBlock is not null && _noTrimTextBlock is not null) + { + _noTrimTextBlock.TrimmedTextBlock = _trimmedTextBlock; + } + } +} diff --git a/WalletWasabi.Fluent/Controls/HistoryPlaceholderPanel.axaml b/WalletWasabi.Fluent/Controls/HistoryPlaceholderPanel.axaml index 03069df86e..51bc5cbb2b 100644 --- a/WalletWasabi.Fluent/Controls/HistoryPlaceholderPanel.axaml +++ b/WalletWasabi.Fluent/Controls/HistoryPlaceholderPanel.axaml @@ -1,8 +1,9 @@ - + + + - - + + + diff --git a/WalletWasabi.Fluent/Controls/HistoryPlaceholderPanel.axaml.cs b/WalletWasabi.Fluent/Controls/HistoryPlaceholderPanel.axaml.cs index 3eac12494c..487eeee86f 100644 --- a/WalletWasabi.Fluent/Controls/HistoryPlaceholderPanel.axaml.cs +++ b/WalletWasabi.Fluent/Controls/HistoryPlaceholderPanel.axaml.cs @@ -5,7 +5,7 @@ namespace WalletWasabi.Fluent.Controls; -public class HistoryPlaceholderPanel : ContentControl +public class HistoryPlaceholderPanel : TemplatedControl { private ItemsControl? _targetItemsControl; @@ -29,7 +29,7 @@ protected override Size MeasureOverride(Size availableSize) var deltaOpacity = 1d / totalRows; - _targetItemsControl.Items = + _targetItemsControl.ItemsSource = Enumerable .Range(1, totalRows) .Reverse() diff --git a/WalletWasabi.Fluent/Controls/InfoMessage.axaml b/WalletWasabi.Fluent/Controls/InfoMessage.axaml index 96f9212153..7d4359502e 100644 --- a/WalletWasabi.Fluent/Controls/InfoMessage.axaml +++ b/WalletWasabi.Fluent/Controls/InfoMessage.axaml @@ -1,16 +1,20 @@ - + - This is a test message. + This is a test message. - - - - + - + - - + diff --git a/WalletWasabi.Fluent/Controls/LabelsItemsPresenter.axaml b/WalletWasabi.Fluent/Controls/LabelsItemsPresenter.axaml index e2bd951b40..2b680abf0f 100644 --- a/WalletWasabi.Fluent/Controls/LabelsItemsPresenter.axaml +++ b/WalletWasabi.Fluent/Controls/LabelsItemsPresenter.axaml @@ -1,24 +1,24 @@ - + - + Label 1 Label 2 Label 3 Label 4 - + - + + + - - + + + + diff --git a/WalletWasabi.Fluent/Controls/LabelsItemsPresenter.axaml.cs b/WalletWasabi.Fluent/Controls/LabelsItemsPresenter.axaml.cs index e3ce75ea3b..70b8553cdf 100644 --- a/WalletWasabi.Fluent/Controls/LabelsItemsPresenter.axaml.cs +++ b/WalletWasabi.Fluent/Controls/LabelsItemsPresenter.axaml.cs @@ -1,49 +1,27 @@ using Avalonia; using Avalonia.Controls; -using Avalonia.Controls.Presenters; -using Avalonia.Media; -using Avalonia.Styling; namespace WalletWasabi.Fluent.Controls; -public class LabelsItemsPresenter : ItemsPresenter, IStyleable +public class LabelsItemsPresenter : ItemsControl { - public static readonly StyledProperty ForegroundProperty = - AvaloniaProperty.Register("Foreground"); - - public static readonly StyledProperty BorderBrushProperty = - AvaloniaProperty.Register("BorderBrush"); + public static readonly StyledProperty InfiniteWidthMeasureProperty = + AvaloniaProperty.Register(nameof(InfiniteWidthMeasure)); public static readonly StyledProperty MaxLabelWidthProperty = AvaloniaProperty.Register("MaxLabelWidth"); - public double MaxLabelWidth - { - get => GetValue(MaxLabelWidthProperty); - set => SetValue(MaxLabelWidthProperty, value); - } - - public IBrush Foreground + public bool InfiniteWidthMeasure { - get => GetValue(ForegroundProperty); - set => SetValue(ForegroundProperty, value); + get => GetValue(InfiniteWidthMeasureProperty); + set => SetValue(InfiniteWidthMeasureProperty, value); } - public IBrush BorderBrush + public double MaxLabelWidth { - get => GetValue(BorderBrushProperty); - set => SetValue(BorderBrushProperty, value); + get => GetValue(MaxLabelWidthProperty); + set => SetValue(MaxLabelWidthProperty, value); } - Type IStyleable.StyleKey => typeof(LabelsItemsPresenter); - - protected override void PanelCreated(IPanel panel) - { - base.PanelCreated(panel); - - if (panel is LabelsPanel labelsPanel) - { - labelsPanel.Presenter = this; - } - } + protected override Type StyleKeyOverride => typeof(LabelsItemsPresenter); } diff --git a/WalletWasabi.Fluent/Controls/LabelsPanel.cs b/WalletWasabi.Fluent/Controls/LabelsPanel.cs index 54ada73c5e..bcb46a8749 100644 --- a/WalletWasabi.Fluent/Controls/LabelsPanel.cs +++ b/WalletWasabi.Fluent/Controls/LabelsPanel.cs @@ -2,11 +2,16 @@ using System.Linq; using Avalonia; using Avalonia.Controls; +using Avalonia.Layout; +using Avalonia.LogicalTree; namespace WalletWasabi.Fluent.Controls; -public class LabelsPanel : VirtualizingStackPanel +public class LabelsPanel : Panel { + public static readonly StyledProperty InfiniteWidthMeasureProperty = + AvaloniaProperty.Register(nameof(InfiniteWidthMeasure)); + public static readonly StyledProperty EllipsisControlProperty = AvaloniaProperty.Register(nameof(EllipsisControl)); @@ -18,6 +23,14 @@ public class LabelsPanel : VirtualizingStackPanel private List? _filteredItems; private IDisposable? _disposable; + private double _spacing = 2.0; + private bool _needToTrim; + + public bool InfiniteWidthMeasure + { + get => GetValue(InfiniteWidthMeasureProperty); + set => SetValue(InfiniteWidthMeasureProperty, value); + } public Control? EllipsisControl { @@ -33,9 +46,29 @@ public List? FilteredItems internal LabelsItemsPresenter? Presenter { get; set; } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == DataContextProperty) + { + InvalidateMeasure(); + InvalidateArrange(); + } + } + + public override void ApplyTemplate() + { + base.ApplyTemplate(); + + Presenter = this + .GetLogicalAncestors() + .FirstOrDefault(x => x is LabelsItemsPresenter) as LabelsItemsPresenter; + } + private void UpdateFilteredItems(int count) { - if (Presenter?.Items is IEnumerable items) + if (Presenter?.ItemsSource is IEnumerable items) { FilteredItems = items.Skip(count).ToList(); } @@ -71,51 +104,90 @@ protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e base.OnDetachedFromVisualTree(e); } + private Size MeasureOverridePanel(Size availableSize) + { + var num1 = 0.0; + var num2 = 0.0; + var visualChildren = VisualChildren; + var count = visualChildren.Count; + for (var index = 0; index < count; ++index) + { + if (visualChildren[index] is Layoutable layoutable) + { + layoutable.Measure(availableSize); + num1 += layoutable.DesiredSize.Width; + num2 = Math.Max(num2, layoutable.DesiredSize.Height); + } + } + return new Size(num1, num2); + } + protected override Size MeasureOverride(Size availableSize) { - var ellipsis = 0.0; + var ellipsisDesiredWidth = 0.0; if (EllipsisControl is { }) { EllipsisControl.Measure(availableSize); - ellipsis = EllipsisControl.DesiredSize.Width; + ellipsisDesiredWidth = EllipsisControl.DesiredSize.Width; } - return base.MeasureOverride(availableSize.WithWidth(availableSize.Width + ellipsis)); + var size = MeasureOverridePanel(availableSize.WithWidth(availableSize.Width)); + + _needToTrim = !(size.Width < availableSize.Width); + + var result = CalculateWidth( + Children, + EllipsisControl, + availableSize.Width, + ellipsisDesiredWidth, + _spacing, + _needToTrim); + + size = size.WithWidth(result.Width); + + // TODO: + // Currently we use quickfix to make work TreeDataGrid with ShowColumnHeaders=false + // The width=double.MaxValue was causing measure exception (measure with infinity) + // Quick fix for now is to limit InfiniteWidthMeasure to 10000 instead of double.MaxValue + return InfiniteWidthMeasure ? new Size(10000, size.Height) : size; } - protected override Size ArrangeOverride(Size finalSize) + private CalculateResult CalculateWidth( + Avalonia.Controls.Controls children, + Control? trimControl, + double finalWidth, + double trimWidth, + double spacing, + bool needToTrim) { - var spacing = Spacing; - var ellipsisWidth = 0.0; + var totalChildren = children.Count; var width = 0.0; var height = 0.0; - var finalWidth = finalSize.Width; - var showEllipsis = false; - var totalChildren = Children.Count; var count = 0; - - if (EllipsisControl is { }) - { - ellipsisWidth = EllipsisControl.DesiredSize.Width; - } + var trim = false; for (var i = 0; i < totalChildren; i++) { - var child = Children[i]; + var child = children[i]; + if (child == trimControl) + { + continue; + } + var childWidth = child.DesiredSize.Width; height = Math.Max(height, child.DesiredSize.Height); - if (width + childWidth > finalWidth) + if (width + childWidth > finalWidth && needToTrim) { while (true) { - if (width + ellipsisWidth > finalWidth) + if (width + trimWidth > finalWidth) { var previous = i - 1; if (previous >= 0) { - var previousChild = Children[previous]; + var previousChild = children[previous]; count--; width -= previousChild.DesiredSize.Width + spacing; } @@ -130,11 +202,7 @@ protected override Size ArrangeOverride(Size finalSize) } } - showEllipsis = true; - if (EllipsisControl is { }) - { - width += EllipsisControl.DesiredSize.Width; - } + trim = true; break; } @@ -143,14 +211,49 @@ protected override Size ArrangeOverride(Size finalSize) count++; } + if (trim) + { + width += trimWidth; + } + + return new CalculateResult(count, width, height, trim); + } + + protected override Size ArrangeOverride(Size finalSize) + { + var spacing = _spacing; + var trimWidth = 0.0; + var finalWidth = finalSize.Width; + var ellipsisControl = EllipsisControl; + var children = Children; + var totalChildren = children.Count; + + if (ellipsisControl is { }) + { + trimWidth = ellipsisControl.DesiredSize.Width; + } + + var result = CalculateWidth( + children, + ellipsisControl, + finalWidth, + trimWidth, + spacing, + _needToTrim); + var offset = 0.0; for (var i = 0; i < totalChildren; i++) { - var child = Children[i]; - if (i < count) + var child = children[i]; + if (child == ellipsisControl) + { + continue; + } + + if (i < result.Count) { - var rect = new Rect(offset, 0.0, child.DesiredSize.Width, height); + var rect = new Rect(offset, 0.0, child.DesiredSize.Width, result.Height); child.Arrange(rect); offset += child.DesiredSize.Width + spacing; } @@ -160,21 +263,23 @@ protected override Size ArrangeOverride(Size finalSize) } } - if (EllipsisControl is { }) + if (ellipsisControl is { }) { - if (showEllipsis) + if (result.Trim) { - var rect = new Rect(offset, 0.0, EllipsisControl.DesiredSize.Width, height); - EllipsisControl.Arrange(rect); + var rect = new Rect(offset, 0.0, trimWidth, result.Height); + ellipsisControl.Arrange(rect); } else { - EllipsisControl.Arrange(new Rect(-10000, -10000, 0, 0)); + ellipsisControl.Arrange(new Rect(-10000, -10000, 0, 0)); } } - UpdateFilteredItems(count); + UpdateFilteredItems(result.Count); - return new Size(width, height); + return new Size(result.Width, result.Height); } + + private record CalculateResult(int Count, double Width, double Height, bool Trim); } diff --git a/WalletWasabi.Fluent/Controls/LineChart.Properties.cs b/WalletWasabi.Fluent/Controls/LineChart.Properties.cs index 3cb5c74b8d..63279a82ce 100644 --- a/WalletWasabi.Fluent/Controls/LineChart.Properties.cs +++ b/WalletWasabi.Fluent/Controls/LineChart.Properties.cs @@ -286,6 +286,9 @@ public partial class LineChart // Cursor + public static readonly StyledProperty EnableCursorProperty = + AvaloniaProperty.Register(nameof(EnableCursor), true); + public static readonly StyledProperty CursorStrokeProperty = AvaloniaProperty.Register(nameof(CursorStroke)); @@ -408,7 +411,8 @@ static LineChart() CursorStrokeDashStyleProperty, CursorStrokeLineCapProperty, CursorStrokeLineJoinProperty, - CursorStrokeMiterLimitProperty); + CursorStrokeMiterLimitProperty, + EnableCursorProperty); AffectsRender( BorderBrushProperty, @@ -895,6 +899,12 @@ public double YAxisTitleFontSize // Cursor + public bool EnableCursor + { + get => GetValue(EnableCursorProperty); + set => SetValue(EnableCursorProperty, value); + } + public IBrush? CursorStroke { get => GetValue(CursorStrokeProperty); diff --git a/WalletWasabi.Fluent/Controls/LineChart.cs b/WalletWasabi.Fluent/Controls/LineChart.cs index 1f4dc47658..48053fe368 100644 --- a/WalletWasabi.Fluent/Controls/LineChart.cs +++ b/WalletWasabi.Fluent/Controls/LineChart.cs @@ -24,7 +24,7 @@ public LineChart() AddHandler(PointerPressedEvent, PointerPressedHandler, RoutingStrategies.Tunnel); AddHandler(PointerReleasedEvent, PointerReleasedHandler, RoutingStrategies.Tunnel); AddHandler(PointerMovedEvent, PointerMovedHandler, RoutingStrategies.Tunnel); - AddHandler(PointerLeaveEvent, PointerLeaveHandler, RoutingStrategies.Tunnel); + AddHandler(PointerExitedEvent, PointerLeaveHandler, RoutingStrategies.Tunnel); } private static double Clamp(double val, double min, double max) @@ -62,18 +62,15 @@ private static Geometry CreateStrokeGeometry(IReadOnlyList points) return geometry; } - private static FormattedText CreateFormattedText(string text, Typeface typeface, TextAlignment alignment, - double fontSize, Size constraint) + private static FormattedText CreateFormattedText(string text, Typeface typeface, TextAlignment alignment, double fontSize) { - return new FormattedText() + var ft = new FormattedText(text, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeface, fontSize, null) { - Typeface = typeface, - Text = text, TextAlignment = alignment, - TextWrapping = TextWrapping.NoWrap, - FontSize = fontSize, - Constraint = constraint + Trimming = TextTrimming.None }; + + return ft; } private void UpdateXAxisCursorPosition(double x) @@ -124,6 +121,11 @@ private void PointerLeaveHandler(object? sender, PointerEventArgs e) private void PointerMovedHandler(object? sender, PointerEventArgs e) { + if (!EnableCursor) + { + return; + } + var position = e.GetPosition(this); if (_captured) { @@ -164,6 +166,11 @@ private void PointerReleasedHandler(object? sender, PointerReleasedEventArgs e) private void PointerPressedHandler(object? sender, PointerPressedEventArgs e) { + if (!EnableCursor) + { + return; + } + var position = e.GetPosition(this); UpdateXAxisCursorPosition(position.X); Cursor = new Cursor(StandardCursorType.SizeWestEast); @@ -411,7 +418,7 @@ private void DrawAreaFill(DrawingContext context, LineChartState state) var deflate = 0.5; var geometry = CreateFillGeometry(state.Points, state.AreaWidth, state.AreaHeight); - var transform = context.PushPreTransform( + var transform = context.PushTransform( Matrix.CreateTranslation( state.AreaMargin.Left + deflate, state.AreaMargin.Top + deflate)); @@ -440,7 +447,7 @@ private void DrawAreaStroke(DrawingContext context, LineChartState state) var pen = new Pen(brush, thickness, dashStyle, lineCap, lineJoin, miterLimit); var deflate = thickness * 0.5; var geometry = CreateStrokeGeometry(state.Points); - var transform = context.PushPreTransform( + var transform = context.PushTransform( Matrix.CreateTranslation( state.AreaMargin.Left + deflate, state.AreaMargin.Top + deflate)); @@ -470,7 +477,7 @@ private void DrawCursor(DrawingContext context, LineChartState state) var deflate = thickness * 0.5; var p1 = new Point(state.XAxisCursorPosition + deflate, 0); var p2 = new Point(state.XAxisCursorPosition + deflate, state.AreaHeight); - var transform = context.PushPreTransform( + var transform = context.PushTransform( Matrix.CreateTranslation( state.AreaMargin.Left, state.AreaMargin.Top)); @@ -563,30 +570,36 @@ private void DrawXAxisLabels(DrawingContext context, LineChartState state) for (var i = 0; i < labels.Count; i++) { var label = labels[i]; - var formattedText = CreateFormattedText(label, typeface, TextAlignment.Left, fontSize, Size.Empty); + var formattedText = CreateFormattedText(label, typeface, TextAlignment.Left, fontSize); formattedTextLabels.Add(formattedText); - constrainWidthMax = Math.Max(constrainWidthMax, formattedText.Bounds.Width); - constrainHeightMax = Math.Max(constrainHeightMax, formattedText.Bounds.Height); + constrainWidthMax = Math.Max(constrainWidthMax, formattedText.Width); + constrainHeightMax = Math.Max(constrainHeightMax, formattedText.Height); } var constraintMax = new Size(constrainWidthMax, constrainHeightMax); - var offsetTransform = context.PushPreTransform(Matrix.CreateTranslation(offset.X, offset.Y)); + var offsetTransform = context.PushTransform(Matrix.CreateTranslation(offset.X, offset.Y)); for (var i = 0; i < formattedTextLabels.Count; i++) { - formattedTextLabels[i].Constraint = constraintMax; - var origin = new Point(i * state.XAxisLabelStep + constraintMax.Width / 2 + state.AreaMargin.Left, - originTop); + formattedTextLabels[i].MaxTextHeight = constraintMax.Height; + formattedTextLabels[i].MaxTextWidth = constraintMax.Width; + var origin = new Point(i * state.XAxisLabelStep + constraintMax.Width / 2 + state.AreaMargin.Left, originTop); var offsetCenter = new Point(constraintMax.Width / 2 - constraintMax.Width / 2, 0); - offsetCenter = AlignXAxisLabelOffset(offsetCenter, formattedTextLabels[i].Bounds.Width, i, formattedTextLabels.Count, alignment); + offsetCenter = AlignXAxisLabelOffset( + offsetCenter, + formattedTextLabels[i].Width, + i, + formattedTextLabels.Count, + alignment); var xPosition = origin.X + constraintMax.Width / 2; var yPosition = origin.Y + constraintMax.Height / 2; var matrix = Matrix.CreateTranslation(-xPosition, -yPosition) * Matrix.CreateRotation(angleRadians) * Matrix.CreateTranslation(xPosition, yPosition); - var labelTransform = context.PushPreTransform(matrix); + var labelTransform = context.PushTransform(matrix); var opacityState = context.PushOpacity(opacity); - context.DrawText(foreground, origin + offsetCenter, formattedTextLabels[i]); + formattedTextLabels[i].SetForegroundBrush(foreground); + context.DrawText(formattedTextLabels[i], origin + offsetCenter); opacityState.Dispose(); labelTransform.Dispose(); } @@ -620,20 +633,20 @@ private void DrawXAxisTitle(DrawingContext context, LineChartState state) var size = new Size(state.AreaWidth, XAxisTitleSize.Height); var angleRadians = Math.PI / 180.0 * XAxisTitleAngle; var alignment = XAxisTitleAlignment; - var offsetTransform = context.PushPreTransform(Matrix.CreateTranslation(offset.X, offset.Y)); + var offsetTransform = context.PushTransform(Matrix.CreateTranslation(offset.X, offset.Y)); var origin = new Point(state.AreaMargin.Left, state.AreaHeight + state.AreaMargin.Bottom); - var constraint = new Size(size.Width, size.Height); - var formattedText = CreateFormattedText(XAxisTitle, typeface, alignment, fontSize, constraint); + var formattedText = CreateFormattedText(XAxisTitle, typeface, alignment, fontSize); var xPosition = origin.X + size.Width / 2; var yPosition = origin.Y + size.Height / 2; var matrix = Matrix.CreateTranslation(-xPosition, -yPosition) * Matrix.CreateRotation(angleRadians) * Matrix.CreateTranslation(xPosition, yPosition); - var labelTransform = context.PushPreTransform(matrix); + var labelTransform = context.PushTransform(matrix); var offsetCenter = new Point(0, 0); var opacityState = context.PushOpacity(opacity); - context.DrawText(foreground, origin + offsetCenter, formattedText); + formattedText.SetForegroundBrush(foreground); + context.DrawText(formattedText, origin + offsetCenter); opacityState.Dispose(); labelTransform.Dispose(); offsetTransform.Dispose(); @@ -737,31 +750,39 @@ private void DrawYAxisLabels(DrawingContext context, LineChartState state) { var label = labels[i]; var textAlignment = GetYAxisLabelTextAlignment(alignment); - var formattedText = CreateFormattedText(label, typeface, textAlignment, fontSize, Size.Empty); + var formattedText = CreateFormattedText(label, typeface, textAlignment, fontSize); formattedTextLabels.Add(formattedText); - constrainWidthMax = Math.Max(constrainWidthMax, formattedText.Bounds.Width); - constrainHeightMax = Math.Max(constrainHeightMax, formattedText.Bounds.Height); + constrainWidthMax = Math.Max(constrainWidthMax, formattedText.Width); + constrainHeightMax = Math.Max(constrainHeightMax, formattedText.Height); } var constraintMax = new Size(constrainWidthMax, constrainHeightMax); - var offsetTransform = context.PushPreTransform(Matrix.CreateTranslation(offset.X, offset.Y)); + var offsetTransform = context.PushTransform(Matrix.CreateTranslation(offset.X, offset.Y)); for (var i = 0; i < formattedTextLabels.Count; i++) { - formattedTextLabels[i].Constraint = constraintMax; + formattedTextLabels[i].MaxTextHeight = constraintMax.Height; + formattedTextLabels[i].MaxTextWidth = constraintMax.Width; - var origin = new Point(originLeft, + var origin = new Point( + originLeft, i * state.YAxisLabelStep - constraintMax.Height / 2 + state.AreaMargin.Top); var offsetCenter = new Point(constraintMax.Width / 2 - constraintMax.Width / 2, 0); - offsetCenter = AlignYAxisLabelOffset(offsetCenter, formattedTextLabels[i].Bounds.Height, i, formattedTextLabels.Count, alignment); + offsetCenter = AlignYAxisLabelOffset( + offsetCenter, + formattedTextLabels[i].Height, + i, + formattedTextLabels.Count, + alignment); var xPosition = origin.X + constraintMax.Width / 2; var yPosition = origin.Y + constraintMax.Height / 2; var matrix = Matrix.CreateTranslation(-xPosition, -yPosition) * Matrix.CreateRotation(angleRadians) * Matrix.CreateTranslation(xPosition, yPosition); - var labelTransform = context.PushPreTransform(matrix); + var labelTransform = context.PushTransform(matrix); var opacityState = context.PushOpacity(opacity); - context.DrawText(foreground, origin + offsetCenter, formattedTextLabels[i]); + formattedTextLabels[i].SetForegroundBrush(foreground); + context.DrawText(formattedTextLabels[i], origin + offsetCenter); opacityState.Dispose(); labelTransform.Dispose(); } @@ -795,19 +816,19 @@ private void DrawYAxisTitle(DrawingContext context, LineChartState state) var size = YAxisTitleSize; var angleRadians = Math.PI / 180.0 * YAxisTitleAngle; var alignment = YAxisTitleAlignment; - var offsetTransform = context.PushPreTransform(Matrix.CreateTranslation(offset.X, offset.Y)); + var offsetTransform = context.PushTransform(Matrix.CreateTranslation(offset.X, offset.Y)); var origin = new Point(state.AreaMargin.Left, state.AreaHeight + state.AreaMargin.Top); - var constraint = new Size(size.Width, size.Height); - var formattedText = CreateFormattedText(YAxisTitle, typeface, alignment, fontSize, constraint); + var formattedText = CreateFormattedText(YAxisTitle, typeface, alignment, fontSize); var xPosition = origin.X + size.Width / 2; var yPosition = origin.Y + size.Height / 2; var matrix = Matrix.CreateTranslation(-xPosition, -yPosition) * Matrix.CreateRotation(angleRadians) * Matrix.CreateTranslation(xPosition, yPosition); - var labelTransform = context.PushPreTransform(matrix); - var offsetCenter = new Point(0, size.Height / 2 - formattedText.Bounds.Height / 2); + var labelTransform = context.PushTransform(matrix); + var offsetCenter = new Point(0, size.Height / 2 - formattedText.Height / 2); var opacityState = context.PushOpacity(opacity); - context.DrawText(foreground, origin + offsetCenter, formattedText); + formattedText.SetForegroundBrush(foreground); + context.DrawText(formattedText, origin + offsetCenter); opacityState.Dispose(); labelTransform.Dispose(); offsetTransform.Dispose(); @@ -842,7 +863,8 @@ private void UpdateSubscription(INotifyCollectionChanged? oldValue, INotifyColle { newValue.CollectionChanged += ItemsPropertyCollectionChanged; - _collectionChangedSubscriptions[newValue] = Disposable.Create(() => newValue.CollectionChanged -= ItemsPropertyCollectionChanged); + _collectionChangedSubscriptions[newValue] = + Disposable.Create(() => newValue.CollectionChanged -= ItemsPropertyCollectionChanged); } } @@ -851,16 +873,16 @@ private void ItemsPropertyCollectionChanged(object? sender, NotifyCollectionChan InvalidateVisual(); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == XAxisValuesProperty || change.Property == YAxisValuesProperty || change.Property == XAxisLabelsProperty || change.Property == YAxisLabelsProperty) { - UpdateSubscription( - change.OldValue.GetValueOrDefault(), - change.NewValue.GetValueOrDefault()); + var oldINCC = change.OldValue as INotifyCollectionChanged; + var newINCC = change.NewValue as INotifyCollectionChanged; + UpdateSubscription(oldINCC, newINCC); } } @@ -872,7 +894,11 @@ public override void Render(DrawingContext context) DrawAreaFill(context, state); DrawAreaStroke(context, state); - DrawCursor(context, state); + + if (EnableCursor) + { + DrawCursor(context, state); + } DrawXAxis(context, state); DrawXAxisTitle(context, state); diff --git a/WalletWasabi.Fluent/Controls/NavBarItem.axaml b/WalletWasabi.Fluent/Controls/NavBarItem.axaml index b7f0199336..6227e9cb8b 100644 --- a/WalletWasabi.Fluent/Controls/NavBarItem.axaml +++ b/WalletWasabi.Fluent/Controls/NavBarItem.axaml @@ -1,8 +1,6 @@ - + @@ -10,30 +8,17 @@ - - 12 - - + + 12 - + - - - - + - + - + - - + - - + + - - + + - - + + - - + - - - + diff --git a/WalletWasabi.Fluent/Controls/NavBarItem.axaml.cs b/WalletWasabi.Fluent/Controls/NavBarItem.axaml.cs index a2504ff6b3..9b5f260ced 100644 --- a/WalletWasabi.Fluent/Controls/NavBarItem.axaml.cs +++ b/WalletWasabi.Fluent/Controls/NavBarItem.axaml.cs @@ -1,32 +1,36 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Metadata; +using Avalonia.Input; using Avalonia.Layout; +using System.Windows.Input; namespace WalletWasabi.Fluent.Controls; /// /// Container for NavBarItems. /// -[PseudoClasses(":horizontal", ":vertical", ":selectable", ":selected")] -public class NavBarItem : Button +[PseudoClasses(":horizontal", ":vertical", ":selected")] +public class NavBarItem : ContentControl { + public static readonly StyledProperty CommandProperty = + AvaloniaProperty.Register(nameof(Command)); + public static readonly StyledProperty IconProperty = AvaloniaProperty.Register(nameof(Icon)); public static readonly StyledProperty IndicatorOrientationProperty = AvaloniaProperty.Register(nameof(IndicatorOrientation), Orientation.Vertical); - public static readonly StyledProperty IsSelectableProperty = - AvaloniaProperty.Register(nameof(IsSelectable)); - - public static readonly StyledProperty IsSelectedProperty = - AvaloniaProperty.Register(nameof(IsSelected)); - public NavBarItem() { UpdateIndicatorOrientationPseudoClasses(IndicatorOrientation); - UpdatePseudoClass(":selectable", IsSelectable); + } + + public ICommand? Command + { + get => GetValue(CommandProperty); + set => SetValue(CommandProperty, value); } /// @@ -47,41 +51,23 @@ public Orientation IndicatorOrientation set => SetValue(IndicatorOrientationProperty, value); } - /// - /// Gets or sets flag indicating whether item supports selected state. - /// - public bool IsSelectable - { - get => GetValue(IsSelectableProperty); - set => SetValue(IsSelectableProperty, value); - } - - /// - /// Gets or sets if the item is selected or not. - /// - public bool IsSelected - { - get => GetValue(IsSelectedProperty); - set => SetValue(IsSelectedProperty, value); - } - - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == IndicatorOrientationProperty) { - UpdateIndicatorOrientationPseudoClasses(change.NewValue.GetValueOrDefault()); + UpdateIndicatorOrientationPseudoClasses(change.GetNewValue()); } + } - if (change.Property == IsSelectableProperty) - { - UpdatePseudoClass(":selectable", change.NewValue.GetValueOrDefault()); - } + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + base.OnPointerPressed(e); - if (change.Property == IsSelectedProperty) + if (Command != null && Command.CanExecute(default)) { - UpdatePseudoClass(":selected", change.NewValue.GetValueOrDefault()); + Command.Execute(default); } } diff --git a/WalletWasabi.Fluent/Controls/PreviewItem.axaml b/WalletWasabi.Fluent/Controls/PreviewItem.axaml index 1bdf4fd624..6c4e80013e 100644 --- a/WalletWasabi.Fluent/Controls/PreviewItem.axaml +++ b/WalletWasabi.Fluent/Controls/PreviewItem.axaml @@ -1,15 +1,17 @@ - + + CopyableContent="Text to copy" /> - - + - + - + - + + + - - - - + + + + + + + - + diff --git a/WalletWasabi.Fluent/Controls/PreviewItem.axaml.cs b/WalletWasabi.Fluent/Controls/PreviewItem.axaml.cs index b5660303fa..bb2625db03 100644 --- a/WalletWasabi.Fluent/Controls/PreviewItem.axaml.cs +++ b/WalletWasabi.Fluent/Controls/PreviewItem.axaml.cs @@ -22,8 +22,8 @@ public class PreviewItem : ContentControl public static readonly StyledProperty IsIconVisibleProperty = AvaloniaProperty.Register(nameof(IsIconVisible)); - public static readonly StyledProperty TextValueProperty = - AvaloniaProperty.Register(nameof(TextValue)); + public static readonly StyledProperty CopyableContentProperty = + AvaloniaProperty.Register(nameof(CopyableContent)); public static readonly StyledProperty CopyCommandProperty = AvaloniaProperty.Register(nameof(CopyCommand)); @@ -58,10 +58,10 @@ public bool IsIconVisible set => SetValue(IsIconVisibleProperty, value); } - public string TextValue + public object CopyableContent { - get => GetValue(TextValueProperty); - set => SetValue(TextValueProperty, value); + get => GetValue(CopyableContentProperty); + set => SetValue(CopyableContentProperty, value); } public ICommand CopyCommand @@ -88,10 +88,10 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) var isCopyButtonVisible = button.CopyCommand.IsExecuting - .CombineLatest(this.WhenAnyValue(x => x.IsPointerOver, x => x.TextValue, (a, b) => a && !string.IsNullOrWhiteSpace(b))) + .CombineLatest(this.WhenAnyValue(x => x.IsPointerOver, x => x.CopyableContent, (a, b) => a && !string.IsNullOrWhiteSpace(b?.ToString()))) .Select(x => x.First || x.Second); - this.Bind(IsCopyButtonVisibleProperty, isCopyButtonVisible); + Bind(IsCopyButtonVisibleProperty, isCopyButtonVisible); base.OnApplyTemplate(e); } diff --git a/WalletWasabi.Fluent/Controls/PrivacyBar.axaml b/WalletWasabi.Fluent/Controls/PrivacyBar.axaml index 1c2b91034c..40bc806591 100644 --- a/WalletWasabi.Fluent/Controls/PrivacyBar.axaml +++ b/WalletWasabi.Fluent/Controls/PrivacyBar.axaml @@ -1,36 +1,28 @@ - - + + + - - - - + + + + + - + - - + diff --git a/WalletWasabi.Fluent/Controls/PrivacyBar.axaml.cs b/WalletWasabi.Fluent/Controls/PrivacyBar.axaml.cs index 2048669729..e17b410ba8 100644 --- a/WalletWasabi.Fluent/Controls/PrivacyBar.axaml.cs +++ b/WalletWasabi.Fluent/Controls/PrivacyBar.axaml.cs @@ -1,13 +1,11 @@ using Avalonia; using Avalonia.Controls; -using Avalonia.Controls.Generators; -using Avalonia.Controls.Presenters; using Avalonia.VisualTree; using System.Linq; namespace WalletWasabi.Fluent.Controls; -public class PrivacyBar : ItemsPresenter +public class PrivacyBar : ItemsControl { private const double GapBetweenSegments = 1.5; private const double EnlargeThreshold = 2; @@ -22,9 +20,34 @@ public decimal TotalAmount set => SetValue(TotalAmountProperty, value); } - protected override IItemContainerGenerator CreateItemContainerGenerator() + protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey) { - return new PrivacyBarItemContainerGenerator(this); + return new PrivacyBarSegment(); + } + + protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey) + { + return NeedsContainer(item, out recycleKey); + } + + protected override void PrepareContainerForItemOverride(Control element, object? item, int index) + { + base.PrepareContainerForItemOverride(element, item, index); + + if (element is PrivacyBarSegment privacyBarSegment) + { + privacyBarSegment.DataContext = item; + } + } + + protected override void ClearContainerForItemOverride(Control element) + { + base.ClearContainerForItemOverride(element); + + if (element is PrivacyBarSegment privacyBarSegment) + { + privacyBarSegment.DataContext = null; + } } protected override Size ArrangeOverride(Size finalSize) @@ -43,7 +66,7 @@ protected override Size ArrangeOverride(Size finalSize) children.Select(segment => { var amount = (double)segment.Amount; - var width = Math.Abs(usableWidth * amount / (double)totalAmount); + var width = totalAmount == 0m ? 0d : Math.Abs(usableWidth * amount / (double)totalAmount); return (Coin: segment, Width: width); }).ToArray(); @@ -73,19 +96,4 @@ protected override Size ArrangeOverride(Size finalSize) return base.ArrangeOverride(finalSize); } - - private class PrivacyBarItemContainerGenerator : ItemContainerGenerator - { - public PrivacyBarItemContainerGenerator(IControl owner) : base(owner) - { - } - - protected override IControl CreateContainer(object item) - { - return new PrivacyBarSegment - { - DataContext = item, - }; - } - } } diff --git a/WalletWasabi.Fluent/Controls/PrivacyBarSegment.axaml b/WalletWasabi.Fluent/Controls/PrivacyBarSegment.axaml new file mode 100644 index 0000000000..5720d8d9d9 --- /dev/null +++ b/WalletWasabi.Fluent/Controls/PrivacyBarSegment.axaml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/WalletWasabi.Fluent/Controls/PrivacyBarSegment.cs b/WalletWasabi.Fluent/Controls/PrivacyBarSegment.axaml.cs similarity index 100% rename from WalletWasabi.Fluent/Controls/PrivacyBarSegment.cs rename to WalletWasabi.Fluent/Controls/PrivacyBarSegment.axaml.cs diff --git a/WalletWasabi.Fluent/Controls/PrivacyContentControl.axaml b/WalletWasabi.Fluent/Controls/PrivacyContentControl.axaml index d3d122c2e9..eb2d60fae8 100644 --- a/WalletWasabi.Fluent/Controls/PrivacyContentControl.axaml +++ b/WalletWasabi.Fluent/Controls/PrivacyContentControl.axaml @@ -1,16 +1,18 @@ - + - - + - + + + - - + + + - - - + + - - - + + - - + - - - + diff --git a/WalletWasabi.Fluent/Controls/PrivacyContentControl.axaml.cs b/WalletWasabi.Fluent/Controls/PrivacyContentControl.axaml.cs index 930cd0bf5c..90ba59991d 100644 --- a/WalletWasabi.Fluent/Controls/PrivacyContentControl.axaml.cs +++ b/WalletWasabi.Fluent/Controls/PrivacyContentControl.axaml.cs @@ -25,6 +25,9 @@ public class PrivacyContentControl : ContentControl public static readonly StyledProperty UseOpacityProperty = AvaloniaProperty.Register(nameof(UseOpacity), defaultValue: true); + public static readonly StyledProperty MaxPrivacyCharsProperty = + AvaloniaProperty.Register(nameof(MaxPrivacyChars), int.MaxValue); + public PrivacyContentControl() { if (Design.IsDesignMode) @@ -60,4 +63,10 @@ public bool UseOpacity get => GetValue(UseOpacityProperty); set => SetValue(UseOpacityProperty, value); } + + public int MaxPrivacyChars + { + get => GetValue(MaxPrivacyCharsProperty); + set => SetValue(MaxPrivacyCharsProperty, value); + } } diff --git a/WalletWasabi.Fluent/Controls/PrivacyTextPresenter.cs b/WalletWasabi.Fluent/Controls/PrivacyTextPresenter.cs index e3a60cb2dc..aa50fcb10b 100644 --- a/WalletWasabi.Fluent/Controls/PrivacyTextPresenter.cs +++ b/WalletWasabi.Fluent/Controls/PrivacyTextPresenter.cs @@ -1,28 +1,40 @@ +using System.Globalization; using System.Linq; -using System.Reactive.Linq; using Avalonia; using Avalonia.Controls; using Avalonia.Media; -using Avalonia.Utilities; -using WalletWasabi.Fluent.ViewModels; namespace WalletWasabi.Fluent.Controls; public class PrivacyTextPresenter : UserControl { + public static readonly StyledProperty MaxPrivacyCharsProperty = + AvaloniaProperty.Register(nameof(MaxPrivacyChars), int.MaxValue); + private GlyphRun? _glyphRun; private double _width; private FormattedText? _formattedText; + public int MaxPrivacyChars + { + get => GetValue(MaxPrivacyCharsProperty); + set => SetValue(MaxPrivacyCharsProperty, value); + } + private FormattedText CreateFormattedText() { return new FormattedText( - "", + "X", + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, new Typeface(FontFamily, FontStyle, FontWeight), FontSize, - TextAlignment.Left, - TextWrapping.NoWrap, - Size.Infinity); + null) + { + TextAlignment = TextAlignment.Left, + MaxTextHeight = Size.Infinity.Height, + MaxTextWidth = Size.Infinity.Width + }; } private GlyphRun? CreateGlyphRun(double width) @@ -32,27 +44,26 @@ private FormattedText CreateFormattedText() var glyphTypeface = new Typeface((FontFamily?)FontFamily).GlyphTypeface; var glyph = glyphTypeface.GetGlyph(privacyChar); - var scale = FontSize / glyphTypeface.DesignEmHeight; + var scale = FontSize / glyphTypeface.Metrics.DesignEmHeight; var advance = glyphTypeface.GetGlyphAdvance(glyph) * scale; - var count = width > 0 && width < advance ? 1 : (int)(width / advance); + var count = Math.Min(width > 0 && width < advance ? 1 : (int)(width / advance), MaxPrivacyChars); if (count == 0) { return null; } - var advances = new ReadOnlySlice(new ReadOnlyMemory(Enumerable.Repeat(advance, count).ToArray())); - var characters = new ReadOnlySlice(new ReadOnlyMemory(Enumerable.Repeat(privacyChar, count).ToArray())); - var glyphs = new ReadOnlySlice(new ReadOnlyMemory(Enumerable.Repeat(glyph, count).ToArray())); + var characters = new ReadOnlyMemory(Enumerable.Repeat(privacyChar, count).ToArray()); + var glyphs = Enumerable.Repeat(glyph, count).ToArray(); - return new GlyphRun(glyphTypeface, FontSize, glyphs, advances, characters: characters); + return new GlyphRun(glyphTypeface, FontSize, characters, glyphs); } protected override Size MeasureOverride(Size availableSize) { _formattedText ??= CreateFormattedText(); - return new Size(0, _formattedText.Bounds.Height); + return new Size(0, _formattedText.Height); } public override void Render(DrawingContext context) @@ -65,7 +76,7 @@ public override void Render(DrawingContext context) var width = Bounds.Width; if (_glyphRun is null || width != _width) { - (_glyphRun as IDisposable)?.Dispose(); + _glyphRun?.Dispose(); _glyphRun = CreateGlyphRun(width); _width = width; } @@ -75,4 +86,14 @@ public override void Render(DrawingContext context) context.DrawGlyphRun(Foreground, _glyphRun); } } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == ForegroundProperty) + { + InvalidateVisual(); + } + } } diff --git a/WalletWasabi.Fluent/Controls/ProgressRing.axaml b/WalletWasabi.Fluent/Controls/ProgressRing.axaml index b69da9c99e..2e293e16c4 100644 --- a/WalletWasabi.Fluent/Controls/ProgressRing.axaml +++ b/WalletWasabi.Fluent/Controls/ProgressRing.axaml @@ -1,12 +1,14 @@ - - - - - - - + + + + + + diff --git a/WalletWasabi.Fluent/Controls/ProgressRing.axaml.cs b/WalletWasabi.Fluent/Controls/ProgressRing.axaml.cs index 46860f7581..56c88e9d7e 100644 --- a/WalletWasabi.Fluent/Controls/ProgressRing.axaml.cs +++ b/WalletWasabi.Fluent/Controls/ProgressRing.axaml.cs @@ -33,7 +33,7 @@ public double StrokeThickness set => SetValue(StrokeThicknessProperty, value); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) { base.OnPropertyChanged(e); diff --git a/WalletWasabi.Fluent/Controls/ProgressRingArc.axaml b/WalletWasabi.Fluent/Controls/ProgressRingArc.axaml index 6920e804f7..22ca54de52 100644 --- a/WalletWasabi.Fluent/Controls/ProgressRingArc.axaml +++ b/WalletWasabi.Fluent/Controls/ProgressRingArc.axaml @@ -1,7 +1,9 @@ - - - + + + + diff --git a/WalletWasabi.Fluent/Controls/ProgressRingArc.axaml.cs b/WalletWasabi.Fluent/Controls/ProgressRingArc.axaml.cs index 0ac155140e..ecf1bd4a9c 100644 --- a/WalletWasabi.Fluent/Controls/ProgressRingArc.axaml.cs +++ b/WalletWasabi.Fluent/Controls/ProgressRingArc.axaml.cs @@ -127,7 +127,7 @@ public bool ArcSegmentIsLargeArc private set => SetAndRaise(ArcSegmentIsLargeArcProperty, ref _arcSegmentIsLargeArc, value); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) { base.OnPropertyChanged(e); diff --git a/WalletWasabi.Fluent/Controls/QrCode.cs b/WalletWasabi.Fluent/Controls/QrCode.cs index d4c640e2cf..ac2cd0e90a 100644 --- a/WalletWasabi.Fluent/Controls/QrCode.cs +++ b/WalletWasabi.Fluent/Controls/QrCode.cs @@ -48,19 +48,7 @@ public QrCode() } }); - _saveCommand = ReactiveCommand.CreateFromTask(async address => - { - await SaveQrCodeAsync(address); - return Unit.Default; - }); - - SaveCommand.ThrownExceptions - .ObserveOn(RxApp.TaskpoolScheduler) - .Subscribe(_ => - { - // The error is thrown also in ReceiveAddressViewModel -> SaveQrCodeCommand.ThrownExceptions. - // However we need to catch it here too but to avoid duplicate logging we don't do anything here. - }); + _saveCommand = ReactiveCommand.CreateFromTask(SaveQrCodeAsync); } private bool[,]? FinalMatrix { get; set; } @@ -109,7 +97,7 @@ public async Task SaveQrCodeAsync(string address) } using var rtb = new RenderTargetBitmap(pixSize); - using (var rtbCtx = rtb.CreateDrawingContext(null)) + using (var rtbCtx = rtb.CreateDrawingContext()) { DrawQrCodeImage(rtbCtx, FinalMatrix, pixSize.ToSize(1)); } @@ -140,7 +128,7 @@ public async Task SaveQrCodeAsync(string address) private (int indexW, int indexH) GetMatrixIndexSize(bool[,] source) => (source.GetUpperBound(0) + 1, source.GetUpperBound(1) + 1); - private void DrawQrCodeImage(IDrawingContextImpl ctx, bool[,] source, Size size) + private void DrawQrCodeImage(DrawingContext ctx, bool[,] source, Size size) { var qrCodeSize = GetQrCodeSize(source, size); var (indexW, indexH) = GetMatrixIndexSize(source); @@ -171,14 +159,9 @@ public override void Render(DrawingContext context) return; } - DrawQrCodeImage(context.PlatformImpl, source, Bounds.Size); + DrawQrCodeImage(context, source, Bounds.Size); } - // TODO: Fix remark. - /// - /// The returned size can differ from the size that is set on the control, which can cause unexpected layout issue. - /// Choose a size on the control which can be divided by minDimension without a remainder. - /// private (Size coercedSize, double gridCellFactor) GetQrCodeSize(bool[,] source, Size size) { var (indexW, indexH) = GetMatrixIndexSize(source); diff --git a/WalletWasabi.Fluent/Controls/QuestionControl.axaml b/WalletWasabi.Fluent/Controls/QuestionControl.axaml index 1ab79c5106..9229afb66c 100644 --- a/WalletWasabi.Fluent/Controls/QuestionControl.axaml +++ b/WalletWasabi.Fluent/Controls/QuestionControl.axaml @@ -1,10 +1,12 @@ - + + + - +