From 6a632dca10b8ff30c65cfc1c18730ecba5c8aeab Mon Sep 17 00:00:00 2001 From: Jesmar Castillo <32423903+jesmarsc@users.noreply.github.com> Date: Thu, 4 Aug 2022 12:52:23 -0700 Subject: [PATCH] init (#1) --- .eslintignore | 10 + .eslintrc.js | 6 + .github/workflows/main.yml | 32 + .gitignore | 20 + .prettierrc.js | 13 + babel.config.js | 3 + .../enabling-cross-border-payments/index.mdx | 49 + .../enabling-cross-border-payments/launch.mdx | 49 + .../metadata.json | 4 + .../reference-implementations.mdx | 34 + .../setting-up-production-server.mdx | 62 + .../setting-up-test-server.mdx | 317 + .../enabling-deposit-and-withdrawal/index.mdx | 34 + .../launch.mdx | 57 + .../metadata.json | 4 + .../reference-implementations.mdx | 34 + .../setting-up-production-server.mdx | 91 + .../setting-up-test-server.mdx | 246 + docs/anchoring-assets/index.mdx | 33 + docs/anchoring-assets/metadata.json | 4 + docs/building-apps/basic-wallet.mdx | 873 ++ .../deposit-anchored-assets.mdx | 453 + .../connect-to-anchors/index.mdx | 14 + .../connect-to-anchors/metadata.json | 4 + .../setup-for-anchored-assets.mdx | 358 + .../withdraw-anchored-assets.mdx | 326 + docs/building-apps/custom-assets.mdx | 457 + docs/building-apps/first-deposit.mdx | 60 + docs/building-apps/index.mdx | 26 + docs/building-apps/key-management.mdx | 38 + docs/building-apps/metadata.json | 4 + docs/building-apps/project-setup.mdx | 492 + .../building-apps/setup-custodial-account.mdx | 204 + docs/building-apps/xlm-payments.mdx | 736 ++ docs/glossary/accounts.mdx | 90 + docs/glossary/assets.mdx | 8 + docs/glossary/channels.mdx | 75 + docs/glossary/claimable-balance.mdx | 417 + docs/glossary/clawback.mdx | 408 + docs/glossary/decentralized-exchange.mdx | 82 + docs/glossary/federation.mdx | 96 + docs/glossary/fee-bumps.mdx | 79 + docs/glossary/fees.mdx | 55 + docs/glossary/inflation.mdx | 12 + docs/glossary/ledger.mdx | 83 + docs/glossary/liquidity-pool.mdx | 506 + docs/glossary/lumen-supply.mdx | 87 + docs/glossary/metadata.json | 5 + docs/glossary/minimum-balance.mdx | 44 + docs/glossary/miscellaneous-core-objects.mdx | 16 + docs/glossary/multisig.mdx | 185 + docs/glossary/muxed-accounts.mdx | 349 + docs/glossary/network-passphrase.mdx | 28 + docs/glossary/operations.mdx | 46 + docs/glossary/scp.mdx | 16 + docs/glossary/sponsored-reserves.mdx | 525 + docs/glossary/testnet.mdx | 78 + docs/glossary/transactions.mdx | 206 + docs/glossary/versioning.mdx | 102 + docs/glossary/xdr.mdx | 20 + docs/index.mdx | 75 + docs/issuing-assets/anatomy-of-an-asset.mdx | 101 + docs/issuing-assets/control-asset-access.mdx | 171 + docs/issuing-assets/how-to-issue-an-asset.mdx | 340 + docs/issuing-assets/index.mdx | 14 + docs/issuing-assets/metadata.json | 4 + docs/issuing-assets/publishing-asset-info.mdx | 314 + docs/metadata.json | 4 + docs/run-api-server/configuring.mdx | 153 + docs/run-api-server/index.mdx | 24 + docs/run-api-server/ingestion.mdx | 309 + docs/run-api-server/installing.mdx | 92 + docs/run-api-server/metadata.json | 4 + docs/run-api-server/migrating.mdx | 173 + docs/run-api-server/monitoring.mdx | 113 + docs/run-api-server/prerequisites.mdx | 58 + docs/run-api-server/remote-core.mdx | 99 + docs/run-api-server/running.mdx | 40 + docs/run-api-server/scaling.mdx | 46 + docs/run-core-node/commands.mdx | 140 + docs/run-core-node/configuring.mdx | 264 + docs/run-core-node/index.mdx | 85 + docs/run-core-node/installation.mdx | 60 + docs/run-core-node/metadata.json | 4 + docs/run-core-node/monitoring.mdx | 462 + docs/run-core-node/network-upgrades.mdx | 59 + docs/run-core-node/prerequisites.mdx | 36 + .../publishing-history-archives.mdx | 332 + docs/run-core-node/running-node.mdx | 180 + docs/run-core-node/tier-1-orgs.mdx | 72 + docs/software-and-sdks/index.mdx | 123 + docs/software-and-sdks/metadata.json | 4 + docs/start/introduction.mdx | 48 + docs/start/list-of-operations.mdx | 760 ++ docs/start/metadata.json | 4 + docs/start/stellar-stack.mdx | 35 + docs/tutorials/create-account.mdx | 251 + docs/tutorials/follow-received-payments.mdx | 266 + docs/tutorials/handling-errors.mdx | 247 + docs/tutorials/metadata.json | 4 + .../moneygram-access-integration-guide.mdx | 412 + docs/tutorials/securing-projects.mdx | 80 + docs/tutorials/send-and-receive-payments.mdx | 787 ++ docs/web-assets/SEP24-state-diagram.png | Bin 0 -> 578352 bytes docs/web-assets/anchor-validation-suite.png | Bin 0 -> 779929 bytes docs/web-assets/demo-client.png | Bin 0 -> 860459 bytes docs/web-assets/demo-wallet-sep31.png | Bin 0 -> 285947 bytes docs/web-assets/demo_wallet_sep24dep.png | Bin 0 -> 542173 bytes docs/web-assets/first-deposit-anchor-flow.png | Bin 0 -> 30663 bytes .../first-deposit-claimable-balance-flow.png | Bin 0 -> 35227 bytes docs/web-assets/first-deposit-wallet-flow.png | Bin 0 -> 27914 bytes .../horizon-scaling/Topology-1VM.png | Bin 0 -> 67454 bytes .../horizon-scaling/Topology-2VMs.png | Bin 0 -> 154957 bytes .../horizon-scaling/Topology-3VMs.png | Bin 0 -> 267015 bytes .../Topology-Enterprise-HotBackup.png | Bin 0 -> 840310 bytes .../horizon-scaling/Topology-Enterprise.png | Bin 0 -> 289829 bytes docs/web-assets/orderbook.png | Bin 0 -> 79217 bytes docs/web-assets/polaris.png | Bin 0 -> 515565 bytes docs/web-assets/ready-to-rock.png | Bin 0 -> 4020 bytes docs/web-assets/select-currency.png | Bin 0 -> 2853 bytes docs/web-assets/stellar-wallet-file.png | Bin 0 -> 12028 bytes docs/web-assets/wallet-mgi-architecture-1.png | Bin 0 -> 30256 bytes docs/web-assets/wallet-mgi-architecture-2.png | Bin 0 -> 27675 bytes docusaurus.config.js | 151 + package.json | 60 + sidebars.js | 142 + src/components/Alert.js | 6 + src/components/CodeExample.js | 46 + src/components/ReadMore.js | 3 + src/css/components/_doc-page.scss | 44 + src/css/components/_footer.scss | 19 + src/css/components/_navbar.scss | 11 + src/css/components/_sidebar.scss | 33 + src/css/components/_toc.scss | 2 + src/css/custom.scss | 80 + src/css/variables.scss | 1 + static/.nojekyll | 0 static/img/favicon-96x96.png | Bin 0 -> 3263 bytes static/img/favicon.ico | Bin 0 -> 1150 bytes static/img/logo.svg | 54 + static/img/stellar-logo-dark.svg | 1 + static/img/stellar-logo.svg | 1 + static/robots.txt | 5 + tsconfig.json | 12 + yarn.lock | 9166 +++++++++++++++++ 145 files changed, 25451 insertions(+) create mode 100644 .eslintignore create mode 100644 .eslintrc.js create mode 100644 .github/workflows/main.yml create mode 100644 .gitignore create mode 100644 .prettierrc.js create mode 100644 babel.config.js create mode 100644 docs/anchoring-assets/enabling-cross-border-payments/index.mdx create mode 100644 docs/anchoring-assets/enabling-cross-border-payments/launch.mdx create mode 100644 docs/anchoring-assets/enabling-cross-border-payments/metadata.json create mode 100644 docs/anchoring-assets/enabling-cross-border-payments/reference-implementations.mdx create mode 100644 docs/anchoring-assets/enabling-cross-border-payments/setting-up-production-server.mdx create mode 100644 docs/anchoring-assets/enabling-cross-border-payments/setting-up-test-server.mdx create mode 100644 docs/anchoring-assets/enabling-deposit-and-withdrawal/index.mdx create mode 100644 docs/anchoring-assets/enabling-deposit-and-withdrawal/launch.mdx create mode 100644 docs/anchoring-assets/enabling-deposit-and-withdrawal/metadata.json create mode 100644 docs/anchoring-assets/enabling-deposit-and-withdrawal/reference-implementations.mdx create mode 100644 docs/anchoring-assets/enabling-deposit-and-withdrawal/setting-up-production-server.mdx create mode 100644 docs/anchoring-assets/enabling-deposit-and-withdrawal/setting-up-test-server.mdx create mode 100644 docs/anchoring-assets/index.mdx create mode 100644 docs/anchoring-assets/metadata.json create mode 100644 docs/building-apps/basic-wallet.mdx create mode 100644 docs/building-apps/connect-to-anchors/deposit-anchored-assets.mdx create mode 100644 docs/building-apps/connect-to-anchors/index.mdx create mode 100644 docs/building-apps/connect-to-anchors/metadata.json create mode 100644 docs/building-apps/connect-to-anchors/setup-for-anchored-assets.mdx create mode 100644 docs/building-apps/connect-to-anchors/withdraw-anchored-assets.mdx create mode 100644 docs/building-apps/custom-assets.mdx create mode 100644 docs/building-apps/first-deposit.mdx create mode 100644 docs/building-apps/index.mdx create mode 100644 docs/building-apps/key-management.mdx create mode 100644 docs/building-apps/metadata.json create mode 100644 docs/building-apps/project-setup.mdx create mode 100644 docs/building-apps/setup-custodial-account.mdx create mode 100644 docs/building-apps/xlm-payments.mdx create mode 100644 docs/glossary/accounts.mdx create mode 100644 docs/glossary/assets.mdx create mode 100644 docs/glossary/channels.mdx create mode 100644 docs/glossary/claimable-balance.mdx create mode 100644 docs/glossary/clawback.mdx create mode 100644 docs/glossary/decentralized-exchange.mdx create mode 100644 docs/glossary/federation.mdx create mode 100644 docs/glossary/fee-bumps.mdx create mode 100644 docs/glossary/fees.mdx create mode 100644 docs/glossary/inflation.mdx create mode 100644 docs/glossary/ledger.mdx create mode 100644 docs/glossary/liquidity-pool.mdx create mode 100644 docs/glossary/lumen-supply.mdx create mode 100644 docs/glossary/metadata.json create mode 100644 docs/glossary/minimum-balance.mdx create mode 100644 docs/glossary/miscellaneous-core-objects.mdx create mode 100644 docs/glossary/multisig.mdx create mode 100644 docs/glossary/muxed-accounts.mdx create mode 100644 docs/glossary/network-passphrase.mdx create mode 100644 docs/glossary/operations.mdx create mode 100644 docs/glossary/scp.mdx create mode 100644 docs/glossary/sponsored-reserves.mdx create mode 100644 docs/glossary/testnet.mdx create mode 100644 docs/glossary/transactions.mdx create mode 100644 docs/glossary/versioning.mdx create mode 100644 docs/glossary/xdr.mdx create mode 100644 docs/index.mdx create mode 100644 docs/issuing-assets/anatomy-of-an-asset.mdx create mode 100644 docs/issuing-assets/control-asset-access.mdx create mode 100644 docs/issuing-assets/how-to-issue-an-asset.mdx create mode 100644 docs/issuing-assets/index.mdx create mode 100644 docs/issuing-assets/metadata.json create mode 100644 docs/issuing-assets/publishing-asset-info.mdx create mode 100644 docs/metadata.json create mode 100644 docs/run-api-server/configuring.mdx create mode 100644 docs/run-api-server/index.mdx create mode 100644 docs/run-api-server/ingestion.mdx create mode 100644 docs/run-api-server/installing.mdx create mode 100644 docs/run-api-server/metadata.json create mode 100644 docs/run-api-server/migrating.mdx create mode 100644 docs/run-api-server/monitoring.mdx create mode 100644 docs/run-api-server/prerequisites.mdx create mode 100644 docs/run-api-server/remote-core.mdx create mode 100644 docs/run-api-server/running.mdx create mode 100644 docs/run-api-server/scaling.mdx create mode 100644 docs/run-core-node/commands.mdx create mode 100644 docs/run-core-node/configuring.mdx create mode 100644 docs/run-core-node/index.mdx create mode 100644 docs/run-core-node/installation.mdx create mode 100644 docs/run-core-node/metadata.json create mode 100644 docs/run-core-node/monitoring.mdx create mode 100644 docs/run-core-node/network-upgrades.mdx create mode 100644 docs/run-core-node/prerequisites.mdx create mode 100644 docs/run-core-node/publishing-history-archives.mdx create mode 100644 docs/run-core-node/running-node.mdx create mode 100644 docs/run-core-node/tier-1-orgs.mdx create mode 100644 docs/software-and-sdks/index.mdx create mode 100644 docs/software-and-sdks/metadata.json create mode 100644 docs/start/introduction.mdx create mode 100644 docs/start/list-of-operations.mdx create mode 100644 docs/start/metadata.json create mode 100644 docs/start/stellar-stack.mdx create mode 100644 docs/tutorials/create-account.mdx create mode 100644 docs/tutorials/follow-received-payments.mdx create mode 100644 docs/tutorials/handling-errors.mdx create mode 100644 docs/tutorials/metadata.json create mode 100644 docs/tutorials/moneygram-access-integration-guide.mdx create mode 100644 docs/tutorials/securing-projects.mdx create mode 100644 docs/tutorials/send-and-receive-payments.mdx create mode 100644 docs/web-assets/SEP24-state-diagram.png create mode 100644 docs/web-assets/anchor-validation-suite.png create mode 100644 docs/web-assets/demo-client.png create mode 100644 docs/web-assets/demo-wallet-sep31.png create mode 100644 docs/web-assets/demo_wallet_sep24dep.png create mode 100644 docs/web-assets/first-deposit-anchor-flow.png create mode 100644 docs/web-assets/first-deposit-claimable-balance-flow.png create mode 100644 docs/web-assets/first-deposit-wallet-flow.png create mode 100644 docs/web-assets/horizon-scaling/Topology-1VM.png create mode 100644 docs/web-assets/horizon-scaling/Topology-2VMs.png create mode 100644 docs/web-assets/horizon-scaling/Topology-3VMs.png create mode 100644 docs/web-assets/horizon-scaling/Topology-Enterprise-HotBackup.png create mode 100644 docs/web-assets/horizon-scaling/Topology-Enterprise.png create mode 100644 docs/web-assets/orderbook.png create mode 100644 docs/web-assets/polaris.png create mode 100644 docs/web-assets/ready-to-rock.png create mode 100644 docs/web-assets/select-currency.png create mode 100644 docs/web-assets/stellar-wallet-file.png create mode 100644 docs/web-assets/wallet-mgi-architecture-1.png create mode 100644 docs/web-assets/wallet-mgi-architecture-2.png create mode 100644 docusaurus.config.js create mode 100644 package.json create mode 100644 sidebars.js create mode 100644 src/components/Alert.js create mode 100644 src/components/CodeExample.js create mode 100644 src/components/ReadMore.js create mode 100644 src/css/components/_doc-page.scss create mode 100644 src/css/components/_footer.scss create mode 100644 src/css/components/_navbar.scss create mode 100644 src/css/components/_sidebar.scss create mode 100644 src/css/components/_toc.scss create mode 100644 src/css/custom.scss create mode 100644 src/css/variables.scss create mode 100644 static/.nojekyll create mode 100644 static/img/favicon-96x96.png create mode 100644 static/img/favicon.ico create mode 100644 static/img/logo.svg create mode 100644 static/img/stellar-logo-dark.svg create mode 100644 static/img/stellar-logo.svg create mode 100644 static/robots.txt create mode 100644 tsconfig.json create mode 100644 yarn.lock diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000..d67cbb1f5 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,10 @@ +dist +node_modules +docs +.github +build +/*.js +/*.ts +/*.d.ts +static +.docusaurus \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 000000000..109acc337 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + extends: ["@stellar/eslint-config"], + rules: { + "import/no-unresolved": 0, + }, +}; diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 000000000..cc432896d --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,32 @@ +name: Format MDX and Lint + +on: + pull_request: + branches: + - main + + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout App Repo + uses: actions/checkout@v3 + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: '16' + cache: 'yarn' + cache-dependency-path: '**/yarn.lock' + + - name: Install App Dependencies + run: yarn --prefer-offline + + - name: Format MDX with Prettier + run: yarn check:mdx + + - name: Lint with ESLint + run: yarn lint:fix diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..44e68351d --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +# Dependencies +node_modules + +# Production +/build + +# Generated files +.docusaurus +.cache-loader + +# Misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 000000000..1fa059776 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,13 @@ +module.exports = { + ...require("@stellar/prettier-config"), + // This is mostly content, and prose wrap has a way of exploding markdown + // diffs. Override the default for a better experience. + overrides: [ + { + files: "*.mdx", + options: { + proseWrap: "never", + }, + }, + ], +}; \ No newline at end of file diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 000000000..e00595dae --- /dev/null +++ b/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: [require.resolve('@docusaurus/core/lib/babel/preset')], +}; diff --git a/docs/anchoring-assets/enabling-cross-border-payments/index.mdx b/docs/anchoring-assets/enabling-cross-border-payments/index.mdx new file mode 100644 index 000000000..b3b212f50 --- /dev/null +++ b/docs/anchoring-assets/enabling-cross-border-payments/index.mdx @@ -0,0 +1,49 @@ +--- +title: Overview +order: 10 +--- + +import { Alert } from "@site/src/components/Alert"; + +Supporting cross-border payments of an asset through the Stellar network requires cooperation between a sending anchor (**SA**) and receiving anchor (**RA**). In this section, we'll only go over the steps necessary for building a SEP-31 receiving anchor, but documentation for building a sending anchor is in development. + +Specifically, we will go over the SDF's [Tools and References](./reference-implementations.mdx) as well as all three stages of the development process: + +1. [Setting up a test server](./setting-up-test-server.mdx) +1. [Setting up a production server](./setting-up-production-server.mdx) +1. [Launching](./launch.mdx) + +## The Basic User Experience + +Unlike with SEP-24, SEP-31 involves a sending user (**SU**) and receiving user (**RU**), so both users' experience is described [here](../../anchoring-assets/enabling-cross-border-payments/setting-up-test-server/#stellar-docsearch-form). + + + +Note: **SA**'s are not necessarily wallet applications. This means the **SU** may not interact with a mobile application, but could go to a website or physical remittance location instead. Obviously, this affects the **SU** experience. + + + +Generally, the experience of the SU and RU would look something like this: + +1. The **SU** opens an app, website, or goes to a physical remittance location +1. The **SU** specifies the asset and amount to send, and the asset that should be received +1. The **SA** finds the appropriate **RA** for the asset, authenticates with the **RA**, and parses the **RA**'s requirements for sending payments +1. The **SA** collects both the **SU** and **RU** KYC information requested by the **RA** from the **SU**, as well as the transaction information + - **RU**'s should not have to take any action to receive a cross-border payment + - However, it is possible to require action from the **RU** if absolutely necessary +1. The **SA** registers the **RU** and potentially **SU** with the **RA** via [SEP-12](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0012.md) +1. Once the **RA** is ready to receive the payment, the **SA** makes the Stellar path payment to the **RA** using the **SU** and **RU** assets +1. Once the **RA** receives the path payment, it attempts make the associated off-chain transfer (usually bank transfer) using the KYC and transaction information sent by the **SA** +1. If the transfer to the **RU** fails, the **SU** will be notified by the **SA** to update the invalid or missing fields, and the **RA** will retry once the **SA** sends the updated info. + +The **RU** should then receive the funds sent by the **SA** once the **RA** has the necessary to make the final transfer. + + + +Note: The final off-chain transfer by the **RA** may not be a bank transfer. For example, the destination could be a mobile money account managed by the **RA**. The methods used to transfer the payment off-chain to the **RU** is up to the **RA**, and **SA**s should be aware of the off-chain transfer methods of their partner **RA**s. + + + +## Interoperability and Stellar Ecosystem Proposals + +As mentioned, this section will outline how to implement a [SEP-31](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0031.md) receiver, but these servers require [SEP-1](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0001.md), [10](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md), and [12](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0012.md) to be implemented as well. SEP-1 is described in [this guide](../../issuing-assets/publishing-asset-info.mdx), and SEP-10 & 12 are described [here](../../anchoring-assets/enabling-cross-border-payments/setting-up-test-server.mdx#authentication). diff --git a/docs/anchoring-assets/enabling-cross-border-payments/launch.mdx b/docs/anchoring-assets/enabling-cross-border-payments/launch.mdx new file mode 100644 index 000000000..ed5b34812 --- /dev/null +++ b/docs/anchoring-assets/enabling-cross-border-payments/launch.mdx @@ -0,0 +1,49 @@ +--- +title: Launch +order: 50 +--- + +Once the testnet and mainnet servers are deployed, KYC collection conforms to regulations, banking rails are integrated, and the whole system is audited, it's time to prepare for launch. + +## Market Making + +It is critical for an anchored asset to have liquid trading markets with XLM and ideally partner **SA** assets. Without a deep orderbook, its impossible to facilitate path payments at rates competivtive with equivalent off-chain fiat assets. + +The SDF maintains a trading bot called [Kelp](https://kelpbot.io/) that supports many trading and market making strategies out of the box, all of which are described in [Kelp's Github Repo](https://github.com/stellar/kelp#kelp). You can use Kelp for free and start automating the order placement process in a matter of minutes. + +Once the order placement is automatic, you should track the health of your token's market with these two parameters: + +- Bid/Ask Amounts: Both sides of the order book should have at least 25k USD +- Spread Size: The average price of the bids and asks should be within 4% of each other. + +The 25k USD value is relative to a new anchor that is still developing its business. Once the volume of traded assets (and the total supply) grows, this value should be increased. + +## Creating an Anchor Website + +A website allows wallets and consumers to find information about your team and your business, get in contact with questions and feedback, and understand how your service works. Your issuing account should link to that website so there's a clear connection between the Stellar asset and the URL. To do that, usee the `set options`, and choose the URL as the home domain. You can find more information about how that process works [in our guide to publishing asset information](../../issuing-assets/publishing-asset-info.mdx). + +Here is a list of requirements to make sure users have a great experience navigating through your website: + +### Branding Visuals + +Users should feel that they are in the company's domain based on the branding of the webpage. The brand universe can include many aspects, such as: company logo or symbol, slogan, colors, fonts, icons, etc... + +### Company Description + +Users want to know who you are and what you do before they'll trust your service or hold your asset. Your website should describe your business, clarify your mission, and outline your vision. Giving more information about your company's executive team, or about your team in general, is a great way to increase users' confidence, and conversions. + +Describing how are you integrated with the Stellar Network (and with which wallets) is also a great way to show users that the system is reliable, and that it conforms to the best protocols and practices of the financial industry. + +Finally, make sure to describe what your asset represents, how it's backed (e.g. it's 1:1 with a fiat reserve), how users can redeem it, and who can use it (anyone, just for retail, etc). + +### Customer Support + +Your webpage is also where users will go to report problems and reach out with questions and feedback. Make sure you clearly describe the process for contacting you, and for reporting bugs, and have a plan in place to offer necessary support.Adding a FAQ section once is a great way to defray support tickets and get information to users faster. + +You should also have a contact email so customers can get in touch, report inconsistencies, and open tickets. Once you get those tickets, you need to answer them in a reasonable amount of time. + +## Connecting to Sending Anchors + +All **RA**s must connect with **SA**s that have transaction volume to send. The SDF works with strategic partners to build and improve these relationships, and the ultimate goal is for there to be an entire ecosystem (or network) of **SA**s and **RA**s globally. + +Stellar.org does not yet host any resources documenting existing SEP-31 anchors, but it is coming. diff --git a/docs/anchoring-assets/enabling-cross-border-payments/metadata.json b/docs/anchoring-assets/enabling-cross-border-payments/metadata.json new file mode 100644 index 000000000..9209772f9 --- /dev/null +++ b/docs/anchoring-assets/enabling-cross-border-payments/metadata.json @@ -0,0 +1,4 @@ +{ + "title": "Enable Cross-border Payments", + "order": 30 +} diff --git a/docs/anchoring-assets/enabling-cross-border-payments/reference-implementations.mdx b/docs/anchoring-assets/enabling-cross-border-payments/reference-implementations.mdx new file mode 100644 index 000000000..bbaf301bf --- /dev/null +++ b/docs/anchoring-assets/enabling-cross-border-payments/reference-implementations.mdx @@ -0,0 +1,34 @@ +--- +title: Tools and References +order: 20 +--- + +These docs walk through the steps necessary to set up and launch both a test server and a production server for SEP-31 cross-border payments, but before you get any deeper, it's worth noting: you don't have to start from scratch. SDF offers some tools that make it easy to implement those servers, and test them from the sending anchor (**SA**) side. + +## Anchor Server Reference Implementation + +To help with server setup, SDF created [Polaris](https://github.com/stellar/django-polaris), an extendable django reusable-app that comes with fully-implemented endpoints, templates, and database models. It's built in Python using the community-supported [Stellar Python SDK](https://github.com/StellarCN/py-stellar-base), and is compliant with the Stellar Ecosystem Protocols mentioned in the previous section. + +TODO: REPLACE PNG ![Screenshots of the reference implementation](../../web-assets/polaris.png) + +Polaris modularizes the parts of the codebase that interface with the Stellar network, and provides clear methods for developers to integrate their own customer registrations, banking connections, and KYC and transcation processing. That means you can focus on implementing the business- and country-related aspects of your product without having to spend much time defining how to connect the server to the Stellar Network. + +If you choose to use Polaris, you can jumpstart your deployment, and connect local rails to Stellar in weeks instead of months. + +## Demo Client & Deployed Example + +Once you have a server set up, SDF also maintains a [Demo Wallet Project](https://demo-wallet.stellar.org/) that makes it easy to test your implementation on both the testnet and the pubnet. + +This allows you to run through your receiving anchor's flow by entering information to a fake wallet application like a real sending user would. + +![Screenshot of the demo client](../../web-assets/demo-wallet-sep31.png) + +With the demo client, you can test your payment flows, and get information about the transactions that are created during those processes. It gives a clear step-by-step visual example of how the functionalities work along with insightful information about the requests, protocols, and possible validation messages. To start testing the deploys, simply update the settings of the Demo Client (click the gear button on the bottom right) to include your project's information. + +## Anchor Validation Suite + +The Anchor Validation Suite is a set of tests that helps confirming if your anchor implementation is compliant with the latest specifications of the SEP-31 standard. This is a great resource for people that are getting started building an anchor and want to check what's still missing in their implementation, or for teams that have already finalized the anchor development and want an extra validation on the codebase. Also, the Anchor Validation Suite doesn't replace the necessity of having a professional security auditor reviewing the whole project. + +TODO: REPLACE PNG ![Screenshot of the Anchor Validation Suite's UI](../../web-assets/anchor-validation-suite.png) + +The Anchor Validation Suite codebase is available in an open-sourced [Github Repository](https://github.com/stellar/transfer-server-validator/), where you can check in details exactly how all tests work. SDF also maintains a [deployed version of the Validation Suite](http://anchor-validator.stellar.org/) with a UI for running the tests without the need to setup your own infrastructure. In order to also test the interactive parts of SEP-24, you'll need to add test values to all the required interactive flow fields. The [Github Repository Readme file](https://github.com/stellar/transfer-server-validator/#providing-field-values) has more information on how exactly those fields need to be added. Finally, you can also trigger the tests directly from a Continuous Integration tool (like CircleCI or Jenkins), in order to have continuous monitoring of your anchor infrastructure. diff --git a/docs/anchoring-assets/enabling-cross-border-payments/setting-up-production-server.mdx b/docs/anchoring-assets/enabling-cross-border-payments/setting-up-production-server.mdx new file mode 100644 index 000000000..c4c74ec85 --- /dev/null +++ b/docs/anchoring-assets/enabling-cross-border-payments/setting-up-production-server.mdx @@ -0,0 +1,62 @@ +--- +title: Deploy a Production Version on Mainnet +order: 40 +--- + +import { Alert } from "@site/src/components/Alert"; + +Once the test server is live and you have tested the payment flow with a **SA**, it's time to deploy a productionized version of the test server on mainnet. + +The **RA** server code deployed to each environment should be the same, and additional functionality offered in production should be enabled via environment variables. This is called _code parity_, which significantly reduces operational and deployment complexity. It also allows **RA**s to use the Testnet service as a staging environment for debugging or new features and allows the **SA**s to test their client code before integrating with the **RA**'s mainnet service. + +## Deploying a Secure Environment + +Make sure to keep the test server up, and deploy the production (mainnet) system in a separate environment. Having two deployments allows you to validate new features on the testnet before deploying to production. + +To switch to Stellar's public (mainnet) network, all you have to do is change the network [passphrase](../../glossary/network-passphrase.mdx) (for authenticating requests) and the [Horizon URL](https://horizon.stellar.org/). Stellar SDKs have all the built-in logic to switch from test to public with minor changes to the codebase. + +If you're issuing your own asset in addition of offering an on/off ramp, make sure you follow [best practices for asset issuance](../../issuing-assets/how-to-issue-an-asset.mdx) by creating a distribution account that is separate from the issuing account before distributing real assets on the public network. The issuing account secret key should be highly secured via means such as cold storage or multi-signature, and should only be used to send (and, consequently, issue) assets to the distribution account. The distribution account will be used by the **RA** server to receive assets from other accounts programmatically. + +Since the distribution account will be managed programmatically, it should only have a balance necessary to keep operations running for a short period of time. The issuing account should send small amounts of an asset to the distribution account frequently to represent the expected deposit volume for a given period. You should set up the transfer from the issuing account to the distribution account once day (or once a week week) to make sure the distribution account only has the minimum necessary balance to keep the operation functional in that short period. + +## Validating Real KYC + +Most anchors need to collect [Know Your Customer](https://en.wikipedia.org/wiki/Know_your_customer) information to comply with local regulations before honoring payments. In SEP-31, the **SA** controls the UX for the **SU**. Typically, the **SA** will offer an online or in-app UX, but physical remittance locations are also acceptable. Its also important for **SA**s to be able to contact **SU**s when information must be updated or when the transaction succeeded. + +How the **RA** complies with their local regulations by validating KYC information is up the **RA**: there are many services that provide KYC solutions through APIs that validate the input data against governmental databases to verify requirements. Each jurisdiction has specific KYC requirements, and they differ from jurisdiction to jurisdiction, so it's best to find a country-specific KYC provider that meets your needs. Many **RA** organizations can validiate the KYC data collected without using a third-party service. + +Some countries require different KYC fields depending on the payment amount. If that's the case in an **RA**'s jurisdiction, the **RA** needs to expect and support different KYC `type` values in `GET /customer` requests. Only one `type` value can be specified in the `/info` response per-customer, so a different large-payment `type` value will have to be documented and communicated to the **RA**'s partner **SA**s outside the SEP. + +KYC information should be linked to the `id` returned by the `PUT /customer` calls, so you only need to ask the **SU** for it once as long as the data provided is valid. + + + +Note: The SDF encourages anchors to have the same KYC collection process on the Testnet server and Mainnet server. However, _validation_ of KYC data on Testnet is unnecessary and prevents third parties from testing your service. KYC validation should only be supported on Mainnet unless the field uses a well-known format like email addresses. + + + +## Connecting to Real Banking Rails + +Fiat-backed token anchors are expected to manage a full reserve. That means there's a 1:1 relationship between Stellar-network tokens and money in the bank. Since each fiat token on Stellar is backed by, and can be redeemed for, an underlying, real-world asset, anchors of fiat-backed tokens need to connect to real banking rails to make payments to receiving users (generally through bank transfers). This applies to both issuing and non-issuing anchors. + +Once Stellar payment transactions have been detected and matched to an internal transaction record using the memo of the payment, **RA**s must attempt the off-chain payment (typically using a bank transfer). + +Make sure to do a full security audit on your systems when banking rails connections are in place. Some banks provide a testing API that can be used for development and deployment to testnet or staging environments, which means you can test and audit the codebase before moving to a final production-ready bank integration. + +## Testing + +Once your application is fully functional, it's a good idea to test different scenarios and edge cases to make sure the system is behaving as expected. + +The SDF offers an [anchor validation test suite](https://anchor-validator.stellar.org/) and UI for testnet and mainnet testing. SEP-24 and SEP-31 are supported, containing a growing list of tests to assess your service's compliance with the protocol. While the coverage is good, the SDF does not claim that it is complete. + +Below are some tests that the suite does not and will not cover. + +### General Tests + +- Check that the issuing account is set up with the correct home domain, and that the website on that home domain has branding visuals, content about your company, clear language telling users how to report problems, and a method for contacting you to seek support. + +### Security Tests + +- Ensure distribution is done through a dedicated distribution account (not the issuing account) +- Ensure market making is done through a dedicated account (not the issuing account) +- Make sure all authenticated endpoints are not accessible without the JWT token diff --git a/docs/anchoring-assets/enabling-cross-border-payments/setting-up-test-server.mdx b/docs/anchoring-assets/enabling-cross-border-payments/setting-up-test-server.mdx new file mode 100644 index 000000000..014d292f1 --- /dev/null +++ b/docs/anchoring-assets/enabling-cross-border-payments/setting-up-test-server.mdx @@ -0,0 +1,317 @@ +--- +title: Build a SEP-31 Anchor on Testnet +order: 30 +--- + +import { CodeExample } from "@site/src/components/CodeExample"; +import { Alert } from "@site/src/components/Alert"; + +Before setting up a server that connects to live banking rails and real money, you should build out a test rig connected to the [Stellar test network](../../glossary/testnet.mdx). The testnet works just like the main Stellar network, but it uses test data and allows you to fund test accounts for free using a tool called [Friendbot](../../glossary/testnet.mdx#friendbot). + + + +Note: the testnet is reset every three months, so when building on it, make sure you have a plan to recreate necessary accounts and other data. For more info, check out the [best practices for using the testnet](../../glossary/testnet.mdx#best-practices-for-using-testnet). + + + +At the end of this section, you should have a sandboxed system capable of interfacing with the Stellar testnet that you can easily convert into a production-ready deployment on the main Stellar network. You should always keep a testnet deployment up and running — even once you have a production version — so that wallets can test your implementation without the risk of losing real user funds. + +You can also find an outline of the basic steps anchors need to complete in the [intro to SEP-31](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0031.md#payment-flow) section of the cross-border payment standard. + +## Prerequisites + +Before you start building infrastructure to connect a Stellar-network token to banking rails, you need to: + +1. Issue an asset on the network – which you can find out how to do in the [Issue Assets](../../issuing-assets/index.mdx/) section — or find another trustworthy issuer and coordinate with them to broker their asset. +1. Add meta-information about the asset (if you issue one) and define the location of your `DIRECT_PAYMENT_SERVER` in your `stellar.toml` so that wallets know where to find the server and relevant endpoints. You can consult [this guide](../../issuing-assets/index.mdx/) for help completing your `stellar.toml`. + +Once you've taken those steps, you're ready to implement an `/info` endpoint. + +## Implementing the `/info` Endpoint + +The `/info` endpoint allows receiving anchors to communicate basic information to sending anchors, exchange interfaces, and other Stellar apps. It responds to client queries with a JSON object detailing which currencies the anchor supports and specifically what pieces of information are required for the sender to collect and provide to the receiver in future requests. + +For example, the SDF's reference server has the following `/info` response: + + + +```json +{ + "receive": { + "SRT": { + "enabled": true, + "min_amount": 10.0, + "max_amount": 10000.0, + "sender_sep12_type": "sep31-sender", + "receiver_sep12_type": "sep31-receiver", + "fee_fixed": 1.0, + "fee_percent": 0.01, + "fields": { + "transaction": { + "routing_number": { + "description": "routing number of the destination bank account" + }, + "account_number": { + "description": "bank account number of the destination" + } + } + } + } + } +} +``` + + + +It has a single `SRT` asset enabled with minimum and maximum amounts and both fixed rate and percetage fees taken from the amount received. If the receiving anchor has a complex fee structure that can't be expressed with the fee attributes from `/info`, sending anchors can support a `/fee` endpoint. Unfortunately, this endpoint is a pending change to the SEP that has not yet been approved. An update will be made when the `/fee` endpoint specification is finalized. + +The `sender_sep12_type` and `receiver_sep12_type` keys signal to the to the **SA** application that [SEP-12](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0012.md) customer registration is required for both the **SU** and **RU**. The values of these attributes are the values the **SA** should include in the `GET /customer?type=` request for each user. In this context, users are synonymous with customers. + +The `fields` object contains the single `transaction` key-value pair, which contains the custom fields the **RA** requires from the **SA**. Typically, these attributes are off-chain rails-related attributes like routing and banking numbers. **SA**s should be aware of their partner **RA**'s `fields.transaction` required attributes and should ideally validate the collected information from the **SU** prior to making the `POST /transaction` API call on the **RA**'s server. + + + +Note: Generally, **RA**s should require the KYC fields of the **SU** and **RU** to be sent via SEP-12 and per-transaction information via SEP-31's `POST /transaction` endpoint in the `forms.transactions` object. Be aware that **SU**s must be able to understand what fields to collect for your service and ideally how to validate. Vauge descriptions or poorly named attributes will cause confusion. + + + +## Authentication + +Stellar Web Authentication ([SEP-10](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md)) is a protocol for verifying that a user controls a Stellar private key for a given account, and for creating a persistent session for that user. It relies on a variation of mutual challenge-response, and uses Stellar transactions to encode challenges and responses: the auth server provides a 'challenge' transaction; the client signs it on behalf of the user and returns it to the anchor; the anchor checks that the signature is valid, and if it is, issues a JWT. + +In the context of SEP-31, the client _and_ key holder is the sending anchor (**SA**). Receiving anchors (**RA**s) should have a list of approved sending anchors and their public keys used in SEP-10 authentication. If an authentication request is made with an unknown Stellar public key address, the receiving anchor should reject the request. + +Once received, the JWT then acts as a reusable key for the **SA** to perform an action on behalf of a **SU**. It can contain an arbitrary amount of information, and is signed by the provider — in this case the asset anchor — to ensure validity. + +### Requesting a Challenge + +To start the authentication flow, the client requests a challenge, which is a Stellar transaction with the sequence number set to 0. A transaction with a sequence number of 0 is, by definition, invalid, so it can't actually be submitted to the network. The issuer can, however, verify that the transaction is signed correctly (which is what happens in the next step). + +The only information needed to request a challenge is the client's public key, passed as the `account` parameter. Here's an example: `GET ?account=GXXXXXX` + +### Exchanging the Signed Challenge + +Once the client signs the challenge transaction on behalf of the user using standard [Stellar SDK tools](../../software-and-sdks/index.mdx), it sends the signed transaction back to the provider. Using those same Stellar SDKs, the provider then checks to see if the transaction is properly signed, and if it is, offers the client a JWT. + +This JWT should be created with the claims that are appropriate given the account that signed the challenge. It can be created with any existing JWT library. + +Here are the fields included in the JWT: + +- The `sub` key contains the account of the authenticated user +- The `exp` key contains the expiration of the JWT. Some JWTs should be short-lived if the claims inside of it are expected to change, or if the JWT is used in less secure environments +- Other keys can contain any type of claim or data the Anchor wishes. + +Since the `sub` field contains the address of the authenticated user, it's kind of like a username. The JWT authenticates said user. + +Since some tokens are used in less secure environments, such as as query parameters in URLs, you may want to create a short-lived or one-time use token to prevent a JWT from falling into the wrong hands. + +## Customer Registration + +Once you have implemented an `/info` endpoint and set up SEP-10 authentication, your next step is to build out [SEP-12](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0012.md). This SEP is a standard for communicating user KYC information between clients and servers running Stellar services. + +In the context of SEP-31, the **SA** collects the information the **RA** specifies as required in the response to `GET /customer?type=` and makes a `PUT /customer` request containing that information back to the **RA**. + +SEP-12 uses the standardized fields defined in [SEP-9](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0009.md), so **SA**s should be familiar with the descriptions and formats of the fields used by the **RA**. + +On testnet, KYC validation is discouraged in order to lower the barrier for other to test your service. However, if information passed to an **RA** turns out to be invalid after attempting to validate it on mainnet, specific customers (**SU**s or **RU**s) can be placed in a `pending_customer_info_update` status. + +This signals to the **SA** that the fields should be updated by the **SU** and resent. The problematic fields can be identified using the `GET /customer?id=` API call. These fields should be included in the future `PUT /customer` request. + +**RA**s should also respect the `DELETE /customer` requests made by the **SA**. + + + +Note: Ideally, **RA**s validate KYC data passed by **SA**s during the request/response cycle instead of reporting KYC issues via the transaction's status later in the process. + + + +**SA**s should poll the `GET /transactions/:id` endpoint(s) provided by their partner **RA**(s) and ensure the transactions initiated are always carried out to completion by fetching updated info from the **SU** when necessary. + +The two diagram below outline a potential flow between a **SA** and **RA** including all possible SEP-12 states: + +```mermaid +sequenceDiagram + participant SU + participant SA Client + participant RA Server + note right of SA Client: After SEP-10 auth + SA Client->>RA Server: [SEP-31 GET /info] + activate RA Server + RA Server-->>SA Client: sender_sep12_type, receiver_sep12_type + deactivate RA Server + SA Client->>RA Server: [SEP-12 GET /customer?type={sender_sep12_type value}] + activate RA Server + RA Server-->>SA Client: NEEDS_INFO, fields + deactivate RA Server + SA Client->>RA Server: [SEP-12 GET /customer?type={receiver_sep12_type value}] + activate RA Server + RA Server-->>SA Client: NEEDS_INFO, fields + deactivate RA Server + SA Client->>SU: Requests fields returned from [SEP-12 GET]s + activate SU + SU-->>SA Client: Provides field values + deactivate SU + note right of SA Client: Register SU KYC + SA Client->>RA Server: [SEP-12 PUT /customer] + activate RA Server + RA Server-->>SA Client: ID, 202 Accepted + deactivate RA Server + note right of SA Client: Register RU KYC + SA Client->>RA Server: [SEP-12 PUT /customer] + activate RA Server + RA Server-->>SA Client: 400 Bad Request + deactivate RA Server + SA Client->>SU: Requests field(s) returned from [SEP-12 GET receiver] + activate SU + SU-->>SA Client: Provides field values + deactivate SU + note right of SA Client: Retry RU registration + SA Client->>RA Server: [SEP-12 PUT /customer] + activate RA Server + RA Server-->>SA Client: ID, 202 Accepted + deactivate RA Server +``` + +Now that both the **SU** and **RU** are registered with the **RA**, the **SA** must initiate the transaction request with the **RA**. This process is described in the section below. Assuming the **SA** made a successful `POST /transaction` request after collecting the KYC and transaction info, a potential update-flow could look something this: + +```mermaid +sequenceDiagram + participant SU + participant SA Client + participant RA Server + participant RU + note right of RA Server: After [SEP-31 POST /transaction] + RA Server->>RU: [SEP-31 off-chain transfer attempt] + activate RU + RU-->>RA Server: transfer failed due to KYC field X, Y + deactivate RU + note right of SA Client: After failed transfer attempt + SA Client->>RA Server: [SEP-31 GET /transaction/:id] + activate RA Server + RA Server-->>SA Client: receiver pending_customer_info_update + deactivate RA Server + SA Client->>RA Server: [SEP-12 GET /customer?id={receiver ID}] + activate RA Server + RA Server-->>SA Client: NEEDS_INFO, fields + deactivate RA Server + SA Client->>SU: Requests updated field values + activate SU + SU-->>SA Client: Provides updated values + deactivate SU + SA Client->>RA Server: [SEP-12 PUT /customer] + activate RA Server + RA Server-->>SA Client: ID, 202 Accepted + deactivate RA Server + RA Server->>RU: [SEP-31 off-chain transfer retry] + activate RU + RU-->>RA Server: transfer successful! + note right of SA Client: Detect success & notify SU + SA Client->>RA Server: [SEP-31 GET /transactions/:id] + activate RA Server + RA Server-->>SA Client: completed + deactivate RA Server + SA Client->>SU: transfer successful! +``` + +
+ + + +Note: It is perfectly acceptable for **RA**s to only require KYC information on the **RU**. To do this, simply omit the `sender_sep12_type` key from the `GET /info` response. This signals to the **SA** to make one `GET` and `PUT` for receiver fields and registration, respectively. + + + +## Transaction Processing + +After registering the receiving and potentially sending user, _but prior to any update flow_, the **SA** must initiate the transaction creation process by making a `POST /transactions` request including the `receiver_id` (and `sender_id`) as well as the `fields.transaction` object containing the per-transaction level fields defined by the **RA** in `/info`. + +On a successful response, the **SA** then makes the path payment on Stellar between the deposited asset from the **SU** to the asset anchored by **RA**. The transaction _must_ contain a memo of type `stellar_memo_type` with a value of `stellar_memo`. These attributes are returned in the **RA**'s `POST /transactions` response. + + + +Note: the payment transaction _can_ be a normal strict-receive payment transaction instead of a path payment. Typically, cross-border payments will involve a cross-currency transaction. + + + +The **RA** should be polling or streaming incoming payment transactions on their Stellar distribution account. Once detected and matched with the transaction record created by the **SA**'s `POST /transactions` request, the **RA** will attempt to transfer the value received off-chain to the associated **RU**. + +The following diagram outlines the process for a successful transaction initiation request and an update flow that differs from the SEP-12 `pending_customer_info_update` process. Most of the time, **SA**s will not have to collect and provide updated transaction information. This is only in the case of data entry error by the **SU**. + + + +Note: If the **SU** and **RU** are already registered, only the per-transaction information defined in the `GET /info` `forms.transaction` object needs to be collected. If SEP-12 registration has not happened or an update is required, it is recommended to collect both KYC and per-transaction field values from the **SU** at the same time. + + +
+ +Due to the separate documentation sections outlining the KYC and transaction API calls, we'll ask the **SU** for information a second time for per-transaction attributes. + +```mermaid +sequenceDiagram + participant SU + participant SA Client + participant Horizon + participant RA Server + participant RU + note right of SA Client: After customer registration + SA Client->>RA Server: [SEP-31 GET /info] + activate RA Server + RA Server-->>SA Client: fields.transaction descriptions + deactivate RA Server + SA Client->>SU: Request the transaction fields + activate SU + SU-->>SA Client: Provide transaction fields + deactivate SU + note right of SA Client: Make path payment + SA Client->>Horizon: Stellar path payment including memo + Horizon->>RA Server: Stream and match path payment + RA Server->>RU: Off-chain transfer attempt + activate RU + RU-->RA Server: transfer failed due to TX fields X, Y + deactivate RU + SA Client->>RA Server: [SEP-31 GET /transactions/:id] + activate RA Server + RA Server-->>SA Client: pending_transaction_info_update, required_info_updates + deactivate RA Server + note left of SA Client: recollect required_info_updates fields + SA Client->>SU: Request valid transaction X, Y values + activate SU + SU-->SA Client: Provide valid X, Y + deactivate SU + SA Client->>RA Server: [SEP-31 PATCH /transactions/:id] + activate RA Server + RA Server-->>SA Client: 200 Success + deactivate RA Server + RA Server->>RU: Off-chain transfer retry attempt + activate RU + RU-->>RA Server: transfer success! + deactivate RU + note right of SA Client: Detect and notify SU + SA Client->>RA Server: [GET /transactions/:id] + activate RA Server + RA Server-->SA Client: pending_external + deactivate RA Server + note right of SA Client: After RA rails completion + SA Client->>RA Server: [GET /transactions/:id] + activate RA Server + RA Server-->SA Client: completed + deactivate RA Server + SA Client->>SU: transaction success! +``` + +
+ +### Transactions Endpoint + +As demonstrated in the diagram from the previous section, the `/transactions` endpoint provides a way for **SA**s to initiate (`POST`), poll (`GET :id`), and update (`PATCH :id`) transactions with an **RA**. + +Check out the [SEP-31 standard](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0031.md) for the API request and response specifications for the `/transactions` endpoint. + +## Streaming Incoming Path Payments + +Receving anchors (**RA**s) must be able to detect and match incoming Stellar path payment transactions with receiving users. This can be done one of two ways: + +- Polling: this option consists of periodically fetching the transactions submitted _after_ an already-seen transaction's `paging_id` that should be updated after each iteration. +- Streaming: this option consists of maintaining a persistent process that streams incoming transactions from the anchored asset's distribution account, which allows detection and transaction matching in close to real time. + +For more information on path payment transactions, checkout the [operation definition](https://developers.stellar.org/api/resources/operations/object/path-payment-strict-receive/). diff --git a/docs/anchoring-assets/enabling-deposit-and-withdrawal/index.mdx b/docs/anchoring-assets/enabling-deposit-and-withdrawal/index.mdx new file mode 100644 index 000000000..f48cb81ea --- /dev/null +++ b/docs/anchoring-assets/enabling-deposit-and-withdrawal/index.mdx @@ -0,0 +1,34 @@ +--- +title: "SEP-24: Deposits & Withdrawals" +order: 10 +--- + +Supporting deposits and withdrawals of an asset on and off the Stellar network requires cooperation between wallet (client) and anchor (server) applications. In this section, we'll only go over the steps necessary for building a SEP-24 server, but we also have documentation for [building a wallet](../../building-apps/index.mdx). + +Specifically, we will go over SDF's [Tools and References](./reference-implementations.mdx) as well as all three stages of the development process: + +1. [Setting up a test server](./setting-up-test-server.mdx) +1. [Setting up a production server](./setting-up-production-server.mdx) +1. [Launching](./launch.mdx) + +## The Basic User Experience + +The complete customer experience from deposit to withdrawal goes something like this: + +1. The customer opens the SEP-24 wallet application of their choice +1. The customer selects an asset to deposit and the wallet finds an anchor (clients could also chose the specific anchor) +1. Once the wallet authenticates with the anchor, the customer begins entering their KYC and transaction information requested by the anchor +1. The wallet provides instructions, and the customer deposits real fiat currency with the anchor (e.g. makes a bank transfer) +1. Once the wallet receives the deposit, the customer receives the tokenized asset on the Stellar network from the anchor's distribution account + +The customer can then use the digital asset on the Stellar network for remittance, payments, trading, store of value, or another use case not listed here. At some later date, the customer could decide to withdraw their assets from the Stellar network, which would look something like this: + +1. The customer opens their wallet application +1. The customer selects the asset for withdrawal and wallet finds the anchor +1. After authenticating with the anchor, the wallet opens the given interactive URL and allows the customer to enter their transaction information (KYC has already been collected) +1. After asking for customer approval, the wallet sends the specified amount of the customer's asset balance to the anchor's distribution account on Stellar +1. Once the anchor receives the payment, the customer receives the withdrawn funds via bank transfer. + +## Interoperability and Stellar Ecosystem Proposals + +As already mentioned, this guide will focus on the **Interactive Anchor/Wallet Asset Transfer** SEP (aka [SEP-24](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md)), but SEP-24 requires [SEP-1](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0001.md), which links meta-information to organizations and assets (which we go over in detail with [this guide](../../issuing-assets/publishing-asset-info.mdx)), and [SEP-10](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md), which creates authenticated user session. diff --git a/docs/anchoring-assets/enabling-deposit-and-withdrawal/launch.mdx b/docs/anchoring-assets/enabling-deposit-and-withdrawal/launch.mdx new file mode 100644 index 000000000..f5ed1fe80 --- /dev/null +++ b/docs/anchoring-assets/enabling-deposit-and-withdrawal/launch.mdx @@ -0,0 +1,57 @@ +--- +title: Launch +order: 50 +--- + +Once the testnet and mainnet servers are deployed, KYC collection conforms to regulations, banking rails are integrated, and the whole system is audited, it's time to prepare for launch. + +## Polishing and Internationalization + +Supporting two languages (English and the fiat currency country language) allows users to have a seamless experience while navigating through screens, and supports international institutions (like wallets) that need to test the product before starting new integrations. + +You can support multiple languages in your webapp by using the `Accept-Language` parameter from the http request headers to localize the content and allowing users to change that in a simple way (e.g. a flag icon on the top bar). If a specific wallet doesn't send the header parameter, we recommend showing the user a language selection screen in the beginning of the deposit and withdraw processes. Once a user chooses a language, you can store their selection so you only need to ask them once. In addition to localizing text, make sure to check number formatting, dates, etc. + +Having a group of beta testers is a great way to check if there are any edge cases that need polishing, and to confirm that the system is working well with a variety of user inputs. You can beta test using a soft launch stage before you start putting effort into marketing and distribution. Documenting the testing process with screenshots and videos is very helpful for future security audits, and gives new partners and potential users clarity and confidence in the product. + +## Market Making + +Once your asset is available for in - app deposit and withdrawal, its order book will start to fill up with user buy and sell orders. To make sure these orders have liquidity — in other words, to make sure people can actually buy and sell your asset if and when they want to—it's essential to have a healthy market with a small spread and a deep set of orders on both sides. + +SDF maintains a trading bot called [Kelp](https://kelpbot.io/) that supports many trading and market making strategies out of the box, all of which are described in [Kelp's Github Repo](https://github.com/stellar/kelp#kelp). You can use Kelp for free and start automating the order placement process in a matter of minutes. + +Once the order placement is automatic, you should track the health of your token's market with these two parameters: + +- Bid/Ask Amounts: Both sides of the order book should have at least 25k USD +- Spread Size: The average price of the bids and asks should be within 4% of each other. + +The 25k USD value is relative to a new anchor that is still developing its business. Once the volume of traded assets (and the total supply) grows, this value should be increased. + +## Creating an Anchor Website + +A website allows wallets and consumers to find information about your team and your business, get in contact with questions and feedback, and understand how your service works. Your issuing account should link to that website so there's a clear connection between the Stellar asset and the URL. To do that, usee the `set options`, and choose the URL as the home domain. You can find more information about how that process works [in our guide to publishing asset information](../../issuing-assets/publishing-asset-info.mdx). + +Here is a list of requirements to make sure users have a great experience navigating through your website: + +### Branding Visuals + +Users should feel that they are in the company's domain based on the branding of the webpage. The brand universe can include many aspects, such as: company logo or symbol, slogan, colors, fonts, icons, etc... + +### Company Description + +Users want to know who you are and what you do before they'll trust your service or hold your asset. Your website should describe your business, clarify your mission, and outline your vision. Giving more information about your company's executive team, or about your team in general, is a great way to increase users' confidence, and conversions. + +Describing how are you integrated with the Stellar Network (and with which wallets) is also a great way to show users that the system is reliable, and that it conforms to the best protocols and practices of the financial industry. + +Finally, make sure to describe what your asset represents, how it's backed (e.g. it's 1:1 with a fiat reserve), how users can redeem it, and who can use it (anyone, just for retail, etc). + +### Customer Support + +Your webpage is also where users will go to report problems and reach out with questions and feedback. Make sure you clearly describe the process for contacting you, and for reporting bugs, and have a plan in place to offer necessary support.Adding a FAQ section once is a great way to defray support tickets and get information to users faster. + +You should also have a contact email so customers can get in touch, report inconsistencies, and open tickets. Once you get those tickets, you need to answer them in a reasonable amount of time. + +## Connecting to Wallets + +All Anchor user interactions are done through a Wallet (or Exchange), so it's vital for Anchors to be connected to Wallets that have a good market penetration in the region where the business is most focused. Connecting to Wallets is a simple process, since both ends of that integration are already compliant with SEPs. + +Stellar.org maintains a [list of wallets](https://www.stellar.org/ecosystem/projects), many of which currently support SEP-24 Sending them a message with more information on an asset and an issuer account is a great way to start getting some real users to the Anchor. diff --git a/docs/anchoring-assets/enabling-deposit-and-withdrawal/metadata.json b/docs/anchoring-assets/enabling-deposit-and-withdrawal/metadata.json new file mode 100644 index 000000000..13f879bdf --- /dev/null +++ b/docs/anchoring-assets/enabling-deposit-and-withdrawal/metadata.json @@ -0,0 +1,4 @@ +{ + "title": "Enable Deposits and Withdrawals", + "order": 20 +} diff --git a/docs/anchoring-assets/enabling-deposit-and-withdrawal/reference-implementations.mdx b/docs/anchoring-assets/enabling-deposit-and-withdrawal/reference-implementations.mdx new file mode 100644 index 000000000..e96b4892f --- /dev/null +++ b/docs/anchoring-assets/enabling-deposit-and-withdrawal/reference-implementations.mdx @@ -0,0 +1,34 @@ +--- +title: Tools and References +order: 20 +--- + +These docs walk through the steps necessary to set up and launch both a test server and a production server to handle deposits and withdrawals, but before you get any deeper, it's worth noting: you don't have to start from scratch. SDF offers some tools that make it easy to implement those servers, and test them from the client side. + +## Anchor Server Reference Implementation + +To help with server setup, SDF created [Polaris](https://github.com/stellar/django-polaris), an extendable django reusable-app that comes with fully-implemented endpoints, templates, and database models. It's built in Python using the community-supported [Stellar Python SDK](https://github.com/StellarCN/py-stellar-base), and is compliant with the Stellar Ecosystem Protocols mentioned in the previous section. + +![Screenshots of the reference implementation](../../web-assets/polaris.png) + +Polaris modularizes the parts of the codebase that interface with the Stellar network, and provides clear methods for developers to integrate their own deposit and withdrawal forms, KYC process, and banking rails connections. That means you can focus on implementing the business- and country-related aspects of your product without having to spend much time defining how to connect the server to the Stellar Network. + +If you choose to use Polaris, you can jumpstart your deployment, and connect local rails to Stellar in weeks instead of months. + +## Demo Client & Deployed Example + +Once you have a server set up, SDF also maintains a [Demo Wallet Project](https://demo-wallet.stellar.org/) that makes it easy to test your implementation on both the testnet and the pubnet. + +This allows you to run those tests with a UI without needing to set up a new hosting infrastructure for that project. + +![Screenshot of the demo wallet](../../web-assets/demo_wallet_sep24dep.png) + +With the demo client, you can test your deposit and withdraw flows, and get information about the transactions that are created during those processes. It gives a clear step-by-step visual example of how the functionalities work along with insightful information about the requests, protocols, and possible validation messages. To start testing the deploys, simply update the settings of the Demo Client (click the gear button on the bottom right) to include your project's information. + +## Anchor Validation Suite + +The SEP-24 Anchor Validation Suite is a set of tests that helps confirming if your anchor implementation is compliant with the latest specifications of the SEP-24 standard. This is a great resource for people that are getting started building an anchor and want to check what's still missing in their implementation, or for teams that have already finalized the anchor development and want an extra validation on the codebase. Also, the Anchor Validation Suite doesn't replace the necessity of having a professional security auditor reviewing the whole project. + +![Screenshot of the Anchor Validation Suite's UI](../../web-assets/anchor-validation-suite.png) + +The SEP-24 Anchor Validation Suite codebase is available in an open-sourced [Github Repository](https://github.com/stellar/transfer-server-validator/), where you can check in details exactly how all tests work. SDF also maintains a [deployed version of the Validation Suite](http://anchor-validator.stellar.org/) with a UI for running the tests without the need to setup your own infrastructure. In order to also test the interactive parts of SEP-24, you'll need to add test values to all the required interactive flow fields. The [Github Repository Readme file](https://github.com/stellar/transfer-server-validator/#providing-field-values) has more information on how exactly those fields need to be added. Finally, you can also trigger the tests directly from a Continuous Integration tool (like CircleCI or Jenkins), in order to have continuous monitoring of your anchor infrastructure. diff --git a/docs/anchoring-assets/enabling-deposit-and-withdrawal/setting-up-production-server.mdx b/docs/anchoring-assets/enabling-deposit-and-withdrawal/setting-up-production-server.mdx new file mode 100644 index 000000000..04c204f10 --- /dev/null +++ b/docs/anchoring-assets/enabling-deposit-and-withdrawal/setting-up-production-server.mdx @@ -0,0 +1,91 @@ +--- +title: Set Up a Production Server +order: 40 +--- + +Once the test server is live and you have tested both deposit and withdraw flows, it's time to get started with the real deploy connected to real KYC and real banking rails providers. Before using any banking APIs, it's critical that you perform a full security audit on the system to make sure that there aren't any vulnerabilities. + +## Deploying a Secure Environment + +Make sure to keep the test server up, and deploy the production (mainnet) system in a separate environment. Having two deploys allows you to validate new features on the testnet before moving them to the final production deploy. You can also have a third staging environment if there's a big team working on this codebase and/or there will be many pushes to be tested internally before sharing with other institutions. + +To switch to Stellar's public (mainnet) network, all you have to do is change the network [passphrase](../../glossary/network-passphrase.mdx) (for authenticating requests) and [Horizon URL](https://horizon.stellar.org/). Stellar SDKs have all the built-in logic to switch from test to public with minor changes to the codebase. + +If you're issuing your own asset in addition of offering an on/off ramp, make sure you follow [best practices for asset issuance](../../issuing-assets/how-to-issue-an-asset.mdx) by creating a distribution account that is separate from the issuing account before distributing real assets on the public network. The issuing account secret key should be highly secured via means such as cold storage or multi-signature, and should only be used to send (and, consequently, issue) assets to the distribution account. The distribution account will be used by your server to send (and receive) assets to other accounts programmatically. + +Since the distribution account will be managed programmatically, it should only have a balance necessary to keep operations running for a short period of time. The issuing account should send small amounts of an asset to the distribution account frequently to represent the expected deposit volume for a given period. You should set up the transfer from the issuing account to the distribution account once day (or once a week week) to make sure the distribution account only has the minimum necessary balance to keep the operation functional in that short period. + +## Connecting to Real KYC + +Most anchors need to collect [Know Your Customer](https://en.wikipedia.org/wiki/Know_your_customer) information to comply with local regulations before honoring deposits and withdrawals. The KYC flow usually consists of a simple form that gathers relevant information about the user such as name, email, address, age, and government-issued ID number. + +How you handle KYC is up to you: there are many services that provide KYC solutions through APIs and iFrames, and validate the input data and sync with governmental databases to verify requirements. Each jurisdiction has specific KYC requirements, and they differ from jurisdiction to jurisdiction, so it's best to find a country-specific KYC provider that meets your needs. + +Some countries require different KYC fields depending on the amount to be deposited or withdrawn. If that's the case in your jurisdiction and you need to adapt your KYC forms based on the deposit or withdrawal amount, simply add an amount field before the KYC form, and make sure that the KYC fields are updated based on that value. + +KYC information should be linked to the session created through [Stellar Web Authentication](./setting-up-test-server.mdx#authentication) and, consequently, to the user, so you only need to ask the user for it once. After the first KYC flow is complete, a user shouldn't have to input the information again. + +Make sure the errors and validation messages are clear and include instructions for what to do next to ensure a good user experience and increase the KYC conversion rate. You should also localize messages based on the user's language and location. + +## Pre-Filling the KYC Form + +Pre-filling the KYC form is a great way to reduce the friction of getting started using an anchor, and wallets usually provide a set of fields that are commonly used throughout the ecosystem. In summary, the anchor can render the KYC form with the user's values that were previously sent by the wallet in the /transactions/deposit/interactive and /transactions/withdraw/interactive endpoints. All fields from [SEP-9](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0009.md) can be sent by wallets in the previously mentioned endpoints, but the most common are: email, first name, last name and phone number. Also, you should still enable the pre-filled fields to be editable, since the user might have inputted a different name in the Wallet's sign-up process, and could want to edit it before finalizing the Anchor's KYC process. + +## Connecting to Real Banking Rails + +Fiat-backed token issuers are expected to manage a full reserve. That means there's a 1:1 relationship between Stellar-network tokens and money in the bank. Since each fiat token on Stellar is backed by, and can be redeemed for, an underlying, real-world asset, issuers of fiat-backed tokens need to connect to real banking rails to validate user deposits (through bank transfers, credit card payments, etc.) and to complete user withdrawals (generally through bank transfers). If you're an anchor honoring deposits and withdrawals of a token another organization issues, you'll follow a similar process. + +In order to fetch (and identify) a user transfer, issuers usually take one of two approaches: + +- API Polling: this option consists of fetching the bank's API, through a cron job, to check for the updated status of the list of transfers received by (and sent from) the issuer's bank account. Once the system confirms a new transaction and identifies that it relates to a specific deposit, it can send the digital funds to that user's account +- Webhook: not all banking rails support this option, but it's the leanest in terms of back-end logic. In this approach, the bank proactively hits an issuer's endpoint once it receives a new transfer, updating that information on the issuer's database. The issuer can then can match that transaction to an existing in-process deposit, and validate that the user can receive their digital funds + +There are many ways to identify that a specific bank transfer relates to a specific deposit (and, consequently, to a user). Some banks (and countries) have transfer infrastructure that allows the creation of a single bank account per transfer; others require users to add an identification parameter to their transfers. Some banks provide the user ID number in the transaction information so issuers can match that with the information provided in the KYC form. + +Make sure to do a full security audit on your systems when banking rails connections are in place. Some banks provide a testing API that can be used for development and deployment to testnet or staging environments, which means you can test and audit the codebase before moving to a final production-ready bank integration. For better security, some anchors also prefer to add a manual final step before approving withdrawal transfers. In terms of UX, this manual approval is acceptable as long as the wait times align with user expectations, which usually means they aren't longer than a couple of hours. + +## Testing Edge Cases + +Once your application is fully functional, it's a good idea to test different scenarios and edge cases to make sure the system is behaving as expected. Here's a list of testing suggestions that should cover a large amount of the application's edge cases: + +### General Tests + +- Check to make sure the interactive interface follows our [UX Guidelines](https://github.com/stellar/anchor-ux-guidelines) +- Test the interactive flow usability on really small and really large screens +- Check that the issuing account is set up with the correct home domain, and that the website on that home domain has branding visuals, content about your company, clear language telling users how to report problems, and a method for contacting you to seek support. +- Test the interface using different locale information, and check for translated content including error messages, responses, date formatting, and number formatting + +### KYC Tests + +- Check that KYC appears with a new wallet SK +- Check that KYC (and email, if it's being used to identify users) is not appearing when using a previous wallet SK +- Check that KYC doesn't accept incorrectly formatted inputs, and that the error messages are comprehensible +- Check that you can use the same KYC information (email, phone number, username, etc) multiple times +- Check that you can go through KYC multiple times with the same Stellar SK. + +### Deposit Test + +- Check that deposits are disabled for really small and really large values +- Check that the deposit flow goes through, and that the banking rails are working +- Check that the endpoint returns an error if the JWT token is missing +- Check that all deposit response params conform to [protocols](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md#deposit) + +### Withdraw Tests + +- Check that withdrawals are disabled for really small and really large values +- Check that you cannot make a withdrawal with a value higher than the current balance +- Check that the withdraw flow goes through, and that the banking rails are working +- Check that the endpoint returns an error if the JWT token is missing +- Check that all withdraw params conform to [protocols](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md#withdraw) + +### Transaction(s) Tests + +- Check that the `/transaction` endpoint is working for a newly created transaction +- Check that the `/transaction` endpoint is working for a completed transaction +- Check that the `/transactions` endpoint returns all transactions from a specific user + +### Security Tests + +- Ensure distribution is done through a dedicated distribution account (not the issuing account) +- Ensure market making is done through a dedicated account (not the issuing account) +- Make sure all authenticated endpoints are not accessible without the JWT tokens diff --git a/docs/anchoring-assets/enabling-deposit-and-withdrawal/setting-up-test-server.mdx b/docs/anchoring-assets/enabling-deposit-and-withdrawal/setting-up-test-server.mdx new file mode 100644 index 000000000..81357e139 --- /dev/null +++ b/docs/anchoring-assets/enabling-deposit-and-withdrawal/setting-up-test-server.mdx @@ -0,0 +1,246 @@ +--- +title: Set Up a Test Server +order: 30 +--- + +import { CodeExample } from "@site/src/components/CodeExample"; +import { Alert } from "@site/src/components/Alert"; + +Before setting up a server that connects to live banking rails and real money, you should build out a test rig connected to the [Stellar test network](../../glossary/testnet.mdx). The testnet works just like the main Stellar network, but it uses test data and allows you to fund test accounts for free using a tool called [Friendbot](../../glossary/testnet.mdx#what-is-the-stellar-testnet-good-for). + + + +Note: the testnet is reset every three months, so when building on it, make sure you have a plan to recreate necessary accounts and other data. For more info, check out the [best practices for using the testnet](../../glossary/testnet.mdx#best-practices-for-using-testnet). + + + +At the end of this section, you should have a sandboxed system capable of interfacing with the Stellar testnet that you can easily convert into a production-ready deployment on the main Stellar network. You should always keep a testnet deployment up and running — even once you have a production version — so that wallets can test your implementation without the risk of losing real user funds. + +You can also find an outline of the basic steps anchors need to complete in the [intro to SEP-24](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md#basic-anchor-implementation), the interactive deposit and withdrawal specification. + +## Prerequisites + +Before you start building infrastructure to connect a Stellar-network token to banking rails, you need to: + +1. Issue an asset on the network – which you can find out how to do in the [Issue Assets](../../issuing-assets/index.mdx/) section — or find another trustworthy issuer and coordinate with them to broker their asset. +1. Add meta-information about the asset (if you issue one) and define the location of your `TRANSFER_SERVER_SEP0024` in your `stellar.toml` so that wallets know where to find the server and relevant endpoints. You can consult [this guide](../../issuing-assets/index.mdx/) for help completing your `stellar.toml`. + +Once you've taken those steps, you're ready to implement an `/info` endpoint. + +## Implementing the `/info` Endpoint + +The `/info` endpoint allows anchors to communicate basic information to wallets, exchange interfaces, and other Stellar apps. It responds to client queries with a JSON object detailing which currencies the anchor supports for deposit and withdrawal, and laying out the fee structure for each currency. + +Generally, anchors structure fees one of two ways: + +- Fixed Fees: a fixed value that is applied to the transactions +- Fee Percent: a percentage of the transaction value + +Since those are both pretty straightforward, the `/info` endpoint can convey those structures directly. If, however, you use a more complicated fee structure, you’ll need to use the `/fee` endpoint, and indicate it’s enabled in the `/info` response. + +Here's an example `/info` response: + + + +```json +{ + "deposit": { + "USD": { + "enabled": true, + "fee_fixed": 5, + "fee_percent": 1, + "min_amount": 0.1, + "max_amount": 1000 + }, + "ETH": { + "enabled": true, + "fee_fixed": 0.002, + "fee_percent": 0 + } + }, + "withdraw": { + "USD": { + "enabled": true, + "authentication_required": true, + "fee_minimum": 5, + "fee_percent": 0.5, + "min_amount": 0.1, + "max_amount": 1000 + }, + "ETH": { + "enabled": false + } + }, + "fee": { + "enabled": false + } +} +``` + + + +You can find the complete parameters for the `/info` endpoint in the Interactive Anchor/Wallet Asset Transfer Server spec (aka [SEP-24](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md)). + +## Authentication + +To provide a simpler experience for wallet users, anchors (aka on/off ramps) can authenticate individuals with their Stellar accounts instead of requiring usernames and passwords. The method for doing that is specified in the Stellar Web Authentication SEP (aka [SEP-10](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md)). + +Stellar Web Authentication is a protocol for verifying that a user controls a Stellar private key for a given account, and for creating a persistent session for that user. It relies on a variation of mutual challenge-response, and uses Stellar transactions to encode challenges and responses: an asset issuer provides a 'challenge' transaction; the client signs it on behalf of the user and returns it to the issuer; the issuer checks that the signature is valid, and if it is, issues a JWT. + +The JWT then acts as a reusable key for a client to perform an action on behalf of a user. It can contain an arbitrary amount of information, and is signed by the provider — in this case the asset issuer — to ensure validity. You can read the full spec [here](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md). + +### Requesting a Challenge + +To start the authentication flow, the client requests a challenge, which is a Stellar transaction with the sequence number set to 0. A transaction with a sequence number of 0 is, by definition, invalid, so it can't actually be submitted to the network. The issuer can, however, verify that the transaction is signed correctly (which is what happens in the next step). + +The only information needed to request a challenge is the client's public key, passed as the `account` parameter. Here's an example: `GET ?account=GXXXXXX` + +### Exchanging the Signed Challenge + +Once the client signs the challenge transaction on behalf of the user using standard [Stellar SDK tools](../../software-and-sdks/index.mdx), it sends the signed transaction back to the provider. Using those same Stellar SDKs, the provider then checks to see if the transaction is properly signed, and if it is, offers the client a JWT. + +This JWT should be created with the claims that are appropriate given the account that signed the challenge. It can be created with any existing JWT library. + +Here are the fields included in the JWT: + +- The `sub` key contains the account of the authenticated user +- The `exp` key contains the expiration of the JWT. Some JWTs should be short-lived if the claims inside of it are expected to change, or if the JWT is used in less secure environments +- Other keys can contain any type of claim or data the Anchor wishes. + +Since the `sub` field contains the address of the authenticated user, it's kind of like a username. The JWT authenticates said user. + +Since some tokens are used in less secure environments, such as as query parameters in URLs, you may want to create a short-lived or one-time use token to prevent a JWT from falling into the wrong hands. + +## Deposit Flow + +Once you have implemented an `/info` endpoint and set up for user authentication, your next step is to set up a deposit flow. The deposit flow is the on-ramp to the Stellar Network. In it, a user transfers funds via local rails to a stablecoin issuer in return for a digital version of those funds on the Stellar Network. This section will go through all the steps necessary to implement a working deposit functionality. + +Deposit flows involve a back-and-forth between an issuer's server and a wallet's client that starts with the wallet polling the `/info` endpoint and setting up an authenticated user session as described in the previous sections. The general sequence of deposit events looks like this: + +```mermaid +sequenceDiagram + participant Wallet + participant Anchor + participant Anchor webapp + participant Stellar + note left of Wallet: Initiate Deposit + Wallet->>Anchor: [SEP-24] GET /info + Anchor-->>Wallet: Authentication required! + Wallet-->>Anchor: [SEP-10] GET /auth_challenge + Wallet-->>Anchor: [SEP-10] POST auth_challenge + Anchor-->>Wallet: [SEP-10] auth JWT + Wallet->>Anchor webapp: [SEP-24] Open URL + Callback + JWT + note right of Anchor: Interactive deposit
-----------------------
Choose amount
Collect KYC info
Deposit method + Anchor webapp->>Wallet: Callback (how + URL) + Wallet->>Anchor webapp: [SEP-24] Open more_info_url + note left of Anchor webapp: Show deposit info + Wallet->>Anchor: Deposit (off-chain) + note left of Anchor: Deposit confirmed + Anchor->>Stellar: POST /transaction [Horizon] + Stellar->>Anchor: Transaction result [Horizon] + Stellar->>Wallet: Transaction result +``` + +### The POST `/transactions/deposit/interactive` Endpoint + +To start a new deposit transaction, the wallet POSTs to the `/transactions/deposit/interactive` endpoint and lets the issuer know the user’s Stellar account via the `account` parameter, and what type of asset the user plans to deposit via the `asset_code` parameter. + +Those are the only required fields to initiate a deposit request, so that’s all we’ll cover here. To find out about other optional parameters you should support, check the [complete spec](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md#deposit). + +### Initiate Interactive Flow + +In the interactive flow, the anchor’s server responds to the wallet’s POST with the `interactive customer information needed` response detailed [here](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md#2-interactive-customer-information-needed). That response is a JSON object containing a URL that the wallet uses to open an issuer-hosted webapp, which is what the issuer uses to collect the information it needs from a user to complete a deposit. The reason this flow relies on an issuer-hosted webapp is that wallets can’t predict what type of information an issuer will need. + +If authentication is required, it should be handled before kicking off the deposit by the server hosting the interactive flow, usually using a one-time-use JWT to start a backend persistent session as described in the [previous section](./#authentication). If you use [Polaris](./reference-implementations.mdx), authentication is handled for you. + +When the flow is finished, the webapp should respond to the callback given by the client. This can be either a javascript `postMessage` call or a call to a wallet-provided URL. In native apps, this URL can have a custom protocol to call back to the opening app. + +### Webapp Interface + +The webapp is an interface that collects any information you need from the user, including KYC requirements and deposit information, such as amount. It doesn’t have to be complicated, but to provide a seamless user experience that aligns with wallets’ expectations, it should follow the UX guidelines described in this [Github repo](https://github.com/stellar/anchor-ux-guidelines). If you’ve already collected information from this user, you can either skip these screens or pre-fill them. + +For testing purposes, it's important that the staging infrastructure allows third parties to complete the flow without a real bank account or phone number, and without transferring any real money. Each screen should be properly internationalized using either an existing customer's preferences or the optional `lang` parameter from the initial `POST /transactions/deposit/interactive` call. + +### More_info_url + +The `more_info_url` is a parameter returned by the `/transactions/deposit/interactive` endpoint that is vital to complete deposits that require a bank transfer. It is automatically opened by wallets once the interactive flow is complete, and should have information on the bank account that the user should send the funds to. + +After the Anchor confirms that the user has sent the transfer, this page can be used to show more information about the transaction. The `more_info_url` is relative to a single transaction, and it's used throughout the endpoints as a way for users to get more data on that specific entry. + +The `more_info_url` should be a standalone page that users can also visit after the interactive flow is complete (not necessarily right after), since it's common for people to only be able to start the actual bank transfer a while after they've started the deposit process in the webapp and wallet interfaces. + +## Transaction(s) Endpoints + +The `/transaction` and `/transactions` endpoints provide a way for wallets to fetch information about a single transaction (to check its status) and about all transactions that belong to a user account (in order to show a historical view of their operations) respectively. + +Wallets poll the `/transaction` endpoint when waiting for a transaction’s status to change. For example, when a user has initiated a withdrawal, the wallet will poll the relevant transaction after sending all the required information to the issuer for processing. Once the transaction is completed on the Stellar network, the transaction’s status will update from “pending_stellar” to “completed.” Through polling, the wallet knows to notify the user of the successful withdrawal. + +Wallets need access to the transactions associated with the user’s account for displaying history or past activity. This can be accomplished with a `/transactions` endpoint. Wallets may also use this endpoint after making a POST request to `/transactions/deposit/interactive` to make sure a transaction was successfully created. + +![Status Diagram of SEP-24 transactions](../../web-assets/SEP24-state-diagram.png) + +### Transactions History Endpoint + +The `/transactions` endpoint should accept a number of parameters for filtering the transactions returned in the response. Refer to the [transaction history section of the SEP](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md#transaction-history) for a complete list of parameters and response values. + +It should also be noted that the JSON object representation of a particular transaction should be the same in response to both `/transaction` and `/transactions`. + +### Single Historical Transaction Endpoint + +The `/transaction` endpoint should return a single JSON object representing the specified transaction. Unlike the filtering parameters for `/transactions`, this endpoint should accept the various identifiers for a particular transaction. Again, refer to the [SEP](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md#single-historical-transaction) for a complete spec. + +## Withdrawal Flow + +The withdrawal flow is the off-ramp from the Stellar Network. In it, a user makes a payment on the Stellar network to return digital assets to an anchor, and receives the equivalent value as fiat currency from the anchor in their bank account in return. This section will go through all the steps necessary to implement working withdrawal functionality. + +Like deposit flows, withdraw flows involve a back-and-forth between an anchor’s server and a wallet’s client that starts with the wallet polling the `/info` endpoint and setting up an authenticated user session as described in the previous sections. The general sequence of deposit events looks like this: + +```mermaid +sequenceDiagram + participant Wallet + participant Anchor + participant Anchor webapp + participant Stellar + note left of Wallet: Initiate Withdraw + Wallet->>Anchor: [SEP-24] GET /info + Anchor-->>Wallet: Authentication required! + Wallet-->>Anchor: [SEP-10] GET /auth_challenge + Wallet-->>Anchor: [SEP-10] POST auth_challenge + Anchor-->>Wallet: [SEP-10] auth JWT + Wallet->>Anchor webapp: [SEP-24] Open URL + Callback + JWT + note right of Anchor: Interactive withdraw
-----------------------
Choose amount
Collect KYC info
Withdraw method + Anchor webapp->>Wallet: Callback (how + URL) + note left of Wallet: Show withdraw info + Wallet->>Stellar: Withdraw Transaction + note left of Anchor: Withdraw confirmed + Stellar->>Anchor: Transaction result [Horizon] + Stellar->>Wallet: Transaction result +``` + +### `/transactions/withdraw/interactive` Endpoint + +Before initiating a withdrawal, a wallet hits the anchor’s `/info` endpoint and creates an authenticated user session as described in the [previous section](./#authentication). Once that’s done, a wallet makes a POST request to an issuer’s `/transactions/withdraw/interactive` endpoint to start a withdrawal, which creates a new but incomplete transaction record in the issuer’s database. The issuer’s response to the request should contain the transaction’s ID as well as the URL that should be requested to begin the interactive flow. + +The webapp then collects the information necessary to complete the transaction. + +The only required parameter for a withdraw query is `asset_code`, however you will likely want to collect `account` as well. Another notable parameter is `lang`. This parameter can be used on the deposit and info endpoints as well, and should be supported if you intend to serve users who speak different languages. Polaris supports English, Spanish, and Portuguese out of the box. + +For an exhaustive list of the parameters and response values check out the [withdraw section](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md#withdraw) of the Interactive Anchor/Wallet Transfer Server SEP. + +### Initiate Interactive Flow + +The response to the wallet’s POST request to the `/transactions/withdraw/interactive` endpoint provides a URL. Since this URL will be requested as if in a browser (via popup or iframe), authentication headers cannot be added. DO NOT include the SEP-10 JWT token in the URL for authentication for the interactive flow, as this is considered insecure. + +[Polaris](./reference-implementations.mdx) gets around this issue by adding a short-lived JWT token to the URL and using session cookies for the remainder of the interactive flow to identify the authenticated user. If you’re not using Polaris, feel free to implement your own authentication mechanisms for the interactive flows, but reference [SEP-24](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md#authentication) for guidance. + +### Mocked Interface + +There are a number of UI pages that should be served to the user while walking through the withdraw flow. These pages can be divided into three separate categories or phases. + +- Mocked KYC: When starting a withdrawal, the anchor may be legally obligated to collect specific information from the user. In the testing phase, you don’t need to store this information permanently, but you still need to provide the interface to collect it. There is no restriction on the number of pages used, but often, you can get all you need with a single page. Make sure you understand relevant legal requirements before implementing this phase of the process. +- Withdrawal information: The issuer needs to collect the numerical amount of the asset to transfer. There may be other pieces of data you want to collect as well. Make sure you have all the information necessary to submit a successful transaction to the stellar network. +- Waiting for transfer: Once the issuer has collected all the information needed for a withdrawal, the last page rendered in the interactive flow should make a `postMessage` call to the Wallet, notifying the wallet that it has all the information it needs. + +The wallet then submits the transaction to the Stellar network that sends tokens from the user’s Stellar account to the issuer’s Stellar account, andl begins polling the issuer’s `/transaction` endpoint until the relevant transaction has the expected status. For withdrawals, the expected status is “complete.” + +The issuer should detect that the wallet has submitted a withdrawal transaction by streaming transaction events for the user’s account. Once a matching transaction has been detected, the issuer should mark the transaction as “complete” if it succeeds on the Stellar network, or “error” if there was a problem. diff --git a/docs/anchoring-assets/index.mdx b/docs/anchoring-assets/index.mdx new file mode 100644 index 000000000..3a2b50150 --- /dev/null +++ b/docs/anchoring-assets/index.mdx @@ -0,0 +1,33 @@ +--- +title: Overview +order: 0 +--- + +_Stellar Anchors_ are the on/off ramps of the Stellar network: they accept deposits of fiat currencies (such as USD, CNY, and BRL) via existing rails (such as bank deposits or cash-in points), and send a user the equivalent digital tokens representing those deposits on the Stellar network. On the flipside, they allow holders to redeem those tokens for the real-world assets they represent. To read more about the kinds of things you can do with digital fiat currency, check out [this explainer](https://www.stellar.org/learn/the-power-of-stellar). + +Stellar Anchors can issue their own assets (in which case, they earn the title _Issuing Anchors_), or they can honor assets that already exist on the network. So if you want to build an on/off ramp for Stellar, you can consult the [Issue Assets](../issuing-assets/index.mdx) section and issue your own token to offer customers _or_ you can find another organization that issues a Stellar-network asset and arrange to broker their token. + +This section covers how anchors can provide services for: + +1. [Deposits & withdrawals on and off Stellar](./enabling-deposit-and-withdrawal/index.mdx) +1. [Cross-border payments through Stellar](./enabling-cross-border-payments/index.mdx) + +This section doesn't cover the basic steps for issuing an asset on the network — you can find those [here](../issuing-assets/index.mdx) — rather, it explains how developers can _connect_ the Stellar Network to their country's banking system and regulatory processes such as [KYC](https://en.wikipedia.org/wiki/Know_your_customer) and [AML](https://en.wikipedia.org/wiki/Money_laundering#Anti-money_laundering). It will walk through the flow, features, and protocol specifications used in each case above, and offer examples and tools to make the process easier to implement and test. + +## Interoperability and Stellar Ecosystem Proposals + +Stellar is a completely open network, and its important that the organizations providing services on Stellar are built in such a way that allow new products to integrate with these existing services quickly. The SDF ensures this is done by defining standards that network participants implement. + +Those standards are specified in Stellar Ecosystem Proposals (SEPs), which are publicly created, open-source documents that live in a [Github repository](https://github.com/stellar/stellar-protocol/tree/master/ecosystem#stellar-ecosystem-proposals-seps). They define how different institutions (such as asset issuers, wallets, exchanges, and service providers) should interact and interoperate. + +There are many use cases covered by the existing SEPs — including regulated assets ([SEP-8](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0008.md)) and federated user identification ([SEP-2](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0002.md)) — and new SEPs are proposed and discussed on Github all the time. We encourage you to participate in those discussions and help build new standards that will make the financial services easier and more accessible. + +## Implementing SEP-24 and/or SEP-31 + +Each of the backend servies described in these protocols can be implemented using a three-phase process: + +1. Building the service from the ground up on Stellar's Testnet network +1. Deploying the same service on Stellar's Mainnet network and connecting the anchor's existing off-chain rails +1. Integrating with existing wallets or anchors on Stellar and launching the product + +See the [Enabling Deposits and Withdrawals](./enabling-deposit-and-withdrawal/index.mdx) or [Enabling Cross-Border Payments](./enabling-cross-border-payments/index.mdx) sections for SEP-24 or 31, repectively. diff --git a/docs/anchoring-assets/metadata.json b/docs/anchoring-assets/metadata.json new file mode 100644 index 000000000..011acab4d --- /dev/null +++ b/docs/anchoring-assets/metadata.json @@ -0,0 +1,4 @@ +{ + "order": 40, + "title": "Anchor Assets" +} diff --git a/docs/building-apps/basic-wallet.mdx b/docs/building-apps/basic-wallet.mdx new file mode 100644 index 000000000..554756420 --- /dev/null +++ b/docs/building-apps/basic-wallet.mdx @@ -0,0 +1,873 @@ +--- +title: Create a Basic Wallet +order: 40 +--- + +import { CodeExample } from "@site/src/components/CodeExample"; +import { Alert } from "@site/src/components/Alert"; + + + +In this tutorial, the goal is to get to a place where a user can create, store, and access their Stellar account using an intuitive pincode encryption method. It assumes that you have already completed the [Project Setup section](./project-setup.mdx). + + + +[View the setup boilerplate code on GitHub][1] + +## User Flow + +Because we've decided to build a non-custodial wallet, we don’t need to communicate with a server or database at all: everything can happen locally right on a user’s device. We’ll be doing all our work inside of `src/components/wallet/`, so head over there now. We’re going to use the `StellarSdk.Keypair.random()` from the StellarSdk to generate a new valid Stellar keypair. That's the easy part. The hard work will be storing that vital information in a secure yet accessible manner. + +The user flow we're building will work like this: Click “Create Account” → UI modal popup asking for a pincode → Enter pincode, click “OK” → App encrypts a new Stellar keypair secret key with pincode → App saves encrypted secret to `localStorage`. On page reload, we’ll fetch the `publicKey` to “login” the user, but for any protected action such as “Copy Secret”, the modal will pop back up asking for the original pincode. + +## Create a Popup Modal + +To start, let's look at at the popup modal. We’ll be mimicking the browser’s `prompt` functionality with our own new, more powerful component. First things first we should generate a new component: + + + +```bash +npm run generate +``` + + + +Call it `stellar-prompt`, and deselect both test files leaving only the styling. Once you have that, open `src/components/prompt/` and rename the `.css` file to `.scss`. Fill that style file with this: + + + +```scss +@import "../../global/style.scss"; + +:host { + display: block; + font-family: $font-family; + font-size: 15px; + + .prompt-wrapper { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + display: flex; + align-items: center; + justify-content: center; + align-content: center; + min-height: 100vh; + min-width: 100vw; + background-color: rgba(black, 0.2); + z-index: 1; + } + .prompt { + background-color: white; + padding: 20px; + max-width: 350px; + width: 100%; + position: relative; + + p { + margin-bottom: 10px; + } + input { + width: 100%; + margin: 0; + padding: 5px; + outline: none; + border: 1px solid black; + text-transform: uppercase; + + &:focus { + border-color: blue; + } + } + } + .select-wrapper { + position: relative; + display: inline-flex; + + select { + border-color: blue; + padding: 0 10px; + min-width: 100px; + } + + &:after, + &:before { + font-size: 12px; + position: absolute; + right: 10px; + color: blue; + } + &:after { + content: "◀"; + top: calc(50% - 5px); + transform: translate(0, -50%) rotate(90deg); + } + &:before { + content: "▶"; + top: calc(50% + 5px); + transform: translate(0, -50%) rotate(90deg); + } + } + .actions { + display: flex; + justify-content: flex-end; + margin-top: 10px; + + button { + margin: 0; + min-width: 50px; + } + .cancel { + background: none; + border: 1px solid blue; + color: blue; + } + .submit { + margin-left: 10px; + } + } +} +``` + + + +Replace the `prompt.tsx` contents with this. + + + +```tsx +import { Component, Prop, Element, Watch, h, State } from "@stencil/core"; +import { defer as loDefer } from "lodash-es"; + +export interface Prompter { + show: boolean; + message?: string; + placeholder?: string; + options?: Array; + resolve?: Function; + reject?: Function; +} + +@Component({ + tag: "stellar-prompt", + styleUrl: "prompt.scss", + shadow: true, +}) +export class Prompt { + @Element() private element: HTMLElement; + + @Prop({ mutable: true }) prompter: Prompter; + + @State() private input: string; + + @Watch("prompter") + watchHandler(newValue: Prompter, oldValue: Prompter) { + if (newValue.show === oldValue.show) return; + + if (newValue.show) { + this.input = null; + + if (newValue.options) + this.input = + this.input || + `${newValue.options[0].code}:${newValue.options[0].issuer}`; + else + loDefer(() => this.element.shadowRoot.querySelector("input").focus()); + } else { + this.prompter.message = null; + this.prompter.placeholder = null; + this.prompter.options = null; + } + } + + componentDidLoad() { + addEventListener("keyup", (e: KeyboardEvent) => { + if (this.prompter.show) + e.keyCode === 13 + ? this.submit(e) + : e.keyCode === 27 + ? this.cancel(e) + : null; + }); + } + + cancel(e: Event) { + e.preventDefault(); + + this.prompter = { + ...this.prompter, + show: false, + }; + this.prompter.reject(null); + } + + submit(e: Event) { + e.preventDefault(); + + this.prompter = { + ...this.prompter, + show: false, + }; + this.prompter.resolve(this.input); + } + + update(e) { + this.input = e.target.value.toUpperCase(); + } + + render() { + return this.prompter.show ? ( +
+
+ {this.prompter.message ?

{this.prompter.message}

: null} + + {this.prompter.options ? ( +
+ +
+ ) : ( + this.update(e)} + > + )} + +
+ + +
+
+
+ ) : null; + } +} +``` + +
+ +One of the first things you’ll notice is the use of `lodash-es`. Let’s make sure we’ve got that imported before moving forward: + + + +```bash +npm i -D lodash-es +``` + + + +There’s a lot going on in this file, but since this isn’t a Stencil tutorial we’ll skip the details. What this allows us to do it to use a `` component elsewhere in our project. It's worth noting the variables available to us in the `prompter` property. + + + +```ts +export interface Prompter { + show: boolean; + message?: string; + placeholder?: string; + options?: Array; + resolve?: Function; + reject?: Function; +} +``` + + + +The values we’ll be making most use of are those first three: `show`, `message` and `placeholder`. The last two—`resolve` and `reject`—are for promisifying the prompt so we can await a response before continuing with further logic. Don't worry: that statement will make more sense in a moment once we include this component in `src/components/wallet/`. Speaking of, let’s swing over to that component now. + +We’ve got a lot of work to do in here so I’ll just paste the code in all its glory and we’ll walk through it block by block: + + + +```ts +import { Component, State } from "@stencil/core"; + +import componentWillLoad from "./events/componentWillLoad"; +import render from "./events/render"; + +import createAccount from "./methods/createAccount"; +import copyAddress from "./methods/copyAddress"; +import copySecret from "./methods/copySecret"; +import signOut from "./methods/signOut"; +import setPrompt from "./methods/setPrompt"; + +import { Prompter } from "@prompt/prompt"; + +interface StellarAccount { + publicKey: string; + keystore: string; +} + +@Component({ + tag: "stellar-wallet", + styleUrl: "wallet.scss", + shadow: true, +}) +export class Wallet { + @State() account: StellarAccount; + @State() prompter: Prompter = { show: false }; + @State() error: any = null; + + // Component events + componentWillLoad() {} + render() {} + + // Stellar methods + createAccount = createAccount; + copyAddress = copyAddress; + copySecret = copySecret; + signOut = signOut; + + // Misc methods + setPrompt = setPrompt; +} + +Wallet.prototype.componentWillLoad = componentWillLoad; +Wallet.prototype.render = render; +``` + + + +They say the beginning is a good place to start. Let’s do that: + + + +```js +import { Component, State } from "@stencil/core"; + +import componentWillLoad from "./events/componentWillLoad"; +import render from "./events/render"; + +import createAccount from "./methods/createAccount"; +import copyAddress from "./methods/copyAddress"; +import copySecret from "./methods/copySecret"; +import signOut from "./methods/signOut"; +import setPrompt from "./methods/setPrompt"; + +import { Prompter } from "@prompt/prompt"; +``` + + + +Just one import from a library we should already have installed. + +The other relative path imports are all the _events_ and _methods_ we’ll create here in a moment. For now, just generate all those files in their appropriate directories. Ensure your console is at the root of the `stellar-wallet` project before running this string of commands: + + + +```bash +mkdir -p src/components/wallet/{events,methods} +touch src/components/wallet/events/{componentWillLoad.ts,render.tsx} +touch src/components/wallet/methods/{createAccount,copyAddress,copySecret,signOut,setPrompt}.ts +``` + + + +Next we have this funky line which may seem like an npm team import, but is actually a fancy typescript module alias path. + + + +```ts +import { Prompter } from "@prompt/prompt"; +``` + + + +This allows us to avoid long error prone `../../../` paths and just use `@{alias}/{path?}/{module}`. In order to get this past both the linter and compiler we’ll need to modify a couple files. + +First, modify the `tsconfig.json` file to include these values in the `compilerOptions` object. + + + +```JSON +{ + "compilerOptions": { + "baseUrl": "./src", + "paths": { + "@prompt/*": ["components/prompt/*"], + "@services/*": ["services/*"] + }, + // ... + }, + // ... +} +``` + + + +Next,modify the `package.json` file to include a `_moduleAliases` key at the root of the object. + + + +```JSON +{ + // ... + "_moduleAliases": { + "@prompt": "dist/collection/components/prompt", + "@services": "dist/collection/services" + } +} +``` + + + +Finally, install the `module-alias` package and add it to the top of the `src/index.ts` file. + + + +```bash +npm i -D module-alias +``` + + + + + +```ts +import "module-alias/register"; +export * from "./components"; +``` + + + +Cool! With any luck we should be able to use these slick alias imports for the prompt and services directories now. + +## Create Stellar Account Class + + + +```ts +interface StellarAccount { + publicKey: string; + keystore: string; +} +``` + + + +`interface` is just the TypeScript way of setting up a tidy typed class. `StellarAccount` will be our account class. It includes the `publicKey` for easy reference later in Horizon or Astrograph calls and the Top Secret `keystore` key containing the encrypted account secret cipher. + + + +```js +@Component({ + tag: 'stellar-wallet', + styleUrl: 'wallet.scss', + shadow: true +}) +export class Wallet { + @State() account: StellarAccount + @State() prompter: Prompter = {show: false} + @State() error: any = null + + ... +} +``` + + + +Pretty standard boring bits, setting up the `@Component` with its defining values and initializing with some `@State` and `@Prop` data. You can see we’re setting up an `account` state with our `StellarAccount` class as well as a `prompter` state with that `Prompter` class from the `stellar-prompt` we imported earlier. We’re initializing that `prompter` state with a `show` value of `false` so the prompt modal rendereth not initially. + +Everything after this is the assignment of our imported events and methods from up above. Let’s begin with the `./events/componentWillLoad.ts` + + + +```js +import { handleError } from "@services/error"; +import { get } from "@services/storage"; + +export default async function componentWillLoad() { + try { + let keystore = await get("keyStore"); + + this.error = null; + + if (keystore) { + keystore = atob(keystore); + + const { publicKey } = JSON.parse(atob(JSON.parse(keystore).adata)); + + this.account = { + publicKey, + keystore, + }; + } + } catch (err) { + this.error = handleError(err); + } +} +``` + + + +`componentWillLoad` is the Stencil way of pre-filling the state and prop values before actually rendering the component. In our case we’ll use this method to populate the `account` `@State` with the saved storage `keyStore` value if there is one. At first there won’t be, so we’ll come back to this once we’ve actually gone over how to create and save accounts. For now just know it’s here, and since you’re smart, I imagine you can already kind of see how it works. + +“But wait!” you say, “What are the `@services/error` and `@services/storage` packages?” Fine, yes, we should go over those. Remember the module alias stuff from earlier? Well one was for `@prompt` and the other was for `@services`. Go ahead and create these two files and add them to the `src/services` directory. + + + +```bash +mkdir -p src/services +touch src/services/{error,storage}.ts +``` + + + +`error.ts` will look like this. + + + +```ts +import { get as loGet } from "lodash-es"; + +export function handleError(err: any) { + return loGet(err, "response.data", loGet(err, "message", err)); +} +``` + + + +Nothing fancy, just a clean little error handler we’ll make use of later when processing API requests. + +## Set Up Key Storage + +Next is `storage.ts`. + + + +```js +import { Plugins } from "@capacitor/core"; + +const { Storage } = Plugins; + +export async function set(key: string, value: any): Promise { + await Storage.set({ + key, + value, + }); +} + +export async function get(key: string): Promise { + const item = await Storage.get({ key }); + return item.value; +} + +export async function remove(key: string): Promise { + await Storage.remove({ key }); +} +``` + + + +You’ll notice a new package `@capacitor/core`. Let’s install and set that up. + + + +```bash +# Install dependencies +npm i -D @capacitor/core@2 @capacitor/cli@2 + +# Initialize Capacitor +npx cap init +``` + + + + + +```bash +? App name Stellar Wallet +? App Package ID (in Java package format, no dashes) com.wallet.stellar +? Which npm client would you like to use? npm +✔ Initializing Capacitor project in /Users/user/Stellar/docs-wallet in 1.91ms + + +🎉 Your Capacitor project is ready to go! 🎉 + +Add platforms using "npx cap add": + + npx cap add android + npx cap add ios + npx cap add electron + +Follow the Developer Workflow guide to get building: +https://capacitor.ionicframework.com/docs/basics/workflow +``` + + + +We’re not really making full use of [ Capacitor ][2], but it is an amazing service so be sure and check it out! For now we just need it to make storing and retrieving our data a bit more stable. + +This storage service is simply a key setter and getter helper for storing and retrieving data. We’ll use this for any persistent data we want to store. For now, that's our Stellar account data. + +## Set Up Event Handling + +That’s everything we need for the `componentWillLoad` event. On to the `./events/render.tsx` file. + + + +```tsx +import { h } from "@stencil/core"; + +export default function render() { + return [ + , + this.account ? ( + [ + , + ] + ) : ( + + ), + + this.error ? ( +
{JSON.stringify(this.error, null, 2)}
+ ) : null, + this.account ? ( + + ) : null, + ]; +} +``` + +
+ +It looks messy, but it’s actually a pretty simple `.tsx` file rendering out our DOM based off a series of conditional values. You can see we’re including the `stellar-prompt` component, and setting the prompter prop to our `this.prompter` state. We then have a ternary operation toggling between a Create Account button and a basic account UI. If `this.account` has a truthy value, we’ll print out the account’s `publicKey` along with some interaction buttons. If `this.account` is falsey, we’ll print out a singular Create Account button connected to, you guessed it, the `createAccount` method. After that logic, we print out an error if there is one, and finally a Sign Out button if there’s an account to sign out of. Those are the two `Wallet` `@Component` events. + +## Create Methods + +Let’s look at the methods now beginning with the `./methods/createAccount.ts` file. + + + +```ts +import sjcl from "@tinyanvil/sjcl"; +import { Keypair } from "stellar-sdk"; + +import { handleError } from "@services/error"; +import { set } from "@services/storage"; + +export default async function createAccount(e: Event) { + try { + e.preventDefault(); + + const pincode_1 = await this.setPrompt("Enter a keystore pincode"); + const pincode_2 = await this.setPrompt("Enter keystore pincode again"); + + if (!pincode_1 || !pincode_2 || pincode_1 !== pincode_2) + throw "Invalid pincode"; + + this.error = null; + + const keypair = Keypair.random(); + + this.account = { + publicKey: keypair.publicKey(), + keystore: sjcl.encrypt(pincode_1, keypair.secret(), { + adata: JSON.stringify({ + publicKey: keypair.publicKey(), + }), + }), + }; + + await set("keyStore", btoa(this.account.keystore)); + } catch (err) { + this.error = handleError(err); + } +} +``` + + + +Aha! Finally something interesting. This method forms the meat of our component. Before we dive into it, though let’s install the missing `@tinyanvil/sjcl` package. + + + +```bash +npm i -D @tinyanvil/sjcl +``` + + + +## Create an Account + +Essentially all we’re doing is making a request to create an account, which triggers the Prompt modal to ask for a pincode. That pincode will be used in the `sjcl.encrypt` method to encrypt the secret key from the `Keypair.random()` method. We set the `this.account` with the `publicKey`, which encrypted the `keystore` cipher, and now we're storing that cipher in base64 format in `localStorage` via our `set('keyStore')` method for easy retrieval when the browser reloads. We could also encode that cipher into a QR code or a link to share with other devices. Since it requires the pincode that encrypted cipher, it's as secure as the pincode you encrypt it with. + +## Copy Address + +Now that we’ve created an account, there are three more actions we'll enable: `copyAddress`, `copySecret`, and `signOut`. + +First `./methods/copyAddress.ts` + + + +```ts +import copy from "copy-to-clipboard"; + +export default async function copyAddress(e: Event) { + e.preventDefault(); + copy(this.account.publicKey); +} +``` + + + +Well there you go, the easiest code you’ll see all day. Just `copy` the `publicKey` from the `this.account` object to the clipboard. Before we jump though don’t forget to install that `copy-to-clipboard` package. + + + +```bash +npm i -D copy-to-clipboard +``` + + + +## Copy Secret + +Next `./methods/copySecret.ts` + + + +```ts +import sjcl from "@tinyanvil/sjcl"; +import copy from "copy-to-clipboard"; + +import { handleError } from "@services/error"; + +export default async function copySecret(e: Event) { + try { + e.preventDefault(); + + const pincode = await this.setPrompt("Enter your keystore pincode"); + + if (!pincode) return; + + this.error = null; + + const secret = sjcl.decrypt(pincode, this.account.keystore); + copy(secret); + } catch (err) { + this.error = handleError(err); + } +} +``` + + + +You may not actually include this in your production wallet, but for now it's a simple demonstration of how to programmatically gain access to the secret key at a later date for making payments, creating trustlines, etc. It’s essentially the `createAccount` in reverse: it asks for the pincode to decrypt the keystore which, once decrypted, we `copy` into the clipboard. + +## Sign Out + +Finally `./methods/signOut.ts` + + + +```ts +import { remove } from "@services/storage"; +import { handleError } from "@services/error"; + +export default async function signOut(e: Event) { + try { + e.preventDefault(); + + const confirmNuke = await this.setPrompt( + "Are you sure? This will nuke your account", + "Enter NUKE to confirm", + ); + + if (!confirm || !/nuke/gi.test(confirmNuke)) return; + + this.error = null; + + await remove("keyStore"); + location.reload(); + } catch (err) { + this.error = handleError(err); + } +} +``` + + + +It’s important to allow users to nuke their account, but we need to be careful to confirm that action with our faithful `setPrompt`. Once they opt to “NUKE” the account we can remove the `keyStore` and reload the app. + +## Set Prompt + +Speaking of `setPrompt` the last method in our `wallet.ts` file is `./methods/setPrompt.ts`. + + + +```ts +export default function setPrompt( + message: string, + placeholder?: string, + options?: Array, +): Promise { + this.prompter = { + ...this.prompter, + show: true, + message, + placeholder, + options, + }; + + return new Promise((resolve, reject) => { + this.prompter.resolve = resolve; + this.prompter.reject = reject; + }); +} +``` + + + +In `setPrompt`, we see how the prompt state is set, and how the Promise is set up to allow us to wait on the prompt whenever we call this method. It’s actually pretty slick, and it might be worth looking back at the `src/components/prompt/prompt.tsx` to see how the `resolve` and `reject` functions get called. It’s not central to our wallet creation, but it’s a pretty handy little component that will serve us well in the future as we continue to request input from the user. + +That’s it folks! Restart the server with `npm start` and you’ve got a perfectly legitimate, minimal Stellar wallet key creation and storage Web Component! It's a solid foundation for a non-custodial wallet that relies on a simple pincode. + +[1]: https://github.com/stellar/docs-wallet/tree/setup +[2]: https://capacitor.ionicframework.com/ "Capacitor" diff --git a/docs/building-apps/connect-to-anchors/deposit-anchored-assets.mdx b/docs/building-apps/connect-to-anchors/deposit-anchored-assets.mdx new file mode 100644 index 000000000..a15de4406 --- /dev/null +++ b/docs/building-apps/connect-to-anchors/deposit-anchored-assets.mdx @@ -0,0 +1,453 @@ +--- +title: Deposit Anchored Assets +order: 20 +--- + +import { CodeExample } from "@site/src/components/CodeExample"; +import { Alert } from "@site/src/components/Alert"; + + + +This section of the tutorial takes the [basic wallet](../basic-wallet.mdx) with [trustlines](../custom-assets.mdx) and [anchor connections](./setup-for-anchored-assets.mdx) enabled and adds the ability to initiate in-app deposits with an Anchor. If you have all that ready, and the stellar.toml file is loaded into a `@Prop` on your component, you’re ready to begin the initial deposit of their asset into our wallet. + + + +## Create Deposit Method + +Let’s create `./methods/depositAsset.ts`. + + + +```ts +import sjcl from "@tinyanvil/sjcl"; +import { Transaction, Keypair } from "stellar-sdk"; +import axios from "axios"; +import { + get as loGet, + each as loEach, + findIndex as loFindIndex, +} from "lodash-es"; + +import { handleError } from "@services/error"; + +export default async function depositAsset(e: Event) { + try { + e.preventDefault(); + + let currency = await this.setPrompt( + "Select the currency you'd like to deposit", + null, + this.toml.CURRENCIES, + ); + currency = currency.split(":"); + + const pincode = await this.setPrompt("Enter your keystore pincode"); + + if (!pincode) return; + + const balances = loGet(this.account, "state.balances"); + const hasCurrency = loFindIndex(balances, { + asset_code: currency[0], + asset_issuer: currency[1], + }); + + if (hasCurrency === -1) + await this.trustAsset(null, currency[0], currency[1], pincode); + + const info = await axios + .get(`${this.toml.TRANSFER_SERVER}/info`) + .then(({ data }) => data); + + console.log(info); + + const auth = await axios + .get(`${this.toml.WEB_AUTH_ENDPOINT}`, { + params: { + account: this.account.publicKey, + }, + }) + .then(async ({ data }) => { + const transaction: any = new Transaction( + data.transaction, + data.network_passphrase, + ); + + this.error = null; + this.loading = { ...this.loading, deposit: true }; + + const keypair = Keypair.fromSecret( + sjcl.decrypt(pincode, this.account.keystore), + ); + + transaction.sign(keypair); + return transaction.toXDR(); + }) + .then((transaction) => + axios.post( + `${this.toml.WEB_AUTH_ENDPOINT}`, + { transaction }, + { headers: { "Content-Type": "application/json" } }, + ), + ) + .then(({ data: { token } }) => token); + + console.log(auth); + + const formData = new FormData(); + + loEach( + { + asset_code: currency[0], + account: this.account.publicKey, + lang: "en", + }, + (value, key) => formData.append(key, value), + ); + + const interactive = await axios + .post( + `${this.toml.TRANSFER_SERVER}/transactions/deposit/interactive`, + formData, + { + headers: { + Authorization: `Bearer ${auth}`, + "Content-Type": "multipart/form-data", + }, + }, + ) + .then(({ data }) => data); + + console.log(interactive); + + const transactions = await axios + .get(`${this.toml.TRANSFER_SERVER}/transactions`, { + params: { + asset_code: currency[0], + limit: 1, + kind: "deposit", + }, + headers: { + Authorization: `Bearer ${auth}`, + }, + }) + .then(({ data: { transactions } }) => transactions); + + console.log(transactions); + + const urlBuilder = new URL(interactive.url); + urlBuilder.searchParams.set("callback", "postMessage"); + const popup = open(urlBuilder.toString(), "popup", "width=500,height=800"); + + if (!popup) { + this.loading = { ...this.loading, deposit: false }; + throw 'Popups are blocked. You\'ll need to enable popups for this demo to work'; + } + + window.onmessage = ({ data: { transaction } }) => { + console.log(transaction.status, transaction); + + if (transaction.status === "completed") { + this.updateAccount(); + this.loading = { ...this.loading, deposit: false }; + } else { + setTimeout(() => { + const urlBuilder = new URL(transaction.more_info_url); + urlBuilder.searchParams.set("callback", "postMessage"); + + popup.location.replace(urlBuilder.toString()); + }, 1000); + } + }; + } catch (err) { + this.loading = { ...this.loading, deposit: false }; + this.error = handleError(err); + } +} +``` + + + +It’s a lot of code, but keep in mind this is everything we need to completely interoperate with financial infrastructure foreign to the Stellar ecosystem, which is a big ask and in ~130 lines of code. It’s a modern day miracle! + +## Currency Code and Issuer Prompt + +First things first install the missing `axios` package. + + + +```bash +npm i -D axios +``` + + + +We’ll skip the rest of the top import stuff as you’re well aware of what that is, and we’ve installed and walked through anything noteworthy here already. + + + +```ts +export default async function depositAsset(e: Event) { + try { + e.preventDefault() + + let currency = await this.setPrompt('Select the currency you\'d like to deposit', null, this.toml.CURRENCIES) + currency = currency.split(':') + + const pincode = await this.setPrompt('Enter your keystore pincode') + + if (!pincode) + return + + const keypair = Keypair.fromSecret( + sjcl.decrypt(pincode, this.account.keystore) + ) +``` + + + +Only thing here we haven’t seen before is the prompt selection of the currency code and issuer we’d like to deposit. To get this, we send the `this.setPrompt` a final options array argument, in this case the values from `this.toml.CURRENCIES`. This will open the prompt with a select<\>options input field rather than a text input field. + +That selection will come back as a string in the form of `{code}:{issuer}` so we split that by the `:` so we’ll have a nice tidy array of `[{code}, {issuer}]` to use later. + +## Check Trustline + + + +```ts +const balances = loGet(this.account, "state.balances"); +const hasCurrency = loFindIndex(balances, { + asset_code: currency[0], + asset_issuer: currency[1], +}); + +if (hasCurrency === -1) + await this.trustAsset(null, currency[0], currency[1], pincode); +``` + + + +Once we’ve got the currency we’re dealing with we need to check if we have a trustline for that asset setup for our account. The anchor won’t be able to deposit their token into our account if we don’t have a trustline set for it first. So we get the balance from the `this.account.state` and then inspect the `balances` to see if that currency exists. If it doesn’t we trigger the `this.trustAsset` method with the arguments set to automatically add that trustline to our account. + +## Get Info + + + +```ts +const info = await axios + .get(`${this.toml.TRANSFER_SERVER}/info`) + .then(({ data }) => data); + +console.log(info); +``` + + + +Once our account is setup, it’s time to start asking the Anchor some questions about itself and the features it supports. We get at that info by calling a GET on the `TRANSFER_SERVER/info` url from the anchor’s TOML file. We won’t make use of any of this data right now, but this is where you'd find fee and feature information which to display in you UI for your wallet user to review. + +## Create Authenticated User Session + + + +```ts +const auth = await axios + .get(`${this.toml.WEB_AUTH_ENDPOINT}`, { + params: { + account: this.account.publicKey, + }, + }) + .then(async ({ data: { transaction, network_passphrase } }) => { + const txn: any = new Transaction(transaction, network_passphrase); + + this.error = null; + this.loading = { ...this.loading, withdraw: true }; + + txn.sign(keypair); + return txn.toXDR(); + }) + .then((transaction) => + axios.post( + `${this.toml.WEB_AUTH_ENDPOINT}`, + { transaction }, + { headers: { "Content-Type": "application/json" } }, + ), + ) + .then(({ data: { token } }) => token); + +console.log(auth); +``` + + + +This call is actually a part of [SEP-0010](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md), which allows us to verify ownership of a user's public key so the Anchor knows the deposit request is coming from a valid entity. You don’t want someone else issuing deposit requests on your account, so to prove you control the Stellar account request the deposit, you sign a dummy authentication transaction and send it to the Anchor's web auth server. The server responds with a JWT token, which will be used to verify all further API calls to the anchor. For more info on how this works, check out the user authetication from an [Anchor's point of view](../../anchoring-assets/enabling-deposit-and-withdrawal/setting-up-test-server.mdx#authentication). + +The endpoint for creating an authenticated user session is specified the `WEB_AUTH_ENDPOINT` on an Anchor's stellar.toml file. The first thing our wallet does is make a GET request with a public `account` param which will send back an unsigned Stellar transaction for that account that has in invalide sequence number, so it doesn't actually _do_ anything when submitted to the network. In response you’ll sign that transaction on behalf of the user and POST it back. If the signature checks out, the success response will contain an Authorization Bearer header JWT which you’ll want to store and use for all future interactions with the Anchor. + +## Initiate Deposit + + + +```ts +const formData = new FormData(); + +loEach( + { + asset_code: currency[0], + account: this.account.publicKey, + lang: "en", + }, + (value, key) => formData.append(key, value), +); + +const interactive = await axios + .post( + `${this.toml.TRANSFER_SERVER}/transactions/withdraw/interactive`, + formData, + { + headers: { + Authorization: `Bearer ${auth}`, + "Content-Type": "multipart/form-data", + }, + }, + ) + .then(({ data }) => data); + +console.log(interactive); +``` + + + +This call is our first little bit of magic. Once we setup our `multipart/form-data` entry values we can POST them to the `${this.toml.TRANSFER_SERVER}/transactions/withdraw/interactive` endpoint along with our `auth` Authorization Bearer `auth` token we acquired earlier. The response to this call will contain a special url that allows a user to interact directly with the Anchor to provide the data required for a deposit. + + + +```json +{ + "type": "interactive_customer_info_needed", + "url": "https://testanchor.stellar.org/transactions/deposit/webapp?asset_code=SRT&transaction_id=3dcf204e-e3e8-4831-a840-8e36c0695a07&token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL3N0ZWxsYXItYW5jaG9yLXNlcnZlci5oZXJva3VhcHAuY29tL3RyYW5zYWN0aW9ucy9kZXBvc2l0L2ludGVyYWN0aXZlIiwiaWF0IjoxNTgxNDUyMjI0Ljk4NzcyNjcsImV4cCI6MTU4MTQ1MjI1NC45ODc3MjY3LCJzdWIiOiJHQjM0QVU2NlRTR1Q1RlMzRVVMSk9UUjMyTVdLQU9NRlpLNk1QV003VjRTSUJOTE41UDVURlJKRyIsImp0aSI6IjNkY2YyMDRlLWUzZTgtNDgzMS1hODQwLThlMzZjMDY5NWEwNyJ9.N4N_vIUGu6y7-Hd5IlvevX4DLg9PjisuYEl98ejvhf8", + "id": "3dcf204e-e3e8-4831-a840-8e36c0695a07" +} +``` + + + +In a moment we’ll open that `url` in an iframe, popup, or new tab. Once we’ve sent that request, there will be a pending transaction request we can inspect at the `${this.toml.TRANSFER_SERVER}/transactions` route. + +## Check Transaction Status + + + +```ts +const transactions = await axios + .get(`${this.toml.TRANSFER_SERVER}/transactions`, { + params: { + asset_code: currency[0], + limit: 1, + kind: "deposit", + }, + headers: { + Authorization: `Bearer ${auth}`, + }, + }) + .then(({ data: { transactions } }) => transactions); + +console.log(transactions); +``` + + + +This request should return the deposit status for the request we just made. + + + +```json +{ + "transactions": [ + { + "id": "3dcf204e-e3e8-4831-a840-8e36c0695a07", + "kind": "deposit", + "status": "incomplete", + "status_eta": 3600, + "amount_in": null, + "amount_out": null, + "amount_fee": null, + "started_at": "2020-02-11T20:17:04.984590Z", + "completed_at": null, + "stellar_transaction_id": null, + "external_transaction_id": null, + "external_extra": null, + "external_extra_text": null, + "deposit_memo": null, + "deposit_memo_type": "text", + "more_info_url": "https://testanchor.stellar.org/transaction/more_info?id=3dcf204e-e3e8-4831-a840-8e36c0695a07", + "to": "GB34AU66TSGT5FS3EULJOTR32MWKAOMFZK6MPWM7V4SIBNLN5P5TFRJG", + "from": null + } + ] +} +``` + + + +Once we’re certain we’ve got a `"status": "incomplete",` which is the correct response since we haven't served the user the URL that allows them to initiate a deposit, we know everything's working, so we can go back to the `interactive.url` route and open that to begin filling out the deposit requirements. + +## Serve the Anchor Webapp + + + +```ts +const urlBuilder = new URL(interactive.url); +urlBuilder.searchParams.set("callback", "postMessage"); +const popup = open(urlBuilder.toString(), "popup", "width=500,height=800"); + +if (!popup) { + this.loading = { ...this.loading, deposit: false }; + throw 'Popups are blocked. You\'ll need to enable popups for this demo to work'; +} +``` + + + +We’ll opt for a popup. When you first open that url, it’s likely the anchor will ask for some level of KYC data from the user: name, email, address, verification docs like bank statements, passport, or license, etc. The SDF demo server we're using — which is hooked up to the testnet — will ask for name and email. + +After passing that screen your user enters the amount of an asset they'd like to deposit on the next screen. If, for instance, a user enters $500, once that deposit clears — and on this demo it clears automatically — the Anchor will credit the user's wallet with 500 of the token **less any fees**. In our case, the user ends up with 498.95: 1.05 is taken out in fees for a $500 deposit. + +If popups are disabled, we should kill the loader and show that error in the UI. + + + +```ts + window.onmessage = ({data: {transaction}}) => { + console.log(transaction.status, transaction) + + if (transaction.status === 'completed') { + this.updateAccount() + this.loading = {...this.loading, deposit: false} + } + + else { + setTimeout(() => { + const urlBuilder = new URL(transaction.more_info_url) + urlBuilder.searchParams.set('callback', 'postMessage') + + popup.location.replace(urlBuilder.toString()) + }, 1000) + } + } + } + + catch (err) { + this.loading = {...this.loading, deposit: false} + this.error = handleError(err) + } +} +``` + + + +## Check Deposit Status + +SEP-0024 allows specifies two methods for further communication between the anchor window and the wallet: a POST JSON for when you have a server, message and postMessage for when you have a client-side service. We don’t have a server, so we opt for the `urlBuilder.searchParams.set('callback', 'postMessage')`. With that set, we can setup a window `'message'` event listener which will fire whenever the anchor popup window sends a `postMessage` to our popup `window`. From there we just listen to the popup message until `transaction.status` equals `'completed'`. At that point our deposit has succeeded, and we can `this.updateAccount()` and kill the loader. If the transaction is still in a pending status we’ll wait 1 second before reloading the anchor popup window and checking again. + +If at any point during this flow there’s an error we should catch that, kill the loader, and display the error to the user. + +Whew! \*wipes sweat from brow then hands on sleeves Nice work! “Just like that” we’ve deposited an anchored asset into our wallet! We can now make payments with that asset to anyone else who has a trustline enabled for this asset. diff --git a/docs/building-apps/connect-to-anchors/index.mdx b/docs/building-apps/connect-to-anchors/index.mdx new file mode 100644 index 000000000..ae06fb605 --- /dev/null +++ b/docs/building-apps/connect-to-anchors/index.mdx @@ -0,0 +1,14 @@ +--- +title: Interoperability Basics +order: 0 +--- + +Stellar makes it easy to issue assets and to connect them to existing banking rails so that users can move value onto — and off of — the network. The services that create those connections are called **anchors**, and you can read all about how they work and what they do in [Enable Deposit and Withdrawal](../../anchoring-assets/enabling-deposit-and-withdrawal/index.mdx). + +Most Anchors set up infrastructure to enable wallets to offer in-app deposits and withdrawals by following best practices specified in Stellar Ecosystem Proposals. SEPs are publicly created, open-source documents that live in a [Github repository](https://github.com/stellar/stellar-protocol/tree/master/ecosystem#stellar-ecosystem-proposals-seps). They define how different institutions (such as asset issuers, wallets, exchanges, and service providers) should interact and interoperate. If you're building an app, you can implement the client side of some key SEPs and immediately give your users access to a whole world of local fiat currencies + +This part of the tutorial will focus on the Interactive Anchor/Wallet Asset Transfer Server SEP (aka SEP-24), which defines the specs for deposit and withdrawal implementations. It's focused on the client side of interactive user flows — meaning flows where users interact with an Anchor-hosted interface rendered inside a wallet — and by the end, you should have an implemenation which can be re-purposed to work with any SEP-supporting Anchor. + +When you implement SEP-0024, users can tokenize their grocery-buying dollars, pesos, or naira, use them on the Stellar network and, when they're ready, redeem them for cash in hand or money in the bank — and they can do it all from the comfort of your app. + +Most Anchors offer fiat-backed assets, and SEP-24 is set up to allow them to handle various KYC and AML requirements. But SEP-24 allows for the deposit and withdraw of cryptocurrencies like BTC, ETH, and ERC20 tokens like USDT. If the financial landscape is a bunch of lakes, SEP-0024 is a series of rivers by which you move your currency canoe in and out of the Stellar lake. diff --git a/docs/building-apps/connect-to-anchors/metadata.json b/docs/building-apps/connect-to-anchors/metadata.json new file mode 100644 index 000000000..930f2b0a1 --- /dev/null +++ b/docs/building-apps/connect-to-anchors/metadata.json @@ -0,0 +1,4 @@ +{ + "order": 70, + "title": "Deposit and Withdraw From Anchors" +} diff --git a/docs/building-apps/connect-to-anchors/setup-for-anchored-assets.mdx b/docs/building-apps/connect-to-anchors/setup-for-anchored-assets.mdx new file mode 100644 index 000000000..d9905450a --- /dev/null +++ b/docs/building-apps/connect-to-anchors/setup-for-anchored-assets.mdx @@ -0,0 +1,358 @@ +--- +title: Setup for Anchored Assets +order: 10 +--- + +import { CodeExample } from "@site/src/components/CodeExample"; + +So how do we make use of SEP-0024 to allow deposits and withdrawals of anchored assets? First let’s boot up our project: + +[View trustline boilerplate code on GitHub][1] + +Next, let's think about our goals. What we’re trying to do is communicate with Anchors to get information about the assets they honor, and about the requirements and steps necessary to initiate a deposit or withdrawal on behalf of a user. When a user deposits with an Anchor — by sending dollars to their bank account via ACH, for example — the Anchor will credit their Stellar account with the equivalent amount of tokens. The user can then hold, transfer, or trade those tokens just like any other Stellar asset. When a user withdraws those tokens — generally by making a payment back to the Anchor's Stellar account — the Anchor redeems them for cash in hand or money in the bank. If you’re still a little lost it will all hopefully become clear as we get coding. + +This tutorial will consist of a few minor `./events/` updates and two new `./methods/`. Let’s start with the updates first. We actually need to update the core `wallet.ts` component. + +## Update Wallet Component + + + +```ts +import { Component, State, Prop } from "@stencil/core"; +import { Server, ServerApi } from "stellar-sdk"; + +import componentWillLoad from "./events/componentWillLoad"; // UPDATE +import render from "./events/render"; // UPDATE + +import createAccount from "./methods/createAccount"; +import updateAccount from "./methods/updateAccount"; +import depositAsset from "./methods/depositAsset"; // NEW +import withdrawAsset from "./methods/withdrawAsset"; // NEW +import trustAsset from "./methods/trustAsset"; +import makePayment from "./methods/makePayment"; +import copyAddress from "./methods/copyAddress"; +import copySecret from "./methods/copySecret"; +import signOut from "./methods/signOut"; +import setPrompt from "./methods/setPrompt"; + +import { Prompter } from "@prompt/prompt"; + +interface StellarAccount { + publicKey: string; + keystore: string; + state?: ServerApi.AccountRecord; +} + +interface Loading { + fund?: boolean; + pay?: boolean; + trust?: boolean; + update?: boolean; + deposit?: boolean; // NEW + withdraw?: boolean; // NEW +} + +@Component({ + tag: "stellar-wallet", + styleUrl: "wallet.scss", + shadow: true, +}) +export class Wallet { + @State() account: StellarAccount; + @State() prompter: Prompter = { show: false }; + @State() loading: Loading = {}; + @State() error: any = null; + + @Prop() server: Server; + @Prop() homeDomain: String; // NEW + @Prop() toml: Object; // NEW + + // Component events + componentWillLoad() {} + render() {} + + // Stellar methods + createAccount = createAccount; + updateAccount = updateAccount; + depositAsset = depositAsset; // NEW + withdrawAsset = withdrawAsset; // NEW + trustAsset = trustAsset; + makePayment = makePayment; + copyAddress = copyAddress; + copySecret = copySecret; + signOut = signOut; + + // Misc methods + setPrompt = setPrompt; +} + +Wallet.prototype.componentWillLoad = componentWillLoad; +Wallet.prototype.render = render; +``` + + + +You can see from the `// NEW` and `// UPDATE` comments what we are adding and updating. Nothing worth noting here other than near the bottom two new `@Prop`’s. + + + +```ts +@Prop() homeDomain: String // NEW +@Prop() toml: Object // NEW +``` + + + +We’ll talk more about home domain’s and stellar.toml files in a moment, but take special note of these as they will play a critical roll in connecting tp the world outside of Stellar. + +## Add Deposit and Withdraw Buttons + +Next let’s add a couple buttons for deposit and withdraw to the `./events/render.tsx`. + + + +```tsx +import { h } from "@stencil/core"; +import { has as loHas } from "lodash-es"; + +export default function render() { + return [ + , + + this.account ? ( + [ + , + + , + , + + , + , + ] + ) : ( + + ), + + this.error ? ( +
{JSON.stringify(this.error, null, 2)}
+ ) : null, + + loHas(this.account, "state") ? ( + + ) : null, + + this.account + ? [ + , + , + ] + : null, + ]; +} +``` + +
+ +This is the same as what we did in the previous tutorial except that we're adding two buttons. + + + +```tsx +, +, +``` + + + +“Deposit Asset” and “Withdraw Asset” connect to the `this.depositAsset` and `this.withdrawAsset` methods respectively. We’ll create those methods momentarily. + +Before that though let’s make a change to the `./events/componentWillLoad.ts` file. + +## Update Components + + + +```ts +import { Server, StellarTomlResolver } from "stellar-sdk"; +import { handleError } from "@services/error"; +import { get } from "@services/storage"; + +export default async function componentWillLoad() { + try { + let keystore = await get("keyStore"); + + this.error = null; + this.server = new Server("https://horizon-testnet.stellar.org"); + this.homeDomain = "testanchor.stellar.org"; + this.toml = await StellarTomlResolver.resolve(this.homeDomain); + + if (keystore) { + keystore = atob(keystore); + + const { publicKey } = JSON.parse(atob(JSON.parse(keystore).adata)); + + this.account = { + publicKey, + keystore, + }; + + this.updateAccount(); + } + } catch (err) { + this.error = handleError(err); + } +} +``` + + + +Here, the only changes we're making are to include of the `StellarTomlResolver` from the `stellar-sdk` package and to set the values for `this.homeDomain` and `this.toml`. + + + +```ts +this.homeDomain = "testanchor.stellar.org"; +this.toml = await StellarTomlResolver.resolve(this.homeDomain); +``` + + + +## About stellar.toml Files + +You know what, let’s just go ahead and cover stellar.toml files. A `stellar.toml` file is a static resource that organizations building on Stellar publish on their home domain at `https://{homeDomain}/.well-known/stellar.toml`. By linking their home domain to a Stellar account using a `set_options` operation, they create a verifiable connection from that account to the information published there. To find out everything you'd ever want to know about stellar.toml files, check out [SEP-1], which is the complete stellar.toml specification. + +Stellar.toml files contain all sorts of information about an organization, the assets they offer, and the integrations they support. Wallets can look at an account, find the home domain, and crawl a stellar.toml to find out pretty much everything they need to know about an Anchor, and that's exactly what the Stellar SDK `StellarTomlResolver.resolve` method does: it pulls in a stellar.toml and parses it. + +Let's look at some concrete examples: here’s [StellarX.com’s stellar.toml file][2]. + + + +```toml +# StellarX +VERSION="2.2.0" +FEDERATION_SERVER="https://federation.stellarx.com/federation" +ACCOUNTS=[ +"GDM4UWTGHCWSTM7Z46PNF4BLH35GS6IUZYBWNNI4VU5KVIHYSIVQ55Y6" +] +[DOCUMENTATION] +ORG_NAME="Ultra Stellar LLC" +ORG_DBA="StellarX" +ORG_URL="https://www.stellarx.com" +ORG_LOGO="https://www.stellarx.com/emailAssets/stellarx.png" +ORG_DESCRIPTION="The first full-featured trading app for Stellar's universal marketplace" +ORG_PHYSICAL_ADDRESS="Tallinn, Estonia" +ORG_OFFICIAL_EMAIL="support@stellarx.com" +``` + + + +And here's [AnchorUSD]'s[3]. + + + +```toml +# ----- AnchorUSD Stellar Anchor ----- + +NETWORK_PASSPHRASE="Public Global Stellar Network ; September 2015" + +ACCOUNTS=["GDUKMGUGDZQK6YHYA5Z6AY2G4XDSZPSZ3SW5UN3ARVMO6QSRDWP5YLEX", "GASWJWFRYE55KC7MGANZMMRBK5NPXT3HMPDQ6SEXZN6ZPWYXVVYBFRTE"] +TRANSFER_SERVER="https://api.anchorusd.com/transfer/" +TRANSFER_SERVER_SEP0024="https://api.anchorusd.com/transfer/" +WEB_AUTH_ENDPOINT="https://api.anchorusd.com/api/webauth" +SIGNING_KEY="GASWJWFRYE55KC7MGANZMMRBK5NPXT3HMPDQ6SEXZN6ZPWYXVVYBFRTE" + +[DOCUMENTATION] +ORG_NAME="AnchorCoin LLC" +ORG_DBA="AnchorUSD" +ORG_URL="https://www.anchorusd.com" +ORG_LOGO="https://stablecoin.anchorusd.com/img/anchorusd.png" +ORG_DESCRIPTION="AnchorUSD develops a stable cryptocurrency backed one-for-one to the US dollar." +ORG_TWITTER="AnchorUSD" +ORG_OFFICIAL_EMAIL="support@anchorusd.com" + +[[PRINCIPALS]] +name="Jim Berkley-Danz" +email="j@anchorusd.com" + +[[CURRENCIES]] +code="USD" +issuer="GDUKMGUGDZQK6YHYA5Z6AY2G4XDSZPSZ3SW5UN3ARVMO6QSRDWP5YLEX" +display_decimals=2 +status="live" +is_asset_anchored=true +anchor_asset_type="fiat" +anchor_asset="USD" +name="US Dollar" +desc="Cryptocurrency backed one-for-one to the US dollar. All dollar deposits are held in an audited, US-domiciled escrow account for the exclusive benefit of AnchorUSD token holders." +image="https://stablecoin.anchorusd.com/img/usdx.png" +``` + + + +You can see the data provided by these companies that identifies who they are, what they do, and what services they provide. In this tutorial, we’re interested in the `[[CURRENCIES]]` they issue as that’s what we’re trying to get ahold of. We’re also looking for the `TRANSFER_SERVER` keyword, which indicates that an Anchor supports SEP-24, and the `WEB_AUTH_ENDPOINT`, which allows a wallet to set up an authenticated user session. Any time you find these three fields, and you’ve found an Anchor you can interoperate with. + +Once we’ve found an Anchor that supports deposit and withdrawal, we can begin the process of connecting with them from our wallet. This tutorial builds on the testnet, so from we’ll be use an SDF testing anchor server located at `testanchor.stellar.org` You can [view the TOML file for this entity][5] here. + +[1]: https://github.com/stellar/docs-wallet/tree/trustline +[2]: https://www.stellarx.com/.well-known/stellar.toml +[3]: https://stablecoin.anchorusd.com/.well-known/stellar.toml +[4]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0001.md +[5]: https://testanchor.stellar.org/.well-known/stellar.toml +[6]: /4%20SEP-0024%20%E2%80%93%20Make%20Use%20of%20Anchors/1%20SEP-0024%20Explained.md +[7]: /4%20SEP-0024%20%E2%80%93%20Make%20Use%20of%20Anchors/3%20Deposit%20Anchored%20Assets.md diff --git a/docs/building-apps/connect-to-anchors/withdraw-anchored-assets.mdx b/docs/building-apps/connect-to-anchors/withdraw-anchored-assets.mdx new file mode 100644 index 000000000..92a2abb11 --- /dev/null +++ b/docs/building-apps/connect-to-anchors/withdraw-anchored-assets.mdx @@ -0,0 +1,326 @@ +--- +title: Withdraw Anchored Assets +order: 30 +--- + +import { CodeExample } from "@site/src/components/CodeExample"; +import { Alert } from "@site/src/components/Alert"; + + + +At this point, you should have a [basic wallet](../basic-wallet.mdx) that can handle [custom assets](../custom-assets.mdx), and you should have [enabled deposits](./deposit-anchored-assets.mdx) and used the SDF-maintained testnet [Anchor Reference Implementation](https://testanchor.stellar.org/.well-known/stellar.toml) to get some test tokens into your wallet. After you’ve had some fun with your new asset, it's time to learn to perform this process in reverse, and that's what we'll do here: set your wallet up to handle withdrawals. + + + +In a live situation, this is the step when a user takes a Stellar-based token and redeems it with an Anchor for the underlying asset it represents. It's how they'd move money off the network and back into their bank account, for instance. + +## Create Withdraw Method + +To start, create `./methods/withdrawAsset.ts` and pop this in. + + + +```ts +import sjcl from "@tinyanvil/sjcl"; +import { + Transaction, + Keypair, + Account, + TransactionBuilder, + BASE_FEE, + Networks, + Operation, + Asset, + Memo, + MemoHash, +} from "stellar-sdk"; + +import axios from "axios"; +import { + get as loGet, + each as loEach, + findIndex as loFindIndex, +} from "lodash-es"; + +import { handleError } from "@services/error"; + +export default async function withdrawAsset(e: Event) { + try { + e.preventDefault(); + + let currency = await this.setPrompt( + "Select the currency you'd like to withdraw", + null, + this.toml.CURRENCIES, + ); + currency = currency.split(":"); + + const pincode = await this.setPrompt("Enter your keystore pincode"); + + if (!pincode) return; + + const keypair = Keypair.fromSecret( + sjcl.decrypt(pincode, this.account.keystore), + ); + + const balances = loGet(this.account, "state.balances"); + const hasCurrency = loFindIndex(balances, { + asset_code: currency[0], + asset_issuer: currency[1], + }); + + if (hasCurrency === -1) + await this.trustAsset(null, currency[0], currency[1], pincode); + + const info = await axios + .get(`${this.toml.TRANSFER_SERVER}/info`) + .then(({ data }) => data); + + console.log(info); + + const auth = await axios + .get(`${this.toml.WEB_AUTH_ENDPOINT}`, { + params: { + account: this.account.publicKey, + }, + }) + .then(async ({ data: { transaction, network_passphrase } }) => { + const txn: any = new Transaction(transaction, network_passphrase); + + this.error = null; + this.loading = { ...this.loading, withdraw: true }; + + txn.sign(keypair); + return txn.toXDR(); + }) + .then((transaction) => + axios.post( + `${this.toml.WEB_AUTH_ENDPOINT}`, + { transaction }, + { headers: { "Content-Type": "application/json" } }, + ), + ) + .then(({ data: { token } }) => token); + + console.log(auth); + + const formData = new FormData(); + + loEach( + { + asset_code: currency[0], + account: this.account.publicKey, + lang: "en", + }, + (value, key) => formData.append(key, value), + ); + + const interactive = await axios + .post( + `${this.toml.TRANSFER_SERVER}/transactions/withdraw/interactive`, + formData, + { + headers: { + Authorization: `Bearer ${auth}`, + "Content-Type": "multipart/form-data", + }, + }, + ) + .then(({ data }) => data); + + console.log(interactive); + + const transactions = await axios + .get(`${this.toml.TRANSFER_SERVER}/transactions`, { + params: { + asset_code: currency[0], + limit: 1, + kind: "withdrawal", + }, + headers: { + Authorization: `Bearer ${auth}`, + }, + }) + .then(({ data: { transactions } }) => transactions); + + console.log(transactions); + + const urlBuilder = new URL(interactive.url); + urlBuilder.searchParams.set("callback", "postMessage"); + const popup = open(urlBuilder.toString(), "popup", "width=500,height=800"); + + if (!popup) { + this.loading = { ...this.loading, withdraw: false }; + throw 'Popups are blocked. You\'ll need to enable popups for this demo to work'; + } + + await new Promise((resolve, reject) => { + let submittedTxn; + + window.onmessage = ({ data: { transaction } }) => { + console.log(transaction.status, transaction); + + if (transaction.status === "completed") { + this.updateAccount(); + this.loading = { ...this.loading, withdraw: false }; + resolve(); + } else if ( + !submittedTxn && + transaction.status === "pending_user_transfer_start" + ) { + this.server + .accounts() + .accountId(keypair.publicKey()) + .call() + .then(({ sequence }) => { + const account = new Account(keypair.publicKey(), sequence); + const txn = new TransactionBuilder(account, { + fee: BASE_FEE, + networkPassphrase: Networks.TESTNET, + }) + .addOperation( + Operation.payment({ + destination: transaction.withdraw_anchor_account, + asset: new Asset(currency[0], currency[1]), + amount: transaction.amount_in, + }), + ) + .addMemo(new Memo(MemoHash, transaction.withdraw_memo)) + .setTimeout(0) + .build(); + + txn.sign(keypair); + return this.server.submitTransaction(txn); + }) + .then((res) => { + console.log(res); + submittedTxn = res; + + const urlBuilder = new URL(transaction.more_info_url); + urlBuilder.searchParams.set("callback", "postMessage"); + + popup.location.replace(urlBuilder.toString()); + }) + .catch((err) => reject(err)); + } else { + setTimeout(() => { + const urlBuilder = new URL(transaction.more_info_url); + urlBuilder.searchParams.set("callback", "postMessage"); + + popup.location.replace(urlBuilder.toString()); + }, 1000); + } + }; + }); + } catch (err) { + this.loading = { ...this.loading, withdraw: false }; + this.error = handleError(err); + } +} +``` + + + +We’ll actually skip everything except the stuff that is significantly different than the [deposit flow](./deposit-anchored-assets.mdx). The main difference between the two is who submits the Stellar transaction: in the deposit flow the anchor is sends the wallet a Stellar asset; in the withdraw flow the wallet sends the asset to the anchor. So for the witdraw flow, we’ll need to build and submit a payment transaction. + +## Create and Submit Payment Transaction + + + +```ts + await new Promise((resolve, reject) => { + let submittedTxn + + window.onmessage = ({data: {transaction}}) => { + console.log(transaction.status, transaction) + + if (transaction.status === 'completed') { + this.updateAccount() + this.loading = {...this.loading, withdraw: false} + resolve() + } +``` + + + +First thing we’ll see is the use of a Promise. This allows us to respond to any errors in the transaction we’re about to build and submit. Inside the promise we have three if statement blocks. The first `if` statement is a response to a status of success, which is where we'll end up once the withdraw has registered and everything is hunky dory. + + + +```ts + else if ( + !submittedTxn + && transaction.status === 'pending_user_transfer_start' + ) { + this.server + .accounts() + .accountId(keypair.publicKey()) + .call() + .then(({sequence}) => { + const account = new Account(keypair.publicKey(), sequence) + const txn = new TransactionBuilder(account, { + fee: BASE_FEE, + networkPassphrase: Networks.TESTNET + }) + .addOperation(Operation.payment({ + destination: transaction.withdraw_anchor_account, + asset: new Asset(currency[0], currency[1]), + amount: transaction.amount_in + })) + .addMemo(new Memo(MemoHash, transaction.withdraw_memo)) + .setTimeout(0) + .build() + + txn.sign(keypair) + return this.server.submitTransaction(txn) + }) + .then((res) => { + console.log(res) + submittedTxn = res + + const urlBuilder = new URL(transaction.more_info_url) + urlBuilder.searchParams.set('callback', 'postMessage') + + popup.location.replace(urlBuilder.toString()) + }) + .catch((err) => reject(err)) + } +``` + + + +Otherwise if the transaction status is `pending_user_transfer_start` and we haven’t yet submitted a transaction to the Anchor, we attempt that. We load up the user's account to get the next valid sequence number, and build a Stellar [transaction](../../glossary/transactions.mdx) consisting of a [payment operation](../../glossary/operations.mdx) with all the details from the transaction object that the anchor is expecting. Once we have a valid transaction built, we’ll sign it with the keypair, submit the transaction to the network, and wait for a response. If the transaction is successful, we save that value to `submittedTxn` and reload the anchor popup to observe the pending status. Make sure to set the `submittedTxn` to a truthy value or else you run the risk of submitting the transaction multiple times, as the anchor may take a moment to realize you’ve successfully submitted a transaction to them. + + + +```ts + else { + setTimeout(() => { + const urlBuilder = new URL(transaction.more_info_url) + urlBuilder.searchParams.set('callback', 'postMessage') + + popup.location.replace(urlBuilder.toString()) + }, 1000) + } + } + }) + } + + catch (err) { + this.loading = {...this.loading, withdraw: false} + this.error = handleError(err) + } +} +``` + + + +The last if block is that if all else fails just keep reloading the anchor popup every second until we get a `'completed'` status. + +Finally catch and respond to any errors. + +With this method saved and the server reloaded we have a fully functional SEP-0024 compliant wallet! Noice! + +[View this code on GitHub][1] + +[1]: https://github.com/stellar/docs-wallet/tree/sep24 diff --git a/docs/building-apps/custom-assets.mdx b/docs/building-apps/custom-assets.mdx new file mode 100644 index 000000000..dac84c93f --- /dev/null +++ b/docs/building-apps/custom-assets.mdx @@ -0,0 +1,457 @@ +--- +title: Handle Custom Assets +order: 60 +--- + +import { CodeExample } from "@site/src/components/CodeExample"; +import { Alert } from "@site/src/components/Alert"; + + + +In this section of the tutorial, we'll add the ability to hold and transfer custom assets to the basic wallet we built in previous sections. It asssumes that you've already completed [Build a Basic Wallet](./basic-wallet.mdx) and [Make XLM Payments](./xlm-payments.mdx). + + + +[View pay boilerplate code on GitHub][1] + +## What's a Custom Asset? + +Stellar allows anyone to easily issue an asset, and all assets can be held, transferred, and traded just like XLM, the network token. Every asset _other_ than XLM exists on the network in the form of trustlines: an asset holder explicitly agrees to allow a balance of a specific token issued by a specific issuing account by creating a persistent ledger entry tied to the holding account. You can find out more in the guide to [creating custom assets](../issuing-assets/index.mdx). + +Each trustline increases the user's [base reserve](../glossary/minimum-balance.mdx) by 0.5 XLM, and in this tutorial, we'll go over how to set up your wallet to create trustlines and manage the base reserve on behalf of a user. + +## Add Trustlines Button + +To enable custom asset handling, we need to modify three files and create one new one. Let’s start with our modifications. First up the `./events/render.tsx` file. We need to add a button for creating these new trustlines! + + + +```tsx +import { h } from "@stencil/core"; +import { has as loHas } from "lodash-es"; + +export default function render() { + return [ + , + + this.account ? ( + [ + , + + , + , + ] + ) : ( + + ), + + this.error ? ( +
{JSON.stringify(this.error, null, 2)}
+ ) : null, + + loHas(this.account, "state") ? ( + + ) : null, + + this.account + ? [ + , + , + ] + : null, + ]; +} +``` + +
+ +If you look closely you’ll spot the Trust Asset button right below our `account-key` div. Nothing funky here, just a button that triggers `this.trustAsset` method which we’ll add in a moment. + +Next up, let’s update the `./methods/makePayment.ts` file. + + + +```ts +import sjcl from "@tinyanvil/sjcl"; +import { + Keypair, + Account, + TransactionBuilder, + BASE_FEE, + Networks, + Operation, + Asset, +} from "stellar-sdk"; +import { has as loHas } from "lodash-es"; + +import { handleError } from "@services/error"; + +export default async function makePayment(e: Event) { + try { + e.preventDefault(); + + let instructions = await this.setPrompt("{Amount} {Asset} {Destination}"); + instructions = instructions.split(" "); + + if (!/xlm/gi.test(instructions[1])) + instructions[3] = await this.setPrompt( + `Who issues the ${instructions[1]} asset?`, + "Enter ME to refer to yourself", + ); + + const pincode = await this.setPrompt("Enter your keystore pincode"); + + if (!instructions || !pincode) return; + + const keypair = Keypair.fromSecret( + sjcl.decrypt(pincode, this.account.keystore), + ); + + if (/me/gi.test(instructions[3])) instructions[3] = keypair.publicKey(); + + this.error = null; + this.loading = { ...this.loading, pay: true }; + + await this.server + .accounts() + .accountId(keypair.publicKey()) + .call() + .then(({ sequence }) => { + const account = new Account(keypair.publicKey(), sequence); + const transaction = new TransactionBuilder(account, { + fee: BASE_FEE, + networkPassphrase: Networks.TESTNET, + }) + .addOperation( + Operation.payment({ + destination: instructions[2], + asset: instructions[3] + ? new Asset(instructions[1], instructions[3]) + : Asset.native(), + amount: instructions[0], + }), + ) + .setTimeout(0) + .build(); + + transaction.sign(keypair); + return this.server.submitTransaction(transaction).catch((err) => { + if ( + // Paying an account which doesn't exist, create it instead + loHas(err, "response.data.extras.result_codes.operations") && + err.response.data.status === 400 && + err.response.data.extras.result_codes.operations.indexOf( + "op_no_destination", + ) !== -1 && + !instructions[3] + ) { + const transaction = new TransactionBuilder(account, { + fee: BASE_FEE, + networkPassphrase: Networks.TESTNET, + }) + .addOperation( + Operation.createAccount({ + destination: instructions[2], + startingBalance: instructions[0], + }), + ) + .setTimeout(0) + .build(); + + transaction.sign(keypair); + return this.server.submitTransaction(transaction); + } else throw err; + }); + }) + .then((res) => console.log(res)) + .finally(() => { + this.loading = { ...this.loading, pay: false }; + this.updateAccount(); + }); + } catch (err) { + this.error = handleError(err); + } +} +``` + + + +This is a big file that was covered in great detail in the [Make XLM Payments](./xlm-payments.mdx) tutorial, so we’ll just focus on the changes we need to make to support custom asset payments. + + + +```ts +let instructions = await this.setPrompt("{Amount} {Asset} {Destination}"); +instructions = instructions.split(" "); + +if (!/xlm/gi.test(instructions[1])) + instructions[3] = await this.setPrompt( + `Who issues the ${instructions[1]} asset?`, + "Enter ME to refer to yourself", + ); +``` + + + +This change allows us to indicate a specific asset code we’d like use to make a payment and triggers an additional prompt to set the issuer for that asset if it’s not the native XLM. + + + +```ts +if (/me/gi.test(instructions[3])) instructions[3] = keypair.publicKey(); +``` + + + +This is just a nifty little helper shortcut to allow us to use the `ME` “issuer” to swap with our actual account publicKey. Niceties make the world go ‘round. + + + +```ts +asset: instructions[3] ? new Asset(instructions[1], instructions[3]) : Asset.native(), +``` + + + +The final noteworthy change is a ternary operation that switches our payment asset between the native XLM and a custom asset based off of responses to our prompt. Essentially, if `instructions[3]` exists — meaning there is an issuer — use that issuer and custom token as the asset for the payment. Otherwise, just use the native `Asset`. + +The final changes are in the `wallet.ts` itself and tie together all the other updates as well as pull in the new `trustAsset` method. + + + +```ts +import { Component, State, Prop } from "@stencil/core"; +import { Server, ServerApi } from "stellar-sdk"; + +import componentWillLoad from "./events/componentWillLoad"; +import render from "./events/render"; + +import createAccount from "./methods/createAccount"; +import updateAccount from "./methods/updateAccount"; +import trustAsset from "./methods/trustAsset"; // NEW +import makePayment from "./methods/makePayment"; // UPDATE +import copyAddress from "./methods/copyAddress"; +import copySecret from "./methods/copySecret"; +import signOut from "./methods/signOut"; +import setPrompt from "./methods/setPrompt"; + +import { Prompter } from "@prompt/prompt"; + +interface StellarAccount { + publicKey: string; + keystore: string; + state?: ServerApi.AccountRecord; +} + +interface Loading { + // UPDATE + fund?: boolean; + pay?: boolean; + trust?: boolean; // NEW + update?: boolean; +} + +@Component({ + tag: "stellar-wallet", + styleUrl: "wallet.scss", + shadow: true, +}) +export class Wallet { + @State() account: StellarAccount; + @State() prompter: Prompter = { show: false }; + @State() loading: Loading = {}; + @State() error: any = null; + + @Prop() server: Server; + + // Component events + componentWillLoad() {} + render() {} + + // Stellar methods + createAccount = createAccount; + updateAccount = updateAccount; + trustAsset = trustAsset; // NEW + makePayment = makePayment; // UPDATE + copyAddress = copyAddress; + copySecret = copySecret; + signOut = signOut; + + // Misc methods + setPrompt = setPrompt; +} + +Wallet.prototype.componentWillLoad = componentWillLoad; +Wallet.prototype.render = render; +``` + + + +Only thing worth seeing here besides the inclusion of the new `trustAsset` method is the addition of the `trust?: boolean,` in the `Loading` class. + +## Add Trustlines + +Alright so, finally we get to the `./methods/trustAsset.ts` file! + + + +```ts +import sjcl from "@tinyanvil/sjcl"; +import { + Keypair, + Account, + TransactionBuilder, + BASE_FEE, + Networks, + Operation, + Asset, +} from "stellar-sdk"; + +import { handleError } from "@services/error"; + +export default async function trustAsset( + e?: Event, + asset?: string, + issuer?: string, + pincode?: string, +) { + try { + if (e) e.preventDefault(); + + let instructions; + + if (asset && issuer) instructions = [asset, issuer]; + else { + instructions = await this.setPrompt("{Asset} {Issuer}"); + instructions = instructions.split(" "); + } + + pincode = pincode || (await this.setPrompt("Enter your keystore pincode")); + + if (!instructions || !pincode) return; + + const keypair = Keypair.fromSecret( + sjcl.decrypt(pincode, this.account.keystore), + ); + + this.error = null; + this.loading = { ...this.loading, trust: true }; + + await this.server + .accounts() + .accountId(keypair.publicKey()) + .call() + .then(({ sequence }) => { + const account = new Account(keypair.publicKey(), sequence); + const transaction = new TransactionBuilder(account, { + fee: BASE_FEE, + networkPassphrase: Networks.TESTNET, + }) + .addOperation( + Operation.changeTrust({ + asset: new Asset(instructions[0], instructions[1]), + }), + ) + .setTimeout(0) + .build(); + + transaction.sign(keypair); + return this.server.submitTransaction(transaction); + }) + .then((res) => console.log(res)) + .finally(() => { + this.loading = { ...this.loading, trust: false }; + this.updateAccount(); + }); + } catch (err) { + this.error = handleError(err); + } +} +``` + + + +This is similar to the `makePayment` method but there are a couple tiny tweaks worth noting: + + + +```ts +export default async function trustAsset( + e?: Event, + asset?: string, + issuer?: string, + pincode?: string +) { + try { + if (e) + e.preventDefault() + + let instructions + + if ( + asset + && issuer + ) instructions = [asset, issuer] + + else { + instructions = await this.setPrompt('{Asset} {Issuer}') + instructions = instructions.split(' ') + } + + pincode = pincode || await this.setPrompt('Enter your keystore pincode') +``` + + + +We’re allowing the inclusion of several arguments in this function, namely `asset`, `issuer`, and `pincode`. We won’t be making use of them here, but transparently creating trustlines from within other functions will prove useful later. + +If we have any of those variables set, we can “preload” our interface a bit, and even bypass user input altogether if a pincode is provided. Again, not something we’ll make use of quite yet, but once we look into depositing and withdrawing assets from an Anchor or accepting incoming payments for which we don’t yet have a trustline this functionality will prove useful. + +So there we have it! The ability to accept and pay with custom assets on Stellar! + +[1]: https://github.com/stellar/docs-wallet/tree/pay +[3]: https://github.com/stellar/docs-wallet/tree/trustline diff --git a/docs/building-apps/first-deposit.mdx b/docs/building-apps/first-deposit.mdx new file mode 100644 index 000000000..95f2b7378 --- /dev/null +++ b/docs/building-apps/first-deposit.mdx @@ -0,0 +1,60 @@ +--- +title: Fund the Account and make the First Deposit +order: 65 +--- + +In this section, we will go over the new user account creation flow between non-custodial wallets and anchors with SEP-24 and/or SEP-6 implementations. Before we dive into the flow, it’s important to understand how Stellar accounts are created in the first place. + +The first step in creating a Stellar account is to generate a keypair. This keypair includes public and private keys. However, an account does not exist and does not warrant space on the ledger until it’s funded with the minimum balance of 1XLM by a [Create Account](../start/list-of-operations.mdx#create-acccount) operation. This requirement was created to minimize unused accounts from bloating the ledger. For a more in-depth look at this topic, check out this [page](../tutorials/create-account.mdx). + +## Who creates the new user accounts? + +When a new customer downloads the wallet application and goes through the deposit flow for the first time, their Stellar account can be created by either the user’s wallet application or the anchor facilitating the first deposit. This section describes the possible strategies and flows for you to consider. + +### Option 1: The anchor creates and funds the Stellar account + +For this option, the wallet needs to allow users to initiate their first deposit without having to add an asset/establish a trustline. The wallet then prompts the user to add the trustline once funds are received by the anchor. The flow looks like this: + +1. The wallet registers a new user and issues a keypair. +1. The wallet initiates the first deposit on behalf of the user without requiring the user to add the asset/create the trustline. +1. The anchor provides deposit instructions to the customer. +1. The user transfers money from a bank account to the anchor’s bank account. +1. Once the anchor receives the transfer, the anchor creates and funds the Stellar account for the customer. +1. The wallet detects that the account has been created and a trustline must be established. +1. The wallet prompts the user to add the asset/create the trustline. +1. Finally, the anchor sends the deposit funds to the user’s Stellar account. + +**Note**: An anchor should always maintain a healthy amount of XLM in its distribution account to support new account creations. If doing so becomes unsustainable, it’s recommended that the anchor collaborates with wallets to determine a strategy based on the number of account creation requests. The recommended amount is 2XLM per user account creation (1XLM to meet the minimum balance requirement, and 1XLM for establishing trustlines and covering transaction fees). + +![anchor creates account flow](../web-assets/first-deposit-anchor-flow.png) + +With the flow described above, the wallet and the anchor have to facilitate listening for and responding to the trustline status, which can create user experience frictions when waiting for the trustline to be established. To address this issue, Protocol 15 introduced [Claimable Balances](../glossary/claimable-balance.mdx), which enhance the flow by allowing users to start using the wallet without having to secure XLM wait to create the trustline after they made their first deposit. Both the wallet and the anchor have to implement Claimable Balance support in order to make this flow work. + +The flow with Claimable Balances looks like this: + +1. The wallet registers a new user, and generates a keypair. +1. The wallet initiates a deposit on behalf of a user. +1. The anchor provides deposit instructions to the wallet. +1. The user transfers money from a bank account to the anchor’s account. +1. The anchor creates and funds the user's Stellar account plus the amount required for trustlines and transaction fees. Again, we suggest 2 XLM to start. +1. The anchor creates a Claimable Balance. +1. The wallet detects the Claimable Balance for the account, claims the funds, and posts it in the wallet. + +![anchor creates account claimable balance flow](../web-assets/first-deposit-claimable-balance-flow.png) + +### Option 2: The wallet creates and funds the Stellar account upon user sign-up + +For this option, the wallet creates and funds the Stellar account upon every new user sign-up with the minimum requirement of 1XLM, plus the .5XLM reserve for establishing the first trustline, plus a bit more to cover transaction fees. For more information on minimum balances, check out the [glossary entry](../glossary/minimum-balance.mdx). + +The flow looks like this: + +1. Upon a new user signup, the wallet issues a keypair, then creates and funds the user's Stellar account with 2XLM. +1. Then the wallet creates a trustline, and initiates the first deposit. +1. Once the deposit request is sent to the anchor, the anchor provides instructions for the deposit. +1. The customer transfer funds from a personal bank account to the anchor’s account. +1. The anchor receives the funds, then sends them to the user’s Stellar account. +1. The wallet detects that funds were sent and notifies the user. + +![wallet creates account flow](../web-assets/first-deposit-wallet-flow.png) + +**Note**: In the examples above, we suggest having the anchor or wallet cover minimum balance and trustline XLM requirements by depositing funds directly into a user's account. We made that suggestion for the sake of simplicity, but in all cases, the anchor or wallet could instead use [Sponsored Reserves](../glossary/sponsored-reserves.mdx) to ensure that when a user closes a trustline or merges their account, the reserve reverts to the sponsoring account rather than to the user's account. diff --git a/docs/building-apps/index.mdx b/docs/building-apps/index.mdx new file mode 100644 index 000000000..85ecac084 --- /dev/null +++ b/docs/building-apps/index.mdx @@ -0,0 +1,26 @@ +--- +title: Overview +order: 0 +--- + +Stellar is a self-serve distributed ledger that you can use as a backend to power all kinds of apps and services. It has built-in logic for creating accounts, signing transactions, and tracking balances, and anyone can use it to issue, store, transfer, and trade assets. Since many of those assets connect to real-world currencies, and since there are open protocols for integrating deposit and withdrawal of those assets, a Stellar-based app can take advantage of real banking rails and connect to real money. + +Currently, developers use Stellar to power cross-border payment apps, currency exchanges, micropayment services, and platforms for in-game purchases, but what you build — and how you build it — is up to you. At its core, though, any app built on Stellar relies on the same basic functions: key storage, account creation, transaction signing, and queries to the Stellar database. To make things easy, and to stick with common parlance, we’ll call the part of an app that implements those core functions a _wallet_. This section of the docs will walk you through the process of building one. + +Stellar wallets can do more than store and move assets, and they don't have to be the end product for your users. They can be front and center in an application, or function invisibly in the background without revealing anything about Stellar to the end user. However you decide to surface the UI of the wallet, there are several basic considerations and common implementations, and the goal of this section of the docs is to cover those by showing you how to build a basic wallet UI. + +It’s a step-by-step tutorial explaining how to construct a robust Stellar wallet following best practices. If you follow it to the end, you'll have a fully functional Stellar interface, and a strong framework for continued development of your own use case. + +## What’s in a Wallet? + +Unlike real-world wallets, which hold real-world cash, Stellar wallets don’t hold digital cash. Not directly. Rather, they allow users to view the history and current state of the Stellar ledger, and to sign and submit transactions. Accounts, balances, and offers to buy and sell assets are kept on the ledger itself, which is shared by all the nodes that make up the network. A wallet may store references or caches to the Stellar database, but the actual record and the value it tracks are on chain. + +What does this mean for the wallet we’re about to build? Good question. It means the wallet acts as a visual and interactive layer on top of Stellar rather than as an extension or storage mechanism. Our conversation moving forward will revolve around accessing Stellar and surfacing data in the network rather than "running" Stellar or storing something on our end. + +A neat outflow of this is that most Stellar tools can operate almost entirely as client-side applications and services. There will certainly be server-side logic specific to your app, but the actual wallet interactions can operate openly on the client/browser. + +## Securing Your Wallet + +Even though wallets can operate client-side, they deal with a user’s secret keys, which give direct access to their account, and to any value they hold in it. That’s why it’s essential to require all web traffic to flow over strong TLS methods. Even when developing locally, use a self-signed localhost certificate to develop secure habits from the very beginning. Stellar may be incredibly easy and intuitive, but it’s also powerful money-moving software. Don’t skimp on security. Your future self and all your users thank you. For more, check out our guide to [securing web-based products][1]. + +[1]: https://www.stellar.org/developers/guides/walkthroughs/securing-web-projects.html diff --git a/docs/building-apps/key-management.mdx b/docs/building-apps/key-management.mdx new file mode 100644 index 000000000..c5d6c66b6 --- /dev/null +++ b/docs/building-apps/key-management.mdx @@ -0,0 +1,38 @@ +--- +title: Key Management Basics +order: 30 +--- + +Step one for any app is to sort out user onboarding. Since a Stellar wallet is an interface that gives a user access to an account stored on the ledger, and since that access is controlled by the account's secret key, the first thing you have to decide is how to handle a user's secret key, and how to append the Stellar account it unlocks to a user object. + +The most important question: who will “own” the account? There are three possible answers: + +1. You, the service provider, store the secret key and delegate usage rights to the user. This is a “custodial” service. +1. They, the user, will store their own account credentials and permission your app to send requests or delegate transaction signing. This is a “non-custodial” service. +1. A mixture of the two via multisig. This is especially useful for maintaining non-custodial status while still allowing for account recovery. + +Be very careful taking a custodial approach or using any mixture where you are storing user secrets. It’s easy to get wrong, and the consequences can be devastating. For our purposes, we will assume choice #2. This tutorial will show you how to build a non-custodial service. + +The non-custodial option poses some very real usability issues: your user has to securely store their own account credentials and safely navigate transaction signing on their end. There are ways to make that process more user-friendly, one of which is to create a keystore file guarded by a passphrase, and that's the approach we'll take in this tutorial. + +Creating a keystore file and storing it locally in the browser allows a user to import or export it into other systems, and works pretty well as long as the user's passphrase is secure enough. We don't recommend this approach for high-security production applications, but it's the best way to get started, and for this tutorial, which lays out the foundation for a basic Stellar wallet, it’ll work just fine. + +## Third-party Key Management Applications + +There are also several apps and services that specialize in adding additional security layers to users' accounts. Check them out if you're interested: + +- [Albedo][1] +- [StellarAuth][2] +- [Stellar Authenticator][3] +- [Ledger][4] +- [Trezor][5] +- [StellarGuard][6] +- [LobstrVault][7] + +[1]: https://albedo.link/ +[2]: https://stellarauth.com/ +[3]: https://stellar-authenticator.org/ +[4]: https://www.ledger.com/ +[5]: https://trezor.io/ +[6]: https://stellarguard.me/ +[7]: https://vault.lobstr.co/ diff --git a/docs/building-apps/metadata.json b/docs/building-apps/metadata.json new file mode 100644 index 000000000..81d0c1c37 --- /dev/null +++ b/docs/building-apps/metadata.json @@ -0,0 +1,4 @@ +{ + "order": 50, + "title": "Build Apps" +} diff --git a/docs/building-apps/project-setup.mdx b/docs/building-apps/project-setup.mdx new file mode 100644 index 000000000..64cc85522 --- /dev/null +++ b/docs/building-apps/project-setup.mdx @@ -0,0 +1,492 @@ +--- +title: Project Setup +order: 20 +--- + +import { CodeExample } from "@site/src/components/CodeExample"; + +Throughout this tutorial we'll be making use of a little toolchain called StencilJs. It takes the best of modern frontend frameworks and pares everything back to small, blazing fast, 100% standards-based Web Components that run in every browser. Don’t worry if you've never heard of it: it's just TS (JS), SCSS (CSS) and JSX (HTML). You should be able to follow along just fine if you've ever built something with modern web dev tools. + +We chose Stencil so you can learn by doing: it’s an easy way to create a web-based application, which means you can see the ins and outs of building a Stellar wallet start to finish. Stellar also has a [suite of SDKs][1] in various programming languages, so if Javascript isn’t your thing, you can follow along, and recreate the steps below using one of them. + +To start the setup, open your terminal and initialize a new project. + + + +```bash +npm init stencil +``` + + + +After running `init` you will be provided with a prompt to choose the type of project to start. While Stencil can be used to create entire apps, we’ll choose the component as we’ll just be dealing with modular components rather than building an entire application + + + +```bash +? Pick a starter › - Use arrow-keys. Return to submit. + + ionic-pwa Everything you need to build fast, production ready PWAs + app Minimal starter for building a Stencil app or website +❯ component Collection of web components that can be used anywhere +``` + + + +We’ll walk through the prompt, `cd` into the project and run `npm i ; npm start` + + + +```bash +✔ Pick a starter › component +✔ Project name › stellar-wallet + +✔ All setup in 9 ms + + $ npm start + Starts the development server. + + $ npm run build + Builds your components/app in production mode. + + $ npm test + Starts the test runner. + + We suggest that you begin by typing: + + $ cd stellar-wallet + $ npm i + $ npm start + + Further reading: + + - https://github.com/ionic-team/stencil-component-starter + + Happy coding! +``` + + + +Now that our project is initialized, let’s take a look at the directory structure and familiarize ourselves with where things are and what roles they play. + +We’re mostly interested in the `src/` directory. The `dist/` and `www/` directories are outputs for compiled code. In the `src/components/` directory you’ll see a `my-component/` folder. We’re about to generate our own component, so go ahead and delete that folder. You can also nuke the `utils/` directory as we won’t be covering tests in this tutorial. Now run: + + + +```bash +$ npm run generate +``` + + + +This will initialize a component generation script. Enter a name of `stellar-wallet`. Disable Spec Test and E2E Test as we’re not interested in those Stencil features today. Press Return and your component will generate and wire up. + + + +```bash +% npm run generate + +> stellar-wallet generate +> stencil generate + +✔ Component tag name (dash-case): … stellar-wallet +✔ Which additional files do you want to generate? › Stylesheet + +$ stencil generate stellar-wallet + +The following files have been generated: + - src/components/wallet/wallet.tsx + - src/components/wallet/wallet.css +``` + + + +Amazing! Just a few more setup bits and we can get coding. I don’t know about you but I prefer to style in SCSS rather than CSS so let’s get some modern CSS dev tools setup. + + + +```bash +npm i -D @stencil/postcss@2 @stencil/sass@1 autoprefixer@9 @types/autoprefixer@9 rollup-plugin-node-polyfills +``` + + + +Once those packages have successfully installed, hop over to the `stencil.config.ts` file at the root of the project and modify it to this: + + + +```ts +import { Config } from "@stencil/core"; +import { sass } from "@stencil/sass"; +import { postcss } from "@stencil/postcss"; +import autoprefixer from "autoprefixer"; +import nodePolyfills from "rollup-plugin-node-polyfills"; + +export const config: Config = { + namespace: "stellar-wallet", + outputTargets: [ + { + type: "dist", + esmLoaderPath: "../loader", + }, + { + type: "docs-readme", + }, + { + type: "www", + serviceWorker: null, // disable service workers + }, + ], + globalStyle: "src/global/style.scss", + commonjs: { + namedExports: { + "stellar-sdk": [ + "StrKey", + "xdr", + "Transaction", + "Keypair", + "Networks", + "Account", + "TransactionBuilder", + "BASE_FEE", + "Operation", + "Asset", + "Memo", + "MemoHash", + ], + "@stellar/wallet-sdk": ["KeyManager", "KeyManagerPlugins", "KeyType"], + }, + }, + plugins: [ + nodePolyfills(), + sass(), + postcss({ + plugins: [autoprefixer()], + }), + ], + nodeResolve: { + browser: true, + preferBuiltins: true, + }, +}; +``` + + + +With that file saved, pop over to the `src/components/wallet/` and rename the `wallet.css` to `wallet.scss`. While we’re here let’s go ahead and modify this new style file with some basic styling to put our project in a pretty place. + + + +```scss +@import "../../global/style.scss"; + +:host { + display: block; + font-family: $font-family; + font-size: 15px; + + p { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: block; + } + button { + margin-bottom: 10px; + } + + .account-key { + display: flex; + align-items: center; + width: 100%; + margin-bottom: 10px; + + .small { + margin: 0 0 0 10px; + flex-shrink: 0; + } + } + .account-state, + .error { + overflow: scroll; + padding: 10px; + font-size: 12px; + line-height: 1.2; + font-family: $font-mono; + margin-bottom: 10px; + width: 100%; + } + .account-state { + background-color: whitesmoke; + } + .error { + background-color: orangered; + color: white; + } + + stellar-loader { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } +} +``` + + + +Before we update our `wallet.tsx` with this new stylesheet, note that we’re also importing a global stylesheet. Go ahead and create that file at the root of the `src/` directory. So `src/global/style.scss`. + + + +```scss +html, +body, +div, +span, +applet, +object, +iframe, +h1, +h2, +h3, +h4, +h5, +h6, +p, +blockquote, +pre, +a, +abbr, +acronym, +address, +big, +cite, +code, +del, +dfn, +em, +img, +ins, +kbd, +q, +s, +samp, +small, +strike, +strong, +sub, +sup, +tt, +var, +b, +u, +i, +center, +dl, +dt, +dd, +ol, +ul, +li, +fieldset, +form, +label, +legend, +table, +caption, +tbody, +tfoot, +thead, +tr, +th, +td, +article, +aside, +canvas, +details, +embed, +figure, +figcaption, +footer, +header, +hgroup, +menu, +nav, +output, +ruby, +section, +summary, +time, +mark, +audio, +video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} +/* HTML5 display-role reset for older browsers */ +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +menu, +nav, +section { + display: block; +} +body { + line-height: 1; +} +ol, +ul { + list-style: none; +} +blockquote, +q { + quotes: none; +} +blockquote:before, +blockquote:after, +q:before, +q:after { + content: ""; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +} + +* { + box-sizing: border-box; +} +input, +button, +select, +textarea { + font-family: $font-family; + font-size: 15px; + outline: none; + appearance: none; + border-radius: 0; +} +input, +select, +button { + height: 30px; +} +button { + border: none; + appearance: none; + position: relative; + background-color: blue; + color: white; + margin: 0; + display: flex; + align-items: center; + align-content: center; + justify-content: center; + justify-items: center; + padding: 0 10px; + cursor: pointer; + + &.loading { + color: transparent; + pointer-events: none; + } + &.small { + font-size: 12px; + height: 20px; + } +} +``` + + + +Save those style files and update the `wallet.tsx` to point to our new styles like so: + + + +```tsx +import { Component, h } from "@stencil/core"; +import * as StellarSdk from "stellar-sdk"; + +@Component({ + tag: "stellar-wallet", + styleUrl: "wallet.scss", + shadow: true, +}) +export class Wallet { + render() { + return [ +

+ {!!StellarSdk + ? "The StellarSdk is ready to rock 🤘" + : "Uh oh, the StellarSdk is missing 😱"} +

, + ]; + } +} +``` + +
+ +You’ll notice we also include a few extra setup lines to get the StellarSdk loaded in and ready to use. Let’s ensure all those dependencies are loaded and ready to rock. + + + +```bash +npm i -D stellar-sdk js-xdr +``` + + + +The last mod: point the `src/index.html` file to use this brand new component. Modify that file to match this. + + + +```html + + + + + + Stellar Wallet + + + + + + + + +``` + + + +You should be all set up now. Restart the server and let’s get coding. + + + +```bash +# Stop any current server and rerun +$ npm start +``` + + + +[View the full setup code on GitHub][2] + +[1]: ../software-and-sdks/index.mdx +[2]: https://github.com/stellar/docs-wallet/tree/setup diff --git a/docs/building-apps/setup-custodial-account.mdx b/docs/building-apps/setup-custodial-account.mdx new file mode 100644 index 000000000..62e46d557 --- /dev/null +++ b/docs/building-apps/setup-custodial-account.mdx @@ -0,0 +1,204 @@ +--- +title: Set Up a Custodial Account +order: 45 +--- + +import { CodeExample } from "@site/src/components/CodeExample"; +import { Alert } from "@site/src/components/Alert"; + +This guide describes how to add assets from the Stellar network to your custodial service. First, we walk through adding Stellar's native asset: lumens (XLM). Following that, we describe how to add other assets. This example uses Node.js and the [JS Stellar SDK](https://github.com/stellar/js-stellar-sdk), but it should be easy to adapt to other languages. + +## Account Setup + +### Pooled account + +Most custodial services, including cryptocurrency exchanges, choose to use a single pooled Stellar account to handle transactions on behalf of their users instead of creating a new Stellar account for each customer. Generally, they keep track of their customers in a separate, internal database and use the memo field of a Stellar transaction to map an incoming payment to the corresponding internal customer. + +The benefits of using a pooled account are _lower costs_ – no base reserves are needed for each account – and _lower key complexity_ – you only need to manage one account keypair. However, with a single pooled account, it is now your responsibility to manage all individual customer balances and payments. You can no longer rely on the Stellar ledger to accumulate value, handle errors and atomicity, or manage transactions on an account-by-account basis. + +## Code Framework + +You can use this code framework to integrate Stellar into your custodial service. For this guide, we use abstract placeholder functions for reading/writing to your internal customer database, since each organization will architect their infrastructure differently. Here are the functions we'll assume exist (along with their expected behavior): + + + +```js +// We assume that these to correspond to your custodial account details. +CUSTODIAL_KEY = StellarSdk.Keypair.fromSecret("..."); +custodialAcc = await server.loadAccount(CUSTODIAL_KEY.publicKey()); + +/** + * Credits a customer (`to`) with `amount` of a particular `asset`. + * + * @param {string} from The public key of the Stellar account that + * submitted this deposit. + * @param {any} to A string representing the customer that's part of + * this pooled account. This could be a key, a database index, an object, + * or some other abstraction. The main point is that this is *NOT* a + * Stellar account. This value either directly comes from or somehow + * resolves from the transaction memo field on the payment. + * @param {Asset} asset The asset deposited into the customer's account. + * @param {string} amount The quantity (as a string, conforming to the way + * payments are submitted on the Stellar network) of `asset` being + * credited. + **/ +function depositIntoPool(from, to, asset, amount) {} + +/** + * Withdraws an `amount` of `asset` from a customer (`from`). + * + * For simplicity, we assume that this function always works. However, you + * should obviously be managing balances appropriately and ensuring that + * a particular customer can in fact fulfill a requested withdrawal. + * + * Note that this should probably be executed *after* the requisite transaction + * (see `createPayment()`) succeeds to ensure accounts aren't debited + * unnecessarily. + * + * @param {any} from As in `depositIntoPool()`, this is some + * representation of a customer that's part of a pooled account but not + * on the Stellar network. + * @param {Asset} asset The asset being debited from the customer. + * @param {string} amount The quantity of `asset` to withdraw. + **/ +function withdrawFromPool(from, asset, amount) {} + +/** + * Creates a PaymentOp corresponding to a withdrawal. + * + * This is just a convenience method for submitting payments on behalf of + * customers in a transaction. You may want to just do one operation per + * transaction (for example to include the source customer's ID in the memo), + * or you can batch many operations together; it's up to you. + * + * @param {string} to The Stellar account address of the recipient + * @param {Asset} asset The asset to debit from the source customer + * @param {string} amount The amount of `asset` to debit + * + * @return {xdr.paymentOp} + */ +function createPayment(to, asset, amount) { + return sdk.Operation.payment({ + source: custodialAcc.accountId(), + destination: to, + asset: asset, + amount: amount, + }); +} +``` + + + +## Integration points for listing XLM + + + +In the following code samples, proper error checking is omitted for brevity. However, you should _always_ validate your results, as there are many ways that requests can fail. You should refer to the guide on [Handling Errors Gracefully](../tutorials/handling-errors.mdx) for tips on error management strategies. + + + +### Setting the memo required flag on your account + +Before we get into the main integration points for adding XLM support to your product or service, make sure to configure your pooled account to require memos. This step is necessary to prevent users from forgetting to attach a memo, which can increase customer support requests and lead to a less desirable user experience for new customers. [This article](https://www.stellar.org/developers-blog/fixing-memo-less-payments) explains how to set that up. + +### Listening for incoming payments from the Stellar network + +First, you need to listen for payments to the pooled account and credit any user that receives XLM there. For every payment received by the pooled account: + +- check the memo field to determine which user should receive the payment, and +- credit the user’s account with the amount of XLM they received. + +This is the role of `depositIntoPool`, described above. You pass this function as the `onmessage` option when you [stream payments](https://developers.stellar.org/docs/tutorials/follow-received-payments/): + + + +```js +const stream = server + .payments() + .forAccount(custodialAcc) + .join("transactions") + .cursor("now") + .stream({ + onmessage: (payment) => { + const from = payment.source_account; + const customerId = payment.transaction.memo; + const amount = payment.amount; + + let asset = sdk.Asset.native(); + if (payment.asset_issuer) { + asset = new sdk.Asset(payment.asset_code, payment.asset_issuer); + } + + depositIntoPool(from, customerId, asset, amount); + }, + }); +``` + + + +When someone **outside** of your customer base wants to send XLM to one of your customers, instruct them to make an XLM payment to your pooled account address with the customer ID in the memo field of the transaction. Assuming you have set the `memo_required` configuration on your account (see [above](#setting-the-memo-required-flag-on-your-account)), [well-behaved](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0029.md) wallets should enforce it and prevent users from forgetting to attach memos to incoming payments. To be on the safe side, however, you should make it incredibly clear to senders that their payment will end up in limbo if they fail to attach a valid one. You can learn more about the transaction memo field [here](https://developers.stellar.org/docs/glossary/transactions/#memo). + +When someone **inside** of your customer base wants to send XLM to _another_ one of your customers, you have two choices: you can send a memo'd payment exactly as above — this lets you maintain an audit trail, ensures the balance exists in the custodial account, etc. — or you can do the exchange "off-chain", i.e. by exclusively adjusting balances within your database. + +### Submitting outgoing payments to the Stellar network + +When one of your customers wants to make an outgoing XLM payment, you must generate a Stellar transaction to send XLM. See [building transactions](https://stellar.github.io/js-stellar-sdk/TransactionBuilder.html) or the [payments tutorial](https://developers.stellar.org/docs/tutorials/send-and-receive-payments/#send-a-payment) for more information. + +The aforementioned `createPayment` function will prepare the corresponding operation whenever a withdrawal is requested (while `withdrawFromPool` should manage your balance sheet). You can use these to queue up transactions (for periodic or batched submission) or submit them immediately. It's up to you how to architect this portion; we adopt the (simpler) latter approach here: + + + +```js +/** + * Submit a payment requested from an internal customer. + * + * Here, `customerId` is your internal representation of a customer in the + * pool, passed in here as the outgoing transaction's memo purely for + * clarity. The required `destination` parameter, however, is the actual + * Stellar address for the payment. + */ +function onPooledPayment(customerId, destination, amount, asset) { + let tx = new sdk.TransactionBuilder(custodialAcc, { + fee: 100, + networkPassphrase: sdk.Networks.TESTNET, + memo: customerId, + }) + .addOperation(createPayment(destination, amount, asset)) + .setTimeout(30) + .build(); + + tx.sign(CUSTODIAL_KEY); + withdrawFromPool(customerId, asset, amount); + return server.submitTransaction(tx).catch((err) => { + return reverseWithdrawal(customerId, asset, amount, error); + }); +} +``` + + + +This code would be called whenever one of your customers submitted a payment through your platform. Note that the balances are adjusted _before_ the transaction is confirmed, so you should take care to adjust them back if there's a failure (e.g. implement `reverseWithdrawal` for your architecture). + +## Listing Other Stellar Assets + +All of the code above is asset-agnostic, so accepting other (non-native/lumen) assets into your custodial account can be achieved after fulfilling a few prerequisites. + +### Account Setup + +First, you must [open a trustline](https://developers.stellar.org/docs/start/list-of-operations/#change-trust) with the issuing account of the asset you’d like to list – without this, you cannot begin accepting the asset. The [Issue an Asset](https://developers.stellar.org/docs/issuing-assets/how-to-issue-an-asset/) tutorial covers the code for this one-time process. + +Next, if the asset issuer has the `authorization_required` flag on their account set, you will need to wait for them to authorize the trustline before you can begin accepting the asset. Read more about trustline authorization [here](https://developers.stellar.org/docs/issuing-assets/control-asset-access/). + + + +_Note_: Users cannot send a non-native asset to your pooled account (nor receive one from your customers) unless they have opted into that asset by opening a trustline to its issuer. + + + +### Managing Supply + +Custodials usually hold a float of the assets they list on their platform. Unlike Stellar’s native asset (XLM), which can only be sourced from the Stellar Decentralized Exchange (SDEX), non-native assets can also be sourced directly from the asset’s issuer. For example, custodians can establish a [Circle Account](https://www.circle.com/blog/circle-account-and-api-services-now-support-stellar-usdc) to source Stellar USDC. + +Regardless of the strategies used to purchase the asset, you’ll need to adapt your balance sheet management (e.g. the internals of `depositIntoPool` and `withdrawFromPool`, [above](#code-framework)) to track things per-asset for each customer. Since this is backend-specific, we won’t provide example code here. + +For more information about non-native assets, check out the [asset issuing guide](../issuing-assets/). diff --git a/docs/building-apps/xlm-payments.mdx b/docs/building-apps/xlm-payments.mdx new file mode 100644 index 000000000..3163d72b0 --- /dev/null +++ b/docs/building-apps/xlm-payments.mdx @@ -0,0 +1,736 @@ +--- +title: Make XLM Payments +order: 50 +--- + +import { CodeExample } from "@site/src/components/CodeExample"; +import { Alert } from "@site/src/components/Alert"; + + + +In this tutorial we’re going to modify our [base wallet app](./basic-wallet.mdx) to include functionality to send XLM to other Stellar accounts. + + + +[View keystore boilerplate code on GitHub][1] + +In the [Build a Basic Wallet section](./basic-wallet.mdx), we did the hard work of wiring up a secure client and rock-solid key creation and storage structure with a clear plan for key use and management. Now that we have a method for creating an account — and for storing that account's secrets so they're safe and easy to use — we're ready to start making payments. We'll start with XLM payments since they're a little simpler; in the [next sectiion](./custom-assets.mdx), we'll look at how to support payments for other assets. + +There isn’t too much that's new or complicated here: we'll be building on what we already have. Again, most of the work will be in `src/components/wallet/`; however, before we dive into that file let’s clean up our project just a touch and add some helpful polish. Namely a loader component. + +## Add Loader Component + +Hopefully by now you’re familiar with how to generate new Stencil components. + + + +```bash +npm run generate +``` + + + +We'll call the component `stellar-loader`, and deselect both test files leaving only the styling. Once you've done that, open `src/components/loader/` and rename the `.css` file to `.scss`. Then replace the `loader.tsx` contents with this: + + + +```tsx +import { BaseN } from "js-combinatorics"; +import { Component, h, State, Prop } from "@stencil/core"; +import { isEqual as loIsEqual, sample as loSample } from "lodash-es"; + +@Component({ + tag: "stellar-loader", + styleUrl: "loader.scss", + shadow: true, +}) +export class Loader { + @State() chances: any = []; + @State() chance: any = null; + @Prop() interval: any; + + componentWillLoad() { + return new Promise((resolve) => { + if (!this.chances.length) this.generateChances(9); + + if (!this.interval) + this.interval = setInterval(() => this.getChance(), 100); + + resolve(); + }); + } + + generateChances(int: number) { + const baseN = new BaseN([0, 1], int); + + this.chances = baseN.toArray(); + this.getChance(); + } + getChance() { + const chance = loSample(this.chances); + + if (loIsEqual(chance, this.chance)) this.getChance(); + else this.chance = chance; + } + + render() { + return ( +
+ {this.chance.map((int, i) => ( +
+ ))} +
+ ); + } +} +``` + +
+ +Don’t forget to install new packages! + + + +```bash +npm i -D js-combinatorics +``` + + + +It’s a fancy wackadoodle little component which at the end of the day just produces a little loader for use in our action buttons. + +## Import Methods + +We’re now ready to tackle the `src/components/wallet/wallet.ts` file. + + + +```ts +import { Component, State, Prop } from "@stencil/core"; +import { Server, ServerApi } from "stellar-sdk"; + +import componentWillLoad from "./events/componentWillLoad"; +import render from "./events/render"; + +import createAccount from "./methods/createAccount"; +import updateAccount from "./methods/updateAccount"; +import makePayment from "./methods/makePayment"; +import copyAddress from "./methods/copyAddress"; +import copySecret from "./methods/copySecret"; +import signOut from "./methods/signOut"; +import setPrompt from "./methods/setPrompt"; + +import { Prompter } from "@prompt/prompt"; + +interface StellarAccount { + publicKey: string; + keystore: string; + state?: ServerApi.AccountRecord; +} + +interface Loading { + fund?: boolean; + pay?: boolean; + update?: boolean; +} + +@Component({ + tag: "stellar-wallet", + styleUrl: "wallet.scss", + shadow: true, +}) +export class Wallet { + @State() account: StellarAccount; + @State() prompter: Prompter = { show: false }; + @State() loading: Loading = {}; + @State() error: any = null; + + @Prop() server: Server; + + // Component events + componentWillLoad() {} + render() {} + + // Stellar methods + createAccount = createAccount; + updateAccount = updateAccount; + makePayment = makePayment; + copyAddress = copyAddress; + copySecret = copySecret; + signOut = signOut; + + // Misc methods + setPrompt = setPrompt; +} + +Wallet.prototype.componentWillLoad = componentWillLoad; +Wallet.prototype.render = render; +``` + + + +If you’ve followed other tutorials in this series much of this may be review, but we’ll just walk along this file and see exactly what’s going on. + + + +```ts +import { Component, State, Prop } from "@stencil/core"; +import { Server, ServerApi } from "stellar-sdk"; + +import componentWillLoad from "./events/componentWillLoad"; // UPDATE +import render from "./events/render"; // UPDATE + +import createAccount from "./methods/createAccount"; // UPDATE +import updateAccount from "./methods/updateAccount"; // NEW +import makePayment from "./methods/makePayment"; // NEW +import copyAddress from "./methods/copyAddress"; +import copySecret from "./methods/copySecret"; +import signOut from "./methods/signOut"; +import setPrompt from "./methods/setPrompt"; + +import { Prompter } from "@prompt/prompt"; +``` + + + +Imports galore! Nothing really noteworthy here other than you’ll notice we’re importing `./methods/updateAccount` and `./methods/makePayment` methods which we’ll be creating soon. There are a number of updates in the other methods from previous tutorials, and we’ll walk through all of those in a moment as well. + + + +```ts +interface StellarAccount { + // UPDATE + publicKey: string; + keystore: string; + state?: ServerApi.AccountRecord; +} + +interface Loading { + // NEW + fund?: boolean; + pay?: boolean; + update?: boolean; +} +``` + + + +Here we’re setting up two TypeScript classes: one for our account and the other for our loading states. + + + +```ts +@Component({ + tag: 'stellar-wallet', + styleUrl: 'wallet.scss', + shadow: true +}) +export class Wallet { + @State() account: StellarAccount + @State() prompter: Prompter = {show: false} + @State() loading: Loading = {} // NEW + @State() error: any = null + + @Prop() server: Server // NEW + + ... +} +``` + + + +Here we set up our `@State`’s, those dynamic properties with values that will change and alter the DOM of the component, and our `@Prop`, which in this case will hold a static reference to our Stellar server instance. + +## Update Component Events + +Next let’s update our two component events `./events/componentWillLoad.ts` and `./events/render.tsx`: + + + +```ts +import { Server } from "stellar-sdk"; +import { handleError } from "@services/error"; +import { get } from "@services/storage"; + +export default async function componentWillLoad() { + try { + let keystore = await get("keyStore"); + + this.error = null; + this.server = new Server("https://horizon-testnet.stellar.org"); + + if (keystore) { + keystore = atob(keystore); + + const { publicKey } = JSON.parse(atob(JSON.parse(keystore).adata)); + + this.account = { + publicKey, + keystore, + }; + + this.updateAccount(); + } + } catch (err) { + this.error = handleError(err); + } +} +``` + + + +In Stencil’s `componentWillLoad` method, we set up default values for the States and Props we initialized earlier. Most notably, we’re setting our server and account. You’ll notice we’re using the public `horizon-testnet` for now — we're just learning, and not ready to send live XLM — but in production you’d want to change this to the public `horizon` endpoint or, if you're running your own Horizon, to one of your own Horizon API endpoints. + +For the account we’re simply checking to see if a `keyStore` value has been stored, and if so we’re grabbing the public key and keystore from it and adding those to the account `@State`. The `state` value, which is optional, is not set here as we’ll need to run the method `updateAccount()` to find if the account exists, and if so what the state of that account looks like. We’ll get to that method shortly. For now let’s update our `render.tsx` method: + + + +```tsx +import { h } from "@stencil/core"; +import { has as loHas } from "lodash-es"; + +export default function render() { + return [ + , + + this.account ? ( + [ + , + + , + ] + ) : ( + + ), + + this.error ? ( +
{JSON.stringify(this.error, null, 2)}
+ ) : null, + + loHas(this.account, "state") ? ( + + ) : null, + + this.account + ? [ + , + , + ] + : null, + ]; +} +``` + +
+ +Yikers amirite!? Don’t worry though: it’s actually quite simple if you’ve spent any time with HTML in JS before. What we're looking at are a few ternary operators toggling the UI between different states based on the account status. Basically just a bunch of buttons wired up to their subsequent actions. Turns out creating those buttons is next on our to-do list! + +## Create Buttons + +`updateAccount` and `makePayment` are new; `createAccount` will just need a few tweaks. Let’s start with that one. + + + +```ts +import sjcl from "@tinyanvil/sjcl"; +import axios from "axios"; +import { Keypair } from "stellar-sdk"; + +import { handleError } from "@services/error"; +import { set } from "@services/storage"; + +export default async function createAccount(e: Event) { + try { + e.preventDefault(); + + const pincode_1 = await this.setPrompt("Enter a keystore pincode"); + const pincode_2 = await this.setPrompt("Enter keystore pincode again"); + + if (!pincode_1 || !pincode_2 || pincode_1 !== pincode_2) + throw "Invalid pincode"; + + this.error = null; + this.loading = { ...this.loading, fund: true }; + + const keypair = Keypair.random(); + + await axios( + `https://friendbot.stellar.org?addr=${keypair.publicKey()}`, + ).finally(() => (this.loading = { ...this.loading, fund: false })); + + this.account = { + publicKey: keypair.publicKey(), + keystore: sjcl.encrypt(pincode_1, keypair.secret(), { + adata: JSON.stringify({ + publicKey: keypair.publicKey(), + }), + }), + }; + + await set("keyStore", btoa(this.account.keystore)); + + this.updateAccount(); + } catch (err) { + this.error = handleError(err); + } +} +``` + + + +## Fund Account Using Friendbot + +The only new thing we’re adding here — other than a loading state and an initial `this.updateAccount()` call at the end — is the call to [friendbot.stellar.org][2], which is a testnet tool that we can use to automatically fund our new testnet account with 10,000 XLM. Nice little shortcut to kickstart our development. + + + +```ts +await axios( + `https://friendbot.stellar.org?addr=${keypair.publicKey()}`, +).finally(() => (this.loading = { ...this.loading, fund: false })); +``` + + + +In production, you would have to find an actual source for funding the account. Some wallets fund users' accounts for them; some require the user to supply the funds. + +So that’s the updated `./methods/createAccount.ts` file. + +## Update Account Method + +Now let’s create two new files for updating the account and making XLM payments. + + + +```bash +touch src/components/wallet/methods/{updateAccount,makePayment}.ts +``` + + + +Let’s start with the simpler one, `./methods/updateAccount.ts` + + + +```ts +import { omit as loOmit, map as loMap } from "lodash-es"; + +import { handleError } from "@services/error"; + +export default async function updateAccount(e?: Event) { + try { + if (e) e.preventDefault(); + + this.error = null; + this.loading = { ...this.loading, update: true }; + + await this.server + .accounts() + .accountId(this.account.publicKey) + .call() + .then((account) => { + account.balances = loMap(account.balances, (balance) => + loOmit(balance, [ + "limit", + "buying_liabilities", + "selling_liabilities", + "is_authorized", + "last_modified_ledger", + balance.asset_type !== "native" ? "asset_type" : null, + ]), + ); + + this.account = { + ...this.account, + state: loOmit(account, [ + "id", + "_links", + "sequence", + "subentry_count", + "last_modified_ledger", + "flags", + "thresholds", + "account_id", + "signers", + "paging_token", + "data_attr", + ]), + }; + }) + .finally(() => (this.loading = { ...this.loading, update: false })); + } catch (err) { + this.error = handleError(err); + } +} +``` + + + +All we’re doing here is looking up the state of the Stellar account on the ledger and saving it to the `this.account.state`. You’ll also notice we’re omitting several fields from the account and balances for easier readability. You may choose to save these and selectively display the values you care about, but in our example we’re just displaying the raw JSON, so cleaning things up a little is the right move. + +`this.account = {...this.account, state: loOmit(account, ['id', ...])}` may feel odd, but it’s just the Stencil way of updating a state’s object key to trigger a re-render of the DOM. You’ll notice `this.loading` follows the same pattern. We’ll make use of this data further down in the `render` method, but for now just know this is how we would grab ahold of the account to get the latest state. + +## Make Payment Method + +Next let’s break down the main subject method for this tutorial, `./methods/makePayment.ts`: + + + +```ts +import sjcl from "@tinyanvil/sjcl"; +import { + Keypair, + Account, + TransactionBuilder, + BASE_FEE, + Networks, + Operation, + Asset, +} from "stellar-sdk"; +import { has as loHas } from "lodash-es"; + +import { handleError } from "@services/error"; + +export default async function makePayment(e: Event) { + try { + e.preventDefault(); + + let instructions = await this.setPrompt("{Amount} {Destination}"); + instructions = instructions.split(" "); + + const pincode = await this.setPrompt("Enter your keystore pincode"); + + if (!instructions || !pincode) return; + + const keypair = Keypair.fromSecret( + sjcl.decrypt(pincode, this.account.keystore), + ); + + this.error = null; + this.loading = { ...this.loading, pay: true }; + + await this.server + .accounts() + .accountId(keypair.publicKey()) + .call() + .then(({ sequence }) => { + const account = new Account(keypair.publicKey(), sequence); + const transaction = new TransactionBuilder(account, { + fee: BASE_FEE, + networkPassphrase: Networks.TESTNET, + }) + .addOperation( + Operation.payment({ + destination: instructions[1], + asset: Asset.native(), + amount: instructions[0], + }), + ) + .setTimeout(0) + .build(); + + transaction.sign(keypair); + return this.server.submitTransaction(transaction).catch((err) => { + if ( + // Paying an account which doesn't exist, create it instead + loHas(err, "response.data.extras.result_codes.operations") && + err.response.data.status === 400 && + err.response.data.extras.result_codes.operations.indexOf( + "op_no_destination", + ) !== -1 + ) { + const transaction = new TransactionBuilder(account, { + fee: BASE_FEE, + networkPassphrase: Networks.TESTNET, + }) + .addOperation( + Operation.createAccount({ + destination: instructions[1], + startingBalance: instructions[0], + }), + ) + .setTimeout(0) + .build(); + + transaction.sign(keypair); + return this.server.submitTransaction(transaction); + } else throw err; + }); + }) + .then((res) => console.log(res)) + .finally(() => { + this.loading = { ...this.loading, pay: false }; + this.updateAccount(); + }); + } catch (err) { + this.error = handleError(err); + } +} +``` + + + +This method is quite massive, so let’s break it down further so it's easier to understand exactly what is going on. + + + +```ts +export default async function makePayment(e: Event) { + try { + e.preventDefault() + + let instructions = await this.setPrompt('{Amount} {Destination}') + instructions = instructions.split(' ') + + const pincode = await this.setPrompt('Enter your keystore pincode') + + if ( + !instructions + || !pincode + ) return +``` + + + +We’re going to need a couple pieces of info from the user: the amount of XLM to send, what address to send it to, and the pincode for unlocking the keystore file. We request those asynchronously and cancel out of the method if they aren’t provided. + + + +```ts +const keypair = Keypair.fromSecret( + sjcl.decrypt(pincode, this.account.keystore), +); + +this.error = null; +this.loading = { ...this.loading, pay: true }; +``` + + + +Next we unpack the keystore with the pincode, reset any existing errors, and trigger the `pay` loading boolean: + + + +```ts + await this.server + .accounts() + .accountId(keypair.publicKey()) + .call() + .then(({sequence}) => { + const account = new Account(keypair.publicKey(), sequence) + const transaction = new TransactionBuilder(account, { + fee: BASE_FEE, + networkPassphrase: Networks.TESTNET + }) + .addOperation(Operation.payment({ + destination: instructions[1], + asset: Asset.native(), + amount: instructions[0] + })) + .setTimeout(0) + .build() + + transaction.sign(keypair) + return this.server.submitTransaction(transaction) +``` + + + +From there we call the keypair account to retrieve its current sequence number so we can prepare a transaction with a payment operation. We set the destination and amount using the instructions from the prompt we collected and split earlier. Finally, we build, sign, and submit that transaction to the Stellar Horizon API server. + + + +```ts + .catch((err) => { + if ( // Paying an account which doesn't exist, create it instead + loHas(err, 'response.data.extras.result_codes.operations') + && err.response.data.status === 400 + && err.response.data.extras.result_codes.operations.indexOf('op_no_destination') !== -1 + ) { + const transaction = new TransactionBuilder(account, { + fee: BASE_FEE, + networkPassphrase: Networks.TESTNET + }) + .addOperation(Operation.createAccount({ + destination: instructions[1], + startingBalance: instructions[0] + })) + .setTimeout(0) + .build() + + transaction.sign(keypair) + return this.server.submitTransaction(transaction) + } + + else throw err + }) +``` + + + +If the account we want to send XLM is unfunded (and therefore doesn't yet exist on the ledger), we'll get back `op_no_destination`. This `catch` handles that issue by trying again with a `createAccount` operation. For any other issues we just pass the error on unmodified. + + + +```ts + }) + .then((res) => console.log(res)) + .finally(() => { + this.loading = {...this.loading, pay: false} + this.updateAccount() + }) + } + + catch (err) { + this.error = handleError(err) + } +} +``` + + + +Finally, we log any success transaction, kill the loader, and `updateAccount` to reflect the new balance in our account after successfully sending XLM. We also have our `catch` block that passes to the `handleError` service. We will render that in a nice error block in the UI. + +There we go! That wasn’t so bad right? Pretty simple, and yet from this tutorial we have the power to hold and observe balances, and to make payments using the power of the Stellar ledger. Amazing! + +[1]: https://github.com/stellar/docs-wallet/tree/keystore +[2]: https://friendbot.stellar.org/ +[3]: https://github.com/stellar/docs-wallet/tree/pay diff --git a/docs/glossary/accounts.mdx b/docs/glossary/accounts.mdx new file mode 100644 index 000000000..fe763062c --- /dev/null +++ b/docs/glossary/accounts.mdx @@ -0,0 +1,90 @@ +--- +title: Accounts +order: +--- + +Accounts are the central data structure in Stellar. They hold balances, sign transactions, and issue assets. All entries that persist on the ledger are owned by a particular account. + +In addition to a valid keypair, an account needs a balance of XLM sufficient to meet the network reserve before it exists on the ledger. + +## Keypair + +Stellar relies on public key cryptography to ensure that transactions are secure: every account requires a valid keypair consisting of a _public key_ and a _private key_. The public key is, as the name suggests, public. It’s visible on the ledger, anyone can look it up, and it’s what others use to send payments to the account, identify the issuer of an asset, and verify that a given transaction is authorized. + +The private key, however, is something an account holder should guard closely. It’s kind of like the combination to a lock — anyone who knows it can access the account, sign transactions, send funds, etc. Do not share your private key with anyone. + +You can use any Stellar wallet or SDK to generate a valid keypair. + +## Account Creation + +A keypair alone doesn’t create an account: before an account exists on the ledger, it needs an XLM balance sufficient to meet the minimum network reserve. The minimum reserve, which is determined by validator vote, is intended to disincentivize the creation of tons of unused accounts in order to prevent ledger spam and maintain the efficiency and scalability of the network. + +There is a specific operation, Create Account, which you use to make a payment to a valid public key that does not exist on the ledger, thereby creating the account. + +## Account fields + +Accounts have the following fields: + +### Account ID + +The public key that was used to create the account. Even if you replace the signer with a different key, the original account ID will always be used to identify the account. + +### Sequence number + +The current transaction sequence number of the account. This number starts equal to the ledger number at which the account was created, and increments upward as the account signs transactions. + +### Sequence time and ledger + +These two fields reflect the last time an account touched its sequence number. This occurs both when an account is the source account for a transaction and when it's the target of a [Bump Sequence](../start/list-of-operations.mdx#bump-sequence) operation. Both the ledger number and timestamp (i.e. the close time of the ledger) of the last touch is tracked by the network. Note that even if the [Bump Sequence](../start/list-of-operations.mdx#bump-sequence) operation has no effect, i.e. does not increase the sequence number, it still counts as a "touch". + +### Number of subentries + +Number of entries the account owns. This number is used to calculate the account's minimum balance: each subentry increases an account’s reserve by 0.5XLM. Subentries include: + +- Trustlines +- Offers +- Signers +- Data entries + +Since protocol version 11, an account cannot increase the number of subentries above 1000. + +### Number of sponsored subentries + +Number of subentries the account owns that are sponsored by another account. Sponsored subentries do not incur any reserve requirement on the account that owns them. For more info, see [sponsored reserves](./sponsored-reserves.mdx). + +### Number of entries sponsored by this account + +Number of entries this account is sponsoring. Entries sponsored by this account incur a reserve requirement. For more info, see [sponsored reserves](./sponsored-reserves.mdx). + +### Thresholds + +Operations have varying levels of access. This field specifies thresholds for low-, medium-, and high-access levels, as well as the weight of the master key. For more info, see [multi-sig](./multisig.mdx). + +### Home domain + +A [fully qualified domain name](https://en.wikipedia.org/wiki/Fully_qualified_domain_name) (of up to 32 characters) such as `example.com` linked to the account. A home domain is required of asset issuers, who use it to [publish meta-information](../issuing-assets/publishing-asset-info.mdx) for Stellar wallets and potential token holders, and for organizations running validators, who use it to [self-identify their nodes](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0020.md). + +To add a home domain to an account, use the [Set Options](../start/list-of-operations.mdx#set-options) operation. + +### Flags + +Asset issuers set flags at the account level (via [Set Options](../start/list-of-operations.mdx#set-options)) if they want to control access to the assets they issue. There are four flags: + +- **Authorization required (0x1)**: Requires the issuing account to grant an account permission to hold an asset. With this flag set, an issuer can either grant full authorization to transact with its asset _or_ it can grant limited authorization allowing the holder to maintain orders on the order books, but not to otherwise transact with the asset. +- **Authorization revocable (0x2)**: Allows the issuing account to reduce the authorization level of an account from full to partial, from full to none, or from partial to none. +- **Authorization immutable (0x4)**: Prevents the issuer from setting either of the above flags or deleting the issuing account. +- **Clawback enabled (0x8)**: Enables [clawbacks](./clawback.mdx) for all assets issued by this account. Note that this only applies along trustlines established _after_ this flag has been set. If you'd like to selectively enable clawback on certain trustlines, you can use [SetTrustLineFlags](../start/list-of-operations.mdx#set-trust-line-flags) to clear their clawback flag. + +### Balances + +Each account has a balance for each token or pool share the account holds, including XLM. + +### Liabilities + +Each account also tracks its liabilities. Buying liabilities equal the total amount of an asset offered to buy aggregated over all offers owned by this account, and selling liabilities equal the total amount of an asset offered to sell aggregated over all offers owned by this account. + +An account must always have a balance sufficiently above the minimum reserve to satisfy its lumen selling liabilities, and a balance sufficiently below the maximum to accommodate its lumen buying liabilities. + +### Signers + +You can add signers to an account, and this field lists other public keys and their weights that can be used to authorize transactions. For more info, see [multisig](./multisig.mdx). diff --git a/docs/glossary/assets.mdx b/docs/glossary/assets.mdx new file mode 100644 index 000000000..42212b1ce --- /dev/null +++ b/docs/glossary/assets.mdx @@ -0,0 +1,8 @@ +--- +title: Assets +order: +--- + +The Stellar distributed network can be used to track, hold, and transfer any type of **asset**: dollars, euros, bitcoin, stocks, gold, and other tokens of value. Any asset on the network can be traded and exchanged with any other using Stellar's built-in [decentralized exchange](./decentralized-exchange.mdx). + +For more information on assets, see the [Anatomy of an Asset](../issuing-assets/anatomy-of-an-asset.mdx) in the [Issue Assets](../issuing-assets) section of the docs. diff --git a/docs/glossary/channels.mdx b/docs/glossary/channels.mdx new file mode 100644 index 000000000..fbc22299c --- /dev/null +++ b/docs/glossary/channels.mdx @@ -0,0 +1,75 @@ +--- +title: Channels +order: +--- + +import { CodeExample } from "@site/src/components/CodeExample"; + +Payment channels provide a method for submitting transactions to the network at a high rate. + +If you are submitting [transactions](./transactions.mdx) to the network at a high rate or from different processes you must be careful that the transactions are submitted in the correct order of their sequence numbers. This can be problematic since typically you are submitting through Horizon and there is no guarantee that a given transaction is received by [Stellar Core](https://github.com/stellar/stellar-core) until ledger close. This means that they can reach stellar-core out of order and will bounce with a bad sequence error. If you do wait for ledger close to avoid this issue that will greatly reduce the rate you can submit transactions to the network. + +The way to avoid this is with the concept of **channels**. + +A channel is simply another Stellar account that is used not to send the funds but as the "source" account of the transaction. Remember transactions in Stellar each have a source account that can be different than the accounts being effected by the operations in the transaction. The source account of the transaction pays the fee and consumes a sequence number. You can then use one common account (your base account) to make the payment [operation](./operations.mdx) inside each transaction. The various channel accounts will consume their sequence numbers even though the funds are being sent from your base account. + +Channels take advantage of the fact that the "source" account of a transaction can be different than the source account of the operations inside the transaction. With this set up you can make as many channels as you need to maintain your desired transaction rate. + +You will, of course, have to sign the transaction with both the base account key and the channel account key. + +For example: + + + +```js +StellarSdk.Network.useTestNetwork(); +// channelAccounts[] is an array of accountIDs, one for each channel +// channelKeys[] is an array of secret keys, one for each channel +// channelIndex is the channel you want to send this transaction over + +// create payment from baseAccount to customerAddress +var transaction = new StellarSdk.TransactionBuilder( + channelAccounts[channelIndex], +) + .addOperation( + StellarSdk.Operation.payment({ + source: baseAccount.address(), + destination: customerAddress, + asset: StellarSdk.Asset.native(), + amount: amountToSend, + }), + ) + // Wait a maximum of three minutes for the transaction + .setTimeout(180) + .build(); + +transaction.sign(baseAccountKey); // base account must sign to approve the payment +transaction.sign(channelKeys[channelIndex]); // channel must sign to approve it being the source of the transaction +``` + +```python +# channelAccounts[] is an array of accountIDs, one for each channel +# channelKeys[] is an array of secret keys, one for each channel +# channelIndex is the channel you want to send this transaction over + +transaction = ( + TransactionBuilder( + source_account=channelAccounts[channelIndex], + network_passphrase=Network.TESTNET_NETWORK_PASSPHRASE, + base_fee=base_fee, + ) + .append_payment_op( + source=baseAccount.public_key, + destination=customerAddress, + asset=Asset.native(), + amount=amountToSend, + ) + .set_timeout(180) # Wait a maximum of three minutes for the transaction + .build() +) + +transaction.sign(baseAccountKey) # base account must sign to approve the payment +transaction.sign(channelKeys[channelIndex]) # channel must sign to approve it being the source of the transaction +``` + + diff --git a/docs/glossary/claimable-balance.mdx b/docs/glossary/claimable-balance.mdx new file mode 100644 index 000000000..ebb34d5ab --- /dev/null +++ b/docs/glossary/claimable-balance.mdx @@ -0,0 +1,417 @@ +--- +title: Claimable Balance +order: +--- + +import { Alert } from "@site/src/components/Alert"; +import { CodeExample } from "@site/src/components/CodeExample"; + +Claimable Balances can be used to "split up" a payment into two parts, which allows the sending to only depend on the sending account, and the receipt to only depend on the receiving account. An account can initiate the "send" by creating a ClaimableBalanceEntry with [Create Claimable Balance](../start/list-of-operations.mdx#create-claimable-balance), and then that entry can be claimed by the claimants specified on the ClaimableBalanceEntry at a later time with [Claim Claimable Balance](../start/list-of-operations.mdx#claim-claimable-balance). + +## Relevant operations + +### Create Claimable Balance + +#### Parameters + +1. **Asset** - Asset that will be held in the ClaimableBalanceEntry in the form `asset_code:issuing_address` or native (for XLM). + +1. **Amount** - Amount of **Asset** stored in the ClaimableBalanceEntry. + +1. **List of Claimants** - A Claimant is an object that holds both the destination account that can claim the ClaimableBalanceEntry, and a ClaimPredicate that must evaluate to true for the claim to succeed. A ClaimPredicate is a recursive data structure that can be used to construct complex conditionals using different ClaimPredicateTypes. Here are some examples (The types have had the `CLAIM_PREDICATE_` prefix removed for readability) - + - Can claim at anytime - `UNCONDITIONAL` + - Can claim if the close time of the ledger including the claim is _before_ X seconds + the ledger close time in which the ClaimableBalanceEntry was created - `BEFORE_RELATIVE_TIME(X)` + - Can claim if the close time of the ledger including the claim is _before_ X (Unix timestamp) - `BEFORE_ABSOLUTE_TIME(X)` + - Can claim if the close time of the ledger including the claim is _at or after_ X seconds + the ledger close time in which the ClaimableBalanceEntry was created - `NOT(BEFORE_RELATIVE_TIME(X))` + - Can claim if the close time of the ledger including the claim is _at or after_ X (Unix timestamp) - `NOT(BEFORE_ABSOLUTE_TIME(X))` + - Can claim _between_ X and Y Unix timestamps (given X < Y) - `AND(NOT(BEFORE_ABSOLUTE_TIME(X)), BEFORE_ABSOLUTE_TIME(Y))` + - Can claim _outside_ X and Y Unix timestamps (given X < Y) - `OR(BEFORE_ABSOLUTE_TIME(X), NOT(BEFORE_ABSOLUTE_TIME(Y))` + +Note that the SDKs expect the Unix timestamps to be expressed in **seconds**. + +#### Operation Information + +This operation will move Amount of Asset from the operation source account into a new ClaimableBalanceEntry. + +Note that the baseReserve requirement for a ClaimableBalanceEntry is dependant on the number of Claimants. The [minimum balance](./minimum-balance.mdx) of the account will increase by `# of Claimants * baseReserve`. + +#### BalanceID + +A successful Create Claimable Balance operation will return a balanceID, which is the required parameter when actually claiming the newly-created entry via the Claim Claimable Balance operation, below. See [ClaimableBalanceID](./miscellaneous-core-objects.mdx#ClaimableBalanceID) for more information. + +### Claim Claimable Balance + +#### Parameters + +1. BalanceID - The ID of the ClaimableBalanceEntry being claimed. + +#### Operation Information + +This operation will load the ClaimableBalanceEntry that corresponds to the BalanceID, and then search for the source account of this operation in the list of Claimants on the entry. If a match on the Claimant is found, and the ClaimPredicate evaluates to true, then the ClaimableBalanceEntry can be claimed. The balance on the entry will be moved to the source account if there are no limit or trustline issues (for non-native assets). + +Once a ClaimableBalanceEntry has been claimed, it will be deleted. + +## Example + + + +In the following code samples, proper error checking is omitted for brevity. However, you should _always_ validate your results, as there are many ways that requests can fail. You should refer to the guide on [Handling Errors Gracefully](../tutorials/handling-errors.mdx) for tips on error management strategies. + + + +The below code demonstrates via both the JavaScript and Go [SDKs](../software-and-sdks/index.mdx) how an account ("Account A") can create a `ClaimableBalanceEntry` with two Claimants: Account A (itself) and "Account B" (another recipient). + +Each of these accounts can only claim the balance under certain, individual conditions. Namely, Account B has a full minute to claim the balance, after which Account A can "reclaim" the balance back for itself. + +It's worth emphasizing that there is no "recovery" mechanism for a claimable balance in general: if none of the predicates can be fulfilled, the balance **cannot be recovered**. The "reclaim" paradigm below acts as a safety net for this situation. + + + +```js +const sdk = require("stellar-sdk"); + +async function main() { + let server = new sdk.Server("https://horizon-testnet.stellar.org"); + + let A = sdk.Keypair.fromSecret("SAQLZCQA6AYUXK6JSKVPJ2MZ5K5IIABJOEQIG4RVBHX4PG2KMRKWXCHJ"); + let B = sdk.Keypair.fromPublicKey("GAS4V4O2B7DW5T7IQRPEEVCRXMDZESKISR7DVIGKZQYYV3OSQ5SH5LVP"); + + // NOTE: Proper error checks are omitted for brevity; always validate things! + + let aAccount = await server.loadAccount(A.publicKey()).catch(function (err) { + console.error(`Failed to load ${A.publicKey()}: ${err}`) + }) + if (!aAccount) { return } + + // Create a claimable balance with our two above-described conditions. + let soon = Math.ceil((Date.now() / 1000) + 60); // .now() is in ms + let bCanClaim = sdk.Claimant.predicateBeforeRelativeTime("60"); + let aCanReclaim = sdk.Claimant.predicateNot( + sdk.Claimant.predicateBeforeAbsoluteTime(soon.toString()) + ); + + // Create the operation and submit it in a transaction. + let claimableBalanceEntry = sdk.Operation.createClaimableBalance({ + claimants: [ + new sdk.Claimant(B.publicKey(), bCanClaim), + new sdk.Claimant(A.publicKey(), aCanReclaim) + ], + asset: sdk.Asset.native(), + amount: "420", + }); + + let tx = new sdk.TransactionBuilder(aAccount, {fee: sdk.BASE_FEE}) + .addOperation(claimableBalanceEntry) + .setNetworkPassphrase(sdk.Networks.TESTNET) + .setTimeout(180) + .build(); + + tx.sign(A); + let txResponse = await server.submitTransaction(tx).then(function() { + console.log("Claimable balance created!"); + }).catch(function (err) { + console.error(`Tx submission failed: ${err}`) + }); +} +``` + +```go +package main + +import ( + "fmt" + "time" + + sdk "github.com/stellar/go/clients/horizonclient" + "github.com/stellar/go/keypair" + "github.com/stellar/go/network" + "github.com/stellar/go/txnbuild" +) + + +func main() { + client := sdk.DefaultTestNetClient + + // Suppose that these accounts exist and are funded accordingly: + A := "SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4" + B := "GA2C5RFPE6GCKMY3US5PAB6UZLKIGSPIUKSLRB6Q723BM2OARMDUYEJ5" + + // Load the corresponding account for A. + aKeys := keypair.MustParseFull(A) + aAccount, err := client.AccountDetail(sdk.AccountRequest{ + AccountID: aKeys.Address(), + }) + check(err) + + // Create a claimable balance with our two above-described conditions. + soon := time.Now().Add(time.Second * 60) + bCanClaim := txnbuild.BeforeRelativeTimePredicate(60) + aCanReclaim := txnbuild.NotPredicate( + txnbuild.BeforeAbsoluteTimePredicate(soon.Unix()), + ) + + claimants := []txnbuild.Claimant{ + txnbuild.NewClaimant(B, &bCanClaim), + txnbuild.NewClaimant(aKeys.Address(), &aCanReclaim), + } + + // Create the operation and submit it in a transaction. + claimableBalanceEntry := txnbuild.CreateClaimableBalance{ + Destinations: claimants, + Asset: txnbuild.NativeAsset{}, + Amount: "420", + } + + // Build, sign, and submit the transaction + tx, err := txnbuild.NewTransaction( + txnbuild.TransactionParams{ + SourceAccount: aAccount.AccountID, + IncrementSequenceNum: true, + BaseFee: txnbuild.MinBaseFee, + // Use a real timeout in production! + Timebounds: txnbuild.NewInfiniteTimeout(), + Operations: []txnbuild.Operation{&claimableBalanceEntry}, + }, + ) + check(err) + tx, err = tx.Sign(network.TestNetworkPassphrase, aKeys) + check(err) + txResp, err := client.SubmitTransaction(tx) + check(err) + + fmt.Println(txResp) + fmt.Println("Claimable balance created!") +} +``` + +```python +import time +from stellar_sdk.xdr import TransactionResult, OperationType +from stellar_sdk.exceptions import NotFoundError, BadResponseError, BadRequestError +from stellar_sdk import ( + Keypair, + Network, + Server, + TransactionBuilder, + Transaction, + Asset, + Operation, + Claimant, + ClaimPredicate, + CreateClaimableBalance, + ClaimClaimableBalance +) + +server = Server("https://horizon-testnet.stellar.org") + +A = Keypair.from_secret("SANRGB5VXZ52E7XDGH2BHVBFZR4S25AUQ4BR7SFXIQYT5J6W2OES2OP7") +B = Keypair.from_public_key("GAAPSRMYNFAO3TDQTLNLKN76IQ3E6IQAKU23PSQX3BIV7RTEBXHQIWU6") + +# NOTE: Proper error checks are omitted for brevity; always validate things! + +try: + aAccount = server.load_account(A.public_key) +except NotFoundError: + raise Exception(f"Failed to load {A.public_key}") + +# Create a claimable balance with our two above-described conditions. +soon = int(time.time() + 60) +bCanClaim = ClaimPredicate.predicate_before_relative_time(60) +aCanClaim = ClaimPredicate.predicate_not( + ClaimPredicate.predicate_before_absolute_time(soon) +) + +# Create the operation and submit it in a transaction. +claimableBalanceEntry = CreateClaimableBalance( + asset = Asset.native(), + amount = "420", + claimants = [ + Claimant(destination = B.public_key, predicate = bCanClaim), + Claimant(destination = A.public_key, predicate = aCanClaim) + ] +) + +tx = ( + TransactionBuilder ( + source_account = aAccount, + network_passphrase = Network.TESTNET_NETWORK_PASSPHRASE, + base_fee = server.fetch_base_fee() + ) + .append_operation(claimableBalanceEntry) + .set_timeout(180) + .build() +) + +tx.sign(A) +try: + txResponse = server.submit_transaction(tx) + print("Claimable balance created!") +except (BadRequestError, BadResponseError) as err: + print(f"Tx submission failed: {err}") +``` + + + +At this point, the `ClaimableBalanceEntry` exists in the ledger, but we'll need its Balance ID to claim it. This can be acquired in a number of ways: + +1. the submitter of the entry (Account A in this case) can retrieve the balance ID _prior_ to submitting the transaction; +1. the submitter parses the XDR of the transaction result's operations; **or** +1. someone queries the list of claimable balances (filtered accordingly, if necessary). + +Either party could also check the `/effects` of the transaction, query `/claimable_balances` with different filters, etc. Note that while (1) may be unavailable in some SDKs as its just a helper, the other methods are universal. + + + +```js +// Method 1: Not available in the JavaScript SDK yet. + +// Method 2: Suppose `txResponse` comes from the transaction submission +// above. +let txResult = sdk.xdr.TransactionResult.fromXDR( + txResponse.result_xdr, "base64"); +let results = txResult.result().results(); + +// We look at the first result since our first (and only) operation +// in the transaction was the CreateClaimableBalanceOp. +let operationResult = results[0].value().createClaimableBalanceResult(); +let balanceId = operationResult.balanceId().toXDR("hex"); +console.log("Balance ID (2):", balanceId); + +// Method 3: Account B could alternatively do something like: +let balances = await server + .claimableBalances() + .claimant(B.publicKey()) + .limit(1) // there may be many in general + .order("desc") // so always get the latest one + .call() + .catch(function(err) { + console.error(`Claimable balance retrieval failed: ${err}`) + }); +if (!balances) { return; } + +balanceId = balances.records[0].id; +console.log("Balance ID (3):", balanceId); +``` + +```go +// Method 1: Suppose `tx` comes from the transaction built above. +// Notice that this can be done *before* submission. +balanceId, err := tx.ClaimableBalanceID(0) +check(err) + +// Method 2: Suppose `txResp` comes from the transaction submission above. +var txResult xdr.TransactionResult +err = xdr.SafeUnmarshalBase64(txResp.ResultXdr, &txResult) +check(err) + +if results, ok := txResult.OperationResults(); ok { + // We look at the first result since our first (and only) operation in the + // transaction was the CreateClaimableBalanceOp. + operationResult := results[0].MustTr().CreateClaimableBalanceResult + balanceId, err := xdr.MarshalHex(operationResult.BalanceId) + check(err) + fmt.Println("Balance ID:", balanceId) +} + +// Method 3: Account B could alternatively do something like: +balances, err := client.ClaimableBalances(sdk.ClaimableBalanceRequest{Claimant: B}) +check(err) +balanceId := balances.Embedded.Records[0].BalanceID +``` + +```python +# Method 1: Not available in the Python SDK yet. + +# Method 2: Suppose `txResponse` comes from the transaction submission +# above. +txResult = TransactionResult.from_xdr(txResponse["result_xdr"]) +results = txResult.result.results + +# We look at the first result since our first (and only) operation +# in the transaction was the CreateClaimableBalanceOp. +operationResult = results[0].tr.create_claimable_balance_result +balanceId = operationResult.balance_id.to_xdr_bytes().hex() +print(f"Balance ID (2): {balanceId}") + +# Method 3: Account B could alternatively do something like: +try: + balances = ( + server + .claimable_balances() + .for_claimant(B.public_key) + .limit(1) + .order(desc = True) + .call() + ) +except (BadRequestError, BadResponseError) as err: + print(f"Claimable balance retrieval failed: {err}") + +balanceId = balances["_embedded"]["records"][0]["id"] +print(f"Balance ID (3): {balanceId}") +``` + + + +With the claimable balance ID acquired, either Account B or A can actually submit a claim, depending on which predicate is fulfilled. We'll assume here that a minute has passed, so Account A just reclaims the balance entry. + + + +```js +let claimBalance = sdk.Operation.claimClaimableBalance({ balanceId: balanceId }); +console.log(A.publicKey(), "claiming", balanceId); + +let tx = new sdk.TransactionBuilder(aAccount, {fee: sdk.BASE_FEE}) + .addOperation(claimBalance) + .setNetworkPassphrase(sdk.Networks.TESTNET) + .setTimeout(180) + .build(); + +tx.sign(A); +await server.submitTransaction(tx).catch(function (err) { + console.error(`Tx submission failed: ${err}`) +}); +``` + +```go +claimBalance := txnbuild.ClaimClaimableBalance{BalanceID: balanceId} +tx, err = txnbuild.NewTransaction( + txnbuild.TransactionParams{ + SourceAccount: aAccount.AccountID, // or Account B, depending on the condition! + IncrementSequenceNum: true, + BaseFee: txnbuild.MinBaseFee, + Timebounds: txnbuild.NewInfiniteTimeout(), + Operations: []txnbuild.Operation{&claimBalance}, + }, +) +check(err) +tx, err = tx.Sign(network.TestNetworkPassphrase, aKeys) +check(err) +txResp, err = client.SubmitTransaction(tx) +check(err) +``` + +```python +claimBalance = ClaimClaimableBalance(balance_id = balanceId) +print(f"{A.public_key} claiming {balanceId}") + +tx = ( + TransactionBuilder ( + source_account = aAccount, + network_passphrase = Network.TESTNET_NETWORK_PASSPHRASE, + base_fee = server.fetch_base_fee() + ) + .append_operation(claimBalance) + .set_timeout(180) + .build() +) + +tx.sign(A) +try: + txResponse = server.submit_transaction(tx) +except (BadRequestError, BadResponseError) as err: + print(f"Tx submission failed: {err}") +``` + + + +And that's it! At this point, since we opted for the "reclaim" path, Account A should have the same balance as what it started with (sans fees), and Account B should be unchanged. diff --git a/docs/glossary/clawback.mdx b/docs/glossary/clawback.mdx new file mode 100644 index 000000000..966a77c80 --- /dev/null +++ b/docs/glossary/clawback.mdx @@ -0,0 +1,408 @@ +--- +title: Clawbacks +order: +--- + +import { CodeExample } from "@site/src/components/CodeExample"; +import { Alert } from "@site/src/components/Alert"; + +Protocol 17 introduces operations that allow asset issuers to maintain tighter control over how their asset is distributed to the world. Specifically, it gives them power to **burn their asset** from a trustline or claimable balance, effectively removing it from the recipient's balance sheet: + +> The amount of the asset clawed back is burned and is not sent to any other address. The issuer may reissue the asset to the same account or to another account if the intent of the clawback is to move the asset to another account. + +This allows for things like regulatory enforcement, safety and control over certain assets, etc. You can refer to [CAP-35](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0035.md) for more motivations or technical details behind these new features. + +## Relevant operations + +Configuring and performing a clawback involves several discrete steps: + +- **the issuer**: sets up their account to enable clawbacks (via [SetOptions](../start/list-of-operations.mdx#set-options)), +- **the recipients**: establish trustlines for the issuer's asset (via [ChangeTrust](../start/list-of-operations.mdx#change-trust)), +- someone transfers value with the issued asset (e.g. via a [Payment](../start/list-of-operations.mdx#payment) or other mechanism), and finally +- **the issuer** claws back some or all of the asset (via [Clawback](../start/list-of-operations.mdx#clawback) or [Clawback Claimable Balance](../start/list-of-operations.mdx#clawback-claimable-balance)). + +### Setting Account Options + +In order to enable clawback, the issuer account must first set the `AUTH_CLAWBACK_ENABLED` flag on itself. This will cause _every_ subsequent trustline established from this account to have its corresponding `TRUSTLINE_CLAWBACK_ENABLED_FLAG` set automatically. + +Note an important corequirement described in [the CAP](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0035.md#semantics): + +> If an issuer wishes to set `AUTH_CLAWBACK_ENABLED_FLAG`, it must also have `AUTH_REVOCABLE_FLAG` set. + +This allows an asset issuer to clawback balances locked up in offers by first revoking authorization from a trustline, which pulls all offers that involve that trustline. The issuer can then perform the clawback. + +If you (as an asset issuer) would like to forgo clawback capabilities on a _specific_ trustline, you can clear its `TRUSTLINE_CLAWBACK_ENABLED_FLAG` flag via [SetTrustLineFlags](../start/list-of-operations.mdx#set-trustline-flags). Note that you can **only** clear this flag, not set it, in order to give asset holders perpetual confidence about the future state of their holdings. + +### Clawback + +Once an account holds a particular asset for which clawbacks have been enabled, you can claw it back (provided you are the issuer, obviously). You need to provide the asset, a quantity, and the account from which you're clawing back the asset. + +### Clawback Claimable Balance + +[Claimable balances](./claimable-balance.mdx), introduced in [Protocol 15](https://www.stellar.org/blog/protocol-14-improvements), provide another way for accounts to acquire assets. They are a special, two-part payment, and thus need to be clawed back differently. All you need to do is provide the claimable balance ID. + +## Examples + +Here we'll cover the two main approaches to clawing back an asset. An issuing account ("Account A") will create an asset and send some to a top-level recipient ("Account B"), who will then make it available to a third party ("Account C"). We'll add this level of indirection to demonstrate that clawbacks don't rely on direct relationships between accounts. + +Finally, A will perform a clawback on the asset from C. In one scenario, Account B will pay Account C directly. In the other, Account B will transfer the asset to Account C via a claimable balance. + + + +In the following code samples, proper error checking is omitted for brevity. However, you should always validate your results, as there are many ways that requests can fail. You can refer to the guide on [Handling Errors Gracefully](../tutorials/handling-errors.mdx) for tips on error management strategies. + + + +### Preamble: Issuing a Clawback-able Asset + +First, we'll set up an account to enable clawbacks and issue an asset accordingly. Note that _properly_ issuing an asset (with separate issuing and distribution accounts) is a little more involved, but you can refer to the [tutorial](../issuing-assets/how-to-issue-an-asset.mdx) for more; we use a simpler method here for brevity. + + + +```js +const sdk = require("stellar-sdk"); + +let server = new sdk.Server("https://horizon-testnet.stellar.org"); + +const A = sdk.Keypair.fromSecret("SAQLZCQA6AYUXK6JSKVPJ2MZ5K5IIABJOEQIG4RVBHX4PG2KMRKWXCHJ"); +const B = sdk.Keypair.fromSecret("SAAY2H7SANIS3JLFBFPLJRTYNLUYH4UTROIKRVFI4FEYV4LDW5Y7HDZ4"); +const C = sdk.Keypair.fromSecret("SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4"); + +const ASSET = new sdk.Asset("CLAW", A.publicKey()); + +/// Enables AuthClawbackEnabledFlag on an account. +function enableClawback(account, keys) { + return server.submitTransaction(buildTx( + account, keys, [ + sdk.Operation.setOptions({ + setFlags: sdk.AuthClawbackEnabledFlag | sdk.AuthRevocableFlag, + }), + ], + )); +} + +/// Establishes a trustline for `recipient` for ASSET (from above). +const establishTrustline = function (recipient, key) { + return server.submitTransaction(buildTx( + recipient, key, [ + sdk.Operation.changeTrust({ + asset: ASSET, + limit: "5000", // arbitrary + }), + ], + )); +}; + +/// Retrieves latest account info for all accounts. +function getAccounts() { + return Promise.all([ + server.loadAccount(A.publicKey()), + server.loadAccount(B.publicKey()), + server.loadAccount(C.publicKey()), + ]); +} + +/// Enables clawback on A, and establishes trustlines from C, B -> A. +function preamble() { + return getAccounts().then(function (accounts) { + let [ accountA, accountB, accountC ] = accounts; + return enableClawback(accountA, A) + .then(Promise.all([ + establishTrustline(accountB, B), + establishTrustline(accountC, C), + ]) + ); + }); +} + +/// Helps simplify creating & signing a transaction. +function buildTx(source, signer, ops) { + var tx = new StellarSdk.TransactionBuilder(source, { + fee: 100, + networkPassphrase: StellarSdk.Networks.TESTNET, + }); + ops.forEach((op) => tx.addOperation(op)); + tx = tx.setTimeout(30).build(); + tx.sign(signer); + return tx; +} + +/// Prints the balances of a list of accounts. +function showBalances(accounts) { + accounts.forEach((acc) => { + console.log(`${acc.accountId().substring(0, 5)}: ${getBalance(acc)}`); + }); +} +``` + + + +Note that above we first enabled clawback and _then_ established trustlines, since you cannot retroactively enable clawback on existing trustlines. + +### Example: Payments + +With the shared setup code out of the way, we can now demonstrate how clawback works for payments. This example will highlight how the asset issuer holds control over their asset regardless of how it gets distributed to the world. + +In our scenario, Account A will pay Account B with 1000 tokens of its custom asset; then, B will pay Account C 500 tokens in turn. Finally, A will "claw back" half of C's balance, burning 250 tokens forever. Let's dive in to the helper functions: + + + +```js +/// Make a payment to `toAccount` from `fromAccount` for `amount`. +function makePayment(toAccount, fromAccount, fromKey, amount) { + return server.submitTransaction(buildTx( + fromAccount, fromKey, [ + sdk.Operation.payment({ + destination: toAccount.accountId(), + asset: ASSET, // defined in preamble + amount: amount, + }), + ], + )); +}; + +/// Perform a clawback by `byAccount` of `amount` from `fromAccount`. +function doClawback(byAccount, byKey, fromAccount, amount) { + return server.submitTransaction(buildTx( + byAccount, byKey, [ + sdk.Operation.clawback({ + from: fromAccount.accountId(), + asset: ASSET, // defined in preamble + amount: amount, + }), + ], + )); +}; + +/// Retrieves the balance of ASSET in `account`. +function getBalance(account) { + const balances = account.balances.filter((balance) => { + return (balance.asset_code == ASSET.code && + balance.asset_issuer == ASSET.issuer); + }); + return balances.length > 0 ? balances[0].balance : "0"; +} +``` + + + +These snippets will help us with the final composition: making some payments to distribute the asset to the world and clawing some of it back. + + + +```js +function examplePaymentClawback() { + return getAccounts().then(function(accounts) { + let [ accountA, accountB, accountC ] = accounts; + return makePayment(accountB, accountA, A, "1000") + .then(makePayment(accountC, accountB, B, "500")) + .then(doClawback( accountA, A, accountC, "250")); + }) + .then(getAccounts) + .then(showBalances); +} + +preamble().then(examplePaymentClawback); +``` + + + +After running our example, we should see the balances reflect the example flow: + + + +```txt +GCIHA: 0 +GDS5N: 500 +GC2BK: 250 +``` + + + +Notice that `GCIHA` (this is Account A, the issuer) holds none of the asset, despite clawing back 250 from Account C. This should drive home the fact that clawed-back assets are _burned_, not transferred. + +_(It may be strange that A never holds any tokens of its custom asset, but that's exactly how issuing works: you create value where there used to be none. Sending an asset to its issuing account is *equivalent* to burning it, and auditing the total amount of an asset in existence is one of the benefits of properly distributing an asset via a [distribution account](../issuing-assets/how-to-issue-an-asset.mdx#why-have-separate-accounts-for-issuing-and-distribution), which we avoid doing here for example brevity.)_ + +### Example: Claimable Balances + +Direct payments aren't the only way to transfer assets between accounts: [claimable balances](./claimable-balance.mdx) essentially create a "two-part payment" in which a valid claimant must actively accept the transfer. As a separate payment mechanism, they also need a separate clawback mechanism. + +We need some additional helper methods to get started working efficiently with claimable balances: + + + +```js +function createClaimable(fromAccount, fromKey, toAccount, amount) { + return server.submitTransaction(buildTx( + fromAccount, fromKey, [ + sdk.Operation.createClaimableBalance({ + asset: ASSET, + amount: amount, + claimants: [ + new sdk.Claimant(toAccount.accountId()), + ], + }), + ], + )); +} + +// https://developers.stellar.org/docs/glossary/claimable-balance/#example +function getBalanceId(txResponse) { + const txResult = sdk.xdr.TransactionResult.fromXDR( + txResponse.result_xdr, "base64"); + const operationResult = txResult.result().results()[0]; + + let creationResult = operationResult.value().createClaimableBalanceResult(); + return creationResult.balanceId().toXDR("hex"); +} + +function clawbackClaimable(issuerAccount, issuerKey, balanceId) { + return server.submitTransaction(buildTx( + issuerAccount, issuerKey, [ + sdk.Operation.clawbackClaimableBalance({ balanceId }) + ], + )); +} +``` + + + +Now, we can fulfill the flow: A pays B, who sends a claimable balance to C, who gets it clawed back by A. (Note that we rely on the `makePayment` helper from the [earlier example][ex1].) + + + +```js +function exampleClaimableBalanceClawback() { + return getAccounts() + .then(function(accounts) { + let [ accountA, accountB, accountC ] = accounts; + + return makePayment(accountB, accountA, A, "1000") + .then(() => createClaimable(accountB, B, accountC, "500")) + .then((txResp) => clawbackClaimable(accountA, A, getBalanceId(txResp))); + }) + .then(getAccounts) + .then(showBalances); +} +``` + + + +After running `preamble().then(examplePaymentClawback)`, we should see the balances reflect our flow: + + + +```txt +GCIHA: 0 +GDS5N: 500 +GC2BK: 0 +``` + + + +Note that after a particular claimable balance has been _claimed_, this method no longer applies: you should claw back your asset directly using the [previous method][ex1], via `Operation.clawback`. This method is for unclaimed, _pending_ claimable balances. + +Further note the divergence from the [earlier example][ex1]: you can't do a _partial_ clawback of the claimable balance. It's an all-or-nothing operation. + +### Example: Selectively Enabling Clawback + +When you enable the `AUTH_CLAWBACK_ENABLED_FLAG` on your account, it will make all future trustlines have clawback enabled for any of your issued assets. This may not always be desireable: you may want certain assets to behave as they did before. Though you could work around this by reissuing assets from a "dedicated clawback" account, you can also simply disable clawbacks for certain trustlines by clearing the `TRUST_LINE_CLAWBACK_ENABLED_FLAG` on a trustline. + +In the following example, we'll have an account (Account A, as before) issue a new asset and distribute it to a second account (Account B). We won't reintroduce Account C like in the earlier examples in order to keep things simple. Then, we'll demonstrate how A claws back some of the asset from B, then clears the trustline and can no longer claw back the asset. + +First, let's prepare the accounts (note that we are relying here on helper functions defined in the earlier examples): + + + +```js +function getAccounts() { + return Promise.all([ + server.loadAccount(A.publicKey()), + server.loadAccount(B.publicKey()), + ]); +} + +function preambleRedux() { + return getAccounts().then((accounts) => { + return enableClawback(accounts[0], A) + .then(() => establishTrustline(accounts[1], B)); + }); +} +``` + + + +Now, let's distribute some of our asset to Account B, just to claw it back. Then, we'll clear the flag from the trustline and show that another clawback isn't possible: + + + +```js +function disableClawback(issuerAccount, issuerKeys, forTrustor) { + return server.submitTransaction(buildTx( + issuerAccount, issuerKeys, [ + sdk.Operation.setTrustLineFlags({ + trustor: forTrustor.accountId(), + asset: ASSET, // defined in the (original) preamble + flags: { + clawbackEnabled: false, + }, + }), + ] + )); +} + +function exampleSelectiveClawback() { + return getAccounts().then((accounts) => { + let [ accountA, accountB ] = accounts; + return makePayment(accountB, accountA, A, "1000") + .then(getAccounts).then(showBalances) + .then(() => doClawback(accountA, A, accountB, "500")) + .then(getAccounts).then(showBalances) + .then(() => disableClawback(accountA, A, accountB)) + .then(() => doClawback(accountA, A, accountB, "500")) + .catch((err) => { + if (err.response && err.response.data) { + // Note that this is a *very* specific way to check for an error, and + // you should probably never do it this way. + // We do this here to demonstrate that the clawback error *does* + // occur as expected. + const opErrors = err.response.data.extras.result_codes.operations; + if (opErrors && opErrors.length > 0 && + opErrors[0] === "op_no_clawback_enabled") { + console.info("Clawback failed, as expected!"); + } else { + console.error("Uh-oh, some other failure occurred:", err.response.data.extras); + } + } else { + console.error("Uh-oh, unknown failure:", err); + } + }); + }) + .then(getAccounts) + .then(showBalances); +} +``` + + + +Run the example (e.g. via `preambleRedux().then(exampleSelectiveClawback)`) and observe its result: + + + +```txt +GCIHA: 0 +GDS5N: 1000 +GCIHA: 0 +GDS5N: 500 +Clawback failed, as expected! +GCIHA: 0 +GDS5N: 500 +``` + + + +It's important to note that **clearing the clawback flag on a trustline is irreversible**: you cannot _set_ the flag, only clear it. This behavior is intended and aligns with the earlier caveat that only _new_ trustlines follow the [account's clawback flag](#setting-account-options): it's done so that you cannot retroactively "change the rules" on your asset's holders after they've established trustlines. If you'd like to enable clawback again, holders need to reissue their trustlines. + +[ex1]: #example:-payments +[ex2]: #example:-claimable-balances +[ex3]: #example:-selectively-enable-clawback diff --git a/docs/glossary/decentralized-exchange.mdx b/docs/glossary/decentralized-exchange.mdx new file mode 100644 index 000000000..2257e77f2 --- /dev/null +++ b/docs/glossary/decentralized-exchange.mdx @@ -0,0 +1,82 @@ +--- +title: Decentralized Exchange +order: +--- + +In addition to supporting the issuing and movement of [assets](./assets.mdx), the Stellar network also acts as a decentralized **distributed exchange** that allows you to trade and convert assets on the network. The Stellar ledger stores both balances held by user accounts and orders that user accounts make to buy or sell assets. + +## Orders + +An account can create orders to buy or sell assets using the [Manage Buy Offer](../start/list-of-operations.mdx#manage-buy-offer) or [Manage Sell Offer](../start/list-of-operations.mdx#manage-sell-offer) operation. In order to initiate an order, the account must hold the asset it wants to use to buy (exchange for) the desired asset to be purchased. The account must also trust the issuer of the asset it's trying to buy. + +Orders in Stellar behave like limit orders in traditional markets. When an account initiates an order, it is checked against the existing orderbook for that asset pair. If the submitted order is a marketable order (for a marketable buy limit order, the limit price is at or above the ask price; for a marketable sell limit order, the limit price is at or below the bid price), it is filled at the existing order price for the available quantity at that price. If the order is not marketable (i.e. does not cross an existing order), the order is saved on the orderbook until it is either consumed by another order, consumed by a path payment, or canceled by the account that created the order. + +Each order constitutes a selling obligation for the selling asset and buying obligation for the buying asset. These obligations are aggregated in the account (for lumens) or trustline (for other assets) owned by the account creating the order. Any operation that would cause an account to be unable to satisfy its obligations — such as sending away too much balance, will fail — This guarantees that any order in the orderbook can be executed entirely. + +Orders are executed on a price-time priority, meaning orders will be executed based first on price; for orders placed at the same price, the order that was entered earlier is given priority and is executed before the newer one. + +### Price + +Each order in Stellar is quoted with an associated price, and is represented as a ratio of the two assets in the order, one being the "quote asset" and the other being the "base asset". This is to ensure there is no loss of precision when representing the price of the order (as opposed to storing the fraction as a floating-point number). + +Prices are specified as a `{numerator, denominator}` pair with both components of the fraction represented as 32 bit signed integers. The numerator is considered the base asset, and the denominator is considered the quote asset. When expressing a price of "Asset A in terms of Asset B", the amount of B is denominator (and therefore the quote asset), and A is the numerator (and therefore the base asset). As a good rule of thumb, it's generally correct to be thinking about the base asset that is being bought/sold (in terms of the quote asset). (see comments below) + +When creating a "buy"/"bid" order in Stellar via the [Manage Buy Offer](../start/list-of-operations.mdx#manage-buy-offer) operation, the price is specified as 1 unit of the base currency (the asset being bought), in terms of the quote asset (the asset that is being sold). For example, if you're _buying_ 100 XLM in exchange for 20 USD, you would specify the price as `{20, 100}`, which would be the equivalent of 5 XLM for 1 USD (or $.20 per XLM). + +When creating a "sell"/"offer"/"ask" order in Stellar via the [Manage Sell Offer](../start/list-of-operations.mdx#manage-sell-offer) operation, the price is specified as 1 unit of base currency (the asset being sold), in terms of the quote asset (the asset that is being bought). For example, if you're _selling_ 100 XLM in exchange for 40 USD, you would specify the price as `{40, 100}`, which would be the equivalent of 2.5 XLM for 1 USD (or $.40 per XLM) (_nice profit_). + +#### Fees + +It's important to note that the price you set is unrelated to the fee you pay for submitting the order as a part of a transaction. Fees are always paid in the native currency of the network (lumens), and are related to the transaction that you submit to the network (which contains your order operation) as opposed to your order itself. + +For more information, take a look at [our guide on fees in Stellar](./fees.mdx). + +## Passive Order + +**Passive orders** allow markets to have zero spread. If you want to exchange USD from anchor A for USD from anchor B at a 1:1 price, you can create two passive orders so the two orders don't fill each other. + +A passive order is an order that does not execute against a marketable counter order with the same price. It will only fill if the prices are not equal. For example, if the best order to buy BTC for XLM has a price of 100XLM/BTC, and you make a passive offer to sell BTC at 100XLM/BTC, your passive offer _does not_ take that existing offer. If you instead make a passive offer to sell BTC at 99XLM/BTC it would cross the existing offer and fill at 100XLM/BTC. + +An account can place a passive sell order via the [Create Passive Sell Offer](../start/list-of-operations.mdx#create-passive-sell-offer) operation. + +## Orderbook + +An **orderbook** is a record of outstanding orders on the Stellar network. This record sits between any two assets. Abstractly, we often discuss assets using two _fictional placeholder_ assets traded in a market, which we call "**wheat**" and "**sheep**". The orderbook for that asset-pair therefore records every account wanting to sell wheat for sheep on one side _and_ every account wanting to sell sheep for wheat on the other side. + +(A bit of further market terminology: outside of Stellar, the concept of an "orderbook" normally contains two kinds of "**order**": buying is expressed by "**bid**" orders, and selling is expressed by "**ask**" orders, also called "**offers**". Within the Stellar network, the representation of orders is simplified: all orders are stored as **selling** — i.e. the system automatically converts all bids to asks in the opposite direction — and Stellar's documentation and code can therefore sometimes be a bit lax in using the words "offer" and "order" as synonyms. Since both words refer to the same thing in Stellar in all cases, they are occasionally used interchangeably. This is harmless in the context of the Stellar ecosystem, but outside that context, an "offer" usually means an "ask" order _specifically_!) + +An orderbook can be summarized by a diagram, as shown below. It is often visible on the trading interface of an exchange, though sometimes inverted left-to-right or drawn horizontally. But the idea is the same. + +The diagram is split into two stacks of orders. Each stack is the set of orders related to selling an asset _in a trading pair_ with the other asset. So at the top of the diagram there are the orders of people trying to sell sheep (or, equivalently, buy wheat). At the bottom there are the orders of people trying to sell wheat (or, equivalently, buy sheep). + +For mnemonic purposes we've arranged the diagram with the sheep "on top" of the wheat here. If it helps you can picture a bunch of sheep standing on a field of wheat or some bushels of wheat, an arrangement less likely to cause chaos than trying to stack bushels of wheat on top of sheep. + +![Diagram of orderbook](../web-assets/orderbook.png) + +Looking at the diagram, there are a few orientation things to notice and think about: + +1. Being willing to sell wheat for sheep is _exactly the same_ as being willing to buy sheep for wheat. There are differences once we get into which direction of price movement you'll accept as "better than your order", but in general it's just a question of which unit you declare the price and quantity for, so for uniformity sake, our order book encodes orders on both sides of a trading pair as _selling_. + +1. Generally more people will be willing to _sell_ more of an asset at higher sale prices. This _makes sense_ intuitively, an embodiment of the notion that "everybody has a price". Not everyone will sell their favourite shoes for $100, but probably everyone will for $1,000 or $10,000. Put another way: higher sale prices for an asset are "better" for people trying to sell it. If you're _selling_ at some price "or better", that means "or higher prices". (Equivalently: "or better" means "or _lower_ price" for _buyers_, but again, we model both sides here as _sellers_). Similarly there is (intuitively) a lowest price at which _anyone_ wants to sell an asset, and likely there aren't _many_ people who want to offer it at that cheapest price. So orders naturally "thin out" toward the center where it wouldn't be especially appealing to bother selling, and "widen out" towards the edges where the prices (should they occur) would be tempting for lots of people to sell at. + +1. At any given moment, the order _book_ (this diagram) contains all the orders that are _not_ "**matched**", i.e. that cannot be executed because the prices offered are not acceptably good to parties on either side to make a trade. Orders recorded in the book are the unmatched _residue_ of orders submitted for trading. In other words, when someone submits a new order, the exchange matching engine will compare it to the orders in the book and _execute_ any part it can, swapping assets from the parties involved and effectively _deleting the intersection_ of the submitted order and the order book from both, writing only the _symmetric difference_ of them back into the book. Orders that _do_ match (and should be executed / symmetric-differenced) are also called "**crossing orders**" (because they occur when the upper and lower parts of the diagram intersect, or _cross_), and sometimes this term is used as a verb, and the entire act of matching and executing is called "**crossing**" a pair of orders/offers. + +1. Because the set of orders is not perfectly smooth -- there is not necessarily every possible quantity of an asset on offer at every possible price -- deleting the symmetric difference of matching orders is likely to open up a _gap_ between the _remaining_ cheapest offers (if there are any!) in either direction of the trading pair. This gap is the **spread** in the pair. The size of the spread will vary depending on quantity and variability of prices asked by sellers: a sparse or highly variably-priced set of offers will produce a bigger spread. + +Some assets will have a very thin or nonexistent orderbook between them. That's fine: as discussed in greater detail below, paths of orders can facilitate exchange between two thinly traded assets. + +## Cross-asset payments + +Suppose you are holding sheep and want to buy something from a store that only accepts wheat. You can create a payment in Stellar that will automatically convert your sheep into wheat. It goes through the sheep/wheat orderbook and converts your sheep at the best available rate. + +You can also make more complicated paths of asset conversion. Imagine that the sheep/wheat orderbook has a very large spread or is nonexistent. In this case, you might get a better rate if you first trade your sheep for brick and then sell that brick for wheat. So a potential path would be 2 hops: sheep->brick->wheat. This path would take you through the sheep/brick orderbook and then the brick/wheat orderbook. + +These paths of asset conversion can contain up to 6 hops, but the whole payment is atomic--it will either succeed or fail. The payment sender will never be left holding an unwanted asset. + +This process of finding the best path of a payment is called **pathfinding**. Pathfinding involves looking at the current orderbooks and finding which series of conversions gives you the best rate. It is handled outside of Stellar Core by something like Horizon. In foreign exchange this is often referred to as multi-leg and cross-currency transactions. + +## Preferred currency + +Because cross-asset payments are so simple with Stellar, users can keep their money in whatever asset they prefer to hold. **Preferred currency** creates a very flexible, open system. + +Imagine a world where, anytime you travel, you never have to exchange currency except at the point of sale. A world where you can choose to keep all your assets in, for example, Google stock, cashing out small amounts as you need to pay for things. Cross-asset payments make this world possible. diff --git a/docs/glossary/federation.mdx b/docs/glossary/federation.mdx new file mode 100644 index 000000000..82d8b0c61 --- /dev/null +++ b/docs/glossary/federation.mdx @@ -0,0 +1,96 @@ +--- +title: Federation +order: +--- + +import { CodeExample } from "@site/src/components/CodeExample"; +import { Alert } from "@site/src/components/Alert"; + +The [Stellar federation protocol](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0002.md) maps Stellar addresses to more information about a given user. It's a way for Stellar client software to resolve email-like addresses such as `name*yourdomain.com` into account IDs like: `GCCVPYFOHY7ZB7557JKENAX62LUAPLMGIWNZJAFV2MITK6T32V37KEJU`. Federated addresses provide an easy way for users to share payment details by using a syntax that interoperates across different domains and providers. + +## Federated addresses + +Stellar federated addresses are divided into two parts separated by `*`: the username and the domain. + +For example: `jed*stellar.org`: + +- `jed` is the username, +- `stellar.org` is the domain. + +The domain can be any valid RFC 1035 domain name. The username is limited to printable UTF-8 with whitespace and the following characters excluded: <\*,>. + +Note that the `@` symbol is allowed in the username. This means you can use email addresses in the username of a federated address. For example: `maria@gmail.com*stellar.org`. + +## Supporting Federation + +To support federation, first create a stellar.toml file, and publish it at `https://YOUR_DOMAIN/.well-known/stellar.toml`. Complete instructions for doing that can be found in the [stellar.toml specifciation](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0001.md) (aka SEP-1). + +In general, you will want to include any and all information about your Stellar integration in your stellar.toml. To support federation specifically, you need to add a `FEDERATION_SERVER` section to your stellar.toml file that tells other people the URL of your federation endpoint. + +For example: `FEDERATION_SERVER="https://api.yourdomain.com/federation"` + +Please note that your federation server **must** use `https` protocol. + +Once you've published the location of your federation server, implement federation url HTTP endpoint that accepts an HTTP GET request and issues responses of the form detailed below: + + + +To make it easier to set up a federation server, the Stellar Development foundation server you can use the [reference implementation](https://github.com/stellar/go/tree/master/services/federation) designed to be dropped into your existing infrastructure.. + + + +## Federation Requests + +You can use the federation endpoint to look up an account id if you have a Stellar address. You can also do reverse federation and look up a Stellar address from an account id or a transaction id. This is useful to see who has sent you a payment. + +Federation requests are http `GET` requests with the following form: + +`?q=&type=` + +Supported types: + +- **name**: Example: `https://YOUR_FEDERATION_SERVER/federation?q=jed*stellar.org&type=name` +- **forward**: Used for forwarding the payment on to a different network or different financial institution. The other parameters of the query will vary depending on what kind of institution is the ultimate destination of the payment and what you as the forwarding anchor supports. Your [stellar.toml](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0001.md) file should specify what parameters you expect in a `forward` federation request. If you are unable to forward or the other parameters in the request are incorrect you should return an error to this effect. Example request: `https://YOUR_FEDERATION_SERVER/federation?type=forward&forward_type=bank_account&swift=BOPBPHMM&acct=2382376` +- **id**: _not supported by all federation servers_ Reverse federation will return the federation record of the Stellar address associated with the given account ID. In some cases this is ambiguous. For instance if an anchor sends transactions on behalf of its users the account id will be of the anchor and the federation server won't be able to resolve the particular user that sent the transaction. In cases like that you may need to use **txid** instead. Example: `https://YOUR_FEDERATION_SERVER/federation?q=GD6WU64OEP5C4LRBH6NK3MHYIA2ADN6K6II6EXPNVUR3ERBXT4AN4ACD&type=id` +- **txid**: _not supported by all federation servers_ Will return the federation record of the sender of the transaction if known by the server. Example: `https://YOUR_FEDERATION_SERVER/federation?q=c1b368c00e9852351361e07cc58c54277e7a6366580044ab152b8db9cd8ec52a&type=txid` + +## Federation Response + +The federation server should respond with an appropriate HTTP status code, headers, and a JSON response. + +You must enable CORS on the federation server so clients can send requests from other sites. The following HTTP header must be set for all federation server responses. + +``` +Access-Control-Allow-Origin: * +``` + +When a record has been found the response should return `200 OK` http status code and the following JSON body: + +``` +{ + "stellar_address": , + "account_id": , + "memo_type": <"text", "id" , or "hash"> *optional* + "memo": *optional* +} +``` + +If a redirect is needed the federation server should return `3xx` http status code and immediately redirect the user to the correct URL using the `Location` header. + +When a record has not been found `404 Not Found` http status code should be returned. + +Every other http status code will be considered an error. The body should contain error details: + +``` +{ + "detail": "extra details provided by the federation server" +} +``` + +## Looking up federation provider via a home domain entry + +Accounts may optionally have a home domain specified. This allows an account to programmatically specify the main federation provider for that account. + +## Caching + +You shouldn't cache responses from federation servers. Some organizations may generate random IDs to protect their users' privacy. Those IDs may change over time. diff --git a/docs/glossary/fee-bumps.mdx b/docs/glossary/fee-bumps.mdx new file mode 100644 index 000000000..750ebfb1a --- /dev/null +++ b/docs/glossary/fee-bumps.mdx @@ -0,0 +1,79 @@ +--- +title: Fee-Bump Transactions +order: +--- + +A fee-bump transaction enables any account to pay the fee for an existing [transaction](./transactions.mdx) without the need to re-sign the existing transaction or manage sequence numbers. They're useful if you need to increase the fee on a pre-signed transaction, or if you want to build a service that covers user fees. Like a regular transaction, these are submitted to the [`/transactions` endpoint](/api/resources/transactions/). _Unlike_ a regular transaction, however, which contains 1-100 [operations](./operations.mdx), a fee-bump transaction contains a single [transaction envelope](./transactions.mdx/#transaction-envelopes). + +## Fee-Bump Transaction Attributes + +### Existing Transaction Envelope + +Each fee-bump transaction encloses a single transaction envelope, which itself encloses a single inner transaction. Before creating a fee-bump transaction, in other words, you must first have a [transaction](./transactions.mdx) wrapped with requisite signatures in a [transaction envelope](./transactions.mdx/#transaction-envelopes). + +In addition to a transaction envelope, each fee-bump transaction has the following attributes: + +### Fee Account + +The account that provides the fee for the fee-bump transaction. It incurs the fee instead of the source account specified in the inner transaction. The sequence number for the fee-bump transaction, however, is still taken from the source account specified in the inner transaction. + +### Fee + +The maximum per-operation fee you are willing to pay for the fee-bump transaction. + +A fee-bump transaction has an effective number of operations equal to one plus the number of operations in the inner transaction. Therefore, the minimum fee for a fee-bump transaction is one base fee _more_ than the minimum fee for +the inner transaction, and the fee rate is normalized by one plus the number of operations in the inner transaction. For more info on fee rate calculation, see [Fees](./fees.mdx). + +#### Replace-by-Fee + +You can use a fee-bump transaction to increase the fee on a transaction originating from your own account — something you may want to consider if a transaction is failing to make the ledger due to surge pricing. However, there is a condition: if you submit two distinct transactions with the same source account and sequence number, and the second transaction is a fee-bump transaction, the second transaction will be included in the transaction queue in place of the first transaction if and only if the fee bid of the second transaction is _at least 10x the fee bid of the first transaction_. Though that limit may seem somewhat arbitrary, it was a deliberate design decision to limit DOS attacks without introducing too much complexity to the protocol. + +## Fee-Bump Transaction Envelopes + +Once a fee-bump transaction is ready to be signed, it's wrapped in a transaction envelope, which contains the fee-bump transaction as well as the signature of the fee account specified in the fee-bump transaction. + +Ultimately, transaction envelopes are passed around the network and are included in transaction sets, as opposed to raw transaction objects. + +## Validity of a Fee-Bump Transaction + +A fee-bump transaction goes through a series of checks in its lifecycle to determine validity. For a fee-bump transaction to be valid, the following conditions must be met: + +- **Fee Account** — The fee account for the fee-bump transaction must exist on the ledger. + +- **Fee** — The fee must be greater than or equal to the [network minimum fee](./fees.mdx) for the number of operations in the inner transaction, +1 for the fee bump. It must also be greater than or equal to the fee specfied in the inner transaction. Additionally, if the fee-bump transaction is taking advantage of the [replace-by-fee](#replace-by-fee) feature, in which a transaction envelope in the transaction queue is replaced by a fee-bump transaction envelope with the same sequence number and source account, the fee must be at least 10x higher. + +- **Fee Account Signature** — The fee-bump transaction envelope must contain a valid signaure for the fee account. Additionally, the weight of that signature must meet the low threshold for the fee account, and the appropriate network passphrase must be part of the transaction hash signed by the fee account. See [Network Passphrases](./network-passphrase.mdx) for more. + +- **Fee Account Balance** — The fee account must have a sufficient XLM balance to cover the fee. + +- **Inner Transaction** — For a fee-bump to succeed, the inner transaction must be valid, which means that it must meet the requirements described in the [Validity of a Transaction](./transactions.mdx/#validity-of-a-transaction) section. If validation of the inner transaction is successful, then the result is `FEE_BUMP_INNER_SUCCESS`, and the validation results from the validation of the inner transaction appear in the inner result. If the inner transaction is invalid, the result is `FEE_BUMP_INNER_FAILED`, and the fee-bump transaction is invalid because the inner transaction is invalid. + +## Application + +The sole purpose of a fee-bump transaction is to get an inner transaction included in a transaction set. Since the fee-bump transaction has no side-effects other than paying a fee — and at the time the fee is paid the outer transaction must have been valid (otherwise nodes would not have voted for it) — there is no reason to check the validity of the fee-bump transaction at apply time. Therefore, the sequence number of the inner transaction is always consumed at apply time. The inner transaction, however, will still have its validity checked at apply time. + +Every fee-bump transaction result contains a complete inner transaction result. This inner-transaction result is exactly what would have been produced had there been no fee-bump transaction, except that the inner fee will always be 0. + +A fee-bump transaction is essentially a wrapper around a transaction that has been bundled with requisite signatures into a transaction envelope. Therefore, before creating a fee-bump transaction, you must first create a regular transaction and transaction envelope. For more on regular transaction creation and a summary of the lifecycle of a transaction, see [Transactions](./transactions.mdx). + +## Result Codes + +Fee-bump transactions share result codes with regular transactions. They're listed in a table below. Error reference for operations can be found in [List of Operations](../start/list-of-operations.mdx) doc. + +| Result | Code | Description | +| --- | --- | --- | +| FEE_BUMP_INNER_SUCCESS | 1 | The inner transaction contained in the fee bump succeeded. | +| SUCCESS | 0 | All operations contained in the transaction succeeded. | +| FAILED | -1 | One of the operations failed (check [List of operations](../start/list-of-operations.mdx) for errors). | +| TOO_EARLY | -2 | Ledger `closeTime` before `minTime` value in the transaction. | +| TOO_LATE | -3 | Ledger `closeTime` after `maxTime` value in the transaction. | +| MISSING_OPERATION | -4 | No operation was specified. | +| BAD_SEQ | -5 | Sequence number does not match source account. | +| BAD_AUTH | -6 | Too few valid signatures / wrong network. | +| INSUFFICIENT_BALANCE | -7 | Fee would bring account below [minimum reserve](./minimum-balance.mdx). | +| NO_ACCOUNT | -8 | Source account not found. | +| INSUFFICIENT_FEE | -9 | [Fee](./fees.mdx) is too small. | +| BAD_AUTH_EXTRA | -10 | Unused signatures attached to transaction. | +| INTERNAL_ERROR | -11 | An unknown error occured. | +| NOT_SUPPORTED | -12 | The transaction type is not supported | +| FEE_BUMP_INNER_FAILED | -13 | The fee bump inner transaction failed. See [Fee Bumps](./fee-bumps.mdx) for more info.| diff --git a/docs/glossary/fees.mdx b/docs/glossary/fees.mdx new file mode 100644 index 000000000..f26180e70 --- /dev/null +++ b/docs/glossary/fees.mdx @@ -0,0 +1,55 @@ +--- +title: Fees +order: +--- + +import { CodeExample } from "@site/src/components/CodeExample"; +import { Alert } from "@site/src/components/Alert"; + + + +This doc explains transaction fees. Stellar also requires accounts to have a minimum balance, which you can read about in the [Minimum Balance](./minimum-balance.mdx) doc. + + + +To prevent ledger spam and maintain the efficiency of the network, Stellar requires small transaction fees and minimum balances on accounts. Transaction fees are also used to prioritize transactions when the network enters surge pricing mode. + +## Fee Formula + +Stellar transactions can contain anywhere from 1 to a defined limit of 100 operations. The fee for a given transaction is equal to the number of operations the transaction contains multiplied by the base fee for a given ledger. + + + +``` +Transaction fee = # of operations * base fee +``` + + + +Stellar deducts the entire fee from the transaction’s source account, regardless of which accounts are involved in each operation or who signed the transaction. + +## Base Fee + +The base fee for a given ledger is determined dynamically using a version of a [VCG auction](https://en.wikipedia.org/wiki/Vickrey%E2%80%93Clarke%E2%80%93Groves_auction). When you submit a transaction to the network, you specify the _maximum base fee_ you’re willing to pay per operation, but you’re actually charged the _lowest possible fee_ based on network activity. + +When network activity is below capacity, you pay the network minimum, which is currently **100 stroops (0.00001 XLM)** per operation. + +## Surge Pricing + +When the number of operations submitted to a ledger exceeds network capacity (**currently 1,000 ops/ledger**), the network enters surge pricing mode, which uses market dynamics to decide which submissions are included. Essentially, submissions that offer a higher fee per operation make it onto the ledger first. + +If there’s a tie — in other words multiple transactions that offer the same base fee are competing for the same limited space in the ledger — the transactions are (pseudo-randomly) shuffled, and transactions at the top of the heap make the ledger. The rest of the transactions, the ones that didn’t make the cut, are pushed on to the next ledger, or discarded if they’ve been waiting for too long. If your transaction is discarded, Horizon will return a [timeout error](/api/errors/http-status-codes/horizon-specific/). For more information, see [transaction life cycle](./transactions.mdx#life-cycle-of-a-transaction). + +The goal of the transaction pricing specification, which you can read in full [here](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0005.md), is to maximize network throughput while minimizing transaction fees. + +## Fee Stats and Fee Strategy + +The general rule of thumb: choose the highest fee you're willing to pay to ensure your transaction makes the ledger. Wallet developers may want to offer users a chance to specify their own base fee, though it may make more sense to set a persistent global base fee multiple orders of magnitude above the market rate — 0.1 XLM, for instance — since the average user probably won't care if they’re paying 0.8 cents or 0.00008 cents. + +If you keep getting a timeout error when you submit a transaction, you may need to increase your base fee, or wait until network activity abates and re-submit your transaction. To help inform that decision, you can consult the Horizon `/fee_stats` endpoint, which provides detailed information about per-operation fee stats for the last five ledgers. You can find the same information on the fee stats panel of the dashboard. All three of the SDF-maintained SDKs also allow you to poll the `/fee_stats` endpoint: [Go](https://godoc.org/github.com/stellar/go/clients/horizonclient#Client.FeeStats), [Java](https://stellar.github.io/java-stellar-sdk/), [Javascript](https://stellar.github.io/js-stellar-sdk/Server.html#feeStats). + +## Fee Pool + +The fee pool is the lot of lumens collected from transaction fees. + +SDF does not retain these lumens. They go into a locked account and sit there unused by anyone. diff --git a/docs/glossary/inflation.mdx b/docs/glossary/inflation.mdx new file mode 100644 index 000000000..e305291c4 --- /dev/null +++ b/docs/glossary/inflation.mdx @@ -0,0 +1,12 @@ +--- +title: Inflation +order: +--- + +Prior to Protocol 12, Stellar had a built-in inflation mechanism conceived to allow account holders to collectively direct inflation-generated lumens toward projects built on Stellar. + +As the network evolved and grew, it became increasingly clear that inflation wasn't working as intended — account holders either didn't set their inflation destination or joined inflation pools to claim the inflation themselves, and the operational costs associated with inflation payments continued to rise — and so a [protocol change to disable inflation](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0026.md) was proposed, implemented, voted on by validators, and ultimately adopted as part of a network upgrade. + +The inflation operation is now deprecated. + +For more info, check [here](https://www.stellar.org/blog/our-proposal-to-disable-inflation) diff --git a/docs/glossary/ledger.mdx b/docs/glossary/ledger.mdx new file mode 100644 index 000000000..f5b695638 --- /dev/null +++ b/docs/glossary/ledger.mdx @@ -0,0 +1,83 @@ +--- +title: Ledger +order: +--- + +A ledger represents the state of the Stellar universe at a given point in time. It’s shared across all the nodes that make up the network, and contains the list of all accounts and balances, all orders on the distributed exchange, and any other data that persists. + +Every Stellar Consensus Protocol (SCP) round, the network reaches consensus on which transaction set to apply to the last closed ledger; when the new set is applied, a new "last closed ledger" is defined. + +Each ledger is cryptographically linked to a unique previous ledger, creating a historical ledger chain that goes back to the genesis ledger, which is the first ledger in the history of the network. + +The sequence number of a ledger is defined recursively: + +- The genesis ledger has sequence number 1 +- The ledger directly following a ledger with sequence number n has sequence number n+1 + +## Ledger header + +Every ledger has a **ledger header**. This header has references to the actual data within the ledger as well as a reference to the previous ledger. References here are cryptographic hashes of the content being referenced — the hashes behave like pointers in typical data structures but with added security guarantees. + +You can think of the historical ledger chain as a linked list of ledger headers: + +[Genesis] <---- [LedgerHeader_1] <----- ... <---- [LedgerHeader_n] + +Every ledger header has the following fields: + +- **Version**: Protocol version of this ledger. + +- **Previous Ledger Hash**: Hash of the previous ledger. Forms a chain of ledgers stretching back to the genesis ledger. + +- **SCP value**: During consensus all the validating nodes in the network run SCP and come to an agreement about a particular value, which is a transaction set they will apply to a ledger. This value is stored here and in the next three fields (transaction set hash, close time, and upgrades). + + - **Transaction set hash**: Hash of the transaction set that was applied to the previous ledger. + + - **Close time**: When the network closed this ledger. UNIX timestamp. + + - **Upgrades**: How the network adjusts overall values like the [base fee](./fees.mdx) and agrees to network-wide changes like switching to a new protocol version. This field is usually empty. When there _is_ a network-wide upgrade in the works, the SDF will inform and help coordinate participants using the [#validators channel on Keybase](https://keybase.io/team/stellar.public). So if you run a validator, it’s important to join and monitor that channel. For more info, see [versioning](./versioning.mdx). + +- **Transaction set result hash**: Hash of the results of applying the transaction set. This data is not, strictly speaking, necessary for validating the results of the transactions. However, it makes it easier for entities to validate the result of a given transaction without having to apply the transaction set to the previous ledger. + +- **Bucket list hash**: Hash of all the objects in this ledger. The data structure that contains all the objects is called the [bucket list](https://github.com/stellar/stellar-core/tree/master/src/bucket). + +- **Ledger sequence**: The sequence number of this ledger. + +- **Total coins**: Total number of lumens in existence. + +- **Fee pool**: Number of lumens that have been paid in fees. Note this is denominated in lumens, even though a transaction’s [`fee`](./transactions.mdx#fee) field is in stroops. + +- **Inflation sequence**: Number of times inflation has been run. Note: the inflation operation was deprecated when validators voted to upgrade the network to Protocol 12 on 10/28/2019. Therefore, inflation no longer runs, so this sequence number no longer changes. + +- **ID pool**: The last used global ID. These IDs are used for generating objects. + +- **Maximum Number of Transactions**: The maximum number of operations validators have agreed to process in a given ledger. If more transactions are submitted than this number, the network will enter into surge pricing mode, which uses a VCG auction to decide which to include in a ledger. For more info, see [fees and surge pricing](./fees.mdx) + +- **Base fee**: The [fee](./fees.mdx) the network charges per [operation](./operations.mdx) in a [transaction](./transactions.mdx). This field is in stroops, which are 1/10,000,000th of a lumen. + +- **Base reserve**: The [reserve](./minimum-balance.mdx) the network uses when calculating an account's minimum balance. + +- **Skip list**: Hashes of ledgers in the past. Allows you to jump back in time in the ledger chain without walking back ledger by ledger. There are 4 ledger hashes stored in the skip list. Each slot contains the oldest ledger that is mod of either 50 5000 50000 or 500000 depending on index skipList[0] mod(50), skipList[1] mod(5000), etc. + +## Ledger Entries + +The ledger is a collection of **entries**. Currently there are 4 types of ledger entries. They're specified in [`src/protocol-curr/xdr/Stellar-ledger-entries.x`](https://github.com/stellar/stellar-core/blob/master/src/protocol-curr/xdr/Stellar-ledger-entries.x). + +### Account entry + +This entry represents an account. In Stellar, everything is built around accounts: transactions are performed by accounts, and accounts control the access rights to balances. + +Other entries are add-ons, owned by a main account entry. With every new entry attached to the account, the minimum balance in XLM goes up for the account. For details, the docs on minimum balance. + +### Trustline entry + +Trustlines are lines of credit the account has given a particular issuer in a specific asset. + +Trustline entries define the rules around the use of this currency. Rules can be defined by the user — who can set a maximum balance limit to limit risk — or by the issuer — who can set a flag to control access to the asset. + +### Offer entry + +Offers are entries that an account creates in the orderbook. They are a way to automate simple trading inside the Stellar network. For more on offers, refer to the distributed exchange documentation. + +### Data entry + +Data entries are key/value pairs attached to an account. They allow account controllers to attach arbitrary data to their account, and provide a flexible extension point to add application specific data to the ledger. diff --git a/docs/glossary/liquidity-pool.mdx b/docs/glossary/liquidity-pool.mdx new file mode 100644 index 000000000..b387f0a29 --- /dev/null +++ b/docs/glossary/liquidity-pool.mdx @@ -0,0 +1,506 @@ +--- +title: Liquidity Pools +order: +--- + +import { CodeExample } from "@site/src/components/CodeExample"; + +As of Protocol 18 and [CAP-38](https://stellar.org/protocol/cap-38), the Stellar network supports _liquidity pools_, which enables _automated market making_ on the network. If you’re already familiar with AMMs, feel free to skip ahead to the relevant [operations](#operations) or dive straight into the [examples](#examples). + +## AMM Primer + +In order to be able to buy an asset, someone needs to be willing to sell it. The easier it is to quickly and cost-effectively trade large amounts of an asset, the more liquid it is. Colloquially, this is what it means to be able to easily liquidate your assets into cash when you need it. + +In a traditional exchange like the NYSE, there are lots of individuals (e.g. retail investors) and companies (e.g. hedge funds) making trades on any given asset. A _market maker_ for a particular asset is anyone regularly offering to buy and sell it at quoted prices; market makers create liquidity because their offers make it easier to quickly trade large amounts of an asset. The market maker is available to trade even when no one else wants to. Typically, market makers are large investment firms that have the capital to buy or sell arbitrary assets in large quantities, but anyone can be a market maker. + +Market makers generally don’t provide liquidity out of the goodness of their hearts. Like other businesses, they are seeking to generate a profit. They achieve this by offering to buy and sell at different prices—the difference between these prices is known as the _spread_. If a market maker successfully buys low and sells high, then they will generate a profit. But nothing is guaranteed, and they may generate a loss if the market moves against them while they hold a position. Making the spread wider makes it less likely that the market maker will lose money, but also discourages people from trading. + +An _automated market maker_ provides liquidity but, unlike a conventional market maker, the quoted prices are determined solely by a mathematical equation. An automated market maker holds two different assets in a _liquidity pool_, and the quantities of those assets—typically called _reserves_—are inputs to the mathematical equation. Liquidity pools democratize market making by letting any eligible participant deposit assets into the liquidity pool. In return for their deposit they will receive _pool shares_ representing their ownership of the assets in the liquidity pool. If there are 150 total pool shares and they own 30, then they are entitled to withdraw 20% of the assets at any time. + +If an automated market maker holds more reserves, then the price moves less in response to a trade. The price always moves against a trade, so traders get better prices when the automated market maker holds more reserves. In the interest of providing competitive prices, automated market makers attract reserves by incentivizing liquidity providers. The automated market maker attempts to capture the spread for the liquidity providers by offering to buy and sell at different prices. In this context, the spread is typically referred to as a fee. The fees cause the reserves to grow. But like traditional market making, nothing is guaranteed—the automated market maker may lose money if the market moves. If the automated market maker does make money, then the reserves will grow and participants will find that they may withdraw more than they deposited. + +## AMM Pricing + +An automated market maker is willing to make some trades and unwilling to make others. For example, if 1 EUR = 1.17 USD then the automated market maker might be willing to sell 1 EUR for 1.18 USD and unwilling to sell 1 EUR for 1.16 USD. How does an automated market maker know what trades are acceptable and what trades aren’t acceptable? How does it even know what the exchange rate is? + +To determine what trades are acceptable, the automated market maker enforces an _invariant_. There are many possible invariants with different advantages and disadvantages, but automated market makers built on Stellar enforce the _constant product invariant_ and are therefore known as _constant product market makers_. This is analogous to the invariant enforced by [Uniswap](https://uniswap.org/). The constant product invariant states that an automated market maker must never allow the product of the reserves to decrease. For example, suppose the current reserves in the liquidity pool are 1000 EUR and 1170 USD which implies a product of 1,170,000. Selling 1 EUR for 1.18 USD would be acceptable because that would leave reserves of 999 EUR and 1171.18 USD, which implies a product of 1,170,008.82. But selling 1 EUR for 1.16 USD would not be acceptable because that would leave reserves of 999 EUR and 1171.16 USD, which implies a product of 1,169,988.84. + +In the above example, it seems that the automated market maker somehow knows that the exchange rate is 1 EUR = 1.17 USD. In reality, the automated market maker infers this from the reserves in the liquidity pool. Ignoring fees, the exchange rate is the ratio of the reserves. If the ratio of the reserves deviates from the true exchange rate, then an _arbitrageur_ will recognize that they can trade with the automated market maker at a favorable price. Arbitrage trades move the ratio of the reserves towards the market exchange rate. + +An automated market maker charges a fee on every trade, and that fee is a fixed percent of the amount bought by the automated market maker. For example, if an automated market maker sells 100 EUR for 118 USD then the fee is charged on the USD. The fee is 30 [bps](https://en.wikipedia.org/wiki/Basis_point), which is equal to 0.30%. So if you actually wanted to make this trade, you would need to pay about 118.355 USD for 100 EUR. The automated market maker actually factors the fees into the constant product invariant, so in reality the product of the reserves grows after every trade. + +## Liquidity Pool Participation + +Participation in a liquidity pool is represented by pool shares. Pool shares are very similar to other Stellar assets, but there is one important difference: pool shares are not transferable. The only way to increase the number of pool shares held is to deposit into a liquidity pool (via [`LiquidityPoolDepositOp`](../start/list-of-operations.mdx#liquidity-pool-deposit), and the only way to decrease the number of pool shares held is to withdraw from a liquidity pool [`LiquidityPoolWithdrawOp`](../start/list-of-operations.mdx#liquidity-pool-withdraw). Two specific examples of this are that pool shares cannot be sent in payments and cannot be sold using offers. + +A pool share has two representations. The full representation is used with [`ChangeTrustOp`](../start/list-of-operations.mdx#change-trust), and the hashed representation is used in all other cases. When constructing the asset representation of a pool share, the assets must be in lexicographical order. For example, A-B is in the correct order but B-A is not. This results in a canonical representation of a pool share. + +### Trustlines + +Like other Stellar assets, an account needs a [trustline](https://developers.stellar.org/docs/issuing-assets/anatomy-of-an-asset/#trustlines) for every pool share it wants to own. It is not possible to deposit into a liquidity pool without a trustline for the corresponding pool share. Pool share trustlines differ from trustlines for other assets in a few important ways: + +1. A pool share trustline cannot be created unless the account already has trustlines that are [authorized or authorized to maintain liabilities](https://developers.stellar.org/docs/issuing-assets/control-asset-access/#authorization-required) for the assets in the liquidity pool. See [below](#authorization) for more information about how authorization impacts pool share trustlines. +1. A pool share trustline requires 2 [base reserves](https://developers.stellar.org/docs/glossary/minimum-balance/) instead of 1. For example, an account (2 base reserves) with a trustline for asset A (1 base reserve), a trustline for asset B (1 base reserve), and a trustline for the A-B pool share (2 base reserves) would have a reserve requirement of 6 base reserves. + +### Fees + +Analogous to the spread in a traditional market, an automated market maker charges a fee on all trades. The participants in the liquidity pool receive a share of the fee proportional to their share of the assets in the liquidity pool. The fee rate is fixed at 30 bps, which is equal to 0.30%. These fees are completely separate from [network fees](https://developers.stellar.org/docs/glossary/fees/). + +### Authorization + +Pool share trustlines cannot be authorized or deauthorized independently. Instead, the authorization of a pool share trustline is derived from the trustlines for the assets in the liquidity pool. This design is necessary because a liquidity pool may contain assets from two different issuers, and both issuers should have a say in whether the pool share trustline is authorized. + +There are a few possibilities with regard to authorization. The behavior of the A-B pool share trustline is determined according to the following table: + +| Scenario | Behavior | +| --- | --- | +| Trustlines for A and B are fully authorized | No restrictions on deposit and withdraw | +| Trustline for A is fully authorized but trustline for B is authorized to maintain liabilities

OR

Trustline for B is fully authorized but trustline for A is authorized to maintain liabilities

OR

Trustlines for A and B are authorized to maintain liabilities | Trustlines for A and B are authorized to maintain liabilities | +| Trustline for A is not authorized or doesn’t exist

OR

Trustline for B is not authorized or doesn’t exist | Pool share trustline does not exist | + +If the issuer of A or B revokes authorization, then the account will automatically withdraw from every liquidity pool containing that asset and those pool share trustlines will be deleted. We say that these pool shares have been _redeemed_. For example, if the account participates in the A-B, A-C, and B-C liquidity pools and the issuer of A revokes authorization then the account will redeem from A-B and A-C but not B-C. For each redeemed pool share trustline, a Claimable Balance will be created for each asset contained in the pool if there is a balance being withdrawn and the redeemer is not the issuer of that asset. The claimant of the Claimable Balance will be the owner of the deleted pool share trustline, and the sponsor of the Claimable Balance will be the sponsor of the deleted pool share trustline. The BalanceID of each Claimable Balance is the SHA-256 hash of the [revokeID](https://github.com/stellar/stellar-core/blob/5bec96c4c9d7080802e80a2e93ddc0bd6bd8a98d/src/xdr/Stellar-transaction.x#L539-L547). + +## Operations + +There are two operations that facilitate participation in a liquidity pool: [LiquidityPoolDeposit](https://developers.stellar.org/docs/start/list-of-operations/#liquidity-pool-deposit) and [LiquidityPoolWithdraw](https://developers.stellar.org/docs/start/list-of-operations/#liquidity-pool-withdraw). Use `LiquidityPoolDeposit` whenever a user wants to start providing liquidity to the market. Use `LiquidityPoolWithdraw` whenever a user wants to stop providing liquidity to the market. It’s really that easy. + +However, users don’t need to participate in the pool to take advantage of what it’s offering: an easy way to exchange two assets. For that, just use [PathPaymentStrictReceive](https://developers.stellar.org/docs/start/list-of-operations/#path-payment-strict-receive) or [PathPaymentStrictSend](https://developers.stellar.org/docs/start/list-of-operations/#path-payment-strict-send). If your application is already using path payments, then you don’t need to change anything for users to take advantage of the prices available in liquidity pools. + +## Examples + +For now, we'll cover basic liquidity pool participation and querying. + + + +In the following code samples, proper error checking is omitted for brevity. However, you should always validate your results, as there are many ways that requests can fail. You can refer to the guide on [Handling Errors Gracefully](../tutorials/handling-errors.mdx) for tips on error management strategies. + + + +### Preamble + +For all of the following examples, we'll be working with three funded testnet accounts. If you'd like to follow along, generate some keypairs and fund them via the [friendbot](https://friendbot.stellar.org/?addr=YOUR_PUBLIC_KEY_HERE). If you use the secrets included in this guide, you may encounter problems with asset issuance or other account balance problems. + +The following code sets up the accounts and defines some helper functions. These should be familiar if you've played around with other examples, like [Clawbacks](./clawback.mdx#examples). + + + +```js +const sdk = require("stellar-sdk"); + +let server = new sdk.Server("https://horizon-testnet.stellar.org"); + +/// Helps simplify creating & signing a transaction. +function buildTx(source, signer, ...ops) { + let tx = new sdk.TransactionBuilder(source, { + fee: sdk.BASE_FEE, + networkPassphrase: sdk.Networks.TESTNET, + withMuxing: true, + }); + ops.forEach(op => tx.addOperation(op)); + tx = tx.setTimeout(30).build(); + tx.sign(signer); + return tx; +} + +/// Returns the given asset pair in "protocol order." +function orderAssets(A, B) { + return (sdk.Asset.compare(A, B) <= 0) ? [A, B] : [B, A]; +} + +/// Returns all of the accounts we'll be using. +function getAccounts() { + return Promise.all(kps.map(kp => server.loadAccount(kp.publicKey()))); +} + +const kps = [ + "SBGCD73TK2PTW2DQNWUYZSTCTHHVJPL4GZF3GVZMCDL6GYETYNAYOADN", + "SAAQFHI2FMSIC6OFPWZ3PDIIX3OF64RS3EB52VLYYZBX6GYB54TW3Q4U", + "SCJWYFTBDMDPAABHVJZE3DRMBRTEH4AIC5YUM54QGW57NUBM2XX6433P", +].map(s => sdk.Keypair.fromSecret(s)); + +// kp1 issues the assets +const kp1 = kps[0]; +const [ A, B ] = orderAssets(...[ + new sdk.Asset("A", kp1.publicKey()), + new sdk.Asset("B", kp1.publicKey()), +]); + +/// Establishes trustlines and funds `recipientKp` for all `assets`. +function distributeAssets(issuerKp, recipientKp, ...assets) { + return server + .loadAccount(issuerKp.publicKey()) + .then(issuer => { + const ops = assets.map(asset => [ + sdk.Operation.changeTrust({ + source: recipientKp.publicKey(), + limit: "100000", + asset: asset, + }), + sdk.Operation.payment({ + source: issuerKp.publicKey(), + destination: recipientKp.publicKey(), + amount: "100000", + asset: asset, + }), + ]).flat(); + + let tx = buildTx(issuer, issuerKp, ...ops); + tx.sign(recipientKp); + return server.submitTransaction(tx); + }); +} + +function preamble() { + return Promise.all([1, 2].map(i => distributeAssets(kp1, kps[i], A, B))); +} +``` + +```python +from decimal import Decimal +from typing import List, Any, Dict + +from stellar_sdk import * + +server = Server("https://horizon-testnet.stellar.org") + + +# Preamble +def new_tx_builder(source: str) -> TransactionBuilder: + network_passphrase = Network.TESTNET_NETWORK_PASSPHRASE + base_fee = 100 + source_account = server.load_account(source) + builder = TransactionBuilder( + source_account=source_account, network_passphrase=network_passphrase, base_fee=base_fee + ).set_timeout(30) + return builder + + +# Returns the given asset pair in "protocol order." +def order_asset(a: Asset, b: Asset) -> List[Asset]: + return [a, b] if LiquidityPoolAsset.is_valid_lexicographic_order(a, b) else [b, a] + + +secrets = [ + "SBGCD73TK2PTW2DQNWUYZSTCTHHVJPL4GZF3GVZMCDL6GYETYNAYOADN", + "SAAQFHI2FMSIC6OFPWZ3PDIIX3OF64RS3EB52VLYYZBX6GYB54TW3Q4U", + "SCJWYFTBDMDPAABHVJZE3DRMBRTEH4AIC5YUM54QGW57NUBM2XX6433P", +] +kps = [Keypair.from_secret(secret=secret) for secret in secrets] + +# kp0 issues the assets +kp0 = kps[0] +asset_a, asset_b = order_asset(Asset("A", kp0.public_key), Asset("B", kp0.public_key)) + + +def distribute_assets( + issuer_kp: Keypair, recipient_kp: Keypair, assets: List[Asset] +) -> Dict[str, Any]: + builder = new_tx_builder(issuer_kp.public_key) + for asset in assets: + builder.append_change_trust_op( + asset=asset, limit="100000", source=recipient_kp.public_key + ).append_payment_op( + destination=recipient_kp.public_key, + asset=asset, + amount="100000", + source=issuer_kp.public_key, + ) + + tx = builder.build() + tx.sign(issuer_kp) + tx.sign(recipient_kp) + resp = server.submit_transaction(tx) + return resp + + +def preamble() -> None: + resp1 = distribute_assets(kp0, kps[1], [asset_a, asset_b]) + resp2 = distribute_assets(kp0, kps[2], [asset_a, asset_b]) + # ... +``` + + + +Here, we use `distributeAssets()` to establish trustlines and set up initial balances of two custom assets (`A` and `B`, issued by `kp1`) for two accounts (`kp2` and `kp3`). For someone to participate in the pool, they must establish trustlines to each of the asset issuers _and_ to the pool share asset (explained below). + +Note the `orderAssets()` helper here. Operations related to liquidity pools refer to the asset pair arbitrarily as `A` and `B`; however, they must be "ordered" such that `A < B`. This ordering is defined by the protocol, but its details should not be relevant (if you're curious, it's essentially lexographically ordered by asset type, code, then issuer). We can use the comparison methods built into the SDKs (like `Asset.compare`) to ensure we pass them in the right order and avoid errors. + +### Participation: Creation + +First, lets create a liquidity pool for the asset pair defined in the preamble. This involves establishing a trustline to the pool itself: + + + +```js +const poolShareAsset = new sdk.LiquidityPoolAsset(A, B, sdk.LiquidityPoolFeeV18); + +function establishPoolTrustline(account, keypair, poolAsset) { + return server.submitTransaction( + buildTx(account, keypair, + sdk.Operation.changeTrust({ + asset: poolAsset, + limit: "100000" + }) + ) + ); +} +``` + +```python +pool_share_asset = LiquidityPoolAsset(asset_a=asset_a, asset_b=asset_b) + + +def establish_pool_trustline(source: Keypair, pool_asset: LiquidityPoolAsset) -> Dict[str, Any]: + tx = ( + new_tx_builder(source.public_key) + .append_change_trust_op(asset=pool_asset, limit="100000") + .build() + ) + tx.sign(source) + return server.submit_transaction(tx) +``` + + + +This lets the participants hold pool shares (refer to the discussion about [pool shares earlier](#liquidity-pool-participation) for details), which means now they can perform deposits and withdrawals. + +### Participation: Deposits + +To work with a liquidity pool, you need to know its ID beforehand. It's a deterministic value, and only a single liquidity pool can exist for a particular asset pair, so you can calculate it locally from the pool parameters. + + + +```js +const poolId = sdk.getLiquidityPoolId( + "constant_product", + poolShareAsset.getLiquidityPoolParameters() +).toString("hex"); + +function addLiquidity(source, signer, poolId, maxReserveA, maxReserveB) { + const exactPrice = reserveA / reserveB; + const minPrice = exactPrice - (exactPrice * 0.10); + const maxPrice = exactPrice + (exactPrice * 0.10); + + return server.submitTransaction( + buildTx(source, signer, + sdk.Operation.liquidityPoolDeposit({ + liquidityPoolId: poolId, + maxAmountA: maxReserveA, + maxAmountB: maxReserveB, + minPrice: minPrice.toFixed(7), + maxPrice: maxPrice.toFixed(7), + }) + ) + ); +} +``` + +```python +pool_id = pool_share_asset.liquidity_pool_id + + +def add_liquidity( + source: Keypair, + pool_id: str, + max_reserve_a: Decimal, + max_reserve_b: Decimal, +) -> dict[str, Any]: + exact_price = max_reserve_a / max_reserve_b + min_price = exact_price - exact_price * Decimal("0.1") + max_price = exact_price + exact_price * Decimal("0.1") + tx = ( + new_tx_builder(source.public_key) + .append_liquidity_pool_deposit_op( + liquidity_pool_id=pool_id, + max_amount_a=f"{max_reserve_a:.7f}", + max_amount_b=f"{max_reserve_b:.7f}", + min_price=min_price, + max_price=max_price, + ) + .build() + ) + tx.sign(source) + return server.submit_transaction(tx) +``` + + + +When depositing assets into a liquidity pool, you need to define your acceptable price bounds. In the above function, we allow for a +/-10% margin of error from the "spot price". This margin is **by no means a recommendation** and is chosen just for demonstration. + +Notice that we also specify the maximum amount of each reserve we're willing to deposit. This, alongside the minimum and maximum prices, helps define boundaries for the deposit, since there can always be a change in the exchange rate between submitting the operation and it getting accepted by the network. + +### Participation: Withdrawals + +If you own shares of a particular pool, you can withdraw reserves from it. The operation structure mirrors the deposit closely: + + + +```js +function removeLiquidity(source, signer, poolId, minReserveA, minReserveB) { + return server.submitTransaction( + buildTx(source, signer, + sdk.Operation.liquidityPoolWithdraw({ + liquidityPoolId: poolId, + minAmountA: minReserveA, + minAmountB: minReserveB, + }) + ) + ) +} +``` + +```python +def remove_liquidity( + source: Keypair, pool_id: str, shares_amount: Decimal +) -> dict[str, Any]: + pool_info = server.liquidity_pools().liquidity_pool(pool_id).call() + total_shares = Decimal(pool_info["total_shares"]) + min_reserve_a = ( + shares_amount + / total_shares + * Decimal(pool_info["reserves"][0]["amount"]) + * Decimal("0.95") + ) # + min_reserve_b = ( + shares_amount + / total_shares + * Decimal(pool_info["reserves"][1]["amount"]) + * Decimal("0.95") + ) + tx = ( + new_tx_builder(source.public_key) + .append_liquidity_pool_withdraw_op( + liquidity_pool_id=pool_id, + amount=f"{shares_amount:.7f}", + min_amount_a=f"{min_reserve_a:.7f}", + min_amount_b=f"{min_reserve_b:.7f}", + ) + .build() + ) + tx.sign(source) + return server.submit_transaction(tx) +``` + + + +Notice here that we specify the minimum amount. Much like with a strict-receive path payment, we're specifying that we're not willing to receive less than this amount of each asset from the pool. This effectively defines a minimum withdrawal price. + +### Putting it all together + +Finally, we can combine these pieces together to simulate some participation in a liquidity pool. We'll have everyone deposit increasing amounts into the pool, then one participant withdraws their shares. Between each step, we'll retrieve the spot price. + + + +```js +function main() { + return getAccounts() + .then(accounts => + Promise.all(kps.map((kp, i) => { + const acc = accounts[i]; + const depositA = ((i+1)*1000).toString(); + const depositB = ((i+1)*3000).toString(); // maintain a 1:3 ratio + + return establishPoolTrustline(acc, kp, poolShareAsset) + .then(_ => addLiquidity(acc, kp, poolId, depositA, depositB)) + .then(_ => getSpotPrice()); + })) + ) + .then(_ => withdrawLiquidity(accounts[1], kps[1], "500", "2000")) + .then(_ => getSpotPrice()); +} + +function getSpotPrice() { + return server.liquidityPools() + .liquidityPoolId(poolId) + .call() + .then(pool => { + const [a, b] = pool.reserves.map(r => r.amount); + const spotPrice = (new BigNumber(a)).div(b); + console.log(`Price: ${a}/${b} = ${spotPrice.toFormat(2)}`); + }); +} + +preamble().then(main); +``` + +```python +def main(): + deposit_a = Decimal(1000) + deposit_b = Decimal(3000) # maintain a 1:3 ratio + establish_pool_trustline(kps[1], pool_share_asset) + add_liquidity(kps[1], pool_id, deposit_a, deposit_b) + get_spot_price() + + deposit_a = Decimal(2000) + deposit_b = Decimal(6000) # maintain a 1:3 ratio + establish_pool_trustline(kps[2], pool_share_asset) + add_liquidity(kps[2], pool_id, deposit_a, deposit_b) + get_spot_price() + + # kp1 takes all his/her shares out + balance = 0 + for b in server.accounts().account_id(kps[1].public_key).call()["balances"]: + if ( + b["asset_type"] == "liquidity_pool_shares" + and b["liquidity_pool_id"] == pool_id + ): + balance = Decimal(b["balance"]) + break + if not balance: + raise + remove_liquidity(kps[1], pool_id, balance) + get_spot_price() + +def get_spot_price(): + resp = server.liquidity_pools().liquidity_pool(pool_id).call() + amount_a = resp["reserves"][0]["amount"] + amount_b = resp["reserves"][1]["amount"] + spot_price = Decimal(amount_a) / Decimal(amount_b) + print(f"Price: {amount_a}/{amount_b} = {spot_price:.7f}") + +if __name__ == '__main__': + preamble() + main() +``` + + + +### Watching Liquidity Pool Activity + +You can access the transactions, operations, and effects related to a liquidity pool if you want to track its activity. Let's see how we can track the latest deposits in a pool (suppose `poolId` is defined as before): + + + +```js +server.operations() + .forLiquidityPool(poolId) + .call() + .then(ops => { + ops.records + .filter(op => op.type == "liquidity_pool_deposit") + .forEach(op => { + console.log("Reserves deposited:"); + op.reserves_deposited.forEach( + r => console.log(` ${r.amount} of ${r.asset}`)); + console.log(" for pool shares: ", op.shares_received); + }); + }); +``` + +```python +def watch_liquidity_pool_activity(): + for op in ( + server.operations() + .for_liquidity_pool(liquidity_pool_id=pool_id) + .cursor("now") + .stream() + ): + if op["type"] == "liquidity_pool_deposit": + print("Reserves deposited:") + for r in op["reserves_deposited"]: + print(f" {r['amount']} of {r['asset']}") + print(f" for pool shares: {op['shares_received']}") + # ... +``` + + diff --git a/docs/glossary/lumen-supply.mdx b/docs/glossary/lumen-supply.mdx new file mode 100644 index 000000000..1c48dc5da --- /dev/null +++ b/docs/glossary/lumen-supply.mdx @@ -0,0 +1,87 @@ +--- +title: Lumen Supply Metrics +order: +--- + +import { CodeExample } from "@site/src/components/CodeExample"; + +[SDF’s Dashboard API endpoint](https://dashboard.stellar.org/api/v2/lumens) will always have the live totals for the essential numbers around lumens. This guide explains important supply metrics like Original Supply, Total Supply, and Circulating Supply entailed in that data. + +## Dashboard API + +As of December 12, 2019, the Dashboard API shows: + + + +```json +{ + "updatedAt": "2019-12-12T21:07:23.480Z", + "originalSupply": "100000000000", + "inflationLumens": "5443902087.3472865", + "burnedLumens": "55442098181.3755700", + "totalSupply": "50001803905.9717165", + "upgradeReserve": "414310437.8111675", + "feePool": "1807341.5766261", + "sdfMandate": "29635032570.5297368", + "circulatingSupply": "19950653556.0541861", + "_details": "https://www.stellar.org/developers/guides/lumen-supply-metrics.html" +} +``` + + + +## Definitions + +### originalSupply + +One hundred billion lumens [were created](https://stellar.expert/explorer/public/ledger/2) when the Stellar network went live. That’s the Original Supply for the network. + +### inflationLumens + +For the first 5 or so years of Stellar’s existence, the supply of lumens [increased](./inflation.mdx) by 1% annually. This “network inflation” was ended by validator vote on October 28, 2019. The total number of lumens generated by inflation was 5,443,902,087.3472865. + +Adding this number to the Original Supply, you get the total lumens that have ever existed: 105,443,902,087.3472865. This number is visible on the [List All Ledgers](https://horizon.stellar.org/ledgers?order=desc) Horizon API endpoint as `_embedded.records.total_coins`. + +### burnedLumens + +These are all the lumens sent to accounts with no signers, meaning the funds are inaccessible and have been removed forever from Stellar’s lumen supply. + +While any address with no signers is counted here, the vast majority of the lumens in this sum are in a single locked address. On November 4, 2019, SDF [reduced](https://www.stellar.org/blog/sdfs-next-steps/) its lumen holdings to better reflect its mission and the growth of the Stellar ecosystem. To do so, the Foundation sent 55,442,095,285.7418 lumens to [GALA...LUTO](https://stellar.expert/explorer/public/account/GALAXYVOIDAOPZTDLHILAJQKCVVFMD4IKLXLSZV5YHO7VY74IWZILUTO). + +### totalSupply + +The Total Supply is the number of lumens now in existence: 50,001,803,905.97172. The Total Supply includes four major categories of lumens, which the API treats in detail: + +#### upgradeReserve + +The Upgrade Reserve is a special address that’s neither circulating nor a part of SDF’s mandate. When Stellar [changed its consensus algorithm](https://www.stellar.org/blog/upgraded-network-is-here/) in 2015 and relaunched the network these lumens were set aside, to be claimed, one-for-one, by holders of the old network tokens. The [Upgrade Reserve account](https://stellar.expert/explorer/public/account/GBEZOC5U4TVH7ZY5N3FLYHTCZSI6VFGTULG7PBITLF5ZEBPJXFT46YZM) is essentially an escrow, and we don’t expect many claimants to come and pull those lumens into the circulating supply at this point. + +#### feePool + +The Fee Pool is where network fees collect. The lumens do not belong to any particular account. No one has access to fee pool, so these lumens are non-circulating. Network validators could _theoretically_ vote for a protocol change that would affect the fee pool, so we include it in the total supply. Stellar’s transaction fees are extremely low–1/100,000th of a lumen per operation–so the fee pool grows very slowly. The Fee Pool is tracked by the protocol itself, and the current number is visible on the [List All Ledgers](https://horizon.stellar.org/ledgers?order=desc) Horizon API endpoint as `_embedded.records.fee_pool`. + +#### sdfMandate + +The SDF Mandate is described in detail [here](https://www.stellar.org/foundation/mandate). The Foundation was funded by lumens generated at Stellar’s inception; all of those lumens will eventually be spent or distributed to enhance and promote Stellar. Here is a complete list of the addresses currently associated with the SDF Mandate: + +| **Name** | +| --- | +| [Direct Development, Available Funds](https://stellar.expert/explorer/public/account/GB6NVEN5HSUBKMYCE5ZOWSK5K23TBWRUQLZY3KNMXUZ3AQ2ESC4MY4AQ) | +| [Jan 1 2021 Escrow](https://stellar.expert/explorer/public/account/GBA6XT7YBQOERXT656T74LYUVJ6MEIOC5EUETGAQNHQHEPUFPKCW5GYM) | +| [Jan 1 2022 Escrow](https://stellar.expert/explorer/public/account/GD2D6JG6D3V52ZMPIYSVHYFKVNIMXGYVLYJQ3HYHG5YDPGJ3DCRGPLTP) | +| [Jan 1 2023 Escrow](https://stellar.expert/explorer/public/account/GA2VRL65L3ZFEDDJ357RGI3MAOKPJZ2Z3IJTPSC24I4KDTNFSVEQURRA) | +| [Direct Development (Hot 1)](https://stellar.expert/explorer/public/account/GCEZYB47RSSSR6RMHQDTBWL4L6RY5CY2SPJU3QHP3YPB6ALPVRLPN7OQ) | +| [Direct Development (Hot 2)](https://stellar.expert/explorer/public/account/GATL3ETTZ3XDGFXX2ELPIKCZL7S5D2HY3VK4T7LRPD6DW5JOLAEZSZBA) | +| [Direct Development (Hot 3)](https://stellar.expert/explorer/public/account/GCVLWV5B3L3YE6DSCCMHLCK7QIB365NYOLQLW3ZKHI5XINNMRLJ6YHVX) | +| [Developer Support](https://stellar.expert/explorer/public/account/GCVJDBALC2RQFLD2HYGQGWNFZBCOD2CPOTN3LE7FWRZ44H2WRAVZLFCU) | +| [Developer Support (Hot)](https://stellar.expert/explorer/public/account/GCKJZ2YVECFGLUDJ5T7NZMJPPWERBNYHCXT2MZPXKELFHUSYQR5TVHJQ) | +| [Currency Support](https://stellar.expert/explorer/public/account/GAMGGUQKKJ637ILVDOSCT5X7HYSZDUPGXSUW67B2UKMG2HEN5TPWN3LQ) | +| [New Products](https://stellar.expert/explorer/public/account/GCPWKVQNLDPD4RNP5CAXME4BEDTKSSYRR4MMEL4KG65NEGCOGNJW7QI2) | +| [Enterprise Fund](https://stellar.expert/explorer/public/account/GDUY7J7A33TQWOSOQGDO776GGLM3UQERL4J3SPT56F6YS4ID7MLDERI4) | +| [Marketing Support](https://stellar.expert/explorer/public/account/GBEVKAYIPWC5AQT6D4N7FC3XGKRRBMPCAMTO3QZWMHHACLHTMAHAM2TP) | +| [In-App Distribution](https://stellar.expert/explorer/public/account/GDKIJJIKXLOM2NRMPNQZUUYK24ZPVFC6426GZAEP3KUK6KEJLACCWNMX) | +| [In-App Distribution (Hot)](https://stellar.expert/explorer/public/account/GAX3BRBNB5WTJ2GNEFFH7A4CZKT2FORYABDDBZR5FIIT3P7FLS2EFOZZ) | + +#### circulatingSupply + +The **Circulating Supply** is lumens in the hands of individuals and independent companies. These are lumens out in the world, used to pay network fees and fund Stellar accounts. They are also used as a general medium of exchange. We expect Stellar’s Circulating Supply to grow steadily as SDF spends and distributes lumens according to its mandate. Lumens in the Total Supply, but not in the SDF Mandate, Upgrade Reserve, or Fee Pool are assumed to be circulating. diff --git a/docs/glossary/metadata.json b/docs/glossary/metadata.json new file mode 100644 index 000000000..2e6f06970 --- /dev/null +++ b/docs/glossary/metadata.json @@ -0,0 +1,5 @@ +{ + "order": 90, + "title": "Glossary", + "sortMethod": "alphabetical" +} diff --git a/docs/glossary/minimum-balance.mdx b/docs/glossary/minimum-balance.mdx new file mode 100644 index 000000000..30cb27b8f --- /dev/null +++ b/docs/glossary/minimum-balance.mdx @@ -0,0 +1,44 @@ +--- +title: Minimum Balance +order: +--- + +import { CodeExample } from "@site/src/components/CodeExample"; +import { Alert } from "@site/src/components/Alert"; + + + +This doc explains minimum balance requirements. If you want to know about transaction fees, check out the [Fees](./fees.mdx) doc. + + + +All Stellar accounts must maintain a minimum balance of lumens. The minimum balance is calculated using the **base reserve,** which is currently **0.5 XLM**: + + + +``` +Minimum Balance = (2 + # of entries + # of sponsoring entries - # of sponsored entries) * base reserve +``` + + + +The absolute minimum balance for an account is 1 XLM, which is equal to `(2 + 0 entries) * 0.5 base reserve`. Each additional entry reserves an additional 0.5 XLM. Entries include: + +- Trustlines +- Offers +- Signers +- Data entries + +For example, an account with 1 trustline and 2 offers would have a minimum balance of `(2 + 3 entries) * 0.5 base reserve = 2.5 XLM`. + +[Sponsored Reserves](./sponsored-reserves.mdx) will affect the # of sponsoring entries and # of sponsored entries. + +[Claimable Balances](./claimable-balance.mdx) are reflected in the # of sponsoring entries. Each claimant in a claimable balance will require an additional 0.5 XLM. For example, an account that creates a claimable balance with 2 claimants would have a minimum balance of `(2 + 0 entries + 2 sponsoring entries) * 0.5 base reserve = 2 XLM`. + +Any transaction that would reduce an account's balance to less than the minimum will be rejected with an `INSUFFICIENT_BALANCE` error. Likewise, lumen selling liabilities that would reduce an account's balance to less than the minimum plus lumen selling liabilities will be rejected with an `INSUFFICIENT_BALANCE` error. + +The minimum balance is held in reserve, and closing an entry frees up the associated base reserve. For instance: if you zero-out a non-lumen balance and close the associated trustline, the 0.5 XLM base reserve that secured that trustline is added to your available balance. + +## Changes to Transaction Fees and Minimum Balances + +Validators can vote to change the base reserve — just as they can vote to change ledger limits and and the minimum base fee — but that's relatively uncommon. It should only happen every few years. For the most part, you can think of the base reserve as a fixed value. When it is changed, the change works by the same consensus process as any transaction. For details, see [versioning](./versioning.mdx). diff --git a/docs/glossary/miscellaneous-core-objects.mdx b/docs/glossary/miscellaneous-core-objects.mdx new file mode 100644 index 000000000..da569eb4b --- /dev/null +++ b/docs/glossary/miscellaneous-core-objects.mdx @@ -0,0 +1,16 @@ +--- +title: Miscellaneous Stellar Core Objects +order: +--- + +## LedgerKey + +LedgerKey holds information to identify a specific ledgerEntry. It is a union that can be any one of the LedgerEntryTypes (ACCOUNT, TRUSTLINE, OFFER, DATA, or CLAIMABLE_BALANCE). Search for LedgerKey in [stellar-ledger-entries.x](https://github.com/stellar/stellar-core/blob/master/src/protocol-curr/xdr/Stellar-ledger-entries.x) for more information. + +## OperationID + +OperationID is a union with one possible type (ENVELOPE_TYPE_OP_ID). It contains the transaction source account, sequence number, and the operation index of the CreateClaimableBalance operation in the transaction. Search for OperationID in [stellar-transactions.x](https://github.com/stellar/stellar-core/blob/master/src/protocol-curr/xdr/Stellar-transaction.x) for more information. + +## ClaimableBalanceID + +ClaimableBalanceID is a union with one possible type (CLAIMABLE_BALANCE_ID_TYPE_V0). It contains a SHA-256 hash of the OperationID for Claimable Balances. diff --git a/docs/glossary/multisig.mdx b/docs/glossary/multisig.mdx new file mode 100644 index 000000000..be8b8cbc3 --- /dev/null +++ b/docs/glossary/multisig.mdx @@ -0,0 +1,185 @@ +--- +title: Multisignature +order: +--- + +import { CodeExample } from "@site/src/components/CodeExample"; +import { Alert } from "@site/src/components/Alert"; + +## Transaction signatures + +Stellar uses **signatures** as authorization. Transactions always need authorization from at least one public key in order to be considered valid. Generally, transactions only need authorization from the public key of the source account. + +Transaction signatures are created by cryptographically signing the transaction object contents with a secret key. Stellar currently uses the ed25519 signature scheme, but there's also a mechanism for adding additional types of public/private key schemes. A transaction with an attached signature is considered to have authorization from that public key. + +In two cases, a transaction may need more than one signature. If the transaction has operations that affect more than one account, it will need authorization from every account in question. A transaction will also need additional signatures if the account associated with the transaction has multiple public keys. + +## Thresholds + +[Operations](./operations.mdx) fall under a specific threshold category: low, medium, or high. The threshold for a given level can be set to any number from 0-255. This threshold is the amount of signature weight required to authorize an operation at that level. + +Let's say Diyang sets the medium threshold on one of her accounts to 4. If that account submits a transaction that includes a payment operation (medium security), the transaction's threshold is 4, which means the signature weights on it need to be greater than or equal to 4 in order to run. If Diyang's master key — the key corresponding to the public key that identifies the account she owns — has a weight less than 4, she cannot authorize a transaction without other signers. + +Once the signature threshold is met, if there are any leftover signatures then the transaction will fail. This will happen even if the signatures are valid. For example, if your transaction requires 3 signatures, providing more than 3 signatures will result in a failed transaction with an `TX_BAD_AUTH_EXTRA` error (even if they are all valid). The reason for this error is performance related, as unnecessary signature verification has a large effect on performance before accepting transactions in consensus. + +Each account can set its own threshold values. By default all thresholds levels are set to 0, and the master key is set to weight 1. The [Set Options](../start/list-of-operations.mdx#set-options) operation allows you to change the weight of the master key and to add other signing keys with different weights. + +Low Security: + +- [Transaction processing](./transactions.mdx) + - Charging a fee or updating the sequence number for the source account +- [Allow Trust](../start/list-of-operations.mdx#allow-trust) and [Set Trust Line Flags](../start/list-of-operations.mdx#set-trustline-flags) operations + - Used to allow people to hold credit from this account without exposing the key that enables sending payments from this account. +- [Bump Sequence](../start/list-of-operations.mdx#bump-sequence) + - Modify the account's sequence number directly + +High Security: + +- [Set Options](../start/list-of-operations.mdx#set-options) to change the signers or the thresholds + - Allows you to create a set of signers that give or revoke access to the account. +- [Account Merge](../start/list-of-operations.mdx#account-merge) to merge accounts + +Medium Security: + +- All [other operations](../start/list-of-operations.mdx) + +For most cases, it is recommended to set thresholds such that `low <= medium <= high`. + +## Additional signing keys + +Accounts are identified by a public key. The private key that corresponds to this public key is called the **master key**. Additional signing keys can be added to the account using the [Set Options](../start/list-of-operations.mdx#set-options) operation. + +If the weight of the master key is updated to 0, it cannot be used to sign transactions (even for operations with a threshold value of 0), until restored by other signers meeting the high threshold. So be very, very careful if you're setting the master key weight to 0. If there are other signers listed on the account, they can still continue to sign transactions. + +"Signers" refers to the master key or to any signing keys added later. A signer is defined as the pair: public key, weight. + +Each additional signer beyond the master key increases the account's [minimum balance](./minimum-balance.mdx). + +## Extra Signers + +You can require that a transaction have a particular signer that isn't associated with any of the accounts used in the transaction by including it in the [transaction validity precondition](./transactions.mdx#extra-signers). The extra signer applies only to that one transaction. + +## Alternative Signer Types + +To enable some advanced smart contract features there are a couple of additional signer types. These signer types also have weights and can be added and removed similarly to the regular ed25519 signer. But rather than check a cryptographic signature for authorization they have a different method of proving validity to the network. + +### Pre-authorized Transaction + +It is possible for an account to pre-authorize a particular transaction by adding the hash of the future transaction as a "signer" on the account. To do that you need to prepare the transaction beforehand with proper sequence number. Then you can obtain the hash of this transaction and add it as signer to account. + +Signers of this type are automatically removed from the account when a matching transaction is applied, regardless of whether the transaction succeeds or fails. In case a matching transaction is never submitted, the signer remains and must be manually removed using the [Set Options](../start/list-of-operations.mdx#set-options) operation. + +This type of signer is especially useful in escrow accounts. You can pre-authorize two different transactions. Both could have the same sequence number but different destinations. This means that only one of them can be executed. + +### Hash(x) + +Adding a signer of type hash(x) allows anyone who knows `x` to sign the transaction. This type of signer is especially useful in [atomic cross-chain swaps](https://en.bitcoin.it/wiki/Atomic_cross-chain_trading) which are needed for inter-blockchain protocols like [lightning networks](https://lightning.network). + +First, create a random 256 bit value, which we call `x`. The SHA256 hash of that value can be added as a signer of type hash(x). Then in order to authorize a transaction, `x` is added as one of the signatures of the transaction. Keep in mind that `x` will be known to the world as soon as a transaction is submitted to the network with `x` as a signature. This means anyone will be able to sign for that account with the hash(x) signer at that point. Often you want there to be additional signers so someone must have a particular secret key and know `x` in order to reach the weight threshold required to authorize transactions on the account. + +### Signed Payloads + +Adding a signed payload as a signer means that transactions are only valid if they are accompanied by a signature of the signer's payload. The signature must be produced using the private key corresponding to the public key specified in the signer. + +More verbosely, you add a signer of `(public key, payload)`, where the public key is the Stellar address of the expected signer and the payload is any arbitrary data of up to 64 bytes. A valid signature, then, is a signature on that payload by the private key corresponding to the public key. This is also described in [CAP-40](https://stellar.org/protocol/cap-40) which introduced this new signer. + +Like with [hash signers](#hashx), once the signed payload signature is revealed to the world, anyone can use the signature to authorize transactions that are contingent on that signed payload signer. + +## Envelopes + +A transaction **envelope** wraps a transaction with a set of signatures. The transaction object is the thing that the signers are actually signing. Technically, a transaction envelope is the thing that is passed around the network and included in transaction sets. + +## Authorization + +To determine if a transaction has the necessary authorization to run, the weights of all the signatures in the transaction envelope are added up. If this sum is equal to or greater than the threshold (see below) set for that operation type, then the operation is authorized. + +This scheme is very flexible. You can require many signers to authorize payments from a particular account. You can have an account that any number of people can authorize for. You can have a master key that grants access or revokes access from others. It supports any m of n setup. + +## Operations + +### Example 1: Anchors + +> You run an anchor that would like to keep its issuing key offline. That way, it's less likely a bad actor can get ahold of the anchor's key and start issuing credit improperly. However, your anchor needs to authorize people holding credit by running the [`Set Trust Line Flags` operation](../start/list-of-operations.mdx#set-trustline-flags). Before you issue credit to an account, you need to verify that account is OK. + +Multisig allows you to do all of this without exposing the master key of your anchor. You can add another signing key to your account with the operation `Set Options`. This additional key should have a weight below your anchor account's medium threshold. Since `Set Trust Line Flags` is a low-threshold operation, this extra key authorizes users to hold your anchor's credit. But, since `Payment` is a medium-threshold operation, this key does not allow anyone who compromises your anchor to issue credit. + +Your account setup: + + + +``` + Master Key Weight: 2 + Additional Signing Key Weight: 1 + Low Threshold: 0 + Medium Threshold: 2 + High Threshold: 2 +``` + + + +### Example 2: Joint Accounts + +> You want to set up a joint account with Bilal and Carina such that any of you can authorize a payment. You also want to set up the account so that, if you choose to change signers (e.g., remove or add someone), a high-threshold operation, all 3 of you must agree. You add Bilal and Carina as signers to the joint account. You also ensure that it takes all of your key weights to clear the high threshold but only one to clear the medium threshold. + +Joint account setup: + + + +``` + Master Key Weight: 1 + Low Threshold: 0 + Medium Threshold: 0 + High Threshold: 3 + Bilal's Signing Key Weight: 1 + Carina's Signing Key Weight: 1 +``` + + + +### Example 3: Expense Accounts + +> You fully control an expense account, but you want your two coworkers Diyuan and Emil to be able to authorize transactions from this account. You add Diyuan and Emil's signing keys to the expense account. If either Diyuan or Emil leave the company, you can remove their signing key, a high-threshold operation. + +Expense account setup: + + + +``` + Master Key Weight: 3 + Low Threshold: 0 + Medium Threshold: 0 + High Threshold: 3 + Diyuan's Key Weight: 1 + Emil's Key Weight: 1 +``` + + + + + +The next example involves setting the master key weight of an account to 0. Be very, very careful if you decide to do that: that key will no longer be able to sign any kind of transaction, so you are in danger of _permanently_ locking yourself out of your account. Make sure you've thought carefully about what you're doing, that you understand the implications, and that you change weights in the correct order. + + + +### Example 4: Company Accounts + +> Your company wants to set up an account that requires 3 of 6 employees to agree to _any_ transaction from that account. + +Company account setup: + + + +``` + Master Key Weight: 0 (Turned off so this account can't do anything without an employee.) + Low Threshold: 3 + Medium Threshold: 3 + High Threshold: 3 + Employee 1 Key Weight: 1 + Employee 2 Key Weight: 1 + Employee 3 Key Weight: 1 + Employee 4 Key Weight: 1 + Employee 5 Key Weight: 1 + Employee 6 Key Weight: 1 +``` + + diff --git a/docs/glossary/muxed-accounts.mdx b/docs/glossary/muxed-accounts.mdx new file mode 100644 index 000000000..75850bd3b --- /dev/null +++ b/docs/glossary/muxed-accounts.mdx @@ -0,0 +1,349 @@ +--- +title: Muxed Accounts +order: +--- + +import { Alert } from "@site/src/components/Alert"; +import { CodeExample } from "@site/src/components/CodeExample"; + +A **muxed** (or _multiplexed_) **account** is an account that exists "virtually" under a traditional Stellar account address. It combines the familiar `GABC...` address with a 64-bit integer ID and can be used to distinguish multiple "virtual" accounts that share an underlying "real" account. + + + +**Warning**: _This feature is in active rollout._ +Muxed accounts are gaining adoption but may not be fully supported. Some wallets may not display a muxed account address and instead display the account ID address, ignoring the 64-bit ID. Continued adoption efforts are actively in progress. + + + +Muxed accounts were defined in [CAP-27](https://stellar.org/protocol/cap-27), introduced in Protocol 13, and their string representation is described in [SEP-23](https://stellar.org/protocol/sep-21). They have their own address format that starts with an `M` prefix. For example, from the account address `GA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVSGZ`, we can create new muxed accounts with two different IDs: + +- `MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUAAAAAAAAAAAACJUQ` has the ID `0`, while +- `MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUAAAAAAAAAABUTGI4` has the ID `420`. + +Both of these addresses, when used with one of the [supported operations][supported-ops], will act on the underlying `GA7Q...` address. + +With muxed accounts, we can _natively_ support the ecosystem-level abstraction of account memo requirements (see [SEP-29](https://stellar.org/protocol/sep-29)), where transaction memos were often used to distinguish between users backed by a single custodial account. Now, the embedded ID can be used for this distinction, and guides like [this one](../building-apps/setup-custodial-account.mdx) can be made superfluous. In other words, virtual accounts are now a first-class citizen on the network. + +There are many other benefits to embedding this abstraction into the protocol: + +- **"Share-ability"**: Rather than worrying about error-prone things like copy-pasting memo IDs, you can just share your `M...` address. +- **SDK support**: The various [SDKs](../software-and-sdks) support this abstraction natively, letting you create, manage, and work with muxed accounts easily. +- **Efficiency**: By combining related virtual accounts under a single account's umbrella, you can avoid holding reserves and paying fees for all of them to exist in the ledger individually. You can also combine multiple payments to multiple destinations within a single transaction, since you do not need the per-transaction memo field anymore. + + + +**Terminology Tip**: The term _muxing_ comes from computer networking and telecommunications, where multiple signals are combined into one signal over a shared medium. We are doing the same thing here with muxed accounts: combining multiple "virtual" accounts into a single shared account in the Stellar ledger. + + + +It's crucial to understand that this feature is intended to be a high-level abstraction, merely embedded into the protocol for convenience and standardization. There's no validation on IDs: **as far as the Stellar Network is concerned, all of the [supported operations][supported-ops] operate exactly as if you did _not_ used a muxed account**. For example, if you make two payments from two muxed accounts that share an underlying Stellar account (i.e. muxed accounts with the same underlying `G...` account but two different IDs), this is _exactly the same_ as that single Stellar account sending two payments, as far as the ledger is concerned. + +Even though only the underlying `G...` account _truly_ exists in the Stellar ledger, the [Horizon API](/api/introduction/) will make some effort to interpret and track the muxed accounts responsible for certain actions. + +Muxed account support is embedded into the SDKs. This means that you may see muxed addresses appear when parsing any of the fields that support them, so you should be ready to handle them. Refer to your SDK's documentation for details; for example, [v7.0.0](https://github.com/stellar/js-stellar-base/releases/tag/v7.0.0) of the JavaScript SDK library `stellar-base` describes all of the fields and functions that relate to muxed accounts. + +## Supported Operations + +Not all operations can be used with muxed accounts. For example, you cannot set the destination of a [`CreateAccount`](../start/list-of-operations.mdx#create-account) operation to be a muxed account, because only their shared, underlying `G...` account exists in the ledger. However, you can use them with: + +- the source account of _any_ [operation](../start/list-of-operations.mdx) or [transaction](./transactions.mdx); +- the fee source of a [fee-bump transaction](./fee-bumps.mdx); +- the destination of all three types of payments: + - [`Payment`](../start/list-of-operations.mdx#payment), + - [`PathPaymentStrictSend`](../start/list-of-operations.mdx#path-payment-strict-send), and + - [`PathPaymentStrictReceive`](../start/list-of-operations.mdx#path-payment-strict-receive); +- the destination of an [`AccountMerge`](../start/list-of-operations.mdx#account-merge); and +- the target of a [`Clawback`](../start/list-of-operations.mdx#clawback) operation (i.e. the `from` field). + +We'll demonstrate some of these in the [Examples](#examples). + +## Examples + +In this section, we'll demonstrate how to create muxed accounts and how seamlessly they interface with their supported operations. To drive home the fact that custodial account workarounds based on transaction memos (as in [this tutorial](../building-apps/setup-custodial-account.mdx)) are superfluous now, we'll use that as a skeleton for our example structure. + +After preparing some supporting code, we'll demonstrate three scenarios: + +- [normal](#payments), "full" Stellar account payments (i.e. G to G), +- [mixed](#muxed-to-unmuxed) payments (i.e. M to G), and +- [fully muxed](#muxed-to-muxed) payments (i.e. M to M) + +but use a shared function for all of them that does the real work, highlighting the ease of implementing muxed account support. + + + +**Warning**: In the following code samples, proper error checking is omitted for brevity. However, you should always validate your results, as there are many ways that requests can fail. You can refer to the guide on [Handling Errors Gracefully](../tutorials/handling-errors.mdx) for tips on error management strategies. + + + +### Preamble + +First, let's create two accounts and then a handful of virtual accounts representing "custodial customers" that the parent account manages: + + + +```js +const sdk = require("stellar-sdk"); + +const passphrase = "Test SDF Network ; September 2015"; +const url = "https://horizon-testnet.stellar.org"; +let server = new sdk.Server(url); + +const custodian = sdk.Keypair.fromSecret("SAQLZCQA6AYUXK6JSKVPJ2MZ5K5IIABJOEQIG4RVBHX4PG2KMRKWXCHJ"); +const outsider = sdk.Keypair.fromSecret("SAAY2H7SANIS3JLFBFPLJRTYNLUYH4UTROIKRVFI4FEYV4LDW5Y7HDZ4"); + +async function preamble() { + [ custodianAcc, outsiderAcc ] = await Promise.all([ + server.loadAccount(custodian.publicKey()), + server.loadAccount(outsider.publicKey()), + ]); + + customers = ["1", "22", "333", "4444"].map( + (id) => new sdk.MuxedAccount(custodianAcc, id) + ); + + console.log("Custodian:\n ", custodian.publicKey()); + console.log("Customers:") + customers.forEach((customer) => { + console.log(" " + customer.id().padStart(4, ' ') + ":", + customer.accountId()); + }); + console.log(); +} +``` + + + +We assume that these accounts exist on the testnet; you can replace them with your own keys and use [friendbot](../tutorials/create-account.mdx#create-account) if you'd like. + +When we run this function, we'll see the similarity in muxed account addresses among the customers, highlighting the fact that they share a public key: + + + +``` +Custodian: + GCIHAQVWZH2AB5BB5NP63FBSIREG77LQZZNUVKD2LN2IOCLOT6N72MJN +Customers: + 1: MCIHAQVWZH2AB5BB5NP63FBSIREG77LQZZNUVKD2LN2IOCLOT6N72AAAAAAAAAAAAEDB4 + 22: MCIHAQVWZH2AB5BB5NP63FBSIREG77LQZZNUVKD2LN2IOCLOT6N72AAAAAAAAAAAC3IHY + 333: MCIHAQVWZH2AB5BB5NP63FBSIREG77LQZZNUVKD2LN2IOCLOT6N72AAAAAAAAAABJV72I + 4444: MCIHAQVWZH2AB5BB5NP63FBSIREG77LQZZNUVKD2LN2IOCLOT6N72AAAAAAAAAARLQOKK +``` + + + +With the accounts out of the way, let's look at how we can manage the difference between "real" Stellar accounts (`G...`) and these "virtual" muxed accounts (`M...`). + +### Muxed Operations Model + +The introduction of muxed addresses as a higher-level abstraction--and their experimental, opt-in nature--means there are mildly diverging branches of code depending on whether the source is a muxed account or not. We still need to, for example, load accounts by their underlying address, because the muxed versions don't actually live on the Stellar ledger: + + + +```js +function loadAccount(account) { + if (StellarSdk.StrKey.isValidMed25519Address(account.accountId())) { + return loadAccount(account.baseAccount()); + } else { + return server.loadAccount(account.accountId()); + } +} + +function showBalance(acc) { + console.log(`${acc.accountId().substring(0, 5)}: ${acc.balances[0].balance}`); +} +``` + + + +For payments--our focus for this set of examples--the divergence only matters because we want to show the balances for the custodian account. + +### Payments + +The actual code to build payments is almost exactly the same as it would be without the muxed situation: + + + +```js +function doPayment(source, dest) { + return loadAccount(source) + .then((accountBeforePayment) => { + showBalance(accountBeforePayment); + + let payment = sdk.Operation.payment({ + source: source.accountId(), + destination: dest.accountId(), + asset: sdk.Asset.native(), + amount: "10" + }); + + let tx = new sdk.TransactionBuilder(accountBeforePayment, { + networkPassphrase: StellarSdk.Networks.TESTNET, + fee: StellarSdk.BASE_FEE, + }) + .addOperation(payment) + .setTimeout(30) + .build(); + + tx.sign(custodian); + return server.submitTransaction(tx); + }) + .then(() => loadAccount(source)) + .then(showBalance); +} +``` + + + +We can use this block to make a payment between normal Stellar accounts with ease: `doPayment("GCIHA...", "GDS5N...")`. The main divergence from the [standard payments code](../tutorials/send-and-receive-payments.mdx#send-a-payment)--aside from the stubs to show XLM balances before and after--is the inclusion of the opt-in `withMuxing` flag. + +#### Muxed to Unmuxed + +The codeblock above covers all payment operations, abstracting away any need for differentiating between muxed (`M...`) and unmuxed (`G...`) addresses. From a high level, then, it's still trivial to make payments between one of our "customers" and someone outside of the "custodian"'s organization: + + + +```js +preamble + .then(() => { + const src = customers[0]; + console.log(`Sending 10 XLM from Customer ${src.id()} to ${outsiderAcc.accountId().substring(0, 5)}.`) + return doPayment(src, outsiderAcc); + }); +``` + + + +Notice that we still sign the transaction with the `custodian` keys, because **muxed accounts have no concept of a secret key**. Ultimately, everything still goes through the "parent" account, and so we should see the parent account's balance decrease by 10 XLM accordingly: + + + +``` +Sending 10 XLM from Customer 1 to GDS5N. +GCIHA: 9519.9997700 XLM +GCIHA: 9509.9997600 XLM +``` + + + +Of course, there's also a fee charged for the transaction itself. + +#### Muxed to Muxed + +As we've mentioned, muxed account actions aren't represented in the Stellar ledger explicitly. When two muxed accounts sharing an underlying Stellar account communicate, it's as if the underlying account is talking to itself. A payment between two such accounts, then, is essentially a no-op. + + + +```js +preamble() + .then(() => { + const [ src, dst ] = customers.slice(0, 2); + console.log(`Sending 10 XLM from Customer ${src.id()} to Customer ${dst.id()}.`) + return doPayment(src, dst); + }); +``` + + + +The output should be something like the following: + + + +``` +Sending 10 XLM from Customer 1 to Customer 22. +GCIHA: 9579.9999800 XLM +GCIHA: 9579.9999700 XLM +``` + + + +Notice that the account's balance is essentially unchanged, yet it was charged a fee since this transaction is still recorded in the ledger (despite doing next to nothing). You may want to detect these types of transactions in your application to avoid paying unnecessary transaction fees. + +If we were to make a payment between two muxed accounts that had _different_ underlying Stellar accounts, this would be equivalent to a payment between those two respective `G...` accounts. + +### More Examples + +As is the case for most protocol-level features, you can find more usage examples and inspiration in the relevant test suite for your favorite SDK. For example, [here](https://github.com/stellar/js-stellar-base/blob/master/test/unit/muxed_account_test.js) are some of the JavaScript test cases. + +## Frequently-Asked Questions + +The different stages of the rollout plan necessitate careful consideration of edge cases. For example, what happens if you send money to a muxed address (`M...`), but the recipient's platform only supports regular addresses (`G...`)? The list below aims to identify and alleviate some of these. + +In all cases, the action specified by the operation (be it a payment, clawback, etc.) will act as though the muxed address is the underlying account ID. Muxed accounts are not part of the network, so if your transaction succeeds, the operation will succeed. In the case of payments, then, **assets will always transfer**, regardless of sender/receiver support for muxed accounts. + +### What happens if I send to the wrong address? + +There are a number of ways to send data incorrectly, so these are grouped into a subcategory. Below, the "recipient" is defined as be the _platform_ that the destination account uses to interact with the Stellar network. + +#### What happens if I pay a muxed address, but the recipient doesn't support them? + +In general, you should not send payments to muxed addresses on platforms that do not support them. These platforms will not be able to provide muxed destination addresses in the first place. + +Even still, if this does occur, parsing a transaction with a muxed parameter without handling them will lead to one of two things occurring: + +- If your SDK is out-of-date, parsing will error out. You should upgrade your SDK. For example, the JavaScript SDK will throw a helpful message: + +> destination is invalid; did you forget to enable muxing? + +- If your SDK is up-to-date, you will see the muxed (`M...`) address parsed out. What happens next depends on your application. + +Note, however, that the **operation will succeed** on the network. In the case of payments, for example, the destination's _parent address_ will still receive the funds. + +#### What happens if I want to pay a muxed account, but my platform does not support them? + +In this case, do not use a muxed address. The platform will likely fail to create the operation. You probably want to use the legacy method of including a transaction memo, instead. + +### What do I do if I receive a transaction with muxed addresses _and_ a memo ID? + +In an ideal world, this situation would never happen. You can determine whether or not the underlying IDs are equal; if they aren't, this is a malformed transaction and we recommend not submitting it to the network. + +### What happens if I get errors when using muxed accounts? + +In up-to-date versions of Stellar SDKs, muxed accounts are natively supported by default. If you are using an older version of an SDK, however, they may still be hidden behind a feature flag. + +If you get errors when using muxed addresses on [supported operations][supported-ops] like: + +> destination is invalid; did you enable muxing? + +We recommend upgrading to the latest version of any and all Stellar SDKs you use. However, if that's not possible for some reason, you will need to enable the feature flag before interacting with muxed accounts. Consult your SDK's documentation for details. + +### What happens if I pass a muxed address to an incompatible operation? + +Only certain operations allow muxed accounts, as described [above][supported-ops]. Passing a muxed address to an incompatible parameter with an up-to-date SDK _should_ result in a compilation or runtime error at the time of use. + +For example, when using the JavaScript SDK incorrectly: + + + +```js + const mAddress = "MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUAAAAAAAAAABUTGI4"; + transactionBuilder.addOperation( + Operation.setTrustLineFlags({ + trustor: mAddress, // wrong! + asset: someAsset, + flags: { clawbackEnabled: false } + }) + ); +``` + + + +the runtime result would be: + +> Error: invalid version byte. expected 48, got 96 + +This error message indicates that the `trustor` failed to parse as a Stellar account ID (`G...`). In other words, your code will fail and the invalid operation will never reach the network. + +### How do I validate Stellar addresses? + +You should use the validation methods provided by your SDK or carefully adhere to [SEP-23](https://stellar.org/protocol/sep-23). For example, the JavaScript SDK provides the following methods for validating Stellar addresses: + +```typescript +namespace StrKey { + function isValidEd25519PublicKey(publicKey: string): boolean; + function isValidMed25519PublicKey(publicKey: string): boolean; +} +``` + +There are also abstractions for constructing and managing both muxed and regular accounts; consult your SDK documentation for details. + +[supported-ops]: #supported-operations diff --git a/docs/glossary/network-passphrase.mdx b/docs/glossary/network-passphrase.mdx new file mode 100644 index 000000000..72c5a8428 --- /dev/null +++ b/docs/glossary/network-passphrase.mdx @@ -0,0 +1,28 @@ +--- +title: Network Passphrase +order: +--- + +Stellar has two public networks: the Public Network (pubnet), which is the main network used by applications in production, and the Test Network ([testnet](./testnet.mdx)), which is a network maintained by the Stellar Development Foundation that developers can use to test their Applications. + +Each Stellar network has its own unique passphrase, which is used when validating signatures on a given transaction. If you sign a transaction for one network but submit it to another, it won't be considered valid. By convention, the format of a passphrase is `'[Network Name] ; [Month of Creation] [Year of Creation]'`. + +The current passphrases for the Stellar pubnet and testnet are: + +- Pubnet: `'Public Global Stellar Network ; September 2015'` +- Testnet: `'Test SDF Network ; September 2015'` + +The passphrase serves two main purposes: + +- It is used as the seed for the root account (master network key) at genesis. +- It is used to build hashes of transactions, which are ultimately what is signed by each signer's secret key in a transaction envelope. Again, this allows you to verify that a transaction was intended for a specific network by its signers. + +Most SDKs have the passphrases hardcoded for the Stellar pubnet and testnet, but if you're running a private network, you'll need to manually pass in a passphrase to be used whenever transaction hashes are generated. All of Stellar's official SDKs give you the ability to use a network with a custom passphrase. + +## Moving To Production + +When creating your application on top of the Stellar network, we recommend starting on the testnet, and migrate to pubnet after rigorous testing has proved it to be production ready (_we are talking about money here_). + +For applications that don't rely on the state of a network (such as specific accounts needing to exist), moving to production is as simple as changing the network passphrase and ensuring your Horizon instance is connected to pubnet. + +If you've been running a stellar-core or Horizon instance against the test network, and want to switch to production, changing the passphrase will require both respective databases to be completely reinitialized. diff --git a/docs/glossary/operations.mdx b/docs/glossary/operations.mdx new file mode 100644 index 000000000..e558eaa54 --- /dev/null +++ b/docs/glossary/operations.mdx @@ -0,0 +1,46 @@ +--- +title: Operations +order: +--- + +Operations are the bread and butter of Stellar: they’re the individual commands that mutate the ledger. Transactions, which accounts sign and submit for inclusion in the ledger, are really just bundles of operations. Transactions can, by definition, include anywhere from 1 to 100 operations. + +Network capacity, which is determined by validator vote, is measured in terms of operations/ledger. Currently, it’s set to 1,000. + +There are thirteen possible operation types, each of which is detailed along with parameters, errors, and links to SDK docs in the [List of Operations](../start/list-of-operations.mdx). + +Operations are executed on behalf of the source account specified in the transaction, unless there is an override defined for the operation. + +## Thresholds + +Each operation falls under a specific threshold category: low, medium, or high. Thresholds define the level of privilege an operation needs in order to succeed. + +- Low Security: + - `AllowTrust` + - `SetTrustLineFlags` + - `BumpSequence` + - `ClaimClaimableBalance` +- Medium Security: + - Everything Else (`Payment`, `ChangeTrust`, etc.). +- High Security: + - `AccountMerge` + - `SetOptions` (only when changing signers and the thresholds for each category). + +## Validity of an Operation + +When a transaction is submitted to a node, the node checks the validity of each operation in the transaction before attempting to include it in a candidate transaction set. These initial operation validity checks are intended to be fast and simple: more intensive checks come later, after fees have been consumed. For an operation to pass this first validity check, it has to meet the following conditions: + +1. The signatures on the transaction must be valid for the operation. That means: + - The signatures are from valid signers for the source account of the operation. + - The combined weight of all signatures for the source account _of the operation_ meets the threshold for the operation. +1. The operation itself must be well formed. Typically this means checking the parameters for the operation to see if they're in a valid format. + - For example, only positive values can be set for the amount of a payment operation. +1. The operation must be valid in the current protocol version of the network. Deprecated operations, such as inflation, are invalid by design. + +For more details on this process, see the [lifecycle of a transaction](./transactions.mdx#transaction-lifecycle). + +## Result + +For each operation, there is a matching result type. In the case of success, this result allows users to gather information about the effects of the operation. In the case of failure, it allows users to learn more about the error. + +There are some generic errors associated with operations. For example, any operation that creates a subentry, such as `ChangeTrust` and `ManageData`, can fail with `opTOO_MANY_SUBENTRIES`. See [operations](/api/errors/result-codes/operations/) in the API reference for more information. diff --git a/docs/glossary/scp.mdx b/docs/glossary/scp.mdx new file mode 100644 index 000000000..d1f753479 --- /dev/null +++ b/docs/glossary/scp.mdx @@ -0,0 +1,16 @@ +--- +title: Stellar Consensus Protocol +order: +--- + +The Stellar Consensus Protocol (SCP) provides a way to reach consensus without relying on a closed system to accurately record financial transactions. SCP has a set of provable safety properties that optimize for safety over liveness—in the event of partition or misbehaving nodes, it halts progress of the network until consensus can be reached. SCP simultaneously enjoys four key properties: decentralized control, low latency, flexible trust, and asymptotic security. + +A few ways to explore SCP: + +- Start out with the peer-reviewed [paper in SOSP 2019](https://www.stellar.org/papers/fast-and-secure-global-payments-with-stellar) on Stellar for a technical overview. +- To learn all about the Stellar Consensus Protocol, read the original [white paper](https://www.stellar.org/papers/stellar-consensus-protocol). +- A [Simplified SCP](http://www.scs.stanford.edu/~dm/blog/simplified-scp.html) description is shorter and easier to read than the full whitepaper. +- If you are implementing SCP, see the [internet draft](https://datatracker.ietf.org/doc/draft-mazieres-dinrg-scp/) and send questions and feedback to the [DIN mailing list](https://www.ietf.org/mailman/listinfo/Din). +- View or contribute to the code in [stellar-core](https://github.com/stellar/stellar-core). +- If white papers aren't your thing, here's a [technical summary](https://medium.com/a-stellar-journey/on-worldwide-consensus-359e9eb3e949). +- To learn about the SCP voting process, read this [blog post](https://stellar.org/developers-blog/intuitive-stellar-consensus-protocol) or watch this [talk](https://www.youtube.com/watch?v=fDt8Eh4T_lE&list=PLmr3tp_7-7GgDUgWJTbp7jhhgdMccVdhs). diff --git a/docs/glossary/sponsored-reserves.mdx b/docs/glossary/sponsored-reserves.mdx new file mode 100644 index 000000000..12ebb256a --- /dev/null +++ b/docs/glossary/sponsored-reserves.mdx @@ -0,0 +1,525 @@ +--- +title: Sponsored Reserves +order: +--- + +import { CodeExample } from "@site/src/components/CodeExample"; +import { Alert } from "@site/src/components/Alert"; + +Protocol 15 introduces operations that allow an account to pay the base reserves for another account. This is done by using the [Begin Sponsoring Future Reserves](../start/list-of-operations.mdx#begin-sponsoring-future-reserves) and [End Sponsoring Future Reserves](../start/list-of-operations.mdx#end-sponsoring-future-reserves) operations. + +The sponsoring account establishes the is-sponsoring-future-reserves-for relationship, and the sponsored account terminates it. While this relationship exists, reserve requirements that would normally accumulate on the sponsored account will now accumulate on the sponsoring account. Both operations must appear in a single transaction, which guarantees that both the sponsoring and sponsored accounts agree to every sponsorship. + +## Sponsorship effect on Minimum Balance + +The [Minimum Balance](./minimum-balance.mdx) calculation once sponsorships are introduced becomes `(2 + numSubEntries + numSponsoring - numSponsored) * baseReserve + liabilities.selling`. + +When account `A` is-sponsoring-future-reserves-for account `B`, any reserve requirements that would normally accumulate on `B` will instead accumulate on `A` as reflected in `numSponsoring`. The fact that these reserves are being provided by another account will be reflected on `B` in `numSponsored`, which will cancel out the increase in `numSubEntries`, keeping the minimum balance unchanged for `B`. + +When a sponsored ledger entry or sub-entry is removed, `numSponsoring` is decreased on the sponsoring account and `numSponsored` is decreased on the sponsored account. + +## What can be sponsored? + +Anything that increases the minimum balance can be sponsored (Accounts, Offers, Trustlines, AccountData, and Signers). + +## Claimable Balances + +[Claimable Balances](./claimable-balance.mdx) are unique in that they must be sponsored. They are not sub-entries of an account, so the sponsoring account uses the sponsorship mechanism to pay the base reserve by increasing `numSponsoring`. The sponsorship logic is handled through the Claimable Balance operations, so the use of sponsorships is transparent to the user. + +## Relevant operations + +### Begin and end sponsorships + +[Begin Sponsoring Future Reserves](../start/list-of-operations.mdx#begin-sponsoring-future-reserves) will establish the is-sponsoring-future-reserves-for relationship where the sponsoring account is the source account of the operation, and the account specified in the operation is the sponsored account. + +[End Sponsoring Future Reserves](../start/list-of-operations.mdx#end-sponsoring-future-reserves) will end the current is-sponsoring-future-reserves-for relationship for the source account of the operation. + +At the end of any transaction, there must be no ongoing is-sponsoring-future-reserves-for relationships. This is why these two operations must be used together in a single transaction. + +### Revoke Sponsorship + +[Revoke Sponsorship](../start/list-of-operations.mdx#revoke-sponsorship) is the third and final operation relevant to sponsorships. It allows the sponsoring account to remove/transfer sponsorships of existing ledgerEntries and signers. If the ledgerEntry/signer is not sponsored, the owner of the ledgerEntry/signer can establish a sponsorship if it is the beneficiary of a is-sponsoring-future-reserves-for relationship. + +See [Revoke Sponsorship](../start/list-of-operations.mdx#revoke-sponsorship) for more information about the structure of this operation object. + +#### Operation logic + +- Entry/signer is sponsored + - Source account is currently the beneficiary of a is-sponsoring-future-reserves-for relationship + - Transfer sponsorship of entry/signer from source account to the account that is-sponsoring-future-reserves-for source account + - Source account is not the beneficiary of a is-sponsoring-future-reserves-for relationship + - Remove the sponsorship from the entry/signer +- Entry/signer is not sponsored + - Source account is currently the beneficiary of a is-sponsoring-future-reserves-for relationship + - Establish sponsorship between entry/signer and the account that is-sponsoring-future-reserves-for source account + - Source account is not the beneficiary of a is-sponsoring-future-reserves-for relationship + - No-Op + +#### Errors + +The logic above does not detail any of the error cases, which are specified [here](../start/list-of-operations.mdx#revoke-sponsorship). + +## Examples + +Each example builds on itself, referencing variables from previous snippets. We'll demonstrate a few different things you can do with sponsoring: + +- [Sponsor creation](#sponsoring-trustlines) of a trustline for another account. +- [Sponsor **two** trustlines](#sponsoring-trustlines) for an account via two _different_ sponsors. +- [Transfer sponsorship](#transferring-sponsorship) responsibility from one account to another. +- [Revoke sponsorship](#sponsorship-revocation) by an account entirely. + +(For brevity in the Golang examples, we'll assume the existence of a `SignAndSend(...)` method (defined [below](#footnote)) which creates and submits a transaction with the proper parameters and basic error-checking. + + + +In the following code samples, proper error checking is omitted for brevity. However, you should _always_ validate your results, as there are many ways that requests can fail. You should refer to the guide on [Handling Errors Gracefully](../tutorials/handling-errors.mdx) for tips on error management strategies. + + + +### Preamble + +We'll start by including the boilerplate of account and asset creation. + + + +```js +const sdk = require("stellar-sdk"); +const http = require("got"); + +let server = new sdk.Server("https://horizon-testnet.stellar.org"); + +async function main() { + // Create & fund the new accounts. + let keypairs = [ + sdk.Keypair.random(), + sdk.Keypair.random(), + sdk.Keypair.random(), + ]; + + for (const keypair of keypairs) { + const base = "https://friendbot.stellar.org/?"; + const path = base + "addr=" + encodeURIComponent(keypair.publicKey()); + + console.log(`Funding:\n ${keypair.secret()}\n ${keypair.publicKey()}`); + + // We use the "got" library here to do the HTTP request synchronously, but + // you can obviously use any method you'd like for this. + const response = await http(path).catch(function (error) { + console.error(" failed:", error.response.body); + }); + } + + // Arbitrary assets to sponsor trustlines for. Let's assume they make sense. + let S1 = keypairs[0], A = keypairs[1], S2 = keypairs[2]; + let assets = [ + new sdk.Asset("ABCD", S1.publicKey()), + new sdk.Asset("EFGH", S1.publicKey()), + new sdk.Asset("IJKL", S2.publicKey()), + ]; + + // ... +``` + +```go +package main + +import ( + "fmt" + "net/http" + + sdk "github.com/stellar/go/clients/horizonclient" + "github.com/stellar/go/keypair" + "github.com/stellar/go/network" + protocol "github.com/stellar/go/protocols/horizon" + "github.com/stellar/go/txnbuild" +) + +func main() { + client := sdk.DefaultTestNetClient + + // Both S1 and S2 will be sponsors for A at various points in time. + S1, A, S2 := keypair.MustRandom(), keypair.MustRandom(), keypair.MustRandom() + addressA := A.Address() + + for _, pair := range []*keypair.Full{S1, A, S2} { + resp, err := http.Get("https://friendbot.stellar.org/?addr=" + pair.Address()) + check(err) + resp.Body.Close() + fmt.Println("Funded", pair.Address()) + } + + // Load the corresponding account for both A and C. + s1Account, err := client.AccountDetail(sdk.AccountRequest{AccountID: S1.Address()}) + check(err) + aAccount, err := client.AccountDetail(sdk.AccountRequest{AccountID: addressA}) + check(err) + s2Account, err := client.AccountDetail(sdk.AccountRequest{AccountID: S2.Address()}) + check(err) + + // Arbitrary assets to sponsor trustlines for. Let's assume they make sense. + assets := []txnbuild.CreditAsset{ + txnbuild.CreditAsset{Code: "ABCD", Issuer: S1.Address()}, + txnbuild.CreditAsset{Code: "EFGH", Issuer: S1.Address()}, + txnbuild.CreditAsset{Code: "IJKL", Issuer: S2.Address()}, + } + + // ... +``` + + + +### Sponsoring Trustlines + +Now, let's sponsor trustlines for Account A. Notice how the `CHANGE_TRUST` operation is sandwiched between the begin and end sponsoring operations and that all relevant accounts need to sign the transaction. + + + +```js + // + // 1. S1 will sponsor a trustline for Account A. + // + let s1Account = await server.loadAccount(S1.publicKey()).catch(accountFail); + let tx = new sdk.TransactionBuilder(s1Account, {fee: sdk.BASE_FEE}) + .addOperation(sdk.Operation.beginSponsoringFutureReserves({ + sponsoredId: A.publicKey(), + })) + .addOperation(sdk.Operation.changeTrust({ + source: A.publicKey(), + asset: assets[0], + limit: "1000", // This limit can vary according with your application; + // if left empty, it defaults to the max limit. + })) + .addOperation(sdk.Operation.endSponsoringFutureReserves({ + source: A.publicKey(), + })) + .setNetworkPassphrase(sdk.Networks.TESTNET) + .setTimeout(180) + .build(); + + // Note that while either can submit this transaction, both must sign it. + tx.sign(S1, A); + let txResponse = await server.submitTransaction(tx).catch(txCheck); + if (!txResponse) { return; } + + console.log("Sponsored a trustline of", A.publicKey()); + + // + // 2. Both S1 and S2 sponsor trustlines for Account A for different assets. + // + let aAccount = await server.loadAccount(A.publicKey()).catch(accountFail); + let tx = new sdk.TransactionBuilder(aAccount, {fee: sdk.BASE_FEE}) + .addOperation(sdk.Operation.beginSponsoringFutureReserves({ + source: S1.publicKey(), + sponsoredId: A.publicKey() + })) + .addOperation(sdk.Operation.changeTrust({ + asset: assets[1], + limit: "5000" + })) + .addOperation(sdk.Operation.endSponsoringFutureReserves()) + + .addOperation(sdk.Operation.beginSponsoringFutureReserves({ + source: S2.publicKey(), + sponsoredId: A.publicKey() + })) + .addOperation(sdk.Operation.changeTrust({ + asset: assets[2], + limit: "2500" + })) + .addOperation(sdk.Operation.endSponsoringFutureReserves()) + .setNetworkPassphrase(sdk.Networks.TESTNET) + .setTimeout(180) + .build(); + + // Note that all 3 accounts must approve/sign this transaction. + tx.sign(S1, S2, A); + let txResponse = await server.submitTransaction(tx).catch(txCheck); + if (!txResponse) { return; } + + console.log("Sponsored two trustlines of", A.publicKey()); +``` + +```go + // + // 1. S1 will sponsor a trustline for Account A. + // + sponsorTrustline := []txnbuild.Operation{ + &txnbuild.BeginSponsoringFutureReserves{ + SourceAccount: s1Account.AccountID, + SponsoredID: addressA, + }, + &txnbuild.ChangeTrust{ + Line: &assets[0], + Limit: txnbuild.MaxTrustlineLimit, + }, + &txnbuild.EndSponsoringFutureReserves{}, + } + + // Note that while A can submit this transaction, both sign it. + SignAndSend(client, aAccount.AccountID, []*keypair.Full{S1, A}, sponsorTrustline...) + fmt.Println("Sponsored a trustline of", A.Address()) + + // + // 2. Both S1 and S2 sponsor trustlines for Account A for different assets. + // + sponsorTrustline = []txnbuild.Operation{ + &txnbuild.BeginSponsoringFutureReserves{ + SourceAccount: s1Account.AccountID, + SponsoredID: addressA, + }, + &txnbuild.ChangeTrust{ + Line: &assets[1], + Limit: txnbuild.MaxTrustlineLimit, + }, + &txnbuild.EndSponsoringFutureReserves{}, + + &txnbuild.BeginSponsoringFutureReserves{ + SourceAccount: s2Account.AccountID, + SponsoredID: addressA, + }, + &txnbuild.ChangeTrust{ + Line: &assets[2], + Limit: txnbuild.MaxTrustlineLimit, + }, + &txnbuild.EndSponsoringFutureReserves{}, + } + + // Note that all 3 accounts must approve/sign this transaction. + SignAndSend(client, aAccount.AccountID, []*keypair.Full{S1, S2, A}, sponsorTrustline...) + fmt.Println("Sponsored two trustlines of", A.Address()) +``` + + + +### Transferring Sponsorship + +Suppose that now Signer 1 wants to transfer responsibility of sponsoring reserves for the trustline to Sponsor 2. This is accomplished by sandwiching the transfer between the `BEGIN`/`END_SPONSORING_FUTURE_RESERVES` operations. Both of the participants must sign the transaction, though either can submit it. + +An intuitive way to think of a sponsorship transfer is that the very act of sponsorship is being sponsored by a new account. That is, the new sponsor takes over the responsibilities of the old sponsor by sponsoring a revocation. + + + +```js + // + // 3. Transfer sponsorship of B's second trustline from S1 to S2. + // + let tx = new sdk.TransactionBuilder(s1Account, {fee: sdk.BASE_FEE}) + .addOperation(sdk.Operation.beginSponsoringFutureReserves({ + source: S2.publicKey(), + sponsoredId: S1.publicKey() + })) + .addOperation(sdk.Operation.revokeTrustlineSponsorship({ + account: A.publicKey(), + asset: assets[1], + })) + .addOperation(sdk.Operation.endSponsoringFutureReserves()) + .setNetworkPassphrase(sdk.Networks.TESTNET) + .setTimeout(180) + .build(); + + // Notice that while the old sponsor *sends* the transaction, both sponsors + // must *approve* the transfer. + tx.sign(S1, S2); + let txResponse = await server.submitTransaction(tx).catch(txCheck); + if (!txResponse) { return; } + + console.log("Transferred sponsorship for", A.publicKey()); +``` + +```go + // + // 3. Transfer sponsorship of B's second trustline from S1 to S2. + // + transferOps := []txnbuild.Operation{ + &txnbuild.BeginSponsoringFutureReserves{ + SourceAccount: s2Account.AccountID, + SponsoredID: S1.Address(), + }, + &txnbuild.RevokeSponsorship{ + SponsorshipType: txnbuild.RevokeSponsorshipTypeTrustLine, + Account: &addressA, + TrustLine: &txnbuild.TrustLineID{ + Account: addressA, + Asset: assets[1], + }, + }, + &txnbuild.EndSponsoringFutureReserves{}, + } + + // Notice that while the old sponsor *sends* the transaction (in this case), + // both sponsors must *approve* the transfer. + SignAndSend(client, s1Account.AccountID, []*keypair.Full{S1, S2}, transferOps...) + fmt.Println("Transferred sponsorship for", A.Address()) +``` + + + +At this point, Signer 1 is only sponsoring the first asset (arbitrarily coded as `ABCD`), while Signer 2 is sponsoring the other two assets. (Recall that [initially](#sponsoring-trustlines) Signer 1 was also sponsoring `EFGH`.) + +### Sponsorship Revocation + +Finally, we can demonstrate complete revocation of sponsorships. Below, Signer 2 removes themselves from all responsibility over the two asset trustlines. Notice that Account A is not involved at all, since revocation should be performable purely at the sponsor's discretion. + + + +```js + // + // 4. S2 revokes sponsorship of B's trustlines entirely. + // + let s2Account = await server.loadAccount(S2.publicKey()).catch(accountFail); + let tx = new sdk.TransactionBuilder(s2Account, {fee: sdk.BASE_FEE}) + .addOperation(sdk.Operation.revokeTrustlineSponsorship({ + account: A.publicKey(), + asset: assets[1], + })) + .addOperation(sdk.Operation.revokeTrustlineSponsorship({ + account: A.publicKey(), + asset: assets[2], + })) + .setNetworkPassphrase(sdk.Networks.TESTNET) + .setTimeout(180) + .build(); + + tx.sign(S2); + let txResponse = await server.submitTransaction(tx).catch(txCheck); + if (!txResponse) { return; } + + console.log("Revoked sponsorship for", A.publicKey()); +} // ends main() +``` + +```go + // + // 4. S2 revokes sponsorship of B's trustlines entirely. + // + revokeOps := []txnbuild.Operation{ + &txnbuild.RevokeSponsorship{ + SponsorshipType: txnbuild.RevokeSponsorshipTypeTrustLine, + Account: &addressA, + TrustLine: &txnbuild.TrustLineID{ + Account: addressA, + Asset: assets[1], + }, + }, + &txnbuild.RevokeSponsorship{ + SponsorshipType: txnbuild.RevokeSponsorshipTypeTrustLine, + Account: &addressA, + TrustLine: &txnbuild.TrustLineID{ + Account: addressA, + Asset: assets[2], + }, + }, + } + + SignAndSend(client, s2Account.AccountID, []*keypair.Full{S2}, revokeOps...) + fmt.Println("Revoked sponsorship for", A.Address()) +} // ends main() +``` + + + +### Sponsorship Source Accounts + +When it comes to the `SourceAccount` fields of the sponsorship sandwich, it's important to refer to the wisdom of [CAP-33](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0033.md#abstract): + +> This relation is initiated by `BeginSponsoringFutureReservesOp`, where the **sponsoring** account is the source account, and is terminated by `EndSponsoringFutureReserveOp`, where the **sponsored** account is the source account. + +Since the source account defaults to the transaction submitter when omitted, this field needs _always_ needs to be set for either the `Begin` or the `End`. + +For example, the following is an identical expression of the [earlier Golang example](#sponsoring-trustlines) of sponsoring a trustline, just submitted by the **sponsor** (Sponsor 1) rather than the **sponsored** account (Account A). Notice the differences in where `SourceAccount` is set: + + + +```go + sponsorTrustline := []txnbuild.Operation{ + &txnbuild.BeginSponsoringFutureReserves{ + SponsoredID: addressA, + }, + &txnbuild.ChangeTrust{ + SourceAccount: aAccount.AccountID, + Line: &assets[0], + Limit: txnbuild.MaxTrustlineLimit, + }, + &txnbuild.EndSponsoringFutureReserves{ + SourceAccount: aAccount.AccountID, + }, + } + + // Again, both participants must still sign the transaction: the sponsored + // account must consent to the sponsorship. + SignAndSend(client, s1Account.AccountID, []*keypair.Full{S1, A}, sponsorTrustline...) +``` + + + +### Other Examples + +If you'd like other examples, or want to view a more-generic pseudocode breakdown of these sponsorship scenarios, you can refer to [CAP-33](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0033.md#example-revoke-sponsorship) directly. + +### Footnote + +For the above examples, an implementation of `SignAndSend` (Golang) and some (very) rudimentary error checking code (all languages) might look something like this: + + + +```js +function txCheck(err) { + console.error("Transaction submission failed:", err); + if (err.response != null && err.response.data != null) { + console.error("More details:", err.response.data.extras); + } else { + console.error("Unknown reason:", err); + } +} + +function accountFail(err) { + console.error(" Failed to load account:", err.response.body); +} +``` + +```go +// Builds a transaction containing `operations...`, signed (by `signers`), and +// submitted using the given `client` on behalf of `account`. +func SignAndSend( + client *sdk.Client, + account txnbuild.Account, + signers []*keypair.Full, + operations ...txnbuild.Operation, +) protocol.Transaction { + // Build, sign, and submit the transaction + tx, err := txnbuild.NewTransaction( + txnbuild.TransactionParams{ + SourceAccount: account, + IncrementSequenceNum: true, + BaseFee: txnbuild.MinBaseFee, + Timebounds: txnbuild.NewInfiniteTimeout(), + Operations: operations, + }, + ) + check(err) + + for _, signer := range signers { + tx, err = tx.Sign(network.TestNetworkPassphrase, signer) + check(err) + } + + txResp, err := client.SubmitTransaction(tx) + if err != nil { + if prob := sdk.GetError(err); prob != nil { + fmt.Printf(" problem: %s\n", prob.Problem.Detail) + fmt.Printf(" extras: %s\n", prob.Problem.Extras["result_codes"]) + } + check(err) + } + + return txResp +} + +func check(err error) { + if err != nil { + panic(err) + } +} +``` + + diff --git a/docs/glossary/testnet.mdx b/docs/glossary/testnet.mdx new file mode 100644 index 000000000..0a3de6054 --- /dev/null +++ b/docs/glossary/testnet.mdx @@ -0,0 +1,78 @@ +--- +title: Testnet +order: +--- + +The testnet is a small test Stellar network, run by the Stellar Development Foundation (SDF). It's free to use, functions just like the main public network, and is the best place to start developing on Stellar since it doesn't connect to real money. + +SDF runs 3 Stellar Core validators on the testnet. + +You can connect a node to the testnet by configuring [Stellar Core](https://github.com/stellar/stellar-core) to use this [configuration](https://github.com/stellar/stellar-core/blob/master/docs/stellar-core_testnet.cfg). + +There is also a [Horizon instance](https://horizon-testnet.stellar.org/) that can directly interact with the testnet. + +## What is the Stellar testnet good for? + +- [Creating test accounts](../tutorials/create-account.mdx) (with funding thanks to Friendbot). +- Developing applications and exploring tutorials on Stellar without the potential to lose any valuable [assets](./assets.mdx). +- Testing existing applications against new releases or release candidates of [Stellar Core](https://github.com/stellar/stellar-core/releases) and [Horizon](https://github.com/stellar/go/releases). +- Performing data analysis on a smaller, non-trivial data set compared to the public network. + +## What is the Stellar testnet not good for? + +- Load and stress testing. +- High availability test infrastructure - SDF makes no guarantees about the availability of the testnet. +- Long term storage of data on the network - [the network is ephemeral, and resets periodically](#periodic-reset-of-testnet-data). +- A testing infrastructure that requires more control over the test environment, such as: + - The ability to control the data reset frequency. + - The need to secure private or sensitive data (before launching on the public network) + +Keep in mind that you can always run your own test network for use cases that don't work well with SDF's testnet. + +## Best Practices For Using Testnet + +### Surge Pricing on the Testnet + +The testnet has a capacity limit of **100 operations per ledger**. When more than 100 operations are submitted to a given ledger, the network enters [surge pricing mode](./fees.mdx#surge-pricing), which uses market dynamics to decide which submissions are included. It works exactly the same way as surge pricing on the public network. + +If you are having trouble submitting transactions to the testnet, you may need to offer a higher fee. You can also take the opportunity to develop a fee strategy, which may prove useful when you move your project into production. + +### Friendbot, the Testnet XLM Faucet + +When building on the testnet, developers can request testnet XLM from Friendbot, which is an XLM faucet. You can use the [Stellar Laboratory](https://laboratory.stellar.org/#?network=test) to fund an account with Friendbot, or you can check out the [Create an Account tutorial](../tutorials/create-account.mdx) to see how to do it with various Stellar SDKs. + +To keep things fair for everyone, requests to Friendbot are rate limited, so please use it judiciously. When called, Friendbot provides 10,000 testnet XLM to create a new testnet account. If you need to create multiple testnet accounts, use Friendbot to create one account, and then use that account to fund the rest using the [Create Account operation](../start/list-of-operations/#create-account). + +### Periodic Reset of Testnet Data + +In order to preserve a good experience for developers, the SDF testnet is periodically reset to the genesis (initial) ledger. Resets declutter the network, remove spam, minimize the time required to catch up to the latest ledger, and help maintain the system over time. + +A reset clears all ledger entries (such as accounts, trustlines, offers, etc), transactions, and historical data from both Stellar Core and Horizon, which is why developers should not rely on the persistence of any accounts or on the state of any balances when using testnet. + +After a reset, you will need to take a few steps to re-join and re-synch to the testnet. Those steps are outlined [here](https://github.com/stellar/packages/blob/master/docs/testnet-reset.md), along with line-by-line instructions for people using core + horizon ubuntu packages. If you need help with other packages, check [Stellar's Stack Exchange](https://stellar.stackexchange.com/) for guidance. + +SDF will try to make testnet resets as painless as possible, and will announce the exact date at least two weeks in advance on the [Stellar Dashboard](http://dashboard.stellar.org/), and via several of Stellar’s online developer communities. + +The testnet resets once per quarter (every three months). The 2022 dates: + +- 3/16/22 +- 6/22/22 +- 9/14/22 +- 12/14/22 + +The testnet will always restart on the announced reset date at 0900 UTC. + +### Test Data Automation + +Since most applications rely on data being present to do anything useful, it is highly recommended that you have testing infrastructure that can repopulate testnet with useful data after a reset. Not only will this make testing more reliable, but it will also help you scale out your testing infrastructure to a private test network if you choose to do so. + +For example, you may want to: + +- Generate issuers of assets for testing the development of a wallet. +- Generate orders on the order book (both current and historical) for testing the development of a trading client. + +As a maintainer of an application, you will want to think about creating a data set that is representative enough to test your primary use cases, and allow for robust testing even when testnet is not available. + +A script can automate this entire process by [creating an account with Friendbot](../tutorials/create-account.mdx), and submitting a set of transactions that are predefined as a part of your testing infrastructure. + +For additional questions we recommend heading over to [Stellar's Stack Exchange](https://stellar.stackexchange.com/). diff --git a/docs/glossary/transactions.mdx b/docs/glossary/transactions.mdx new file mode 100644 index 000000000..69ba60bd8 --- /dev/null +++ b/docs/glossary/transactions.mdx @@ -0,0 +1,206 @@ +--- +title: Transactions +order: +--- + +Transactions are commands that modify the ledger state. They consist of a list of anywhere from 1 to 100 operations, and they are signed, submitted to the network, and considered for inclusion in the transaction set via [SCP](./scp.mdx). They contain the operations used to send payments, enter orders into the [decentralized exchange](./decentralized-exchange.mdx), change settings on accounts, and authorize accounts to hold assets. If you think of the ledger as a database, then transactions are SQL commands. + +## Transaction Attributes + +Each transaction has the following attributes: + +### Source Account + +The account that originates the transaction. This account also provides the fee and sequence number for the transaction. + +### Fee + +Each transaction incurs a fee, which is paid by the source account. When you submit a transaction, you set the maximum that you are willing to pay per operation, but you’re charged the minimum fee possible based on network activity. For more info, see [transaction fees](./fees.mdx) + +### Sequence Number + +Each transaction has a sequence number associated with the source account. Transactions follow a strict ordering rule when it comes to processing transactions per account in order to prevent double-spending. When submitting a single transaction, you should submit a sequence number 1 greater than the current sequence number. For example, if the sequence number on the account is 4, then the incoming transaction should have a sequence number of 5. + +However, if several transactions with the same source account make it into the same transaction set, they are ordered and applied according to sequence number. For example, if you submitted 3 transactions that shared the same source account and the account is currently at sequence number 5, the transactions must have sequence numbers 6, 7, and 8. + +### List of Operations + +Transactions contain an arbitrary list of [operations](./operations.mdx) inside them. Typically there is just one operation, but it's possible to have multiple (up to 100). Operations are executed in order as one ACID transaction, meaning that either all operations are applied or none are. If any operation fails, the whole transaction fails. If operations are on accounts other than the source account, then they require signatures of the accounts in question. + +### List of Signatures + +Up to 20 signatures can be attached to a transaction. See [Multi-sig](./multisig.mdx) for more information. A transaction is considered invalid if it includes signatures that aren't needed to authorize the transaction — superfluous signatures aren't allowed. + +Signatures are required to authorize operations and to authorize changes to the source account (fee and sequence number). + +### Memo + +The memo contains optional extra information. It is the responsibility of the client to interpret this value. Memos can be one of the following types: + +- `MEMO_TEXT` : A string encoded using either ASCII or UTF-8, up to 28-bytes long. +- `MEMO_ID` : A 64 bit unsigned integer. +- `MEMO_HASH` : A 32 byte hash. +- `MEMO_RETURN` : A 32 byte hash intended to be interpreted as the hash of the transaction the sender is refunding. + +### Validity Conditions + +There are a number of ways to control when a transaction should be considered valid. These conditions are checked first, prior to the validity of the operations or other parts of the transaction, so they're often referred to as "preconditions". They can all be combined, provided the combination is logically sound. + +Some of the validity preconditions, namely the minimum sequence [age](#minimum-sequence-age) or [ledger gap](#minimum-sequence-ledger-gap) preconditions, are based on the account's [sequence number age](./accounts.mdx#sequence-time-and-ledger). + +#### Time Bounds + +The optional UNIX timestamp (in seconds), determined by ledger time, of a lower and upper bound of when this transaction will be valid. If a transaction is submitted too early or too late, it will fail to make it into the transaction set. `maxTime` equal `0` means that it's not set. _We highly advise for all transactions to use time bounds, and many SDKs enforce their usage._ If a transaction doesn't make it into the transaction set, it is kept around in memory in order to be added to the next transaction set on a best-effort basis. Because of this behavior, we highly advise that all transactions are created with time bounds in order to invalidate transactions after a certain amount of time, especially if you plan to resubmit your transaction at a later time. + +#### Ledger Bounds + +These are like [Time Bounds](#time-bounds), except they apply to ledger numbers. With them set, a transaction will only be valid for ledger numbers that fall into the range you set. The lower bound is inclusive while the upper bound is not. If you set the upper bound to zero, this indicates that there is no upper bound. + +#### Minimum Sequence Number + +If a minimum sequence number is set, the transaction will only be valid when its source account's sequence number (call it `S`) is large enough. Specifically, it's valid when `S` satisfies `minSeqNum <= S < tx.seqNum`. If this precondition is omitted, the default behavior applies: the transaction's sequence number must be exactly one greater than the account's sequence number. + +Note that after a transaction is executed, the account will always set its sequence number to the transaction's sequence number. + +#### Minimum Sequence Age + +When the sequence age precondition is set, the transaction is only valid after a particular duration (expressed in seconds) elapses since the account's [sequence number age](./accounts.mdx#sequence-time-and-ledger). + +Minimum sequence age is a precondition relating to time, but unlike [time bounds](#time-bounds) which express absolute times, minimum sequence age is relative to when the transaction source account's sequence number was touched. + +#### Minimum Sequence Ledger Gap + +When the ledger gap precondition is set, the transaction is only valid after the current network ledger number meets (or exceeds) a particular gap relative to the ledger corresponding to the account's [sequence number age](./accounts.mdx#sequence-time-and-ledger). + +This is similar to the [minimum sequence age](#minimum-sequence-age), except it's expressed as a number of ledgers rather than a duration of time. + +#### Extra Signers + +A transaction can specify up to two extra signers as a precondition, meaning it must have signatures that correspond to those extra signers, even if those signatures would not otherwise be required to authorize the transaction (i.e. for its source account or operations). + +The additional signers can be of any type besides a pre-authorized transaction signer, since to pre-authorize a transaction, you need to know its hash, but the hash must include the extra signers. This Catch-22 relationship means including this type of extra signer will return an error. + +## Transaction Envelopes + +Once a transaction is ready to be signed, the transaction object is wrapped in an object called a `Transaction Envelope`, which contains the transaction as well as a set of signatures. Most transaction envelopes only contain a single signature along with the transaction, but in [multi-signature setups](./multisig.mdx) it can contain many signatures. + +Ultimately, transaction envelopes are passed around the network and are included in transaction sets, as opposed to raw Transaction objects. + +It's of note that each signer signs the hash of the transaction object in addition to the network passphrase. This is done to ensure that a given transaction can only be submitted to the intended network by its signers. For more information, see [Network Passphrases](./network-passphrase.mdx). + +## Validity of a Transaction + +To determine if a transaction is valid, many checks take place over the course of the transaction's lifecycle. The following conditions determine whether a transaction is valid: + +- **Source Account** — The source account must exist on the ledger. +- **Fee** — The fee must be greater than or equal to the [network minimum fee](./fees.mdx) for the number of operations submitted as part of the transaction. Note that this does not guarantee that the transaction will be applied; it only guarantees that it is valid. In addition, the source account must be able to pay the fee specified. In the case where multiple transactions are submitted but only a subset of them can be paid for, they are checked for validity in order of sequence number. +- **Sequence Number** — For the transaction to be valid, the sequence number must be 1 greater than the sequence number stored in the source account [account entry](./accounts.mdx) _when the transaction is applied_. This means when checking the validity of multiple transactions with the same source account in a candidate transaction set, they must all be valid transactions and their sequence numbers must be offset by 1 from each other. When it comes to apply time, they are ordered and applied according to their sequence number. + - For example, if your source account's sequence number is 5 and you submit 3 transactions, all transactions must be considered valid and their sequence numbers must be 6, 7, and 8 in order for any of them to make it into a candidate transaction set. +- **List of Operations** — Each operation must pass all of the [validity checks for an operation](./operations.mdx#validity-of-an-operation). +- **List of Signatures** — In addition to meeting the signature requirements of each operation in the transaction, the following requirements must be met for the transaction: + - The appropriate network passphrase was part of the transaction hash that was signed by each of the signers. See [Network Passphrases](./network-passphrase.mdx) for more. + - The combined weight of all signatures for the source account _of the transaction_ meets the low threshold for the source account. This is necessary in order for fees to be taken and the sequence number to be incremented later in the transaction lifecycle. +- **Memo** — The memo type must be a valid type, and the memo itself must be adhere to the formatting of the memo type. +- **Preconditions** — refer to the [Validity Conditions](#validity-conditions) described above for thorough explanations of each of these. For each of these that are set, the transaction will be considered invalid unless the transaction is submitted: + - **Time Bounds** — within the set time bounds of the transaction + - **Ledger Bounds** — within the set ledger bounds of the transaction + - **Minimum Sequence Number** — by a source account whose sequence number is greater than or equal to this value + - **Minimum Sequence Age** — after a duration meeting or exceeding the source account's [sequence number age](./accounts.mdx#sequence-time-and-ledger) + - **Minimum Sequence Ledger Gap** — in a ledger meeting or exceeding the source account's [sequence number age](./accounts.mdx#sequence-time-and-ledger) + - **Extra Signers** — with signatures that fulfill each of the extra signers + +## Transaction Lifecycle + +1. **Creation (Transaction Creator)**: A user creates a transaction by setting the source + account, sequence number, list of operations and their respective parameters, fee, and + optionally a memo and timebounds. You can try this out [using the Stellar + Laboratory](https://laboratory.stellar.org/#txbuilder?network=test). + +1. **Signing (Transaction Signers)**: Once the transaction is completely filled out, the + transaction is formed into a transaction envelope, which contains the transaction itself and a + list of signers. All the required signatures must be collected and added to the transaction + envelope's list of signers. Commonly it's just the signature of the account doing the + transaction, but more complicated setups can require collecting [signatures from multiple + parties](./multisig.mdx). + +1. **Submitting (Transaction Submitter)**: After signing, the transaction must be valid and can now + be submitted to the Stellar network. If the transaction is invalid, it will be immediately + rejected by stellar-core based on [the validity rules of a + transaction](#validity-of-a-transaction), the account's sequence number will not be incremented, + and no fee will be consumed from the source account. Multiple transactions for the same account + can be submitted, provided each of their sequence numbers are off by one. If they are all valid, + Stellar Core will craft a transaction set with each of those transactions applied in sequence + number order. Transactions are typically submitted using [Horizon](/api/introduction/), but you + can also submit the transaction directly to an instance of + [Stellar Core](..//run-core-node/index.mdx). + +1. **Propagating (Validator)**: Once Stellar Core has determined that a transaction is valid, it + will then propagate the transaction to all of the other servers to which it's connected. In this + way, a valid transaction is flooded to the entire Stellar network. + +1. **Crafting a candidate transaction set (Validator)**: When it's time to close the ledger, each + Stellar Core validator (a Stellar Core node participating in consensus) takes + all valid transactions it is aware of since the last ledger close and collects them into a + candidate transaction set. If it hears about any incoming transactions now, it puts them aside + for the next ledger close. If the number of operations in the candidate transaction set is + greater than the maximum number of operations per ledger, transactions will be prioritized by + their fee for inclusion in the set. See the [Fees](./fees.mdx) doc for more info. + +1. **Nominating a transaction set (Validator)**: Once each validator has crafted a candidate + transaction set, the set is nominated to the network. + +1. **Stellar Consensus Protocol (SCP) determines the final transaction set (Validator Network)**: + [SCP](./scp.mdx) resolves any differences between candidate transaction sets, and ultimately determines a + single transaction set to apply, the close time of the ledger, and any upgrades to the protocol + that need to be applied network wide at apply time. + - If a transaction doesn't make it into the transaction set, it is kept around in memory in + order to be added to the next transaction set on a best effort basis. + - If a transaction is kept in memory after a certain number of ledger closes, it will be banned + for several additional ledgers. This means no attempt will be made to include it in a + candidate transaction set additional ledgers during this time. + +1. **Transaction apply order is determined (Validator Network)**: Once SCP agrees on a particular + transaction set, the apply order is computed for the transaction set. This both shuffles the + order of the set to create uncertainty for competing transactions and maintains the + order of sequence numbers for multiple transactions per account. + +1. **Fees are collected (Validator)**: [Fees](./fees.mdx) are collected for all transactions + simultaneously. + +1. **Application (Validator)**: Each transaction is applied in the order previously determined. + For each transaction, the account's sequence number is consumed (increased by 1), the + transaction's validity is checked again, and each operation is applied in the order they occur + in the transaction. Operations may fail at this stage due to errors that can occur outside of + the transaction and operation validity checks. For example, an insufficient balance for a + payment is not checked at submission, and would fail at this time. If any operation fails, the + entire transaction will fail, and all previous operations will be rolled back. + +1. **Protocol Upgrades (Validator)**: Finally, upgrades are run if an upgrade took place. This + can include arbitrary logic to upgrade the ledger state for protocol upgrades, along with + ledger header modifications including the protocol version, base fee, maximum number of + operations per ledger, etc. Once this has completed, the life cycle begins anew. + +## Result Codes + +Transactions return a result code listed in a table below. Error reference for operations can be +found in [List of Operations](../start/list-of-operations.mdx) doc. + +| Result | Code | Description | +| --- | --- | --- | +| SUCCESS | 0 | All operations contained in the transaction succeeded. | +| FAILED | -1 | One of the operations failed (check [List of operations](../start/list-of-operations.mdx) for errors). | +| TOO_EARLY | -2 | Ledger `closeTime` before `minTime` value in the transaction. | +| TOO_LATE | -3 | Ledger `closeTime` after `maxTime` value in the transaction. | +| MISSING_OPERATION | -4 | No operation was specified. | +| BAD_SEQ | -5 | Sequence number does not match source account. | +| BAD_AUTH | -6 | Too few valid signatures / wrong network. | +| INSUFFICIENT_BALANCE | -7 | Fee would bring account below [minimum reserve](./minimum-balance.mdx). | +| NO_ACCOUNT | -8 | Source account not found. | +| INSUFFICIENT_FEE | -9 | [Fee](./fees.mdx) is too small. | +| BAD_AUTH_EXTRA | -10 | Unused signatures attached to transaction. | +| INTERNAL_ERROR | -11 | An unknown error occured. | +| NOT_SUPPORTED | -12 | The transaction type is not supported. | +| FEE_BUMP_INNER_FAILED | -13 | The fee bump inner transaction failed. | +| BAD_SPONSORSHIP | -14 | The sponsorship is not confirmed. | +| BAD_MIN_SEQ_AGE_OR_GAP | -15 | The minimum sequence age and/or minimum sequence ledger gap conditions aren't met | +| MALFORMED | -16 | The precondition is somehow invalid | diff --git a/docs/glossary/versioning.mdx b/docs/glossary/versioning.mdx new file mode 100644 index 000000000..f28aef268 --- /dev/null +++ b/docs/glossary/versioning.mdx @@ -0,0 +1,102 @@ +--- +title: Versioning and Network Upgrades +order: +--- + +import { CodeExample } from "@site/src/components/CodeExample"; + +This document describes the various mechanisms used to keep the overall system working as it evolves. + +## Ledger versioning + +### ledgerVersion + +This uint32 stored in the ledger header describes the version number of the overall protocol. Protocol in this case is defined both as "wire format"--i.e., the serialized forms of all objects stored in the ledger — and its behavior. + +This version number is incremented every time the protocol changes. + +### Integration with consensus + +Most of the time, consensus is simply reached on which transaction set needs to be applied to the previous ledger. + +Consensus can also, however, be reached on upgrade steps. + +One such upgrade step is "update ledgerVersion to value X after ledger N". + +If nodes do not consider that the upgrade step is valid, they simply drop the upgrade step from their vote. + +A node considers a step invalid either because they do not understand it or some condition is not met. In the previous example, it could be that X is not supported by the node or that the ledger number didn't reach N yet. + +Upgrade steps are applied before applying the transaction set to ensure that the logic scheduling steps is the same that is processing it. Otherwise, the steps would have to be applied after the ledger is closed. + +### Supported versions + +Each node has its own way of tracking which version it supports--for example, a "min version", "max version"--but it can also include things like "black listed versions." Supported versions are not tracked from within the protocol. + +Note that minProtocolVersion is distinct from the version an instance understands: typically an implementation understands versions n .. maxProtocolVersion, where n <= minProtocolVersion. The reason for this is that nodes must be able to replay transactions from history (down to version 'n'), yet there might be some issue/vulnerability that we don't want to be exploitable for new transactions. + +### Ledger object versioning + +Data structures that are likely to evolve over time contain the following extension point: + + + +```cpp +union switch(int v) +{ +case 0: + void; +} ext; +``` + + + +In this case, the version 'v' refers to the version of the object and permits the addition of new arms. + +This scheme offers several benefits: + +- Implementations become wire compatible without code changes only by updating their protocol definition files. +- Even without updating the protocol definition files, older implementations continue to function as long as they don't encounter newer formats. +- It promotes code sharing between versions of the objects. + +Note that while this scheme promotes code sharing for components consuming those objects, code sharing is not necessarily promoted for stellar-core itself because the behavior must be preserved for all versions: In order to reconstruct the ledger chain from arbitrary points in time, the behavior must be 100% compatible. + +### Operations versioning + +Operations are versioned as a whole: If a new parameter needs to be added or changed, versioning is achieved by adding a new operation. This causes some duplication of logic in clients but avoids introducing potential bugs. For example, code that would sign only certain types of transactions must be fully aware of what it's signing. Envelope versioning Pattern used to allow for extensibility of envelopes (signed content): + + + +```cpp +union TransactionEnvelope switch (int v) +{ +case 0: + struct + { + Transaction tx; + DecoratedSignature signatures<20>; + } v0; +}; +``` + + + +This pattern allows the capability to modify the envelope if needed and ensures that clients don't blindly consume content that they couldn't validate. Upgrading objects that don't have an extension point The object's schema must be cloned and its parent object must be updated to use the new object type. The assumption here is that there is no unversioned "root" object. Supported implementations lifetime considerations In order to keep the codebase in a maintainable state, implementations may not preserve the ability to play back from genesis. Instead they may opt to support a limited range--for example, only preserve the capability to replay the previous 3 months of transactions (assuming that the network's minProtocolVersion is more recent than that). + +This does not change the ability of the node to (re)join or participate in the network; it only affects the ability for a node to do historical validation. + +## Overlay versioning + +Overlay follows a similar pattern for versioning: It has a min-maxOverlayVersion. + +The versioning policy at the overlay layer is a lot more aggressive when it comes to the deprecation schedule; the set of nodes involved is limited to the ones that connect directly to the instance. + +With this in mind, structures follow the "clone" model at this layer: if a message needs to be modified, a new message is defined by cloning the old message type using a new type identifier. + +Knowing that the older implementation will be deleted anyway, the clone model makes it possible to refactor large parts of the code and avoids the headache of maintaining older versions. + +At this layer, it's acceptable to modify the behavior of older versions as long as it stays compatible. + +The implementation may decide to share the underlying code--for example, by converting legacy messages into the new format internally. + +The "HELLO" message exchanged when peers connect to each other contains the min and max version the instance supports. The other endpoint may decide to disconnect right away if it's not compatible. diff --git a/docs/glossary/xdr.mdx b/docs/glossary/xdr.mdx new file mode 100644 index 000000000..0222078f4 --- /dev/null +++ b/docs/glossary/xdr.mdx @@ -0,0 +1,20 @@ +--- +title: XDR +order: +--- + +**XDR**, also known as _External Data Representation_, is used throughout the Stellar network and protocol. The ledger, transactions, results, history, and even the messages passed between computers running stellar-core are encoded using XDR. + +XDR is specified in [RFC 4506](http://tools.ietf.org/html/rfc4506.html) and is similar to tools like Protocol Buffers or Thrift. XDR provides a few important features: + +- It is very compact, so it can be transmitted quickly and stored with minimal disk space. +- Data encoded in XDR is reliably and predictably stored. Fields are always in the same order, which makes cryptographically signing and verifying XDR messages simple. +- XDR definitions include rich descriptions of data types and structures, which is not possible in simpler formats like JSON, TOML, or YAML. + +## Parsing XDR + +Since XDR is a binary format and not as widely known as simpler formats like JSON, the Stellar SDKs all include tools for parsing XDR and will do so automatically when retrieving data. + +In addition, the Horizon API server generally exposes the most important parts of the XDR data in JSON, so they are easier to parse if you are not using an SDK. The XDR data is still included (encoded as a base64 string) inside the JSON in case you need direct access to it. .X files + +Data structures in XDR are specified in an _interface definition file_ (IDL). The IDL files used for the Stellar Network are available [on GitHub](https://github.com/stellar/stellar-core/tree/master/src/protocol-curr/xdr). diff --git a/docs/index.mdx b/docs/index.mdx new file mode 100644 index 000000000..9e3cecbd7 --- /dev/null +++ b/docs/index.mdx @@ -0,0 +1,75 @@ +--- +title: Welcome +order: -100 +--- + +import { ReadMore } from "@site/src/components/ReadMore"; + +This is the authoritative guide to all things Stellar. Here, you will find helpful tutorials, guides for deploying infrastructure, a glossary of common terms, and more! The documentation is designed to be modular, organized around different learning paths that expand as network use cases evolve. + +In addition to the docs here, you'll also see a link at the bottom of the left nav to the API Reference. You should check those out, too: that's a resource most developers consult as they build on Stellar. + +As you read through these docs, you may find bugs, errors, typos, or omissions. When you do, please file PRs or issues in [this repo](https://github.com/stellar/new-docs/issues/new). Like the Stellar codebase, the docs are open source, so we welcome and appreciate your contributions! + +A rundown of what's here: + +## Tutorials + +If you’re new to Stellar and want to get an overview of the network, this is where to start. The early tutorials will show you how to do some basic things like create an account and make payments. The later tutorials cover more advanced topics like Stellar smart contracts. If you want to issue an asset or build an app, you're better served checking the sections dedicated to those paths. + + + +## Issue Assets + +Stellar is a multi-currency network by design. The ability to issue assets is fundamental to Stellar, and it's something you can do quickly, safely, and in a few lines of code. Once you've issued an asset, you can also publish canonical information about it for wallets and consumers, control access to it by setting simple flags, and make it available for trade on the Stellar decentralized exchange. This section will show you how. + + + +## Anchor Assets + +Organizations can connect assets issued on Stellar with external banking and payment systems, allowing users and businesses to transfer assets onto or through the Stellar network. Specifically, organizations can **anchor** assets issued on the Stellar network by facilitating 1-1 trades for the off-chain representation of the tokenized asset. + +### Deposits & Withdrawals + +For example, a USD anchor could accept $1000 USD from a customer's wire transfer and send 1000 USDX tokens to the customer's Stellar account. Conversely, another customer could send USDX tokens to the anchor on Stellar and expect an incoming $1000 USD wire transfer from the anchor. These kinds of deposit and withdrawal operations are facilitated by wallet applications and SEP-24 anchor servers. + + +
+ +### Cross-border Payments + +Anchors can also facilitate payments made through Stellar instead of simply on Stellar. + +For example, a customer could want to send $1000 USD worth of EUR to a friend's bank account in Germany. Anchor A could collect the sending and receiving customer's information, make a USD->EUR path payment on Stellar to Anchor B (in Germany), and Anchor B could deposit the funds into the recipent's bank account. + + + +## Build Apps + +Stellar is a self-serve distributed ledger that you can use as a backend to power all kinds of apps and services. Any app built on Stellar relies on the same basic functions: key storage, account creation, transaction signing, and queries to the Stellar database. This section of the docs will walk you through the process of building a basic wallet that does all those things, and will show you how to add features to it like the support for in-app deposits and withdrawals from anchors. + + + +## Run a Core Node + +This section explains the technical and operational aspects of installing, configuring, and maintaining a Stellar Core node, which is a server that connects to the Stellar peer-to-peer network to keep a common distributed ledger. You don’t have to run a node to get started on Stellar, but you will likely want to if you're in production, need high-availability access network, or want to help increase network health and decentralization. + + + +## Run an API Server + +Most developers access the network using Horizon, the Stellar API. It takes the performance-oriented data structures from Stellar Core and converts them into a friendlier format. If you're running your own Stellar Core node and using it to submit transactions or get network data, you will likely also want to run your own Horizon instance, and this section will show you how. If you're just looking to use Horizon (vs. setting up a Horizon server), consult the API Reference. + + + +## Software and SDKs + +This is where you'll find all the Stellar SDKs. There are a lot of them, and they're all pretty well maintained and documented, so you should be able to build on Stellar in your language of choice. This section is also home to some tools and reference implementations created and maintained by the Stellar Development Foundation to kickstart development. + + + +## Glossary + +This section defines all the terms and explains all the concepts germane to Stellar. Use it to look up a word, or to dig deeper into nitty-gritty details. + + diff --git a/docs/issuing-assets/anatomy-of-an-asset.mdx b/docs/issuing-assets/anatomy-of-an-asset.mdx new file mode 100644 index 000000000..26f98d212 --- /dev/null +++ b/docs/issuing-assets/anatomy-of-an-asset.mdx @@ -0,0 +1,101 @@ +--- +title: Anatomy of an Asset +order: 15 +--- + +import { CodeExample } from "@site/src/components/CodeExample"; + +Each Stellar asset has two characteristics: the asset code and the issuer. When you look up or interact with an asset on Stellar, you always use both to identify it. + +Many Stellar tokens represent credits that can be redeemed for something outside the network—often fiat currency, but also bonds, carbon credits, gold, etc.—and since more than one organization can issue a credit representing the same underlying asset, asset codes often overlap. More than one company offers a USD token on Stellar, for instance. + +However, the combination of asset code and issuer allows each asset to be uniquely identified: each USD token is offered by, and redeemable with, one specific issuer. + +## Asset Code + +When you issue an asset, the first thing you do is choose an identifying code. Currently, there are two supported formats. + +- **Alphanumeric 4-character maximum**: Any characters from the set [a-z][A-Z][0-9] are allowed. The code can be shorter than 4 characters, but the trailing characters must all be empty. +- **Alphanumeric 12-character maximum**: Any characters from the set [a-z][A-Z][0-9] are allowed. The code can be any number of characters from 5 to 12, but the trailing characters must all be empty. + +Provided it falls into one of those two buckets, you can choose any asset code you like. That said, if you’re issuing a currency, you should use the appropriate [ISO 4217 code](https://en.wikipedia.org/wiki/ISO_4217), and if you’re issuing a stock or bond, the appropriate [ISIN number](https://en.wikipedia.org/wiki/International_Securities_Identification_Number). Doing so makes it easier for Stellar interfaces to properly display and sort your token in their listings, and allows potential token holders to understand, at a glance, what your token represents. + +## Issuer + +There is no dedicated operation to create an asset on Stellar. Instead, assets are created with a payment operation: an issuing account makes a payment using the asset it’s issuing, and that payment actually creates the asset on the network. + +The public key of the issuing account is linked on the ledger to the asset itself. Responsibility for and control over an asset resides with the issuing account, and since settings are stored at the account level on the ledger, the issuing account is where you use `set_options` operations to link to meta-information about an asset and set authorization flags. + +## Trustlines + +Before an account can hold an asset another account issues, it has to establish something called a trustline, which is a persistent account-level ledger entry created with a `change_trust` operation. + +A trustline is an explicit opt-in to hold a particular token, so it specifies both asset code and issuer. Each trustline increases an account’s minimum lumen balance by one [base reserve](../glossary/minimum-balance.mdx) — currently 0.5 XLM — and tracks the balance of the asset the account holds. Trustlines can also limit the amount of an asset an account can hold, though more often than not, account holders don’t set that limit: they simply turn the trustline on, which by default allows the maximum. + +### Liabilities + +A trustline also tracks liabilities. Buying liabilities equal the total amount of the asset offered to buy aggregated over all offers owned by an account, and selling liabilities equal the total amount of the asset offered to sell aggregated over all offers owned by an account. A trustline must always have balance sufficiently large to satisfy its selling liabilities, and a balance sufficiently below its limit to accommodate its buying liabilities. + +### Pool Shares + +With the introduction of native support for liquidity pools in [Protocol 18](https://stellar.org/developers-blog/liquidity-liquidity-liquidity), assets can also be _pool shares_, representing ownership of the pool's reserves. Users need to establish trustlines to three different assets to participate in a liquidity pool: both of the reserve assets (unless one of them is native) and the pool share asset itself. The pool share asset is defined by the liquidity pool identifier, which in turn is defined by the two assets its reserves are composed of. You should refer to the [glossary entry](../glossary/liquidity-pool.mdx#liquidity-pool-participation) for details on liquidity pool participation. + +## Representation + +In Horizon, assets are represented in a JSON object: + + + +```json +{ + "asset_code": "AstroDollar", + "asset_issuer": "GC2BKLYOOYPDEFJKLKY6FNNRQMGFLVHJKQRGNSSRRGSMPGF32LHCQVGF", + // `asset_type` is used to determine how asset data is stored. + // It can be `native` (lumens), `credit_alphanum4`, or `credit_alphanum12`. + "asset_type": "credit_alphanum12" +} +``` + + + +In the Stellar SDKs, they’re represented with the `Asset` class: + + + +```js +var astroDollar = new StellarSdk.Asset( + "AstroDollar", + "GC2BKLYOOYPDEFJKLKY6FNNRQMGFLVHJKQRGNSSRRGSMPGF32LHCQVGF", +); +``` + +```java +KeyPair issuer = KeyPair.fromAccountId("GC2BKLYOOYPDEFJKLKY6FNNRQMGFLVHJKQRGNSSRRGSMPGF32LHCQVGF"); +Asset astroDollar = Asset.createNonNativeAsset("AstroDollar", issuer.getAccountId()); +``` + +```python +from stellar_sdk import Asset + +astro_dollar = Asset("AstroDollar", "GC2BKLYOOYPDEFJKLKY6FNNRQMGFLVHJKQRGNSSRRGSMPGF32LHCQVGF") +``` + + + +## Amount Precision + +Each asset amount is encoded as a signed 64-bit integer in the [XDR structures](../glossary/xdr.mdx) that Stellar uses to encode transactions. The asset amount unit seen by end users is scaled down by a factor of ten million (10,000,000) to arrive at the native 64-bit integer representation. + +For example, the integer amount value `25,123,456` equals `2.5123456` units of the asset. This scaling allows for seven decimal places of precision in human-friendly amount units. + +The smallest non-zero amount unit is `0.0000001` (one ten-millionth) represented as an integer value of one. The largest amount unit possible is `((2^63)-1)/(10^7)` (derived from max int64 scaled down) which is `922,337,203,685.4775807`. + +The numbers are represented as `int64`s. Amount values are stored as only signed integers to avoid bugs that arise from mixing signed and unsigned integers. + +## Relevance in Horizon and Stellar Client Libraries + +In Horizon and client-side libraries such as `js-stellar-sdk`, the integer encoded value is abstracted away. Many APIs expect an amount in unit value (the scaled-up amount displayed to end users). Some programming languages (such as JavaScript) have problems with maintaining precision on a number amount. It is recommended to use "big number" libraries that can record arbitrary precision decimal numbers without a loss of precision. + +## Lumens (XLM) + +Lumens (XLM) are the native currency of the Stellar network, and are the only asset that doesn't require an issuer or a trustline. Every account is required to hold a [minimum lumen balance](../glossary/minimum-balance.mdx), and all [transaction fees](../glossary/fees.mdx) are paid in lumens. The smallest unit of a lumen is a stroop, which is one ten-millionth of a lumen. For more on lumens, check out the [Stellar.org explainer](https://www.stellar.org/lumens). diff --git a/docs/issuing-assets/control-asset-access.mdx b/docs/issuing-assets/control-asset-access.mdx new file mode 100644 index 000000000..b49dc3e5c --- /dev/null +++ b/docs/issuing-assets/control-asset-access.mdx @@ -0,0 +1,171 @@ +--- +title: Control Access to an Asset +order: 50 +--- + +import { CodeExample } from "@site/src/components/CodeExample"; +import { Alert } from "@site/src/components/Alert"; + +When you issue an asset on Stellar, anyone can hold it by default. In general, that’s a good thing: easy access means better reach and better liquidity, and most of the time, issuers with KYC requirements can handle those when an asset moves onto or off of the network. Most fiat-backed token issuers, for instance, use the transfer server protocol specified in SEP-24 to decide whether to honor deposits and withdrawals rather than setting account-level flags. You can read about how to do that in the [Enable Deposit and Withdrawal](../anchoring-assets/enabling-deposit-and-withdrawal/index.mdx) section. + +However, if you need to control access to an asset to comply with regulations (or for any other reason), you can easily do so by enabling flags on your issuing account. + +## Flags + +Flags are created on the _account level_ using a [`set_options`](../start/list-of-operations.mdx#set-options) operation. They can be set at any time in the life cycle of an asset, not just when you issue it: + +### Authorization Required + +When `AUTHORIZATION REQUIRED` is enabled, an issuer must approve an account before that account can hold its asset. This setting allows issuers to vet potential token holders using whatever means they see fit, and to approve trustlines if and only if the holders pass muster. + +To allow access, the user creates a trustline, and the issuer approves it by changing the `AUTHORIZE` flag with the [`allow_trust`](../start/list-of-operations.mdx#allow-trust) operation. + +There are two levels of authorization an asset issuer can grant using the `allow_trust` operation: + +- `AUTHORIZED`: This flag signifies complete authorization allowing an account to transact freely with the asset to make and receive payments and place orders. +- `AUTHORIZED_TO_MAINTAIN_LIABILITIES`: This flag denotes limited authorization that allows an account to maintain current orders, but not to otherwise transact with the asset. + +### Authorization Revocable + +When `AUTHORIZATION_REVOCABLE` is enabled, an issuer can revoke an existing trustline's authorization, thereby freezing the asset held by an account. Doing so prevents that account from transfering or trading the asset, and cancels the account’s open orders for the asset. + +`AUTHORIZATION_REVOCABLE` also allows an issuer to reduce authorization from complete to limited, which prevents the account from transferring or trading the asset, but does not cancel the account's open orders for the asset. This setting is useful for issuers of regulated assets who need to authorize transactions on a case-by-case basis to ensure each conforms to certain requirements. + +All changes to asset authorization are performed with the [`allow_trust`](../start/list-of-operations.mdx#allow-trust) operation. + +### Authorization Immutable + +With this setting, neither of the other authorization flags can be set, and the issuing account can’t be merged. You set this flag to signal to potential token holders that your issuing account and its assets will persist on ledger in an open and accessible state. + +### Clawback Enabled + +With the `AUTHORIZATION_CLAWBACK_ENABLED` flag set, any _subsequent_ trustlines established with this account will have clawbacks enabled. You can read more about clawbacks (and selectively controlling them on a per-trustline basis) [here](../glossary/clawback.mdx). + +Note that this flag requires that [revocable](#authorization-revocable) is also set. + +## Example flow + +To get a sense of how authorization flags work, let's look at how an issuer of a regulated asset might use the `AUTHORIZED_TO_MAINTAIN_LIABILITIES` flag. + +If the issuer wants to approve transactions on a case-by-base basis while allowing accounts to maintain offers, they can leave an account in the `AUTHORIZED_TO_MAINTAIN_LIABILITIES` state. That account can own offers, but cannot otherwise do anything with the asset. + +To intitiate a new operation, the holding account requests that the issuer approve and sign a transaction. Once the issuer inspects the operation and decides to approve it, they sandwich it between a set of operations, first granting authorization, then reducing it. + +Here's a payment from A to B sandwiched between [`set_trust_line_flags`](../start/list-of-operations.mdx#set-trustline-flags) operations: + +- Operation 1: Issuer uses `SetTrustLineFlags` to fully authorize account A, asset X +- Operation 2: Issuer uses `SetTrustLineFlags` to fully authorize account B, asset X +- Operation 3: Payment from A to B +- Operation 4: Issuer uses `SetTrustLineFlags` to set account B, asset X to `AUTHORIZED_TO_MAINTAIN_LIABILITIES_FLAG` state +- Operation 5: Issuer uses `SetTrustLineFlags` to set account A, asset X to `AUTHORIZED_TO_MAINTAIN_LIABILITIES_FLAG` state + +The authorization sandwich allows the issuer to inspect the specifc payment, and to grant authorization for it and it alone. Since operations bundled in a transaction are simultaneous, A and B are only authorized for the specific, pre-approved payment operation. Complete authorization does not extend beyond the specific transaction. + +## Sample code + + + +In the following code samples, proper error checking is omitted for brevity. However, you should _always_ validate your results, as there are many ways that requests can fail. You should refer to the guide on [Handling Errors Gracefully](../tutorials/handling-errors.mdx) for tips on error management strategies. + + + +The following example sets authorization to be both required and revocable: + + + +```js +var StellarSdk = require("stellar-sdk"); +var server = new StellarSdk.Server("https://horizon-testnet.stellar.org"); + +// Keys for issuing account +var issuingKeys = StellarSdk.Keypair.fromSecret( + "SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4", +); + +server + .loadAccount(issuingKeys.publicKey()) + .then(function (issuer) { + var transaction = new StellarSdk.TransactionBuilder(issuer, { + fee: 100, + networkPassphrase: StellarSdk.Networks.TESTNET, + }) + .addOperation( + StellarSdk.Operation.setOptions({ + setFlags: StellarSdk.AuthRevocableFlag | StellarSdk.AuthRequiredFlag, + }), + ) + // setTimeout is required for a transaction + .setTimeout(100) + .build(); + transaction.sign(issuingKeys); + return server.submitTransaction(transaction); + }) + .then(console.log) + .catch(function (error) { + console.error("Error!", error); + }); +``` + +```java +import org.stellar.sdk.*; +import org.stellar.sdk.Network; +import org.stellar.sdk.responses.AccountResponse; + +Server server = new Server("https://horizon-testnet.stellar.org"); + +// Keys for issuing account +KeyPair issuingKeys = KeyPair + .fromSecretSeed("SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4"); +AccountResponse sourceAccount = server.accounts().account(issuingKeys.getAccountId()); + +Transaction setAuthorization = new Transaction.Builder(sourceAccount, Network.TESTNET) + .addOperation(new SetOptionsOperation.Builder() + .setSetFlags( + AccountFlag.AUTH_REQUIRED_FLAG.getValue() | + AccountFlag.AUTH_REVOCABLE_FLAG.getValue()) + .build()) + .build(); +setAuthorization.sign(issuingKeys); +server.submitTransaction(setAuthorization); +``` + +```python +from stellar_sdk import Keypair, Network, Server, TransactionBuilder, AuthorizationFlag +from stellar_sdk.exceptions import BaseHorizonError + +# Configure Stellar SDK to talk to the horizon instance hosted by Stellar.org +# To use the live network, set the hostname to 'https://horizon.stellar.org' +server = Server(horizon_url="https://horizon-testnet.stellar.org") +# Use the test network, if you want to use the live network, please set it to `Network.PUBLIC_NETWORK_PASSPHRASE` +network_passphrase = Network.TESTNET_NETWORK_PASSPHRASE + +# Keys for accounts to issue and receive the new asset +issuing_keypair = Keypair.from_secret( + "SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4" +) +issuing_public = issuing_keypair.public_key + +# Transactions require a valid sequence number that is specific to this account. +# We can fetch the current sequence number for the source account from Horizon. +issuing_account = server.load_account(issuing_public) + +transaction = ( + TransactionBuilder( + source_account=issuing_account, + network_passphrase=network_passphrase, + base_fee=100, + ) + .append_set_options_op( + set_flags=AuthorizationFlag.AUTHORIZATION_REVOCABLE | AuthorizationFlag.AUTHORIZATION_REQUIRED + ) + .build() +) +transaction.sign(issuing_keypair) +try: + transaction_resp = server.submit_transaction(transaction) + print(f"Transaction Resp:\n{transaction_resp}") +except BaseHorizonError as e: + print(f"Error: {e}") +``` + + diff --git a/docs/issuing-assets/how-to-issue-an-asset.mdx b/docs/issuing-assets/how-to-issue-an-asset.mdx new file mode 100644 index 000000000..6e0630ef0 --- /dev/null +++ b/docs/issuing-assets/how-to-issue-an-asset.mdx @@ -0,0 +1,340 @@ +--- +title: Issue an Asset +order: 20 +--- + +import { CodeExample } from "@site/src/components/CodeExample"; +import { Alert } from "@site/src/components/Alert"; + +There is no dedicated operation to create an asset on Stellar. Instead, assets are created with a [payment operation](../start/list-of-operations.mdx#payment): an issuing account makes a payment using the asset it’s issuing, and that payment actually creates the asset on the network. + +It’s a pretty simple process that requires four operations: one to create an issuing account, one to create a distribution account, one to establish a trustline, and one to make a payment. + + + +Note: you don't actually have to issue assets to a dedicated distribution account: you can issue them to any account with the requisite trustline. However, using a distribution account is the recommended practice, and it makes the process a lot easier to explain. So that's how we'll do it in this guide. + + + +The code to create those operations and submit them as transactions is below. Here, we’ll walk through each step so that the process makes sense. You can breeze through to get a general understanding, or you can use the [Stellar laboratory](https://www.stellar.org/laboratory/), which is an interface that allows you to create and submit transactions, to actually follow along and issue a token right here right now. + +One caveat: if you are creating a token on the public network, there is an additional prerequisite. You need a funded account to provide the XLM necessary to create the issuing and distribution accounts. + +## Create the Issuing and Distribution Accounts + +The issuing account is the origin of the asset, and will be forever linked to the asset’s identity. The distribution account is the first recipient of the asset. In the final step of this process, you’ll create the asset by sending a payment from the issuing account to the distribution account. + +There are two steps to account creation: + +1. Generate a keypair +1. Fund the account using a `create_account` operation + +You can generate a keypair for free, but an account doesn’t exist on the Stellar ledger until it is funded with XLM to cover the minimum balance. + +If you’re issuing an asset on the testnet, you can fund your accounts by getting free test XLM from Friendbot. If you’re issuing an asset in production, you will need to use an existing account to send enough live XLM to cover the minimum balance, transaction fees, and, in the case of the distribution account, a trustline. If you’re not sure where to acquire XLM, consult our [Lumen Buying Guide](https://www.stellar.org/lumens/exchanges#cryptocurrency-exchanges). + +Rule of thumb for production: don’t skirt too close to the minimum network balance. If you do, you may not have enough XLM to do what you need to do. Since the network minimum balance and transaction fees are low, it doesn’t require much to get started. 5 XLM should be sufficient. 100 XLM is even better. + +### Why Have Separate Accounts for Issuing and Distribution? + +Distributing assets through a distribution account is a design pattern. Functionally, you can do away with the distribution account and distribute directly from the issuer account. A less-known fact is that you can even create a market directly from the issuing account and issue by trading. + +With that said, there are two main reasons to use a distribution account: + +1. Security +1. Auditing + +The account you use to distribute your asset from is going to be a _hot_ account, meaning that some web service out there has direct access to sign its transactions. + +If the account you use to distribute your asset _is also the issuing account_ and is compromised by a malicious actor, _that actor can now issue as much of your asset as they want_. This is a hostile takeover of the asset and can increase your potential off-chain liabilities. If the malicous actor redeems these newly issued tokens with the anchor service, the anchor may not have the liquidity to support customers' withdrawals. + +If the account you use to distribute your asset _is not the issuing account_, then the stakes are lower. Once discovered, the issuer account can effectively freeze the compromised account's asset balance and start fresh with a new distribution account. This is possible without changing the issuing account. + +The second reason is bookkeeping or auditability. The issuing account can't actually hold a balance of its own asset. If you have standing inventory of your own asset in a separate account, it is easier to track. This is a common pattern in various ledgering solutions. + +As an added bonus, distribution accounts decouple our ecosystem standards from issuance. This allows ecosystem participants to come up with interesting concepts like non-issuing anchors _without_ actually changing protocols. + +## Create a Trustline + +Stellar requires accounts to explicitly opt-in to holding an asset by creating a trustline, and in this step the distribution account submits a `change_trust` operation to do just that. The trustline goes from the distribution account to the issuing account. + +The distribution account specifies the issuer’s public key and the asset code in the `change_trust` operation, and since it’s the first time the asset code appears on the ledger, that means the _distribution_ account actually names the asset, not the issuing account. + +Currently, there are two supported formats for asset codes. + +- **Alphanumeric 4-character maximum**: Any characters from the set [a-z][A-Z][0-9] are allowed. The code can be shorter than 4 characters, but the trailing characters must all be empty. +- **Alphanumeric 12-character maximum**: Any characters from the set [a-z][A-Z][0-9] are allowed. The code can be any number of characters from 5 to 12, but the trailing characters must all be empty. + +Any asset code works provided it falls into one of those two buckets. That said, if you’re issuing a currency, you should use the appropriate [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217) code, and if you’re issuing a stock or bond, the appropriate [ISIN number](https://en.wikipedia.org/wiki/International_Securities_Identification_Number). Doing so makes it easier for Stellar interfaces to properly display and sort your token in their listings, and allows potential token holders to understand, at a glance, what your token represents. + +## Make a Payment + +This is the step where the magic happens. The issuing account makes a payment to the distribution account using the newly named asset, and tokens exist where before there were none. Presto! + +As long as the issuing account remains unlocked, it can continue to create new tokens by making payments to the distribution account, or to any other account with the requisite trustline. + +If you’re planning to do, really, anything with your asset, your next step is to [complete a stellar.toml file](./publishing-asset-info.mdx) to provide wallets, exchanges, market listing services, and potential token holders with the information they need to understand what it represents. + +Once you’ve done that, you can also create a sell offer to get your asset onto the Stellar decentralized exchange, and put some effort into market making to create liquidity for it. + +## Sample Code + + + +```js +var StellarSdk = require("stellar-sdk"); +var server = new StellarSdk.Server("https://horizon-testnet.stellar.org"); + +// Keys for accounts to issue and receive the new asset +var issuingKeys = StellarSdk.Keypair.fromSecret( + "SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4", +); +var receivingKeys = StellarSdk.Keypair.fromSecret( + "SDSAVCRE5JRAI7UFAVLE5IMIZRD6N6WOJUWKY4GFN34LOBEEUS4W2T2D", +); + +// Create an object to represent the new asset +var astroDollar = new StellarSdk.Asset("AstroDollar", issuingKeys.publicKey()); + +// First, the receiving account must trust the asset +server + .loadAccount(receivingKeys.publicKey()) + .then(function (receiver) { + var transaction = new StellarSdk.TransactionBuilder(receiver, { + fee: 100, + networkPassphrase: StellarSdk.Networks.TESTNET, + }) + // The `changeTrust` operation creates (or alters) a trustline + // The `limit` parameter below is optional + .addOperation( + StellarSdk.Operation.changeTrust({ + asset: astroDollar, + limit: "1000", + }), + ) + // setTimeout is required for a transaction + .setTimeout(100) + .build(); + transaction.sign(receivingKeys); + return server.submitTransaction(transaction); + }) + .then(console.log) + + // Second, the issuing account actually sends a payment using the asset + .then(function () { + return server.loadAccount(issuingKeys.publicKey()); + }) + .then(function (issuer) { + var transaction = new StellarSdk.TransactionBuilder(issuer, { + fee: 100, + networkPassphrase: StellarSdk.Networks.TESTNET, + }) + .addOperation( + StellarSdk.Operation.payment({ + destination: receivingKeys.publicKey(), + asset: astroDollar, + amount: "10", + }), + ) + // setTimeout is required for a transaction + .setTimeout(100) + .build(); + transaction.sign(issuingKeys); + return server.submitTransaction(transaction); + }) + .then(console.log) + .catch(function (error) { + console.error("Error!", error); + }); +``` + +```java +Server server = new Server("https://horizon-testnet.stellar.org"); + +// Keys for accounts to issue and receive the new asset +KeyPair issuingKeys = KeyPair + .fromSecretSeed("SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4"); +KeyPair receivingKeys = KeyPair + .fromSecretSeed("SDSAVCRE5JRAI7UFAVLE5IMIZRD6N6WOJUWKY4GFN34LOBEEUS4W2T2D"); + +// Create an object to represent the new asset +Asset astroDollar = Asset.createNonNativeAsset("AstroDollar", issuingKeys.getAccountId()); + +// First, the receiving account must trust the asset +AccountResponse receiving = server.accounts().account(receivingKeys.getAccountId()); +Transaction allowAstroDollars = new Transaction.Builder(receiving, Network.TESTNET) + .addOperation( + // The `ChangeTrust` operation creates (or alters) a trustline + // The second parameter limits the amount the account can hold + new ChangeTrustOperation.Builder(astroDollar, "1000").build()) + .build(); +allowAstroDollars.sign(receivingKeys); +server.submitTransaction(allowAstroDollars); + +// Second, the issuing account actually sends a payment using the asset +AccountResponse issuing = server.accounts().account(issuingKeys.getAccountId()); +Transaction sendAstroDollars = new Transaction.Builder(issuing, Network.TESTNET) + .addOperation( + new PaymentOperation.Builder(receivingKeys.getAccountId(), astroDollar, "10").build()) + .build(); +sendAstroDollars.sign(issuingKeys); +server.submitTransaction(sendAstroDollars); +``` + +```go +package main + +import ( + "github.com/stellar/go/clients/horizonclient" + "github.com/stellar/go/keypair" + "github.com/stellar/go/network" + "github.com/stellar/go/txnbuild" + "log" +) + +func main() { + client := horizonclient.DefaultTestNetClient + + // Remember, these are just examples, so replace them with your own seeds. + issuerSeed := "SDR4C2CKNCVK4DWMTNI2IXFJ6BE3A6J3WVNCGR6Q3SCMJDTSVHMJGC6U" + distributorSeed := "SBUW3DVYLKLY5ZUJD5PL2ZHOFWJSVWGJA47F6FLO66UUFZLUUA2JVU5U" + + /* + * We omit error checks here for brevity, but you should always check your + * return values. + */ + + // Keys for accounts to issue and distribute the new asset. + issuer, err := keypair.ParseFull(issuerSeed) + distributor, err := keypair.ParseFull(distributorSeed) + + request := horizonclient.AccountRequest{AccountID: issuer.Address()} + issuerAccount, err := client.AccountDetail(request) + + request = horizonclient.AccountRequest{AccountID: distributor.Address()} + distributorAccount, err := client.AccountDetail(request) + + // Create an object to represent the new asset + astroDollar := txnbuild.CreditAsset{Code: "AstroDollar", Issuer: issuer.Address()} + + // First, the receiving (distribution) account must trust the asset from the + // issuer. + tx, err := txnbuild.NewTransaction( + txnbuild.TransactionParams{ + SourceAccount: distributorAccount.AccountID, + IncrementSequenceNum: true, + BaseFee: txnbuild.MinBaseFee, + Timebounds: txnbuild.NewInfiniteTimeout(), + Operations: []txnbuild.Operation{ + &txnbuild.ChangeTrust{ + Line: astroDollar, + Limit: "5000", + }, + }, + }, + ) + + signedTx, err := tx.Sign(network.TestNetworkPassphrase, distributor) + resp, err := client.SubmitTransaction(signedTx) + if err != nil { + log.Fatal(err) + } else { + log.Printf("Trust: %s\n", resp.Hash) + } + + // Second, the issuing account actually sends a payment using the asset + tx, err = txnbuild.NewTransaction( + txnbuild.TransactionParams{ + SourceAccount: issuerAccount.AccountID, + IncrementSequenceNum: true, + BaseFee: txnbuild.MinBaseFee, + Timebounds: txnbuild.NewInfiniteTimeout(), + Operations: []txnbuild.Operation{ + &txnbuild.Payment{ + Destination: distributor.Address(), + Asset: astroDollar, + Amount: "10", + }, + }, + }, + ) + + signedTx, err = tx.Sign(network.TestNetworkPassphrase, issuer) + resp, err = client.SubmitTransaction(signedTx) + + if err != nil { + log.Fatal(err) + } else { + log.Printf("Pay: %s\n", resp.Hash) + } +} +``` + +```python +from stellar_sdk import Asset, Keypair, Network, Server, TransactionBuilder + +# Configure Stellar SDK to talk to the horizon instance hosted by Stellar.org +# To use the live network, set the hostname to 'https://horizon.stellar.org' +server = Server(horizon_url="https://horizon-testnet.stellar.org") +# Use test network, if you need to use public network, please set it to `Network.PUBLIC_NETWORK_PASSPHRASE` +network_passphrase = Network.TESTNET_NETWORK_PASSPHRASE + +# Keys for accounts to issue and receive the new asset +issuing_keypair = Keypair.from_secret( + "SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4" +) +issuing_public = issuing_keypair.public_key + +distributor_keypair = Keypair.from_secret( + "SDSAVCRE5JRAI7UFAVLE5IMIZRD6N6WOJUWKY4GFN34LOBEEUS4W2T2D" +) +distributor_public = distributor_keypair.public_key + +# Transactions require a valid sequence number that is specific to this account. +# We can fetch the current sequence number for the source account from Horizon. +distributor_account = server.load_account(distributor_public) + +# Create an object to represent the new asset +astro_dollar = Asset("AstroDollar", issuing_public) + +# First, the receiving account must trust the asset +trust_transaction = ( + TransactionBuilder( + source_account=distributor_account, + network_passphrase=network_passphrase, + base_fee=100, + ) + # The `changeTrust` operation creates (or alters) a trustline + # The `limit` parameter below is optional + .append_change_trust_op(asset=astro_dollar, limit="1000") + .set_timeout(100) + .build() +) + +trust_transaction.sign(distributor_keypair) +trust_transaction_resp = server.submit_transaction(trust_transaction) +print(f"Change Trust Transaction Resp:\n{trust_transaction_resp}") + +issuing_account = server.load_account(issuing_public) +# Second, the issuing account actually sends a payment using the asset. +payment_transaction = ( + TransactionBuilder( + source_account=issuing_account, + network_passphrase=network_passphrase, + base_fee=100, + ) + .append_payment_op( + destination=distributor_public, + asset=astro_dollar, + amount="10", + ) + .build() +) +payment_transaction.sign(issuing_keypair) +payment_transaction_resp = server.submit_transaction(payment_transaction) +print(f"Payment Transaction Resp:\n{payment_transaction_resp}") +``` + + + +Naturally, the balances for the distributor's account will now hold both XLM and our new Astrodollars. diff --git a/docs/issuing-assets/index.mdx b/docs/issuing-assets/index.mdx new file mode 100644 index 000000000..77042aee7 --- /dev/null +++ b/docs/issuing-assets/index.mdx @@ -0,0 +1,14 @@ +--- +title: Overview +order: 10 +--- + +The ability to issue assets is a core feature of Stellar. In a few simple operations, you can create Stellar-network tokens, and this section of the docs will show you how. + +The possibilities are endless: any asset can be tokenized, and, once tokenized, transferred or traded over the Stellar network quickly and cheaply. Since any account can issue an asset on the Stellar network and anyone can set up a Stellar account, _anyone_ can issue an asset: banks, payment processors, money service businesses of all stripes, for-profit enterprises, nonprofits, local communities, even individuals. It’s a self-serve process, no permission needed. + +In addition to making it easy to issue an asset, Stellar also provides built-in mechanisms that allow you to tune your asset to specific use cases. You can — and should — [publish important identifying information](publishing-asset-info.mdx) about your asset so that wallets and consumers know what it represents, and it's easy to link that info to your asset in a single step. To comply with regulations, you can [control access](control-asset-access.mdx) to your asset using protocol-level flags. To take advantage of Stellar’s global reach, you can list your asset on the Stellar decentralized exchange, and use [market making bots](https://kelpbot.io/) to ensure necessary liquidity. + +Currently, the biggest use case for Stellar is the tokenization of fiat currency to optimize processes like cross-border payments, so there’s also a whole subsequent section — [Enable Deposit and Withdrawal](../anchoring-assets/enabling-deposit-and-withdrawal/index.mdx) — that focuses on how to connect Stellar tokens to existing rails to allow users to easily deposit real-world assets in exchange for them, and, on the flipside, to redeem them for real-world assets. Check that section if you're interested in creating an accessible on/off ramp that interoperates with wallets for seamless handling of user KYC, deposits, and withdrawals. + +As more and more developers and businesses explore other possibilities, we’ll expand the docs to cover emerging ideas and applications. diff --git a/docs/issuing-assets/metadata.json b/docs/issuing-assets/metadata.json new file mode 100644 index 000000000..ea2535805 --- /dev/null +++ b/docs/issuing-assets/metadata.json @@ -0,0 +1,4 @@ +{ + "order": 30, + "title": "Issue Assets" +} diff --git a/docs/issuing-assets/publishing-asset-info.mdx b/docs/issuing-assets/publishing-asset-info.mdx new file mode 100644 index 000000000..8769ee1d7 --- /dev/null +++ b/docs/issuing-assets/publishing-asset-info.mdx @@ -0,0 +1,314 @@ +--- +title: Publish Information About an Asset +order: 40 +--- + +import { CodeExample } from "@site/src/components/CodeExample"; +import { Alert } from "@site/src/components/Alert"; + +When you issue an asset, it’s crucial to provide clear information about what it represents. On Stellar, you do that by linking your issuing account to a home domain, publishing a [Stellar info file](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0001.md) on that domain, and making sure that file is complete and accurate. + +The most successful asset issuers give exchanges, wallets, and potential buyers lots of information about themselves in order to establish trust. More information in your Stellar Info File will mean: + +- Your asset gets _more_ exposure, and is listed on _more_ exchanges. +- Your asset holders are _more_ confident in you and the assets you issue. +- Your project will most likely be _more_ successful! + +The Stellar ticker, which is the source of market data for sites like CoinMarketCap, only includes assets with valid Stellar info files. Trading interfaces like StellarX, Stellarport, and StellarTerm and wallets like Lobstr and Solar use Stellar info files to populate their listings, and to decide if and how to present assets to their users. Any and all ecosystem integrations that allow for interoperability — from federation to in-app deposit and withdrawal — rely on information in your Stellar info detailing your Stellar setup. + +Completing your Stellar info file is not a step you can skip. + +## What is a Stellar info file? + +The Stellar info file is a common place where the Internet can find information about your organization’s Stellar integration. You write it in TOML, a simple and widely used configuration file format designed to be readable by both humans and machines, and publish it at `https://YOUR_DOMAIN/.well-known/stellar.toml`. + +That way, everyone knows where to find it, anyone can look it up, and it _proves_ that the owner of the HTTPS domain hosting the stellar.toml claims _responsibility_ for the accounts and assets listed in it. + +Using a [`set_options`](../start/list-of-operations.mdx#set-options) operation, you can link your Stellar account to the domain that hosts your Stellar info file, thereby creating a definitive on-chain connection between this information and that account. + +## How to complete your stellar.toml + +Stellar Ecosystem Proposals are open protocols for building on top of Stellar, and the very first SEP, aptly named [SEP-1: Stellar Info File](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0001.md), specifies everything you could ever want to include in a Stellar info file. This guide, which is targeted toward asset issuers, won’t cover the `Validator Information` section (that’s covered in the [Run a Core Node section](../run-core-node/index.mdx)), and may omit some details relevant to your use case. + +The goal here is to walk through the sections of [SEP-1: Stellar Info File](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0001.md) that directly relate to asset issuers, so you should use this guide in conjunction with that SEP to make sure you complete your `stellar.toml` correctly. The four sections we’ll cover: + +- General Information +- Organization Documentation +- Point of Contact Documentation +- Currency Documentation + +For each of those sections, we’ll let you know which fields are **required**, meaning all asset issuers _must_ include them to be listed by exchanges and wallets, and which fields are **suggested**. Completing suggested fields is a good way to make your asset stand out. + + + +Note: it's a good idea to keep the sections in the order presented in [SEP-1: Stellar Info File](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0001.md), which is also the order they're presented here. TOML requires arrays to be at the end, so if you scramble the order, you may cause errors for TOML parsers. + + + +### General Information + +There is one field in the General Information section required for _all_ token issuers: + +- `ACCOUNTS`: A list of **public keys** for all the Stellar accounts associated with your asset. + +Listing your public keys lets users confirm that you, in fact, own them. For example, when https://google.com hosts a stellar.toml file, users can be sure that _only_ the accounts listed on it belong to Google. If someone then says, "You need to pay your Google bill this month, send payment to address GIAMGOOGLEIPROMISE", but that key is not listed on Google's stellar.toml, then users know to not trust it. + +In addition, there are several fields where you list information about your Stellar integration to aid in discoverability. If you are an anchor service, and you have [set up infrastructure](../anchoring-assets/enabling-deposit-and-withdrawal/index.mdx) to interoperate with wallets and allow for in-app deposit and withdrawal of assets, make sure to include the locations of your servers on your stellar.toml file so those wallets know where to find relevant endpoints to query. In particular, list your: + +- `TRANSFER_SERVER` if you support [SEP-6: Deposit and Withdrawal API](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0006.md) +- `TRANSFER_SERVER_SEP0024`if you support [SEP-24: Interactive Deposit and Withdrawal](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md) +- `KYC_SERVER` if you support [SEP-12: KYC API](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0012.md) +- `WEB_AUTH_ENDPOINT` if you support [SEP-10: Stellar Web Authentication](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md) +- `DIRECT_PAYMENT_SERVER` if you support [SEP-31: Cross-Border Payments API](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0031.md) + +If you support other Stellar Ecosystem Proposals — such as federation or delegated signing — or host a public Horizon instance that other people can use to query the ledger, you should also add the location of those resources to [General Information](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0001.md#general-information) so they're discoverable. + +### Organization Documentation + +Basic information about your organization goes into a TOML **table** called `[DOCUMENTATION]`. Organization Documentation is your chance to inform exchanges and buyers about your business, and to demonstrate that your business is legitimate and trustworthy. + +#### Required + +- `ORG_NAME` The legal name of your organization, and if your business has one, its official `ORG_DBA`. +- `ORG_URL` The HTTPS URL of your organization's official website. In order to prove the website is yours, _you must host your stellar.toml on the same domain you list here._ That way, exchanges and buyers can view the SSL certificate on your website, and feel reasonably confident that you are who you say you are. +- `ORG_LOGO` A URL to a company logo, which will show up next to your organization on exchanges. This image should be a square aspect ratio transparent PNG, ideally of size 128x128. If you fail to provide a logo, the icon next to your organization will appear blank on many exchanges. +- `ORG_PHYSICAL_ADDRESS` The physical address of your organization. We understand you might want to keep your work address private. At the very least, you should put the _city_ and _country_ in which you operate. A street address is ideal and provides a higher level of trust and transparency to your potential asset holders. +- `ORG_OFFICIAL_EMAIL` The best business email address for your organization. This should be hosted at the same domain as your official website. +- `ORG_SUPPORT_EMAIL` The best email for support requests. + +#### Suggested + +- `ORG_GITHUB` Your organization's official Github account. +- `ORG_KEYBASE` Your organization's official Keybase account. Your Keybase account should contain proof of ownership of any public online accounts you list here, including your organization's domain. +- `ORG_TWITTER` Your organization's official Twitter handle. +- `ORG_DESCRIPTION` A description of your organization. This is fairly open-ended, and you can write as much as you want. It's a great place to distinguish yourself by describing what it is that you do. + +Issuers that list verified information including phone/address attestations and Keybase verifications are prioritized by Stellar clients. + +### Point of Contact Documentation + +Information about the primary point(s) of contact for your organization goes into a TOML [array of tables](https://github.com/toml-lang/toml#array-of-tables) called `[[PRINCIPALS]]`. You need to put contact information for _at least one person_ at your organization. If you don't, exchanges can't verify your offering, and it is unlikely that buyers will be interested. Multiple principals can be added with additional `[[PRINCIPALS]]` entries. + +#### Required + +- `name` The name of the primary contact. +- `email` The primary contact's official email address. This should be hosted at the same domain as your organization's official website. + +#### Suggested + +- `github` The personal Github account of the point of contact. +- `twitter` The personal Twitter handle of the point of contact. +- `keybase` The personal Keybase account for the point of contact. This account should contain proof of ownership of any public online accounts listed here and may contain proof of ownership of your organization's domain. + +### Currency Documentation + +Information about the asset(s) you issue goes into a TOML [array of tables](https://github.com/toml-lang/toml#array-of-tables) called `[[CURRENCIES]]`. If you issue multiple assets, you can include them all in one stellar.toml. Each asset should have its own `[[CURRENCIES]]` entry. + +(These entries are also used for assets you support but don’t issue, but as this section focuses on issuing assets the language will reflect that.) + +#### Required + +- `code` The asset code. This is one of two key pieces of information that identify your token. Without it, your token cannot be listed anywhere. +- `issuer` The Stellar public key of the issuing account. This is the second key piece of information that identifies your token. Without it, your token cannot be listed anywhere. +- `is_asset_anchored` An indication of whether your token is anchored or native: `true` if your token can be redeemed for an asset outside the Stellar network, `false` if it can’t. Exchanges use this information to sort tokens by type in listings. If you fail to provide it, your token is unlikely to show up in filtered market views. + +If you're issuing anchored (tethered, stablecoin, asset-backed) tokens, there are several additional **required** fields: + +- `anchor_asset_type` The type of asset your token represents. The possible categories are `fiat`, `crypto`, `stock`, `bond`, `commodity`, `realestate`, and `other`. +- `anchor_asset` The name of the asset that serves as the anchor for your token. +- `redemption_instructions` Instructions to redeem your token for the underlying asset. +- `attestation_of_reserve` A URL to attestation or other proof, evidence, or verification of reserves, such as third-party audits, which all issuers of stablecoins should offer to adhere to best practices. + +#### Suggested + +- `desc` A description of your token and what it represents. This is a good place to clarify what your token does, and why someone might want to own it. +- `conditions` Any conditions you place on the redemption of your token. +- `image` A URL to a PNG or GIF image with a transparent background representing your token. Without it, your token will appear blank on many exchanges. + +## How to publish your Stellar info file + +After you've followed the steps above to complete your Stellar info file, post it at the following location: + +`https://YOUR_DOMAIN/.well-known/stellar.toml` + +Enable [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) so people can access this file from other sites, and set the following header for an HTTP response for a `/.well-known/stellar.toml` file request. + +`Access-Control-Allow-Origin: *` + +Set a `text/plain` content type so that browsers render the contents, rather than prompting for a download. + +`content-type: text/plain` + +You should also use the `set_options` operation to set the home domain on your issuing account. + +## Sample code to set the home domain of your issuing account + + + +```js +var StellarSdk = require("stellar-sdk"); +var server = new StellarSdk.Server("https://horizon-testnet.stellar.org"); + +// Keys for issuing account +var issuingKeys = StellarSdk.Keypair.fromSecret( + "SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4", +); + +server + .loadAccount(issuingKeys.publicKey()) + .then(function (issuer) { + var transaction = new StellarSdk.TransactionBuilder(issuer, { + fee: 100, + networkPassphrase: StellarSdk.Networks.TESTNET, + }) + .addOperation( + StellarSdk.Operation.setOptions({ + homeDomain: "yourdomain.com", + }), + ) + // setTimeout is required for a transaction + .setTimeout(100) + .build(); + transaction.sign(issuingKeys); + return server.submitTransaction(transaction); + }) + .then(console.log) + .catch(function (error) { + console.error("Error!", error); + }); +``` + +```java +Server server = new Server("https://horizon-testnet.stellar.org"); + +// Keys for issuing account +KeyPair issuingKeys = KeyPair + .fromSecretSeed("SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4"); +AccountResponse sourceAccount = server.accounts().account(issuingKeys.getAccountId()); + +Transaction setHomeDomain = new Transaction.Builder(sourceAccount, Network.TESTNET) + .addOperation(new SetOptionsOperation.Builder() + .setHomeDomain("yourdomain.com").build()) + .build(); +setHomeDomain.sign(issuingKeys); +server.submitTransaction(setHomeDomain); +``` + +```python +from stellar_sdk import Keypair, Network, Server, TransactionBuilder +from stellar_sdk.exceptions import BaseHorizonError + +# Configure Stellar SDK to talk to the horizon instance hosted by Stellar.org +# To use the live network, set the hostname to 'https://horizon.stellar.org' +server = Server(horizon_url="https://horizon-testnet.stellar.org") +# Use the test network, if you want to use the live network, please set it to `Network.PUBLIC_NETWORK_PASSPHRASE` +network_passphrase = Network.TESTNET_NETWORK_PASSPHRASE + +# Keys for accounts to issue and receive the new asset +issuing_keypair = Keypair.from_secret( + "SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4" +) +issuing_public = issuing_keypair.public_key + + +# Transactions require a valid sequence number that is specific to this account. +# We can fetch the current sequence number for the source account from Horizon. +issuing_account = server.load_account(issuing_public) + +transaction = ( + TransactionBuilder( + source_account=issuing_account, + network_passphrase=network_passphrase, + base_fee=100, + ) + .append_set_options_op( + home_domain="yourdomain.com" + ) + .build() +) +transaction.sign(issuing_keypair) +try: + transaction_resp = server.submit_transaction(transaction) + print(f"Transaction Resp:\n{transaction_resp}") +except BaseHorizonError as e: + print(f"Error: {e}") +``` + + + +## Sample stellar.toml + + + +```toml +NETWORK_PASSPHRASE="Public Global Stellar Network ; September 2015" +FEDERATION_SERVER="https://api.domain.com/federation" +AUTH_SERVER="https://api.domain.com/auth" +TRANSFER_SERVER="https://api.domain.com" +SIGNING_KEY="GBBHQ7H4V6RRORKYLHTCAWP6MOHNORRFJSDPXDFYDGJB2LPZUFPXUEW3" +HORIZON_URL="https://horizon.domain.com" +ACCOUNTS=[ +"GD5DJQDDBKGAYNEAXU562HYGOOSYAEOO6AS53PZXBOZGCP5M2OPGMZV3", +"GAENZLGHJGJRCMX5VCHOLHQXU3EMCU5XWDNU4BGGJFNLI2EL354IVBK7", +"GAOO3LWBC4XF6VWRP5ESJ6IBHAISVJMSBTALHOQM2EZG7Q477UWA6L7U" +] +VERSION="2.0.0" + +[DOCUMENTATION] +ORG_NAME="Organization Name" +ORG_DBA="Organization DBA" +ORG_URL="https://www.domain.com" +ORG_LOGO="https://www.domain.com/awesomelogo.png" +ORG_DESCRIPTION="Description of issuer" +ORG_PHYSICAL_ADDRESS="123 Sesame Street, New York, NY 12345, United States" +ORG_PHYSICAL_ADDRESS_ATTESTATION="https://www.domain.com/address_attestation.jpg" +ORG_PHONE_NUMBER="1 (123)-456-7890" +ORG_PHONE_NUMBER_ATTESTATION="https://www.domain.com/phone_attestation.jpg" +ORG_KEYBASE="accountname" +ORG_TWITTER="orgtweet" +ORG_GITHUB="orgcode" +ORG_OFFICIAL_EMAIL="support@domain.com" + +[[PRINCIPALS]] +name="Jane Jedidiah Johnson" +email="jane@domain.com" +keybase="crypto_jane" +twitter="crypto_jane" +github="crypto_jane" +id_photo_hash="be688838ca8686e5c90689bf2ab585cef1137c999b48c70b92f67a5c34dc15697b5d11c982ed6d71be1e1e7f7b4e0733884aa97c3f7a339a8ed03577cf74be09" +verification_photo_hash="016ba8c4cfde65af99cb5fa8b8a37e2eb73f481b3ae34991666df2e04feb6c038666ebd1ec2b6f623967756033c702dde5f423f7d47ab6ed1827ff53783731f7" + +[[CURRENCIES]] +code="USD" +issuer="GCZJM35NKGVK47BB4SPBDV25477PZYIYPVVG453LPYFNXLS3FGHDXOCM" +display_decimals=2 + +[[CURRENCIES]] +code="BTC" +issuer="GAOO3LWBC4XF6VWRP5ESJ6IBHAISVJMSBTALHOQM2EZG7Q477UWA6L7U" +display_decimals=7 +anchor_asset_type="crypto" +anchor_asset="BTC" +redemption_instructions="Use SEP6 with our federation server" +collateral_addresses=["2C1mCx3ukix1KfegAY5zgQJV7sanAciZpv"] +collateral_address_signatures=["304502206e21798a42fae0e854281abd38bacd1aeed3ee3738d9e1446618c4571d10"] + +# asset with meta info +[[CURRENCIES]] +code="GOAT" +issuer="GD5T6IPRNCKFOHQWT264YPKOZAWUMMZOLZBJ6BNQMUGPWGRLBK3U7ZNP" +display_decimals=2 +name="goat share" +desc="1 GOAT token entitles you to a share of revenue from Elkins Goat Farm." +conditions="There will only ever be 10,000 GOAT tokens in existence. We will distribute the revenue share annually on Jan. 15th" +image="https://static.thenounproject.com/png/2292360-200.png" +fixed_number=10000 + +``` + + + +[sep-0001]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0001.md +[sep-0020]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0020.md +[seps]: https://github.com/stellar/stellar-protocol/tree/master/ecosystem +[twilio-guide]: https://support.twilio.com/hc/en-us/articles/223183008-Formatting-International-Phone-Numbers diff --git a/docs/metadata.json b/docs/metadata.json new file mode 100644 index 000000000..25b512673 --- /dev/null +++ b/docs/metadata.json @@ -0,0 +1,4 @@ +{ + "order": 0, + "title": "Documentation" +} diff --git a/docs/run-api-server/configuring.mdx b/docs/run-api-server/configuring.mdx new file mode 100644 index 000000000..4da4b8c63 --- /dev/null +++ b/docs/run-api-server/configuring.mdx @@ -0,0 +1,153 @@ +--- +title: Configuring +order: 30 +--- + +import { Alert } from "@site/src/components/Alert"; +import { CodeExample } from "@site/src/components/CodeExample"; + +Once Horizon is [installed](./installing.mdx), you are ready to configure it. Note that Horizon fulfills three important, distinct roles: + +- **serving requests** like a regular web-based API, +- **ingesting ledgers** from the Stellar network to keep its world-view up to date, and +- **transaction submission** for interacting with the Stellar network. + +Though we encourage operators to separate these responsibilities across instances for resilience and independent scaling, a single Horizon instance can perform all of these functions at once. + +For the remainder of this guide, we will assume that you want a single, standalone Horizon instance that performs ingestion and allows transaction submission. We'll cover ingestion in detail [later](./ingestion.mdx) if you want to read ahead and decide which approach is right for you. + +## Parameters + +Horizon can be configured by both command line flags and environment variables. To see Horizon's list of available command line flags, their default values, and their corresponding environmental variable names, run: + + + +```bash +stellar-horizon --help +``` + + + +You'll see that Horizon defines a large number of flags; however, only a handful are required to get started: + +| flag | envvar | example | +| ---- | ------ | ------- | +| `--db-url` | `DATABASE_URL` | postgres://localhost/horizon_testnet | +| `--history-archive-urls` | `HISTORY_ARCHIVE_URLS` | https://history.stellar.org/prd/core-testnet/core_testnet_001,https://history.stellar.org/prd/core-testnet/core_testnet_002 | + +- The most important parameter, `--db-url`, specifies the Horizon database; its value should be a valid [PostgreSQL Connection URI](http://www.postgresql.org/docs/9.6/static/libpq-connect.html#AEN46025). +- The other parameter, `--history-archive-urls`, specifies a set of comma-separated locations from which Horizon should download [history archives](../run-core-node/publishing-history-archives.mdx). + +#### With Ingestion + +As outlined at the beginning, we presume you are interested in starting an ingesting instance. For this, you need to specify some additional flags: + +| flag | envvar | example | +| ---- | ------ | ------- | +| `--captive-core-config-path` | `CAPTIVE_CORE_CONFIG_PATH` | /etc/default/stellar-captive-core.toml | +| `--stellar-core-binary-path` | `STELLAR_CORE_BINARY_PATH` | /usr/bin/stellar-core | +| `--captive-core-use-db` | `CAPTIVE_CORE_USE_DB` | true | + +Note that **ingestion is enabled by default**. + +- The first parameter, `--captive-core-config-path`, points to a Captive Core configuration file. This TOML file only requires a few fields (explained [below](#configuring-captive-core)) to get up and running. +- The second parameter, `--stellar-core-binary-path`, is a filesystem path to a Stellar Core binary. Horizon will actually search your PATH for `stellar-core` by default, so if your environment is configured appropriately, you don't need to pass this. +- The third parameter, `--captive-core-use-db`, by default this value is false, which means Captive Core ingestion will run with ledger states stored in RAM. When set to true, enables Captive Core ingestion to store ledger states in local SQLite database rather than in memory (RAM). As of this writing, ledger states require approximately 8GB, but this will continue to increase as the ledger grows over time. The database location is determined by the `DATABASE` parameter within the `--captive-core-config-path` file. By default, it is set to `sqlite3://stellar.db`, which resolves to runtime directory location derived from `--captive-core-storage-path`. + +#### Without Ingestion + +If you aren't configuring your Horizon instance to perform ingestion, it still needs awareness about what's going on in the Stellar network to be useful. Thus, you need to point Horizon to a running Stellar Core instance: + +| flag | envvar | example | +| ---- | ------ | ------- | +| `--ingest` | `INGEST` | false | +| `--stellar-core-url` | `STELLAR_CORE_URL` | http://127.0.0.1:11626 | + +This can be a [Remote Captive Core](./remote-core.mdx) instance with its underlying Core node exposed or a [standalone](../run-core-node/) Stellar-Core instance. + +### Manual Installation + +Specifying command line flags every time you invoke Horizon can be cumbersome, so we recommend using environment variables. There are many tools you can use to manage them, such as [direnv](http://direnv.net/) or [dotenv](https://github.com/bkeepers/dotenv). + +For configuration related to [Captive Core](#configuring-captive-core), you should prepare a separate TOML file and pass it to the `--captive-core-config-path`/`CAPTIVE_CORE_CONFIG_PATH` argument. + +### Package Manager Installation + +If you installed Horizon [via your package manager](./installing.mdx#package-manager), the provided `stellar-horizon-cmd` wrapper will import a configuration from `/etc/default/stellar-horizon` and set up the environment accordingly. Hence, if you want to change things, edit the configuration file in `/etc/default/stellar-horizon`. + + + +This script invokes Horizon with the `stellar` user, so make sure that permissions for this user are set up accordingly. For example: the `--captive-core-storage-path` (by default the current working directory) should be writable for this user; the user should be able to execute the `horizon` and `stellar-core` binaries; etc. + + + +Note that the default configuration (located at `/etc/default/stellar-horizon`) provided by the package manager **enables ingestion by default**. Again, refer to the later [Ingestion](./ingestion.mdx) page to see what setup is right for you. If you want certain nodes dedicated exclusively to fulfilling requests, you should set this flag to `false` accordingly. + +## Preparing the Database + +Before running the Horizon server, you must first prepare the Horizon database specified by the `DATABASE_URL`. This database will be used for all of the information produced by Horizon, most notably historical information about transactions that have occurred on the Stellar network. + +To prepare a database for Horizon's use, you must first ensure it is blank. It's easiest to create a new database on your PostgreSQL server specifically for Horizon's use (e.g. `createdb horizon`). Note that you may need to [add a role](https://www.postgresql.org/docs/9.6/sql-createrole.html) for yourself (or the `stellar` user) through the `postgres` user if you're starting from scratch. Next, install the schema by running `stellar-horizon db init`. This command will log any errors that occur. + +Remember to update the appropriate DB-related flags or environment variables to configure Horizon as explained [above](#parameters). + +### Postgres Configuration + +It is recommended to set `random_page_cost=1` in Postgres' configuration if you are using SSD storage. With this setting, Query Planner will make a better use of indices, especially for `JOIN` queries. We've noticed a huge speed improvement for some queries with this setting. + +To improve availability of both ingestion and frontend servers it's recommended to set the following values: + +- `tcp_keepalives_idle`: 10 seconds +- `tcp_keepalives_interval`: 1 second +- `tcp_keepalives_count`: 5 + +With the config above, if there are no queries from a given client for 10 seconds, Postgres should start sending TCP keepalive packets. It will retry 5 times every second. If there is no response from the client after that time it will drop the connection. + +## Configuring Captive Core + +While a full Stellar Core node requires a complex configuration with [lots of possible fields](https://github.com/stellar/stellar-core/blob/master/docs/stellar-core_example.cfg), the Captive Core configuration file can be kept extremely barebones. Most of the configuration will be generated automagically at runtime. Here's is a minimal working example, operating under the assumption that you want to connect to the testnet and trust SDF's validators exclusively: + + + +```toml +[[HOME_DOMAINS]] +HOME_DOMAIN="testnet.stellar.org" +QUALITY="HIGH" + +[[VALIDATORS]] +NAME="sdf_testnet_1" +HOME_DOMAIN="testnet.stellar.org" +PUBLIC_KEY="GDKXE2OZMJIPOSLNA6N6F2BVCI3O777I2OOC4BV7VOYUEHYX7RTRYA7Y" +ADDRESS="core-testnet1.stellar.org" +HISTORY="curl -sf http://history.stellar.org/prd/core-testnet/core_testnet_001/{0} -o {1}" + +[[VALIDATORS]] +NAME="sdf_testnet_2" +HOME_DOMAIN="testnet.stellar.org" +PUBLIC_KEY="GCUCJTIYXSOXKBSNFGNFWW5MUQ54HKRPGJUTQFJ5RQXZXNOLNXYDHRAP" +ADDRESS="core-testnet2.stellar.org" +HISTORY="curl -sf http://history.stellar.org/prd/core-testnet/core_testnet_002/{0} -o {1}" + +[[VALIDATORS]] +NAME="sdf_testnet_3" +HOME_DOMAIN="testnet.stellar.org" +PUBLIC_KEY="GC2V2EFSXN6SQTWVYA5EPJPBWWIMSD2XQNKUOHGEKB535AQE2I6IXV2Z" +ADDRESS="core-testnet3.stellar.org" +HISTORY="curl -sf http://history.stellar.org/prd/core-testnet/core_testnet_003/{0} -o {1}" +``` + + + +_(For the remainder of this guide, we'll assume this file lives at `/etc/default/stellar-captive-core.toml`.)_ + +The minimum required fields are the `[[HOME_DOMAINS]]` and a set of `[[VALIDATORS]]`. + +If you wanted to adapt this and configure your nodes to work on the Stellar **pubnet**, you'll need to think more carefully about the validators you want to trust in your quorum. As inspiration, [here](https://github.com/stellar/go/blob/master/services/horizon/docker/stellar-core-pubnet.cfg#L15-L202) is the set of domains and validators that SDF includes in its pubnet quorum. You should also familiarize yourself with how to configure a proper quorum set; the [Core documentation](../run-core-node/configuring.mdx#choosing-your-quorum-set) has more on this. + +Captive Core's functionality is controlled through this file. Note that while the Captive Core configuration looks like a subset of a traditional Stellar Core configuration file, you cannot use a traditional Stellar Core configuration file to configure Captive Core. The TOML format is preserved for operator ease of [migrating](./migrating.mdx) from Horizon 1.x, but this is a fundamentally different architecture and should be treated as such. + +## Remote Captive Core + +As mentioned [earlier](./installing.mdx#installing-remote-captive-core), we provide a library that wraps a Captive Core instance in an HTTP API that Horizon can consume remotely. This is an advanced (and still experimental) architecture, so refer to the [Remote Captive Core](./remote-core.mdx) page (up next) for more on setting that up. + +Otherwise, jump ahead to [Running Horizon](./running.mdx)! diff --git a/docs/run-api-server/index.mdx b/docs/run-api-server/index.mdx new file mode 100644 index 000000000..6d9f7f3f2 --- /dev/null +++ b/docs/run-api-server/index.mdx @@ -0,0 +1,24 @@ +--- +title: "Overview" +order: 0 +--- + +Horizon is responsible for providing an HTTP API to data in the Stellar network. It ingests and re-serves the data produced by the Stellar network in a form that is easier to consume by the average application relative to the performance-oriented data representations used by Stellar Core. + +This guide describes how to administer a production Horizon 2.0+ instance (you can refer to the [Developers' Blog](https://www.stellar.org/developers-blog/a-new-sun-on-the-horizon) for some background on the performance and architectural improvements of this major version bump). For information about developing on the Horizon codebase, check out the [Development Guide](https://github.com/stellar/go/blob/master/services/horizon/internal/docs/developing.md). + +Before we begin, it's worth reiterating the sentiment echoed in the [Run a Core Node](../run-core-node) guide: **we do not endorse running Horizon backed by a standalone Stellar Core instance**, and especially not by a _validating_ Stellar Core. These are two separate concerns, and decoupling them is important for both reliability and performance. Horizon instead manages its own, pared-down version of Stellar Core optimized for its own subset of needs (we'll refer to this as a "Captive Core" instance). + +## Upgrading From Horizon 1.x + +If you're coming from an existing deployment of Horizon, you're probably running it alongside a standalone "Watcher" Stellar Core node. As noted above, this architecture is now **deprecated**, and support for it will be dropped in Horizon 3.x. The [Migration](./migrating.mdx) guide should facilitate the process to moving to the Captive Core architecture and get you up to speed. + +## Why Run Horizon? + +You don't need to run your own Horizon instance to build on Stellar: the Stellar Development Foundation runs two Horizon servers, one for the public network and one for the test network: https://horizon.stellar.org and https://horizon-testnet.stellar.org. These servers are free for anyone to use and should be fine for development and small-scale projects. They are, however, rate limited, and we don't recommended using them for production services that need strong reliability. + +Running Horizon within your own infrastructure provides a number of benefits. You can: + +- Disable request rate limiting for guaranteed network access +- Have full operational control without dependency on the Stellar Development Foundation +- Run multiple instances for redundancy and scalability diff --git a/docs/run-api-server/ingestion.mdx b/docs/run-api-server/ingestion.mdx new file mode 100644 index 000000000..9939442aa --- /dev/null +++ b/docs/run-api-server/ingestion.mdx @@ -0,0 +1,309 @@ +--- +title: Ingestion +order: 45 +--- + +Horizon provides access to both current and historical state on the Stellar network through a process called **ingestion**. + +Horizon provides most of its utility through ingested data, and your Horizon server can be configured to listen for and ingest transaction results from the Stellar network. Ingestion enables API access to both current (e.g. someone's balance) and historical state (e.g. someone's transaction history). + +## Ingestion Types + +There are two primary ingestion use-cases for Horizon operations: + +- ingesting **live** data to stay up to date with the latest, real-time changes to the Stellar network, and +- ingesting **historical** data to peek how the Stellar ledger has changed over time + +### Ingesting Live Data + +Though this option is disabled by default, in this guide we've [assumed](./configuring.mdx) you turned it on. If you haven't, pass the `--ingest` flag or set `INGEST=true` in your environment. + +For a serious setup, **we highly recommend having more than one live ingesting instance**, as this makes it easier to avoid downtime during upgrades and adds resilience to your infrastructure, ensuring you always have the latest network data. + +### Ingesting Historical Data + +Providing API access to historical data is facilitated by a Horizon subcommand: + + + +``` +stellar-horizon db reingest range +``` + + + +_(The command name is a bit of a misnomer: you can use `reingest` both to ingest new ledger data and reingest old data.)_ + +You can run this process in the background while your Horizon server is up. It will continuously decrement the `history.elder_ledger` in your `/metrics` endpoint until the `` ledger is reached and the backfill is complete. If Horizon receives a request for a ledger it hasn't ingested, it returns a 503 error and clarify that it's `Still Ingesting` (see [below](#some-endpoints-are-not-available-during-state-ingestion)). + +#### Deciding on how much history to ingest + +You should think carefully about the amount of ingested data you'd like to keep around. Though the storage requirements for the entire Stellar network are substantial, **most organizations and operators only need a small fraction of the history** to fit their use case. For example, + +- If you just started developing a new application or service, you can probably get away with just doing live ingestion, since nothing you do requires historical data. + +- If you're moving an existing service away from reliance on SDF's Horizon, you likely only need history from the point at which you started using the Stellar network. + +- If you provide temporal guarantees to your users--a 6-month guarantee of transaction history like some online banks do, or history only for the last thousand ledgers (see [below](#managing-storage)), for example--then you similarly don't have heavy ingestion requirements. + +Even a massively-popular, well-established custodial service probably doesn't need full history to service its users. It will, however, need full history to be a [Full Validator](../run-core-node/index.mdx#full-validator) with published history archives. + +#### Reingestion + +Regardless of whether you are running live ingestion or building up historical data, you may occasionally need to _re_ingest ledgers anew (for example on certain upgrades of Horizon). For this, you use the same command as above. + +#### Parallel ingestion + +Note that historical (re)ingestion happens independently for any given ledger range, so you can reingest in parallel across multiple Horizon processes: + + + +``` +horizon1> stellar-horizon db reingest range 1 10000 +horizon2> stellar-horizon db reingest range 10001 20000 +horizon3> stellar-horizon db reingest range 20001 30000 +# ... etc. +``` + + + +#### Managing storage + +Over time, the recorded network history will grow unbounded, increasing storage used by the database. Horizon needs sufficient disk space to expand the data ingested from Stellar Core. Unless you need to maintain a [history archive](../run-core-node/publishing-history-archives.mdx), you should configure Horizon to only retain a certain number of ledgers in the database. + +This is done using the `--history-retention-count` flag or the `HISTORY_RETENTION_COUNT` environment variable. Set the value to the number of recent ledgers you wish to keep around, and every hour the Horizon subsystem will reap expired data. Alternatively, Horizon provides a command to force a collection: + + + +```bash +stellar-horizon db reap +``` + + + +If you configure this parameter, we also recommend [configuring](./configuring.mdx#configuring-captive-core) the `CATCHUP_RECENT` value for your Captive Core instance. + +### Common Issues + +Ingestion is a complicated process, so there are a number of things to look out for. + +#### Some endpoints are not available during state ingestion + +Endpoints that display state information are not available during initial state ingestion and will return a `503 Service Unavailable`/`Still Ingesting` error. An example is the `/paths` endpoint (built using offers). Such endpoints will become available after state ingestion is done (usually within a couple of minutes). + +#### State ingestion is taking a lot of time + +State ingestion shouldn't take more than a couple of minutes on an AWS `c5.xlarge` instance or equivalent. + +It's possible that the progress logs (see [below](#reading-the-logs)) will not show anything new for a longer period of time or print a lot of progress entries every few seconds. This happens because of the way history archives are designed. + +The ingestion is still working but it's processing entries of type `DEADENTRY`. If there is a lot of them in the bucket, there are no _active_ entries to process. We plan to improve the progress logs to display actual percentage progress so it's easier to estimate an ETA. + +If you see that ingestion is not proceeding for a very long period of time: + +1. Check the RAM usage on the machine. It's possible that system ran out of RAM and is using swap memory that is extremely slow. +1. If above is not the case, file a [new issue](https://github.com/stellar/go/issues/new/choose) in the [Horizon repository](https://github.com/stellar/go/tree/master/services/horizon). + +#### CPU usage goes high every few minutes + +**This is by design**. Horizon runs a state verifier routine that compares state in local storage to history archives every 64 ledgers to ensure data changes are applied correctly. If data corruption is detected, Horizon will block access to endpoints serving invalid data. + +We recommend keeping this security feature turned on; however, if it's causing problems (due to CPU usage) this can be disabled via the `--ingest-disable-state-verification`/`INGEST_DISABLE_STATE_VERIFICATION` parameter. + +## Ingesting Full Public Network History + +In some (albeit rare) cases, it can be convenient to (re)ingest the full Stellar Public Network history into Horizon (e.g. when running Horizon for the first time). Using multiple Captive Core workers on a high performance environment (powerful machines on which to run Horizon + a powerful database) makes this possible in ~1.5 days. + +The following instructions assume the reingestion is done on AWS. However, they should be applicable to any other environment with equivalent capacity. In the same way, the instructions can be adapted to reingest only specific parts of the history. + +### Prerequisites + +Before we begin, we make some assumptions around the environment required. Please refer to the [Prerequisites](./prerequisites.mdx) section for the current HW requirements to run Horizon reingestion for either historical catch up or real-time ingestion (for staying in sync with the ledger). A few things to keep in mind: + +1. For reingestion, the more parallel workers are provisioned to speed up the process, the larger the machine size is required in terms of RAM, CPU, IOPS and disk size. The size of the RAM per worker also increases over time (14GB RAM / worker as of mid 2022) due to the growth of the ledger. HW specs can be downsized once reingestion is completed. + +1. [Horizon](./installing.mdx) latest version installed on the machine from (1). + +1. [Core](https://github.com/stellar/stellar-core) latest version installed on the machine from (1). + +1. A Horizon database where to reingest the history. Preferably, the database should be empty to minimize storage (Postgres accumulates data during usage, which is only deleted when `VACUUM`ed) and have the minimum spec's for reingestion as outlined in [Prerequisites](./prerequisites.mdx). + +As the DB storage grows, the IO capacity will grow along with it. The number of workers (and the size of the instance created in (1), should be increased accordingly if we want to take advantage of it. To make sure we are minimizing reingestion time, we should watch write IOPS. It should ideally always be close to the theoretical limit of the DB. + +### Parallel Reingestion + +Once the prerequisites are satisfied, we can spawn two Horizon reingestion +processes in parallel: + +1. One for the first 17 million ledgers (which are almost empty). +1. Another one for the rest of the history. + +This is due to first 17 million ledgers being almost empty whilst the rest are much more packed. Having a single Horizon instance with enough workers to saturate the IO capacity of the machine for the first 17 million would kill the machine when reingesting the rest (during which there is a higher CPU and memory consumption per worker). + +64 workers for (1) and 20 workers for (2) saturates instance with RAM and 15K IOPS. Again, as the DB storage grows, a larger number of workers and faster storage should be considered. + +In order to run the reingestion, first set the following environment variables in the [configuration](./configuring.mdx) (updating values to match your database environment, of course): + + + +```bash +export DATABASE_URL=postgres://postgres:secret@db.local:5432/horizon +export APPLY_MIGRATIONS=true +export HISTORY_ARCHIVE_URLS=https://s3-eu-west-1.amazonaws.com/history.stellar.org/prd/core-live/core_live_001 +export NETWORK_PASSPHRASE="Public Global Stellar Network ; September 2015" +export STELLAR_CORE_BINARY_PATH=$(which stellar-core) +export ENABLE_CAPTIVE_CORE_INGESTION=true +# Number of ledgers per job sent to the workers. +# The larger the job, the better performance from Captive Core's perspective, +# but, you want to choose a job size which maximizes the time all workers are +# busy. +export PARALLEL_JOB_SIZE=100000 +# Retries per job +export RETRIES=10 +export RETRY_BACKOFF_SECONDS=20 + +# Enable optional config when running captive core ingestion + +# For stellar-horizon to download buckets locally at specific location. +# If not enabled, stellar-horizon would download data in the current working directory. +# export CAPTIVE_CORE_STORAGE_PATH="/var/lib/stellar" + +# For stellar-horizon to use local disk file for ledger states rather than in memory(RAM), approximately +# 8GB of space and increasing as size of ledger entries grows over time. +# export CAPTIVE_CORE_USE_DB=true +``` + + + +(Naturally, you can also edit the configuration file at `/etc/default/stellar-horizon` directly if you installed [from a package manager](./installing.mdx#package-manager).) + +If Horizon was previously running, first ensure it is stopped. Then, run the following commands in parallel: + +1. `stellar-horizon db reingest range --parallel-workers=64 1 16999999` +1. `stellar-horizon db reingest range --parallel-workers=20 17000000 ` + +(Where you can find `` under [SDF Horizon's](https://horizon.stellar.org/) `core_latest_ledger` field.) + +When saturating a database instance with 15K IOPS capacity: + +(1) should take a few hours to complete. + +(2) should take about 3 days to complete. + +Although there is a retry mechanism, reingestion may fail half-way. Horizon will print the recommended range to use in order to restart it. + +When reingestion is complete it's worth running `ANALYZE VERBOSE [table]` on all tables to recalculate the stats. This should improve the query speed. + +### Monitoring reingestion process + +This script should help monitor the reingestion process by printing the ledger subranges being reingested: + + + +```bash +#!/bin/bash +echo "Current ledger ranges being reingested:" +echo +I=1 +for S in $(ps aux | grep stellar-core | grep catchup | awk '{print $15}' | sort -n); do + printf '%15s' $S + if [ $(( I % 5 )) = 0 ]; then + echo + fi + I=$(( I + 1)) +done +``` + + + +Ideally we would be using Prometheus metrics for this, but they haven't been implemented yet. + +Here is an example run: + + + +``` +Current ledger ranges being reingested: + 99968/99968 199936/99968 299904/99968 399872/99968 499840/99968 + 599808/99968 699776/99968 799744/99968 899712/99968 999680/99968 + 1099648/99968 1199616/99968 1299584/99968 1399552/99968 1499520/99968 + 1599488/99968 1699456/99968 1799424/99968 1899392/99968 1999360/99968 + 2099328/99968 2199296/99968 2299264/99968 2399232/99968 2499200/99968 + 2599168/99968 2699136/99968 2799104/99968 2899072/99968 2999040/99968 + 3099008/99968 3198976/99968 3298944/99968 3398912/99968 3498880/99968 + 3598848/99968 3698816/99968 3798784/99968 3898752/99968 3998720/99968 + 4098688/99968 4198656/99968 4298624/99968 4398592/99968 4498560/99968 + 4598528/99968 4698496/99968 4798464/99968 4898432/99968 4998400/99968 + 5098368/99968 5198336/99968 5298304/99968 5398272/99968 5498240/99968 + 5598208/99968 5698176/99968 5798144/99968 5898112/99968 5998080/99968 + 6098048/99968 6198016/99968 6297984/99968 6397952/99968 17099967/99968 + 17199935/99968 17299903/99968 17399871/99968 17499839/99968 17599807/99968 + 17699775/99968 17799743/99968 17899711/99968 17999679/99968 18099647/99968 + 18199615/99968 18299583/99968 18399551/99968 18499519/99968 18599487/99968 + 18699455/99968 18799423/99968 18899391/99968 18999359/99968 19099327/99968 + 19199295/99968 19299263/99968 19399231/99968 +``` + + + +## Reading Logs + +In order to check the progress and status of ingestion you should check your logs regularly; all logs related to ingestion are tagged with `service=ingest`. + +It starts with informing you about state ingestion: + + + +``` +INFO[...] Starting ingestion system from empty state... pid=5965 service=ingest temp_set="*io.MemoryTempSet" +INFO[...] Reading from History Archive Snapshot pid=5965 service=ingest ledger=25565887 +``` + + + +During state ingestion, Horizon will log the number of processed entries every 100,000 entries (there are currently around 10M entries in the public network): + + + +``` +INFO[...] Processing entries from History Archive Snapshot ledger=25565887 numEntries=100000 pid=5965 service=ingest +INFO[...] Processing entries from History Archive Snapshot ledger=25565887 numEntries=200000 pid=5965 service=ingest +INFO[...] Processing entries from History Archive Snapshot ledger=25565887 numEntries=300000 pid=5965 service=ingest +INFO[...] Processing entries from History Archive Snapshot ledger=25565887 numEntries=400000 pid=5965 service=ingest +INFO[...] Processing entries from History Archive Snapshot ledger=25565887 numEntries=500000 pid=5965 service=ingest +``` + + + +When state ingestion is finished, it will proceed to ledger ingestion starting from the next ledger after the checkpoint ledger (25565887+1 in this example) to update the state using transaction metadata: + + + +``` +INFO[...] Processing entries from History Archive Snapshot ledger=25565887 numEntries=5400000 pid=5965 service=ingest +INFO[...] Processing entries from History Archive Snapshot ledger=25565887 numEntries=5500000 pid=5965 service=ingest +INFO[...] Processed ledger ledger=25565887 pid=5965 service=ingest type=state_pipeline +INFO[...] Finished processing History Archive Snapshot duration=2145.337575904 ledger=25565887 numEntries=5529931 pid=5965 service=ingest shutdown=false +INFO[...] Reading new ledger ledger=25565888 pid=5965 service=ingest +INFO[...] Processing ledger ledger=25565888 pid=5965 service=ingest type=ledger_pipeline updating_database=true +INFO[...] Processed ledger ledger=25565888 pid=5965 service=ingest type=ledger_pipeline +INFO[...] Finished processing ledger duration=0.086024492 ledger=25565888 pid=5965 service=ingest shutdown=false transactions=14 +INFO[...] Reading new ledger ledger=25565889 pid=5965 service=ingest +INFO[...] Processing ledger ledger=25565889 pid=5965 service=ingest type=ledger_pipeline updating_database=true +INFO[...] Processed ledger ledger=25565889 pid=5965 service=ingest type=ledger_pipeline +INFO[...] Finished processing ledger duration=0.06619956 ledger=25565889 pid=5965 service=ingest shutdown=false transactions=29 +INFO[...] Reading new ledger ledger=25565890 pid=5965 service=ingest +INFO[...] Processing ledger ledger=25565890 pid=5965 service=ingest type=ledger_pipeline updating_database=true +INFO[...] Processed ledger ledger=25565890 pid=5965 service=ingest type=ledger_pipeline +INFO[...] Finished processing ledger duration=0.071039012 ledger=25565890 pid=5965 service=ingest shutdown=false transactions=20 +``` + + + +## Managing Stale Historical Data + +Horizon ingests ledger data from a managed, [possibly-remote](./remote-core.mdx), pared-down Captive Stellar Core instance. In the event that Captive Core crashes, lags, or if Horizon stops ingesting data for any other reason, the view provided by Horizon will start to lag behind reality. For simpler applications, this may be fine, but in many cases this lag is unacceptable and the application should not continue operating until the lag is resolved. + +To help applications that cannot tolerate lag, Horizon provides a configurable "staleness" threshold. If enough lag accumulates to surpass this threshold (expressed in number of ledgers), Horizon will only respond with an error: [`stale_history`](https://github.com/stellar/go/blob/master/services/horizon/internal/docs/reference/errors/stale-history.md). To configure this option, use the `--history-stale-threshold`/`HISTORY_STALE_THRESHOLD` parameter. + +**Note:** Non-historical requests (such as submitting transactions or checking account balances) will not error out if the staleness threshold is surpassed. diff --git a/docs/run-api-server/installing.mdx b/docs/run-api-server/installing.mdx new file mode 100644 index 000000000..d711b62a9 --- /dev/null +++ b/docs/run-api-server/installing.mdx @@ -0,0 +1,92 @@ +--- +title: Installing +order: 20 +--- + +import { CodeExample } from "@site/src/components/CodeExample"; + +To install Horizon, you have a few choices. You can... + +- install prebuilt binaries [from our repositories](#package-manager) via your package manager if running a Debian-based system, +- download a [prebuilt release](https://github.com/stellar/go/releases/latest) of Horizon for your target architecture and operation system, or +- [build Horizon and Stellar Core yourself](#building) from scratch. + +**The first method is recommended**: Not only do you ensure OS compatibility and dependency management, you'll also install some convenient wrappers that make running Horizon and Stellar Core in their respective environments much simpler. + +## Installation Methods + +### Package Manager + +SDF publishes new releases to its custom Ubuntu repositories. Follow [this guide](https://github.com/stellar/packages/blob/master/docs/adding-the-sdf-stable-repository-to-your-system.md#adding-the-sdf-stable-repository-to-your-system) to add the stable SDF repository to your system. [This page](https://github.com/stellar/packages/blob/master/docs/installing-individual-packages.md#installing-individual-packages) outlines the various commands that these packages make available. We'll need: + + + +```bash +sudo apt update +sudo apt install stellar-horizon stellar-core +``` + + + +Next, you can jump to [Testing Your Installation](#completing-and-testing-your-installation). + +### Building + +Should you decide not to use one of our prebuilt releases, you may instead build Horizon from source. To do so, you need to prepare a developer environment, including: + +- A Unix-like operating system with the common core commands (cp, tar, mkdir, bash, etc.) +- A compatible distribution of [Golang](https://golang.org/dl/) (v1.15 or later) +- [git](https://git-scm.com/) + +_(Though Horizon can run on Windows, *building* directly on Windows is not supported.)_ + +At this point, you can easily build the Horizon binary: + + + +```bash +git clone https://github.com/stellar/go monorepo && cd monorepo +go install -v ./services/horizon +``` + + + +_(You should refer to the list of [Horizon releases](https://github.com/stellar/go/releases) and `git checkout` accordingly before building if you're looking for a stable release rather than the bleeding edge `master` branch.)_ + +At this point, you can either copy the binary from the `GOPATH` to the system PATH (as [we'll do later](#completing-and-testing-your-installation)), or add Go binaries to your PATH in your `.bashrc` (or equivalent): + + + +```bash +export PATH=$(go env GOPATH)/bin:$PATH +``` + + + +You will also need to compile Stellar Core from its source code if you need ingestion or transaction submission. You should refer to [their installation guide](https://github.com/stellar/stellar-core/blob/master/INSTALL.md) for details. + +Next, jump ahead to [Testing Your Installation](#completing-and-testing-your-installation). + +## Installing Remote Captive Core + +If you want to run Captive Core instances for [transaction ingestion](./running.mdx#ingesting-transactions) separately from other Horizon instances, we support that. This architecture allows flexibility in scaling and redundancy. For example, you may want each of your ingesting Horizon instances to have a dedicated Captive Core while your request-serving instances share a single Remote Captive Core for transaction submission. Or perhaps you want a dedicated Remote Captive Core living on more powerful hardware catered towards ingestion. + +If you are interested in this approach, you'll need some additional binaries and configuration on the relevant machines. Refer to the separate [Remote Captive Core](./remote-core.mdx) page for details. + +## Completing and Testing Your Installation + +If you [built from source](#building) or downloaded a release [from GitHub](https://github.com/stellar/go/releases), make sure to copy the native binary into a directory that is part of your PATH. Most Unix-like systems have `/usr/local/bin` in PATH by default, so unless you have a preference or know better, we recommend you copy the binary there: + + + +```bash +sudo cp horizon /usr/local/bin/stellar-horizon +``` + + + +_(We've renamed it here to keep it consistent with the results of the recommended [Package Manager](#package-manager) method.)_ + +To test your installation, simply run `stellar-horizon --help` from a terminal. If the help for Horizon is displayed, your installation was successful. + +**Note**: Some shells (such as [zsh](https://www.zsh.org/)) cache PATH lookups. You may need to clear your cache (by using `rehash` in zsh, for example) or restart your shell before trying to run the aforementioned command. diff --git a/docs/run-api-server/metadata.json b/docs/run-api-server/metadata.json new file mode 100644 index 000000000..2ffa1a14a --- /dev/null +++ b/docs/run-api-server/metadata.json @@ -0,0 +1,4 @@ +{ + "order": 70, + "title": "Run an API Server" +} diff --git a/docs/run-api-server/migrating.mdx b/docs/run-api-server/migrating.mdx new file mode 100644 index 000000000..a47dc157f --- /dev/null +++ b/docs/run-api-server/migrating.mdx @@ -0,0 +1,173 @@ +--- +title: Migrating From 1.x +order: 15 +--- + +import { Alert } from "@site/src/components/Alert"; +import { CodeExample } from "@site/src/components/CodeExample"; + + + +If you aren't coming from an existing deployment of Horizon, feel free to skip this section and move on to [installing Horizon](./installing.mdx)! + + + +## Introduction + +Starting with version v1.6.0, Horizon allows using Stellar Core in "captive" mode for ingestion. This mode has been enabled by default since Horizon 2.0, so even though you can enable captive mode on 1.6+, this migration guide is catered towards upgrading to 2.x due to the stability and configuration improvements introduced in the later versions. + + + +Please note that Horizon team will support the previous non-captive mode for the time being. To use the previous method, set `ENABLE_CAPTIVE_CORE_INGESTION=false` in your ingesting instances. After 6 months, this compatibility flag will be removed. + + + +Please start with the [blog post](https://www.stellar.org/developers-blog/a-new-sun-on-the-horizon) to understand the major changes that Horizon 2.0 introduces with the Captive Core architecture. In summary, Captive Core is a specialized, narrowed-down Stellar-Core instance with the sole aim of emitting transaction metadata to Horizon. It means: + +- no separate Stellar Core instance +- no Core database: everything done in-memory +- _much_ faster ingestion + +Captive Stellar Core completely eliminates all Horizon issues caused by connecting to Stellar Core's database, but it requires extra time to initialize and manage its Stellar Core subprocess. Captive Core can be used in both reingestion (`horizon db reingest range`) and normal Horizon operation (`horizon serve`). In fact, using Captive Core to reingest historical data is considerably faster than without it. + +### How It Works + +The blog post linked [above](#introduction) gives a high-level overview, while this section dives a little deeper into the technical differences relative to Horizon's relationship with standalone, "Watcher" Core. + +When using Captive Core, Horizon runs the `stellar-core` binary as a subprocess. Then, both processes communicate over filesystem pipe: Core sends `xdr.LedgerCloseMeta` structs with information about each ledger and Horizon reads it. + +The behaviour is slightly different when reingesting old ledgers and when reading recently closed ledgers: + +- **When reingesting**, Stellar Core is started in a special `catchup` mode that simply replays the requested range of ledgers. This mode requires an additional 3GiB of RAM because all ledger entries are stored in memory, making it extremely fast. This mode only depends on the history archives, so a Captive Core configuration (see [below](#configuration)) **is not** required. + +- **When reading recently closed ledgers**, Core is started with a normal `run` command. This mode _also_ requires an additional 3GiB of RAM for in-memory ledger entries. In this case, a configuration file (again, read on [below](#configuration)) **is** required in order to configure a quorum set so that it can connect to the Stellar network. + +### Known Limitations + +As discussed earlier, Captive Core provides much better decoupling for Horizon at the expense of persistence. You should be aware of the following consequences: + +- Captive Core requires a couple of minutes to complete the "apply buckets" stage _first_ time Horizon is started, but it should reuse the cached buckets on subsequent restarts (as of Horizon 2.5 and Core 17.1). +- If the Horizon process terminates, Stellar Core is also terminated (unless you are using [Remote Captive Core](./remote-core.mdx)). +- Running Horizon now requires more RAM and less disk space. You can refer to the earlier [Prerequisites](./prerequisites.mdx) page for details. + +To hedge against these limitations, we recommend running multiple ingesting Horizon servers in a single cluster. This allows other ingesting instances to maintain service without interruptions if a Captive Core instance is restarted. + +## Migration + +Now, we'll discuss migrating existing systems running the pre-2.0 versions of Horizon to the new Captive Core world. + +### Configuration + +The first major change from 1.x is how you will configure Horizon. You will no longer need your Stellar Core configuration, but will rather need to craft a configuration file describing Captive Core's behavior. Read [this section](./configuring.mdx#configuring-captive-core) to understand what the stub should contain. + +**Your old configuration cannot be used directly**: Horizon needs special settings for Captive Core. Otherwise, running Horizon may fail with the following error, or errors like it: + + + +``` +Invalid captive core toml file: LOG_FILE_PATH in captive core config file does not match Horizon captive-core-log-path flag +``` + + + +Again, while the Captive Core configuration file may appear to just be a subset of Stellar Core's configuration, you shouldn't think about it that way and treat it as its own format. It may diverge in the future, and not all of Core's options are available to Captive Core. + +You should pass the location of this new TOML configuration to the `--captive-core-config-path`/`CAPTIVE_CORE_CONFIG_PATH` command-line flag / environmental variable. + +If you want to continue to have access to the underlying Stellar Core subprocess (like you did previously with a standalone Watcher Core), you should set the `HTTP_PORT` field in your configuration file accordingly. + +### Installation + +Once you have a configuration file ready, you should also modify your Horizon configuration to include Captive Core parameters. Within `/etc/default/stellar-horizon`, you should add: + + + +```bash +# Captive Core Ingestion Config +ENABLE_CAPTIVE_CORE_INGESTION=true +STELLAR_CORE_BINARY_PATH=/usr/bin/stellar-core +CAPTIVE_CORE_CONFIG_PATH=/etc/default/stellar-captive-core.toml +CAPTIVE_CORE_STORAGE_PATH=/var/lib/stellar +# end Captive Core +``` + + + +You may need to adjust these accordingly, for example by pointing `CAPTIVE_CORE_CONFIG_PATH` to your configuration file and possibly `CAPTIVE_CORE_STORAGE_PATH` to where you'd like Captive Core to store its bucket files (but keep in mind the [disk space](./prerequisites.mdx) and [permissions](./configuring.mdx#package-manager-installation) requirements). + +Finally, the process for upgrading both Stellar Core and Horizon is covered [here](https://github.com/stellar/packages/blob/master/docs/upgrading.md#upgrading). + + + +Depending on the version you're migrating from, you may need to include an additional step here: **manual reingestion**. This can still be accomplished with Captive Core; see [below](#reingestion). + + + +### Restarting Services + +Now, we can stop Core and restart Horizon: + + + +```bash +sudo systemctl stop stellar-core +sudo systemctl restart stellar-horizon +``` + + + +After a few moments, the logs should show Captive Core running successfully as a subprocess, and eventually Horizon will be running as usual except with Captive Core rapidly generating transaction metadata in-memory! + +### A Multi-Machine Setup + +If you plan on running Horizon and Captive Core on separate machines, you'll need to read the page on [Remote Captive Core](./remote-core.mdx) for setting that up. + +The most important change is that you will need to point Horizon at Captive Core via the `REMOTE_CAPTIVE_CORE_URL` (for the wrapper API) and `STELLAR_CORE_URL` (for the raw Core API, if enabled). For example, via: + + + +```bash +echo "STELLAR_CORE_URL='http://captivecore.local:11626' +REMOTE_CAPTIVE_CORE_URL='http://captivecore.local:8000' +" | sudo tee -a /etc/default/stellar-horizon +``` + + + +Where `captivecore.local` is the machine on which the Remote Captive Core API wrapper is running. + +## Private Networks + +If you want your Captive Core instance to connect to a private Stellar network, you will need to specify the validator(s) of the private network in the Captive Core configuration file. + +Assuming the validator of your private network has a public key of `GD5KD2KEZJIGTC63IGW6UMUSMVUVG5IHG64HUTFWCHVZH2N2IBOQN7PS` and can be accessed at `private1.validator.com`, then the Captive Core config would consist of the following: + + + +```toml +UNSAFE_QUORUM=true +FAILURE_SAFETY=0 + +[[VALIDATORS]] +NAME="private" +HOME_DOMAIN="validator.com" +PUBLIC_KEY="GD5KD2KEZJIGTC63IGW6UMUSMVUVG5IHG64HUTFWCHVZH2N2IBOQN7PS" +ADDRESS="private1.validator.com" +QUALITY="MEDIUM" +``` + + + +`UNSAFE_QUORUM=true` and `FAILURE_SAFETY=0` are required when there are too few validators in the private network to form a quorum. + +You will also need to set `RUN_STANDALONE=false` in the Stellar Core configuration _for the validator_. Otherwise, the validator will not accept connections on its peer port, which means Captive Core will not be able to connect to the validator. + +On a new Stellar network, the first history archive snapshot is published after ledger 63 is closed. Captive Core depends on the history archives, which means that Horizon ingestion via Captive Core will not begin until after ledger 63 is closed. Assuming the standard 5 second delay in between ledgers, it will take ~5 minutes for the network to progress from the genesis ledger to ledger 63. + +There are cases where you may need to repeatedly create new private networks (e.g. spawning a private network during integration tests) and this 5 minute delay is too costly. In that case, you can consider including `ARTIFICIALLY_ACCELERATE_TIME_FOR_TESTING=true` in both the validator configuration and the Captive Core configuration. When this parameter is set, Stellar Core will publish a new ledger every _second_. It will also publish history archive snapshots every 8 ledgers, so you will need to set Horizon's checkpoint frequency parameter (`--checkpoint-frequency`/`CHECKPOINT_FREQUENCY`) to 8. + +## Reingestion + +After migrating to the Captive Core world, you will assuredly need to reingest your history again. + +The [Ingestion guide](./ingestion.mdx#reingestion) should refresh your memory on this: nothing has really changed aside from how quickly reingestion gets done. For example, a [full reingestion](#using-captive-core-to-reingest-the-full-public-network-history) of the entire network only takes ~1.5 days (as opposed to weeks previously) on an [m5.8xlarge](https://aws.amazon.com/ec2/pricing/on-demand/) instance. diff --git a/docs/run-api-server/monitoring.mdx b/docs/run-api-server/monitoring.mdx new file mode 100644 index 000000000..264642264 --- /dev/null +++ b/docs/run-api-server/monitoring.mdx @@ -0,0 +1,113 @@ +--- +title: Monitoring +order: 60 +--- + +import { CodeExample } from "@site/src/components/CodeExample"; + +To ensure that your instance of Horizon is performing correctly, we encourage you to monitor it and provide both logs and metrics to do so. + +## Metrics + +Metrics are collected while a Horizon process is running and they are exposed _privately_ via the `/metrics` path, accessible only through the Horizon admin port. You need to configure this via `--admin-port` or `ADMIN_PORT`, since it's disabled by default. If you're running such an instance locally, you can access this endpoint: + + + +``` +$ stellar-horizon --admin-port=4200 & +$ curl localhost:4200/metrics +# HELP go_gc_duration_seconds A summary of the GC invocation durations. +# TYPE go_gc_duration_seconds summary +go_gc_duration_seconds{quantile="0"} 1.665e-05 +go_gc_duration_seconds{quantile="0.25"} 2.1889e-05 +go_gc_duration_seconds{quantile="0.5"} 2.4062e-05 +go_gc_duration_seconds{quantile="0.75"} 3.4226e-05 +go_gc_duration_seconds{quantile="1"} 0.001294239 +go_gc_duration_seconds_sum 0.002469679 +go_gc_duration_seconds_count 25 +# HELP go_goroutines Number of goroutines that currently exist. +# TYPE go_goroutines gauge +go_goroutines 23 +and so on... +``` + + + +## Logs + +Horizon will output logs to standard out. Information about what requests are coming in will be reported, but more importantly, warnings or errors will also be emitted by default. A correctly running Horizon instance will not output any warning or error log entries. + +Below we present a few standard log entries with associated fields. You can use them to build metrics and alerts. Please note that these represent Horizon app metrics only. You should also monitor your hardware metrics like CPU or RAM Utilization. + +### Starting HTTP request + +| Key | Value | +| --- | ----- | +| **`msg`** | **`Starting request`** | +| `client_name` | Value of `X-Client-Name` HTTP header representing client name | +| `client_version` | Value of `X-Client-Version` HTTP header representing client version | +| `app_name` | Value of `X-App-Name` HTTP header representing app name | +| `app_version` | Value of `X-App-Version` HTTP header representing app version | +| `forwarded_ip` | First value of `X-Forwarded-For` header | +| `host` | Value of `Host` header | +| `ip` | IP of a client sending HTTP request | +| `ip_port` | IP and port of a client sending HTTP request | +| `method` | HTTP method (`GET`, `POST`, ...) | +| `path` | Full request path, including query string (ex. `/transactions?order=desc`) | +| `streaming` | Boolean, `true` if request is a streaming request | +| `referer` | Value of `Referer` header | +| `req` | Random value that uniquely identifies a request, attached to all logs within this HTTP request | + +### Finished HTTP request + +| Key | Value | +| --- | ----- | +| **`msg`** | **`Finished request`** | +| `bytes` | Number of response bytes sent | +| `client_name` | Value of `X-Client-Name` HTTP header representing client name | +| `client_version` | Value of `X-Client-Version` HTTP header representing client version | +| `app_name` | Value of `X-App-Name` HTTP header representing app name | +| `app_version` | Value of `X-App-Version` HTTP header representing app version | +| `duration` | Duration of request in seconds | +| `forwarded_ip` | First value of `X-Forwarded-For` header | +| `host` | Value of `Host` header | +| `ip` | IP of a client sending HTTP request | +| `ip_port` | IP and port of a client sending HTTP request | +| `method` | HTTP method (`GET`, `POST`, ...) | +| `path` | Full request path, including query string (ex. `/transactions?order=desc`) | +| `route` | Route pattern without query string (ex. `/accounts/{id}`) | +| `status` | HTTP status code (ex. `200`) | +| `streaming` | Boolean, `true` if request is a streaming request | +| `referer` | Value of `Referer` header | +| `req` | Random value that uniquely identifies a request, attached to all logs within this HTTP request | + +### Metrics + +Using the entries above you can build metrics that will help understand performance of a given Horizon node. For example: + +- Number of requests per minute. +- Number of requests per route (the most popular routes). +- Average response time per route. +- Maximum response time for non-streaming requests. +- Number of streaming vs. non-streaming requests. +- Number of rate-limited requests. +- List of rate-limited IPs. +- Unique IPs. +- The most popular SDKs/apps sending requests to a given Horizon node. +- Average ingestion time of a ledger. +- Average ingestion time of a transaction. + +### Alerts + +Below are example alerts with potential causes and solutions. Feel free to add more alerts using your metrics: + +| Alert | Cause | Solution | +| ----- | ----- | -------- | +| Spike in number of requests | Potential DoS attack | Lower rate-limiting threshold | +| Large number of rate-limited requests | Rate-limiting threshold too low | Increase rate-limiting threshold | +| Ingestion is slow | Horizon server spec too low | Increase hardware spec | +| Spike in average response time of a single route | Possible bug in a code responsible for rendering a route | Report an issue in Horizon repository. | + +## I'm Stuck! Help! + +If any of the above steps don't work or you are otherwise prevented from correctly setting up Horizon, please join our community and let us know. Either post a question at [our Stack Exchange](https://stellar.stackexchange.com/) or chat with us on [Keybase in #dev_discussion](https://keybase.io/team/stellar.public) to ask for help. diff --git a/docs/run-api-server/prerequisites.mdx b/docs/run-api-server/prerequisites.mdx new file mode 100644 index 000000000..71b9eb87f --- /dev/null +++ b/docs/run-api-server/prerequisites.mdx @@ -0,0 +1,58 @@ +--- +title: Prerequisites +order: 10 +--- + +Horizon only has one true dependency: a PostgreSQL server that it uses to store data that has been processed and ingested from Stellar Core. **Horizon requires PostgreSQL version >= 9.5**. + +As far as system requirements go, there are a few main things to keep in mind. Starting from version 2.0, Horizon must be run as a standalone service. A full Horizon build consists of three functions: + +1. **ingesting data** _from_ the decentralized Stellar network, +1. **submitting transactions** _to_ the network, and +1. **serving** API requests + +The first two happen through _Captive Core_, a pared down, non-validating version of Stellar Core packaged directly into Horizon. + +With these three functions in mind, you can also run Horizon in two different ways: **real-time ingestion** and **historical catch-up**: + +- _Real-time ingestion_ is an “online” process: it involves keeping in sync with the live Stellar network state and digesting ledger data into a holistic view of the network. If you just want function (3) from above, you still need to do this. + +- _Historical catch-up_ is an “offline” process: it lets you look into the past and catch up your Horizon instance to a given retention period (e.g. 30 days of history). Because it’s typically done offline and a one-time process, you can dedicate more compute power and configure parallel workers to catch up faster. + +### Historical Catch-up + +In this scenario, the hardware specifications are more demanding than what is necessary for the day-to-day operation of real-time ingestion, but catch-up only needs to occur once. + +However, the requirements will vary depending on your chosen retention period and desired catch-up speed. Note that **most operators will not need full history**, and as the network continues to grow, tracking full history will become increasingly prohibitive. As of late 2021, DB storage to support historical retention is growing at a rate of 0.8 TB / month. It is highly recommended to configure retention of only the history needed to support your functionality. + +#### Requirements + +Minimally, your disk storage type **must** be an SSD (e.g. NVMe, Direct Attached Storage) and your I/O **must** handle >15k iops (I/O operations per second). The following table breaks down hardware specifications for ingestion at different retention levels and performance tiers. + +Note that each component can be scaled independently and for redundancy, in the manner of traditional _n_-tier systems which is covered later in [Scaling](./scaling.mdx). Ingestion can be sped up via configuring more Captive Core parallel workers (requiring more compute and RAM). + +| Component | | Retention Period | +|:----------|:--|:-----------------| +| | **30 days** | **90 days** | **Full History** | +| **Parallel worker count**
(est. ingestion time) | 6 workers (1 day) | 10 workers (1 day) | 20+ workers (2 days) | +| **Horizon** | **CPU**: 10 cores (min: 6)
**RAM**: 64 GB (min: 32)
| **CPU**: 16 (min: 8)
**RAM**: 128 GB (64)
| **CPU**: 16 (10)
**RAM**: 512 GB (256) | +| **Database** | **CPU**: 16 cores (min: 8)
**RAM**: 64 GB (min: 32GB)
**Storage**: 2 TB
**IOPS**: 20K (min: 15K) | **CPU**: 16 (12)
**RAM**: 128 GB (64)
**Storage**: 4 TB
**IOPS**: 20K (15K) | **CPU**: 64 (32)
**RAM**: 512 GB (256)
**Storage**: 10 TB
**IOPS**: 20k (15k) | +| **Storage**
(all same) | | **SSD** (NVMe, Direct Attached Storage preferred) | | +| **AWS**
(reference) | **Captive Core**: `m5.2xlarge`
**Database**: `r5.2xlarge` | **Captive Core**: `m5.4xlarge`
**DB**: `r5.4xlarge` | **Captive Core**: `c5.2xlarge` (x2)
**DB**: `r5.16xlarge` (ro)
`r5.8xlarge` (rw) | + +### Real-Time Ingestion + +In this scenario, the goal is just to stay in sync with the Stellar network for day-to-day operations. + +There are two extremes to this spectrum: running a **single private instance** of Horizon for a specific application all the way up to a serious **enterprise public instance** of Horizon. In the former case, you’d run all three functions on a single machine and have low request volume; in the latter case, you’d have high-availability, redundancy, high request volume, full history, etc. + +#### Requirements + +The following table breaks down requirements along this spectrum; if you fall somewhere in between, interpolate the requirements accordingly. + +| Category | Private Instance | Enterprise Public Instance | +|:---------|:-----------------|:---------------------------| +| **Compute** | Both **API Service** + **Captive Core**:
**CPU**: 4
**RAM**: 32 GB | **API Service**
**CPU**: 4
**RAM**: 8 GB
N instances, load balanced

**Captive Core**
**CPU**: 8
**RAM**: 256 GB
2 instances for redundancy | +| **Database** | **CPU**: 4
**RAM**: 32 GB
**IOPS**: 7k (min: 2.5k) | **CPU**: 32 - 64
**RAM**: 256 - 512 GB
**IOPS**: 20k (min: 15k)
2 HA instances: 1RO, 1RW | +| **Storage** (SSD) | depends on retention period | 10 TB | +| **AWS** (reference) | **API Service + Captive Core**
`m5.2xlarge`

**Database**
`r5.2xlarge` (ro)
`r5.xlarge` (rw) | **API Service**
`c5.xlarge` (_n_)

**Captive Core**
`c5.2xlarge` (x2)

**Database** `r5.16xlarge` (ro)
`r5.8xlarge` (rw) | diff --git a/docs/run-api-server/remote-core.mdx b/docs/run-api-server/remote-core.mdx new file mode 100644 index 000000000..62649c63b --- /dev/null +++ b/docs/run-api-server/remote-core.mdx @@ -0,0 +1,99 @@ +--- +title: Remote Captive Core +order: 35 +--- + +import { Alert } from "@site/src/components/Alert"; +import { CodeExample } from "@site/src/components/CodeExample"; + +As briefly alluded to in the [Installation](./installing.mdx) page, there is a way to decouple the Horizon instance from its Captive Core ingesting instance by configuring Captive Core to operate remotely. + +This allows for greater flexibility in architectural scaling: you can scale your request-serving Horizon, ingesting Horizon, and Captive Core instances separately. There's also nothing wrong with running Horizon and Remote Captive Core on the same machine, e.g. if you just want them to be in different processes or are running a containerized setup. + + + +**This architecture is still considered experimental.** While we expect to continue to support this architecture moving forward, the implementation itself is subject to changes. You will have to build the package from source, and the JSON API responses should not be considered stable: though Horizon will stay up-to-date and be compatible with the response format, it's not recommended to build custom functionality on top of the API. + + + +## Getting Started + +As noted above, this setup requires building packages from source. It will eventually make its way into the [unstable SDF repository](https://github.com/stellar/packages/blob/master/docs/adding-the-sdf-stable-repository-to-your-system.md#adding-the-bleeding-edge-testing-repository), then into the stable one. For now, though, we will build the [package](https://github.com/stellar/go/blob/master/exp/services/captivecore/README.md) directly. + +The requirements here are the same as for [building Horizon](./installing.mdx#building): a Unix-like OS, [Golang](https://golang.org/doc/install), and [`git`](https://git-scm.com/). With those set up, + + + +```bash +git clone https://github.com/stellar/go monorepo && cd monorepo +go install -v ./exp/services/captivecore +sudo cp $(go env GOPATH)/bin/captivecore /usr/bin/stellar-captive-core-api +``` + + + +_(You should refer to the list of [Horizon releases](https://github.com/stellar/go/releases) and `git checkout` accordingly before building if you're looking for a stable release rather than the bleeding edge `master` branch.)_ + +## Running Captive Core + +Running the API requires some familiar parameters: + +| flag | envvar | example | +| ---- | ------ | ------- | +| `--captive-core-config-append-path` | `CAPTIVE_CORE_CONFIG_PATH` | /etc/default/stellar-captive-core.toml | +| `--history-archive-urls` | `HISTORY_ARCHIVE_URLS` | https://history.stellar.org/prd/core-testnet/core_testnet_001,https://history.stellar.org/prd/core-testnet/core_testnet_002 | +| `--stellar-core-binary-path` | `STELLAR_CORE_BINARY_PATH` | /usr/bin/stellar-core | + +These should be self-explanatory if you've read the [earlier sections](./configuring.mdx#parameters). There are a few others that you can see under the `--help` text that may be of interest, particularly the ones related to ports if you want to expose the underlying Stellar Core or ensure the Remote Captive Core and Horizon ports don't conflict (both default to 8000). + +If you wanted to rely on SDF for history archives and connect to the test network, you might prepare your environment and run the Remote Captive Core API as follows: + + + +```bash +stellar-captive-core-api \ + --stellar-core-binary-path=$(which stellar-core) \ + --captive-core-config-append-path=/etc/default/stellar-captive-core-stub.toml \ + --history-archive-urls="https://history.stellar.org/prd/core-testnet/core_testnet_001" \ + --port=8001 + +# INFO[...] Starting Captive Core server on 8001 pid=9450 +``` + + + +After this, you should be able to hit endpoints locally and see results, e.g.: + + + +```bash +curl http://localhost:8001/latest-sequence +PrepareRange must be called before any other operations +``` + + + +For using pubnet, refer to the earlier section on [Configuring Captive Core](./configuring.mdx#configuring-captive-core) that outlines considerations about quorum sets and choosing who you trust. + +## Running Horizon + +With the Captive Core API server running, you can now point Horizon to this remote instance for ingestion. Rather than passing the configuration and binary paths described [earlier](./configuring.mdx#with-ingestion), you instead point Horizon to the remote instance: + +| flag | envvar | example | +| ---- | ------ | ------- | +| `--remote-captive-core-url` | `REMOTE_CAPTIVE_CORE_URL` | http://10.0.0.42:8000 | +| `--stellar-core-url` | `STELLAR_CORE_URL` | http://127.0.0.1:11626 | + +The remote URL should point to a running Captive Core API like the one we set up above, while the Core URL should point to the underlying Stellar Core port that the remote node exposes. In our case, we'll assume that both Horizon and Remote Captive Core are running on the same machine for simplicity; otherwise, you should fill in the hostnames and ports accordingly: + + + +```bash +stellar-horizon-cmd serve \ + --remote-captive-core-url=http://localhost:8001 \ + --stellar-core-url=http://localhost:11626 +``` + + + +After ingestion catches up, you should be able to query Horizon's endpoints just like before. diff --git a/docs/run-api-server/running.mdx b/docs/run-api-server/running.mdx new file mode 100644 index 000000000..c090bf55a --- /dev/null +++ b/docs/run-api-server/running.mdx @@ -0,0 +1,40 @@ +--- +title: Running +order: 40 +--- + +import { CodeExample } from "@site/src/components/CodeExample"; + +Once your Horizon database and Captive Core configuration is set up properly, you're ready to run Horizon. Run `stellar-horizon` with the [appropriate parameters](./configuring.mdx#parameters) set (or `stellar-horizon-cmd serve` if you [installed via the package manager](./installing.mdx#package-manager), which will automatically import your configuration from `/etc/default/stellar-horizon`), which starts the HTTP server and starts logging to standard out. When run, you should see output similar to: + + + +``` +INFO[...] Starting horizon on :8000 pid=29013 +``` + + + +Note that the numbers may naturally be different for your installation. The log line above announces that Horizon is ready to serve client requests. + +Next, you can confirm that Horizon is responding correctly by loading the root resource. In the example above, that URL would be http://127.0.0.1:8000/, and simply running `curl http://127.0.0.1:8000/` would show you that the root resource loads correctly: + + + +```json +{ + "_links": { + "account": { + "href": "http://127.0.0.1:8000/accounts/{account_id}", + "templated": true + }, + "accounts": { + "href": "http://127.0.0.1:8000/accounts{?signer,sponsor,asset,cursor,limit,order}", + "templated": true + } + }, + // etc. +} +``` + + diff --git a/docs/run-api-server/scaling.mdx b/docs/run-api-server/scaling.mdx new file mode 100644 index 000000000..267a47219 --- /dev/null +++ b/docs/run-api-server/scaling.mdx @@ -0,0 +1,46 @@ +--- +title: Scaling +order: 70 +--- + +As alluded to in the discussion on [Prerequisites](./prerequisites.mdx), Horizon encompasses different logical tiers that can be scaled independently for high throughput, isolation, and high availability. The following components can be independently scaled: + +- Web service API (serving) +- Captive Core (ingestion and transaction submission) +- Database (storage) + +As always, scaling encompasses a spectrum. A few common scaling architectures follow. + +## Single VM + +As a starting point, for development purposes or low load environments with limited history retention (e.g. a few ledger entries), a single VM would suffice. + +![](../web-assets/horizon-scaling/Topology-1VM.png) + +## Low to Medium Load + +For low to medium load environments with up to 30-90 days of data history retention and modest API request traffic, this configuration isolates the database instance from the API service and ingestion process. + +![](../web-assets/horizon-scaling/Topology-2VMs.png) + +### Extension: Isolating Captive Core + +Additionally, Captive Core can be further isolated into its own VM, especially for isolating high throughput historical catch-up with parallel workers, leaving it unaffected by API request servicing load. + +![](../web-assets/horizon-scaling/Topology-3VMs.png) + +## Enterprise _n_-Tier + +This architecture services high request and data processing throughput with isolation and redundancy for each component. Scale the API service horizontally by adding a load balancer in front of multiple API service instances, each only limited by the database I/O limit. If necessary, use ALB routing to direct specific endpoints to specific request-serving instances, which are tied to a specific, dedicated DB. Now, if an intense endpoint gets clobbered, all other endpoints are unaffected. + +Database instances can be scaled when the I/O limit is reached by using read-only replicated copies that stay in sync and a read/write instance connected to Captive Core. Each DB replica can support a set of request servers to support additional horizontal scaling. + +Additionally, a second Captive Core instance shares ingestion load and serves as a backup in case of an instance failure. + +![](../web-assets/horizon-scaling/Topology-Enterprise.png) + +### Extension: Redundant Hot Backup + +The entire architecture can be replicated to a second cluster. The backup cluster can be upgraded independently or fail-overed to with no downtime. Additionally, capacity can be doubled in an emergency if needed. + +![](../web-assets/horizon-scaling/Topology-Enterprise-HotBackup.png) diff --git a/docs/run-core-node/commands.mdx b/docs/run-core-node/commands.mdx new file mode 100644 index 000000000..98dd3f1b6 --- /dev/null +++ b/docs/run-core-node/commands.mdx @@ -0,0 +1,140 @@ +--- +title: Commands +order: 80 +--- + +import { CodeExample } from "@site/src/components/CodeExample"; + +Stellar Core can be controlled via the following commands. + +## Common options + +Common options can be placed at any place in the command line. + +- **--conf ``**: Specify a config file to use. You can use '-' and provide the config file via STDIN. _default 'stellar-core.cfg'_ +- **--ll ``**: Set the log level. It is redundant with `http-command ll` but we need this form if you want to change the log level during test runs. +- **--metric ``**: Report metric METRIC on exit. Used for gathering a metric cumulatively during a test run. +- **--help**: Show help message for given command. + +## Command line options + +Command options can only by placed after command. + +- **catchup ``**: Perform catchup from history archives without connecting to network. For new instances (with empty history tables - only ledger 1 present in the database) it will respect LEDGER-COUNT configuration and it will perform bucket application on such a checkpoint that at least LEDGER-COUNT entries are present in history table afterwards. For instances that already have some history entries, all ledgers since last closed ledger will be replayed. +- **check-quorum**: Check quorum intersection from history to ensure there is closure over all the validators in the network. +- **convert-id ``**: Will output the passed ID in all known forms and then exit. Useful for determining the public key that corresponds to a given private key. For example: + +`$ stellar-core convert-id SDQVDISRYN2JXBS7ICL7QJAEKB3HWBJFP2QECXG7GZICAHBK4UNJCWK2` + +- **dump-xdr ``**: Dumps the given XDR file and then exits. +- **force-scp**: This command is used to start a network from scratch or when a network has lost quorum because of failed nodes or otherwise. It sets a flag in the database. The next time stellar-core is run, stellar-core will start emitting SCP messages based on its last known ledger. Without this flag stellar-core waits to hear a ledger close from the network before starting SCP.
force-scp doesn't change the requirements for quorum so although this node will emit SCP messages SCP won't complete until there are also a quorum of other nodes also emitting SCP messages on this same ledger. Value of force-scp can be reset with --reset flag. +- **fuzz ``**: Run a single fuzz input and exit. +- **gen-fuzz ``**: Generate a random fuzzer input file. +- **gen-seed**: Generate and print a random public/private key and then exit. +- **help**: Print the available command line options and then exit.. +- **http-command ``** Send an [HTTP command](#http-commands) to an already running local instance of stellar-core and then exit. For example: + +`$ stellar-core http-command info` + +- **infer-quorum**: Print a potential quorum set inferred from history. +- **load-xdr ``**: Load an XDR bucket file, for testing. +- **new-db**: Clears the local database and resets it to the genesis ledger. If you connect to the network after that it will catch up from scratch. +- **new-hist `` ...**: Initialize the named history archives HISTORY-LABEL. HISTORY-LABEL should be one of the history archives you have specified in the stellar-core.cfg. This will write a `.well-known/stellar-history.json` file in the archive root. +- **offline-info**: Returns an output similar to `--c info` for an offline instance +- **print-xdr ``**: Pretty-print a binary file containing an XDR object. If FILE-NAME is "-", the XDR object is read from standard input.
Option --filetype [auto|ledgerheader|meta|result|resultpair|tx|txfee]\*\* controls type used for printing (default: auto).
Option --base64 alters the behavior to work on base64-encoded XDR rather than raw XDR. +- **publish**: Execute publish of all items remaining in publish queue without connecting to network. May not publish last checkpoint if last closed ledger is on checkpoint boundary. +- **report-last-history-checkpoint**: Download and report last history checkpoint from a history archive. +- **run**: Runs stellar-core service. +- **sec-to-pub**: Reads a secret key on standard input and outputs the corresponding public key. Both keys are in Stellar's standard base-32 ASCII format. +- **sign-transaction ``**: Add a digital signature to a transaction envelope stored in binary format in ``, and send the result to standard output (which should be redirected to a file or piped through a tool such as `base64`). The private signing key is read from standard input, unless `` is "-" in which case the transaction envelope is read from standard input and the signing key is read from `/dev/tty`. In either event, if the signing key appears to be coming from a terminal, stellar-core disables echo. Note that if you do not have a STELLAR_NETWORK_ID environment variable, then before this argument you must specify the --netid option. For example, the production stellar network is "`Public Global Stellar Network ; September 2015`" while the test network is "`Test SDF Network ; September 2015`".
Option --base64 alters the behavior to work on base64-encoded XDR rather than raw XDR. +- **test**: Run all the unit tests. + - Suboptions specific to stellar-core: + - `--all-versions` : run with all possible protocol versions + - `--version ` : run tests for protocol version N, can be specified multiple times (default latest) + - `--base-instance ` : run tests with instance numbers offset by N, used to run tests in parallel + - For [further info](https://github.com/philsquared/Catch/blob/master/docs/command-line.md) on possible options for test. + - For example this will run just the tests tagged with `[tx]` using protocol versions 9 and 10 and stop after the first failure: `stellar-core test -a --version 9 --version 10 "[tx]"` +- **upgrade-db**: Upgrades local database to current schema version. This is usually done automatically during stellar-core run or other command. +- **version**: Print version info and then exit. +- **write-quorum**: Print a quorum set graph from history. + +## HTTP Commands + +By default stellar-core listens for connections from localhost on port 11626. You can send commands to stellar-core via a web browser, curl, or using the --c command line option (see above). Most commands return their results in JSON format. + +- **bans** List current active bans + +- **checkdb** Triggers the instance to perform a background check of the database's state. + +- **checkpoint** Triggers the instance to write an immediate history checkpoint. And uploads it to the archive. + +- **connect** `connect?peer=NAME&port=NNN`
Triggers the instance to connect to peer NAME at port NNN. + +- **dropcursor**\ + `dropcursor?id=ID`
Deletes the tracking cursor identified by `id`. See `setcursor` for more information. + +- **droppeer** `droppeer?node=NODE_ID[&ban=D]`
Drops peer identified by NODE_ID, when D is 1 the peer is also banned. + +- **info** Returns information about the server in JSON format (sync state, connected peers, etc). + +- **ll**\ + `ll?level=L[&partition=P]`
Adjust the log level for partition P where P is one of Bucket, Database, Fs, Herder, History, Ledger, Overlay, Process, SCP, Tx (or all if no partition is specified). Level is one of FATAL, ERROR, WARNING, INFO, DEBUG, VERBOSE, TRACE. + +- **logrotate** Rotate log files. + +- **maintenance** `maintenance?[queue=true]`
Performs maintenance tasks on the instance. + + - `queue` performs deletion of queue data. See `setcursor` for more information. + +- **metrics** Returns a snapshot of the metrics registry (for monitoring and debugging purpose). + +- **clearmetrics** `clearmetrics?[domain=DOMAIN]`
Clear metrics for a specified domain. If no domain specified, clear all metrics (for testing purposes). + +- **peers?[&fullkeys=true]** Returns the list of known peers in JSON format. If `fullkeys` is set, outputs unshortened public keys. + +- **quorum** `quorum?[node=NODE_ID][&compact=true][&fullkeys=true][&transitive=true]`
Returns information about the quorum for `NODE_ID` (local node by default). If `transitive` is set, information is for the transitive quorum centered on `NODE_ID`, otherwise only for nodes in the quorum set of `NODE_ID`. + + `NODE_ID` is either a full key (`GABCD...`), an alias (`$name`) or an abbreviated ID (`@GABCD`). + + If `compact` is set, only returns a summary version. + + If `fullkeys` is set, outputs unshortened public keys. + +- **setcursor** `setcursor?id=ID&cursor=N`
Sets or creates a cursor identified by `ID` with value `N`. ID is an uppercase AlphaNum, N is an uint32 that represents the last ledger sequence number that the instance ID processed. Cursors are used by dependent services to tell stellar-core which data can be safely deleted by the instance. The data is historical data stored in the SQL tables such as txhistory or ledgerheaders. When all consumers processed the data for ledger sequence N the data can be safely removed by the instance. The actual deletion is performed by invoking the `maintenance` endpoint or on startup. See also `dropcursor`. + +- **getcursor** `getcursor?[id=ID]`
Gets the cursor identified by `ID`. If ID is not defined then all cursors will be returned. + +- **scp** `scp?[limit=n][&fullkeys=true]`
Returns a JSON object with the internal state of the SCP engine for the last n (default 2) ledgers. Outputs unshortened public keys if fullkeys is set. + +- **tx** `tx?blob=Base64`
Submit a transaction to the network. blob is a base64 encoded XDR serialized 'TransactionEnvelope', and it returns a JSON object with the following properties status: + + - "PENDING" - transaction is being considered by consensus + - "DUPLICATE" - transaction is already PENDING + - "ERROR" - transaction rejected by transaction engine error: set when status is "ERROR". Base64 encoded, XDR serialized 'TransactionResult' + +- **upgrades** + + - `upgrades?mode=get`
Retrieves the currently configured upgrade settings.
+ - `upgrades?mode=clear`
Clears any upgrade settings.
+ - `upgrades?mode=set&upgradetime=DATETIME&[basefee=NUM]&[basereserve=NUM]&[maxtxsize=NUM]&[protocolversion=NUM]`
+ - upgradetime is a required date (UTC) in the form `1970-01-01T00:00:00Z`. It is the time the upgrade will be scheduled for. If it is in the past by less than 12 hours, the upgrade will occur immediately. If it's more than 12 hours, then the upgrade will be ignored
+ - fee (uint32) This is what you would prefer the base fee to be. It is in stroops
+ - basereserve (uint32) This is what you would prefer the base reserve to be. It is in stroops.
+ - maxtxsize (uint32) This defines the maximum number of transactions to include in a ledger. When too many transactions are pending, surge pricing is applied. The instance picks the top maxtxsize transactions locally to be considered in the next ledger. Where transactions are ordered by transaction fee(lower fee transactions are held for later). {" "}
+ - protocolversion (uint32) defines the protocol version to upgrade to. When specified it must match one of the protocol versions supported by the node and should be greater than ledgerVersion from the current ledger
+ +- **surveytopology** `surveytopology?duration=DURATION&node=NODE_ID`
Starts a survey that will request peer connectivity information from nodes in the backlog. `DURATION` is the number of seconds this survey will run for, and `NODE_ID` is the public key you will add to the backlog to survey. Running this command while the survey is running will add the node to the backlog and reset the timer to run for `DURATION` seconds. By default, this node will respond to/relay a survey message if the message originated from a node in it's transitive quorum. This behaviour can be overridden by adding keys to `SURVEYOR_KEYS` in the config file, which will be the set of keys to check instead of the transitive quorum. If you would like to opt-out of this survey mechanism, just set `SURVEYOR_KEYS` to `$self` or a bogus key + +- **stopsurvey** `stopsurvey`
Will stop the survey if one is running. Noop if no survey is running + +- **getsurveyresult** `getsurveyresult`
Returns the current survey results. The results will be reset everytime a new survey is started + +### The following HTTP commands are exposed on test instances + +- **generateload** `generateload[?mode=(create|pay)&accounts=N&offset=K&txs=M&txrate=R&batchsize=L&spikesize=S&spikeinterval=I]`
Artificially generate load for testing; must be used with `ARTIFICIALLY_GENERATE_LOAD_FOR_TESTING` set to true. Depending on the mode, either creates new accounts or generates payments on accounts specified (where number of accounts can be offset). Additionally, allows batching up to 100 account creations per transaction via 'batchsize'. When a nonzero I is given, a spike will occur every I seconds injecting S transactions on top of `txrate`. + +- **manualclose** If MANUAL_CLOSE is set to true in the .cfg file. This will cause the current ledger to close. + +- **testacc** `testacc?name=N`
Returns basic information about the account identified by name. Note that N is a string used as seed, but "root" can be used as well to specify the root account used for the test instance. + +- **testtx** `testtx?from=F&to=T&amount=N&[create=true]`
Injects a payment transaction (or a create transaction if "create" is specified) from the account F to the account T, sending N XLM to the account. Note that F and T are seed strings but can also be specified as "root" as shorthand for the root account for the test instance. diff --git a/docs/run-core-node/configuring.mdx b/docs/run-core-node/configuring.mdx new file mode 100644 index 000000000..f95e00906 --- /dev/null +++ b/docs/run-core-node/configuring.mdx @@ -0,0 +1,264 @@ +--- +title: Configuring +order: 40 +--- + +import { CodeExample } from "@site/src/components/CodeExample"; + +After you've [installed](./installation.mdx) Stellar Core, your next step is to complete a configuration file that specifies crucial things about your node — like whether it connects to the testnet or the public network, what database it writes to, and which other nodes are in its [quorum set](#choosing-your-quorum-set). You do that using [TOML](https://github.com/toml-lang/toml), and by default Stellar Core loads that file from `./stellar-core.cfg`. You can specify a different file to load using the command line: + +`$ stellar-core --conf betterfile.cfg ` + +This section of the docs will walk you through the key fields you'll need to include in your config file to get your node up and runninig. + +## Example Configurations + +This doc works best in conjunction with concrete config examples, so as you read through it, you may want to check out the following: + +- The [complete example config](https://github.com/stellar/stellar-core/blob/master/docs/stellar-core_example.cfg) documents all possible configuration elements, as well as their default values. It's got every knob you can twiddle and every setting you can tweak along wiith detailed explanations of how to twiddle and tweak them. You don't need to put everything from the complete example config into your config file — fields you omit will assume the default setting, and the default setting will generally serve you well — but there are a few required fields, and this doc will explain what they are. + +- If you want to connect to the testnet, check out the [example test network config](https://github.com/stellar/docker-stellar-core-horizon/blob/master/testnet/core/etc/stellar-core.cfg). As you can see, most of the fields from the [complete example config](https://github.com/stellar/stellar-core/blob/master/docs/stellar-core_example.cfg) are omitted since the default settings work fine. You can easily tailor this config to meet your testnet needs. + +- If you want to connect to the public network, check out this [public network config for a Full Validator](https://github.com/stellar/packages/blob/master/docs/examples/pubnet-validator-full/stellar-core.cfg). It includes a properly crafted quorum set with all the current [Tier 1 validators](./tier-1-orgs.mdx), which is a good place to start for most configurations. This node is set up to both [validate](#validating) and write history to a [public archive](./publishing-history-archives.mdx), but you can disable either feature by adjusting this config so it's a little lighter. + +## Database + +Stellar Core stores two copies of the ledger: one in a SQL database and one in XDR files on local disk called [buckets](#buckets). The database is consulted during consensus, and modified atomically when a transaction set is applied to the ledger. It's random access, fine-grained, and fast. + +While a SQLite database works with Stellar Core, we generally recommend using a separate PostgreSQL server. A Postgres database is the bread and butter of Stellar Core. + +You specify your node's database in the aptly named `DATABASE` field of your config file, which you can can read more about in the [complete example config](https://github.com/stellar/stellar-core/blob/master/docs/stellar-core_example.cfg#L23). It defaults to an in-memory database, but you can specify a path as per the example. + +If using Postgresql, We recommend you configure your local database to be accessed over a Unix domain socket as well as updating the below Postgresql configuration parameters: + +``` +# !!! DB connection should be over a Unix domain socket !!! +# shared_buffers = 25% of available system ram +# effective_cache_size = 50% of available system ram +# max_wal_size = 5GB +# max_connections = 150 +``` + +## Buckets + +Stellar-core also stores a duplicate copy of the ledger in the form of flat XDR files called "buckets." These files are placed in a directory specified in the config file as `BUCKET_DIR_PATH`, which defaults to `buckets`. The bucket files are used for hashing and transmission of ledger differences to history archives. + +Buckets should be stored on a fast local disk with sufficient space to store several times the size of the current ledger. + +For the most part, the contents of both the database and buckets directories can be ignored as they are managed by Stellar Core. However, when running Stellar Core for the first time, you must initialize both with the following command: + +`$ stellar-core new-db` + +This command initializes the database and bucket directories, and then exits. You can also use this command if your DB gets corrupted and you want to restart it from scratch. + +## Network Passphrase + +Use the `NETWORK_PASSPHRASE` field to specify whether your node connects to the [testnet](../glossary/testnet.mdx) or the public network. Your choices: + +- `NETWORK_PASSPHRASE="Test SDF Network ; September 2015"` +- `NETWORK_PASSPHRASE="Public Global Stellar Network ; September 2015"` + +For more about the Network Passphrase and how it works, check out the [glossary entry](../glossary/network-passphrase.mdx). + +## Validating + +By default, Stellar Core isn't set up to validate. If you want your node to be a [Basic Validator](./index.mdx#basic-validator) or a [Full Validator](index.mdx#full-validator), you need to configure it to do so, which means preparing it to take part in [SCP](../glossary/scp.mdx) and sign messages pledging that the network agrees to a particular transaction set. + +Configuring a node to participate in SCP and sign messages is a three step process: + +- Create a keypair `stellar-core gen-seed` +- Add `NODE_SEED="SD7DN..."` to your configuration file, where `SD7DN...` is the secret key from the keypair +- Add `NODE_IS_VALIDATOR=true` to your configuration file + +If you want other validators to add your node to their quorum sets, you should also share your public key (GDMTUTQ... ) by publishing a stellar.toml file on your homedomain following specs laid out in [SEP-20](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0020.md). + +It's essential to store and safeguard your node's secret key: if someone else has access to it, they can send messages to the network and they will appear to originate from your node. Each node you run should have its own secret key. + +If you run more than one node, set the `HOME_DOMAIN` common to those nodes using the `NODE_HOME_DOMAIN` property. Doing so will allow your nodes to be grouped correctly during [quorum set generation](#home-domains-array). + +## Choosing Your Quorum Set + +No matter what kind of node you run — Basic Validator, Full Validator, or Archiver — you need to select a quorum set, which consists of validators (grouped by organization) that your node checks with to determine whether to apply a transaction set to a ledger. If you want to know more about how quorum sets work, check this article about [how Stellar approaches quorums](https://www.stellar.org/developers-blog/why-quorums-matter-and-how-stellar-approaches-them). If you want to see what a quorum set consisting of all the Tier 1 validators looks like — a tried and true setup — check out the [public network config for a Full Validator](https://github.com/stellar/packages/blob/master/docs/examples/pubnet-validator-full/stellar-core.cfg) + +A good quorum set: + +- aligns with your organization’s priorities +- has enough redundancy to handle arbitrary node failures +- maintains good quorum intersection + +Since crafting a good quorum set is a difficult thing to do, stellar core _automatically_ generates a quorum set for you based on structured information you provide in your config file. You choose the validators you want to trust; stellar core configures them into an optimal quorum set. + +To generate a quorum set, stellar core: + +- Groups validators run by the same organization into a subquorum +- Sets the threshold for each of those subquorums +- Gives weights to those subquorums based on quality + +While this does not absolve you of all responsibility — you still need to pick trustworthy validators and keep an eye on them to ensure that they’re consistent and reliable — it does make your life easier and reduces the chances for human error. + +### Validator discovery + +When you add a validating node to your quorum set, it’s generally because you trust the _organization_ running the node: you trust SDF, not some anonymous Stellar public key. + +In order to create a self-verified link between a node and the organization that runs it, a validator declares a home domain on-chain using a [`set_options` operation](../start/list-of-operations.mdx#set-options), and publishes organizational information in a stellar.toml file hosted on that domain. To find out how that works, take a look at [SEP-20](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0020.md). + +As a result of that link, you can look up a node by its Stellar public key and check the stellar.toml to find out who runs it. It’s possible to do that manually, but you can also just consult the list of nodes on [Stellarbeat.io](https://stellarbeat.io/nodes). If you decide to trust an organization, you can use that list to collect the information necessary to add their nodes to your configuration. + +When you look at that list, you will discover that the most reliable organizations actually run more than one validator, and adding all of an organization’s nodes to your quorum set creates the redundancy necessary to sustain arbitrary node failure. When an organization with a trio of nodes takes one down for maintenance, for instance, the remaining two vote on the organization’s behalf, and the organization’s network presence persists. + +One important thing to note: you need to either depend on exactly one entity OR have **at least 4 entities** for automatic quorum set configuration to work properly. At least 4 is the better option. + +### Home domains array + +To create your quorum set, Stellar Core relies on two arrays of tables: `[[HOME_DOMAINS]]` and `[[VALIDATORS]]`. Check out the [example config](https://github.com/stellar/stellar-core/blob/master/docs/stellar-core_example.cfg#L372) to see those arrays in action. + +`[[HOME_DOMAINS]]` defines a superset of validators: when you add nodes hosted by the same organization to your configuration, they share a home domain, and the information in the `[[HOME_DOMAINS]]` table, specifically the quality rating, will automatically apply to every one of those validators. + +For each organization you want to add, create a separate `[[HOME_DOMAINS]]` table, and complete the following required fields: + +| Field | Requirements | Description | +| --- | --- | --- | +| HOME_DOMAIN | string | URL of home domain linked to a group of validators | +| QUALITY | string | Rating for organization's nodes: `HIGH`, `MEDIUM`, or `LOW` | + +Here’s an example: + + + +``` +[[HOME_DOMAINS]] +HOME_DOMAIN="testnet.stellar.org" +QUALITY="HIGH" + +[[HOME_DOMAINS]] +HOME_DOMAIN="some-other-domain" +QUALITY="LOW" +``` + + + +### Validators array + +For each node you would like to add to your quorum set, complete a `[[VALIDATORS]]` table with the following fields: + +| Field | Requirements | Description | +| --- | --- | --- | +| NAME | string | A unique alias for the node | +| QUALITY | string | Rating for node (required unless specified in `[[HOME_DOMAINS]]`): `HIGH`, `MEDIUM`, or `LOW`. | +| HOME_DOMAIN | string | URL of home domain linked to validator | +| PUBLIC_KEY | string | Stellar public key associated with validator | +| ADDRESS | string | Peer:port associated with validator (optional) | +| HISTORY | string | archive GET command associated with validator (optional) | + +If the node's `HOME_DOMAIN` aligns with an organization defined in the `[[HOME_DOMAINS]]` array, the quality rating specified there will apply to the node. If you’re adding an individual node that is _not_ covered in that array, you’ll need to specify the `QUALITY` here. + +Here’s an example: + + + +``` +[[VALIDATORS]] +NAME="sdftest1" +HOME_DOMAIN="testnet.stellar.org" +PUBLIC_KEY="GDKXE2OZMJIPOSLNA6N6F2BVCI3O777I2OOC4BV7VOYUEHYX7RTRYA7Y" +ADDRESS="core-testnet1.stellar.org" +HISTORY="curl -sf http://history.stellar.org/prd/core-testnet/core_testnet_001/{0} -o {1}" + +[[VALIDATORS]] +NAME="sdftest2" +HOME_DOMAIN="testnet.stellar.org" +PUBLIC_KEY="GCUCJTIYXSOXKBSNFGNFWW5MUQ54HKRPGJUTQFJ5RQXZXNOLNXYDHRAP" +ADDRESS="core-testnet2.stellar.org" +HISTORY="curl -sf http://history.stellar.org/prd/core-testnet/core_testnet_002/{0} -o {1}" + +[[VALIDATORS]] +NAME="rando-node" +QUALITY="LOW" +HOME_DOMAIN="rando.com" +PUBLIC_KEY="GC2V2EFSXN6SQTWVYA5EPJPBWWIMSD2XQNKUOHGEKB535AQE2I6IXV2Z" +ADDRESS="core.rando.com" +``` + + + +### Validator quality + +`QUALITY` is a required field for each node you add to your quorum set. Whether you specify it for a suite of nodes in `[[HOME_DOMAINS]]` or for a single node in `[[VALIDATORS]]`, it means the same thing, and you have the same three rating options: HIGH, MEDIUM, or LOW. + +**HIGH** quality validators are given the most weight in automatic quorum set configuration. Before assigning a high quality rating to a node, make sure it has low latency and good uptime, and that the organization running the node is reliable and trustworthy. + +A high quality validator: + +- publishes an archive +- belongs to a suite of nodes that provide redundancy + +Choosing redundant nodes is good practice. The archive requirement is programmatically enforced. + +**MEDIUM** quality validators are nested below high quality validators, and their combined weight is equivalent to a _single high quality entity_. If a node doesn't publish an archive, but you deem it reliable or have an organizational interest in including in your quorum set, give it a medium quality rating. + +**LOW** quality validators are nested below medium quality validators, and their combined weight is equivalent to a _single medium quality entity_. Should they prove reliable over time, you can upgrade their rating to medium to give them a bigger role in your quorum set configuration. + +### Automatic quorum set generation + +Once you add validators to your configuration, stellar core automatically generates a quorum set using the following rules: + +- Validators with the same home domain are automatically grouped together and given a threshold requiring a simple majority (2f+1) +- Heterogeneous groups of validators are given a threshold assuming byzantine failure (3f+1) +- Entities are grouped by QUALITY and nested from HIGH to LOW +- HIGH quality entities are at the top, and are given decision-making priority +- The combined weight of MEDIUM quality entities equals a single HIGH quality entity +- The combined weight of LOW quality entities equals a single MEDIUM quality entity + +### Quorum and Overlay Network + +It is generally a good idea to give information to your validator on other validators that you rely on. This is achieved by configuring `KNOWN_PEERS` and `PREFERRED_PEERS` with the addresses of your dependencies. + +Additionally, configuring `PREFERRED_PEER_KEYS` with the keys from your quorum set might be a good idea to give priority to the nodes that allow you to reach consensus. + +Without those settings, your validator depends on other nodes on the network to forward you the right messages, which is typically done as a best effort. + +### Updating and Coordinating Your Quorum Set + +When you join the ranks of node operators, it's also important to join the conversation. The best way to do that: get on the #validators channel on the [Stellar Keybase](https://keybase.io/team/stellar.public) and sign up for the [validators google group](https://groups.google.com/forum/#!forum/stellar-validators). That way, you can and coordinate changes with the rest of the network. + +When you need to make changes to your validator or to your quorum set — say you take a validator down for maintenance or add new validators to your node's quorum set — it's crucial to stage the changes to preserve quorum intersection and general good health of the network: + +- Don't remove too many nodes from your quorum set _before_ the nodes are taken down. If different validators remove different sets, the remaining sets may not overlap, which could cause network splits + +- Don't add too many nodes in your quorum set at the same time. If not done carefully, the new nodes could overpower your configuration + +When you want to add or remove nodes, start by making changes to your own nodes' quorum sets, and then coordinate work with others to reflect those changes gradually. + +## History + +Stellar Core normally interacts with one or more history archive, which are configurable facilities where [Full Validators](index.mdx#full-validator) and [Archivers](index.mdx#archiver) store flat files containing history checkpoints: bucket files and history logs. History archives are usually off-site commodity storage services such as Amazon S3, Google Cloud Storage, Azure Blob Storage, or custom SCP/SFTP/HTTP servers. To find out how to _publish_ a history archive, consult [Publishing History Archives](./publishing-history-archives.mdx). + +No matter what kind of node you're running, you should configure it to `get` history from one or more public archives. You can configure any number of archives to download from: Stellar Core will automatically round-robin between them. + +When you're [choosing your quorum set](#choosing-your-quorum-set), you should include high-quality nodes — which, by defintion, publish archives — and add the location for each node's archive in the `HISTORY` field in the [validators array](#validators-array). + +You can also use command templates in the config file to specify additional archives you'd like to use and how to access them. The [example config](https://github.com/stellar/stellar-core/blob/master/docs/stellar-core_example.cfg) shows how to configure a history archive through command templates. + +Note: if you notice a lot of errors related to downloading archives, you should check that all archives in your configuration are up to date. + +## Automatic Maintenance + +Some tables in Stellar Core's database act as a publishing queue for external systems such as Horizon and generate **meta data** for changes happening to the distributed ledger. + +If not managed properly those tables will grow without bounds. To avoid this, a built-in scheduler will delete data from old ledgers that are not used anymore by other parts of the system (external systems included). + +The settings that control the automatic maintenance behavior are: `AUTOMATIC_MAINTENANCE_PERIOD`, `AUTOMATIC_MAINTENANCE_COUNT` and `KNOWN_CURSORS`. + +By default, Stellar Core will perform this automatic maintenance, so be sure to disable it until you have done the appropriate data ingestion in downstream systems (Horizon for example sometimes needs to reingest data). + +If you need to regenerate the metadata, the simplest way is to replay ledgers for the range you're interested in after (optionally) clearing the database with `newdb`. + +## Metadata Snapshots and Restoration + +Some deployments of Stellar Core and Horizon will want to retain metadata for the _entire history_ of the network. This metadata can be quite large and computationally expensive to regenerate anew by replaying ledgers in stellar-core from an empty initial database state, as described in the previous section. + +This can be especially costly if run more than once. For instance, when bringing a new node online. Or even if running a single node with Horizon, having already ingested the metadata _once_: a subsequent version of Horizon may have a schema change that entails re-ingesting it _again_. + +Some operators therefore prefer to shut down their stellar-core (and/or Horizon) processes and _take filesystem-level snapshots_ or _database-level dumps_ of the contents of Stellar Core's database and bucket directory, and/or Horizon's database, after metadata generation has occurred the first time. Such snapshots can then be restored, putting stellar-core and/or Horizon in a state containing metadata without performing full replay. + +Any reasonably recent state will do — if such a snapshot is a little old, stellar-core will replay ledgers from whenever the snapshot was taken to the current network state anyways — but this procedure can greatly accelerate restoring validator nodes, or cloning them to create new ones. diff --git a/docs/run-core-node/index.mdx b/docs/run-core-node/index.mdx new file mode 100644 index 000000000..187243ea7 --- /dev/null +++ b/docs/run-core-node/index.mdx @@ -0,0 +1,85 @@ +--- +title: "Overview" +order: 10 +--- + +import { Alert } from "@site/src/components/Alert"; + +Stellar is a peer-to-peer network made up of nodes, which are computers that keep a common distributed [ledger](../glossary/ledger.mdx), and that communicate to validate and add [transactions](../glossary/transactions.mdx) to it. Nodes use a program called Stellar Core — an implementation of the [Stellar Consensus Protocol](../glossary/scp.mdx) — to stay in sync as they work to agree on the validity of transaction sets and to apply them to the ledger. Generally, nodes reach consensus, apply a transaction set, and update the ledger every 3-5 seconds. + +You don’t need to run a node to build on Stellar: you can start developing with your [SDK of choice](../software-and-sdks/index.mdx), and use public instances of Horizon to query the ledger and submit transactions right away. In fact, the Stellar Development Foundation offers two public instances of Horizon — one for the public network and one for the testnet — which you can read more about in our [API reference docs](/api/introduction/). [Lobstr](https://horizon.stellar.lobstr.co), [Public Node](https://horizon.publicnode.org/), and [Coinqvest](https://horizon.stellar.coinqvest.com) also offer public Horizon instances. + +Even if you _do_ want to run your [own instance of Horizon](../run-api-server/index.mdx), it bundles its own version of Core and manages its lifetime entirely, so there's no need to run a standalone instance. + +If you’re serious about building on Stellar, have a production-level product or service that requires high-availability access network, or want to help increase network health and decentralization, then you probably _do_ want to run a node, or even a trio of nodes (more on that in the [Tier 1 section](./tier-1-orgs.mdx)). At that point, you have a choice: you can pay a service provider like [Blockdaemon](https://app.blockdaemon.com/marketplace/categories/-/stellar-horizon) to set up and run your node for you, or you can do it yourself. + +If you’re going the DIY route, this section of the docs is for you. It explains the technical and operational aspects of installing, configuring, and maintaining a Stellar Core node, and should help you figure out the best way to set up your Stellar integration. + +The basic flow, which you can navigate through using the menu on the left, goes like this: + +- Choose which type of node you want to run +- Prepare Your Environment +- Install Stellar Core +- Configure Stellar Core +- Join the network +- Monitor and maintain your node +- Join the validators channels to stay on top of critical upgrades and network votes + +## Types of nodes + +All nodes perform the same basic functions: they run Stellar Core, connect to peers, submit transactions, store the state of the ledger in a SQL [database](./configuring.mdx#database), and keep a duplicate copy of the ledger in flat XDR files called [buckets](./configuring.mdx#buckets). Though all nodes also support [Horizon](../run-api-server/index.mdx), the Stellar API, this is a deprecated way of architecting your system and will be discontinued soon. If you want to run Horizon, you don't need a separate Stellar Core node. + +In addition to those basic functions, there are two key configuration options that determine how a node behaves. A node can: + +- Participate in consensus to [validate transactions](./configuring.mdx#validating) +- Publish an [archive](./publishing-history-archives.mdx) that other nodes can consult to find the complete history of the network. + +To make things easier, we’ll define three types of nodes based on permutations of those two options: **Basic Validator**, **Full Validator**, and **Archiver**. You’ll notice that they _all_ support Horizon and submit transactions to the network: + +| Type of Node | Supports Horizon | Submits Transactions | Validates Transactions | Publishes History | +| --- | --- | --- | --- | --- | +| **Basic Validator** | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | +| **Full Validator** | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | +| **Archiver** | :heavy_check_mark: | :heavy_check_mark: | | :heavy_check_mark: | + + + +In the past, there was also a **Watcher** node, which was designed to run alongside Horizon for transaction submission and observing ledger changes, but not participate in validation or history publication. + +This architecture was **deprecated** as of [Horizon 2.0](https://www.stellar.org/developers-blog/a-new-sun-on-the-horizon), which bundles an optimized "Captive" Core for its operational needs. + + + +So why choose one type over another? Let’s break it down a bit and take a look at what each type is good for. + +### Basic Validator + +#### Validating, no public archive + +A Basic Validator keeps track of the ledger and submits transactions for possible inclusion, but it is _not_ configured to participate publish history archives. It does require a secret key, and is [configured to participate in consensus](./configuring.mdx#validating) by voting on — and signing off on — changes to the ledger, meaning it supports the network and increases decentralization. + +The advantage: signatures can serve as official endorsements of specific ledgers in real time. That’s important if, for instance, you issue an asset on Stellar that represents a real-world asset: you can let your customers know that you will only honor transactions and redeem assets from ledgers signed by your validator, and in the unlikely scenario that something happens to the network, you can use your node as the final arbiter of truth. Setting up your node as a validator allows you to resolve any questions _up front and in writing_ about how you plan to deal with disasters and disputes. + +**Use a Basic Validator to ensure reliable access to the network and sign off on transactions.** + +### Full Validator + +#### Validating, offers public archive + +A Full Validator is the same as a Basic Validator except that it also publishes a [History Archive](./publishing-history-archives.mdx) containing snapshots of the ledger, including all transactions and their results. A Full Validator writes to an internet-facing blob store — such as AWS or Azure — so it's a bit more expensive and complex to run, but it also does the most to support the network’s resilience and decentralization. + +When other nodes join the network — or experience difficulty and temporarily fall out of sync — they can consult archives offered by Full Validators to catch up on the history of the network. Redundant archives prevent a single point of failure, and allow network participants to verify the veracity of a given history. + +Generally, organizations that run Full Validators don't use them to query network data or submit transactions. Most of those organizations are also part of — or on track to join — [Tier 1](./tier-1-orgs.mdx), which is a core group of network participants who run three Full Validators to contribute maximum redundancy. + +**Use a Full Validator to sign off on transactions and to contribute to the health and decentralization of the network.** + +### Archiver + +#### Non-validating, offers public archive + +An Archiver is a rare bird: like a Full Validator, it publishes the activity of the network in long-term storage; unlike a Full Validator, it does not participate in consensus. + +Archivers help with decentralization a bit by offering redundant accounts of the network’s history, but they don’t vote or sign ledgers, so their usefulness is fairly limited. If you run a Stellar-facing service, like a blockchain explorer, you may want to run one. Otherwise, you’re probably better off choosing one of the other two types. + +**Use an archiver if you want to referee the network. Which is unlikely.** diff --git a/docs/run-core-node/installation.mdx b/docs/run-core-node/installation.mdx new file mode 100644 index 000000000..4f2823379 --- /dev/null +++ b/docs/run-core-node/installation.mdx @@ -0,0 +1,60 @@ +--- +title: Installing +order: 30 +--- + +There are three ways to install Stellar Core: you can use a Docker image, use pre-built packages, or build from source. Using a Docker image is the quickest and easiest method, so it's a good choice for a lot of developers, but if you're running Stellar Core in production you may need to use packages or, if you want to sacrifice convenience for maximum control, build from source. Whichever method you use, you should make sure to install the latest [release](https://github.com/stellar/stellar-core/releases) since releases are cumulative and backwards compatible. + +## Docker-based installation + +### Development environments + +SDF maintains a [quickstart image](https://github.com/stellar/docker-stellar-core-horizon) that bundles Stellar Core with Horizon and postgreSQL databases. It's a quick way to set up a default, non-validating, ephemeral configuration that should work for most developers. + +In addition to SDF images, Satoshipay maintains separate Docker images for [Stellar Core](https://github.com/satoshipay/docker-stellar-core) and [Horizon](https://github.com/satoshipay/docker-stellar-horizon). The Satoshipay Stellar Core Docker image comes in a few flavors, including one with the AWS CLI installed and one with the Google Cloud SDK installed. The Horizon image supports all Horizon environment variables. + +### Production environments + +The SDF also maintains a Stellar-Core-only [standalone image](https://hub.docker.com/repository/docker/stellar/stellar-core). + +Example usage: + +``` +docker run stellar/stellar-core:latest help +docker run stellar/stellar-core:latest gen-seed +``` + +To run daemon you need to provide a configuration file: + +``` +# Initialize postgres DB (see DATABASE config option) +docker run -v "/path/to/config/dir:/etc/stellar/" stellar/stellar-core:latest new-db +# Run stellar-core daemon in the background +docker run -d -v "/path/to/config/dir:/etc/stellar/" stellar/stellar-core:latest run +``` + +The image utilizes deb packages so it's possible to confirm checksum of the stellar-core binary in the docker image matches that in the cryptographically signed deb package. See [packages](https://github.com/stellar/packages/) documentation for information on installing Ubuntu packages. To calculate checksum in the docker image you can run: + +``` +docker run --entrypoint=/bin/sha256sum stellar/stellar-core:latest /usr/bin/stellar-core +``` + +## Package-based Installation + +If you are using Ubuntu 18.04 LTS or later, we provide the latest stable releases of [stellar-core](https://github.com/stellar/stellar-core) and [stellar-horizon](https://github.com/stellar/go/tree/master/services/horizon) in Debian binary package format. + +You may choose to install these packages individually, which offers the greatest flexibility but requires **manual** creation of the relevant configuration files and configuration of a **PostgreSQL** database. + +Most people, however, choose to install the [stellar-quickstart](https://github.com/stellar/packages/blob/master/docs/quickstart.md) package which configures a **Testnet** `stellar-core` and `stellar-horizon` both backed by a local PostgreSQL database. Once installed you can easily modify either the Stellar Core configuration if you want to connect to the public network. + +## Installing from source + +See the [install from source](https://github.com/stellar/stellar-core/blob/master/INSTALL.md) for build instructions. + +## Release version + +In general you should install the latest [release](https://github.com/stellar/stellar-core/releases) build. Builds are backward compatible and are cumulative. + +The version number scheme that we follow is `protocol_version.release_number.patch_number` where: + +`protocol_version` is the maximum protocol version supported by that release (all versions are 100% backward compatible), `release_number` is bumped when a set of new features or bug fixes not impacting the protocol are included in the release, `patch_number` is used when a critical fix has to be deployed. diff --git a/docs/run-core-node/metadata.json b/docs/run-core-node/metadata.json new file mode 100644 index 000000000..2f579a424 --- /dev/null +++ b/docs/run-core-node/metadata.json @@ -0,0 +1,4 @@ +{ + "order": 60, + "title": "Run a Core Node" +} diff --git a/docs/run-core-node/monitoring.mdx b/docs/run-core-node/monitoring.mdx new file mode 100644 index 000000000..b97ef7c74 --- /dev/null +++ b/docs/run-core-node/monitoring.mdx @@ -0,0 +1,462 @@ +--- +title: Monitoring +order: 70 +--- + +import { CodeExample } from "@site/src/components/CodeExample"; + +Once your node is up and running, it's important to keep an eye on it to make sure it stays afloat and continues to contribute to the health of the overall network. To help with that, Stellar Core exposes vital information that you can use to monitor your node and diagnose potential problems. + +You can access this information using commands and inspecting Stellar Core's output, which is what the first half of this doc covers. You can also connect [Prometheus](#using-prometheus) to make monitoring easier, combine it with [Alertmanager](#configure-notifications-using-alertmanager) to automate notification, and use pre-built [Grafana dashboards](#visualize-metrics-using-grafana) to create visual representations of your node's well-being. + +However you decide to monitor, the most important thing is that you have a system in place to ensure that your integration keeps ticking. + +## General Node Information + +If you run `$ stellar-core http-command 'info'`, the output will look something like this: + + + +```json +{ + "build" : "v11.1.0", + "history_failure_rate" : "0", + "ledger" : { + "age" : 3, + "baseFee" : 100, + "baseReserve" : 5000000, + "closeTime" : 1560350852, + "hash" : "40d884f6eb105da56bea518513ba9c5cda9a4e45ac824e5eac8f7262c713cc60", + "maxTxSetSize" : 1000, + "num" : 24311579, + "version" : 11 + }, + "network" : "Public Global Stellar Network ; September 2015", + "peers" : { + "authenticated_count" : 5, + "pending_count" : 0 + }, + "protocol_version" : 10, + "quorum" : { + "qset" : { + "agree" : 6, + "delayed" : 0, + "disagree" : 0, + "fail_at" : 2, + "hash" : "d5c247", + "ledger" : 24311579, + "missing" : 1, + "phase" : "EXTERNALIZE" + }, + "transitive" : { + "critical" : null, + "intersection" : true, + "last_check_ledger" : 24311536, + "node_count" : 21 + } + }, + "startedOn" : "2019-06-10T17:40:29Z", + "state" : "Catching up", + "status" : [ "Catching up: downloading and verifying buckets: 30/30 (100%)" ] + } +} +``` + + + +Some notable fields in `info` are: + +- `build`: the build number for this Stellar Core instance +- `ledger`: the local state of your node, which may be different from the network state if your node was disconnected from the network. Some important sub-fields: + - `age`: time elapsed since this ledger closed (during normal operation less than 10 seconds) + - `num`: ledger number + - `version`: protocol version supported by this ledger +- `network` is the [network passphrase](../glossary/network-passphrase.mdx) that this core instance is using to decide whether to connect to the testnet or the public network +- `peers`: information on the connectivity to the network + - `authenticated_count`: the number of live connections + - `pending_count`: the number of connections that are not fully established yet +- `protocol_version`: the maximum version of the protocol that this instance recognizes +- `state`: the node's synchronization status relative to the network +- `quorum`: summarizes the state of the SCP protocol participants, the same as the information returned by the `quorum` command ([see below](#quorum-health)). + +## Overlay information + +The `peers` command returns information on the peers your node is connected to. + +This list is the result of both inbound connections from other peers and outbound connections from this node to other peers. + +`$ stellar-core http-command 'peers'` + + + +```json +{ + "authenticated_peers": { + "inbound": [ + { + "address": "54.161.82.181:11625", + "elapsed": 6, + "id": "sdf1", + "olver": 5, + "ver": "v9.1.0" + } + ], + "outbound": [ + { + "address": "54.211.174.177:11625", + "elapsed": 2303, + "id": "sdf2", + "olver": 5, + "ver": "v9.1.0" + }, + { + "address": "54.160.175.7:11625", + "elapsed": 14082, + "id": "sdf3", + "olver": 5, + "ver": "v9.1.0" + } + ] + }, + "pending_peers": { + "inbound": ["211.249.63.74:11625", "45.77.5.118:11625"], + "outbound": ["178.21.47.226:11625", "178.131.109.241:11625"] + } +} +``` + + + +## Quorum Health + +To help node operators monitor their quorum sets and maintain the health of the overall network, Stellar Core also provides metrics on other nodes in your quorum set. You should monitor them to make sure they're up and running, and that your quorum set is maintaining good overlap with the rest of the network. + +### Quorum set diagnostics + +The `quorum` command allows to diagnose problems with the quorum set of the local node. + +If you run: + +`$ stellar-core http-command 'quorum'` + +The output will look something like: + + + +```json +{ + "node": "GCTSFJ36M7ZMTSX7ZKG6VJKPIDBDA26IEWRGV65DVX7YVVLBPE5ZWMIO", + "qset": { + "agree": 6, + "delayed": null, + "disagree": null, + "fail_at": 2, + "fail_with": ["sdf_watcher1", "sdf_watcher2"], + "hash": "d5c247", + "ledger": 24311847, + "missing": ["stronghold1"], + "phase": "EXTERNALIZE", + "value": { + "t": 3, + "v": [ + "sdf_watcher1", + "sdf_watcher2", + "sdf_watcher3", + { + "t": 3, + "v": ["stronghold1", "eno", "tempo.eu.com", "satoshipay"] + } + ] + } + }, + "transitive": { + "critical": [["GDM7M262ZJJPV4BZ5SLGYYUTJGIGM25ID2XGKI3M6IDN6QLSTWQKTXQM"]], + "intersection": true, + "last_check_ledger": 24311536, + "node_count": 21 + } +} +``` + + + +This output has two main sections: `qset` and `transitive`. The former describes the node and its quorum set; the latter describes the transitive closure of the node's quorum set. + +### Per-node Quorum-set Information + +Entries to watch for in the `qset` section — which describe the node and its quorum set — are: + +- `agree` : the number of nodes in the quorum set that agree with this instance. +- `delayed` : the nodes that are participating in consensus but seem to be behind. +- `disagree`: the nodes that are participating but disagreed with this instance. +- `fail_at` : the number of failed nodes that _would_ cause this instance to halt. +- `fail_with`: an example of such potential failure. +- `missing` : the nodes that were missing during this consensus round. +- `value` : the quorum set used by this node (`t` is the threshold expressed as a number of nodes). + +In the example above, 6 nodes are functioning properly, one is down (`stronghold1`), and the instance will fail if any two nodes still working (or one node and one inner-quorum-set) fail as well. + +If a node is stuck in state `Joining SCP`, this command allows to quickly find the reason: + +- too many validators missing (down or without a good connectivity), solutions are: + - [adjust your quorum set](./configuring.mdx#choosing-a-quorum-set) based on the nodes that are not missing + - try to get a [better connectivity path](./configuring.mdx#quorum-and-overlay-network) to the missing validators +- network split would cause SCP to stick because of nodes that disagree. This would happen if either there is a bug in SCP, the network does not have quorum intersection, or the disagreeing nodes are misbehaving (compromised, etc). + +Note that the node not being able to reach consensus does not mean that the network as a whole will not be able to reach consensus (and the opposite is true: the network may fail because of a different set of validators failing). + +You can get a sense of the quorum set health of a different node using using: `$ stellar-core http-command 'quorum?node=$sdf1` or `$ stellar-core http-command 'quorum?node=@GABCDE` + +Overall network health can be evaluated by walking through all nodes and looking at their health. Note that this is only an approximation, as remote nodes may not have received the same messages (in particular: `missing` for other nodes is not reliable). + +### Transitive Closure Summary Information + +When showing quorum-set information about the local node, a summary of the transitive closure of the quorum set is also provided in the `transitive` field. This has several important sub-fields: + +- `last_check_ledger` : the last ledger in which the transitive closure was checked for quorum intersection. This will reset when the node boots and whenever a node in the transitive quorum changes its quorum set. It may lag behind the last-closed ledger by a few ledgers depending on the computational cost of checking quorum intersection. +- `node_count` : the number of nodes in the transitive closure, which are considered when calculating quorum intersection. +- `intersection` : whether or not the transitive closure enjoyed quorum intersection at the most recent check. This is of **utmost importance** in preventing network splits. It should always be true. If it is ever false, one or more nodes in the transitive closure of the quorum set is _currently_ misconfigured, and the network is at risk of splitting. Corrective action should be taken immediately, for which two additional sub-fields will be present to help suggest remedies: + - `last_good_ledger` : this will note the last ledger for which the `intersection` field was evaluated as true; if some node reconfigured at or around that ledger, reverting that configuration change is the easiest corrective action to take. + - `potential_split` : this will contain a pair of lists of validator IDs, which is a potential pair of disjoint quorums allowed by the current configuration. In other words, a possible split in consensus allowed by the current configuration. This may help narrow down the cause of the misconfiguration: likely it involves too-low a consensus threshold in one of the two potential quorums, and/or the absence of a mandatory trust relationship that would bridge the two. +- `critical`: an "advance warning" field that lists nodes that _could cause_ the network to fail to enjoy quorum intersection, if they were misconfigured sufficiently badly. In a healthy transitive network configuration, this field will be `null`. If it is non-`null` then the network is essentially "one misconfiguration" (of the quorum sets of the listed nodes) away from no longer enjoying quorum intersection, and again, corrective action should be taken: careful adjustment to the quorum sets of _nodes that depend on_ the listed nodes, typically to strengthen quorums that depend on them. + +### Detailed transitive quorum analysis + +The quorum endpoint can also retrieve detailed information for the transitive quorum. + +This is a format that's easier to process than what `scp` returns as it doesn't contain all SCP messages. + +`$ stellar-core http-command 'quorum?transitive=true'` + +The output looks something like: + + + +```json +{ + "critical": null, + "intersection": true, + "last_check_ledger": 121235, + "node_count": 4, + "nodes": [ + { + "distance": 0, + "heard": 121235, + "node": "GB7LI", + "qset": { + "t": 2, + "v": ["sdf1", "sdf2", "sdf3"] + }, + "status": "tracking", + "value": "[ txH: d99591, ct: 1557426183, upgrades: [ ] ]", + "value_id": 1 + }, + { + "distance": 1, + "heard": 121235, + "node": "sdf2", + "qset": { + "t": 2, + "v": ["sdf1", "sdf2", "sdf3"] + }, + "status": "tracking", + "value": "[ txH: d99591, ct: 1557426183, upgrades: [ ] ]", + "value_id": 1 + }, + { + "distance": 1, + "heard": 121235, + "node": "sdf3", + "qset": { + "t": 2, + "v": ["sdf1", "sdf2", "sdf3"] + }, + "status": "tracking", + "value": "[ txH: d99591, ct: 1557426183, upgrades: [ ] ]", + "value_id": 1 + }, + { + "distance": 1, + "heard": 121235, + "node": "sdf1", + "qset": { + "t": 2, + "v": ["sdf1", "sdf2", "sdf3"] + }, + "status": "tracking", + "value": "[ txH: d99591, ct: 1557426183, upgrades: [ ] ]", + "value_id": 1 + } + ] +} +``` + + + +The output begins with the same summary information as in the `transitive` block of the non-transitive query (if queried for the local node), but also includes a `nodes` array that represents a walk of the transitive quorum centered on the query node. + +Fields are: + +- `node` : the identity of the validator +- `distance` : how far that node is from the root node (ie. how many quorum set hops) +- `heard` : the latest ledger sequence number that this node voted on +- `qset` : the node's quorum set +- `status` : one of `behind|tracking|ahead` (compared to the root node) or `missing|unknown` (when there are no recent SCP messages for that node) +- `value_id` : a unique ID for what the node is voting for (allows to quickly tell if nodes are voting for the same thing) +- `value` : what the node is voting for + +## Using Prometheus + +Monitoring `stellar-core` using Prometheus is by far the simplest solution, especially if you already have a Prometheus server within your infrastructure. Prometheus is a free and open source time-series database with a simple yet incredibly powerful query language `PromQL`. Prometheus is also tightly integrated with Grafana, so you can render complex visualisations with ease. + +In order for Prometheus to scrape `stellar-core` application metrics, you will need to install the stellar-core-prometheus-exporter (`apt-get install stellar-core-prometheus-exporter`) and configure your Prometheus server to scrape this exporter (default port: `9473`). On top of that grafana can be used to visualize metrics. + +### Install a Prometheus server within your infrastructure + +Installing and configuring a Prometheus server is out of scope of this document, however it is a fairly simple process: Prometheus is a single Go binary which you can download from https://prometheus.io/docs/prometheus/latest/installation/. + +### Install the stellar-core-prometheus-exporter + +The stellar-core-prometheus-exporter is an exporter that scrapes the `stellar-core` metrics endpoint (`http://localhost:11626/metrics`) and renders these metrics in the Prometheus text-based format available for Prometheus to scrape and store in its timeseries database. + +The exporter needs to be installed on every Stellar Core node you wish to monitor. + +- `apt-get install stellar-core-prometheus-exporter` + +You will need to open up port `9473` between your Prometheus server and all your Stellar Core nodes for your Prometheus server to be able to scrape metrics. + +### Point Prometheus to stellar-core-prometheus-exporter + +Pointing your Prometheus instance to the exporter can be achieved by manually configuring a scrape job; however, depending on the number of hosts you need to monitor this can quickly become unwieldy. Luckily, the process can also be automated using Prometheus' various "service discovery" plugins. For example with AWS hosted instance you can use the `ec2_sd_config` plugin. + +#### Manual + + + +```yaml +- job_name: "stellar-core" + scrape_interval: 10s + scrape_timeout: 10s + static_configs: + - targets: [ + "core-node-001.example.com:9473", + "core-node-002.example.com:9473", + ] # stellar-core-prometheus-exporter default port is 9473 + - labels: + application: "stellar-core" +``` + + + +#### Using Service Discovery (EC2) + + + +```yaml +- job_name: stellar-core + scrape_interval: 10s + scrape_timeout: 10s + ec2_sd_configs: + - region: eu-west-1 + port: 9473 + relabel_configs: + # ignore stopped instances + - source_labels: [__meta_ec2_instance_state] + regex: stopped + action: drop + # only keep with `core` in the Name tag + - source_labels: [__meta_ec2_tag_Name] + regex: "(.*core.*)" + action: keep + # use Name tag as instance label + - source_labels: [__meta_ec2_tag_Name] + regex: "(.*)" + action: replace + replacement: "${1}" + target_label: instance + # set application label to stellar-core + - source_labels: [__meta_ec2_tag_Name] + regex: "(.*core.*)" + action: replace + replacement: stellar-core + target_label: application +``` + + + +### Create Alerting Rules + +Once Prometheus scrapes metrics we can add alerting rules. Recommended rules are [**here**](https://github.com/stellar/packages/blob/master/docs/stellar-core-alerting.rules) (require Prometheus 2.0 or later). Copy rules to _/etc/prometheus/stellar-core-alerting.rules_ on the Prometheus server and add the following to the prometheus configuration file to include the file: + + + +```yaml +rule_files: + - "/etc/prometheus/stellar-core-alerting.rules" +``` + + + +Rules are documented in-line,and we strongly recommend that you review and verify all of them as every environment is different. + +### Configure Notifications Using Alertmanager + +Alertmanager is responsible for sending notifications. Installing and configuring an Alertmanager server is out of scope of this document, however it is a fairly simple process. Official documentation is [here](https://github.com/prometheus/alertmanager/). + +All recommended alerting rules have "severity" label: + +- **critical** normally require immediate attention. They indicate an ongoing or very likely outage. We recommend that critical alerts notify administrators 24x7 +- **warning** normally can wait until working hours. Warnings indicate problems that likely do not have production impact but may lead to critical alerts or outages if left unhandled + +The following example alertmanager configuration demonstrates how to send notifications using different methods based on severity label: + + + +```yaml +global: + smtp_smarthost: localhost:25 + smtp_from: alertmanager@example.com +route: + receiver: default-receiver + group_by: [alertname] + group_wait: 30s + group_interval: 5m + repeat_interval: 1h + routes: + - receiver: critical-alerts + match: + severity: critical + - receiver: warning-alerts + match: + severity: warning +receivers: + - name: critical-alerts + pagerduty_configs: + - routing_key: + - name: warning-alerts + slack_configs: + - api_url: https://hooks.slack.com/services/slack/warning/channel/webhook + - name: default-receiver + email_configs: + - to: alerts-fallback@example.com +``` + + + +In the above examples alerts with severity "critical" are sent to pagerduty and warnings are sent to slack. + +### Useful Exporters + +You may find the below exporters useful for monitoring your infrastructure as they provide incredible insight into your operating system and database metrics. Installing and configuring these exporters is out of the scope of this document but should be relatively straightforward. + +- [node_exporter](https://prometheus.io/docs/guides/node-exporter/) can be used to track all operating system metrics. +- [postgresql_exporter](https://github.com/wrouesnel/postgres_exporter) can be used to monitor the local stellar-core database. + +### Visualize metrics using Grafana + +Once you've configured Prometheus to scrape and store your stellar-core metrics, you will want a nice way to render this data for human consumption. Grafana offers the simplest and most effective way to achieve this. Installing Grafana is out of scope of this document but is a very simple process, especially when using the [prebuilt apt packages](https://grafana.com/docs/installation/debian/#apt-repository) + +We recommend that administrators import the following two dashboards into their grafana deployments: + +- [**Stellar Core Monitoring**](https://grafana.com/grafana/dashboards/10603) - shows the most important metrics, node status and tries to surface common problems. It's a good troubleshooting starting point +- [**Stellar Core Full**](https://grafana.com/grafana/dashboards/10334) - shows a simple health summary as well as all metrics exposed by the `stellar-core-prometheus-exporter`. It's much more detailed than the _Stellar Core Monitoring_ and might be useful during in-depth troubleshooting diff --git a/docs/run-core-node/network-upgrades.mdx b/docs/run-core-node/network-upgrades.mdx new file mode 100644 index 000000000..f86df32fb --- /dev/null +++ b/docs/run-core-node/network-upgrades.mdx @@ -0,0 +1,59 @@ +--- +title: Upgrading the Network +order: 80 +--- + +import { CodeExample } from "@site/src/components/CodeExample"; + +The network itself has network wide settings that can be updated. + +This is performed by validators voting for and agreeing to new values the same way that consensus is reached for transaction sets, etc. + +A node can be configured to vote for upgrades using the `upgrades` endpoint . See [Commands](./commands.mdx) for more information. + +The network settings are: + +- the version of the protocol used to process transactions +- the maximum number of transactions that can be included in a given ledger close +- the cost (fee) associated with processing operations +- the base reserve used to calculate the lumen balance needed to store things in the ledger + +When the network time is later than the `upgradetime` specified in the upgrade settings, the validator will vote to update the network to the value specified in the upgrade setting. If the network time is passed the `upgradetime` by more than 12 hours, the upgrade will be ignored + +When a validator is armed to change network values, the output of `info` will contain information about the vote. + +For a new value to be adopted, the same level of consensus between nodes needs to be reached as for transaction sets. + +## Important notes on network wide settings + +Changes to network wide settings have to be orchestrated properly between validators as well as non validating nodes: + +- a change is vetted between operators (changes can be bundled) +- an effective date in the future is picked for the change to take effect (controlled by `upgradetime`) +- if applicable, communication is sent out to all network users + +An improper plan may cause issues such as: + +- nodes missing consensus (aka "getting stuck"), and having to use history to rejoin +- network reconfiguration taking effect at a non deterministic time (causing fees to change ahead of schedule for example) + +For more information look at [Network Upgrade](./network-upgrades.mdx). + +## Example upgrade command + +Example here is to upgrade the protocol version to version 9 on January-31-2018. + +1. `$ stellar-core http-command 'upgrades?mode=set&upgradetime=2018-01-31T20:00:00Z&protocolversion=9'` +1. `$ stellar-core http-command info` + +At this point `info` will tell you that the node is setup to vote for this upgrade: + + + +```json +"status" : [ + "Armed with network upgrades: upgradetime=2018-01-31T20:00:00Z, protocolversion=9" +] +``` + + diff --git a/docs/run-core-node/prerequisites.mdx b/docs/run-core-node/prerequisites.mdx new file mode 100644 index 000000000..357b5954b --- /dev/null +++ b/docs/run-core-node/prerequisites.mdx @@ -0,0 +1,36 @@ +--- +title: Prerequisites +order: 20 +--- + +You can install Stellar Core a [number of different ways](./installation.mdx), and once you do, you can [configure](./configuring.mdx) it to participate in the network on a several [different levels](./index.mdx#types-of-nodes): it can be either a Basic Validator or a Full Validator. No matter how you install Stellar Core or what kind of node you run, however, you need to set up to connect to the peer-to-peer network and store the state of the ledger in a SQL [database](configuring.mdx#database). + +## Compute Requirements + +We recently asked Stellar Core operators about their setups, and should have some updated information soon based on their responses. So stay tuned. In early 2018, Stellar Core with PostgreSQL running on the same machine worked well on a [m5.large](https://aws.amazon.com/ec2/instance-types/m5/) in AWS (dual core 2.5 GHz Intel Xeon, 8 GB RAM). Storage-wise, 20 GB was enough in 2018, but the ledger has grown a lot since then, and most people seem to have at least 1TB on hand. + +If you decide to run Stellar Core on the same machine as Horizon (though note that this is a deprecated architecture, since Horizon now bundles Core for its needs), you will additionally need to ensure that your setup is also equipped to handle Horizon's [compute requirements](../run-api-server/prerequisites.mdx) as well. + +Stellar Core is designed to run on relatively modest hardware so that a whole range of individuals and organizations can participate in the network, and basic nodes should be able to function pretty well without tremendous overhead. That said, the more you ask of your node, the greater the requirements. + +## Network access + +Stellar Core interacts with the peer-to-peer network to keep a distributed ledger in sync, which means that your node needs to make certain [TCP ports](https://en.wikipedia.org/wiki/Transmission_Control_Protocol#TCP_ports) available for inbound and outbound communication. + +- **Inbound**: a Stellar Core node needs to allow all IPs to connect to its `PEER_PORT` over TCP. You can specify a port when you [configure Stellar Core](./configuring.mdx), but most people use the default, which is **11625**. +- **Outbound**: a Stellar Core needs to connect to other nodes via their `PEER_PORT`s TCP. You can find information about other nodes' `PEER_PORT`s on a network explorer like [Stellarbeat](https://stellarbeat.io/), but most use the default port, which is, again, **11625**. + +## Internal System Access + +Stellar Core also needs to connect to certain internal systems, though exactly how varies based on your setup. + +- **Outbound**: + - Stellar Core requires access to a PostgreSQL database. If that database resides on a different machine on your network, you'll need to allow that connection. You specify the database when you configure Stellar Core. + - You can block all other connections. +- **Inbound**: Stellar Core exposes an _unauthenticated_ HTTP endpoint on its `HTTP_PORT`. You can specify a port when you [configure Stellar Core](./configuring.mdx), but most people use the default, which is **11626**. + - The `HTTP_PORT` is used by Horizon to submit transactions, so may have to be exposed to the rest of your internal IPs + - It's also used to query Stellar Core [info](./commands.mdx) and provide [metrics](./monitoring.mdx) + - And to perform administrative commands such as [scheduling upgrades](./network-upgrades.mdx) and changing log levels + - For more on that, see [commands](./commands.mdx) + +Note: if you need to expose your HTTP endpoint to other hosts in your local network, we recommended using an intermediate reverse proxy server to implement authentication. Don't expose the HTTP endpoint to the raw and cruel open internet. diff --git a/docs/run-core-node/publishing-history-archives.mdx b/docs/run-core-node/publishing-history-archives.mdx new file mode 100644 index 000000000..c33de6295 --- /dev/null +++ b/docs/run-core-node/publishing-history-archives.mdx @@ -0,0 +1,332 @@ +--- +title: Publishing History Archives +order: 50 +--- + +import { CodeExample } from "@site/src/components/CodeExample"; + +If you want to run a [Full Validator](./index.mdx#full-validator) or an [Archiver](./index.mdx#archiver), you need to set up your node to publish a history archive. You can host an archive using a blob store such as Amazon's S3 or Digital Ocean's spaces, or you can simply serve a local archive directly via an HTTP server such as Nginx or Apache. If you're setting up a [Basic Validator](index.mdx#basic-validator), you can skip this section. No matter what kind of node you're planning to run, make sure to set it up to `get` history, which is covered in [Configuration](./configuring.mdx). + +## Caching and History Archives + +You can significantly reduce the data transfer costs associated with running a public History archive by using common caching techniques or a CDN. + +Three simple rules apply to caching the History archives: + +- Do not cache the archive state file `.well-known/stellar-history.json` (**"Cache-Control: no-cache"**) +- Do not cache HTTP 4xx responses (**"Cache-Control: no-cache"**) +- Cache everything else for as long as possible (**> 1 day**) + +## Local History Archive Using nginx + +To publish a local history archive using nginx: + +- Add a history configuration stanza to your `/etc/stellar/stellar-core.cfg`: + + + +``` +[HISTORY.local] +get="cp /mnt/xvdf/stellar-core-archive/node_001/{0} {1}" +put="cp {0} /mnt/xvdf/stellar-core-archive/node_001/{1}" +mkdir="mkdir -p /mnt/xvdf/stellar-core-archive/node_001/{0}" +``` + + + +- Run new-hist to create the local archive + +`# sudo -u stellar stellar-core --conf /etc/stellar/stellar-core.cfg new-hist local` + +This command creates the history archive structure: + + + +``` +# tree -a /mnt/xvdf/stellar-core-archive/ +/mnt/xvdf/stellar-core-archive +└── node_001 + ├── history + │ └── 00 + │ └── 00 + │ └── 00 + │ └── history-00000000.json + └── .well-known + └── stellar-history.json + +6 directories, 2 files +``` + + + +- Configure a virtual host to serve the local archive (Nginx) + + + +``` +server { + listen 80; + root /mnt/xvdf/stellar-core-archive/node_001/; + + server_name history.example.com; + + # default is to deny all + location / { deny all; } + + # do not cache 404 errors + error_page 404 /404.html; + location = /404.html { + add_header Cache-Control "no-cache" always; + } + + # do not cache history state file + location ~ ^/.well-known/stellar-history.json$ { + add_header Cache-Control "no-cache" always; + try_files $uri; + } + + # cache entire history archive for 1 day + location / { + add_header Cache-Control "max-age=86400"; + try_files $uri; + } +} +``` + + + +## Amazon S3 History Archive + +To publish a history archive using Amazon S3: + +- Add a history configuration stanza to your `/etc/stellar/stellar-core.cfg`: + + + +```toml +[HISTORY.s3] +get='curl -sf http://history.example.com/{0} -o {1}' # Cached HTTP endpoint +put='aws s3 cp --region us-east-1 {0} s3://bucket.name/{1}' # Direct S3 access +``` + + + +- Run new-hist to create the s3 archive + +`# sudo -u stellar stellar-core --conf /etc/stellar/stellar-core.cfg new-hist s3` + +- Serve the archive using an Amazon S3 static site +- Optionally place a reverse proxy and CDN in front of the S3 static site + + + +``` +server { + listen 80; + root /srv/nginx/history.example.com; + index index.html index.htm; + + server_name history.example.com; + + # use google nameservers for lookups + resolver 8.8.8.8 8.8.4.4; + + # bucket.name s3 static site endpoint + set $s3_bucket "bucket.name.s3-website-us-east-1.amazonaws.com"; + + # default is to deny all + location / { deny all; } + + # do not cache 404 errors + error_page 404 /404.html; + location = /404.html { + add_header Cache-Control "no-cache" always; + } + + # do not cache history state file + location ~ ^/.well-known/stellar-history.json$ { + add_header Cache-Control "no-cache" always; + proxy_intercept_errors on; + proxy_pass http://$s3_bucket; + proxy_read_timeout 120s; + proxy_redirect off; + proxy_buffering off; + proxy_set_header Host $s3_bucket; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # cache history archive for 1 day + location / { + add_header Cache-Control "max-age=86400"; + proxy_intercept_errors on; + proxy_pass http://$s3_bucket; + proxy_read_timeout 120s; + proxy_redirect off; + proxy_buffering off; + proxy_set_header Host $s3_bucket; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +``` + + + +## Backfilling a history archive + +Given the choice, it's best to configure your history archive prior to your node's initial synch to the network. That way your validator's history publishes as you join/synch to the network. + +However, if you have _not_ published an archive during the node's initial synch, it's still possible to use the [stellar-archivist](https://github.com/stellar/go/tree/master/tools/stellar-archivist) command line tool to mirror, scan, and repair existing archives. + +Using the [SDF package repositories](https://github.com/stellar/packages) you can install `stellar-archivist` by running `apt-get install stellar-archivist` + +The steps required to create a History archive for an existing validator — in other words, to upgrade a Basic Validator to a Full Validator — are straightforward: + +- Stop your stellar-core instance (`systemctl stop stellar-core`) +- Configure a history archive for the new node + + + +```toml +[HISTORY.local] +get="cp /mnt/xvdf/stellar-core-archive/node_001/{0} {1}" +put="cp {0} /mnt/xvdf/stellar-core-archive/node_001/{1}" +mkdir="mkdir -p /mnt/xvdf/stellar-core-archive/node_001/{0}" +``` + + + +- Run new-hist to create the local archive + +`# sudo -u stellar stellar-core --conf /etc/stellar/stellar-core.cfg new-hist local` + +This command creates the History archive structure: + + + +``` +# tree -a /mnt/xvdf/stellar-core-archive/ +/mnt/xvdf/stellar-core-archive +└── node_001 + ├── history + │ └── 00 + │ └── 00 + │ └── 00 + │ └── history-00000000.json + └── .well-known + └── stellar-history.json + +6 directories, 2 file +``` + + + +- Start your Stellar Core instance (`systemctl start stellar-core`) +- Allow your node to join the network and watch it start publishing a few checkpoints to the newly created archive + + + +``` +2019-04-25T12:30:43.275 GDUQJ [History INFO] Publishing 1 queued checkpoints [16895-16895]: Awaiting 0/0 prerequisites of: publish-000041ff +``` + + + +At this stage your validator is successfully publishing its history, which enables other users to join the network using your archive (although it won't allow them to `CATCHUP_COMPLETE=true` as the archive only has partial network history). + +## Complete History Archive + +If you decide to publish a complete archive — which enables other users to join the network from the genesis ledger — it's also possible to use `stellar-archivist` to add all missing history data to your partial archive, and to verify the state and integrity of your archive. For example: + + + +``` +# stellar-archivist scan file:///mnt/xvdf/stellar-core-archive/node_001 +2019/04/25 11:42:51 Scanning checkpoint files in range: [0x0000003f, 0x0000417f] +2019/04/25 11:42:51 Checkpoint files scanned with 324 errors +2019/04/25 11:42:51 Archive: 3 history, 2 ledger, 2 transactions, 2 results, 2 scp +2019/04/25 11:42:51 Scanning all buckets, and those referenced by range +2019/04/25 11:42:51 Archive: 30 buckets total, 30 referenced +2019/04/25 11:42:51 Examining checkpoint files for gaps +2019/04/25 11:42:51 Examining buckets referenced by checkpoints +2019/04/25 11:42:51 Missing history (260): [0x0000003f-0x000040ff] +2019/04/25 11:42:51 Missing ledger (260): [0x0000003f-0x000040ff] +2019/04/25 11:42:51 Missing transactions (260): [0x0000003f-0x000040ff] +2019/04/25 11:42:51 Missing results (260): [0x0000003f-0x000040ff] +2019/04/25 11:42:51 No missing buckets referenced in range [0x0000003f, 0x0000417f] +2019/04/25 11:42:51 324 errors scanning checkpoints +``` + + + +As you can tell from the output of the `scan` command, some history, ledger, transactions, and results are missing from the local history archive. + +You can repair the missing data using stellar-archivist's `repair` command combined with a known full archive — such as the SDF public history archive: + +`# stellar-archivist repair http://history.stellar.org/prd/core-testnet/core_testnet_001/ file:///mnt/xvdf/stellar-core-archive/node_001/` + + + +``` +2019/04/25 11:50:15 repairing http://history.stellar.org/prd/core-testnet/core_testnet_001/ -> file:///mnt/xvdf/stellar-core-archive/node_001/ +2019/04/25 11:50:15 Starting scan for repair +2019/04/25 11:50:15 Scanning checkpoint files in range: [0x0000003f, 0x000041bf] +2019/04/25 11:50:15 Checkpoint files scanned with 244 errors +2019/04/25 11:50:15 Archive: 4 history, 3 ledger, 263 transactions, 61 results, 3 scp +2019/04/25 11:50:15 Error: 244 errors scanning checkpoints +2019/04/25 11:50:15 Examining checkpoint files for gaps +2019/04/25 11:50:15 Repairing history/00/00/00/history-0000003f.json +2019/04/25 11:50:15 Repairing history/00/00/00/history-0000007f.json +2019/04/25 11:50:15 Repairing history/00/00/00/history-000000bf.json +... +2019/04/25 11:50:22 Repairing ledger/00/00/00/ledger-0000003f.xdr.gz +2019/04/25 11:50:23 Repairing ledger/00/00/00/ledger-0000007f.xdr.gz +2019/04/25 11:50:23 Repairing ledger/00/00/00/ledger-000000bf.xdr.gz +... +2019/04/25 11:51:18 Repairing results/00/00/0e/results-00000ebf.xdr.gz +2019/04/25 11:51:18 Repairing results/00/00/0e/results-00000eff.xdr.gz +2019/04/25 11:51:19 Repairing results/00/00/0f/results-00000f3f.xdr.gz +... +2019/04/25 11:51:39 Repairing scp/00/00/00/scp-0000003f.xdr.gz +2019/04/25 11:51:39 Repairing scp/00/00/00/scp-0000007f.xdr.gz +2019/04/25 11:51:39 Repairing scp/00/00/00/scp-000000bf.xdr.gz +... +2019/04/25 11:51:50 Re-running checkpoing-file scan, for bucket repair +2019/04/25 11:51:50 Scanning checkpoint files in range: [0x0000003f, 0x000041bf] +2019/04/25 11:51:50 Checkpoint files scanned with 5 errors +2019/04/25 11:51:50 Archive: 264 history, 263 ledger, 263 transactions, 263 results, 241 scp +2019/04/25 11:51:50 Error: 5 errors scanning checkpoints +2019/04/25 11:51:50 Scanning all buckets, and those referenced by range +2019/04/25 11:51:50 Archive: 40 buckets total, 2478 referenced +2019/04/25 11:51:50 Examining buckets referenced by checkpoints +2019/04/25 11:51:50 Repairing bucket/57/18/d4/bucket-5718d412bdc19084dafeb7e1852cf06f454392df627e1ec056c8b756263a47f1.xdr.gz +2019/04/25 11:51:50 Repairing bucket/8a/a1/62/bucket-8aa1624cc44aa02609366fe6038ffc5309698d4ba8212ef9c0d89dc1f2c73033.xdr.gz +2019/04/25 11:51:50 Repairing bucket/30/82/6a/bucket-30826a8569cb6b178526ddba71b995c612128439f090f371b6bf70fe8cf7ec24.xdr.gz +... +``` + + + +A final scan of the local archive confirms that it has been successfully repaired + +`# stellar-archivist scan file:///mnt/xvdf/stellar-core-archive/node_001` + + + +``` +2019/04/25 12:15:41 Scanning checkpoint files in range: [0x0000003f, 0x000041bf] +2019/04/25 12:15:41 Archive: 264 history, 263 ledger, 263 transactions, 263 results, 241 scp +2019/04/25 12:15:41 Scanning all buckets, and those referenced by range +2019/04/25 12:15:41 Archive: 2478 buckets total, 2478 referenced +2019/04/25 12:15:41 Examining checkpoint files for gaps +2019/04/25 12:15:41 Examining buckets referenced by checkpoints +2019/04/25 12:15:41 No checkpoint files missing in range [0x0000003f, 0x000041bf] +2019/04/25 12:15:41 No missing buckets referenced in range [0x0000003f, 0x000041bf] +``` + + + +Start your stellar-core instance (`systemctl start stellar-core`), and you should have a complete history archive being written to by your full validator. diff --git a/docs/run-core-node/running-node.mdx b/docs/run-core-node/running-node.mdx new file mode 100644 index 000000000..5d8742337 --- /dev/null +++ b/docs/run-core-node/running-node.mdx @@ -0,0 +1,180 @@ +--- +title: Running +order: 60 +--- + +import { CodeExample } from "@site/src/components/CodeExample"; + +## Starting Stellar Core + +Once you've [set up your environment](./prerequisites.mdx), [configured your node](./configuring.mdx), set up your [quorum set](./configuring.mdx#choosing-your-quorum-set), and selected archives to `get` [history from](./configuring.mdx#history), you're ready to start Stellar Core. + +Use a command equivalent to: + +`$ stellar-core run` + +At this point, you're ready to observe your node's activity as it joins the network. + +You may want to skip ahead and review the [logging](#logging) section to familiarize yourself wiith Stellar Core's output. + +## Interacting With Your Instance + +When your node is running, you can interact with Stellar Core via an administrative HTTP endpoint. Commands can be submitted using command-line HTTP tools such as `curl`, or by running a command such as + +`$ stellar-core http-command ` + +That HTTP endpoint is not intended to be exposed to the public internet. It's typically accessed by administrators, or by a mid-tier application to submit transactions to the Stellar network. + +See [commands](./commands.mdx) for a description of the available commands. + +## Joining the Network + +Your node will go through the following phases as it joins the network: + +### Establishing Connection to Other Peers. + +You should see `authenticated_count` increase. + + + +```json +"peers" : { + "authenticated_count" : 3, + "pending_count" : 4 +}, +``` + + + +### Observing Consensus + +Until the node sees a quorum, it will say: + + + +```json +"state" : "Joining SCP" +``` + + + +After observing consensus, a new field `quorum` will display information about network decisions. At this point the node will switch to "_Catching up_": + + + +```json +"quorum" : { + "qset" : { + "ledger" : 22267866, + "agree" : 5, + "delayed" : 0, + "disagree" : 0, + "fail_at" : 3, + "hash" : "980a24", + "missing" : 0, + "phase" : "EXTERNALIZE" + }, + "transitive" : { + "intersection" : true, + "last_check_ledger" : 22267866, + "node_count" : 21 + } +}, +"state" : "Catching up", +``` + + + +### Catching up + +This is a phase where the node downloads data from archives. The state will start with something like: + + + +```json +"state" : "Catching up", +"status" : [ "Catching up: Awaiting checkpoint (ETA: 35 seconds)" ] +``` + + + +And then go through the various phases of downloading and applying state such as + + + +```json +"state" : "Catching up", +"status" : [ "Catching up: downloading ledger files 20094/119803 (16%)" ] +``` + + + +You can specify how far back your node goes to catch up in your config file. If you set`CATCHUP_COMPLETE` to `true`, your node will replay the entire history of the network, which can take a long time. Weeks. Satoshipay offers a [parallel catchup script](https://github.com/satoshipay/stellar-core-parallel-catchup) to speed up the process, but you only need to replay the complete network history if you're setting up a Full Validator. Otherwise, you can specify a starting point for catchup using `CATCHUP_RECENT`. See the [complete example configuration](https://github.com/stellar/stellar-core/blob/master/docs/stellar-core_example.cfg) for more details. + +### Synced + +When the node is done catching up, its state will change to: + + + +```json +"state" : "Synced!" +``` + + + +## Logging + +Stellar Core sends logs to standard output and `stellar-core.log` by default, configurable as `LOG_FILE_PATH`. + +Log messages are classified by progressive _priority levels_:`TRACE`, `DEBUG`, `INFO`, `WARNING`, `ERROR` and `FATAL`. The logging system only emits those messages at or above its configured logging level. + +The log level can be controlled by configuration, the `-ll` command-line flag, or adjusted dynamically by administrative (HTTP) commands. To do so, run: + +`$ stellar-core http-command "ll?level=debug"` + +while your system is running. + +Log levels can also be adjusted on a partition-by-partition basis through the administrative interface. For example the history system can be set to DEBUG-level logging by running: + +`$ stellar-core http-command "ll?level=debug&partition=history"` + +Against a running system. + +The default log level is `INFO`, which is moderately verbose and should emit progress messages every few seconds under normal operation. + +## Validator maintenance + +Maintenance here refers to anything involving taking your validator temporarily out of the network (to apply security patches, system upgrade, etc). + +As an administrator of a validator, you must ensure that the maintenance you are about to apply to the validator is safe for the overall network and for your validator. + +Safe means that the other validators that depend on yours will not be affected too much when you turn off your validator for maintenance and that your validator will continue to operate as part of the network when it comes back up. + +If you are changing some settings that may impact network wide settings such as protocol version, review [the section on network configuration](#network-configuration). + +If you're changing your quorum set configuration, also read the [section on what to do](#special-considerations-during-quorum-set-updates). + +### Recommended steps to perform as part of a maintenance + +We recommend performing the following steps in order (repeat sequentially as needed if you run multiple nodes). + +1. Advertise your intention to others that may depend on you. Some coordination is required to avoid situations where too many nodes go down at the same time. +1. Dependencies should assess the health of their quorum, refer to the section "Understanding quorum and reliability". +1. If there is no objection, take your instance down +1. When done, start your instance that should rejoin the network +1. The instance will be completely caught up when it's both `Synced` and _there is no backlog in uploading history_. + +#### Special considerations during quorum set updates + +Sometimes an organization needs to make changes that impact other's quorum sets: + +- taking a validator down for long period of time +- adding new validators to their pool + +In both cases, it's crucial to stage the changes to preserve quorum intersection and general good health of the network: + +- removing too many nodes from your quorum set _before_ the nodes are taken down : if different people remove different sets the remaining sets may not overlap between nodes and may cause network splits +- adding too many nodes in your quorum set at the same time : if not done carefully can cause those nodes to overpower your configuration + +Recommended steps are for the entity that adds/removes nodes to do so first between their own nodes, and then have people reflect those changes gradually (over several rounds) in their quorum configuration. diff --git a/docs/run-core-node/tier-1-orgs.mdx b/docs/run-core-node/tier-1-orgs.mdx new file mode 100644 index 000000000..04c2ad015 --- /dev/null +++ b/docs/run-core-node/tier-1-orgs.mdx @@ -0,0 +1,72 @@ +--- +title: Tier 1 Organizations +order: 120 +--- + +To help with Stellar’s decentralization, the most reliable and advanced Stellar teams join the ranks of “Tier 1 Organizations.” These organizations run three validators, coordinate any changes to their quorumsets, and hold themselves to a higher standard of uptime and responsiveness. + +SDF works closely with Tier 1 Orgs to ensure the health of the network, maintain good quorum intersection, and build in redundancy to minimize network disruptions. This guide outlines what it takes to be a Tier 1 Org. + +## Why Three Validators + +The most important function of a Tier 1 Org is to set up and maintain three Full Validators. Why three? + +On Stellar, validators choose to trust organizations when they build a quorum set. If you are a trustworthy organization, you want your presence on the network to persist even if a node fails or you take it down for maintenance. A trio of validating nodes allows that to happen: other participants can create a quorum slice for your organization that requires ⅔ of your validating nodes to agree. If 1 has issues, no big deal: the other two still vote on your organization’s behalf, so the show goes on. To ensure redundancy, it's also important that those three Full Validators are geographically dispersed: if they're in the same data center, they run the risk of going down at the same time. + +Here’s what else Tier 1 Orgs expect of one another: + +## Publish History Archives + +In addition to participating in SCP, a full validator publishes an archive of network transactions. To do that, you need to configure Stellar Core to record history to a publicly accessible archive, and add the location of that archive to your stellar.toml. To be a Tier 1 Org, you should set each of your nodes to record history to a separate archive. + +Public archives make the network more resilient: when new nodes come online, or when existing nodes lose synch, they need to consult an archive to figure out what they missed. Sharing snapshots of the ledger, which detail transactions and their results, allows those nodes to catch up, and more archives mean more redundancy and greater decentralization. Plus, sharing history keeps everyone honest. + +## Set Up a Safe Quorum Set + +To maximize network resilience, we’re asking every Tier 1 node to use the same quorum set configuration, which is made up of subquorums of all validators from each Tier 1 Org. + +That way, the validator community can experiment with a larger quorum, and can analyze the results of those experiments without disrupting the network. Using existing Tier 1 Orgs as a safety net, we can work together to expand the quorum methodically and deliberately. To see what that quorum set currently looks like, check out the [example Full Validator config file](https://github.com/stellar/packages/blob/master/docs/examples/pubnet-validator-full/stellar-core.cfg). + +## Declare Your Node + +[SEP-20](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0020.md) is an open spec that explains how self-verification of validator nodes works. The steps it specifies are pretty simple: you set the homedomain of your validator’s Stellar account to your website, where you publish information about your node and your organization in a stellar.toml file. + +It’s an easy way to propagate information, and it harnesses the network to allow other participants to discover your node and add it to their quorum sets without the need for a centralized database. + +## Keep Your Nodes Up To Date + +Running a validator requires vigilance. You need to keep an eye on your nodes, keep them up to date with the latest version of Stellar Core, and check in on public channels for information about what’s currently happening with other validators. + +The best two ways to do that: + +- Join the validators [email list](https://groups.google.com/forum/#!forum/stellar-validators) +- download Keybase and join the [#validators channel](https://keybase.io/team/stellar.public) on the stellar.public team + +We always announce new Stellar Core releases in those channels. You can also find those releases on our github. + +It’s also critical that you pay attention to information about what those updates mean: often, you’ll need to set your validators to vote on something timely, such as when to upgrade the network as a whole, or how high to set the operations-per-ledger limit. + +## Coordinate With Other Validators + +Whether you run a trio of validators or a single node, it’s important that you coordinate with other validators when you make a significant change or notice something wrong. You should let them know when you plan to: + +- Take your node down for maintenance +- Make changes to your quorum set + +Letting other validators know when you plan to take your node down for maintenance or to upgrade to the latest version of stellar-core prevents a critical mass of nodes from going offline at the same time. + +Letting other validators know when you plan to change your quorum set allows them to respond, adjust, and think through the implications of expanding the quorum. For the quorum to expand safely, we all need to coordinate to ensure we maintain good quorum intersection. + +## Monitor your quorum set + +We recommend using Prometheus to to scrape and store your stellar-core metrics, and Grafana to render that data for human consumption. You can find step-by-step instructions for setting up monitoring and alerts in [Monitoring and Diagnostics](./monitoring.mdx), along with links to Grafana dashboards we’ve created to make things easier. + +You can also use stellarbeat.io to view validators’ quorum configurations, and get information about their availability and uptime, and the quorum command to diagnose problems with the quorum set of the local node. + +You should do regular check-ins on your quorum set. If nodes have bad uptime or prove otherwise unreliable, you may need to remove them from your quorum set so that you don’t get stuck and so that the network doesn’t halt. You may also want to add new organizations that come online and prove reliable. If you plan to do either of those things, remember to communicate and coordinate with other validators. + +## Get in touch + +If you think you can be a Tier 1 Org, let us know on the #validators channel on [Keybase](https://keybase.io/team/stellar.public). We can help you through the process, and once you’re up and running, we’ll work to fold you into the quorum so that you can take your rightful place as a pillar of the network. Once you’ve proven that you are responsive, reliable, and maintain good uptime, we will adjust the quorum set recipe above to include your validators. + +As Stellar grows, and more and more businesses build on the network, Tier 1 Orgs will be crucial to the methodical expansion of the network. diff --git a/docs/software-and-sdks/index.mdx b/docs/software-and-sdks/index.mdx new file mode 100644 index 000000000..3fa8122c4 --- /dev/null +++ b/docs/software-and-sdks/index.mdx @@ -0,0 +1,123 @@ +--- +title: "Software and SDKs" +order: 0 +--- + +## Software + +There are two key pieces of network software: Stellar Core, which tracks and adds transaction sets to the ledger, and Horizon, an API that allows programmatic access to submit transactions and query network data. To find out more about how they work together, visit the description of the [Stellar Stack](../start/stellar-stack.mdx). + +You do not have to run a Stellar Core node or Horizon instance to build on Stellar: you can start developing in your language of choice by installing one of the [Stellar SDKs](#sdks) below, and interacting with a public Horizon instance. To find out more about how to interact with Horizon, check out the [API Reference](/api/introduction/) section, which chronicles every Horizon endpoint, resource, aggregation, and error. + +### Stellar Core + +Stellar Core is the backbone of the Stellar network and does the hard work of validating and agreeing on the status of every transaction with other instances of Core through the Stellar Consensus Protocol. The processes for installing, configuring, and maintaining a Stellar Core node are covered in great detail in the [Run a Core Node](../run-core-node/index.mdx) section of the docs. + +### Horizon + +Horizon is the client-facing API server for the Stellar ecosystem. It acts as the interface between Stellar Core and applications that want to access the Stellar network. If you're running Stellar Core, you will probably also want to run Horizon. For more information on how to set up and operate a Horizon instance, see the [Run an API Server](../run-api-server/index.mdx) section of the docs. + +## SDKs + +There are a wide variety of Stellar SDKs, which means you can interact with the network in your language of choice. The Javascript, Java, and Go SDKs are maintained by the Stellar Development Foundation; the rest are maintained by dedicated community developers. They're all open source, so if you have a question, suggestion, or contribution to make, you can file a Github issue or pull request in the relevant SDK repository. You can also get in touch with SDK maintainers by joining the [Stellar public Keybase team](https://keybase.io/team/stellar.public), and navigating to the #sdk-mainteners channel. + +Each SDK has its own source code and documentation, and we've linked to both in the list below. Often, the best place to find out how to use a given SDK is to check the documentation specific to it. Most offer practical examples that demonstrate how to construct and submit transactions and interact with Horizon endpoints. + +### Javascript + +- [Source](https://github.com/stellar/js-stellar-sdk) +- [Docs](https://stellar.github.io/js-stellar-sdk/) + +### Java + +- [Source](https://github.com/stellar/java-stellar-sdk) +- [Docs](https://stellar.github.io/java-stellar-sdk/) + +### Go + +The Go SDK is split up into a few separate packages, all of which you can find in [the Go monorepo README](https://github.com/stellar/go/blob/master/docs/reference/readme.md). The two key libraries for interacting with Horizon are `txnbuild`, which enables the construction, signing, and encoding of Stellar transactions, and `horizonclient`, which provides a web client for interfacing with Horizon server REST endpoints to retrieve ledger information and submit transactions built with `txnbuild`. + +- `txnbuild` [Source](https://github.com/stellar/go/tree/master/txnbuild) +- `txnbuild` [Docs](https://godoc.org/github.com/stellar/go/txnbuild) +- `Horizonclient` [Source](https://github.com/stellar/go/tree/master/clients/horizonclient) +- `Horizonclient`[Docs](https://godoc.org/github.com/stellar/go/clients/horizonclient) + +### Python + +- [Source](https://github.com/StellarCN/py-stellar-base) +- [Docs](https://stellar-sdk.readthedocs.io/en/latest/) +- [Examples](https://github.com/StellarCN/py-stellar-base/tree/master/examples) + +### C# .NET + +- [Source](https://github.com/elucidsoft/dotnet-stellar-sdk) +- [Docs](https://elucidsoft.github.io/dotnet-stellar-sdk/api/index.html) +- [Tutorials](https://elucidsoft.github.io/dotnet-stellar-sdk/tutorials/index.html) + +### Ruby + +- [Source](https://github.com/astroband/ruby-stellar-sdk) +- [Base Source](https://github.com/astroband/ruby-stellar-sdk/blob/master/base/README.md) +- [SDK Source](https://github.com/astroband/ruby-stellar-sdk/blob/master/sdk/README.md) +- [Docs](https://www.rubydoc.info/gems/stellar-sdk) +- [Base examples](https://github.com/astroband/ruby-stellar-sdk/tree/master/base/examples) +- [SDK examples](https://github.com/astroband/ruby-stellar-sdk/tree/master/sdk/examples) + +### iOS + +- [Source](https://github.com/Soneso/stellar-ios-mac-sdk) +- [Docs](https://github.com/Soneso/stellar-ios-mac-sdk/tree/master/docs) + +### Scala + +- [Source](https://github.com/Synesso/scala-stellar-sdk) +- [Docs](https://synesso.github.io/scala-stellar-sdk/) + +### Qt/C++ + +- [Source](https://github.com/bnogalm/StellarQtSDK) +- [Docs](https://github.com/bnogalm/StellarQtSDK/wiki) + +### Flutter + +- [Source](https://github.com/Soneso/stellar_flutter_sdk) +- [Docs](https://github.com/Soneso/stellar_flutter_sdk/tree/master/documentation) +- [Examples](https://github.com/Soneso/stellar_flutter_sdk/tree/master/documentation/sdk_examples) + +## Tools + +The Stellar Development Foundation maintains a small suite of tools to make it easier for developers to interact with the network. + +### [Laboratory](https://www.stellar.org/laboratory/) + +The Stellar laboratory is a GUI that allows you to create accounts, construct and submit transactions, read XDRs, and query all of Horizon's endpoints. It exposes the relevant calls to Horizon, so it's a great way to experiment with and learn more about the Stellar API. The source is available [here](https://github.com/stellar/laboratory). + +### [Account Viewer](https://www.stellar.org/account-viewer/) + +The account viewer is a stripped-down wallet that you can use to check an account's XLM balance and send simple payments. It works on the testnet as well as the public network. The source is available [here](https://github.com/stellar/account-viewer-v2). + +### [Anchor Validator](https://anchor-tests.stellar.org/) + +The validator is an application that runs a series of tests against an integration; it currently can assess SEP-1, SEP-6, SEP-10, SEP-12, SEP-24, and SEP-31. It allows developers to understand whether they're building against protocols correctly and allows them to view progress of their development. + +### [Demo Wallet](https://demo-wallet.stellar.org/) + +The demo wallet lets financial application developers interactively test their integrations and learn how Stellar ecosystem protocols (SEPs) work. You can use the demo wallet to test Regulated Assets (SEP-8), Hosted Deposit and Withdrawal (SEP-24), and Cross-Border Payments (SEP-31) with any home domain that has a Stellar Info File (also known as SEP-1, or a stellar.toml file). + +### [Dashboard](http://dashboard.stellar.org) + +The dashboard shows the current status of the public network and the test network. The source is available [here](https://github.com/stellar/dashboard). + +### [Status Page](https://status.stellar.org/) + +The status page tracks network incidents and scheduled maintenance for both the public network and the test network. We recommend subscribing to updates so you're notified about important events including protocol upgrades and testnet resets. + +There is also a small [suite of tools](https://github.com/stellar/go/tree/master/tools) built in Go that can be useful for Stellar Core and Horizon operators. It includes things like Stellar Archivist, which is for Stellar Core archive maintenance, and Horizon cmp, which compares the responses of two Horizon servers and shows the diffs. + +## Reference Implementations + +The Stellar Development Foundation maintains reference implementations of some [Stellar Ecosystem Proposals](https://github.com/stellar/stellar-protocol/tree/master/ecosystem) to jumpstart the process of building infrastructure on top of Stellar in a way that maximizes interoperability among ecosystem participants. + +- [Polaris](https://github.com/stellar/django-polaris) is an extendable django app that makes it easy for anchors to [facilitate cross-border payments and enable deposits and withdrawals](../anchoring-assets/index.mdx). Using Polaris, you can run a web server supporting any combination of SEP-1, 6, 10, 12, and 24. +- The [SEP-24 demo client](https://github.com/stellar/sep24-demo-client) makes it easy for anchors to test their deposit and withdrawal flows by implementing the client side of a Stellar SEP24 interactive flow. +- The [Federation Server](https://github.com/stellar/go/tree/master/services/federation) is a Go implementation of the federation protocol described in [SEP-2](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0002.md). It's designed to be dropped into your existing infrastructure. diff --git a/docs/software-and-sdks/metadata.json b/docs/software-and-sdks/metadata.json new file mode 100644 index 000000000..1c747a7ca --- /dev/null +++ b/docs/software-and-sdks/metadata.json @@ -0,0 +1,4 @@ +{ + "order": 80, + "title": "Software and SDKs" +} diff --git a/docs/start/introduction.mdx b/docs/start/introduction.mdx new file mode 100644 index 000000000..f0e8f2e0a --- /dev/null +++ b/docs/start/introduction.mdx @@ -0,0 +1,48 @@ +--- +title: Introduction +order: 0 +--- + +import { CodeExample } from "@site/src/components/CodeExample"; + +Stellar is designed to make it easy for developers to issue digital assets and build applications that take advantage of a public distributed ledger that allows for near-instant payments and universal currency conversion. The goal of these docs is to explain key concepts and offer practical examples so developers can roll up their sleeves and get building on Stellar. Ideally, they answer crucial developer questions, and are thorough enough to guide a project from conception to production. + +Like the Stellar codebase, these docs are open source and constantly evolving, so if you can't find what you're looking for or have ideas for improvements, please contribute by filing a Github issue or pull request in this repository. + +This section outlines some of the basic concepts and channels for developers, so if you're already familiar with Stellar, you may want to skip ahead to whatever section applies to your product or use case. You can use the left-side menu of the docs to navigate to various sections, and the right-side menu to navigate to different parts of a given page. For a quick summary of each section, please visit the [Welcome page](../index.mdx) + +## Background Reading + +If you're new to Stellar, you may want to start by getting a high-level understanding of the network. A great place to do that is the Learn section of stellar.org. It has five pages, each of which delves into a fundamental aspect of Stellar: + +- [Intro to Stellar](https://www.stellar.org/learn/intro-to-stellar) explains what Stellar is for, how it works, and who builds on it and why. + +- [The Power of Stellar](https://www.stellar.org/learn/the-power-of-stellar) outlines the fundamental things you can do with Stellar, including issue digital assets, trade peer-to-peer, and convert currency as you send it. + +- [Anchor Basics](https://www.stellar.org/learn/anchor-basics) outlines the role of Anchors, which are enterprises that connect the Stellar network to traditional banking rails so that all the world's currencies can interoperate on a single, seamless platform. + +- [Blockchain Basics](https://www.stellar.org/learn/blockchain-basics) explains the basic idea behind blockchain, and shows how Stellar relates to other networks like Bitcoin and Ethereum. + +- [Stellar Lumens](https://www.stellar.org/lumens) explains the origins and use of the network token — the lumen (aka XLM) — and gives pointers on how to buy and store lumens. Another way to gain an understanding of what you can do with the network is to check out some of the existing projects built on Stellar. For a current list, take a look at the [Projects and Partners](https://www.stellar.org/ecosystem/projects) section of stellar.org. + +## Getting Started + +Once you have a high-level understanding of Stellar, you can start building right away, and the subsequent sections of these docs will focus on how to do that. You don't have to run your own Stellar Core node to develop on Stellar — several organizations including the Stellar Development Foundation offer public-access API endpoints that allow you to submit transactions and query the ledger — so you can focus on building your product before committing to setting up network infrastructure. + +If you are new, you may want to start with the early [Tutorials](../tutorials/create-account.mdx) to familiarize yourself with some of the building blocks of Stellar. You should also investigate the [SDKs](../software-and-sdks/index.mdx) designed to make developing in your language of choice easy, and familiarize yourself with the canonical [List of Operations](../start/list-of-operations.mdx), which documents everything you can do with Stellar — along with parameters and error codes — and links to the relevant documentation for key SDKs. Finally, you may want to explore the [API Reference](/api/introduction/) documentation, which details every resource, aggregation, and error provided by Horizon, the Stellar API. + +## Developer Channels + +Stellar has an active developer community, and it's often helpful to interact with other devs who are working on Stellar-based projects. They're great at answering questions, giving feedback, and sharing information about the best ways to use the network. For general information on our community channels, check out the stellar.org [Community Page](https://www.stellar.org/community). + +There are also several channels dedicated to developers, and it's a good idea to join them to keep abreast of important plans, developments, and events: + +- The Stellar [Status Page](https://status.stellar.org/) tracks the uptime of the public network and the test network, and displays information about incidents and scheduled maintenance. If you're building on Stellar, you should sign up for updates so you're aware of crucial network events that require action on your part — including protocol upgrades and testnet resets. + +- The Stellar Public [Keybase Team](https://keybase.io/team/stellar.public) is a great place to chat with members of the Stellar Development Foundation and other Stellar devs, and is where a lot of ecosystem collaboration and coordination happens. The #dev-discussion channel is a great place to ask and answer questions in real time. + +- The Stellar [Stack Exchange](https://stellar.stackexchange.com/) is persistent knowledge base for asking and answering questions about all things Stellar. If you can't find what you're looking for in the docs, you can often search Stack Exchange and find your question has been asked and answered. + +- The [Developers Google Group](https://groups.google.com/forum/#!forum/stellar-dev) is dedicated to discussions about Core Advancement Proposals and Stellar Ecosystem Proposals (aka CAPs and SEPs), and to important notifications about upgrades and network-wide decisions. It's not the best place to ask questions about how to do something on Stellar — Stack Exchange and Keybase are better suited for that — but it's a great place to participate in the growth and development of the protocol itself. + +- The [Stellar Community Fund](https://communityfund.stellar.org/) is a quarterly grant program that allows the Stellar community to reward Stellar-based projects with lumen awards. It's a great way to kickstart a project on Stellar. diff --git a/docs/start/list-of-operations.mdx b/docs/start/list-of-operations.mdx new file mode 100644 index 000000000..e40d49258 --- /dev/null +++ b/docs/start/list-of-operations.mdx @@ -0,0 +1,760 @@ +--- +title: List of Operations +order: 30 +--- + +import { Alert } from "@site/src/components/Alert"; +import { CodeExample } from "@site/src/components/CodeExample"; + +This is the canonical list of Stellar operations, which lists every Stellar operation along with parameters, error codes, and links to relevant documentation for key SDKs. + +For a description of how operations work in Stellar, see [Operations](../glossary/operations.mdx). + +For the protocol specification, see [stellar-transaction.x](https://github.com/stellar/stellar-core/blob/master/src/protocol-curr/xdr/Stellar-transaction.x). + +- [Create Account](#create-account) +- [Payment](#payment) +- [Path Payment Strict Send](#path-payment-strict-send) +- [Path Payment Strict Receive](#path-payment-strict-receive) +- [Manage Buy Offer](#manage-buy-offer) +- [Manage Sell Offer](#manage-sell-offer) +- [Create Passive Sell Offer](#create-passive-sell-offer) +- [Set Options](#set-options) +- [Change Trust](#change-trust) +- [Allow Trust](#allow-trust) +- [Account Merge](#account-merge) +- [Manage Data](#manage-data) +- [Bump Sequence](#bump-sequence) +- [Create Claimable Balance](#create-claimable-balance) +- [Claim Claimable Balance](#claim-claimable-balance) +- [Begin Sponsoring Future Reserves](#begin-sponsoring-future-reserves) +- [End Sponsoring Future Reserves](#end-sponsoring-future-reserves) +- [Revoke Sponsorship](#revoke-sponsorship) +- [Clawback](#clawback) +- [Clawback Claimable Balance](#clawback-claimable-balance) +- [Set Trustline Flags](#set-trustline-flags) +- [Liquidity Pool Deposit](#liquidity-pool-deposit) +- [Liquidity Pool Withdraw](#liquidity-pool-withdraw) + +## Create Account + +[JavaScript](http://stellar.github.io/js-stellar-sdk/Operation.html#.createAccount) | [Java](http://stellar.github.io/java-stellar-sdk/org/stellar/sdk/CreateAccountOperation.Builder.html) | [Go](https://godoc.org/github.com/stellar/go/txnbuild#CreateAccount) + +Creates and funds a new account with the specified starting balance. + +Threshold: Medium + +Result: `CreateAccountResult` + +Parameters: + +| Parameter | Type | Description | +| --- | --- | --- | +| Destination | account ID | Account address that is created and funded. | +| Starting Balance | integer | Amount of XLM to send to the newly created account. This XLM comes from the source account. | + +Possible errors: + +| Error | Code | Description | +| --- | --- | --- | +| CREATE_ACCOUNT_MALFORMED | -1 | The `destination` is invalid. | +| CREATE_ACCOUNT_UNDERFUNDED | -2 | The source account performing the command does not have enough funds to give `destination` the `starting balance` amount of XLM and still maintain its minimum XLM reserve plus satisfy its XLM selling liabilities. | +| CREATE_ACCOUNT_LOW_RESERVE | -3 | This operation would create an account with fewer than the minimum number of XLM an account must hold. | +| CREATE_ACCOUNT_ALREADY_EXIST | -4 | The `destination` account already exists. | + +## Payment + +[JavaScript](http://stellar.github.io/js-stellar-sdk/Operation.html#.payment) | [Java](http://stellar.github.io/java-stellar-sdk/org/stellar/sdk/PaymentOperation.Builder.html) | [Go](https://godoc.org/github.com/stellar/go/txnbuild#Payment) + +Sends an amount in a specific asset to a destination account. + +Threshold: Medium + +Result: `PaymentResult` + +Parameters: + +| Parameters | Type | Description | +| ----------- | ---------- | ------------------------------------------- | +| Destination | account ID | Account address that receives the payment. | +| Asset | asset | Asset to send to the destination account. | +| Amount | integer | Amount of the aforementioned asset to send. | + +Possible errors: + +| Error | Code | Description | +| --- | --- | --- | +| PAYMENT_MALFORMED | -1 | The input to the payment is invalid. | +| PAYMENT_UNDERFUNDED | -2 | The source account (sender) does not have enough funds to send `amount` and still satisfy its selling liabilities. Note that if sending XLM then the sender must additionally maintain its minimum XLM reserve. | +| PAYMENT_SRC_NO_TRUST | -3 | The source account does not trust the issuer of the asset it is trying to send. | +| PAYMENT_SRC_NOT_AUTHORIZED | -4 | The source account is not authorized to send this payment. | +| PAYMENT_NO_DESTINATION | -5 | The receiving account does not exist. Note that this error will **not** be returned if the receiving account is the issuer of `asset`. | +| PAYMENT_NO_TRUST | -6 | The receiver does not trust the issuer of the asset being sent. For more information, see the [assets doc](../glossary/assets.mdx). | +| PAYMENT_NOT_AUTHORIZED | -7 | The destination account is not authorized by the asset's issuer to hold the asset. | +| PAYMENT_LINE_FULL | -8 | The destination account (receiver) does not have sufficient limits to receive `amount` and still satisfy its buying liabilities. | + +## Path Payment Strict Send + +[JavaScript](http://stellar.github.io/js-stellar-sdk/Operation.html#.pathPaymentStrictSend) | [Java](http://stellar.github.io/java-stellar-sdk/org/stellar/sdk/PathPaymentStrictSendOperation.Builder.html) | [Go](https://godoc.org/github.com/stellar/go/txnbuild#PathPaymentStrictSend) + +A path payment sends an amount of a specific asset to a destination account through a path of offers. Since the asset sent (e.g., 450 XLM) can be different from the asset received (e.g, 6 BTC), path payments allow for the simultaneous transfer and conversion of currencies. + +A Path Payment Strict Send allows a user to specify the _amount of the asset to send_. The amount received will vary based on offers in the order books. If you would like to instead specify the amount received, use the [Path Payment Strict Receive](#path-payment-strict-receive) operation. + +A few things to note: + +- path payments don't allow intermediate offers to be from the source account as this would yield a worse exchange rate. You'll need to either split the path payment into two smaller path payments, or ensure that the source account's offers are not at the top of the order book. +- balances are settled at the very end of the operation + - this is especially important when `(Destination, Destination Asset) == (Source, Send Asset)` as this provides a functionality equivalent to getting a no interest loan for the duration of the operation. +- `Destination min` is a protective measure: it allows you to specify a lower bound for an acceptable conversion. If offers in the order books are not favorable enough for the operation to deliver that amount, the operation will fail. + +As of protocol 18, the network automatically considers both the orderbook and any liquidity pools when trying to find paths to exchange one asset for another. For example, if a user wants their recipient to get 1000 Zs from your As, they may consume an order to get Ws for their As, then exchange against the W-Z liquidity pool for the second hop of the payment. + +Threshold: Medium + +Result: `PathPaymentStrictSendResult` + +Parameters: + +| Parameters | Type | Description | +| --- | --- | --- | +| Send asset | asset | The asset deducted from the sender's account. | +| Send amount | integer | The amount of `send asset` to deduct (excluding fees). | +| Destination | account ID | Account ID of the recipient. | +| Destination asset | asset | The asset the destination account receives. | +| Destination min | integer | The minimum amount of `destination asset` the destination account can receive. | +| Path | list of assets | The assets (other than `send asset` and `destination asset`) involved in the offers the path takes. For example, if you can only find a path from USD to EUR through XLM and BTC, the path would be USD -> XLM -> BTC -> EUR and the `path` field would contain XLM and BTC. | + +Possible errors: + +| Error | Code | Description | +| --- | --- | --- | +| PATH_PAYMENT_STRICT_SEND_MALFORMED | -1 | The input to this path payment is invalid. | +| PATH_PAYMENT_STRICT_SEND_UNDERFUNDED | -2 | The source account (sender) does not have enough funds to send and still satisfy its selling liabilities. Note that if sending XLM then the sender must additionally maintain its minimum XLM reserve. | +| PATH_PAYMENT_STRICT_SEND_SRC_NO_TRUST | -3 | The source account does not trust the issuer of the asset it is trying to send. | +| PATH_PAYMENT_STRICT_SEND_SRC_NOT_AUTHORIZED | -4 | The source account is not authorized to send this payment. | +| PATH_PAYMENT_STRICT_SEND_NO_DESTINATION | -5 | The destination account does not exist. | +| PATH_PAYMENT_STRICT_SEND_NO_TRUST | -6 | The destination account does not trust the issuer of the asset being sent. For more, see the [assets doc](../glossary/assets.mdx). | +| PATH_PAYMENT_STRICT_SEND_NOT_AUTHORIZED | -7 | The destination account is not authorized by the asset's issuer to hold the asset. | +| PATH_PAYMENT_STRICT_SEND_LINE_FULL | -8 | The destination account does not have sufficient limits to receive `destination amount` and still satisfy its buying liabilities. | +| PATH_PAYMENT_STRICT_SEND_TOO_FEW_OFFERS | -10 | There is no path of offers connecting the `send asset` and `destination asset`. Stellar only considers paths of length 5 or shorter. | +| PATH_PAYMENT_STRICT_SEND_OFFER_CROSS_SELF | -11 | The payment would cross one of its own offers. | +| PATH_PAYMENT_STRICT_SEND_UNDER_DESTMIN | -12 | The paths that could send `destination amount` of `destination asset` would fall short of `destination min`. | + +## Path Payment Strict Receive + +[JavaScript](https://stellar.github.io/js-stellar-sdk/Operation.html#.pathPaymentStrictReceive) | [Java](https://stellar.github.io/java-stellar-sdk/org/stellar/sdk/PathPaymentStrictReceiveOperation.Builder.html) | [Go](https://godoc.org/github.com/stellar/go/txnbuild#PathPaymentStrictReceive) + +A path payment sends an amount of a specific asset to a destination account through a path of offers. Since the asset sent (e.g., 450 XLM) can be different from the asset received (e.g, 6 BTC), path payments allow for the simultaneous transfer and conversion of currencies. + +A Path Payment Strict Receive allows a user to specify the _amount of the asset received_. The amount sent varies based on offers in the order books. If you would like to instead specify the amount sent, use the [Path Payment Strict Send](#path-payment-strict-send) operation. + +A few things to note: + +- path payment doesn't allow intermediate offers to be from the source account as this would yield a worse exchange rate. You'll need to either split the path payment into two smaller path payments, or ensure that the source account's offers are not at the top of the order book. +- balances are settled at the very end of the operation + - this is especially important when `(Destination, Destination Asset) == (Source, Send Asset)` as this provides a functionality equivalent to getting a no interest loan for the duration of the operation. +- `Send max` is a protective measure: it allows you to specify an upper bound for an acceptable conversion. If offers in the order books are not favorable enough for the operation to succeed for less than `Send max`, the operation will fail. + +As of protocol 18, the network automatically considers both the orderbook and any liquidity pools when trying to find paths to exchange one asset for another. For example, if a user wants their recipient to get 1000 Zs from your As, they may consume an order to get Ws for their As, then exchange against the W-Z liquidity pool for the second hop of the payment. + +Threshold: Medium + +Result: `PathPaymentStrictReceiveResult` + +Parameters: + +| Parameters | Type | Description | +| --- | --- | --- | +| Send asset | asset | The asset deducted from the sender's account. | +| Send max | integer | The maximum amount of `send asset` to deduct (excluding fees). | +| Destination | account ID | Account ID of the recipient. | +| Destination asset | asset | The asset the destination account receives. | +| Destination amount | integer | The amount of `destination asset` the destination account receives. | +| Path | list of assets | The assets (other than `send asset` and `destination asset`) involved in the offers the path takes. For example, if you can only find a path from USD to EUR through XLM and BTC, the path would be USD -> XLM -> BTC -> EUR and the `path` field would contain XLM and BTC. | + +Possible errors: + +| Error | Code | Description | +| --- | --- | --- | +| PATH_PAYMENT_STRICT_RECEIVE_MALFORMED | -1 | The input to this path payment is invalid. | +| PATH_PAYMENT_STRICT_RECEIVE_UNDERFUNDED | -2 | The source account (sender) does not have enough funds to send and still satisfy its selling liabilities. Note that if sending XLM then the sender must additionally maintain its minimum XLM reserve. | +| PATH_PAYMENT_STRICT_RECEIVE_SRC_NO_TRUST | -3 | The source account does not trust the issuer of the asset it is trying to send. | +| PATH_PAYMENT_STRICT_RECEIVE_SRC_NOT_AUTHORIZED | -4 | The source account is not authorized to send this payment. | +| PATH_PAYMENT_STRICT_RECEIVE_NO_DESTINATION | -5 | The destination account does not exist. | +| PATH_PAYMENT_STRICT_RECEIVE_NO_TRUST | -6 | The destination account does not trust the issuer of the asset being sent. For more, see the [assets doc](../glossary/assets.mdx). | +| PATH_PAYMENT_STRICT_RECEIVE_NOT_AUTHORIZED | -7 | The destination account is not authorized by the asset's issuer to hold the asset. | +| PATH_PAYMENT_STRICT_RECEIVE_LINE_FULL | -8 | The destination account does not have sufficient limits to receive `destination amount` and still satisfy its buying liabilities. | +| PATH_PAYMENT_STRICT_RECEIVE_TOO_FEW_OFFERS | -10 | There is no path of offers connecting the `send asset` and `destination asset`. Stellar only considers paths of length 5 or shorter. | +| PATH_PAYMENT_STRICT_RECEIVE_OFFER_CROSS_SELF | -11 | The payment would cross one of its own offers. | +| PATH_PAYMENT_STRICT_RECEIVE_OVER_SENDMAX | -12 | The paths that could send `destination amount` of `destination asset` would exceed `send max`. | + +## Manage Buy Offer + +[JavaScript](http://stellar.github.io/js-stellar-sdk/Operation.html#.manageBuyOffer) | [Java](https://stellar.github.io/java-stellar-sdk/org/stellar/sdk/ManageBuyOfferOperation.Builder.html) | [Go](https://godoc.org/github.com/stellar/go/txnbuild#ManageBuyOffer) + +Creates, updates, or deletes an offer to buy one asset for another, otherwise known as a "bid" order on a traditional orderbook. + +If you want to create a new offer, set Offer ID to `0`. + +If you want to update an existing offer, set Offer ID to existing offer ID. + +If you want to delete an existing offer, set Offer ID to existing offer ID and set Amount to `0`. + +Threshold: Medium + +Result: `ManageBuyOfferResult` + +| Parameters | Type | Description | +| --- | --- | --- | +| Selling | asset | Asset the offer creator is selling. | +| Buying | asset | Asset the offer creator is buying. | +| Amount | integer | Amount of `buying` being bought. Set to `0` if you want to delete an existing offer. | +| Price | {numerator, denominator} | Price of 1 unit of `buying` in terms of `selling`. For example, if you wanted to buy 30 XLM and sell 5 BTC, the price would be {5,30}. | +| Offer ID | unsigned integer | The ID of the offer. `0` for new offer. Set to existing offer ID to update or delete. | + +Possible errors: + +| Error | Code | Description | +| --- | --- | --- | +| MANAGE_BUY_OFFER_MALFORMED | -1 | The input is incorrect and would result in an invalid offer. | +| MANAGE_BUY_OFFER_SELL_NO_TRUST | -2 | The account creating the offer does not have a trustline for the asset it is selling. | +| MANAGE_BUY_OFFER_BUY_NO_TRUST | -3 | The account creating the offer does not have a trustline for the asset it is buying. | +| MANAGE_BUY_OFFER_BUY_NOT_AUTHORIZED | -4 | The account creating the offer is not authorized to sell this asset. | +| MANAGE_BUY_OFFER_SELL_NOT_AUTHORIZED | -5 | The account creating the offer is not authorized to buy this asset. | +| MANAGE_BUY_OFFER_LINE_FULL | -6 | The account creating the offer does not have sufficient limits to receive `buying` and still satisfy its buying liabilities. | +| MANAGE_BUY_OFFER_UNDERFUNDED | -7 | The account creating the offer does not have sufficient limits to send `selling` and still satisfy its selling liabilities. Note that if selling XLM then the account must additionally maintain its minimum XLM reserve, which is calculated assuming this offer will not completely execute immediately. | +| MANAGE_BUY_OFFER_CROSS_SELF | -8 | The account has opposite offer of equal or lesser price active, so the account creating this offer would immediately cross itself. | +| MANAGE_BUY_OFFER_NOT_FOUND | -11 | An offer with that `offerID` cannot be found. | +| MANAGE_BUY_OFFER_LOW_RESERVE | -12 | The account creating this offer does not have enough XLM to satisfy the minimum XLM reserve increase caused by adding a subentry and still satisfy its XLM selling liabilities. For every offer an account creates, the minimum amount of XLM that account must hold will increase. | + +## Manage Sell Offer + +[JavaScript](http://stellar.github.io/js-stellar-sdk/Operation.html#.manageSellOffer) | [Java](https://stellar.github.io/java-stellar-sdk/org/stellar/sdk/ManageSellOfferOperation.Builder.html) | [Go](https://godoc.org/github.com/stellar/go/txnbuild#ManageSellOffer) + +Creates, updates, or deletes an offer to sell one asset for another, otherwise known as an "ask" order or "offer" on a traditional orderbook. + +If you want to create a new offer, set Offer ID to `0`. + +If you want to update an existing offer, set Offer ID to existing offer ID. + +If you want to delete an existing offer, set Offer ID to existing offer ID and set Amount to `0`. + +Threshold: Medium + +Result: `ManageSellOfferResult` + +| Parameters | Type | Description | +| --- | --- | --- | +| Selling | asset | Asset the offer creator is selling. | +| Buying | asset | Asset the offer creator is buying. | +| Amount | integer | Amount of `selling` being sold. Set to `0` if you want to delete an existing offer. | +| Price | {numerator, denominator} | Price of 1 unit of `selling` in terms of `buying`. For example, if you wanted to sell 30 XLM and buy 5 BTC, the price would be {5,30}. | +| Offer ID | unsigned integer | The ID of the offer. `0` for new offer. Set to existing offer ID to update or delete. | + +Possible errors: + +| Error | Code | Description | +| --- | --- | --- | +| MANAGE_SELL_OFFER_MALFORMED | -1 | The input is incorrect and would result in an invalid offer. | +| MANAGE_SELL_OFFER_SELL_NO_TRUST | -2 | The account creating the offer does not have a trustline for the asset it is selling. | +| MANAGE_SELL_OFFER_BUY_NO_TRUST | -3 | The account creating the offer does not have a trustline for the asset it is buying. | +| MANAGE_SELL_OFFER_SELL_NOT_AUTHORIZED | -4 | The account creating the offer is not authorized to sell this asset. | +| MANAGE_SELL_OFFER_BUY_NOT_AUTHORIZED | -5 | The account creating the offer is not authorized to buy this asset. | +| MANAGE_SELL_OFFER_LINE_FULL | -6 | The account creating the offer does not have sufficient limits to receive `buying` and still satisfy its buying liabilities. | +| MANAGE_SELL_OFFER_UNDERFUNDED | -7 | The account creating the offer does not have sufficient limits to send `selling` and still satisfy its selling liabilities. Note that if selling XLM then the account must additionally maintain its minimum XLM reserve, which is calculated assuming this offer will not completely execute immediately. | +| MANAGE_SELL_OFFER_CROSS_SELF | -8 | The account has opposite offer of equal or lesser price active, so the account creating this offer would immediately cross itself. | +| MANAGE_SELL_OFFER_NOT_FOUND | -11 | An offer with that `offerID` cannot be found. | +| MANAGE_SELL_OFFER_LOW_RESERVE | -12 | The account creating this offer does not have enough XLM to satisfy the minimum XLM reserve increase caused by adding a subentry and still satisfy its XLM selling liabilities. For every offer an account creates, the minimum amount of XLM that account must hold will increase. | + +## Create Passive Sell Offer + +[JavaScript](https://stellar.github.io/js-stellar-sdk/Operation.html#.createPassiveSellOffer) | [Java](https://stellar.github.io/java-stellar-sdk/org/stellar/sdk/CreatePassiveSellOfferOperation.Builder.html) | [Go](https://godoc.org/github.com/stellar/go/txnbuild#CreatePassiveSellOffer) + +Creates an offer to sell one asset for another, otherwise known as a "ask" order or "offer" on a traditional orderbook, _without taking a reverse offer of equal price_. + +A passive sell offer is an offer that does not act on and take a reverse offer of equal price. Instead, they only take offers of lesser price. For example, if an offer exists to buy 5 BTC for 30 XLM, and you make a passive offer to buy 30 XLM for 5 BTC, your passive offer _does not_ take the first offer. Passive offers in Stellar are always expressed as "ask" or "offer" orders in a traditional orderbook. + +Note that regular offers made later than your passive offer can act on and take your passive offer, even if the regular offer is of the same price as your passive offer. + +Passive offers allow market makers to have zero spread. If you want to trade EUR for USD at 1:1 price and USD for EUR also at 1:1, you can create two passive offers so the two offers don't immediately act on each other. + +Once the passive offer is created, you can manage it like any other offer using the [manage sell offer](#manage-sell-offer) operation. + +Threshold: Medium + +Result: `ManageSellOfferResult` + +| Parameters | Type | Description | +| --- | --- | --- | +| Selling | asset | Asset the offer creator is selling. | +| Buying | asset | Asset the offer creator is buying. | +| Amount | integer | Amount of `selling` being sold. | +| Price | {numerator, denominator} | Price of 1 unit of `selling` in terms of `buying`. For example, if you wanted to sell 30 XLM and buy 5 BTC, the price would be {5,30}. | + +Possible errors: + +| Error | Code | Description | +| --- | --- | --- | +| MANAGE_SELL_OFFER_MALFORMED | -1 | The input is incorrect and would result in an invalid offer. | +| MANAGE_SELL_OFFER_SELL_NO_TRUST | -2 | The account creating the offer does not have a trustline for the asset it is selling. | +| MANAGE_SELL_OFFER_BUY_NO_TRUST | -3 | The account creating the offer does not have a trustline for the asset it is buying. | +| MANAGE_SELL_OFFER_SELL_NOT_AUTHORIZED | -4 | The account creating the offer is not authorized to sell this asset. | +| MANAGE_SELL_OFFER_BUY_NOT_AUTHORIZED | -5 | The account creating the offer is not authorized to buy this asset. | +| MANAGE_SELL_OFFER_LINE_FULL | -6 | The account creating the offer does not have sufficient limits to receive `buying` and still satisfy its buying liabilities. | +| MANAGE_SELL_OFFER_UNDERFUNDED | -7 | The account creating the offer does not have sufficient limits to send `selling` and still satisfy its selling liabilities. Note that if selling XLM then the account must additionally maintain its minimum XLM reserve, which is calculated assuming this offer will not completely execute immediately. | +| MANAGE_SELL_OFFER_CROSS_SELF | -8 | The account has opposite offer of equal or lesser price active, so the account creating this offer would immediately cross itself. | +| MANAGE_SELL_OFFER_NOT_FOUND | -11 | An offer with that `offerID` cannot be found. | +| MANAGE_SELL_OFFER_LOW_RESERVE | -12 | The account creating this offer does not have enough XLM to satisfy the minimum XLM reserve increase caused by adding a subentry and still satisfy its XLM selling liabilities. For every offer an account creates, the minimum amount of XLM that account must hold will increase. | + +## Set Options + +[JavaScript](http://stellar.github.io/js-stellar-sdk/Operation.html#.setOptions) | [Java](http://stellar.github.io/java-stellar-sdk/org/stellar/sdk/SetOptionsOperation.Builder.html) | [Go](https://godoc.org/github.com/stellar/go/txnbuild#SetOptions) + +Sets options for an account, such as setting the inflation destination or adding an additional signer on an account. + +Allows you to set multiple options on an account in a single operation, such as changing an operation threshold and setting the flags on an account at the same time. + +For more information on the options related to signing, see our docs on [multi-sig](../glossary/multisig.mdx). + +When updating signers or other thresholds, the threshold of this operation is High. + +Threshold: Medium or High + +Result: `SetOptionsResult` + +Parameters: + +| Parameters | Type | Description | +| --- | --- | --- | +| Inflation Destination | account ID | Account of the inflation destination. | +| Clear flags | integer | Indicates which flags to clear. For details about the flags, please refer to the [accounts doc](../glossary/accounts.mdx). The bit mask integer subtracts from the existing flags of the account. This allows for setting specific bits without knowledge of existing flags. | +| Set flags | integer | Indicates which flags to set. For details about the flags, please refer to the [accounts doc](../glossary/accounts.mdx). The bit mask integer adds onto the existing flags of the account. This allows for setting specific bits without knowledge of existing flags. | +| Master weight | integer | A number from 0-255 (inclusive) representing the weight of the master key. If the weight of the master key is updated to 0, it is effectively disabled. | +| Low threshold | integer | A number from 0-255 (inclusive) representing the threshold this account sets on all operations it performs that have [a low threshold](../glossary/multisig.mdx). | +| Medium threshold | integer | A number from 0-255 (inclusive) representing the threshold this account sets on all operations it performs that have [a medium threshold](../glossary/multisig.mdx). | +| High threshold | integer | A number from 0-255 (inclusive) representing the threshold this account sets on all operations it performs that have [a high threshold](../glossary/multisig.mdx). | +| Home domain | string | Sets the home domain of an account. See [Federation](../glossary/federation.mdx). | +| Signer | {Public Key, weight} | Add, update, or remove a signer from an account. Signer weight is a number from 0-255 (inclusive). The signer is deleted if the weight is 0. | + +Possible errors: + +| Error | Code | Description | +| --- | --- | --- | +| SET_OPTIONS_LOW_RESERVE | -1 | This account does not have enough XLM to satisfy the minimum XLM reserve increase caused by adding a subentry and still satisfy its XLM selling liabilities. For every new signer added to an account, the minimum reserve of XLM that account must hold increases. | +| SET_OPTIONS_TOO_MANY_SIGNERS | -2 | 20 is the maximum number of signers an account can have, and adding another signer would exceed that. | +| SET_OPTIONS_BAD_FLAGS | -3 | The flags set and/or cleared are invalid by themselves or in combination. | +| SET_OPTIONS_INVALID_INFLATION | -4 | The destination account set in the `inflation` field does not exist. | +| SET_OPTIONS_CANT_CHANGE | -5 | This account can no longer change the option it wants to change. | +| SET_OPTIONS_UNKNOWN_FLAG | -6 | The account is trying to set a flag that is unknown. | +| SET_OPTIONS_THRESHOLD_OUT_OF_RANGE | -7 | The value for a key weight or threshold is invalid. | +| SET_OPTIONS_BAD_SIGNER | -8 | Any additional signers added to the account cannot be the master key. | +| SET_OPTIONS_INVALID_HOME_DOMAIN | -9 | Home domain is malformed. | + +## Change Trust + +[JavaScript](http://stellar.github.io/js-stellar-sdk/Operation.html#.changeTrust) | [Java](http://stellar.github.io/java-stellar-sdk/org/stellar/sdk/ChangeTrustOperation.Builder.html) | [Go](https://godoc.org/github.com/stellar/go/txnbuild#ChangeTrust) + +Creates, updates, or deletes a trustline. For more on trustlines, please refer to the [assets documentation](../glossary/assets.mdx). + +To delete an existing trustline, set Line to the asset of the trustline, and Limit to `0`. + +The `Line` parameter is an instance of a `ChangeTrustAsset`. If you are modifying a trustline to a regular asset (i.e. one in a `Code:Issuer` format), this is equivalent to the `Asset` type. If you are modifying a trustline to a _pool share_, however, this is composed of the liquidity pool reserve assets and the pool fee. You can refer to the liquidity pool [glossary entry](../glossary/liquidity-pool.mdx#liquidity-pool-participation) and the examples therein for details. + +Threshold: Medium + +Result: `ChangeTrustResult` + +| Parameters | Type | Description | +| --- | --- | --- | +| Line | ChangeTrustAsset | The asset of the trustline. For example, if a user extends a trustline of up to 200 USD to an anchor, the `line` is USD:anchor. | +| Limit | integer | The limit of the trustline. In the previous example, the `limit` would be 200. | + +Possible errors: + +| Error | Code | Description | +| --- | --- | --- | +| CHANGE_TRUST_MALFORMED | -1 | The input to this operation is invalid. | +| CHANGE_TRUST_NO_ISSUER | -2 | The issuer of the asset cannot be found. | +| CHANGE_TRUST_INVALID_LIMIT | -3 | The `limit` is not sufficient to hold the current balance of the trustline and still satisfy its buying liabilities. This error occurs when attempting to remove a trustline with a non-zero asset balance. | +| CHANGE_TRUST_LOW_RESERVE | -4 | This account does not have enough XLM to satisfy the minimum XLM reserve increase caused by adding a subentry and still satisfy its XLM selling liabilities. For every new trustline added to an account, the minimum reserve of XLM that account must hold increases. | +| CHANGE_TRUST_SELF_NOT_ALLOWED | -5 | The source account attempted to create a trustline for itself, which is not allowed. | +| CHANGE_TRUST_TRUST_LINE_MISSING | -6 | The asset trustline is missing for the liquidity pool. | +| CHANGE_TRUST_CANNOT_DELETE | -7 | The asset trustline is still referenced by a liquidity pool. | +| CHANGE_TRUST_NOT_AUTH_MAINTAIN_LIABILITIES | -8 | The asset trustline is deauthorized. | + +## Allow Trust + +[JavaScript](http://stellar.github.io/js-stellar-sdk/Operation.html#.allowTrust) | [Java](http://stellar.github.io/java-stellar-sdk/org/stellar/sdk/AllowTrustOperation.Builder.html) | [Go](https://godoc.org/github.com/stellar/go/txnbuild#AllowTrust) + + + +This operation is deprecated as of Protocol 17. Prefer [SetTrustLineFlags](#set-trustline-flags) instead. + + + +Updates the `authorized` flag of an existing trustline. + +This can only be called by the issuer of a trustline's [asset](../glossary/assets.mdx), and only when `AUTHORIZATION REQUIRED` has been set on the issuer's account. + +There are two different kinds of asset authorization: complete authorization, which allows an account to transact with an asset (by making payments, creating offers, etc.) and limited authorization, which allows an account to maintain and reduce current offers, but not to perform other operations with the asset. + +The issuer can only change a flag from complete to limited authorization or clear the `authorized` flag if the issuer has the `AUTH_REVOCABLE_FLAG` set. Otherwise, the issuer can only set the `authorized` flag. For more on what toggling between authorization states allows an issuer to do, see the [Control Access to an Asset](../issuing-assets/control-asset-access.mdx) doc. + +If the issuer clears the `authorized` flag, all offers owned by the `trustor` that are either selling `type` or buying `type` will be deleted. + +Threshold: Low + +Result: `AllowTrustResult` + +| Parameters | Type | Description | +| --- | --- | --- | +| Trustor | account ID | The account of the recipient of the trustline. | +| Type | asset code | The 4 or 12 character-maximum asset code of the trustline the source account is authorizing. For example, if an issuing account wants to allow another account to hold its USD credit, the `type` is `USD`. | +| Authorize | integer | Flag indicating whether the trustline is authorized. `1` if the account is authorized to transact with the asset. `2` if the account is authorized to maintain offers, but not to perform other transactions | + +Possible errors: + +| Error | Code | Description | +| --- | --- | --- | +| ALLOW_TRUST_MALFORMED | -1 | The asset specified in `type` is invalid. In addition, this error happens when the native asset is specified. | +| ALLOW_TRUST_NO_TRUST_LINE | -2 | The `trustor` does not have a trustline with the issuer performing this operation. | +| ALLOW_TRUST_TRUST_NOT_REQUIRED | -3 | The source account (issuer performing this operation) does not require trust. In other words, it does not have the flag `AUTH_REQUIRED_FLAG` set. | +| ALLOW_TRUST_CANT_REVOKE | -4 | The source account is trying to revoke the trustline of the `trustor`, but it cannot do so. | +| ALLOW_TRUST_SELF_NOT_ALLOWED | -5 | The source account attempted to allow a trustline for itself, which is not allowed because an account cannot create a trustline with itself. | +| ALLOW_TRUST_LOW_RESERVE | -6 | Claimable balances can't be created on revocation of asset (or pool share) trustlines associated with a liquidity pool due to low reserves. | + +## Account Merge + +[JavaScript](http://stellar.github.io/js-stellar-sdk/Operation.html#.accountMerge) | [Java](http://stellar.github.io/java-stellar-sdk/org/stellar/sdk/AccountMergeOperation.Builder.html) | [Go](https://godoc.org/github.com/stellar/go/txnbuild#AccountMerge) + +Transfers the native balance (the amount of XLM an account holds) to another account and removes the source account from the ledger. + +Threshold: High + +Result: `AccountMergeResult` + +| Parameters | Type | Description | +| --- | --- | --- | +| Destination | account ID | The account that receives the remaining XLM balance of the source account. | + +Possible errors: + +| Error | Code | Description | +| --- | --- | --- | +| ACCOUNT_MERGE_MALFORMED | -1 | The operation is malformed because the source account cannot merge with itself. The `destination` must be a different account. | +| ACCOUNT_MERGE_NO_ACCOUNT | -2 | The `destination` account does not exist. | +| ACCOUNT_MERGE_IMMUTABLE_SET | -3 | The source account has `AUTH_IMMUTABLE` flag set. | +| ACCOUNT_MERGE_HAS_SUB_ENTRIES | -4 | The source account has trustlines/offers. | +| ACCOUNT_MERGE_SEQNUM_TOO_FAR | -5 | Source's account sequence number is too high. It must be less than `(ledgerSeq << 32) = (ledgerSeq * 0x100000000)`. | +| ACCOUNT_MERGE_DEST_FULL | -6 | The `destination` account cannot receive the balance of the source account and still satisfy its lumen buying liabilities. | +| ACCOUNT_MERGE_IS_SPONSOR | -7 | The source account is a sponsor. | + +## Manage Data + +[JavaScript](http://stellar.github.io/js-stellar-sdk/Operation.html#.manageData) | [Java](http://stellar.github.io/java-stellar-sdk/org/stellar/sdk/ManageDataOperation.Builder.html) | [Go](https://godoc.org/github.com/stellar/go/txnbuild#ManageData) + +Sets, modifies, or deletes a data entry (name/value pair) that is attached to a particular account. + +An account can have a large amount of data entries attached to it (subject to sub-entry limits for an account). Each data entry increases the minimum balance (via the base reserve) needed to be held by the account. + +Data entries can be used for storing application-specific data on the Stellar Network. They are not used by the core Stellar Protocol. + +Threshold: Medium + +Result: `ManageDataResult` + +| Parameters | Type | Description | +| --- | --- | --- | +| Name | string | String up to 64 bytes long. If this is a new Name it will add the given name/value pair to the account. If this Name is already present then the associated value will be modified. | +| Value | binary data | (optional) If not present then the existing Name will be deleted. If present then this value will be set in the DataEntry. Up to 64 bytes long. | + +Possible errors: + +| Error | Code | Description | +| --- | --- | --- | +| MANAGE_DATA_NOT_SUPPORTED_YET | -1 | The network hasn't moved to this protocol change yet. This failure means the network doesn't support this feature yet. | +| MANAGE_DATA_NAME_NOT_FOUND | -2 | Trying to remove a Data Entry that isn't there. This will happen if Name is set (and Value isn't) but the Account doesn't have a DataEntry with that Name. | +| MANAGE_DATA_LOW_RESERVE | -3 | This account does not have enough XLM to satisfy the minimum XLM reserve increase caused by adding a subentry and still satisfy its XLM selling liabilities. For every new DataEntry added to an account, the minimum reserve of XLM that account must hold increases. | +| MANAGE_DATA_INVALID_NAME | -4 | Name not a valid string. | + +## Bump Sequence + +[JavaScript](http://stellar.github.io/js-stellar-sdk/Operation.html#.bumpSequence) | [Java](http://stellar.github.io/java-stellar-sdk/org/stellar/sdk/BumpSequenceOperation.Builder.html) | [Go](https://godoc.org/github.com/stellar/go/txnbuild#BumpSequence) + +Bumps forward the sequence number of the source account to the given sequence number. + +This operation invalidates any transactions with a smaller sequence number, and is often utilized in complex contracting scenarios. + +If the specified `bumpTo` sequence number is greater than the source account's sequence number, the account's sequence number is updated with that value, otherwise it's not modified. + +Threshold: Low + +Result: `BumpSequenceResult` + +| Parameters | Type | Description | +| --- | --- | --- | +| bumpTo | SequenceNumber | desired value for the operation's source account sequence number. | + +Possible errors: + +| Error | Code | Description | +| --- | --- | --- | +| BUMP_SEQUENCE_BAD_SEQ | -1 | The specified `bumpTo` sequence number is not a valid sequence number. It must be between 0 and `INT64_MAX` (9223372036854775807 or 0x7fffffffffffffff). | + +## Create Claimable Balance + +Creates a ClaimableBalanceEntry. See [Claimable Balance](../glossary/claimable-balance.mdx) for more information on parameters and usage. + +Threshold: Medium + +Result: `CreateClaimableBalanceResult` + +| Parameters | Type | Description | +| --- | --- | --- | +| Asset | asset | Asset that will be held in the ClaimableBalanceEntry in the form `asset_code:issuing_address` or `native` (XLM). | +| Amount | integer | Amount of `asset` stored in the ClaimableBalanceEntry. | +| Claimants | list of claimants | List of Claimants (account address and ClaimPredicate pair) that can claim this ClaimableBalanceEntry. | + +Possible errors: + +| Error | Code | Description | +| --- | --- | --- | +| CREATE_CLAIMABLE_BALANCE_MALFORMED | -1 | The input to this operation is invalid. | +| CREATE_CLAIMABLE_BALANCE_LOW_RESERVE | -2 | The account creating this entry does not have enough XLM to satisfy the minimum XLM reserve increase caused by adding a ClaimableBalanceEntry. For every claimant in the list, the minimum amount of XLM this account must hold will increase by baseReserve. | +| CREATE_CLAIMABLE_BALANCE_NO_TRUST | -3 | The source account does not trust the issuer of the asset it is trying to include in the ClaimableBalanceEntry. | +| CREATE_CLAIMABLE_BALANCE_NOT_AUTHORIZED | -4 | The source account is not authorized to transfer this asset. | +| CREATE_CLAIMABLE_BALANCE_UNDERFUNDED | -5 | The source account does not have enough funds to transfer `amount` of this asset to the ClaimableBalanceEntry. | + +## Claim Claimable Balance + +Claims a ClaimableBalanceEntry and adds the amount of asset on the entry to the source account. + +Threshold: Low + +Result: `ClaimClaimableBalanceResult` + +| Parameters | Type | Description | +| --- | --- | --- | +| BalanceID | claimableBalanceID | BalanceID on the ClaimableBalanceEntry that the source account is claiming. The balanceID can be retrieved from a successful `CreateClaimableBalanceResult`. See [ClaimableBalanceID](../glossary/miscellaneous-core-objects.mdx#ClaimableBalanceID) for more information. | + +Possible errors: + +| Error | Code | Description | +| --- | --- | --- | +| CLAIM_CLAIMABLE_BALANCE_DOES_NOT_EXIST | -1 | There is no existing ClaimableBalanceEntry that matches the input BalanceID. | +| CLAIM_CLAIMABLE_BALANCE_CANNOT_CLAIM | -2 | There is no claimant that matches the source account, or the claimants predicate is not satisfied. | +| CLAIM_CLAIMABLE_BALANCE_LINE_FULL | -3 | The account claiming the ClaimableBalanceEntry does not have sufficient limits to receive amount of the asset and still satisfy its buying liabilities. | +| CLAIM_CLAIMABLE_BALANCE_NO_TRUST | -4 | The source account does not trust the issuer of the asset it is trying to claim in the ClaimableBalanceEntry. | +| CLAIM_CLAIMABLE_BALANCE_NOT_AUTHORIZED | -5 | The source account is not authorized to claim the asset in the ClaimableBalanceEntry. | + +## Begin Sponsoring Future Reserves + +Establishes the is-sponsoring-future-reserves-for relationship between the source account and sponsoredID. See [Sponsored Reserves](../glossary/sponsored-reserves.mdx) for more information. + +There must be a corresponding [end sponsoring future reserves](#end-sponsoring-future-reserves) operation in the same transaction to end the is-sponsoring-future-reserves-for relationship. The transaction will fail with `txBAD_SPONSORSHIP` otherwise. + +Threshold: Medium + +Result: `BeginSponsoringFutureReservesResult` + +| Parameters | Type | Description | +| ----------- | ---------- | ---------------------------------------------- | +| SponsoredID | account ID | Account that will have its reserves sponsored. | + +Possible errors: + +| Error | Code | Description | +| --- | --- | --- | +| BEGIN_SPONSORING_FUTURE_RESERVES_MALFORMED | -1 | Source account is equal to sponsoredID. | +| BEGIN_SPONSORING_FUTURE_RESERVES_ALREADY_SPONSORED | -2 | Source account is already sponsoring sponsoredID. | +| BEGIN_SPONSORING_FUTURE_RESERVES_RECURSIVE | -3 | Either source account is currently being sponsored, or sponsoredID is sponsoring another account. | + +## End Sponsoring Future Reserves + +Terminates the current is-sponsoring-future-reserves-for relationship in which the source account is sponsored. + +Threshold: Medium + +Result: `EndSponsoringFutureReservesResult` + +| Error | Code | Description | +| --- | --- | --- | +| END_SPONSORING_FUTURE_RESERVES_NOT_SPONSORED | -1 | Source account is not sponsored. | + +## Revoke Sponsorship + +The logic of this operation depends on the state of the source account. + +If the source account is not sponsored or is sponsored by the owner of the specified entry or sub-entry, then attempt to revoke the sponsorship. If the source account is sponsored, the next step depends on whether the entry is sponsored or not. If it is sponsored, attempt to transfer the sponsorship to the sponsor of the source account. If the entry is not sponsored, then establish the sponsorship. + +Threshold: Medium + +Result: `RevokeSponsorshipResult` + +This operation is a union with **two** possible types - + +| Union Type | Parameters | Type | Description | +| --- | --- | --- | --- | +| REVOKE_SPONSORSHIP_LEDGER_ENTRY | LedgerKey | ledgerKey | Ledger key that holds information to identify a specific ledgerEntry that may have its sponsorship modified. See [LedgerKey](../glossary/miscellaneous-core-objects.mdx#LedgerKey) for more information. | + +Or + +| Union Type | Parameters | Type | Description | +| --- | --- | --- | --- | +| REVOKE_SPONSORSHIP_SIGNER | Signer | {account ID, Signer Key} | Signer that may have its sponsorship modified. | + +Possible errors: + +| Error | Code | Description | +| --- | --- | --- | +| REVOKE_SPONSORSHIP_DOES_NOT_EXIST | -1 | The ledgerEntry for LedgerKey doesn't exist, the account ID on signer doesn't exist, or the Signer Key doesn't exist on account ID's account. | +| REVOKE_SPONSORSHIP_NOT_SPONSOR | -2 | If the ledgerEntry/signer is sponsored, then the source account must be the sponsor. If the ledgerEntry/signer is not sponsored, the source account must be the owner. This error will be thrown otherwise. | +| REVOKE_SPONSORSHIP_LOW_RESERVE | -3 | The sponsored account does not have enough XLM to satisfy the minimum balance increase caused by revoking sponsorship on a ledgerEntry/signer it owns, or the sponsor of the source account doesn't have enough XLM to satisfy the minimum balance increase caused by sponsoring a transferred ledgerEntry/signer. | +| REVOKE_SPONSORSHIP_ONLY_TRANSFERABLE | -4 | Sponsorship cannot be removed from this ledgerEntry. This error will happen if the user tries to remove the sponsorship from a ClaimableBalanceEntry. | +| REVOKE_SPONSORSHIP_MALFORMED | -5 | One or more of the inputs to the operation was malformed. | + +## Set Trustline Flags + +[JavaScript](http://stellar.github.io/js-stellar-sdk/Operation.html#.setTrustLineFlags) | [Java](http://stellar.github.io/java-stellar-sdk/org/stellar/sdk/SetTrustlineFlagsOperation.Builder.html) | [Go](https://godoc.org/github.com/stellar/go/txnbuild#SetTrustLineFlags) + +Allows an issuing account to configure various authorization and trustline flags for all trustlines to an asset. This supersedes the deprecated [`AllowTrust`](#allow-trust), but the documentation there still applies. + +The `Asset` parameter is of the `TrustLineAsset` type. If you are modifying a trustline to a regular asset (i.e. one in a `Code:Issuer` format), this is equivalent to the `Asset` type. If you are modifying a trustline to a _pool share_, however, this is composed of the liquidity pool's unique ID. You can refer to the liquidity pool [glossary entry](../glossary/liquidity-pool.mdx#liquidity-pool-participation) and the examples therein for details. + +Threshold: Low + +Result: `SetTrustLineFlagsResult` + +| Parameters | Type | Description | +| --- | --- | --- | +| Trustor | account ID | The account that established this trustline. | +| Asset | TrustLineAsset | The asset trustline whose flags are being modified. | +| SetFlags | integer | One or more flags (combined via bitwise-OR) indicating which flags to **set**. Possible flags are: `1` if the trustor is authorized to transact with the asset or `2` if the trustor is authorized to maintain offers but not to perform other transactions. | +| ClearFlags | integer | One or more flags (combined via bitwise `OR`) indicating which flags to **clear**. Possibilities include those for `SetFlags`as well as `4`, which prevents the issuer from clawing back its asset (both from accounts and claimable balances). | + +Note that passing `0` to either `SetFlags` or `ClearFlags` will be a no-op (nothing will be set or cleared). + +Possible errors: + +| Error | Code | Description | +| --- | --- | --- | +| SET_TRUST_LINE_FLAGS_MALFORMED | -1 | This can happen for a number of reasons: the asset specified by `AssetCode` and `AssetIssuer` is invalid; the asset issuer isn't the source account; the `Trustor` is the source account; the native asset is specified; or the flags being set/cleared conflict or are otherwise invalid. | +| SET_TRUST_LINE_FLAGS_NO_TRUST_LINE | -2 | The `Trustor` does not have a trustline with the issuer performing this operation. | +| SET_TRUST_LINE_FLAGS_CANT_REVOKE | -3 | The issuer is trying to revoke the trustline authorization of `Trustor`, but it cannot do so because `AUTH_REVOCABLE_FLAG` is not set on the account. | +| SET_TRUST_LINE_FLAGS_INVALID_STATE | -4 | If the final state of the trustline has both `AUTHORIZED_FLAG` (1) and `AUTHORIZED_TO_MAINTAIN_LIABILITIES_FLAG` (2) set, which are mutually exclusive. | +| SET_TRUST_LINE_FLAGS_LOW_RESERVE | -5 | Claimable balances can't be created on revocation of asset (or pool share) trustlines associated with a liquidity pool due to low reserves. | + +## Clawback + +[JavaScript](http://stellar.github.io/js-stellar-sdk/Operation.html#.clawback) | [Java](http://stellar.github.io/java-stellar-sdk/org/stellar/sdk/ClawbackOperation.Builder.html) | [Go](https://godoc.org/github.com/stellar/go/txnbuild#Clawback) + +Reduces (burns) an amount in a specific asset from an account. + +Threshold: Medium + +Result: `ClawbackResult` + +Parameters: + +| Parameters | Type | Description | +| ---------- | ---------- | ------------------------------------------- | +| From | account ID | Account address that receives the clawback. | +| Asset | asset | Asset held by the destination account. | +| Amount | integer | Amount of the aforementioned asset to burn. | + +Possible errors: + +| Error | Code | Description | +| --- | --- | --- | +| CLAWBACK_MALFORMED | -1 | The input to the clawback is invalid. | +| CLAWBACK_NOT_CLAWBACK_ENABLED | -2 | The trustline between `From` and the issuer account for this `Asset` does not have clawback enabled. | +| CLAWBACK_NO_TRUST | -3 | The `From` account does not trust the issuer of the asset. | +| CLAWBACK_UNDERFUNDED | -4 | The `From` account does not have a sufficient available balance of the asset (after accounting for selling liabilities). | + +## Clawback Claimable Balance + +Claws back an unclaimed ClaimableBalanceEntry, burning the pending amount of the asset. + +Threshold: Medium + +Result: `ClaimClaimableBalanceResult` + +| Parameters | Type | Description | +| --- | --- | --- | +| BalanceID | claimableBalanceID | The BalanceID on the ClaimableBalanceEntry that the source account is claiming, which can be retrieved from a succesful `CreateClaimableBalanceResult`. | + +Refer to the [ClaimableBalanceID](../glossary/miscellaneous-core-objects.mdx#ClaimableBalanceID) or the [Claimable Balance](../glossary/claimable-balance.mdx) glossary entries for more information on acquiring a claimable balance ID. + +Possible errors: + +| Error | Code | Description | +| --- | --- | --- | +| CLAWBACK_CLAIMABLE_BALANCE_DOES_NOT_EXIST | -1 | There is no existing ClaimableBalanceEntry that matches the input BalanceID. | +| CLAWBACK_CLAIMABLE_BALANCE_NOT_ISSUER | -2 | The source account is not the issuer of the asset in the claimable balance. | +| CLAWBACK_CLAIMABLE_BALANCE_NOT_CLAWBACK_ENABLED | -3 | The `CLAIMABLE_BALANCE_CLAWBACK_ENABLED_FLAG` is not set for this trustline. | + +## Liquidity Pool Deposit + +Deposits assets into a liquidity pool. + +Depositing increases the reserves of a liquidity pool in exchange for pool shares. + +Parameters to this operation depend on the ordering of assets in the liquidity pool: “A” refers to the first asset in the liquidity pool, and “B” refers to the second asset in the liquidity pool. Refer to the liquidity pool [glossary entry](../glossary/liquidity-pool.mdx#liquidity-pool-participation) for information about how to determine the fixed ordering of assets. SDKs often have helpers (like [`Asset.compare`](https://stellar.github.io/js-stellar-base/Asset.html#.compare)) to determine the right order. + +If the pool is empty, then this operation deposits `maxAmountA` of A and `maxAmountB` of B into the pool. If the pool is not empty, then this operation deposits **at most** `maxAmountA` of A and `maxAmountB` of B into the pool. The actual amounts deposited are determined using the current reserves of the pool. You can use these parameters to control a percentage of slippage; refer to the [example](../glossary/liquidity-pool.mdx#participation:-deposits) for details. + +Threshold: Medium + +Result: `LiquidityPoolDepositResult` + +| Parameters | Type | Description | +| --- | --- | --- | +| Liquidity Pool ID | liquidityPoolID | The PoolID for the Liquidity Pool to deposit into | +| Max Amount A | integer | Maximum amount of first asset to deposit | +| Max Amount B | integer | Maximum amount of second asset to deposit | +| Min Price | {numerator, denominator} | Minimum depositA/depositB | +| Max Price | {numerator, denominator} | Maximum depositA/depositB | + +Possible errors: + +| Error | Code | Description | +| --- | --- | --- | +| LIQUIDITY_POOL_DEPOSIT_MALFORMED | -1 | One or more of the inputs to the operation was malformed. | +| LIQUIDITY_POOL_DEPOSIT_NO_TRUST | -2 | No trustline exists for one of the assets being deposited. | +| LIQUIDITY_POOL_DEPOSIT_NOT_AUTHORIZED | -3 | The account does not have authorization for one of the assets. | +| LIQUIDITY_POOL_DEPOSIT_UNDERFUNDED | -4 | There is not enough balance of one of the assets to perform the deposit. | +| LIQUIDITY_POOL_DEPOSIT_LINE_FULL | -5 | The pool share trustline does not have a sufficient limit. | +| LIQUIDITY_POOL_DEPOSIT_BAD_PRICE | -6 | The deposit price is outside of the given bounds. | +| LIQUIDITY_POOL_DEPOSIT_POOL_FULL | -7 | The liquidity pool reserves are full. | + +## Liquidity Pool Withdraw + +Withdraw assets from a liquidity pool. + +Withdrawing reduces the number of pool shares in exchange for reserves from a liquidity pool. + +Refer to the discussion on asset ordering in [Liquidity Pool Deposit](#liquidity-pool-deposit), above, for details on the “A” and “B” parameters. + +The `minAmountA` and `minAmountB` parameters can be used to control a percentage of slippage from the "spot price" on the pool; refer to the [example](../glossary/liquidity-pool.mdx#participation:-deposits) for details. + +Threshold: Medium + +Result: `LiquidityPoolWithdrawResult` + +| Parameters | Type | Description | +| --- | --- | --- | +| Liquidity Pool ID | liquidityPoolID | The PoolID for the Liquidity Pool to withdraw from | +| Amount | integer | Amount of pool shares to withdraw | +| Min Amount A | integer | Minimum amount of the first asset to withdraw | +| Min Amount B | integer | Minimum amount of the second asset to withdraw | + +Possible errors: + +| Error | Code | Description | +| --- | --- | --- | +| LIQUIDITY_POOL_WITHDRAW_MALFORMED | -1 | One or more of the inputs to the operation was malformed. | +| LIQUIDITY_POOL_WITHDRAW_NO_TRUST | -2 | There is no trustline for one of the assets. | +| LIQUIDITY_POOL_WITHDRAW_UNDERFUNDED | -3 | Insufficient balance for the pool shares. | +| LIQUIDITY_POOL_WITHDRAW_LINE_FULL | -4 | The withdrawal would exceed the trustline limit for one of the assets. | +| LIQUIDITY_POOL_WITHDRAW_UNDER_MINIMUM | -5 | Unable to withdraw enough to satisfy the minimum price. | diff --git a/docs/start/metadata.json b/docs/start/metadata.json new file mode 100644 index 000000000..83f2bea41 --- /dev/null +++ b/docs/start/metadata.json @@ -0,0 +1,4 @@ +{ + "order": 10, + "title": "Where to Start" +} diff --git a/docs/start/stellar-stack.mdx b/docs/start/stellar-stack.mdx new file mode 100644 index 000000000..1e88906d0 --- /dev/null +++ b/docs/start/stellar-stack.mdx @@ -0,0 +1,35 @@ +--- +title: The Stellar Stack +order: 10 +--- + +import { CodeExample } from "@site/src/components/CodeExample"; + +Fundamentally, Stellar is a collection of Stellar Core nodes, which are computers that keep a common ledger of accounts and balances, listen for incoming transactions, and, using the Stellar Consensus Protocol, agree to apply a valid set of those transactions to update the ledger. Each transaction applied to the ledger incurs a small fee — which is necessary to prevent bad actors from spamming the network — and the ledger is generally updated every 3-5 seconds. + +However, most developers don't interact directly with a Stellar Core node. Rather, they program using a Software Development Kit written in their preferred language, and those SDKs in turn interact with Horizon, the Stellar-network API. This three-tiered stack divides responsibilities so each piece of software can focus on a specific purpose. Stellar Core concentrates on transaction submission and consensus; Horizon handles queries and converts network data into a friendly format; SDKs abstract away complexity and offer ergonomic access in a variety of languages. + +## Stellar SDKs + +SDKs make it easy to craft code and handle network queries and transaction submissions. They're linked to in the [SDK section of the docs](../software-and-sdks/index.mdx), and each is robust, and has its own documentation showing you how to request data and create and submit transactions. When you start developing on Stellar, the first step is usually to find the SDK in your language of choice and familiarize yourself with how it works. + +## API: Horizon + +[Horizon](../run-api-server/index.mdx) is a RESTful HTTP API server that provides a straightforward way to submit transactions, check accounts, and subscribe to events. Because it’s HTTP, you can communicate with Horizon using an SDK, but you can also use your web browser, or simple command line tools like cURL. Everything there is to know about Horizon is documented in the [API Reference](/api/introduction/) section of the docs. + +At the moment, Horizon requires access to Stellar Core's database to function properly — so every Horizon instance connects to a Stellar Core node — but we are increasing its independence from Stellar Core, and soon developers will be able deploy the API without having to run their own node. + +## Network Backbone: Stellar Core + +The Stellar Core software does the hard work of validating and agreeing with other instances of Core on the status of every transaction through the [Stellar Consensus Protocol](../glossary/scp.mdx) (SCP). The ledger, transactions, results, history, and even the messages passed between computers running Stellar Core are encoded using XDR, which is incredibly efficient, but not human readable. Stellar Core nodes make up the network — and running a node is crucial if you want to ensure constant access or contribute to the health and decentralization of the network — but most developers don't work directly with Stellar Core. For more on how to set up a node, consult the [Run a Core Node](../run-core-node/index.mdx) section. + +## The Public Network and the Test Network + +There are two different versions of the Stellar network: one for testing and one for real-world deployments. The Stellar Development Foundation provides a free public Horizon instance for each, which you can use to submit transactions or query network data. + +- https://horizon.stellar.org/ is for interacting with the public network +- https://horizon-testnet.stellar.org/ is for interacting with the testnet + +Assets on the testnet don't represent anything in the real world, and when you're developing on the testnet, you can get free test XLM from a tool called Friendbot. On the testnet, you're free to experiment, create, and troubleshoot without risking the loss of funds. It generally upgrades a month before the public network — so if you're using it, you need to keep an eye out for major protocol releases — and unlike the public network — where data persists forever — the testnet gets reset every quarter. Additionally, it has a lower ledger limit than the public network: currently, the testnet tops out at 100 operations/ledger; the public network at 1,000 operations/ledger. + +Other than that, the two networks are the same: they consist of Stellar Core nodes, support the Horizon API, and work with Stellar SDKs. Both support the same operations, process transactions in 3-5 seconds, and require the same fees and network minimum. In fact, if you build something on the testnet and decide you're ready to deploy it on the public network, all you need to do is change the [Network Passphrase](../glossary/network-passphrase.mdx). For more, check out our [guide to best practices for building on the testnet](../glossary/testnet.mdx). diff --git a/docs/tutorials/create-account.mdx b/docs/tutorials/create-account.mdx new file mode 100644 index 000000000..0687fdda8 --- /dev/null +++ b/docs/tutorials/create-account.mdx @@ -0,0 +1,251 @@ +--- +title: Create an Account +order: 10 +--- + +import { CodeExample } from "@site/src/components/CodeExample"; +import { Alert } from "@site/src/components/Alert"; + +_Before we get started with working with Stellar in code, consider going through the following examples using the [Stellar Laboratory](https://www.stellar.org/laboratory/). The lab allows you create accounts, fund accounts on the Stellar test network, build transactions, run any operation, and inspect responses from Horizon via the Endpoint Explorer._ + +[Accounts](../glossary/accounts.mdx) are a fundamental building block of Stellar: they hold all your balances, allow you to send and receive payments, and let you place offers to buy and sell assets. Since pretty much everything on Stellar is in some way tied to an account, the first thing you generally need to do when you start developing is create one. This beginner-level tutorial will show you how to do that. + +## Create a Keypair + +Stellar uses public key cryptography to ensure that every transaction is secure: every Stellar account has a keypair consisting of a **public key** and a **secret key**. The public key is always safe to share — other people need it to identify your account and verify that you authorized a transaction. It's like an email address. The secret key, however, is private information that proves you own — and gives you access to — your account. It's like a password, and you should never share it with anyone. + +Before creating an account, you need to generate your own keypair: + + + +```js +// create a completely new and unique pair of keys +// see more about KeyPair objects: https://stellar.github.io/js-stellar-sdk/Keypair.html +const pair = StellarSdk.Keypair.random(); + +pair.secret(); +// SAV76USXIJOBMEQXPANUOQM6F5LIOTLPDIDVRJBFFE2MDJXG24TAPUU7 +pair.publicKey(); +// GCFXHS4GXL6BVUCXBWXGTITROWLVYXQKQLF4YH5O5JT3YZXCYPAFBJZB +``` + +```java +// create a completely new and unique pair of keys. +// see more about KeyPair objects: https://stellar.github.io/java-stellar-sdk/org/stellar/sdk/KeyPair.html +import org.stellar.sdk.KeyPair; +KeyPair pair = KeyPair.random(); + +System.out.println(new String(pair.getSecretSeed())); +// SAV76USXIJOBMEQXPANUOQM6F5LIOTLPDIDVRJBFFE2MDJXG24TAPUU7 +System.out.println(pair.getAccountId()); +// GCFXHS4GXL6BVUCXBWXGTITROWLVYXQKQLF4YH5O5JT3YZXCYPAFBJZB +``` + +```go +package main + +import ( + "log" + + "github.com/stellar/go/keypair" +) + +func main() { + pair, err := keypair.Random() + if err != nil { + log.Fatal(err) + } + + log.Println(pair.Seed()) + // SAV76USXIJOBMEQXPANUOQM6F5LIOTLPDIDVRJBFFE2MDJXG24TAPUU7 + log.Println(pair.Address()) + // GCFXHS4GXL6BVUCXBWXGTITROWLVYXQKQLF4YH5O5JT3YZXCYPAFBJZB +} +``` + +```python +# stellar-sdk >= 2.0.0 required +# create a completely new and unique pair of keys +# see more about KeyPair objects: https://stellar-sdk.readthedocs.io/en/latest/api.html#keypair +from stellar_sdk import Keypair + +pair = Keypair.random() +print(f"Secret: {pair.secret}") +# Secret: SCMDRX7A7OVRPAGXLUVRNIYTWBLCS54OV7UH2TF5URSG4B4JQMUADCYU +print(f"Public Key: {pair.public_key}") +# Public Key: GAG7SXULMNWCW6LX42JKZOZRA2JJXQT23LYY32OXA6XECUQG7RZTQJHO +``` + + + +## Create Account + +A valid keypair, however, does not make an account: in order to prevent unused accounts from bloating the ledger, Stellar requires accounts to hold a [minimum balance](../glossary/minimum-balance.mdx) of 1 XLM before they actually exist. Until it gets a bit of funding, your keypair doesn't warrant space on the ledger. + +On the [public network](../glossary/network-passphrase.mdx), where live users make live transactions, your next step would be to acquire XLM, which you can do by consulting our [lumen buying guide](https://www.stellar.org/lumens/exchanges). Because this tutorial runs on the [test network](../glossary/testnet.mdx), you can get 10,000 test XLM from Friendbot, which is a friendly account funding tool. + +To do that, send Friendbot the public key you created. It’ll create and fund a new account using that public key as the account ID. + + + +```js +// The SDK does not have tools for creating test accounts, so you'll have to +// make your own HTTP request. + +// if you're trying this on Node, install the `node-fetch` library and +// uncomment the next line: +// const fetch = require('node-fetch'); + +(async function main() { + try { + const response = await fetch( + `https://friendbot.stellar.org?addr=${encodeURIComponent( + pair.publicKey(), + )}`, + ); + const responseJSON = await response.json(); + console.log("SUCCESS! You have a new account :)\n", responseJSON); + } catch (e) { + console.error("ERROR!", e); + } +})(); +``` + +```java +// The SDK does not have tools for creating test accounts, so you'll have to +// make your own HTTP request. +import java.net.*; +import java.io.*; +import java.util.*; + +String friendbotUrl = String.format( + "https://friendbot.stellar.org/?addr=%s", + pair.getAccountId()); +InputStream response = new URL(friendbotUrl).openStream(); +String body = new Scanner(response, "UTF-8").useDelimiter("\\A").next(); +System.out.println("SUCCESS! You have a new account :)\n" + body); +``` + +```go +package main + +import ( + "net/http" + "io/ioutil" + "log" + "fmt" +) + +func main() { + // pair is the pair that was generated from previous example, or create a pair based on + // existing keys. + address := pair.Address() + resp, err := http.Get("https://friendbot.stellar.org/?addr=" + address) + if err != nil { + log.Fatal(err) + } + + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Fatal(err) + } + fmt.Println(string(body)) +} +``` + +```python +# The SDK does not have tools for creating test accounts, so you'll have to +# make your own HTTP request. + +# if you're trying this on Python, install the `requests` library. +import requests + +public_key = "GD4NB2FLQAN5JO7PKPGZJMNBDYQXVSNVC7DEIZMOL5WSNSBLEBUTEF5Q" +response = requests.get(f"https://friendbot.stellar.org?addr={public_key}") +if response.status_code == 200: + print(f"SUCCESS! You have a new account :)\n{response.text}") +else: + print(f"ERROR! Response: \n{response.text}") +``` + + + +Now for the last step: getting the account’s details and checking its balance. Accounts can carry multiple balances — one for each type of currency they hold. + + + +```js +const server = new StellarSdk.Server("https://horizon-testnet.stellar.org"); + +// the JS SDK uses promises for most actions, such as retrieving an account +const account = await server.loadAccount(pair.publicKey()); +console.log("Balances for account: " + pair.publicKey()); +account.balances.forEach(function (balance) { + console.log("Type:", balance.asset_type, ", Balance:", balance.balance); +}); +``` + +```java +import org.stellar.sdk.Server; +import org.stellar.sdk.responses.AccountResponse; + +Server server = new Server("https://horizon-testnet.stellar.org"); +AccountResponse account = server.accounts().account(pair.getAccountId()); +System.out.println("Balances for account " + pair.getAccountId()); +for (AccountResponse.Balance balance : account.getBalances()) { + System.out.printf( + "Type: %s, Code: %s, Balance: %s%n", + balance.getAssetType(), + balance.getAssetCode(), + balance.getBalance() + ); +} +``` + +```go +package main + +import ( + "log" + + "github.com/stellar/go/clients/horizonclient" +) + +func main() { + // Replace this with the output from earlier, or use pair.Address() + address := "GCFXHS4GXL6BVUCXBWXGTITROWLVYXQKQLF4YH5O5JT3YZXCYPAFBJZB" + + request := horizonclient.AccountRequest{AccountID: address} + account, err := horizonclient.DefaultTestNetClient.AccountDetail(request) + if err != nil { + log.Fatal(err) + } + + log.Println("Balances for account:", address) + + for _, balance := range account.Balances { + log.Println(balance) + } +} +``` + +```python +from stellar_sdk import Server + +server = Server("https://horizon-testnet.stellar.org") +public_key = "GD4NB2FLQAN5JO7PKPGZJMNBDYQXVSNVC7DEIZMOL5WSNSBLEBUTEF5Q" +account = server.accounts().account_id(public_key).call() +for balance in account['balances']: + print(f"Type: {balance['asset_type']}, Balance: {balance['balance']}") +``` + + + +Now that you’ve got an account, you can [start sending and receiving payments](send-and-receive-payments.mdx), or, if you're ready to hunker down, you can skip ahead and [build a wallet](../building-apps/index.mdx) or [issue a Stellar-network asset](../issuing-assets/index.mdx). + + + +In the above code samples, proper error checking is omitted for brevity. However, you should _always_ validate your results, as there are many ways that requests can fail. You should refer to the guide on [Handling Errors Gracefully](../tutorials/handling-errors.mdx) for tips on error management strategies. + + diff --git a/docs/tutorials/follow-received-payments.mdx b/docs/tutorials/follow-received-payments.mdx new file mode 100644 index 000000000..3386b7ee8 --- /dev/null +++ b/docs/tutorials/follow-received-payments.mdx @@ -0,0 +1,266 @@ +--- +title: Follow Received Payments +order: 30 +--- + +import { CodeExample } from "@site/src/components/CodeExample"; + +This tutorial shows how easy it is to use Horizon to watch for incoming payments on an [account](../glossary/accounts.mdx) using JavaScript and `EventSource`. We will eschew using [`js-stellar-sdk`](https://github.com/stellar/js-stellar-sdk), the high-level helper library, to show that it is possible for you to perform this task on your own with whatever programming language you would like to use. + +This tutorial assumes that you: + +- Have node.js installed locally on your machine. +- Have curl installed locally on your machine. +- Are running on Linux, macOS, or any other system that has access to a bash-like shell. +- Are familiar with launching and running commands in a terminal. + +In this tutorial we will learn: + +- How to create a new account. +- How to fund your account using friendbot. +- How to follow payments to your account using curl and EventSource. + +## Project Skeleton + +Let's get started by building our project skeleton: + + + +```bash +$ mkdir follow_tutorial +$ cd follow_tutorial +$ npm install --save stellar-base +$ npm install --save eventsource +``` + + + +This should have created a `package.json` in the `follow_tutorial` directory. You can check that everything went well by running the following command: + + + +```bash +$ node -e "require('stellar-base')" +``` + + + +Everything was successful if no output was generated from the above command. Now let's write a script to create a new account. + +## Creating an account + +Create a new file named `make_account.js` and paste the following text into it: + + + +```javascript +var Keypair = require("stellar-base").Keypair; + +var newAccount = Keypair.random(); + +console.log("New key pair created!"); +console.log(" Account ID: " + newAccount.publicKey()); +console.log(" Secret: " + newAccount.secret()); +``` + + + +Save the file and run it: + + + +```bash +$ node make_account.js +New key pair created! + Account ID: GB7JFK56QXQ4DVJRNPDBXABNG3IVKIXWWJJRJICHRU22Z5R5PI65GAK3 + Secret: SCU36VV2OYTUMDSSU4EIVX4UUHY3XC7N44VL4IJ26IOG6HVNC7DY5UJO +$ +``` + + + +Before our account can do anything it must be funded. Indeed, before an account is funded it does not truly exist! + +## Funding your account + +The Stellar test network provides the Friendbot, a tool that developers can use to get testnet lumens for testing purposes. To fund your account, simply execute the following curl command: + + + +```bash +$ curl "https://friendbot.stellar.org/?addr=GB7JFK56QXQ4DVJRNPDBXABNG3IVKIXWWJJRJICHRU22Z5R5PI65GAK3" +``` + + + +Don't forget to replace the account id above with your own. If the request succeeds, you should see a response like: + + + +```json +{ + "hash": "ed9e96e136915103f5d8978cbb2036628e811f2c59c4c3d88534444cf504e360", + "result": "received", + "submission_result": "000000000000000a0000000000000001000000000000000000000000" +} +``` + + + +After a few seconds, the Stellar network will perform consensus, close the ledger, and your account will have been created. Next up we will write a command that watches for new payments to your account and outputs a message to the terminal. + +## Following payments using `curl` + +To follow new payments connected to your account you simply need to send the `Accept: text/event-stream` header to the [/payments](/api/resources/operations/object/payment/) endpoint. + + + +```bash +$ curl -H "Accept: text/event-stream" "https://horizon-testnet.stellar.org/accounts/GB7JFK56QXQ4DVJRNPDBXABNG3IVKIXWWJJRJICHRU22Z5R5PI65GAK3/payments" +``` + + + +As a result you will see something like: + + + +```bash +retry: 1000 +event: open +data: "hello" + +id: 713226564145153 +data: {"_links":{"effects":{"href":"/operations/713226564145153/effects/{?cursor,limit,order}","templated":true}, + "precedes":{"href":"/operations?cursor=713226564145153\u0026order=asc"}, + "self":{"href":"/operations/713226564145153"}, + "succeeds":{"href":"/operations?cursor=713226564145153\u0026order=desc"}, + "transactions":{"href":"/transactions/713226564145152"}}, + "account":"GB7JFK56QXQ4DVJRNPDBXABNG3IVKIXWWJJRJICHRU22Z5R5PI65GAK3", + "funder":"GBS43BF24ENNS3KPACUZVKK2VYPOZVBQO2CISGZ777RYGOPYC2FT6S3K", + "id":713226564145153, + "paging_token":"713226564145153", + "starting_balance":"10000", + "type_i":0, + "type":"create_account"} +``` + + + +Every time you receive a new payment you will get a new row of data. Payments is not the only endpoint that supports streaming. You can also stream transactions [/transactions](/api/resources/transactions/) and operations [/operations](/api/resources/operations/). + +## Following payments using `EventStream` + +> **Warning!** `EventSource` object does not reconnect for certain error types so it can stop working. If you need a reliable streaming connection please use our [SDK](https://github.com/stellar/js-stellar-sdk). + +Another way to follow payments is writing a simple JS script that will stream payments and print them to console. Create `stream_payments.js` file and paste the following code into it: + + + +```js +var EventSource = require("eventsource"); +var es = new EventSource( + "https://horizon-testnet.stellar.org/accounts/GB7JFK56QXQ4DVJRNPDBXABNG3IVKIXWWJJRJICHRU22Z5R5PI65GAK3/payments", +); +es.onmessage = function (message) { + var result = message.data ? JSON.parse(message.data) : message; + console.log("New payment:"); + console.log(result); +}; +es.onerror = function (error) { + console.log("An error occurred!"); +}; +``` + + + +Now, run our script: `node stream_payments.js`. You should see following output: + + + +```bash +New payment: +{ _links: + { effects: + { href: '/operations/713226564145153/effects/{?cursor,limit,order}', + templated: true }, + precedes: { href: '/operations?cursor=713226564145153&order=asc' }, + self: { href: '/operations/713226564145153' }, + succeeds: { href: '/operations?cursor=713226564145153&order=desc' }, + transactions: { href: '/transactions/713226564145152' } }, + account: 'GB7JFK56QXQ4DVJRNPDBXABNG3IVKIXWWJJRJICHRU22Z5R5PI65GAK3', + funder: 'GBS43BF24ENNS3KPACUZVKK2VYPOZVBQO2CISGZ777RYGOPYC2FT6S3K', + id: 713226564145153, + paging_token: '713226564145153', + starting_balance: '10000', + type_i: 0, + type: 'create_account' } +``` + + + +## Testing it out + +We now know how to get a stream of transactions to an account. Let's check if our solution actually works and if new payments appear. Let's watch as we send a payment ([`create_account` operation](../start/list-of-operations.mdx#create-account)) from our account to another account. + +We use the `create_account` operation because we are sending payment to a new, unfunded account. If we were sending payment to an account that is already funded, we would use the [`payment` operation](../start/list-of-operations.mdx#payment). + +First, let's check our account sequence number so we can create a payment transaction. To do this we send a request to horizon: + + + +```bash +$ curl "https://horizon-testnet.stellar.org/accounts/GB7JFK56QXQ4DVJRNPDBXABNG3IVKIXWWJJRJICHRU22Z5R5PI65GAK3" +``` + + + +Sequence number can be found under the `sequence` field. For our example, the current sequence number is `713226564141056`. Save your value somewhere. + +Now, create `make_payment.js` file and paste the following code into it, replacing the sequence number accordingly: + + + +```js +var StellarBase = require("stellar-base"); +var StellarSdk = require("stellar-sdk"); + +var keypair = StellarBase.Keypair.fromSecret( + "SCU36VV2OYTUMDSSU4EIVX4UUHY3XC7N44VL4IJ26IOG6HVNC7DY5UJO", +); +var account = new StellarBase.Account(keypair.publicKey(), "713226564141056"); + +var amount = "100"; +var transaction = new StellarSdk.TransactionBuilder(account, { + networkPassphrase: StellarBase.Networks.TESTNET, + fee: StellarSdk.BASE_FEE, + }) + .addOperation( + StellarBase.Operation.createAccount({ + destination: StellarBase.Keypair.random().publicKey(), + startingBalance: amount, + }), + ) + .setTimeout(180) + .build(); + +transaction.sign(keypair); + +console.log(transaction.toEnvelope().toXDR().toString("base64")); +``` + + + +After running this script you should see a signed transaction blob. To submit this transaction we send it to Horizon or Stellar-Core. But before we do, let's open a new console and start our previous script by `node stream_payments.js`. + +Now to send a transaction just use Horizon: + + + +```bash +curl -H "Content-Type: application/json" -X POST -d '{"tx":"AAAAAgAAAAB+kqu+heHB1TFrxhuALTbRVSL2slMUoEeNNaz2PXo90wAAAGQAAoitAAAAAQAAAAEAAAAAAAAAAAAAAABgJHaDAAAAAAAAAAEAAAAAAAAAAAAAAAByS4gefO1iu/ZfYlr+PMA2AZsHJmSK/4NActJ1Oa1BIgAAAAA7msoAAAAAAAAAAAE9ej3TAAAAQPo1YHJMpdWKatEQxj7DqP1rrR6pA+OjK9q3WcU/sBwvKk6GhpdwA3gkUDrkREU0cFQSNKwugNFkGkR0zFmROgw="}' "https://horizon-testnet.stellar.org/transactions" +``` + + + +You should see a new payment in a window running `stream_payments.js` script. diff --git a/docs/tutorials/handling-errors.mdx b/docs/tutorials/handling-errors.mdx new file mode 100644 index 000000000..3c18b4aa3 --- /dev/null +++ b/docs/tutorials/handling-errors.mdx @@ -0,0 +1,247 @@ +--- +title: Handling Errors Gracefully +order: 100 +--- + +import { CodeExample } from "@site/src/components/CodeExample"; + +When working with a payment network like Stellar[,](https://www.youtube.com/watch/TLXo9IFVboY) expecting the unexpected is critical in ensuring a good user experience: one in which money doesn't get lost, actions happen when they're supposed to (or as early as they can), and everyone's view of the network is as accurate as possible. + +In many of the tutorials and code samples throughout this documentation, we've minimized the error handling code to reduce verbosity and focus on the essence of the examples. Here, we'll do the exact opposite, because error handling _is_ the essence: by the end, you should be able to categorize errors and understand the best, most idiomatic way to handle them in your application. + +This document is broken down into two main sections. In the [first section](#resolution-strategies), we cover the recommended resolution strategies that apply to most error scenarios developers may encounter. Then, in the [second section](#managing-specific-errors), we dive deeper into the errors themselves; refer to this latter section if you have encountered a specific error and want a broader understanding of its causes. + +## Resolution Strategies + +Despite the fact that there are many ways to interact with the Stellar network through the Horizon API, the possible actions fall into two main categories: **queries** (any `GET` request, like to `/accounts`) and **transaction submissions** (a `POST /transactions`). Though there are a myriad of possible error codes (again, we break some down [later](#managing-specific-errors)) when executing these actions, they can be handled through a few primary strategies: + +- **Adjusting the request** to resolve structural errors with queries or transaction submissions is the first line of defense: if you've included a bad parameter, malformed your XDR, or otherwise didn't follow the endpoint's specification, the error can be resolved by referencing the details or result codes of the error response. +- **Retrying until success** is the recommended way to work around latency or congestion issues encountered anywhere along the pipeline between your machine and the Stellar network. This ephemeral scenario is [unavoidable](https://en.wikipedia.org/wiki/High_availability#%22Nines%22) due to the very nature of a distributed system. +- **Adjusting the transaction** can also resolve issues but it should only be done with _extreme_ care: if one of the above scenarios is in effect, it's possible to trigger destructive duplicate actions (like sending a payment twice). + +Let's dive into these strategies in detail. The main scenario we'll focus on is transaction submission, since it's an action with meaningful side-effects rather than a read-only request. + +### Request Adjustments + +> We cannot direct the wind, but we can adjust the sails. + +Some errors cannot be overcome without changes to the request itself. + +#### Queries + +Many of the `GET` requests have specific parameter requirements, and while the [SDKs](../software-and-sdks/) can help enforce them, you can still pass invalid arguments (e.g. an asset string that isn't [SEP-11 conformant](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0011.md#asset)) that error out every time. In this scenario, there's nothing you can do aside from following the [API specification](/api) precisely. The `extras` field of the error response will often clue you in on where to look and what to look for: + + + +```bash +curl -s https://horizon-testnet.stellar.org/claimable_balances/0000 | jq '.extras' +{ + "invalid_field": "id", + "reason": "Invalid claimable balance ID" +} +``` + + + +Note that the SDKs make it a point to distinguish an **invalid request** (as above) vs. a **missing resource** (a `404 Not Found`) (e.g. the generic `NetworkError` vs. a `NotFoundError` in the JavaScript SDK), where the latter might not be considered an error depending on your situation. + +#### Transaction Submissions + +Certain transaction submission failures also need adjustments to succeed. If the XDR is malformed or the transaction is somehow otherwise invalid, you'll encounter a `400 Bad Request` (for example, when excluding a source account as seen in the [API reference example](/api/errors/response/)). Both transactions and their requisite operations can easily be malformed: look at the `extras.result_codes` field for details and cross-reference them with the appropriate [Result Codes documentation](/api/errors/result-codes/) to determine specifics. + +Another class of safe adjustments involves transaction fees: if you get a `tx_insufficient_fee` error, it's worth reading the [later section](#insufficient-fees-and-surge-pricing) to adjust your fee-paying strategy. + +### Retrying Until Success + +> If at first you don't succeed,\ +> Try, try again. + +The old adage rings true in this case. There are many possible scenarios (for example: `504 Timeout`s, transient outages, congestion on the Stellar network) in which retrying your transaction submission is the only viable fallback. This should be considered the last line of defense, though; often-times, the error can be rectified by a [safe modification](#transaction-submissions) to the transaction. + +Since there's no mechanism to "cancel" a transaction after it has been submitted, the first key to successful retries is leveraging [timebounds](https://developers.stellar.org/docs/glossary/transactions/#time-bounds): though optional, timebounds allow you to introduce a bit of determinism into an innately non-deterministic system. If the timebound has been exceeded, the transaction has a definitive, final state: either it made it into a ledger or it timed out. The second key comes from a certain Horizon guarantee: **you can safely retry submission as long as you don't modify the transaction**. Specifically, + +> If the transaction has already been successfully applied to the ledger, Horizon will simply return the saved result and not attempt to submit the transaction again. Only in cases where a transaction’s status is unknown (and thus will have a chance of being included into a ledger) will a resubmission to the network occur. + +_(The above excerpt is from [this example](https://developers.stellar.org/docs/tutorials/send-and-receive-payments/#send-a-payment).)_ + +In other words, if your transaction makes it through at any point in time, you'll get your previously submitted transaction back for all subsequent retries. + +#### Example Scenario + +Suppose you submit a transaction and it enters the queue of the Stellar network, but Horizon crashes while giving you a response. Uncertain about the transaction status, you resubmit (with no changes!) until either **(a)** Horizon comes back up to give you a reply or **(b)** your timebounds are exceeded. There are only two possible results: the transaction makes it into a ledger (exactly once!) and Horizon gives you the response, or the transaction never makes it out of the queue and you receive the corresponding `tx_too_late` response. + +#### Example Implementation + + + +```js +let server = sdk.Server("horizon.stellar.org"); + +function submitTransaction(tx, timeout) { + if (!tx.timeBounds || tx.timeBounds.maxTime === 0) { + throw new Error("Always set a reasonable timebound!"); + } + const expiration = parseInt(tx.timeBounds.maxTime); + + return server.submitTransaction(tx).catch(function (error) { + if (isNonRetryErrorCase(error)) { + // ...do other error handling... + return; + } + + // the tx no longer has a chance of making it into a ledger + if (Date.now() >= expiration) { + return new Error("The transaction timed out."); + } + + timeout = timeout || 1; // start the (linear) back-off process + return sleep(timeout).then(function () { + return submitTransaction(tx, timeout + 5); + }); + }); +} +``` + + + +_(We assume the existence of a `sleep` implementation akin to the one [here](https://stackoverflow.com/a/39914235).)_ + +#### Details: Retry Backoff + +Be sure to integrate backoff into your retry mechanism. In our example error-handling code above, we implement a simple linear backoff, but there are [plenty of recommendations](https://backoff-utils.readthedocs.io/en/latest/strategies.html#why-are-backoff-strategies-useful) out there for various other strategies you can employ. Backoff is important both for maintaining performance and avoiding [rate-limiting](#rate-limiting) issues. + +### Unsafe Transaction Adjustments + +As outlined in the section on [retries](#retrying-until-success), resubmitting an _unchanged_ (and valid) transaction (with the same operations, signatures, sequence number, etc.) is always safe to do. You should be careful when working around an error that _does_ require changes to the transaction, though: it's very possible to cause **duplication transactions** which can result in all sorts of problems (double-payments, erroneous trustlines, etc.). + +#### Example: Invalid Sequence Numbers + +These errors typically occur when you have an outdated view of an account due to other transactions happening outside of your worldview. This could be because the account is used on multiple devices, you have concurrent submissions happening, or a number of other reasons. Thankfully, the solution is **usually** relatively simple: retrieve the account details and try again with an updated sequence number. + + + +```js +// suppose `account` is an outdated `AccountResponse` object +let tx = sdk.TransactionBuilder(account, ...)/* etc */.build() +server.submitTransaction(tx).catch(function (error)) { + if (error.response && error.status == 400 && error.extras && + error.extras.result_codes.transaction == sdk.TX_BAD_SEQ) { + return server.accounts() + .accountId(account.accountId()) + .then(function (response) { + let tx = sdk.TransactionBuilder(response, ...)/* etc */.build() + return server.submitTransaction(tx); + }); + } + // ...other error conditions... +} +``` + + + +Despite the solution's simplicity, things can go very wrong very fast if you don't understand _why_ the error occurred. + +Suppose you submit transactions from multiple places in your application concurrently, and your user spammed the "Send Payment" button a few times in their impatience. If you try to send the _exact same_ payment transaction for each tap, naturally only one will succeed. The others will fail with an invalid sequence number (`tx_bad_seq`), and if you resubmit blindly with an updated sequence number (as we do above), these payments will **also succeed**, ultimately resulting in more than one payment being made when only one was intended. + +In essence, **be very careful with resubmitting transactions that have been modified** to work around an error. + +## Managing Specific Errors + +This section covers a smattering of specific error cases commonly encountered during transaction submission. Obviously it's impossible to be exhaustive in this list, but we try to explain why certain situations occur and direct you to the appropriate resolution strategy. + +### Timeouts + +If you receive a `504 Timeout` from Horizon after a transaction submission, you've encountered something a little nebulous and non-traditional in the Stellar universe: timeouts are more of a _warning_ that your request hasn't been fulfilled within a reasonable amount of time _yet_ rather than an error. This subtlety arises because of the nature of the relationship between Horizon and Stellar Core: the network might take some time to accept a transaction—especially one with a low fee, see the discussion of [Surge Pricing](#insufficient-fees-and-surge-pricing) later—on the order of 5-10 minutes during congestion, whereas Horizon needs to provide developers with a response within a reasonable 30 seconds or so. + +This leads to a very important point: **receiving a 504 for your transaction submission does not mean the transaction did not make it to the network**. You should continue with [retries](#retrying-until-success) until getting a more definitive response. + +If you _continue_ to face timeouts on retries, you may want to consider using a [fee-bump transaction](#bumping-fees-on-past-transactions) to get into the ledger (after timebounds expire, of course) or increasing the maximum fee you're willing to pay. Read on about [surge pricing](#insufficient-fees-and-surge-pricing) for more details. + +### Insufficient Fees and Surge Pricing + +When the Stellar network undergoes bursts of activity, [surge pricing](https://developers.stellar.org/docs/glossary/fees/#surge-pricing) might kick in. This and other fee fluctuations can cause unexpected errors during transaction submission in applications that don't plan ahead for its dynamic nature. A recent [blog post](https://www.stellar.org/developers-blog/transaction-submission-timeouts-and-dynamic-fees-faq) by the SDF discusses fee surges and other frequently asked questions in much greater detail. + +There are two main approaches for dealing with this variance: + +1. **Track fee fluctuations** via the [`fee_stats` endpoint](https://developers.stellar.org/api/aggregations/fee-stats/). This can let you make informed, specific choices about the fee you're comfortable paying. Alternatively, simply... + +1. **Set the highest fee** you are comfortable with. Crucially, you should remember that _this doesn't mean you'll **pay** that on every transaction_. You will only pay whatever is necessary to get you into the ledger: under normal (non-surge) circumstances, even with a higher _maximum_ fee set, you will pay the _standard_ fee (100 stroops as of this writing). + +The latter strategy balances simplicity, efficacy, and convenience, but unless you set your maximum high enough so that it's _never_ exceeded, it can still lead to failures. The former strategy can provide more reliable submissions by allowing tighter guarantees about whether or not a transaction will be accepted. In general, though, it's important to track fee costs: if the network saturates beyond your maximum willingness to pay, perhaps waiting for activity to die down or periodically retrying with the same fee is the best approach for your use case. + +If you want to match a fee error **exactly**, you might write something like this: + + + +```js +function isFeeError(error) { + return + error.response !== undefined && + error.status == 400 && + error.extras && + error.extras.result_codes.transaction == sdk.TX_INSUFFICIENT_FEE; +} +``` + + + +Of course, there are much more streamlined ways to _combine_ errors together, but this will be used below to demonstrate the (very) specific check. + +#### Example: Paying 10% above average + +Suppose we want a fairly conservative fee-paying strategy: we're only willing to pay a 10% higher fee than the average transaction paid. + + + +```js +// when submitting any transaction, first query fee stats +server.feeStats().then(function (response) { + let avgFee = parseFloat(response.fee_charged.p50); + let tx = base.TransactionBuilder(someAccount, { + fee: (avgFee * 1.10).toFixed(0), // bump & convert to int + networkPassphrase: // ... + }); + // ...build the rest of the tx... + tx.sign(someAccount); + return server.submitTransaction(tx); +}); +``` + + + +#### Bumping fees on past transactions + +It's possible that even with a liberal fee-paying policy, your transaction fails to make it into the ledger due to insufficient funds or untimely surges. Resolving this problem is exactly the goal of a [fee-bump transaction](/docs/glossary/fee-bumps/). The following snippet shows how you can resubmit a transaction with a higher fee given that you have the original [transaction envelope](../glossary/transactions/#transaction-envelopes): + + + +```js +// Let `lastTx` be some transaction that fails submission due to high fees, and +// `lastFee` be the maximum fee (expressed as an int) willing to be paid by +// `account` for `lastTx`. +server.submitTransaction(lastTx).catch(function (error) { + if (isFeeError(error)) { + let bump = sdk.TransactionBuilder.buildFeeBumpTransaction( + account, // account that will PAY the new fee + lastFee * 10, // new fee + lastTx, // the (entire) failing transaction + server.networkPassphrase + ); + bump.sign(someAccount); + return server.submitTransaction(bump); + } + // ...other error conditions... +}).then(...); +``` + + + +Note an [important stipulation](https://developers.stellar.org/docs/glossary/fee-bumps/#replace-by-fee) of fee bumping that's fulfilled above: + +> If you submit two distinct transactions with the same source account and sequence number, and the second transaction is a fee-bump transaction, the second transaction will be included in the transaction queue in place of the first transaction if and only if **the fee bid of the second transaction is at least 10x the fee bid of the first transaction**. + +This value can typically be found in the `fee_charged` field of the [transaction response](/api/resources/transactions/object/) under the `tx_insufficient_fee` error case. + +### Rate Limiting + +If you're using the SDF's public Horizon instance, you may get a `429 Too Many Requests` error when exceeding the [rate limits](/api/introduction/rate-limiting/). If you're encountering this frequently, it may be time to [deploy your own](../run-api-server/) Horizon instance! diff --git a/docs/tutorials/metadata.json b/docs/tutorials/metadata.json new file mode 100644 index 000000000..d855de20d --- /dev/null +++ b/docs/tutorials/metadata.json @@ -0,0 +1,4 @@ +{ + "order": 20, + "title": "Tutorials" +} diff --git a/docs/tutorials/moneygram-access-integration-guide.mdx b/docs/tutorials/moneygram-access-integration-guide.mdx new file mode 100644 index 000000000..43f166794 --- /dev/null +++ b/docs/tutorials/moneygram-access-integration-guide.mdx @@ -0,0 +1,412 @@ +--- +title: Integrate with MoneyGram Access +order: 140 +--- + +import { CodeExample } from "@site/src/components/CodeExample"; +import { Alert } from "@site/src/components/Alert"; + +This document guides the reader through the technical requirements for integrating [MoneyGram Access] into an existing application. MoneyGram Access is a MoneyGram product that enables users of third-party applications, such as crypto wallets and exchanges, to cash-in (deposit) and cash-out (withdrawal) of Stellar USDC. + +MoneyGram requires businesses to go through an onboarding process in order to get access to their testing and production environments. To get started with this process, reach out to partnerships@stellar.org. + +## Resources + +- [MoneyGram Access Wallet MVP Implementation] + - Use this MVP implementation as a reference for building your own integration. Many of the code snippets shared in this document are pulled from this project. +- [Stellar Test Anchor] + - Before getting access to MoneyGram's test environment, you can use the SDF's test anchor while developing your integration +- [Stellar Demo Wallet] + - This application visualizes the API calls necessary to connect to a Stellar Anchor +- [Stellar Ecosystem Proposal 24 (SEP-24)][sep-24] + - The standardized API protocol for Stellar on & off ramps, implemented by MoneyGram +- [Stellar Ecosystem Proposal 10 (SEP-10)][sep-10] + - The standardized API protocol for Stellar authentication, implemented by MoneyGram + +## Asset Information + +Before you get access to MoneyGram's test environment, you should test your implementation with the SDF's [Stellar Test Anchor]. It implements the same APIs as MoneyGram's service but uses a different asset. The information for each asset is below. + +### Stellar Reference Token + +This token is only on testnet. + +- **Issuing Account**: [GCDNJUBQSX7AJWLJACMJ7I4BC3Z47BQUTMHEICZLE6MU4KQBRYG5JY6B] +- **Asset Code**: SRT + +### USD Coin + +Testnet: + +- **Issuing Account**: [GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5] +- **Asset Code**: USDC + +Pubnet: + +- **Issuing Account**: [GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN] +- **Asset Code**: USDC + +## Introduction + +Applications seeking to integrate MoneyGram Access must implement the client side of [Stellar Ecosystem Proposal 24 (SEP-24)][sep-24], a standardized protocol defined for applications to connect to businesses such as MoneyGram, more generally called anchors, that offer Stellar deposit & withdrawal services utilizing local payment rails. + +This document will walk you through the necessary steps to develop a functional implementation of this standard. + +The guide will assume your application is first being developed on Stellar’s test network and using MoneyGram’s testing deployment of Access, but there are no functional differences deploying the application to Stellar’s public network and using MoneyGram’s production deployment. + +This guide also assumes your application is custodial, meaning the application has either direct or indirect access to its users’ funds on Stellar. Typically, custodial applications pool user funds into a smaller set of managed Stellar accounts, called pooled, shared, or omnibus accounts. This custody model requires minor but concrete differences in how applications integrate with MoneyGram Access compared to non-custodial applications. + +## Application Flow & Architecture + +This guide will assume the application has a basic client-server architecture. The application’s client will request resources and initiate actions with the application’s server, which will communicate directly with MoneyGram’s server. + +Below are the 7 high-level steps to take to facilitate a cash-out (withdrawal) transaction. + +![Wallet - MoneyGram Page 1](../web-assets/wallet-mgi-architecture-1.png) + +After Step 4, the application should open the URL provided by MoneyGram in a mobile webview or browser tab. MoneyGram will then prompt the user to provide KYC and transaction information. On completion of this flow, the application’s client should close the MoneyGram tab or webview and initiate the disbursement of funds. + +![Wallet - MoneyGram Page 2](../web-assets/wallet-mgi-architecture-2.png) + +The provided reference number would then be taken to any MoneyGram cash agent in order to receive cash in the user’s fiat currency. These steps document the cash-out, or withdrawal flow. The deposit flow is similar and detailed in the steps below. + +## Generate Stellar Keypairs + +In this section, you will generate at least two Stellar keypairs, one that will be used to prove your application’s identity when authenticating with MoneyGram Access, and another that will hold, send, & receive USDC on Stellar. You should always use one keypair for authentication, but application could use many keypairs for sending & receiving payments. In this guide, we'll assume the application uses one keypair for each purpose. + +This section assumes that your application does not have any support for the Stellar network. If your application already supports deposits & withdrawals of XLM, you already have one or more Stellar accounts that can be used for these purposes, although it is heavily encouraged to use a new keypair for authentication. + +Go to [Stellar Lab] and generate 2 keypairs. The secret keys should be handled securely, because they will be used to authenticate with and disburse funds to MoneyGram. + +The first keypair will be called the “authentication” keypair (or public / secret key). The second keypair will be the “funds” keypair (or account, public key, or secret key). Unlike the authentication keypair, the funds keypair will reference a funded account on the Stellar network. The authentication keypair does not need to funded. + +Provide the public keys (starting with a G) of both the authentication and funds keypairs to MoneyGram. They will add these keys to their known lists of keys, granting them access to their deployment. + +## Get XLM & USDC + +Many cryptocurrency exchanges support purchasing XLM or USDC on Stellar. The SDF also maintains an [Anchor Directory] that attempts to list all the on & off-ramps for the Stellar Network. + +When you’ve purchased XLM and / or USDC on an exchange, you can make a payment to an external account, specifically to the funds public key you generated in the previous step. Note that you will first need to send XLM to create the account, then add a USDC [trustline], then send the USDC. Creating a trustline to USDC can be done using [Stellar Lab] or any Stellar-enabled wallet application, such as [Lobstr]. + +Some exchanges support XLM but do not support USDC on Stellar. This is not a problem, because you can always sell XLM for USDC on Stellar’s decentralized exchange (or SDEX). + +To do this, send your XLM to the funds public key from the exchange, add a USDC trustline, and sell XLM for USDC using a [sell offer]. + +## Authenticate + +This section emcompasses steps 1 & 2 of the diagram displayed in the Architecture section above. The application’s client should request a MoneyGram transaction URL from the application’s server on user initiation. This should trigger an authentication process between the application’s server and MoneyGram’s server. This process is standardized in [SEP-10][sep-10]. + +This section assumes that the application’s server has the following pieces of information: + +- The user’s integer ID (must be positive and represented using 64 bits or less) MoneyGram’s authentication endpoint + - Testing: https://extstellar.moneygram.com/stellaradapterservice/auth + - Production: https://stellar.moneygram.com/stellaradapterservice/auth +- MoneyGram’s authentication public key + - Testing: `GCSESAP5ILVM6CWIEGK2SDOCQU7PHVFYYT7JNKRDAQNVQWKD5YEE5ZJ4` + - Production: `GD5NUMEX7LYHXGXCAD4PGW7JDMOUY2DKRGY5XZHJS5IONVHDKCJYGVCL` +- The application’s authentication public and secret key + +The flow can be described with the following steps: + +1. The application requests an authentication challenge +1. The server (MoneyGram) provides the authentication challenge +1. The application verifies that MoneyGram signed the authentication with it's SIGNING_KEY +1. The application signs the authentication challenge with its own key +1. The application sends the authentication challenge back to the server +1. The server verifies the application signed the challenge with the account it initially used to request the challenge +1. The server returns a session token for the account & memo used in the initial authentication request + +The following code demonstrates how to implement the application’s side of this flow. Note that this code does not handle retries in the event of network connection issues. It also does not handle unexpected status codes, and does not include logging or metrics. + + + +```python +import requests +from stellar_sdk import Network +from stellar_sdk.sep.stellar_web_authentication import read_challenge_transaction + +def get_token() -> str: + query = f"{AUTH_URL}?account={AUTH_PUBLIC_KEY}&memo={USER_ID}" + response = requests.get(query) + body = response.json() + challenge = read_challenge_transaction( + challenge_transaction=body["transaction"], + server_account_id=MGI_ACCESS_SIGNING_KEY, + home_domains=MGI_ACCESS_HOST, + web_auth_domain=MGI_ACCESS_HOST, + network_passphrase=Network.TESTNET_NETWORK_PASSPHRASE + ) + challenge.transaction.sign(AUTH_SECRET_KEY) + post_body = { + "transaction": challenge.transaction.to_xdr() + } + response = requests.post(f"{AUTH_URL}", json=post_body) + response_body = response.json() + return response_body["token"] +``` + + + +## Initiate a Transaction + +This section encompasses steps 3 & 4 of the architecture diagram displayed above. The application’s server will make a deposit or withdrawal initiation request to MoneyGram’s server, and MoneyGram will return a transaction ID, which will be used later to poll the transaction’s status, and a transaction URL, which should be returned to the application’s client and opened for the user. + +For the purpose of this guide, we will go through the withdrawal case. + +You will need the following pieces of information: + +- The authentication token provided by MoneyGram. This token can only be used for actions associated with the user identified by the ID used in the previous steps. +- The public key of the keypair the application will use to send funds +- The language code MoneyGram should render their UI’s content with +- The amount the user would like to withdraw / cash-out + - This should be collected from the user prior to initiating this transaction + +The following code can be used as a reference for implementing this logic yourself. This code is not necessarily production-ready. + + + +```python +import requests + +def initiate_withdraw(token: str, amount: str) -> Tuple[str, str]: + post_body = { + "asset_code": ASSET_CODE, # USDC + "account": FUNDS_STELLAR_KEYPAIR.public_key, + "lang": "en", + "amount": amount + } + response = requests.post( + MGI_ACCESS_WITHDRAW_URL, + json=post_body, + headers={ + "Authorization": f"Bearer {token}" + } + ) + body = response.json() + return body["url"] + "&callback=postmessage", body["id"] +``` + + + +The logic for initiating a deposit transaction looks very similar. See the [SEP-24][sep-24] standard specification for detailed information. MoneyGram requires the `amount` field in both cases. + +The `&callback=postmessage` query parameter added to the returned URL is critical; it informs MoneyGram that the application’s client would like to be notified when the user has completed the MGI experience and has requested to close the window. We’ll cover this in more detail in the subsequent section. + +## Listen for the Close Notification + +The next step is to open the provided URL in the application’s client using a mobile webview, browser tab, or popup. The user will then go through KYC if they have not before in a prior transaction. In the deposit case, the user may also select a MoneyGram agent location to go to when providing cash. + +Finally, when the user is done with the MoneyGram UI, the user will select a button displayed on MoneyGram’s UI and MoneyGram will send a [postMessage] to the window or app that opened its flow initially. The message sent will be the SEP-24 transaction JSON object that represents the transaction. + +Below is a simple JavaScript example listening for a postmessage notification. + + + +```javascript +webview = window.open(moneygramURL, "webview", "width=500,height=800"); +window.addEventListener("message", closeWebView); + +function closeWebView(e) { + webview.close(); + const txJson = e.data.transaction; + console.log(`Transaction ${txJson.id} is ${txJson.status}`); +} +``` + + + +## Send or Receive Funds + +In withdrawal (or cash-out) transactions, applications must send USDC to the Stellar account MoneyGram specifies. In deposit (cash-in) transactions, applications must monitor their Stellar account for a payment from MoneyGram. + +In each case, the transaction submitted to Stellar must have a memo attached to it. This memo is provided by MoneyGram in the withdrawal case, and provided by the application in the deposit case. The memo is an identifier that allows the parties to tie the on-chain payment to the transaction record in the application’s or MoneyGram’s database. + +### Poll Until MoneyGram is Ready + +Before the application can send funds or instruct the user to provide cash to a MoneyGram agent, the application should confirm with MoneyGram’s server that the transaction is ready to proceed. + +You will need the following information to do so. + +- The authentication token provided by MoneyGram +- The transaction’s ID provided by MoneyGram +- MoneyGram’s transaction’s endpoint + - Testing: https://extstellar.moneygram.com/stellaradapterservice/sep24/transaction + - Production: https://stellar.moneygram.com/stellaradapterservice/sep24/transaction + +This code uses a simple polling mechanism with no bail-out condition. The application’s code should be more robust. + + + +```python +import requests + +response_body = poll_transaction_until_status( + transaction_id, + token=token, + until_status="pending_user_transfer_start" +) + +def poll_transaction_until_status( + txid: str, + token: str, + until_status: str +) -> dict: + first_iteration = True + response_body = None + status = None + while status != until_status: + if first_iteration: + first_iteration = False + else: + time.sleep(1) + query = f"{MGI_ACCESS_TRANSACTION_URL}?id={txid}" + response = requests.get( + query, + headers={ + "Authorization": f"Bearer {token}" + } + ) + response_body = response.json() + status = response_body["transaction"]["status"] + return response_body +``` + + + +### Sending Funds + +Once MoneyGram is ready to receive funds, your application should extract the Stellar account and memo to use in the payment transaction, construct a Stellar transaction, and submit it to the Stellar network. You’ll need: + +- A copy of MoneyGram’s transaction object +- The application’s funds public & secret key + +Code for submitting transactions to Stellar should be developed thoughtfully. The SDF has a documentation page dedicated to [submitting transactions and handling errors gracefully]. Here are a few things you need to keep in mind: + +- Offer a high fee. Your fee should be as high as you would offer before deciding the transaction is no longer worth sending. Stellar will only charge you the minimum necessary to be included in the ledger -- you won't be charged the amount you offer unless everyone else is offering the same amount or greater. Otherwise, you’ll pay the smallest fee offered in the set of transactions included in the ledger. +- Set a maximum timebound on the transaction. This ensures that if your transaction is not included in a ledger before the set time, you can reconstruct the transaction with a higher offered fee and submit it again with better chances of inclusion. +- Resubmit the transaction when you get 504 status codes. 504 status codes are just telling you that your transaction is still pending -- not that it has been canceled or that your request was invalid. You should simply make the request again with the same transaction to get a final status (either included or expired). + +Below is highly simplified code for submitting a payment transaction. It does not use timebounds, handle a transaction’s expiration, or handle 504 status codes. + + + +```python +from stellar_sdk import ( + Server, TransactionBuilder, Network, Asset, IdMemo +) + +submit_payment( + destination=response_body["transaction"]["withdraw_anchor_account"], + memo=response_body["transaction"]["withdraw_memo"], + amount=response_body["transaction"]["amount_in"] +) + +def submit_payment(destination: str, memo: str, amount: str): + server = Server() + account = server.load_account(FUNDS_STELLAR_KEYPAIR.public_key) + transaction = TransactionBuilder( + source_account=account, + network_passphrase=Network.TESTNET_NETWORK_PASSPHRASE, + base_fee=10000 # this is 0.001 XLM + ).append_payment_op( + destination=destination, + asset=Asset(ASSET_CODE, ASSET_ISSUER), + amount=amount, + ).add_memo( + IdMemo(int(memo)) + ).build() + transaction.sign(FUNDS_STELLAR_KEYPAIR) + response = server.submit_transaction(transaction) + print(f"Stellar-generated transaction ID: {response['id']}") +``` + + + +### Receiving Funds + +Once MoneyGram is ready for the user to drop off cash at an MGI agent (in deposit or cash-in cases), the application’s server should begin monitoring its Stellar account for an inbound USDC payment sent by MoneyGram. + +The application should have provided a memo for MoneyGram to use when it initiated the deposit. MoneyGram will attach this memo to the transaction used to send the payment to the application, and the application should use this check the memo of transactions involving it’s account to associate the payment back to the user and the specific transaction. + +The best way to monitor payments made to an account is to stream events from Stellar’s payments endpoint. The use of streaming cursors can help ensure you never miss an event, even if your application’s streaming process goes down for a period of time. + +Note that this code does not handle [path payments] or [claimable balances], two slightly different forms of payment. At the time of writing, MoneyGram does not use either of these options, but you may want to add support for them in case they do in the future. + + + +```python +from stellar_sdk import Server +from .queries import get_transaction_by_memo + +def stream_payments(account: str, cursor: str): + s = Server() + payments = s.payments().for_account(account).join("transactions") + for payment in payments.cursor(cursor).stream(): + if ( + payment["type"] != "payment" + or payment["from"] == account + or payment["asset_type"] == "native" + or payment["asset_code"] != ASSET_CODE + or payment["asset_issuer"] != ASSET_ISSUER + ): + continue + transaction = get_transaction_by_memo( + payment["transaction"]["memo"], + payment["transaction"]["memo_type"] + ) # DB query + if not transaction: + continue + print( + f"Payment for deposit transaction {transaction.id} " + "matched with Stellar transaction " + f"{payment['transaction']['id']}" + ) +``` + + + +## Fetch the Reference Number + +For deposit or cash-in transactions, MoneyGram does not provide reference numbers. All the user needs to do is drop off cash at the agent location chosen in the MoneyGram UI earlier in the flow, and the application should complete the transaction when a matching payment is detected on Stellar. + +For withdrawal or cash-out transactions, MoneyGram provides a reference number in their UI and API once MoneyGram detects the application’s payment of USDC on Stellar. Users should be able to use the application’s client interface to view the reference number directly or find the MoneyGram transaction details page and view it there. + +Note that MoneyGram’s transaction details page is protected with a JWT token in the url that expires relatively quickly after being fetched. This means applications must fetch the URL at the time the user requests the page, potentially requiring re-authentication via SEP-10. + + + +```python +from .api import poll_transction_until_status + +response_body = poll_transaction_until_status( + transaction_id, + "pending_user_transfer_complete" +) +tx_dict = response_body["transaction"] +print( + f"Transaction reference number {tx_dict['external_transaction_id']} " + f"also viewable at {tx_dict['more_info_url']}" +) +``` + + + +[moneygram access]: https://stellar.org/moneygram?locale=e +[moneygram access wallet mvp implementation]: https://github.com/stellar/moneygram-access-wallet-mvp +[stellar test anchor]: https://testanchor.stellar.org/.well-known/stellar.toml +[stellar demo wallet]: https://demo-wallet.stellar.org +[sep-24]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0024.md +[sep-10]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md +[stellar lab]: https://laboratory.stellar.org/ +[anchor directory]: https://resources.stellar.org/anchors? +[lobstr]: https://lobstr.co/ +[trustline]: https://developers.stellar.org/api/resources/operations/object/change-trust/ +[sell offer]: https://developers.stellar.org/api/resources/operations/object/sell-offer/ +[postmessage]: https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage +[submitting transactions and handling errors gracefully]: https://developers.stellar.org/docs/tutorials/handling-errors/ +[path payments]: https://developers.stellar.org/api/resources/operations/object/path-payment-strict-receive/ +[claimable balances]: https://developers.stellar.org/api/resources/operations/object/create-claimable-balance/ +[moneygram screening questionnaire]: https://stellarquestionnaire.typeform.com/to/RD1a71wQ +[gcdnjubqsx7ajwljacmj7i4bc3z47bqutmheiczle6mu4kqbryg5jy6b]: https://stellar.expert/explorer/testnet/asset/SRT-GCDNJUBQSX7AJWLJACMJ7I4BC3Z47BQUTMHEICZLE6MU4KQBRYG5JY6B-1 +[gbbd47if6lwk7p7mdevscwr7dpuwv3ny3dtqevfl4nat4aqh3zllfla5]: https://stellar.expert/explorer/testnet/asset/USDC-GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5-1 +[ga5zsejyb37jrc5avcia5mop4rhtm335x2kgx3ihojapp5re34k4kzvn]: https://stellar.expert/explorer/public/asset/USDC-GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN-1 diff --git a/docs/tutorials/securing-projects.mdx b/docs/tutorials/securing-projects.mdx new file mode 100644 index 000000000..98c5ae6a6 --- /dev/null +++ b/docs/tutorials/securing-projects.mdx @@ -0,0 +1,80 @@ +--- +title: Securing Web-based Projects +order: 120 +--- + +It’s critical for any app managing the flow of cryptocurrency to architect their app to follow security best-practices. Cryptocurrency-enabled apps are significant targets for malicious actors in the sense they enable the attacker to realize immediate monetary gain from exploits. The following checklist offers guidance on the most common vulnerabilities. Even if every piece of advice is followed, security is not guaranteed. Web security is constantly evolving, which warrants a certain degree of paranoia. + +# SSL/TLS + +Ensure TLS enabled (Letsencrypt allows you to do this for free). Redirect http to https where necessary. This ensures that Man in the Middle attacks cannot occur, and sensitive data is securely transferred between the client and browser. Learn how to get an SSL certificate for free [here](https://letsencrypt.org/getting-started/). + +_If you don’t have SSL/TLS enabled, stop everything and do this first._ + +# Content Security Policy (CSP) Headers + +CSP headers tell the browser where it can download static resources from. For example, if you astralwallet.io and it requests a JavaScript file from myevilsite.com, your browser would block it unless it was whitelisted with CSP headers. You may read about how to implement CSP headers [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP). + +Most web frameworks have a configuration file or extensions where you may specify your CSP policy, and the headers will be auto-generated for you. For example, see [Helmet](https://www.npmjs.com/package/helmet) for Node.js. + +This would have prevented the [Blackwallet Hack](https://www.ccn.com/yet-another-crypto-wallet-hack-causes-users-lose-400000/). + +# HTTP Strict-Transport-Security Headers + +This is an HTTP header that tells the browser that all future connections to a particular site should use HTTPS. To implement this, add the [header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security) to your website. Some web frameworks have this built in, like [Django](https://docs.djangoproject.com/en/2.0/topics/security/#ssl-https)! + +This would have prevented the [MyEtherWallet DNS hack](https://bitcoinmagazine.com/articles/popular-ether-wallet-mew-hijacked-dns-attack/). + +# Storing sensitive data + +In an ideal world, you don’t have to store much sensitive data. If you must, tread carefully. There are many strategies in storing sensitive data; start by ensuring sensitive data is encrypted using a proven cipher like AES-256, and stored separately from application data and always pick an AEAD mode. Any communication between the application server and secret server should be in a private network and / or authenticated via HMAC. Your cipher strategy will change based on whether you will be sending the ciphertext over the wire multiple times. Finally, back up any encryption keys you may use offline, and store them only in-memory in your app. + +Consult a good cryptographer and read up on best practices. A good place to start is looking into the documentation of your favorite web framework. + +Rolling your own crypto is a bad idea. Always use tried and tested libraries. A good example is [NaCl](https://en.wikipedia.org/wiki/NaCl_\(software\)). + +# Monitoring + +Attackers often need to spend some time exploring your website for unexpected or overlooked behavior. Examining logs defensively can help you catch on to what their trying to achieve and ensure you’re protected. At the least, you can block their IP, or automate blocking based on suspicious behavior you unearth. + +Finally, it is worth setting up error reporting (e.g. [Sentry](https://sentry.io/welcome/)): Oftentimes, people trigger strange bugs when they are trying to hack things. + +# Authentication weaknesses + +If you have logins for users, it is critical that your authentication system is built securely. Of course, the best way to do this is to use something off the shelf. Both Ruby on Rails and Django have robust, built-in authentication schemes. + +Many JSON web token implementations are poorly done, so ensure the library you use is audited. + +Hash passwords with a time-tested scheme. The winner of the last password hashing contest was Argon2. Balloon Hashing is also worth looking into. + +Strongly prefer 2FA, and require U2F or [TOTP](https://tools.ietf.org/html/rfc6238) 2FA for sensitive actions. + +2FA is really important. Email accounts are usually not very secure. Having a second factor of authentication ensures that users who accidentally stay logged on, or have their password guessed are still protected. + +Finally, require strong passwords. Common and short passwords can easily be brute forced. Dropbox has a [great open source tool](https://blogs.dropbox.com/tech/2012/04/zxcvbn-realistic-password-strength-estimation/) that gauges password strength fairly quickly, making it usable for user interactions. + +# Denial of Service Attacks (DOS) + +Denial of service attacks are usually accomplished by overloading your web servers with traffic. To mitigate this risk, rate limit traffic from IPs and browser fingerprints. Sometimes people will use proxies to bypass IP rate-limiting. In the end, malicious actors can always find ways to spoof their identity, so the surest way to block DOS attacks are to implement proof of work checks in your client, or use a managed service like [Cloudflare](https://www.cloudflare.com/ddos/). + +# Lock down unused ports + +Attackers will often scan your ports, and see if you were negligent and left any open. Services like Heroku do this for you. Read about how to enable this on AWS [here](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/authorizing-access-to-an-instance.html). + +# Phishing and social engineering + +Phishing attacks will thwart any well-formed security infrastructure. Have clear policies published on your website, and articulate them to users when they sign up (you will never ask for their password, etc.). Sign messages to your users. Prompt users to check the domain of the website they are on. + +# Scan your website and libraries for vulnerabilities + +Use a tool like [Snyk](https://snyk.io/) to scan your third party client libraries for vulnerabilities. Make sure you keep your third-party libraries up-to-date -- oftentimes upgrades are triggered by security exploits. + +You can use [Mozilla Observatory](https://observatory.mozilla.org/) to check your HTTP security as well. + +# Low hanging fruit: Cross-Site Request Forgery Protection (CSRF), SQL Injections + +Most modern web and mobile frameworks handle both CSRF protection and SQL injections. Ensure CSRF protection enabled, and that you are using a database ORM instead of running raw SQL based on user input. For example, see what [Ruby on Rails documentation](http://guides.rubyonrails.org/security.html#sql-injection) says about SQL injections. + +# Closing remarks + +We hope this guide was useful! It is by no means comprehensive, but it's a good place to start if you haven’t paid attention to security yet. Remember, security is only as strong as its weakest link. All of this is pointless if you have bad passwords and hackers can guess your AWS password. diff --git a/docs/tutorials/send-and-receive-payments.mdx b/docs/tutorials/send-and-receive-payments.mdx new file mode 100644 index 000000000..5889a2278 --- /dev/null +++ b/docs/tutorials/send-and-receive-payments.mdx @@ -0,0 +1,787 @@ +--- +title: Send and Receive Payments +order: 20 +--- + +import { CodeExample } from "@site/src/components/CodeExample"; +import { Alert } from "@site/src/components/Alert"; + +Most of the time, you’ll be sending money to someone else who has their own account. For this tutorial, however, you'll need a second account to transact with. So before proceeding, follow the steps outlined in [Create an Account](./create-account.mdx) to make _two_ accounts: one for sending and one for receiving. + +## About Operations and Transactions + +Actions that do things on Stellar — like sending payments or making buy or sell offers — are called [operations](../glossary/operations.mdx). To submit an operation to the network, you bundle it into a [transaction](../glossary/transactions.mdx), which is a group of anywhere from 1 to 100 operations accompanied by some extra information, like which account is making the transaction and a cryptographic signature to verify that the transaction is authentic. + +Transactions are atomic, meaning that if any operation in a transaction fails, they all fail. Let’s say you have 100 lumens and you make two payment operations of 60 lumens each. If you make two transactions (each with one operation), the first will succeed and the second will fail because you don’t have enough lumens. You’ll be left with 40 lumens. However, if you group the two payments into a single transaction, they will both fail and you’ll be left with the full 100 lumens still in your account. + +Every transaction also incurs a small fee. Like the minimum balance on accounts, this fee deters spam and prevents people from overloading the system. This [base fee](../glossary/fees.mdx) is very small — 100 stroops per operation where a stroop equals 1 \* 10 ^-7 XLM — and it's charged for each operation in a transaction. A transaction with two operations, for instance, would cost 200 stroops. + + + +In the following code samples, proper error checking is omitted for brevity. However, you should _always_ validate your results, as there are many ways that requests can fail. You should refer to the guide on [Handling Errors Gracefully](../tutorials/handling-errors.mdx) for tips on error management strategies. + + + +## Send a Payment + +Stellar stores and communicates transaction data in a binary format called [XDR](../glossary/xdr.mdx), which is optimized for network performance but unreadable to the human eye. Luckily, [Horizon](/api/introduction/), the Stellar API, and the [Stellar SDKs](../software-and-sdks/index.mdx) convert XDRs into friendlier formats. Here’s how you might send 10 lumens to an account: + + + +```js +var StellarSdk = require("stellar-sdk"); +var server = new StellarSdk.Server("https://horizon-testnet.stellar.org"); +var sourceKeys = StellarSdk.Keypair.fromSecret( + "SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4", +); +var destinationId = "GA2C5RFPE6GCKMY3US5PAB6UZLKIGSPIUKSLRB6Q723BM2OARMDUYEJ5"; +// Transaction will hold a built transaction we can resubmit if the result is unknown. +var transaction; + +// First, check to make sure that the destination account exists. +// You could skip this, but if the account does not exist, you will be charged +// the transaction fee when the transaction fails. +server + .loadAccount(destinationId) + // If the account is not found, surface a nicer error message for logging. + .catch(function (error) { + if (error instanceof StellarSdk.NotFoundError) { + throw new Error("The destination account does not exist!"); + } else return error; + }) + // If there was no error, load up-to-date information on your account. + .then(function () { + return server.loadAccount(sourceKeys.publicKey()); + }) + .then(function (sourceAccount) { + // Start building the transaction. + transaction = new StellarSdk.TransactionBuilder(sourceAccount, { + fee: StellarSdk.BASE_FEE, + networkPassphrase: StellarSdk.Networks.TESTNET, + }) + .addOperation( + StellarSdk.Operation.payment({ + destination: destinationId, + // Because Stellar allows transaction in many currencies, you must + // specify the asset type. The special "native" asset represents Lumens. + asset: StellarSdk.Asset.native(), + amount: "10", + }), + ) + // A memo allows you to add your own metadata to a transaction. It's + // optional and does not affect how Stellar treats the transaction. + .addMemo(StellarSdk.Memo.text("Test Transaction")) + // Wait a maximum of three minutes for the transaction + .setTimeout(180) + .build(); + // Sign the transaction to prove you are actually the person sending it. + transaction.sign(sourceKeys); + // And finally, send it off to Stellar! + return server.submitTransaction(transaction); + }) + .then(function (result) { + console.log("Success! Results:", result); + }) + .catch(function (error) { + console.error("Something went wrong!", error); + // If the result is unknown (no response body, timeout etc.) we simply resubmit + // already built transaction: + // server.submitTransaction(transaction); + }); +``` + +```java +Server server = new Server("https://horizon-testnet.stellar.org"); + +KeyPair source = KeyPair.fromSecretSeed("SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4"); +KeyPair destination = KeyPair.fromAccountId("GA2C5RFPE6GCKMY3US5PAB6UZLKIGSPIUKSLRB6Q723BM2OARMDUYEJ5"); + +// First, check to make sure that the destination account exists. +// You could skip this, but if the account does not exist, you will be charged +// the transaction fee when the transaction fails. +// It will throw HttpResponseException if account does not exist or there was another error. +server.accounts().account(destination.getAccountId()); + +// If there was no error, load up-to-date information on your account. +AccountResponse sourceAccount = server.accounts().account(source.getAccountId()); + +// Start building the transaction. +Transaction transaction = new Transaction.Builder(sourceAccount, Network.TESTNET) + .addOperation(new PaymentOperation.Builder(destination.getAccountId(), new AssetTypeNative(), "10").build()) + // A memo allows you to add your own metadata to a transaction. It's + // optional and does not affect how Stellar treats the transaction. + .addMemo(Memo.text("Test Transaction")) + // Wait a maximum of three minutes for the transaction + .setTimeout(180) + // Set the amount of lumens you're willing to pay per operation to submit your transaction + .setBaseFee(Transaction.MIN_BASE_FEE) + .build(); +// Sign the transaction to prove you are actually the person sending it. +transaction.sign(source); + +// And finally, send it off to Stellar! +try { + SubmitTransactionResponse response = server.submitTransaction(transaction); + System.out.println("Success!"); + System.out.println(response); +} catch (Exception e) { + System.out.println("Something went wrong!"); + System.out.println(e.getMessage()); + // If the result is unknown (no response body, timeout etc.) we simply resubmit + // already built transaction: + // SubmitTransactionResponse response = server.submitTransaction(transaction); +} +``` + +```go +package main + +import ( + "github.com/stellar/go/keypair" + "github.com/stellar/go/network" + "github.com/stellar/go/txnbuild" + "github.com/stellar/go/clients/horizonclient" + "fmt" +) + +func main () { + source := "SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4" + destination := "GA2C5RFPE6GCKMY3US5PAB6UZLKIGSPIUKSLRB6Q723BM2OARMDUYEJ5" + client := horizonclient.DefaultTestNetClient + + // Make sure destination account exists + destAccountRequest := horizonclient.AccountRequest{AccountID: destination} + destinationAccount, err := client.AccountDetail(destAccountRequest) + if err != nil { + panic(err) + } + + // Load the source account + sourceKP := keypair.MustParseFull(source) + sourceAccountRequest := horizonclient.AccountRequest{AccountID: sourceKP.Address()} + sourceAccount, err := client.AccountDetail(sourceAccountRequest) + if err != nil { + panic(err) + } + + // Build transaction + tx, err := txnbuild.NewTransaction( + txnbuild.TransactionParams{ + SourceAccount: &sourceAccount, + IncrementSequenceNum: true, + BaseFee: txnbuild.MinBaseFee, + Timebounds: txnbuild.NewInfiniteTimeout(), // Use a real timeout in production! + Operations: []txnbuild.Operation{ + &txnbuild.Payment{ + Destination: destination, + Amount: "10", + Asset: txnbuild.NativeAsset{}, + }, + }, + }, + ) + + if err != nil { + panic(err) + } + + // Sign the transaction to prove you are actually the person sending it. + tx, err = tx.Sign(network.TestNetworkPassphrase, sourceKP) + if err != nil { + panic(err) + } + + // And finally, send it off to Stellar! + resp, err := horizonclient.DefaultTestNetClient.SubmitTransaction(tx) + if err != nil { + panic(err) + } + + fmt.Println("Successful Transaction:") + fmt.Println("Ledger:", resp.Ledger) + fmt.Println("Hash:", resp.Hash) +} +``` + +```python +from stellar_sdk import Asset, Keypair, Network, Server, TransactionBuilder +from stellar_sdk.exceptions import NotFoundError, BadResponseError, BadRequestError + +server = Server("https://horizon-testnet.stellar.org") +source_key = Keypair.from_secret("SCZANGBA5YHTNYVVV4C3U252E2B6P6F5T3U6MM63WBSBZATAQI3EBTQ4") +destination_id = "GA2C5RFPE6GCKMY3US5PAB6UZLKIGSPIUKSLRB6Q723BM2OARMDUYEJ5" + +# First, check to make sure that the destination account exists. +# You could skip this, but if the account does not exist, you will be charged +# the transaction fee when the transaction fails. +try: + server.load_account(destination_id) +except NotFoundError: + # If the account is not found, surface an error message for logging. + raise Exception("The destination account does not exist!") + +# If there was no error, load up-to-date information on your account. +source_account = server.load_account(source_key.public_key) + +# Let's fetch base_fee from network +base_fee = server.fetch_base_fee() + +# Start building the transaction. +transaction = ( + TransactionBuilder( + source_account=source_account, + network_passphrase=Network.TESTNET_NETWORK_PASSPHRASE, + base_fee=base_fee, + ) + # Because Stellar allows transaction in many currencies, you must specify the asset type. + # Here we are sending Lumens. + .append_payment_op(destination=destination_id, asset=Asset.native(), amount="10") + # A memo allows you to add your own metadata to a transaction. It's + # optional and does not affect how Stellar treats the transaction. + .add_text_memo("Test Transaction") + # Wait a maximum of three minutes for the transaction + .set_timeout(10) + .build() +) + +# Sign the transaction to prove you are actually the person sending it. +transaction.sign(source_key) + +try: + # And finally, send it off to Stellar! + response = server.submit_transaction(transaction) + print(f"Response: {response}") +except (BadRequestError, BadResponseError) as err: + print(f"Something went wrong!\n{err}") +``` + + + +What exactly happened there? Let’s break it down. + +1. Confirm that the account ID (aka the _public key_) you are sending to actually exists by loading the associated account data from the Stellar network. It's okay to skip this step, but it gives you an opportunity to avoid making a transaction that will inevitably fail. + + + +```js +server.loadAccount(destinationId).then(function (account) { + /* validate the account */ +}); +``` + +```java +server.accounts().account(destination.getAccountId()); +``` + +```go +destAccountRequest := horizonclient.AccountRequest{AccountID: destination} +destinationAccount, err := client.AccountDetail(destAccountRequest) +if err != nil { + panic(err) +} +``` + +```python +server.load_account(destination_id) +``` + + + +2. Load data for the account you are sending from. An account can only perform one transaction at a time and has something called a [sequence number](../glossary/accounts.mdx#sequence-number), which helps Stellar verify the order of transactions. A transaction’s sequence number needs to match the account’s sequence number, so you need to get the account’s current sequence number from the network. + + + +```js +.then(function() { +return server.loadAccount(sourceKeys.publicKey()); +}) +``` + +```java +AccountResponse sourceAccount = server.accounts().account(source.getAccountId()); +``` + +```go +sourceKP := keypair.MustParseFull(source) +sourceAccountRequest := horizonclient.AccountRequest{AccountID: sourceKP.Address()} +sourceAccount, err := client.AccountDetail(sourceAccountRequest) +if err != nil { + panic(err) +} +``` + +```python +source_account = server.load_account(source_key.public_key) +``` + + + +The SDK will automatically increment the account’s sequence number when you build a transaction, so you won’t need to retrieve this information again if you want to perform a second transaction. + +3. Start building a transaction. This requires an account object, not just an account ID, because it will increment the account’s sequence number. + + + +```js +var transaction = new StellarSdk.TransactionBuilder(sourceAccount); +``` + +```java +Transaction transaction = new Transaction.Builder(sourceAccount, Network.TESTNET) +``` + +```go +tx, err := txnbuild.NewTransaction( + txnbuild.TransactionParams{ + SourceAccount: &sourceAccount, + IncrementSequenceNum: true, + BaseFee: MinBaseFee, + Timebounds: txnbuild.NewInfiniteTimeout(), // Use a real timeout in production! + ... + }, +) + +if err != nil { + panic(err) +} +``` + +```python +transaction = TransactionBuilder( + source_account=source_account, + network_passphrase=Network.TESTNET_NETWORK_PASSPHRASE, + base_fee=base_fee +) +``` + + + +4. Add the payment operation to the account. Note that you need to specify the type of asset you are sending: Stellar’s network currency is the [lumen](https://www.stellar.org/lumens), but you can send any asset issued on the network. We'll cover sending non-lumen assets [below](#transacting-in-other-currencies). For now, though, we’ll stick to lumens, which are called “native” assets in the SDK: + + + +```js +.addOperation(StellarSdk.Operation.payment({ + destination: destinationId, + asset: StellarSdk.Asset.native(), + amount: "10" +})) +``` + +```java +.addOperation(new PaymentOperation.Builder(destination.getAccountId(), new AssetTypeNative(), "10").build()) +``` + +```go +Operations: []txnbuild.Operation{ + &txnbuild.Payment{ + Destination: destination, + Amount: "10", + Asset: txnbuild.NativeAsset{}, + }, + }, +``` + +```python +.append_payment_op(destination=destination_id, asset=Asset.native(), amount="10") +``` + + + +You should also note that the amount is a string rather than a number. When working with extremely small fractions or large values, [floating point math can introduce small inaccuracies](https://en.wikipedia.org/wiki/Floating_point#Accuracy_problems). Since not all systems have a native way to accurately represent extremely small or large decimals, Stellar uses strings as a reliable way to represent the exact amount across any system. + +5. Optionally, you can add your own metadata, called a [memo](../glossary/transactions.mdx#memo), to a transaction. Stellar doesn’t do anything with this data, but you can use it for any purpose you’d like. Many exchanges require memos for incoming transactions because they use a single Stellar account for all their users and rely on the memo to differentiate between internal user accounts. + + + +```js +.addMemo(StellarSdk.Memo.text('Test Transaction')) +``` + +```java +.addMemo(Memo.text("Test Transaction")); +``` + +```go +Memo: txnbuild.MemoText("Test Transaction") +``` + +```python +.add_text_memo("Test Transaction") +``` + + + +6. Now that the transaction has all the data it needs, you have to cryptographically sign it using your secret key. This proves that the data actually came from you and not someone impersonating you. + + + +```js +transaction.sign(sourceKeys); +``` + +```java +transaction.sign(source); +``` + +```go +tx, err = tx.Sign(network.TestNetworkPassphrase, sourceKP) +if err != nil { + panic(err) +} +``` + +```python +transaction.sign(source_key) +``` + + + +7. And finally, submit it to the Stellar network! + + + +```js +server.submitTransaction(transaction); +``` + +```java +server.submitTransaction(transaction); +``` + +```go +resp, err := horizonclient.DefaultTestNetClient.SubmitTransaction(tx) +if err != nil { + panic(err) +} +``` + +```python +server.submit_transaction(transaction) +``` + + + +In this example, we're submitting the transaction to the SDF-maintained public testnet instance of Horizon, the Stellar API. When submitting transactions to a Horizon server — which is what most people do — it's possible that you will not receive a response from the server due to a bug, network conditions, etc. In such a situation it's impossible to determine the status of your transaction. That's why you should always save a built transaction (or transaction encoded in XDR format) in a variable or a database and resubmit it if you don't know its status. If the transaction has already been successfully applied to the ledger, Horizon will simply return the saved result and not attempt to submit the transaction again. Only in cases where a transaction’s status is unknown (and thus will have a chance of being included into a ledger) will a resubmission to the network occur. + +## Receive a Payment + +You don’t actually need to do anything to receive payments into a Stellar account: if a payer makes a successful transaction to send assets to you, those assets will automatically be added to your account. + +However, you may want to keep an eye out for incoming payments. A simple program that watches the network for payments and prints each one might look like: + + + +```js +var StellarSdk = require("stellar-sdk"); + +var server = new StellarSdk.Server("https://horizon-testnet.stellar.org"); +var accountId = "GC2BKLYOOYPDEFJKLKY6FNNRQMGFLVHJKQRGNSSRRGSMPGF32LHCQVGF"; + +// Create an API call to query payments involving the account. +var payments = server.payments().forAccount(accountId); + +// If some payments have already been handled, start the results from the +// last seen payment. (See below in `handlePayment` where it gets saved.) +var lastToken = loadLastPagingToken(); +if (lastToken) { + payments.cursor(lastToken); +} + +// `stream` will send each recorded payment, one by one, then keep the +// connection open and continue to send you new payments as they occur. +payments.stream({ + onmessage: function (payment) { + // Record the paging token so we can start from here next time. + savePagingToken(payment.paging_token); + + // The payments stream includes both sent and received payments. We only + // want to process received payments here. + if (payment.to !== accountId) { + return; + } + + // In Stellar’s API, Lumens are referred to as the “native” type. Other + // asset types have more detailed information. + var asset; + if (payment.asset_type === "native") { + asset = "lumens"; + } else { + asset = payment.asset_code + ":" + payment.asset_issuer; + } + + console.log(payment.amount + " " + asset + " from " + payment.from); + }, + + onerror: function (error) { + console.error("Error in payment stream"); + }, +}); + +function savePagingToken(token) { + // In most cases, you should save this to a local database or file so that + // you can load it next time you stream new payments. +} + +function loadLastPagingToken() { + // Get the last paging token from a local database or file +} +``` + +```java +Server server = new Server("https://horizon-testnet.stellar.org"); +KeyPair account = KeyPair.fromAccountId("GC2BKLYOOYPDEFJKLKY6FNNRQMGFLVHJKQRGNSSRRGSMPGF32LHCQVGF"); + +// Create an API call to query payments involving the account. +PaymentsRequestBuilder paymentsRequest = server.payments().forAccount(account.getAccountId()); + +// If some payments have already been handled, start the results from the +// last seen payment. (See below in `handlePayment` where it gets saved.) +String lastToken = loadLastPagingToken(); +if (lastToken != null) { + paymentsRequest.cursor(lastToken); +} + +// `stream` will send each recorded payment, one by one, then keep the +// connection open and continue to send you new payments as they occur. +paymentsRequest.stream(new EventListener() { + @Override + public void onEvent(OperationResponse payment) { + // Record the paging token so we can start from here next time. + savePagingToken(payment.getPagingToken()); + + // The payments stream includes both sent and received payments. We only + // want to process received payments here. + if (payment instanceof PaymentOperationResponse) { + if (!((PaymentOperationResponse) payment).getTo().equals(account.getAccountId()) { + return; + } + + String amount = ((PaymentOperationResponse) payment).getAmount(); + + Asset asset = ((PaymentOperationResponse) payment).getAsset(); + String assetName; + if (asset.equals(new AssetTypeNative())) { + assetName = "lumens"; + } else { + StringBuilder assetNameBuilder = new StringBuilder(); + assetNameBuilder.append(((AssetTypeCreditAlphaNum) asset).getCode()); + assetNameBuilder.append(":"); + assetNameBuilder.append(((AssetTypeCreditAlphaNum) asset).getIssuer()); + assetName = assetNameBuilder.toString(); + } + + StringBuilder output = new StringBuilder(); + output.append(amount); + output.append(" "); + output.append(assetName); + output.append(" from "); + output.append(((PaymentOperationResponse) payment).getFrom()); + System.out.println(output.toString()); + } + } + + @Override + public void onFailure(Optional optional, Optional optional1) { + + } +}); +``` + +```go +package main + +import ( + "context" + "fmt" + "github.com/stellar/go/clients/horizonclient" +) + +func main() { + client := horizonclient.DefaultTestNetClient + opRequest := horizonclient.OperationRequest{ForAccount: "GC2BKLYOOYPDEFJKLKY6FNNRQMGFLVHJKQRGNSSRRGSMPGF32LHCQVGF", Cursor: "now"} + + ctx, cancel := context.WithCancel(context.Background()) + go func() { + // Stop streaming after 60 seconds. + time.Sleep(60 * time.Second) + cancel() + }() + + printHandler := func(op operations.Operation) { + fmt.Println(op) + } + err := client.StreamPayments(ctx, opRequest, printHandler) + if err != nil { + fmt.Println(err) + } + +} +``` + +```python +from stellar_sdk import Server + +def load_last_paging_token(): + # Get the last paging token from a local database or file + return "now" + +def save_paging_token(paging_token): + # In most cases, you should save this to a local database or file so that + # you can load it next time you stream new payments. + pass + +server = Server("https://horizon-testnet.stellar.org") +account_id = "GC2BKLYOOYPDEFJKLKY6FNNRQMGFLVHJKQRGNSSRRGSMPGF32LHCQVGF" + +# Create an API call to query payments involving the account. +payments = server.payments().for_account(account_id) + +# If some payments have already been handled, start the results from the +# last seen payment. (See below in `handle_payment` where it gets saved.) +last_token = load_last_paging_token() +if last_token: + payments.cursor(last_token) + +# `stream` will send each recorded payment, one by one, then keep the +# connection open and continue to send you new payments as they occur. +for payment in payments.stream(): + # Record the paging token so we can start from here next time. + save_paging_token(payment["paging_token"]) + + # We only process `payment`, ignore `create_account` and `account_merge`. + if payment["type"] != "payment": + continue + + # The payments stream includes both sent and received payments. We + # only want to process received payments here. + if payment['to'] != account_id: + continue + + # In Stellar’s API, Lumens are referred to as the “native” type. Other + # asset types have more detailed information. + if payment["asset_type"] == "native": + asset = "Lumens" + else: + asset = f"{payment['asset_code']}:{payment['asset_issuer']}" + print(f"{payment['amount']} {asset} from {payment['from']}") +``` + + + +There are two main parts to this program. First, you create a query for payments involving a given account. Like most queries in Stellar, this could return a huge number of items, so the API returns paging tokens, which you can use later to start your query from the same point where you previously left off. In the example above, the functions to save and load paging tokens are left blank, but in a real application, you’d want to save the paging tokens to a file or database so you can pick up where you left off in case the program crashes or the user closes it. + + + +```js +var payments = server.payments().forAccount(accountId); +var lastToken = loadLastPagingToken(); +if (lastToken) { + payments.cursor(lastToken); +} +``` + +```java +PaymentsRequestBuilder paymentsRequest = server.payments().forAccount(account.getAccountId()); +String lastToken = loadLastPagingToken(); +if (lastToken != null) { + paymentsRequest.cursor(lastToken); +} +``` + +```go +client := horizonclient.DefaultTestNetClient +opRequest := horizonclient.OperationRequest{ForAccount: "GC2BKLYOOYPDEFJKLKY6FNNRQMGFLVHJKQRGNSSRRGSMPGF32LHCQVGF", Cursor: "now"} +``` + +```python +payments = server.payments().for_account(account_id) +last_token = load_last_paging_token() +if last_token: + payments.cursor(last_token) +``` + + + +Second, the results of the query are streamed. This is the easiest way to watch for payments or other transactions. Each existing payment is sent through the stream, one by one. Once all existing payments have been sent, the stream stays open and new payments are sent as they are made. + +Try it out: Run this program, and then, in another window, create and submit a payment. You should see this program log the payment. + + + +```js +payments.stream({ + onmessage: function (payment) { + // handle a payment + }, +}); +``` + +```java +paymentsRequest.stream(new EventListener() { + @Override + public void onEvent(OperationResponse payment) { + // Handle a payment + } +}); +``` + +```go +ctx, cancel := context.WithCancel(context.Background()) +go func() { + // Stop streaming after 60 seconds. + time.Sleep(60 * time.Second) + cancel() +}() + +printHandler := func(op operations.Operation) { + fmt.Println(op) +} +err := client.StreamPayments(ctx, opRequest, printHandler) +if err != nil { + fmt.Println(err) +} +``` + +```python +for payment in payments.stream(): + # handle a payment +``` + + + +You can also request payments in groups or pages. Once you’ve processed each page of payments, you’ll need to request the next one until there are none left. + + + +```js +payments.call().then(function handlePage(paymentsPage) { + paymentsPage.records.forEach(function (payment) { + // handle a payment + }); + return paymentsPage.next().then(handlePage); +}); +``` + +```java +Page page = payments.execute(); + +for (OperationResponse operation : page.getRecords()) { + // handle a payment +} + +page = page.getNextPage(); +``` + +```python +payments_current = payments.call() +payments_next = payments.next() +``` + + + +## Transacting in Other Currencies + +One of the amazing things about the Stellar network is that you can create, hold, send, receive, and trade any type of asset. Many organizations issue assets on Stellar that represent real-world currencies such as US dollars or Nigerian naira or other cryptocurrencies such as bitcoin or ether. + +Each of these redeemable assets — _anchored_ in the Stellar vernacular — is essentially a credit issued by a particular account that represents reserves those accounts hold outside the network. That's why the assets in the example above had both a `code` and an `issuer`: the `issuer` is the public key of the account that created the asset, an account owned by the organization that ultimately honors the credit that asset represents. To find out more about how that works, check out [Enable Deposits and Withdrawals](../anchoring-assets/enabling-deposit-and-withdrawal/index.mdx). diff --git a/docs/web-assets/SEP24-state-diagram.png b/docs/web-assets/SEP24-state-diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..ba06bb5947384b4725f959c546ca8b9ecedd2b08 GIT binary patch literal 578352 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

>3*MVcmu7I<}rZdV{ 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

TE@;8DK8e!kXl7C=fvhh?7k>pJwQXL%K^1M+v zUIg4{v~@eB#x$U|UVZ|AH@CXwYg93Z$5?lKP%>{qa&)}w?x;h5djP~kP;;zMSNyIY zyH_1UGv>Yl;{;D_thTtL6fxE(=H0osk7vt4L1s}wYJRhI&MgEhY{Bwg7>+PY)6o}%ShYFx*e=IYa`)-HVRTW^l2zlSx8rv6PKN5+>bR{@f!S9tNkF zGzGaZRR-+z~m3{vgmF5mck^QV9`8o&Eq~+x98wlW^aK?)KuFEoRc5<<>FM( z()9(t?{;gZIu} zF6^KiOS52h-t*2@WEUkA6sy!q;0w;4Qn%WiL1*pMgLi!Jx$*dt0~Pu(Lmkx2*U2{U z;{KVQGTjwZ6YsCvks$(ia4Jx-)rp2`aoTkBiFXS)$ccxR&P4c$?ha!6UE?VGq(Z9R zX|QbxJh{56NcARlaDn2{NpEfQ-fa|ql_tVCJ(^RYus8_FS(ygq;i~_hi-DPPC>Nl# z2t0MZID)w>zLMhhsLww=*h$czDKZ=VN({V~wBLxHdJllid$#g-<67!m4`AHBxteH}KXaCe0(SJ}^>cfIci16rH|Kitsy0q&T6M;N4rZ8%f-G2^>Nqy`qP z1xhWSI56K47tB<3R=Ms=TgCHHo?k^1((kWQz*AAR51D^oP#s8Xd5JEjRA~Nm@FnVJ zzJ8NU1X(JuN)DDiq$z-&NUPKLG0#k1vN$j!^0*!eNyq76D@QOF9s$_?)(E1Q;`XYW zXl=)|G4@P=%Md6S;t2Utlv-{ka=7y^Gz;KcigY~^kkAv_PSxz|U0sRR_~|{~@W$Bj6gVgSI{Ju}smu6? zmwvso+>Pg6qQa!HY%XZs_!Ds5@t{Le&_9n&GfC?u>h5VX-?kQVl`NaAqsf#O)266- z2W{H;y!&{Mhe_^`++G$@U?MRS*qTfc0r%`TFH+MYL!b!Fg(@(E^|I=-3$#TS{fSYx z_YiN3LK$Te!XCKL-@w?jo?R!BtkXdsEu5=QIx}oImdkh^GIyDLoFWum2?*VG(7zv} z*-2VPhZm4xzpbF(M!jc7mN^s*u$u#lC7+*K;7`VV*EsDer)*zWbk*mhc=Aao4L8`( z?E26p-<-LwD8(zH>){|>VDr-W=gRUxGow$$(%5k*y~5?>9{IxH*N&sd=o3iYKszpfU%d|}r`VNZM8{yjoK4>jcX_-6CQ#rO`Q__3}V6>lMZ zBb%`{UGyjZo2WbP)U1ri>7_kLpP3W7aqLdb5v#2VruJCmo3VX&_`&$&8McV zlY#x4lVh0MaDhn;>(T-aiDp-}gpvsM?Rnt^C84SNQZvkaY>SD@<4I+fGc{kAZ=y3L zZ9Ekw)tMx1KAR;BeHi!WI|Qgt_ijW&8|K@42LvUwTEsuKzQoAG5{M*cc~F$ zIZ7{5M4p7xhrve9SC;SMM&;H%IQX(B1Bn5%26WWNkpyE@a(^o60TBmprs;vD>u$KE z&ptCk>Lq*Kv1h2`uMLEKlcI86O;^iQzW23b*Q#kK$fU~E9$4ig$T@0WkXVCp0B+V7Ij&x)7^Rx-?8vZOrJ z8+)Kxzsi&S_WjqLP`mfP#viVc88T2C<$lSJ50$O-=?HgK@cp$5m8>jWnqJ(ZB@}Mv zBR>VXNeuaT90w>6-Znc>Ku}@*+PB`u)jFng6zEC$+wK_5n|0X$g^7_Va}U56f=>Fo zmVsrlvZzcZ!feGQ0Mk?#36K z=k7VZa9uR>lB@=?D5S))r!CtYJ$`BUs_G@*k%7Sd24;;C?K{Zj=;SKSQdnE)hDq9? z4d(WZGL}5M1aWPVWQtOxepC~ppGb4NhM0~Ce~)}X*z2ax>g$RZJu?YHvy0md!H35u zt37#jmdaf%r!VQ;{MKFBfQl5@1555>g=cM7)RXe`4FxdISQEzEGz^4jy>~L01pF3R zW>_3U;7ZtcSC909klk;Xgox55z3;I1E85FRr_C?J5?G^W0afRnNK+afS;mT#&0gR5;>QjuSsT%Sl@_Bk+x$WOQ($ z48}4jUj+Ff7Ng=aPv12l%3~;G%iayHD$O5+Kt%7` z1inuQq8*eB-^-@1-HL_f^$ zR0-{f(Pt?*uxTjsnU1A!d_4c$m%1Che7dgJsfv0#NRUTD1H|!(q;q3%uQ8L{N|kG6 z#W5rOb72DJaGCp$txTt!kG;LVg<6+aWR80tOV}0jPm)N$9$_jt?HZKuvaaKNeCJh* zT>7(JTFX%ri?*?DxXzEN&jMUGyD6?D>3)QfUE&)2DbbJ3R^yE~hEAQ>+%J$}n(lV% zFvl_bU&cmr^JYfZo5$d(TCSzVwY5so%phden)XBNna)GK4)MNH8m1!i4r~|LD^yKa ztw8|7^_tXYH9C4$TWOr*K_Q8|WES`+BuO*OYOuK4q&;Qq-CDHM?V;m@X!+$ik5Wt7 zWyj;C-c8o_@Zptbcf9I(OiPPwq6cLYlhvvox4_9dk8;6utLV*_>#q~SLFPdh5LIX% z&ZNcn{5{`jmSheFs#LW}rt*)JGQ7_Q5j`EkrIT%i`8{s>tZjSoJ4pY9AjrFeSJuv* zaD5~N`O61?x+p^C7`mSL)2x5PGxR>cC-b8?0cj$R*FUp}tb1(H{f zF7$bR|1yckBo36e*qM2@xiE=s(Msd?j3F5%Uqx7nPbkz9lH&J_=#s*>ow)oi>0cBT z(Hbk|SMibp34~s%T;9M=-p2r>aN&L()iPTzKI+KuWK{}*9j~8Xk{9D$`=tt>d(G=p zh6rF7rREeMv|(XP&r^s&SDO!&KM=hZeo5YqSW6J@YJtgODKj&bE$$m zv5x%QZxg}Wy9j|(KVWAj%s>GZNM=k@0yk1+rzME>Te zFuIz!Fv)Fmq;6tEf&~s(qwf0dw|FMOWGI_19;A9H@DtqC3RmO0|W|v$$KAfl2E2gsXG8m4&qX< zzaP{OOs4?|$+F_>lLr}9*CW0RUtMqp7@d^^!k|9N10CLbc*>%GTp=D`7aFbE<$|@^ zXH-l)abUduoL(8xUXi`JusV(pIizWJB4lKd(|0$!=NVSd`clec(SP=ic@AIkY|ix? zbN1EZ)J$2t+T>vWNmf#!!z4`_K~$IpeG9=>70tj_swPCWwb7$3uWWBb#zJYMJ&u#h zQ$akI(w#A|&4_P*q&3aK=1z<%%ql`y^*gss&-)HoPKgHb-d(Q{{rV_4%w2+)zyh>$ zJH!vG57u)I<9B1VNe^hYiW zwn7hbL?rHITtI%A>uSh0$LS?RZazfO78#d+#_71jLU~KHnBm<=r7PmR9(2Emj~~|* zGF#1_ceGrzpmjc@JKOLO-SG5F1TiLN7;L8B3jjL~dIDVC!tj@K@vdv9uObT9bX7m% zey^U9;s3|GsVZDP*jWP!u1ox-`WTHD_gVgB-}t85#{X`^XPHht3g zWllo#EbWJ2A9ffsGS33J5#+b9Hf>C)4dSaiE{bEETq!^-){i78N-0lc^k! zRdP75Uyz!)tzjDGdfI_a%x!o|H_Gc$(N**Pfje^n{g9 za!uWNmDKi4q0iru{YZdHQ)7Wogdt@TO6-tv$Jn0OOAAiL>c)<5Qi}`S5^|4IEy(}k zDID8)9pG=}{y5JON5|)V$-)T>ShYlOgl?U`hbKoD{RG}(u}R(vKh~-(IUBLiDDt^@ z@#!a_nOnbCFnZvBRT4&N|3yq&4*8p)We}G)CR}dIC8IR6@*A~0f`qL1yM$f`5YAd% zQ}5dg+U$OQ_S^q14dTwWeEL{ziEjQ;A2T_PBrdMVasFu`o!5CH?vP1iTzU_Kr)aZi;MYO3dL1al)xpksXUmneq z55xJ1*PW#X7X4y-dA*B*$LrthWAM;Dxz^@U1g2}D6IoP;nuBY~8z1@~?}<=PbYMb; zM>pneJ%6y1RMf8rLzccBm0VCAK{xFYy+`LGH5ENv%8D;zl_kU&lJN1rI$Y^!xi=dsuT*R(0a z`so4mYc4%?&#_d2@n4}wX}V`SUT5#gjOidZ<)F1+^M5uCvVbOguB-`+%uXHqri zGXr;0oL0{0y&9(lJrNX8pX4=}j^OYY6!?Tp`dc#oj*>i=ONuG*XXSWBZm&*>RXB~> z37<4`f?VtCb2?ol1{USFWPm+0{aY)7$O+cEV-%$AaB<%o(3QLr%D3^!47`SKEZqcf z6I9f|MIa$E+Gdt*`*=D;VrOe}} zg+Q$d5eW+-)z0MTH>M{+?le>u!p{o6I1*T#^jCUJ3{*IE1buylJpZc8bUJA;Jq z;Bzy&5GKF&%P~o<)DN!4O)ytvq#nYXG({tYaL&$Ga-VG?_ASb&x-q z#yq@RW}_VCD(9{IE)?~N3uQM2LT;BHJ51b1eOsknll--~!Cdg_ zm1j7M#7=MEl;%gFjB^fR7k-^(X)hO_)23gpOHSM+3<=6(H8EDHb$PSj7`{&&flL)) zsE`W@sy{p8Z)x&Bb^$m22p?beG@>P*srT)ILIKQ_=cZGq%W8|TbgQ*b$Dc-J2U$IGyi)wQ zFOt!8Q5KVl7!nESU3f3eOp$Ba^C;v*-R@Rr+A{)xG`#DAgtOoO0C^$Vexw)R`Hr3U-lNrP zZ=|Y|Z=4M&a(+VRTWwh%NgvwXf9O_KZ|3oK@_vX|^5N)FzK-{qO-dHXA1`}c()K>o zP;#We4F9TS@$$Px8lTM=k;Q+9D&0Yory!S(If*1P_Kl#UHK#J`+7LK9 zmU084qBPD7qSqK2-5=C>%A_BdL^u9sIIH5$anoB%=erx_#`$|O%2lPymKr$iPKzF1 zAR%NHQMUu+(&Cu@%TpJjB>h{^{X5$3EBpba+e*I8NCC897a*q|S{rM#lK={l18YkP zK9tu+j275whe$L0y|=5L(88*6^BtWv5VJ?EGEkRU>I;ktTu_Kq?Zx8**;9x<#R~m)Bi4s35|0L zbem!VQAPJQlgN|4p@f0x*F)u7W~n zC#c46i2E9Vg4IL`Ql!?|HJ2DtDm_`*)tCyoS-$QWH`vN}5tu#cPs0 zw5LBZBi^(oTr+1l4Qvq*ZNt2`Z{YBO7rOeKO6xOz5^7^p`N`~42DhB4MzrLw+$!CQKqB(A7eXB%| zFcY6AN=`F`Kx4jTZpa2tw<{0)fd6EzTP`yAK47Z#d6OE5rqJ@Dli#WZE@G)SVct|H zxA(oDq!o9}{9hR>G(hw)kA+lz;RCuQFT!lvW)i-=?y1#0S}J+s${(Fasl_$+7P!#E zp;SnRo55rIzkGy%uVHcX@?ve^7Yi0Lgh!q2dxEr1?#Ic-0op|G6OCKGMyUz$+F0#ebe3}K@1g? zgDM?4QBUoe^022Agl=c_NOK-(u#D{|tw%`=P@>bDYeVs>(-sYx?m#&k3TJ3C+sz2t zUi|7q2rzpRD^B(ka0F)t=Rc6U^brhH3ok4BndKFe=D;x#>59M{5`k3c37QP+lc4>m( z6S@mFLErT}d#n9TpB6zx zqWQEdz_&b`m>G|czt0Rc*y#tv0Cs2ltT->A#p|SolJ4+Usy9i_N;H5*jW?( z>}1m6i#1$Y7WhM;FFm017ER-Wvx$RL1U+c;px=>y~+TXkN@oh*%WPWyb*Mo1Yps|KZtRT9PeZ#RNj zF&@B?#FI^%{Pfj=uu#6}u3IC+&Hs5Y8(ms80Y|Aii)zHsE%pa055k^b z95I0TKzX7j?V$GH6GwPJPmh)uQze>C$UVSW@cyKTU)_*o~CWqtxU-T3I8KmkpUPVHfPmx zf7`lbxZ!mw0b-JqYc01%AdI`0-VHkS>fW}Olj+%BDxTz|;FbdT@uQP3Rd%woj6NOp zpRngu9}MQ*hv8OyOOx-~6#(15|D7xHu78UxZw6o^rz9@{x>l*8764W}o(06YLxMGV z39aGbl~n`BCs%~`2aAqR+c(>fYd&yn1KhqsOCS7t*vh(=*wW%^d#wk*91I_3}IP8hSkS7h} zeD6z3O^u<%JEiOVmQ8HDJu1k`<>oD0Dh*J8>PL=gA7UDrH7~Jb-S;ht(^HOCL!cW- z(i0+z)G=>6uj)y)WT(2AP`qzH9r|>bI9)=yMCZ5->JWM)oCeL{v93aO?9lm$JlwNnXJ#Yl zJ6Nd5^Q&euRJ`Hr`=;3^U%B06RXUO9HCp24STQ)#nqb9Z;9`us=C1VoQ#EJ3?IVwy za_7-tjK0XW*zpdi1fU-L_j@$@GlnE?POJbF3sEj*-=3TSDgd4;Yj}{YCQG#{?MwvD z%+lO-$Up1nkPZi%kX_&AmmCvK>~cpIVow$80$>f)#PpNvJ9`ll4vC9|Fnc3Ks~WIj z;H0Y4BmSj{0L?TU&1AcfR*}z}8<-Z8`xt%BDc!@0!K?q%MMj2O^28emkE6z;H2HAu z3*&)@K6pX}%epxvKehf50qAxM<_*5sf9i!A~XcQz47An*W zJ!sc&zyEeYd;C&*wW{UNd0J#M4$jFQ|LC-cs@-ngD!%PvJ?$M%X1w~8>o6f=1Zes` zQxhVeE4up3N2z7&o_A+nSwiCJQPP5F;1ow$_Ia~KP1{rD;ykcS*J8f zx^lHUa9SO|?I#d8zu@*1Y^o}(gDG(Kk##~*uSomxyCWj(ZNs;K%K&Jn7k&z+lXJP_ zajK#|WrJC|gl(l5C*X^KVAD1C8ipr$CZ{yh(01*B(>T|vdwbCgqY8;{5yG>^d2Z_L zh@nl%c1(Xoz%%tNjV1nUr2p93*Ilk3=K&2q%77oZbS>^P3et~qQ>c5=vOqo0aWdv67^$nz*J{VpR z#SGrHNA|b*GS}_RdQA@FnP%vT381r8=w(kNYi1-13p|862eBHd91IvrCsq#pQ+_6+ zv-HgZ%NGB%Ls8Anm@&JG8br=?u*J0d{m_kS^YldpkSLh;Pn7R?+-fq z@R?_r{oPupRM~gdOFe!Oa%0Sh#MTT-;wv|ilwVT1M1&nlDM6cuOBD>I*A7LTN)2gA zo*Nr5!-PPSdOo7fOCMiuYjy^utga~rGGCpnfUfh}f8v7xtcChe^Q`P4*M>RyTb~yL zwmrlxp0N>zEJxXHp~tlKb8M>=_Awj4F)81$UReAOB6GnZB6Ml$cU}5lTPr5!?$r@B zs+a0^jr#q2(FDe`L%Lf8mv|z_RPof~U7p>U*)w+UhM2wln-Z|WY(q&_>ZPxWgw3<` z>D(u8_Rl~bW%knxHxxa+ClKk9N$O&eLGQtPy#%p5bT0SR5HOb+1+GrZ%oP;}g|s1NjJJalBU;&_ui@=X;CK3lrwMUW~^ z4BzO7(|afVr@y|o$!J+zY~oA1tohuh7Ba6VJ2LS3=YU#k)W&E92>8AEXpKh6gmxhD zxs*F>w~(Fb>*z2UISD+>S4WPNzp?4_A7+~LMI%>c_r@d}%o0U4eTi-F-q_Goo`k#i zh4r>H&v+c!;;d{~5S!%orbBlZ&=S2l!r}^r^5h-kUQnK60^+%TZ zCsgh${F{M&^^lHT3x{RX2<>>+C075-LmCT>z=MYwiBX7kZ}E;w?QO* zUJ~J^TY;LZCD7*o00}SLesg`W)kj-8Eb}_HAD5>#2tQ=skgaN&UUgbzciB8lV|8SD z;Nu$Udnrj)?RhgV__aLu@|((Wo|P&c_Vfx|Lox&h!R)|3c?(wuh6$0SE zgjgH$91A;Q`^)P~`k}wqUHM9|@i-#JuN3f4*~WR|`uAiCVAp5I05hv;XlEPTn;k;9 zH)G6f=iE0)d@ccAQOYX{n)bZ(EMH5xpq#lz}yfU)CAZ_VglFiL+_{5)`ndg=?}F02;cMm!K{KThcMw znxkuIMvqOS*B8&cD+$v9Su7)RzHj=D8m_ zaMtHh@319`w!Qpx0uzF=uofTJ-hKu8!Uqpi@oRJ!ycSze15HsuoKw6a6V7R1a7j)&Md0)rMC z38h0rIXqmpF)u!6k*HpPoFvWg49}Fv%^t+F$cu6aFBkRF{ABh@`tlqw<|!~;Z2Q*r zU*<0WH1!tX=l_02V&7}qA`*pqbqju_0FEu`HV8680Yh_tSp*&dFCM9gD^c(ZQR0o> zoK7PT5M&oq<*aF+_;;l5`>xi7SGN_tDJ3h8WD_$4DDHKpb5W>e*CWJfMW<>W zHD6nyvOl`N*}Cnkcau%dK&t4GyST#kOhur~#qg7qo7f7Pb(rPYWHxGDruAgJS{)Ze zR?t&wJMv~EK!kBZ=`y>eQK{V9A{4UzX&Ra48CQK;JR4%Uut8uaDt%szFQnB2jw9!> z`lK4~-ghkHB?w0XwD`=qss_Ljf!$6TYaH2HFFZ^PefR2s=G>pizW^OApN^vJu0#G& zzBL-vq3J$DRZcR=-3vyKf6<)|Hdmf}Yiv_xvthLJRyhe%JoZxm`Erp<=g^W4ABzfX zQ#9TFc~4F?5%4FeX)eW=Z`YpcK2UFIpK_|GUx-Z&+18&B|JR3>gC;o?<$D)LYkNaII~1e-}SE~6Q!de zr=nazYhE~mP;`0yw^(*`@5(ce7D?8>Ep4doEf}Wq2a@v+RKfsb$u8uwRft8E!W^L{_rSL5#C!0M>n%YP}b;*Us<^C zt%Oc}2T##YNteHWl(}Vb4!<>iku+-`9z)fK9O+#+UOg;Sf~0;wV&Tb*_H^Z|DF``hUwNneSL7A!P;9*77bqg9qU-v*+YA23lyVuDe|EM4a zP~qy%cQWet0mtMbMsXQUXP3f};j&<_Si*nhQX?)#;j_>}_PJ~7=|c(pA%eFkUi%?% z0jp`B-4oLM1k@gE{ONZJqxEq&vW(zeS~_U~(!5{nz;)lBR7G7#&pRT8?N5YyW{8W} zkAAuc?EUPDIUKG9iyqnG&xZeP#t}-5D>sTL5{Y>ET{pxlXG!Ey6x1d0b1x%Tvt@pT z?yUm4-NuC zWyILs(pxB*C6rTfdJaFZB-Rr$FJg-RIPTPkW&)kaN83aRYPaHDj-93k zSnAZpRDXNnlv+9&w>xvU(Fe6Xw>j*&uZ5T=&r;lNpdU+VjQ~%EE5H@ECP7A_TR_ej z3+U9D)8)zOfRY>)P*bv-Q*f>COA>r=mD|XFewbgGEI#PY?VY)8!_2=e?eXRYAhtbe zs8~Gx=Ho9Sam0{+@K*0>)u{d07v3|w$E<}o6595^Z*+v0&CMQih4FF6D_(lo zg6VZOVHL34Q({918}SiSID5>aVgX`GLJFSlI$W&9WVNDc8^tuxgLK)?8GvQDGcy#(;ic=PkQp=fueyv}RulrLIm z_9CINw%wH%u?zc<`X#IqJS8fZX4$sN*x0VphRy`b35|2js2?tS4WErIxndaSOyV*B zpS)02*WSunh zHxFonj_8|!AmwLhubq`cqn5fOv58$}Mga#Jq^zZuTdu844dIv>iIx zeR!qTX`m|-t9QWy7?RAicm8Py;MtO~L8)%}Wu%wz%OJmr8d~=m(^S62aQ7xODJ-$! zm*xHn`hM!^2~>^QD)=0tF>O)5_*KoEvq=ce{l)Hia>BZ-Eg4lxDgS}djp1zLWm@_9 zgh+mw%--cS;mkoZ?tyB;gagB&m%+Yw3oY{gExP#w9gzz4Ix}}ogbQ<*V%4Bl1N>*Q z!zU)fS-Qr#< zxn+muTxHSwVPE&vzMNL3#?N@)YQ;G!?OPQa8UxNnQ<`_bZx9x5c>3=KFbEOZACVV! zsGS2)kk5VmxDTrMEshTse_i@`Mok}UG$F5wm&X9N&1!+1tcM*%|dWNJhfIDzI% zh2Uk?WYbD1z1N-wfa4Q(XAtPAlD!!c|1>u~7W07M!CW&*&-CvXwJe79>RPawXkT)s$|6$B#lz#w-x&HfzBJU<=y8{PcA$Foi zqVL!+y~meKT^N>6V900hVOrnOv~R;|W_$CJ$p@E?C(a>@s+|Z~Hp55r{c4XAgK2mU z=+w_%_GDN#=sD9D^M(abQgdRVfXkZNLfK&rqsQ24&7Ch&ruM*)5X5wPKqnqUeFJ;1 zoO1q@(SKQdJ+#iXFXb4oRq|Z9qWQsgKtGe=$-Ng(E8mfa@u@hJyRt(7{|Y#?|b4 z+UT%lMw(GmKQqUw!&4xEVYidFIIXz2oZwPA^^gY=Mbbj_X`xZ3>pq(_i+u%KLJWO6AY|`DxkEu9^UnoJwd=KWK zH!O(5@X^8vJr9oaI}?Q8b-&)OYi(w}s)TizDnRcjGVykecJ&=wYtDia)I`ZEdU)fW z&?h-xN~}x-CTt%T1)zmD70dKT%sd>X&9VB~4f^`=v99i9?9az82t{5zXRFci9Q%aD zC%)MwE#{gZwW&YuMP+G@swuzlGQjo4^DGK+Q|p@y)(v;=c#L}yj&h8;Re3?i%w-b> z+SgEqqL(XPEHoG2oy>fb@@2(A^pDqsbQ5riIJ?TI%&7wwAiSf6=+OnUD+Pyd!s zV)}==uG2M|1)ajSink=G)6Jf%{7qMENS0^R2 zG0B;CKeuZpm~+yZ52e{rui(%-Skp~q9e^=EQEj&8q><*H1X@}qDmv{R_D|mL*+5-cQu=Hiy^^ZUwVv{2dZ|B|28H1Q`H8R ze*|>l|9&;#bn3oE-B$9X66k>T5Dlo>4R-vJjxj_u#l`|XECE$H1Y3{5e;igAUsTD~gKIFrsx zqhv@Nmu?7svx#o$Vqb@yHyL}bCv$Q8QRaH+14G46@^$A(`h#tA!-dX-LQKg`NlnlB zM|g8l|BtIP4~Oz?|GySdDx|3F6e^5TC=6LrAxYWyHH>{{P}#F3`yM4JJ7cUf_I=5| z3?}@j99r?2?t$vA8A)(eHmki&34>64pzJjSygjAPXL-wFwy zHs%)gRNYT*N3yJQ^yCO5`(VX)InxbSb1qEvP04@`>&vZi7B4YKI-*mSkz&xGY@)Za zheFid)l->+1a4z-&w&a`6o{Q=>uY7t3_eO-Q%cT9t^j_#BE~Kn3;Ff?4BkAQDe$?k zimCtSjc*Q-hv7PQr)pl6e)+uXqkR^y-1&1_z+Q%qS>ke(d3~qUXqNham93x+9kZPgDfLFk(h87qJkO4FkQuBcWl0^y!{@~VdJ(bm&`dXkVV7H-ENWI~PA_uB zrP;;-UFD!+*x?{;$I3-l0Uw3y`;C8y-rJZ_NZEt%3G0kJ(e&Bpd99J!YpwUVMh3F* zxLs&6nz){W`90@7{O*T|=Eb3JeLLhQPwNL82bHEbaCmbuQv9EdcylEy#VJn$&UJAh4C zm%VjAWc#cF^RG^G&g<717VK{OKjSKM5q`kw^uIczwS2J|vvT-h2xwCbTL^!aa9Pr) z-AIvt>MP9i0HW#Y$M>-fXl=z7=bP)d1Ns<;@IPrwVV$~CIfJd^YCH{trOsomRA_+T z!ol|sZObsG$RAgR&;sY#M9rUsU-Jti?W8*|H1Vwp(}$PwJdEmSc4bW+=!bqDv8i*) zhoE^3bwd+s3``EqS0q8T#FBQ0em?UlROh-xg`;10_%F zE)}9zK960@9FHyJwvoKnS@j0U4pH_{eA&2C0hi)t8RWjLm7HYRBo~Qhzi_zJjDZK! ztg=gsr$2ugwspODRKm>I^yF?*v56rUJ?oC_Wxw5AsHVyN6Lh6*lt@^O(!y6M|KF`R|y6exmBo9gl{P?OH~{1M&SrAF9F{F1JZZ=P#qD7C%~OynGh1G%0a;<;JO1rT=S|CwWdD%+F{eC{ z19dBLa{dj|vI9IB0*lAkRC~75UnLw6f~;(Hfst%NDp&2cq)U?gpNQqy1%LTDNh28S z-oq83X5A2ym;8P z%dmQG!x8-E|LmOq?V(`ZuH)d5|NTeny%tt#=!eBK3>L>WP#V`Fh7Wq)-3pecE5m!d zSRPC0I`^2b+IJ}!-qGMM4Jix%-oO^VhEkYryHw(6SGuZmlliVavVfht4#APr^om%y z(D(3z+1v-qY^q^B0P{U37@$3ps=OP1ai|?SRpkI$mt=`}YozV$FQASOdhIP^d9b5Q zmGl#39YAGOd5Gu+_Vj>SH(vkbelum+*m@RwJx$^A-3lpmGCwoT`5t9&rR!zvE>C~W zhL92`uIKeV=;qt|et{iou-D)-h>3}9aOns9ju{^>&R#Mq8ITb0eY;kEmxiG7?K9=X znroJ4-$#*Bo?}2N-{6O(X7YCdxUQu`s6&0@YQ-sI=NX18zVAu%elVCOan@O+j2qkM zFBJ)U%o@zBPIEcD$HR%8rE%nB@TGDd_m@&bm??eS4wXw&fJIEb=Q=?ZRGnSGzf=mY z)}?a2u9ZVI{2KH7Ys?zsk{JN_9Fn++-*;9%2B+?1&6Mw}2MHAC4-o%!@TKmaV6NN< zHC#kd=bd*8eagQ>MMPNo`EVQfSGrY9?I)od)k`YZ6n=F0X+pb4E6o%&p-Rmg-UTa_ zuSl;y?CxynbQecgNlNtagJNao?;b(t?n=b=1lf#~W){j!mai;vSB13kQ@3P;&{ui* z&n2^6JeUyEVr{4YU72%$_kMuQOcqQi_jw8sl6hqRs;IoF`ZTh3 zyv=Nb+Q0WBzWonr@wjntlw>;bXgr9UEyTj`O_zoh;s$Ip8>GpP-353h5Ootx;UI_m|v;0d;db zyJBr^YQ0YjN+*5hzbo9lsKqbyIY#S$3m%9BWB5mQ7G7T3`L2!C=j8dG_eTpIhJdp1 zW2@rd!nAcgC*y~stU`k@3Pwf`f@?7fR^JHD&e?HL8RJrohyI=VJV7LGKzB7QRRp-bT(&Dpzy2?!Xb~RUG&O_#9kW8g?v?3RF&*_;YtaO*% z2dUei(pM_UcV>d8auT>L66R0NNz-DSCW+hIxx3FJBrlbuML(zo&e$2;C) zUd}-|w%r|HlY|AY*3HM?EAz(3t&j@lw;f0qN>m%@3eL3t*}ge%-;(_nQPrelTx8#} zcr48ENjDL@f$2Gah-_8lkj3;Eo@c#h4Xa^(=Z)-(7JDD~)fi1SCu>jgyt^j`e@CpU zp(Rv#aP9aU&mI4HLQ#n8F)QJChHJXBknXs;7#VQ55hUdKYexFB6@OAfYK6aG8M9n; zQ+mu70Dr8AZ##4^6K=#`s@xFHYNNcR!-8SCYYTasi-WbM|12%_*#%Noj+tPQ!B`ck zEImjsX;bhbg^{6Lh5LmhYZFqbb}C?1K%tMLJ?{*7cu;LLthJ$a|zvp^CnK?aPx5-kGu zCn~PtE;8L(^}8M5WhzlYdHYtpxNCWp%fvpA*I4^CFN4(=4L%4{jESJr^i~HuEurj@ zOSHS(PAQvRwFKeW?GM%BKcx>#FT*kDQ!FhdgsguWQQ^8Pxh|_$KqotNQ@8(wSQg&B zD*K=KmgAMx6Sr57d_cs$Cv|RDcy1Q?zxYAg=p7HHo|Q8y#zB3LLWgroqARyxcU1@g zhWG)cb1&Hd*hT6R4Z;D(|1Dadu@2}ONe z4a$SS;V$AeJEwh4JX@_UQw}-fu@Trx;Pf9F?e4=!LZNRP_8j51WYsqvnMaoG1hy;> zf7+F{)iO+Hs!qMpc<$9h*Pqe~yOru42$lahw=MVonH|-n(0K8;b&p+&uHug5dFsR) zd2js$v%*f_IsIboGud)WnDh{3VRd!ivcJ^2ycX8(m<-M=c%$0}L3i3-i<57<@udBb zFffG3c54o4n^Ycd2l_i!! z+#_8%fz!epbt&1pxBj>B(*Ti&6~yH5yVzfY?O@OT8J9Ho6qZ{4R93ZSTg5NJdnC51 zQ02xfE7fMWmv{%G624>8g;CM-tC+hw@t~E@Z>O^z=3GV3%}lKV&AdtAS`peaCjf^x zY3Nkc*&>eZL$f+|ZvkU(C(j^#2dxAhcMuOYXXOR!4Er%3Yqpc;I!H5X>qtIZeB0$~ z(zZn&?Z#L~g}-TyFjO-A0te{wbn*Kwqc8_=vz7N4?#JD(9`#04t_fME8!OlK*1i6buN2<* zKH>(FB+#wwcNn-nLsO$e_h=Ur2~^&F z;|t;uje$Z+IP|l{{YXVL+5eeQL?q`{kygxaI3fk{X8F1$>N-WMxa%C=?%ku{l)$lN zsf6^E!En;71B$EH0S;gvrT%-Z{LhQz)n-^o7Og9Yb@H9_0n>ZNW(KyvW;|jsa>PP| zXD^A9Ym>WsDmW7lPR4-I=nS74NPW#J5_d$=na&rlaNM1@KdN9=UUZVA?-({_(E+<3 zzjWBfRZP#0UyOOd zD$phgJW^Df4vu90WOG7%rMayN)_76}9B?YHgJ{#=#^BTyCZqTJx>Nb(NZMpSt_R!D zZWY7eUjR!zh3!mSdDDspSw`p-HzGb(HZl&Runwo_C4_Vd&wl5CeZ0)y+sHqW3Ne5=&D_IXUymXqj1*_W zE>dv2I{nQ3J4@a+<=#jKlxLtQM621w-`%=6PxxDql$VggETJ=Ny07%3!zG3$d>QRh zDT~>T5GV5eW1a((Uk-*Z*n>aNYZG^~;&z)L-gP#nvHjm(h+kD&LVx7@)$a?PMN zxsmv2|;Dbw>aR(YnG##S*$G9d+#^W_s@jyA-jIU3xFn;Ou>WmmqdO>Ykz5A8%iX zr?0>j6sX5=S1mo*7PuJ*Ix=dFJCTWLiT%f~pzOv!N4s#JKUW)wbCjdwKHzi$o9uM* zxwE}5H0qkzfTS%iI^}fASae{ z$GCTYtmcj)KRa}ygoyD<#oQU?he$S0m4SlU@B>fobr*&l2ULtM)C&hFc=_B1 z+~Sv>=?2F*oUkvj=>$Ua>iPlaC)7%W{75~0#dIeP zI8?xDP;>(`)>NRAjL*{PX~GlbEm^WeT8J%4-`?2EkUxl=Uhx?cn4ePc9$M0iU(w&N zVOUK)`z}*&-_dq3tl4I~NeKIXRffg)!W5@jjWb8m&CP6C{5&8;aGz0d52vxD#K9Dw z)&Rf*!S*}CwN(+_31wRw^1cE7eBj@Ej2FL=^reCcL+FR&-$H)fK*To6PbB|fS>R~D zvh+nXA(nQ{%-}cMm?UA@I2JY`z`)MAPkWGB7Cj>lX*tJV+#zUdjA+R9>wM{o6jJfJ z@AwsI&Z5Lr9k!^6ZOGGq%TUZt7Fn3y6DY0=F-^Z$N%!GQqcxL{#KM$mOT{K~Z>#=D zyZa2Q6A%+vEIlc7vYaZgh1%ra^-)a*hcTU4Shkz>Nywdsz1oj{Ul~hQp8@>WntBcD zpDF2@eMmYVo`A->55B0<+5a4e|1H0dFa5K@!2Y)1AKcEf@|WMgy*+wQ(tUBGRL&X! zFO-)KkcX=JN0i2sS{1kOBr!dz$scJ!e>{uzVc+C#`AcJ{e~wcky5Y)l#;fC-UyUu) z!)pfd5DyAHG6uP3iPQ1KPH@X+hF37=Bj%TZsGCCv$J2jI3ftiQPRke0%X`n1<B{xVEG_pw zaaKLCpgqeR1#$*X{ZL)1dHQNdI`?JfcjClBb@n>o9V8Crg494a_bF3(v(UZ+Gl=@2 zd}HVFS*p-2+@2xQ@4HfM9Edm)37)B`c{m=paV@YOw;FH!CE$K{h5ueLUmQ8&4)>EK ze}2gZ8DXddr)suuC3Zsl9+=1_i~mS9?v9xV^G(?T6U`Lge|nC6b&DIKXqc^i$#J*= zu%!NMznC`^+jFE~nIHSP(!_JY$%N?eNk?wJN}13@RNcaI?J^%<-4m>5I5gxGMjB%S zcKMIXci}4{=!37j6z+rdC-B$qvgIA>Gu3mnzg-mOX{?$XBI*~mLV(=M{fJHaB)6Cd z{E|Q^e?mA7UyS)s>*OY4L&j}FM_~lHK zY^V?@=B+?o#p*jLj(jLM6W5U4?quI^rytaf-r+Sg2Geh=`#;jD;z~#+TZ932@T-wF zXNZc8pJbZJAw)$rR~{Aj7<_oWp0BnxD}y``x=E7RS#Qd{V$Y~VpGTs#J7$6T)A?e+$`u$z%d(gWQ!l8hfYRGAfXl5!t$x|< zEYb?uhRihwD03)Li#URh6)*7EQ8+ck%%lS%ccCWB0>;kcDv%obA^1cx z0}y#BIfRQx@Bbaf3ky1qixU-6)g(UXciYhJK&DD_mX&|P5@3e9{UTvD{8=;Bxhq}1 z5Hnp_d-boMMeJ`GJiwZ-y*UT}k zyjrgj6y5ciF#DWYKSO>!wK@?`l#U(>4?e*i7OeK%M ztRwThFULsP!hq+bf~^OB*>3m7E8>opeGh`oIF|psk_aA4dJeJ6LDYoogV-!yD0BkH zb+%W;wu}``jcX}3M&9Ke105!IE{}nA({rj}{5U1`C{N&j}T6)n^nYqMUb@ zFCZW0#QA&OGQV$qtZiUD^I2AeLQltgc3AHAIRx^an|L&u>eBy?1DJR{f_Ue@d!!6dRGmuE(p9_vwGKl;D#Q%gQY_-)*4v1opIg zCZAz8$$e*mPDitcDu&H*T;}`KO)o@dXgbMq3VT4pf@N78R2q+BeoP}d^HjLLjqapo zyPL>u{h;*apj~Gesjjb!(~aq~@O_#S3RL9nOF?1j<4)C12ZttvC1sAR-L7m`gx@yj z9svnM`Qvw;uAHrl%>c4O2MLVBkK)gGZUCuLw-0iolC9P%tEc-#oFe^i2Izu z!wj+-b%T*5iI=~2j7`W?GJ5jFjE3x@;0L)>gxi29l|9`x%egfuh7OsA5NdADZ|nNJTQ((wCAX>9 z|3B^OaG&!HuA&|RX`R*YX(XGc_~V90GU}HrEkFC*>X3>bWOi2B=XmcFB;y>s3Gl13 zE#;hZnHc=&E?TkN#Q7b_$MI*J-SyYt^Z-P}^D<1#u^F{e-xvnYf9og`Rg4qo)bu~a z>k{1Jz~;e_+$UJV1%T2tXiwduGoSYlFH~8Zdnsxqw@w#aK~81ie0)?m48Q5}GClfM zV3vhcr4GQ>3s7;}ur{mX_L+bi2qnw{xJ|e$$(l`SzmGN-DpJ(c`7G1un|wnH^cMez zb*V}e_mtL%zy$cRZj|H&DGoxig645h>d-v^W|665U2(<5|JtwK3Tmna*l<->kOM2x zxO`GmYTja@OQ`R8yv8GzxPwy8;j^dW`kDi@)s2oLaI$yHlUk2M_~hZ2Bc{D-wAi(z>cJ_Z zjJ9<*N@|g{zKgAzApEM@i1bkxsf>wq!Tps!ih0j=IE@h-Jw^5nl(tZFPPVZJQI{I^ zjsAGC2d-c{(SEuE*uT}-@uo@y9#Gt&ai!f1f!SA)PI_Hvf4tjUO(4kG|Lyg&%dLB0 zkNYAm8@rY4zDM71ZoZ8F3XIIl%pC6H&)mMtRH$d{`0^+Z>ZMiG=g3xwd-7YhvgFqcvoC*ZYTfHhmgq{7>JeohMIR%#t~}1HqOx_EPDQyLaVTulm+-H~ylr?d ze;7i=p$guTfU_#$!%heJD1yyJWk2xGcp(3CieIsq^R&mF%w|hm$N1OXqZK5j2Mo@g zryq!){ey#|V7SSp7T(Jmh_5!r#FJZ-<&2qrw=N8qQR95fqNFeJ9wCr{o$8sXYP7%E z&+@H?m&Tq)zPj;5&U3SG*N_Y7BKiT5uj9AthEv=#6 zu=*|+EKx=d`LGu5win@(I%Cs?lxp(h zv|^Y+^`Cd1K@26M$sh$v8>zf{$@lg-Mv<gJ%y3b9X_;)cXB$)4Rlya-=n2C7MV{CyGN__4b@4>Rv!V5|O zwngeZx?;@|AU$lp9=t2@njHvh*L?WAh>dZ0d+vKG&aJoO4nP1mV#`bpxEfO_1TR5_ z>9}GxhRqx`-5oawZutJCQR(!2&0l?m7qtiKtL}Nrz2kg7$q%#umM5p1)szE^`p2wC zW*}F_tcC{YKK1~XUXrf=^U3@qvaZjakXtGR_Fu+`7qQG!q9DT>Og}wVTchFcw?6jB z&CP8ykOC+GbM~?V18&I9?zUYhwm_rt&1h)UvNY}~?ZA6ncr$+hSXy?$BZi3yU29=* zTZu8eW$y@2ZOad-mipWK7f`xrzzuC=Nb%b68!0+HwFhk*PbZ%uEcl^fD5~w(9^yZi zR7Ctb#kWOY>^42bUo_hs_nL?65m9cPc7{#^p_^b`Wu&&7i8O<<9PT+>k@snFPKZX+ zW5@Ml`+vykvM2KunoJx4s}XEYTH5(Ca6D?b5>+{RE!A%n@GGk#6|MC<&5CrgnHxolZd zq%QGHVWvE^VOXR29!uCEU+6JaN$zoBnFET*H%J7^xT{D!S>2?xKpvLTP^c05mz+wC zok~%^{P#NuW`f5S0iiD#yJ@Ih!P~BD+{YtyU`uKX2&5T#TbquO&OAcweA&w!UC?00 zo6X&cd_L*z^KhOmeYSp1Qp2F2a2fL#90>eQ>rgYtNWaS7O8-q(^9PFA+}YKeA-$sY ztOF67P)Igrq7Dt4E!qMdEu>Y|*NCG2VT^4RYut7kbzogy4_{rsJ58D*c;Yr!=zw zN2AG(EaO@!*yoRTQ`ok}0&u*SVO;i}NVCo!=VzpNEcfQ~9uK_&j2fR_j)5%|w!k0E z1m|BI5n*sSMiEg%?Xw$bd@N%(1%06nWwdxN;eqOU%FN5l!0N*J)s2}~ypky2JwFE7 zQz3qlwG|&~;{sG}WEk2k{T^GW`$ofwwdh<1?P zF?%r>Q(XqSrQre|>j>AFspw#KW?Yfh^DmqcqQkzpRwQShPNcy1af?`#0BL69w$|qL zQldafX~RQ}8L|mSR&o~1;cXoR#hSGdJzg=c>owM7kbPW+1~V<$+S%;{3Kl~bmDlKE zWDVSBl~(g}k$vn2uEX-J2tpR91}89gwgXKJ^Xg;pL&rFPv$x?Ozc}~PD`7AEdpu%!JoeK)_c#WbG12sH5x0i(mOemCnlio;W_ z^7zG#lM+h*xh5;z4~^*{Qeyx4NmxKjvZN#Okxq^ik0H%%){95KwoMmSm)cvyxEIOt zI+uQteE;Ril~wZGvsSQBC{(_`^6w&65-AOkybJ}L)r5Hz#913F%{q1&zb5P5sGh~? zdE7~aiG7qh?2B#qQF}IuU;BkGKPVG`zT-`^PGFMi7zz96a3ma1Dl69&9fU`_W%#CT zJt8p|%y zfK5cXlA!<#{l!ZO*#Lgy7+Ku`2cC~6QbP+xvf>j({<0f> zxB~cTKND}J+YfUrJ%dWWNXz(ow8)(lkFfc$v6x;I3u8CmtOO9~kq7`(jrl+m0BoT; zyC3cs+@Ij|_SJoQ117HTNC=l=ax$d$#o0ieGsRLOOvfec+ z*X%M|nL46r33WgY6#yQq$!mtRf1^#8FS|NXjGg;-A{)1DQEdDaKzKf*a?(lm?|IV^ zv9ly&^(@c&X=^&dCqeVOE`(E5uUo;m|EneiI-%8nQ09Z&@m<@@bLpmW+9wEh7SYbkjOive-;%tN zXYg}c__t-5h3>fb39Khh>uw+?p#^uz=^XTz+C&Js**~CZv8Ym*asD{yk#U?kQxM$Y zaKh?louz{c=lYfR{328IQ38~@l31_oksx0DK#wMcMsyPNKA|{FY zLpr54V@IRA@xql&83ro=Owed#HgEnSII!DD^#Eo@>P&Y9I8pi}&nFV4p1qjZIX(yE z>VtBptE{M~dgyl{4Jg!X@|D38tAY`@NNgb%YJ9U;rTcAwG`HOC*hyO;E(|~Pe?tudiL<}8l8!TL2&n(3=t^6~{XV>uj#X}PPOZ`l(-Gr!h0m1DpQr0^^Fv=An~ zdGU6S7r8ylT7epb0)Ox~m~Xk#Rx0`XUV(z9?+C5Ik}dP=Q>4YBK}R$!e^ts2JWsxi z`r?4P7&xtj)(hLj)EuW$?xab{I7o}jVi%l+3mK{y1CzfFJ!s6hPVrY2K5{UbwZnHR z%(Td?37XBM8YDRMKxSZEe_6cdV4(atKDyVNKFUQOSa05K-ulXBa)%wX=e>&_=>t|6 zvnvY^RnAGRpFuigb-&g_`6MRcs}=QYF+aKSPb0qg_nt5>B-~WQ~ z^)5MX)U!)dLou5VS8p#lnd9@`PF{^Hv#3Z?N|{NizZjt}5Y?FxB<s;Wn4lYg{hd>AecR=7DlZ)PCv%;%w z+WWywDT3$MxTDvth|K~cEavYisz8qpiu5ELPS@&_w&LIXyyKta{Hzs9m@I)Z(^}Zv2vi%&2ieQi>Ut^ivGDb9+wo+K@aJAfBmT3tc^Q% z7ve1e9LTGbutq5D7~A+AqQeA7yt>zbi8KU%^?)P!dg%i^;Qu*vg0y9Dwa$rXEL*FQ ze7NX7f;|zeDcqNmBrSm`gEcbNUQ<@zZN2Mmx>x1rFGfOpjoTK=689-)UKM@*D9`FA zaSdk*xyvKu4U6vti1-VAMu*EN7HU{iV*3hGQ;Yo%k;gBw)sAj%|Gf$If+T5MeNPTc z@jIHR^}raSVv3Bn*zXdAgNr`>VCM{Ii~J0q^@ForQ!zYfu91vXYGtmVZl>b){d1?h za63ppMh@3Z;lB55+bs+OooJa!Pr1A&VYJ_ zeacL0BIC+rl@pbBynZus))DLH35aoQjBklrxE~8}Y_p=xt$nLjC9T+z?vrraTYpa_ zK^hW{c>%*!vaDaYrK!vHZj0%C{OMU|caF<$bMU{+#`jo>7_s`ZN`HbaJKQ)>;d~Q( z0iao?hcYdhy%opI>dbFaw_N_DUp)y>^`FFed)3zw2bI3e$)|x&GJiv-Pb&N8v?{Oz zOTzVW9<-24f|WbKB=uu* zj?q5E761__=A=?gxVf#h)}FuC}IbT|GTiNWw*|UZviZ& zxy$>}rIY$J%+l?5Wf=J_;f8t_9Gb7%Y}HJKuqKOFo^{}Fbrr-J{{=Y0f+9RB!gl~I zOfl6Uf5v;4P43zP4=TeG2yyJ1;DB9;YHzg+a^r4h#7kF=31xss-?_-Tfvj~>-qlRr zwO4#=|79QgKuo1$tU4Bn_cRR6=sR&;xuAA9w!m*Vw4G>k{Dk3&or{3C-+kYTdQFD# z0R!E#FGg-fQOHLsM(9DoVP{~n8B~tH5;=3v_`*|fK=M1^AJq_rTQ5d6dnNKs!(a{5E4DBFr zN+q0&VR77P|3(Q6!1mn4C__`Kcj7eUgiRhWWe;?2-9e~*8w}i-T*b2&b1{YBrm5i| zxp}NM;uViJZb6CofC4xH*J3tW9)f3N>Av1WYz&xXZ+}y#M|1n zGcMy4-e=TiK;Jo+dII!g_UU})`+sHwh4H1hsNh=ST>K1LRFei4%^ zi0&lTEc5Wmsklrb=}1Pq%)-Z$<(>kZG3lL3FrS@t@9|tuu{td(1gWsfH+$bK@HRAC zWgPMIH5s1R1gPSxk636Wuh2JdOJDXi=-WCU#ltmF3gpaIFFntb z$T>KUKM&bUx|0y?^2csoVML%ZnyDwV-VX@0J@)BHB&}*Fp)MR;Iw;nzPooXaajqf_ z3B;FOj(qCy6(z=fz+~JhTcPP}5)z$$Xx^su8mv}whj~>3aF3;4eK^0fAG@XXW+9S2 z`rwZ6lkrx0#vAR-Q;=aoGiUSQK{j$c`R#ba+HznXCiWHq+ z)@4X?-Oj8HVSjFbUhSMXw@qe3wbl*Qo$O|F3N{pa^kSxgv8p*Gv8`>Usl6v5*)?74 zs_iE_EIZ{%W<~91r#$uPDsX8-=dy6{gSyg!8W$jv*p@1qm`~Dh&~$iOfKb<`ALKVT zc^;0qUF6%x~zsdNzl#xhX!$&yRfe$RK1HMCiNU<{cBjF+=xyhQAT+s4z(9Y|-`1ENIs zw?Q1Td|ck^URWea9uj`FYR_E0p?kDpF#XQo7hR~tM2C}g?hGU|43I5=!q*Kgb7-hp z{T<}!j66hAi%}#Ps19)$W=Kgg*_}rN(`m^|oGRz1|M>M}iWrO4`}cT)l4fV{-f`83 z$yE^Iwl(v&*ydy57vs2&)cm;8|vn*o!RN>aWyDjf%?xBby-ufot~0t@W%- z%~UTW-eG*Ca&|oV9^c2=8mgua2wUe>Q~iSOPN;HWz{VjOkM08-C&-p7&n-sW2P7}S za_tW{6m^65Y+I^0QHK1sD3`#givr#n28qY}+s{;Gz3B;U-{AJqmsm6Q)|CP}TN{iQ zubs-yk|HIH^tnU+Gpj92Ht|WN$?>N^`d1$F*?W$ZA4`td5{VeTuA4H@p>(p@ZK;v> z(QfxcT+~f<&)$Bw*L(_%`G#bA%dM^tLIGTA*E=9VY( z{wy0Hq<|#bNT>;2g#zQ#v{vOI0=pM@}wLX`P6tQ7av0;`x=}CaNR$fji z<&7`D6Tu_u<$cEHU}R+EwcB)@<&G1TA^#WG@SA3*448w;ZC#;O^NLHG5C8rpc;t4^ zcFMiF91w0B1;k<8c=3h-Bfby|==%1NQC5nBE(YOJdJAVvwM9=apc#TUL>io%zLC@? zAW>zSPFF0L+k*l$Z_Tfzsz)_YOWrfwyJmlPGxtSRBaEx;&iwrq;i>z9mN$WZ$F_wT zSfDYcq1%CYKD;#9AcH*#bQv#pL8m!{_M#CP(fM~K?{nSZ*vcgfhv%tF zjn$I4kk2a5j#AXGd{(Cw9-!pj-K_V$>50!JzKP1eR?(`k)8YX_z%arkeFq<~KVJNO zU&sFlspC1>FvYS9J2Gt8}W$?);iJI1XJ8%DQ7Qaquxw0+;dN`U0-E zgppuf%;I_XvF|c%swwp9_nME;a{s7GQN~HsFopO2%=94s?K8o+&$2> z(0&Z@KOoaRXscN$jxG+|_W^}ET3icmtl%-^jtrMuYkA=C_V%S;l2zdScc4?b%V6Ud zQr(j}-a~=UL->0~rUmiyAy%Dt@ ze=h&}CSEssHuQfr7PP3j&mI?0^x7cHcRul#qAbBaSe;cG?TfAXv}@N@kybnM3jKtj zb$0M?oBRHm(*y0Nni>*Mv#8(l>-b8UacvXm3}w;OxgBr`SBI z%cb2c`&TP7@A+!8;Sds}G_Do5zqa;TX4;h3ayXRC%91AUEdgMD*&}1`iuxXAm~0vJ zc;5gWE=zAhR#QT0o1?a+xlt=}&H!ZjO503@=|UClzN;?O01y>KmT9SEN7AzbAKMUrk~ZZ6E`?59YFeQANUq-Lf?~X$2vy@5}8pylDuePqMu-8t^mJw zBn&>jlw5&PL;P1Dn+yIqGXi34C-&_G{nOV63O}yak(oy;{*+$+RRGybp#a-(b09s1 z0<_@aw&M6J616~dNL|y)@<7wu=LeLaf$W{NH+OdW>q7yY1ajNI?e?6Ub(=LZzZht~tUQ{XWE&{BC60dYbGM;u>U2difC^l@ zud%};veW7q{(VpO&DLD6HNVY3^;jLQ3wCWuaRgi$$JQw4(o4lg_n%nflNk^QYS3VV zvPU(zfbE7qWZRqLy|vFxl1;hSBRNfh;$OLn*RaDP!XbwHmv5FXFm5{hRTtTO9No9V z*VG^8siOv8im8KgLI7W6$(dd<3VFICJ>=5$keT=GD*y-vwKTJ1~Ui-Q~7ozi+3N}Y7II_8e%Zq|*)U?@O{g#@ z$1n-T2~LXnXaIIowb663AFIcGw*CYeAlBtSB43=!>XfjNjf+WGTTESBCws{~gcYq- zr(VoilSwUw2}@^2qEHDQ)&}>~>lSdOR@ZJ=O%qxd^2F_M64w6qa?r`gW=^+o2%D_5 zG^*-eVt+o&umE}bOj3V0fN#Sz>`6W4gkz9ns!54J&!FxndVA=iuj<1~V76N-xGKvd z;@uI0Bz@4N4IzaKiH*@&&%;9>J+V1*A1S7MYml3w)Qg}}Eh-KJQdE%(d=%b}t=C3q zpWl>U{aP3^Oc6ZcUS7bj8sf8(LK|F`tQKnUVSrZXtOX42NY@}YJQq;4=O>4?VoY-? z_Of_w{}rg5h`zEzQJAt2HyX;BLb~*hO6c{$51n4t*&>PUr(Y7Xeq9_UK7gRC zoa)A(^-cRyj8?jw&g+>N1z;6jtXCv!Cy8E_s1m;( zsi@kb28VqZP97&gxb1I!rgDLMy?o`7==wHTE%1w3Y};ELd~&MO1~dGca>LIV8IliAMMt%tnVdM{5TjG#hDSEI7#ymv_wXIUp@ngX)E0U4B7H>g;Eh;4s-?CrY zHzG?LN_zf=-E8RLyf&jt<|i22+@z;Ol-pwnNn?18m&&voj;lO8sO?|y?9|B`s9fumDhmwsF?^I% z>^mH_R-WZ<*;^^iRb8T%yz+gF;_OxH{?pKHbF#1fPKnU)$dXlUr}Ede(-|?z4r{w9 zOB!k>6DW=5)^b@r>u!AoEPChrHB78p{KW*1T0`ABM_)uiq!zd6Qq*(_Th}ryA?Hli zn*!vkszb_(QPQNPtezuIrXW3`1}<(Z;v{0yJ(DHL_GCP!Qwv3y+1%MNOvNn~lpPpcf5xabCn%{Iyq_8GzP>BWI%MPaDNWv_V;gjpi}evvj_TTNtDrN{vb>C zDf|Wewe*##VFFjnVHHgD7dl||uzs^)f^X+q*`b?PzqcVZVB}G8pd7I(X6AABL+n73 zXDSM}VrF>_d#Hm(VtIboQg@)ZBg|-V{^WZ7UUk8eQ;FBh%ssz>D5E-;u>lR5Kl=%u z-=6ZkGI+SMnqebi`6X*|Osge*YT1oTf3$S>Q8)_H(?fDnsCCP65@jtyXW9&3w8mYy zK2U$y+`(q&D9FDk3X(z zh2msHg1Ee0e)8X(F%FbqyH9{ z(^$a%&L-X`&@%Jd{!g9m!HbpIs#S75J<5|-?>?l5QbfTE9A=G_!hjCs)6Q!&G>vzi zs*bK;C30saFl7BH0Y6k#mFHBPFA8X|s2u)3rrtfC$^QQXSKPYYh3@*?Nl4w@(m{xF zKBN=nSSsYOB$mUF8O>~zQr(KW6UkwnkR0Z?HM5OWWNc1tY_`>{8D6O_>t6GpP8|S@e+WZuIyFP^OaSPASwRO{-$kAI|BnU*=ZC zGogIcQ7UU##jS{uk@(*4;8RJmU0&pyPHei>gmW@mCm<`8(NFrTw87N*stM6!q1(c| zxeQQ@Es3|r$aU;!@g=)8mj`z9|8-oKBqKhBl<$qlwm`<5@;FY+h2C`i*`7zdXqLe4 zySiT1zu|&tUZD?43xFeJv1BK|UQ|^ngd}eO$0-u`&~WQM32?6P3D&wju#t-0Ys59^ z@wH$1;_Y|a$?;i5_*SkxjJrJ_vQFdcV0r5BsexzSoDn<`_w5gzmj(4QR6_`|3qH_v zmCZAaqdK0X=xG@HY<1P>)Y+%oNqivG&}bZ;Lr%>Mf5xOm_^j(sb?U!e18;Cc`<0iZ zvG@`ABW=0Yz|T@0>crr7&Uz2;o-f{30!u5kr{X4K!JiZSeyBZqW@1=wYtX#A%xY&; z<>M&-hiOMYFEUSPI-Q<+IRZfwT7X_QRRftRgoX0e{UfA}hj%81q={1TM-H#a&@rco z`9A4Y{Ysz~U!8gd3TY-1qf5Mn7882vT7!S}ehZ~64?l9Ui?F}29ZY1)p%$r9kkg~7 z%-xjzT@%F-95%h9h*!M6A8{q5z%M*RUu(iFFU%7i9Lr;D+0+W^2ZOc>e?DVQoIt4(Bu*1(wP9stFkHca zF>U=59*&z{>ZI5u`SDd#RxB*PsNzL4Z`si&>*gOD=gmB4HQaG=-{aLx=bbb0G3gOb zUU@3xkF_xhW#IbiWmUfuPNLIRmmv#fbHe?Njc*0t{F&|w3m~>uQ^4M$MQunHq7>_! zl;u3vfttFXCCxV!^~|ABIl4>ED4VR=7ok=oJxlbEqhIW~huXqJg0L@KK?++t58WZv z{Q>YbrB4~tbDe2-i5rhK`WxzMRs@ac`2$hO6I3$)2GNBVD>&`5xM&T-9L?j_tyF?}Jb<(D1HnQ!t`#IM?)uGWE zoeO)j!~(7|)4!hkNX$N9@Gqo8m76p5+3{{v~S843zMw7Q|CE`egbsC1z8cH~&D_=d#a4JQ@vz6;rx zs(jMbU85fKv!s6X`m+p^_j1US^}c}(+a@kB&PYS{k6B160^W_L zJHNB&IgcgFj*f+WFXgAGhMa%Bu3LZ+)b&KFK)sHLe8K3)1U}>1)Y6F#9p_r|naY*u z&f8S1l!R^xS8`}T4KYTXcK+%rBMmn!R|g8;wuNKf3NeRM_YT{IXnVxB`AXwieU4dc zrx2`0DAhek0fN}q^Cx0O^-7J|L$QiDoOhLvlFlH63xUTFFUPs(Nt^A$U@&{H`lUO; zq>d2aA-p@k<8BB?V2`;DOyQi8dB#?K+`ly9iYg6sZy#%f!&NR1{FUG?Xb{Ru8dN0f zIZ>jp%aFC`OtsdW=iv%7>b^L=RqmNxAyTLsSuF<;V|GK+!lZx#bXrzUnh_lL;GI|% zD>DF|LZMcX6diPMzGyTlza_&ZrhOd67L9H&{qdaCDK4qee%O{|>9s@%iwnIKTemSwb>GD&d<0zz}HbNZggHoIV70E4S0n+XT ztkoKgqPMz1S+5O^)akmIG1Z_ZFU{i4QUQ0NB!(Sr8mEyxnPZ&+{7j8YpHKff_I74t z{hM!~S{mWfCo$M>-A$a;eE*3T0!({V>e&PudVTojja*Wrogh}9UIA<2f21&B*rH*=n zUzW{|V`xpNJvjx@1h6rGgoQpr8XwJTN61d)4<#_9zuemWDNID0>lr<-y?Yv&6+_hd z-T1G_Wfo~AfQCZq-4yOaYacNg9IW^oew`3_va*>gCbFDHF_|a>dz-CbncwKn*M%5 z>}q+#m&j!=I&j%^si8ycQ^}}xReMFaze@zE)Z+WU9fe(?7%B&!zc`C>Ph&(8{8SHtciqvs|L4A^nI-nl4;c zcq*x`EGJgax1+RiDCt6uVja?yCX;7aK9TtOputrZu)z+g0*;_5fY~ZLit%^FkNL7) zQ<)^a3v@anv3>G}Vj1fz#X>5>u{b40)o^xQO06&pwz2aq_6(gHyxc~4vgVaDIjRn> zJ{PLkPu49z6oribI^*xBO#27=y)~T9eG@VMsfHUnoNuIKjo#O@Uzpw|OnI?|nQs>g z%H~{>Iz5fTaNPb!RL1mWkX8mg$e2+$Q*&V;dH`WHGtat;3#V3U{|F)~Xw-*Z?+^{P`{8pv2l>m;U}6OZZDJ2DPGcz0+L# zD-J0l>G#Xh`VkMvVJ!sk*0;ayfT_(hmxnHNE`3N<4*c=2+Z!Jkp4c-nKQno-CpkQg z9EE&w(qQbKbIKBvRjwnTK0JZhHIilrH|P5;=mqG|Av=Zm^$k4X{H zCd64dumNnTsWvId#D(T!YbHhcFDQda<;vMBgH7d~9h@%RSp>v#Z=lG@Hp%eKi+%L z^5=4v+D@_End1freXa(RxuDaIRg}lk{H=m;>2ip+&Q`2%>ee<0iF?fz&D~XNtPY_+ zM{O1xYOSV0emXlowSn;MdDmt~)u^)EW4C=~>a~U&YgfNGor zNTWPpaf+0!!u)eZ-`mh9A$RjSz#Lq=?tn<>^?puF-4})3i{ORz|&U^T$J3ZFb z5HfS48y^~b$}OG>#hH8GpeG;gP%b-i5I$hVre?gv*1k9j#bOs1^S!SMV4gDj4H}nb zKBu=wlK9(a)3K2ZThqJ+*A#4u$vsg`9zOpz{Tc~8MYyJ|`;=nMGdVQy%|SaiT=vbP zd+1lB`!m=EU$!TQ;I% z(|vsMbj&(u+n2bK5R*X^oJVp52#q|5}X*^P4MM9vYczD~wkE zR-_%dq@kgiKu|QKEWG~-7CB5`R^EDORt6DP78mFJZ&O6E-?CAIlf$&L3CA9JW#5N- zw%5h?U!4SAJ+f36|7y7;D11aWpo~JrWYvcS94s!pHWt0zDy8W&oi=gS@@&b0)E{16 z8)KxBM;l(z4&tW5!NnT*44tBe8$cDTmTpHDCQ$@w#v{c4eHfcBw*H3>clh#4+Hg9-^JeX3S z)0THV@t=(y;r?zHZvJ5MUB=0dpwPPs;y$Y5IqIuF71!n_jl7@B6Sy;irb8%}NlU~= z;={%7V=NW#&a*DT_RKod*j=w)X&(DEWxiDWZ}$t5JJipW3#|ydj<(L3Bg`*yekPC0 z#9t6YD=*%Vk1d5>Kqh_x(D>XYEbXAH>YlAOs^~%1zn^S=e3WAPUFJ6hfOHA<5rI*;MuQ3;RgM3*(*KcW(=)!tD;H}_0gq!&z*Yl zKsopCMckhrVj*o|ju8>KMiJPPfjvmByfS$6d=irNk3JjN;d^^U+sx)5PFi!v+u19j!dn+i!vD+`Q}Oq;r{l4a(k262O+OYE7$yI{Kr1qXREmqVML`2 z^if?TMbS?d(^N_B?oLijV_M2W*HO#u!Lunlk7Da1QV*KGq5xsLf420{%ANE+KtWlrj)uFB+w3v5;kd<%Mslj;&*Pv zlP~KC{9rDj6PdQ>0y;f%oJ(YdaqR(01`c)jt}Icuw)tnTBuE}ky7FdqBc=^@xZQ8J zO1YL9N)=ovOB?R6oc1qS4Ed+aID?CmZ^*OdUw7xF-`Q}EUKHgdIn2Vxkez1gdpe43 z2lB1h>4PhBmy7ByeIK^F=;m28vFEtq_ZNZYAwl_Ki~X8v3G<-I^$^O4)s8=-L;G(y zHLEX>dA_!NWj23J{3)mybC4fh;Q}En1cj|; z$GE-8@*;$%1|ZUO=iL9vomrp!CeEZ{-1h3qQP$dcqic|;F!%3Afd5bAOg{L>C@0aX zp5o`cXiecL`bXX3arZ1++3&WiV@v0sU`1`@jBlA+7;+Qg9H}RS<9(-v`(KY;4dG7f zTSJ-}{l_d%du>EIVUkF~{CV+YxngDqiDzq@Gcw%d8VLpZWC!b;Yx(ERVd#KQLJ<7T zc=DGoJgj8BtSI1Jhn-Jwq!X!ti}I9VXO@ln#5$hw?jWmKhlt9C46AcquJaR;(ffHD z@(=3_&KVqXy1&U!)|*Z+-~qvWx9^w6a#63`@5%Z|aX4wiCP|lmp~mck^*axzf`v=rdX zgtV{9w=?VSZJrCG9f-So?R~pAEO9R)$w8VB9nzaVvaKJzOvCquT}bUS)CsK+D3W!v zxug5yrJ~HWWCVrj77?e^gC0zt8n_hgsAuCpWyLQjSFAU=vM=QZ%<$Lxmiq}oeTBmQ zNng{F_D*L^G4#3Lcnj&Gfq-L6UjLF`o4X-Vy%NsOYi0n$Y)U^v`A|rW$6v~vB_^xN zC_c#^(7U{-szo~+IYn=a zQRp*)EzWqE$QLui_#JTf>J1XtvwVNodbs+dCZD3PR_ug5k~bev=2wrqMfJF~;eA?O zKsQvE32m<6;h68ZhjU^NG(*x-ZCN|Iuyk9|mf%P_OI|O}{5!)~RN*#%+6y5ap#-82 zgrJbzFVFLzjGzPef}H!6&zDQ0)zK`zqV!{eq)?JjrV4*UKT|I*bQa~f1PS(1oS@5N zNv5^1><@=Ugm-H+vb~(6m}SFNj?>Nwke0CiHPb+>Wy`%o10YuQ!Y`Et$7$@TV!OG#afLUHN*QU+*ZZ7cB=`03?j3Cx}YinU(X~w=$L1Xb9{Pw5y1Ah&uSN)aQeW%@P4$)s`c)` zh{2xGBaL1?iya&cG~=dr%Y742M;5iSPyY0`(v^$&=YM(JP)L1l<139HD`fU8Cz`NA z&$Is*V*{e-UqeyqUnb-ap6z}fxhnH)=Z249zpCBB?cOplk z$?YN6mS=bqJGi3eim*K$N{apy@jue#9ydH*j9hpY)F)TvQDmI3PQt$rz=sOvGva2~ zPWp{+{w;z(aH65+RoX*TAMiBB`G|*Xd4uCw%e7gR7UD2ZTf)VS! zL{?bkWSZU2$|z0gx;ATRuoyG=R-TD7N$t2|(jB(J1;r{ap8;{@mMAFxi@e93@Z$(+ z)bps?4$vO0HewFajpfMMwxd25#(*(Wl8BO`spQYMmmR4#&o9n7|40c*q88hOHL#pT z4yvcj;6DGRBIAC3mXrz?k;^dpVpRfb@iw9qM$U-Db(HWw`5|MDFW);+*eFmmxSUQCMaw-9A!-@%Tgme>}`ahVno2k0LJJMu|!i zqBuBMsNmqEsCPu&kO+iETckS%npoDwiW{$*B4%_&F2R#_fBzHhh!?JitNX4M+axUy z>_0trY|RJdJfa5rH;u7W?8a?-DZb6Mk+ELG-q1$>HactZ~JhC zeFBiBL-Xcm8D|6sXhL>NbqR)veTJQmj7E%*N~L`lbJGa%qGpEv&=$8hzNxE-ft^q; zg&2;SzCl{()v`}=F#mNh6wsRo^p-op<6{o)p`=ERAiPL~xnR~fFU;n~;c51dG> z7IK}4j{gk5aCz`u=g&OYNI7N1qXU#3_WmQw;J4Sdif8HN;oHVp2HJHF!q>wuG7(@$ z`4Qn6zDNF1ql+2JAOVsBR)2mR=LwG7Gy?Ah7WJS=YqBf9Uq$t}J(K$BC$|P`ibi=4EI}L@AyA2O3w@e_ z$uut=BWHcJQPgK)g`W>8iw^g9tS-ol%EJZ+^_|wuf=LR2mx#M7ykct%+F}Z2M1Kwm zzZx=|dFO2U`ththB5ex-WI;dZh?W~DY-7iGCNs1*<8((>1{IjEre0E$?7MGy7|*KW zr_M~pHOaUO9~_-Edj8%lSuy!~{M&?9cGPX?=Ip3rgo5c8*h*a%w(Y4fOI$MLRiAWl z;6jk+%kr!ytUYqVwoyT)UbEwNjq&$&A1y*U%qAWzc<^fIj%Ic@Mbxg*_-;it0cx1s zPDqQw??&v^>f}EYa0K6PRDO0`uVMVG*jZhqM7sygqeta9*|J=Jenax1x}P762r|$L z&i!ikB&DXhg4y{eLSCq?ksTuRz9fGVRG2Ngm8w7cwIciT1a=|=+9DmMoQ}$>VqaJ8 z_uz+|?5BIUM$X=$oB-OQRyRXW7+>Ap%VM3+nJ~@#sxo{b)fwqe)$uev6uy?uD*tWx z9!cwadIctOsnjjyV}PLUo7%S8XurpN4Zr50#OSnVF8)0?C9-p@2eUWYUn7RCYAQ&w zpo9&DsM@`)N)I9dzBClU`}6Lto&sbF6u<=;J_o8>$PC|gl%Sbk|AFmxa9gtoYu6g* zKRp#3OAohtyHzndW%#YK)LC2=R#Uw1TSZ&`9b)jep&4tHN!BuZQ?}`0YZwKbIFmc3 zsbOrjMX~l@d{MCoD&Ov$uo3y4Fww~6=Z;qZP03c2*=JLliKAwp1~4;6^!OB!oWG=U zoe;}x``tq7ly;^$SlEl`ko%BeFQt{#Ep!ff?NI*bk1C&SX>iX)+qedp-bH`6NoWK4 zS}&B}y-;G!urWZW*Jv2;?>q7dq`p{PKQ((oN&VH+n<+e5WQ86o(g8BR#c>qd8p+Dv z(xOaistTElK2S71JL}adwxUZD@sgq#07UI~zKeQlV{v8t&a3$?;xUJ9$kmzwa5x1i zc)ILZvHgD)q;bqIQA6YDdEJ)<0NCy(Xw^CHe@vaLkG%A@Nj*&2GWt!G9Fq0ru&q;D z0YFS{I?1t|WX?27kRz-NK%B^3yM8&wNiO%3EoQ+1MPx3!h@ z!==C^K<S)+=qMTVmE|GzJ)I?bTfQD;qhv^!1qe%S`(L{3uY; zT$M;hy3g02kD$sGdQ%}vyhh{F>6_EGoj*RP8?=OJr{>ZoD1T{p?<2W3_-?mwzR-&MP5}TrgfkWVK0BK$aedrU21MvOTWj+re zw6YyvAIzM4Tw{|MMsS7|Z4-q?X zrst-C5dzzHVC!RIu0s<_nV zlI$|>@pfu7zyWkVdMK#uN!qrbu`+d$T_*6mJq(K2j;qW%zIHfTOb$^N z*~7ZOiL)9!^4b0_bR-Ip7!HiJ*wXz zOTPG-C#1cbbzY%4xev-i>4fxUCs9O+)yS2|h7;PWH^)I*wmRU}-ttQPIepmdwX^P= zVAJtcwZ?C07(ge}JUmST>YRQCWam!)oxrt+hf8*@c@)pdI#r$ds&eE=l7pgg6A7Ks z_eMJN@L{%i)4%GP0rq;W+z|fv58{DGh0pUoTKB8!1+xZDXr~g&SesGl0W=;T=1}P5 zt9+7glJ3s#_mp}c^)MCo6NJ$8!;|Q(`!%)d1je{=&@S(};9oQxE+i27(|70xelh%Y zAKw@Ft&=Uh6u!(*I%;}x>Z-S+Utu21yh=dCJ+OeB2fp0-!!C1aR$H%lb$@jX)8jX< z(E#LYOdNk^YAD4Q z!lDKMN!*31Q02UsqSk2w!IuqrsvJ;Rds;LQl4**N?@mG@&4r!NfvR(%IzffWi1LHd zg$F$O7k)XYK_|)EQCkvvY9n%rmCDxTWuyQXoMQp9W6otozMSAXH(^2?+h48;?%SPZ zGfqU)tTndg{+BDG8zr~)Cbp|`XHH6u4E}fXp`K>!Q~zD+>(Cw#Om(FDt1+3~oT=q_ z(Jkjz=f&z*9u=TB2_RwS!~FF|Za;6>^eJBvEcdY7`<*;bg4`u56|UvchwmitjJW4i zm~uPjZu8yG0Qe07eKU;zlXbATPR)ynH1L1zw)r^u+uL|OCl!{B46+67758~tiS){V zq$s7Jf`{O4Jx;4(qmg`>0-Wq_@X0Tg>2R`CQf~Km#+mrT_^hlJp*a08p?ckRH*?4d zfL)1dw}=8ZmUN~=Ln1a)T9mRTu{@1kS^UJVxxPH@Zx6xeBfVDk=1+K3JNAy)sLwXb z12~>7?hpZ8wk-exRGWAyjES)MnM!QLa;261(9fAlEbL*S6(p*%0{}^xyBHm!e}_+& zFm_A*3wuhF+ONGGfV9`Y2~StjA4{7OqNWCv!V$>nlKGcZC=mh<39^jH_kg)DoN754 z{zg|Jo<1c~UtGO#e-dglLKsLp%w0Fmb%dX$-MGn@!nABVeE35gmYQ0H6OVS)42u2)B%6x$1& z#BD6s?Q{LksFC23v>r_vjCIupLVwJMK1!&dEh1QDDc|O%{O_9_3g0fuxGYV3&|f;U znAOK%RkewY^kJ-vn;g)5^LPS%Ny@v-9QD{<-EVjoZnt;Dv8I+b;SxpSU`tBGE}R2_ z^=PJDVRNfQrrUp#4ou5Gd?#dRN}FcCJ!1PEhG=o9sm zlm}0u`4=qz%!;9TP_~7WI(%?MaPkZ=x5?8GU-M^Vr$e!|vN2lIU7)$C@~jk0cw;l?&0!= zMKz^7%NY%CT@yT;w!%|aBh(fE@v`C6s;JH-`h)DyQ}fiz4as>PV)GRjT~QPx(Zb+o zb<2^P8jE4umTk8oM>pQST?gK3*yQ$#VIH=F=Vdr$^x;QeGZn43wPbeX;g6duWMa$8 zXpru6G^D&O@Pw1i>xwwy+%rAZ*kFkzrd~b$g)NC0Kb#sR%HE-@v&A{xJrW*qbeoO( z6SZCMOKxq0C68v)GB?qj<$c5((GFmA)X~8A`{P13MKrxQFFTyRI%qzb%K|NLa zsjs63UH2;YTC1k{)IZs(n&f-?+jlO9TqruT?pUwjj`*jhM(~@=o;j1NSGSgcL;a^2 zO;=v9kPd%Y)Td>*J$fP<&yc zv+R|qjCXUw0@2z6Dez;2Pf?zCw&~WOnJe zOX6SLO#M}%9`0Y??G3Lug=8!G6m?K{8bmR%U-SqzqG{4v@TH4qazKHh`}YMMo%{c) zY4<+lCMGTz$@!&Hh_b`)ekVORw(t=_5dZZW#kb#XIWmX(cfkCmEV$*%xxUR;Ne?+@nC2=!J$Ur}?`MlTNUN-2Y|GrJzjNl!t|ha~SK?)9 z$a~YxeaS-#%ZlGfPa`4@gd*VUpCsR}+toZbk>;ujp6MN08V#i5Uuq45+@e_6%x@e+ zO6{6uxT}ZS?5+x16Oi9ux;)?%cchQwig*_JW!cyKVc|~2{x!Sc4SKoOZT;QpE5HnU z?kHK1?ILb!v!zL?=@t7t;%bA4Gf(BDmO-JL@u#G1&R4%yZuTYZH=jPB_$Z56jp#sD zSe()ECwApp(G2nPvdxtTu6zjv>HO|b@YanG>8d$DhT>ymaa#c{WHW+)vx1aUV`D=$Dr#i3tUuwW0g4!RO>BNCVk+B*``2`h#^gw`3h6ST3@$y%B~?~wg7yyarS z7p=K!OB)bj#c?;H_dl5CnT){azx#^MEa^M$V5{+?uDQqyKA9 zz=v)lLla@dF1E-nh@7+Z`16W0S=E*js`y5sh-V7^L1{XZu7hTr0~FPxFumEp$10uf zh|uzxVg0HwgDk;7G!mGYTryZjoMB-4yEyN!y3#z32ZJ>m&q1;-BU;iggYa3)y?s+? zoc5?j_W0^dJ{V+4#TZP>PIt@aku?2G2=_*D>G#fIiOpdHs6!I#ey$O>48MZ!856Pi70h3|O zfuy1@wKx=lawcbQuA>T&Z=7he6@B=iB5`~2!G^D6MYf1`&3(H6`1R>jm;wsuVK{v6 zDL#Lds<}kS^mH5E^a)L6)^xvWQdM>&t*(dM26J+#N<^G))dFYnZl;hUf-|N<_1(;C z_}jTDZOTWJxepfDmNjkh!v_@o=C{qx}kYWX^@WUc~=GhaYIoG zGbPn^*vIY3oXzaOzjAIos(Ak+a&(8>IEkPbJt2acGewvFFMg6+>)k}}Q*@6qr3j3> zmucY>Xjo6E!s2y-$MTlf{+=Q81god*sO8P}MJPidYCEiareF}|7Ts$T_gq;PjgIha zp!sFpS$Z`Nq{@^9Anh@Zk02+&#VpYZV?gA(m}O^`u0qb`kw;wKsrC=_)_QKsuKY_g z=6)6uDqBeEkQ@W1K*8!1_-fw+Va~P*947;+bI~*{Ret-ldG^PIB@CtXK<_0tL0Nr% zZaDX7RrnixpEJ67!$o?dVC%{73rUXoqYLXal3TAFL2sx^{S(DKwUJ}p6|1O#zcspr z9ewSyH`L^Vz^m3H-E-Gnx1vQ7P`C5-yRnr^rIwnVCOWSu)pXnGiK{7>XUh&62(d{3 z8&cdTc><_s`prDJM6tOR63`(SInaLk+S|o*p^-Y#H25Tdb?p~~Ccs<`SKbjSX7U~w zT<$@maO0UQlY!pe#Tf7E@;#IY;;gUjP1&6gu&%;=(+mEqa@dtDOYBEW1S+df2)NF| zP>)YN%$hy0QzEg?H?|5%5&=2gmy%KN#W)?j?StO`-KYGw8^X{iO{2}UG!Y=%W6TX*|EF?j}2eFy3BV z2j_kqm^U}wKT?07Db!tZhyNZ)S1rrny1_1Lg!U3+VoP7Yqp0cp#8L#RZ zTT%!sLdcsNf*MADvFI+O&2}*pYsNizN9TOJh{lPop3sRZNNx7Bz9fy({W~6Gn;|xxsbGm2?c=WD_ItgTwgasDtVNH9GJ;JIu@qH(i<_=K zU0ibZ%Vn>Ec6TmcS(5NpcHul;Oi2D^pgev!n*Sa7o}>Q}Xn=icP0G(tHC@+nsBawj zojk@2)a?RIo_i2Ra=l==WCEBvFNAEjTqDXe<3A1Y*Jc?hM*H)OvA$@3s}D>+JQR3V zG4}_syRZFM2tr+*hSAqo*eWmUHcW^d_J0$UM?spp>ClPM_3em)(v=wN_RixrjJ6PnFpf~d^yv7 zr&4FwY8TQcCERN{ew3tT_yYyVIP_+X9>#C6Q98OJ^ZZbWJ)zcJ3z=o~8QY(6YR}#4mojfa#N+c3d$anA;j@c@r1tQ@H8c9fHdQ`& zvf`d-du!Rf8AjYw1iie+Rv`M;+%Ou|*8w1MdT+)3>3-F7fOi;*b15+&(Q7ej%a3=h zRL!`>-&HcgFvVvDufh@GzCg)xg(5{`ZLAKK{=A)j1qomWrA) zMZ^0$Cn%DEhs%c2-r`8aGH=OxFxMFw@dvwW;3B_O=L`)HHA1j(gs+d)E8K%h?Io+C z_KrVBx_1RZn#t?>J+M$*`WP(cA$E)tC~{0pSs4{8A}fsv6)rzlaF*aE z%`w3O7cudA``P^4+Zp(u&akIYby^AZQ97!$R{`0{I?JC)FH;QsUzBO+p|w7W0QRzF zzPotfx*O7DF%uE6ymZZwETpd5xk}5kROj$!ciohs%uj$^K#>8IimBX!2sU{`aYR~x zJcCi9ZlOM@Cz-~b+@L=G( z|8Aq~S7vArxkVrNtb|ogtL0v&tTWPhs=unr1B%^0t@5g-7?G{NJ&U{&&@%Z*RHTP- zRxN$~x}-%jyS@qtQx2_eMlHb564><>{qEYdr1DsTvxD2Fo7>|#?tdIDdbs3cs~UH1 z7o3)#r5-?+7&b<{=Q#6v54^Z!Hl_m9aGqRGXW9cmlL9|buqw++C&CCVt}ucvJ8U;a zntymz)6zc@ftoePV9HSkUc~C;QpBg!H;~wd2h8Xbi3@dJkHQQ-2VK0TEMql zebjS9lW?~v#7LJT&-hTl5mDOfd>l;We;33?2l6W7_OQo7MA8`sOW=cVrq(pG1M(kp zCU^1%NIrLF>tz$HdUZPlrZP4vA?F^L!b7W^VSE%qd7dpZYG`i)l-OVN- zLgI1eBiKHUk+@LaSx6MnV6=8C1d3iUvADJIR|p`eSKmX7W|I zNcr8^K^(2Jz!V8{srWR@-Uxfj!9jnMrNjVbRp#ET{L+i6gm~@0y&df$$c=e5@ycjt zhR-@q50iTB*`za#u|qBIwGp#U^gVTYwr%1^)BM*5YiHJEA3NAQ_j+ClHV4H40rvHH z>et)oG*N*CG}cWc`RXtgP1@H+KJ5-Ak_KUxF(6<+2hg!mxlCc8BFAXCB+2RYm;rj; zGl+jel16@OIU=oy_B?aTdHMb2B{h$GSi>50v14`R%r@H_2|Bz|d-`fnB1Gqg|Hd&O zTSkzU1-9Wgxw+>x`I~=+3x5tH2WG{VdnkEA!0_i1A^Qf{^IvD`xOsmK3cbk{WG+kt zo9Rxt&W@z?ufD;-??1)eRjeva>t1oC{ktg>fENdLj7i`LEcR2-WA5aQ?f5GM7AvTG zSUNu*sPb*m=nOdSbM_Ll)GwRO-ZvW{twj7Ny(ui3*#Y zserW&2N_B24KpdbWSJvX0?TUun>=5@LUp7E%b6r;b7}To8-WWKkbviNrhr;Ekg;=v8~=^Xm4Ca@_ZnhGutMdYI`Y&9n# z@w#s=2#9(c2e*H1NDsB;{=La73yw?JD?X}ywY2OyivC=U;?c15MI24e-^F^wO=oI4 z9XIKGt~46Wj5huLF0&EPM|B8gml5F_8sfIg-_NrZG?R@d0rg(zy&19^V?dAv5x~_L$d&=ethX;!QonWXriqYlw|R3@UeSIg2omWN2%xo_y1(r z+k;Wd2L_k~^$Gcgw$lLubjJ~s)wc8Z|mlJ8Mw{qcbHyZ<^0 znC19?aLQq3W&77G0j?~Vj+ysXCUDz|7%g>$wcPeaCIxDtel(+jug7t0Cgs~m6&q#e z!M^poJd)NoEiT!fPL9xxEvTaubiX{P*sQVR0w#<;rz)-4xa_XrtRHYz57n+Br4BAY4i-BvfiS?;w!u zvXaV8(75!X_4FHUzF0^aO^O)iPp4%HIW+2v` z8VgKUsd~E3|YiT`GeblshD znh6KxQ0F9o@YMC?;`^K*syR#o*qs+PU1da(2bAwtqaQ1W;Sjr8E)q2|Q5Ze(rJgnx zN)?glz&?A^k?&s)Rs)p?CmYKAf<&jZ$&{QKF%>rQ0^r{@mWrkAw>L~^D}M!s)U6!g zMwCiK*TY-j{2Pnc=W5-rx~T=~M#p-w3-FJPMK;zfA@|v|TZE#B@n2EyyeItEo?H#A zMPGwo8#rim8gF{$fKr(7u&c9bkIv38K#*|gj<@dnB<~%U>ExN^CO|Hjrh8EY5Du}x zcX?e*pNSs${y`Np2}p!QHHHp*x!p49Eg&#c_DyH95tbGse|>dU^A}e4|NbC)spym3 zvP=Of1w{9Sv4GYqUGsquTO4~f-{V5zjhbiFR9UeESA4KIA%50s z=Nt?HH3F1S<5&-Y@AS$Cm9o4UG*mx%YtWsEC(qVnZgnx9t31gI$+(0NFg2Vh=#MvY zf|W6#5%#4Pwvh{gFje1jOWoY74aXZd3X$l^%A)-}SLygBaPm$08%5P=_SDwZn660p zIsIjwZYmJ>#)d{5IWy+U_$i*MWnDyEqI&B1YP~eSS*Gc3FF%VX1O0b^c^+`kTxuHS zmQRk)kZ~FoKH9@?Pj1+^nj)PRe!%5@TC8Q{H;>-4^QO*<_w!5tKZ5z#AG8uZP=df7 zn7^n=7b;MnQhWamUv;mAMX)ji2h3Iut})W_n^-IfbrLv}2~?K|`0~s^$^XONe+D#_ zZEwJMRFq*XcwsCEf{p?fiUR^tGvJJ%NKup`B`5;YkH3W%Swus)z7gM% z@FDq5k}lKGvtypxR#{LmsqXB`o>47md$5%x@$;4o?1!wH3Dk~=b$dMy{Sa#7L~&Wv za^``X{H3IEVLotcWRvrWwY|NeO%XHEALnL5i^)^5N}~DW%E1lBSGoWsyaLh}gR~J& zt*<9q$XuG`mS}bQAKR= zOY*m5hjZIjs{E@IwdncG768h|tz2J0QLf>c42@2IaocfJT1Mq%Y(hPQz@gwW=W;L_ zK#SFvAzIWXRHx!oANPWsA=?35n1D%m_HtAkvl(k+5v%SzrNQ`JI2Ab-S|jq$C-aul zkdJnmAW{WyNIAmTD7U0f0MBxJGeDj;t3!nq`AQ5q{Y&8tQ~uf_DAjszPL7L&)g)&9L%gT5sEDh#5cT zVrH5xNtEPU#5Imj#3G{G^dMsKrrl~7bLZ%zG{ar#i9|s?(GM&yC*GuYJ(nnSF3>YS zYqr*MXaxrk3oaJ`mlA|A3KM=(nJ6~`!VWL6()?9amh^Tsa{tn@G8?0H?%x^=V#peu z`$La}$4)lM7}c38}+5zlUN(#)o&Y)Kqi1)+ zxx!sXjLAW!uN!OP?0}9!XGa&AG8{uS<#%m| zxv!C_Q)_L#7#;1D2b1{HD z?Z=VEt@|nm)yg)I7;_*cGw^dYFmE!BMGqBzxHCxz1yei);a(+Z89swOY!5iZ_?^0C9aFROp{>2JD)~zit8^f z@EVD@9#yavt67-+0Ex}G+?lAlzPVR|Q?TQP+3tVfFq!~5GQ7jZw{mjM?D4@gs@clmcNrFPW%VjkW4B| zAjIVqF*!D{hyOa9QJc`}ok`gB8=|1SBqH{ zEMt2pCxBw$7684#8&v(40ZBJ_r;rnn!l{ z7{cOSUhI~)4mgpTb8dzaF%}ddQdxHd17W9diiiE3hN4YNt8w6XC^S~SM^;vlwXKsI z0p~^XFu-%v$trEQ)DkKcSGGouX%g!s^@m0CiM6A$3dHe7V8lnI(?FczjM>L(I?4gh z%_wL`3ivmkOJAfftbkne=cZ+zhG0r*nl3vr_Ur>0c>H2eiNvZP7U9>U9Qg=;2We0Z z6kV7P%=N276}Km znIpuhr`2h#AOY7#Oe8k+G?hRi4^xo(^Z_i}EK>EryVEmt`W?)rr;x5NWzWqw_QwR5 z`Fm+wTuVLn(8Xbem`WS%bne;IYYm(4ZiXGf_&GG@_lY#orF}L0XI`&f z7w-axA5%Pi<1Maddc{GUwI>>OZ0$T(7lYXsj~x_|c&=HSg8y!3j3e)BrU-oSmhynf zfx$Y)p{Phy45WG#`#ltVMd?m}dGX58ySCt`+1Sv32{KF&MgkGYu5qsl9vj)PLY!ad%;KH8^%}%LHMp|;@%# z4`>lXFhH?6#sgWsJw8fx6`S0%O>>zB+OFf2hb=Timb0HB>#881-5YlK$hezR_;^_- z6w8@eL9Xu)0;*;?+TADzEcyhUKaoQ~I7NGkfU>yEE>YIAnbAcr54a>1Ng;Gcb)M+c zaJ~%p=S{0W#%@SC9F`+&_}gv=QoYP!AV@f+MN0{P`NuQWaH_Bp8-1s%w)0&Tn@L=eT%PVE7C9|VqD*;CD4-(B^S=T%b(4uI**6A@R@UiJ5XxXPj zC2``SPSw2J3?8c4Hs775db2~I#@kOPaFepB|B^3j_`45SKvEK17|2}&KDFKT-Uutn zb83Xf&WoG9LFR=qg{^;u`p*7stl`#2>a@RlS0Ta5yHu7Km3syUvNMdI#QCE8=bN@SmAvp+Wj$ ze2%B;XyYb}w^1booBU0Er5DBEvF@hp=lD1f>w$)GZ%Gt~ zk}8jPUIlzy^fW`>y)9o77u2E|jat|IUPtu$niq$>4iwYswp%B&#tM(>O>XWD&$8r5S;3#bXFbl zj#ru>6pZ{Z_t;_Wzgj@>K+4qOzzafIgG{uo*@04Gu7nKWNB=#KG218;a`$0k!q)#3 z;Ro%n|3vNF|HHnEKLRE6+?}!Ru->f+H%?9bmYNovAOWP{%CW=w74y-C574hd9e)Fv z8jWGjy_n!b343V|Sg*xQ#t*M2QYw`Xz@KxZ;$CZ2c0Bk6RKxuUY}+gCZ}@HNpC%&_ zf7bje1@lUYn}623|D3T(!Ma!b_D-K)+ss_3nA``lQsxq_LkYQfC`I@Atz z)xnK!GB6VkM8H6A><=Ecmn+kEuT&0YdDABv82|HltJ`MH4aD z;klYe9%!(yfop2{&_!+LP_$f0>l`H!kCnSfMu8$W5>9q^iiJn7mK zmLe7})Q{DM_l3Y&87yHrr0w5Q4agdjqZ`&wQ?rvqAW}^i0L{ZRw4%!3pT3*C1`ELi zc*n#r)dfdrZsWBfMIBKZo~52|nx+G))mlA3-94?|$@%E|B)rgvMrwNVaHJxfU-hKb zb@vbMEBEczX?BgJIWro?q1erys{bENsmBQ>zo>;-fz zFf08sPws8KE+PFidrM#$1PY{FEsmJ3WjcVYa^=xWP)5VsR}Xy%KrlD&;#Mqk?(SG` zn9{#xBIm{m_uPOo{{ak%tKbm~+mJvj5k1JXa?%#hs45<&-D?*5n-ju7oZe^ra67~! zcs2q7VunH6ynW8%5;Pg`-_X?A$3y~gbVUpX0!pyB=Tq*_rRYhU!n1V?p8c!aG`*b( zb{B{;nfbMS1lO3CkXKE6=WFOxZ#88b&=B~vo@^m^>5^D9Id8qbP<5iIZNVow({e+A zbq4L)av#<|hUmT!{yMs+fynDj%shYsx}zXFXssZl+%zltz=XE;EqoR5MkO^_ar$wU$%FQ}0 zieP(yQd6CYoN6!&h?>=NIg$W9Y#}ZGUKZ3t9H|;;r6@)g$_awtR|YY?+)TQd&ry!! zHE%@2*IKJlteei6h#~#~`5em@ZkfKmb(h@A2P7$m%hTNM~AY|=NI z@V-BvKkegvm}ZIC55ZRns z`Ls0?+7_pYOR#nEmob<1`;@Zyn+6-XTbE?Pa?uU|4yR?L->PTS9)bDbe*tx29VoEL z%8D%Pn`bG7D+RIo>A_2WS@#@pkp3XpV@Hy1@nEp8Tywc7@PREqMiHl`2g|>sW?0x) z1Jnz%zT-W!Ty=|^UD^?$$iZGsYj4DZZWbG8K>kqPxy9ja6c#r~?we4`hbuoS#%C#~Ml+4IhJZ5-pK?)m0hZwr?{^JLr z0V}hJV=j}|X=5#KF$c+ju(xz=YV8W{dfjZ071Klm19as7fXi;bY%{}Lz;h77AV~5L zcD%cG5Qv5iB~(mpE=W!CzOUKPzv}2$6S?=tbAVoYGf*2%Un4HlfG=7I%W}!?QUv2N z!S0X1A&7CkMxg=X*Mn$vJUgQxD<#M%ufGUNHeB50K z4kK?QadU#An@0z1yaqFdS@&gS$Hec(@<_QBcQIxnpcueQlsbS?I0wTTxAwv9JhOg$ zN9Si`Sw4(;@u9vQ{vY98TLBQ)JV~faMy+Iebvx7TvCeAF2A<>!9Z<_=fC0v7*b4&h z8&eEktA`l*FAqt!1psn>16fm251sJ=lx`Wif4X0A2`><6>eP$IUxlb|YX0Nr zURbGu{$qXbG0FoG+pc##7EDsG4VX#gNrK;TvK9u}C9A$z=Z^4X)S@Jbfe|X{7EIpS z08=Q?f>FLUq{YojJmlWIr#joft$iaDWbF$8_w9Cl zL6$$LVY8O*ecxT!k!8DV1z3iGaiC}}bmwYQqGON;R8+B=;^ZXqrYbtO!|phV$b1uj ze93O`L1St)9sQv(E?7h`nD}+=OmB8fIKB|+G95WuuRV1o_w1m8O=EjooaDxPC^)+F zPZFq$G|}_2h>SK=y~fY!^|DL87LYR;oqVk^2CH#Re7k}KZdA20Ck8PgKV+~-y*(DT zw-nZVwrcYEbC%*{+GNZoSo6mVoN=5wuvwrDy*-%@?Uiw|9XBJI2y#y0XKTgN*U~a+ zM13G~Qz)BvWdQ!8O}K4enQu?242ZJ6v=|D%-XJT}wDdA90%!9i!i@$+6sS-xt4+Sm zEp95vjWL~-l0X3ZsNK(r?80m}s%07cXJ`RZS|4 zVAP?Uvi1;N*NEeK4^X-Grzvodf#u0lUKX{rx6dcui-sutPzFhOc97Xx*SSEG?!&&i zD};(CdFD8A{q3{SC!{DiKU}^w(lUc4VBWOe{mP})x-5ZgGfhV2d=o>y0b9IO*=jh#v z5UzSL(7-wV8;ah5uS)@P{0*rEX~Q&WV$7Yqwg79sI=K`yqe%}dvdUrqdf42p8*m0r zK<;iQ233G&kW%{)oF>buZp}gSSen=062Z+Dm1rwKT>~6AVNa`3R76iL6 z$ODy40f?w8zO?{@q}GDG&+xVoQZ~mc3u316h8G1nJegmcO0?M^P!`d4tVf|p4StP# zZ!GNy&p76k@v9mADIZ`M(hN#DDr&}idCUvz)`7vxP#LF6bNX&RHQdXJznta!tDwgz z|Mqm*J2JXhaM-iyY8^J5g74^CVj-|pQ0Q%{x}gwM@m%|)^_71}Y|u>CFjSieRA%_a zp5WaX=7A?WZ#cJnKM`Vo`Id1^u*c)VAU86C)WcpW2TR+xMM?h%xT z^K2=XfPwnafS!``FWCKs7F7rUaiHL*)%TR+VgXbF0bB15Z^BG0RF&yh-mCHmO6QpP zfhWk3_Gy7ay(!!clb{^ZaPWb4rTo$@5xm4-=%~66o+e!$MXBB&c=)DQ2I2ZmJ~k3@ zvUUO&N(9XMH-U+|HDoC3ICg$3|8ag z*}_MwFbSI#TPWzKo-g~?Z_+Sx+ZQ!EWL`JE2N<&A|=~CciUL@G|%M#6FaqOmu8KeQ8ac z@}Br9e;%2>#aP%ABrbP~8K>wTHg|oo>UM!1tL=`~f_mO8<-hWGuin0P>p%jUkvKL7 z;zfssOEAl%KgL%Okygm(nk)XCWXz0rW~V_XIPP}{(lxz|6~t&hcpe6YT;n0yD&!Id&tXdINu%cg;}18eYv`wNoY}S2(4Lv}RAz|JQX>0dKW!4sl@`taY=a%6 zwnQjiQ9(pTO-wSI4JL6=ra9Eg&rRH0{j6G(fG>q2z03O~Gg0?32(0BpzZWjZbr9+P z9c*d-!}D@z6eMROE!DIAp#Z)Y8(WaIiZejkxNFvHjdWZBAK zwCIZ?UQJpQ@L+r(w7A*@@I!4q2$O-|aWlW$&_pZdBw$^pZoUU8`hGHI5TAc|?*cS@ zz#8`ga|(rZVM7xql@e}9)U=QrHNyinf4I6HmXujVL%&t)8qbl81(5nvOWpKIsrPFI z6p+hD+8nOljaeK4rF>`D#I9+6){ZPFunh|B{hdH5`@Bsa^&4E7@&aziHZ!1}0nPG1 z(6}700R9$>yedZE@ zX?)UmebfQX@0$@eDhYTSVD1M`-dcIMhqtK)gmb$d8iG~P12ImkkR2TFe$Wt*r;aZG zS=!Yoo*uOXlGlhYaqCiFw23L zHo}@1qVdBoo&WZj=6fn20m<>bJ&+%LikV*9fu@294)+1oq9sKigR&SZh(lb5I}N@q zabL#q+cUt-GXX>9ep22kJOmGB1k*%OFeQEJfN}HL`;>edQRf}1VM)CVL{$H`C8@05 zVfAoM*(d%TV)4h9f~=3iEUiqkAY;*w_+`Gxi*@MJT=}n4G`~}ZdOr_w*8WD^XEP%J z&;Mj0t#%L)E*PGFk#3j?-ae_J=*yh@_V{C&(0tJzn#RMAJbfm8LxUVeL;=CeP8AwQ-9qDz@xi*24H0$h9s;=}54 z7hvY6%qDt?BVo$mW%qs1%US-XHJ{xF9qv^E1S$B*{?4`l08wiCYPqkxxLky9xR7+4 zG`bPkk=qA{Ifu=yeaKEcZ4x94aspx6)wtRJoYNATfI~Iq<0QCqU~3*JS>a0N{Yg1s5GJ29vRL(Kml<{r@ae&0_`muOC>p4C zhvQ$mAeJKkn^G+i$UXkAg#~lp37O8+hZ66SxgZ^hZKQd3HIDG#*Z-7uuk2;J6{Y<7 z8$-*w|1fdK(2!>}AlTO)I{dYu9(0l|a0e}rW49N%{+$Dmr>>qi332#@TX&&rpV@3@!0L)Sb*c>C>vo?jzxN>H2_avwKmtWX$*?QlZ0bb0cM z)hcz#y-j1ESlHELz>ns8LqDGX@(;&L(Da9vVVlrfx}mFIMEV2t%fX}-_y5x*HQ$3$ zIX}15^n0GFdXPe4)f}vUzfK`eIQ~ZgfD9 z)UFEi{$adCvstnv9&t8v{5oQ}Z(K>-PdQo;mcM3bA~Jmb@jP#Z#cs4+J0bloO8WLH z<~P8y{oEWs%D6WGEpbBrV#N@2mk8}3W@*UfBiEg|9=^c zW|+LL{}uGOH#(o!NEbBTd}-;rPOUT+JB|z!@)tRonw-(4g-#ni1$6vUrwFhqWhj>4 zKYN72_YkFHDp@pyuhJ|`rwDfUkyQX z?w6q7r(07if-lFzub{^q(2pg|*EJ9Sbxm@f>s2U8z)X869Ufa5fr|iYw}uNB{@H=H ze6y;+Y242kiM(tXSlYvY z5{#i_3ego)n8xRp6}40Nw6U4$4pUnC$P>Dj-A&$%fG0wIbq9u)zenwfXRh1GhF^$x zq#PT$Bl9rReXxIig)N$4ll3XGHb!RxrHy8P^X=*ZgHsVRRP5}jFCEqVmi~OOo(q;r+C^Zx1e5E zR~teMv!{gQQrYBhlZxzhjYtvO5iY=^vVEHP-DPAyL$Q!3=6YiJu|4nXtJN2mCv7lw zawpp6Y(#J12R(~Dp9*v?z9b51L}L%W1izi%)W?#!hY@g=K}mZCo8g=9CnmjF zS-d)4(%ylrvYs-T*0UiJA1?-Zi}Ig|mlh0Frsz3ORheaDY+=14F>gTwNeJke*KAt; zIKv6cPz5jXx{Vv=%e=q%Ag(hZPvs}B zfG$RJ(lETVF)>{%q_dl_vx9_P^O?=F1{CqKk~mMITIILJUa$oC8w4u0i#Ra{Gw4rm zYD_2^7x~Ln3PpWu9&Hs_>l+IQA>+%HX&`8vdxN2OjXks@1t2u|Taiz9p;g?6zn zo^V)3B`a)fQKG}4$_^V&Vw)B z7t8)lQ{1y>%&PH`hVOFh`Qz8LQD)1`O5Z)9_1JwhTdoaw2T#*XN&IwtxP?jiLO10) z8k0QbeLN;E`9^G@1FzvVqoo{^e3lT4^{j2gRqH%z)N^SKB#9}}7((QU z4`cQ;aTaK$bwSa|xLqm(gK!pVplpk;(+~=NWh&pCl_3sq|EiYd6pt0a!NJ7r-9cDVefw@ot?!#3j-@lyL=p zWhBN~zxJ(T!E_1V%>JbBkn1w?$KY$a`rUC9(u=$4hd zv?6tJxBX_{9Q7+T8fClBy0KM%ScW#$Etv&3pTapE+5-%kBiXP%iO1fJQd`K;4EZ|Z z)+(buhxkWo(7Ts4dWw$1WsMwuYcV&-c`g-^h97%+YfqQQUFy5*4|XT0nAr`zkDIQ8 zUC^rW@|9?U_4e<#kV~xF_+I1w`~F`aZRpyO7q>CKEAQmp6V0zatUJ5SvF*S%(GRxf ze@gTm{q48<`f|(73EDZPm%Tzw+J$g8wH_01oDKOJ-G)jPOgHvK!@1P#Dt$`UkwNp_ zZdtH_g){K2(FZz?q0;sYcj@Q0ZL@XBB|BFK=1>Xzq1&2GI~DMO`U4(o+U>T#eH{bG znMYB?WcCW#5oeCq52EzGTg-9H@tW7D8GCAt95{t$NuAPfxo(+VbO`i%|%vs zgUq}RUkP8F>JCU}J&%`E`r%c{o~|?33O&yqW4m-HTSU~MR8&-`g1{%t8rIaQa#qS= z2kKi|!jk>YkT&Zp(M0z4voE@fpJo`f%~BKA6d;PW97h&v=C<{*S9qL&A&h$LUD3=8 z0;x9KK5%1wTuPxy@}b{iLqaN1=q~**4F-#6fjV1V)EUa4&s;yWn4t}XAoFcc2{x`8 zZVex>LHHj|@*nY)abxC53UY=&!BIqy)}%t+O*!Bp_lfGg^fLl4rlhmY;9qZ6&E^w@ z`}q%!q4g2$z#yrIB||-UZwkSb5EVQ8;zr_i=VT@L;D-7ntuz?#@!5-Y$HPv$v+Kf` zCuV4L)#0W|oWy@rjv5P71PiV&x;WIDHO=^Yb5G(1GK`8UEcS%F$g)F^w-W+rici|9 zRel9#i}jVRYjN6=vL`Sz{k2QHgf8hfOMQU`?K+V=BW<~y1J&8)-h87>R#c3Jj0~1@ z)`S~k(*AL$GDEE6*JD&JbXhyWD4q&mo*gcv?MoZeo-y**tLw^WZ-$+y(Mtut*p7}O8zp=z2oNWI(rS1@(r=A=V?rPzg< z;&FaCfbUPi8V3voy!fCAo{ZWvUxsaZjGQmy?9|QI1&Edu5yj}Vc%T;PJ#|c8FEG}< z-q(TP)KUAxodU9@HcD~KSibYdwO+=V96Q;&SS0l=1UJWB{3v0`K(DN`rlm>=RsvH5jp z%k|fljH5#@J8e5^q$K50QfZxIpcAf7BLw)dWk(H*6t^96p|$jsIMW38Rc&M zM;BZJg&`*v2h57h^rRdVYDf!lN~R>g-wikIw81Q;+;v?$PF3f~aj(+)sy^ht=w4EDd8>Xn z84r6AI>x3|mo=6}>HqYEuEZ4eQ?lQA~ zEXBbsBRY@(jK>p>lNhPW=nI^U;x^c`ESn?6+PW+`em@JH&!zb3FO7~~*4{rnSJ-X~ zKbM1?&M>(@#W6uqm52|F7itZYa)*{P9%*cQKN#ddHF$QwT3D~LIL!^>mrc<(*Re$n zjvHThJTl?DCyR&?Q`&)7=2jDDR>xIQn(`^v_|qsKiI-Kiel|QPb;fEo@VftD+a+KC z?S>yqwXin4Q;A)i9ezo=woZ~dGz1XxYaN=Zksepy`o^OZZj`Gs=@)WeZ2Jid>z4T7 zg$s-mx+r%YYo+`md&rGd1QT{)4y0G{^Ol&snt~jC-=wGbPpgxo5 zFBUHZD6t#4Zg|I{9I4*j)j4MtPv1hX?@`hZdE|5@LvOM*!Lk5-P;Vp!heV9%{Uplj z)f((hI&+QREvY}As_slhX|-xA>TGC!|0e1eHu0)Savh_yfkKaBdW%Q{I7fSt+#k|r913lk($&}3-=<}JS+K-nF#!PB?R0`DEu-s_ zpT%wd_?oAWGuKxNqtHBa;mKYxgV^H00X_r&V6H_z=o#*$)E4%J93?n9c8XIekh90- z>BE=0y`$B&>^#S^Ge=ncwVd1|{7Dn(_oA*srDl`;VeMB#&@-Z#zfB_Y$O9u2H(ydz zej;9`Kht4NQW9P3!#6J>xby=fC#c+&ZW8P!Pwj=LCKC?j!7M#>ErR2Tq{_)mhxS_;S#AMR^>i3 zt@lusZ2zUKb`#_;y&VC_;_{2D6_p5SsIoF{banDoHBY={>CLqrhSQ zyGa2|!)?+cvzHbF8=6a{h~K< zD1VN;e{4y@8plwb^@E3g6>$3S-UZm=0Ga7%FfxUefrAZwO(y#KeNAp~ZWvdo2s35m zDzx)jDr1znH5#yfNu;J5EVYw3Q9@{LF-MjzF1}=z5eEZtf=Md9yCjB)8Zgm}3d|Q} zSf`}u_RQVn=c#!j7ChDXNZyAEkItm4ZCESuLc0a3b8vFX@Lbs$@ zb2zQPBu41GPw+^(c>0VO5;cBmL+_%dZsUC1x0{GpI2&M&yVz)P=XgCELp2fgWN97^ zA}*S(Yrj|f|m=rAHxd?X^%=rY?d#_`G+E zqS1%JkF^Y}A6qZ5|JZpq@Vpz>!l3-6mZ$_u9k(?Kjrll55)jrOyY4Ev=C%Apm*-=GtWc`mW*xP z+D@Vl^*7erOl6ulf{CSlYOPzff1lR!g=$KD*i(;ITk+`vep4Au={y-Ym03szxyp02 zX28Ag)x_;mk;|>g@sO$1$}-n_-71?1(PbB#>|=Ak%1MbxB~xk6)pxq84dIDPg_Q!& zJKE-n4Y8wT`HC1L-r|OIq2Ot_gqJPC_vLGM^~_Wx0p|xWEeyXW)6!E43uTot3VF6-iA zqDrUe@bI<$hmBBAO1;V1QA66L0`^@#_x;-%Z=yt_gHkBmkJBeA!0-!5pR<4vxeL~rTTaX&7vpQa zLXLl9E7H0xsw4(mi0lFO&ip1dEgN13z{ierPQpEc2N}-Xm3z&CY`ghb@?KYc+-P>E z*ySl|o(jxHRz@f-ldu`pblw&ff$PKW<4*T=n$X3YN;vLGX|@Z2a*MU^l;ZEWR^RCp z2_E@%c~8G(xV3fEJO)-ZIrDf_)Ab2K7>N>Pe36|uZi(sV)U=?0Lu0qt<~P85K&g^w zwecn(XTe(9Xdr36epvbqQC=004t4n$qL3BzPqa`?^E=NzwIbn`$|(Q!y?p_;{wL!m zfOAA^)n1=TwqNUR=k~%{aIFBJ68GL>){pDxW7&<-y;gusnLpHQ6oLyMH!*FTWc%)n9b?nI zL4G&3a2aNRfj^};ZOl1n+BFdHH~(O3S^4mxytSxK+(7X7k6tFR%d^K9i%?05##C54 z;(F&kMq+GIw|+5M8|a^fxVuX?y5NaH^d{7l!s2sDC8P3y92-6(d)jXAytcZQtUi6L zA^q+$I#}=o8QtxfkG!lG+i23tOdrtlUGpHJI89=*pd)r>sdZ0)+Z{?3aAV~17OpB5 zX#6q|`-Y-}IPs^siDkVF|I~zA&=9&w)x@+VS+Ch z2o^VX97qtZuj3X&gSAQKayA8Tot^(8JHe&xxW$>7}kI1qS<{8$F|Y;Dc(vP**yOj zCvts3vVr&cD=`7pTSG*<3=(^z#`BU*GNVhcQ}H%A@cKhPG!U<}+|9;^SnoEFOyb8f zv5W#k^KhD-VAG=0cB?l_1F)qF*^dM+T*t*)!d;Ie_j57pE-TcT3XV_l&TK60B3`Wn zTVC&AK}ntoZ3p#wbxsFUCo{aL7re zo<^MsNVh_4B8>skYD-|2ZNJ1^uR>ewDct^MjK#S9+Hma?&IEs->JpyrPY;|~mJerb z1wV6+UTe_LFL!lpnu>kE^;h5j&ViBCP$1jV)Wx2hsg3Q@FL;zR5^L>MpRL*{3XEK~ zy&K7YOuiS?Fn0oCn0+Ys=E#QZqmo+`uBfQ0()Mr?CKyP+uBe)p-0gyi;;k#@jm_pM z@IyyUd=*JvOsA}`xDP;1`O`5FPTLMaGO%6NOs{P!M##oBrPrC(j0DzqZLib}c`C6>)dk*?(Jp1! zbakYqYo&0ge>!dJm}un<2ihsCE%gf{x{aSNNZ~0Z&IK0k^QUtgn&<7=jH}0+m0qot z-9~P*Wn^bFLflFS5j5>kHx_oEUv9F2xr#elED}m*@7qW5Bu?MQ-0mWp%lP5~@K-Ey z+Utiow}o;Bi1WRZ<&igr#;XGBQ*SRs@KF!0nvE9RkL&$$G3QNeduG{< ziQ7NkqQo9UuPm?g!1BsH+f$zl>o}^k#}r@DWsR{awdl74Cav(;hn%g?c^&WEK2RT#Rcmn|Adpra6okQMXZr(G+OuIs^>uiy1zfdR&$Q~c z++PUOn&TIrD?cax;`)$%>B8%#*N*1n^Db)xJ0-W>$}S%@j#+wk?RADZc*e0;1QPoS z$hFF+#Sr=JVi_571C#|HZzL$}_zl+LfV-y;A2l}q$r{1viu}oxcD@xLu}g#Cp?6*a za7p?`YqCc+@-l+2R^5O_4>m+0V{n|9sk=zuB}-5yD8!_U9NuJ9L>az8xQhg(`aZR| zl#%ZNcA-4qH(n&;kztE0hsu1pA)MwMz1z;dnPI$3yhne89JBBzXW zeRDH&N4fxr%ElruFP_|ONb?j-_oj(TkyonKyIWp~HszEhOgY5Z08X>Uij-%rgV_Gk zjBNVlW?}YQzT;2Y&zaR9UU{bUG;3%YSSlT)&QFT)`@qZ-xWc%;gj8#5Ym;ZFDQr3Q zfzD5xviEdZ?3uo6zU}6P+_6HstrDYlv%Kz6ZwfY`<&4J%gwdX1!er+&Bzv$ie(VwZ zeIi80d)f7vEe;$*RfMysa)O@&(gQV=js*|pqO>&Mdr!vaR?2=<8rU;FeV-);fuo_^ zy8|ifI&FF;(A1DV=hj1U*CRwV`VHvNwJaGNg@teTF9XQ z7CDobs7hT_X8jl}*mbl(x~v*o7U_ zuL)JNt$Iy!e-<*NArub^uT0>HCdbXyf74FG;5xbu*$tiNa&C@W3S+$cd6heT)lx^k zF@j^?ZwXGPY{S>u%8q9~?2^b+d2n=1DVUX6)Ii-ekEQlll}BFpkhj% zLtqf(?_$w5I|GP=eO~8oq3M9)rlMq~Gv6)+;&8{V{vF`KS3yxVRgps-8QP?pvjqx= zg*m_WNgOP=Qw!Fg#Y9lj{W?z#Z(L^kwsUmpz1Dph#eJ%@aK;G|jyo6= z3LO}=Y7g&mG30dvz)jN+BfpLb>tAmo)4y9^derD1C#r2HcLn~4ED&I>T4=b;|#*{q8Wn_E0LU#J$VH(f`F6 z+0&VC-v-)pPq$h|23@}*`=g<}RJt*WA1Z%fZ~#6w&XdSo8mj`1-?QOLDSfNBU~Z(; zfwFS?jLEjkw9WCZr=+5-9A_fH3tkC9f<zvlVKHoid@g$Nfy%jFE$eNDXAJK=BE$Q{9lA5{SvC z98NGP^Hf~i0e{WO$UC@?)>GnUIlvc$zD)I_J;vtEK57$avfuppD0${@c_;Uu8GmN4 zF?oFbX`{=1~;01d-n;Yjrxe~$EVKRu2t(+=t$T2 zo&HsxP#%iqfArp!euDxRgA%W)E%kKqGoTK_5t50E;~Pn|pvJe&A(u-(ZK+3;nu6BG z%_-aJ=01}87b~$wW3fW!X6DwFX?UPx^o{Y>V*+J`#c@&={*cEn=NcI_uJ$6&EJaqw zL@FhR?c#LDfb1zEJ)W-Z=4&YUL&lGMUnH&%}2!CSI5}I+U#{VyLjFZ%_^up z*5*RfrwM%pwb_ZS1KxYZCECs#A+vai^xOj18se|l9`6@1dxX95^NiWviKm#*n%4@< zb{gZM{DIUYadz9Oi{Ex1Hgr|>jv~ZImTT37OG*g>vOFq zG#YdJkr2m||6!m6)MthIVN>Vu z0G*5jkBxL+19cZ|076K3N3HF(n@{!?z62x#&3T$yRXku4wriUru9@#|CwL%1_4b#2 zkEr{A8N@l0-GVAqwy0EA@aF&bojPpb^4PpE4T-1k8sX^)>S- zseiRCm$)_ZuN~nVrFpC}lpvO!-varMkQYM@z?0to;+rI8?< zd-H~aFpGWMQE@)!e9QiSTvDjR?okl(Ex%L3pSQD1W}}umy-W^0jfL<>LUx)_S~(}7 z905jscBhm##jjS`{gMTi622HcW6pwrTw!mEzJ2vdZDy&7a2{}Af@TVM=VYtDfnONLaEz!ztZ*Lk1;8T=LVr!g;XyNhJ4o+T@zC5m9IM+4uho(Shbp^($ zS~hWBN2qO_L#>f67Lg)c)ueff>K=dn6TN~uUfyu>4+)rKi`#|8$Azs*gQA9v>2 z@chgJ@jm|#Ti+hfbo>9WqHa3qLsAj-scsdeC}&o5r&1>8LQ_IH#+=#Kf$IJwRB~oX z2!(RWaZ;HuQ)n?Wg^e-8Y_rX_-}Tmg*L{BRw)XxC2lniGqJ&%l$LV?B8|hRV}|LK9tGkPE_L{fms6Q!2uaiR?pi|9EXbE* zbkJ9=1kdcTUnboSPXi`a1)PU4S~W=2XdVmjeifVgds8p;2>WuosDKTvW_i_$Sxe??(qQ}$$6 zEp1USSypqeg@(RzaiH|PjUJhvg70zDRNuQ}EU|6~J2%Nmb{OkDJfo^vR=AZ?H)?z% zH!Q)07&MSlJ(^bAce5p8RpX3m{TeZvA{Yy*5%-7Hu^c|% zo)7nj^?bD_-(jTB7LhFbU}JEB*(34c?$yBLujoymTPKC=HbiEC2PVm<1A6 zKd@;rMps!$TE}fSKPmR+&fy)~$7>%m$u;n}*BZ%BArj9K7cu+YKC<8JHS_)ZV{8Zy zlvmZ(#QLcntIX{hzB6z~*zH`o3%m zJi8}B?nNY^mDPNla?}}#s=lBjGbcr;f=A5>a+4=^FNT04WQ__+m7Zd;cG$|7IF>5_ z9L*X;{9ccvKNRtrR;35&=c7wMk;0ePmb#MCkSJ5qG?IYBQ3HW1^US{6nZCXK8u*Vd zOY_6W49;DtPRK@?3C8odn?f6LI3k4XvepMTsMhoz3_gQvSBWwWHf=2ZQ0QOlV4A3A zDrgShSQXf`BE85TfXx1r6o=3g^**!YiJgNDH`$X$n3@1Udqxf$VXE{|=Qt&^#bzfO z=b~g1!x8XvhF+5TG`~!lxC%GU*#v~gqqs;R6=P5K-A+NPf0?APyo$v|9@7{)_lp2!y`4ul)?zbx zns=pVB88%!sj5lU=HM%EaQpuB75#<1CS75XSwDHEIo^YIx;xdqclrq z@H-%_>_{tu&uk|(&S8uK2jBb>TT5x_!@*3e<%^r`K;%R%iZytd`6;lZKV!3b4%<%4 zTn+Ibd^4LXb>BqU;v$o?%23vFvymeepi-78lvJAs0Vpd*4Oj>C7CoL1Zg;o<8{UaI zQW-$0;~L1G-fX$t6wL#6=3EcrPS3*Wt4=b1c5Z0Hr430I)M;a7F_2>jT+OGFx4k^^ zoC|tgCV=Dh@5rkMV6E1Nq7XL0D5KooTC0p)CPmh>&ULBH8p$YhFH3CloWi`!wDP|0cEC$!)Cd8oHc62&>k zu?+VE#$l8zXFTD|%|5(nzo+We@YWJzHIC?PxQRfNDTwdERiT#Cg+2-SjUv7yIUJXH z3Q=HfF_LU{Mv{-IdkrT%7|`%1EWS9xJL^Gy>=BMz&ES$kJiW2WkLK!-+c+nwJbDbr z^1|e_-LiQ~t+Pk>+Ak$R8HAezsGWXZ&7c1Rt%jYA+R4})o&THTJjb`QBht_9F(J2x>uX{=@NYy9M#fUy zGb%Oksu~eH$28UyFdmhsPjN9?Xth;PO1YdX93elvNl&LBlCNXNO{M$)Ak&ogEmUgMCy_(}jDh zcmCF>iOONAzJ`bU1!K96IF7{>JrChngD&cLvs#j1i+qN85lbY6n|{hBouw*=z8kMN z)4B)sNxkqa>OB4oHI(;p@rB_bAHRAt0239xu3N205>_z={XO&>%*H}_n={yMOUr}s zvX~T&+^)O>YU$%0oFB|6|H6)6bnk9RkOGFwD_X}HHXb8c5GAtodFc5buf9}oT!D>D zz~~qGE1UCM`n(6kXJbEER$X{zCE1uVB#W zy@9us)Zd^!SU@9UxfOeReE-M!ftV6)_v~+q$Q_a29*~o5N+$>XLivfEeVP z>$=G9`>0;HGxpN%;T;NX4_sP~*WK8m8<+0%+XvgsGi_0fh>_O&UfXxZ)7l?v5@PbL z`-qz#y}Ki?5237}691%zDL9ZSw|cWu>%$T6%-Dp}%IkCiA=`BijPK4(H>tcvc}W0c zHgIt+m0oG{W22;L756H!t{`jj6Ge*Unpb*#O75KcfCi~vF$o3Gy~tG$EM>;&UuEmC z){IncXSM(8?Kj4*vRA3c9N-Mv%76ytSfVh!YY(urb;`5+DRot%Ax)9X+bMpj78ScQ z#B{Z4h%)TCSiNfIXthC;tsUw7Y&hV?f<7T7rc! zaiwC{i!|9co8)xi5tf~L%`w~Uvs?GnxA^WoeYv!CZ8)|Bys;V)_S!?Be$)eO;8ofV zPRSp?1$Ry_U$M)-NGhi`eq=%(W56-^3&}DOdsR65g{-JAthbi5YnD}#=Ded9)7uHN zUUIZj9p&Pwpl2^))RTM{9!v^2ly|m$dF`9SZw%&7Vfn?ZcXOsEM~xrE;F6Ee?VEXhS`h7HkzL_{%c${_ zn3`UZm<}-$K9fK0s{HdPZ)y=iTD;&UAyxSTEhSU*r=o^h)IOXheZUTlofcz0E+)jx zkALERt<5=f@ZhH=vrH=0`=RgNTD_z?yv^pm>glRVSH-sKbE5&{89!Y%Qq6B5-HJ1J zD2C7X>SM#JmNp@)mN(-cyWfi~(s=SzoR+VN8GYD%->+wF34G!B8X3o(M|R9v?!Q-L z&Z^&A?y=jwvCRCBIZHzee9oTuJWbKTy_LUfsu&7PY}G~c{zQZbC?`bA@( z2-?it_zAnH{iDK6vB4Pqtd9_gFDggUS(S|XI)GxYLrV@9Z{>@3kI^Fd(;7$tkmEAL zLn*8R1#9JsbddTaRsL{v;0(G(O(1#9cdOqRtRiSM##zv-1R}#Z%x-;PAX3x5*I){A zKA0$xxM{s4QJ17@d%T^eEsZP8usAx1> zau6aPHiZZCSH}rO@J#9g6;*qVGd4@wc*^EOx~}&C%=P4fKg8WM73#uq;dz;Bmow2< zj<=B5cs>5DjMz|n8(<_-ZyMzUE&oQ}v7PVx<;7Ki3RA`Zv1uqV!MSV41c zCzg|e_^pMe`NFKsd%ACMO8d6WNb-?|Ao^J+0L9C_CGueWoIO-ZCpV(MsYz%UHGif4 z$LoqV%3dK|I)Mo)%%q$qzin=&n23B)$a6L}SG4C85L~+2PB_l|J|| zqt+L?w3048`S7d`ItqXfiM>Mz;a@`tf*WWHh{Y%1O!EH_AlW;t{G&Q$RQ?xv=ve)f;=&YcFFLKUoFHGTYQYs6vWMzdgnrD>u6^(F2>CY&?HQrd)p+>~qn` z&T(+KtUuG20^3l0jk|c`Y&BDS!?p%yn`q=HIDpxmOrmLy%mXg2GLdb0LTN_F>8jOo z+o!DuIHdXN9%Fxr_rLZ>52r+>U_02fHE+|!_;)KTB>$ml7vqTRF~d5h7KfF^W7Rj zmVnY0EHw|$--=FlMYgdfWGji1nwOIbMc!hBH@a)qQl$3FYgujs8Gv7`FUi5w@xg*} zS`_o0i0+Pw0riOAn7p$>6B-g!iJ|>OqNU73l6vFidg)Y>nF!YOXRIsmec}v*zQ4C9 z+tqAZww9SXLa*}Eb>lnG&vFO{gC{%Gr|dUhwFodA0t0nr;v=J>H$i!nX&|j9gow_f zK8q}JCoW5Rci`1`E&Fd3;kidaXKwYnlQ^2QD5wmUmT@1B)?+0|_*84rB9}-T>nQmP zpP0Ga7QU?Zi`%`ruu$ye?dZPn73X$WtFKHsQYyDXU6A8m8Z0ti{qgulALgoCg@a3S zH@WfY>X(dOe-~4dzl)RpWwa5X-|a|r;n>cY7m~qC#b)haclzNpVSqJxf*O>dy?d9h zm>|vb%!`{c{F9<&#=UP0jo%??{IJCug>sZU)Q{Su0IRl&L{V7-8@%_E- zM?H>sQwEOXDl?X7Ojo{iaI{qXN+*yDxRq``R90>@7on+ZuMt^K`HlKGGCkuD9*;I% zP4Qh(yDnK`l`gR~x?D=yQCB>8etj8cu^8@Fzcikf{&3cU%F!2_qA{EjLb^QWYfOX? zPk=Cu)8g4|S~O0Sm{HrN{%TbIs#iVH?^cKMXIF;F<*aLoC$3q>g~t~Y^QIDOo$s2( z2))_UL*9tWL!}st<#*d4BPF@B414I^~Eebr@JV@u8WIG)lEsZ z*xp^9`EUgqtn$EI5}2v2<63y{Xu9hn_lH`G?lf5lX!^MRm2;$+Hof#!{e{MXI)!`R zh=C^j7IL@9Uc`pzciodX@7ddxhIp%f3g3ksqI{vtxgdUm7SkHjL=AEnM<=94K0=>9 zXBd{#A@m$?gi1p8V=9tud=)Jru%3XU<_-im{+Hx9cOETY`Ujc8Tga8b(mi@Pc>yzv zG0MRm;!uAL5?XLi>bMLIGxK)UkI4giTg{gsV{>`S;!qhJoWs>90S6&TMudcL2)2jB zaOE4Cz@kQubZX)e>D8P5Do0(+aiy-cpI3HhoGxi;Qe-?nFokZ89_7c{Y0R4}LntXY z3bjJkqqYiL;#T{o%i^a~GoMV!?aG9c>vF#tqRsogZJC5w#fOgD=OHCP6D0S6mOYI1 z){U254W%=5C|~D=yA`HWxCLU0l%V%8$m542c2oY435HQFwV-5Az_;3!1zd@)X)40N zo;OVaUsGe*wne!I9cV}o&jw2B({VkdS@r-VqRX)0oELa!9Xulexclslp!5lpZmJ>7 z=_=-?phshmCKJB2>(?q+MArkLpy- z`mvfme+M=;M9WcXZ3R;BPF*ih@(H#W)CZ0X&W$9S|F$%c;0@BqT??8w5VEqYRaKCw zNSiD#6RVxlpJGg|_9SnrQ9ot=$*Tu*@9KUJyW9ZTXqQd_u?nO7dU2lMH*gNQn`TU< z>*(DRT~LWi>X2VPvg=u*HShu%O0=ANqU_Hs{Lx2C!7} zU_&JPhNw;m$y^!XOjnhUEirtG!RlU44It6xD@h8$S?6N{?Kev$KK%dlEZ@^X;E9qq zs|;PmmkceOWdrPojKxT(gTCl7Wvu@N;lK#T>K!C0Uah=;!6L(R_#GJwTYpSKvm!!g zXoBm><83cpd$9LoBVlqC`@*aJGkxp5$WSBsI~NOJ#gV$$Ir^N~xou9lo(KeS2Lva6 zy!_jXUFin|(p@B06NL(|0Zd|37^AnS=#P25fWd|4gOH$WNaMZP#rf4QTgE)Lm;G9$ zq&k@{rhNQU>zpu-RF)50h1*n^b*s#159J2)2zg1W>wDwtIjbz+IifbYd{T^8Px!+ zNp~&)`J8aL+7rlmn5?pz0E$nwU;k$yEy`Zik4VH*#x|3~1Ima=SA*@y1E4UHws$1AR!uV!|zAr}fu@x+oFP!DY9TmkS8${%M{)`*xOQeS)X7(0s0W0!HvMRI-axmV~@@!4)pzz}gq692Jm8+q?_Sa>Spula~aDyeA4ujKoG7>}*W9W@c_bZus_nj}|^#bu%z3DS5~-O+hauNLzg5!fr@D?DwvA2XACH61d$nfs#wV{M3Bgralda!3d3PmfvNi7Q z19MtoBf@&WTU{^2ivsQwRX_ft2>JV$&s>Ip=NaiD5OR$bRf1L^X28T9#0<$bA@b-v zJSo-Dv!In@wJD~yzw&Ai*d&Zc1N5$A|1;yI^5!m1 zz$jS@Pnn=i`FRE)z%M`7l@HpZiV68a{?S>%y$pYZf*FQolcnaU3u9Q{z+E@y0X-W8 z4Z@5>miXDX*_pZbsc5|+_+jlcCOr*++-{*S4f2S~DZXE$&EKbboG{Z&toK`}lKAUYCU2PaX?Fx~#kdKA{cKb+3CIBW#Xv!|Au1C_XAxF%T41fr! zc)6?O)l}Dnmobrx(8@vd6sm^vrYSonyD|ILZ8hozi+2Sj9IWm@hZEi27{j0I4XQwx z*Ou%{-S6f@?nafm9B+q5LyO>-J&t7;C}uGG8Y8cPC_rVes)k1+vjSsAR0v>N&PcZx zRRjwD8{Day$H=UsryR_m2gHhR7glsr*A5p`;4smCbT!M95 zNc+d2g4KC2j5tOmf?^sFAe&$6&PvXHT}KSg>J);xNlrxcm7PnN{yJJ4 zh0c1V?pbi~MK$S?6eWs7w|$I7*`m$c>qU>5>?nW4>J6hQQCUFpwHfb6530$wI67~^ zd=ZJbP4k-qgUe_8U38|xdd%$i535>n37LXWEHOF~?4j&ftOiH?xtahsU?1TtK0^pY zcq)5wWrFuu-oWzPvK9%E}cYnS)L1+f-h#+CsUL zs`HX4D8!6d#{)->*Yu8Ziq^jK40io6d2H7b8}QPx$)V32uoKdnf2-KsaI(- zMeNaBHZF@Os~Zbysnc}F{H~OAA(!TMSgpUHWf*XqSvtjoK8WSRcUsZ84x3_HE8B@_ z1sFwxoZ5ajgY6?u7pD(9RZzjATnNmmJ`%uvsmX8EMACEd-wT19!j9}nwWo4;Mb~HJ z9?s0lC70Dr15pmj#QJMk97DsTL1kF&uv%!t2^Yl;IrO@*0#sG`k=8oQi@48B>U7#M zfj+WG-THzksrJiSL!z@ixivjn^oprI3B(G%>KfxQuLckdQF3toND|M7KnNO0_Afpo zd#6kiy4H$aqfy1s^a0(c3gzlLC@t6?4?V0L`_iqZA>!sBA`v>ej6KwV%J=JMPbW|eT6ukzX0e?M714@kd$T+Oc^bkk$cTAE|; zv=nI03vbDrrA19=QWw3DATWRimO&tynZ5w(Sobu+DaU1`tK?G;UcHIo2dkXEaFn7{ z0FDmNJ;Lg|1?DW%1Fa*mRH73nupLO}|Ey1^SC8^x+8zz0yHRAByv%@hfsqCjb8`#% z?D9Ig>)}JVzsK{^6I{>#6+7gUR=fJCk7BCND zT-XKr zn(vZ}wc9&hNA2A1ZNzZTOz9+g6(j~a${GU%CD7>lX5?RkaXCv7=WP$Tq$) zuOg;(d0ki-wX0FO0j_~W4kqy@hLl(D@<@s*Jfj{nbCjImV;2zl(%7tLV!zJ8%sJgv z1-f?F{RN#AKuVvfCybO!{aYOKBIF;B@j%+fX*g;czqWq*)RE8t+5cH5ae<e@){)&K1c(=Bpmcha|~mjJ6S`bo}N)Zyazey&SiUdMh286{OAznUq_rskw zK71~-2NaTr-cbF>l}QZKv|5v1*zE4ff7jL~KhOZ^!J2iDGsADWVrY2230O z?AM1ZFVlP;z57^$d7a|A3wvkGg!3!%<+mCga_kNxy?_6Hh3CE$aC$8+P+`!t7t!$n zDAEU#rnPjEUOj&@SL$>jGQW|P=+PPuTrvc|oxLUsXsY}wqUsj8HB_+bMPtPTlkpB@ zdbCn$PS{QxQ;rHlm|>|D56hM&X~d$QPXRauBrFZ!C-Bp=#n}@ikgNdg+Ap z8*WrJ+-#E>yqIU12&Sd>()AL+o|?^zPopYk-UBDESxGnV?AzIj$B~&w1j*Tk{jh+p zW$8OC*#B+@=O}24@#E6cl#RCDhHA-DEXW>aq01xmoz1@WL5i4lss`P+oiy4^@NHof zrsi;6dmB>r{SdH?M=wgZYn)i37# zKvZfl?cQ!(^tiM~m$na2V7;A_UfNkdF)}|idiU(D8;oHp) zYnM#*8h#TLg~P5lQoFR8ZHQP)5{q3LH>Lisrg8sf`ZLw2SuP0L4Bf;i`W zSkNNajFK`va0BJZLoqCv9t6(BlN*8gdnFcgwJ$Y;hvDIV(8fRRa^vS;p6VCdAe>+Y zUEhD8_jWf}OnE8ba{n&Wz!x~L`L*yuU%#;pGA+D_5*NLy_J22tVV3;k z4w^4P;KPq=CUyxUoA{i+auYBn)@0CgqDvb?vrQs_sTI@?gb^+JJ&oLKaGy?p)0y#A z{x>HP7dZJppB_Bbt6!NT5jS_)M*~qR0gCFei2}dKUCb3ToH9vG+|Eieq?%m<)Bd^z z=mQjK^7lXCTI z2#%ixjT^D_h8#^LGdIssqbB42`P_ScJ-73A2=W2BU4@2WQ_c|XNbAnEfC{hyG89K~ z+SpdjY(bES8*;kdco?f&cpNN_pI~6Q#6vX# z)P{rCxBvsNaqmI!S+bfgB&$*@b8(zj zoHw<1y!jGDwi!550FF~`cc<7Xr-N9VZ5&<-v^@*6Ju>a#YpJ_EWeiat-Uo$nS&3!Y|xf)chw)yD>Ki&91y^Clr9aCeWY26Z)Bj z#ZJ>b^pO*$^`V4)W3(_J&a0jRTYX}Qya4I>Tz>BS@{dS_aP;eD+~DUNbfykGxPg(` zlz~aFF}#2iTd=h`#2{d|XCK+Ti@cUFdDd$S85HjN0E7&r?btqjc%y|!1jxs$mOpjh zqMZWjx)pdvlcb(SZ+m4+hj1r(6Dro?EDJYJg|t;)n_yr13#$K{i)NO3JcC})QmNLcPa`Z4l zGF7dlbE*bV7e(kc6vv{a4kQ(PVaEgjpGuk>g6OLhqc*{o(}XXC^U|u|`=o4a9S2gP zo>8shO6XNVE#p1-KK+SXdyP3~A|e^*LNklx>yWwBRU+=ZaNFK2laU8ZU+no4+k_!x zBSaB~78Ndb5b1jRh<8olgy(Ov*vJ5-Yq0hZ50`H<={WWS9Ua(b>;yG{n%@;^=6U(m zUE6OW5ziT2dVN7|kcCqNN~^4>{YMuzXU3o9cm_nmJk@9hqM(vjr93|lrq$@E2$aVQ zU>Jw6g_4k`s6{ZJSHk`=i)txJZf*XngS*F_d@>L86&avQz)hj(?4p^8*l;I%lWB`40Tjjk z0Gtp8z~&KU*A^DDuFc3TCP-sJJV?Ehb99l^ydU)?Cc)0X#GU8Y!@L)CxuJM!{%8%# zA*_1y`O8nJt;KBOa<)(4(Y z^8wS$KU4)i2X7=F?4_PFd5U{}q!DimkYHcx6wuTW7fTK@a@30#D+|e~{RK1|LdW>Z zAs)42^CTJu);nD?f555WpXpLo6aN_OyDqXR+h!g%kHvd{P}+3-RiPP1%nmG|!!o_({QLDcxRn0o>8AGE;YIw&>OvX%xikoZ0rm2fxpl8 zn_wQQhB-OM?7MAqlzaB=TtDXd;+8@N(NDi8x;i=r%=1?u{W0wwPTasB!7ZL;v&@>P zF)=Ji2-0*#O}P-ZcyWcv6kwhEw7em%1SF&r^>B0dh?q||Y zI*WpBm-Q>9nLfKY51H(}46U4bK!K@NOb-FxkG1eO;marTZoUV4&-`E@kL9B4lK2fN z!<=}&dF&_c{MvY?fsYvfoOeWqSolINA(27s#7cjpSE&X98k_0czyBipFm~@TNLgfu zFAeI5+`jt1YbH|NLZ`LYfR}o2&-Qr@@%L4PSr|wj*&Cx4VKUk1SY?KRue{QF!@HT_txz+p;#=V+9%qWG@3nQ`Q0-=RYJ87n+aXP#Spz zjzA*1{2y-al;dh8ni(lp?E}a1=e=)CJOHiXOP5-$^wxdeveH<>WS< zar+L5bwrRKNhp^I-;HV+XGa0W%Wh_<2 zS^%9Rxg=UmC=-on>*MBIbO|IUe$T4mPlEXhtR6{1L$d?L5}2QuDVYf;NJ3b6CR;Wi zBTSq=B>Bdr6Ii02KW|%lLknwY&aGAQ)iwB|r6>D1IUjlv&kv4`8I9)Il174G?q7ZA zC--y!BesMJ<)!Q1-nXN&olT&)Zu4hULEgQNPr&~_AOsT{!ANS)QES=z=NqF!g-F4F zm%%}|Qt_3drn?w zFKMIMBt%33Uqp~Zs+h}{XI*{}pOqPGJOj_5ElOB{nG*ctdb`|tykv0|fdVvI4Z=)Z z_knQaxsX}#$~1j0z7HBZ-=z}R@M#DOA}Dix%~t1c^oe&hZm)WF{hY%P&J_~s~89W{S8 z8gX$1yurs?GNrNdC*6F`-Ux7akO2VRrnhF{*q? znG|16X0pi=oY`1)@ip}#BeB9nn?IP~FY|QH~xLK2$DVHmO zWRoO{f9rG!brf+Ukfe=4n>{IYQ-VVCFQK9Ie&ce-0w8 z`@i-Z&c|e}jgYj{kl9r4xl!(@(vuKKezQ#wYq`%!Ru;Y~X8Bvi!R+gtUSk-&>1MrR zubaa6Y-~?;K-&VS`UX69`t1=N+;C**@ivg%T#cykBG~uyYgMWr*UjH*Q?_k4f)W#s z^VUQq ziFyL}>|n#jp5;Ws>`iZ*;}qUFhPI8MT@=KKT(Rhh;2{suer78OPc(Kw-<09Yjf_zn;lY?}K>QP+pU=ZdhB4*~}QZc5s4#fw9dx%U{vi`(wR=83KYPf;q@DJY&21}7Z)Z9 zi^(8~ts40_3d)Aqr{Q^d3W?=H0knM?dDOE-tyX6|wzSLQGpxU9N(dq)h&bG}09PsBrA6Ffg%GLQ39Q}ZiN0sN|nD0ZX7KHA!4edv!|)LNG&eE7$2zT2BYN05qe(?ThV)p-Xx36*F`&0a>mj0c0EKGtCJ9y)BAcQ;#42qD}V3 zov$nx94_;|(Y4Q|dQD3IR;p1z>uYmyPiw%N=#5sLm;UEgd;9T-cYTl`zkMRn6Nj^` zsy7C_ZUESXVV*QFh;8g5@2ac44QT;b@tfX;wpWm^wutE!#fpDdWgL;l``asXZ=&BD z>+gO8^$qiL1uuW5@chvMY|zb@sd>G|9L$F8-xFU?H33hNZ6EBLEpm@{5rg*}A5=X? z;*P{46KWNxBv5(W2ig)`rrcABU^Vf#r>oR7aoj}QV8hHJRZ{c?()=x1n6(;XQ#D){ zK^OPm6fv!N-RvlZa)kXH%w_)z=bdZ|uciRp!9sYfr9&|#$7UXi6iM8HF9!clB^`v^ zuoo&I>$-{~%*haV$2Hb8MlZU)`#33k0DF>qay-UO{ghNRx^$a>U%Xz>dk_yo=dMMg zirU9hjW?v^yz2a#f4?^W{$}z*7f2(qi9GDsufZ%qfBbryq`kRg>d~tyZL@M4h22<} zxbGyB%zz7*)z&f}HN|({0#UF4I7m1QbZcA`jaCDa_xI$(>A;M$zf9D2+GuIQUH>JIef% z5V*nqTlzp0LBGi?_3L!s#@n*lSqlK{dYXRbR3)vSNKlWy|PM2eQg7ipX{6hhB z>H#W?@BBGMt`H6j<9+0&BS0D>{zVfUNB91Fz)JnX&A_Lcp9YKXQesTnq_|8Ln(-r~mdk=N0%U1F9! zQjYLr2_$n_H>FZ6U+0j%=3PwFy`NyB6#U<5Q_WB70Tg4Gh=>qRtQ-zva~vcyh39!@ z!mID*906Z@bHuyk=3C@ouGDN?((v3pW$kJ_EOPF$3$GZ{LIBkgWhiGSv~`Gz0V*eu zi+2#=bMDA6Sp4ipQlcE)K)fns~KRF5R8st@-x!Xz}~8vXSvw`me6~!MlJ{1 zfp^mcmKGKr~WF-mp*B7bjh8g@g9{7(l`!{oVp&de7J%Ll&LgS z!*=7asVfjmn+5(amjp~`Fr_0H!0dbF#uu|=$ggca)1XAKL?~1&xIV5pFplc|=|&SI z5wsZd>J0Aqz6u4@W#}u^fasLyUnjn4-um_ID|eM&vA?3QdJ;-6h6Ir-6=t76fC36E z+&Nyg=Ve2of@b)r(sC%J1Hygf8*}K;X6=ASCAXG7D~qWxt1#W^UerzJ8k0*rO^5l(6hzxJp4Etwy4a z>ydh~vz8VK_W00S_P-mkF@6v%!~n0ODB5C=%=EjeoY{nG`A~srX+btjVb{-tepZwF zz5n-RW5@?6J0gG+4@0q#iP!A9O9cRq3-`x5wa|N45iR#_vcnW0g>idR%?KX#1f z#r^nmPHr~%MFN}96k;9?XG_n?)82^KGtxBDh)7K7?x>a8SGQbo#e5SF(qUwQ53=)%V43ok&v;`e0E1-PZ8&wFgH^ zAFVY0&sXolKX0@ptb4p+rQfp0yOJ(8B`kQ@U9HeJVGCYNm&x#^9d&H0q(p_^b(71GG zo;SAi-MGgjiS;53&B~Q?ch1Ypw#w*qp4RsGt=6TSK)D~@k^2$vrw+Mls);o{Ck zlbYnzV7s9ZbLI$`4ztS&UwkI@C#&CT-KB6>Ov?-m`_iKn1)F+`-J>z~ds<^WuiT%Z zzn>tmie5z+v*)aNIJ_`e*P6drI+TJ^m_~F}%ndw=)I^HFRLf2Hm=(e)%tHR`gl@l@ zyL-ZQT`n}y)2L!)F6ZXB$NZ~1B~0F+lbgwR{Z;O>A6dD`#w(sT-_d(wQejk1C+WYr z_aa@rf^!J?TG_vUWESGrkIc%;iJAMz0OPX{AdC!pEft6xL<>XFtd}X;h#}rra59V| z=v~j%S)kIjJ%DN#fpfJGjbK9Ju)$8&0hk}2gTyZr`@5)$Mc_C%J&-fkBTjxSxTpHa zgFOHyKqnRwm}#QON=~i^J#qijSqI?&C>%%#W&q{MBZj8`Nw?Y$ZEYvE>KyWO<54jf zx3H_;tuK11hpQ(Lt$P0LS!?YO18GsGwx%a$1~%0;ov|E9Ik>f;%rgN9b=`>3PMP%r zr({uK4^2m}sHPGGSNfw8_XrDL=I2!Uf?awxKACsAx{%^|x<_&muMsWZPrtA0;co3n`anmW#?!XSmL_;l@leIcPAowy&rw% zS0&Epl(~jRR!!GwJI0K+eY4=0ulg`RL>Qv$i^1mqZm_=|Nx0GH!C&>k^$*rROUq`% zy+(K1lwnMZAn!G|rC)d2u%OdY)>i$v69R;! zpCwTamwG0=7X7J_Gwz4vMo#3X{@(kDnV;z9Zw(`_UY;y~*3i`nyE)G!ZurZ%Uc2Q( zR;HjDDD`NME{J!lcyUqdm`X=&&$>L~qNmHy4(5@C`0Hx#9xz<4WGn?|5 zeb!Kcqm>ryzn?UGyKx=9rS0%5fZ)9fXYM1F4*?vNae5E^)JE=fjr(65e6xA&lfOZk z;jBYOM(4AE{$t(k1nHOb$N;;zD&8Y^j>0i)FdNGQnMCIs`LxVXK0i^!hjZBbj#6YN ztVOl?Vui{^AQHatTLjEg^;C1%&Hv7sjBS|k1@ULx`HFc64+=$;HF5{`OrF{l-QE>* z3?vm(*G*Zc%MNIJkWKb~WA3_+YjfS%yv+ma@wrJg)kC2)i8+C{w+)=|-AuA|+?6*!N9j^QD?Xca_EocbaJ~vVFEWTl}-DfX6bXmgpsyx zVdT&ne4F88y?m7S8~~lA45Fv_A1nG)0wq=)xFy=jALs22mN0szYe(()(|g5AnvQD* zwQ($O+~D5jrCpbm6+R~ZS0d*d+M>pHsq#PBJ@0mf$@t+m`{G-ywW8~hNprs9(ch*}2S^)qk&FsF(PS}KdWU+tVN75r_%rc~K; z_WQ+B`a$Pb9a48NVejfa^nuXYE<5czK*1N|u-T(jNa_Q>5OQ+AgQ?3$S zB`L?0Lzr@1MGg@aa!NvS$~lK^k`7nokU6taA!N>nIc&uhwXw-*7?!X(&0=P4GrrID zzOHxg-|hGME9;-_^?JUZ&&T8bxF7yrc&gWa@GkaUhYVgi7HRO8!MlMHYYbMrXz$=F zz!RLm_4J%$X{{>vrk^ zw9-ocRsQFt_@aodcQoptqcBSep`ubZ-c2v277O}j#hZmfxAv7ykPlhZytyG%_SZP{ z6-V*U>V3G#1B=D#7fzstp{89?kA7hDt7qVt9`sAEdMwq#@c$EPFvS9=MGETV7aMh)-VbBj!S zi%hjj1%kl_8-18ybP6vz%U%1^qmAWjjGXz#D!Zd&%S@%?UEAR65rZuymj-9BVE5;K zw60hhG&b{bvTwA88yk6M+bUe$2rFRmh z&PJcn_lXwZHWkpK8^Y50v?&h)cOhbR&6lSUlBSAonEwo`LHz|JdjGBFez*9{wa{q0@))BmVeoDSci!YR=q*oczK$yF z)-eXb*;~cH ze7$S_>+OSW%$Ciy-)D~5y@U-f8DllV>zC{^v+JTiJH*&9w2h;2WiZfo0YDkOs0e zVq%B3x*dM+!Eblf`|;t52uUvcS7WL$(yMhBZcn5eYhyC`L@W+P|5Eez))FZ&R6<$> zV~op4JAa!L*T`>!QMU%~54DtS>Iq!O9ZjWFIUC=0`-yb=g=%PS4Kb)xnY!@^*O>yZ zE#Qi4EMtW$_Nn+&dz<73FpIgqX`rpg?xm1}B30&4y8BRoyL3tuNVT&%Q zbtc0pO8O`3E=_Lcvbu0OiVPT7^If2K$+npYWmG$Egp$({#oCoU`oyNwwB9i15#wNV zJ;AA;4I0aVQjqhH9uUlJ~JDKd4Cwt=*{8;J!*+ouLvo!UFN5 z++u|hWL7ok{b(pQ{hY_};Kl%^fswUnFs}bMY(aMUu{maC#P3N=O1icS_U(oUGs;m| z4GDyH;4JNNU$c~i+>pv5Ucq_K>GxGyy% z_5FIX9oJ`Li=pU=qWHi(pN_g}{eZ~UJA-8cQAX_Z8f!?L3NJEH2RN5G{N?PZ$^^$) z_t1WV$f|x>I(gUjgyi^uKmu!Mo;9{hE((43>n8<6!x;pm_mb$PG2f>?cyRuX17z(| zfk3fn9CA4D=M-M8NCSV}h!c@)?Z_B47W`2!Lgx?0nCCubd5m~x;pDtrb2y@eM5hWL22dy&|F=MgVy0Fub7(DE1A$gDvRUfX^_9p0XQ{|6U&)#1!@=?| zRm*anh}}%jXsre3O!MAwPF5dA%BKY0L-}jj^Vew z@XgEd%=o(GmF+VnPCeHcFj_o~MWs2-*`pAieimkta_n%Y7koL`c%&t*7Mb0#hI=0M zSd{dkbwXU^oUaTq2>SAeOZaGx@6(jO;-d=vCagOl4I1lWDNDVh59Vf@K%``a--9A_!o zu3%6)5AhyI%9`)LR(TM%7^H4h@{GkPkGDAD@Y^}X%OiI)`D z*8*8@NY?Q4CQZ7C$@%$;sjf2j#mL1;$mGNc+>x8+yj^btB@3KdZje7LvmDqZP2$k*gvylthx+_j&;&l5fRM%*Kw(beN zH0S{*$pDVf0pUx6BeofwHciMlGONpsS=!Q!Dp*yGUk2=?0qJ|wFlp0F7Gyf$_&X|Y z$u3DBIFrMTH0WZK)`NMvDh5AOtL{z(LeE7PMPIL+uR5@GwME1UJG~e35=T! zu{!KNUj)}bdTyfUWXn8>c{k~!8vOWF3T5E{bunZqm#Wz!$U7ObZ!f z>D+--7z#0yQElmFcL|40ixr+!NPYOnieY2%9NB}YaevlRJ=i&Js{fHxMyAeoTZW=% z36^m$Lz4%+LeKEE<0q+lqyQ(6ljxbi)*6Wrp9rV8d5^NKnIwmd8|!tA0ESWvKyADl z%R3C-g)^hvaJA{Q5N#?shv|?l%-hQfQa!iI;>GAQe1U%7hD~SO1T>uQ*N<&+-zZ>* zifeGoU+SJf4O2G1!Sbt?vG9^#mggGVsny)IYa>r)DqUN5CA01L7*BwBDq@)Z^ogY%YIR{KeM zjN!$!njGnWp{fL6d^Nzcdl9W+IX}GkO`SpGKpJ(FHK9F$ML_pW?DWBqv$`KkJvTM! z(nfTwYPoaZBm;z>?XjrN&1a39o+e-g&!6zzm9v1e#T+(9;vy`ZayDXG^B*)ssIVqN z`zykbu0l4UJWvR9^<@w1ge@}6TTHs)6^e@>g=aD$GiN7kC9WrLeC`Mdck$gWE)*a$ z0hY$OGy=l({g(`SEc8vzeoHDqq~}At56PbRzz*c+W2FQc5~E|vw;NO^^qgiI`)ab3 z&pJ6O>P!EjTe-~-BxywS6v#31yJo=4pRyNHOAdSBr$KGb3GO+B3BwK9U^92v%9fJD z?t2N%$CZF$JAfIafAl`Pr|Ros?u}(ss_*}~V7Mb<4o_NVp|i9PPoFmz5;<|ztD&fy z!HgpMJ$kt7RnHwVQf}S14q9yXZ7|y3L6kJ80gw1Qd$^wtU^wMQUoitg5;YEvd52SySUO+I89DY;4WhbDRUd=**d%8>jaE z`xyH%Lo1t?c$F$_9I%@pxKI)QS*Nj_AK-s1=dUYU*d1p3E@>ZmQM&$^xBmE>SJS!7 z^)B9u0+Y67rwT#pI*((If zq!G9X#kKc`(|(ebi4}EI+|aw9=cll}%);#?DZDzcv#bMv=d=&q&q5+1geGns@H;&E ztr`akX)f=+M*8!1DgT_Y`Q@RvFk^7%cFa;%cjlQVh_~O0u$5KJ}zaDKhf_L%@1#Z;|S? zPkVktQ&Ira*!tUc6>1gKY@$f@PV{W=G@-CWDBQSme-f9J;a{{%>4_ocrAyZ|H#q<0 ziKaGsSpG^B;h_mJuEnLV0UMkv9hA9si{K)Gc=|aOak_GRyHH*yEmD>0*22(9*gEa} zQC~ZK^$^|4MOd6V z*!PO&PR+7c+_t|1WLxp|_Dh)iKNIb@`f7#JGserxd4}%N5VhI8otkEGs?Zp8K|I(3 zS0xM0C2UYP9+loA#M>(;!n&BoH04)?C*sRk!1;Etp6cqf*(F-7a73D@J=(EP!y`op z75hX=3l+R_J?@}-%VJG)<%scSHNlZuHRTWowe{-M4C@I!`1GU#M2W3?(B;rp|LlZS z?V0HV7juL0(~(Oge*aEo%%AbsURB6(;#E)~#}edFHh<3jyxv&U=HcNHW~ATM*T{eE z-Qd6eDH(AEcsK`L@VC5ybo@{#eFt5DVX|e%G zi)WzWcu+0=#8Y{v=O_CJhY4`w*srYRa3wQ>JRbd3=#^`?X4!cQTa@&Pz}}e7@GOsPsVJb{&o;ppWKF# z4`DB%FUHx||7fqy-7-?NbdIVX%qny?&cou?yVa%ddIwF^dD`UWQgutNA8U4w&Lvth zw5aRBz{x8IsL|n_A;tHXe0+&_9@8i@FEkRSF&=89<4)B->YwE!N`=derrJCj%W+@T0+1mgBZ>%1t@`tTL4J_x zyDvQ(3Km0P$> zxHma+CxSb5zss+6Zs`6Y)_Y}pxI;kR8GZdk#HO_Hs3F1ksiNS^^T?h5((r(#C%S0g zhK2N_Gm(NWH}adsLzLwAx4}O2hFrIXTN~Pa?oY?*N!ApIG`SF;iqYcd|F*fLL2y?( zQmcB6s-bB>IAjReDQ$F0bJBmRBIGCAc&k31KH$;>vp2`4+X(3~ma z&9v01;BG%;+Rx;~RwQS{4n`w--*hE;I|+raq#o)bJ1~JAt|2POZPT@p4W4T807I$U zXjQmIkXT-Wb;Kz2+&ai{dA@KLr|yh@{DO`K(6#m4i)rXbZG#^9cFIu zi(5@|sKqVaGynK`F#jj`n;9#mBlekKuR%|y#Xem$xccF3!AgzVAr_>^=MjJVe&^mR z?Ok@4;--W|-!yt;z<-$e>!v5>|(JZGBO!jI$DjlIz_rj5U<&bUB z4j0B9nEAbEdt({cgwEC6{^Bt~z5XPx)Vqwo|H>soL(eO;`~1B3A)3Q2{LgJn4nBw6 zzMZ8r<80Pz>SG-pEzGm{I(YM!$6F*~aSh@r$jfA-vFMG_@@+@s(F*!8ulHV4z~o&> ziERI5aeEy=X5l}NQ!>3A;BVFkM(UdeGz-@u-vUSV{`zf8_XP2(45i3uij4`Q0ya@V zrD*KxRW9zSw(9+tN_BPXI-y{KEXX*!w4U^7&#U~m{m_FsPWnd+U+5nt$WKQaSk@Z` zs=sz{B?VgdG(*RtI(Fy|%z)OETvx98YS{Vq$(1Og@PjGL7S8f@Td3FKsc6t-zKv|i z7wPY0!Ib`8=X;SojRGAO+Ln;t4?*uab81Y`L7s6;GIp`v>RV+O{4w# z0BImWuF|)%tzqx2(KjB0Wmm5n_xn4|;~6==QTG=|GP-;M*Ay-=Uc;AA1>a;Ng1(a27e#^!P^wqfYAKv>st7p#@ zx4dSDGOJZh^4a%S;OWAZMpHh`FNN`9n@hq)x9}0q{$J)4Ua7X`tq|*L>Wru_$;_=O zdpZAPW$GEY*!_Y_5O%OJp1tDSzC!yP|9&_pYDh z++HFrOvJXR5)yslXEACM{tr#j32^7cnhz!+Z)zPn{sW&hR9>rW6Of+1S9v!AStb)W zGPYtiups07!pi=0Cg&{v6`4vk-wii0&+R(WbgDA*Eys}ZB}%2`183}KwI~Na;4igv zlFar6J>#1N0J2AviF^OyXviV0-XpUj|*LehTU@vAr~!BeKBQMo{%EmHaIV zZWDaSo8zyo5ykvF)GPB`6m%2`Kw`^9pMb z{dGn3Jf49yEtp)=<6}B!v6RaUa8#`+jh|-1`(Si!V{Z$IQ+XERnUAnEt|wPIel?9! z8>2oQQpe{Ht*@(Lu9h~gC(*-l{d*(k?6(73jd@jR|4<)w!;s?i@Nl);<5w7qz~N-S z9-|*ue3BMvnHz$gj#f)}KW%V|=>W-Ac$H_(->E8H-CZ&u!{1O%z-{O7CULL%_#6rk zJWla-cjJc|RQ$K*Tcu2O%-2@=3&`oOg%C=F@z?w__oB0CvmVKodf|sUX6biUJ$%ZO znNPhwBaVb(9%>2FFqh6$q9X;%cc%7YvFiL`(|KV&z12=n^xJcrg?RMQyQj8fo{zJX zu{p>}S%FTYB|b?7W)}1vNDDTfofAWyJ>n#q`B)YdMc+;zt8)KcW4KjbmA?3UlJBF@ zj2nZCn3{yXA?WIe>S1^6r!nL{x!icJZkIP06UoeOp)-^oC@uJ#2G@KkRzo)f}IRsbrJd_os@) z2p*iFjmfSx4`pg0Ni)tLD_rb3&T@6T&t4W(0g;BvLcVm7 zf8;I{;Ta=X{FOzcZCSE+907d@jv*k0)m7WYj}JURnIHIq`0t;aZjG$mDQrFchvw%P zJq43d4Smy}czesGJ{fa3ndMfx<9#@zL#vL7hYDAH?GZ0r|C+2A`m#m!O}w{)QB*J`J8vk3jUgbQ4l5=f_yzWM&rqIv1H;G2=+i|&~$ zT=Dq^M*5I6|Kskhz-dSA+@#BvH>JiR>*#*`v5=IMF>LkRFDmMXD&=ai9_BX|oEs|S z{%{K&G_or_;oqGXp5GDKmiWHlE)~~zn(65b$Xi!;yv)OR8s>_NF3e*Wcu%WN$3j9d z8B+D|A$5Jbxn8urL!|7$vBIgI^XFFNawyMYl63dVst~N#@kCjo+ zOcxzx%;O?ANNynSg#QWnaZExFdl7VxL48_2PL7&IaztZHj&#c@HBLJ`r-aUiFouC{OH5_#V==>(;iO)zE8>dlOKe9M^_ z@v(~TW2eH&5ANSUAgSHO>mMTSM4x+-_p;VSPlYirJk2i6+bd!ig&5eg{}$?hUJw3n z(UTB?Xx4co>Cz2UL7zB8st78=&*%)zO`lldiT9T55k4 zwse($?I6AY%I9dW2#g=wsLgQ~EF5mH3daM0J{vG%%=bmu7euD^_Qi+m(2Qh2 zfO4fU4X^KxnQ{rT4-T9e=fpk0?MEp~ypIOyChl|vhi8(M}dSf{XSouavdmk}7!)C7V zv(DQSy?>9J&1G|9mNF<0VjdMG))-;n{5L(Y9tj~TS3uO_Z~y3h|l1K#|ijW!o5{^C9wPtE8_=JLA+>OAqNe50- zaDB;Rlo?57KKg|J;&ney9pZ2QQZW{N)bo0ytv=^5N3ldzUo?whz-AcES*0i0o(EeP zMl3E*{MLa>^MB?adcLgymKPgY=J|k$p)&O7dGPM49CSG5{TWo2$4JlADTxwSK7ED2naTTYc?`x*^@y)Aq?VvqEREqRb; z%deg$p1JR;A&M^5H_y{OWR}`?7Y=uAi@}pU-`)cQkA{Olvc6aSx`~TS4f^vi|9Q1# z?}8#LWkRem9w`pL?GtkDKHraaGYIg5=l$$tjm>uZ>u*BuImIQ(_^G8FBp+1zcBZkl zlEm}IM-)eCWKtdgLe^vb|Gdpv9?%!C{VYTg`kebn4|^1P8_iBeh_df!WuelYjP1CU z{78eV!fk_B1?f&3kB(LD_%#7=Lk9~veDwTi#2L;${(g1(u6}*{=3B4{FrOl8&0Rm` zUZ#JXX=XgbevUFqE8v!r@7Ey#V9BMY$t|ok)zLEi#bk$KTiKJxrv}yyZtE~-LK#72 z|A;O@3Z?j6`1^V*5xBmlD+7pDr?qbJ(lFP@M>lQ4bZ&2}Cxop#)Q66&%sbbt_E}rr zLx1K#9Wl?3TK9bRY!wQ?-4? z^{bB`?fm7X#<>PYVawm)Go`b@(3seqY!Q?ICR~x2d|}2_R4t zUvEbi;{MB|_-y#RpyH=(J69!%;J=Dj#GRHQSaN?(av=Tqd($0fzqqx2^^;%14+F-* zpbR?MNc2Y-*5`3H8|m)$UYOz99MjIho$+4S%?Fc7c7yOf+cMH}!g&8?M}>@QwC|So z^6b~^*G_`V$0?570*sr?Q{W(m)v5e@HKKmTPAQi2Q5my1hPGLPnjqfqvA+L2UK8~?xMOh$s0czUj(&=t3CDnR2d7zpsW4!R6 zSvR9KclxS#-36>EE}Z-ji%SUJK!vM$`S_AMz8|j*-RI}ng}-}uN$N>xn&DeWyj8!= zjKBHj1-U=o%+IYUie>SCx`6iPJMt$EY79I}#T^x0`sze@+kk+YMBo#FmXnETymVmy z9YS@90;Y;RN^v=@@|S^_L%XeMiyyD&U01Vx*Cb1zmSF(I zvKM1lnY~pZ=i3LNSFTB(1>|<;wAjV*UE6miTBn2?TtQ6P2e8H>NV-c|UxDPsE`4(Y zk$lB~5b0e%Cv9&pbi3DZ-3;ObGD^y$C?LfUj2nNw*9{J~&aPYDVGU*qjR+BWg0mX5 zfc)0?wpWCWbUSwVu*_zD&N5m9vpz5np_E)tRGfOw2|e!krZCcVrY(aLb3D3p0WgI; zK0U@#A3L7CKe-O5MV@fX4L(4S2i`IGMoP)K@91LIrMvQ}Q!K9Go)z7Wk*XVim-%l;%>U=vkpV5vO}-bU znScb|{n+12Iy*bbrJ4fhzKqX1fw7GqMMxD!CFsUc%m1Wij6n34O$UfWUq!U{mQ3QYL-8|V7w>m&bqMQQay(3rlRO71KD za+xf8b6`6sk10n(upd5?XV=$NlE6!5eQ%??KRF_U1Z2EMglv>1JN~iqv{B(O->7Ik zX$1rmSnH`57S1#sNPD|@AhUO6eWMRNQT^|P5{$WgNVxVUF3n;3QcPzu-%DA*V9*ZqY_wEVNyn&?qvN zL~Zd>PmmRNO`mxOIVX^WTRX)pK!E|U(-B{>d`>E_^Qz_!)C7V<;5~h=_}ALr|E^F79>|VUr0r`8RLCf zU1cnc$N|J5;JoLso1dcB=^Me0^}W#0w(uEcL4ftwFvu2by>NrOMzpofdEGFxHAV1_ z+MgB-cl<;`Mz^Z5zAmDK;?dX@gk(REC;M# zlw18FtB$+iK~tLItcY2tYVp{R*WKgq_Y#l?b!OIY|GYpD2`4wpZ3saZ;ht#kGwPrn z-LmT6qdy;|v7O`U%fr>-0{Z)A@|hzh{He{*R^GP?`AzH@C(r5@DOY99m@@=v)D#qL zbk!_;jkKX+%(XP-3=f=kBe@uL5w-I4ak zkexRdHaCjS{ITIz9TbDNe#R@3Z>Co!8*DNP?+D#u>d(kD#7rg4XM8>S#ob3{ft*JP%2)~dukRM*s49k5N}@AxM?joJ)Ft^Lel696a$PJr^JB8&*8+yiYjNo-dOvY~A(q zU}n(8T=TclgeZ@>R{2D)zc zHX^{@Iq1B6o~yWf83lt~k)Hb(F|kJeU-(-ep9`+F(=yeZu8E0UgDW#6WmIk~7kYdG z<^_=HK{2tDaP0`nXz~j#8I>t8|IBAK(p`aNOnD=6%?kACya~ZjM%4-`y}<=I^Vkig zHY7fK#$9^`24O?)OdmE-2u4@(%4_w}E*DHq%f;%8oi3DnUW?`}LTz;c56&kbFv&i%?t}-eD3Xlgi?Ju}SO|sdZy~t+iZ%E9ytTp8 z18FP8+Ol>dqH?VQfg^hddYq~oX$|j7s8}tT9*QL+g0tuEo=z%pBf|)ND`Mn1dEYw|GE1c`2J~c!f zeU#0%fUTuYOr1#-F*T^k+8FVW_4M9Rrn}R8RCH^FXjG4yl$d;RKDRQYexdCv;PzrO z)SX;={vfLOW6RzzPY8Z&C6anHK5OSu11I`kKG5N1{p=EP1I)!2fT3AvAGZ}3I^vl* zNG%|8HUIN?KQAtWk#Jp{{@TPtabN%as+v8bXIQuZakSoj9vBXoy?>@V;jrz|kyQg) zZAB}3cdFDAWoNwEzUX$c3>N+N&+vUQChu#l4V+=L{zpLEvDU5$wLt*J1a)Sn9_k9b zRC|qRw0B=2(bkTo1i8Dm^JZtGMz0DSB*kIz4%>oG{l0RTWu=egXCn38{Q&byVC$t_ z(5B{p(NXTP)Cb*kMM0uz5@CIXr2Nb@iS^5SigN?=xc|p$$0Le@9?!M?|NbRS3k2PS zikjk|ZvUmqdN-WVAfEXF5K(v$sV&QTxXb(=yz{rI!^2V%v9&#Y7MavWe#+g&+K(~d zHPO1NAai#jbosfqOaQ)@c}Z6OgFn7I3~Bz&P(s3v6qe^4o%QiW&EL1@?Fj2f_u~)> zUClqs2U%4E?+7EA;?nzm(H9d0Ha0}0K z6MB%o{YA1=Vlr6Lqqa#GaY|!`lN>9^*<`P`(X~llyWu|GYgQv-Ja;f-+02dRp}lv5 zkJ-fSbiat}dCQ05(aXmwO+yTGJizf_56XwnHZ<)_7fW6?VNgySP&HZUR`<%bEx&w6+n_6;8~D+O)UQTnb{#f z;5*eb5D)e*KdBs|LpB0P8~W;#Y>1Zi?Wi^D5BD0vo4->oUVrJ#_H&xaKDGTK}F~X$8VD@C9Xa$9Kh)e1{0V#RoL# zx$g^))`96U<aW05mq9QiR=Ib{%TJESP{!4PJ#EqRZqvoM%p}AY1hPb>~X#Qs} zyBqMs2_C?d@kFMq(p|hS=GY-XH&no~WilqL4GddRsb{|-pxx-V&%G2QO}@xnz(}lr zXN;@S>|7-&x&Ft(ZV5#^x^d~cZGg{a_Lb4mJ=3lzO+n&EoVvSEpLde#sTG&E>p>?V z7Js%h-z{2VFTGI+6toWxy6_g?fM(Bvi3fa~vIYd-Vi2I9Tvr-kg$;tWYCRlSb~YM3 z!9*z#$@l$w%fI-eaL#)Om4cZ;&TAB|_6Gsy76AfO-Qcju49vu6uEb{@d2PsLE~i<; z9K`}OCvSO(Hl;!g^<&c;GGGyNzb1!Vl~uV1OvuYy*!bx7o01k8s-DFW3&AFtRiTOy z0(Y%7Y{K*zOW79r)s`r%!E{cqctk>QBx&K6Y-P8Q1Gy@VDui+7uU6RcTbD;xu4C!9 zf+n$B|EwLpJAYjIXXdDBl3G_I&x$YRjRQH~*hr`9qdebjAzC~Ai!}6)h84HgJmD0C z>vq*Scl-*Nj-9ooheLD*+ws~yPHR1CQ=?-Ff3N7AAL7Edj>p_TKR}&*icLv&Pv5{D58ne%>-}MivX%hh9qv*oZ&Q?EBly&HQ0^XZp2YC^B(Bk0oH`*; z>9+2chJ-m?9HMVXYDuu;PR+?o*4rsDn1MHrT74b#KwHs$(sBAU`%?^=5e|Gp=0R>9 zH*f(!k!f)~oc-$nDDH1Ln#K#@WH~?nsOHItw8Oz4IjICODFWbFMkSPU8n`6NHp9vYOV;vJyNwH=5@vgeZW8HEVP)jK;bh5c7Y8u z&1CU$%-!D~E)rn_j~qY%Q=WW}v{=DvHGg~r-RMV}N?a~@nC#uxP*;wN4bv}P{pT(} zmHux}@cB*3-J;*z_dsOkBTl%>&SZU;MKuGS%YqPbpVPA<=2%Q}y$UWSX%w2W?k4tO z$9Bz|wpWttpc%%u4f`m!le=*0Oh<;{q$Z?<662W%eGOiKs;oCHO9_J;ZldDR(0(<$ zrQFZJX`;Du!fm(FU3?RmdzOpm;5&&fi_lxG0RNf?ta}5t?4JiNbRG>E*{=LE`3>|B zH04cv7Xj#rx$V1g8t!vSukoc)X_Y|A-mWEX$l8n!E#nTREvSg0!NBuzBu)$f>FpXc zW`F_nIfg!VfD070L)0zi!j@@)nF)myR9agHD7ijvc5~jHhr&=%L~ao_Xy1`@t8yI! zKq=n*<>`ref5_9oJCE9!SzaQ!DJ-HGsq_RUt=iai^c|Z;p(T0~iEE6e>FegkVEr=Q z47iM5>nyuZeU@fT7Z5&Umm#;z`*pU$_jAZmZc zp9#C@2-*E7@NGr3H%R%udN$%}0uyUuOTn#{@?5QTFgaPQeb5s@zN&9}?Y|>HwGcOx zke|2mev){bebRPYB71Rs$OXvX*Pq|uj~mCdMoqDhrE7pgJ2+HH7wd?wa{a~pZ_#?8 z!bX#z@IO~9I>3~ zdet{HSaD)d3kKYDpvgR&)AfhsB#C#Ks1ee4Ftes95a@F%x}Cs8CTn2aP^j|le~qnS#ngS=~HD=@FJMt4e3fC4i#a**=KT#xDS1iBPWI7OapDx zAgFH*m=*CW-o{|x_k*ee4xTWoF^meY{<4k=Im97wqIcPQ03I=(#TR0RD7q7&<-4OC z{(``MMH8GWQ;kt$PV%rEq-LsIn7AH}(QIiskz5hpu))(xWla> zW)t?tvjdwz#(oW0X5?Qnt#wH_G}e|#xiXcyk#~{mhJ?$1!RQ=aJmNtOO!7MJvCIlY zwloWuS{9<9wswry`hnh2s^YJhg|Af^c6l3slzig8OytI|BQW&-+(Rsvgm_fJGJ<+> zMKC*i`?QnNzY)FMteOg`TFMnjDRJngwcqc#L|b3f8UmUkN)SyCmp$AI0=RPWc}3?vT3&OLW3C7 zk!gRl@(sKe^IJ=Gf6|8D+WamWwK?x9rt>XSeUnO3o^C~@SAjV0aPE#%#0AKRS&RAY z=uD}kR|*gv>#7T-9yTpxl++Vo-*WfDRJ9FIMO}M5e5BxLBM3ja$~gphBO8Ls&8T+% z-t0!HPtq_kp1|n)PNjo(=N1gbS21{5>*_N6@e>5vH1BmQn%-W61WY%!O==Z=T_Q-1D_*9ZRF4z!77A68p^I!-u(ns--X8&IXQV z=oHa=dcP!|D^+HRjgX^-ru=+Oe+}X3E)D6M4e9j1azvDB&(>V?d~u0@ELTnVwfNbb zz2hH2#e_qrg>}H&q>SG5%33S;O#c$f4)N21ZgQzM(?VZ)8&|)wNW@pGtkVcdWw{Wv z!h1NHO#cS1f${x^nUv?wyHqgmdnhllnu3-6$O$&5bU^#qc7!@DMM8wiT>ikk!B8@zGixq}-d*hf_tVo<{Xze%dUE9U;l@%_L7|j}lGNct z=OOd6d!L~FA>Yhw?j>d4*$nPZ(7-!n8ZT;{Z)ZPF)qEA-y35XV6hpJz{wuX^68JjU z?a^W_XO*>?CLgT$S+3ZMwdT3ye!OjZ!ydaHQ+t;O?Vq4#88l0>*e*SG?#{N_gRqz; z{0#32^a2L3_A}|nYbuUysl*WgPd$nTh33+Ah&Q7Fr%~Q-17MWYLY{sDPVrU}$yeOZ zi=r?LUSZB(2Hd=gGy&v8!rDbp1jg>c{H6ll0o01L<071$whQ2YrPUFTA08?wyOIRV=`cwv2(VYz)5X^Q`{XE z5=|ao!+jdWF!)y(xYE4FxLNn*4?=^4@e9-!?P4HOglF|_KaQ~$&0}`W{^0UQ0tcdMiMzrrgC@^wg~*=w7n$B+ObtbYVq z++|5M8PKwqjdt2wz2?Bzv>FLTb2}5(%>$4iu!-*m?<4 z&e+<^J%AGFdzy3oAwJe{&qbo%EhmE`$cCJEte$F1r;vlF>Vp?Lu~MJBEptWWF^2OU z3jEVzNGEQDDQ1U&w@{|Rd&{R|?%h^J#y-dpG)c=7egC~$g417M6uCfIb4OTHn9Y1DL!I}Aq6k)CG+%`brh@aqgLMDzCMnE8mOVXefw zn1+f0Z94OBouvfx|Lao)I0iD%z8o8xxV1i4p;UMJ93Aq>N+Md@QuiGevMmWQ*$5$y zgCvFcf?==Ndv=gII@qw{qx#TNuS~TI)&PDtXbM|NZIPKU8DPO^^TiWyN_4~@ z5IY`l;Y1bbUr{yCOA1QP%@ySo&r8h>OlK8oHACK5NE@|3GjD%3y>A2SG0Ie|`Ziq) z6FkI4sLft(@8paODbaQJx`bYCQ~cnE>J`Rhpu!uB08YV$^YMA5sw>qu-#;a@OBfMr z1vl15j=@=t-z-ltU#R|-OBKK)>X_I80PJBSSyBFl|18qCe5)Ug;P>r@8Si!XUjL_; zrVCCGp8xSfbPysuYuFHv&^`Ur!`#WX87{Vf8&4AFjOtt8Qet2(Ronj-3a72mJR)E8!O8sz?cLDd zd!&^KQnyB<*}BGiIgZz4Kiz)Z$gIV+4sGNe$L#GubTT+4F_iIpQ_s>QyMcC!N4T ze&1+0%E?BSsL^laq_Tg)Ie${CI0N_f_uF;5sp@Z%{`Ws6VmW6ehh#2HY`@L=<=>dn zcu8aabXr)JpJcpDEZRx(UW?@WggRxGxl@k6gk(U5&knL*`#U5TeNfwVO> zsTi2s3>41$xJgoEiqQfN?mEo3F9ZAjRPkZ|o`_q-*2Nl96g{~IRwVMgpM=W@mJy(r zRj2G2zj4w8GV#VIPxcuc^{~fdg!MSp12&5QghewCG=jAlwTa3W<$6-i(@vuK>c{;_ zYl(qWp5lLQlmt`v$Eo)Hqr847u^E*4uCD`3JS^@mUYVTkPt_dJrN`9n+;!>h-S%#a zg2=denNzf#9IMZZAW5U%)gXtq?cv8Y>AHO1OxGwMy1&wHB$&M)&6z^=PK+Eo#GkV> zxxX!&jVN)0JW&_9Y1GH&vpSEXGrecOR z#XKI7XmEWeiFYx;U&#R$wp|#Rwblk2P_DPDqfOBV-u1Xy=AFrBUPf~v0X;CQJ$c4X zrH4gU`$s);*mF`Jbsx-dzI}f808Yyj(fr8{VLcIYB&op&II{bdKWo{qpmdhgy90fW%;$y?SocN7OoUd^{gtAd3q? zmtS=rc=azqFSP6ftf)8F{Q$1Lpg&vEw(qQQ_RIL6t?MZ<$C>`*ggk|wki*gkkHqZL zGnnT5mhjzwHEeeztc1wCY(f-*u&m$VBrSa%%uf$s(Y68eA9b)t1hi4i5xS(CjX?RMfQ%xOJAgxq0_7&T&MbW$1RK z5nGG=`IiEQwCV3Q{{EeL|6>95{iC~NbHzZ(Hks*WKv=y~cw~1&Jn^WF&g3M;i051n zDtXr@OCW#qIvu!5;&wilzhL4eW8>By$FzWGik&p+@RL|Adf}p-%DFbBuMsv?oGag% z#jE~0Gu=aC=t$?4F|ax>iw)_pG}hqA>E=^jWg`$)jpz}*e!9GNDYCh%yDqukNS(gt zf$TqG)z}}H#P_f1Fbz#H*spY}wL+?la*gN+m`-quV^Maa=(qeDH!*}Zv4-$$NRJ&w zxUWL!1hL1A_(ZI&Mu-HiMk=)=F_=ad8{3@eH{Wlb+g!&ivH)hwRd51gmmS$J9|VHp z*%1N2ZRnpR5gYP9R(#(-lx>3&jmCE`0Ritg?c5A~bAz|-QKofQjVCFJJBHj8r8N+M zD*{&}9-qjI^Ixt47-pdj4O;?n+uDh+!nHX&qAxXV3OEa1H&%CF0X-(WBuQ*%gUCCX z%Ssq<5rl$K%C*|mw?c8r07sYhJxcUs)T^)g)*@DY!4d~*F4)wl$`;L4Y^O@pwhZ< zn1mb4pFVw>2U}YOEvxD)^j{c>Rp{7;xU*_Rnow#3=Qq-T0b6r^Dm<6v^YaH~Rk)wu z8?18SG#GHsKC_A&4snj_rfpdkw`VT)kdqWGyN4gVqt~oNr5Y>?QP0LJN>8jP+bp+# zty>)SR+ila?$^RvF%nWw2c~bYf-ds)1B;(WUL^EWNa`(b&|Ro>n{P}E60d$jhm&Oz z-Qzg@>{YdyqBn{5CQJRBW;Zr0f9I6ETxeC}dczJ+{x^b1;z7)GThJcM4h0Z8|I0tW z?FM#nV~}0`*i{G@eZ$r(ls6#PJizFOi7sl{Fl8?zg(S~CZ*jB9oBhiZQ;p&4El>HE zO{Qf2Lic{1`w*lL>r%QE)7(Yzc7a42gM^?qw&wjg?F!|0YuYR%yrR8#~kD4mR0P!UlPQCd(0L`r~A zgpdT45tU}4BSe%Uy?00u1(X)0_lO9Agb*MkfrKRQT&(lG`sdvrGZ^&d-gD00d+oM1 zppZ7qS{13u%Z`f;Dn*Ckxi`ZhVe)Oh%U*>QBcW?V| zU&1fHc3->U#{jEeK8AjDU;BT0us0w}eMuhBd}9L^#{er3h|mA{N3n_jquv4!ZeKCq zV}&yI2I)RC$cMx5puivz8gx#ziXuQ$hkiVMtjW*d%h!84{v+{%#DB8GC=(KOEFXf> zfPV4<(MJ`uwA67>P&>j+P#*VVjcNma&I$Q?k7w&6^tm_Wj}bwq3l&&UsXB_(N!(#H zpVgDK%+rDZ5$IC`K!Z?!Mm7Q$xBu6OifAfs=v4qxk+ALhcnc z1^*p(2E*1g+*B>7&u?#7VwlGim4Kh~{0iYtKW1DHK`Fi-4?}(87+D zU|C}eS1H+{2x?nww8tig-0u$?h#L5rmtB5K_qKk(3al0y@czjT79ZLb@*EeW)77sE zgW{BGtHtWo5QJ)NKv&`on$xI|tV}N_(<2|TB7LFckp64Bmg*4@)^@4lvj>!>5b7`n z823e3Ch!|Yg4}s!_Q?t;_gDh^SuuyFDV(lKYG@dDI4wx*+9q@jZolV<$?L zW(0sshZSk}#I8v*4W2r*`(<(RE)pP8Y&r!3gyz&r5M!WvR#HzxoYRMrm#UNIfKwK6 z(HA&VLD(pd32ji>_4U%^{Os}i`Uxck+Jd#uOm6X@m%PkxTT-G099JO8LMJDv9%&P1l0tPE%3(|>an5)JH!Y<9O)h9z~r-PK*ihxvDZqE z^xM-SV?Y;A*@2CBp)Gei8dr3or?>Yq-=aaE=XI(|$_4>?x9mp(<;)E@Kemzi-*RnG zF12chZjD&2k~$v@fSqzRdgUi^0r`8gQj2b*Jz8{B%Ee~;N&Wy?;KSD(^Z6|SOcMYD za=c%mGbmt8ST@X22t`FvJC4vLETFtN&C*hont*8=1-2hkH`_{|1U23-#hPP$los^u zKv4>zB;wRMp-bxUg|k%{s4Q}yqxC34bSi-vy?>8u+vjg-MUd)SJpMo0?%7>HTz!$O z^r~jlO`9(z8`5mY(hH4{TL?%Y%>jR=$9!lzLCrZQ@^+CRsAR=m_e^&VBGX z^24-&XK+{0_FOmz7^A+Hv0n3NTAC6}`(`po+-Ms+0vv+U)@^Tn1=~ zWHJFqUDx6kOslAbqXE^Jf3QGkP6QbvzKOD}hehERe*HGU{H;H~A5!TI1dh)l@FM3_ z!AYhDK(@M))Eupn1(DRtzFf?Qh_SFA7GhkPeLr-?r$LXO)d|qsCT-%sR_uaEviP@z za9zt)Nyu-ffW!8qEfm}&*-4rMvN|(s-2)d~7rBXD?e?H3i8=lLX&2as3pIKKSp(VPzZ3G-$g)QhaJb&`29KB3@uLB)G{Vnbh5l+cD+0-ak2I_o0h2 zufmD~G=Rms`F@~bV^nd}wQbf3*Z+Z(ZwD$H7Y1?bxPo+fyAN_9e6=YWo%8snrj}CD zqMFH>SEHoH0qpF%Cj^$9){?*X%_X^44Wu{oCC^sbTaIUi@<1I@`Ly%62at`}b8dY& zPzq^$!-R-Z-#R~O*A{YeC?+(b?;^$B6x~+Umh4kwOh@^CxV)kRlrwX00w-hH+$YKj zb$5--AGcWs4{$7KQl-)pG?2N|M4^BR3huMrfU!7bzcaEXC^_cSJjz;vjEq2_c~%e% z&w}Ps5OypOvT|KQlyl2p`~)0L|1rR|e&Z#$fQq&B@SuB2gF4AW$Ll70_>Lk7{c60`rIeCNR*P|8b zX^M2hJzk9UnqDiTY|8~{m+sYj$(7!;6uLxWL{N9kbC%c#Fk5}9nZ51E1O)&1n#rtX z(6nk(<4JLx#AmhTipAG_>-bUB+!uGe>!zO0n1VI~QRCe&tD`?~q0*YKSK2SVd*BoI zq(0_TN1E-F_B+0%PL?sE!oH3TS{Aatw{mrnnhB((A*rrz zUTm$RZkYoGM}@vqXSRpxjJZ@!f*q8^4We__y1;N4S`Qg{)S#oIG`BQd@aZvBxNE$N zS)xd!m3W^U0(GL~TX4oy*zvk%Q$`!n?-j>C!*Oa-DHE_wn0FLVR!J^bM=Dgk=qrO7 z2C?)f5ttM%lSb&3y{qbESl8V#PUlQbu|`a2hjHl^8rXpCUV=CxI-m|aIRH^2W}XeW zY;)&hN4Q)WMF~!?Npk0w>5D&LP?E*oM)VBnDs8xrw@aX$?ckxj5PA(Up|FE&W{eWO zXu5KZcY+#VTs3Y?YE;M=7<|SFJu3Ri2;EN7UWZ*`&|28_}=aVbJtAdw+Mj+GK2b6?ot#i;K?$1M6MYnj2)j*s5@*1d`mo%Q@6qr))Ub|+!U2#c(P)n^%#kYtbEbynyxt3- z9B^(8yI*@kpLvmuqh_T0k3tE;S3iVOrr|$ADI%2(()v<$UyORWvP3;#{_}NT_gNCK zlpuG`hUYcHe=%KFM(g|x`Vv-5+Tl10DW&5ccZLzXM#Nc7!cg~-)1Lyq#74$EpC6~C zwq=O+S8dPdf8HT=x2vNph-#wYu7_gwI6gbpr;^yw6lvdCWMtP>|s7h6HM&5)PHTVs&_&i9N zv$Te*jm^=|z#NB#H;cO{ZW1pdJg0Zo!eVtKRB@n1Fmt(76mL@N=jQk95exFpEeHXN zL3V{mn`I~^!VYRz64%>cfCCQusm5ot4OG0xgV0l(BB0D^-4fHk?sGy@X{fKnQ~c=$ zJ-cU%PHk7hjrU=Co_)uABJ;o0&H?eM{fE9~Mgz<9$KXFqX>7htsdu^4XoP_(pi4lz zQ)e;<*p73-_ce}Eq(t$SH4Ilo;zeP`C0Pe4honynrWo%0^!rK9p=3T(_Fe*t;;DpG z#3VWY>Y`MCG`5C=?Ol=$mSok#O``0-mg@T%!hvO;d8d6$1_EA*#p(Rpa z^71)%KSEae=KBTofjpHf?ZT?9MJy%2B_dEZ#*?T7%~Ilbm#`s|bPj|iX`qAU3`tkm99R&^5@ zx$`P@wLQd~&ntzrLtc&lb0qoR<9$osIQFD=Z2&}$Rd&l^DSC@AF4oB%cZXaSuxP~6 z%Ae+{LB79tRsZXS)jRCCprs&J;1nR2v{fHhSlX!{6f8h?29A|g!6&vIAr2A_1*hn_=QLM+tB zP7!tWoh%JT(=*HD9}0l@NhLvDeI@J&{qw;uq$MoWZhuZcn>6*?JveiIl;v!Ul!qUl zEcv&6+Ooera`qG%G_5@tc9u9p=CXAB2hz>RP?$#y{$i6iYZ?_qVUlXH}!&7uVg$2|rZeSLmY274&e zYBsk@nS-u)h(!F`)$;6Tya9alsk#^xk8-igm@(2sshZC}$KX*%OUdjmuKtStD%ir= z63`Z8O~;M&5gK=1)aONjGlzi#UZbw=6SlXuPwC%g?$)tSof>o`i5J~@BX&w_0>PaXgrSxa&CMh+zK-%I@gF!-3G6iFCj7S1 zfG=gi{utVmf`^I1*<71MiL1!q_Yjh0kOmfF(}t%K?BbIb3Y-Fn#b7+MJ#pRcvPpDDj>=$RV2f$>9qiNwnOhkhZDdEw4`sMU=yGG_X|K%(>PBcE^t&k+CW z1a8K?1n?GTc3X4m35^hUFfAWYfmIjjPZN;CX0Fwf(WhJo02t`xNE|oms~}!qDtE*R zJdwm8JY(^_39T`84b<-6uWn;gHz(Z>@WcLL&Khso_9Uo|vSf_rd!|#c`Aw9;q)iN?wvWbPD7WT?>L`>azFlmAQ}CYJa63 zcqJYC6DPF}4ULXhk{qdW7H24`-=zw)ZjJv(P5k%;h@PXu|08n$`PHNUe}DC13rgM{`P{zWW-?khg3mUzne_HlBQ|L&dus(=j2bF!`G+G$&9lFDvsfAkvi>p-Nc6 z_ydO21C`W0JY6Ot>D91iz$>gEeU#<0IS;v~wQ!ZZh8fNX&(USOgEDSYmOkE!HR%); zWjhC26m$zs#;-yS%AV18x8NMWi|OwiFm>WYbSEnE_{wEcR@M|S6*lMtxYk)URt|qD ziY1ql+hUGNQnUx&I+w@7y6b<5m9Sr<6%O8ISf|Y4fT4Xg*D0L7W0Z2Ptzuj^s+35M z=5TmlGP}Aeam1^yP*hT5V|tC|n~NAELHa^tKI&P4KW$-_>#>Ow`PiL@rOP_#{ybm{ zAASMzdjm_1gx)0{W5kR!<;J}=3kWmlTn$U^6x#DOkM#R3bw3!bp)VUcaKd{A&L-+! z34W2se`#`&6tHgEx~Z36+jf%{L%c{AOt*2GHL(E~tiiebpQZ-lSaK|Fd z4?z+6)92S?d!6S9zeE=bmxNiq!U)d2e&J^v|1mXp)(-Ro>Rl?PZl3Dc)%?tl%v)7w}fIIO`c3x_L^<_i@@lJvjFkS z)xecF-bs@64YM9MIJ;M`iuB*`7k*25sNXa3oNcItImj1=!U^66i^g{w ze889k=Q_<}Osn@EPKYA;)Zy{+?V6xc8`Xs*damtS{a3(;o@%?4uq{$+EB$TS2iUXxe^e{LVYd{N3B4!yWlOU{3l(`dDDL~Jwu5tGY8kKaI2u7a zMep{ou`kBik^G2xb^DDPv5$rl(`T*6^j%M1_AN?++Fsqv4QRYg{P!kGL5+>jL?8}; zvNmKq_jyv<6r9U}C$4i(@+syFLGIPL8K9PWNVwU$qNfe2u4g}B?!2WmB{dG??{09g z0bk&Jrck`rWqg8P=*w0}&mN4&EcY@2Kj)@_CVJd+Ed7!>G*g`y%?puccQt|zxC_`T z_n*Jhdek#OKZ%j3L7uK@6^zH~;>IT%X%q>jiC==n=o4+GMUyfGFQmS8DjvLM?ZqSPv4xD*##Jjr{|S5nU@jyGjh7 z-NhLIqG#FCVrqj|tbE!NB9Jbknv=Bw1pdxu2iA)JG6DNX6TW&_eiT>>fjCY_R- zy|QiK$f!gT8H9l{z43gn8kJL#T@y(!^nSR2^of$s^~hhltb|ScR^ZfC+H<)S~vN9P!)!4ZDgA?Qk*v{xHGc8)9`~!zKkyvLk)M{zKG}82qf!wEl;B{mv z1|#zB)@y(sF8M9wQs9l6p|NuvvvaC>5OcD=1HH*)Ui5?FZPs$rE}%Z@&O?IuU?v`t9RVBE>7Hi6pQ-N`f1cfbesj~j2oUu_DnkQq3#dXE+G9hG!V&Itsd z^=0in9PUVvWmXAh9Ftp(rXqgy-GHj>hF{W#d-J`85kkun_7fp!xE@H?Da*6w=4FK` z3ynd-z_IS#yC*g?zNR+KDkFnnbEA+@bR5Fr^Bzt#s7nnZA}Qeo!>4Po?^B7{K)>H= z7f580R5l!jScEc#qP4XjX%10HFU6UR7b! z(tR*f>dXGfp1l|xk2 zSI(Jo+88ysw(G;3rB9Fjhoy?~zpWkbf6h+S_JanSeYo^o5P=J*EfWBIKpiJbiM0$*>fL;{!>e1xcS)dI zvf2W8I~Czj`A3r;*!WR!^1zAEG=(sU_LTODL6R>2nEaQ-b!;BN&s=`IX7cWq=}DPQSK3$iO#ks0Ys~`jy7z@R_kBk5*9`v)~KC%>T?}z|1!KQDDg&2x{F#-61Qx=(cjx*>PKqc{a zjFy4aoK+0yEvT#p$EA7K3ch7Sqbd(6k96Er6?qh7pN3){pmmU<3}Tki2FSk(D0pEpeO{S2f|x< zSnT_HVP>WC(jl;{Pd5ZXTP2oY0 z7y=TY;N&cLfH(mqX#XicEuYC%uSY8&4uCV8t17b_0HxBB8OT#*(YEjhhXr8%k5{DD z&9Zbb!pO05LmK`cUg#T053c+e9HmbO?l!tH#>Zjy&XF+yrAB)@Oy&$6GY7Xs1hV-I9X-v3)ies}YB6SxA-*Pw z*$@g1+k550s;eY3zYd8PNHoaeak1L=f1+FV@l|+d%!c>bx2C8a%y3L@R_4dMZPId! zwfntf77G3Yrzpq!AAUY&laIp2&x5pnq-xvKf9D#7leZYNmO29K-`q=`1wOt#{SjRu zMsT;bBJZH83?}0P6%C>kK5n>n4%>%fEy;KZOqPDMp!PWIkM3Bv2SqBfMR}%w-%CA`P}g zKG}rwMW#B?do%ukTUQk%rd>P}Z)*sDgz5{FQaMP31=$)Vl{)Qud+>U71{2V5fGYuS%Mf@{h!$3JUWUL5ZY= zau{#9CFsxVp?=gNF$3@n;lb0r^ZXx=3w|gv{8j$k zEjh_t1(9Ua^YJlubck^laF;#?Uvr_GS5YG&lBQ_zg(sYKbefqma@rYPR(*P%-uK(@ zy%wYN{pY~dGbC_t;c$1f%@qCbW0HICBk+2bOKZ4zdHG34J|CJ&8aO#wIh(0&x=H{S zYX4BZAw7Tp7+Gq2@1F1U891GmRpGN}W}ccg%p0EXmvuX7zQjhRE9VllSG1(XAOiN+ z%nza_9fKv)@i46?Sk_C}-#vIeyw zyZJOs?foskTbeb=Wg06)tU~=^{c+vd)-i{Ne9)pIu_{9vHc`I|CMix|q+c3GAuHwz zJI(yQH^GQp9#l2?{sbZ&>eVPm<8_#3G&upw%M)6x)63stBW&|cBMatYI?SSo$NNai zxR;-ie~Z-Czv7G}sn2{#78BW*0Cr-JE05o!VT~PFlV_s5hxFs7A-_ z?Qz`n$u%4@UX@7A6w5#N8TmnYVbQYFVlg4Io}7uF?t2RKa2`c*pre#C2u{GxcE2~s zjmh(-4-O2u3N)^MzyIYwel~uK81uUNiYDuahpB$hJ6H(6m~5!MR**fXF`3y!PTMPR zsL?&}L`3AI(*&=vR}b zXo~_*v$1}{X+1;PQq=f-#^*RT$MqEHVt@Rr!RuTFbn%Ghwm|Hs_eUpPQo|-38Ao+G z=0Nuqt2;uCvq_|U-i13vKIz(wi~pTO=$ye0JXAL2#rFVt{q}NBXVME)?OqU zuH%oHTSo!*g@u9nJfG?8Bq87}-4>}J^4vzO7Rh}{Gi4mrtLKcMC>J<-mZk&o&LJPz z*_@HY48D9>%F6Zo;PDS_Ihql)KKGz%_@JquV7i#D=ld6{e*KEsL`IFGx3F)>snM|V zq4WAB1^O}hzCP)*f0KMKCFQ8~m}Q_2tbawEY@nHv3`@-iRC-|EhSnb&Wy}NMexLh1 zMrE}7jNVV zWrMRwskhl0#tJ7~Lk>`=fJS`G^t*+lLV@J7< z5r40D5q^2C9;Zuih4tFhY~+Uu*6L$Y#Ish3s2W2W$Rf8o+n-Q9W|ooV33V|zi-npGrh3($1f(vi)AEaDylalYlCgvfK3iMIuxQHzJN8ioc~eKmQJ zJVC*>XPD#G6#;Y|6WFPzPEM{l(f9{96hR-qv;~4r?$!KU#7nADs)j$-C0I++vn!I0 zPAKlh)i;?*02wK^^7HFw^YTm}W7Rgw`_CFW{ZiViJ{EKIXcr^TuOk6y6W@BI(1*p# zBRx9>q+MNV1N?kkw0(2?%uI!PkA*Tl;+gYc+q=uW*|?Qoip2X7AGf#=&&#t&_ev8E zw`m@OiIi)4k#Ct%VcaXTHOzbDGv61Wwq5aOee8{x-ZIO+RB&*jJ`PWX4vX1$@RMM z0|B|?i-&rw5t{~*ujxXzNKj>(4+VKQ7>I8Ow{Kl&3 z^tK((yK`SlhuH0Q0`{tSyv!!?k}xmttgCYFnFK7SmsY-7Xvt#dV!j57&>32u|4Ny*?turzfO zFNa^jBh;u7j;N~hhCCg&Q}R)Y!95f%_080Z!13ean4n8#EIy~x;}b4TPdL-DEfTvM z@VrPzEF>JfHJbma}p@4}oOYx3yprDT2mqe%X` zZ6~okj-0gNV4sa6wB%IpEB(wdrJDxhygigE1?s6Gj!TY>bCr`F6?hHX-eC?8ZN_94 z|2;CdmCE2HDD8Aj=eOn5JMuNP88vF&xa{*&U^Ywj&65>ETB_HAaOsNn+!CoH;sn+VMPzbt2kGUhhrYdA{yrN%)m$mo z{-uBh5uS?#{x;*e**GSZd38QxVHs4J4)l=mXJbUWe7$@%_WHWpZjpy=&P+l&Ax*MQ zt3`s?akYa1S@AxGP}iF-YXDEA?A&v(YXO{z+IfJTA!wNGk7b#erzDl}vmo5J^=z*fqFOrraTW$o_e@+w*&^j8( ze%9P9qE3u}yAV=eB1p!kcQ=tU<;#k9LhlYP+es#3@ABI0ee`krqi-#8nn<9fFEpad z(*9O9-wFOw$T}{8!7ug~q~pr>4&j)x;B7-LEV)(+$AzhLWz?vdMFMWYsh=qcH~xBK zk=l0O3@sXlQD>i1q7vI=V z)+4H)fk$2>UY#w=#;}kn(LMf;za!qg_FvyyWMkl7hynU?{MkgUr)&<#oTO1)k{TAZ z95~!UgK_<|b>?WFd0pn#Tkk7ezOe0)V1@}dfp1C_qD*SC(n}Hju$bVfH_uV&uKBRJB;Y)i~Sh;{>tGqQwi9hzv!5EGeqxU>^dO{dzurb9Ftr)iB@X=|U*v=#LhI|t zpDyGB1Kq`d`=6K5bDv#=3-V$z$vsQmSyJv!n{}B@{&b&B2!kv zHiLT?dQOhxX|?`uPbdJ8W&dR@hnM8@xOq(Z9Hx|pSNTYM z8MTNe99UI>?@>i0b(@Xn_hC=S?;kz~7pCuFqF)`59OLF8YAR!cLD8Oiwi@-}p|Fzh z5{axmn92_oAp|+U@wnqeAOt9<4hG>ceK8onkxUE-*v_AXmF^4-dH@_1|x|D^E6TsFbetXft)NgJdcu(l?*Irn6d)n96+xQlae0yd6 zV^{jbNi-U*v=?^di?yEKp%rY_w(S^aG`j!eP-2=gbqLI%&86a1iuy=%0Vme+i|n>8 znYN;`yK+y*iRVkMHxS-HAo{JfJj*dJ^;s9$Z=#!uS}pcCi;C=nb%U)nZH|gbeT)cA zlLo4UJB;JSc)cO|xr&y{1ki~B6!+u1yoZctOB=K5z?J3d^>EsCb8A08(aV{+Zhm9J zKQSqD4EJxZQrkX8{Ms-JX`eiZmaaB+>z%edaG-Sd(sj zutYtcV`EMO3?jjQn3*HI64btOulH=(6pAdLn^YzorhPM&&;wla_^wCZKL??}z|^Z( zjEwqwyD#ZQJfE=xxQ+?-IK`|v#+=lXXET(aB(BEDSiLWz5=mrre_q62r0UsZl*w7x zh^6>?rMy|S_AmMwhg@)q!nhUK*7(ow_`2%*&PnOR$c%}pGo(woCOf(v(I%wwN|YWe zd8QzGy$vysNK9+J%#3|1UKXKE!-wp5c@?>v5$3=C7(!?ji7=le$?kEM`i!kOGq04S zV>KxWyz5K|8bT(wtpE`+^Fc-o?y`V(3&>Ddi|8Wk}i*^NJ3#E+$zX-gr7`EO0?C zbok6Vv{(k>B;VJ(snu}7uh@)CQesbK#F6u>($i&3!2r_Z>jLgSVv85zZ|0&F#W7^d z-3VAA3^S40iOW~VkLe#ru&4VEjaC0UL28f~SK%Vo#zkO{;izwDz9~$eqUZ#QjQWRv z{Rl@y#OMk*ed)Qfd8c(G@j?n-4F)@9ZkGF1j8Z31IN#g_%V=+NK|{_BKeO+eQAFhO z-d~~?Epb&e(N4v4dZInFm87FV(S|btTF3NO6_}(5hI3C3J?+9R)ZUKgDlRO2BoNel z)^_~{oOasXi6ZgEh;s{tp5?sj+urKcvA{=1twKbWfF=p8lEyqhKW(*Kw8=JF`zX(x zxN~;u*vOnmjsA6}?ykC#9w#_U(sB~W#k_xKt=^>W9 zRCHxboq77emQ~|@byDwX&=4tF9I58~mpEeM$P6oav9)*L?d5G#1e>5ylbzfP(n)evm4&36MhTe+0z?oPtEMkWQJum5D2rvX2W0O6izz(XVo zk-{g#Od;?5Y?(5!+Y1A~*VfxzU$CNLZaA_uXBGs@kPeb~qmN$$Wr2Q5cOe6s9Qnb`joQd5FSs9f*!!&wBfK<*-sF?y!n; z&YCYFhtoH_o=a~_%JEB+>mPo1@ipV>(IL$QiBQP`)voe14MuGG`h z0xH?9{`5y;j^3=8n_KpRp9yE`5vfT+q3{fj<@?6z4WnAsbt#d2QV6JJ;=RmL|Ex9O zjsR6gJWu8!iMvE65MZMYamx4tBuD|4i?1=1;lI)AVge^EEnKHeeXow^w3RlqE@N9Q zfj%Dg-OPz>l={{lv)cCJX2{-KZ3vIP)2!$opna9$>+5y#&BW1wJRCtTRAKAH`sm?o zl_+jA36?mzj|+56&ojmp-I!*cm!vL+&E=ZHZjjHc!@Q)7KLir|>B5GQR%a}&D}$q> zmR6)lRLwp92^FN*xFSEc#LDjq^``TwM2#f}(NFEAlM%UUbrVM^H3Oz3C7&Oz|K+_u zkJk3El0yG6Q{!R+$jcFUcuCAivOvPX&n3?C2zb zUE;}P;7F?(Ojzbt>#lHjlD#!|4ipp3>y_$w!}AqOidDZ8kBa2W{6wgQP_*sS3v<|E z{h@tadli-X$FvN|5aI`}dCukYC@giVPd(<^UXyuNs*bi!*6sP(3bpW+s~+<9-xE)b z*dhqo@eGT=;rbDq$d6yY789v`3&kNpLxGvcpGzI2j!$th?eAg2J!|+8G41cvmTosS%}4$|^gW{e&i#3z2So=^H_$<2953%w6?u85-4CDh#fS57 z55O%<2j;7Y=YiVD|EqYYdcSL>D{3=Xq?=CG(ZCJMTlg+jf%V_UkpMUlwcJ8%^=%P`n| z_Gfma_L{ffH!vR-PVj`jSUP=K9Ae(QPw;TL@e0&5ybBnjc9pR3dI=6ig2lgF$pk0U z^&rmu$#%UiVr^qYJ~y}E6SrPfl-2B9Q}XhqOw5+=6I{N&%Er3?(MQW4pzg7hTrbf(WXPIZBlVV}WSZ zsWp1g!qoy#J%7U{~i#9zl10w^b4+gk~g!etc35S8Z$XB1yJzU;RROJ!osp+E~+ODD4mN` zYkXRkE=47$d1toWi<0-8&0z)fw6F~OzCv|tsW6XLZ`#dnUdyCu7k{0qm)e)r+M5%31JwRcTA7lK+Qlx=C|O8$n*98 zCne@aF5XS2G1Lf#ul&U_HDtL2;55cFTcawe$N#~stpGBq(Q*~IkGF3Z*QBTW4|jA$ zm3n!d;hqtrBXc7AhI-6ve1NomRlLvX&PjJ{7>nMI=Jwb8^=ff$-<`Jg-f3!)~hW-pfr9$3g z`K_*1xj@moQY#>U`Dk(7im5PzaS#w7y%OAAxpI^mVauy1^jw_VmZAsKdM>-!z-0mzmNpcs~GJ) zV>?$*yKU!gV43`N4CYBx45-<(er~A6s+d1qa{LC@>Y@IZ{bvscf5eMNBYj-2S*H~Z zE53O%c}22HH7TxU^mX-9WX2ZY$Ijsdf{VIR|_FTZlF1~~KvZ!d>!o=LXgJlTR z!$}N=Z|zMtxhQmb@SGQ0g;G@9%{dr;Vg>I?WY~u#uK&iWul8}cNXeDJ>h?e@DYM^( z%P$+=@w(fM9P^{^EiGbF5B21jDCb^@x~JT87Ax#6f6futL)cW1Jt)AKh1D_%q^aDB zZl8{ZVh4KVX&vOQJC&=1?2{$*8%m=clcw0KEkj7l3G8GC9;RVKQJWdjZJ0Que{RFmX&E(6P-*C-N*PM5VJKOQR~lOeSLk`%ixdO@}k4TC1~J~y@Jy+ilw(MwssRs zE^w~10BC%t7W|1^07l1VSXJezf+E+0P2C*Si+?!v+s6-dZ^`2nBqv|PT!Tfv;n;=p zxV#R*%2mtmR7;ec%a9)flfk<^`+9VJ_7L-NZv44<@i_vkv{mY!r=hKn){L$}8ya5a zYjx7UbCWk^W_rSigC9!d60w@bJAzXUKlm~z?2WFSJwvlZ@`VnDUKExUaLxP;ciZ!7 z{S=Hg>VUK-fBio&zMDFZWfz*iD3GX|AU)GRr1}pIO1@Y}m1Mw1KXn*_43w|5H^OCv zQUl`pS`0yN__ah@6lKd>N070}iF93>`w+|XT!$&*>9-{ear{)Sk$tO?M*c=)2c4~b zJZjXTWrnU;#J8;R_|U|JGE^**?{!SZU+bK%Xk%W^GspE0+~jSz z?pB{^>hl@@Fwv&;gG#6!cP4mBal{G3K9K5^>i+vS#eRQytnh?gC>x}?rm90d^g&4f z)y`@nP`Yb?_tSC&VUKO%Dhv$eyI?*DFY@$lMDiWzTx!YW6t6d-K&f9Dg8(r&R_w&s zI_PnstM#YDX|%k4#ht6dg2mV3_v!|KIED5g84;8jbNmw*V6@No-PwiWH8>wz+rfiU zUBb6XdL5ZUwsFEk{o}W|0ew9KmO<@3&6v=aSOvQ?!IE+}ZYpdHk+FM;*>EjjtmOV; zg!EdM9Q5PYADgV8@2>s#VLtOgU3t-o*V8~D_T;;O1e?g^nSfe16B;hiY|W$}HsM#EndWcpjd#h3jH& zV&ac=&!3=NzxL<)iD8#5!fPF-ljy0Zz$sO)hM|Zh2UhJTEM%+seu;BjmP@Nw;sb6c z#5`l(lqmMroZsaORv?r7G60;4_DD>*8^-v?lBtQpv$;5s5jwlMJt-$wmON82dv%_* z^^>*nJ&YQnVoz%-R||jAZ@RAULOzz=LRN^oGGsg*k(&9jS%L~dcsh4#nEF^%7BQaD zu5i4@i? z_s3rO2Ng6zsP7gVMDtpi(i_&nSPU9ng+MA6m^!wlIH$`P7HzwZVO+*-W*rQVeKTQK zfd^ibn|7f79ag$BRYBgeZ1TSQl4g7q=TysN0q9MzvnmPjTd$9>3E^q$gxKwA4cE9c zGYXxNKnEyi1z9@kB2XP24=-%t0eP4_vs)a)Qoqxp7>fqg01p}K*C`y~D35DS={3_G zoO3N>qMtYzEYemA1`@=Lh%z}>EUQY>O_)Q9X-3 zzNRZ?YbWE)iTEqd&=-U2*W>Ut8gu;W=x;%#hBAI~D0?W3B`%JylC27IQbhvGEvh8MOccD>geT^j5Bw98_oq%-v34Z z%g<}O{|dG>GoH!a$4!=Myx9hUGP#5M@mJ>^Pmf+cuh9@v-&g5AFFr&Bu-6AkT_7Vf zu5eaHFqFgBU1iN~`xii4n*jW2KI1v%rFH#&DQ3lw#D*;Ar9svCK zHO&^5%iuI$eJ^wxV-BX9q?aaiR9F3F&>w?Hxy@^RD7{v0p<$$$mYn4EX_h}aEeZ0= znoudkt^oldCxc&O+5S+1Eu7i=`@7XYnoT!Cjy1Rff7=(IvOHcP#9XW$0%;;K(M9Pd zJIZB_y$fJCa#C|w`M1|T4Q{XPS)2BnsCOWDFi!ex>Tt0C1z`+3HOXY^bb?#evOfU* zW2?}gFLULGGgZ5-yS>t-orfkYy?wHp5k4~;GW2ey;;rBM(_~;d>(?_6czwPM*?-_s z+}O+&6x^)nm4+~ZDY{Zf3pNGd;yXsKz2N!7XNrb`M9Wv;zlXg(-)i;4$nYOVDLc0T z+n5-AM|?X;wfi}{%elvl*eLT`dxSJk%is%Izd&bA0Hse|b3VYL$OmH_2a=u&8opzE zspuvGq^Y56*Ee#LUlRe~e~2j6<;M(CzsGIF*vCpO4L$0^=-}ufPL89F1bHZ=Z>X>@5cA7>uN#J8S5xfCd$fP$;r`5fUx$qn<2CBUNT6cT0wZ%J| z7hqZ7Y2Ui|o&igki<4=-i1q`JlF}R|KU)~ow~lGCmKRae!HF|Wz!DZ*RcimQmU`Jx zEB|WE%`}=){t7`le?4|-3@=xzgYglj=XQJCJg2dbKnfL-^ z+67{pBSO<`sw!h%$O3of<}ZEV7sww1_=$6oTLU-Vft9u93|h~_*?)aouv&v}H4y)E zG{#Q{W%`m)zU!&hYIRh_UA&gnd4rZ7t(idcMBnqSuK~hxvr-`bY1v~r278hCulPR< z{?zccY+V`v(>Iap*6D-j+WIXDoSmyrB0{y{?ub?yJC_8vYQp8wLfr;qoEN6Gk`&W> zl%=-n{$ef}{*fg9G>?pbnjsk$fswXzE!G(A2X$fRi&jy2(U85yeY|9kFuCa4;upG` z_+bl|nEt>v;M`U3 zlxzp0{e-HIP%1ECMYAA~_n6pzW1uhxne02HYPr(({^{L^H7WRscjP43<(dbXO zT37>9yDGCuaLOLf!21jV0|cMm@k8*L-zLI9*|5J2f0UiJ*YO{tC=D#FJ0w|9FKgu`z%h1-4#hlV zH~CANH(XddBq;q0BHuTKr5Z?{?DXBEt0hq_B!! zP;-zrm{5ZsiitLFI+H#P1vPvyK1kM%AN4(}N9LBHL2b3{_Y5XjdjLyE75_v71EsWA z>d5R$vY$`;0oJy8wEM&&bc9abEd_yR(zLVo1XdNmH2mfV9>)TOUX*M5AAI1yZ;+Yu z2Ts?lpn^Xykx#9|F&M_}rw`866Bi3rU2 z87O)cYox5Oc$LND2m>F-yffUXokSUdCocaeXjeut{eK7DDh1*&xyJiKKQ4%mC&Nl==9`R-%$_d6_$Q~ z#`6+a(%C|{6m9FCQ~^BE`(pNfJHPGD@PP7AT(MM(e60~rfKhnYd2fMkcdG_wwT1xqscHUmrt8%_R zFR~GU9dDohc9Q{4-!O2b4i`g~*S3JvZxFwZ4^Ydqi_LtM8jH@g%dT1ahpz6l?%%Pw z-X}9M3W!rjq%nKeGruR#;xYU3@dq59aP&uN_4jCszmNZ;Rk^-#H~z;*`tfj@-&&s8 zl&}B4h^_c<&L{nUT*!|nU$6cD`_?-BUw*jp{&;$m|35aI;t+QG-x>Pj57xR1H?5bp zSz84^9$eRM@FRWsyV&#ZL*jqwP}Y*bb^qpoKmMf2?cdDmX_rR`P*$fLCxLWL%k?2> zZ~cQ6|Kkbgv;JLuPsc1Dn}usDnnzYd?host(CWtxCKH3Ayj(P(a?04aoSDrYORp37 zVZb?V^pR1+q}?QN-o-?+Gw6@6yXXmL!^6U4sOC#wZvCiSt-ViLqvqc$jvdHZe`-SY zv&rj6peJw;&CR3A-}&WqaotA>LNq^WhSIz_OBc?C%aU~ItVmj2yV=@H{P>Cgr*X;k zjaTUj@0$d+K7AX`~H_Qfn3dStX z{2@CvnMXPf+FxzSz;{(Q^&`OTX8OE$;ocW+ z4f%SlrAG0dt(Go7_h8n20$Z#n&Z?ddnpRv!-z1uy|kFULTv(rbQi8*um zMhKd(mV7*OblH1cg=c1gLbOpo@pK7htp+EDjZPKNJvw?GFxBS7O#!?gcg)q6ebz$< zjvC#YZY7udzZ77y4XA>r_vP&MFvcY;Yd>kVempn8dew6ON40v0`w|8+o$gmYe-Q-0nzh>QMJjypBFL&G# zCVrUEcyNoIL+(ngc_`>gfRrYQQU`D&w`K3X^xV=FEy1DYPI#fCvx|%^+wGKs5`J&l zN!n9P89e+VO5s|AsChlKSP_8Hk;QuYpo_3Oet|!ZF7m*`k>V~Qd|>L~#!)Z9#R<0x zBfj28-erTSaF!x`B^7;ja96X-TVZ&^^tsFXYKd|<&~?qFqnxo=&KD{!h_tD8hM&?Y5z{ny^aI^h2W{3!eW0!s7N{WTZY#Nf znY373H@Ay7aM$Fnk7rRUZ&&4gii2p@2QBilkZ*uwIba#|bG6h<4)hHA%J?;mR`eSI zk7ZBi_g1HiX6>sCb)^^$YccP~Bu#zR_V-be9T>t{(aqH7LpmfoBn*&CkrIbe-%O}p zHC_Ki^@*wFdlKlaHM#^o<_S0a)d__e1u7YM=r`ZrP&D?)7`6$H#LnAteqa9$w=$;e zIGs?H(+SZpk)@5rnS#ehm-E$&X`4pu_qLAj=0i48XDL3CdqHu{zFg~Om6vw#=dc(& zb9vt;I`hN==;v{kX~vhKQk=ZzRGJ#@L%2yVJ>69_qMlvnEx7cQurB)DCj|rqX!Xa7 znvH|cM{;1B?jUgsBEO2m&~-}bzvPgmp-D=IW*EYUG!@p!WZbuf&oEt@ZuoNeDMTI@ zV=%5p9^8}R+kpUbxU+&(=I>ua<4uw*l1cCVq_bGiDCO)SI61L*w+Ck0mG07gSTc!V zDla_FX61LeQS?fU7TUJ7Vzp_Gq9MlO6vGc~;Z?$*Tg@!TM@wa=}$x6Szy1&{1k-L+WAj_o$8T(WRtnu%Cl zo*LAjLN>(rsx=z9fFQ0dDUeb)Qe9C^QwaATIP2zd8GhS7nhIE zc4*#JEe@ESJ5CGg4>|PHEIWX)z-TsZt>SUEkMJL$D@sb_!iDACXFQNS@_TrXWih{x z=U4tqmiRuJO(%cQbvwL%78*mpMuFy0A;3g=s*?X=$@{DH45W;x5e2r8)rgnUP&MIV zHM!wJ1LLLYRX*Nh#j_)AfqVPU4P5z+c|Oo|#X}<|aIB$zI{r~1%i0qTtx`HP(RJKp zzqxh(l4n<6pYyC^pV$|hQO;m0MSXkqM_UWOIcWE)T5pL5{e@dh9*@gCLmjJ5v#YQV z{uWi}7pOiMN;@~%O#;Z$&&$>K-h6G#Vl&S3YUmnQEmn``*4Z!Mw@+EoefV|w<@aNc zZC8Y?V7(tk{S5e0UI3McZYRzbub{$cPWJ>OB2mS5|B@BKQH4vC*gGE!^nBE!)aGpD zR!8RMLiSWuRm&p#N{BYx`-;d(dBP*8`bLyst37=^wdJ6Y>QqY>@a2?5V^_W4;)1W_g{%I8gFx*}3;Ks0(4f3$Cidr_KZ3g1C@MSH8L$FRYWMpE zT>hgvsQ+}gd+ng8^!$oOHOK?g*9r4gUo$~rT=o*-te7n=BOG$xNqK4DclTvmHqHSV z$O4qTio%oOwIryM^6aqVqX#vqDQyoa3bQE`#QV+t?%Dz{N9j%8HvS2nQz1 z+OEto8v}|HH2d*oU9oUt+5;=EpT2QX4+Vq1Zkn2xT2U!Xy(w)f+Bw0Dm}nBDpBwB! zbL>^ArXLYTHJ8`%W(wx+Lqr5qZxz1rC*po8CZ|DWiZ^?k^FQ1A^{4-ySM9p(bEp@n zP`-HzVnyF@bUc`}{~!BbUwqWu`~*OZ(_U(MIAG5ct(43!V& zQwh=5eUSBp;5yBK@ed1D_U30Qc2aH39Mt$b_e{<=DAW*bn$RX8=IcMIp08IpA@Y6N zK3Do>dGKrk2J>{5SZXn@8DegdIIb~eX0%v#!-)hRh8IWV!eP3iu@v2hv?saB zDa>MJO^;#OQl(hR;-NXe3P;88A@V5(_pSh4?Bx&m*Jq|XF;D6jJK`d}j%NqSQ+f*; z2_s1$aW`bwD;CZ?_hrW)%`owAd?fr)wHGh%rnNo=Ag*La%V5E|Y50sp2~s*l;-kGx z*$M@>Lb16X8uKSJ*kff>7EdjSVrZ6c{qT4?PP9K6;vvO~Vw(B<77=Aw1VRPmKM3IU zZ>JOGEIAWsC=qqQ1ISOnm@`QE4SicLj6mVaV|eTi7y=^Bn!B4-EQMApN$HtXCra7_ zZ+AHd`r(<|hXuQ%JxmfTQmLasmK7BV)1h^+)!te>*k|7XF!% z0MN)+qjL5-rFY(Qxp+$fwGH`SZ?+v@rOU?^!P|pI|UuQ0ReGUTD;y zUdlo5YdiGKKB2)H#+T+=XX+9d%XdLSXB7GVpenRX`G&AG9F-r{eyZ?P3$1 zsvz|%rW-1ZKR+F@2%Oe`ZL1fr;u3U+pISXGE0|0Jk?Py9ZHs|x@DboGQX=cO^%Y~! zYYyzR^(|6Y4xT|Lu0vqgNLfoIKst`^j8(y2l3{2RUrdHgc@^ zc+`xRCB7ea7KfB#D7^w*Tembarq|iZccdiTZL3}Tq zpt%~@N*@b%R`yh8))H3+U~Lx`yWEqpV6XXf6((#3+~$~ouhGp;=|l3mMtj_&9EhoI zw2R!g4WQ!&Zm_Z*nXzo%elY-0)$|{03e9HcK>(*h){*P#>LNoDH{Le-&!l$So-dS^ zsrY;Bh+>OExCdUa48PgLQ#8H@^c%+pG-qv;v)gUod{O;U4^a;aQl^FVJhJKpGp3Vw z8k{8*cWn+txK9q)4g~HcOK4Fq5Ho_CRxVE6`Z5N;$yEI!Oln0?=qGpjC@^;OUyQWK z;|)<0(FvGiPu-v=NpB`7y?$l$iwPSm=^&+;iBB8&Y&LI;rSFQLUp&ppJuKN2Iu`N0Ty5l}o zi>m*#nvYR`02duw_|G7rt6$X-&M_3^kVUwSt)-zf%=xTx=f-K z1_f%CSe+#|>ZRQJfJ$2t&{sv1aQ9?FBjY+J4G$S_!$jzQ20r8SAFqE#;Fe->P6Z+4gSD2?T7(tAm61ii+EdVrU`@F#c*jh@oG7U$c0`2*-HCvv_G{O7G zFMo)&IK84aTJ1tVThtG%cAL4ipj&ae+xbVcAGG3WueHt07VGN76s1A`*s~t2=1?is{P)-97!$9H) z_r5`{;IbE-mA&Z+s#N%?iiW2-UV{z3KH*P?<$NTPmg4$G%J159=OU`9ZS=+}D{ruelR)0k)miFAX)%{)%V6V|-(w-mESvJ579JGYCHyYR?dY}G zwC?=nxG&rn7xq71`PJ*f*MaH)KesUOcB9;_l`bP*f{3LO0Pc zpSrt_4Y9>V?{|teRe`U7I1ms4dp)2U1AqN42Nz3u-4mo-M|{(Q@)!p|ANJ}>2xq4M zOiVGtV-hEMLdr~22z!2K#<8_|weU~o-q(yVFe|!R8;EK*c{!OwUKbX+7IQF*+u4=H zCutXdnmw1|6YMcf(`DUI-xqrBh%v6+C$0hGafxnw$?uX!GgRejqaP(HB>a-3q+_EG zLzwnvl{GhYRNm)>^8dh~PLXTnfa{yzWy7qc+2X*g;s)PTM5!#e`4}}364Qn}kAgpV z^FiDn>z4t(7<6OXIs@Eo(l{H|l?QnmxV=p2M)$VLQ*-nX>&!0lU>I6y*TyFl;bCmt zZr1zP_44n>h_;e*oqlH!EUwCVOzwi*uA(;t0M1=wmg35(lztMjpaTLt8=9| z<9KAY4j)26I62du@WCGC5xNI^ zpfIV=4zh7{U#i{SsU+Rm;c_20IqXdAV3@=h5=SgjKx4f~%}VA5hP=c@FF2z6|7HpQ z769FScL~!V*@4?@EK+JC)hugdTP>@G5*`(QvTdiiiSs}W3j{z^b2saHu!FO5%}Vr~ z*~?CKeg0#XQ!e|3pz1Xle|;7tiKCd9DCE*&S?lshtB=f215k+4jiGQy3swK9PYaC8 zI2;c8S_nvz23cbc+U_+w4vjmgSvI$gEY9z|Fg)+y(-7E^9s+3hqqQ30AVaXJjj&kX zn9~7SM=&G+!9z?Xvi9(vac94$!;n*i6TIN2NRx|=_lp!2ZE)2OEqPD<1T*lGohi)KN`MVWQ6J(5d=*t-8jK?uD1#^U`P&$oA%f zmj?yEdihC@Zv2U1F?jB>+4z-bA9&p7j(OOWhOMOL$TR)ukiwE}8~Z30_%Qsp)mC>r zcYm9}%c~Zb;#WHdNnHKolCr-13XwObn1bDLgM((ZJZ?eTGomHQglnm7Z_}9#_bA&? z@_YEY*$YCUCBb*KEPcT2T^H{BITtm5^zdt%aB|(%_Dyw#yl8~uLlE1r;G~HhQI8QoDCI<}#sX6T%UaYEq+MQ=Fi;Mv67{!}f({Ep6y|Sh+ zFnEor5AOns=62Haqe3Y_4+pMjkj;eabMdVyHpcwgkr9>sQDxWXz1!?K?XPa!$PS`P zNgnSf^=%&`9mmO=D|>&WKWeAletA!HT#azaaH6Eyw+I%hO zZO2)r;=b3h*yVlR)uf9{Ii958K3B)yM1mtUJc(5G-r z5Bsm3s;f}dxz^k;!rj$8L)WOJ8W%^5;`+tzkfGLJc!^*EIKFW5R zi=uz9Iobd8M+CTomg0+^LVz2B_i@{D7MQ73g@tT?#rl4AvJb$AI4L$N5fdHuA1}24 z1p3E7LW8Wz3t-i%&fncZD?AF=49}QQV`a8;?@NDYhfdtnHM-!vk3Z7osntd?(?@2o zrK)Q5`-|6n7W;Gn_QW5vtf6$L$DY{E*r)b^Vy`WrFWJhhq}Ns&w~jJ4>yj=(&mz8M z`mC`X*R62`V!oR)e1Em`+8pe7SIB~ZX;rgH#C{dBc>7d=5bJv7CRKGXit1I@{qEaU zU~XeKFgHX5M42p|6!;6tZa4N?I(79IsYOKx3& zTWWMVDe==ZRO!3vmcSYd5%U6*&HG%^#eeXe{oV2G9RVn zSRwyv+^@Ho6nxxsi&opi#Ee^qy^>08Ftx=sseXpZtrAY&U~~oaQf~U2!(78Dlsh4f31nZk&WdsE5JpoOJbhHj52})XgUW-xVH5aO#k6WHSNxQXWEt8S0 z1LW7oz!e85!48JSBmh;1%m_zpdZSWfZuwdO__oG`Z*%*LDV0EJqrILyRxcnH6#y47 z@i29D(d! z{=#WR6YHEZCVYs@c)o%c9;1u9hG-OUc=}~t zOm9j?tB8rbqwcFTDcM(jn;I*~E^4peTP05dFO+OIpaRB!m<^WW*;>0g*n{l~AIV$k zSZ=gd#-Cv6LWh+CY*vG7my|}n0;jnUx6E5sR9Q;yKp?4NHh?$msECP@2{_X~3p6y> zP#*#JR{tEvqTH;gHV_*+mu(MBoA&p|nR*3eCtme?S~71wM;p)&gNj0;3ZSQCj3wUJ5#WfyiyV9ZAa)wC2f=#V}IFT;%Tue!r z1dUzoQp_VczAjrlkB|9Q(ZNpB@wvgrz8|>uJbdW~V=jY*~$`&dbQk`j%v$6B@`W4N>ojW`SI zFH!A>YKuQ_)7=D!%|;iLRpb!-6UCZT*WqK$$JXz|{O%5y4oGhMR6+xC20T}(+Ty9~ z?{0Bvw?Mp5rlsa1ifs_n8y4!6CkiHy30Dl$Z!t8fxRMfaToZqt-}A4Q?~nfg-1j^i z;tkqOS*2^$cJhE<%Vk~sixp79uyawfTpbzua=-35SwW-t^{+3VP`($vAfVu>AZnhqYHrikzi8G!4JKHR;9IB4 zx+t){;;(XpC!%7SC=SUD%(-i`QZ(*}rRA`yadgB@-Ng1UWK{eI5s?t`w90@cH1!s&fM*n}C5Xde8}3t-C0de6$6If7;aS4>??gp4%f5k>Fp_ zgMB&D`0my0^O6ks2@7JPs2Sdo0)JRNSaEHz!N8BYYUIS^jlIR@(=~NBL4!Sr8}_Wx z`I|gLeh`HKF4z=O!9RR|vFJb?kADXT+;!wC*qGqO zO8S|}mY;8o;%&gn7Lx+CS`3#sU1BpueT9MBap=z+BrOk6B0t{sJ1cf8*p8f*$XbE^ zrSxhN`lpBFF~q6=WCZC`10J-a64d!RKrsNVAqhUbI}O|X?kAS)!pR#2Uv>Dsmx{*b zv`8cP^}9L_2_#VB>lJ^!xVNU_soVViFT%HZ$Q}e(F{W64$d!NqyXoGdW6G;bly1Q6 zxzc)A8l8**xMPZ#)+VmRjy}>5fbPc#Mbi)steEx%`q64k)vSsHc!7iHK2m8X&0Ert zRM;WSeXDnERm_zE1qh2XZmVLp1Dw{rlpSPpM4K3IVM8hhJLL}2_oSYxA?|$V`Iyld zn|5mgqdkzn+7-w#d#jMl3<52DCvPdQoVUw#P;1%2PMsbEO`OF!PYGOYD$!!{l$3=P*P3(DcWHA=`~scn@z(}h~i#-2DN zg8jHzuavBo&ODs{NZ?J(@VCtpvFzf)vdLYnNL~88Spt985R8f~A4{xa(~B33VSwbj z+R`3ULSYF(9hOs=9qCO5`Hx>Qf#S@?56Ky01G$)Uy1&pj$dmRy@uAK8-m9Om1FX=kI zQoaO){sy#Z3Q4eyt;F-}m>wAF8^|D*xKY_VE@cnD@BO}grnwxOmcL5{rjhoFtc<$z zvk+F2Dj=jjG^L}EQ!klwoc0ebSMvEF4%veslJYQJtn3AJa>pn_6Ax^TA4zb*1Lt4d z=PD3VH+OcDd}0%hLWD%nzj1( z^k26QAOC}{WBLa0rmRKbt8?@nhraHyyyp=ya(fhRSqT8TZcw0k&QPr}D@J|`jaH*= zTR>e?qN_%jex*uN8v*!R;*K}W@sBoK|9JXVmg;qj^#v%o$g{=a+4F$581#G*)_>PW zWu0k~!V~4GTc1!Q6}LJCkz!W&wHSq6nQe#ZjmsTDGBj04Nn__AFKsMX4jbcDU0p4U z#eZNMXE#E26Pl*nm~QbR>vw2Lcalbe1p)J{X$B%sJ+#7@|7voDv-QS8V?Iu8ytJgG z1hSVP*w*3atCrAMf5&73F%_rU>q#Y=e=1`vE)WY+Su_^4>~rk0`*3V+M;iNYj*z17 zc=c&S(!ygKkBlLst`;!^?H8uSB2L)0ZI`($2`%ZlY7{y0`XZTfm@IT6!g>%EvEeD>EY_Tew~rA0LOHp41ZMXBG4QjCVr(x+Rsp z(fbpGA5PitxG^JW=}!oDSZzFtoxk~H;Mf?+Heu}H(M;Vcx$vktrRRdHPm2|1qO*js z>hjW}BBz|RYGUi)!?qo>r(1htyJ!RiPn}5cX%ohW#x=fOt}T__{1xlq@qPPud#1hj zTRqSyHM51dh9H19#k7<-*F}E}qxNjPOXv|AS%G8NQFG-XV&rP~@D)(c>x*Xg_JBls zS8=jfX?u;;1RP_*q4P@1whPlmDXXi3K1e93m|yy76InKkFSgai7OIC1qNC?jd>h!q zg1h$?YeNYBO``#pSi&x1{;Xp@S~^)y0J zRMCFW5smg&w1wx4gSXi40Qf1C2P6gBS&S(!nC7hlgk;2u0tzVn!C&S*!WJoW^_mo_ii8anHaw>=X z28&HewP}hhbeNf3*yTN67bmz@qM)I~hO&2Ynb=c-dc+LhM0J4{Bf6!rRe(_wsI4EJ zW;U*5B#4aoX40sg7&Gbx1(WL*{Ijd#%cFY*suSZyl*qSw^D4J$PZG;lcQ&z%a_YGwt3f+A-zOZnhCnDYUbgfG zzP$T^-?}PuAn+VzJ%6k|+T-g7jCiZ9>0zv!hYTx4@!wUfZ%}<1QI`c%&TKuE-D{J(t9&%TyFq(cqHGjKz8;AU|tPB~dlxZ+xj`4Ji*e za4_A8G)I9n_pq|q2YuW%hC#xP08$k$n+oJ3PjhX%cwNb1<~mvc3# zfsdmBO7YAqOUwyo|;bSV0%SD1Hbw;9?cb1`7cOepLNaOGZg=YFZIPWCxU0j3kR-)Kl2hd>zUJ#&a+JHG(e)-@EN(XEsm6xUL513;p_b&pbZlHm znKIsW`vx`F*N88@tK$y|0<*F$t1t2EZhroAI7^pkOcgv+FjWd=@TW-c4>9I#-Km@J z^UfAJg3QVFLiRxp6(?dh?`mFZqk22|49hP^Cuq`S$&dJkW8qspJLMZ<_ONd8=_j@2 zxhWn$J18Ob1Zs24T0-O$?t#e(au@L(wvGDMXZQ^x(rOs7{KzdRkOvcu!tSyXEoXe% zE@Jff$sW6O&&~Uz&y8WwzqZ2MHGu!0Kw6+mmd+Mk0svJzIWZuh`99+>zL&cF*%BK- z-z$}7FQ7&MYt(>NT~(b23;+`}We-g}a0U``yCc!jz}JB$HUm73&C072r0_MdcfFpS zSSeFXVUm;;{I9_EW^o+#n`700z)+kC=@uIb*(FxGb9^w98&7l;ezX4akpWvZjH=(;Z$trh2YG*r_$f zHw5SysL5*(UXJNm(xjrsu2#Cm~iNx!uA3l7*GTSSrnk zzje>5i4Lx8H^(Wb_p>s~L>Z!dV+weUBUor6l~!E}oCykv3sG-<+L6#QkNzMu3_v%= zy>8XT`Q-i8MnK6~ISAlAykYLg`9`fzSXnG7LR;+B5lsP;90iX5cd^h^QHf|w(GER?;r9=u^LxRZg9xQ#CRYzf@9p7_m&D3qKA&em>NCP z+vJNpAF6uI{AL|W4rTJflxDUK`sAFl67kk>p6ctIG2gws(3gk$p&u1hc9Y#DP9xW$ zW4=)2a?^R6cVPC6Ba)oh3%kands(;3K$>)8{z1_^0E`zsR}IqJKC5#5xMGa9<4(RA zUvBJ*e(LhZO{I4d7d=+g)Vh6fm3QW`q7u!Gy*|~zz1C{OhUUj5L07+bY5^qQ8O0mSHy~#aihZkSKgSY- zoEB&BW2<=J3?6xR3@DwO0p0BYs5%9F^>NW$U7ZFyEw?8Xe~=HcB8V8lT$Lq@BtE|k zq7&4^JFF4|Pl_~&YjDRlbj=ZA$_g}Qr$EE|bwN%((WJz|x!ubxDgS`17pty^?kB81lN*nYU^1G z3O1{|W)oLdE(%`uHJl{())we82^V;Hg zONMHK$@(-^Hvu^J$ttw0FmWR0ZOyh^YrXlS~I$SSG(wUN9?Nw`F zZ779cxn*l_KgC_~%aXdVQbUt1W|kjO1J}T@`oYFgX5@mY;h(qa+HSCFhavk3fAM{d zk~9W1CO`>Rp{lCNaFETtKlHUQq)+_OgqGI2XOCy(A@7zSg zfU|85Z5cjg0*tx}j7RAPDvvETb$b+gM$sHw(=&8$UG10kH{Q>eL&V^$A0j78BIp1^TrH!&KbpKAIVKa0w42gcq`bl2fd3 zQ7P2k@L>Ufw$2#yqvCfjq~+OTr%rv+;SW~FyQ|zwc{8kjpeu^Yj&1J4aQaM)bP z89hV8>O1Z3&%Mm-)bU%Jyk(2aF2`Nigx1`1{#5Ui%Uf@19k8*yT?+R<(~smn2)TZkx2z~wy$bYZ&$(A{ z76lKOCd?K3D)K<4A)TG0yXhnHASUJr@<@10UI{?3B4O)0VJ|1L^&NDR?k5=OG5bdMd0ZD|0Uggs*!TcFxXlz{4?voRqaI3c&$SSfNMOXVJJze~rZ5d$qrpDV83y%LQW=bz zFSyDs{kaD@q)`_-P&KM2dW52jgqQss9j}oM6@<2B#gzu3Az`_Rz=<;S*fD66^0YKx zZKxzLX$lX26E3#;gxR1_{pb4L)X45xtR1xG?M7_al%((n zUK5i}!ppVYXmuk^ihJDr*Vk3U4AS>s)511v2vYRo#@k0_FAX+ecrPxgz#|W6xmFb} zC7hp+Q~^o2FMIONZA!g1n0+CP6i3Ng*;(c?P&+xL-0JbOyIAC@V3Ke8nyy|x+kKy= z){;!w@e?=a_f7uHhQgEqO%_Wre9&R$OjPe(gI%+=JD`Jzn9U>zFCMPtC6%~-KsE*kb1J^olJ7_(wx!^n>f+t)TIo*1{6|?OI zr)zwZtIIPCClefvMShfRV{r#cWuZq&9)>#Zz`ls+A^DkbjiZE!KKyTn^!IqTtT}^j zk<(E03LOZj-ZAnHnid68jxDvy7v760;IY3Fh;enS#=4~h(UpP3Z{zSJx497O&u9rJ z%F5J61oO+Z!uE5|-FJsb{#pF%0YYi&n|i+Vl2>vw0(YGn61R6~W{TCTd&cm~3@j{m0O?U`2;J)L zs$R?tZJQ=BcCVKHoI(LiL1oW#)7%|?iWn2;8($ceg|b!3v~2o(;k7`cDIsNbNjbJX zN8zAHop{S&alWkYy+(Q_c!JLGqBuWGaIAw_{|UY!bY;p#q0<`7{Vhd@K{+M*lBM z^Bi9vA1>UoEyv%&eP;6a^*uRmG5N6&YZ%FA+Qk(=T}zGh+yXP_xeZPomG-eZjR@WCyZW`RQ*kFKIYBzodO*6^PC9F+SqMg zg!3GT4dML6P+hpJ5UC7}R3bsQXTHfQSBO@;&;|WC^!aldfMw3Q;mdUAYEmgmvtKJ) zM_rcTtv1UFH_rvK*`(DdQy?HJ6*-;@JySZY+qsZ#eLkYLLC&gTn}GZEfqibli%9O2 zr~3ss^&r|}{=h9cuXe=EY7}OEOES-xf7b#ynIE6s^_8ht_vPm1y9dxX(8NvmBErvx zUMgE!{f4{4OS6PGdAA|y1vg^OGYPGPR_LNd@N~y_`YHHjk}@FIO3pF6#-UUf;EZxt zniDy#JN(y5%QGo5S9k_3(%63{h3v>^-ZmZLn^vj>w38k_`t}7+_O$3xHD9Pt3xKzj zwLCBx)@3J;D^Eyiq>E0Oy={(<;;eqx- ztgv9Vyjp$ju8wQ}lcnmNU+>Jh4LO{&jsfu}T%CYmJ5_csBZPUYrzPC%b0HZ?>n0#i zl62;bEn{GmhJKx?Zs@BCP}$h=(+86$W?PKN@fZtg3g++uCSZ+NP^#a+@=ju3A=m|?xiw}WxE!tb3)~y zH(I1x96lwUJ#1ngO4GcxrD7QGaIyYCU@W`);Q@X6u&y^^oX;?-0@t7`U zi;&%w(JOabcZEKwai3Ypmf5SG*|VL1;Sd!fDd{#DYZyUNb#q{VtsS^!d-X zv#8c`!WP#+ZOc9a+0Jow`Lt8}D!9s^=_f17tj$Lu7@@x+e@qG45h0(O6BOaGi3 zBwOlKz4i=fm}axwKB;ECRGKWVF@maQ$^|BX?mwWDM>e~5lU3=(1 z<$mv`ri)L$Te^L}&0EH-L7xlmtm60ex%^vd(NVuZC)a8>8coTTLRggioz{TDn1*qD z=Y`*3l21A@MtKiGF7}EJ?QdA^!;05$-kp|>pB5z@`S?h6zCe>EugNH}>qs$d%g!8c zJ1fI&y+otG+@s}Ezc=wxAiCY5gbVDh(ylbi)rHS#G}5Z*Y|g3XC44Yq$aUwbeJ!Vz z{1UWM3y_I7-FPF5ojwuz} z7WEsPL%hcSd^p2VN$|e(@&4+HGK3bcG4kEYw0WNLbGR47=UG@3cRaK(*qnctsmGs) zTzciH$d>ao@25!XDErZ(CilVqxufgWb!xExW?6Ly{>AwY42Z|;`upv#2DOxh=cnqc zRYhedr&!a+W~&P|i$6WA`)aRkdrm4ys}+zT2O`=!nuQP~;mf?6VIygRrP--ESjTQm z%AWOijJe^+kE8mQFeir!xMPl|TLYAOf_i`#kC%gCe|%M)5?ZIIAh~0fuy=|(gMOAT zi#;t+=%I{fvoyAOB?S8&&t%prTI{$Uz0xz7L>_c4QV#V}3>HFuC-7^JqkkQZEWA*0 zpO#a#>W1KXJRf#RrJOQj)hJ_9rneZ)X><>UjKod5M7w)N>pmQBML7_suTHxJxtFTV zrsx)~ehqEr8Uu4_or}0O;X}&6!hI>pE3nk(*yC+fRN)zpyces4<51`kS|YW@IWN-F zACcg**!`n*NYkS=f%q0VxrpiSb4>g#&2|=tK49C(^nvAe2Eo&-giO|xMOfyRU36|y zwdH`SLVfAALzQkkDPlqLz-fsD9$a{OYN_?%sLRslPc#{N2F&$LHTBBDh^K?7SdV@E z10Ku%b)i;JLwEU~m0r=WA@~-3K}gfZU{;#t8`mHw*P`m@!TVpH{{tFEY+lNtdTOT# zbXPTG_b|`_{jC1e3G){*(|3dCcdA@9Z|RzMbp830Q~;?y@Lj}1ZC8hOlksxj`9F2J z9-1wu>Uz=eHyw3ZW#Q0K660EU^laA^@(>-D>qIM#RknT87fgR&Nmw3V^wO?uF)JB7 z8(n;ba$nGKDZ7D4MqfLEsdZa6No4*%jJzx5+Zx25kb5$ccsiC+? z&ixCoQ`%UNDVKoNXAPt389Wsuk<$p_Na1+xX-@Jg zKWID}Ao=$65GH~!-Ji)@6LAOqHo!h=YC7+=63h0-QJ{k3ILDPHjb@3SJYO3ZRW<91 zs{7sjtY!FTT61tsnJWKc?u8`1%958YP4CsN8)iu(h+?6psEtRsvR*_)*WCWa6!p@z z_`rl(I5nMFqT7SL(7;u*!XG+%1E0bvdzF8+SHD<}$>9WiO3S+YxIws;ej_?-yrVlx zOQfShO|j*>X3V$5-Qj(^S^G7W%9|ugI+tXI3FALyZp%v}(Wnfpcu-JLRR>$Prx4VL zymtNi-s26v33F$UG&4zCFmHE!2JXomyiM7?rNq%lPbu}6Ay(L2<|yG6(pMwo!dOGf zCEiX(mUJ6=S68ghBW<$c31-O)?~jx&mImhP7tTAm)}n~wEr?ZM|99}FjCV+94rto7 zT>0vQMdzJ(wDrb@pLe5W_p@?P!b&fh9pBb77>sxsWdqv~Y-LE$MXg^Xzb^@Pj~T+d zdVRQsZr!rXOpHFy$7>WAFmA=m>Cjr1>-$ls%`5wbq+UDey$JngM*KE~jEPALlZ|+d zwa?LS1Hm%?b0Cg(!OJ0*^xGwDN#Li~9v`-uy^`FoABYWAu{?&8$)tauN{x#t4ImIM zb0U#5@A76>Yev$Y&_tQta<@1y7ZgpIO4!cee2cUsj}54VGUd)^VCg>W=y`iMDOW_F zIHfQ-dU9!$T~g-%)eE}mTItn|=FyZ-LCUt)bD($YPzt2ps-1IE?rcs)N4a~O6>5?z zd`?Q#?{AzA74p(D4=IHvj$D?;;c%_jf2aEycoAmNgtH43b#UguJ+@Qmnx%bq=bq{^ z_ZlWqQBP~QVm?34^lJdUwR|nZutn8MjGjM@@IuY`JpO zf6pc)J4kS4SlnVAYO)GspH3*hJGD^ouwA`f4)QeqElw`X>>Cr_&+c zb@`Pynxsy-xqVbLHIi}nai$?LsCM1jx&vgfp3}WD^Ni!e13?RJ!g-Y&ofYLnzP|38;%aERE7i#{`i*Fr`%0|+kM>-AG$7SZ_<&}6+ z0S?9P!ZzVyJJ}vhrIJ-@`%y^u@M%3R{Ks zttm^f?;(EUX>{{Gu_JLu$Mj9+0>a}M>(uCKuMGs_zyVJCtklly2w8|s5!XSn#_t{{ z05J!=-z|U|Y_`0YM^n7K-y2caW4)Eo<}PV?js_Wqr=CY{=XLxwZ@}uY?9M5+5pK1Z zKoNZnM6^>;I((R45(##}sRb!sR8L zicXscju0Cah15rwM>Hlyh&Nj>}2yKqZN3WgY4<`aiq_`nn%> zobAX84Ub=GLILdp+d`bOX|c-Y`{F2x!xbr?MW{}@P#HDH$^mlGrk9f!x^om-p8Q3D zMbg2jr`ks!jiSWlYscG9MBtLQwO}YgI{WNSS8P`uZSy@~Md05X|2f>i2j;BV^Lum+ z7|~?tZ(*&i(`FJoCfQZdIti4D2k4EaP43>?Ijm=pLC6H6A=psU%O0ToHkWg_d3st@ z$iSSk-o5Ix`i_bo$w`4g5b5;kpFiB*m|azwpSP_D^cIv;h2=NX?`;$uf#Bf+pu|(< zJBg~V|C-LE-~u2aU_(5@l8%xryuhV?2Dy<#x5crsv)$GbgYs_&Nq@!=rsaNhJS@ac zQc)Da2f$kLv$3RYnp7He$LTF8YcZd&t?Nj3iB~U5c|?F6GSR;ic5)KWY`XMA zeXC_>E8IyrhuErg3KM_7gLG2zC=V~I#JBCZS)je&zzcH`tShw__-x;;?00&}F2Q^K zpkN%#a|_`H>SOuQezI~79_3H_*J6Tk$_xMrEjtsgtY+5kb=u%GMjr6p^r8l9{6=kIp@06 zeWFfVh=H>u&2}iD%xNoqo5lI1`((%XMLX@&?ZEFkKz=w{U?1vH_M_$8qWK0__-1w( zwS@Yt>C@YpLM9`6ed&fHbnzIy#Z=DWY)CGaT3Ld8GUII@G#EDmY<-s9;jTz^lcyTb z*|l7NU|*u0-%q0HoSDDh=&L87yIWyeft}yXNFfG0l@G+~-|f9K&71yWydlPoBHEE^OZ?0uBf9gEz^QK! z-19sKgSmoOVIk)uzfM2C%ini?=IdRBQ#r3?=~fej64BA%+2IF4@q;?AizS1hzpMzm zf225bH_)1CR8r?hvqnf5DB?XYDRHq32CEsgboTgO-T(JCXdG8K!ygmM5Q>YxgC7aw zO@A)_ZdU&5)h}{tj(W%QHz?^n-m=%$N;)x?1Iq;X1;OBTh<((|^wvS&Bp5$dJ(n5E z?`@xfUlq>{5apQhdoP@k9&h6I_az(RS|ABRKmLN9UH6Lz?ANjbk@%F}X$_)2@eeSR z0!cFw|M&?4i+pVnU8r~Vm72KKf=8!{$oG$%+= zw=Zzu1$)p%SD-TD@+x^|?iy{&c)P->3sN7^8$*K}W;n`Dfis_WWzmKGwNX(ndV+g~ zcBMXpFYvzl&0garsU%mjJ@DA-obblR!INNPYu{%V$sr66pCl}ddu7bClHylfeH3`< zghaRUv>_qD9W;gnM>_49WzMO1`=aH3#9I1|mOtOity{&h)1dDH%%AvW0xJJ#^o8nGM$w_! z)9$XFE^n9Z5Uoiz$Fx4gpU4liXb^6^G#ONp`b#Ie`{*%b{S(H4iwhv?#^~vvm`1y_VgF$#h*jH>+O=_PI@qy}nko zzUC+p(?K+2zBtz4N2AO#-f?Us;T|BZ-?I1KF7z$ADbpV+;ajdfGt>ZLq30B!y zrkeO*V#eORJ7Im0Ma{U;c$Zn5EkEcG{IBGi4pPX>CnHsEUNUL!!>-70Jemq28jIULB9bj>GBc1St?oS))C^N|` zS5yUl_HsSR^60TOL`{E?T{Uv+LK=BSe>Yy&nN?VvQM~WsqC2oOS}U4PEDcw6<_^KF zUtcw&ZT%_RM-+1%Kt&FB>cSu*DmA&M=?2I&pDG)>y>7q!nO(O(skiByyztL~F_gXJ zLqERf7pfT;{g)iG610R5v55`b7S%X)bEu4 z*(;sdLm&hL$5Xb->^{?Iy8Ja2j7Svo+@z<+^1N9b5tpnb9B<-ty*9!;x4F6x)O@V2 zYk1#&G*NI<_~|(##!5?@TO*#pFGkMSo-}et=SBLv6uA*gC z==sUgH*z~kq=yfAE+tyLX#n&CG(f*Dj?}4WfW?10j)COCx_*qEbWLP~hxwDhI^zQU z9=gT6scc^C>HYOIgUhkn5rF8NukAvA#Y;6Yy=h9O+X*sEvMvF7Ou$u+e3sqSGFc8X za%C+I9^!50Sxt5Hkew9%Gxy)CZVED}qVCxZam-U*Ag!m}9e!Mengu;-LI||&n>RcG zpdQ&=*1L?GVGTl)^t->TgTT_AJDJxpOb|<%_v#01VltS)xWT7CvtYodht!q;i%X6# zV6R9r@Ux*1X1%();TA(A61g4t;lES;9`eVEDX;brcd@k1{1b47&9oTZz66L-?%x`z#ZzokemTy?uj&5LGYlp)6d0*{0apHm@8_RBFD*S%3=$$( zUKKNc!PoRkz)T3R5qEMNvd*U!OP&)R4Uqycs}jtng*#(W6;sEj4@B!?L;b#pfb9ub zXSA5tGT=Mr>UC{?c&)Y2K`9eN`slsgL0B~8N!^L%o7^i1f(O9pztAv1i|zmTSp#Fu`b522+26nML1vSc6@klCI2&Lg+hD$9dz6ZEqN zcHFjgZ%Dss(_yKQv#C7^MoyK>?E$-LO<%)Kw(~l!*%gP&v3VP&tM6~TB^-P^Ki7DP zJI~TsHM(+zW4`HEPN}*1NPAP$ES*M_W2kM{tA07KBEf>QZQGfex|k8engy>#P;@=n z`~B^L;oEv^@##AYt8CRFj0S^8P2#E4y_5Fazu@uTYvM=c@}T%t9rzYaXTNBY}Jp^o|W zt8IwPS-i$BCy%5H-zpH{i@6<}zOA>Y-YAsMTH~4KlUseRt5Z~df*%=*wB$&sl zjPFbSoOzu(vRYGW8DDpiT3R8FZ;XfEDJ~?fE_OGa<6gOEH3n;LddcJ-r_fp%It=?q z(0rd|5JU{)>Y9e}!EY0a^-3#wrza=MCoL-e;Hg&+sDK#o6630YsyMGnwVtsqh0bqK zyBS*G@*^c`|DNPd711#I(eLY3-j4yvQ%A>n>1*U9Cj;zs<5=fAm#;ea=A-|(K~Sq_ z|BeGQLRWe>2lDzz+>RZR?8V{>O%2@CaruBo4=}B23mXtrP<6fPY*2Ug-y;LZ0U5v^7CWq)-%U^=&O6lhC|u|9tWyPP_uSDg7L4C*OTwcF4V6$hq9 zLKhN;gKLre`pS>$V2kp7vbUWn66KnkPK5wdbH2f3egHwencu@tegnF2v<&5?Qrx=L z?KJ%$Dxp%Z6+X!5bS?T?fFTYAIdX z@hrcw;pS@!LD|jzrDOLi{}Pc6ZKa42r}i)y02wm>@ZEUr9*|@bd$7({*?Pi@Q5^6} zJubh7E646DxE_?ZCN;;yP@A8e5g^Bu1ISz9`7UyfO}44CqWWk34!`F9T2qC=ph52s zl^=rdc1;h4x|Ig|O&i8X;gRP{&*Sgjwf01f0w6En6*V~^CQa;{@WB0ee>vKiCum!^ zd_6}uJ1cu6^3aw%Vjf-;=bcf1P(_&cw#6$b^cv|?7q5ZtywEd7@Wj#w;KWz2M$Y!?EJ$V278Z?}y20h4`6;$EsS zpjMBDjNN&>$#~2yJD}8}fZI;S`<+ahIr#gc?;J69IvSDO=`$QG6G2CumnVW+bST#R zh22q=YA+0vi4cVLu|(|%DtW}!{SC%vY7jvvg*_sw*PZDm7_dw2a~9tDOnrNQIp^AH zHm8ra(BEf^Jpv|92HOF&v|SAGs>~AN^(NW!o63~Gch(k?KwIjxu-RO+@?KE=FdA81 zoN9&})fhERg0;)Qla&Y8{DPJMt!l^jg7bVy?cU&7b|raRGc`4fTkY#+)H2!PoW7 zlAIGeSV~H^zcbO0yt_&LW(DAHHZf5=7Gq23WLIe}3^OSfA9OPS7Vu%0*9Du|%^ri^ zMa**-=m-zJqs3rmo2O9|>>l5zzn1kSif4{kPV~OH9HI{9TwX`sP#(sV;!-Tf={)C# zT3fq8ck=7xJ}lwp$K(%3oF=?=^;A2WvEv61K6~_i8kgmZ=$gbPw`6Jw7R+J` z7(wDF|NXm%RGhGSby~~KUoy8ZZBk~|TSDPymA?1Ee~3VeA{AB^%P|CnfQl+!jX2YW z^wP!3@j42g_CZb7fADq-VhcafJ;&^%npt9}ra~i*>ZI405ZGYg!Ub7IJVz~VQ9xgE z_5Mb|M%B)vFAUdKp{)YKgU}EBWw@SzcIx#nBTSijqi$1l@tLVGe?$}xE|YwxN__=# z6{vpY5>R-ZmZT81eZnEGsGoDCG*<0P`T1~v&;n7}H@q0#2||@qNofye$@B8=J zVC}+_ZMrktKkH0(H%ALAXyKdv%=V+;B zMpJ6(NOcM+mVAA8$4-7H_d~!NBVo;F1nMB5iuEtU@b^Na)dhC8PqDLcgDwXqys~DW zo7##$#qZ2koaq@OH=@W(1uLq|p8l`y&3gCoH;eVrjbE#O6-?7q5S6H@s@0}dewE11 zP}6Tgv+YbK3E-P`O5sW?dru=GM$q?y6}3BHgJJ+c9NO`~07$)qOC_-R*wW+?qn}piZ?Rd`j7awSvEqWjZX(q-JcP8}&V!v30Ucp#qVJ%(a8nU%^~3+;A#BNB(AL ziC>iA>+xMpe4CEz6`XrI^Or`_R&#&M`#r(@W;H8JGp|)PO^}R0?9ucEp_zZk;EyuD z$sX1F!#KSEOktOf;^&h>-L7Xt!e+u)v#pT0UG#84z@RR%vsImG zgLJaF61K-m+5=SXfh7?)PC;J6%l_%7#+AEL^1R>Q<8M&Wn-9?1;_&u}`B|x!fXN<= z)F&1F%^&a!XJMiK2eylbQm6?Hxk>twbl6E|U<2>6v_L4ZxcioIw-;5e3z0N>3sH}p zV)6&#SQpA;^NM;=f$5$E8U}fBR>z(IoRePfh5lnYdn>a6i&v}dnjN33cxL~qA*USl zKdXU8yfQnpg`W4!a4(ofsPW<+gW=#Y;mJ!2roGl2cJPn)M(?LY8680dg4Fw=S<=HB zwEIe)U=4m+ycJsA%lzN!)o?TlVV(gK#!gL)Y*g*?HhVH18>+|B>yCDwcvOnaXzd6b zHRm1FN$i{&dyIXtd=2m=`Vnn^(G&~NUy7_Q)>*VxYaLmTK|?91~Msh*N-AbEv|;myQ)qHWdmS* zy`Ao=83haV>-hY(H61kHLtXD4vjPBge5C$zuUyg)6QzedeotIod+Nmc|EStOm))*o za1+#nr3YNG(=MD&%LD2c)8UW?AWmQ~LV`WuXCNJmsVk1nD9*@iDS44MXK-eO(A@ur zTSahkpF?!sy90HKCMs(7s5s%#)1$tft$Z4sxo zyRSoheGYM&b?Ol%=#BFyIX&k`5WGu(ZVhtZ57a&2Dbqh~^i|j=4}~0O0tVkvyDi5$wB`vJ(OLzhg~)zoEXR7=b1 z=o#8w9=&SwQayguI4RcMd|(mb&pd6+JD_fABcQA6HC45Kv7;f4rt8M*LE^}#30D! zgm+L2&IC0764eTVf>V%AXo7L~HE~DB;n)OLb#3#+j9MGAPD%7*>M8+y{LgIWBIWs*@oK*15n`PydCXKklO&I8 zyky=8@%t_H$Xbq^Q*>cB??XVO&-~FbF)#MAp;HnE^MR8zls38mh#y49CCT^rNW%+~ zPwtuwp*5dR`g0&}sMS46P8pH|#pQV@ugZ-AWl@nry0J;W_^xmo}I{=gadsJO% zSUC>L(tZ()6oPIKf2!ofC}NaXeiwy0ZE)K#9^j#OLGt|(7x1K_oMPhcFW_k}{isdQ z^iyB4Xefq*?}g3e1J5X{qhV$tYHGgTzP?P%2{_|4h!^f-NAjGapIdTmx_PdkZ0GHd zsX0%G__x5reLpF9Z+D3W5u5U8;B~zb4L+Ew85kocCt$;~NUz6E7cdug$w7`YCcM0u z*?xK(jo1Ij$|Sx&Xd|7UXVDv%7t8axJsnA5o`32xkwK**`~Y?V7k=V^W;xo#x#;bJ zeQ`S3MGir+sYwaw9{OcO3Ug)ws07Kb2l)y{v%Bi%_eOwBj@IwI;B3UTr4dQ+9Rq~&Y^2y@)Wjttw7Jpyxx42Qh9J2UG((YKL@qI|SeVBiC_L~r} z@GHy`$M@l@$}Ni}BiC?yIf?b9Z9MIkBfv$zyqAWVF#l{Wm(D}@Z9?q$7#dSdYP~>z zI3(u;lruM{1(n_KwcWSP6Qo>FTme(9MrmQzR`{7-AJR`x$Y#Lz_cvV?T1OXoIi zS=QJ&e#>z>4C&_=RT&VRIvk7z%EV$X>^7o?ibt(>`^;Q04!7h;f{djMAQ-#I$@I$v zCgM=H2}}e5mOn2Y_HL;d#a1mVy`kHXbYbL8R_yL#Q^m^V$(pBR_M~5thj1n zJaD^hJ_c!QIp~xlzT*VYSGXd;SFf2FpHKp%*PGaZV}pH5dd8}! zmdkn$kOqc@3$9U6!gnZ5!SMLFQk#FWO$eyRoJgz42T(n+1Q{f%53NmT2z6e& zL3wX|U7{}`NHiJ)hi(-*5|n`_3q|%PMWNNhG$+w6@+mNn&(n#(x55!93)2Olhtw>{m{Hqj`_}Ob1!hMS0^&X!6BQ;P2)!g8P?YFhdF9hT{TxV; z;%)L#09AU`<|>I!tiY9udeBmZG?#ncgS_~Cc89Oy;8H5+AbahmBPkO^{A_A}Feo6Z zP0lY-ff8MAf{c-K>>(fMMZFEJo$LP?TD$E>C1}S^EZTy=*w3ms&jiy9IPxSAaHtCZ zeL(P2tNYNQ{U_HU{+sSc^gl0ifi2ow;HwCLo|7{g0b``-gId*E17L4k%9fR5P!W$Q zh;zw8%bcd)n#~vhBr{@FmpZ5=*Gmwwu^9{uZl9l@-{woxToaOZk^L?rdr~|x*#c70 zqIg|hU6J{xK{qn>wRLEahv|oYB>ZiI!PGJB${w3=0UEoQp^#hHj|pF31=YftPUHO; zkCVuyTd%raFvupRPpBCD>%fD*AJI=*a^_b%nZHEbKPFJzuEKl=J3(Ztu42C{*|Ks1 zG^op3QyKvaA$}1=Ceb$k%%^Ze-9%DqH(1L!0ABFJQF0qVaJ zUW2|TzM`?V8N#pb2qLY7&|0k()L!fXOTu^*HUHyLZLqU;Uxoi#8O7x_ru7^I9LbOc zp2L4~&VtB_^OJst3y#qIhXBWGuJ%^;kn#)?*X^^fxMI}_c#NEa!qmCGFJBWnAVu<< z$Ge$Ce3vK1a9qHPz4_zIEu&Cn{jTR_nhc^1G8NC3W}g9p@}0V)4~Ei)PT8b;5U=M% zXHh*-HTQYD&LHY~fydV#TsWX-{}c^$UQs%cJ~cUmHzK(UU3c1p5DvWes%*WMvi1(L z5_TfBXo8lDap-N`$nIt`u#Jo9?qF=g-qHURk^}i8SekJ)Z6G)05fGQWVW@9HhV;vl zI0b^)ETsTEEUXqZnyvS#=(l`Z=)iC|@F93OcH zLlWJcLBwNQu-W4SHZbxAZwOWT7Un`%voaht750J+^>NG#?(o4;8aLU{ax)_ppGXs3 zXEZ5+3X;+pxs#gprW*J}73Ai8WTHI!an#}Zfo{J&281I3aoa!SH}~cJHHzyl!RJ`8 z?6t|ZH1hyY#Y1GD(%_Wq&3!d?QM^>rYdKLzZucK?(-v2HeN>=5J9O5DNBPPkRus~M zLd$`NyfknBcajeL`4aUA-0EKA@O<=BpDLV;8%jz0?nb9) z_kbWo%~n!%t61#Z3=q*n7i5(5&W6!$Yrz%1n9uy zv~Be6CWtMrxf;y#!!IYpJtcWrHa6rq)2h(M!k?>+4A41IrweIctYRBpul_r4VDri;D7`L< z{z;i}C3@NK9?FMii_tV-y-dOYnZpm6%p5HQNN2&^b)e9*?#{^7obak}x8~x(@^h0A zf_4{%`WtgEO68-wL7C+&kU-Er*?e9mi+*c#K3K7kD|bE@c%JtAZ%?alL>*_bgC;U? zQ4e^$`UKFg3VuN4`wX|vHrWQ}mV27u=1fV!=pP7){j!;CxL9~ z@y;6Cx?OLTe|}HcLG$nzH2C)sD2AWK@BV67Z?avqh+qfcLxnFKIci~{+kq|=&=+8{ zAHNGTK_$KAKmm$GepGj#{(L}ktc}2-An?-zAqwY@o$hqlidDSeDFi`z=6t@uL-+j9 zc`wW()rijl1b1BUL?^{~LS?@8gCNtP_$%ov9R!$Xhi(*nl~rA&K%x6;``H&(YxT|q zQ>;`rx+En06O;wC?F0d$l=jH+)xeFSzX?t544{9%H1iTTI5FsG!?rcemPPCKm%NyZ zE|j-W#w9NmRa}v{TkUa$qJaufIbX5*8e%KBwHZvY=ek(Exk--6qMRCb3@Aan`eEgJ zXktB8wzYFX7`H3vE-=Vxrs@ln7ajE#YT@`jLvb`auML;&0fu$*(M#4yMx@-YCeQaV z?4NH_P>7;D%eGo}_nOu(NA^`Ft;3p-RuA-EiX|=l*k$@4*yA-0%lt%msx2O@4mO%q z|D}LmJUoFiF)w*(_l!O7eVme?g0dtwR+BvbjXUgnRrIJ5$ItFVE_=j3_v1x`ekar z-p|Ri;>xni_~K@?jm!n$G|{47aAALw3n91Y;BBUQK_zIZU?RS`53sGU$3$)5+Zo=O zFr@N*DxM)C32Ye9?qsX6o1DHum-}%jK zBC+|Tv7|l8F0|FUjxibF7M@mh^U6p}6pvb2G9ap%`;4bskrw6Ntamjs#KQP(T zSMGnCzx&G_{t!rDT8d1=4lL8HP(Yyuz1LB`VhLu8>h#f9$lG7Zz-@>8-kZ&)a+_J} zEJW+>6|Ni~fP35o&JT8*BX{0}`x~ zs0Xz8Tp)9E#fJ}|g{LPW7g-QR-w1k9ZePw6YLWsG@1EPe&@KzdE33}J4=`t%H=IWn zZ*lIw$f78@eU@hLp>?%goWEXq{har{iN6(jebcouruu+4Jy&q_%om|aZJ=(SkFQ@v zDIG-p+kU@Gstt_NW-?A6JuEj6sRwCnwHaMEgvFyddy(CM2QcE)f1j>tSICn00xH=^ zxm5)=Lz@O7)rTJQuN?T$!&xWQf3RbF*M;n5uYxC;?uxN_!3msF9Jq1c|6! zZ&3v{ZE6SFRMNAI=Dl z-miRgYxXn2=X>V2`NB-mDJ9dVu*-;5-$33@4&y@RqshNv`b4cc#SUfFNgzcV4_Ca1 z+6v`K?0cgaQ~|&hbC2S_@Rp99uS;r35L}@zF&n`qL3+MSc;0fifoST&I9p1 z*fyp8AC@P~&TGx9$v_}KRa)zI_~Wf<2(?sbLOz4#0CBH=&fycFG?8`_ONM&rd!-NM zEtH+ah{9)F!we#;1M75@gC?xokuKyYYH)!-^i_%VFGK@Cnm(w9h2uZc%iC7#{F5)R z>sTl^@futMWydx<;jpxb6QXvizc9Gnm2n_D^nl=*D`f!DkvbR45WgspI(-K#4m$q2u0C zo(t6Z?_5tq7RYfZahQF|0gKa8LUq|CVtqQ_w1te+LmC*9P!RAwFN5+Oi|r|8KK9Rt zo=qfmTNAK^Yr?T{H#Li5@y*d7LFP?h8ry(%Um7GWx&PIVLgJGIw%EJ7XqI|NH^W9> z|Ci>~2m47x6$E~*sOFgtBw2xk(qT`%mjp||)Qn7l-0NllsY1hlsN2slUuU1gT&0>k zUaxi=lmk3llR?_kL4N6N^vwJ+>iP5iiiJH8NuiVYx-|!tgrA$8XajPmB{Q3#xg<8$ z-wOE#iMs{5UKZ5x(QP+9#6hey4%DE!DGFi1T9{VJ@NGh&R+^;)O_dIX*MLoD8gk48 zqCUz<3|91}MET`x==k^uR~>z3Al-cN-bc7Smi%-mgB38C|8ybCUzUIhuwEU%WEV4i ze6`a75xMmyCC|s>JHXcU2CX>BBuWC{Ty}gu48rY1i$m-Cs4%)$<;`=~s11@eOQuds zZh0!LZh=||99eDhjWbzRp2dReNf3H^ksE`hIi_B}R17z;_ z{fxWzQQYS_1508KM~&rKzx_{W&N_OsnH51+8$)X{ka62koTqBrw>g$U%Dt9yo1DKw znG08gx!uhDxuVJ${cCt9r)xb0yCL*GF~(w+o94jmdl;*zXSF!r3Wn|$8rPe?4Fvi3 zn_mt~$4Ly+AQ!|p?RsI7xUvnI6Zn5Xds4a-hCr4+Z9)$vJPU+lrs=GA^ zfL!=#MSw??YGvg^n{hpj(otXX>`9J%nB!6+ed`knR z_DCu6C z!TF!-`%IaI9(BG9ZsFRfpxOkG@~JzFryO9jE2Oqho-rWI`3c2D7Cj}-{L#)Pzx*{Oa+BML1fAl3I%5XRDH&`^>5RuppGEsP#Zw+TXJDu zDkn`HgA!@ny|=sd0qms|;JKJ00}oX8@Hd=sylB1qd4tJ~V5y*pNAD+amZ40Wwn>y9 zqL_hi`0VC1s4;pjH;#-(HKQjo#;qA?JZi;Jk2le(xu!`8mh4RNyI6$h-dxkC2BgIZ z{qP33XEH60rl>l>t=oFpAq9^iy;gCGD%6My)^4)pba8i+13K?pgPesKzG9eiir5wR zNVuOom={l!8PU8A3VXKQ`R8Sdnq^`^qA z5FJ;%3eDy_qaQ4|9_*OA;WqI-BCbq3Rwtuldog@)f9B)K;6V4h{mq#1cN>l70jlN^ z$BpLshq}{V=M(^v2bx|F9yAx#%X7=LY+jbN(2P?ur%AN=Teo3(F zJAyZ$bah@Z^qOnSI5Xgae6-?f)Gu+&KfNji*A^E^(?CE+`9-_dg}Zm5*LJ*;YnLT^GhQ}+_k5%j) zeX_dV4j^bD%KE{F%uPISgY2VM41Q=$16%eQCz{IyZXHuBnK=Wt)VtT7YN*_rfcqmc zbkXb;TU@qfDHC=ARf_Ch8%Lae%qZucT_Ku2^HFRYQ>5#VF%d6R-hZ$1;!w-EJFAAB z7QFY3at$cu7x&kHcxG-AoujCzB2++!vt6dIpAZ((uPSmMcdv7>3~>Q=`*r9R&KzL= z14lW>;9)98^V5c8BYG=ycU?SVe}``J%bBnJ810B($JD{2LPyng)g^J&`p1iV`YC*| zyA2m7)l5fCR!C`egB%A!UXU+$0a7l%T#Ub6?kHANzP0!wd;)5y=vC%(uasgHHC9pF zSAXXeg55~>^BU=xm<*&`^chd1YuSo$|4Q*AMp_3Eds-2SD5OM$6w%iN`@vYMP z@pKH$6?gFA4Uyv4N=aMg1i2A01iv*~?(eB`lMIqq-PBHWrz#F(WN<2E(r@9cj`OJev);MFL+>Y zi$&+bu}k{oOK8uN5dbx*fHgpinq(ys@WGQhj2&T}qe*gD`|uVh`^j{ph#j#ZBjAwk zE^v{sccwM+)y)x_tsuE{bdqHEy_*)xONWxJ<@sQ}q^CRYs$mRl&Jt#9x0QU@M5bSv zGzZv`>2uK10x4A@20yP1iZ|$AE`{w-*?&bi*W9P} zIAs4Ka23V{bp!HX7&@LeR(S{x%F=vOZ>%^hkL)M{7Ods$C!95TQZ_WSJuij<9zIP4 zDLOk1n!)jpfbKK6@OPHZAj!#mm)fZ5#?Z!84x4zMjHqz*f*W8v0C%r;MG_GgWcmd! zf)^Y)EX?V`_T%m;!+h-rbgX)Q}LEY=2rL9sceu=uE-rDgHU{qm8S-@p` z!({r^Zie!CStQW~uF#!s7!SSjrZsRal1*;} zgqIcDztj8tN5lrDy%5=XU|8@Ft9Vkj*@ydSKEQC{+Y2^$zgyhT%2bf92YIQTr0iWa zYPm}bIkd@008P-g6H@Phs-#hqj@x5Wg=^d=AWOd@%59HMmz+%sMy!3knG@w>H!-e( z-#HTkUi#K;oiU%-eaPJxHXha?jhZCJ$SoAJ(|M3TiDM29G{cDri}Q z7TcE*XhCfM^)Y#~FHyaE%n5A%tkCCCMWYv>bn4!j#LHu#E_ZHdoq8g&Llu8MW&A7u z>!j>Y6aad&7RyYby!;Wr9pHT?99(0O_!>_l^aX*JtTdGpc91m$r>gwg493n+o28`+K2z9EwVl>Hk*uC3x zp5t!Wi#<9VEHy~O2N+fqx|VbLVhh9_-PWyOks}}lL{O|3R{AZ@H0ljgjyiRGRl>ShOo%sEI8TdR+{`iyhHu%tVPfZc`VT4m z*vNzA>vlVDcS7Fb!0lh0ep4rXuG5R3Zaf+V?yE&XqWVvOuUn!gaK4T&KBlN_Jh|kC{CAq!!=HIv2QMG{ z9Re5<;Y9%f8L6OnEnTkLCUkt!}4d*oa1vocZbEBcBxgprrr;SLOGoCKK)1tYn?<7li zj&bBx=}0@O;)1AeDt0N0d__X}!oEdXKMbfT9#A2>;uu?+3reK^qSCZEC+AKRwu?&m zpe6X6lmzN=G9Fk7^I-=G1!p`fKyTBbJ8?xBZ$wWwz7*tLEqo~{?8v$B=&(o6k=yap+lIV^B5+ip3qT z?Ykamr#Hjw)2Nh;o&%}sJzT6rLOw^;6*ZSGMjqg-gz}2Ji!aY{jfs3H68t6Xowd{p z!rL_G4;wjqkApgwp8HpQaycbDs9Gm?Ns9AXfDE#@ebp=tSuV-pis;ITiQz# zQ?O(V8XcBsnEeNa)rpubi#8}!?Mj6rM#X5FKGvmkeGV7?+-qc5{^s~lj6&A?5pEY* zI)#vz;qn1IUzhrk&=Q`@agSkPM}6>mxjNDvenXK>M?m&WaBbP9OS5sDkjEamvL-46 zZ=_Q;G+T^$dnnZ07iD=6N(`gLMQz#WHsagGoQ`yCjs8#ummugH4WbbxFJWnG6Eyn+ zKzIViyIB0Gd`VfQRs7!`khZwNm$%)GM&A&)stzi2b*=e325q$vdudgNdp8l(m|QvH zd^FH7N{gd30FdHqYl(`Pcw%X`!@H{=X0+o$#o=~N-vEuKn9I!b9(XPE{xDCdk3Sj5wT9*!;I+Jd0=?qeQxf*i^8>h&M@=45+`z!Tp>6isG|#I ztL)*_cX+@4n0{gP-Qu*4oy%5Pi}BxmgOa-7y;XIpuWD>i9e_W>$lOx-l>Y23CnGrc z?&HpKg5(8Hy$WBjkuzO@vc|k=bO+T<`{&GeC*XHzKH0ByvyXua{>J#{?{#SDvJJvL z1G}RP757b{;uWI0w$Jy%~{&~L#w;vde(h@+=nP`5^xKVvCKKuM3oz3Vi?>V8nEX*zn z;a4y+xkN2a7w_oE(`SAd{kw1H@*;H*YdJ(FN9c?Zuhanl*08eBL1$!M$NI z7lN>5l1}C;8_CqwRS6#b0oDt#f~zC7Tc`E23r7~gVep+R5`;?-ZuzamyW6^kqA-w9PRiA5{9xdLszfM3ncG zNf#d_*&jMo6EpHm@|^-Ip3HTF71cH^5k$;p$$Fh7o5DgD7X3@;0{`)~$8@6c7-%$` z)I_^1*6XQweZhHroqJysv9{2=Yn&k!&j&Zdt{&!3FiD34{I|Ru57pr$?H{}`_lvZ& z+W~HeR4?jl`#D9~W{_C|T|41Ik_rxcEs(ZQFisG7c#G?^yf(e0gMlPmoM%$?a|Dyj zIPqr!pLY|Jw!Z3S;2>D&lPPjy@rK${2Owi`XM!wg<--t^gq&IV90=vlyM{|%<~NZ0OS{2XNwZh!QBQsuhapA@L1zbR}X zGClpK$6#>yEaF1R4XdOa2M4zROUq~O^+tT?x`{_GKJQCxgSN$-0!nZQO>DQCzo7T2?-myH8Ufpl-uMs%uI=~ zu@T#Bv+e(^bWYCi^}oFS|JVO{z1ZWK=Xt)@&-e3rzdxVPm%W!PQevMWJ4z9d@y?%G zCjRpb>}YM{4aCq4b-eBybUxrwqthjyYi6eK-8*bRkX@Q1=HNE(c(bUvGY)$Na}~q4 z-(J8^QFIsGfUMEdcY3j0hvw`0#rGC(71AX_VU1)6I`algJC!Vd) zNPlAU6TzZ(g&AF33^8^39Iy)9e>cJKYYP8;5$ueOy%K*Rwl4<3ZKX?9u<_37%kN)W z(*JX{>2c}4l!4pHN=l|!MYLyCgum&z{VH?%+pHh1T%98&ZJ4Ev3|zEGb9x7 z;@DClD7tX%UL&?R|EiSq1CrzdYo^mT?)FC5(l0B#mE_rb*i7+{?9=Nr*gh7IiTIPi&Ho$^&#tPuKL|&GsW;OY-KhpnK#o^TtN;SFR(DM5=$ByF$H>gIq}!+L zJ^;3Q3r|xoD*$?6r*eB!qn5cif`V3zOViMZ01s*kXn9BO6}Pvi#b55>#~hbfKL^wW z%kVphncSDSe_jimh4`whF8;3ooe_dywbB*8&)0}}QlCoMKPkT0N|CO%A-=aBE`+^- z%cZpa{F72tq@t;`Qcup-SiIw!L`7^eCDv&jLDfA~EWx@?4OK+|ZE^Tw0NGcFargQD z4sDE@|N6J%K+>AU5yG~&sqN0~`aiBI#rW)3&TxCVa2(z%5fjbmm#0?@0G}Nz3CczA zdGYByn4+kd*loRW9j$*1JiXGuVOm#IJxx! zsA%~3rzM?Y4RTF`JN^0^oN;Y-<9@h@2O8LK6S3oTIs3J3Zm;v`*r4Omy;pfYX(v@I zmzev7qL$3e4`yr1_?4*A^7I%!vL~Tw$5}~6*-z4`?Zv`{0L@ER3ZfYQ#=qFr<$v7O zm1BOrbGJFcBPM2)GW71AX~6Y28tS_qn2!K@Qc+w%>u_72*sxmXjrsKjAG_fYAPxVW zX6}aHu&Te%fK3B(&Z?`KYzCSRk3uz69_n5`aC_nP3(Qs6!KSnmHLk)7*MHu8Myoz6UBO4IpBPghqV8+0$;th$ z@A`i|=m3ZOaw^u^_FQa5pFiX}iwvm##jRaxCdG3=g#KdIekksV)+nUkb>cvKn<-hI z(8dW15pvrb2M4pAV?dmjU7o@C2-Alsxi?iT{c8%CLw}z9l2*iUc-? zg+EA~-+h@_m-YbOc<$lYrGk`IyMFiVSD@%~jXP^rZSe=67Z8~T#{W5j8UsY|wGWV) ze)=)N%HGf7!Yk<3v4uGA#s6&aP`g`j;%-dR-QcTGwF~;!$rDdi_W&9oU@c@(u!^1< z6`~E@XpE*{;dc%KNSWTiV^oC8af6+S$e!Bu)~SG#jIh468~IVBpZphHq?R_sjGJp4 zn?#f=KGt8y0!ZM`1zY_Ua%33c6_HaBz6~bXeT-PQ z5|h6VzTxku0VmB%cSAnE!C*ZmI4(NC{nF9>pT|+BJFo<&;AXijmLTxR1La<-L`^6K5%GqRdhV;&@zN&Dy z#YH|5Uocz??TP9Q0MLt&;v*iPjpD!S)i(#YkJ%c$l}~zS z_$66ELbZF6P#gt#Dx|r)TOqFb^XhJF?8B4GPqQ1uf{4^gQ^q{~2ZQ@7N52c10@j7# z;nCr2)BV=JALFN{sP&DsLpBanM@z&J;Xv|;Q)uegU}jxZ#Gc~Sh>Wjqp8DQ<>8z~x z(w#5C5aWT|G)i3&;23d3xXw`TC6WXUwwLDjxyuJ?fAI6m03XKS;gNw1-Y#wVvTCjtqSf)PsJ7dM6}q125b#4}-LDGknjA`XuCIXA?X@}WXgGZjC5;9b=>ASE*0 z@h_*npN{#oU1-ZZ=no)VyE9#Z`UV9e#VEPpYvx{XD- zCM5uOcafw9xO((2XOo_MNs0C{7u%F00Cr$qV$>7e3Eb66ewdvg?enTzp#RXEZM)TC zlfKmq56p8j1pqBm=c86h-TIi7(ekSSX8>^nWLequf79%5f!aEF!r`3w7SELv|C!%t zp!Q2o@aI%)!L!q+fQ(PzP_A@z`qL44SRWN_X7iUw`~H40KU*Af2>NMrN5dgh$a1e3G+Y z890Ebug52DbDtkb9StzW-PfEB>iW(Av;}wn$1Ov>C$?VG`%b9Q{83x&rR%kXt9#9^ z_Gleh_vbY)S6c;5m|Vq4!*~7$;q~}>3o}tgv@N1>@1BPUhdlubGhWzs{s;dnYvhwz9?g6i?NmPdy59x65dFy)@&A)WcppBQu*Jc z{A&Bn2Ynd{39{nYSe&4qPRb?0Q46;jSn*Ww{)Bu63E1XjK=YH_igm>i zw*k8oLL?;c$=Mj!v8{%)F|05+dOOIlHU z{U4j{>UqEhDV#;3X#V(@(wh8lX-&SC7D9-}34jOyq@(O?gtXcNB-E^1>rnpxbz#4) zz?bs`_fua=^S@s;>jA^|e=kd~c&VQ`HTyaHF- z=!W1eF8~WHz5Ak&H}^UH*tZX>TeZA^x7AN`^UVfQihyBQqOE;nt#$+46a3fztK$4$ zH0X%y=Ls?!CT+8zgZfTsJ0TcH2FXXry@*apb3$vF{;F;kcKM0v^I*olcI0qWcxY9e zU?tXpR|T+yeGU#~lLf%o;gpLjmLCU&qo_!(P5z7%*X$gRzJd$y3TEPXJc_?Ne6cIW+ye%Dk>Bd^9%2~yOalhdG|XCb`Q_Jrn`ZWLE2lJD{y zd_N00D?vUK9u3>1K>eHD`r7D?tGlfj+}QmtpqsXa9yz4pJwOz8VpafF!M zM@y|!yO?LD9uZT2n3;$mct-EM5ak<$0bS705+dA5t#OYS?uFgdr%;$6s#woy^l95x zzY!lAXA)|z`7oQ@pikv@xK*g;pr0oS-erg{tMB~P9;iJV*sv7ZM62j%>Tdm)6qJ#< zW$}4VH0z}|^Ypc~JU~utn-{Z7vw^AGy$ zXIgHf1K|W$i0PE8srW0_+4u@ftvz!=>o781d?x_#B(eHLAB;_C=@Ja39nznCFYD^^ zIY0K-9+Q5Kr4v?*e7}d9)DqPi%41WU@>dvTjOOJS4n9seH#uo!SrZNmnSB-c^6Kq= zv-(#Lhvhb;1)TE=+F=&p<$V0e0Wfai0}mCc*uKb_|McBKgP z%pQdggOM7@h@n&5j;*}36eA&reD_EsH;}XRzO{H(GjXtlWvbRXIFhaw$-*q93Ypvc z+4niIVLtgrfc4!+;MAB6%1zO*q|V_5>UV#DG8GB~>bIOhK2n(Uc;HO_lR!2rf{C3i}c!I90jMLvVj0ACamKZWOq_ zTrw%#v51t37g(Ha=O#{vi`};x=&Pj|D*cWd+J{c=8=YRd(t}%LmjbO)slKY`;M+%E zzRYqzKwfboEqIaPJG7JM(~-rJ_#F~R=9OQ)aHBsOc`u_({O(D8QtgHR>ahtqaP**M~tf2 zt_4F%*8}iTt$OyT|Md~;Di5Od0FZ9TXBIMkWr+W4-nO5A2Md@XBb~8P3r0dIO8D(D z-@yI#^WXSitbBv@^e{;Kp4pSKb=ulm&wTQG#*{r8@8Pnu4}Mp#ogNjJBr}%uA$srS z*g~%I-Q0yAwpmAm2dkwMSaEM4k}y7>e)?&=lSzEzo7#nw`oD;oc5hY;K>uZ9RjJ80 z8x;%7q5HziRZl|Nk(06qUlq2cDP-b#^Y3%ms-{X2+ln;;B!H+6_@QtK{sZVSJGCT< zi;V_z(t-zB-j+)zknF+O*rdgOuc70%tAN<-_WG0L5DQwQZ6Yl&J@@=~V=gpK=YL*Yk?b&%QK{DQw!!DE?QUz78<;k=%=60hcRj-l}h_NH=ReO88E``OxR22fu zlGPZVyZ|gKk+mv_k&Tx$jQ$cw4%VBzj}#gjnmAFN-P}arfGvm!@`t5~q2GBwh{9;A zOwTvP8msy$78jnXEMK}FjTX(f-V}0LM`)2TLaN8PPxSl{{`ox{>djQM6ZMT<6q|Aflx+?)Ulkn4GnP>q?&rJoi8DxVYK|1Cx4R*XtsCKw^6CFzt_L z$&0i{MSv(7z~4othwyF_0Ry|N@BaM( zsk-y!xvRhb*2F7yXEoGZ7ezz&b`MaCiULed{T^h}AHAC1pg_4Y?~Sej4M&!wS|R>8}A^o&*LPVrW^0HPf3`$1{xn!SToo@|UdUlBcKUG>F9t? z1r}-wqW8XE0Nt~vHHd6z_5(Ds8A}wo_^SQPcQUh^a40;WL{@vy!R4_%=LoQdCT)J$ zZ@=~NqS(D99AFz$Vd}>4@PX25KQdL{a%mfnDXyRa@x5B{`H;7`^rN2=*3RUp^i4msH;jV zFKQbbr-XK!9z%c@m3yZO>hugxn{QPbDB|-U33-VwY*?7Oyv$hR4IFOjyI>!C?ZU;}a!J5qgjlA(8BwYhd%E`S@LJ+Pus) z182U8vZ}2sgT80ptdovX&_e_*@44e+v?3tOQ{ew1$#zDhgJ!mgSkceM-?{TNmFw7OdF*C zHOGy~O5Wn^AnQdxQz<+{@FjJTh|h!k4ia5x8_feWMx$l{ucrulixD4{R4xR?kc5?C zCm1Az_lQ~<+fdmNJPqD88Kig62<^an2`HjF@7_062BRhQr-sdc$Cx|oD|Pc5o5-^Y zk4jLbg*935;ZbIUFk7N>3Xdbr$Wx;`qT2Gf4HI^hePp6NBh_zcbo41oU#Z_Fn^BlH zxZ)wmgnz1uCMpiIAPV#s`U<1 z!}WrkQ{@VP`0g;o+)XE~`R78-`@RV{MZ{K=-b$Qw5zMvD-?Db7U6M z-_pi`s!KW~YCEu$eG=5&x=#d8JzBLzB*EFmza8O7OOBHG5)t%nB9F;mEbdA_sPvML zY(|_BkQLoYh(MlqRA`aj{W!d&?H=-p8^}Z7r<*rWM}g$NOVqwEKGO;p^`0laGW z17PO=TF(eA8U?f`-NiradRf_VlJvQ{T>gGo`q7RmT^>V!Bw6wDm|%h-@jNAl=(H@i zUZ1X}`6Vl1!B3v$skWI(k}n7Z?i6HGNjFPZSkjqd`~(tID`FMw=8v&=uNlgXwqGpp z1oz#tt<)_%xs1K10t@Gdl2Z3ZU^Gtg^xVsZy%*OF58l69AG8d!BHNKC)_GMpVXEle zu$YuCLC--$cTZ2^k%*^)o*icKF#Wen|LQTZ+sa#=_;K=3%`!}Lr zc*|OHky9UD1?=9JWm+8+eAvW&@7AAWh8mR3#|VD9s0oy-q#~Z+k;?`~EN?yNKWxa- zL2s@-6SnXEC!FdV#95q^&b3KUB1$mt?cmv7(v+^t$>>=c)i1_ivF@%8G)EZOF z2H>%HFb#ghj$yCSp!AEWpv<%4CRue-@?C=jGViXYhpJgbX`Og|pWBY`0DH1&!~xjl zPQXa&CR|{!<%hCtms;_${3Br#PItwX~`uvCSg;H-m9LCm3BqpnZV zs8xG3Em-wD?QP*I&Ym>(6u7SQ6`A8)O^cJAV`i7J5qKZHxlDNep)3*nqqou>7}rf9_fG~ z2CZvtxOSC&VO^KZ18QSxXDdGN)XHcj@p1IR^a!nw^9` zIXJR|X_$X*V5;Ri*u%Lv`e}c^q1Zn|`ZvOwVVA)u=ilNcXp{847B9`ot| z<{ix793S2aL|bd_H<=%RZ>yH?)<5O5BP>)3Lhos!?CbQKYAeCkYN!9 z+0YbE%8NX{WXK95{b-Hilo=_lynVr*8WJx#sBEGo&DXa<2@Af+F#9Z`LXMCEfKG*T z2$oK7n|XwP!x#z^9)utltT$nTr!F3*Ux5`N)=2e)dYJuZ47IXjj~t6)oKT_beFd~% zSC&m3$H}4hK3KNW|DEf82CZC9V><|_>eeXq9UqKpjY9LKu2!3$R6Rj?v)CU+i04gv z&MqD2a%!R2q{@@AJvL6+^q}!+Kz}ybghJf!ExBQnH5h2~VEWs*(SL7?$i96HR%iJk0FTA2CcE=ZI8r2EUWLJ4;#?i#GL^jO}{a@65I#N@X4*K1G zp6|z@TPm4ue?>@E%B@qdD4%k64St<{j2rK%Sc>I!#4HN`Fo{<(V~lS46FlW@3CfYw zG}*TwS!9r1SgnR7SL_aTr*|P-3>5%_s=LKtQj&5=d$I*}6r2_3Eol|RJ!?vXvi6xN zsoqjHr9o)D*rl10H1M-EGJ4#7XjR>?0(}P;j9aQ)j`Ba%9JnWM76A5aHD{S}C4_x5 z4lldFkcg)ZxEDi7Ws17>_uFJERWf?!%YSsjc&KmR%-D{1oYH)s&#b5t9km(vi&pH& zi4v=WWwdrJ(gxauVTjS)vX7)|UZ~>t7NQU1ExwH=OmY;}A$`8LS3zc+vnS6ywN6P* z5R2dGc?2Hi^}Fz7JHgw9taK+0D__7P3w$X){U z3@b0rJjOA1rUS^Tt%OwvqyQKW(wAk4k_})bon4NJT%RECN4U0j$wY+4x_+f(PsL$u zhv-+7e)ym8u9eKAFFYm2?dt`--IHA80hlK)sv;Y^BI$@zLg$q7_95mp+QK$}^gbyL zTGM!5+N=JavjL#oF5O30Nd=JLbsuiYb!b%v{$QV+)ExX#@X~5A&i%@c8PqnLs_f7o zT-BADDR;fzpiRlR^X_^1Fpu1em?}_4C#M#kViC1GpL49!Tlqj-a4x3e%B*XC@}YvR zGX@gJkr`}N;$YEv##jKc-qaqHsDEv~u;$*mzDB_W>?N1^b6QxzCPpj2+ON#ozM$L; zCDRNjd?dHVq4Ku9r_3KMxs)s(%SLWRwDZUmmPGY05qAueL_sez${-eb#k_O*qsNPz zHqO-ny86J4xD+B7LfkHct_wrouUu5s)y@5WzdlC^AH2_U78F5Rw#nTV4(u<$ov^)G zSfZ4jJE|0YoK4jDv<2OCNSi7w-H{fEeceV*w-rr#gZG|oGGEaNzbi@&+R`X-5k5>{ z!iEqd7VaA8{#=Eh6gL*y9}!mwmZzLIHSjtTWiMFo+}R_+U+y`&W}3?c-~NJ%jVIo1 z#!|NkG7@Ti)RLabRvLlQN6$Q5P7bOsuPPj2Ets-pycU(0!N$ge@pf;b*L~oTY3xDv z2{F=6bGdWWEwUJCKeP-;X~tCV1(hB#FyWzAZ4N@l3qMdk-Ubdk)S`KFFL% zkF|vyQYFP*<62XEw!|JgR6eU+(_rUi0VDQbc_2fT1#{Afe9*|S;^>I>unO;8@*%Mv2 zca!=1PbMP|1?etNd2IQ2*4JBAR>Xl(b^L;J3ryJ4n0xGfhSbcNOx<0Jr4q{IB;!{q z7f=YC+AGX3ap2>cpZ=;+q>`xtDuNh}$LxS=XwFBPC4-*mKU$h$QUyDH`0T9kccq*0pWfNm zesBC9*qo(YdNm49tC9U}Va9~^s|-7lEv4{C#O3$Nx2PSawTpW+rFLko`sRpK{aLNS zeF@a}2!bkN5Mv>NS4dHS7cnnis{8^jcXPt=D1aIf)V%z3^5$GPG!{2Q=~Ga8Rv(%6 zU?I4en8;15Sc=wN zxh4Uc%Q}-M2|P->#c$DQfZ7_Us^9vH29`3&oR#RYN@@M0f`3x+N$Zx7zq&E8^pEDv zyL)y>PyRlJHh(ePr56&EdVql+5EpP}daYYlCG>B0O-2TMtSxnizsKKC_-q>pvs*^EdPI-{4{i0Z>(ISkQ!g zt8eaXECyuP0D%8K^CO{w{rkQ-DjjCOvWWokpkk7l`An#^^DsJ19qg80kaI~i*FI1- za63!CY?ai5)=k3eG`4DZIv78CZ^CT&ty~ZOes-AAtc@=BFPqH`SxD!98gDU*3@pa@ zK~~PWH$_!kDrFwHB0rO*Ht0)CvrgOL-=xH7iwhrA2QukR8i{m$wi+kTKu2sSZ6jRk zdr?7TEE0kzqCCm@bq$LUi=!@x(CMCL+>VIZAU!?DGyt?vj}{CE)A4O-Ww)!Vq1>+c zit_Tq^mmowCHix`MYu~B$*F`X<_g!6%E6R>y3Q-v=EX8Fdl{XbXH`xXkO8BVGx^sq zi>~p4x7q_h+pBEy_!wRn+11%)6j_uCG@z|P93ysVc_AQsB@>oKX;8jYz}4e0+IdKK z>6*?y!LW4)S(CW^k@3egS1Rf>_NkUa&*eg=ih>L$%p9`JBDu4S(aWR$yM6L;F;&Cd zr|QQAW>+`SYA5XRX!WDdN7CekeyP=Lf^k3i0_r+N$|g$VyJC9 zji6@56Z?bgUm&;9D}ACL?S%W6Y>Q^UNRJZq4o-F-J^U1soy>|Kt)SUxD4#*kO*XE9 zFcYFr|1&TuC-x?p%hG6i`cBbVnU02somNb&6O zHJ`IMKhuwvL43FOAZ!)QTjHVbsVk?08 zPeAqhKEYGbzQx1fTeJ7MKQT-j7B9XY%h2R?p3kmsw4Q-R>p5_JMIN084;)75Ug@=) zdX924-VMtu$Xnq>g?IOd|GG0iq%3G6&g|w)gIjxi==8a@!-1Xi4-4+qX7e=e`V?N4 zCY|!vdxC2%Cqoh5k7DhoW30x3+Gl4E(1PoBeF)D#_o?`-$bzC`>hW?!Xvy4rOpG_) zDjv)?ThGi1T2FIuvij)YBp3tK?*+s@bn_CXCfjQb;c(2aS+zOWQ>TnnEW8c z9pO{eNn@(}(BjNPS126gA0XG3vf_403Qy&W>JDKV}; z+m0ZZ1-io!VPxX6Nf|0HnWOg2ga$lt>BZ*Fne+3F%D*FvRW^)?2v@E8RDXKr z01H`3;q#3wSs-Wj&2&`&yY13FGqeJ*@r<$7WNbQO>El~;do5c8&Pezq*MXwRH)z0) z_mS_9^W_LA00~SgM_;9-!M6NDEC|{niX&<_TTJW^KdN)t0&)7Ga1t=gL=C$nxicL_ zz))^kIG;MFBjmLTZeMVk;i)PpHC-s)!2`rh;8qso7Dup__cOckPp^y`afF@bUxVw<_AiDlD?hhc%Mt~A_(5<0A zzPs+f*SMey>}0e(soPN_CCwzV=ua5(5(90cP2-JPgrjzoaFWb||#H=`F8T7+XpIkjauJ+5K>;|eFhHhkN zQa^%pAP-mDzKCtVEa{H~8oktv^0+?(@%$irU-+}J!HhTn8aDAc?Y{RT5~Elm0jxBJ z?U51rYIJ~bXoys zB)xV=qxkU+LD(bjz!Bq}ot;FDtT=Q9wbcINfAGD9=0^~Pk3tz`l0IXdX?60$h-jl0A?E&3)_3Qsq?hINn1tUD#Nr|yLn9fl0(IH+ht$_80j|&RF+A8 z2-cp+FJ2d;5f7xk#URD6ezIDOK{=Ns@a{!^?-f0?kN0@5PP}Aj2znz+du0Wn6mN3U zxq};%G>S0iDY}!!0pVsTBlX_OO-Oc2;v-^o;;?6ZMb{9>noke|+_TnaKH1m#M3r#=!INQ~ILyj)jX zyNwb#Ydqn;V=ck*tx+L^odg)JwJundi?eGG z^^KPJ_tnoQ_`Cmz&b8nC|KNr8jUu}CT|+niA5|;iUVKe3jX{S18)E|AdAv571j$(} z=r-E)?c>IeujWyaUflW?aMZ#g#jU|8SHLY#B1n_G5b!6CCx0eMzuuOhl=4qAcM8&e)R!a|b1@vWsY}75cT=6`GW{j2u#=HF^ilsH5Z&@Y4H!aQb7Xg35CIQR# zCP19_*4(SR6;C8s4uxFzOS88%YI#8gVgsYF=LeJDjD)ys71cGw1I{y_h|(8l%pJBc zb438`IR~&|zLy#EgLCJ~kj9490?{N*lz%MF%R2VF=lw9)exxJqwq#fv{lyhTS_bK% zvAQv7WrsRCuUy&$__tCFaf^Vtaif%EW&qILk9d+uK`CdE_YSUXuqpn98&)1bRu1X! z+_A_;0*qiS5%Hs+uU}a<4hIy$1a(nzP@LsQ!XQx#^?;Wt3neBo(ufkock`H!+tZ=% z&LC{}hgAq}3%=};`K2UMYjR-hvt>Ipz_N%rr@s90et-$TNwHRn@!g^_X8=u)FO=5P zOFNvfeb&WHe+7R-_R(S&VPrYxI~jIiRUAy{gHT!`+1PaPqyujNLM3{ph|(nIMFX@I z+wbO$MIDfp;~XNB*{gvRQU+k4M;eRs+Fy$x{TyGf{l%X&>S5CGn44SqP5|1DheR!h zM8*X$GQvRmgkkxnlsSr%I{+>k#fVQ52uXF2xRnav*$iq&5KM&gKL9KJU%w6*o|``XFtk;1il0e$P}n0Bt&` z)&|&g0YwLEWikR z@;f?hUf&Nba`QLy5!L89)kU|Nw#Q~JUIoCY8Qx?Z2K-E3VJ0)y9#=f3m!NOUl{-+G^tXcT?+Ak#D7Fadg)`=2vl9j9G5pCm!!tA* zhr>AOsweN>hYzoO7ncc*XI#aQVHzDr23?bryILK|d=}!D8F)0oP6H2xp*)08=2IBb z^kdUwR8T~kYiUV2RgG$r{)tK5xQRh~82+pra0`8p7mUW9N1+@={s(E(NI(aFj3z^e z3gW0jB4=bLYPl;o$6z`G*rAu9pSrU`+*jo@fLg{%YDYf1WCO%+ZljoHLOZO~x;()~ z5vO>>a(3I=n4fd9Uqp^q$>}4^h0;>32TQk&-Qwe->T+D2pEkLBcqqC<9D?yxFNWkY zeO+JLs}#qXSbpTxCFN#@jP_^K((2w9gRWYDj}EVRaP8GVHUwAlu%fN)xj?SH=X1}I zvv(O(Z2PcFZ-Pq@NbfI85zFbS@kE6|vI)FjA6xDh$J6^xL8y1Zf<) zsTQf$_4D+OvXuf_uSeZp8D50h;2nKRE{#$ZMXK)4r5QSj7Ry1on6`_IZCrzEE1BRu z&h>fvlUpaTL`98-PAW;+9h=|zk?zW~LY6dZNR zI_Nz&IdTsMeL2m9Uaiw^j(0v5J7bNTdXi5XVP2kT)np6eG9vd2%cE`C^{#`}v(bbC z`eS@SWZONQ-8^dWk(gToJGSyLRtM_Da1VCBB{ubB?SAe#RY>%RPV+&=+9-?3K=*+P873HM3K>t$y zLGX5s;Oh?9ddun1QbjxV%|Z;zvz~S^_t+p{qhsWST7g_&wSLnBv(f2d*hQ=BHbYD@ zCqAO}o&5l84)Ebona+L4%WlxP!n(DjJ&_l*eo_HPmLKpX;*|5;(uCEm$+>eS3bR7I zw*6qbjS!RvaLd_QL6F{)bxIpu?M?*|m9FTi$c>_Onjcn5mWqg?7z!`X?+T_jD`}5k zlYoBgwE=mCXRwb_VHkO3b6As+Wm_bz`e0kB+NJ!Tr(PgK0TK^86a2cM@9yPLv^9dhr-)o zb{Wk=BnmHI5!%{K=<~0yZt|rCK~7p^8K#BLAx^j)$Me9G$THo5E!-J8C19Cmtx$TQ!9lGMX?q3^n~rfW5FzBp)97Veq0 zuq>vAe2On9$SX(kK3!R~XeqJRFn3mY;<2tvCBEUFT))IzY=_heSI4C+|HCJ)6{YH~ECXI*ui>(jeMT}K)g|HQp%v{2jfj0~Xm zzv{Ia&{X7h%mhy@0B$VDg(207gKYU0I-JqnB>GwLdrMX@3F&Vpn(4mSp7)eVh@{(v z2jYFU?ft#!PL-!$JekV@(1S9ib<+h-r4U;1h7UK?gVQ4!XVgaAciy?+7AUEpjow5t zXRI+ds7zPE&$I7$BG@0|G9Gf5uz1W9r7Io{bG|IWTyY@E!shqb=p9#J`bw7j>}n`P zc13G;a*$b(KF(>V^T;O)nAQ`dMeFOVhUeULqp%+2JIGG$xLxdFRF9~cA5|g3)JNGx z@AT;D45nh2CZY&Go!l9>C9!)_hyDqOf_R3NWAS+3B~WKc4ETNY0P~~73Hug;Wp5Kd z_efMZC%Jd->bT;Y+Q|t)@I5!EI9ybJ3S)tj2YrqI|I%5`(BRL2((Kl~Zssk|5oC%% zzs|05&aZRIvmLMM5apw6NbtEYPWN$n zU4sfN-+qFdQn>y)Gp$~9hOoDXADKGr9841ej+TpHox>3N$GG=x&3F^3SmcSILBmm4 z!cT!0aXZttn?r8ZhZ5QwH^pr^bA8CB5)QJ_SPr?@nrywfMI++Q$@8i2;*`?f9=T9V zG@iVso49kZ_u}iUV?x$((3uB9-tm!xLRdtn4X)DxROtzLrz!53T1-^P*M;gyF2h6;A^C`VqS`*5_xNb%6Ja*qm3^*m!b6P-aYX{I7t5!_ZUFAvvpf^W$P~9#CytGGy zV=A@YtW16QpxaW0C|WrQyYpD?G0zx8+8(Uqo2|UK&9PH^jjMBnnOdZhIWP(iEIV%KhF_uT67t z1Y(z%s$N-rr@?Nrf%BBW<-*#C98vK5mTkcw;*8F`5i(3Io>1$7Jr2z`T*)^wL>%D{ zI1O+V2P>}$>o`bvnr@))0cMvCxnwRlcG_lev}->-YKBrmXy#+`CPa7nfG@z*k1ljl zzvxIL%Prh~4eksy$h_VcOE^J&_xoocBlRD~g~Nb9=z{TRP10j-;&0Z959})i6=u;M zpaolzXV9(}wOF~ipjwn`s1&yY++koOAi(J3X~U^5`Q=P6Zqoi}i)T!DKC)ON3+oPT z(oo+rMa$5nCJl2FLlzNbEn;g5?#0iV<6ugA?6OwgRT9>+ZQq1>T}!+Sd+;gq*Y?XP zZ;x9vv~$YcHr~9^9$e$`aeJn5&;h-mINEu_AIyt_JcJMdj^zd#qRtQA@Sf9>Lz)o| zp(4!^tp@R*Tv@y8o*QSx5`e7}b%~1b1j|jiSx!^WDws_wpF_7(cb`3MYN&Bn12tIlV69*1AyAFy*mTAdgk&PeW#{!H2aie z;+C9<0b^3{RigG5BiZJCeM%Q?LqgX1;IB>j@1ATI4;L7J;#*N!%u85Q@R{p>=;xV{ zm62irsMR`Za|c zChOU+b(sBpK|?mweejwa{!_388RHfnM%BIco*#{dT4AXBvKqo*4{$M|t;i=StV@}K zj>tf>#y^+?Ru=a6YM;-|mjUIIT4TCvB;fYEQTTF5t3u<)m3@ozro65H?w7?cCmzFR$3w@2bK27>X!%GpLbMcW2$yz-XBvyp4_8%_gNkW832+(@uau zdXL`jEsrwPt7)mb25V|dxDBvJ{rLT)x%cy}VCx*%7MdouMjV>C6~%i<_c zufL69rG4zkgv-_S33y7_N|^a#+$GX*?vdbxu5DNHZ(+j^VmcHXq9P>~bhs5Dz59*a zQ6!>}v*{AFSNw;7LpA;`E||yJf8~PTx(*>T7}N18CGIQ)Tkm&JmpS|}s{S~JpG67H z6fdQ{dl-5d)-@8*Se=;>?sJX`a+5j?Ruej*(eXvy9L(a36EEgTAWL75GuRpixmzh< zZO@t$MVcuUyj{>y`gt0Q9cBl)w*i{*lF6dcRlAceSi_r1R6k;#5L?}}1}Xu%VhQoT z_@T_86hDk}I%B}7JT{c{2hrK3RO;)iHrC*QOBsn#FXXaqhq^;yydwbcanaA$H>dJL z*Am;di=Jz)FzE2Q`aFgG48<;7rNOV#Q>Zftmy+N5p=3u%1u>;|%GM2QY#3cQh}-5b zsorhvsPU`ANSw+dx=voyG_U|G#;d{D_jjJn9t11&aaS0FD^FVUpRk7 zy8apBji*<+@v&P;J#A9-1mJUjm6^u?e*0y`01HS`Qhx^iWn1)X^xtVFzurC61s0S# zwrH&^PozmWK!P=B6*Bhmc`$X{60p*rR3|4tDl2B02Fqr z>_Uz>{BOZN|4$`zZLs<`5BM8w97vv(266y^ew=&h?nv3$C@GgMjd?Wa{0x$Hb^#;6 zuPpcW?KN1M^jYlGFM#2AqOl1;%#YnP5Ky97houhzsS*#|L6U{DZ|+Hn_F5OvV+h!2 zAWxgW@)@`KrYOK^LiB2=?EKgfLo-)^JH9B>%zYsNJjzTZ(+d+u6lo+M0y%xI*2M(C z?pMJo!6E>tJexBvC#`)PK;i^^Q8DKWUI1iGq*r|7I+?n*J|L8Z!+F(_1+7Ej8{J?h zu1`+@x&Wtm6VveDPr$Cib!PoidQY6F&_(x2R&oNET}PBn53@_aQ&CZ|-2w$?g=sel zo{bQLJr_mqCfWvo7(>Pq;94+#-`{j+IB>z754hyKEFNJL#OrYJ4zGPwA3L1N3DC!I6u_ z6>^E5Vqu&)?x$#SqBkd+Ec9lQHG#0r0n>0tf#=j-sNHCwFcg51_yx2JK|P|jhr(+E zf|0tg1CRTi1f%(jmTj@;aEIzr1a5*Mi>#({dJR!8b8*jvjrz8>p&{pR?0Hs71LCxr z2>$ON_ypdoFOCO8=ap;X;=`Vs8XES=J)1I;LZHL)Bx@@#I-u$sa<~qDB;zZ}ou*Wz zzVe3Lu)T-tPCX5%(;g+b>cRFrG~(=yH{#^`Q|xpCqv0>T@XtL3`G-n*F9Bz+cz}rW zc#^WDkL_QCsX3e7e?HfAuZKng*zA!zNv&hh#5jj&aCBQg73EAR)xDzJo;#PFII;U? zogeWKsT3mW$V)dE#Q;!n+s}cRGq|OL%29A!0S%xXD~8z~@X=d&O8)v2a|ODkYU8&1 z$!Is5@VU1Ds?{*$UJ^!NV*NtP2*sIDFART}et{mBC2}ZsTW{muMo}pKfiH?;=j1*g z_E<+NbW5(TfV;D?u)-xG8*o)Dg9b!fyy3y~!GNbM6EWk55^cXcWZF9?sXVGHVq;6v zOrfwnJ))CpQM3wn80Dl(css(n6x(SFrl}dwX|?oIfSQ&}wU0=%b=hsgSbP}157CLR za+-QFYL+g%w`4}?XewJg$J3_(0jn4PJwMpHS1pg$JxR5olkOfEmfpdNCgm3+N~df? z0`_I`fXHvu_x@DPzoH%hQ)QqpPST&n)8DNAET|s_Smkbg-C~5$vGwY1?^;j*;fNU} zoexAA`AU*rbJq{3OkH9S%}vNlCXpEkESZyTd}d21{~$0Q_~ul3Ov&6V@f^9C;n)!6yT4= z@9Sfu{Hrw{g0CV4Ov};PB)p&ETcgH1xK8`PrONarL*_3i^%-#G9!r7CMtpc(54ds9 zHNFe_xr85wDZCT@VB=iXK~Ba*y3F#9k_$9I|IH1%;(n`UcrKjF?vB03*MVL>Y7Ah! z8|PvVm6QB^dT{9tVF!ft)9ul0>oD+T(H!KM)}!S~lYez0Kp1`z;6vWZ*K>OMlG=X) zdE35$LNp_T00)w>gQ@^Wj;;IeTvUdjfuiy{hfOsOs(Zu?uLgo;KUqu$Exq(~@GCVr z)GgQIaW(M@9=0d_|6uRUP}p_D=^$}%EUwvcU@8I>YL zrO4hUMGV>3AzRi7G4`>6UTQKABEm4w5ZUFutThFezh%w~(AC{8s&H>i(X) ziUGxbgF~f!vpunQ`1_?ls4;=zgV`HzXQ4F4^g)cD%4@NpGiZqpyoDB)vF#u z)Ey+pnl6MmM@9&@Lc6_;PgRCb&Srw00WMEyfXx3eXAf+GxW5mkaYYB#^d?ECrbuU~ zsE#}i=R_@CVLy3e4#`2Vo&8VmhrpS7*;{XIm+&WeXxZcM2@iFwa@+ix{|$}F;(0Vm z@R!|g-f1~k9PIpL3*-3JvFEdlREL7uu2#x5nTHIqK@Y z^ejzDda|CwMG8$A9xqp!^jvTAX6zqRrV{p*Vj|Gy zDeH%Zl0YnTJ#xpIoZu0Jnvt)d7A-%shvIc!Ko|-F(ym`+@nn}-&uEsXqkHx{+0uc$ zV)J+CtUSjdxc3LMuvQ`}>n`O=M(c_Y{9A5%w`(}OIE8pB7jb(kk?58`GuZFz|8fSb z!P$3Aq*6ehx0jt@2k;4E)R#%SdK9n1LxCG%uEtfu{86EB?6ANCz2{?0snQgXd3wIv z>{61V>@BpwOTChDQHRgoU0e!UrvvpQ8|rYUT~fpA!^lG_FA#%Q-s79^-1ZfD9Bt&WIz&R{Y<1ra-q452rQ}zie0ID?6lFJK za=Qfz_gX7XWy*yWyJ^`+_tN!;r{HeL?(Er$(mTnB4PWrl6ot@NvHS4C2V;7BrPp}4 zxKMItWZ3!XnFl9L7kP%&{BbbfMWZI`o!?^u8^|1@dHl&uJo-O2fA#*)A7XK$4l^Zpj6(RO}!lE?bwo)XWM-Q#Bct%? zOFZ&o%w!cDi;T}Ap2?~IAlFTrv3`iBNYtI3A?bwR9A2k*7cutBs7Nl+s7p|8Or@}0 zcTGQJw63U?Ja2HDT|k(t>6S2czpqq}?E{e;(OX&v!ordlk$8*W#W5gl6i14FWHHU( zV-5mR-^rp+PERNI6?e81GF^e{^iB;?oK@K613kyhJ|nqpQO*b~1ExW}d@G~9XH_xm z>K*51Ln_z=o#Ecsl_x#pp{M2+`M}3(__V9~StIJnEfZwLNsrTF&XISX=$Xu_n>oqe zDj{Iw93URdXe*Z&a?uuS;TYIxvAU57L zPqM~5I}MdAiqt1xJvr(9E;;#2BoXdWzxy&{5N_5wAg?B9dZK8A`xI`5FuT*r{NVUC z2l>z^hbs_cz%3w}W4JrUkgrrg4S}9QnXiz+C4aWsZq`#qPH5#dDK$S9v7!8 zM?qqfe)!cx%8$;a5_dDV0$! z0%Uos`s|rRfe_TxYn9fr5It_!##TZd;qD?rlct%CaV~eu;EN^Q{{M`+0#q+V&tsfh zjPQ)}B3S{;P_~uTA9pV2oWZRPoF*=)q3d9Cr+ySM+^$D z&rZJzIQDphHg@f6V^3w-wTSw!zQCjHED~VoD6bR*2o%QgSINY1+6AHK0cac5Iv)Lq z^Uur6H6F+3pcxOpUgjhIIWyN6n8yA&7-<3mNP}mLS26}sq^i}Dsi5Xh(OJ3D0T2L{ z{4}4{Rty3czFrYof`9s%77eJ&%VE5LyPIKabf;fLf}7b5wLXgjz87-=Zww5lpmZR^ zrjr8aDO*VgGBR2~muGBKd&2VxqYfrdA2MfD7LDDhjKS+hQm$}2L3yRPBd&2)k3KUg zLugbXDAbasdL}e)Q)ayCepq~Kr;GYoX@}L1RXC{=Mx(7mbYA;d#}C_7kf;hdN4F`|kzIPaiy> zgtK>AOjql4a%Vt>J0+E z(BRhu0w-hZe@+6iivo6BcdxvB2o#2hT{8_g(X1)hIxN8CRsDE!GBQz6;yeiAzqsmX zs9%T&F~1=5ycw`_13C&cd64|b*H8g++u_sBeqx% zgs$~XBd%r4>>YoTa%50(+jwF_ofh<~`XY8@G*S}h3U~PsnHap6Gy)r)aC90rjVUa|`D2fWCMu`-m}cK}=XK~Pj*{PPU(gh{ zw4GO3;*Adk<``;(zpq$Qys1C_1}nF5#A#F?W#C>zpv5e;YY2lwl@l?+$6-?uj()hTjB*kr|)T1Id@ka z!V$jifeWK>kRc%Ja7u8*X!O(?gv8=WN=vfc?HQ59@u%r7b-I`)8?Uz|Z$B8AcNgy{ zgib%r+e3K@0+6h#y(`Uivtv$AaV-dJL+WGXi*2^BL4Bg@hnXqxU_xgaGuq)H3fXCYnz^NWR(!i_ z1RoU?SxK(4%lLpLE3V`a-Q|Knp&O;n=`FVp{4ul#<77A_y@=-JdBm&tuqH)BH{K{@ z04!WaZ3>EC$yF~A(fP&Rec~LqTVageQGq`_3zxwJ1?mI?iBQ_(24!r^m`a= zb|r7`^O8Q_?& zPkrnS@{3U-L0M@-7pRg)J7Oe0SM?w3#_~Ut@r8iQ>o@Wag{iGGVDV1)9J00!I)3!Cos+KLmy(GAaVY~ z5IwOd*fn(mpBV4+LA3#B6LzBwEnyz@?%73w|z&xxe}zd_5Tm-6VoOoo=<(Um8i`$u2)y`rULM|-s65)>@h{0)k*9j|2s>rO* z-t}4I7WvvV5-~OvWyg$q$f&bsJQ@fmRXk<*)Mcx8ZK?W@dy!VawYbHK8+dHAc;uX| z>*_C7)rxZl-M9P7_ayzoerR@S9+}JOlLWO?C*mi%P1uFB-qPhgPx!rMx(bPBn0ygZ zrNsCayKA?5gZO<7cr`KOz@>q4Cd**{mrNCzTh9ohNb(k9fYsKF7)- zjqz0Rp?YaASfguwva7G}+e(dFJAa_ed-c-E9fgGg${>Yian(5-P*4~dk&Olh~xPf3<%Uqi-6UDs?43UV{Kmf`opW6+yQ z#4s+;nu&HGZSc)njHx$QLJruf=@lc%{ z;|lY9I2Yl5mT>ho`WZRiVjvGQM6uYUWYp|X^taNwAXbJ7mrv# z3D$cydsneUZR@0flfiQjfO6SDZ0q^5ZnAd^Ux{Z2fh;|{(OtmdEZR{reP(p`))|4l z(8hc51PZ9-$xHFl(@0$EhmI(jVIk8+-^8m%JL~H!j`x^>a-VyT5boS2G{;-Tx}aHx z7+!tLLX6%9ogt6ZSei?rt{9Vx?O~T4^BNKmN8On0aS*nDsWx)+lVRKS9j1OeR~1X2 zR;j=LIxWHUk49tM5)<5knMS_BRQo@pv_1s+W?wYg{uI4zxrx|F(wfV+Qk5$dJ(z-u z#WY$^Va7mdWp4ii1M0sQ5lM~v4=JU zLARfF^0tY`UAJBa84_XM@D5@xC+`_3YydxK3^XU|J9u(^4ya4I26#WA$xc9x>jn&+ z#V<3jj}t3w6^=3E$}xGa@X0&XJb-o^ikH+#K&?1&2b-TieGmm)#o#9Z+2( z#;)8s&`D3`a5(&G_GZCxareJf%w1YxU}raiIi}MLC#$XWVO)a~@SMGi#&$rFzcm(Cc=e^qektX>W1ZZn7C=i0(NTD)@W@zdLd4t*WB59C*hK0YLyd4k4JTuT#~jQq&WGv& zZ(l{)47TnPHZ?w8h{OeQ@osK5(sNjbZ_={- zTVl&|9ibP{{TgQXp5r4UPdK$)A8|USQ+xPeiGC0%ob%|>RKXBwXsCu1M_)ZU9b2|Y z;Q~4lww9|_@FCk!0uM9SrySeHPQ{5K1aTE!IpxDNKVk)~XWd*j;`0@CSN_%p`7wf( z)u13>np)51jk-Ft5lL8pFGnWpk}4Du*PwUmbyv);RyX*~gJv;t`tq{0qPeW#X~y8~ z6$6g3GosQm%^oXOF!vwFe8`U~Ha9&*i-}C$Cb|o0V9e{R3#L^oSeRX@<3st$2M(m9!Mo(w zdAm6J6lP@`doxvv>b6w|A?{~6X4@(_mNW5tdxG5J)SaaeApAa`G#zKg)JW18E!3fi zmv#5Imrp}X)QMpg2him#$92#ajRxdQV>s%xMM2SM7rMT+tG44iDgOGq`f>4(06Vk5 zVJ6ec>Rr#(Csne^$s??po3rLSZuiIQ>1nt+XxAnWdj`AO`1q753&}$dD4anW=21HF z*$wTw{GU(?s+Tyal_4r-9bfa8wW7`@KP6}%*JudxvX^PNf2hK(C2Al}ww{hi?5vO| zrw{rB9;%8vPcZcMw6?J+^NCk7-_g3(^_?b(td?5PA)b86iaj+rRTy2tL3FPi_J9oGlt^M+oepLc_4xke~zf5W?jG;d3SM014pU5%Za&=#WV$*_lH7~?cf*xnaT`; zNshSw9L)%+gA&Ci#>cDt{cCYT$3vHSmiJ--BL&qz^U|&`2n`7*loOZv#i!tE2a+=~ ztWT*|s;KO5Qk6*!!pbROiO2kKR>7pGThujMr=B(ESr%T8+xl?`&#gkee)Y#6gXHth z@c|!ohFJsI&yfYju)6VNOY;_$l)UvgbNh)C1<@6G={*6t@$wCJP?_7zSa0J8qO^|d z;|k{QcMYv^eWy(?GPP51%t}?J)%F)n*Lz=Je(}QRPMn?Y@H7;P!x*YpJuACgUEOLd z1P`6o$6|AaJg#2diC{JA3?g=?M>r#xMKBw*rh0}aI?CWs55*Ex;Epvm@gv3O{H8|U zdHWqJ_zp)%?MHe$I%@T5+$QyLrd(##7WWAq#k8#V$0$N!=I1K9w#jkW{g$%Q5>jFk z1np;qq6+by74-2Wy62%pUBvaIb@u2Xp(>#!yl$OFrCn%Cm2(~2rgLM#nWMAvU#@SW z?yC%nUzVrigE)YTwi^p>K|xe5HY6bMmhY)+5I*be-cxghZ_%i>l*?o<^@!apiGEfC zIY$_E{P}c`e7;c%Q*f2TGXsi!G9nP4p1o`!2c2TB(HVl8JJftjtNVVjbeji%3%ai1 zjiiM7POaWp^2{<{onZBkqZgFf(fv`orOabRzGL87yWnMUIzFtULu(t!yPx%Jcmevc zEj3idUV3PqRi#(Fo&j|F;w6|t(c2-4xrrY}IGwrM5akkv#n?5ZYvfAQJY*}DQbvmv z+zF~lG%bp0YD3)<*LQIz#|*s7Q3qPg@77bVTe}?^zccKlhTW|LgHa=)2IJ1z>4UPQ_Wt#ng;^GTyIbLr9XUmH$ zAuTaUZ@rbIJ``WrSp>&uWSZZ4dbe_0TWRSU zw|H5k5q1!%Uf}SNIqsd?Zk`~){d5?u$ozvpJ-hxR_b;sYezx@P&rCkEg8gbHOYoT8 zNFynAi6eA(q2$mTj1+)(109z(IE2!6i$)PpMm{|f#+Wbb-;<7YscFveFnBOT#WX&e zcs+PoZ+w@OF*0*QKKsVH>Pqzbv-_M7axaH(QeHD7i)zQNN3E8}Hqb^L8 zS@_f#)}_=i{sm&5#T~~gw;GR6PoMX8o6f;>-$Z8zwPW74p>sXPV2R!NGIJHHpna{oTI>1^n*QYEMF3Gz1Lkt z!-`{4MW+L|cm;bUZ15(D%lDusHV}~eRW6$2RWRYoiM4F zNqyi}eqfa(ry2SbGZ0y+B7{Z{Ww$?TVI@Cj8+xz9Qb)(^N3GHMEydYw(G6KZT9OG2GT2U<=rZS zLk*PKS=&mlxZH`_+K5_aynL#lGd3W<${Tu9yMBNj@Y3kkR4&Ke8QUo91xDXH!=K?>j|j%8dp-WvX^x0**2@djpt7iouBab!R= z`hDt~S9k8De40&u)*RG;$xn@#HMHv*GnZEljp)u9pXeN?cfWT?q$ZiDQJlhJX=O_# zGtDK5bSgK>bH6M{kKmKe`3kcYkz(ozV}_>{Q3XiKKF0`$1X>I`{m)* zpNeE#xl&NjNFCi1xNV>O?TT-w7SNv!^40Uf6vprrL0LPy-?A;JugZRW2Vdh%CAzwf zOcZ$!l1I)ZHs#&FuZJyAJrFh_FAt>^Mz2^gVn`NQvUEw)-Aj8nOG-?5y{EITIwJ_o zh>Z_sdOjUzc-q^Sk*h^ynx3||XI={KM&d`!LEVL#X*3p_=(Jw68EB4kQyuu2Cw`$< zH)?WA5mYgJ^uaN&LI6|^1_lu%w>Vl5XwB2e$X4eVTKktT_ogATJ{P%okmtvVMc_^4sOv{lB3#9{o4V(>Nn3yqe2)UoH( zjM;B^8tUn7TT&F*gi+c*RY%Do5KPckF##Vv0+kW#-eSPw0Ed&dR~)8tF<8>7oE^G) zWbEE;Gq80SS(>E!aUcVT^w{0P+?a(|HSQ9pk^EenG3zr4HY zacv~>Gi!EaWTxT1$RLq+>{tyRM<`ALy1pN`{I>P$_s4$!`!M4d2i@e^ zY0=RqWej3=YLSKfF#8W=^u;V|jE{e|6Ay(Pe9{o|;OQfJz~?Gk;Ex&f0z#^pXFegx zvk`#pCtBLuZx)F`A;}XSmt8)H%ceR=%9=WIRtvoeot9f?b#x7vr@1n$FPvdNJE?pC z5~si3&IvO&%#UyE=ufD+x%VX*C_znkZ2~kFlZ8S;m02KYz12(Ty5t^6kg`wF2EWi} z%<Ze8olC4&e zGl~mM?@e083gwIbHii~xwM_HlpYO;R#C>-J^isUq)?1)x4Y4z-U7)Bst6Sm@D$+IC zHDvYbaG}|V4YIe%z$)tuX-R4DuA~o|dRW*iAOGP^sOQcp0mt{dkB6^>m?aAOUGx+C zevOeQL;>zz3jcvLzrOx?vPJDDwSIo{>+`<>*nhqI$BFOC-@G6{FS+tlrT+QFuh0LQ ztSW)HUbT2mVT_dd3s|6;BjkZrkRnUk_v`WBPb@rswruQx)5=v@=1|1U&P8Kxpcn+# zzGy;n!-+XJlqCPu_tLPe`r)fYN_;U}5!`{{GKj6jG8z5vtn$}wy)A2ga8{cw$KC!t z1wLN+fIkh#ezEr$p0o7OUsrxR^X=a8Lvp5JXF`KtYXNhW7%u_UM-{rtEE>DOosP!8 zP7IZJipGm=?3nzI?_ag@b&ehI%%N~Z>n9Qx==-EMEiiVH8*3hSocZP-a@iL#oKjs4 zJXz0u*kGrhRq3|AMN-AlaS-_VVx-m8-v`WG;n$sSoh-I&j*Pm#8MyMc9v2+jC0pTf z)<@|Aa3v-x-SZcj)7i<()2SH{V`Su&P6|y*6P4L`d`^?p!RDE$+nclHju~KoXg3z5 ziz&EfZbO5kr~JzqCnWtcV|Ju@#1_M{9#wPMzG&<<2(b=_qVip3Qh)~&31@kh5rEt6 zS{nV=*4aPaV&QE$GqzX(4wv~c$llE)0-<>TINYqgR2CKL`!8A2RPyCK+|FhH98vHY zjF0;*F^wXT@9gATB&#`tiI0{on|V1?lDuAs0fu_NCf=y;ee!nsaQmN@Gc)@CqG4Lt z-fzrU(aQYc&6`78KE8IL-%$3)HUE!knRDCl-vCB)o51&9RGjBNZ=p-yDe_+v8FQ(+ zFiJlt@|VRnmqZJUx4?LR=ZOUn|4xnnjQ8X3bmN^b%%$%XSpf0xbXb^6-|4V0m%h)*Q$&Tn7&EgXpLu1tx7l|!dyoKuL z$-RZCUb)D~|6st+$@2Hl1<&zo3sA5C1q)EH z00j$B04T5k1q)E{KMV>icx?IC;VbcfHJWW^H7m1VeurX-1|jBFXWYEueqf^LECl>5 z#4j#b(hKVIg3SEi%S?J(fyuC9DywgRHZmgjpz-6S*DH2>NKiQOd~cIxr2YE*?FmQT zta{h?ru^RCN2^3E%l4-Kc}{AnoXfCW8-d#_VZB+0))0Jfs`l%&icZyin}hNm=DoDJ zwJ~^7V)MV&U-^9GUrPln-1I}hE)ZgY5I-pJ-$IDAm!RgzYbb{tx&jLJjn-q~I2@#V zO#gvc{kbI+zPInQ#LuEi|7#Sz%U(_XHWMnilJbaa^G{a;c6kk36Q~2udH);h{ z4mhhSD}R#1cX!7kTCGtiXlz`ZoGEp*CHV`H(xhr?YWkXhjkk?(a&Slxl{bH)_-M7D zV6z~UHMvzAnv<6oX^ue_Wn0Z(PHK3!0{rS3{$^}L!-v{!?~5D65d7Wp^75i`YAPxx zyuH1zZSIdbNzf2qeG(eKdHUrVwoiM8Ew(9I$?0E;3Q!P_d|Ov8xs#cm{>aRHc{X~q zzh7$8OThuMa!T#U$TjELfvV7;+R)HY(R;%L%dR)g*XBb^P+ulV8tl~XpZ_OG}B-$SIp-#HABb92=-XM_;= zjt*UiK!r`!kmF#A`YU;Ng<>w9j*ncrZ4l z+DLz+Z(v}Tk`sb3a>&@-AbIT783jdde?<@&)_Pz^t&WHxthC1HaF2gCx$@x+JXl@6uf(jI^z2t$aia}dj z+jo0~?qCWF-xndnZXjY25$7M2Np0G+=^c((|Jy#f-=DO{0Nih}Pi$>O&IUiHjoDDZ zd=BqAX#4+d9(91aFLr6^>1S4^8%T$J2`7U}(QH7FR!jPY&UP@yb6fcR|E^UEaO1F2 zi-%_GkI;jHf-;lZ8rs|2KUycO@i9?gHM#r!ON!G(xR}QK>TjIuDl{vtdxY7BLgP$w z?bRg`6{(xSt(^HyvRpNow8h@+?Cii=EEe0G6cQ5hp;l@M%F8SAP4kFMt(2eS)>*(2 zneUUH6^kKUo%Q;sIc#P-zP`S`{m`XB)fBC+b2PRXG1p5XuX>Xq`E=^8dHdE4R17=e z^bycGIXSuYa_GQ392nOqv-QllQ_}5&&ZZYiS=KZSJW9mh-@n3PFk7@rsF5AhMaQ)} z1-#{`N)2x9|ElofaEz9??Sy*2dEh`>d;3Kq`D;0Ov!o;zl3+DPV_s7he=|Nlo+W7& zY$k4?AKX&k%k(L%8X%9g2XJAcFX%j$Ll%rFRJa8X1S)vbCc0&qxpI}V6T~){dQkF` zQ7e1`6XD^Zl(mD_wPz#Ey233mfGg#kV(5jMmx4ZW<-?aBNQuV;pi+D^@c?qUhx{@&))wfRe3cqt4w(#z+~c;y%@6bfsNINiC*tuVrl6S>(3};utKS(PaG6gOtlI=?lsa;gk&>bG1VHZ zP$-OYkF(jRY3Namor?xRDnK%xZl*3TM21<{S>h8EuF3XFykK`BrG_wIesoNBd*H1j zD%d~k2F)3))8j$>@Ac%KLqz)~tdFiywUu}wf3%NqdD{AkCLKq^CQ1cz>Xq@{DIBS1@2gGaY%6f5_yQFh4iZdN=yNF(}4+FId9x#=9h#B)Flv6DWU??_4 z7k8lC$!K%N?Dn-PLd8y6Bi#xTq31T~l5)}t3Zmz<=KH^Ir2*??W@MZ*E!I87nw;EM zFb01+3uf~Q+GWhhi$E_4}0GN_H7gGY8=TC0%?(;iQL*{xThv4KpzAg!A# z_i+?8TMG*>#u*9?h+BPW%`+7pkmv)Y$#NXp6@u0vu1N)CYmyVh5W#MnXdWX`6<+YF zdE4`a2Tf}mznY^X%!+lltI;~6Ukshioc;QBdsZGP&yMTVL6j#lW@bO920S0OG)5R=&@G`+2@3-Vh@O|v^nC#kpZ$~G0Z;~ zC6!;Cv@96K9M7>^@xF>08WPgvprWpBdXfdzoFQMcgzP%nOt3`iMS;mwEf-{G0(U3A zJ@7_>n!H=j^Ngy_d~fH2P^}lY?(UyVPv5>|6T6Zo zW;>Dd53M?=Q=evYp^p%tr}RKe=|O3gN08-=P_W}h1@!WxXyJVG${Wft2wlVbjc6_H zd@IbW#+{qS;^NPh*AV^sw>>T%YpZf1`V9|w4&-igj}vK}$wf=S%{>Zou1Pe7(z%rT zOZt@ua$Um&%cmrf^cT6~UVacr!WwUJEg#FGWlcs7N|ot1(Oqjr4r-Sm9~~v#%C}rO zs?D(ykBGctm6{ov3rS$kDYNu_h|A{=y$WiR&dG12PqJxX zXX$s_Q;Cg@XOfafyAqbGc|Sf_eB%?Y5k@n^dxEP zd0D*I%5vt$3aQnxA}@$Q5T7^N6==5n5RJ*ci`Sv6guF;SyHhns@N7qCXLQAI#5!j( z3BxVUZJ3?)y52uE^;q&rL~5Dd@#e-;HNx zhReHPqK&MxYfN^+({mM*{e}sE=k`aQIxE6tGT&WC2{pnlXqN-<#aY@THe1#it!ng7 zB4k{D?3mdFntYG+Wp*)Bz~jV^g)YwO_Pb+?=*Ohl*kkUpR%)OUEc` zGF*$y<(a$8!laYp;-zspGlf;|x?#F|TwGi}Ofr>+kQmI3i_wkD(}&*8uR+HPzw$RZ zyIe0mHa0<3*O|c6EQJB)+R8s(p@%=O+R#LW+B%!vE4zIC$WN7m6RF5dkpmT&zj{uXR##idl+Ewuw4DHVRllO z!~9R)r2x;}ai{dTSg`!v9<5!Yrb=H-Rqv5KO<4{t!qE>YV8Ld~i{-macor1<*_!-2HSd5Z_O$I;BZuXMU{!F$KI zDj&b(Sp_J2?b>LjT3lbRr@&acJ=JyOQWfPjPzCM*tKs|(=@+?$g{HY<@L=S@jn+BN zH*Y=}{ZwRC2%EPf;WX>Tj`cT`>y|`pV~F3HTvjRHWcXIaV`Vx?`5s`8yBIYM4Ludj zM!}PEau5Ps&WR&l@pUzUKp0^&Y1dR^8#q08msJv^KLpsqot)5gW?5O;U3`^yKciqb zXLBF(C<$osrg`yA%!G+6zg4LJ)+Qg_UqYQpfIzPZe z=i18$O~|3nc}+hH71;LPR_#ngj&W)pn9nzukal6N12a^vj5Uh95z?5U))7bUfveVkqXMr+j$y_HyGKxUbm``J+IB1 zrVg0JnvH}O>=C^pcQn<*-Tmyj&d$yim{9r*(8KholIcXC;Cm|fU{Ekq&T!)^ZS(S$ zcql3^4uXKo|1qOfv0Y3hta^LF9vUQ&fN1nvpD>Bor6@TNxv45iKz-hk-6;ZizxCkC z&9`_aQDx`uU4M=N9*4}Pu6^84k@zC0--|UA?bHM`W%y2A?69YLgB*Z(Q#yNM!jY(%njLy!s?us1x7T_H3N= zW_o(spjfuzWyGrfGA)s<4B&C8NT?~q*tQ<)pdQhxs;c(!$gz3JY;g$4p4wFZA<_GMGZeR0 zq3a=&H^D>}i-xGE%noyf667j!tkgccxoORi?JKqmsi~<+N+Yg*2z4tT))L3|_4Uc% zu`(sO8}4KEZVMe@H>vFjukyauSg-@U+j1taS}ut?FH0THk{7;_ETwN2_$fX<9yP-~ zdWNv((GZyX;fKUmiy|eRM~PcUqpMuaVrpOJ9;ETQ9$1UVpyKImz~^Kgz^Mol9U1S; z85yjN$RNBt5rFL0CB+;SG5;!E8t@kJv`ejTcz8IB)Q<@BuG*{CK7YOf7eI!`7R@OY zTv?sa-B9CcspNZ6C(n!!tM)~n`apczaEFQN0*v+>ENVLs6|BMA*B`AvtUt3(QI11b@ zb;qjFOJAK#AHHyh-t{2Kf}0t)9PM@O%*)SbthwJ%@$!zvOxe04j2PnODXiJ4WkOi3h8xPK z1domsIV1Xmy0(kyHMjw1Ngs_2i8OFuGZ0mPlbV`W^(+|$f-vOC$IlTF5gAuq71#c3nbjlU&1uIkYpkMRb|GPYdom4ox3fe&Bd=|+Y(HjQ*?mwO-B;|;1yf)jAzOe#a0H^0{cdI zm)h2HQ#9_RVD;65m*@!o;Q>=u93qmSk%K0Csx#FDd+}>&Lt2}R<{KSfCcyBCPpz$n z?ic-D7#J92VC)WjQUAvL3asiYkJ+(e?6Pd0F&ekx?sR&RG26^RHgTq_@({MjBWWT+ z8gt*{WDK#Q;^=jF@O*FJ<9W6`u2@-Z+w?T_xU1Zk!c%MWw~Hb}wjg&Mhls5d7BuQY zB@BCXq4M_4egMAGbGWZ@&DP@L;;-Fg!E|PZ5|EFq7n9S`xmXuw;7+!b17={}7eQ%v46gDeKCIaN|e8qheu8>3wTRMMn`vNT?UR@Zv(vT1)7)K z+nz6;>cvluQP+fTH%-SyE5EAwMoGCs$tt7eL-S(j=U~8XFP4=mrRU|f)0N!D#>Ng_ z!hrEkeWsr{;<^r3wALil(?|QO(Yi;|irKEBNfVTSB=p9yQ1Waio)hj^dqdei6DhY7 zrZds~cvD8UOy|6PqIcZ~)3u|c<9a41U$b5>r*kHkbaq#^L_ zgU3}@*8>!IQ3p}bzbtcg&W6Nd2R7KUgy)GsG)Ntjq+LEyKFnp{FmFa~MnwgDhWib7 z2EN7LUG~nXTT4erXQa~!<8Tm?n3y;Mc8+_XNp*N&6-Lu9{~BHR$}x~^H-+>%PFg=- zcYXaqUHA#(F!~het59%i!Fa&ADu&=uai?nF7TmKeZpD4**N5=U&U;}+@C$k7{J3p< zB*~n!H#I6{rDk~U7JJizeMoup!ndXbI^9zdA~X*6S~1Sm4>k$b(;l|!&ZR|payf|W zwZKwOp}2c^G$%zBnivY;=N;wAck&`P5~aR~-y-ccF)^`wP*O!DmZ#&^mBS*uu3~OF z3M~sYcrQwX!e_3oUyGtmm;nPRdEGYDr~ZyAl?f6_6jxAKcnTY*5zQ{ZldzT7U0q$N z++_(>&ni^SLbs`H-aG!F*o6>=4ptk-6Q5C4%mlE?EN5BRm@naJct;@%87Za1gfHlsjifr=(lQ}9W zYHcqhh^zvEq;BpEorE{jef-!b8ySjPT%Z&0Un5y1LQL6B6$ukPZ^rajY@AnWmS{~L zT|9A~KYm-Qsde{@p@2G^N=Se%^q|nIQ0F&eU?1~SKtf;JEH%Udc6Flv?zjg^PCf6( zfd)hZ-m2s-4i7EL)Yrh8hSX%@N*^ne{FVRZRR~}M(hgtXQ`f8qCR8`Y8CY2+Dj{xk z3Jzvh0gN66h=M*?7kQ^A^fDByC5r7+y*;DQgN9+^9-!2%c6@Vxm-VIP7Vh4H*KR4W z?*13Uz$X=ey2RV%r;YB$be>)3H$h{9sieSR;<<9iVP&-u@;vvuVMr=i*Qu8xPOm|9 z3egJI5GH|7Ax9VowjmFunYI*I0Lwf3Nsp{^oU@O^eM0IC^4;=*d3h^M9Kie1B&o@q zb+6DJ2GP!=1#3!)cqDW0_{4;Cjb$)!Wu1D0l(-m*t@nN-8@5P07*Z)y`L3S4;lZ&A zd>pU@c`FY0lqby5IX5o0s9oET(92S#&MO=b>vBm@uFR1?xEvZrMExd^TqATkU8~Dm zf*xWF*Un#h=g26HnSn7Ac#H1u@3&Mm#8nZ2p^%XtNS+05D;ukIA{%`|2Hb!Po9EJt z>F-%i#?j|xNH=z$AnN$%58Qyp=4b!1JIdjL^5KhjxIT~OeF7ifFrd!-Kl!fyt^EJb z3x~HX-1LnW3xxQ=5(|V_AjD5}`2M-T5epo#z!CqXzye47lO78g@jsm-e8JZ1-F%%F z*14C#7}`g!yY!{I9pl$doCQk{&2LnWE~xtmP5% zu(oQ}1*bP{fsioeb2~EwRkijQif@4U-D~hY2YDRXV1dVVt>`<~SHyilOd~a@(BiyZ zom@!OBqi6!)9JJhJ9HMc8q!W+iGHI|@RrN}R%>_L@qf{M`AU`v|65+bzT1BPR_}M= zRljEG0s()Kf^UH>e$irqEq;+=fi3`)0=Jf5T74w-GstJ-&gdt2wOHot+$vpGuqb^4&3(c7dA`Lm9nO6RCrFE)L1&HDzB0L4)M= z=lZthkHe7%=CLToWTPDsx%R9cEzRLY;g9^9&|Jz>>v4sp>%iShM2=yl)*Qoj0a0P` zuG)UE6Un-g$I%IWN^(NIw2x=KTTxkZ+r+KJwMIc6X|BSEh%;&xM;=xgj}Y9$>%m_~ zNsk-3_5OZjZR#~_ih6I};8=@nEzulq;ZY+%Qus2499%EeFqI3%s%wTK39z0+=&t=ePSIn;iGd~c`3hz8{5VBm+oC!&Y8&hYKg!*Q5pgVRa|r&c7u zyz5rfR4PfF1@CH$-3MQPYae__9~fLGOW&AC%5B2X9v&w0t7 zkf0p>Cu9?MibOnEOY|ZMSXmD)!tUkU;~=QPIY0YR&^;W@ntajYN<pyo! z_pL~`?%ZY#I!cw`mI^FYIs!T zi`XUV@A@cL^>1YHZqDLzZp^akMVd87a1ztEKad~zuLU>$?Xb8!_Kbe*V!a%Wi3 zEbnC@%`bg%$rPnWWVM|9#jKZk&{_I9YgV=yPrEWU1)7tu_13L313Bv6eZ7Kj9|2n z8Q!IT9b=J1)Xdd(h1rZD&I?(iC(SU*N6-ygeuP9@wCR;8a$-%kB=jPc#!7$!u3Z#|9URpGfZ?gw6TDD&q(|{pV($6lcEnET!y5E z;olym=;Zy{1A1h}&CtX*VLM4rl-n8v7^M3{@+k=Q{2W>j5mJ}Nk99+5*U9hX3l4!H zMs5``@lJXW9`o-}Kat8;^4J}q+cz%gf&wmbVI-Ry^Mxnj^vs6ei`blkaL%_=2V~|1 zfbN5iu??bg}rB=tU15X#$#EfK{S+m0Z?_vSRA8Rmi?BHm-cHShWy0j&64IAK; zBZGCXV5e70x~j~XVSH_v6ChACJkyjd=~g;F>MzsxuY3H zRsKa|pIllyvAaroG6s`mYpF~UNyD5=4&up$G!&0tz`=Y6@b}b;mrwvjQ#~S;QHPEA zdy*teKbe9S{Kap!&Srq3qHKaFhRw4&Ay^ao=xDr+XDJz4d@AgejfBj ztk752UZcUcPuNA@we&t0F)B`IpPc3OR1W|R?}i`89*0DBrCoIA|DxNIs5GQoa?>b=L;VMF1(-n{tv(5|4_~HJWbd7RyWI~XS+C=zq0~7 zd4@P43g^0OV8Gdi9>R**r0li~?PFksbjmV9q$SMAF7>E2RDc=VJkll3ZXHU0ILb^OiQ! zUn>SZ>*BVxap)j)ZTV}X7`X8lBY#VL8{c;!c19-U%9^4V;xzTT90l67L&dHcT?WLG zkN5Kd?^i`Ay?&{AlS-PsVPjPw90;5b1!L8X2t+^qS}w0<75z=6j_&5yFqRw z;6J0FuF3)uZvOQ&?0kEh zz3FnOM^m)lgR5|vM3{kF&A=Vqs=3yUrkGGXUDDswYof-&F8D0c?@v5)LJ(_N> zGVj=Iyq?@>$@u?m!5nj%q7Ze#?arOTTDr$DF8a&{H>a{szy6-)0z0Qs;jMG?3aW{h zx4zNE{{VnN4Cc=KkMfQ7qmE{w`v0+CF8u+V6m9|Ad6`4{`6o~U#z?;YA7gHM*hvb39@l_GM`FY>U3?YsGE{9By@(7CwPM}AV1O{ zwhEC9LG*Wf-m|UaHi;j37cxw*CBwNDFR~|wA}r(X$gU;mS@T2G%pqKC=s`$>wQH|POzIK z>fhA0yYnnuCN5QolDlP|6+K^6!ESdpRV|)mRI*cT^iz{4*&qD^oYD@Fmt7#zGH+>d@%-*>xUyhHY@4x@!K|$!KH;J5m6eLL@v5-{OTiM*? zMrhF)rQ2lWwOT<__ws7+?c1+wWq)~b&B>9yRS4DJ7~?nQGRQrn!_UmdN79B$A>L$W zr``bjO$o0DhK0We*9)k@IVY~+IZFu5YmDge5Q-VCOgx}Lsn(?K3?U)e)IioCcx5Sr z0>7LZl-H6%zDKvzH58jYNzY-MBZbKWuL#HWe9)%rnR#8f~(`;ohj#*NsopIdlN z?ns~jaG40Q%saa!Y&t8z)b;^MCMJpXmq*JDOLnzDa-qCl-yz|lm0 zc|Ns}p)op!AFHbjA@T%86p{K=s4Hvu*I%ptFhs*Xnn=Zsy%i>*NA3T*wy!1pw<=!pK0m5u~V{FN^4@Ivz1{UHsPB zU+oMV`7D&CTmk)1np$*qsEL(MNuMGTRA~NjqWjuOscCc$3(k=Jq_??}_*8y2cai2S z;)xk_5zjRL?p#&Se9GoXl8?{Ot;KzrdAu~nS`TA>bCb~Wg5xLn#$0$3L-`)L4%hPD z#ecZ?^Q1Yq^ye_UqhKkzcaw3TGuSvdCFET%MNy4y8T9Xxv@CW6*;>^hA3FF_nij^XV+vx40l&Q=gZ-Dv<8x)n zvPGM0gsKyGrqY1~9TKb#7J^Nn5Sr~W8?h9vbR9=Vk5}GRJ@(EiFmB$r%@bN%OlYca z5H@n5NeP3EPKQ!VSH4{U_m`ylRXU-!Po`t6B+COR#b~lc`+jtY%MI^hOf7;n?B6Iq zRZFX`ag^oDv-}wPZ+9&xehSpZ=)j2ocU>$RS?{o{!+9j@Yr`^8XN4d$ZoOm#E_g=F zr;DfEH*TZ-M0|HvQgYDpZqdm2yS%(yXWqcqeTHs)zFLDSeu0JAU%b0e-@WdXE3 zSiDL2q8esVC%iuafsDpqO2%<2{U!>{92Cbnb#MQT_qnn~rKyryyvy_f?O`>;y8CSl zoXT-&S-7;_Ap7{B#Ri&WE-#!y;?jGtd}DkRd1RN3A)IH!xY7ylyiyO%Us%zHkbH)x zOxI?o{OFq4yThgqnBk@gFF(n3HRa$;ObNT@=NnA}3IyfIi?;hlBC*FS%=EwPxhz>P z+PGmYy|rsRB|jbD4G41~lw{UuV8ak1Z4ij|IU{JzmG%zc$RNydbNAne=YA2%BpiAt zSY$$AzZlX$vutW!mB?1kof!(244%sB8p)~dwx2dDa93B5l;H_GZ+hp2we5Ah2v(+T|mWb1&Fc!rAQLvcNe^ z|0R&4N8zKyLY#m0As^>MgH|$9%8r#~TgzsdJ}lC{j@N6MgY7!B{ja&_l8g7ymXE;| z$$?$V&`$Q2;($?y=+P5T{7z#Dn-a#rI?n_-pS{;>&fNazSxrPPWkHJ6ykj$9 z2+F-@H?VcDkRE3IUK6PNEdCcu-@G&8e?n${;dPa+1U&+J_QBL)T~~*kdz)_!TL1s) z&-Qr7w(U&6v8h4z1}G65Xg}f^t1tBXWvX%$BpTm6=t)HG%;i zQP<;YT`5^4NY9v0k4$ua+<7~jWEUzq$)8w{Pyd4-PU>Bo$Z*3`r@tt=`^e>5QgmvC4k(le=Z^VWBnU2oJ!-Q zCrY>o>GFx4^0+V0{a*F(Pj#H#X1uw-mVN)nc=&LR>Gs?bADuwF5^njYdq%L1Nv4JC z=t2NKE3D2xD>=`GwfHB(0hV=BAmY+*+<2z2VceCfy~yT$j5iHv$qhXn{g)8ZMq=?Es9_|OfcWkKo! zv)IgTw@^|Hd%bE|rOQ5)pTSCrqS8iafMqZvBD_utfvL@zF>=7D$W+}c@5bwfDy|8t zL~DHsU4bNcTcTfKFa70w7WGbM%CdDi0CyBt%RM<2mtOdfrzHQ|w z&M2vt_I5WL^Q>Xqz>;RMo>ztn&HfNP19~V@h}3B%+N0MaC*y6VQ8!%LW8MYkv|4njzxnDS!*deQvI4ic3XiMU+9bqV2j=H!`@@ z)-SXKsD=p*$8WgMRotV*WQ+5nye>-ATW^V|Ua~nbsr*#= zwp7Yn8?qIz+aIT5KAB>^xi5fj@v7|!D1QW%wH!<}6pwN3WN7WmbWg3&&5l$e;Em7LIMlohkENRo!Yf5Ho%`WzLm zb)T|MSXlAA73}Yu|7y(h586T@es53#ADJ`xYb2e(_+E9+;9~DPM}IogKEKW=Qrd3T z9d(?9>5Blu=U516ccG!^4Nv%Kel^IyMmm#_V`#Mfgo{Rh{%C?Nuz{w4`etwYxl*!O z8PWfTo7=qUzhGL-)Xc9^)q&UTXJZuGM0N_pnib*l+WUojG9p zSRvSc$Ap9SJ%uw}Q6}H@X&n}EbD5{Bkd6Md-a2MJZ4EQ(m*H5S1&AAi|3CX(Ic3XW2eM2J&u(O)Aw);b_T&`|gH(m@q); z_mWre+0-oR?JHhe*um)4OsE5YfB>%UIpJU7%*Hr2E=A9o92sjcunB3bXPeWSAoo6R z?y)F>Yj6R`fZK0x2p!I`uVVwot34&`%D{nwX}?tGFY}pQZIrf=W$e%g49B}d8Dgm_ z3YPuB6?=iuHCJFeX|%s~)+53mO{ z6mv*mj6>+G6*y!H9wi)r&?(|ekm|UZ1Z$_I`y&%?1zt(1mE&PftJOCiOGmb+z8vKH ze{i9i`>e7hdru1b@oc?kwo~chS1Ua?&ID^@$B2q(4z@zQq+B)T4bX)TuX_Jd`p`@2 zqQhV1o|)pc7lQdkK+WaI6SPpxfv4t0H;B*^y{ySy@aLG0fIQnJ zccWbg9W((D51`Hfa=|_mWuS9-CAvm2DTh#BQ74zHc&l=Ae_=PWSsXGoZ8xwo_^@p> zb|OKchfvWSNLr*$-(FhBjUK(QVBU^0_(U$C>!@1d>$XwzOEFhgSqx#BzhKdaHRO4c zZEU|{mu6kU+1!G;n()pOC=!9o&0wdU4M3)jPuQIL`9tUN{ut?rtL>XXQsFe0Ec=nA zS(C`9FIG3Ld7UfPB>`~L`n9Lz(chpy%C{T@Y3Y9sk;LY(EEFEv-@ln||H-{&V&{&` zilv(yJL^hsknpSd20y&3OFE{rUZ54|4I_(U#fAxcTx z;W28Xe1Za-<%*~Jk5cwEc?*rN5gO8=5j0UL3=?d)T;V}L(w2kCSaJRok@XfAzWwkr zp~e>7P^eiGGTNWWzA3y(O3SGqi{|wVPzwcr)i9K`2yElKKt$MTv+_Cg!Lq^)!sq9R zyn6N+8B6}-EQEG|MqZ+uenZ=rdXTP&Lx@|57m_IT(Mkc_Mgdi3k;;#hjNrCI&U|*HNFp&)&MPoR=5Ryj3jkGq}Bi+cOsE0Ax7B#I@xQ&?d;&={a4tzLMT4jbm4sF0p|D!Yz zzWKy15ftql0ot->`qZWTb8JSPI>`9hR93gC;Gfawi+#$^$P#Q9N(}$rbY5^&k|rk& z>iP?^^E^pq5dJR8!0KY}hH%b+q}Dtc-erDTGWHkwxd`L^ZPm87*f{Ho7nr%jIIp;1 z%{ls)s-I44w9eidWl3-G^oRV3Y4zh9rdz*7hW0`#L+3I+E(+?Fu3pd>QmoTNm~$Bo zk-S9wyGMC@px|rBwop$!)v z;wAF|IdFbe>pH*?*p6wt)%MG-IMv^esJZ&>LiTrwkN8tkYF6y@!|$8q^k4vWFQ@0O zFcZ49k9nZqJ%ChP=chBpIrq+UTi3antjPl&ogt8B2lJzZtiUy2wa!42vE;x{0iNPp zpjdQNSTcN$^}!=lxY5;M{f9 zJ8dMZN=KYqW-2`G8dcNHT--dAgiX`g!AP_|Gy^~>%Z95Ty}V$zKxL<=eA2_$v!BQt z3Nj)EkDOyJ@Y%Qf@{)kg(%{8NYSyxDkx_$s>l0uf?7&5}~Er4vCZB5=CG=KcUa#>b%D zd+>BG@*}3Uv7UPgu@wKF-ICu0D+^HkQ1+PvUC$ug3WaB(GJ$NZs3JgdiL>PkZ0|4B zxuft_ycc-lgaRQ{1_noNWit|8SCwpuy+_etXvO#JX0D8{J3QX{T}2+mO4VK_<^6i4Pp3!X$_Q=6A9 zHe40nkXh#k?kw4NC=cH2ke&ulz%uXt)!V+bdWacl&Mjirvf)pnoZQI){`E@68wNuV zdm_Nbn$;WyaegW1FDN57;GKTLXrVPXo&;UM~ZKtsa~8`<{kG(~{SaRli5=AAVX=I#Kv&)qHngyT>bQ!5sE zINI2WgW>*fdgmvgS(l=80uVrj|MTe7+Xhdo-o*Bt0)ucmP$vZ%y9VSTVB*FHQ`5h^ zgt1|BV}vo!hL9ZlQ|MCkTwcXBAu=a|r4g8Vv$i1}*? z&=p+4J(y9i(iABi=C*NDjbH9#$h`1g8>rp zc%uiV$);K|I+q&uaR(IOTS=K0_ec(_Mb+v9D9ewER7;d8^JdHI2dkaTMw9l21|Xqm zVfIrz#yv!sI(502I$8z0uflgJpht4XwQR>(6{8CZZ*d(T{_K(bCpY^a5n(warE1@6 zy7G}SZB|^Ya&i3oq0^j;x7?M~9lp6+e$u0L2zI{C{9cRPaZBk=w*}j1PQYR#D)Nx` z?|Ud&IIQ@h^VaghcSi4C=l*6vtUGms@S2oQAqS@ll<8M$>ou8qC~t0Eytd9+OV&_S zgb)Np0yOuF)en+Ux-8@k-4#0OAG^4P@2J$1e^jqhS3B3lO&SalQ>zvhkht2`fnZ1h zlnTYODaeq7CbFv%5e7RrMe%)UUC?ie$Z;>MIaeBx7yZhMkoY;PP4rSh?L+8kA2!jJ zP)iFi!2D4JnH%8l;q3RQCY-3LXT60^?{td-#)%5)=i3~Wy5~7vx8sW_^{Dm$plY~B zK-iP~^W-~G438j^^0nlS22uezC`RU)+*mK8H?Q8Llnh{;KXAY9H8bA4YHZf7jvl+x zHB~#pW@Lg!fbKn_rJ-1l(W5fskw8&5(?2Oyqj*`fEP^e4hT_!eqogO`pYP~bxV(hz zT=2Z#h4in3P@XLny4&)mxx$c;L_}cyRLsG<`d1*1svv`ueWRQ?s zpJ$7=Tw3GdWo!O+uci+8{8UhJrYW7ENIT@+ThpGCYxZhh&_E1^5|~yi8r=IA;G`{| zm~}on!I}1$O77mmqBEp05df6Ebe;C>WK*_`Cg(n-z}JEbo)C8(kR$1)9-C~i;eQLr zP~TK^W}`456*0%Gwr%&wbJOek4Itax^4FnOy|B6~RgtF`gvOOPh&lW9b%WoOe#@Ni zy_okk_zT~VWj z*xen$d9>-BiTTa^thux+BF@7JpH@ylf1bRStt`&}EqU`eedCYaFO^#d3n4j$V0zd` zK$Zaby>^ZN+Z5CY96?;utlyhMmi&Ycp|y?V-JZohO)1K9&GY|7s4%oYrtX_~`v0U; z81YiN&D!z@=%l&mVM#4t@u5>@xPEbCsM0@be~fs>{Zh>~?M71h(1cpb_h#e>nie9y zafb5fIi}R-A$^C0TEkwdNM476RUJkNCDudA7 zd#XtTC`k(;SXR|4$3luW%lnT3@TlrkKALkBeT+ZrS}5{FTMbDr+6X;B{LUtI)L11+ zqm)nhnd8O&D8#C&EGu?}z=%n8H3ySUykV}#sU1=ykLLMlBf|> z-F{qr9?KXXRvt`R%Wsr;N!~Db+!>eZZRT0`?JYCHCafxkpUR81Sap<`Su$^#%Q{J7wnEN_sC)+4(81hY z>MWgfMp_a=5+D>_nt+)BaA@orr%nrii0uM!5=*w#8%Et#;(SR)M^inab`xE=%ySZ$ zK+~Xywlxt;b9Do54l`+QT#(3 zK(5{81mNn$X)guKbOlhZk?*ajzv9@$sWm{Z5!iO!6*$l3!%B9}=IhlmoFtb0J{wmu zKy6iQ!I3A*uwmV!Ca*n4lgH}hmf;}B!|4@jjVSbY8V2WVB;dvv8 zc7pn~rj@UTrosXLR)>Z334MeU$s5p340v~8qbCn)&N?<6pD_ktW_#MYg6N<0g@sXr zPkA{hi^#RN>Q5hQ-Z zMY7j;F3OU*@zGG;eZ3P$J#@Uf(3imN8m3(V0djKwsQ-ibF_XA1sMAT4z5+x}sV=op z8*D|1`C80>Xw8}g`l0s62kYG?BV~~9$V2lb5I0_Q&DAcos=R$` znG03cO6MGChr8JTp*6di4cd0x4|PUbyw4&M7)PA(aM18xxWP3-{Efm~n(id0k@R9JY3VpAj3%r5BVU6vh_!nJ~7wLQ_=EIm{%Ya1}+I#fcd32t|?3KCaS}P8jso|BL0u#LUwS+WF z2aRbKFo6pRNS|D?!&x=gLWvk^Y7QV~=?bu^!~t}UfZ1{k@2WbUFUN_mD#V314Ta(@ zw1;kY#9v)i&)*@jP!>!YuU-P>4VK`;{t^}?mFB`IV6NG5Pmfr(&B6nflajosDPs?j z7WW)Ig)gw!TS=18Nc4H<8m}|dV3J>!&04F?d}->!@_G;4V=+q#DJD9l?-h)hPBBM1 z>jCx*>s$CpprXr8-yutTQByo|kS2agT6x+Jtra#fts70>`<7$d0yFCN;Mpg(Tq}(K z#x%X#{|T8_3*aXWgb1;g1n*lQ00h!qsp}##z;;li7)t*k#42;)&k4nrG~Rk=a0NKI zo2bOy2jS1ltX+^B`x1)aAuE+#g%2}eO>>>ET4oZ8P{NkAkH+^4r3lDDsJX))iP3KL zfh77z@t9^!W4$RRRCGv=DgBv{aFbibi0rDecxo0MZ%a zUzO1c^67C7-ow&s%SLz&_dWC{?Y{tCZ#Ww3?*pxy>-3NP98{Og6TE%Aic3Y{R$gtC z>z@BFI=5*q6p+6ft^vTAJVf@U{Xa?Ah(i(jweopW>G#TO;@=$Jqi^f0`1^T?(r<4d z+f2Sh)-2TQv-kvO<|iPA?}?G`ksBqi)s{3B{&d)Oi!Sht`<8GVZ(8>GACt%BUWC>7 zKFSYHRoBZ~EoFmZV9sXf##6@o2X3$xRD*s@jaPwu?-YdH|ML5VlMm7rk8cqcly4S6 z?Wj7h3fQOl@#@1LSE&Yjx6TSh(yBjN2HSV72>0^L1^wswBb^dae(IG$^$SPeJSsV# z-o;Zq{#=d+cVi0KboCexOlStk)8IMDjeE~CdiD2Ao2_;AB{_|(UQ;sSCKWA`F*S8P z?O%GI?1eb%e6APb?tfV8LZP8N3hc9v!m|1HRwHI%IbrY(*P`Qux@-V zIKvA$8p^suGwE(H@US^p&;&1Sjqa%`9X#&yi~S_7zhFI)0T?X%(wiU;oy^7>oX19i zi;!wlt+}O|Y_su3R+vbvYB$oJQ7|D_tmdZI4}rh&a8f32 zJib!s!34&}hqjEcm<=Bwq_1J^8m?GCI=bvtxTTmpHD=o0W0y1_Umg(h4$IGcPsaUJ zgdF*ohL|FoGjj`Il@ftlcT+Slhu=^Jd?{IH(B^^QsY%m`G8e1}%cfTM1aPw=XeG3* zqvS@(D$q|oTo>x!lHaasFq+3+rA+StH*qAIK`mqA*)1sqjF{N@G5mA2MsL0%s&2=p zJ-crwG-`bs+5G@ujLZiNmgo@^sA-Lc@qBr$@s(-gM>kxii2tzzfd#`7EQ!UMMG-EZ!j$^cWi!LE6(W z-#TMB`kjc^yaHHEV@B!xAE}LE5b4+E%`vOup^W`*O;_?)!{&t^sTjHDaFf-MeH3XY;<;5a4(q;6p*-iQ{Lj3o zxgtm6TdBgYU{xUGMZTYa6_i)0`{h!!M0M(De&aih zVTN*35Bi$jLE7gJ_S)ZYIOK@;_Qr9rN&axIo1k9u6LmS^+1($~=*e1T0FDX2c{+OI z+-~@_Sax2sMDi7oo$0d11!&^b<&VpgjHSBxi%bRw;0h&Z=xSn0%1rL2p^nyy`Io%5 z22%h%AFGHSE>0cju43zHww9&Blj73@?A~!IsvF|dD7@s+PF5iVz5RpuBkeMJ9FW3d zgvPeCxH}MdSL96+CN!rg_rxT&+Rw5G-n$qiyU@}dK)vCO4+O-mK_1D_1eqbJGx}dn^;EDi^TV)$n6*%Gz-Yap3V7vKRu@=lpQ6GO&B3je z&j3UeYU)8!8UVEOdP?gSV}UAN(IG)|vBJj{8vA+c!#JSa0#A?p3{Zil<9fpK?g>f{ zYMjMJfIh;U{WD>;9tO9Z9V7|7FFPRdtWaLr=K7USfFgYNTd<{I;OS?;8WT9+O5o(7 z6kczgJ^=BAimNxI4sDS8$lR&tuEBq*$lC;ukqp`20E>sd0isu3xsRTQ|Fv~>uIQ^M z;@8K{jkGVxjQ_6^M}5-BqkWnA-+$Tu40bPgbdDRi2UGKW2g#E2$+!1aeCr2oi~_Q& zMg#@<#F`{F8ITHhrsNrkg3Ia()Y0?hJ*G>ZD6ztu7(>sL_0QCMDq19ow^i_`vq5sRc`W(Ia+_z0Z9Op%xThg43Pnf)24$ z=SB35>b#|V7jY15zuA1~1*YZd$L~7GUg&{rYsPpleQ86eh%?;M(#ehNjT-0}h+W|k zo6V%X9-V!~$-zr@^(0kKfYamFewMuP2Q&MOH~ScVvEZv?QdaZDwY9KRUR5p z!e=Ga(=7_ZWh)cOFbtuI5BN__o_{sERy%0IpRJXts={T^ z<$QH?AijzQs2e(_7cCj9z2m`qQGvz5gvOe^$>oM|YoqiK5_c4k5*%4qlKYvDYPJxF zq*AxbZOZxB(DFrKjRG1cax2_+ri@nu$E>T(83Gs))?QcaX|rhWNyjz4DluPtd!iH2 zQL@HmuZjfpA7-(@S~u@s&l}CL`z%Mk7H8=Ver8056)GcSZQus*Zoh{JKT2n;7f4}HGy>z-4UXv5vduk#v@YR6AZPKs5b+g41yD{bggu^|AUAD^& z%Xv%N{@vd9ec-q&r$y24N-OB|zSI(p?Ouofu&`Zp*FhB9&X3C0MFWjf)2cyU@A6cg z@@TV#c(8thCHY;FSM`2{GAGHL#jMblRppmw%m3DN_)3G?WWK3eZK&5?O-&f}7~_M?2f~874YXGKl;9vrow&*!iI+uTOZ;RxIXZ5P`fwvV0-7f3O=!t^JVMo zn+9*txl4FBlh667K%PX#Ty;2RG6t_zdUbzIsZ<5unCj|M)IpL-O!_3 z9uW3f-l0xtJUwOJ+-7f(zbhqQ@Lob%{6}dDVCAkRnF~Hud{@rd2ZNeB0r~3uFks-w z8}0@CHeP-JLEsI*N&UU;I-M$-4V7yX$xYr(l@j9AgC}f1<^)fMI~s#k2Hz@C7yok2 z7&626*4>*vr= zK+@&cE)k}8Jb6+u=9}c@m4xjHYh;t(4wph2X;TbDJG+tMA_W9|_F2iH9wqzKbOAHZ zlwS{vVMH*TtfpB(mel^c!h3h>R`$fY$g7lO?0!{jpMtj7i5H=SM=*|YvvOOnn1e{<9Q>@LK%4CJbIfj%PhI0a2h7==7@nez$(c1@HO2L^{8PG;JgO@FoHVaeoLCH zQ>*B#H$}zaK1EBXpLidUtdtTvzf3s4cm}h2tLLZCTDGAbK+4BUx+rpUKnn9uZkbVi zl)EsUh5&^7g4kcG&AD3B@rF!(=*uO~!!u2xOdzarf!x<8TS=CU&f ze=gje3lwO98_SIgt_e#j{kqb1(ySkh6uEeT5ygl=Zb0mKz|AHOV;m5`U`k<)C(7-p zhs1Q{B3LqCo)eJAaU-vI%qaZ6%YzC|-B#6psqKcSFzILA<-LKFZ>Q!v7d^$;GS$BZ z%U>+HFb5F;0PWY*PtNd;3N(IoP9y-rrW5RStVKREsHxq%3Zyu@dyo-Iv(B50vqk{| zRY!C#mtQ+&DILflHR^4pzZ67o#c1PPEB*k^TUViVFHA1|m+(h2C$T7}iHMjZKkc-$ z*MzMtKN_EKxcG4qCao#K40JRyhhZ` z;=jwL^l`=ral=9+;03}=LuzEf@GdmDt1amW*J>K~f`Q}HI-k1;A2d3zQ%u;`K!0p^ z>AolJOK)AzjCDC&CQ>P4KRuax-@mf=EA`!hSGb%ek;-Pdm%7X|Y{N|G8cR3FFzy+k zFYn3dtG8`Yx7$L@uYYm+auOKH5?<834?d&D$7{!rq5yKp zdcg_?=I58|W4))Rfy|J`OnVhUv-&yg(S&U+>4L5dbmheS9HkoZ-2wjK1z%H5YX5iZ zyf3s?NmFvKPFf$F&E(Zhi)kGFr-1;YpG{>bk-_;vET5($^&BXPqP7(%=7-B8JB)p3 zd+{tAY+o)o;9a+prZs8xAC^{YqF6owZ)4I_oGAXb`wWr4E-&}`Zm3jCtHcZ3p5;gZ z>I^za&>S$7%;l2}C8GOXf4Q3aA_|b|$Tjlv{)&`#*Vu3H?r7h+%)JrOUqd7eD5EBg zQ4Nq4TMh7rYQkV)jcD`aXi_zn>o@yG33sy5Nue7uZzQVXuRw4$iL#+7;~IqBm>Gcn zi8!?UB;M+9zyw6bE_t;~_dP;$f(VM?O@p~nJtsqv5z=RY`8hsg>oKrdI``icb2Ho5 zkMFyw|ErT!M9O{Yv|rc$FQq%qkC%)dFco^77Q$Pet6EqyQkE~SAZv1#t|i~hkUs|M zM!$+}3!plAdnBIbrRANTsTz3sQXGE)^wQdASOMQ#3dX0f6b(MTr1bKXlAWw~=I3La z@7=F`=7P-Umhspy98tl^xfzMdi+>Lqv|u4A#jjWM^lnp%Iz zoYlS6b!`RO%KdZn_7ARTD@5G40JXR-{m0sWK7MZsq}ITKZ&bCx|3uPH}4__@#D|RF;Nn5tDd*e0o7eQArI@ROm zvUQQ)i_t?LBY7Ew-0JT2n%N!5Kz_&H^|o?+z5Fwq_7*^>5^MA0QM)U}4+Dg$;BOhn z2_(*(|HfD!u+4VkN3WB_K3&f=RK`4jqW{skoIAnCp|~H?y`ZGK_V0yt0kZKDu01(T z+`Zzgd3XUkVp#4UVz|8cg8Z=ZVUFvpY+7*+hg{7Ct!6$2Kl1^`Z*=7g)ChTS%n5&T9yI`Rcm3HSF<(>vi?exs zsDzx{f~-==wMG7kYBJ0`r5?e^Dwj7_b?B~(3Oo4Y%f<8G;Llov;(kZTAvRTLIRrk+ zIBa)L<&o*d&HML1^ElVK0j_j-!}QNHM^lVWDF6ApR@}4AUpDK7n7>5txb=J3mnU-8 z2SU|*H7|zeqLu4~CsDZc6{rMxqpX@jtrnIw7B}G^zoRBsp8%dk=;=`hyf|u~<+kX= zn&t6vy;A9X>6_z(H!_**5C5LEa5sr;&E}D}{LlGgx97K=(lif$`_uSK>eG?R0A~*B z@e8nYLv*a8zLb%0I;?16|fA=4=Hzvuw?o&TXyhb^AB(I{UGb8pS@Du{$a@3M- z3iW9d&ucE*)jRTV<&)ssIPJu|jaL!#r$2(HCQf;U{H%0qu;xaKOWt#?UFYxpk&ysKBmKu2}9o&#T6>;T~>v6ParbBvtt#8sQ7txI$J{+??w zRXuG54&8dx-^Kv!AAP+eITim;I+@e%ixD2rh4%{ska!Oy2R@9>hl7pM#ci3&~vkn#E zLse8{Jn}wp&f;$4mLFy}628Kl+|>qhHT>~Ly4$qRTAHsxloRMOAJmBSXP1wdMB6XW zv`E1u9g+O7$L$`MOgjI3Ap6OX*FGyu+0zJot@f9bJ~vJt?=hHTXEu9$Tv!g9y)oKn zxl0(#e0$rlLfnW_8z*0u-=6kR;bzW`vvLGg%QRu7HSods8I^4}Do-1Wlog#k()hZ{ zLtydC;ljt8UhdE+P#JQtGCzFZsaZ!t*=y|F{pCKYX*J3krfN4X6{9Y1egTb+I1{(+ zI1=u5^DX@rSwA>?3-UHx?|}DYs*|Eo`vyW!=VNR6{kyDHcHGb0`MUPj&CbU^%2(4* z7T(0~B(>Qsr!5i;8%}k9s7`K80QcmpHQ*M7zfqZ*<42db+VwqtjsArsNhsuG``QvM z>kXasov-sDm7_L>qsEVH>#U4(^lsjQ$5m(zYRCjB_-gibjh{3GaNk(=TjpxS(i343 z_yU-#{~H2s?Fq!6oMma?#f5M>Mutxa50kj%j7GS0SyIO_s;{PFm&=2tTP#I>JfzAyt1K4Z7M{^9j|7QX}Yg@K)j zUtp4@hu415-}qW3^0#-(U@eIx;PHEb4Mxk?zZD*%e^h)B693f_qx5T3o$XNjr)9mZ zE|MRl7P47>)E$lDx9$5lMA7t6 z`&vC{$;wWA#f9XR4(PThN-}=GTBx_lDJM4SsA^7oQeg-!D4u=yo&5bBXx$;OX|axA)$FAj2>j zVLI=@rxs?*BPuV=7AL5~Nt&DHo6@rO^&swnM z8~u~lZ$LA|BAV`QM-t_hr8*Aq1~fMvd7A} zVqXSS*kDNGlp5R>>Xv!#ZJLdLm-FPatKWoiU*$k-sv5Or#J-2^_<(Iy*lojh`|-Y3 z!$xHKOtW@)DLZfEVj@#B+PyZgt3WTNI&lF)1tX zc=p|UF%Rcd5Hk2|bZ;$Oo*^l~_)*RIv)pG5GRqjb#ZssDC&cnxY1BAFfBc+pga$5eXg zQ>_g|vg2StHsBDwc`>Ev(8+@qts@+ny0W^IFv`O5Ul`gy_Wqv-@3o3;inKae3SYsi z$Y4Jg0%SYSGgh5_?k>86gBeLRWo*tq;|~|HG?4bCU%hT=6T-ldyntW{iBZNAon%T z>%7iuaNq9w$9`MQUv26#fAP&7VF$a8ANy>nFy&Irz6aHLh~)&aLe3JEgs{2GS_oBn)10 zJbSOI!uthsc$E?OG*yy6tNYV?H>grSs`)Niad)N#`nI+`4o_csc_N0w9&|h7k2+8o zJ@Q6ZtNXlz{N(G0Ymu0R+=Od)N@l)jJtX-R{_->Cvz*Sfr6I>C#*>lqIv@K|-Ez;0 znh4fJ-K1#Nza6r8kt=HG7{Ar{Io)6pZCGbZc7f}!an8g5_$?EeH1Ejhtdz@Ddp?ImhptoLXy-CI3ep=$lq ze7S@%lhkQ`r`!Dhc?QlxW1*KcOv*X+#Ve8W-XEZkp8`!)~yk{Z=0vQIPi;Te`t>%h9IZ4fuenSzX=3Fb9QSSARxI?9{viI~? z?{CQk_L)OiYH1JKp!h=VUyx(}B6fl;BB{umCl|#vgzzZc*Q%bs_}#c*<<$*=$gCBU zn=5=Jw3e`Uw3;-5yIZv(^_t8E&lXnPXD6qdq^Y;&uK;ygKA==0XpqOafbkM8Ms6G4 zIApVvr%`tuEf|@<#A)YVj{0CSpU{H49RLiG6i&z?bGZB>r>|(8(MRshxPp-7JfBCGjBb|~2Wm)q0EDxJO z4mT>TdzzC03!@|^$NL|}{-!{a=%N^I63OdY3A zS){J;?}=|y8lJ{&q93yD?k~bydQy&kY0UiaHTOC{O*AWcmnrTKquMa@;wINWa$^Rc z`yZ|EPtvaOPL5x|EnMkiBUkV?UvQ{r7$}OquP})#f9EOVz&9*i6-PfQE`B;0RWo_{ zX;cA?3F?PLv_j|jzgC_px0P$1#SA`TR@U$_I1cp>iZH}B^75-rG3K*mz8q2h*lM>YSTp}l@2#Z zSac!mp2tDC-<3KNo^&&q0V?D_ats_(<9MX5-V9*qoIq^HS@Z66haE4fD|Mpql?($R zB3hHJb9^-p$Q`F+ddXv}+g3P3JEjtB>_F@=vk1hwBfDjYP4J>isWijnq!4QH_T_iG zX-1R@`fYLTa&_|sR5PaqRXjuJoH2$4uSPEeZ({8y%Hei4p?_@SDD6JXDD>p1Il1t~ z!k&KZ>r8Y<*pr&&eJXD#Pc~2>V+$wx^c3aYHB26uXu$T?CyZnH-i2{DFr3gir3mX4 z{)RS(Um;HJqgcL*8iRrtb@w>OSk{l4Nd|sO2hoGHO&Db=$9gsm zO{J$bD4vu>x^P+-x4)@rCadADcZ0j?JE zo^z+DiyYCK`6rAnhCVn_?bq%=r^ceBM?NDN^U6+{+sI?jcIH&j+UtBUo_iTX9A zv-Z={rwHMoF6(%FAMNV!-M%i54$daVm&r+;w9nKNOuC3*KOijZT3JQ(Fl=V*<1;>I z#;w((p`8^4IbC06DhgV05cvvfk~|IBZJBh!#gh1dg$`cUtfbWoJ9km@?%hN$AWf;% zS%|b&kKRq0?1?$ukk#3~0slu^hT-~|l`Xp3bf?}>h5d5g`mN|b6l;UIrR?&pZoO96 z_*a-eSs<;5!4aSi#qd?!JY5ue1dRRDeYmLmQ~XbI63uSrlEo*eT*3`za3qsdy{Atp zRQ2~(N14n#=Mq*AX1{1R0gGb{!H)r~H124noK#0r`L~5Qx9^i4>pZ`lQ!gT#Bo5Tc zxmV8w6~OKho5yi{>SaNP4D)ISTH#BVx5it-+nq?9OuxkB1TIT%rPV2Q4uxJXGUatp z{ptO1e8S!=wcIV}1NW5qAWH+e(N#5T@`4R!{o<=%o(zJ6d+@5$^qe#*!{DnOd{L8y zMXi?zEtZV2374=&6OS7ckyM?sdnv_0iwC5g(QWs<`ZCoFHCR(zw-o-`S*!rLObQ`{`rt+Q zi%9PvU$WfmHnd#SxY9?><3&%tbQm3YR`mnDH2%!0QS9ekRZGPs@g9`?!BR^J|6KOQ8cl-M@sY!zm3V=9Jv6Yld!w&e@5` zuI>EMj=$0pNpYH2@O>*VB-*tOIh4+rEB4OL@Y8bgPjg!g56awhZuH%jq3y7YN;uZ? zqc2VE>SZZAUqQa7lUJ?by;6T^Y{?v;JAIVRQ?4=~GbwBYy86q+1_!lPQ}NwPOAIi` zL%D_;9t!a2JnccM<`C=fZ!#_#m*2xh+{l;bMfPOi>0L~SLQ0=|Hdl#lNk@QFZk%`6 z%7*P=dP?pQzh|cVXq`JOD9N>6Ct}0UqZJ2Sncf7>)`8(+{wL#a@LTy;twVw+<)y$< z3HMzVbte1-85kN%>_fRbgMhQp(`Pf@nvJ}!2EiYMMgfxD$?kp9gdfxS`#OK9`Vn?X zd*aP#+OV==KNmK}tHAFCU4B?7OSzH^@qp$dgw(*&ep+Ce1&;G2gjF*5LQ zb>!zoRAt-{NY(j~-exLOKIS^oaX8{J73ms~Es1W)rO<1eCW2qDT&1sZ4bfd^s*5e? zFRDu1m;X{*JwoH3b%Sb$`uSz@MWw^pFGqHLLI56eyRJ zTeZiCeAV1a7)Z%;BbA+ctr^-i11&6LFhl0>RQ220Ovrg(^}e91-qDUk-;$1iM-{(*Gk$;85%ap zkBw=VjH#+<&CvWiQW}4ecHm1cpRSGX(~vxTj9d4gP~%?aq39x)uj81=bzZkW@B1Q6 z+r(yA>#anmCLxKv;(h)apgbF7&v7%qPz})w2sf_*RF~vUGPK-nk+d2A@;KkVSJ&m_ z;fp%W#3iBmN9M`R>2Zr(<3v$nfLW|#asPoIKEP)%s4?U$4653W9&{6EBaed_ zX7tfVx_c~r%Ld_-5H1c5+Ovh4stb`J%0`Oewx@uE-F2@x0E5VmJsVv9*-))Qf;0wBNw zk@Py*?t{o1K;IeG+=!tIaVOysO{3*#=3s`jhtPikq5%f}WCm(3Y?)vbWZNQ0CzX=% zC_q@>T6x9#28upocg}J)UB0VVs)}KB+}6QS2jO7 zQtXOUK!;n<$i$vfJyR8=LC_w))2Q6HFN9L6E!bOd%{ck7$L;&r_)X$D9GEoO{V^~H znlU4*6ARH8^o{D%l=Q;sfBSLKl9E?)d1-u~&GWdUbOzS@_WM3i?^{zc9EblS%_g02 zB{*s$Xxez#MqQYF{o?aovx|*~Md*=K#erFe(xGRYtD(OShD1|V5SG=Wy;3(ZnF=KY z`?357^9CO2TbIEmZ$iwNvldXQpC(D9rV|C1wI8d}Xgq;MOwNnm-HGo4TuINaJS*1z zPlL#rvG#?%iBYARzJJjA$U`mipcg`1m4HpwukO}upxA&~iER?lnOqg;4hNL{mRF?% z_uPuN{=U?NB044bj8qoN%R_``-pI|;i58gM^%DkRAN|Ax}uKG;{ zP&S>ZMLI3KoDAdidSh~PHq2Zk_N@LS+@ABA4q;gyRv@mHyT-jo`5jD$HCH)cmw9L4 z>L_GU>a%ZusjGCiEK}(hoG0l4zGe6P^}hm3c&0lqwMU?CbEZMG_8wDe14TtTHs$R^ z@dWb(xhjP%;UzpZjL#KOTO1y*opKmfrGl!OR_T-0!<9PW+nnZ3_Nvhn@6*x{+Xa=+ z?COR>0|~cxqx}wI_B0?SDxC95{nOmE=Aj9~`Zf+%9#f(}v0NW~whZqFlsP?XfT%RK z=gr;ebWm=U;_5pmj=FyC_va5F2A9~de|ax^Jc=ei3qDkRB!+TxH=kcg#@LI6c%Zf6 z3MYo+<+q`)MACyX5gXj6!II#vqx`%Ml z(9jKhQk5RmIH~-+Nq~gH@N@k@b@pL5z zsFu9a8exTWeDXuKC4RfpCAS}mt5U}Oi>j+8vJ!jja(d6;FCFIKKWFKtbC}iyDJ0LJ zslwhLJjqa>$LK(fFw#qihuQ!QlYY?IE0AnR z9=!d~e8fCu${en@C^zsB?l^Rvd0d=YdC=GnL%XtZ!;yhbM&kh-$p7WeOOf;@sJqo9 zpYPF`1J_C+S25y2iKd_H^7TsYZXe{Xj!ISXuv*WQYSQcdO&=JVtSVkpUK+@u-Aawx zc!8Q%q-49{A2FObYLcmY(7>91lDo)x4(yyozyMJ%E5^^FrO%MInDnESvl0N`G^a7e1F z@^B+h&x10fqfpVW%Knoezrzht(``UNl~A@n(%fhV3UYd%W|BRXuZCH#y96pwFv`^^511Mh>W5r7E@$pmrT;RhT}?_7 zv!ix9&98M+{+KMFapL+$d55jVgnp~O3FqsKoQ~-FMu}93PfPk{1n(zzAZZAx4wzh z0w0|j1wntZ{G|}8v;FYE%>vR}E>GVtz~4o_ab-h? zM5-{*LAVL@N4L#F42S$(`3~xb?soK{4Q=Q%;oy_8yPoNOr-vP~Xh6Y0E|L(5uP-bL zh&8To+l_|7`N|W#+VnlQ5=Mk1kCs~NfU^vzUQ2=tlxK|@2y$&{4cd0=7dELo45niQFT&M)~M*nh|cGB#AG5RSL=d zoT5i557sO?NY(a@clL7OAN}gJ4G-Qy2)ohr9@<9ZcAgGgSc5muIN42X_rDKTPdE#r zznUt%+s~~W^g9UiW8GI=Qz|yMxX9zXzl$+9PGYULWvU9RNqY`L*=Cgd7YqKYFLTAa zZ*xEHRQL!aRNL@TTL=0T4Fg=cYuEg^b3~mXW6=}C#+#%!4HxZBu}&_==m%8Kqgss( zqO^oUo1q@iRVnDJa5m4qQQH+9kaS~}ww)gC4Abd_v)tC|f@+9>+Mt!4ZLx;+*ZlAv z%#pSkg+2Yex$VJ=5tdsi+*|ke7!s|wL?Lcr9?7zLUG`-=B99_!BD4ZZIUdcU^ z;v=T~{0tXe#+c?F#!nCr0R6A;dDc!ZKrEqm?sF^j5-|{B;ad|{&*VGa?ArT)4#6$? zvjr2eLGa_1Fs0+^8lt~fGhIu+W|YrdXM!tR&%CVI$DQA7h<*sX`=(#M-Ar@w&-riJ ziY79)y{TRrUvr)rzObI3@}2U2##CqKvpd+WwVbdID(|q}nm6s)?v5m66^_e5_Igz6 zw)Vw_uhGAS1m1r3_1;KG5y<@QRu1%NGo2~ReACaE;jFVtc5PZcT4@N|r{qyxIQa0t zzx{AXw&5@KOO5v71KO*x!I)g$Yn<|z8?&h%IZv?!YB5re|HMa^5>Kp}U=@*PUH=bF zK5);xL8dg-(XK;$?TuQWnvF{?4onii$whH~GWLEMLsgUThG~UMnT*`_i4Qzi1p;%` zIsNWxSl0-wp=LQx|8d(m-3xI@F7Yvjl3iF|Sws5?R8Vhc(M4ymnJVPtE}W%_3K|Tj zeYRDULY1I#X{fC0kh_K=X2QFItE_4WX7eRo=rwd&SB|>xb9yVeVBfDDO5kD@jH4lY z7OXz6RRnRGAnR3fZ~`YV_{+txAi3O6m$Eee@ht_%Fyg!;F>=Nj;oFv5?9_?&j6Bqx z2Cv$A`GdcAQHADNG$x2bt&o}K7h0A1w`aUT-$!Ti^tQQ8L3M{ahMTRtcRCyI3o>Mi z3y+yn^jpW-*b77!8iX_ZsgE*hF0h~G9{T{Q^0w5+H2odIT-046rprSM&6$!Y+kiZW zo4@Sfz-nGuXf*jIPZjS$qqNB5@*PgE#f-w!?M|yho$9L6O6JGyV>G!ZDpxfHLC`hSPs0TgdbH=}G(jsf z{wc?97fbizGy<)iFgjL{(9X?45MQIf8I0-09^7%X)r(ydN%`LfN7Yv%(H8U1ZHZ9M zNSZzQ?$}bpyUsjY3bZLSz_fsS-q!I%dQ0LAivlV+x@Y1>>8&HbGE~A)2J|~#E}~n? zb5_;C3@5Zm=^YjpF%wy_Qz7xtW~O$$e{90mrQNSK!cGtKS#KWIw%m@>Pd<>`macoT z^%Uf-8xKP2?Wk+oL&W59WJr0>Ezwk~xi+|9-T-@>GA z|J@s|eE$uL+WteDg+8rP`V1fQ5`HZ|V1LwR>9hMPb#Dazwq|`=#a51Zd8^U~bEoWn z`^#@zoGc)ic*I5xKBikdK7sJUb4GjP<#uCUIPs;Datr(Ms?N#o|G*^4iq3i`l_Xjw z8>b$SM`_C(@wMzY1LK;lzkN6sj<>7-q>Ny^-Cq6qV3_dh#SD*Wki@w!I zE>16Iv0ZItE0Ey3dO`kP&${cnA^76RLDxbPB_)JIVB7DN!fEdxG3a!6k@#DwGr&r@ zB9VU5Pk`wG7m%>0ol`|iRaomqnyZU@`W=T@tY310qcg>IDeQSpGBt7CjxEDo5<4da z&kmtm`r5tZwenZM1~($%;fhpUiA8M7IZu7KAouP!rdfB=*{t-#d_-)3OU{!cbh`O%C z)>{x$uIgO|B}$NWW#`%40|1!6m{3XYM{Z$N?xQ)FOc@f@-}P~3u8l-A2x=Hu!Fmmh zg*1iDMILcQ`gt3?cLS=@Pzo;*P;*uM9XU5P)T!n@1y}|b!iu0up;G9;eGl1(RWFvxn`!nW~X)4;xxO zrROYRb4gLr7Zm>Tt$kMce_+g6skSarZOt3}k`Y@WBP|}f__{Ck8<0I&%A_x06oyNy zf=5_qdJPtuA9m|L4Nqq!c}&Ct=%I4=)ssAfdz^3@b)m`Nk*rtt;jJrwWS$}6hufF^+p8a~JfPuz_}@&KG7p_m+O1a@aZTPau*L&dEp;rWRjUDX6x zv531q+Z_Vap&Ne0bSxR6v}J-e?k~o zXWo=A^isc!7h z`P1dRh^a`eyFfu2L`q3u^ade3-7L{ueOonV{FnlP+0Z&)0wC~wPn4KG`vExsOOetOgt*<5Ip z(!_bRj?x?>7}N*o3cR%uU?16kZd0(Yl3ayKZd+>#oI7=j^+OPTBGK5L19R%3~GF>B7p)bgM&MJ05DNG+~7j(`{}0K z8RAH@a_wDewIW9XXfZBNR3VM3XZvDjf)UNIn7vex0viC=v(uCZwfgk5j5}O>o$USN z@jc(N?@%UcW3IF8R)hc~$48abKsqy#WoW_ko-@vU7mF1yU`l7M)$^x5fI*oZpkAsqPZi{sVmqzeBZG{^uV9t7b)nqAKt-WfE)%a00_E%S-;oCD=;oefy2gD7u(hbVj zOPJLAQihK7d;KtJ^K9}uZ;A~jCaic+EBz7p^_+u1wsx(PnJ#!C6OvC>d>;^5EKj;c z2Prs$2mGo}AQYDOK1~ONHbKUQmpg41X0Y+%5qyM0?d#isYbH=eI^h-8FDl0V>lOb4 zl>ZklRL!SY9ycAo3Ajbg-$1nuGlxTHxQuXw0H8Ke5MX*D)mCLsr&3+5{=G*3L1`gV z)9{1^X|h4uxHJDcT`d$6)^y6S+qICm^{P94hN^gRQaeDbv#$}bV(g1}kWzuWw+am)z8Zx?fh-fIB=;Osg!V1U zXBB$5a7yAQ=fC5njFdW3p?qo0D##JBKLJ~3>(_~QCJ^bX{&Z?Gkq8g_4eRbxlm|~d1k#vPCj>% zX}IjYupX<-Ttm8She`J({7zQ9Gd`|8GE+~<>A`Ir)qY7H#7;{`7piaWQ~AP3WLwCH z9n8q%26}4>$WZL=7_wVa&yVqZz-bxW3N6wzL%%Obg97J~-mA=$Eq4=rfjc;(ymeg1 zQ?hjfTqkKF46Q=+f0|Egp6$v9m!N4RFf+qc~oXySt}xkW(k>-r?sB1ZU$Fb z;K}GLkXa7FXAuf$|H+)Yec1yS78N9qnk#Ju*}{^A2*sK547PLF8(ZYpvd+*O)vImK zN9(Wkfh`o6r)#)Q7S-`>c{hv~#?>N0)_81`B=6Xxn`*-s3FJ|JQv7cZ3r&9BeU^9C zL_0kSX%$wTCizYY)!_-w`+NimB{bZDK2HXa;jeRD$Fx=Pf+>fwl1B4zsE8(^q#cZ$ zOD&rd44Qj#c*mB;^8~MSYPIJDfG;`yQ77SP_TTD&vUVICqMh^-c{Ka&uCv8Z@tv?9 zp4r0?XNS|qvj$p+b*D^-G# znsT*_Q!AGH-s%+ZA{vuTiP1L4twRjJj9FkOc2UNtFn%Mb@|uzw7p+Kf)|aFr>($i9 zB1y5PS>mb7&4x!iK`9Y^?ZZ2A2gzQdR^khcc$=Byw5awhZAA%*Pf>XUj9SkF`-%43 zeJuMJ`JIk}9a2~P0JOPtYzc$+%*0-AEig1Z%27OL)`Xv`{x=&r4`IsTPqGSS*#b>Z zO+Yhk&<~|EA9Y;=tU)upxAY!uvN5z6drfcWHZ`qXwk!w660-8(*HF`*OY{x?J|#i2 zl`K||Bz%C(2i}$5opi8KSANR%nm4qkb>Um3b?36$_wt@=Ppf)(H-)KtuRtl_3P%97 zTjt)ccHFG*@`t(&O1i(x70KoY6N^;`GqMmR{`bA+t8e#+PBlS|kAsGb-X8cUSG*?z zL}q4<=#z10a`Sy&5$?@>$;f^0xk<^!L7P7|=d-IHz?WXDQ7T7q0tSC1WG<*-ZtdiN zA1Ut`n|p~K!ZEhtQS>J7wN5%Iexp9F^Q7=oYWa&+hajjddlQZ-ml_Kf zqTOp6Db*avycv$S*NWc+`MU+;>eHeS$U9*A^|TY>-q40(aZy>HK^&K=w*?O~j=I!>j)b(J_AC9>BfcpI@WrSp zhy~C{-G>S?md(Tv#HI}WvH|< zG(1SXl1Vt3?@1{~n^;|XTW<^B0zfDG-(edVU;+1t_3rTIx2^a6`InZg$gI{!o{sU) zs(*vS4wEc0M&ID_+LZ1OBQOs=WzQ6wHamq&?sc~(#cHs|p(k15qmje52p_jp5-UP8 zqR;qSd>2-ItlQM%;e-CNEt73Q;p<#MJ^b$?{5?cKu@bj%#QO-|7ryu8pHM5cK~v|M z+P)WbOL?FT%qGR*>mlv=?!tDR=BXPi?UUXb59017vHH{-I-4k&;IS7Ce5*}!0?i7 z?9!HQL+$!QDoM{j3&?{zN8lYL_&5f*ieA*7 z0eQWAWigEjrtVCT5#fop@`7r5$6z)V_?Jvy!rR-9NAZpR#x-6lUwH1lwZ zZ>!6op_+IW?mNQ`?rm%;P+&MNZG9vH6$vs=ZP`^*7cOP!A(v(N`$js=(LYUOStTA3 z5;b8|)7qpCT8WO1wpLaM_neX;m!L7OXjfJM#79R0Wk9xW*Shk)_l&x$MVbpVrQUZg zLtD4Lu}2CnEQrqT%q{KBG@AK?LoMj`FRk?ln;8_!#i=!)n*emCg{_;BEXz~oU+)%U zl~KD8gR`ju_L^ z7J4;;qBRffxP7FqM8IBJ@^ECq+ET2ktY+S)*yU^!@4;+*1p_CoG ze4Zc1B3du>DQ!U~nHu}2erDa7?1H5a3V~u@@{5)ibcVhNy|Oz1qc{fbEjT?v?#A$IPf^3N?6obIzl^TEQvF@x=v1FJg6-&n zb-{3bRj2`~wZ{qZ>Iu^wyXVAjDFztq)yb;*vH@`6NtLiU(1zPTT0eF=VfEQWavNFM z4~XA{1C=b3+W%zbieG;mns=Kc-<^XY;mh1VATDczPafV={mCE@6N1y54Y$c5ANbg> z9%Z*UrtA9f?RGNFMHkiOdf+(US4~T99)_4C3Em#~FpN5ivILFHh>CYuGWT&vWk^kM zTB=43>1+U7Dnnst{9aHRl~Vg!dAPy7bT@TFLdyllxjhH&s-CIxgP*}}X^CJ+o<>#1 z#XXn^Pn1$}w>j&TR`)50sgpH+7Lk?xxPt}iUx9GsKznSC&yUMJ)%FrU0yO};pK2Owgcx16OgS{S( z(9vtL_a}D1lLcC=r~|w`$wJ(M9?&~T4cchzgvfpUZ&hvfZdNl!#_oHRA{ub_|J40)G!+jqbo4+9{F1~)b zyJL#Fg#pouYP)JT^~{SL ze?0ISdW64MXli9FPi}PX%O6Wm4b$Iv2>VDz4kLYS>-se#?tOK8u_wSZ8Jl5Rsh)TEpc}6~>8^W}x2KT)kx#|HAtCz0 zc4<=OedqaGXz>;8Ro+L@HQ63yqMK|2@kqo1m7Sg1?nm>JpVm`1E_pwZiFIq!8g-`j zoL4LuJIP*P;c0mVeSLsBlB}KH0(^R9-uD0Z=?j^(ZrsNL=gkv;8_)_joiQXM=6D}> zpK<(YXIlJ|#)lHkgf*z2uNx%EC!iW|wbsrj3N1sA{0>bP!L!eB{XsR7d()Kmb4g0X z77f*#7fbIpc{PylP2O<0{$Rq$G_OR4 z2U78xp~yR;eG>!q*j!GJxp;cCtYv+KO3BtRi}c5wSV(Qgq~k$~z-2Rvu{Xz9MqC)c z_#c-g9eT&P0f&B`ow6a`9#3ShwV*xgPi@x94Z_pGhL7HqT@8QH#vAK~=6i}L>mWwc z%8fa^tP6MO5@vC@%;RNV%&cE1blt?%2$?3u$w=&2Cmy+K`WDCu9-$AqZ}U|3BnsTt zIbZTSJp)o?>Ty4_oTe!;x~L6Ffbi*AZ)qSyWURyC1~+{h-<~j!nA@y!gAO*VCASPr z{0@dHHL+wTFTi(7vrJojjp2ItFXCa3lsCCl9SZe7jZDl!saFl660YGb0=|g;h4#g< zR7yDQ#&60uF^Rl^GU`8#XOH`Qw%wtBA^V2!*2~n(kqzDRZeoARLQ-@?)*S*ksXLB& zAK5p{tX<693Gn2`^USWh!Y$91j)Fh_)ifmhe(P1$)DupkI6T;12~4L~>YOY(m*-uoN7dKd8v&~=GBX*iruVOwUae;kFj`O$px4o%^7 zLaa(O>J~b;lUhnV6dd+jmfpGU1SI?~=H$gfp1ltM&R6o=CBSd+L7taZ1)bEGuI~(c zWGIS%a;T_%{^nh?q5WQFX#NJRQnf!T7$wo@7>m0uJ4b!|X*;ARMZ?96JOvrecUyHh z#>pfl1-d#Bn`JyP-K8L@=_iKo9l?hzHl&5O5M2~K9fauN0os$k#I^B_jDeKmF%x@Km z<2d(;lad^>Gd=^8x*a*H+txl5D^PTB|Km|7_Mcx$De-G0V}wFX9&u+tyF96&j7^<~ z3dR<9vXiHzIh3+XJaN7{Qm%DuVu8`MiKMjLKU4PNEZ1u*vB_(VpYD>S>rvfBZ$-nR zgjEU-6o9kC>rRl{T#)^R%1AdylXE@+Pwr)Ny7R#N-$U{VcXyYvA-VfSr&|YpQwifV`(#CVebIC zqP0Wwf~Pe*Ff=?Hc&JoM_8ruSZIiFdx-X=_;;*T4)%wU1+44+XiXx(A>{5Vjh^yuA z7G6DJYkaKQ?CsMi=f?YfePddS3Y~HXeZtQ?XT2MUZip@Kq2_gJ)Kc-aLVJqRn0mW5 z!<74AobO}LXs|AU0ph;6pTLT5eJ^pMjP@Zkpu_AE+bAov6_@(5*2JI6r(BckyE+{% z{o~kAhd1pY_`GwG7cm&ASv-00SLuFf ziDWp(^Zo72ZG=qyijChHLH2YwFr)#i?~u^kmzirOh6gaS(CEqq=@qMfglXfvj;&~i z>Q3FEh}$pG?%(P8O7x-*@e;{+xsga#$o}<6EM1+NI^~|v^_JPjz&pksZGrrtd1Prc zh527*YHGt1zWV#=QK24O`_Y9x$5{oYL_0qtopaK2HT$&#DHc(pxHdeS-O z3iMTLi<&`F$b`uI*8wbV(fgC$A(_%ef$!xbOGT7ga4#+4a36gQT89As#{oD%iIO=>s6S{py=H1x``bK*z zRyrLPXe-FGt7UP?ODe@4l`6!AkeqC=(_ffb=D%cBgQSAiKXDKqIcC5&$0N2Zzn3Ko z<9l>oVZfBu3sq$B0D368!VW)elSGOH>rTn9=JywOR^hLwIkxk+7*qs2n1q{7ZfME} zGbWReKgHLX3z+Pv0hvbXb9YD!^`JR|?s!I&AWyecp`RDs4GU?I4|fLeN#ecv+ud_k zxii2h_JcK;7B2@KE~SohKbt8%F3?b*V-sRT2hz5XjXtu>hOwD@dgEnzB4N`T0Pvw= z|3xwEy|1nas5f}S$EatS!Fc;v*(V{wNs7_!SUK6Fr}Aq|WuAv~&?^&GNlP_0vdwW@ z>(*gU&xEDAm*92a{GwR{P}jdUk>?wVE1Qwot~PkkGFoJ|CE(E1smx(D05CXM&3q5% zbSUggljI{+%89?B(?9IL=}BwPTzQEy{Q1EeJ2SG$2jNqKhhVxY6wli*$=&~0kmn&C zpyR)In99;u+o#S+Q-{;0RIKK`9=o{zE1{t22q0<0%*Ve9>#2{WhUsj+wgS_i_?6Jf zbwvV=e^#|9NHC}X6G6g zF=_fbL&fSfU%$K6s+VoW@o3{Ko-+)Q@TynRla)GSw*G%X8< zI3w+Uj137e`KGw7pWb0I5U{p!d(e6&r+7(96NCuN!K@*jG0ufFY=t1HBX3uCiDS;cNNz1g^4Y1;+cK~mab z?y^-pe-+yt$OpP~cCmVw!u2wBox$}MXq^XR#+AS2z<}3;-PiKk0&rs(I~pk3Z+$U2 zF42bBo`-vB?2;N&zU}mN>ZUu?k$GcCoyd^zg>-Mnul&@%Is-LJP)F}3=tU~ZFoI-z z&qLj)YIG7sx~(3vK(=NC4vqnfd9z7@KzhUw1QQ1v(!5U(7i{+|%S(h2^te{*r=q-U zY;0X{i__*98|Y5Yn2t%h)~_bh&ZqMG5K$pv$lOiPaO-KK`+!3n2WpD5RIT&$G`SdZ zw$>ZGXnjlhd_SK@jshPPRrxJEoeSqs&FUkZIX2M(#!Aa64w+KGYXI7I=+>qi>{9pf zg(ayy46CH4k*``{Na*PESD3Y`WKZ@MVqx^5Tx2V&e8}9(T%QeWwYba8F-a>@zIUx@ zg%G>>EAusmtURA&|J(=>Uj7L2B;0b2x6xTBf5|cHxSs-?#4>MpwLGp6wC zh29h4x6j;?YYG07SSfq6lvNy?`lHa)gr!}Lxpg!97wn*2b=LE8hDo+c>b1El4}^{6 za}#wV6TDTY2$;@kuk2G;iT1NdOa~sILRtEh=dvz$0j&`6ySO(*=n|vb3|<5!u(uI) zL#j!7*kqt|G{E$nLRC&*hwwdG%4MVBp4T#bf`MsSc*a58;zv!Qj`L;8>of{M!#YZ2 zWMr=9gye_BJ*3tt=Yy#e+$q)%`vRdPUWLIL<`x_ENX?}P?OQM|;t7oRm@%?Rw! zTQ01?LepUNhJI+5!w}2H%z?8lciaE=0MET0=hbTuhdzI0 z8RESz15k%0UU)|v+*_*p3|`;>7U?pCRv4L!IdmO3y_YJTP!C4v8Kq(3(BIbm!dBj+ z1So^y{C8!u8!3YkET>tJ0;*pu$jqU=N7r^=uU+d?4Za?^$__BAGp_cG%^wedtf2=* z7H{cs)ftX;&vPb>54*~M=G%cWHMfW)*;gev+-953rO$;^aPE*buk067^aKd<^j))q z%dp3TAEx-o+wViMjbpkAxW5rccd@T#_NG@KkH-H%EBrWMaM}ZQnN;MbPPHOBh@?7( z&7FNCVbtY1zu}?dIX|u^;@XeDD2%8E1mV7n#2nPp)4^|klkD@7(Wp_XDtOv(mV+{t zk_KBz*!_(O;}t|mu@r|Y+GaOk3UXo$R($F(qe@Cf2~c9lcGQ=Q_nR&UH~d=6q9006 z2)(V0Q!l*`6%=A3LOvrN`KkWb!r3&TJNT<`Z5JT zh%Hm0H9qy2D0ze$v`BgEoXla-xqTPlY7FccZG?^J^A~Plv`1mHhn;@5%ExLz0cVvs zSU0NosyK7y#UGTx!NN-CozA?cY=JhBvqo>p!e`!lAi&tijjMO?2%FvLjTbl;(?oY# zC55f0sv+6y63svZ=3NjYIF5U=XG5ZY} zzFdDEGWd6z!)>3o=4F#N5@w#fSYTXu2{y~4Vq&dMip#9%Y|}`HXczpb-bo+>8=7d0 zCf$h?E+hEhYNARM)pnn7sr#2!UjonSn{Gpwpi`LTh%HTYM zEBcg5FQzt@XDiyB(Tlym&fb^+-{uY_&_GSq;1iZ<1DP z+rv^<9z7g8reHN6kuVUwVLaC00P=E?>a=oz?Yi1kFxH;^4ZKufGTvETin$*a(0Ad_ z_l5Y4Vz|5<>aRD-!rKkj0agy19L172P)jqBtN+HfEE2#o(MY5h_V`|igX5nVCocl^ zE;CGGT=Na?pz!?k0t%-N-ZrWTwb`QI@3(QZ1m<&`!P+xY{e!lIh$_T8)!~BR? zV6g;<8(0@1NKTp)=bAKG*M`uDY)3I-ch_ z9^-z$J?emlA}!YcL`y9sk0rH2kZd&1d|@WsM%xd%iuZq(^59fa0sPlr&!dMaSW|WU z6}B;7R%Lw2&XR?`*{Y^GwG8EvCr+(bg_N&)w^_SOf=jqcfv&6Bn&ZmFjD4mutr5+i z%EQKKvh#zHLj3 zIUB{t44%2a;6Fxuy2ye|iW`&VsBjv2B|YCt=;*M}hHy$W#Q{t32L}S)9|Nw>aq?^r zKqJ4(`o2X!l*DfVJN*&XNQUTZ$S!N=Q$Jz&qmIX>zQWW0`dB6PCJqh+NVI!Qv{08zU4_$UtRch%K{;+;D{ zFW7AN&)bUuXMP78<`}NA6{~;=>G>-F;#^8-U#PJ5AB6MnyWv=DC)p(CGHm1(n&OPA zMwuy1Y^47CRsY=xVx0dQkl#lZyuH(h@X^T&t)R|v^xrxNLw^)8H1_Y!8cy@KL*JiE z5=doiY3XF?rS0eAE)ae4^cOD!@xkf;cGr5}cLP!z*SctpPQz~c`7=;oe1A6Z2J?*- zsm#5S9usdL;F<`v&v^m*nTxzT9~}qVdT=*3zA# zt-mXphsP;S(NE$#P&Q@XCd<0?T^l#cFrvD^nk@2DKjLGF*(5xFVf38U{=wbrOxDbC z(7TO(ocDhLJ7kz_(@#h2jr`Pyz{cJTYMPZO{o-)l2AqLkl|(ssR>*9iOrxG3y(uZk zeW2N#8ezqb^x?DPQkGA;Q@klYQsH2*WM3WY8KM~+9Gpa5&3ZO=2c%QWLUPs`mUhL{5k*ag}f5c=Wn?FgfHG?3|mz@t9g0@ z`CZr&Lj!UVsoIQF4WjwGFUCA9c51Kf^5u_u(%s#febYO0gPzD(Vh#uF&45Be5xIf_gWoyhiUEhd4q`mU3o1kBjg%#Qi`=>x=hlb^&q6p&%EDe z`hwLTlruje?XsK+AR~0BwFTsgrNvQc&M0FvHl(y0iD(9xOLwV}7W+ZqwgUC2GWOMK zOHF-13DQ(nEW<(&*?uVTju7#`8p5eWMlsMW9-rm5t`4>cfKDf1fnwn~1k{cLo7X&# zcSBjj4NQLXG|iJ8{Xn$2V~tKS^m#=W2*Z)KntMiZx<}xehrDajES12yPpvqp?g^uD z(6<@>SZ%#E1M>3M)sM^5WT>rY03F`NcKoiC+2(Qj8p$mmw6Eh)`UF@?bMfk_F!m<2 zKMB7%;kywERC5YNd}0Ofp6llyHB$X_kKz2!&fqe#bU_$=9>w%!{Q{$!)R7i2Wv2() z)$7aLPpMK@x9Iu2)>M{v4%t0JH4uXrFKTT$E`B$RgyYj!G^i2I?2wTCy~cgie%mTvhOdT7nz(sF&eI!VS}*;SFBr$4 zY|nL*m#iiFP7DW*A7SFaHu3CIPQpF33eWVu*$;e5 zMHP@8nuiBvLMq!oP(5mCgt{$u(}0M z7WobzneEqXgfn;&Zyh-d)sC1oF6D+EL}}5MF7(p;D8cVamA+wsmnmT1BOa`I`3-fJ zHcywL>1ge26D}MT4eHm<_8hJdK^wIGH!_mfpm90f;GXjFu3CJIRC8e5zIBE{(nH@i zn%)zIZHOxhto_RAvF}nVYnW4j5jNw*15@v1q6fuCd;l}RVV^wN=GjluORkmLk?ssa zUzX=yo43X}*%~rG)S>1{{{J1OaMc=0+Q$YRHKg>ZqMfX{qyTn~d`*=SQB2ENtdT4j zpm-VmcOd&yxF=?l>WiMj(-)vBNDa}&%5v-aDdiPkKZ?v&>vSDoEqVS~`3g&Swq$d3 zF114p)JH=-dV4R6P2Q6p_wx(MZ_Tk;vvq|{ThW{IEabD1`4M&C!B&*96Z_@m%m1FK zewtMnGt=s++)!d)0p2}I8EF5n+|c4awU%$Ad-@vSTyYyKm9zZ^0!zGO*@9|pLBT3O zK)2C=%XL(UG@nXYBQ>;o9RO>J%eQfw{|B!#pZQmB*xK&arhy3ovv64VSR#HnO^b^g zivvAigVAxW_y(=xmCkQbRrE^l7|g#rC|CZV4IVDMmH?e`oM1U_dM72)8b)c1gchz@ zdk!t1Gh7)SS{H&a>odl5w&^1l4nitw3#>{%#MB#ROuP~@dAXR5pYeP;>x`Qx$a){# z9O1ugDVkE{`~nc;!Atjs6L0n30FG9cr3N>Z`{exnib1F*uy%9 z1Fvu;4_M_h(|0a5Ha3Cl6PiWA;yA!;JhMuMS~USssEm>DD#;AZ^XHo*68r#bgZRmH zqzCwAnILmsoYE=>wyN!X1(WoRJ0TTJ6zx{byXn-(5S{(9xO~f-&ZAe%H>iu<*|I%= znS2@;e`q0~fRx0u`tA6c;VC9%Lx$xEbP7)&fEDA6em71|nu;q7l=+6b@rXpQ$?G4@ zzg%Z!##g?vRbFG9bbjzuc5nY$&=x->!+T#K5=m0nyy=09t|~wnRvc^0PW0sh=gSlI zht+e0^6c97g}Kbs@y$~@s7k%)y?fVHtj%WlF9Az3P7%1-PX%K5%+6)c=o4p9yu4lH zIezDZ%2GRo$orVRaZgU&d2REKS!-9fvP9VMgfXj<+8JUTs4j&jm%#{u2$uF~mb7=$ z9*s5PYgXvHBe8JlcL968(TkSJn=h;&4Ux|iLf;91dL0^vYTXhvYo2%gwta_dhn!^1 zryRKrwmYY1zPzWr;zXZTBEWDg=eB^5@$ff!YxqvBm-v*aGAW zI&|z~@GC9=xH4-@B-f#bqvs|jbsh|VJ3bx@H9N12=-IfFT1tDU2Ssr*?lK$1v9wYPq!oaP2B z_M^km+laahm;1pOd3OF^PAN}+LnBs30J3TvZ0-BOUF~SpBW$JkL4|gT#aQJko59mV z@(*dl*^;GjyxFXA=6ax$%2&-QnVZ-9tN|@frzS(DbL^Guw1nYwdEfg6jM~DNpQvp9 zM9Lj8u`CGMn>;m$T2+CkpqW_sgD;#I4Y%%W2XmWmOgmW@n3Ns_6AMI z$z^0OC)?w@IF1CJSSunFOWoXzN+A|&n5gyBXNDZ)m@fw+4RCiP*C4pff1e4sc24)%yKN`QOA}`74ids`0xx zz=LcfW%B>wkpYXT!fL7{9u?b-Y%S(a4$V#l^PB=-$Z;qX362@_wVslpzS)|kOd$hw zO!)eFLlTp7{C8@pnt?>8DU!CY^L$4Q4f=~Q_eHaO53Si-7kD4>?bC((1&Z^=T0UuO z>;~sWx#Xz-@y#_Ta(No;eciFKWqzBCW2u5D-@9d{W`0(6j>=(chJ9WNoR|@`VcT7{ z#8YWzBA^8C>rgql2%&7T2E0r3K;`}adV(rUa?TLuFo>BK1N&4C+c)_rAD*r9^0JiH zi!JB2EN-S-Ka|k^G-fK-p`Z==I`Y9M4BgV5)|)*ZsXa;$izaPXH_=~}5#ZW?6l1@c z1PYc}uAipo@ASH>_8k>}Y}U?BzwttfNk-uwsx*J9H0|ki7$f>00TpLDm0~g2^zlj> zLH51vQS`#W;gMs6t4`PGcSKtg9+q;Yh5h}yKUX?S+`Er2eU<4<3cKfzQ7}&7-OC&hOXfWp9ZDk=;rTxp|ny&Ks z&sr6<5c)F_e_K%G{iBm0$8*^FxQ{Ma%9uSY%$epf7$w@1E|p8-%-T?Et&0jBqq19S zTM*WN^HP5Nsu0*-o%Ml~9Lta~0wLJ_)Ue{#&l^xgUC1gNN* z_7?m(yA*#KBT-43+48k}d%r{kXB2gFCA1Qg=HhT$3J}<)uS4@{wY#|Q8geCapB{%! z8+__({x$ABBO<-N9fgTjq~;mz{9lLAn~0>O2EwVJdoCE&`g?M5dO&kz+KENjebsMc zd{vg^ke3uD%F(o|O8fa#p8~Ag8?KgsORGfb?{$twYcNOSW-mE3&lgTwr#VMOtNYKJ zwR-9V5h1YqgMZMfb;yBx5-Flk20T&k?e^)kFxOv zypYfWBOgXeqh&^Gn;DTY0g?zN;)dOOR zwFpgb$F%1dFMU~8BKS$n|r>{>|DINJ)R+qj%@fj^IujQTU4uKN%*~T}$i4y0> zPYC@rNk!gt_or*T>P-B#efYGY{|L3A3OrFXU3kC7tirHH_IglP&^U1IT}$xmasuB* zA7DP*-Z1?EN6~#|fTq*nE1UU`*F*c6OiX?r2W9)`o2Jkvf~UR`PJYZR-`WHAXG4B> zX){|jFcN^QC^{-12|Jg-I;uRnRw5u`Ev#oU>)dyzCMkgNzV3gFqtnk0lrhYj%7c z+VL>B1=E}J|9tdYu}`X#sMoJuU7N%EUCwTwklio@4H^4-0Lzd|DIzUU86AIijmouv z4485oN4Tdtv(W46JhY6|L4Hoh=75chvxN13tc9q#|OY zNWh61h+9{x80Cvb&II`xzfO(vWZ=fIyof1dKi9T_L) zYb3b+6+}Kj(r(Tu$>Dta-P@9n-B*q;-k+B+9$e){V0bJ8mJcl%+B5MSI)k;gnz|{lMCG^Mn{YC$%ERe^&HUXh$ zMdlx%8g=M#D53!K}q=WR(oVZa@LN0&6O;i(lM4ci?0K^H%j}^6^3Fr z_IKVJ4H;*m2LYl`(X#cSFn6Yt2kK)cHRT_jgb7aE-q3G*;|((1%Ng{A)$+nderD^E z#g4tJ6Ll-v^p?BBS91<}KwFiBPWC%yH*^pSL0El5GAq07zXG>QFX(w zpN8U7`&Vw&S2k0kZNgWT^e1w$-{~FT>r0|#%ZEiCcreh?DvSSLi}2sWLEdF909NBU zKgp3hIz*jO?Ge%ss78uUWm?hUDxu6%v0F>+O&IDx=Q~M$0u2neQZy%pS+YPWDu#xZ zpY(9QH8qT{sd4#=Yfu7yse(+SbC8$VhvlHW}nrM z&I+IqJH&}LytZr|Nn>J_dI}$AUsJ6#8E8}cTImAMmfzH7rZWA;QE_w`GeqijDGF$v zJ4pK-O;?B}MLQqO5g!|grdz7jwDF}xj7kj6C7Bd!!X+-J<3L2N)7t{MPNvdn^c;n} zG}#QK;sU(JUIZHh09w z-7WWZ=`EjuvLn`)n(WoxTl2fGL!&ON;R~9-ShaY@SFq|AKC)vDkel+Ns0VwRZ!ni< z8t!rb+_*L987?)ZW5;(EC{72pEdKXa{onO>dXLN~roxFc+ANXWJg;Kdysl>6P^t0) z`ub#`3jgot@ArUM+vh&OxNQ4!+M0fkI-0+@`*HX&w<3_ACyN5|N&{H~#K=uEz)CngI^@zEs7`$CM);LWzhS+r zBpbO=obRXrJyC3Y@b+{GvSe3BN(E#ACx1LJ-}RWAy5E`ncG%aK&5%cQqyi1*zaMA^ z1n9qu(`4sh4zoQD1aJ>TyT~>QO*AqGO zOi~1f5v%NLQz>7pi1<)p_@ytDG3(-~I<4hKvxvX%{(7$`JGdJFw|(Gz)!gDDYr5G^ z*%!+pbNSyl@pMIiU+zWpYBG8sPKATQ^3;v5G4XM7a5${Iiet=@Ag7kHjKhbMbw(b= zJR!L(8RglQXK$;7GbV%b>L#jZc7@0F2Igxi{7G`U7p7cd^C5h{Igif0`+9*E7*if4 z4NKbT!Wi$>CT3!XE!D1WcVWJ3)grI#%D6PjWO8zED*W?Rpk=^Tp?Tl2ahmS{ng7aT9!8;dGH~+WpLH$j@SjzIfB$WV^+YX93F#MBt;;NM^>opQG{vqh z2{|ftODn|(j0-!{_--nC^I{Gvh+8Kg7!BYuhanrE!WjuzlJfPMQjhJ5sy+d_DgBVMa7 z*3@t9wsWi7ooN|8CmTkVz;wRBAhyXB^9sQ|;A-oj>_-85$JhQDK+%3PZ38awzZXUp z(MAivwYI1zI>0@{&*w7!Sr@ps6lI)gaE&Qozb7$U^$Pz#I4%B{^~7J$!?pC3bvd$@ z695$%r8HYdpA4 z0J3pP)SyQ$v-I4Vx-K)CU;me#lGpt!62g7C4mq05IBxBAW79}akb?y*BO%+B9fk3^ z%9swKMRPpf81F;&?_YEs+4HYeDC-KW1LP=7dkYv`P|-076477To$l>^s4raq2z&KD z(i5Kp2s^WWaLV0OyCL{{^$E;yWqq6MV}zLl)^+;@km#VhGu& zOP_I7sg|A?OvahXdTR=6aqS2!KE@oFoE#395_01dNL4$Md2&U;RbL#j6_y)Iz;JrL zSg>7d7~A6%`wr|t#9$FAeohsoN@{d+*s3sq!k$1KCdW#K^^H?sRIG1*iiW0T9Y4e^ z)6~qAu*~6pxaswVi(~c~6pmfwGiovadWqyQuL_VcK)Z>|?)w-Eu#D;A8%YRIc%D`v zEiw+69mK7IwhAOGNTcg^UIH_Nl-Yn#p+YG`0hkxVt|Do!bP`b!m4{l&C9lFLl|@~& zuYL3jYBN!<7Uy&I^st|lr-vq9NvQblbMtt0oOyObdqnGS&UAn`Xg?!A+aQ!Yp=CBB zl7ZJaVkcrFP&xdPWJ|!cyhMqpxarY+ze;?aa}Q;d?CoXiFVmC4dLm%T{6F4#decii zg=A>J9tohS6=I`slK_FGcY@*4CCIEt+coREvrNMl@0{CWm!*!FEs*?V<1nq>NXJIH<6*VtX7r)25|p+%Pc)gLVxEvwkd#(R9to@ z@Td~!C(ACE1cVyR2lL(4?pG(tGW!3|@$9EhEbTwmILU-b0tfV3^?sk|-p-ezU8%xQ zKrnbUJ<5g%xB;CB4of9-yTjD5wc|mxOwJy;sSxg#H*G9e(}1ULn|)J$e|K>EPkpHH&Ye1>eoKSC0m!bO49=Xw|RqsjxW9=M2sm`}LD7 z|B)5$EJxm*I+5QKxACm$6Hgw*TBXovuo!;sWv)c+yhPKcp%Y=|%|{f7qP)iMHmgN; zSR8SpAZ%aaE{SaDK=O+|Kzmn$u$ITkuCu=hrHOK}3M9hn22jL%)VoA|wnsuNE?z44 zz1lI><yYZ|3p~%38OzH2N}9A#X$Hied~16s9Z7qx1CakqhLui^MQLxk ze$(uN684YrXlEO+p7AyesPzq+1EsklO_P$IH^OKbldx4rz>oX<9TsFEqW{ zdX@x)p{4VDEu*S3mq2zqAIQ&GeCH{K@abiCb@PR0Hmu~>d68$bnL`E|IfL20}5GGTff#9(yI5$rB8ZFgu(o2YAKG!;zyPFA89l2`8BNTPJUGq<9HK` z#K<&{xf6=G=4>r?FAu|E%5;_XGjd94HFzdo8%hJ@rv}lNY)8)to4)%e;-mYe1vmOE z>e=@F3hKT|p8_6v7B)9}D%zHg1i2y#GL+2)p#K~;-I|W`Mw@NjpP+nVRzWc!em0(^yi^-JK_P&Q2sraq0u?+rrsXIatS!j>XEx!=FW5D)!0!HfT zrm~?Wym4?47D_Nl{d-~jYzEW@gMhuJsSP>;C<#>;HZBqy;4dznuO4Y|&KL9T>!jbX z;Mp=ZUx*!EDP+y4&Rz&zH1m4{UR@8ah%+H3jyr$W6iQ*j;b^mtPRF1$H782WEb*h- zID&t;SL}A(M=ph*I8I6h9(?wmu>rRHePm~rT@T}y2un(f474xYn|0v+uX`KUsLkx6 zDtY_D32e-I3V^$##Qk``CH(A`Wy{9h$MM0#&n>D}UmZF?=eyd24Wb$6+92C!QI&*} z5~3XZ`ccz#f(9I?Jb^LYdOo2Le(3}AAI;XrcPtw^>^HBrh7wc!@y^%#a!{Jcbo=i{ zE^q+8*ojg@)=}eyhR*~o)t;=DqT6WZg5xSBGJUs|s?zKc@}%&SO^|pE~%1cAR+y7$Qh|~y+!-k4Zpbvd|>v|FQbasJJ zCx=U4ddAY8mj4(TGy5PnZT}308T$uetn2*ay$-dQe4;#=0>_IdQ{bXlQ#rz%4VVE= zO;;?yfT06~Sj^|ub)A516$37W1IOOOR&`K z!=}K7J4;8e05Hf_n&U-NOpxlpF5-4UWou*wO@I>D{DSVuJ9yD}0S~=ls`^P$kK9F6 zlU3K1qd{r3bkzhxdxN#W~)E`@v3E-ABg%0bXtJpDOW4XGi@Ke zmVz8tIBr%5$)m)dX$fNl^M!<0Gmlb+OIHL+)YBjHi~7I6JotBZ2joW!-H1B_0LFX} zei!M*;}lIYop>24O&LjG1I=9VkE2IADI2ez0D3{J3w?({zx$8?7xs)<@mj^a8wultG91yt>)c}; zzc5xl-gvQ^_|;^svLR#}!LiWwL8uvbLd01Y<}Lh|XB?LG6XiLPt^fvpbo8j-AT_if z-52l!oNkj0O%;FpeJQH7OUl~_iJo(%@gavJikSXLE|Nz(r&MJ-k<-Td~>&?+wS+m?hN3E zMNgUtb-g#edz|Kdp1Ctuj5u{s@n>_PHD@DkCOcX#a7A*{r&kIl^lHAlw6|R?AN=yb zfz*Wd7)eD2(iaz2$L@?4=ZV;+^k#23w(Ywby{zHc$X{zaZr=TvaL{y=Yj6-Xnp}Qu zpzNODPlkZK%O>$%&GW?$1tNtLMU7q8unh~h*nvS%;ekSWp+G7#%zYRZz8@!Ey1nBNecKD`TUQA z8D1TCx{7@pA+*S1bY7)yN<0BcURxDwK_OBvzZYo?sIWhbz4`rXj5Zt6Fb-(e3({J! z9qs#Z{Ol!r=N`yH-e11F@mvWo$D!}lnDyAKeALW=Yu0(iCrPkp$aYnrZ?ApVp;K`L zy*m(hIN8P1Z_LC{-0S@17?YwC3ycen!3Vzeen&)PgF!HMa>SSzi=fSP3_!h}xN|x*aTdL>NO~?e^358ub zjiX|eOX<(A_is=1nJ=`pwcYQE&C)N^O?&c1H;qtLtj`{7k9kj)BY#UsK_dfODs&z6 z?Rpy0tE(YbUw8q*sZ|PdHr)7v%7&xk#e;0~1wu^6pv%m?nqj%r4S(5?#J-+nsa$}9 zS+~aYS?vv+cWHfM*U!QyH-qJixGT(BLowQ~;@cpRbM)1xi-7EcW%Y!6I}_;lF_U|o z{G8{xyc;YVuQQP3X7LH(?nh9r8;bB|&y7-heV2|iR~SZC57Vz_o1O_hOG7D5w$*-# zkEU}FLpGeDsOd>^e1&e7SxRE0sS!!0pJcs%s0s`&;EIw(z#QLB1BjT1qvpY9UWKm_ zsA{I=^ffwsS7Q)82eYh4iQ?HI{T70ycZ7F>VcES6Vf1?=iYW^141W68VT8l;l(yR} zACgT3!Isk;LNDy7)a+&Tay{w&luf@-0~5g8DL*s9r(`MJ;Ax|Z*C|vcF5$HweaF#m zx7)bA0(M$`X`U8YyU$~d zF^r#ZVmyi=cpv)oxUbfbVLMj2Z!Vj)iY6)0Jzctsf5j%|`J&I~-_Sua%k7`x1>`>G zq;FM+ov~jnfl-2HIZZ5}4bG(N+nuzeRtPG}(E=L0ohaxhk!UFD>Yc*M^Mjl>J;tJq zo>D5b&n{%bCnfxNHZa4gYo9K@0_B3Rsg>`pdFYgnzcslHp0BO-KbFkfjt6D5I>zid zdIZEFI88IgskpYwHQy6cvkn>-%-n;ILv`y~cUK23{Gv82$QMOi4yzHzDerVT@`EN? zHGr=1R$l^R9b;NEsXmTXsvlI=`XJBmT`Tt%r^R9Kq4mzgMde>$v)CY5El`Y^>+>=I zeS{Xr;E#ggj{WhhSHM~M^gqmtr2vPL?SN-g#-lq!O^~iIN0PccTA%Kjar700=Aknz zN*$}(Wwl~mL4l9Ib15cCU8c9t*g+5s0<>+se}NC<6iQ20=YAtUwwvQ9UKBh^uQ@zq z2g*mQ#t$a1^Y}pe4FL&i(k~Awv*;Gu0KOZj^^fJD!>fMIFM0)kk2_l7mG2PSA?eSI%1XBbplqSEcf?_}u(@eh8tbN1 z$B9;##sfSzuzn-B;HgCtFpbUS^tSJ4MvJ|9ME!)iAD6B2?6kkYAD=I#T;s{fNxN4J z&a)3-OYR)95;g2um#eLUtZ+SZeaXUp_gdEIe0!~ac4ymk4x^+%`klS;aW7ojLE=VD zd6L2HOEJ4NF!t-Zp%|qhj;H+7R0i0F z%~TAUKe>tUVU|%_?Xfn`dLh7y(lSeyB(`K*Fc7J45vYtkT_ggj!CxBfnVF>;>4O^~ z%o^%ziygtxEJkFYgy65$vse~3@h6j1G{9`Wy)I-GJDss8L9Ke4b6Xwk+L2pcEhkW1PUiUDfsP_jG1X&Axi!( zj>w)>8bKTts}|8w>U^o9cTcAoI`T9pMO;{LbmeFGK6@G#v z`2TalyyUfc!gqr1e=&2E$FI(i2j$PYM2&vFB-=(9?>)d4bXLnPy0?T!gcxSrfZh#! z9-Fl%Iu2j-s>m(ul`yuWaw7+Wz$M@;`e7cJcI9w0!<*ESuK8>_{e8G*B=J>DM(|eS zr=EJuSj%Zx7n6}2Q7jhk?L<|h5nUENS@U5eeH?L3qqg?ZWb}u@yu$y{rK}q&= zPxe);2AAs6;H7V6?CdqX<1sYoe0QJ@bTbNp;?#Hs}|ttyS+#K^r>I&;?r<;+LA zzMQ#Ac-y<;ig2J<8*oUBwD8EfE+G-PHFa1FC`6Ssz>FV3Ft`q3vLV-uEEH>8aiRlC z$p3HxNJ?qndeW9aGoU5D66OSQV(h$pzl`;el0~3N35Ny^6>9WK>YF6jAA_9no-p};kpL%C8hk1fQP z`LTqpJLqU^(nC5sdCcumON$q9p%f(@2t1!LSVk$e&t_a_r8VE8cbA>VMdRbS%+ zs?GK3j&9w!-|(Pt8M?L%nfejQY-e+qhC~I66WH*`fuH;Hnu&SZ4u$fwLO5(#Wlyd? z)RQyA@LrEvi{&Q%{Hr)Z8Up8l;`M+oeTZlkt-0gh*7l=kYd7zZ7botj!qw%p6knLb zik5#TvhZP7nF(X0yD#QCVOg-X_U(J!mL#F{s#1H&{yz~n+?+0c^R;h>?JMW^bypbH zgck++HWp05g`&`vBM5foYu|>HuN^znjFI)H$I40D$1{VQdq+6RZ|$|XutY*-wuZr- zI4y!Y`CA}{wzgspiC5~%)@!dCZcleR#I(L{G4|k_x8)^x-#W~?b>i_jgo_M8k8SS* z(hS$#nCn#_Z;0wDDO#e>#=Bsjd$v!e-3}IEc@?lx_k;hRN7bvx`5wRMwjUm}E`YEPuRR;SLf<#)A@<%8oi1NJte*^o z2Mo-AIIN-^sT(HQD2zzyMSEjTC*&r4T0@9H091D@Zrb-;Hlh1GL(^rQvm_h|7+z5_ zyfw=}6g}i;W*UhNo?MJ?ML7x}-?w1S=Qs}Vz5M)D{z)$KqEAT;vtT%%A9wXkq`XJmYufTEr!(@}0x z3}pNZ39SHR=TW7~)M7G;pUxtvTR_{NGSupCtY=`YRCZVi z>7)z(26Sy%Lh~>#y8Lh_a0Cj(&mie|LuNt^wGHKx5f0T5j2gV+nt|h;@x*ZmvaRld6ZA zT}zk6Q)d(w$WlUWdHDT44Ow#{-YA;H}g7XJbPl<9}o)XsdKv4E1t z+i!Dk8>T=rV6l<5|APTYd47_vWld9mS@_^8u6Q@2_e|}i%(*2Fz@XMW(GF?za5cTG zvJ+TwqO^t*JL}_6(#v9Vv}aJ;dA5k>+*%RV^u?G+7l|tyl0p8CT4pDQ_s>`qk&{yh z#8NAe@ZQ@Yeao8Jufn5072W6!bp`qP1&F;KDA{qWV;X)}Xw*J-0e6I_KS>w!o7W~p zySjjsmG8mVEx_MXP3NZNd55ws#Ml(8krt6_W#tSo{MCw~wxgDLDdf|Vk~uR^c0H(2 zp^>4kNl#w(kjq>noN3Ogpws#F%FOF&x1=ACjWS@$*E5*e2GCR)_kp^~T8Qu&o6O)V zwvRkNZr`&1Eo>aKAsAIiHB{K@3h8M!kb7^`e3Vbx@reHkuC42l9GUM+UBEK$CzLyG z$6_flN8Wy-dr4q_xnXO^=wnBB%DC}xINH8X40izh)WAHYP22o}#?^*G)nc<=B*M&N z!JpP4U2IP~HLSk!*g2Ty^JpEz9+t84fRMhk?`NFKZtE7a{|@lX?tdgN;g?JCJRK7k zpRZ;6--ST*y!&3aoZHQ(M$nyG1J@tBP06I~frzH*8F`Nn$BZe?iV^&Yg(tgJ3lR~f zXtSNDHG_mQzx&BSM&BK+#aG>i|&$H%aCxt21Wg@!aX#1nM z*?<;vlE;=8$xS#m1f}EPPf85%5r(2yu6`4t1qAKr2|g8t!$}J>oADo-gGLH)ljfrw zRujfUrmpBBsQIC)OW%E#=kt}i{_=69|Mq^Z!o z%6p5}GCPKFPcra;4bO0v-84{ei~*xoQ~J=wKV9V>5RL9oK;H^oePf+P#puj6xS`)* zDo_-l2;vaX3&wi1LDA>C(`;b5pESK$&+u?dE{C7CWFOjA>n8sW`xxtsY4lVmKX#?q4bnw7g`CH=JPHysu zQ=sYmklpzEIESqM0A`k*M(^9@aM{slSt0r8=9_6@J8?hUO{@2Cl978j4s& zm+MTPvF{m)n8$@5F84h67Sl64L&Z3rAMI_kzAh!I9#Q8N^{!ni#$JsuJc@8&pBn=xAWhSy zA458WJOddlGFXXRMQ8avQ}j=r75uEsPpyE0I?zz@-COM>jbh})n4plxp%3f?@*TY#%=IE77|b)hnHTmQ8_q8lOYPk{&^Jl}Nv~cXD1m*~D=K04Jn=cv zIPb@vxKO&?R!_eL^s8m(a-u!y_um+6WFH`Zil4Y}4(r!qRNyyGcT|p@aLf{@c;X&d zq-+17fO6Hi1v9QJ)!jDN|6S|AuT`eIX)_sH0(oog_1#)W7bWN0ksB?KF?t=R&6-?p)NKd| z2u?*T@qOfBGMaD2yjy0k#aC+Nr<02l=iTMRtQqjIX-A&{|Hzikw>i_IW=aXieMQ4p%T&=0jo;{}F+ufOG>tDZ z$Ad*H+-0CSeoSAB@Aak#R|L@E`;A&(}WqA=x8cx8KKgw=wf?^m~& z54j_fmZQN3;KCSv?CzRmVVYseU7Rm_RKd4L%YM9w?(kzoU7P3G#%9=DMZm7O<-dan z-+auI&TYnfbU}Q-*bk3s{?V)wqzWw6OV?ZQWXn)`^P2_oj1}OdjJ$fDv@p(A|J1e1 zd5tkjfT4{}5bW54uEFqktcA#J+NUP;I<}S#4C#OEY*I0d?hevl4zM1py5um%?fm(C ztll}Ctv1VTKBfO~h-Zc6$bq$Y%xF^k)=H`^Ezkb$=-1uvg+~U?kk@LTxL6*7n;7O^bLDhv8 z@~l@fxQHxp%rup><(nL*onE(+faHAQpeDT7RRsqw`aQkYxa`{sK$>1F$|}A^eO;UM zx0j->KEiv}*W0Ih`$%PSPSxkI8mIbR+T(qR6LO?+h2dfS2z4xZ^ktHFT?kojiJWLa zt0hvGG-7ILjE;&8m_e1jZ`Q64DE1`p7Df-xx5jaAJGx)>OzTxGlZfp&Uu|HhnW`q& z=DgQtS+R@W+OMtLeg*|qniUK8&=bV$@ZS`yc?pN&fvyxFpR>ESpCt6CB^3%AJq~|^ z?|RO!g_Jqph4yF>ZG~1sEK-=xER;G$y0rq0rI{)&yx7{1iE>$*2|m{HzHJd{&V8>Q|Kg6y0*PG}y!#%Xa3tP6JzkJ6EuU`b;`@PHTrhVBdsJ zU&lX{^=BZzLj6YHLH)v39p<**8Jtqi`?D6k3x8zvG<)n|bc@ll4kj0E{9eaR% z0qZS$Z}Kq18&yw;s!#lNc=1>jhx$q?GI?o!UnBa|hoYr1q0(6=SbO5Md-FxuyD8AH zg0eIMP+qpDbuR!yep^5tGmJ{38ENOf+_#|ot-ziD-;Ej8QcpybD8VztH{irxSKQW! zVlTQ_@nqDaGAu&8c4~sM{+*g}+VoMeW_`!%U7r0P<@+#ps-DPYiW)&rK2Sp5JwF6@pC6dr>4cUq< z)GT`e#6pW6!s_uSpe5`q@uVD}NQdnogB+P}pHFK|G)=y+(7NegK*ZTG7s3#b$5AI1 zcl6N(XAd0!ijRW9NsQYQt`y<9!U&Vu5s)@x62d`0o>li_(?IbV^+RL+(P8_biyF1& zVA586k>5ZpDgK~|YStj&R*(}a!-4n$-eWQlxe~l#p7G2J^04>dmG!}7y)+2>x??oB zoTi4qz+H%yISM65x;~m%J}kc0c${Fz_r7)X^GN#vNA^`J@FB!@7+bEV@QU>)^LFpL z!1k2vxo{oNzzYvs<_ca~k?$lR$6QFVbH67SocQE*Vc-vbZzpt{UixjcN(J@%)-b== zj&vg(^lzBH(Lt;8S{QwIQ5-}WyX6r^WWHLmlNf+H5ad$e{UoFMry>leAv(0&u+03_ zo76UA=C|d<^*lYI_^iJh!LtDoO`qq=aC!?~a;1C7wIg+$%7(ai$hfniv3QeoBA=d7 z{AQ8$baurM6&?pWOS9tpRsdmhczK6!+WJh2bE6-)%FJJfwoaWLo5+j|RX=Vjh=9sI z(SwJ?^lbdfja|qVWOjEOQmfF+V%qvNhT^~x*S+HBGV7yHs$=U>^^Pf;O1bH(4qWuO z1K9KB(zJ5~%Wj!|YpPK(^w~1^7abW=CP$sj^EivLVhB;(ILIVIgnUD{;XN07(xv$amc~~1rb>f zrNU3s9(z04jceO+c=e@z4uY;->9>cp|5Mo)ad5G1K*#e?l0S83LGui*#r`YXAk=ME z-G&N+17#-ktPZ%9TY^Gh;&FqDJoS^dhbf_ zHAIw-G?88+f?%PS&d!KWj`%g&aN#4C@_L`YBv(ja^ zYIveys0cjIzDp6d$8YX}?DC|y5nA1>v~Mpgi5fg)x9f)8S3QZ{JrUnBWxlsn4`;-5 zO^h*?S+>Xf&FGexvKe+9-88x+>T&7=v?EMR$Q;8%en}IT{m_3>%!>1rUi~g%l2wt- zo13mWt6J2oraNp{9xw)@2W8NOg@r)wU9%!^$ybq9B+7+_JsvzU*=h4P=sY9BOgWJH zS`Prqyv-q--_9&*ugvc3z$nWBy9{Q$+V8-a9mLfF=(L$lY8+Fe*TAAd{d>QMT~O zPW5oUQ8^mV))TfS*Yv=Q{Tn3ts!~DUCOs17*4lR;H#=(3w5s=WbWyxIWw(*p?`T_k z9q-8u3-q7c>QHUEuACiM>2UZ#?&K-&1K4xsjN?#)c+DK%2+KEq=~0Sh(hgf!-I{Di3H_+VWOkbb#T*#UV$R#X;4#fu-1NGuEb zK$;JcRr@A5Z+y4&ow9mh*4tk{nzrlH#J*q33#K7k(_Yn3&(|PhOz!f5Gtg=A9cD52 z?>*k;ce~b1cRbtU$cP;mIb!@Cn0z&H@5D6*+jgQJ1`wB+Ewq#O))P;Nh^izh4xF#V zGYyWohkPUKYs}M%@)9#4MJ}jWm+#)Rl}Tk~Ww^a71m6LXU7uGqFtrbZr{+x+3+9Y| zEzG84boI;K+GeW_0rlVd{)w|AcI-7!lbNxXIv(>mRRlIf*u9Ec^$uL{5c{S%(~v4t zE=o_!abvcRY0Wyzuvu>wLn*ZC-ndZF7%C=2%N zogBIsF*joz^GB&}9(;(oh7aH4=lfHkp5{b5EOI+9&Yt?76Qd)}Y#y99^Idujfv-pW z9MoCVt4Q1R*r)l5qy}Wt`ZY|wK73^6MaTbnK(eJR+UX^iOQXUDZvY#0E zC2G4&M# zI7{#z32QitWkKRpZe?206 z$4%dD_N64Adc3-m=`+$2Vj_Yw2@zoy)%$*2?mK!<&p9P80-{T;WG7jgEEdu(!`dKFO8ahwfRT zd^Iw@QG(&A8V06v+}I)^^O?U?pVnQu#AS}+E)B;D?+!1bbcFW_5syX9yTBP9H4L+I za>vp63iD-s@nSY6ow5BGF-LBhla_zVkknj?XwCt7pn=cAx8KPEnE1W{!wxd2Q;@X- zeY(8apz`&E`wsJ%`@mF&gF*TW$P-*fEnMvqmwyOKudt!ebc@^Uf(`qvCh#~7IQXTSKPf0%kVUxBcw6K$|G98zW%x~^Dg6O-wT6n=L970(x85G#TwpQ>)~MHJqnnCk*ZUWBXMVNT0$b( z5Yr8@T9&tsraK&{*Zhf_bbLA&EmxmGlhyFIljsF)M*U|^E!)H!=x{tu{PBk!|6-I! z=diSIzu5VV)nh4+jG2o5xHa^YZ9`yJ!?T9_17>Xw7C?jbIN7}A#Rn9AI zKR?psX>u^fN#81{EX(7B)yzKuLPro>jjD;=vmzbSB+k(IiAi3YQ9GIUM;G~%s@XiU zRyx9deRtQkGyP`It#9aeaykko=$zr)Ge?+A2mlL1wkDJNX|Mo|_x3+fkJzc$th2+a zjG*qVe*7le#@w8fv5LpJU=IDQ=NS5Bhr=`?Yv*|Eg#?|0q(sU!4nK4_y^-8{USc_- zy?k@Bj7K)mF}-WmDV4gc3Ph>;ksLj>B*jsITxDI##mHR8Qe+^b=rZD-6)hiK2y5S{ zDsL(2V7>lZuezChTE3|(ylLyl+Kqk2phWb8$-um-R~rT2iq4vSzmV+j*hcI{LKknL zWizr9ArtDpor@=}O9N}w+u1jAY%i>4zpM>xpmXSuU$I!4Jq-N){rK>u+KrPAmWrf~ zo^`7#-sksBdt9uQSoLoz`0q2qo#zazy+$_ zpdMzi>4_;}&~YiCZ&V<}gS`x2>b-&tN=MXbwzE&asK;;>6^U~sA9zf814M0`6F`MK zf~xfqZDNL61dny|`+(EWy-%vuQ}O48$(bB|WCtQC5NhTY1DRG?oaA$k=gl{6u}Gb9 z^6$}@ccczod=E$*(5+5=)?{UU@zB5}T29hsK9j1{*McHYGU5FFjKU8-UM{Ov0^vt) zVt0AqGa$o3tr(|@qP|zF>bHBhS7vQMveLDjUkSRt_kJ93(3(-%_T>=+hKs(H ze9V&MYW4zr#W*3(TO8o|3~O#fC(e@A1hZeHdWm4GV6Rg!>QdE=!jBRK9Ob|c=>bWv zD-rC+_%LvQ)aFNE?wR=BA7WVuUX%o4$hyg2M@3{T-nRl%cU|N}F=e3F*=9NfE@YV6 z+B`J-IW>|^+C6Vk4A&1Njj8%4P=>$U}B`p^OJCdnSG! z(s=-)Opri^iUgWgepMA|eAsTDK}SB|;9x41Jdro35U_p38dM1g25Ig#`+O4jf-SKy zIlU_xZK!>$Iy23O2^=p!DF9n8ubah>nM`JOtjt-d!>Q1_S4d3ggr}|()pxu z*FczV(2CVojG+CK1PE4J&z=B|{@PU69&30~V&JZXPAb4Tg6Zae3H)rorTN!IP`-#s z2*6&k(sa3O1l==CmG?1Stc@6V`FC8RtOQ-gA&cV@Z+2GcI9OucnKf( z@uA~HvR8m^JfywM&hId#|EFW%%^eLUj%%=tdwYsK&(pL;ErRAC20~Xx$3L6dmH8`I z+{pj2qpjrj)=$u&{Ps^B4pque)dt45-tGI9d3;APD^5!9-u;nS)A&%JBnwM$MlmJ* zd{eJ@oc*9fFI#!*qZzLRRMRROb^;7PpC$W4kyrBv?Z zEt5rAR2D3r<|DoDsFxz2%p`2DX1EOS(m!i!vRmpSzkhn}!um&;Nt@!&$Q+NRP0VEM zGl^YMw%SCb@M`rnv1DmSz7m0B0rr1Gs>yFc>O-dlR zdpk(@q9qP)WSM=VQOHuT;#j0a?QKk%h|zf#`bq_|j*YS1&sp#b?=AC!W5&K$K^%^B ziDR^}5;g73qD_&#e+bh*;t_ylh)zwCF+=Ey*gy9d1#oWs!}Z?Fc+h6+g##Iyrt%=W zS=0kjS!4=^&z^BVfpYtyw&m+0%cZE}@JBj0o#S|-k(il5z=6WZceYV!*!SXB{jXk5}LkG>)L|(goe=cP8&k z^G+xbqApMB4jY}w&0oW1yq9LuoP?iFJ)0SB~0yJy7FkY*##K`P*@ z%=BVmR@5p^LGkY&It4{}1aeADBuY%U!ajy0$XS+S*q!>?Hez-zZ@|z}%q-hR*)*BC zb`EE;!G;3q^AB?+0m9zBi6NvTZ~KeitGeIQ^?nWt_xH>`tK)w;T;%>d@E^7Ch^=?P zRA%|MDfD%qX<~dH#HwqYz{D^Gw7lK-1)AtdO>t-#&?xl^Xt*$K^Xn(u{I{CO#T!-= zTrPS;LPE7qbw2&llH7;&8}Hw0I$qRUQdJAgct05*S-4kP^{YL9OXaOv zF+qJ_e|`Vqj+}epbZl30n_uQI zvpfEqzC2eQolDijAP~U!|VDUV_Z`M^r%G&tIPIo90 zru*Rr;w8Ghzl*%T`$}@vg-7}xsn#`Wh1}DgG6A&E87=d6*4hGnTLDA1=3T{$ z{=Glhf-mQF3F}DQq za$uJuk9*0b(`i1Xo>LL9+{kmVWCP4E6!&FQpG{x(t6bpxWwo_w;k5C?U>Q-a8@s!f z`Q%@S@mKe|s#mLPvd&081aeXd1n)iL7JdNxVO1ND13)LxYm1n{(_uS z;^&~Zpy66nb_NxUVJLE83TS;V=g|x)x&F<^8w!6bhqHHl4pi&-Y+C$yZ(~~uC?_&L zXHIn_TY z@TF{mpX31NbFnBw-P)P&AkZ;mW|-+lNhErjdTvIXXuz1U^w4Ssh8mv*{BJ^&{gZM(@U41T?R)5BB_`;o$H0hK7>JUeNw7YOT| z&~jnj5r;2cILa%LRHdumb&MGOt7!DsHJsg;A7)yWy^iQRo7O7`NXVWEY}}YB=a*IH zfZV?V$j|EIib}*Ohj|3yhlgH3^alK-eRjC;WC+6QpJLXy-npHtymjic3YVjwq^c=k zoZE0W#&hzP^c^7&K^bc@bVkvcdcX`I%l$J}6JyJ`Y@(@YpvKg$zR@Jy$!JU3%}_UX zEIr|6Q=l@NYu@-}1HaR0h*3RTmy&p|KVA_ z!GUVY?RdI-s^L&0E zIyry5o!O&fTzwo=YHKM&ePGWq*;EBRrXwvDN^>dqHHk6x=za03iqEzh)rznOREK~7V2ur$7NE>gDUX>qHdcTT}@`A32a-u9%Y-_ zwmIWU^5{U2dEkc%Uph?L^~Zf8_Qszj$de7MjOsxDpq|Tn{GxpkR?|3H; zYV4+FOCi8=NP)7n&n*?s?ARB(7SJqVrquP=Ek>{u@w=Bt$n>$i-{G6v0oe!9GWUXO zF8{e_v#<(bH#mGaJY6YNCW>-x(5^S|7?`F)aT@22rx>CyIBKR1!S|DPbF#`WaNIEa zTIaNB8~OVzMWlM2(fs0txm(ZLb?9=|#A+5;J1FK!#+oV|PZ)U8i~?@bl4BV!35jhRq{(?ug)Xpx-F z=+dr%*J=^0K{TE2PVdBfn$hLXt;&o)dg;A&3h^KZXZ?(Z{Q1J2iHdQxW7uv^Ts%VH zK$;+3B;oSmPG)tfNDlaCHs~LS`j3YQ(VzE}N3_wvK+C$7vym-|uIvi@WRt#}YT%v4 zj`BS~-NLs^YKt{B8tckT?ZajU^GWq$a4X*;AQu7(oQC($*tH z5wX}}ip0+!*>1W~D*ZrDjeffGMO`hTU(c{G^N>NCY*j!c?5D1Mm!-}(N%@J&BO}Ct zQ4pO*6@+apal*xX^Xt)(K56tzJO-TGUr4;#`vg&bs5dMTvZIS(SYpM&!~v=U?p1G) zP;|XTQ$>%MGv@jh%*s)s>8$l`f3RIylxgEB-+9VQ^P~9f2)YMu5KN+^*#{3$nO|<> z{%q)SmP@Lhl-rWxRKxBxh_CcdGYIK{uP)zwQqr`!S^AUA&`VB&@2daTF5&il3!1H&5r98o5(FDVgAj56W z?x@vU3x)#T@{ti7^ef?Wly4> zWO?<;kwe256Tnb(3>xJtWfqY!!~Vs!>8=hsDD};dU1tX<*|oUsP%uZ~jxR@<7a9Ay zq(gY!TY0X@hzGp+v}dpq-PPU1fqPV6X=g)&vkZ84L-I?4w?-Y!{a^^18J{ji!Qj_3 zL=hW~lG|>+SbVGR;-O{Wp+Wg1YXz5w)w^Aba*BZpQLQl0F8d)a5qy$p-5{q^K(#xn z*J+Q>qc2yHOZ57cdN+gUIB?V?gVTiF5F^+XODp3)OpSXR3L>Vq(=0UF25(Q+RR#4W zxjN0%*}!0skre^d*9MJH3{%il1I>O_Lj zY{>`C)~R(nwS)FFWE8X11B&IIM{@Q3*G<8gIv8>*Bvn(ehNi&C^c=r}HB(Jek|*yK z%7>JlKgbj;4K^A2E|FOX-=TEBcwv}QxbLFu)yht*G73zXN9y(VNwpU~<#BC8Lqme~ z^!oPP#C5n(jhMRb=bR$`asOm<881U{6K+Fb;;zLbH`9^x7w)uNuuYL!7d3k87!hMY zmGl4X#Q$=Esb+?Tx^+ao%iYLLI)U{+?zx-66m!jG3>?uP^cmy2HNiVrhM#lrH?Jy% z*$*x_T+R90kfndCwEA5Qr2S};nITG-XNxV$hXpfi&O!fp6f;pKFUx9$?R96wSj|FD zyP%nrMKd6Z=Ge*Gy|D&b^JJ*0ln$P(D{l{p3v@*D@jwIF{;RA7?CnGA(ft+uUSAIq zLL=q4oBN6MhkG|zNfRWTS;{7tRpuk$qUK>b}0xjEQ4bfG+@;yBY!XBk_E7KjGR7%qvNtXrP za2c204<&{=bPQM(Cnr*3v~%~?i;A~*K~Qje`7UqS z{BQe1-y_0Xfmz%f_@hpZtZjUAz!65MzU19!c|(fosE##X)%>ROPO(WJDTFg84Co?qX*2@iCLCaMjyUhSV zo+)Oo`ed9+l1*Okhd}gxO~nS{I>hXQ@_`wCe>v(vFN1HsesjwsquSByi3|Gdu1GPDlvB&1PfNsC`RM+&_MxT~B z>PqE-U1iA!S5)BJ=g%ch5IQ5jDldM8IB%@dNCx86T=(G|s@cEF>H9;PCEp!U=5gN_ zWHWx+%!EC!Aw%7_TotLwxX(=9#}I!YLi&R!oD}u~5%x!I&g&(|Oyj(&OV3H8RmE?J z1pAmPeDdQ2#WwDe4Kp(aQiOW2lB-|EJmWN}FqgMT6iguA|NKCZE;CUhZ2N5Djx=A0 zKULUDre{2#)ruUa>%+YHgL8#u(+d>`SP}sc2j+0-7+3w;eo2*vMGXCp6WJijXbrJ!bq%{h%z{SGbWzf!Ci@Z3&`SIudAUy2Or&G{=(R zMR(g1{jfhvntF<&AvX=MdNLwdRz)%aDk?+7-Z$@{?EAS?H_EG94kKTdKf5#eq&-ke zQ+UtWSoLo}?*!QXj*JLu ze7V6giMs7DXB)Yzhj;!qrc$9aZdkO>)@eL&;33GwerWGuMyBAzXd$VvMbtY?`FI40 zr?ibjFk*KkxLhAzCa1k_j1Z_9e?^De#n@1^_c654cFg%f_s1d}%=W&y_$-YmmAKs} z4kuZYR)g0v9HCWDnTn7SJ0(76BtEy)Q`iUF}DEcL7h{^&I*=MfA(M`4KzA4XnR= zBJ-63vmgvdrxgbT`lcU+r~<#4A0N3dM543CUY<1TY*z>So8L9Tcl8{*Y)vb zl%UM~t$qiYgmWG}lgmw;+e_$WaNWmGJLo?zjjNNySiC@u{V~}eoS0{<+b3S*uB{V8 z(5%D0t10cY254k0Awq4eB5idLPSSZ7aXJfjChWtFcKY$g_>!PDugW&y6*3T-HTIh-ia7>@^Cr_CysSUS8byaM6@ZZ9EAP6MGTpn1a)Uzs`T;~vq?)|U#V)j zR!pH5FyJvNfK~sq_DHQ#Sm59O;;YWCiA(F85BQrq)-ZxIJFa z{#K@x8BzePd?N4N*YnayM3t)3D)MG>@4q3+|7NW~LNqk2k?yq{`pw;B1nXf|X_p<> z#%o=ps*%Fl;%a0}eg!TN?0SO-|PgD*=pFe(^;aGeJqst9fN5_M+eam4Xy^&9&*t^amp%{@p!K*dcpvC;B)jF1y@3jIp8)D9YZgv#~@xO2keJ=~pQlD^v0keTqkVLfDY*PC__JD&XuP zQ_(rvlWk}d+`Q4|7hy#=t~5~4uj!Qa=XR%s1e5gdb6q;95B{$Edcs%wIWd>pFTXo_$wiG-2T$NwpTn z4R*^^n>O(KMwZl+Gx=9;Ho07nZYt?hlG5~M-(!{))4c6;AL{f|xAwOKyU@=z%+=x; zIoYnMnL%DuageXm2(*HVVAnVlcji8ioE)t`(XpLW6>_Yu!|Ml~V#nV|X;u|qER$2< zyd{bSAHN50w+>T2XJNVQSs`xJP$sjXy5J&&9>XIfHbFBpQhr_VYMK%V6P; zWH)(0vGg*7zu#TH2@bJbPimIP^w^rMRT5IBi)a@Up)3pcP(wVk-ad8!b$XXp^ac=x`_xMb|8-9n?MCE4l&xxs1CMF@+_ z(?)d5XEqi()K2M6Dq%}JEG(=6l05qVwaLHRK~Pv`ylZI@V_;Vz5m4te2*zGFoktz{ zTzou5*(f=y`_iQbnoZb`7}iWW+!&EqnxRYzHzYR^!Th?GYbO%;O>^O>3hF&qm3Ig# zrwA&~qoI$TdU)Me9WjXj9gaWc77p4R)x3APnT6Ng1!#02`eHiWaMC+do#g~{Wfu>I z89yr@EJ*KvZXL33kmuBklvCD>@eOvzy=#{7ziJ`9n%Ib+d>fR``{?dvtoW@jqTJrj zXfpamNiJL|U7rPeb(GmLiit(wFlvLk;}rc>K%6&ZcKAJK!sBm33ZzzvQi17P_S7L= zR`pq~qmp2ksMT{0Tn;8i_*_2C6m( z@w*4Pn5ea(|HhGVswydZki;^L z>n$csG%W(3e%QIto&u`v((Yt!eMG)FoXC@)Bko(IDj{U@zGTgx>>B zwR!2^*vmfcYdn0+v^`oYnSwK{biIo;ck0EkFE|c9WKS=$d0V?5LNe2KZWi-DK?a)n zY(9OEB}R_T%Zlu3ML*!Pd}G`Y_ny|yFk39S?w3|fuACelYQx?$Tbl7Zh048F%%{Z` z>3M#=p5oZga?2Xg+sl+H*W`t?T{7dQTg&XQJaDkN!LQt=h0N^3!&}cbM2*Y^0@y0R z>mqv;lNn9J6Q1Kf(-wkb!9tBsWZmyS&=`aa_orOn@pT*U5BFS|?Hbtc9c=qLX=*c7 zkt>?0z>tX&q%WBDY-LWbtm1d1;U zukF!iuuf2ugI>A7Oc#C?iYtI_Y@E+Xi$^d_-??297Xuedd2svQ(?K0wQaOh0qa^Q! z4Z8v3;;q2#77CBv#Ife~?RA~I5iq`en3w7N^!w-NoOA*PGEihAm$X_xjru&k8xxfM z<6!Wc-M?g)|M=1W2}~6$EJ|cK$>>ZU*5B5OxhreAkJ*G1rgWRqZd)GRu{b;wj~nwe zRSol%#uKIlf`oqbDh@ws@hNpHD_~>T2JpN;;o{-X3YRQ^M=xKgB%fLoB_`I*?oGBj zrK4%)*f54nzZzDciAl7B;bqAFOt^e|Js6NI^!P)EC{);v{6|gCravHg#u7 zRcpt{nj`3Git~?SJ)%4r8hFoaj5jHYY#ktaDv*P4BkF2|J_y3&D`~*w-U4U4#TXMH zK<6JD@4Hyla$7*0R)uUTJeGauF)A}W-q+~)=WHv3o1G5lb9_I87~@46#`LZV#30Z*FI_E};?SSD9&I1SKM1IPW3#i9GAq23*?gOqGA{9EmKzL;2XfCk98RLE!B$;~ zxFtV1jMG!NGwIhFlu2q5!tu`Apttan5zC@4eS>BR*3~rRS8n=o;Jh^Dl;pH&E^0tB zr?l3UT0--nxJ7wFs}a?2BQMxhl{XmozoV)D-hdJH=kY^RWoP$5gV}IeQIFWO6`D@HT z*94Amnx*L!Gd;BtSI{x?+QD+Sjs@gDITHv(65v?V^Y}&doofJ()*eiDe*Z9>gTK*C z4VRg3MNpoAze&oV-j5XRlwsm)7&vE0v8MEy@aRFXZK>s-9z_hKEZzn`l;3~^a?R5m z{|VzF0xkhCghABgRA3@t3&9IWwQ+My?Fwv_Q8XFoZb1pKOnmN7Qa@qw>lfy)Dxb6W zI!RL{b=Q%n=|FAh1(;FC>`u2l(@#$MrS;WEnX#-tP^xLsPq@58+T6_3v?LeWSAzGh zzQ8)@fp=o_ThZaYto6XbW&iZUJlPa=37bYpdoY|{PcmV9Ne%Ps%FK$?wM;M&w@bepN zc;=tkDj9ud{+Ru&Ladbg6H$N1_&*Q3x>*NOk`($ta`OkRhqrm;e8YF_CQMYHtEG9V zkc5^4id<>WUsykQpGsK!&KUU!5u7!Go*C5PAN||Cd3FZ<=Ysuj_hx@`wh~{wj`Q~C zkPhe_=6Y0^Nm%hP31{sRDD=9-p*<0a~9mK)`Ic_N|$Jp2*{kj-gPUqcOBnQXD zy!PruMz4_5T|eDh!!!3BB69!4haC+&om;g6(yr8SpF*To^?l;XZvnTg1R*CE=-?Kl z*k^El*27Shz(QP1^UHDohX0;h&Dqw5F{3%8yl2JV8;OZEHFdn5N+{bg=;cwZuoRv= zPQ&jT;|BFhnf@tcoc*k18SOzPZcFneRY?fCgy0YoPl`hkw* zJy0Luy|==#+GOi|`9Z5pJj)5A%sFl_aT)((D-M%qnem_s_Z7v~gfWDJ-INthIgqt4 z=XcX8!^9Q(>S|)B)6%2LAcZ69na1}&(cS9Bs42S!SDD9HUS3nGo`r8uu4V1Hjd5E# zckMNIuV$r&(;iDmr>Nw%{>d_epX{XC%UQ1HRQPk_8Mjtug3QYaM<>A z2FK8(+EwtQ%NYcF8*I$zL<8(>EMdP9H*I#$iXcoEFLFTE8Bu4I-M{N->6U}U{Jr;B zki(Pk&!oZstg)MmRY2*K9(z;`{wRBT!|PHp1ox~1a@>E;!uQb?P~JEM2^RqgH9&qI zE7=--@FS^0Z19+}OViYh96<|prjv{5Eo1#GO`1967w~h@EkOaek)5pYhS*>QzxHJd z?k8G^hM2)#VyA6i8r6oF&pt4hFkuQs9)Nu&b#9IQ<-Li!kb3Vrz!xO5p<_CK=@${T z6y>dDR7SZVnf_0o-$$aWbk&Z`1>5rT#m<|~aBcWZ4~$Bd)GIGkP(M+L`@~idG&=tG zm)6Ye!(cBQ)6X8Hp9vyF+#mtbrzuGUZFDu~M9!D=E7|NBk)0Svnw!I|Bc*^V6l3*n z|2?BwT}S6y0P}47iWg}O`gI}8Z8X^Go~P-O)@2UsN)5YRXI)zdkJWU0-{a5yzwZ=q zY$gx_wkO@NR}OAA@=yr8TYcSS$pmBqmIh7AEWZvW$DH$5!9Tg4^|_&1_>C22<>|_h z@t?E&GDl1-n*wB@gUk8L{%Z=tTt1W0KoXJJ$KQ=~T}UYCbkg&HHLJep`qisylBsfl zd{GEJHQmH$91X$xg{kJ>w#T#QdT$bJ|K@q=El)n9|8nC0+fX9{SlY{{bha)HtG?Uz znU7ZLV~~|}LqDHncR3ua@M+3!+^7tFBoRPjutA-c>F^XEP1(JXLE=n(YX@Pn!~ug6 zF7u^B3p4w0H@8Cc$!53PAqh40<;BobbEvEgr9}SLH=rG2h}kjRwsnX_rJ)pLbCcS$ zv9!^W^TX|~Vc?l&Z9BgjkykK+&bzTmCQ%AWBAo81`1K0^2m2y?y)SSb6IzYe;u zjC=ECbMoB@7yr30$DeXVZqxpH=f0R}WwaLEDQlM6)DDDecEuh>q-Omg&F0Si#GSnb zaqlX3`Gn2y)%Bizt6%}qN9l!wY@53`q+Wtj#^R1ifz!bBdVp%Gn;#ARI{6T|`}kpK zi7Z#LG@}mKqdLfY2BBW|gnoJI*_Qa?U6ajD8imzC8xLk)C2sz==`Rksc%NMX!12-55&zjS|F{C+8r4XO zw)i1@z*W_m%MF9s+rPyz!xHBm4+(pz#~6S0TNgXG*TssrM?fNB&YMSwaxjBU$+(0K z!~E7gs*gNm$oc>m?oOuwT+ZHiryy{EZ(qoeOMl-(Cz!k;EnuN3?Y~t5S@vHqjZidc zhQ(y5Ou~XCJ@;L; z;Y=C*mq3iS?Vgr+jK7?@Nbzxb)NEcj#;zT0Kh)(3b|MTTTkL1oY(mf}ZWYe=6xjCp z=U|26N?D;$Q7NptRQ|J_DM=GcR(brE+Y-O*lnspNb+eDu!~`3t}9rFb z4Iu?HY>JpGAC|`!pu}CW=wKk(pU@9H7#~UqDgnet1_4T_4*)sRlF^6ucGd?43F2qn zca(0DsX#fGz`Ru0RYJqdm zmJDn@eJuP~ic>?w4;d$91UYGXNoWWX{AHXSCO8;TV1(#nn*8OcD>CV<^ehiycLb^) zTIuIlwv^w$6uWiOOt#!Tw8==ONcZ5Am>KPho$5 zeL8OU4%u*9n9&ln=+QQPW-9F?Du}6n*(0De+}{D>|8YBj(%HopbZvxbgO0vmz&bLQ+rvSB@$zRgR6crv> z5o0KSZ*119ShaD=MV@zdo3w25n9C(Jyi0#9ZcgX*t#va%3nl5juxF(TUP!fD!uQkZ z0uH_do}g!Qi#4_f0vphyy_vbS?+JM%lVRjo_>f<&XUYSsaBduRH{fQ2j3uBSXcmuh zII?%(8Je!fBcM`eU$wa!hJLFSAkVX(nCFPy@+D}I@8IMdojxES5;q6;V+J9z@Sw9T zTvjPcvD7{`IIDee*rZsqTycoO7S;Ihw&qgJ9nAMj0|s+ACz$KETR>211D8d*7Ik__ zQHA8^Iy8soMQk7DJbp;WAtmZVFXTcPhj>Nf_wW_tUOm`z;~SQt2M>t@($z1!acbLH zPg-R}5!)w=9;KfWZ}x@m4Wq)U>>lDE+`yNJ=xEa>_MdUGfff2jXF~c9T=~D&9(c)K zVzin#eE@?+vYXX>Uow+EJE0=p1kO>F^}QzjcDY@GB<+~hkPe_Y?|xJhsQ{YRz`jYm z{_o-ZLe>dM8cgd72wG+~vCxUGpV>#$A;IO3UP5raGZnFbEZLD|@=&?^pF+$CWeDH| z(4l0218TbR*!#7DLXXYw-}QE8%3x}ug-u1dU_r0-K@6fueO|II8@b4lG@uShLv$${ zpB*$(6$7H@Ne}mVqK!I4i~$yd!_^=aMVN{|>+had8+!i6@iy4~>HE8#Nw2a+^W%8v z2YKWt*1e{Q+qurCxdM@!Tn_!6C|xt~!9e0&K5V@P@ay&Ox^i+m50fUVjm^5MXSFT7 zJk{SfZ~1x>x~kMDsZwk5S8wq)1l0zve*uSw$X2x;Xa_?4P=-_qugRd#cH66xS`HQh z`M939`957!*ozp0hvnwwht z0#>4*RgY(^zBB#RUwN8jiL;Q17rN_F{qm7{3OWs_@u#qE~X+5T~AYtDDz!{wqUZb>5bKl^|ka2w`kdwqEO|s+qMTEt2EXb zA!vVRm8l)i@*+e2eS&( zs)RBjynU^soPz5l8+m+3QflWbDF9m!-68@pt#b#aHe!gh_|L-dF+l`u6g34V+vm4s8&zGhTz`R_Rt`jE&Q=x2^5R2m86eq2(j=CVa0= zG-PPL{cE%hWZmDQ9*~!mQ9;Tq0zs+U$ku8BJUIpi(g?yuqN8wPr=1oq;lZEurQZ!U z5w(qsq1DPxdH7Hv{*;O2-!_|*QUZ5C<#2`>xwCoX%`w=AL?OVWL zVvk277gnQ1KV;o<>qxVlI{iBFye{|!fz!fw+_KTi`<0_3OcU2?$8CJ%hooMVc{X7;dE_HxrJnd38GFF;qgW63 zA?perv^CdQy>Ux*BI%Bd*(UxrR?OTQQZPemD@C69VEr4(OaB!RC|XS(oV8y3-Gl$$ zL@=Jj1|2dNmws287W(%kQHmi~G+KYrAG*gphp5{)+jd+J5`b~|48{-R)QO>I)d*92 zBI#h9v`I`k5ZVG_tC7NTj=9tC%!LK4mBq(K#hMw9fxZQeZ4()sEVQdiX|OjTIXS5q zU(04{tZx0j`PP>&0Q}bHmEvpHHs1(<4xWWDd+MZf&k&i4xlm@Tu`-q>AiI^FHdl7x4IHm zl()w#5XB{OB@hqqy`BrJ@ob^SghLdpC*@rWG;l6nhuG;w@?2s}lMfdITYlW+P_(Iv zjgj`!PP6Y#AMPQx#{2>L0gkB7*Ig=_o32*F$DLo`R~(0{9SWc^{og49=vZ00ukYon zW8zGJ%w0&ebnM-70{L2?V-d_sRqDR~j|KeoSjo!XRiT-NZ86B_tTQe~=TUG;#?Cnj z{(-(vR9~4@CAy8M7xmk`=~FbKBg6B$Y|w*z5_gN}kX@?Gxm^^_rz(0MJ@_XGr?euj zY=-J zpH;&S{B818IR7S1-jX2gLx>ZM5jK10p5%^23ym`B z0r(VbtYNv2J39a^xoY&F71Uhq+D|4(fITo>_#bFzjLuNxtXPMBLz*crSvD#zQgVL^ z0JY2)g(-juiDqnpabl99gjZYmGjxiD`ur*X?Rtiu6*mL>skmmN@QBqgIH7!Sjf}FN zM~YRZYkPZ_m?R8`9pn+d6u*^VBj^}^-W*=jEvcf0r|A z*U&hkkM!eA|M0YO3RcVCFoxPxmEG^cnE1Xn%Wp)Vx3F7fXPNf>&cSkpT_*k$S~yYx zAg3;%JdQx%|3lVShc(%TeG8(1h?LTefC@-SNhu;BB?jFoGD@Tw6KN?yLL8wW806?4 zjglfUdi3alFa{fA-#wms-|zj$<5oO$yY4H_^H&#YU(R9gvH8sM*Guc#G=|1zG_pqO zn+nLuyD1!@K^?^#=kVbN`Ge!KGtOaBLV-x^gH~b_zrfHL-rMn8eeOyoU&h^QEA>rX zWl@&-gSxe2^2m^M(7 z2o_1cQ1!}^EfF&_rHr$Pkfp=-P0oAYJGvE&97^;N^xCO4i0Y-8@%@Q{4q6!Dsgj(l z24UGVLFgiQvdxi6SpwxH?wuU!$ujgK+?9~@vD+(+=$i?&T*{ARg^LdqrkmstV^`_s zDp*UHR^kTfF!|k+EEBgmMF<8{S1CS0h|1oZ{(p`n%_A}9s+)>@y&8TiFE@&aY(;S}Ng%BR`VBjEt%dTr>W5E*^* zGcm!H_U31XEFzM)l!kBDxlaV~@841A@y-US!mib$cqZ=1*^|j^$eX#OA z+)T?#V7ph~;$ z=^}Gh)EpZvuti6d{bJ?Um&HpHe(PPda>h988MG?Rycx{LOzc`(T}2i1L1~SHBK~-k zhJVHu0)XV(IbU=L-~4Wh%rmv{T@AtMcE0H6ESzZk?c8<8Q~B8a8AUUy2Vu9k^*Euv zt*TXFZSW1t8U(F2o3@DZmHr)tU$a71+7rvuBO=wGaL(qAgc^!xHJkIPM zcbhR&{k``pTE!I@@oqnAjHnVoH| zaR{)5oxXtYdH)AQ#+_m!#ADt4!(83P`=X+Vl8{Y6_&5EMexo__S=Zr&z&$R^W~d7@ zN#1vRlIO+g7PByWo=_b_#3BJGg&6i1Zk7tbuj=G&f9&CVe$y?Qr;R}R_Sm5N;EndK zx1`ebv@3|`1GYs?0CGF+=YeXq?q%gHx|ukWiq&0t9*)v`c5ks1bpxkZ9fZNfP^Asd^j@>AG2UfkiGX zQRTQrl@9Lpd9!wc%n*|xY|$=}2C&o}t#1uL`w)P8dK)bocFs8=i__uIMbzD5JM!VOP&Yq~n5l zUmPDzu|rrWeT(z;D!j((Ih1^-M`K43%_~W!=v`uquuSxdn690n_>4Fxu^Dlxp+s0t z(Dj>l@7Rc+!8Bd?LHp-(2Lw4w!L@trD$rwyEAtwf2_O6ve|-AFLuwKKzVJW)Yu!ss zHWPDB`;T>d&>!)cLkKPd;k&kRnAVWad%kJsMb5@dliywJiKuVnto#CYDGt#5#(*w} zZ+QdFVlBXl-$=7h?6#YcZaKMl@R`()i&0cf>XM2}yX!;s3m$c${UVl{Qv^~6OrL8- zqVq2_wD`K`L*L^vpvU5>el;P1Q^kiXV5_2Lpd;jCAZcM}2FAZJ7-wDw{1-u%(ya3S zEwN9GydMfoMy^g4+YJ>5+2w77gfy7nxX7ivaQ{yFqnvaminF}0Y91!ok^bwtGKhUF zU3p{rstNo$^Si_nJz?PWW8japW*0yB{;)0`@E@9=ox%4EY(c!fa40J*gh_AJ?*#Me z!s*|$dgZmDOy8Gz_4Z7>UiMRbD4|m}6>C&#w!A`q0Da%M4dIB-R$?F&Tt5uZqz|0y zU$g?@jNYE%ouR{()yGhi-GKdSTO4g+CY4O&i_y_Fs*R&Oa%5X(KNyEKHm8z#Zb9q% z904R0e$zxG*pEI=mpm3Y5uZP}Fcb&-(C>l}esryv7<185$&N=|?e4mDB}ay`zF9yvzr=1K=>N2(lu` zdN?F;tjz+IjJ$hb9&NKU>zw07LKOj72dZSyDIryzwdfrTE1pu-9cW3lk_R5c0M-rS zhj8xk%Z3L)okfCiFQfRJt;+>8Y)wgO|$T2q>AmF26HRq7ZSMQQ4 zqba&`Yf;D8Ut#VyAF?DK={|2&&X!Cka0!uy{b=6mU-0dk=4n+5{PDl-UB7)N0bAFc za7BfB2f$)#JI{EHZ`#72iq?~ON4nRYjujAF01F8be^Sfx8mzmCCH3DNiUDnU$VP4+ z2(~CO%9$>#GavaO!z<4t%M+nO7$-K7dibE_;&_tN3zg$NYzV=P7lNT+S>a#~&PAWG zqx1NZBHcjFsGTp>i!A?@}>_%(4b0K(I0?9mMFBAE$}bB+lb7US?^tW76)*; zhXlL%kN{kQa(ffc<9x|4y_cH2KKG?XbBzkR-;d8g;+Mb;^$E0YX{$F#st-cyRb3%mOhq;i(^1bw5(=ePq zW3I9TCVP@6l5q*yNe%1O~jgW5%aGaRTEA9NTQv7tFs!H}?>_<_RY>wJk{D9yx1Cqyo&;L=R zWSJ4wg_{F?OK*mZaTcSBWAe4W)%`4QoT|~C$2mbl%xg%lsR%Ty)m~lYpu3nG4!t9j zi2FEy3jmal!e2yBARYi}2SRW+*&;{pWD_$u>SYm99<{_8mqZi}#b$jBz2BpPqI&Y zB!wBszO%a>u5cQwXomJf61N{9s)2K+vkj26kVs472Y;W1P6kCDdD#VQEfdWwaX}xB zTKeO_IjA2TG@q{mxwJb4_`9d5o(UAV1+iY6A}k5Pk@kQ%5-_cx455n03Sr#|-6-A# z@K<`8AWbyW7o?BCto5K*I9fq}^Q2N0(vePUzp%Er`ZMY3%52x&jKBpXjM59U=~zyn zsLUvdl{`?w>T+7cL+sb4xlWXNyb@}9bwMmvH7q_%anV`GMn=GF)`*%r& zwY;}8&=P!;8=T#|QUPYIri_K<&s(g7)we6(PwD24KrGaiD^$zEXza@|?i)8MC*Gub zPuZVV>t1sIXe>Gre^+X3x~UzCEEV+1mvsim~Zk{%1Z{!C_X!7fws@p3SmNR z*~sE4Vol#V_!I0~ohVotwSI>2%D$fYK4kWt(so1QM};BIdIG({+Y*mkrJ+t0eO2=5 zwhq}>Kw^js^tS}m2~)Y~(bulw)lUnL_r}eIgLyW{f}~Zw&a5PTHi)YLn6o>gTkEwvExFb=CfjE{^e?p(UaM`%(O`HP^&S>XL{R!lP9b#0%xp4E?FFAD ztL*1@9HMA+R3iKX^L=`p!_Pd}v>|KTm$7WlzbyZuB(bWCHKX^}L7!`2wwfq?>Kr0a zRTEf1cfV2)4k$Omt8{s3NoH4yS z9ezWOJOwv(v~Oe`i9Xvk$WD#7-Kd&H3jO{&cq&l>rdTYk1mT z*MyiLIwHr`T&Ti~0mHJ1-Cfe2_V0=C*Hd(7+Z#OE`z+MBeh7J#dS6hTC$-qJq&}~2${b867%`$r*#(N(p56FwIMWoZ{Z0pzpDEM7W(MrOmsae)+AmJ8Sv0S#rgdPj&<|!caCF`Lulw{@-C{j;$GyBq zjI3nca8j0x{lc*aEq75s&uO*N(O>SIx0f!H;2G@1LqUHJ^(Cfz!odThv)~N$XB*2Btuhz!K2(MA z;^ZoZ0pa3C$GMX`EJ0XyGp2(zVN=fyNp(p`$kKLG@7rhh$yy9bH$Ob);CPg9fcFAlih;*f zS0S8n!Ti)M=M?pKBxI{`ZB=R2V2=o7`glJM@dNcWscs)G4T!q}iEGl@)}eLO5kH~I zW48iic}nhTsU&89)dzey#R@d)0|F<6llxBi_-NTn_cy>2I4j;yQMNg5=IGSPi`{+L zIB*(}+b@0P#AvCZexqG4gqw=L8kz2r?88{)g0yV*KwCcRaZU6`D#=c>2$Y-(BFYhIT2k@_rG#)#O@~$}8_9nAN7>UeKcl2aeqGz)cN+4qI=* zN=TLPxlbi8#F|iDZeL9&JZtO=+f+WNaGn^B-AWH%?Vj}j<&aBy+s<@UcLs$H$W$U+ z^eN0c8(RoLboFDI1fTP}W<~efaUkkT{?%0+176QO#_pM?`l^9uO|i^0TB7_xB46c7 z^*~L;d=)>157ujR|81QP-pDN~VRf$KW0Wds-FK)B9T13_jVFolh90r2^5?(kB4Yu!8U+ZkA}gjKwKLG{L4|5|ONKykwpmIQIv z;YqsA75e^rb8+>Jb5=_dodL3M=FEPwMLnjS*F#eztmk>3TV>hjJ2Ry_u-HPpbA?)+V0p|DjoMok;!#QP0c@9eNj%^U93&#gA6{AFVK|hmy zE<_gXz(Lok@MFDye0a}gVyOazWKwXipirU5>?P~~*c>^r$Pj1aTF8w501nKP3DPipb33m#uGT|IC$MPn*W-K;jY zh{3_m&ZF>ONacPDRhPEGS2K#N{@xuva8D0P%Z*d&yR)xpA4)<(4A_^LHk7#Vt>aIk z6JPa5%@^pF+zTeJJ5RAReDa~$Qn85@+(bRLPL{*!cCj(wxP-uLp(Xn5&Z@y5s3^SQ zr*KS(Z~(%>L?2OP>d@bAv8%GZ>Zh3>?ySRg$|0EC5Y;69#|3vZB^;l2(T?@R)xqhG zed80KjW_V+^RlHcx!ka*H2BoXM^}2lk9SdUN=8yn1ioA&B)?N5Xj9G z-q_OFss`jxOz&`LGCDiW=Qo?&QuUJ0FyBZ-&)vpNn)J6M+Gp|TwXb0M-}DcN;5ng za^P%N4X<#cwg5`>!}v)FzJRW1dhnPT$L&Cev0Ak$#97yyoWegDO!898W9)gx{8qnK zS4F-8f>Ox6{M8*mw1-Er0|uo}Imehkp6Qd6y&D|4CsNhE`=FKPbRTzNzY^(MP>To3 z=Hse_(VrMPMDQB#h{m((of(Ub3WtY#qwW>G{rM#t-IKb{xtxo^#ZO8YptAPd?BDXz zc4dQ#FeAuoz?e^L4UQJLe2>qcJsGT_TD8M%XNW|b__|N5Z^&_Ymr~yj3Hl56eJ^r6 z+o_s7D-mf;fi%c`?|bh>PR^hBL*;Ges}%)2An=JHJ5D?AAT^o(`>;D2lorWUioT^e`E(7cH%)dOKDoSG%ch*5@!tD z|M25B^E@E_g{ms_T=1xy3*calwS5K$ievSjs z&!08_K;JYTTXeDXu=$&X7QJ=~`9S2RqsbyM31_ z#66@)gz16f@wh^7agKy{O4v>{mt)*4wPMTGF$u@thHpUQOrnGkhJI>RJ~qC&DocV=SAUhV9fjvzNlVY|YyEo1V#Bkec% z=C;7m=j%FJ6sKy-SeCu^qS$TJ7rt*|2mundy!KcisdOb$Ro`xk6t;o?gbXb|bPWa% zd93#j;p0J!7dLFfI8Nez$+h7R^XmB_V`W&K%e~?It-{dQV<8ZTf>FJ@Ii4_^c=xp} z4gv&(h3s*Z_M?n-@Z;o=NnrDvuKGc=v)306uKw(|;@WfLu@3Znh|*M%=4%%ni*UN= zJvKTC)#5_#eYXk!iD?!bXq?xym^g0|HLLWFa5op35%Y?+#UO8~CFi@IRg;%XNuZ{|4ksrMFgFiICs3$<*y|CCVT$scbQaO&s;*Bn=|A4e9(y#c%jxmhU1!1dqa$Dm zUyDGTw*(3U3qZMQiS{0>g<)^ry1$gIU;^9<-DVMuAv?fnauR*UUG+^aTI9V~{vCMX zj&8PxVrtPR=IBJbSurQ^OncXNtY0M(9{l%3o;bUFK6e@$YogBk+cBd+Sm*T_B`kW< z^xzEB&G&$sLRH`aLz;p$BX>(u+WDL#*C z{)`%)Z7?5i^l^ypd`};z5bC`V&55a!Z!gZSq%ETBJlD5(YN*4xoEch?)!}W4w+vDH;ewY^y z#NB)DGTbv{Eh5J`8cTZ^RG(7$<2bjm`Cu)zeD^~%cwCP0k6}87mdCW#a1^9Bv0DT9 zQDl0zUFa=qU=smx0^wD|PuZj5;fsoNsiQg0IyiHec$mtL;ES7!D5Ycz)P!6nWloFY zZRWK0=1()A;>QEW%@wHmfukb2fiKqcCYm~G$7^+;+*E>6zKuo!1NA=|g}+|^5D3SE zA4_J;MIMhB5tUx<|0u%#hVgDDI>Fy=ag-}Bram*rd_+2{0&eY29I9t&w6-K|B>QA7 zfHBwkIQmawHhYHSSq|=Of==HzBDg9h`*4kY&X3Oh3z9?!kpW&o(r<5(iL-Mn6tq$5_Z&Q8 ze$ktpbu;YUh_kIbFVx!z93AxD)8DILm6-YUx)q1sLi@0+Zu*1{d!M>sc` z?D;>FB{~36x2a)}P}}Aj_S3`VSnTR<|KzU>N)GI*rpe&5Z~dY7D&)@l-Y1aCIF>$x z_Vcpqb7no*-TjQuM83@4F!h&}gBrDFQ^HAH(1+J`6t=l|UuKVQE`v`pq%67(OZ@yY zFHVGyxw5lQWW61 zz5Js&Gb#)G8nDZgoz(76!f@@o@6v3`@2jtrETwClmURPcG3#fHG73Oi|N~wz^CJw&W)4>Lp6pyQYwQp)5OZO31=|m zk?>$gBz@TLqeC5LH_0|Wl%?}F z`4)#{$1u^@7G1qxR`oDuKx``6%S!&RPIbB|jwx zgk!{^ZR1)-G$8rR!?=!LbVA)AZ@aNBspRhdb6qUB&XwOGwJ|T4YuddJf*68=qcD{U zzJJ1NoS)hE9L9$d=g%6`?I#7S=>wWkuekUm7gf746W57N1olC|Yt^xLZbF}xWnJ`x zeU<;%7K;|5$#PzF9ZBD2@vZ#;`di!b&l~t_Uvat{*EQGr zTBPx%Fn2gBig8dPbaPR-C$8u`I~kYIP2?Imje$rG2pi5j{5MRfp2Gn zB3>^Q)JT9A_Z)cOt;qJF;44<^P{&@0>@VEG7NhyGRsuH1nqJd-VlcT%1L^wY33EzGX%2m(c-vH%Lv}hg!pUe)rM8gOxHSboY7+v)28O$2$QdszQYFv;C^l}?~ELzi6+N?99@?(K8ugSyR-pIu-1kdAY z$qLd@E<555yxTxP>hKM8@8-cJaHR%tcs3E=27DF^PH9idNu^C{rW=9*ISj7r-I_dI z+Giv4>Mltt>H{v52}sw}$7JN=|H`-jo}NU35q~=(LK16LnKvOqv-Tvb#ejg(4KDFc zq_SXtXxXLMnV(bk?vkc1e|Z|kn}i3fK&a6M3h+VcKF1m(GfK@Gx@xOOd?jS4T_Rie11YxAC{Y zo^*QF0XDAFoo9vOl12y3kI1->p^j(K!NJ8Wi1V2y^51`S#P6J4z8?Ga3?0+mJ72RA z4jr<#m)2!kY0nDF@ZJi2OYZh44UV^tyRWd&*N<8&0nJDgkV7#AJxTd1acyjZkNIx_B>C@EV<>y7}g~C=#pvo)5_yHuF~Gw zd|lrjkSppdN$Elq!P{%D0)@27L-7NhSWVGe<53uA*W@}!Wl3(FrQWs9t}YW0?F$t< zPQI(&EkaI(JJQM3g`^otq5ZbjiQbQ=1OsqCI|%pQtOnT4y5_Ev*woHhxJE5YH|Y{# zV9f=G(NQCrp>rfJG83mUj*IO=@}e{~PjS9_eN~&ntg~Mk{Slwh(Lq3ErD$J4xl>J| z*aU0Ts-lm;cH;Zn&BaI0E0{&8ci*Hnz)efXb{8ACfb?B!=v7Twb7re`@AzLNi)s`# z)AI-N^r@eC00tEw^2-AkI#^@`cgA_EDRXVeX*BVvue-wgZj=}9-m4_Ai067~Gs z9}FRapo=tIkL?z}kGNOhWq7KuYB(>n(>2PfOXCe)tx%@ z-}C#Gpwq>ezfDNMSB9}3)&I5mc$2sKbQ>tcvLuQ`e4hTbCQS-^rTNVf zeI~96!slhd=b7Ly_nLuf+LVn#5CGDyD$splQ)6l(?z4%<(T~Y_0;)?nwS{O^Ky1j+cr^^4sWpx4Kemg*P_b*RaAoC{D@@{eEAP^-Yr z+y?N6i=wvjCr^SCj+=#(UMK?&w)q@bVe$OhlQu6%(MXi1eV&0G7g)CbL9;fa?3{TX zPu1}WC!oWbj!zbPmNUxo*D-lITmSn-oxlsS0;>FO*O-;Cl2(Ahc3)_ZwSxbZxqdo* zZqfZ+tS`O#8o*4&@_Fq>%-Q#^?)LViR>&R;@Jo1tI%4l#c3#jUnzBLh)?)ri9z71D zOk+{J%H-q$jz@~1;hqtG{)96}HV@{)WWT^0vrJWHjAjqZ2L=mSU~d`^)YPD+P}98> zgbHe}?5Oa%z5j^wq}E`-WOa(b{GI#&^2~mvKVuICrv|wPPo!+$C@K1$%Vkd0Dn)Pl zG^iS16i}8`gz(yfpH@7bZ6xJvF3dXo8BPS3^H8*2&O~tH{I=rW(KG?^e|yyLv|d$C zhj%i7ug09wE|}yA8IcKFey;P^=%_PekHVWl6go8TpuE1W=d3Tn&4K(f?aUzp)-driK#rGq_f{|YDL1E6#w&&;|XLdaYhAp;(^~vc;xxqh4ec(a;fjE1FJTENuSY1srg?Aqn zjW;e~mW|#eGyJ#g|Hl(-`j=JBQCO|eLen_*N$kzY|4!VmS*535K_I7n{Fet5w>PIO zVqC17AGm(q+yLyIpSIiY+~EBwDFh*fdbq(kVuqZtDcLjIIHIYuBIyudwS1lzhl&9m zQq0OLWriab$?^7Z`gA71tSO66a?t2Q^)jqaGs39UX+;ij}2KknB}v#!%2mdsm~gtkCtjLq$|$OOtKx_Pr}z95i)k zELs{pRzq^4ZXGn4dD7euJie+?wi^WxW5A%4H#H$**ZGSI`7;O6$dgBeOa!2Bv4%XW zQNkx=-=1d@|D$jweBoY$sRCr1Mll&TiRj?R&MTFDO5M|zXDOlHOc-yDN2nN#v2^LN z%?SKudncVFSbnWHh*dE8z~XV;w1@e%mD^*1CJ%fWfS>)n9_?U;z)}Qkmg)4Th1_2! zi_&RK_*r0>HgPQ;%21 z`r+(FGocbD%%JmbkV6LX&$7VgIiX++uB^k&S7l)M&iEEr2BhvyA(R$6 zrvtH@7|d_Kb&YE!gn7+(d*gBI_mPk6m*p3{QoGnE1(c1{yMPV6;|c>`1Di+5{liiL zdgIu0>-twwzC}ohLRu!#y!f#ce;XP3L|CiEN~{zhY}49oxLs}yqzhiz$W`uoqs3v< z`;9~H*p-3~rYX-7U&N7@R`JbSr3(1S;`Df=n-ajXWqC^Vb>z&_D=4D^n&?_ZU=akhWFT+DDb~#U-9hJU`EUjgmdG^bYtF=!6XP>qcGF zgez(q#HWlJZ+NUbwH!8;jek1pxQOzzkw5Y0nn?%|=1B;VV_skPP;#!39&_S5;=q%_ zfn)ok4(_w6h(^sE#{cx#|K*-SgCF9W5m}IP3*=rHGc12#@ zSUgb|%u(HyzgoW-a&Hw?n(Ep?t&U<+Wt`nJj~kI1GutwXcbdu{s`{%S`tOMO`-u-b zli;Z~(mDt^0iy8%dg(IBZTxeCc-T+~pnsz0LiVg@w5$xkXf$arvXg{8DS-uL&#V-9 zOz$j=fqEPSu95d!y)7Z|S)aG{nYDknOwwDi@!gzy8M9-8eFqIDIsEFrL38jgs6zN! zQZ+Yfw)ZfPvYo^0E|l_=)HRcqJO{AIbHzat%Na00Ffl`8D6pPzh-iTdSn2ltm>lWR z*f_CNY<$b=`%z``Qtv~aEwynuivBVVyXh23h8OF(mK2k(^a`U(O>?x*w2Zgw6~)EK zHriOeR9SGVY?rnRP3<_tCbAVJ+nNy9Dtl;$1i;Aw_Ote%Gl!1G%~TU!T5IM>3H`~+ z7FqwK0u>2ApeL*GY4=}MbsL>byj7$#+0H%UydhQSVN1wtXDw?dWl~^aJbkbZ*ljGmQ}2u}CM6Fi7(-mVv`emEkK5g1x(ZGwr^hxfyZOyC7CNb%66`B4BYyYQ6Pq63vvG3yF4+iqWP(xO*q`1U0(K)P{`Y=ZQIrT42PL1 z=WIBefzFeg7UF8rD|yIGpepL%U$Mf(>ZK+tR~1=g1c3+9DGPnNP8ntcr5gD-j!xiC z224=JR1NM%m9+u1Dz200^bJPDj5{?yJ2<2b*)YrwdRSF$xWQhMZ!xPhQZ95O3sMS* znSocWbFT9TX=(cLHj^=+2YxtD*)>o9K&*qOp9cSkrA-!N*wTshvaAbe&CyJrXP_q>UnEHKQ-v6)L$K2oDaDF zP4{z2hO+S5u)kA`No%WJ3>{9=)%kzNHhFRSr;IwSc6a00WYTaLyeCZURDW z6K5%DO6&=dZ=lK*9p!pU4UfPeA;-m=Z8KE)7cOv-)lvB$nm&I%<)0;!{ZR~_X!`HViM1SHKLn$CT$!Vz&{Tez|QvNF`kce zSd8e@2iO$dj9=q2a{6$2-fk9QQ5Ql$an~!w?RQ?!CV>HH7bW*TDi=u=EwHU}+?DjX zRMyz`1v#{Eb|&cEi|7=O1Q3V&TuDApfG4-NjoNbcO$L08SNYARIsXi<#r}O;Lerqi zJHwmRa`pz;ybtusm8mPUvdZ?9h23N9ZEFCbBHimAkdyJC`qqMsN%Yk9er2F(Q0eLz zuK)odGFtArXF;Z-6H0Eqfb&ZPKdXfp43?@cvk-*7OX25Kg?#A_w{)|deE)l+8Q%a6 zR-AVpol2}TRm-LR>-ZbFCq|A;YxzF3hk69b8yPNjsan{m#~fwH(>AAOq=N1=hXWD)C>0fiFNGkVFor!?`7B1#^j$bl*UTs z6m|Ct@S81r+bXj@WgW1Gyb6_$Rv8mxM>CVUbABy#h- za^fFtK?3wuoyHv-F5R7}b--gEg{I?(m9D6X7En9R0D)kSd~ewj44mTmM5Ab3>~9B(e`e;x zJvVtMmZQ%fN={Fe&Ah_mXhTl~#yY zS(AjxsIhi#NmOnhbpJizey;TIto{EEA$4vlFD(4gHEIe=Q9!#XO+5toiS`qR z9xr>59Xe}+5)5h{DYOf7LtJvi+r6`2HL%2skKV5X2%tknXK;g28e-Uxjd5o019q5x z@U_Q;ol*3f7WSR5JkjWe{#8+ET-{R6N>%5`&WG~Uu6NEH$(6=8?7?|@A{>*6lBkJ3 zT!u_ZWpQ8j<4|!`We?MXVvT1tFN0E7>KR8t=SNr$ntq!-z*Zy$R0KRo{0}RNWjgSF zsa*FnD{-4)ry(1Ooo{;Gc>ym0xW$s2q^erv3zfA@sHtKeL%f^{?6RggT!4@fZ~YN1 zW#AsCtROn4l9AioL!1sCq-h)-6nl~tWPvF7(#stIE)BFj$ftO{!$qgOaQ;%`3VZ0T zZwbLoE^tAN_yPAkpEZ^1TI2Tp!T2GjFEhX9?cEj1-**jUO2~EIUI9MfjorUN@4tsw zC$lnoud2}NBR^EqP1;Q>aPj$vv?pL8_4tuZpXM+Y8KR?#?=LXQ%m?5i zsC)sWyu<^cj{uHFIA7h~w_+=*1~K}P(ve7(Ux@>BFxp?uy4QE4FNc<1Ve%C|Ozkf- z66P>7Z&xQPy=;Mqf;N7=ZEq6w7LTKK)Fz{+_Xy&>lpEh&*YD##@!86#nKoB--E| zHt*9xIjFQTfEOOzIihK9)p9bbW~yMvd;Su0u*0)j-mv8Zb&thzR)(-$>)7Mtaam=aN|!z60Zp-RUullz zewpf@Z)m7UH^g+)Ps%i{ls&b+Zq;U|`xOG{nxxS4W*6`ZUTe9Dozxq2h-QrK>Z+|O zH>kIfR&&uNq|NK!MQpd*cESo7Tm;FFn(5LGIyUR)9W`O}T% z*&p92XDsGsFMwTKT)ezHg|9XSfgkjM#Tt4V-)? z=B9Rz=8>D5&Drm6$LD2PYg9Qsq@1KdR2;F?agG;{&)a(Y2VM+$j9&7AxBBicQNt(F z&*+BGUAE%bUs%$ZwRlRyU7XM3%{Tpd`e`%rowq%^8c56=xDP)S^UOp z$f-8jeClw+C$)6{4`OOjxj>2$8$fpaRRzk# zaTdna6m)Q8uksTQ1c}wE_?R`e1G%qEl7Uv{`0@S)HTqmSCdMUil|sFtucSjh-1R3QfVERro0r7tEA_2Bqcwh`%-eb=a5kEaAGm|eDuwt=^hcru zJpNnZ3TBMi9CV^Om=0fQc2Jb$6I?m)A7@pg@yS1ecf zDnl~eT?=j)At64|SQ)au2R$+6U-e5|+mIs!OL_q4jLF}`Q25s$B=nN@OsrAd$wSUL zaY@|K*)K5U$PRA)$Jo?qaSOzR-0!U+cmvU}KhKZW{Q}Z}AuIK@zpK6=Gc#d7s|~5% ze;z1`Z*gg44RBe!2G;V390Y-M8oVtSW&W ziJ!oF0p#B$=u>Ey;;IDMyYJMp9b;PGES9Jj6TU{QLA^e@^!C(R1Ig9FT%VfUntzYn zI5UW|36n%@fw)U=r3aLaQ5{ysFF6vT*R#=g9_Bpc{ZJ9O!%>d#5XA>CO_Z~XC3$nR zn=ZQa84a@`&kg6>*ixTf=l4(1boxTn33G{^QMA!AeD4$JjaP?-s>4jk!)x1XLw96U z%)^J8{INfotVXyDUqVhq@V&wHBT+5bhx;K`oNaF&^}WUvGejf0D7g~^6AZ`Mj`mcw zI(m0w1#YSYuB$IB@<32=s-;8nG0iFOO;KrXF=vAD?TLZrxEJ62Uo5<-2>%w1&`z{p zsEBtw@t^g$E=Wezo0YiovZZsuF3!t)jTLN@9qoDmBLk9~H)7T^fv`x%bX%L=%4l%Y zW$6Xk)tXM1C+&<}tVM!jMg78d_&CRFS5Tpr_}pgcu?OA{GVhzftts< zGea1x|Lygoaq~5PnH{C9gaP*$blB%@Wly9HaErsX+5Qyw*nK#9Vu~p(F9EpsDh9px zxo6VKy&-XKs!6@PMV^fhOEdm%_+%$b`DRL1y;r;NGtQ7Of&}&`h|*RO2p!TsxMiX^ zt@tT8fAt!%u%=(|6m zg+N>klD1OG3E{DMVqG;zlDm>pdoD!L)$gHZea=!^=`$gRJ9t=AkGkoHc&9&&o={+oH2;Lm#!iayKY<%KMIptYWTKL2r zlZV`0{N8yf05ip@o3?TWstYs?gLlUc<qZheP6?z>8czE0MY99Myg#L{F{qFRG8(`UsyiTn8`-=xV1() z330S4S$=cVmzvxEoNWK?7Z`>vc8dmf{7%fd7NCY=d=E`*W?Z3^^AZeR%uGK>oRW-{ zoA)^0JFHWj_!7f}TGb=ecG;!(v7gj#_U$LWHkwRorGF$&K)Si2zPBqW2V*SSQEA!st_-|0T zMhTP|(LJ6X0<(%d1YbB8ve1_9L2Xo65c{;|TsA|gHaqFMrVahArzk8DNPjBz zI9#1yvZ8Wyqhr7ZZ)T6YTmv_hw$#m^P!=Zo$|DCV7WZ9_w<;XB0LexLI$}|U&Hh%l zCz=)yxy{oc&CQ(&ot?6;Rv_w6lSCPCx=Q+ecgZOm-ZP?bN_FIoc;Q0J?Fb*^p?hUj z3YUxJ#!CAIvGQxNnErz|yST|O#um>|MXl}{p`{?>!*YCb>e|UJD!^7vi->*HM&q1`vl4W+==k0m`#-)t%3!Ckx zlZSJ$gBX;rY5I&js@)KORBK22`FFjt?Erp#6Y4?m9J`RO6-(1c{a~Y5Q_BbL#3IOB zHbWfXGqsuGP;j24XEXIkwf95EV|`Uy_LBEi@(OT??7ml4I5}nTby=3?GC{v(Jg)tW zz9+x$u|`~IX(B2f`iEXna*0lFbG!B?@|wcVkBs6C1=Fq4t-RqCvEoNiEcyL_yM)n8 zp07Jh`(4Cmm-bC}B=1o?wJ9;&UJ#S}v=4(Z8b=+9F&B`=wx%}0UZs1B(!2Eixh02k zq#Il9S*3(ggN3qd`47Cgrhj3^r`AGmHunCt4T>+ULqKXHAkNbz23o+}zXpIBaiZ~G z17$WozlL4=?2rUjYLk1Ii&Zi2+gmvGd~zF@rw|8N#|#Wc{~pXDCu`z6aoa{z01Qhg zphQRH2vj-xf7=nz%v4nEpD=y~HI^dQu-?1A*9bdd%i+I@YM771tc zW4&pEt?sSdj&_MzF5(rffND-@m7k;bFv(V~bJB4JJnqv&$+N8EQvP0&+*>@<>0^3} zAH0DU-wj8RI+#^)sa1CE<`&aLgfeP?2dMhL!mIIYx9)#}I$+=Tr$IxxH9b>n%j$+7 ze4X>YK=E&Els>wE7Dro`%j__636I3Q_n&=MPKzTpJI={ka1XpQ+xGgY8h6M3qvn2L zaPb>@7N_MvWAOk@`f`3NfVB0jkd)n){F>{d0k;}m_?^(}6@z~PfmNou$(R;eMq$qY zbNF^CMF9T9*q2~=%b|hqtI^CS?HbN>>dDOB{BTvTe{E4)V*vksDL5HG%Y4@F(bkP; z7L0)8>zf&hDiHM9$hf~*8Qn%BmhiYDe~>~QFJhu$AwC4GiZ88NrL+yq3RRN7TaYiRlz(nD_h=fbbIowv*`(W^4Qw zA(2Ks-Ln}1QJ6Poy8!Uj)COZe*(D?j&6CQszPzDmrItlYk{9mep=8?7o}@k98!5q< zc;;#LT3~cigmb9e2QnmR9}Fu`Z;c#gXdz>Aed)26{aT-w;J3=oy0Y3o<4c{aup{ZJ;+UYXsE;U1ITwWE z{km$TX<}*b{9f;dNI!C0 zJ}3_NDUBV|QrKNAf3veOb^PoA`sV4Qe+RwcC0*K z%v676@s#@1pSHcDqBmmJHQaw6%&0hvF{&myA4`=WpFwTdDhA=sjhve2vZB(VB3>e4 z@UPVkR6fT?@m*`fWo6ktGa+@Z7daVKB{PMqRARWb_r7ccvx|KdtzvSK_?vP`=9mrR z{de;_UZRoyE%xOdbJ6Ejv86Ig)Ail))jMB!Fbk{cWTK{Oer8Xk&&|rdi|d z@b{y^>gP4~{JHEqkVr0b^MQk!{)@2IYI|sgV#*ik%g(yI4v8oR%Nxqma<{c2Uae_0 zVwIPuh~xoSBfoZE194%3QB4@yu1#-gT1q@kP-gt~Kp)IC*Pwki*Cvg1SdL;-@aHWT z3>#8?8Cyzf2s|F5g>gyq6cvwW?D_kpj>g-$#;*tWTJjscwjWmhAyy%$k{r_tlMX0} zUF+ehdW^Ic;)0AL3>Uw`oLhfP>fZv8rxAuue|7g$=tI88y%Mh)LOm>dajth!!p7a# zM8R$>qj(wKTrhIV_Fv~!Pn*_>goNW-i8dmvh&!YoG>ORHM zxCEpy1!oP1J|T)p*KvGD9RA;m1_{i?=3f2ZX!Q@xIWwTOkvH@q()$r_pb@CfnIC@6 zW(EDP(-iWofh169mydC=_T%dGcSs7msi3&tG-f)c=du-p>YG&iboUCN~)s*a?PuVq%`8?!E)A zl+Cw==GdrsSCLARv@>SAC0wScx0^Uf;Q1SfIhV9TlKmf^A?VpczRl_sZTNh`BvpyM z6gJiP&~xE;Q7Gyh0xPBEH^(MNUI&2gj(Y2fSDKM($#k=~sQ)rm05%DzzjtRRn5y51 zf`CF2@x|i5q!XmS?v3u!}w zdqN-oaD~~b-B)ZddsyhsQ6SD9q9>Lir9=y0E$kJO6n}&cWus_M>AEsg`U7jCv%ef@ z|1B4tq$B4OlG6lyy zR4Ase4o99i?iEr*!Tjc)9_UdGF>F&9`@N|3ol=3TAEAO{YH50ERg8pQc*hdN8~=k; zoc(m2^^E0ql{eu*$*mXX);od8-f5rDjIvKEq{&*-duGoSG~W1djF-f=9EAvG zpZI$C$Q^-t&oM;OzU7g7II)_`BT8diTo3mt}C|k{~|UWiU=Zg3mu3S zgpd%KL)7hQg$uZMW*1@6TzfXKFQ2t|F2o0_zZibYf}$U z(bnFR!{y@IgEqVv`Uz82?Sc%VaFW~tNJK~t(~;@UG|7n0x8e#{xHFJ`mUDvKvPYPp zWP&&?_8JFQ8zl8zPk%gV2j0K%zv-yAqRz_!$xY6!h{raZ!Rh^<51P5nX~r9@j8gD> zBMc(49|nxGm7TbB=_cn?%jhqJ+z$Fh7{LJ3Erbt0QrHX7m96xT7u2%$dw=zOI>q&> zLj4L4U6l@q!Ip;I`K=dD|4&c*pv1`x=Uyv6?#^Mg`%Rp<4lRB}mSM&EKzhxA@3Dn~8GCJH zYckkW?b=z3M;iII)!LZ6JNNESFW;%WjOk@s>~F)|IsH3xRMd2pHy)lO+HITu*ECg& z!Uo+wn+W~^|GEDF&vdHMQ~!<89n@8Woe`aM%q7kpG2w?_tYn+Rzl-xM*A4mS8@iK(p|0IR15`d%Pig)- z{lA8Z+d`X!7>N%#pgQ$rN}a>U{%stG!ou*ps;%~Vs5x!eLqr(blX9LBcSmo=(WI*Y)z>(gnWWp@`WipN$!BJb*F?#NzxuO!ujm(gs6WfI z+kXdDAsUFj14)5;B>Gn~)%YKXpY|^pFlCdgsH>0!_cE)cHbD~Da_oa5cajghHT%(j za??*3fBuP8N00!$)KTr7%gJq!bvQ|CA9Zjxc+~z~*gJIN?R9qHPtXyeam5wmR*0de`|2VI@-b_38U2;pDAKZfA?KGdsLdku=`O#s(gi$CJ)>Sx zlyg8geJ3vJM-I$!cWq1TJ4F%cXxJrTU&GY+lY46CFTHoY?T)W?YT0p6GB^^*l69&1&1RtZzgGW_B z9Vx3YZ1PIMwcAQ2h9Dq&i{Vy#0)kVX9a_>;?U*%@v;KI(Z+uhYE^HJwI*FtHSI$*E z3xo{b!K|4~(2`4Ck~aZ(A3mGy!waFU3D-87e*4E>4o*$L*q5$C0^GBZeh zbrXtr6tw4(-a7m2l<)TGT%PCmSNOI((hfO5FZw$4JoEcWR~{Tyh!5NgO^#Uhi)$6F zU{^_ZC~S7rtpX^J4jMkHEQi1?eI_;K&O6^2E*_P5zuzN_v^TsKs5meDnEXq!Zan28 z<;Q7ccc0argeRG?oM9=YW4M-XX!*WF)x0qVc@@p&iT@zNe!f{J(u?zLwCbzKEjaPj ztK9w>Q~&jEPhiqflDKhUW9!QBSYg7j=J;PA{16{jbvtqPBxtI(I<&INz`oXSO%!#n z*UqvScjpIGXs2M%O{ZIbHQ%SHIC^WvBt)jTT5D_CQG6fH zJGk;On(f6+#EFJ)7=4w#R?(?4X?;r^!0aK9#6Tj;~XPw?vJWPO}y728ccVYWt3W-EXq_%3U;RL0G|oq3!+55SlB@f)3@ zh31D+=76n=rw4byrhJb{BOxMsooWr&uSQ2HMXsZ7oLUX;dbC)6%~3A0azJzexbzRo|7&Hi2SHTT!TMI}zfB5IP5ex_LUK&YXf!V$E9ycLeliqPv zy+0ITRs+kjH%Kg3`%HX9(yOihs4ORYp;)P-V|E zCGxP|T)NIfeg4y+4&RaV)iXABe;&N-XaTTe4Hcqhk64{IpRaHNs|3Q2K@mQ);~@p{ zM%k*#JCpxp=)yc9tRF55b^FC9l6~n;tuDLR3dd1XyRfpBxJSlbY22t{cLJRhelD4k zTGprK(p*3O3X~XiuFi-Ww|W2P{Rr;i`oRRLfr?^$W&|>7j;z=8o=(s-v|FkV9Se4H zw13)`wle}izFQpqtIXW9Z(AM&XWe@L#r@H9$x~y_l8XRU^+g5+et*x?NcQ<@Y0P+n zvVt_eW#3tHQs9)U{*}fDP{kTpZ&p+BMh%!vG#By`@=EezP;LtN*^}yM9yK%3=UqDh z3DkKJLU(T*eM7v2qC>~HQ<>y9e6nlH{>hvyOV)84Lak8}GMC^m<8&!FV6QS@0OnQy zHE;{)QeYuMGQ;IGyc#KuFKaFA`o@2^kP9OW(OiBMhg;&5jxv|G#_7d6g)NG-4#ww@ zTGQL~$h)p~RRZ6?PwJ!NJO}=tt)6$SpA;lNQnzRdl+lpSJuwc0@J@LK*KjnY7XS8e z71~TCUiBAj&i&-6qpww`5Fp~C-ttD7`+uN_(#yg8p~WKv^txiTSGTSd0C-4HH-C8Z zsMG+UL0VC}-_@m!8j0W}j%PZHe%eleDuifoE3k0;{m{5^2QuYgd=Xti>VZ{G_H?|# zYM|HyqCz5~t3S7E_yoCjd3HPtFw5vIx(L?(bmW{O1JI24EJEl}0jXGMOtp!l+_s$k z^Q%{)B5cPgHiVe`;D&zX#x3qiz6ulF+GpwYPHXR~8{q#fM|woJs4ox)8|z8?<4fBL z-1Bd0*_#Q*tOA)D05z%sqP8|()U$MUUwt0eHp+1DUA~q#0NbSTG~|}BwR7=^($U~$ z6h-SVd9cBxJS27GjIJ;pfRh(jk<=LES+_ z1y@;?8AJCS7@%T7SL~5z53R0mc)hsGW}vi&3*E1=9;~yA1^IDQJE~~6)dX+#`WD+P zCttSj!-QDTtC0*(maG-ode7K*U>=68M@x8P0>!Rtj8Pm8UFkof7%4k%11RCPOTpG% zU0?3|%bY_sXmK%3_}o?5{vrh_e0Xl?zeCu8XD*ojF&fjwww1hxt&h-v`O}#s5Yjrba22c9l>5GyaCm-KlP28R<8q!LCK6Y zh`?0jOi4+)yQ7iql=A_L$^q>&L#jOs!j#_v54#F6gNI6%UdL_a(#Vk7dI2@q#V_JU zRNJ40br?lxg_tQ(NZ4GU$Nh%>;bVEl9kM7(39gU%bOLHjt_FTx}+ox~q z=ewrpJ_kn8=}1|KIj@LPZ!TXn_mN{eUpO39#ob?9yhII@Uf+Lg(#yIh)KEk59dPg` z%{0~nh*=^vKsBcBWRhS8T5nvrtLdN^RrMOqAtz}sbgrCp7?peA+AV)!RLf8C^{DQ4 z(N{Oiop0}($IRL8NQ?txh)KWx2mF_JrH%1BfW%Qg!0HeGvO! z!P1yR{*@Dj55%N+`oJiJ8*bj;jCs@f@gY<2ymSCMhWIo_9VTRIM%jMmm>efR`5g}o z%I^2tko7G)9}i!jQnr>Q7IV z2JfK@!TX=hE+AfyE3uxkT-(mqpG@H-iSVHJ@}BoeIov6I5H0hguqf=kTvbPp-s8Ou z=@z^J)*R*_A3kyLIRmWE$dxPX$1YTu`sG(-xIYF<|WVE=jA-G#Gz($}i zx2~S449&7o>|Qv3O1xyqI}>M*ar`t(wE2`xPFm*2zwdF-8YUD z^{4j=dSjQ_Dt@dBYN9wGBZfnRPtZ5QTQ^Bh`F!pvowXleISXtqH<_+A ztNn#glfO(t-<}@s&eHzhEN1(!W`ET52u{ibwQC)1$(_tY3c!Tq=O%VB#cGL5x=en$0bT*UeH!;&@9fH-D#s5(>3D}A;# zYTr`6yz>j{HLD;d^33l-%`CUkKr8!jl1(sH;QTw}@YC}S@{*9OY#Egvg^GxX_KrZX z-A>?Z%Ek9e2tIh&FUl8wWorPG_^v`V7eM{MRnK1E&*4+^?!PQO`~0b?3_6l=7qLF= z-srr%-JN)k$f(OlYu*p9KZ$q=@hk5uY40<*Z&Bi%ppN+|7T<&_Urd_zW07?^?pzUo z!KCZVDn{A}|EWwpJ;|ATCn7P^F!Hu&vTT{P@H2@T?NUJ|KLEHe0v&pniLQ z3GMLpN~h!Yl?OHH!RPjH=JRV+k5~Mq@2#vy?ngBinH&{gIu6V^wV)qWVQbGic1FxV zKsrjZ5E2z@k+}cNGMi^jDwbE25Y6}4ss+A~3QFtt8vh7jYb2LdnaaZIpQ-fQ_>LeO z67XK{z|Sc`-#JbA{_~(48h)_hh=_&-&_lJE?G^%19y2EGTATRIE&Kp}z%@|G&u?T2 z&TP(O=mK<_T=9_j&@LToz-f|6w@{$eXtrvRi#*#nC&dL%P=@=;I?zz4K7)_)MIyAg z1*D(Lq{&q)Cd%EF1O4*u(+Zo>3*7Pwk4)KxW-_Sm)1C)-i?=I*i46nIDWlhulZL$miyywOW=t%;f24Pq`}8 z#JyU=D-TiG8V@DXbHl_F{djs1MadOKdCAzSH~?Tj5g^k$QIL=%u|XHE=gZ23M zYj>9e@>*YsU0?I4{rrdZ!k!)h6;aIdcdnBQ5taZ1ZAxt8jo&XA1@ZuB^Ym4idA-ri zo_D9ew)!XrE;4i7ile#JqRC6ca{v5JLI@3m0auQ+w)1=Jc44-r+|1g=O#>Cq=yce8 zI)B;ie?{pZ-wWBG{dg2`LIg%1OxGq=2QM%!^Sxp^Kxw3HeB;Hxf!|r5h(F*K)NR5N zvK%Gj;nZ+wFZVI46%)5L(4<@?YPvl-67NeUZv?hIb}%llnmdrg&G!~PNH)<1ZP05P z*KzE4L3;6Xes5cO4(SgyMm!YlP}8_);ItjriDSErHQ%6JIUriV#q3G!o8F2UMe3Kn zj*F^fvk~kjjnrBhO*peQ5~fWYPfK2%{G>Gr5(Wq1z0K8Lemc4rU#Ptp7m=CL3U?A7 zZ_;(Hu}i0Bd5OlnQ&J+`7brFFz-%5WfuX?*C`g95@1Dt^)=X#97SD zsRS8>&{ZSdVrIdbf{?*L%>1vv4a$qkSR!o$;|C{vwla@}&jsL@5upnY95`!^bv(hP z!fdwrU!-^}Uj0RQDAgo+Nby&NVs&==S<=#*h_!x9jPI+jIR^XiZTEKki7LV9>meFu zT71|WIG}ZDz{XgK=iS|Ujevk9`7TrzTDg$1?i8@hoLE-5gjR^dIC7=GM_*`UtEr~A#NZA~9ngVbQ_(49Wra)C)$UAJfR0jdsh57S2PVw)32d#+R-}$64%;ZN zkq2Gc5EFTkCXr)zF;!;y(FnK!{{6t!l&g_hw%U<+xE#9wn!arXS#m%tQfk_%`0>}n zBdN2{>!BY~(Z4+l2H`}aehgxS;gC09IosWVD}x(bH&zc!Ek+~Y^!5SH8wpK^kM_us zTtdmmr6!Xc;VUX1nsRS^>Gvs=>0|vroE+D^GxQBxwm9Lh;YOz6*VxYfMl*d75AQvc zE@*L;dCZ0*f=q~~s8UXN^9)p&Tfe&A=?R~?SVklL(+a(=kLZ*;GmNET75jZ!FwLW| z>4fyR$y6-T+Xh&CJnm`8M*qI5mZdQr9!(e2r{NHw?^{s77=-H#iEynvnYnJ1ewWC{ z!uDQX+b22vYwzig7Pb1p{Z|!P!vHMy6y}D0x#VF610$uaTl@bv<$Fjr?a?`3y0{Ns zyrmJ&C*%~0^gMAJ9>oba4_740q1UV65%lkO$QnKD(!gE?c3Jx-Ep-&ij1T(uA9vrOT#G+oBN4Diap1m*_~RY`hDDy`K0K1K@hkx~D-Zbz>;B-HJ0AdPXd(*205P4EA z_aUHtn0OW6up1EuB+7qwMf=LU{MpEe8h~^R8vdxeYfUw9jb4iA=x5q7<3lq;D;`wH zETvT~18h4QEDRwxR|+b*1AWff4q-d5R!8}V_Gw1ddU(`Q`6*B)w)QQV%r0LjjF0AV z)s+e3FSws`|6MSXMJop@{Bi7<>Q%Y^zRp$aG0$e2p;d78@o0eGE5qbDV?)fEG0W{& z_E9o~$hAw6^i2EKzOj8cee#v=RZF%u3oEE@+2S1@EmwT%sQU~~@2@eupB}oJKJuDp zkl_DS%6M)gW9e#10+%{#*hi4d$;91)VJ6}s-23z|r~MDt6#v9N^i5UEb`db;VcF#^ zNbi^>N*(ag&o7xD7cvA5&_`yYAJZv@Ex%OY=CorC&2^kq3rBq89^UP|YVVkSZ!Jzj zKu(cqBLAgd*`(NmeLL*;@Q92VqMIXn9#p<~+pqwbs}cVH=W248K0%BsyX1D}HKr-P zV+&(ThX7ni<8a7Y58dfkU8dn=*?>)lH8o|bj4+^jSnD77F)s}q%Jk7{FXsd_vFQk? z8pf=EoO@}^LrS^Z+6yWzu}faNR|voD&9*|6;sO3#v)L!D&x`meCQa|*J+L|Q37wdf zr>NrEhr>GY6t4k879%w}P+^l{v)7YZ`K`tEu#`{z?JDF7^+iV(>ObfF^*MVkTNd4T zu5#~G8y@UH=lNxsoAUPDvEC`%b;}$-`Q+3)r+twSNM3;^4W2nokVqY{J0f$i$n57R zjk%LejJ!Sts!X(s-WR!{RX%(fn2EJ_ZggB`CkU)|D=pJiTWsRkR(#m{Z;Ah z_Mgz-CGb`d64bs}^^SZa36E-q=zDslljaiieNRdo<$EyC=VAL;%DHHERKTbb`^KH= zr2KwH_i8b@cl?i+Y%a`Z*Wn&DUbh}DR~95Ukvp_~!P z^n*jr?YgC}oC|uvIA}0G3fX3jT6G!Yt!V-(GO!s>{rZq}+_}NPP=Yul#M8L=+XDr* zohYQn;ke7@ilC`YDrsd|9I9TzzFHm{Ix;D+8@9T-mAS2}=WW7k)9;Zb>pNMp9BUA^SIi|@Pt`zZf~1OIDK z{e94oFTg6)6p#x}|3T_akAJKNI=La)p#bOWaqG-6)x_&*k4(a;z=iP~qvB-p*v^wD zKh_ejP9BI~VebW)QWhT{t1e_6VS7z!cYP+-th#>1jfU32dt;8vkjcvZ zu4CiAGYu4J`Eyfh%GC^ZKWu=cyNg$H8t~Ef2&!P5f+2t4ZC`6`XVqceg2RbMKKLD4 zs3Mw(-cQ9bOy<5uWI~H(qITOX1$H_-FA_X&5m>RT*T)-Q?`222^l7;J3d*^+YCa(K zBCX3Lt97hH-hA`a7(J$2gj6H&T*%HoB;Woh*F}VtE0ibCo{bl8^>t1^M7l$0 z@$*l&4xE?y+~m{?H&gya`^i*S`slfv-YlrVu(xr z%dJ`pV&=~48NDQ2dbvR!x%ZMkp;e1}Dt*Z#s2xD2gLhjV?gYEO@!gWd3;!JOjeG=k z)+oTfj`SEQ&)Thjp)eC2`f=FWzRc19FZcJO{PT^g+xsr!J|0~uMf^mU$(YQu{|o>M zJ$QfLuy&C&EW@4}PZep^vP4al!%R&TedcZdZZ}ycR)&3{|5ZIcu@|PZV=j4^9400+ zwbSP=&zRkiI5P3v?A;KiR#xlG;OUcp(%iyqzOr0Hchn>r?7}$Xtt*~=Jltm^^KP(-uCgRj zo1qzjk2`RM;>(#99&h*_OX#6yIDaGGtfD~q+{6Bc6V{8nJeo-qK%NdGoar87rXJIVq9xHs>9C&2J5sE%DLmQ+?o5wRhW%G1gjMNh%slC zGG?8oesWHOw#WN-0<%VeWrPqA>aE!>0!pX>;yK1r*iw zBbL4^=e@)=P1m~^0Cb1hJgR?mxxsd9rT0I=sC>54AMPTDR$rgE)5o z!fBh1TtQw}X`D~s0Vh#(_PGgae$VpsuO#b>@+wB1p}O89e2R=7>?0`adyTFbTc=R) zG)a#{VKL13#7@FPyDY-r)3nEh_2>RVpWSrJ>TC-Lnl{O;XdIsze0yJ{O!`Y8HcUTH zOoYReF0+tn9@xDZttLV0X5aZ^IQ0k1M8pJjLOfp26L%dvL5orFLPW75~plu3l`O86rhqtG|I871;xJ~h>X zJ5Zh}PwgzVu>Rkq4!sFw9!@YZr`avSa8NMD!b=#`5kj88tm*duW26SaR zUw-Nq4OIf?;w`E-j)_{-`r`-mXQ*6Pi;mNevx&YKO&^x#!8e^nf>LRS-Qxq>>)EVY z@?yWZl;*Z_UmAVB$2>Or{5NVq5A<6}q}Jh) z?1-%O5Fj_LgPEyETz2WXoGAKg`XAYXBI(*pP`QQY)*0LsaXO90p7lr4Vcp0)5|p@(4&vG!_yu=;Ddvc65skNb|>9f+1{b| z$6IX)>(eq^x2n`Dp=TX2q8TZ3xoJK9S_+3fBU^Kq@36avA-ehA-m3U17#O&h*2Xx|z3g^y>GO9-#f60f1XRuU+bv95!wagu2` zI+DFA_b7P1(GEWTU#iP#d`_hwG!cxUabsTJv}tAw(C{i5)^ zk{ATBSk4}OM?E*qeK7_pr+Q6yBBwS2e-jF37`gqxs26I-2F7@jQtw1{XT&K?dujKP zm2S-v?G zwxzgGPB$MpTfD<;`Wo#~vFkl-;_(OnlM?K&Rn(7;!@DGS>L?Hm8mi&Ck7+a)u957= z{tT2Xg3sFHBf;}-4#TeN2782KJLssp=4&%o1Gt$h8rw*BJ_>BnbA1CUYc=(m1$K$r zR0>n77$3{t7p52)&kl_wsn84|x1h0aN;$$-q#344??!nvz$}x!xrrf%9sr!v+GnRd z7vBP;+-^yv2^V&ql>W_67n$wNl>;mp4Mwswnfj8J1qH%8-|pLiqp&Zzvux~_^c!ld zCXA9h>SLv~a^yt1oWn@fiu+D8RStTQnm2J;T!Al53tYw8#@mT`yVl>`8SX<^k&?6v zpgfXb!u5?g!23qWHo(@@Xt9p4 zNnHamWR=;uH?w-#P@87BT3pu^+}YX9)^>`@Iijd~d4F1k3HO9R-my5jEdjxk%VWqY zic;_$7c4=GRqP#A0Fuah@+KA4)^IbPb7&(no+cW^*F{{fcQiQ#ygXCt(O+D#{403q z#Mc3?GOh}i%VW$#z4lth*k*L4!WARrRWC}OFmiDkonuU}G7H9AWuil6Yn*Ks{FZE4~AAU`R;9RZF2OP&OYkZ!$|UuuIn zx;ql20(nTW$fS`9Qr!TGGEFbJ^GEC_805cHewo)0!!07zwbnG7SwUQv+q6EE8bxzi z4!cbz?s*Y@N<=t^!}E59cB4cqG(EH$ATqmAVxRkI9T8bX*mkaW1J=uJO(h7gHtk&4 zT~k=dP08~q?47M#vfQ>;FY0~G{Je7e&%$1L{57UwE94tvZzr*zx#(PU^XSNQ@=`|=lWMcCap27; z7qD=#j{MOQUj%-VyP&oTxCO?aB%2C|uXD@C9Qc>TQx_&qg4+c9lQ2&}Z1iUzC!whv ze$PeYS^HLmM>WHK~7fqK=gyDZ*TZ7+nHr7wfH~`1iNpg^) zjgk^ssooh3)*8jE=s?@g`}3C?CbL>xjsEtC7hs%!2gjecd0zxJk#3(6zY+NM)0a z&N|w1r@5$xws9s>imfJWlaZfFeixb*af=qAhS+`1I`t}GDj6xZKfu+)gRFRc(}$~M zg_1iK>5&(?8aJsgNehjIc4boGm-aS*5;*5>BrQafT^00<%8!})pcahH*eky!*I`Qz z7FkGt6mBMO7y;dqmV&U8iqs684?dF*L#w2Mo!V7GM=g1fyy1}}Na!#binC8kSykYjSIjeL952ki~8)$ERubN4Y@Gxf8ap)`#7~|H%vAw&BY;CNUo}2H_MwR z&Qjsv{b8g2MYtDr>&uQ;IJ*$B;;~`*oCS_{_Z{$9ZesYO9gdD#nNfn7L5TCPr&*?7 zQQvZYr1q8M24$?LT-VOhCQJ@HB!47c>wXe9IQ>ZZLPcd`TlPBD(&eUr-~3}d;5|{* zMVfTwxVz_k6h1{-#g6wS08GCZZZlhe%tV)E4|}fQ(Y2n=inJY%O`rZN zj=*!=UzI%!KM-&65x78#^vTB#a$V)QehD_K!+cb(Kmd_K^-kA^jaRP$keVlcun9e% zwwWq278rCrMKSnyx`%EPLLQilGxL1$9VS0NGwXuzAe~{Yp4gk;eGK9&%)7m=B{KIE zpttJtKMWQp*YCl|oiU=>i@WQEjUaOkqD^&SzOYG^JSIx%lDRn+TM9|~exR#sS}6+( zqaRgyhYU$gfvhCb?F$%KA zN~xADk$TmH94MAP&MI8o5}xEDoErjEa8{qij}fQ5F^Qp!K3bTE?C)E=XMe6QEmExH zAbXf`AgfZW2c!vCiw>t@oxvjgN_<*x9^whBMg8)qn#C6?Lliqy(mY{vvzo+tVa6oE zES``~inPAkRvGHjduulofx|ZH-xS>~FlFZ;kL;iLhfNrKdPAqueJo8a#3x2+Vkd{r zD2lL!!`q=MH?K#bM%Z)09?@VYH7mhly3R$JlW{eUV#)27Ji^Vyg;2EC_jK`9^>5XJ zq#tX)qlqV()ZZ0Z(rcTCI``)MQEzMcz+A_37;)5YosBSGxP(fvNZ-MtMu0rqt#VOT zj!mQw8t(3)>ezN;A@1Pwl6A}#RK5!pMf0>weWvga{bK9(twsB%+GaBDJ8*;OPj)zW z;RXZlV-94VV24CksJEm7`3085SJjDJsM=T5G3gG+&r8R`Z{i~#F=JCgq1b~NeS7p6 zAtf=&h-=kF^WHsyPw6wdax+&#=z-Wv#W2V{Q3`g)($)xk`!`lcoqNxMB zTbd5q{b^D0qN{hAbN*s*+fnb~iA*Ml1Oe%DkFr6wjKGJV(Iw3HLD4VZb}kZ^6a>mF z${ZE|5B0Qc|2%rpM>`N(&a-G&i;F`t}GP5OTM_kL<{M&4|Ip&r$t!BXiBs`LRT1ev$pQU* zFP27#qMKZ%JH23;QW)S|A87EdgQ8~hSZ-GB$n`H!euMnBzjCEq;(a_iTL2%_6DTU- zIo~AQE5+~7RNKfOVpkmVy&{bf)p{f+C$|M1^N}JXJr&#eUODL;rd%p58Kt!AqWTR|`(EfNGzOFw$s;mu9y+ zawqY36moq|AOGKm({}>TAw=>;oT53?Zs)QK$wdWu9yYZF1x;A zn>hAcd8vWDmdp#WmLNjAbA)rmYLGS|XA&L(=P6FzG9M1yDJ8E@*0LFJjy6QO96jFS z*HDbqs;EvoeUB)OpCUcW;X;x(mTq}p(x;Ic^SVs@o7@kSl=wM^mAx7xxvGCsLT!w#x&e^1!sCmN>yqJ2$Wh} zm(!I2?XR5nQmOT=<<)hadFcbeku_IwBP)$RB$o~FX6HtwDabpV)cdxyf573-c)reim+NB4`Ei|bj}wG@KNAo z!E|*QxS7U{=cA~1qq`l7Iv>BNa;^%cf{bAmy!)3dgp(=X1S62>G5Nc-+?Ac3`WzYI z$X9ryou63^^(Zf#77NuEUb@Df-9##YtxN#$0@hD>dS9&HT;|k<03tbcgeYs zV^&wlbHqb3*77j(2x7WZWFr3gd)5G5;d{YuS*qM0|;J~#xS z9?vUgdYTqfx06~ezcxK?CbO^3EsY`$u}xWY=P*({?-kg`Fw&x8l-YxQ5?)PhWp0oE z2ErQ(@NIo_LUNzN$D-UW+#~M5>UyodY z7MZc8=v)6e-?sOPUL;CU!vpG~Q&g0(w{MUV5BIc)K|gnYl%h83TG>RNKfPxhao{f< z_3XDVGc;tk10IHnDwDQ6oLp*-*?DAU6xmL8t15fbIz(J8B>J%8Z&4FQtDfiZNF#EX zRYCJyjkUVEBW4Osy$H2YeD2lwrL7G{vuCMc>{=Z0b_7P+%C#OaRy5n&k6k<1GRt1zdgl_`+ zdJXJmo=<3B!bEf}B4aZOq%v`%MfTBO;NX~8$Vy3Lq*!9j^c^n?5yhx7m%Jm2RwtSb zKW0!Bw%g%Ol&unq{>`jvCQywn_cRrlR`<(VyB_(dg5Pg>RvBuwr(dKnX?&T|{&Rds zvhRsIC zZ%FsGX!7Rjs%2WJ7}h%Zz*Haj_!plEB)gsn{F^yjH;_HeZp^jZ&XK^PyIph7daYub z({+%fD5mdpXYCn2e)h4APkC=VgAjf!J#u}c!5acGuK(=wO)y5Br|i`_IFjOvRa~^I z&pfabeLbbb1LfFXIiwxcTNmp?rSrro+#^kj()mm_(3A38ZoIZ~fs{yh48!iO{xWw7IOW>4|JV5-}s z?^qY54P+Wm8;@b{GQcLesiT|nVVG=BL^aF&S>Q7v1IYFDS67Za@sJHq5tWs0x17!D z_dA3|x}TA&=Ye{1H|op0;Y+jO${&2ZlN@-6U_<>+AZS+Jez#Ef`#mb#g1N)Jsh-De z4ej@x$T35FS@%P-$+_3DuKD1dwpmb0bzK@q#$q68jQyPFV!lfRE;F#I8^t7qT3CEm zTRi#`j5Br`45uV@7jk3h{2+==3kgc%VfT!#!0MWAS_NT{JC&QBz6T_W9MoFZE@yI= z(8Q%tdlz_!GFPrND6We6eL7lhh9Dg|x`|~Dlj|ELMmR@qUO(pTip02*kpH(t}cM+RX#N)+xd->}y(ZRgXKh&qR*yB`HcVYWNE`h1Xr4HomB_kfgcZ?nZX z>>P?PCSYp0DKUBCGPinHjMBaCY9%@T+$lNk@Sp0_zg2fz01Z?V??5<}jKJ)_7MYts z>1qaWutw10?4C0qxGl%s?mM!OKB-b%Xw76zhtCeoBrRP}?^#I~GE4F)D2z(=IIDpp zuwmZ22yB$pbb|%0g+jd>EKbPd28zcWFl{yK;T4qFtt)PeP2o|)aYR^~Omz3p#&yo) z%?E|bJSE+?CpiHIP9^tspKP(a@Ok$bnP{fHm7mLlpKc1K;kFL`tvNLNzG(U(BYQ6i zI1(y-^1Sy88rOD-j2eREdPxMu$Vaaz%^g#tBt>gw=IzXPLt(d{#Dh{4BKaesbM$Xbs>D2ZQ;io$$d@nS05pse&Ga6wsF%9A5$kH=m{CF3fr;M;M7|H^2ckeOsc8%LV>h*^=ByY{zpY)b z;Sp!Gf+HS+Ne;z>8rH>=W5to}u6q6BHitY-WgJ#sK#lW{{r^YTdj~X?b=|{JnGwf= zh$A9RVZ=_c&>=QN1w};Z5-}nILPVsK1QbQ2S!ha$NRuwoL?A&aQ3OISK?o8cv=9P> zkp7*EGw(a^^F6=&heR82XazKjeKS`7qwlT zeeo;|X1dSgjq0v>jdnYg99x-c8oq1L`}`D7aA4}dUsm{1b;bM|pTW@Zvd?NgR`kO1 zVs>sf=eu}!+}l&8Ppo)D)z|XAb?H~y$-YdSOFEaL;iECW23Ldp;!}Lm+q9K*e?(Jd z4vD*MQYK>l_GJ0NS-dCVu&3fnjs>eMPW*06`O>wDLZ(A?wA%BTf;hyT9iov_3qH(- zhs&2wmwM%XBORjj#p@=ipwnwSUt%Ne^5S~!_JthkE{;SiKf`?68nGL??ZCI#t;V%W zs(8YQLsLn0I#fi~oo$G=NVRK?(%mBw)*860+k6zNS@C3kvk8X-AfRNLtCCobf?rux z@kdnIkhmxp6MkSkWm5+llIKu)_h6zS7ix?{uBK~qwDA&2jqLRAl5C^y@~bSlHSC?Va*GvQ5d|rr5AdPe5HD5 zzQx^Tj6l$4AxcyT38(5%a@zSJi=G>lM)k>x-VNwJbCe9RBfg{PBPvjJbSKx=M#CpF z`mhJn!9F1==VOmF+nrr#+qq=eP7!~(pK{t^Wf032!I zn$YEFzsd@i$>p*Fr9wnKrSnu7p(KJY&z0@TR^C@w8TXzqL)XG6_O?nRDdD}+=hgjb zBX!c7rF-}4y2ZF!p3ocFUXQnMS`*sJbW*vH?N>yq#7D*uvp}%jO7=be{oB$%(VQ%zwoRJ>D)N#|F1mk5WZd>tw+^*XmYk>tY{`5aBfF=yF( zhUX+f>Zu+Tu$TL|baGbDO{!|zRqBfLMcr3dYo5d5D5{Bs@#TdKhehQtno2v-jMPE@ z7oH!&8(Upe#J)?b-*95?EfKqSG%UTsf`&I>sr`081=Dzfb!E@Mz%l+}Idi^M|6Plu z(%~ORpZDrSd?O|9O!oM47{&gw#JwQ6ptY`E>^4WIdw;JJf5b#(?E3~@BReHLH75Tn zaVP%vrDzLMm{qC#EhsyN!P?FT?NS|YdW^%oWg3jI(??hcwbRBdX#>r z)1!*K?*Q4bnwt5rJX|B`{K7}0?SXm6m-c2?=HKSEqKl3;=91$l8$Bt)H92t;r}FT} zP!xTiOoHSPyc^@0QXIGCetY?XyzrK%L_f96M%7*m64Sq}O)A9kSl5 z`M}bQN#W)6=6c{nO)fsyH!rEwS?V4}yFu)RY2umV0j7-w_GO=XX1iU=Msh#zlvE!ke8kC#!1seLBVT}%wg*k}SR>A2jjrhFzv zW6$LWCBHLj)G7|0%o`CbueK{$uEw_j6^iA?w>VV96^Oc&qmSrb3SH&hJTIK?t4I+H z6lBM0Sq;eLaK}5YAidua^W?p{Db~q{X9rSSMqYH5Ichp7Mi(c2_oAMavxDyM9bF8& zPZMwMGCorF8Ab%gy(Sz{93TozJzI9pUUI*Nv8aT}*-~22J6-tXp1DnQ`K?(Qg*!pDeMGeJvG$KBNEU z+l`(g>(Zhi6r8*bX-@|1dli*Ko}P-3Yh;ESN1l>PpRn5$swiQIfVYgqlt{7~1a1XW zk@(iE)_Xz983xfNu`=wbjX&NS5)ds)dwc!LF@opu?C%$L&mS2HQvB#R7+Ox$kte!l z$^6#MjG`wQHQ_r{dOnU>{ls@2L(-Pv+kA~t2I1)Vuqu|+n%he?a$xf5VwavTL}-l zcSq3b#i?P%1uyta&43zSs59s~MYj!@-^X#zTh|Qu;%X&cT5oZ+Mw(8X_Sz9i0g5&8 z`UZ=gczn>WS6*lH?$tI;%U7*fsV+%K(L1fxRyULkDQ$OqQ+~1K!!y5x7O%)@S8-p%!wt!7++9_5ouq^jT zz1;OwLO;JYl#Nty`k-dgm@ujI#BkhAY(;ic%a?X^lU)60uL7^I%CIG~&PEg5N9x#m z-_Zq9)?LPOVbC|pcRN776(lkb^(B@)WZ27}a%Ek$TFmymudpYK&t}qogmNDP z6}2s!1v?09^D74-?MM4;T%6IG8%cih#J${Y1FAXQ6-+mY7MnIq$;Pq;rAB;Zs}Gla zUI(9GU#_G`*?VMu|J&w67o6gUx$mlWj1Lt?9cnxvf3}idV$=LS68j^rO`~n>0&1nax$nW_?~!~zW0_rzI*nBU3bB;By-3yiO3MO0 z=SN+Q8KqWx4PbM(w5fv(vj}PLaT=1a-K4H6OsY2M*y5KQNb)v^+N4W zFCce#3v=(y2>5CBu!UB{Pw8Zz!IWGvyCuxGEH zo|^$Aw8=w6b&>fk(@~?d#`%93=pKHthe^&kqB4veu=dwgIVwvpaF><}^1tW(`p@RP z09UDO&t|2YHG~g4{9+TV{wOMAUP`f;wlK9k^UB1Hc1A;!d)~rs>Oh=vO}FRvyeQIb z_ZL;4P-xNfhp9O7)>H!Z5#1$gzTV@4Y5g{h!s50s_qEi(^n;`M3T=cb`-(`@YYM7= zoMrUJ#9DwSCwE){HyHoo!4Go4n9tVIeKUw|Bx=M>r;v%|lQ%eruXV{F-n$9E zI5e9y1d>WQnMH|=r=J!Q=3Tv^a#UUG#)-O*s$yZMkZH|p6<;s#SQ5_t;1{)gNs2`g z#;WGY`{~$Oy$Qib!fijI31=v4xe(1!@b`KW$l?td1u^typFb5qQW+cU59KnHD5RWh zL%v*yJZ(2p#il=*mS?`39k`gXt1-*a@MVNgS>9l=YsH`^qi@)NP2<(x5Pd6m^Pt`o zxOU?$z0X*ljO;a?N?$ni#*ovzg0)F=7P?K7FMZipdRv)JL$?; z!-5Zqxm+_<2bp`RwoNhk3uTJ1+MioaEaJ3z5}nSe z9K!sxkZAa-yb!JLU0tBOs(DZG^~PZf)aW|QOeqCKpxsaedVJ&Z{*y%HZ)2c>Og=1z z><|!ycOB?D_=D|0M&iHPpBORV^-A)IxuuTkDuyh9+ZH+PVr&{|bg40?>_gQ>J>`vX z!A*y6{yt9E68DUAi-5EabLKj?RT^h@Drx$JoYNPNTwRmpvkv~BG%rG0xrY^l9oy6W zu2$0VzZ4YyY(ql^q%8eU=SvMR*CZ?NA)LQep z!mRDz_-AtP(^CURgVU%TUy+x%@ofL`N`8r0tFiTY@H7dm{s`ys!0(rJ3pLyO`)w2n zRePcldP;BLQ;|OT1!V?JgNqG%UNU3rH%lf`0Ky}souROIcSK}I< z_*Tyaxf_nBH5xKAzx>R`y>s{UB6Gehl|RJY*yK{$QW>9kDqJT^JTrz9|LWk|4bZ(b z%H7Wvud|os-s0`DUdXuv{%v?dntkkoFTBfsw)ecygBPWxg^z%19)6)Yh&IMc?8k>u zqAe*@oEm`VeaHq0$ic!obLT22g{k7?>2lIl|F8fBxm*rkZ#7IV(2!}@o1w-Z9y&Td zbsGu*VFnWX)->9kRn3{p75juye|*=C&w@{IYsBOnhmdN~MLWgVhV-hyL;{dq{UDViBBh!D?;VTO~! zG~wzlJcye5^w-+=MQ*WHWXnUSa;1EODRFZjeV;p6!?mdyS)NaI(`SWlJ*E=_5JZ$# zz2yx6bK0<*-YQCbia{fT(xbQB{ou=4BCl)CVPI_Z6?;|=pc z2zS*zQ!DB%FjKI^n32?+`IPx(On_J?Zb4`hHYIm?UwS{Y+0dq7>JJA3@X1DWM9HS8kBdx&2|$Tcw2M#>DV*?IjKYH7LS#=%m( zf$W?NuIEZIb(XbL>&I_S||>FX5og zE0VVNx2pMtd5rngLHE5%9}V$0b^J&Io$KI5KZJRJB$GwCzYwYmVXY34+S{&2&qL%B zfskoAZr@V2%BxN!T64El9Sbd|_j8Hau;5B3Lnn`V1N#^__bi@~Grf3Z9&Ojndj}gj zD15v+xQ_O6K!})JPa$M8KQSl;7X;GzX7Z@%voO@-Vnn3;*3+W!lA?1s5tfe&55K6- ztQYf|sa>}Y7Od5MC2eccEM7UxMs9ZJocK=M_v=eOO#ou){LQyHBfoOi?EwAvsM5~( zZ%wbitH1o#qw&$$jTCFI=0<1pBL6tP@Yf!;EXJ|Kr#r> z-dL{Ga=H{reJC+@i7$-leYo_CvjHDFo<4NdZ64ff+CD+T5z&z-;fIh1NrCG!P;F%dbo&hl>+qbmGJtVs@fe#_HD}!JHdZf~aIMoboA) zseAH`@`g{mLa)x+3Xg9;+n}Tb=H<@#isz%6hb?3~a&TDe)F7W=CjM4b-`_imO%GP4 zeOz;F-L2sS0^ezMu(H!64dJ0L`mA`!1ZMp3|0z?A9wYP<&lq=1d_9Z7-|wG~9{^mJ zTNBCI&Y3pSQ1VCG+Htr@BvIS`g)a{-wDt=!bv0}}p0hRhQ48e~?C#AVFuYl$Alg!C z;2qRzzaxhnx}F=Nhq62Nv-_Bt>8iNEbV}@>AsC)0^<@_x zE#-uCdG$Z-^5(t@elw}=18OGhy96~#TJYgZ~lbzH4`lU>Cb}Hu96z6V@AQKL{2%f_St{gZ( zPWfI?7C8CANs9a--I4@a19WcN1KIQLLpcDdud;Q{^Hg(cSauh1m373>XeayJ#xrN` z!KvD1O&6b(&kmI2IiL#E9#&-8dsr0|If)J-(AUYHu9+NuIP(ee{w3Mh(Wih;?cxLq zbPNbB-CYbN0)S+#6n^?~W5|7b)rwEo7Cv6gs^m{$3L1+5XkA7LAA7k}l*0T@q(}H_ zuAX&1JPVC^_2~~&h1(QPEY~DBB<~q{+N#g2 zuGTbPI_H4hR zToKjE?A?*DR-A87r~r}*Bl&M7x_yq?Ar7dONBy?OHk#Nua8K{#x1_z5TiPGL)0=uy z9*YEIK~rhJ)Nc4&IoyP?!U8#Qo8(6YqF!U!qE-{4KUU8?+NRWL;sD!B`xa;+fg2i{ zjkDA72?+GuK<#nacq5*s0v>u?Qwj0p6s-;0o|52_#}5)Kzz{&^t4SRl~am{FJM-%)9c z0=*MkeoaxBG4U;R2MM7NXs}8>kcSXlrTP<j-UW$pw&1i6wC2kB}rvw^;xF`aOo zt8}uqibU8879S^+O$`)4Vp~Ol)=={Ih@HGX)uHLCS#I^Un+o;Q9e8ndhAzE%agez* zX8(XJ1hUU`$*qQQ;?6Oe07ycW>V#oqWI?0BtXr(iz#X8-_1~cM7Z!-+vw&=~nE_z^ zmlYK2=|C3*DNN0bw=|=>_mTl^`{|FOkK%C>gVOeW@4UWS&EhW6;>jcs{%HAOODuW^+p!Bt=vVM)F zb7@gB{5j>40Tw`gs`PhJpWC#`<&QRI&gKqX&6A~>M4IT_(r_3PEZZo452qb3)oCSt zxYlKXs^jkCNk+)l`(5El7E$>Tn9>~tZ7r2WR5gm}RcF8+3yR2TZMVMprlhCho<}cM z>v!8*NN6I|k5N9Xh9#yYc13*4nR5MMl;0?qXUOhVSx;?u?a1rabQ&o<h~#59_En zeBV#g;Kal8x@2$-q0U2$a=Ux}PMOoUy4_z7SWX?1DrE-c2JD$OUI=wBJ=EmlY@g-& zD_N>NS&>0K1e#0w)gl#!FY*5sV%2dC*u18ZE5&b`BryDlU(0&3oxsUne93GeA~${f zw0Rfo?&AIvLy|B=w=bQg1H*F%h2n__|L)gW^qw)sR03V;wBDE6z@+ksxmj0jwCKVb zY6x8-JLmM9z#tNwEIpvbP)wWcw6SwTO7c$M4G{Y(%^Vcnwu`l?pYHScesqi-bkit3 zWmk+*!3>aPW)lyfBWUKgWFBt$Lb0a-B573U!D*`3e=sgITb{9)mi@fcLd=%;oOwGa zS?yqOMzU0as1{L)aDB=xz&tIT2%Lh@{Wv)C#pJ}Dif8(jJNcKwyyzmFet zadz*>H$7;)HxMY9$J}q+P_N(LqoE@$bzLKTo78=$8=9#J-%9)TOwE|BiPD93A!^Cq z^!%p^Ql7;hyk1?3_*U`ZwNIJ5IVOMGwW+A3xPb3}-OdXzL8K>776DIObT2 zpePhnAU#r8QAy*11<-}RfHk8)I$-7opKC33^uuBd=IFSrBH@rI{$g$ol&aRYD{tFT z%??bD>aAg516RYgJ(gE9Ou$;6`g$sejhRT2zEFkT)QT@qaHW8bps2gNE$<8l@eoy< z|$8$MdV3W4<-*4Vlm*4#>!Ce4h4x>65n$ba7*aj&AU#kR3rt%X;VUhZmos+;?H=}7TH1D)f{$nBmSUP^wBJ#*`%i``Wn z!xr3C5^l>;#70*vvWm-*`aC@-ol%#ysN7gKYIMcm2u8s@SH2{-ow-?5`Gz`ciB-?6ygynLxWIrtaov5j13`E%KmrEV-oelx?X3ke!+LtXDKBEO)C2%g3%} zY4xa-HH}grvXvbUTfTq@+zWq;VRTmVT1cRiYB(OvbkJj5QAbA+p5e?U*=4=_or69dHDA5H^@ zDEURYHN}U&X#-B{5K;BZM3U76L`wld!jpg@-kD|o!vW$JSP8q{1P&6O21D3&{IniV zJ_p(E$>DtGA9_55p)W46ZV%DH;Mo#^o45E-QKSTx&rm!p@s+V?9H$~yb48j5RE)XEcBYG< za!;mRdAD@d)8BHzx?zVY&Hz#t0Hati2ZtTonL=_|YG#@LAF%6=dqZZKiSB#RH&<^M z(tK9erYwh^OXUraQFrd&CkM~Bp4Sfa;(@~Am{Xf?)!QG-L)cNJ<};(epWgJjZJg&d z5d^m7aW>L^MDW${>*pn^&AQCizH%F}-Y}ns1IlZZoTZZkEunJ2W!g72>Di;U=*zuB zgH>+s$;&p?R^)-EE5z&xYkMpqRkAqXi?ipZ(Rhe!DiK3_Nr5TQ2Nu@wGkLlpGA5??{=TghSDb)FQUMCTt-QRNj?eqwI_=!i}| zdmw~ypvX4mVIOjAEb-kB&@ZMFYoKB@aIG>(^Ty%Q(nC|x#NmR9jQ&0?L&w025#4X? ze*0N*S>;qjV(rEQZuF;`XZi%U^MkJ)U|g~c{{{1D+n!z?B}-J7Faw4M;4Nv3uu?VQ zAc#w-AdPai0A&WdTRM>^FnGVThH<-E*ioW9>19?k!hk||u>Zlr!TMv#Y6iSU{%L#a zB>k}QD%kpOyAuk&!cD{jBtK7?T+~%v+iaFCcGp;VysSAFec|);^HZChaCYl%qcWYV z^T9Cm-xG;HhhLHWT|p3YIBEJerYP=(>}Hv@&+{&kprg<=4xp#x4zX7f7rx{IG^9EAwQ^Gq)K2|lVH%=aI+^upsJqqgS zrEa1B3PjqLiz$NWuHuQ0eEyiAC_50B(x1G(wpdj-$p6+i&EdR=t`HAcHT>?#T~B8^ z0YT{T6AUjEFY*w)a{-GeA$aB*xL`t%h)6hVC9J@Wx@j9VU#LF@0B*-{2FHl5uL#)G&PB{vMD)<&8D_qa7EOn_llHz7fkt3AlAp z0aWjPtq;4Jwi9(E(YDym+KjQQF=jg08#8Z{-{ma^4liwFeQ;$M_ZJV=X7|w0TvRpZ z&GDsElC%q@HMVB_8PIBejvek(u1k8G41gflQK$RSxB(}A6Hf!vPPmNHNi9aj_Mcxj z8AFNM9(HfWLjlqC^X{$t&~zU>y^@xIbO!z$8-JV)LiZRB>Td!q9sx~gE_n5>MhI5p;Lh1xUhUs<;SgD(>oQf69<@ImiQ&6TS%u(#WwwLKz!3x zk<=RWcJ}ZEV4K`gPY5Xo8O3JrgSv(VaCXN1@8tp3CqlO+UkPML!1w0EuIoavZr4mE zVTm!>5nb^YOq;yHVZM-G@i~hir15pPN-^_K$hmJ1IR39o5?L1A|tu1HX;2gt|T)#Xgk>G#-f?Pbw1w;hZI-(jtzXe@TW zfg)Z%%*AYJkOL)>1u=%n1f=3UA;=+3#dpQ1&xv1+iG1L>ge3Fzt^jU2P2imn$ z+48b7=T0Rle*HO-Y2X<-(syBZ<2w$29}xhkJ@H&mdw3eb97FM*?y$E#j2Aziv#0b( zVL@w&c!VN%y!^~2qfN5}=6K^W)Y3j{7l{z-CMs=OD^HT$Q{+qSRcN9em#ONb#SdtW zwh2?z3~|tC-lll2b=}fjtA^!#7lRo$b&v=@{DcqTul;K|X;04OCzeL8>r0e7c;nG{ z;*Y1{g-|KTR!S5k7h24R-ZFiBwPA^g*(+shM_Bj~>i)@%*BE-u^uq;9U$Ii6kH&LL z%T1`EBI|n{o>6n0cW}#x3d=L{W#<~FC8iLss@H31$h`cd$FAu}oN>F#$T1n}{!rxK zFhv$~l}VUDo(BP*MQORci2c(qZjt9H;(u}(Bk|pAzTwQV7CCY zT<*~CO<2tc zkldO~SLt1*^fS%^qD=#bm-v$sr6@0R4(PSD@G+FaDsXz%LQ%~ie3hot1UJXB{ID}ttCE54MtH#e3S{2k; zw^ol?FD2Dnyarm6S*5J$n2gp|ZbFyxPW1Qjwf8m);?Ki^98xMZi)9^W1-l@wk>%FzusEr?6tH>4@KI!0HPy3o{7?km0V?`y)1;z{8ro zB;V*;@=(2P(KuSK)l;Scz#z^3pbDk399Be2mBogsIUfdRq6r{0(9EcMtBkZKTwcI{sOSf|Ip zMfsP%-6NHwk9h8->m_@)mgP((yNk$l;`2Z3arBSnB^3J@+~xpqzTi`PTtGgx1CWz$ z`iOw}c{%)$@8GA0DtK<;wS+w(fXaGSeg_XHNDrLW4kRsS%-tMVr1*IW^C5=^IR!;; zq&HG5`{Plw15WmDj;_E4&D>hu5Ap?LFe7G3yxCoKNK5?Mbhm zmT&mcrsJ4z;(sk0_X=b}dM3I$69a!L;q|2wW9w-hBXS^cI=9BV@ve9mpf7=tuzxNmE3 zo92dI+Jhe7UTLpYQ@x4E=jpSV5fIoVR}kIP9I|=UBAVc9BpkwsE@)-Xu2mo5Pj~n(*{?Un1wpTtgXk8{NkhQh)PDXKX%MW_d+{09-u;LaJznc&0PRDR zM!2EXXOKIU_ACrl9Q!h%_`?u#6VSe;&+H({l72e`x;Q840f9nPZ@i-LVA${LGS~W@ z3T=b7CNFU`fZ$d}RI1_RM#p8*G6MSB-BAFR`IEWLUKSowlp;kSPZcvh35*y3y9;LP z!Y?{uczXwHR?6#mXJ6jh4xtq`4l@TRT1$WOI;&(V1zv05iQlBV?uc?BH%GK7UGa?#qk44LN0E{ z`s7WU7-oXOJ%7uKp&|bZd{O9vc1bpug3Q@e_A#5*yM`ik;Z89GsL8)VF2HJ6|G9qd z2d}2Rae9bNFj!UFo=>YG#4FC-+7WaMOXYTOXSpX=)JL`aV$>q5l(|;)!>=Z1shW}R zn-N`6Y4$61fIh}(R&FIBN18(M7{uo9B*c&?;1j#QT@3dAQiEuke{+V#4+3PtWmE!a zMOZnmJIcxpt=uzIRv+Lh4yr>RSDV%k`Dp+OZ_81Tyex2j^i?@q>Arx-i(WlVU8si% zrUL1;z!9whp&ZjBgB&LJGab2la#iqelS9tN^P=gkf-=!sTQV~q!otAu{t{glZLpEP z%sIh8@u#sD!7yKdeo?aGO8}bG?@h3ZXPLkk2x#Ck9^C=XIh|mKivyk#*PG}{{OYRd ztv~rx2yQmiTA~ldMQ3O`m@_7xtbCvA_x_ptE_R4-R+*xoF_o2&Dt`_aI2Bha>PGAa z)Xg0xHzRrQrXa>lLAwky-T)^! zcoSOJkJ&YvlQig_v=hxr=g?|U5cGVU=TrSr@CG)!GEN;YUM86N0gCK3f4Hv={n^N6 z`UvTQ9|}SD-PszIEB+QWv1^Ejh`?F8 z-;NU6aGWx?Af#)^9vM0NaOV$pbm_y5Tt!D+!k4E^l9WLDKuTi)JPGg^>C zb)Q`aB1G`y3=|+)#m4u<*?>*k0aW#+H~Z%yHm%VavWv1KI8bf@9dkCYl`%sqE4zHR zQalU1QbH}v;73?Nenz&o0~LKB_T(U$BcL%<05xqgDDC~PdK{`ezWD4~T?=ZGy9FE5 z{gYC^tCAdlUZr>SyH?(%DRi!<>rOsBfD8Orta?fJ}K@H8bK7Censh}E7 zbrf{Z=c2EDx?=-zU!7T1jN6OpR}tAquJR%v+nM&~?Ll|m;$x#tfK89~yZnyA2}-CC z*qlK0*d|-GTp~ureVs<3hL2N{i7S1-6Vd-q`jhg;g(=Te*PxMP;EMAto(`NVzO17e z_m>lVFqV|0spS1sfU9es1`p`MC}GQiaNH{;6X2(p8%4+6n+J3%(o@I1MykpJZip@Z^MVtx@CD&Fc;CcVm3>a0yhofeTg+ zS9Kn@&<=r_xjVCEr8g2r{3z~B4`7FLBA)OXg}VvTA%&9TKw%r4ZVo~%h&It(zDs(4 zGavxzPP-@Sy>cTdiX`xwVi>NJlokUO&Q_ZqM&Dfmb}O`N>CPFYuHNJo!oNI3PP?X} z{F+!Pd)6VPBP7-mG^G#GUPvHPv5?t{`={yXQULkw`23UgY>`Y^tk^Wl9+7mUKvift zhE{oIm<1XTn|UB5yM;OZ&cC`B+j)G7P^|E~LRu%i%Vw|;sG;xS%I`S595@X8Lv)@P za<`u$@?=9eX^UD9YD~7H#k$x%P3z53k*8v%c~J9TqxKn&`Nb|po+Xbv2z+tGEpSaY z=`kAlOGlcb%8hD@-6QP|)S033Sd-s?t%>ZaaamV%q(Ip(XbjL4tTU9ghJi#--!j@C z_xT`Zc><2WbKP(0W!ALg_v$@pMDowuA%noeG*C?V7E3ih zIhs!5)MTrjKNWYK^BGjspQobcmdVqf$%#fuBYv@7uER?pg)Zx*xxa;*TP_{~#xtFM zK%Lwgq2HsWMJ@)F@tMs*GNN%Wz@T|mg%%`GZMgCSkp6cnU(ig28wCe+Ng`!<^_9}Q z=q&{4O!sRD_`(}6HLksZ^XoEh3i(5&kZ0J9X@E-P!*3B|Kb~CTO}!PT%Y{u?Ejk+0 z*=i!5#Xhyxnh%3F=_DlR8;3T5oxl8kf{_lW-hs|Gk#O1t%|7P>_7AXww&yCG#9rPZ zgbS@s{LQJ?YB+)?yt+^kB8!0m6!$YcyQT~hO$6r{eOzh-a*Vkdabh%0a_cp3S9(bb z&*5C1oV4+!GLXhY;{184ecif zc{n=|awgO=TyD!i+R{m6a;1gAP`Bn|xjsY8kY&YyoALByT7)r}A{fMyg5!gD8UtRT@Z*zbLsbVr1eLwjY+~13TLl}> zTgJu}QnWQhgzxcjA`>C@)iU;Vv{BZ{Uz=wzvTk@zcywthq`X2gkY(j^${nK1hTDv+ z14AE3OIpJDbqOkB#}jtWrKnxpu#I6TdRsc5du*+HMbVZ<`JYUA(Ba#M0$}Yub)TGy z6kV@5-uT*%-3iw)Z9c4L%%!43q2jirU2|F+O((Eex_4}4=90e`!X)WrsEgHrR*JZj zTG~|ilCcgjr@07Z4t^(Lfu;M%hvR^FpcQWNCNNeJ{d8WTC>BuVDVZ(Z_-$hguJ6%e z^Tq^Tp`BJ*O+n+H=o5L=3b0v##t}s9)h{h3dR9G6;iU6z0UZ4Y3toB&he)*Z2lX%g z`E#n|ITkU|dH?66XY~zH$bc4*A-ikeJ`sG`w=8;y zNyVd{w(+*(zG1SLE=a{Iz7}1x4jg1fm4^k*0BOPc{$g>O*VG|5Xbqu_F$&Z_k=MQqC?^{}V0nG?jo8f3iL@kd*~PhP zKK?>S{0|1_>r=TiqUH3M3~IFAGg3sg_2=~iouF&gUtj4qL8r8a_M_w;&a#JFdaoK` z*YVQ;p>jl;+BS3PA@x+~7dF_36*`CVdYbzgS6KE>=Be6USmJbquH2O86CdLwtslD2 zq@O-{v=RgUa4Ce_Gkqnrn123?F<` z?yFAL+qHTLIdDP3u*f}|3G{gUCrv8JFeBdh!j`+Xij79Q6FHzkWE|So93pkt_zDu1gw7Bf^u z>4GoC|5A;lALiWdi6B+bOL)se1gvV|U{Nnc3mY_#{P}s%d=}unDzWVg&;R6Ddw`V& zGOLa|r~84m%iM|eu=?#SOUbvlh0NP&QgEkefmYq82Gil@zGAt(1D=jZ-n~{ixe?)c z-4{(9ZT_OvTC}Ea%Y$_wng0q0aMd6IIGds9*b7PkH0|cl(gfi8^fuRG>2rhG5aT;QTW!yA0Vj+UzXoXJ=#g3c9ef$Kme<|oeuu` zah*+Q0=6MC=9C_U((%--c@;?2=s&@@y;p}e`0+Am_3CbuPrxy-EUU@2pzK_oSfn6G z!k!0}rvV(Iu-C0$0lofruwlkHAlUJGc0cqM*gh~#?!W_8ZEdKr`2A25BB>0>5$nEW zum6DB#=FBAFE7oo^aU&eTn92eQ_E<7Qu$@r-3nK%KBUY%^Ob!kSx;fZ(7=^5 zK*QIKjm8>UNG2*ElfD<09s9D>3sgrK=w@W#YO zt+e|G{o1Vb5MQp)ZigljJ)_>RhHO;dU%DFYyzWL!`TZ21Gj_DrthI`{EPo?9+Y|Ja zL>wy7XTLVOgyCOwubz%xN3bwoI^rH$1zfC+$w%?1(qb>ZZ{=R%E;XM^I-3lrM9(ad zW$A~sCB~NyK8o1Q@q=Q4+`pY^|JrC+0(aj&gu@YN8B!fqSO(T)&!G=ln*f6QKnCLJ zj((Zw#RXper`!Svz|n4C)?CJUCiH3vho%Lk+IwL5Iz|n=wB8$!5so!3DDvAsEZ%y0 zoYvY9A-ikNB3zx_{``4)1*)R@J{&@C@nsQc9BD0!@}<05GYlLA-xeGm=K?bX=yIdWy=1_-wFA~cw|z~t5iy%ZOO5sD z*NjKNKcWw&tPpE`uVCX30mbo6r1__y3|kWKQ@s+1pv(IEP%mQvaUd%k`3-o|W_-3= z+tFqv(knpccn`=3 zhxcj#`gaGEaRYv4VR~&vOy(Yu(0LM$d4B{wO2HplC3g<=^r&^zVnr?x;2FhM3N;iK z{KqW7RHWB$J`IBqO;{KXfN8_;4*Pwpx(rZ(&J~7{3pi++`UJT4*Q)%MitGr?LT?9w z!>Y)7={f+KIunQvB+>XUa7g{XfJ6Tb=*rK9rz@Wzi*oGbc3J6y zqJx>9kXZ(mHe%`tNEh7`z{nCM;Nss6`ndB_om8(fCvd;QP;sj*bM( zmKk(YE%f(&Jv*gKH~m$>A2$JG02|NQNSZ;3wTJoPOBX^{uI*nJ0=<+o{Kv!@^i>uf z1z_-sO53JfAL;{V+5nk&M^902g$+=w>1;53@UtR_6;fJKenFeHcm%Q{$jB^1M*z*o zE8T5s7ZbXMD9a6c3qp|YxX{a{RqjZGRrS`gx2{Yxa@;*YwMyR`ako8Gb9S)gfVYoo ztNYA5!3@yEuH#9#W3G3U*BgS>n8>r{ZuMTd(Dg(n=!Ws%^I&3a7+Y~sc%3fTcxY%P zv;Ef>8E?e{tL# z%YEQ`4yK_1Bp3E)plZA0F^p^f&GofvGhFrCkPH0{l-veeQx1s|Qa@fC45)ZS_!kt8e4PF+jvU1z`^)=-2_S=>MEezM*8;db2(Rlca;qTSzpC8 z;KgnIfszvHX>$L>YX!#Pj~mNV%9c>zF6;sb^RJ8div}5gvux+e#S_w^=>K``b;_%M znrudI|0sw)%CFM`BA#`AK-#n*wL}Lnz?scx?<#bl&#@s{dHHkyg9CH`x$uQ|6>*0E zwNKxBVsV#)v7aEmg>81nq`>3X9sB-n3NRu~lqPR{tETkKF#m6a^4}kiDinVL5z>Ij zGyz7Q7m?}8lb04j23U~K%Bh-;h1yr-wktZ$cfaR-+W=>s_AmojznLGF2AutU8jzok z2STU2?x+ny`&?F%%Vsp_$CSNQ451tUKGZ8f`}N%^9Y%gBKoE4aF+g(1(XfL8IS@e< zTJ}70fw&CFbo=w7xmgu>c*e1xlB*Vh{piYE2P28Uy14E;u|B77exR%_<;?ZcQ0sOW(IzsEVCZ+#B+jM(~5Hn>=_&RFw9Vl_dD3K@&{ z$~$d>9!ZhO0KTyM*bI=K^N&o$2$Vw&xtoh1jb|)xsi!==_MdqJ*~xz%cb`EC?Gqj( zfP%&!^5O4TNo%3?z2af(3nDip!mpeBmD+mbba#uwtg;~%dniV810NwfPqPTGN~d;`X1b;jhGlP@B;3tzou9cb0J9m!NzUhAR9;s{ZFYz~9$LukdL9* z(d)m7mT^VLQhjT9^-u;SXZT-6E!h8GoiepwnDrDCYJ0 zK>}=ZJj`1A?<%sv82ta>$`wO&@ao@3qAz$z@(sX+*<u#PtlA3f>r<1fv;!IlR5k`x%KWqp08 z<$vok)@q*z+4otYZ}mixfQK*r|JZu(fTqs(eHam|xUf(~lu;+j6i_yaVigqy6d7Sk zSuz3&0y09-BG|G-_6Ttxd&o)*D+o&15!ne4kP%4;DJ`oO9pL zJ+A9|?q^0-7^rm{qyIkNNQ}t}v?B1zsg=LSZ2sp>M0$LLH0NZ6vrnUt;$-tGdB=^|I)YS@Ozbxmi?V=d zzJ?`U0;o+a6?kn+^8LRF(*L|qCi(wQNuFIH!LytH4^c-6bZbk_jF6PX;=voMrFv13 zz-GJbl}!M*zPL|V$;!I?U+(nUSnQzC;m&UK@N4}So!7#D4wHR^TNSwzDYSvzzp4q5 zM3w(zWwqRV4JdJn0c@vsE*5MG(6hrk-fX|a|8pN(X#Tw~mjOyuSal^D{`b`_jar(T z&x4mI5pXqol!~fz^WTTT{32Aju<=Hx!6^O&egJ^cfX#dljc6OVkcyg52FjR*!@Y7 zsp;y@c?Yt0#0aLggEQg$kIvjn>RX zn48^LZnroQN@_OpAMrbE_a{%$$7E0dJnPz<{M%5!6YYwqq-Gn={-mRX!<7o5v&pBs zi-u)rf3ccZo~X~xShOonD!YCzne1#Yz#wVz-NEf|2>iSLV~T{azR^oWJFg{ID@Lvt ztMTrnV*6{WiTk--wwD#UK>beN?@DB<32fDousl{sRZV zuVPW^fOWyY6CZLHkbn|~iN+#GIekdMCX)Qk5CIK;0fWX(>bRfhi;fW-CYsZ}+Mj#T zM98?2qkV2NPWv~Avb9iO>CO4w;dkWc5zUHl=3=MoZT4WvQNC#hd|iX7J~KHs z7HX06WmM3&%r%>=K-@n~LsD$b&IL<5J#^{Dn<{{L3+Cp!eSnK3Ol{sZ8Pu8x?e|7% zs@;q<1SXCT$^p-^0m1+A5JUpW|Jmv*BCGxLXWmN**ePBcn0xWta)76cuy@?^-#f2% zVg2W5rwK*lW>=!n<5b%kkDcd+z*!*!pFUZz0q@8_#-1t_>xc8{NdMGLG@OI>8mo5K zJ7d^N@l@_ozp#E-$1m3+GI}0I{`n3FF-iu2HlOGk6N~W1M4ZE^BkTR`r#OK~j>BGm z^`NsO_M$7LUu=PCyP)=C?1MOM?g+51B}yt~OGg#iHti=8dvDK3;xw6mx{ft&v^e~E ze)Hi6y?sHcbl(#C(Z1K#oDa4!x)Q(NkM_7}9nHXv;xL;?0Ud7kk=vO+NKfoJ*IN<-< zuN;7ICjqWMkF0`=zn6HExbNjmx#$w{W-H(wMxyKXX$sSnhG5z?{{qPu^3Zrx-^nSX zc3n|k_vq!URefJlGpzOC